mirror of
https://github.com/strapi/strapi.git
synced 2025-10-24 06:23:40 +00:00
Merge branch 'v4/ds-migration' into webhooks-listview-ds
This commit is contained in:
commit
f79f6eda59
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
|
||||
@ -81,7 +81,6 @@ const Admin = () => {
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<AppLayout
|
||||
sideNav={
|
||||
// eslint-disable-next-line react/jsx-wrap-multilines
|
||||
<LeftMenu
|
||||
generalSectionLinks={generalSectionLinks}
|
||||
pluginsSectionLinks={pluginsSectionLinks}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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": " ",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -1,2 +0,0 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as LocaleRow } from './LocaleRow';
|
||||
@ -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 = {
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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==
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user