Merge pull request #10907 from strapi/ds-migration/roles-edit-input-block

[v4] Edit users-permissions roles' name and description
This commit is contained in:
cyril lopez 2021-09-14 15:24:42 +02:00 committed by GitHub
commit cad6e45ea5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 790 additions and 148 deletions

View File

@ -1,13 +1,17 @@
import { useCallback, useReducer, useEffect } from 'react';
import { request, useNotification } from '@strapi/helper-plugin';
import { useCallback, useReducer, useEffect, useRef } from 'react';
import { useNotification } from '@strapi/helper-plugin';
import reducer, { initialState } from './reducer';
import axiosIntance from '../../utils/axiosInstance';
import pluginId from '../../pluginId';
const useFetchRole = id => {
const [state, dispatch] = useReducer(reducer, initialState);
const toggleNotification = useNotification();
const isMounted = useRef(null);
useEffect(() => {
isMounted.current = true;
if (id) {
fetchRole(id);
} else {
@ -17,17 +21,23 @@ const useFetchRole = id => {
});
}
return () => (isMounted.current = false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
const fetchRole = async roleId => {
try {
const { role } = await request(`/${pluginId}/roles/${roleId}`, { method: 'GET' });
const {
data: { role },
} = await axiosIntance.get(`/${pluginId}/roles/${roleId}`);
dispatch({
type: 'GET_DATA_SUCCEEDED',
role,
});
// Prevent updating state on an unmounted component
if (isMounted.current) {
dispatch({
type: 'GET_DATA_SUCCEEDED',
role,
});
}
} catch (err) {
console.error(err);

View File

@ -1,20 +1,24 @@
import React, { useState, useRef } from 'react';
import { Main, HeaderLayout, Button } from '@strapi/parts';
import React, { useState } from 'react';
import { Main, Button, Stack, Box, GridItem, Grid, TextInput, Textarea } from '@strapi/parts';
import { ContentLayout, HeaderLayout } from '@strapi/parts/Layout';
import { H3 } from '@strapi/parts/Text';
import { CheckIcon } from '@strapi/icons';
import { Formik } from 'formik';
import { useIntl } from 'react-intl';
import { useRouteMatch } from 'react-router-dom';
import {
request,
useNotification,
useOverlayBlocker,
SettingsPageTitle,
LoadingIndicatorPage,
Form,
useNotification,
} from '@strapi/helper-plugin';
import getTrad from '../../../utils/getTrad';
import pluginId from '../../../pluginId';
import { usePlugins, useFetchRole } from '../../../hooks';
import schema from './utils/schema';
import axiosInstance from '../../../utils/axiosInstance';
const EditPage = () => {
const { formatMessage } = useIntl();
@ -24,43 +28,44 @@ const EditPage = () => {
const {
params: { id },
} = useRouteMatch(`/settings/${pluginId}/roles/:id`);
const { isLoading } = usePlugins();
const { role, onSubmitSucceeded } = useFetchRole(id);
const permissionsRef = useRef();
const { isLoading: isLoadingPlugins } = usePlugins();
const { role, onSubmitSucceeded, isLoading: isLoadingRole } = useFetchRole(id);
const handleCreateRoleSubmit = data => {
const handleCreateRoleSubmit = async data => {
// Set loading state
lockApp();
setIsSubmitting(true);
const permissions = permissionsRef.current.getPermissions();
Promise.resolve(
request(`/${pluginId}/roles/${id}`, {
method: 'PUT',
body: { ...data, ...permissions, users: [] },
})
)
.then(() => {
onSubmitSucceeded({ name: data.name, description: data.description });
permissionsRef.current.setFormAfterSubmit();
toggleNotification({
type: 'success',
message: { id: getTrad('Settings.roles.edited') },
});
})
.catch(err => {
console.error(err);
toggleNotification({
type: 'warning',
message: { id: 'notification.error' },
});
})
.finally(() => {
setIsSubmitting(false);
unlockApp();
try {
// Update role in Strapi
await axiosInstance.put(`/${pluginId}/roles/${id}`, { ...data, users: [] });
// Notify success
onSubmitSucceeded({ name: data.name, description: data.description });
toggleNotification({
type: 'success',
message: {
id: getTrad('Settings.roles.edited'),
defaultMessage: 'Role edited',
},
});
} catch (err) {
console.error(err);
toggleNotification({
type: 'warning',
message: {
id: 'notification.error',
defaultMessage: 'An error occurred',
},
});
}
// Unset loading state
setIsSubmitting(false);
unlockApp();
};
if (isLoadingRole) {
return <LoadingIndicatorPage />;
}
return (
<Main>
<SettingsPageTitle name="Roles" />
@ -70,15 +75,16 @@ const EditPage = () => {
onSubmit={handleCreateRoleSubmit}
validationSchema={schema}
>
{({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
{({ handleSubmit, values, handleChange, errors }) => (
<Form noValidate onSubmit={handleSubmit}>
<HeaderLayout
primaryAction={
!isLoading && (
!isLoadingPlugins && (
<Button
disabled={role.code === 'strapi-super-admin'}
onClick={handleSubmit}
type="submit"
loading={isSubmitting}
startIcon={<CheckIcon />}
>
{formatMessage({
id: 'app.components.Button.save',
@ -90,7 +96,66 @@ const EditPage = () => {
title={role.name}
subtitle={role.description}
/>
</form>
<ContentLayout>
<Stack size={7}>
<Box
background="neutral0"
hasRadius
shadow="filterShadow"
paddingTop={6}
paddingBottom={6}
paddingLeft={7}
paddingRight={7}
>
<Stack size={4}>
<H3 as="h2">
{formatMessage({
id: getTrad('EditPage.form.roles'),
defaultMessage: 'Role details',
})}
</H3>
<Grid gap={4}>
<GridItem col={6}>
<TextInput
name="name"
value={values.name || ''}
onChange={handleChange}
label={formatMessage({
id: 'Settings.roles.form.input.name',
defaultMessage: 'Name',
})}
error={
errors.name
? formatMessage({ id: errors.name, defaultMessage: 'Invalid value' })
: null
}
/>
</GridItem>
<GridItem col={6}>
<Textarea
name="description"
value={values.description || ''}
onChange={handleChange}
label={formatMessage({
id: 'Settings.roles.form.input.description',
defaultMessage: 'Description',
})}
error={
errors.description
? formatMessage({
id: errors.description,
defaultMessage: 'Invalid value',
})
: null
}
/>
</GridItem>
</Grid>
</Stack>
</Box>
</Stack>
</ContentLayout>
</Form>
)}
</Formik>
</Main>

View File

@ -1,5 +1,6 @@
import React from 'react';
import { render } from '@testing-library/react';
import { render, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider, lightTheme } from '@strapi/parts';
import { Router, Switch, Route } from 'react-router-dom';
import { IntlProvider } from 'react-intl';
@ -7,6 +8,7 @@ import { createMemoryHistory } from 'history';
import pluginId from '../../../../pluginId';
import RolesEditPage from '..';
import server from './server';
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
@ -19,23 +21,15 @@ jest.mock('../../../../hooks', () => {
return {
...originalModule,
useFetchRole: id => {
const role = {
id,
name: 'Authenticated',
description: 'Default role given to authenticated user.',
type: 'authenticated',
};
const onSubmitSucceed = jest.fn();
return { role, onSubmitSucceed };
},
usePlugins: () => ({
...originalModule.usePlugins,
isLoading: false,
}),
};
});
it('renders users-permissions edit role and matches snapshot', () => {
function makeAndRenderApp() {
const history = createMemoryHistory();
const app = (
<IntlProvider locale="en" messages={{ en: {} }} textComponent="span">
<ThemeProvider theme={lightTheme}>
@ -47,110 +41,640 @@ it('renders users-permissions edit role and matches snapshot', () => {
</ThemeProvider>
</IntlProvider>
);
const { container } = render(app);
const renderResult = render(app);
history.push(`/settings/${pluginId}/roles/1`);
expect(container.firstChild).toMatchInlineSnapshot(`
.c4 {
font-weight: 600;
font-size: 2rem;
line-height: 1.25;
color: #32324d;
}
return renderResult;
}
.c5 {
font-weight: 400;
font-size: 0.875rem;
line-height: 1.43;
color: #666687;
}
describe('Admin | containers | RoleEditPage', () => {
beforeAll(() => server.listen());
.c6 {
font-size: 1rem;
line-height: 1.5;
}
beforeEach(() => jest.clearAllMocks());
.c1 {
background: #f6f6f9;
padding-top: 56px;
padding-right: 56px;
padding-bottom: 56px;
padding-left: 56px;
}
afterEach(() => server.resetHandlers());
.c2 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
afterAll(() => server.close());
.c3 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
it('renders users-permissions edit role and matches snapshot', async () => {
const { container, getByTestId } = makeAndRenderApp();
await waitForElementToBeRemoved(() => getByTestId('loader'));
.c0 {
outline: none;
}
expect(container.firstChild).toMatchInlineSnapshot(`
.c10 {
font-weight: 500;
font-size: 0.75rem;
line-height: 1.33;
color: #32324d;
}
<main
aria-labelledby="main-content-title"
class="c0"
id="main-content"
tabindex="-1"
>
<form>
<div
class=""
.c8 {
padding-right: 8px;
}
.c15 {
background: #ffffff;
padding-top: 24px;
padding-right: 32px;
padding-bottom: 24px;
padding-left: 32px;
border-radius: 4px;
box-shadow: 0px 1px 4px rgba(33,33,52,0.1);
}
.c22 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c23 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
cursor: pointer;
padding: 8px;
border-radius: 4px;
background: #ffffff;
border: 1px solid #dcdce4;
}
.c5 svg {
height: 12px;
width: 12px;
}
.c5 svg > g,
.c5 svg path {
fill: #ffffff;
}
.c5[aria-disabled='true'] {
pointer-events: none;
}
.c6 {
padding: 8px 16px;
background: #4945ff;
border: none;
border: 1px solid #4945ff;
background: #4945ff;
}
.c6 .c7 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c6 .c9 {
color: #ffffff;
}
.c6[aria-disabled='true'] {
border: 1px solid #dcdce4;
background: #eaeaef;
}
.c6[aria-disabled='true'] .c9 {
color: #666687;
}
.c6[aria-disabled='true'] svg > g,
.c6[aria-disabled='true'] svg path {
fill: #666687;
}
.c6[aria-disabled='true']:active {
border: 1px solid #dcdce4;
background: #eaeaef;
}
.c6[aria-disabled='true']:active .c9 {
color: #666687;
}
.c6[aria-disabled='true']:active svg > g,
.c6[aria-disabled='true']:active svg path {
fill: #666687;
}
.c6:hover {
border: 1px solid #7b79ff;
background: #7b79ff;
}
.c6:active {
border: 1px solid #4945ff;
background: #4945ff;
}
.c14 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c14 > * {
margin-top: 0;
margin-bottom: 0;
}
.c14 > * + * {
margin-top: 32px;
}
.c16 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c16 > * {
margin-top: 0;
margin-bottom: 0;
}
.c16 > * + * {
margin-top: 16px;
}
.c21 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c21 > * {
margin-top: 0;
margin-bottom: 0;
}
.c21 > * + * {
margin-top: 4px;
}
.c25 {
border: none;
border-radius: 4px;
padding-left: 16px;
padding-right: 16px;
color: #32324d;
font-weight: 400;
font-size: 0.875rem;
display: block;
width: 100%;
height: 2.5rem;
}
.c25::-webkit-input-placeholder {
color: #8e8ea9;
opacity: 1;
}
.c25::-moz-placeholder {
color: #8e8ea9;
opacity: 1;
}
.c25:-ms-input-placeholder {
color: #8e8ea9;
opacity: 1;
}
.c25::placeholder {
color: #8e8ea9;
opacity: 1;
}
.c25:disabled {
background: inherit;
color: inherit;
}
.c24 {
border: 1px solid #dcdce4;
border-radius: 4px;
background: #ffffff;
}
.c20 textarea {
height: 5rem;
}
.c18 {
display: grid;
grid-template-columns: repeat(12,1fr);
gap: 16px;
}
.c19 {
grid-column: span 6;
word-break: break-all;
}
.c0 {
outline: none;
}
.c27 {
display: block;
width: 100%;
border: 1px solid #dcdce4;
border-radius: 4px;
padding-left: 16px;
padding-right: 16px;
padding-top: 12px;
padding-bottom: 12px;
font-weight: 400;
font-size: 0.875rem;
color: #32324d;
background: #ffffff;
}
.c27::-webkit-input-placeholder {
color: #8e8ea9;
opacity: 1;
}
.c27::-moz-placeholder {
color: #8e8ea9;
opacity: 1;
}
.c27:-ms-input-placeholder {
color: #8e8ea9;
opacity: 1;
}
.c27::placeholder {
color: #8e8ea9;
opacity: 1;
}
.c26 textarea {
height: 5rem;
line-height: 1.25rem;
font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans', 'Helvetica Neue',sans-serif;
}
.c26 textarea::-webkit-input-placeholder {
font-weight: 400;
font-size: 0.875rem;
line-height: 1.43;
color: #8e8ea9;
opacity: 1;
}
.c26 textarea::-moz-placeholder {
font-weight: 400;
font-size: 0.875rem;
line-height: 1.43;
color: #8e8ea9;
opacity: 1;
}
.c26 textarea:-ms-input-placeholder {
font-weight: 400;
font-size: 0.875rem;
line-height: 1.43;
color: #8e8ea9;
opacity: 1;
}
.c26 textarea::placeholder {
font-weight: 400;
font-size: 0.875rem;
line-height: 1.43;
color: #8e8ea9;
opacity: 1;
}
.c1 {
background: #f6f6f9;
padding-top: 56px;
padding-right: 56px;
padding-bottom: 56px;
padding-left: 56px;
}
.c13 {
padding-right: 56px;
padding-left: 56px;
}
.c2 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c4 {
font-weight: 600;
font-size: 2rem;
line-height: 1.25;
color: #32324d;
}
.c11 {
font-weight: 400;
font-size: 0.875rem;
line-height: 1.43;
color: #666687;
}
.c12 {
font-size: 1rem;
line-height: 1.5;
}
.c17 {
font-weight: 500;
font-size: 1rem;
line-height: 1.25;
color: #32324d;
}
@media (max-width:68.75rem) {
.c19 {
grid-column: span;
}
}
@media (max-width:34.375rem) {
.c19 {
grid-column: span;
}
}
<main
aria-labelledby="main-content-title"
class="c0"
id="main-content"
tabindex="-1"
>
<form
action="#"
novalidate=""
>
<div
class="c1"
data-strapi-header="true"
class=""
style="height: 0px;"
>
<div
class="c2"
class="c1"
data-strapi-header="true"
>
<div
class="c3"
class="c2"
>
<h1
class="c4"
id="main-content-title"
<div
class="c3"
>
Authenticated
</h1>
<h1
class="c4"
id="main-content-title"
>
Authenticated
</h1>
</div>
<button
aria-disabled="false"
class="c5 c6"
type="submit"
>
<div
aria-hidden="true"
class="c7 c8"
>
<svg
fill="none"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20.727 2.97a.2.2 0 01.286 0l2.85 2.89a.2.2 0 010 .28L9.554 20.854a.2.2 0 01-.285 0l-9.13-9.243a.2.2 0 010-.281l2.85-2.892a.2.2 0 01.284 0l6.14 6.209L20.726 2.97z"
fill="#212134"
/>
</svg>
</div>
<span
class="c9 c10"
>
Save
</span>
</button>
</div>
<p
class="c11 c12"
>
Default role given to authenticated user.
</p>
</div>
</div>
<div
class="c13"
>
<div
class="c14"
>
<div
class="c7 c15"
>
<div
class="c16"
>
<h2
class="c17"
>
Role details
</h2>
<div
class="c7 c18"
>
<div
class="c19"
>
<div
class="c7 "
>
<div
class="c20"
>
<div>
<div
class="c21"
>
<div
class="c7 c22"
>
<label
class="c9 c10"
for="textinput-1"
>
Name
</label>
</div>
<div
class="c7 c23 c24"
>
<input
aria-invalid="false"
class="c25"
id="textinput-1"
name="name"
value="Authenticated"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="c19"
>
<div
class="c7 "
>
<div
class="c26"
>
<div>
<div
class="c21"
>
<div
class="c7 c22"
>
<label
class="c9 c10"
for="textarea-2"
>
Description
</label>
</div>
<textarea
aria-invalid="false"
class="c27"
id="textarea-2"
name="description"
>
Default role given to authenticated user.
</textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<p
class="c5 c6"
>
Default role given to authenticated user.
</p>
</div>
</div>
</form>
</main>
`);
</form>
</main>
`);
});
it("can edit a users-permissions role's name and description", async () => {
const { getByLabelText, getByRole, getByTestId, getAllByText } = makeAndRenderApp();
// Check loading screen
const loader = getByTestId('loader');
expect(loader).toBeInTheDocument();
// After loading, check other elements
await waitForElementToBeRemoved(loader).catch(e => console.error(e));
const saveButton = getByRole('button', { name: /save/i });
expect(saveButton).toBeInTheDocument();
const nameField = getByLabelText(/name/i);
expect(nameField).toBeInTheDocument();
const descriptionField = getByLabelText(/description/i);
expect(descriptionField).toBeInTheDocument();
// Shows error when name is missing
await userEvent.clear(nameField);
expect(nameField).toHaveValue('');
await userEvent.clear(descriptionField);
expect(descriptionField).toHaveValue('');
// Show errors after form submit
await userEvent.click(saveButton);
await waitFor(() => expect(saveButton).not.toBeDisabled());
const errorMessages = await getAllByText(/invalid value/i);
errorMessages.forEach(errorMessage => expect(errorMessage).toBeInTheDocument());
});
});

View File

@ -0,0 +1,43 @@
import { setupServer } from 'msw/node';
import { rest } from 'msw';
const handlers = [
// Mock get role route
rest.get('*/users-permissions/roles/:roleId', (req, res, ctx) => {
return res(
ctx.delay(500),
ctx.status(200),
ctx.json({
role: {
id: req.params.roleId,
name: 'Authenticated',
description: 'Default role given to authenticated user.',
type: 'authenticated',
created_at: '2021-09-08T16:26:18.061Z',
updated_at: '2021-09-08T16:26:18.061Z',
permissions: {
application: {
controllers: {
address: {
create: {
enabled: false,
policy: '',
},
},
},
},
},
},
})
);
}),
// Mock edit role route
rest.put('*/users-permissions/roles/:roleId', (req, res, ctx) => {
return res(ctx.delay(500), ctx.status(200), ctx.json({ ok: true }));
}),
];
const server = setupServer(...handlers);
export default server;