@strapi/admin 4.11.3 → 4.12.0-beta.0
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/components/AuthenticatedApp/index.js +2 -2
- package/admin/src/constants.js +83 -83
- package/admin/src/content-manager/components/CollectionTypeFormWrapper/index.js +8 -5
- package/admin/src/content-manager/components/Inputs/index.js +3 -47
- package/admin/src/content-manager/components/RelationInput/RelationInput.js +99 -178
- package/admin/src/content-manager/components/RelationInput/components/Option.js +17 -15
- package/admin/src/content-manager/components/RelationInput/components/RelationList.js +2 -2
- package/admin/src/content-manager/components/SingleTypeFormWrapper/index.js +34 -37
- package/admin/src/content-manager/pages/EditSettingsView/components/ModalForm.js +0 -27
- package/admin/src/content-manager/pages/ListView/components/TableRows/index.js +93 -14
- package/admin/src/content-manager/pages/ListView/index.js +65 -47
- package/admin/src/content-manager/pages/ListView/utils/buildValidGetParams.js +30 -0
- package/admin/src/content-manager/pages/ListView/utils/index.js +1 -1
- package/admin/src/content-manager/utils/mergeMetasWithSchema.js +5 -1
- package/admin/src/hooks/index.js +0 -1
- package/admin/src/hooks/useAdminUsers/useAdminUsers.js +3 -3
- package/admin/src/hooks/useEnterprise/useEnterprise.js +4 -4
- package/admin/src/pages/App/index.js +28 -23
- package/admin/src/pages/AuthPage/components/Register/index.js +5 -1
- package/admin/src/pages/ProfilePage/index.js +6 -1
- package/admin/src/pages/SettingsPage/components/Tokens/Table/index.js +15 -1
- package/admin/src/pages/SettingsPage/components/Tokens/TokenBox/index.js +1 -1
- package/admin/src/pages/SettingsPage/pages/Roles/ProtectedEditPage/index.js +4 -10
- package/admin/src/pages/SettingsPage/pages/Users/EditPage/index.js +2 -2
- package/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/WebhookForm/utils/makeWebhookValidationSchema.js +11 -5
- package/admin/src/translations/ca.json +1 -0
- package/admin/src/translations/en.json +4 -1
- package/admin/src/translations/es.json +5 -0
- package/admin/src/translations/fr.json +1 -0
- package/admin/src/translations/zh-Hans.json +1 -1
- package/build/0cd5f8915b265d5b1856.png +0 -0
- package/build/2799.cf9b491f.chunk.js +1 -0
- package/build/4485.d3c6dd1d.chunk.js +6 -0
- package/build/539.865446c0.chunk.js +1 -0
- package/build/{5542.64b623c9.chunk.js → 5542.c62d0daf.chunk.js} +1 -1
- package/build/{5563.86f9aa9c.chunk.js → 5563.a146acac.chunk.js} +2 -2
- package/build/7018.f3dad3c1.chunk.js +1 -0
- package/build/7259.0e25ab5d.chunk.js +1 -0
- package/build/9465.d8fc1377.chunk.js +112 -0
- package/build/{6405.27e1bee5.chunk.js → 970.89601f27.chunk.js} +2 -2
- package/build/9944.29289a16.chunk.js +26 -0
- package/build/Admin-authenticatedApp.9d3afb79.chunk.js +79 -0
- package/build/{Admin_settingsPage.4069bb8a.chunk.js → Admin_settingsPage.074655f6.chunk.js} +13 -13
- package/build/admin-app.3ede71ad.chunk.js +61 -0
- package/build/{admin-edit-roles-page.2040034a.chunk.js → admin-edit-roles-page.3fdd6b9d.chunk.js} +11 -11
- package/build/admin-edit-users.78552758.chunk.js +10 -0
- package/build/admin-users.c23322fc.chunk.js +11 -0
- package/build/api-tokens-list-page.a103f526.chunk.js +16 -0
- package/build/audit-logs-settings-page.37fe915c.chunk.js +1 -0
- package/build/ca-json.1fed5d8b.chunk.js +1 -0
- package/build/content-manager.08541eeb.chunk.js +1094 -0
- package/build/{content-type-builder-list-view.0c3ceb4e.chunk.js → content-type-builder-list-view.a200a358.chunk.js} +1 -1
- package/build/content-type-builder-translation-en-json.38e20391.chunk.js +1 -0
- package/build/content-type-builder.de22f7c9.chunk.js +166 -0
- package/build/{email-settings-page.6b38222d.chunk.js → email-settings-page.45695daa.chunk.js} +1 -1
- package/build/en-json.fb9f6ddd.chunk.js +1 -0
- package/build/es-json.42096084.chunk.js +1 -0
- package/build/fr-json.69789980.chunk.js +1 -0
- package/build/{i18n-settings-page.ff863f20.chunk.js → i18n-settings-page.29308d0b.chunk.js} +1 -1
- package/build/index.html +1 -1
- package/build/main.a8ede50d.js +2927 -0
- package/build/review-workflows-settings-create-view.56f61e18.chunk.js +1 -0
- package/build/review-workflows-settings-edit-view.912bc9c0.chunk.js +1 -0
- package/build/review-workflows-settings-list-view.cf6a08d3.chunk.js +56 -0
- package/build/runtime~main.5e9bf4b3.js +2 -0
- package/build/sso-settings-page.0cdb96a6.chunk.js +1 -0
- package/build/transfer-tokens-list-page.7237443d.chunk.js +16 -0
- package/build/{upload-settings.43cf16cd.chunk.js → upload-settings.cb6c14c3.chunk.js} +1 -1
- package/build/{upload.72f8f8fc.chunk.js → upload.7e629643.chunk.js} +2 -2
- package/build/users-advanced-settings-page.750b1f76.chunk.js +9 -0
- package/build/{users-email-settings-page.33359797.chunk.js → users-email-settings-page.e9bcd865.chunk.js} +1 -1
- package/build/{users-providers-settings-page.1e7a4a71.chunk.js → users-providers-settings-page.a94253e9.chunk.js} +1 -1
- package/build/{users-roles-settings-page.235378b6.chunk.js → users-roles-settings-page.d286426a.chunk.js} +5 -5
- package/build/webhook-edit-page.77ef4f1a.chunk.js +33 -0
- package/build/{zh-Hans-json.4cfef87d.chunk.js → zh-Hans-json.fada6f40.chunk.js} +1 -1
- package/ee/admin/constants.js +14 -14
- package/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js +84 -30
- package/ee/admin/content-manager/{components/DynamicTable/CellContent/ReviewWorkflowsStage → pages/ListView/ReviewWorkflowsColumn}/ReviewWorkflowsStageEE.js +7 -2
- package/ee/admin/content-manager/pages/ListView/ReviewWorkflowsColumn/constants.js +24 -0
- package/ee/admin/content-manager/pages/ListView/ReviewWorkflowsColumn/index.js +1 -0
- package/ee/admin/hooks/useLicenseLimitNotification/index.js +17 -6
- package/ee/admin/hooks/useLicenseLimits/index.js +1 -32
- package/ee/admin/hooks/useLicenseLimits/useLicenseLimits.js +44 -0
- package/ee/admin/pages/SettingsPage/constants.js +25 -1
- package/ee/admin/pages/SettingsPage/pages/ApplicationInfosPage/components/AdminSeatInfo/index.js +6 -4
- package/ee/admin/pages/SettingsPage/pages/AuditLogs/ListView/hooks/useAuditLogsData.js +6 -4
- package/ee/admin/pages/SettingsPage/pages/AuditLogs/ListView/index.js +4 -9
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/actions/index.js +19 -4
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Layout/Layout.js +65 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Layout/index.js +1 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/LimitsModal.js +111 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/assets/balloon.png +0 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/index.js +3 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/ProtectedPage/ProtectedPage.js +21 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/ProtectedPage/index.js +1 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +4 -4
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/WorkflowAttributes/WorkflowAttributes.js +110 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/WorkflowAttributes/index.js +1 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js +3 -1
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows.js +13 -19
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/CreateView/CreateView.js +246 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/CreateView/index.js +13 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/EditView/EditView.js +269 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/EditView/index.js +13 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/ListView/ListView.js +382 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/ListView/index.js +13 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js +53 -23
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/getWorkflowValidationSchema.js +43 -28
- package/ee/admin/pages/SettingsPage/pages/SingleSignOn/index.js +4 -9
- package/ee/admin/pages/SettingsPage/pages/Users/ListPage/CreateAction/index.js +9 -2
- package/ee/server/config/admin-actions.js +24 -0
- package/ee/server/constants/default-stages.json +8 -4
- package/ee/server/constants/default-workflow.json +3 -1
- package/ee/server/constants/workflows.js +10 -1
- package/ee/server/content-types/workflow/index.js +10 -0
- package/ee/server/content-types/workflow-stage/index.js +3 -1
- package/ee/server/controllers/admin.js +1 -0
- package/ee/server/controllers/workflows/index.js +135 -8
- package/ee/server/controllers/workflows/stages/index.js +38 -38
- package/ee/server/migrations/review-workflows-content-types.js +29 -0
- package/ee/server/migrations/review-workflows-deleted-ct-in-workflows.js +39 -0
- package/ee/server/migrations/review-workflows-stage-attribute.js +49 -0
- package/ee/server/migrations/review-workflows-stages-color.js +2 -2
- package/ee/server/migrations/review-workflows-workflow-name.js +21 -0
- package/ee/server/register.js +12 -2
- package/ee/server/routes/review-workflows.js +44 -10
- package/ee/server/services/index.js +1 -0
- package/ee/server/services/review-workflows/entity-service-decorator.js +28 -24
- package/ee/server/services/review-workflows/review-workflows.js +45 -53
- package/ee/server/services/review-workflows/stages.js +84 -46
- package/ee/server/services/review-workflows/validation.js +60 -0
- package/ee/server/services/review-workflows/workflows/content-types.js +80 -0
- package/ee/server/services/review-workflows/workflows/index.js +207 -0
- package/ee/server/utils/review-workflows.js +30 -25
- package/ee/server/validation/review-workflows.js +49 -10
- package/package.json +13 -14
- package/server/content-types/User.js +10 -0
- package/server/strategies/api-token.js +9 -5
- package/server/strategies/data-transfer.js +9 -5
- package/admin/src/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +0 -2
- package/admin/src/content-manager/components/RelationInput/components/Relation.js +0 -53
- package/admin/src/content-manager/pages/ListView/utils/buildQueryString.js +0 -36
- package/admin/src/content-manager/pages/ListView/utils/createPluginsFilter.js +0 -4
- package/admin/src/hooks/useLicenseLimits/index.js +0 -3
- package/admin/src/pages/App/utils/index.js +0 -3
- package/admin/src/pages/App/utils/unique-identifier.js +0 -12
- package/build/1799.84268ad3.chunk.js +0 -33
- package/build/5932.6a23b88c.chunk.js +0 -1
- package/build/7018.98feed67.chunk.js +0 -1
- package/build/7259.fb69d4bf.chunk.js +0 -1
- package/build/Admin-authenticatedApp.69855f1b.chunk.js +0 -79
- package/build/admin-app.fea867af.chunk.js +0 -61
- package/build/admin-edit-users.53e4290a.chunk.js +0 -10
- package/build/admin-users.3b12dca2.chunk.js +0 -11
- package/build/api-tokens-list-page.201fb67a.chunk.js +0 -16
- package/build/audit-logs-settings-page.b07ad202.chunk.js +0 -1
- package/build/ca-json.43e14418.chunk.js +0 -1
- package/build/content-manager.66cec770.chunk.js +0 -1094
- package/build/content-type-builder-translation-en-json.f592325b.chunk.js +0 -1
- package/build/content-type-builder.e1b6d13b.chunk.js +0 -166
- package/build/en-json.f5fa476a.chunk.js +0 -1
- package/build/es-json.715b6fd8.chunk.js +0 -1
- package/build/fr-json.73494bf5.chunk.js +0 -1
- package/build/main.83edb3fc.js +0 -2926
- package/build/review-workflows-settings.93808ae0.chunk.js +0 -110
- package/build/runtime~main.20c3cac6.js +0 -2
- package/build/sso-settings-page.35b67909.chunk.js +0 -1
- package/build/transfer-tokens-list-page.217573c3.chunk.js +0 -16
- package/build/users-advanced-settings-page.1911adf5.chunk.js +0 -9
- package/build/webhook-edit-page.1ee02c4b.chunk.js +0 -33
- package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +0 -58
- package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/index.js +0 -3
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ProtectedPage.js +0 -20
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js +0 -204
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/index.js +0 -3
- package/ee/server/services/review-workflows/workflows.js +0 -25
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Button, Flex, Loader } from '@strapi/design-system';
|
|
4
|
+
import {
|
|
5
|
+
ConfirmDialog,
|
|
6
|
+
useAPIErrorHandler,
|
|
7
|
+
useFetchClient,
|
|
8
|
+
useNotification,
|
|
9
|
+
} from '@strapi/helper-plugin';
|
|
10
|
+
import { Check } from '@strapi/icons';
|
|
11
|
+
import { useFormik, Form, FormikProvider } from 'formik';
|
|
12
|
+
import { useIntl } from 'react-intl';
|
|
13
|
+
import { useMutation } from 'react-query';
|
|
14
|
+
import { useSelector, useDispatch } from 'react-redux';
|
|
15
|
+
import { useParams } from 'react-router-dom';
|
|
16
|
+
|
|
17
|
+
import { useContentTypes } from '../../../../../../../../admin/src/hooks/useContentTypes';
|
|
18
|
+
import { useInjectReducer } from '../../../../../../../../admin/src/hooks/useInjectReducer';
|
|
19
|
+
import { useLicenseLimits } from '../../../../../../hooks';
|
|
20
|
+
import { setWorkflow } from '../../actions';
|
|
21
|
+
import * as Layout from '../../components/Layout';
|
|
22
|
+
import * as LimitsModal from '../../components/LimitsModal';
|
|
23
|
+
import { Stages } from '../../components/Stages';
|
|
24
|
+
import { WorkflowAttributes } from '../../components/WorkflowAttributes';
|
|
25
|
+
import { REDUX_NAMESPACE } from '../../constants';
|
|
26
|
+
import { useReviewWorkflows } from '../../hooks/useReviewWorkflows';
|
|
27
|
+
import { reducer, initialState } from '../../reducer';
|
|
28
|
+
import { getWorkflowValidationSchema } from '../../utils/getWorkflowValidationSchema';
|
|
29
|
+
|
|
30
|
+
export function ReviewWorkflowsEditView() {
|
|
31
|
+
const { workflowId } = useParams();
|
|
32
|
+
const { formatMessage } = useIntl();
|
|
33
|
+
const dispatch = useDispatch();
|
|
34
|
+
const { put } = useFetchClient();
|
|
35
|
+
const { formatAPIError } = useAPIErrorHandler();
|
|
36
|
+
const toggleNotification = useNotification();
|
|
37
|
+
const {
|
|
38
|
+
isLoading: isWorkflowLoading,
|
|
39
|
+
meta,
|
|
40
|
+
workflows: [workflow],
|
|
41
|
+
status: workflowStatus,
|
|
42
|
+
refetch,
|
|
43
|
+
} = useReviewWorkflows({ id: workflowId });
|
|
44
|
+
const { collectionTypes, singleTypes, isLoading: isLoadingModels } = useContentTypes();
|
|
45
|
+
const {
|
|
46
|
+
status,
|
|
47
|
+
clientState: {
|
|
48
|
+
currentWorkflow: {
|
|
49
|
+
data: currentWorkflow,
|
|
50
|
+
isDirty: currentWorkflowIsDirty,
|
|
51
|
+
hasDeletedServerStages: currentWorkflowHasDeletedServerStages,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
} = useSelector((state) => state?.[REDUX_NAMESPACE] ?? initialState);
|
|
55
|
+
const [isConfirmDeleteDialogOpen, setIsConfirmDeleteDialogOpen] = React.useState(false);
|
|
56
|
+
const { getFeature, isLoading: isLicenseLoading } = useLicenseLimits();
|
|
57
|
+
const [showLimitModal, setShowLimitModal] = React.useState(false);
|
|
58
|
+
|
|
59
|
+
const { mutateAsync, isLoading } = useMutation(
|
|
60
|
+
async ({ workflow }) => {
|
|
61
|
+
const {
|
|
62
|
+
data: { data },
|
|
63
|
+
} = await put(`/admin/review-workflows/workflows/${workflow.id}`, {
|
|
64
|
+
data: workflow,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return data;
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
onSuccess() {
|
|
71
|
+
toggleNotification({
|
|
72
|
+
type: 'success',
|
|
73
|
+
message: { id: 'notification.success.saved', defaultMessage: 'Saved' },
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const updateWorkflow = async (workflow) => {
|
|
80
|
+
try {
|
|
81
|
+
const res = await mutateAsync({ workflow });
|
|
82
|
+
|
|
83
|
+
return res;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
toggleNotification({
|
|
86
|
+
type: 'warning',
|
|
87
|
+
message: formatAPIError(error),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const submitForm = async () => {
|
|
95
|
+
await updateWorkflow(currentWorkflow);
|
|
96
|
+
await refetch();
|
|
97
|
+
|
|
98
|
+
setIsConfirmDeleteDialogOpen(false);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const handleConfirmDeleteDialog = async () => {
|
|
102
|
+
await submitForm();
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const toggleConfirmDeleteDialog = () => {
|
|
106
|
+
setIsConfirmDeleteDialogOpen((prev) => !prev);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const formik = useFormik({
|
|
110
|
+
enableReinitialize: true,
|
|
111
|
+
initialValues: currentWorkflow,
|
|
112
|
+
async onSubmit() {
|
|
113
|
+
if (currentWorkflowHasDeletedServerStages) {
|
|
114
|
+
setIsConfirmDeleteDialogOpen(true);
|
|
115
|
+
} else {
|
|
116
|
+
submitForm();
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
validationSchema: getWorkflowValidationSchema({ formatMessage }),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
useInjectReducer(REDUX_NAMESPACE, reducer);
|
|
123
|
+
|
|
124
|
+
const limits = getFeature('review-workflows');
|
|
125
|
+
|
|
126
|
+
React.useEffect(() => {
|
|
127
|
+
dispatch(setWorkflow({ status: workflowStatus, data: workflow }));
|
|
128
|
+
}, [workflowStatus, workflow, dispatch]);
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* If the current license has a limit:
|
|
132
|
+
* check if the total count of workflows or stages exceeds that limit and display
|
|
133
|
+
* the limits modal on page load. It can be closed by the user, but the
|
|
134
|
+
* API will throw an error in case they try to create a new workflow or update the
|
|
135
|
+
* stages.
|
|
136
|
+
*
|
|
137
|
+
* If the current license does not have a limit (e.g. offline license):
|
|
138
|
+
* do nothing (for now). In case they are trying to create the 201st workflow/ stage
|
|
139
|
+
* the API will throw an error.
|
|
140
|
+
*
|
|
141
|
+
*/
|
|
142
|
+
|
|
143
|
+
React.useEffect(() => {
|
|
144
|
+
if (!isWorkflowLoading && !isLicenseLoading) {
|
|
145
|
+
if (limits?.workflows && meta?.workflowCount >= limits.workflows) {
|
|
146
|
+
setShowLimitModal('workflow');
|
|
147
|
+
} else if (
|
|
148
|
+
limits?.stagesPerWorkflow &&
|
|
149
|
+
currentWorkflow.stages.length >= limits.stagesPerWorkflow
|
|
150
|
+
) {
|
|
151
|
+
setShowLimitModal('stage');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}, [
|
|
155
|
+
currentWorkflow.stages.length,
|
|
156
|
+
isLicenseLoading,
|
|
157
|
+
isWorkflowLoading,
|
|
158
|
+
limits.stagesPerWorkflow,
|
|
159
|
+
limits.workflows,
|
|
160
|
+
meta?.workflowCount,
|
|
161
|
+
meta.workflowsTotal,
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
// TODO: redirect back to list-view if workflow is not found?
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<>
|
|
168
|
+
<Layout.DragLayerRendered />
|
|
169
|
+
|
|
170
|
+
<FormikProvider value={formik}>
|
|
171
|
+
<Form onSubmit={formik.handleSubmit}>
|
|
172
|
+
<Layout.Header
|
|
173
|
+
navigationAction={<Layout.Back href="/settings/review-workflows" />}
|
|
174
|
+
primaryAction={
|
|
175
|
+
<Button
|
|
176
|
+
startIcon={<Check />}
|
|
177
|
+
type="submit"
|
|
178
|
+
size="M"
|
|
179
|
+
disabled={!currentWorkflowIsDirty}
|
|
180
|
+
// if the confirm dialog is open the loading state is on
|
|
181
|
+
// the confirm button already
|
|
182
|
+
loading={!isConfirmDeleteDialogOpen && isLoading}
|
|
183
|
+
>
|
|
184
|
+
{formatMessage({
|
|
185
|
+
id: 'global.save',
|
|
186
|
+
defaultMessage: 'Save',
|
|
187
|
+
})}
|
|
188
|
+
</Button>
|
|
189
|
+
}
|
|
190
|
+
subtitle={formatMessage(
|
|
191
|
+
{
|
|
192
|
+
id: 'Settings.review-workflows.page.subtitle',
|
|
193
|
+
defaultMessage: '{count, plural, one {# stage} other {# stages}}',
|
|
194
|
+
},
|
|
195
|
+
{ count: currentWorkflow?.stages?.length ?? 0 }
|
|
196
|
+
)}
|
|
197
|
+
title={currentWorkflow.name}
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
<Layout.Root>
|
|
201
|
+
{isLoadingModels || status === 'loading' ? (
|
|
202
|
+
<Loader>
|
|
203
|
+
{formatMessage({
|
|
204
|
+
id: 'Settings.review-workflows.page.isLoading',
|
|
205
|
+
defaultMessage: 'Workflow is loading',
|
|
206
|
+
})}
|
|
207
|
+
</Loader>
|
|
208
|
+
) : (
|
|
209
|
+
<Flex alignItems="stretch" direction="column" gap={7}>
|
|
210
|
+
<WorkflowAttributes contentTypes={{ collectionTypes, singleTypes }} />
|
|
211
|
+
<Stages stages={formik.values?.stages} />
|
|
212
|
+
</Flex>
|
|
213
|
+
)}
|
|
214
|
+
</Layout.Root>
|
|
215
|
+
</Form>
|
|
216
|
+
</FormikProvider>
|
|
217
|
+
|
|
218
|
+
<ConfirmDialog
|
|
219
|
+
bodyText={{
|
|
220
|
+
id: 'Settings.review-workflows.page.delete.confirm.body',
|
|
221
|
+
defaultMessage:
|
|
222
|
+
'All entries assigned to deleted stages will be moved to the previous stage. Are you sure you want to save?',
|
|
223
|
+
}}
|
|
224
|
+
isConfirmButtonLoading={isLoading}
|
|
225
|
+
isOpen={isConfirmDeleteDialogOpen}
|
|
226
|
+
onToggleDialog={toggleConfirmDeleteDialog}
|
|
227
|
+
onConfirm={handleConfirmDeleteDialog}
|
|
228
|
+
/>
|
|
229
|
+
|
|
230
|
+
<LimitsModal.Root
|
|
231
|
+
isOpen={showLimitModal === 'workflow'}
|
|
232
|
+
onClose={() => setShowLimitModal(false)}
|
|
233
|
+
>
|
|
234
|
+
<LimitsModal.Title>
|
|
235
|
+
{formatMessage({
|
|
236
|
+
id: 'Settings.review-workflows.edit.page.workflows.limit.title',
|
|
237
|
+
defaultMessage: 'You’ve reached the limit of workflows in your plan',
|
|
238
|
+
})}
|
|
239
|
+
</LimitsModal.Title>
|
|
240
|
+
|
|
241
|
+
<LimitsModal.Body>
|
|
242
|
+
{formatMessage({
|
|
243
|
+
id: 'Settings.review-workflows.edit.page.workflows.limit.body',
|
|
244
|
+
defaultMessage: 'Delete a workflow or contact Sales to enable more workflows.',
|
|
245
|
+
})}
|
|
246
|
+
</LimitsModal.Body>
|
|
247
|
+
</LimitsModal.Root>
|
|
248
|
+
|
|
249
|
+
<LimitsModal.Root
|
|
250
|
+
isOpen={showLimitModal === 'stage'}
|
|
251
|
+
onClose={() => setShowLimitModal(false)}
|
|
252
|
+
>
|
|
253
|
+
<LimitsModal.Title>
|
|
254
|
+
{formatMessage({
|
|
255
|
+
id: 'Settings.review-workflows.edit.page.stages.limit.title',
|
|
256
|
+
defaultMessage: 'You have reached the limit of stages for this workflow in your plan',
|
|
257
|
+
})}
|
|
258
|
+
</LimitsModal.Title>
|
|
259
|
+
|
|
260
|
+
<LimitsModal.Body>
|
|
261
|
+
{formatMessage({
|
|
262
|
+
id: 'Settings.review-workflows.edit.page.stages.limit.body',
|
|
263
|
+
defaultMessage: 'Try deleting some stages or contact Sales to enable more stages.',
|
|
264
|
+
})}
|
|
265
|
+
</LimitsModal.Body>
|
|
266
|
+
</LimitsModal.Root>
|
|
267
|
+
</>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { ProtectedPage } from '../../components/ProtectedPage';
|
|
4
|
+
|
|
5
|
+
import { ReviewWorkflowsEditView } from './EditView';
|
|
6
|
+
|
|
7
|
+
export default function () {
|
|
8
|
+
return (
|
|
9
|
+
<ProtectedPage>
|
|
10
|
+
<ReviewWorkflowsEditView />
|
|
11
|
+
</ProtectedPage>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Flex,
|
|
5
|
+
IconButton,
|
|
6
|
+
Loader,
|
|
7
|
+
Table,
|
|
8
|
+
Thead,
|
|
9
|
+
Tbody,
|
|
10
|
+
Tr,
|
|
11
|
+
Td,
|
|
12
|
+
TFooter,
|
|
13
|
+
Th,
|
|
14
|
+
Typography,
|
|
15
|
+
VisuallyHidden,
|
|
16
|
+
} from '@strapi/design-system';
|
|
17
|
+
import {
|
|
18
|
+
ConfirmDialog,
|
|
19
|
+
Link,
|
|
20
|
+
LinkButton,
|
|
21
|
+
onRowClick,
|
|
22
|
+
pxToRem,
|
|
23
|
+
useAPIErrorHandler,
|
|
24
|
+
useFetchClient,
|
|
25
|
+
useNotification,
|
|
26
|
+
useTracking,
|
|
27
|
+
} from '@strapi/helper-plugin';
|
|
28
|
+
import { Pencil, Plus, Trash } from '@strapi/icons';
|
|
29
|
+
import { useIntl } from 'react-intl';
|
|
30
|
+
import { useMutation } from 'react-query';
|
|
31
|
+
import { useHistory } from 'react-router-dom';
|
|
32
|
+
import styled from 'styled-components';
|
|
33
|
+
|
|
34
|
+
import { useContentTypes } from '../../../../../../../../admin/src/hooks/useContentTypes';
|
|
35
|
+
import { useLicenseLimits } from '../../../../../../hooks';
|
|
36
|
+
import * as Layout from '../../components/Layout';
|
|
37
|
+
import * as LimitsModal from '../../components/LimitsModal';
|
|
38
|
+
import { useReviewWorkflows } from '../../hooks/useReviewWorkflows';
|
|
39
|
+
|
|
40
|
+
const ActionLink = styled(Link)`
|
|
41
|
+
align-items: center;
|
|
42
|
+
height: ${pxToRem(32)};
|
|
43
|
+
display: flex;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
padding: ${({ theme }) => `${theme.spaces[2]}}`};
|
|
46
|
+
width: ${pxToRem(32)};
|
|
47
|
+
|
|
48
|
+
svg {
|
|
49
|
+
height: ${pxToRem(12)};
|
|
50
|
+
width: ${pxToRem(12)};
|
|
51
|
+
|
|
52
|
+
path {
|
|
53
|
+
fill: ${({ theme }) => theme.colors.neutral500};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
&:hover,
|
|
58
|
+
&:focus {
|
|
59
|
+
svg {
|
|
60
|
+
path {
|
|
61
|
+
fill: ${({ theme }) => theme.colors.neutral800};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
export function ReviewWorkflowsListView() {
|
|
68
|
+
const { formatMessage } = useIntl();
|
|
69
|
+
const { push } = useHistory();
|
|
70
|
+
const { collectionTypes, singleTypes, isLoading: isLoadingModels } = useContentTypes();
|
|
71
|
+
const { meta, workflows, isLoading, refetch } = useReviewWorkflows();
|
|
72
|
+
const [workflowToDelete, setWorkflowToDelete] = React.useState(null);
|
|
73
|
+
const [showLimitModal, setShowLimitModal] = React.useState(false);
|
|
74
|
+
const { del } = useFetchClient();
|
|
75
|
+
const { formatAPIError } = useAPIErrorHandler();
|
|
76
|
+
const toggleNotification = useNotification();
|
|
77
|
+
const { getFeature, isLoading: isLicenseLoading } = useLicenseLimits();
|
|
78
|
+
const { trackUsage } = useTracking();
|
|
79
|
+
|
|
80
|
+
const limits = getFeature('review-workflows');
|
|
81
|
+
|
|
82
|
+
const { mutateAsync, isLoading: isLoadingMutation } = useMutation(
|
|
83
|
+
async ({ workflowId, stages }) => {
|
|
84
|
+
const {
|
|
85
|
+
data: { data },
|
|
86
|
+
} = await del(`/admin/review-workflows/workflows/${workflowId}`, {
|
|
87
|
+
data: stages,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return data;
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
onSuccess() {
|
|
94
|
+
toggleNotification({
|
|
95
|
+
type: 'success',
|
|
96
|
+
message: { id: 'notification.success.deleted', defaultMessage: 'Deleted' },
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const getContentTypeDisplayName = (uid) => {
|
|
103
|
+
const contentType = [...collectionTypes, ...singleTypes].find(
|
|
104
|
+
(contentType) => contentType.uid === uid
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return contentType.info.displayName;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleDeleteWorkflow = (workflowId) => {
|
|
111
|
+
setWorkflowToDelete(workflowId);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const toggleConfirmDeleteDialog = () => {
|
|
115
|
+
setWorkflowToDelete(null);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleConfirmDeleteDialog = async () => {
|
|
119
|
+
try {
|
|
120
|
+
const res = await mutateAsync({ workflowId: workflowToDelete });
|
|
121
|
+
|
|
122
|
+
await refetch();
|
|
123
|
+
setWorkflowToDelete(null);
|
|
124
|
+
|
|
125
|
+
return res;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
toggleNotification({
|
|
128
|
+
type: 'warning',
|
|
129
|
+
message: formatAPIError(error),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* If the current license has a limit:
|
|
138
|
+
* check if the total count of workflows or stages exceeds that limit and display
|
|
139
|
+
* the limits modal on page load. It can be closed by the user, but the
|
|
140
|
+
* API will throw an error in case they try to create a new workflow or update the
|
|
141
|
+
* stages.
|
|
142
|
+
*
|
|
143
|
+
* If the current license does not have a limit (e.g. offline license):
|
|
144
|
+
* do nothing (for now). In case they are trying to create the 201st workflow/ stage
|
|
145
|
+
* the API will throw an error.
|
|
146
|
+
*
|
|
147
|
+
*/
|
|
148
|
+
|
|
149
|
+
React.useEffect(() => {
|
|
150
|
+
if (!isLoading && !isLicenseLoading) {
|
|
151
|
+
if (limits?.workflows && meta?.workflowCount >= limits.workflows) {
|
|
152
|
+
setShowLimitModal(true);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}, [
|
|
156
|
+
isLicenseLoading,
|
|
157
|
+
isLoading,
|
|
158
|
+
limits.stagesPerWorkflow,
|
|
159
|
+
limits.workflows,
|
|
160
|
+
meta?.workflowCount,
|
|
161
|
+
meta.workflowsTotal,
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<>
|
|
166
|
+
<Layout.Header
|
|
167
|
+
primaryAction={
|
|
168
|
+
<LinkButton
|
|
169
|
+
startIcon={<Plus />}
|
|
170
|
+
size="S"
|
|
171
|
+
to="/settings/review-workflows/create"
|
|
172
|
+
onClick={(event) => {
|
|
173
|
+
/**
|
|
174
|
+
* If the current license has a workflow limit:
|
|
175
|
+
* check if the total count of workflows exceeds that limit. If so,
|
|
176
|
+
* prevent the navigation and show the limits overlay.
|
|
177
|
+
*
|
|
178
|
+
* If the current license does not have a limit (e.g. offline license):
|
|
179
|
+
* allow the user to navigate to the create-view. In case they exceed the
|
|
180
|
+
* current hard-limit of 200 they will see an error thrown by the API.
|
|
181
|
+
*/
|
|
182
|
+
|
|
183
|
+
if (limits?.workflows && meta?.workflowCount >= limits.workflows) {
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
setShowLimitModal(true);
|
|
186
|
+
} else {
|
|
187
|
+
trackUsage('willCreateWorkflow');
|
|
188
|
+
}
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
{formatMessage({
|
|
192
|
+
id: 'Settings.review-workflows.list.page.create',
|
|
193
|
+
defaultMessage: 'Create new workflow',
|
|
194
|
+
})}
|
|
195
|
+
</LinkButton>
|
|
196
|
+
}
|
|
197
|
+
subtitle={formatMessage({
|
|
198
|
+
id: 'Settings.review-workflows.list.page.subtitle',
|
|
199
|
+
defaultMessage:
|
|
200
|
+
'Manage content review stages and collaborate during content creation from draft to publication',
|
|
201
|
+
})}
|
|
202
|
+
title={formatMessage({
|
|
203
|
+
id: 'Settings.review-workflows.list.page.title',
|
|
204
|
+
defaultMessage: 'Review Workflows',
|
|
205
|
+
})}
|
|
206
|
+
/>
|
|
207
|
+
|
|
208
|
+
<Layout.Root>
|
|
209
|
+
{isLoading || isLoadingModels ? (
|
|
210
|
+
<Loader>
|
|
211
|
+
{formatMessage({
|
|
212
|
+
id: 'Settings.review-workflows.page.list.isLoading',
|
|
213
|
+
defaultMessage: 'Workflows are loading',
|
|
214
|
+
})}
|
|
215
|
+
</Loader>
|
|
216
|
+
) : (
|
|
217
|
+
<Table
|
|
218
|
+
colCount={3}
|
|
219
|
+
footer={
|
|
220
|
+
// TODO: we should be able to use a link here instead of an (inaccessible onClick) handler
|
|
221
|
+
<TFooter
|
|
222
|
+
icon={<Plus />}
|
|
223
|
+
onClick={() => {
|
|
224
|
+
/**
|
|
225
|
+
* If the current license has a workflow limit:
|
|
226
|
+
* check if the total count of workflows exceeds that limit
|
|
227
|
+
*
|
|
228
|
+
* If the current license does not have a limit (e.g. offline license):
|
|
229
|
+
* allow the user to navigate to the create-view. In case they exceed the
|
|
230
|
+
* current hard-limit of 200 they will see an error thrown by the API.
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
if (limits?.workflows && meta?.workflowCount >= limits.workflows) {
|
|
234
|
+
setShowLimitModal(true);
|
|
235
|
+
} else {
|
|
236
|
+
push('/settings/review-workflows/create');
|
|
237
|
+
trackUsage('willCreateWorkflow');
|
|
238
|
+
}
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
{formatMessage({
|
|
242
|
+
id: 'Settings.review-workflows.list.page.create',
|
|
243
|
+
defaultMessage: 'Create new workflow',
|
|
244
|
+
})}
|
|
245
|
+
</TFooter>
|
|
246
|
+
}
|
|
247
|
+
rowCount={1}
|
|
248
|
+
>
|
|
249
|
+
<Thead>
|
|
250
|
+
<Tr>
|
|
251
|
+
<Th>
|
|
252
|
+
<Typography variant="sigma">
|
|
253
|
+
{formatMessage({
|
|
254
|
+
id: 'Settings.review-workflows.list.page.list.column.name.title',
|
|
255
|
+
defaultMessage: 'Name',
|
|
256
|
+
})}
|
|
257
|
+
</Typography>
|
|
258
|
+
</Th>
|
|
259
|
+
<Th>
|
|
260
|
+
<Typography variant="sigma">
|
|
261
|
+
{formatMessage({
|
|
262
|
+
id: 'Settings.review-workflows.list.page.list.column.stages.title',
|
|
263
|
+
defaultMessage: 'Stages',
|
|
264
|
+
})}
|
|
265
|
+
</Typography>
|
|
266
|
+
</Th>
|
|
267
|
+
<Th>
|
|
268
|
+
<Typography variant="sigma">
|
|
269
|
+
{formatMessage({
|
|
270
|
+
id: 'Settings.review-workflows.list.page.list.column.contentTypes.title',
|
|
271
|
+
defaultMessage: 'Content Types',
|
|
272
|
+
})}
|
|
273
|
+
</Typography>
|
|
274
|
+
</Th>
|
|
275
|
+
<Th>
|
|
276
|
+
<VisuallyHidden>
|
|
277
|
+
{formatMessage({
|
|
278
|
+
id: 'Settings.review-workflows.list.page.list.column.actions.title',
|
|
279
|
+
defaultMessage: 'Actions',
|
|
280
|
+
})}
|
|
281
|
+
</VisuallyHidden>
|
|
282
|
+
</Th>
|
|
283
|
+
</Tr>
|
|
284
|
+
</Thead>
|
|
285
|
+
|
|
286
|
+
<Tbody>
|
|
287
|
+
{workflows.map((workflow) => (
|
|
288
|
+
<Tr
|
|
289
|
+
{...onRowClick({
|
|
290
|
+
fn(event) {
|
|
291
|
+
// Abort row onClick event when the user click on the delete button
|
|
292
|
+
if (event.target.nodeName === 'BUTTON') {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
push(`/settings/review-workflows/${workflow.id}`);
|
|
297
|
+
},
|
|
298
|
+
})}
|
|
299
|
+
key={`workflow-${workflow.id}`}
|
|
300
|
+
>
|
|
301
|
+
<Td width={pxToRem(250)}>
|
|
302
|
+
<Typography textColor="neutral800" fontWeight="bold" ellipsis>
|
|
303
|
+
{workflow.name}
|
|
304
|
+
</Typography>
|
|
305
|
+
</Td>
|
|
306
|
+
<Td>
|
|
307
|
+
<Typography textColor="neutral800">{workflow.stages.length}</Typography>
|
|
308
|
+
</Td>
|
|
309
|
+
<Td>
|
|
310
|
+
<Typography textColor="neutral800">
|
|
311
|
+
{(workflow?.contentTypes ?? []).map(getContentTypeDisplayName).join(', ')}
|
|
312
|
+
</Typography>
|
|
313
|
+
</Td>
|
|
314
|
+
<Td>
|
|
315
|
+
<Flex alignItems="center" justifyContent="end">
|
|
316
|
+
<ActionLink
|
|
317
|
+
to={`/settings/review-workflows/${workflow.id}`}
|
|
318
|
+
aria-label={formatMessage(
|
|
319
|
+
{
|
|
320
|
+
id: 'Settings.review-workflows.list.page.list.column.actions.edit.label',
|
|
321
|
+
defaultMessage: 'Edit {name}',
|
|
322
|
+
},
|
|
323
|
+
{ name: workflow.name }
|
|
324
|
+
)}
|
|
325
|
+
>
|
|
326
|
+
<Pencil />
|
|
327
|
+
</ActionLink>
|
|
328
|
+
|
|
329
|
+
<IconButton
|
|
330
|
+
aria-label={formatMessage(
|
|
331
|
+
{
|
|
332
|
+
id: 'Settings.review-workflows.list.page.list.column.actions.delete.label',
|
|
333
|
+
defaultMessage: 'Delete {name}',
|
|
334
|
+
},
|
|
335
|
+
{ name: 'Default workflow' }
|
|
336
|
+
)}
|
|
337
|
+
disabled={workflows.length === 1}
|
|
338
|
+
icon={<Trash />}
|
|
339
|
+
noBorder
|
|
340
|
+
onClick={() => {
|
|
341
|
+
handleDeleteWorkflow(workflow.id);
|
|
342
|
+
}}
|
|
343
|
+
/>
|
|
344
|
+
</Flex>
|
|
345
|
+
</Td>
|
|
346
|
+
</Tr>
|
|
347
|
+
))}
|
|
348
|
+
</Tbody>
|
|
349
|
+
</Table>
|
|
350
|
+
)}
|
|
351
|
+
|
|
352
|
+
<ConfirmDialog
|
|
353
|
+
bodyText={{
|
|
354
|
+
id: 'Settings.review-workflows.list.page.delete.confirm.body',
|
|
355
|
+
defaultMessage:
|
|
356
|
+
'If you remove this worfklow, all stage-related information will be removed for this content-type. Are you sure you want to remove it?',
|
|
357
|
+
}}
|
|
358
|
+
isConfirmButtonLoading={isLoadingMutation}
|
|
359
|
+
isOpen={!!workflowToDelete}
|
|
360
|
+
onToggleDialog={toggleConfirmDeleteDialog}
|
|
361
|
+
onConfirm={handleConfirmDeleteDialog}
|
|
362
|
+
/>
|
|
363
|
+
|
|
364
|
+
<LimitsModal.Root isOpen={showLimitModal} onClose={() => setShowLimitModal(false)}>
|
|
365
|
+
<LimitsModal.Title>
|
|
366
|
+
{formatMessage({
|
|
367
|
+
id: 'Settings.review-workflows.list.page.workflows.limit.title',
|
|
368
|
+
defaultMessage: 'You’ve reached the limit of workflows in your plan',
|
|
369
|
+
})}
|
|
370
|
+
</LimitsModal.Title>
|
|
371
|
+
|
|
372
|
+
<LimitsModal.Body>
|
|
373
|
+
{formatMessage({
|
|
374
|
+
id: 'Settings.review-workflows.list.page.workflows.limit.body',
|
|
375
|
+
defaultMessage: 'Delete a workflow or contact Sales to enable more workflows.',
|
|
376
|
+
})}
|
|
377
|
+
</LimitsModal.Body>
|
|
378
|
+
</LimitsModal.Root>
|
|
379
|
+
</Layout.Root>
|
|
380
|
+
</>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { ProtectedPage } from '../../components/ProtectedPage';
|
|
4
|
+
|
|
5
|
+
import { ReviewWorkflowsListView } from './ListView';
|
|
6
|
+
|
|
7
|
+
export default function () {
|
|
8
|
+
return (
|
|
9
|
+
<ProtectedPage>
|
|
10
|
+
<ReviewWorkflowsListView />
|
|
11
|
+
</ProtectedPage>
|
|
12
|
+
);
|
|
13
|
+
}
|