@strapi/admin 4.10.2 → 4.11.0-alpha.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 (66) 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/components/Wysiwyg/WysiwygNav.js +156 -155
  6. package/admin/src/content-manager/pages/ListSettingsView/{utils/excludedSortOptions.js → constants.js} +1 -1
  7. package/admin/src/content-manager/pages/ListSettingsView/index.js +33 -36
  8. package/admin/src/content-manager/pages/ListView/index.js +118 -2
  9. package/admin/src/content-manager/utils/index.js +2 -0
  10. package/admin/src/content-manager/{components/EditViewDataManagerProvider/utils → utils}/schema.js +1 -1
  11. package/admin/src/injectionZones.js +6 -1
  12. package/admin/src/pages/Admin/index.js +3 -0
  13. package/admin/src/pages/AuthPage/components/Register/index.js +1 -1
  14. package/admin/src/pages/MarketplacePage/components/NpmPackagesFilters/FilterSelect.js +1 -1
  15. package/admin/src/pages/MarketplacePage/components/NpmPackagesFilters/FiltersPopover.js +52 -41
  16. package/admin/src/pages/MarketplacePage/components/SortSelect/index.js +16 -0
  17. package/admin/src/pages/SettingsPage/pages/Users/components/SelectRoles/index.js +2 -2
  18. package/admin/src/translations/en.json +6 -1
  19. package/admin/src/translations/ru.json +7 -0
  20. package/build/3562.57745ccd.chunk.js +50 -0
  21. package/build/6970.5159b068.chunk.js +1 -0
  22. package/build/{Admin-authenticatedApp.a8373103.chunk.js → Admin-authenticatedApp.27a2329f.chunk.js} +2 -2
  23. package/build/{Admin_marketplace.a60cde15.chunk.js → Admin_marketplace.5f7b89e5.chunk.js} +32 -8
  24. package/build/{Admin_settingsPage.5e045f42.chunk.js → Admin_settingsPage.9d0419bc.chunk.js} +1 -1
  25. package/build/Upload_ConfigureTheView.d429a7fc.chunk.js +1 -0
  26. package/build/{admin-app.9bfe4ec7.chunk.js → admin-app.8e1f56bc.chunk.js} +2 -2
  27. package/build/{admin-edit-users.ba27c532.chunk.js → admin-edit-users.2aae89f5.chunk.js} +4 -4
  28. package/build/{admin-users.ad5dd832.chunk.js → admin-users.04a823ca.chunk.js} +5 -5
  29. package/build/content-manager.cf467ecc.chunk.js +1123 -0
  30. package/build/{content-type-builder-translation-en-json.446b611d.chunk.js → content-type-builder-translation-en-json.5e5f8607.chunk.js} +1 -1
  31. package/build/en-json.7edb00f6.chunk.js +1 -0
  32. package/build/i18n-translation-en-json.1ec7becf.chunk.js +1 -0
  33. package/build/i18n-translation-ru-json.401bc498.chunk.js +1 -0
  34. package/build/index.html +1 -1
  35. package/build/{main.41970e4c.js → main.14dca275.js} +183 -188
  36. package/build/review-workflows-settings.6a662ebd.chunk.js +61 -0
  37. package/build/ru-json.678cd48b.chunk.js +1 -0
  38. package/build/{runtime~main.c20330f1.js → runtime~main.1074f2bb.js} +1 -1
  39. package/build/users-permissions-translation-ru-json.8e883c67.chunk.js +1 -0
  40. package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/ReviewWorkflowsStageEE.js +12 -1
  41. package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +6 -1
  42. package/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js +4 -1
  43. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +10 -7
  44. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/OptionColor/OptionColor.js +11 -1
  45. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/components/SingleValueColor/SingleValueColor.js +14 -1
  46. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/colors.js +7 -1
  47. package/ee/server/routes/audit-logs.js +43 -0
  48. package/ee/server/routes/index.js +5 -226
  49. package/ee/server/routes/license-limit.js +29 -0
  50. package/ee/server/routes/review-workflows.js +112 -0
  51. package/ee/server/routes/sso.js +60 -0
  52. package/ee/server/routes/utils.js +15 -0
  53. package/package.json +16 -16
  54. package/server/routes/index.js +16 -11
  55. package/webpack.alias.js +0 -1
  56. package/admin/src/content-manager/components/DynamicTable/ConfirmDialogDeleteAll/index.js +0 -73
  57. package/admin/src/content-manager/pages/ListSettingsView/utils/api.js +0 -10
  58. package/build/6858.56d4d528.chunk.js +0 -50
  59. package/build/9703.e590889d.chunk.js +0 -1
  60. package/build/Upload_ConfigureTheView.4fc648b5.chunk.js +0 -1
  61. package/build/content-manager.d28eb183.chunk.js +0 -1111
  62. package/build/en-json.c7fc79af.chunk.js +0 -1
  63. package/build/i18n-translation-en-json.60af6722.chunk.js +0 -1
  64. package/build/review-workflows-settings.7c0b4e73.chunk.js +0 -61
  65. package/build/ru-json.e0662702.chunk.js +0 -1
  66. package/build/users-permissions-translation-ru-json.20e177db.chunk.js +0 -1
@@ -1,9 +1,9 @@
1
1
  import React, { useRef, useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
+ import styled from 'styled-components';
3
4
  import { useIntl } from 'react-intl';
4
5
  import {
5
6
  FocusTrap,
6
- Box,
7
7
  Button,
8
8
  IconButtonGroup,
9
9
  Option,
@@ -33,6 +33,9 @@ import {
33
33
  CustomLinkIconButton,
34
34
  } from './WysiwygStyles';
35
35
 
36
+ /**
37
+ * TODO: refactor this mess.
38
+ */
36
39
  const WysiwygNav = ({
37
40
  disabled,
38
41
  editorRef,
@@ -56,68 +59,9 @@ const WysiwygNav = ({
56
59
 
57
60
  if (disabled || isPreviewMode) {
58
61
  return (
59
- <Box padding={2} background="neutral100">
60
- <Flex justifyContent="space-between">
61
- <Flex>
62
- <Select
63
- disabled
64
- id="selectTitle"
65
- placeholder={selectPlaceholder}
66
- size="S"
67
- aria-label={selectPlaceholder}
68
- >
69
- <Option value="h1">h1</Option>
70
- <Option value="h2">h2</Option>
71
- <Option value="h3">h3</Option>
72
- <Option value="h4">h4</Option>
73
- <Option value="h5">h5</Option>
74
- <Option value="h6">h6</Option>
75
- </Select>
76
-
77
- <MainButtons>
78
- <CustomIconButton disabled id="Bold" label="Bold" name="Bold" icon={<Bold />} />
79
- <CustomIconButton
80
- disabled
81
- id="Italic"
82
- label="Italic"
83
- name="Italic"
84
- icon={<Italic />}
85
- />
86
- <CustomIconButton
87
- disabled
88
- id="Underline"
89
- label="Underline"
90
- name="Underline"
91
- icon={<Underline />}
92
- />
93
- </MainButtons>
94
-
95
- <MoreButton disabled id="more" label="More" icon={<More />} />
96
- </Flex>
97
-
98
- {!isExpandMode && (
99
- <Button onClick={onTogglePreviewMode} variant="tertiary" id="preview">
100
- {formatMessage({
101
- id: 'components.Wysiwyg.ToggleMode.markdown-mode',
102
- defaultMessage: 'Markdown mode',
103
- })}
104
- </Button>
105
- )}
106
- </Flex>
107
- </Box>
108
- );
109
- }
110
-
111
- return (
112
- <Box padding={2} background="neutral100">
113
- <Flex justifyContent="space-between">
114
- <Flex>
115
- <Select
116
- id="selectTitle"
117
- placeholder={selectPlaceholder}
118
- size="S"
119
- onChange={(value) => onActionClick(value, editorRef)}
120
- >
62
+ <Flex padding={2} background="neutral100" justifyContent="space-between">
63
+ <StyledFlex>
64
+ <Select disabled placeholder={selectPlaceholder} size="S" label={selectPlaceholder}>
121
65
  <Option value="h1">h1</Option>
122
66
  <Option value="h2">h2</Option>
123
67
  <Option value="h3">h3</Option>
@@ -127,22 +71,10 @@ const WysiwygNav = ({
127
71
  </Select>
128
72
 
129
73
  <MainButtons>
74
+ <CustomIconButton disabled id="Bold" label="Bold" name="Bold" icon={<Bold />} />
75
+ <CustomIconButton disabled id="Italic" label="Italic" name="Italic" icon={<Italic />} />
130
76
  <CustomIconButton
131
- onClick={() => onActionClick('Bold', editorRef)}
132
- id="Bold"
133
- label="Bold"
134
- name="Bold"
135
- icon={<Bold />}
136
- />
137
- <CustomIconButton
138
- onClick={() => onActionClick('Italic', editorRef)}
139
- id="Italic"
140
- label="Italic"
141
- name="Italic"
142
- icon={<Italic />}
143
- />
144
- <CustomIconButton
145
- onClick={() => onActionClick('Underline', editorRef)}
77
+ disabled
146
78
  id="Underline"
147
79
  label="Underline"
148
80
  name="Underline"
@@ -150,90 +82,145 @@ const WysiwygNav = ({
150
82
  />
151
83
  </MainButtons>
152
84
 
153
- <MoreButton
154
- ref={buttonMoreRef}
155
- onClick={handleTogglePopover}
156
- id="more"
157
- label="More"
158
- icon={<More />}
159
- />
160
- {visiblePopover && (
161
- <Popover centered source={buttonMoreRef} spacing={4} id="popover">
162
- <FocusTrap onEscape={handleTogglePopover} restoreFocus={false}>
163
- <Flex>
164
- <IconButtonGroupMargin>
165
- <CustomIconButton
166
- onClick={() => onActionClick('Strikethrough', editorRef, handleTogglePopover)}
167
- id="Strikethrough"
168
- label="Strikethrough"
169
- name="Strikethrough"
170
- icon={<StrikeThrough />}
171
- />
172
- <CustomIconButton
173
- onClick={() => onActionClick('BulletList', editorRef, handleTogglePopover)}
174
- id="BulletList"
175
- label="BulletList"
176
- name="BulletList"
177
- icon={<BulletList />}
178
- />
179
- <CustomIconButton
180
- onClick={() => onActionClick('NumberList', editorRef, handleTogglePopover)}
181
- id="NumberList"
182
- label="NumberList"
183
- name="NumberList"
184
- icon={<NumberList />}
185
- />
186
- </IconButtonGroupMargin>
187
- <IconButtonGroup>
188
- <CustomIconButton
189
- onClick={() => onActionClick('Code', editorRef, handleTogglePopover)}
190
- id="Code"
191
- label="Code"
192
- name="Code"
193
- icon={<Code />}
194
- />
195
- <CustomIconButton
196
- onClick={() => {
197
- handleTogglePopover();
198
- onToggleMediaLib();
199
- }}
200
- id="Image"
201
- label="Image"
202
- name="Image"
203
- icon={<Image />}
204
- />
205
- <CustomLinkIconButton
206
- onClick={() => onActionClick('Link', editorRef, handleTogglePopover)}
207
- id="Link"
208
- label="Link"
209
- name="Link"
210
- // eslint-disable-next-line jsx-a11y/anchor-is-valid
211
- icon={<Link />}
212
- />
213
- <CustomIconButton
214
- onClick={() => onActionClick('Quote', editorRef, handleTogglePopover)}
215
- id="Quote"
216
- label="Quote"
217
- name="Quote"
218
- icon={<Quote />}
219
- />
220
- </IconButtonGroup>
221
- </Flex>
222
- </FocusTrap>
223
- </Popover>
224
- )}
225
- </Flex>
85
+ <MoreButton disabled id="more" label="More" icon={<More />} />
86
+ </StyledFlex>
226
87
 
227
- {onTogglePreviewMode && (
88
+ {!isExpandMode && (
228
89
  <Button onClick={onTogglePreviewMode} variant="tertiary" id="preview">
229
90
  {formatMessage({
230
- id: 'components.Wysiwyg.ToggleMode.preview-mode',
231
- defaultMessage: 'Preview mode',
91
+ id: 'components.Wysiwyg.ToggleMode.markdown-mode',
92
+ defaultMessage: 'Markdown mode',
232
93
  })}
233
94
  </Button>
234
95
  )}
235
96
  </Flex>
236
- </Box>
97
+ );
98
+ }
99
+
100
+ return (
101
+ <Flex padding={2} background="neutral100" justifyContent="space-between">
102
+ <StyledFlex>
103
+ <Select
104
+ placeholder={selectPlaceholder}
105
+ label={selectPlaceholder}
106
+ size="S"
107
+ onChange={(value) => onActionClick(value, editorRef)}
108
+ >
109
+ <Option value="h1">h1</Option>
110
+ <Option value="h2">h2</Option>
111
+ <Option value="h3">h3</Option>
112
+ <Option value="h4">h4</Option>
113
+ <Option value="h5">h5</Option>
114
+ <Option value="h6">h6</Option>
115
+ </Select>
116
+
117
+ <MainButtons>
118
+ <CustomIconButton
119
+ onClick={() => onActionClick('Bold', editorRef)}
120
+ id="Bold"
121
+ label="Bold"
122
+ name="Bold"
123
+ icon={<Bold />}
124
+ />
125
+ <CustomIconButton
126
+ onClick={() => onActionClick('Italic', editorRef)}
127
+ id="Italic"
128
+ label="Italic"
129
+ name="Italic"
130
+ icon={<Italic />}
131
+ />
132
+ <CustomIconButton
133
+ onClick={() => onActionClick('Underline', editorRef)}
134
+ id="Underline"
135
+ label="Underline"
136
+ name="Underline"
137
+ icon={<Underline />}
138
+ />
139
+ </MainButtons>
140
+
141
+ <MoreButton
142
+ ref={buttonMoreRef}
143
+ onClick={handleTogglePopover}
144
+ id="more"
145
+ label="More"
146
+ icon={<More />}
147
+ />
148
+ {visiblePopover && (
149
+ <Popover centered source={buttonMoreRef} spacing={4} id="popover">
150
+ <FocusTrap onEscape={handleTogglePopover} restoreFocus={false}>
151
+ <Flex>
152
+ <IconButtonGroupMargin>
153
+ <CustomIconButton
154
+ onClick={() => onActionClick('Strikethrough', editorRef, handleTogglePopover)}
155
+ id="Strikethrough"
156
+ label="Strikethrough"
157
+ name="Strikethrough"
158
+ icon={<StrikeThrough />}
159
+ />
160
+ <CustomIconButton
161
+ onClick={() => onActionClick('BulletList', editorRef, handleTogglePopover)}
162
+ id="BulletList"
163
+ label="BulletList"
164
+ name="BulletList"
165
+ icon={<BulletList />}
166
+ />
167
+ <CustomIconButton
168
+ onClick={() => onActionClick('NumberList', editorRef, handleTogglePopover)}
169
+ id="NumberList"
170
+ label="NumberList"
171
+ name="NumberList"
172
+ icon={<NumberList />}
173
+ />
174
+ </IconButtonGroupMargin>
175
+ <IconButtonGroup>
176
+ <CustomIconButton
177
+ onClick={() => onActionClick('Code', editorRef, handleTogglePopover)}
178
+ id="Code"
179
+ label="Code"
180
+ name="Code"
181
+ icon={<Code />}
182
+ />
183
+ <CustomIconButton
184
+ onClick={() => {
185
+ handleTogglePopover();
186
+ onToggleMediaLib();
187
+ }}
188
+ id="Image"
189
+ label="Image"
190
+ name="Image"
191
+ icon={<Image />}
192
+ />
193
+ <CustomLinkIconButton
194
+ onClick={() => onActionClick('Link', editorRef, handleTogglePopover)}
195
+ id="Link"
196
+ label="Link"
197
+ name="Link"
198
+ // eslint-disable-next-line jsx-a11y/anchor-is-valid
199
+ icon={<Link />}
200
+ />
201
+ <CustomIconButton
202
+ onClick={() => onActionClick('Quote', editorRef, handleTogglePopover)}
203
+ id="Quote"
204
+ label="Quote"
205
+ name="Quote"
206
+ icon={<Quote />}
207
+ />
208
+ </IconButtonGroup>
209
+ </Flex>
210
+ </FocusTrap>
211
+ </Popover>
212
+ )}
213
+ </StyledFlex>
214
+
215
+ {onTogglePreviewMode && (
216
+ <Button onClick={onTogglePreviewMode} variant="tertiary" id="preview">
217
+ {formatMessage({
218
+ id: 'components.Wysiwyg.ToggleMode.preview-mode',
219
+ defaultMessage: 'Preview mode',
220
+ })}
221
+ </Button>
222
+ )}
223
+ </Flex>
237
224
  );
238
225
  };
239
226
 
@@ -255,3 +242,17 @@ WysiwygNav.propTypes = {
255
242
  };
256
243
 
257
244
  export default WysiwygNav;
245
+
246
+ const StyledFlex = styled(Flex)`
247
+ /* Hide the label, every input needs a label. */
248
+ label {
249
+ border: 0;
250
+ clip: rect(0 0 0 0);
251
+ height: 1px;
252
+ margin: -1px;
253
+ overflow: hidden;
254
+ padding: 0;
255
+ position: absolute;
256
+ width: 1px;
257
+ }
258
+ `;
@@ -1,4 +1,4 @@
1
- export const EXCLUDED_SORT_OPTIONS = [
1
+ export const EXCLUDED_SORT_ATTRIBUTE_TYPES = [
2
2
  'media',
3
3
  'richtext',
4
4
  'dynamiczone',
@@ -7,7 +7,13 @@ import pick from 'lodash/pick';
7
7
  import get from 'lodash/get';
8
8
  import isEmpty from 'lodash/isEmpty';
9
9
  import { stringify } from 'qs';
10
- import { useNotification, useTracking, ConfirmDialog, Link } from '@strapi/helper-plugin';
10
+ import {
11
+ useNotification,
12
+ useTracking,
13
+ ConfirmDialog,
14
+ Link,
15
+ useFetchClient,
16
+ } from '@strapi/helper-plugin';
11
17
  import { useIntl } from 'react-intl';
12
18
  import {
13
19
  Box,
@@ -19,18 +25,19 @@ import {
19
25
  Button,
20
26
  } from '@strapi/design-system';
21
27
  import { Check, ArrowLeft } from '@strapi/icons';
28
+
22
29
  import { checkIfAttributeIsDisplayable, getTrad } from '../../utils';
23
30
  import ModelsContext from '../../contexts/ModelsContext';
24
31
  import { usePluginsQueryParams } from '../../hooks';
25
- import putCMSettingsLV from './utils/api';
26
32
  import Settings from './components/Settings';
27
33
  import SortDisplayedFields from './components/SortDisplayedFields';
28
34
  import EditFieldForm from './components/EditFieldForm';
29
35
  import init from './init';
30
36
  import reducer, { initialState } from './reducer';
31
- import { EXCLUDED_SORT_OPTIONS } from './utils/excludedSortOptions';
37
+ import { EXCLUDED_SORT_ATTRIBUTE_TYPES } from './constants';
32
38
 
33
39
  const ListSettingsView = ({ layout, slug }) => {
40
+ const { put } = useFetchClient();
34
41
  const { formatMessage } = useIntl();
35
42
  const { trackUsage } = useTracking();
36
43
  const pluginsQueryParams = usePluginsQueryParams();
@@ -130,18 +137,21 @@ const ListSettingsView = ({ layout, slug }) => {
130
137
  handleCloseModal();
131
138
  };
132
139
 
133
- const submitMutation = useMutation((body) => putCMSettingsLV(body, slug), {
134
- onSuccess() {
135
- trackUsage('didEditListSettings');
136
- refetchData();
137
- },
138
- onError() {
139
- toggleNotification({
140
- type: 'warning',
141
- message: { id: 'notification.error' },
142
- });
143
- },
144
- });
140
+ const submitMutation = useMutation(
141
+ (body) => put(`/content-manager/content-types/${slug}/configuration`, body),
142
+ {
143
+ onSuccess() {
144
+ trackUsage('didEditListSettings');
145
+ refetchData();
146
+ },
147
+ onError() {
148
+ toggleNotification({
149
+ type: 'warning',
150
+ message: { id: 'notification.error' },
151
+ });
152
+ },
153
+ }
154
+ );
145
155
  const { isLoading: isSubmittingForm } = submitMutation;
146
156
 
147
157
  const handleChangeEditLabel = ({ target: { name, value } }) => {
@@ -153,29 +163,16 @@ const ListSettingsView = ({ layout, slug }) => {
153
163
  };
154
164
 
155
165
  const listRemainingFields = Object.entries(attributes)
156
- .reduce((acc, cur) => {
157
- const [attrName, fieldSchema] = cur;
158
-
159
- const isDisplayable = checkIfAttributeIsDisplayable(fieldSchema);
160
- const isAlreadyDisplayed = displayedFields.includes(attrName);
161
-
162
- if (isDisplayable && !isAlreadyDisplayed) {
163
- acc.push(attrName);
164
- }
165
-
166
- return acc;
167
- }, [])
166
+ .filter(
167
+ ([name, attribute]) =>
168
+ checkIfAttributeIsDisplayable(attribute) && !displayedFields.includes(name)
169
+ )
170
+ .map(([name]) => name)
168
171
  .sort();
169
172
 
170
- const sortOptions = Object.entries(attributes).reduce((acc, cur) => {
171
- const [name, { type }] = cur;
172
-
173
- if (!EXCLUDED_SORT_OPTIONS.includes(type)) {
174
- acc.push(name);
175
- }
176
-
177
- return acc;
178
- }, []);
173
+ const sortOptions = Object.entries(attributes)
174
+ .filter(([, attribute]) => !EXCLUDED_SORT_ATTRIBUTE_TYPES.includes(attribute.type))
175
+ .map(([name]) => name);
179
176
 
180
177
  const move = (originalIndex, atIndex) => {
181
178
  dispatch({
@@ -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
@@ -200,6 +247,70 @@ function ListView({
200
247
  [slug, params, fetchData, toggleNotification, formatAPIError, del]
201
248
  );
202
249
 
250
+ /**
251
+ * @param {number[]} selectedEntries - Array of ids to publish
252
+ * @returns {{validIds: number[], errors: Object.<number, string>}} - Returns an object with the valid ids and the errors
253
+ */
254
+ const validateEntriesToPublish = async (selectedEntries) => {
255
+ const validations = { validIds: [], errors: {} };
256
+ // Create the validation schema based on the contentType
257
+ const schema = createYupSchema(
258
+ contentType,
259
+ { components: layout.components },
260
+ { isDraft: false }
261
+ );
262
+ // Get the selected entries
263
+ const entries = data.filter((entry) => {
264
+ return selectedEntries.includes(entry.id);
265
+ });
266
+ // Validate each entry and map the unresolved promises
267
+ const validationPromises = entries.map((entry) =>
268
+ schema.validate(entry, { abortEarly: false })
269
+ );
270
+ // Resolve all the promises in one go
271
+ const resolvedPromises = await Promise.allSettled(validationPromises);
272
+ // Set the validations
273
+ resolvedPromises.forEach((promise) => {
274
+ if (promise.status === 'rejected') {
275
+ const entityId = promise.reason.value.id;
276
+ validations.errors[entityId] = getYupInnerErrors(promise.reason);
277
+ }
278
+
279
+ if (promise.status === 'fulfilled') {
280
+ validations.validIds.push(promise.value.id);
281
+ }
282
+ });
283
+
284
+ return validations;
285
+ };
286
+
287
+ const handleConfirmPublishAllData = async (selectedEntries) => {
288
+ const validations = await validateEntriesToPublish(selectedEntries);
289
+
290
+ if (Object.values(validations.errors).length) {
291
+ toggleNotification({
292
+ type: 'warning',
293
+ title: {
294
+ id: 'content-manager.listView.validation.errors.title',
295
+ defaultMessage: 'Action required',
296
+ },
297
+ message: {
298
+ id: 'content-manager.listView.validation.errors.message',
299
+ defaultMessage:
300
+ 'Please make sure all fields are valid before publishing (required field, min/max character limit, etc.)',
301
+ },
302
+ });
303
+
304
+ throw new Error('Validation error');
305
+ }
306
+
307
+ return bulkPublishMutation.mutateAsync({ ids: selectedEntries });
308
+ };
309
+
310
+ const handleConfirmUnpublishAllData = (selectedEntries) => {
311
+ return bulkUnpublishMutation.mutateAsync({ ids: selectedEntries });
312
+ };
313
+
203
314
  useEffect(() => {
204
315
  const CancelToken = axios.CancelToken;
205
316
  const source = CancelToken.source();
@@ -331,9 +442,12 @@ function ListView({
331
442
  <DynamicTable
332
443
  canCreate={canCreate}
333
444
  canDelete={canDelete}
445
+ canPublish={canPublish}
334
446
  contentTypeName={headerLayoutTitle}
335
- onConfirmDeleteAll={handleConfirmDeleteAllData}
336
447
  onConfirmDelete={handleConfirmDeleteData}
448
+ onConfirmDeleteAll={handleConfirmDeleteAllData}
449
+ onConfirmPublishAll={handleConfirmPublishAllData}
450
+ onConfirmUnpublishAll={handleConfirmUnpublishAllData}
337
451
  isBulkable={isBulkable}
338
452
  isLoading={isLoading}
339
453
  // FIXME: remove the layout props drilling
@@ -355,10 +469,12 @@ ListView.propTypes = {
355
469
  canCreate: PropTypes.bool.isRequired,
356
470
  canDelete: PropTypes.bool.isRequired,
357
471
  canRead: PropTypes.bool.isRequired,
472
+ canPublish: PropTypes.bool.isRequired,
358
473
  data: PropTypes.array.isRequired,
359
474
  layout: PropTypes.exact({
360
475
  components: PropTypes.object.isRequired,
361
476
  contentType: PropTypes.shape({
477
+ uid: PropTypes.string.isRequired,
362
478
  attributes: PropTypes.object.isRequired,
363
479
  metadatas: PropTypes.object.isRequired,
364
480
  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);
@@ -13,7 +13,12 @@ const injectionZones = {
13
13
  },
14
14
  contentManager: {
15
15
  editView: { informations: [], 'right-links': [] },
16
- listView: { actions: [], deleteModalAdditionalInfos: [] },
16
+ listView: {
17
+ actions: [],
18
+ deleteModalAdditionalInfos: [],
19
+ publishModalAdditionalInfos: [],
20
+ unpublishModalAdditionalInfos: [],
21
+ },
17
22
  },
18
23
  };
19
24