Skip to content

Commit c48f204

Browse files
Udit-takkarPeerRichCarinaWolli
authored
feat: booking reports table and blocklist table for org (calcom#24736)
* feat: booking reports and blocklist table * chore: save progress * chore: save progress * feat: finish * fix: types and test * fix: types and test * refactor: separate tables * refactor: ffedbac * refactor: remove delete report * refactor: add go back button * feat: add icon and svg * chore: nit * fix: type errror * fix: type errror * refactor: code improvements * chore: add docs url * chore: feedback * minor UI fix * fix: add Is-calcom check --------- Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com>
1 parent 630eee4 commit c48f204

20 files changed

Lines changed: 1017 additions & 289 deletions

File tree

apps/ui-playground/components/ui/IconGrid.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type IconName =
2222
| "binary"
2323
| "blocks"
2424
| "bold"
25+
| "book"
2526
| "book-open-check"
2627
| "book-open"
2728
| "book-user"
Lines changed: 42 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,18 @@
11
"use client";
22

3-
import { keepPreviousData } from "@tanstack/react-query";
4-
import { getCoreRowModel, getSortedRowModel, useReactTable, type ColumnDef } from "@tanstack/react-table";
5-
import { useMemo, useState } from "react";
3+
import { useState } from "react";
64

7-
import { DataTableWrapper, DataTableToolbar, useDataTable } from "@calcom/features/data-table";
5+
import { DataTableToolbar } from "@calcom/features/data-table";
86
import { useLocale } from "@calcom/lib/hooks/useLocale";
9-
import { trpc } from "@calcom/trpc";
10-
import type { RouterOutputs } from "@calcom/trpc/react";
11-
import { Badge } from "@calcom/ui/components/badge";
127
import { Button } from "@calcom/ui/components/button";
13-
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
14-
import { ConfirmationDialogContent, Dialog } from "@calcom/ui/components/dialog";
15-
import { showToast } from "@calcom/ui/components/toast";
8+
import { ToggleGroup } from "@calcom/ui/components/form";
169

17-
import { BlocklistEntryDetailsSheet } from "./components/blocklist-entry-details-sheet";
10+
import PendingReportsBadge from "./components/PendingReportsBadge";
11+
import { BlockedEntriesTable } from "./components/blocked-entries-table";
1812
import { CreateBlocklistEntryModal } from "./components/create-blocklist-entry-modal";
13+
import { PendingReportsTable } from "./components/pending-reports-table";
1914

20-
type BlocklistEntry = RouterOutputs["viewer"]["organizations"]["listWatchlistEntries"]["rows"][number];
15+
type ViewType = "blocked" | "pending";
2116

2217
interface BlocklistTableProps {
2318
permissions?: {
@@ -29,179 +24,49 @@ interface BlocklistTableProps {
2924

3025
export function BlocklistTable({ permissions }: BlocklistTableProps) {
3126
const { t } = useLocale();
32-
const { limit, offset, searchTerm } = useDataTable();
33-
27+
const [activeView, setActiveView] = useState<ViewType>("blocked");
3428
const [showCreateModal, setShowCreateModal] = useState(false);
35-
const [showDetailsSheet, setShowDetailsSheet] = useState(false);
36-
const [selectedEntry, setSelectedEntry] = useState<BlocklistEntry | null>(null);
37-
const [entryToDelete, setEntryToDelete] = useState<BlocklistEntry | null>(null);
38-
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
39-
40-
const { data, isPending } = trpc.viewer.organizations.listWatchlistEntries.useQuery(
41-
{
42-
limit,
43-
offset,
44-
searchTerm,
45-
},
46-
{
47-
placeholderData: keepPreviousData,
48-
}
49-
);
50-
51-
const utils = trpc.useUtils();
52-
53-
const deleteWatchlistEntry = trpc.viewer.organizations.deleteWatchlistEntry.useMutation({
54-
onSuccess: async () => {
55-
await utils.viewer.organizations.listWatchlistEntries.invalidate();
56-
showToast(t("blocklist_entry_deleted"), "success");
57-
setShowDeleteDialog(false);
58-
setEntryToDelete(null);
59-
},
60-
onError: (error) => {
61-
showToast(error.message, "error");
62-
},
63-
});
64-
65-
const handleDelete = (entry: BlocklistEntry) => {
66-
setEntryToDelete(entry);
67-
setShowDeleteDialog(true);
68-
};
69-
70-
const confirmDelete = () => {
71-
if (entryToDelete) {
72-
deleteWatchlistEntry.mutate({ id: entryToDelete.id });
73-
}
74-
};
75-
76-
const handleViewDetails = (entry: BlocklistEntry) => {
77-
setSelectedEntry(entry);
78-
setShowDetailsSheet(true);
79-
};
80-
81-
const totalRowCount = data?.meta?.totalRowCount ?? 0;
82-
const flatData = useMemo<BlocklistEntry[]>(() => data?.rows ?? [], [data]);
83-
84-
const columns = useMemo<ColumnDef<BlocklistEntry>[]>(
85-
() => [
86-
{
87-
id: "value",
88-
header: t("value"),
89-
accessorKey: "value",
90-
enableHiding: false,
91-
cell: ({ row }) => <span className="text-emphasis">{row.original.value}</span>,
92-
},
93-
{
94-
id: "type",
95-
header: t("type"),
96-
accessorKey: "type",
97-
size: 100,
98-
cell: ({ row }) => (
99-
<Badge variant="blue">{row.original.type === "EMAIL" ? t("email") : t("domain")}</Badge>
100-
),
101-
},
102-
{
103-
id: "createdBy",
104-
header: t("blocked_by"),
105-
size: 180,
106-
cell: ({ row }) => {
107-
const audit = row.original.audits?.[0] as
108-
| { changedByUserId: number | null }
109-
| {
110-
changedByUser?: { id: number; email: string; name: string | null } | undefined;
111-
changedByUserId: number | null;
112-
}
113-
| undefined;
114-
const email =
115-
(audit && "changedByUser" in audit ? audit.changedByUser?.email : undefined) ?? undefined;
116-
return <span className="text-default">{email ?? "—"}</span>;
117-
},
118-
},
119-
{
120-
id: "actions",
121-
header: "",
122-
size: 120,
123-
enableHiding: false,
124-
enableSorting: false,
125-
enableResizing: false,
126-
cell: ({ row }) => {
127-
const entry = row.original;
128-
return (
129-
<div className="flex items-center justify-end">
130-
<ButtonGroup combined containerProps={{ className: "border-default" }}>
131-
<Button
132-
color="secondary"
133-
variant="icon"
134-
StartIcon="eye"
135-
onClick={() => handleViewDetails(entry)}
136-
tooltip={t("view")}
137-
/>
138-
{permissions?.canDelete && (
139-
<Button
140-
color="destructive"
141-
variant="icon"
142-
StartIcon="trash"
143-
onClick={() => handleDelete(entry)}
144-
tooltip={t("delete")}
145-
/>
146-
)}
147-
</ButtonGroup>
148-
</div>
149-
);
150-
},
151-
},
152-
],
153-
[t, permissions?.canDelete]
154-
);
155-
156-
const table = useReactTable({
157-
data: flatData,
158-
columns,
159-
getCoreRowModel: getCoreRowModel(),
160-
getSortedRowModel: getSortedRowModel(),
161-
manualPagination: true,
162-
pageCount: Math.ceil(totalRowCount / limit),
163-
});
16429

16530
return (
16631
<>
167-
<DataTableWrapper
168-
table={table}
169-
isPending={isPending}
170-
variant="default"
171-
paginationMode="standard"
172-
totalRowCount={totalRowCount}>
173-
<div className="flex items-center justify-between">
32+
<div className="mb-4 flex items-center justify-between">
33+
<div className="flex items-center gap-2">
34+
<ToggleGroup
35+
value={activeView}
36+
onValueChange={(value) => {
37+
if (value) setActiveView(value as ViewType);
38+
}}
39+
options={[
40+
{ value: "blocked", label: t("blocked") },
41+
{
42+
value: "pending",
43+
label: (
44+
<span className="flex items-center">
45+
{t("pending")}
46+
<PendingReportsBadge />
47+
</span>
48+
),
49+
},
50+
]}
51+
/>
17452
<DataTableToolbar.SearchBar />
175-
<div className="flex items-center gap-2">
176-
{permissions?.canCreate && (
177-
<Button color="primary" StartIcon="plus" onClick={() => setShowCreateModal(true)}>
178-
{t("create_block_entry")}
179-
</Button>
180-
)}
181-
</div>
18253
</div>
183-
</DataTableWrapper>
184-
185-
<CreateBlocklistEntryModal isOpen={showCreateModal} onClose={() => setShowCreateModal(false)} />
54+
<div className="flex items-center gap-2">
55+
{permissions?.canCreate && (
56+
<Button color="primary" StartIcon="plus" onClick={() => setShowCreateModal(true)}>
57+
{t("add")}
58+
</Button>
59+
)}
60+
</div>
61+
</div>
18662

187-
<BlocklistEntryDetailsSheet
188-
entry={selectedEntry}
189-
isOpen={showDetailsSheet}
190-
onClose={() => {
191-
setShowDetailsSheet(false);
192-
setSelectedEntry(null);
193-
}}
194-
/>
63+
{activeView === "blocked" ? (
64+
<BlockedEntriesTable permissions={permissions} onAddClick={() => setShowCreateModal(true)} />
65+
) : (
66+
<PendingReportsTable />
67+
)}
19568

196-
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
197-
<ConfirmationDialogContent
198-
variety="danger"
199-
title={t("delete_blocklist_entry")}
200-
confirmBtnText={t("delete")}
201-
onConfirm={confirmDelete}>
202-
{t("delete_blocklist_entry_confirmation", { value: entryToDelete?.value })}
203-
</ConfirmationDialogContent>
204-
</Dialog>
69+
<CreateBlocklistEntryModal isOpen={showCreateModal} onClose={() => setShowCreateModal(false)} />
20570
</>
20671
);
20772
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { trpc } from "@calcom/trpc/react";
2+
import { Badge } from "@calcom/ui/components/badge";
3+
4+
export default function PendingReportsBadge() {
5+
const { data: pendingReportsCount } = trpc.viewer.organizations.pendingReportsCount.useQuery();
6+
if (!pendingReportsCount) return null;
7+
return (
8+
<Badge rounded variant="orange" className="ml-1">
9+
{pendingReportsCount}
10+
</Badge>
11+
);
12+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import type { ColumnDef } from "@tanstack/react-table";
2+
import { useMemo } from "react";
3+
4+
import type { RouterOutputs } from "@calcom/trpc/react";
5+
import { Badge } from "@calcom/ui/components/badge";
6+
import { Button } from "@calcom/ui/components/button";
7+
import {
8+
Dropdown,
9+
DropdownItem,
10+
DropdownMenuContent,
11+
DropdownMenuItem,
12+
DropdownMenuTrigger,
13+
} from "@calcom/ui/components/dropdown";
14+
15+
type BlocklistEntry = RouterOutputs["viewer"]["organizations"]["listWatchlistEntries"]["rows"][number];
16+
17+
interface UseBlockedEntriesColumnsProps {
18+
t: (key: string) => string;
19+
canDelete?: boolean;
20+
onViewDetails: (entry: BlocklistEntry) => void;
21+
onDelete: (entry: BlocklistEntry) => void;
22+
}
23+
24+
export function useBlockedEntriesColumns({
25+
t,
26+
canDelete,
27+
onViewDetails,
28+
onDelete,
29+
}: UseBlockedEntriesColumnsProps) {
30+
return useMemo<ColumnDef<BlocklistEntry>[]>(
31+
() => [
32+
{
33+
id: "email_slash_domain",
34+
header: t("email_slash_domain"),
35+
accessorKey: "value",
36+
enableHiding: false,
37+
cell: ({ row }) => <span className="text-emphasis">{row.original.value}</span>,
38+
},
39+
{
40+
id: "type",
41+
header: t("type"),
42+
accessorKey: "type",
43+
size: 100,
44+
cell: ({ row }) => (
45+
<Badge variant="blue">{row.original.type === "EMAIL" ? t("email") : t("domain")}</Badge>
46+
),
47+
},
48+
{
49+
id: "createdBy",
50+
header: t("blocked_by"),
51+
size: 180,
52+
cell: ({ row }) => {
53+
const audit = row.original.audits?.[0] as
54+
| { changedByUserId: number | null }
55+
| {
56+
changedByUser?: { id: number; email: string; name: string | null } | undefined;
57+
changedByUserId: number | null;
58+
}
59+
| undefined;
60+
const email =
61+
(audit && "changedByUser" in audit ? audit.changedByUser?.email : undefined) ?? undefined;
62+
return <span className="text-default">{email ?? "—"}</span>;
63+
},
64+
},
65+
{
66+
id: "actions",
67+
header: "",
68+
size: 90,
69+
minSize: 90,
70+
maxSize: 90,
71+
enableHiding: false,
72+
enableSorting: false,
73+
enableResizing: false,
74+
cell: ({ row }) => {
75+
const entry = row.original;
76+
return (
77+
<div className="flex items-center justify-end">
78+
<Dropdown modal={false}>
79+
<DropdownMenuTrigger asChild>
80+
<Button type="button" variant="icon" color="secondary" StartIcon="ellipsis" />
81+
</DropdownMenuTrigger>
82+
<DropdownMenuContent>
83+
<DropdownMenuItem>
84+
<DropdownItem type="button" StartIcon="eye" onClick={() => onViewDetails(entry)}>
85+
{t("view_details")}
86+
</DropdownItem>
87+
</DropdownMenuItem>
88+
{canDelete && (
89+
<DropdownMenuItem>
90+
<DropdownItem
91+
type="button"
92+
color="destructive"
93+
StartIcon="trash"
94+
onClick={() => onDelete(entry)}>
95+
{t("remove_from_blocklist")}
96+
</DropdownItem>
97+
</DropdownMenuItem>
98+
)}
99+
</DropdownMenuContent>
100+
</Dropdown>
101+
</div>
102+
);
103+
},
104+
},
105+
],
106+
[t, canDelete, onViewDetails, onDelete]
107+
);
108+
}

0 commit comments

Comments
 (0)