mirror of
https://github.com/strapi/strapi.git
synced 2025-08-08 00:37:38 +00:00
Fix: Apply multiple review workflow changes
This commit is contained in:
parent
28aca28856
commit
ebd3d25c50
@ -128,11 +128,15 @@ function ListView({
|
|||||||
enabled: !!options?.reviewWorkflows,
|
enabled: !!options?.reviewWorkflows,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const ReviewWorkflowsStage = useEnterprise(
|
const ReviewWorkflowsColumns = useEnterprise(
|
||||||
REVIEW_WORKFLOW_COLUMNS_CELL_CE,
|
REVIEW_WORKFLOW_COLUMNS_CELL_CE,
|
||||||
async () =>
|
async () => {
|
||||||
(await import('../../../../../ee/admin/content-manager/pages/ListView/ReviewWorkflowsColumn'))
|
const { ReviewWorkflowsStageEE, ReviewWorkflowsAssigneeEE } = await import(
|
||||||
.ReviewWorkflowsStageEE,
|
'../../../../../ee/admin/content-manager/pages/ListView/ReviewWorkflowsColumn'
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ReviewWorkflowsStageEE, ReviewWorkflowsAssigneeEE };
|
||||||
|
},
|
||||||
{
|
{
|
||||||
enabled: hasReviewWorkflows,
|
enabled: hasReviewWorkflows,
|
||||||
}
|
}
|
||||||
@ -457,7 +461,7 @@ function ListView({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Block rendering until the review stage component is fully loaded in EE
|
// Block rendering until the review stage component is fully loaded in EE
|
||||||
if (!ReviewWorkflowsStage) {
|
if (!ReviewWorkflowsColumns) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -607,11 +611,12 @@ function ListView({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasReviewWorkflows && name === 'strapi_stage') {
|
if (hasReviewWorkflows) {
|
||||||
|
if (name === 'strapi_stage') {
|
||||||
return (
|
return (
|
||||||
<Td key={key}>
|
<Td key={key}>
|
||||||
{rowData.strapi_stage ? (
|
{rowData.strapi_stage ? (
|
||||||
<ReviewWorkflowsStage
|
<ReviewWorkflowsColumns.ReviewWorkflowsStageEE
|
||||||
color={
|
color={
|
||||||
rowData.strapi_stage.color ?? lightTheme.colors.primary600
|
rowData.strapi_stage.color ?? lightTheme.colors.primary600
|
||||||
}
|
}
|
||||||
@ -623,6 +628,22 @@ function ListView({
|
|||||||
</Td>
|
</Td>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (name === 'strapi_assignee') {
|
||||||
|
return (
|
||||||
|
<Td key={key}>
|
||||||
|
{rowData.strapi_assignee ? (
|
||||||
|
<ReviewWorkflowsColumns.ReviewWorkflowsAssigneeEE
|
||||||
|
firstname={rowData.strapi_assignee.firstname}
|
||||||
|
lastname={rowData?.strapi_assignee?.lastname}
|
||||||
|
displayname={rowData?.strapi_assignee?.username}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography textColor="neutral800">-</Typography>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Td key={key}>
|
<Td key={key}>
|
||||||
|
@ -18,6 +18,8 @@ import {
|
|||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useMutation } from 'react-query';
|
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 { useReviewWorkflows } from '../../../../../../pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows';
|
||||||
import { getStageColorByHex } from '../../../../../../pages/SettingsPage/pages/ReviewWorkflows/utils/colors';
|
import { getStageColorByHex } from '../../../../../../pages/SettingsPage/pages/ReviewWorkflows/utils/colors';
|
||||||
import { STAGE_ATTRIBUTE_NAME } from '../../constants';
|
import { STAGE_ATTRIBUTE_NAME } from '../../constants';
|
||||||
@ -25,7 +27,6 @@ import { STAGE_ATTRIBUTE_NAME } from '../../constants';
|
|||||||
export function StageSelect() {
|
export function StageSelect() {
|
||||||
const {
|
const {
|
||||||
initialData,
|
initialData,
|
||||||
isCreatingEntry,
|
|
||||||
layout: { uid },
|
layout: { uid },
|
||||||
isSingleType,
|
isSingleType,
|
||||||
onChange,
|
onChange,
|
||||||
@ -34,17 +35,19 @@ export function StageSelect() {
|
|||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { formatAPIError } = useAPIErrorHandler();
|
const { formatAPIError } = useAPIErrorHandler();
|
||||||
const toggleNotification = useNotification();
|
const toggleNotification = useNotification();
|
||||||
const { workflows, isLoading } = useReviewWorkflows();
|
const {
|
||||||
|
meta,
|
||||||
const activeWorkflowStage = initialData?.[STAGE_ATTRIBUTE_NAME] ?? null;
|
workflows: [workflow],
|
||||||
|
isLoading,
|
||||||
// TODO: this works only as long as we support one workflow
|
} = useReviewWorkflows({ filters: { contentTypes: uid } });
|
||||||
const workflow = workflows?.[0] ?? null;
|
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
|
// it is possible to rely on initialData here, because it always will
|
||||||
// be updated at the same time when modifiedData is updated, otherwise
|
// be updated at the same time when modifiedData is updated, otherwise
|
||||||
// the entity is flagged as modified
|
// the entity is flagged as modified
|
||||||
const currentWorkflowStage = initialData?.[STAGE_ATTRIBUTE_NAME] ?? null;
|
const activeWorkflowStage = initialData?.[STAGE_ATTRIBUTE_NAME] ?? null;
|
||||||
|
|
||||||
const mutation = useMutation(
|
const mutation = useMutation(
|
||||||
async ({ entityId, stageId, uid }) => {
|
async ({ entityId, stageId, uid }) => {
|
||||||
@ -71,36 +74,53 @@ export function StageSelect() {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
message: {
|
message: {
|
||||||
id: 'content-manager.reviewWorkflows.stage.notification.saved',
|
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 }) => {
|
const handleChange = async ({ value: stageId }) => {
|
||||||
mutation.mutate({
|
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,
|
entityId: initialData.id,
|
||||||
stageId,
|
stageId,
|
||||||
uid,
|
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
|
const { themeColorName } = activeWorkflowStage?.color
|
||||||
@ -108,10 +128,11 @@ export function StageSelect() {
|
|||||||
: {};
|
: {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Field name={STAGE_ATTRIBUTE_NAME} id={STAGE_ATTRIBUTE_NAME}>
|
<Field name={STAGE_ATTRIBUTE_NAME} id={STAGE_ATTRIBUTE_NAME}>
|
||||||
<Flex direction="column" gap={2} alignItems="stretch">
|
<Flex direction="column" gap={2} alignItems="stretch">
|
||||||
<SingleSelect
|
<SingleSelect
|
||||||
error={formattedError}
|
error={(mutation.error && formatAPIError(mutation.error)) || null}
|
||||||
name={STAGE_ATTRIBUTE_NAME}
|
name={STAGE_ATTRIBUTE_NAME}
|
||||||
id={STAGE_ATTRIBUTE_NAME}
|
id={STAGE_ATTRIBUTE_NAME}
|
||||||
value={activeWorkflowStage?.id}
|
value={activeWorkflowStage?.id}
|
||||||
@ -170,5 +191,44 @@ export function StageSelect() {
|
|||||||
<FieldError />
|
<FieldError />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<LimitsModal.Root
|
||||||
|
isOpen={showLimitModal === 'workflow'}
|
||||||
|
onClose={() => setShowLimitModal(false)}
|
||||||
|
>
|
||||||
|
<LimitsModal.Title>
|
||||||
|
{formatMessage({
|
||||||
|
id: 'content-manager.reviewWorkflows.workflows.limit.title',
|
||||||
|
defaultMessage: 'You’ve reached the limit of workflows in your plan',
|
||||||
|
})}
|
||||||
|
</LimitsModal.Title>
|
||||||
|
|
||||||
|
<LimitsModal.Body>
|
||||||
|
{formatMessage({
|
||||||
|
id: 'content-manager.reviewWorkflows.workflows.limit.body',
|
||||||
|
defaultMessage: 'Delete a workflow or contact Sales to enable more workflows.',
|
||||||
|
})}
|
||||||
|
</LimitsModal.Body>
|
||||||
|
</LimitsModal.Root>
|
||||||
|
|
||||||
|
<LimitsModal.Root
|
||||||
|
isOpen={showLimitModal === 'stage'}
|
||||||
|
onClose={() => setShowLimitModal(false)}
|
||||||
|
>
|
||||||
|
<LimitsModal.Title>
|
||||||
|
{formatMessage({
|
||||||
|
id: 'content-manager.reviewWorkflows.stages.limit.title',
|
||||||
|
defaultMessage: 'You have reached the limit of stages for this workflow in your plan',
|
||||||
|
})}
|
||||||
|
</LimitsModal.Title>
|
||||||
|
|
||||||
|
<LimitsModal.Body>
|
||||||
|
{formatMessage({
|
||||||
|
id: 'content-manager.reviewWorkflows.stages.limit.body',
|
||||||
|
defaultMessage: 'Try deleting some stages or contact Sales to enable more stages.',
|
||||||
|
})}
|
||||||
|
</LimitsModal.Body>
|
||||||
|
</LimitsModal.Root>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -92,21 +92,6 @@ describe('EE | Content Manager | EditView | InformationBox | StageSelect', () =>
|
|||||||
server.close();
|
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', () => {
|
it('renders an enabled select input, if the entity is edited', () => {
|
||||||
useCMEditViewDataManager.mockReturnValue({
|
useCMEditViewDataManager.mockReturnValue({
|
||||||
initialData: {
|
initialData: {
|
||||||
|
@ -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';
|
export const ASSIGNEE_ATTRIBUTE_NAME = 'strapi_assignee';
|
||||||
|
@ -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(<InformationBoxEE {...props} />, {
|
||||||
|
wrapper({ children }) {
|
||||||
|
const store = createStore((state = {}) => state, {});
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<IntlProvider locale="en" defaultLocale="en">
|
||||||
|
<ThemeProvider theme={lightTheme}>{children}</ThemeProvider>
|
||||||
|
</IntlProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user