Files
calendar/packages/features/data-table/lib/utils.ts
T
a7e7561efa feat: add text & numeric filters to insights routing responses (#18016)
* feat: improve text filters (WIP)

* move function to bottom

* apply some styles

* fix selection of TextFilterOptions

* rename value to operand

* remove unused file

* merge filters/filters into filters/utils

* fix regression of not putting url params correctly

* move makeWhereClause to filters/utils

* fix negative, empty, and not empty operators

* fix initial filtering from search state (url)

* fix type errors

* do not send an empty array to query

* update yarn.lock

* i18n for text filter operators

* extract logic as useColumnFilters()

* add missing import

* fix type error

* revert yarn.lock

* use i18n

* insensitive text match

* move data-table to @calcom/features

* fix type errors

* fix type errors

* fix type errors

* feat: support DataTable filters for Insights Routing WIP

* remove unused filters

* remove additionalFilters and fix types

* clean up filter components

* support icons for ActiveFilters

* support filters on json

* fix filter ui

* fix type error and clean up

* revert changes

* revert change

* clean up

* revert change

* fix compatibility with insights booking page

* remove unused params

* fix type errors

* update yarn.lock

* fix field filter and adjust ui

* chore: update yarn.lock

* fix text filter

* add Clear Filters button

* add more test data

* feat: support numeric filter WIP

* feat: support numeric filter

* feat: move People to an external filter

* fixing query states

* rename state to activeFilters

* add useDataTable

* fix ResponseCellValue bug and enable resizing for RoutingFormResponsesTable

* fix response values

* truncate attribute text

* seed booking attendees

* fix accessor (no actual change though)

* add a gap

* rename

* use pathname as default identifier for DataTable

* fix type error

* support text filter for assignment reason

* Apply suggestions from code review

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* rename fields to headers

* use safeParse

* fix type error

* push yarn.lock with faker

* fix MemberList

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: sean-brydon <sean@cal.com>
2024-12-12 16:06:30 +00:00

156 lines
5.0 KiB
TypeScript

"use client";
import { useMemo, useContext } from "react";
import type { z } from "zod";
import { DataTableContext } from "./context";
import type {
SelectFilterValue,
TextFilterValue,
FilterValue,
NumberFilterValue,
ColumnFilter,
} from "./types";
import { ZFilterValue, ZNumberFilterValue, ZSelectFilterValue, ZTextFilterValue } from "./types";
export function useDataTable() {
const context = useContext(DataTableContext);
if (!context) {
throw new Error("useDataTable must be used within a DataTableProvider");
}
return context;
}
export function useFilterValue<T>(columnId: string, schema: z.ZodType<T>) {
const { activeFilters } = useDataTable();
return useMemo(() => {
const value = activeFilters.find((filter) => filter.f === columnId)?.v;
if (schema && value) {
const result = schema.safeParse(value);
if (result.success) {
return result.data;
}
}
return undefined;
}, [activeFilters, columnId, schema]);
}
export function useColumnFilters(): ColumnFilter[] {
const { activeFilters } = useDataTable();
return useMemo(() => {
return (activeFilters || [])
.filter(
(filter) =>
typeof filter === "object" && filter && "f" in filter && "v" in filter && filter.v !== undefined
)
.map((filter) => {
const parsedValue = ZFilterValue.safeParse(filter.v);
if (!parsedValue.success) return null;
return {
id: filter.f,
value: parsedValue.data,
};
})
.filter((filter): filter is ColumnFilter => filter !== null)
.filter((filter) => {
// The empty arrays in `filtersSearchState` keep the filter UI component,
// but we do not send them to the actual query.
// Otherwise, `{ my_column_name: { in: []} }` would result in nothing being returned.
if (isSelectFilterValue(filter.value) && filter.value.length === 0) {
return false;
}
return true;
});
}, [activeFilters]);
}
export const textFilter = (cellValue: unknown, filterValue: TextFilterValue) => {
if (filterValue.data.operator === "isEmpty" && cellValue === undefined) {
return true;
}
if (typeof cellValue !== "string") {
return false;
}
switch (filterValue.data.operator) {
case "equals":
return cellValue.toLowerCase() === (filterValue.data.operand || "").toLowerCase();
case "notEquals":
return cellValue.toLowerCase() !== (filterValue.data.operand || "").toLowerCase();
case "contains":
return cellValue.toLowerCase().includes((filterValue.data.operand || "").toLowerCase());
case "notContains":
return !cellValue.toLowerCase().includes((filterValue.data.operand || "").toLowerCase());
case "startsWith":
return cellValue.toLowerCase().startsWith((filterValue.data.operand || "").toLowerCase());
case "endsWith":
return cellValue.toLowerCase().endsWith((filterValue.data.operand || "").toLowerCase());
case "isEmpty":
return cellValue.trim() === "";
case "isNotEmpty":
return cellValue.trim() !== "";
default:
return false;
}
};
export const isTextFilterValue = (filterValue: unknown): filterValue is TextFilterValue => {
return ZTextFilterValue.safeParse(filterValue).success;
};
export const selectFilter = (cellValue: unknown | undefined, filterValue: SelectFilterValue) => {
const cellValueArray = Array.isArray(cellValue) ? cellValue : [cellValue];
if (!cellValueArray.every((value) => typeof value === "string")) {
return false;
}
return filterValue.length === 0 ? true : cellValueArray.some((v) => filterValue.includes(v));
};
export const isSelectFilterValue = (filterValue: unknown): filterValue is SelectFilterValue => {
return ZSelectFilterValue.safeParse(filterValue).success;
};
export const numberFilter = (cellValue: unknown, filterValue: NumberFilterValue) => {
if (typeof cellValue !== "number") {
return false;
}
switch (filterValue.data.operator) {
case "eq":
return cellValue === filterValue.data.operand;
case "neq":
return cellValue !== filterValue.data.operand;
case "gt":
return cellValue > filterValue.data.operand;
case "gte":
return cellValue >= filterValue.data.operand;
case "lt":
return cellValue < filterValue.data.operand;
case "lte":
return cellValue <= filterValue.data.operand;
}
return false;
};
export const isNumberFilterValue = (filterValue: unknown): filterValue is NumberFilterValue => {
return ZNumberFilterValue.safeParse(filterValue).success;
};
export const dataTableFilter = (cellValue: unknown, filterValue: FilterValue) => {
if (isSelectFilterValue(filterValue)) {
return selectFilter(cellValue, filterValue);
} else if (isTextFilterValue(filterValue)) {
return textFilter(cellValue, filterValue);
} else if (isNumberFilterValue(filterValue)) {
return numberFilter(cellValue, filterValue);
}
return false;
};
export const convertToTitleCase = (str: string) => {
return str.replace(/\b\w/g, (char) => char.toUpperCase());
};