diff --git a/packages/features/data-table/DataTableProvider.tsx b/packages/features/data-table/DataTableProvider.tsx index 23384dea60..b41760b12a 100644 --- a/packages/features/data-table/DataTableProvider.tsx +++ b/packages/features/data-table/DataTableProvider.tsx @@ -172,6 +172,7 @@ export function DataTableProvider({ columnVisibility, columnSizing, pageSize, + searchTerm, defaultPageSize, segmentId, setSegmentId, @@ -181,6 +182,7 @@ export function DataTableProvider({ setColumnSizing, setPageSize, setPageIndex, + setSearchTerm, }); const clearAll = useCallback( diff --git a/packages/features/data-table/components/DataTableToolbar.tsx b/packages/features/data-table/components/DataTableToolbar.tsx index 2e324f466f..d5ad8bcdf9 100644 --- a/packages/features/data-table/components/DataTableToolbar.tsx +++ b/packages/features/data-table/components/DataTableToolbar.tsx @@ -1,7 +1,7 @@ "use client"; import type { Table } from "@tanstack/react-table"; -import { forwardRef } from "react"; +import { forwardRef, useEffect } from "react"; import type { ComponentPropsWithoutRef } from "react"; import { useState, type Ref, type ChangeEvent } from "react"; @@ -39,6 +39,10 @@ function SearchBarComponent({ className }: SearchBarProps, ref: Ref { + setLocalValue(searchTerm); + }, [searchTerm]); + const handleSearchChange = (event: ChangeEvent) => { const value = event.target.value; setLocalValue(value); @@ -48,7 +52,7 @@ function SearchBarComponent({ className }: SearchBarProps, ref: Ref(() => @@ -105,6 +106,7 @@ export function SaveFilterSegmentButton() { columnVisibility, columnSizing, perPage: pageSize, + searchTerm, }; if (saveMode === "update") { diff --git a/packages/features/data-table/lib/segments.ts b/packages/features/data-table/lib/segments.ts index 88876f5517..21a45bf54b 100644 --- a/packages/features/data-table/lib/segments.ts +++ b/packages/features/data-table/lib/segments.ts @@ -14,6 +14,7 @@ type UseSegmentsProps = { columnVisibility: VisibilityState; columnSizing: ColumnSizingState; pageSize: number; + searchTerm: string; defaultPageSize: number; segmentId: number; setSegmentId: (segmentId: number | null) => void; @@ -23,6 +24,7 @@ type UseSegmentsProps = { setColumnSizing: (columnSizing: ColumnSizingState) => void; setPageSize: (pageSize: number) => void; setPageIndex: (pageIndex: number) => void; + setSearchTerm: (searchTerm: string | null) => void; }; export function useSegments({ @@ -32,6 +34,7 @@ export function useSegments({ columnVisibility, columnSizing, pageSize, + searchTerm, defaultPageSize, segmentId, setSegmentId, @@ -41,6 +44,7 @@ export function useSegments({ setColumnSizing, setPageSize, setPageIndex, + setSearchTerm, }: UseSegmentsProps) { const { data: segments, isFetching: isFetchingSegments } = trpc.viewer.filterSegments.list.useQuery({ tableIdentifier, @@ -81,6 +85,7 @@ export function useSegments({ setColumnVisibility(selectedSegment.columnVisibility); setColumnSizing(selectedSegment.columnSizing); setPageSize(selectedSegment.perPage); + setSearchTerm(selectedSegment.searchTerm); setPageIndex(0); } }, [ @@ -91,6 +96,7 @@ export function useSegments({ setColumnSizing, setPageSize, setPageIndex, + setSearchTerm, ]); const canSaveSegment = useMemo(() => { @@ -101,7 +107,8 @@ export function useSegments({ sorting.length > 0 || Object.keys(columnVisibility).length > 0 || Object.keys(columnSizing).length > 0 || - pageSize !== defaultPageSize + pageSize !== defaultPageSize || + searchTerm?.length > 0 ); } else { // if a segment is selected, we can save the segment if the active filters, sorting, etc. are different from the segment @@ -110,10 +117,20 @@ export function useSegments({ !isEqual(sorting, selectedSegment.sorting) || !isEqual(columnVisibility, selectedSegment.columnVisibility) || !isEqual(columnSizing, selectedSegment.columnSizing) || - !isEqual(pageSize, selectedSegment.perPage) + !isEqual(pageSize, selectedSegment.perPage) || + !isEqual(searchTerm, selectedSegment.searchTerm) ); } - }, [selectedSegment, activeFilters, sorting, columnVisibility, columnSizing, pageSize, defaultPageSize]); + }, [ + selectedSegment, + activeFilters, + sorting, + columnVisibility, + columnSizing, + pageSize, + searchTerm, + defaultPageSize, + ]); const setSegmentIdAndSaveToLocalStorage = useCallback( (segmentId: number | null) => { diff --git a/packages/features/data-table/lib/types.ts b/packages/features/data-table/lib/types.ts index 65737c42db..b82287f9a7 100644 --- a/packages/features/data-table/lib/types.ts +++ b/packages/features/data-table/lib/types.ts @@ -247,6 +247,7 @@ export type FilterSegmentOutput = { columnVisibility: Record; columnSizing: Record; perPage: number; + searchTerm: string | null; createdAt: Date; updatedAt: Date; userId: number; diff --git a/packages/prisma/migrations/20250403094343_add_search_term_to_filter_segment/migration.sql b/packages/prisma/migrations/20250403094343_add_search_term_to_filter_segment/migration.sql new file mode 100644 index 0000000000..130c7f869a --- /dev/null +++ b/packages/prisma/migrations/20250403094343_add_search_term_to_filter_segment/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "FilterSegment" ADD COLUMN "searchTerm" TEXT; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index b8d6ca88a4..f74477dea2 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -2028,6 +2028,7 @@ model FilterSegment { columnVisibility Json? columnSizing Json? perPage Int + searchTerm String? @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Creator of the segment diff --git a/packages/trpc/server/routers/viewer/filterSegments/__tests__/create.handler.test.ts b/packages/trpc/server/routers/viewer/filterSegments/__tests__/create.handler.test.ts index 02a3696d6a..c87e236907 100644 --- a/packages/trpc/server/routers/viewer/filterSegments/__tests__/create.handler.test.ts +++ b/packages/trpc/server/routers/viewer/filterSegments/__tests__/create.handler.test.ts @@ -2,8 +2,9 @@ import prismock from "../../../../../../../tests/libs/__mocks__/prisma"; import { describe, expect, it } from "vitest"; +import { ColumnFilterType } from "@calcom/features/data-table/lib/types"; import { MembershipRole } from "@calcom/prisma/enums"; -import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; +import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { createHandler } from "../create.handler"; import { type TCreateFilterSegmentInputSchema } from "../create.schema"; @@ -16,11 +17,20 @@ describe("createHandler", () => { const baseInput = { tableIdentifier: "bookings", - activeFilters: { f: "status" }, + activeFilters: [ + { + f: "status", + v: { + type: ColumnFilterType.SINGLE_SELECT, + data: "active", + }, + }, + ], sorting: [{ id: "date", desc: true }], columnVisibility: {}, columnSizing: {}, perPage: 10, + searchTerm: "test search", }; it("should create a user-scoped filter segment", async () => { @@ -41,6 +51,7 @@ describe("createHandler", () => { scope: "USER", tableIdentifier: "bookings", name: "My Bookings", + searchTerm: "test search", }) ); }); @@ -74,6 +85,7 @@ describe("createHandler", () => { scope: "TEAM", tableIdentifier: "bookings", name: "Team Bookings", + searchTerm: "test search", }) ); }); diff --git a/packages/trpc/server/routers/viewer/filterSegments/__tests__/update.handler.test.ts b/packages/trpc/server/routers/viewer/filterSegments/__tests__/update.handler.test.ts index 7267def8ed..73f45aae7c 100644 --- a/packages/trpc/server/routers/viewer/filterSegments/__tests__/update.handler.test.ts +++ b/packages/trpc/server/routers/viewer/filterSegments/__tests__/update.handler.test.ts @@ -2,8 +2,9 @@ import prismock from "../../../../../../../tests/libs/__mocks__/prisma"; import { describe, expect, it } from "vitest"; +import { ColumnFilterType } from "@calcom/features/data-table/lib/types"; import { MembershipRole } from "@calcom/prisma/enums"; -import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; +import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { updateHandler } from "../update.handler"; import { type TUpdateFilterSegmentInputSchema } from "../update.schema"; @@ -14,31 +15,50 @@ describe("updateHandler", () => { name: "Test User", } as NonNullable; + const baseInput = { + tableIdentifier: "bookings", + activeFilters: [ + { + f: "status", + v: { + type: ColumnFilterType.SINGLE_SELECT, + data: "active", + }, + }, + ], + sorting: [{ id: "date", desc: true }], + columnVisibility: {}, + columnSizing: {}, + perPage: 10, + searchTerm: "test search", + }; + it("should update a user-scoped filter segment", async () => { // Create a user-scoped segment first const segment = await prismock.filterSegment.create({ data: { + ...baseInput, userId: mockUser.id, scope: "USER", - tableIdentifier: "bookings", name: "My Bookings", - activeFilters: { f: "status" }, - sorting: [{ id: "date", desc: true }], - columnVisibility: {}, - columnSizing: {}, - perPage: 10, }, }); const input: TUpdateFilterSegmentInputSchema = { + ...baseInput, id: segment.id, scope: "USER", name: "Updated Bookings", - activeFilters: { f: "status" }, - sorting: [{ id: "date", desc: true }], - columnVisibility: {}, - columnSizing: {}, - perPage: 20, + searchTerm: "updated search", + activeFilters: [ + { + f: "status", + v: { + type: ColumnFilterType.SINGLE_SELECT, + data: "inactive", + }, + }, + ], }; const result = await updateHandler({ @@ -48,13 +68,17 @@ describe("updateHandler", () => { expect(result).toEqual( expect.objectContaining({ - id: segment.id, + userId: mockUser.id, + scope: "USER", + tableIdentifier: "bookings", name: "Updated Bookings", - activeFilters: { f: "status" }, - sorting: [{ id: "date", desc: true }], - columnVisibility: {}, - columnSizing: {}, - perPage: 20, + searchTerm: "updated search", + activeFilters: [ + { + f: "status", + v: { type: ColumnFilterType.SINGLE_SELECT, data: "inactive" }, + }, + ], }) ); }); @@ -81,16 +105,11 @@ describe("updateHandler", () => { // Create a team-scoped segment const segment = await prismock.filterSegment.create({ data: { + ...baseInput, userId: mockUser.id, teamId: team.id, scope: "TEAM", - tableIdentifier: "bookings", name: "Team Bookings", - activeFilters: { f: "status" }, - sorting: [{ id: "date", desc: true }], - columnVisibility: {}, - columnSizing: {}, - perPage: 10, }, }); @@ -99,11 +118,16 @@ describe("updateHandler", () => { scope: "TEAM", teamId: team.id, name: "Updated Team Bookings", - activeFilters: { f: "status" }, - sorting: [{ id: "date", desc: false }], - columnVisibility: {}, - columnSizing: {}, - perPage: 15, + searchTerm: "updated search", + activeFilters: [ + { + f: "status", + v: { + type: ColumnFilterType.SINGLE_SELECT, + data: "inactive", + }, + }, + ], }; const result = await updateHandler({ @@ -114,12 +138,17 @@ describe("updateHandler", () => { expect(result).toEqual( expect.objectContaining({ id: segment.id, + teamId: team.id, + scope: "TEAM", + tableIdentifier: "bookings", name: "Updated Team Bookings", - activeFilters: { f: "status" }, - sorting: [{ id: "date", desc: false }], - columnVisibility: {}, - columnSizing: {}, - perPage: 15, + searchTerm: "updated search", + activeFilters: [ + { + f: "status", + v: { type: ColumnFilterType.SINGLE_SELECT, data: "inactive" }, + }, + ], }) ); }); @@ -129,7 +158,12 @@ describe("updateHandler", () => { id: 999, scope: "USER", name: "Updated Bookings", - activeFilters: { f: "status" }, + activeFilters: [ + { + f: "status", + v: { type: ColumnFilterType.SINGLE_SELECT, data: "inactive" }, + }, + ], sorting: [{ id: "date", desc: true }], columnVisibility: {}, columnSizing: {}, @@ -171,7 +205,12 @@ describe("updateHandler", () => { scope: "TEAM", tableIdentifier: "bookings", name: "Team Bookings", - activeFilters: { f: "status" }, + activeFilters: [ + { + f: "status", + v: { type: ColumnFilterType.SINGLE_SELECT, data: "active" }, + }, + ], sorting: [{ id: "date", desc: true }], columnVisibility: {}, columnSizing: {}, @@ -184,7 +223,12 @@ describe("updateHandler", () => { scope: "TEAM", teamId: team.id, name: "Updated Team Bookings", - activeFilters: { f: "status" }, + activeFilters: [ + { + f: "status", + v: { type: ColumnFilterType.SINGLE_SELECT, data: "inactive" }, + }, + ], sorting: [{ id: "date", desc: true }], columnVisibility: {}, columnSizing: {}, diff --git a/packages/trpc/server/routers/viewer/filterSegments/create.schema.ts b/packages/trpc/server/routers/viewer/filterSegments/create.schema.ts index 92c44efe3f..447230f194 100644 --- a/packages/trpc/server/routers/viewer/filterSegments/create.schema.ts +++ b/packages/trpc/server/routers/viewer/filterSegments/create.schema.ts @@ -10,6 +10,7 @@ const baseCreateSchema = { sorting: ZSortingState.optional(), columnVisibility: ZColumnVisibility.optional(), columnSizing: ZColumnSizing.optional(), + searchTerm: z.string().nullable().optional(), perPage: z.number().int().min(1), }; diff --git a/packages/trpc/server/routers/viewer/filterSegments/update.handler.ts b/packages/trpc/server/routers/viewer/filterSegments/update.handler.ts index ef5c7561b1..db71b25725 100644 --- a/packages/trpc/server/routers/viewer/filterSegments/update.handler.ts +++ b/packages/trpc/server/routers/viewer/filterSegments/update.handler.ts @@ -14,7 +14,7 @@ export const updateHandler = async ({ }; input: TUpdateFilterSegmentInputSchema; }) => { - const { id, name, activeFilters, sorting, columnVisibility, columnSizing, perPage } = input; + const { id, name, activeFilters, sorting, columnVisibility, columnSizing, perPage, searchTerm } = input; const userId = ctx.user.id; // First, fetch the existing segment to check permissions @@ -64,6 +64,7 @@ export const updateHandler = async ({ columnVisibility, columnSizing, perPage, + searchTerm, }, }); diff --git a/packages/trpc/server/routers/viewer/filterSegments/update.schema.ts b/packages/trpc/server/routers/viewer/filterSegments/update.schema.ts index 6a9ea2bc9b..7bc4fbf993 100644 --- a/packages/trpc/server/routers/viewer/filterSegments/update.schema.ts +++ b/packages/trpc/server/routers/viewer/filterSegments/update.schema.ts @@ -10,6 +10,7 @@ const baseUpdateSchema = { sorting: ZSortingState.optional(), columnVisibility: ZColumnVisibility.optional(), columnSizing: ZColumnSizing.optional(), + searchTerm: z.string().nullable().optional(), perPage: z.number().int().min(1).optional(), };