diff --git a/packages/core/admin/admin/src/content-manager/pages/ListView/index.js b/packages/core/admin/admin/src/content-manager/pages/ListView/index.js index 18ac0bc812..bd90194ca1 100644 --- a/packages/core/admin/admin/src/content-manager/pages/ListView/index.js +++ b/packages/core/admin/admin/src/content-manager/pages/ListView/index.js @@ -128,11 +128,15 @@ function ListView({ enabled: !!options?.reviewWorkflows, } ); - const ReviewWorkflowsStage = useEnterprise( + const ReviewWorkflowsColumns = useEnterprise( REVIEW_WORKFLOW_COLUMNS_CELL_CE, - async () => - (await import('../../../../../ee/admin/content-manager/pages/ListView/ReviewWorkflowsColumn')) - .ReviewWorkflowsStageEE, + async () => { + const { ReviewWorkflowsStageEE, ReviewWorkflowsAssigneeEE } = await import( + '../../../../../ee/admin/content-manager/pages/ListView/ReviewWorkflowsColumn' + ); + + return { ReviewWorkflowsStageEE, ReviewWorkflowsAssigneeEE }; + }, { enabled: hasReviewWorkflows, } @@ -457,7 +461,7 @@ function ListView({ }; // Block rendering until the review stage component is fully loaded in EE - if (!ReviewWorkflowsStage) { + if (!ReviewWorkflowsColumns) { return null; } @@ -607,21 +611,38 @@ function ListView({ ); } - if (hasReviewWorkflows && name === 'strapi_stage') { - return ( - - {rowData.strapi_stage ? ( - - ) : ( - - - )} - - ); + if (hasReviewWorkflows) { + if (name === 'strapi_stage') { + return ( + + {rowData.strapi_stage ? ( + + ) : ( + - + )} + + ); + } + if (name === 'strapi_assignee') { + return ( + + {rowData.strapi_assignee ? ( + + ) : ( + - + )} + + ); + } } return ( diff --git a/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/components/StageSelect/StageSelect.js b/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/components/StageSelect/StageSelect.js index ab2ec539e1..7cc035b48e 100644 --- a/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/components/StageSelect/StageSelect.js +++ b/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/components/StageSelect/StageSelect.js @@ -18,6 +18,8 @@ import { import { useIntl } from 'react-intl'; import { useMutation } from 'react-query'; +import { useLicenseLimits } from '../../../../../../hooks/useLicenseLimits'; +import * as LimitsModal from '../../../../../../pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal'; import { useReviewWorkflows } from '../../../../../../pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows'; import { getStageColorByHex } from '../../../../../../pages/SettingsPage/pages/ReviewWorkflows/utils/colors'; import { STAGE_ATTRIBUTE_NAME } from '../../constants'; @@ -25,7 +27,6 @@ import { STAGE_ATTRIBUTE_NAME } from '../../constants'; export function StageSelect() { const { initialData, - isCreatingEntry, layout: { uid }, isSingleType, onChange, @@ -34,17 +35,19 @@ export function StageSelect() { const { formatMessage } = useIntl(); const { formatAPIError } = useAPIErrorHandler(); const toggleNotification = useNotification(); - const { workflows, isLoading } = useReviewWorkflows(); - - const activeWorkflowStage = initialData?.[STAGE_ATTRIBUTE_NAME] ?? null; - - // TODO: this works only as long as we support one workflow - const workflow = workflows?.[0] ?? null; + const { + meta, + workflows: [workflow], + isLoading, + } = useReviewWorkflows({ filters: { contentTypes: uid } }); + const { getFeature } = useLicenseLimits(); + const [showLimitModal, setShowLimitModal] = React.useState(false); + const limits = getFeature('review-workflows'); // it is possible to rely on initialData here, because it always will // be updated at the same time when modifiedData is updated, otherwise // the entity is flagged as modified - const currentWorkflowStage = initialData?.[STAGE_ATTRIBUTE_NAME] ?? null; + const activeWorkflowStage = initialData?.[STAGE_ATTRIBUTE_NAME] ?? null; const mutation = useMutation( async ({ entityId, stageId, uid }) => { @@ -71,36 +74,53 @@ export function StageSelect() { type: 'success', message: { id: 'content-manager.reviewWorkflows.stage.notification.saved', - defaultMessage: 'Success: Review stage updated', + defaultMessage: 'Review stage updated', }, }); }, } ); - // if entities are created e.g. through lifecycle methods - // they may not have a stage assigned. Updating the entity won't - // set the default stage either which may lead to entities that - // do not have a stage assigned for a while. By displaying an - // error by default we are trying to nudge users into assigning a stage. - const initialStageNullError = - currentWorkflowStage === null && - !isLoading && - !isCreatingEntry && - formatMessage({ - id: 'content-manager.reviewWorkflows.stage.select.placeholder', - defaultMessage: 'Select a stage', - }); - - const formattedError = - (mutation.error && formatAPIError(mutation.error)) || initialStageNullError || null; - const handleChange = async ({ value: stageId }) => { - mutation.mutate({ - entityId: initialData.id, - stageId, - uid, - }); + try { + /** + * If the current license has a limit: + * check if the total count of workflows exceeds that limit and display + * the limits modal. + * + * If the current license does not have a limit (e.g. offline license): + * do nothing (for now). + * + */ + + if (limits?.workflows && parseInt(limits.workflows, 10) > meta.workflowCount) { + setShowLimitModal('workflow'); + + /** + * If the current license has a limit: + * check if the total count of stages exceeds that limit and display + * the limits modal. + * + * If the current license does not have a limit (e.g. offline license): + * do nothing (for now). + * + */ + } else if ( + limits?.stagesPerWorkflow && + parseInt(limits.stagesPerWorkflow, 10) > workflow.stages.length + ) { + setShowLimitModal('stage'); + } else { + mutation.mutateAsync({ + entityId: initialData.id, + stageId, + uid, + }); + } + } catch (error) { + // react-query@v3: the error doesn't have to be handled here + // see: https://github.com/TanStack/query/issues/121 + } }; const { themeColorName } = activeWorkflowStage?.color @@ -108,67 +128,107 @@ export function StageSelect() { : {}; return ( - - - handleChange({ value })} - label={formatMessage({ - id: 'content-manager.reviewWorkflows.stage.label', - defaultMessage: 'Review stage', - })} - startIcon={ - - } - // eslint-disable-next-line react/no-unstable-nested-components - customizeContent={() => ( - - - {activeWorkflowStage?.name} - - {isLoading ? : null} - - )} - > - {workflow - ? workflow.stages.map(({ id, color, name }) => { - const { themeColorName } = getStageColorByHex(color); + <> + + + handleChange({ value })} + label={formatMessage({ + id: 'content-manager.reviewWorkflows.stage.label', + defaultMessage: 'Review stage', + })} + startIcon={ + + } + // eslint-disable-next-line react/no-unstable-nested-components + customizeContent={() => ( + + + {activeWorkflowStage?.name} + + {isLoading ? : null} + + )} + > + {workflow + ? workflow.stages.map(({ id, color, name }) => { + const { themeColorName } = getStageColorByHex(color); - return ( - - } - value={id} - textValue={name} - > - {name} - - ); - }) - : []} - - - - + return ( + + } + value={id} + textValue={name} + > + {name} + + ); + }) + : []} + + + + + + setShowLimitModal(false)} + > + + {formatMessage({ + id: 'content-manager.reviewWorkflows.workflows.limit.title', + defaultMessage: 'You’ve reached the limit of workflows in your plan', + })} + + + + {formatMessage({ + id: 'content-manager.reviewWorkflows.workflows.limit.body', + defaultMessage: 'Delete a workflow or contact Sales to enable more workflows.', + })} + + + + setShowLimitModal(false)} + > + + {formatMessage({ + id: 'content-manager.reviewWorkflows.stages.limit.title', + defaultMessage: 'You have reached the limit of stages for this workflow in your plan', + })} + + + + {formatMessage({ + id: 'content-manager.reviewWorkflows.stages.limit.body', + defaultMessage: 'Try deleting some stages or contact Sales to enable more stages.', + })} + + + ); } diff --git a/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/components/StageSelect/tests/StageSelect.test.js b/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/components/StageSelect/tests/StageSelect.test.js index f6939eb8cf..9bdf650646 100644 --- a/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/components/StageSelect/tests/StageSelect.test.js +++ b/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/components/StageSelect/tests/StageSelect.test.js @@ -92,21 +92,6 @@ describe('EE | Content Manager | EditView | InformationBox | StageSelect', () => server.close(); }); - it('renders an error, if no workflow stage is assigned to the entity', async () => { - useCMEditViewDataManager.mockReturnValue({ - initialData: { - [STAGE_ATTRIBUTE_NAME]: null, - }, - layout: { uid: 'api::articles:articles' }, - }); - - const { getByText, queryByRole } = setup(); - - expect(queryByRole('combobox')).toBeInTheDocument(); - - await waitFor(() => expect(getByText(/select a stage/i)).toBeInTheDocument()); - }); - it('renders an enabled select input, if the entity is edited', () => { useCMEditViewDataManager.mockReturnValue({ initialData: { diff --git a/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/constants.js b/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/constants.js index a1a9c9f8b3..c0f04ed517 100644 --- a/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/constants.js +++ b/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/constants.js @@ -1,2 +1,2 @@ -export const STAGE_ATTRIBUTE_NAME = 'strapi_reviewWorkflows_stage'; +export const STAGE_ATTRIBUTE_NAME = 'strapi_stage'; export const ASSIGNEE_ATTRIBUTE_NAME = 'strapi_assignee'; 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 new file mode 100644 index 0000000000..fe38fdda6f --- /dev/null +++ b/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/tests/InformationBoxEE.test.js @@ -0,0 +1,137 @@ +import React from 'react'; + +import { lightTheme, ThemeProvider } from '@strapi/design-system'; +import { useCMEditViewDataManager } from '@strapi/helper-plugin'; +import { render } from '@testing-library/react'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { IntlProvider } from 'react-intl'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { Provider } from 'react-redux'; +import { createStore } from 'redux'; + +import { STAGE_ATTRIBUTE_NAME, ASSIGNEE_ATTRIBUTE_NAME } from '../constants'; +import { InformationBoxEE } from '../InformationBoxEE'; + +jest.mock('@strapi/helper-plugin', () => ({ + ...jest.requireActual('@strapi/helper-plugin'), + useCMEditViewDataManager: jest.fn(), + useNotification: jest.fn(() => ({ + toggleNotification: jest.fn(), + })), +})); + +const server = setupServer( + rest.get('*/users', (req, res, ctx) => + res( + ctx.json({ + data: { + results: [ + { + id: 1, + }, + ], + + pagination: { + page: 1, + }, + }, + }) + ) + ), + + rest.get('*/review-workflows/workflows', (req, res, ctx) => + res( + ctx.json({ + data: [ + { + id: 1, + }, + ], + }) + ) + ) +); + +const setup = (props) => { + return render(, { + wrapper({ children }) { + const store = createStore((state = {}) => state, {}); + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + return ( + + + + {children} + + + + ); + }, + }); +}; + +describe('EE | Content Manager | EditView | InformationBox', () => { + beforeAll(() => { + server.listen(); + }); + + afterAll(() => { + server.close(); + }); + + it('renders the title and body of the Information component', () => { + useCMEditViewDataManager.mockReturnValue({ + initialData: {}, + isCreatingEntry: true, + layout: { uid: 'api::articles:articles' }, + }); + + const { getByText } = setup(); + + expect(getByText('Information')).toBeInTheDocument(); + expect(getByText('Last update')).toBeInTheDocument(); + }); + + it('renders neither stage nor assignee select inputs, if no nothing is returned for an entity', () => { + useCMEditViewDataManager.mockReturnValue({ + initialData: {}, + layout: { uid: 'api::articles:articles' }, + }); + + const { queryByRole } = setup(); + + expect(queryByRole('combobox')).not.toBeInTheDocument(); + }); + + it('renders stage and assignee select inputs, if it is returned for an entity', () => { + useCMEditViewDataManager.mockReturnValue({ + initialData: { + [STAGE_ATTRIBUTE_NAME]: { + id: 1, + color: '#4945FF', + name: 'Stage 1', + worklow: 1, + }, + + [ASSIGNEE_ATTRIBUTE_NAME]: { + id: 1, + firstname: 'Firstname', + lastname: 'Lastname', + }, + }, + layout: { uid: 'api::articles:articles' }, + }); + + const { queryAllByRole } = setup(); + + expect(queryAllByRole('combobox').length).toBe(2); + }); +});