@ramesesinc/platform-core 0.1.6 → 0.1.9

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 (90) hide show
  1. package/dist/components/action/LookupPage.js +9 -31
  2. package/dist/components/action/ViewPage.d.ts +2 -0
  3. package/dist/components/action/ViewPage.js +25 -31
  4. package/dist/components/common/UIComponent.js +4 -3
  5. package/dist/components/index.d.ts +3 -0
  6. package/dist/components/index.js +1 -0
  7. package/dist/components/table/DataList.js +2 -2
  8. package/dist/components/view/PopupView.d.ts +13 -0
  9. package/dist/components/view/PopupView.js +25 -20
  10. package/dist/core/DataContext.d.ts +7 -4
  11. package/dist/core/DataContext.js +16 -4
  12. package/dist/core/Page.js +25 -26
  13. package/dist/core/PageCache.js +7 -7
  14. package/dist/core/PageContext.js +17 -7
  15. package/dist/core/PageViewContext.d.ts +13 -1
  16. package/dist/core/PageViewContext.js +75 -2
  17. package/dist/core/PopupContext.d.ts +49 -0
  18. package/dist/core/PopupContext.js +380 -0
  19. package/dist/core/RowContext.js +1 -1
  20. package/dist/core/WindowContext.d.ts +15 -0
  21. package/dist/core/WindowContext.js +28 -0
  22. package/dist/core/index.d.ts +17 -0
  23. package/dist/core/index.js +1 -0
  24. package/dist/index.css +25 -7
  25. package/dist/lib/utils/BeanUtils.js +7 -7
  26. package/dist/templates/DataListTemplate.js +7 -2
  27. package/dist/templates/ExplorerTemplate.js +1 -1
  28. package/package.json +5 -5
  29. package/dist/components/action/AlertMessage.tsx +0 -38
  30. package/dist/components/action/Button.tsx +0 -230
  31. package/dist/components/action/CancelEdit.tsx +0 -40
  32. package/dist/components/action/DeleteData.tsx +0 -73
  33. package/dist/components/action/Edit.tsx +0 -40
  34. package/dist/components/action/LookupPage.tsx +0 -113
  35. package/dist/components/action/ProcessRunner.tsx +0 -337
  36. package/dist/components/action/Refresh.tsx +0 -35
  37. package/dist/components/action/SaveData.tsx +0 -74
  38. package/dist/components/action/SelectData.tsx +0 -47
  39. package/dist/components/action/Undo.tsx +0 -50
  40. package/dist/components/action/UpdateData.tsx +0 -49
  41. package/dist/components/action/UpdateState.tsx +0 -40
  42. package/dist/components/action/ViewBackPage.tsx +0 -46
  43. package/dist/components/action/ViewPage.tsx +0 -141
  44. package/dist/components/common/UIComponent.tsx +0 -86
  45. package/dist/components/common/UIInput.tsx +0 -49
  46. package/dist/components/common/UIMenu.tsx +0 -91
  47. package/dist/components/index.ts +0 -51
  48. package/dist/components/input/CodeEditor.tsx +0 -188
  49. package/dist/components/input/DateField.tsx +0 -274
  50. package/dist/components/input/DayPicker.tsx +0 -5
  51. package/dist/components/input/HtmlCode.tsx +0 -203
  52. package/dist/components/input/JsonCode.tsx +0 -205
  53. package/dist/components/input/MonthPicker.tsx +0 -5
  54. package/dist/components/input/ScriptCode.tsx +0 -195
  55. package/dist/components/input/Select.tsx +0 -78
  56. package/dist/components/input/SqlCode.tsx +0 -162
  57. package/dist/components/input/StringDecision.tsx +0 -64
  58. package/dist/components/input/Text.tsx +0 -57
  59. package/dist/components/input/YearPicker.tsx +0 -81
  60. package/dist/components/list/IconMenu.tsx +0 -115
  61. package/dist/components/list/TabMenu.tsx +0 -127
  62. package/dist/components/list/TreeMenu.tsx +0 -279
  63. package/dist/components/list/TxnTaskList.tsx +0 -198
  64. package/dist/components/output/Label.tsx +0 -50
  65. package/dist/components/table/DataList.tsx +0 -820
  66. package/dist/components/table/DataTable.tsx +0 -572
  67. package/dist/components/table/ListHandler.ts +0 -276
  68. package/dist/components/table/TableContext.tsx +0 -122
  69. package/dist/components/view/ComponentView.tsx +0 -102
  70. package/dist/components/view/FilterView.tsx +0 -21
  71. package/dist/components/view/HtmlForm.tsx +0 -176
  72. package/dist/components/view/HtmlView.tsx +0 -98
  73. package/dist/components/view/IFrameView.tsx +0 -48
  74. package/dist/components/view/Modal.tsx +0 -72
  75. package/dist/components/view/PageView.tsx +0 -131
  76. package/dist/components/view/PopupView.tsx +0 -160
  77. package/dist/components/view/RootView.tsx +0 -109
  78. package/dist/components/view/WizardView.tsx +0 -48
  79. package/dist/lib/layouts/BorderLayout.tsx +0 -31
  80. package/dist/lib/layouts/CardLayout.tsx +0 -73
  81. package/dist/lib/layouts/CenterLayout.tsx +0 -20
  82. package/dist/lib/layouts/GridLayout.tsx +0 -20
  83. package/dist/lib/layouts/HPanel.tsx +0 -31
  84. package/dist/lib/layouts/HorizontalLayout.tsx +0 -29
  85. package/dist/lib/layouts/MainLayout.tsx +0 -16
  86. package/dist/lib/layouts/PageLayout.tsx +0 -29
  87. package/dist/lib/layouts/VPanel.tsx +0 -27
  88. package/dist/lib/layouts/XLayout.tsx +0 -29
  89. package/dist/lib/layouts/YLayout.tsx +0 -29
  90. package/dist/lib/layouts/index.ts +0 -13
@@ -1,276 +0,0 @@
1
- /**
2
- * ListHandler - Functional style implementation for managing data fetching,
3
- * pagination, filtering, and sorting for DataList, DataTable, or any list-like components.
4
- */
5
-
6
- import { localAPI } from "@ramesesinc/lib/local-api";
7
-
8
- export interface ListHandlerConfig {
9
- tenant: string;
10
- module: string;
11
- api: string;
12
- params?: Record<string, any>;
13
- cols: string[];
14
- rowsPerPage?: number;
15
- disableTotalCount?: boolean;
16
- useCursorPagination?: boolean;
17
- }
18
-
19
- interface QueryParams {
20
- cols: string[];
21
- start: number;
22
- limit: number;
23
- sort?: {
24
- column: string;
25
- direction: "asc" | "desc";
26
- };
27
- search?: string;
28
- skipCount?: boolean;
29
- cursor?: string;
30
- [key: string]: any;
31
- }
32
-
33
- interface DataResult {
34
- data: any[];
35
- total?: number;
36
- page: number;
37
- totalPages?: number;
38
- hasMore?: boolean;
39
- nextCursor?: string;
40
- }
41
-
42
- export type ListActionHandler = {
43
- getData: () => any[];
44
- load: () => Promise<void>;
45
- reset: () => Promise<void>;
46
- moveNextPage: () => Promise<void>;
47
- movePrevPage: () => Promise<void>;
48
- moveFirstPage: () => Promise<void>;
49
- moveLastPage: () => Promise<void>;
50
- moveToPage: (page: number) => Promise<void>;
51
- setFilter: (filter: Record<string, any>) => Promise<void>;
52
- resetFilter: () => Promise<void>;
53
- doSearch: (text: string) => Promise<void>;
54
- sortAsc: (column: string) => Promise<void>;
55
- sortDesc: (column: string) => Promise<void>;
56
- setRowsPerPage: (rows: number) => Promise<void>;
57
- hasNextPage: () => boolean;
58
- hasPrevPage: () => boolean;
59
- getTotalPageCount: () => number;
60
- getTotalRecordCount: () => number;
61
- getCurrentPage: () => number;
62
- getCurrentSortedColumn: () => string | null;
63
- getCurrentSortDirection: () => "asc" | "desc" | null;
64
- getRowsPerPage: () => number;
65
- getSearchText: () => string;
66
- getCustomFilter: () => Record<string, any>;
67
- };
68
-
69
- // FIX: removed the erroneous second parameter `ListHandlerConfig: any`
70
- export const ListHandler = (config: ListHandlerConfig): ListActionHandler => {
71
- const api = config.api;
72
- const cols = config.cols;
73
-
74
- const { filter = {}, ...rest } = config.params ?? {};
75
-
76
- const disableTotalCount = config.disableTotalCount || false;
77
- const useCursorPagination = config.useCursorPagination || false;
78
-
79
- let rowsPerPage = config.rowsPerPage || 20;
80
- let currentPage = 1;
81
- let customFilter: Record<string, any> = {};
82
- let searchText = "";
83
- let sortColumn: string | null = null;
84
- let sortDirection: "asc" | "desc" | null = null;
85
- let lastCursor: string | null = null;
86
-
87
- let dataResult: any[] = [];
88
- let totalRecordCount = 0;
89
- let totalPageCount = 0;
90
- let hasMorePages = false;
91
-
92
- const getData = (): any[] => dataResult;
93
-
94
- const load = async (): Promise<void> => {
95
- const queryParams: QueryParams = {
96
- cols,
97
- start: (currentPage - 1) * rowsPerPage,
98
- limit: rowsPerPage,
99
- filter: {
100
- ...filter,
101
- ...customFilter,
102
- },
103
- };
104
-
105
- if (useCursorPagination && lastCursor) {
106
- queryParams.cursor = lastCursor;
107
- }
108
-
109
- if (disableTotalCount) {
110
- queryParams.skipCount = true;
111
- }
112
-
113
- if (searchText) {
114
- queryParams.search = searchText;
115
- }
116
-
117
- if (sortColumn && sortDirection) {
118
- queryParams.sort = { column: sortColumn, direction: sortDirection };
119
- }
120
-
121
- try {
122
- // console.log("datalist pass 1", { api, query: { ...queryParams, ...rest } });
123
- const result = await localAPI.exec(`/services/exec/${config.tenant}/${config.module}/${api}`, { ...queryParams, ...rest });
124
- // console.log("datalist pass 2", { result });
125
- dataResult = result.data ?? result ?? [];
126
-
127
- if (useCursorPagination) {
128
- lastCursor = result.nextCursor ?? null;
129
- hasMorePages = result.hasMore ?? false;
130
- }
131
-
132
- if (!disableTotalCount) {
133
- totalRecordCount = result.total || 0;
134
- totalPageCount = result.totalPages || Math.ceil(totalRecordCount / rowsPerPage);
135
- } else {
136
- hasMorePages = result.data && result.data.length === rowsPerPage;
137
- }
138
- } catch (error) {
139
- console.error("Error loading data:", error);
140
- dataResult = [];
141
- totalRecordCount = 0;
142
- totalPageCount = 0;
143
- hasMorePages = false;
144
- throw error;
145
- }
146
- };
147
-
148
- const reset = async (): Promise<void> => {
149
- currentPage = 1;
150
- await load();
151
- };
152
-
153
- const moveNextPage = async (): Promise<void> => {
154
- if (hasNextPage()) {
155
- currentPage++;
156
- await load();
157
- }
158
- };
159
-
160
- const movePrevPage = async (): Promise<void> => {
161
- if (hasPrevPage()) {
162
- currentPage--;
163
- await load();
164
- }
165
- };
166
-
167
- const moveFirstPage = async (): Promise<void> => {
168
- currentPage = 1;
169
- await load();
170
- };
171
-
172
- const moveLastPage = async (): Promise<void> => {
173
- if (disableTotalCount) {
174
- console.warn("moveLastPage is not available when total count is disabled");
175
- return;
176
- }
177
- if (totalPageCount > 0) {
178
- currentPage = totalPageCount;
179
- await load();
180
- }
181
- };
182
-
183
- const moveToPage = async (page: number): Promise<void> => {
184
- if (page >= 1 && page <= totalPageCount) {
185
- currentPage = page;
186
- await load();
187
- } else {
188
- console.warn(`Invalid page number: ${page}. Must be between 1 and ${totalPageCount}`);
189
- }
190
- };
191
-
192
- const setFilter = async (filter: Record<string, any>): Promise<void> => {
193
- customFilter = { ...filter };
194
- currentPage = 1;
195
- await load();
196
- };
197
-
198
- const resetFilter = async (): Promise<void> => {
199
- customFilter = {};
200
- currentPage = 1;
201
- await load();
202
- };
203
-
204
- const doSearch = async (text: string): Promise<void> => {
205
- searchText = text;
206
- currentPage = 1;
207
- await load();
208
- };
209
-
210
- const sortAsc = async (column: string): Promise<void> => {
211
- sortColumn = column;
212
- sortDirection = "asc";
213
- currentPage = 1;
214
- await load();
215
- };
216
-
217
- const sortDesc = async (column: string): Promise<void> => {
218
- sortColumn = column;
219
- sortDirection = "desc";
220
- currentPage = 1;
221
- await load();
222
- };
223
-
224
- const setRowsPerPage = async (rows: number): Promise<void> => {
225
- if (rows > 0) {
226
- rowsPerPage = rows;
227
- currentPage = 1;
228
- await load();
229
- }
230
- };
231
-
232
- const hasNextPage = (): boolean => {
233
- if (disableTotalCount || useCursorPagination) return hasMorePages;
234
- return currentPage < totalPageCount;
235
- };
236
-
237
- const hasPrevPage = (): boolean => currentPage > 1;
238
-
239
- const getTotalPageCount = (): number => totalPageCount;
240
- const getTotalRecordCount = (): number => totalRecordCount;
241
- const getCurrentPage = (): number => currentPage;
242
- const getCurrentSortedColumn = (): string | null => sortColumn;
243
- const getCurrentSortDirection = (): "asc" | "desc" | null => sortDirection;
244
- const getRowsPerPage = (): number => rowsPerPage;
245
- const getSearchText = (): string => searchText;
246
- const getCustomFilter = (): Record<string, any> => ({ ...customFilter });
247
-
248
- return {
249
- getData,
250
- load,
251
- reset,
252
- moveNextPage,
253
- movePrevPage,
254
- moveFirstPage,
255
- moveLastPage,
256
- moveToPage,
257
- setFilter,
258
- resetFilter,
259
- doSearch,
260
- sortAsc,
261
- sortDesc,
262
- setRowsPerPage,
263
- hasNextPage,
264
- hasPrevPage,
265
- getTotalPageCount,
266
- getTotalRecordCount,
267
- getCurrentPage,
268
- getCurrentSortedColumn,
269
- getCurrentSortDirection,
270
- getRowsPerPage,
271
- getSearchText,
272
- getCustomFilter,
273
- };
274
- };
275
-
276
- export default ListHandler;
@@ -1,122 +0,0 @@
1
- import React, { createContext, useContext, useState, useEffect, useRef, ReactNode } from "react";
2
- import { ColumnDefinition } from "./DataTable";
3
- import { ListHandlerConfig, ListActionHandler } from "./ListHandler";
4
-
5
- // ============================================================================
6
- // INTERFACES
7
- // ============================================================================
8
-
9
- export interface DataConfig {
10
- api: string;
11
- params?: Record<string, any>;
12
- }
13
-
14
- export interface TableContextValue {
15
- listHandler: ListActionHandler | null;
16
- columns: ColumnDefinition[];
17
- setColumns: React.Dispatch<React.SetStateAction<ColumnDefinition[]>>;
18
- rows: any[];
19
- setRows: React.Dispatch<React.SetStateAction<any[]>>;
20
- loading: boolean;
21
- setLoading: React.Dispatch<React.SetStateAction<boolean>>;
22
- }
23
-
24
- // ============================================================================
25
- // CONTEXT
26
- // ============================================================================
27
-
28
- const TableContext = createContext<TableContextValue | null>(null);
29
-
30
- export const useTableContext = (): TableContextValue => {
31
- const ctx = useContext(TableContext);
32
- if (!ctx) throw new Error("useTableContext must be used inside <TableProvider>");
33
- return ctx;
34
- };
35
-
36
- // ============================================================================
37
- // PROVIDER PROPS
38
- // ============================================================================
39
-
40
- export interface TableProviderProps {
41
- data: DataConfig;
42
- columns?: ColumnDefinition[];
43
- rowsPerPage?: number;
44
- disableTotalCount?: boolean;
45
- useCursorPagination?: boolean;
46
- children: ReactNode;
47
- listHandlerFactory: (config: ListHandlerConfig) => ListActionHandler;
48
- // Resolved by DataList before being passed here — no hooks needed inside Provider
49
- tenant: string;
50
- module: string;
51
- resolvedParams: Record<string, any> | undefined;
52
- }
53
-
54
- // ============================================================================
55
- // PROVIDER
56
- // ============================================================================
57
-
58
- export const TableProvider: React.FC<TableProviderProps> = ({
59
- data,
60
- columns: initialColumns = [],
61
- rowsPerPage = 20,
62
- disableTotalCount = true,
63
- useCursorPagination = false,
64
- children,
65
- listHandlerFactory,
66
- tenant,
67
- module,
68
- resolvedParams,
69
- }) => {
70
- const colIds = initialColumns.map((c) => c.id ?? c.expr ?? "").filter(Boolean);
71
-
72
- // ListHandler created ONCE via ref.
73
- // resolvedParams already has all placeholders replaced — passed in from DataList.
74
- const listHandlerRef = useRef<ListActionHandler | null>(null);
75
- if (listHandlerRef.current === null) {
76
- listHandlerRef.current = listHandlerFactory({
77
- tenant,
78
- module,
79
- api: data.api,
80
- params: resolvedParams,
81
- cols: colIds,
82
- rowsPerPage,
83
- disableTotalCount,
84
- useCursorPagination,
85
- });
86
- }
87
-
88
- const [columns, setColumns] = useState<ColumnDefinition[]>(initialColumns);
89
- const [rows, setRows] = useState<any[]>([]);
90
- const [loading, setLoading] = useState(false);
91
-
92
- // Initial data load on mount
93
- useEffect(() => {
94
- const lh = listHandlerRef.current;
95
- if (!lh) return;
96
-
97
- setLoading(true);
98
- lh.load()
99
- .then(() => setRows([...lh.getData()]))
100
- .catch((err) => console.error("TableProvider initial load failed:", err))
101
- .finally(() => setLoading(false));
102
- }, []);
103
-
104
- return (
105
- <TableContext.Provider
106
- value={{
107
- listHandler: listHandlerRef.current,
108
- columns,
109
- setColumns,
110
- rows,
111
- setRows,
112
- loading,
113
- setLoading,
114
- }}
115
- >
116
- {children}
117
- </TableContext.Provider>
118
- );
119
- };
120
-
121
- export type { ListHandlerConfig };
122
- export default TableContext;
@@ -1,102 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { useApp } from "../../core/AppContext";
3
- import { DynamicComponent } from "../../core/DynamicComponent";
4
- import { usePageContext } from "../../core/PageContext";
5
- import useDependHandler from "../../core/UIDependHandler";
6
- import UIComponent, { UIComponentProps } from "../common/UIComponent";
7
-
8
- interface ComponentViewProps extends UIComponentProps {
9
- // use for dynamic component rendering
10
- id?: string;
11
-
12
- // use to get value from page context
13
- depends?: string;
14
-
15
- // use to statically set the actual component
16
- component?: React.ReactNode;
17
- }
18
-
19
- const ComponentView = (props: ComponentViewProps) => {
20
- const { id, name, depends, component } = props ?? {};
21
- const [comp, setComp] = useState<React.ReactNode | null>(null);
22
- const [error, setError] = useState("");
23
- const [forceUpdate, setForceUpdate] = useState({});
24
- const { getComponentCache } = useApp();
25
- const pageContext = usePageContext();
26
-
27
- const getPreferredName = () => {
28
- if (name == null || name.trim() === "") {
29
- return depends;
30
- }
31
- return name;
32
- };
33
-
34
- const loadComponentById = async () => {
35
- if (id != null && id.trim() !== "") {
36
- const cache = await getComponentCache(id);
37
- const template: React.ReactNode = cache.template;
38
- setComp(template);
39
- return template;
40
- }
41
- return null;
42
- };
43
-
44
- const loadComponentByName = () => {
45
- const preferredName = getPreferredName();
46
- if (preferredName != null && preferredName.trim() !== "") {
47
- const info = pageContext?.get(preferredName) as { component: string; attr?: Record<string, any> };
48
- const { component, attr = {} } = info ?? {};
49
-
50
- if (component == null || component.trim() === "") {
51
- return null;
52
- }
53
-
54
- return <DynamicComponent config={{ component, attr }} />;
55
- }
56
- return null;
57
- };
58
-
59
- const loadStaticComponent = () => {
60
- if (typeof component === "string") {
61
- return <DynamicComponent config={{ component }} />;
62
- }
63
- return component;
64
- };
65
-
66
- const onRefresh = () => {
67
- setForceUpdate({});
68
- };
69
-
70
- useDependHandler({ name: depends, onRefresh });
71
-
72
- useEffect(() => {
73
- const prefName = name ?? depends ?? "";
74
-
75
- const tmpl = loadStaticComponent();
76
- if (tmpl != null) {
77
- setComp(tmpl);
78
- return;
79
- }
80
-
81
- loadComponentById()
82
- .then((template) => {
83
- if (template != null) {
84
- setComp(template);
85
- return;
86
- }
87
-
88
- const tmpl2 = loadComponentByName();
89
- if (tmpl2 != null) {
90
- setComp(tmpl2);
91
- }
92
- })
93
- .catch((err) => {
94
- console.log("error", err);
95
- setError(err.message);
96
- });
97
- }, [forceUpdate]);
98
-
99
- return <UIComponent {...props}>{error ? <div>{error}</div> : <div>{comp}</div>}</UIComponent>;
100
- };
101
-
102
- export default ComponentView;
@@ -1,21 +0,0 @@
1
- import VPanel from "../../layouts/VPanel";
2
- import { renderListDef } from "../../lib/utils/SectionProvider";
3
-
4
- const FilterView = (props: Record<string, any>) => {
5
- const { items = [] } = props ?? {};
6
-
7
- if (items.length === 0) return null;
8
-
9
- const buildPanel = () => {
10
- const comps = renderListDef(items);
11
- return <VPanel gap={8}>{comps}</VPanel>;
12
- };
13
-
14
- return (
15
- <>
16
- <div>{buildPanel()}</div>
17
- </>
18
- );
19
- };
20
-
21
- export default FilterView;
@@ -1,176 +0,0 @@
1
- import { localAPI } from "@ramesesinc/lib/local-api";
2
- import React, { useEffect, useMemo, useState } from "react";
3
- import { useApp } from "../../core/AppContext";
4
- import { DynamicComponent } from "../../core/DynamicComponent";
5
- import useDependHandler from "../../core/UIDependHandler";
6
- import UIComponent from "../common/UIComponent";
7
-
8
- // ─────────────────────────────────────────────────────────────────────────────
9
- // Types
10
- // ─────────────────────────────────────────────────────────────────────────────
11
-
12
- type HtmlFormProps = {
13
- label?: string;
14
- depends?: string;
15
- templateid?: string;
16
- };
17
-
18
- type SentinelConfig = {
19
- component: string;
20
- attr: any;
21
- };
22
-
23
- // ─────────────────────────────────────────────────────────────────────────────
24
- // Attr Parser
25
- // Handles both strict JSON {"key":"value"} and JS object literals {key: "value"}
26
- // ─────────────────────────────────────────────────────────────────────────────
27
-
28
- function parseAttr(attrStr: string | null): any {
29
- if (!attrStr) return {};
30
- try {
31
- return JSON.parse(attrStr);
32
- } catch {
33
- try {
34
- // eslint-disable-next-line no-new-func
35
- // return new Function(`"use strict"; return (${attrStr})`)();
36
- const normalized = attrStr.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)(\s*:)/g, '$1"$2"$3').replace(/'([^'\\]*(\\.[^'\\]*)*)'/g, '"$1"');
37
- return JSON.parse(normalized);
38
- } catch {
39
- console.warn("HtmlForm: could not parse attr →", attrStr);
40
- return {};
41
- }
42
- }
43
- }
44
-
45
- // ─────────────────────────────────────────────────────────────────────────────
46
- // DOM Attribute → React Prop mapper
47
- // ─────────────────────────────────────────────────────────────────────────────
48
-
49
- const ATTR_MAP: Record<string, string> = {
50
- class: "className",
51
- colspan: "colSpan",
52
- rowspan: "rowSpan",
53
- for: "htmlFor",
54
- tabindex: "tabIndex",
55
- readonly: "readOnly",
56
- maxlength: "maxLength",
57
- cellpadding: "cellPadding",
58
- cellspacing: "cellSpacing",
59
- };
60
-
61
- function styleStringToObject(styleStr: string): Record<string, string> {
62
- const style: Record<string, string> = {};
63
- styleStr.split(";").forEach((rule) => {
64
- const [prop, val] = rule.split(":").map((s) => s.trim());
65
- if (prop && val) {
66
- const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
67
- style[camel] = val;
68
- }
69
- });
70
- return style;
71
- }
72
-
73
- function domAttrsToProps(el: Element): Record<string, any> {
74
- const props: Record<string, any> = {};
75
- for (const attr of Array.from(el.attributes)) {
76
- if (["component", "attr"].includes(attr.name)) continue;
77
- const reactName = ATTR_MAP[attr.name] ?? attr.name;
78
- props[reactName] = attr.name === "style" ? styleStringToObject(attr.value) : attr.value;
79
- }
80
- return props;
81
- }
82
-
83
- // ─────────────────────────────────────────────────────────────────────────────
84
- // Table structural elements that cannot contain whitespace text nodes
85
- // ─────────────────────────────────────────────────────────────────────────────
86
-
87
- const TABLE_ELEMENTS = new Set(["table", "thead", "tbody", "tfoot", "tr", "colgroup"]);
88
-
89
- // ─────────────────────────────────────────────────────────────────────────────
90
- // DOM Walker
91
- // Turns a DOM tree into a React element tree.
92
- // Sentinel <span component="X" attr="..."> becomes <DynamicComponent config={...} />
93
- // Everything else becomes a plain React element.
94
- // ─────────────────────────────────────────────────────────────────────────────
95
-
96
- let _keyIndex = 0;
97
- const nextKey = () => `hf_${_keyIndex++}`;
98
-
99
- function walkNode(node: ChildNode, parentTag?: string): React.ReactNode {
100
- if (node.nodeType === Node.TEXT_NODE) {
101
- // Whitespace-only text nodes are invalid inside table structural elements
102
- // and cause React's validateDOMNesting warning.
103
- if (parentTag && TABLE_ELEMENTS.has(parentTag) && !node.textContent?.trim()) {
104
- return null;
105
- }
106
- return node.textContent || null;
107
- }
108
-
109
- if (node.nodeType === Node.ELEMENT_NODE) {
110
- const el = node as Element;
111
- const tag = el.tagName.toLowerCase();
112
- const componentName = el.getAttribute("component");
113
-
114
- // ── Sentinel span → DynamicComponent ──────────────────────────────────
115
- if (tag === "span" && componentName) {
116
- const attr: any = parseAttr(el.getAttribute("attr"));
117
- const config: SentinelConfig = { component: componentName, attr };
118
- return <DynamicComponent key={nextKey()} config={config} />;
119
- }
120
-
121
- // ── Regular element → recurse ──────────────────────────────────────────
122
- const props = { ...domAttrsToProps(el), key: nextKey() };
123
- const children: React.ReactNode[] = Array.from(el.childNodes)
124
- .map((child) => walkNode(child, tag)) // pass current tag as parentTag
125
- .filter((n) => n !== null && n !== undefined);
126
-
127
- return children.length > 0 ? React.createElement(tag, props, ...children) : React.createElement(tag, props);
128
- }
129
-
130
- return null;
131
- }
132
-
133
- // ─────────────────────────────────────────────────────────────────────────────
134
- // HtmlForm
135
- // ─────────────────────────────────────────────────────────────────────────────
136
-
137
- const HtmlForm = (props: HtmlFormProps) => {
138
- const { depends, templateid } = props ?? {};
139
-
140
- const [htmlTemplate, setHtmlTemplate] = useState("");
141
-
142
- const { tenant, module } = useApp();
143
-
144
- // ── Load raw HTML from html_forms ──────────────────────────────────────────
145
- const loadHtmlContent = async (tempid: string) => {
146
- try {
147
- const record = await localAPI.useMgmt(tenant!, module!).get("html_templates", tempid);
148
- setHtmlTemplate(record?.htmlCode ?? "");
149
- } catch (error) {
150
- console.error("HtmlForm: error loading html_forms →", error);
151
- setHtmlTemplate("");
152
- }
153
- };
154
-
155
- useDependHandler({ name: depends, onRefresh: () => {} });
156
-
157
- useEffect(() => {
158
- if (templateid?.trim()) loadHtmlContent(templateid);
159
- }, [templateid]);
160
-
161
- // ── Walk DOM → React tree (only re-runs when htmlTemplate changes) ─────────
162
- const reactTree = useMemo(() => {
163
- if (!htmlTemplate) return null;
164
- _keyIndex = 0;
165
- const doc = new DOMParser().parseFromString(htmlTemplate, "text/html");
166
- return Array.from(doc.body.childNodes).map((child) => walkNode(child));
167
- }, [htmlTemplate]);
168
-
169
- return (
170
- <UIComponent {...(props ?? {})}>
171
- <div className="h-[calc(100vh-80px)] overflow-y-auto overflow-x-auto">{reactTree}</div>
172
- </UIComponent>
173
- );
174
- };
175
-
176
- export default HtmlForm;