Merge branch 'v4/ds-migration' into webhooks-listview-ds

This commit is contained in:
ronronscelestes 2021-08-17 14:05:36 +02:00
commit f79f6eda59
21 changed files with 802 additions and 575 deletions

View File

@ -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,
},
};

View File

@ -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 (
<CustomRow onClick={onClick}>
{prefix && <td style={{ width: 55 }}>{prefix}</td>}
<td>
<Text fontWeight="semiBold">{role.name}</Text>
</td>
<td>
<RoleDescription>{role.description}</RoleDescription>
</td>
<td>
<Text>{text}</Text>
</td>
<td>
<IconLinks links={links} />
</td>
</CustomRow>
<Tr>
{Boolean(onToggle) && (
<Td>
<BaseCheckbox
name="role-checkbox"
onValueChange={() => onToggle(id)}
value={isChecked}
aria-label={formatMessage({ id: `Roles.RoleRow.select-all` }, { name })}
/>
</Td>
)}
<Td>
<Text textColor="neutral800">{name}</Text>
</Td>
<Td>
<RoleDescription textColor="neutral800">{description}</RoleDescription>
</Td>
<Td>
<Text textColor="neutral800">{usersCountText}</Text>
</Td>
<Td>
<Row>
{icons.map((icon, i) =>
icon ? (
<Box key={icon.label} paddingLeft={i === 0 ? 0 : 1}>
<IconButton onClick={icon.onClick} label={icon.label} noBorder icon={icon.icon} />
</Box>
) : null
)}
</Row>
</Td>
</Tr>
);
};
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;

View File

@ -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 };
};

View File

@ -81,7 +81,6 @@ const Admin = () => {
<DndProvider backend={HTML5Backend}>
<AppLayout
sideNav={
// eslint-disable-next-line react/jsx-wrap-multilines
<LeftMenu
generalSectionLinks={generalSectionLinks}
pluginsSectionLinks={pluginsSectionLinks}

View File

@ -1,148 +1,193 @@
import React, { useCallback, useEffect, useState } from 'react';
import { List, Header } from '@buffetjs/custom';
import { Button } from '@buffetjs/core';
import { Duplicate, Pencil, Plus } from '@buffetjs/icons';
import { useQuery, useTracking } from '@strapi/helper-plugin';
import { AddIcon, DeleteIcon, EditIcon, Duplicate } from '@strapi/icons';
import {
Button,
ContentLayout,
HeaderLayout,
Table,
TableLabel,
Tbody,
TFooter,
Th,
Thead,
Tr,
VisuallyHidden,
Main,
} from '@strapi/parts';
import matchSorter from 'match-sorter';
import React, { useCallback, useState } from 'react';
import { useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { ListButton, useTracking, useQuery, useRBAC } from '@strapi/helper-plugin';
import adminPermissions from '../../../permissions';
import { useHistory } from 'react-router';
import { EmptyRole, RoleRow } from '../../../components/Roles';
import PageTitle from '../../../components/SettingsPageTitle';
import { EmptyRole, RoleListWrapper, RoleRow } from '../../../components/Roles';
import { useRolesList, useSettingsHeaderSearchContext } from '../../../hooks';
import UpgradePlanModal from '../../../components/UpgradePlanModal';
import BaselineAlignment from './BaselineAlignment';
import { useRolesList } from '../../../hooks';
const RoleListPage = () => {
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: <Duplicate />,
},
{
onClick: () => handleGoTo(role.id),
label: formatMessage({ id: 'app.utils.edit', defaultMessage: 'Edit' }),
icon: <EditIcon />,
},
{
onClick: handleToggle,
label: formatMessage({ id: 'app.utils.delete', defaultMessage: 'Delete' }),
icon: <DeleteIcon />,
},
],
[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 (
<>
<Main labelledBy="title">
<PageTitle name="Roles" />
<Header
icon
title={{
label: formatMessage({
id: 'Settings.roles.title',
defaultMessage: 'roles',
}),
}}
content={formatMessage({
id: 'Settings.roles.list.description',
defaultMessage: 'List of roles',
})}
// Show a loader in the header while requesting data
isLoading={isLoading}
actions={headerActions}
/>
<BaselineAlignment />
<RoleListWrapper>
<List
title={formatMessage(
{
id: `Settings.roles.list.title${results.length > 1 ? '.plural' : '.singular'}`,
},
{ number: resultsCount }
)}
items={results}
isLoading={isLoading}
customRowComponent={role => (
<RoleRow
onClick={() => handleGoTo(role.id)}
canUpdate={canUpdate}
links={[
{
icon: <Duplicate fill="#0e1622" />,
onClick: handleToggle,
},
{
icon: canUpdate ? <Pencil fill="#0e1622" /> : null,
onClick: () => {
handleGoTo(role.id);
},
},
{
icon: <FontAwesomeIcon icon="trash-alt" />,
onClick: handleToggle,
},
]}
role={role}
/>
)}
/>
{!resultsCount && !isLoading && <EmptyRole />}
<ListButton>
<Button
onClick={handleToggleModalForCreatingRole}
icon={<Plus fill="#007eff" width="11px" height="11px" />}
label={formatMessage({
<HeaderLayout
id="title"
primaryAction={
<Button onClick={handleToggleModalForCreatingRole} startIcon={<AddIcon />}>
{formatMessage({
id: 'Settings.roles.list.button.add',
defaultMessage: 'Add new role',
})}
/>
</ListButton>
</RoleListWrapper>
<UpgradePlanModal isOpen={isOpen} onToggle={handleToggle} />
</>
</Button>
}
title={formatMessage({
id: 'Settings.roles.title',
defaultMessage: 'roles',
})}
subtitle={formatMessage({
id: 'Settings.roles.list.description',
defaultMessage: 'List of roles',
})}
/>
<ContentLayout>
<Table
colCount={colCount}
rowCount={rowCount}
footer={
<TFooter onClick={handleToggleModalForCreatingRole} icon={<AddIcon />}>
{formatMessage({
id: 'Settings.roles.list.button.add',
defaultMessage: 'Add new role',
})}
</TFooter>
}
>
<Thead>
<Tr>
<Th>
<TableLabel>
{formatMessage({
id: 'Settings.roles.list.header.name',
defaultMessage: 'Name',
})}
</TableLabel>
</Th>
<Th>
<TableLabel>
{formatMessage({
id: 'Settings.roles.list.header.description',
defaultMessage: 'Description',
})}
</TableLabel>
</Th>
<Th>
<TableLabel>
{formatMessage({
id: 'Settings.roles.list.header.users',
defaultMessage: 'Users',
})}
</TableLabel>
</Th>
<Th>
<VisuallyHidden>
{formatMessage({
id: 'Settings.roles.list.header.actions',
defaultMessage: 'Actions',
})}
</VisuallyHidden>
</Th>
</Tr>
</Thead>
<Tbody>
{sortedRoles?.map(role => (
<RoleRow
key={role.id}
id={role.id}
name={role.name}
description={role.description}
usersCount={role.usersCount}
icons={getIcons(role)}
/>
))}
</Tbody>
</Table>
{!rowCount && !isLoading && <EmptyRole />}
</ContentLayout>
<UpgradePlanModal isOpen={isModalOpen} onToggle={handleToggle} />
</Main>
);
};

View File

@ -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": " ",

View File

@ -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 }) => (
<form onSubmit={handleSubmit}>
<ContainerFluid padding="0">
<>
<HeaderLayout
primaryAction={<Button startIcon={<AddIcon />}>Add an entry</Button>}
secondaryAction={
<Button variant="tertiary" startIcon={<EditIcon />}>
Edit
</Button>
}
title="Other CT"
subtitle="36 entries found"
as="h1"
/>
<Header
title={{
label: formatMessage({
@ -202,7 +214,7 @@ const CreatePage = () => {
/>
</Padded>
)}
</ContainerFluid>
</>
</form>
)}
</Formik>

View File

@ -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 ? (
<Checkbox
value={selectedRoles.findIndex(selectedRoleId => selectedRoleId === role.id) !== -1}
onClick={handleRoleSelection}
name="role-checkbox"
/>
) : null;
return (
<RoleRowBase
onClick={handleGoTo}
selectedRoles={selectedRoles}
prefix={prefix}
role={role}
links={[
{
icon: canCreate ? <Duplicate fill="#0e1622" /> : null,
onClick: e => {
e.preventDefault();
e.stopPropagation();
onRoleDuplicate(role.id);
},
},
{
icon: canUpdate ? <Pencil fill="#0e1622" /> : null,
onClick: handleGoTo,
},
{
icon: canDelete ? <FontAwesomeIcon icon="trash-alt" /> : 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;

View File

@ -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: <Duplicate />,
},
]
: []),
...(canUpdate
? [
{
onClick: () => handleGoTo(role.id),
label: formatMessage({ id: 'app.utils.edit', defaultMessage: 'Edit' }),
icon: <EditIcon />,
},
]
: []),
...(canDelete
? [
{
onClick: e => handleClickDelete(e, role),
label: formatMessage({ id: 'app.utils.delete', defaultMessage: 'Delete' }),
icon: <DeleteIcon />,
},
]
: []),
],
[
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 <LoadingIndicatorPage />;
}
return (
<>
<Main labelledBy="title">
<PageTitle name="Roles" />
<Header
title={{
label: formatMessage({
id: 'Settings.roles.title',
defaultMessage: 'roles',
}),
}}
content={formatMessage({
<HeaderLayout
id="title"
primaryAction={
canCreate ? (
<Button onClick={handleNewRoleClick} startIcon={<AddIcon />}>
{formatMessage({
id: 'Settings.roles.list.button.add',
defaultMessage: 'Add new role',
})}
</Button>
) : 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"
/>
<BaselineAlignment />
{canRead && (
<RoleListWrapper>
<List
title={formatMessage(
{
id: `Settings.roles.list.title${resultsCount > 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
<ContentLayout>
<Table
colCount={colCount}
rowCount={rowCount}
footer={
canCreate ? (
<TFooter onClick={handleNewRoleClick} icon={<AddIcon />}>
{formatMessage({
id: 'Settings.roles.list.button.add',
defaultMessage: 'Add new role',
})}
</TFooter>
) : null
}
/* eslint-enable indent */
items={results}
customRowComponent={role => (
<RoleRow
canCreate={canCreate}
canDelete={canDelete}
canUpdate={canUpdate}
selectedRoles={selectedRoles}
onRoleDuplicate={handleDuplicateRole}
onRoleRemove={handleRemoveRole}
onRoleToggle={handleRoleToggle}
role={role}
/>
)}
/>
{!resultsCount && !isLoading && <EmptyRole />}
{canCreate && (
<ListButton>
<Button
onClick={handleNewRoleClick}
icon={<Plus fill="#007eff" width="11px" height="11px" />}
label={formatMessage({
id: 'Settings.roles.list.button.add',
defaultMessage: 'Add new role',
})}
/>
</ListButton>
)}
</RoleListWrapper>
>
<Thead>
<Tr>
{!!onRoleToggle && (
<Th>
<BaseCheckbox
aria-label="Select all entries"
indeterminate={isAllEntriesIndeterminate}
value={isAllChecked}
onChange={onAllRolesToggle}
/>
</Th>
)}
<Th>
<TableLabel>
{formatMessage({
id: 'Settings.roles.list.header.name',
defaultMessage: 'Name',
})}
</TableLabel>
</Th>
<Th>
<TableLabel>
{formatMessage({
id: 'Settings.roles.list.header.description',
defaultMessage: 'Description',
})}
</TableLabel>
</Th>
<Th>
<TableLabel>
{formatMessage({
id: 'Settings.roles.list.header.users',
defaultMessage: 'Users',
})}
</TableLabel>
</Th>
<Th>
<VisuallyHidden>
{formatMessage({
id: 'Settings.roles.list.header.actions',
defaultMessage: 'Actions',
})}
</VisuallyHidden>
</Th>
</Tr>
</Thead>
<Tbody>
{sortedRoles?.map(role => (
<BaseRoleRow
key={role.id}
id={role.id}
onToggle={onRoleToggle}
isChecked={
selectedRoles.findIndex(selectedRoleId => selectedRoleId === role.id) !== -1
}
name={role.name}
description={role.description}
usersCount={role.usersCount}
icons={getIcons(role)}
/>
))}
</Tbody>
</Table>
{!rowCount && !isLoading && <EmptyRole />}
</ContentLayout>
)}
<PopUpWarning
isOpen={isWarningDeleteAllOpened}
@ -253,7 +413,7 @@ const RoleListPage = () => {
toggleModal={handleToggleModal}
isConfirmButtonLoading={showModalConfirmButtonLoading}
/>
</>
</Main>
);
};

View File

@ -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;

View File

@ -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",

View File

@ -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';

View File

@ -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;

View File

@ -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';

View File

@ -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 (
<Table colCount={4} rowCount={sortedLocales.length + 1}>
<Thead>
<Tr>
<Th
action={
<IconButton
label={formatMessage({ id: getTrad('Settings.locales.list.sort.id') })}
icon={
<ActionIconWrapper reverse={isReversedArrow(SortingKeys.id)}>
<DropdownIcon />
</ActionIconWrapper>
}
noBorder
onClick={() => handleSorting(SortingKeys.id)}
/>
}
>
<TableLabel textColor="neutral600">
{formatMessage({ id: getTrad('Settings.locales.row.id') })}
</TableLabel>
</Th>
<Th
action={
<IconButton
label={formatMessage({ id: getTrad('Settings.locales.list.sort.displayName') })}
icon={
<ActionIconWrapper reverse={isReversedArrow(SortingKeys.name)}>
<DropdownIcon />
</ActionIconWrapper>
}
noBorder
onClick={() => handleSorting(SortingKeys.name)}
/>
}
>
<TableLabel textColor="neutral600">
{formatMessage({ id: getTrad('Settings.locales.row.displayName') })}
</TableLabel>
</Th>
<Th
action={
<IconButton
label={formatMessage({ id: getTrad('Settings.locales.list.sort.default') })}
icon={
<ActionIconWrapper reverse={isReversedArrow(SortingKeys.default)}>
<DropdownIcon />
</ActionIconWrapper>
}
noBorder
onClick={() => handleSorting(SortingKeys.default)}
/>
}
>
<TableLabel textColor="neutral600">
{formatMessage({ id: getTrad('Settings.locales.row.default-locale') })}
</TableLabel>
</Th>
<Th>
<VisuallyHidden>Actions</VisuallyHidden>
</Th>
</Tr>
</Thead>
<Tbody>
{sortedLocales.map(locale => (
<Tr key={locale.id}>
<Td>
<Text textColor="neutral800">{locale.id}</Text>
</Td>
<Td>
<Text textColor="neutral800">{locale.name}</Text>
</Td>
<Td>
<Text textColor="neutral800">
{locale.isDefault
? formatMessage({ id: getTrad('Settings.locales.row.default-locale') })
: null}
</Text>
</Td>
<Td>
<Stack horizontal size={1}>
{onEditLocale && (
<IconButton
onClick={() => onEditLocale(locale)}
label={formatMessage({ id: getTrad('Settings.list.actions.edit') })}
icon={<EditIcon />}
noBorder
/>
)}
{onDeleteLocale && !locale.isDefault && (
<IconButton
onClick={() => onDeleteLocale(locale)}
label={formatMessage({ id: getTrad('Settings.list.actions.delete') })}
icon={<DeleteIcon />}
noBorder
/>
)}
</Stack>
</Td>
</Tr>
))}
</Tbody>
</Table>
);
};
LocaleTable.defaultProps = {
locales: [],
onDeleteLocale: undefined,
onEditLocale: undefined,
};
LocaleTable.propTypes = {
locales: PropTypes.array,
onDeleteLocale: PropTypes.func,
onEditLocale: PropTypes.func,
};
export default LocaleTable;

View File

@ -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 (
<>
<List
radius="2px"
title={listTitle}
items={locales}
isLoading={isLoading}
customRowComponent={locale => (
<LocaleRow locale={locale} onDelete={handleDeleteLocale} onEdit={handleEditLocale} />
)}
/>
<ModalCreate
isOpened={isCreating}
onClose={onToggleCreateModal}
alreadyUsedLocales={locales}
/>
<ModalDelete localeToDelete={localeToDelete} onClose={closeModalToDelete} />
<ModalEdit localeToEdit={localeToEdit} onClose={closeModalToEdit} locales={locales} />
</>
);
}
return (
<>
<EmptyState
title={formatMessage({ id: getTrad('Settings.list.empty.title') })}
description={formatMessage({ id: getTrad('Settings.list.empty.description') })}
<Main labelledBy="title" tabIndex={-1}>
<HeaderLayout
id="title"
primaryAction={
<Button startIcon={<AddIcon />} onClick={onToggleCreateModal}>
{formatMessage({ id: getTrad('Settings.list.actions.add') })}
</Button>
}
title={formatMessage({ id: getTrad('plugin.name') })}
subtitle={formatMessage({ id: getTrad('Settings.list.description') })}
/>
{onToggleCreateModal && (
<ListButton>
<Button
label={formatMessage({ id: getTrad('Settings.list.actions.add') })}
onClick={onToggleCreateModal}
color="primary"
type="button"
icon={<Plus fill="#007eff" width="11px" height="11px" />}
<ContentLayout>
{locales?.length > 0 ? (
<LocaleTable
locales={locales}
onDeleteLocale={handleDeleteLocale}
onEditLocale={handleEditLocale}
/>
</ListButton>
)}
) : (
<ContentLayout>
<EmptyStateLayout
icon={<EmptyStateDocument width={undefined} height={undefined} />}
content={formatMessage({ id: getTrad('Settings.list.empty.title') })}
action={
onToggleCreateModal ? (
<Button variant="secondary" startIcon={<AddIcon />} onClick={onToggleCreateModal}>
{formatMessage({ id: getTrad('Settings.list.actions.add') })}
</Button>
) : null
}
/>
</ContentLayout>
)}
</ContentLayout>
<ModalCreate isOpened={isCreating} onClose={onToggleCreateModal} />
</>
<ModalCreate
isOpened={isCreating}
onClose={onToggleCreateModal}
alreadyUsedLocales={locales}
/>
<ModalDelete localeToDelete={localeToDelete} onClose={closeModalToDelete} />
<ModalEdit localeToEdit={localeToEdit} onClose={closeModalToEdit} locales={locales} />
</Main>
);
};

View File

@ -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: (
<span aria-label={formatMessage({ id: getTrad('Settings.list.actions.edit') })}>
<Pencil fill="#0e1622" />
</span>
),
onClick: () => onEdit(locale),
});
}
if (onDelete && !locale.isDefault) {
links.push({
icon: !locale.isDefault ? (
<span aria-label={formatMessage({ id: getTrad('Settings.list.actions.delete') })}>
<FontAwesomeIcon icon="trash-alt" />
</span>
) : null,
onClick: e => {
e.stopPropagation();
onDelete(locale);
},
});
}
return (
<CustomRow onClick={() => onEdit(locale)}>
<td>
<Text>{locale.code}</Text>
</td>
<td>
<Text fontWeight="regular">{locale.name}</Text>
</td>
<td>
<Text>
{locale.isDefault
? formatMessage({ id: getTrad('Settings.locales.row.default-locale') })
: null}
</Text>
</td>
<td>
<IconLinks links={links} />
</td>
</CustomRow>
);
};
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;

View File

@ -1,2 +0,0 @@
// eslint-disable-next-line import/prefer-default-export
export { default as LocaleRow } from './LocaleRow';

View File

@ -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 ? <Button {...props} /> : null),
style: {
paddingLeft: 15,
paddingRight: 15,
},
},
];
return (
<>
<Header
title={{
label: formatMessage({ id: getTrad('plugin.name') }),
}}
content={formatMessage({ id: getTrad('Settings.list.description') })}
actions={actions}
/>
<BaselineAlignment top size="3px" />
{canReadLocale ? (
<LocaleList
canUpdateLocale={canUpdateLocale}
canDeleteLocale={canDeleteLocale}
onToggleCreateModal={handleToggleModalCreate}
isCreating={isOpenedCreateModal}
/>
) : null}
</>
);
return canReadLocale ? (
<LocaleList
canUpdateLocale={canUpdateLocale}
canDeleteLocale={canDeleteLocale}
onToggleCreateModal={handleToggleModalCreate}
isCreating={isOpenedCreateModal}
/>
) : null;
};
LocaleSettingsPage.propTypes = {

View File

@ -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.",

View File

@ -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==