feat: move filter segment selection from localStorage to database (#21523)
This commit is contained in:
1 parent
1f52ed7290
commit
b3c822abf7
14 files changed
+373
-73
No files matched your search
@@ -172,3 +172,7 @@ export async function listSegments(page: Page): Promise<string[]> {
|
||||
await page.keyboard.press("Escape");
|
||||
return segments;
|
||||
}
|
||||
|
||||
export function locateSelectedSegmentName(page: Page, expectedName: string) {
|
||||
return page.locator('[data-testid="filter-segment-select"]').filter({ hasText: expectedName });
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
listSegments,
|
||||
clearFilters,
|
||||
openSegmentSubmenu,
|
||||
locateSelectedSegmentName,
|
||||
getByTableColumnText,
|
||||
} from "./filter-helpers";
|
||||
import { test } from "./lib/fixtures";
|
||||
@@ -126,6 +127,157 @@ test.describe("Filter Segment Functionality", () => {
|
||||
await deleteSegment(page, segmentName);
|
||||
});
|
||||
|
||||
test("Filter segment preferences persist in database across page reloads", async ({
|
||||
page,
|
||||
users,
|
||||
orgs,
|
||||
}) => {
|
||||
const orgOwner = await users.create(undefined, {
|
||||
hasTeam: true,
|
||||
isOrg: true,
|
||||
});
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
|
||||
const memberUser = await users.create({
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
organizationId: org.id,
|
||||
username: "member-user",
|
||||
});
|
||||
|
||||
const adminUser = await users.create({
|
||||
roleInOrganization: MembershipRole.ADMIN,
|
||||
organizationId: org.id,
|
||||
username: "admin-user",
|
||||
});
|
||||
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto(`/settings/organizations/${org.slug}/members`);
|
||||
|
||||
const dataTable = page.getByTestId("user-list-data-table");
|
||||
await expect(dataTable).toBeVisible();
|
||||
|
||||
await applySelectFilter(page, "role", "admin");
|
||||
const segmentName = "Admin Only";
|
||||
await createFilterSegment(page, segmentName);
|
||||
|
||||
await expect(getByTableColumnText(page, "member", adminUser.email)).toBeVisible();
|
||||
await expect(getByTableColumnText(page, "member", memberUser.email)).toBeHidden();
|
||||
|
||||
await page.goto(`/settings/organizations/${org.slug}/members`);
|
||||
await expect(locateSelectedSegmentName(page, segmentName)).toBeVisible();
|
||||
await expect(dataTable).toBeVisible();
|
||||
|
||||
await expect(getByTableColumnText(page, "member", adminUser.email)).toBeVisible();
|
||||
await expect(getByTableColumnText(page, "member", memberUser.email)).toBeHidden();
|
||||
|
||||
await deleteSegment(page, segmentName);
|
||||
});
|
||||
|
||||
test("Filter segment preferences persist across different browser sessions", async ({
|
||||
browser,
|
||||
page,
|
||||
users,
|
||||
orgs,
|
||||
}) => {
|
||||
const orgOwner = await users.create(undefined, {
|
||||
hasTeam: true,
|
||||
isOrg: true,
|
||||
});
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
|
||||
const adminUser = await users.create({
|
||||
roleInOrganization: MembershipRole.ADMIN,
|
||||
organizationId: org.id,
|
||||
username: "admin-user-session",
|
||||
});
|
||||
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto(`/settings/organizations/${org.slug}/members`);
|
||||
|
||||
await expect(page.getByTestId("user-list-data-table")).toBeVisible();
|
||||
await applySelectFilter(page, "role", "admin");
|
||||
const segmentName = "Cross Session Admins";
|
||||
await createFilterSegment(page, segmentName);
|
||||
|
||||
const [secondContext, secondPage] = await orgOwner.apiLoginOnNewBrowser(browser);
|
||||
await secondPage.goto(`/settings/organizations/${org.slug}/members`);
|
||||
|
||||
const dataTable = secondPage.getByTestId("user-list-data-table");
|
||||
await expect(dataTable).toBeVisible();
|
||||
await expect(locateSelectedSegmentName(secondPage, segmentName)).toBeVisible();
|
||||
await expect(getByTableColumnText(secondPage, "member", adminUser.email)).toBeVisible();
|
||||
|
||||
await deleteSegment(secondPage, segmentName);
|
||||
await secondContext.close();
|
||||
});
|
||||
|
||||
test("Team segment preferences persist in database", async ({ page, users, prisma }) => {
|
||||
const orgOwner = await users.create(undefined, {
|
||||
hasTeam: true,
|
||||
isOrg: true,
|
||||
hasSubteam: true,
|
||||
});
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
const { team: subTeam } = await orgOwner.getFirstTeamMembership();
|
||||
|
||||
const adminUser = await users.create({
|
||||
roleInOrganization: MembershipRole.ADMIN,
|
||||
organizationId: org.id,
|
||||
username: "team-admin",
|
||||
});
|
||||
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto(`/settings/organizations/${org.slug}/members`);
|
||||
|
||||
const dataTable = page.getByTestId("user-list-data-table");
|
||||
await expect(dataTable).toBeVisible();
|
||||
|
||||
await applySelectFilter(page, "role", "admin");
|
||||
const segmentName = "Team Admin";
|
||||
await createFilterSegment(page, segmentName, {
|
||||
teamScope: true,
|
||||
teamName: subTeam.name,
|
||||
});
|
||||
|
||||
await page.goto(`/settings/organizations/${org.slug}/members`);
|
||||
await expect(dataTable).toBeVisible();
|
||||
await expect(locateSelectedSegmentName(page, segmentName)).toBeVisible();
|
||||
|
||||
await expect(getByTableColumnText(page, "member", adminUser.email)).toBeVisible();
|
||||
|
||||
await deleteSegment(page, segmentName);
|
||||
});
|
||||
|
||||
test("Filter segment preferences are isolated per table identifier", async ({ page, users, orgs }) => {
|
||||
const orgOwner = await users.create(undefined, {
|
||||
hasTeam: true,
|
||||
isOrg: true,
|
||||
});
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
|
||||
const adminUser = await users.create({
|
||||
roleInOrganization: MembershipRole.ADMIN,
|
||||
organizationId: org.id,
|
||||
username: "admin-table-isolation",
|
||||
});
|
||||
|
||||
await orgOwner.apiLogin();
|
||||
|
||||
await page.goto(`/settings/organizations/${org.slug}/members`);
|
||||
await expect(page.getByTestId("user-list-data-table")).toBeVisible();
|
||||
|
||||
await applySelectFilter(page, "role", "admin");
|
||||
const membersSegmentName = "Members Table Segment";
|
||||
await createFilterSegment(page, membersSegmentName);
|
||||
|
||||
await page.goto(`/settings/organizations/${org.slug}/members`);
|
||||
await expect(page.getByTestId("user-list-data-table")).toBeVisible();
|
||||
await expect(locateSelectedSegmentName(page, membersSegmentName)).toBeVisible();
|
||||
await expect(getByTableColumnText(page, "member", adminUser.email)).toBeVisible();
|
||||
|
||||
await deleteSegment(page, membersSegmentName);
|
||||
});
|
||||
|
||||
test("Admin can create and use team scope filter segments", async ({ page, users, prisma }) => {
|
||||
const orgOwner = await users.create(undefined, {
|
||||
hasTeam: true,
|
||||
|
||||
@@ -75,6 +75,7 @@ interface DataTableProviderProps {
|
||||
ctaContainerClassName?: string;
|
||||
defaultPageSize?: number;
|
||||
segments?: FilterSegmentOutput[];
|
||||
preferredSegmentId?: number | null;
|
||||
}
|
||||
|
||||
export function DataTableProvider({
|
||||
@@ -84,6 +85,7 @@ export function DataTableProvider({
|
||||
defaultPageSize = DEFAULT_PAGE_SIZE,
|
||||
ctaContainerClassName = CTA_CONTAINER_CLASS_NAME,
|
||||
segments: providedSegments,
|
||||
preferredSegmentId,
|
||||
}: DataTableProviderProps) {
|
||||
const filterToOpen = useRef<string | undefined>(undefined);
|
||||
const [activeFilters, setActiveFilters] = useQueryState("activeFilters", activeFiltersParser);
|
||||
@@ -93,7 +95,10 @@ export function DataTableProvider({
|
||||
columnVisibilityParser
|
||||
);
|
||||
const [columnSizing, setColumnSizing] = useQueryState<ColumnSizingState>("widths", columnSizingParser);
|
||||
const [segmentId, setSegmentId] = useQueryState("segment", segmentIdParser);
|
||||
const [segmentId, setSegmentId] = useQueryState(
|
||||
"segment",
|
||||
segmentIdParser.withDefault(preferredSegmentId ?? -1)
|
||||
);
|
||||
const [pageIndex, setPageIndex] = useQueryState("page", pageIndexParser);
|
||||
const [pageSize, setPageSize] = useQueryState("size", pageSizeParser);
|
||||
const [searchTerm, setSearchTerm] = useQueryState("q", searchTermParser);
|
||||
@@ -185,6 +190,7 @@ export function DataTableProvider({
|
||||
setPageIndex,
|
||||
setSearchTerm,
|
||||
segments: providedSegments,
|
||||
preferredSegmentId,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useCallback, useMemo, useEffect } from "react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
import { recalculateDateRange } from "../lib/dateRange";
|
||||
import { ZSegmentStorage, type UseSegments } from "../lib/types";
|
||||
import { type UseSegments } from "../lib/types";
|
||||
import { isDateRangeFilterValue } from "../lib/utils";
|
||||
|
||||
export const useSegments: UseSegments = ({
|
||||
@@ -27,6 +27,7 @@ export const useSegments: UseSegments = ({
|
||||
setPageIndex,
|
||||
setSearchTerm,
|
||||
segments: providedSegments,
|
||||
preferredSegmentId,
|
||||
}) => {
|
||||
const { data: rawSegments, isFetching: isFetchingSegments } = trpc.viewer.filterSegments.list.useQuery(
|
||||
{
|
||||
@@ -37,10 +38,11 @@ export const useSegments: UseSegments = ({
|
||||
}
|
||||
);
|
||||
|
||||
const { mutate: setPreference } = trpc.viewer.filterSegments.setPreference.useMutation();
|
||||
|
||||
// Recalculate date ranges based on the current timestamp
|
||||
const segments = useMemo(() => {
|
||||
const segmentsSource = providedSegments || rawSegments;
|
||||
|
||||
const segmentsSource = providedSegments || rawSegments?.segments;
|
||||
if (!segmentsSource) return [];
|
||||
|
||||
return segmentsSource.map((segment) => ({
|
||||
@@ -57,10 +59,9 @@ export const useSegments: UseSegments = ({
|
||||
}));
|
||||
}, [rawSegments, providedSegments]);
|
||||
|
||||
const selectedSegment = useMemo(
|
||||
() => segments?.find((segment) => segment.id === segmentId),
|
||||
[segments, segmentId]
|
||||
);
|
||||
const selectedSegment = useMemo(() => {
|
||||
return segments?.find((segment) => segment.id === segmentId);
|
||||
}, [segments, segmentId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (segments && segmentId > 0 && !isFetchingSegments) {
|
||||
@@ -74,16 +75,16 @@ export const useSegments: UseSegments = ({
|
||||
}
|
||||
}, [segments, segmentId, setSegmentId, isFetchingSegments]);
|
||||
|
||||
const memoizedPreferredSegmentId = useMemo(
|
||||
() => preferredSegmentId ?? rawSegments?.preferredSegmentId,
|
||||
[preferredSegmentId, rawSegments]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// this hook doesn't include segmentId in the dependency array
|
||||
// because we want to only run this once, when the component mounts
|
||||
if (segmentId === -1) {
|
||||
const segments = getSegmentsFromLocalStorage();
|
||||
if (segments[tableIdentifier]) {
|
||||
setSegmentId(segments[tableIdentifier].segmentId);
|
||||
}
|
||||
if (memoizedPreferredSegmentId) {
|
||||
setSegmentId(memoizedPreferredSegmentId);
|
||||
}
|
||||
}, [tableIdentifier, setSegmentId]);
|
||||
}, [memoizedPreferredSegmentId, setSegmentId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSegment) {
|
||||
@@ -143,9 +144,12 @@ export const useSegments: UseSegments = ({
|
||||
const setAndPersistSegmentId = useCallback(
|
||||
(segmentId: number | null) => {
|
||||
setSegmentId(segmentId);
|
||||
saveSegmentToLocalStorage({ tableIdentifier, segmentId });
|
||||
setPreference({
|
||||
tableIdentifier,
|
||||
segmentId,
|
||||
});
|
||||
},
|
||||
[tableIdentifier, setSegmentId]
|
||||
[tableIdentifier, setSegmentId, setPreference]
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -156,29 +160,3 @@ export const useSegments: UseSegments = ({
|
||||
isSegmentEnabled: true,
|
||||
};
|
||||
};
|
||||
|
||||
const LOCAL_STORAGE_KEY = "data-table:segments";
|
||||
|
||||
function getSegmentsFromLocalStorage() {
|
||||
try {
|
||||
return ZSegmentStorage.parse(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) ?? "{}"));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveSegmentToLocalStorage({
|
||||
tableIdentifier,
|
||||
segmentId,
|
||||
}: {
|
||||
tableIdentifier: string;
|
||||
segmentId: number | null;
|
||||
}) {
|
||||
const segments = getSegmentsFromLocalStorage();
|
||||
if (segmentId) {
|
||||
segments[tableIdentifier] = { segmentId };
|
||||
} else {
|
||||
delete segments[tableIdentifier];
|
||||
}
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(segments));
|
||||
}
|
||||
@@ -255,6 +255,11 @@ export type FilterSegmentOutput = {
|
||||
team: { id: number; name: string } | null;
|
||||
};
|
||||
|
||||
export type FilterSegmentsListResponse = {
|
||||
segments: FilterSegmentOutput[];
|
||||
preferredSegmentId: number | null;
|
||||
};
|
||||
|
||||
export type SegmentStorage = {
|
||||
[tableIdentifier: string]: {
|
||||
segmentId: number;
|
||||
@@ -289,6 +294,7 @@ export type UseSegmentsProps = {
|
||||
setPageIndex: (pageIndex: number) => void;
|
||||
setSearchTerm: (searchTerm: string | null) => void;
|
||||
segments?: FilterSegmentOutput[];
|
||||
preferredSegmentId?: number | null;
|
||||
};
|
||||
|
||||
export type UseSegmentsReturn = {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
export class FilterSegmentRepository {
|
||||
static async setPreference({
|
||||
userId,
|
||||
tableIdentifier,
|
||||
segmentId,
|
||||
}: {
|
||||
userId: number;
|
||||
tableIdentifier: string;
|
||||
segmentId: number | null;
|
||||
}) {
|
||||
if (segmentId === null) {
|
||||
await prisma.userFilterSegmentPreference.deleteMany({
|
||||
where: {
|
||||
userId,
|
||||
tableIdentifier,
|
||||
},
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const preference = await prisma.userFilterSegmentPreference.upsert({
|
||||
where: {
|
||||
userId_tableIdentifier: {
|
||||
userId,
|
||||
tableIdentifier,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
segmentId,
|
||||
},
|
||||
create: {
|
||||
userId,
|
||||
tableIdentifier,
|
||||
segmentId,
|
||||
},
|
||||
});
|
||||
|
||||
return preference;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserFilterSegmentPreference" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"tableIdentifier" TEXT NOT NULL,
|
||||
"segmentId" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "UserFilterSegmentPreference_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "UserFilterSegmentPreference_userId_idx" ON "UserFilterSegmentPreference"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "UserFilterSegmentPreference_segmentId_idx" ON "UserFilterSegmentPreference"("segmentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UserFilterSegmentPreference_userId_tableIdentifier_key" ON "UserFilterSegmentPreference"("userId", "tableIdentifier");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserFilterSegmentPreference" ADD CONSTRAINT "UserFilterSegmentPreference_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserFilterSegmentPreference" ADD CONSTRAINT "UserFilterSegmentPreference_segmentId_fkey" FOREIGN KEY ("segmentId") REFERENCES "FilterSegment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -377,35 +377,36 @@ model User {
|
||||
bookingRedirectsTo OutOfOfficeEntry[] @relation(name: "toUser")
|
||||
|
||||
// Used to lock the user account
|
||||
locked Boolean @default(false)
|
||||
locked Boolean @default(false)
|
||||
platformOAuthClients PlatformOAuthClient[]
|
||||
AccessToken AccessToken[]
|
||||
RefreshToken RefreshToken[]
|
||||
PlatformAuthorizationToken PlatformAuthorizationToken[]
|
||||
profiles Profile[]
|
||||
movedToProfileId Int?
|
||||
movedToProfile Profile? @relation("moved_to_profile", fields: [movedToProfileId], references: [id], onDelete: SetNull)
|
||||
movedToProfile Profile? @relation("moved_to_profile", fields: [movedToProfileId], references: [id], onDelete: SetNull)
|
||||
secondaryEmails SecondaryEmail[]
|
||||
isPlatformManaged Boolean @default(false)
|
||||
isPlatformManaged Boolean @default(false)
|
||||
OutOfOfficeReasons OutOfOfficeReason[]
|
||||
smsLockState SMSLockState @default(UNLOCKED)
|
||||
smsLockReviewedByAdmin Boolean @default(false)
|
||||
smsLockState SMSLockState @default(UNLOCKED)
|
||||
smsLockReviewedByAdmin Boolean @default(false)
|
||||
NotificationsSubscriptions NotificationsSubscriptions[]
|
||||
referralLinkId String?
|
||||
features UserFeatures[]
|
||||
reassignedBookings Booking[] @relation("reassignByUser")
|
||||
createdAttributeToUsers AttributeToUser[] @relation("createdBy")
|
||||
updatedAttributeToUsers AttributeToUser[] @relation("updatedBy")
|
||||
createdTranslations EventTypeTranslation[] @relation("CreatedEventTypeTranslations")
|
||||
updatedTranslations EventTypeTranslation[] @relation("UpdatedEventTypeTranslations")
|
||||
createdWatchlists Watchlist[] @relation("CreatedWatchlists")
|
||||
updatedWatchlists Watchlist[] @relation("UpdatedWatchlists")
|
||||
reassignedBookings Booking[] @relation("reassignByUser")
|
||||
createdAttributeToUsers AttributeToUser[] @relation("createdBy")
|
||||
updatedAttributeToUsers AttributeToUser[] @relation("updatedBy")
|
||||
createdTranslations EventTypeTranslation[] @relation("CreatedEventTypeTranslations")
|
||||
updatedTranslations EventTypeTranslation[] @relation("UpdatedEventTypeTranslations")
|
||||
createdWatchlists Watchlist[] @relation("CreatedWatchlists")
|
||||
updatedWatchlists Watchlist[] @relation("UpdatedWatchlists")
|
||||
BookingInternalNote BookingInternalNote[]
|
||||
creationSource CreationSource?
|
||||
createdOrganizationOnboardings OrganizationOnboarding[] @relation("CreatedOrganizationOnboardings")
|
||||
createdOrganizationOnboardings OrganizationOnboarding[] @relation("CreatedOrganizationOnboardings")
|
||||
filterSegments FilterSegment[]
|
||||
filterSegmentPreferences UserFilterSegmentPreference[]
|
||||
creditBalance CreditBalance?
|
||||
whitelistWorkflows Boolean @default(false)
|
||||
whitelistWorkflows Boolean @default(false)
|
||||
|
||||
@@unique([email])
|
||||
@@unique([email, username])
|
||||
@@ -2231,7 +2232,7 @@ enum FilterSegmentScope {
|
||||
}
|
||||
|
||||
model FilterSegment {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
// Identifies which data table this segment belongs to (e.g. "organization_members", "team_members", "bookings", etc.)
|
||||
tableIdentifier String
|
||||
@@ -2242,15 +2243,16 @@ model FilterSegment {
|
||||
columnVisibility Json?
|
||||
columnSizing Json?
|
||||
perPage Int
|
||||
searchTerm String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
searchTerm String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
// Creator of the segment
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
// Team scope - optional, only set when scope is TEAM
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
teamId Int?
|
||||
userPreferences UserFilterSegmentPreference[]
|
||||
|
||||
// For user-scoped segments: scope + userId + tableIdentifier
|
||||
@@index([scope, userId, tableIdentifier])
|
||||
@@ -2258,6 +2260,22 @@ model FilterSegment {
|
||||
@@index([scope, teamId, tableIdentifier])
|
||||
}
|
||||
|
||||
model UserFilterSegmentPreference {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
tableIdentifier String
|
||||
segmentId Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
segment FilterSegment @relation(fields: [segmentId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, tableIdentifier])
|
||||
@@index([userId])
|
||||
@@index([segmentId])
|
||||
}
|
||||
|
||||
model BookingInternalNote {
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import prismock from "../../../../../../../tests/libs/__mocks__/prisma";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
|
||||
|
||||
import { listHandler } from "../list.handler";
|
||||
import { type TListFilterSegmentsInputSchema } from "../list.schema";
|
||||
@@ -53,8 +53,8 @@ describe("listHandler", () => {
|
||||
input,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual(
|
||||
expect(result.segments).toHaveLength(2);
|
||||
expect(result.segments).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "My Other Bookings",
|
||||
@@ -137,8 +137,8 @@ describe("listHandler", () => {
|
||||
input,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual(
|
||||
expect(result.segments).toHaveLength(2);
|
||||
expect(result.segments).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "Other Team Bookings",
|
||||
@@ -221,8 +221,8 @@ describe("listHandler", () => {
|
||||
input,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual(
|
||||
expect(result.segments).toHaveLength(2);
|
||||
expect(result.segments).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "My Bookings",
|
||||
@@ -256,6 +256,9 @@ describe("listHandler", () => {
|
||||
input,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(result).toEqual({
|
||||
segments: [],
|
||||
preferredSegmentId: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { router } from "../../../trpc";
|
||||
import { ZCreateFilterSegmentInputSchema } from "./create.schema";
|
||||
import { ZDeleteFilterSegmentInputSchema } from "./delete.schema";
|
||||
import { ZListFilterSegmentsInputSchema } from "./list.schema";
|
||||
import { ZSetFilterSegmentPreferenceInputSchema } from "./preference.schema";
|
||||
import { ZUpdateFilterSegmentInputSchema } from "./update.schema";
|
||||
|
||||
type FilterSegmentsRouterHandlerCache = {
|
||||
@@ -48,4 +49,15 @@ export const filterSegmentsRouter = router({
|
||||
input,
|
||||
});
|
||||
}),
|
||||
|
||||
setPreference: authedProcedure
|
||||
.input(ZSetFilterSegmentPreferenceInputSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { setPreferenceHandler } = await import("./preference.handler");
|
||||
|
||||
return setPreferenceHandler({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -76,5 +76,20 @@ export const listHandler = async ({
|
||||
columnSizing: ZColumnSizing.catch({}).parse(segment.columnSizing),
|
||||
}));
|
||||
|
||||
return parsedSegments;
|
||||
const preference = await prisma.userFilterSegmentPreference.findUnique({
|
||||
where: {
|
||||
userId_tableIdentifier: {
|
||||
userId,
|
||||
tableIdentifier,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
segmentId: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
segments: parsedSegments,
|
||||
preferredSegmentId: preference?.segmentId || null,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { FilterSegmentRepository } from "@calcom/lib/server/repository/filterSegment";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
|
||||
|
||||
import type { TSetFilterSegmentPreferenceInputSchema } from "./preference.schema";
|
||||
|
||||
export const setPreferenceHandler = async ({
|
||||
ctx,
|
||||
input,
|
||||
}: {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
};
|
||||
input: TSetFilterSegmentPreferenceInputSchema;
|
||||
}) => {
|
||||
const { tableIdentifier, segmentId } = input;
|
||||
const userId = ctx.user.id;
|
||||
|
||||
return await FilterSegmentRepository.setPreference({
|
||||
userId,
|
||||
tableIdentifier,
|
||||
segmentId,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZSetFilterSegmentPreferenceInputSchema = z.object({
|
||||
tableIdentifier: z.string(),
|
||||
segmentId: z.number().nullable(),
|
||||
});
|
||||
|
||||
export type TSetFilterSegmentPreferenceInputSchema = z.infer<typeof ZSetFilterSegmentPreferenceInputSchema>;
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { FilterSegmentOutput } from "@calcom/features/data-table/lib/types";
|
||||
|
||||
export type { SortingState, ColumnSizingState, VisibilityState } from "@tanstack/react-table";
|
||||
export {
|
||||
type ActiveFilter,
|
||||
@@ -7,3 +9,8 @@ export {
|
||||
ZColumnSizing,
|
||||
ZColumnVisibility,
|
||||
} from "@calcom/features/data-table/lib/types";
|
||||
|
||||
export type FilterSegmentsListResponse = {
|
||||
segments: FilterSegmentOutput[];
|
||||
preferredSegmentId: number | null;
|
||||
};
|
||||
Reference in new issue
Block a user