Files
calendar/apps/web/modules/blocklist/components/BlockedEntriesTable.tsx
T
Eunjae LeeGitHubClaude Opus 4.5Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
81e4241f85 refactor: apply biome formatting to apps/web (#27692)
* refactor: apply biome formatting to apps/web (batch 1)

Formats apps/web non-modules directories:
- apps/web/app
- apps/web/lib
- apps/web/pages
- apps/web/styles
- apps/web/server
- apps/web/test
- Root-level .ts and .mjs files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/modules (batch 2)

Formats smaller apps/web/modules directories:
- onboarding, data-table, users, apps, auth
- integration-attribute-sync, shell, availability
- feature-flags, team, webhooks, getting-started
- feature-opt-in, troubleshooter, schedules, notifications
- signup-view.tsx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/modules (batch 3)

Formats medium apps/web/modules directories:
- bookings
- event-types
- insights
- embed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/modules/ee (batch 4)

Formats apps/web/modules/ee directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web (batch 5)

Formats remaining apps/web directories:
- apps/web/modules/settings
- apps/web/modules/booking-audit
- apps/web/playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/components (batch 6)

Formats remaining apps/web/components files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to new apps/web files from main

Formats newly added/modified files from main merge:
- WorkflowStepContainer.tsx (resolved merge conflicts)
- Newly moved files (blocklist, form-builder, schedules, etc.)
- Other files modified in main

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to new apps/web files from main

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 23:25:24 +05:30

220 lines
7.0 KiB
TypeScript

"use client";
import type { RowSelectionState } from "@tanstack/react-table";
import { getCoreRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
import type { ReactNode } from "react";
import { useMemo, useState } from "react";
import { DataTableSelectionBar, DataTableWrapper } from "@calcom/web/modules/data-table/components";
import { IS_CALCOM } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui/components/button";
import { ConfirmationDialogContent, Dialog } from "@calcom/ui/components/dialog";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import type { BlocklistEntry, BlocklistPermissions, BlocklistScope } from "@calcom/features/blocklist/types";
import { useBlockedEntriesColumns } from "./BlockedEntriesColumns";
import { BlocklistEntryDetailsSheet } from "./BlocklistEntryDetailsSheet";
export interface BlockedEntriesTableProps<T extends BlocklistEntry> {
scope: BlocklistScope;
data: T[];
totalRowCount: number;
isPending: boolean;
limit: number;
searchTerm?: string;
permissions?: BlocklistPermissions;
onAddClick: () => void;
onDelete: (entry: T) => void;
isDeleting?: boolean;
detailsQuery?: {
data?: {
entry: {
id: string;
value: string;
type: import("@calcom/prisma/enums").WatchlistType;
description: string | null;
source?: string;
bookingReports?: Array<{ booking: { uid: string; title: string | null } }>;
};
auditHistory: Array<{
id: string;
value: string;
changedAt: Date | string;
changedByUser?: { name: string | null; email: string } | null;
}>;
};
isLoading: boolean;
};
selectedEntryId?: string;
onSelectEntry?: (id: string | null) => void;
enableRowSelection?: boolean;
renderBulkActions?: (selectedEntries: T[], clearSelection: () => void) => ReactNode;
}
export function BlockedEntriesTable<T extends BlocklistEntry>({
scope,
data,
totalRowCount,
isPending,
limit,
searchTerm,
permissions,
onAddClick,
onDelete,
isDeleting = false,
detailsQuery,
selectedEntryId,
onSelectEntry,
enableRowSelection = false,
renderBulkActions,
}: BlockedEntriesTableProps<T>) {
const { t } = useLocale();
const isSystem = scope === "system";
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [showDetailsSheet, setShowDetailsSheet] = useState(false);
const [selectedEntry, setSelectedEntry] = useState<T | null>(null);
const [entryToDelete, setEntryToDelete] = useState<T | null>(null);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const handleViewDetails = (entry: T) => {
setSelectedEntry(entry);
setShowDetailsSheet(true);
onSelectEntry?.(entry.id);
};
const handleDelete = (entry: T) => {
setEntryToDelete(entry);
setShowDeleteDialog(true);
};
const confirmDelete = () => {
if (entryToDelete) {
onDelete(entryToDelete);
setShowDeleteDialog(false);
setEntryToDelete(null);
}
};
const handleCloseDetailsSheet = () => {
setShowDetailsSheet(false);
setSelectedEntry(null);
onSelectEntry?.(null);
};
const canDelete = isSystem || permissions?.canDelete;
const columns = useBlockedEntriesColumns<T>({
t,
scope,
canDelete,
enableRowSelection,
onViewDetails: handleViewDetails,
onDelete: handleDelete,
});
const flatData = useMemo(() => data ?? [], [data]);
const table = useReactTable({
data: flatData,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
manualPagination: true,
pageCount: Math.ceil(totalRowCount / limit),
enableRowSelection,
onRowSelectionChange: setRowSelection,
state: {
rowSelection,
},
getRowId: (row) => row.id,
});
const numberOfSelectedRows = table.getFilteredSelectedRowModel().rows.length;
const selectedEntries = table.getSelectedRowModel().flatRows.map((row) => row.original);
const clearSelection = () => table.toggleAllPageRowsSelected(false);
return (
<>
<DataTableWrapper
table={table}
isPending={isPending}
variant="default"
paginationMode="standard"
EmptyView={
<EmptyScreen
customIcon={<img className="mb-6" src="/slash-icon-cards.svg" />}
headline={
searchTerm
? t("no_result_found_for", { searchTerm })
: t(isSystem ? "system_blocklist" : "pbac_resource_blocklist")
}
description={t(isSystem ? "add_people_to_system_blocklist" : "add_people_to_blocklist")}
className="bg-muted mb-16"
iconWrapperClassName="bg-default"
dashedBorder={false}
buttonRaw={
<div className="flex gap-2">
<Button
StartIcon="plus"
onClick={onAddClick}
color="primary"
disabled={!isSystem && !permissions?.canCreate}>
{t("add")}
</Button>
{IS_CALCOM && (
<Button
StartIcon="book"
color="secondary"
onClick={() =>
window.open("https://cal.com/help/security/blocklist", "_blank", "noopener,noreferrer")
}>
{t("docs")}
</Button>
)}
</div>
}
/>
}
totalRowCount={totalRowCount}>
{enableRowSelection && numberOfSelectedRows > 0 && renderBulkActions && (
<DataTableSelectionBar.Root className="bottom-16! justify-center md:w-max">
<p className="text-brand-subtle px-2 text-center text-xs leading-none sm:text-sm sm:font-medium">
{t("number_selected", { count: numberOfSelectedRows })}
</p>
{renderBulkActions(selectedEntries, clearSelection)}
</DataTableSelectionBar.Root>
)}
</DataTableWrapper>
<BlocklistEntryDetailsSheet
scope={scope}
entry={selectedEntry}
isOpen={showDetailsSheet}
onClose={handleCloseDetailsSheet}
handleDeleteBlocklistEntry={handleDelete}
detailsData={detailsQuery?.data}
isLoading={detailsQuery?.isLoading ?? false}
/>
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<ConfirmationDialogContent
variety="danger"
title={t(isSystem ? "remove_value_from_system_blocklist" : "remove_value_from_blocklist", {
value: entryToDelete?.value,
})}
confirmBtnText={t("remove")}
isPending={isDeleting}
onConfirm={confirmDelete}>
{t(
isSystem
? "remove_value_from_system_blocklist_description"
: "remove_value_from_blocklist_description"
)}
</ConfirmationDialogContent>
</Dialog>
</>
);
}