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 React from 'react';
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 usersCountText = formatMessage(
@ -16,19 +16,6 @@ const RoleRow = ({ onToggle, id, name, description, usersCount, isChecked, icons
return (
<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>
<Text textColor="neutral800">{name}</Text>
</Td>
@ -53,19 +40,12 @@ const RoleRow = ({ onToggle, id, name, description, usersCount, isChecked, icons
);
};
RoleRow.defaultProps = {
onToggle: undefined,
isChecked: undefined,
};
RoleRow.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
usersCount: PropTypes.number.isRequired,
icons: PropTypes.array.isRequired,
onToggle: PropTypes.func,
isChecked: PropTypes.bool,
};
export default RoleRow;

View File

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

View File

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

View File

@ -699,46 +699,6 @@ describe('ADMIN | Pages | USERS | ListPage', () => {
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 {
display: -webkit-box;
display: -webkit-flex;
@ -788,6 +748,46 @@ describe('ADMIN | Pages | USERS | ListPage', () => {
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
aria-labelledby="title"
class="c0"

View File

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

View File

@ -2,7 +2,7 @@
import produce from 'immer';
export const initialState = {
selectedRoles: [],
roleToDelete: null,
showModalConfirmButtonLoading: false,
shouldRefetchData: false,
};
@ -10,57 +10,26 @@ export const initialState = {
const reducer = (state, action) =>
produce(state, draftState => {
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': {
draftState.showModalConfirmButtonLoading = true;
break;
}
case 'ON_REMOVE_ROLES_SUCCEEDED': {
draftState.shouldRefetchData = true;
draftState.roleToDelete = null;
break;
}
case 'RESET_DATA_TO_DELETE': {
draftState.shouldRefetchData = false;
draftState.selectedRoles = [];
draftState.roleToDelete = null;
draftState.showModalConfirmButtonLoading = false;
break;
}
case 'SET_ROLE_TO_DELETE': {
draftState.selectedRoles = [action.id];
draftState.roleToDelete = action.id;
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:
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', () => {
it('should set the showModalConfirmButtonLoading to true', () => {
const action = {
type: 'ON_REMOVE_ROLES',
};
const initialState = {
selectedRoles: [],
roleToDelete: 1,
shouldRefetchData: false,
showModalConfirmButtonLoading: false,
};
const expected = {
selectedRoles: [],
roleToDelete: 1,
shouldRefetchData: false,
showModalConfirmButtonLoading: true,
};
@ -73,12 +37,12 @@ describe('ADMIN | ee | CONTAINERS | ROLES | ListPage | reducer', () => {
type: 'ON_REMOVE_ROLES_SUCCEEDED',
};
const initialState = {
selectedRoles: [],
roleToDelete: 1,
shouldRefetchData: false,
showModalConfirmButtonLoading: true,
};
const expected = {
selectedRoles: [],
roleToDelete: null,
shouldRefetchData: 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', () => {
it('should set the selected roles property correctly', () => {
const action = {
@ -114,11 +58,11 @@ describe('ADMIN | ee | CONTAINERS | ROLES | ListPage | reducer', () => {
id: 6,
};
const initialState = {
selectedRoles: [1, 2, 4],
roleToDelete: null,
shouldRefetchData: false,
};
const expected = {
selectedRoles: [6],
roleToDelete: 6,
shouldRefetchData: false,
};

View File

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

View File

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