diff --git a/.eslintrc.front.js b/.eslintrc.front.js
index d0a6fb5f83..34565411f9 100644
--- a/.eslintrc.front.js
+++ b/.eslintrc.front.js
@@ -118,5 +118,6 @@ module.exports = {
'react/state-in-constructor': 0,
'react/static-property-placement': 0,
'react/display-name': 0,
+ 'react/jsx-wrap-multilines': 0,
},
};
diff --git a/packages/core/admin/admin/src/components/Roles/RoleList/RoleRow.js b/packages/core/admin/admin/src/components/Roles/RoleList/RoleRow.js
index 0939aff4a1..117ff7ec69 100644
--- a/packages/core/admin/admin/src/components/Roles/RoleList/RoleRow.js
+++ b/packages/core/admin/admin/src/components/Roles/RoleList/RoleRow.js
@@ -1,48 +1,66 @@
-import React from 'react';
+import { Box, Row, Td, Text, Tr, IconButton, BaseCheckbox } from '@strapi/parts';
import PropTypes from 'prop-types';
-import { CustomRow } from '@buffetjs/styles';
-import { IconLinks, Text } from '@buffetjs/core';
+import React from 'react';
import { useIntl } from 'react-intl';
-
import RoleDescription from './RoleDescription';
-const RoleRow = ({ role, onClick, links, prefix }) => {
+const RoleRow = ({ onToggle, id, name, description, usersCount, isChecked, icons }) => {
const { formatMessage } = useIntl();
- const number = role.usersCount;
- const text = formatMessage(
- { id: `Roles.RoleRow.user-count.${number > 1 ? 'plural' : 'singular'}` },
- { number }
+
+ const usersCountText = formatMessage(
+ { id: `Roles.RoleRow.user-count.${usersCount > 1 ? 'plural' : 'singular'}` },
+ { number: usersCount }
);
return (
-
- {prefix && | {prefix} | }
-
- {role.name}
- |
-
- {role.description}
- |
-
- {text}
- |
-
-
- |
-
+
+ {Boolean(onToggle) && (
+ |
+ onToggle(id)}
+ value={isChecked}
+ aria-label={formatMessage({ id: `Roles.RoleRow.select-all` }, { name })}
+ />
+ |
+ )}
+
+ {name}
+ |
+
+ {description}
+ |
+
+ {usersCountText}
+ |
+
+
+ {icons.map((icon, i) =>
+ icon ? (
+
+
+
+ ) : null
+ )}
+
+ |
+
);
};
RoleRow.defaultProps = {
- onClick: null,
- prefix: null,
+ onToggle: undefined,
+ isChecked: undefined,
};
RoleRow.propTypes = {
- links: PropTypes.array.isRequired,
- onClick: PropTypes.func,
- prefix: PropTypes.node,
- role: PropTypes.object.isRequired,
+ id: PropTypes.number.isRequired,
+ name: PropTypes.string.isRequired,
+ description: PropTypes.string.isRequired,
+ usersCount: PropTypes.number.isRequired,
+ icons: PropTypes.array.isRequired,
+ onToggle: PropTypes.func,
+ isChecked: PropTypes.bool,
};
export default RoleRow;
diff --git a/packages/core/admin/admin/src/hooks/useRolesList/index.js b/packages/core/admin/admin/src/hooks/useRolesList/index.js
index d3bc0ce504..5ace36a7e8 100644
--- a/packages/core/admin/admin/src/hooks/useRolesList/index.js
+++ b/packages/core/admin/admin/src/hooks/useRolesList/index.js
@@ -1,4 +1,4 @@
-import { useEffect, useReducer } from 'react';
+import { useEffect, useReducer, useCallback } from 'react';
import { request, useNotification } from '@strapi/helper-plugin';
import { get } from 'lodash';
import init from './init';
@@ -17,7 +17,7 @@ const useRolesList = (shouldFetchData = true) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldFetchData]);
- const fetchRolesList = async () => {
+ const fetchRolesList = useCallback(async () => {
try {
dispatch({
type: 'GET_DATA',
@@ -43,7 +43,7 @@ const useRolesList = (shouldFetchData = true) => {
});
}
}
- };
+ }, [toggleNotification]);
return { roles, isLoading, getData: fetchRolesList };
};
diff --git a/packages/core/admin/admin/src/pages/Admin/index.js b/packages/core/admin/admin/src/pages/Admin/index.js
index 8f9d8a8d5b..5bee443605 100644
--- a/packages/core/admin/admin/src/pages/Admin/index.js
+++ b/packages/core/admin/admin/src/pages/Admin/index.js
@@ -81,7 +81,6 @@ const Admin = () => {
{
- const { formatMessage } = useIntl();
- const { push } = useHistory();
- const [isOpen, setIsOpen] = useState(false);
- const { trackUsage } = useTracking();
+const useSortedRoles = () => {
const { roles, isLoading } = useRolesList();
- const { toggleHeaderSearch } = useSettingsHeaderSearchContext();
- const {
- allowedActions: { canUpdate },
- } = useRBAC(adminPermissions.settings.roles);
+
const query = useQuery();
const _q = decodeURIComponent(query.get('_q') || '');
- const results = matchSorter(roles, _q, { keys: ['name', 'description'] });
+ const sortedRoles = matchSorter(roles, _q, { keys: ['name', 'description'] });
- useEffect(() => {
- toggleHeaderSearch({ id: 'Settings.permissions.menu.link.roles.label' });
+ return { isLoading, sortedRoles };
+};
- return () => {
- toggleHeaderSearch();
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+const useRoleActions = () => {
+ const { formatMessage } = useIntl();
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const { trackUsage } = useTracking();
+ const { push } = useHistory();
const handleGoTo = useCallback(
id => {
push(`/settings/roles/${id}`);
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
- []
+ [push]
);
- const handleToggle = useCallback(e => {
- e.preventDefault();
- e.stopPropagation();
- setIsOpen(prev => !prev);
+ const handleToggle = useCallback(() => {
+ setIsModalOpen(prev => !prev);
}, []);
- const handleToggleModalForCreatingRole = useCallback(e => {
- e.preventDefault();
- e.stopPropagation();
+ const handleToggleModalForCreatingRole = useCallback(() => {
trackUsage('didShowRBACUpgradeModal');
+ setIsModalOpen(true);
+ }, [trackUsage]);
- setIsOpen(true);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ const getIcons = useCallback(
+ role => [
+ {
+ onClick: handleToggle,
+ label: formatMessage({ id: 'app.utils.duplicate', defaultMessage: 'Duplicate' }),
+ icon: ,
+ },
+ {
+ onClick: () => handleGoTo(role.id),
+ label: formatMessage({ id: 'app.utils.edit', defaultMessage: 'Edit' }),
+ icon: ,
+ },
+ {
+ onClick: handleToggle,
+ label: formatMessage({ id: 'app.utils.delete', defaultMessage: 'Delete' }),
+ icon: ,
+ },
+ ],
+ [formatMessage, handleToggle, handleGoTo]
+ );
- const headerActions = [
- {
- label: formatMessage({
- id: 'Settings.roles.list.button.add',
- defaultMessage: 'Add new role',
- }),
- onClick: handleToggleModalForCreatingRole,
- color: 'primary',
- type: 'button',
- icon: true,
- },
- ];
+ return {
+ isModalOpen,
+ handleToggleModalForCreatingRole,
+ handleToggle,
+ getIcons,
+ };
+};
- const resultsCount = results.length;
+const RoleListPage = () => {
+ const { formatMessage } = useIntl();
+
+ const { sortedRoles, isLoading } = useSortedRoles();
+ const {
+ isModalOpen,
+ handleToggle,
+ handleToggleModalForCreatingRole,
+ getIcons,
+ } = useRoleActions();
+
+ const rowCount = sortedRoles.length + 1;
+ const colCount = 5;
+
+ // ! TODO - Add the search input
return (
- <>
+
-
-
-
- 1 ? '.plural' : '.singular'}`,
- },
- { number: resultsCount }
- )}
- items={results}
- isLoading={isLoading}
- customRowComponent={role => (
- handleGoTo(role.id)}
- canUpdate={canUpdate}
- links={[
- {
- icon: ,
- onClick: handleToggle,
- },
- {
- icon: canUpdate ? : null,
- onClick: () => {
- handleGoTo(role.id);
- },
- },
- {
- icon: ,
- onClick: handleToggle,
- },
- ]}
- role={role}
- />
- )}
- />
- {!resultsCount && !isLoading && }
-
- }
- label={formatMessage({
+ }>
+ {formatMessage({
id: 'Settings.roles.list.button.add',
defaultMessage: 'Add new role',
})}
- />
-
-
-
- >
+
+ }
+ title={formatMessage({
+ id: 'Settings.roles.title',
+ defaultMessage: 'roles',
+ })}
+ subtitle={formatMessage({
+ id: 'Settings.roles.list.description',
+ defaultMessage: 'List of roles',
+ })}
+ />
+
+ }>
+ {formatMessage({
+ id: 'Settings.roles.list.button.add',
+ defaultMessage: 'Add new role',
+ })}
+
+ }
+ >
+
+
+ |
+
+ {formatMessage({
+ id: 'Settings.roles.list.header.name',
+ defaultMessage: 'Name',
+ })}
+
+ |
+
+
+ {formatMessage({
+ id: 'Settings.roles.list.header.description',
+ defaultMessage: 'Description',
+ })}
+
+ |
+
+
+ {formatMessage({
+ id: 'Settings.roles.list.header.users',
+ defaultMessage: 'Users',
+ })}
+
+ |
+
+
+ {formatMessage({
+ id: 'Settings.roles.list.header.actions',
+ defaultMessage: 'Actions',
+ })}
+
+ |
+
+
+
+ {sortedRoles?.map(role => (
+
+ ))}
+
+
+ {!rowCount && !isLoading && }
+
+
+
);
};
diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json
index cdf3d0b779..520753e537 100644
--- a/packages/core/admin/admin/src/translations/en.json
+++ b/packages/core/admin/admin/src/translations/en.json
@@ -74,6 +74,7 @@
"Roles.ListPage.notification.delete-not-allowed": "A role cannot be deleted if associated with users",
"Roles.RoleRow.user-count.plural": "{number} users",
"Roles.RoleRow.user-count.singular": "{number} user",
+ "Roles.RoleRow.select-all": "Select {name} for bulk actions",
"Roles.components.List.empty.withSearch": "There is no role corresponding to the search ({search})...",
"Settings.PageTitle": "Settings - {name}",
"Settings.application.description": "See your project's details",
@@ -137,6 +138,10 @@
"Settings.roles.list.description": "List of roles",
"Settings.roles.list.title.plural": "{number} roles",
"Settings.roles.list.title.singular": "{number} role",
+ "Settings.roles.list.header.name": "Name",
+ "Settings.roles.list.header.description": "Description",
+ "Settings.roles.list.header.users": "Users",
+ "Settings.roles.list.header.actions": "Actions",
"Settings.roles.title": "Roles",
"Settings.roles.title.singular": "role",
"Settings.sso.description": "Configure the settings for the Single Sign-On feature.",
@@ -301,6 +306,8 @@
"app.utils.add-filter": "Add filter",
"app.utils.defaultMessage": " ",
"app.utils.delete": "Delete",
+ "app.utils.duplicate": "Duplicate",
+ "app.utils.edit": "Edit",
"app.utils.errors.file-too-big.message": "The file is too big",
"app.utils.filters": "Filters",
"app.utils.placeholder.defaultMessage": " ",
diff --git a/packages/core/admin/ee/admin/pages/Roles/CreatePage/index.js b/packages/core/admin/ee/admin/pages/Roles/CreatePage/index.js
index 5ea1dcca02..ef311da534 100644
--- a/packages/core/admin/ee/admin/pages/Roles/CreatePage/index.js
+++ b/packages/core/admin/ee/admin/pages/Roles/CreatePage/index.js
@@ -5,6 +5,8 @@ import moment from 'moment';
import { Formik } from 'formik';
import { get, isEmpty } from 'lodash';
import { useIntl } from 'react-intl';
+import { HeaderLayout, Button } from '@strapi/parts';
+import { AddIcon, EditIcon } from '@strapi/icons';
import {
BaselineAlignment,
CheckPagePermissions,
@@ -17,7 +19,6 @@ import { useHistory, useRouteMatch } from 'react-router-dom';
import adminPermissions from '../../../../../admin/src/permissions';
import { useFetchPermissionsLayout, useFetchRole } from '../../../../../admin/src/hooks';
import PageTitle from '../../../../../admin/src/components/SettingsPageTitle';
-import ContainerFluid from '../../../../../admin/src/components/ContainerFluid';
import FormCard from '../../../../../admin/src/components/FormBloc';
import { ButtonWithNumber } from '../../../../../admin/src/components/Roles';
import SizedInput from '../../../../../admin/src/components/SizedInput';
@@ -142,7 +143,18 @@ const CreatePage = () => {
>
{({ handleSubmit, values, errors, handleReset, handleChange, handleBlur }) => (
)}
diff --git a/packages/core/admin/ee/admin/pages/Roles/ListPage/RoleRow.js b/packages/core/admin/ee/admin/pages/Roles/ListPage/RoleRow.js
deleted file mode 100644
index 296ab8ea99..0000000000
--- a/packages/core/admin/ee/admin/pages/Roles/ListPage/RoleRow.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import React, { useCallback } from 'react';
-import PropTypes from 'prop-types';
-import { useHistory } from 'react-router-dom';
-import { useNotification } from '@strapi/helper-plugin';
-import { Pencil, Duplicate } from '@buffetjs/icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { RoleRow as RoleRowBase } from '../../../../../admin/src/components/Roles';
-import Checkbox from './CustomCheckbox';
-
-const RoleRow = ({
- canCreate,
- canDelete,
- canUpdate,
- role,
- onRoleToggle,
- onRoleDuplicate,
- onRoleRemove,
- selectedRoles,
-}) => {
- const { push } = useHistory();
- const toggleNotification = useNotification();
-
- const handleRoleSelection = e => {
- e.stopPropagation();
-
- onRoleToggle(role.id);
- };
-
- const handleClickDelete = e => {
- e.preventDefault();
- e.stopPropagation();
-
- if (role.usersCount) {
- toggleNotification({
- type: 'info',
- message: { id: 'Roles.ListPage.notification.delete-not-allowed' },
- });
- } else {
- onRoleRemove(role.id);
- }
- };
-
- const handleGoTo = useCallback(() => {
- push(`/settings/roles/${role.id}`);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [role.id]);
-
- const prefix = canDelete ? (
- selectedRoleId === role.id) !== -1}
- onClick={handleRoleSelection}
- name="role-checkbox"
- />
- ) : null;
-
- return (
- : null,
- onClick: e => {
- e.preventDefault();
- e.stopPropagation();
- onRoleDuplicate(role.id);
- },
- },
- {
- icon: canUpdate ? : null,
- onClick: handleGoTo,
- },
- {
- icon: canDelete ? : null,
- onClick: handleClickDelete,
- },
- ]}
- />
- );
-};
-
-RoleRow.defaultProps = {
- selectedRoles: [],
-};
-
-RoleRow.propTypes = {
- canCreate: PropTypes.bool.isRequired,
- canDelete: PropTypes.bool.isRequired,
- canUpdate: PropTypes.bool.isRequired,
- onRoleToggle: PropTypes.func.isRequired,
- onRoleDuplicate: PropTypes.func.isRequired,
- onRoleRemove: PropTypes.func.isRequired,
- role: PropTypes.object.isRequired,
- selectedRoles: PropTypes.arrayOf(PropTypes.number),
-};
-
-export default RoleRow;
diff --git a/packages/core/admin/ee/admin/pages/Roles/ListPage/index.js b/packages/core/admin/ee/admin/pages/Roles/ListPage/index.js
index b898685a9f..e63736d0e1 100644
--- a/packages/core/admin/ee/admin/pages/Roles/ListPage/index.js
+++ b/packages/core/admin/ee/admin/pages/Roles/ListPage/index.js
@@ -1,68 +1,76 @@
-import React, { useEffect, useReducer, useRef, useState } from 'react';
-import { useHistory } from 'react-router-dom';
-import { Button } from '@buffetjs/core';
-import { List, Header } from '@buffetjs/custom';
-import { Plus } from '@buffetjs/icons';
-import matchSorter from 'match-sorter';
-import { get } from 'lodash';
import {
- useQuery,
- ListButton,
+ LoadingIndicatorPage,
PopUpWarning,
request,
- useRBAC,
useNotification,
- LoadingIndicatorPage,
+ useQuery,
+ useRBAC,
} from '@strapi/helper-plugin';
+import { AddIcon, DeleteIcon, Duplicate, EditIcon } from '@strapi/icons';
+import {
+ Button,
+ ContentLayout,
+ HeaderLayout,
+ Table,
+ Tbody,
+ TFooter,
+ Thead,
+ Th,
+ Tr,
+ TableLabel,
+ VisuallyHidden,
+ BaseCheckbox,
+ Main,
+} from '@strapi/parts';
+import { get } from 'lodash';
+import matchSorter from 'match-sorter';
+import React, { useCallback, useEffect, useReducer, useState } from 'react';
import { useIntl } from 'react-intl';
-import adminPermissions from '../../../../../admin/src/permissions';
+import { useHistory } from 'react-router-dom';
+import { EmptyRole, RoleRow as BaseRoleRow } from '../../../../../admin/src/components/Roles';
import PageTitle from '../../../../../admin/src/components/SettingsPageTitle';
-import useSettingsHeaderSearchContext from '../../../../../admin/src/hooks/useSettingsHeaderSearchContext';
-import { EmptyRole, RoleListWrapper } from '../../../../../admin/src/components/Roles';
import { useRolesList } from '../../../../../admin/src/hooks';
-import RoleRow from './RoleRow';
-import BaselineAlignment from './BaselineAlignment';
+import adminPermissions from '../../../../../admin/src/permissions';
import reducer, { initialState } from './reducer';
-const RoleListPage = () => {
- const toggleNotification = useNotification();
- const [isWarningDeleteAllOpened, setIsWarningDeleteAllOpenend] = useState(false);
- const { formatMessage } = useIntl();
- const { push } = useHistory();
- const [{ selectedRoles, showModalConfirmButtonLoading, shouldRefetchData }, dispath] = useReducer(
- reducer,
- initialState
- );
+const useSortedRoles = () => {
const {
isLoading: isLoadingForPermissions,
allowedActions: { canCreate, canDelete, canRead, canUpdate },
} = useRBAC(adminPermissions.settings.roles);
const { getData, roles, isLoading } = useRolesList(false);
- const getDataRef = useRef(getData);
- const { toggleHeaderSearch } = useSettingsHeaderSearchContext();
const query = useQuery();
const _q = decodeURIComponent(query.get('_q') || '');
- const results = matchSorter(roles, _q, { keys: ['name', 'description'] });
-
- useEffect(() => {
- // Show the search bar only if the user is allowed to read
- if (canRead) {
- toggleHeaderSearch({ id: 'Settings.permissions.menu.link.roles.label' });
- }
-
- return () => {
- if (canRead) {
- toggleHeaderSearch();
- }
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [canRead]);
+ const sortedRoles = matchSorter(roles, _q, { keys: ['name', 'description'] });
useEffect(() => {
if (!isLoadingForPermissions && canRead) {
- getDataRef.current();
+ getData();
}
- }, [isLoadingForPermissions, canRead]);
+ }, [isLoadingForPermissions, canRead, getData]);
+
+ return {
+ isLoadingForPermissions,
+ canCreate,
+ canDelete,
+ canRead,
+ canUpdate,
+ isLoading,
+ getData,
+ sortedRoles,
+ roles,
+ };
+};
+
+const useRoleActions = ({ getData, canCreate, canDelete, canUpdate, roles, sortedRoles }) => {
+ const { formatMessage } = useIntl();
+ const toggleNotification = useNotification();
+ const [isWarningDeleteAllOpened, setIsWarningDeleteAllOpenend] = useState(false);
+ const { push } = useHistory();
+ const [
+ { selectedRoles, showModalConfirmButtonLoading, shouldRefetchData },
+ dispatch,
+ ] = useReducer(reducer, initialState);
const handleClosedModal = () => {
if (shouldRefetchData) {
@@ -70,14 +78,14 @@ const RoleListPage = () => {
}
// Empty the selected ids when the modal closes
- dispath({
+ dispatch({
type: 'RESET_DATA_TO_DELETE',
});
};
const handleConfirmDeleteData = async () => {
try {
- dispath({
+ dispatch({
type: 'ON_REMOVE_ROLES',
});
const filteredRoles = selectedRoles.filter(currentId => {
@@ -103,7 +111,7 @@ const RoleListPage = () => {
// Empty the selectedRolesId and set the shouldRefetchData to true so the
// list is updated when closing the modal
- dispath({
+ dispatch({
type: 'ON_REMOVE_ROLES_SUCCEEDED',
});
}
@@ -128,123 +136,275 @@ const RoleListPage = () => {
}
};
- const handleDuplicateRole = id => {
- push(`/settings/roles/duplicate/${id}`);
- };
+ const onRoleDuplicate = useCallback(
+ id => {
+ push(`/settings/roles/duplicate/${id}`);
+ },
+ [push]
+ );
const handleNewRoleClick = () => push('/settings/roles/new');
- const handleRemoveRole = roleId => {
- dispath({
+ const onRoleRemove = useCallback(roleId => {
+ dispatch({
type: 'SET_ROLE_TO_DELETE',
id: roleId,
});
handleToggleModal();
- };
+ }, []);
- const handleRoleToggle = roleId => {
- dispath({
+ const onRoleToggle = roleId => {
+ dispatch({
type: 'ON_SELECTION',
id: roleId,
});
};
+ const onAllRolesToggle = () =>
+ dispatch({
+ type: 'TOGGLE_ALL',
+ ids: sortedRoles.map(r => r.id),
+ });
+
const handleToggleModal = () => setIsWarningDeleteAllOpenend(prev => !prev);
- /* eslint-disable indent */
- const headerActions = canCreate
- ? [
- {
- label: formatMessage({
- id: 'Settings.roles.list.button.add',
- defaultMessage: 'Add new role',
- }),
- onClick: handleNewRoleClick,
- color: 'primary',
- type: 'button',
- icon: true,
- },
- ]
- : [];
- /* eslint-enable indent */
+ const handleGoTo = useCallback(
+ id => {
+ push(`/settings/roles/${id}`);
+ },
+ [push]
+ );
- const resultsCount = results.length;
+ const handleClickDelete = useCallback(
+ (e, role) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (role.usersCount) {
+ toggleNotification({
+ type: 'info',
+ message: { id: 'Roles.ListPage.notification.delete-not-allowed' },
+ });
+ } else {
+ onRoleRemove(role.id);
+ }
+ },
+ [toggleNotification, onRoleRemove]
+ );
+
+ const handleClickDuplicate = useCallback(
+ (e, role) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onRoleDuplicate(role.id);
+ },
+ [onRoleDuplicate]
+ );
+
+ const getIcons = useCallback(
+ role => [
+ ...(canCreate
+ ? [
+ {
+ onClick: e => handleClickDuplicate(e, role),
+ label: formatMessage({ id: 'app.utils.duplicate', defaultMessage: 'Duplicate' }),
+ icon: ,
+ },
+ ]
+ : []),
+ ...(canUpdate
+ ? [
+ {
+ onClick: () => handleGoTo(role.id),
+ label: formatMessage({ id: 'app.utils.edit', defaultMessage: 'Edit' }),
+ icon: ,
+ },
+ ]
+ : []),
+ ...(canDelete
+ ? [
+ {
+ onClick: e => handleClickDelete(e, role),
+ label: formatMessage({ id: 'app.utils.delete', defaultMessage: 'Delete' }),
+ icon: ,
+ },
+ ]
+ : []),
+ ],
+ [
+ formatMessage,
+ handleClickDelete,
+ handleClickDuplicate,
+ handleGoTo,
+ canCreate,
+ canUpdate,
+ canDelete,
+ ]
+ );
+
+ return {
+ handleClosedModal,
+ handleConfirmDeleteData,
+ handleNewRoleClick,
+ onRoleToggle,
+ onAllRolesToggle,
+ getIcons,
+ selectedRoles,
+ isWarningDeleteAllOpened,
+ showModalConfirmButtonLoading,
+ handleToggleModal,
+ };
+};
+
+const RoleListPage = () => {
+ const { formatMessage } = useIntl();
+ const {
+ isLoadingForPermissions,
+ canCreate,
+ canRead,
+ canDelete,
+ canUpdate,
+ isLoading,
+ getData,
+ sortedRoles,
+ roles,
+ } = useSortedRoles();
+
+ const {
+ handleClosedModal,
+ handleConfirmDeleteData,
+ handleNewRoleClick,
+ onRoleToggle,
+ onAllRolesToggle,
+ getIcons,
+ selectedRoles,
+ isWarningDeleteAllOpened,
+ showModalConfirmButtonLoading,
+ handleToggleModal,
+ } = useRoleActions({ getData, canCreate, canDelete, canUpdate, roles, sortedRoles });
+
+ // ! TODO - Show the search bar only if the user is allowed to read - add the search input
+ // canRead
+
+ const rowCount = sortedRoles.length;
+ const colCount = sortedRoles.length ? Object.keys(sortedRoles[0]).length : 0;
+
+ const isAllEntriesIndeterminate = selectedRoles.length
+ ? selectedRoles.length !== rowCount
+ : false;
+ const isAllChecked = selectedRoles.length ? selectedRoles.length === rowCount : false;
if (isLoadingForPermissions) {
return ;
}
return (
- <>
+
- }>
+ {formatMessage({
+ id: 'Settings.roles.list.button.add',
+ defaultMessage: 'Add new role',
+ })}
+
+ ) : null
+ }
+ title={formatMessage({
+ id: 'Settings.roles.title',
+ defaultMessage: 'roles',
+ })}
+ subtitle={formatMessage({
id: 'Settings.roles.list.description',
defaultMessage: 'List of roles',
})}
- actions={headerActions}
- isLoading={isLoading}
+ as="h2"
/>
-
{canRead && (
-
- 1 ? '.plural' : '.singular'}`,
- defaultMessage: `{number} ${resultsCount > 1 ? 'roles' : 'role'}`,
- },
- { number: resultsCount }
- )}
- isLoading={isLoading}
- /* eslint-disable indent */
- button={
- canDelete
- ? {
- color: 'delete',
- disabled: selectedRoles.length === 0,
- label: formatMessage({ id: 'app.utils.delete', defaultMessage: 'Delete' }),
- onClick: handleToggleModal,
- type: 'button',
- }
- : null
+
+ }>
+ {formatMessage({
+ id: 'Settings.roles.list.button.add',
+ defaultMessage: 'Add new role',
+ })}
+
+ ) : null
}
- /* eslint-enable indent */
- items={results}
- customRowComponent={role => (
-
- )}
- />
- {!resultsCount && !isLoading && }
- {canCreate && (
-
- }
- label={formatMessage({
- id: 'Settings.roles.list.button.add',
- defaultMessage: 'Add new role',
- })}
- />
-
- )}
-
+ >
+
+
+ {!!onRoleToggle && (
+ |
+
+ |
+ )}
+
+
+ {formatMessage({
+ id: 'Settings.roles.list.header.name',
+ defaultMessage: 'Name',
+ })}
+
+ |
+
+
+ {formatMessage({
+ id: 'Settings.roles.list.header.description',
+ defaultMessage: 'Description',
+ })}
+
+ |
+
+
+ {formatMessage({
+ id: 'Settings.roles.list.header.users',
+ defaultMessage: 'Users',
+ })}
+
+ |
+
+
+ {formatMessage({
+ id: 'Settings.roles.list.header.actions',
+ defaultMessage: 'Actions',
+ })}
+
+ |
+
+
+
+ {sortedRoles?.map(role => (
+ selectedRoleId === role.id) !== -1
+ }
+ name={role.name}
+ description={role.description}
+ usersCount={role.usersCount}
+ icons={getIcons(role)}
+ />
+ ))}
+
+
+ {!rowCount && !isLoading && }
+
)}
{
toggleModal={handleToggleModal}
isConfirmButtonLoading={showModalConfirmButtonLoading}
/>
- >
+
);
};
diff --git a/packages/core/admin/ee/admin/pages/Roles/ListPage/reducer.js b/packages/core/admin/ee/admin/pages/Roles/ListPage/reducer.js
index 7dcb9e7738..8b7e07b04c 100644
--- a/packages/core/admin/ee/admin/pages/Roles/ListPage/reducer.js
+++ b/packages/core/admin/ee/admin/pages/Roles/ListPage/reducer.js
@@ -21,6 +21,15 @@ const reducer = (state, action) =>
}
break;
}
+ case 'TOGGLE_ALL': {
+ if (state.selectedRoles.length) {
+ draftState.selectedRoles = [];
+ } else {
+ const { ids } = action;
+ draftState.selectedRoles = ids;
+ }
+ break;
+ }
case 'ON_REMOVE_ROLES': {
draftState.showModalConfirmButtonLoading = true;
break;
diff --git a/packages/core/admin/package.json b/packages/core/admin/package.json
index 7e246dee02..a164b0fc5f 100644
--- a/packages/core/admin/package.json
+++ b/packages/core/admin/package.json
@@ -106,8 +106,8 @@
"react-loadable": "^5.5.0",
"react-query": "3.19.0",
"react-redux": "7.2.3",
- "react-router": "^5.2.0",
- "react-router-dom": "^5.0.0",
+ "react-router": "5.2.0",
+ "react-router-dom": "5.2.0",
"react-select": "^4.0.2",
"react-tooltip": "4.2.18",
"react-transition-group": "4.4.1",
diff --git a/packages/core/email/admin/src/pages/Settings/index.js b/packages/core/email/admin/src/pages/Settings/index.js
index baedd23eae..3b4d7b12ac 100644
--- a/packages/core/email/admin/src/pages/Settings/index.js
+++ b/packages/core/email/admin/src/pages/Settings/index.js
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-wrap-multilines */
import React, { useState, useEffect, useRef } from 'react';
import { useIntl, FormattedMessage } from 'react-intl';
import { get } from 'lodash';
diff --git a/packages/core/helper-plugin/lib/src/hooks/useFocusWhenNavigate/index.js b/packages/core/helper-plugin/lib/src/hooks/useFocusWhenNavigate/index.js
new file mode 100644
index 0000000000..db3644b959
--- /dev/null
+++ b/packages/core/helper-plugin/lib/src/hooks/useFocusWhenNavigate/index.js
@@ -0,0 +1,19 @@
+import { useEffect } from 'react';
+
+const useFocusWhenNavigate = (selector = 'main', dependencies = []) => {
+ useEffect(() => {
+ const mainElement = document.querySelector(selector);
+
+ if (mainElement) {
+ mainElement.focus();
+ window.scrollTo({ top: 0 });
+ } else {
+ console.warn(
+ `[useFocusWhenNavigate] The page does not contain the selector "${selector}" and can't be focused.`
+ );
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, dependencies);
+};
+
+export default useFocusWhenNavigate;
diff --git a/packages/core/helper-plugin/lib/src/index.js b/packages/core/helper-plugin/lib/src/index.js
index b6f1cbf329..86c5324316 100644
--- a/packages/core/helper-plugin/lib/src/index.js
+++ b/packages/core/helper-plugin/lib/src/index.js
@@ -129,6 +129,7 @@ export { default as useAutoReloadOverlayBlocker } from './hooks/useAutoReloadOve
export { default as useRBACProvider } from './hooks/useRBACProvider';
export { default as useRBAC } from './hooks/useRBAC';
export { default as usePersistentState } from './hooks/usePersistentState';
+export { default as useFocusWhenNavigate } from './hooks/useFocusWhenNavigate';
// Providers
export { default as LibraryProvider } from './providers/LibraryProvider';
diff --git a/packages/plugins/i18n/admin/src/components/LocaleList/LocaleTable.js b/packages/plugins/i18n/admin/src/components/LocaleList/LocaleTable.js
new file mode 100644
index 0000000000..e583379133
--- /dev/null
+++ b/packages/plugins/i18n/admin/src/components/LocaleList/LocaleTable.js
@@ -0,0 +1,177 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import {
+ Table,
+ Thead,
+ Tr,
+ Th,
+ Td,
+ Tbody,
+ Text,
+ TableLabel,
+ VisuallyHidden,
+ Stack,
+ IconButton,
+} from '@strapi/parts';
+import EditIcon from '@strapi/icons/EditIcon';
+import DeleteIcon from '@strapi/icons/DeleteIcon';
+import DropdownIcon from '@strapi/icons/FilterDropdown';
+import styled from 'styled-components';
+import orderBy from 'lodash/orderBy';
+import { useIntl } from 'react-intl';
+import { getTrad } from '../../utils';
+
+const ActionIconWrapper = styled.span`
+ svg {
+ transform: ${({ reverse }) => (reverse ? `rotateX(180deg)` : undefined)};
+ }
+`;
+
+const SortingKeys = {
+ id: 'id',
+ name: 'name',
+ default: 'isDefault',
+};
+
+const SortingOrder = {
+ asc: 'asc',
+ desc: 'desc',
+};
+
+const LocaleTable = ({ locales, onDeleteLocale, onEditLocale }) => {
+ const { formatMessage } = useIntl();
+ const [sortingKey, setSortingKey] = useState(SortingKeys.id);
+ const [sortingOrder, setSortingOrder] = useState(SortingOrder.asc);
+
+ const sortedLocales = orderBy([...locales], [sortingKey], [sortingOrder]);
+
+ const handleSorting = key => {
+ if (key === sortingKey) {
+ setSortingOrder(prev => (prev === SortingOrder.asc ? SortingOrder.desc : SortingOrder.asc));
+ } else {
+ setSortingKey(key);
+ setSortingOrder(SortingOrder.asc);
+ }
+ };
+
+ const isReversedArrow = key => sortingKey === key && sortingOrder === SortingOrder.desc;
+
+ return (
+
+
+
+ |
+
+
+ }
+ noBorder
+ onClick={() => handleSorting(SortingKeys.id)}
+ />
+ }
+ >
+
+ {formatMessage({ id: getTrad('Settings.locales.row.id') })}
+
+ |
+
+
+
+ }
+ noBorder
+ onClick={() => handleSorting(SortingKeys.name)}
+ />
+ }
+ >
+
+ {formatMessage({ id: getTrad('Settings.locales.row.displayName') })}
+
+ |
+
+
+
+ }
+ noBorder
+ onClick={() => handleSorting(SortingKeys.default)}
+ />
+ }
+ >
+
+ {formatMessage({ id: getTrad('Settings.locales.row.default-locale') })}
+
+ |
+
+ Actions
+ |
+
+
+
+ {sortedLocales.map(locale => (
+
+ |
+ {locale.id}
+ |
+
+ {locale.name}
+ |
+
+
+ {locale.isDefault
+ ? formatMessage({ id: getTrad('Settings.locales.row.default-locale') })
+ : null}
+
+ |
+
+
+ {onEditLocale && (
+ onEditLocale(locale)}
+ label={formatMessage({ id: getTrad('Settings.list.actions.edit') })}
+ icon={}
+ noBorder
+ />
+ )}
+ {onDeleteLocale && !locale.isDefault && (
+ onDeleteLocale(locale)}
+ label={formatMessage({ id: getTrad('Settings.list.actions.delete') })}
+ icon={}
+ noBorder
+ />
+ )}
+
+ |
+
+ ))}
+
+
+ );
+};
+
+LocaleTable.defaultProps = {
+ locales: [],
+ onDeleteLocale: undefined,
+ onEditLocale: undefined,
+};
+
+LocaleTable.propTypes = {
+ locales: PropTypes.array,
+ onDeleteLocale: PropTypes.func,
+ onEditLocale: PropTypes.func,
+};
+
+export default LocaleTable;
diff --git a/packages/plugins/i18n/admin/src/components/LocaleList/index.js b/packages/plugins/i18n/admin/src/components/LocaleList/index.js
index 8226a56b92..f89cf0d824 100644
--- a/packages/plugins/i18n/admin/src/components/LocaleList/index.js
+++ b/packages/plugins/i18n/admin/src/components/LocaleList/index.js
@@ -1,89 +1,77 @@
import React, { useState } from 'react';
import { useIntl } from 'react-intl';
-import { EmptyState, ListButton } from '@strapi/helper-plugin';
-import { List } from '@buffetjs/custom';
-import { Button } from '@buffetjs/core';
-import { Plus } from '@buffetjs/icons';
import PropTypes from 'prop-types';
+import { ContentLayout, EmptyStateLayout, Button, Main, HeaderLayout } from '@strapi/parts';
+import { useFocusWhenNavigate } from '@strapi/helper-plugin';
+import AddIcon from '@strapi/icons/AddIcon';
+import EmptyStateDocument from '@strapi/icons/EmptyStateDocument';
import useLocales from '../../hooks/useLocales';
-import LocaleRow from '../LocaleRow';
import { getTrad } from '../../utils';
import ModalEdit from '../ModalEdit';
import ModalDelete from '../ModalDelete';
import ModalCreate from '../ModalCreate';
+import LocaleTable from './LocaleTable';
const LocaleList = ({ canUpdateLocale, canDeleteLocale, onToggleCreateModal, isCreating }) => {
const [localeToDelete, setLocaleToDelete] = useState();
const [localeToEdit, setLocaleToEdit] = useState();
- const { locales, isLoading } = useLocales();
+ const { locales } = useLocales();
const { formatMessage } = useIntl();
+ useFocusWhenNavigate();
+
// Delete actions
const closeModalToDelete = () => setLocaleToDelete(undefined);
const handleDeleteLocale = canDeleteLocale ? setLocaleToDelete : undefined;
// Edit actions
- const closeModalToEdit = () => {
- setLocaleToEdit(undefined);
- };
+ const closeModalToEdit = () => setLocaleToEdit(undefined);
const handleEditLocale = canUpdateLocale ? setLocaleToEdit : undefined;
- if (isLoading || (locales && locales.length > 0)) {
- const listTitle = isLoading
- ? null
- : formatMessage(
- {
- id: getTrad(
- `Settings.locales.list.title${locales.length > 1 ? '.plural' : '.singular'}`
- ),
- },
- { number: locales.length }
- );
-
- return (
- <>
- (
-
- )}
- />
-
-
-
-
- >
- );
- }
-
return (
- <>
-
+ } onClick={onToggleCreateModal}>
+ {formatMessage({ id: getTrad('Settings.list.actions.add') })}
+
+ }
+ title={formatMessage({ id: getTrad('plugin.name') })}
+ subtitle={formatMessage({ id: getTrad('Settings.list.description') })}
/>
-
- {onToggleCreateModal && (
-
- }
+
+ {locales?.length > 0 ? (
+
-
- )}
+ ) : (
+
+ }
+ content={formatMessage({ id: getTrad('Settings.list.empty.title') })}
+ action={
+ onToggleCreateModal ? (
+ } onClick={onToggleCreateModal}>
+ {formatMessage({ id: getTrad('Settings.list.actions.add') })}
+
+ ) : null
+ }
+ />
+
+ )}
+
-
- >
+
+
+
+
);
};
diff --git a/packages/plugins/i18n/admin/src/components/LocaleRow/index.js b/packages/plugins/i18n/admin/src/components/LocaleRow/index.js
deleted file mode 100644
index 3f54cc43f7..0000000000
--- a/packages/plugins/i18n/admin/src/components/LocaleRow/index.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { useIntl } from 'react-intl';
-import { Pencil } from '@buffetjs/icons';
-import { Text, IconLinks } from '@buffetjs/core';
-import { CustomRow } from '@buffetjs/styles';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { getTrad } from '../../utils';
-
-const LocaleSettingsPage = ({ locale, onDelete, onEdit }) => {
- const { formatMessage } = useIntl();
-
- const links = [];
-
- if (onEdit) {
- links.push({
- icon: (
-
-
-
- ),
- onClick: () => onEdit(locale),
- });
- }
-
- if (onDelete && !locale.isDefault) {
- links.push({
- icon: !locale.isDefault ? (
-
-
-
- ) : null,
- onClick: e => {
- e.stopPropagation();
- onDelete(locale);
- },
- });
- }
-
- return (
- onEdit(locale)}>
- |
- {locale.code}
- |
-
- {locale.name}
- |
-
-
- {locale.isDefault
- ? formatMessage({ id: getTrad('Settings.locales.row.default-locale') })
- : null}
-
- |
-
-
- |
-
- );
-};
-
-LocaleSettingsPage.defaultProps = {
- onDelete: undefined,
- onEdit: undefined,
-};
-
-LocaleSettingsPage.propTypes = {
- locale: PropTypes.shape({
- isDefault: PropTypes.bool,
- name: PropTypes.string,
- code: PropTypes.string.isRequired,
- }).isRequired,
- onDelete: PropTypes.func,
- onEdit: PropTypes.func,
-};
-
-export default LocaleSettingsPage;
diff --git a/packages/plugins/i18n/admin/src/components/index.js b/packages/plugins/i18n/admin/src/components/index.js
deleted file mode 100644
index 0101599086..0000000000
--- a/packages/plugins/i18n/admin/src/components/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-// eslint-disable-next-line import/prefer-default-export
-export { default as LocaleRow } from './LocaleRow';
diff --git a/packages/plugins/i18n/admin/src/pages/SettingsPage/LocaleSettingsPage.js b/packages/plugins/i18n/admin/src/pages/SettingsPage/LocaleSettingsPage.js
index ce2ce0e143..a2d44ac876 100644
--- a/packages/plugins/i18n/admin/src/pages/SettingsPage/LocaleSettingsPage.js
+++ b/packages/plugins/i18n/admin/src/pages/SettingsPage/LocaleSettingsPage.js
@@ -1,10 +1,5 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
-import { useIntl } from 'react-intl';
-import { BaselineAlignment } from '@strapi/helper-plugin';
-import { Header } from '@buffetjs/custom';
-import { Button } from '@buffetjs/core';
-import { getTrad } from '../../utils';
import LocaleList from '../../components/LocaleList';
const LocaleSettingsPage = ({
@@ -13,50 +8,20 @@ const LocaleSettingsPage = ({
canDeleteLocale,
canUpdateLocale,
}) => {
- const { formatMessage } = useIntl();
const [isOpenedCreateModal, setIsOpenedCreateModal] = useState(false);
const handleToggleModalCreate = canCreateLocale
? () => setIsOpenedCreateModal(s => !s)
: undefined;
- const actions = [
- {
- label: formatMessage({ id: getTrad('Settings.list.actions.add') }),
- onClick: handleToggleModalCreate,
- color: 'primary',
- type: 'button',
- icon: true,
- Component: props => (canCreateLocale ? : null),
- style: {
- paddingLeft: 15,
- paddingRight: 15,
- },
- },
- ];
-
- return (
- <>
-
-
-
-
- {canReadLocale ? (
-
- ) : null}
- >
- );
+ return canReadLocale ? (
+
+ ) : null;
};
LocaleSettingsPage.propTypes = {
diff --git a/packages/plugins/i18n/admin/src/translations/en.json b/packages/plugins/i18n/admin/src/translations/en.json
index cfe651effe..c60e92330d 100644
--- a/packages/plugins/i18n/admin/src/translations/en.json
+++ b/packages/plugins/i18n/admin/src/translations/en.json
@@ -17,6 +17,9 @@
"Settings.list.empty.title": "There are no locales.",
"Settings.locales.list.title.plural": "{number} Locales",
"Settings.locales.list.title.singular": "{number} Locale",
+ "Settings.locales.list.sort.id": "Sort by ID",
+ "Settings.locales.list.sort.displayName": "Sort by display name",
+ "Settings.locales.list.sort.default": "Sort by the default locale",
"Settings.locales.modal.advanced": "Advanced settings",
"Settings.locales.modal.advanced.setAsDefault": "Set as default locale",
"Settings.locales.modal.advanced.setAsDefault.hint": "One default locale is required, change it by selecting another one",
@@ -40,7 +43,9 @@
"Settings.locales.modal.locales.displayName.error": "The locale display name can only be less than 50 characters.",
"Settings.locales.modal.locales.label": "Locales",
"Settings.locales.modal.title": "Configurations",
- "Settings.locales.row.default-locale": "Default locale",
+ "Settings.locales.row.id": "ID",
+ "Settings.locales.row.displayName": "Display name",
+ "Settings.locales.row.default-locale": "Default",
"Settings.permissions.loading": "Loading permissions",
"Settings.permissions.read.denied.description": "In order to be able to read this, make sure to get in touch with the administrator of your system.",
"Settings.permissions.read.denied.title": "You don't have the permissions to access this content.",
diff --git a/yarn.lock b/yarn.lock
index 80062c52b4..2339c684a3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -17328,7 +17328,7 @@ react-redux@7.2.3:
prop-types "^15.7.2"
react-is "^16.13.1"
-react-router-dom@^5.0.0, react-router-dom@^5.2.0:
+react-router-dom@5.2.0, react-router-dom@^5.0.0, react-router-dom@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662"
integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==