Skip to content

Commit

Permalink
Fixes #32045 - Add start-end date pickers to job wizard
Browse files Browse the repository at this point in the history
  • Loading branch information
MariaAga committed Jul 20, 2021
1 parent c7f7dbb commit 9fc3597
Show file tree
Hide file tree
Showing 9 changed files with 414 additions and 70 deletions.
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@
},
"devDependencies": {
"@babel/core": "^7.7.0",
"@theforeman/builder": "^4.14.0",
"@theforeman/eslint-plugin-foreman": "^4.14.0",
"@theforeman/stories": "^4.14.0",
"@theforeman/test": "^4.14.0",
"@theforeman/vendor-dev": "^4.14.0",
"@theforeman/builder": "^8.7.0",
"@theforeman/eslint-plugin-foreman": "^8.7.0",
"@theforeman/stories": "^8.7.0",
"@theforeman/test": "^8.7.0",
"@theforeman/vendor-dev": "^8.7.0",
"babel-eslint": "^10.0.0",
"eslint": "^6.8.0",
"prettier": "^1.19.1",
"@patternfly/react-catalog-view-extension": "^4.8.126",
"redux-mock-store": "^1.2.2"
},
"peerDependencies": {
"@theforeman/vendor": "^8.3.0"
"@theforeman/vendor": "^8.7.0"
}
}
18 changes: 16 additions & 2 deletions webpack/JobWizard/JobWizard.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { translate as __ } from 'foremanReact/common/I18n';
import history from 'foremanReact/history';
import CategoryAndTemplate from './steps/CategoryAndTemplate/';
import { AdvancedFields } from './steps/AdvancedFields/AdvancedFields';
import { JOB_TEMPLATE } from './JobWizardConstants';
import { JOB_TEMPLATE, repeatTypes } from './JobWizardConstants';
import { selectTemplateError } from './JobWizardSelectors';
import Schedule from './steps/Schedule/';
import './JobWizard.scss';
Expand All @@ -17,6 +17,15 @@ export const JobWizard = () => {
const [advancedValues, setAdvancedValues] = useState({});
const dispatch = useDispatch();

const [scheduleValue, setScheduleValue] = useState({
repeatType: repeatTypes.noRepeat,
repeatAmount: '',
starts: '',
ends: '',
isFuture: false,
isNeverEnds: false,
});

const setDefaults = useCallback(response => {
const responseJob = response.data;
const advancedTemplateValues = {};
Expand Down Expand Up @@ -82,7 +91,12 @@ export const JobWizard = () => {
},
{
name: __('Schedule'),
component: <Schedule />,
component: (
<Schedule
scheduleValue={scheduleValue}
setScheduleValue={setScheduleValue}
/>
),
canJumpTo: isTemplate,
},
{
Expand Down
14 changes: 14 additions & 0 deletions webpack/JobWizard/JobWizard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,18 @@
text-align: start;
}
}

.pf-c-date-picker {
vertical-align: top;
}

.time-picker {
width: 150px;
}

input[type='radio'],
input[type='checkbox'] {
// overwriting bootstrap/_forms.scss margin: 4px 0 0;
margin: 0;
}
}
45 changes: 24 additions & 21 deletions webpack/JobWizard/steps/Schedule/ScheduleType.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import React, { useState } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { FormGroup, Radio } from '@patternfly/react-core';
import { translate as __ } from 'foremanReact/common/I18n';

export const ScheduleType = () => {
const [isFuture, setIsFuture] = useState(false);
return (
<FormGroup label={__('Schedule type')} fieldId="schedule-type">
<Radio
isChecked={!isFuture}
name="schedule-type"
onChange={() => setIsFuture(false)}
id="schedule-type-now"
label={__('Execute now')}
/>
<Radio
isChecked={isFuture}
name="schedule-type"
onChange={() => setIsFuture(true)}
id="schedule-type-future"
label={__('Schedule for future execution')}
/>
</FormGroup>
);
export const ScheduleType = ({ isFuture, setIsFuture }) => (
<FormGroup label={__('Schedule type')} fieldId="schedule-type">
<Radio
isChecked={!isFuture}
name="schedule-type"
onChange={() => setIsFuture(false)}
id="schedule-type-now"
label={__('Execute now')}
/>
<Radio
isChecked={isFuture}
name="schedule-type"
onChange={() => setIsFuture(true)}
id="schedule-type-future"
label={__('Schedule for future execution')}
/>
</FormGroup>
);

ScheduleType.propTypes = {
isFuture: PropTypes.bool.isRequired,
setIsFuture: PropTypes.func.isRequired,
};
57 changes: 36 additions & 21 deletions webpack/JobWizard/steps/Schedule/StartEndDates.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,48 @@
import React, { useState } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { FormGroup, TextInput, Checkbox } from '@patternfly/react-core';
import { FormGroup, Checkbox } from '@patternfly/react-core';
import { translate as __ } from 'foremanReact/common/I18n';
import { DateTimePicker } from '../form/DateTimePicker';

// TODO: change to datepicker
export const StartEndDates = ({ starts, setStarts, ends, setEnds }) => {
const [isNeverEnds, setIsNeverEnds] = useState(false);
export const StartEndDates = ({
starts,
setStarts,
ends,
setEnds,
isNeverEnds,
setIsNeverEnds,
}) => {
const toggleIsNeverEnds = (checked, event) => {
const value = event?.target?.checked;
setIsNeverEnds(value);
setEnds('');
};
const validateEndDate = () => {
if (isNeverEnds) return 'success';
if (!starts || !ends) return 'success';
if (new Date(starts).getTime() <= new Date(ends).getTime())
return 'success';
return 'error';
};
return (
<>
<FormGroup label={__('Starts')} fieldId="start-date">
<TextInput
id="start-date"
value={starts}
type="text"
onChange={newValue => setStarts(newValue)}
placeholder="mm/dd/yy, hh:mm UTC"
/>
<FormGroup
className="start-date"
label={__('Starts')}
fieldId="start-date"
>
<DateTimePicker dateTime={starts} setDateTime={setStarts} />
</FormGroup>
<FormGroup label={__('Ends')} fieldId="end-date">
<TextInput
<FormGroup
className="end-date"
label={__('Ends')}
fieldId="end-date"
helperTextInvalid={__('End time needs to be after start time')}
validated={validateEndDate()}
>
<DateTimePicker
dateTime={ends}
setDateTime={setEnds}
isDisabled={isNeverEnds}
id="end-date"
value={ends}
type="text"
onChange={newValue => setEnds(newValue)}
placeholder="mm/dd/yy, hh:mm UTC"
/>
<Checkbox
label={__('Never ends')}
Expand All @@ -48,4 +61,6 @@ StartEndDates.propTypes = {
setStarts: PropTypes.func.isRequired,
ends: PropTypes.string.isRequired,
setEnds: PropTypes.func.isRequired,
isNeverEnds: PropTypes.bool.isRequired,
setIsNeverEnds: PropTypes.func.isRequired,
};
155 changes: 155 additions & 0 deletions webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import React from 'react';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import { fireEvent, screen, render, act } from '@testing-library/react';
import * as api from 'foremanReact/redux/API';
import { JobWizard } from '../../../JobWizard';
import * as selectors from '../../../JobWizardSelectors';
import { jobTemplate, jobTemplateResponse } from '../../../__tests__/fixtures';

const lodash = require('lodash');

lodash.debounce = fn => fn;
jest.spyOn(api, 'get');
jest.spyOn(selectors, 'selectJobTemplate');
jest.spyOn(selectors, 'selectJobTemplates');
jest.spyOn(selectors, 'selectJobCategories');

const jobCategories = ['Ansible Commands', 'Puppet', 'Services'];

selectors.selectJobCategories.mockImplementation(() => jobCategories);

selectors.selectJobTemplates.mockImplementation(() => [
jobTemplate,
{ ...jobTemplate, id: 2, name: 'template2' },
]);
selectors.selectJobTemplate.mockImplementation(() => jobTemplateResponse);
api.get.mockImplementation(({ handleSuccess, ...action }) => {
if (action.key === 'JOB_CATEGORIES') {
handleSuccess && handleSuccess({ data: { job_categories: jobCategories } });
} else if (action.key === 'JOB_TEMPLATE') {
handleSuccess &&
handleSuccess({
data: jobTemplateResponse,
});
} else if (action.key === 'JOB_TEMPLATES') {
handleSuccess &&
handleSuccess({
data: { results: [jobTemplateResponse.job_template] },
});
}
return { type: 'get', ...action };
});

const mockStore = configureMockStore([]);
const store = mockStore({});
jest.useFakeTimers();

describe('Schedule', () => {
it('should save date time between steps ', async () => {
render(
<Provider store={store}>
<JobWizard />
</Provider>
);
await act(async () => {
fireEvent.click(screen.getByText('Schedule'));
});
const newStartDate = '2020/03/12';
const newStartTime = '12:03';
const newEndsDate = '2030/03/12';
const newEndsTime = '17:34';
const [startsDateField, endsDateField] = screen.getAllByPlaceholderText(
'yyyy/mm/dd'
);
const [startsTimeField, endsTimeField] = screen.getAllByPlaceholderText(
'hh:mm'
);
await act(async () => {
await fireEvent.change(startsDateField, {
target: { value: newStartDate },
});
await fireEvent.change(startsTimeField, {
target: { value: newStartTime },
});
await fireEvent.change(endsDateField, { target: { value: newEndsDate } });
await fireEvent.change(endsTimeField, { target: { value: newEndsTime } });
jest.runAllTimers(); // to handle pf4 date picket popover useTimer
});
await act(async () => {
fireEvent.click(screen.getByText('Category and Template'));
});
expect(screen.getAllByText('Category and Template')).toHaveLength(3);

await act(async () => {
fireEvent.click(screen.getByText('Schedule'));
jest.runAllTimers();
});
expect(startsDateField.value).toBe(newStartDate);
expect(startsTimeField.value).toBe(newStartTime);
expect(endsDateField.value).toBe(newEndsDate);
expect(endsTimeField.value).toBe(newEndsTime);
});
it('should remove start date time on execute now', async () => {
render(
<Provider store={store}>
<JobWizard />
</Provider>
);
await act(async () => {
fireEvent.click(screen.getByText('Schedule'));
});
const executeNow = screen.getByLabelText('Execute now');
const executeFuture = screen.getByLabelText(
'Schedule for future execution'
);
expect(executeNow.checked).toBeTruthy();
const newStartDate = '2020/03/12';
const newStartTime = '12:03';
const [startsDateField] = screen.getAllByPlaceholderText('yyyy/mm/dd');
const [startsTimeField] = screen.getAllByPlaceholderText('hh:mm');
await act(async () => {
await fireEvent.change(startsDateField, {
target: { value: newStartDate },
});
await fireEvent.change(startsTimeField, {
target: { value: newStartTime },
});
await jest.runOnlyPendingTimers();
});
expect(startsDateField.value).toBe(newStartDate);
expect(startsTimeField.value).toBe(newStartTime);
expect(executeFuture.checked).toBeTruthy();
await act(async () => {
await fireEvent.click(executeNow);
});
expect(executeNow.checked).toBeTruthy();
expect(startsDateField.value).toBe('');
expect(startsTimeField.value).toBe('');
});

it('should disable end date on never ends', async () => {
render(
<Provider store={store}>
<JobWizard />
</Provider>
);
await act(async () => {
await fireEvent.click(screen.getByText('Schedule'));
jest.runAllTimers();
});
const neverEnds = screen.getByLabelText('Never ends');
expect(neverEnds.checked).toBeFalsy();

const [, endsDateField] = screen.getAllByPlaceholderText('yyyy/mm/dd');
const [, endsTimeField] = screen.getAllByPlaceholderText('hh:mm');
expect(endsDateField.disabled).toBeFalsy();
expect(endsTimeField.disabled).toBeFalsy();
await act(async () => {
fireEvent.click(neverEnds);
});
expect(neverEnds.checked).toBeTruthy();
expect(endsDateField.disabled).toBeTruthy();
expect(endsTimeField.disabled).toBeTruthy();
});
});
17 changes: 9 additions & 8 deletions webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import { render, fireEvent, screen, act } from '@testing-library/react';
import { StartEndDates } from '../StartEndDates';

const setEnds = jest.fn();
const setIsNeverEnds = jest.fn();
const props = {
starts: '',
setStarts: jest.fn(),
ends: 'some-end-date',
setEnds,
setIsNeverEnds,
isNeverEnds: false,
};

describe('StartEndDates', () => {
it('never ends', () => {
render(<StartEndDates {...props} />);
const neverEnds = screen.getByLabelText('Never ends', {
selector: 'input',
});
fireEvent.click(neverEnds);
expect(setEnds).toBeCalledWith('');
it('never ends', async () => {
await act(async () => render(<StartEndDates {...props} />));
const neverEnds = screen.getByRole('checkbox', { name: 'Never ends' });
await act(async () => fireEvent.click(neverEnds));
expect(setIsNeverEnds).toBeCalledWith(true);
});
});

0 comments on commit 9fc3597

Please sign in to comment.