feat: optimize style of role permissions form.

This commit is contained in:
a.bouhuolia
2021-12-22 19:17:41 +02:00
parent 10c421dea0
commit 934b0abbe6
14 changed files with 913 additions and 181 deletions

View File

@@ -0,0 +1,50 @@
import React from 'react';
import { useFormikContext } from 'formik';
import styled from 'styled-components';
import { Intent, Button } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'components';
/**
* Role form floating actions.
* @returns {React.JSX}
*/
export function RoleFormFloatingActions() {
const { isSubmitting } = useFormikContext();
const history = useHistory();
const handleCloseClick = () => {
history.go(-1);
};
return (
<RoleFormFloatingActionsRoot>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
type="submit"
style={{ minWidth: '90px' }}
>
<T id={'save'} />
</Button>
<Button onClick={handleCloseClick} disabled={isSubmitting}>
<T id={'cancel'} />
</Button>
</RoleFormFloatingActionsRoot>
);
}
const RoleFormFloatingActionsRoot = styled.div`
position: fixed;
bottom: 0;
width: 100%;
background: #fff;
padding: 14px 18px;
border-top: 1px solid #d2dde2;
box-shadow: 0px -1px 4px 0px rgb(0 0 0 / 5%);
.bp3-button {
margin-right: 10px;
}
`;

View File

@@ -1,37 +1,27 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { ErrorMessage, FastField, Form, useFormikContext } from 'formik';
import {
Button,
FormGroup,
InputGroup,
Intent,
TextArea,
} from '@blueprintjs/core';
import { ErrorMessage, FastField, Form } from 'formik';
import { FormGroup, InputGroup, TextArea } from '@blueprintjs/core';
import { inputIntent } from 'utils';
import { FormattedMessage as T, FieldRequiredHint } from 'components';
import { FormattedMessage as T, FieldRequiredHint, Card } from 'components';
import { useAutofocus } from 'hooks';
import { RolesPermissionList } from './components';
import { RoleFormFloatingActions } from './RoleFormFloatingActions';
/**
* Preferences - Roles Form content.
* Role form header.
* @returns {React.JSX}
*/
export default function RolesFormContent() {
const history = useHistory();
const { isSubmitting, values } = useFormikContext();
function RoleFormHeader() {
const roleNameFieldRef = useAutofocus();
const handleCloseClick = () => {
history.go(-1);
};
return (
<Form>
<Card>
{/* ---------- name ---------- */}
<FastField name={'role_name'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'name'} />}
label={<strong><T id={'role_name'} /></strong>}
labelInfo={<FieldRequiredHint />}
className={'form-group--name'}
intent={inputIntent({ error, touched })}
@@ -57,20 +47,28 @@ export default function RolesFormContent() {
helperText={<ErrorMessage name={'role_description'} />}
inline={true}
>
<TextArea growVertically={true} height={280} {...field} />
<TextArea
growVertically={true}
height={280}
{...field}
placeholder="Max. 500 characters"
/>
</FormGroup>
)}
</FastField>
<RolesPermissionList />
</Card>
);
}
<div className={'card__footer'}>
<Button intent={Intent.PRIMARY} loading={isSubmitting} type="submit">
<T id={'save'} />
</Button>
<Button onClick={handleCloseClick} disabled={isSubmitting}>
<T id={'cancel'} />
</Button>
</div>
/**
* Preferences - Roles Form content.
*/
export default function RolesFormContent() {
return (
<Form>
<RoleFormHeader />
<RolesPermissionList />
<RoleFormFloatingActions />
</Form>
);
}

View File

@@ -1,7 +1,6 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import _, { isArray } from 'lodash';
import {
useCreateRolePermissionSchema,
@@ -10,7 +9,6 @@ import {
useRolePermission,
} from 'hooks/query';
import PreferencesPageLoader from '../../../PreferencesPageLoader';
import { transformToObject } from './utils';
const RolesFormContext = React.createContext();
@@ -32,10 +30,12 @@ function RolesFormProvider({ roleId, ...props }) {
isFetching: isPermissionsSchemaFetching,
} = usePermissionsSchema();
const { data: role, isLoading: isPermissionLoading } =
useRolePermission(roleId, {
const { data: role, isLoading: isPermissionLoading } = useRolePermission(
roleId,
{
enabled: !!roleId,
});
},
);
// Detarmines whether the new or edit mode.
const isNewMode = !roleId;
@@ -59,13 +59,11 @@ function RolesFormProvider({ roleId, ...props }) {
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_ROLES_FORM,
)}
>
<div className={classNames(CLASSES.CARD)}>
{isPermissionsSchemaLoading || isPermissionLoading ? (
<PreferencesPageLoader />
) : (
<RolesFormContext.Provider value={provider} {...props} />
)}
</div>
{isPermissionsSchemaLoading || isPermissionLoading ? (
<PreferencesPageLoader />
) : (
<RolesFormContext.Provider value={provider} {...props} />
)}
</div>
);
}

View File

@@ -1,148 +1,496 @@
import React from 'react';
import { Checkbox } from '@blueprintjs/core';
import { Checkbox, Switch, Popover } from '@blueprintjs/core';
import styled from 'styled-components';
import { castArray } from 'lodash';
import { FastField } from 'formik';
import { FastField, useFormikContext } from 'formik';
import { useRolesFormContext } from './RolesFormProvider';
import { permissions, ModulePermissionsStyle } from 'common/permissionsSchema';
import { Card, If, ButtonLink, Choose } from 'components';
import {
getSerivceColumnPermission,
getServiceExtraPermissions,
} from './utils';
const RoleLabelCheckbox = ({ subject, label, description }) => (
<>
<LabelCheckbox>
{/*------------- subject checbox ------------- */}
<FastField name={subject} type="checkbox">
{({ form: { setFieldValue, values }, field }) => (
<Checkbox
className={'block'}
inline={true}
label={label}
name={subject}
/>
)}
</FastField>
<p>{description}</p>
</LabelCheckbox>
</>
);
// Module permissions context.
const ModulePermissionsContext = React.createContext();
const ModuleServiceContext = React.createContext();
const AbilitiesList = ({ subject, abilities }) => {
/**
* Retrieves the module permissions provider.
* @returns {React.JSX}
*/
const useModulePermissionsProvider = () =>
React.useContext(ModulePermissionsContext);
/**
* Module permissions service context provider.
*/
const useModulePermissionsServiceProvider = () =>
React.useContext(ModuleServiceContext);
/**
* Module permissions context state provider.
* @returns {React.JSX}
*/
function ModulePermissionsProvider({ module, children }) {
return (
<AbilitieList>
{abilities?.map(({ key, label }) => (
<FastField name={`permissions.${subject}/${key}`} type="checkbox">
<ModulePermissionsContext.Provider value={{ module }}>
{children}
</ModulePermissionsContext.Provider>
);
}
/**
* Module permissions service context state provider.
* @returns {React.JSX}
*/
function ModulePermissionsServiceProvider({ service, children }) {
return (
<ModuleServiceContext.Provider value={{ service }}>
{children}
</ModuleServiceContext.Provider>
);
}
/**
* Permissions body columns.
* @returns {React.JSX}
*/
function PermissionBodyColumn({ column }) {
// Module permssions service context.
const { service } = useModulePermissionsServiceProvider();
// Retrieve the related permission of the given column key.
const permission = getSerivceColumnPermission(service, column.key);
// Display empty cell if the current column key has no related permissions.
if (!permission) {
return <td class={'permission-checkbox'}></td>;
}
return (
<td class={'permission-checkbox'}>
<FastField
name={`permissions.${service.subject}/${permission.key}`}
type="checkbox"
>
{({ field }) => <PermissionCheckbox inline={true} {...field} />}
</FastField>
</td>
);
}
/**
*
* @returns {React.JSX}
*/
function ModulePermissionsTableColumns({ columns }) {
return columns.map((column) => <PermissionBodyColumn column={column} />);
}
/**
* Module columns permissions extra permissions popover.
* @returns {React.JSX}
*/
function ModuleExtraPermissionsPopover() {
const { service } = useModulePermissionsServiceProvider();
// Retrieve the extra permissions of the given service.
const extraPermissions = getServiceExtraPermissions(service);
return (
<Popover>
<MorePermissionsLink>More Permissions</MorePermissionsLink>
<ExtraPermissionsRoot>
{extraPermissions.map((permission) => (
<FastField
name={`permissions.${service.subject}/${permission.key}`}
type="checkbox"
>
{({ form: { setFieldValue, values }, field }) => (
<PermissionCheckbox
inline={true}
label={permission.label}
{...field}
/>
)}
</FastField>
))}
</ExtraPermissionsRoot>
</Popover>
);
}
/**
* Module permissions extra permissions.
* @returns {React.JSX}
*/
function ModulePermissionExtraPermissions() {
const { service } = useModulePermissionsServiceProvider();
// Retrieve the extra permissions of the given service.
const extraPermissions = getServiceExtraPermissions(service);
return (
<td>
<If condition={extraPermissions.length > 0}>
<ModuleExtraPermissionsPopover />
</If>
</td>
);
}
/**
* Module permissions table head.
* @returns {React.JSX}
*/
function ModulePermissionsTableHead() {
const {
module: { serviceFullAccess, columns },
} = useModulePermissionsProvider();
return (
<thead>
<tr>
<th></th>
<If condition={serviceFullAccess}>
<th class={'full'}>Full Access</th>
</If>
{columns.map((column) => (
<th class={'permission'}>{column.label}</th>
))}
<th></th>
</tr>
</thead>
);
}
/**
* Module permissions service full access.
* @returns {React.JSX}
*/
function ModulePermissionsServiceFullAccess() {
// Module permissions provider.
const { module } = useModulePermissionsProvider();
// Module service provider.
const { service } = useModulePermissionsServiceProvider();
return (
<If condition={module.serviceFullAccess}>
<td class="full-access-permission">
<FastField
name={`serviceFullAccess.${service.subject}`}
type="checkbox"
>
{({ form: { setFieldValue, values }, field }) => (
<Checkbox inline={true} label={label} {...field} />
<PermissionCheckbox inline={true} />
)}
</FastField>
))}
</AbilitieList>
</td>
</If>
);
};
}
const ExtraAbilitiesList = ({ subject, extraAbilities }) => {
return extraAbilities?.map(({ key, label }) => (
<AbilitieList>
<FastField name={`permissions.${subject}/${key}`} type="checkbox">
{({ form: { setFieldValue, values }, field }) => (
<Checkbox inline={true} label={label} {...field} />
)}
</FastField>
</AbilitieList>
));
};
export const RolesPermissionList = () => {
const { permissionsSchema } = useRolesFormContext();
/**
* Module permissions table body.
* @returns {React.JSX}
*/
function ModulePermissionsTableBody() {
const {
module: { services, columns },
} = useModulePermissionsProvider();
return (
<GroupList>
<BoxedGroupList>
{permissionsSchema.map(
({
subject,
subject_label,
description,
abilities,
extra_abilities,
}) => {
const extraAbilitiesList = Array.isArray(extra_abilities)
? extra_abilities
: [];
<tbody>
{services.map((service) => (
<ModulePermissionsServiceProvider service={service}>
<tr>
<td className="service-label">{service.label} </td>
const abilitiesList = castArray(abilities) ? abilities : [];
<ModulePermissionsServiceFullAccess />
<ModulePermissionsTableColumns columns={columns} />
<ModulePermissionExtraPermissions />
</tr>
</ModulePermissionsServiceProvider>
))}
</tbody>
);
}
return (
<React.Fragment>
<RoleList>
<RoleLabelCheckbox
subject={subject}
label={subject_label}
description={description}
/>
/**
* Module permissions table.
* @returns {React.JSX}
*/
function ModulePermissionsTable() {
return (
<ModulePermissionsTableRoot>
<ModulePermissionsTableHead />
<ModulePermissionsTableBody />
</ModulePermissionsTableRoot>
);
}
<AbilitiesList subject={subject} abilities={abilitiesList} />
<ExtraAbilitiesList
subject={subject}
extraAbilities={extraAbilitiesList}
/>
</RoleList>
</React.Fragment>
);
},
)}
</BoxedGroupList>
</GroupList>
/**
* Module vertical table cells.
* @returns {React.JSX}
*/
function ModuleVerticalTableCells() {
const { service } = useModulePermissionsServiceProvider();
return (
<td class={'permissions'}>
{service.permissions.map((permission) => (
<div>
<FastField
name={`permissions.${service.subject}/${permission.key}`}
type="checkbox"
>
{({ form: { setFieldValue, values }, field }) => (
<PermissionCheckbox
inline={true}
label={permission.label}
{...field}
/>
)}
</FastField>
</div>
))}
</td>
);
}
/**
* Module permissions vertical services.
* @returns {React.JSX}
*/
function ModulePermissionsVerticalServices() {
const { module } = useModulePermissionsProvider();
return (
<ModulePermissionsVerticalServicesRoot>
<ModulePermissionsVerticalTable>
<tbody>
{module.services.map((service) => (
<ModulePermissionsServiceProvider service={service}>
<tr>
<td class={'service-label'}>{service.label} </td>
<ModuleVerticalTableCells />
</tr>
</ModulePermissionsServiceProvider>
))}
</tbody>
</ModulePermissionsVerticalTable>
</ModulePermissionsVerticalServicesRoot>
);
}
/**
* Module permissions body.
* @returns {React.JSX}
*/
function ModulePermissionsBody() {
const { module } = useModulePermissionsProvider();
return (
<ModulePermissionBodyRoot>
<Choose>
<Choose.When
condition={module.type === ModulePermissionsStyle.Vertical}
>
<ModulePermissionsVerticalServices />
</Choose.When>
<Choose.When condition={module.type === ModulePermissionsStyle.Columns}>
<ModulePermissionsTable />
</Choose.When>
</Choose>
</ModulePermissionBodyRoot>
);
}
/**
* Permissions module.
* @returns {React.JSX}
*/
function ModulePermissions({ module }) {
return (
<ModulePermissionsRoot>
<ModulePermissionsProvider module={module}>
<ModulePermissionHead>
<ModulePermissionTitle>{module.label} </ModulePermissionTitle>
<If condition={module.moduleFullAccess}>
<ModulePermissionFullControlRoot>
<Switch />
</ModulePermissionFullControlRoot>
</If>
</ModulePermissionHead>
<ModulePermissionsBody />
</ModulePermissionsProvider>
</ModulePermissionsRoot>
);
}
/**
* Permissions modules list.
* @return {React.JSX}
*/
export const RolesPermissionList = () => {
return (
<ModulesPermission>
{permissions.map((module) => (
<ModulePermissions module={module} />
))}
</ModulesPermission>
);
};
const GroupList = styled.div`
list-style: none;
border: 1px solid #d2dce2;
border-radius: 6px;
font-size: 13px;
const PermissionCheckbox = styled(Checkbox)`
&.bp3-control.bp3-checkbox .bp3-control-indicator {
border-radius: 2px;
border-color: #555;
ul:first-child > li:last-child {
border-bottom: 0;
border-top: 0;
&,
&:before {
width: 15px;
height: 15px;
}
}
`;
const BoxedGroupList = styled.ul`
margin: 0;
list-style: none;
const ModulesPermission = styled.div``;
const ModulePermissionsRoot = styled(Card)`
padding: 0 !important;
`;
const RoleList = styled.li`
display: block;
padding: 5px 10px;
margin: 0;
line-height: 20px;
border-bottom: 1px solid #e0e0e0;
const ModulePermissionHead = styled.div`
border-bottom: 1px solid #d9d9d9;
height: 38px;
padding: 0 15px;
display: flex;
`;
const LabelCheckbox = styled.label`
> * {
display: inline-block;
}
.block {
width: 220px;
padding: 2px 0;
font-weight: 500;
const ModulePermissionTitle = styled.div`
font-weight: 500;
font-size: 16px;
line-height: 38px;
color: #878787;
`;
const ModulePermissionFullControlRoot = styled.div`
margin-left: auto;
display: flex;
.bp3-switch {
margin: auto;
}
`;
const AbilitieList = styled.ul`
list-style: none;
/* margin-left: 12px; // 10px */
margin: 0px 10px 0px;
const ModulePermissionBodyRoot = styled.div``;
> li {
display: inline-block;
margin-top: 3px;
const ModulePermissionsTableRoot = styled.table`
border-spacing: 0;
thead {
tr th {
font-weight: 400;
vertical-align: top;
&.full,
&.permission {
min-width: 70px;
}
&.full {
background-color: #fcfcfc;
}
}
}
thead,
tbody {
tr td,
tr th {
border-bottom: 1px solid #eee;
border-left: 1px solid #eee;
padding: 10px;
&:first-of-type {
border-left: 0;
}
}
tr:last-of-type td {
border-bottom: 0;
}
tr td:last-of-type,
tr th:last-of-type {
width: 100%;
}
}
tbody {
tr td.service-label {
min-width: 250px;
}
tr td {
.bp3-control.bp3-inline {
margin: 0;
}
&.full-access-permission {
background-color: #fcfcfc;
}
&.full-access-permission,
&.permission-checkbox {
text-align: center;
}
}
}
`;
const AbilitiesChildList = styled.li`
display: inline-block;
margin-top: 3px;
const MorePermissionsLink = styled(ButtonLink)`
font-size: 12px;
`;
const ExtraPermissionsRoot = styled.div`
display: flex;
flex-direction: column;
padding: 15px;
`;
const ModulePermissionsVerticalServicesRoot = styled.div``;
const ModulePermissionsVerticalTable = styled.table`
border-spacing: 0;
tbody {
tr td {
padding: 10px;
vertical-align: top;
border-left: 1px solid #eee;
border-bottom: 1px solid #eee;
&.service-label {
min-width: 250px;
color: #333;
}
&:first-of-type {
border-left: 0;
}
&.permissions {
width: 100%;
}
}
tr:last-of-type td {
border-bottom: 0;
}
}
`;

View File

@@ -51,3 +51,15 @@ export const getNewRoleInitialValues = (schema) => {
),
};
};
export function getSerivceColumnPermission(service, columnKey) {
return service.permissions.find((permission) => {
return permission.relatedColumn === columnKey;
});
}
export function getServiceExtraPermissions(service) {
return service.permissions.filter((permission) => {
return !permission.relatedColumn;
});
}