@solidxai/core-ui 0.1.2 → 0.1.4-beta.0

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