Skip to content

Commit 11c6060

Browse files
committed
feat: allow updating group
1 parent f85212d commit 11c6060

3 files changed

Lines changed: 68 additions & 25 deletions

File tree

apps/web/src/components/WithFallback.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import { useEffect, useState } from 'react';
44

55
import { LoadingFallback } from './LoadingFallback';
66

7-
const MIN_DELAY = 300; // ms
8-
97
function isDataReady<TProps extends { data: unknown }>(
108
props: TProps
119
): props is TProps & { data: NonNullable<TProps['data']> } {
@@ -14,9 +12,12 @@ function isDataReady<TProps extends { data: unknown }>(
1412

1513
export function WithFallback<TProps extends { [key: string]: unknown }>({
1614
Component,
15+
minDelay = 300, // ms
1716
props
1817
}: {
1918
Component: React.FC<TProps>;
19+
/** the minimum duration to suspend in ms */
20+
minDelay?: number;
2021
props: TProps extends { data: infer TData extends NonNullable<unknown> }
2122
? Omit<TProps, 'data'> & { data: null | TData | undefined }
2223
: never;
@@ -29,7 +30,7 @@ export function WithFallback<TProps extends { [key: string]: unknown }>({
2930
if (!isMinDelayComplete) {
3031
timeout = setTimeout(() => {
3132
setIsMinDelayComplete(true);
32-
}, MIN_DELAY);
33+
}, minDelay);
3334
}
3435
return () => clearTimeout(timeout);
3536
}, []);

apps/web/src/features/admin/pages/ManageUsersPage.tsx

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useEffect, useState } from 'react';
22

33
import { snakeToCamelCase } from '@douglasneuroinformatics/libjs';
44
import { Button, ClientTable, Heading, SearchBar, Sheet } from '@douglasneuroinformatics/libui/components';
@@ -7,31 +7,44 @@ import type { User } from '@opendatacapture/schemas/user';
77
import { Link } from 'react-router-dom';
88

99
import { PageHeader } from '@/components/PageHeader';
10+
import { WithFallback } from '@/components/WithFallback';
1011
import { useSearch } from '@/hooks/useSearch';
1112
import { useAppStore } from '@/store';
1213

1314
import { useDeleteUserMutation } from '../hooks/useDeleteUserMutation';
15+
import { useGroupsQuery } from '../hooks/useGroupsQuery';
1416
import { useUpdateUserMutation } from '../hooks/useUpdateUserMutation';
1517
import { useUsersQuery } from '../hooks/useUsersQuery';
1618
import { UpdateUserForm, type UpdateUserFormInputData } from './UpdateUserForm';
1719

1820
export const ManageUsersPage = () => {
1921
const currentUser = useAppStore((store) => store.currentUser);
2022
const { t } = useTranslation();
23+
const groupsQuery = useGroupsQuery();
2124
const usersQuery = useUsersQuery();
2225
const deleteUserMutation = useDeleteUserMutation();
2326
const updateUserMutation = useUpdateUserMutation();
2427
const [selectedUser, setSelectedUser] = useState<null | User>(null);
2528
const { filteredData, searchTerm, setSearchTerm } = useSearch(usersQuery.data ?? [], 'username');
2629

27-
const data: UpdateUserFormInputData = {
28-
disableDelete: selectedUser?.username === currentUser?.username,
29-
initialValues: selectedUser?.additionalPermissions.length
30-
? {
31-
additionalPermissions: selectedUser.additionalPermissions
32-
}
33-
: undefined
34-
};
30+
const [data, setData] = useState<null | UpdateUserFormInputData>(null);
31+
32+
useEffect(() => {
33+
const groups = groupsQuery.data;
34+
if (!selectedUser || !groups) {
35+
setData(null);
36+
} else {
37+
setData({
38+
disableDelete: selectedUser?.username === currentUser?.username,
39+
groupOptions: Object.fromEntries(groups.map((group) => [group.id, group.name])),
40+
initialValues: selectedUser?.additionalPermissions.length
41+
? {
42+
additionalPermissions: selectedUser.additionalPermissions
43+
}
44+
: undefined
45+
});
46+
}
47+
}, [groupsQuery.data, selectedUser]);
3548

3649
return (
3750
<Sheet open={Boolean(selectedUser)} onOpenChange={() => setSelectedUser(null)}>
@@ -96,17 +109,23 @@ export const ManageUsersPage = () => {
96109
})}
97110
</Sheet.Description>
98111
</Sheet.Header>
99-
<Sheet.Body className="grid gap-4">
100-
<UpdateUserForm
101-
data={data}
102-
onDelete={() => {
103-
deleteUserMutation.mutate({ id: selectedUser!.id });
104-
setSelectedUser(null);
105-
}}
106-
onSubmit={(data) => {
107-
void updateUserMutation.mutateAsync({ data, id: selectedUser!.id }).then(() => {
112+
<Sheet.Body className="grid h-full gap-4">
113+
<WithFallback
114+
Component={UpdateUserForm}
115+
minDelay={1000}
116+
props={{
117+
data,
118+
onDelete: () => {
119+
deleteUserMutation.mutate({ id: selectedUser!.id });
108120
setSelectedUser(null);
109-
});
121+
},
122+
onSubmit: ({ groupIds, ...data }) => {
123+
void updateUserMutation
124+
.mutateAsync({ data: { groupIds: Array.from(groupIds), ...data }, id: selectedUser!.id })
125+
.then(() => {
126+
setSelectedUser(null);
127+
});
128+
}
110129
}}
111130
/>
112131
</Sheet.Body>

apps/web/src/features/admin/pages/UpdateUserForm.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@ import { useTranslation } from '@douglasneuroinformatics/libui/hooks';
33
import type { FormTypes } from '@opendatacapture/runtime-core';
44
import { $UpdateUserData } from '@opendatacapture/schemas/user';
55
import type { Promisable } from 'type-fest';
6-
import type { z } from 'zod';
6+
import { z } from 'zod';
77

8-
const $UpdateUserFormData = $UpdateUserData.pick({ additionalPermissions: true }).required();
8+
const $UpdateUserFormData = $UpdateUserData
9+
.pick({ additionalPermissions: true })
10+
.required()
11+
.extend({
12+
groupIds: z.set(z.string())
13+
});
914

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

1217
export type UpdateUserFormInputData = {
1318
disableDelete: boolean;
19+
groupOptions: {
20+
[id: string]: string;
21+
};
1422
initialValues?: FormTypes.PartialNullableData<UpdateUserFormData>;
1523
};
1624

@@ -19,7 +27,7 @@ export const UpdateUserForm: React.FC<{
1927
onDelete: () => void;
2028
onSubmit: (data: UpdateUserFormData) => Promisable<void>;
2129
}> = ({ data, onDelete, onSubmit }) => {
22-
const { disableDelete, initialValues } = data;
30+
const { disableDelete, groupOptions, initialValues } = data;
2331
const { t } = useTranslation();
2432
return (
2533
<Form
@@ -123,6 +131,21 @@ export const UpdateUserForm: React.FC<{
123131
en: 'Authorization',
124132
fr: 'Autorisation'
125133
})
134+
},
135+
{
136+
description: '',
137+
fields: {
138+
groupIds: {
139+
kind: 'set',
140+
label: 'Group IDs',
141+
options: groupOptions,
142+
variant: 'listbox'
143+
}
144+
},
145+
title: t({
146+
en: 'Groups',
147+
fr: 'Groupes'
148+
})
126149
}
127150
]}
128151
initialValues={initialValues}

0 commit comments

Comments
 (0)