Merge pull request #10820 from AlexSCorey/10654-10739-10740-fix

Host List Expand All and Survey List -> Tables
This commit is contained in:
Sarah Akus 2021-08-11 09:58:49 -04:00 committed by GitHub
commit ec729a3f15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 573 additions and 465 deletions

View File

@ -14,6 +14,7 @@ import PaginatedTable, {
} from 'components/PaginatedTable';
import useRequest, { useDeleteItems } from 'hooks/useRequest';
import useSelected from 'hooks/useSelected';
import useExpanded from 'hooks/useExpanded';
import { encodeQueryString, getQSConfig, parseQueryString } from 'util/qs';
import HostListItem from './HostListItem';
@ -88,6 +89,9 @@ function HostList() {
const { selected, isAllSelected, handleSelect, selectAll, clearSelected } =
useSelected(hosts);
const { expanded, isAllExpanded, handleExpand, expandAll } =
useExpanded(hosts);
const {
isLoading: isDeleteLoading,
deleteItems: deleteHosts,
@ -165,6 +169,8 @@ function HostList() {
{...props}
isAllSelected={isAllSelected}
onSelectAll={selectAll}
isAllExpanded={isAllExpanded}
onExpandAll={expandAll}
qsConfig={QS_CONFIG}
additionalControls={[
...(canAdd
@ -195,6 +201,8 @@ function HostList() {
<HostListItem
key={host.id}
host={host}
isExpanded={expanded.some((row) => row.id === host.id)}
onExpand={() => handleExpand(host)}
detailUrl={`${match.url}/${host.id}/details`}
isSelected={selected.some((row) => row.id === host.id)}
onSelect={() => handleSelect(host)}

View File

@ -1,5 +1,5 @@
import 'styled-components/macro';
import React, { useState } from 'react';
import React from 'react';
import { string, bool, func } from 'prop-types';
import { t } from '@lingui/macro';
@ -13,9 +13,16 @@ import HostToggle from 'components/HostToggle';
import { DetailList, Detail } from 'components/DetailList';
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 [isExpanded, setIsExpanded] = useState(false);
const {
summary_fields: { recent_jobs: recentJobs = [] },
@ -28,7 +35,7 @@ function HostListItem({ host, isSelected, onSelect, detailUrl, rowIndex }) {
expand={{
rowIndex,
isExpanded,
onToggle: () => setIsExpanded(!isExpanded),
onToggle: onExpand,
}}
/>
<Td
@ -78,6 +85,7 @@ function HostListItem({ host, isSelected, onSelect, detailUrl, rowIndex }) {
<ExpandableRowContent>
<DetailList gutter="sm">
<Detail
dataCy={`${host.name}-activity`}
label={t`Activity`}
value={
recentJobs.length > 0 ? (

View File

@ -3,15 +3,14 @@ import React, { useState } from 'react';
import { t } from '@lingui/macro';
import { useRouteMatch } from 'react-router-dom';
import {
DataList,
Button as _Button,
Button,
Title,
EmptyState,
EmptyStateIcon,
EmptyStateBody,
} from '@patternfly/react-core';
import { TableComposable, Thead, Tr, Th, Tbody } from '@patternfly/react-table';
import { CubesIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
import ContentLoading from 'components/ContentLoading';
import AlertModal from 'components/AlertModal';
import { ToolbarAddButton } from 'components/PaginatedTable';
@ -19,11 +18,7 @@ import { ToolbarAddButton } from 'components/PaginatedTable';
import useSelected from 'hooks/useSelected';
import SurveyListItem from './SurveyListItem';
import SurveyToolbar from './SurveyToolbar';
import SurveyPreviewModal from './SurveyPreviewModal';
const Button = styled(_Button)`
margin: 20px;
`;
import SurveyReorderModal from './SurveyReorderModal';
function SurveyList({
isLoading,
@ -38,7 +33,7 @@ function SurveyList({
const questions = survey?.spec || [];
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false);
const [isOrderModalOpen, setIsOrderModalOpen] = useState(false);
const { selected, isAllSelected, setSelected, selectAll, clearSelected } =
useSelected(questions);
@ -61,26 +56,6 @@ function SurveyList({
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 = (
<AlertModal
variant="danger"
@ -129,36 +104,47 @@ function SurveyList({
content = <ContentLoading />;
} else {
content = (
<DataList aria-label={t`Survey List`}>
{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)}
onMoveUp={moveUp}
onMoveDown={moveDown}
canEdit={canEdit}
/>
))}
<>
<TableComposable ouiaId="survey-list">
<Thead>
<Tr>
<Th />
<Th datalabel={t`Name`}>{t`Name`}</Th>
<Th datalabel={t`Type`}>{t`Type`}</Th>
<Th datalabel={t`Default`}>{t`Default`}</Th>
<Th datalabel={t`Actions`}>{t`Actions`}</Th>
</Tr>
</Thead>
<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}
{isPreviewModalOpen && (
<SurveyPreviewModal
isPreviewModalOpen={isPreviewModalOpen}
onToggleModalOpen={() => setIsPreviewModalOpen(false)}
{isOrderModalOpen && (
<SurveyReorderModal
isOrderModalOpen={isOrderModalOpen}
onCloseOrderModal={() => setIsOrderModalOpen(false)}
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 (
<>
<SurveyToolbar
onOpenOrderModal={
questions.length > 1 &&
(() => {
setIsOrderModalOpen(true);
})
}
isAllSelected={isAllSelected}
onSelectAll={selectAll}
surveyEnabled={surveyEnabled}

View File

@ -12,6 +12,7 @@ const surveyData = {
description: 'description for survey',
spec: [
{ 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();
expect(wrapper.find('Button[variant="secondary"]').prop('isDisabled')).toBe(
true
);
expect(
wrapper.find('Button[ouiaId="survey-delete-button"]').prop('isDisabled')
).toBe(true);
expect(wrapper.find('Button[ouiaId="edit-order"]')).toHaveLength(1);
expect(
wrapper.find('Checkbox[aria-label="Select all"]').prop('isChecked')
).toBe(false);
@ -76,11 +78,11 @@ describe('<SurveyList />', () => {
expect(
wrapper.find('Checkbox[aria-label="Select all"]').prop('isChecked')
).toBe(true);
expect(wrapper.find('Button[variant="secondary"]').prop('isDisabled')).toBe(
false
);
expect(
wrapper.find('Button[ouiaId="survey-delete-button"]').prop('isDisabled')
).toBe(false);
act(() => {
wrapper.find('Button[variant="secondary"]').invoke('onClick')();
wrapper.find('Button[ouiaId="survey-delete-button"]').invoke('onClick')();
});
wrapper.update();
@ -91,38 +93,38 @@ describe('<SurveyList />', () => {
expect(deleteSurvey).toHaveBeenCalled();
});
test('should render Preview button ', async () => {
test('should render Edit Order button ', async () => {
let wrapper;
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;
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();
expect(wrapper.find('SurveyPreviewModal').length).toBe(1);
expect(wrapper.find('SurveyReorderModal').length).toBe(1);
});
test('Modal close button should close modal', async () => {
let wrapper;
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();
expect(wrapper.find('SurveyPreviewModal').length).toBe(1);
expect(wrapper.find('SurveyReorderModal').length).toBe(1);
act(() => wrapper.find('Modal').prop('onClose')());

View File

@ -2,179 +2,103 @@ import 'styled-components/macro';
import React from 'react';
import { t } from '@lingui/macro';
import { Link } from 'react-router-dom';
import {
Button as _Button,
Chip,
DataListAction as _DataListAction,
DataListCell,
DataListCheck,
DataListItemCells,
DataListItemRow,
DataListItem,
Stack,
StackItem,
Tooltip,
} from '@patternfly/react-core';
import {
CaretDownIcon,
CaretUpIcon,
PencilAltIcon,
} from '@patternfly/react-icons';
import { Chip, Tooltip, Button } from '@patternfly/react-core';
import { Tr, Td } from '@patternfly/react-table';
import { PencilAltIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
import ChipGroup from 'components/ChipGroup';
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;
`;
import { ActionItem, ActionsTd } from 'components/PaginatedTable';
const Required = styled.span`
color: var(--pf-global--danger-color--100);
margin-left: var(--pf-global--spacer--xs);
`;
const Label = styled.b`
margin-right: 20px;
const SurveyActionsTd = styled(ActionsTd)`
&& {
padding-right: 35px;
}
`;
const EditSection = styled(_DataListAction)``;
const EditButton = styled(_Button)``;
function SurveyListItem({
canEdit,
question,
isLast,
isFirst,
isChecked,
onSelect,
onMoveUp,
onMoveDown,
}) {
function SurveyListItem({ canEdit, question, isChecked, onSelect, rowIndex }) {
return (
<DataListItem
aria-labelledby={t`Survey questions`}
id={`survey-list-item-${question.variable}`}
>
<DataListItemRow css="padding-left:16px">
<DataListAction
id="sortQuestions"
aria-labelledby={t`Sort question order`}
aria-label={t`Sort question order`}
>
<Stack>
<StackItem>
<Button
ouiaId={`${question.variable}-move-up-button`}
variant="plain"
aria-label={t`move up`}
isDisabled={isFirst || !canEdit}
onClick={() => onMoveUp(question)}
>
<CaretUpIcon />
</Button>
</StackItem>
<StackItem>
<Button
ouiaId={`${question.variable}-move-down-button`}
variant="plain"
aria-label={t`move down`}
isDisabled={isLast || !canEdit}
onClick={() => onMoveDown(question)}
>
<CaretDownIcon />
</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>
<Tr>
<Td
data-cy={`${question.variable}-select`}
select={{
rowIndex,
isSelected: isChecked,
onSelect,
}}
dataLabel={t`Selected`}
/>
<Td
data-cy={`${question.variable}-name`}
id={`survey-list-item-${question.variable}`}
dataLabel={t`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>
)}
</EditSection>
</DataListItemRow>
</DataListItem>
</>
</Td>
<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;

View File

@ -1,6 +1,9 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import {
mountWithContexts,
shallowWithContexts,
} from '../../../../testUtils/enzymeHelpers';
import SurveyListItem from './SurveyListItem';
describe('<SurveyListItem />', () => {
@ -11,47 +14,37 @@ describe('<SurveyListItem />', () => {
type: 'text',
id: 1,
};
test('renders successfully', () => {
let wrapper;
act(() => {
wrapper = mountWithContexts(
wrapper = shallowWithContexts(
<SurveyListItem question={item} isFirst={false} isLast={false} />
);
});
expect(wrapper.length).toBe(1);
});
test('fields are rendering properly', () => {
let wrapper;
act(() => {
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"]');
const moveDown = wrapper.find('Button[aria-label="move down"]');
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);
expect(wrapper.find('SelectColumn').length).toBe(1);
expect(wrapper.find('Td').length).toBe(5);
});
test('required item has required asterisk', () => {
const newItem = {
question_name: 'Foo',
@ -64,7 +57,17 @@ describe('<SurveyListItem />', () => {
let wrapper;
act(() => {
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);
@ -72,12 +75,19 @@ describe('<SurveyListItem />', () => {
test('items that are not required should not have an asterisk', () => {
let wrapper;
act(() => {
wrapper = mountWithContexts(
<SurveyListItem question={item} isChecked={false} isFirst isLast />
wrapper = shallowWithContexts(
<SurveyListItem
question={item}
isChecked={false}
isFirst
isLast
canEdit
/>
);
});
expect(wrapper.find('span[aria-label="Required"]').length).toBe(0);
});
test('required item has required asterisk', () => {
const newItem = {
question_name: 'Foo',
@ -89,7 +99,17 @@ describe('<SurveyListItem />', () => {
let wrapper;
act(() => {
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);
@ -98,6 +118,7 @@ describe('<SurveyListItem />', () => {
.filter((chip) => chip.prop('isOverFlowChip') !== true)
.map((chip) => expect(chip.prop('isReadOnly')).toBe(true));
});
test('items that are no required should have no an asterisk', () => {
const newItem = {
question_name: 'Foo',
@ -109,24 +130,31 @@ describe('<SurveyListItem />', () => {
let wrapper;
act(() => {
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');
});
test('users without edit/delete permissions are unable to reorder the questions', () => {
let wrapper;
act(() => {
wrapper = mountWithContexts(
wrapper = shallowWithContexts(
<SurveyListItem canEdit={false} question={item} isChecked={false} />
);
});
expect(wrapper.find('button[aria-label="move up"]').prop('disabled')).toBe(
true
);
expect(
wrapper.find('button[aria-label="move down"]').prop('disabled')
).toBe(true);
expect(wrapper.find('button[aria-label="move up"]')).toHaveLength(0);
expect(wrapper.find('button[aria-label="move down"]')).toHaveLength(0);
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
});
@ -134,9 +162,20 @@ describe('<SurveyListItem />', () => {
let wrapper;
act(() => {
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('Button[ouiaId="edit-survey-buzz"]').prop('to')).toBe(
'survey/edit?question_variable=buzz'

View File

@ -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;

View 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;

View File

@ -5,7 +5,7 @@ import {
mountWithContexts,
} from '../../../../testUtils/enzymeHelpers';
import SurveyPreviewModal from './SurveyPreviewModal';
import SurveyReorderModal from './SurveyReorderModal';
const questions = [
{
@ -65,59 +65,55 @@ const questions = [
},
];
describe('<SurveyPreviewModal />', () => {
describe('<SurveyReorderModal />', () => {
let wrapper;
beforeAll(async () => {
await act(async () => {
wrapper = mountWithContexts(
<SurveyPreviewModal questions={questions} isPreviewModalOpen />
<SurveyReorderModal questions={questions} isOrderModalOpen />
);
});
waitForElement(wrapper, 'Form');
});
test('Renders proper fields', async () => {
const question1 = wrapper.find('FormGroup[label="Text Question"]');
const question1Value = wrapper.find('TextInputBase').at(0);
const question1 = wrapper.find('td[aria-label="Text Question"]');
const question1Value = wrapper
.find('TextInput#survey-preview-text-dfgh')
.find('input');
const question2 = wrapper
.find('FormGroup[label="Select Question"]')
.find('label');
const question2 = wrapper.find('td[aria-label="Select Question"]');
const question2Value = wrapper.find('Select[aria-label="Multiple Choice"]');
const question3 = wrapper
.find('FormGroup[label="Text Area Question"]')
.find('label');
const question3 = wrapper;
wrapper.find('td[aria-label="Text Area Question"]');
const question3Value = wrapper.find('textarea');
const question4 = wrapper.find('FormGroup[label="Password Question"]');
const question4Value = wrapper.find('TextInputBase[type="password"]');
const question4 = wrapper.find('td[aria-label="Password Question"]');
const question4Value = wrapper.find('TextInputBase#survey-preview-text-c');
const question5 = wrapper
.find('FormGroup[label="Multiple select Question"]')
.find('label');
const question5 = wrapper.find('td[aria-label="Multiple select Question"]');
const question5Value = wrapper
.find('Select[aria-label="Multi-Select"]')
.find('Chip');
expect(question1.text()).toBe('Text Question * ');
expect(question1).toHaveLength(1);
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(question2Value.find('.pf-c-select__toggle-text').text()).toBe(
expect(question2).toHaveLength(1);
expect(question2Value.prop('placeholderText')).toBe(
'Select Question Value'
);
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('disabled')).toBe(true);
expect(question4.text()).toBe('Password Question * ');
expect(question4Value.prop('placeholder')).toBe('ENCRYPTED');
expect(question4).toHaveLength(1);
expect(question4Value.prop('value')).toBe('$encrypted$');
expect(question4Value.prop('isDisabled')).toBe(true);
expect(question5.text()).toBe('Multiple select Question *');
expect(question5).toHaveLength(1);
expect(question5Value.length).toBe(4);
expect(
wrapper.find('Select[aria-label="Multi-Select"]').prop('isDisabled')

View File

@ -12,22 +12,26 @@ import {
ToolbarContent,
ToolbarGroup,
ToolbarItem,
Tooltip,
} from '@patternfly/react-core';
import { ToolbarAddButton } from 'components/PaginatedTable';
const Toolbar = styled(_Toolbar)`
margin-left: 52px;
margin-left: 10px;
`;
const SwitchWrapper = styled(ToolbarItem)`
padding-left: 4px;
`;
function SurveyToolbar({
canEdit,
isAllSelected,
onSelectAll,
surveyEnabled,
onToggleSurvey,
isDeleteDisabled,
onToggleDeleteModal,
onOpenOrderModal,
}) {
isDeleteDisabled = !canEdit || isDeleteDisabled;
const match = useRouteMatch();
@ -45,17 +49,6 @@ function SurveyToolbar({
id="select-all"
/>
</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>
<ToolbarItem>
<ToolbarAddButton
@ -63,17 +56,55 @@ function SurveyToolbar({
linkTo={`${match.url}/add`}
/>
</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>
<Button
ouiaId="survey-delete-button"
variant="secondary"
isDisabled={isDeleteDisabled}
onClick={() => onToggleDeleteModal(true)}
<Tooltip
content={
isDeleteDisabled
? t`Select a question to delete`
: t`Delete survey question`
}
>
{t`Delete`}
</Button>
<div>
<Button
ouiaId="survey-delete-button"
variant="secondary"
isDisabled={isDeleteDisabled}
onClick={() => onToggleDeleteModal(true)}
>
{t`Delete`}
</Button>
</div>
</Tooltip>
</ToolbarItem>
</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>
</Toolbar>
);

View File

@ -17,16 +17,18 @@ describe('<SurveyToolbar />', () => {
isAllSelected
onToggleDeleteModal={jest.fn()}
onToggleSurvey={jest.fn()}
canEdit={false}
/>
);
});
expect(wrapper.find('Button[variant="secondary"]').prop('isDisabled')).toBe(
true
);
expect(
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;
act(() => {
wrapper = mountWithContexts(
@ -36,6 +38,7 @@ describe('<SurveyToolbar />', () => {
isAllSelected
onToggleDeleteModal={jest.fn()}
onToggleSurvey={jest.fn()}
onOpenOrderModal={jest.fn()}
canEdit
/>
);
@ -43,9 +46,10 @@ describe('<SurveyToolbar />', () => {
expect(
wrapper.find('Checkbox[aria-label="Select all"]').prop('isChecked')
).toBe(true);
expect(wrapper.find('Button[variant="secondary"]').prop('isDisabled')).toBe(
false
);
expect(
wrapper.find('Button[ouiaId="survey-delete-button"]').prop('isDisabled')
).toBe(false);
expect(wrapper.find('Button[ouiaId="edit-order"]')).toHaveLength(1);
});
test('switch is off', () => {
@ -105,8 +109,9 @@ describe('<SurveyToolbar />', () => {
);
expect(wrapper.find('Switch').prop('isDisabled')).toBe(true);
expect(wrapper.find('ToolbarAddButton').prop('isDisabled')).toBe(true);
expect(wrapper.find('Button[variant="secondary"]').prop('isDisabled')).toBe(
true
);
expect(
wrapper.find('Button[ouiaId="survey-delete-button"]').prop('isDisabled')
).toBe(true);
expect(wrapper.find('Button[ouiaId="edit-order"]')).toHaveLength(0);
});
});

View File

@ -1,4 +1,4 @@
export { default as SurveyList } from './SurveyList';
export { default as SurveyQuestionAdd } from './SurveyQuestionAdd';
export { default as SurveyQuestionEdit } from './SurveyQuestionEdit';
export { default as SurveyPreviewModal } from './SurveyPreviewModal';
export { default as SurveyReorderModal } from './SurveyReorderModal';