Skip to content

Commit

Permalink
624 permissions enhancements 2 (#640)
Browse files Browse the repository at this point in the history
* Remove translation for essential permission

* Add iconClassName prop to HelpTooltip

* Add tooltips to PermissionsView

* Fix spelling mistake

* Adjust UsersView title

* Adjust translation of comment pinning

* Remove unused function

* Add warning when editing permissions of the public user

* Add tip to "invalid forgot-password link" error message

* Show password requirements in change/reset password views
  • Loading branch information
MariusDoe authored Aug 9, 2024
1 parent e628203 commit bdd70dd
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 84 deletions.
20 changes: 15 additions & 5 deletions projects/bp-gallery/src/components/common/HelpTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import { ReactNode } from 'react';
import { HelpOutline } from '@mui/icons-material';
import { IconButton, Tooltip, Typography } from '@mui/material';
import { styled } from '@mui/material/styles';
import { tooltipClasses } from '@mui/material/Tooltip';
import { styled } from '@mui/material/styles';
import { ReactNode } from 'react';

export const HelpTooltip = styled(
({ title, content, className }: { title: ReactNode; content: ReactNode; className?: string }) => (
({
title,
content,
popupClassName,
iconClassName,
}: {
title: ReactNode;
content: ReactNode;
popupClassName?: string;
iconClassName?: string;
}) => (
<Tooltip
title={
<>
<Typography color='inherit'>{title}</Typography>
<p>{content}</p>
</>
}
classes={{ popper: className }}
classes={{ popper: popupClassName }}
>
<IconButton className={'info-icon'}>
<IconButton className={`info-icon ${iconClassName ?? ''}`}>
<HelpOutline />
</IconButton>
</Tooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const ChangePasswordView = () => {
<ProtectedRoute canUse={canChangePassword} canUseLoading={canChangePasswordLoading}>
<CenteredContainer title={t('admin.changePassword.title')}>
<Stack gap={4}>
<p>{t('admin.passwordRequirements')}</p>
<PasswordInput
label={t('admin.changePassword.currentPassword')}
value={currentPassword}
Expand Down
168 changes: 114 additions & 54 deletions projects/bp-gallery/src/components/views/admin/user/PermissionsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
Typography,
} from '@mui/material';
import { Operation, Parameter } from 'bp-graphql/build';
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
import { ChangeEventHandler, useCallback, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Redirect } from 'react-router-dom';
import {
Expand All @@ -27,6 +27,7 @@ import {
FlatParameterizedPermission,
FlatUsersPermissionsUser,
} from '../../../../types/additionalFlatTypes';
import { HelpTooltip } from '../../../common/HelpTooltip';
import Loading from '../../../common/Loading';
import ProtectedRoute from '../../../common/ProtectedRoute';
import QueryErrorDisplay from '../../../common/QueryErrorDisplay';
Expand All @@ -39,9 +40,9 @@ import { BooleanParameter } from './permissions/BooleanParamater';
import { Coverage, CoverageCheckbox } from './permissions/Coverage';
import { combineCoverages } from './permissions/combineCoverages';
import { GroupStructure, sections } from './permissions/operations';
import { ParametersWithoutArchive, PresetType, presets } from './permissions/presets';
import { PermissionParametersWithoutArchive, PresetType, presets } from './permissions/presets';

export type Parameters = Pick<FlatParameterizedPermission, Parameter>;
export type PermissionParameters = Pick<FlatParameterizedPermission, Parameter>;

const PermissionsView = ({ userId }: { userId: string }) => {
const { t } = useTranslation();
Expand Down Expand Up @@ -127,8 +128,26 @@ const PermissionsView = ({ userId }: { userId: string }) => {

const dialog = useDialog();

const warnedAboutPublicUserEditing = useRef(false);
const warnAboutPublicUserEditing = useCallback(async () => {
if (warnedAboutPublicUserEditing.current || !isPublic) {
return;
}
const really = await dialog({
preset: DialogPreset.CONFIRM,
title: t('admin.permissions.reallyEditPublicUserTitle'),
content: t('admin.permissions.reallyEditPublicUserContent'),
});
if (really) {
warnedAboutPublicUserEditing.current = true;
return;
}
return Promise.race([]); // never resolves - prevent the actual editing from happening
}, [warnedAboutPublicUserEditing, isPublic, dialog, t]);

const addPermission = useCallback(
async (operation: Operation, { archive_tag, ...parameters }: Parameters) => {
async (operation: Operation, { archive_tag, ...parameters }: PermissionParameters) => {
await warnAboutPublicUserEditing();
await createPermission({
variables: {
operation_name: operation.document.name,
Expand All @@ -138,22 +157,31 @@ const PermissionsView = ({ userId }: { userId: string }) => {
},
});
},
[createPermission, parsedUserId]
[warnAboutPublicUserEditing, createPermission, parsedUserId]
);

const removePermission = useCallback(
async (options: Parameters<typeof deletePermission>[0]) => {
await warnAboutPublicUserEditing();
await deletePermission(options);
},
[warnAboutPublicUserEditing, deletePermission]
);

const addPreset = useCallback(
async (
operations: { operation: Operation; parameters: ParametersWithoutArchive }[],
operations: { operation: Operation; parameters: PermissionParametersWithoutArchive }[],
archive: FlatArchiveTag | null
) => {
await warnAboutPublicUserEditing();
await Promise.all(
operations.map(async ({ operation, parameters }) => {
await addPermission(operation, { archive_tag: archive ?? undefined, ...parameters });
})
);
await refetchPermissions();
},
[addPermission, refetchPermissions]
[warnAboutPublicUserEditing, addPermission, refetchPermissions]
);

const toggleOperations = useCallback(
Expand All @@ -164,6 +192,7 @@ const PermissionsView = ({ userId }: { userId: string }) => {
header: string,
prompt = false
) => {
await warnAboutPublicUserEditing();
const removeAll = coverage !== Coverage.NONE;
if (prompt) {
const really = await dialog({
Expand All @@ -184,7 +213,7 @@ const PermissionsView = ({ userId }: { userId: string }) => {
if (!permission) {
return;
}
await deletePermission({
await removePermission({
variables: {
id: permission.id,
},
Expand All @@ -200,7 +229,15 @@ const PermissionsView = ({ userId }: { userId: string }) => {
}
await refetchPermissions();
},
[dialog, t, findPermission, deletePermission, addPermission, refetchPermissions]
[
warnAboutPublicUserEditing,
dialog,
t,
findPermission,
removePermission,
addPermission,
refetchPermissions,
]
);

const [filter, setFilter] = useState('');
Expand All @@ -220,6 +257,12 @@ const PermissionsView = ({ userId }: { userId: string }) => {
: archive
? archive.name
: t('admin.permissions.withoutArchive');
const summaryTooltip =
type === 'system'
? t('admin.permissions.systemPermissionsTooltip')
: archive
? t('admin.permissions.archiveTooltip', { archiveName: archive.name })
: t('admin.permissions.withoutArchiveTooltip');
if (!summary.toLocaleLowerCase().includes(filter.toLocaleLowerCase())) {
return null;
}
Expand Down Expand Up @@ -287,6 +330,7 @@ const PermissionsView = ({ userId }: { userId: string }) => {
clickThrough
/>
</Typography>
<HelpTooltip title={summary} content={summaryTooltip} iconClassName='!ml-auto' />
</AccordionSummary>
<div className='m-4'>
{relevantPresets.length > 0 && (
Expand All @@ -304,46 +348,62 @@ const PermissionsView = ({ userId }: { userId: string }) => {
</Stack>
)}
<div className='mt-2'>
{sectionsWithCoverages.map(section => (
<Accordion key={section.name}>
<AccordionSummary expandIcon={<ExpandMore />}>
<CoverageCheckbox
coverage={combineCoverages(section.groups.map(group => group.coverage))}
operations={section.groups.flatMap(group => group.operations)}
archive={archive}
label={t(`admin.permissions.section.${section.name}`, { context: type })}
prompt
toggleOperations={toggleOperations}
clickThrough
/>
</AccordionSummary>
<AccordionDetails>
{section.groups
.map(group => [t(`admin.permissions.group.${group.name}`), group] as const)
.sort(([a], [b]) => a.localeCompare(b))
.map(([name, group]) => (
// div forces every checkbox on a separate line
<div key={group.name}>
<CoverageCheckbox
coverage={group.coverage}
operations={group.operations}
archive={archive}
label={name}
toggleOperations={toggleOperations}
/>
<ParameterInputs
group={group}
findPermission={findPermission}
archive={archive}
deletePermission={deletePermission}
addPermission={addPermission}
refetchPermissions={refetchPermissions}
/>
</div>
))}
</AccordionDetails>
</Accordion>
))}
{sectionsWithCoverages.map(section => {
const label = t(`admin.permissions.section.${section.name}`, { context: type });
return (
<Accordion key={section.name}>
<AccordionSummary expandIcon={<ExpandMore />}>
<CoverageCheckbox
coverage={combineCoverages(section.groups.map(group => group.coverage))}
operations={section.groups.flatMap(group => group.operations)}
archive={archive}
label={label}
prompt
toggleOperations={toggleOperations}
clickThrough
/>
<HelpTooltip
title={label}
content={t(`admin.permissions.section-tooltip.${section.name}`, {
context: type,
archiveName: archive?.name,
})}
iconClassName='!ml-auto'
/>
</AccordionSummary>
<AccordionDetails>
{section.groups
.map(group => [t(`admin.permissions.group.${group.name}`), group] as const)
.sort(([a], [b]) => a.localeCompare(b))
.map(([name, group]) => (
// div forces every checkbox on a separate line
<div key={group.name}>
<CoverageCheckbox
coverage={group.coverage}
operations={group.operations}
archive={archive}
label={name}
toggleOperations={toggleOperations}
/>
<ParameterInputs
group={group}
findPermission={findPermission}
archive={archive}
removePermission={removePermission}
addPermission={addPermission}
refetchPermissions={refetchPermissions}
/>
<HelpTooltip
title={name}
content={t(`admin.permissions.group-tooltip.${group.name}`)}
iconClassName='float-right'
/>
</div>
))}
</AccordionDetails>
</Accordion>
);
})}
</div>
</div>
</Accordion>
Expand All @@ -357,7 +417,7 @@ const PermissionsView = ({ userId }: { userId: string }) => {
addPreset,
findPermission,
addPermission,
deletePermission,
removePermission,
refetchPermissions,
]
);
Expand Down Expand Up @@ -420,7 +480,7 @@ const ParameterInputs = ({
group,
findPermission,
archive,
deletePermission,
removePermission,
addPermission,
refetchPermissions,
}: {
Expand All @@ -430,8 +490,8 @@ const ParameterInputs = ({
archive: FlatArchiveTag | null
) => FlatParameterizedPermission | null;
archive: FlatArchiveTag | null;
deletePermission: (parameters: { variables: { id: string } }) => Promise<unknown>;
addPermission: (operation: Operation, parameters: Parameters) => Promise<unknown>;
removePermission: (parameters: { variables: { id: string } }) => Promise<unknown>;
addPermission: (operation: Operation, parameters: PermissionParameters) => Promise<unknown>;
refetchPermissions: () => Promise<unknown>;
}) => {
const { t } = useTranslation();
Expand All @@ -440,7 +500,7 @@ const ParameterInputs = ({
operations: group.operations,
findPermission,
addPermission,
deletePermission,
removePermission,
refetchPermissions,
archive,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const ResetPasswordView = () => {
<ProtectedRoute canUse={canResetPassword} canUseLoading={canResetPasswordLoading}>
<CenteredContainer title={t('admin.resetPassword.title')}>
<Stack gap={4}>
<p>{t('admin.passwordRequirements')}</p>
<PasswordInput
label={t('admin.resetPassword.password')}
value={password}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,28 @@ import {
FlatParameterizedPermission,
} from '../../../../../types/additionalFlatTypes';
import { KeysWithValueExtending } from '../../../../../types/helper';
import { Parameters } from '../PermissionsView';
import { PermissionParameters } from '../PermissionsView';

export const BooleanParameter = ({
parameter,
operations,
archive,
findPermission,
deletePermission,
removePermission,
addPermission,
refetchPermissions,
falseTitle,
trueTitle,
}: {
parameter: KeysWithValueExtending<Parameters, Maybe<boolean> | undefined>;
parameter: KeysWithValueExtending<PermissionParameters, Maybe<boolean> | undefined>;
operations: Operation[];
archive: FlatArchiveTag | null;
findPermission: (
operation: Operation,
archive: FlatArchiveTag | null
) => FlatParameterizedPermission | null;
deletePermission: (options: { variables: { id: string } }) => Promise<unknown>;
addPermission: (operation: Operation, parameters: Parameters) => Promise<unknown>;
removePermission: (options: { variables: { id: string } }) => Promise<unknown>;
addPermission: (operation: Operation, parameters: PermissionParameters) => Promise<unknown>;
refetchPermissions: () => Promise<unknown>;
falseTitle: string;
trueTitle: string;
Expand All @@ -39,7 +39,7 @@ export const BooleanParameter = ({
operations.forEach(async operation => {
const permission = findPermission(operation, archive);
if (permission) {
await deletePermission({
await removePermission({
variables: {
id: permission.id,
},
Expand All @@ -60,7 +60,7 @@ export const BooleanParameter = ({
archive,
addPermission,
parameter,
deletePermission,
removePermission,
refetchPermissions,
]
);
Expand Down
Loading

0 comments on commit bdd70dd

Please sign in to comment.