mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-09 04:06:36 +00:00
move all components to components dir
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
} from "@tanstack/react-table";
|
||||
import { DataTable } from "@app/components/ui/data-table";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
}
|
||||
|
||||
export function InvitationsDataTable<TData, TValue>({
|
||||
columns,
|
||||
data
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
persistPageSize="invitations-table"
|
||||
title={t('invite')}
|
||||
searchPlaceholder={t('inviteSearch')}
|
||||
searchColumn="email"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { MoreHorizontal } from "lucide-react";
|
||||
import { InvitationsDataTable } from "./InvitationsDataTable";
|
||||
import { useState } from "react";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import RegenerateInvitationForm from "./RegenerateInvitationForm";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import moment from "moment";
|
||||
|
||||
export type InvitationRow = {
|
||||
id: string;
|
||||
email: string;
|
||||
expiresAt: string;
|
||||
role: string;
|
||||
roleId: number;
|
||||
};
|
||||
|
||||
type InvitationsTableProps = {
|
||||
invitations: InvitationRow[];
|
||||
};
|
||||
|
||||
export default function InvitationsTable({
|
||||
invitations: i
|
||||
}: InvitationsTableProps) {
|
||||
const [invitations, setInvitations] = useState<InvitationRow[]>(i);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isRegenerateModalOpen, setIsRegenerateModalOpen] = useState(false);
|
||||
const [selectedInvitation, setSelectedInvitation] =
|
||||
useState<InvitationRow | null>(null);
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
const { org } = useOrgContext();
|
||||
|
||||
const columns: ColumnDef<InvitationRow>[] = [
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: t("email")
|
||||
},
|
||||
{
|
||||
accessorKey: "expiresAt",
|
||||
header: t("expiresAt"),
|
||||
cell: ({ row }) => {
|
||||
const expiresAt = new Date(row.original.expiresAt);
|
||||
const isExpired = expiresAt < new Date();
|
||||
|
||||
return (
|
||||
<span className={isExpired ? "text-red-500" : ""}>
|
||||
{moment(expiresAt).format("lll")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "role",
|
||||
header: t("role")
|
||||
},
|
||||
{
|
||||
id: "dots",
|
||||
cell: ({ row }) => {
|
||||
const invitation = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<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={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setSelectedInvitation(invitation);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">
|
||||
{t("inviteRemove")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => {
|
||||
setIsRegenerateModalOpen(true);
|
||||
setSelectedInvitation(invitation);
|
||||
}}
|
||||
>
|
||||
<span>{t("inviteRegenerate")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
async function removeInvitation() {
|
||||
if (selectedInvitation) {
|
||||
const res = await api
|
||||
.delete(
|
||||
`/org/${org?.org.orgId}/invitations/${selectedInvitation.id}`
|
||||
)
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("inviteRemoveError"),
|
||||
description: t("inviteRemoveErrorDescription")
|
||||
});
|
||||
});
|
||||
|
||||
if (res && res.status === 200) {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: t("inviteRemoved"),
|
||||
description: t("inviteRemovedDescription", {
|
||||
email: selectedInvitation.email
|
||||
})
|
||||
});
|
||||
|
||||
setInvitations((prev) =>
|
||||
prev.filter(
|
||||
(invitation) => invitation.id !== selectedInvitation.id
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
setIsDeleteModalOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteModalOpen}
|
||||
setOpen={(val) => {
|
||||
setIsDeleteModalOpen(val);
|
||||
setSelectedInvitation(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
{t("inviteQuestionRemove", {
|
||||
email: selectedInvitation?.email || ""
|
||||
})}
|
||||
</p>
|
||||
<p>{t("inviteMessageRemove")}</p>
|
||||
<p>{t("inviteMessageConfirm")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("inviteRemoveConfirm")}
|
||||
onConfirm={removeInvitation}
|
||||
string={selectedInvitation?.email ?? ""}
|
||||
title={t("inviteRemove")}
|
||||
/>
|
||||
<RegenerateInvitationForm
|
||||
open={isRegenerateModalOpen}
|
||||
setOpen={setIsRegenerateModalOpen}
|
||||
invitation={selectedInvitation}
|
||||
onRegenerate={(updatedInvitation) => {
|
||||
setInvitations((prev) =>
|
||||
prev.map((inv) =>
|
||||
inv.id === updatedInvitation.id
|
||||
? updatedInvitation
|
||||
: inv
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<InvitationsDataTable columns={columns} data={invitations} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
CredenzaClose,
|
||||
CredenzaContent,
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { useState, useEffect } from "react";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
import { Checkbox } from "@app/components/ui/checkbox";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@app/components/ui/select";
|
||||
import { Label } from "@app/components/ui/label";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type RegenerateInvitationFormProps = {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
invitation: {
|
||||
id: string;
|
||||
email: string;
|
||||
roleId: number;
|
||||
role: string;
|
||||
} | null;
|
||||
onRegenerate: (updatedInvitation: {
|
||||
id: string;
|
||||
email: string;
|
||||
expiresAt: string;
|
||||
role: string;
|
||||
roleId: number;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export default function RegenerateInvitationForm({
|
||||
open,
|
||||
setOpen,
|
||||
invitation,
|
||||
onRegenerate
|
||||
}: RegenerateInvitationFormProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inviteLink, setInviteLink] = useState<string | null>(null);
|
||||
const [sendEmail, setSendEmail] = useState(true);
|
||||
const [validHours, setValidHours] = useState(72);
|
||||
const api = createApiClient(useEnvContext());
|
||||
const { org } = useOrgContext();
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const validForOptions = [
|
||||
{ hours: 24, name: t('day', {count: 1}) },
|
||||
{ hours: 48, name: t('day', {count: 2}) },
|
||||
{ hours: 72, name: t('day', {count: 3}) },
|
||||
{ hours: 96, name: t('day', {count: 4}) },
|
||||
{ hours: 120, name: t('day', {count: 5}) },
|
||||
{ hours: 144, name: t('day', {count: 6}) },
|
||||
{ hours: 168, name: t('day', {count: 7}) }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setSendEmail(true);
|
||||
setValidHours(72);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
async function handleRegenerate() {
|
||||
if (!invitation) return;
|
||||
|
||||
if (!org?.org.orgId) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('orgMissing'),
|
||||
description: t('orgMissingMessage'),
|
||||
duration: 5000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const res = await api.post(`/org/${org.org.orgId}/create-invite`, {
|
||||
email: invitation.email,
|
||||
roleId: invitation.roleId,
|
||||
validHours,
|
||||
sendEmail,
|
||||
regenerate: true
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
const link = res.data.data.inviteLink;
|
||||
setInviteLink(link);
|
||||
|
||||
if (sendEmail) {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: t('inviteRegenerated'),
|
||||
description: t('inviteSent', {email: invitation.email}),
|
||||
duration: 5000
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: t('inviteRegenerated'),
|
||||
description: t('inviteGenerate', {email: invitation.email}),
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
|
||||
onRegenerate({
|
||||
id: invitation.id,
|
||||
email: invitation.email,
|
||||
expiresAt: res.data.data.expiresAt,
|
||||
role: invitation.role,
|
||||
roleId: invitation.roleId
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 409) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('inviteDuplicateError'),
|
||||
description: t('inviteDuplicateErrorDescription'),
|
||||
duration: 5000
|
||||
});
|
||||
} else if (error.response?.status === 429) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('inviteRateLimitError'),
|
||||
description: t('inviteRateLimitErrorDescription'),
|
||||
duration: 5000
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('inviteRegenerateError'),
|
||||
description: t('inviteRegenerateErrorDescription'),
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Credenza
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
if (!isOpen) {
|
||||
setInviteLink(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>{t('inviteRegenerate')}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t('inviteRegenerateDescription')}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
{!inviteLink ? (
|
||||
<div>
|
||||
<p>
|
||||
{t('inviteQuestionRegenerate', {email: invitation?.email || ""})}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-4">
|
||||
<Checkbox
|
||||
id="send-email"
|
||||
checked={sendEmail}
|
||||
onCheckedChange={(e) =>
|
||||
setSendEmail(e as boolean)
|
||||
}
|
||||
/>
|
||||
<label htmlFor="send-email">
|
||||
{t('inviteSentEmail')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label>
|
||||
{t('inviteValidityPeriod')}
|
||||
</Label>
|
||||
<Select
|
||||
value={validHours.toString()}
|
||||
onValueChange={(value) =>
|
||||
setValidHours(parseInt(value))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={t('inviteValidityPeriodSelect')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{validForOptions.map((option) => (
|
||||
<SelectItem
|
||||
key={option.hours}
|
||||
value={option.hours.toString()}
|
||||
>
|
||||
{option.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 max-w-md">
|
||||
<p>
|
||||
{t('inviteRegenerateMessage')}
|
||||
</p>
|
||||
<CopyTextBox text={inviteLink} wrapText={false} />
|
||||
</div>
|
||||
)}
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
{!inviteLink ? (
|
||||
<>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t('cancel')}</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
onClick={handleRegenerate}
|
||||
loading={loading}
|
||||
>
|
||||
{t('inviteRegenerateButton')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t('close')}</Button>
|
||||
</CredenzaClose>
|
||||
)}
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { AxiosResponse } from "axios";
|
||||
import InvitationsTable, { InvitationRow } from "./InvitationsTable";
|
||||
import InvitationsTable, { InvitationRow } from "../../../../../components/InvitationsTable";
|
||||
import { GetOrgResponse } from "@server/routers/org";
|
||||
import { cache } from "react";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
import UserProvider from "@app/providers/UserProvider";
|
||||
import { verifySession } from "@app/lib/auth/verifySession";
|
||||
import AccessPageHeaderAndNav from "../AccessPageHeaderAndNav";
|
||||
import AccessPageHeaderAndNav from "../../../../../components/AccessPageHeaderAndNav";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user