mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 18:40:01 -03:30
Merge pull request #6229 from AlexSCorey/5895-SurveyListToolbar
Adds SurveyList tool bar Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
commit
ecb7147614
@ -74,6 +74,14 @@ class JobTemplates extends SchedulesMixin(
|
||||
readSurvey(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/survey_spec/`);
|
||||
}
|
||||
|
||||
updateSurvey(id, survey = null) {
|
||||
return this.http.post(`${this.baseUrl}${id}/survey_spec/`, survey);
|
||||
}
|
||||
|
||||
destroySurvey(id) {
|
||||
return this.http.delete(`${this.baseUrl}${id}/survey_spec/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default JobTemplates;
|
||||
|
||||
@ -1,48 +1,227 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import useRequest, { useDeleteItems } from '@util/useRequest';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
|
||||
import useRequest from '@util/useRequest';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import { JobTemplatesAPI } from '@api';
|
||||
import ContentEmpty from '@components/ContentEmpty';
|
||||
import { getQSConfig } from '@util/qs';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import SurveyListItem from './SurveyListItem';
|
||||
import SurveyToolbar from './SurveyToolbar';
|
||||
|
||||
const QS_CONFIG = getQSConfig('survey', {
|
||||
page: 1,
|
||||
});
|
||||
|
||||
function SurveyList({ template, i18n }) {
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [surveyEnabled, setSurveyEnabled] = useState(template.survey_enabled);
|
||||
const [showToggleError, setShowToggleError] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
||||
function SurveyList({ template }) {
|
||||
const {
|
||||
result: questions,
|
||||
result: { questions, name, description },
|
||||
error: contentError,
|
||||
isLoading,
|
||||
request: fetchSurvey,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const {
|
||||
data: { spec },
|
||||
data: { spec = [], description: surveyDescription, name: surveyName },
|
||||
} = await JobTemplatesAPI.readSurvey(template.id);
|
||||
|
||||
return spec.map((s, index) => ({ ...s, id: index }));
|
||||
}, [template.id])
|
||||
return {
|
||||
questions: spec.map((s, index) => ({ ...s, id: index })),
|
||||
description: surveyDescription,
|
||||
name: surveyName,
|
||||
};
|
||||
}, [template.id]),
|
||||
{ questions: [], name: '', description: '' }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSurvey();
|
||||
}, [fetchSurvey]);
|
||||
if (contentError) {
|
||||
return <ContentError error={contentError} />;
|
||||
}
|
||||
if (isLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
questions?.length > 0 &&
|
||||
questions.map((question, index) => (
|
||||
const isAllSelected =
|
||||
selected.length === questions?.length && selected.length > 0;
|
||||
|
||||
const {
|
||||
isLoading: isDeleteLoading,
|
||||
deleteItems: deleteQuestions,
|
||||
deletionError,
|
||||
clearDeletionError,
|
||||
} = useDeleteItems(
|
||||
useCallback(async () => {
|
||||
if (isAllSelected) {
|
||||
return JobTemplatesAPI.destroySurvey(template.id);
|
||||
}
|
||||
const surveyQuestions = [];
|
||||
questions.forEach(q => {
|
||||
if (!selected.some(s => s.id === q.id)) {
|
||||
surveyQuestions.push(q);
|
||||
}
|
||||
});
|
||||
return JobTemplatesAPI.updateSurvey(template.id, {
|
||||
name,
|
||||
description,
|
||||
spec: surveyQuestions,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selected]),
|
||||
{
|
||||
qsConfig: QS_CONFIG,
|
||||
fetchItems: fetchSurvey,
|
||||
}
|
||||
);
|
||||
const {
|
||||
isToggleLoading,
|
||||
error: toggleError,
|
||||
request: toggleSurvey,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
await JobTemplatesAPI.update(template.id, {
|
||||
survey_enabled: !surveyEnabled,
|
||||
});
|
||||
return setSurveyEnabled(!surveyEnabled);
|
||||
}, [template, surveyEnabled]),
|
||||
template.survey_enabled
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (toggleError) {
|
||||
setShowToggleError(true);
|
||||
}
|
||||
}, [toggleError]);
|
||||
|
||||
const handleSelectAll = isSelected => {
|
||||
setSelected(isSelected ? [...questions] : []);
|
||||
};
|
||||
|
||||
const handleSelect = item => {
|
||||
if (selected.some(s => s.id === item.id)) {
|
||||
setSelected(selected.filter(s => s.id !== item.id));
|
||||
} else {
|
||||
setSelected(selected.concat(item));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteQuestions();
|
||||
setIsDeleteModalOpen(false);
|
||||
setSelected([]);
|
||||
};
|
||||
const canEdit = template.summary_fields.user_capabilities.edit;
|
||||
const canDelete = template.summary_fields.user_capabilities.delete;
|
||||
|
||||
let content;
|
||||
if (isLoading || isToggleLoading || isDeleteLoading) {
|
||||
content = <ContentLoading />;
|
||||
} else if (contentError) {
|
||||
content = <ContentError error={contentError} />;
|
||||
} else if (!questions || questions?.length <= 0) {
|
||||
content = (
|
||||
<ContentEmpty
|
||||
title={i18n._(t`No Survey Questions Found`)}
|
||||
message={i18n._(t`Please add survey questions.`)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = questions?.map((question, index) => (
|
||||
<SurveyListItem
|
||||
key={question.id}
|
||||
isLast={index === questions.length - 1}
|
||||
isFirst={index === 0}
|
||||
question={question}
|
||||
isChecked={selected.some(s => s.id === question.id)}
|
||||
onSelect={() => handleSelect(question)}
|
||||
/>
|
||||
))
|
||||
));
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<SurveyToolbar
|
||||
isAllSelected={isAllSelected}
|
||||
onSelectAll={handleSelectAll}
|
||||
surveyEnabled={surveyEnabled}
|
||||
onToggleSurvey={toggleSurvey}
|
||||
isDeleteDisabled={selected?.length === 0 || !canEdit || !canDelete}
|
||||
onToggleDeleteModal={() => setIsDeleteModalOpen(true)}
|
||||
/>
|
||||
{content}
|
||||
{isDeleteModalOpen && (
|
||||
<AlertModal
|
||||
variant="danger"
|
||||
title={
|
||||
isAllSelected
|
||||
? i18n._(t`Delete Survey`)
|
||||
: i18n._(t`Delete Questions`)
|
||||
}
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setSelected([]);
|
||||
}}
|
||||
actions={[
|
||||
<Button
|
||||
key="delete"
|
||||
variant="danger"
|
||||
aria-label={i18n._(t`confirm delete`)}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{i18n._(t`Delete`)}
|
||||
</Button>,
|
||||
<Button
|
||||
key="cancel"
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`cancel delete`)}
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setSelected([]);
|
||||
}}
|
||||
>
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div>{i18n._(t`This action will delete the following:`)}</div>
|
||||
{selected.map(question => (
|
||||
<span key={question.id}>
|
||||
<strong>{question.question_name}</strong>
|
||||
<br />
|
||||
</span>
|
||||
))}
|
||||
</AlertModal>
|
||||
)}
|
||||
{deletionError && (
|
||||
<AlertModal
|
||||
isOpen={deletionError}
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={clearDeletionError}
|
||||
>
|
||||
{i18n._(t`Failed to delete one or more jobs.`)}
|
||||
<ErrorDetail error={deletionError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
{toggleError && (
|
||||
<AlertModal
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
isOpen={showToggleError && !isLoading}
|
||||
onClose={() => setShowToggleError(false)}
|
||||
>
|
||||
{i18n._(t`Failed to toggle host.`)}
|
||||
<ErrorDetail error={toggleError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(SurveyList);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import SurveyList from './SurveyList';
|
||||
import { JobTemplatesAPI } from '@api';
|
||||
import mockJobTemplateData from './data.job_template.json';
|
||||
@ -10,7 +10,11 @@ jest.mock('@api/models/JobTemplates');
|
||||
describe('<SurveyList />', () => {
|
||||
beforeEach(() => {
|
||||
JobTemplatesAPI.readSurvey.mockResolvedValue({
|
||||
data: { spec: [{ question_name: 'Foo', type: 'text', default: 'Bar' }] },
|
||||
data: {
|
||||
name: 'Survey',
|
||||
description: 'description for survey',
|
||||
spec: [{ question_name: 'Foo', type: 'text', default: 'Bar' }],
|
||||
},
|
||||
});
|
||||
});
|
||||
test('expect component to mount successfully', async () => {
|
||||
@ -30,6 +34,7 @@ describe('<SurveyList />', () => {
|
||||
);
|
||||
});
|
||||
expect(JobTemplatesAPI.readSurvey).toBeCalledWith(7);
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('SurveyListItem').length).toBe(1);
|
||||
});
|
||||
@ -41,7 +46,91 @@ describe('<SurveyList />', () => {
|
||||
<SurveyList template={{ ...mockJobTemplateData, id: 'a' }} />
|
||||
);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('ContentError').length).toBe(1);
|
||||
});
|
||||
test('can toggle survey on and off', async () => {
|
||||
JobTemplatesAPI.update.mockResolvedValue();
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SurveyList
|
||||
template={{ ...mockJobTemplateData, survey_enabled: false }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
expect(wrapper.find('Switch').length).toBe(1);
|
||||
expect(wrapper.find('Switch').prop('isChecked')).toBe(false);
|
||||
await act(async () => {
|
||||
wrapper.find('Switch').invoke('onChange')(true);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('Switch').prop('isChecked')).toBe(true);
|
||||
expect(JobTemplatesAPI.update).toBeCalledWith(7, {
|
||||
survey_enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('selectAll enables delete button and calls the api to delete properly', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SurveyList
|
||||
template={{ ...mockJobTemplateData, survey_enabled: false }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'SurveyListItem');
|
||||
expect(wrapper.find('Button[variant="danger"]').prop('isDisabled')).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('Checkbox[aria-label="Select all"]').prop('isChecked')
|
||||
).toBe(false);
|
||||
act(() => {
|
||||
wrapper.find('Checkbox[aria-label="Select all"]').invoke('onChange')(
|
||||
{ target: { checked: true } },
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('Checkbox[aria-label="Select all"]').prop('isChecked')
|
||||
).toBe(true);
|
||||
|
||||
expect(wrapper.find('Button[variant="danger"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
act(() => {
|
||||
wrapper.find('Button[variant="danger"]').invoke('onClick')();
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
await act(() =>
|
||||
wrapper.find('Button[aria-label="confirm delete"]').invoke('onClick')()
|
||||
);
|
||||
expect(JobTemplatesAPI.destroySurvey).toBeCalledWith(7);
|
||||
});
|
||||
});
|
||||
describe('Survey with no questions', () => {
|
||||
test('Survey with no questions renders empty state', async () => {
|
||||
JobTemplatesAPI.readSurvey.mockResolvedValue({});
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SurveyList template={mockJobTemplateData} />
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('ContentEmpty').length).toBe(1);
|
||||
expect(wrapper.find('SurveyListItem').length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -28,11 +28,18 @@ const Button = styled(_Button)`
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
`;
|
||||
function SurveyListItem({ question, i18n, isLast, isFirst }) {
|
||||
function SurveyListItem({
|
||||
question,
|
||||
i18n,
|
||||
isLast,
|
||||
isFirst,
|
||||
isChecked,
|
||||
onSelect,
|
||||
}) {
|
||||
return (
|
||||
<DataList aria-label={i18n._(t`Survey List`)}>
|
||||
<DataListItem aria-labelledby={i18n._(t`Survey questions`)}>
|
||||
<DataListItemRow>
|
||||
<DataListItemRow css="padding-left:16px">
|
||||
<DataListAction
|
||||
id="sortQuestions"
|
||||
aria-labelledby={i18n._(t`Sort question order`)}
|
||||
@ -59,7 +66,11 @@ function SurveyListItem({ question, i18n, isLast, isFirst }) {
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</DataListAction>
|
||||
<DataListCheck checked={false} aria-labelledby="survey check" />
|
||||
<DataListCheck
|
||||
checked={isChecked}
|
||||
onChange={onSelect}
|
||||
aria-labelledby="survey check"
|
||||
/>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key={question.question_name}>
|
||||
|
||||
@ -4,7 +4,7 @@ import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import SurveyListItem from './SurveyListItem';
|
||||
|
||||
describe('<SurveyListItem />', () => {
|
||||
const item = { question_name: 'Foo', default: 'Bar', type: 'text' };
|
||||
const item = { question_name: 'Foo', default: 'Bar', type: 'text', id: 1 };
|
||||
test('renders successfully', () => {
|
||||
let wrapper;
|
||||
act(() => {
|
||||
@ -32,7 +32,7 @@ describe('<SurveyListItem />', () => {
|
||||
let wrapper;
|
||||
act(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<SurveyListItem question={item} isFirst isLast />
|
||||
<SurveyListItem question={item} isChecked={false} isFirst isLast />
|
||||
);
|
||||
});
|
||||
const moveUp = wrapper
|
||||
|
||||
65
awx/ui_next/src/screens/Template/shared/SurveyToolbar.jsx
Normal file
65
awx/ui_next/src/screens/Template/shared/SurveyToolbar.jsx
Normal file
@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
|
||||
import {
|
||||
DataToolbar,
|
||||
DataToolbarContent,
|
||||
DataToolbarGroup,
|
||||
DataToolbarItem,
|
||||
} from '@patternfly/react-core/dist/umd/experimental';
|
||||
import { Switch, Checkbox, Button } from '@patternfly/react-core';
|
||||
import { ToolbarAddButton } from '@components/PaginatedDataList';
|
||||
|
||||
function SurveyToolbar({
|
||||
isAllSelected,
|
||||
onSelectAll,
|
||||
i18n,
|
||||
surveyEnabled,
|
||||
onToggleSurvey,
|
||||
isDeleteDisabled,
|
||||
onToggleDeleteModal,
|
||||
}) {
|
||||
return (
|
||||
<DataToolbar id="survey-toolbar">
|
||||
<DataToolbarContent>
|
||||
<DataToolbarItem>
|
||||
<Checkbox
|
||||
isChecked={isAllSelected}
|
||||
onChange={isChecked => {
|
||||
onSelectAll(isChecked);
|
||||
}}
|
||||
aria-label={i18n._(t`Select all`)}
|
||||
id="select-all"
|
||||
/>
|
||||
</DataToolbarItem>
|
||||
<DataToolbarItem>
|
||||
<Switch
|
||||
aria-label={i18n._(t`Survey Toggle`)}
|
||||
id="survey-toggle"
|
||||
label={i18n._(t`On`)}
|
||||
labelOff={i18n._(t`Off`)}
|
||||
isChecked={surveyEnabled}
|
||||
onChange={() => onToggleSurvey(!surveyEnabled)}
|
||||
/>
|
||||
</DataToolbarItem>
|
||||
<DataToolbarGroup>
|
||||
<DataToolbarItem>
|
||||
<ToolbarAddButton linkTo="/" />
|
||||
</DataToolbarItem>
|
||||
<DataToolbarItem>
|
||||
<Button
|
||||
variant="danger"
|
||||
isDisabled={isDeleteDisabled}
|
||||
onClick={() => onToggleDeleteModal(true)}
|
||||
>
|
||||
{i18n._(t`Delete`)}
|
||||
</Button>
|
||||
</DataToolbarItem>
|
||||
</DataToolbarGroup>
|
||||
</DataToolbarContent>
|
||||
</DataToolbar>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(SurveyToolbar);
|
||||
@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import SurveyToolbar from './SurveyToolbar';
|
||||
|
||||
jest.mock('@api/models/JobTemplates');
|
||||
|
||||
describe('<SurveyToolbar />', () => {
|
||||
test('delete Button is disabled', () => {
|
||||
let wrapper;
|
||||
|
||||
act(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<SurveyToolbar
|
||||
isDeleteDisabled
|
||||
onSelectAll={jest.fn()}
|
||||
isAllSelected
|
||||
onToggleDeleteModal={jest.fn()}
|
||||
onToggleSurvey={jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
expect(wrapper.find('Button[variant="danger"]').prop('isDisabled')).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('delete Button is enabled', () => {
|
||||
let wrapper;
|
||||
act(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<SurveyToolbar
|
||||
isDeleteDisabled={false}
|
||||
onSelectAll={jest.fn()}
|
||||
isAllSelected
|
||||
onToggleDeleteModal={jest.fn()}
|
||||
onToggleSurvey={jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(
|
||||
wrapper.find('Checkbox[aria-label="Select all"]').prop('isChecked')
|
||||
).toBe(true);
|
||||
expect(wrapper.find('Button[variant="danger"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('switch is off', () => {
|
||||
let wrapper;
|
||||
act(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<SurveyToolbar
|
||||
surveyEnabled={false}
|
||||
isDeleteDisabled={false}
|
||||
onSelectAll={jest.fn()}
|
||||
isAllSelected
|
||||
onToggleDelete={jest.fn()}
|
||||
onToggleSurvey={jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
expect(wrapper.find('Switch').length).toBe(1);
|
||||
expect(wrapper.find('Switch').prop('isChecked')).toBe(false);
|
||||
});
|
||||
|
||||
test('switch is on', () => {
|
||||
let wrapper;
|
||||
act(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<SurveyToolbar
|
||||
surveyEnabled
|
||||
isDeleteDisabled={false}
|
||||
onSelectAll={jest.fn()}
|
||||
isAllSelected
|
||||
onToggleDelete={jest.fn()}
|
||||
onToggleSurvey={jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
expect(wrapper.find('Switch').length).toBe(1);
|
||||
expect(wrapper.find('Switch').prop('isChecked')).toBe(true);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user