@strapi/admin 4.12.4 → 4.13.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.
- package/admin/src/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumns.js +2 -0
- package/admin/src/content-manager/components/EditViewDataManagerProvider/reducer.js +8 -1
- package/admin/src/content-manager/components/Filter/CustomInputs/AdminUsersFilter.js +42 -0
- package/admin/src/content-manager/components/{AttributeFilter/Filters.js → Filter/Filter.js} +5 -7
- package/admin/src/content-manager/components/Filter/index.js +1 -0
- package/admin/src/content-manager/hooks/useAllowedAttributes.js +47 -0
- package/admin/src/content-manager/hooks/useSyncRbac/index.js +10 -2
- package/admin/src/content-manager/pages/EditView/Information/index.js +9 -8
- package/admin/src/content-manager/pages/EditViewLayoutManager/index.js +2 -2
- package/admin/src/content-manager/pages/ListSettingsView/components/Settings.js +40 -7
- package/admin/src/content-manager/pages/ListSettingsView/index.js +6 -2
- package/admin/src/content-manager/pages/ListView/components/FieldPicker/index.js +67 -69
- package/admin/src/content-manager/pages/ListView/components/ViewSettingsMenu/index.js +74 -0
- package/admin/src/content-manager/pages/ListView/index.js +254 -68
- package/admin/src/content-manager/pages/ListViewLayoutManager/index.js +2 -2
- package/admin/src/content-manager/utils/getDisplayName.js +33 -0
- package/admin/src/content-manager/utils/index.js +1 -0
- package/admin/src/translations/en.json +3 -1
- package/admin/src/translations/zh-Hans.json +918 -902
- package/build/1227.32fe57ce.chunk.js +1 -0
- package/build/4174.fa8f9954.chunk.js +1 -0
- package/build/4546.ff09eeda.chunk.js +1 -0
- package/build/4724.baf7c5b1.chunk.js +6 -0
- package/build/6158.c3c13c20.chunk.js +1 -0
- package/build/78.dcc6df5c.chunk.js +1 -0
- package/build/{9806.3392505e.chunk.js → 9806.5d5a0e8d.chunk.js} +16 -16
- package/build/{Admin-authenticatedApp.f5ece8ff.chunk.js → Admin-authenticatedApp.53a24d28.chunk.js} +2 -2
- package/build/audit-logs-settings-page.0f73ccf8.chunk.js +1 -0
- package/build/content-manager.7f96a2f1.chunk.js +1097 -0
- package/build/{content-type-builder.40534de5.chunk.js → content-type-builder.cd999f6e.chunk.js} +2 -2
- package/build/{en-json.08c05fcf.chunk.js → en-json.4f06fe03.chunk.js} +1 -1
- package/build/i18n-translation-ru-json.a3dbc125.chunk.js +1 -0
- package/build/index.html +1 -1
- package/build/main.40b94779.js +2859 -0
- package/build/{runtime~main.bb4efc54.js → runtime~main.b16af570.js} +2 -2
- package/build/users-permissions-translation-zh-Hans-json.8d82c809.chunk.js +1 -0
- package/build/{users-roles-settings-page.3f9f063e.chunk.js → users-roles-settings-page.9d9a1eff.chunk.js} +1 -1
- package/build/zh-Hans-json.97efd015.chunk.js +1 -0
- package/ee/admin/content-manager/components/Filter/CustomInputs/ReviewWorkflows/AssigneeFilter.js +42 -0
- package/ee/admin/content-manager/components/Filter/CustomInputs/ReviewWorkflows/StageFilter.js +70 -0
- package/ee/admin/content-manager/components/Filter/CustomInputs/ReviewWorkflows/constants.js +71 -0
- package/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js +9 -217
- package/ee/admin/content-manager/pages/EditView/InformationBox/components/AssigneeSelect/AssigneeSelect.js +149 -0
- package/ee/admin/content-manager/pages/EditView/InformationBox/components/AssigneeSelect/index.js +1 -0
- package/ee/admin/content-manager/pages/EditView/InformationBox/components/StageSelect/StageSelect.js +241 -0
- package/ee/admin/content-manager/pages/EditView/InformationBox/components/StageSelect/index.js +1 -0
- package/ee/admin/content-manager/pages/EditView/InformationBox/constants.js +2 -0
- package/ee/admin/content-manager/pages/ListSettingsView/constants.js +7 -0
- package/ee/admin/content-manager/pages/ListView/ReviewWorkflowsColumn/ReviewWorkflowsAssigneeEE.js +51 -0
- package/ee/admin/content-manager/pages/ListView/ReviewWorkflowsColumn/constants.js +44 -17
- package/ee/admin/content-manager/pages/ListView/ReviewWorkflowsColumn/index.js +1 -0
- package/ee/server/constants/workflows.js +1 -0
- package/ee/server/controllers/index.js +1 -0
- package/ee/server/controllers/workflows/assignees/index.js +44 -0
- package/ee/server/routes/review-workflows.js +17 -0
- package/ee/server/services/index.js +1 -0
- package/ee/server/services/review-workflows/assignees.js +54 -0
- package/ee/server/services/review-workflows/metrics/index.js +5 -0
- package/ee/server/services/review-workflows/review-workflows.js +20 -11
- package/ee/server/validation/review-workflows.js +8 -0
- package/package.json +10 -10
- package/server/services/permission/permissions-manager/sanitize.js +12 -0
- package/admin/src/content-manager/components/AttributeFilter/hooks/useAllowedAttributes.js +0 -42
- package/admin/src/content-manager/components/AttributeFilter/index.js +0 -40
- package/build/3984.dda474f7.chunk.js +0 -1
- package/build/4546.cfafae68.chunk.js +0 -1
- package/build/5483.6dd2e776.chunk.js +0 -6
- package/build/6158.c974fd83.chunk.js +0 -1
- package/build/audit-logs-settings-page.4b422831.chunk.js +0 -1
- package/build/content-manager.2af15f57.chunk.js +0 -1099
- package/build/i18n-translation-ru-json.401bc498.chunk.js +0 -1
- package/build/main.f13fc96c.js +0 -2856
- package/build/users-permissions-translation-zh-Hans-json.6ab714ee.chunk.js +0 -1
- package/build/zh-Hans-json.937b395b.chunk.js +0 -1
|
@@ -239,7 +239,14 @@ const reducer = (state, action) =>
|
|
|
239
239
|
(value) => {
|
|
240
240
|
return value.type === 'relation';
|
|
241
241
|
},
|
|
242
|
-
(
|
|
242
|
+
(value, { path }) => {
|
|
243
|
+
const relationFieldName = path[path.length - 1];
|
|
244
|
+
|
|
245
|
+
// When editing, we don't want to fetch the relations with creator fields because we already have it
|
|
246
|
+
if (value && (relationFieldName === 'createdBy' || relationFieldName === 'updatedBy')) {
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
|
|
243
250
|
if (state.modifiedData?.id === data.id && get(state.modifiedData, path)) {
|
|
244
251
|
return get(state.modifiedData, path);
|
|
245
252
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Combobox, ComboboxOption } from '@strapi/design-system';
|
|
4
|
+
import PropTypes from 'prop-types';
|
|
5
|
+
import { useIntl } from 'react-intl';
|
|
6
|
+
|
|
7
|
+
import { useAdminUsers } from '../../../../hooks/useAdminUsers';
|
|
8
|
+
import { getDisplayName } from '../../../utils';
|
|
9
|
+
|
|
10
|
+
export const AdminUsersFilter = ({ value, onChange }) => {
|
|
11
|
+
const { formatMessage } = useIntl();
|
|
12
|
+
const { users, isLoading } = useAdminUsers();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Combobox
|
|
16
|
+
value={value}
|
|
17
|
+
aria-label={formatMessage({
|
|
18
|
+
id: 'content-manager.components.Filters.usersSelect.label',
|
|
19
|
+
defaultMessage: 'Search and select an user to filter',
|
|
20
|
+
})}
|
|
21
|
+
onChange={onChange}
|
|
22
|
+
loading={isLoading}
|
|
23
|
+
>
|
|
24
|
+
{users.map((user) => {
|
|
25
|
+
return (
|
|
26
|
+
<ComboboxOption key={user.id} value={user.id.toString()}>
|
|
27
|
+
{getDisplayName(user, formatMessage)}
|
|
28
|
+
</ComboboxOption>
|
|
29
|
+
);
|
|
30
|
+
})}
|
|
31
|
+
</Combobox>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
AdminUsersFilter.propTypes = {
|
|
36
|
+
onChange: PropTypes.func.isRequired,
|
|
37
|
+
value: PropTypes.string,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
AdminUsersFilter.defaultProps = {
|
|
41
|
+
value: '',
|
|
42
|
+
};
|
package/admin/src/content-manager/components/{AttributeFilter/Filters.js → Filter/Filter.js}
RENAMED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React, { useRef, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Button, Box } from '@strapi/design-system';
|
|
4
4
|
import { FilterListURLQuery, FilterPopoverURLQuery, useTracking } from '@strapi/helper-plugin';
|
|
5
|
-
import { Filter } from '@strapi/icons';
|
|
5
|
+
import { Filter as FilterIcon } from '@strapi/icons';
|
|
6
6
|
import PropTypes from 'prop-types';
|
|
7
7
|
import { useIntl } from 'react-intl';
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
export const Filter = ({ displayedFilters }) => {
|
|
10
10
|
const [isVisible, setIsVisible] = useState(false);
|
|
11
11
|
const { formatMessage } = useIntl();
|
|
12
12
|
const buttonRef = useRef();
|
|
@@ -25,7 +25,7 @@ const Filters = ({ displayedFilters }) => {
|
|
|
25
25
|
<Button
|
|
26
26
|
variant="tertiary"
|
|
27
27
|
ref={buttonRef}
|
|
28
|
-
startIcon={<
|
|
28
|
+
startIcon={<FilterIcon />}
|
|
29
29
|
onClick={handleToggle}
|
|
30
30
|
size="S"
|
|
31
31
|
>
|
|
@@ -45,7 +45,7 @@ const Filters = ({ displayedFilters }) => {
|
|
|
45
45
|
);
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
Filter.propTypes = {
|
|
49
49
|
displayedFilters: PropTypes.arrayOf(
|
|
50
50
|
PropTypes.shape({
|
|
51
51
|
name: PropTypes.string.isRequired,
|
|
@@ -54,5 +54,3 @@ Filters.propTypes = {
|
|
|
54
54
|
})
|
|
55
55
|
).isRequired,
|
|
56
56
|
};
|
|
57
|
-
|
|
58
|
-
export default Filters;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Filter';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useRBACProvider, findMatchingPermissions } from '@strapi/helper-plugin';
|
|
2
|
+
|
|
3
|
+
const NOT_ALLOWED_FILTERS = ['json', 'component', 'media', 'richtext', 'dynamiczone', 'password'];
|
|
4
|
+
const TIMESTAMPS = ['createdAt', 'updatedAt'];
|
|
5
|
+
const CREATOR_ATTRIBUTES = ['createdBy', 'updatedBy'];
|
|
6
|
+
|
|
7
|
+
export const useAllowedAttributes = (contentType, slug) => {
|
|
8
|
+
const { allPermissions } = useRBACProvider();
|
|
9
|
+
|
|
10
|
+
const readPermissionsForSlug = findMatchingPermissions(allPermissions, [
|
|
11
|
+
{
|
|
12
|
+
action: 'plugin::content-manager.explorer.read',
|
|
13
|
+
subject: slug,
|
|
14
|
+
},
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const canReadAdminUsers =
|
|
18
|
+
findMatchingPermissions(allPermissions, [
|
|
19
|
+
{
|
|
20
|
+
action: 'admin::users.read',
|
|
21
|
+
subject: null,
|
|
22
|
+
},
|
|
23
|
+
]).length > 0;
|
|
24
|
+
|
|
25
|
+
const attributesWithReadPermissions = readPermissionsForSlug?.[0]?.properties?.fields ?? [];
|
|
26
|
+
|
|
27
|
+
const allowedAttributes = attributesWithReadPermissions.filter((attr) => {
|
|
28
|
+
const current = contentType?.attributes?.[attr] ?? {};
|
|
29
|
+
|
|
30
|
+
if (!current.type) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (NOT_ALLOWED_FILTERS.includes(current.type)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return true;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return [
|
|
42
|
+
'id',
|
|
43
|
+
...allowedAttributes,
|
|
44
|
+
...TIMESTAMPS,
|
|
45
|
+
...(canReadAdminUsers ? CREATOR_ATTRIBUTES : []),
|
|
46
|
+
];
|
|
47
|
+
};
|
|
@@ -6,9 +6,10 @@ import { resetPermissions, setPermissions } from './actions';
|
|
|
6
6
|
import { selectCollectionTypePermissions, selectPermissions } from './selectors';
|
|
7
7
|
|
|
8
8
|
const useSyncRbac = (query, collectionTypeUID, containerName = 'listView') => {
|
|
9
|
+
const dispatch = useDispatch();
|
|
10
|
+
|
|
9
11
|
const collectionTypesRelatedPermissions = useSelector(selectCollectionTypePermissions);
|
|
10
12
|
const permissions = useSelector(selectPermissions);
|
|
11
|
-
const dispatch = useDispatch();
|
|
12
13
|
|
|
13
14
|
const relatedPermissions = collectionTypesRelatedPermissions[collectionTypeUID];
|
|
14
15
|
|
|
@@ -24,7 +25,14 @@ const useSyncRbac = (query, collectionTypeUID, containerName = 'listView') => {
|
|
|
24
25
|
return () => {};
|
|
25
26
|
}, [relatedPermissions, dispatch, query, containerName]);
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
// Check if the permissions are related to the current collectionTypeUID
|
|
29
|
+
const isPermissionMismatch =
|
|
30
|
+
permissions?.some((permission) => permission.subject !== collectionTypeUID) ?? true;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
isValid: permissions && !isPermissionMismatch,
|
|
34
|
+
permissions,
|
|
35
|
+
};
|
|
28
36
|
};
|
|
29
37
|
|
|
30
38
|
export default useSyncRbac;
|
|
@@ -5,8 +5,7 @@ import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
|
|
5
5
|
import PropTypes from 'prop-types';
|
|
6
6
|
import { useIntl } from 'react-intl';
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import { getTrad } from '../../../utils';
|
|
8
|
+
import { getTrad, getDisplayName } from '../../../utils';
|
|
10
9
|
|
|
11
10
|
import getUnits from './utils/getUnits';
|
|
12
11
|
|
|
@@ -42,9 +41,13 @@ const KeyValuePair = ({ label, value }) => {
|
|
|
42
41
|
);
|
|
43
42
|
};
|
|
44
43
|
|
|
44
|
+
KeyValuePair.defaultProps = {
|
|
45
|
+
value: '-',
|
|
46
|
+
};
|
|
47
|
+
|
|
45
48
|
KeyValuePair.propTypes = {
|
|
46
49
|
label: PropTypes.string.isRequired,
|
|
47
|
-
value: PropTypes.string
|
|
50
|
+
value: PropTypes.string,
|
|
48
51
|
};
|
|
49
52
|
|
|
50
53
|
const Body = () => {
|
|
@@ -53,18 +56,16 @@ const Body = () => {
|
|
|
53
56
|
const currentTime = useRef(Date.now());
|
|
54
57
|
|
|
55
58
|
const getFieldInfo = (atField, byField) => {
|
|
56
|
-
const
|
|
59
|
+
const user = initialData[byField] ?? {};
|
|
57
60
|
|
|
58
|
-
const
|
|
59
|
-
const userLastname = lastname ?? '';
|
|
60
|
-
const user = username ?? getFullName(userFirstname, userLastname);
|
|
61
|
+
const displayName = getDisplayName(user, formatMessage);
|
|
61
62
|
const timestamp = initialData[atField] ? new Date(initialData[atField]).getTime() : Date.now();
|
|
62
63
|
const elapsed = timestamp - currentTime.current;
|
|
63
64
|
const { unit, value } = getUnits(-elapsed);
|
|
64
65
|
|
|
65
66
|
return {
|
|
66
67
|
at: formatRelativeTime(value, unit, { numeric: 'auto' }),
|
|
67
|
-
by: isCreatingEntry ? '-' :
|
|
68
|
+
by: isCreatingEntry ? '-' : displayName,
|
|
68
69
|
};
|
|
69
70
|
};
|
|
70
71
|
|
|
@@ -16,7 +16,7 @@ const EditViewLayoutManager = ({ layout, ...rest }) => {
|
|
|
16
16
|
const dispatch = useDispatch();
|
|
17
17
|
const [{ query }] = useQueryParams();
|
|
18
18
|
const { runHookWaterfall } = useStrapiApp();
|
|
19
|
-
const permissions = useSyncRbac(query, rest.slug, 'editView');
|
|
19
|
+
const { permissions, isValid: isValidPermissions } = useSyncRbac(query, rest.slug, 'editView');
|
|
20
20
|
|
|
21
21
|
useEffect(() => {
|
|
22
22
|
// Allow the plugins to extend the edit view layout
|
|
@@ -29,7 +29,7 @@ const EditViewLayoutManager = ({ layout, ...rest }) => {
|
|
|
29
29
|
};
|
|
30
30
|
}, [layout, dispatch, query, runHookWaterfall]);
|
|
31
31
|
|
|
32
|
-
if (!currentLayout || !
|
|
32
|
+
if (!currentLayout || !isValidPermissions) {
|
|
33
33
|
return <LoadingIndicatorPage />;
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -10,14 +10,41 @@ import {
|
|
|
10
10
|
ToggleInput,
|
|
11
11
|
Typography,
|
|
12
12
|
} from '@strapi/design-system';
|
|
13
|
+
import { useCollator } from '@strapi/helper-plugin';
|
|
13
14
|
import PropTypes from 'prop-types';
|
|
14
15
|
import { useIntl } from 'react-intl';
|
|
15
16
|
|
|
17
|
+
import { useEnterprise } from '../../../../hooks/useEnterprise';
|
|
16
18
|
import { getTrad } from '../../../utils';
|
|
17
19
|
|
|
18
|
-
export const Settings = ({
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
export const Settings = ({
|
|
21
|
+
contentTypeOptions,
|
|
22
|
+
modifiedData,
|
|
23
|
+
onChange,
|
|
24
|
+
sortOptions: sortOptionsCE,
|
|
25
|
+
}) => {
|
|
26
|
+
const { formatMessage, locale } = useIntl();
|
|
27
|
+
const formatter = useCollator(locale, {
|
|
28
|
+
sensitivity: 'base',
|
|
29
|
+
});
|
|
30
|
+
const sortOptions = useEnterprise(
|
|
31
|
+
sortOptionsCE,
|
|
32
|
+
async () =>
|
|
33
|
+
(await import('../../../../../../ee/admin/content-manager/pages/ListSettingsView/constants'))
|
|
34
|
+
.REVIEW_WORKFLOW_STAGE_SORT_OPTION_NAME,
|
|
35
|
+
{
|
|
36
|
+
combine(ceOptions, eeOption) {
|
|
37
|
+
return [...ceOptions, { ...eeOption, label: formatMessage(eeOption.label) }];
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
defaultValue: sortOptionsCE,
|
|
41
|
+
|
|
42
|
+
enabled: !!contentTypeOptions?.reviewWorkflows,
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const sortOptionsSorted = sortOptions.sort((a, b) => formatter.compare(a.label, b.label));
|
|
47
|
+
const { settings } = modifiedData;
|
|
21
48
|
|
|
22
49
|
return (
|
|
23
50
|
<Flex direction="column" alignItems="stretch" gap={4}>
|
|
@@ -129,9 +156,9 @@ export const Settings = ({ modifiedData, onChange, sortOptions }) => {
|
|
|
129
156
|
name="settings.defaultSortBy"
|
|
130
157
|
value={modifiedData.settings.defaultSortBy || ''}
|
|
131
158
|
>
|
|
132
|
-
{
|
|
133
|
-
<Option key={
|
|
134
|
-
{
|
|
159
|
+
{sortOptionsSorted.map(({ value, label }) => (
|
|
160
|
+
<Option key={value} value={value}>
|
|
161
|
+
{label}
|
|
135
162
|
</Option>
|
|
136
163
|
))}
|
|
137
164
|
</Select>
|
|
@@ -164,7 +191,13 @@ Settings.defaultProps = {
|
|
|
164
191
|
};
|
|
165
192
|
|
|
166
193
|
Settings.propTypes = {
|
|
194
|
+
contentTypeOptions: PropTypes.object.isRequired,
|
|
167
195
|
modifiedData: PropTypes.object,
|
|
168
196
|
onChange: PropTypes.func.isRequired,
|
|
169
|
-
sortOptions: PropTypes.
|
|
197
|
+
sortOptions: PropTypes.arrayOf(
|
|
198
|
+
PropTypes.shape({
|
|
199
|
+
value: PropTypes.string,
|
|
200
|
+
label: PropTypes.string,
|
|
201
|
+
}).isRequired
|
|
202
|
+
),
|
|
170
203
|
};
|
|
@@ -55,7 +55,7 @@ const ListSettingsView = ({ layout, slug }) => {
|
|
|
55
55
|
|
|
56
56
|
const isModalFormOpen = Object.keys(fieldForm).length !== 0;
|
|
57
57
|
|
|
58
|
-
const { attributes } = layout;
|
|
58
|
+
const { attributes, options } = layout;
|
|
59
59
|
const displayedFields = modifiedData.layouts.list;
|
|
60
60
|
|
|
61
61
|
const goBackUrl = () => {
|
|
@@ -179,7 +179,10 @@ const ListSettingsView = ({ layout, slug }) => {
|
|
|
179
179
|
|
|
180
180
|
const sortOptions = Object.entries(attributes)
|
|
181
181
|
.filter(([, attribute]) => !EXCLUDED_SORT_ATTRIBUTE_TYPES.includes(attribute.type))
|
|
182
|
-
.map(([name]) =>
|
|
182
|
+
.map(([name]) => ({
|
|
183
|
+
value: name,
|
|
184
|
+
label: layout.metadatas[name].list.label,
|
|
185
|
+
}));
|
|
183
186
|
|
|
184
187
|
const move = (originalIndex, atIndex) => {
|
|
185
188
|
dispatch({
|
|
@@ -235,6 +238,7 @@ const ListSettingsView = ({ layout, slug }) => {
|
|
|
235
238
|
paddingRight={7}
|
|
236
239
|
>
|
|
237
240
|
<Settings
|
|
241
|
+
contentTypeOptions={options}
|
|
238
242
|
modifiedData={modifiedData}
|
|
239
243
|
onChange={handleChange}
|
|
240
244
|
sortOptions={sortOptions}
|
|
@@ -1,81 +1,93 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { useTracking } from '@strapi/helper-plugin';
|
|
3
|
+
import { Flex, BaseCheckbox, TextButton, Typography } from '@strapi/design-system';
|
|
4
|
+
import { useCollator, useTracking } from '@strapi/helper-plugin';
|
|
5
5
|
import PropTypes from 'prop-types';
|
|
6
6
|
import { useIntl } from 'react-intl';
|
|
7
7
|
import { useDispatch, useSelector } from 'react-redux';
|
|
8
|
+
import styled from 'styled-components';
|
|
8
9
|
|
|
9
|
-
import {
|
|
10
|
-
import { onChangeListHeaders } from '../../actions';
|
|
10
|
+
import { checkIfAttributeIsDisplayable } from '../../../../utils';
|
|
11
|
+
import { onChangeListHeaders, onResetListHeaders } from '../../actions';
|
|
11
12
|
import { selectDisplayedHeaders } from '../../selectors';
|
|
12
13
|
|
|
14
|
+
const ChackboxWrapper = styled(Flex)`
|
|
15
|
+
:hover {
|
|
16
|
+
background-color: ${(props) => props.theme.colors.primary100};
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
|
|
13
20
|
export const FieldPicker = ({ layout }) => {
|
|
14
21
|
const dispatch = useDispatch();
|
|
15
22
|
const displayedHeaders = useSelector(selectDisplayedHeaders);
|
|
16
23
|
const { trackUsage } = useTracking();
|
|
17
|
-
const { formatMessage } = useIntl();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const metadatas = layout.contentType.metadatas[attrName].list;
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
name: attrName,
|
|
24
|
-
intlLabel: { id: metadatas.label, defaultMessage: metadatas.label },
|
|
25
|
-
};
|
|
24
|
+
const { formatMessage, locale } = useIntl();
|
|
25
|
+
const formatter = useCollator(locale, {
|
|
26
|
+
sensitivity: 'base',
|
|
26
27
|
});
|
|
27
28
|
|
|
28
|
-
const
|
|
29
|
+
const columns = Object.keys(layout.contentType.attributes)
|
|
30
|
+
.filter((name) => checkIfAttributeIsDisplayable(layout.contentType.attributes[name]))
|
|
31
|
+
.map((name) => ({
|
|
32
|
+
name,
|
|
33
|
+
label: layout.contentType.metadatas[name].list.label,
|
|
34
|
+
}))
|
|
35
|
+
.sort((a, b) => formatter.compare(a.label, b.label));
|
|
29
36
|
|
|
30
|
-
const
|
|
31
|
-
trackUsage('didChangeDisplayedFields');
|
|
37
|
+
const displayedHeaderKeys = displayedHeaders.map(({ name }) => name);
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
dispatch(onChangeListHeaders({ name: removedHeader[0], value: true }));
|
|
40
|
-
} else {
|
|
41
|
-
const addedHeader = updatedValues.filter((value) => {
|
|
42
|
-
return values.indexOf(value) === -1;
|
|
43
|
-
});
|
|
39
|
+
const handleChange = (name) => {
|
|
40
|
+
trackUsage('didChangeDisplayedFields');
|
|
41
|
+
dispatch(onChangeListHeaders({ name, value: displayedHeaderKeys.includes(name) }));
|
|
42
|
+
};
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const handleReset = () => {
|
|
45
|
+
dispatch(onResetListHeaders());
|
|
47
46
|
};
|
|
48
47
|
|
|
49
48
|
return (
|
|
50
|
-
<
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
49
|
+
<Flex as="fieldset" direction="column" alignItems="stretch" gap={3}>
|
|
50
|
+
<Flex justifyContent="space-between">
|
|
51
|
+
<Typography as="legend" variant="pi" fontWeight="bold">
|
|
52
|
+
{formatMessage({
|
|
53
|
+
id: 'containers.ListPage.displayedFields',
|
|
54
|
+
defaultMessage: 'Displayed fields',
|
|
55
|
+
})}
|
|
56
|
+
</Typography>
|
|
57
|
+
|
|
58
|
+
<TextButton onClick={handleReset}>
|
|
59
|
+
{formatMessage({
|
|
60
|
+
id: 'app.components.Button.reset',
|
|
61
|
+
defaultMessage: 'Reset',
|
|
62
|
+
})}
|
|
63
|
+
</TextButton>
|
|
64
|
+
</Flex>
|
|
65
|
+
|
|
66
|
+
<Flex direction="column" alignItems="stretch">
|
|
67
|
+
{columns.map((header) => {
|
|
68
|
+
const isActive = displayedHeaderKeys.includes(header.name);
|
|
69
|
+
|
|
68
70
|
return (
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
71
|
+
<ChackboxWrapper
|
|
72
|
+
wrap="wrap"
|
|
73
|
+
gap={2}
|
|
74
|
+
as="label"
|
|
75
|
+
background={isActive ? 'primary100' : 'transparent'}
|
|
76
|
+
hasRadius
|
|
77
|
+
padding={2}
|
|
78
|
+
key={header.name}
|
|
79
|
+
>
|
|
80
|
+
<BaseCheckbox
|
|
81
|
+
onChange={() => handleChange(header.name)}
|
|
82
|
+
value={isActive}
|
|
83
|
+
name={header.name}
|
|
84
|
+
/>
|
|
85
|
+
<Typography fontSize={1}>{header.label}</Typography>
|
|
86
|
+
</ChackboxWrapper>
|
|
75
87
|
);
|
|
76
88
|
})}
|
|
77
|
-
</
|
|
78
|
-
</
|
|
89
|
+
</Flex>
|
|
90
|
+
</Flex>
|
|
79
91
|
);
|
|
80
92
|
};
|
|
81
93
|
|
|
@@ -92,17 +104,3 @@ FieldPicker.propTypes = {
|
|
|
92
104
|
}).isRequired,
|
|
93
105
|
}).isRequired,
|
|
94
106
|
};
|
|
95
|
-
|
|
96
|
-
const getAllAllowedHeaders = (attributes) => {
|
|
97
|
-
const allowedAttributes = Object.keys(attributes).reduce((acc, current) => {
|
|
98
|
-
const attribute = attributes[current];
|
|
99
|
-
|
|
100
|
-
if (checkIfAttributeIsDisplayable(attribute)) {
|
|
101
|
-
acc.push(current);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return acc;
|
|
105
|
-
}, []);
|
|
106
|
-
|
|
107
|
-
return allowedAttributes.sort();
|
|
108
|
-
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Flex, IconButton, Popover } from '@strapi/design-system';
|
|
4
|
+
import { CheckPermissions, LinkButton } from '@strapi/helper-plugin';
|
|
5
|
+
import { Cog, Layer } from '@strapi/icons';
|
|
6
|
+
import PropTypes from 'prop-types';
|
|
7
|
+
import { useIntl } from 'react-intl';
|
|
8
|
+
import { useSelector } from 'react-redux';
|
|
9
|
+
|
|
10
|
+
import { selectAdminPermissions } from '../../../../../pages/App/selectors';
|
|
11
|
+
import { FieldPicker } from '../FieldPicker';
|
|
12
|
+
|
|
13
|
+
export const ViewSettingsMenu = ({ slug, layout }) => {
|
|
14
|
+
const [isVisible, setIsVisible] = React.useState(false);
|
|
15
|
+
const cogButtonRef = React.useRef();
|
|
16
|
+
const permissions = useSelector(selectAdminPermissions);
|
|
17
|
+
const { formatMessage } = useIntl();
|
|
18
|
+
|
|
19
|
+
const handleToggle = () => {
|
|
20
|
+
setIsVisible((prev) => !prev);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<IconButton
|
|
26
|
+
icon={<Cog />}
|
|
27
|
+
label={formatMessage({
|
|
28
|
+
id: 'components.ViewSettings.tooltip',
|
|
29
|
+
defaultMessage: 'View Settings',
|
|
30
|
+
})}
|
|
31
|
+
ref={cogButtonRef}
|
|
32
|
+
onClick={handleToggle}
|
|
33
|
+
/>
|
|
34
|
+
{isVisible && (
|
|
35
|
+
<Popover placement="bottom-end" source={cogButtonRef} onDismiss={handleToggle} padding={2}>
|
|
36
|
+
<Flex alignItems="stretch" direction="column" gap={3}>
|
|
37
|
+
<CheckPermissions
|
|
38
|
+
permissions={permissions.contentManager.collectionTypesConfigurations}
|
|
39
|
+
>
|
|
40
|
+
<LinkButton
|
|
41
|
+
size="S"
|
|
42
|
+
startIcon={<Layer />}
|
|
43
|
+
to={`${slug}/configurations/list`}
|
|
44
|
+
variant="secondary"
|
|
45
|
+
>
|
|
46
|
+
{formatMessage({
|
|
47
|
+
id: 'app.links.configure-view',
|
|
48
|
+
defaultMessage: 'Configure the view',
|
|
49
|
+
})}
|
|
50
|
+
</LinkButton>
|
|
51
|
+
</CheckPermissions>
|
|
52
|
+
|
|
53
|
+
<FieldPicker layout={layout} />
|
|
54
|
+
</Flex>
|
|
55
|
+
</Popover>
|
|
56
|
+
)}
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
ViewSettingsMenu.propTypes = {
|
|
62
|
+
slug: PropTypes.string.isRequired,
|
|
63
|
+
layout: PropTypes.shape({
|
|
64
|
+
contentType: PropTypes.shape({
|
|
65
|
+
attributes: PropTypes.object.isRequired,
|
|
66
|
+
metadatas: PropTypes.object.isRequired,
|
|
67
|
+
layouts: PropTypes.shape({
|
|
68
|
+
list: PropTypes.array.isRequired,
|
|
69
|
+
}).isRequired,
|
|
70
|
+
options: PropTypes.object.isRequired,
|
|
71
|
+
settings: PropTypes.object.isRequired,
|
|
72
|
+
}).isRequired,
|
|
73
|
+
}).isRequired,
|
|
74
|
+
};
|