Skip to content

Commit c0811d5

Browse files
committed
fix: Send consistent payloads when creating and updating roles
https://harperdb.atlassian.net/browse/FAB-402
1 parent f072967 commit c0811d5

File tree

7 files changed

+212
-165
lines changed

7 files changed

+212
-165
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { z } from 'zod';
2+
3+
export const OrganizationRoleOverviewSchema = z.object({
4+
name: z
5+
.string()
6+
.nonempty({
7+
error: 'Please enter a role name.',
8+
})
9+
.regex(/^[a-zA-Z_]*$/, {
10+
error: 'Role must contain only letters and underscores.',
11+
})
12+
.max(30, { error: 'Role name cannot be longer than 30 characters.' }),
13+
update: z.boolean(),
14+
delete: z.boolean(),
15+
});
16+
17+
export const OrganizationRoleSpecificPermissionsSchema = z.object({
18+
roles: z.object({
19+
create: z.boolean(),
20+
delete: z.boolean(),
21+
update: z.boolean(),
22+
view: z.boolean(),
23+
}),
24+
clusters: z.object({
25+
create: z.boolean(),
26+
delete: z.boolean(),
27+
update: z.boolean(),
28+
view: z.boolean(),
29+
resources: z.array(z.object({
30+
id: z.string(),
31+
delete: z.boolean(),
32+
update: z.boolean(),
33+
view: z.boolean(),
34+
instances: z.object({
35+
create: z.boolean(),
36+
delete: z.boolean(),
37+
update: z.boolean(),
38+
view: z.boolean(),
39+
}),
40+
})),
41+
}),
42+
});
43+
44+
export type OrganizationRoleUpdatePayloadType =
45+
& z.infer<typeof OrganizationRoleOverviewSchema>
46+
& z.infer<typeof OrganizationRoleSpecificPermissionsSchema>
47+
& { organizationId: string };
48+
49+
export type OrganizationRoleOverviewType = z.infer<typeof OrganizationRoleOverviewSchema>;
50+
export type OrganizationRoleSpecificPermissionsType = z.infer<typeof OrganizationRoleSpecificPermissionsSchema>;
Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,14 @@
11
import { apiClient } from '@/config/apiClient';
2-
import { SchemaRole } from '@/integrations/api/api.gen';
32
import { useMutation } from '@tanstack/react-query';
4-
import z from 'zod';
3+
import { OrganizationRoleUpdatePayloadType } from './OrganizationRoleFormSchema';
54

6-
export const AddOrganizationRoleSchema = z.object({
7-
roleName: z
8-
.string()
9-
.nonempty({
10-
error: 'Please enter a role name.',
11-
})
12-
.regex(/^[a-zA-Z_]*$/, {
13-
error: 'Role must contain only letters and underscores.',
14-
})
15-
.max(30, { error: 'Role name cannot be longer than 30 characters.' }),
16-
updateOrganization: z.boolean(),
17-
deleteOrganization: z.boolean(),
18-
});
19-
20-
export async function onAddOrganizationRoleSubmit(formData: SchemaRole) {
5+
export async function onAddOrganizationRoleSubmit(formData: OrganizationRoleUpdatePayloadType) {
216
const { data } = await apiClient.post('/Role/', formData);
227
return data;
238
}
249

2510
export function useAddOrganizationRole() {
2611
return useMutation({
27-
mutationFn: (formData: SchemaRole) => onAddOrganizationRoleSubmit(formData),
12+
mutationFn: onAddOrganizationRoleSubmit,
2813
});
2914
}
Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,28 @@
11
import { apiClient } from '@/config/apiClient';
22
import { SchemaRole } from '@/integrations/api/api.gen';
33
import { useMutation } from '@tanstack/react-query';
4-
import z from 'zod';
4+
import { OrganizationRoleUpdatePayloadType } from './OrganizationRoleFormSchema';
55

6-
export const UpdateOrganizationRoleSchema = z.object({
7-
roleName: z
8-
.string()
9-
.nonempty({
10-
error: 'Please enter a role name.',
11-
})
12-
.regex(/^[a-zA-Z_]*$/, {
13-
error: 'Role must contain only letters and underscores.',
14-
})
15-
.max(30, { error: 'Role name cannot be longer than 30 characters.' }),
16-
updateOrganization: z.boolean(),
17-
deleteOrganization: z.boolean(),
18-
});
6+
interface UpdateOrganizationRoleParams {
7+
roleId: string;
8+
updatedRoleInfo: OrganizationRoleUpdatePayloadType;
9+
}
1910

2011
export async function onUpdateOrganizationRole({
2112
roleId,
2213
updatedRoleInfo,
23-
}: {
24-
roleId: string;
25-
updatedRoleInfo: SchemaRole;
26-
}) {
14+
}: UpdateOrganizationRoleParams) {
2715
const { data } = await apiClient.put(`/Role/${roleId}` as '/Role/{id}', updatedRoleInfo);
2816
return data as SchemaRole;
2917
}
3018

3119
export function useUpdateOrganizationRole() {
32-
return useMutation<SchemaRole, Error, { roleId: string; updatedRoleInfo: SchemaRole }>({
33-
mutationFn: ({ roleId, updatedRoleInfo }: { roleId: string; updatedRoleInfo: SchemaRole }) =>
20+
return useMutation<
21+
SchemaRole,
22+
Error,
23+
UpdateOrganizationRoleParams
24+
>({
25+
mutationFn: ({ roleId, updatedRoleInfo }: UpdateOrganizationRoleParams) =>
3426
onUpdateOrganizationRole({ roleId, updatedRoleInfo }),
3527
});
3628
}

src/features/organization/roles/index.tsx

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,22 @@ import { SimpleBrowseDataTable } from '@/components/SimpleBrowseDataTable';
33
import { SubNavMenu } from '@/components/SubNavMenu';
44
import { Button } from '@/components/ui/button';
55
import { getOrganizationRolesQueryOptions } from '@/features/organization/queries/getOrganizationRoles';
6-
import { dataTableColumns } from '@/features/organization/roles/constants/tableDefinition';
7-
import { AddOrganizationRoleModal } from '@/features/organization/roles/modals/AddOrganizationRoleModal';
8-
import { EditOrganizationRoleModal } from '@/features/organization/roles/modals/EditOrganizationRoleModal';
96
import { useOrganizationRolePermissions } from '@/hooks/usePermissions';
107
import { useRefreshClick } from '@/hooks/useRefreshClick';
118
import { SchemaOrganizationRole } from '@/integrations/api/api.gen';
12-
import { useSuspenseQuery } from '@tanstack/react-query';
9+
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
1310
import { useNavigate, useParams } from '@tanstack/react-router';
1411
import { Row } from '@tanstack/react-table';
1512
import { PlusIcon, RefreshCwIcon } from 'lucide-react';
1613
import { Suspense, useCallback, useMemo, useState } from 'react';
14+
import { dataTableColumns } from './constants/tableDefinition';
15+
import { AddOrganizationRoleModal } from './modals/AddOrganizationRoleModal';
16+
import { EditOrganizationRoleModal } from './modals/EditOrganizationRoleModal';
1717

1818
export function OrgConfigRolesIndex() {
1919
const navigate = useNavigate();
20+
const queryClient = useQueryClient();
21+
2022
const { organizationId, orgRoleId }: { organizationId: string; orgRoleId?: string } = useParams({ strict: false });
2123
const { create } = useOrganizationRolePermissions(organizationId);
2224

@@ -37,43 +39,40 @@ export function OrgConfigRolesIndex() {
3739
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
3840

3941
const onSelectOrgRole = useCallback(
40-
(newOrgRole: string | undefined) => {
42+
async (newOrgRole: string | undefined, madeChanges: boolean) => {
4143
const parts = [orgRoleId ? '..' : '', newOrgRole].filter(Boolean);
42-
void navigate({ to: parts.join('/') });
44+
await navigate({ to: parts.join('/') });
45+
if (madeChanges) {
46+
await queryClient.invalidateQueries({
47+
queryKey: [organizationId, 'roles'],
48+
refetchType: 'active',
49+
});
50+
}
4351
},
44-
[orgRoleId, navigate],
52+
[orgRoleId, navigate, queryClient],
4553
);
4654

47-
const onRoleDeleted = useCallback(() => {
48-
void refetch();
49-
setIsAddModalOpen(false);
50-
}, [refetch, setIsAddModalOpen]);
51-
5255
const onAddClicked = useCallback(() => {
5356
setIsAddModalOpen(true);
5457
}, [setIsAddModalOpen]);
55-
const onRoleAdded = useCallback(() => {
56-
void refetch();
57-
setIsAddModalOpen(false);
58-
}, [refetch, setIsAddModalOpen]);
5958

6059
const onRowClick = useCallback(
6160
(rowData: Row<SchemaOrganizationRole>) => {
62-
onSelectOrgRole(rowData.original.id);
61+
return onSelectOrgRole(rowData.original.id, false);
6362
},
6463
[onSelectOrgRole],
6564
);
6665

67-
const closeEditModal = useCallback(() => {
68-
onSelectOrgRole(undefined);
66+
const closeEditModal = useCallback((madeChanges: boolean) => {
67+
return onSelectOrgRole(undefined, madeChanges);
6968
}, [onSelectOrgRole]);
7069

7170
const onRefreshClick = useRefreshClick(refetch);
7271

7372
return (
7473
<>
7574
<SubNavMenu />
76-
<div className="mt-32 px-4 pt-4 md:px-12 min-h-[calc(100vh-theme(spacing.32))]">
75+
<div className="mt-32 px-4 pt-4 md:px-12 min-h-[calc(100vh-(--spacing(32)))]">
7776
<Suspense fallback={<Loading className="flex flex-col items-center justify-center h-full" text="Loading..." />}>
7877
<SimpleBrowseDataTable data={orgRoles} columns={dataTableColumns} onRowClick={onRowClick}>
7978
<Button
@@ -99,13 +98,11 @@ export function OrgConfigRolesIndex() {
9998
{create && (
10099
<AddOrganizationRoleModal
101100
isModalOpen={isAddModalOpen}
102-
onChangesSaved={onRoleAdded}
103101
setIsModalOpen={setIsAddModalOpen}
104102
/>
105103
)}
106104
{isEditOrgRoleModalOpen && (
107105
<EditOrganizationRoleModal
108-
roleDeleted={onRoleDeleted}
109106
data={selectedOrgRole}
110107
isModalOpen={isEditOrgRoleModalOpen}
111108
closeModal={closeEditModal}

src/features/organization/roles/modals/AddOrganizationRoleModal.tsx

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,23 @@ import { FormItem } from '@/components/ui/form/FormItem';
77
import { FormLabel } from '@/components/ui/form/FormLabel';
88
import { FormMessage } from '@/components/ui/form/FormMessage';
99
import { Input } from '@/components/ui/input';
10+
import { useAddOrganizationRole } from '@/features/organization/mutations/addOrganizationRole';
1011
import {
11-
AddOrganizationRoleSchema,
12-
useAddOrganizationRole,
13-
} from '@/features/organization/mutations/addOrganizationRole';
14-
import { SchemaRoleOrganizationPermissions } from '@/integrations/api/api.gen';
12+
OrganizationRoleOverviewSchema,
13+
OrganizationRoleOverviewType,
14+
OrganizationRoleSpecificPermissionsType,
15+
OrganizationRoleUpdatePayloadType,
16+
} from '@/features/organization/mutations/OrganizationRoleFormSchema';
17+
import { safeParse } from '@/lib/string/safeParse';
1518
import { zodResolver } from '@hookform/resolvers/zod';
1619
import { Editor } from '@monaco-editor/react';
20+
import { useQueryClient } from '@tanstack/react-query';
1721
import { useParams } from '@tanstack/react-router';
1822
import { useCallback, useState } from 'react';
1923
import { useForm } from 'react-hook-form';
2024
import { toast } from 'sonner';
21-
import z from 'zod';
2225

23-
const defaultPermissions: Pick<SchemaRoleOrganizationPermissions, 'roles' | 'clusters'> = {
26+
const defaultPermissions: OrganizationRoleSpecificPermissionsType = {
2427
roles: {
2528
create: true,
2629
view: true,
@@ -38,25 +41,24 @@ const defaultPermissions: Pick<SchemaRoleOrganizationPermissions, 'roles' | 'clu
3841

3942
export function AddOrganizationRoleModal({
4043
isModalOpen,
41-
onChangesSaved,
4244
setIsModalOpen,
4345
}: {
44-
onChangesSaved: () => void;
4546
isModalOpen: boolean;
4647
setIsModalOpen: (isOpen: boolean) => void;
4748
}) {
49+
const queryClient = useQueryClient();
4850
const { organizationId }: { organizationId: string } = useParams({ strict: false });
4951
const [isValidJSON, setIsValidJSON] = useState(true);
5052
const [updatedPermissions, setUpdatedPermissions] = useState<string>(JSON.stringify(defaultPermissions, null, 2));
5153

5254
const { mutate: addOrganizationRole, isPending } = useAddOrganizationRole();
5355

5456
const form = useForm({
55-
resolver: zodResolver(AddOrganizationRoleSchema),
57+
resolver: zodResolver(OrganizationRoleOverviewSchema),
5658
defaultValues: {
57-
roleName: '',
58-
updateOrganization: false,
59-
deleteOrganization: false,
59+
name: '',
60+
update: false,
61+
delete: false,
6062
},
6163
});
6264

@@ -68,28 +70,34 @@ export function AddOrganizationRoleModal({
6870
);
6971

7072
const onSubmitRoleEdits = useCallback(
71-
async (formData: z.infer<typeof AddOrganizationRoleSchema>) => {
72-
const updatedFormData = {
73+
async (formData: OrganizationRoleOverviewType) => {
74+
const parsedPermissions = safeParse<OrganizationRoleSpecificPermissionsType>(updatedPermissions);
75+
if (!parsedPermissions) {
76+
return;
77+
}
78+
const updatedFormData: OrganizationRoleUpdatePayloadType = {
79+
...formData,
80+
...parsedPermissions,
7381
organizationId: organizationId,
74-
name: formData.roleName,
75-
update: formData.updateOrganization,
76-
delete: formData.deleteOrganization,
77-
...JSON.parse(updatedPermissions),
7882
};
7983
if (formData && isValidJSON) {
8084
addOrganizationRole(updatedFormData, {
8185
onSuccess: () => {
82-
form.reset();
83-
onChangesSaved();
8486
toast.success('Organization role added successfully!');
87+
setIsModalOpen(false);
88+
void queryClient.invalidateQueries({
89+
queryKey: [organizationId, 'roles'],
90+
refetchType: 'active',
91+
});
92+
form.reset();
8593
},
8694
onError: (error: Error) => {
8795
toast.error(`Failed to add organization role: ${error.message}`);
8896
},
8997
});
9098
}
9199
},
92-
[isValidJSON, updatedPermissions, addOrganizationRole, form, onChangesSaved, organizationId],
100+
[isValidJSON, updatedPermissions, addOrganizationRole, form, queryClient, setIsModalOpen, organizationId],
93101
);
94102

95103
return (
@@ -101,20 +109,20 @@ export function AddOrganizationRoleModal({
101109
<form className="grid grid-cols-2 gap-4 my-4" onSubmit={form.handleSubmit(onSubmitRoleEdits)}>
102110
<FormField
103111
control={form.control}
104-
name="roleName"
112+
name="name"
105113
render={({ field }) => (
106114
<FormItem className="col-span-2">
107115
<FormLabel className="pb-1">Role Name</FormLabel>
108116
<FormControl>
109-
<Input type="text" placeholder="Developer" className="" {...field} />
117+
<Input type="text" className="" {...field} />
110118
</FormControl>
111119
<FormMessage />
112120
</FormItem>
113121
)}
114122
/>
115123
<FormField
116124
control={form.control}
117-
name="updateOrganization"
125+
name="update"
118126
render={({ field }) => (
119127
<FormItem>
120128
<FormLabel className="pb-1">Can Update Organization</FormLabel>
@@ -132,7 +140,7 @@ export function AddOrganizationRoleModal({
132140
/>
133141
<FormField
134142
control={form.control}
135-
name="deleteOrganization"
143+
name="delete"
136144
render={({ field }) => (
137145
<FormItem>
138146
<FormLabel className="pb-1">Can Delete Organization</FormLabel>
@@ -173,7 +181,11 @@ export function AddOrganizationRoleModal({
173181
>
174182
Cancel
175183
</Button>
176-
<Button variant="submit" className="rounded-full" disabled={isPending || !isValidJSON}>
184+
<Button
185+
variant="submit"
186+
className="rounded-full"
187+
disabled={isPending || !isValidJSON || !form.formState.isValid}
188+
>
177189
Save Changes
178190
</Button>
179191
</div>

0 commit comments

Comments
 (0)