chore(admin): convert review workflows page to TS (fix feedback)

This commit is contained in:
HichamELBSI 2023-12-06 14:16:21 +01:00 committed by ELABBASSI Hicham
parent 31c6013e13
commit 59a6b3828b
22 changed files with 331 additions and 289 deletions

View File

@ -19,11 +19,7 @@ import {
import { useIntl } from 'react-intl';
import { useMutation } from 'react-query';
import {
LimitsModal,
Body,
Title,
} from '../../../../../../pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal';
import { LimitsModal } from '../../../../../../pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal';
import { useLicenseLimits } from '../../../../../hooks/useLicenseLimits';
import {
CHARGEBEE_STAGES_PER_WORKFLOW_ENTITLEMENT_NAME,
@ -217,37 +213,43 @@ export function StageSelect() {
</Flex>
</Field>
<LimitsModal isOpen={showLimitModal === 'workflow'} onClose={() => setShowLimitModal(false)}>
<Title>
<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',
})}
</Title>
</LimitsModal.Title>
<Body>
<LimitsModal.Body>
{formatMessage({
id: 'content-manager.reviewWorkflows.workflows.limit.body',
defaultMessage: 'Delete a workflow or contact Sales to enable more workflows.',
})}
</Body>
</LimitsModal>
</LimitsModal.Body>
</LimitsModal.Root>
<LimitsModal isOpen={showLimitModal === 'stage'} onClose={() => setShowLimitModal(false)}>
<Title>
<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',
})}
</Title>
</LimitsModal.Title>
<Body>
<LimitsModal.Body>
{formatMessage({
id: 'content-manager.reviewWorkflows.stages.limit.body',
defaultMessage: 'Try deleting some stages or contact Sales to enable more stages.',
})}
</Body>
</LimitsModal>
</LimitsModal.Body>
</LimitsModal.Root>
</>
);
}

View File

@ -31,7 +31,7 @@ function useLicenseLimits({ enabled }: UseLicenseLimitsArgs = { enabled: true })
type FeatureNames = GetLicenseLimitInformation.Response['data']['features'][number]['name'];
type GetFeatureType = (name: FeatureNames) => Record<string, unknown> | undefined;
type GetFeatureType = <T>(name: FeatureNames) => Record<string, T> | undefined;
const getFeature = React.useCallback<GetFeatureType>(
(name) => {

View File

@ -32,7 +32,7 @@ import {
setWorkflows,
} from './actions';
import * as Layout from './components/Layout';
import { LimitsModal, Body, Title } from './components/LimitsModal';
import { LimitsModal } from './components/LimitsModal';
import { Stages } from './components/Stages';
import { WorkflowAttributes } from './components/WorkflowAttributes';
import {
@ -71,9 +71,9 @@ export const ReviewWorkflowsCreatePage = () => {
const [initialErrors, setInitialErrors] = React.useState<FormikErrors<CurrentWorkflow>>();
const [savePrompts, setSavePrompts] = React.useState<{ hasReassignedContentTypes?: boolean }>({});
const limits = getFeature('review-workflows');
const numberOfWorkflows = limits?.[CHARGEBEE_WORKFLOW_ENTITLEMENT_NAME] as string;
const stagesPerWorkflow = limits?.[CHARGEBEE_STAGES_PER_WORKFLOW_ENTITLEMENT_NAME] as string;
const limits = getFeature<string>('review-workflows');
const numberOfWorkflows = limits?.[CHARGEBEE_WORKFLOW_ENTITLEMENT_NAME];
const stagesPerWorkflow = limits?.[CHARGEBEE_STAGES_PER_WORKFLOW_ENTITLEMENT_NAME];
const contentTypesFromOtherWorkflows = workflows?.flatMap((workflow) => workflow.contentTypes);
const { mutateAsync } = useMutation<
@ -165,7 +165,7 @@ export const ReviewWorkflowsCreatePage = () => {
* update, because it would throw an API error.
*/
if (meta && meta?.workflowCount >= parseInt(numberOfWorkflows, 10)) {
if (meta && numberOfWorkflows && meta?.workflowCount >= parseInt(numberOfWorkflows, 10)) {
setShowLimitModal('workflow');
/**
@ -175,6 +175,7 @@ export const ReviewWorkflowsCreatePage = () => {
*/
} else if (
currentWorkflow.stages &&
stagesPerWorkflow &&
currentWorkflow.stages.length >= parseInt(stagesPerWorkflow, 10)
) {
setShowLimitModal('stage');
@ -242,6 +243,7 @@ export const ReviewWorkflowsCreatePage = () => {
if (
currentWorkflow.stages &&
limits?.[CHARGEBEE_STAGES_PER_WORKFLOW_ENTITLEMENT_NAME] &&
stagesPerWorkflow &&
currentWorkflow.stages.length >= parseInt(stagesPerWorkflow, 10)
) {
setShowLimitModal('stage');
@ -250,7 +252,7 @@ export const ReviewWorkflowsCreatePage = () => {
}, [isLicenseLoading, isLoadingWorkflow, limits, currentWorkflow.stages, stagesPerWorkflow]);
React.useEffect(() => {
if (!isLoading && roles.length === 0) {
if (!isLoading && roles?.length === 0) {
toggleNotification({
blockTransition: true,
type: 'warning',
@ -349,37 +351,40 @@ export const ReviewWorkflowsCreatePage = () => {
</ConfirmDialog.Body>
</ConfirmDialog.Root>
<LimitsModal isOpen={showLimitModal === 'workflow'} onClose={() => setShowLimitModal(null)}>
<Title>
<LimitsModal.Root
isOpen={showLimitModal === 'workflow'}
onClose={() => setShowLimitModal(null)}
>
<LimitsModal.Title>
{formatMessage({
id: 'Settings.review-workflows.create.page.workflows.limit.title',
defaultMessage: 'Youve reached the limit of workflows in your plan',
})}
</Title>
</LimitsModal.Title>
<Body>
<LimitsModal.Body>
{formatMessage({
id: 'Settings.review-workflows.create.page.workflows.limit.body',
defaultMessage: 'Delete a workflow or contact Sales to enable more workflows.',
})}
</Body>
</LimitsModal>
</LimitsModal.Body>
</LimitsModal.Root>
<LimitsModal isOpen={showLimitModal === 'stage'} onClose={() => setShowLimitModal(null)}>
<Title>
<LimitsModal.Root isOpen={showLimitModal === 'stage'} onClose={() => setShowLimitModal(null)}>
<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',
})}
</Title>
</LimitsModal.Title>
<Body>
<LimitsModal.Body>
{formatMessage({
id: 'Settings.review-workflows.create.page.stages.limit.body',
defaultMessage: 'Try deleting some stages or contact Sales to enable more stages.',
})}
</Body>
</LimitsModal>
</LimitsModal.Body>
</LimitsModal.Root>
</>
);
};

View File

@ -22,7 +22,7 @@ import { useAdminRoles } from '../../../../../../../admin/src/hooks/useAdminRole
import { useContentTypes } from '../../../../../../../admin/src/hooks/useContentTypes';
import { useInjectReducer } from '../../../../../../../admin/src/hooks/useInjectReducer';
import { selectAdminPermissions } from '../../../../../../../admin/src/selectors';
import { Stage, Update } from '../../../../../../../shared/contracts/review-workflows';
import { Stage, Update, Workflow } from '../../../../../../../shared/contracts/review-workflows';
import { useLicenseLimits } from '../../../../hooks/useLicenseLimits';
import {
@ -34,7 +34,7 @@ import {
setWorkflows,
} from './actions';
import * as Layout from './components/Layout';
import { LimitsModal, Body, Title } from './components/LimitsModal';
import { LimitsModal } from './components/LimitsModal';
import { Stages } from './components/Stages';
import { WorkflowAttributes } from './components/WorkflowAttributes';
import {
@ -89,9 +89,9 @@ export const ReviewWorkflowsEditPage = () => {
?.filter((workflow) => workflow.id !== parseInt(workflowId, 10))
.flatMap((workflow) => workflow.contentTypes);
const limits = getFeature('review-workflows');
const numberOfWorkflows = limits?.[CHARGEBEE_WORKFLOW_ENTITLEMENT_NAME] as string;
const stagesPerWorkflow = limits?.[CHARGEBEE_STAGES_PER_WORKFLOW_ENTITLEMENT_NAME] as string;
const limits = getFeature<string>('review-workflows');
const numberOfWorkflows = limits?.[CHARGEBEE_WORKFLOW_ENTITLEMENT_NAME];
const stagesPerWorkflow = limits?.[CHARGEBEE_STAGES_PER_WORKFLOW_ENTITLEMENT_NAME];
const { mutateAsync, isLoading: isLoadingMutation } = useMutation<
Update.Response['data'],
@ -117,7 +117,7 @@ export const ReviewWorkflowsEditPage = () => {
}
);
const updateWorkflow = async (workflow: CurrentWorkflow) => {
const updateWorkflow = async (workflow: Partial<Workflow>) => {
// reset the error messages
setInitialErrors(undefined);
@ -131,16 +131,16 @@ export const ReviewWorkflowsEditPage = () => {
// permissions to see roles
stages: workflow.stages?.map((stage) => {
let hasUpdatedPermissions = true;
const serverStage = serverState.workflow?.stages.find(
const serverStage = serverState.workflow?.stages?.find(
(serverStage) => serverStage.id === stage?.id
);
if (serverStage) {
hasUpdatedPermissions =
serverStage.permissions?.length !== stage.permissions?.length ||
!serverStage.permissions.every(
!serverStage.permissions?.every(
(serverPermission) =>
!!stage.permissions.find(
!!stage.permissions?.find(
(permission) => permission.role === serverPermission.role
)
);
@ -149,7 +149,7 @@ export const ReviewWorkflowsEditPage = () => {
return {
...stage,
permissions: hasUpdatedPermissions ? stage.permissions : undefined,
} as Stage;
} satisfies Stage;
}),
},
});
@ -296,10 +296,11 @@ export const ReviewWorkflowsEditPage = () => {
React.useEffect(() => {
if (!isLoadingWorkflow && !isLicenseLoading) {
if (meta && meta?.workflowCount > parseInt(numberOfWorkflows, 10)) {
if (meta && numberOfWorkflows && meta?.workflowCount > parseInt(numberOfWorkflows, 10)) {
setShowLimitModal('workflow');
} else if (
currentWorkflow.stages &&
stagesPerWorkflow &&
currentWorkflow.stages.length > parseInt(stagesPerWorkflow, 10)
) {
setShowLimitModal('stage');
@ -316,7 +317,7 @@ export const ReviewWorkflowsEditPage = () => {
]);
React.useEffect(() => {
if (!isLoading && roles.length === 0) {
if (!isLoading && roles?.length === 0) {
toggleNotification({
blockTransition: true,
type: 'warning',
@ -438,37 +439,40 @@ export const ReviewWorkflowsEditPage = () => {
</ConfirmDialog.Body>
</ConfirmDialog.Root>
<LimitsModal isOpen={showLimitModal === 'workflow'} onClose={() => setShowLimitModal(null)}>
<Title>
<LimitsModal.Root
isOpen={showLimitModal === 'workflow'}
onClose={() => setShowLimitModal(null)}
>
<LimitsModal.Title>
{formatMessage({
id: 'Settings.review-workflows.edit.page.workflows.limit.title',
defaultMessage: 'Youve reached the limit of workflows in your plan',
})}
</Title>
</LimitsModal.Title>
<Body>
<LimitsModal.Body>
{formatMessage({
id: 'Settings.review-workflows.edit.page.workflows.limit.body',
defaultMessage: 'Delete a workflow or contact Sales to enable more workflows.',
})}
</Body>
</LimitsModal>
</LimitsModal.Body>
</LimitsModal.Root>
<LimitsModal isOpen={showLimitModal === 'stage'} onClose={() => setShowLimitModal(null)}>
<Title>
<LimitsModal.Root isOpen={showLimitModal === 'stage'} onClose={() => setShowLimitModal(null)}>
<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',
})}
</Title>
</LimitsModal.Title>
<Body>
<LimitsModal.Body>
{formatMessage({
id: 'Settings.review-workflows.edit.page.stages.limit.body',
defaultMessage: 'Try deleting some stages or contact Sales to enable more stages.',
})}
</Body>
</LimitsModal>
</LimitsModal.Body>
</LimitsModal.Root>
</>
);
};

View File

@ -41,7 +41,7 @@ import { Update } from '../../../../../../../shared/contracts/review-workflows';
import { useLicenseLimits } from '../../../../hooks/useLicenseLimits';
import * as Layout from './components/Layout';
import { LimitsModal, Title, Body } from './components/LimitsModal';
import { LimitsModal } from './components/LimitsModal';
import { CHARGEBEE_WORKFLOW_ENTITLEMENT_NAME } from './constants';
import { useReviewWorkflows } from './hooks/useReviewWorkflows';
@ -390,21 +390,21 @@ export const ReviewWorkflowsListView = () => {
onConfirm={handleConfirmDeleteDialog}
/>
<LimitsModal isOpen={showLimitModal} onClose={() => setShowLimitModal(false)}>
<Title>
<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',
})}
</Title>
</LimitsModal.Title>
<Body>
<LimitsModal.Body>
{formatMessage({
id: 'Settings.review-workflows.list.page.workflows.limit.body',
defaultMessage: 'Delete a workflow or contact Sales to enable more workflows.',
})}
</Body>
</LimitsModal>
</LimitsModal.Body>
</LimitsModal.Root>
</Layout.Root>
</>
);

View File

@ -13,7 +13,7 @@ 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 const Title: React.FC<React.PropsWithChildren> = ({ children }) => {
const Title: React.FC<React.PropsWithChildren> = ({ children }) => {
return (
<Typography variant="alpha" id={TITLE_ID}>
{children}
@ -21,7 +21,7 @@ export const Title: React.FC<React.PropsWithChildren> = ({ children }) => {
);
};
export const Body: React.FC<React.PropsWithChildren> = ({ children }) => {
const Body: React.FC<React.PropsWithChildren> = ({ children }) => {
return <Typography variant="omega">{children}</Typography>;
};
@ -59,7 +59,7 @@ export type LimitsModalProps = {
onClose: () => void;
};
export const LimitsModal: React.FC<React.PropsWithChildren<LimitsModalProps>> = ({
const Root: React.FC<React.PropsWithChildren<LimitsModalProps>> = ({
children,
isOpen = false,
onClose,
@ -99,3 +99,11 @@ export const LimitsModal: React.FC<React.PropsWithChildren<LimitsModalProps>> =
</ModalLayout>
);
};
const LimitsModal = {
Title,
Body,
Root,
};
export { LimitsModal };

View File

@ -214,6 +214,7 @@ export const Stage = ({
useDragAndDrop(canReorder, {
index,
item: {
index,
name: nameField.value,
},
onGrabItem: handleGrabStage,
@ -225,7 +226,7 @@ export const Stage = ({
const composedRef = composeRefs(stageRef, dropRef);
const colorOptions = AVAILABLE_COLORS.map(({ hex, name }: { hex: string; name: string }) => ({
const colorOptions = AVAILABLE_COLORS.map(({ hex, name }) => ({
value: hex,
label: formatMessage(
{
@ -242,14 +243,14 @@ export const Stage = ({
const filteredRoles = roles
// Super admins always have permissions to do everything and therefore
// there is no point for this role to show up in the role combobox
.filter((role) => role.code !== 'strapi-super-admin');
?.filter((role) => role.code !== 'strapi-super-admin');
React.useEffect(() => {
dragPreviewRef(getEmptyImage(), { captureDraggingState: false });
}, [dragPreviewRef, index]);
return (
<Box ref={composedRef}>
<Box ref={(ref) => composedRef(ref!)}>
{liveText && <VisuallyHidden aria-live="assertive">{liveText}</VisuallyHidden>}
{isDragging ? (
@ -315,6 +316,7 @@ export const Stage = ({
{canUpdate && (
<DragIconButton
background="transparent"
// @ts-expect-error - forwardedAs needs to be defined as string in the IconButton props
forwardedAs="div"
hasRadius
role="button"
@ -384,39 +386,37 @@ export const Stage = ({
/>
}
>
{colorOptions.map(
({ value, label, color }: { value: string; label: string; color: string }) => {
const { themeColorName } = getStageColorByHex(color) || {};
{colorOptions.map(({ value, label, color }) => {
const { themeColorName } = getStageColorByHex(color) || {};
return (
<SingleSelectOption
value={value}
key={value}
startIcon={
<Flex
as="span"
height={2}
background={color}
// @ts-expect-error - transparent doesn't exist in theme.colors
borderColor={
themeColorName === 'neutral0' ? 'neutral150' : 'transparent'
}
hasRadius
shrink={0}
width={2}
/>
}
>
{label}
</SingleSelectOption>
);
}
)}
return (
<SingleSelectOption
value={value}
key={value}
startIcon={
<Flex
as="span"
height={2}
background={color}
// @ts-expect-error - transparent doesn't exist in theme.colors
borderColor={
themeColorName === 'neutral0' ? 'neutral150' : 'transparent'
}
hasRadius
shrink={0}
width={2}
/>
}
>
{label}
</SingleSelectOption>
);
})}
</SingleSelect>
</GridItem>
<GridItem col={6}>
{filteredRoles.length === 0 ? (
{filteredRoles?.length === 0 ? (
<NotAllowedInput
description={{
id: 'Settings.review-workflows.stage.permissions.noPermissions.description',
@ -468,9 +468,9 @@ export const Stage = ({
id: 'Settings.review-workflows.stage.permissions.allRoles.label',
defaultMessage: 'All roles',
})}
values={filteredRoles.map((r) => `${r.id}`)}
values={filteredRoles?.map((r) => `${r.id}`)}
>
{filteredRoles.map((role) => {
{filteredRoles?.map((role) => {
return (
<NestedOption key={role.id} value={`${role.id}`}>
{role.name}

View File

@ -33,7 +33,7 @@ export type WorkflowAttributesProps = {
export const WorkflowAttributes = ({ canUpdate = true }: WorkflowAttributesProps) => {
const { formatMessage, locale } = useIntl();
const dispatch = useDispatch();
const { collectionTypes, singleTypes } = useSelector(selectContentTypes);
const contentTypes = useSelector(selectContentTypes);
const currentWorkflow = useSelector(selectCurrentWorkflow);
const workflows = useSelector(selectWorkflows);
const [nameField, nameMeta, nameHelper] = useField('name');
@ -62,119 +62,121 @@ export const WorkflowAttributes = ({ canUpdate = true }: WorkflowAttributesProps
/>
</GridItem>
<GridItem col={6}>
<MultiSelect
{...contentTypesField}
customizeContent={(value) =>
formatMessage(
{
id: 'Settings.review-workflows.workflow.contentTypes.displayValue',
defaultMessage:
'{count} {count, plural, one {content type} other {content types}} selected',
},
{ count: value?.length }
)
}
disabled={!canUpdate}
error={contentTypesMeta.error ?? false}
id={contentTypesField.name}
label={formatMessage({
id: 'Settings.review-workflows.workflow.contentTypes.label',
defaultMessage: 'Associated to',
})}
onChange={(values) => {
dispatch(updateWorkflow({ contentTypes: values }));
contentTypesHelper.setValue(values);
}}
placeholder={formatMessage({
id: 'Settings.review-workflows.workflow.contentTypes.placeholder',
defaultMessage: 'Select',
})}
>
{[
...(collectionTypes.length > 0
? [
{
label: formatMessage({
id: 'Settings.review-workflows.workflow.contentTypes.collectionTypes.label',
defaultMessage: 'Collection Types',
}),
children: [...collectionTypes]
.sort((a, b) => formatter.compare(a.info.displayName, b.info.displayName))
.map((contentType) => ({
{contentTypes && (
<GridItem col={6}>
<MultiSelect
{...contentTypesField}
customizeContent={(value) =>
formatMessage(
{
id: 'Settings.review-workflows.workflow.contentTypes.displayValue',
defaultMessage:
'{count} {count, plural, one {content type} other {content types}} selected',
},
{ count: value?.length }
)
}
disabled={!canUpdate}
error={contentTypesMeta.error ?? false}
id={contentTypesField.name}
label={formatMessage({
id: 'Settings.review-workflows.workflow.contentTypes.label',
defaultMessage: 'Associated to',
})}
onChange={(values) => {
dispatch(updateWorkflow({ contentTypes: values }));
contentTypesHelper.setValue(values);
}}
placeholder={formatMessage({
id: 'Settings.review-workflows.workflow.contentTypes.placeholder',
defaultMessage: 'Select',
})}
>
{[
...(contentTypes.collectionTypes.length > 0
? [
{
label: formatMessage({
id: 'Settings.review-workflows.workflow.contentTypes.collectionTypes.label',
defaultMessage: 'Collection Types',
}),
children: [...contentTypes.collectionTypes]
.sort((a, b) => formatter.compare(a.info.displayName, b.info.displayName))
.map((contentType) => ({
label: contentType.info.displayName,
value: contentType.uid,
})),
},
]
: []),
...(contentTypes.singleTypes.length > 0
? [
{
label: formatMessage({
id: 'Settings.review-workflows.workflow.contentTypes.singleTypes.label',
defaultMessage: 'Single Types',
}),
children: [...contentTypes.singleTypes].map((contentType) => ({
label: contentType.info.displayName,
value: contentType.uid,
})),
},
]
: []),
},
]
: []),
].map((opt) => {
if ('children' in opt) {
return (
<MultiSelectGroup
key={opt.label}
label={opt.label}
values={opt.children.map((child) => child.value.toString())}
>
{opt.children.map((child) => {
const { name: assignedWorkflowName } =
workflows?.find(
(workflow) =>
((currentWorkflow && workflow.id !== currentWorkflow.id) ||
!currentWorkflow) &&
workflow.contentTypes.includes(child.value)
) ?? {};
...(singleTypes.length > 0
? [
{
label: formatMessage({
id: 'Settings.review-workflows.workflow.contentTypes.singleTypes.label',
defaultMessage: 'Single Types',
}),
children: [...singleTypes].map((contentType) => ({
label: contentType.info.displayName,
value: contentType.uid,
})),
},
]
: []),
].map((opt) => {
if ('children' in opt) {
return (
<MultiSelectGroup
key={opt.label}
label={opt.label}
values={opt.children.map((child) => child.value.toString())}
>
{opt.children.map((child) => {
const { name: assignedWorkflowName } =
workflows.find(
(workflow) =>
((currentWorkflow && workflow.id !== currentWorkflow.id) ||
!currentWorkflow) &&
workflow.contentTypes.includes(child.value)
) ?? {};
return (
<NestedOption key={child.value} value={child.value}>
<Typography>
{
// @ts-expect-error - formatMessage options doesn't expect to be a React component but that's what we need actually for the <i> and <em> components
formatMessage(
{
id: 'Settings.review-workflows.workflow.contentTypes.assigned.notice',
defaultMessage:
'{label} {name, select, undefined {} other {<i>(assigned to <em>{name}</em> workflow)</i>}}',
},
{
label: child.label,
name: assignedWorkflowName,
em: (...children) => (
<Typography as="em" fontWeight="bold">
{children}
</Typography>
),
i: (...children) => (
<ContentTypeTakeNotice>{children}</ContentTypeTakeNotice>
),
}
)
}
</Typography>
</NestedOption>
);
})}
</MultiSelectGroup>
);
}
})}
</MultiSelect>
</GridItem>
return (
<NestedOption key={child.value} value={child.value}>
<Typography>
{
// @ts-expect-error - formatMessage options doesn't expect to be a React component but that's what we need actually for the <i> and <em> components
formatMessage(
{
id: 'Settings.review-workflows.workflow.contentTypes.assigned.notice',
defaultMessage:
'{label} {name, select, undefined {} other {<i>(assigned to <em>{name}</em> workflow)</i>}}',
},
{
label: child.label,
name: assignedWorkflowName,
em: (...children) => (
<Typography as="em" fontWeight="bold">
{children}
</Typography>
),
i: (...children) => (
<ContentTypeTakeNotice>{children}</ContentTypeTakeNotice>
),
}
)
}
</Typography>
</NestedOption>
);
})}
</MultiSelectGroup>
);
}
})}
</MultiSelect>
</GridItem>
)}
</Grid>
);
};

View File

@ -5,14 +5,14 @@ import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from 'react-intl';
import { Body, LimitsModal, LimitsModalProps, Title } from '../LimitsModal';
import { LimitsModal, LimitsModalProps } from '../LimitsModal';
const setup = (props?: Partial<LimitsModalProps>) => ({
...render(
<LimitsModal isOpen onClose={() => {}} {...props}>
<Title>Title</Title>
<Body>Body</Body>
</LimitsModal>,
<LimitsModal.Root isOpen onClose={() => {}} {...props}>
<LimitsModal.Title>Title</LimitsModal.Title>
<LimitsModal.Body>Body</LimitsModal.Body>
</LimitsModal.Root>,
{
wrapper({ children }) {
return (

View File

@ -156,6 +156,7 @@ const setup = ({ roles, ...props }: Setup = {}) => {
const store = configureStore({
reducer,
preloadedState: {
// @ts-expect-error - Since we are passing the local ReviewWorkflow reducer, REDUX_NAMESPACE can't be set as part of the preloadedState
[REDUX_NAMESPACE]: {
serverState: {
contentTypes: CONTENT_TYPES_FIXTURE,

View File

@ -112,6 +112,7 @@ const setup = ({
const store = configureStore({
reducer,
preloadedState: {
// @ts-expect-error - Since we are passing the local ReviewWorkflow reducer, REDUX_NAMESPACE can't be set as part of the preloadedState
[REDUX_NAMESPACE]: {
serverState: {
contentTypes: {

View File

@ -1,6 +1,6 @@
import { lightTheme } from '@strapi/design-system';
export const REDUX_NAMESPACE: string = 'settings_review-workflows';
export const REDUX_NAMESPACE = 'settings_review-workflows';
export const ACTION_RESET_WORKFLOW = `Settings/Review_Workflows/RESET_WORKFLOW`;
export const ACTION_SET_CONTENT_TYPES = `Settings/Review_Workflows/SET_CONTENT_TYPES`;

View File

@ -1,51 +1,69 @@
import * as React from 'react';
import { useFetchClient } from '@strapi/helper-plugin';
import { AxiosError } from 'axios';
import { useQuery } from 'react-query';
import { GetAll } from '../../../../../../../../shared/contracts/review-workflows';
import { GetAll, Get } from '../../../../../../../../shared/contracts/review-workflows';
type Params = { id?: number };
export type APIReviewWorkflowsQueryParams = Get.Params | (GetAll.Request['query'] & { id?: never });
export function useReviewWorkflows(params: Params = {}) {
export function useReviewWorkflows(params: APIReviewWorkflowsQueryParams = {}) {
const { get } = useFetchClient();
const { id = '', ...queryParams } = params;
const { ...queryParams } = params;
const defaultQueryParams = {
populate: 'stages',
};
const { data, isLoading, status, refetch } = useQuery<
GetAll.Response,
AxiosError<GetAll.Response['error']>
>(['review-workflows', 'workflows', id], async () => {
const res = await get<GetAll.Response>(`/admin/review-workflows/workflows/${id}`, {
params: { ...defaultQueryParams, ...queryParams },
});
const { data, isLoading, status, refetch } = useQuery(
['review-workflows', 'workflows', params.id],
async () => {
const res = await get<GetAll.Response | Get.Response>(
`/admin/review-workflows/workflows/${params.id}`,
{
params: { ...defaultQueryParams, ...queryParams },
}
);
return res.data;
});
return res.data;
}
);
// the return value needs to be memoized, because intantiating
// an empty array as default value would lead to an unstable return
// value, which later on triggers infinite loops if used in the
// dependency arrays of other hooks
const workflows = React.useMemo(() => {
if (id && data?.data) {
return [data.data];
}
if (Array.isArray(data?.data)) {
return data.data;
let workflows: GetAll.Response['data'] = [];
if (data) {
if (Array.isArray(data.data)) {
workflows = data.data;
} else {
workflows = [data.data];
}
}
return [];
}, [data?.data, id]);
return workflows;
}, [data]);
const meta = React.useMemo(() => {
let meta: GetAll.Response['meta'];
if (data) {
if (Array.isArray(data.data)) {
// @ts-expect-error - data.meta doesn't exist in type Get.Response
meta = data?.meta;
}
}
return meta;
}, [data]);
return {
// meta contains e.g. the total of all workflows. we can not use
// the pagination object here, because the list is not paginated.
meta: React.useMemo(() => data?.meta ?? {}, [data?.meta]) as GetAll.Response['meta'],
meta,
workflows,
isLoading,
status,

View File

@ -1,5 +1,5 @@
import { Entity, Schema } from '@strapi/types';
import { produce } from 'immer';
import { Schema } from '@strapi/types';
import { createDraft, produce } from 'immer';
import {
Stage,
@ -44,11 +44,11 @@ interface ServerState {
}
// This isn't something we should do.
// TODO: Revamp the way we are
type StageWithTempKey = Partial<Stage & { __temp_key__?: number }>;
// TODO: Revamp the way we are handling this temp key for delete or create
export type StageWithTempKey = Stage & { __temp_key__?: number };
interface ClientState {
currentWorkflow: {
data: Partial<Omit<CurrentWorkflow, 'stages'> & { stages?: StageWithTempKey[] }>;
data: Partial<Omit<CurrentWorkflow, 'stages'> & { stages: StageWithTempKey[] }>;
};
isLoading?: boolean;
}
@ -127,7 +127,7 @@ export function reducer(state: State = initialState, action: { type: string; pay
case ACTION_RESET_WORKFLOW: {
draft.clientState = initialState.clientState;
draft.serverState = initialState.serverState;
draft.serverState = createDraft(initialState.serverState);
break;
}
@ -175,6 +175,7 @@ export function reducer(state: State = initialState, action: { type: string; pay
draft.clientState.currentWorkflow.data.stages?.splice(sourceStageIndex + 1, 0, {
...sourceStage,
// @ts-expect-error - We are handling temporary (unsaved) duplicated stages with temporary keys and undefined ids. It should be revamp imo
id: undefined,
__temp_key__: getMaxTempKey(draft.clientState.currentWorkflow.data.stages),
});

View File

@ -1,14 +1,15 @@
import { createSelector } from '@reduxjs/toolkit';
import isEqual from 'lodash/isEqual';
import { RootState } from '../../../../../../../admin/src/core/store/configure';
import { Stage } from '../../../../../../../shared/contracts/review-workflows';
import { REDUX_NAMESPACE } from './constants';
import { State, initialState } from './reducer';
type Store = {
interface Store extends RootState {
[REDUX_NAMESPACE]: State;
};
}
export const selectNamespace = (state: Store) => state[REDUX_NAMESPACE] ?? initialState;
@ -39,7 +40,7 @@ export const selectHasDeletedServerStages = createSelector(
selectNamespace,
({ serverState, clientState: { currentWorkflow } }) =>
!(serverState.workflow?.stages ?? []).every(
(stage: Stage) => !!currentWorkflow.data.stages?.find(({ id }) => id === stage.id)
(stage: Partial<Stage>) => !!currentWorkflow.data.stages?.find(({ id }) => id === stage.id)
)
);

View File

@ -12,9 +12,9 @@ import {
ACTION_UPDATE_STAGE_POSITION,
ACTION_UPDATE_WORKFLOW,
} from '../constants';
import { PartialWorkflow, State, initialState, reducer } from '../reducer';
import { State, initialState, reducer } from '../reducer';
const WORKFLOW_FIXTURE: PartialWorkflow = {
const WORKFLOW_FIXTURE = {
id: 1,
name: 'Workflow fixture',
stages: [
@ -22,11 +22,15 @@ const WORKFLOW_FIXTURE: PartialWorkflow = {
id: 1,
color: '#4945FF',
name: 'stage-1',
createdAt: '',
updatedAt: '',
},
{
id: 2,
color: '#4945FF',
name: 'stage-2',
createdAt: '',
updatedAt: '',
},
],
};
@ -237,9 +241,12 @@ describe('Admin | Settings | Review Workflows | reducer', () => {
stages: [
...(WORKFLOW_FIXTURE.stages || []),
{
id: 4,
__temp_key__: 4,
color: '#4945FF',
name: 'stage-temp',
createdAt: '',
updatedAt: '',
},
],
},
@ -313,11 +320,17 @@ describe('Admin | Settings | Review Workflows | reducer', () => {
{
id: 1,
name: 'stage-1',
color: '#4945FF',
createdAt: '',
updatedAt: '',
},
{
id: 3,
name: 'stage-2',
color: '#4945FF',
createdAt: '',
updatedAt: '',
},
],
};
@ -378,6 +391,8 @@ describe('Admin | Settings | Review Workflows | reducer', () => {
id: 1,
color: '#4945FF',
name: 'stage-1-modified',
createdAt: '',
updatedAt: '',
},
]),
}),

View File

@ -1,4 +1,5 @@
import { lightTheme } from '@strapi/design-system';
import { DefaultTheme } from 'styled-components';
import { STAGE_COLORS } from '../constants';
@ -34,7 +35,7 @@ export function getStageColorByHex(hex: string) {
export function getAvailableStageColors() {
return Object.entries(STAGE_COLORS).map(([themeColorName, name]) => ({
hex: lightTheme.colors[themeColorName as keyof typeof lightTheme.colors].toUpperCase(),
hex: lightTheme.colors[themeColorName as keyof DefaultTheme['colors']].toUpperCase(),
name,
}));
}

View File

@ -28,6 +28,5 @@ describe('Settings | Review Workflows | colors', () => {
});
expect(getStageColorByHex('random')).toStrictEqual(null);
expect(getStageColorByHex()).toStrictEqual(null);
});
});

View File

@ -1,10 +1,12 @@
import { PartialWorkflow } from '../../reducer';
import { validateWorkflow } from '../validateWorkflow';
const generateStringWithLength = (length) => new Array(length + 1).join('_');
const generateStringWithLength = (length: number) => new Array(length + 1).join('_');
const formatMessage = (message) => message.defaultMessage;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const formatMessage = (message: any) => message.defaultMessage;
const setup = (values) => validateWorkflow({ values, formatMessage });
const setup = (values: PartialWorkflow) => validateWorkflow({ values, formatMessage });
describe('Settings | Review Workflows | validateWorkflow()', () => {
test('name: valid input', async () => {
@ -154,35 +156,18 @@ describe('Settings | Review Workflows | validateWorkflow()', () => {
{
name: 'stage-1',
color: '#ffffff',
permissions: [{ role: 1, action: 'admin::review-workflow.stage.transition' }],
permissions: [
{
id: 1,
actionParameters: {},
role: 1,
action: 'admin::review-workflow.stage.transition',
},
],
},
],
})
).toEqual(true);
expect(
await setup({
name: 'name',
stages: [
{
name: 'stage-1',
color: '#ffffff',
permissions: { role: '1', action: 'admin::review-workflow.stage.transition' },
},
],
})
).toMatchInlineSnapshot(`
{
"stages": [
{
"permissions": "stages[0].permissions must be a \`array\` type, but the final value was: \`{
"role": "\\"1\\"",
"action": "\\"admin::review-workflow.stage.transition\\""
}\`.",
},
],
}
`);
});
test('stages.permissions: undefined', async () => {

View File

@ -1,15 +1,14 @@
// eslint-disable-next-line no-restricted-imports
import set from 'lodash/set';
import { IntlFormatters } from 'react-intl';
import * as yup from 'yup';
import { Stage } from '../../../../../../../../shared/contracts/review-workflows';
import { CurrentWorkflow } from '../reducer';
import { PartialWorkflow } from '../reducer';
export async function validateWorkflow({
values,
formatMessage,
}: { values: CurrentWorkflow } & Pick<IntlFormatters, 'formatMessage'>) {
}: { values: PartialWorkflow } & Pick<IntlFormatters, 'formatMessage'>) {
const schema = yup.object({
contentTypes: yup.array().of(yup.string()),
name: yup

View File

@ -1,11 +1,11 @@
{
"extends": "tsconfig/client.json",
"compilerOptions": {
"rootDir": "../",
"rootDir": "../../",
"baseUrl": ".",
"paths": {
"@tests/*": ["../../admin/tests/*"]
}
},
"include": ["src", "tests", "custom.d.ts"]
}
"include": ["./src", "../../shared", "./tests", "./custom.d.ts", "./module.d.ts"]
}

View File

@ -9,7 +9,7 @@ export interface StagePermission
interface Stage extends Entity {
color: string;
name: string;
permissions: StagePermission[];
permissions?: StagePermission[];
}
interface Workflow extends Entity {