@solidxai/core-ui 0.1.3 → 0.1.4-beta.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 (150) hide show
  1. package/dist/components/auth/SolidInitialLoginOtp.d.ts.map +1 -1
  2. package/dist/components/auth/SolidInitialLoginOtp.js +0 -5
  3. package/dist/components/auth/SolidInitialLoginOtp.js.map +1 -1
  4. package/dist/components/auth/SolidInitialLoginOtp.tsx +0 -5
  5. package/dist/components/auth/SolidLogin.d.ts.map +1 -1
  6. package/dist/components/auth/SolidLogin.js +7 -5
  7. package/dist/components/auth/SolidLogin.js.map +1 -1
  8. package/dist/components/auth/SolidLogin.tsx +10 -8
  9. package/dist/components/common/GeneralSettings.d.ts.map +1 -1
  10. package/dist/components/common/GeneralSettings.js +48 -47
  11. package/dist/components/common/GeneralSettings.js.map +1 -1
  12. package/dist/components/common/GeneralSettings.tsx +41 -10
  13. package/dist/components/core/common/FilterComponent.js.map +1 -1
  14. package/dist/components/core/common/FilterComponent.tsx +1 -1
  15. package/dist/components/core/common/GroupingComponent.d.ts +54 -0
  16. package/dist/components/core/common/GroupingComponent.d.ts.map +1 -0
  17. package/dist/components/core/common/GroupingComponent.js +196 -0
  18. package/dist/components/core/common/GroupingComponent.js.map +1 -0
  19. package/dist/components/core/common/GroupingComponent.tsx +452 -0
  20. package/dist/components/core/common/SolidGlobalSearchElement.d.ts +18 -1
  21. package/dist/components/core/common/SolidGlobalSearchElement.d.ts.map +1 -1
  22. package/dist/components/core/common/SolidGlobalSearchElement.js +152 -52
  23. package/dist/components/core/common/SolidGlobalSearchElement.js.map +1 -1
  24. package/dist/components/core/common/SolidGlobalSearchElement.tsx +212 -35
  25. package/dist/components/core/extension/solid-core/modelSequence/modelSequenceFormViewChangeHandler.d.ts +19 -0
  26. package/dist/components/core/extension/solid-core/modelSequence/modelSequenceFormViewChangeHandler.d.ts.map +1 -0
  27. package/dist/components/core/extension/solid-core/modelSequence/modelSequenceFormViewChangeHandler.js +90 -0
  28. package/dist/components/core/extension/solid-core/modelSequence/modelSequenceFormViewChangeHandler.js.map +1 -0
  29. package/dist/components/core/extension/solid-core/modelSequence/modelSequenceFormViewChangeHandler.tsx +59 -0
  30. package/dist/components/core/extension/solid-core/roleMetadata/RolePermissionsManyToManyFieldWidget.d.ts.map +1 -1
  31. package/dist/components/core/extension/solid-core/roleMetadata/RolePermissionsManyToManyFieldWidget.js +17 -28
  32. package/dist/components/core/extension/solid-core/roleMetadata/RolePermissionsManyToManyFieldWidget.js.map +1 -1
  33. package/dist/components/core/extension/solid-core/roleMetadata/RolePermissionsManyToManyFieldWidget.tsx +71 -56
  34. package/dist/components/core/filter/SolidOneToManyFilterElement.d.ts +2 -0
  35. package/dist/components/core/filter/SolidOneToManyFilterElement.d.ts.map +1 -0
  36. package/dist/components/core/filter/SolidOneToManyFilterElement.js +86 -0
  37. package/dist/components/core/filter/SolidOneToManyFilterElement.js.map +1 -0
  38. package/dist/components/core/filter/SolidOneToManyFilterElement.tsx +62 -0
  39. package/dist/components/core/filter/SolidVarInputsFilterElement.d.ts +1 -0
  40. package/dist/components/core/filter/SolidVarInputsFilterElement.d.ts.map +1 -1
  41. package/dist/components/core/filter/SolidVarInputsFilterElement.js +4 -1
  42. package/dist/components/core/filter/SolidVarInputsFilterElement.js.map +1 -1
  43. package/dist/components/core/filter/SolidVarInputsFilterElement.tsx +10 -0
  44. package/dist/components/core/filter/fields/SolidRelationField.d.ts.map +1 -1
  45. package/dist/components/core/filter/fields/SolidRelationField.js +4 -2
  46. package/dist/components/core/filter/fields/SolidRelationField.js.map +1 -1
  47. package/dist/components/core/filter/fields/SolidRelationField.tsx +4 -2
  48. package/dist/components/core/filter/fields/relations/SolidRelationOneToManyField.d.ts +4 -0
  49. package/dist/components/core/filter/fields/relations/SolidRelationOneToManyField.d.ts.map +1 -0
  50. package/dist/components/core/filter/fields/relations/SolidRelationOneToManyField.js +25 -0
  51. package/dist/components/core/filter/fields/relations/SolidRelationOneToManyField.js.map +1 -0
  52. package/dist/components/core/filter/fields/relations/SolidRelationOneToManyField.tsx +60 -0
  53. package/dist/components/core/form/SolidFormFooter.js +4 -4
  54. package/dist/components/core/form/SolidFormFooter.js.map +1 -1
  55. package/dist/components/core/form/SolidFormFooter.tsx +4 -4
  56. package/dist/components/core/form/fields/SolidBooleanField.d.ts.map +1 -1
  57. package/dist/components/core/form/fields/SolidBooleanField.js +11 -8
  58. package/dist/components/core/form/fields/SolidBooleanField.js.map +1 -1
  59. package/dist/components/core/form/fields/SolidBooleanField.tsx +20 -8
  60. package/dist/components/core/form/fields/relations/SolidRelationManyToManyField.d.ts +40 -0
  61. package/dist/components/core/form/fields/relations/SolidRelationManyToManyField.d.ts.map +1 -1
  62. package/dist/components/core/form/fields/relations/SolidRelationManyToManyField.js +317 -157
  63. package/dist/components/core/form/fields/relations/SolidRelationManyToManyField.js.map +1 -1
  64. package/dist/components/core/form/fields/relations/SolidRelationManyToManyField.tsx +463 -243
  65. package/dist/components/core/form/fields/relations/SolidRelationOneToManyField.d.ts.map +1 -1
  66. package/dist/components/core/form/fields/relations/SolidRelationOneToManyField.js +46 -95
  67. package/dist/components/core/form/fields/relations/SolidRelationOneToManyField.js.map +1 -1
  68. package/dist/components/core/form/fields/relations/SolidRelationOneToManyField.tsx +57 -113
  69. package/dist/components/core/form/fields/relations/widgets/helpers/useRelationEntityHandler.d.ts +15 -4
  70. package/dist/components/core/form/fields/relations/widgets/helpers/useRelationEntityHandler.d.ts.map +1 -1
  71. package/dist/components/core/form/fields/relations/widgets/helpers/useRelationEntityHandler.js +220 -33
  72. package/dist/components/core/form/fields/relations/widgets/helpers/useRelationEntityHandler.js.map +1 -1
  73. package/dist/components/core/form/fields/relations/widgets/helpers/useRelationEntityHandler.ts +167 -36
  74. package/dist/components/core/kanban/SolidKanbanView.d.ts.map +1 -1
  75. package/dist/components/core/kanban/SolidKanbanView.js +13 -12
  76. package/dist/components/core/kanban/SolidKanbanView.js.map +1 -1
  77. package/dist/components/core/kanban/SolidKanbanView.tsx +8 -7
  78. package/dist/components/core/list/SolidListView.d.ts +18 -10
  79. package/dist/components/core/list/SolidListView.d.ts.map +1 -1
  80. package/dist/components/core/list/SolidListView.js +176 -177
  81. package/dist/components/core/list/SolidListView.js.map +1 -1
  82. package/dist/components/core/list/SolidListView.tsx +130 -143
  83. package/dist/components/core/list/SolidListViewConfigure.d.ts +7 -0
  84. package/dist/components/core/list/SolidListViewConfigure.d.ts.map +1 -1
  85. package/dist/components/core/list/SolidListViewConfigure.js +6 -5
  86. package/dist/components/core/list/SolidListViewConfigure.js.map +1 -1
  87. package/dist/components/core/list/SolidListViewConfigure.tsx +21 -12
  88. package/dist/components/core/list/columns/SolidShortTextColumn.d.ts.map +1 -1
  89. package/dist/components/core/list/columns/SolidShortTextColumn.js +1 -37
  90. package/dist/components/core/list/columns/SolidShortTextColumn.js.map +1 -1
  91. package/dist/components/core/list/columns/SolidShortTextColumn.tsx +0 -41
  92. package/dist/components/core/list/columns/relations/SolidRelationManyToOneColumn.d.ts.map +1 -1
  93. package/dist/components/core/list/columns/relations/SolidRelationManyToOneColumn.js +9 -5
  94. package/dist/components/core/list/columns/relations/SolidRelationManyToOneColumn.js.map +1 -1
  95. package/dist/components/core/list/columns/relations/SolidRelationManyToOneColumn.tsx +14 -3
  96. package/dist/components/core/list/listViewRegistry.js.map +1 -1
  97. package/dist/components/core/list/listViewRegistry.ts +1 -2
  98. package/dist/components/core/tree/SolidTreeView.d.ts +38 -0
  99. package/dist/components/core/tree/SolidTreeView.d.ts.map +1 -0
  100. package/dist/components/core/tree/SolidTreeView.js +1179 -0
  101. package/dist/components/core/tree/SolidTreeView.js.map +1 -0
  102. package/dist/components/core/tree/SolidTreeView.tsx +1637 -0
  103. package/dist/components/core/tree/treeViewRegistry.d.ts +7 -0
  104. package/dist/components/core/tree/treeViewRegistry.d.ts.map +1 -0
  105. package/dist/components/core/tree/treeViewRegistry.js +17 -0
  106. package/dist/components/core/tree/treeViewRegistry.js.map +1 -0
  107. package/dist/components/core/tree/treeViewRegistry.ts +23 -0
  108. package/dist/components/core/users/CreateUser.d.ts.map +1 -1
  109. package/dist/components/core/users/CreateUser.js +19 -6
  110. package/dist/components/core/users/CreateUser.js.map +1 -1
  111. package/dist/components/core/users/CreateUser.tsx +39 -0
  112. package/dist/helpers/helpers.d.ts +2 -0
  113. package/dist/helpers/helpers.d.ts.map +1 -1
  114. package/dist/helpers/helpers.js +3 -1
  115. package/dist/helpers/helpers.js.map +1 -1
  116. package/dist/helpers/helpers.ts +4 -1
  117. package/dist/helpers/registry.d.ts.map +1 -1
  118. package/dist/helpers/registry.js +5 -1
  119. package/dist/helpers/registry.js.map +1 -1
  120. package/dist/helpers/registry.ts +7 -2
  121. package/dist/index.d.ts +3 -1
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +2 -0
  124. package/dist/index.js.map +1 -1
  125. package/dist/index.ts +6 -1
  126. package/dist/resources/globals.css +32 -4
  127. package/dist/routes/pages/admin/core/ListPage.d.ts.map +1 -1
  128. package/dist/routes/pages/admin/core/ListPage.js +2 -2
  129. package/dist/routes/pages/admin/core/ListPage.js.map +1 -1
  130. package/dist/routes/pages/admin/core/ListPage.tsx +3 -2
  131. package/dist/routes/pages/admin/core/ModuleHomePage.d.ts.map +1 -1
  132. package/dist/routes/pages/admin/core/ModuleHomePage.js +4 -15
  133. package/dist/routes/pages/admin/core/ModuleHomePage.js.map +1 -1
  134. package/dist/routes/pages/admin/core/ModuleHomePage.tsx +4 -3
  135. package/dist/routes/pages/admin/core/TreePage.d.ts +2 -0
  136. package/dist/routes/pages/admin/core/TreePage.d.ts.map +1 -0
  137. package/dist/routes/pages/admin/core/TreePage.js +37 -0
  138. package/dist/routes/pages/admin/core/TreePage.js.map +1 -0
  139. package/dist/routes/pages/admin/core/TreePage.tsx +30 -0
  140. package/dist/routes/solidRoutes.d.ts.map +1 -1
  141. package/dist/routes/solidRoutes.js +2 -0
  142. package/dist/routes/solidRoutes.js.map +1 -1
  143. package/dist/routes/solidRoutes.tsx +3 -1
  144. package/dist/routes/types.d.ts +1 -1
  145. package/dist/routes/types.d.ts.map +1 -1
  146. package/dist/routes/types.js.map +1 -1
  147. package/dist/routes/types.ts +1 -0
  148. package/dist/types/index.d.ts +8 -2
  149. package/dist/types/solid-core.d.ts +40 -0
  150. package/package.json +1 -1
@@ -0,0 +1,1637 @@
1
+ import React, {
2
+ forwardRef,
3
+ useEffect,
4
+ useImperativeHandle,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+ import { Toast } from "primereact/toast";
10
+ import { useDispatch, useSelector } from "react-redux";
11
+ import { showNavbar, toggleNavbar } from "../../../redux/features/navbarSlice";
12
+ import { useGetSolidViewLayoutQuery } from "../../../redux/api/solidViewApi";
13
+ import qs from "qs";
14
+ import { useSearchParams } from "../../../hooks/useSearchParams";
15
+ import { SolidGlobalSearchElement } from "../common/SolidGlobalSearchElement";
16
+ import { Button } from "primereact/button";
17
+ import { permissionExpression } from "../../../helpers/permissions";
18
+ import { SolidCreateButton } from "../common/SolidCreateButton";
19
+ import { Dialog } from "primereact/dialog";
20
+ import { createSolidEntityApi } from "../../../redux/api/solidEntityApi";
21
+ import { AggregationRule, GroupingRule } from "../common/GroupingComponent";
22
+ import { TreeTable } from "primereact/treetable";
23
+ import { Dropdown } from "primereact/dropdown";
24
+ import type { TreeNode } from "primereact/treenode";
25
+ import { Column } from "primereact/column";
26
+ import { SolidListViewColumn } from "../list/SolidListViewColumn";
27
+ import { hasAnyRole } from "../../../helpers/rolesHelper";
28
+ import { useSession } from "../../../hooks/useSession";
29
+ import { useLazyCheckIfPermissionExistsQuery } from "../../../redux/api/userApi";
30
+ import { SolidListViewConfigure } from "../list/SolidListViewConfigure";
31
+ import CompactImage from '../../../resources/images/layout/images/compact.png';
32
+ import CozyImage from '../../../resources/images/layout/images/cozy.png';
33
+ import ComfortableImage from '../../../resources/images/layout/images/comfortable.png';
34
+ import { Divider } from "primereact/divider";
35
+ import { ERROR_MESSAGES } from "../../../constants/error-messages";
36
+ import { getSingularAndPlural } from "../../../helpers/helpers";
37
+ import { getFilterObjectFromLocalStorage, setFilterObjectToLocalStorage } from "../list/SolidListView";
38
+ import { HomePageModuleSvg } from "../../Svg/HomePageModuleSvg";
39
+ import { SolidBeforeTreeNodeLoad } from "../../../types";
40
+ import { getExtensionFunction } from "../../../helpers/registry";
41
+ import { SolidTreeLoad, SolidTreeUiEventResponse } from "../../../types/solid-core";
42
+ import { Tooltip } from "primereact/tooltip";
43
+
44
+ // ─── Types ────────────────────────────────────────────────────────────────────
45
+
46
+ type SolidTreeViewParams = {
47
+ moduleName: string;
48
+ modelName: string;
49
+ inlineCreate?: boolean;
50
+ handlePopUpOpen?: any;
51
+ embeded?: boolean;
52
+ customLayout?: any;
53
+ customFilter?: any;
54
+ };
55
+
56
+ export type SolidTreeViewHandle = {
57
+ refresh: () => void;
58
+ clearFilters: () => void;
59
+ applyFilter: (filter: {
60
+ custom_filter_predicate?: any;
61
+ search_predicate?: any;
62
+ saved_filter_predicate?: any;
63
+ predefined_search_predicate?: any;
64
+ }) => void;
65
+ setPagination: (nextFirst: number, nextRows: number) => void;
66
+ setSort: (nextSortField: string, nextSortOrder: 1 | -1 | 0) => void;
67
+ setShowArchived: (value: boolean) => void;
68
+ getState: () => {
69
+ first: number;
70
+ rows: number;
71
+ sortField: string;
72
+ sortOrder: 1 | -1 | 0;
73
+ showArchived: boolean;
74
+ filters: any;
75
+ filterPredicates: any;
76
+ listData: any[];
77
+ totalRecords: number;
78
+ loading: boolean;
79
+ };
80
+ };
81
+
82
+ type GroupPathItem = {
83
+ ruleIndex: number;
84
+ fieldName: string;
85
+ filterField: string;
86
+ value: any;
87
+ dateGranularity?: string | null;
88
+ };
89
+
90
+ type NodeMeta = {
91
+ nodeType: "group" | "record";
92
+ ruleIndex: number;
93
+ groupLabel?: string;
94
+ idCount?: number | string;
95
+ aggregates?: Record<string, any>;
96
+ groupPath: GroupPathItem[];
97
+ };
98
+
99
+ type TreeRowData = Record<string, any> & {
100
+ __treeMeta?: NodeMeta;
101
+ };
102
+
103
+ /**
104
+ * Pagination entry stored per node key.
105
+ * - "root" is used for the top-level group list.
106
+ * - Any other node.key is used for its children (groups or records).
107
+ */
108
+ type PaginationEntry = {
109
+ offset: number; // current page start
110
+ limit: number; // page size
111
+ total: number; // total items returned from API (used to determine hasNext)
112
+ };
113
+
114
+ const DEFAULT_PAGE_SIZE = 25;
115
+
116
+ // ─── Component ────────────────────────────────────────────────────────────────
117
+
118
+ export const SolidTreeView = forwardRef<SolidTreeViewHandle, SolidTreeViewParams>((params, ref) => {
119
+ const toast = useRef<Toast>(null);
120
+ const dispatch = useDispatch();
121
+ const visibleNavbar = useSelector((state: any) => state.navbarState?.visibleNavbar);
122
+ const searchParams = useSearchParams();
123
+
124
+ const session = useSession();
125
+ const user = session?.data?.user;
126
+
127
+ const solidGlobalSearchElementRef = useRef<any>(null);
128
+
129
+ const [showSaveFilterPopup, setShowSaveFilterPopup] = useState<boolean>(false);
130
+ const [showGlobalSearchElement, setShowGlobalSearchElement] = useState<boolean>(false);
131
+ const [toPopulate, setToPopulate] = useState<string[]>([]);
132
+ const [toPopulateMedia, setToPopulateMedia] = useState<string[]>([]);
133
+ const [actionsAllowed, setActionsAllowed] = useState<string[]>([]);
134
+ const [createButtonUrl, setCreateButtonUrl] = useState<string>();
135
+ const [createActionQueryParams, setCreateActionQueryParams] = useState<Record<string, string>>({});
136
+ const [selectedRecords, setSelectedRecords] = useState<any[]>([]);
137
+ const [selectedRecoverRecords, setSelectedRecoverRecords] = useState<any[]>([]);
138
+
139
+
140
+ const [groupingRules, setGroupingRules] = useState<GroupingRule[]>([]);
141
+ const [aggregationRules, setAggregationRules] = useState<AggregationRule[]>([]);
142
+
143
+ const [treeNodes, setTreeNodes] = useState<TreeNode[]>([]);
144
+ const [selectedNodeKeys, setSelectedNodeKeys] = useState<Record<string, any>>({});
145
+ const [expandedKeys, setExpandedKeys] = useState<any>({});
146
+ const [treeLoading, setTreeLoading] = useState<boolean>(false);
147
+
148
+ const [sortField, setSortField] = useState<string>("");
149
+ const [sortOrder, setSortOrder] = useState<number>(0);
150
+
151
+ const [pageSizeOptions, setPageSizeOptions] = useState<number[]>([10, 25, 50]);
152
+ const [globalLimit, setGlobalLimit] = useState<number>(25);
153
+
154
+ const [isDeleteRecordsDialogVisible, setDeleteRecordsDialogVisible] = useState(false);
155
+ const [isRecoverDialogVisible, setRecoverDialogVisible] = useState(false);
156
+ const [showArchived, setShowArchived] = useState(false);
157
+
158
+
159
+ const sizeOptions = [
160
+ { label: "Compact", value: "small", image: CompactImage },
161
+ { label: "Cozy", value: "normal", image: CozyImage },
162
+ { label: "Comfortable", value: "large", image: ComfortableImage },
163
+ ];
164
+
165
+ const [size, setSize] = useState<string | any>(sizeOptions[1].value);
166
+ const [viewModes, setViewModes] = useState<any>([]);
167
+
168
+
169
+ // ── Pagination state ──────────────────────────────────────────────────────
170
+ /**
171
+ * Key: "root" for top-level group list; node.key (string) for any other node.
172
+ * Value: { offset, limit, total }
173
+ */
174
+ const [paginationMap, setPaginationMap] = useState<Record<string, PaginationEntry>>({});
175
+
176
+ const getPagination = (key: string): PaginationEntry =>
177
+ paginationMap[key] ?? { offset: 0, limit: globalLimit, total: 0 };
178
+
179
+ const setPagination = (key: string, entry: Partial<PaginationEntry>) =>
180
+ setPaginationMap((prev) => ({
181
+ ...prev,
182
+ [key]: { ...getPagination(key), ...entry },
183
+ }));
184
+
185
+ // ── Pagination helpers ────────────────────────────────────────────────────
186
+
187
+ const hasPrev = (key: string) => getPagination(key).offset > 0;
188
+
189
+ const hasNext = (key: string) => {
190
+ const { offset, limit, total } = getPagination(key);
191
+ // total here is the count of records returned by the last fetch for that node.
192
+ // We consider "has next" when we received a full page (i.e. there might be more).
193
+ return offset + limit < total;
194
+ };
195
+
196
+ // ─────────────────────────────────────────────────────────────────────────
197
+
198
+ const menuItemId = searchParams.get("menuItemId");
199
+ const menuItemName = searchParams.get("menuItemName");
200
+ const actionId = searchParams.get("actionId");
201
+ const actionName = searchParams.get("actionName");
202
+
203
+ const entityApi = createSolidEntityApi(params.modelName);
204
+ const {
205
+ useCreateSolidEntityMutation,
206
+ useDeleteMultipleSolidEntitiesMutation,
207
+ useDeleteSolidEntityMutation,
208
+ useGetSolidEntitiesQuery,
209
+ useGetSolidEntityByIdQuery,
210
+ useLazyGetSolidEntitiesQuery,
211
+ useLazyGetSolidEntityByIdQuery,
212
+ usePrefetch,
213
+ useUpdateSolidEntityMutation,
214
+ useRecoverSolidEntityByIdQuery,
215
+ useLazyRecoverSolidEntityByIdQuery,
216
+ useRecoverSolidEntityMutation,
217
+ } = entityApi;
218
+
219
+ const [triggerGetSolidEntities] = useLazyGetSolidEntitiesQuery();
220
+
221
+ const [
222
+ triggerRecoverSolidEntitiesById,
223
+ {
224
+ data: recoverByIdData,
225
+ isLoading: recoverByIdIsLoading,
226
+ error: recoverByIdError,
227
+ isError: recoverByIdIsError,
228
+ isSuccess: recoverByIdIsSuccess,
229
+ },
230
+ ] = useLazyRecoverSolidEntityByIdQuery();
231
+
232
+ const [
233
+ triggerRecoverSolidEntities,
234
+ {
235
+ data: recoverByData,
236
+ isLoading: recoverByIsLoading,
237
+ error: recoverError,
238
+ isError: recoverIsError,
239
+ isSuccess: recoverByIsSuccess,
240
+ },
241
+ ] = useRecoverSolidEntityMutation();
242
+
243
+
244
+
245
+ const treeViewMetaDataQs = qs.stringify(
246
+ {
247
+ modelName: params.modelName,
248
+ moduleName: params.moduleName,
249
+ viewType: "list",
250
+ menuItemId,
251
+ menuItemName,
252
+ actionId,
253
+ actionName,
254
+ },
255
+ { encodeValuesOnly: true }
256
+ );
257
+
258
+ const { data: solidTreeViewMetaData } = useGetSolidViewLayoutQuery(treeViewMetaDataQs);
259
+ const solidTreeViewLayout = params.customLayout || solidTreeViewMetaData?.data?.solidView?.layout;
260
+
261
+ const [triggerCheckIfPermissionExists] = useLazyCheckIfPermissionExistsQuery();
262
+
263
+ useEffect(() => {
264
+ const fetchPermissions = async () => {
265
+ if (params.modelName) {
266
+ const permissionNames = [
267
+ permissionExpression(params.modelName, 'create'),
268
+ permissionExpression(params.modelName, 'delete'),
269
+ permissionExpression(params.modelName, 'update'),
270
+ permissionExpression(params.modelName, 'deleteMany'),
271
+ permissionExpression(params.modelName, 'findOne'),
272
+ permissionExpression(params.modelName, 'findMany'),
273
+ permissionExpression(params.modelName, 'insertMany'),
274
+ permissionExpression('importTransaction', 'create'),
275
+ permissionExpression('exportTransaction', 'create'),
276
+ permissionExpression('userViewMetadata', 'create'),
277
+ permissionExpression('savedFilters', 'create')
278
+ ];
279
+ const queryData = {
280
+ permissionNames: permissionNames,
281
+ };
282
+ const queryString = qs.stringify(queryData, {
283
+ encodeValuesOnly: true,
284
+ });
285
+ const response = await triggerCheckIfPermissionExists(queryString);
286
+ setActionsAllowed(response.data.data);
287
+ }
288
+ };
289
+ fetchPermissions();
290
+ }, [params.modelName]);
291
+
292
+ const [deleteManySolidEntities] = useDeleteMultipleSolidEntitiesMutation();
293
+
294
+ const treeViewTitle = solidTreeViewMetaData?.data?.solidView?.displayName;
295
+
296
+ const toggleBothSidebars = () => {
297
+ if (visibleNavbar) {
298
+ dispatch(toggleNavbar());
299
+ } else {
300
+ dispatch(showNavbar());
301
+ }
302
+ };
303
+
304
+ const [filters, setFilters] = useState<any>(null);
305
+ const [filterPredicates, setFilterPredicates] = useState<any>(null);
306
+
307
+ const latestFiltersRef = useRef<any>(filters);
308
+ const latestFilterPredicatesRef = useRef<any>(filterPredicates);
309
+
310
+ const activeGroupingRules = useMemo(
311
+ () => (groupingRules || []).filter((rule) => !!rule?.fieldName),
312
+ [groupingRules]
313
+ );
314
+
315
+ useEffect(() => { latestFiltersRef.current = filters; }, [filters]);
316
+ useEffect(() => { latestFilterPredicatesRef.current = filterPredicates; }, [filterPredicates]);
317
+
318
+ useEffect(() => {
319
+ if (solidTreeViewMetaData) {
320
+ setViewModes(solidTreeViewMetaData?.data?.viewModes);
321
+ }
322
+ }, [solidTreeViewMetaData]);
323
+
324
+ useEffect(() => {
325
+ const solidFieldsMetadata = solidTreeViewMetaData?.data?.solidFieldsMetadata;
326
+ const queryObject = getFilterObjectFromLocalStorage();
327
+
328
+ const layoutPageSizeOptions = solidTreeViewMetaData?.data?.solidView?.layout?.attrs?.pageSizeOptions;
329
+ const currentOptions = (Array.isArray(layoutPageSizeOptions) && layoutPageSizeOptions.length > 0)
330
+ ? layoutPageSizeOptions
331
+ : [15, 25, 50];
332
+
333
+ setPageSizeOptions(currentOptions);
334
+
335
+ if (queryObject) {
336
+ setToPopulate(queryObject.populate || []);
337
+ setToPopulateMedia(queryObject.populateMedia || []);
338
+ setSortField(queryObject.sortField || "");
339
+ setSortOrder(queryObject.sortOrder || 0);
340
+
341
+ const savedLimit = queryObject.limit;
342
+ if (savedLimit && currentOptions.includes(savedLimit)) {
343
+ setGlobalLimit(savedLimit);
344
+ } else {
345
+ setGlobalLimit(currentOptions[0]);
346
+ }
347
+ }
348
+ else {
349
+ if (!solidTreeViewLayout || !solidFieldsMetadata) {
350
+ setToPopulate([]);
351
+ setToPopulateMedia([]);
352
+ setGlobalLimit(currentOptions[0]);
353
+ return;
354
+ }
355
+
356
+ const nextPopulate: string[] = [];
357
+ const nextPopulateMedia: string[] = [];
358
+
359
+ for (const column of solidTreeViewLayout?.children || []) {
360
+ const fieldMetadata = solidFieldsMetadata?.[column?.attrs?.name];
361
+ if (!fieldMetadata) continue;
362
+
363
+ if (fieldMetadata.type === "relation" && fieldMetadata.relationType === "many-to-one") {
364
+ if (!nextPopulate.includes(fieldMetadata.name)) nextPopulate.push(fieldMetadata.name);
365
+ }
366
+ if (fieldMetadata.type === "mediaSingle" || fieldMetadata.type === "mediaMultiple") {
367
+ if (!nextPopulateMedia.includes(fieldMetadata.name)) nextPopulateMedia.push(fieldMetadata.name);
368
+ }
369
+ }
370
+
371
+ setToPopulate(nextPopulate);
372
+ setToPopulateMedia(nextPopulateMedia);
373
+ setGlobalLimit(currentOptions[0]);
374
+ }
375
+ }, [solidTreeViewLayout, solidTreeViewMetaData]);
376
+
377
+ useEffect(() => {
378
+
379
+ // event invocation is not tested
380
+ if (solidTreeViewMetaData && solidTreeViewMetaData?.data) {
381
+ const handleDynamicFunction = async () => {
382
+ const dynamicHeader = solidTreeViewMetaData?.data?.solidView?.layout?.onTreeLoad;
383
+ let dynamicExtensionFunction = null;
384
+ let treeLayout = solidTreeViewMetaData?.data?.solidView?.layout;
385
+ let treeViewNodes = treeNodes;
386
+ if (params.customLayout) {
387
+ treeLayout = params.customLayout;
388
+ }
389
+ const event: SolidTreeLoad = {
390
+ fieldsMetadata: solidTreeViewMetaData?.data?.solidFieldsMetadata,
391
+ type: "onTreeLoad",
392
+ nodes: treeViewNodes,
393
+ viewMetadata: solidTreeViewMetaData?.data?.solidView,
394
+ treeViewLayout: treeLayout,
395
+ queryParams: {
396
+ menuItemId: menuItemId,
397
+ menuItemName: menuItemName,
398
+ actionId: actionId,
399
+ actionName: actionName,
400
+ },
401
+ user: user,
402
+ session: session.data,
403
+ params: params
404
+ };
405
+
406
+ if (dynamicHeader) {
407
+ dynamicExtensionFunction = getExtensionFunction(dynamicHeader);
408
+ if (dynamicExtensionFunction) {
409
+ const updatedListData: SolidTreeUiEventResponse = await dynamicExtensionFunction(event);
410
+
411
+ if (updatedListData && updatedListData?.dataChanged && updatedListData?.newNodes) {
412
+ treeViewNodes = updatedListData.newNodes;
413
+ }
414
+ if (updatedListData && updatedListData?.layoutChanged && updatedListData?.newLayout) {
415
+ treeLayout = updatedListData.newLayout;
416
+ }
417
+ }
418
+ if (treeViewNodes) {
419
+ setTreeNodes(treeViewNodes);
420
+ }
421
+ if (treeLayout) {
422
+ solidTreeViewLayout(treeLayout);
423
+ }
424
+ }
425
+ };
426
+ handleDynamicFunction();
427
+ }
428
+
429
+ }, [treeNodes]);
430
+
431
+
432
+ // ─── Field / filter helpers ───────────────────────────────────────────────
433
+
434
+ const getFieldMetadata = (fieldName: string) =>
435
+ solidTreeViewMetaData?.data?.solidFieldsMetadata?.[fieldName];
436
+
437
+ const getResolvedGroupField = (fieldName: string) => {
438
+ const fieldMetadata = getFieldMetadata(fieldName);
439
+ if (
440
+ fieldMetadata?.type === "relation" &&
441
+ fieldMetadata?.relationType === "many-to-one" &&
442
+ fieldMetadata?.relationModel?.userKeyField?.name
443
+ ) {
444
+ return `${fieldName}.${fieldMetadata.relationModel.userKeyField.name}`;
445
+ }
446
+ return fieldName;
447
+ };
448
+
449
+ const toGroupByParam = (rule: GroupingRule) => {
450
+ const fieldName = String(rule.fieldName);
451
+ const resolvedField = getResolvedGroupField(fieldName);
452
+ if (!rule.dateGrouping) return resolvedField;
453
+ if (rule.dateGrouping === "YYYY") return `${resolvedField}:year:YYYY`;
454
+ if (rule.dateGrouping === "MMM") return `${resolvedField}:month:MMM`;
455
+ if (rule.dateGrouping === "YYYY-MM") return `${resolvedField}:month:YYYY-MM`;
456
+ if (rule.dateGrouping === "YYYY-MM-DD") return `${resolvedField}:day:YYYY-MM-DD`;
457
+ return resolvedField;
458
+ };
459
+
460
+ // const dateTimeImplicitFilter = (rule: GroupingRule) => {
461
+ // const fieldMetadata = getFieldMetadata(String(rule.fieldName));
462
+ // return !!rule.dateGrouping && ["date", "datetime"].includes(fieldMetadata?.type);
463
+ // };
464
+
465
+ const getDateGranularity = (rule: GroupingRule) => {
466
+ const fieldMetadata = getFieldMetadata(String(rule.fieldName));
467
+ if (rule.dateGrouping && ["date", "datetime"].includes(fieldMetadata?.type)) {
468
+ switch (rule.dateGrouping) {
469
+ case "YYYY":
470
+ return "year";
471
+ case "MMM":
472
+ return "month";
473
+ case "YYYY-MM":
474
+ return "month";
475
+ case "YYYY-MM-DD":
476
+ return "day";
477
+ }
478
+ }
479
+ return null;
480
+ }
481
+
482
+ const buildNestedEqCondition = (fieldPath: string, value: any, dateGranularity: any) => {
483
+ const parts = fieldPath.split(".").filter(Boolean);
484
+ if (parts.length === 0) return {};
485
+ if (dateGranularity) {
486
+ return { [`${parts[0]}:${dateGranularity}`]: { $eq: value } };
487
+ }
488
+ return parts.reduceRight((acc: any, part: string) => ({ [part]: acc }), { $eq: value });
489
+ };
490
+
491
+ const buildImplicitFiltersFromPath = (groupPath: GroupPathItem[]) =>
492
+ groupPath
493
+ // .filter((item) => !item.skipImplicitFilter)
494
+ .map((item) => buildNestedEqCondition(item.filterField, item.value, item.dateGranularity));
495
+
496
+ const mergeFiltersWithImplicit = (implicitFilters: any[]) => {
497
+ const baseFilters = latestFiltersRef.current;
498
+ const hasBaseFilters = baseFilters && Object.keys(baseFilters).length > 0;
499
+ if (!hasBaseFilters && implicitFilters.length === 0) return null;
500
+ const merged = hasBaseFilters ? structuredClone(baseFilters) : { $and: [] };
501
+ if (implicitFilters.length === 0) return merged;
502
+ if (Array.isArray(merged.$and)) {
503
+ merged.$and.push(...implicitFilters);
504
+ return merged;
505
+ }
506
+ return { $and: [merged, ...implicitFilters] };
507
+ };
508
+
509
+ const buildAggregates = () => {
510
+ const derivedAggregates = (aggregationRules || [])
511
+ .filter((rule) => !!rule?.fieldName && !!rule?.operator)
512
+ .map((rule) => `${rule.fieldName}:${rule.operator}`);
513
+ return derivedAggregates.length > 0 ? derivedAggregates : ["id:count"];
514
+ };
515
+
516
+ const extractGroupCount = (groupMeta: any) => {
517
+ if (groupMeta?.id_count !== undefined) return groupMeta.id_count;
518
+ const countKey = Object.keys(groupMeta || {}).find((key) => key.endsWith("_count"));
519
+ if (countKey) return groupMeta[countKey];
520
+ return undefined;
521
+ };
522
+
523
+ const normalizeRecord = (record: any) => {
524
+ const newRecord = { ...record };
525
+ Object.entries(newRecord).forEach(([key, value]) => {
526
+ if (typeof value === "string") {
527
+ try {
528
+ const parsed = JSON.parse(value);
529
+ if (Array.isArray(parsed)) newRecord[key] = parsed.join(", ");
530
+ } catch {
531
+ if (/^\[.*\]$/.test(value)) newRecord[key] = value.replace(/[\[\]"]+/g, "");
532
+ }
533
+ }
534
+ });
535
+ return newRecord;
536
+ };
537
+
538
+ const formatHeader = (key: string) => {
539
+ return key
540
+ .split("_")
541
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
542
+ .join(" ");
543
+ };
544
+
545
+ const getSortParam = (ruleIndex: number) => {
546
+ if (!sortField || !sortOrder) return null;
547
+
548
+ const dir = sortOrder === 1 ? "ASC" : "DESC";
549
+
550
+ // 1. Sorting by the "Group" column
551
+ if (sortField === "__group") {
552
+ const rule = activeGroupingRules[ruleIndex];
553
+ if (!rule) return null;
554
+ // At this level, sort by the field we are grouping by
555
+ return `${toGroupByParam(rule)}:${dir}`;
556
+ }
557
+
558
+ // 2. Sorting by an aggregate column
559
+ const isAggregate = aggregationRules.some((rule) => {
560
+ const responseKey = `${rule.fieldName}:${rule.operator}`;
561
+ return responseKey === sortField || `${rule.fieldName}_${rule.operator}` === sortField;
562
+ });
563
+
564
+ if (isAggregate) {
565
+ const [field, op] = sortField.includes(":") ? sortField.split(":") : sortField.split("_");
566
+
567
+ // For leaf level: only field:DIR
568
+ if (ruleIndex >= activeGroupingRules.length) {
569
+ return `${field}:${dir}`;
570
+ }
571
+
572
+ // For group levels: field:operator:DIR
573
+ return `${field}:${op}:${dir}`;
574
+ }
575
+
576
+ // 3. Sorting by a leaf column (regular record field)
577
+ return `${sortField}:${dir}`;
578
+ };
579
+
580
+ // ─── API fetch helpers (now accept offset + limit) ────────────────────────
581
+
582
+ const runGroupedQuery = async (
583
+ ruleIndex: number,
584
+ groupPath: GroupPathItem[],
585
+ offset: number,
586
+ limit: number
587
+ ) => {
588
+ const rule = activeGroupingRules[ruleIndex];
589
+ if (!rule?.fieldName) return { response: null };
590
+
591
+ let queryData: any = {
592
+ offset,
593
+ limit,
594
+ groupBy: [toGroupByParam(rule)],
595
+ aggregates: buildAggregates(),
596
+ };
597
+
598
+ const sortParam = getSortParam(ruleIndex);
599
+ if (sortParam) queryData.sort = [sortParam];
600
+
601
+ const implicitFilters = buildImplicitFiltersFromPath(groupPath);
602
+ const mergedFilters = mergeFiltersWithImplicit(implicitFilters);
603
+ if (mergedFilters) queryData.filters = mergedFilters;
604
+
605
+ // event invocation is not tested
606
+ const dynamicHeader = solidTreeViewMetaData?.data?.solidView?.layout?.onBeforeTreeDataLoad;
607
+ let dynamicExtensionFunction = null;
608
+ const event: SolidBeforeTreeNodeLoad = {
609
+ type: "onBeforeTreeDataLoad",
610
+ level: ruleIndex,
611
+ levelFieldName: rule.fieldName,
612
+ fieldsMetadata: solidTreeViewMetaData?.data?.solidFieldsMetadata,
613
+ viewMetadata: solidTreeViewMetaData?.data?.solidView,
614
+ treeViewLayout: solidTreeViewMetaData?.data.solidView.layout,
615
+ filter: structuredClone(queryData),
616
+ queryParams: {
617
+ menuItemId: menuItemId,
618
+ menuItemName: menuItemName,
619
+ actionId: actionId,
620
+ actionName: actionName,
621
+ },
622
+ user: user,
623
+ session: session.data,
624
+ params: params
625
+ };
626
+
627
+ if (dynamicHeader) {
628
+ dynamicExtensionFunction = getExtensionFunction(dynamicHeader);
629
+ if (dynamicExtensionFunction) {
630
+ try {
631
+ const updatedListData: SolidTreeUiEventResponse = await dynamicExtensionFunction(event);
632
+ if (updatedListData && updatedListData?.filterApplied && updatedListData?.newFilter) {
633
+ queryData = updatedListData?.newFilter;
634
+ }
635
+ } catch (err) {
636
+ console.error("Error executing onBeforeTreeDataLoad extension:", err);
637
+ }
638
+ }
639
+ }
640
+
641
+ const queryString = qs.stringify(queryData, { encodeValuesOnly: true });
642
+
643
+ const response = await triggerGetSolidEntities(queryString).unwrap();
644
+ return { queryData, queryString, response };
645
+ };
646
+
647
+ const runLeafQuery = async (
648
+ groupPath: GroupPathItem[],
649
+ offset: number,
650
+ limit: number
651
+ ) => {
652
+ let queryData: any = {
653
+ offset,
654
+ limit,
655
+ sort: getSortParam(groupPath.length) ? [getSortParam(groupPath.length)] : ["id:desc"],
656
+ populate: toPopulate,
657
+ populateMedia: toPopulateMedia,
658
+ };
659
+
660
+ const implicitFilters = buildImplicitFiltersFromPath(groupPath);
661
+ const mergedFilters = mergeFiltersWithImplicit(implicitFilters);
662
+ if (mergedFilters) queryData.filters = mergedFilters;
663
+
664
+
665
+ // event invocation is not tested
666
+ const dynamicHeader = solidTreeViewMetaData?.data?.solidView?.layout?.onBeforeTreeDataLoad;
667
+ let dynamicExtensionFunction = null;
668
+ const event: SolidBeforeTreeNodeLoad = {
669
+ type: "onBeforeTreeDataLoad",
670
+ level: groupPath.length,
671
+ levelFieldName: groupPath[groupPath.length - 1].fieldName,
672
+ fieldsMetadata: solidTreeViewMetaData?.data?.solidFieldsMetadata,
673
+ viewMetadata: solidTreeViewMetaData?.data?.solidView,
674
+ treeViewLayout: solidTreeViewMetaData?.data.solidView.layout,
675
+ filter: structuredClone(queryData),
676
+ queryParams: {
677
+ menuItemId: menuItemId,
678
+ menuItemName: menuItemName,
679
+ actionId: actionId,
680
+ actionName: actionName,
681
+ },
682
+ user: user,
683
+ session: session.data,
684
+ params: params
685
+ };
686
+
687
+ if (dynamicHeader) {
688
+ dynamicExtensionFunction = getExtensionFunction(dynamicHeader);
689
+ if (dynamicExtensionFunction) {
690
+ try {
691
+ const updatedListData: SolidTreeUiEventResponse = await dynamicExtensionFunction(event);
692
+ if (updatedListData && updatedListData?.filterApplied && updatedListData?.newFilter) {
693
+ queryData = updatedListData?.newFilter;
694
+ }
695
+ } catch (err) {
696
+ console.error("Error executing onBeforeTreeDataLoad extension:", err);
697
+ }
698
+ }
699
+ }
700
+
701
+
702
+
703
+ const queryString = qs.stringify(queryData, { encodeValuesOnly: true });
704
+
705
+
706
+
707
+
708
+ const response = await triggerGetSolidEntities(queryString).unwrap();
709
+ return { queryData, queryString, response };
710
+ };
711
+
712
+ // ─── Node builders ────────────────────────────────────────────────────────
713
+
714
+ const buildGroupNodes = (
715
+ groupMetaRows: any[],
716
+ ruleIndex: number,
717
+ parentPath: GroupPathItem[],
718
+ parentKey: string
719
+ ): TreeNode[] => {
720
+ const rule = activeGroupingRules[ruleIndex];
721
+ if (!rule?.fieldName) return [];
722
+
723
+ const fieldName = String(rule.fieldName);
724
+ const filterField = getResolvedGroupField(fieldName);
725
+ // const dateGranularity = dateTimeImplicitFilter(rule);
726
+ const dateGranularity = getDateGranularity(rule);
727
+ return (groupMetaRows || []).map((groupMeta, index) => {
728
+ const groupLabel = groupMeta?.groupName ?? "(empty)";
729
+ const groupValue = groupMeta?.groupValue ?? "(empty)";
730
+ const idCount = extractGroupCount(groupMeta);
731
+
732
+ const groupPath: GroupPathItem[] = [
733
+ ...parentPath,
734
+ { ruleIndex, fieldName, filterField, value: groupValue, dateGranularity },
735
+ ];
736
+
737
+ return {
738
+ key: `${parentKey}-g-${ruleIndex}-${index}`,
739
+ data: {
740
+ __treeMeta: {
741
+ nodeType: "group",
742
+ ruleIndex,
743
+ groupLabel,
744
+ idCount,
745
+ aggregates: groupMeta,
746
+ groupPath,
747
+ },
748
+ } as TreeRowData,
749
+ children: [],
750
+ leaf: false,
751
+ };
752
+ });
753
+ };
754
+
755
+ const buildRecordNodes = (
756
+ records: any[],
757
+ parentKey: string,
758
+ groupPath: GroupPathItem[]
759
+ ): TreeNode[] => {
760
+ return (records || []).map((record: any, index: number) => {
761
+ const normalizedRecord = normalizeRecord(record);
762
+ return {
763
+ key: `${parentKey}-r-${record?.id ?? index}`,
764
+ data: {
765
+ ...normalizedRecord,
766
+ __treeMeta: {
767
+ nodeType: "record",
768
+ ruleIndex: groupPath.length,
769
+ groupPath,
770
+ },
771
+ } as TreeRowData,
772
+ leaf: true,
773
+ };
774
+ });
775
+ };
776
+
777
+ const updateNodeChildren = (
778
+ nodes: TreeNode[],
779
+ targetKey: string | number,
780
+ children: TreeNode[]
781
+ ): TreeNode[] => {
782
+ return nodes.map((node) => {
783
+ if (node.key === targetKey) {
784
+ return { ...node, children, leaf: children.length === 0 };
785
+ }
786
+ if (node.children && node.children.length > 0) {
787
+ return { ...node, children: updateNodeChildren(node.children, targetKey, children) };
788
+ }
789
+ return node;
790
+ });
791
+ };
792
+
793
+ // ─── Root group load / paginate ───────────────────────────────────────────
794
+
795
+ const loadRootGroups = async (offset = 0) => {
796
+ if (!solidTreeViewMetaData || activeGroupingRules.length === 0) {
797
+ setTreeNodes([]);
798
+ setExpandedKeys({});
799
+
800
+ const queryObject = getFilterObjectFromLocalStorage();
801
+ if (queryObject) {
802
+ delete queryObject.grouping_rules;
803
+ delete queryObject.aggregation_rules;
804
+ setFilterObjectToLocalStorage(queryObject);
805
+ }
806
+
807
+ return;
808
+ }
809
+
810
+ const limit = globalLimit || DEFAULT_PAGE_SIZE;
811
+ setTreeLoading(true);
812
+ try {
813
+ const { response } = await runGroupedQuery(0, [], offset, limit);
814
+ const rootNodes = buildGroupNodes(response?.groupMeta || [], 0, [], "root");
815
+ setTreeNodes(rootNodes);
816
+ setExpandedKeys({});
817
+ // Collapse expanded keys since data changed
818
+ const total = response?.meta.totalRecords ?? 0;
819
+ setPagination("root", { offset, total: total });
820
+
821
+ if (latestFilterPredicatesRef.current && latestFilterPredicatesRef.current.persistFilter) {
822
+ let queryData: any = {
823
+ offset: offset,
824
+ limit: limit,
825
+ populate: toPopulate,
826
+ populateMedia: toPopulateMedia,
827
+ sortField: sortField,
828
+ sortOrder: sortOrder,
829
+ custom_filter_predicate: latestFilterPredicatesRef.current.custom_filter_predicate || null,
830
+ search_predicate: latestFilterPredicatesRef.current.search_predicate || null,
831
+ saved_filter_predicate: latestFilterPredicatesRef.current.saved_filter_predicate || null,
832
+ predefined_search_predicate: latestFilterPredicatesRef.current.predefined_search_predicate || null,
833
+ grouping_rules: latestFilterPredicatesRef.current.grouping_rules || null,
834
+ aggregation_rules: latestFilterPredicatesRef.current.aggregation_rules || null,
835
+ };
836
+
837
+ setFilterObjectToLocalStorage(queryData);
838
+ }
839
+
840
+ } catch (error: any) {
841
+ setTreeNodes([]);
842
+ toast.current?.show({
843
+ severity: "error",
844
+ summary: "Failed to load tree",
845
+ detail: error?.data?.message || error?.message || "Unable to load grouped data",
846
+ life: 4000,
847
+ });
848
+ } finally {
849
+ setTreeLoading(false);
850
+ }
851
+ };
852
+
853
+ useEffect(() => {
854
+ // Reset root pagination on data dependencies change
855
+ setPaginationMap({});
856
+ if (filters && filterPredicates) {
857
+ loadRootGroups(0);
858
+ }
859
+ // eslint-disable-next-line react-hooks/exhaustive-deps
860
+ }, [solidTreeViewMetaData, params.modelName, activeGroupingRules, aggregationRules, filters, sortField, sortOrder, globalLimit]);
861
+
862
+ // ─── Expand handler ───────────────────────────────────────────────────────
863
+
864
+ const loadNodeChildren = async (node: TreeNode, offset: number) => {
865
+ const meta = (node.data as TreeRowData)?.__treeMeta;
866
+ if (!meta || meta.nodeType !== "group") return;
867
+
868
+ const nodeKey = String(node.key);
869
+ const limit = getPagination(nodeKey).limit || DEFAULT_PAGE_SIZE;
870
+ const nextRuleIndex = meta.ruleIndex + 1;
871
+
872
+ setTreeLoading(true);
873
+ try {
874
+ let children: TreeNode[] = [];
875
+ let total = 0;
876
+
877
+ if (nextRuleIndex < activeGroupingRules.length) {
878
+
879
+ const { response } = await runGroupedQuery(
880
+ nextRuleIndex,
881
+ meta.groupPath || [],
882
+ offset,
883
+ limit
884
+ );
885
+ children = buildGroupNodes(
886
+ response?.groupMeta || [],
887
+ nextRuleIndex,
888
+ meta.groupPath || [],
889
+ nodeKey
890
+ );
891
+ total = response?.meta.totalRecords ?? 0;
892
+ } else {
893
+ const { response } = await runLeafQuery(meta.groupPath || [], offset, limit);
894
+ children = buildRecordNodes(response?.records || [], nodeKey, meta.groupPath || []);
895
+ total = response?.meta.totalRecords ?? 0;
896
+ }
897
+
898
+ setTreeNodes((prev) => updateNodeChildren(prev, nodeKey, children));
899
+ setPagination(nodeKey, { offset, total });
900
+
901
+ // Collapse all immediate children's expanded state so stale
902
+ // sub-trees don't appear open with no data after pagination.
903
+ if (offset !== 0 || children.length > 0) {
904
+ setExpandedKeys((prevKeys: any) => {
905
+ const next = { ...prevKeys };
906
+ Object.keys(next).forEach((k) => {
907
+ if (k !== nodeKey && k.startsWith(`${nodeKey}-`)) {
908
+ delete next[k];
909
+ }
910
+ });
911
+ return next;
912
+ });
913
+
914
+ // Also wipe pagination state for all descendant keys so page
915
+ // counters don't carry over to the newly loaded children.
916
+ setPaginationMap((prevMap) => {
917
+ const next = { ...prevMap };
918
+ Object.keys(next).forEach((k) => {
919
+ if (k !== nodeKey && k.startsWith(`${nodeKey}-`)) {
920
+ delete next[k];
921
+ }
922
+ });
923
+ return next;
924
+ });
925
+ }
926
+ } catch (error: any) {
927
+ toast.current?.show({
928
+ severity: "error",
929
+ summary: "Failed to expand node",
930
+ detail: error?.data?.message || error?.message || "Unable to load children",
931
+ life: 4000,
932
+ });
933
+ setTreeNodes((prev) => updateNodeChildren(prev, nodeKey, []));
934
+ } finally {
935
+ setTreeLoading(false);
936
+ }
937
+ };
938
+
939
+ const handleNodeExpand = async (event: any) => {
940
+ const node: TreeNode | undefined = event?.node;
941
+ if (!node) return;
942
+ const nodeKey = String(node.key);
943
+
944
+ // If already loaded (has children) don't re-fetch, just expand
945
+ const alreadyLoaded = node.children && node.children.length > 0;
946
+
947
+ if (!alreadyLoaded) {
948
+ await loadNodeChildren(node, 0);
949
+ }
950
+
951
+
952
+ // If this node is checked, propagate selection to its freshly loaded children.
953
+ const isChecked = selectedNodeKeys?.[nodeKey]?.checked === true;
954
+ if (!isChecked) return;
955
+
956
+ // Use setTreeNodes callback to read the latest tree state after loadNodeChildren
957
+ // has updated it, then select all immediate children.
958
+ setTreeNodes((currentNodes) => {
959
+ const parentNode = findNodeByKey(currentNodes, nodeKey);
960
+ if (!parentNode?.children?.length) return currentNodes;
961
+
962
+ setSelectedNodeKeys((prevKeys: any) => {
963
+ const next = { ...prevKeys };
964
+ parentNode.children!.forEach((child) => {
965
+ next[String(child.key)] = {
966
+ checked: true,
967
+ partialChecked: false,
968
+ nodeType: child.data?.__treeMeta?.nodeType, // already on the node
969
+ };
970
+ });
971
+ return next;
972
+ });
973
+
974
+
975
+ return currentNodes; // tree shape unchanged, we only need the read
976
+ });
977
+
978
+ };
979
+
980
+ // ─── Pagination click handlers ────────────────────────────────────────────
981
+
982
+ /**
983
+ * Navigate root-level groups to next/prev page.
984
+ */
985
+ const handleRootPageChange = async (direction: "prev" | "next") => {
986
+ const { offset, limit } = getPagination("root");
987
+ const nextOffset = direction === "prev"
988
+ ? Math.max(0, offset - limit)
989
+ : offset + limit;
990
+ await loadRootGroups(nextOffset);
991
+ };
992
+
993
+ /**
994
+ * Navigate a node's children to next/prev page.
995
+ * We need to find the node in the tree by key to call loadNodeChildren.
996
+ */
997
+ const findNodeByKey = (nodes: TreeNode[], key: string): TreeNode | null => {
998
+ for (const node of nodes) {
999
+ if (node.key === key) return node;
1000
+ if (node.children) {
1001
+ const found = findNodeByKey(node.children, key);
1002
+ if (found) return found;
1003
+ }
1004
+ }
1005
+ return null;
1006
+ };
1007
+
1008
+ const handleNodePageChange = async (nodeKey: string, direction: "prev" | "next") => {
1009
+ const { offset, limit } = getPagination(nodeKey);
1010
+ const nextOffset = direction === "prev"
1011
+ ? Math.max(0, offset - limit)
1012
+ : offset + limit;
1013
+
1014
+ const node = findNodeByKey(treeNodes, nodeKey);
1015
+ if (!node) return;
1016
+
1017
+ await loadNodeChildren(node, nextOffset);
1018
+ };
1019
+
1020
+ // ─── Filter handler ───────────────────────────────────────────────────────
1021
+
1022
+ const handleApplyCustomFilter = (nextFilterPredicates: any, persistFilter = false) => {
1023
+ const queryfilter = structuredClone(params.customFilter) || { $and: [] };
1024
+
1025
+ if (nextFilterPredicates?.custom_filter_predicate) queryfilter.$and.push(nextFilterPredicates.custom_filter_predicate);
1026
+ if (nextFilterPredicates?.search_predicate) queryfilter.$and.push(nextFilterPredicates.search_predicate);
1027
+ if (nextFilterPredicates?.saved_filter_predicate) queryfilter.$and.push(nextFilterPredicates.saved_filter_predicate);
1028
+ if (nextFilterPredicates?.predefined_search_predicate) queryfilter.$and.push(nextFilterPredicates.predefined_search_predicate);
1029
+
1030
+ latestFiltersRef.current = queryfilter;
1031
+
1032
+ const updatedFilterPredicates = structuredClone(nextFilterPredicates || {});
1033
+ updatedFilterPredicates.persistFilter = persistFilter;
1034
+ latestFilterPredicatesRef.current = updatedFilterPredicates;
1035
+
1036
+ setFilters(queryfilter);
1037
+ setFilterPredicates(updatedFilterPredicates);
1038
+
1039
+ const grouping_rules = updatedFilterPredicates.grouping_rules;
1040
+ const aggregation_rules = updatedFilterPredicates.aggregation_rules;
1041
+
1042
+ setGroupingRules(Array.isArray(grouping_rules) ? grouping_rules : []);
1043
+ setAggregationRules(Array.isArray(aggregation_rules) ? aggregation_rules : []);
1044
+ };
1045
+
1046
+ // ─── Bulk delete ──────────────────────────────────────────────────────────
1047
+
1048
+
1049
+ // handle bulk deletion
1050
+ const deleteBulk = () => {
1051
+ let deleteList: any = [];
1052
+ selectedRecords.forEach((element: any) => {
1053
+ deleteList.push(element.id);
1054
+ });
1055
+ deleteManySolidEntities(deleteList)
1056
+ .unwrap()
1057
+ .then(() => {
1058
+ toast.current?.show({
1059
+ severity: 'success',
1060
+ summary: 'Deleted',
1061
+ detail: ERROR_MESSAGES.RECORD_DELETE,
1062
+ life: 3000
1063
+ });
1064
+ setDeleteRecordsDialogVisible(false);
1065
+ })
1066
+ .catch((error) => {
1067
+ toast.current?.show({
1068
+ severity: 'error',
1069
+ summary: 'Delete Failed',
1070
+ detail: error?.data?.message,
1071
+ life: 4000
1072
+ });
1073
+ });
1074
+ };
1075
+
1076
+ // handle closing of the delete dialog...
1077
+ const onDeleteClose = () => {
1078
+ setDeleteRecordsDialogVisible(false);
1079
+ setSelectedRecords([]);
1080
+ setSelectedRecoverRecords([]);
1081
+ };
1082
+
1083
+
1084
+ const recoverAll = () => {
1085
+ let recoverList: any = [];
1086
+ selectedRecoverRecords.forEach((element: any) => {
1087
+ recoverList.push(element.id);
1088
+ });
1089
+ triggerRecoverSolidEntities(recoverList);
1090
+ setRecoverDialogVisible(false);
1091
+ };
1092
+
1093
+
1094
+ const handleFetchUpdatedRecords = () => {
1095
+ // setQueryString();
1096
+ };
1097
+
1098
+ // ─── Column rendering ─────────────────────────────────────────────────────
1099
+
1100
+ const renderColumnsDynamically = () => {
1101
+ if (!solidTreeViewMetaData?.data || !solidTreeViewLayout) return null;
1102
+
1103
+ const solidFieldsMetadata = solidTreeViewMetaData.data.solidFieldsMetadata;
1104
+ if (!solidFieldsMetadata) return null;
1105
+
1106
+ return solidTreeViewLayout.children?.map((column: any) => {
1107
+ const fieldMetadata = solidFieldsMetadata[column.attrs.name];
1108
+ if (!fieldMetadata) return null;
1109
+
1110
+ const visibleToRole = column?.attrs?.roles || [];
1111
+ if (visibleToRole.length > 0 && !hasAnyRole(user?.roles, visibleToRole)) return null;
1112
+
1113
+ const listColumn = SolidListViewColumn({
1114
+ solidListViewMetaData: solidTreeViewMetaData,
1115
+ fieldMetadata,
1116
+ column,
1117
+ setLightboxUrls: () => { },
1118
+ setOpenLightbox: () => { },
1119
+ });
1120
+
1121
+ if (!React.isValidElement(listColumn)) return null;
1122
+
1123
+ const originalBody = (listColumn as any)?.props?.body;
1124
+ const originalProps = (listColumn as any)?.props || {};
1125
+ const mergedColumnStyle = {
1126
+ minWidth: "12rem",
1127
+ ...(originalProps.style || {}),
1128
+ };
1129
+
1130
+ return (
1131
+ <Column
1132
+ key={`tree-col-${fieldMetadata.name}`}
1133
+ field={originalProps.field ?? fieldMetadata.name}
1134
+ header={originalProps.header}
1135
+ // sortable
1136
+ style={mergedColumnStyle}
1137
+ className={originalProps.className}
1138
+ headerClassName={originalProps.headerClassName}
1139
+ bodyClassName={originalProps.bodyClassName}
1140
+ align={originalProps.align}
1141
+ alignHeader={originalProps.alignHeader}
1142
+ body={(node: any, options: any) => {
1143
+ const rowData = node?.data ?? node;
1144
+ const nodeMeta = rowData?.__treeMeta;
1145
+
1146
+ if (nodeMeta?.nodeType === "group") return <span>&nbsp;</span>;
1147
+
1148
+ if (typeof originalBody === "function") return originalBody(rowData, options);
1149
+
1150
+ return rowData?.[fieldMetadata.name] ?? <span>&nbsp;</span>;
1151
+ }}
1152
+ />
1153
+ );
1154
+ });
1155
+ };
1156
+
1157
+ const renderAggregateColumns = () => {
1158
+ if (activeGroupingRules.length === 0) return null;
1159
+
1160
+ const derivedAggregates = buildAggregates();
1161
+ // derivedAggregates is an array like ["id:count", "price:sum"]
1162
+ // We want to render columns for each of these.
1163
+
1164
+ return derivedAggregates.map((agg) => {
1165
+ const [field, operator] = agg.split(":");
1166
+ const responseKey = `${field}_${operator}`;
1167
+ const header = formatHeader(responseKey);
1168
+
1169
+ return (
1170
+ <Column
1171
+ key={`agg-col-${agg}`}
1172
+ field={responseKey}
1173
+ header={header}
1174
+ sortable
1175
+ style={{ minWidth: "8rem" }}
1176
+ body={(node: any) => {
1177
+ const rowData = node?.data ?? node;
1178
+ const nodeMeta = rowData?.__treeMeta;
1179
+
1180
+ if (nodeMeta?.nodeType !== "group") return <span>&nbsp;</span>;
1181
+
1182
+ const value = nodeMeta?.aggregates?.[responseKey];
1183
+ return <span>{value ?? 0}</span>;
1184
+ }}
1185
+ />
1186
+ );
1187
+ });
1188
+ };
1189
+
1190
+ // ─── Group column body: label + child pagination controls ─────────────────
1191
+
1192
+ const groupColumnBody = (node: TreeNode) => {
1193
+ const rowData = node?.data as TreeRowData;
1194
+ const nodeMeta = rowData?.__treeMeta;
1195
+
1196
+ if (nodeMeta?.nodeType !== "group") return <span>&nbsp;</span>;
1197
+
1198
+ const label = nodeMeta.groupLabel ?? "";
1199
+ const truncateAfter = 30;
1200
+ return (
1201
+ <div className="flex align-items-center">
1202
+ <div
1203
+ className="solid-table-row"
1204
+ style={{ maxWidth: `${truncateAfter}ch` }}
1205
+ // title={truncateAfter ? displayValue : undefined}
1206
+ >
1207
+ <span className="font-semibold">{label}</span>
1208
+ </div>
1209
+ {truncateAfter && label.length > truncateAfter &&
1210
+ <>
1211
+ <Tooltip target=".solid-field-tooltip-icon" />
1212
+ <i className="pi pi-info-circle solid-field-tooltip-icon"
1213
+ data-pr-tooltip={label}
1214
+ />
1215
+ </>
1216
+ }
1217
+ </div>
1218
+ );
1219
+ };
1220
+
1221
+ // ─── Root pagination bar ──────────────────────────────────────────────────
1222
+
1223
+ const RootPaginationBar = () => {
1224
+ if (activeGroupingRules.length === 0) return null;
1225
+
1226
+ const { offset, total } = getPagination("root");
1227
+ const currentPage = Math.floor(offset / globalLimit) + 1;
1228
+ const rootHasPrev = hasPrev("root");
1229
+ const rootHasNext = hasNext("root");
1230
+
1231
+ if (!rootHasPrev && !rootHasNext) return null;
1232
+
1233
+ return (
1234
+ <div style={{ width: "100%", display: "flex", alignItems: "center", justifyContent: "space-between", borderTop: "1px solid var(--surface-border)" }}>
1235
+ <div style={{ display: "flex", alignItems: "center", gap: "0.5rem", padding: "0.5rem 0.75rem" }}>
1236
+ <span className="text-sm text-color-secondary">Items per page</span>
1237
+ <Dropdown
1238
+ value={globalLimit}
1239
+ options={pageSizeOptions}
1240
+ onChange={(e) => {
1241
+ setGlobalLimit(e.value);
1242
+ }}
1243
+ className="solid-page-size-dropdown"
1244
+ style={{ height: '2rem', display: 'flex', alignItems: 'center' }}
1245
+ />
1246
+ </div>
1247
+ <div style={{ display: "flex", alignItems: "center", gap: "0.5rem", padding: "0.5rem 0.75rem" }}>
1248
+ <span className="text-sm text-color-secondary">{offset + 1}–{Math.min(offset + globalLimit, total)} of {total}</span>
1249
+ <Button
1250
+ type="button"
1251
+ icon="pi pi-angle-left"
1252
+ size="small"
1253
+ outlined
1254
+ severity="secondary"
1255
+ disabled={!rootHasPrev || treeLoading}
1256
+ onClick={() => handleRootPageChange("prev")}
1257
+ style={{ padding: 0, border: "none", width: "2rem" }}
1258
+ className="small-button"
1259
+ />
1260
+ <Button
1261
+ type="button"
1262
+ icon="pi pi-angle-right"
1263
+ iconPos="right"
1264
+ size="small"
1265
+ outlined
1266
+ severity="secondary"
1267
+ disabled={!rootHasNext || treeLoading}
1268
+ onClick={() => handleRootPageChange("next")}
1269
+ style={{ padding: 0, border: "none", width: "2rem" }}
1270
+ className="small-button"
1271
+ />
1272
+ </div>
1273
+ </div>
1274
+ );
1275
+ };
1276
+
1277
+ // ─── Imperative handle ────────────────────────────────────────────────────
1278
+
1279
+ useImperativeHandle(ref, () => ({
1280
+ refresh: () => { void loadRootGroups(getPagination("root").offset); },
1281
+ clearFilters: () => {
1282
+ setFilters(params.customFilter || { $and: [] });
1283
+ solidGlobalSearchElementRef.current?.clearFilter?.();
1284
+ },
1285
+ applyFilter: (filter) => { handleApplyCustomFilter(filter); },
1286
+ setPagination: () => { /* pagination wired via paginationMap */ },
1287
+ setSort: (nextSortField: string, nextSortOrder: 1 | -1 | 0) => {
1288
+ setSortField(nextSortField);
1289
+ setSortOrder(nextSortOrder);
1290
+ },
1291
+ setShowArchived: () => { /* archived toggle for grouped tree will be wired later */ },
1292
+ getState: () => ({
1293
+ first: getPagination("root").offset,
1294
+ rows: getPagination("root").limit,
1295
+ sortField,
1296
+ sortOrder: sortOrder as any,
1297
+ showArchived: false,
1298
+ filters,
1299
+ filterPredicates,
1300
+ listData: treeNodes,
1301
+ totalRecords: getPagination("root").total,
1302
+ loading: treeLoading,
1303
+ }),
1304
+ }), [filters, filterPredicates, params.customFilter, treeLoading, treeNodes, paginationMap]);
1305
+
1306
+ // ─── Render ───────────────────────────────────────────────────────────────
1307
+
1308
+ return (
1309
+ <div className="page-parent-wrapper">
1310
+ <Toast ref={toast} />
1311
+
1312
+ {/* ── Header ── */}
1313
+ <div className="page-header flex-column lg:flex-row">
1314
+ <div className="flex justify-content-between w-full">
1315
+ <div className="flex align-items-center solid-header-buttons-wrapper">
1316
+ {params.embeded !== true && (
1317
+ <div className="apps-icon block md:hidden cursor-pointer" onClick={toggleBothSidebars}>
1318
+ <i className="pi pi-th-large"></i>
1319
+ </div>
1320
+ )}
1321
+
1322
+ <p className="m-0 view-title solid-text-wrapper">{treeViewTitle}</p>
1323
+
1324
+ {solidTreeViewMetaData?.data?.solidView?.layout?.attrs.enableGlobalSearch === true && (
1325
+ <div className="hidden lg:flex">
1326
+ <SolidGlobalSearchElement
1327
+ viewType="tree"
1328
+ showSaveFilterPopup={showSaveFilterPopup}
1329
+ setShowSaveFilterPopup={setShowSaveFilterPopup}
1330
+ ref={solidGlobalSearchElementRef}
1331
+ viewData={solidTreeViewMetaData}
1332
+ handleApplyCustomFilter={handleApplyCustomFilter}
1333
+ />
1334
+ </div>
1335
+ )}
1336
+ </div>
1337
+
1338
+ <div className="flex align-items-center solid-header-buttons-wrapper">
1339
+ {solidTreeViewMetaData?.data?.solidView?.layout?.attrs.enableGlobalSearch === true && (
1340
+ <div className="flex lg:hidden">
1341
+ <Button
1342
+ type="button"
1343
+ size="small"
1344
+ icon="pi pi-search"
1345
+ severity="secondary"
1346
+ outlined
1347
+ className="solid-icon-button"
1348
+ onClick={() => setShowGlobalSearchElement(!showGlobalSearchElement)}
1349
+ />
1350
+ </div>
1351
+ )}
1352
+
1353
+ {actionsAllowed.includes(`${permissionExpression(params.modelName, "create")}`) &&
1354
+ solidTreeViewMetaData?.data?.solidView?.layout?.attrs.create !== false && (
1355
+ <SolidCreateButton
1356
+ createButtonUrl={createButtonUrl}
1357
+ createActionQueryParams={createActionQueryParams}
1358
+ responsiveIconOnly={true}
1359
+ />
1360
+ )}
1361
+
1362
+ {actionsAllowed.includes(`${permissionExpression(params.modelName, "delete")}`) &&
1363
+ solidTreeViewMetaData?.data?.solidView?.layout?.attrs.delete !== false &&
1364
+ selectedRecords.length > 0 && (
1365
+ <Button
1366
+ type="button"
1367
+ label="Delete"
1368
+ size="small"
1369
+ onClick={() => setDeleteRecordsDialogVisible(true)}
1370
+ className="small-button"
1371
+ severity="danger"
1372
+ />
1373
+ )}
1374
+
1375
+ <Button
1376
+ type="button"
1377
+ size="small"
1378
+ icon="pi pi-refresh"
1379
+ severity="secondary"
1380
+ className="solid-icon-button"
1381
+ outlined
1382
+ onClick={() => { void loadRootGroups(getPagination("root").offset); }}
1383
+ />
1384
+ {showArchived && (
1385
+ <Button
1386
+ type="button"
1387
+ icon="pi pi-refresh"
1388
+ label="Recover"
1389
+ size="small"
1390
+ severity="secondary"
1391
+ className="hidden lg:flex solid-icon-button "
1392
+ onClick={() => setRecoverDialogVisible(true)}
1393
+ ></Button>
1394
+ )}
1395
+
1396
+ {params.embeded === false &&
1397
+ solidTreeViewLayout?.attrs?.configureView !== false && (
1398
+ <SolidListViewConfigure
1399
+ listViewMetaData={solidTreeViewMetaData}
1400
+ solidListViewLayout={solidTreeViewLayout}
1401
+ setShowArchived={setShowArchived}
1402
+ showArchived={showArchived}
1403
+ viewData={solidTreeViewMetaData}
1404
+ sizeOptions={sizeOptions}
1405
+ setSize={setSize}
1406
+ size={size}
1407
+ viewModes={viewModes}
1408
+ params={params}
1409
+ actionsAllowed={actionsAllowed}
1410
+ selectedRecords={selectedRecords}
1411
+ setDialogVisible={setDeleteRecordsDialogVisible}
1412
+ setShowSaveFilterPopup={setShowSaveFilterPopup}
1413
+ filters={filters}
1414
+ handleFetchUpdatedRecords={handleFetchUpdatedRecords}
1415
+ setRecoverDialogVisible={setRecoverDialogVisible}
1416
+ />
1417
+ )}
1418
+ </div>
1419
+ </div>
1420
+
1421
+ {solidTreeViewMetaData?.data?.solidView?.layout?.attrs.enableGlobalSearch === true &&
1422
+ showGlobalSearchElement && (
1423
+ <div className="flex lg:hidden">
1424
+ <SolidGlobalSearchElement
1425
+ viewType="tree"
1426
+ showSaveFilterPopup={showSaveFilterPopup}
1427
+ setShowSaveFilterPopup={setShowSaveFilterPopup}
1428
+ ref={solidGlobalSearchElementRef}
1429
+ viewData={solidTreeViewMetaData}
1430
+ handleApplyCustomFilter={handleApplyCustomFilter}
1431
+ />
1432
+ </div>
1433
+ )}
1434
+ </div>
1435
+
1436
+ <style>{`
1437
+
1438
+ `}</style>
1439
+
1440
+ {/* ── Tree table ── */}
1441
+ <div className="solid-datatable-wrapper solid-treetable-wrapper flex-1 min-h-0 overflow-auto">
1442
+ {activeGroupingRules.length === 0 ? (
1443
+ <div className="flex flex-column align-items-center justify-content-center h-full p-6 text-center">
1444
+ <div className="mb-4" style={{ opacity: 0.1 }}>
1445
+ <HomePageModuleSvg />
1446
+ </div>
1447
+ <h3 className="m-0 mb-2" style={{ color: "var(--solid-dark-title)", fontWeight: 700, fontSize: '1.5rem' }}>
1448
+ Tree View
1449
+ </h3>
1450
+ <p className="m-0 text-sl" style={{ maxWidth: '35rem', lineHeight: '1.5', color: 'var(--text-color)' }}>
1451
+ To visualize your data in a hierarchical structure, please apply a <strong>Grouping Rule</strong> from the Global Search bar above.
1452
+ </p>
1453
+ </div>
1454
+
1455
+ ) : (
1456
+ <TreeTable
1457
+ value={treeNodes}
1458
+ lazy
1459
+ loading={treeLoading}
1460
+ expandedKeys={expandedKeys}
1461
+ onToggle={(event: any) => setExpandedKeys(event.value)}
1462
+ onExpand={handleNodeExpand}
1463
+ scrollable
1464
+ tableStyle={{ minWidth: "max-content" }}
1465
+ tableClassName="solid-data-table"
1466
+ resizableColumns
1467
+ columnResizeMode="expand"
1468
+ selectionMode="checkbox"
1469
+ selectionKeys={selectedNodeKeys}
1470
+ sortField={sortField}
1471
+ sortOrder={sortOrder as any}
1472
+ removableSort
1473
+ onSort={(e) => {
1474
+ setSortField(e.sortField);
1475
+ setSortOrder(e.sortOrder as any);
1476
+ }}
1477
+ onSelectionChange={(e) => {
1478
+ const incoming = e.value as Record<string, any>;
1479
+
1480
+ setSelectedNodeKeys((prev: any) => {
1481
+ const next: Record<string, any> = {};
1482
+
1483
+ Object.keys(incoming).forEach((key) => {
1484
+ // Find the node to get its type
1485
+ const node = findNodeByKey(treeNodes, key);
1486
+ next[key] = {
1487
+ ...incoming[key], // checked, partialChecked from PrimeReact
1488
+ nodeType: node?.data?.__treeMeta?.nodeType // add type from tree
1489
+ ?? prev[key]?.nodeType, // fallback to prev if node not found
1490
+ };
1491
+ });
1492
+
1493
+ return next;
1494
+ });
1495
+ }}
1496
+
1497
+ >
1498
+ <Column
1499
+ key="tree-group-column"
1500
+ field="__group"
1501
+ header="Group"
1502
+ sortable
1503
+ expander={(node: any) => node?.data?.__treeMeta?.nodeType === "group"}
1504
+ body={groupColumnBody}
1505
+
1506
+ style={{ minWidth: "18rem", display: "flex", alignItems: "center" }}
1507
+ />
1508
+
1509
+ {renderColumnsDynamically()}
1510
+ {renderAggregateColumns()}
1511
+
1512
+ <Column
1513
+ key="tree-last-frozen-column"
1514
+ header=""
1515
+ style={{ width: "20rem" }}
1516
+ body={(node: any) => {
1517
+ const rowData = node?.data as TreeRowData;
1518
+ const nodeMeta = rowData?.__treeMeta;
1519
+ if (nodeMeta?.nodeType !== "group") return <span>&nbsp;</span>;
1520
+
1521
+ const nodeKey = String(node.key);
1522
+ const isExpanded = expandedKeys[nodeKey];
1523
+ const childrenLoaded = isExpanded && node.children && node.children.length > 0;
1524
+ if (!childrenLoaded) return <span>&nbsp;</span>;
1525
+
1526
+ const pagEntry = getPagination(nodeKey);
1527
+ const canPrev = hasPrev(nodeKey);
1528
+ const canNext = hasNext(nodeKey);
1529
+
1530
+ // "in Jharkhand" — this node's own group label
1531
+ const inLabel = nodeMeta.groupLabel ?? "";
1532
+
1533
+ // "of cities" — what the children represent
1534
+ // nextRuleIndex = nodeMeta.ruleIndex + 1
1535
+ const nextRuleIndex = nodeMeta.ruleIndex + 1;
1536
+ const isLeafLevel = nextRuleIndex >= activeGroupingRules.length;
1537
+ const ofLabel = isLeafLevel
1538
+ ? solidTreeViewMetaData?.data?.solidView?.model?.displayName // leaf → model name
1539
+ : (() => {
1540
+ const nextRule = activeGroupingRules[nextRuleIndex];
1541
+ const fieldName = String(nextRule?.fieldName ?? "");
1542
+ const fieldMeta = getFieldMetadata(fieldName);
1543
+ // Use displayName if available, fallback to fieldName
1544
+ return fieldMeta?.displayName ?? fieldMeta?.name ?? fieldName;
1545
+ })();
1546
+
1547
+ return (
1548
+ <div
1549
+ style={{ display: "flex", alignItems: "center", gap: "0.2rem", justifyContent: "flex-end" }}
1550
+ onClick={(e) => e.stopPropagation()}
1551
+ >
1552
+ {/* <span style={{ fontSize: "0.9rem", color: "var(--text-color-secondary)", whiteSpace: "nowrap" }}>
1553
+ {pagEntry.offset + 1}–{Math.min(pagEntry.offset + pagEntry.limit, pagEntry.total)} of {pagEntry.total} {getSingularAndPlural(ofLabel).toPlural ?? ofLabel} in {inLabel}
1554
+ </span> */}
1555
+ <Button
1556
+ type="button"
1557
+ icon="pi pi-angle-left"
1558
+ size="small"
1559
+ rounded
1560
+ outlined
1561
+ disabled={!canPrev || treeLoading}
1562
+ style={{ padding: 0, border: "none", width: "2rem" }}
1563
+ className="small-button"
1564
+ onClick={() => handleNodePageChange(nodeKey, "prev")}
1565
+ />
1566
+ <Button
1567
+ type="button"
1568
+ icon="pi pi-angle-right"
1569
+ size="small"
1570
+ rounded
1571
+ outlined
1572
+ disabled={!canNext || treeLoading}
1573
+ style={{ padding: 0, border: "none", width: "2rem" }}
1574
+ className="small-button"
1575
+ onClick={() => handleNodePageChange(nodeKey, "next")}
1576
+ />
1577
+ </div>
1578
+ );
1579
+ }}
1580
+ />
1581
+ </TreeTable>
1582
+ )}
1583
+ </div>
1584
+
1585
+ {/* ── Root-level pagination bar ── */}
1586
+ <RootPaginationBar />
1587
+
1588
+ {/* ── Delete dialog ── */}
1589
+ <Dialog
1590
+ visible={isDeleteRecordsDialogVisible}
1591
+ header="Confirm Delete"
1592
+ onHide={() => setDeleteRecordsDialogVisible(false)}
1593
+ headerClassName="py-2"
1594
+ contentClassName="px-0 pb-0"
1595
+ // style={{ width: '20vw' }}
1596
+ breakpoints={{ '1199px': '30rem', '550px': '85vw' }}
1597
+ >
1598
+ <Divider className="m-0" />
1599
+ <div className="p-4">
1600
+ <p className="m-0 solid-primary-title" style={{ fontSize: 16 }}>Are you sure you want to delete the selected records?</p>
1601
+ <div className="flex align-items-center gap-2 mt-3">
1602
+ <Button label="Delete" severity="danger" size="small" autoFocus onClick={deleteBulk} />
1603
+ <Button label="Cancel" size="small" onClick={onDeleteClose} outlined className='bg-primary-reverse' />
1604
+ </div>
1605
+ </div>
1606
+ </Dialog>
1607
+ <Dialog
1608
+ visible={isRecoverDialogVisible}
1609
+ header="Confirm Recover"
1610
+ modal
1611
+ className="solid-confirm-dialog"
1612
+ footer={() => (
1613
+ <div className="flex justify-content-center">
1614
+ <Button
1615
+ label="Yes"
1616
+ icon="pi pi-check"
1617
+ severity="danger"
1618
+ autoFocus
1619
+ onClick={recoverAll}
1620
+ />
1621
+ <Button
1622
+ label="No"
1623
+ icon="pi pi-times"
1624
+ onClick={() => setRecoverDialogVisible(false)}
1625
+ />
1626
+ </div>
1627
+ )}
1628
+ onHide={() => setRecoverDialogVisible(false)}
1629
+ >
1630
+ <p>Are you sure you want to recover all records?</p>
1631
+ </Dialog>
1632
+
1633
+ </div>
1634
+ );
1635
+ });
1636
+
1637
+ SolidTreeView.displayName = "SolidTreeView";