@react-fabric/data 1.0.0-beta.8 → 1.0.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.
Files changed (141) hide show
  1. package/{src/histogram/Histogram.module.css → css/_histogram.css} +16 -13
  2. package/css/styles.css +24 -0
  3. package/dist/cjs/checklist/CheckList.js +59 -33
  4. package/dist/cjs/histogram/Histogram.js +41 -26
  5. package/dist/cjs/hooks/useFilteredList.js +100 -17
  6. package/dist/cjs/hooks/usePagination.js +29 -8
  7. package/dist/cjs/hooks/useSelectableList.js +30 -5
  8. package/dist/cjs/i18n/{ar.json.js → ar/data.json.js} +8 -1
  9. package/dist/cjs/i18n/{en.json.js → en/data.json.js} +8 -1
  10. package/dist/cjs/i18n/index.js +4 -4
  11. package/dist/cjs/index.js +30 -0
  12. package/dist/cjs/json/JsonViewer.js +80 -43
  13. package/dist/cjs/pagination/Pagination.js +24 -15
  14. package/dist/cjs/table/AddColumn.js +5 -5
  15. package/dist/cjs/table/BodyCell.js +20 -13
  16. package/dist/cjs/table/BodyRow.js +41 -28
  17. package/dist/cjs/table/CheckboxCell.js +10 -9
  18. package/dist/cjs/table/ColumnFilters.js +100 -0
  19. package/dist/cjs/table/EmptyCell.js +4 -4
  20. package/dist/cjs/table/HeaderCell.js +79 -53
  21. package/dist/cjs/table/HtmlTable.js +84 -0
  22. package/dist/cjs/table/{Context.js → Provider.js} +8 -10
  23. package/dist/cjs/table/Table.js +99 -33
  24. package/dist/cjs/{tree/types.js → table/context.js} +9 -5
  25. package/dist/cjs/table/types.js +29 -0
  26. package/dist/cjs/table/useResizer.js +10 -10
  27. package/dist/cjs/table/useTableColumns.js +4 -4
  28. package/dist/cjs/table/useVirtualTable.js +129 -22
  29. package/dist/cjs/tree/TreeNode.js +66 -46
  30. package/dist/cjs/tree/TreePanel.js +60 -24
  31. package/dist/cjs/tree/useTree.js +212 -47
  32. package/dist/cjs/tree/utils.js +63 -34
  33. package/dist/cjs/utils/getColor.js +58 -0
  34. package/dist/cjs/virtual/Gallery.js +119 -61
  35. package/dist/cjs/virtual/List.js +116 -55
  36. package/dist/cjs/virtual/Scroller.js +16 -8
  37. package/dist/esm/checklist/CheckList.js +61 -35
  38. package/dist/esm/histogram/Histogram.js +43 -28
  39. package/dist/esm/hooks/useFilteredList.js +101 -18
  40. package/dist/esm/hooks/usePagination.js +29 -8
  41. package/dist/esm/hooks/useSelectableList.js +30 -5
  42. package/dist/esm/i18n/{ar.json.js → ar/data.json.js} +8 -2
  43. package/dist/esm/i18n/{en.json.js → en/data.json.js} +8 -2
  44. package/dist/esm/i18n/index.js +2 -2
  45. package/dist/esm/index.js +31 -0
  46. package/dist/esm/json/JsonViewer.js +82 -45
  47. package/dist/esm/pagination/Pagination.js +25 -16
  48. package/dist/esm/table/AddColumn.js +6 -6
  49. package/dist/esm/table/BodyCell.js +21 -14
  50. package/dist/esm/table/BodyRow.js +41 -28
  51. package/dist/esm/table/CheckboxCell.js +11 -10
  52. package/dist/esm/table/ColumnFilters.js +100 -0
  53. package/dist/esm/table/EmptyCell.js +4 -4
  54. package/dist/esm/table/HeaderCell.js +80 -54
  55. package/dist/esm/table/HtmlTable.js +84 -0
  56. package/dist/esm/table/{Context.js → Provider.js} +11 -12
  57. package/dist/esm/table/Table.js +100 -34
  58. package/dist/esm/{tree/types.js → table/context.js} +8 -4
  59. package/dist/esm/table/types.js +29 -0
  60. package/dist/esm/table/useResizer.js +10 -10
  61. package/dist/esm/table/useTableColumns.js +4 -4
  62. package/dist/esm/table/useVirtualTable.js +131 -24
  63. package/dist/esm/tree/TreeNode.js +69 -49
  64. package/dist/esm/tree/TreePanel.js +62 -26
  65. package/dist/esm/tree/useTree.js +215 -50
  66. package/dist/esm/tree/utils.js +64 -35
  67. package/dist/esm/utils/getColor.js +58 -0
  68. package/dist/esm/virtual/Gallery.js +121 -63
  69. package/dist/esm/virtual/List.js +118 -57
  70. package/dist/esm/virtual/Scroller.js +17 -9
  71. package/dist/types/checklist/CheckList.d.ts +25 -6
  72. package/dist/types/histogram/Histogram.d.ts +28 -6
  73. package/dist/types/hooks/useFilteredList.d.ts +62 -5
  74. package/dist/types/hooks/usePagination.d.ts +18 -2
  75. package/dist/types/hooks/useSelectableList.d.ts +24 -1
  76. package/dist/types/index.d.ts +27 -0
  77. package/dist/types/json/JsonViewer.d.ts +23 -3
  78. package/dist/types/pagination/Pagination.d.ts +8 -2
  79. package/dist/types/table/AddColumn.d.ts +2 -2
  80. package/dist/types/table/BodyCell.d.ts +2 -2
  81. package/dist/types/table/BodyRow.d.ts +9 -9
  82. package/dist/types/table/CheckboxCell.d.ts +3 -3
  83. package/dist/types/table/ColumnFilters.d.ts +12 -0
  84. package/dist/types/table/EmptyCell.d.ts +1 -1
  85. package/dist/types/table/HeaderCell.d.ts +1 -1
  86. package/dist/types/table/HtmlTable.d.ts +21 -0
  87. package/dist/types/table/Provider.d.ts +2 -0
  88. package/dist/types/table/Table.d.ts +10 -1
  89. package/dist/types/table/context.d.ts +7 -0
  90. package/dist/types/table/types.d.ts +52 -10
  91. package/dist/types/table/useResizer.d.ts +1 -1
  92. package/dist/types/table/useTableColumns.d.ts +1 -1
  93. package/dist/types/table/useVirtualTable.d.ts +31 -12
  94. package/dist/types/tree/TreeNode.d.ts +1 -1
  95. package/dist/types/tree/TreePanel.d.ts +36 -1
  96. package/dist/types/tree/types.d.ts +75 -28
  97. package/dist/types/tree/useTree.d.ts +8 -12
  98. package/dist/types/tree/utils.d.ts +15 -8
  99. package/dist/types/utils/getColor.d.ts +43 -0
  100. package/dist/types/virtual/Gallery.d.ts +37 -6
  101. package/dist/types/virtual/List.d.ts +38 -6
  102. package/dist/types/virtual/Scroller.d.ts +1 -1
  103. package/package.json +17 -19
  104. package/src/checklist/CheckList.tsx +68 -71
  105. package/src/histogram/Histogram.tsx +47 -48
  106. package/src/hooks/useFilteredList.tsx +111 -28
  107. package/src/hooks/usePagination.tsx +39 -39
  108. package/src/hooks/useSelectableList.ts +45 -33
  109. package/src/i18n/{ar.json → ar/data.json} +5 -0
  110. package/src/i18n/{en.json → en/data.json} +5 -0
  111. package/src/i18n/index.ts +2 -2
  112. package/src/index.ts +23 -17
  113. package/src/json/JsonViewer.tsx +89 -120
  114. package/src/pagination/Pagination.tsx +104 -105
  115. package/src/table/AddColumn.tsx +6 -15
  116. package/src/table/BodyCell.tsx +27 -36
  117. package/src/table/BodyRow.tsx +36 -59
  118. package/src/table/CheckboxCell.tsx +12 -15
  119. package/src/table/ColumnFilters.tsx +93 -0
  120. package/src/table/EmptyCell.tsx +5 -18
  121. package/src/table/HeaderCell.tsx +72 -53
  122. package/src/table/HtmlTable.tsx +106 -0
  123. package/src/table/{Context.tsx → Provider.tsx} +11 -28
  124. package/src/table/Table.tsx +98 -83
  125. package/src/table/context.ts +33 -0
  126. package/src/table/types.ts +53 -27
  127. package/src/table/useResizer.ts +5 -9
  128. package/src/table/useTableColumns.ts +4 -6
  129. package/src/table/useVirtualTable.ts +136 -37
  130. package/src/tree/TreeNode.tsx +91 -86
  131. package/src/tree/TreePanel.tsx +87 -51
  132. package/src/tree/types.ts +85 -46
  133. package/src/tree/useTree.ts +210 -65
  134. package/src/tree/utils.ts +81 -62
  135. package/src/utils/getColor.ts +55 -0
  136. package/src/virtual/Gallery.tsx +129 -120
  137. package/src/virtual/List.tsx +124 -91
  138. package/src/virtual/Scroller.tsx +25 -9
  139. package/dist/cjs/histogram/Histogram.module.css +0 -1
  140. package/dist/esm/histogram/Histogram.module.css +0 -1
  141. 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
- const makeRange = (start: number, end: number) => {
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
- export const usePagination = ({
61
- totalRecords,
62
- currentPage = 0,
63
- perPage = 25,
64
- onChange,
65
- }: PaginationOptions) => {
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
- setPage(Math.min(currentPage, totalPages - 1));
115
- onChange?.(Math.min(currentPage, totalPages - 1));
116
- }, [currentPage, totalPages]);
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((page: number) => {
130
- setPage(page);
131
- startTransition(() => onChange?.(page));
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
- export const useSelectableList = ({
50
- items,
51
- selected,
52
- nonselected,
53
- onChange,
54
- onClick,
55
- }: SelectableProps) => {
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((id: string, isNegative = false) => {
75
- const check = isNegative ? -1 : 1;
76
- if (selectionRef.current[id] !== check) selectionRef.current[id] = check;
77
- else if (selectionRef.current[id] === check) selectionRef.current[id] = 0;
78
- const change: string[][] = [[], []];
79
- Object.entries(selectionRef.current).forEach(([id, select]) =>
80
- select === 1
81
- ? change[0].push(id)
82
- : select === -1
83
- ? change[1].push(id)
84
- : undefined,
85
- );
86
- setSelection({ ...selectionRef.current });
87
- startTransition(() => {
88
- onClick?.(id, isNegative);
89
- onChange?.(change[0], change[1]);
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
+ }
@@ -32,5 +32,10 @@
32
32
  "uncheckAll": "الغاءالكل",
33
33
  "addFilter": "أضف عامل تصفية",
34
34
  "addFilterNot": "إضافة لا تصفية"
35
+ },
36
+ "data": {
37
+ "tree": {
38
+ "noItems": "لا توجد عناصر شجرة متوفرة"
39
+ }
35
40
  }
36
41
  }
@@ -32,5 +32,10 @@
32
32
  "uncheckAll": "Uncheck All",
33
33
  "addFilter": "Add Filter",
34
34
  "addFilterNot": "Add not Filter"
35
+ },
36
+ "data": {
37
+ "tree": {
38
+ "noItems": "No tree items available"
39
+ }
35
40
  }
36
41
  }
package/src/i18n/index.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  */
23
23
 
24
24
  import { addTranslationBundle } from "@react-fabric/core";
25
- import ar from "./ar.json";
26
- import en from "./en.json";
25
+ import ar from "./ar/data.json";
26
+ import en from "./en/data.json";
27
27
 
28
28
  addTranslationBundle("data", { en, ar });
package/src/index.ts CHANGED
@@ -1,32 +1,38 @@
1
- /*
1
+ /**
2
2
  * React Fabric
3
- * @version: 1.0.0
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 MIT License (MIT)
7
- * Copyright (c) 2024 Adarsh Pastakia
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
- * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
10
- * and associated documentation files (the "Software"), to deal in the Software without restriction,
11
- * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
12
- * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
13
- * subject to the following conditions:
14
- *
15
- * The above copyright notice and this permission notice shall be included in all copies or substantial
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";
@@ -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
- import {
25
- ActionLabel,
26
- Button,
27
- Copy,
28
- CoreIcons,
29
- EmptyContent,
30
- Icon,
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
- const reduceObject = (json: AnyObject, keys: string[] = [], prefix = "") => {
109
- const ret: AnyObject[] = [];
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
- const JsonEntry = ({
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
- ret = Format.date(actualValue, "pp");
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
- ret = Format.percent(actualValue);
178
- if (propertyScheme?.number?.includes(path))
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
- <ActionLabel
220
- className="text-dimmed text-sm px-2 flex-1 truncate"
221
- actions={
222
- !canExpand && canFilter
223
- ? [
224
- <Button
225
- key="negative"
226
- color="danger"
227
- size="sm"
228
- variant="link"
229
- icon={CoreIcons.funnelMinus}
230
- aria-label="Filter Negative"
231
- onClick={() =>
232
- onFilter?.(pathWithoutOrdinal, actualValue, true)
233
- }
234
- />,
235
- <Button
236
- key="positive"
237
- color="primary"
238
- size="sm"
239
- variant="link"
240
- icon={CoreIcons.funnelPlus}
241
- aria-label="Filter"
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
- </ActionLabel>
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={classNames(
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={classNames(
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={classNames("px-2", inline && "flex-1")}>
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={classNames(
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={classNames("px-2", inline && "flex-1")}>
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
- export const JsonViewer = <T extends KeyValue = KeyValue>({
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={classNames(className, "p-4 bg-alternate")}>
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
+ }