@strapi/admin 4.11.4 → 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/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/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 -59
- 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/TokenBox/index.js +1 -1
- package/admin/src/pages/SettingsPage/pages/Users/EditPage/index.js +1 -1
- 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/{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/9944.29289a16.chunk.js +26 -0
- package/build/{Admin-authenticatedApp.cb649fc1.chunk.js → Admin-authenticatedApp.9d3afb79.chunk.js} +2 -2
- 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-users.78552758.chunk.js +10 -0
- package/build/admin-users.c23322fc.chunk.js +11 -0
- package/build/audit-logs-settings-page.37fe915c.chunk.js +1 -0
- package/build/content-manager.08541eeb.chunk.js +1094 -0
- 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/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/{users-roles-settings-page.1f505119.chunk.js → users-roles-settings-page.d286426a.chunk.js} +1 -1
- 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/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/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 +8 -13
- 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 +10 -11
- package/admin/src/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +0 -2
- 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.44d2e264.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-app.fea867af.chunk.js +0 -61
- package/build/admin-edit-users.200551e3.chunk.js +0 -10
- package/build/admin-users.3b12dca2.chunk.js +0 -11
- package/build/audit-logs-settings-page.f538490f.chunk.js +0 -1
- package/build/content-manager.c40f5ff9.chunk.js +0 -1088
- package/build/content-type-builder-translation-en-json.f592325b.chunk.js +0 -1
- package/build/content-type-builder.bd1bbff1.chunk.js +0 -166
- package/build/main.ee36abd9.js +0 -2927
- package/build/review-workflows-settings.93808ae0.chunk.js +0 -110
- package/build/runtime~main.efd966f6.js +0 -2
- 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
|
@@ -2,17 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
4
|
mapAsync,
|
|
5
|
-
errors: { ApplicationError },
|
|
5
|
+
errors: { ApplicationError, ValidationError },
|
|
6
6
|
} = require('@strapi/utils');
|
|
7
7
|
const { map } = require('lodash/fp');
|
|
8
8
|
|
|
9
|
-
const { STAGE_MODEL_UID, ENTITY_STAGE_ATTRIBUTE } = require('../../constants/workflows');
|
|
9
|
+
const { STAGE_MODEL_UID, ENTITY_STAGE_ATTRIBUTE, ERRORS } = require('../../constants/workflows');
|
|
10
10
|
const { getService } = require('../../utils');
|
|
11
|
-
const { getContentTypeUIDsWithActivatedReviewWorkflows } = require('../../utils/review-workflows');
|
|
12
11
|
|
|
13
12
|
module.exports = ({ strapi }) => {
|
|
14
|
-
const workflowsService = getService('workflows', { strapi });
|
|
15
13
|
const metrics = getService('review-workflows-metrics', { strapi });
|
|
14
|
+
const workflowsValidationService = getService('review-workflows-validation', { strapi });
|
|
16
15
|
|
|
17
16
|
return {
|
|
18
17
|
find({ workflowId, populate }) {
|
|
@@ -30,8 +29,8 @@ module.exports = ({ strapi }) => {
|
|
|
30
29
|
return strapi.entityService.findOne(STAGE_MODEL_UID, id, params);
|
|
31
30
|
},
|
|
32
31
|
|
|
33
|
-
async createMany(stagesList, { fields }) {
|
|
34
|
-
const params = { select: fields };
|
|
32
|
+
async createMany(stagesList, { fields } = {}) {
|
|
33
|
+
const params = { select: fields ?? '*' };
|
|
35
34
|
|
|
36
35
|
const stages = await Promise.all(
|
|
37
36
|
stagesList.map((stage) =>
|
|
@@ -62,24 +61,34 @@ module.exports = ({ strapi }) => {
|
|
|
62
61
|
return stage;
|
|
63
62
|
},
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
return strapi.entityService.
|
|
64
|
+
async deleteMany(stagesId) {
|
|
65
|
+
return strapi.entityService.deleteMany(STAGE_MODEL_UID, {
|
|
66
|
+
filters: { id: { $in: stagesId } },
|
|
67
|
+
});
|
|
67
68
|
},
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
const
|
|
70
|
+
count({ workflowId } = {}) {
|
|
71
|
+
const opts = {};
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
if (workflowId) {
|
|
74
|
+
opts.where = {
|
|
75
|
+
workflow: workflowId,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return strapi.entityService.count(STAGE_MODEL_UID, opts);
|
|
79
|
+
},
|
|
73
80
|
|
|
74
|
-
|
|
81
|
+
async replaceStages(srcStages, destStages, contentTypesToMigrate = []) {
|
|
82
|
+
const { created, updated, deleted } = getDiffBetweenStages(srcStages, destStages);
|
|
75
83
|
|
|
84
|
+
assertAtLeastOneStageRemain(srcStages || [], { created, deleted });
|
|
85
|
+
|
|
86
|
+
// Update stages and assign entity stages
|
|
76
87
|
return strapi.db.transaction(async ({ trx }) => {
|
|
77
88
|
// Create the new stages
|
|
78
89
|
const createdStages = await this.createMany(created, { fields: ['id'] });
|
|
79
90
|
// Put all the newly created stages ids
|
|
80
91
|
const createdStagesIds = map('id', createdStages);
|
|
81
|
-
const stagesIds = stages.map((stage) => stage.id ?? createdStagesIds.shift());
|
|
82
|
-
const contentTypes = getContentTypeUIDsWithActivatedReviewWorkflows(strapi.contentTypes);
|
|
83
92
|
|
|
84
93
|
// Update the workflow stages
|
|
85
94
|
await mapAsync(updated, (stage) => this.update(stage.id, stage));
|
|
@@ -89,15 +98,15 @@ module.exports = ({ strapi }) => {
|
|
|
89
98
|
// Find the nearest stage in the workflow and newly created stages
|
|
90
99
|
// that is not deleted, prioritizing the previous stages
|
|
91
100
|
const nearestStage = findNearestMatchingStage(
|
|
92
|
-
[...
|
|
93
|
-
|
|
101
|
+
[...srcStages, ...createdStages],
|
|
102
|
+
srcStages.findIndex((s) => s.id === stage.id),
|
|
94
103
|
(targetStage) => {
|
|
95
104
|
return !deleted.find((s) => s.id === targetStage.id);
|
|
96
105
|
}
|
|
97
106
|
);
|
|
98
107
|
|
|
99
108
|
// Assign the new stage to entities that had the deleted stage
|
|
100
|
-
await mapAsync(
|
|
109
|
+
await mapAsync(contentTypesToMigrate, (contentTypeUID) => {
|
|
101
110
|
this.updateEntitiesStage(contentTypeUID, {
|
|
102
111
|
fromStageId: stage.id,
|
|
103
112
|
toStageId: nearestStage.id,
|
|
@@ -108,9 +117,7 @@ module.exports = ({ strapi }) => {
|
|
|
108
117
|
return this.delete(stage.id);
|
|
109
118
|
});
|
|
110
119
|
|
|
111
|
-
return
|
|
112
|
-
stages: stagesIds,
|
|
113
|
-
});
|
|
120
|
+
return destStages.map((stage) => ({ ...stage, id: stage.id ?? createdStagesIds.shift() }));
|
|
114
121
|
});
|
|
115
122
|
},
|
|
116
123
|
|
|
@@ -125,6 +132,8 @@ module.exports = ({ strapi }) => {
|
|
|
125
132
|
async updateEntity(entityInfo, stageId) {
|
|
126
133
|
const stage = await this.findById(stageId);
|
|
127
134
|
|
|
135
|
+
await workflowsValidationService.validateWorkflowCount();
|
|
136
|
+
|
|
128
137
|
if (!stage) {
|
|
129
138
|
throw new ApplicationError(`Selected stage does not exist`);
|
|
130
139
|
}
|
|
@@ -140,43 +149,72 @@ module.exports = ({ strapi }) => {
|
|
|
140
149
|
},
|
|
141
150
|
|
|
142
151
|
/**
|
|
143
|
-
* Updates
|
|
152
|
+
* Updates entity stages of a content type:
|
|
153
|
+
* - If fromStageId is undefined, all entities with an existing stage will be assigned the new stage
|
|
154
|
+
* - If fromStageId is null, all entities without a stage will be assigned the new stage
|
|
155
|
+
* - If fromStageId is a number, all entities with that stage will be assigned the new stage
|
|
156
|
+
*
|
|
157
|
+
* For performance reasons we use knex queries directly.
|
|
158
|
+
*
|
|
144
159
|
* @param {string} contentTypeUID
|
|
145
|
-
* @param {number} fromStageId
|
|
160
|
+
* @param {number | undefined | null} fromStageId
|
|
146
161
|
* @param {number} toStageId
|
|
147
162
|
* @param {import('knex').Knex.Transaction} trx
|
|
148
163
|
* @returns
|
|
149
164
|
*/
|
|
150
|
-
async updateEntitiesStage(contentTypeUID, { fromStageId, toStageId
|
|
165
|
+
async updateEntitiesStage(contentTypeUID, { fromStageId, toStageId }) {
|
|
151
166
|
const { attributes, tableName } = strapi.db.metadata.get(contentTypeUID);
|
|
152
167
|
const joinTable = attributes[ENTITY_STAGE_ATTRIBUTE].joinTable;
|
|
153
168
|
const joinColumn = joinTable.joinColumn.name;
|
|
154
169
|
const invJoinColumn = joinTable.inverseJoinColumn.name;
|
|
155
170
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
await workflowsValidationService.validateWorkflowCount();
|
|
172
|
+
|
|
173
|
+
return strapi.db.transaction(async ({ trx }) => {
|
|
174
|
+
// Update all already existing links to the new stage
|
|
175
|
+
if (fromStageId === undefined) {
|
|
176
|
+
return strapi.db
|
|
177
|
+
.getConnection()
|
|
178
|
+
.from(joinTable.name)
|
|
179
|
+
.update({ [invJoinColumn]: toStageId })
|
|
180
|
+
.transacting(trx);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Update all links from the specified stage to the new stage
|
|
184
|
+
const selectStatement = strapi.db
|
|
185
|
+
.getConnection()
|
|
186
|
+
.select({ [joinColumn]: 't1.id', [invJoinColumn]: toStageId })
|
|
187
|
+
.from(`${tableName} as t1`)
|
|
188
|
+
.leftJoin(`${joinTable.name} as t2`, `t1.id`, `t2.${joinColumn}`)
|
|
189
|
+
.where(`t2.${invJoinColumn}`, fromStageId)
|
|
190
|
+
.toSQL();
|
|
191
|
+
|
|
192
|
+
// Insert rows for all entries of the content type that have the specified stage
|
|
193
|
+
return strapi.db
|
|
194
|
+
.getConnection(joinTable.name)
|
|
195
|
+
.insert(
|
|
196
|
+
strapi.db.connection.raw(
|
|
197
|
+
`(${joinColumn}, ${invJoinColumn}) ${selectStatement.sql}`,
|
|
198
|
+
selectStatement.bindings
|
|
199
|
+
)
|
|
172
200
|
)
|
|
173
|
-
|
|
201
|
+
.transacting(trx);
|
|
202
|
+
});
|
|
203
|
+
},
|
|
174
204
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Deletes all entity stages of a content type
|
|
207
|
+
* @param {string} contentTypeUID
|
|
208
|
+
* @returns
|
|
209
|
+
*/
|
|
210
|
+
async deleteAllEntitiesStage(contentTypeUID) {
|
|
211
|
+
const { attributes } = strapi.db.metadata.get(contentTypeUID);
|
|
212
|
+
const joinTable = attributes[ENTITY_STAGE_ATTRIBUTE].joinTable;
|
|
178
213
|
|
|
179
|
-
|
|
214
|
+
// Delete all stage links for the content type
|
|
215
|
+
return strapi.db.transaction(async ({ trx }) =>
|
|
216
|
+
strapi.db.getConnection().from(joinTable.name).delete().transacting(trx)
|
|
217
|
+
);
|
|
180
218
|
},
|
|
181
219
|
};
|
|
182
220
|
};
|
|
@@ -231,13 +269,13 @@ function getDiffBetweenStages(sourceStages, comparisonStages) {
|
|
|
231
269
|
* @param {Array} diffStages.deleted - An array of stages that are planned to be deleted from the workflow.
|
|
232
270
|
* @param {Array} diffStages.created - An array of stages that are planned to be created in the workflow.
|
|
233
271
|
*
|
|
234
|
-
* @throws {
|
|
272
|
+
* @throws {ValidationError} If the number of remaining stages in the workflow after applying deletions and additions is less than 1.
|
|
235
273
|
*/
|
|
236
274
|
function assertAtLeastOneStageRemain(workflowStages, diffStages) {
|
|
237
275
|
const remainingStagesCount =
|
|
238
276
|
workflowStages.length - diffStages.deleted.length + diffStages.created.length;
|
|
239
277
|
if (remainingStagesCount < 1) {
|
|
240
|
-
throw new
|
|
278
|
+
throw new ValidationError(ERRORS.WORKFLOW_WITHOUT_STAGES);
|
|
241
279
|
}
|
|
242
280
|
}
|
|
243
281
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { ValidationError } = require('@strapi/utils').errors;
|
|
4
|
+
const { getService } = require('../../utils');
|
|
5
|
+
const { ERRORS, MAX_WORKFLOWS, MAX_STAGES_PER_WORKFLOW } = require('../../constants/workflows');
|
|
6
|
+
const { clampMaxWorkflows, clampMaxStagesPerWorkflow } = require('../../utils/review-workflows');
|
|
7
|
+
|
|
8
|
+
module.exports = ({ strapi }) => {
|
|
9
|
+
return {
|
|
10
|
+
limits: {
|
|
11
|
+
workflows: MAX_WORKFLOWS,
|
|
12
|
+
stagesPerWorkflow: MAX_STAGES_PER_WORKFLOW,
|
|
13
|
+
},
|
|
14
|
+
register({ workflows, stagesPerWorkflow }) {
|
|
15
|
+
if (!Object.isFrozen(this.limits)) {
|
|
16
|
+
this.limits.workflows = clampMaxWorkflows(workflows || this.limits.workflows);
|
|
17
|
+
this.limits.stagesPerWorkflow = clampMaxStagesPerWorkflow(
|
|
18
|
+
stagesPerWorkflow || this.limits.stagesPerWorkflow
|
|
19
|
+
);
|
|
20
|
+
Object.freeze(this.limits);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
/**
|
|
24
|
+
* Validates the stages of a workflow.
|
|
25
|
+
* @param {Array} stages - Array of stages to be validated.
|
|
26
|
+
* @throws {ValidationError} - If the workflow has no stages or exceeds the limit.
|
|
27
|
+
*/
|
|
28
|
+
validateWorkflowStages(stages) {
|
|
29
|
+
if (!stages || stages.length === 0) {
|
|
30
|
+
throw new ValidationError(ERRORS.WORKFLOW_WITHOUT_STAGES);
|
|
31
|
+
}
|
|
32
|
+
if (stages.length > this.limits.stagesPerWorkflow) {
|
|
33
|
+
throw new ValidationError(ERRORS.STAGES_LIMIT);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async validateWorkflowCountStages(workflowId, countAddedStages = 0) {
|
|
38
|
+
const stagesService = getService('stages', { strapi });
|
|
39
|
+
const countWorkflowStages = await stagesService.count({ workflowId });
|
|
40
|
+
|
|
41
|
+
if (countWorkflowStages + countAddedStages > this.limits.stagesPerWorkflow) {
|
|
42
|
+
throw new ValidationError(ERRORS.STAGES_LIMIT);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validates the count of existing and added workflows.
|
|
48
|
+
* @param {number} [countAddedWorkflows=0] - The count of workflows to be added.
|
|
49
|
+
* @throws {ValidationError} - If the total count of workflows exceeds the limit.
|
|
50
|
+
* @returns {Promise<void>} - A Promise that resolves when the validation is completed.
|
|
51
|
+
*/
|
|
52
|
+
async validateWorkflowCount(countAddedWorkflows = 0) {
|
|
53
|
+
const workflowsService = getService('workflows', { strapi });
|
|
54
|
+
const countWorkflows = await workflowsService.count();
|
|
55
|
+
if (countWorkflows + countAddedWorkflows > this.limits.workflows) {
|
|
56
|
+
throw new ValidationError(ERRORS.WORKFLOWS_LIMIT);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { mapAsync } = require('@strapi/utils');
|
|
4
|
+
const { difference, merge } = require('lodash/fp');
|
|
5
|
+
const { getService } = require('../../../utils');
|
|
6
|
+
const { WORKFLOW_MODEL_UID } = require('../../../constants/workflows');
|
|
7
|
+
|
|
8
|
+
module.exports = ({ strapi }) => {
|
|
9
|
+
const contentManagerContentTypeService = strapi
|
|
10
|
+
.plugin('content-manager')
|
|
11
|
+
.service('content-types');
|
|
12
|
+
const stagesService = getService('stages', { strapi });
|
|
13
|
+
|
|
14
|
+
const updateContentTypeConfig = async (uid, reviewWorkflowOption) => {
|
|
15
|
+
// Merge options in the configuration as the configuration service use a destructuration merge which doesn't include nested objects
|
|
16
|
+
const modelConfig = await contentManagerContentTypeService.findConfiguration(uid);
|
|
17
|
+
|
|
18
|
+
await contentManagerContentTypeService.updateConfiguration(
|
|
19
|
+
{ uid },
|
|
20
|
+
{ options: merge(modelConfig.options, { reviewWorkflows: reviewWorkflowOption }) }
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
/**
|
|
26
|
+
* Migrates entities stages. Used when a content type is assigned to a workflow.
|
|
27
|
+
* @param {*} options
|
|
28
|
+
* @param {Array<string>} options.srcContentTypes - The content types assigned to the previous workflow
|
|
29
|
+
* @param {Array<string>} options.destContentTypes - The content types assigned to the new workflow
|
|
30
|
+
* @param {Workflow.Stage} options.stageId - The new stage to assign the entities to
|
|
31
|
+
*/
|
|
32
|
+
async migrate({ srcContentTypes = [], destContentTypes, stageId }) {
|
|
33
|
+
// Workflows service is using this content-types service, to avoid an infinite loop, we need to get the service in the method
|
|
34
|
+
const workflowsService = getService('workflows', { strapi });
|
|
35
|
+
const { created, deleted } = diffContentTypes(srcContentTypes, destContentTypes);
|
|
36
|
+
|
|
37
|
+
await mapAsync(created, async (uid) => {
|
|
38
|
+
// If it was assigned to another workflow, transfer it from the previous workflow
|
|
39
|
+
const srcWorkflow = await workflowsService.getAssignedWorkflow(uid);
|
|
40
|
+
if (srcWorkflow) {
|
|
41
|
+
// Updates all existing entities stages links to the new stage
|
|
42
|
+
await stagesService.updateEntitiesStage(uid, { toStageId: stageId });
|
|
43
|
+
return this.transferContentType(srcWorkflow, uid);
|
|
44
|
+
}
|
|
45
|
+
await updateContentTypeConfig(uid, true);
|
|
46
|
+
|
|
47
|
+
// Create new stages links to the new stage
|
|
48
|
+
return stagesService.updateEntitiesStage(uid, {
|
|
49
|
+
fromStageId: null,
|
|
50
|
+
toStageId: stageId,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await mapAsync(deleted, async (uid) => {
|
|
55
|
+
await updateContentTypeConfig(uid, false);
|
|
56
|
+
await stagesService.deleteAllEntitiesStage(uid, {});
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Filters the content types assigned to the previous workflow.
|
|
62
|
+
* @param {Workflow} srcWorkflow - The workflow to transfer from
|
|
63
|
+
* @param {string} uid - The content type uid
|
|
64
|
+
*/
|
|
65
|
+
async transferContentType(srcWorkflow, uid) {
|
|
66
|
+
// Update assignedContentTypes of the previous workflow
|
|
67
|
+
await strapi.entityService.update(WORKFLOW_MODEL_UID, srcWorkflow.id, {
|
|
68
|
+
data: {
|
|
69
|
+
contentTypes: srcWorkflow.contentTypes.filter((ct) => ct !== uid),
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const diffContentTypes = (srcContentTypes, destContentTypes) => {
|
|
77
|
+
const created = difference(destContentTypes, srcContentTypes);
|
|
78
|
+
const deleted = difference(srcContentTypes, destContentTypes);
|
|
79
|
+
return { created, deleted };
|
|
80
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { set, isString, map, get } = require('lodash/fp');
|
|
4
|
+
const { ApplicationError } = require('@strapi/utils').errors;
|
|
5
|
+
const { WORKFLOW_MODEL_UID } = require('../../../constants/workflows');
|
|
6
|
+
const { getService } = require('../../../utils');
|
|
7
|
+
const { getWorkflowContentTypeFilter } = require('../../../utils/review-workflows');
|
|
8
|
+
const workflowsContentTypesFactory = require('./content-types');
|
|
9
|
+
|
|
10
|
+
const processFilters = ({ strapi }, filters = {}) => {
|
|
11
|
+
const processedFilters = { ...filters };
|
|
12
|
+
|
|
13
|
+
if (isString(filters.contentTypes)) {
|
|
14
|
+
processedFilters.contentTypes = getWorkflowContentTypeFilter({ strapi }, filters.contentTypes);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return processedFilters;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
module.exports = ({ strapi }) => {
|
|
21
|
+
const workflowsContentTypes = workflowsContentTypesFactory({ strapi });
|
|
22
|
+
const workflowsValidationService = getService('review-workflows-validation', { strapi });
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
/**
|
|
26
|
+
* Returns all the workflows matching the user-defined filters.
|
|
27
|
+
* @param {object} opts - Options for the query.
|
|
28
|
+
* @param {object} opts.filters - Filters object.
|
|
29
|
+
* @returns {Promise<object[]>} - List of workflows that match the user's filters.
|
|
30
|
+
*/
|
|
31
|
+
async find(opts = {}) {
|
|
32
|
+
const filters = processFilters({ strapi }, opts.filters);
|
|
33
|
+
return strapi.entityService.findMany(WORKFLOW_MODEL_UID, { ...opts, filters });
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns the workflow with the specified ID.
|
|
38
|
+
* @param {string} id - ID of the requested workflow.
|
|
39
|
+
* @param {object} opts - Options for the query.
|
|
40
|
+
* @returns {Promise<object>} - Workflow object matching the requested ID.
|
|
41
|
+
*/
|
|
42
|
+
findById(id, opts) {
|
|
43
|
+
return strapi.entityService.findOne(WORKFLOW_MODEL_UID, id, opts);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a new workflow.
|
|
48
|
+
* @param {object} opts - Options for creating the new workflow.
|
|
49
|
+
* @returns {Promise<object>} - Workflow object that was just created.
|
|
50
|
+
* @throws {ValidationError} - If the workflow has no stages.
|
|
51
|
+
*/
|
|
52
|
+
async create(opts) {
|
|
53
|
+
let createOpts = { ...opts, populate: { stages: true } };
|
|
54
|
+
|
|
55
|
+
workflowsValidationService.validateWorkflowStages(opts.data.stages);
|
|
56
|
+
await workflowsValidationService.validateWorkflowCount(1);
|
|
57
|
+
|
|
58
|
+
return strapi.db.transaction(async () => {
|
|
59
|
+
// Create stages
|
|
60
|
+
const stages = await getService('stages', { strapi }).createMany(opts.data.stages);
|
|
61
|
+
const mapIds = map(get('id'));
|
|
62
|
+
|
|
63
|
+
createOpts = set('data.stages', mapIds(stages), createOpts);
|
|
64
|
+
|
|
65
|
+
// Update (un)assigned Content Types
|
|
66
|
+
if (opts.data.contentTypes) {
|
|
67
|
+
await workflowsContentTypes.migrate({
|
|
68
|
+
destContentTypes: opts.data.contentTypes,
|
|
69
|
+
stageId: stages[0].id,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Create Workflow
|
|
74
|
+
return strapi.entityService.create(WORKFLOW_MODEL_UID, createOpts);
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Updates an existing workflow.
|
|
80
|
+
* @param {object} workflow - The existing workflow to update.
|
|
81
|
+
* @param {object} opts - Options for updating the workflow.
|
|
82
|
+
* @returns {Promise<object>} - Workflow object that was just updated.
|
|
83
|
+
* @throws {ApplicationError} - If the supplied stage ID does not belong to the workflow.
|
|
84
|
+
*/
|
|
85
|
+
async update(workflow, opts) {
|
|
86
|
+
const stageService = getService('stages', { strapi });
|
|
87
|
+
let updateOpts = { ...opts, populate: { stages: true } };
|
|
88
|
+
let updatedStageIds;
|
|
89
|
+
|
|
90
|
+
await workflowsValidationService.validateWorkflowCount();
|
|
91
|
+
|
|
92
|
+
return strapi.db.transaction(async () => {
|
|
93
|
+
// Update stages
|
|
94
|
+
if (opts.data.stages) {
|
|
95
|
+
workflowsValidationService.validateWorkflowStages(opts.data.stages);
|
|
96
|
+
opts.data.stages.forEach((stage) =>
|
|
97
|
+
this.assertStageBelongsToWorkflow(stage.id, workflow)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
updatedStageIds = await stageService
|
|
101
|
+
.replaceStages(workflow.stages, opts.data.stages, workflow.contentTypes)
|
|
102
|
+
.then((stages) => stages.map((stage) => stage.id));
|
|
103
|
+
|
|
104
|
+
updateOpts = set('data.stages', updatedStageIds, opts);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Update (un)assigned Content Types
|
|
108
|
+
if (opts.data.contentTypes) {
|
|
109
|
+
await workflowsContentTypes.migrate({
|
|
110
|
+
srcContentTypes: workflow.contentTypes,
|
|
111
|
+
destContentTypes: opts.data.contentTypes,
|
|
112
|
+
stageId: updatedStageIds ? updatedStageIds[0] : workflow.stages[0].id,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Update Workflow
|
|
117
|
+
return strapi.entityService.update(WORKFLOW_MODEL_UID, workflow.id, updateOpts);
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Deletes an existing workflow.
|
|
123
|
+
* Also deletes all the workflow stages and migrate all assigned the content types.
|
|
124
|
+
* @param {*} workflow
|
|
125
|
+
* @param {*} opts
|
|
126
|
+
* @returns
|
|
127
|
+
*/
|
|
128
|
+
async delete(workflow, opts) {
|
|
129
|
+
const stageService = getService('stages', { strapi });
|
|
130
|
+
|
|
131
|
+
const workflowCount = await this.count();
|
|
132
|
+
|
|
133
|
+
if (workflowCount <= 1) {
|
|
134
|
+
throw new ApplicationError('Can not delete the last workflow');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return strapi.db.transaction(async () => {
|
|
138
|
+
// Delete stages
|
|
139
|
+
await stageService.deleteMany(workflow.stages.map((stage) => stage.id));
|
|
140
|
+
|
|
141
|
+
// Unassign all content types, this will migrate the content types to null
|
|
142
|
+
await workflowsContentTypes.migrate({
|
|
143
|
+
srcContentTypes: workflow.contentTypes,
|
|
144
|
+
destContentTypes: [],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Delete Workflow
|
|
148
|
+
return strapi.entityService.delete(WORKFLOW_MODEL_UID, workflow.id, opts);
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
/**
|
|
152
|
+
* Returns the total count of workflows.
|
|
153
|
+
* @returns {Promise<number>} - Total count of workflows.
|
|
154
|
+
*/
|
|
155
|
+
count() {
|
|
156
|
+
return strapi.entityService.count(WORKFLOW_MODEL_UID);
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Finds the assigned workflow for a given content type ID.
|
|
161
|
+
* @param {string} uid - Content type ID to find the assigned workflow for.
|
|
162
|
+
* @param {object} opts - Options for the query.
|
|
163
|
+
* @returns {Promise<object|null>} - Assigned workflow object if found, or null.
|
|
164
|
+
*/
|
|
165
|
+
async getAssignedWorkflow(uid, opts = {}) {
|
|
166
|
+
const workflows = await this.find({
|
|
167
|
+
...opts,
|
|
168
|
+
filters: { contentTypes: getWorkflowContentTypeFilter({ strapi }, uid) },
|
|
169
|
+
});
|
|
170
|
+
return workflows.length > 0 ? workflows[0] : null;
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Asserts that a content type has an assigned workflow.
|
|
175
|
+
* @param {string} uid - Content type ID to verify the assignment of.
|
|
176
|
+
* @returns {Promise<object>} - Workflow object associated with the content type ID.
|
|
177
|
+
* @throws {ApplicationError} - If no assigned workflow is found for the content type ID.
|
|
178
|
+
*/
|
|
179
|
+
async assertContentTypeBelongsToWorkflow(uid) {
|
|
180
|
+
const workflow = await this.getAssignedWorkflow(uid, {
|
|
181
|
+
populate: 'stages',
|
|
182
|
+
});
|
|
183
|
+
if (!workflow) {
|
|
184
|
+
throw new ApplicationError(`Review workflows is not activated on Content Type ${uid}.`);
|
|
185
|
+
}
|
|
186
|
+
return workflow;
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Asserts that a stage belongs to a given workflow.
|
|
191
|
+
* @param {string} stageId - ID of stage to check.
|
|
192
|
+
* @param {object} workflow - Workflow object to check against.
|
|
193
|
+
* @returns
|
|
194
|
+
* @throws {ApplicationError} - If the stage does not belong to the specified workflow.
|
|
195
|
+
*/
|
|
196
|
+
assertStageBelongsToWorkflow(stageId, workflow) {
|
|
197
|
+
if (!stageId) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const belongs = workflow.stages.some((stage) => stage.id === stageId);
|
|
202
|
+
if (!belongs) {
|
|
203
|
+
throw new ApplicationError(`Stage does not belong to workflow "${workflow.name}"`);
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
};
|
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
const {
|
|
3
|
+
const { getOr, keys, pickBy, pipe, has, clamp } = require('lodash/fp');
|
|
4
|
+
const {
|
|
5
|
+
ENTITY_STAGE_ATTRIBUTE,
|
|
6
|
+
MAX_WORKFLOWS,
|
|
7
|
+
MAX_STAGES_PER_WORKFLOW,
|
|
8
|
+
} = require('../constants/workflows');
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// If the input is a string, assume it's the modelUID of the content type and retrieve the corresponding object.
|
|
14
|
-
return hasReviewWorkflow({ strapi }, strapi.getModel(contentType));
|
|
15
|
-
}
|
|
16
|
-
// Otherwise, assume it's the content type object itself and return its `reviewWorkflows` option if it exists.
|
|
17
|
-
return contentType?.options?.reviewWorkflows || false;
|
|
18
|
-
}
|
|
19
|
-
// TODO To be refactored when multiple workflows are added
|
|
20
|
-
const getDefaultWorkflow = async ({ strapi }) =>
|
|
21
|
-
strapi.query(WORKFLOW_MODEL_UID).findOne({ populate: ['stages'] });
|
|
22
|
-
|
|
23
|
-
const getContentTypeUIDsWithActivatedReviewWorkflows = pipe([
|
|
24
|
-
// Pick only content-types with reviewWorkflows options set to true
|
|
25
|
-
pickBy(get('options.reviewWorkflows')),
|
|
10
|
+
const getVisibleContentTypesUID = pipe([
|
|
11
|
+
// Pick only content-types visible in the content-manager and option is not false
|
|
12
|
+
pickBy(
|
|
13
|
+
(value) =>
|
|
14
|
+
getOr(true, 'pluginOptions.content-manager.visible', value) &&
|
|
15
|
+
!getOr(false, 'options.noStageAttribute', value)
|
|
16
|
+
),
|
|
26
17
|
// Get UIDs
|
|
27
18
|
keys,
|
|
28
19
|
]);
|
|
29
20
|
|
|
21
|
+
const hasStageAttribute = has(['attributes', ENTITY_STAGE_ATTRIBUTE]);
|
|
22
|
+
|
|
23
|
+
const getWorkflowContentTypeFilter = ({ strapi }, contentType) => {
|
|
24
|
+
if (strapi.db.dialect.supportsOperator('$jsonSupersetOf')) {
|
|
25
|
+
return { $jsonSupersetOf: JSON.stringify([contentType]) };
|
|
26
|
+
}
|
|
27
|
+
return { $contains: `"${contentType}"` };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const clampMaxWorkflows = clamp(1, MAX_WORKFLOWS);
|
|
31
|
+
const clampMaxStagesPerWorkflow = clamp(1, MAX_STAGES_PER_WORKFLOW);
|
|
32
|
+
|
|
30
33
|
module.exports = {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
clampMaxWorkflows,
|
|
35
|
+
clampMaxStagesPerWorkflow,
|
|
36
|
+
getVisibleContentTypesUID,
|
|
37
|
+
hasStageAttribute,
|
|
38
|
+
getWorkflowContentTypeFilter,
|
|
34
39
|
};
|