@etsoo/materialui 1.6.13 → 1.6.15

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.
@@ -51,6 +51,7 @@ function ButtonPopupList(props) {
51
51
  ...selectedIds.toggleItem(item.id, checked)
52
52
  ];
53
53
  setSelectedIdsHandler(newIds);
54
+ onValueChange(newIds);
54
55
  } }), label: `${index + 1}. ${labelFormatter(item)}` })] })), mRef: dndRef }), onAdd && ((0, jsx_runtime_1.jsxs)(FlexBox_1.HBox, { gap: 1, children: [(0, jsx_runtime_1.jsx)(TextField_1.default, { variant: "outlined", label: labels?.more, fullWidth: true, inputRef: inputRef }), (0, jsx_runtime_1.jsx)(Button_1.default, { sx: { width: "120px" }, variant: "contained", startIcon: (0, jsx_runtime_1.jsx)(Add_1.default, {}), size: "small", onClick: async () => {
55
56
  if (inputRef.current == null)
56
57
  return;
@@ -0,0 +1,20 @@
1
+ import { TextFieldProps } from "@mui/material/TextField";
2
+ /**
3
+ * JSON text field props
4
+ */
5
+ export type JsonTextFieldProps = TextFieldProps & {
6
+ /**
7
+ * Whether the value is an array
8
+ */
9
+ isArray?: boolean;
10
+ /**
11
+ * Edit button click handler
12
+ */
13
+ onEdit?: (input: HTMLInputElement) => void;
14
+ };
15
+ /**
16
+ * JSON text field component
17
+ * @param props Props
18
+ * @returns Component
19
+ */
20
+ export declare function JsonTextField(props: JsonTextFieldProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.JsonTextField = JsonTextField;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("@etsoo/react");
9
+ const IconButton_1 = __importDefault(require("@mui/material/IconButton"));
10
+ const InputAdornment_1 = __importDefault(require("@mui/material/InputAdornment"));
11
+ const TextField_1 = __importDefault(require("@mui/material/TextField"));
12
+ const Edit_1 = __importDefault(require("@mui/icons-material/Edit"));
13
+ const react_2 = __importDefault(require("react"));
14
+ const MUGlobal_1 = require("./MUGlobal");
15
+ const ReactApp_1 = require("./app/ReactApp");
16
+ /**
17
+ * JSON text field component
18
+ * @param props Props
19
+ * @returns Component
20
+ */
21
+ function JsonTextField(props) {
22
+ // Destruct
23
+ const { fullWidth = true, inputRef, isArray = false, multiline = true, onChange, onEdit, rows = 3, slotProps, ...rest } = props;
24
+ // Slot props
25
+ const { input, inputLabel, ...restSlotProps } = slotProps ?? {};
26
+ const localRef = react_2.default.useRef(null);
27
+ // Global app
28
+ const app = (0, ReactApp_1.useRequiredAppContext)();
29
+ return ((0, jsx_runtime_1.jsx)(TextField_1.default, { fullWidth: fullWidth, inputRef: (0, react_1.useCombinedRefs)(inputRef, localRef), multiline: multiline, onChange: (event) => {
30
+ const value = event.target.value.trim();
31
+ let errorMessage = "";
32
+ if (value.length > 0) {
33
+ try {
34
+ const parsed = JSON.parse(value);
35
+ if (isArray && !Array.isArray(parsed)) {
36
+ errorMessage =
37
+ app.get("jsonDataArrayError") || "Value must be a JSON array";
38
+ }
39
+ if (typeof parsed !== "object") {
40
+ throw new Error("Parsed value is not an object");
41
+ }
42
+ }
43
+ catch (e) {
44
+ errorMessage =
45
+ (app.get("jsonDataError") || "Invalid JSON text") + " - " + e;
46
+ }
47
+ }
48
+ event.target.setCustomValidity(errorMessage);
49
+ event.target.reportValidity();
50
+ onChange?.(event);
51
+ }, rows: rows, slotProps: {
52
+ input: {
53
+ endAdornment: onEdit ? ((0, jsx_runtime_1.jsx)(InputAdornment_1.default, { position: "end", children: (0, jsx_runtime_1.jsx)(IconButton_1.default, { onClick: () => onEdit?.(localRef.current), children: (0, jsx_runtime_1.jsx)(Edit_1.default, {}) }) })) : undefined,
54
+ ...input
55
+ },
56
+ inputLabel: {
57
+ shrink: MUGlobal_1.MUGlobal.inputFieldShrink,
58
+ ...inputLabel
59
+ },
60
+ ...restSlotProps
61
+ }, ...rest }));
62
+ }
@@ -0,0 +1,11 @@
1
+ import { TextFieldProps } from "@mui/material/TextField";
2
+ /**
3
+ * Custom attribute area properties
4
+ */
5
+ export type CustomAttributeAreaProps = TextFieldProps & {};
6
+ /**
7
+ * Custom attribute area
8
+ * @param props Properties
9
+ * @returns Component
10
+ */
11
+ export declare function CustomAttributeArea(props: CustomAttributeAreaProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CustomAttributeArea = CustomAttributeArea;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const IconButton_1 = __importDefault(require("@mui/material/IconButton"));
9
+ const Add_1 = __importDefault(require("@mui/icons-material/Add"));
10
+ const Edit_1 = __importDefault(require("@mui/icons-material/Edit"));
11
+ const Delete_1 = __importDefault(require("@mui/icons-material/Delete"));
12
+ const DragIndicator_1 = __importDefault(require("@mui/icons-material/DragIndicator"));
13
+ const react_1 = __importDefault(require("react"));
14
+ const Card_1 = __importDefault(require("@mui/material/Card"));
15
+ const CardContent_1 = __importDefault(require("@mui/material/CardContent"));
16
+ const Grid_1 = __importDefault(require("@mui/material/Grid"));
17
+ const appscript_1 = require("@etsoo/appscript");
18
+ const Button_1 = __importDefault(require("@mui/material/Button"));
19
+ const material_1 = require("@mui/material");
20
+ const shared_1 = require("@etsoo/shared");
21
+ const CustomFieldUtils_1 = require("./CustomFieldUtils");
22
+ const ComboBox_1 = require("../ComboBox");
23
+ const InputField_1 = require("../InputField");
24
+ const JsonTextField_1 = require("../JsonTextField");
25
+ const FlexBox_1 = require("../FlexBox");
26
+ const DnDSortableList_1 = require("../DnDSortableList");
27
+ const ReactApp_1 = require("../app/ReactApp");
28
+ const size = { xs: 6, sm: 4, lg: 3, xl: 2 };
29
+ const smallSize = { xs: 3, sm: 2, xl: 1 };
30
+ const random4Digit = () => {
31
+ return Math.floor(1000 + Math.random() * 9000);
32
+ };
33
+ const isCamelCase = (name) => {
34
+ return /^[a-z][a-zA-Z0-9]*$/.test(name);
35
+ };
36
+ function InputItemUIs({ data }) {
37
+ // Global app
38
+ const app = (0, ReactApp_1.useRequiredAppContext)();
39
+ // Labels
40
+ const labels = app.getLabels("gridItemProps", "helperText", "label", "mainSlotProps", "nameB", "options", "optionsFormat", "refs", "size", "type");
41
+ const types = Object.keys(CustomFieldUtils_1.CustomFieldUtils.customFieldCreators);
42
+ const nameRef = react_1.default.useRef(null);
43
+ const optionsRef = react_1.default.useRef(null);
44
+ return ((0, jsx_runtime_1.jsxs)(Grid_1.default, { container: true, spacing: 2, marginTop: 1, children: [(0, jsx_runtime_1.jsx)(Grid_1.default, { size: { xs: 12, sm: 6 }, children: (0, jsx_runtime_1.jsx)(ComboBox_1.ComboBox, { name: "type", label: labels.type, inputRequired: true, size: "small", loadData: () => Promise.resolve(types.map((t) => ({ id: t, label: t }))), onValueChange: (item) => {
45
+ const type = item?.id;
46
+ optionsRef.current.disabled =
47
+ type !== "combobox" && type !== "select";
48
+ const nameInput = nameRef.current;
49
+ if (nameInput.value === "" &&
50
+ (type === "amountlabel" || type === "divider" || type === "label")) {
51
+ nameInput.value = type + random4Digit();
52
+ }
53
+ }, idValue: data?.type, fullWidth: true }) }), (0, jsx_runtime_1.jsx)(Grid_1.default, { size: { xs: 12, sm: 6 }, children: (0, jsx_runtime_1.jsx)(ComboBox_1.ComboBox, { name: "space", label: labels.size, inputRequired: true, size: "small", loadData: () => Promise.resolve(appscript_1.CustomFieldSpaceValues.map((t) => ({ id: t, label: t }))), idValue: data?.space, fullWidth: true }) }), (0, jsx_runtime_1.jsx)(Grid_1.default, { size: { xs: 12, sm: 6 }, children: (0, jsx_runtime_1.jsx)(InputField_1.InputField, { fullWidth: true, required: true, name: "name", size: "small", inputRef: nameRef, slotProps: { htmlInput: { maxLength: 128, readOnly: !!data } }, label: labels.nameB, defaultValue: data?.name ?? "" }) }), (0, jsx_runtime_1.jsx)(Grid_1.default, { size: { xs: 12, sm: 6 }, children: (0, jsx_runtime_1.jsx)(InputField_1.InputField, { fullWidth: true, name: "label", size: "small", slotProps: { htmlInput: { maxLength: 128 } }, label: labels.label, defaultValue: data?.label ?? "" }) }), (0, jsx_runtime_1.jsx)(Grid_1.default, { size: { xs: 12, sm: 12 }, children: (0, jsx_runtime_1.jsx)(InputField_1.InputField, { fullWidth: true, name: "helperText", size: "small", label: labels.helperText, defaultValue: data?.helperText ?? "" }) }), (0, jsx_runtime_1.jsx)(Grid_1.default, { size: { xs: 12, sm: 12 }, children: (0, jsx_runtime_1.jsx)(InputField_1.InputField, { fullWidth: true, name: "options", size: "small", multiline: true, rows: 2, label: labels.options, inputRef: optionsRef, helperText: labels.optionsFormat, slotProps: { htmlInput: { disabled: true } }, defaultValue: data?.options
54
+ ? data.options
55
+ .map((o) => `${o.id}=${shared_1.DataTypes.getListItemLabel(o)}`)
56
+ .join("\n")
57
+ : "" }) }), (0, jsx_runtime_1.jsx)(Grid_1.default, { size: { xs: 12, sm: 12 }, children: (0, jsx_runtime_1.jsx)(JsonTextField_1.JsonTextField, { isArray: true, name: "refs", size: "small", multiline: false, label: labels.refs + " (JSON)", defaultValue: data?.refs ? JSON.stringify(data.refs) : "" }) }), (0, jsx_runtime_1.jsx)(Grid_1.default, { size: { xs: 12, sm: 12 }, children: (0, jsx_runtime_1.jsx)(JsonTextField_1.JsonTextField, { name: "gridItemProps", size: "small", multiline: false, label: labels.gridItemProps + " (JSON)", defaultValue: data?.gridItemProps ? JSON.stringify(data.gridItemProps) : "" }) }), (0, jsx_runtime_1.jsx)(Grid_1.default, { size: { xs: 12, sm: 12 }, children: (0, jsx_runtime_1.jsx)(JsonTextField_1.JsonTextField, { name: "mainSlotProps", size: "small", multiline: false, label: labels.mainSlotProps + " (JSON)", defaultValue: data?.mainSlotProps ? JSON.stringify(data.mainSlotProps) : "", helperText: '{"required":true}' }) })] }));
58
+ }
59
+ function InputUIs({ source, onChange }) {
60
+ // Global app
61
+ const app = (0, ReactApp_1.useRequiredAppContext)();
62
+ // Labels
63
+ const labels = app.getLabels("add", "delete", "edit", "sortTip", "dragIndicator");
64
+ const [items, setItems] = react_1.default.useState([]);
65
+ const doChange = (items) => {
66
+ setItems(items);
67
+ onChange(items);
68
+ };
69
+ const editItem = (item) => {
70
+ app.showInputDialog({
71
+ title: item ? labels.edit : labels.add,
72
+ message: "",
73
+ callback: async (form) => {
74
+ // Cancelled
75
+ if (form == null) {
76
+ return;
77
+ }
78
+ // Validate form
79
+ if (!form.reportValidity()) {
80
+ return false;
81
+ }
82
+ // Form data
83
+ const { typeInput: type, spaceInput: space, name, label, helperText, options, refs, gridItemProps, mainSlotProps } = shared_1.DomUtils.dataAs(new FormData(form), {
84
+ typeInput: "string",
85
+ spaceInput: "string",
86
+ name: "string",
87
+ label: "string",
88
+ helperText: "string",
89
+ options: "string",
90
+ refs: "string",
91
+ gridItemProps: "string",
92
+ mainSlotProps: "string"
93
+ });
94
+ if (!type || !space || !name) {
95
+ return app.get("noData");
96
+ }
97
+ if (!isCamelCase(name)) {
98
+ shared_1.DomUtils.setFocus("name", form);
99
+ return app.get("invalidNaming") + " (camelCase)";
100
+ }
101
+ if (type !== "divider" && !label) {
102
+ shared_1.DomUtils.setFocus("label", form);
103
+ return false;
104
+ }
105
+ if (item == null && items.some((item) => item.name === name)) {
106
+ return app.get("itemExists")?.format(name);
107
+ }
108
+ const optionsJson = options
109
+ ? options.split("\n").map((line) => {
110
+ const [id, ...labelParts] = line.split("=");
111
+ return { id, label: labelParts.join("=") };
112
+ })
113
+ : undefined;
114
+ const refsJson = refs ? JSON.parse(refs) : undefined;
115
+ const gridItemPropsJson = gridItemProps
116
+ ? JSON.parse(gridItemProps)
117
+ : undefined;
118
+ const mainSlotPropsJson = mainSlotProps
119
+ ? JSON.parse(mainSlotProps)
120
+ : undefined;
121
+ if (item == null) {
122
+ const newItem = {
123
+ type,
124
+ name,
125
+ space: space,
126
+ label,
127
+ helperText,
128
+ options: optionsJson,
129
+ refs: refsJson,
130
+ gridItemProps: gridItemPropsJson,
131
+ mainSlotProps: mainSlotPropsJson
132
+ };
133
+ doChange([...items, newItem]);
134
+ }
135
+ else {
136
+ item.type = type;
137
+ item.space = space;
138
+ item.name = name;
139
+ item.label = label;
140
+ item.helperText = helperText;
141
+ item.options = optionsJson;
142
+ item.refs = refsJson;
143
+ item.gridItemProps = gridItemPropsJson;
144
+ item.mainSlotProps = mainSlotPropsJson;
145
+ doChange([...items]);
146
+ }
147
+ return;
148
+ },
149
+ inputs: (0, jsx_runtime_1.jsx)(InputItemUIs, { data: item })
150
+ });
151
+ };
152
+ react_1.default.useEffect(() => {
153
+ try {
154
+ if (source) {
155
+ const parsed = JSON.parse(source);
156
+ if (Array.isArray(parsed)) {
157
+ setItems(parsed);
158
+ }
159
+ }
160
+ }
161
+ catch (error) {
162
+ console.error("Failed to parse source:", error);
163
+ }
164
+ }, [source]);
165
+ return ((0, jsx_runtime_1.jsxs)(react_1.default.Fragment, { children: [(0, jsx_runtime_1.jsxs)(FlexBox_1.HBox, { marginBottom: 0.5, gap: 1, alignItems: "center", children: [(0, jsx_runtime_1.jsx)(material_1.Typography, { children: labels.sortTip }), (0, jsx_runtime_1.jsx)(Button_1.default, { size: "small", color: "primary", variant: "outlined", startIcon: (0, jsx_runtime_1.jsx)(Add_1.default, {}), onClick: () => editItem(), children: labels.add })] }), (0, jsx_runtime_1.jsx)(Card_1.default, { children: (0, jsx_runtime_1.jsx)(CardContent_1.default, { children: (0, jsx_runtime_1.jsx)(Grid_1.default, { container: true, spacing: 0, children: (0, jsx_runtime_1.jsx)(DnDSortableList_1.DnDSortableList, { items: items, idField: (item) => item.name, labelField: (item) => item.label || item.name, onChange: (items) => doChange(items), itemRenderer: (data, style, { sortable: { index }, ref, handleRef }) => ((0, jsx_runtime_1.jsxs)(Grid_1.default, { container: true, size: { xs: 12, sm: 12 }, ref: ref, style: style, gap: 1, children: [(0, jsx_runtime_1.jsx)(Grid_1.default, { size: smallSize, children: (0, jsx_runtime_1.jsx)(IconButton_1.default, { style: { cursor: "move" }, size: "small", title: labels.dragIndicator, ref: handleRef, children: (0, jsx_runtime_1.jsx)(DragIndicator_1.default, {}) }) }), (0, jsx_runtime_1.jsxs)(Grid_1.default, { size: size, children: [index + 1, " - ", data.type, " / ", data.space] }), (0, jsx_runtime_1.jsxs)(Grid_1.default, { size: size, children: [data.name, " - ", data.label] }), (0, jsx_runtime_1.jsxs)(Grid_1.default, { size: smallSize, children: [(0, jsx_runtime_1.jsx)(IconButton_1.default, { size: "small", title: labels.delete, onClick: () => doChange(items.filter((item) => item.name !== data.name)), children: (0, jsx_runtime_1.jsx)(Delete_1.default, {}) }), (0, jsx_runtime_1.jsx)(IconButton_1.default, { size: "small", title: labels.edit, onClick: () => editItem(data), children: (0, jsx_runtime_1.jsx)(Edit_1.default, {}) })] })] })) }) }) }) })] }));
166
+ }
167
+ /**
168
+ * Custom attribute area
169
+ * @param props Properties
170
+ * @returns Component
171
+ */
172
+ function CustomAttributeArea(props) {
173
+ // Global app
174
+ const app = (0, ReactApp_1.useRequiredAppContext)();
175
+ // Destruct
176
+ const { label = app.get("attributeDefinition"), ...rest } = props;
177
+ const ref = react_1.default.useRef([]);
178
+ const showUI = (input) => {
179
+ app.showInputDialog({
180
+ title: label,
181
+ message: "",
182
+ fullScreen: true,
183
+ callback: (form) => {
184
+ if (form == null) {
185
+ return;
186
+ }
187
+ input.value = ref.current.length > 0 ? JSON.stringify(ref.current) : "";
188
+ },
189
+ inputs: ((0, jsx_runtime_1.jsx)(InputUIs, { source: input.value, onChange: (items) => (ref.current = items) }))
190
+ });
191
+ };
192
+ // Layout
193
+ return (0, jsx_runtime_1.jsx)(JsonTextField_1.JsonTextField, { label: label, onEdit: showUI, isArray: true, ...rest });
194
+ }
@@ -6,6 +6,7 @@ export * from "./app/ISmartERPUser";
6
6
  export * from "./app/Labels";
7
7
  export * from "./app/ReactApp";
8
8
  export * from "./app/ServiceApp";
9
+ export * from "./custom/CustomAttributeArea";
9
10
  export * from "./custom/CustomFieldUtils";
10
11
  export * from "./custom/CustomFieldViewer";
11
12
  export * from "./custom/CustomFieldWindow";
@@ -73,6 +74,7 @@ export * from "./InputTipField";
73
74
  export * from "./IntInputField";
74
75
  export * from "./ItemList";
75
76
  export * from "./JsonDataInput";
77
+ export * from "./JsonTextField";
76
78
  export * from "./LineChart";
77
79
  export * from "./LinkEx";
78
80
  export * from "./ListChooser";
package/lib/cjs/index.js CHANGED
@@ -22,6 +22,7 @@ __exportStar(require("./app/ISmartERPUser"), exports);
22
22
  __exportStar(require("./app/Labels"), exports);
23
23
  __exportStar(require("./app/ReactApp"), exports);
24
24
  __exportStar(require("./app/ServiceApp"), exports);
25
+ __exportStar(require("./custom/CustomAttributeArea"), exports);
25
26
  __exportStar(require("./custom/CustomFieldUtils"), exports);
26
27
  __exportStar(require("./custom/CustomFieldViewer"), exports);
27
28
  __exportStar(require("./custom/CustomFieldWindow"), exports);
@@ -89,6 +90,7 @@ __exportStar(require("./InputTipField"), exports);
89
90
  __exportStar(require("./IntInputField"), exports);
90
91
  __exportStar(require("./ItemList"), exports);
91
92
  __exportStar(require("./JsonDataInput"), exports);
93
+ __exportStar(require("./JsonTextField"), exports);
92
94
  __exportStar(require("./LineChart"), exports);
93
95
  __exportStar(require("./LinkEx"), exports);
94
96
  __exportStar(require("./ListChooser"), exports);
@@ -45,6 +45,7 @@ function ButtonPopupList(props) {
45
45
  ...selectedIds.toggleItem(item.id, checked)
46
46
  ];
47
47
  setSelectedIdsHandler(newIds);
48
+ onValueChange(newIds);
48
49
  } }), label: `${index + 1}. ${labelFormatter(item)}` })] })), mRef: dndRef }), onAdd && (_jsxs(HBox, { gap: 1, children: [_jsx(TextField, { variant: "outlined", label: labels?.more, fullWidth: true, inputRef: inputRef }), _jsx(Button, { sx: { width: "120px" }, variant: "contained", startIcon: _jsx(AddIcon, {}), size: "small", onClick: async () => {
49
50
  if (inputRef.current == null)
50
51
  return;
@@ -0,0 +1,20 @@
1
+ import { TextFieldProps } from "@mui/material/TextField";
2
+ /**
3
+ * JSON text field props
4
+ */
5
+ export type JsonTextFieldProps = TextFieldProps & {
6
+ /**
7
+ * Whether the value is an array
8
+ */
9
+ isArray?: boolean;
10
+ /**
11
+ * Edit button click handler
12
+ */
13
+ onEdit?: (input: HTMLInputElement) => void;
14
+ };
15
+ /**
16
+ * JSON text field component
17
+ * @param props Props
18
+ * @returns Component
19
+ */
20
+ export declare function JsonTextField(props: JsonTextFieldProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCombinedRefs } from "@etsoo/react";
3
+ import IconButton from "@mui/material/IconButton";
4
+ import InputAdornment from "@mui/material/InputAdornment";
5
+ import TextField from "@mui/material/TextField";
6
+ import EditIcon from "@mui/icons-material/Edit";
7
+ import React from "react";
8
+ import { MUGlobal } from "./MUGlobal";
9
+ import { useRequiredAppContext } from "./app/ReactApp";
10
+ /**
11
+ * JSON text field component
12
+ * @param props Props
13
+ * @returns Component
14
+ */
15
+ export function JsonTextField(props) {
16
+ // Destruct
17
+ const { fullWidth = true, inputRef, isArray = false, multiline = true, onChange, onEdit, rows = 3, slotProps, ...rest } = props;
18
+ // Slot props
19
+ const { input, inputLabel, ...restSlotProps } = slotProps ?? {};
20
+ const localRef = React.useRef(null);
21
+ // Global app
22
+ const app = useRequiredAppContext();
23
+ return (_jsx(TextField, { fullWidth: fullWidth, inputRef: useCombinedRefs(inputRef, localRef), multiline: multiline, onChange: (event) => {
24
+ const value = event.target.value.trim();
25
+ let errorMessage = "";
26
+ if (value.length > 0) {
27
+ try {
28
+ const parsed = JSON.parse(value);
29
+ if (isArray && !Array.isArray(parsed)) {
30
+ errorMessage =
31
+ app.get("jsonDataArrayError") || "Value must be a JSON array";
32
+ }
33
+ if (typeof parsed !== "object") {
34
+ throw new Error("Parsed value is not an object");
35
+ }
36
+ }
37
+ catch (e) {
38
+ errorMessage =
39
+ (app.get("jsonDataError") || "Invalid JSON text") + " - " + e;
40
+ }
41
+ }
42
+ event.target.setCustomValidity(errorMessage);
43
+ event.target.reportValidity();
44
+ onChange?.(event);
45
+ }, rows: rows, slotProps: {
46
+ input: {
47
+ endAdornment: onEdit ? (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { onClick: () => onEdit?.(localRef.current), children: _jsx(EditIcon, {}) }) })) : undefined,
48
+ ...input
49
+ },
50
+ inputLabel: {
51
+ shrink: MUGlobal.inputFieldShrink,
52
+ ...inputLabel
53
+ },
54
+ ...restSlotProps
55
+ }, ...rest }));
56
+ }
@@ -0,0 +1,11 @@
1
+ import { TextFieldProps } from "@mui/material/TextField";
2
+ /**
3
+ * Custom attribute area properties
4
+ */
5
+ export type CustomAttributeAreaProps = TextFieldProps & {};
6
+ /**
7
+ * Custom attribute area
8
+ * @param props Properties
9
+ * @returns Component
10
+ */
11
+ export declare function CustomAttributeArea(props: CustomAttributeAreaProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,188 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import IconButton from "@mui/material/IconButton";
3
+ import AddIcon from "@mui/icons-material/Add";
4
+ import EditIcon from "@mui/icons-material/Edit";
5
+ import DeleteIcon from "@mui/icons-material/Delete";
6
+ import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
7
+ import React from "react";
8
+ import Card from "@mui/material/Card";
9
+ import CardContent from "@mui/material/CardContent";
10
+ import Grid from "@mui/material/Grid";
11
+ import { CustomFieldSpaceValues } from "@etsoo/appscript";
12
+ import Button from "@mui/material/Button";
13
+ import { Typography } from "@mui/material";
14
+ import { DataTypes, DomUtils } from "@etsoo/shared";
15
+ import { CustomFieldUtils } from "./CustomFieldUtils";
16
+ import { ComboBox } from "../ComboBox";
17
+ import { InputField } from "../InputField";
18
+ import { JsonTextField } from "../JsonTextField";
19
+ import { HBox } from "../FlexBox";
20
+ import { DnDSortableList } from "../DnDSortableList";
21
+ import { useRequiredAppContext } from "../app/ReactApp";
22
+ const size = { xs: 6, sm: 4, lg: 3, xl: 2 };
23
+ const smallSize = { xs: 3, sm: 2, xl: 1 };
24
+ const random4Digit = () => {
25
+ return Math.floor(1000 + Math.random() * 9000);
26
+ };
27
+ const isCamelCase = (name) => {
28
+ return /^[a-z][a-zA-Z0-9]*$/.test(name);
29
+ };
30
+ function InputItemUIs({ data }) {
31
+ // Global app
32
+ const app = useRequiredAppContext();
33
+ // Labels
34
+ const labels = app.getLabels("gridItemProps", "helperText", "label", "mainSlotProps", "nameB", "options", "optionsFormat", "refs", "size", "type");
35
+ const types = Object.keys(CustomFieldUtils.customFieldCreators);
36
+ const nameRef = React.useRef(null);
37
+ const optionsRef = React.useRef(null);
38
+ return (_jsxs(Grid, { container: true, spacing: 2, marginTop: 1, children: [_jsx(Grid, { size: { xs: 12, sm: 6 }, children: _jsx(ComboBox, { name: "type", label: labels.type, inputRequired: true, size: "small", loadData: () => Promise.resolve(types.map((t) => ({ id: t, label: t }))), onValueChange: (item) => {
39
+ const type = item?.id;
40
+ optionsRef.current.disabled =
41
+ type !== "combobox" && type !== "select";
42
+ const nameInput = nameRef.current;
43
+ if (nameInput.value === "" &&
44
+ (type === "amountlabel" || type === "divider" || type === "label")) {
45
+ nameInput.value = type + random4Digit();
46
+ }
47
+ }, idValue: data?.type, fullWidth: true }) }), _jsx(Grid, { size: { xs: 12, sm: 6 }, children: _jsx(ComboBox, { name: "space", label: labels.size, inputRequired: true, size: "small", loadData: () => Promise.resolve(CustomFieldSpaceValues.map((t) => ({ id: t, label: t }))), idValue: data?.space, fullWidth: true }) }), _jsx(Grid, { size: { xs: 12, sm: 6 }, children: _jsx(InputField, { fullWidth: true, required: true, name: "name", size: "small", inputRef: nameRef, slotProps: { htmlInput: { maxLength: 128, readOnly: !!data } }, label: labels.nameB, defaultValue: data?.name ?? "" }) }), _jsx(Grid, { size: { xs: 12, sm: 6 }, children: _jsx(InputField, { fullWidth: true, name: "label", size: "small", slotProps: { htmlInput: { maxLength: 128 } }, label: labels.label, defaultValue: data?.label ?? "" }) }), _jsx(Grid, { size: { xs: 12, sm: 12 }, children: _jsx(InputField, { fullWidth: true, name: "helperText", size: "small", label: labels.helperText, defaultValue: data?.helperText ?? "" }) }), _jsx(Grid, { size: { xs: 12, sm: 12 }, children: _jsx(InputField, { fullWidth: true, name: "options", size: "small", multiline: true, rows: 2, label: labels.options, inputRef: optionsRef, helperText: labels.optionsFormat, slotProps: { htmlInput: { disabled: true } }, defaultValue: data?.options
48
+ ? data.options
49
+ .map((o) => `${o.id}=${DataTypes.getListItemLabel(o)}`)
50
+ .join("\n")
51
+ : "" }) }), _jsx(Grid, { size: { xs: 12, sm: 12 }, children: _jsx(JsonTextField, { isArray: true, name: "refs", size: "small", multiline: false, label: labels.refs + " (JSON)", defaultValue: data?.refs ? JSON.stringify(data.refs) : "" }) }), _jsx(Grid, { size: { xs: 12, sm: 12 }, children: _jsx(JsonTextField, { name: "gridItemProps", size: "small", multiline: false, label: labels.gridItemProps + " (JSON)", defaultValue: data?.gridItemProps ? JSON.stringify(data.gridItemProps) : "" }) }), _jsx(Grid, { size: { xs: 12, sm: 12 }, children: _jsx(JsonTextField, { name: "mainSlotProps", size: "small", multiline: false, label: labels.mainSlotProps + " (JSON)", defaultValue: data?.mainSlotProps ? JSON.stringify(data.mainSlotProps) : "", helperText: '{"required":true}' }) })] }));
52
+ }
53
+ function InputUIs({ source, onChange }) {
54
+ // Global app
55
+ const app = useRequiredAppContext();
56
+ // Labels
57
+ const labels = app.getLabels("add", "delete", "edit", "sortTip", "dragIndicator");
58
+ const [items, setItems] = React.useState([]);
59
+ const doChange = (items) => {
60
+ setItems(items);
61
+ onChange(items);
62
+ };
63
+ const editItem = (item) => {
64
+ app.showInputDialog({
65
+ title: item ? labels.edit : labels.add,
66
+ message: "",
67
+ callback: async (form) => {
68
+ // Cancelled
69
+ if (form == null) {
70
+ return;
71
+ }
72
+ // Validate form
73
+ if (!form.reportValidity()) {
74
+ return false;
75
+ }
76
+ // Form data
77
+ const { typeInput: type, spaceInput: space, name, label, helperText, options, refs, gridItemProps, mainSlotProps } = DomUtils.dataAs(new FormData(form), {
78
+ typeInput: "string",
79
+ spaceInput: "string",
80
+ name: "string",
81
+ label: "string",
82
+ helperText: "string",
83
+ options: "string",
84
+ refs: "string",
85
+ gridItemProps: "string",
86
+ mainSlotProps: "string"
87
+ });
88
+ if (!type || !space || !name) {
89
+ return app.get("noData");
90
+ }
91
+ if (!isCamelCase(name)) {
92
+ DomUtils.setFocus("name", form);
93
+ return app.get("invalidNaming") + " (camelCase)";
94
+ }
95
+ if (type !== "divider" && !label) {
96
+ DomUtils.setFocus("label", form);
97
+ return false;
98
+ }
99
+ if (item == null && items.some((item) => item.name === name)) {
100
+ return app.get("itemExists")?.format(name);
101
+ }
102
+ const optionsJson = options
103
+ ? options.split("\n").map((line) => {
104
+ const [id, ...labelParts] = line.split("=");
105
+ return { id, label: labelParts.join("=") };
106
+ })
107
+ : undefined;
108
+ const refsJson = refs ? JSON.parse(refs) : undefined;
109
+ const gridItemPropsJson = gridItemProps
110
+ ? JSON.parse(gridItemProps)
111
+ : undefined;
112
+ const mainSlotPropsJson = mainSlotProps
113
+ ? JSON.parse(mainSlotProps)
114
+ : undefined;
115
+ if (item == null) {
116
+ const newItem = {
117
+ type,
118
+ name,
119
+ space: space,
120
+ label,
121
+ helperText,
122
+ options: optionsJson,
123
+ refs: refsJson,
124
+ gridItemProps: gridItemPropsJson,
125
+ mainSlotProps: mainSlotPropsJson
126
+ };
127
+ doChange([...items, newItem]);
128
+ }
129
+ else {
130
+ item.type = type;
131
+ item.space = space;
132
+ item.name = name;
133
+ item.label = label;
134
+ item.helperText = helperText;
135
+ item.options = optionsJson;
136
+ item.refs = refsJson;
137
+ item.gridItemProps = gridItemPropsJson;
138
+ item.mainSlotProps = mainSlotPropsJson;
139
+ doChange([...items]);
140
+ }
141
+ return;
142
+ },
143
+ inputs: _jsx(InputItemUIs, { data: item })
144
+ });
145
+ };
146
+ React.useEffect(() => {
147
+ try {
148
+ if (source) {
149
+ const parsed = JSON.parse(source);
150
+ if (Array.isArray(parsed)) {
151
+ setItems(parsed);
152
+ }
153
+ }
154
+ }
155
+ catch (error) {
156
+ console.error("Failed to parse source:", error);
157
+ }
158
+ }, [source]);
159
+ return (_jsxs(React.Fragment, { children: [_jsxs(HBox, { marginBottom: 0.5, gap: 1, alignItems: "center", children: [_jsx(Typography, { children: labels.sortTip }), _jsx(Button, { size: "small", color: "primary", variant: "outlined", startIcon: _jsx(AddIcon, {}), onClick: () => editItem(), children: labels.add })] }), _jsx(Card, { children: _jsx(CardContent, { children: _jsx(Grid, { container: true, spacing: 0, children: _jsx(DnDSortableList, { items: items, idField: (item) => item.name, labelField: (item) => item.label || item.name, onChange: (items) => doChange(items), itemRenderer: (data, style, { sortable: { index }, ref, handleRef }) => (_jsxs(Grid, { container: true, size: { xs: 12, sm: 12 }, ref: ref, style: style, gap: 1, children: [_jsx(Grid, { size: smallSize, children: _jsx(IconButton, { style: { cursor: "move" }, size: "small", title: labels.dragIndicator, ref: handleRef, children: _jsx(DragIndicatorIcon, {}) }) }), _jsxs(Grid, { size: size, children: [index + 1, " - ", data.type, " / ", data.space] }), _jsxs(Grid, { size: size, children: [data.name, " - ", data.label] }), _jsxs(Grid, { size: smallSize, children: [_jsx(IconButton, { size: "small", title: labels.delete, onClick: () => doChange(items.filter((item) => item.name !== data.name)), children: _jsx(DeleteIcon, {}) }), _jsx(IconButton, { size: "small", title: labels.edit, onClick: () => editItem(data), children: _jsx(EditIcon, {}) })] })] })) }) }) }) })] }));
160
+ }
161
+ /**
162
+ * Custom attribute area
163
+ * @param props Properties
164
+ * @returns Component
165
+ */
166
+ export function CustomAttributeArea(props) {
167
+ // Global app
168
+ const app = useRequiredAppContext();
169
+ // Destruct
170
+ const { label = app.get("attributeDefinition"), ...rest } = props;
171
+ const ref = React.useRef([]);
172
+ const showUI = (input) => {
173
+ app.showInputDialog({
174
+ title: label,
175
+ message: "",
176
+ fullScreen: true,
177
+ callback: (form) => {
178
+ if (form == null) {
179
+ return;
180
+ }
181
+ input.value = ref.current.length > 0 ? JSON.stringify(ref.current) : "";
182
+ },
183
+ inputs: (_jsx(InputUIs, { source: input.value, onChange: (items) => (ref.current = items) }))
184
+ });
185
+ };
186
+ // Layout
187
+ return _jsx(JsonTextField, { label: label, onEdit: showUI, isArray: true, ...rest });
188
+ }
@@ -6,6 +6,7 @@ export * from "./app/ISmartERPUser";
6
6
  export * from "./app/Labels";
7
7
  export * from "./app/ReactApp";
8
8
  export * from "./app/ServiceApp";
9
+ export * from "./custom/CustomAttributeArea";
9
10
  export * from "./custom/CustomFieldUtils";
10
11
  export * from "./custom/CustomFieldViewer";
11
12
  export * from "./custom/CustomFieldWindow";
@@ -73,6 +74,7 @@ export * from "./InputTipField";
73
74
  export * from "./IntInputField";
74
75
  export * from "./ItemList";
75
76
  export * from "./JsonDataInput";
77
+ export * from "./JsonTextField";
76
78
  export * from "./LineChart";
77
79
  export * from "./LinkEx";
78
80
  export * from "./ListChooser";
package/lib/mjs/index.js CHANGED
@@ -6,6 +6,7 @@ export * from "./app/ISmartERPUser";
6
6
  export * from "./app/Labels";
7
7
  export * from "./app/ReactApp";
8
8
  export * from "./app/ServiceApp";
9
+ export * from "./custom/CustomAttributeArea";
9
10
  export * from "./custom/CustomFieldUtils";
10
11
  export * from "./custom/CustomFieldViewer";
11
12
  export * from "./custom/CustomFieldWindow";
@@ -73,6 +74,7 @@ export * from "./InputTipField";
73
74
  export * from "./IntInputField";
74
75
  export * from "./ItemList";
75
76
  export * from "./JsonDataInput";
77
+ export * from "./JsonTextField";
76
78
  export * from "./LineChart";
77
79
  export * from "./LinkEx";
78
80
  export * from "./ListChooser";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/materialui",
3
- "version": "1.6.13",
3
+ "version": "1.6.15",
4
4
  "description": "TypeScript Material-UI Implementation",
5
5
  "main": "lib/cjs/index.js",
6
6
  "module": "lib/mjs/index.js",
@@ -39,9 +39,9 @@
39
39
  "@dnd-kit/react": "^0.3.2",
40
40
  "@emotion/react": "^11.14.0",
41
41
  "@emotion/styled": "^11.14.1",
42
- "@etsoo/appscript": "^1.6.53",
42
+ "@etsoo/appscript": "^1.6.56",
43
43
  "@etsoo/notificationbase": "^1.1.66",
44
- "@etsoo/react": "^1.8.74",
44
+ "@etsoo/react": "^1.8.76",
45
45
  "@etsoo/shared": "^1.2.80",
46
46
  "@mui/icons-material": "^7.3.8",
47
47
  "@mui/material": "^7.3.8",
@@ -215,6 +215,7 @@ function ButtonPopupList<D extends DnDItemType>(
215
215
  ...selectedIds.toggleItem(item.id, checked)
216
216
  ];
217
217
  setSelectedIdsHandler(newIds);
218
+ onValueChange(newIds);
218
219
  }}
219
220
  />
220
221
  }
@@ -0,0 +1,104 @@
1
+ import { useCombinedRefs } from "@etsoo/react";
2
+ import IconButton from "@mui/material/IconButton";
3
+ import InputAdornment from "@mui/material/InputAdornment";
4
+ import TextField, { TextFieldProps } from "@mui/material/TextField";
5
+ import EditIcon from "@mui/icons-material/Edit";
6
+ import React from "react";
7
+ import { MUGlobal } from "./MUGlobal";
8
+ import { useRequiredAppContext } from "./app/ReactApp";
9
+
10
+ /**
11
+ * JSON text field props
12
+ */
13
+ export type JsonTextFieldProps = TextFieldProps & {
14
+ /**
15
+ * Whether the value is an array
16
+ */
17
+ isArray?: boolean;
18
+
19
+ /**
20
+ * Edit button click handler
21
+ */
22
+ onEdit?: (input: HTMLInputElement) => void;
23
+ };
24
+
25
+ /**
26
+ * JSON text field component
27
+ * @param props Props
28
+ * @returns Component
29
+ */
30
+ export function JsonTextField(props: JsonTextFieldProps) {
31
+ // Destruct
32
+ const {
33
+ fullWidth = true,
34
+ inputRef,
35
+ isArray = false,
36
+ multiline = true,
37
+ onChange,
38
+ onEdit,
39
+ rows = 3,
40
+ slotProps,
41
+ ...rest
42
+ } = props;
43
+
44
+ // Slot props
45
+ const { input, inputLabel, ...restSlotProps } = slotProps ?? {};
46
+
47
+ const localRef = React.useRef<HTMLInputElement>(null);
48
+
49
+ // Global app
50
+ const app = useRequiredAppContext();
51
+
52
+ return (
53
+ <TextField
54
+ fullWidth={fullWidth}
55
+ inputRef={useCombinedRefs(inputRef, localRef)}
56
+ multiline={multiline}
57
+ onChange={(event) => {
58
+ const value = event.target.value.trim();
59
+ let errorMessage = "";
60
+
61
+ if (value.length > 0) {
62
+ try {
63
+ const parsed = JSON.parse(value);
64
+ if (isArray && !Array.isArray(parsed)) {
65
+ errorMessage =
66
+ app.get("jsonDataArrayError") || "Value must be a JSON array";
67
+ }
68
+
69
+ if (typeof parsed !== "object") {
70
+ throw new Error("Parsed value is not an object");
71
+ }
72
+ } catch (e) {
73
+ errorMessage =
74
+ (app.get("jsonDataError") || "Invalid JSON text") + " - " + e;
75
+ }
76
+ }
77
+
78
+ event.target.setCustomValidity(errorMessage);
79
+ event.target.reportValidity();
80
+
81
+ onChange?.(event);
82
+ }}
83
+ rows={rows}
84
+ slotProps={{
85
+ input: {
86
+ endAdornment: onEdit ? (
87
+ <InputAdornment position="end">
88
+ <IconButton onClick={() => onEdit?.(localRef.current!)}>
89
+ <EditIcon />
90
+ </IconButton>
91
+ </InputAdornment>
92
+ ) : undefined,
93
+ ...input
94
+ },
95
+ inputLabel: {
96
+ shrink: MUGlobal.inputFieldShrink,
97
+ ...inputLabel
98
+ },
99
+ ...restSlotProps
100
+ }}
101
+ {...rest}
102
+ />
103
+ );
104
+ }
@@ -0,0 +1,454 @@
1
+ import { TextFieldProps } from "@mui/material/TextField";
2
+ import IconButton from "@mui/material/IconButton";
3
+ import AddIcon from "@mui/icons-material/Add";
4
+ import EditIcon from "@mui/icons-material/Edit";
5
+ import DeleteIcon from "@mui/icons-material/Delete";
6
+ import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
7
+ import React from "react";
8
+ import Card from "@mui/material/Card";
9
+ import CardContent from "@mui/material/CardContent";
10
+ import Grid from "@mui/material/Grid";
11
+ import { CustomFieldData, CustomFieldSpaceValues } from "@etsoo/appscript";
12
+ import Button from "@mui/material/Button";
13
+ import { Typography } from "@mui/material";
14
+ import { DataTypes, DomUtils } from "@etsoo/shared";
15
+ import { CustomFieldUtils } from "./CustomFieldUtils";
16
+ import { ComboBox } from "../ComboBox";
17
+ import { InputField } from "../InputField";
18
+ import { JsonTextField } from "../JsonTextField";
19
+ import { HBox } from "../FlexBox";
20
+ import { DnDSortableList } from "../DnDSortableList";
21
+ import { useRequiredAppContext } from "../app/ReactApp";
22
+
23
+ const size = { xs: 6, sm: 4, lg: 3, xl: 2 };
24
+ const smallSize = { xs: 3, sm: 2, xl: 1 };
25
+
26
+ const random4Digit = (): number => {
27
+ return Math.floor(1000 + Math.random() * 9000);
28
+ };
29
+
30
+ const isCamelCase = (name: string): boolean => {
31
+ return /^[a-z][a-zA-Z0-9]*$/.test(name);
32
+ };
33
+
34
+ function InputItemUIs({ data }: { data?: CustomFieldData }) {
35
+ // Global app
36
+ const app = useRequiredAppContext();
37
+
38
+ // Labels
39
+ const labels = app.getLabels(
40
+ "gridItemProps",
41
+ "helperText",
42
+ "label",
43
+ "mainSlotProps",
44
+ "nameB",
45
+ "options",
46
+ "optionsFormat",
47
+ "refs",
48
+ "size",
49
+ "type"
50
+ );
51
+
52
+ const types = Object.keys(CustomFieldUtils.customFieldCreators);
53
+
54
+ const nameRef = React.useRef<HTMLInputElement>(null);
55
+ const optionsRef = React.useRef<HTMLInputElement>(null);
56
+
57
+ return (
58
+ <Grid container spacing={2} marginTop={1}>
59
+ <Grid size={{ xs: 12, sm: 6 }}>
60
+ <ComboBox
61
+ name="type"
62
+ label={labels.type}
63
+ inputRequired
64
+ size="small"
65
+ loadData={() =>
66
+ Promise.resolve(types.map((t) => ({ id: t, label: t })))
67
+ }
68
+ onValueChange={(item) => {
69
+ const type = item?.id;
70
+ optionsRef.current!.disabled =
71
+ type !== "combobox" && type !== "select";
72
+
73
+ const nameInput = nameRef.current!;
74
+ if (
75
+ nameInput.value === "" &&
76
+ (type === "amountlabel" || type === "divider" || type === "label")
77
+ ) {
78
+ nameInput.value = type + random4Digit();
79
+ }
80
+ }}
81
+ idValue={data?.type}
82
+ fullWidth
83
+ />
84
+ </Grid>
85
+ <Grid size={{ xs: 12, sm: 6 }}>
86
+ <ComboBox
87
+ name="space"
88
+ label={labels.size}
89
+ inputRequired
90
+ size="small"
91
+ loadData={() =>
92
+ Promise.resolve(
93
+ CustomFieldSpaceValues.map((t) => ({ id: t, label: t }))
94
+ )
95
+ }
96
+ idValue={data?.space}
97
+ fullWidth
98
+ />
99
+ </Grid>
100
+ <Grid size={{ xs: 12, sm: 6 }}>
101
+ <InputField
102
+ fullWidth
103
+ required
104
+ name="name"
105
+ size="small"
106
+ inputRef={nameRef}
107
+ slotProps={{ htmlInput: { maxLength: 128, readOnly: !!data } }}
108
+ label={labels.nameB}
109
+ defaultValue={data?.name ?? ""}
110
+ />
111
+ </Grid>
112
+ <Grid size={{ xs: 12, sm: 6 }}>
113
+ <InputField
114
+ fullWidth
115
+ name="label"
116
+ size="small"
117
+ slotProps={{ htmlInput: { maxLength: 128 } }}
118
+ label={labels.label}
119
+ defaultValue={data?.label ?? ""}
120
+ />
121
+ </Grid>
122
+ <Grid size={{ xs: 12, sm: 12 }}>
123
+ <InputField
124
+ fullWidth
125
+ name="helperText"
126
+ size="small"
127
+ label={labels.helperText}
128
+ defaultValue={data?.helperText ?? ""}
129
+ />
130
+ </Grid>
131
+ <Grid size={{ xs: 12, sm: 12 }}>
132
+ <InputField
133
+ fullWidth
134
+ name="options"
135
+ size="small"
136
+ multiline
137
+ rows={2}
138
+ label={labels.options}
139
+ inputRef={optionsRef}
140
+ helperText={labels.optionsFormat}
141
+ slotProps={{ htmlInput: { disabled: true } }}
142
+ defaultValue={
143
+ data?.options
144
+ ? data.options
145
+ .map((o) => `${o.id}=${DataTypes.getListItemLabel(o)}`)
146
+ .join("\n")
147
+ : ""
148
+ }
149
+ />
150
+ </Grid>
151
+ <Grid size={{ xs: 12, sm: 12 }}>
152
+ <JsonTextField
153
+ isArray
154
+ name="refs"
155
+ size="small"
156
+ multiline={false}
157
+ label={labels.refs + " (JSON)"}
158
+ defaultValue={data?.refs ? JSON.stringify(data.refs) : ""}
159
+ />
160
+ </Grid>
161
+ <Grid size={{ xs: 12, sm: 12 }}>
162
+ <JsonTextField
163
+ name="gridItemProps"
164
+ size="small"
165
+ multiline={false}
166
+ label={labels.gridItemProps + " (JSON)"}
167
+ defaultValue={
168
+ data?.gridItemProps ? JSON.stringify(data.gridItemProps) : ""
169
+ }
170
+ />
171
+ </Grid>
172
+ <Grid size={{ xs: 12, sm: 12 }}>
173
+ <JsonTextField
174
+ name="mainSlotProps"
175
+ size="small"
176
+ multiline={false}
177
+ label={labels.mainSlotProps + " (JSON)"}
178
+ defaultValue={
179
+ data?.mainSlotProps ? JSON.stringify(data.mainSlotProps) : ""
180
+ }
181
+ helperText='{"required":true}'
182
+ />
183
+ </Grid>
184
+ </Grid>
185
+ );
186
+ }
187
+
188
+ function InputUIs({
189
+ source,
190
+ onChange
191
+ }: {
192
+ source?: string;
193
+ onChange: (items: CustomFieldData[]) => void;
194
+ }) {
195
+ // Global app
196
+ const app = useRequiredAppContext();
197
+
198
+ // Labels
199
+ const labels = app.getLabels(
200
+ "add",
201
+ "delete",
202
+ "edit",
203
+ "sortTip",
204
+ "dragIndicator"
205
+ );
206
+
207
+ const [items, setItems] = React.useState<CustomFieldData[]>([]);
208
+
209
+ const doChange = (items: CustomFieldData[]) => {
210
+ setItems(items);
211
+ onChange(items);
212
+ };
213
+
214
+ const editItem = (item?: CustomFieldData) => {
215
+ app.showInputDialog({
216
+ title: item ? labels.edit : labels.add,
217
+ message: "",
218
+ callback: async (form) => {
219
+ // Cancelled
220
+ if (form == null) {
221
+ return;
222
+ }
223
+
224
+ // Validate form
225
+ if (!form.reportValidity()) {
226
+ return false;
227
+ }
228
+
229
+ // Form data
230
+ const {
231
+ typeInput: type,
232
+ spaceInput: space,
233
+ name,
234
+ label,
235
+ helperText,
236
+ options,
237
+ refs,
238
+ gridItemProps,
239
+ mainSlotProps
240
+ } = DomUtils.dataAs(new FormData(form), {
241
+ typeInput: "string",
242
+ spaceInput: "string",
243
+ name: "string",
244
+ label: "string",
245
+ helperText: "string",
246
+ options: "string",
247
+ refs: "string",
248
+ gridItemProps: "string",
249
+ mainSlotProps: "string"
250
+ });
251
+
252
+ if (!type || !space || !name) {
253
+ return app.get("noData");
254
+ }
255
+
256
+ if (!isCamelCase(name)) {
257
+ DomUtils.setFocus("name", form);
258
+ return app.get("invalidNaming") + " (camelCase)";
259
+ }
260
+
261
+ if (type !== "divider" && !label) {
262
+ DomUtils.setFocus("label", form);
263
+ return false;
264
+ }
265
+
266
+ if (item == null && items.some((item) => item.name === name)) {
267
+ return app.get("itemExists")?.format(name);
268
+ }
269
+
270
+ const optionsJson = options
271
+ ? options.split("\n").map((line) => {
272
+ const [id, ...labelParts] = line.split("=");
273
+ return { id, label: labelParts.join("=") };
274
+ })
275
+ : undefined;
276
+
277
+ const refsJson = refs ? JSON.parse(refs) : undefined;
278
+ const gridItemPropsJson = gridItemProps
279
+ ? JSON.parse(gridItemProps)
280
+ : undefined;
281
+ const mainSlotPropsJson = mainSlotProps
282
+ ? JSON.parse(mainSlotProps)
283
+ : undefined;
284
+
285
+ if (item == null) {
286
+ const newItem: CustomFieldData = {
287
+ type,
288
+ name,
289
+ space: space as CustomFieldData["space"],
290
+ label,
291
+ helperText,
292
+ options: optionsJson,
293
+ refs: refsJson,
294
+ gridItemProps: gridItemPropsJson,
295
+ mainSlotProps: mainSlotPropsJson
296
+ };
297
+
298
+ doChange([...items, newItem]);
299
+ } else {
300
+ item.type = type;
301
+ item.space = space as CustomFieldData["space"];
302
+ item.name = name;
303
+ item.label = label;
304
+ item.helperText = helperText;
305
+ item.options = optionsJson;
306
+ item.refs = refsJson;
307
+ item.gridItemProps = gridItemPropsJson;
308
+ item.mainSlotProps = mainSlotPropsJson;
309
+
310
+ doChange([...items]);
311
+ }
312
+
313
+ return;
314
+ },
315
+ inputs: <InputItemUIs data={item} />
316
+ });
317
+ };
318
+
319
+ React.useEffect(() => {
320
+ try {
321
+ if (source) {
322
+ const parsed = JSON.parse(source);
323
+ if (Array.isArray(parsed)) {
324
+ setItems(parsed);
325
+ }
326
+ }
327
+ } catch (error) {
328
+ console.error("Failed to parse source:", error);
329
+ }
330
+ }, [source]);
331
+
332
+ return (
333
+ <React.Fragment>
334
+ <HBox marginBottom={0.5} gap={1} alignItems="center">
335
+ <Typography>{labels.sortTip}</Typography>
336
+ <Button
337
+ size="small"
338
+ color="primary"
339
+ variant="outlined"
340
+ startIcon={<AddIcon />}
341
+ onClick={() => editItem()}
342
+ >
343
+ {labels.add}
344
+ </Button>
345
+ </HBox>
346
+ <Card>
347
+ <CardContent>
348
+ <Grid container spacing={0}>
349
+ <DnDSortableList<CustomFieldData>
350
+ items={items}
351
+ idField={(item) => item.name!}
352
+ labelField={(item) => item.label || item.name!}
353
+ onChange={(items) => doChange(items)}
354
+ itemRenderer={(
355
+ data,
356
+ style,
357
+ { sortable: { index }, ref, handleRef }
358
+ ) => (
359
+ <Grid
360
+ container
361
+ size={{ xs: 12, sm: 12 }}
362
+ ref={ref}
363
+ style={style}
364
+ gap={1}
365
+ >
366
+ <Grid size={smallSize}>
367
+ <IconButton
368
+ style={{ cursor: "move" }}
369
+ size="small"
370
+ title={labels.dragIndicator}
371
+ ref={handleRef}
372
+ >
373
+ <DragIndicatorIcon />
374
+ </IconButton>
375
+ </Grid>
376
+ <Grid size={size}>
377
+ {index + 1} - {data.type} / {data.space}
378
+ </Grid>
379
+ <Grid size={size}>
380
+ {data.name} - {data.label}
381
+ </Grid>
382
+ <Grid size={smallSize}>
383
+ <IconButton
384
+ size="small"
385
+ title={labels.delete}
386
+ onClick={() =>
387
+ doChange(
388
+ items.filter((item) => item.name !== data.name)
389
+ )
390
+ }
391
+ >
392
+ <DeleteIcon />
393
+ </IconButton>
394
+ <IconButton
395
+ size="small"
396
+ title={labels.edit}
397
+ onClick={() => editItem(data)}
398
+ >
399
+ <EditIcon />
400
+ </IconButton>
401
+ </Grid>
402
+ </Grid>
403
+ )}
404
+ ></DnDSortableList>
405
+ </Grid>
406
+ </CardContent>
407
+ </Card>
408
+ </React.Fragment>
409
+ );
410
+ }
411
+
412
+ /**
413
+ * Custom attribute area properties
414
+ */
415
+ export type CustomAttributeAreaProps = TextFieldProps & {};
416
+
417
+ /**
418
+ * Custom attribute area
419
+ * @param props Properties
420
+ * @returns Component
421
+ */
422
+ export function CustomAttributeArea(props: CustomAttributeAreaProps) {
423
+ // Global app
424
+ const app = useRequiredAppContext();
425
+
426
+ // Destruct
427
+ const { label = app.get("attributeDefinition"), ...rest } = props;
428
+
429
+ const ref = React.useRef<CustomFieldData[]>([]);
430
+
431
+ const showUI = (input: HTMLInputElement) => {
432
+ app.showInputDialog({
433
+ title: label,
434
+ message: "",
435
+ fullScreen: true,
436
+ callback: (form) => {
437
+ if (form == null) {
438
+ return;
439
+ }
440
+
441
+ input.value = ref.current.length > 0 ? JSON.stringify(ref.current) : "";
442
+ },
443
+ inputs: (
444
+ <InputUIs
445
+ source={input.value}
446
+ onChange={(items) => (ref.current = items)}
447
+ />
448
+ )
449
+ });
450
+ };
451
+
452
+ // Layout
453
+ return <JsonTextField label={label} onEdit={showUI} isArray {...rest} />;
454
+ }
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./app/Labels";
7
7
  export * from "./app/ReactApp";
8
8
  export * from "./app/ServiceApp";
9
9
 
10
+ export * from "./custom/CustomAttributeArea";
10
11
  export * from "./custom/CustomFieldUtils";
11
12
  export * from "./custom/CustomFieldViewer";
12
13
  export * from "./custom/CustomFieldWindow";
@@ -79,6 +80,7 @@ export * from "./InputTipField";
79
80
  export * from "./IntInputField";
80
81
  export * from "./ItemList";
81
82
  export * from "./JsonDataInput";
83
+ export * from "./JsonTextField";
82
84
  export * from "./LineChart";
83
85
  export * from "./LinkEx";
84
86
  export * from "./ListChooser";