Performance cleanup; check main box when all children are checked; closes #2379

This commit is contained in:
Dane Everitt 2020-09-22 21:41:35 -07:00
parent 2182a15494
commit cbedd4539c
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
2 changed files with 107 additions and 76 deletions

View file

@ -1,7 +1,8 @@
import React from 'react'; import React, { memo } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { IconProp } from '@fortawesome/fontawesome-svg-core';
import tw from 'twin.macro'; import tw from 'twin.macro';
import isEqual from 'react-fast-compare';
interface Props { interface Props {
icon?: IconProp; icon?: IconProp;
@ -27,4 +28,4 @@ const TitledGreyBox = ({ icon, title, children, className }: Props) => (
</div> </div>
); );
export default TitledGreyBox; export default memo(TitledGreyBox, isEqual);

View file

@ -1,4 +1,4 @@
import React, { forwardRef, useEffect, useRef } from 'react'; import React, { forwardRef, memo, useCallback, useEffect, useRef } from 'react';
import { Subuser } from '@/state/server/subusers'; import { Subuser } from '@/state/server/subusers';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import { array, object, string } from 'yup'; import { array, object, string } from 'yup';
@ -11,7 +11,6 @@ import Checkbox from '@/components/elements/Checkbox';
import styled from 'styled-components/macro'; import styled from 'styled-components/macro';
import createOrUpdateSubuser from '@/api/server/users/createOrUpdateSubuser'; import createOrUpdateSubuser from '@/api/server/users/createOrUpdateSubuser';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import { httpErrorToHuman } from '@/api/http';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import Can from '@/components/elements/Can'; import Can from '@/components/elements/Can';
import { usePermissions } from '@/plugins/usePermissions'; import { usePermissions } from '@/plugins/usePermissions';
@ -20,6 +19,7 @@ import tw from 'twin.macro';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import Label from '@/components/elements/Label'; import Label from '@/components/elements/Label';
import Input from '@/components/elements/Input'; import Input from '@/components/elements/Input';
import isEqual from 'react-fast-compare';
type Props = { type Props = {
subuser?: Subuser; subuser?: Subuser;
@ -31,7 +31,7 @@ interface Values {
} }
const PermissionLabel = styled.label` const PermissionLabel = styled.label`
${tw`flex items-center border border-transparent rounded md:p-2`}; ${tw`flex items-center border border-transparent rounded md:p-2 transition-colors duration-75`};
text-transform: none; text-transform: none;
&:not(.disabled) { &:not(.disabled) {
@ -41,6 +41,10 @@ const PermissionLabel = styled.label`
${tw`border-neutral-500 bg-neutral-800`}; ${tw`border-neutral-500 bg-neutral-800`};
} }
} }
&:not(:first-of-type) {
${tw`mt-4 sm:mt-2`};
}
&.disabled { &.disabled {
${tw`opacity-50`}; ${tw`opacity-50`};
@ -51,8 +55,58 @@ const PermissionLabel = styled.label`
} }
`; `;
interface TitleProps {
isEditable: boolean;
permission: string;
permissions: string[];
children: React.ReactNode;
className?: string;
}
const PermissionTitledBox = memo(({ isEditable, permission, permissions, className, children }: TitleProps) => {
const { values, setFieldValue } = useFormikContext<Values>();
const onCheckboxClicked = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.currentTarget.checked, [
...values.permissions,
...permissions.filter(p => !values.permissions.includes(p)),
]);
if (e.currentTarget.checked) {
setFieldValue('permissions', [
...values.permissions,
...permissions.filter(p => !values.permissions.includes(p)),
]);
} else {
setFieldValue('permissions', [
...values.permissions.filter(p => !permissions.includes(p)),
]);
}
}, [ permissions, values.permissions ]);
return (
<TitledGreyBox
title={
<div css={tw`flex items-center`}>
<p css={tw`text-sm uppercase flex-1`}>{permission}</p>
{isEditable &&
<Input
type={'checkbox'}
checked={permissions.every(p => values.permissions.includes(p))}
onChange={onCheckboxClicked}
/>
}
</div>
}
className={className}
>
{children}
</TitledGreyBox>
);
}, isEqual);
const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...props }, ref) => { const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...props }, ref) => {
const { values, isSubmitting, setFieldValue } = useFormikContext<Values>(); const { isSubmitting } = useFormikContext<Values>();
const [ canEditUser ] = usePermissions(subuser ? [ 'user.update' ] : [ 'user.create' ]); const [ canEditUser ] = usePermissions(subuser ? [ 'user.update' ] : [ 'user.create' ]);
const permissions = useStoreState(state => state.permissions.data); const permissions = useStoreState(state => state.permissions.data);
@ -104,73 +158,48 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
</div> </div>
} }
<div css={tw`my-6`}> <div css={tw`my-6`}>
{Object.keys(permissions).filter(key => key !== 'websocket').map((key, index) => ( {Object.keys(permissions).filter(key => key !== 'websocket').map((key, index) => {
<TitledGreyBox const group = Object.keys(permissions[key].keys).map(pkey => `${key}.${pkey}`);
key={key}
title={ return (
<div css={tw`flex items-center`}> <PermissionTitledBox
<p css={tw`text-sm uppercase flex-1`}>{key}</p> key={`permission_${key}`}
{canEditUser && isEditable={canEditUser}
<Input permission={key}
type={'checkbox'} permissions={group}
onClick={e => { css={index > 0 ? tw`mt-4` : undefined}
if (e.currentTarget.checked) { >
setFieldValue('permissions', [ <p css={tw`text-sm text-neutral-400 mb-4`}>
...values.permissions, {permissions[key].description}
...Object.keys(permissions[key].keys) </p>
.map(pkey => `${key}.${pkey}`) {Object.keys(permissions[key].keys).map(pkey => (
.filter(permission => values.permissions.indexOf(permission) === -1), <PermissionLabel
]); key={`permission_${key}_${pkey}`}
} else { htmlFor={`permission_${key}_${pkey}`}
setFieldValue('permissions', [ className={(!canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0) ? 'disabled' : undefined}
...values.permissions.filter( >
permission => Object.keys(permissions[key].keys) <div css={tw`p-2`}>
.map(pkey => `${key}.${pkey}`) <Checkbox
.indexOf(permission) < 0, id={`permission_${key}_${pkey}`}
), name={'permissions'}
]); value={`${key}.${pkey}`}
css={tw`w-5 h-5 mr-2`}
disabled={!canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0}
/>
</div>
<div css={tw`flex-1`}>
<Label as={'p'} css={tw`font-medium`}>{pkey}</Label>
{permissions[key].keys[pkey].length > 0 &&
<p css={tw`text-xs text-neutral-400 mt-1`}>
{permissions[key].keys[pkey]}
</p>
} }
}} </div>
/> </PermissionLabel>
} ))}
</div> </PermissionTitledBox>
} );
css={index > 0 ? tw`mt-4` : undefined} })}
>
<p css={tw`text-sm text-neutral-400 mb-4`}>
{permissions[key].description}
</p>
{Object.keys(permissions[key].keys).map((pkey, index) => (
<PermissionLabel
key={`permission_${key}_${pkey}`}
htmlFor={`permission_${key}_${pkey}`}
css={[
tw`transition-colors duration-75`,
index > 0 ? tw`mt-4 sm:mt-2` : undefined,
]}
className={(!canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0) ? 'disabled' : undefined}
>
<div css={tw`p-2`}>
<Checkbox
id={`permission_${key}_${pkey}`}
name={'permissions'}
value={`${key}.${pkey}`}
css={tw`w-5 h-5 mr-2`}
disabled={!canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0}
/>
</div>
<div css={tw`flex-1`}>
<Label css={tw`font-medium`}>{pkey}</Label>
{permissions[key].keys[pkey].length > 0 &&
<p css={tw`text-xs text-neutral-400 mt-1`}>
{permissions[key].keys[pkey]}
</p>
}
</div>
</PermissionLabel>
))}
</TitledGreyBox>
))}
</div> </div>
<Can action={subuser ? 'user.update' : 'user.create'}> <Can action={subuser ? 'user.update' : 'user.create'}>
<div css={tw`pb-6 flex justify-end`}> <div css={tw`pb-6 flex justify-end`}>
@ -187,8 +216,7 @@ export default ({ subuser, ...props }: Props) => {
const ref = useRef<HTMLHeadingElement>(null); const ref = useRef<HTMLHeadingElement>(null);
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const appendSubuser = ServerContext.useStoreActions(actions => actions.subusers.appendSubuser); const appendSubuser = ServerContext.useStoreActions(actions => actions.subusers.appendSubuser);
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => { const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('user:edit'); clearFlashes('user:edit');
@ -200,7 +228,7 @@ export default ({ subuser, ...props }: Props) => {
.catch(error => { .catch(error => {
console.error(error); console.error(error);
setSubmitting(false); setSubmitting(false);
addError({ key: 'user:edit', message: httpErrorToHuman(error) }); clearAndAddHttpError({ key: 'user:edit', error });
if (ref.current) { if (ref.current) {
ref.current.scrollIntoView(); ref.current.scrollIntoView();
@ -209,7 +237,9 @@ export default ({ subuser, ...props }: Props) => {
}; };
useEffect(() => { useEffect(() => {
clearFlashes('user:edit'); return () => {
clearFlashes('user:edit');
};
}, []); }, []);
return ( return (