@sio-group/ui-datatable 0.1.0
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/CHANGELOG.md +16 -0
- package/README.md +429 -0
- package/dist/index.cjs +647 -0
- package/dist/index.d.cts +83 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +620 -0
- package/dist/styles/index.css +154 -0
- package/dist/styles/index.css.map +1 -0
- package/package.json +44 -0
- package/src/assets/scss/index.scss +170 -0
- package/src/assets/scss/tokens/_color.scss +19 -0
- package/src/assets/scss/tokens/_datatable.scss +10 -0
- package/src/components/ActionCell.tsx +88 -0
- package/src/components/DataTable.tsx +85 -0
- package/src/components/DataTableBody.tsx +34 -0
- package/src/components/DataTableControls.tsx +35 -0
- package/src/components/DataTableHeader.tsx +59 -0
- package/src/components/DefaultSortIcon.tsx +13 -0
- package/src/components/TableCell.tsx +17 -0
- package/src/components/cell-types/BooleanCell.tsx +29 -0
- package/src/components/cell-types/DateCell.tsx +28 -0
- package/src/components/cell-types/EmptyCell.tsx +3 -0
- package/src/components/cell-types/InlineInputCell.tsx +129 -0
- package/src/hooks/useDataTable.ts +113 -0
- package/src/index.ts +14 -0
- package/src/types/action-cell-props.d.ts +9 -0
- package/src/types/action-menu.d.ts +15 -0
- package/src/types/column.d.ts +10 -0
- package/src/types/data-table-body-props.d.ts +16 -0
- package/src/types/data-table-header-props.d.ts +11 -0
- package/src/types/data-table-props.d.ts +32 -0
- package/src/types/entity.d.ts +4 -0
- package/src/types/form-field.d.ts +8 -0
- package/src/types/index.ts +11 -0
- package/src/types/pagination-meta.d.ts +7 -0
- package/src/types/sort-state.d.ts +6 -0
- package/src/types/table-cell-props.d.ts +9 -0
- package/src/types/use-data-table-props.d.ts +14 -0
- package/src/types/use-data-table-return.d.ts +14 -0
- package/src/utils/is-pill-value.ts +7 -0
- package/src/utils/render-object.tsx +18 -0
- package/src/utils/render-value.tsx +89 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +8 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {Column, DataTableHeaderProps} from "../types";
|
|
2
|
+
import {DefaultSortIcon} from "./DefaultSortIcon";
|
|
3
|
+
|
|
4
|
+
export const DataTableHeader = <T extends { id: string | number }>({
|
|
5
|
+
columns,
|
|
6
|
+
onSort,
|
|
7
|
+
sortValue,
|
|
8
|
+
hasActionMenu,
|
|
9
|
+
renderSortIcon,
|
|
10
|
+
}: DataTableHeaderProps<T>) => {
|
|
11
|
+
const handleSort = (column: Column<T>) => {
|
|
12
|
+
if (!onSort) return;
|
|
13
|
+
|
|
14
|
+
if (!sortValue || sortValue.name !== column.name) {
|
|
15
|
+
onSort({ name: column.name, direction: 'asc' });
|
|
16
|
+
} else if (sortValue.direction === 'asc') {
|
|
17
|
+
onSort({ ...sortValue, direction: 'desc' });
|
|
18
|
+
} else {
|
|
19
|
+
onSort(null);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<thead>
|
|
25
|
+
<tr>
|
|
26
|
+
{hasActionMenu && <th aria-label="Actions" />}
|
|
27
|
+
{columns.map((column: Column<T>) => (
|
|
28
|
+
<th
|
|
29
|
+
onClick={() => column.sort && handleSort(column)}
|
|
30
|
+
className={[column.className, column.sort ? 'sort' : null].filter(Boolean).join(' ')}
|
|
31
|
+
style={column.style}
|
|
32
|
+
key={String(column.name)}
|
|
33
|
+
>
|
|
34
|
+
<span>
|
|
35
|
+
<span className="label">{column.label}</span>
|
|
36
|
+
{column.sort && (
|
|
37
|
+
<span className="icons">
|
|
38
|
+
{renderSortIcon ? (
|
|
39
|
+
<>
|
|
40
|
+
{renderSortIcon('asc', sortValue?.name === column.name && sortValue?.direction === 'asc')}
|
|
41
|
+
{renderSortIcon('desc', sortValue?.name === column.name && sortValue?.direction === 'desc')}
|
|
42
|
+
</>
|
|
43
|
+
) : (
|
|
44
|
+
<>
|
|
45
|
+
<DefaultSortIcon direction="asc"
|
|
46
|
+
active={sortValue?.name === column.name && sortValue?.direction === 'asc'}/>
|
|
47
|
+
<DefaultSortIcon direction="desc"
|
|
48
|
+
active={sortValue?.name === column.name && sortValue?.direction === 'desc'}/>
|
|
49
|
+
</>
|
|
50
|
+
)}
|
|
51
|
+
</span>
|
|
52
|
+
)}
|
|
53
|
+
</span>
|
|
54
|
+
</th>
|
|
55
|
+
))}
|
|
56
|
+
</tr>
|
|
57
|
+
</thead>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {SortDirection} from "../types";
|
|
2
|
+
|
|
3
|
+
export const DefaultSortIcon = ({
|
|
4
|
+
direction,
|
|
5
|
+
active,
|
|
6
|
+
}: { direction: SortDirection, active: boolean }) => (
|
|
7
|
+
<span
|
|
8
|
+
style={{ opacity: active ? 1 : 0.3, fontSize: '0.7em' }}
|
|
9
|
+
aria-hidden="true"
|
|
10
|
+
>
|
|
11
|
+
{direction === 'asc' ? '▲' : '▼'}
|
|
12
|
+
</span>
|
|
13
|
+
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {TableCellProps} from "../types";
|
|
2
|
+
import {renderValue} from "../utils/render-value";
|
|
3
|
+
|
|
4
|
+
export const TableCell = <T extends { id: number | string }> ({
|
|
5
|
+
column,
|
|
6
|
+
item,
|
|
7
|
+
formFields,
|
|
8
|
+
updateData,
|
|
9
|
+
}: TableCellProps<T>) => {
|
|
10
|
+
const cellValue: T[keyof T] = item[column.name];
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<td className={column.className ?? ''}>
|
|
14
|
+
{renderValue({value: cellValue, column, item, formFields, updateData})}
|
|
15
|
+
</td>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {Column} from "../../types";
|
|
2
|
+
import {Button} from "@sio-group/ui-core";
|
|
3
|
+
|
|
4
|
+
interface BooleanCellProps<T extends { id: number | string }> {
|
|
5
|
+
item: T;
|
|
6
|
+
column: Column<T>;
|
|
7
|
+
value: T[keyof T];
|
|
8
|
+
updateData?: (id: string | number, values: Partial<T>) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const BooleanCell = <T extends { id: string | number }> ({
|
|
12
|
+
column,
|
|
13
|
+
value,
|
|
14
|
+
item,
|
|
15
|
+
updateData,
|
|
16
|
+
}: BooleanCellProps<T>) => (
|
|
17
|
+
<Button
|
|
18
|
+
color={value ? "success" : "error"}
|
|
19
|
+
variant={column.format === "button" ? "primary" : "link"}
|
|
20
|
+
onClick={() =>
|
|
21
|
+
updateData?.(item.id, {
|
|
22
|
+
[column.name]: !value
|
|
23
|
+
} as Partial<T>)
|
|
24
|
+
}
|
|
25
|
+
ariaLabel={String(column.name)}
|
|
26
|
+
>
|
|
27
|
+
{value ? '✓' : '✗'}
|
|
28
|
+
</Button>
|
|
29
|
+
);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {Column} from "../../types";
|
|
2
|
+
|
|
3
|
+
interface DateCellProps<T extends { id: number | string }> {
|
|
4
|
+
column: Column<T>;
|
|
5
|
+
value: T[keyof T];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const DateCell = <T extends { id: string | number }> ({
|
|
9
|
+
column,
|
|
10
|
+
value,
|
|
11
|
+
}: DateCellProps<T>) => (
|
|
12
|
+
new Date(value as string)
|
|
13
|
+
.toLocaleString("nl-BE", {
|
|
14
|
+
timeZone: "Europe/Brussels",
|
|
15
|
+
year: "numeric",
|
|
16
|
+
month: "2-digit",
|
|
17
|
+
day: "2-digit",
|
|
18
|
+
...(column.format === "datetime"
|
|
19
|
+
? {
|
|
20
|
+
hour: "2-digit",
|
|
21
|
+
minute: "2-digit",
|
|
22
|
+
second: "2-digit",
|
|
23
|
+
}
|
|
24
|
+
: {}),
|
|
25
|
+
hour12: false,
|
|
26
|
+
})
|
|
27
|
+
.replace(/\//g, "-")
|
|
28
|
+
);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {KeyboardEventHandler, useEffect, useState} from "react";
|
|
2
|
+
import {Column, FormField} from "../../types";
|
|
3
|
+
import {Button} from "@sio-group/ui-core";
|
|
4
|
+
|
|
5
|
+
interface InlineInputCellProps<T extends { id: string | number }> {
|
|
6
|
+
column: Column<T>;
|
|
7
|
+
formField: FormField;
|
|
8
|
+
item: T;
|
|
9
|
+
value: T[keyof T];
|
|
10
|
+
updateData?: (id: string | number, values: Partial<T>) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const InlineInputCell = <T extends { id: string | number }>({
|
|
14
|
+
column,
|
|
15
|
+
formField,
|
|
16
|
+
item,
|
|
17
|
+
value,
|
|
18
|
+
updateData,
|
|
19
|
+
}: InlineInputCellProps<T>) => {
|
|
20
|
+
const [showEdit, setShowEdit] = useState(false);
|
|
21
|
+
const [fieldValue, setFieldValue] = useState(String(value));
|
|
22
|
+
const [isValid, setIsValid] = useState(true)
|
|
23
|
+
|
|
24
|
+
const handleCancel = () => {
|
|
25
|
+
setShowEdit(false);
|
|
26
|
+
setFieldValue(String(value));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleSave = async () => {
|
|
30
|
+
updateData?.(item.id, {[formField.name]: fieldValue} as Partial<T>);
|
|
31
|
+
setFieldValue(String(value));
|
|
32
|
+
setShowEdit(false);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
let bool: boolean = true;
|
|
37
|
+
if (formField.required) {
|
|
38
|
+
bool = fieldValue !== null && fieldValue !== '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setIsValid(bool)
|
|
42
|
+
}, [fieldValue]);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const handleEsc = (e: KeyboardEvent) => {
|
|
46
|
+
if (e.key === "Escape") setShowEdit(false);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
document.addEventListener("keydown", handleEsc);
|
|
50
|
+
return () => document.removeEventListener("keydown", handleEsc);
|
|
51
|
+
}, [setShowEdit]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
{showEdit ? (
|
|
56
|
+
<form noValidate>
|
|
57
|
+
{formField.type === "select" || formField.type === "radio" ? (
|
|
58
|
+
<select
|
|
59
|
+
id={formField.name}
|
|
60
|
+
name={formField.name}
|
|
61
|
+
value={fieldValue}
|
|
62
|
+
onChange={(e) => setFieldValue(e.target.value)}
|
|
63
|
+
autoFocus={true}
|
|
64
|
+
>
|
|
65
|
+
{formField?.options?.map((option) => {
|
|
66
|
+
const val: string = typeof option === 'string' ? option : option.value;
|
|
67
|
+
const label: string = typeof option === 'string' ? option : option.label;
|
|
68
|
+
|
|
69
|
+
return <option value={val} key={val}>{label}</option>
|
|
70
|
+
})}
|
|
71
|
+
</select>
|
|
72
|
+
) : (
|
|
73
|
+
<input
|
|
74
|
+
type="text"
|
|
75
|
+
id={formField.name}
|
|
76
|
+
name={formField.name}
|
|
77
|
+
value={fieldValue as string}
|
|
78
|
+
onChange={(e) => setFieldValue(e.target.value)}
|
|
79
|
+
autoFocus={true}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
<div className="btn-group">
|
|
83
|
+
<Button
|
|
84
|
+
type="submit"
|
|
85
|
+
variant="link"
|
|
86
|
+
color="success"
|
|
87
|
+
onClick={handleSave}
|
|
88
|
+
ariaLabel="inline edit field"
|
|
89
|
+
disabled={!isValid}
|
|
90
|
+
label="✓"
|
|
91
|
+
/>
|
|
92
|
+
<Button
|
|
93
|
+
type="button"
|
|
94
|
+
variant="link"
|
|
95
|
+
color="error"
|
|
96
|
+
onClick={handleCancel}
|
|
97
|
+
ariaLabel="inline edit field"
|
|
98
|
+
label="✗"
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
</form>
|
|
102
|
+
) : (
|
|
103
|
+
<>
|
|
104
|
+
{value}
|
|
105
|
+
<Button
|
|
106
|
+
variant="link"
|
|
107
|
+
onClick={() => setShowEdit(true)}
|
|
108
|
+
ariaLabel="inline edit field"
|
|
109
|
+
>
|
|
110
|
+
<svg
|
|
111
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
112
|
+
width="16"
|
|
113
|
+
height="16"
|
|
114
|
+
viewBox="0 0 24 24"
|
|
115
|
+
fill="none"
|
|
116
|
+
stroke="currentColor"
|
|
117
|
+
strokeWidth="2"
|
|
118
|
+
strokeLinecap="round"
|
|
119
|
+
strokeLinejoin="round"
|
|
120
|
+
>
|
|
121
|
+
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
|
122
|
+
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
|
123
|
+
</svg>
|
|
124
|
+
</Button>
|
|
125
|
+
</>
|
|
126
|
+
)}
|
|
127
|
+
</>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {useMemo, useState} from "react";
|
|
2
|
+
import {UseDataTableProps} from "../types/use-data-table-props";
|
|
3
|
+
import {UseDataTableReturn} from "../types/use-data-table-return";
|
|
4
|
+
import {SortState} from "../types";
|
|
5
|
+
|
|
6
|
+
export const useDataTable = <T extends { id: string | number }>({
|
|
7
|
+
data,
|
|
8
|
+
pagination,
|
|
9
|
+
onSearch,
|
|
10
|
+
onSort,
|
|
11
|
+
onPaginate,
|
|
12
|
+
searchValue,
|
|
13
|
+
sortValue,
|
|
14
|
+
clientPageSize,
|
|
15
|
+
clientSearchKeys
|
|
16
|
+
}: UseDataTableProps<T>): UseDataTableReturn<T> => {
|
|
17
|
+
const isControlled: boolean = pagination !== undefined;
|
|
18
|
+
|
|
19
|
+
const showPagination: boolean = isControlled ? !!onPaginate : !!clientPageSize;
|
|
20
|
+
const showSearch: boolean = isControlled ? !!onSearch : !!clientSearchKeys?.length;
|
|
21
|
+
|
|
22
|
+
const [clientSearch, setClientSearch] = useState('');
|
|
23
|
+
const [clientSort, setClientSort] = useState<SortState<T> | null>(null);
|
|
24
|
+
const [clientPage, setClientPage] = useState(1);
|
|
25
|
+
|
|
26
|
+
const handleSearch = (query: string) => {
|
|
27
|
+
if (isControlled) {
|
|
28
|
+
onSearch?.(query);
|
|
29
|
+
} else {
|
|
30
|
+
setClientSearch(query);
|
|
31
|
+
setClientPage(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handleSort = (sort: SortState<T> | null) => {
|
|
36
|
+
if (isControlled) {
|
|
37
|
+
onSort?.(sort);
|
|
38
|
+
} else {
|
|
39
|
+
setClientSort(sort);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const handlePaginate = (page: number) => {
|
|
44
|
+
if (isControlled) {
|
|
45
|
+
onPaginate?.(page);
|
|
46
|
+
} else {
|
|
47
|
+
setClientPage(page);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const processedData = useMemo(() => {
|
|
52
|
+
if (isControlled) return data;
|
|
53
|
+
|
|
54
|
+
let result = [...data];
|
|
55
|
+
|
|
56
|
+
if (clientSearch && clientSearchKeys?.length) {
|
|
57
|
+
result = result.filter((item) =>
|
|
58
|
+
clientSearchKeys.some((key) =>
|
|
59
|
+
String(item[key]).toLowerCase().includes(clientSearch.toLowerCase())
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (clientSort) {
|
|
65
|
+
result.sort((a: T, b: T) => {
|
|
66
|
+
const aVal: string = String(a[clientSort.name]);
|
|
67
|
+
const bVal: string = String(b[clientSort.name]);
|
|
68
|
+
return clientSort.direction === 'asc'
|
|
69
|
+
? aVal.localeCompare(bVal)
|
|
70
|
+
: bVal.localeCompare(aVal);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result;
|
|
75
|
+
}, [data, clientSearch, clientSort, isControlled]);
|
|
76
|
+
|
|
77
|
+
const paginationMeta = useMemo(() => {
|
|
78
|
+
if (isControlled || !clientPageSize) return pagination;
|
|
79
|
+
|
|
80
|
+
const pageSize: number = clientPageSize;
|
|
81
|
+
const total: number = processedData.length;
|
|
82
|
+
const pageCount: number = Math.ceil(total / pageSize);
|
|
83
|
+
const from: number = (clientPage - 1) * pageSize;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
currentPage: clientPage,
|
|
87
|
+
pageCount,
|
|
88
|
+
total,
|
|
89
|
+
from: from + 1,
|
|
90
|
+
to: Math.min(from + pageSize, total)
|
|
91
|
+
};
|
|
92
|
+
}, [isControlled, pagination, processedData, clientPage, clientPageSize]);
|
|
93
|
+
|
|
94
|
+
const pagedData = useMemo(() => {
|
|
95
|
+
if (isControlled || !clientPageSize) return data;
|
|
96
|
+
|
|
97
|
+
const pageSize: number = clientPageSize;
|
|
98
|
+
const from: number = (clientPage - 1) * pageSize;
|
|
99
|
+
return processedData.slice(from, from + pageSize);
|
|
100
|
+
}, [isControlled, processedData, clientPage, clientPageSize]);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
pagedData,
|
|
104
|
+
paginationMeta,
|
|
105
|
+
showPagination,
|
|
106
|
+
showSearch,
|
|
107
|
+
handleSearch,
|
|
108
|
+
handleSort,
|
|
109
|
+
handlePaginate,
|
|
110
|
+
currentSort: isControlled ? sortValue : clientSort,
|
|
111
|
+
currentSearch: isControlled ? searchValue : clientSearch,
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import {ActionMenu} from "./action-menu";
|
|
2
|
+
import {Entity} from "./entity";
|
|
3
|
+
import {ReactNode} from "react";
|
|
4
|
+
|
|
5
|
+
export interface ActionCellProps <T extends { id: string | number }> {
|
|
6
|
+
actionMenu?: ActionMenu<T>;
|
|
7
|
+
item: T;
|
|
8
|
+
renderMenuIcon?: () => ReactNode;
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {ReactNode} from "react";
|
|
2
|
+
|
|
3
|
+
export type ActionMenuType = 'inline' | 'dropdown';
|
|
4
|
+
|
|
5
|
+
export interface Action <T extends { id: string | number }> {
|
|
6
|
+
name: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: ReactNode;
|
|
9
|
+
onClick: (item: T) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ActionMenu <T extends { id: string | number }> {
|
|
13
|
+
type: ActionMenuType;
|
|
14
|
+
actions: Action<T>[];
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {CSSProperties} from "react";
|
|
2
|
+
|
|
3
|
+
export interface Column<T extends { id: number | string }> {
|
|
4
|
+
name: keyof T;
|
|
5
|
+
label: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
style?: CSSProperties;
|
|
8
|
+
sort?: boolean;
|
|
9
|
+
format?: 'boolean' | 'button' | 'datetime' | 'date' | 'pill' | 'email' | { key: string };
|
|
10
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {Column} from "./column";
|
|
2
|
+
import {Entity} from "./entity";
|
|
3
|
+
import {ActionMenu} from "./action-menu";
|
|
4
|
+
import {FormField} from "./form-field";
|
|
5
|
+
import {ReactNode} from "react";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export interface DataTableBodyProps <T extends { id: number | string }> {
|
|
9
|
+
item: T;
|
|
10
|
+
columns: Column<T>[];
|
|
11
|
+
entity?: Entity;
|
|
12
|
+
actionMenu?: ActionMenu<T>;
|
|
13
|
+
formFields?: FormField[];
|
|
14
|
+
updateData?: (id: string | number, values: Partial<T>) => void;
|
|
15
|
+
renderMenuIcon?: () => ReactNode;
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {Column} from "./column";
|
|
2
|
+
import {SortDirection, SortState} from "./sort-state";
|
|
3
|
+
import {ReactNode} from "react";
|
|
4
|
+
|
|
5
|
+
export interface DataTableHeaderProps<T extends { id: string | number }>{
|
|
6
|
+
columns: Column<T>[];
|
|
7
|
+
onSort: (sort: SortState | null) => void;
|
|
8
|
+
sortValue?: SortState | null;
|
|
9
|
+
hasActionMenu: boolean;
|
|
10
|
+
renderSortIcon?: (direction: SortDirection, active: boolean) => ReactNode,
|
|
11
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {CSSProperties, ReactNode} from "react";
|
|
2
|
+
import {SortDirection, SortState} from "./sort-state";
|
|
3
|
+
import {Column} from "./column";
|
|
4
|
+
import {Entity} from "./entity";
|
|
5
|
+
import {ActionMenu} from "./action-menu";
|
|
6
|
+
import {FormField} from "./form-field";
|
|
7
|
+
import {PaginationMeta} from "./pagination-meta";
|
|
8
|
+
|
|
9
|
+
export type Color = 'default' | 'error' | 'success' | 'warning' | 'caution' | 'info';
|
|
10
|
+
|
|
11
|
+
export interface DataTableProps<T extends { id: string | number }> {
|
|
12
|
+
columns: Column<T>[],
|
|
13
|
+
data: T[],
|
|
14
|
+
pagination?: PaginationMeta,
|
|
15
|
+
onPaginate?: (page: number) => void,
|
|
16
|
+
onSearch?: (query: string) => void,
|
|
17
|
+
onSort?: (sort: SortState | null) => void,
|
|
18
|
+
searchValue?: string,
|
|
19
|
+
sortValue?: SortState | null,
|
|
20
|
+
clientPageSize?: number | null,
|
|
21
|
+
clientSearchKeys?: (keyof T)[],
|
|
22
|
+
entity?: Entity,
|
|
23
|
+
actionMenu?: ActionMenu<T>,
|
|
24
|
+
renderMenuIcon?: () => ReactNode,
|
|
25
|
+
onUpdate?: (id: string | number, values: Partial<T>) => void,
|
|
26
|
+
formFields?: FormField[],
|
|
27
|
+
renderSortIcon?: (direction: SortDirection, active: boolean) => ReactNode,
|
|
28
|
+
emptyMessage?: string,
|
|
29
|
+
striped?: boolean,
|
|
30
|
+
hover?: boolean,
|
|
31
|
+
style?: CSSProperties,
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { DataTableProps } from "./data-table-props";
|
|
2
|
+
export { DataTableHeaderProps } from "./data-table-header-props";
|
|
3
|
+
export { ActionCellProps } from "./action-cell-props";
|
|
4
|
+
export { TableCellProps } from "./table-cell-props";
|
|
5
|
+
export { SortDirection, SortState } from "./sort-state";
|
|
6
|
+
|
|
7
|
+
export { ActionMenuType, Action, ActionMenu } from "./action-menu";
|
|
8
|
+
export { FormFieldType, FormField } from "./form-field";
|
|
9
|
+
|
|
10
|
+
export { Column } from "./column";
|
|
11
|
+
export { Entity } from "./entity";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import {Column} from "./column";
|
|
2
|
+
import {FormField} from "./form-field";
|
|
3
|
+
|
|
4
|
+
export interface TableCellProps <T extends { id: string | number }> {
|
|
5
|
+
column: Column<T>;
|
|
6
|
+
item: T;
|
|
7
|
+
formFields?: FormField[];
|
|
8
|
+
updateData?: (id: string | number, values: Partial<T>) => void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {SortState} from "./sort-state";
|
|
2
|
+
import {PaginationMeta} from "./pagination-meta";
|
|
3
|
+
|
|
4
|
+
export interface UseDataTableProps <T extends { id: number | string }>{
|
|
5
|
+
data: T[],
|
|
6
|
+
pagination?: PaginationMeta,
|
|
7
|
+
onSearch?: (query: string) => void,
|
|
8
|
+
onSort?: (sort: SortState | null) => void,
|
|
9
|
+
onPaginate?: (page: number) => void,
|
|
10
|
+
searchValue?: string,
|
|
11
|
+
sortValue?: SortState | null,
|
|
12
|
+
clientPageSize?: number | null,
|
|
13
|
+
clientSearchKeys?: (keyof T)[],
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {SortState} from "./sort-state";
|
|
2
|
+
import {PaginationMeta} from "./pagination-meta";
|
|
3
|
+
|
|
4
|
+
export interface UseDataTableReturn <T extends { id: number | string }>{
|
|
5
|
+
pagedData: T[],
|
|
6
|
+
paginationMeta?: PaginationMeta,
|
|
7
|
+
showPagination: boolean,
|
|
8
|
+
showSearch: boolean,
|
|
9
|
+
handleSearch: (query: string) => void,
|
|
10
|
+
handleSort: (sort: SortState | null) => void,
|
|
11
|
+
handlePaginate: (page: number) => void,
|
|
12
|
+
currentSort?: SortState | null,
|
|
13
|
+
currentSearch?: string | null,
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {EmptyCell} from "../components/cell-types/EmptyCell";
|
|
2
|
+
|
|
3
|
+
export const renderObject = (obj: Record<string, unknown>) => {
|
|
4
|
+
const entries = Object.entries(obj);
|
|
5
|
+
|
|
6
|
+
if (!entries.length) return <EmptyCell />;
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
{entries.map(([key, val]) => (
|
|
11
|
+
<div key={key}>
|
|
12
|
+
<span>{key}: </span>
|
|
13
|
+
<span>{String(val)}</span>
|
|
14
|
+
</div>
|
|
15
|
+
))}
|
|
16
|
+
</>
|
|
17
|
+
)
|
|
18
|
+
}
|