@strapi/admin 4.10.0-beta.0 → 4.10.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/.eslintignore +4 -0
  2. package/.eslintrc.js +14 -0
  3. package/admin/src/components/LanguageProvider/index.js +1 -1
  4. package/admin/src/components/LocalesProvider/__mocks__/useLocalesProvider.js +7 -0
  5. package/admin/src/components/LocalesProvider/index.js +2 -3
  6. package/admin/src/content-manager/components/CollectionTypeFormWrapper/index.js +3 -6
  7. package/admin/src/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +2 -0
  8. package/admin/src/content-manager/components/DynamicTable/index.js +11 -29
  9. package/admin/src/content-manager/components/EditViewDataManagerProvider/reducer.js +12 -6
  10. package/admin/src/content-manager/components/EditViewDataManagerProvider/utils/findAllAndReplace.js +10 -3
  11. package/admin/src/content-manager/components/InputUID/endActionStyle.js +4 -13
  12. package/admin/src/content-manager/components/InputUID/index.js +95 -72
  13. package/admin/src/content-manager/components/SingleTypeFormWrapper/index.js +4 -8
  14. package/admin/src/content-manager/pages/App/LeftMenu/index.js +56 -35
  15. package/admin/src/content-manager/pages/App/actions.js +19 -7
  16. package/admin/src/content-manager/pages/App/constants.js +3 -3
  17. package/admin/src/content-manager/pages/App/index.js +5 -4
  18. package/admin/src/content-manager/pages/App/reducer.js +7 -6
  19. package/admin/src/content-manager/pages/App/selectors.js +3 -0
  20. package/admin/src/content-manager/pages/App/{useModels.js → useContentManagerInitData.js} +29 -28
  21. package/admin/src/content-manager/pages/App/utils/generateModelsLinks.js +2 -2
  22. package/admin/src/content-manager/pages/App/utils/getContentTypeLinks.js +17 -15
  23. package/admin/src/content-manager/pages/EditSettingsView/components/ModalForm.js +4 -5
  24. package/admin/src/content-manager/pages/EditSettingsView/index.js +4 -0
  25. package/admin/src/content-manager/pages/EditSettingsView/reducer.js +6 -7
  26. package/admin/src/content-manager/pages/EditSettingsView/utils/layout.js +1 -30
  27. package/admin/src/content-manager/pages/EditView/DeleteLink/index.js +5 -11
  28. package/admin/src/content-manager/pages/EditView/index.js +2 -7
  29. package/admin/src/content-manager/pages/ListSettingsView/index.js +1 -0
  30. package/admin/src/content-manager/sharedReducers/crudReducer/actions.js +6 -0
  31. package/admin/src/content-manager/sharedReducers/crudReducer/constants.js +1 -0
  32. package/admin/src/content-manager/sharedReducers/crudReducer/reducer.js +5 -0
  33. package/admin/src/hooks/useConfigurations/__mocks__/index.js +7 -0
  34. package/admin/src/hooks/useFetchMarketplacePlugins/utils/api.js +7 -8
  35. package/admin/src/hooks/useFetchMarketplaceProviders/utils/api.js +5 -0
  36. package/admin/src/hooks/useRegenerate/index.js +12 -7
  37. package/admin/src/pages/AuthPage/components/Register/index.js +46 -38
  38. package/admin/src/pages/HomePage/SocialLinks.js +1 -1
  39. package/admin/src/pages/MarketplacePage/components/EmptyNpmPackageSearch/index.js +3 -3
  40. package/admin/src/pages/MarketplacePage/components/NpmPackagesGrid/index.js +42 -9
  41. package/admin/src/pages/MarketplacePage/components/NpmPackagesPagination/index.js +26 -0
  42. package/admin/src/pages/MarketplacePage/components/OfflineLayout/index.js +45 -0
  43. package/admin/src/pages/MarketplacePage/index.js +80 -175
  44. package/admin/src/pages/MarketplacePage/utils/useMarketplaceData.js +85 -0
  45. package/admin/src/pages/SettingsPage/components/Tokens/FormHead/index.js +4 -0
  46. package/admin/src/pages/SettingsPage/components/Tokens/Regenerate/index.js +5 -3
  47. package/admin/src/pages/SettingsPage/components/Tokens/TokenTypeSelect/index.js +7 -5
  48. package/admin/src/pages/SettingsPage/pages/Roles/ListPage/components/RoleRow/index.js +12 -4
  49. package/admin/src/pages/SettingsPage/pages/Roles/ListPage/index.js +21 -2
  50. package/admin/src/pages/SettingsPage/pages/TransferTokens/EditView/components/FormTransferTokenContainer/index.js +41 -0
  51. package/admin/src/pages/SettingsPage/pages/TransferTokens/EditView/index.js +53 -9
  52. package/admin/src/pages/SettingsPage/pages/TransferTokens/EditView/utils/schema.js +1 -0
  53. package/admin/src/pages/SettingsPage/pages/TransferTokens/ListView/index.js +27 -5
  54. package/admin/src/translations/en.json +51 -49
  55. package/admin/src/translations/ru.json +51 -19
  56. package/build/1387.84b454d3.chunk.js +1 -0
  57. package/build/1657.45231968.chunk.js +168 -0
  58. package/build/3081.bcf9a12f.chunk.js +108 -0
  59. package/build/462.8fff7f3b.chunk.js +71 -0
  60. package/build/4628.20631dd1.chunk.js +1 -0
  61. package/build/5542.b8240e3f.chunk.js +70 -0
  62. package/build/5563.905daa13.chunk.js +79 -0
  63. package/build/6404.68405699.chunk.js +100 -0
  64. package/build/7259.b7d00cea.chunk.js +1 -0
  65. package/build/8694.6522968d.chunk.js +247 -0
  66. package/build/Admin-authenticatedApp.52c88751.chunk.js +79 -0
  67. package/build/Admin_InternalErrorPage.15c6bf07.chunk.js +1 -0
  68. package/build/Admin_homePage.f9309c6d.chunk.js +73 -0
  69. package/build/Admin_marketplace.56bc1008.chunk.js +31 -0
  70. package/build/Admin_pluginsPage.f6b52ee9.chunk.js +6 -0
  71. package/build/Admin_profilePage.9112cffc.chunk.js +15 -0
  72. package/build/Admin_settingsPage.257b3477.chunk.js +79 -0
  73. package/build/Upload_ConfigureTheView.eaaec495.chunk.js +1 -0
  74. package/build/admin-app.dfaeea5d.chunk.js +110 -0
  75. package/build/admin-edit-roles-page.4f1858e9.chunk.js +280 -0
  76. package/build/admin-edit-users.7e14d85f.chunk.js +10 -0
  77. package/build/admin-roles-list.329c1f63.chunk.js +31 -0
  78. package/build/admin-users.d02de059.chunk.js +34 -0
  79. package/build/api-tokens-create-page.97595e12.chunk.js +1 -0
  80. package/build/api-tokens-edit-page.cd36e30e.chunk.js +1 -0
  81. package/build/api-tokens-list-page.6757c7b9.chunk.js +16 -0
  82. package/build/audit-logs-settings-page.19d90bda.chunk.js +76 -0
  83. package/build/content-manager.def692c2.chunk.js +1130 -0
  84. package/build/content-type-builder-list-view.9c2c020c.chunk.js +214 -0
  85. package/build/content-type-builder-translation-en-json.510e88ca.chunk.js +1 -0
  86. package/build/content-type-builder.5e1f4afc.chunk.js +126 -0
  87. package/build/email-settings-page.1095e1ab.chunk.js +10 -0
  88. package/build/en-json.08303b37.chunk.js +1 -0
  89. package/build/{highlight.js.26ef649f.chunk.js → highlight.js.28a1547e.chunk.js} +2 -2
  90. package/build/i18n-settings-page.7d80aae0.chunk.js +60 -0
  91. package/build/index.html +1 -1
  92. package/build/main.120be100.js +2280 -0
  93. package/build/review-workflows-settings.9092ed72.chunk.js +106 -0
  94. package/build/ru-json.e0662702.chunk.js +1 -0
  95. package/build/{runtime~main.5a95bee6.js → runtime~main.112b3101.js} +2 -2
  96. package/build/sso-settings-page.1dd4886e.chunk.js +1 -0
  97. package/build/transfer-tokens-create-page.ec2ca215.chunk.js +1 -0
  98. package/build/transfer-tokens-edit-page.22bf28e5.chunk.js +1 -0
  99. package/build/transfer-tokens-list-page.cf8c77f2.chunk.js +16 -0
  100. package/build/upload-settings.945fdcfa.chunk.js +13 -0
  101. package/build/{upload-translation-th-json.3847dae0.chunk.js → upload-translation-th-json.98d35574.chunk.js} +1 -1
  102. package/build/upload.a86b1054.chunk.js +33 -0
  103. package/build/users-advanced-settings-page.5b5a9baa.chunk.js +8 -0
  104. package/build/users-email-settings-page.e5506eb4.chunk.js +23 -0
  105. package/build/users-providers-settings-page.e32089c2.chunk.js +28 -0
  106. package/build/users-roles-settings-page.2f85dcec.chunk.js +30 -0
  107. package/build/webhook-edit-page.213f0075.chunk.js +75 -0
  108. package/build/webhook-list-page.5beb2a5c.chunk.js +71 -0
  109. package/{admin/src/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/ReviewWorkflowsStage.js → ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/ReviewWorkflowsStageEE.js} +2 -2
  110. package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +45 -0
  111. package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/index.js +3 -0
  112. package/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js +61 -18
  113. package/ee/admin/hooks/useLicenseLimitNotification/index.js +1 -1
  114. package/ee/admin/pages/SettingsPage/pages/AuditLogs/ListView/hooks/useAuditLogsData.js +6 -3
  115. package/ee/admin/pages/SettingsPage/pages/AuditLogs/ListView/index.js +15 -5
  116. package/ee/admin/pages/SettingsPage/pages/AuditLogs/ListView/utils/getDisplayedFilters.js +52 -45
  117. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js +8 -4
  118. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +2 -2
  119. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows.js +2 -2
  120. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js +2 -1
  121. package/ee/server/content-types/workflow/index.js +0 -3
  122. package/ee/server/content-types/workflow-stage/index.js +0 -5
  123. package/ee/server/controllers/workflows/stages/index.js +8 -1
  124. package/ee/server/register.js +3 -1
  125. package/ee/server/services/audit-logs.js +75 -16
  126. package/ee/server/services/review-workflows/entity-service-decorator.js +17 -5
  127. package/ee/server/services/review-workflows/review-workflows.js +27 -91
  128. package/ee/server/services/review-workflows/stages.js +108 -7
  129. package/ee/server/utils/persisted-tables.js +114 -22
  130. package/ee/server/utils/review-workflows.js +9 -0
  131. package/jest.config.front.js +1 -6
  132. package/package.json +24 -22
  133. package/server/controllers/transfer/runner.js +4 -2
  134. package/server/middlewares/data-transfer.js +4 -1
  135. package/server/routes/transfer.js +13 -4
  136. package/server/services/constants.js +4 -0
  137. package/server/services/transfer/permission.js +1 -1
  138. package/server/services/transfer/token.js +33 -31
  139. package/server/validation/transfer/token.js +10 -2
  140. package/webpack.config.js +6 -2
  141. package/admin/src/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/index.js +0 -1
  142. package/admin/src/content-manager/pages/App/LeftMenu/utils/index.js +0 -1
  143. package/admin/src/content-manager/pages/App/LeftMenu/utils/matchByTitle.js +0 -24
  144. package/build/2263.4c5916f9.chunk.js +0 -98
  145. package/build/4049.64715f20.chunk.js +0 -1
  146. package/build/4649.213b8a3b.chunk.js +0 -30
  147. package/build/6985.66cca29c.chunk.js +0 -1
  148. package/build/7259.aefb51e8.chunk.js +0 -1
  149. package/build/8469.853c822b.chunk.js +0 -1
  150. package/build/9505.dbe702ab.chunk.js +0 -14
  151. package/build/9816.01ee964f.chunk.js +0 -2
  152. package/build/Admin-authenticatedApp.f50ad423.chunk.js +0 -79
  153. package/build/Admin_InternalErrorPage.4ad8b0df.chunk.js +0 -1
  154. package/build/Admin_homePage.1411fb7c.chunk.js +0 -68
  155. package/build/Admin_marketplace.02608d56.chunk.js +0 -22
  156. package/build/Admin_pluginsPage.15e3b0fd.chunk.js +0 -1
  157. package/build/Admin_profilePage.76afeca0.chunk.js +0 -15
  158. package/build/Admin_settingsPage.147755cd.chunk.js +0 -9
  159. package/build/Upload_ConfigureTheView.34dde278.chunk.js +0 -1
  160. package/build/admin-app.55dd7921.chunk.js +0 -112
  161. package/build/admin-edit-roles-page.cf543488.chunk.js +0 -216
  162. package/build/admin-edit-users.31c20712.chunk.js +0 -10
  163. package/build/admin-roles-list.489c501f.chunk.js +0 -2
  164. package/build/admin-users.3e111a7d.chunk.js +0 -11
  165. package/build/api-tokens-create-page.4328b852.chunk.js +0 -1
  166. package/build/api-tokens-edit-page.bce5050f.chunk.js +0 -1
  167. package/build/api-tokens-list-page.93f24348.chunk.js +0 -16
  168. package/build/audit-logs-settings-page.7be97e82.chunk.js +0 -1
  169. package/build/content-manager.4480ae88.chunk.js +0 -1137
  170. package/build/content-type-builder-list-view.cf38fe2f.chunk.js +0 -191
  171. package/build/content-type-builder-translation-en-json.7961593e.chunk.js +0 -1
  172. package/build/content-type-builder.af9abf1e.chunk.js +0 -126
  173. package/build/email-settings-page.4bdbef9a.chunk.js +0 -3
  174. package/build/en-json.697b4bcf.chunk.js +0 -1
  175. package/build/i18n-settings-page.2bb5be96.chunk.js +0 -1
  176. package/build/main.af8c0f31.js +0 -3790
  177. package/build/review-workflows-settings.7a7dc773.chunk.js +0 -57
  178. package/build/ru-json.6a01cea6.chunk.js +0 -1
  179. package/build/sso-settings-page.272b87c8.chunk.js +0 -1
  180. package/build/transfer-tokens-create-page.a1f14bb1.chunk.js +0 -1
  181. package/build/transfer-tokens-edit-page.00ee1c74.chunk.js +0 -1
  182. package/build/transfer-tokens-list-page.ce37354b.chunk.js +0 -16
  183. package/build/upload-settings.0875e973.chunk.js +0 -1
  184. package/build/upload.c7da1611.chunk.js +0 -13
  185. package/build/users-advanced-settings-page.1d3c14c7.chunk.js +0 -1
  186. package/build/users-email-settings-page.e8db68c4.chunk.js +0 -1
  187. package/build/users-providers-settings-page.14cac425.chunk.js +0 -1
  188. package/build/users-roles-settings-page.2ea4de84.chunk.js +0 -30
  189. package/build/webhook-edit-page.329141a5.chunk.js +0 -23
  190. package/build/webhook-list-page.029957a4.chunk.js +0 -1
  191. package/ee/server/migrations/review-workflows.js +0 -39
  192. package/ee/server/utils/test.js +0 -11
  193. /package/admin/src/{content-manager/components/InputUID/useDebounce.js → hooks/useDebounce/index.js} +0 -0
@@ -45,7 +45,7 @@ function Stage({ id, name, index, canDelete, isOpen: isOpenDefault = false }) {
45
45
  action={
46
46
  canDelete ? (
47
47
  <IconButton
48
- backgroundColor="transparent"
48
+ background="transparent"
49
49
  noBorder
50
50
  onClick={() => dispatch(deleteStage(id))}
51
51
  label={formatMessage({
@@ -57,7 +57,7 @@ function Stage({ id, name, index, canDelete, isOpen: isOpenDefault = false }) {
57
57
  ) : null
58
58
  }
59
59
  />
60
- <AccordionContent padding={6} background="neutral0">
60
+ <AccordionContent padding={6} background="neutral0" hasRadius>
61
61
  <Grid gap={4}>
62
62
  <GridItem col={6}>
63
63
  <TextInput
@@ -22,8 +22,8 @@ export function useReviewWorkflows(workflowId) {
22
22
  }
23
23
  }
24
24
 
25
- function refetchWorkflow() {
26
- client.refetchQueries(workflowQueryKey);
25
+ async function refetchWorkflow() {
26
+ await client.refetchQueries(workflowQueryKey);
27
27
  }
28
28
 
29
29
  const workflows = useQuery(workflowQueryKey, fetchWorkflows);
@@ -35,6 +35,7 @@ export function reducer(state = initialState, action) {
35
35
  draft.serverState.workflows = workflows;
36
36
  draft.serverState.currentWorkflow = defaultWorkflow;
37
37
  draft.clientState.currentWorkflow.data = defaultWorkflow;
38
+ draft.clientState.currentWorkflow.hasDeletedServerStages = false;
38
39
  }
39
40
  break;
40
41
  }
@@ -97,7 +98,7 @@ export function reducer(state = initialState, action) {
97
98
  if (state.clientState.currentWorkflow.data) {
98
99
  draft.clientState.currentWorkflow.isDirty = !isEqual(
99
100
  current(draft.clientState.currentWorkflow).data,
100
- state.serverState.currentWorkflow
101
+ draft.serverState.currentWorkflow
101
102
  );
102
103
  }
103
104
  });
@@ -20,9 +20,6 @@ module.exports = {
20
20
  },
21
21
  },
22
22
  attributes: {
23
- uid: {
24
- type: 'string',
25
- },
26
23
  stages: {
27
24
  type: 'relation',
28
25
  target: 'admin::workflow-stage',
@@ -31,11 +31,6 @@ module.exports = {
31
31
  inversedBy: 'stages',
32
32
  configurable: false,
33
33
  },
34
- related: {
35
- type: 'relation',
36
- relation: 'morphToMany',
37
- configurable: false,
38
- },
39
34
  },
40
35
  },
41
36
  };
@@ -46,6 +46,11 @@ module.exports = {
46
46
  };
47
47
  },
48
48
 
49
+ /**
50
+ * Replace all stages in a workflow
51
+ * @param {import('koa').BaseContext} ctx - koa context
52
+ *
53
+ */
49
54
  async replace(ctx) {
50
55
  const { workflow_id: workflowId } = ctx.params;
51
56
  const stagesService = getService('stages');
@@ -90,6 +95,8 @@ module.exports = {
90
95
  // TODO When multiple workflows are possible, check if the stage is part of the right one
91
96
  // Didn't need this today as their can only be one workflow
92
97
 
93
- ctx.body = await stagesService.updateEntity({ id: entityId, modelUID }, stageId);
98
+ const data = await stagesService.updateEntity({ id: entityId, modelUID }, stageId);
99
+
100
+ ctx.body = { data };
94
101
  },
95
102
  };
@@ -8,7 +8,9 @@ const reviewWorkflowsMiddlewares = require('./middlewares/review-workflows');
8
8
  const { getService } = require('./utils');
9
9
 
10
10
  module.exports = async ({ strapi }) => {
11
- if (features.isEnabled('audit-logs')) {
11
+ const auditLogsIsEnabled = strapi.config.get('admin.auditLogs.enabled', true);
12
+
13
+ if (auditLogsIsEnabled) {
12
14
  strapi.hook('strapi::content-types.beforeSync').register(migrateAuditLogsTable);
13
15
  const auditLogsService = createAuditLogsService(strapi);
14
16
  strapi.container.register('audit-logs', auditLogsService);
@@ -63,16 +63,37 @@ 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
+
66
84
  const createAuditLogsService = (strapi) => {
85
+ // Manage internal service state privately
86
+ const state = {};
87
+
67
88
  // NOTE: providers should be able to replace getEventMap to add or remove events
68
89
  const eventMap = getEventMap(defaultEvents);
69
90
 
70
91
  const processEvent = (name, ...args) => {
71
- const state = strapi.requestContext.get()?.state;
92
+ const requestState = strapi.requestContext.get()?.state;
72
93
 
73
94
  // Ignore events with auth strategies different from admin
74
- const isUsingAdminAuth = state?.auth?.strategy.name === 'admin';
75
- const user = state?.user;
95
+ const isUsingAdminAuth = requestState?.auth?.strategy.name === 'admin';
96
+ const user = requestState?.user;
76
97
 
77
98
  if (!isUsingAdminAuth || !user) {
78
99
  return null;
@@ -103,26 +124,60 @@ const createAuditLogsService = (strapi) => {
103
124
  const processedEvent = processEvent(name, ...args);
104
125
 
105
126
  if (processedEvent) {
106
- await this._provider.saveEvent(processedEvent);
127
+ await state.provider.saveEvent(processedEvent);
107
128
  }
108
129
  }
109
130
 
110
131
  return {
111
132
  async register() {
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 * * *', () => {
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 * * *', () => {
117
172
  const expirationDate = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);
118
- this._provider.deleteExpiredEvents(expirationDate);
173
+ state.provider.deleteExpiredEvents(expirationDate);
119
174
  });
120
175
 
121
176
  return this;
122
177
  },
123
178
 
124
179
  async findMany(query) {
125
- const { results, pagination } = await this._provider.findMany(query);
180
+ const { results, pagination } = await state.provider.findMany(query);
126
181
 
127
182
  const sanitizedResults = results.map((result) => {
128
183
  const { user, ...rest } = result;
@@ -139,7 +194,7 @@ const createAuditLogsService = (strapi) => {
139
194
  },
140
195
 
141
196
  async findOne(id) {
142
- const result = await this._provider.findOne(id);
197
+ const result = await state.provider.findOne(id);
143
198
 
144
199
  if (!result) {
145
200
  return null;
@@ -153,12 +208,16 @@ const createAuditLogsService = (strapi) => {
153
208
  },
154
209
 
155
210
  unsubscribe() {
156
- if (this._eventHubUnsubscribe) {
157
- this._eventHubUnsubscribe();
211
+ if (state.eeDisableUnsubscribe) {
212
+ state.eeDisableUnsubscribe();
213
+ }
214
+
215
+ if (state.eventHubUnsubscribe) {
216
+ state.eventHubUnsubscribe();
158
217
  }
159
218
 
160
- if (this._deleteExpiredJob) {
161
- this._deleteExpiredJob.cancel();
219
+ if (state.deleteExpiredJob) {
220
+ state.deleteExpiredJob.cancel();
162
221
  }
163
222
 
164
223
  return this;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { isNil } = require('lodash/fp');
3
+ const { isNil, isNull } = require('lodash/fp');
4
4
  const { ENTITY_STAGE_ATTRIBUTE } = require('../../constants/workflows');
5
5
  const { hasReviewWorkflow, getDefaultWorkflow } = require('../../utils/review-workflows');
6
6
 
@@ -30,10 +30,22 @@ const decorator = (service) => ({
30
30
  }
31
31
 
32
32
  const data = await getDataWithStage(opts.data);
33
- return service.create.call(this, uid, {
34
- ...opts,
35
- data,
36
- });
33
+ return service.create.call(this, uid, { ...opts, data });
34
+ },
35
+ async update(uid, entityId, opts = {}) {
36
+ const hasRW = hasReviewWorkflow({ strapi }, uid);
37
+
38
+ if (!hasRW) {
39
+ return service.update.call(this, uid, entityId, opts);
40
+ }
41
+
42
+ // Prevents the stage from being set to null
43
+ const data = { ...opts.data };
44
+ if (isNull(data[ENTITY_STAGE_ATTRIBUTE])) {
45
+ delete data[ENTITY_STAGE_ATTRIBUTE];
46
+ }
47
+
48
+ return service.update.call(this, uid, entityId, { ...opts, data });
37
49
  },
38
50
  });
39
51
 
@@ -1,46 +1,16 @@
1
1
  'use strict';
2
2
 
3
- const { set, get, forEach, keys, pickBy, pipe } = require('lodash/fp');
3
+ const { set, forEach, pipe } = require('lodash/fp');
4
4
  const { mapAsync } = require('@strapi/utils');
5
5
  const { getService } = require('../../utils');
6
+ const { getContentTypeUIDsWithActivatedReviewWorkflows } = require('../../utils/review-workflows');
6
7
 
7
8
  const defaultStages = require('../../constants/default-stages.json');
8
9
  const defaultWorkflow = require('../../constants/default-workflow.json');
9
10
  const { ENTITY_STAGE_ATTRIBUTE } = require('../../constants/workflows');
10
11
 
11
- const {
12
- disableOnContentTypes: disableReviewWorkflows,
13
- } = require('../../migrations/review-workflows');
14
12
  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
- }
13
+ const { persistTable, removePersistedTablesWithSuffix } = require('../../utils/persisted-tables');
44
14
 
45
15
  async function initDefaultWorkflow({ workflowsService, stagesService, strapi }) {
46
16
  const wfCount = await workflowsService.count();
@@ -53,7 +23,7 @@ async function initDefaultWorkflow({ workflowsService, stagesService, strapi })
53
23
  const workflow = {
54
24
  ...defaultWorkflow,
55
25
  stages: {
56
- connect: buildStagesConnectArray(stages),
26
+ connect: stages.map((stage) => stage.id),
57
27
  },
58
28
  };
59
29
 
@@ -63,19 +33,18 @@ async function initDefaultWorkflow({ workflowsService, stagesService, strapi })
63
33
  }
64
34
  }
65
35
 
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
36
  function extendReviewWorkflowContentTypes({ strapi }) {
78
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
+ });
79
48
  strapi.container.get('content-types').extend(contentTypeUID, setStageAttribute);
80
49
  };
81
50
  pipe([
@@ -101,55 +70,23 @@ function enableReviewWorkflow({ strapi }) {
101
70
  return;
102
71
  }
103
72
  const firstStage = defaultWorkflow.stages[0];
73
+ const stagesService = getService('stages', { strapi });
104
74
 
105
75
  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
- );
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
+ });
151
86
  };
152
87
 
88
+ await removePersistedTablesWithSuffix('_strapi_review_workflows_stage_links');
89
+
153
90
  return pipe([
154
91
  getContentTypeUIDsWithActivatedReviewWorkflows,
155
92
  // Iterate over UIDs to extend the content-type
@@ -169,7 +106,6 @@ module.exports = ({ strapi }) => {
169
106
  async register() {
170
107
  extendReviewWorkflowContentTypes({ strapi });
171
108
  strapi.hook('strapi::content-types.afterSync').register(enableReviewWorkflow({ strapi }));
172
- strapi.hook('strapi::content-types.afterSync').register(disableReviewWorkflows);
173
109
  },
174
110
  };
175
111
  };
@@ -4,9 +4,11 @@ const {
4
4
  mapAsync,
5
5
  errors: { ApplicationError },
6
6
  } = require('@strapi/utils');
7
+ const { map } = require('lodash/fp');
7
8
 
8
9
  const { STAGE_MODEL_UID, ENTITY_STAGE_ATTRIBUTE } = require('../../constants/workflows');
9
10
  const { getService } = require('../../utils');
11
+ const { getContentTypeUIDsWithActivatedReviewWorkflows } = require('../../utils/review-workflows');
10
12
 
11
13
  module.exports = ({ strapi }) => {
12
14
  const workflowsService = getService('workflows', { strapi });
@@ -20,9 +22,8 @@ module.exports = ({ strapi }) => {
20
22
  return strapi.entityService.findMany(STAGE_MODEL_UID, params);
21
23
  },
22
24
 
23
- findById(id, { workflowId, populate }) {
25
+ findById(id, { populate } = {}) {
24
26
  const params = {
25
- filters: { workflow: workflowId },
26
27
  populate,
27
28
  };
28
29
  return strapi.entityService.findOne(STAGE_MODEL_UID, id, params);
@@ -58,12 +59,41 @@ module.exports = ({ strapi }) => {
58
59
 
59
60
  assertAtLeastOneStageRemain(workflow.stages, { created, deleted });
60
61
 
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);
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);
64
69
 
70
+ // Update the workflow stages
65
71
  await mapAsync(updated, (stage) => this.update(stage.id, stage));
66
- await mapAsync(deleted, (stage) => this.delete(stage.id));
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
+
67
97
  return workflowsService.update(workflowId, {
68
98
  stages: stagesIds,
69
99
  });
@@ -78,12 +108,58 @@ module.exports = ({ strapi }) => {
78
108
  * @param {string} entityInfo.modelUID - the content-type of the entity
79
109
  * @param {number} stageId - The id of the stage to assign to the entity
80
110
  */
81
- updateEntity(entityInfo, stageId) {
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
+
82
118
  return strapi.entityService.update(entityInfo.modelUID, entityInfo.id, {
83
119
  data: { [ENTITY_STAGE_ATTRIBUTE]: stageId },
84
120
  populate: [ENTITY_STAGE_ATTRIBUTE],
85
121
  });
86
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
+ },
87
163
  };
88
164
  };
89
165
 
@@ -146,3 +222,28 @@ function assertAtLeastOneStageRemain(workflowStages, diffStages) {
146
222
  throw new ApplicationError('At least one stage must remain in the workflow.');
147
223
  }
148
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
+ }