Skip to content

Commit

Permalink
Merge pull request #1070 from joshunrau/update-group
Browse files Browse the repository at this point in the history
  • Loading branch information
joshunrau authored Dec 30, 2024
2 parents 2eed15e + 9bb3c90 commit d73f253
Show file tree
Hide file tree
Showing 9 changed files with 5,277 additions and 3,157 deletions.
9 changes: 7 additions & 2 deletions apps/api/src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,14 @@ export class UsersService {
return user;
}

async updateById(id: string, data: UpdateUserDto, { ability }: EntityOperationOptions = {}) {
async updateById(id: string, { groupIds, ...data }: UpdateUserDto, { ability }: EntityOperationOptions = {}) {
return this.userModel.update({
data,
data: {
...data,
groups: {
connect: groupIds?.map((id) => ({ id }))
}
},
where: { AND: [accessibleQuery(ability, 'update', 'User')], id }
});
}
Expand Down
5 changes: 5 additions & 0 deletions apps/web/.storybook/main.cts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ const config: StorybookConfig = {
files: '**/*.stories.@(js|jsx|ts|tsx)',
titlePrefix: 'Components'
},
{
directory: '../src/features/admin/components',
files: '**/*.stories.@(js|jsx|ts|tsx)',
titlePrefix: 'Admin'
},
{
directory: '../src/features/auth/components',
files: '**/*.stories.@(js|jsx|ts|tsx)',
Expand Down
7 changes: 4 additions & 3 deletions apps/web/src/components/WithFallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { useEffect, useState } from 'react';

import { LoadingFallback } from './LoadingFallback';

const MIN_DELAY = 300; // ms

function isDataReady<TProps extends { data: unknown }>(
props: TProps
): props is TProps & { data: NonNullable<TProps['data']> } {
Expand All @@ -14,9 +12,12 @@ function isDataReady<TProps extends { data: unknown }>(

export function WithFallback<TProps extends { [key: string]: unknown }>({
Component,
minDelay = 300, // ms
props
}: {
Component: React.FC<TProps>;
/** the minimum duration to suspend in ms */
minDelay?: number;
props: TProps extends { data: infer TData extends NonNullable<unknown> }
? Omit<TProps, 'data'> & { data: null | TData | undefined }
: never;
Expand All @@ -29,7 +30,7 @@ export function WithFallback<TProps extends { [key: string]: unknown }>({
if (!isMinDelayComplete) {
timeout = setTimeout(() => {
setIsMinDelayComplete(true);
}, MIN_DELAY);
}, minDelay);
}
return () => clearTimeout(timeout);
}, []);
Expand Down
26 changes: 26 additions & 0 deletions apps/web/src/features/admin/components/UpdateUserForm.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Meta, StoryObj } from '@storybook/react';

import { UpdateUserForm } from './UpdateUserForm';

type Story = StoryObj<typeof UpdateUserForm>;

export default { component: UpdateUserForm } as Meta<typeof UpdateUserForm>;

export const Default: Story = {
args: {
data: {
disableDelete: false,
groupOptions: {},
initialValues: {
additionalPermissions: [
{
action: 'create',
subject: 'User'
}
]
}
},
onDelete: () => alert('Delete!'),
onSubmit: (data) => alert(JSON.stringify({ data }))
}
};
179 changes: 179 additions & 0 deletions apps/web/src/features/admin/components/UpdateUserForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { isAllUndefined } from '@douglasneuroinformatics/libjs';
import { Button, Form } from '@douglasneuroinformatics/libui/components';
import { useTranslation } from '@douglasneuroinformatics/libui/hooks';
import type { FormTypes } from '@opendatacapture/runtime-core';
import { $UserPermission } from '@opendatacapture/schemas/user';
import type { Promisable } from 'type-fest';
import { z } from 'zod';

const $UpdateUserFormData = z
.object({
additionalPermissions: z.array($UserPermission.partial()).optional(),
groupIds: z.set(z.string())
})
.transform((arg) => {
const firstPermission = arg.additionalPermissions?.[0];
if (firstPermission && isAllUndefined(firstPermission)) {
arg.additionalPermissions?.pop();
}
return arg;
})
.superRefine((arg, ctx) => {
arg.additionalPermissions?.forEach((permission, i) => {
Object.entries(permission).forEach(([key, val]) => {
if ((val satisfies string) === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.invalid_type,
expected: 'string',
path: ['additionalPermissions', i, key],
received: 'undefined'
});
}
});
});
});

type UpdateUserFormData = z.infer<typeof $UpdateUserFormData>;

export type UpdateUserFormInputData = {
disableDelete: boolean;
groupOptions: {
[id: string]: string;
};
initialValues: FormTypes.PartialNullableData<UpdateUserFormData>;
};

export const UpdateUserForm: React.FC<{
data: UpdateUserFormInputData;
onDelete: () => void;
onSubmit: (data: UpdateUserFormData) => Promisable<void>;
}> = ({ data, onDelete, onSubmit }) => {
const { disableDelete, groupOptions, initialValues } = data;
const { t } = useTranslation();

return (
<Form
additionalButtons={{
left: (
<Button className="w-full" disabled={disableDelete} type="button" variant="danger" onClick={onDelete}>
{t('core.delete')}
</Button>
)
}}
content={[
{
description: t({
en: 'IMPORTANT: These permissions are not specific to any group. To manage granular permissions, please use the API.',
fr: "IMPORTANT : Ces autorisations ne sont pas spécifiques à un groupe. Pour gérer des autorisations granulaires, veuillez utiliser l'API."
}),
fields: {
additionalPermissions: {
fieldset: {
action: {
kind: 'string',
label: t({
en: 'Action',
fr: 'Action'
}),
options: {
create: t({
en: 'Create',
fr: 'Créer'
}),
delete: t({
en: 'Delete',
fr: 'Effacer'
}),
manage: t({
en: 'Manage (All)',
fr: 'Gérer (Tout)'
}),
read: t({
en: 'Read',
fr: 'Lire'
}),
update: t({
en: 'Update',
fr: 'Mettre à jour'
})
},
variant: 'select'
},
subject: {
kind: 'string',
label: t({
en: 'Resource',
fr: 'Resource'
}),
options: {
all: t({
en: 'All',
fr: 'Tous'
}),
Assignment: t({
en: 'Assignment',
fr: 'Devoir'
}),
Group: t({
en: 'Group',
fr: 'Groupe'
}),
Instrument: t({
en: 'Instrument',
fr: 'Instrument'
}),
InstrumentRecord: t({
en: 'Instrument Record',
fr: "Enregistrement de l'instrument"
}),
Session: t({
en: 'Session',
fr: 'Session'
}),
Subject: t({
en: 'Subject',
fr: 'Client'
}),
User: t({
en: 'User',
fr: 'Utilisateur'
})
},
variant: 'select'
}
},
kind: 'record-array',
label: t({
en: 'Permission',
fr: 'Autorisations supplémentaires'
})
}
},
title: t({
en: 'Authorization',
fr: 'Autorisation'
})
},
{
fields: {
groupIds: {
kind: 'set',
label: 'Group IDs',
options: groupOptions,
variant: 'listbox'
}
},
title: t({
en: 'Groups',
fr: 'Groupes'
})
}
]}
initialValues={initialValues}
key={JSON.stringify(initialValues)}
submitBtnLabel={t('core.save')}
validationSchema={$UpdateUserFormData}
onSubmit={onSubmit}
/>
);
};
Loading

0 comments on commit d73f253

Please sign in to comment.