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 4326a729c2..3a215aaa10 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 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 { getStageColorByHex } from '../../../../pages/SettingsPage/pages/ReviewWorkflows/utils/colors'; @@ -33,8 +35,11 @@ export function InformationBoxEE() { const { formatMessage } = useIntl(); const { formatAPIError } = useAPIErrorHandler(); const toggleNotification = useNotification(); + const { limits } = useReviewWorkflowLicenseLimits(); + const [showLimitModal, setShowLimitModal] = React.useState(false); const { + pagination, workflows: [workflow], isLoading: isWorkflowLoading, } = useReviewWorkflows({ filters: { contentTypes: uid } }); @@ -72,6 +77,17 @@ export function InformationBoxEE() { const handleStageChange = async ({ value: stageId }) => { try { + if (limits?.workflows > pagination.total) { + setShowLimitModal('workflow'); + + return; + } + if (limits?.stagesPerWorkflow > workflow.stages.length) { + setShowLimitModal('stage'); + + return; + } + await mutateAsync({ entityId: initialData.id, stageId, @@ -152,6 +168,44 @@ export function InformationBoxEE() { )} + + 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/tests/InformationBoxEE.test.js b/packages/core/admin/ee/admin/content-manager/pages/EditView/InformationBox/tests/InformationBoxEE.test.js index b73afa1082..8aa3941b0a 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 @@ -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 { useCMEditViewDataManager } from '@strapi/helper-plugin'; +import { useCMEditViewDataManager, RBACContext } from '@strapi/helper-plugin'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { IntlProvider } from 'react-intl'; @@ -66,13 +67,27 @@ const ComponentFixture = (props) => ; const setup = (props) => ({ ...render(, { 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 ( - {children} + + {children} + diff --git a/packages/core/admin/ee/admin/hooks/useLicenseLimits/index.js b/packages/core/admin/ee/admin/hooks/useLicenseLimits/index.js index 9d2a0ff734..aa4c8f79e1 100644 --- a/packages/core/admin/ee/admin/hooks/useLicenseLimits/index.js +++ b/packages/core/admin/ee/admin/hooks/useLicenseLimits/index.js @@ -6,27 +6,27 @@ import { selectAdminPermissions } from '../../../../admin/src/pages/App/selector const useLicenseLimits = () => { const permissions = useSelector(selectAdminPermissions); - const rbac = useRBAC(permissions.settings.users); - + const { get } = useFetchClient(); const { isLoading: isRBACLoading, allowedActions: { canRead, canCreate, canUpdate, canDelete }, - } = rbac; + } = useRBAC(permissions.settings.users); const isRBACAllowed = canRead && canCreate && canUpdate && canDelete; - const { get } = useFetchClient(); - const fetchLicenseLimitInfo = async () => { - const { - data: { data }, - } = await get('/admin/license-limit-information'); + const license = useQuery( + ['ee', 'license-limit-info'], + async () => { + const { + data: { data }, + } = await get('/admin/license-limit-information'); - return data; - }; - - const license = useQuery(['ee', 'license-limit-info'], fetchLicenseLimitInfo, { - enabled: !isRBACLoading && isRBACAllowed, - }); + return data; + }, + { + enabled: !isRBACLoading && isRBACAllowed, + } + ); return { license }; }; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/LimitsModal.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/LimitsModal.js new file mode 100644 index 0000000000..7b14f93944 --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/LimitsModal.js @@ -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 ( + + {children} + + ); +} + +Title.propTypes = { + children: PropTypes.node.isRequired, +}; + +export function Body({ children }) { + return {children}; +} + +Body.propTypes = { + children: PropTypes.node.isRequired, +}; + +function CallToActions() { + const { formatMessage } = useIntl(); + + return ( + + + {formatMessage({ + id: 'Settings.review-workflows.limit.cta.learn', + defaultMessage: 'Learn more', + })} + + + + {formatMessage({ + id: 'Settings.review-workflows.limit.cta.sales', + defaultMessage: 'Contact Sales', + })} + + + ); +} + +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 ( + + + + + {children} + + + + + + + + + } + aria-label={formatMessage({ + id: 'global.close', + defaultMessage: 'Close', + })} + onClick={onClose} + /> + + + + + + ); +} + +LimitsModal.defaultProps = { + isOpen: false, +}; + +LimitsModal.propTypes = { + children: PropTypes.node.isRequired, + isOpen: PropTypes.bool, + onClose: PropTypes.func.isRequired, +}; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/assets/balloon.png b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/assets/balloon.png new file mode 100644 index 0000000000..8a779c8993 Binary files /dev/null and b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/assets/balloon.png differ diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/index.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/index.js new file mode 100644 index 0000000000..ef4508bceb --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/index.js @@ -0,0 +1,3 @@ +import { Title, Body, LimitsModal as Root } from './LimitsModal'; + +export { Title, Body, Root }; diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/tests/LimitsModal.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/tests/LimitsModal.test.js new file mode 100644 index 0000000000..315a5c6332 --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/tests/LimitsModal.test.js @@ -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( + {}} {...props}> + Title + Body + , + { + wrapper({ children }) { + return ( + + + {children} + + + ); + }, + } + ), + + 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); + }); +}); diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/tests/useReviewWorkflowLicenseLimits.test.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/tests/useReviewWorkflowLicenseLimits.test.js new file mode 100644 index 0000000000..6fec4f9881 --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/tests/useReviewWorkflowLicenseLimits.test.js @@ -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, + }); + }); +}); 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 index 530f2b381d..c6979f9d14 100644 --- 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 @@ -25,6 +25,13 @@ const server = setupServer( stages: populate === 'stages' ? [STAGE_FIXTURE] : [], }, ], + + pagination: { + page: 1, + pageSize: 100, + pageCount: 1, + total: 1, + }, }) ); }), @@ -38,6 +45,13 @@ const server = setupServer( id: 1, 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.objectContaining({ status: 'success', + pagination: expect.objectContaining({ total: 1 }), workflows: [{ id: expect.any(Number), stages: expect.any(Array) }], }) ); @@ -92,6 +107,7 @@ describe('useReviewWorkflows', () => { expect(result.current).toStrictEqual( expect.objectContaining({ + pagination: expect.objectContaining({ total: 1 }), workflows: [expect.objectContaining({ id: 1, stages: expect.any(Array) })], }) ); diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflowLicenseLimits.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflowLicenseLimits.js new file mode 100644 index 0000000000..51b38e8ee1 --- /dev/null +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflowLicenseLimits.js @@ -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, + }; +} diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows.js index 9d186bdc55..eb2b64a010 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows.js @@ -15,9 +15,7 @@ export function useReviewWorkflows(params = {}) { ['review-workflows', 'workflows', id], async () => { try { - const { - data: { data }, - } = await get( + const { data } = await get( `/admin/review-workflows/workflows/${id}${queryString ? `?${queryString}` : ''}` ); @@ -31,13 +29,14 @@ export function useReviewWorkflows(params = {}) { let workflows = []; - if (id && data) { - workflows = [data]; - } else if (Array.isArray(data)) { - workflows = data; + if (id && data?.data) { + workflows = [data.data]; + } else if (Array.isArray(data?.data)) { + workflows = data.data; } return { + pagination: data?.pagination ?? {}, workflows, isLoading, status, diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/CreateView/CreateView.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/CreateView/CreateView.js index a8a895a1cb..0a41179ba5 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/CreateView/CreateView.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/CreateView/CreateView.js @@ -13,9 +13,12 @@ import { useContentTypes } from '../../../../../../../../admin/src/hooks/useCont import { useInjectReducer } from '../../../../../../../../admin/src/hooks/useInjectReducer'; import { resetWorkflow } from '../../actions'; import * as Layout from '../../components/Layout'; +import * as LimitsModal from '../../components/LimitsModal'; import { Stages } from '../../components/Stages'; import { WorkflowAttributes } from '../../components/WorkflowAttributes'; import { REDUX_NAMESPACE } from '../../constants'; +import { useReviewWorkflowLicenseLimits } from '../../hooks/useReviewWorkflowLicenseLimits'; +import { useReviewWorkflows } from '../../hooks/useReviewWorkflows'; import { reducer, initialState } from '../../reducer'; import { getWorkflowValidationSchema } from '../../utils/getWorkflowValidationSchema'; @@ -32,6 +35,9 @@ export function ReviewWorkflowsCreateView() { currentWorkflow: { data: currentWorkflow, isDirty: currentWorkflowIsDirty }, }, } = 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( async ({ workflow }) => { @@ -88,6 +94,23 @@ export function ReviewWorkflowsCreateView() { dispatch(resetWorkflow()); }, [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 ( <> @@ -141,6 +164,44 @@ export function ReviewWorkflowsCreateView() { + + setShowLimitModal(false)} + > + + {formatMessage({ + id: 'Settings.review-workflows.create.page.workflows.limit.title', + defaultMessage: 'You’ve reached the limit of workflows in your plan', + })} + + + + {formatMessage({ + id: 'Settings.review-workflows.create.page.workflows.limit.body', + defaultMessage: 'Delete a workflow or contact Sales to enable more workflows.', + })} + + + + setShowLimitModal(false)} + > + + {formatMessage({ + id: 'Settings.review-workflows.create.page.stages.limit.title', + defaultMessage: 'You have reached the limit of stages for this workflow in your plan', + })} + + + + {formatMessage({ + id: 'Settings.review-workflows.create.page.stages.limit.body', + defaultMessage: 'Try deleting some stages or contact Sales to enable more stages.', + })} + + ); } diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/EditView/EditView.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/EditView/EditView.js index 664ceac188..7e015b8ca2 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/EditView/EditView.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/EditView/EditView.js @@ -19,9 +19,11 @@ import { useContentTypes } from '../../../../../../../../admin/src/hooks/useCont import { useInjectReducer } from '../../../../../../../../admin/src/hooks/useInjectReducer'; import { setWorkflow } from '../../actions'; import * as Layout from '../../components/Layout'; +import * as LimitsModal from '../../components/LimitsModal'; import { Stages } from '../../components/Stages'; import { WorkflowAttributes } from '../../components/WorkflowAttributes'; import { REDUX_NAMESPACE } from '../../constants'; +import { useReviewWorkflowLicenseLimits } from '../../hooks/useReviewWorkflowLicenseLimits'; import { useReviewWorkflows } from '../../hooks/useReviewWorkflows'; import { reducer, initialState } from '../../reducer'; import { getWorkflowValidationSchema } from '../../utils/getWorkflowValidationSchema'; @@ -35,6 +37,8 @@ export function ReviewWorkflowsEditView() { const { formatAPIError } = useAPIErrorHandler(); const toggleNotification = useNotification(); const { + isLoading: isWorkflowLoading, + pagination, workflows: [workflow], status: workflowStatus, refetch, @@ -51,6 +55,8 @@ export function ReviewWorkflowsEditView() { }, } = useSelector((state) => state?.[REDUX_NAMESPACE] ?? initialState); const [isConfirmDeleteDialogOpen, setIsConfirmDeleteDialogOpen] = React.useState(false); + const { limits, isLoading: isLicenseLoading } = useReviewWorkflowLicenseLimits(); + const [showLimitModal, setShowLimitModal] = React.useState(false); const { mutateAsync, isLoading } = useMutation( async ({ workflow }) => { @@ -126,6 +132,23 @@ export function ReviewWorkflowsEditView() { dispatch(setWorkflow({ status: workflowStatus, data: workflow })); }, [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? return ( @@ -191,6 +214,44 @@ export function ReviewWorkflowsEditView() { onToggleDialog={toggleConfirmDeleteDialog} onConfirm={handleConfirmDeleteDialog} /> + + setShowLimitModal(false)} + > + + {formatMessage({ + id: 'Settings.review-workflows.edit.page.workflows.limit.title', + defaultMessage: 'You’ve reached the limit of workflows in your plan', + })} + + + + {formatMessage({ + id: 'Settings.review-workflows.edit.page.workflows.limit.body', + defaultMessage: 'Delete a workflow or contact Sales to enable more workflows.', + })} + + + + setShowLimitModal(false)} + > + + {formatMessage({ + id: 'Settings.review-workflows.edit.page.stages.limit.title', + defaultMessage: 'You have reached the limit of stages for this workflow in your plan', + })} + + + + {formatMessage({ + id: 'Settings.review-workflows.edit.page.stages.limit.body', + defaultMessage: 'Try deleting some stages or contact Sales to enable more stages.', + })} + + ); } diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/ListView/ListView.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/ListView/ListView.js index 10e200a33c..4291e54876 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/ListView/ListView.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/ListView/ListView.js @@ -32,6 +32,8 @@ import styled from 'styled-components'; import { useContentTypes } from '../../../../../../../../admin/src/hooks/useContentTypes'; import * as Layout from '../../components/Layout'; +import * as LimitsModal from '../../components/LimitsModal'; +import { useReviewWorkflowLicenseLimits } from '../../hooks/useReviewWorkflowLicenseLimits'; import { useReviewWorkflows } from '../../hooks/useReviewWorkflows'; const ActionLink = styled(Link)` @@ -65,11 +67,13 @@ export function ReviewWorkflowsListView() { const { formatMessage } = useIntl(); const { push } = useHistory(); const { collectionTypes, singleTypes, isLoading: isLoadingModels } = useContentTypes(); - const { workflows, isLoading, refetch } = useReviewWorkflows(); + const { pagination, workflows, isLoading, refetch } = useReviewWorkflows(); const [workflowToDelete, setWorkflowToDelete] = React.useState(null); + const [showLimitModal, setShowLimitModal] = React.useState(false); const { del } = useFetchClient(); const { formatAPIError } = useAPIErrorHandler(); const toggleNotification = useNotification(); + const { limits } = useReviewWorkflowLicenseLimits(); const { mutateAsync, isLoading: isLoadingMutation } = useMutation( async ({ workflowId, stages }) => { @@ -129,7 +133,17 @@ export function ReviewWorkflowsListView() { <> } size="S" to="/settings/review-workflows/create"> + } + size="S" + to="/settings/review-workflows/create" + onClick={(event) => { + if (pagination?.total >= limits.workflows) { + event.preventDefault(); + setShowLimitModal(true); + } + }} + > {formatMessage({ id: 'Settings.review-workflows.list.page.create', defaultMessage: 'Create new workflow', @@ -158,9 +172,18 @@ export function ReviewWorkflowsListView() { ) : ( } onClick={() => push('/settings/review-workflows/create')}> + // TODO: we should be able to use a link here instead of an (inaccessible onClick) handler + } + onClick={() => { + if (pagination?.total >= limits?.workflows) { + setShowLimitModal(true); + } else { + push('/settings/review-workflows/create'); + } + }} + > {formatMessage({ id: 'Settings.review-workflows.list.page.create', defaultMessage: 'Create new workflow', @@ -283,6 +306,22 @@ export function ReviewWorkflowsListView() { onToggleDialog={toggleConfirmDeleteDialog} onConfirm={handleConfirmDeleteDialog} /> + + setShowLimitModal(false)}> + + {formatMessage({ + id: 'Settings.review-workflows.list.page.workflows.limit.title', + defaultMessage: 'You’ve reached the limit of workflows in your plan', + })} + + + + {formatMessage({ + id: 'Settings.review-workflows.list.page.workflows.limit.body', + defaultMessage: 'Delete a workflow or contact Sales to enable more workflows.', + })} + + );