diff --git a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/WorkflowAttributes/WorkflowAttributes.js b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/WorkflowAttributes/WorkflowAttributes.js index 7182743aec..53e4de2100 100644 --- a/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/WorkflowAttributes/WorkflowAttributes.js +++ b/packages/core/admin/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/WorkflowAttributes/WorkflowAttributes.js @@ -1,15 +1,37 @@ import * as React from 'react'; -import { Grid, GridItem, MultiSelectNested, TextInput } from '@strapi/design-system'; +import { + Grid, + GridItem, + MultiSelect, + MultiSelectGroup, + MultiSelectOption, + TextInput, + Typography, +} from '@strapi/design-system'; import { useCollator } from '@strapi/helper-plugin'; import { useField } from 'formik'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; import { updateWorkflow } from '../../actions'; -export function WorkflowAttributes({ canUpdate, contentTypes: { collectionTypes, singleTypes } }) { +const NestedOption = styled(MultiSelectOption)` + padding-left: ${({ theme }) => theme.spaces[7]}; +`; + +const ContentTypeTakeNotice = styled(Typography)` + font-style: italic; +`; + +export function WorkflowAttributes({ + canUpdate, + contentTypes: { collectionTypes, singleTypes }, + currentWorkflow, + workflows, +}) { const { formatMessage, locale } = useIntl(); const dispatch = useDispatch(); const [nameField, nameMeta, nameHelper] = useField('name'); @@ -39,7 +61,7 @@ export function WorkflowAttributes({ canUpdate, contentTypes: { collectionTypes, - formatMessage( @@ -62,7 +84,12 @@ export function WorkflowAttributes({ canUpdate, contentTypes: { collectionTypes, dispatch(updateWorkflow({ contentTypes: values })); contentTypesHelper.setValue(values); }} - options={[ + placeholder={formatMessage({ + id: 'Settings.review-workflows.workflow.contentTypes.placeholder', + defaultMessage: 'Select', + })} + > + {[ ...(collectionTypes.length > 0 ? [ { @@ -94,12 +121,53 @@ export function WorkflowAttributes({ canUpdate, contentTypes: { collectionTypes, }, ] : []), - ]} - placeholder={formatMessage({ - id: 'Settings.review-workflows.workflow.contentTypes.placeholder', - defaultMessage: 'Select', + ].map((opt) => { + if ('children' in opt) { + return ( + 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 ( + + {formatMessage( + { + id: 'Settings.review-workflows.workflow.contentTypes.assigned.notice', + defaultMessage: + '{label} {name, select, undefined {} other {(assigned to {name} workflow)}}', + }, + { + label: child.label, + name: assignedWorkflowName, + i: (...children) => ( + {children} + ), + } + )} + + ); + })} + + ); + } + + return ( + + {opt.label} + + ); })} - /> + ); @@ -114,6 +182,7 @@ const ContentTypeType = PropTypes.shape({ WorkflowAttributes.defaultProps = { canUpdate: true, + currentWorkflow: undefined, }; WorkflowAttributes.propTypes = { @@ -122,4 +191,6 @@ WorkflowAttributes.propTypes = { collectionTypes: PropTypes.arrayOf(ContentTypeType).isRequired, singleTypes: PropTypes.arrayOf(ContentTypeType).isRequired, }).isRequired, + currentWorkflow: PropTypes.object, + workflows: PropTypes.array.isRequired, }; 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 0cc578fde0..c5cc3ce05c 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 @@ -1,7 +1,8 @@ import * as React from 'react'; -import { Button, Flex, Loader } from '@strapi/design-system'; +import { Button, Flex, Loader, Typography } from '@strapi/design-system'; import { + ConfirmDialog, useAPIErrorHandler, useFetchClient, useNotification, @@ -42,6 +43,7 @@ export function ReviewWorkflowsCreateView() { const permissions = useSelector(selectAdminPermissions); const toggleNotification = useNotification(); const { collectionTypes, singleTypes, isLoading: isLoadingModels } = useContentTypes(); + const { isLoading: isWorkflowLoading, meta, workflows } = useReviewWorkflows(); const { clientState: { currentWorkflow: { data: currentWorkflow, isDirty: currentWorkflowIsDirty }, @@ -52,8 +54,11 @@ export function ReviewWorkflowsCreateView() { } = useRBAC(permissions.settings['review-workflows']); const [showLimitModal, setShowLimitModal] = React.useState(false); const { isLoading: isLicenseLoading, getFeature } = useLicenseLimits(); - const { meta, isLoading: isWorkflowLoading } = useReviewWorkflows(); const [initialErrors, setInitialErrors] = React.useState(null); + const [savePrompts, setSavePrompts] = React.useState({}); + + const limits = getFeature('review-workflows'); + const contentTypesFromOtherWorkflows = workflows.flatMap((workflow) => workflow.contentTypes); const { mutateAsync, isLoading } = useMutation( async ({ workflow }) => { @@ -79,6 +84,8 @@ export function ReviewWorkflowsCreateView() { ); const submitForm = async () => { + setSavePrompts({}); + try { const workflow = await mutateAsync({ workflow: currentWorkflow }); @@ -110,13 +117,23 @@ export function ReviewWorkflowsCreateView() { } }; - const limits = getFeature('review-workflows'); + const handleConfirmDeleteDialog = async () => { + await submitForm(); + }; + + const handleConfirmClose = () => { + setSavePrompts({}); + }; const formik = useFormik({ enableReinitialize: true, initialErrors, initialValues: currentWorkflow, async onSubmit() { + const isContentTypeReassignment = currentWorkflow.contentTypes.some((contentType) => + contentTypesFromOtherWorkflows.includes(contentType) + ); + /** * If the current license has a limit, check if the total count of workflows * exceeds that limit and display the limits modal instead of sending the @@ -140,6 +157,8 @@ export function ReviewWorkflowsCreateView() { parseInt(limits[CHARGEBEE_STAGES_PER_WORKFLOW_ENTITLEMENT_NAME], 10) ) { setShowLimitModal('stage'); + } else if (isContentTypeReassignment) { + setSavePrompts((prev) => ({ ...prev, hasReassignedContentTypes: true })); } else { submitForm(); } @@ -243,7 +262,10 @@ export function ReviewWorkflowsCreateView() { ) : ( - + )} @@ -252,6 +274,41 @@ export function ReviewWorkflowsCreateView() { + 0} + onToggleDialog={handleConfirmClose} + onConfirm={handleConfirmDeleteDialog} + > + + + {savePrompts.hasReassignedContentTypes && ( + + {formatMessage( + { + id: 'Settings.review-workflows.page.delete.confirm.contentType.body', + defaultMessage: + '{count} {count, plural, one {content-type} other {content-types}} {count, plural, one {is} other {are}} already mapped to {count, plural, one {another workflow} other {other workflows}}. If you save changes, {count, plural, one {this} other {these}} {count, plural, one {content-type} other {{count} content-types}} will no more be mapped to the {count, plural, one {another workflow} other {other workflows}} and all corresponding information will be removed.', + }, + { + count: contentTypesFromOtherWorkflows.filter((contentType) => + currentWorkflow.contentTypes.includes(contentType) + ).length, + } + )} + + )} + + + {formatMessage({ + id: 'Settings.review-workflows.page.delete.confirm.confirm', + defaultMessage: 'Are you sure you want to save?', + })} + + + + + setShowLimitModal(false)} 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 97e95635a4..35563a0846 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 @@ -1,6 +1,6 @@ import * as React from 'react'; -import { Button, Flex, Loader } from '@strapi/design-system'; +import { Button, Flex, Loader, Typography } from '@strapi/design-system'; import { ConfirmDialog, useAPIErrorHandler, @@ -45,10 +45,10 @@ export function ReviewWorkflowsEditView() { const { isLoading: isWorkflowLoading, meta, - workflows: [workflow], + workflows, status: workflowStatus, refetch, - } = useReviewWorkflows({ id: workflowId }); + } = useReviewWorkflows(); const { collectionTypes, singleTypes, isLoading: isLoadingModels } = useContentTypes(); const { status, @@ -56,18 +56,23 @@ export function ReviewWorkflowsEditView() { currentWorkflow: { data: currentWorkflow, isDirty: currentWorkflowIsDirty, - hasDeletedServerStages: currentWorkflowHasDeletedServerStages, + hasDeletedServerStages, }, }, } = useSelector((state) => state?.[REDUX_NAMESPACE] ?? initialState); const { allowedActions: { canDelete, canUpdate }, } = useRBAC(permissions.settings['review-workflows']); - const [isConfirmDeleteDialogOpen, setIsConfirmDeleteDialogOpen] = React.useState(false); + const [savePrompts, setSavePrompts] = React.useState({}); const { getFeature, isLoading: isLicenseLoading } = useLicenseLimits(); const [showLimitModal, setShowLimitModal] = React.useState(false); const [initialErrors, setInitialErrors] = React.useState(null); + const workflow = workflows.find((workflow) => workflow.id === parseInt(workflowId, 10)); + const contentTypesFromOtherWorkflows = workflows + .filter((workflow) => workflow.id !== parseInt(workflowId, 10)) + .flatMap((workflow) => workflow.contentTypes); + const { mutateAsync, isLoading } = useMutation( async ({ workflow }) => { const { @@ -125,15 +130,15 @@ export function ReviewWorkflowsEditView() { await updateWorkflow(currentWorkflow); await refetch(); - setIsConfirmDeleteDialogOpen(false); + setSavePrompts({}); }; const handleConfirmDeleteDialog = async () => { await submitForm(); }; - const toggleConfirmDeleteDialog = () => { - setIsConfirmDeleteDialogOpen((prev) => !prev); + const handleConfirmClose = () => { + setSavePrompts({}); }; const formik = useFormik({ @@ -141,9 +146,11 @@ export function ReviewWorkflowsEditView() { initialErrors, initialValues: currentWorkflow, async onSubmit() { - if (currentWorkflowHasDeletedServerStages) { - setIsConfirmDeleteDialogOpen(true); - } else if ( + const isContentTypeReassignment = currentWorkflow.contentTypes.some((contentType) => + contentTypesFromOtherWorkflows.includes(contentType) + ); + + if ( limits?.[CHARGEBEE_WORKFLOW_ENTITLEMENT_NAME] && meta?.workflowCount > parseInt(limits[CHARGEBEE_WORKFLOW_ENTITLEMENT_NAME], 10) ) { @@ -165,6 +172,14 @@ export function ReviewWorkflowsEditView() { parseInt(limits[CHARGEBEE_STAGES_PER_WORKFLOW_ENTITLEMENT_NAME], 10) ) { setShowLimitModal('stage'); + } else if (hasDeletedServerStages || isContentTypeReassignment) { + if (hasDeletedServerStages) { + setSavePrompts((prev) => ({ ...prev, hasDeletedServerStages: true })); + } + + if (isContentTypeReassignment) { + setSavePrompts((prev) => ({ ...prev, hasReassignedContentTypes: true })); + } } else { submitForm(); } @@ -243,7 +258,7 @@ export function ReviewWorkflowsEditView() { disabled={!currentWorkflowIsDirty || !canUpdate} // if the confirm dialog is open the loading state is on // the confirm button already - loading={!isConfirmDeleteDialogOpen && isLoading} + loading={!Object.keys(savePrompts).length > 0 && isLoading} > {formatMessage({ id: 'global.save', @@ -279,6 +294,8 @@ export function ReviewWorkflowsEditView() { - 0} + onToggleDialog={handleConfirmClose} onConfirm={handleConfirmDeleteDialog} - /> + > + + + {savePrompts.hasDeletedServerStages && ( + + {formatMessage({ + id: 'Settings.review-workflows.page.delete.confirm.stages.body', + defaultMessage: + 'All entries assigned to deleted stages will be moved to the previous stage.', + })} + + )} + + {savePrompts.hasReassignedContentTypes && ( + + {formatMessage( + { + id: 'Settings.review-workflows.page.delete.confirm.contentType.body', + defaultMessage: + '{count} {count, plural, one {content-type} other {content-types}} {count, plural, one {is} other {are}} already mapped to {count, plural, one {another workflow} other {other workflows}}. If you save changes, {count, plural, one {this} other {these}} {count, plural, one {content-type} other {{count} content-types}} will no more be mapped to the {count, plural, one {another workflow} other {other workflows}} and all corresponding information will be removed.', + }, + { + count: contentTypesFromOtherWorkflows.filter((contentType) => + currentWorkflow.contentTypes.includes(contentType) + ).length, + } + )} + + )} + + + {formatMessage({ + id: 'Settings.review-workflows.page.delete.confirm.confirm', + defaultMessage: 'Are you sure you want to save?', + })} + + + + stage.id === stageId); } diff --git a/packages/core/helper-plugin/src/components/ConfirmDialog/index.js b/packages/core/helper-plugin/src/components/ConfirmDialog/index.js index 44ecd4022b..544454c055 100644 --- a/packages/core/helper-plugin/src/components/ConfirmDialog/index.js +++ b/packages/core/helper-plugin/src/components/ConfirmDialog/index.js @@ -1,18 +1,25 @@ import React from 'react'; -import { Button, Dialog, DialogBody, DialogFooter, Flex, Typography } from '@strapi/design-system'; +import { + Button, + Box, + Dialog, + DialogBody, + DialogFooter, + Flex, + Typography, +} from '@strapi/design-system'; import { ExclamationMarkCircle, Trash } from '@strapi/icons'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; -const ConfirmDialog = ({ - bodyText, +export const Root = ({ + children, iconRightButton, - iconBody, isConfirmButtonLoading, leftButtonText, - onToggleDialog, onConfirm, + onToggleDialog, rightButtonText, title, variantRightButton, @@ -31,46 +38,165 @@ const ConfirmDialog = ({ describedBy="confirm-description" {...props} > - - - - - {formatMessage({ - id: bodyText.id, - defaultMessage: bodyText.defaultMessage, - })} - - - - - - {formatMessage({ - id: leftButtonText.id, - defaultMessage: leftButtonText.defaultMessage, - })} - - } - endAction={ - - } + {children} + +