Files
calendar/packages/features/data-table/lib/segments.ts
T
Eunjae LeeandGitHub f15d347804 feat: implement filter segment (#20066)
* feat: add filter segment

* add tests

* add ui

* provide tableIdentifier from context

* fix components

* update save button

* try to fix router

* add proper implementation

* update icon

* remove segmentId if invalid

* fix tests

* fix type errors

* fix type errors

* fix unit test

* clean up

* minor fixes

* clean up types

* avoid barrel imports

* fix imports

* minor improvements

* fix type error

* fix radio import

* fix little issues

* menu permission

* update icon snapshot

* fix: remember last used filter segment (#20261)

* fix: remember last used filter segment

* extract segments logic as a separate file

* remove unnecessary query

* apply feedback

* deep comparison
2025-03-31 08:53:48 +00:00

159 lines
4.9 KiB
TypeScript

import type { SortingState, VisibilityState, ColumnSizingState } from "@tanstack/react-table";
// eslint-disable-next-line no-restricted-imports
import { isEqual } from "lodash";
import { useCallback, useMemo, useEffect } from "react";
import { trpc } from "@calcom/trpc/react";
import { type ActiveFilters, ZSegmentStorage } from "./types";
type UseSegmentsProps = {
tableIdentifier: string;
activeFilters: ActiveFilters;
sorting: SortingState;
columnVisibility: VisibilityState;
columnSizing: ColumnSizingState;
pageSize: number;
defaultPageSize: number;
segmentId: number;
setSegmentId: (segmentId: number | null) => void;
setActiveFilters: (activeFilters: ActiveFilters) => void;
setSorting: (sorting: SortingState) => void;
setColumnVisibility: (columnVisibility: VisibilityState) => void;
setColumnSizing: (columnSizing: ColumnSizingState) => void;
setPageSize: (pageSize: number) => void;
setPageIndex: (pageIndex: number) => void;
};
export function useSegments({
tableIdentifier,
activeFilters,
sorting,
columnVisibility,
columnSizing,
pageSize,
defaultPageSize,
segmentId,
setSegmentId,
setActiveFilters,
setSorting,
setColumnVisibility,
setColumnSizing,
setPageSize,
setPageIndex,
}: UseSegmentsProps) {
const { data: segments, isFetching: isFetchingSegments } = trpc.viewer.filterSegments.list.useQuery({
tableIdentifier,
});
const selectedSegment = useMemo(
() => segments?.find((segment) => segment.id === segmentId),
[segments, segmentId]
);
useEffect(() => {
if (segments && segmentId > 0 && !isFetchingSegments) {
const segment = segments.find((segment) => segment.id === segmentId);
if (!segment) {
// If segmentId is invalid (or not found), clear the segmentId from the query params,
// but we still keep all the other states like activeFilters, etc.
// This is useful when someone shares a URL that is inaccessible to someone else.
setSegmentId(null);
}
}
}, [segments, segmentId, setSegmentId, isFetchingSegments]);
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);
}
}
}, [tableIdentifier, setSegmentId]);
useEffect(() => {
if (selectedSegment) {
// segment is selected, so we apply the filters, sorting, etc. from the segment
setActiveFilters(selectedSegment.activeFilters);
setSorting(selectedSegment.sorting);
setColumnVisibility(selectedSegment.columnVisibility);
setColumnSizing(selectedSegment.columnSizing);
setPageSize(selectedSegment.perPage);
setPageIndex(0);
}
}, [
selectedSegment,
setActiveFilters,
setSorting,
setColumnVisibility,
setColumnSizing,
setPageSize,
setPageIndex,
]);
const canSaveSegment = useMemo(() => {
if (!selectedSegment) {
// if no segment is selected, we can save the segment if there are any active filters, sorting, etc.
return (
activeFilters.length > 0 ||
sorting.length > 0 ||
Object.keys(columnVisibility).length > 0 ||
Object.keys(columnSizing).length > 0 ||
pageSize !== defaultPageSize
);
} else {
// if a segment is selected, we can save the segment if the active filters, sorting, etc. are different from the segment
return (
!isEqual(activeFilters, selectedSegment.activeFilters) ||
!isEqual(sorting, selectedSegment.sorting) ||
!isEqual(columnVisibility, selectedSegment.columnVisibility) ||
!isEqual(columnSizing, selectedSegment.columnSizing) ||
!isEqual(pageSize, selectedSegment.perPage)
);
}
}, [selectedSegment, activeFilters, sorting, columnVisibility, columnSizing, pageSize, defaultPageSize]);
const setSegmentIdAndSaveToLocalStorage = useCallback(
(segmentId: number | null) => {
setSegmentId(segmentId);
saveSegmentToLocalStorage({ tableIdentifier, segmentId });
},
[tableIdentifier, setSegmentId]
);
return {
segments: segments ?? [],
selectedSegment,
canSaveSegment,
setSegmentIdAndSaveToLocalStorage,
};
}
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));
}