feat: blocklist table (#24459)

* feat: blocklist table

* feat: blocklist table

* refactor: feedback

* chore: add select

* UI improvements

* chore: remove un unsed

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
Udit TakkarandCarinaWolli authored and GitHub committed 2025-10-15 11:20:49 +00:00
1 parent 17ab088af8
commit 5a59bb86cd
30 files changed
+2209 -24

No files matched your search

@@ -1,4 +1,6 @@
// eslint-disable-next-line no-restricted-imports
"use client";
import debounce from "lodash/debounce";
import { useState, useEffect } from "react";
@@ -38,7 +38,7 @@ function MembersList(props: MembersListProps) {
return (
<div className="flex flex-col gap-y-3">
{members?.length && team ? (
<ul className="divide-subtle border-subtle divide-y rounded-md border ">
<ul className="divide-subtle border-subtle divide-y rounded-md border">
{members.map((member) => {
return <MemberListItem key={member.id} member={member} />;
})}
@@ -95,7 +95,6 @@ const MembersView = () => {
const router = useRouter();
const params = useParamsWithFallback();
const teamId = Number(params.id);
const session = useSession();
const utils = trpc.useUtils();
// const [query, setQuery] = useState<string | undefined>("");
// const [queryToFetch, setQueryToFetch] = useState<string | undefined>("");
@@ -193,7 +192,7 @@ const MembersView = () => {
{team && (
<>
<hr className="border-subtle my-8" />
<hr className="border-subtle my-8 mt-6" />
<MakeTeamPrivateSwitch
teamId={team.id}
isPrivate={team.isPrivate}
@@ -1,10 +1,26 @@
"use client";
import { DataTableProvider } from "@calcom/features/data-table";
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import MakeTeamPrivateSwitch from "@calcom/features/ee/teams/components/MakeTeamPrivateSwitch";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
const PrivacyView = ({ permissions }: { permissions: { canRead: boolean; canEdit: boolean } }) => {
import { BlocklistTable } from "~/settings/organizations/privacy/blocklist-table";
const PrivacyView = ({
permissions,
watchlistPermissions,
}: {
permissions: { canRead: boolean; canEdit: boolean };
watchlistPermissions?: {
canRead: boolean;
canCreate: boolean;
canDelete: boolean;
};
}) => {
const { t } = useLocale();
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery();
const isInviteOpen = !currentOrg?.user.accepted;
@@ -14,13 +30,27 @@ const PrivacyView = ({ permissions }: { permissions: { canRead: boolean; canEdit
return (
<LicenseRequired>
<div>
<div className="space-y-8">
<MakeTeamPrivateSwitch
isOrg={true}
teamId={currentOrg.id}
isPrivate={currentOrg.isPrivate}
disabled={isDisabled}
/>
{watchlistPermissions?.canRead && (
<div>
<div>
<h2 className="text-emphasis text-base font-semibold">{t("organization_blocklist")}</h2>
<p className="text-muted text-sm">{t("manage_blocked_emails_and_domains")}</p>
</div>
<div className="mt-2">
<DataTableProvider useSegments={useSegments} defaultPageSize={25}>
<BlocklistTable permissions={watchlistPermissions} />
</DataTableProvider>
</div>
</div>
)}
</div>
</LicenseRequired>
);
@@ -45,7 +45,6 @@ const MakeTeamPrivateSwitch = ({
setTeamPrivate(checked);
mutation.mutate({ id: teamId, isPrivate: checked });
}}
switchContainerClassName="mt-6"
data-testid="make-team-private-check"
/>
</>
@@ -159,7 +159,6 @@ const PrivacySettingsView = ({ team }: ProfileViewProps) => {
const isAdmin = team && checkAdminOrOwner(team.membership.role);
const isOrgAdminOrOwner = checkAdminOrOwner(session?.data?.user.org?.role);
const isInviteOpen = !team?.membership.accepted;
const { t } = useLocale();
return (
<>
@@ -173,12 +172,14 @@ const PrivacySettingsView = ({ team }: ProfileViewProps) => {
)}
{team && team.id && (isAdmin || isOrgAdminOrOwner) && (
<MakeTeamPrivateSwitch
isOrg={false}
teamId={team.id}
isPrivate={team.isPrivate ?? false}
disabled={isInviteOpen}
/>
<div className="mt-6">
<MakeTeamPrivateSwitch
isOrg={false}
teamId={team.id}
isPrivate={team.isPrivate ?? false}
disabled={isInviteOpen}
/>
</div>
)}
</div>
</>
@@ -12,6 +12,7 @@ export enum Resource {
Webhook = "webhook",
Availability = "availability",
OutOfOffice = "ooo",
Watchlist = "watchlist",
}
export enum CrudAction {
@@ -120,7 +121,7 @@ export const isValidPermissionString = (val: unknown): val is PermissionString =
* @returns A new object without the _resource property
*/
export const filterResourceConfig = (config: ResourceConfig): Omit<ResourceConfig, "_resource"> => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { _resource, ...rest } = config;
return rest;
};
@@ -147,7 +148,8 @@ export const getPermissionsForScope = (scope: Scope, isPrivate?: boolean): Permi
const scopeMatches = !permissionDetails.scope || permissionDetails.scope.includes(scope);
// Check privacy visibility (only if isPrivate is provided)
const privacyMatches = teamPrivacy === undefined ||
const privacyMatches =
teamPrivacy === undefined ||
!permissionDetails.visibleWhen?.teamPrivacy ||
permissionDetails.visibleWhen.teamPrivacy === "both" ||
permissionDetails.visibleWhen.teamPrivacy === teamPrivacy;
@@ -672,4 +674,40 @@ export const PERMISSION_REGISTRY: PermissionRegistry = {
dependsOn: ["ooo.read"],
},
},
[Resource.Watchlist]: {
_resource: {
i18nKey: "pbac_resource_blocklist",
},
[CrudAction.Create]: {
description: "Create watchlist entries",
category: "watchlist",
i18nKey: "pbac_action_create",
descriptionI18nKey: "pbac_desc_create_watchlist_entries",
scope: [Scope.Organization],
dependsOn: ["watchlist.read"],
},
[CrudAction.Read]: {
description: "View watchlist entries",
category: "watchlist",
i18nKey: "pbac_action_read",
descriptionI18nKey: "pbac_desc_view_watchlist_entries",
scope: [Scope.Organization],
},
[CrudAction.Update]: {
description: "Update watchlist entries",
category: "watchlist",
i18nKey: "pbac_action_update",
descriptionI18nKey: "pbac_desc_update_watchlist_entries",
scope: [Scope.Organization],
dependsOn: ["watchlist.read"],
},
[CrudAction.Delete]: {
description: "Delete watchlist entries",
category: "watchlist",
i18nKey: "pbac_action_delete",
descriptionI18nKey: "pbac_desc_delete_watchlist_entries",
scope: [Scope.Organization],
dependsOn: ["watchlist.read"],
},
},
};
@@ -1042,6 +1042,19 @@ export class UserRepository {
});
}
async findUsersByIds(userIds: number[]) {
return this.prismaClient.user.findMany({
where: {
id: { in: userIds },
},
select: {
id: true,
name: true,
email: true,
},
});
}
async findUsersWithLastBooking({ userIds, eventTypeId }: { userIds: number[]; eventTypeId: number }) {
return this.prismaClient.user.findMany({
where: {