mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-31 04:29:52 +00:00
241 lines
8.3 KiB
TypeScript
241 lines
8.3 KiB
TypeScript
"use client";
|
|
|
|
import { Button } from "@app/components/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger
|
|
} from "@app/components/ui/dropdown-menu";
|
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
|
import { toast } from "@app/hooks/useToast";
|
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
|
import { type PaginationState } from "@tanstack/react-table";
|
|
import {
|
|
ArrowDown01Icon,
|
|
ArrowUp10Icon,
|
|
ChevronsUpDownIcon,
|
|
MoreHorizontal,
|
|
PencilIcon,
|
|
PencilLineIcon
|
|
} from "lucide-react";
|
|
import { useTranslations } from "next-intl";
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
import { useActionState, useMemo, useState, useTransition } from "react";
|
|
import { useDebouncedCallback } from "use-debounce";
|
|
import {
|
|
ControlledDataTable,
|
|
type ExtendedColumnDef
|
|
} from "./ui/controlled-data-table";
|
|
import { LabelBadge } from "./label-badge";
|
|
import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
|
|
import { cn } from "@app/lib/cn";
|
|
import ConfirmDeleteDialog from "./ConfirmDeleteDialog";
|
|
import { CreateOrgLabelDialog } from "./CreateOrgLabelDialog";
|
|
import { EditOrgLabelDialog } from "./EditOrgLabelDialog";
|
|
|
|
export type LabelRow = {
|
|
labelId: number;
|
|
name: string;
|
|
color: string;
|
|
};
|
|
|
|
type OrgLabelsTableProps = {
|
|
labels: LabelRow[];
|
|
pagination: PaginationState;
|
|
orgId: string;
|
|
rowCount: number;
|
|
};
|
|
|
|
export default function OrgLabelsTable({
|
|
labels,
|
|
orgId,
|
|
pagination,
|
|
rowCount
|
|
}: OrgLabelsTableProps) {
|
|
const router = useRouter();
|
|
|
|
const {
|
|
navigate: filter,
|
|
isNavigating: isFiltering,
|
|
searchParams
|
|
} = useNavigationContext();
|
|
|
|
const [selectedLabel, setSelectedLabel] = useState<LabelRow | null>(null);
|
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
|
|
|
const [isRefreshing, startTransition] = useTransition();
|
|
|
|
const api = createApiClient(useEnvContext());
|
|
const t = useTranslations();
|
|
|
|
function refreshData() {
|
|
startTransition(async () => {
|
|
try {
|
|
router.refresh();
|
|
} catch {
|
|
toast({
|
|
title: t("error"),
|
|
description: t("refreshError"),
|
|
variant: "destructive"
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
const handlePaginationChange = (newPage: PaginationState) => {
|
|
searchParams.set("page", (newPage.pageIndex + 1).toString());
|
|
searchParams.set("pageSize", newPage.pageSize.toString());
|
|
filter({ searchParams });
|
|
};
|
|
|
|
const handleSearchChange = useDebouncedCallback((query: string) => {
|
|
searchParams.set("query", query);
|
|
searchParams.delete("page");
|
|
filter({ searchParams });
|
|
}, 300);
|
|
|
|
const columns = useMemo<ExtendedColumnDef<LabelRow>[]>(
|
|
() => [
|
|
{
|
|
accessorKey: "name",
|
|
enableHiding: false,
|
|
header: () => {
|
|
return <span className="p-3">{t("name")}</span>;
|
|
},
|
|
cell: ({ row }) => (
|
|
<div className="flex items-center gap-1.5 group">
|
|
<div
|
|
className="size-2.5 rounded-full bg-(--color) flex-none"
|
|
style={{
|
|
// @ts-expect-error css color
|
|
"--color": row.original.color
|
|
}}
|
|
/>
|
|
|
|
{row.original.name}
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
accessorKey: "actions",
|
|
enableHiding: false,
|
|
header: () => {
|
|
return <span className="p-3">{t("actions")}</span>;
|
|
},
|
|
cell: ({ row }) => (
|
|
<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={() => {
|
|
setSelectedLabel(row.original);
|
|
setIsEditModalOpen(true);
|
|
}}
|
|
>
|
|
{t("edit")}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setSelectedLabel(row.original);
|
|
setIsDeleteModalOpen(true);
|
|
}}
|
|
>
|
|
<span className="text-red-500">
|
|
{t("delete")}
|
|
</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|
|
],
|
|
[searchParams, t]
|
|
);
|
|
|
|
function deleteLabel(label: LabelRow) {
|
|
startTransition(async () => {
|
|
await api
|
|
.delete(`/org/${orgId}/label/${label.labelId}`)
|
|
.catch((e) => {
|
|
toast({
|
|
variant: "destructive",
|
|
title: t("labelErrorDelete"),
|
|
description: formatAxiosError(e, t("labelErrorDelete"))
|
|
});
|
|
})
|
|
.then(() => {
|
|
router.refresh();
|
|
setIsDeleteModalOpen(false);
|
|
});
|
|
});
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{selectedLabel && (
|
|
<>
|
|
<ConfirmDeleteDialog
|
|
open={isDeleteModalOpen}
|
|
setOpen={(val) => {
|
|
setIsDeleteModalOpen(val);
|
|
setSelectedLabel(null);
|
|
}}
|
|
dialog={
|
|
<div className="space-y-2">
|
|
<p>{t("labelQuestionRemove")}</p>
|
|
<p>{t("labelMessageRemove")}</p>
|
|
</div>
|
|
}
|
|
buttonText={t("labelDeleteConfirm")}
|
|
onConfirm={async () => deleteLabel(selectedLabel)}
|
|
string={selectedLabel.name}
|
|
title={t("labelDelete")}
|
|
/>
|
|
|
|
<EditOrgLabelDialog
|
|
open={isEditModalOpen}
|
|
setOpen={setIsEditModalOpen}
|
|
orgId={orgId}
|
|
onSuccess={() =>
|
|
startTransition(() => router.refresh())
|
|
}
|
|
label={selectedLabel}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
<CreateOrgLabelDialog
|
|
open={isCreateModalOpen}
|
|
setOpen={setIsCreateModalOpen}
|
|
orgId={orgId}
|
|
onSuccess={() => startTransition(() => router.refresh())}
|
|
/>
|
|
|
|
<ControlledDataTable
|
|
columns={columns}
|
|
rows={labels}
|
|
addButtonText={t("labelAdd")}
|
|
onAdd={() => setIsCreateModalOpen(true)}
|
|
tableId="org-labels-table"
|
|
searchPlaceholder={t("labelSearch")}
|
|
pagination={pagination}
|
|
onPaginationChange={handlePaginationChange}
|
|
searchQuery={searchParams.get("query")?.toString()}
|
|
onSearch={handleSearchChange}
|
|
onRefresh={refreshData}
|
|
isRefreshing={isRefreshing || isFiltering}
|
|
rowCount={rowCount}
|
|
/>
|
|
</>
|
|
);
|
|
}
|