Merge pull request #10873 from strapi/migration/rbac-delete

Migration/rbac delete
This commit is contained in:
cyril lopez 2021-09-03 12:13:51 +02:00 committed by GitHub
commit 070d7d64ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 92 additions and 264 deletions

View File

@ -1,9 +1,9 @@
import { Box, Row, Td, Text, Tr, IconButton, BaseCheckbox } from '@strapi/parts'; import { Box, Row, Td, Text, Tr, IconButton } from '@strapi/parts';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const RoleRow = ({ onToggle, id, name, description, usersCount, isChecked, icons }) => { const RoleRow = ({ id, name, description, usersCount, icons }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const usersCountText = formatMessage( const usersCountText = formatMessage(
@ -16,19 +16,6 @@ const RoleRow = ({ onToggle, id, name, description, usersCount, isChecked, icons
return ( return (
<Tr key={id}> <Tr key={id}>
{Boolean(onToggle) && (
<Td>
<BaseCheckbox
name="role-checkbox"
onValueChange={() => onToggle(id)}
value={isChecked}
aria-label={formatMessage(
{ id: `Roles.RoleRow.select-all`, defaultMessage: 'Select {name} for bulk actions' },
{ name }
)}
/>
</Td>
)}
<Td> <Td>
<Text textColor="neutral800">{name}</Text> <Text textColor="neutral800">{name}</Text>
</Td> </Td>
@ -53,19 +40,12 @@ const RoleRow = ({ onToggle, id, name, description, usersCount, isChecked, icons
); );
}; };
RoleRow.defaultProps = {
onToggle: undefined,
isChecked: undefined,
};
RoleRow.propTypes = { RoleRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
description: PropTypes.string.isRequired, description: PropTypes.string.isRequired,
usersCount: PropTypes.number.isRequired, usersCount: PropTypes.number.isRequired,
icons: PropTypes.array.isRequired, icons: PropTypes.array.isRequired,
onToggle: PropTypes.func,
isChecked: PropTypes.bool,
}; };
export default RoleRow; export default RoleRow;

View File

@ -1,6 +1,7 @@
import { useEffect, useReducer, useCallback } from 'react'; import { useEffect, useReducer, useCallback } from 'react';
import { request, useNotification } from '@strapi/helper-plugin'; import { useNotification } from '@strapi/helper-plugin';
import { get } from 'lodash'; import get from 'lodash/get';
import { axiosInstance } from '../../core/utils';
import init from './init'; import init from './init';
import reducer, { initialState } from './reducer'; import reducer, { initialState } from './reducer';
@ -23,7 +24,9 @@ const useRolesList = (shouldFetchData = true) => {
type: 'GET_DATA', type: 'GET_DATA',
}); });
const { data } = await request('/admin/roles', { method: 'GET' }); const {
data: { data },
} = await axiosInstance.get('/admin/roles');
dispatch({ dispatch({
type: 'GET_DATA_SUCCEEDED', type: 'GET_DATA_SUCCEEDED',

View File

@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import { import {
CustomContentLayout, CustomContentLayout,
useRBAC, Search,
SettingsPageTitle, SettingsPageTitle,
useRBAC,
useNotification, useNotification,
useFocusWhenNavigate, useFocusWhenNavigate,
} from '@strapi/helper-plugin'; } from '@strapi/helper-plugin';
@ -15,7 +16,6 @@ import get from 'lodash/get';
import adminPermissions from '../../../permissions'; import adminPermissions from '../../../permissions';
import DynamicTable from './DynamicTable'; import DynamicTable from './DynamicTable';
import Filters from './Filters'; import Filters from './Filters';
import Search from './Search';
import PaginationFooter from './PaginationFooter'; import PaginationFooter from './PaginationFooter';
import { deleteData, fetchData } from './utils/api'; import { deleteData, fetchData } from './utils/api';
import displayedFilters from './utils/displayedFilters'; import displayedFilters from './utils/displayedFilters';

View File

@ -699,46 +699,6 @@ describe('ADMIN | Pages | USERS | ListPage', () => {
height: 0.25rem; height: 0.25rem;
} }
.c13 {
padding-right: 56px;
padding-left: 56px;
}
.c36 tr:last-of-type {
border-bottom: none;
}
.c37 {
border-bottom: 1px solid #eaeaef;
}
.c37 td,
.c37 th {
padding: 16px;
}
.c37 td:first-of-type,
.c37 th:first-of-type {
padding: 0 4px;
}
.c38 {
vertical-align: middle;
text-align: left;
color: #666687;
outline-offset: -4px;
}
.c38 input {
vertical-align: sub;
}
.c34 {
-webkit-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
.c15 { .c15 {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
@ -788,6 +748,46 @@ describe('ADMIN | Pages | USERS | ListPage', () => {
fill: #666687; fill: #666687;
} }
.c13 {
padding-right: 56px;
padding-left: 56px;
}
.c36 tr:last-of-type {
border-bottom: none;
}
.c37 {
border-bottom: 1px solid #eaeaef;
}
.c37 td,
.c37 th {
padding: 16px;
}
.c37 td:first-of-type,
.c37 th:first-of-type {
padding: 0 4px;
}
.c38 {
vertical-align: middle;
text-align: left;
color: #666687;
outline-offset: -4px;
}
.c38 input {
vertical-align: sub;
}
.c34 {
-webkit-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
<main <main
aria-labelledby="title" aria-labelledby="title"
class="c0" class="c0"

View File

@ -1,10 +1,11 @@
import React, { useCallback, useEffect, useReducer, useState } from 'react';
import { import {
ConfirmDialog,
LoadingIndicatorPage, LoadingIndicatorPage,
PopUpWarning, Search,
SettingsPageTitle, SettingsPageTitle,
request,
useNotification, useNotification,
useQuery, useQueryParams,
useRBAC, useRBAC,
} from '@strapi/helper-plugin'; } from '@strapi/helper-plugin';
import { AddIcon, DeleteIcon, Duplicate, EditIcon } from '@strapi/icons'; import { AddIcon, DeleteIcon, Duplicate, EditIcon } from '@strapi/icons';
@ -20,14 +21,14 @@ import {
Tr, Tr,
TableLabel, TableLabel,
VisuallyHidden, VisuallyHidden,
BaseCheckbox,
Main, Main,
ActionLayout,
} from '@strapi/parts'; } from '@strapi/parts';
import { get } from 'lodash'; import { get } from 'lodash';
import matchSorter from 'match-sorter'; import matchSorter from 'match-sorter';
import React, { useCallback, useEffect, useReducer, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { axiosInstance } from '../../../../../admin/src/core/utils';
import { EmptyRole, RoleRow as BaseRoleRow } from '../../../../../admin/src/components/Roles'; import { EmptyRole, RoleRow as BaseRoleRow } from '../../../../../admin/src/components/Roles';
import { useRolesList } from '../../../../../admin/src/hooks'; import { useRolesList } from '../../../../../admin/src/hooks';
import adminPermissions from '../../../../../admin/src/permissions'; import adminPermissions from '../../../../../admin/src/permissions';
@ -40,8 +41,8 @@ const useSortedRoles = () => {
} = useRBAC(adminPermissions.settings.roles); } = useRBAC(adminPermissions.settings.roles);
const { getData, roles, isLoading } = useRolesList(false); const { getData, roles, isLoading } = useRolesList(false);
const query = useQuery(); const [{ query }] = useQueryParams();
const _q = decodeURIComponent(query.get('_q') || ''); const _q = query?._q || '';
const sortedRoles = matchSorter(roles, _q, { keys: ['name', 'description'] }); const sortedRoles = matchSorter(roles, _q, { keys: ['name', 'description'] });
useEffect(() => { useEffect(() => {
@ -63,62 +64,33 @@ const useSortedRoles = () => {
}; };
}; };
const useRoleActions = ({ getData, canCreate, canDelete, canUpdate, roles, sortedRoles }) => { const useRoleActions = ({ getData, canCreate, canDelete, canUpdate }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const [isWarningDeleteAllOpened, setIsWarningDeleteAllOpenend] = useState(false); const [isWarningDeleteAllOpened, setIsWarningDeleteAllOpenend] = useState(false);
const { push } = useHistory(); const { push } = useHistory();
const [ const [{ selectedRoles, showModalConfirmButtonLoading, roleToDelete }, dispatch] = useReducer(
{ selectedRoles, showModalConfirmButtonLoading, shouldRefetchData }, reducer,
dispatch, initialState
] = useReducer(reducer, initialState); );
const handleClosedModal = () => { const handleDeleteData = async () => {
if (shouldRefetchData) {
getData();
}
// Empty the selected ids when the modal closes
dispatch({
type: 'RESET_DATA_TO_DELETE',
});
};
const handleConfirmDeleteData = async () => {
try { try {
dispatch({ dispatch({
type: 'ON_REMOVE_ROLES', type: 'ON_REMOVE_ROLES',
}); });
const filteredRoles = selectedRoles.filter(currentId => {
const currentRole = roles.find(role => role.id === currentId);
return currentRole.usersCount === 0; await axiosInstance.post('/admin/roles/batch-delete', {
ids: [roleToDelete],
}); });
if (selectedRoles.length !== filteredRoles.length) { await getData();
toggleNotification({
type: 'info',
message: { id: 'Roles.ListPage.notification.delete-all-not-allowed' },
});
}
if (filteredRoles.length) {
await request('/admin/roles/batch-delete', {
method: 'POST',
body: {
ids: filteredRoles,
},
});
// Empty the selectedRolesId and set the shouldRefetchData to true so the
// list is updated when closing the modal
dispatch({ dispatch({
type: 'ON_REMOVE_ROLES_SUCCEEDED', type: 'RESET_DATA_TO_DELETE',
}); });
}
} catch (err) { } catch (err) {
console.error(err);
const errorIds = get(err, ['response', 'payload', 'data', 'ids'], null); const errorIds = get(err, ['response', 'payload', 'data', 'ids'], null);
if (errorIds && Array.isArray(errorIds)) { if (errorIds && Array.isArray(errorIds)) {
@ -133,9 +105,8 @@ const useRoleActions = ({ getData, canCreate, canDelete, canUpdate, roles, sorte
message: { id: 'notification.error' }, message: { id: 'notification.error' },
}); });
} }
} finally {
handleToggleModal();
} }
handleToggleModal();
}; };
const onRoleDuplicate = useCallback( const onRoleDuplicate = useCallback(
@ -156,19 +127,6 @@ const useRoleActions = ({ getData, canCreate, canDelete, canUpdate, roles, sorte
handleToggleModal(); handleToggleModal();
}, []); }, []);
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); const handleToggleModal = () => setIsWarningDeleteAllOpenend(prev => !prev);
const handleGoTo = useCallback( const handleGoTo = useCallback(
@ -246,16 +204,13 @@ const useRoleActions = ({ getData, canCreate, canDelete, canUpdate, roles, sorte
); );
return { return {
handleClosedModal,
handleConfirmDeleteData,
handleNewRoleClick, handleNewRoleClick,
onRoleToggle,
onAllRolesToggle,
getIcons, getIcons,
selectedRoles, selectedRoles,
isWarningDeleteAllOpened, isWarningDeleteAllOpened,
showModalConfirmButtonLoading, showModalConfirmButtonLoading,
handleToggleModal, handleToggleModal,
handleDeleteData,
}; };
}; };
@ -271,21 +226,16 @@ const RoleListPage = () => {
isLoading, isLoading,
getData, getData,
sortedRoles, sortedRoles,
roles,
} = useSortedRoles(); } = useSortedRoles();
const { const {
handleClosedModal,
handleConfirmDeleteData,
handleNewRoleClick, handleNewRoleClick,
onRoleToggle,
onAllRolesToggle,
getIcons, getIcons,
selectedRoles,
isWarningDeleteAllOpened, isWarningDeleteAllOpened,
showModalConfirmButtonLoading, showModalConfirmButtonLoading,
handleToggleModal, handleToggleModal,
} = useRoleActions({ getData, canCreate, canDelete, canUpdate, roles, sortedRoles }); handleDeleteData,
} = useRoleActions({ getData, canCreate, canDelete, canUpdate });
// ! TODO - Show the search bar only if the user is allowed to read - add the search input // ! TODO - Show the search bar only if the user is allowed to read - add the search input
// canRead // canRead
@ -293,11 +243,6 @@ const RoleListPage = () => {
const rowCount = sortedRoles.length + 1; const rowCount = sortedRoles.length + 1;
const colCount = 6; const colCount = 6;
const isAllEntriesIndeterminate = selectedRoles.length
? selectedRoles.length !== rowCount
: false;
const isAllChecked = selectedRoles.length ? selectedRoles.length === rowCount : false;
if (isLoadingForPermissions) { if (isLoadingForPermissions) {
return <LoadingIndicatorPage />; return <LoadingIndicatorPage />;
} }
@ -327,6 +272,7 @@ const RoleListPage = () => {
})} })}
as="h2" as="h2"
/> />
{canRead && <ActionLayout startActions={<Search />} />}
{canRead && ( {canRead && (
<ContentLayout> <ContentLayout>
<Table <Table
@ -345,16 +291,6 @@ const RoleListPage = () => {
> >
<Thead> <Thead>
<Tr> <Tr>
{!!onRoleToggle && (
<Th>
<BaseCheckbox
aria-label="Select all entries"
indeterminate={isAllEntriesIndeterminate}
value={isAllChecked}
onChange={onAllRolesToggle}
/>
</Th>
)}
<Th> <Th>
<TableLabel> <TableLabel>
{formatMessage({ {formatMessage({
@ -394,10 +330,6 @@ const RoleListPage = () => {
<BaseRoleRow <BaseRoleRow
key={role.id} key={role.id}
id={role.id} id={role.id}
onToggle={onRoleToggle}
isChecked={
selectedRoles.findIndex(selectedRoleId => selectedRoleId === role.id) !== -1
}
name={role.name} name={role.name}
description={role.description} description={role.description}
usersCount={role.usersCount} usersCount={role.usersCount}
@ -409,12 +341,11 @@ const RoleListPage = () => {
{!rowCount && !isLoading && <EmptyRole />} {!rowCount && !isLoading && <EmptyRole />}
</ContentLayout> </ContentLayout>
)} )}
<PopUpWarning <ConfirmDialog
isOpen={isWarningDeleteAllOpened} isVisible={isWarningDeleteAllOpened}
onClosed={handleClosedModal} onConfirm={handleDeleteData}
onConfirm={handleConfirmDeleteData}
toggleModal={handleToggleModal}
isConfirmButtonLoading={showModalConfirmButtonLoading} isConfirmButtonLoading={showModalConfirmButtonLoading}
onToggleDialog={handleToggleModal}
/> />
</Main> </Main>
); );

View File

@ -2,7 +2,7 @@
import produce from 'immer'; import produce from 'immer';
export const initialState = { export const initialState = {
selectedRoles: [], roleToDelete: null,
showModalConfirmButtonLoading: false, showModalConfirmButtonLoading: false,
shouldRefetchData: false, shouldRefetchData: false,
}; };
@ -10,57 +10,26 @@ export const initialState = {
const reducer = (state, action) => const reducer = (state, action) =>
produce(state, draftState => { produce(state, draftState => {
switch (action.type) { switch (action.type) {
case 'ON_SELECTION': {
const { id } = action;
const roleIndex = state.selectedRoles.findIndex(roleId => roleId === id);
if (roleIndex === -1) {
draftState.selectedRoles.push(id);
} else {
draftState.selectedRoles = state.selectedRoles.filter(roleId => roleId !== id);
}
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;
} }
case 'ON_REMOVE_ROLES_SUCCEEDED': { case 'ON_REMOVE_ROLES_SUCCEEDED': {
draftState.shouldRefetchData = true; draftState.shouldRefetchData = true;
draftState.roleToDelete = null;
break; break;
} }
case 'RESET_DATA_TO_DELETE': { case 'RESET_DATA_TO_DELETE': {
draftState.shouldRefetchData = false; draftState.shouldRefetchData = false;
draftState.selectedRoles = []; draftState.roleToDelete = null;
draftState.showModalConfirmButtonLoading = false; draftState.showModalConfirmButtonLoading = false;
break; break;
} }
case 'SET_ROLE_TO_DELETE': { case 'SET_ROLE_TO_DELETE': {
draftState.selectedRoles = [action.id]; draftState.roleToDelete = action.id;
break; break;
} }
// Leaving this code for the moment
// case 'ON_DUPLICATION': {
// const { id } = action;
// draftState.roles = state.roles.reduce((acc, c) => {
// if (c.id === id) {
// return acc.concat([c, { ...c, id: state.roles.length + 1 }]);
// }
// return [...acc, c];
// }, []);
// break;
// }
default: default:
return draftState; return draftState;
} }

View File

@ -11,54 +11,18 @@ describe('ADMIN | ee | CONTAINERS | ROLES | ListPage | reducer', () => {
}); });
}); });
describe('ON_SELECTION', () => {
it('should add the selected role correctly', () => {
const action = {
type: 'ON_SELECTION',
id: 2,
};
const initialState = {
selectedRoles: [],
shouldRefetchData: false,
};
const expected = {
selectedRoles: [2],
shouldRefetchData: false,
};
expect(reducer(initialState, action)).toEqual(expected);
});
it('should remove the selected role correctly', () => {
const action = {
type: 'ON_SELECTION',
id: 2,
};
const initialState = {
selectedRoles: [1, 2],
shouldRefetchData: false,
};
const expected = {
selectedRoles: [1],
shouldRefetchData: false,
};
expect(reducer(initialState, action)).toEqual(expected);
});
});
describe('ON_REMOVE_ROLES', () => { describe('ON_REMOVE_ROLES', () => {
it('should set the showModalConfirmButtonLoading to true', () => { it('should set the showModalConfirmButtonLoading to true', () => {
const action = { const action = {
type: 'ON_REMOVE_ROLES', type: 'ON_REMOVE_ROLES',
}; };
const initialState = { const initialState = {
selectedRoles: [], roleToDelete: 1,
shouldRefetchData: false, shouldRefetchData: false,
showModalConfirmButtonLoading: false, showModalConfirmButtonLoading: false,
}; };
const expected = { const expected = {
selectedRoles: [], roleToDelete: 1,
shouldRefetchData: false, shouldRefetchData: false,
showModalConfirmButtonLoading: true, showModalConfirmButtonLoading: true,
}; };
@ -73,12 +37,12 @@ describe('ADMIN | ee | CONTAINERS | ROLES | ListPage | reducer', () => {
type: 'ON_REMOVE_ROLES_SUCCEEDED', type: 'ON_REMOVE_ROLES_SUCCEEDED',
}; };
const initialState = { const initialState = {
selectedRoles: [], roleToDelete: 1,
shouldRefetchData: false, shouldRefetchData: false,
showModalConfirmButtonLoading: true, showModalConfirmButtonLoading: true,
}; };
const expected = { const expected = {
selectedRoles: [], roleToDelete: null,
shouldRefetchData: true, shouldRefetchData: true,
showModalConfirmButtonLoading: true, showModalConfirmButtonLoading: true,
}; };
@ -87,26 +51,6 @@ describe('ADMIN | ee | CONTAINERS | ROLES | ListPage | reducer', () => {
}); });
}); });
describe('RESET_DATA_TO_DELETE', () => {
it('should empty the selected role array and set the shouldRefetchData to false', () => {
const action = {
type: 'RESET_DATA_TO_DELETE',
};
const initialState = {
selectedRoles: [1, 2, 4],
shouldRefetchData: true,
showModalConfirmButtonLoading: true,
};
const expected = {
selectedRoles: [],
shouldRefetchData: false,
showModalConfirmButtonLoading: false,
};
expect(reducer(initialState, action)).toEqual(expected);
});
});
describe('SET_ROLE_TO_DELETE', () => { describe('SET_ROLE_TO_DELETE', () => {
it('should set the selected roles property correctly', () => { it('should set the selected roles property correctly', () => {
const action = { const action = {
@ -114,11 +58,11 @@ describe('ADMIN | ee | CONTAINERS | ROLES | ListPage | reducer', () => {
id: 6, id: 6,
}; };
const initialState = { const initialState = {
selectedRoles: [1, 2, 4], roleToDelete: null,
shouldRefetchData: false, shouldRefetchData: false,
}; };
const expected = { const expected = {
selectedRoles: [6], roleToDelete: 6,
shouldRefetchData: false, shouldRefetchData: false,
}; };

View File

@ -1,13 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useQueryParams } from '@strapi/helper-plugin';
import { SearchIcon } from '@strapi/icons'; import { SearchIcon } from '@strapi/icons';
import { IconButton } from '@strapi/parts/IconButton'; import { IconButton } from '@strapi/parts/IconButton';
import { TextInput } from '@strapi/parts/TextInput'; import { TextInput } from '@strapi/parts/TextInput';
import useQueryParams from '../../hooks/useQueryParams';
const Search = () => { const Search = () => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [{ query }, setQuery] = useQueryParams(); const [{ query }, setQuery] = useQueryParams();
const [value, setValue] = useState(query._q || ''); const [value, setValue] = useState(query?._q || '');
useEffect(() => { useEffect(() => {
const handler = setTimeout(() => { const handler = setTimeout(() => {

View File

@ -179,6 +179,7 @@ export { default as EmptyBodyTable } from './components/EmptyBodyTable';
export * from './components/InjectionZone'; export * from './components/InjectionZone';
export { default as LoadingIndicatorPage } from './components/LoadingIndicatorPage'; export { default as LoadingIndicatorPage } from './components/LoadingIndicatorPage';
export { default as SettingsPageTitle } from './components/SettingsPageTitle'; export { default as SettingsPageTitle } from './components/SettingsPageTitle';
export { default as Search } from './components/Search';
export { default as Status } from './components/Status'; export { default as Status } from './components/Status';
// New icons // New icons