@strapi/admin 4.9.0 → 4.10.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.
Files changed (184) hide show
  1. package/admin/src/content-manager/components/DynamicTable/CellContent/PublicationState/PublicationState.js +26 -0
  2. package/admin/src/content-manager/components/DynamicTable/CellContent/PublicationState/index.js +1 -0
  3. package/admin/src/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/ReviewWorkflowsStage.js +15 -0
  4. package/admin/src/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/index.js +1 -0
  5. package/admin/src/content-manager/components/DynamicTable/index.js +43 -49
  6. package/admin/src/content-manager/components/DynamicZone/components/DynamicComponent.js +2 -0
  7. package/admin/src/content-manager/components/EditViewDataManagerProvider/reducer.js +1 -3
  8. package/admin/src/content-manager/components/EditViewDataManagerProvider/utils/findAllAndReplace.js +3 -10
  9. package/admin/src/content-manager/components/InputUID/endActionStyle.js +13 -4
  10. package/admin/src/content-manager/components/InputUID/index.js +71 -94
  11. package/admin/src/content-manager/pages/EditView/Information/index.js +77 -53
  12. package/admin/src/content-manager/pages/EditView/InformationBox/InformationBoxCE.js +13 -0
  13. package/admin/src/content-manager/pages/EditView/InformationBox/index.js +3 -0
  14. package/admin/src/content-manager/pages/EditView/index.js +3 -4
  15. package/admin/src/content-manager/pages/ListView/index.js +6 -9
  16. package/admin/src/hooks/useRegenerate/index.js +7 -12
  17. package/admin/src/index.js +1 -0
  18. package/admin/src/pages/AuthPage/components/Register/index.js +38 -46
  19. package/admin/src/pages/SettingsPage/components/Tokens/FormHead/index.js +0 -4
  20. package/admin/src/pages/SettingsPage/components/Tokens/Regenerate/index.js +3 -5
  21. package/admin/src/pages/SettingsPage/components/Tokens/TokenTypeSelect/index.js +5 -7
  22. package/admin/src/pages/SettingsPage/pages/TransferTokens/EditView/components/FormTransferTokenContainer/index.js +0 -41
  23. package/admin/src/pages/SettingsPage/pages/TransferTokens/EditView/index.js +9 -53
  24. package/admin/src/pages/SettingsPage/pages/TransferTokens/EditView/utils/schema.js +0 -1
  25. package/admin/src/pages/SettingsPage/pages/TransferTokens/ListView/index.js +5 -27
  26. package/admin/src/translations/en.json +6 -1
  27. package/build/2263.4c5916f9.chunk.js +98 -0
  28. package/build/4049.64715f20.chunk.js +1 -0
  29. package/build/4649.213b8a3b.chunk.js +30 -0
  30. package/build/6985.66cca29c.chunk.js +1 -0
  31. package/build/7259.aefb51e8.chunk.js +1 -0
  32. package/build/8469.853c822b.chunk.js +1 -0
  33. package/build/9505.dbe702ab.chunk.js +14 -0
  34. package/build/9816.01ee964f.chunk.js +2 -0
  35. package/build/Admin-authenticatedApp.f50ad423.chunk.js +79 -0
  36. package/build/Admin_InternalErrorPage.4ad8b0df.chunk.js +1 -0
  37. package/build/Admin_homePage.1411fb7c.chunk.js +68 -0
  38. package/build/Admin_marketplace.02608d56.chunk.js +22 -0
  39. package/build/Admin_pluginsPage.15e3b0fd.chunk.js +1 -0
  40. package/build/Admin_profilePage.76afeca0.chunk.js +15 -0
  41. package/build/Admin_settingsPage.147755cd.chunk.js +9 -0
  42. package/build/Upload_ConfigureTheView.34dde278.chunk.js +1 -0
  43. package/build/admin-app.55dd7921.chunk.js +112 -0
  44. package/build/admin-edit-roles-page.cf543488.chunk.js +216 -0
  45. package/build/admin-edit-users.31c20712.chunk.js +10 -0
  46. package/build/admin-roles-list.489c501f.chunk.js +2 -0
  47. package/build/admin-users.3e111a7d.chunk.js +11 -0
  48. package/build/api-tokens-create-page.4328b852.chunk.js +1 -0
  49. package/build/api-tokens-edit-page.bce5050f.chunk.js +1 -0
  50. package/build/api-tokens-list-page.93f24348.chunk.js +16 -0
  51. package/build/audit-logs-settings-page.7be97e82.chunk.js +1 -0
  52. package/build/content-manager.4480ae88.chunk.js +1137 -0
  53. package/build/content-type-builder-list-view.cf38fe2f.chunk.js +191 -0
  54. package/build/content-type-builder-translation-en-json.7961593e.chunk.js +1 -0
  55. package/build/content-type-builder.af9abf1e.chunk.js +126 -0
  56. package/build/email-settings-page.4bdbef9a.chunk.js +3 -0
  57. package/build/en-json.697b4bcf.chunk.js +1 -0
  58. package/build/{highlight.js.28a1547e.chunk.js → highlight.js.26ef649f.chunk.js} +2 -2
  59. package/build/i18n-settings-page.2bb5be96.chunk.js +1 -0
  60. package/build/index.html +1 -1
  61. package/build/main.af8c0f31.js +3790 -0
  62. package/build/review-workflows-settings.7a7dc773.chunk.js +57 -0
  63. package/build/runtime~main.5a95bee6.js +2 -0
  64. package/build/sso-settings-page.272b87c8.chunk.js +1 -0
  65. package/build/transfer-tokens-create-page.a1f14bb1.chunk.js +1 -0
  66. package/build/transfer-tokens-edit-page.00ee1c74.chunk.js +1 -0
  67. package/build/transfer-tokens-list-page.ce37354b.chunk.js +16 -0
  68. package/build/upload-settings.0875e973.chunk.js +1 -0
  69. package/build/{upload-translation-th-json.98d35574.chunk.js → upload-translation-th-json.3847dae0.chunk.js} +1 -1
  70. package/build/upload.c7da1611.chunk.js +13 -0
  71. package/build/users-advanced-settings-page.1d3c14c7.chunk.js +1 -0
  72. package/build/users-email-settings-page.e8db68c4.chunk.js +1 -0
  73. package/build/users-providers-settings-page.14cac425.chunk.js +1 -0
  74. package/build/users-roles-settings-page.2ea4de84.chunk.js +30 -0
  75. package/build/webhook-edit-page.329141a5.chunk.js +23 -0
  76. package/build/webhook-list-page.029957a4.chunk.js +1 -0
  77. package/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js +92 -0
  78. package/ee/admin/content-manager/pages/EditView/InformationBox/index.js +3 -0
  79. package/ee/admin/hooks/useSettingsMenu/utils/customAdminLinks.js +12 -12
  80. package/ee/admin/hooks/useSettingsMenu/utils/customGlobalLinks.js +21 -13
  81. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js +195 -0
  82. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/actions/index.js +42 -0
  83. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/AddStage/AddStage.js +87 -0
  84. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/AddStage/index.js +1 -0
  85. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +90 -0
  86. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/index.js +1 -0
  87. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stages.js +92 -0
  88. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/index.js +1 -0
  89. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js +6 -0
  90. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows.js +35 -0
  91. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/index.js +3 -0
  92. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js +121 -0
  93. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/getWorkflowValidationSchema.js +25 -0
  94. package/ee/admin/pages/SettingsPage/utils/customRoutes.js +16 -2
  95. package/ee/admin/permissions/customPermissions.js +3 -0
  96. package/ee/server/bootstrap.js +13 -0
  97. package/ee/server/config/admin-actions.js +10 -0
  98. package/ee/server/constants/default-stages.json +14 -0
  99. package/ee/server/constants/default-workflow.json +1 -0
  100. package/ee/server/constants/workflows.js +8 -0
  101. package/ee/server/content-types/index.js +9 -0
  102. package/ee/server/content-types/workflow/index.js +34 -0
  103. package/ee/server/content-types/workflow-stage/index.js +41 -0
  104. package/ee/server/controllers/index.js +2 -0
  105. package/ee/server/controllers/workflows/index.js +36 -0
  106. package/ee/server/controllers/workflows/stages/index.js +95 -0
  107. package/ee/server/index.js +1 -0
  108. package/ee/server/middlewares/review-workflows.js +40 -0
  109. package/ee/server/migrations/review-workflows.js +39 -0
  110. package/ee/server/register.js +9 -3
  111. package/ee/server/routes/index.js +104 -0
  112. package/ee/server/services/audit-logs.js +16 -75
  113. package/ee/server/services/index.js +4 -0
  114. package/ee/server/services/review-workflows/entity-service-decorator.js +42 -0
  115. package/ee/server/services/review-workflows/review-workflows.js +175 -0
  116. package/ee/server/services/review-workflows/stages.js +148 -0
  117. package/ee/server/services/review-workflows/workflows.js +25 -0
  118. package/ee/server/utils/index.js +8 -0
  119. package/ee/server/utils/review-workflows.js +25 -0
  120. package/ee/server/utils/test.js +11 -0
  121. package/ee/server/validation/review-workflows.js +24 -0
  122. package/jest.config.front.js +6 -1
  123. package/package.json +15 -17
  124. package/server/controllers/transfer/runner.js +2 -4
  125. package/server/middlewares/data-transfer.js +1 -4
  126. package/server/routes/transfer.js +4 -13
  127. package/server/services/constants.js +0 -4
  128. package/server/services/transfer/permission.js +1 -1
  129. package/server/services/transfer/token.js +31 -33
  130. package/server/validation/transfer/token.js +2 -10
  131. package/webpack.config.js +1 -1
  132. package/.eslintignore +0 -4
  133. package/.eslintrc.js +0 -14
  134. package/admin/src/components/LocalesProvider/__mocks__/useLocalesProvider.js +0 -7
  135. package/admin/src/hooks/useConfigurations/__mocks__/index.js +0 -7
  136. package/build/1387.84b454d3.chunk.js +0 -1
  137. package/build/1657.45231968.chunk.js +0 -168
  138. package/build/3081.bcf9a12f.chunk.js +0 -108
  139. package/build/462.8fff7f3b.chunk.js +0 -71
  140. package/build/4628.20631dd1.chunk.js +0 -1
  141. package/build/5542.b8240e3f.chunk.js +0 -70
  142. package/build/5563.905daa13.chunk.js +0 -79
  143. package/build/6404.68405699.chunk.js +0 -100
  144. package/build/7259.b7d00cea.chunk.js +0 -1
  145. package/build/8694.6522968d.chunk.js +0 -247
  146. package/build/9347.058ddb22.chunk.js +0 -1
  147. package/build/Admin-authenticatedApp.31bf88ef.chunk.js +0 -79
  148. package/build/Admin_InternalErrorPage.15c6bf07.chunk.js +0 -1
  149. package/build/Admin_homePage.da2181fe.chunk.js +0 -73
  150. package/build/Admin_marketplace.d99044eb.chunk.js +0 -31
  151. package/build/Admin_pluginsPage.f6b52ee9.chunk.js +0 -6
  152. package/build/Admin_profilePage.9112cffc.chunk.js +0 -15
  153. package/build/Admin_settingsPage.cb63220f.chunk.js +0 -79
  154. package/build/Upload_ConfigureTheView.eaaec495.chunk.js +0 -1
  155. package/build/admin-app.8cde5b22.chunk.js +0 -110
  156. package/build/admin-edit-roles-page.4f1858e9.chunk.js +0 -280
  157. package/build/admin-edit-users.7e14d85f.chunk.js +0 -10
  158. package/build/admin-roles-list.97e198f9.chunk.js +0 -31
  159. package/build/admin-users.d02de059.chunk.js +0 -34
  160. package/build/api-tokens-create-page.97595e12.chunk.js +0 -1
  161. package/build/api-tokens-edit-page.cd36e30e.chunk.js +0 -1
  162. package/build/api-tokens-list-page.6757c7b9.chunk.js +0 -16
  163. package/build/audit-logs-settings-page.ca9a3c46.chunk.js +0 -76
  164. package/build/content-manager.de0ee3e5.chunk.js +0 -1132
  165. package/build/content-type-builder-list-view.9c2c020c.chunk.js +0 -214
  166. package/build/content-type-builder-translation-en-json.e577d595.chunk.js +0 -1
  167. package/build/content-type-builder.ec5ac7ab.chunk.js +0 -126
  168. package/build/email-settings-page.1095e1ab.chunk.js +0 -10
  169. package/build/en-json.b052667a.chunk.js +0 -1
  170. package/build/i18n-settings-page.7d80aae0.chunk.js +0 -60
  171. package/build/main.d40f9ca1.js +0 -2280
  172. package/build/runtime~main.7cdc9956.js +0 -2
  173. package/build/sso-settings-page.1dd4886e.chunk.js +0 -1
  174. package/build/transfer-tokens-create-page.ec2ca215.chunk.js +0 -1
  175. package/build/transfer-tokens-edit-page.22bf28e5.chunk.js +0 -1
  176. package/build/transfer-tokens-list-page.cf8c77f2.chunk.js +0 -16
  177. package/build/upload-settings.945fdcfa.chunk.js +0 -13
  178. package/build/upload.a86b1054.chunk.js +0 -33
  179. package/build/users-advanced-settings-page.5b5a9baa.chunk.js +0 -8
  180. package/build/users-email-settings-page.e5506eb4.chunk.js +0 -23
  181. package/build/users-providers-settings-page.e32089c2.chunk.js +0 -28
  182. package/build/users-roles-settings-page.a5c5b0df.chunk.js +0 -30
  183. package/build/webhook-edit-page.213f0075.chunk.js +0 -75
  184. package/build/webhook-list-page.5beb2a5c.chunk.js +0 -71
@@ -122,4 +122,108 @@ module.exports = [
122
122
  ],
123
123
  },
124
124
  },
125
+
126
+ // Review workflow
127
+ {
128
+ method: 'GET',
129
+ path: '/review-workflows/workflows',
130
+ handler: 'workflows.find',
131
+ config: {
132
+ middlewares: [enableFeatureMiddleware('review-workflows')],
133
+ policies: [
134
+ 'admin::isAuthenticatedAdmin',
135
+ {
136
+ name: 'admin::hasPermissions',
137
+ config: {
138
+ actions: ['admin::review-workflows.read'],
139
+ },
140
+ },
141
+ ],
142
+ },
143
+ },
144
+ {
145
+ method: 'GET',
146
+ path: '/review-workflows/workflows/:id',
147
+ handler: 'workflows.findById',
148
+ config: {
149
+ middlewares: [enableFeatureMiddleware('review-workflows')],
150
+ policies: [
151
+ 'admin::isAuthenticatedAdmin',
152
+ {
153
+ name: 'admin::hasPermissions',
154
+ config: {
155
+ actions: ['admin::review-workflows.read'],
156
+ },
157
+ },
158
+ ],
159
+ },
160
+ },
161
+ {
162
+ method: 'GET',
163
+ path: '/review-workflows/workflows/:workflow_id/stages',
164
+ handler: 'stages.find',
165
+ config: {
166
+ middlewares: [enableFeatureMiddleware('review-workflows')],
167
+ policies: [
168
+ 'admin::isAuthenticatedAdmin',
169
+ {
170
+ name: 'admin::hasPermissions',
171
+ config: {
172
+ actions: ['admin::review-workflows.read'],
173
+ },
174
+ },
175
+ ],
176
+ },
177
+ },
178
+ {
179
+ method: 'PUT',
180
+ path: '/review-workflows/workflows/:workflow_id/stages',
181
+ handler: 'stages.replace',
182
+ config: {
183
+ middlewares: [enableFeatureMiddleware('review-workflows')],
184
+ policies: [
185
+ 'admin::isAuthenticatedAdmin',
186
+ {
187
+ name: 'admin::hasPermissions',
188
+ config: {
189
+ actions: ['admin::review-workflows.read'],
190
+ },
191
+ },
192
+ ],
193
+ },
194
+ },
195
+ {
196
+ method: 'GET',
197
+ path: '/review-workflows/workflows/:workflow_id/stages/:id',
198
+ handler: 'stages.findById',
199
+ config: {
200
+ middlewares: [enableFeatureMiddleware('review-workflows')],
201
+ policies: [
202
+ 'admin::isAuthenticatedAdmin',
203
+ {
204
+ name: 'admin::hasPermissions',
205
+ config: {
206
+ actions: ['admin::review-workflows.read'],
207
+ },
208
+ },
209
+ ],
210
+ },
211
+ },
212
+ {
213
+ method: 'PUT',
214
+ path: '/content-manager/(collection|single)-types/:model_uid/:id/stage',
215
+ handler: 'stages.updateEntity',
216
+ config: {
217
+ middlewares: [enableFeatureMiddleware('review-workflows')],
218
+ policies: [
219
+ 'admin::isAuthenticatedAdmin',
220
+ {
221
+ name: 'admin::hasPermissions',
222
+ config: {
223
+ actions: ['admin::review-workflows.read'],
224
+ },
225
+ },
226
+ ],
227
+ },
228
+ },
125
229
  ];
@@ -63,37 +63,16 @@ const getEventMap = (defaultEvents) => {
63
63
  }, {});
64
64
  };
65
65
 
66
- const getRetentionDays = (strapi) => {
67
- const licenseRetentionDays = features.get('audit-logs')?.options.retentionDays;
68
- const userRetentionDays = strapi.config.get('admin.auditLogs.retentionDays');
69
-
70
- // For enterprise plans, use 90 days by default, but allow users to override it
71
- if (licenseRetentionDays == null) {
72
- return userRetentionDays ?? DEFAULT_RETENTION_DAYS;
73
- }
74
-
75
- // Allow users to override the license retention days, but not to increase it
76
- if (userRetentionDays && userRetentionDays < licenseRetentionDays) {
77
- return userRetentionDays;
78
- }
79
-
80
- // User didn't provide a retention days value, use the license one
81
- return licenseRetentionDays;
82
- };
83
-
84
66
  const createAuditLogsService = (strapi) => {
85
- // Manage internal service state privately
86
- const state = {};
87
-
88
67
  // NOTE: providers should be able to replace getEventMap to add or remove events
89
68
  const eventMap = getEventMap(defaultEvents);
90
69
 
91
70
  const processEvent = (name, ...args) => {
92
- const requestState = strapi.requestContext.get()?.state;
71
+ const state = strapi.requestContext.get()?.state;
93
72
 
94
73
  // Ignore events with auth strategies different from admin
95
- const isUsingAdminAuth = requestState?.auth?.strategy.name === 'admin';
96
- const user = requestState?.user;
74
+ const isUsingAdminAuth = state?.auth?.strategy.name === 'admin';
75
+ const user = state?.user;
97
76
 
98
77
  if (!isUsingAdminAuth || !user) {
99
78
  return null;
@@ -124,60 +103,26 @@ const createAuditLogsService = (strapi) => {
124
103
  const processedEvent = processEvent(name, ...args);
125
104
 
126
105
  if (processedEvent) {
127
- await state.provider.saveEvent(processedEvent);
106
+ await this._provider.saveEvent(processedEvent);
128
107
  }
129
108
  }
130
109
 
131
110
  return {
132
111
  async register() {
133
- // Handle license being enabled
134
- if (!state.eeEnableUnsubscribe) {
135
- state.eeEnableUnsubscribe = strapi.eventHub.on('ee.enable', () => {
136
- // Recreate the service to use the new license info
137
- this.destroy();
138
- this.register();
139
- });
140
- }
141
-
142
- // Handle license being updated
143
- if (!state.eeUpdateUnsubscribe) {
144
- state.eeUpdateUnsubscribe = strapi.eventHub.on('ee.update', () => {
145
- // Recreate the service to use the new license info
146
- this.destroy();
147
- this.register();
148
- });
149
- }
150
-
151
- // Handle license being disabled
152
- state.eeDisableUnsubscribe = strapi.eventHub.on('ee.disable', () => {
153
- // Turn off service when the license gets disabled
154
- // Only ee.enable and ee.update listeners remain active to recreate the service
155
- this.destroy();
156
- });
157
-
158
- // Register the provider now because collections can't be added later at runtime
159
- state.provider = await localProvider.register({ strapi });
160
-
161
- // Check current state of license
162
- if (!features.isEnabled('audit-logs')) {
163
- return this;
164
- }
165
-
166
- // Start saving events
167
- state.eventHubUnsubscribe = strapi.eventHub.subscribe(handleEvent.bind(this));
168
-
169
- // Manage audit logs auto deletion
170
- const retentionDays = getRetentionDays(strapi);
171
- state.deleteExpiredJob = scheduleJob('0 0 * * *', () => {
112
+ const retentionDays =
113
+ features.get('audit-logs')?.options.retentionDays ?? DEFAULT_RETENTION_DAYS;
114
+ this._provider = await localProvider.register({ strapi });
115
+ this._eventHubUnsubscribe = strapi.eventHub.subscribe(handleEvent.bind(this));
116
+ this._deleteExpiredJob = scheduleJob('0 0 * * *', () => {
172
117
  const expirationDate = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);
173
- state.provider.deleteExpiredEvents(expirationDate);
118
+ this._provider.deleteExpiredEvents(expirationDate);
174
119
  });
175
120
 
176
121
  return this;
177
122
  },
178
123
 
179
124
  async findMany(query) {
180
- const { results, pagination } = await state.provider.findMany(query);
125
+ const { results, pagination } = await this._provider.findMany(query);
181
126
 
182
127
  const sanitizedResults = results.map((result) => {
183
128
  const { user, ...rest } = result;
@@ -194,7 +139,7 @@ const createAuditLogsService = (strapi) => {
194
139
  },
195
140
 
196
141
  async findOne(id) {
197
- const result = await state.provider.findOne(id);
142
+ const result = await this._provider.findOne(id);
198
143
 
199
144
  if (!result) {
200
145
  return null;
@@ -208,16 +153,12 @@ const createAuditLogsService = (strapi) => {
208
153
  },
209
154
 
210
155
  unsubscribe() {
211
- if (state.eeDisableUnsubscribe) {
212
- state.eeDisableUnsubscribe();
213
- }
214
-
215
- if (state.eventHubUnsubscribe) {
216
- state.eventHubUnsubscribe();
156
+ if (this._eventHubUnsubscribe) {
157
+ this._eventHubUnsubscribe();
217
158
  }
218
159
 
219
- if (state.deleteExpiredJob) {
220
- state.deleteExpiredJob.cancel();
160
+ if (this._deleteExpiredJob) {
161
+ this._deleteExpiredJob.cancel();
221
162
  }
222
163
 
223
164
  return this;
@@ -5,4 +5,8 @@ module.exports = {
5
5
  role: require('./role'),
6
6
  user: require('./user'),
7
7
  'seat-enforcement': require('./seat-enforcement'),
8
+ workflows: require('./review-workflows/workflows'),
9
+ stages: require('./review-workflows/stages'),
10
+ 'review-workflows': require('./review-workflows/review-workflows'),
11
+ 'review-workflows-decorator': require('./review-workflows/entity-service-decorator'),
8
12
  };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const { isNil } = require('lodash/fp');
4
+ const { ENTITY_STAGE_ATTRIBUTE } = require('../../constants/workflows');
5
+ const { hasReviewWorkflow, getDefaultWorkflow } = require('../../utils/review-workflows');
6
+
7
+ /**
8
+ * Assigns the entity data to the default workflow stage if no stage is present in the data
9
+ * @param {Object} data
10
+ * @returns
11
+ */
12
+ const getDataWithStage = async (data) => {
13
+ if (!isNil(ENTITY_STAGE_ATTRIBUTE, data)) {
14
+ const defaultWorkflow = await getDefaultWorkflow({ strapi });
15
+ return { ...data, [ENTITY_STAGE_ATTRIBUTE]: defaultWorkflow.stages[0].id };
16
+ }
17
+ return data;
18
+ };
19
+
20
+ /**
21
+ * Decorates the entity service with RW business logic
22
+ * @param {object} service - entity service
23
+ */
24
+ const decorator = (service) => ({
25
+ async create(uid, opts = {}) {
26
+ const hasRW = hasReviewWorkflow({ strapi }, uid);
27
+
28
+ if (!hasRW) {
29
+ return service.create.call(this, uid, opts);
30
+ }
31
+
32
+ const data = await getDataWithStage(opts.data);
33
+ return service.create.call(this, uid, {
34
+ ...opts,
35
+ data,
36
+ });
37
+ },
38
+ });
39
+
40
+ module.exports = () => ({
41
+ decorator,
42
+ });
@@ -0,0 +1,175 @@
1
+ 'use strict';
2
+
3
+ const { set, get, forEach, keys, pickBy, pipe } = require('lodash/fp');
4
+ const { mapAsync } = require('@strapi/utils');
5
+ const { getService } = require('../../utils');
6
+
7
+ const defaultStages = require('../../constants/default-stages.json');
8
+ const defaultWorkflow = require('../../constants/default-workflow.json');
9
+ const { ENTITY_STAGE_ATTRIBUTE } = require('../../constants/workflows');
10
+
11
+ const {
12
+ disableOnContentTypes: disableReviewWorkflows,
13
+ } = require('../../migrations/review-workflows');
14
+ const { getDefaultWorkflow } = require('../../utils/review-workflows');
15
+
16
+ const getContentTypeUIDsWithActivatedReviewWorkflows = pipe([
17
+ // Pick only content-types with reviewWorkflows options set to true
18
+ pickBy(get('options.reviewWorkflows')),
19
+ // Get UIDs
20
+ keys,
21
+ ]);
22
+
23
+ /**
24
+ * Map every stage in the array to be ordered in the relation
25
+ * @param {Object[]} stages
26
+ * @param {number} stages.id
27
+ * @return {Object[]}
28
+ */
29
+ function buildStagesConnectArray(stages) {
30
+ return stages.map((stage, index) => {
31
+ const connect = {
32
+ id: stage.id,
33
+ position: {},
34
+ };
35
+
36
+ if (index === 0) {
37
+ connect.position.start = true;
38
+ } else {
39
+ connect.position.after = stages[index - 1].id;
40
+ }
41
+ return connect;
42
+ });
43
+ }
44
+
45
+ async function initDefaultWorkflow({ workflowsService, stagesService, strapi }) {
46
+ const wfCount = await workflowsService.count();
47
+ const stagesCount = await stagesService.count();
48
+
49
+ // Check if there is nothing about review-workflow in DB
50
+ // If any, the feature has already been initialized with a workflow and stages
51
+ if (wfCount === 0 && stagesCount === 0) {
52
+ const stages = await stagesService.createMany(defaultStages, { fields: ['id'] });
53
+ const workflow = {
54
+ ...defaultWorkflow,
55
+ stages: {
56
+ connect: buildStagesConnectArray(stages),
57
+ },
58
+ };
59
+
60
+ await workflowsService.create(workflow);
61
+ // If there is any manually activated RW on content-types, we want to migrate the related entities
62
+ await enableReviewWorkflow({ strapi })({ contentTypes: strapi.contentTypes });
63
+ }
64
+ }
65
+
66
+ const setStageAttribute = set(`attributes.${ENTITY_STAGE_ATTRIBUTE}`, {
67
+ writable: true,
68
+ private: false,
69
+ configurable: false,
70
+ visible: false,
71
+ type: 'relation',
72
+ relation: 'morphOne',
73
+ target: 'admin::workflow-stage',
74
+ morphBy: 'related',
75
+ });
76
+
77
+ function extendReviewWorkflowContentTypes({ strapi }) {
78
+ const extendContentType = (contentTypeUID) => {
79
+ strapi.container.get('content-types').extend(contentTypeUID, setStageAttribute);
80
+ };
81
+ pipe([
82
+ getContentTypeUIDsWithActivatedReviewWorkflows,
83
+ // Iterate over UIDs to extend the content-type
84
+ forEach(extendContentType),
85
+ ])(strapi.contentTypes);
86
+ }
87
+
88
+ /**
89
+ * Enables the review workflow for the given content types.
90
+ * @param {Object} strapi - Strapi instance
91
+ */
92
+ function enableReviewWorkflow({ strapi }) {
93
+ /**
94
+ * @param {Array<string>} contentTypes - Content type UIDs to enable the review workflow for.
95
+ * @returns {Promise<void>} - Promise that resolves when the review workflow is enabled.
96
+ */
97
+ return async ({ contentTypes }) => {
98
+ const defaultWorkflow = await getDefaultWorkflow({ strapi });
99
+ // This is possible if this is the first start of EE, there won't be any workflow in DB before bootstrap
100
+ if (!defaultWorkflow) {
101
+ return;
102
+ }
103
+ const firstStage = defaultWorkflow.stages[0];
104
+
105
+ const up = async (contentTypeUID) => {
106
+ const contentTypeMetadata = strapi.db.metadata.get(contentTypeUID);
107
+ const { target, morphBy } = contentTypeMetadata.attributes[ENTITY_STAGE_ATTRIBUTE];
108
+ const { joinTable } = strapi.db.metadata.get(target).attributes[morphBy];
109
+ const { idColumn, typeColumn } = joinTable.morphColumn;
110
+
111
+ // Execute an SQL query to insert records into the join table mapping the specified content type with the first stage of the default workflow.
112
+ // Only entities that do not have a record in the join table yet are selected.
113
+ const selectStatement = strapi.db
114
+ .getConnection()
115
+ .select({
116
+ [idColumn.name]: 'entity.id',
117
+ field: strapi.db.connection.raw('?', [ENTITY_STAGE_ATTRIBUTE]),
118
+ order: 1,
119
+ [joinTable.joinColumn.name]: firstStage.id,
120
+ [typeColumn.name]: strapi.db.connection.raw('?', [contentTypeUID]),
121
+ })
122
+ .leftJoin(`${joinTable.name} AS jointable`, function joinFunc() {
123
+ this.on('entity.id', '=', `jointable.${idColumn.name}`).andOn(
124
+ `jointable.${typeColumn.name}`,
125
+ '=',
126
+ strapi.db.connection.raw('?', [contentTypeUID])
127
+ );
128
+ })
129
+ .where(`jointable.${idColumn.name}`, null)
130
+ .from(`${contentTypeMetadata.tableName} AS entity`)
131
+ .toSQL();
132
+
133
+ const columnsToInsert = [
134
+ idColumn.name,
135
+ 'field',
136
+ strapi.db.connection.raw('??', ['order']),
137
+ joinTable.joinColumn.name,
138
+ typeColumn.name,
139
+ ];
140
+
141
+ // Insert rows for all entries of the content type that do not have a
142
+ // default stage
143
+ await strapi.db
144
+ .getConnection(joinTable.name)
145
+ .insert(
146
+ strapi.db.connection.raw(
147
+ `(${columnsToInsert.join(',')}) ${selectStatement.sql}`,
148
+ selectStatement.bindings
149
+ )
150
+ );
151
+ };
152
+
153
+ return pipe([
154
+ getContentTypeUIDsWithActivatedReviewWorkflows,
155
+ // Iterate over UIDs to extend the content-type
156
+ (contentTypesUIDs) => mapAsync(contentTypesUIDs, up),
157
+ ])(contentTypes);
158
+ };
159
+ }
160
+
161
+ module.exports = ({ strapi }) => {
162
+ const workflowsService = getService('workflows', { strapi });
163
+ const stagesService = getService('stages', { strapi });
164
+
165
+ return {
166
+ async bootstrap() {
167
+ await initDefaultWorkflow({ workflowsService, stagesService, strapi });
168
+ },
169
+ async register() {
170
+ extendReviewWorkflowContentTypes({ strapi });
171
+ strapi.hook('strapi::content-types.afterSync').register(enableReviewWorkflow({ strapi }));
172
+ strapi.hook('strapi::content-types.afterSync').register(disableReviewWorkflows);
173
+ },
174
+ };
175
+ };
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ mapAsync,
5
+ errors: { ApplicationError },
6
+ } = require('@strapi/utils');
7
+
8
+ const { STAGE_MODEL_UID, ENTITY_STAGE_ATTRIBUTE } = require('../../constants/workflows');
9
+ const { getService } = require('../../utils');
10
+
11
+ module.exports = ({ strapi }) => {
12
+ const workflowsService = getService('workflows', { strapi });
13
+
14
+ return {
15
+ find({ workflowId, populate }) {
16
+ const params = {
17
+ filters: { workflow: workflowId },
18
+ populate,
19
+ };
20
+ return strapi.entityService.findMany(STAGE_MODEL_UID, params);
21
+ },
22
+
23
+ findById(id, { workflowId, populate }) {
24
+ const params = {
25
+ filters: { workflow: workflowId },
26
+ populate,
27
+ };
28
+ return strapi.entityService.findOne(STAGE_MODEL_UID, id, params);
29
+ },
30
+
31
+ createMany(stagesList, { fields }) {
32
+ const params = {
33
+ select: fields,
34
+ };
35
+ return Promise.all(
36
+ stagesList.map((stage) =>
37
+ strapi.entityService.create(STAGE_MODEL_UID, { data: stage, ...params })
38
+ )
39
+ );
40
+ },
41
+
42
+ update(stageId, stageData) {
43
+ return strapi.entityService.update(STAGE_MODEL_UID, stageId, { data: stageData });
44
+ },
45
+
46
+ delete(stageId) {
47
+ return strapi.entityService.delete(STAGE_MODEL_UID, stageId);
48
+ },
49
+
50
+ count() {
51
+ return strapi.entityService.count(STAGE_MODEL_UID);
52
+ },
53
+
54
+ async replaceWorkflowStages(workflowId, stages) {
55
+ const workflow = await workflowsService.findById(workflowId, { populate: ['stages'] });
56
+
57
+ const { created, updated, deleted } = getDiffBetweenStages(workflow.stages, stages);
58
+
59
+ assertAtLeastOneStageRemain(workflow.stages, { created, deleted });
60
+
61
+ return strapi.db.transaction(async () => {
62
+ const newStages = await this.createMany(created, { fields: ['id'] });
63
+ const stagesIds = stages.map((stage) => stage.id ?? newStages.shift().id);
64
+
65
+ await mapAsync(updated, (stage) => this.update(stage.id, stage));
66
+ await mapAsync(deleted, (stage) => this.delete(stage.id));
67
+ return workflowsService.update(workflowId, {
68
+ stages: stagesIds,
69
+ });
70
+ });
71
+ },
72
+
73
+ /**
74
+ * Update the stage of an entity
75
+ *
76
+ * @param {object} entityInfo
77
+ * @param {number} entityInfo.id - Entity id
78
+ * @param {string} entityInfo.modelUID - the content-type of the entity
79
+ * @param {number} stageId - The id of the stage to assign to the entity
80
+ */
81
+ updateEntity(entityInfo, stageId) {
82
+ return strapi.entityService.update(entityInfo.modelUID, entityInfo.id, {
83
+ data: { [ENTITY_STAGE_ATTRIBUTE]: stageId },
84
+ populate: [ENTITY_STAGE_ATTRIBUTE],
85
+ });
86
+ },
87
+ };
88
+ };
89
+
90
+ /**
91
+ * Compares two arrays of stages and returns an object indicating the differences.
92
+ *
93
+ * The function compares the `id` properties of each stage in `sourceStages` and `comparisonStages` to determine if the stage is present in both arrays.
94
+ * If a stage with the same `id` is found in both arrays but the `name` property is different, the stage is considered updated.
95
+ * If a stage with a particular `id` is only found in `comparisonStages`, it is considered created.
96
+ * If a stage with a particular `id` is only found in `sourceStages`, it is considered deleted.
97
+ *
98
+ * @typedef {{id: Number, name: String, workflow: Number}} Stage
99
+ * @typedef {{created: Stage[], updated: Stage[], deleted: Stage[]}} DiffStages
100
+ *
101
+ * The DiffStages object has three properties: `created`, `updated`, and `deleted`.
102
+ * `created` is an array of stages that are in `comparisonStages` but not in `sourceStages`.
103
+ * `updated` is an array of stages that have different names in `comparisonStages` and `sourceStages`.
104
+ * `deleted` is an array of stages that are in `sourceStages` but not in `comparisonStages`.
105
+ *
106
+ * @param {Stage[]} sourceStages
107
+ * @param {Stage[]} comparisonStages
108
+ * @returns { DiffStages }
109
+ */
110
+ function getDiffBetweenStages(sourceStages, comparisonStages) {
111
+ const result = comparisonStages.reduce(
112
+ (acc, stageToCompare) => {
113
+ const srcStage = sourceStages.find((stage) => stage.id === stageToCompare.id);
114
+
115
+ if (!srcStage) {
116
+ acc.created.push(stageToCompare);
117
+ } else if (srcStage.name !== stageToCompare.name) {
118
+ acc.updated.push(stageToCompare);
119
+ }
120
+ return acc;
121
+ },
122
+ { created: [], updated: [] }
123
+ );
124
+
125
+ result.deleted = sourceStages.filter(
126
+ (srcStage) => !comparisonStages.some((cmpStage) => cmpStage.id === srcStage.id)
127
+ );
128
+
129
+ return result;
130
+ }
131
+
132
+ /**
133
+ * Asserts that at least one stage remains in the workflow after applying deletions and additions.
134
+ *
135
+ * @param {Array} workflowStages - An array of stages in the current workflow.
136
+ * @param {Object} diffStages - An object containing the stages to be deleted and created.
137
+ * @param {Array} diffStages.deleted - An array of stages that are planned to be deleted from the workflow.
138
+ * @param {Array} diffStages.created - An array of stages that are planned to be created in the workflow.
139
+ *
140
+ * @throws {ApplicationError} If the number of remaining stages in the workflow after applying deletions and additions is less than 1.
141
+ */
142
+ function assertAtLeastOneStageRemain(workflowStages, diffStages) {
143
+ const remainingStagesCount =
144
+ workflowStages.length - diffStages.deleted.length + diffStages.created.length;
145
+ if (remainingStagesCount < 1) {
146
+ throw new ApplicationError('At least one stage must remain in the workflow.');
147
+ }
148
+ }
@@ -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
+ });
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ const getService = (name, { strapi } = { strapi: global.strapi }) => {
4
+ return strapi.service(`admin::${name}`);
5
+ };
6
+ module.exports = {
7
+ getService,
8
+ };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const { WORKFLOW_MODEL_UID } = require('../constants/workflows');
4
+
5
+ /**
6
+ * Checks if a content type has review workflows enabled.
7
+ * @param {string|Object} contentType - Either the modelUID of the content type, or the content type object.
8
+ * @returns {boolean} Whether review workflows are enabled for the specified content type.
9
+ */
10
+ function hasReviewWorkflow({ strapi }, contentType) {
11
+ if (typeof contentType === 'string') {
12
+ // If the input is a string, assume it's the modelUID of the content type and retrieve the corresponding object.
13
+ return hasReviewWorkflow({ strapi }, strapi.getModel(contentType));
14
+ }
15
+ // Otherwise, assume it's the content type object itself and return its `reviewWorkflows` option if it exists.
16
+ return contentType?.options?.reviewWorkflows || false;
17
+ }
18
+ // TODO To be refactored when multiple workflows are added
19
+ const getDefaultWorkflow = async ({ strapi }) =>
20
+ strapi.query(WORKFLOW_MODEL_UID).findOne({ populate: ['stages'] });
21
+
22
+ module.exports = {
23
+ hasReviewWorkflow,
24
+ getDefaultWorkflow,
25
+ };