Merge pull request #17101 from strapi/feature/review-workflow-multiple-workflow-limits

This commit is contained in:
Josh 2023-06-29 14:43:56 +01:00 committed by GitHub
commit 4e45c46dc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 506 additions and 29 deletions

View File

@ -11,6 +11,8 @@ import { useIntl } from 'react-intl';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import Information from '../../../../../../admin/src/content-manager/pages/EditView/Information'; import Information from '../../../../../../admin/src/content-manager/pages/EditView/Information';
import * as LimitsModal from '../../../../pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal';
import { useReviewWorkflowLicenseLimits } from '../../../../pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflowLicenseLimits';
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';
@ -33,8 +35,11 @@ export function InformationBoxEE() {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { formatAPIError } = useAPIErrorHandler(); const { formatAPIError } = useAPIErrorHandler();
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const { limits } = useReviewWorkflowLicenseLimits();
const [showLimitModal, setShowLimitModal] = React.useState(false);
const { const {
pagination,
workflows: [workflow], workflows: [workflow],
isLoading: isWorkflowLoading, isLoading: isWorkflowLoading,
} = useReviewWorkflows({ filters: { contentTypes: uid } }); } = useReviewWorkflows({ filters: { contentTypes: uid } });
@ -72,6 +77,17 @@ export function InformationBoxEE() {
const handleStageChange = async ({ value: stageId }) => { const handleStageChange = async ({ value: stageId }) => {
try { try {
if (limits?.workflows > pagination.total) {
setShowLimitModal('workflow');
return;
}
if (limits?.stagesPerWorkflow > workflow.stages.length) {
setShowLimitModal('stage');
return;
}
await mutateAsync({ await mutateAsync({
entityId: initialData.id, entityId: initialData.id,
stageId, stageId,
@ -152,6 +168,44 @@ export function InformationBoxEE() {
)} )}
<Information.Body /> <Information.Body />
<LimitsModal.Root
isOpen={showLimitModal === 'workflow'}
onClose={() => setShowLimitModal(false)}
>
<LimitsModal.Title>
{formatMessage({
id: 'content-manager.reviewWorkflows.workflows.limit.title',
defaultMessage: 'Youve 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>
</Information.Root> </Information.Root>
); );
} }

View File

@ -1,7 +1,8 @@
import React from 'react'; import * as React from 'react';
import { fixtures } from '@strapi/admin-test-utils';
import { lightTheme, ThemeProvider } from '@strapi/design-system'; import { lightTheme, ThemeProvider } from '@strapi/design-system';
import { useCMEditViewDataManager } from '@strapi/helper-plugin'; import { useCMEditViewDataManager, RBACContext } from '@strapi/helper-plugin';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { IntlProvider } from 'react-intl'; import { IntlProvider } from 'react-intl';
@ -66,13 +67,27 @@ const ComponentFixture = (props) => <InformationBoxEE {...props} />;
const setup = (props) => ({ const setup = (props) => ({
...render(<ComponentFixture {...props} />, { ...render(<ComponentFixture {...props} />, {
wrapper({ children }) { wrapper({ children }) {
const store = createStore((state = {}) => state, {}); const store = createStore((state = {}) => state, {
admin_app: {
permissions: fixtures.permissions.app,
},
});
// eslint-disable-next-line react-hooks/rules-of-hooks
const rbacContextValue = React.useMemo(
() => ({
allPermissions: fixtures.permissions.allPermissions,
}),
[]
);
return ( return (
<Provider store={store}> <Provider store={store}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<IntlProvider locale="en" defaultLocale="en"> <IntlProvider locale="en" defaultLocale="en">
<ThemeProvider theme={lightTheme}>{children}</ThemeProvider> <ThemeProvider theme={lightTheme}>
<RBACContext.Provider value={rbacContextValue}>{children}</RBACContext.Provider>
</ThemeProvider>
</IntlProvider> </IntlProvider>
</QueryClientProvider> </QueryClientProvider>
</Provider> </Provider>

View File

@ -6,27 +6,27 @@ import { selectAdminPermissions } from '../../../../admin/src/pages/App/selector
const useLicenseLimits = () => { const useLicenseLimits = () => {
const permissions = useSelector(selectAdminPermissions); const permissions = useSelector(selectAdminPermissions);
const rbac = useRBAC(permissions.settings.users); const { get } = useFetchClient();
const { const {
isLoading: isRBACLoading, isLoading: isRBACLoading,
allowedActions: { canRead, canCreate, canUpdate, canDelete }, allowedActions: { canRead, canCreate, canUpdate, canDelete },
} = rbac; } = useRBAC(permissions.settings.users);
const isRBACAllowed = canRead && canCreate && canUpdate && canDelete; const isRBACAllowed = canRead && canCreate && canUpdate && canDelete;
const { get } = useFetchClient(); const license = useQuery(
const fetchLicenseLimitInfo = async () => { ['ee', 'license-limit-info'],
const { async () => {
data: { data }, const {
} = await get('/admin/license-limit-information'); data: { data },
} = await get('/admin/license-limit-information');
return data; return data;
}; },
{
const license = useQuery(['ee', 'license-limit-info'], fetchLicenseLimitInfo, { enabled: !isRBACLoading && isRBACAllowed,
enabled: !isRBACLoading && isRBACAllowed, }
}); );
return { license }; return { license };
}; };

View File

@ -0,0 +1,111 @@
import * as React from 'react';
import { Box, Flex, IconButton, ModalLayout, ModalBody, Typography } from '@strapi/design-system';
import { LinkButton } from '@strapi/design-system/v2';
import { Cross } from '@strapi/icons';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import styled from 'styled-components';
import balloonImageSrc from './assets/balloon.png';
const TITLE_ID = 'limits-title';
const CTA_LEARN_MORE_HREF = 'https://strapi.io/pricing-cloud';
const CTA_SALES_HREF = 'https://strapi.io/contact-sales';
export function Title({ children }) {
return (
<Typography variant="alpha" id={TITLE_ID}>
{children}
</Typography>
);
}
Title.propTypes = {
children: PropTypes.node.isRequired,
};
export function Body({ children }) {
return <Typography variant="omega">{children}</Typography>;
}
Body.propTypes = {
children: PropTypes.node.isRequired,
};
function CallToActions() {
const { formatMessage } = useIntl();
return (
<Flex gap={2} paddingTop={4}>
<LinkButton variant="default" isExternal href={CTA_LEARN_MORE_HREF}>
{formatMessage({
id: 'Settings.review-workflows.limit.cta.learn',
defaultMessage: 'Learn more',
})}
</LinkButton>
<LinkButton variant="tertiary" isExternal href={CTA_SALES_HREF}>
{formatMessage({
id: 'Settings.review-workflows.limit.cta.sales',
defaultMessage: 'Contact Sales',
})}
</LinkButton>
</Flex>
);
}
const BalloonImage = styled.img`
// Margin top|right reverse the padding of ModalBody
margin-right: ${({ theme }) => `-${theme.spaces[7]}`};
margin-top: ${({ theme }) => `-${theme.spaces[7]}`};
width: 360px;
`;
export function LimitsModal({ children, isOpen, onClose }) {
const { formatMessage } = useIntl();
if (!isOpen) {
return null;
}
return (
<ModalLayout labelledBy={TITLE_ID}>
<ModalBody>
<Flex gap={2} paddingLeft={7} position="relative">
<Flex alignItems="start" direction="column" gap={2} width="60%">
{children}
<CallToActions />
</Flex>
<Flex justifyContent="end" height="100%" width="40%">
<BalloonImage src={balloonImageSrc} aria-hidden alt="" loading="lazy" />
<Box display="flex" position="absolute" right={0} top={0}>
<IconButton
icon={<Cross />}
aria-label={formatMessage({
id: 'global.close',
defaultMessage: 'Close',
})}
onClick={onClose}
/>
</Box>
</Flex>
</Flex>
</ModalBody>
</ModalLayout>
);
}
LimitsModal.defaultProps = {
isOpen: false,
};
LimitsModal.propTypes = {
children: PropTypes.node.isRequired,
isOpen: PropTypes.bool,
onClose: PropTypes.func.isRequired,
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@ -0,0 +1,3 @@
import { Title, Body, LimitsModal as Root } from './LimitsModal';
export { Title, Body, Root };

View File

@ -0,0 +1,62 @@
import React from 'react';
import { ThemeProvider, lightTheme } from '@strapi/design-system';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from 'react-intl';
import * as LimitsModal from '..';
const setup = (props) => ({
...render(
<LimitsModal.Root isOpen onClose={() => {}} {...props}>
<LimitsModal.Title>Title</LimitsModal.Title>
<LimitsModal.Body>Body</LimitsModal.Body>
</LimitsModal.Root>,
{
wrapper({ children }) {
return (
<ThemeProvider theme={lightTheme}>
<IntlProvider locale="en" messages={{}}>
{children}
</IntlProvider>
</ThemeProvider>
);
},
}
),
user: userEvent.setup(),
});
describe('Admin | Settings | Review Workflow | LimitsModal', () => {
it('should not render the modal if isOpen=false', () => {
const { queryByText } = setup({ isOpen: false });
expect(queryByText('Title')).not.toBeInTheDocument();
expect(queryByText('Body')).not.toBeInTheDocument();
});
it('should render the modal if isOpen=true', () => {
const { getByText } = setup();
expect(getByText('Title')).toBeInTheDocument();
expect(getByText('Body')).toBeInTheDocument();
});
it('should render call to action links', () => {
const { getByText } = setup();
expect(getByText('Learn more')).toBeInTheDocument();
expect(getByText('Contact Sales')).toBeInTheDocument();
});
it('should call onClose callback when closing the modal', async () => {
const onCloseSpy = jest.fn();
const { getByRole, user } = setup({ onClose: onCloseSpy });
await user.click(getByRole('button', { name: /close/i }));
expect(onCloseSpy).toBeCalledTimes(1);
});
});

View File

@ -0,0 +1,44 @@
import { renderHook } from '@testing-library/react';
import { useReviewWorkflowLicenseLimits } from '../useReviewWorkflowLicenseLimits';
// TODO: use msw instead. I wish I could have done it already, but in its current
// state, useLicenseLimits requires to be wrapped in a redux provider and RBAC
// context provider and at the time of writing there wasn't any time to create
// that setup.
jest.mock('../../../../../../hooks', () => ({
...jest.requireActual('../../../../../../hooks'),
useLicenseLimits: jest.fn(() => ({
isLoading: false,
license: {
data: {
something: true,
features: [
{
name: 'review-workflows',
options: {
workflows: 10,
stagesPerWorkflow: 10,
},
},
],
},
},
})),
}));
function setup(...args) {
return renderHook(() => useReviewWorkflowLicenseLimits(...args));
}
describe('useReviewWorkflowLicenseLimits', () => {
it('returns options for the feature only', async () => {
const { result } = setup();
expect(result.current.limits).toStrictEqual({
workflows: 10,
stagesPerWorkflow: 10,
});
});
});

View File

@ -25,6 +25,13 @@ const server = setupServer(
stages: populate === 'stages' ? [STAGE_FIXTURE] : [], stages: populate === 'stages' ? [STAGE_FIXTURE] : [],
}, },
], ],
pagination: {
page: 1,
pageSize: 100,
pageCount: 1,
total: 1,
},
}) })
); );
}), }),
@ -38,6 +45,13 @@ const server = setupServer(
id: 1, id: 1,
stages: populate === 'stages' ? [STAGE_FIXTURE] : [], stages: populate === 'stages' ? [STAGE_FIXTURE] : [],
}, },
pagination: {
page: 1,
pageSize: 100,
pageCount: 1,
total: 1,
},
}) })
); );
}) })
@ -80,6 +94,7 @@ describe('useReviewWorkflows', () => {
expect(result.current).toStrictEqual( expect(result.current).toStrictEqual(
expect.objectContaining({ expect.objectContaining({
status: 'success', status: 'success',
pagination: expect.objectContaining({ total: 1 }),
workflows: [{ id: expect.any(Number), stages: expect.any(Array) }], workflows: [{ id: expect.any(Number), stages: expect.any(Array) }],
}) })
); );
@ -92,6 +107,7 @@ describe('useReviewWorkflows', () => {
expect(result.current).toStrictEqual( expect(result.current).toStrictEqual(
expect.objectContaining({ expect.objectContaining({
pagination: expect.objectContaining({ total: 1 }),
workflows: [expect.objectContaining({ id: 1, stages: expect.any(Array) })], workflows: [expect.objectContaining({ id: 1, stages: expect.any(Array) })],
}) })
); );

View File

@ -0,0 +1,12 @@
import { useLicenseLimits } from '../../../../../hooks';
export function useReviewWorkflowLicenseLimits() {
const { license, isLoading } = useLicenseLimits();
const limits =
license?.data?.features?.filter(({ name }) => name === 'review-workflows')?.[0]?.options ?? {};
return {
limits,
isLoading,
};
}

View File

@ -15,9 +15,7 @@ export function useReviewWorkflows(params = {}) {
['review-workflows', 'workflows', id], ['review-workflows', 'workflows', id],
async () => { async () => {
try { try {
const { const { data } = await get(
data: { data },
} = await get(
`/admin/review-workflows/workflows/${id}${queryString ? `?${queryString}` : ''}` `/admin/review-workflows/workflows/${id}${queryString ? `?${queryString}` : ''}`
); );
@ -31,13 +29,14 @@ export function useReviewWorkflows(params = {}) {
let workflows = []; let workflows = [];
if (id && data) { if (id && data?.data) {
workflows = [data]; workflows = [data.data];
} else if (Array.isArray(data)) { } else if (Array.isArray(data?.data)) {
workflows = data; workflows = data.data;
} }
return { return {
pagination: data?.pagination ?? {},
workflows, workflows,
isLoading, isLoading,
status, status,

View File

@ -13,9 +13,12 @@ import { useContentTypes } from '../../../../../../../../admin/src/hooks/useCont
import { useInjectReducer } from '../../../../../../../../admin/src/hooks/useInjectReducer'; import { useInjectReducer } from '../../../../../../../../admin/src/hooks/useInjectReducer';
import { resetWorkflow } from '../../actions'; import { resetWorkflow } from '../../actions';
import * as Layout from '../../components/Layout'; import * as Layout from '../../components/Layout';
import * as LimitsModal from '../../components/LimitsModal';
import { Stages } from '../../components/Stages'; import { Stages } from '../../components/Stages';
import { WorkflowAttributes } from '../../components/WorkflowAttributes'; import { WorkflowAttributes } from '../../components/WorkflowAttributes';
import { REDUX_NAMESPACE } from '../../constants'; import { REDUX_NAMESPACE } from '../../constants';
import { useReviewWorkflowLicenseLimits } from '../../hooks/useReviewWorkflowLicenseLimits';
import { useReviewWorkflows } from '../../hooks/useReviewWorkflows';
import { reducer, initialState } from '../../reducer'; import { reducer, initialState } from '../../reducer';
import { getWorkflowValidationSchema } from '../../utils/getWorkflowValidationSchema'; import { getWorkflowValidationSchema } from '../../utils/getWorkflowValidationSchema';
@ -32,6 +35,9 @@ export function ReviewWorkflowsCreateView() {
currentWorkflow: { data: currentWorkflow, isDirty: currentWorkflowIsDirty }, currentWorkflow: { data: currentWorkflow, isDirty: currentWorkflowIsDirty },
}, },
} = useSelector((state) => state?.[REDUX_NAMESPACE] ?? initialState); } = useSelector((state) => state?.[REDUX_NAMESPACE] ?? initialState);
const [showLimitModal, setShowLimitModal] = React.useState(false);
const { limits, isLoading: isLicenseLoading } = useReviewWorkflowLicenseLimits();
const { pagination, isLoading: isWorkflowLoading } = useReviewWorkflows();
const { mutateAsync, isLoading } = useMutation( const { mutateAsync, isLoading } = useMutation(
async ({ workflow }) => { async ({ workflow }) => {
@ -88,6 +94,23 @@ export function ReviewWorkflowsCreateView() {
dispatch(resetWorkflow()); dispatch(resetWorkflow());
}, [dispatch]); }, [dispatch]);
React.useEffect(() => {
if (!isWorkflowLoading && !isLicenseLoading) {
if (pagination?.total >= limits?.workflows) {
setShowLimitModal('workflow');
} else if (currentWorkflow.stages.length >= limits.stagesPerWorkflow) {
setShowLimitModal('stage');
}
}
}, [
currentWorkflow.stages.length,
isLicenseLoading,
isWorkflowLoading,
limits.stagesPerWorkflow,
limits?.workflows,
pagination?.total,
]);
return ( return (
<> <>
<Layout.DragLayerRendered /> <Layout.DragLayerRendered />
@ -141,6 +164,44 @@ export function ReviewWorkflowsCreateView() {
</Layout.Root> </Layout.Root>
</Form> </Form>
</FormikProvider> </FormikProvider>
<LimitsModal.Root
isOpen={showLimitModal === 'workflow'}
onClose={() => setShowLimitModal(false)}
>
<LimitsModal.Title>
{formatMessage({
id: 'Settings.review-workflows.create.page.workflows.limit.title',
defaultMessage: 'Youve reached the limit of workflows in your plan',
})}
</LimitsModal.Title>
<LimitsModal.Body>
{formatMessage({
id: 'Settings.review-workflows.create.page.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: 'Settings.review-workflows.create.page.stages.limit.title',
defaultMessage: 'You have reached the limit of stages for this workflow in your plan',
})}
</LimitsModal.Title>
<LimitsModal.Body>
{formatMessage({
id: 'Settings.review-workflows.create.page.stages.limit.body',
defaultMessage: 'Try deleting some stages or contact Sales to enable more stages.',
})}
</LimitsModal.Body>
</LimitsModal.Root>
</> </>
); );
} }

View File

@ -19,9 +19,11 @@ import { useContentTypes } from '../../../../../../../../admin/src/hooks/useCont
import { useInjectReducer } from '../../../../../../../../admin/src/hooks/useInjectReducer'; import { useInjectReducer } from '../../../../../../../../admin/src/hooks/useInjectReducer';
import { setWorkflow } from '../../actions'; import { setWorkflow } from '../../actions';
import * as Layout from '../../components/Layout'; import * as Layout from '../../components/Layout';
import * as LimitsModal from '../../components/LimitsModal';
import { Stages } from '../../components/Stages'; import { Stages } from '../../components/Stages';
import { WorkflowAttributes } from '../../components/WorkflowAttributes'; import { WorkflowAttributes } from '../../components/WorkflowAttributes';
import { REDUX_NAMESPACE } from '../../constants'; import { REDUX_NAMESPACE } from '../../constants';
import { useReviewWorkflowLicenseLimits } from '../../hooks/useReviewWorkflowLicenseLimits';
import { useReviewWorkflows } from '../../hooks/useReviewWorkflows'; import { useReviewWorkflows } from '../../hooks/useReviewWorkflows';
import { reducer, initialState } from '../../reducer'; import { reducer, initialState } from '../../reducer';
import { getWorkflowValidationSchema } from '../../utils/getWorkflowValidationSchema'; import { getWorkflowValidationSchema } from '../../utils/getWorkflowValidationSchema';
@ -35,6 +37,8 @@ export function ReviewWorkflowsEditView() {
const { formatAPIError } = useAPIErrorHandler(); const { formatAPIError } = useAPIErrorHandler();
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const { const {
isLoading: isWorkflowLoading,
pagination,
workflows: [workflow], workflows: [workflow],
status: workflowStatus, status: workflowStatus,
refetch, refetch,
@ -51,6 +55,8 @@ export function ReviewWorkflowsEditView() {
}, },
} = useSelector((state) => state?.[REDUX_NAMESPACE] ?? initialState); } = useSelector((state) => state?.[REDUX_NAMESPACE] ?? initialState);
const [isConfirmDeleteDialogOpen, setIsConfirmDeleteDialogOpen] = React.useState(false); const [isConfirmDeleteDialogOpen, setIsConfirmDeleteDialogOpen] = React.useState(false);
const { limits, isLoading: isLicenseLoading } = useReviewWorkflowLicenseLimits();
const [showLimitModal, setShowLimitModal] = React.useState(false);
const { mutateAsync, isLoading } = useMutation( const { mutateAsync, isLoading } = useMutation(
async ({ workflow }) => { async ({ workflow }) => {
@ -126,6 +132,23 @@ export function ReviewWorkflowsEditView() {
dispatch(setWorkflow({ status: workflowStatus, data: workflow })); dispatch(setWorkflow({ status: workflowStatus, data: workflow }));
}, [workflowStatus, workflow, dispatch]); }, [workflowStatus, workflow, dispatch]);
React.useEffect(() => {
if (!isWorkflowLoading && !isLicenseLoading) {
if (pagination?.total >= limits?.workflows) {
setShowLimitModal('workflow');
} else if (currentWorkflow.stages.length >= limits?.stagesPerWorkflow) {
setShowLimitModal('stage');
}
}
}, [
currentWorkflow.stages.length,
isLicenseLoading,
isWorkflowLoading,
limits?.stagesPerWorkflow,
limits?.workflows,
pagination?.total,
]);
// TODO redirect back to list-view if workflow is not found? // TODO redirect back to list-view if workflow is not found?
return ( return (
@ -191,6 +214,44 @@ export function ReviewWorkflowsEditView() {
onToggleDialog={toggleConfirmDeleteDialog} onToggleDialog={toggleConfirmDeleteDialog}
onConfirm={handleConfirmDeleteDialog} onConfirm={handleConfirmDeleteDialog}
/> />
<LimitsModal.Root
isOpen={showLimitModal === 'workflow'}
onClose={() => setShowLimitModal(false)}
>
<LimitsModal.Title>
{formatMessage({
id: 'Settings.review-workflows.edit.page.workflows.limit.title',
defaultMessage: 'Youve reached the limit of workflows in your plan',
})}
</LimitsModal.Title>
<LimitsModal.Body>
{formatMessage({
id: 'Settings.review-workflows.edit.page.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: 'Settings.review-workflows.edit.page.stages.limit.title',
defaultMessage: 'You have reached the limit of stages for this workflow in your plan',
})}
</LimitsModal.Title>
<LimitsModal.Body>
{formatMessage({
id: 'Settings.review-workflows.edit.page.stages.limit.body',
defaultMessage: 'Try deleting some stages or contact Sales to enable more stages.',
})}
</LimitsModal.Body>
</LimitsModal.Root>
</> </>
); );
} }

View File

@ -32,6 +32,8 @@ import styled from 'styled-components';
import { useContentTypes } from '../../../../../../../../admin/src/hooks/useContentTypes'; import { useContentTypes } from '../../../../../../../../admin/src/hooks/useContentTypes';
import * as Layout from '../../components/Layout'; import * as Layout from '../../components/Layout';
import * as LimitsModal from '../../components/LimitsModal';
import { useReviewWorkflowLicenseLimits } from '../../hooks/useReviewWorkflowLicenseLimits';
import { useReviewWorkflows } from '../../hooks/useReviewWorkflows'; import { useReviewWorkflows } from '../../hooks/useReviewWorkflows';
const ActionLink = styled(Link)` const ActionLink = styled(Link)`
@ -65,11 +67,13 @@ export function ReviewWorkflowsListView() {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { push } = useHistory(); const { push } = useHistory();
const { collectionTypes, singleTypes, isLoading: isLoadingModels } = useContentTypes(); const { collectionTypes, singleTypes, isLoading: isLoadingModels } = useContentTypes();
const { workflows, isLoading, refetch } = useReviewWorkflows(); const { pagination, workflows, isLoading, refetch } = useReviewWorkflows();
const [workflowToDelete, setWorkflowToDelete] = React.useState(null); const [workflowToDelete, setWorkflowToDelete] = React.useState(null);
const [showLimitModal, setShowLimitModal] = React.useState(false);
const { del } = useFetchClient(); const { del } = useFetchClient();
const { formatAPIError } = useAPIErrorHandler(); const { formatAPIError } = useAPIErrorHandler();
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const { limits } = useReviewWorkflowLicenseLimits();
const { mutateAsync, isLoading: isLoadingMutation } = useMutation( const { mutateAsync, isLoading: isLoadingMutation } = useMutation(
async ({ workflowId, stages }) => { async ({ workflowId, stages }) => {
@ -129,7 +133,17 @@ export function ReviewWorkflowsListView() {
<> <>
<Layout.Header <Layout.Header
primaryAction={ primaryAction={
<LinkButton startIcon={<Plus />} size="S" to="/settings/review-workflows/create"> <LinkButton
startIcon={<Plus />}
size="S"
to="/settings/review-workflows/create"
onClick={(event) => {
if (pagination?.total >= limits.workflows) {
event.preventDefault();
setShowLimitModal(true);
}
}}
>
{formatMessage({ {formatMessage({
id: 'Settings.review-workflows.list.page.create', id: 'Settings.review-workflows.list.page.create',
defaultMessage: 'Create new workflow', defaultMessage: 'Create new workflow',
@ -158,9 +172,18 @@ export function ReviewWorkflowsListView() {
) : ( ) : (
<Table <Table
colCount={3} colCount={3}
// TODO: we should be able to use a link here instead of an (inaccessible onClick) handler
footer={ footer={
<TFooter icon={<Plus />} onClick={() => push('/settings/review-workflows/create')}> // TODO: we should be able to use a link here instead of an (inaccessible onClick) handler
<TFooter
icon={<Plus />}
onClick={() => {
if (pagination?.total >= limits?.workflows) {
setShowLimitModal(true);
} else {
push('/settings/review-workflows/create');
}
}}
>
{formatMessage({ {formatMessage({
id: 'Settings.review-workflows.list.page.create', id: 'Settings.review-workflows.list.page.create',
defaultMessage: 'Create new workflow', defaultMessage: 'Create new workflow',
@ -283,6 +306,22 @@ export function ReviewWorkflowsListView() {
onToggleDialog={toggleConfirmDeleteDialog} onToggleDialog={toggleConfirmDeleteDialog}
onConfirm={handleConfirmDeleteDialog} onConfirm={handleConfirmDeleteDialog}
/> />
<LimitsModal.Root isOpen={showLimitModal} onClose={() => setShowLimitModal(false)}>
<LimitsModal.Title>
{formatMessage({
id: 'Settings.review-workflows.list.page.workflows.limit.title',
defaultMessage: 'Youve reached the limit of workflows in your plan',
})}
</LimitsModal.Title>
<LimitsModal.Body>
{formatMessage({
id: 'Settings.review-workflows.list.page.workflows.limit.body',
defaultMessage: 'Delete a workflow or contact Sales to enable more workflows.',
})}
</LimitsModal.Body>
</LimitsModal.Root>
</Layout.Root> </Layout.Root>
</> </>
); );