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/state-in-constructor': 0,
'react/static-property-placement': 0, 'react/static-property-placement': 0,
'react/display-name': 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 PropTypes from 'prop-types';
import { CustomRow } from '@buffetjs/styles'; import React from 'react';
import { IconLinks, Text } from '@buffetjs/core';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import RoleDescription from './RoleDescription'; import RoleDescription from './RoleDescription';
const RoleRow = ({ role, onClick, links, prefix }) => { const RoleRow = ({ onToggle, id, name, description, usersCount, isChecked, icons }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const number = role.usersCount;
const text = formatMessage( const usersCountText = formatMessage(
{ id: `Roles.RoleRow.user-count.${number > 1 ? 'plural' : 'singular'}` }, { id: `Roles.RoleRow.user-count.${usersCount > 1 ? 'plural' : 'singular'}` },
{ number } { number: usersCount }
); );
return ( return (
<CustomRow onClick={onClick}> <Tr>
{prefix && <td style={{ width: 55 }}>{prefix}</td>} {Boolean(onToggle) && (
<td> <Td>
<Text fontWeight="semiBold">{role.name}</Text> <BaseCheckbox
</td> name="role-checkbox"
<td> onValueChange={() => onToggle(id)}
<RoleDescription>{role.description}</RoleDescription> value={isChecked}
</td> aria-label={formatMessage({ id: `Roles.RoleRow.select-all` }, { name })}
<td> />
<Text>{text}</Text> </Td>
</td> )}
<td> <Td>
<IconLinks links={links} /> <Text textColor="neutral800">{name}</Text>
</td> </Td>
</CustomRow> <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 = { RoleRow.defaultProps = {
onClick: null, onToggle: undefined,
prefix: null, isChecked: undefined,
}; };
RoleRow.propTypes = { RoleRow.propTypes = {
links: PropTypes.array.isRequired, id: PropTypes.number.isRequired,
onClick: PropTypes.func, name: PropTypes.string.isRequired,
prefix: PropTypes.node, description: PropTypes.string.isRequired,
role: PropTypes.object.isRequired, usersCount: PropTypes.number.isRequired,
icons: PropTypes.array.isRequired,
onToggle: PropTypes.func,
isChecked: PropTypes.bool,
}; };
export default RoleRow; 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 { request, useNotification } from '@strapi/helper-plugin';
import { get } from 'lodash'; import { get } from 'lodash';
import init from './init'; import init from './init';
@ -17,7 +17,7 @@ const useRolesList = (shouldFetchData = true) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldFetchData]); }, [shouldFetchData]);
const fetchRolesList = async () => { const fetchRolesList = useCallback(async () => {
try { try {
dispatch({ dispatch({
type: 'GET_DATA', type: 'GET_DATA',
@ -43,7 +43,7 @@ const useRolesList = (shouldFetchData = true) => {
}); });
} }
} }
}; }, [toggleNotification]);
return { roles, isLoading, getData: fetchRolesList }; return { roles, isLoading, getData: fetchRolesList };
}; };

View File

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

View File

@ -1,148 +1,193 @@
import React, { useCallback, useEffect, useState } from 'react'; import { useQuery, useTracking } from '@strapi/helper-plugin';
import { List, Header } from '@buffetjs/custom'; import { AddIcon, DeleteIcon, EditIcon, Duplicate } from '@strapi/icons';
import { Button } from '@buffetjs/core'; import {
import { Duplicate, Pencil, Plus } from '@buffetjs/icons'; Button,
ContentLayout,
HeaderLayout,
Table,
TableLabel,
Tbody,
TFooter,
Th,
Thead,
Tr,
VisuallyHidden,
Main,
} from '@strapi/parts';
import matchSorter from 'match-sorter'; import matchSorter from 'match-sorter';
import React, { useCallback, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { EmptyRole, RoleRow } from '../../../components/Roles';
import { ListButton, useTracking, useQuery, useRBAC } from '@strapi/helper-plugin';
import adminPermissions from '../../../permissions';
import PageTitle from '../../../components/SettingsPageTitle'; import PageTitle from '../../../components/SettingsPageTitle';
import { EmptyRole, RoleListWrapper, RoleRow } from '../../../components/Roles';
import { useRolesList, useSettingsHeaderSearchContext } from '../../../hooks';
import UpgradePlanModal from '../../../components/UpgradePlanModal'; import UpgradePlanModal from '../../../components/UpgradePlanModal';
import BaselineAlignment from './BaselineAlignment'; import { useRolesList } from '../../../hooks';
const RoleListPage = () => { const useSortedRoles = () => {
const { formatMessage } = useIntl();
const { push } = useHistory();
const [isOpen, setIsOpen] = useState(false);
const { trackUsage } = useTracking();
const { roles, isLoading } = useRolesList(); const { roles, isLoading } = useRolesList();
const { toggleHeaderSearch } = useSettingsHeaderSearchContext();
const {
allowedActions: { canUpdate },
} = useRBAC(adminPermissions.settings.roles);
const query = useQuery(); const query = useQuery();
const _q = decodeURIComponent(query.get('_q') || ''); const _q = decodeURIComponent(query.get('_q') || '');
const results = matchSorter(roles, _q, { keys: ['name', 'description'] }); const sortedRoles = matchSorter(roles, _q, { keys: ['name', 'description'] });
useEffect(() => { return { isLoading, sortedRoles };
toggleHeaderSearch({ id: 'Settings.permissions.menu.link.roles.label' }); };
return () => { const useRoleActions = () => {
toggleHeaderSearch(); const { formatMessage } = useIntl();
}; const [isModalOpen, setIsModalOpen] = useState(false);
// eslint-disable-next-line react-hooks/exhaustive-deps const { trackUsage } = useTracking();
}, []); const { push } = useHistory();
const handleGoTo = useCallback( const handleGoTo = useCallback(
id => { id => {
push(`/settings/roles/${id}`); push(`/settings/roles/${id}`);
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps [push]
[]
); );
const handleToggle = useCallback(e => { const handleToggle = useCallback(() => {
e.preventDefault(); setIsModalOpen(prev => !prev);
e.stopPropagation();
setIsOpen(prev => !prev);
}, []); }, []);
const handleToggleModalForCreatingRole = useCallback(e => { const handleToggleModalForCreatingRole = useCallback(() => {
e.preventDefault();
e.stopPropagation();
trackUsage('didShowRBACUpgradeModal'); trackUsage('didShowRBACUpgradeModal');
setIsModalOpen(true);
}, [trackUsage]);
setIsOpen(true); const getIcons = useCallback(
// eslint-disable-next-line react-hooks/exhaustive-deps 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 = [ return {
{ isModalOpen,
label: formatMessage({ handleToggleModalForCreatingRole,
id: 'Settings.roles.list.button.add', handleToggle,
defaultMessage: 'Add new role', getIcons,
}), };
onClick: handleToggleModalForCreatingRole, };
color: 'primary',
type: 'button',
icon: true,
},
];
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 ( return (
<> <Main labelledBy="title">
<PageTitle name="Roles" /> <PageTitle name="Roles" />
<Header <HeaderLayout
icon id="title"
title={{ primaryAction={
label: formatMessage({ <Button onClick={handleToggleModalForCreatingRole} startIcon={<AddIcon />}>
id: 'Settings.roles.title', {formatMessage({
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({
id: 'Settings.roles.list.button.add', id: 'Settings.roles.list.button.add',
defaultMessage: 'Add new role', defaultMessage: 'Add new role',
})} })}
/> </Button>
</ListButton> }
</RoleListWrapper> title={formatMessage({
<UpgradePlanModal isOpen={isOpen} onToggle={handleToggle} /> 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.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.plural": "{number} users",
"Roles.RoleRow.user-count.singular": "{number} user", "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})...", "Roles.components.List.empty.withSearch": "There is no role corresponding to the search ({search})...",
"Settings.PageTitle": "Settings - {name}", "Settings.PageTitle": "Settings - {name}",
"Settings.application.description": "See your project's details", "Settings.application.description": "See your project's details",
@ -137,6 +138,10 @@
"Settings.roles.list.description": "List of roles", "Settings.roles.list.description": "List of roles",
"Settings.roles.list.title.plural": "{number} roles", "Settings.roles.list.title.plural": "{number} roles",
"Settings.roles.list.title.singular": "{number} role", "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": "Roles",
"Settings.roles.title.singular": "role", "Settings.roles.title.singular": "role",
"Settings.sso.description": "Configure the settings for the Single Sign-On feature.", "Settings.sso.description": "Configure the settings for the Single Sign-On feature.",
@ -301,6 +306,8 @@
"app.utils.add-filter": "Add filter", "app.utils.add-filter": "Add filter",
"app.utils.defaultMessage": " ", "app.utils.defaultMessage": " ",
"app.utils.delete": "Delete", "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.errors.file-too-big.message": "The file is too big",
"app.utils.filters": "Filters", "app.utils.filters": "Filters",
"app.utils.placeholder.defaultMessage": " ", "app.utils.placeholder.defaultMessage": " ",

View File

@ -5,6 +5,8 @@ import moment from 'moment';
import { Formik } from 'formik'; import { Formik } from 'formik';
import { get, isEmpty } from 'lodash'; import { get, isEmpty } from 'lodash';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { HeaderLayout, Button } from '@strapi/parts';
import { AddIcon, EditIcon } from '@strapi/icons';
import { import {
BaselineAlignment, BaselineAlignment,
CheckPagePermissions, CheckPagePermissions,
@ -17,7 +19,6 @@ import { useHistory, useRouteMatch } from 'react-router-dom';
import adminPermissions from '../../../../../admin/src/permissions'; import adminPermissions from '../../../../../admin/src/permissions';
import { useFetchPermissionsLayout, useFetchRole } from '../../../../../admin/src/hooks'; import { useFetchPermissionsLayout, useFetchRole } from '../../../../../admin/src/hooks';
import PageTitle from '../../../../../admin/src/components/SettingsPageTitle'; import PageTitle from '../../../../../admin/src/components/SettingsPageTitle';
import ContainerFluid from '../../../../../admin/src/components/ContainerFluid';
import FormCard from '../../../../../admin/src/components/FormBloc'; import FormCard from '../../../../../admin/src/components/FormBloc';
import { ButtonWithNumber } from '../../../../../admin/src/components/Roles'; import { ButtonWithNumber } from '../../../../../admin/src/components/Roles';
import SizedInput from '../../../../../admin/src/components/SizedInput'; import SizedInput from '../../../../../admin/src/components/SizedInput';
@ -142,7 +143,18 @@ const CreatePage = () => {
> >
{({ handleSubmit, values, errors, handleReset, handleChange, handleBlur }) => ( {({ handleSubmit, values, errors, handleReset, handleChange, handleBlur }) => (
<form onSubmit={handleSubmit}> <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 <Header
title={{ title={{
label: formatMessage({ label: formatMessage({
@ -202,7 +214,7 @@ const CreatePage = () => {
/> />
</Padded> </Padded>
)} )}
</ContainerFluid> </>
</form> </form>
)} )}
</Formik> </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 { import {
useQuery, LoadingIndicatorPage,
ListButton,
PopUpWarning, PopUpWarning,
request, request,
useRBAC,
useNotification, useNotification,
LoadingIndicatorPage, useQuery,
useRBAC,
} from '@strapi/helper-plugin'; } 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 { 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 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 { useRolesList } from '../../../../../admin/src/hooks';
import RoleRow from './RoleRow'; import adminPermissions from '../../../../../admin/src/permissions';
import BaselineAlignment from './BaselineAlignment';
import reducer, { initialState } from './reducer'; import reducer, { initialState } from './reducer';
const RoleListPage = () => { const useSortedRoles = () => {
const toggleNotification = useNotification();
const [isWarningDeleteAllOpened, setIsWarningDeleteAllOpenend] = useState(false);
const { formatMessage } = useIntl();
const { push } = useHistory();
const [{ selectedRoles, showModalConfirmButtonLoading, shouldRefetchData }, dispath] = useReducer(
reducer,
initialState
);
const { const {
isLoading: isLoadingForPermissions, isLoading: isLoadingForPermissions,
allowedActions: { canCreate, canDelete, canRead, canUpdate }, allowedActions: { canCreate, canDelete, canRead, canUpdate },
} = useRBAC(adminPermissions.settings.roles); } = useRBAC(adminPermissions.settings.roles);
const { getData, roles, isLoading } = useRolesList(false); const { getData, roles, isLoading } = useRolesList(false);
const getDataRef = useRef(getData);
const { toggleHeaderSearch } = useSettingsHeaderSearchContext();
const query = useQuery(); const query = useQuery();
const _q = decodeURIComponent(query.get('_q') || ''); const _q = decodeURIComponent(query.get('_q') || '');
const results = matchSorter(roles, _q, { keys: ['name', 'description'] }); const sortedRoles = 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]);
useEffect(() => { useEffect(() => {
if (!isLoadingForPermissions && canRead) { 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 = () => { const handleClosedModal = () => {
if (shouldRefetchData) { if (shouldRefetchData) {
@ -70,14 +78,14 @@ const RoleListPage = () => {
} }
// Empty the selected ids when the modal closes // Empty the selected ids when the modal closes
dispath({ dispatch({
type: 'RESET_DATA_TO_DELETE', type: 'RESET_DATA_TO_DELETE',
}); });
}; };
const handleConfirmDeleteData = async () => { const handleConfirmDeleteData = async () => {
try { try {
dispath({ dispatch({
type: 'ON_REMOVE_ROLES', type: 'ON_REMOVE_ROLES',
}); });
const filteredRoles = selectedRoles.filter(currentId => { const filteredRoles = selectedRoles.filter(currentId => {
@ -103,7 +111,7 @@ const RoleListPage = () => {
// Empty the selectedRolesId and set the shouldRefetchData to true so the // Empty the selectedRolesId and set the shouldRefetchData to true so the
// list is updated when closing the modal // list is updated when closing the modal
dispath({ dispatch({
type: 'ON_REMOVE_ROLES_SUCCEEDED', type: 'ON_REMOVE_ROLES_SUCCEEDED',
}); });
} }
@ -128,123 +136,275 @@ const RoleListPage = () => {
} }
}; };
const handleDuplicateRole = id => { const onRoleDuplicate = useCallback(
push(`/settings/roles/duplicate/${id}`); id => {
}; push(`/settings/roles/duplicate/${id}`);
},
[push]
);
const handleNewRoleClick = () => push('/settings/roles/new'); const handleNewRoleClick = () => push('/settings/roles/new');
const handleRemoveRole = roleId => { const onRoleRemove = useCallback(roleId => {
dispath({ dispatch({
type: 'SET_ROLE_TO_DELETE', type: 'SET_ROLE_TO_DELETE',
id: roleId, id: roleId,
}); });
handleToggleModal(); handleToggleModal();
}; }, []);
const handleRoleToggle = roleId => { const onRoleToggle = roleId => {
dispath({ dispatch({
type: 'ON_SELECTION', type: 'ON_SELECTION',
id: roleId, id: roleId,
}); });
}; };
const onAllRolesToggle = () =>
dispatch({
type: 'TOGGLE_ALL',
ids: sortedRoles.map(r => r.id),
});
const handleToggleModal = () => setIsWarningDeleteAllOpenend(prev => !prev); const handleToggleModal = () => setIsWarningDeleteAllOpenend(prev => !prev);
/* eslint-disable indent */ const handleGoTo = useCallback(
const headerActions = canCreate id => {
? [ push(`/settings/roles/${id}`);
{ },
label: formatMessage({ [push]
id: 'Settings.roles.list.button.add', );
defaultMessage: 'Add new role',
}),
onClick: handleNewRoleClick,
color: 'primary',
type: 'button',
icon: true,
},
]
: [];
/* eslint-enable indent */
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) { if (isLoadingForPermissions) {
return <LoadingIndicatorPage />; return <LoadingIndicatorPage />;
} }
return ( return (
<> <Main labelledBy="title">
<PageTitle name="Roles" /> <PageTitle name="Roles" />
<Header <HeaderLayout
title={{ id="title"
label: formatMessage({ primaryAction={
id: 'Settings.roles.title', canCreate ? (
defaultMessage: 'roles', <Button onClick={handleNewRoleClick} startIcon={<AddIcon />}>
}), {formatMessage({
}} id: 'Settings.roles.list.button.add',
content={formatMessage({ defaultMessage: 'Add new role',
})}
</Button>
) : null
}
title={formatMessage({
id: 'Settings.roles.title',
defaultMessage: 'roles',
})}
subtitle={formatMessage({
id: 'Settings.roles.list.description', id: 'Settings.roles.list.description',
defaultMessage: 'List of roles', defaultMessage: 'List of roles',
})} })}
actions={headerActions} as="h2"
isLoading={isLoading}
/> />
<BaselineAlignment />
{canRead && ( {canRead && (
<RoleListWrapper> <ContentLayout>
<List <Table
title={formatMessage( colCount={colCount}
{ rowCount={rowCount}
id: `Settings.roles.list.title${resultsCount > 1 ? '.plural' : '.singular'}`, footer={
defaultMessage: `{number} ${resultsCount > 1 ? 'roles' : 'role'}`, canCreate ? (
}, <TFooter onClick={handleNewRoleClick} icon={<AddIcon />}>
{ number: resultsCount } {formatMessage({
)} id: 'Settings.roles.list.button.add',
isLoading={isLoading} defaultMessage: 'Add new role',
/* eslint-disable indent */ })}
button={ </TFooter>
canDelete ) : null
? {
color: 'delete',
disabled: selectedRoles.length === 0,
label: formatMessage({ id: 'app.utils.delete', defaultMessage: 'Delete' }),
onClick: handleToggleModal,
type: 'button',
}
: null
} }
/* eslint-enable indent */ >
items={results} <Thead>
customRowComponent={role => ( <Tr>
<RoleRow {!!onRoleToggle && (
canCreate={canCreate} <Th>
canDelete={canDelete} <BaseCheckbox
canUpdate={canUpdate} aria-label="Select all entries"
selectedRoles={selectedRoles} indeterminate={isAllEntriesIndeterminate}
onRoleDuplicate={handleDuplicateRole} value={isAllChecked}
onRoleRemove={handleRemoveRole} onChange={onAllRolesToggle}
onRoleToggle={handleRoleToggle} />
role={role} </Th>
/> )}
)} <Th>
/> <TableLabel>
{!resultsCount && !isLoading && <EmptyRole />} {formatMessage({
{canCreate && ( id: 'Settings.roles.list.header.name',
<ListButton> defaultMessage: 'Name',
<Button })}
onClick={handleNewRoleClick} </TableLabel>
icon={<Plus fill="#007eff" width="11px" height="11px" />} </Th>
label={formatMessage({ <Th>
id: 'Settings.roles.list.button.add', <TableLabel>
defaultMessage: 'Add new role', {formatMessage({
})} id: 'Settings.roles.list.header.description',
/> defaultMessage: 'Description',
</ListButton> })}
)} </TableLabel>
</RoleListWrapper> </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 <PopUpWarning
isOpen={isWarningDeleteAllOpened} isOpen={isWarningDeleteAllOpened}
@ -253,7 +413,7 @@ const RoleListPage = () => {
toggleModal={handleToggleModal} toggleModal={handleToggleModal}
isConfirmButtonLoading={showModalConfirmButtonLoading} isConfirmButtonLoading={showModalConfirmButtonLoading}
/> />
</> </Main>
); );
}; };

View File

@ -21,6 +21,15 @@ const reducer = (state, action) =>
} }
break; break;
} }
case 'TOGGLE_ALL': {
if (state.selectedRoles.length) {
draftState.selectedRoles = [];
} else {
const { ids } = action;
draftState.selectedRoles = ids;
}
break;
}
case 'ON_REMOVE_ROLES': { case 'ON_REMOVE_ROLES': {
draftState.showModalConfirmButtonLoading = true; draftState.showModalConfirmButtonLoading = true;
break; break;

View File

@ -106,8 +106,8 @@
"react-loadable": "^5.5.0", "react-loadable": "^5.5.0",
"react-query": "3.19.0", "react-query": "3.19.0",
"react-redux": "7.2.3", "react-redux": "7.2.3",
"react-router": "^5.2.0", "react-router": "5.2.0",
"react-router-dom": "^5.0.0", "react-router-dom": "5.2.0",
"react-select": "^4.0.2", "react-select": "^4.0.2",
"react-tooltip": "4.2.18", "react-tooltip": "4.2.18",
"react-transition-group": "4.4.1", "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 React, { useState, useEffect, useRef } from 'react';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import { get } from 'lodash'; 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 useRBACProvider } from './hooks/useRBACProvider';
export { default as useRBAC } from './hooks/useRBAC'; export { default as useRBAC } from './hooks/useRBAC';
export { default as usePersistentState } from './hooks/usePersistentState'; export { default as usePersistentState } from './hooks/usePersistentState';
export { default as useFocusWhenNavigate } from './hooks/useFocusWhenNavigate';
// Providers // Providers
export { default as LibraryProvider } from './providers/LibraryProvider'; 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 React, { useState } from 'react';
import { useIntl } from 'react-intl'; 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 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 useLocales from '../../hooks/useLocales';
import LocaleRow from '../LocaleRow';
import { getTrad } from '../../utils'; import { getTrad } from '../../utils';
import ModalEdit from '../ModalEdit'; import ModalEdit from '../ModalEdit';
import ModalDelete from '../ModalDelete'; import ModalDelete from '../ModalDelete';
import ModalCreate from '../ModalCreate'; import ModalCreate from '../ModalCreate';
import LocaleTable from './LocaleTable';
const LocaleList = ({ canUpdateLocale, canDeleteLocale, onToggleCreateModal, isCreating }) => { const LocaleList = ({ canUpdateLocale, canDeleteLocale, onToggleCreateModal, isCreating }) => {
const [localeToDelete, setLocaleToDelete] = useState(); const [localeToDelete, setLocaleToDelete] = useState();
const [localeToEdit, setLocaleToEdit] = useState(); const [localeToEdit, setLocaleToEdit] = useState();
const { locales, isLoading } = useLocales(); const { locales } = useLocales();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
useFocusWhenNavigate();
// Delete actions // Delete actions
const closeModalToDelete = () => setLocaleToDelete(undefined); const closeModalToDelete = () => setLocaleToDelete(undefined);
const handleDeleteLocale = canDeleteLocale ? setLocaleToDelete : undefined; const handleDeleteLocale = canDeleteLocale ? setLocaleToDelete : undefined;
// Edit actions // Edit actions
const closeModalToEdit = () => { const closeModalToEdit = () => setLocaleToEdit(undefined);
setLocaleToEdit(undefined);
};
const handleEditLocale = canUpdateLocale ? 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 ( return (
<> <Main labelledBy="title" tabIndex={-1}>
<EmptyState <HeaderLayout
title={formatMessage({ id: getTrad('Settings.list.empty.title') })} id="title"
description={formatMessage({ id: getTrad('Settings.list.empty.description') })} 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') })}
/> />
<ContentLayout>
{onToggleCreateModal && ( {locales?.length > 0 ? (
<ListButton> <LocaleTable
<Button locales={locales}
label={formatMessage({ id: getTrad('Settings.list.actions.add') })} onDeleteLocale={handleDeleteLocale}
onClick={onToggleCreateModal} onEditLocale={handleEditLocale}
color="primary"
type="button"
icon={<Plus fill="#007eff" width="11px" height="11px" />}
/> />
</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 React, { useState } from 'react';
import PropTypes from 'prop-types'; 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'; import LocaleList from '../../components/LocaleList';
const LocaleSettingsPage = ({ const LocaleSettingsPage = ({
@ -13,50 +8,20 @@ const LocaleSettingsPage = ({
canDeleteLocale, canDeleteLocale,
canUpdateLocale, canUpdateLocale,
}) => { }) => {
const { formatMessage } = useIntl();
const [isOpenedCreateModal, setIsOpenedCreateModal] = useState(false); const [isOpenedCreateModal, setIsOpenedCreateModal] = useState(false);
const handleToggleModalCreate = canCreateLocale const handleToggleModalCreate = canCreateLocale
? () => setIsOpenedCreateModal(s => !s) ? () => setIsOpenedCreateModal(s => !s)
: undefined; : undefined;
const actions = [ return canReadLocale ? (
{ <LocaleList
label: formatMessage({ id: getTrad('Settings.list.actions.add') }), canUpdateLocale={canUpdateLocale}
onClick: handleToggleModalCreate, canDeleteLocale={canDeleteLocale}
color: 'primary', onToggleCreateModal={handleToggleModalCreate}
type: 'button', isCreating={isOpenedCreateModal}
icon: true, />
Component: props => (canCreateLocale ? <Button {...props} /> : null), ) : 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}
</>
);
}; };
LocaleSettingsPage.propTypes = { LocaleSettingsPage.propTypes = {

View File

@ -17,6 +17,9 @@
"Settings.list.empty.title": "There are no locales.", "Settings.list.empty.title": "There are no locales.",
"Settings.locales.list.title.plural": "{number} Locales", "Settings.locales.list.title.plural": "{number} Locales",
"Settings.locales.list.title.singular": "{number} Locale", "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": "Advanced settings",
"Settings.locales.modal.advanced.setAsDefault": "Set as default locale", "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", "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.displayName.error": "The locale display name can only be less than 50 characters.",
"Settings.locales.modal.locales.label": "Locales", "Settings.locales.modal.locales.label": "Locales",
"Settings.locales.modal.title": "Configurations", "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.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.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.", "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" prop-types "^15.7.2"
react-is "^16.13.1" 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" version "5.2.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662"
integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==