create & update role with device approval

This commit is contained in:
Fred KISSIE
2026-01-08 03:33:03 +01:00
parent abfe476cb9
commit 39bebea5f7
12 changed files with 449 additions and 128 deletions

View File

@@ -734,6 +734,11 @@
"accessRoleCreatedDescription": "The role has been successfully created.", "accessRoleCreatedDescription": "The role has been successfully created.",
"accessRoleErrorCreate": "Failed to create role", "accessRoleErrorCreate": "Failed to create role",
"accessRoleErrorCreateDescription": "An error occurred while creating the role.", "accessRoleErrorCreateDescription": "An error occurred while creating the role.",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessRoleErrorNewRequired": "New role is required", "accessRoleErrorNewRequired": "New role is required",
"accessRoleErrorRemove": "Failed to remove role", "accessRoleErrorRemove": "Failed to remove role",
"accessRoleErrorRemoveDescription": "An error occurred while removing the role.", "accessRoleErrorRemoveDescription": "An error occurred while removing the role.",

View File

@@ -554,6 +554,14 @@ authenticated.get(
verifyUserHasAction(ActionsEnum.listRoles), verifyUserHasAction(ActionsEnum.listRoles),
role.listRoles role.listRoles
); );
authenticated.post(
"/org/:orgId/role/:roleId",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.updateRole),
logActionAudit(ActionsEnum.updateRole),
role.updateRole
);
// authenticated.get( // authenticated.get(
// "/role/:roleId", // "/role/:roleId",
// verifyRoleAccess, // verifyRoleAccess,

View File

@@ -467,6 +467,14 @@ authenticated.put(
role.createRole role.createRole
); );
authenticated.post(
"/org/:orgId/role/:roleId",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.updateRole),
logActionAudit(ActionsEnum.updateRole),
role.updateRole
);
authenticated.get( authenticated.get(
"/org/:orgId/roles", "/org/:orgId/roles",
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,

View File

@@ -10,6 +10,8 @@ import { fromError } from "zod-validation-error";
import { ActionsEnum } from "@server/auth/actions"; import { ActionsEnum } from "@server/auth/actions";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build";
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
const createRoleParamsSchema = z.strictObject({ const createRoleParamsSchema = z.strictObject({
orgId: z.string() orgId: z.string()
@@ -17,7 +19,8 @@ const createRoleParamsSchema = z.strictObject({
const createRoleSchema = z.strictObject({ const createRoleSchema = z.strictObject({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
description: z.string().optional() description: z.string().optional(),
requireDeviceApproval: z.boolean().optional()
}); });
export const defaultRoleAllowedActions: ActionsEnum[] = [ export const defaultRoleAllowedActions: ActionsEnum[] = [
@@ -97,6 +100,11 @@ export async function createRole(
); );
} }
const isLicensed = await isLicensedOrSubscribed(orgId);
if (build === "oss" || !isLicensed) {
roleData.requireDeviceApproval = undefined;
}
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
const newRole = await trx const newRole = await trx
.insert(roles) .insert(roles)

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db, orgs, type Role } from "@server/db";
import { roles } from "@server/db"; import { roles } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
@@ -8,20 +8,28 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { build } from "@server/build";
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
const updateRoleParamsSchema = z.strictObject({ const updateRoleParamsSchema = z.strictObject({
orgId: z.string(),
roleId: z.string().transform(Number).pipe(z.int().positive()) roleId: z.string().transform(Number).pipe(z.int().positive())
}); });
const updateRoleBodySchema = z const updateRoleBodySchema = z
.strictObject({ .strictObject({
name: z.string().min(1).max(255).optional(), name: z.string().min(1).max(255).optional(),
description: z.string().optional() description: z.string().optional(),
requireDeviceApproval: z.boolean().optional()
}) })
.refine((data) => Object.keys(data).length > 0, { .refine((data) => Object.keys(data).length > 0, {
error: "At least one field must be provided for update" error: "At least one field must be provided for update"
}); });
export type UpdateRoleBody = z.infer<typeof updateRoleBodySchema>;
export type UpdateRoleResponse = Role;
export async function updateRole( export async function updateRole(
req: Request, req: Request,
res: Response, res: Response,
@@ -48,13 +56,14 @@ export async function updateRole(
); );
} }
const { roleId } = parsedParams.data; const { roleId, orgId } = parsedParams.data;
const updateData = parsedBody.data; const updateData = parsedBody.data;
const role = await db const role = await db
.select() .select()
.from(roles) .from(roles)
.where(eq(roles.roleId, roleId)) .where(eq(roles.roleId, roleId))
.innerJoin(orgs, eq(roles.orgId, orgs.orgId))
.limit(1); .limit(1);
if (role.length === 0) { if (role.length === 0) {
@@ -66,7 +75,7 @@ export async function updateRole(
); );
} }
if (role[0].isAdmin) { if (role[0].roles.isAdmin) {
return next( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, HttpCode.FORBIDDEN,
@@ -75,6 +84,11 @@ export async function updateRole(
); );
} }
const isLicensed = await isLicensedOrSubscribed(orgId);
if (build === "oss" || !isLicensed) {
updateData.requireDeviceApproval = undefined;
}
const updatedRole = await db const updatedRole = await db
.update(roles) .update(roles)
.set(updateData) .set(updateData)

View File

@@ -5,7 +5,7 @@ import { GetOrgResponse } from "@server/routers/org";
import { cache } from "react"; import { cache } from "react";
import OrgProvider from "@app/providers/OrgProvider"; import OrgProvider from "@app/providers/OrgProvider";
import { ListRolesResponse } from "@server/routers/role"; import { ListRolesResponse } from "@server/routers/role";
import RolesTable, { RoleRow } from "../../../../../components/RolesTable"; import RolesTable, { type RoleRow } from "@app/components/RolesTable";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";

View File

@@ -1,5 +1,15 @@
"use client"; "use client";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle
} from "@app/components/Credenza";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { import {
Form, Form,
@@ -11,37 +21,26 @@ import {
FormMessage FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { build } from "@server/build";
import type { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useState } from "react"; import { useTranslations } from "next-intl";
import { useTransition } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle
} from "@app/components/Credenza";
import { useOrgContext } from "@app/hooks/useOrgContext";
import type { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl";
import { CheckboxWithLabel } from "./ui/checkbox";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { build } from "@server/build";
import { PaidFeaturesAlert } from "./PaidFeaturesAlert"; import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
import { CheckboxWithLabel } from "./ui/checkbox";
type CreateRoleFormProps = { type CreateRoleFormProps = {
open: boolean; open: boolean;
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
afterCreate?: (res: CreateRoleResponse) => Promise<void>; afterCreate?: (res: CreateRoleResponse) => void;
}; };
export default function CreateRoleForm({ export default function CreateRoleForm({
@@ -54,13 +53,14 @@ export default function CreateRoleForm({
const { isPaidUser } = usePaidStatus(); const { isPaidUser } = usePaidStatus();
const formSchema = z.object({ const formSchema = z.object({
name: z.string({ message: t("nameRequired") }).max(32), name: z
.string({ message: t("nameRequired") })
.min(1)
.max(32),
description: z.string().max(255).optional(), description: z.string().max(255).optional(),
requireDeviceApproval: z.boolean().optional() requireDeviceApproval: z.boolean().optional()
}); });
const [loading, setLoading] = useState(false);
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
@@ -72,17 +72,13 @@ export default function CreateRoleForm({
} }
}); });
async function onSubmit(values: z.infer<typeof formSchema>) { const [loading, startTransition] = useTransition();
setLoading(true);
async function onSubmit(values: z.infer<typeof formSchema>) {
const res = await api const res = await api
.put<AxiosResponse<CreateRoleResponse>>( .put<
`/org/${org?.org.orgId}/role`, AxiosResponse<CreateRoleResponse>
{ >(`/org/${org?.org.orgId}/role`, values satisfies CreateRoleBody)
name: values.name,
description: values.description
} as CreateRoleBody
)
.catch((e) => { .catch((e) => {
toast({ toast({
variant: "destructive", variant: "destructive",
@@ -105,21 +101,16 @@ export default function CreateRoleForm({
setOpen(false); setOpen(false);
} }
if (afterCreate) { afterCreate?.(res.data.data);
afterCreate(res.data.data);
} }
} }
setLoading(false);
}
return ( return (
<> <>
<Credenza <Credenza
open={open} open={open}
onOpenChange={(val) => { onOpenChange={(val) => {
setOpen(val); setOpen(val);
setLoading(false);
form.reset(); form.reset();
}} }}
> >
@@ -133,7 +124,9 @@ export default function CreateRoleForm({
<CredenzaBody> <CredenzaBody>
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit((values) =>
startTransition(() => onSubmit(values))
)}
className="space-y-4" className="space-y-4"
id="create-role-form" id="create-role-form"
> >
@@ -168,23 +161,37 @@ export default function CreateRoleForm({
)} )}
/> />
{build !== "oss" && ( {build !== "oss" && (
<> <div className="pt-3">
<PaidFeaturesAlert /> <PaidFeaturesAlert />
<FormField <FormField
control={form.control} control={form.control}
name="requireDeviceApproval" name="requireDeviceApproval"
render={({ field }) => ( render={({ field }) => (
<FormItem className="pt-3"> <FormItem className="my-2">
<FormControl> <FormControl>
<CheckboxWithLabel <CheckboxWithLabel
{...field} {...field}
disabled={ disabled={
!isPaidUser !isPaidUser
} }
value={undefined} value="on"
defaultChecked={ checked={form.watch(
field.value "requireDeviceApproval"
)}
onCheckedChange={(
checked
) => {
if (
checked !==
"indeterminate"
) {
form.setValue(
"requireDeviceApproval",
checked
);
} }
}}
label={t( label={t(
"requireDeviceApproval" "requireDeviceApproval"
)} )}
@@ -201,7 +208,7 @@ export default function CreateRoleForm({
</FormItem> </FormItem>
)} )}
/> />
</> </div>
)} )}
</form> </form>
</Form> </Form>

View File

@@ -78,10 +78,7 @@ const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
const CredenzaClose = isDesktop ? DialogClose : DrawerClose; const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
return ( return (
<CredenzaClose <CredenzaClose className={cn("", className)} {...props}>
className={cn("mb-3 mt-3 md:mt-0 md:mb-0", className)}
{...props}
>
{children} {children}
</CredenzaClose> </CredenzaClose>
); );
@@ -172,14 +169,13 @@ const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => { const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
const isDesktop = useMediaQuery(desktop); const isDesktop = useMediaQuery(desktop);
// const isDesktop = true;
const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter; const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter;
return ( return (
<CredenzaFooter <CredenzaFooter
className={cn( className={cn(
"mt-8 md:mt-0 -mx-6 px-6 pt-4 border-t border-border", "mt-8 md:mt-0 -mx-6 px-6 py-4 border-t border-border",
className className
)} )}
{...props} {...props}

View File

@@ -0,0 +1,241 @@
"use client";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle
} from "@app/components/Credenza";
import { Button } from "@app/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import { Input } from "@app/components/ui/input";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { build } from "@server/build";
import type { Role } from "@server/db";
import type {
CreateRoleBody,
CreateRoleResponse,
UpdateRoleBody,
UpdateRoleResponse
} from "@server/routers/role";
import { AxiosResponse } from "axios";
import { useTranslations } from "next-intl";
import { useTransition } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
import { CheckboxWithLabel } from "./ui/checkbox";
type CreateRoleFormProps = {
role: Role;
open: boolean;
setOpen: (open: boolean) => void;
onSuccess?: (res: CreateRoleResponse) => void;
};
export default function EditRoleForm({
open,
role,
setOpen,
onSuccess
}: CreateRoleFormProps) {
const { org } = useOrgContext();
const t = useTranslations();
const { isPaidUser } = usePaidStatus();
const formSchema = z.object({
name: z
.string({ message: t("nameRequired") })
.min(1)
.max(32),
description: z.string().max(255).optional(),
requireDeviceApproval: z.boolean().optional()
});
const api = createApiClient(useEnvContext());
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: role.name,
description: role.description ?? "",
requireDeviceApproval: role.requireDeviceApproval ?? false
}
});
const [loading, startTransition] = useTransition();
async function onSubmit(values: z.infer<typeof formSchema>) {
const res = await api
.post<
AxiosResponse<UpdateRoleResponse>
>(`/org/${org?.org.orgId}/role/${role.roleId}`, values satisfies UpdateRoleBody)
.catch((e) => {
toast({
variant: "destructive",
title: t("accessRoleErrorUpdate"),
description: formatAxiosError(
e,
t("accessRoleErrorUpdateDescription")
)
});
});
if (res && res.status === 200) {
toast({
variant: "default",
title: t("accessRoleUpdated"),
description: t("accessRoleUpdatedDescription")
});
if (open) {
setOpen(false);
}
onSuccess?.(res.data.data);
}
}
return (
<>
<Credenza
open={open}
onOpenChange={(val) => {
setOpen(val);
form.reset();
}}
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t("accessRoleCreate")}</CredenzaTitle>
<CredenzaDescription>
{t("accessRoleCreateDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<Form {...form}>
<form
onSubmit={form.handleSubmit((values) =>
startTransition(() => onSubmit(values))
)}
className="space-y-4"
id="create-role-form"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("accessRoleName")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("description")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{build !== "oss" && (
<div className="pt-3">
<PaidFeaturesAlert />
<FormField
control={form.control}
name="requireDeviceApproval"
render={({ field }) => (
<FormItem className="my-2">
<FormControl>
<CheckboxWithLabel
{...field}
disabled={
!isPaidUser
}
value="on"
checked={form.watch(
"requireDeviceApproval"
)}
onCheckedChange={(
checked
) => {
if (
checked !==
"indeterminate"
) {
form.setValue(
"requireDeviceApproval",
checked
);
}
}}
label={t(
"requireDeviceApproval"
)}
/>
</FormControl>
<FormDescription>
{t(
"requireDeviceApprovalDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
</form>
</Form>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
<Button
type="submit"
form="create-role-form"
loading={loading}
disabled={loading}
>
{t("accessRoleUpdateSubmit")}
</Button>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
</>
);
}

View File

@@ -1,9 +1,8 @@
"use client"; "use client";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { DataTable } from "@app/components/ui/data-table";
import { ExtendedColumnDef } from "@app/components/ui/data-table";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { DataTable, ExtendedColumnDef } from "@app/components/ui/data-table";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -13,18 +12,12 @@ import {
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
ArrowRight,
ArrowUpDown,
ArrowUpRight,
MoreHorizontal
} from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useMemo, useState, useTransition } from "react"; import { useMemo, useState, useTransition } from "react";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
import { InfoPopup } from "./ui/info-popup";
export type ClientRow = { export type ClientRow = {
id: number; id: number;

View File

@@ -10,12 +10,18 @@ import { useOrgContext } from "@app/hooks/useOrgContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { Role } from "@server/db"; import { Role } from "@server/db";
import { ArrowUpDown } from "lucide-react"; import { ArrowRight, ArrowUpDown, Link, MoreHorizontal } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState, useTransition } from "react";
import { Switch } from "./ui/switch";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { usePaidStatus } from "@app/hooks/usePaidStatus";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem
} from "./ui/dropdown-menu";
import EditRoleForm from "./EditRoleForm";
export type RoleRow = Role; export type RoleRow = Role;
@@ -23,13 +29,13 @@ type RolesTableProps = {
roles: RoleRow[]; roles: RoleRow[];
}; };
export default function UsersTable({ roles: r }: RolesTableProps) { export default function UsersTable({ roles }: RolesTableProps) {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [editingRole, setEditingRole] = useState<RoleRow | null>(null);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const [roles, setRoles] = useState<RoleRow[]>(r);
const [roleToRemove, setUserToRemove] = useState<RoleRow | null>(null); const [roleToRemove, setUserToRemove] = useState<RoleRow | null>(null);
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
@@ -38,13 +44,11 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
const { isPaidUser } = usePaidStatus(); const { isPaidUser } = usePaidStatus();
const t = useTranslations(); const t = useTranslations();
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, startTransition] = useTransition();
const refreshData = async () => { const refreshData = async () => {
console.log("Data refreshed"); console.log("Data refreshed");
setIsRefreshing(true);
try { try {
await new Promise((resolve) => setTimeout(resolve, 200));
router.refresh(); router.refresh();
} catch (error) { } catch (error) {
toast({ toast({
@@ -52,8 +56,6 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
description: t("refreshError"), description: t("refreshError"),
variant: "destructive" variant: "destructive"
}); });
} finally {
setIsRefreshing(false);
} }
}; };
@@ -81,52 +83,76 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
friendlyName: t("description"), friendlyName: t("description"),
header: () => <span className="p-3">{t("description")}</span> header: () => <span className="p-3">{t("description")}</span>
}, },
// {
// id: "actions",
// enableHiding: false,
// header: () => <span className="p-3"></span>,
// cell: ({ row }) => {
// const roleRow = row.original;
...(isPaidUser // return (
? ([ // <div className="flex items-center gap-2 justify-end">
{ // <Button
accessorKey: "requireDeviceApproval", // variant={"outline"}
friendlyName: t("requireDeviceApproval"), // disabled={roleRow.isAdmin || false}
header: () => ( // onClick={() => {
<span className="p-3"> // setIsDeleteModalOpen(true);
{t("requireDeviceApproval")} // setUserToRemove(roleRow);
</span> // }}
), // >
cell: ({ row }) => ( // {t("accessRoleDelete")}
<Switch // </Button>
defaultChecked={ // </div>
!!row.original.requireDeviceApproval // );
} // }
disabled={!!row.original.isAdmin} // },
onCheckedChange={(val) => {
// ...
}}
/>
)
}
] as ExtendedColumnDef<RoleRow>[])
: []),
{ {
id: "actions", id: "actions",
enableHiding: false, enableHiding: false,
header: () => <span className="p-3"></span>, header: () => <span className="p-3"></span>,
cell: ({ row }) => { cell: ({ row }) => {
const roleRow = row.original; const roleRow = row.original;
return ( return (
!roleRow.isAdmin && (
<div className="flex items-center gap-2 justify-end"> <div className="flex items-center gap-2 justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button <Button
variant={"outline"} variant="ghost"
disabled={roleRow.isAdmin || false} className="h-8 w-8 p-0"
>
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => { onClick={() => {
// setSelectedInternalResource(
// resourceRow
// );
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
setUserToRemove(roleRow);
}} }}
> >
{t("accessRoleDelete")} <span className="text-red-500">
{t("delete")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
variant={"outline"}
onClick={() => {
setEditingRole(roleRow);
setIsEditDialogOpen(true);
}}
>
{t("edit")}
</Button> </Button>
</div> </div>
)
); );
} }
} }
@@ -134,11 +160,26 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
return ( return (
<> <>
{editingRole && (
<EditRoleForm
role={editingRole}
open={isEditDialogOpen}
key={editingRole.roleId}
setOpen={setIsEditDialogOpen}
onSuccess={() => {
// Delay refresh to allow modal to close smoothly
setTimeout(() => {
router.refresh();
setEditingRole(null);
}, 150);
}}
/>
)}
<CreateRoleForm <CreateRoleForm
open={isCreateModalOpen} open={isCreateModalOpen}
setOpen={setIsCreateModalOpen} setOpen={setIsCreateModalOpen}
afterCreate={async (role) => { afterCreate={() => {
setRoles((prev) => [...prev, role]); startTransition(refreshData);
}} }}
/> />
@@ -148,9 +189,7 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
setOpen={setIsDeleteModalOpen} setOpen={setIsDeleteModalOpen}
roleToDelete={roleToRemove} roleToDelete={roleToRemove}
afterDelete={() => { afterDelete={() => {
setRoles((prev) => startTransition(refreshData);
prev.filter((r) => r.roleId !== roleToRemove.roleId)
);
setUserToRemove(null); setUserToRemove(null);
}} }}
/> />
@@ -162,7 +201,7 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
createRole={() => { createRole={() => {
setIsCreateModalOpen(true); setIsCreateModalOpen(true);
}} }}
onRefresh={refreshData} onRefresh={() => startTransition(refreshData)}
isRefreshing={isRefreshing} isRefreshing={isRefreshing}
/> />
</> </>

View File

@@ -30,7 +30,8 @@ const checkboxVariants = cva(
); );
interface CheckboxProps interface CheckboxProps
extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>, extends
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
VariantProps<typeof checkboxVariants> {} VariantProps<typeof checkboxVariants> {}
const Checkbox = React.forwardRef< const Checkbox = React.forwardRef<
@@ -49,17 +50,18 @@ const Checkbox = React.forwardRef<
)); ));
Checkbox.displayName = CheckboxPrimitive.Root.displayName; Checkbox.displayName = CheckboxPrimitive.Root.displayName;
interface CheckboxWithLabelProps interface CheckboxWithLabelProps extends React.ComponentPropsWithoutRef<
extends React.ComponentPropsWithoutRef<typeof Checkbox> { typeof Checkbox
> {
label: string; label: string;
} }
const CheckboxWithLabel = React.forwardRef< const CheckboxWithLabel = React.forwardRef<
React.ElementRef<typeof Checkbox>, React.ComponentRef<typeof Checkbox>,
CheckboxWithLabelProps CheckboxWithLabelProps
>(({ className, label, id, ...props }, ref) => { >(({ className, label, id, ...props }, ref) => {
return ( return (
<div className={cn("flex items-center space-x-2", className)}> <div className={cn("flex items-center gap-x-2", className)}>
<Checkbox id={id} ref={ref} {...props} /> <Checkbox id={id} ref={ref} {...props} />
<label <label
htmlFor={id} htmlFor={id}