@strapi/admin 4.9.2 → 4.10.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/src/content-manager/components/DynamicTable/CellContent/PublicationState/PublicationState.js +26 -0
- package/admin/src/content-manager/components/DynamicTable/CellContent/PublicationState/index.js +1 -0
- package/admin/src/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +2 -0
- package/admin/src/content-manager/components/DynamicTable/index.js +25 -49
- package/admin/src/content-manager/components/DynamicZone/components/DynamicComponent.js +2 -0
- package/admin/src/content-manager/pages/EditView/Information/index.js +77 -53
- package/admin/src/content-manager/pages/EditView/InformationBox/InformationBoxCE.js +13 -0
- package/admin/src/content-manager/pages/EditView/InformationBox/index.js +3 -0
- package/admin/src/content-manager/pages/EditView/index.js +3 -4
- package/admin/src/content-manager/pages/ListView/index.js +6 -9
- package/admin/src/content-manager/sharedReducers/crudReducer/actions.js +6 -0
- package/admin/src/content-manager/sharedReducers/crudReducer/constants.js +1 -0
- package/admin/src/content-manager/sharedReducers/crudReducer/reducer.js +5 -0
- package/admin/src/index.js +1 -0
- package/admin/src/translations/en.json +6 -0
- package/build/{Admin-authenticatedApp.217db666.chunk.js → Admin-authenticatedApp.52c88751.chunk.js} +2 -2
- package/build/{Admin_settingsPage.1dbfc9ce.chunk.js → Admin_settingsPage.257b3477.chunk.js} +7 -7
- package/build/{admin-app.558af642.chunk.js → admin-app.dfaeea5d.chunk.js} +18 -18
- package/build/content-manager.def692c2.chunk.js +1130 -0
- package/build/content-type-builder-translation-en-json.510e88ca.chunk.js +1 -0
- package/build/content-type-builder.5e1f4afc.chunk.js +126 -0
- package/build/en-json.08303b37.chunk.js +1 -0
- package/build/index.html +1 -1
- package/build/{main.ef8db4a2.js → main.120be100.js} +145 -145
- package/build/review-workflows-settings.9092ed72.chunk.js +106 -0
- package/build/{runtime~main.3a92d953.js → runtime~main.112b3101.js} +1 -1
- package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/ReviewWorkflowsStageEE.js +15 -0
- package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +45 -0
- package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/index.js +3 -0
- package/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js +135 -0
- package/ee/admin/content-manager/pages/EditView/InformationBox/index.js +3 -0
- package/ee/admin/hooks/useSettingsMenu/utils/customAdminLinks.js +12 -12
- package/ee/admin/hooks/useSettingsMenu/utils/customGlobalLinks.js +21 -13
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js +199 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/actions/index.js +42 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/AddStage/AddStage.js +87 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/AddStage/index.js +1 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +90 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/index.js +1 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stages.js +92 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/index.js +1 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js +6 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows.js +35 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/index.js +3 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js +122 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/getWorkflowValidationSchema.js +25 -0
- package/ee/admin/pages/SettingsPage/utils/customRoutes.js +16 -2
- package/ee/admin/permissions/customPermissions.js +3 -0
- package/ee/server/bootstrap.js +13 -0
- package/ee/server/config/admin-actions.js +10 -0
- package/ee/server/constants/default-stages.json +14 -0
- package/ee/server/constants/default-workflow.json +1 -0
- package/ee/server/constants/workflows.js +8 -0
- package/ee/server/content-types/index.js +9 -0
- package/ee/server/content-types/workflow/index.js +31 -0
- package/ee/server/content-types/workflow-stage/index.js +36 -0
- package/ee/server/controllers/index.js +2 -0
- package/ee/server/controllers/workflows/index.js +36 -0
- package/ee/server/controllers/workflows/stages/index.js +102 -0
- package/ee/server/index.js +1 -0
- package/ee/server/middlewares/review-workflows.js +40 -0
- package/ee/server/register.js +8 -0
- package/ee/server/routes/index.js +104 -0
- package/ee/server/services/index.js +4 -0
- package/ee/server/services/review-workflows/entity-service-decorator.js +54 -0
- package/ee/server/services/review-workflows/review-workflows.js +111 -0
- package/ee/server/services/review-workflows/stages.js +249 -0
- package/ee/server/services/review-workflows/workflows.js +25 -0
- package/ee/server/utils/index.js +8 -0
- package/ee/server/utils/persisted-tables.js +114 -22
- package/ee/server/utils/review-workflows.js +34 -0
- package/ee/server/validation/review-workflows.js +24 -0
- package/package.json +9 -9
- package/build/content-manager.d1565bfc.chunk.js +0 -1132
- package/build/content-type-builder-translation-en-json.6c8e69ab.chunk.js +0 -1
- package/build/content-type-builder.9d780e7f.chunk.js +0 -126
- package/build/en-json.cf600231.chunk.js +0 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { FormikProvider, useFormik, Form } from 'formik';
|
|
3
|
+
import { useIntl } from 'react-intl';
|
|
4
|
+
import { useSelector, useDispatch } from 'react-redux';
|
|
5
|
+
import { useMutation } from 'react-query';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
CheckPagePermissions,
|
|
9
|
+
ConfirmDialog,
|
|
10
|
+
SettingsPageTitle,
|
|
11
|
+
useAPIErrorHandler,
|
|
12
|
+
useFetchClient,
|
|
13
|
+
useNotification,
|
|
14
|
+
useTracking,
|
|
15
|
+
} from '@strapi/helper-plugin';
|
|
16
|
+
import { Button, ContentLayout, HeaderLayout, Layout, Loader, Main } from '@strapi/design-system';
|
|
17
|
+
import { Check } from '@strapi/icons';
|
|
18
|
+
|
|
19
|
+
import { Stages } from './components/Stages';
|
|
20
|
+
import { reducer, initialState } from './reducer';
|
|
21
|
+
import { REDUX_NAMESPACE } from './constants';
|
|
22
|
+
import { useInjectReducer } from '../../../../../../admin/src/hooks/useInjectReducer';
|
|
23
|
+
import { useReviewWorkflows } from './hooks/useReviewWorkflows';
|
|
24
|
+
import { setWorkflows } from './actions';
|
|
25
|
+
import { getWorkflowValidationSchema } from './utils/getWorkflowValidationSchema';
|
|
26
|
+
import adminPermissions from '../../../../../../admin/src/permissions';
|
|
27
|
+
|
|
28
|
+
export function ReviewWorkflowsPage() {
|
|
29
|
+
const { trackUsage } = useTracking();
|
|
30
|
+
const { formatMessage } = useIntl();
|
|
31
|
+
const dispatch = useDispatch();
|
|
32
|
+
const { put } = useFetchClient();
|
|
33
|
+
const { formatAPIError } = useAPIErrorHandler();
|
|
34
|
+
const toggleNotification = useNotification();
|
|
35
|
+
const { workflows: workflowsData, refetchWorkflow } = useReviewWorkflows();
|
|
36
|
+
const {
|
|
37
|
+
status,
|
|
38
|
+
clientState: {
|
|
39
|
+
currentWorkflow: {
|
|
40
|
+
data: currentWorkflow,
|
|
41
|
+
isDirty: currentWorkflowIsDirty,
|
|
42
|
+
hasDeletedServerStages: currentWorkflowHasDeletedServerStages,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
} = useSelector((state) => state?.[REDUX_NAMESPACE] ?? initialState);
|
|
46
|
+
const [isConfirmDeleteDialogOpen, setIsConfirmDeleteDialogOpen] = useState(false);
|
|
47
|
+
|
|
48
|
+
const { mutateAsync, isLoading } = useMutation(
|
|
49
|
+
async ({ workflowId, stages }) => {
|
|
50
|
+
try {
|
|
51
|
+
const {
|
|
52
|
+
data: { data },
|
|
53
|
+
} = await put(`/admin/review-workflows/workflows/${workflowId}/stages`, {
|
|
54
|
+
data: stages,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return data;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
toggleNotification({
|
|
60
|
+
type: 'warning',
|
|
61
|
+
message: formatAPIError(error),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
onError(error) {
|
|
69
|
+
toggleNotification({
|
|
70
|
+
type: 'warning',
|
|
71
|
+
message: formatAPIError(error),
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
onSuccess() {
|
|
76
|
+
toggleNotification({
|
|
77
|
+
type: 'success',
|
|
78
|
+
message: { id: 'notification.success.saved', defaultMessage: 'Saved' },
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const updateWorkflowStages = (workflowId, stages) => {
|
|
85
|
+
return mutateAsync({ workflowId, stages });
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const submitForm = async () => {
|
|
89
|
+
await updateWorkflowStages(currentWorkflow.id, currentWorkflow.stages);
|
|
90
|
+
await refetchWorkflow();
|
|
91
|
+
|
|
92
|
+
setIsConfirmDeleteDialogOpen(false);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handleConfirmDeleteDialog = async () => {
|
|
96
|
+
await submitForm();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const toggleConfirmDeleteDialog = () => {
|
|
100
|
+
setIsConfirmDeleteDialogOpen((prev) => !prev);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const formik = useFormik({
|
|
104
|
+
enableReinitialize: true,
|
|
105
|
+
initialValues: currentWorkflow,
|
|
106
|
+
async onSubmit() {
|
|
107
|
+
if (currentWorkflowHasDeletedServerStages) {
|
|
108
|
+
setIsConfirmDeleteDialogOpen(true);
|
|
109
|
+
} else {
|
|
110
|
+
submitForm();
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
validationSchema: getWorkflowValidationSchema({ formatMessage }),
|
|
114
|
+
validateOnChange: false,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
useInjectReducer(REDUX_NAMESPACE, reducer);
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
dispatch(setWorkflows({ status: workflowsData.status, data: workflowsData.data }));
|
|
121
|
+
}, [workflowsData.status, workflowsData.data, dispatch]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
trackUsage('didViewWorkflow');
|
|
125
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<CheckPagePermissions permissions={adminPermissions.settings['review-workflows'].main}>
|
|
130
|
+
<Layout>
|
|
131
|
+
<SettingsPageTitle
|
|
132
|
+
name={formatMessage({
|
|
133
|
+
id: 'Settings.review-workflows.page.title',
|
|
134
|
+
defaultMessage: 'Review Workflows',
|
|
135
|
+
})}
|
|
136
|
+
/>
|
|
137
|
+
<Main tabIndex={-1}>
|
|
138
|
+
<FormikProvider value={formik}>
|
|
139
|
+
<Form onSubmit={formik.handleSubmit}>
|
|
140
|
+
<HeaderLayout
|
|
141
|
+
primaryAction={
|
|
142
|
+
<Button
|
|
143
|
+
startIcon={<Check />}
|
|
144
|
+
type="submit"
|
|
145
|
+
size="M"
|
|
146
|
+
disabled={!currentWorkflowIsDirty}
|
|
147
|
+
// if the confirm dialog is open the loading state is on
|
|
148
|
+
// the confirm button already
|
|
149
|
+
loading={!isConfirmDeleteDialogOpen && isLoading}
|
|
150
|
+
>
|
|
151
|
+
{formatMessage({
|
|
152
|
+
id: 'global.save',
|
|
153
|
+
defaultMessage: 'Save',
|
|
154
|
+
})}
|
|
155
|
+
</Button>
|
|
156
|
+
}
|
|
157
|
+
title={formatMessage({
|
|
158
|
+
id: 'Settings.review-workflows.page.title',
|
|
159
|
+
defaultMessage: 'Review Workflows',
|
|
160
|
+
})}
|
|
161
|
+
subtitle={formatMessage(
|
|
162
|
+
{
|
|
163
|
+
id: 'Settings.review-workflows.page.subtitle',
|
|
164
|
+
defaultMessage: '{count, plural, one {# stage} other {# stages}}',
|
|
165
|
+
},
|
|
166
|
+
{ count: currentWorkflow?.stages?.length ?? 0 }
|
|
167
|
+
)}
|
|
168
|
+
/>
|
|
169
|
+
<ContentLayout>
|
|
170
|
+
{status === 'loading' && (
|
|
171
|
+
<Loader>
|
|
172
|
+
{formatMessage({
|
|
173
|
+
id: 'Settings.review-workflows.page.isLoading',
|
|
174
|
+
defaultMessage: 'Workflow is loading',
|
|
175
|
+
})}
|
|
176
|
+
</Loader>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
<Stages stages={formik.values?.stages} />
|
|
180
|
+
</ContentLayout>
|
|
181
|
+
</Form>
|
|
182
|
+
</FormikProvider>
|
|
183
|
+
|
|
184
|
+
<ConfirmDialog
|
|
185
|
+
bodyText={{
|
|
186
|
+
id: 'Settings.review-workflows.page.delete.confirm.body',
|
|
187
|
+
defaultMessage:
|
|
188
|
+
'All entries assigned to deleted stages will be moved to the first stage. Are you sure you want to save this?',
|
|
189
|
+
}}
|
|
190
|
+
isConfirmButtonLoading={isLoading}
|
|
191
|
+
isOpen={isConfirmDeleteDialogOpen}
|
|
192
|
+
onToggleDialog={toggleConfirmDeleteDialog}
|
|
193
|
+
onConfirm={handleConfirmDeleteDialog}
|
|
194
|
+
/>
|
|
195
|
+
</Main>
|
|
196
|
+
</Layout>
|
|
197
|
+
</CheckPagePermissions>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACTION_SET_WORKFLOWS,
|
|
3
|
+
ACTION_DELETE_STAGE,
|
|
4
|
+
ACTION_ADD_STAGE,
|
|
5
|
+
ACTION_UPDATE_STAGE,
|
|
6
|
+
} from '../constants';
|
|
7
|
+
|
|
8
|
+
export function setWorkflows({ status, data }) {
|
|
9
|
+
return {
|
|
10
|
+
type: ACTION_SET_WORKFLOWS,
|
|
11
|
+
payload: {
|
|
12
|
+
status,
|
|
13
|
+
workflows: data,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function deleteStage(stageId) {
|
|
19
|
+
return {
|
|
20
|
+
type: ACTION_DELETE_STAGE,
|
|
21
|
+
payload: {
|
|
22
|
+
stageId,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function addStage(stage = {}) {
|
|
28
|
+
return {
|
|
29
|
+
type: ACTION_ADD_STAGE,
|
|
30
|
+
payload: stage,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function updateStage(stageId, payload) {
|
|
35
|
+
return {
|
|
36
|
+
type: ACTION_UPDATE_STAGE,
|
|
37
|
+
payload: {
|
|
38
|
+
stageId,
|
|
39
|
+
...payload,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
|
|
5
|
+
import { Box, Flex, Typography } from '@strapi/design-system';
|
|
6
|
+
import { PlusCircle } from '@strapi/icons';
|
|
7
|
+
|
|
8
|
+
const StyledAddIcon = styled(PlusCircle)`
|
|
9
|
+
> circle {
|
|
10
|
+
fill: ${({ theme }) => theme.colors.neutral150};
|
|
11
|
+
}
|
|
12
|
+
> path {
|
|
13
|
+
fill: ${({ theme }) => theme.colors.neutral600};
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const StyledButton = styled(Box)`
|
|
18
|
+
border-radius: 26px;
|
|
19
|
+
|
|
20
|
+
svg {
|
|
21
|
+
height: ${({ theme }) => theme.spaces[6]};
|
|
22
|
+
width: ${({ theme }) => theme.spaces[6]};
|
|
23
|
+
|
|
24
|
+
> path {
|
|
25
|
+
fill: ${({ theme }) => theme.colors.neutral600};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&:hover {
|
|
30
|
+
color: ${({ theme }) => theme.colors.primary600} !important;
|
|
31
|
+
${Typography} {
|
|
32
|
+
color: ${({ theme }) => theme.colors.primary600} !important;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
${StyledAddIcon} {
|
|
36
|
+
> circle {
|
|
37
|
+
fill: ${({ theme }) => theme.colors.primary600};
|
|
38
|
+
}
|
|
39
|
+
> path {
|
|
40
|
+
fill: ${({ theme }) => theme.colors.neutral100};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
&:active {
|
|
46
|
+
${Typography} {
|
|
47
|
+
color: ${({ theme }) => theme.colors.primary600};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
${StyledAddIcon} {
|
|
51
|
+
> circle {
|
|
52
|
+
fill: ${({ theme }) => theme.colors.primary600};
|
|
53
|
+
}
|
|
54
|
+
> path {
|
|
55
|
+
fill: ${({ theme }) => theme.colors.neutral100};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
export function AddStage({ children, ...props }) {
|
|
62
|
+
return (
|
|
63
|
+
<StyledButton
|
|
64
|
+
as="button"
|
|
65
|
+
background="neutral0"
|
|
66
|
+
border="neutral150"
|
|
67
|
+
paddingBottom={3}
|
|
68
|
+
paddingLeft={4}
|
|
69
|
+
paddingRight={4}
|
|
70
|
+
paddingTop={3}
|
|
71
|
+
shadow="filterShadow"
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
<Flex gap={2}>
|
|
75
|
+
<StyledAddIcon aria-hidden />
|
|
76
|
+
|
|
77
|
+
<Typography variant="pi" fontWeight="bold" textColor="neutral500">
|
|
78
|
+
{children}
|
|
79
|
+
</Typography>
|
|
80
|
+
</Flex>
|
|
81
|
+
</StyledButton>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
AddStage.propTypes = {
|
|
86
|
+
children: PropTypes.node.isRequired,
|
|
87
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './AddStage';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { useField } from 'formik';
|
|
4
|
+
import { useIntl } from 'react-intl';
|
|
5
|
+
import { useDispatch } from 'react-redux';
|
|
6
|
+
import {
|
|
7
|
+
Accordion,
|
|
8
|
+
AccordionToggle,
|
|
9
|
+
AccordionContent,
|
|
10
|
+
Grid,
|
|
11
|
+
GridItem,
|
|
12
|
+
IconButton,
|
|
13
|
+
TextInput,
|
|
14
|
+
} from '@strapi/design-system';
|
|
15
|
+
import { useTracking } from '@strapi/helper-plugin';
|
|
16
|
+
import { Trash } from '@strapi/icons';
|
|
17
|
+
|
|
18
|
+
import { deleteStage, updateStage } from '../../../actions';
|
|
19
|
+
|
|
20
|
+
function Stage({ id, name, index, canDelete, isOpen: isOpenDefault = false }) {
|
|
21
|
+
const { formatMessage } = useIntl();
|
|
22
|
+
const { trackUsage } = useTracking();
|
|
23
|
+
const [isOpen, setIsOpen] = useState(isOpenDefault);
|
|
24
|
+
const fieldIdentifier = `stages.${index}.name`;
|
|
25
|
+
const [field, meta] = useField(fieldIdentifier);
|
|
26
|
+
const dispatch = useDispatch();
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Accordion
|
|
30
|
+
size="S"
|
|
31
|
+
variant="primary"
|
|
32
|
+
onToggle={() => {
|
|
33
|
+
setIsOpen(!isOpen);
|
|
34
|
+
|
|
35
|
+
if (!isOpen) {
|
|
36
|
+
trackUsage('willEditStage');
|
|
37
|
+
}
|
|
38
|
+
}}
|
|
39
|
+
expanded={isOpen}
|
|
40
|
+
shadow="tableShadow"
|
|
41
|
+
>
|
|
42
|
+
<AccordionToggle
|
|
43
|
+
title={name}
|
|
44
|
+
togglePosition="left"
|
|
45
|
+
action={
|
|
46
|
+
canDelete ? (
|
|
47
|
+
<IconButton
|
|
48
|
+
background="transparent"
|
|
49
|
+
noBorder
|
|
50
|
+
onClick={() => dispatch(deleteStage(id))}
|
|
51
|
+
label={formatMessage({
|
|
52
|
+
id: 'Settings.review-workflows.stage.delete',
|
|
53
|
+
defaultMessage: 'Delete stage',
|
|
54
|
+
})}
|
|
55
|
+
icon={<Trash />}
|
|
56
|
+
/>
|
|
57
|
+
) : null
|
|
58
|
+
}
|
|
59
|
+
/>
|
|
60
|
+
<AccordionContent padding={6} background="neutral0" hasRadius>
|
|
61
|
+
<Grid gap={4}>
|
|
62
|
+
<GridItem col={6}>
|
|
63
|
+
<TextInput
|
|
64
|
+
{...field}
|
|
65
|
+
id={fieldIdentifier}
|
|
66
|
+
value={name}
|
|
67
|
+
label={formatMessage({
|
|
68
|
+
id: 'Settings.review-workflows.stage.name.label',
|
|
69
|
+
defaultMessage: 'Stage name',
|
|
70
|
+
})}
|
|
71
|
+
error={meta.error ?? false}
|
|
72
|
+
onChange={(event) => {
|
|
73
|
+
field.onChange(event);
|
|
74
|
+
dispatch(updateStage(id, { name: event.target.value }));
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
</GridItem>
|
|
78
|
+
</Grid>
|
|
79
|
+
</AccordionContent>
|
|
80
|
+
</Accordion>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { Stage };
|
|
85
|
+
|
|
86
|
+
Stage.propTypes = PropTypes.shape({
|
|
87
|
+
id: PropTypes.number.isRequired,
|
|
88
|
+
name: PropTypes.string.isRequired,
|
|
89
|
+
canDelete: PropTypes.bool.isRequired,
|
|
90
|
+
}).isRequired;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Stage';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { useIntl } from 'react-intl';
|
|
5
|
+
import { useDispatch } from 'react-redux';
|
|
6
|
+
import { Box, Flex } from '@strapi/design-system';
|
|
7
|
+
import { useTracking } from '@strapi/helper-plugin';
|
|
8
|
+
|
|
9
|
+
import { addStage } from '../../actions';
|
|
10
|
+
import { AddStage } from '../AddStage';
|
|
11
|
+
import { Stage } from './Stage';
|
|
12
|
+
|
|
13
|
+
const StagesContainer = styled(Box)`
|
|
14
|
+
position: relative;
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const Background = styled(Box)`
|
|
18
|
+
left: 50%;
|
|
19
|
+
position: absolute;
|
|
20
|
+
top: 0;
|
|
21
|
+
transform: translateX(-50%);
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
function Stages({ stages }) {
|
|
25
|
+
const { formatMessage } = useIntl();
|
|
26
|
+
const dispatch = useDispatch();
|
|
27
|
+
const { trackUsage } = useTracking();
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Flex direction="column" gap={6} width="100%">
|
|
31
|
+
<StagesContainer spacing={4} width="100%">
|
|
32
|
+
<Background background="neutral200" height="100%" width={2} zIndex={1} />
|
|
33
|
+
|
|
34
|
+
<Flex
|
|
35
|
+
direction="column"
|
|
36
|
+
alignItems="stretch"
|
|
37
|
+
gap={6}
|
|
38
|
+
zIndex={2}
|
|
39
|
+
position="relative"
|
|
40
|
+
as="ol"
|
|
41
|
+
>
|
|
42
|
+
{stages.map((stage, index) => {
|
|
43
|
+
const id = stage?.id ?? stage.__temp_key__;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Box key={`stage-${id}`} as="li">
|
|
47
|
+
<Stage
|
|
48
|
+
{...stage}
|
|
49
|
+
id={id}
|
|
50
|
+
index={index}
|
|
51
|
+
canDelete={stages.length > 1}
|
|
52
|
+
isOpen={!stage.id}
|
|
53
|
+
/>
|
|
54
|
+
</Box>
|
|
55
|
+
);
|
|
56
|
+
})}
|
|
57
|
+
</Flex>
|
|
58
|
+
</StagesContainer>
|
|
59
|
+
|
|
60
|
+
<Flex direction="column" gap={6}>
|
|
61
|
+
<AddStage
|
|
62
|
+
type="button"
|
|
63
|
+
onClick={() => {
|
|
64
|
+
dispatch(addStage({ name: '' }));
|
|
65
|
+
trackUsage('willCreateStage');
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
{formatMessage({
|
|
69
|
+
id: 'Settings.review-workflows.stage.add',
|
|
70
|
+
defaultMessage: 'Add new stage',
|
|
71
|
+
})}
|
|
72
|
+
</AddStage>
|
|
73
|
+
</Flex>
|
|
74
|
+
</Flex>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { Stages };
|
|
79
|
+
|
|
80
|
+
Stages.defaultProps = {
|
|
81
|
+
stages: [],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
Stages.propTypes = {
|
|
85
|
+
stages: PropTypes.arrayOf(
|
|
86
|
+
PropTypes.shape({
|
|
87
|
+
id: PropTypes.number,
|
|
88
|
+
__temp_key__: PropTypes.number,
|
|
89
|
+
name: PropTypes.string.isRequired,
|
|
90
|
+
})
|
|
91
|
+
),
|
|
92
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Stages';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const REDUX_NAMESPACE = 'settings_review-workflows';
|
|
2
|
+
|
|
3
|
+
export const ACTION_SET_WORKFLOWS = `Settings/Review_Workflows/SET_WORKFLOWS`;
|
|
4
|
+
export const ACTION_DELETE_STAGE = `Settings/Review_Workflows/WORKFLOW_DELETE_STAGE`;
|
|
5
|
+
export const ACTION_ADD_STAGE = `Settings/Review_Workflows/WORKFLOW_ADD_STAGE`;
|
|
6
|
+
export const ACTION_UPDATE_STAGE = `Settings/Review_Workflows/WORKFLOW_UPDATE_STAGE`;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useQuery, useQueryClient } from 'react-query';
|
|
2
|
+
import { useFetchClient } from '@strapi/helper-plugin';
|
|
3
|
+
|
|
4
|
+
const QUERY_BASE_KEY = 'review-workflows';
|
|
5
|
+
const API_BASE_URL = '/admin/review-workflows';
|
|
6
|
+
|
|
7
|
+
export function useReviewWorkflows(workflowId) {
|
|
8
|
+
const { get } = useFetchClient();
|
|
9
|
+
const client = useQueryClient();
|
|
10
|
+
const workflowQueryKey = [QUERY_BASE_KEY, workflowId ?? 'default'];
|
|
11
|
+
|
|
12
|
+
async function fetchWorkflows({ params = { populate: 'stages' } }) {
|
|
13
|
+
try {
|
|
14
|
+
const {
|
|
15
|
+
data: { data },
|
|
16
|
+
} = await get(`${API_BASE_URL}/workflows/${workflowId ?? ''}`, { params });
|
|
17
|
+
|
|
18
|
+
return data;
|
|
19
|
+
} catch (err) {
|
|
20
|
+
// silence
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function refetchWorkflow() {
|
|
26
|
+
await client.refetchQueries(workflowQueryKey);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const workflows = useQuery(workflowQueryKey, fetchWorkflows);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
workflows,
|
|
33
|
+
refetchWorkflow,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { current, produce } from 'immer';
|
|
2
|
+
import isEqual from 'lodash/isEqual';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ACTION_SET_WORKFLOWS,
|
|
6
|
+
ACTION_DELETE_STAGE,
|
|
7
|
+
ACTION_ADD_STAGE,
|
|
8
|
+
ACTION_UPDATE_STAGE,
|
|
9
|
+
} from '../constants';
|
|
10
|
+
|
|
11
|
+
export const initialState = {
|
|
12
|
+
status: 'loading',
|
|
13
|
+
serverState: {
|
|
14
|
+
currentWorkflow: null,
|
|
15
|
+
workflows: [],
|
|
16
|
+
},
|
|
17
|
+
clientState: {
|
|
18
|
+
currentWorkflow: { data: null, isDirty: false, hasDeletedServerStages: false },
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function reducer(state = initialState, action) {
|
|
23
|
+
return produce(state, (draft) => {
|
|
24
|
+
const { payload } = action;
|
|
25
|
+
|
|
26
|
+
switch (action.type) {
|
|
27
|
+
case ACTION_SET_WORKFLOWS: {
|
|
28
|
+
const { status, workflows } = payload;
|
|
29
|
+
|
|
30
|
+
draft.status = status;
|
|
31
|
+
|
|
32
|
+
if (workflows) {
|
|
33
|
+
const defaultWorkflow = workflows[0];
|
|
34
|
+
|
|
35
|
+
draft.serverState.workflows = workflows;
|
|
36
|
+
draft.serverState.currentWorkflow = defaultWorkflow;
|
|
37
|
+
draft.clientState.currentWorkflow.data = defaultWorkflow;
|
|
38
|
+
draft.clientState.currentWorkflow.hasDeletedServerStages = false;
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case ACTION_DELETE_STAGE: {
|
|
44
|
+
const { stageId } = payload;
|
|
45
|
+
const { currentWorkflow } = state.clientState;
|
|
46
|
+
|
|
47
|
+
draft.clientState.currentWorkflow.data.stages = currentWorkflow.data.stages.filter(
|
|
48
|
+
(stage) => (stage?.id ?? stage.__temp_key__) !== stageId
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (!currentWorkflow.hasDeletedServerStages) {
|
|
52
|
+
draft.clientState.currentWorkflow.hasDeletedServerStages =
|
|
53
|
+
!!state.serverState.currentWorkflow.stages.find((stage) => stage.id === stageId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case ACTION_ADD_STAGE: {
|
|
60
|
+
const { currentWorkflow } = state.clientState;
|
|
61
|
+
|
|
62
|
+
if (!currentWorkflow.data) {
|
|
63
|
+
draft.clientState.currentWorkflow.data = {
|
|
64
|
+
stages: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const newTempKey = getMaxTempKey(draft.clientState.currentWorkflow.data.stages);
|
|
69
|
+
|
|
70
|
+
draft.clientState.currentWorkflow.data.stages.push({
|
|
71
|
+
...payload,
|
|
72
|
+
__temp_key__: newTempKey,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case ACTION_UPDATE_STAGE: {
|
|
79
|
+
const { currentWorkflow } = state.clientState;
|
|
80
|
+
const { stageId, ...modified } = payload;
|
|
81
|
+
|
|
82
|
+
draft.clientState.currentWorkflow.data.stages = currentWorkflow.data.stages.map((stage) =>
|
|
83
|
+
(stage.id ?? stage.__temp_key__) === stageId
|
|
84
|
+
? {
|
|
85
|
+
...stage,
|
|
86
|
+
...modified,
|
|
87
|
+
}
|
|
88
|
+
: stage
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
default:
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (state.clientState.currentWorkflow.data) {
|
|
99
|
+
draft.clientState.currentWorkflow.isDirty = !isEqual(
|
|
100
|
+
current(draft.clientState.currentWorkflow).data,
|
|
101
|
+
draft.serverState.currentWorkflow
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @type {(stages: Array<{id?: number; __temp_key__: number}>) => number}
|
|
109
|
+
*/
|
|
110
|
+
const getMaxTempKey = (stages = []) => {
|
|
111
|
+
/**
|
|
112
|
+
* We check if there are ids or __temp_key__ because you may add a stage to a list of stages
|
|
113
|
+
* already in the DB, alternatively you might add multiple new stages at once.
|
|
114
|
+
*/
|
|
115
|
+
const ids = stages.map((stage) => stage.id ?? stage.__temp_key__);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* If there are no ids it will return 0 as the max value
|
|
119
|
+
* because the max value is -1.
|
|
120
|
+
*/
|
|
121
|
+
return Math.max(...ids, -1) + 1;
|
|
122
|
+
};
|