mirror of
https://github.com/strapi/strapi.git
synced 2025-09-22 06:50:51 +00:00
Merge pull request #17101 from strapi/feature/review-workflow-multiple-workflow-limits
This commit is contained in:
commit
4e45c46dc4
@ -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: '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>
|
||||||
</Information.Root>
|
</Information.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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 };
|
||||||
};
|
};
|
||||||
|
@ -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 |
@ -0,0 +1,3 @@
|
|||||||
|
import { Title, Body, LimitsModal as Root } from './LimitsModal';
|
||||||
|
|
||||||
|
export { Title, Body, Root };
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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) })],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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: 'You’ve 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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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: 'You’ve 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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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: 'You’ve 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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user