@axzydev/axzy_ui_system 1.3.2 → 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/dist/index.cjs +1008 -834
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +53 -8
- package/dist/index.d.ts +53 -8
- package/dist/index.js +927 -753
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/molecules/table/table.doc.mdx +11 -0
- package/src/components/molecules/table/table.props.ts +30 -2
- package/src/components/molecules/table/table.stories.tsx +30 -0
- package/src/components/molecules/table/table.tsx +105 -8
- package/src/components/organisms/data-table/ITDataTable.doc.mdx +11 -2
- package/src/components/organisms/data-table/ITDataTable.stories.tsx +73 -0
- package/src/components/organisms/data-table/dataTable.props.ts +8 -4
- package/src/components/organisms/data-table/dataTable.tsx +74 -1
- package/src/components/theme-provider/themePresets.ts +6 -0
- package/src/components/theme-provider/themeProvider.doc.mdx +1 -0
- package/src/components/theme-provider/themeProvider.props.ts +2 -0
- package/src/components/theme-provider/themeProvider.tsx +13 -2
- package/src/hooks/useTableState.ts +17 -11
- package/src/index.ts +3 -3
- package/src/sandbox/LandingShowcase.tsx +416 -227
- package/src/showcases/DataShowcases.tsx +156 -21
- package/src/theme/theme.types.ts +2 -0
- package/src/types/table.types.ts +17 -0
- package/src/utils/styles.ts +3 -1
package/package.json
CHANGED
|
@@ -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 `"
|
|
27
|
-
filter?:
|
|
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,12 +15,16 @@ 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 {
|
|
21
24
|
tableActionsCell,
|
|
22
25
|
tableBody,
|
|
23
26
|
tableCardClickable,
|
|
27
|
+
tableCardHover,
|
|
24
28
|
tableCell,
|
|
25
29
|
tableCellText,
|
|
26
30
|
tableContainer,
|
|
@@ -145,21 +149,39 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
145
149
|
|
|
146
150
|
const filteredData = sortedData.filter((row) =>
|
|
147
151
|
columns.every((col) => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
)
|
|
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) {
|
|
153
156
|
return true;
|
|
157
|
+
}
|
|
154
158
|
|
|
155
159
|
const value = getNestedValue(row, col.key);
|
|
156
|
-
|
|
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();
|
|
157
179
|
|
|
158
180
|
switch (col.type) {
|
|
159
181
|
case "number":
|
|
160
182
|
return String(value).includes(filterValue);
|
|
161
183
|
case "boolean":
|
|
162
|
-
return value ===
|
|
184
|
+
return value === rawFilter;
|
|
163
185
|
case "catalog": {
|
|
164
186
|
if (!col.catalogOptions) return true;
|
|
165
187
|
const catalogItem = col.catalogOptions.data.find(
|
|
@@ -282,6 +304,81 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
282
304
|
);
|
|
283
305
|
}
|
|
284
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
|
+
|
|
285
382
|
return (
|
|
286
383
|
<ITInput
|
|
287
384
|
name={`filter-${col.key}`}
|
|
@@ -441,7 +538,7 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
441
538
|
currentData.map((row, i) => (
|
|
442
539
|
<div
|
|
443
540
|
key={i}
|
|
444
|
-
className={clsx(onRowClick && tableCardClickable)}
|
|
541
|
+
className={clsx(tableCardHover, onRowClick && tableCardClickable)}
|
|
445
542
|
role={onRowClick ? "button" : undefined}
|
|
446
543
|
tabIndex={onRowClick ? 0 : undefined}
|
|
447
544
|
onClick={onRowClick ? handleRowClick(row) : undefined}
|
|
@@ -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` | `
|
|
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"` (
|
|
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
|
-
/**
|
|
12
|
-
|
|
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?:
|
|
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";
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
tableActionsCell,
|
|
18
20
|
tableBody,
|
|
19
21
|
tableCardClickable,
|
|
22
|
+
tableCardHover,
|
|
20
23
|
tableCell,
|
|
21
24
|
tableCellText,
|
|
22
25
|
tableContainer,
|
|
@@ -252,6 +255,76 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
252
255
|
);
|
|
253
256
|
}
|
|
254
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
|
+
|
|
255
328
|
return (
|
|
256
329
|
<ITInput
|
|
257
330
|
name={`filter-${col.key}`}
|
|
@@ -551,7 +624,7 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
551
624
|
data.map((row, i) => (
|
|
552
625
|
<div
|
|
553
626
|
key={i}
|
|
554
|
-
className={clsx(onRowClick && !isLoading && tableCardClickable)}
|
|
627
|
+
className={clsx(tableCardHover, onRowClick && !isLoading && tableCardClickable)}
|
|
555
628
|
role={onRowClick && !isLoading ? "button" : undefined}
|
|
556
629
|
tabIndex={onRowClick && !isLoading ? 0 : undefined}
|
|
557
630
|
onClick={onRowClick && !isLoading ? handleRowClick(row) : undefined}
|
|
@@ -21,6 +21,7 @@ export const DEFAULT_PALETTE: ITThemePalette = {
|
|
|
21
21
|
headerText: "#334155", // Slate-700
|
|
22
22
|
rowBg: "#ffffff", // White
|
|
23
23
|
rowText: "#1e293b", // Slate-800
|
|
24
|
+
rowHover: "#f1f5f9", // Slate-100
|
|
24
25
|
},
|
|
25
26
|
};
|
|
26
27
|
|
|
@@ -48,6 +49,7 @@ export const PRESETS: { name: string; colors: ITThemePalette }[] = [
|
|
|
48
49
|
headerText: "#334155",
|
|
49
50
|
rowBg: "#ffffff",
|
|
50
51
|
rowText: "#0f172a",
|
|
52
|
+
rowHover: "#1e293b",
|
|
51
53
|
},
|
|
52
54
|
},
|
|
53
55
|
},
|
|
@@ -73,6 +75,7 @@ export const PRESETS: { name: string; colors: ITThemePalette }[] = [
|
|
|
73
75
|
headerText: "#881337",
|
|
74
76
|
rowBg: "#ffffff",
|
|
75
77
|
rowText: "#1c1212",
|
|
78
|
+
rowHover: "#fff1f2",
|
|
76
79
|
},
|
|
77
80
|
},
|
|
78
81
|
},
|
|
@@ -98,6 +101,7 @@ export const PRESETS: { name: string; colors: ITThemePalette }[] = [
|
|
|
98
101
|
headerText: "#115e59",
|
|
99
102
|
rowBg: "#ffffff",
|
|
100
103
|
rowText: "#042f2e",
|
|
104
|
+
rowHover: "#f0fdfa",
|
|
101
105
|
},
|
|
102
106
|
},
|
|
103
107
|
},
|
|
@@ -123,6 +127,7 @@ export const PRESETS: { name: string; colors: ITThemePalette }[] = [
|
|
|
123
127
|
headerText: "#713f12",
|
|
124
128
|
rowBg: "#ffffff",
|
|
125
129
|
rowText: "#292524",
|
|
130
|
+
rowHover: "#fefce8",
|
|
126
131
|
},
|
|
127
132
|
},
|
|
128
133
|
},
|
|
@@ -148,6 +153,7 @@ export const PRESETS: { name: string; colors: ITThemePalette }[] = [
|
|
|
148
153
|
headerText: "#166534",
|
|
149
154
|
rowBg: "#ffffff",
|
|
150
155
|
rowText: "#052e16",
|
|
156
|
+
rowHover: "#f0fdf4",
|
|
151
157
|
},
|
|
152
158
|
},
|
|
153
159
|
},
|
|
@@ -38,6 +38,7 @@ Root theme context that injects CSS custom properties into the entire component
|
|
|
38
38
|
| `table.headerText` | `string` | Table header text color. |
|
|
39
39
|
| `table.rowBg` | `string` | Table row background color. |
|
|
40
40
|
| `table.rowText` | `string` | Table row text color. |
|
|
41
|
+
| `table.rowHover` | `string` | Table row background color while hovering a row. |
|
|
41
42
|
|
|
42
43
|
## Usage
|
|
43
44
|
|
|
@@ -36,6 +36,8 @@ export interface ITThemePalette {
|
|
|
36
36
|
rowBg: string;
|
|
37
37
|
/** Text color of table body rows. @default "#1e293b" */
|
|
38
38
|
rowText: string;
|
|
39
|
+
/** Background color applied to a table body row while the pointer is over it (hex). @default "#f1f5f9" */
|
|
40
|
+
rowHover: string;
|
|
39
41
|
};
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -364,6 +364,7 @@ export default function ITThemeProvider({
|
|
|
364
364
|
let tableRowText = palette.table.rowText;
|
|
365
365
|
let tableHeaderBg = palette.table.headerBg;
|
|
366
366
|
let tableHeaderText = palette.table.headerText;
|
|
367
|
+
let tableRowHover = palette.table.rowHover;
|
|
367
368
|
let navbarBg = palette.layout.navbarBg;
|
|
368
369
|
let navbarText = palette.layout.navbarText;
|
|
369
370
|
let sidebarBg = palette.layout.sidebarBg;
|
|
@@ -382,6 +383,9 @@ export default function ITThemeProvider({
|
|
|
382
383
|
if (!isLightColor(tableHeaderText)) {
|
|
383
384
|
tableHeaderText = `color-mix(in srgb, ${palette.table.headerText} 20%, #f9fafb)`;
|
|
384
385
|
}
|
|
386
|
+
if (isLightColor(tableRowHover)) {
|
|
387
|
+
tableRowHover = `color-mix(in srgb, ${palette.table.rowHover} 12%, #1f2937)`;
|
|
388
|
+
}
|
|
385
389
|
if (isLightColor(navbarBg)) {
|
|
386
390
|
navbarBg = `color-mix(in srgb, ${palette.layout.navbarBg} 8%, #111827)`;
|
|
387
391
|
}
|
|
@@ -419,10 +423,15 @@ export default function ITThemeProvider({
|
|
|
419
423
|
tableHeaderText = `color-mix(in srgb, ${palette.table.headerText} 30%, #334155)`;
|
|
420
424
|
} else if (
|
|
421
425
|
!isLightColor(tableHeaderText) &&
|
|
422
|
-
isVeryDarkColor(
|
|
426
|
+
isVeryDarkColor(tableHeaderText)
|
|
423
427
|
) {
|
|
424
428
|
// Dark header text is fine for light mode
|
|
425
429
|
}
|
|
430
|
+
if (!isLightColor(tableRowHover)) {
|
|
431
|
+
tableRowHover = isVeryDarkColor(palette.table.rowHover)
|
|
432
|
+
? "#f1f5f9"
|
|
433
|
+
: `color-mix(in srgb, ${palette.table.rowHover} 12%, #f8fafc)`;
|
|
434
|
+
}
|
|
426
435
|
if (!isLightColor(navbarBg)) {
|
|
427
436
|
navbarBg = isVeryDarkColor(palette.layout.navbarBg)
|
|
428
437
|
? "#ffffff"
|
|
@@ -637,6 +646,7 @@ export default function ITThemeProvider({
|
|
|
637
646
|
--color-table-headerText: ${tableHeaderText} !important;
|
|
638
647
|
--color-table-rowBg: ${tableRowBg} !important;
|
|
639
648
|
--color-table-rowText: ${tableRowText} !important;
|
|
649
|
+
--color-table-rowHover: ${tableRowHover} !important;
|
|
640
650
|
|
|
641
651
|
/* Native library variables integration */
|
|
642
652
|
--it-sidebar-bg: var(--color-sidebarBg);
|
|
@@ -980,7 +990,7 @@ export default function ITThemeProvider({
|
|
|
980
990
|
color: var(--color-table-rowText) !important;
|
|
981
991
|
}
|
|
982
992
|
tbody tr:hover {
|
|
983
|
-
background-color:
|
|
993
|
+
background-color: var(--color-table-rowHover) !important;
|
|
984
994
|
}
|
|
985
995
|
|
|
986
996
|
/* Animation for Saved Indicator */
|
|
@@ -1781,6 +1791,7 @@ export default function ITThemeProvider({
|
|
|
1781
1791
|
"table.headerText",
|
|
1782
1792
|
"table.rowBg",
|
|
1783
1793
|
"table.rowText",
|
|
1794
|
+
"table.rowHover",
|
|
1784
1795
|
].map((key) => (
|
|
1785
1796
|
<ColorRow key={key} colorKey={key} />
|
|
1786
1797
|
))}
|