@vrobots/storybook 0.2.0 → 0.2.1
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/package.json +3 -2
- package/dist/src/components/index.d.ts +4 -0
- package/dist/src/components/index.js +4 -0
- package/dist/src/components/table/DataTable.d.ts +16 -0
- package/dist/src/components/table/DataTable.js +42 -0
- package/dist/src/components/table/components/ColumnText.d.ts +2 -0
- package/dist/src/components/table/components/ColumnText.js +5 -0
- package/dist/src/components/table/components/PageSizeSelector.d.ts +7 -0
- package/dist/src/components/table/components/PageSizeSelector.js +8 -0
- package/dist/src/components/table/components/Pagination.d.ts +10 -0
- package/dist/src/components/table/components/Pagination.js +51 -0
- package/dist/src/components/table/components/TableFilter.d.ts +6 -0
- package/dist/src/components/table/components/TableFilter.js +28 -0
- package/dist/src/components/table/components/TableFilterDatePicker.d.ts +5 -0
- package/dist/src/components/table/components/TableFilterDatePicker.js +10 -0
- package/dist/src/components/table/components/TableFilterInput.d.ts +2 -0
- package/dist/src/components/table/components/TableFilterInput.js +5 -0
- package/dist/src/components/table/components/TableFilterSelect.d.ts +2 -0
- package/dist/src/components/table/components/TableFilterSelect.js +5 -0
- package/dist/src/components/table/constants.d.ts +2 -0
- package/dist/src/components/table/constants.js +2 -0
- package/dist/src/components/table/hooks/useDataTable.d.ts +24 -0
- package/dist/src/components/table/hooks/useDataTable.js +14 -0
- package/dist/src/components/table/hooks/usePageSize.d.ts +4 -0
- package/dist/src/components/table/hooks/usePageSize.js +8 -0
- package/dist/src/components/table/hooks/usePagination.d.ts +5 -0
- package/dist/src/components/table/hooks/usePagination.js +12 -0
- package/dist/src/components/table/hooks/useTableFilters.d.ts +15 -0
- package/dist/src/components/table/hooks/useTableFilters.js +118 -0
- package/dist/src/components/table/types.d.ts +11 -0
- package/dist/src/components/table/types.js +1 -0
- package/dist/src/stories/Breadcrumbs.stories.js +1 -1
- package/dist/src/stories/DataTable.stories.d.ts +26 -0
- package/dist/src/stories/DataTable.stories.js +92 -0
- package/dist/src/stories/FileUploader.stories.js +1 -1
- package/dist/src/stories/Login.stories.js +1 -1
- package/dist/src/stories/SecondFactorAuth.stories.js +1 -1
- package/dist/src/stories/VerifyAction.stories.js +1 -1
- package/package.json +3 -2
package/dist/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vrobots/storybook",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
"@chakra-ui/react": "^3.32.0",
|
|
22
22
|
"@emotion/react": "^11.14.0",
|
|
23
23
|
"@storybook/addon-themes": "^10.2.16",
|
|
24
|
+
"@tanstack/react-table": "^8.21.3",
|
|
24
25
|
"axios": "^1.13.4",
|
|
25
26
|
"motion": "^12.34.0",
|
|
27
|
+
"next": "^16.2.4",
|
|
26
28
|
"next-themes": "^0.4.6",
|
|
27
29
|
"owasp-password-strength-test": "^1.3.0",
|
|
28
30
|
"react": "^19.2.0",
|
|
@@ -40,7 +42,6 @@
|
|
|
40
42
|
"@storybook/addon-docs": "^10.2.16",
|
|
41
43
|
"@storybook/addon-vitest": "^10.2.16",
|
|
42
44
|
"@storybook/react-vite": "^10.2.16",
|
|
43
|
-
"@storybook/test": "^8.6.15",
|
|
44
45
|
"@types/node": "^24.10.1",
|
|
45
46
|
"@types/react": "^19.2.5",
|
|
46
47
|
"@types/react-dom": "^19.2.3",
|
|
@@ -19,3 +19,7 @@ export * from './form/Login';
|
|
|
19
19
|
export * from './form/SecondFactorAuth';
|
|
20
20
|
export * from './form/FileUploader';
|
|
21
21
|
export { default as Form } from './form';
|
|
22
|
+
export { default as DataTable } from './table/DataTable';
|
|
23
|
+
export * from './table/types';
|
|
24
|
+
export * from './table/components/ColumnText';
|
|
25
|
+
export { default as useDataTable } from './table/hooks/useDataTable';
|
|
@@ -19,3 +19,7 @@ export * from './form/Login';
|
|
|
19
19
|
export * from './form/SecondFactorAuth';
|
|
20
20
|
export * from './form/FileUploader';
|
|
21
21
|
export { default as Form } from './form';
|
|
22
|
+
export { default as DataTable } from './table/DataTable';
|
|
23
|
+
export * from './table/types';
|
|
24
|
+
export * from './table/components/ColumnText';
|
|
25
|
+
export { default as useDataTable } from './table/hooks/useDataTable';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { IColumnFilter, TFilterChange } from "./types";
|
|
2
|
+
export interface IDataTableProps {
|
|
3
|
+
data: any[];
|
|
4
|
+
total: number;
|
|
5
|
+
pageNumber: number;
|
|
6
|
+
pagesTotal: number;
|
|
7
|
+
pageSize: number;
|
|
8
|
+
setPageNumber: React.Dispatch<React.SetStateAction<number>>;
|
|
9
|
+
setPageSize: React.Dispatch<React.SetStateAction<number>>;
|
|
10
|
+
columns: any[];
|
|
11
|
+
filters?: IColumnFilter[];
|
|
12
|
+
onRowClick?: (row: any) => void;
|
|
13
|
+
onFilterChange?: TFilterChange;
|
|
14
|
+
}
|
|
15
|
+
declare const DataTable: React.FC<IDataTableProps>;
|
|
16
|
+
export default DataTable;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Card, Table } from "@chakra-ui/react";
|
|
3
|
+
import { useColorMode } from "../ui/color-mode";
|
|
4
|
+
import { flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import TableFilter from "./components/TableFilter";
|
|
7
|
+
import { Pagination } from "./components/Pagination";
|
|
8
|
+
const Pager = ({ border, pagesTotal, pageNumber, setPageNumber, pageSize, setPageSize }) => (_jsx(Box, { p: 2, pt: 6, border: border, borderBottom: 'none', children: _jsx(Pagination, { pagesTotal: pagesTotal, pageNumber: pageNumber, setPageNumber: setPageNumber, pageSize: pageSize, setPageSize: setPageSize, showPageSelector: true }) }));
|
|
9
|
+
const DataTable = ({ data: defaultData, total, pageNumber, pagesTotal, setPageNumber, setPageSize, pageSize, columns, filters = [], onRowClick, onFilterChange, }) => {
|
|
10
|
+
const [data, setData] = useState(() => [...defaultData]);
|
|
11
|
+
const { colorMode } = useColorMode();
|
|
12
|
+
const columnResizeMode = 'onChange';
|
|
13
|
+
const columnResizeDirection = 'ltr';
|
|
14
|
+
const isDarkMode = colorMode === 'dark';
|
|
15
|
+
const border = `1px solid ${isDarkMode ? '#333333' : '#E2E8F0'} !important`;
|
|
16
|
+
const table = useReactTable({
|
|
17
|
+
data,
|
|
18
|
+
columns,
|
|
19
|
+
columnResizeMode,
|
|
20
|
+
columnResizeDirection,
|
|
21
|
+
getCoreRowModel: getCoreRowModel(),
|
|
22
|
+
});
|
|
23
|
+
const pagerProps = { border, pagesTotal, pageNumber, setPageNumber, pageSize, setPageSize };
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setData([...defaultData]);
|
|
26
|
+
}, [defaultData, setData]);
|
|
27
|
+
return (_jsx(Card.Root, { children: _jsxs(Card.Body, { p: 0, children: [_jsx(Pager, { ...pagerProps }), _jsx(Box, { overflow: 'auto', children: _jsxs(Table.Root, { width: '100%', children: [_jsx(Table.Header, { children: table.getHeaderGroups().map(headerGroup => (_jsx(Table.Row, { children: headerGroup.headers.map(header => (_jsxs(Table.ColumnHeader, { borderLeft: border, borderRight: border, children: [header.isPlaceholder
|
|
28
|
+
? null
|
|
29
|
+
: flexRender(header.column.columnDef.header, header.getContext()), filters.find((filter) => filter.id === header.id) && !!onFilterChange ? (_jsx(TableFilter, { filter: filters.find(filter => filter.id === header.id), onFilterChange: onFilterChange })) : null] }, header.id))) }, headerGroup.id))) }), _jsxs(Table.Body, { children: [total === 0 ? (_jsx(Table.Row, { children: _jsx(Table.Cell, { colSpan: columns.length, textAlign: 'center', children: "No records found" }) })) : null, table.getRowModel().rows.map((row, i) => {
|
|
30
|
+
const isEven = i % 2 === 0;
|
|
31
|
+
const colorEven = isDarkMode ? 'gray.700' : 'gray.50';
|
|
32
|
+
const colorOdd = isDarkMode ? 'gray.600' : 'white';
|
|
33
|
+
const backgroundColor = isEven ? colorEven : colorOdd;
|
|
34
|
+
return (_jsx(Table.Row, { background: backgroundColor, _hover: !!onRowClick ? {
|
|
35
|
+
cursor: 'pointer',
|
|
36
|
+
backgroundColor: 'rgba(107, 160, 252, 0.3) !important',
|
|
37
|
+
} : void 0, onClick: () => onRowClick && onRowClick(row.original), children: row.getVisibleCells().map(cell => (_jsx(Table.Cell, { border: border, children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))) }, row.id));
|
|
38
|
+
})] }), _jsx(Table.Footer, { children: table.getFooterGroups().map(footerGroup => (_jsx(Table.Row, { children: footerGroup.headers.map(header => (_jsx(Table.ColumnHeader, { borderLeft: border, borderRight: border, children: header.isPlaceholder
|
|
39
|
+
? null
|
|
40
|
+
: flexRender(header.column.columnDef.header, header.getContext()) }, header.id))) }, footerGroup.id))) })] }) }), _jsx(Pager, { ...pagerProps })] }) }));
|
|
41
|
+
};
|
|
42
|
+
export default DataTable;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { NativeSelect, HStack, Text } from '@chakra-ui/react';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
export const PageSizeSelector = ({ sizes, selectedPageSize, onSelect, }) => {
|
|
5
|
+
const idKey = React.useId();
|
|
6
|
+
const handleSetPageSize = (e) => onSelect(Number(e.target.value));
|
|
7
|
+
return (_jsxs(HStack, { gap: "3", align: "center", children: [_jsx(Text, { fontSize: "sm", fontWeight: "normal", children: "Showing" }), _jsxs(NativeSelect.Root, { size: 'sm', maxW: '75px', children: [_jsx(NativeSelect.Field, { id: `page-size-selector-${idKey}`, value: selectedPageSize, onChange: handleSetPageSize, cursor: 'pointer', children: sizes.map((size, key) => (_jsx("option", { value: size, children: size }, `options-${size}-${key}`))) }), _jsx(NativeSelect.Indicator, {})] })] }));
|
|
8
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface IPaginationProps {
|
|
3
|
+
pagesTotal: number;
|
|
4
|
+
pageNumber: number;
|
|
5
|
+
pageSize: number;
|
|
6
|
+
showPageSelector?: boolean;
|
|
7
|
+
setPageSize: React.Dispatch<React.SetStateAction<number>>;
|
|
8
|
+
setPageNumber: React.Dispatch<React.SetStateAction<number>>;
|
|
9
|
+
}
|
|
10
|
+
export declare const Pagination: React.FC<IPaginationProps>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { Flex, Button, Field, HStack, Text, Grid, GridItem, NumberInput } from '@chakra-ui/react';
|
|
4
|
+
import { PAGE_SIZES } from '../constants';
|
|
5
|
+
import { PageSizeSelector } from './PageSizeSelector';
|
|
6
|
+
import { useBreakpoint } from '../../../hooks';
|
|
7
|
+
const PAGE_LIMIT_MOBILE = 2;
|
|
8
|
+
const PAGE_LIMIT = 4;
|
|
9
|
+
const makeNavButton = (onChange, pageNumber) => function PageButton(page) {
|
|
10
|
+
return (_jsx(Button, { "aria-label": `page-${page}`, onClick: () => onChange(page.toString()), variant: page === pageNumber ? 'solid' : 'outline', size: 'sm', paddingLeft: '1px !important', paddingRight: '1px !important', marginRight: 1, children: page }, `nav-button-${page}`));
|
|
11
|
+
};
|
|
12
|
+
export const Pagination = ({ pagesTotal, pageNumber, pageSize, showPageSelector, setPageNumber, setPageSize }) => {
|
|
13
|
+
const [pageInput, setPageInput] = React.useState(pageNumber);
|
|
14
|
+
const [shouldDebounce, setShouldDebounce] = React.useState(false);
|
|
15
|
+
const pageInputRef = React.useRef(pageNumber);
|
|
16
|
+
const breakpoint = useBreakpoint() ?? 'base';
|
|
17
|
+
const isMobile = ['base', 'sm'].includes(breakpoint);
|
|
18
|
+
const limit = isMobile ? PAGE_LIMIT_MOBILE : PAGE_LIMIT;
|
|
19
|
+
const pages = [];
|
|
20
|
+
for (let i = 0; pagesTotal > i; i++) {
|
|
21
|
+
pages.push(i + 1);
|
|
22
|
+
}
|
|
23
|
+
const handlePages = (pages, limit, pageNumber) => pages.filter(page => {
|
|
24
|
+
const cp = pageNumber < 4 ? 3 : pageNumber;
|
|
25
|
+
const rangeA = cp - (limit / 2);
|
|
26
|
+
const rangeB = cp + (limit / 2);
|
|
27
|
+
return page >= rangeA && page <= rangeB;
|
|
28
|
+
});
|
|
29
|
+
const handleChangePageInput = (shouldDebounce) => (ivalue) => {
|
|
30
|
+
const value = Number(ivalue) > pagesTotal ? pagesTotal : Number(ivalue);
|
|
31
|
+
setShouldDebounce(shouldDebounce);
|
|
32
|
+
setPageInput(value);
|
|
33
|
+
};
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
let timeout;
|
|
36
|
+
if ((pageInputRef.current !== pageInput) && !!pageInput) {
|
|
37
|
+
pageInputRef.current = pageInput;
|
|
38
|
+
setShouldDebounce(previousShouldDebounce => {
|
|
39
|
+
timeout = setTimeout(() => setPageNumber(pageInput), previousShouldDebounce ? 1000 : 0);
|
|
40
|
+
return false;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return () => clearTimeout(timeout);
|
|
44
|
+
}, [pageInput, setShouldDebounce, setPageNumber]);
|
|
45
|
+
return (_jsxs(Grid, { templateRows: {
|
|
46
|
+
sm: 'repeat(1, 1fr)',
|
|
47
|
+
}, templateColumns: {
|
|
48
|
+
sm: `repeat(1, 1fr)`,
|
|
49
|
+
md: `repeat(2, 1fr)`,
|
|
50
|
+
}, children: [showPageSelector ? (_jsx(_Fragment, { children: _jsx(GridItem, { mb: 4, children: _jsxs(Flex, { children: [_jsx(PageSizeSelector, { sizes: PAGE_SIZES, selectedPageSize: pageSize, onSelect: setPageSize }), _jsxs(Text, { fontSize: 'sm', fontWeight: 'normal', ml: 4, mt: 1, children: ["page ", _jsx("strong", { children: pageNumber.toString() }), " of ", _jsx("strong", { children: pagesTotal.toString() })] })] }) }) })) : _jsx(GridItem, { colSpan: 1 }), _jsx(GridItem, { children: _jsx(Field.Root, { children: _jsxs(HStack, { justifyContent: 'right', children: [handlePages(pages, limit, pageNumber).map(makeNavButton(handleChangePageInput(false), pageNumber)), _jsx(Field.Label, { htmlFor: 'pagination', style: { fontSize: 10 }, mt: 2, ml: 4, children: "goto page:" }), _jsxs(NumberInput.Root, { value: String(pageInput), size: 'sm', max: pagesTotal, min: 1, onValueChange: (details) => handleChangePageInput(true)(details.value), children: [_jsx(NumberInput.Input, { id: 'pagination', style: { width: 90 } }), _jsxs(NumberInput.Control, { children: [_jsx(NumberInput.IncrementTrigger, {}), _jsx(NumberInput.DecrementTrigger, {})] })] })] }) }) })] }));
|
|
51
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { IColumnFilter, TFilterChange } from "../types";
|
|
2
|
+
export interface ITableFilterProps {
|
|
3
|
+
filter: IColumnFilter;
|
|
4
|
+
onFilterChange: TFilterChange;
|
|
5
|
+
}
|
|
6
|
+
export default function TableFilter({ filter, onFilterChange, }: ITableFilterProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import TableFilterInput from "./TableFilterInput";
|
|
3
|
+
import { Field, IconButton, InputGroup } from "@chakra-ui/react";
|
|
4
|
+
import TableFilterDatePicker from "./TableFilterDatePicker";
|
|
5
|
+
import { FaX } from "react-icons/fa6";
|
|
6
|
+
import { Tooltip } from "../../ui/tooltip";
|
|
7
|
+
export default function TableFilter({ filter, onFilterChange, }) {
|
|
8
|
+
const handleChangeInput = (event) => {
|
|
9
|
+
const { value } = event.target;
|
|
10
|
+
onFilterChange({ ...filter, value });
|
|
11
|
+
};
|
|
12
|
+
const handleChangeDate = (date) => {
|
|
13
|
+
if (date) {
|
|
14
|
+
onFilterChange({ ...filter, value: new Date(date) });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const handleClearFilter = () => {
|
|
18
|
+
onFilterChange({ ...filter, value: undefined });
|
|
19
|
+
};
|
|
20
|
+
const ClearFilterButton = () => !!filter.value ? (_jsx(Tooltip, { content: `Clear filter`, positioning: { placement: 'bottom-end' }, showArrow: true, children: _jsx(IconButton, { "aria-label": `Clear filter ${filter.type}`, onClick: handleClearFilter, size: 'sm', variant: 'ghost', children: _jsx(FaX, {}) }) })) : null;
|
|
21
|
+
if (filter.type === 'string' || filter.type === 'number') {
|
|
22
|
+
return (_jsxs(Field.Root, { children: [_jsx(InputGroup, { endElement: _jsx(ClearFilterButton, {}), children: _jsx(TableFilterInput, { value: filter.value?.toString() || '', onChange: handleChangeInput }) }), _jsx(Field.HelperText, { color: 'gray.300', whiteSpace: 'nowrap', wordBreak: 'break-word', children: filter.helperText })] }));
|
|
23
|
+
}
|
|
24
|
+
if (filter.type === 'date') {
|
|
25
|
+
return (_jsxs(Field.Root, { children: [_jsx(InputGroup, { endElement: _jsx(ClearFilterButton, {}), children: _jsx(TableFilterDatePicker, { selected: filter.value, onChange: (date) => handleChangeDate(date) }) }), _jsx(Field.HelperText, { color: 'gray.300', whiteSpace: 'nowrap', wordBreak: 'break-word', children: filter.helperText })] }));
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Input } from "@chakra-ui/react";
|
|
3
|
+
export default function TableFilterDatePicker({ selected, onChange }) {
|
|
4
|
+
const value = selected ? selected.toISOString().split('T')[0] : '';
|
|
5
|
+
const handleChange = (e) => {
|
|
6
|
+
const val = e.target.value;
|
|
7
|
+
onChange(val ? new Date(val) : null);
|
|
8
|
+
};
|
|
9
|
+
return (_jsx(Input, { type: "date", value: value, onChange: handleChange, width: '100%', mt: 2, size: 'md' }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { NativeSelect } from "@chakra-ui/react";
|
|
3
|
+
export default function TableFilterSelect(props) {
|
|
4
|
+
return (_jsxs(NativeSelect.Root, { width: '100%', ...props, children: [_jsx(NativeSelect.Field, {}), _jsx(NativeSelect.Indicator, {})] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { IFilter } from "@vrobots/types";
|
|
2
|
+
import { IColumnFilter } from "../types";
|
|
3
|
+
export interface IUseDataTableArgs {
|
|
4
|
+
columnFilters: IColumnFilter[];
|
|
5
|
+
skip: number;
|
|
6
|
+
limit: number;
|
|
7
|
+
totalRecords: number;
|
|
8
|
+
sort?: {
|
|
9
|
+
[key: string]: 1 | -1;
|
|
10
|
+
};
|
|
11
|
+
baseFilter?: IFilter;
|
|
12
|
+
additionalFilters?: IFilter[];
|
|
13
|
+
shouldUseQuerystring?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export default function useDataTable(options: IUseDataTableArgs): {
|
|
16
|
+
pageSize: number;
|
|
17
|
+
setPageSize: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
18
|
+
pageNumber: number;
|
|
19
|
+
pagesTotal: number;
|
|
20
|
+
setPageNumber: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
21
|
+
query: import("@vrobots/types").IQuery;
|
|
22
|
+
filters: IColumnFilter[];
|
|
23
|
+
onFilterChange: (filter: IColumnFilter) => void;
|
|
24
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import useTableFilters from "./useTableFilters";
|
|
2
|
+
import usePagination from "./usePagination";
|
|
3
|
+
import usePageSize from "./usePageSize";
|
|
4
|
+
export default function useDataTable(options) {
|
|
5
|
+
const { columnFilters, skip, limit, totalRecords, sort, baseFilter, additionalFilters, shouldUseQuerystring, } = options;
|
|
6
|
+
const pageSize = usePageSize(limit);
|
|
7
|
+
const pagination = usePagination(skip, limit, totalRecords, shouldUseQuerystring);
|
|
8
|
+
const filters = useTableFilters(columnFilters, skip, pageSize.pageSize, pagination.pageNumber, sort, baseFilter, additionalFilters, shouldUseQuerystring);
|
|
9
|
+
return {
|
|
10
|
+
...filters,
|
|
11
|
+
...pagination,
|
|
12
|
+
...pageSize,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { PAGE_FIRST_PAGE } from "../constants";
|
|
3
|
+
export default function usePagination(skip, limit, totalRecords, shouldUseQuerystring) {
|
|
4
|
+
const defaultPageNumber = !!limit ? (skip / limit) + 1 : PAGE_FIRST_PAGE;
|
|
5
|
+
const [pageNumber, setPageNumber] = useState(defaultPageNumber);
|
|
6
|
+
const pagesTotal = Math.ceil(totalRecords / limit);
|
|
7
|
+
return {
|
|
8
|
+
pageNumber,
|
|
9
|
+
pagesTotal,
|
|
10
|
+
setPageNumber,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { IColumnFilter, TColumnFilterType } from "../types";
|
|
2
|
+
import { IFilter, IQuery } from "@vrobots/types";
|
|
3
|
+
export interface ITableFilter {
|
|
4
|
+
[key: string]: {
|
|
5
|
+
type: TColumnFilterType;
|
|
6
|
+
value: string | number | Date;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export default function useTableFilters(columnFilters: IColumnFilter[], skip: number, limit: number, pageNumber: number, sort?: {
|
|
10
|
+
[key: string]: 1 | -1;
|
|
11
|
+
}, baseFilter?: IFilter, additionalFilters?: IFilter[], shouldUseQuerystring?: boolean): {
|
|
12
|
+
query: IQuery;
|
|
13
|
+
filters: IColumnFilter[];
|
|
14
|
+
onFilterChange: (filter: IColumnFilter) => void;
|
|
15
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
export default function useTableFilters(columnFilters, skip, limit, pageNumber, sort, baseFilter, additionalFilters, shouldUseQuerystring) {
|
|
4
|
+
const [filters, setFilters] = useState(columnFilters);
|
|
5
|
+
const pathname = usePathname();
|
|
6
|
+
const router = useRouter();
|
|
7
|
+
const searchParams = useSearchParams();
|
|
8
|
+
const refPath = useRef('');
|
|
9
|
+
const query = useMemo(() => {
|
|
10
|
+
const coreFilters = filters.filter(filter => typeof filter.value !== 'undefined' && filter.value !== null && filter.value !== '');
|
|
11
|
+
const dateFilters = coreFilters
|
|
12
|
+
.filter(filter => filter.type === 'date')
|
|
13
|
+
.map((filter) => ({
|
|
14
|
+
logic: 'and',
|
|
15
|
+
column: filter.accessor,
|
|
16
|
+
operator: filter.operator,
|
|
17
|
+
dataType: filter.type,
|
|
18
|
+
value: new Date(filter?.value).getTime(),
|
|
19
|
+
}));
|
|
20
|
+
const nonDateFilters = coreFilters
|
|
21
|
+
.filter(filter => filter.type !== 'date')
|
|
22
|
+
.map((filter) => {
|
|
23
|
+
const filterParts = filter?.value?.toString().split(' ') || [];
|
|
24
|
+
if (filterParts.length === 1) {
|
|
25
|
+
return {
|
|
26
|
+
logic: 'and',
|
|
27
|
+
column: filter.accessor,
|
|
28
|
+
operator: filter.operator,
|
|
29
|
+
dataType: filter.type,
|
|
30
|
+
value: filter?.value,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
return {
|
|
35
|
+
logic: 'and',
|
|
36
|
+
filters: filterParts.map((part) => ({
|
|
37
|
+
logic: 'or',
|
|
38
|
+
column: filter.accessor,
|
|
39
|
+
operator: filter.operator,
|
|
40
|
+
dataType: filter.type,
|
|
41
|
+
value: part,
|
|
42
|
+
})),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
limit,
|
|
48
|
+
skip: (pageNumber - 1) * limit,
|
|
49
|
+
filter: {
|
|
50
|
+
logic: 'and',
|
|
51
|
+
...baseFilter,
|
|
52
|
+
filters: [
|
|
53
|
+
...dateFilters,
|
|
54
|
+
...nonDateFilters,
|
|
55
|
+
...additionalFilters || []
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
sort,
|
|
59
|
+
};
|
|
60
|
+
}, [limit, pageNumber, filters, baseFilter, additionalFilters, sort]);
|
|
61
|
+
const onFilterChange = (filter) => {
|
|
62
|
+
setFilters((previousFilters) => ([
|
|
63
|
+
...previousFilters.filter(f => f.accessor !== filter.accessor),
|
|
64
|
+
filter,
|
|
65
|
+
]));
|
|
66
|
+
};
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
69
|
+
const incomingQuery = searchParams.get('query');
|
|
70
|
+
if (refPath.current === '' && !!incomingQuery) {
|
|
71
|
+
const query = JSON.parse(incomingQuery);
|
|
72
|
+
const filters = query?.filter?.filters || [];
|
|
73
|
+
const updatedFiltersPopulated = filters.map(filter => {
|
|
74
|
+
const isNestedFilter = Array.isArray(filter.filters);
|
|
75
|
+
const columnFilter = columnFilters.find(f => f.accessor === filter.column);
|
|
76
|
+
if (columnFilter && !isNestedFilter) {
|
|
77
|
+
return {
|
|
78
|
+
...columnFilter,
|
|
79
|
+
value: filter.value,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (isNestedFilter) {
|
|
83
|
+
const nestedColumn = filter.filters?.[0]?.column;
|
|
84
|
+
const nestedColumnFilter = columnFilters.find(f => f.accessor === nestedColumn);
|
|
85
|
+
return {
|
|
86
|
+
...nestedColumnFilter,
|
|
87
|
+
value: filter.filters?.map(f => f.value).join(' '),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
})
|
|
92
|
+
.filter(filter => filter !== null);
|
|
93
|
+
const update = columnFilters.map(filter => {
|
|
94
|
+
const updatedFilter = updatedFiltersPopulated.find(f => f.accessor === filter.accessor);
|
|
95
|
+
if (updatedFilter) {
|
|
96
|
+
return updatedFilter;
|
|
97
|
+
}
|
|
98
|
+
return filter;
|
|
99
|
+
});
|
|
100
|
+
if (update?.length) {
|
|
101
|
+
setFilters(update);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
params.set('query', JSON.stringify(query));
|
|
106
|
+
}
|
|
107
|
+
const path = `${pathname}?${params.toString()}`;
|
|
108
|
+
if (shouldUseQuerystring && refPath.current !== path) {
|
|
109
|
+
refPath.current = path;
|
|
110
|
+
router.push(path);
|
|
111
|
+
}
|
|
112
|
+
}, [filters, pathname, router, shouldUseQuerystring, searchParams, skip, limit, baseFilter, additionalFilters, sort, columnFilters, query]);
|
|
113
|
+
return {
|
|
114
|
+
query,
|
|
115
|
+
filters,
|
|
116
|
+
onFilterChange,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type TFilterChange = (filter: IColumnFilter) => void;
|
|
2
|
+
export type TColumnFilterType = 'date' | 'string' | 'number' | 'boolean';
|
|
3
|
+
export type TColumnFilterOperation = '%' | '!%' | '^%' | '%^' | '=' | '!=' | '>' | '>=' | '<' | '<=' | 'null';
|
|
4
|
+
export interface IColumnFilter {
|
|
5
|
+
id: string;
|
|
6
|
+
accessor: string;
|
|
7
|
+
type: TColumnFilterType;
|
|
8
|
+
operator: TColumnFilterOperation;
|
|
9
|
+
helperText?: string;
|
|
10
|
+
value?: string | number | Date;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { IDataTableProps } from '../components/table/DataTable';
|
|
3
|
+
declare const meta: {
|
|
4
|
+
title: string;
|
|
5
|
+
component: import("react").FC<IDataTableProps>;
|
|
6
|
+
tags: string[];
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: string;
|
|
9
|
+
};
|
|
10
|
+
args: {
|
|
11
|
+
data: never[];
|
|
12
|
+
total: number;
|
|
13
|
+
pageNumber: number;
|
|
14
|
+
pagesTotal: number;
|
|
15
|
+
pageSize: number;
|
|
16
|
+
setPageNumber: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
17
|
+
setPageSize: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
18
|
+
columns: never[];
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
export declare const Default: Story;
|
|
24
|
+
export declare const WithRowClick: Story;
|
|
25
|
+
export declare const EmptyState: Story;
|
|
26
|
+
export declare const WithFilters: Story;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import DataTable from '../components/table/DataTable';
|
|
4
|
+
const noopSetPageNumber = () => undefined;
|
|
5
|
+
const noopSetPageSize = () => undefined;
|
|
6
|
+
const SAMPLE_DATA = [
|
|
7
|
+
{ id: 'P-001', name: 'Alice Johnson', dob: '1985-03-12', provider: 'Dr. Smith', status: 'Active', balance: 125.0 },
|
|
8
|
+
{ id: 'P-002', name: 'Bob Martinez', dob: '1990-07-24', provider: 'Dr. Patel', status: 'Inactive', balance: 0.0 },
|
|
9
|
+
{ id: 'P-003', name: 'Carol White', dob: '1978-11-05', provider: 'Dr. Smith', status: 'Active', balance: 340.5 },
|
|
10
|
+
{ id: 'P-004', name: 'David Brown', dob: '2000-01-30', provider: 'Dr. Lee', status: 'Pending', balance: 75.25 },
|
|
11
|
+
{ id: 'P-005', name: 'Eva Green', dob: '1995-09-18', provider: 'Dr. Patel', status: 'Active', balance: 210.0 },
|
|
12
|
+
];
|
|
13
|
+
const COLUMNS = [
|
|
14
|
+
{
|
|
15
|
+
accessorKey: 'id',
|
|
16
|
+
header: 'ID',
|
|
17
|
+
cell: (info) => info.getValue(),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
accessorKey: 'name',
|
|
21
|
+
header: 'Patient Name',
|
|
22
|
+
cell: (info) => info.getValue(),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
accessorKey: 'dob',
|
|
26
|
+
header: 'Date of Birth',
|
|
27
|
+
cell: (info) => info.getValue(),
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
accessorKey: 'provider',
|
|
31
|
+
header: 'Provider',
|
|
32
|
+
cell: (info) => info.getValue(),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
accessorKey: 'status',
|
|
36
|
+
header: 'Status',
|
|
37
|
+
cell: (info) => info.getValue(),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
accessorKey: 'balance',
|
|
41
|
+
header: 'Balance',
|
|
42
|
+
cell: (info) => `$${info.getValue().toFixed(2)}`,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
function DataTableDemo(props) {
|
|
46
|
+
const [pageNumber, setPageNumber] = useState(1);
|
|
47
|
+
const [pageSize, setPageSize] = useState(25);
|
|
48
|
+
return (_jsx(DataTable, { data: SAMPLE_DATA, total: SAMPLE_DATA.length, columns: COLUMNS, pageNumber: pageNumber, pagesTotal: Math.ceil(SAMPLE_DATA.length / pageSize), pageSize: pageSize, setPageNumber: setPageNumber, setPageSize: setPageSize, ...props }));
|
|
49
|
+
}
|
|
50
|
+
const meta = {
|
|
51
|
+
title: 'Data/DataTable',
|
|
52
|
+
component: DataTable,
|
|
53
|
+
tags: ['autodocs'],
|
|
54
|
+
parameters: {
|
|
55
|
+
layout: 'padded',
|
|
56
|
+
},
|
|
57
|
+
args: {
|
|
58
|
+
data: [],
|
|
59
|
+
total: 0,
|
|
60
|
+
pageNumber: 1,
|
|
61
|
+
pagesTotal: 1,
|
|
62
|
+
pageSize: 25,
|
|
63
|
+
setPageNumber: noopSetPageNumber,
|
|
64
|
+
setPageSize: noopSetPageSize,
|
|
65
|
+
columns: [],
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
export default meta;
|
|
69
|
+
export const Default = {
|
|
70
|
+
args: {},
|
|
71
|
+
render: () => _jsx(DataTableDemo, {}),
|
|
72
|
+
};
|
|
73
|
+
export const WithRowClick = {
|
|
74
|
+
args: {},
|
|
75
|
+
render: () => (_jsx(DataTableDemo, { onRowClick: (row) => alert(`Clicked: ${row.name}`) })),
|
|
76
|
+
};
|
|
77
|
+
export const EmptyState = {
|
|
78
|
+
args: {},
|
|
79
|
+
render: () => _jsx(EmptyStateDemo, {}),
|
|
80
|
+
};
|
|
81
|
+
function EmptyStateDemo() {
|
|
82
|
+
const [pageNumber, setPageNumber] = useState(1);
|
|
83
|
+
const [pageSize, setPageSize] = useState(25);
|
|
84
|
+
return (_jsx(DataTable, { data: [], total: 0, columns: COLUMNS, pageNumber: pageNumber, pagesTotal: 0, pageSize: pageSize, setPageNumber: setPageNumber, setPageSize: setPageSize }));
|
|
85
|
+
}
|
|
86
|
+
export const WithFilters = {
|
|
87
|
+
args: {},
|
|
88
|
+
render: () => (_jsx(DataTableDemo, { filters: [
|
|
89
|
+
{ id: 'name', accessor: 'name', type: 'string', operator: '%', helperText: 'Search by name' },
|
|
90
|
+
{ id: 'status', accessor: 'status', type: 'string', operator: '=', helperText: 'Filter by status' },
|
|
91
|
+
], onFilterChange: (filter) => console.log('Filter changed:', filter) })),
|
|
92
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vrobots/storybook",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
"@chakra-ui/react": "^3.32.0",
|
|
22
22
|
"@emotion/react": "^11.14.0",
|
|
23
23
|
"@storybook/addon-themes": "^10.2.16",
|
|
24
|
+
"@tanstack/react-table": "^8.21.3",
|
|
24
25
|
"axios": "^1.13.4",
|
|
25
26
|
"motion": "^12.34.0",
|
|
27
|
+
"next": "^16.2.4",
|
|
26
28
|
"next-themes": "^0.4.6",
|
|
27
29
|
"owasp-password-strength-test": "^1.3.0",
|
|
28
30
|
"react": "^19.2.0",
|
|
@@ -40,7 +42,6 @@
|
|
|
40
42
|
"@storybook/addon-docs": "^10.2.16",
|
|
41
43
|
"@storybook/addon-vitest": "^10.2.16",
|
|
42
44
|
"@storybook/react-vite": "^10.2.16",
|
|
43
|
-
"@storybook/test": "^8.6.15",
|
|
44
45
|
"@types/node": "^24.10.1",
|
|
45
46
|
"@types/react": "^19.2.5",
|
|
46
47
|
"@types/react-dom": "^19.2.3",
|