mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-23 05:16:38 +00:00
Merge pull request #2121 from Fredkiss3/feat/device-approvals
feat: device approvals
This commit is contained in:
243
src/components/ApprovalFeed.tsx
Normal file
243
src/components/ApprovalFeed.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
"use client";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import {
|
||||
approvalFiltersSchema,
|
||||
approvalQueries,
|
||||
type ApprovalItem
|
||||
} from "@app/lib/queries";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ArrowRight, Ban, Check, LaptopMinimal, RefreshCw } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { Fragment, useActionState } from "react";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Button } from "./ui/button";
|
||||
import { Card, CardHeader } from "./ui/card";
|
||||
import { Label } from "./ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "./ui/select";
|
||||
import { Separator } from "./ui/separator";
|
||||
|
||||
export type ApprovalFeedProps = {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export function ApprovalFeed({ orgId }: ApprovalFeedProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const path = usePathname();
|
||||
const t = useTranslations();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const filters = approvalFiltersSchema.parse(
|
||||
Object.fromEntries(searchParams.entries())
|
||||
);
|
||||
|
||||
const { data, isFetching, refetch } = useQuery(
|
||||
approvalQueries.listApprovals(orgId, filters)
|
||||
);
|
||||
|
||||
const approvals = data?.approvals ?? [];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<Card className="">
|
||||
<CardHeader className="flex flex-col sm:flex-row sm:items-end lg:items-end gap-2 ">
|
||||
<div className="flex flex-col items-start gap-2 w-48 mb-0">
|
||||
<Label htmlFor="approvalState">
|
||||
{t("filterByApprovalState")}
|
||||
</Label>
|
||||
<Select
|
||||
onValueChange={(newValue) => {
|
||||
const newSearch = new URLSearchParams(
|
||||
searchParams
|
||||
);
|
||||
newSearch.set("approvalState", newValue);
|
||||
|
||||
router.replace(
|
||||
`${path}?${newSearch.toString()}`
|
||||
);
|
||||
}}
|
||||
value={filters.approvalState ?? "all"}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="approvalState"
|
||||
className="w-full"
|
||||
>
|
||||
<SelectValue
|
||||
placeholder={t("selectApprovalState")}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-full">
|
||||
<SelectItem value="pending">
|
||||
{t("pending")}
|
||||
</SelectItem>
|
||||
<SelectItem value="approved">
|
||||
{t("approved")}
|
||||
</SelectItem>
|
||||
<SelectItem value="denied">
|
||||
{t("denied")}
|
||||
</SelectItem>
|
||||
<SelectItem value="all">{t("all")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
refetch();
|
||||
}}
|
||||
disabled={isFetching}
|
||||
className="lg:static gap-2"
|
||||
>
|
||||
<RefreshCw
|
||||
className={cn(
|
||||
"size-4",
|
||||
isFetching && "animate-spin"
|
||||
)}
|
||||
/>
|
||||
{t("refresh")}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<ul className="flex flex-col gap-4">
|
||||
{approvals.map((approval, index) => (
|
||||
<Fragment key={approval.approvalId}>
|
||||
<li>
|
||||
<ApprovalRequest
|
||||
approval={approval}
|
||||
orgId={orgId}
|
||||
onSuccess={() => refetch()}
|
||||
/>
|
||||
</li>
|
||||
{index < approvals.length - 1 && <Separator />}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{approvals.length === 0 && (
|
||||
<li className="flex justify-center items-center p-4 text-muted-foreground">
|
||||
{t("approvalListEmpty")}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ApprovalRequestProps = {
|
||||
approval: ApprovalItem;
|
||||
orgId: string;
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const [_, formAction, isSubmitting] = useActionState(onSubmit, null);
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
async function onSubmit(_previousState: any, formData: FormData) {
|
||||
const decision = formData.get("decision");
|
||||
const res = await api
|
||||
.put(`/org/${orgId}/approvals/${approval.approvalId}`, { decision })
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("accessApprovalErrorUpdate"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("accessApprovalErrorUpdateDescription")
|
||||
)
|
||||
});
|
||||
});
|
||||
if (res && res.status === 200) {
|
||||
const result = res.data.data;
|
||||
toast({
|
||||
variant: "default",
|
||||
title: t("accessApprovalUpdated"),
|
||||
description:
|
||||
result.decision === "approved"
|
||||
? t("accessApprovalApprovedDescription")
|
||||
: t("accessApprovalDeniedDescription")
|
||||
});
|
||||
|
||||
onSuccess?.();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
<div className="inline-flex items-start md:items-center gap-2">
|
||||
<LaptopMinimal className="size-4 text-muted-foreground flex-none relative top-2 sm:top-0" />
|
||||
<span>
|
||||
<span className="text-primary">
|
||||
{approval.user.username}
|
||||
</span>
|
||||
|
||||
{approval.type === "user_device" && (
|
||||
<span>{t("requestingNewDeviceApproval")}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="inline-flex gap-2">
|
||||
{approval.decision === "pending" && (
|
||||
<form action={formAction} className="inline-flex gap-2">
|
||||
<Button
|
||||
value="approved"
|
||||
name="decision"
|
||||
className="gap-2"
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<Check className="size-4 flex-none" />
|
||||
{t("approve")}
|
||||
</Button>
|
||||
<Button
|
||||
value="denied"
|
||||
name="decision"
|
||||
variant="destructive"
|
||||
className="gap-2"
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<Ban className="size-4 flex-none" />
|
||||
{t("deny")}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
{approval.decision === "approved" && (
|
||||
<Badge variant="green">{t("approved")}</Badge>
|
||||
)}
|
||||
{approval.decision === "denied" && (
|
||||
<Badge variant="red">{t("denied")}</Badge>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {}}
|
||||
className="gap-2"
|
||||
asChild
|
||||
>
|
||||
<Link href={"#"}>
|
||||
{t("viewDetails")}
|
||||
<ArrowRight className="size-4 flex-none" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
@@ -26,17 +10,37 @@ import {
|
||||
CredenzaHeader,
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
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 { CreateRoleBody, CreateRoleResponse } 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 = {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
afterCreate?: (res: CreateRoleResponse) => Promise<void>;
|
||||
afterCreate?: (res: CreateRoleResponse) => void;
|
||||
};
|
||||
|
||||
export default function CreateRoleForm({
|
||||
@@ -46,35 +50,35 @@ export default function CreateRoleForm({
|
||||
}: CreateRoleFormProps) {
|
||||
const { org } = useOrgContext();
|
||||
const t = useTranslations();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string({ message: t("nameRequired") }).max(32),
|
||||
description: z.string().max(255).optional()
|
||||
name: z
|
||||
.string({ message: t("nameRequired") })
|
||||
.min(1)
|
||||
.max(32),
|
||||
description: z.string().max(255).optional(),
|
||||
requireDeviceApproval: z.boolean().optional()
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: ""
|
||||
description: "",
|
||||
requireDeviceApproval: false
|
||||
}
|
||||
});
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
setLoading(true);
|
||||
const [loading, startTransition] = useTransition();
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
const res = await api
|
||||
.put<AxiosResponse<CreateRoleResponse>>(
|
||||
`/org/${org?.org.orgId}/role`,
|
||||
{
|
||||
name: values.name,
|
||||
description: values.description
|
||||
} as CreateRoleBody
|
||||
)
|
||||
.put<
|
||||
AxiosResponse<CreateRoleResponse>
|
||||
>(`/org/${org?.org.orgId}/role`, values satisfies CreateRoleBody)
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
@@ -97,12 +101,8 @@ export default function CreateRoleForm({
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
if (afterCreate) {
|
||||
afterCreate(res.data.data);
|
||||
}
|
||||
afterCreate?.(res.data.data);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -111,7 +111,6 @@ export default function CreateRoleForm({
|
||||
open={open}
|
||||
onOpenChange={(val) => {
|
||||
setOpen(val);
|
||||
setLoading(false);
|
||||
form.reset();
|
||||
}}
|
||||
>
|
||||
@@ -125,7 +124,9 @@ export default function CreateRoleForm({
|
||||
<CredenzaBody>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
onSubmit={form.handleSubmit((values) =>
|
||||
startTransition(() => onSubmit(values))
|
||||
)}
|
||||
className="space-y-4"
|
||||
id="create-role-form"
|
||||
>
|
||||
@@ -159,6 +160,56 @@ export default function CreateRoleForm({
|
||||
</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>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { useMediaQuery } from "@app/hooks/useMediaQuery";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -14,16 +12,9 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger
|
||||
} from "@/components/ui/drawer";
|
||||
import { DrawerClose } from "@/components/ui/drawer";
|
||||
import { useMediaQuery } from "@app/hooks/useMediaQuery";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -78,10 +69,7 @@ const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
|
||||
const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
|
||||
|
||||
return (
|
||||
<CredenzaClose
|
||||
className={cn("mb-3 mt-3 md:mt-0 md:mb-0", className)}
|
||||
{...props}
|
||||
>
|
||||
<CredenzaClose className={cn("", className)} {...props}>
|
||||
{children}
|
||||
</CredenzaClose>
|
||||
);
|
||||
@@ -172,14 +160,13 @@ const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
|
||||
|
||||
const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
|
||||
const isDesktop = useMediaQuery(desktop);
|
||||
// const isDesktop = true;
|
||||
|
||||
const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter;
|
||||
|
||||
return (
|
||||
<CredenzaFooter
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@@ -191,12 +178,12 @@ const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
|
||||
|
||||
export {
|
||||
Credenza,
|
||||
CredenzaTrigger,
|
||||
CredenzaBody,
|
||||
CredenzaClose,
|
||||
CredenzaContent,
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle,
|
||||
CredenzaBody,
|
||||
CredenzaFooter
|
||||
CredenzaTrigger
|
||||
};
|
||||
|
||||
@@ -71,10 +71,10 @@ export const DismissableBanner = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="mb-6 relative border-primary/30 bg-gradient-to-br from-primary/10 via-background to-background overflow-hidden">
|
||||
<Card className="mb-6 relative border-primary/30 bg-linear-to-br from-primary/10 via-background to-background overflow-hidden">
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className="absolute top-3 right-3 z-10 p-1.5 rounded-md hover:bg-background/80 transition-colors"
|
||||
className="absolute top-3 right-3 z-10 p-1.5 rounded-md hover:bg-background/80 transition-colors cursor-pointer"
|
||||
aria-label={t("dismiss")}
|
||||
>
|
||||
<X className="w-4 h-4 text-muted-foreground" />
|
||||
@@ -91,7 +91,7 @@ export const DismissableBanner = ({
|
||||
</p>
|
||||
</div>
|
||||
{children && (
|
||||
<div className="flex flex-wrap gap-3 lg:flex-shrink-0 lg:justify-end">
|
||||
<div className="flex flex-wrap gap-3 lg:shrink-0 lg:justify-end">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
|
||||
241
src/components/EditRoleForm.tsx
Normal file
241
src/components/EditRoleForm.tsx
Normal 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("accessRoleEdit")}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t("accessRoleEditDescription")}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
|
||||
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 { DataTable, ExtendedColumnDef } from "@app/components/ui/data-table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -16,7 +15,6 @@ import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUpDown,
|
||||
ArrowUpRight,
|
||||
MoreHorizontal,
|
||||
CircleSlash
|
||||
} from "lucide-react";
|
||||
@@ -25,7 +23,6 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo, useState, useTransition } from "react";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { InfoPopup } from "./ui/info-popup";
|
||||
|
||||
export type ClientRow = {
|
||||
id: number;
|
||||
@@ -45,6 +42,7 @@ export type ClientRow = {
|
||||
agent: string | null;
|
||||
archived?: boolean;
|
||||
blocked?: boolean;
|
||||
approvalState: "approved" | "pending" | "denied";
|
||||
};
|
||||
|
||||
type ClientTableProps = {
|
||||
@@ -214,7 +212,10 @@ export default function MachineClientsTable({
|
||||
</Badge>
|
||||
)}
|
||||
{r.blocked && (
|
||||
<Badge variant="destructive" className="flex items-center gap-1">
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<CircleSlash className="h-3 w-3" />
|
||||
{t("blocked")}
|
||||
</Badge>
|
||||
@@ -410,7 +411,9 @@ export default function MachineClientsTable({
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{clientRow.archived ? "Unarchive" : "Archive"}
|
||||
{clientRow.archived
|
||||
? "Unarchive"
|
||||
: "Archive"}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
@@ -424,7 +427,9 @@ export default function MachineClientsTable({
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{clientRow.blocked ? "Unblock" : "Block"}
|
||||
{clientRow.blocked
|
||||
? "Unblock"
|
||||
: "Block"}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
@@ -539,15 +544,27 @@ export default function MachineClientsTable({
|
||||
value: "blocked"
|
||||
}
|
||||
],
|
||||
filterFn: (row: ClientRow, selectedValues: (string | number | boolean)[]) => {
|
||||
filterFn: (
|
||||
row: ClientRow,
|
||||
selectedValues: (string | number | boolean)[]
|
||||
) => {
|
||||
if (selectedValues.length === 0) return true;
|
||||
const rowArchived = row.archived || false;
|
||||
const rowBlocked = row.blocked || false;
|
||||
const isActive = !rowArchived && !rowBlocked;
|
||||
|
||||
if (selectedValues.includes("active") && isActive) return true;
|
||||
if (selectedValues.includes("archived") && rowArchived) return true;
|
||||
if (selectedValues.includes("blocked") && rowBlocked) return true;
|
||||
|
||||
if (selectedValues.includes("active") && isActive)
|
||||
return true;
|
||||
if (
|
||||
selectedValues.includes("archived") &&
|
||||
rowArchived
|
||||
)
|
||||
return true;
|
||||
if (
|
||||
selectedValues.includes("blocked") &&
|
||||
rowBlocked
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
},
|
||||
defaultValues: ["active"] // Default to showing active clients
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { ExtendedColumnDef } from "@app/components/ui/data-table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { ArrowUpDown, Crown, MoreHorizontal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { RolesDataTable } from "@app/components/RolesDataTable";
|
||||
import { Role } from "@server/db";
|
||||
import CreateRoleForm from "@app/components/CreateRoleForm";
|
||||
import DeleteRoleForm from "@app/components/DeleteRoleForm";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { RolesDataTable } from "@app/components/RolesDataTable";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { ExtendedColumnDef } from "@app/components/ui/data-table";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { Role } from "@server/db";
|
||||
import { ArrowRight, ArrowUpDown, Link, MoreHorizontal } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, useTransition } from "react";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem
|
||||
} from "./ui/dropdown-menu";
|
||||
import EditRoleForm from "./EditRoleForm";
|
||||
|
||||
export type RoleRow = Role;
|
||||
|
||||
@@ -29,27 +29,26 @@ type RolesTableProps = {
|
||||
roles: RoleRow[];
|
||||
};
|
||||
|
||||
export default function UsersTable({ roles: r }: RolesTableProps) {
|
||||
export default function UsersTable({ roles }: RolesTableProps) {
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [editingRole, setEditingRole] = useState<RoleRow | null>(null);
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const [roles, setRoles] = useState<RoleRow[]>(r);
|
||||
|
||||
const [roleToRemove, setUserToRemove] = useState<RoleRow | null>(null);
|
||||
const [roleToRemove, setRoleToRemove] = useState<RoleRow | null>(null);
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
const { org } = useOrgContext();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
const t = useTranslations();
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [isRefreshing, startTransition] = useTransition();
|
||||
|
||||
const refreshData = async () => {
|
||||
console.log("Data refreshed");
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
toast({
|
||||
@@ -57,8 +56,6 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
||||
description: t("refreshError"),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -86,26 +83,74 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
||||
friendlyName: t("description"),
|
||||
header: () => <span className="p-3">{t("description")}</span>
|
||||
},
|
||||
// {
|
||||
// id: "actions",
|
||||
// enableHiding: false,
|
||||
// header: () => <span className="p-3"></span>,
|
||||
// cell: ({ row }) => {
|
||||
// const roleRow = row.original;
|
||||
|
||||
// return (
|
||||
// <div className="flex items-center gap-2 justify-end">
|
||||
// <Button
|
||||
// variant={"outline"}
|
||||
// disabled={roleRow.isAdmin || false}
|
||||
// onClick={() => {
|
||||
// setIsDeleteModalOpen(true);
|
||||
// setUserToRemove(roleRow);
|
||||
// }}
|
||||
// >
|
||||
// {t("accessRoleDelete")}
|
||||
// </Button>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
{
|
||||
id: "actions",
|
||||
enableHiding: false,
|
||||
header: () => <span className="p-3"></span>,
|
||||
cell: ({ row }) => {
|
||||
const roleRow = row.original;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
disabled={roleRow.isAdmin || false}
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setUserToRemove(roleRow);
|
||||
}}
|
||||
>
|
||||
{t("accessRoleDelete")}
|
||||
</Button>
|
||||
</div>
|
||||
!roleRow.isAdmin && (
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
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={() => {
|
||||
setRoleToRemove(roleRow);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">
|
||||
{t("delete")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => {
|
||||
setEditingRole(roleRow);
|
||||
setIsEditDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("edit")}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -113,11 +158,29 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{editingRole && (
|
||||
<EditRoleForm
|
||||
role={editingRole}
|
||||
open={isEditDialogOpen}
|
||||
key={editingRole.roleId}
|
||||
setOpen={setIsEditDialogOpen}
|
||||
onSuccess={() => {
|
||||
// Delay refresh to allow modal to close smoothly
|
||||
setTimeout(() => {
|
||||
startTransition(async () => {
|
||||
await refreshData().then(() =>
|
||||
setEditingRole(null)
|
||||
);
|
||||
});
|
||||
}, 150);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<CreateRoleForm
|
||||
open={isCreateModalOpen}
|
||||
setOpen={setIsCreateModalOpen}
|
||||
afterCreate={async (role) => {
|
||||
setRoles((prev) => [...prev, role]);
|
||||
afterCreate={() => {
|
||||
startTransition(refreshData);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -127,10 +190,11 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
||||
setOpen={setIsDeleteModalOpen}
|
||||
roleToDelete={roleToRemove}
|
||||
afterDelete={() => {
|
||||
setRoles((prev) =>
|
||||
prev.filter((r) => r.roleId !== roleToRemove.roleId)
|
||||
);
|
||||
setUserToRemove(null);
|
||||
startTransition(async () => {
|
||||
await refreshData().then(() =>
|
||||
setRoleToRemove(null)
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -141,7 +205,7 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
||||
createRole={() => {
|
||||
setIsCreateModalOpen(true);
|
||||
}}
|
||||
onRefresh={refreshData}
|
||||
onRefresh={() => startTransition(refreshData)}
|
||||
isRefreshing={isRefreshing}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
|
||||
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 { DataTable, ExtendedColumnDef } from "@app/components/ui/data-table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -24,9 +23,11 @@ import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo, useState, useTransition } from "react";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { InfoPopup } from "./ui/info-popup";
|
||||
import ClientDownloadBanner from "./ClientDownloadBanner";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { build } from "@server/build";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { t } from "@faker-js/faker/dist/airline-DF6RqYmq";
|
||||
|
||||
export type ClientRow = {
|
||||
id: number;
|
||||
@@ -44,6 +45,7 @@ export type ClientRow = {
|
||||
userEmail: string | null;
|
||||
niceId: string;
|
||||
agent: string | null;
|
||||
approvalState: "approved" | "pending" | "denied" | null;
|
||||
archived?: boolean;
|
||||
blocked?: boolean;
|
||||
};
|
||||
@@ -210,11 +212,22 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
</Badge>
|
||||
)}
|
||||
{r.blocked && (
|
||||
<Badge variant="destructive" className="flex items-center gap-1">
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<CircleSlash className="h-3 w-3" />
|
||||
{t("blocked")}
|
||||
</Badge>
|
||||
)}
|
||||
{r.approvalState === "pending" && (
|
||||
<Badge
|
||||
variant="outlinePrimary"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{t("pendingApproval")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -272,33 +285,6 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
);
|
||||
}
|
||||
},
|
||||
// {
|
||||
// accessorKey: "siteName",
|
||||
// header: ({ column }) => {
|
||||
// return (
|
||||
// <Button
|
||||
// variant="ghost"
|
||||
// onClick={() =>
|
||||
// column.toggleSorting(column.getIsSorted() === "asc")
|
||||
// }
|
||||
// >
|
||||
// Site
|
||||
// <ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
// </Button>
|
||||
// );
|
||||
// },
|
||||
// cell: ({ row }) => {
|
||||
// const r = row.original;
|
||||
// return (
|
||||
// <Link href={`/${r.orgId}/settings/sites/${r.siteId}`}>
|
||||
// <Button variant="outline">
|
||||
// {r.siteName}
|
||||
// <ArrowUpRight className="ml-2 h-4 w-4" />
|
||||
// </Button>
|
||||
// </Link>
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
{
|
||||
accessorKey: "online",
|
||||
friendlyName: "Connectivity",
|
||||
@@ -460,7 +446,11 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>{clientRow.archived ? "Unarchive" : "Archive"}</span>
|
||||
<span>
|
||||
{clientRow.archived
|
||||
? "Unarchive"
|
||||
: "Archive"}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
@@ -472,7 +462,11 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>{clientRow.blocked ? "Unblock" : "Block"}</span>
|
||||
<span>
|
||||
{clientRow.blocked
|
||||
? "Unblock"
|
||||
: "Block"}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
{!clientRow.userId && (
|
||||
// Machine client - also show delete option
|
||||
@@ -482,7 +476,9 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">Delete</span>
|
||||
<span className="text-red-500">
|
||||
Delete
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
@@ -570,32 +566,65 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
options: [
|
||||
{
|
||||
id: "active",
|
||||
label: t("active") || "Active",
|
||||
label: t("active"),
|
||||
value: "active"
|
||||
},
|
||||
{
|
||||
id: "pending",
|
||||
label: t("pendingApproval"),
|
||||
value: "pending"
|
||||
},
|
||||
{
|
||||
id: "denied",
|
||||
label: t("deniedApproval"),
|
||||
value: "denied"
|
||||
},
|
||||
{
|
||||
id: "archived",
|
||||
label: t("archived") || "Archived",
|
||||
label: t("archived"),
|
||||
value: "archived"
|
||||
},
|
||||
{
|
||||
id: "blocked",
|
||||
label: t("blocked") || "Blocked",
|
||||
label: t("blocked"),
|
||||
value: "blocked"
|
||||
}
|
||||
],
|
||||
filterFn: (row: ClientRow, selectedValues: (string | number | boolean)[]) => {
|
||||
filterFn: (
|
||||
row: ClientRow,
|
||||
selectedValues: (string | number | boolean)[]
|
||||
) => {
|
||||
if (selectedValues.length === 0) return true;
|
||||
const rowArchived = row.archived || false;
|
||||
const rowBlocked = row.blocked || false;
|
||||
const rowArchived = row.archived;
|
||||
const rowBlocked = row.blocked;
|
||||
const approvalState = row.approvalState;
|
||||
const isActive = !rowArchived && !rowBlocked;
|
||||
|
||||
if (selectedValues.includes("active") && isActive) return true;
|
||||
if (selectedValues.includes("archived") && rowArchived) return true;
|
||||
if (selectedValues.includes("blocked") && rowBlocked) return true;
|
||||
|
||||
if (selectedValues.includes("active") && isActive)
|
||||
return true;
|
||||
if (
|
||||
selectedValues.includes("pending") &&
|
||||
approvalState === "pending"
|
||||
)
|
||||
return true;
|
||||
if (
|
||||
selectedValues.includes("denied") &&
|
||||
approvalState === "denied"
|
||||
)
|
||||
return true;
|
||||
if (
|
||||
selectedValues.includes("archived") &&
|
||||
rowArchived
|
||||
)
|
||||
return true;
|
||||
if (
|
||||
selectedValues.includes("blocked") &&
|
||||
rowBlocked
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
},
|
||||
defaultValues: ["active"] // Default to showing active clients
|
||||
defaultValues: ["active", "pending"] // Default to showing active clients
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -30,7 +30,8 @@ const checkboxVariants = cva(
|
||||
);
|
||||
|
||||
interface CheckboxProps
|
||||
extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
|
||||
extends
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
|
||||
VariantProps<typeof checkboxVariants> {}
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
@@ -49,17 +50,18 @@ const Checkbox = React.forwardRef<
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
interface CheckboxWithLabelProps
|
||||
extends React.ComponentPropsWithoutRef<typeof Checkbox> {
|
||||
interface CheckboxWithLabelProps extends React.ComponentPropsWithoutRef<
|
||||
typeof Checkbox
|
||||
> {
|
||||
label: string;
|
||||
}
|
||||
|
||||
const CheckboxWithLabel = React.forwardRef<
|
||||
React.ElementRef<typeof Checkbox>,
|
||||
React.ComponentRef<typeof Checkbox>,
|
||||
CheckboxWithLabelProps
|
||||
>(({ className, label, id, ...props }, ref) => {
|
||||
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} />
|
||||
<label
|
||||
htmlFor={id}
|
||||
|
||||
@@ -15,7 +15,7 @@ const DialogPortal = DialogPrimitive.Portal;
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
@@ -30,7 +30,7 @@ const DialogOverlay = React.forwardRef<
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
|
||||
Reference in New Issue
Block a user