mirror of
https://github.com/ansible/awx.git
synced 2026-05-09 10:27:37 -02:30
Merge pull request #10820 from AlexSCorey/10654-10739-10740-fix
Host List Expand All and Survey List -> Tables
This commit is contained in:
@@ -14,6 +14,7 @@ import PaginatedTable, {
|
|||||||
} from 'components/PaginatedTable';
|
} from 'components/PaginatedTable';
|
||||||
import useRequest, { useDeleteItems } from 'hooks/useRequest';
|
import useRequest, { useDeleteItems } from 'hooks/useRequest';
|
||||||
import useSelected from 'hooks/useSelected';
|
import useSelected from 'hooks/useSelected';
|
||||||
|
import useExpanded from 'hooks/useExpanded';
|
||||||
import { encodeQueryString, getQSConfig, parseQueryString } from 'util/qs';
|
import { encodeQueryString, getQSConfig, parseQueryString } from 'util/qs';
|
||||||
|
|
||||||
import HostListItem from './HostListItem';
|
import HostListItem from './HostListItem';
|
||||||
@@ -88,6 +89,9 @@ function HostList() {
|
|||||||
const { selected, isAllSelected, handleSelect, selectAll, clearSelected } =
|
const { selected, isAllSelected, handleSelect, selectAll, clearSelected } =
|
||||||
useSelected(hosts);
|
useSelected(hosts);
|
||||||
|
|
||||||
|
const { expanded, isAllExpanded, handleExpand, expandAll } =
|
||||||
|
useExpanded(hosts);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isLoading: isDeleteLoading,
|
isLoading: isDeleteLoading,
|
||||||
deleteItems: deleteHosts,
|
deleteItems: deleteHosts,
|
||||||
@@ -165,6 +169,8 @@ function HostList() {
|
|||||||
{...props}
|
{...props}
|
||||||
isAllSelected={isAllSelected}
|
isAllSelected={isAllSelected}
|
||||||
onSelectAll={selectAll}
|
onSelectAll={selectAll}
|
||||||
|
isAllExpanded={isAllExpanded}
|
||||||
|
onExpandAll={expandAll}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
additionalControls={[
|
additionalControls={[
|
||||||
...(canAdd
|
...(canAdd
|
||||||
@@ -195,6 +201,8 @@ function HostList() {
|
|||||||
<HostListItem
|
<HostListItem
|
||||||
key={host.id}
|
key={host.id}
|
||||||
host={host}
|
host={host}
|
||||||
|
isExpanded={expanded.some((row) => row.id === host.id)}
|
||||||
|
onExpand={() => handleExpand(host)}
|
||||||
detailUrl={`${match.url}/${host.id}/details`}
|
detailUrl={`${match.url}/${host.id}/details`}
|
||||||
isSelected={selected.some((row) => row.id === host.id)}
|
isSelected={selected.some((row) => row.id === host.id)}
|
||||||
onSelect={() => handleSelect(host)}
|
onSelect={() => handleSelect(host)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'styled-components/macro';
|
import 'styled-components/macro';
|
||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { string, bool, func } from 'prop-types';
|
import { string, bool, func } from 'prop-types';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -13,9 +13,16 @@ import HostToggle from 'components/HostToggle';
|
|||||||
import { DetailList, Detail } from 'components/DetailList';
|
import { DetailList, Detail } from 'components/DetailList';
|
||||||
import Sparkline from 'components/Sparkline';
|
import Sparkline from 'components/Sparkline';
|
||||||
|
|
||||||
function HostListItem({ host, isSelected, onSelect, detailUrl, rowIndex }) {
|
function HostListItem({
|
||||||
|
host,
|
||||||
|
isSelected,
|
||||||
|
onSelect,
|
||||||
|
detailUrl,
|
||||||
|
rowIndex,
|
||||||
|
isExpanded,
|
||||||
|
onExpand,
|
||||||
|
}) {
|
||||||
const labelId = `check-action-${host.id}`;
|
const labelId = `check-action-${host.id}`;
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
summary_fields: { recent_jobs: recentJobs = [] },
|
summary_fields: { recent_jobs: recentJobs = [] },
|
||||||
@@ -28,7 +35,7 @@ function HostListItem({ host, isSelected, onSelect, detailUrl, rowIndex }) {
|
|||||||
expand={{
|
expand={{
|
||||||
rowIndex,
|
rowIndex,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
onToggle: () => setIsExpanded(!isExpanded),
|
onToggle: onExpand,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Td
|
<Td
|
||||||
@@ -78,6 +85,7 @@ function HostListItem({ host, isSelected, onSelect, detailUrl, rowIndex }) {
|
|||||||
<ExpandableRowContent>
|
<ExpandableRowContent>
|
||||||
<DetailList gutter="sm">
|
<DetailList gutter="sm">
|
||||||
<Detail
|
<Detail
|
||||||
|
dataCy={`${host.name}-activity`}
|
||||||
label={t`Activity`}
|
label={t`Activity`}
|
||||||
value={
|
value={
|
||||||
recentJobs.length > 0 ? (
|
recentJobs.length > 0 ? (
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ import React, { useState } from 'react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { useRouteMatch } from 'react-router-dom';
|
import { useRouteMatch } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
DataList,
|
Button,
|
||||||
Button as _Button,
|
|
||||||
Title,
|
Title,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
EmptyStateIcon,
|
EmptyStateIcon,
|
||||||
EmptyStateBody,
|
EmptyStateBody,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
import { TableComposable, Thead, Tr, Th, Tbody } from '@patternfly/react-table';
|
||||||
import { CubesIcon } from '@patternfly/react-icons';
|
import { CubesIcon } from '@patternfly/react-icons';
|
||||||
import styled from 'styled-components';
|
|
||||||
import ContentLoading from 'components/ContentLoading';
|
import ContentLoading from 'components/ContentLoading';
|
||||||
import AlertModal from 'components/AlertModal';
|
import AlertModal from 'components/AlertModal';
|
||||||
import { ToolbarAddButton } from 'components/PaginatedTable';
|
import { ToolbarAddButton } from 'components/PaginatedTable';
|
||||||
@@ -19,11 +18,7 @@ import { ToolbarAddButton } from 'components/PaginatedTable';
|
|||||||
import useSelected from 'hooks/useSelected';
|
import useSelected from 'hooks/useSelected';
|
||||||
import SurveyListItem from './SurveyListItem';
|
import SurveyListItem from './SurveyListItem';
|
||||||
import SurveyToolbar from './SurveyToolbar';
|
import SurveyToolbar from './SurveyToolbar';
|
||||||
import SurveyPreviewModal from './SurveyPreviewModal';
|
import SurveyReorderModal from './SurveyReorderModal';
|
||||||
|
|
||||||
const Button = styled(_Button)`
|
|
||||||
margin: 20px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function SurveyList({
|
function SurveyList({
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -38,7 +33,7 @@ function SurveyList({
|
|||||||
|
|
||||||
const questions = survey?.spec || [];
|
const questions = survey?.spec || [];
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false);
|
const [isOrderModalOpen, setIsOrderModalOpen] = useState(false);
|
||||||
|
|
||||||
const { selected, isAllSelected, setSelected, selectAll, clearSelected } =
|
const { selected, isAllSelected, setSelected, selectAll, clearSelected } =
|
||||||
useSelected(questions);
|
useSelected(questions);
|
||||||
@@ -61,26 +56,6 @@ function SurveyList({
|
|||||||
clearSelected();
|
clearSelected();
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveUp = (question) => {
|
|
||||||
const index = questions.indexOf(question);
|
|
||||||
if (index < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const beginning = questions.slice(0, index - 1);
|
|
||||||
const swapWith = questions[index - 1];
|
|
||||||
const end = questions.slice(index + 1);
|
|
||||||
updateSurvey([...beginning, question, swapWith, ...end]);
|
|
||||||
};
|
|
||||||
const moveDown = (question) => {
|
|
||||||
const index = questions.indexOf(question);
|
|
||||||
if (index === -1 || index > questions.length - 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const beginning = questions.slice(0, index);
|
|
||||||
const swapWith = questions[index + 1];
|
|
||||||
const end = questions.slice(index + 2);
|
|
||||||
updateSurvey([...beginning, swapWith, question, ...end]);
|
|
||||||
};
|
|
||||||
const deleteModal = (
|
const deleteModal = (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -129,36 +104,47 @@ function SurveyList({
|
|||||||
content = <ContentLoading />;
|
content = <ContentLoading />;
|
||||||
} else {
|
} else {
|
||||||
content = (
|
content = (
|
||||||
<DataList aria-label={t`Survey List`}>
|
<>
|
||||||
{questions?.map((question, index) => (
|
<TableComposable ouiaId="survey-list">
|
||||||
<SurveyListItem
|
<Thead>
|
||||||
key={question.variable}
|
<Tr>
|
||||||
isLast={index === questions.length - 1}
|
<Th />
|
||||||
isFirst={index === 0}
|
<Th datalabel={t`Name`}>{t`Name`}</Th>
|
||||||
question={question}
|
<Th datalabel={t`Type`}>{t`Type`}</Th>
|
||||||
isChecked={selected.some((q) => q.variable === question.variable)}
|
<Th datalabel={t`Default`}>{t`Default`}</Th>
|
||||||
onSelect={() => handleSelect(question)}
|
<Th datalabel={t`Actions`}>{t`Actions`}</Th>
|
||||||
onMoveUp={moveUp}
|
</Tr>
|
||||||
onMoveDown={moveDown}
|
</Thead>
|
||||||
canEdit={canEdit}
|
<Tbody>
|
||||||
/>
|
{questions?.map((question, index) => (
|
||||||
))}
|
<SurveyListItem
|
||||||
|
key={question.variable}
|
||||||
|
isLast={index === questions.length - 1}
|
||||||
|
isFirst={index === 0}
|
||||||
|
question={question}
|
||||||
|
isChecked={selected.some(
|
||||||
|
(q) => q.variable === question.variable
|
||||||
|
)}
|
||||||
|
onSelect={() => handleSelect(question)}
|
||||||
|
canEdit={canEdit}
|
||||||
|
rowIndex={index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</TableComposable>
|
||||||
{isDeleteModalOpen && deleteModal}
|
{isDeleteModalOpen && deleteModal}
|
||||||
{isPreviewModalOpen && (
|
{isOrderModalOpen && (
|
||||||
<SurveyPreviewModal
|
<SurveyReorderModal
|
||||||
isPreviewModalOpen={isPreviewModalOpen}
|
isOrderModalOpen={isOrderModalOpen}
|
||||||
onToggleModalOpen={() => setIsPreviewModalOpen(false)}
|
onCloseOrderModal={() => setIsOrderModalOpen(false)}
|
||||||
questions={questions}
|
questions={questions}
|
||||||
|
onSave={(newOrder) => {
|
||||||
|
updateSurvey(newOrder);
|
||||||
|
setIsOrderModalOpen(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Button
|
</>
|
||||||
onClick={() => setIsPreviewModalOpen(true)}
|
|
||||||
variant="primary"
|
|
||||||
aria-label={t`Preview`}
|
|
||||||
>
|
|
||||||
{t`Preview`}
|
|
||||||
</Button>
|
|
||||||
</DataList>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,6 +163,12 @@ function SurveyList({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SurveyToolbar
|
<SurveyToolbar
|
||||||
|
onOpenOrderModal={
|
||||||
|
questions.length > 1 &&
|
||||||
|
(() => {
|
||||||
|
setIsOrderModalOpen(true);
|
||||||
|
})
|
||||||
|
}
|
||||||
isAllSelected={isAllSelected}
|
isAllSelected={isAllSelected}
|
||||||
onSelectAll={selectAll}
|
onSelectAll={selectAll}
|
||||||
surveyEnabled={surveyEnabled}
|
surveyEnabled={surveyEnabled}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const surveyData = {
|
|||||||
description: 'description for survey',
|
description: 'description for survey',
|
||||||
spec: [
|
spec: [
|
||||||
{ question_name: 'Foo', type: 'text', default: 'Bar', variable: 'foo' },
|
{ question_name: 'Foo', type: 'text', default: 'Bar', variable: 'foo' },
|
||||||
|
{ question_name: 'Bizz', type: 'text', default: 'bazz', variable: 'bizz' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,9 +60,10 @@ describe('<SurveyList />', () => {
|
|||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
expect(wrapper.find('Button[variant="secondary"]').prop('isDisabled')).toBe(
|
expect(
|
||||||
true
|
wrapper.find('Button[ouiaId="survey-delete-button"]').prop('isDisabled')
|
||||||
);
|
).toBe(true);
|
||||||
|
expect(wrapper.find('Button[ouiaId="edit-order"]')).toHaveLength(1);
|
||||||
expect(
|
expect(
|
||||||
wrapper.find('Checkbox[aria-label="Select all"]').prop('isChecked')
|
wrapper.find('Checkbox[aria-label="Select all"]').prop('isChecked')
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
@@ -76,11 +78,11 @@ describe('<SurveyList />', () => {
|
|||||||
expect(
|
expect(
|
||||||
wrapper.find('Checkbox[aria-label="Select all"]').prop('isChecked')
|
wrapper.find('Checkbox[aria-label="Select all"]').prop('isChecked')
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(wrapper.find('Button[variant="secondary"]').prop('isDisabled')).toBe(
|
expect(
|
||||||
false
|
wrapper.find('Button[ouiaId="survey-delete-button"]').prop('isDisabled')
|
||||||
);
|
).toBe(false);
|
||||||
act(() => {
|
act(() => {
|
||||||
wrapper.find('Button[variant="secondary"]').invoke('onClick')();
|
wrapper.find('Button[ouiaId="survey-delete-button"]').invoke('onClick')();
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
@@ -91,38 +93,38 @@ describe('<SurveyList />', () => {
|
|||||||
expect(deleteSurvey).toHaveBeenCalled();
|
expect(deleteSurvey).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render Preview button ', async () => {
|
test('should render Edit Order button ', async () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<SurveyList survey={surveyData} />);
|
wrapper = mountWithContexts(<SurveyList survey={surveyData} canEdit />);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapper.find('Button[aria-label="Preview"]').length).toBe(1);
|
expect(wrapper.find('Button[ouiaId="edit-order"]').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Preview button should render Modal', async () => {
|
test('Edit Order button should render Modal', async () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<SurveyList survey={surveyData} />);
|
wrapper = mountWithContexts(<SurveyList survey={surveyData} canEdit />);
|
||||||
});
|
});
|
||||||
act(() => wrapper.find('Button[aria-label="Preview"]').prop('onClick')());
|
act(() => wrapper.find('Button[ouiaId="edit-order"]').prop('onClick')());
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
expect(wrapper.find('SurveyPreviewModal').length).toBe(1);
|
expect(wrapper.find('SurveyReorderModal').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Modal close button should close modal', async () => {
|
test('Modal close button should close modal', async () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<SurveyList survey={surveyData} />);
|
wrapper = mountWithContexts(<SurveyList survey={surveyData} canEdit />);
|
||||||
});
|
});
|
||||||
act(() => wrapper.find('Button[aria-label="Preview"]').prop('onClick')());
|
act(() => wrapper.find('Button[ouiaId="edit-order"]').prop('onClick')());
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
expect(wrapper.find('SurveyPreviewModal').length).toBe(1);
|
expect(wrapper.find('SurveyReorderModal').length).toBe(1);
|
||||||
|
|
||||||
act(() => wrapper.find('Modal').prop('onClose')());
|
act(() => wrapper.find('Modal').prop('onClose')());
|
||||||
|
|
||||||
|
|||||||
@@ -2,179 +2,103 @@ import 'styled-components/macro';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import { Chip, Tooltip, Button } from '@patternfly/react-core';
|
||||||
Button as _Button,
|
|
||||||
Chip,
|
import { Tr, Td } from '@patternfly/react-table';
|
||||||
DataListAction as _DataListAction,
|
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||||
DataListCell,
|
|
||||||
DataListCheck,
|
|
||||||
DataListItemCells,
|
|
||||||
DataListItemRow,
|
|
||||||
DataListItem,
|
|
||||||
Stack,
|
|
||||||
StackItem,
|
|
||||||
Tooltip,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import {
|
|
||||||
CaretDownIcon,
|
|
||||||
CaretUpIcon,
|
|
||||||
PencilAltIcon,
|
|
||||||
} from '@patternfly/react-icons';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import ChipGroup from 'components/ChipGroup';
|
import ChipGroup from 'components/ChipGroup';
|
||||||
|
import { ActionItem, ActionsTd } from 'components/PaginatedTable';
|
||||||
const DataListAction = styled(_DataListAction)`
|
|
||||||
&& {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 20px;
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Button = styled(_Button)`
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Required = styled.span`
|
const Required = styled.span`
|
||||||
color: var(--pf-global--danger-color--100);
|
color: var(--pf-global--danger-color--100);
|
||||||
margin-left: var(--pf-global--spacer--xs);
|
margin-left: var(--pf-global--spacer--xs);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Label = styled.b`
|
const SurveyActionsTd = styled(ActionsTd)`
|
||||||
margin-right: 20px;
|
&& {
|
||||||
|
padding-right: 35px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EditSection = styled(_DataListAction)``;
|
function SurveyListItem({ canEdit, question, isChecked, onSelect, rowIndex }) {
|
||||||
|
|
||||||
const EditButton = styled(_Button)``;
|
|
||||||
|
|
||||||
function SurveyListItem({
|
|
||||||
canEdit,
|
|
||||||
question,
|
|
||||||
isLast,
|
|
||||||
isFirst,
|
|
||||||
isChecked,
|
|
||||||
onSelect,
|
|
||||||
onMoveUp,
|
|
||||||
onMoveDown,
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<DataListItem
|
<Tr>
|
||||||
aria-labelledby={t`Survey questions`}
|
<Td
|
||||||
id={`survey-list-item-${question.variable}`}
|
data-cy={`${question.variable}-select`}
|
||||||
>
|
select={{
|
||||||
<DataListItemRow css="padding-left:16px">
|
rowIndex,
|
||||||
<DataListAction
|
isSelected: isChecked,
|
||||||
id="sortQuestions"
|
onSelect,
|
||||||
aria-labelledby={t`Sort question order`}
|
}}
|
||||||
aria-label={t`Sort question order`}
|
dataLabel={t`Selected`}
|
||||||
>
|
/>
|
||||||
<Stack>
|
<Td
|
||||||
<StackItem>
|
data-cy={`${question.variable}-name`}
|
||||||
<Button
|
id={`survey-list-item-${question.variable}`}
|
||||||
ouiaId={`${question.variable}-move-up-button`}
|
dataLabel={t`Name`}
|
||||||
variant="plain"
|
>
|
||||||
aria-label={t`move up`}
|
<>
|
||||||
isDisabled={isFirst || !canEdit}
|
<Link
|
||||||
onClick={() => onMoveUp(question)}
|
to={`survey/edit?question_variable=${encodeURIComponent(
|
||||||
>
|
question.variable
|
||||||
<CaretUpIcon />
|
)}`}
|
||||||
</Button>
|
>
|
||||||
</StackItem>
|
{question.question_name}
|
||||||
<StackItem>
|
</Link>
|
||||||
<Button
|
{question.required && (
|
||||||
ouiaId={`${question.variable}-move-down-button`}
|
<Required
|
||||||
variant="plain"
|
aria-label={t`Required`}
|
||||||
aria-label={t`move down`}
|
className="pf-c-form__label-required"
|
||||||
isDisabled={isLast || !canEdit}
|
aria-hidden="true"
|
||||||
onClick={() => onMoveDown(question)}
|
>
|
||||||
>
|
*
|
||||||
<CaretDownIcon />
|
</Required>
|
||||||
</Button>
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
|
||||||
</DataListAction>
|
|
||||||
<DataListCheck
|
|
||||||
isDisabled={!canEdit}
|
|
||||||
checked={isChecked}
|
|
||||||
onChange={onSelect}
|
|
||||||
aria-labelledby="survey check"
|
|
||||||
/>
|
|
||||||
<DataListItemCells
|
|
||||||
dataListCells={[
|
|
||||||
<DataListCell key="name">
|
|
||||||
<>
|
|
||||||
<Link
|
|
||||||
to={`survey/edit?question_variable=${encodeURIComponent(
|
|
||||||
question.variable
|
|
||||||
)}`}
|
|
||||||
>
|
|
||||||
{question.question_name}
|
|
||||||
</Link>
|
|
||||||
{question.required && (
|
|
||||||
<Required
|
|
||||||
aria-label={t`Required`}
|
|
||||||
className="pf-c-form__label-required"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
*
|
|
||||||
</Required>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</DataListCell>,
|
|
||||||
<DataListCell key="type">
|
|
||||||
<Label>{t`Type`}</Label>
|
|
||||||
{question.type}
|
|
||||||
</DataListCell>,
|
|
||||||
<DataListCell key="default">
|
|
||||||
<Label>{t`Default`}</Label>
|
|
||||||
{[question.type].includes('password') && (
|
|
||||||
<span>{t`encrypted`.toUpperCase()}</span>
|
|
||||||
)}
|
|
||||||
{[question.type].includes('multiselect') &&
|
|
||||||
question.default.length > 0 && (
|
|
||||||
<ChipGroup
|
|
||||||
numChips={5}
|
|
||||||
totalChips={question.default.split('\n').length}
|
|
||||||
>
|
|
||||||
{question.default.split('\n').map((chip) => (
|
|
||||||
<Chip key={chip} isReadOnly>
|
|
||||||
{chip}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</ChipGroup>
|
|
||||||
)}
|
|
||||||
{![question.type].includes('password') &&
|
|
||||||
![question.type].includes('multiselect') && (
|
|
||||||
<span>{question.default}</span>
|
|
||||||
)}
|
|
||||||
</DataListCell>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<EditSection aria-label={t`actions`}>
|
|
||||||
{canEdit && (
|
|
||||||
<EditButton variant="plain">
|
|
||||||
<Tooltip content={t`Edit Survey`} position="top">
|
|
||||||
<EditButton
|
|
||||||
ouiaId={`edit-survey-${question.variable}`}
|
|
||||||
aria-label={t`edit survey`}
|
|
||||||
variant="plain"
|
|
||||||
component={Link}
|
|
||||||
to={`survey/edit?question_variable=${encodeURIComponent(
|
|
||||||
question.variable
|
|
||||||
)}`}
|
|
||||||
>
|
|
||||||
<PencilAltIcon />
|
|
||||||
</EditButton>
|
|
||||||
</Tooltip>
|
|
||||||
</EditButton>
|
|
||||||
)}
|
)}
|
||||||
</EditSection>
|
</>
|
||||||
</DataListItemRow>
|
</Td>
|
||||||
</DataListItem>
|
<Td data-cy={`${question.variable}-type`} dataLabel={t`Type`}>
|
||||||
|
{question.type}
|
||||||
|
</Td>
|
||||||
|
<Td dataLabel={t`Default`}>
|
||||||
|
{[question.type].includes('password') && (
|
||||||
|
<span>{t`encrypted`.toUpperCase()}</span>
|
||||||
|
)}
|
||||||
|
{[question.type].includes('multiselect') &&
|
||||||
|
question.default.length > 0 && (
|
||||||
|
<ChipGroup
|
||||||
|
numChips={5}
|
||||||
|
totalChips={question.default.split('\n').length}
|
||||||
|
>
|
||||||
|
{question.default.split('\n').map((chip) => (
|
||||||
|
<Chip key={chip} isReadOnly>
|
||||||
|
{chip}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</ChipGroup>
|
||||||
|
)}
|
||||||
|
{![question.type].includes('password') &&
|
||||||
|
![question.type].includes('multiselect') && (
|
||||||
|
<span>{question.default}</span>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<SurveyActionsTd dataLabel={t`Actions`}>
|
||||||
|
<ActionItem visible={canEdit}>
|
||||||
|
<Tooltip content={t`Edit Survey`} position="top">
|
||||||
|
<Button
|
||||||
|
ouiaId={`edit-survey-${question.variable}`}
|
||||||
|
variant="plain"
|
||||||
|
component={Link}
|
||||||
|
to={`survey/edit?question_variable=${encodeURIComponent(
|
||||||
|
question.variable
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
<PencilAltIcon />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</ActionItem>
|
||||||
|
</SurveyActionsTd>
|
||||||
|
</Tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default SurveyListItem;
|
export default SurveyListItem;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
shallowWithContexts,
|
||||||
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import SurveyListItem from './SurveyListItem';
|
import SurveyListItem from './SurveyListItem';
|
||||||
|
|
||||||
describe('<SurveyListItem />', () => {
|
describe('<SurveyListItem />', () => {
|
||||||
@@ -11,47 +14,37 @@ describe('<SurveyListItem />', () => {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
id: 1,
|
id: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
test('renders successfully', () => {
|
test('renders successfully', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
act(() => {
|
act(() => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = shallowWithContexts(
|
||||||
<SurveyListItem question={item} isFirst={false} isLast={false} />
|
<SurveyListItem question={item} isFirst={false} isLast={false} />
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(wrapper.length).toBe(1);
|
expect(wrapper.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fields are rendering properly', () => {
|
test('fields are rendering properly', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
act(() => {
|
act(() => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<SurveyListItem question={item} isFirst={false} isLast={false} />
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<SurveyListItem
|
||||||
|
question={item}
|
||||||
|
isFirst={false}
|
||||||
|
isLast={false}
|
||||||
|
canEdit
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const moveUp = wrapper.find('Button[aria-label="move up"]');
|
expect(wrapper.find('SelectColumn').length).toBe(1);
|
||||||
const moveDown = wrapper.find('Button[aria-label="move down"]');
|
expect(wrapper.find('Td').length).toBe(5);
|
||||||
expect(moveUp.length).toBe(1);
|
|
||||||
expect(moveDown.length).toBe(1);
|
|
||||||
expect(wrapper.find('b').at(0).text()).toBe('Type');
|
|
||||||
expect(wrapper.find('b').at(1).text()).toBe('Default');
|
|
||||||
expect(wrapper.find('DataListCheck').length).toBe(1);
|
|
||||||
expect(wrapper.find('DataListCell').length).toBe(3);
|
|
||||||
});
|
|
||||||
test('move up and move down buttons are disabled', () => {
|
|
||||||
let wrapper;
|
|
||||||
act(() => {
|
|
||||||
wrapper = mountWithContexts(
|
|
||||||
<SurveyListItem question={item} isChecked={false} isFirst isLast />
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const moveUp = wrapper
|
|
||||||
.find('Button[aria-label="move up"]')
|
|
||||||
.prop('isDisabled');
|
|
||||||
const moveDown = wrapper
|
|
||||||
.find('Button[aria-label="move down"]')
|
|
||||||
.prop('isDisabled');
|
|
||||||
expect(moveUp).toBe(true);
|
|
||||||
expect(moveDown).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('required item has required asterisk', () => {
|
test('required item has required asterisk', () => {
|
||||||
const newItem = {
|
const newItem = {
|
||||||
question_name: 'Foo',
|
question_name: 'Foo',
|
||||||
@@ -64,7 +57,17 @@ describe('<SurveyListItem />', () => {
|
|||||||
let wrapper;
|
let wrapper;
|
||||||
act(() => {
|
act(() => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<SurveyListItem question={newItem} isChecked={false} isFirst isLast />
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<SurveyListItem
|
||||||
|
question={newItem}
|
||||||
|
isChecked={false}
|
||||||
|
isFirst
|
||||||
|
isLast
|
||||||
|
canEdit
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(wrapper.find('span[aria-label="Required"]').length).toBe(1);
|
expect(wrapper.find('span[aria-label="Required"]').length).toBe(1);
|
||||||
@@ -72,12 +75,19 @@ describe('<SurveyListItem />', () => {
|
|||||||
test('items that are not required should not have an asterisk', () => {
|
test('items that are not required should not have an asterisk', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
act(() => {
|
act(() => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = shallowWithContexts(
|
||||||
<SurveyListItem question={item} isChecked={false} isFirst isLast />
|
<SurveyListItem
|
||||||
|
question={item}
|
||||||
|
isChecked={false}
|
||||||
|
isFirst
|
||||||
|
isLast
|
||||||
|
canEdit
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(wrapper.find('span[aria-label="Required"]').length).toBe(0);
|
expect(wrapper.find('span[aria-label="Required"]').length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('required item has required asterisk', () => {
|
test('required item has required asterisk', () => {
|
||||||
const newItem = {
|
const newItem = {
|
||||||
question_name: 'Foo',
|
question_name: 'Foo',
|
||||||
@@ -89,7 +99,17 @@ describe('<SurveyListItem />', () => {
|
|||||||
let wrapper;
|
let wrapper;
|
||||||
act(() => {
|
act(() => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<SurveyListItem question={newItem} isChecked={false} isFirst isLast />
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<SurveyListItem
|
||||||
|
question={newItem}
|
||||||
|
isChecked={false}
|
||||||
|
isFirst
|
||||||
|
isLast
|
||||||
|
canEdit
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(wrapper.find('Chip').length).toBe(6);
|
expect(wrapper.find('Chip').length).toBe(6);
|
||||||
@@ -98,6 +118,7 @@ describe('<SurveyListItem />', () => {
|
|||||||
.filter((chip) => chip.prop('isOverFlowChip') !== true)
|
.filter((chip) => chip.prop('isOverFlowChip') !== true)
|
||||||
.map((chip) => expect(chip.prop('isReadOnly')).toBe(true));
|
.map((chip) => expect(chip.prop('isReadOnly')).toBe(true));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('items that are no required should have no an asterisk', () => {
|
test('items that are no required should have no an asterisk', () => {
|
||||||
const newItem = {
|
const newItem = {
|
||||||
question_name: 'Foo',
|
question_name: 'Foo',
|
||||||
@@ -109,24 +130,31 @@ describe('<SurveyListItem />', () => {
|
|||||||
let wrapper;
|
let wrapper;
|
||||||
act(() => {
|
act(() => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<SurveyListItem question={newItem} isChecked={false} isFirst isLast />
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<SurveyListItem
|
||||||
|
question={newItem}
|
||||||
|
isChecked={false}
|
||||||
|
isFirst
|
||||||
|
isLast
|
||||||
|
canEdit
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(wrapper.find('span').text()).toBe('ENCRYPTED');
|
expect(wrapper.find('span').text()).toBe('ENCRYPTED');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('users without edit/delete permissions are unable to reorder the questions', () => {
|
test('users without edit/delete permissions are unable to reorder the questions', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
act(() => {
|
act(() => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = shallowWithContexts(
|
||||||
<SurveyListItem canEdit={false} question={item} isChecked={false} />
|
<SurveyListItem canEdit={false} question={item} isChecked={false} />
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(wrapper.find('button[aria-label="move up"]').prop('disabled')).toBe(
|
expect(wrapper.find('button[aria-label="move up"]')).toHaveLength(0);
|
||||||
true
|
expect(wrapper.find('button[aria-label="move down"]')).toHaveLength(0);
|
||||||
);
|
|
||||||
expect(
|
|
||||||
wrapper.find('button[aria-label="move down"]').prop('disabled')
|
|
||||||
).toBe(true);
|
|
||||||
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
|
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,9 +162,20 @@ describe('<SurveyListItem />', () => {
|
|||||||
let wrapper;
|
let wrapper;
|
||||||
act(() => {
|
act(() => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<SurveyListItem canEdit question={item} isChecked={false} />
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<SurveyListItem
|
||||||
|
question={item}
|
||||||
|
isFirst
|
||||||
|
isLast
|
||||||
|
isChecked={false}
|
||||||
|
canEdit
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
|
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
|
||||||
expect(wrapper.find('Button[ouiaId="edit-survey-buzz"]').prop('to')).toBe(
|
expect(wrapper.find('Button[ouiaId="edit-survey-buzz"]').prop('to')).toBe(
|
||||||
'survey/edit?question_variable=buzz'
|
'survey/edit?question_variable=buzz'
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import { Formik } from 'formik';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormGroup,
|
|
||||||
Modal,
|
|
||||||
TextInput,
|
|
||||||
TextArea,
|
|
||||||
Select,
|
|
||||||
SelectOption,
|
|
||||||
SelectVariant,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { PasswordField } from 'components/FormField';
|
|
||||||
|
|
||||||
function SurveyPreviewModal({
|
|
||||||
questions,
|
|
||||||
isPreviewModalOpen,
|
|
||||||
onToggleModalOpen,
|
|
||||||
}) {
|
|
||||||
const initialValues = {};
|
|
||||||
questions.forEach((q) => {
|
|
||||||
initialValues[q.variable] = q.default;
|
|
||||||
return initialValues;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={t`Survey Preview`}
|
|
||||||
aria-label={t`Survey preview modal`}
|
|
||||||
isOpen={isPreviewModalOpen}
|
|
||||||
onClose={() => onToggleModalOpen(false)}
|
|
||||||
variant="small"
|
|
||||||
>
|
|
||||||
<Formik initialValues={initialValues}>
|
|
||||||
{() => (
|
|
||||||
<Form>
|
|
||||||
{questions.map((q) => (
|
|
||||||
<div key={q.variable}>
|
|
||||||
{['text', 'integer', 'float'].includes(q.type) && (
|
|
||||||
<FormGroup
|
|
||||||
fieldId={`survey-preview-text-${q.variable}`}
|
|
||||||
label={q.question_name}
|
|
||||||
isRequired={q.required}
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
id={`survey-preview-text-${q.variable}`}
|
|
||||||
value={q.default}
|
|
||||||
isDisabled
|
|
||||||
aria-label={t`Text`}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
{['textarea'].includes(q.type) && (
|
|
||||||
<FormGroup
|
|
||||||
fieldId={`survey-preview-textArea-${q.variable}`}
|
|
||||||
label={q.question_name}
|
|
||||||
isRequired={q.required}
|
|
||||||
>
|
|
||||||
<TextArea
|
|
||||||
id={`survey-preview-textArea-${q.variable}`}
|
|
||||||
type={`survey-preview-textArea-${q.variable}`}
|
|
||||||
value={q.default}
|
|
||||||
aria-label={t`Text Area`}
|
|
||||||
isDisabled
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
{['password'].includes(q.type) && (
|
|
||||||
<PasswordField
|
|
||||||
id={`survey-preview-password-${q.variable}`}
|
|
||||||
label={q.question_name}
|
|
||||||
name={q.variable}
|
|
||||||
isDisabled
|
|
||||||
isRequired={q.required}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{['multiplechoice'].includes(q.type) && (
|
|
||||||
<FormGroup
|
|
||||||
fieldId={`survey-preview-multipleChoice-${q.variable}`}
|
|
||||||
label={q.question_name}
|
|
||||||
isRequired={q.required}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
id={`survey-preview-multipleChoice-${q.variable}`}
|
|
||||||
isDisabled
|
|
||||||
aria-label={t`Multiple Choice`}
|
|
||||||
typeAheadAriaLabel={t`Multiple Choice`}
|
|
||||||
placeholderText={q.default}
|
|
||||||
onToggle={() => {}}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
{['multiselect'].includes(q.type) && (
|
|
||||||
<FormGroup
|
|
||||||
fieldId={`survey-preview-multiSelect-${q.variable}`}
|
|
||||||
label={q.question_name}
|
|
||||||
isRequired={q.required}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
isDisabled
|
|
||||||
isReadOnly
|
|
||||||
variant={SelectVariant.typeaheadMulti}
|
|
||||||
isOpen={false}
|
|
||||||
selections={
|
|
||||||
q.default.length > 0 ? q.default.split('\n') : []
|
|
||||||
}
|
|
||||||
onToggle={() => {}}
|
|
||||||
aria-label={t`Multi-Select`}
|
|
||||||
typeAheadAriaLabel={t`Multi-Select`}
|
|
||||||
id={`survey-preview-multiSelect-${q.variable}`}
|
|
||||||
>
|
|
||||||
{q.choices.length > 0 &&
|
|
||||||
q.choices
|
|
||||||
.split('\n')
|
|
||||||
.map((option) => (
|
|
||||||
<SelectOption key={option} value={option} />
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default SurveyPreviewModal;
|
|
||||||
235
awx/ui/src/screens/Template/Survey/SurveyReorderModal.js
Normal file
235
awx/ui/src/screens/Template/Survey/SurveyReorderModal.js
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import React, { useState, useRef } from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { GripVerticalIcon } from '@patternfly/react-icons';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
TextInput,
|
||||||
|
TextArea,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
SelectVariant,
|
||||||
|
Button,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import {
|
||||||
|
TableComposable,
|
||||||
|
Thead,
|
||||||
|
Tbody,
|
||||||
|
Tr,
|
||||||
|
Th,
|
||||||
|
Td,
|
||||||
|
} from '@patternfly/react-table';
|
||||||
|
|
||||||
|
function SurveyReorderModal({
|
||||||
|
questions,
|
||||||
|
isOrderModalOpen,
|
||||||
|
onCloseOrderModal,
|
||||||
|
onSave,
|
||||||
|
}) {
|
||||||
|
const [surveyQuestions, setSurveyQuestions] = useState([...questions]);
|
||||||
|
const [itemStartIndex, setStartItemIndex] = useState(null);
|
||||||
|
const [draggedItemId, setDraggedItemId] = useState(null);
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
const isValidDrop = (evt) => {
|
||||||
|
const ulRect = ref.current.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
evt.clientX > ulRect.x &&
|
||||||
|
evt.clientX < ulRect.x + ulRect.width &&
|
||||||
|
evt.clientY > ulRect.y &&
|
||||||
|
evt.clientY < ulRect.y + ulRect.height
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const onDrop = (evt) => {
|
||||||
|
if (!isValidDrop(evt)) {
|
||||||
|
onDragCancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragCancel = () => {
|
||||||
|
Array.from(ref.current.children).forEach((el) => {
|
||||||
|
el.setAttribute('aria-pressed', 'false');
|
||||||
|
});
|
||||||
|
setDraggedItemId(null);
|
||||||
|
setStartItemIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragOver = (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
|
||||||
|
const curListItem = evt.target.closest('tr');
|
||||||
|
|
||||||
|
const dragId = curListItem.id;
|
||||||
|
const newDraggedItemIndex = Array.from(ref.current.children).findIndex(
|
||||||
|
(item) => item.id === dragId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newDraggedItemIndex !== itemStartIndex) {
|
||||||
|
const temporaryOrder = moveItem(
|
||||||
|
[...surveyQuestions],
|
||||||
|
draggedItemId,
|
||||||
|
newDraggedItemIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
setSurveyQuestions(temporaryOrder);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveItem = (arr, itemId, toIndex) => {
|
||||||
|
const fromIndex = arr.findIndex((item) => item.variable === itemId);
|
||||||
|
|
||||||
|
if (fromIndex === toIndex) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
const temp = arr.splice(fromIndex, 1);
|
||||||
|
arr.splice(toIndex, 0, temp[0]);
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragLeave = (evt) => {
|
||||||
|
if (!isValidDrop(evt)) {
|
||||||
|
setStartItemIndex(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = (evt) => {
|
||||||
|
evt.target.setAttribute('aria-pressed', 'false');
|
||||||
|
|
||||||
|
setDraggedItemId(null);
|
||||||
|
setStartItemIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragStart = (evt) => {
|
||||||
|
evt.dataTransfer.effectAllowed = 'move';
|
||||||
|
const newDraggedItemId = evt.currentTarget.id;
|
||||||
|
|
||||||
|
const originalStartIndex = Array.from(ref.current.children).findIndex(
|
||||||
|
(item) => item.id === evt.currentTarget.id
|
||||||
|
);
|
||||||
|
|
||||||
|
evt.currentTarget.setAttribute('aria-pressed', 'true');
|
||||||
|
setDraggedItemId(newDraggedItemId);
|
||||||
|
setStartItemIndex(originalStartIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultAnswer = (q) => {
|
||||||
|
let component = null;
|
||||||
|
switch (q.type) {
|
||||||
|
case 'textarea':
|
||||||
|
component = (
|
||||||
|
<TextArea
|
||||||
|
id={`survey-preview-textArea-${q.variable}`}
|
||||||
|
type={`survey-preview-textArea-${q.variable}`}
|
||||||
|
value={q.default}
|
||||||
|
aria-label={t`Text Area`}
|
||||||
|
isDisabled
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'multiplechoice':
|
||||||
|
component = (
|
||||||
|
<Select
|
||||||
|
id={`survey-preview-multipleChoice-${q.variable}`}
|
||||||
|
isDisabled
|
||||||
|
aria-label={t`Multiple Choice`}
|
||||||
|
typeAheadAriaLabel={t`Multiple Choice`}
|
||||||
|
placeholderText={q.default}
|
||||||
|
onToggle={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'multiselect':
|
||||||
|
component = (
|
||||||
|
<Select
|
||||||
|
isDisabled
|
||||||
|
isReadOnly
|
||||||
|
variant={SelectVariant.typeaheadMulti}
|
||||||
|
isOpen={false}
|
||||||
|
selections={q.default.length > 0 ? q.default.split('\n') : []}
|
||||||
|
onToggle={() => {}}
|
||||||
|
aria-label={t`Multi-Select`}
|
||||||
|
typeAheadAriaLabel={t`Multi-Select`}
|
||||||
|
id={`survey-preview-multiSelect-${q.variable}`}
|
||||||
|
>
|
||||||
|
{q.choices.length > 0 &&
|
||||||
|
q.choices
|
||||||
|
.split('\n')
|
||||||
|
.map((option) => <SelectOption key={option} value={option} />)}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
component = (
|
||||||
|
<TextInput
|
||||||
|
id={`survey-preview-text-${q.variable}`}
|
||||||
|
value={q.default}
|
||||||
|
isDisabled
|
||||||
|
aria-label={t`Text`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return component;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t`Survey Question Order`}
|
||||||
|
aria-label={t`Survey preview modal`}
|
||||||
|
isOpen={isOrderModalOpen}
|
||||||
|
description={t`To reoder the survey questions drag and drop them in the desired location.`}
|
||||||
|
onClose={() => onCloseOrderModal()}
|
||||||
|
variant="medium"
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
ouiaId="survey-order-save"
|
||||||
|
key="save"
|
||||||
|
onClick={() => {
|
||||||
|
onSave(surveyQuestions);
|
||||||
|
}}
|
||||||
|
>{t`Save`}</Button>,
|
||||||
|
<Button
|
||||||
|
ouiaId="survey-order-cancel"
|
||||||
|
key="cancel"
|
||||||
|
variant="link"
|
||||||
|
onClick={() => onCloseOrderModal()}
|
||||||
|
>{t`Cancel`}</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TableComposable>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th dataLabel={t`Order`}>{t`Order`}</Th>
|
||||||
|
<Th dataLabel={t`Name`}>{t`Name`}</Th>
|
||||||
|
<Th dataLabel={t`Default Answer(s)`}>{t`Default Answer(s)`}</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody onDragOver={onDragOver} onDragLeave={onDragLeave} ref={ref}>
|
||||||
|
{surveyQuestions.map((q) => (
|
||||||
|
<Tr
|
||||||
|
key={q.variable}
|
||||||
|
id={q.variable}
|
||||||
|
draggable
|
||||||
|
onDrop={onDrop}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
>
|
||||||
|
<Td dataLabel={t`Order`}>
|
||||||
|
<Button variant="plain">
|
||||||
|
<GripVerticalIcon />
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
<Td dataLabel={t`Name`} aria-label={q.question_name}>
|
||||||
|
{q.question_name}
|
||||||
|
</Td>
|
||||||
|
<Td dataLabel={t`Default Answer(s)`}>{defaultAnswer(q)}</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</TableComposable>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default SurveyReorderModal;
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
import SurveyPreviewModal from './SurveyPreviewModal';
|
import SurveyReorderModal from './SurveyReorderModal';
|
||||||
|
|
||||||
const questions = [
|
const questions = [
|
||||||
{
|
{
|
||||||
@@ -65,59 +65,55 @@ const questions = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('<SurveyPreviewModal />', () => {
|
describe('<SurveyReorderModal />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<SurveyPreviewModal questions={questions} isPreviewModalOpen />
|
<SurveyReorderModal questions={questions} isOrderModalOpen />
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
waitForElement(wrapper, 'Form');
|
waitForElement(wrapper, 'Form');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Renders proper fields', async () => {
|
test('Renders proper fields', async () => {
|
||||||
const question1 = wrapper.find('FormGroup[label="Text Question"]');
|
const question1 = wrapper.find('td[aria-label="Text Question"]');
|
||||||
const question1Value = wrapper.find('TextInputBase').at(0);
|
const question1Value = wrapper
|
||||||
|
.find('TextInput#survey-preview-text-dfgh')
|
||||||
|
.find('input');
|
||||||
|
|
||||||
const question2 = wrapper
|
const question2 = wrapper.find('td[aria-label="Select Question"]');
|
||||||
.find('FormGroup[label="Select Question"]')
|
|
||||||
.find('label');
|
|
||||||
const question2Value = wrapper.find('Select[aria-label="Multiple Choice"]');
|
const question2Value = wrapper.find('Select[aria-label="Multiple Choice"]');
|
||||||
|
|
||||||
const question3 = wrapper
|
const question3 = wrapper;
|
||||||
.find('FormGroup[label="Text Area Question"]')
|
wrapper.find('td[aria-label="Text Area Question"]');
|
||||||
.find('label');
|
|
||||||
const question3Value = wrapper.find('textarea');
|
const question3Value = wrapper.find('textarea');
|
||||||
|
|
||||||
const question4 = wrapper.find('FormGroup[label="Password Question"]');
|
const question4 = wrapper.find('td[aria-label="Password Question"]');
|
||||||
const question4Value = wrapper.find('TextInputBase[type="password"]');
|
const question4Value = wrapper.find('TextInputBase#survey-preview-text-c');
|
||||||
|
|
||||||
const question5 = wrapper
|
const question5 = wrapper.find('td[aria-label="Multiple select Question"]');
|
||||||
.find('FormGroup[label="Multiple select Question"]')
|
|
||||||
.find('label');
|
|
||||||
const question5Value = wrapper
|
const question5Value = wrapper
|
||||||
.find('Select[aria-label="Multi-Select"]')
|
.find('Select[aria-label="Multi-Select"]')
|
||||||
.find('Chip');
|
.find('Chip');
|
||||||
|
expect(question1).toHaveLength(1);
|
||||||
expect(question1.text()).toBe('Text Question * ');
|
|
||||||
expect(question1Value.prop('value')).toBe('Text Question Value');
|
expect(question1Value.prop('value')).toBe('Text Question Value');
|
||||||
expect(question1Value.prop('isDisabled')).toBe(true);
|
expect(question1Value.prop('disabled')).toBe(true);
|
||||||
|
|
||||||
expect(question2.text()).toBe('Select Question *');
|
expect(question2).toHaveLength(1);
|
||||||
expect(question2Value.find('.pf-c-select__toggle-text').text()).toBe(
|
expect(question2Value.prop('placeholderText')).toBe(
|
||||||
'Select Question Value'
|
'Select Question Value'
|
||||||
);
|
);
|
||||||
expect(question2Value.prop('isDisabled')).toBe(true);
|
expect(question2Value.prop('isDisabled')).toBe(true);
|
||||||
|
|
||||||
expect(question3.text()).toBe('Text Area Question *');
|
expect(question3).toHaveLength(1);
|
||||||
expect(question3Value.prop('value')).toBe('Text Area Question Value');
|
expect(question3Value.prop('value')).toBe('Text Area Question Value');
|
||||||
expect(question3Value.prop('disabled')).toBe(true);
|
expect(question3Value.prop('disabled')).toBe(true);
|
||||||
expect(question4.text()).toBe('Password Question * ');
|
expect(question4).toHaveLength(1);
|
||||||
expect(question4Value.prop('placeholder')).toBe('ENCRYPTED');
|
expect(question4Value.prop('value')).toBe('$encrypted$');
|
||||||
expect(question4Value.prop('isDisabled')).toBe(true);
|
expect(question4Value.prop('isDisabled')).toBe(true);
|
||||||
|
|
||||||
expect(question5.text()).toBe('Multiple select Question *');
|
expect(question5).toHaveLength(1);
|
||||||
expect(question5Value.length).toBe(4);
|
expect(question5Value.length).toBe(4);
|
||||||
expect(
|
expect(
|
||||||
wrapper.find('Select[aria-label="Multi-Select"]').prop('isDisabled')
|
wrapper.find('Select[aria-label="Multi-Select"]').prop('isDisabled')
|
||||||
@@ -12,22 +12,26 @@ import {
|
|||||||
ToolbarContent,
|
ToolbarContent,
|
||||||
ToolbarGroup,
|
ToolbarGroup,
|
||||||
ToolbarItem,
|
ToolbarItem,
|
||||||
|
Tooltip,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { ToolbarAddButton } from 'components/PaginatedTable';
|
import { ToolbarAddButton } from 'components/PaginatedTable';
|
||||||
|
|
||||||
const Toolbar = styled(_Toolbar)`
|
const Toolbar = styled(_Toolbar)`
|
||||||
margin-left: 52px;
|
margin-left: 10px;
|
||||||
|
`;
|
||||||
|
const SwitchWrapper = styled(ToolbarItem)`
|
||||||
|
padding-left: 4px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function SurveyToolbar({
|
function SurveyToolbar({
|
||||||
canEdit,
|
canEdit,
|
||||||
isAllSelected,
|
isAllSelected,
|
||||||
onSelectAll,
|
onSelectAll,
|
||||||
|
|
||||||
surveyEnabled,
|
surveyEnabled,
|
||||||
onToggleSurvey,
|
onToggleSurvey,
|
||||||
isDeleteDisabled,
|
isDeleteDisabled,
|
||||||
onToggleDeleteModal,
|
onToggleDeleteModal,
|
||||||
|
onOpenOrderModal,
|
||||||
}) {
|
}) {
|
||||||
isDeleteDisabled = !canEdit || isDeleteDisabled;
|
isDeleteDisabled = !canEdit || isDeleteDisabled;
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
@@ -45,17 +49,6 @@ function SurveyToolbar({
|
|||||||
id="select-all"
|
id="select-all"
|
||||||
/>
|
/>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem>
|
|
||||||
<Switch
|
|
||||||
aria-label={t`Survey Toggle`}
|
|
||||||
id="survey-toggle"
|
|
||||||
label={t`On`}
|
|
||||||
labelOff={t`Off`}
|
|
||||||
isChecked={surveyEnabled}
|
|
||||||
isDisabled={!canEdit}
|
|
||||||
onChange={() => onToggleSurvey(!surveyEnabled)}
|
|
||||||
/>
|
|
||||||
</ToolbarItem>
|
|
||||||
<ToolbarGroup>
|
<ToolbarGroup>
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<ToolbarAddButton
|
<ToolbarAddButton
|
||||||
@@ -63,17 +56,55 @@ function SurveyToolbar({
|
|||||||
linkTo={`${match.url}/add`}
|
linkTo={`${match.url}/add`}
|
||||||
/>
|
/>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
|
{canEdit && onOpenOrderModal && (
|
||||||
|
<ToolbarItem>
|
||||||
|
<Tooltip
|
||||||
|
content={t`Click to rearrange the order of the survey questions`}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
onOpenOrderModal();
|
||||||
|
}}
|
||||||
|
variant="secondary"
|
||||||
|
ouiaId="edit-order"
|
||||||
|
>
|
||||||
|
{t`Edit Order`}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</ToolbarItem>
|
||||||
|
)}
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Button
|
<Tooltip
|
||||||
ouiaId="survey-delete-button"
|
content={
|
||||||
variant="secondary"
|
isDeleteDisabled
|
||||||
isDisabled={isDeleteDisabled}
|
? t`Select a question to delete`
|
||||||
onClick={() => onToggleDeleteModal(true)}
|
: t`Delete survey question`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{t`Delete`}
|
<div>
|
||||||
</Button>
|
<Button
|
||||||
|
ouiaId="survey-delete-button"
|
||||||
|
variant="secondary"
|
||||||
|
isDisabled={isDeleteDisabled}
|
||||||
|
onClick={() => onToggleDeleteModal(true)}
|
||||||
|
>
|
||||||
|
{t`Delete`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
</ToolbarGroup>
|
</ToolbarGroup>
|
||||||
|
<SwitchWrapper>
|
||||||
|
<Switch
|
||||||
|
aria-label={t`Survey Toggle`}
|
||||||
|
id="survey-toggle"
|
||||||
|
label={t`Survey Enabled`}
|
||||||
|
labelOff={t`Survey Disabled`}
|
||||||
|
isChecked={surveyEnabled}
|
||||||
|
isDisabled={!canEdit}
|
||||||
|
onChange={() => onToggleSurvey(!surveyEnabled)}
|
||||||
|
/>
|
||||||
|
</SwitchWrapper>
|
||||||
</ToolbarContent>
|
</ToolbarContent>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,16 +17,18 @@ describe('<SurveyToolbar />', () => {
|
|||||||
isAllSelected
|
isAllSelected
|
||||||
onToggleDeleteModal={jest.fn()}
|
onToggleDeleteModal={jest.fn()}
|
||||||
onToggleSurvey={jest.fn()}
|
onToggleSurvey={jest.fn()}
|
||||||
|
canEdit={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapper.find('Button[variant="secondary"]').prop('isDisabled')).toBe(
|
expect(
|
||||||
true
|
wrapper.find('Button[ouiaId="survey-delete-button"]').prop('isDisabled')
|
||||||
);
|
).toBe(true);
|
||||||
|
expect(wrapper.find('Button[ouiaId="edit-order"]')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('delete Button is enabled', () => {
|
test('delete Button is enabled and Edit order button is rendered', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
act(() => {
|
act(() => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
@@ -36,6 +38,7 @@ describe('<SurveyToolbar />', () => {
|
|||||||
isAllSelected
|
isAllSelected
|
||||||
onToggleDeleteModal={jest.fn()}
|
onToggleDeleteModal={jest.fn()}
|
||||||
onToggleSurvey={jest.fn()}
|
onToggleSurvey={jest.fn()}
|
||||||
|
onOpenOrderModal={jest.fn()}
|
||||||
canEdit
|
canEdit
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -43,9 +46,10 @@ describe('<SurveyToolbar />', () => {
|
|||||||
expect(
|
expect(
|
||||||
wrapper.find('Checkbox[aria-label="Select all"]').prop('isChecked')
|
wrapper.find('Checkbox[aria-label="Select all"]').prop('isChecked')
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(wrapper.find('Button[variant="secondary"]').prop('isDisabled')).toBe(
|
expect(
|
||||||
false
|
wrapper.find('Button[ouiaId="survey-delete-button"]').prop('isDisabled')
|
||||||
);
|
).toBe(false);
|
||||||
|
expect(wrapper.find('Button[ouiaId="edit-order"]')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('switch is off', () => {
|
test('switch is off', () => {
|
||||||
@@ -105,8 +109,9 @@ describe('<SurveyToolbar />', () => {
|
|||||||
);
|
);
|
||||||
expect(wrapper.find('Switch').prop('isDisabled')).toBe(true);
|
expect(wrapper.find('Switch').prop('isDisabled')).toBe(true);
|
||||||
expect(wrapper.find('ToolbarAddButton').prop('isDisabled')).toBe(true);
|
expect(wrapper.find('ToolbarAddButton').prop('isDisabled')).toBe(true);
|
||||||
expect(wrapper.find('Button[variant="secondary"]').prop('isDisabled')).toBe(
|
expect(
|
||||||
true
|
wrapper.find('Button[ouiaId="survey-delete-button"]').prop('isDisabled')
|
||||||
);
|
).toBe(true);
|
||||||
|
expect(wrapper.find('Button[ouiaId="edit-order"]')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export { default as SurveyList } from './SurveyList';
|
export { default as SurveyList } from './SurveyList';
|
||||||
export { default as SurveyQuestionAdd } from './SurveyQuestionAdd';
|
export { default as SurveyQuestionAdd } from './SurveyQuestionAdd';
|
||||||
export { default as SurveyQuestionEdit } from './SurveyQuestionEdit';
|
export { default as SurveyQuestionEdit } from './SurveyQuestionEdit';
|
||||||
export { default as SurveyPreviewModal } from './SurveyPreviewModal';
|
export { default as SurveyReorderModal } from './SurveyReorderModal';
|
||||||
|
|||||||
Reference in New Issue
Block a user