@etsoo/materialui 1.6.12 → 1.6.14

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.
@@ -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";