@strapi/admin 4.10.6 → 4.11.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 (73) hide show
  1. package/admin/src/content-manager/components/DynamicTable/BulkActionsBar/index.js +307 -0
  2. package/admin/src/content-manager/components/DynamicTable/index.js +20 -4
  3. package/admin/src/content-manager/components/EditViewDataManagerProvider/index.js +3 -2
  4. package/admin/src/content-manager/components/EditViewDataManagerProvider/utils/index.js +0 -1
  5. package/admin/src/content-manager/pages/ListView/index.js +118 -2
  6. package/admin/src/content-manager/utils/index.js +2 -0
  7. package/admin/src/content-manager/{components/EditViewDataManagerProvider/utils → utils}/schema.js +1 -1
  8. package/admin/src/hooks/index.js +1 -1
  9. package/admin/src/hooks/useAdminUsers/__mocks__/index.js +5 -0
  10. package/admin/src/hooks/useAdminUsers/index.js +1 -0
  11. package/admin/src/hooks/useAdminUsers/useAdminUsers.js +38 -0
  12. package/admin/src/hooks/useContentTypes/__mocks__/index.js +6 -0
  13. package/admin/src/hooks/useContentTypes/index.js +1 -0
  14. package/admin/src/hooks/useContentTypes/useContentTypes.js +47 -0
  15. package/admin/src/injectionZones.js +6 -1
  16. package/admin/src/pages/HomePage/index.js +3 -2
  17. package/admin/src/pages/MarketplacePage/components/NpmPackageCard/InstallPluginButton.js +12 -8
  18. package/admin/src/pages/SettingsPage/components/Tokens/TokenBox/index.js +28 -26
  19. package/admin/src/pages/SettingsPage/pages/Users/EditPage/index.js +36 -26
  20. package/admin/src/pages/SettingsPage/pages/Users/EditPage/utils/api.js +1 -8
  21. package/admin/src/pages/SettingsPage/pages/Users/ListPage/ModalForm/index.js +6 -7
  22. package/admin/src/pages/SettingsPage/pages/Users/ListPage/index.js +54 -63
  23. package/admin/src/pages/SettingsPage/pages/Users/components/MagicLink/MagicLinkWrapper.js +11 -9
  24. package/admin/src/pages/SettingsPage/pages/Webhooks/EditView/index.js +2 -2
  25. package/admin/src/translations/en.json +5 -1
  26. package/build/3562.e0b1a0b3.chunk.js +50 -0
  27. package/build/5563.79950369.chunk.js +79 -0
  28. package/build/6970.7ea35fbd.chunk.js +1 -0
  29. package/build/7259.5cc67413.chunk.js +1 -0
  30. package/build/{Admin-authenticatedApp.0318dfb3.chunk.js → Admin-authenticatedApp.65172a0c.chunk.js} +2 -2
  31. package/build/{Admin_homePage.e15dcf28.chunk.js → Admin_homePage.107a9fe0.chunk.js} +9 -9
  32. package/build/{Admin_marketplace.f446ba2b.chunk.js → Admin_marketplace.717bd7ca.chunk.js} +11 -11
  33. package/build/{Admin_profilePage.1687246a.chunk.js → Admin_profilePage.a8fa3a56.chunk.js} +1 -1
  34. package/build/{Admin_settingsPage.f8c46a9a.chunk.js → Admin_settingsPage.bd715ed3.chunk.js} +2 -2
  35. package/build/admin-app.9c79b484.chunk.js +63 -0
  36. package/build/{admin-edit-roles-page.02c3b136.chunk.js → admin-edit-roles-page.0d12b741.chunk.js} +3 -3
  37. package/build/admin-edit-users.f9ce7844.chunk.js +10 -0
  38. package/build/{admin-roles-list.a323aa9f.chunk.js → admin-roles-list.e8bf9685.chunk.js} +1 -1
  39. package/build/admin-users.751b28b2.chunk.js +34 -0
  40. package/build/audit-logs-settings-page.3c6cea81.chunk.js +129 -0
  41. package/build/content-manager.bf060d8e.chunk.js +1123 -0
  42. package/build/{en-json.d965e364.chunk.js → en-json.19e9ff9b.chunk.js} +1 -1
  43. package/build/i18n-translation-en-json.1ec7becf.chunk.js +1 -0
  44. package/build/index.html +1 -1
  45. package/build/main.576a9d22.js +2630 -0
  46. package/build/review-workflows-settings.4b39b837.chunk.js +61 -0
  47. package/build/{runtime~main.0201a49b.js → runtime~main.96d92f16.js} +2 -2
  48. package/build/{sso-settings-page.c9d7c8df.chunk.js → sso-settings-page.265e3d72.chunk.js} +1 -1
  49. package/build/{webhook-edit-page.cb2cf1a5.chunk.js → webhook-edit-page.ddd5963d.chunk.js} +13 -13
  50. package/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js +1 -2
  51. package/ee/admin/pages/SettingsPage/pages/AuditLogs/ListView/hooks/useAuditLogsData.js +36 -28
  52. package/ee/admin/pages/SettingsPage/pages/AuditLogs/ListView/utils/getDisplayedFilters.js +1 -1
  53. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js +3 -3
  54. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows.js +32 -21
  55. package/ee/server/services/audit-logs.js +5 -1
  56. package/package.json +10 -11
  57. package/webpack.alias.js +0 -1
  58. package/admin/src/content-manager/components/DynamicTable/ConfirmDialogDeleteAll/index.js +0 -73
  59. package/admin/src/hooks/useModels/index.js +0 -58
  60. package/admin/src/hooks/useModels/reducer.js +0 -45
  61. package/admin/src/pages/SettingsPage/pages/Users/ListPage/utils/api.js +0 -20
  62. package/build/5563.2c8334ef.chunk.js +0 -79
  63. package/build/6858.85d76858.chunk.js +0 -50
  64. package/build/6970.36d3ffff.chunk.js +0 -1
  65. package/build/7259.116a9960.chunk.js +0 -1
  66. package/build/admin-app.6d48536c.chunk.js +0 -63
  67. package/build/admin-edit-users.49363035.chunk.js +0 -10
  68. package/build/admin-users.69f4900a.chunk.js +0 -34
  69. package/build/audit-logs-settings-page.482909d7.chunk.js +0 -129
  70. package/build/content-manager.cd71cb6e.chunk.js +0 -1123
  71. package/build/i18n-translation-en-json.60af6722.chunk.js +0 -1
  72. package/build/main.adab8b96.js +0 -2630
  73. package/build/review-workflows-settings.26409ce4.chunk.js +0 -61
@@ -0,0 +1,307 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Button, Dialog, DialogBody, DialogFooter, Flex, Typography } from '@strapi/design-system';
4
+ import { Check, ExclamationMarkCircle, Trash } from '@strapi/icons';
5
+ import { useIntl } from 'react-intl';
6
+ import { useSelector } from 'react-redux';
7
+ import { useTracking } from '@strapi/helper-plugin';
8
+ import { getTrad } from '../../../utils';
9
+ import InjectionZoneList from '../../InjectionZoneList';
10
+ import { listViewDomain } from '../../../pages/ListView/selectors';
11
+
12
+ const ConfirmBulkActionDialog = ({ onToggleDialog, isOpen, dialogBody, endAction }) => {
13
+ const { formatMessage } = useIntl();
14
+
15
+ return (
16
+ <Dialog
17
+ onClose={onToggleDialog}
18
+ title={formatMessage({
19
+ id: 'app.components.ConfirmDialog.title',
20
+ defaultMessage: 'Confirmation',
21
+ })}
22
+ labelledBy="confirmation"
23
+ describedBy="confirm-description"
24
+ isOpen={isOpen}
25
+ >
26
+ <DialogBody icon={<ExclamationMarkCircle />}>
27
+ <Flex direction="column" alignItems="stretch" gap={2}>
28
+ {dialogBody}
29
+ </Flex>
30
+ </DialogBody>
31
+ <DialogFooter
32
+ startAction={
33
+ <Button onClick={onToggleDialog} variant="tertiary">
34
+ {formatMessage({
35
+ id: 'app.components.Button.cancel',
36
+ defaultMessage: 'Cancel',
37
+ })}
38
+ </Button>
39
+ }
40
+ endAction={endAction}
41
+ />
42
+ </Dialog>
43
+ );
44
+ };
45
+
46
+ ConfirmBulkActionDialog.propTypes = {
47
+ isOpen: PropTypes.bool.isRequired,
48
+ onToggleDialog: PropTypes.func.isRequired,
49
+ dialogBody: PropTypes.node.isRequired,
50
+ endAction: PropTypes.node.isRequired,
51
+ };
52
+
53
+ const confirmDialogsPropTypes = {
54
+ isConfirmButtonLoading: PropTypes.bool.isRequired,
55
+ isOpen: PropTypes.bool.isRequired,
56
+ onConfirm: PropTypes.func.isRequired,
57
+ onToggleDialog: PropTypes.func.isRequired,
58
+ };
59
+
60
+ const ConfirmDialogPublishAll = ({ isOpen, onToggleDialog, isConfirmButtonLoading, onConfirm }) => {
61
+ const { formatMessage } = useIntl();
62
+
63
+ return (
64
+ <ConfirmBulkActionDialog
65
+ isOpen={isOpen}
66
+ onToggleDialog={onToggleDialog}
67
+ dialogBody={
68
+ <>
69
+ <Typography id="confirm-description" textAlign="center">
70
+ {formatMessage({
71
+ id: getTrad('popUpWarning.bodyMessage.contentType.publish.all'),
72
+ defaultMessage: 'Are you sure you want to publish these entries?',
73
+ })}
74
+ </Typography>
75
+ <InjectionZoneList area="contentManager.listView.publishModalAdditionalInfos" />
76
+ </>
77
+ }
78
+ endAction={
79
+ <Button
80
+ onClick={onConfirm}
81
+ variant="secondary"
82
+ startIcon={<Check />}
83
+ loading={isConfirmButtonLoading}
84
+ >
85
+ {formatMessage({
86
+ id: 'app.utils.publish',
87
+ defaultMessage: 'Publish',
88
+ })}
89
+ </Button>
90
+ }
91
+ />
92
+ );
93
+ };
94
+
95
+ ConfirmDialogPublishAll.propTypes = confirmDialogsPropTypes;
96
+
97
+ const ConfirmDialogUnpublishAll = ({
98
+ isOpen,
99
+ onToggleDialog,
100
+ isConfirmButtonLoading,
101
+ onConfirm,
102
+ }) => {
103
+ const { formatMessage } = useIntl();
104
+
105
+ return (
106
+ <ConfirmBulkActionDialog
107
+ isOpen={isOpen}
108
+ onToggleDialog={onToggleDialog}
109
+ dialogBody={
110
+ <>
111
+ <Typography id="confirm-description" textAlign="center">
112
+ {formatMessage({
113
+ id: getTrad('popUpWarning.bodyMessage.contentType.unpublish.all'),
114
+ defaultMessage: 'Are you sure you want to unpublish these entries?',
115
+ })}
116
+ </Typography>
117
+ <InjectionZoneList area="contentManager.listView.unpublishModalAdditionalInfos" />
118
+ </>
119
+ }
120
+ endAction={
121
+ <Button
122
+ onClick={onConfirm}
123
+ variant="secondary"
124
+ startIcon={<Check />}
125
+ loading={isConfirmButtonLoading}
126
+ >
127
+ {formatMessage({
128
+ id: 'app.utils.unpublish',
129
+ defaultMessage: 'Unpublish',
130
+ })}
131
+ </Button>
132
+ }
133
+ />
134
+ );
135
+ };
136
+
137
+ ConfirmDialogUnpublishAll.propTypes = confirmDialogsPropTypes;
138
+
139
+ const ConfirmDialogDeleteAll = ({ isOpen, onToggleDialog, isConfirmButtonLoading, onConfirm }) => {
140
+ const { formatMessage } = useIntl();
141
+
142
+ return (
143
+ <ConfirmBulkActionDialog
144
+ isOpen={isOpen}
145
+ onToggleDialog={onToggleDialog}
146
+ dialogBody={
147
+ <>
148
+ <Typography id="confirm-description" textAlign="center">
149
+ {formatMessage({
150
+ id: getTrad('popUpWarning.bodyMessage.contentType.delete.all'),
151
+ defaultMessage: 'Are you sure you want to delete these entries?',
152
+ })}
153
+ </Typography>
154
+ <InjectionZoneList area="contentManager.listView.deleteModalAdditionalInfos" />
155
+ </>
156
+ }
157
+ endAction={
158
+ <Button
159
+ onClick={onConfirm}
160
+ variant="danger-light"
161
+ startIcon={<Trash />}
162
+ id="confirm-delete"
163
+ loading={isConfirmButtonLoading}
164
+ >
165
+ {formatMessage({
166
+ id: 'app.components.Button.confirm',
167
+ defaultMessage: 'Confirm',
168
+ })}
169
+ </Button>
170
+ }
171
+ />
172
+ );
173
+ };
174
+
175
+ ConfirmDialogDeleteAll.propTypes = confirmDialogsPropTypes;
176
+
177
+ const BulkActionsBar = ({
178
+ showPublish,
179
+ showDelete,
180
+ onConfirmDeleteAll,
181
+ onConfirmPublishAll,
182
+ onConfirmUnpublishAll,
183
+ selectedEntries,
184
+ clearSelectedEntries,
185
+ }) => {
186
+ const { formatMessage } = useIntl();
187
+ const { trackUsage } = useTracking();
188
+ const { data } = useSelector(listViewDomain());
189
+
190
+ const [isConfirmButtonLoading, setIsConfirmButtonLoading] = useState(false);
191
+ const [dialogToOpen, setDialogToOpen] = useState(null);
192
+
193
+ // Filters for Bulk actions
194
+ const selectedEntriesObjects = data.filter((entry) => selectedEntries.includes(entry.id));
195
+ const publishButtonIsShown =
196
+ showPublish && selectedEntriesObjects.some((entry) => !entry.publishedAt);
197
+ const unpublishButtonIsShown =
198
+ showPublish && selectedEntriesObjects.some((entry) => entry.publishedAt);
199
+
200
+ const toggleDeleteModal = () => {
201
+ if (dialogToOpen === 'delete') {
202
+ setDialogToOpen(null);
203
+ } else {
204
+ setDialogToOpen('delete');
205
+ trackUsage('willBulkDeleteEntries');
206
+ }
207
+ };
208
+
209
+ const togglePublishModal = () => {
210
+ if (dialogToOpen === 'publish') {
211
+ setDialogToOpen(null);
212
+ } else {
213
+ setDialogToOpen('publish');
214
+ trackUsage('willBulkPublishEntries');
215
+ }
216
+ };
217
+
218
+ const toggleUnpublishModal = () => {
219
+ if (dialogToOpen === 'unpublish') {
220
+ setDialogToOpen(null);
221
+ } else {
222
+ setDialogToOpen('unpublish');
223
+ trackUsage('willBulkUnpublishEntries');
224
+ }
225
+ };
226
+
227
+ const handleBulkAction = async (confirmAction, toggleModal) => {
228
+ try {
229
+ setIsConfirmButtonLoading(true);
230
+ await confirmAction(selectedEntries);
231
+ setIsConfirmButtonLoading(false);
232
+ toggleModal();
233
+ clearSelectedEntries();
234
+ } catch (error) {
235
+ setIsConfirmButtonLoading(false);
236
+ toggleModal();
237
+ }
238
+ };
239
+
240
+ const handleBulkDelete = () => handleBulkAction(onConfirmDeleteAll, toggleDeleteModal);
241
+ const handleBulkPublish = () => handleBulkAction(onConfirmPublishAll, togglePublishModal);
242
+ const handleBulkUnpublish = () => handleBulkAction(onConfirmUnpublishAll, toggleUnpublishModal);
243
+
244
+ return (
245
+ <>
246
+ {publishButtonIsShown && (
247
+ <>
248
+ <Button variant="tertiary" onClick={togglePublishModal}>
249
+ {formatMessage({ id: 'app.utils.publish', defaultMessage: 'Publish' })}
250
+ </Button>
251
+ <ConfirmDialogPublishAll
252
+ isOpen={dialogToOpen === 'publish'}
253
+ onToggleDialog={togglePublishModal}
254
+ isConfirmButtonLoading={isConfirmButtonLoading}
255
+ onConfirm={handleBulkPublish}
256
+ />
257
+ </>
258
+ )}
259
+ {unpublishButtonIsShown && (
260
+ <>
261
+ <Button variant="tertiary" onClick={toggleUnpublishModal}>
262
+ {formatMessage({ id: 'app.utils.unpublish', defaultMessage: 'Unpublish' })}
263
+ </Button>
264
+ <ConfirmDialogUnpublishAll
265
+ isOpen={dialogToOpen === 'unpublish'}
266
+ onToggleDialog={toggleUnpublishModal}
267
+ isConfirmButtonLoading={isConfirmButtonLoading}
268
+ onConfirm={handleBulkUnpublish}
269
+ />
270
+ </>
271
+ )}
272
+ {showDelete && (
273
+ <>
274
+ <Button variant="danger-light" onClick={toggleDeleteModal}>
275
+ {formatMessage({ id: 'global.delete', defaultMessage: 'Delete' })}
276
+ </Button>
277
+ <ConfirmDialogDeleteAll
278
+ isOpen={dialogToOpen === 'delete'}
279
+ onToggleDialog={toggleDeleteModal}
280
+ isConfirmButtonLoading={isConfirmButtonLoading}
281
+ onConfirm={handleBulkDelete}
282
+ />
283
+ </>
284
+ )}
285
+ </>
286
+ );
287
+ };
288
+
289
+ BulkActionsBar.defaultProps = {
290
+ showPublish: false,
291
+ showDelete: false,
292
+ onConfirmDeleteAll() {},
293
+ onConfirmPublishAll() {},
294
+ onConfirmUnpublishAll() {},
295
+ };
296
+
297
+ BulkActionsBar.propTypes = {
298
+ showPublish: PropTypes.bool,
299
+ showDelete: PropTypes.bool,
300
+ onConfirmDeleteAll: PropTypes.func,
301
+ onConfirmPublishAll: PropTypes.func,
302
+ onConfirmUnpublishAll: PropTypes.func,
303
+ selectedEntries: PropTypes.array.isRequired,
304
+ clearSelectedEntries: PropTypes.func.isRequired,
305
+ };
306
+
307
+ export default BulkActionsBar;
@@ -9,19 +9,22 @@ import { INJECT_COLUMN_IN_TABLE } from '../../../exposedHooks';
9
9
  import { selectDisplayedHeaders } from '../../pages/ListView/selectors';
10
10
  import { getTrad } from '../../utils';
11
11
  import TableRows from './TableRows';
12
- import ConfirmDialogDeleteAll from './ConfirmDialogDeleteAll';
13
12
  import ConfirmDialogDelete from './ConfirmDialogDelete';
14
13
  import { PublicationState } from './CellContent/PublicationState/PublicationState';
14
+ import BulkActionsBar from './BulkActionsBar';
15
15
 
16
16
  const DynamicTable = ({
17
17
  canCreate,
18
18
  canDelete,
19
+ canPublish,
19
20
  contentTypeName,
20
21
  action,
21
22
  isBulkable,
22
23
  isLoading,
23
24
  onConfirmDelete,
24
25
  onConfirmDeleteAll,
26
+ onConfirmPublishAll,
27
+ onConfirmUnpublishAll,
25
28
  layout,
26
29
  rows,
27
30
  }) => {
@@ -89,17 +92,27 @@ const DynamicTable = ({
89
92
 
90
93
  return (
91
94
  <Table
92
- components={{ ConfirmDialogDelete, ConfirmDialogDeleteAll }}
95
+ components={{ ConfirmDialogDelete }}
93
96
  contentType={contentTypeName}
94
97
  action={action}
95
98
  isLoading={isLoading}
96
99
  headers={tableHeaders}
97
100
  onConfirmDelete={onConfirmDelete}
98
- onConfirmDeleteAll={onConfirmDeleteAll}
99
101
  onOpenDeleteAllModalTrackedEvent="willBulkDeleteEntries"
100
102
  rows={rows}
101
103
  withBulkActions
102
- withMainAction={canDelete && isBulkable}
104
+ withMainAction={(canDelete || canPublish) && isBulkable}
105
+ renderBulkActionsBar={({ selectedEntries, clearSelectedEntries }) => (
106
+ <BulkActionsBar
107
+ showPublish={canPublish && hasDraftAndPublish}
108
+ showDelete={canDelete}
109
+ onConfirmDeleteAll={onConfirmDeleteAll}
110
+ onConfirmPublishAll={onConfirmPublishAll}
111
+ onConfirmUnpublishAll={onConfirmUnpublishAll}
112
+ selectedEntries={selectedEntries}
113
+ clearSelectedEntries={clearSelectedEntries}
114
+ />
115
+ )}
103
116
  >
104
117
  <TableRows
105
118
  canCreate={canCreate}
@@ -121,6 +134,7 @@ DynamicTable.defaultProps = {
121
134
  DynamicTable.propTypes = {
122
135
  canCreate: PropTypes.bool.isRequired,
123
136
  canDelete: PropTypes.bool.isRequired,
137
+ canPublish: PropTypes.bool.isRequired,
124
138
  contentTypeName: PropTypes.string.isRequired,
125
139
  action: PropTypes.node,
126
140
  isBulkable: PropTypes.bool.isRequired,
@@ -139,6 +153,8 @@ DynamicTable.propTypes = {
139
153
  }).isRequired,
140
154
  onConfirmDelete: PropTypes.func.isRequired,
141
155
  onConfirmDeleteAll: PropTypes.func.isRequired,
156
+ onConfirmPublishAll: PropTypes.func.isRequired,
157
+ onConfirmUnpublishAll: PropTypes.func.isRequired,
142
158
  rows: PropTypes.array.isRequired,
143
159
  };
144
160
 
@@ -21,12 +21,13 @@ import {
21
21
  getAPIInnerErrors,
22
22
  } from '@strapi/helper-plugin';
23
23
 
24
- import { getTrad } from '../../utils';
24
+ import { createYupSchema, getTrad } from '../../utils';
25
25
 
26
26
  import selectCrudReducer from '../../sharedReducers/crudReducer/selectors';
27
27
 
28
28
  import reducer, { initialState } from './reducer';
29
- import { cleanData, createYupSchema } from './utils';
29
+ import { cleanData } from './utils';
30
+
30
31
  import { clearSetModifiedDataOnly } from '../../sharedReducers/crudReducer/actions';
31
32
  import { usePrev } from '../../hooks';
32
33
 
@@ -1,4 +1,3 @@
1
1
  export { default as moveFields } from './moveFields';
2
2
  export { default as cleanData } from './cleanData';
3
- export { default as createYupSchema } from './schema';
4
3
  export { findAllAndReplace } from './findAllAndReplace';
@@ -21,6 +21,7 @@ import {
21
21
  useTracking,
22
22
  Link,
23
23
  useAPIErrorHandler,
24
+ getYupInnerErrors,
24
25
  } from '@strapi/helper-plugin';
25
26
 
26
27
  import {
@@ -35,6 +36,7 @@ import {
35
36
  } from '@strapi/design-system';
36
37
 
37
38
  import { ArrowLeft, Plus, Cog } from '@strapi/icons';
39
+ import { useMutation } from 'react-query';
38
40
 
39
41
  import DynamicTable from '../../components/DynamicTable';
40
42
  import AttributeFilter from '../../components/AttributeFilter';
@@ -42,7 +44,7 @@ import { InjectionZone } from '../../../shared/components';
42
44
 
43
45
  import permissions from '../../../permissions';
44
46
 
45
- import { getRequestUrl, getTrad } from '../../utils';
47
+ import { createYupSchema, getRequestUrl, getTrad } from '../../utils';
46
48
 
47
49
  import FieldPicker from './FieldPicker';
48
50
  import PaginationFooter from './PaginationFooter';
@@ -64,6 +66,7 @@ function ListView({
64
66
  canCreate,
65
67
  canDelete,
66
68
  canRead,
69
+ canPublish,
67
70
  data,
68
71
  getData,
69
72
  getDataSucceeded,
@@ -100,6 +103,50 @@ function ListView({
100
103
  const fetchClient = useFetchClient();
101
104
  const { post, del } = fetchClient;
102
105
 
106
+ const bulkPublishMutation = useMutation(
107
+ (data) =>
108
+ post(`/content-manager/collection-types/${contentType.uid}/actions/bulkPublish`, data),
109
+ {
110
+ onSuccess() {
111
+ toggleNotification({
112
+ type: 'success',
113
+ message: { id: 'content-manager.success.record.publish', defaultMessage: 'Published' },
114
+ });
115
+
116
+ fetchData(`/content-manager/collection-types/${slug}${params}`);
117
+ },
118
+ onError(error) {
119
+ toggleNotification({
120
+ type: 'warning',
121
+ message: formatAPIError(error),
122
+ });
123
+ },
124
+ }
125
+ );
126
+ const bulkUnpublishMutation = useMutation(
127
+ (data) =>
128
+ post(`/content-manager/collection-types/${contentType.uid}/actions/bulkUnpublish`, data),
129
+ {
130
+ onSuccess() {
131
+ toggleNotification({
132
+ type: 'success',
133
+ message: {
134
+ id: 'content-manager.success.record.unpublish',
135
+ defaultMessage: 'Unpublished',
136
+ },
137
+ });
138
+
139
+ fetchData(`/content-manager/collection-types/${slug}${params}`);
140
+ },
141
+ onError(error) {
142
+ toggleNotification({
143
+ type: 'warning',
144
+ message: formatAPIError(error),
145
+ });
146
+ },
147
+ }
148
+ );
149
+
103
150
  // FIXME
104
151
  // Using a ref to avoid requests being fired multiple times on slug on change
105
152
  // We need it because the hook as mulitple dependencies so it may run before the permissions have checked
@@ -199,6 +246,70 @@ function ListView({
199
246
  [slug, params, fetchData, toggleNotification, formatAPIError, del]
200
247
  );
201
248
 
249
+ /**
250
+ * @param {number[]} selectedEntries - Array of ids to publish
251
+ * @returns {{validIds: number[], errors: Object.<number, string>}} - Returns an object with the valid ids and the errors
252
+ */
253
+ const validateEntriesToPublish = async (selectedEntries) => {
254
+ const validations = { validIds: [], errors: {} };
255
+ // Create the validation schema based on the contentType
256
+ const schema = createYupSchema(
257
+ contentType,
258
+ { components: layout.components },
259
+ { isDraft: false }
260
+ );
261
+ // Get the selected entries
262
+ const entries = data.filter((entry) => {
263
+ return selectedEntries.includes(entry.id);
264
+ });
265
+ // Validate each entry and map the unresolved promises
266
+ const validationPromises = entries.map((entry) =>
267
+ schema.validate(entry, { abortEarly: false })
268
+ );
269
+ // Resolve all the promises in one go
270
+ const resolvedPromises = await Promise.allSettled(validationPromises);
271
+ // Set the validations
272
+ resolvedPromises.forEach((promise) => {
273
+ if (promise.status === 'rejected') {
274
+ const entityId = promise.reason.value.id;
275
+ validations.errors[entityId] = getYupInnerErrors(promise.reason);
276
+ }
277
+
278
+ if (promise.status === 'fulfilled') {
279
+ validations.validIds.push(promise.value.id);
280
+ }
281
+ });
282
+
283
+ return validations;
284
+ };
285
+
286
+ const handleConfirmPublishAllData = async (selectedEntries) => {
287
+ const validations = await validateEntriesToPublish(selectedEntries);
288
+
289
+ if (Object.values(validations.errors).length) {
290
+ toggleNotification({
291
+ type: 'warning',
292
+ title: {
293
+ id: 'content-manager.listView.validation.errors.title',
294
+ defaultMessage: 'Action required',
295
+ },
296
+ message: {
297
+ id: 'content-manager.listView.validation.errors.message',
298
+ defaultMessage:
299
+ 'Please make sure all fields are valid before publishing (required field, min/max character limit, etc.)',
300
+ },
301
+ });
302
+
303
+ throw new Error('Validation error');
304
+ }
305
+
306
+ return bulkPublishMutation.mutateAsync({ ids: selectedEntries });
307
+ };
308
+
309
+ const handleConfirmUnpublishAllData = (selectedEntries) => {
310
+ return bulkUnpublishMutation.mutateAsync({ ids: selectedEntries });
311
+ };
312
+
202
313
  useEffect(() => {
203
314
  const CancelToken = axios.CancelToken;
204
315
  const source = CancelToken.source();
@@ -330,9 +441,12 @@ function ListView({
330
441
  <DynamicTable
331
442
  canCreate={canCreate}
332
443
  canDelete={canDelete}
444
+ canPublish={canPublish}
333
445
  contentTypeName={headerLayoutTitle}
334
- onConfirmDeleteAll={handleConfirmDeleteAllData}
335
446
  onConfirmDelete={handleConfirmDeleteData}
447
+ onConfirmDeleteAll={handleConfirmDeleteAllData}
448
+ onConfirmPublishAll={handleConfirmPublishAllData}
449
+ onConfirmUnpublishAll={handleConfirmUnpublishAllData}
336
450
  isBulkable={isBulkable}
337
451
  isLoading={isLoading}
338
452
  // FIXME: remove the layout props drilling
@@ -354,10 +468,12 @@ ListView.propTypes = {
354
468
  canCreate: PropTypes.bool.isRequired,
355
469
  canDelete: PropTypes.bool.isRequired,
356
470
  canRead: PropTypes.bool.isRequired,
471
+ canPublish: PropTypes.bool.isRequired,
357
472
  data: PropTypes.array.isRequired,
358
473
  layout: PropTypes.exact({
359
474
  components: PropTypes.object.isRequired,
360
475
  contentType: PropTypes.shape({
476
+ uid: PropTypes.string.isRequired,
361
477
  attributes: PropTypes.object.isRequired,
362
478
  metadatas: PropTypes.object.isRequired,
363
479
  info: PropTypes.shape({ displayName: PropTypes.string.isRequired }).isRequired,
@@ -18,3 +18,5 @@ export { default as mergeMetasWithSchema } from './mergeMetasWithSchema';
18
18
 
19
19
  export { default as removeKeyInObject } from './removeKeyInObject';
20
20
  export { default as removePasswordFieldsFromData } from './removePasswordFieldsFromData';
21
+
22
+ export { default as createYupSchema } from './schema';
@@ -7,7 +7,7 @@ import toNumber from 'lodash/toNumber';
7
7
  import * as yup from 'yup';
8
8
  import { translatedErrors as errorsTrads } from '@strapi/helper-plugin';
9
9
 
10
- import isFieldTypeNumber from '../../../utils/isFieldTypeNumber';
10
+ import isFieldTypeNumber from './isFieldTypeNumber';
11
11
 
12
12
  yup.addMethod(yup.mixed, 'defined', function () {
13
13
  return this.test('defined', errorsTrads.required, (value) => value !== undefined);
@@ -1,5 +1,5 @@
1
1
  export { default as useConfigurations } from './useConfigurations';
2
- export { default as useModels } from './useModels';
2
+ export { useContentTypes } from './useContentTypes';
3
3
  export { default as useFetchPermissionsLayout } from './useFetchPermissionsLayout';
4
4
  export { default as useFetchRole } from './useFetchRole';
5
5
  export { default as useMenu } from './useMenu';
@@ -0,0 +1,5 @@
1
+ export const useAdminUsers = jest.fn().mockReturnValue({
2
+ users: [],
3
+ isLoading: false,
4
+ isError: false,
5
+ });
@@ -0,0 +1 @@
1
+ export * from './useAdminUsers';
@@ -0,0 +1,38 @@
1
+ import { useQuery } from 'react-query';
2
+ import { useFetchClient } from '@strapi/helper-plugin';
3
+ import { stringify } from 'qs';
4
+
5
+ export function useAdminUsers(params = {}, queryOptions = {}) {
6
+ const { id = '', ...queryParams } = params;
7
+ const queryString = stringify(queryParams, { encode: false });
8
+
9
+ const { get } = useFetchClient();
10
+
11
+ const { data, isError, isLoading, refetch } = useQuery(
12
+ ['users', id, queryParams],
13
+ async () => {
14
+ const {
15
+ data: { data },
16
+ } = await get(`/admin/users/${id}${queryString ? `?${queryString}` : ''}`);
17
+
18
+ return data;
19
+ },
20
+ queryOptions
21
+ );
22
+
23
+ let users = [];
24
+
25
+ if (id && data) {
26
+ users = [data];
27
+ } else if (Array.isArray(data?.results)) {
28
+ users = data.results;
29
+ }
30
+
31
+ return {
32
+ users,
33
+ pagination: data?.pagination ?? null,
34
+ isLoading,
35
+ isError,
36
+ refetch,
37
+ };
38
+ }
@@ -0,0 +1,6 @@
1
+ export const useContentTypes = jest.fn().mockReturnValue({
2
+ isLoading: false,
3
+ components: [],
4
+ collectionTypes: [],
5
+ singleTypes: [],
6
+ });
@@ -0,0 +1 @@
1
+ export * from './useContentTypes';