mirror of
https://github.com/strapi/strapi.git
synced 2025-11-30 17:18:24 +00:00
Merge pull request #16546 from strapi/feature/review-workflow-1-d-and-d-stages
Settings: Add drag and drop to allow reordering of stages
This commit is contained in:
commit
84a9da4b2a
@ -18,12 +18,24 @@ import { Check } from '@strapi/icons';
|
|||||||
|
|
||||||
import { Stages } from './components/Stages';
|
import { Stages } from './components/Stages';
|
||||||
import { reducer, initialState } from './reducer';
|
import { reducer, initialState } from './reducer';
|
||||||
import { REDUX_NAMESPACE } from './constants';
|
import { REDUX_NAMESPACE, DRAG_DROP_TYPES } from './constants';
|
||||||
import { useInjectReducer } from '../../../../../../admin/src/hooks/useInjectReducer';
|
import { useInjectReducer } from '../../../../../../admin/src/hooks/useInjectReducer';
|
||||||
import { useReviewWorkflows } from './hooks/useReviewWorkflows';
|
import { useReviewWorkflows } from './hooks/useReviewWorkflows';
|
||||||
import { setWorkflows } from './actions';
|
import { setWorkflows } from './actions';
|
||||||
import { getWorkflowValidationSchema } from './utils/getWorkflowValidationSchema';
|
import { getWorkflowValidationSchema } from './utils/getWorkflowValidationSchema';
|
||||||
import adminPermissions from '../../../../../../admin/src/permissions';
|
import adminPermissions from '../../../../../../admin/src/permissions';
|
||||||
|
import { StageDragPreview } from './components/StageDragPreview';
|
||||||
|
import { DragLayer } from '../../../../../../admin/src/components/DragLayer';
|
||||||
|
|
||||||
|
function renderDragLayerItem({ type, item }) {
|
||||||
|
switch (type) {
|
||||||
|
case DRAG_DROP_TYPES.STAGE:
|
||||||
|
return <StageDragPreview {...item} />;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function ReviewWorkflowsPage() {
|
export function ReviewWorkflowsPage() {
|
||||||
const { trackUsage } = useTracking();
|
const { trackUsage } = useTracking();
|
||||||
@ -135,6 +147,8 @@ export function ReviewWorkflowsPage() {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Main tabIndex={-1}>
|
<Main tabIndex={-1}>
|
||||||
|
<DragLayer renderItem={renderDragLayerItem} />
|
||||||
|
|
||||||
<FormikProvider value={formik}>
|
<FormikProvider value={formik}>
|
||||||
<Form onSubmit={formik.handleSubmit}>
|
<Form onSubmit={formik.handleSubmit}>
|
||||||
<HeaderLayout
|
<HeaderLayout
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
ACTION_DELETE_STAGE,
|
ACTION_DELETE_STAGE,
|
||||||
ACTION_ADD_STAGE,
|
ACTION_ADD_STAGE,
|
||||||
ACTION_UPDATE_STAGE,
|
ACTION_UPDATE_STAGE,
|
||||||
|
ACTION_UPDATE_STAGE_POSITION,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
|
|
||||||
export function setWorkflows({ status, data }) {
|
export function setWorkflows({ status, data }) {
|
||||||
@ -40,3 +41,13 @@ export function updateStage(stageId, payload) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateStagePosition(oldIndex, newIndex) {
|
||||||
|
return {
|
||||||
|
type: ACTION_UPDATE_STAGE_POSITION,
|
||||||
|
payload: {
|
||||||
|
newIndex,
|
||||||
|
oldIndex,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,45 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Flex, Typography } from '@strapi/design-system';
|
||||||
|
import { CarretDown } from '@strapi/icons';
|
||||||
|
import { pxToRem } from '@strapi/helper-plugin';
|
||||||
|
|
||||||
|
const Toggle = styled(Flex)`
|
||||||
|
svg path {
|
||||||
|
fill: ${({ theme }) => theme.colors.neutral600};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function StageDragPreview({ name }) {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
background="primary100"
|
||||||
|
borderStyle="dashed"
|
||||||
|
borderColor="primary600"
|
||||||
|
borderWidth="1px"
|
||||||
|
gap={3}
|
||||||
|
hasRadius
|
||||||
|
padding={3}
|
||||||
|
shadow="tableShadow"
|
||||||
|
width={pxToRem(300)}
|
||||||
|
>
|
||||||
|
<Toggle
|
||||||
|
alignItems="center"
|
||||||
|
background="neutral200"
|
||||||
|
borderRadius="50%"
|
||||||
|
height={6}
|
||||||
|
justifyContent="center"
|
||||||
|
width={6}
|
||||||
|
>
|
||||||
|
<CarretDown width={`${8 / 16}rem`} />
|
||||||
|
</Toggle>
|
||||||
|
|
||||||
|
<Typography fontWeight="bold">{name}</Typography>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StageDragPreview.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './StageDragPreview';
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import * as React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@ -7,6 +7,7 @@ import {
|
|||||||
Accordion,
|
Accordion,
|
||||||
AccordionToggle,
|
AccordionToggle,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
|
Box,
|
||||||
Field,
|
Field,
|
||||||
FieldLabel,
|
FieldLabel,
|
||||||
FieldError,
|
FieldError,
|
||||||
@ -15,24 +16,151 @@ import {
|
|||||||
GridItem,
|
GridItem,
|
||||||
IconButton,
|
IconButton,
|
||||||
TextInput,
|
TextInput,
|
||||||
|
VisuallyHidden,
|
||||||
} from '@strapi/design-system';
|
} from '@strapi/design-system';
|
||||||
import { ReactSelect, useTracking } from '@strapi/helper-plugin';
|
import { ReactSelect, useTracking } from '@strapi/helper-plugin';
|
||||||
import { Trash } from '@strapi/icons';
|
import { Drag, Trash } from '@strapi/icons';
|
||||||
|
|
||||||
import { deleteStage, updateStage } from '../../../actions';
|
import { deleteStage, updateStagePosition, updateStage } from '../../../actions';
|
||||||
import { getAvailableStageColors } from '../../../utils/colors';
|
import { getAvailableStageColors } from '../../../utils/colors';
|
||||||
import { OptionColor } from './components/OptionColor';
|
import { OptionColor } from './components/OptionColor';
|
||||||
import { SingleValueColor } from './components/SingleValueColor';
|
import { SingleValueColor } from './components/SingleValueColor';
|
||||||
|
import { useDragAndDrop } from '../../../../../../../../../admin/src/content-manager/hooks';
|
||||||
|
import { composeRefs } from '../../../../../../../../../admin/src/content-manager/utils';
|
||||||
|
import { DRAG_DROP_TYPES } from '../../../constants';
|
||||||
|
|
||||||
const AVAILABLE_COLORS = getAvailableStageColors();
|
const AVAILABLE_COLORS = getAvailableStageColors();
|
||||||
|
|
||||||
export function Stage({ id, index, canDelete, isOpen: isOpenDefault = false }) {
|
function StageDropPreview() {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
background="primary100"
|
||||||
|
borderStyle="dashed"
|
||||||
|
borderColor="primary600"
|
||||||
|
borderWidth="1px"
|
||||||
|
display="block"
|
||||||
|
hasRadius
|
||||||
|
padding={6}
|
||||||
|
shadow="tableShadow"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Stage({
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
canDelete,
|
||||||
|
canReorder,
|
||||||
|
isOpen: isOpenDefault = false,
|
||||||
|
stagesCount,
|
||||||
|
}) {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} index
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const getItemPos = (index) => `${index + 1} of ${stagesCount}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} index
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const handleGrabStage = (index) => {
|
||||||
|
setLiveText(
|
||||||
|
formatMessage(
|
||||||
|
{
|
||||||
|
id: 'dnd.grab-item',
|
||||||
|
defaultMessage: `{item}, grabbed. Current position in list: {position}. Press up and down arrow to change position, Spacebar to drop, Escape to cancel.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item: nameField.value,
|
||||||
|
position: getItemPos(index),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} index
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const handleDropStage = (index) => {
|
||||||
|
setLiveText(
|
||||||
|
formatMessage(
|
||||||
|
{
|
||||||
|
id: 'dnd.drop-item',
|
||||||
|
defaultMessage: `{item}, dropped. Final position in list: {position}.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item: nameField.value,
|
||||||
|
position: getItemPos(index),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} index
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const handleCancelDragStage = () => {
|
||||||
|
setLiveText(
|
||||||
|
formatMessage(
|
||||||
|
{
|
||||||
|
id: 'dnd.cancel-item',
|
||||||
|
defaultMessage: '{item}, dropped. Re-order cancelled.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item: nameField.value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMoveStage = (newIndex, oldIndex) => {
|
||||||
|
setLiveText(
|
||||||
|
formatMessage(
|
||||||
|
{
|
||||||
|
id: 'dnd.reorder',
|
||||||
|
defaultMessage: '{item}, moved. New position in list: {position}.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item: nameField.value,
|
||||||
|
position: getItemPos(newIndex),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(updateStagePosition(oldIndex, newIndex));
|
||||||
|
};
|
||||||
|
|
||||||
|
const [liveText, setLiveText] = React.useState(null);
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { trackUsage } = useTracking();
|
const { trackUsage } = useTracking();
|
||||||
const [isOpen, setIsOpen] = useState(isOpenDefault);
|
const dispatch = useDispatch();
|
||||||
|
const [isOpen, setIsOpen] = React.useState(isOpenDefault);
|
||||||
const [nameField, nameMeta] = useField(`stages.${index}.name`);
|
const [nameField, nameMeta] = useField(`stages.${index}.name`);
|
||||||
const [colorField, colorMeta] = useField(`stages.${index}.color`);
|
const [colorField, colorMeta] = useField(`stages.${index}.color`);
|
||||||
const dispatch = useDispatch();
|
const [{ handlerId, isDragging, handleKeyDown }, stageRef, dropRef, dragRef] = useDragAndDrop(
|
||||||
|
canReorder,
|
||||||
|
{
|
||||||
|
index,
|
||||||
|
item: {
|
||||||
|
name: nameField.value,
|
||||||
|
},
|
||||||
|
onGrabItem: handleGrabStage,
|
||||||
|
onDropItem: handleDropStage,
|
||||||
|
onMoveItem: handleMoveStage,
|
||||||
|
onCancel: handleCancelDragStage,
|
||||||
|
type: DRAG_DROP_TYPES.STAGE,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const composedRef = composeRefs(stageRef, dropRef);
|
||||||
|
|
||||||
const colorOptions = AVAILABLE_COLORS.map(({ hex, name }) => ({
|
const colorOptions = AVAILABLE_COLORS.map(({ hex, name }) => ({
|
||||||
value: hex,
|
value: hex,
|
||||||
label: formatMessage(
|
label: formatMessage(
|
||||||
@ -46,91 +174,119 @@ export function Stage({ id, index, canDelete, isOpen: isOpenDefault = false }) {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion
|
<Box ref={composedRef}>
|
||||||
size="S"
|
{liveText && <VisuallyHidden aria-live="assertive">{liveText}</VisuallyHidden>}
|
||||||
variant="primary"
|
|
||||||
onToggle={() => {
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
|
|
||||||
if (!isOpen) {
|
{isDragging ? (
|
||||||
trackUsage('willEditStage');
|
<StageDropPreview />
|
||||||
}
|
) : (
|
||||||
}}
|
<Accordion
|
||||||
expanded={isOpen}
|
size="S"
|
||||||
shadow="tableShadow"
|
variant="primary"
|
||||||
>
|
onToggle={() => {
|
||||||
<AccordionToggle
|
setIsOpen(!isOpen);
|
||||||
title={nameField.value}
|
|
||||||
togglePosition="left"
|
|
||||||
action={
|
|
||||||
canDelete ? (
|
|
||||||
<IconButton
|
|
||||||
background="transparent"
|
|
||||||
noBorder
|
|
||||||
onClick={() => dispatch(deleteStage(id))}
|
|
||||||
label={formatMessage({
|
|
||||||
id: 'Settings.review-workflows.stage.delete',
|
|
||||||
defaultMessage: 'Delete stage',
|
|
||||||
})}
|
|
||||||
icon={<Trash />}
|
|
||||||
/>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<AccordionContent padding={6} background="neutral0" hasRadius>
|
|
||||||
<Grid gap={4}>
|
|
||||||
<GridItem col={6}>
|
|
||||||
<TextInput
|
|
||||||
{...nameField}
|
|
||||||
id={nameField.name}
|
|
||||||
label={formatMessage({
|
|
||||||
id: 'Settings.review-workflows.stage.name.label',
|
|
||||||
defaultMessage: 'Stage name',
|
|
||||||
})}
|
|
||||||
error={nameMeta.error ?? false}
|
|
||||||
onChange={(event) => {
|
|
||||||
nameField.onChange(event);
|
|
||||||
dispatch(updateStage(id, { name: event.target.value }));
|
|
||||||
}}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</GridItem>
|
|
||||||
|
|
||||||
<GridItem col={6}>
|
if (!isOpen) {
|
||||||
<Field
|
trackUsage('willEditStage');
|
||||||
error={colorMeta?.error ?? false}
|
}
|
||||||
name={colorField.name}
|
}}
|
||||||
id={colorField.name}
|
expanded={isOpen}
|
||||||
required
|
shadow="tableShadow"
|
||||||
>
|
>
|
||||||
<Flex direction="column" gap={1} alignItems="stretch">
|
<AccordionToggle
|
||||||
<FieldLabel>
|
title={nameField.value}
|
||||||
{formatMessage({
|
togglePosition="left"
|
||||||
id: 'content-manager.reviewWorkflows.stage.color',
|
action={
|
||||||
defaultMessage: 'Color',
|
<>
|
||||||
|
{canDelete && (
|
||||||
|
<IconButton
|
||||||
|
background="transparent"
|
||||||
|
icon={<Trash />}
|
||||||
|
label={formatMessage({
|
||||||
|
id: 'Settings.review-workflows.stage.delete',
|
||||||
|
defaultMessage: 'Delete stage',
|
||||||
|
})}
|
||||||
|
noBorder
|
||||||
|
onClick={() => dispatch(deleteStage(id))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
background="transparent"
|
||||||
|
forwardedAs="div"
|
||||||
|
role="button"
|
||||||
|
noBorder
|
||||||
|
tabIndex={0}
|
||||||
|
data-handler-id={handlerId}
|
||||||
|
ref={dragRef}
|
||||||
|
label={formatMessage({
|
||||||
|
id: 'Settings.review-workflows.stage.drag',
|
||||||
|
defaultMessage: 'Drag',
|
||||||
})}
|
})}
|
||||||
</FieldLabel>
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
<ReactSelect
|
>
|
||||||
components={{ Option: OptionColor, SingleValue: SingleValueColor }}
|
<Drag />
|
||||||
error={colorMeta?.error}
|
</IconButton>
|
||||||
inputId={colorField.name}
|
</>
|
||||||
name={colorField.name}
|
}
|
||||||
options={colorOptions}
|
/>
|
||||||
onChange={({ value }) => {
|
<AccordionContent padding={6} background="neutral0" hasRadius>
|
||||||
colorField.onChange({ target: { value } });
|
<Grid gap={4}>
|
||||||
dispatch(updateStage(id, { color: value }));
|
<GridItem col={6}>
|
||||||
|
<TextInput
|
||||||
|
{...nameField}
|
||||||
|
id={nameField.name}
|
||||||
|
label={formatMessage({
|
||||||
|
id: 'Settings.review-workflows.stage.name.label',
|
||||||
|
defaultMessage: 'Stage name',
|
||||||
|
})}
|
||||||
|
error={nameMeta.error ?? false}
|
||||||
|
onChange={(event) => {
|
||||||
|
nameField.onChange(event);
|
||||||
|
dispatch(updateStage(id, { name: event.target.value }));
|
||||||
}}
|
}}
|
||||||
value={colorOptions.find(({ value }) => value === colorField.value)}
|
required
|
||||||
/>
|
/>
|
||||||
|
</GridItem>
|
||||||
|
|
||||||
<FieldError />
|
<GridItem col={6}>
|
||||||
</Flex>
|
<Field
|
||||||
</Field>
|
error={colorMeta?.error ?? false}
|
||||||
</GridItem>
|
name={colorField.name}
|
||||||
</Grid>
|
id={colorField.name}
|
||||||
</AccordionContent>
|
required
|
||||||
</Accordion>
|
>
|
||||||
|
<Flex direction="column" gap={1} alignItems="stretch">
|
||||||
|
<FieldLabel>
|
||||||
|
{formatMessage({
|
||||||
|
id: 'content-manager.reviewWorkflows.stage.color',
|
||||||
|
defaultMessage: 'Color',
|
||||||
|
})}
|
||||||
|
</FieldLabel>
|
||||||
|
|
||||||
|
<ReactSelect
|
||||||
|
components={{ Option: OptionColor, SingleValue: SingleValueColor }}
|
||||||
|
error={colorMeta?.error}
|
||||||
|
inputId={colorField.name}
|
||||||
|
name={colorField.name}
|
||||||
|
options={colorOptions}
|
||||||
|
onChange={({ value }) => {
|
||||||
|
colorField.onChange({ target: { value } });
|
||||||
|
dispatch(updateStage(id, { color: value }));
|
||||||
|
}}
|
||||||
|
value={colorOptions.find(({ value }) => value === colorField.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FieldError />
|
||||||
|
</Flex>
|
||||||
|
</Field>
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
</AccordionContent>
|
||||||
|
</Accordion>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,4 +294,6 @@ Stage.propTypes = PropTypes.shape({
|
|||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.number.isRequired,
|
||||||
color: PropTypes.string.isRequired,
|
color: PropTypes.string.isRequired,
|
||||||
canDelete: PropTypes.bool.isRequired,
|
canDelete: PropTypes.bool.isRequired,
|
||||||
|
canReorder: PropTypes.bool.isRequired,
|
||||||
|
stagesCount: PropTypes.number.isRequired,
|
||||||
}).isRequired;
|
}).isRequired;
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import userEvent from '@testing-library/user-event';
|
|||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import { FormikProvider, useFormik } from 'formik';
|
import { FormikProvider, useFormik } from 'formik';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
import { DndProvider } from 'react-dnd';
|
||||||
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
|
||||||
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||||
|
|
||||||
@ -40,15 +42,17 @@ const ComponentFixture = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<FormikProvider value={formik}>
|
<Provider store={store}>
|
||||||
<IntlProvider locale="en" messages={{}}>
|
<FormikProvider value={formik}>
|
||||||
<ThemeProvider theme={lightTheme}>
|
<IntlProvider locale="en" messages={{}}>
|
||||||
<Stage {...STAGES_FIXTURE} {...props} />
|
<ThemeProvider theme={lightTheme}>
|
||||||
</ThemeProvider>
|
<Stage {...STAGES_FIXTURE} {...props} />
|
||||||
</IntlProvider>
|
</ThemeProvider>
|
||||||
</FormikProvider>
|
</IntlProvider>
|
||||||
</Provider>
|
</FormikProvider>
|
||||||
|
</Provider>
|
||||||
|
</DndProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -62,12 +66,13 @@ describe('Admin | Settings | Review Workflow | Stage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should render a stage', async () => {
|
it('should render a stage', async () => {
|
||||||
const { getByRole, getByText, queryByRole } = setup();
|
const { container, getByRole, getByText, queryByRole } = setup();
|
||||||
|
|
||||||
expect(queryByRole('textbox')).not.toBeInTheDocument();
|
expect(queryByRole('textbox')).not.toBeInTheDocument();
|
||||||
|
|
||||||
// open accordion
|
// open accordion; getByRole is not sufficient here, because the accordion
|
||||||
await user.click(getByRole('button'));
|
// does not have better identifiers
|
||||||
|
await user.click(container.querySelector('button[aria-expanded]'));
|
||||||
|
|
||||||
expect(queryByRole('textbox')).toBeInTheDocument();
|
expect(queryByRole('textbox')).toBeInTheDocument();
|
||||||
expect(getByRole('textbox').value).toBe('something');
|
expect(getByRole('textbox').value).toBe('something');
|
||||||
|
|||||||
@ -44,7 +44,14 @@ function Stages({ stages }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box key={`stage-${id}`} as="li">
|
<Box key={`stage-${id}`} as="li">
|
||||||
<Stage id={id} index={index} canDelete={stages.length > 1} isOpen={!stage.id} />
|
<Stage
|
||||||
|
id={id}
|
||||||
|
index={index}
|
||||||
|
canDelete={stages.length > 1}
|
||||||
|
isOpen={!stage.id}
|
||||||
|
canReorder={stages.length > 1}
|
||||||
|
stagesCount={stages.length}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import { IntlProvider } from 'react-intl';
|
|||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { FormikProvider, useFormik } from 'formik';
|
import { FormikProvider, useFormik } from 'formik';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { DndProvider } from 'react-dnd';
|
||||||
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
|
||||||
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||||
|
|
||||||
@ -59,15 +61,17 @@ const ComponentFixture = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<FormikProvider value={formik}>
|
<Provider store={store}>
|
||||||
<IntlProvider locale="en" messages={{}}>
|
<FormikProvider value={formik}>
|
||||||
<ThemeProvider theme={lightTheme}>
|
<IntlProvider locale="en" messages={{}}>
|
||||||
<Stages stages={STAGES_FIXTURE} {...props} />
|
<ThemeProvider theme={lightTheme}>
|
||||||
</ThemeProvider>
|
<Stages stages={STAGES_FIXTURE} {...props} />
|
||||||
</IntlProvider>
|
</ThemeProvider>
|
||||||
</FormikProvider>
|
</IntlProvider>
|
||||||
</Provider>
|
</FormikProvider>
|
||||||
|
</Provider>
|
||||||
|
</DndProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export const ACTION_SET_WORKFLOWS = `Settings/Review_Workflows/SET_WORKFLOWS`;
|
|||||||
export const ACTION_DELETE_STAGE = `Settings/Review_Workflows/WORKFLOW_DELETE_STAGE`;
|
export const ACTION_DELETE_STAGE = `Settings/Review_Workflows/WORKFLOW_DELETE_STAGE`;
|
||||||
export const ACTION_ADD_STAGE = `Settings/Review_Workflows/WORKFLOW_ADD_STAGE`;
|
export const ACTION_ADD_STAGE = `Settings/Review_Workflows/WORKFLOW_ADD_STAGE`;
|
||||||
export const ACTION_UPDATE_STAGE = `Settings/Review_Workflows/WORKFLOW_UPDATE_STAGE`;
|
export const ACTION_UPDATE_STAGE = `Settings/Review_Workflows/WORKFLOW_UPDATE_STAGE`;
|
||||||
|
export const ACTION_UPDATE_STAGE_POSITION = `Settings/Review_Workflows/WORKFLOW_UPDATE_STAGE_POSITION`;
|
||||||
|
|
||||||
export const STAGE_COLORS = {
|
export const STAGE_COLORS = {
|
||||||
primary600: 'Blue',
|
primary600: 'Blue',
|
||||||
@ -25,3 +26,7 @@ export const STAGE_COLORS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const STAGE_COLOR_DEFAULT = lightTheme.colors.primary600;
|
export const STAGE_COLOR_DEFAULT = lightTheme.colors.primary600;
|
||||||
|
|
||||||
|
export const DRAG_DROP_TYPES = {
|
||||||
|
STAGE: 'stage',
|
||||||
|
};
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
ACTION_DELETE_STAGE,
|
ACTION_DELETE_STAGE,
|
||||||
ACTION_ADD_STAGE,
|
ACTION_ADD_STAGE,
|
||||||
ACTION_UPDATE_STAGE,
|
ACTION_UPDATE_STAGE,
|
||||||
|
ACTION_UPDATE_STAGE_POSITION,
|
||||||
STAGE_COLOR_DEFAULT,
|
STAGE_COLOR_DEFAULT,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
|
|
||||||
@ -103,6 +104,27 @@ export function reducer(state = initialState, action) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ACTION_UPDATE_STAGE_POSITION: {
|
||||||
|
const {
|
||||||
|
currentWorkflow: {
|
||||||
|
data: { stages },
|
||||||
|
},
|
||||||
|
} = state.clientState;
|
||||||
|
const { newIndex, oldIndex } = payload;
|
||||||
|
|
||||||
|
if (newIndex >= 0 && newIndex < stages.length) {
|
||||||
|
const stage = stages[oldIndex];
|
||||||
|
let newStages = [...stages];
|
||||||
|
|
||||||
|
newStages.splice(oldIndex, 1);
|
||||||
|
newStages.splice(newIndex, 0, stage);
|
||||||
|
|
||||||
|
draft.clientState.currentWorkflow.data.stages = newStages;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
ACTION_DELETE_STAGE,
|
ACTION_DELETE_STAGE,
|
||||||
ACTION_ADD_STAGE,
|
ACTION_ADD_STAGE,
|
||||||
ACTION_UPDATE_STAGE,
|
ACTION_UPDATE_STAGE,
|
||||||
|
ACTION_UPDATE_STAGE_POSITION,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
|
|
||||||
const WORKFLOWS_FIXTURE = [
|
const WORKFLOWS_FIXTURE = [
|
||||||
@ -408,4 +409,112 @@ describe('Admin | Settings | Review Workflows | reducer', () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('ACTION_UPDATE_STAGE_POSITION', () => {
|
||||||
|
const action = {
|
||||||
|
type: ACTION_UPDATE_STAGE_POSITION,
|
||||||
|
payload: { oldIndex: 0, newIndex: 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
status: expect.any(String),
|
||||||
|
serverState: {
|
||||||
|
currentWorkflow: WORKFLOWS_FIXTURE[0],
|
||||||
|
},
|
||||||
|
clientState: {
|
||||||
|
currentWorkflow: {
|
||||||
|
data: WORKFLOWS_FIXTURE[0],
|
||||||
|
isDirty: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(reducer(state, action)).toStrictEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
clientState: expect.objectContaining({
|
||||||
|
currentWorkflow: expect.objectContaining({
|
||||||
|
data: expect.objectContaining({
|
||||||
|
stages: [
|
||||||
|
expect.objectContaining({ name: 'stage-2' }),
|
||||||
|
expect.objectContaining({ name: 'stage-1' }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
isDirty: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ACTION_UPDATE_STAGE_POSITION - does not update position if new index is smaller than 0', () => {
|
||||||
|
const action = {
|
||||||
|
type: ACTION_UPDATE_STAGE_POSITION,
|
||||||
|
payload: { oldIndex: 0, newIndex: -1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
status: expect.any(String),
|
||||||
|
serverState: {
|
||||||
|
currentWorkflow: WORKFLOWS_FIXTURE[0],
|
||||||
|
},
|
||||||
|
clientState: {
|
||||||
|
currentWorkflow: {
|
||||||
|
data: WORKFLOWS_FIXTURE[0],
|
||||||
|
isDirty: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(reducer(state, action)).toStrictEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
clientState: expect.objectContaining({
|
||||||
|
currentWorkflow: expect.objectContaining({
|
||||||
|
data: expect.objectContaining({
|
||||||
|
stages: [
|
||||||
|
expect.objectContaining({ name: 'stage-1' }),
|
||||||
|
expect.objectContaining({ name: 'stage-2' }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
isDirty: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ACTION_UPDATE_STAGE_POSITION - does not update position if new index is greater than the amount of stages', () => {
|
||||||
|
const action = {
|
||||||
|
type: ACTION_UPDATE_STAGE_POSITION,
|
||||||
|
payload: { oldIndex: 0, newIndex: 3 },
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
status: expect.any(String),
|
||||||
|
serverState: {
|
||||||
|
currentWorkflow: WORKFLOWS_FIXTURE[0],
|
||||||
|
},
|
||||||
|
clientState: {
|
||||||
|
currentWorkflow: {
|
||||||
|
data: WORKFLOWS_FIXTURE[0],
|
||||||
|
isDirty: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(reducer(state, action)).toStrictEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
clientState: expect.objectContaining({
|
||||||
|
currentWorkflow: expect.objectContaining({
|
||||||
|
data: expect.objectContaining({
|
||||||
|
stages: [
|
||||||
|
expect.objectContaining({ name: 'stage-1' }),
|
||||||
|
expect.objectContaining({ name: 'stage-2' }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
isDirty: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { rest } from 'msw';
|
|||||||
import { setupServer } from 'msw/node';
|
import { setupServer } from 'msw/node';
|
||||||
import { useNotification } from '@strapi/helper-plugin';
|
import { useNotification } from '@strapi/helper-plugin';
|
||||||
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||||
|
import { DndProvider } from 'react-dnd';
|
||||||
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
|
||||||
import configureStore from '../../../../../../../admin/src/core/store/configureStore';
|
import configureStore from '../../../../../../../admin/src/core/store/configureStore';
|
||||||
import ReviewWorkflowsPage from '..';
|
import ReviewWorkflowsPage from '..';
|
||||||
@ -65,15 +67,17 @@ const ComponentFixture = () => {
|
|||||||
const store = configureStore([], [reducer]);
|
const store = configureStore([], [reducer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={client}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<Provider store={store}>
|
<QueryClientProvider client={client}>
|
||||||
<IntlProvider locale="en" messages={{}}>
|
<Provider store={store}>
|
||||||
<ThemeProvider theme={lightTheme}>
|
<IntlProvider locale="en" messages={{}}>
|
||||||
<ReviewWorkflowsPage />
|
<ThemeProvider theme={lightTheme}>
|
||||||
</ThemeProvider>
|
<ReviewWorkflowsPage />
|
||||||
</IntlProvider>
|
</ThemeProvider>
|
||||||
</Provider>
|
</IntlProvider>
|
||||||
</QueryClientProvider>
|
</Provider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</DndProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user