mirror of
https://github.com/strapi/strapi.git
synced 2025-11-01 10:23:34 +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 { 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 { useReviewWorkflows } from './hooks/useReviewWorkflows';
|
||||
import { setWorkflows } from './actions';
|
||||
import { getWorkflowValidationSchema } from './utils/getWorkflowValidationSchema';
|
||||
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() {
|
||||
const { trackUsage } = useTracking();
|
||||
@ -135,6 +147,8 @@ export function ReviewWorkflowsPage() {
|
||||
})}
|
||||
/>
|
||||
<Main tabIndex={-1}>
|
||||
<DragLayer renderItem={renderDragLayerItem} />
|
||||
|
||||
<FormikProvider value={formik}>
|
||||
<Form onSubmit={formik.handleSubmit}>
|
||||
<HeaderLayout
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
ACTION_DELETE_STAGE,
|
||||
ACTION_ADD_STAGE,
|
||||
ACTION_UPDATE_STAGE,
|
||||
ACTION_UPDATE_STAGE_POSITION,
|
||||
} from '../constants';
|
||||
|
||||
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 { useField } from 'formik';
|
||||
import { useIntl } from 'react-intl';
|
||||
@ -7,6 +7,7 @@ import {
|
||||
Accordion,
|
||||
AccordionToggle,
|
||||
AccordionContent,
|
||||
Box,
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldError,
|
||||
@ -15,24 +16,151 @@ import {
|
||||
GridItem,
|
||||
IconButton,
|
||||
TextInput,
|
||||
VisuallyHidden,
|
||||
} from '@strapi/design-system';
|
||||
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 { OptionColor } from './components/OptionColor';
|
||||
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();
|
||||
|
||||
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 { trackUsage } = useTracking();
|
||||
const [isOpen, setIsOpen] = useState(isOpenDefault);
|
||||
const dispatch = useDispatch();
|
||||
const [isOpen, setIsOpen] = React.useState(isOpenDefault);
|
||||
const [nameField, nameMeta] = useField(`stages.${index}.name`);
|
||||
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 }) => ({
|
||||
value: hex,
|
||||
label: formatMessage(
|
||||
@ -46,91 +174,119 @@ export function Stage({ id, index, canDelete, isOpen: isOpenDefault = false }) {
|
||||
}));
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
size="S"
|
||||
variant="primary"
|
||||
onToggle={() => {
|
||||
setIsOpen(!isOpen);
|
||||
<Box ref={composedRef}>
|
||||
{liveText && <VisuallyHidden aria-live="assertive">{liveText}</VisuallyHidden>}
|
||||
|
||||
if (!isOpen) {
|
||||
trackUsage('willEditStage');
|
||||
}
|
||||
}}
|
||||
expanded={isOpen}
|
||||
shadow="tableShadow"
|
||||
>
|
||||
<AccordionToggle
|
||||
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>
|
||||
{isDragging ? (
|
||||
<StageDropPreview />
|
||||
) : (
|
||||
<Accordion
|
||||
size="S"
|
||||
variant="primary"
|
||||
onToggle={() => {
|
||||
setIsOpen(!isOpen);
|
||||
|
||||
<GridItem col={6}>
|
||||
<Field
|
||||
error={colorMeta?.error ?? false}
|
||||
name={colorField.name}
|
||||
id={colorField.name}
|
||||
required
|
||||
>
|
||||
<Flex direction="column" gap={1} alignItems="stretch">
|
||||
<FieldLabel>
|
||||
{formatMessage({
|
||||
id: 'content-manager.reviewWorkflows.stage.color',
|
||||
defaultMessage: 'Color',
|
||||
if (!isOpen) {
|
||||
trackUsage('willEditStage');
|
||||
}
|
||||
}}
|
||||
expanded={isOpen}
|
||||
shadow="tableShadow"
|
||||
>
|
||||
<AccordionToggle
|
||||
title={nameField.value}
|
||||
togglePosition="left"
|
||||
action={
|
||||
<>
|
||||
{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>
|
||||
|
||||
<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 }));
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Drag />
|
||||
</IconButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<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 }));
|
||||
}}
|
||||
value={colorOptions.find(({ value }) => value === colorField.value)}
|
||||
required
|
||||
/>
|
||||
</GridItem>
|
||||
|
||||
<FieldError />
|
||||
</Flex>
|
||||
</Field>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</AccordionContent>
|
||||
</Accordion>
|
||||
<GridItem col={6}>
|
||||
<Field
|
||||
error={colorMeta?.error ?? false}
|
||||
name={colorField.name}
|
||||
id={colorField.name}
|
||||
required
|
||||
>
|
||||
<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,
|
||||
color: PropTypes.string.isRequired,
|
||||
canDelete: PropTypes.bool.isRequired,
|
||||
canReorder: PropTypes.bool.isRequired,
|
||||
stagesCount: PropTypes.number.isRequired,
|
||||
}).isRequired;
|
||||
|
||||
@ -4,6 +4,8 @@ import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { FormikProvider, useFormik } from 'formik';
|
||||
import { Provider } from 'react-redux';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
|
||||
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||
|
||||
@ -40,15 +42,17 @@ const ComponentFixture = (props) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<FormikProvider value={formik}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<Stage {...STAGES_FIXTURE} {...props} />
|
||||
</ThemeProvider>
|
||||
</IntlProvider>
|
||||
</FormikProvider>
|
||||
</Provider>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Provider store={store}>
|
||||
<FormikProvider value={formik}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<Stage {...STAGES_FIXTURE} {...props} />
|
||||
</ThemeProvider>
|
||||
</IntlProvider>
|
||||
</FormikProvider>
|
||||
</Provider>
|
||||
</DndProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -62,12 +66,13 @@ describe('Admin | Settings | Review Workflow | Stage', () => {
|
||||
});
|
||||
|
||||
it('should render a stage', async () => {
|
||||
const { getByRole, getByText, queryByRole } = setup();
|
||||
const { container, getByRole, getByText, queryByRole } = setup();
|
||||
|
||||
expect(queryByRole('textbox')).not.toBeInTheDocument();
|
||||
|
||||
// open accordion
|
||||
await user.click(getByRole('button'));
|
||||
// open accordion; getByRole is not sufficient here, because the accordion
|
||||
// does not have better identifiers
|
||||
await user.click(container.querySelector('button[aria-expanded]'));
|
||||
|
||||
expect(queryByRole('textbox')).toBeInTheDocument();
|
||||
expect(getByRole('textbox').value).toBe('something');
|
||||
|
||||
@ -44,7 +44,14 @@ function Stages({ stages }) {
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -4,6 +4,8 @@ import { IntlProvider } from 'react-intl';
|
||||
import { Provider } from 'react-redux';
|
||||
import { FormikProvider, useFormik } from 'formik';
|
||||
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';
|
||||
|
||||
@ -59,15 +61,17 @@ const ComponentFixture = (props) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<FormikProvider value={formik}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<Stages stages={STAGES_FIXTURE} {...props} />
|
||||
</ThemeProvider>
|
||||
</IntlProvider>
|
||||
</FormikProvider>
|
||||
</Provider>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Provider store={store}>
|
||||
<FormikProvider value={formik}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<Stages stages={STAGES_FIXTURE} {...props} />
|
||||
</ThemeProvider>
|
||||
</IntlProvider>
|
||||
</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_ADD_STAGE = `Settings/Review_Workflows/WORKFLOW_ADD_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 = {
|
||||
primary600: 'Blue',
|
||||
@ -25,3 +26,7 @@ export const STAGE_COLORS = {
|
||||
};
|
||||
|
||||
export const STAGE_COLOR_DEFAULT = lightTheme.colors.primary600;
|
||||
|
||||
export const DRAG_DROP_TYPES = {
|
||||
STAGE: 'stage',
|
||||
};
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
ACTION_DELETE_STAGE,
|
||||
ACTION_ADD_STAGE,
|
||||
ACTION_UPDATE_STAGE,
|
||||
ACTION_UPDATE_STAGE_POSITION,
|
||||
STAGE_COLOR_DEFAULT,
|
||||
} from '../constants';
|
||||
|
||||
@ -103,6 +104,27 @@ export function reducer(state = initialState, action) {
|
||||
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:
|
||||
break;
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
ACTION_DELETE_STAGE,
|
||||
ACTION_ADD_STAGE,
|
||||
ACTION_UPDATE_STAGE,
|
||||
ACTION_UPDATE_STAGE_POSITION,
|
||||
} from '../../constants';
|
||||
|
||||
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 { useNotification } from '@strapi/helper-plugin';
|
||||
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 ReviewWorkflowsPage from '..';
|
||||
@ -65,15 +67,17 @@ const ComponentFixture = () => {
|
||||
const store = configureStore([], [reducer]);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={client}>
|
||||
<Provider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<ReviewWorkflowsPage />
|
||||
</ThemeProvider>
|
||||
</IntlProvider>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<QueryClientProvider client={client}>
|
||||
<Provider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<ReviewWorkflowsPage />
|
||||
</ThemeProvider>
|
||||
</IntlProvider>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</DndProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user