From 466035face3a0472fe8804e91d3d25854c3502a0 Mon Sep 17 00:00:00 2001 From: Marc-Roig Date: Mon, 24 Apr 2023 11:20:49 +0200 Subject: [PATCH 01/24] feat: limit number of stages to 200 --- .../core/admin/ee/server/validation/review-workflows.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/admin/ee/server/validation/review-workflows.js b/packages/core/admin/ee/server/validation/review-workflows.js index 4f67c8645f..af8464d83f 100644 --- a/packages/core/admin/ee/server/validation/review-workflows.js +++ b/packages/core/admin/ee/server/validation/review-workflows.js @@ -7,7 +7,12 @@ const stageObject = yup.object().shape({ name: yup.string().max(255).required(), }); -const validateUpdateStagesSchema = yup.array().of(stageObject).required(); +const validateUpdateStagesSchema = yup + .array() + .of(stageObject) + .required() + .max(200, 'You can not create more than 200 stages'); + const validateUpdateStageOnEntity = yup .object() .shape({ From 09e21070815dd1fad10071ca69f7fe2239a25a14 Mon Sep 17 00:00:00 2001 From: Marc-Roig Date: Mon, 24 Apr 2023 11:52:05 +0200 Subject: [PATCH 02/24] test: can not create more than 200 stages --- .../core/admin/ee/review-workflows.test.api.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/api-tests/core/admin/ee/review-workflows.test.api.js b/api-tests/core/admin/ee/review-workflows.test.api.js index c21cb4d975..23684987d0 100644 --- a/api-tests/core/admin/ee/review-workflows.test.api.js +++ b/api-tests/core/admin/ee/review-workflows.test.api.js @@ -344,6 +344,19 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { expect(workflowRes.body.data).toBeUndefined(); } }); + test('It should throw an error if trying to create more than 200 stages', async () => { + const stagesRes = await requests.admin.put( + `/admin/review-workflows/workflows/${testWorkflow.id}/stages`, + { body: { data: Array(201).fill({ name: 'new stage' }) } } + ); + + if (hasRW) { + expect(stagesRes.status).toBe(400); + expect(stagesRes.body.error).toBeDefined(); + expect(stagesRes.body.error.name).toEqual('ValidationError'); + expect(stagesRes.body.error.message).toBeDefined(); + } + }); }); describe('Enabling/Disabling review workflows on a content type', () => { @@ -407,7 +420,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { }); }); - describe('update a stage on an entity', () => { + describe('Update a stage on an entity', () => { describe('Review Workflow is enabled', () => { beforeAll(async () => { await updateContentType(productUID, { From 4cf5ab1a0cca1b49ab63d193c37007a170c6b5f3 Mon Sep 17 00:00:00 2001 From: Marc-Roig Date: Mon, 24 Apr 2023 13:28:18 +0200 Subject: [PATCH 03/24] feat: add stage color validation --- packages/core/admin/ee/server/validation/review-workflows.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/admin/ee/server/validation/review-workflows.js b/packages/core/admin/ee/server/validation/review-workflows.js index 4f67c8645f..f6690fd338 100644 --- a/packages/core/admin/ee/server/validation/review-workflows.js +++ b/packages/core/admin/ee/server/validation/review-workflows.js @@ -5,6 +5,10 @@ const { yup, validateYupSchema } = require('@strapi/utils'); const stageObject = yup.object().shape({ id: yup.number().integer().min(1), name: yup.string().max(255).required(), + color: yup + .string() + .matches(/^#(?:[0-9a-fA-F]{3}){1,2}$/i) + .nullable(), // hex color }); const validateUpdateStagesSchema = yup.array().of(stageObject).required(); From e7e30045243e053816beedc2917b43b5f5a09ac9 Mon Sep 17 00:00:00 2001 From: Marc-Roig Date: Mon, 24 Apr 2023 13:28:33 +0200 Subject: [PATCH 04/24] feat: add stage color attribute --- .../admin/ee/server/content-types/workflow-stage/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/admin/ee/server/content-types/workflow-stage/index.js b/packages/core/admin/ee/server/content-types/workflow-stage/index.js index 48ab4118ad..5f93eb69c9 100644 --- a/packages/core/admin/ee/server/content-types/workflow-stage/index.js +++ b/packages/core/admin/ee/server/content-types/workflow-stage/index.js @@ -24,6 +24,11 @@ module.exports = { type: 'string', configurable: false, }, + color: { + type: 'string', + configurable: false, + default: '#4945FF', + }, workflow: { type: 'relation', target: 'admin::workflow', From ec118b342d466a973f4567c55c8e0e9d98db7560 Mon Sep 17 00:00:00 2001 From: Marc-Roig Date: Mon, 24 Apr 2023 13:28:44 +0200 Subject: [PATCH 05/24] test: stage color --- .../core/admin/ee/review-workflows.test.api.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/api-tests/core/admin/ee/review-workflows.test.api.js b/api-tests/core/admin/ee/review-workflows.test.api.js index c21cb4d975..fbc1a3dc30 100644 --- a/api-tests/core/admin/ee/review-workflows.test.api.js +++ b/api-tests/core/admin/ee/review-workflows.test.api.js @@ -270,10 +270,25 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { ]; }); + test("It should assign a default color to stages if they don't have one", async () => { + await requests.admin.put(`/admin/review-workflows/workflows/${testWorkflow.id}/stages`, { + body: { + data: [defaultStage, { id: secondStage.id, name: 'new_name', color: '#000000' }], + }, + }); + + const workflowRes = await requests.admin.get( + `/admin/review-workflows/workflows/${testWorkflow.id}?populate=*` + ); + + expect(workflowRes.status).toBe(200); + expect(workflowRes.body.data.stages[0].color).toBe('#4945FF'); + expect(workflowRes.body.data.stages[1].color).toBe('#000000'); + }); test("It shouldn't be available for public", async () => { const stagesRes = await requests.public.put( `/admin/review-workflows/workflows/${testWorkflow.id}/stages`, - stagesUpdateData + { body: { data: stagesUpdateData } } ); const workflowRes = await requests.public.get( `/admin/review-workflows/workflows/${testWorkflow.id}` From 69cc9c5e8edc818d940fd18aab6be1cd850435c7 Mon Sep 17 00:00:00 2001 From: Marc-Roig Date: Mon, 24 Apr 2023 13:34:33 +0200 Subject: [PATCH 06/24] fix: stage color should not be nullabke --- packages/core/admin/ee/server/validation/review-workflows.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/admin/ee/server/validation/review-workflows.js b/packages/core/admin/ee/server/validation/review-workflows.js index f6690fd338..f47e2184fe 100644 --- a/packages/core/admin/ee/server/validation/review-workflows.js +++ b/packages/core/admin/ee/server/validation/review-workflows.js @@ -5,10 +5,7 @@ const { yup, validateYupSchema } = require('@strapi/utils'); const stageObject = yup.object().shape({ id: yup.number().integer().min(1), name: yup.string().max(255).required(), - color: yup - .string() - .matches(/^#(?:[0-9a-fA-F]{3}){1,2}$/i) - .nullable(), // hex color + color: yup.string().matches(/^#(?:[0-9a-fA-F]{3}){1,2}$/i), // hex color }); const validateUpdateStagesSchema = yup.array().of(stageObject).required(); From ef3d7ff09a0525310e52056a10f841ac5e931ac3 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Tue, 25 Apr 2023 17:40:48 +0200 Subject: [PATCH 07/24] Settings: Implement color select for stages --- .../components/Stages/Stage/Stage.js | 75 +++++++++++++++---- .../components/OptionColor/OptionColor.js | 29 +++++++ .../Stage/components/OptionColor/index.js | 1 + .../SingleValueColor/SingleValueColor.js | 27 +++++++ .../components/SingleValueColor/index.js | 1 + .../Stages/Stage/tests/Stage.test.js | 16 ++-- .../components/Stages/Stages.js | 8 +- .../components/Stages/tests/Stages.test.js | 4 +- .../pages/ReviewWorkflows/constants.js | 21 ++++++ .../pages/ReviewWorkflows/reducer/index.js | 16 +++- .../reducer/tests/index.test.js | 16 +++- .../tests/ReviewWorkflows.test.js | 30 +++++--- .../pages/ReviewWorkflows/utils/colors.js | 34 +++++++++ .../utils/getWorkflowValidationSchema.js | 9 +++ .../utils/tests/colors.test.js | 27 +++++++ 15 files changed, 275 insertions(+), 39 deletions(-) create mode 100644 packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/OptionColor.js create mode 100644 packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/index.js create mode 100644 packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/SingleValueColor.js create mode 100644 packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/index.js create mode 100644 packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js create mode 100644 packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js index 1b8900e739..38d4eccab1 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js @@ -7,23 +7,43 @@ import { Accordion, AccordionToggle, AccordionContent, + Field, + FieldLabel, + FieldError, + Flex, Grid, GridItem, IconButton, TextInput, } from '@strapi/design-system'; -import { useTracking } from '@strapi/helper-plugin'; +import { ReactSelect, useTracking } from '@strapi/helper-plugin'; import { Trash } from '@strapi/icons'; import { deleteStage, updateStage } from '../../../actions'; +import { getAvailableStageColors } from '../../../utils/colors'; +import { OptionColor } from './components/OptionColor'; +import { SingleValueColor } from './components/SingleValueColor'; -function Stage({ id, name, index, canDelete, isOpen: isOpenDefault = false }) { +const AVAILABLE_COLORS = getAvailableStageColors(); + +export function Stage({ id, index, canDelete, isOpen: isOpenDefault = false }) { const { formatMessage } = useIntl(); const { trackUsage } = useTracking(); const [isOpen, setIsOpen] = useState(isOpenDefault); - const fieldIdentifier = `stages.${index}.name`; - const [field, meta] = useField(fieldIdentifier); + const [nameField, nameMeta] = useField(`stages.${index}.name`); + const [colorField, colorMeta] = useField(`stages.${index}.color`); const dispatch = useDispatch(); + const colorOptions = AVAILABLE_COLORS.map(({ themeColorName, hex, name }) => ({ + value: hex, + label: formatMessage( + { + id: 'Settings.review-workflows.stage.color.name', + defaultMessage: '{name}', + }, + { name } + ), + themeColorName, + })); return ( { - field.onChange(event); + nameField.onChange(event); dispatch(updateStage(id, { name: event.target.value })); }} + required /> + + + + + + {formatMessage({ + id: 'content-manager.reviewWorkflows.stage.color', + defaultMessage: 'Color', + })} + + + { + colorField.onChange({ target: { value } }); + dispatch(updateStage(id, { color: value })); + }} + value={{ + value: colorField.value, + label: colorOptions.find(({ value }) => value === colorField.value).label, + }} + /> + + + + + ); } -export { Stage }; - Stage.propTypes = PropTypes.shape({ id: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, + color: PropTypes.string.isRequired, canDelete: PropTypes.bool.isRequired, }).isRequired; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/OptionColor.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/OptionColor.js new file mode 100644 index 0000000000..a2becdf3ea --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/OptionColor.js @@ -0,0 +1,29 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { components } from 'react-select'; +import { Box, Flex, Typography } from '@strapi/design-system'; + +export function OptionColor({ children, ...props }) { + const { value } = props.data; + + return ( + + + + + + {children} + + + + ); +} + +OptionColor.propTypes = { + children: PropTypes.node.isRequired, + data: PropTypes.shape({ + label: PropTypes.string, + themeColorName: PropTypes.string, + value: PropTypes.string, + }).isRequired, +}; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/index.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/index.js new file mode 100644 index 0000000000..492d0cce23 --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/index.js @@ -0,0 +1 @@ +export * from './OptionColor'; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/SingleValueColor.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/SingleValueColor.js new file mode 100644 index 0000000000..8be0b4a942 --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/SingleValueColor.js @@ -0,0 +1,27 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { components } from 'react-select'; +import { Box, Flex, Typography } from '@strapi/design-system'; + +export function SingleValueColor({ children, ...props }) { + const { value } = props.data; + + return ( + + + + + + {children} + + + + ); +} + +SingleValueColor.propTypes = { + children: PropTypes.node.isRequired, + data: PropTypes.shape({ + value: PropTypes.string, + }).isRequired, +}; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/index.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/index.js new file mode 100644 index 0000000000..e19756824f --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/index.js @@ -0,0 +1 @@ +export * from './SingleValueColor'; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/tests/Stage.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/tests/Stage.test.js index 3a216c561d..7e205f612c 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/tests/Stage.test.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/tests/Stage.test.js @@ -11,6 +11,8 @@ import configureStore from '../../../../../../../../../../admin/src/core/store/c import { Stage } from '../Stage'; import { reducer } from '../../../../reducer'; +import { STAGE_COLOR_DEFAULT } from '../../../../constants'; + jest.mock('@strapi/helper-plugin', () => ({ ...jest.requireActual('@strapi/helper-plugin'), useTracking: jest.fn().mockReturnValue({ trackUsage: jest.fn() }), @@ -18,8 +20,7 @@ jest.mock('@strapi/helper-plugin', () => ({ const STAGES_FIXTURE = { id: 1, - name: 'stage-1', - index: 1, + index: 0, }; const ComponentFixture = (props) => { @@ -30,6 +31,7 @@ const ComponentFixture = (props) => { initialValues: { stages: [ { + color: STAGE_COLOR_DEFAULT, name: 'something', }, ], @@ -60,15 +62,19 @@ describe('Admin | Settings | Review Workflow | Stage', () => { }); it('should render a stage', async () => { - const { getByRole, queryByRole } = setup(); + const { getByRole, getByText, queryByRole } = setup(); expect(queryByRole('textbox')).not.toBeInTheDocument(); + // open accordion await user.click(getByRole('button')); expect(queryByRole('textbox')).toBeInTheDocument(); - expect(getByRole('textbox').value).toBe(STAGES_FIXTURE.name); - expect(getByRole('textbox').getAttribute('name')).toBe('stages.1.name'); + expect(getByRole('textbox').value).toBe('something'); + expect(getByRole('textbox').getAttribute('name')).toBe('stages.0.name'); + + expect(getByText(/blue/i)).toBeInTheDocument(); + expect( queryByRole('button', { name: /delete stage/i, diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stages.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stages.js index 9a4060614b..e030090a71 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stages.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stages.js @@ -44,13 +44,7 @@ function Stages({ stages }) { return ( - 1} - isOpen={!stage.id} - /> + 1} isOpen={!stage.id} /> ); })} diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/tests/Stages.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/tests/Stages.test.js index 63ce038e77..b02015c66b 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/tests/Stages.test.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/tests/Stages.test.js @@ -10,7 +10,7 @@ import { ThemeProvider, lightTheme } from '@strapi/design-system'; import configureStore from '../../../../../../../../../admin/src/core/store/configureStore'; import { Stages } from '../Stages'; import { reducer } from '../../../reducer'; -import { ACTION_SET_WORKFLOWS } from '../../../constants'; +import { ACTION_SET_WORKFLOWS, STAGE_COLOR_DEFAULT } from '../../../constants'; import * as actions from '../../../actions'; // without mocking actions as ESM it is impossible to spy on named exports @@ -27,11 +27,13 @@ jest.mock('@strapi/helper-plugin', () => ({ const STAGES_FIXTURE = [ { id: 1, + color: STAGE_COLOR_DEFAULT, name: 'stage-1', }, { id: 2, + color: STAGE_COLOR_DEFAULT, name: 'stage-2', }, ]; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js index eac72aeb29..9b33e82ba2 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js @@ -1,6 +1,27 @@ +import { lightTheme } from '@strapi/design-system'; + export const REDUX_NAMESPACE = 'settings_review-workflows'; 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 STAGE_COLORS = { + primary600: 'Blue', + primary200: 'Lilac', + alternative600: 'Violet', + alternative200: 'Lavender', + success600: 'Green', + success200: 'Pale Green', + danger500: 'Cherry', + danger200: 'Pink', + warning600: 'Orange', + warning200: 'Yellow', + secondary600: 'Teal', + secondary200: 'Baby Blue', + neutral400: 'Gray', + neutral0: 'White', +}; + +export const STAGE_COLOR_DEFAULT = lightTheme.colors.primary600; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js index dedec82f3e..cf6b409251 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js @@ -6,6 +6,7 @@ import { ACTION_DELETE_STAGE, ACTION_ADD_STAGE, ACTION_UPDATE_STAGE, + STAGE_COLOR_DEFAULT, } from '../constants'; export const initialState = { @@ -29,8 +30,18 @@ export function reducer(state = initialState, action) { draft.status = status; - if (workflows) { - const defaultWorkflow = workflows[0]; + if (workflows?.length > 0) { + let defaultWorkflow = workflows[0]; + + // A safety net in case a stage does not have a color assigned; + // this normallly should not happen + defaultWorkflow = { + ...defaultWorkflow, + stages: defaultWorkflow.stages.map((stage) => ({ + ...stage, + color: stage?.color ?? STAGE_COLOR_DEFAULT, + })), + }; draft.serverState.workflows = workflows; draft.serverState.currentWorkflow = defaultWorkflow; @@ -69,6 +80,7 @@ export function reducer(state = initialState, action) { draft.clientState.currentWorkflow.data.stages.push({ ...payload, + color: payload?.color ?? STAGE_COLOR_DEFAULT, __temp_key__: newTempKey, }); diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/tests/index.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/tests/index.test.js index cef48abf9b..8aff6af4a3 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/tests/index.test.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/tests/index.test.js @@ -13,6 +13,7 @@ const WORKFLOWS_FIXTURE = [ stages: [ { id: 1, + color: 'red', name: 'stage-1', }, @@ -41,16 +42,26 @@ describe('Admin | Settings | Review Workflows | reducer', () => { payload: { status: 'loading-state', workflows: WORKFLOWS_FIXTURE }, }; + const DEFAULT_WORKFLOW_FIXTURE = { + ...WORKFLOWS_FIXTURE[0], + + // stages without a color should have a default color assigned + stages: WORKFLOWS_FIXTURE[0].stages.map((stage) => ({ + ...stage, + color: stage?.color ?? '#4945ff', + })), + }; + expect(reducer(state, action)).toStrictEqual( expect.objectContaining({ status: 'loading-state', serverState: expect.objectContaining({ - currentWorkflow: WORKFLOWS_FIXTURE[0], + currentWorkflow: DEFAULT_WORKFLOW_FIXTURE, workflows: WORKFLOWS_FIXTURE, }), clientState: expect.objectContaining({ currentWorkflow: expect.objectContaining({ - data: WORKFLOWS_FIXTURE[0], + data: DEFAULT_WORKFLOW_FIXTURE, isDirty: false, hasDeletedServerStages: false, }), @@ -338,6 +349,7 @@ describe('Admin | Settings | Review Workflows | reducer', () => { stages: expect.arrayContaining([ { id: 1, + color: 'red', name: 'stage-1-modified', }, ]), diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/tests/ReviewWorkflows.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/tests/ReviewWorkflows.test.js index 88639fb61f..378e806eb7 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/tests/ReviewWorkflows.test.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/tests/ReviewWorkflows.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { act, fireEvent, render } from '@testing-library/react'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; import { IntlProvider } from 'react-intl'; import { Provider } from 'react-redux'; import { QueryClientProvider, QueryClient } from 'react-query'; @@ -111,8 +111,10 @@ describe('Admin | Settings | Review Workflow | ReviewWorkflowsPage', () => { expect(queryByText('Workflow is loading')).not.toBeInTheDocument(); }); - test('display stages', () => { - const { getByText } = setup(); + test('display stages', async () => { + const { getByText, queryByText } = setup(); + + await waitFor(() => expect(queryByText('Workflow is loading')).not.toBeInTheDocument()); expect(getByText('1 stage')).toBeInTheDocument(); expect(getByText('stage-1')).toBeInTheDocument(); @@ -128,7 +130,9 @@ describe('Admin | Settings | Review Workflow | ReviewWorkflowsPage', () => { }); test('Save button is enabled after a stage has been added', async () => { - const { user, getByText, getByRole } = setup(); + const { user, getByText, getByRole, queryByText } = setup(); + + await waitFor(() => expect(queryByText('Workflow is loading')).not.toBeInTheDocument()); await user.click( getByRole('button', { @@ -144,7 +148,9 @@ describe('Admin | Settings | Review Workflow | ReviewWorkflowsPage', () => { test('Successful Stage update', async () => { const toggleNotification = useNotification(); - const { user, getByRole } = setup(); + const { user, getByRole, queryByText } = setup(); + + await waitFor(() => expect(queryByText('Workflow is loading')).not.toBeInTheDocument()); await user.click( getByRole('button', { @@ -169,7 +175,9 @@ describe('Admin | Settings | Review Workflow | ReviewWorkflowsPage', () => { test('Stage update with error', async () => { SHOULD_ERROR = true; const toggleNotification = useNotification(); - const { user, getByRole } = setup(); + const { user, getByRole, queryByText } = setup(); + + await waitFor(() => expect(queryByText('Workflow is loading')).not.toBeInTheDocument()); await user.click( getByRole('button', { @@ -191,14 +199,18 @@ describe('Admin | Settings | Review Workflow | ReviewWorkflowsPage', () => { }); }); - test('Does not show a delete button if only stage is left', () => { - const { queryByRole } = setup(); + test('Does not show a delete button if only stage is left', async () => { + const { queryByRole, queryByText } = setup(); + + await waitFor(() => expect(queryByText('Workflow is loading')).not.toBeInTheDocument()); expect(queryByRole('button', { name: /delete stage/i })).not.toBeInTheDocument(); }); test('Show confirmation dialog when a stage was deleted', async () => { - const { user, getByRole, getAllByRole } = setup(); + const { user, getByRole, getAllByRole, queryByText } = setup(); + + await waitFor(() => expect(queryByText('Workflow is loading')).not.toBeInTheDocument()); await user.click( getByRole('button', { diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js new file mode 100644 index 0000000000..a6d5f821f0 --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js @@ -0,0 +1,34 @@ +import { lightTheme } from '@strapi/design-system'; + +import { STAGE_COLORS } from '../constants'; + +export function getStageColorByHex(hex) { + // there are multiple colors with the same hex code in the design tokens. In order to find + // the correct one we have to find all matching colors and then check, which ones are usable + // for stages. + const themeColors = Object.entries(lightTheme.colors).filter(([, value]) => value === hex); + const themeColorName = themeColors.reduce((acc, [name]) => { + if (STAGE_COLORS?.[name]) { + acc = name; + } + + return acc; + }, null); + + if (!themeColorName) { + return null; + } + + return { + themeColorName, + name: STAGE_COLORS[themeColorName], + }; +} + +export function getAvailableStageColors() { + return Object.entries(STAGE_COLORS).map(([themeColorName, name]) => ({ + hex: lightTheme.colors[themeColorName], + themeColorName, + name, + })); +} diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/getWorkflowValidationSchema.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/getWorkflowValidationSchema.js index ed87817568..164a2eeb96 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/getWorkflowValidationSchema.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/getWorkflowValidationSchema.js @@ -19,6 +19,15 @@ export function getWorkflowValidationSchema({ formatMessage }) { defaultMessage: 'Name can not be longer than 255 characters', }) ), + color: yup + .string() + .required( + formatMessage({ + id: 'Settings.review-workflows.validation.stage.color', + defaultMessage: 'Color is required', + }) + ) + .matches(/^#(?:[0-9a-fA-F]{3}){1,2}$/i), }) ), }); diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js new file mode 100644 index 0000000000..6e31b2d3c8 --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js @@ -0,0 +1,27 @@ +import { getAvailableStageColors, getStageColorByHex } from '../colors'; + +describe('Settings | Review Workflows | colors', () => { + test('getAvailableStageColors()', () => { + const colors = getAvailableStageColors(); + + expect(colors.length).toBe(14); + + colors.forEach((color) => { + expect(color).toMatchObject({ + hex: expect.any(String), + themeColorName: expect.any(String), + name: expect.any(String), + }); + }); + }); + + test('getStageColorByHex()', () => { + expect(getStageColorByHex('#4945ff')).toStrictEqual({ + name: 'Blue', + themeColorName: 'primary600', + }); + + expect(getStageColorByHex('random')).toStrictEqual(null); + expect(getStageColorByHex()).toStrictEqual(null); + }); +}); From 19855acfa41e06b79ccaa54adcad66c67faf67df Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Tue, 25 Apr 2023 18:05:15 +0200 Subject: [PATCH 08/24] Settings: Fix Field <> ReactSelect connection --- .../ReviewWorkflows/components/Stages/Stage/Stage.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js index 38d4eccab1..716a99605b 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js @@ -97,7 +97,12 @@ export function Stage({ id, index, canDelete, isOpen: isOpenDefault = false }) { - + {formatMessage({ @@ -109,7 +114,7 @@ export function Stage({ id, index, canDelete, isOpen: isOpenDefault = false }) { { From 2c6624f64ffa287e89cde8cd231d572207e6d8ca Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Wed, 26 Apr 2023 10:15:47 +0200 Subject: [PATCH 09/24] Chore: Update reducer tests --- .../pages/ReviewWorkflows/reducer/tests/index.test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/tests/index.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/tests/index.test.js index 8aff6af4a3..544c8e8b6f 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/tests/index.test.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/tests/index.test.js @@ -236,6 +236,7 @@ describe('Admin | Settings | Review Workflows | reducer', () => { stages: expect.arrayContaining([ { __temp_key__: 3, + color: '#4945ff', name: 'something', }, ]), @@ -268,6 +269,7 @@ describe('Admin | Settings | Review Workflows | reducer', () => { stages: expect.arrayContaining([ { __temp_key__: 0, + color: expect.any(String), name: 'something', }, ]), @@ -317,6 +319,7 @@ describe('Admin | Settings | Review Workflows | reducer', () => { stages: expect.arrayContaining([ { __temp_key__: 4, + color: expect.any(String), name: 'something', }, ]), From ac89f99b1d6ad7506df116bee774c042ab095fb4 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Wed, 26 Apr 2023 10:53:15 +0200 Subject: [PATCH 10/24] CM: Display stage colors in the edit view sidebar --- .../InformationBox/InformationBoxEE.js | 18 ++++++++++++++++-- .../tests/InformationBoxEE.test.js | 1 + .../components/Stages/Stage/Stage.js | 9 +++------ .../components/OptionColor/OptionColor.js | 8 +++----- .../SingleValueColor/SingleValueColor.js | 12 ++++++++---- .../pages/ReviewWorkflows/utils/colors.js | 1 - .../ReviewWorkflows/utils/tests/colors.test.js | 1 - 7 files changed, 31 insertions(+), 19 deletions(-) diff --git a/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js b/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js index c5235c0862..b6440c9742 100644 --- a/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js +++ b/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js @@ -11,6 +11,8 @@ import { useIntl } from 'react-intl'; import { useMutation } from 'react-query'; import { useReviewWorkflows } from '../../../../pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows'; +import { OptionColor } from '../../../../pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor'; +import { SingleValueColor } from '../../../../pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor'; import Information from '../../../../../../admin/src/content-manager/pages/EditView/Information'; const ATTRIBUTE_NAME = 'strapi_reviewWorkflows_stage'; @@ -113,6 +115,8 @@ export function InformationBoxEE() { , + Option: OptionColor, + SingleValue: SingleValueColor, }} error={formattedError} inputId={ATTRIBUTE_NAME} @@ -122,9 +126,19 @@ export function InformationBoxEE() { name={ATTRIBUTE_NAME} onChange={handleStageChange} options={ - workflow ? workflow.stages.map(({ id, name }) => ({ value: id, label: name })) : [] + workflow + ? workflow.stages.map(({ id, color, name }) => ({ + value: id, + label: name, + color, + })) + : [] } - value={{ value: activeWorkflowStage?.id, label: activeWorkflowStage?.name }} + value={{ + value: activeWorkflowStage?.id, + label: activeWorkflowStage?.name, + color: activeWorkflowStage?.color, + }} /> diff --git a/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/tests/InformationBoxEE.test.js b/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/tests/InformationBoxEE.test.js index c480d80817..f433fcafa0 100644 --- a/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/tests/InformationBoxEE.test.js +++ b/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/tests/InformationBoxEE.test.js @@ -12,6 +12,7 @@ import { InformationBoxEE } from '../InformationBoxEE'; const STAGE_ATTRIBUTE_NAME = 'strapi_reviewWorkflows_stage'; const STAGE_FIXTURE = { id: 1, + color: 'red', name: 'Stage 1', worklow: 1, }; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js index 716a99605b..6b4061ae5a 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js @@ -33,7 +33,7 @@ export function Stage({ id, index, canDelete, isOpen: isOpenDefault = false }) { const [nameField, nameMeta] = useField(`stages.${index}.name`); const [colorField, colorMeta] = useField(`stages.${index}.color`); const dispatch = useDispatch(); - const colorOptions = AVAILABLE_COLORS.map(({ themeColorName, hex, name }) => ({ + const colorOptions = AVAILABLE_COLORS.map(({ hex, name }) => ({ value: hex, label: formatMessage( { @@ -42,7 +42,7 @@ export function Stage({ id, index, canDelete, isOpen: isOpenDefault = false }) { }, { name } ), - themeColorName, + color: hex, })); return ( @@ -121,10 +121,7 @@ export function Stage({ id, index, canDelete, isOpen: isOpenDefault = false }) { colorField.onChange({ target: { value } }); dispatch(updateStage(id, { color: value })); }} - value={{ - value: colorField.value, - label: colorOptions.find(({ value }) => value === colorField.value).label, - }} + value={colorOptions.find(({ value }) => value === colorField.value)} /> diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/OptionColor.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/OptionColor.js index a2becdf3ea..6700aa79fa 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/OptionColor.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/OptionColor.js @@ -4,12 +4,12 @@ import { components } from 'react-select'; import { Box, Flex, Typography } from '@strapi/design-system'; export function OptionColor({ children, ...props }) { - const { value } = props.data; + const { color } = props.data; return ( - + {children} @@ -22,8 +22,6 @@ export function OptionColor({ children, ...props }) { OptionColor.propTypes = { children: PropTypes.node.isRequired, data: PropTypes.shape({ - label: PropTypes.string, - themeColorName: PropTypes.string, - value: PropTypes.string, + color: PropTypes.string, }).isRequired, }; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/SingleValueColor.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/SingleValueColor.js index 8be0b4a942..6fcc13bee0 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/SingleValueColor.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/SingleValueColor.js @@ -4,12 +4,12 @@ import { components } from 'react-select'; import { Box, Flex, Typography } from '@strapi/design-system'; export function SingleValueColor({ children, ...props }) { - const { value } = props.data; + const { color } = props.data; return ( - + {children} @@ -19,9 +19,13 @@ export function SingleValueColor({ children, ...props }) { ); } +SingleValueColor.defaultProps = { + children: null, +}; + SingleValueColor.propTypes = { - children: PropTypes.node.isRequired, + children: PropTypes.node, data: PropTypes.shape({ - value: PropTypes.string, + color: PropTypes.string, }).isRequired, }; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js index a6d5f821f0..e33a85d796 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js @@ -28,7 +28,6 @@ export function getStageColorByHex(hex) { export function getAvailableStageColors() { return Object.entries(STAGE_COLORS).map(([themeColorName, name]) => ({ hex: lightTheme.colors[themeColorName], - themeColorName, name, })); } diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js index 6e31b2d3c8..746dc1ee8b 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js @@ -9,7 +9,6 @@ describe('Settings | Review Workflows | colors', () => { colors.forEach((color) => { expect(color).toMatchObject({ hex: expect.any(String), - themeColorName: expect.any(String), name: expect.any(String), }); }); From 476821033f4324a53fc0a10e258d06bae8e3c2dc Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Thu, 27 Apr 2023 17:10:14 +0200 Subject: [PATCH 11/24] CM: Allow list-view to be sorted by stages --- .../ReviewWorkflowsStage/getTableColumn.js | 5 +- .../DynamicTable/TableHead/index.js | 102 ++++++++++-------- 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js b/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js index 1390d99bb9..7c89948127 100644 --- a/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +++ b/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js @@ -24,7 +24,7 @@ export default (layout) => { key: '__strapi_reviewWorkflows_stage_temp_key__', name: 'strapi_reviewWorkflows_stage', fieldSchema: { - type: 'custom', + type: 'relation', }, metadatas: { label: formatMessage({ @@ -32,7 +32,8 @@ export default (layout) => { defaultMessage: 'Review stage', }), searchable: false, - sortable: false, + sortable: true, + mainField: 'name', }, cellFormatter({ strapi_reviewWorkflows_stage }) { // if entities are created e.g. through lifecycle methods diff --git a/packages/core/helper-plugin/src/components/DynamicTable/TableHead/index.js b/packages/core/helper-plugin/src/components/DynamicTable/TableHead/index.js index bd0fba0ee5..2d405c3413 100644 --- a/packages/core/helper-plugin/src/components/DynamicTable/TableHead/index.js +++ b/packages/core/helper-plugin/src/components/DynamicTable/TableHead/index.js @@ -26,7 +26,6 @@ const TableHead = ({ const [{ query }, setQuery] = useQueryParams(); const sort = query?.sort || ''; const [sortBy, sortOrder] = sort.split(':'); - const isIndeterminate = !areAllEntriesSelected && entriesToDelete.length > 0; return ( @@ -45,54 +44,67 @@ const TableHead = ({ /> )} - {headers.map(({ name, metadatas: { sortable: isSortable, label } }) => { - const isSorted = sortBy === name; - const isUp = sortOrder === 'ASC'; + {headers.map( + ({ fieldSchema, name, metadatas: { sortable: isSortable, label, mainField } }) => { + let isSorted = sortBy === name; + const isUp = sortOrder === 'ASC'; - const sortLabel = formatMessage( - { id: 'components.TableHeader.sort', defaultMessage: 'Sort on {label}' }, - { label } - ); - - const handleClickSort = (shouldAllowClick = true) => { - if (isSortable && shouldAllowClick) { - const nextSortOrder = isSorted && sortOrder === 'ASC' ? 'DESC' : 'ASC'; - const nextSort = `${name}:${nextSortOrder}`; - - setQuery({ - sort: nextSort, - }); + // relations always have to be sorted by their main field instead of only the + // attribute name; sortBy e.g. looks like: &sortBy=attributeName[mainField]:ASC + if (fieldSchema?.type === 'relation' && mainField) { + isSorted = sortBy === `${name}[${mainField}]`; } - }; - return ( - } - noBorder - /> - ) + const sortLabel = formatMessage( + { id: 'components.TableHeader.sort', defaultMessage: 'Sort on {label}' }, + { label } + ); + + const handleClickSort = (shouldAllowClick = true) => { + if (isSortable && shouldAllowClick) { + let nextSort = name; + + // relations always have to be sorted by their main field instead of only the + // attribute name; nextSort e.g. looks like: &nextSort=attributeName[mainField]:ASC + if (fieldSchema?.type === 'relation' && mainField) { + nextSort = `${name}[${mainField}]`; + } + + setQuery({ + sort: `${nextSort}:${isSorted && sortOrder === 'ASC' ? 'DESC' : 'ASC'}`, + }); } - > - - handleClickSort(!isSorted)} - variant="sigma" - > - {label} - - - - ); - })} + }; + + return ( + } + noBorder + /> + ) + } + > + + handleClickSort(!isSorted)} + variant="sigma" + > + {label} + + + + ); + } + )} {withBulkActions && ( From 9978e0fda3add2d0cafd656c03c81ea0ccc91216 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Wed, 26 Apr 2023 14:15:41 +0200 Subject: [PATCH 12/24] Settings: Add drag and drop to allow reodering of stages --- .../pages/ReviewWorkflows/ReviewWorkflows.js | 16 +- .../pages/ReviewWorkflows/actions/index.js | 11 + .../StageDragPreview/StageDragPreview.js | 45 +++ .../components/StageDragPreview/index.js | 1 + .../components/Stages/Stage/Stage.js | 328 +++++++++++++----- .../components/Stages/Stages.js | 9 +- .../pages/ReviewWorkflows/constants.js | 5 + .../pages/ReviewWorkflows/reducer/index.js | 22 ++ .../reducer/tests/index.test.js | 109 ++++++ 9 files changed, 459 insertions(+), 87 deletions(-) create mode 100644 packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/StageDragPreview/StageDragPreview.js create mode 100644 packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/StageDragPreview/index.js diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js index 44e729e23b..c8a3ec61c3 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js @@ -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 ; + + default: + return null; + } +} export function ReviewWorkflowsPage() { const { trackUsage } = useTracking(); @@ -135,6 +147,8 @@ export function ReviewWorkflowsPage() { })} />
+ +
theme.colors.neutral600}; + } +`; + +export function StageDragPreview({ name }) { + return ( + + + + + + {name} + + ); +} + +StageDragPreview.propTypes = { + name: PropTypes.string.isRequired, +}; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/StageDragPreview/index.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/StageDragPreview/index.js new file mode 100644 index 0000000000..a771750e68 --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/StageDragPreview/index.js @@ -0,0 +1 @@ +export * from './StageDragPreview'; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js index 6b4061ae5a..9985f3be06 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js @@ -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 ( + + ); +} + +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 ( - { - setIsOpen(!isOpen); + + {liveText && {liveText}} - if (!isOpen) { - trackUsage('willEditStage'); - } - }} - expanded={isOpen} - shadow="tableShadow" - > - dispatch(deleteStage(id))} - label={formatMessage({ - id: 'Settings.review-workflows.stage.delete', - defaultMessage: 'Delete stage', - })} - icon={} - /> - ) : null - } - /> - - - - { - nameField.onChange(event); - dispatch(updateStage(id, { name: event.target.value })); - }} - required - /> - + {isDragging ? ( + + ) : ( + { + setIsOpen(!isOpen); - - - - - {formatMessage({ - id: 'content-manager.reviewWorkflows.stage.color', - defaultMessage: 'Color', + if (!isOpen) { + trackUsage('willEditStage'); + } + }} + expanded={isOpen} + shadow="tableShadow" + > + + {canDelete && ( + } + label={formatMessage({ + id: 'Settings.review-workflows.stage.delete', + defaultMessage: 'Delete stage', + })} + noBorder + onClick={() => dispatch(deleteStage(id))} + /> + )} + + - - { - colorField.onChange({ target: { value } }); - dispatch(updateStage(id, { color: value })); + onClick={(e) => e.stopPropagation()} + onKeyDown={handleKeyDown} + > + + + + } + /> + + + + { + nameField.onChange(event); + dispatch(updateStage(id, { name: event.target.value })); }} - value={colorOptions.find(({ value }) => value === colorField.value)} + required /> + - - - - - - - + + + + + {formatMessage({ + id: 'content-manager.reviewWorkflows.stage.color', + defaultMessage: 'Color', + })} + + + { + colorField.onChange({ target: { value } }); + dispatch(updateStage(id, { color: value })); + }} + value={colorOptions.find(({ value }) => value === colorField.value)} + /> + + + + + + + + + )} + ); } @@ -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; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stages.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stages.js index e030090a71..3bb55d8459 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stages.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stages.js @@ -44,7 +44,14 @@ function Stages({ stages }) { return ( - 1} isOpen={!stage.id} /> + 1} + isOpen={!stage.id} + canReorder={stages.length > 1} + stagesCount={stages.length} + /> ); })} diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js index 9b33e82ba2..388ab4d8a1 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js @@ -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', +}; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js index cf6b409251..7a9d6db2bc 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js @@ -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; } diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/tests/index.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/tests/index.test.js index 544c8e8b6f..60563a58a9 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/tests/index.test.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/tests/index.test.js @@ -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, + }), + }), + }) + ); + }); }); From 12638795e9eb38ca18fcd089c48984feeb424727 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Fri, 28 Apr 2023 12:51:16 +0200 Subject: [PATCH 13/24] Chore: Update tests --- .../Stages/Stage/tests/Stage.test.js | 29 +++++++++++-------- .../components/Stages/tests/Stages.test.js | 22 ++++++++------ .../tests/ReviewWorkflows.test.js | 22 ++++++++------ 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/tests/Stage.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/tests/Stage.test.js index 7e205f612c..2607a5f2d3 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/tests/Stage.test.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/tests/Stage.test.js @@ -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 ( - - - - - - - - - + + + + + + + + + + + ); }; @@ -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'); diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/tests/Stages.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/tests/Stages.test.js index b02015c66b..c6dc9bb58f 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/tests/Stages.test.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/tests/Stages.test.js @@ -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 ( - - - - - - - - - + + + + + + + + + + + ); }; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/tests/ReviewWorkflows.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/tests/ReviewWorkflows.test.js index 34da591caa..b279883011 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/tests/ReviewWorkflows.test.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/tests/ReviewWorkflows.test.js @@ -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 ( - - - - - - - - - + + + + + + + + + + + ); }; From 2cba0325e0e221043c47427802f3c2098b8272b8 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Wed, 26 Apr 2023 11:16:46 +0200 Subject: [PATCH 14/24] CM: Render stage color in the list-view --- .../ReviewWorkflowsStageEE.js | 15 ++++++++++----- .../ReviewWorkflowsStage/getTableColumn.js | 5 ++++- .../tests/ReviewWorkflowsStage.test.js | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/ReviewWorkflowsStageEE.js b/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/ReviewWorkflowsStageEE.js index 28683acf4c..224f83db6d 100644 --- a/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/ReviewWorkflowsStageEE.js +++ b/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/ReviewWorkflowsStageEE.js @@ -1,15 +1,20 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Typography } from '@strapi/design-system'; +import { Box, Flex, Typography } from '@strapi/design-system'; -export function ReviewWorkflowsStageEE({ name }) { +export function ReviewWorkflowsStageEE({ color, name }) { return ( - - {name} - + + + + + {name} + + ); } ReviewWorkflowsStageEE.propTypes = { + color: PropTypes.string.isRequired, name: PropTypes.string.isRequired, }; diff --git a/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js b/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js index 1390d99bb9..0ba49b8eab 100644 --- a/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +++ b/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js @@ -4,6 +4,7 @@ import { Typography } from '@strapi/design-system'; import ReviewWorkflowsStage from '.'; import getTrad from '../../../../../../../admin/src/content-manager/utils/getTrad'; +import { STAGE_COLOR_DEFAULT } from '../../../../../pages/SettingsPage/pages/ReviewWorkflows/constants'; export default (layout) => { const { formatMessage } = useIntl(); @@ -41,7 +42,9 @@ export default (layout) => { return -; } - return ; + const { color, name } = strapi_reviewWorkflows_stage; + + return ; }, }; }; diff --git a/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/tests/ReviewWorkflowsStage.test.js b/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/tests/ReviewWorkflowsStage.test.js index 8d17245507..36a9e30e59 100644 --- a/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/tests/ReviewWorkflowsStage.test.js +++ b/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/tests/ReviewWorkflowsStage.test.js @@ -17,7 +17,7 @@ const setup = (props) => render(); describe('DynamicTable | ReviewWorkflowsStage', () => { test('render stage name', () => { - const { getByText } = setup({ name: 'reviewed' }); + const { getByText } = setup({ color: 'red', name: 'reviewed' }); expect(getByText('reviewed')).toBeInTheDocument(); }); From eb4764ca27fd0f4d51b283f9bd9aa6468eaf4d5e Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Fri, 28 Apr 2023 16:40:09 +0200 Subject: [PATCH 15/24] Settings: Normalize color hex to be lowercase; display custom values --- .../components/Stages/Stage/Stage.js | 15 ++++++++++++++- .../pages/ReviewWorkflows/utils/colors.js | 2 +- .../ReviewWorkflows/utils/tests/colors.test.js | 2 ++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js index 9985f3be06..2497d29a46 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js @@ -172,6 +172,7 @@ export function Stage({ ), color: hex, })); + const colorValue = colorOptions.find(({ value }) => value === colorField.value.toLowerCase()); return ( @@ -275,7 +276,19 @@ export function Stage({ colorField.onChange({ target: { value } }); dispatch(updateStage(id, { color: value })); }} - value={colorOptions.find(({ value }) => value === colorField.value)} + // If no color was found in all the valid theme colors it means a user + // has set a custom value e.g. through the content API. In that case we + // display the custom color and a "Custom" label. + value={ + colorValue ?? { + value: colorField.value, + label: formatMessage({ + id: 'Settings.review-workflows.stage.color.name.custom', + defaultMessage: 'Custom', + }), + color: colorField.value, + } + } /> diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js index e33a85d796..2133f70486 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js @@ -27,7 +27,7 @@ export function getStageColorByHex(hex) { export function getAvailableStageColors() { return Object.entries(STAGE_COLORS).map(([themeColorName, name]) => ({ - hex: lightTheme.colors[themeColorName], + hex: lightTheme.colors[themeColorName].toLowerCase(), name, })); } diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js index 746dc1ee8b..3589e7d0b2 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js @@ -11,6 +11,8 @@ describe('Settings | Review Workflows | colors', () => { hex: expect.any(String), name: expect.any(String), }); + + expect(color.hex).toBe(color.hex.toLowerCase()); }); }); From e114800c2d88482333bce3d21c3909714a3b90da Mon Sep 17 00:00:00 2001 From: Marc-Roig Date: Fri, 28 Apr 2023 16:58:43 +0200 Subject: [PATCH 16/24] fix: update stage if only changes its color --- .../core/admin/ee/server/services/review-workflows/stages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/admin/ee/server/services/review-workflows/stages.js b/packages/core/admin/ee/server/services/review-workflows/stages.js index 520e1fb6fd..235bb7edb6 100644 --- a/packages/core/admin/ee/server/services/review-workflows/stages.js +++ b/packages/core/admin/ee/server/services/review-workflows/stages.js @@ -208,7 +208,7 @@ function getDiffBetweenStages(sourceStages, comparisonStages) { if (!srcStage) { acc.created.push(stageToCompare); - } else if (srcStage.name !== stageToCompare.name) { + } else if (srcStage.name !== stageToCompare.name || srcStage.color !== stageToCompare.color) { acc.updated.push(stageToCompare); } return acc; From b544fe622f5619f46228309925d2fc1036acbce8 Mon Sep 17 00:00:00 2001 From: Marc-Roig Date: Fri, 28 Apr 2023 16:58:57 +0200 Subject: [PATCH 17/24] test: change only color of a stage --- api-tests/core/admin/ee/review-workflows.test.api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-tests/core/admin/ee/review-workflows.test.api.js b/api-tests/core/admin/ee/review-workflows.test.api.js index 66e6766c4c..d76f174816 100644 --- a/api-tests/core/admin/ee/review-workflows.test.api.js +++ b/api-tests/core/admin/ee/review-workflows.test.api.js @@ -282,7 +282,7 @@ describeOnCondition(edition === 'EE')('Review workflows', () => { test("It should assign a default color to stages if they don't have one", async () => { await requests.admin.put(`/admin/review-workflows/workflows/${testWorkflow.id}/stages`, { body: { - data: [defaultStage, { id: secondStage.id, name: 'new_name', color: '#000000' }], + data: [defaultStage, { id: secondStage.id, name: secondStage.name, color: '#000000' }], }, }); From be52a7cca445285fd5d4a72ed7a33fb655967b3e Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Fri, 28 Apr 2023 17:01:54 +0200 Subject: [PATCH 18/24] Settings: Improve error handling for stage update errors --- .../pages/ReviewWorkflows/ReviewWorkflows.js | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js index c8a3ec61c3..af6419b078 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js @@ -59,31 +59,15 @@ export function ReviewWorkflowsPage() { const { mutateAsync, isLoading } = useMutation( async ({ workflowId, stages }) => { - try { - const { - data: { data }, - } = await put(`/admin/review-workflows/workflows/${workflowId}/stages`, { - data: stages, - }); + const { + data: { data }, + } = await put(`/admin/review-workflows/workflows/${workflowId}/stages`, { + data: stages, + }); - return data; - } catch (error) { - toggleNotification({ - type: 'warning', - message: formatAPIError(error), - }); - } - - return null; + return data; }, { - onError(error) { - toggleNotification({ - type: 'warning', - message: formatAPIError(error), - }); - }, - onSuccess() { toggleNotification({ type: 'success', @@ -93,8 +77,19 @@ export function ReviewWorkflowsPage() { } ); - const updateWorkflowStages = (workflowId, stages) => { - return mutateAsync({ workflowId, stages }); + const updateWorkflowStages = async (workflowId, stages) => { + try { + const res = await mutateAsync({ workflowId, stages }); + + return res; + } catch (error) { + toggleNotification({ + type: 'warning', + message: formatAPIError(error), + }); + + return null; + } }; const submitForm = async () => { From dad3437033d63f96fe545491d303249faa642d21 Mon Sep 17 00:00:00 2001 From: Marc-Roig Date: Sat, 29 Apr 2023 11:59:33 +0200 Subject: [PATCH 19/24] chore: add default stage color constant --- packages/core/admin/ee/server/constants/workflows.js | 1 + .../admin/ee/server/content-types/workflow-stage/index.js | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/admin/ee/server/constants/workflows.js b/packages/core/admin/ee/server/constants/workflows.js index b9d04fba51..aa15028863 100644 --- a/packages/core/admin/ee/server/constants/workflows.js +++ b/packages/core/admin/ee/server/constants/workflows.js @@ -4,5 +4,6 @@ module.exports = { WORKFLOW_MODEL_UID: 'admin::workflow', STAGE_MODEL_UID: 'admin::workflow-stage', + STAGE_DEFAULT_COLOR: '#4945FF', ENTITY_STAGE_ATTRIBUTE: 'strapi_reviewWorkflows_stage', }; diff --git a/packages/core/admin/ee/server/content-types/workflow-stage/index.js b/packages/core/admin/ee/server/content-types/workflow-stage/index.js index 5f93eb69c9..4c24926c49 100644 --- a/packages/core/admin/ee/server/content-types/workflow-stage/index.js +++ b/packages/core/admin/ee/server/content-types/workflow-stage/index.js @@ -1,5 +1,7 @@ 'use strict'; +const { STAGE_DEFAULT_COLOR } = require('../../constants/workflows'); + module.exports = { schema: { collectionName: 'strapi_workflows_stages', @@ -27,7 +29,7 @@ module.exports = { color: { type: 'string', configurable: false, - default: '#4945FF', + default: STAGE_DEFAULT_COLOR, }, workflow: { type: 'relation', From ff33454691372416108e067336271cd6e19267df Mon Sep 17 00:00:00 2001 From: Marc-Roig Date: Sat, 29 Apr 2023 12:00:50 +0200 Subject: [PATCH 20/24] feat: migrateReviewWorkflowStagesColor --- .../review-workflows-stages-color.js | 20 +++++++++++++++++++ packages/core/admin/ee/server/register.js | 2 ++ 2 files changed, 22 insertions(+) create mode 100644 packages/core/admin/ee/server/migrations/review-workflows-stages-color.js diff --git a/packages/core/admin/ee/server/migrations/review-workflows-stages-color.js b/packages/core/admin/ee/server/migrations/review-workflows-stages-color.js new file mode 100644 index 0000000000..8c9a74869b --- /dev/null +++ b/packages/core/admin/ee/server/migrations/review-workflows-stages-color.js @@ -0,0 +1,20 @@ +'use strict'; + +const { STAGE_DEFAULT_COLOR } = require('../constants/workflows'); + +async function migrateReviewWorkflowStagesColor({ oldContentTypes, contentTypes }) { + // Check if stages table name has a color attribute + const hadColor = !!oldContentTypes?.['admin::workflow-stage']?.attributes?.color; + const hasColor = !!contentTypes['admin::workflow-stage']?.attributes?.color; + + // Add the default stage color if color attribute was added + if (!hadColor || hasColor) { + await strapi.query('admin::workflow-stage').updateMany({ + data: { + color: STAGE_DEFAULT_COLOR, + }, + }); + } +} + +module.exports = migrateReviewWorkflowStagesColor; diff --git a/packages/core/admin/ee/server/register.js b/packages/core/admin/ee/server/register.js index 72726acbb1..46cce24c38 100644 --- a/packages/core/admin/ee/server/register.js +++ b/packages/core/admin/ee/server/register.js @@ -3,6 +3,7 @@ const { features } = require('@strapi/strapi/lib/utils/ee'); const executeCERegister = require('../../server/register'); const migrateAuditLogsTable = require('./migrations/audit-logs-table'); +const migrateReviewWorkflowStagesColor = require('./migrations/review-workflows-stages-color'); const createAuditLogsService = require('./services/audit-logs'); const reviewWorkflowsMiddlewares = require('./middlewares/review-workflows'); const { getService } = require('./utils'); @@ -17,6 +18,7 @@ module.exports = async ({ strapi }) => { await auditLogsService.register(); } if (features.isEnabled('review-workflows')) { + strapi.hook('strapi::content-types.afterSync').register(migrateReviewWorkflowStagesColor); const reviewWorkflowService = getService('review-workflows'); reviewWorkflowsMiddlewares.contentTypeMiddleware(strapi); From 57ebe9c3e8f78e38a99d83730543306d542f7cf3 Mon Sep 17 00:00:00 2001 From: Marc-Roig Date: Sat, 29 Apr 2023 12:01:18 +0200 Subject: [PATCH 21/24] chore: change comment --- .../admin/ee/server/migrations/review-workflows-stages-color.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/admin/ee/server/migrations/review-workflows-stages-color.js b/packages/core/admin/ee/server/migrations/review-workflows-stages-color.js index 8c9a74869b..6f2395dc43 100644 --- a/packages/core/admin/ee/server/migrations/review-workflows-stages-color.js +++ b/packages/core/admin/ee/server/migrations/review-workflows-stages-color.js @@ -3,7 +3,7 @@ const { STAGE_DEFAULT_COLOR } = require('../constants/workflows'); async function migrateReviewWorkflowStagesColor({ oldContentTypes, contentTypes }) { - // Check if stages table name has a color attribute + // Look for CT's color attribute const hadColor = !!oldContentTypes?.['admin::workflow-stage']?.attributes?.color; const hasColor = !!contentTypes['admin::workflow-stage']?.attributes?.color; From dc33e41ce22f2fdd2b75fda457f8cb6bae509c2d Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Fri, 28 Apr 2023 16:40:09 +0200 Subject: [PATCH 22/24] Settings: Normalize color hex to be lowercase; display custom values --- .../components/Stages/Stage/Stage.js | 17 ++++++++++++++++- .../pages/ReviewWorkflows/utils/colors.js | 2 +- .../ReviewWorkflows/utils/tests/colors.test.js | 2 ++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js index 9985f3be06..c7447b96b3 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js @@ -172,6 +172,9 @@ export function Stage({ ), color: hex, })); + // TODO: the .toUpperCase() conversion can be removed once the hex code is normalized in + // the admin API + const colorValue = colorOptions.find(({ value }) => value === colorField.value.toUpperCase()); return ( @@ -275,7 +278,19 @@ export function Stage({ colorField.onChange({ target: { value } }); dispatch(updateStage(id, { color: value })); }} - value={colorOptions.find(({ value }) => value === colorField.value)} + // If no color was found in all the valid theme colors it means a user + // has set a custom value e.g. through the content API. In that case we + // display the custom color and a "Custom" label. + value={ + colorValue ?? { + value: colorField.value, + label: formatMessage({ + id: 'Settings.review-workflows.stage.color.name.custom', + defaultMessage: 'Custom', + }), + color: colorField.value, + } + } /> diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js index e33a85d796..0d10447f33 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js @@ -27,7 +27,7 @@ export function getStageColorByHex(hex) { export function getAvailableStageColors() { return Object.entries(STAGE_COLORS).map(([themeColorName, name]) => ({ - hex: lightTheme.colors[themeColorName], + hex: lightTheme.colors[themeColorName].toUpperCase(), name, })); } diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js index 746dc1ee8b..73b198c4d5 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/tests/colors.test.js @@ -11,6 +11,8 @@ describe('Settings | Review Workflows | colors', () => { hex: expect.any(String), name: expect.any(String), }); + + expect(color.hex).toBe(color.hex.toUpperCase()); }); }); From c996eca2ea8e0ee4f63c093c9547eed9612a2561 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Tue, 2 May 2023 12:08:20 +0200 Subject: [PATCH 23/24] Settings: Fix color badge with long stage names --- .../Stages/Stage/components/OptionColor/OptionColor.js | 4 ++-- .../Stage/components/SingleValueColor/SingleValueColor.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/OptionColor.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/OptionColor.js index 6700aa79fa..6285e4982a 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/OptionColor.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/OptionColor.js @@ -1,7 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { components } from 'react-select'; -import { Box, Flex, Typography } from '@strapi/design-system'; +import { Flex, Typography } from '@strapi/design-system'; export function OptionColor({ children, ...props }) { const { color } = props.data; @@ -9,7 +9,7 @@ export function OptionColor({ children, ...props }) { return ( - + {children} diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/SingleValueColor.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/SingleValueColor.js index 6fcc13bee0..c3346aac39 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/SingleValueColor.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/SingleValueColor.js @@ -1,7 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { components } from 'react-select'; -import { Box, Flex, Typography } from '@strapi/design-system'; +import { Flex, Typography } from '@strapi/design-system'; export function SingleValueColor({ children, ...props }) { const { color } = props.data; @@ -9,7 +9,7 @@ export function SingleValueColor({ children, ...props }) { return ( - + {children} From 392fa438b73081e4bb7816b3aca5a21d3da3d67e Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Tue, 2 May 2023 12:54:09 +0200 Subject: [PATCH 24/24] CM: Add max length for stage names in the list-view --- .../ReviewWorkflowsStage/ReviewWorkflowsStageEE.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/ReviewWorkflowsStageEE.js b/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/ReviewWorkflowsStageEE.js index 224f83db6d..7904b05f0d 100644 --- a/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/ReviewWorkflowsStageEE.js +++ b/packages/core/admin/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/ReviewWorkflowsStageEE.js @@ -1,13 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Box, Flex, Typography } from '@strapi/design-system'; +import { pxToRem } from '@strapi/helper-plugin'; export function ReviewWorkflowsStageEE({ color, name }) { return ( - - + + - + {name}