@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.
Files changed (77) hide show
  1. package/admin/src/content-manager/components/DynamicTable/CellContent/PublicationState/PublicationState.js +26 -0
  2. package/admin/src/content-manager/components/DynamicTable/CellContent/PublicationState/index.js +1 -0
  3. package/admin/src/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +2 -0
  4. package/admin/src/content-manager/components/DynamicTable/index.js +25 -49
  5. package/admin/src/content-manager/components/DynamicZone/components/DynamicComponent.js +2 -0
  6. package/admin/src/content-manager/pages/EditView/Information/index.js +77 -53
  7. package/admin/src/content-manager/pages/EditView/InformationBox/InformationBoxCE.js +13 -0
  8. package/admin/src/content-manager/pages/EditView/InformationBox/index.js +3 -0
  9. package/admin/src/content-manager/pages/EditView/index.js +3 -4
  10. package/admin/src/content-manager/pages/ListView/index.js +6 -9
  11. package/admin/src/content-manager/sharedReducers/crudReducer/actions.js +6 -0
  12. package/admin/src/content-manager/sharedReducers/crudReducer/constants.js +1 -0
  13. package/admin/src/content-manager/sharedReducers/crudReducer/reducer.js +5 -0
  14. package/admin/src/index.js +1 -0
  15. package/admin/src/translations/en.json +6 -0
  16. package/build/{Admin-authenticatedApp.217db666.chunk.js → Admin-authenticatedApp.52c88751.chunk.js} +2 -2
  17. package/build/{Admin_settingsPage.1dbfc9ce.chunk.js → Admin_settingsPage.257b3477.chunk.js} +7 -7
  18. package/build/{admin-app.558af642.chunk.js → admin-app.dfaeea5d.chunk.js} +18 -18
  19. package/build/content-manager.def692c2.chunk.js +1130 -0
  20. package/build/content-type-builder-translation-en-json.510e88ca.chunk.js +1 -0
  21. package/build/content-type-builder.5e1f4afc.chunk.js +126 -0
  22. package/build/en-json.08303b37.chunk.js +1 -0
  23. package/build/index.html +1 -1
  24. package/build/{main.ef8db4a2.js → main.120be100.js} +145 -145
  25. package/build/review-workflows-settings.9092ed72.chunk.js +106 -0
  26. package/build/{runtime~main.3a92d953.js → runtime~main.112b3101.js} +1 -1
  27. package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/ReviewWorkflowsStageEE.js +15 -0
  28. package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +45 -0
  29. package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/index.js +3 -0
  30. package/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js +135 -0
  31. package/ee/admin/content-manager/pages/EditView/InformationBox/index.js +3 -0
  32. package/ee/admin/hooks/useSettingsMenu/utils/customAdminLinks.js +12 -12
  33. package/ee/admin/hooks/useSettingsMenu/utils/customGlobalLinks.js +21 -13
  34. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js +199 -0
  35. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/actions/index.js +42 -0
  36. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/AddStage/AddStage.js +87 -0
  37. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/AddStage/index.js +1 -0
  38. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +90 -0
  39. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/index.js +1 -0
  40. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stages.js +92 -0
  41. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/index.js +1 -0
  42. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js +6 -0
  43. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows.js +35 -0
  44. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/index.js +3 -0
  45. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js +122 -0
  46. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/getWorkflowValidationSchema.js +25 -0
  47. package/ee/admin/pages/SettingsPage/utils/customRoutes.js +16 -2
  48. package/ee/admin/permissions/customPermissions.js +3 -0
  49. package/ee/server/bootstrap.js +13 -0
  50. package/ee/server/config/admin-actions.js +10 -0
  51. package/ee/server/constants/default-stages.json +14 -0
  52. package/ee/server/constants/default-workflow.json +1 -0
  53. package/ee/server/constants/workflows.js +8 -0
  54. package/ee/server/content-types/index.js +9 -0
  55. package/ee/server/content-types/workflow/index.js +31 -0
  56. package/ee/server/content-types/workflow-stage/index.js +36 -0
  57. package/ee/server/controllers/index.js +2 -0
  58. package/ee/server/controllers/workflows/index.js +36 -0
  59. package/ee/server/controllers/workflows/stages/index.js +102 -0
  60. package/ee/server/index.js +1 -0
  61. package/ee/server/middlewares/review-workflows.js +40 -0
  62. package/ee/server/register.js +8 -0
  63. package/ee/server/routes/index.js +104 -0
  64. package/ee/server/services/index.js +4 -0
  65. package/ee/server/services/review-workflows/entity-service-decorator.js +54 -0
  66. package/ee/server/services/review-workflows/review-workflows.js +111 -0
  67. package/ee/server/services/review-workflows/stages.js +249 -0
  68. package/ee/server/services/review-workflows/workflows.js +25 -0
  69. package/ee/server/utils/index.js +8 -0
  70. package/ee/server/utils/persisted-tables.js +114 -22
  71. package/ee/server/utils/review-workflows.js +34 -0
  72. package/ee/server/validation/review-workflows.js +24 -0
  73. package/package.json +9 -9
  74. package/build/content-manager.d1565bfc.chunk.js +0 -1132
  75. package/build/content-type-builder-translation-en-json.6c8e69ab.chunk.js +0 -1
  76. package/build/content-type-builder.9d780e7f.chunk.js +0 -126
  77. 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,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,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,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,3 @@
1
+ import { ReviewWorkflowsPage } from './ReviewWorkflows';
2
+
3
+ export default ReviewWorkflowsPage;
@@ -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
+ };