Skip to content

Commit 0330d14

Browse files
authored
feat: system admin blocklist table (calcom#25039)
* feat: system admin blocklist table * chore: text * fix: type error * fix: type error * fix: type error * fix: frontend bug * chore: improvments * refactor: use shared components * refactor: improvements * fix: delete * refactor: support multiple watchlist * chore: update translation * fix: type err * fix: bulk bug * tests: add unit test * refactor: file * refactor: file * fix: type error * fix: type error * chore: translation * refactor: use shared comments * fix: imports * fix: type error
1 parent d865f6c commit 0330d14

34 files changed

Lines changed: 1970 additions & 633 deletions
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { _generateMetadata, getTranslate } from "app/_utils";
2+
3+
import SystemBlocklistView from "@calcom/features/ee/admin/pages/settings/blocklist";
4+
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
5+
6+
export const generateMetadata = async () =>
7+
await _generateMetadata(
8+
(t) => t("system_blocklist"),
9+
(t) => t("system_blocklist_description"),
10+
undefined,
11+
undefined,
12+
"/settings/admin/blocklist"
13+
);
14+
15+
const Page = async () => {
16+
const t = await getTranslate();
17+
return (
18+
<SettingsHeader title={t("system_blocklist")} description={t("system_blocklist_description")}>
19+
<SystemBlocklistView />
20+
</SettingsHeader>
21+
);
22+
};
23+
24+
export default Page;

apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
240240
href: "/settings/admin/lockedSMS",
241241
trackingMetadata: { section: "admin", page: "locked_sms" },
242242
},
243+
{
244+
name: "pbac_resource_blocklist",
245+
href: "/settings/admin/blocklist",
246+
trackingMetadata: { section: "admin", page: "blocklist" },
247+
},
243248
{
244249
name: "oAuth",
245250
href: "/settings/admin/oAuth",

apps/web/modules/ee/organizations/privacy/blocklist-table.tsx

Lines changed: 131 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
"use client";
22

3+
import { keepPreviousData } from "@tanstack/react-query";
34
import { useState } from "react";
45

6+
import {
7+
BlockedEntriesTable,
8+
CreateBlocklistEntryModal,
9+
PendingReportsBadge,
10+
PendingReportsTable,
11+
} from "@calcom/features/blocklist";
12+
import { useDataTable } from "@calcom/features/data-table";
513
import { DataTableToolbar } from "~/data-table/components";
614
import { useLocale } from "@calcom/lib/hooks/useLocale";
15+
import { trpc } from "@calcom/trpc/react";
716
import { Button } from "@calcom/ui/components/button";
817
import { ToggleGroup } from "@calcom/ui/components/form";
9-
10-
import PendingReportsBadge from "./components/PendingReportsBadge";
11-
import { BlockedEntriesTable } from "./components/blocked-entries-table";
12-
import { CreateBlocklistEntryModal } from "./components/create-blocklist-entry-modal";
13-
import { PendingReportsTable } from "./components/pending-reports-table";
18+
import { showToast } from "@calcom/ui/components/toast";
1419

1520
type ViewType = "blocked" | "pending";
1621

@@ -24,8 +29,86 @@ interface BlocklistTableProps {
2429

2530
export function BlocklistTable({ permissions }: BlocklistTableProps) {
2631
const { t } = useLocale();
32+
const { limit, offset, searchTerm } = useDataTable();
2733
const [activeView, setActiveView] = useState<ViewType>("blocked");
2834
const [showCreateModal, setShowCreateModal] = useState(false);
35+
const [selectedEntryId, setSelectedEntryId] = useState<string | null>(null);
36+
37+
const utils = trpc.useUtils();
38+
39+
const { data: blockedData, isPending: isBlockedPending } =
40+
trpc.viewer.organizations.listWatchlistEntries.useQuery(
41+
{ limit, offset, searchTerm },
42+
{ placeholderData: keepPreviousData, enabled: activeView === "blocked" }
43+
);
44+
45+
const { data: reportsData, isPending: isReportsPending } =
46+
trpc.viewer.organizations.listBookingReports.useQuery(
47+
{
48+
limit,
49+
offset,
50+
searchTerm,
51+
filters: { hasWatchlist: false, status: ["PENDING"] },
52+
},
53+
{ placeholderData: keepPreviousData, enabled: activeView === "pending" }
54+
);
55+
56+
const { data: pendingReportsCount } =
57+
trpc.viewer.organizations.pendingReportsCount.useQuery();
58+
59+
const { data: entryDetails, isLoading: isDetailsLoading } =
60+
trpc.viewer.organizations.getWatchlistEntryDetails.useQuery(
61+
{ id: selectedEntryId ?? "" },
62+
{ enabled: !!selectedEntryId }
63+
);
64+
65+
const createEntry =
66+
trpc.viewer.organizations.createWatchlistEntry.useMutation({
67+
onSuccess: async () => {
68+
await utils.viewer.organizations.listWatchlistEntries.invalidate();
69+
showToast(t("blocklist_entry_created"), "success");
70+
setShowCreateModal(false);
71+
},
72+
onError: (error) => {
73+
showToast(error.message, "error");
74+
},
75+
});
76+
77+
const deleteEntry =
78+
trpc.viewer.organizations.deleteWatchlistEntry.useMutation({
79+
onSuccess: async () => {
80+
await utils.viewer.organizations.listWatchlistEntries.invalidate();
81+
setSelectedEntryId(null);
82+
showToast(t("blocklist_entry_deleted"), "success");
83+
},
84+
onError: (error) => {
85+
showToast(error.message, "error");
86+
},
87+
});
88+
89+
const addToWatchlist = trpc.viewer.organizations.addToWatchlist.useMutation({
90+
onSuccess: async () => {
91+
await utils.viewer.organizations.listBookingReports.invalidate();
92+
await utils.viewer.organizations.listWatchlistEntries.invalidate();
93+
await utils.viewer.organizations.pendingReportsCount.invalidate();
94+
showToast(t("blocklist_entry_created"), "success");
95+
},
96+
onError: (error) => {
97+
showToast(error.message, "error");
98+
},
99+
});
100+
101+
const dismissReport =
102+
trpc.viewer.organizations.dismissBookingReport.useMutation({
103+
onSuccess: async () => {
104+
await utils.viewer.organizations.listBookingReports.invalidate();
105+
await utils.viewer.organizations.pendingReportsCount.invalidate();
106+
showToast(t("booking_report_dismissed"), "success");
107+
},
108+
onError: (error) => {
109+
showToast(error.message, "error");
110+
},
111+
});
29112

30113
return (
31114
<>
@@ -43,7 +126,7 @@ export function BlocklistTable({ permissions }: BlocklistTableProps) {
43126
label: (
44127
<span className="flex items-center">
45128
{t("pending")}
46-
<PendingReportsBadge />
129+
<PendingReportsBadge count={pendingReportsCount} />
47130
</span>
48131
),
49132
},
@@ -53,20 +136,58 @@ export function BlocklistTable({ permissions }: BlocklistTableProps) {
53136
</div>
54137
<div className="flex items-center gap-2">
55138
{permissions?.canCreate && (
56-
<Button color="primary" StartIcon="plus" onClick={() => setShowCreateModal(true)}>
139+
<Button
140+
color="primary"
141+
StartIcon="plus"
142+
onClick={() => setShowCreateModal(true)}
143+
>
57144
{t("add")}
58145
</Button>
59146
)}
60147
</div>
61148
</div>
62149

63150
{activeView === "blocked" ? (
64-
<BlockedEntriesTable permissions={permissions} onAddClick={() => setShowCreateModal(true)} />
151+
<BlockedEntriesTable
152+
scope="organization"
153+
data={blockedData?.rows ?? []}
154+
totalRowCount={blockedData?.meta?.totalRowCount ?? 0}
155+
isPending={isBlockedPending}
156+
limit={limit}
157+
searchTerm={searchTerm}
158+
permissions={permissions}
159+
onAddClick={() => setShowCreateModal(true)}
160+
onDelete={(entry) => deleteEntry.mutate({ id: entry.id })}
161+
isDeleting={deleteEntry.isPending}
162+
detailsQuery={{ data: entryDetails, isLoading: isDetailsLoading }}
163+
selectedEntryId={selectedEntryId ?? undefined}
164+
onSelectEntry={setSelectedEntryId}
165+
/>
65166
) : (
66-
<PendingReportsTable />
167+
<PendingReportsTable
168+
scope="organization"
169+
data={reportsData?.rows ?? []}
170+
totalRowCount={reportsData?.meta?.totalRowCount ?? 0}
171+
isPending={isReportsPending}
172+
limit={limit}
173+
onAddToBlocklist={(reportIds, type, onSuccess) =>
174+
addToWatchlist.mutate({ reportIds, type }, { onSuccess })
175+
}
176+
onDismiss={(reportId, onSuccess) =>
177+
dismissReport.mutate({ reportId }, { onSuccess })
178+
}
179+
isAddingToBlocklist={addToWatchlist.isPending}
180+
isDismissing={dismissReport.isPending}
181+
/>
67182
)}
68183

69-
<CreateBlocklistEntryModal isOpen={showCreateModal} onClose={() => setShowCreateModal(false)} />
184+
<CreateBlocklistEntryModal
185+
isOpen={showCreateModal}
186+
onClose={() => setShowCreateModal(false)}
187+
scope="organization"
188+
onCreateEntry={(data) => createEntry.mutate(data)}
189+
isPending={createEntry.isPending}
190+
/>
70191
</>
71192
);
72193
}

apps/web/modules/ee/organizations/privacy/components/PendingReportsBadge.tsx

Lines changed: 0 additions & 12 deletions
This file was deleted.

apps/web/modules/ee/organizations/privacy/components/blocked-entries-columns.tsx

Lines changed: 0 additions & 108 deletions
This file was deleted.

0 commit comments

Comments
 (0)