@strapi/admin 4.9.2 → 4.10.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/src/content-manager/components/DynamicTable/CellContent/PublicationState/PublicationState.js +26 -0
- package/admin/src/content-manager/components/DynamicTable/CellContent/PublicationState/index.js +1 -0
- package/admin/src/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +2 -0
- package/admin/src/content-manager/components/DynamicTable/index.js +25 -49
- package/admin/src/content-manager/components/DynamicZone/components/DynamicComponent.js +2 -0
- package/admin/src/content-manager/pages/EditView/Information/index.js +77 -53
- package/admin/src/content-manager/pages/EditView/InformationBox/InformationBoxCE.js +13 -0
- package/admin/src/content-manager/pages/EditView/InformationBox/index.js +3 -0
- package/admin/src/content-manager/pages/EditView/index.js +3 -4
- package/admin/src/content-manager/pages/ListView/index.js +6 -9
- package/admin/src/content-manager/sharedReducers/crudReducer/actions.js +6 -0
- package/admin/src/content-manager/sharedReducers/crudReducer/constants.js +1 -0
- package/admin/src/content-manager/sharedReducers/crudReducer/reducer.js +5 -0
- package/admin/src/index.js +1 -0
- package/admin/src/translations/en.json +6 -0
- package/build/{Admin-authenticatedApp.217db666.chunk.js → Admin-authenticatedApp.52c88751.chunk.js} +2 -2
- package/build/{Admin_settingsPage.1dbfc9ce.chunk.js → Admin_settingsPage.257b3477.chunk.js} +7 -7
- package/build/{admin-app.558af642.chunk.js → admin-app.dfaeea5d.chunk.js} +18 -18
- package/build/content-manager.def692c2.chunk.js +1130 -0
- package/build/content-type-builder-translation-en-json.510e88ca.chunk.js +1 -0
- package/build/content-type-builder.5e1f4afc.chunk.js +126 -0
- package/build/en-json.08303b37.chunk.js +1 -0
- package/build/index.html +1 -1
- package/build/{main.ef8db4a2.js → main.120be100.js} +145 -145
- package/build/review-workflows-settings.9092ed72.chunk.js +106 -0
- package/build/{runtime~main.3a92d953.js → runtime~main.112b3101.js} +1 -1
- package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/ReviewWorkflowsStageEE.js +15 -0
- package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +45 -0
- package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/index.js +3 -0
- package/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js +135 -0
- package/ee/admin/content-manager/pages/EditView/InformationBox/index.js +3 -0
- package/ee/admin/hooks/useSettingsMenu/utils/customAdminLinks.js +12 -12
- package/ee/admin/hooks/useSettingsMenu/utils/customGlobalLinks.js +21 -13
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js +199 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/actions/index.js +42 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/AddStage/AddStage.js +87 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/AddStage/index.js +1 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +90 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/index.js +1 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stages.js +92 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/index.js +1 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js +6 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows.js +35 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/index.js +3 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js +122 -0
- package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/getWorkflowValidationSchema.js +25 -0
- package/ee/admin/pages/SettingsPage/utils/customRoutes.js +16 -2
- package/ee/admin/permissions/customPermissions.js +3 -0
- package/ee/server/bootstrap.js +13 -0
- package/ee/server/config/admin-actions.js +10 -0
- package/ee/server/constants/default-stages.json +14 -0
- package/ee/server/constants/default-workflow.json +1 -0
- package/ee/server/constants/workflows.js +8 -0
- package/ee/server/content-types/index.js +9 -0
- package/ee/server/content-types/workflow/index.js +31 -0
- package/ee/server/content-types/workflow-stage/index.js +36 -0
- package/ee/server/controllers/index.js +2 -0
- package/ee/server/controllers/workflows/index.js +36 -0
- package/ee/server/controllers/workflows/stages/index.js +102 -0
- package/ee/server/index.js +1 -0
- package/ee/server/middlewares/review-workflows.js +40 -0
- package/ee/server/register.js +8 -0
- package/ee/server/routes/index.js +104 -0
- package/ee/server/services/index.js +4 -0
- package/ee/server/services/review-workflows/entity-service-decorator.js +54 -0
- package/ee/server/services/review-workflows/review-workflows.js +111 -0
- package/ee/server/services/review-workflows/stages.js +249 -0
- package/ee/server/services/review-workflows/workflows.js +25 -0
- package/ee/server/utils/index.js +8 -0
- package/ee/server/utils/persisted-tables.js +114 -22
- package/ee/server/utils/review-workflows.js +34 -0
- package/ee/server/validation/review-workflows.js +24 -0
- package/package.json +9 -9
- package/build/content-manager.d1565bfc.chunk.js +0 -1132
- package/build/content-type-builder-translation-en-json.6c8e69ab.chunk.js +0 -1
- package/build/content-type-builder.9d780e7f.chunk.js +0 -126
- package/build/en-json.cf600231.chunk.js +0 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { set, forEach, pipe } = require('lodash/fp');
|
|
4
|
+
const { mapAsync } = require('@strapi/utils');
|
|
5
|
+
const { getService } = require('../../utils');
|
|
6
|
+
const { getContentTypeUIDsWithActivatedReviewWorkflows } = require('../../utils/review-workflows');
|
|
7
|
+
|
|
8
|
+
const defaultStages = require('../../constants/default-stages.json');
|
|
9
|
+
const defaultWorkflow = require('../../constants/default-workflow.json');
|
|
10
|
+
const { ENTITY_STAGE_ATTRIBUTE } = require('../../constants/workflows');
|
|
11
|
+
|
|
12
|
+
const { getDefaultWorkflow } = require('../../utils/review-workflows');
|
|
13
|
+
const { persistTable, removePersistedTablesWithSuffix } = require('../../utils/persisted-tables');
|
|
14
|
+
|
|
15
|
+
async function initDefaultWorkflow({ workflowsService, stagesService, strapi }) {
|
|
16
|
+
const wfCount = await workflowsService.count();
|
|
17
|
+
const stagesCount = await stagesService.count();
|
|
18
|
+
|
|
19
|
+
// Check if there is nothing about review-workflow in DB
|
|
20
|
+
// If any, the feature has already been initialized with a workflow and stages
|
|
21
|
+
if (wfCount === 0 && stagesCount === 0) {
|
|
22
|
+
const stages = await stagesService.createMany(defaultStages, { fields: ['id'] });
|
|
23
|
+
const workflow = {
|
|
24
|
+
...defaultWorkflow,
|
|
25
|
+
stages: {
|
|
26
|
+
connect: stages.map((stage) => stage.id),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
await workflowsService.create(workflow);
|
|
31
|
+
// If there is any manually activated RW on content-types, we want to migrate the related entities
|
|
32
|
+
await enableReviewWorkflow({ strapi })({ contentTypes: strapi.contentTypes });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extendReviewWorkflowContentTypes({ strapi }) {
|
|
37
|
+
const extendContentType = (contentTypeUID) => {
|
|
38
|
+
const setStageAttribute = set(`attributes.${ENTITY_STAGE_ATTRIBUTE}`, {
|
|
39
|
+
writable: true,
|
|
40
|
+
private: false,
|
|
41
|
+
configurable: false,
|
|
42
|
+
visible: false,
|
|
43
|
+
useJoinTable: true, // We want a join table to persist data when downgrading to CE
|
|
44
|
+
type: 'relation',
|
|
45
|
+
relation: 'oneToOne',
|
|
46
|
+
target: 'admin::workflow-stage',
|
|
47
|
+
});
|
|
48
|
+
strapi.container.get('content-types').extend(contentTypeUID, setStageAttribute);
|
|
49
|
+
};
|
|
50
|
+
pipe([
|
|
51
|
+
getContentTypeUIDsWithActivatedReviewWorkflows,
|
|
52
|
+
// Iterate over UIDs to extend the content-type
|
|
53
|
+
forEach(extendContentType),
|
|
54
|
+
])(strapi.contentTypes);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Enables the review workflow for the given content types.
|
|
59
|
+
* @param {Object} strapi - Strapi instance
|
|
60
|
+
*/
|
|
61
|
+
function enableReviewWorkflow({ strapi }) {
|
|
62
|
+
/**
|
|
63
|
+
* @param {Array<string>} contentTypes - Content type UIDs to enable the review workflow for.
|
|
64
|
+
* @returns {Promise<void>} - Promise that resolves when the review workflow is enabled.
|
|
65
|
+
*/
|
|
66
|
+
return async ({ contentTypes }) => {
|
|
67
|
+
const defaultWorkflow = await getDefaultWorkflow({ strapi });
|
|
68
|
+
// This is possible if this is the first start of EE, there won't be any workflow in DB before bootstrap
|
|
69
|
+
if (!defaultWorkflow) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const firstStage = defaultWorkflow.stages[0];
|
|
73
|
+
const stagesService = getService('stages', { strapi });
|
|
74
|
+
|
|
75
|
+
const up = async (contentTypeUID) => {
|
|
76
|
+
// Persist the stage join table
|
|
77
|
+
const { attributes, tableName } = strapi.db.metadata.get(contentTypeUID);
|
|
78
|
+
const joinTableName = attributes[ENTITY_STAGE_ATTRIBUTE].joinTable.name;
|
|
79
|
+
await persistTable(joinTableName, [tableName]);
|
|
80
|
+
|
|
81
|
+
// Update CT entities stage
|
|
82
|
+
return stagesService.updateEntitiesStage(contentTypeUID, {
|
|
83
|
+
fromStageId: null,
|
|
84
|
+
toStageId: firstStage.id,
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
await removePersistedTablesWithSuffix('_strapi_review_workflows_stage_links');
|
|
89
|
+
|
|
90
|
+
return pipe([
|
|
91
|
+
getContentTypeUIDsWithActivatedReviewWorkflows,
|
|
92
|
+
// Iterate over UIDs to extend the content-type
|
|
93
|
+
(contentTypesUIDs) => mapAsync(contentTypesUIDs, up),
|
|
94
|
+
])(contentTypes);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = ({ strapi }) => {
|
|
99
|
+
const workflowsService = getService('workflows', { strapi });
|
|
100
|
+
const stagesService = getService('stages', { strapi });
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
async bootstrap() {
|
|
104
|
+
await initDefaultWorkflow({ workflowsService, stagesService, strapi });
|
|
105
|
+
},
|
|
106
|
+
async register() {
|
|
107
|
+
extendReviewWorkflowContentTypes({ strapi });
|
|
108
|
+
strapi.hook('strapi::content-types.afterSync').register(enableReviewWorkflow({ strapi }));
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
mapAsync,
|
|
5
|
+
errors: { ApplicationError },
|
|
6
|
+
} = require('@strapi/utils');
|
|
7
|
+
const { map } = require('lodash/fp');
|
|
8
|
+
|
|
9
|
+
const { STAGE_MODEL_UID, ENTITY_STAGE_ATTRIBUTE } = require('../../constants/workflows');
|
|
10
|
+
const { getService } = require('../../utils');
|
|
11
|
+
const { getContentTypeUIDsWithActivatedReviewWorkflows } = require('../../utils/review-workflows');
|
|
12
|
+
|
|
13
|
+
module.exports = ({ strapi }) => {
|
|
14
|
+
const workflowsService = getService('workflows', { strapi });
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
find({ workflowId, populate }) {
|
|
18
|
+
const params = {
|
|
19
|
+
filters: { workflow: workflowId },
|
|
20
|
+
populate,
|
|
21
|
+
};
|
|
22
|
+
return strapi.entityService.findMany(STAGE_MODEL_UID, params);
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
findById(id, { populate } = {}) {
|
|
26
|
+
const params = {
|
|
27
|
+
populate,
|
|
28
|
+
};
|
|
29
|
+
return strapi.entityService.findOne(STAGE_MODEL_UID, id, params);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
createMany(stagesList, { fields }) {
|
|
33
|
+
const params = {
|
|
34
|
+
select: fields,
|
|
35
|
+
};
|
|
36
|
+
return Promise.all(
|
|
37
|
+
stagesList.map((stage) =>
|
|
38
|
+
strapi.entityService.create(STAGE_MODEL_UID, { data: stage, ...params })
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
update(stageId, stageData) {
|
|
44
|
+
return strapi.entityService.update(STAGE_MODEL_UID, stageId, { data: stageData });
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
delete(stageId) {
|
|
48
|
+
return strapi.entityService.delete(STAGE_MODEL_UID, stageId);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
count() {
|
|
52
|
+
return strapi.entityService.count(STAGE_MODEL_UID);
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async replaceWorkflowStages(workflowId, stages) {
|
|
56
|
+
const workflow = await workflowsService.findById(workflowId, { populate: ['stages'] });
|
|
57
|
+
|
|
58
|
+
const { created, updated, deleted } = getDiffBetweenStages(workflow.stages, stages);
|
|
59
|
+
|
|
60
|
+
assertAtLeastOneStageRemain(workflow.stages, { created, deleted });
|
|
61
|
+
|
|
62
|
+
return strapi.db.transaction(async ({ trx }) => {
|
|
63
|
+
// Create the new stages
|
|
64
|
+
const createdStages = await this.createMany(created, { fields: ['id'] });
|
|
65
|
+
// Put all the newly created stages ids
|
|
66
|
+
const createdStagesIds = map('id', createdStages);
|
|
67
|
+
const stagesIds = stages.map((stage) => stage.id ?? createdStagesIds.shift());
|
|
68
|
+
const contentTypes = getContentTypeUIDsWithActivatedReviewWorkflows(strapi.contentTypes);
|
|
69
|
+
|
|
70
|
+
// Update the workflow stages
|
|
71
|
+
await mapAsync(updated, (stage) => this.update(stage.id, stage));
|
|
72
|
+
|
|
73
|
+
// Delete the stages that are not in the new stages list
|
|
74
|
+
await mapAsync(deleted, async (stage) => {
|
|
75
|
+
// Find the nearest stage in the workflow and newly created stages
|
|
76
|
+
// that is not deleted, prioritizing the previous stages
|
|
77
|
+
const nearestStage = findNearestMatchingStage(
|
|
78
|
+
[...workflow.stages, ...createdStages],
|
|
79
|
+
workflow.stages.findIndex((s) => s.id === stage.id),
|
|
80
|
+
(targetStage) => {
|
|
81
|
+
return !deleted.find((s) => s.id === targetStage.id);
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Assign the new stage to entities that had the deleted stage
|
|
86
|
+
await mapAsync(contentTypes, (contentTypeUID) => {
|
|
87
|
+
this.updateEntitiesStage(contentTypeUID, {
|
|
88
|
+
fromStageId: stage.id,
|
|
89
|
+
toStageId: nearestStage.id,
|
|
90
|
+
trx,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return this.delete(stage.id);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return workflowsService.update(workflowId, {
|
|
98
|
+
stages: stagesIds,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Update the stage of an entity
|
|
105
|
+
*
|
|
106
|
+
* @param {object} entityInfo
|
|
107
|
+
* @param {number} entityInfo.id - Entity id
|
|
108
|
+
* @param {string} entityInfo.modelUID - the content-type of the entity
|
|
109
|
+
* @param {number} stageId - The id of the stage to assign to the entity
|
|
110
|
+
*/
|
|
111
|
+
async updateEntity(entityInfo, stageId) {
|
|
112
|
+
const stage = await this.findById(stageId);
|
|
113
|
+
|
|
114
|
+
if (!stage) {
|
|
115
|
+
throw new ApplicationError(`Selected stage does not exist`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return strapi.entityService.update(entityInfo.modelUID, entityInfo.id, {
|
|
119
|
+
data: { [ENTITY_STAGE_ATTRIBUTE]: stageId },
|
|
120
|
+
populate: [ENTITY_STAGE_ATTRIBUTE],
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Updates the stage of all entities of a content type that are in a specific stage
|
|
126
|
+
* @param {string} contentTypeUID
|
|
127
|
+
* @param {number} fromStageId
|
|
128
|
+
* @param {number} toStageId
|
|
129
|
+
* @param {KnexTransaction} trx
|
|
130
|
+
* @returns
|
|
131
|
+
*/
|
|
132
|
+
async updateEntitiesStage(contentTypeUID, { fromStageId, toStageId, trx = null }) {
|
|
133
|
+
const { attributes, tableName } = strapi.db.metadata.get(contentTypeUID);
|
|
134
|
+
const joinTable = attributes[ENTITY_STAGE_ATTRIBUTE].joinTable;
|
|
135
|
+
const joinColumn = joinTable.joinColumn.name;
|
|
136
|
+
const invJoinColumn = joinTable.inverseJoinColumn.name;
|
|
137
|
+
|
|
138
|
+
const selectStatement = strapi.db
|
|
139
|
+
.getConnection()
|
|
140
|
+
.select({ [joinColumn]: 't1.id', [invJoinColumn]: toStageId })
|
|
141
|
+
.from(`${tableName} as t1`)
|
|
142
|
+
.leftJoin(`${joinTable.name} as t2`, `t1.id`, `t2.${joinColumn}`)
|
|
143
|
+
.where(`t2.${invJoinColumn}`, fromStageId)
|
|
144
|
+
.toSQL();
|
|
145
|
+
|
|
146
|
+
// Insert rows for all entries of the content type that do not have a
|
|
147
|
+
// default stage
|
|
148
|
+
const query = strapi.db
|
|
149
|
+
.getConnection(joinTable.name)
|
|
150
|
+
.insert(
|
|
151
|
+
strapi.db.connection.raw(
|
|
152
|
+
`(${joinColumn}, ${invJoinColumn}) ${selectStatement.sql}`,
|
|
153
|
+
selectStatement.bindings
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (trx) {
|
|
158
|
+
query.transacting(trx);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return query;
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Compares two arrays of stages and returns an object indicating the differences.
|
|
168
|
+
*
|
|
169
|
+
* The function compares the `id` properties of each stage in `sourceStages` and `comparisonStages` to determine if the stage is present in both arrays.
|
|
170
|
+
* If a stage with the same `id` is found in both arrays but the `name` property is different, the stage is considered updated.
|
|
171
|
+
* If a stage with a particular `id` is only found in `comparisonStages`, it is considered created.
|
|
172
|
+
* If a stage with a particular `id` is only found in `sourceStages`, it is considered deleted.
|
|
173
|
+
*
|
|
174
|
+
* @typedef {{id: Number, name: String, workflow: Number}} Stage
|
|
175
|
+
* @typedef {{created: Stage[], updated: Stage[], deleted: Stage[]}} DiffStages
|
|
176
|
+
*
|
|
177
|
+
* The DiffStages object has three properties: `created`, `updated`, and `deleted`.
|
|
178
|
+
* `created` is an array of stages that are in `comparisonStages` but not in `sourceStages`.
|
|
179
|
+
* `updated` is an array of stages that have different names in `comparisonStages` and `sourceStages`.
|
|
180
|
+
* `deleted` is an array of stages that are in `sourceStages` but not in `comparisonStages`.
|
|
181
|
+
*
|
|
182
|
+
* @param {Stage[]} sourceStages
|
|
183
|
+
* @param {Stage[]} comparisonStages
|
|
184
|
+
* @returns { DiffStages }
|
|
185
|
+
*/
|
|
186
|
+
function getDiffBetweenStages(sourceStages, comparisonStages) {
|
|
187
|
+
const result = comparisonStages.reduce(
|
|
188
|
+
(acc, stageToCompare) => {
|
|
189
|
+
const srcStage = sourceStages.find((stage) => stage.id === stageToCompare.id);
|
|
190
|
+
|
|
191
|
+
if (!srcStage) {
|
|
192
|
+
acc.created.push(stageToCompare);
|
|
193
|
+
} else if (srcStage.name !== stageToCompare.name) {
|
|
194
|
+
acc.updated.push(stageToCompare);
|
|
195
|
+
}
|
|
196
|
+
return acc;
|
|
197
|
+
},
|
|
198
|
+
{ created: [], updated: [] }
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
result.deleted = sourceStages.filter(
|
|
202
|
+
(srcStage) => !comparisonStages.some((cmpStage) => cmpStage.id === srcStage.id)
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Asserts that at least one stage remains in the workflow after applying deletions and additions.
|
|
210
|
+
*
|
|
211
|
+
* @param {Array} workflowStages - An array of stages in the current workflow.
|
|
212
|
+
* @param {Object} diffStages - An object containing the stages to be deleted and created.
|
|
213
|
+
* @param {Array} diffStages.deleted - An array of stages that are planned to be deleted from the workflow.
|
|
214
|
+
* @param {Array} diffStages.created - An array of stages that are planned to be created in the workflow.
|
|
215
|
+
*
|
|
216
|
+
* @throws {ApplicationError} If the number of remaining stages in the workflow after applying deletions and additions is less than 1.
|
|
217
|
+
*/
|
|
218
|
+
function assertAtLeastOneStageRemain(workflowStages, diffStages) {
|
|
219
|
+
const remainingStagesCount =
|
|
220
|
+
workflowStages.length - diffStages.deleted.length + diffStages.created.length;
|
|
221
|
+
if (remainingStagesCount < 1) {
|
|
222
|
+
throw new ApplicationError('At least one stage must remain in the workflow.');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Find the id of the nearest object in an array that matches a condition.
|
|
228
|
+
* Used for searching for the nearest stage that is not deleted.
|
|
229
|
+
* Starts by searching the elements before the index, then the remaining elements in the array.
|
|
230
|
+
*
|
|
231
|
+
* @param {Array} stages
|
|
232
|
+
* @param {Number} startIndex the index to start searching from
|
|
233
|
+
* @param {Function} condition must evaluate to true for the object to be considered a match
|
|
234
|
+
* @returns {Object} stage
|
|
235
|
+
*/
|
|
236
|
+
function findNearestMatchingStage(stages, startIndex, condition) {
|
|
237
|
+
// Start by searching the elements before the startIndex
|
|
238
|
+
for (let i = startIndex; i >= 0; i -= 1) {
|
|
239
|
+
if (condition(stages[i])) {
|
|
240
|
+
return stages[i];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// If no matching element is found before the startIndex,
|
|
245
|
+
// search the remaining elements in the array
|
|
246
|
+
const remainingArray = stages.slice(startIndex + 1);
|
|
247
|
+
const nearestObject = remainingArray.filter(condition)[0];
|
|
248
|
+
return nearestObject;
|
|
249
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { WORKFLOW_MODEL_UID } = require('../../constants/workflows');
|
|
4
|
+
|
|
5
|
+
module.exports = ({ strapi }) => ({
|
|
6
|
+
find(opts) {
|
|
7
|
+
return strapi.entityService.findMany(WORKFLOW_MODEL_UID, opts);
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
findById(id, opts) {
|
|
11
|
+
return strapi.entityService.findOne(WORKFLOW_MODEL_UID, id, opts);
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
create(workflowData) {
|
|
15
|
+
return strapi.entityService.create(WORKFLOW_MODEL_UID, { data: workflowData });
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
count() {
|
|
19
|
+
return strapi.entityService.count(WORKFLOW_MODEL_UID);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
update(id, workflowData) {
|
|
23
|
+
return strapi.entityService.update(WORKFLOW_MODEL_UID, id, { data: workflowData });
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -1,49 +1,141 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { differenceWith, isEqual } = require('lodash/fp');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
5
|
-
* @param {string}
|
|
6
|
-
* @returns {Array}
|
|
6
|
+
* Transform table name to the object format
|
|
7
|
+
* @param {Array<string|{ table: string; dependsOn?: Array<{ table: string;}> }>} table
|
|
8
|
+
* @returns Array<{ table: string; dependsOn?: Array<{ table: string;}> }>
|
|
7
9
|
*/
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
const transformTableName = (table) => {
|
|
11
|
+
if (typeof table === 'string') {
|
|
12
|
+
return { name: table };
|
|
13
|
+
}
|
|
14
|
+
return table;
|
|
11
15
|
};
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
|
-
*
|
|
15
|
-
* @
|
|
18
|
+
* Finds all tables in the database matching the regular expression
|
|
19
|
+
* @param {Object} ctx
|
|
20
|
+
* @param {Strapi} ctx.strapi
|
|
21
|
+
* @param {RegExp} regex
|
|
22
|
+
* @returns {Promise<string[]>}
|
|
16
23
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
async function findTables({ strapi }, regex) {
|
|
25
|
+
const tables = await strapi.db.dialect.schemaInspector.getTables();
|
|
26
|
+
return tables.filter((tableName) => regex.test(tableName));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Add tables name to the reserved tables in core store
|
|
31
|
+
* @param {Object} ctx
|
|
32
|
+
* @param {Strapi} ctx.strapi
|
|
33
|
+
* @param {Array<string|{ table: string; dependsOn?: Array<{ table: string;}> }>} tableNames
|
|
34
|
+
* @return {Promise<void>}
|
|
35
|
+
*/
|
|
36
|
+
async function addPersistTables({ strapi }, tableNames) {
|
|
37
|
+
const persistedTables = await getPersistedTables({ strapi });
|
|
38
|
+
const tables = tableNames.map(transformTableName);
|
|
39
|
+
|
|
40
|
+
// Get new tables to be persisted, remove tables if they already were persisted
|
|
41
|
+
const notPersistedTableNames = differenceWith(isEqual, tables, persistedTables);
|
|
42
|
+
// Remove tables that are going to be changed
|
|
43
|
+
const tablesToPersist = differenceWith(
|
|
44
|
+
(t1, t2) => t1.name === t2.name,
|
|
45
|
+
persistedTables,
|
|
46
|
+
notPersistedTableNames
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (!notPersistedTableNames.length) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
tablesToPersist.push(...notPersistedTableNames);
|
|
54
|
+
await strapi.store.set({
|
|
19
55
|
type: 'core',
|
|
20
56
|
key: 'persisted_tables',
|
|
21
|
-
|
|
57
|
+
value: tablesToPersist,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
22
60
|
|
|
23
61
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* @param {
|
|
62
|
+
* Remove tables name from the reserved tables in core store
|
|
63
|
+
* @param {Object} ctx
|
|
64
|
+
* @param {Strapi} ctx.strapi
|
|
65
|
+
* @param {Array<string>} tableNames
|
|
66
|
+
* @return {Promise<void>}
|
|
27
67
|
*/
|
|
68
|
+
async function removePersistedTables({ strapi }, tableNames) {
|
|
69
|
+
const persistedTables = await getPersistedTables({ strapi });
|
|
28
70
|
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
71
|
+
// Get new tables to be persisted, remove tables if they already were persisted
|
|
72
|
+
const newPersistedTables = differenceWith(
|
|
73
|
+
(t1, t2) => t1.name === t2,
|
|
74
|
+
persistedTables,
|
|
75
|
+
tableNames
|
|
76
|
+
);
|
|
33
77
|
|
|
34
|
-
if (
|
|
78
|
+
if (newPersistedTables.length === persistedTables.length) {
|
|
35
79
|
return;
|
|
36
80
|
}
|
|
37
81
|
|
|
38
|
-
persistedTables.push(...notReservedTableNames);
|
|
39
82
|
await strapi.store.set({
|
|
40
83
|
type: 'core',
|
|
41
84
|
key: 'persisted_tables',
|
|
42
|
-
value:
|
|
85
|
+
value: newPersistedTables,
|
|
43
86
|
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get all reserved table names from the core store
|
|
91
|
+
* @param {Object} ctx
|
|
92
|
+
* @param {Strapi} ctx.strapi
|
|
93
|
+
* @returns {Promise<string[]>}
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
async function getPersistedTables({ strapi }) {
|
|
97
|
+
const persistedTables = await strapi.store.get({
|
|
98
|
+
type: 'core',
|
|
99
|
+
key: 'persisted_tables',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return (persistedTables || []).map(transformTableName);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Add all table names that start with a prefix to the reserved tables in
|
|
107
|
+
* core store
|
|
108
|
+
* @param {string} tableNamePrefix
|
|
109
|
+
* @return {Promise<void>}
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
const persistTablesWithPrefix = async (tableNamePrefix) => {
|
|
113
|
+
const tableNameRegex = new RegExp(`^${tableNamePrefix}.*`);
|
|
114
|
+
const tableNames = await findTables({ strapi }, tableNameRegex);
|
|
115
|
+
|
|
116
|
+
await addPersistTables({ strapi }, tableNames);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Remove all table names that end with a suffix from the reserved tables in core store
|
|
121
|
+
* @param {string} tableNameSuffix
|
|
122
|
+
* @return {Promise<void>}
|
|
123
|
+
*/
|
|
124
|
+
const removePersistedTablesWithSuffix = async (tableNameSuffix) => {
|
|
125
|
+
const tableNameRegex = new RegExp(`.*${tableNameSuffix}$`);
|
|
126
|
+
const tableNames = await findTables({ strapi }, tableNameRegex);
|
|
127
|
+
await removePersistedTables({ strapi }, tableNames);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const persistTable = async (tableName, dependsOn) => {
|
|
131
|
+
await addPersistTables({ strapi }, [
|
|
132
|
+
{ name: tableName, dependsOn: dependsOn?.map((depTableName) => ({ name: depTableName })) },
|
|
133
|
+
]);
|
|
44
134
|
};
|
|
45
135
|
|
|
46
136
|
module.exports = {
|
|
47
137
|
persistTablesWithPrefix,
|
|
48
|
-
|
|
138
|
+
removePersistedTablesWithSuffix,
|
|
139
|
+
persistTable,
|
|
140
|
+
findTables,
|
|
49
141
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { get, keys, pickBy, pipe } = require('lodash/fp');
|
|
4
|
+
const { WORKFLOW_MODEL_UID } = require('../constants/workflows');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Checks if a content type has review workflows enabled.
|
|
8
|
+
* @param {string|Object} contentType - Either the modelUID of the content type, or the content type object.
|
|
9
|
+
* @returns {boolean} Whether review workflows are enabled for the specified content type.
|
|
10
|
+
*/
|
|
11
|
+
function hasReviewWorkflow({ strapi }, contentType) {
|
|
12
|
+
if (typeof contentType === 'string') {
|
|
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')),
|
|
26
|
+
// Get UIDs
|
|
27
|
+
keys,
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
hasReviewWorkflow,
|
|
32
|
+
getDefaultWorkflow,
|
|
33
|
+
getContentTypeUIDsWithActivatedReviewWorkflows,
|
|
34
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { yup, validateYupSchema } = require('@strapi/utils');
|
|
4
|
+
|
|
5
|
+
const stageObject = yup.object().shape({
|
|
6
|
+
id: yup.number().integer().min(1),
|
|
7
|
+
name: yup.string().max(255).required(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const validateUpdateStagesSchema = yup.array().of(stageObject).required();
|
|
11
|
+
const validateUpdateStageOnEntity = yup
|
|
12
|
+
.object()
|
|
13
|
+
.shape({
|
|
14
|
+
id: yup.number().integer().min(1).required(),
|
|
15
|
+
})
|
|
16
|
+
.required();
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
validateUpdateStages: validateYupSchema(validateUpdateStagesSchema, {
|
|
20
|
+
strict: false,
|
|
21
|
+
stripUnknown: true,
|
|
22
|
+
}),
|
|
23
|
+
validateUpdateStageOnEntity: validateYupSchema(validateUpdateStageOnEntity),
|
|
24
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strapi/admin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.10.0-beta.1",
|
|
4
4
|
"description": "Strapi Admin",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
"@casl/ability": "^5.4.3",
|
|
49
49
|
"@fingerprintjs/fingerprintjs": "3.3.6",
|
|
50
50
|
"@pmmmwh/react-refresh-webpack-plugin": "0.5.10",
|
|
51
|
-
"@strapi/babel-plugin-switch-ee-ce": "4.
|
|
52
|
-
"@strapi/data-transfer": "4.
|
|
51
|
+
"@strapi/babel-plugin-switch-ee-ce": "4.10.0-beta.1",
|
|
52
|
+
"@strapi/data-transfer": "4.10.0-beta.1",
|
|
53
53
|
"@strapi/design-system": "1.6.6",
|
|
54
|
-
"@strapi/helper-plugin": "4.
|
|
54
|
+
"@strapi/helper-plugin": "4.10.0-beta.1",
|
|
55
55
|
"@strapi/icons": "1.6.6",
|
|
56
|
-
"@strapi/permissions": "4.
|
|
57
|
-
"@strapi/provider-audit-logs-local": "4.
|
|
58
|
-
"@strapi/typescript-utils": "4.
|
|
59
|
-
"@strapi/utils": "4.
|
|
56
|
+
"@strapi/permissions": "4.10.0-beta.1",
|
|
57
|
+
"@strapi/provider-audit-logs-local": "4.10.0-beta.1",
|
|
58
|
+
"@strapi/typescript-utils": "4.10.0-beta.1",
|
|
59
|
+
"@strapi/utils": "4.10.0-beta.1",
|
|
60
60
|
"axios": "1.3.4",
|
|
61
61
|
"babel-loader": "^9.1.2",
|
|
62
62
|
"babel-plugin-styled-components": "2.0.2",
|
|
@@ -168,5 +168,5 @@
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
},
|
|
171
|
-
"gitHead": "
|
|
171
|
+
"gitHead": "95d581b31bee464af42e5d8db408fa578d8532c7"
|
|
172
172
|
}
|