feat: support searchTerm in Filter Segment (#20526)

* feat: support searchTerm in Filter Segment

* update tests

* save and load searchTerm

* fix types

* add missing condition
This commit is contained in:
Eunjae Lee
2025-04-07 05:27:58 -03:00
committed by GitHub
parent b6aedb830b
commit 703e20a43a
12 changed files with 133 additions and 45 deletions
@@ -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(
@@ -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<HTMLInputEle
const { t } = useLocale();
const [localValue, setLocalValue] = useState(searchTerm);
useEffect(() => {
setLocalValue(searchTerm);
}, [searchTerm]);
const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setLocalValue(value);
@@ -48,7 +52,7 @@ function SearchBarComponent({ className }: SearchBarProps, ref: Ref<HTMLInputEle
return (
<Input
ref={ref}
className={classNames("max-w-48" ,className)}
className={classNames("max-w-48", className)}
placeholder={t("search")}
value={localValue}
onChange={handleSearchChange}
@@ -52,6 +52,7 @@ export function SaveFilterSegmentButton() {
canSaveSegment,
setSegmentId,
pageSize,
searchTerm,
} = useDataTable();
const [saveMode, setSaveMode] = useState<"create" | "update">(() =>
@@ -105,6 +106,7 @@ export function SaveFilterSegmentButton() {
columnVisibility,
columnSizing,
perPage: pageSize,
searchTerm,
};
if (saveMode === "update") {
+20 -3
View File
@@ -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) => {
@@ -247,6 +247,7 @@ export type FilterSegmentOutput = {
columnVisibility: Record<string, boolean>;
columnSizing: Record<string, number>;
perPage: number;
searchTerm: string | null;
createdAt: Date;
updatedAt: Date;
userId: number;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "FilterSegment" ADD COLUMN "searchTerm" TEXT;
+1
View File
@@ -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
@@ -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",
})
);
});
@@ -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<TrpcSessionUser>;
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: {},
@@ -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),
};
@@ -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,
},
});
@@ -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(),
};