@react-fabric/data 1.0.0-beta.9 → 1.0.2
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/{src/histogram/Histogram.module.css → css/_histogram.css} +16 -13
- package/css/styles.css +24 -0
- package/dist/cjs/checklist/CheckList.js +59 -33
- package/dist/cjs/histogram/Histogram.js +41 -26
- package/dist/cjs/hooks/useFilteredList.js +100 -17
- package/dist/cjs/hooks/usePagination.js +29 -8
- package/dist/cjs/hooks/useSelectableList.js +30 -5
- package/dist/cjs/i18n/{ar.json.js → ar/data.json.js} +8 -1
- package/dist/cjs/i18n/{en.json.js → en/data.json.js} +8 -1
- package/dist/cjs/i18n/index.js +4 -4
- package/dist/cjs/index.js +30 -0
- package/dist/cjs/json/JsonViewer.js +80 -43
- package/dist/cjs/pagination/Pagination.js +24 -15
- package/dist/cjs/table/AddColumn.js +5 -5
- package/dist/cjs/table/BodyCell.js +20 -13
- package/dist/cjs/table/BodyRow.js +41 -28
- package/dist/cjs/table/CheckboxCell.js +10 -9
- package/dist/cjs/table/ColumnFilters.js +100 -0
- package/dist/cjs/table/EmptyCell.js +4 -4
- package/dist/cjs/table/HeaderCell.js +79 -53
- package/dist/cjs/table/HtmlTable.js +84 -0
- package/dist/cjs/table/{Context.js → Provider.js} +8 -10
- package/dist/cjs/table/Table.js +99 -33
- package/dist/cjs/{tree/types.js → table/context.js} +9 -5
- package/dist/cjs/table/types.js +29 -0
- package/dist/cjs/table/useResizer.js +10 -10
- package/dist/cjs/table/useTableColumns.js +4 -4
- package/dist/cjs/table/useVirtualTable.js +129 -22
- package/dist/cjs/tree/TreeNode.js +66 -46
- package/dist/cjs/tree/TreePanel.js +60 -24
- package/dist/cjs/tree/useTree.js +212 -47
- package/dist/cjs/tree/utils.js +63 -34
- package/dist/cjs/utils/getColor.js +58 -0
- package/dist/cjs/virtual/Gallery.js +119 -61
- package/dist/cjs/virtual/List.js +116 -55
- package/dist/cjs/virtual/Scroller.js +16 -8
- package/dist/esm/checklist/CheckList.js +61 -35
- package/dist/esm/histogram/Histogram.js +43 -28
- package/dist/esm/hooks/useFilteredList.js +101 -18
- package/dist/esm/hooks/usePagination.js +29 -8
- package/dist/esm/hooks/useSelectableList.js +30 -5
- package/dist/esm/i18n/{ar.json.js → ar/data.json.js} +8 -2
- package/dist/esm/i18n/{en.json.js → en/data.json.js} +8 -2
- package/dist/esm/i18n/index.js +2 -2
- package/dist/esm/index.js +31 -0
- package/dist/esm/json/JsonViewer.js +82 -45
- package/dist/esm/pagination/Pagination.js +25 -16
- package/dist/esm/table/AddColumn.js +6 -6
- package/dist/esm/table/BodyCell.js +21 -14
- package/dist/esm/table/BodyRow.js +41 -28
- package/dist/esm/table/CheckboxCell.js +11 -10
- package/dist/esm/table/ColumnFilters.js +100 -0
- package/dist/esm/table/EmptyCell.js +4 -4
- package/dist/esm/table/HeaderCell.js +80 -54
- package/dist/esm/table/HtmlTable.js +84 -0
- package/dist/esm/table/{Context.js → Provider.js} +11 -12
- package/dist/esm/table/Table.js +100 -34
- package/dist/esm/{tree/types.js → table/context.js} +8 -4
- package/dist/esm/table/types.js +29 -0
- package/dist/esm/table/useResizer.js +10 -10
- package/dist/esm/table/useTableColumns.js +4 -4
- package/dist/esm/table/useVirtualTable.js +131 -24
- package/dist/esm/tree/TreeNode.js +69 -49
- package/dist/esm/tree/TreePanel.js +62 -26
- package/dist/esm/tree/useTree.js +215 -50
- package/dist/esm/tree/utils.js +64 -35
- package/dist/esm/utils/getColor.js +58 -0
- package/dist/esm/virtual/Gallery.js +121 -63
- package/dist/esm/virtual/List.js +118 -57
- package/dist/esm/virtual/Scroller.js +17 -9
- package/dist/types/checklist/CheckList.d.ts +25 -6
- package/dist/types/histogram/Histogram.d.ts +28 -6
- package/dist/types/hooks/useFilteredList.d.ts +62 -5
- package/dist/types/hooks/usePagination.d.ts +18 -2
- package/dist/types/hooks/useSelectableList.d.ts +24 -1
- package/dist/types/index.d.ts +27 -0
- package/dist/types/json/JsonViewer.d.ts +23 -3
- package/dist/types/pagination/Pagination.d.ts +8 -2
- package/dist/types/table/AddColumn.d.ts +2 -2
- package/dist/types/table/BodyCell.d.ts +2 -2
- package/dist/types/table/BodyRow.d.ts +9 -9
- package/dist/types/table/CheckboxCell.d.ts +3 -3
- package/dist/types/table/ColumnFilters.d.ts +12 -0
- package/dist/types/table/EmptyCell.d.ts +1 -1
- package/dist/types/table/HeaderCell.d.ts +1 -1
- package/dist/types/table/HtmlTable.d.ts +21 -0
- package/dist/types/table/Provider.d.ts +2 -0
- package/dist/types/table/Table.d.ts +10 -1
- package/dist/types/table/context.d.ts +7 -0
- package/dist/types/table/types.d.ts +52 -10
- package/dist/types/table/useResizer.d.ts +1 -1
- package/dist/types/table/useTableColumns.d.ts +1 -1
- package/dist/types/table/useVirtualTable.d.ts +31 -12
- package/dist/types/tree/TreeNode.d.ts +1 -1
- package/dist/types/tree/TreePanel.d.ts +36 -1
- package/dist/types/tree/types.d.ts +75 -28
- package/dist/types/tree/useTree.d.ts +8 -12
- package/dist/types/tree/utils.d.ts +15 -8
- package/dist/types/utils/getColor.d.ts +43 -0
- package/dist/types/virtual/Gallery.d.ts +37 -6
- package/dist/types/virtual/List.d.ts +38 -6
- package/dist/types/virtual/Scroller.d.ts +1 -1
- package/package.json +17 -19
- package/src/checklist/CheckList.tsx +68 -71
- package/src/histogram/Histogram.tsx +47 -48
- package/src/hooks/useFilteredList.tsx +111 -28
- package/src/hooks/usePagination.tsx +39 -39
- package/src/hooks/useSelectableList.ts +45 -33
- package/src/i18n/{ar.json → ar/data.json} +5 -0
- package/src/i18n/{en.json → en/data.json} +5 -0
- package/src/i18n/index.ts +2 -2
- package/src/index.ts +23 -17
- package/src/json/JsonViewer.tsx +89 -120
- package/src/pagination/Pagination.tsx +104 -105
- package/src/table/AddColumn.tsx +6 -15
- package/src/table/BodyCell.tsx +27 -36
- package/src/table/BodyRow.tsx +36 -59
- package/src/table/CheckboxCell.tsx +12 -15
- package/src/table/ColumnFilters.tsx +93 -0
- package/src/table/EmptyCell.tsx +5 -18
- package/src/table/HeaderCell.tsx +72 -53
- package/src/table/HtmlTable.tsx +106 -0
- package/src/table/{Context.tsx → Provider.tsx} +11 -28
- package/src/table/Table.tsx +98 -83
- package/src/table/context.ts +33 -0
- package/src/table/types.ts +53 -27
- package/src/table/useResizer.ts +5 -9
- package/src/table/useTableColumns.ts +4 -6
- package/src/table/useVirtualTable.ts +136 -37
- package/src/tree/TreeNode.tsx +91 -86
- package/src/tree/TreePanel.tsx +87 -51
- package/src/tree/types.ts +85 -46
- package/src/tree/useTree.ts +210 -65
- package/src/tree/utils.ts +81 -62
- package/src/utils/getColor.ts +55 -0
- package/src/virtual/Gallery.tsx +129 -120
- package/src/virtual/List.tsx +124 -91
- package/src/virtual/Scroller.tsx +25 -9
- package/dist/cjs/histogram/Histogram.module.css +0 -1
- package/dist/esm/histogram/Histogram.module.css +0 -1
- package/dist/types/table/Context.d.ts +0 -8
|
@@ -21,13 +21,7 @@
|
|
|
21
21
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import {
|
|
25
|
-
useCallback,
|
|
26
|
-
useEffect,
|
|
27
|
-
useMemo,
|
|
28
|
-
useState,
|
|
29
|
-
useTransition,
|
|
30
|
-
} from "react";
|
|
24
|
+
import { useCallback, useEffect, useMemo, useState, useTransition } from "react";
|
|
31
25
|
|
|
32
26
|
export interface PaginationOptions {
|
|
33
27
|
/**
|
|
@@ -52,25 +46,33 @@ const TOTAL_PILLS = 12;
|
|
|
52
46
|
const TOTAL_SIBLINGS = 2;
|
|
53
47
|
const TOTAL_MID_SIBLINGS = 3;
|
|
54
48
|
|
|
55
|
-
|
|
49
|
+
function makeRange(start: number, end: number) {
|
|
56
50
|
const length = end - start;
|
|
57
51
|
return Array.from({ length }, (_: AnyObject, idx: number) => idx + start);
|
|
58
|
-
}
|
|
52
|
+
}
|
|
59
53
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Custom hook to manage pagination state and logic.
|
|
56
|
+
* It calculates the total number of pages, generates page ranges,
|
|
57
|
+
* and provides a way to change the current page.
|
|
58
|
+
* It also handles the transition of page changes to optimize performance.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```jsx
|
|
62
|
+
* const { page, totalPages, headLabel, ranges, onPageChange } = usePagination({
|
|
63
|
+
* totalRecords: 100,
|
|
64
|
+
* currentPage: 0,
|
|
65
|
+
* perPage: 10,
|
|
66
|
+
* onChange: (newPage) => console.log("Page changed to:", newPage),
|
|
67
|
+
* });
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export function usePagination({ totalRecords, currentPage = 0, perPage = 25, onChange }: PaginationOptions) {
|
|
66
71
|
const [page, setPage] = useState(0);
|
|
67
72
|
const [, startTransition] = useTransition();
|
|
68
73
|
|
|
69
74
|
/** ***************** calculate total pages *******************/
|
|
70
|
-
const totalPages = useMemo(
|
|
71
|
-
() => Math.max(1, Math.ceil(totalRecords / perPage)),
|
|
72
|
-
[totalRecords, perPage],
|
|
73
|
-
);
|
|
75
|
+
const totalPages = useMemo(() => Math.max(1, Math.ceil(totalRecords / perPage)), [totalRecords, perPage]);
|
|
74
76
|
|
|
75
77
|
/** ***************** calculate ranges *******************/
|
|
76
78
|
const ranges = useMemo(() => {
|
|
@@ -79,24 +81,15 @@ export const usePagination = ({
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
const leftSiblingIndex = Math.max(page - TOTAL_MID_SIBLINGS, 0);
|
|
82
|
-
const rightSiblingIndex = Math.min(
|
|
83
|
-
page + TOTAL_MID_SIBLINGS,
|
|
84
|
-
totalPages - 1,
|
|
85
|
-
);
|
|
84
|
+
const rightSiblingIndex = Math.min(page + TOTAL_MID_SIBLINGS, totalPages - 1);
|
|
86
85
|
const shouldShowLeftDots = leftSiblingIndex >= TOTAL_SIBLINGS;
|
|
87
86
|
const shouldShowRightDots = rightSiblingIndex < totalPages - TOTAL_SIBLINGS;
|
|
88
87
|
|
|
89
88
|
if (!shouldShowLeftDots && shouldShowRightDots) {
|
|
90
|
-
return [
|
|
91
|
-
makeRange(0, TOTAL_SIBLINGS * 3),
|
|
92
|
-
makeRange(totalPages - TOTAL_SIBLINGS, totalPages),
|
|
93
|
-
];
|
|
89
|
+
return [makeRange(0, TOTAL_SIBLINGS * 3), makeRange(totalPages - TOTAL_SIBLINGS, totalPages)];
|
|
94
90
|
}
|
|
95
91
|
if (shouldShowLeftDots && !shouldShowRightDots) {
|
|
96
|
-
return [
|
|
97
|
-
makeRange(0, TOTAL_SIBLINGS),
|
|
98
|
-
makeRange(totalPages - TOTAL_SIBLINGS * 3, totalPages),
|
|
99
|
-
];
|
|
92
|
+
return [makeRange(0, TOTAL_SIBLINGS), makeRange(totalPages - TOTAL_SIBLINGS * 3, totalPages)];
|
|
100
93
|
}
|
|
101
94
|
if (shouldShowLeftDots && shouldShowRightDots) {
|
|
102
95
|
return [
|
|
@@ -111,9 +104,13 @@ export const usePagination = ({
|
|
|
111
104
|
|
|
112
105
|
/** ***************** set page *******************/
|
|
113
106
|
useEffect(() => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
107
|
+
if (totalRecords > -1) {
|
|
108
|
+
const actual = Math.min(currentPage, totalPages - 1);
|
|
109
|
+
// eslint-disable-next-line @eslint-react/set-state-in-effect
|
|
110
|
+
setPage(actual);
|
|
111
|
+
if (actual !== currentPage) onChange?.(actual);
|
|
112
|
+
}
|
|
113
|
+
}, [currentPage, totalRecords, totalPages, onChange]);
|
|
117
114
|
|
|
118
115
|
const headLabel = useMemo(
|
|
119
116
|
() => (
|
|
@@ -126,10 +123,13 @@ export const usePagination = ({
|
|
|
126
123
|
[page, perPage, totalRecords],
|
|
127
124
|
);
|
|
128
125
|
|
|
129
|
-
const onPageChange = useCallback(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
126
|
+
const onPageChange = useCallback(
|
|
127
|
+
(page: number) => {
|
|
128
|
+
setPage(page);
|
|
129
|
+
startTransition(() => onChange?.(page));
|
|
130
|
+
},
|
|
131
|
+
[onChange],
|
|
132
|
+
);
|
|
133
133
|
|
|
134
134
|
return { page, totalPages, headLabel, ranges, onPageChange };
|
|
135
|
-
}
|
|
135
|
+
}
|
|
@@ -27,7 +27,7 @@ export interface SelectableProps {
|
|
|
27
27
|
/**
|
|
28
28
|
* list items
|
|
29
29
|
*/
|
|
30
|
-
items: Array<{ label: string } & KeyValue>;
|
|
30
|
+
items: Array<{ label: string; id: string } & KeyValue>;
|
|
31
31
|
/**
|
|
32
32
|
* selected item ids
|
|
33
33
|
*/
|
|
@@ -46,49 +46,61 @@ export interface SelectableProps {
|
|
|
46
46
|
onClick?: (id: string, isNegative: boolean) => void;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Custom hook to manage selection of items in a list.
|
|
51
|
+
* It allows toggling selection state of items, handling both selected and non-selected states.
|
|
52
|
+
* The hook maintains a selection state and provides a function to toggle the selection of items.
|
|
53
|
+
* It also supports callbacks for when the selection changes or when an item is clicked.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```jsx
|
|
57
|
+
* const { selection, toggleSelection } = useSelectableList({
|
|
58
|
+
* items: [{ id: '1', label: 'Item 1' }, { id: '2', label: 'Item 2' }],
|
|
59
|
+
* selected: ['1'],
|
|
60
|
+
* nonselected: ['2'],
|
|
61
|
+
* onChange: (selectedIds, nonSelectedIds) => {
|
|
62
|
+
* console.log('Selected:', selectedIds);
|
|
63
|
+
* console.log('Non-selected:', nonSelectedIds);
|
|
64
|
+
* },
|
|
65
|
+
* onClick: (id, isNegative) => {
|
|
66
|
+
* console.log(`Item ${id} clicked, isNegative: ${isNegative}`);
|
|
67
|
+
* },
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function useSelectableList({ items, selected, nonselected, onChange, onClick }: SelectableProps) {
|
|
56
72
|
const selectionRef = useRef<KeyValue<number>>({});
|
|
57
|
-
const [selection, setSelection] = useState<KeyValue<number>>({});
|
|
73
|
+
const [selection, setSelection] = useState<KeyValue<number>>(() => ({}));
|
|
58
74
|
const [, startTransition] = useTransition();
|
|
59
75
|
useEffect(() => {
|
|
60
76
|
selectionRef.current = items.reduce(
|
|
61
77
|
(ret, { id }) => ({
|
|
62
78
|
...ret,
|
|
63
|
-
[id]: selected?.includes?.(id)
|
|
64
|
-
? 1
|
|
65
|
-
: nonselected?.includes?.(id)
|
|
66
|
-
? -1
|
|
67
|
-
: 0,
|
|
79
|
+
[id]: selected?.includes?.(id) ? 1 : nonselected?.includes?.(id) ? -1 : 0,
|
|
68
80
|
}),
|
|
69
81
|
{},
|
|
70
82
|
);
|
|
83
|
+
// eslint-disable-next-line @eslint-react/set-state-in-effect
|
|
71
84
|
setSelection({ ...selectionRef.current });
|
|
72
85
|
}, [items, selected, nonselected]);
|
|
73
86
|
|
|
74
|
-
const toggleSelection = useCallback(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
select
|
|
81
|
-
? change[0].push(id)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}, []);
|
|
87
|
+
const toggleSelection = useCallback(
|
|
88
|
+
(id: string, isNegative = false) => {
|
|
89
|
+
const check = isNegative ? -1 : 1;
|
|
90
|
+
if (selectionRef.current[id] !== check) selectionRef.current[id] = check;
|
|
91
|
+
else if (selectionRef.current[id] === check) selectionRef.current[id] = 0;
|
|
92
|
+
const change: string[][] = [[], []];
|
|
93
|
+
Object.entries(selectionRef.current).forEach(([id, select]) =>
|
|
94
|
+
select === 1 ? change[0].push(id) : select === -1 ? change[1].push(id) : undefined,
|
|
95
|
+
);
|
|
96
|
+
setSelection({ ...selectionRef.current });
|
|
97
|
+
startTransition(() => {
|
|
98
|
+
onClick?.(id, isNegative);
|
|
99
|
+
onChange?.(change[0], change[1]);
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
[onChange, onClick],
|
|
103
|
+
);
|
|
92
104
|
|
|
93
105
|
return { selection, toggleSelection };
|
|
94
|
-
}
|
|
106
|
+
}
|
package/src/i18n/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,32 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* React Fabric
|
|
3
|
-
* @version
|
|
3
|
+
* @version 1.0.0
|
|
4
|
+
* @license MIT
|
|
5
|
+
* @copyright 2024 Adarsh Pastakia
|
|
4
6
|
*
|
|
7
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
* in the Software without restriction, including without limitation the rights
|
|
10
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
* furnished to do so, subject to the following conditions:
|
|
5
13
|
*
|
|
6
|
-
* The
|
|
7
|
-
*
|
|
14
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
* copies or substantial portions of the Software.
|
|
8
16
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* portions of the Software.
|
|
17
|
-
*
|
|
18
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
19
|
-
* TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
20
|
-
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
-
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
17
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
* SOFTWARE.
|
|
22
24
|
*/
|
|
23
25
|
|
|
26
|
+
export const DATA_PACKAGE = "@react-fabric/data";
|
|
27
|
+
export const DATA_VERSION = "1.0.0";
|
|
28
|
+
|
|
24
29
|
import "./i18n";
|
|
25
30
|
|
|
26
31
|
export { CheckList } from "./checklist/CheckList";
|
|
27
32
|
export { Histogram } from "./histogram/Histogram";
|
|
28
33
|
export { JsonViewer } from "./json/JsonViewer";
|
|
29
34
|
export { Pagination } from "./pagination/Pagination";
|
|
35
|
+
export { HtmlTable } from "./table/HtmlTable";
|
|
30
36
|
export { Table } from "./table/Table";
|
|
31
37
|
export { type TableColumn, type TableRef } from "./table/types";
|
|
32
38
|
export { TreePanel } from "./tree/TreePanel";
|
package/src/json/JsonViewer.tsx
CHANGED
|
@@ -21,27 +21,15 @@
|
|
|
21
21
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Meter,
|
|
32
|
-
Text,
|
|
33
|
-
} from "@react-fabric/core";
|
|
24
|
+
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
25
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
26
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
27
|
+
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
28
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
29
|
+
|
|
30
|
+
import { Button, Copy, EmptyContent, Icon, Meter, Text } from "@react-fabric/core";
|
|
34
31
|
import { type CssProp } from "@react-fabric/core/dist/types/types";
|
|
35
|
-
import {
|
|
36
|
-
Format,
|
|
37
|
-
isArray,
|
|
38
|
-
isBoolean,
|
|
39
|
-
isEmpty,
|
|
40
|
-
isNil,
|
|
41
|
-
isObject,
|
|
42
|
-
isTrue,
|
|
43
|
-
} from "@react-fabric/utilities";
|
|
44
|
-
import classNames from "classnames";
|
|
32
|
+
import { cn, Format, isArray, isBoolean, isEmpty, isNil, isObject, isTrue } from "@react-fabric/utilities";
|
|
45
33
|
import { Fragment, useMemo, useState } from "react";
|
|
46
34
|
import { useTranslation } from "react-i18next";
|
|
47
35
|
|
|
@@ -99,21 +87,18 @@ export interface JsonViewProps<T> extends CssProp {
|
|
|
99
87
|
/**
|
|
100
88
|
* render function for custom value display
|
|
101
89
|
*/
|
|
102
|
-
formatter?: (
|
|
103
|
-
path?: string,
|
|
104
|
-
value?: AnyObject,
|
|
105
|
-
) => string | JSX.Element | undefined;
|
|
90
|
+
formatter?: (path?: string, value?: AnyObject) => string | React.ReactElement | undefined;
|
|
106
91
|
}
|
|
107
92
|
|
|
108
|
-
|
|
109
|
-
const ret:
|
|
93
|
+
function reduceObject(json: KeyValue, keys: string[] = [], prefix = "") {
|
|
94
|
+
const ret: KeyValue[] = [];
|
|
110
95
|
Object.entries(json).forEach(([key, value]) => {
|
|
111
96
|
if (isArray(value) && isObject(value[0])) {
|
|
112
97
|
ret.push(...reduceObject(value, [...keys, key], key));
|
|
113
98
|
} else {
|
|
114
99
|
const obj: KeyValue = {
|
|
115
100
|
field: prefix ? `${prefix}.${key}` : key,
|
|
116
|
-
value,
|
|
101
|
+
value: value as never,
|
|
117
102
|
path: [...keys, key].join("."),
|
|
118
103
|
};
|
|
119
104
|
|
|
@@ -126,9 +111,9 @@ const reduceObject = (json: AnyObject, keys: string[] = [], prefix = "") => {
|
|
|
126
111
|
});
|
|
127
112
|
|
|
128
113
|
return ret.flat(99);
|
|
129
|
-
}
|
|
114
|
+
}
|
|
130
115
|
|
|
131
|
-
|
|
116
|
+
function JsonEntry({
|
|
132
117
|
value,
|
|
133
118
|
path,
|
|
134
119
|
field,
|
|
@@ -143,14 +128,14 @@ const JsonEntry = ({
|
|
|
143
128
|
inline,
|
|
144
129
|
labelWidth = "30%",
|
|
145
130
|
level = 0,
|
|
146
|
-
}: KeyValue)
|
|
131
|
+
}: KeyValue) {
|
|
147
132
|
const { t } = useTranslation("data");
|
|
148
133
|
|
|
149
134
|
const [expanded, setExpanded] = useState(true);
|
|
150
135
|
|
|
151
136
|
const canExpand = useMemo(() => !!children, [children]);
|
|
152
137
|
|
|
153
|
-
const actualValue = useMemo(() => value?._label_ ?? value ?? "", [value]);
|
|
138
|
+
const actualValue = useMemo<string>(() => value?._label_ ?? value ?? "", [value]);
|
|
154
139
|
|
|
155
140
|
const display = useMemo(() => {
|
|
156
141
|
if (children) return null;
|
|
@@ -169,85 +154,67 @@ const JsonEntry = ({
|
|
|
169
154
|
if (propertyScheme?.date?.includes(path)) {
|
|
170
155
|
// return <DateDisplay date={value} />;
|
|
171
156
|
}
|
|
172
|
-
if (propertyScheme?.time?.includes(path))
|
|
173
|
-
|
|
174
|
-
if (propertyScheme?.number?.phone(path)) ret = Format.phone(actualValue);
|
|
157
|
+
if (propertyScheme?.time?.includes(path)) ret = Format.date(actualValue, "pp");
|
|
158
|
+
if (propertyScheme?.number?.phone(path)) ret = Format.phone(actualValue) ?? "";
|
|
175
159
|
if (propertyScheme?.bytes?.includes(path)) ret = Format.bytes(actualValue);
|
|
176
|
-
if (propertyScheme?.percent?.includes(path))
|
|
177
|
-
|
|
178
|
-
if (propertyScheme?.
|
|
179
|
-
ret = Format.number(actualValue);
|
|
180
|
-
if (propertyScheme?.boolean?.includes(path) || isBoolean(actualValue))
|
|
181
|
-
ret = actualValue ? t("json.true") : t("json.false");
|
|
160
|
+
if (propertyScheme?.percent?.includes(path)) ret = Format.percent(actualValue);
|
|
161
|
+
if (propertyScheme?.number?.includes(path)) ret = Format.number(actualValue);
|
|
162
|
+
if (propertyScheme?.boolean?.includes(path) || isBoolean(actualValue)) ret = actualValue ? t("json.true") : t("json.false");
|
|
182
163
|
if (ret?.length > 128) {
|
|
183
|
-
ret = <Text clamp={3}>{ret}</Text
|
|
164
|
+
ret = (<Text clamp={3}>{ret}</Text>) as never;
|
|
165
|
+
} else {
|
|
166
|
+
ret = (<div className="break-words">{ret}</div>) as never;
|
|
184
167
|
}
|
|
185
168
|
|
|
186
169
|
return (
|
|
187
170
|
<Fragment>
|
|
188
171
|
{ret}
|
|
189
|
-
{isObject(value) && value._score_ > 0 &&
|
|
190
|
-
<Meter className="text-xs" value={value._score_} />
|
|
191
|
-
)}
|
|
172
|
+
{isObject(value) && value._score_ > 0 && <Meter className="text-xs" value={value._score_} />}
|
|
192
173
|
</Fragment>
|
|
193
174
|
);
|
|
175
|
+
// eslint-disable-next-line @eslint-react/exhaustive-deps
|
|
194
176
|
}, [value, path, formatter, propertyScheme, actualValue]);
|
|
195
177
|
|
|
196
|
-
const pathWithoutOrdinal = useMemo(
|
|
197
|
-
() => path.replace(/\.\d*\./, "."),
|
|
198
|
-
[path],
|
|
199
|
-
);
|
|
178
|
+
const pathWithoutOrdinal = useMemo(() => path.replace(/\.\d*\./, "."), [path]);
|
|
200
179
|
|
|
201
180
|
const canCopy = useMemo(
|
|
202
|
-
() =>
|
|
203
|
-
!isEmpty(value) &&
|
|
204
|
-
(isTrue(copy) ||
|
|
205
|
-
copy?.includes?.(path) ||
|
|
206
|
-
copy?.includes?.(pathWithoutOrdinal)),
|
|
181
|
+
() => !isEmpty(value) && (isTrue(copy) || copy?.includes?.(path) || copy?.includes?.(pathWithoutOrdinal)),
|
|
207
182
|
[copy, value, path, pathWithoutOrdinal],
|
|
208
183
|
);
|
|
209
184
|
const canFilter = useMemo(
|
|
210
|
-
() =>
|
|
211
|
-
isTrue(filters) ||
|
|
212
|
-
filters?.includes?.(path) ||
|
|
213
|
-
filters?.includes?.(pathWithoutOrdinal),
|
|
185
|
+
() => isTrue(filters) || filters?.includes?.(path) || filters?.includes?.(pathWithoutOrdinal),
|
|
214
186
|
[filters, path, pathWithoutOrdinal],
|
|
215
187
|
);
|
|
216
188
|
|
|
217
189
|
const labelDisplay = useMemo(
|
|
218
190
|
() => (
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
onClick={() => onFilter?.(pathWithoutOrdinal, actualValue)}
|
|
243
|
-
/>,
|
|
244
|
-
]
|
|
245
|
-
: []
|
|
246
|
-
}
|
|
247
|
-
>
|
|
191
|
+
<div className={cn("text-dimmed text-sm px-2 flex-1 group", inline ? "truncate" : "break-words")}>
|
|
192
|
+
{!canExpand && canFilter && (
|
|
193
|
+
<div className="hidden pe-1 group-hover:inline-block align-middle">
|
|
194
|
+
<Button
|
|
195
|
+
key="positive"
|
|
196
|
+
color="primary"
|
|
197
|
+
size="xs"
|
|
198
|
+
variant="link"
|
|
199
|
+
icon="icon-[mdi--filter-plus]"
|
|
200
|
+
aria-label="Filter"
|
|
201
|
+
onClick={() => onFilter?.(pathWithoutOrdinal, actualValue)}
|
|
202
|
+
/>
|
|
203
|
+
<Button
|
|
204
|
+
key="negative"
|
|
205
|
+
color="danger"
|
|
206
|
+
size="xs"
|
|
207
|
+
variant="link"
|
|
208
|
+
icon="icon-[mdi--filter-minus]"
|
|
209
|
+
aria-label="Filter Negative"
|
|
210
|
+
onClick={() => onFilter?.(pathWithoutOrdinal, actualValue, true)}
|
|
211
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
248
214
|
{labeler?.(path) ?? field}
|
|
249
|
-
</
|
|
215
|
+
</div>
|
|
250
216
|
),
|
|
217
|
+
// eslint-disable-next-line @eslint-react/exhaustive-deps
|
|
251
218
|
[field, labeler, pathWithoutOrdinal, actualValue, canExpand, canFilter],
|
|
252
219
|
);
|
|
253
220
|
|
|
@@ -255,63 +222,44 @@ const JsonEntry = ({
|
|
|
255
222
|
<Fragment>
|
|
256
223
|
{showPropertyTree && (
|
|
257
224
|
<div
|
|
258
|
-
className={
|
|
259
|
-
"py-2",
|
|
260
|
-
inline && "flex flex-nowrap items-center",
|
|
261
|
-
)}
|
|
225
|
+
className={cn("py-2", inline && "flex flex-nowrap items-center")}
|
|
262
226
|
style={{
|
|
263
227
|
paddingInlineStart: level * 8,
|
|
264
228
|
}}
|
|
265
229
|
>
|
|
266
230
|
<div
|
|
267
231
|
role="none"
|
|
268
|
-
className={
|
|
269
|
-
"flex overflow-hidden items-center",
|
|
270
|
-
inline && "flex-content",
|
|
271
|
-
)}
|
|
232
|
+
className={cn("flex overflow-hidden items-center", inline && "flex-content")}
|
|
272
233
|
style={{
|
|
273
234
|
flexBasis: labelWidth,
|
|
274
235
|
}}
|
|
275
236
|
onClick={() => canExpand && setExpanded(!expanded)}
|
|
276
237
|
>
|
|
277
|
-
{canExpand &&
|
|
278
|
-
<Icon
|
|
279
|
-
rtlFlip
|
|
280
|
-
icon={expanded ? CoreIcons.chevronDown : CoreIcons.chevronRight}
|
|
281
|
-
/>
|
|
282
|
-
)}
|
|
238
|
+
{canExpand && <Icon rtlFlip icon={expanded ? "icon-[mdi--chevron-down]" : "icon-[mdi--chevron-right]"} />}
|
|
283
239
|
{labelDisplay}
|
|
284
240
|
</div>
|
|
285
241
|
{display && (
|
|
286
|
-
<div className={
|
|
242
|
+
<div className={cn("px-2 flex flex-nowrap overflow-hidden", inline && "flex-1")}>
|
|
287
243
|
{canCopy && <Copy size="xs" text={actualValue} />}
|
|
288
|
-
{display}
|
|
244
|
+
<div className="flex-1">{display}</div>
|
|
289
245
|
</div>
|
|
290
246
|
)}
|
|
291
247
|
</div>
|
|
292
248
|
)}
|
|
293
249
|
{!showPropertyTree && !children && (
|
|
294
|
-
<div
|
|
295
|
-
className={classNames(
|
|
296
|
-
"py-2",
|
|
297
|
-
inline && "flex flex-nowrap items-center",
|
|
298
|
-
)}
|
|
299
|
-
>
|
|
250
|
+
<div className={cn("py-2", inline && "flex flex-nowrap items-center")}>
|
|
300
251
|
<div
|
|
301
252
|
role="none"
|
|
302
|
-
className={
|
|
303
|
-
"flex overflow-hidden items-center",
|
|
304
|
-
inline && "flex-content",
|
|
305
|
-
)}
|
|
253
|
+
className={cn("flex overflow-hidden items-center", inline && "flex-content")}
|
|
306
254
|
style={{
|
|
307
255
|
flexBasis: labelWidth,
|
|
308
256
|
}}
|
|
309
257
|
>
|
|
310
258
|
{labelDisplay}
|
|
311
259
|
</div>
|
|
312
|
-
<div className={
|
|
260
|
+
<div className={cn("px-2 flex flex-nowrap overflow-hidden", inline && "flex-1")}>
|
|
313
261
|
{canCopy && <Copy size="xs" text={value} />}
|
|
314
|
-
{display}
|
|
262
|
+
<div className="flex-1">{display}</div>
|
|
315
263
|
</div>
|
|
316
264
|
</div>
|
|
317
265
|
)}
|
|
@@ -334,9 +282,30 @@ const JsonEntry = ({
|
|
|
334
282
|
))}
|
|
335
283
|
</Fragment>
|
|
336
284
|
);
|
|
337
|
-
}
|
|
285
|
+
}
|
|
338
286
|
|
|
339
|
-
|
|
287
|
+
/**
|
|
288
|
+
* JsonViewer component to display JSON data in a structured format.
|
|
289
|
+
* It supports inline display, property filtering, and custom formatting.
|
|
290
|
+
* The component can handle nested objects and arrays, displaying them in a tree-like structure.
|
|
291
|
+
* It also provides options for copying values and filtering properties.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```jsx
|
|
295
|
+
* <JsonViewer
|
|
296
|
+
* json={{ name: "John", age: 30, address: { city: "New York", zip: "10001" } }}
|
|
297
|
+
* copy={true}
|
|
298
|
+
* filters={true}
|
|
299
|
+
* propertyScheme={{ date: ["createdAt"], phone: ["contact"] }}
|
|
300
|
+
* inline={true}
|
|
301
|
+
* labelWidth="25%"
|
|
302
|
+
* formatter={(path, value) => (path === "age" ? `${value} years` : value)}
|
|
303
|
+
* labeler={(path) => path.replace(/_/g, " ").toUpperCase()}
|
|
304
|
+
* onFilter={(path, value, negate) => console.log(`Filter ${negate ? "negate" : ""} on ${path}:`, value)}
|
|
305
|
+
* />
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
export function JsonViewer<T extends KeyValue = KeyValue>({
|
|
340
309
|
className,
|
|
341
310
|
json,
|
|
342
311
|
copy,
|
|
@@ -350,13 +319,13 @@ export const JsonViewer = <T extends KeyValue = KeyValue>({
|
|
|
350
319
|
emptyMessage,
|
|
351
320
|
showPropertyTree = true,
|
|
352
321
|
...rest
|
|
353
|
-
}: JsonViewProps<T>)
|
|
322
|
+
}: JsonViewProps<T>) {
|
|
354
323
|
const { t } = useTranslation("data");
|
|
355
324
|
|
|
356
325
|
const list = useMemo(() => reduceObject(json), [json]);
|
|
357
326
|
|
|
358
327
|
return (
|
|
359
|
-
<div {...rest} className={
|
|
328
|
+
<div {...rest} className={cn(className, "bg-alternate")}>
|
|
360
329
|
{!isEmpty(json) ? (
|
|
361
330
|
list.map((item) => (
|
|
362
331
|
<JsonEntry
|
|
@@ -378,4 +347,4 @@ export const JsonViewer = <T extends KeyValue = KeyValue>({
|
|
|
378
347
|
)}
|
|
379
348
|
</div>
|
|
380
349
|
);
|
|
381
|
-
}
|
|
350
|
+
}
|