foreman_remote_execution 7.2.1 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +2 -2
  3. data/app/controllers/ui_job_wizard_controller.rb +15 -0
  4. data/app/helpers/remote_execution_helper.rb +1 -1
  5. data/app/models/job_invocation.rb +2 -4
  6. data/app/models/job_invocation_composer.rb +5 -2
  7. data/app/views/templates/script/package_action.erb +8 -3
  8. data/config/routes.rb +3 -1
  9. data/lib/foreman_remote_execution/engine.rb +5 -5
  10. data/lib/foreman_remote_execution/version.rb +1 -1
  11. data/test/functional/api/v2/job_invocations_controller_test.rb +8 -0
  12. data/test/helpers/remote_execution_helper_test.rb +4 -0
  13. data/test/unit/job_invocation_report_template_test.rb +1 -1
  14. data/test/unit/job_invocation_test.rb +1 -2
  15. data/webpack/JobWizard/JobWizard.js +154 -20
  16. data/webpack/JobWizard/JobWizard.scss +43 -1
  17. data/webpack/JobWizard/JobWizardConstants.js +11 -1
  18. data/webpack/JobWizard/JobWizardPageRerun.js +112 -0
  19. data/webpack/JobWizard/__tests__/JobWizardPageRerun.test.js +79 -0
  20. data/webpack/JobWizard/__tests__/fixtures.js +73 -0
  21. data/webpack/JobWizard/__tests__/integration.test.js +17 -3
  22. data/webpack/JobWizard/autofill.js +8 -1
  23. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +36 -17
  24. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -3
  25. data/webpack/JobWizard/steps/ReviewDetails/index.js +1 -3
  26. data/webpack/JobWizard/steps/Schedule/PurposeField.js +1 -3
  27. data/webpack/JobWizard/steps/Schedule/QueryType.js +33 -40
  28. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +55 -16
  29. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +19 -56
  30. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +1 -1
  31. data/webpack/JobWizard/steps/Schedule/ScheduleFuture.js +126 -0
  32. data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +287 -0
  33. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +88 -20
  34. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +206 -186
  35. data/webpack/JobWizard/steps/form/DateTimePicker.js +23 -6
  36. data/webpack/JobWizard/steps/form/Formatter.js +7 -8
  37. data/webpack/JobWizard/submit.js +8 -3
  38. data/webpack/Routes/routes.js +8 -2
  39. data/webpack/__mocks__/foremanReact/common/hooks/API/APIHooks.js +1 -0
  40. data/webpack/react_app/components/HostKebab/KebabItems.js +0 -1
  41. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +0 -5
  42. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +59 -51
  43. data/webpack/react_app/extend/Fills.js +4 -4
  44. metadata +8 -6
  45. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +0 -106
  46. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +0 -32
  47. data/webpack/JobWizard/steps/Schedule/index.js +0 -178
@@ -5,50 +5,89 @@ import {
5
5
  Select,
6
6
  SelectOption,
7
7
  SelectVariant,
8
+ Alert,
9
+ AlertActionCloseButton,
10
+ ValidatedOptions,
8
11
  } from '@patternfly/react-core';
9
- import { range } from 'lodash';
10
12
  import { translate as __ } from 'foremanReact/common/I18n';
13
+ import { helpLabel } from '../form/FormHelpers';
14
+
15
+ export const RepeatHour = ({ repeatData, setRepeatData }) => {
16
+ const isValidMinute = newMinute =>
17
+ Number.isInteger(parseInt(newMinute, 10)) &&
18
+ newMinute >= 0 &&
19
+ newMinute < 60;
11
20
 
12
- export const RepeatHour = ({ repeatData, setRepeatData, setValid }) => {
13
21
  const { minute } = repeatData;
14
22
  useEffect(() => {
15
- if (minute) {
16
- setValid(true);
17
- } else {
18
- setValid(false);
23
+ if (!isValidMinute(minute)) {
24
+ setRepeatData({ minute: 0 });
19
25
  }
20
- return () => setValid(true);
21
- }, [setValid, minute]);
26
+ }, [minute, setRepeatData]);
22
27
  const [minuteOpen, setMinuteOpen] = useState(false);
28
+ const [options, setOptions] = useState([0, 15, 30, 45]);
29
+ const [isAlertOpen, setIsAlertOpen] = useState(false);
23
30
  return (
24
- <FormGroup label={__('At minute')} isRequired>
31
+ <FormGroup
32
+ label={__('At minute')}
33
+ labelIcon={helpLabel(<div>{__('range: 0-59')}</div>)}
34
+ isRequired
35
+ >
25
36
  <Select
26
37
  id="repeat-on-hourly"
27
38
  variant={SelectVariant.typeahead}
28
39
  typeAheadAriaLabel="repeat-at-minute-typeahead"
29
40
  onSelect={(event, selection) => {
30
- setRepeatData({ minute: selection });
41
+ setRepeatData({ minute: parseInt(selection, 10) });
31
42
  setMinuteOpen(false);
32
43
  }}
33
- selections={minute || ''}
44
+ selections={`${minute}` || ''}
34
45
  onToggle={toggle => {
35
46
  setMinuteOpen(toggle);
36
47
  }}
37
48
  isOpen={minuteOpen}
38
- width={85}
49
+ width={125}
39
50
  menuAppendTo={() => document.querySelector('.pf-c-form.schedule-tab')}
40
51
  toggleAriaLabel="select minute toggle"
41
- validated={minute ? 'success' : 'error'}
52
+ validated={
53
+ isValidMinute(minute)
54
+ ? ValidatedOptions.noval
55
+ : ValidatedOptions.error
56
+ }
57
+ onCreateOption={newValue => {
58
+ if (isValidMinute(newValue)) {
59
+ setOptions(prev => [...prev, parseInt(newValue, 10)].sort());
60
+ setRepeatData({ minute: parseInt(newValue, 10) });
61
+ setIsAlertOpen(false);
62
+ } else {
63
+ setIsAlertOpen(true);
64
+ }
65
+ }}
66
+ isCreatable
67
+ createText={__('Create')}
42
68
  >
43
- {range(60).map(minuteNumber => (
44
- <SelectOption key={minuteNumber} value={`${minuteNumber}`} />
69
+ {options.map(minuteNumber => (
70
+ <SelectOption
71
+ key={minuteNumber}
72
+ value={`${minuteNumber}`}
73
+ onClick={() => setIsAlertOpen(false)}
74
+ />
45
75
  ))}
46
76
  </Select>
77
+ {isAlertOpen && (
78
+ <Alert
79
+ variant="danger"
80
+ isInline
81
+ title={__('Minute can only be a number between 0-59')}
82
+ actionClose={
83
+ <AlertActionCloseButton onClose={() => setIsAlertOpen(false)} />
84
+ }
85
+ />
86
+ )}
47
87
  </FormGroup>
48
88
  );
49
89
  };
50
90
  RepeatHour.propTypes = {
51
91
  repeatData: PropTypes.object.isRequired,
52
92
  setRepeatData: PropTypes.func.isRequired,
53
- setValid: PropTypes.func.isRequired,
54
93
  };
@@ -1,6 +1,6 @@
1
- import React, { useState } from 'react';
1
+ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { TextInput, Grid, GridItem, FormGroup } from '@patternfly/react-core';
3
+ import { FormGroup } from '@patternfly/react-core';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
5
  import { SelectField } from '../form/SelectField';
6
6
  import { repeatTypes } from '../../JobWizardConstants';
@@ -13,18 +13,10 @@ import { RepeatWeek } from './RepeatWeek';
13
13
  export const RepeatOn = ({
14
14
  repeatType,
15
15
  setRepeatType,
16
- repeatAmount,
17
- setRepeatAmount,
18
16
  repeatData,
19
17
  setRepeatData,
20
18
  setValid,
21
19
  }) => {
22
- const [repeatValidated, setRepeatValidated] = useState('default');
23
- const handleRepeatInputChange = newValue => {
24
- setRepeatValidated(!newValue || newValue >= 1 ? 'default' : 'error');
25
- setRepeatAmount(newValue);
26
- };
27
-
28
20
  const getRepeatComponent = () => {
29
21
  switch (repeatType) {
30
22
  case repeatTypes.cronline:
@@ -61,64 +53,35 @@ export const RepeatOn = ({
61
53
  );
62
54
  case repeatTypes.hourly:
63
55
  return (
64
- <RepeatHour
65
- repeatData={repeatData}
66
- setRepeatData={setRepeatData}
67
- setValid={setValid}
68
- />
56
+ <RepeatHour repeatData={repeatData} setRepeatData={setRepeatData} />
69
57
  );
70
- case repeatTypes.noRepeat:
71
58
  default:
72
59
  return null;
73
60
  }
74
61
  };
75
62
  return (
76
- <FormGroup label={__('Repeat On')}>
77
- <Grid>
78
- <GridItem span={6}>
79
- <SelectField
80
- isRequired
81
- fieldId="repeat-select"
82
- options={Object.values(repeatTypes)}
83
- setValue={newValue => {
84
- setRepeatType(newValue);
85
- if (newValue === repeatTypes.noRepeat) {
86
- setRepeatValidated('default');
87
- }
88
- }}
89
- value={repeatType}
90
- />
91
- </GridItem>
92
- <GridItem span={1} />
93
- <GridItem span={5}>
94
- <FormGroup
95
- helperTextInvalid={__(
96
- 'Repeat amount can only be a positive number'
97
- )}
98
- validated={repeatValidated}
99
- >
100
- <TextInput
101
- isDisabled={repeatType === repeatTypes.noRepeat}
102
- id="repeat-amount"
103
- value={repeatAmount}
104
- type="text"
105
- onChange={newValue => handleRepeatInputChange(newValue)}
106
- placeholder={__('Repeat N times')}
107
- />
108
- </FormGroup>
109
- </GridItem>
110
- {getRepeatComponent()}
111
- </Grid>
112
- </FormGroup>
63
+ <>
64
+ <FormGroup label={__('Repeats')}>
65
+ <SelectField
66
+ isRequired
67
+ fieldId="repeat-select"
68
+ options={Object.values(repeatTypes).filter(
69
+ type => type !== repeatTypes.noRepeat
70
+ )}
71
+ setValue={newValue => {
72
+ setRepeatType(newValue);
73
+ }}
74
+ value={repeatType}
75
+ />
76
+ </FormGroup>
77
+ {getRepeatComponent()}
78
+ </>
113
79
  );
114
80
  };
115
81
 
116
82
  RepeatOn.propTypes = {
117
83
  repeatType: PropTypes.oneOf(Object.values(repeatTypes)).isRequired,
118
84
  setRepeatType: PropTypes.func.isRequired,
119
- repeatAmount: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
120
- .isRequired,
121
- setRepeatAmount: PropTypes.func.isRequired,
122
85
  repeatData: PropTypes.object.isRequired,
123
86
  setRepeatData: PropTypes.func.isRequired,
124
87
  setValid: PropTypes.func.isRequired,
@@ -7,7 +7,7 @@ import { noop } from '../../../helpers';
7
7
 
8
8
  const getWeekDays = () => {
9
9
  const locale = documentLocale().replace(/-/g, '_');
10
- const baseDate = new Date(Date.UTC(2017, 0, 2)); // just a Monday
10
+ const baseDate = new Date(Date.UTC(2017, 0, 1)); // just a Sunday
11
11
  const weekDays = [];
12
12
  for (let i = 0; i < 7; i++) {
13
13
  try {
@@ -0,0 +1,126 @@
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ FormGroup,
5
+ Form,
6
+ Button,
7
+ ValidatedOptions,
8
+ } from '@patternfly/react-core';
9
+ import { translate as __ } from 'foremanReact/common/I18n';
10
+ import { DateTimePicker } from '../form/DateTimePicker';
11
+ import { helpLabel } from '../form/FormHelpers';
12
+ import { SCHEDULE_TYPES } from '../../JobWizardConstants';
13
+ import { WizardTitle } from '../form/WizardTitle';
14
+
15
+ export const ScheduleFuture = ({
16
+ scheduleValue: { startsAt, startsBefore },
17
+ setScheduleValue,
18
+ setValid,
19
+ }) => {
20
+ const [error, setError] = useState(null);
21
+
22
+ const wrappedSetValid = useCallback(setValid, []);
23
+ useEffect(() => {
24
+ if (!startsBefore?.length && !startsAt?.length) {
25
+ wrappedSetValid(false);
26
+ setError(
27
+ __(
28
+ "For Future execution a 'Starts at' date or 'Starts before' date must be selected. Immediate execution can be selected in the previous step."
29
+ )
30
+ );
31
+ } else if (!startsBefore?.length) {
32
+ wrappedSetValid(true);
33
+ setError(null);
34
+ } else if (
35
+ new Date(startsAt).getTime() >= new Date(startsBefore).getTime()
36
+ ) {
37
+ wrappedSetValid(false);
38
+ setError(__("'Starts before' date must be after 'Starts at' date"));
39
+ } else if (new Date().getTime() >= new Date(startsBefore).getTime()) {
40
+ wrappedSetValid(false);
41
+ setError(__("'Starts before' date must in the future"));
42
+ } else {
43
+ wrappedSetValid(true);
44
+ setError(null);
45
+ }
46
+ }, [wrappedSetValid, startsAt, startsBefore]);
47
+
48
+ return (
49
+ <>
50
+ <WizardTitle title={SCHEDULE_TYPES.FUTURE} />
51
+ <Form className="future-schedule-tab">
52
+ <FormGroup label={__('Starts at')} fieldId="start-at-date">
53
+ <DateTimePicker
54
+ ariaLabel="starts at"
55
+ dateTime={startsAt}
56
+ setDateTime={newValue =>
57
+ setScheduleValue(current => ({
58
+ ...current,
59
+ startsAt: newValue,
60
+ }))
61
+ }
62
+ />
63
+ <Button
64
+ variant="link"
65
+ isInline
66
+ className="clear-datetime-button"
67
+ onClick={() =>
68
+ setScheduleValue(current => ({
69
+ ...current,
70
+ startsAt: null,
71
+ }))
72
+ }
73
+ >
74
+ {__('Clear input')}
75
+ </Button>
76
+ </FormGroup>
77
+
78
+ <FormGroup
79
+ label={__('Starts before')}
80
+ fieldId="start-before-date"
81
+ labelIcon={helpLabel(
82
+ __(
83
+ 'Indicates that the action should be cancelled if it cannot be started before this time.'
84
+ ),
85
+ 'start-before-date'
86
+ )}
87
+ validated={error ? ValidatedOptions.error : ValidatedOptions.noval}
88
+ helperTextInvalid={error}
89
+ >
90
+ <DateTimePicker
91
+ ariaLabel="starts before"
92
+ dateTime={startsBefore}
93
+ setDateTime={newValue =>
94
+ setScheduleValue(current => ({
95
+ ...current,
96
+ startsBefore: newValue,
97
+ }))
98
+ }
99
+ />
100
+ <Button
101
+ variant="link"
102
+ isInline
103
+ className="clear-datetime-button"
104
+ onClick={() =>
105
+ setScheduleValue(current => ({
106
+ ...current,
107
+ startsBefore: null,
108
+ }))
109
+ }
110
+ >
111
+ {__('Clear input')}
112
+ </Button>
113
+ </FormGroup>
114
+ </Form>
115
+ </>
116
+ );
117
+ };
118
+
119
+ ScheduleFuture.propTypes = {
120
+ scheduleValue: PropTypes.shape({
121
+ startsAt: PropTypes.string,
122
+ startsBefore: PropTypes.string,
123
+ }).isRequired,
124
+ setScheduleValue: PropTypes.func.isRequired,
125
+ setValid: PropTypes.func.isRequired,
126
+ };
@@ -0,0 +1,287 @@
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ Form,
5
+ FormGroup,
6
+ Radio,
7
+ TextInput,
8
+ ValidatedOptions,
9
+ Divider,
10
+ } from '@patternfly/react-core';
11
+ import { ExclamationCircleIcon } from '@patternfly/react-icons';
12
+ import { translate as __ } from 'foremanReact/common/I18n';
13
+ import { RepeatOn } from './RepeatOn';
14
+ import { SCHEDULE_TYPES } from '../../JobWizardConstants';
15
+ import { PurposeField } from './PurposeField';
16
+ import { DateTimePicker } from '../form/DateTimePicker';
17
+ import { WizardTitle } from '../form/WizardTitle';
18
+
19
+ export const ScheduleRecurring = ({
20
+ scheduleValue,
21
+ setScheduleValue,
22
+ setValid,
23
+ }) => {
24
+ const {
25
+ repeatType,
26
+ repeatAmount,
27
+ repeatData,
28
+ startsAt,
29
+ startsBefore,
30
+ ends,
31
+ isNeverEnds,
32
+ isFuture,
33
+ purpose,
34
+ } = scheduleValue;
35
+ const [validEnd, setValidEnd] = useState(true);
36
+ const [repeatValidated, setRepeatValidated] = useState('default');
37
+ const handleRepeatInputChange = newValue => {
38
+ if (!newValue.length) newValue = 0;
39
+ setRepeatValidated(
40
+ !newValue || parseInt(newValue, 10) >= 1 ? 'default' : 'error'
41
+ );
42
+ setScheduleValue(current => ({
43
+ ...current,
44
+ repeatAmount: newValue,
45
+ }));
46
+ };
47
+ const [repeatValid, setRepeatValid] = useState(true);
48
+
49
+ const wrappedSetValid = useCallback(setValid, []);
50
+ useEffect(() => {
51
+ if (isNeverEnds) setValidEnd(true);
52
+ else if (!ends) setValidEnd(true);
53
+ else if (
54
+ !startsAt.length &&
55
+ new Date().getTime() <= new Date(ends).getTime()
56
+ )
57
+ setValidEnd(true);
58
+ else if (new Date(startsAt).getTime() <= new Date(ends).getTime())
59
+ setValidEnd(true);
60
+ else {
61
+ setValidEnd(false);
62
+ }
63
+
64
+ if (!validEnd || !repeatValid) {
65
+ wrappedSetValid(false);
66
+ } else if (isFuture && startsAt.length) {
67
+ wrappedSetValid(true);
68
+ } else if (!isFuture) {
69
+ wrappedSetValid(true);
70
+ } else {
71
+ wrappedSetValid(false);
72
+ }
73
+ }, [
74
+ wrappedSetValid,
75
+ isNeverEnds,
76
+ startsAt,
77
+ startsBefore,
78
+ isFuture,
79
+ validEnd,
80
+ repeatValid,
81
+ ends,
82
+ ]);
83
+
84
+ return (
85
+ <>
86
+ <WizardTitle title={SCHEDULE_TYPES.RECURRING} />
87
+ <Form className="schedule-tab">
88
+ <FormGroup label={__('Starts')} fieldId="schedule-starts">
89
+ <div className="pf-c-form">
90
+ <FormGroup fieldId="schedule-starts-now">
91
+ <Radio
92
+ isChecked={!isFuture}
93
+ onChange={() =>
94
+ setScheduleValue(current => ({
95
+ ...current,
96
+ startsAt: '',
97
+ startsBefore: '',
98
+ isFuture: false,
99
+ }))
100
+ }
101
+ name="start-now"
102
+ id="start-now"
103
+ label={__('Now')}
104
+ />
105
+ </FormGroup>
106
+ <FormGroup fieldId="start-at-date">
107
+ <Radio
108
+ isChecked={isFuture}
109
+ onChange={() =>
110
+ setScheduleValue(current => ({
111
+ ...current,
112
+ startsAt: new Date().toISOString(),
113
+ isFuture: true,
114
+ }))
115
+ }
116
+ name="start-at"
117
+ id="start-at"
118
+ className="schedule-radio"
119
+ label={
120
+ <div className="scheudle-radio-wrapper">
121
+ <div className="schedule-radio-title">{__('At')}</div>
122
+ <DateTimePicker
123
+ ariaLabel="starts at"
124
+ dateTime={startsAt}
125
+ setDateTime={newValue =>
126
+ setScheduleValue(current => ({
127
+ ...current,
128
+ startsAt: newValue,
129
+ }))
130
+ }
131
+ isDisabled={!isFuture}
132
+ />
133
+ </div>
134
+ }
135
+ />
136
+ </FormGroup>
137
+ </div>
138
+ </FormGroup>
139
+
140
+ <Divider component="div" />
141
+ <RepeatOn
142
+ repeatType={repeatType}
143
+ repeatData={repeatData}
144
+ setRepeatType={newValue => {
145
+ setScheduleValue(current => ({
146
+ ...current,
147
+ repeatType: newValue,
148
+ startsBefore: '',
149
+ }));
150
+ }}
151
+ setRepeatData={newValue => {
152
+ setScheduleValue(current => ({
153
+ ...current,
154
+ repeatData: newValue,
155
+ }));
156
+ }}
157
+ setValid={setRepeatValid}
158
+ />
159
+ <Divider component="div" />
160
+ <FormGroup label={__('Ends')} fieldId="schedule-ends">
161
+ <div className="pf-c-form">
162
+ <FormGroup fieldId="schedule-ends-never">
163
+ <Radio
164
+ isChecked={isNeverEnds}
165
+ onChange={() =>
166
+ setScheduleValue(current => ({
167
+ ...current,
168
+ isNeverEnds: true,
169
+ ends: null,
170
+ repeatAmount: null,
171
+ }))
172
+ }
173
+ name="never-ends"
174
+ id="never-ends"
175
+ label={__('Never')}
176
+ />
177
+ </FormGroup>
178
+ <FormGroup
179
+ fieldId="ends-on-date"
180
+ validated={
181
+ validEnd ? ValidatedOptions.noval : ValidatedOptions.error
182
+ }
183
+ helperTextInvalid={__('End time needs to be after start time')}
184
+ helperTextInvalidIcon={<ExclamationCircleIcon />}
185
+ >
186
+ <Radio
187
+ isChecked={!!ends}
188
+ onChange={() =>
189
+ setScheduleValue(current => ({
190
+ ...current,
191
+ ends: new Date().toISOString(),
192
+ isNeverEnds: false,
193
+ repeatAmount: null,
194
+ }))
195
+ }
196
+ name="ends-on"
197
+ id="ends-on"
198
+ className="schedule-radio"
199
+ label={
200
+ <div className="scheudle-radio-wrapper">
201
+ <div className="schedule-radio-title">{__('On')}</div>
202
+ <DateTimePicker
203
+ ariaLabel="ends on"
204
+ dateTime={ends}
205
+ isDisabled={!ends}
206
+ setDateTime={newValue => {
207
+ setScheduleValue(current => ({
208
+ ...current,
209
+ ends: newValue,
210
+ }));
211
+ }}
212
+ />
213
+ </div>
214
+ }
215
+ />
216
+ </FormGroup>
217
+ <FormGroup fieldId="ends-after">
218
+ <Radio
219
+ isChecked={repeatAmount === 0 || !!repeatAmount}
220
+ onChange={() =>
221
+ setScheduleValue(current => ({
222
+ ...current,
223
+ ends: null,
224
+ isNeverEnds: false,
225
+ repeatAmount: 1,
226
+ }))
227
+ }
228
+ name="ends-after"
229
+ id="ends-after"
230
+ className="schedule-radio"
231
+ label={
232
+ <div className="scheudle-radio-wrapper">
233
+ <div className="schedule-radio-title">{__('After')}</div>
234
+ <FormGroup
235
+ helperTextInvalid={__(
236
+ 'Repeat amount can only be a positive number'
237
+ )}
238
+ validated={repeatValidated}
239
+ className="schedule-radio-repeat-text"
240
+ >
241
+ <TextInput
242
+ id="repeat-amount"
243
+ value={repeatAmount || ''}
244
+ type="number"
245
+ onChange={handleRepeatInputChange}
246
+ isDisabled={!(repeatAmount === 0 || !!repeatAmount)}
247
+ />
248
+ </FormGroup>
249
+ <div className="schedule-radio-occurences">
250
+ {__('occurences')}
251
+ </div>
252
+ </div>
253
+ }
254
+ />
255
+ </FormGroup>
256
+ </div>
257
+ </FormGroup>
258
+ <Divider component="div" />
259
+ <PurposeField
260
+ purpose={purpose}
261
+ setPurpose={newValue => {
262
+ setScheduleValue(current => ({
263
+ ...current,
264
+ purpose: newValue,
265
+ }));
266
+ }}
267
+ />
268
+ </Form>
269
+ </>
270
+ );
271
+ };
272
+
273
+ ScheduleRecurring.propTypes = {
274
+ scheduleValue: PropTypes.shape({
275
+ repeatType: PropTypes.string.isRequired,
276
+ repeatAmount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
277
+ repeatData: PropTypes.object,
278
+ startsAt: PropTypes.string,
279
+ startsBefore: PropTypes.string,
280
+ ends: PropTypes.string,
281
+ isFuture: PropTypes.bool,
282
+ isNeverEnds: PropTypes.bool,
283
+ purpose: PropTypes.string,
284
+ }).isRequired,
285
+ setScheduleValue: PropTypes.func.isRequired,
286
+ setValid: PropTypes.func.isRequired,
287
+ };