@axzydev/axzy_ui_system 1.3.3 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "packageManager": "pnpm@10.34.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
- "version": "1.3.3",
7
+ "version": "1.3.4",
8
8
  "type": "module",
9
9
  "sideEffects": [
10
10
  "*.css"
@@ -42,6 +42,7 @@ Basic data table with column definitions, sorting, and configurable visual varia
42
42
  | `minWidth` | `number` | — | Minimum column width in px, applied to header and body cells via inline style. |
43
43
  | `align` | `"left" \| "center" \| "right"` | `"left"` | Horizontal alignment of the column's header and cells. |
44
44
  | `truncate` | `boolean` | `false` | Truncates overflowing cell content with an ellipsis and exposes the raw string/number via the native `title` attribute. |
45
+ | `filter` | `true \| "catalog" \| "search" \| "date" \| "date-range"` | `false` | Filter control rendered in the header. `true` = text/number input (or boolean toggle when `type: "boolean"`); `"catalog"` = `ITSelect` fed by `catalogOptions.data`; `"search"` = `ITSearchSelect` typeahead fed by `catalogOptions.data` (optional `catalogOptions.onSearch` for server-side search); `"date"` / `"date-range"` = `ITDatePicker` single/range, bounded by `dateFilterOptions`. |
45
46
 
46
47
  ## Usage
47
48
 
@@ -67,6 +68,15 @@ const data = [
67
68
  onRowClick={(row, event) => console.log(row, event.type)}
68
69
  />
69
70
 
71
+ // Filter modes: ITSearchSelect typeahead + ITDatePicker single / range
72
+ const filterColumns = [
73
+ { key: 'role', label: 'Role', type: 'string', filter: 'search', catalogOptions: { data: roles } },
74
+ { key: 'lastLogin', label: 'Last login', type: 'date', filter: 'date', dateFilterOptions: { maxDate: new Date() } },
75
+ { key: 'hiredAt', label: 'Hired', type: 'date', filter: 'date-range', dateFilterOptions: { maxDate: new Date() } },
76
+ ];
77
+
78
+ <ITTable columns={filterColumns} data={data} />
79
+
70
80
  // No horizontal scroll: fixed layout + per-column widths + truncation
71
81
  const fixedColumns = [
72
82
  { key: 'name', label: 'Name', type: 'string', width: '30%', truncate: true },
@@ -81,6 +91,7 @@ const fixedColumns = [
81
91
 
82
92
  - `columns[].render` provides custom cell rendering via render function
83
93
  - `columns[].sortable` enables column header sort toggles
94
+ - `columns[].filter` supports `true` (text/number input, or boolean toggle for `type: "boolean"`), `"catalog"` (`ITSelect`), `"search"` (`ITSearchSelect` typeahead; add `catalogOptions.onSearch` for server-side search), and `"date"` / `"date-range"` (`ITDatePicker`; bound with `dateFilterOptions: { minDate, maxDate }`). Filtering is client-side: `"date"` matches the whole day, `"date-range"` includes both ends (`startOfDay` / `endOfDay`).
84
95
  - `onRowClick` fires in **both** the table and cards views. The event is a `MouseEvent` for pointer interaction and a `KeyboardEvent` for Enter/Space activation.
85
96
  - Clicks/keys that start inside an interactive child (`button`, `a`, `input`, `select`, `textarea`, `[role="button"]`, `[role="link"]`, `[data-row-click-ignore]`) do **not** trigger `onRowClick`.
86
97
  - When `onRowClick` is omitted no `cursor-pointer`, focus ring, `tabIndex`, or `role` is added.
@@ -3,6 +3,18 @@ import { TableDensity, TableSize, TableVariants } from "@/types/table.types";
3
3
  /** Data type of a table column, controls default rendering, filter UI, and sort comparison. */
4
4
  export type ColumnType = "string" | "date" | "number" | "boolean" | "actions" | "catalog";
5
5
 
6
+ /**
7
+ * Per-column filter mode, set through `Column.filter`.
8
+ *
9
+ * - `true` — text/number input (or boolean toggle when `type: "boolean"`).
10
+ * - `"catalog"` — `ITSelect` dropdown populated from `catalogOptions`.
11
+ * - `"search"` — `ITSearchSelect` typeahead populated from `catalogOptions`
12
+ * (respects `catalogOptions.onSearch` for server-side search).
13
+ * - `"date"` — `ITDatePicker` single-date filter.
14
+ * - `"date-range"` — `ITDatePicker` in range mode.
15
+ */
16
+ export type ColumnFilterMode = boolean | "catalog" | "search" | "date" | "date-range";
17
+
6
18
  /** A single selectable entry for a `"catalog"`-type column's filter/value lookup. */
7
19
  export interface CatalogOption {
8
20
  /** Unique identifier matched against the row's field value. */
@@ -23,8 +35,8 @@ export interface Column<T = any> {
23
35
  currencyMX?: boolean;
24
36
  /** Custom action buttons/content rendered for a `"actions"`-type column. Receives the full row object. */
25
37
  actions?: (row: T) => React.ReactNode;
26
- /** Enables per-column filtering. Pass `true` for a text/number/boolean filter matching `type`, or `"catalog"` to filter against `catalogOptions`. @default false */
27
- filter?: boolean | "catalog";
38
+ /** Enables per-column filtering. Pass `true` for a text/number/boolean filter matching `type`, `"catalog"` for an `ITSelect` fed by `catalogOptions`, `"search"` for an `ITSearchSelect` typeahead, or `"date"` / `"date-range"` for an `ITDatePicker`. @default false */
39
+ filter?: ColumnFilterMode;
28
40
  /** Column data type. Drives default cell rendering, filter UI, and sort comparison. */
29
41
  type: ColumnType;
30
42
  /** Whether clicking the header sorts by this column. Ignored for `type: "actions"`. @default false */
@@ -45,6 +57,22 @@ export interface Column<T = any> {
45
57
  loading?: boolean;
46
58
  /** Shows an error state in the filter UI when the catalog failed to load. @default false */
47
59
  error?: boolean;
60
+ /**
61
+ * Server-side search handler for a `filter: "search"` column. When provided,
62
+ * `ITSearchSelect` delegates filtering to this callback (debounced) instead
63
+ * of filtering `data` locally; the parent is expected to update `data`.
64
+ */
65
+ onSearch?: (query: string) => void;
66
+ };
67
+ /**
68
+ * Date bounds applied by the `"date"` and `"date-range"` filters, forwarded to
69
+ * the underlying `ITDatePicker`. Ignored by every other filter mode.
70
+ */
71
+ dateFilterOptions?: {
72
+ /** Earliest selectable date. */
73
+ minDate?: Date;
74
+ /** Latest selectable date. */
75
+ maxDate?: Date;
48
76
  };
49
77
  /**
50
78
  * Fixed column width. A `number` is treated as pixels (`width: 120` → `120px`);
@@ -325,3 +325,33 @@ export const VirtualizedLargeDataset: Story = {
325
325
  itemsPerPageOptions: [50, 100, 250],
326
326
  } as any,
327
327
  };
328
+
329
+ // --- COLUMN FILTER MODES ---
330
+
331
+ const filterModeColumns: Column<User>[] = [
332
+ { key: "name", label: "Name", type: "string", filter: true },
333
+ {
334
+ key: "roleId",
335
+ label: "Role (ITSearchSelect)",
336
+ type: "catalog",
337
+ filter: "search",
338
+ catalogOptions: { data: mockRoles },
339
+ },
340
+ {
341
+ key: "lastLogin",
342
+ label: "Last login (ITDatePicker range)",
343
+ type: "date",
344
+ filter: "date-range",
345
+ dateFilterOptions: { maxDate: new Date() },
346
+ render: (row) => new Date(row.lastLogin).toLocaleDateString("es-MX"),
347
+ },
348
+ ];
349
+
350
+ export const WithSearchAndDateFilters: Story = {
351
+ args: {
352
+ columns: filterModeColumns,
353
+ data: generateMockData(25),
354
+ title: "Filtros: ITSearchSelect + ITDatePicker (rango)",
355
+ defaultItemsPerPage: 8,
356
+ } as any,
357
+ };
@@ -15,6 +15,9 @@ import { MdOutlineSwapVert } from "react-icons/md";
15
15
  import ITInput from "@/components/atoms/input/input";
16
16
  import ITPagination from "@/components/molecules/pagination/pagination";
17
17
  import ITSelect from "@/components/molecules/select/select";
18
+ import ITSearchSelect from "@/components/molecules/search-select/search-select";
19
+ import ITDatePicker from "@/components/molecules/date-picker/datePicker";
20
+ import { isSameDay, startOfDay, endOfDay } from "date-fns";
18
21
  import { Column, ITTableProps } from "./table.props";
19
22
  import ITText from "@/components/atoms/text/text";
20
23
  import {
@@ -146,21 +149,39 @@ export default function ITTable<T extends Record<string, unknown>>({
146
149
 
147
150
  const filteredData = sortedData.filter((row) =>
148
151
  columns.every((col) => {
149
- if (
150
- !col.filter ||
151
- filters[col.key] === undefined ||
152
- filters[col.key] === ""
153
- )
152
+ const rawFilter = filters[col.key];
153
+ if (!col.filter || rawFilter === undefined || rawFilter === "") return true;
154
+ // A date range with both ends empty means "no filter".
155
+ if (Array.isArray(rawFilter) && rawFilter[0] == null && rawFilter[1] == null) {
154
156
  return true;
157
+ }
155
158
 
156
159
  const value = getNestedValue(row, col.key);
157
- const filterValue = String(filters[col.key]).toLowerCase();
160
+
161
+ if (col.filter === "date" || col.filter === "date-range") {
162
+ const rowDate = value instanceof Date ? value : new Date(String(value));
163
+ if (isNaN(rowDate.getTime())) return false;
164
+ if (col.filter === "date") {
165
+ return isSameDay(rowDate, rawFilter as Date);
166
+ }
167
+ const [start, end] = rawFilter as [Date | null, Date | null];
168
+ if (start && rowDate.getTime() < startOfDay(start).getTime()) return false;
169
+ if (end && rowDate.getTime() > endOfDay(end).getTime()) return false;
170
+ return true;
171
+ }
172
+
173
+ if (col.filter === "search") {
174
+ // The search-select stores the exact option value (id).
175
+ return String(value) === String(rawFilter);
176
+ }
177
+
178
+ const filterValue = String(rawFilter).toLowerCase();
158
179
 
159
180
  switch (col.type) {
160
181
  case "number":
161
182
  return String(value).includes(filterValue);
162
183
  case "boolean":
163
- return value === filters[col.key];
184
+ return value === rawFilter;
164
185
  case "catalog": {
165
186
  if (!col.catalogOptions) return true;
166
187
  const catalogItem = col.catalogOptions.data.find(
@@ -283,6 +304,81 @@ export default function ITTable<T extends Record<string, unknown>>({
283
304
  );
284
305
  }
285
306
 
307
+ if (col.filter === "search" && col.catalogOptions) {
308
+ if (col.catalogOptions.loading) {
309
+ return (
310
+ <FaSpinner
311
+ className="animate-spin"
312
+ aria-label="Cargando opciones"
313
+ title="Cargando opciones"
314
+ />
315
+ );
316
+ }
317
+
318
+ if (col.catalogOptions.error) {
319
+ return <ITText as="span" className="text-danger-500 text-xs">Error cargando</ITText>;
320
+ }
321
+
322
+ const searchValue = filters[col.key];
323
+ return (
324
+ <ITSearchSelect
325
+ name={`filter-${col.key}`}
326
+ options={col.catalogOptions.data.map((item) => ({
327
+ value: item.id,
328
+ label: item.name,
329
+ }))}
330
+ value={
331
+ typeof searchValue === "string" || typeof searchValue === "number"
332
+ ? searchValue
333
+ : ""
334
+ }
335
+ onChange={(value) => handleFilterChange(col.key, value === "" ? undefined : value)}
336
+ onSearch={col.catalogOptions.onSearch}
337
+ isLoading={col.catalogOptions.loading}
338
+ size="sm"
339
+ placeholder="Buscar..."
340
+ clearable
341
+ className="w-full"
342
+ onBlur={() => { }}
343
+ />
344
+ );
345
+ }
346
+
347
+ if (col.filter === "date") {
348
+ const dateValue = filters[col.key];
349
+ return (
350
+ <ITDatePicker
351
+ name={`filter-${col.key}`}
352
+ value={dateValue instanceof Date ? dateValue : undefined}
353
+ onChange={(e) => handleFilterChange(col.key, e.target.value as Date)}
354
+ minDate={col.dateFilterOptions?.minDate}
355
+ maxDate={col.dateFilterOptions?.maxDate}
356
+ size="sm"
357
+ placeholder="dd/mm/aaaa"
358
+ className="w-full"
359
+ />
360
+ );
361
+ }
362
+
363
+ if (col.filter === "date-range") {
364
+ const rangeValue = filters[col.key];
365
+ return (
366
+ <ITDatePicker
367
+ name={`filter-${col.key}`}
368
+ range
369
+ value={Array.isArray(rangeValue) ? rangeValue : undefined}
370
+ onChange={(e) =>
371
+ handleFilterChange(col.key, e.target.value as [Date | null, Date | null])
372
+ }
373
+ minDate={col.dateFilterOptions?.minDate}
374
+ maxDate={col.dateFilterOptions?.maxDate}
375
+ size="sm"
376
+ placeholder="dd/mm/aaaa - dd/mm/aaaa"
377
+ className="w-full"
378
+ />
379
+ );
380
+ }
381
+
286
382
  return (
287
383
  <ITInput
288
384
  name={`filter-${col.key}`}
@@ -14,7 +14,7 @@ Advanced server-side data table with automatic pagination, column sorting, and f
14
14
  | `columns` | `Column<T>[]` | — | Column definitions with key, label, type, sortable, filter, render, plus layout fields: `width` (`number` px or CSS `string`), `minWidth` (px), `align` (`"left" \| "center" \| "right"`), and `truncate` (ellipsis + native `title`). |
15
15
  | `fetchData` | `(params: ITDataTableFetchParams) => Promise<ITDataTableResponse<T>>` | — | Async callback triggered on pagination, filter, or sort change. Must return `{ data, total }`. |
16
16
  | `debounceMs` | `number` | `500` | Delay in ms before firing fetch after filter state changes. |
17
- | `externalFilters` | `Record<string, string \| number \| boolean \| Date>` | `{}` | Filters managed outside the table (e.g., date pickers) merged with internal column filters. |
17
+ | `externalFilters` | `ColumnFilters` | `{}` | Filters managed outside the table (e.g., date pickers) merged with internal column filters. Values may be scalars, a `Date`, or a `[start, end]` tuple. |
18
18
  | `loadingIndicator` | `ReactNode` | `<FaSpinner />` | Custom element shown while loading. |
19
19
  | `fetchOnMount` | `boolean` | `true` | Whether to fetch data on initial mount. |
20
20
  | `reloadTrigger` | `number \| string \| boolean` | — | Changing this value forces a re-fetch. Use after mutations. |
@@ -56,6 +56,15 @@ import { ITDataTable } from '@axzydev/axzy_ui_system';
56
56
  variant="bordered"
57
57
  />
58
58
 
59
+ // Search + date filters (values arrive in params.filters)
60
+ <ITDataTable
61
+ columns={[
62
+ { key: 'role', label: 'Role', type: 'string', filter: 'search', catalogOptions: { data: roles, onSearch: (q) => api.searchRoles(q) } },
63
+ { key: 'hiredAt', label: 'Hired', type: 'date', filter: 'date-range', dateFilterOptions: { maxDate: new Date() } },
64
+ ]}
65
+ fetchData={async (params) => api.fetchItems(params)}
66
+ />
67
+
59
68
  // No horizontal scroll: fixed layout + per-column widths + truncation
60
69
  <ITDataTable
61
70
  columns={[
@@ -71,7 +80,7 @@ import { ITDataTable } from '@axzydev/axzy_ui_system';
71
80
 
72
81
  ## Notes
73
82
 
74
- - Column filters support `"catalog"` (dropdown), `true` (text input), and `boolean` (tri-state toggle).
83
+ - Column filters support `true` (text/number input, or boolean tri-state toggle for `type: "boolean"`), `"catalog"` (`ITSelect`), `"search"` (`ITSearchSelect` typeahead; add `catalogOptions.onSearch` for server-side search), and `"date"` / `"date-range"` (`ITDatePicker`, bounded by `dateFilterOptions`). In `fetchData(params)`, `params.filters[key]` is a `Date` for `"date"`, a `[start, end]` tuple for `"date-range"`, and the scalar/id otherwise.
75
84
  - The `externalFilters` prop allows integrating external filter controls like date pickers without building filter UI into the table itself.
76
85
  - Debounce prevents excessive API calls while the user types in filter inputs.
77
86
  - A loading skeleton overlay covers the table body while fetching, keeping the header visible.
@@ -1,5 +1,6 @@
1
1
  import React, { useState, useCallback, useMemo } from 'react';
2
2
  import { Meta, StoryObj } from '@storybook/react';
3
+ import { startOfDay, endOfDay } from 'date-fns';
3
4
  import ITDataTable from './dataTable';
4
5
  import { ITDataTableFetchParams, ITDataTableResponse } from './dataTable.props';
5
6
  import ITCard from '@/components/molecules/card/card';
@@ -243,6 +244,78 @@ export const WithRowClick: Story = {
243
244
  },
244
245
  };
245
246
 
247
+ export const WithSearchAndDateFilters: Story = {
248
+ render: () => {
249
+ const searchDateFetch = useCallback(
250
+ async (params: ITDataTableFetchParams): Promise<ITDataTableResponse<any>> => {
251
+ await new Promise((r) => setTimeout(r, 400));
252
+ let filtered = [...MOCK_DATA];
253
+
254
+ Object.entries(params.filters).forEach(([key, value]) => {
255
+ if (value === undefined || value === '') return;
256
+
257
+ if (key === 'role') {
258
+ filtered = filtered.filter((item) => item.role === value);
259
+ } else if (key === 'createdAt') {
260
+ if (Array.isArray(value)) {
261
+ const [rangeStart, rangeEnd] = value as [Date | null, Date | null];
262
+ if (!rangeStart && !rangeEnd) return;
263
+ filtered = filtered.filter((item) => {
264
+ const time = new Date(item.createdAt).getTime();
265
+ if (rangeStart && time < startOfDay(rangeStart).getTime()) return false;
266
+ if (rangeEnd && time > endOfDay(rangeEnd).getTime()) return false;
267
+ return true;
268
+ });
269
+ } else if (value instanceof Date) {
270
+ filtered = filtered.filter(
271
+ (item) => new Date(item.createdAt).toDateString() === value.toDateString()
272
+ );
273
+ }
274
+ }
275
+ });
276
+
277
+ const start = (params.page - 1) * params.limit;
278
+ return { data: filtered.slice(start, start + params.limit), total: filtered.length };
279
+ },
280
+ []
281
+ );
282
+
283
+ const filterColumns = useMemo(
284
+ () => [
285
+ { key: 'username', label: 'Usuario', type: 'string' as const, filter: true, sortable: true },
286
+ {
287
+ key: 'role',
288
+ label: 'Rol (ITSearchSelect)',
289
+ type: 'string' as const,
290
+ filter: 'search' as const,
291
+ sortable: true,
292
+ catalogOptions: { data: ROLES.map((role) => ({ id: role, name: role })) },
293
+ render: (row: any) => <ITBadget label={row.role} />,
294
+ },
295
+ {
296
+ key: 'createdAt',
297
+ label: 'Alta (ITDatePicker range)',
298
+ type: 'date' as const,
299
+ filter: 'date-range' as const,
300
+ sortable: true,
301
+ dateFilterOptions: { maxDate: new Date() },
302
+ render: (row: any) => new Date(row.createdAt).toLocaleDateString('es-MX'),
303
+ },
304
+ ],
305
+ []
306
+ );
307
+
308
+ return (
309
+ <ITDataTable
310
+ title="Filtros: ITSearchSelect + ITDatePicker (rango)"
311
+ columns={filterColumns as any}
312
+ fetchData={searchDateFetch}
313
+ defaultItemsPerPage={8}
314
+ />
315
+ );
316
+ },
317
+ };
318
+
246
319
  export const SkeletonLoading: Story = {
247
320
  args: {
248
321
  title: 'Cargando Datos...',
@@ -1,6 +1,6 @@
1
1
  import { ReactNode } from "react";
2
2
  import { Column } from "@/components/molecules/table/table.props";
3
- import { TableDensity } from "@/types/table.types";
3
+ import { ColumnFilters, TableDensity } from "@/types/table.types";
4
4
 
5
5
  /** Parameters passed to `fetchData` every time pagination, filters, or sorting change. */
6
6
  export interface ITDataTableFetchParams {
@@ -8,8 +8,12 @@ export interface ITDataTableFetchParams {
8
8
  page: number;
9
9
  /** Number of rows requested per page. */
10
10
  limit: number;
11
- /** Active per-column filter values (from `Column.filter`), keyed by column `key`. */
12
- filters: Record<string, string | number | boolean | Date>;
11
+ /**
12
+ * Active per-column filter values (from `Column.filter`), keyed by column
13
+ * `key`. Scalars for text/number/boolean/date filters, a `Date` for
14
+ * `filter: "date"`, and a `[start, end]` tuple for `filter: "date-range"`.
15
+ */
16
+ filters: ColumnFilters;
13
17
  /** Active sort, present only when the user has clicked a sortable column header. */
14
18
  sort?: {
15
19
  /** Column `key` currently sorted by. */
@@ -50,7 +54,7 @@ export interface ITDataTableProps<T extends Record<string, unknown>> {
50
54
  * Filters managed outside of the ITDataTable (e.g. a date range picker).
51
55
  * These will be merged with the internal column filters before calling fetchData.
52
56
  */
53
- externalFilters?: Record<string, string | number | boolean | Date>;
57
+ externalFilters?: ColumnFilters;
54
58
 
55
59
  /**
56
60
  * Custom element to display instead of the default spinner while `isLoading` is true.
@@ -9,6 +9,8 @@ import { MdOutlineSwapVert } from "react-icons/md";
9
9
  import ITInput from "@/components/atoms/input/input";
10
10
  import ITPagination from "@/components/molecules/pagination/pagination";
11
11
  import ITSelect from "@/components/molecules/select/select";
12
+ import ITSearchSelect from "@/components/molecules/search-select/search-select";
13
+ import ITDatePicker from "@/components/molecules/date-picker/datePicker";
12
14
  import { Column } from "@/components/molecules/table/table.props";
13
15
  import { formatCurrencyMX, isInteractiveTarget } from "@/utils/table.utils";
14
16
  import { ITDataTableProps } from "./dataTable.props";
@@ -253,6 +255,76 @@ export default function ITDataTable<T extends Record<string, unknown>>({
253
255
  );
254
256
  }
255
257
 
258
+ if (col.filter === "search" && col.catalogOptions) {
259
+ if (col.catalogOptions.loading) {
260
+ return <FaSpinner className="animate-spin" aria-label="Cargando opciones" title="Cargando opciones" />;
261
+ }
262
+ if (col.catalogOptions.error) {
263
+ return <ITText as="span" className="text-danger-500 text-xs">Error cargando</ITText>;
264
+ }
265
+ const searchValue = filters[col.key];
266
+ return (
267
+ <ITSearchSelect
268
+ name={`filter-${col.key}`}
269
+ options={col.catalogOptions.data.map((item) => ({
270
+ value: item.id,
271
+ label: item.name,
272
+ }))}
273
+ value={
274
+ typeof searchValue === "string" || typeof searchValue === "number"
275
+ ? searchValue
276
+ : ""
277
+ }
278
+ onChange={(value) => handleFilterChange(col.key, value === "" ? undefined : value)}
279
+ onSearch={col.catalogOptions.onSearch}
280
+ isLoading={col.catalogOptions.loading}
281
+ size="sm"
282
+ placeholder="Buscar..."
283
+ clearable
284
+ className="w-full"
285
+ onBlur={() => {}}
286
+ disabled={isLoading}
287
+ />
288
+ );
289
+ }
290
+
291
+ if (col.filter === "date") {
292
+ const dateValue = filters[col.key];
293
+ return (
294
+ <ITDatePicker
295
+ name={`filter-${col.key}`}
296
+ value={dateValue instanceof Date ? dateValue : undefined}
297
+ onChange={(e) => handleFilterChange(col.key, e.target.value as Date)}
298
+ minDate={col.dateFilterOptions?.minDate}
299
+ maxDate={col.dateFilterOptions?.maxDate}
300
+ size="sm"
301
+ placeholder="dd/mm/aaaa"
302
+ className="w-full"
303
+ disabled={isLoading}
304
+ />
305
+ );
306
+ }
307
+
308
+ if (col.filter === "date-range") {
309
+ const rangeValue = filters[col.key];
310
+ return (
311
+ <ITDatePicker
312
+ name={`filter-${col.key}`}
313
+ range
314
+ value={Array.isArray(rangeValue) ? rangeValue : undefined}
315
+ onChange={(e) =>
316
+ handleFilterChange(col.key, e.target.value as [Date | null, Date | null])
317
+ }
318
+ minDate={col.dateFilterOptions?.minDate}
319
+ maxDate={col.dateFilterOptions?.maxDate}
320
+ size="sm"
321
+ placeholder="dd/mm/aaaa - dd/mm/aaaa"
322
+ className="w-full"
323
+ disabled={isLoading}
324
+ />
325
+ );
326
+ }
327
+
256
328
  return (
257
329
  <ITInput
258
330
  name={`filter-${col.key}`}
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useMemo, useState } from "react";
2
+ import type { ColumnFilters, ColumnFilterValue } from "@/types/table.types";
2
3
 
3
4
  interface SortConfig {
4
5
  key: string;
@@ -13,30 +14,35 @@ export interface UseTableStateOptions {
13
14
  export interface UseTableStateResult {
14
15
  currentPage: number;
15
16
  itemsPerPage: number;
16
- filters: Record<string, string | boolean | number>;
17
+ filters: ColumnFilters;
17
18
  sortConfig: SortConfig | null;
18
19
  totalPages: number;
19
20
  setTotalPages: (pages: number) => void;
20
21
  goToPage: (page: number) => void;
21
22
  handleItemsPerPageChange: (value: number) => void;
22
- handleFilterChange: (
23
- key: string,
24
- value: string | boolean | number | undefined
25
- ) => void;
23
+ handleFilterChange: (key: string, value: ColumnFilterValue | undefined) => void;
26
24
  handleSort: (key: string, sortable?: boolean) => void;
27
25
  resetPage: () => void;
28
26
  clearFilters: () => void;
29
27
  }
30
28
 
29
+ /**
30
+ * A filter value is considered empty when it should be removed from the filter
31
+ * map: `undefined`/`null`/`""`, or a date range whose both ends are null.
32
+ */
33
+ const isEmptyFilterValue = (value: ColumnFilterValue | undefined): boolean => {
34
+ if (value === undefined || value === null || value === "") return true;
35
+ if (Array.isArray(value)) return value[0] == null && value[1] == null;
36
+ return false;
37
+ };
38
+
31
39
  export function useTableState({
32
40
  defaultItemsPerPage = 10,
33
41
  initialSort = null,
34
42
  }: UseTableStateOptions = {}): UseTableStateResult {
35
43
  const [currentPage, setCurrentPage] = useState(1);
36
44
  const [itemsPerPage, setItemsPerPage] = useState(defaultItemsPerPage);
37
- const [filters, setFilters] = useState<
38
- Record<string, string | boolean | number>
39
- >({});
45
+ const [filters, setFilters] = useState<ColumnFilters>({});
40
46
  const [sortConfig, setSortConfig] = useState<SortConfig | null>(initialSort);
41
47
  const [totalPages, setTotalPages] = useState(1);
42
48
 
@@ -58,14 +64,14 @@ export function useTableState({
58
64
  }, []);
59
65
 
60
66
  const handleFilterChange = useCallback(
61
- (key: string, value: string | boolean | number | undefined) => {
67
+ (key: string, value: ColumnFilterValue | undefined) => {
62
68
  setFilters((prev) => {
63
- if (value === undefined || value === null || value === "") {
69
+ if (isEmptyFilterValue(value)) {
64
70
  const next = { ...prev };
65
71
  delete next[key];
66
72
  return next;
67
73
  }
68
- return { ...prev, [key]: value };
74
+ return { ...prev, [key]: value as ColumnFilterValue };
69
75
  });
70
76
  setCurrentPage(1);
71
77
  },
package/src/index.ts CHANGED
@@ -107,7 +107,7 @@ import type { ITSlideToggleProps } from "./components/atoms/slide/slide.props";
107
107
  import type { ITSliderProps } from "./components/atoms/slider/slider.props";
108
108
  import type { ITStackProps } from "./components/atoms/stack/stack.props";
109
109
  import type { ITStatCardProps } from "./components/molecules/stat-card/stat-card.props";
110
- import type { ITTableProps, Column } from "./components/molecules/table/table.props";
110
+ import type { ITTableProps, Column, ColumnFilterMode } from "./components/molecules/table/table.props";
111
111
  import type { ITTextProps } from "./components/atoms/text/text.props";
112
112
  import type { ITTextareaProps } from "./components/atoms/textarea/textarea.props";
113
113
  import type { ITTabsProps, ITTabItem } from "./components/molecules/tabs/tabs.props";
@@ -115,7 +115,7 @@ import type { ITTripleFilterProps, ITTripleFilterOption } from "./components/org
115
115
  import type { ITToastProps } from "./components/organisms/toast/toast.props";
116
116
  import type { ITWysiwygProps } from "./components/molecules/wysiwyg/wysiwyg.props";
117
117
  import type { FieldConfig, FieldConfigV2 } from "./types/field.types";
118
- import type { TableDensity } from "./types/table.types";
118
+ import type { ColumnFilters, ColumnFilterValue, TableDensity } from "./types/table.types";
119
119
 
120
120
  // UTILS
121
121
  import ITDropfile, { UploadStatus, FileTypeEnum } from "./components/organisms/dropfile/dropfile";
@@ -157,7 +157,7 @@ export {
157
157
  };
158
158
 
159
159
  export type {
160
- Column, FieldConfig,
160
+ Column, ColumnFilters, ColumnFilterMode, ColumnFilterValue, FieldConfig,
161
161
  FieldConfigV2,
162
162
  ITAccordionItem, ITAccordionProps,
163
163
  ITAlertProps, ITAvatarProps, ITBadgetProps,