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 && } - - + } + 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', + })} + + } + > + + + + + + + + + + {sortedRoles?.map(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', + })} + +
+ {!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 }) => (
- + <> + }>Add an entry} + secondaryAction={ + + } + title="Other CT" + subtitle="36 entries found" + as="h1" + />
{ /> )} - + )} 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 && ( - - + + {!!onRoleToggle && ( + + )} + + + + + + + + {sortedRoles?.map(role => ( + selectedRoleId === role.id) !== -1 + } + name={role.name} + description={role.description} + usersCount={role.usersCount} + icons={getIcons(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', + })} + +
+ {!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 ( + + + + + + + + + + + {sortedLocales.map(locale => ( + + + + + + + ))} + +
+ + + } + 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 +
+ {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 && ( - - + ) : 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 ?