Skip to content

Commit 3e0568f

Browse files
committed
chore: add change role to the user's invitations
1 parent fec66a3 commit 3e0568f

File tree

6 files changed

+175
-69
lines changed

6 files changed

+175
-69
lines changed

ui/actions/invitations/invitation.ts

+25-11
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,33 @@ export const updateInvite = async (formData: FormData) => {
104104
const invitationId = formData.get("invitationId");
105105
const invitationEmail = formData.get("invitationEmail");
106106
const expiresAt = formData.get("expires_at");
107+
const role = formData.get("role");
107108

108109
const url = new URL(`${keyServer}/tenants/invitations/${invitationId}`);
109110

111+
const body = JSON.stringify({
112+
data: {
113+
type: "invitations",
114+
id: invitationId,
115+
attributes: {
116+
email: invitationEmail,
117+
...(expiresAt && { expires_at: expiresAt }),
118+
},
119+
relationships: {
120+
roles: {
121+
data: role
122+
? [
123+
{
124+
id: role,
125+
type: "role",
126+
},
127+
]
128+
: [],
129+
},
130+
},
131+
},
132+
});
133+
110134
try {
111135
const response = await fetch(url.toString(), {
112136
method: "PATCH",
@@ -115,22 +139,12 @@ export const updateInvite = async (formData: FormData) => {
115139
Accept: "application/vnd.api+json",
116140
Authorization: `Bearer ${session?.accessToken}`,
117141
},
118-
body: JSON.stringify({
119-
data: {
120-
type: "invitations",
121-
id: invitationId,
122-
attributes: {
123-
email: invitationEmail,
124-
...(expiresAt && { expires_at: expiresAt }),
125-
},
126-
},
127-
}),
142+
body,
128143
});
129144
const data = await response.json();
130145
revalidatePath("/invitations");
131146
return parseStringify(data);
132147
} catch (error) {
133-
// eslint-disable-next-line no-console
134148
console.error("Error updating invitation:", error);
135149
return {
136150
error: getErrorMessage(error),

ui/app/(prowler)/invitations/page.tsx

+37-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Spacer } from "@nextui-org/react";
2-
import { Suspense } from "react";
2+
import React, { Suspense } from "react";
33

44
import { getInvitations } from "@/actions/invitations/invitation";
5+
import { getRoles } from "@/actions/roles";
56
import { FilterControls } from "@/components/filters";
67
import { filterInvitations } from "@/components/filters/data-filters";
78
import { SendInvitationButton } from "@/components/invitations";
@@ -11,7 +12,7 @@ import {
1112
} from "@/components/invitations/table";
1213
import { Header } from "@/components/ui";
1314
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
14-
import { SearchParamsProps } from "@/types";
15+
import { InvitationProps, Role, SearchParamsProps } from "@/types";
1516

1617
export default async function Invitations({
1718
searchParams,
@@ -54,12 +55,45 @@ const SSRDataTable = async ({
5455
// Extract query from filters
5556
const query = (filters["filter[search]"] as string) || "";
5657

58+
// Fetch invitations and roles
5759
const invitationsData = await getInvitations({ query, page, sort, filters });
60+
const rolesData = await getRoles({});
61+
62+
// Create a dictionary for roles by invitation ID
63+
const roleDict = rolesData?.data?.reduce(
64+
(acc: Record<string, Role>, role: Role) => {
65+
role.relationships.invitations.data.forEach((invitation: any) => {
66+
acc[invitation.id] = role;
67+
});
68+
return acc;
69+
},
70+
{},
71+
);
72+
73+
// Expand invitations with role information
74+
const expandedInvitations = invitationsData?.data?.map(
75+
(invitation: InvitationProps) => {
76+
const role = roleDict[invitation.id];
77+
return {
78+
...invitation,
79+
relationships: {
80+
...invitation.relationships,
81+
role,
82+
},
83+
};
84+
},
85+
);
86+
87+
// Create the expanded response
88+
const expandedResponse = {
89+
...invitationsData,
90+
data: expandedInvitations,
91+
};
5892

5993
return (
6094
<DataTable
6195
columns={ColumnsInvitation}
62-
data={invitationsData?.data || []}
96+
data={expandedResponse?.data || []}
6397
metadata={invitationsData?.meta}
6498
/>
6599
);

ui/components/invitations/forms/edit-form.tsx

+35-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
"use client";
2-
31
import { zodResolver } from "@hookform/resolvers/zod";
2+
import { Select, SelectItem } from "@nextui-org/react";
43
import { Dispatch, SetStateAction } from "react";
5-
import { useForm } from "react-hook-form";
4+
import { Controller, useForm } from "react-hook-form";
65
import * as z from "zod";
76

87
import { updateInvite } from "@/actions/invitations/invitation";
@@ -15,10 +14,14 @@ import { editInviteFormSchema } from "@/types";
1514
export const EditForm = ({
1615
invitationId,
1716
invitationEmail,
17+
roles = [],
18+
defaultRole = "",
1819
setIsOpen,
1920
}: {
2021
invitationId: string;
2122
invitationEmail?: string;
23+
roles: Array<{ id: string; name: string }>;
24+
defaultRole?: string;
2225
setIsOpen: Dispatch<SetStateAction<boolean>>;
2326
}) => {
2427
const formSchema = editInviteFormSchema;
@@ -27,7 +30,8 @@ export const EditForm = ({
2730
resolver: zodResolver(formSchema),
2831
defaultValues: {
2932
invitationId,
30-
invitationEmail: invitationEmail,
33+
invitationEmail: invitationEmail || "",
34+
role: defaultRole,
3135
},
3236
});
3337

@@ -37,7 +41,6 @@ export const EditForm = ({
3741

3842
const onSubmitClient = async (values: z.infer<typeof formSchema>) => {
3943
const formData = new FormData();
40-
console.log(values);
4144

4245
Object.entries(values).forEach(
4346
([key, value]) => value !== undefined && formData.append(key, value),
@@ -83,6 +86,33 @@ export const EditForm = ({
8386
isInvalid={!!form.formState.errors.invitationEmail}
8487
/>
8588
</div>
89+
<div>
90+
<Controller
91+
name="role"
92+
control={form.control}
93+
render={({ field }) => (
94+
<Select
95+
{...field}
96+
label="Role"
97+
placeholder="Select a role"
98+
variant="bordered"
99+
selectedKeys={[field.value]}
100+
onSelectionChange={(selected) =>
101+
field.onChange(selected?.currentKey || "")
102+
}
103+
>
104+
{roles.map((role) => (
105+
<SelectItem key={role.id}>{role.name}</SelectItem>
106+
))}
107+
</Select>
108+
)}
109+
/>
110+
{form.formState.errors.role && (
111+
<p className="mt-2 text-sm text-red-600">
112+
{form.formState.errors.role.message}
113+
</p>
114+
)}
115+
</div>
86116
<input type="hidden" name="invitationId" value={invitationId} />
87117

88118
<div className="flex w-full justify-center sm:space-x-6">

ui/components/invitations/table/column-invitations.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ export const ColumnsInvitation: ColumnDef<InvitationProps>[] = [
3131
return <p className="font-semibold">{state}</p>;
3232
},
3333
},
34+
{
35+
accessorKey: "role",
36+
header: () => <div className="text-left">Role</div>,
37+
cell: ({ row }) => {
38+
const roleName =
39+
row.original.relationships?.role?.attributes?.name || "No Role";
40+
return <p className="font-semibold">{roleName}</p>;
41+
},
42+
},
3443
{
3544
accessorKey: "inserted_at",
3645
header: ({ column }) => (
@@ -60,7 +69,6 @@ export const ColumnsInvitation: ColumnDef<InvitationProps>[] = [
6069
return <DateWithTime dateTime={expires_at} showTime={false} />;
6170
},
6271
},
63-
6472
{
6573
accessorKey: "actions",
6674
header: () => <div className="text-right">Actions</div>,

ui/types/components.ts

+68-49
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,72 @@ export interface InvitationProps {
222222
id: string;
223223
};
224224
};
225+
role?: {
226+
data: {
227+
type: "roles";
228+
id: string;
229+
};
230+
attributes?: {
231+
name: string;
232+
manage_users?: boolean;
233+
manage_account?: boolean;
234+
manage_billing?: boolean;
235+
manage_providers?: boolean;
236+
manage_integrations?: boolean;
237+
manage_scans?: boolean;
238+
permission_state?: "unlimited" | "limited" | "none";
239+
};
240+
};
241+
};
242+
links: {
243+
self: string;
244+
};
245+
}
246+
247+
export interface Role {
248+
type: "role";
249+
id: string;
250+
attributes: {
251+
name: string;
252+
manage_users: boolean;
253+
manage_account: boolean;
254+
manage_billing: boolean;
255+
manage_providers: boolean;
256+
manage_integrations: boolean;
257+
manage_scans: boolean;
258+
unlimited_visibility: boolean;
259+
permission_state: "unlimited" | "limited" | "none";
260+
inserted_at: string;
261+
updated_at: string;
262+
};
263+
relationships: {
264+
provider_groups: {
265+
meta: {
266+
count: number;
267+
};
268+
data: {
269+
type: string;
270+
id: string;
271+
}[];
272+
};
273+
users: {
274+
meta: {
275+
count: number;
276+
};
277+
data: {
278+
type: string;
279+
id: string;
280+
}[];
281+
};
282+
invitations: {
283+
meta: {
284+
count: number;
285+
};
286+
data: {
287+
type: string;
288+
id: string;
289+
}[];
290+
};
225291
};
226292
links: {
227293
self: string;
@@ -235,55 +301,7 @@ export interface RolesProps {
235301
next: string | null;
236302
prev: string | null;
237303
};
238-
data: {
239-
type: "role";
240-
id: string;
241-
attributes: {
242-
name: string;
243-
manage_users: boolean;
244-
manage_account: boolean;
245-
manage_billing: boolean;
246-
manage_providers: boolean;
247-
manage_integrations: boolean;
248-
manage_scans: boolean;
249-
unlimited_visibility: boolean;
250-
permission_state: "unlimited" | "limited" | "none";
251-
inserted_at: string;
252-
updated_at: string;
253-
};
254-
relationships: {
255-
provider_groups: {
256-
meta: {
257-
count: number;
258-
};
259-
data: {
260-
type: string;
261-
id: string;
262-
}[];
263-
};
264-
users: {
265-
meta: {
266-
count: number;
267-
};
268-
data: {
269-
type: string;
270-
id: string;
271-
}[];
272-
};
273-
invitations: {
274-
meta: {
275-
count: number;
276-
};
277-
data: {
278-
type: string;
279-
id: string;
280-
}[];
281-
};
282-
};
283-
links: {
284-
self: string;
285-
};
286-
}[];
304+
data: Role[];
287305
meta: {
288306
pagination: {
289307
page: number;
@@ -293,6 +311,7 @@ export interface RolesProps {
293311
version: string;
294312
};
295313
}
314+
296315
export interface UserProfileProps {
297316
data: {
298317
type: "users";

ui/types/formSchemas.ts

+1
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ export const editInviteFormSchema = z.object({
180180
invitationId: z.string().uuid(),
181181
invitationEmail: z.string().email(),
182182
expires_at: z.string().optional(),
183+
role: z.string().optional(),
183184
});
184185

185186
export const editUserFormSchema = () =>

0 commit comments

Comments
 (0)