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 1ebd661e2d..28f44cf5c8 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 @@ -5,6 +5,7 @@ import { SingleSelectOption, Field, FieldError, + FieldHint, Flex, Loader, Typography, @@ -24,26 +25,22 @@ import { CHARGEBEE_STAGES_PER_WORKFLOW_ENTITLEMENT_NAME, CHARGEBEE_WORKFLOW_ENTITLEMENT_NAME, } from '../../../../../../pages/SettingsPage/pages/ReviewWorkflows/constants'; -import { useReviewWorkflows } from '../../../../../../pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows'; +import { useReviewWorkflowsStages } from '../../../../../../pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflowsStages'; import { getStageColorByHex } from '../../../../../../pages/SettingsPage/pages/ReviewWorkflows/utils/colors'; import { STAGE_ATTRIBUTE_NAME } from '../../constants'; export function StageSelect() { - const { - initialData, - layout: { uid }, - isSingleType, - onChange, - } = useCMEditViewDataManager(); + const { initialData, layout: contentType, isSingleType, onChange } = useCMEditViewDataManager(); const { put } = useFetchClient(); const { formatMessage } = useIntl(); const { formatAPIError } = useAPIErrorHandler(); const toggleNotification = useNotification(); - const { - meta, - workflows: [workflow], - isLoading, - } = useReviewWorkflows({ filters: { contentTypes: uid } }); + const { meta, stages, isLoading } = useReviewWorkflowsStages( + { id: initialData.id, layout: contentType }, + { + enabled: !!initialData?.id, + } + ); const { getFeature } = useLicenseLimits(); const [showLimitModal, setShowLimitModal] = React.useState(false); @@ -114,15 +111,14 @@ export function StageSelect() { */ } else if ( limits?.[CHARGEBEE_STAGES_PER_WORKFLOW_ENTITLEMENT_NAME] && - parseInt(limits[CHARGEBEE_STAGES_PER_WORKFLOW_ENTITLEMENT_NAME], 10) < - workflow.stages.length + parseInt(limits[CHARGEBEE_STAGES_PER_WORKFLOW_ENTITLEMENT_NAME], 10) < stages.length ) { setShowLimitModal('stage'); } else { mutation.mutateAsync({ entityId: initialData.id, stageId, - uid, + uid: contentType.uid, }); } } catch (error) { @@ -137,9 +133,20 @@ export function StageSelect() { return ( <> - + + activeWorkflowStage && ( + + ) } // eslint-disable-next-line react/no-unstable-nested-components customizeContent={() => ( - {activeWorkflowStage?.name} + {activeWorkflowStage?.name ?? ''} - {isLoading ? : null} + {isLoading ? ( + + ) : null} )} > - {workflow - ? workflow.stages.map(({ id, color, name }) => { - const { themeColorName } = getStageColorByHex(color); + {stages.map(({ id, color, name }) => { + const { themeColorName } = getStageColorByHex(color); - return ( - - } - value={id} - textValue={name} - > - {name} - - ); - }) - : []} + return ( + + } + value={id} + textValue={name} + > + {name} + + ); + })} + 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 9bdf650646..d8eba82dd1 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 @@ -1,8 +1,7 @@ import React from 'react'; import { ThemeProvider, lightTheme } from '@strapi/design-system'; -import { useCMEditViewDataManager } from '@strapi/helper-plugin'; -import { render, waitFor } from '@testing-library/react'; +import { render, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -11,45 +10,49 @@ import { QueryClientProvider, QueryClient } from 'react-query'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; -import { STAGE_ATTRIBUTE_NAME } from '../../../constants'; import { StageSelect } from '../StageSelect'; -const STAGE_1_STATE_FIXTURE = { - id: 1, - color: '#4945FF', - name: 'Stage 1', - worklow: 1, -}; - const server = setupServer( - rest.get('*/review-workflows/workflows/', (req, res, ctx) => - res( - ctx.json({ - data: [ - { - id: 1, - stages: [ - { - id: 1, - color: '#4945FF', - name: 'Stage 1', - }, - { - id: 2, - color: '#4945FF', - name: 'Stage 2', - }, - ], - }, - ], - }) - ) - ) + ...[ + rest.get('*/content-manager/:kind/:uid/:id/stages', (req, res, ctx) => + res( + ctx.json({ + data: [ + { + id: 1, + color: '#4945FF', + name: 'Stage 1', + }, + + { + id: 2, + color: '#4945FF', + name: 'Stage 2', + }, + ], + }) + ) + ), + + rest.get('*/license-limit-information', (req, res, ctx) => res(ctx.json({}))), + ] ); jest.mock('@strapi/helper-plugin', () => ({ ...jest.requireActual('@strapi/helper-plugin'), - useCMEditViewDataManager: jest.fn(), + useCMEditViewDataManager: jest.fn().mockReturnValue({ + initialData: { + id: 1, + strapi_stage: { + id: 1, + color: '#4945FF', + name: 'Stage 1', + }, + }, + isCreatingEntry: false, + isSingleType: false, + layout: { uid: 'api::articles:articles' }, + }), useNotification: jest.fn(() => ({ toggleNotification: jest.fn(), })), @@ -92,40 +95,36 @@ describe('EE | Content Manager | EditView | InformationBox | StageSelect', () => server.close(); }); - it('renders an enabled select input, if the entity is edited', () => { - useCMEditViewDataManager.mockReturnValue({ - initialData: { - [STAGE_ATTRIBUTE_NAME]: null, - }, - isCreatingEntry: false, - layout: { uid: 'api::articles:articles' }, - }); - - const { queryByRole } = setup(); - const select = queryByRole('combobox'); - - expect(select).toBeInTheDocument(); + afterEach(() => { + jest.clearAllMocks(); }); it('renders a select input, if a workflow stage is assigned to the entity', async () => { - useCMEditViewDataManager.mockReturnValue({ - initialData: { - [STAGE_ATTRIBUTE_NAME]: STAGE_1_STATE_FIXTURE, - }, - isCreatingEntry: false, - layout: { uid: 'api::articles:articles' }, - }); + const { queryByRole, getByTestId, getByText, user } = setup(); - const { queryByRole, queryByTestId, getByText, user } = setup(); + await waitForElementToBeRemoved(() => getByTestId('loader')); - await waitFor(() => expect(queryByTestId('loader')).not.toBeInTheDocument()); + await waitFor(() => expect(getByText('Stage 1')).toBeInTheDocument()); - const select = queryByRole('combobox'); + await user.click(queryByRole('combobox')); - expect(getByText('Stage 1')).toBeInTheDocument(); + await waitFor(() => expect(getByText('Stage 2')).toBeInTheDocument()); + }); - await user.click(select); + it("renders the select as disabled with a hint, if there aren't any stages", async () => { + server.use( + rest.get('*/content-manager/:kind/:uid/:id/stages', (req, res, ctx) => { + return res.once(ctx.json({ data: [] })); + }) + ); - expect(getByText('Stage 2')).toBeInTheDocument(); + const { queryByRole, getByText, getByTestId } = setup(); + + await waitForElementToBeRemoved(() => getByTestId('loader')); + + await waitFor(() => expect(queryByRole('combobox')).toHaveAttribute('aria-disabled', 'true')); + await waitFor(() => + expect(getByText('You don’t have the permission to update this stage.')).toBeInTheDocument() + ); }); }); diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/tests/useReviewWorkflows.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/tests/useReviewWorkflows.test.js new file mode 100644 index 0000000000..2fdad2ecb2 --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/tests/useReviewWorkflows.test.js @@ -0,0 +1,108 @@ +import * as React from 'react'; + +import { renderHook, waitFor } 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 { useReviewWorkflows } from '../useReviewWorkflows'; + +const server = setupServer( + rest.get( + '*/content-manager/collection-types/api::collection.collection/stages', + (req, res, ctx) => + res( + ctx.json({ + data: [ + { + id: 1, + name: 'Todo', + }, + + { + id: 2, + name: 'Done', + }, + ], + + meta: { + workflowCount: 10, + stagesCount: 5, + }, + }) + ) + ), + + rest.get('*/content-manager/single-types/api::single.single/stages', (req, res, ctx) => + res( + ctx.json({ + data: [ + { + id: 2, + name: 'Todo', + }, + + { + id: 3, + name: 'Done', + }, + ], + + meta: { + workflowCount: 10, + stagesCount: 5, + }, + }) + ) + ) +); + +const setup = (...args) => + renderHook(() => useReviewWorkflows(...args), { + wrapper({ children }) { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + return ( + + + {children} + + + ); + }, + }); + +describe('useReviewWorkflows', () => { + beforeAll(() => { + server.listen(); + }); + + afterAll(() => { + server.close(); + }); + + test('fetches many workflows', async () => { + const { result } = setup(); + + await waitFor(() => result.current.isLoading === false); + }); + + test('fetches one workflow', async () => { + const { result } = setup({ id: 1 }); + + await waitFor(() => result.current.isLoading === false); + }); + + test('forwards all params except "id" as query params', async () => { + const { result } = setup({ id: 1 }); + + await waitFor(() => result.current.isLoading === false); + }); +}); diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/tests/useReviewWorkflowsStages.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/tests/useReviewWorkflowsStages.test.js new file mode 100644 index 0000000000..10c0a97d17 --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/tests/useReviewWorkflowsStages.test.js @@ -0,0 +1,136 @@ +import * as React from 'react'; + +import { renderHook, waitFor } 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 { useReviewWorkflowsStages } from '../useReviewWorkflowsStages'; + +const server = setupServer( + ...[ + rest.get('*/content-manager/collection-types/:uid/:id/stages', (req, res, ctx) => + res( + ctx.json({ + data: [ + { + id: 1, + name: 'Default', + }, + ], + + meta: { + workflowCount: 10, + }, + }) + ) + ), + + rest.get('*/content-manager/single-types/:uid/:id/stages', (req, res, ctx) => + res( + ctx.json({ + data: [ + { + id: 1, + name: 'Default', + }, + ], + + meta: { + workflowCount: 10, + }, + }) + ) + ), + ] +); + +const setup = (...args) => + renderHook(() => useReviewWorkflowsStages(...args), { + wrapper({ children }) { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + return ( + + + {children} + + + ); + }, + }); + +describe('useReviewWorkflowsStages', () => { + beforeAll(() => { + server.listen(); + }); + + afterAll(() => { + server.close(); + }); + + test('fetches stages for collection-types', async () => { + const { result } = setup({ + id: 1, + layout: { + uid: 'api::collection.collection', + kind: 'collectionType', + }, + }); + + await waitFor(() => expect(result.current.stages).toStrictEqual([])); + await waitFor(() => expect(result.current.meta).toStrictEqual({})); + + await waitFor(() => result.current.isLoading === false); + + expect(result.current.stages).toStrictEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 1, + }), + ]) + ); + + expect(result.current.meta).toStrictEqual( + expect.objectContaining({ + workflowCount: expect.any(Number), + }) + ); + }); + + test('fetches stages for single-types', async () => { + const { result } = setup({ + id: 1, + layout: { + uid: 'api::single.single', + kind: 'singleType', + }, + }); + + await waitFor(() => expect(result.current.stages).toStrictEqual([])); + await waitFor(() => expect(result.current.meta).toStrictEqual({})); + + await waitFor(() => result.current.isLoading === false); + + expect(result.current.stages).toStrictEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 1, + }), + ]) + ); + + expect(result.current.meta).toStrictEqual( + expect.objectContaining({ + workflowCount: expect.any(Number), + }) + ); + }); +}); diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflowsStages.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflowsStages.js new file mode 100644 index 0000000000..e7edbf4333 --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflowsStages.js @@ -0,0 +1,35 @@ +import * as React from 'react'; + +import { useFetchClient } from '@strapi/helper-plugin'; +import { useQuery } from 'react-query'; + +export function useReviewWorkflowsStages({ id, layout } = {}, queryOptions = {}) { + const { kind, uid } = layout; + const slug = kind === 'collectionType' ? 'collection-types' : 'single-types'; + + const { get } = useFetchClient(); + + const { data, isLoading } = useQuery( + ['content-manager', slug, layout.uid, id, 'stages'], + async () => { + const { data } = await get(`/admin/content-manager/${slug}/${uid}/${id}/stages`); + + return data; + }, + queryOptions + ); + + // these return values need to be memoized, because the default value + // would lead to infinite rendering loops when used in a dependency array + // on an effect + const meta = React.useMemo(() => data?.meta ?? {}, [data?.meta]); + const stages = React.useMemo(() => data?.data ?? [], [data?.data]); + + return { + // meta contains e.g. the total of all workflows. we can not use + // the pagination object here, because the list is not paginated. + meta, + stages, + isLoading, + }; +}