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:
@@ -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") {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "FilterSegment" ADD COLUMN "searchTerm" TEXT;
|
||||
@@ -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
|
||||
|
||||
+14
-2
@@ -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",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
+81
-37
@@ -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(),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user