@etsoo/materialui 1.3.66 → 1.3.68
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.
- package/__tests__/CustomFields.tsx +62 -1
- package/__tests__/SelectEx.tsx +3 -5
- package/lib/custom/CustomFieldViewer.d.ts +50 -0
- package/lib/custom/CustomFieldViewer.js +54 -0
- package/lib/custom/CustomFieldWindow.d.ts +55 -0
- package/lib/custom/CustomFieldWindow.js +89 -0
- package/lib/custom/FieldSelect.js +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/package.json +2 -2
- package/src/custom/CustomFieldViewer.tsx +138 -0
- package/src/custom/CustomFieldWindow.tsx +192 -0
- package/src/custom/FieldSelect.tsx +1 -0
- package/src/index.ts +2 -0
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
act,
|
|
4
|
+
fireEvent,
|
|
5
|
+
getByText,
|
|
6
|
+
render,
|
|
7
|
+
screen
|
|
8
|
+
} from "@testing-library/react";
|
|
3
9
|
import { CustomFieldData } from "@etsoo/appscript";
|
|
4
10
|
import { CustomFieldUtils } from "../src/custom/CustomFieldUtils";
|
|
5
11
|
import { CustomFieldReactCollection } from "@etsoo/react";
|
|
@@ -91,3 +97,58 @@ it("Render FieldCheckbox", () => {
|
|
|
91
97
|
checkboxItems[1].click();
|
|
92
98
|
});
|
|
93
99
|
});
|
|
100
|
+
|
|
101
|
+
it("Render FieldSelect", async () => {
|
|
102
|
+
// Arrange
|
|
103
|
+
const fields: CustomFieldData[] = [
|
|
104
|
+
{
|
|
105
|
+
type: "select",
|
|
106
|
+
name: "select",
|
|
107
|
+
options: [
|
|
108
|
+
{ id: "a", label: "A" },
|
|
109
|
+
{ id: "b", label: "B" },
|
|
110
|
+
{ id: "c", label: "C" }
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const collections: CustomFieldReactCollection<CustomFieldData> = {};
|
|
116
|
+
|
|
117
|
+
act(() => {
|
|
118
|
+
render(
|
|
119
|
+
<div>
|
|
120
|
+
{CustomFieldUtils.create(
|
|
121
|
+
fields,
|
|
122
|
+
collections,
|
|
123
|
+
(field) => {
|
|
124
|
+
switch (field.type) {
|
|
125
|
+
case "select":
|
|
126
|
+
return "c";
|
|
127
|
+
default:
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
(name, value) => {
|
|
132
|
+
expect(name).toBe("checkbox");
|
|
133
|
+
expect(value).toStrictEqual(["a"]);
|
|
134
|
+
}
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const button = screen.getByRole("combobox");
|
|
141
|
+
|
|
142
|
+
act(() => {
|
|
143
|
+
// Act, click to open the dropdown list
|
|
144
|
+
jest.useFakeTimers();
|
|
145
|
+
fireEvent.mouseDown(button);
|
|
146
|
+
jest.advanceTimersByTime(100);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const input = document.querySelector<HTMLInputElement>("input");
|
|
150
|
+
expect(input?.value).toBe("c");
|
|
151
|
+
|
|
152
|
+
const listItems = document.querySelectorAll("li");
|
|
153
|
+
expect(listItems.length).toBe(3);
|
|
154
|
+
});
|
package/__tests__/SelectEx.tsx
CHANGED
|
@@ -19,7 +19,7 @@ it("Render SelectEx", async () => {
|
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
// Render component
|
|
22
|
-
render(
|
|
22
|
+
const { findByText } = render(
|
|
23
23
|
<SelectEx<T>
|
|
24
24
|
options={options}
|
|
25
25
|
name="test"
|
|
@@ -44,12 +44,11 @@ it("Render SelectEx", async () => {
|
|
|
44
44
|
jest.advanceTimersByTime(100);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
/*
|
|
48
47
|
// Get list item
|
|
49
|
-
const itemName2 = await findByText(
|
|
48
|
+
const itemName2 = await findByText("Name 2");
|
|
50
49
|
expect(itemName2.nodeName).toBe("SPAN");
|
|
51
50
|
|
|
52
|
-
const itemBlank = await findByText(
|
|
51
|
+
const itemBlank = await findByText("---");
|
|
53
52
|
expect(itemBlank.nodeName).toBe("SPAN");
|
|
54
53
|
|
|
55
54
|
act(() => {
|
|
@@ -57,7 +56,6 @@ it("Render SelectEx", async () => {
|
|
|
57
56
|
});
|
|
58
57
|
|
|
59
58
|
expect(itemChangeCallback).toBeCalledTimes(2);
|
|
60
|
-
*/
|
|
61
59
|
});
|
|
62
60
|
|
|
63
61
|
it("Render multiple SelectEx", async () => {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { CustomFieldData } from "@etsoo/appscript";
|
|
2
|
+
import { GridProps, TypographyProps } from "@mui/material";
|
|
3
|
+
/**
|
|
4
|
+
* Custom field viewer props
|
|
5
|
+
* 自定义字段查看器属性
|
|
6
|
+
*/
|
|
7
|
+
export type CustomFieldViewerProps = {
|
|
8
|
+
/**
|
|
9
|
+
* Custom fields
|
|
10
|
+
* 自定义字段
|
|
11
|
+
*/
|
|
12
|
+
fields: CustomFieldData[];
|
|
13
|
+
/**
|
|
14
|
+
* Grid props
|
|
15
|
+
* 网格属性
|
|
16
|
+
*/
|
|
17
|
+
gridProps?: GridProps;
|
|
18
|
+
/**
|
|
19
|
+
* JSON data
|
|
20
|
+
* JSON 数据
|
|
21
|
+
*/
|
|
22
|
+
jsonData: unknown;
|
|
23
|
+
/**
|
|
24
|
+
* Title label props
|
|
25
|
+
* 标题标签属性
|
|
26
|
+
*/
|
|
27
|
+
titleProps?: TypographyProps;
|
|
28
|
+
/**
|
|
29
|
+
* Vertical gap
|
|
30
|
+
* 垂直间距
|
|
31
|
+
*/
|
|
32
|
+
verticalGap?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Value label formatter
|
|
35
|
+
* 值标签格式化
|
|
36
|
+
*/
|
|
37
|
+
valueLabelFormatter?: (value: any, field: CustomFieldData) => string;
|
|
38
|
+
/**
|
|
39
|
+
* Value label props
|
|
40
|
+
* 值标签属性
|
|
41
|
+
*/
|
|
42
|
+
valueProps?: TypographyProps;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Custom field viewer
|
|
46
|
+
* 自定义字段查看器
|
|
47
|
+
* @param props Props
|
|
48
|
+
* @returns Component
|
|
49
|
+
*/
|
|
50
|
+
export declare function CustomFieldViewer(props: CustomFieldViewerProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Grid, Typography } from "@mui/material";
|
|
3
|
+
import { CustomFieldUtils } from "./CustomFieldUtils";
|
|
4
|
+
import { MUGlobal } from "../MUGlobal";
|
|
5
|
+
import { VBox } from "../FlexBox";
|
|
6
|
+
import { DataTypes, Utils } from "@etsoo/shared";
|
|
7
|
+
/**
|
|
8
|
+
* Custom field viewer
|
|
9
|
+
* 自定义字段查看器
|
|
10
|
+
* @param props Props
|
|
11
|
+
* @returns Component
|
|
12
|
+
*/
|
|
13
|
+
export function CustomFieldViewer(props) {
|
|
14
|
+
// Destruct
|
|
15
|
+
const { fields, gridProps, jsonData, titleProps, verticalGap = 0.5, valueLabelFormatter = (value, field) => {
|
|
16
|
+
if (value == null)
|
|
17
|
+
return "";
|
|
18
|
+
if (field.options) {
|
|
19
|
+
const option = field.options.find((o) => o.id === value);
|
|
20
|
+
if (option) {
|
|
21
|
+
return DataTypes.getListItemLabel(option);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === "object") {
|
|
25
|
+
if (value instanceof Date) {
|
|
26
|
+
return value.toLocaleString();
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
return JSON.stringify(value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
return `${value}`;
|
|
34
|
+
}
|
|
35
|
+
}, valueProps } = props;
|
|
36
|
+
const spacing = MUGlobal.half(MUGlobal.pagePaddings);
|
|
37
|
+
const data = typeof jsonData === "string" ? JSON.parse(jsonData) : jsonData;
|
|
38
|
+
if (data == null || typeof data !== "object" || Array.isArray(data)) {
|
|
39
|
+
throw new Error("Invalid JSON data");
|
|
40
|
+
}
|
|
41
|
+
return (_jsx(Grid, { container: true, justifyContent: "left", spacing: spacing, sx: {
|
|
42
|
+
".MuiTypography-subtitle2": {
|
|
43
|
+
fontWeight: "bold"
|
|
44
|
+
}
|
|
45
|
+
}, ...gridProps, children: fields.map((field, index) => {
|
|
46
|
+
// Field name
|
|
47
|
+
const name = field.name;
|
|
48
|
+
if (!name)
|
|
49
|
+
return;
|
|
50
|
+
// Field value
|
|
51
|
+
const value = Utils.getNestedValue(data, name);
|
|
52
|
+
return (_jsx(Grid, { item: true, ...field.gridItemProps, ...CustomFieldUtils.transformSpace(field.space), children: _jsxs(VBox, { gap: verticalGap, children: [_jsx(Typography, { fontWeight: "bold", fontSize: "small", ...titleProps, children: field.label ?? name }), _jsx(Typography, { ...valueProps, children: valueLabelFormatter(value, field) })] }) }, name ?? index));
|
|
53
|
+
}) }));
|
|
54
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CustomFieldData } from "@etsoo/appscript";
|
|
2
|
+
import { GridProps } from "@mui/material";
|
|
3
|
+
import React from "react";
|
|
4
|
+
/**
|
|
5
|
+
* Custom field window props
|
|
6
|
+
* 自定义字段窗口属性
|
|
7
|
+
*/
|
|
8
|
+
export type CustomFieldWindowProps<D extends CustomFieldData> = {
|
|
9
|
+
/**
|
|
10
|
+
* Children creation callback
|
|
11
|
+
* 子元素创建回调
|
|
12
|
+
* @param open Open callback
|
|
13
|
+
* @param label Label
|
|
14
|
+
* @param pc Property count
|
|
15
|
+
* @returns Component
|
|
16
|
+
*/
|
|
17
|
+
children: (open: (customFields: D[], jsonData?: unknown) => void, label: string | undefined, pc: number) => React.ReactNode;
|
|
18
|
+
/**
|
|
19
|
+
* Label
|
|
20
|
+
* 标签
|
|
21
|
+
*/
|
|
22
|
+
label?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Grid props
|
|
25
|
+
* 网格属性
|
|
26
|
+
*/
|
|
27
|
+
gridProps?: GridProps;
|
|
28
|
+
/**
|
|
29
|
+
* JSON data
|
|
30
|
+
* JSON 数据
|
|
31
|
+
*/
|
|
32
|
+
jsonData?: unknown;
|
|
33
|
+
/**
|
|
34
|
+
* Input name
|
|
35
|
+
* 输入框名称
|
|
36
|
+
*/
|
|
37
|
+
inputName?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Input ref
|
|
40
|
+
* 输入框引用
|
|
41
|
+
*/
|
|
42
|
+
inputRef?: React.MutableRefObject<HTMLInputElement | null>;
|
|
43
|
+
/**
|
|
44
|
+
* On update callback
|
|
45
|
+
* 更新回调
|
|
46
|
+
*/
|
|
47
|
+
onUpdate?: (data: Record<string, unknown>) => void;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Custom field window
|
|
51
|
+
* 自定义字段窗口
|
|
52
|
+
* @param props Props
|
|
53
|
+
* @returns Component
|
|
54
|
+
*/
|
|
55
|
+
export declare function CustomFieldWindow<D extends CustomFieldData = CustomFieldData>(props: CustomFieldWindowProps<D>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Utils } from "@etsoo/shared";
|
|
3
|
+
import { Grid, Stack } from "@mui/material";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { globalApp } from "../app/ReactApp";
|
|
6
|
+
import { MUGlobal } from "../MUGlobal";
|
|
7
|
+
import { CustomFieldUtils } from "./CustomFieldUtils";
|
|
8
|
+
function calculateKeys(data) {
|
|
9
|
+
let count = 0;
|
|
10
|
+
for (const key in data) {
|
|
11
|
+
const item = data[key];
|
|
12
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
13
|
+
count += calculateKeys(item);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
count++;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return count;
|
|
20
|
+
}
|
|
21
|
+
function parseJsonData(jsonData) {
|
|
22
|
+
let data = {};
|
|
23
|
+
if (jsonData) {
|
|
24
|
+
try {
|
|
25
|
+
data =
|
|
26
|
+
typeof jsonData === "object"
|
|
27
|
+
? jsonData
|
|
28
|
+
: typeof jsonData === "string"
|
|
29
|
+
? JSON.parse(jsonData)
|
|
30
|
+
: {};
|
|
31
|
+
}
|
|
32
|
+
catch { }
|
|
33
|
+
}
|
|
34
|
+
return [data, calculateKeys(data)];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Custom field window
|
|
38
|
+
* 自定义字段窗口
|
|
39
|
+
* @param props Props
|
|
40
|
+
* @returns Component
|
|
41
|
+
*/
|
|
42
|
+
export function CustomFieldWindow(props) {
|
|
43
|
+
// Validate app
|
|
44
|
+
const app = globalApp;
|
|
45
|
+
if (app == null) {
|
|
46
|
+
throw new Error("No globalApp");
|
|
47
|
+
}
|
|
48
|
+
const { children, label = app.get("jsonData"), gridProps, jsonData, inputName = "jsonData", inputRef, onUpdate } = props;
|
|
49
|
+
const spacing = MUGlobal.half(MUGlobal.pagePaddings);
|
|
50
|
+
const [count, setCount] = React.useState(0);
|
|
51
|
+
const [initData, propertyCount] = parseJsonData(jsonData);
|
|
52
|
+
React.useEffect(() => setCount(propertyCount), [propertyCount]);
|
|
53
|
+
return (_jsxs(React.Fragment, { children: [_jsx("input", { type: "hidden", name: inputName, ref: inputRef }), children((customFields, jsonData) => {
|
|
54
|
+
const collections = {};
|
|
55
|
+
let data = {};
|
|
56
|
+
jsonData ?? (jsonData = inputRef?.current?.value);
|
|
57
|
+
if (jsonData) {
|
|
58
|
+
const [d, pc] = parseJsonData(jsonData);
|
|
59
|
+
data = d;
|
|
60
|
+
setCount(pc);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
data = initData;
|
|
64
|
+
}
|
|
65
|
+
app.notifier.confirm(label, undefined, (value) => {
|
|
66
|
+
if (value) {
|
|
67
|
+
if (inputRef?.current) {
|
|
68
|
+
inputRef.current.value = JSON.stringify(data);
|
|
69
|
+
}
|
|
70
|
+
setCount(calculateKeys(data));
|
|
71
|
+
if (onUpdate)
|
|
72
|
+
onUpdate(data);
|
|
73
|
+
}
|
|
74
|
+
}, {
|
|
75
|
+
fullScreen: app.smDown,
|
|
76
|
+
inputs: (_jsx(Stack, { marginTop: 1.5, children: _jsx(Grid, { container: true, justifyContent: "left", spacing: spacing, sx: {
|
|
77
|
+
".MuiTypography-subtitle2": {
|
|
78
|
+
fontWeight: "bold"
|
|
79
|
+
}
|
|
80
|
+
}, ...gridProps, children: CustomFieldUtils.create(customFields, collections, (field) => {
|
|
81
|
+
if (field.name == null)
|
|
82
|
+
return;
|
|
83
|
+
return Utils.getNestedValue(data, field.name);
|
|
84
|
+
}, (name, value) => {
|
|
85
|
+
Utils.setNestedValue(data, name, value);
|
|
86
|
+
}) }) }))
|
|
87
|
+
});
|
|
88
|
+
}, label, count)] }));
|
|
89
|
+
}
|
|
@@ -37,7 +37,7 @@ export const FieldSelect = ({ field, mref, onChange, defaultValue }) => {
|
|
|
37
37
|
if (!name) {
|
|
38
38
|
return (_jsxs(Typography, { children: ["No name for FieldSelect ", JSON.stringify(field)] }));
|
|
39
39
|
}
|
|
40
|
-
return (_jsx(SelectEx, { label: field.label ?? "", helperText: field.helperText, name: name, options: field.options, fullWidth: true, onChange: (event) => {
|
|
40
|
+
return (_jsx(SelectEx, { label: field.label ?? "", helperText: field.helperText, name: name, options: field.options, fullWidth: true, value: value, onChange: (event) => {
|
|
41
41
|
const value = event.target.value;
|
|
42
42
|
const newValue = typeof value === "string" || typeof value === "number"
|
|
43
43
|
? value
|
package/lib/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export * from "./app/Labels";
|
|
|
9
9
|
export * from "./app/ReactApp";
|
|
10
10
|
export * from "./app/ServiceApp";
|
|
11
11
|
export * from "./custom/CustomFieldUtils";
|
|
12
|
+
export * from "./custom/CustomFieldViewer";
|
|
13
|
+
export * from "./custom/CustomFieldWindow";
|
|
12
14
|
export * from "./messages/MessageUtils";
|
|
13
15
|
export * from "./messages/OperationMessageContainer";
|
|
14
16
|
export * from "./messages/OperationMessageDto";
|
package/lib/index.js
CHANGED
|
@@ -9,6 +9,8 @@ export * from "./app/Labels";
|
|
|
9
9
|
export * from "./app/ReactApp";
|
|
10
10
|
export * from "./app/ServiceApp";
|
|
11
11
|
export * from "./custom/CustomFieldUtils";
|
|
12
|
+
export * from "./custom/CustomFieldViewer";
|
|
13
|
+
export * from "./custom/CustomFieldWindow";
|
|
12
14
|
export * from "./messages/MessageUtils";
|
|
13
15
|
export * from "./messages/OperationMessageContainer";
|
|
14
16
|
export * from "./messages/OperationMessageDto";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@etsoo/materialui",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.68",
|
|
4
4
|
"description": "TypeScript Material-UI Implementation",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@emotion/styled": "^11.11.5",
|
|
53
53
|
"@etsoo/appscript": "^1.4.96",
|
|
54
54
|
"@etsoo/notificationbase": "^1.1.42",
|
|
55
|
-
"@etsoo/react": "^1.7.
|
|
55
|
+
"@etsoo/react": "^1.7.54",
|
|
56
56
|
"@etsoo/shared": "^1.2.41",
|
|
57
57
|
"@mui/icons-material": "^5.15.18",
|
|
58
58
|
"@mui/material": "^5.15.18",
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { CustomFieldData } from "@etsoo/appscript";
|
|
2
|
+
import { Grid, GridProps, Typography, TypographyProps } from "@mui/material";
|
|
3
|
+
import { CustomFieldUtils } from "./CustomFieldUtils";
|
|
4
|
+
import { MUGlobal } from "../MUGlobal";
|
|
5
|
+
import { VBox } from "../FlexBox";
|
|
6
|
+
import { DataTypes, Utils } from "@etsoo/shared";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Custom field viewer props
|
|
10
|
+
* 自定义字段查看器属性
|
|
11
|
+
*/
|
|
12
|
+
export type CustomFieldViewerProps = {
|
|
13
|
+
/**
|
|
14
|
+
* Custom fields
|
|
15
|
+
* 自定义字段
|
|
16
|
+
*/
|
|
17
|
+
fields: CustomFieldData[];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Grid props
|
|
21
|
+
* 网格属性
|
|
22
|
+
*/
|
|
23
|
+
gridProps?: GridProps;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* JSON data
|
|
27
|
+
* JSON 数据
|
|
28
|
+
*/
|
|
29
|
+
jsonData: unknown;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Title label props
|
|
33
|
+
* 标题标签属性
|
|
34
|
+
*/
|
|
35
|
+
titleProps?: TypographyProps;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Vertical gap
|
|
39
|
+
* 垂直间距
|
|
40
|
+
*/
|
|
41
|
+
verticalGap?: number;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Value label formatter
|
|
45
|
+
* 值标签格式化
|
|
46
|
+
*/
|
|
47
|
+
valueLabelFormatter?: (value: any, field: CustomFieldData) => string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Value label props
|
|
51
|
+
* 值标签属性
|
|
52
|
+
*/
|
|
53
|
+
valueProps?: TypographyProps;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Custom field viewer
|
|
58
|
+
* 自定义字段查看器
|
|
59
|
+
* @param props Props
|
|
60
|
+
* @returns Component
|
|
61
|
+
*/
|
|
62
|
+
export function CustomFieldViewer(props: CustomFieldViewerProps) {
|
|
63
|
+
// Destruct
|
|
64
|
+
const {
|
|
65
|
+
fields,
|
|
66
|
+
gridProps,
|
|
67
|
+
jsonData,
|
|
68
|
+
titleProps,
|
|
69
|
+
verticalGap = 0.5,
|
|
70
|
+
valueLabelFormatter = (value, field) => {
|
|
71
|
+
if (value == null) return "";
|
|
72
|
+
if (field.options) {
|
|
73
|
+
const option = field.options.find((o) => o.id === value);
|
|
74
|
+
if (option) {
|
|
75
|
+
return DataTypes.getListItemLabel(option);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof value === "object") {
|
|
80
|
+
if (value instanceof Date) {
|
|
81
|
+
return value.toLocaleString();
|
|
82
|
+
} else {
|
|
83
|
+
return JSON.stringify(value);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
return `${value}`;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
valueProps
|
|
90
|
+
} = props;
|
|
91
|
+
|
|
92
|
+
const spacing = MUGlobal.half(MUGlobal.pagePaddings);
|
|
93
|
+
const data = typeof jsonData === "string" ? JSON.parse(jsonData) : jsonData;
|
|
94
|
+
if (data == null || typeof data !== "object" || Array.isArray(data)) {
|
|
95
|
+
throw new Error("Invalid JSON data");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Grid
|
|
100
|
+
container
|
|
101
|
+
justifyContent="left"
|
|
102
|
+
spacing={spacing}
|
|
103
|
+
sx={{
|
|
104
|
+
".MuiTypography-subtitle2": {
|
|
105
|
+
fontWeight: "bold"
|
|
106
|
+
}
|
|
107
|
+
}}
|
|
108
|
+
{...gridProps}
|
|
109
|
+
>
|
|
110
|
+
{fields.map((field, index) => {
|
|
111
|
+
// Field name
|
|
112
|
+
const name = field.name;
|
|
113
|
+
if (!name) return;
|
|
114
|
+
|
|
115
|
+
// Field value
|
|
116
|
+
const value = Utils.getNestedValue(data, name);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<Grid
|
|
120
|
+
item
|
|
121
|
+
key={name ?? index}
|
|
122
|
+
{...field.gridItemProps}
|
|
123
|
+
{...CustomFieldUtils.transformSpace(field.space)}
|
|
124
|
+
>
|
|
125
|
+
<VBox gap={verticalGap}>
|
|
126
|
+
<Typography fontWeight="bold" fontSize="small" {...titleProps}>
|
|
127
|
+
{field.label ?? name}
|
|
128
|
+
</Typography>
|
|
129
|
+
<Typography {...valueProps}>
|
|
130
|
+
{valueLabelFormatter(value, field)}
|
|
131
|
+
</Typography>
|
|
132
|
+
</VBox>
|
|
133
|
+
</Grid>
|
|
134
|
+
);
|
|
135
|
+
})}
|
|
136
|
+
</Grid>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { CustomFieldData } from "@etsoo/appscript";
|
|
2
|
+
import { CustomFieldReactCollection } from "@etsoo/react";
|
|
3
|
+
import { Utils } from "@etsoo/shared";
|
|
4
|
+
import { Grid, GridProps, Stack } from "@mui/material";
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { globalApp } from "../app/ReactApp";
|
|
7
|
+
import { MUGlobal } from "../MUGlobal";
|
|
8
|
+
import { CustomFieldUtils } from "./CustomFieldUtils";
|
|
9
|
+
|
|
10
|
+
function calculateKeys(data: Record<string, unknown>) {
|
|
11
|
+
let count = 0;
|
|
12
|
+
for (const key in data) {
|
|
13
|
+
const item = data[key];
|
|
14
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
15
|
+
count += calculateKeys(item as Record<string, unknown>);
|
|
16
|
+
} else {
|
|
17
|
+
count++;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return count;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseJsonData(jsonData: unknown): [Record<string, unknown>, number] {
|
|
24
|
+
let data: Record<string, unknown> = {};
|
|
25
|
+
if (jsonData) {
|
|
26
|
+
try {
|
|
27
|
+
data =
|
|
28
|
+
typeof jsonData === "object"
|
|
29
|
+
? jsonData
|
|
30
|
+
: typeof jsonData === "string"
|
|
31
|
+
? JSON.parse(jsonData)
|
|
32
|
+
: {};
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
return [data, calculateKeys(data)];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Custom field window props
|
|
40
|
+
* 自定义字段窗口属性
|
|
41
|
+
*/
|
|
42
|
+
export type CustomFieldWindowProps<D extends CustomFieldData> = {
|
|
43
|
+
/**
|
|
44
|
+
* Children creation callback
|
|
45
|
+
* 子元素创建回调
|
|
46
|
+
* @param open Open callback
|
|
47
|
+
* @param label Label
|
|
48
|
+
* @param pc Property count
|
|
49
|
+
* @returns Component
|
|
50
|
+
*/
|
|
51
|
+
children: (
|
|
52
|
+
open: (customFields: D[], jsonData?: unknown) => void,
|
|
53
|
+
label: string | undefined,
|
|
54
|
+
pc: number
|
|
55
|
+
) => React.ReactNode;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Label
|
|
59
|
+
* 标签
|
|
60
|
+
*/
|
|
61
|
+
label?: string;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Grid props
|
|
65
|
+
* 网格属性
|
|
66
|
+
*/
|
|
67
|
+
gridProps?: GridProps;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* JSON data
|
|
71
|
+
* JSON 数据
|
|
72
|
+
*/
|
|
73
|
+
jsonData?: unknown;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Input name
|
|
77
|
+
* 输入框名称
|
|
78
|
+
*/
|
|
79
|
+
inputName?: string;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Input ref
|
|
83
|
+
* 输入框引用
|
|
84
|
+
*/
|
|
85
|
+
inputRef?: React.MutableRefObject<HTMLInputElement | null>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* On update callback
|
|
89
|
+
* 更新回调
|
|
90
|
+
*/
|
|
91
|
+
onUpdate?: (data: Record<string, unknown>) => void;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Custom field window
|
|
96
|
+
* 自定义字段窗口
|
|
97
|
+
* @param props Props
|
|
98
|
+
* @returns Component
|
|
99
|
+
*/
|
|
100
|
+
export function CustomFieldWindow<D extends CustomFieldData = CustomFieldData>(
|
|
101
|
+
props: CustomFieldWindowProps<D>
|
|
102
|
+
) {
|
|
103
|
+
// Validate app
|
|
104
|
+
const app = globalApp;
|
|
105
|
+
if (app == null) {
|
|
106
|
+
throw new Error("No globalApp");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const {
|
|
110
|
+
children,
|
|
111
|
+
label = app.get("jsonData"),
|
|
112
|
+
gridProps,
|
|
113
|
+
jsonData,
|
|
114
|
+
inputName = "jsonData",
|
|
115
|
+
inputRef,
|
|
116
|
+
onUpdate
|
|
117
|
+
} = props;
|
|
118
|
+
|
|
119
|
+
const spacing = MUGlobal.half(MUGlobal.pagePaddings);
|
|
120
|
+
|
|
121
|
+
const [count, setCount] = React.useState(0);
|
|
122
|
+
|
|
123
|
+
const [initData, propertyCount] = parseJsonData(jsonData);
|
|
124
|
+
|
|
125
|
+
React.useEffect(() => setCount(propertyCount), [propertyCount]);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<React.Fragment>
|
|
129
|
+
<input type="hidden" name={inputName} ref={inputRef} />
|
|
130
|
+
{children(
|
|
131
|
+
(customFields, jsonData) => {
|
|
132
|
+
const collections: CustomFieldReactCollection<D> = {};
|
|
133
|
+
let data: Record<string, unknown> = {};
|
|
134
|
+
jsonData ??= inputRef?.current?.value;
|
|
135
|
+
if (jsonData) {
|
|
136
|
+
const [d, pc] = parseJsonData(jsonData);
|
|
137
|
+
data = d;
|
|
138
|
+
setCount(pc);
|
|
139
|
+
} else {
|
|
140
|
+
data = initData;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
app.notifier.confirm(
|
|
144
|
+
label,
|
|
145
|
+
undefined,
|
|
146
|
+
(value) => {
|
|
147
|
+
if (value) {
|
|
148
|
+
if (inputRef?.current) {
|
|
149
|
+
inputRef.current.value = JSON.stringify(data);
|
|
150
|
+
}
|
|
151
|
+
setCount(calculateKeys(data));
|
|
152
|
+
if (onUpdate) onUpdate(data);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
fullScreen: app.smDown,
|
|
157
|
+
inputs: (
|
|
158
|
+
<Stack marginTop={1.5}>
|
|
159
|
+
<Grid
|
|
160
|
+
container
|
|
161
|
+
justifyContent="left"
|
|
162
|
+
spacing={spacing}
|
|
163
|
+
sx={{
|
|
164
|
+
".MuiTypography-subtitle2": {
|
|
165
|
+
fontWeight: "bold"
|
|
166
|
+
}
|
|
167
|
+
}}
|
|
168
|
+
{...gridProps}
|
|
169
|
+
>
|
|
170
|
+
{CustomFieldUtils.create(
|
|
171
|
+
customFields,
|
|
172
|
+
collections,
|
|
173
|
+
(field) => {
|
|
174
|
+
if (field.name == null) return;
|
|
175
|
+
return Utils.getNestedValue(data, field.name);
|
|
176
|
+
},
|
|
177
|
+
(name, value) => {
|
|
178
|
+
Utils.setNestedValue(data, name, value);
|
|
179
|
+
}
|
|
180
|
+
)}
|
|
181
|
+
</Grid>
|
|
182
|
+
</Stack>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
},
|
|
187
|
+
label,
|
|
188
|
+
count
|
|
189
|
+
)}
|
|
190
|
+
</React.Fragment>
|
|
191
|
+
);
|
|
192
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,8 @@ export * from "./app/ReactApp";
|
|
|
10
10
|
export * from "./app/ServiceApp";
|
|
11
11
|
|
|
12
12
|
export * from "./custom/CustomFieldUtils";
|
|
13
|
+
export * from "./custom/CustomFieldViewer";
|
|
14
|
+
export * from "./custom/CustomFieldWindow";
|
|
13
15
|
|
|
14
16
|
export * from "./messages/MessageUtils";
|
|
15
17
|
export * from "./messages/OperationMessageContainer";
|