* 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>
220 lines
7.0 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|