@etsoo/materialui 1.4.89 → 1.4.91
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/lib/cjs/index.d.ts +1 -0
- package/lib/cjs/index.js +1 -0
- package/lib/cjs/pages/ViewPage.d.ts +39 -3
- package/lib/cjs/pages/ViewPage.js +139 -34
- package/lib/cjs/useCurrentBreakpoint.d.ts +6 -0
- package/lib/cjs/useCurrentBreakpoint.js +19 -0
- package/lib/mjs/index.d.ts +1 -0
- package/lib/mjs/index.js +1 -0
- package/lib/mjs/pages/ViewPage.d.ts +39 -3
- package/lib/mjs/pages/ViewPage.js +138 -34
- package/lib/mjs/useCurrentBreakpoint.d.ts +6 -0
- package/lib/mjs/useCurrentBreakpoint.js +16 -0
- package/package.json +1 -1
- package/src/app/ReactApp.ts +0 -1
- package/src/index.ts +1 -0
- package/src/pages/ViewPage.tsx +223 -46
- package/src/useCurrentBreakpoint.ts +17 -0
package/lib/cjs/index.d.ts
CHANGED
|
@@ -112,6 +112,7 @@ export * from "./TiplistPro";
|
|
|
112
112
|
export * from "./TagList";
|
|
113
113
|
export * from "./TagListPro";
|
|
114
114
|
export * from "./TwoFieldInput";
|
|
115
|
+
export * from "./useCurrentBreakpoint";
|
|
115
116
|
export * from "./TooltipClick";
|
|
116
117
|
export * from "./UserAvatar";
|
|
117
118
|
export * from "./UserAvatarEditor";
|
package/lib/cjs/index.js
CHANGED
|
@@ -128,6 +128,7 @@ __exportStar(require("./TiplistPro"), exports);
|
|
|
128
128
|
__exportStar(require("./TagList"), exports);
|
|
129
129
|
__exportStar(require("./TagListPro"), exports);
|
|
130
130
|
__exportStar(require("./TwoFieldInput"), exports);
|
|
131
|
+
__exportStar(require("./useCurrentBreakpoint"), exports);
|
|
131
132
|
__exportStar(require("./TooltipClick"), exports);
|
|
132
133
|
__exportStar(require("./UserAvatar"), exports);
|
|
133
134
|
__exportStar(require("./UserAvatarEditor"), exports);
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { GridColumnRenderProps, GridDataType } from "@etsoo/react";
|
|
2
2
|
import { DataTypes } from "@etsoo/shared";
|
|
3
|
-
import { Grid2Props } from "@mui/material";
|
|
3
|
+
import { Breakpoint, Grid2Props } from "@mui/material";
|
|
4
4
|
import React from "react";
|
|
5
5
|
import { CommonPageProps } from "./CommonPage";
|
|
6
6
|
import type { OperationMessageHandlerAll } from "../messages/OperationMessageHandler";
|
|
7
|
+
/**
|
|
8
|
+
* View page item size
|
|
9
|
+
*/
|
|
10
|
+
export type ViewPageItemSize = Record<Breakpoint, number | undefined>;
|
|
11
|
+
/**
|
|
12
|
+
* View page grid item size
|
|
13
|
+
*/
|
|
14
|
+
export declare namespace ViewPageSize {
|
|
15
|
+
const medium: ViewPageItemSize;
|
|
16
|
+
const line: ViewPageItemSize;
|
|
17
|
+
const small: ViewPageItemSize;
|
|
18
|
+
const smallLine: ViewPageItemSize;
|
|
19
|
+
function matchSize(size: ViewPageItemSize): {
|
|
20
|
+
[k: string]: number | undefined;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
7
23
|
/**
|
|
8
24
|
* View page grid item properties
|
|
9
25
|
*/
|
|
@@ -21,7 +37,7 @@ export declare function ViewPageGridItem(props: ViewPageGridItemProps): import("
|
|
|
21
37
|
/**
|
|
22
38
|
* View page row width type
|
|
23
39
|
*/
|
|
24
|
-
export type ViewPageRowType = boolean | "default" | "small" | "medium" |
|
|
40
|
+
export type ViewPageRowType = boolean | "default" | "small" | "medium" | ViewPageItemSize;
|
|
25
41
|
/**
|
|
26
42
|
* View page display field
|
|
27
43
|
*/
|
|
@@ -51,7 +67,7 @@ type ViewPageFieldTypeNarrow<T extends object> = (string & keyof T) | [string &
|
|
|
51
67
|
/**
|
|
52
68
|
* View page field type
|
|
53
69
|
*/
|
|
54
|
-
export type ViewPageFieldType<T extends object> = ViewPageFieldTypeNarrow<T> | ((data: T, refresh: () => Promise<void>) => React.ReactNode);
|
|
70
|
+
export type ViewPageFieldType<T extends object> = ViewPageFieldTypeNarrow<T> | ((data: T, refresh: () => Promise<void>) => React.ReactNode | [React.ReactNode, ViewPageItemSize]);
|
|
55
71
|
/**
|
|
56
72
|
* View page props
|
|
57
73
|
*/
|
|
@@ -95,6 +111,26 @@ export interface ViewPageProps<T extends DataTypes.StringRecord> extends Omit<Co
|
|
|
95
111
|
id: number;
|
|
96
112
|
types: string[];
|
|
97
113
|
};
|
|
114
|
+
/**
|
|
115
|
+
* Title bar
|
|
116
|
+
* @param data Data to render
|
|
117
|
+
* @returns
|
|
118
|
+
*/
|
|
119
|
+
titleBar?: (data: T) => React.ReactNode;
|
|
120
|
+
/**
|
|
121
|
+
* Left container
|
|
122
|
+
*/
|
|
123
|
+
leftContainer?: (data: T) => React.ReactNode;
|
|
124
|
+
/**
|
|
125
|
+
* Left container height in lines
|
|
126
|
+
*/
|
|
127
|
+
leftContainerLines?: number;
|
|
128
|
+
/**
|
|
129
|
+
* Left container properties
|
|
130
|
+
*/
|
|
131
|
+
leftContainerProps?: Omit<Grid2Props, "size"> & {
|
|
132
|
+
size?: ViewPageItemSize;
|
|
133
|
+
};
|
|
98
134
|
}
|
|
99
135
|
/**
|
|
100
136
|
* View page
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ViewPageSize = void 0;
|
|
6
7
|
exports.ViewPageGridItem = ViewPageGridItem;
|
|
7
8
|
exports.ViewPage = ViewPage;
|
|
8
9
|
const react_1 = require("react");
|
|
@@ -19,6 +20,43 @@ const CommonPage_1 = require("./CommonPage");
|
|
|
19
20
|
const MessageUtils_1 = require("../messages/MessageUtils");
|
|
20
21
|
const OperationMessageContainer_1 = require("../messages/OperationMessageContainer");
|
|
21
22
|
const ReactApp_1 = require("../app/ReactApp");
|
|
23
|
+
const useCurrentBreakpoint_1 = require("../useCurrentBreakpoint");
|
|
24
|
+
const breakpoints = ["xs", "sm", "md", "lg", "xl"];
|
|
25
|
+
/**
|
|
26
|
+
* View page grid item size
|
|
27
|
+
*/
|
|
28
|
+
var ViewPageSize;
|
|
29
|
+
(function (ViewPageSize) {
|
|
30
|
+
ViewPageSize.medium = {
|
|
31
|
+
xs: 12,
|
|
32
|
+
sm: 12,
|
|
33
|
+
md: 6,
|
|
34
|
+
lg: 4,
|
|
35
|
+
xl: 3
|
|
36
|
+
};
|
|
37
|
+
ViewPageSize.line = {
|
|
38
|
+
xs: 12,
|
|
39
|
+
sm: 12,
|
|
40
|
+
md: 12,
|
|
41
|
+
lg: 12,
|
|
42
|
+
xl: 12
|
|
43
|
+
};
|
|
44
|
+
ViewPageSize.small = { xs: 6, sm: 6, md: 4, lg: 3, xl: 2 };
|
|
45
|
+
ViewPageSize.smallLine = {
|
|
46
|
+
xs: 12,
|
|
47
|
+
sm: 6,
|
|
48
|
+
md: 4,
|
|
49
|
+
lg: 3,
|
|
50
|
+
xl: 2
|
|
51
|
+
};
|
|
52
|
+
function matchSize(size) {
|
|
53
|
+
return Object.fromEntries(Object.entries(size).map(([key, value]) => [
|
|
54
|
+
key,
|
|
55
|
+
value == null ? undefined : value === 12 ? 12 : 12 - value
|
|
56
|
+
]));
|
|
57
|
+
}
|
|
58
|
+
ViewPageSize.matchSize = matchSize;
|
|
59
|
+
})(ViewPageSize || (exports.ViewPageSize = ViewPageSize = {}));
|
|
22
60
|
/**
|
|
23
61
|
* View page grid item
|
|
24
62
|
* @param props Props
|
|
@@ -51,33 +89,32 @@ function getResp(singleRow) {
|
|
|
51
89
|
const size = typeof singleRow === "object"
|
|
52
90
|
? singleRow
|
|
53
91
|
: singleRow === "medium"
|
|
54
|
-
?
|
|
92
|
+
? ViewPageSize.medium
|
|
55
93
|
: singleRow === true
|
|
56
|
-
?
|
|
57
|
-
:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
lg: 3,
|
|
62
|
-
xl: 2
|
|
63
|
-
};
|
|
64
|
-
return { size };
|
|
94
|
+
? ViewPageSize.line
|
|
95
|
+
: singleRow === false
|
|
96
|
+
? ViewPageSize.smallLine
|
|
97
|
+
: ViewPageSize.small;
|
|
98
|
+
return size;
|
|
65
99
|
}
|
|
66
100
|
function getItemField(app, field, data) {
|
|
67
101
|
// Item data and label
|
|
68
|
-
let itemData, itemLabel, gridProps = {};
|
|
102
|
+
let itemData, itemLabel, gridProps = {}, size;
|
|
69
103
|
if (Array.isArray(field)) {
|
|
70
104
|
const [fieldData, fieldType, renderProps, singleRow = "small"] = field;
|
|
71
105
|
itemData = (0, GridDataFormat_1.GridDataFormat)(data[fieldData], fieldType, renderProps);
|
|
72
106
|
itemLabel = app.get(fieldData) ?? fieldData;
|
|
73
|
-
|
|
107
|
+
size = getResp(singleRow);
|
|
108
|
+
gridProps = { size };
|
|
74
109
|
}
|
|
75
110
|
else if (typeof field === "object") {
|
|
76
111
|
// Destruct
|
|
77
112
|
const { data: fieldData, dataType, label: fieldLabel, renderProps, singleRow = "default", ...rest } = field;
|
|
113
|
+
// Size
|
|
114
|
+
size = getResp(singleRow);
|
|
78
115
|
gridProps = {
|
|
79
116
|
...rest,
|
|
80
|
-
|
|
117
|
+
size
|
|
81
118
|
};
|
|
82
119
|
// Field data
|
|
83
120
|
if (typeof fieldData === "function")
|
|
@@ -88,17 +125,36 @@ function getItemField(app, field, data) {
|
|
|
88
125
|
itemData = (0, GridDataFormat_1.GridDataFormat)(data[fieldData], dataType, renderProps);
|
|
89
126
|
// Field label
|
|
90
127
|
itemLabel =
|
|
91
|
-
|
|
92
|
-
?
|
|
93
|
-
: fieldLabel
|
|
94
|
-
? app.get(
|
|
95
|
-
: fieldLabel
|
|
128
|
+
fieldLabel === ""
|
|
129
|
+
? undefined
|
|
130
|
+
: fieldLabel == null && typeof fieldData === "string"
|
|
131
|
+
? app.get(fieldData) ?? fieldData
|
|
132
|
+
: typeof fieldLabel === "function"
|
|
133
|
+
? fieldLabel(data)
|
|
134
|
+
: fieldLabel != null
|
|
135
|
+
? app.get(fieldLabel) ?? fieldLabel
|
|
136
|
+
: undefined;
|
|
96
137
|
}
|
|
97
138
|
else {
|
|
139
|
+
// Single field format
|
|
98
140
|
itemData = formatItemData(app, data[field]);
|
|
99
141
|
itemLabel = app.get(field) ?? field;
|
|
142
|
+
size = ViewPageSize.small;
|
|
143
|
+
gridProps = { size };
|
|
100
144
|
}
|
|
101
|
-
return [itemData, itemLabel, gridProps];
|
|
145
|
+
return [itemData, itemLabel, gridProps, size];
|
|
146
|
+
}
|
|
147
|
+
function getItemSize(bp, size) {
|
|
148
|
+
const v = size[bp];
|
|
149
|
+
if (v != null)
|
|
150
|
+
return v;
|
|
151
|
+
const index = breakpoints.indexOf(bp);
|
|
152
|
+
for (let i = index; i >= 0; i--) {
|
|
153
|
+
const v = size[breakpoints[i]];
|
|
154
|
+
if (v != null)
|
|
155
|
+
return v;
|
|
156
|
+
}
|
|
157
|
+
return 12;
|
|
102
158
|
}
|
|
103
159
|
/**
|
|
104
160
|
* View page
|
|
@@ -108,19 +164,80 @@ function ViewPage(props) {
|
|
|
108
164
|
// Global app
|
|
109
165
|
const app = (0, ReactApp_1.useRequiredAppContext)();
|
|
110
166
|
// Destruct
|
|
111
|
-
const { actions, children, fields, loadData, paddings = MUGlobal_1.MUGlobal.pagePaddings, spacing = MUGlobal_1.MUGlobal.half(MUGlobal_1.MUGlobal.pagePaddings), supportRefresh = true, fabColumnDirection = true, fabTop = true, supportBack = true, pullToRefresh = true, gridRef, operationMessageHandler, ...rest } = props;
|
|
167
|
+
const { actions, children, fields, loadData, paddings = MUGlobal_1.MUGlobal.pagePaddings, spacing = MUGlobal_1.MUGlobal.half(MUGlobal_1.MUGlobal.pagePaddings), supportRefresh = true, fabColumnDirection = true, fabTop = true, supportBack = true, pullToRefresh = true, gridRef, operationMessageHandler, titleBar, leftContainer, leftContainerLines = 3, leftContainerProps = {}, ...rest } = props;
|
|
168
|
+
// Current breakpoint
|
|
169
|
+
const bp = (0, useCurrentBreakpoint_1.useCurrentBreakpoint)();
|
|
112
170
|
// Data
|
|
113
171
|
const [data, setData] = react_3.default.useState();
|
|
114
172
|
// Labels
|
|
115
173
|
const labels = Labels_1.Labels.CommonPage;
|
|
116
174
|
// Container
|
|
117
175
|
const pullContainer = "#page-container";
|
|
176
|
+
// Left container
|
|
177
|
+
const { size = ViewPageSize.smallLine, ...leftContainerPropsRest } = leftContainerProps;
|
|
118
178
|
// Load data
|
|
119
179
|
const refresh = react_3.default.useCallback(async () => {
|
|
120
180
|
const result = await loadData();
|
|
121
181
|
// When failed or no data returned, show the loading bar
|
|
122
182
|
setData(result);
|
|
123
183
|
}, [loadData]);
|
|
184
|
+
// Create fields
|
|
185
|
+
const fieldIndexRef = react_3.default.useRef(0);
|
|
186
|
+
const createFields = react_3.default.useCallback((data, maxItems = 0) => {
|
|
187
|
+
let validItems = 0;
|
|
188
|
+
const items = [];
|
|
189
|
+
let i = fieldIndexRef.current;
|
|
190
|
+
for (; i < fields.length; i++) {
|
|
191
|
+
const field = fields[i];
|
|
192
|
+
let oneSize;
|
|
193
|
+
let oneItem;
|
|
194
|
+
if (typeof field === "function") {
|
|
195
|
+
// Most flexible way, do whatever you want
|
|
196
|
+
const createdResult = field(data, refresh);
|
|
197
|
+
if (createdResult == null || createdResult === "")
|
|
198
|
+
continue;
|
|
199
|
+
if (Array.isArray(createdResult)) {
|
|
200
|
+
const [created, size] = createdResult;
|
|
201
|
+
oneSize = size;
|
|
202
|
+
oneItem = created;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
oneSize = ViewPageSize.line;
|
|
206
|
+
oneItem = createdResult;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const [itemData, itemLabel, gridProps, size] = getItemField(app, field, data);
|
|
211
|
+
// Some callback function may return '' instead of undefined
|
|
212
|
+
if (itemData == null || itemData === "")
|
|
213
|
+
continue;
|
|
214
|
+
oneSize = size;
|
|
215
|
+
oneItem = ((0, react_1.createElement)(ViewPageGridItem, { ...gridProps, key: i, data: itemData, label: itemLabel }));
|
|
216
|
+
}
|
|
217
|
+
// Max lines
|
|
218
|
+
if (maxItems > 0) {
|
|
219
|
+
const itemSize = getItemSize(bp, oneSize);
|
|
220
|
+
if (maxItems < validItems + itemSize) {
|
|
221
|
+
fieldIndexRef.current = i;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
items.push(oneItem);
|
|
226
|
+
validItems += itemSize;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
items.push(oneItem);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (maxItems === 0) {
|
|
234
|
+
fieldIndexRef.current = 0;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
fieldIndexRef.current = i;
|
|
238
|
+
}
|
|
239
|
+
return items;
|
|
240
|
+
}, [app, refresh, fields, data, bp]);
|
|
124
241
|
react_3.default.useEffect(() => {
|
|
125
242
|
const refreshHandler = async () => {
|
|
126
243
|
await refresh();
|
|
@@ -136,23 +253,11 @@ function ViewPage(props) {
|
|
|
136
253
|
refresh,
|
|
137
254
|
operationMessageHandler.id
|
|
138
255
|
]
|
|
139
|
-
: operationMessageHandler })), (0, jsx_runtime_1.
|
|
256
|
+
: operationMessageHandler })), titleBar && titleBar(data), (0, jsx_runtime_1.jsxs)(material_1.Grid2, { container: true, justifyContent: "left", className: "ET-ViewPage", ref: gridRef, spacing: spacing, sx: {
|
|
140
257
|
".MuiTypography-subtitle2": {
|
|
141
258
|
fontWeight: "bold"
|
|
142
259
|
}
|
|
143
|
-
}, children:
|
|
144
|
-
// Get data
|
|
145
|
-
if (typeof field === "function") {
|
|
146
|
-
// Most flexible way, do whatever you want
|
|
147
|
-
return field(data, refresh);
|
|
148
|
-
}
|
|
149
|
-
const [itemData, itemLabel, gridProps] = getItemField(app, field, data);
|
|
150
|
-
// Some callback function may return '' instead of undefined
|
|
151
|
-
if (itemData == null || itemData === "")
|
|
152
|
-
return undefined;
|
|
153
|
-
// Layout
|
|
154
|
-
return ((0, react_1.createElement)(ViewPageGridItem, { ...gridProps, key: index, data: itemData, label: itemLabel }));
|
|
155
|
-
}) }), actions !== null && ((0, jsx_runtime_1.jsx)(material_1.Stack, { className: "ET-ViewPage-Actions", direction: "row", width: "100%", flexWrap: "wrap", justifyContent: "flex-end", paddingTop: actions == null ? undefined : paddings, paddingBottom: paddings, gap: paddings, children: actions != null && shared_1.Utils.getResult(actions, data, refresh) })), shared_1.Utils.getResult(children, data, refresh), pullToRefresh && ((0, jsx_runtime_1.jsx)(PullToRefreshUI_1.PullToRefreshUI, { mainElement: pullContainer, triggerElement: pullContainer, instructionsPullToRefresh: labels.pullToRefresh, instructionsReleaseToRefresh: labels.releaseToRefresh, instructionsRefreshing: labels.refreshing, onRefresh: refresh, shouldPullToRefresh: () => {
|
|
260
|
+
}, children: [leftContainer && ((0, jsx_runtime_1.jsxs)(react_3.default.Fragment, { children: [(0, jsx_runtime_1.jsx)(material_1.Grid2, { container: true, className: "ET-ViewPage-LeftContainer", spacing: spacing, size: size, ...leftContainerPropsRest, children: leftContainer(data) }), (0, jsx_runtime_1.jsx)(material_1.Grid2, { container: true, className: "ET-ViewPage-LeftOthers", spacing: spacing, size: ViewPageSize.matchSize(size), children: createFields(data, leftContainerLines * (12 - getItemSize(bp, size))) })] })), createFields(data)] }), actions !== null && ((0, jsx_runtime_1.jsx)(material_1.Stack, { className: "ET-ViewPage-Actions", direction: "row", width: "100%", flexWrap: "wrap", justifyContent: "flex-end", paddingTop: actions == null ? undefined : paddings, paddingBottom: paddings, gap: paddings, children: actions != null && shared_1.Utils.getResult(actions, data, refresh) })), shared_1.Utils.getResult(children, data, refresh), pullToRefresh && ((0, jsx_runtime_1.jsx)(PullToRefreshUI_1.PullToRefreshUI, { mainElement: pullContainer, triggerElement: pullContainer, instructionsPullToRefresh: labels.pullToRefresh, instructionsReleaseToRefresh: labels.releaseToRefresh, instructionsRefreshing: labels.refreshing, onRefresh: refresh, shouldPullToRefresh: () => {
|
|
156
261
|
const container = document.querySelector(pullContainer);
|
|
157
262
|
return !container?.scrollTop;
|
|
158
263
|
} })), (0, jsx_runtime_1.jsx)(react_2.ScrollRestoration, {})] })) }));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useCurrentBreakpoint = useCurrentBreakpoint;
|
|
4
|
+
const material_1 = require("@mui/material");
|
|
5
|
+
/**
|
|
6
|
+
* Hook to get the current breakpoint
|
|
7
|
+
* @returns The current breakpoint
|
|
8
|
+
*/
|
|
9
|
+
function useCurrentBreakpoint() {
|
|
10
|
+
const theme = (0, material_1.useTheme)();
|
|
11
|
+
const items = [
|
|
12
|
+
(0, material_1.useMediaQuery)(theme.breakpoints.down("xs")) ? "xs" : null,
|
|
13
|
+
(0, material_1.useMediaQuery)(theme.breakpoints.between("xs", "sm")) ? "sm" : null,
|
|
14
|
+
(0, material_1.useMediaQuery)(theme.breakpoints.between("sm", "md")) ? "md" : null,
|
|
15
|
+
(0, material_1.useMediaQuery)(theme.breakpoints.between("md", "lg")) ? "lg" : null,
|
|
16
|
+
(0, material_1.useMediaQuery)(theme.breakpoints.up("lg")) ? "xl" : null
|
|
17
|
+
];
|
|
18
|
+
return items.find((item) => item != null) ?? "lg";
|
|
19
|
+
}
|
package/lib/mjs/index.d.ts
CHANGED
|
@@ -112,6 +112,7 @@ export * from "./TiplistPro";
|
|
|
112
112
|
export * from "./TagList";
|
|
113
113
|
export * from "./TagListPro";
|
|
114
114
|
export * from "./TwoFieldInput";
|
|
115
|
+
export * from "./useCurrentBreakpoint";
|
|
115
116
|
export * from "./TooltipClick";
|
|
116
117
|
export * from "./UserAvatar";
|
|
117
118
|
export * from "./UserAvatarEditor";
|
package/lib/mjs/index.js
CHANGED
|
@@ -112,6 +112,7 @@ export * from "./TiplistPro";
|
|
|
112
112
|
export * from "./TagList";
|
|
113
113
|
export * from "./TagListPro";
|
|
114
114
|
export * from "./TwoFieldInput";
|
|
115
|
+
export * from "./useCurrentBreakpoint";
|
|
115
116
|
export * from "./TooltipClick";
|
|
116
117
|
export * from "./UserAvatar";
|
|
117
118
|
export * from "./UserAvatarEditor";
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { GridColumnRenderProps, GridDataType } from "@etsoo/react";
|
|
2
2
|
import { DataTypes } from "@etsoo/shared";
|
|
3
|
-
import { Grid2Props } from "@mui/material";
|
|
3
|
+
import { Breakpoint, Grid2Props } from "@mui/material";
|
|
4
4
|
import React from "react";
|
|
5
5
|
import { CommonPageProps } from "./CommonPage";
|
|
6
6
|
import type { OperationMessageHandlerAll } from "../messages/OperationMessageHandler";
|
|
7
|
+
/**
|
|
8
|
+
* View page item size
|
|
9
|
+
*/
|
|
10
|
+
export type ViewPageItemSize = Record<Breakpoint, number | undefined>;
|
|
11
|
+
/**
|
|
12
|
+
* View page grid item size
|
|
13
|
+
*/
|
|
14
|
+
export declare namespace ViewPageSize {
|
|
15
|
+
const medium: ViewPageItemSize;
|
|
16
|
+
const line: ViewPageItemSize;
|
|
17
|
+
const small: ViewPageItemSize;
|
|
18
|
+
const smallLine: ViewPageItemSize;
|
|
19
|
+
function matchSize(size: ViewPageItemSize): {
|
|
20
|
+
[k: string]: number | undefined;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
7
23
|
/**
|
|
8
24
|
* View page grid item properties
|
|
9
25
|
*/
|
|
@@ -21,7 +37,7 @@ export declare function ViewPageGridItem(props: ViewPageGridItemProps): import("
|
|
|
21
37
|
/**
|
|
22
38
|
* View page row width type
|
|
23
39
|
*/
|
|
24
|
-
export type ViewPageRowType = boolean | "default" | "small" | "medium" |
|
|
40
|
+
export type ViewPageRowType = boolean | "default" | "small" | "medium" | ViewPageItemSize;
|
|
25
41
|
/**
|
|
26
42
|
* View page display field
|
|
27
43
|
*/
|
|
@@ -51,7 +67,7 @@ type ViewPageFieldTypeNarrow<T extends object> = (string & keyof T) | [string &
|
|
|
51
67
|
/**
|
|
52
68
|
* View page field type
|
|
53
69
|
*/
|
|
54
|
-
export type ViewPageFieldType<T extends object> = ViewPageFieldTypeNarrow<T> | ((data: T, refresh: () => Promise<void>) => React.ReactNode);
|
|
70
|
+
export type ViewPageFieldType<T extends object> = ViewPageFieldTypeNarrow<T> | ((data: T, refresh: () => Promise<void>) => React.ReactNode | [React.ReactNode, ViewPageItemSize]);
|
|
55
71
|
/**
|
|
56
72
|
* View page props
|
|
57
73
|
*/
|
|
@@ -95,6 +111,26 @@ export interface ViewPageProps<T extends DataTypes.StringRecord> extends Omit<Co
|
|
|
95
111
|
id: number;
|
|
96
112
|
types: string[];
|
|
97
113
|
};
|
|
114
|
+
/**
|
|
115
|
+
* Title bar
|
|
116
|
+
* @param data Data to render
|
|
117
|
+
* @returns
|
|
118
|
+
*/
|
|
119
|
+
titleBar?: (data: T) => React.ReactNode;
|
|
120
|
+
/**
|
|
121
|
+
* Left container
|
|
122
|
+
*/
|
|
123
|
+
leftContainer?: (data: T) => React.ReactNode;
|
|
124
|
+
/**
|
|
125
|
+
* Left container height in lines
|
|
126
|
+
*/
|
|
127
|
+
leftContainerLines?: number;
|
|
128
|
+
/**
|
|
129
|
+
* Left container properties
|
|
130
|
+
*/
|
|
131
|
+
leftContainerProps?: Omit<Grid2Props, "size"> & {
|
|
132
|
+
size?: ViewPageItemSize;
|
|
133
|
+
};
|
|
98
134
|
}
|
|
99
135
|
/**
|
|
100
136
|
* View page
|
|
@@ -12,6 +12,43 @@ import { CommonPage } from "./CommonPage";
|
|
|
12
12
|
import { MessageUtils } from "../messages/MessageUtils";
|
|
13
13
|
import { OperationMessageContainer } from "../messages/OperationMessageContainer";
|
|
14
14
|
import { useRequiredAppContext } from "../app/ReactApp";
|
|
15
|
+
import { useCurrentBreakpoint } from "../useCurrentBreakpoint";
|
|
16
|
+
const breakpoints = ["xs", "sm", "md", "lg", "xl"];
|
|
17
|
+
/**
|
|
18
|
+
* View page grid item size
|
|
19
|
+
*/
|
|
20
|
+
export var ViewPageSize;
|
|
21
|
+
(function (ViewPageSize) {
|
|
22
|
+
ViewPageSize.medium = {
|
|
23
|
+
xs: 12,
|
|
24
|
+
sm: 12,
|
|
25
|
+
md: 6,
|
|
26
|
+
lg: 4,
|
|
27
|
+
xl: 3
|
|
28
|
+
};
|
|
29
|
+
ViewPageSize.line = {
|
|
30
|
+
xs: 12,
|
|
31
|
+
sm: 12,
|
|
32
|
+
md: 12,
|
|
33
|
+
lg: 12,
|
|
34
|
+
xl: 12
|
|
35
|
+
};
|
|
36
|
+
ViewPageSize.small = { xs: 6, sm: 6, md: 4, lg: 3, xl: 2 };
|
|
37
|
+
ViewPageSize.smallLine = {
|
|
38
|
+
xs: 12,
|
|
39
|
+
sm: 6,
|
|
40
|
+
md: 4,
|
|
41
|
+
lg: 3,
|
|
42
|
+
xl: 2
|
|
43
|
+
};
|
|
44
|
+
function matchSize(size) {
|
|
45
|
+
return Object.fromEntries(Object.entries(size).map(([key, value]) => [
|
|
46
|
+
key,
|
|
47
|
+
value == null ? undefined : value === 12 ? 12 : 12 - value
|
|
48
|
+
]));
|
|
49
|
+
}
|
|
50
|
+
ViewPageSize.matchSize = matchSize;
|
|
51
|
+
})(ViewPageSize || (ViewPageSize = {}));
|
|
15
52
|
/**
|
|
16
53
|
* View page grid item
|
|
17
54
|
* @param props Props
|
|
@@ -44,33 +81,32 @@ function getResp(singleRow) {
|
|
|
44
81
|
const size = typeof singleRow === "object"
|
|
45
82
|
? singleRow
|
|
46
83
|
: singleRow === "medium"
|
|
47
|
-
?
|
|
84
|
+
? ViewPageSize.medium
|
|
48
85
|
: singleRow === true
|
|
49
|
-
?
|
|
50
|
-
:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
lg: 3,
|
|
55
|
-
xl: 2
|
|
56
|
-
};
|
|
57
|
-
return { size };
|
|
86
|
+
? ViewPageSize.line
|
|
87
|
+
: singleRow === false
|
|
88
|
+
? ViewPageSize.smallLine
|
|
89
|
+
: ViewPageSize.small;
|
|
90
|
+
return size;
|
|
58
91
|
}
|
|
59
92
|
function getItemField(app, field, data) {
|
|
60
93
|
// Item data and label
|
|
61
|
-
let itemData, itemLabel, gridProps = {};
|
|
94
|
+
let itemData, itemLabel, gridProps = {}, size;
|
|
62
95
|
if (Array.isArray(field)) {
|
|
63
96
|
const [fieldData, fieldType, renderProps, singleRow = "small"] = field;
|
|
64
97
|
itemData = GridDataFormat(data[fieldData], fieldType, renderProps);
|
|
65
98
|
itemLabel = app.get(fieldData) ?? fieldData;
|
|
66
|
-
|
|
99
|
+
size = getResp(singleRow);
|
|
100
|
+
gridProps = { size };
|
|
67
101
|
}
|
|
68
102
|
else if (typeof field === "object") {
|
|
69
103
|
// Destruct
|
|
70
104
|
const { data: fieldData, dataType, label: fieldLabel, renderProps, singleRow = "default", ...rest } = field;
|
|
105
|
+
// Size
|
|
106
|
+
size = getResp(singleRow);
|
|
71
107
|
gridProps = {
|
|
72
108
|
...rest,
|
|
73
|
-
|
|
109
|
+
size
|
|
74
110
|
};
|
|
75
111
|
// Field data
|
|
76
112
|
if (typeof fieldData === "function")
|
|
@@ -81,17 +117,36 @@ function getItemField(app, field, data) {
|
|
|
81
117
|
itemData = GridDataFormat(data[fieldData], dataType, renderProps);
|
|
82
118
|
// Field label
|
|
83
119
|
itemLabel =
|
|
84
|
-
|
|
85
|
-
?
|
|
86
|
-
: fieldLabel
|
|
87
|
-
? app.get(
|
|
88
|
-
: fieldLabel
|
|
120
|
+
fieldLabel === ""
|
|
121
|
+
? undefined
|
|
122
|
+
: fieldLabel == null && typeof fieldData === "string"
|
|
123
|
+
? app.get(fieldData) ?? fieldData
|
|
124
|
+
: typeof fieldLabel === "function"
|
|
125
|
+
? fieldLabel(data)
|
|
126
|
+
: fieldLabel != null
|
|
127
|
+
? app.get(fieldLabel) ?? fieldLabel
|
|
128
|
+
: undefined;
|
|
89
129
|
}
|
|
90
130
|
else {
|
|
131
|
+
// Single field format
|
|
91
132
|
itemData = formatItemData(app, data[field]);
|
|
92
133
|
itemLabel = app.get(field) ?? field;
|
|
134
|
+
size = ViewPageSize.small;
|
|
135
|
+
gridProps = { size };
|
|
93
136
|
}
|
|
94
|
-
return [itemData, itemLabel, gridProps];
|
|
137
|
+
return [itemData, itemLabel, gridProps, size];
|
|
138
|
+
}
|
|
139
|
+
function getItemSize(bp, size) {
|
|
140
|
+
const v = size[bp];
|
|
141
|
+
if (v != null)
|
|
142
|
+
return v;
|
|
143
|
+
const index = breakpoints.indexOf(bp);
|
|
144
|
+
for (let i = index; i >= 0; i--) {
|
|
145
|
+
const v = size[breakpoints[i]];
|
|
146
|
+
if (v != null)
|
|
147
|
+
return v;
|
|
148
|
+
}
|
|
149
|
+
return 12;
|
|
95
150
|
}
|
|
96
151
|
/**
|
|
97
152
|
* View page
|
|
@@ -101,19 +156,80 @@ export function ViewPage(props) {
|
|
|
101
156
|
// Global app
|
|
102
157
|
const app = useRequiredAppContext();
|
|
103
158
|
// Destruct
|
|
104
|
-
const { actions, children, fields, loadData, paddings = MUGlobal.pagePaddings, spacing = MUGlobal.half(MUGlobal.pagePaddings), supportRefresh = true, fabColumnDirection = true, fabTop = true, supportBack = true, pullToRefresh = true, gridRef, operationMessageHandler, ...rest } = props;
|
|
159
|
+
const { actions, children, fields, loadData, paddings = MUGlobal.pagePaddings, spacing = MUGlobal.half(MUGlobal.pagePaddings), supportRefresh = true, fabColumnDirection = true, fabTop = true, supportBack = true, pullToRefresh = true, gridRef, operationMessageHandler, titleBar, leftContainer, leftContainerLines = 3, leftContainerProps = {}, ...rest } = props;
|
|
160
|
+
// Current breakpoint
|
|
161
|
+
const bp = useCurrentBreakpoint();
|
|
105
162
|
// Data
|
|
106
163
|
const [data, setData] = React.useState();
|
|
107
164
|
// Labels
|
|
108
165
|
const labels = Labels.CommonPage;
|
|
109
166
|
// Container
|
|
110
167
|
const pullContainer = "#page-container";
|
|
168
|
+
// Left container
|
|
169
|
+
const { size = ViewPageSize.smallLine, ...leftContainerPropsRest } = leftContainerProps;
|
|
111
170
|
// Load data
|
|
112
171
|
const refresh = React.useCallback(async () => {
|
|
113
172
|
const result = await loadData();
|
|
114
173
|
// When failed or no data returned, show the loading bar
|
|
115
174
|
setData(result);
|
|
116
175
|
}, [loadData]);
|
|
176
|
+
// Create fields
|
|
177
|
+
const fieldIndexRef = React.useRef(0);
|
|
178
|
+
const createFields = React.useCallback((data, maxItems = 0) => {
|
|
179
|
+
let validItems = 0;
|
|
180
|
+
const items = [];
|
|
181
|
+
let i = fieldIndexRef.current;
|
|
182
|
+
for (; i < fields.length; i++) {
|
|
183
|
+
const field = fields[i];
|
|
184
|
+
let oneSize;
|
|
185
|
+
let oneItem;
|
|
186
|
+
if (typeof field === "function") {
|
|
187
|
+
// Most flexible way, do whatever you want
|
|
188
|
+
const createdResult = field(data, refresh);
|
|
189
|
+
if (createdResult == null || createdResult === "")
|
|
190
|
+
continue;
|
|
191
|
+
if (Array.isArray(createdResult)) {
|
|
192
|
+
const [created, size] = createdResult;
|
|
193
|
+
oneSize = size;
|
|
194
|
+
oneItem = created;
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
oneSize = ViewPageSize.line;
|
|
198
|
+
oneItem = createdResult;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const [itemData, itemLabel, gridProps, size] = getItemField(app, field, data);
|
|
203
|
+
// Some callback function may return '' instead of undefined
|
|
204
|
+
if (itemData == null || itemData === "")
|
|
205
|
+
continue;
|
|
206
|
+
oneSize = size;
|
|
207
|
+
oneItem = (_createElement(ViewPageGridItem, { ...gridProps, key: i, data: itemData, label: itemLabel }));
|
|
208
|
+
}
|
|
209
|
+
// Max lines
|
|
210
|
+
if (maxItems > 0) {
|
|
211
|
+
const itemSize = getItemSize(bp, oneSize);
|
|
212
|
+
if (maxItems < validItems + itemSize) {
|
|
213
|
+
fieldIndexRef.current = i;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
items.push(oneItem);
|
|
218
|
+
validItems += itemSize;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
items.push(oneItem);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (maxItems === 0) {
|
|
226
|
+
fieldIndexRef.current = 0;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
fieldIndexRef.current = i;
|
|
230
|
+
}
|
|
231
|
+
return items;
|
|
232
|
+
}, [app, refresh, fields, data, bp]);
|
|
117
233
|
React.useEffect(() => {
|
|
118
234
|
const refreshHandler = async () => {
|
|
119
235
|
await refresh();
|
|
@@ -129,23 +245,11 @@ export function ViewPage(props) {
|
|
|
129
245
|
refresh,
|
|
130
246
|
operationMessageHandler.id
|
|
131
247
|
]
|
|
132
|
-
: operationMessageHandler })),
|
|
248
|
+
: operationMessageHandler })), titleBar && titleBar(data), _jsxs(Grid2, { container: true, justifyContent: "left", className: "ET-ViewPage", ref: gridRef, spacing: spacing, sx: {
|
|
133
249
|
".MuiTypography-subtitle2": {
|
|
134
250
|
fontWeight: "bold"
|
|
135
251
|
}
|
|
136
|
-
}, children:
|
|
137
|
-
// Get data
|
|
138
|
-
if (typeof field === "function") {
|
|
139
|
-
// Most flexible way, do whatever you want
|
|
140
|
-
return field(data, refresh);
|
|
141
|
-
}
|
|
142
|
-
const [itemData, itemLabel, gridProps] = getItemField(app, field, data);
|
|
143
|
-
// Some callback function may return '' instead of undefined
|
|
144
|
-
if (itemData == null || itemData === "")
|
|
145
|
-
return undefined;
|
|
146
|
-
// Layout
|
|
147
|
-
return (_createElement(ViewPageGridItem, { ...gridProps, key: index, data: itemData, label: itemLabel }));
|
|
148
|
-
}) }), actions !== null && (_jsx(Stack, { className: "ET-ViewPage-Actions", direction: "row", width: "100%", flexWrap: "wrap", justifyContent: "flex-end", paddingTop: actions == null ? undefined : paddings, paddingBottom: paddings, gap: paddings, children: actions != null && Utils.getResult(actions, data, refresh) })), Utils.getResult(children, data, refresh), pullToRefresh && (_jsx(PullToRefreshUI, { mainElement: pullContainer, triggerElement: pullContainer, instructionsPullToRefresh: labels.pullToRefresh, instructionsReleaseToRefresh: labels.releaseToRefresh, instructionsRefreshing: labels.refreshing, onRefresh: refresh, shouldPullToRefresh: () => {
|
|
252
|
+
}, children: [leftContainer && (_jsxs(React.Fragment, { children: [_jsx(Grid2, { container: true, className: "ET-ViewPage-LeftContainer", spacing: spacing, size: size, ...leftContainerPropsRest, children: leftContainer(data) }), _jsx(Grid2, { container: true, className: "ET-ViewPage-LeftOthers", spacing: spacing, size: ViewPageSize.matchSize(size), children: createFields(data, leftContainerLines * (12 - getItemSize(bp, size))) })] })), createFields(data)] }), actions !== null && (_jsx(Stack, { className: "ET-ViewPage-Actions", direction: "row", width: "100%", flexWrap: "wrap", justifyContent: "flex-end", paddingTop: actions == null ? undefined : paddings, paddingBottom: paddings, gap: paddings, children: actions != null && Utils.getResult(actions, data, refresh) })), Utils.getResult(children, data, refresh), pullToRefresh && (_jsx(PullToRefreshUI, { mainElement: pullContainer, triggerElement: pullContainer, instructionsPullToRefresh: labels.pullToRefresh, instructionsReleaseToRefresh: labels.releaseToRefresh, instructionsRefreshing: labels.refreshing, onRefresh: refresh, shouldPullToRefresh: () => {
|
|
149
253
|
const container = document.querySelector(pullContainer);
|
|
150
254
|
return !container?.scrollTop;
|
|
151
255
|
} })), _jsx(ScrollRestoration, {})] })) }));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useMediaQuery, useTheme } from "@mui/material";
|
|
2
|
+
/**
|
|
3
|
+
* Hook to get the current breakpoint
|
|
4
|
+
* @returns The current breakpoint
|
|
5
|
+
*/
|
|
6
|
+
export function useCurrentBreakpoint() {
|
|
7
|
+
const theme = useTheme();
|
|
8
|
+
const items = [
|
|
9
|
+
useMediaQuery(theme.breakpoints.down("xs")) ? "xs" : null,
|
|
10
|
+
useMediaQuery(theme.breakpoints.between("xs", "sm")) ? "sm" : null,
|
|
11
|
+
useMediaQuery(theme.breakpoints.between("sm", "md")) ? "md" : null,
|
|
12
|
+
useMediaQuery(theme.breakpoints.between("md", "lg")) ? "lg" : null,
|
|
13
|
+
useMediaQuery(theme.breakpoints.up("lg")) ? "xl" : null
|
|
14
|
+
];
|
|
15
|
+
return items.find((item) => item != null) ?? "lg";
|
|
16
|
+
}
|
package/package.json
CHANGED
package/src/app/ReactApp.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -117,6 +117,7 @@ export * from "./TiplistPro";
|
|
|
117
117
|
export * from "./TagList";
|
|
118
118
|
export * from "./TagListPro";
|
|
119
119
|
export * from "./TwoFieldInput";
|
|
120
|
+
export * from "./useCurrentBreakpoint";
|
|
120
121
|
export * from "./TooltipClick";
|
|
121
122
|
export * from "./UserAvatar";
|
|
122
123
|
export * from "./UserAvatarEditor";
|
package/src/pages/ViewPage.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "@etsoo/react";
|
|
6
6
|
import { DataTypes, Utils } from "@etsoo/shared";
|
|
7
7
|
import {
|
|
8
|
+
Breakpoint,
|
|
8
9
|
Grid2,
|
|
9
10
|
Grid2Props,
|
|
10
11
|
LinearProgress,
|
|
@@ -22,6 +23,50 @@ import { MessageUtils } from "../messages/MessageUtils";
|
|
|
22
23
|
import type { RefreshHandler } from "../messages/RefreshHandler";
|
|
23
24
|
import { OperationMessageContainer } from "../messages/OperationMessageContainer";
|
|
24
25
|
import { ReactAppType, useRequiredAppContext } from "../app/ReactApp";
|
|
26
|
+
import { useCurrentBreakpoint } from "../useCurrentBreakpoint";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* View page item size
|
|
30
|
+
*/
|
|
31
|
+
export type ViewPageItemSize = Record<Breakpoint, number | undefined>;
|
|
32
|
+
|
|
33
|
+
const breakpoints: Breakpoint[] = ["xs", "sm", "md", "lg", "xl"];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* View page grid item size
|
|
37
|
+
*/
|
|
38
|
+
export namespace ViewPageSize {
|
|
39
|
+
export const medium: ViewPageItemSize = {
|
|
40
|
+
xs: 12,
|
|
41
|
+
sm: 12,
|
|
42
|
+
md: 6,
|
|
43
|
+
lg: 4,
|
|
44
|
+
xl: 3
|
|
45
|
+
};
|
|
46
|
+
export const line: ViewPageItemSize = {
|
|
47
|
+
xs: 12,
|
|
48
|
+
sm: 12,
|
|
49
|
+
md: 12,
|
|
50
|
+
lg: 12,
|
|
51
|
+
xl: 12
|
|
52
|
+
};
|
|
53
|
+
export const small: ViewPageItemSize = { xs: 6, sm: 6, md: 4, lg: 3, xl: 2 };
|
|
54
|
+
export const smallLine: ViewPageItemSize = {
|
|
55
|
+
xs: 12,
|
|
56
|
+
sm: 6,
|
|
57
|
+
md: 4,
|
|
58
|
+
lg: 3,
|
|
59
|
+
xl: 2
|
|
60
|
+
};
|
|
61
|
+
export function matchSize(size: ViewPageItemSize) {
|
|
62
|
+
return Object.fromEntries(
|
|
63
|
+
Object.entries(size).map(([key, value]) => [
|
|
64
|
+
key,
|
|
65
|
+
value == null ? undefined : value === 12 ? 12 : 12 - value
|
|
66
|
+
])
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
25
70
|
|
|
26
71
|
/**
|
|
27
72
|
* View page grid item properties
|
|
@@ -69,7 +114,12 @@ export function ViewPageGridItem(props: ViewPageGridItemProps) {
|
|
|
69
114
|
/**
|
|
70
115
|
* View page row width type
|
|
71
116
|
*/
|
|
72
|
-
export type ViewPageRowType =
|
|
117
|
+
export type ViewPageRowType =
|
|
118
|
+
| boolean
|
|
119
|
+
| "default"
|
|
120
|
+
| "small"
|
|
121
|
+
| "medium"
|
|
122
|
+
| ViewPageItemSize;
|
|
73
123
|
|
|
74
124
|
/**
|
|
75
125
|
* View page display field
|
|
@@ -111,7 +161,10 @@ type ViewPageFieldTypeNarrow<T extends object> =
|
|
|
111
161
|
*/
|
|
112
162
|
export type ViewPageFieldType<T extends object> =
|
|
113
163
|
| ViewPageFieldTypeNarrow<T>
|
|
114
|
-
| ((
|
|
164
|
+
| ((
|
|
165
|
+
data: T,
|
|
166
|
+
refresh: () => Promise<void>
|
|
167
|
+
) => React.ReactNode | [React.ReactNode, ViewPageItemSize]);
|
|
115
168
|
|
|
116
169
|
/**
|
|
117
170
|
* View page props
|
|
@@ -168,6 +221,28 @@ export interface ViewPageProps<T extends DataTypes.StringRecord>
|
|
|
168
221
|
operationMessageHandler?:
|
|
169
222
|
| OperationMessageHandlerAll
|
|
170
223
|
| { id: number; types: string[] };
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Title bar
|
|
227
|
+
* @param data Data to render
|
|
228
|
+
* @returns
|
|
229
|
+
*/
|
|
230
|
+
titleBar?: (data: T) => React.ReactNode;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Left container
|
|
234
|
+
*/
|
|
235
|
+
leftContainer?: (data: T) => React.ReactNode;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Left container height in lines
|
|
239
|
+
*/
|
|
240
|
+
leftContainerLines?: number;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Left container properties
|
|
244
|
+
*/
|
|
245
|
+
leftContainerProps?: Omit<Grid2Props, "size"> & { size?: ViewPageItemSize };
|
|
171
246
|
}
|
|
172
247
|
|
|
173
248
|
function formatItemData(
|
|
@@ -185,34 +260,32 @@ function getResp(singleRow: ViewPageRowType) {
|
|
|
185
260
|
typeof singleRow === "object"
|
|
186
261
|
? singleRow
|
|
187
262
|
: singleRow === "medium"
|
|
188
|
-
?
|
|
263
|
+
? ViewPageSize.medium
|
|
189
264
|
: singleRow === true
|
|
190
|
-
?
|
|
191
|
-
:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
lg: 3,
|
|
196
|
-
xl: 2
|
|
197
|
-
};
|
|
198
|
-
return { size };
|
|
265
|
+
? ViewPageSize.line
|
|
266
|
+
: singleRow === false
|
|
267
|
+
? ViewPageSize.smallLine
|
|
268
|
+
: ViewPageSize.small;
|
|
269
|
+
return size;
|
|
199
270
|
}
|
|
200
271
|
|
|
201
272
|
function getItemField<T extends object>(
|
|
202
273
|
app: ReactAppType,
|
|
203
274
|
field: ViewPageFieldTypeNarrow<T>,
|
|
204
275
|
data: T
|
|
205
|
-
): [React.ReactNode, React.ReactNode, Grid2Props] {
|
|
276
|
+
): [React.ReactNode, React.ReactNode, Grid2Props, ViewPageItemSize] {
|
|
206
277
|
// Item data and label
|
|
207
278
|
let itemData: React.ReactNode,
|
|
208
279
|
itemLabel: React.ReactNode,
|
|
209
|
-
gridProps: Grid2Props = {}
|
|
280
|
+
gridProps: Grid2Props = {},
|
|
281
|
+
size: ViewPageItemSize;
|
|
210
282
|
|
|
211
283
|
if (Array.isArray(field)) {
|
|
212
284
|
const [fieldData, fieldType, renderProps, singleRow = "small"] = field;
|
|
213
285
|
itemData = GridDataFormat(data[fieldData], fieldType, renderProps);
|
|
214
286
|
itemLabel = app.get<string>(fieldData) ?? fieldData;
|
|
215
|
-
|
|
287
|
+
size = getResp(singleRow);
|
|
288
|
+
gridProps = { size };
|
|
216
289
|
} else if (typeof field === "object") {
|
|
217
290
|
// Destruct
|
|
218
291
|
const {
|
|
@@ -224,9 +297,12 @@ function getItemField<T extends object>(
|
|
|
224
297
|
...rest
|
|
225
298
|
} = field;
|
|
226
299
|
|
|
300
|
+
// Size
|
|
301
|
+
size = getResp(singleRow);
|
|
302
|
+
|
|
227
303
|
gridProps = {
|
|
228
304
|
...rest,
|
|
229
|
-
|
|
305
|
+
size
|
|
230
306
|
};
|
|
231
307
|
|
|
232
308
|
// Field data
|
|
@@ -236,17 +312,37 @@ function getItemField<T extends object>(
|
|
|
236
312
|
|
|
237
313
|
// Field label
|
|
238
314
|
itemLabel =
|
|
239
|
-
|
|
315
|
+
fieldLabel === ""
|
|
316
|
+
? undefined
|
|
317
|
+
: fieldLabel == null && typeof fieldData === "string"
|
|
318
|
+
? app.get<string>(fieldData) ?? fieldData
|
|
319
|
+
: typeof fieldLabel === "function"
|
|
240
320
|
? fieldLabel(data)
|
|
241
321
|
: fieldLabel != null
|
|
242
322
|
? app.get<string>(fieldLabel) ?? fieldLabel
|
|
243
|
-
:
|
|
323
|
+
: undefined;
|
|
244
324
|
} else {
|
|
325
|
+
// Single field format
|
|
245
326
|
itemData = formatItemData(app, data[field]);
|
|
246
327
|
itemLabel = app.get<string>(field) ?? field;
|
|
328
|
+
size = ViewPageSize.small;
|
|
329
|
+
gridProps = { size };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return [itemData, itemLabel, gridProps, size];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getItemSize(bp: Breakpoint, size: ViewPageItemSize) {
|
|
336
|
+
const v = size[bp];
|
|
337
|
+
if (v != null) return v;
|
|
338
|
+
|
|
339
|
+
const index = breakpoints.indexOf(bp);
|
|
340
|
+
for (let i = index; i >= 0; i--) {
|
|
341
|
+
const v = size[breakpoints[i]];
|
|
342
|
+
if (v != null) return v;
|
|
247
343
|
}
|
|
248
344
|
|
|
249
|
-
return
|
|
345
|
+
return 12;
|
|
250
346
|
}
|
|
251
347
|
|
|
252
348
|
/**
|
|
@@ -274,9 +370,16 @@ export function ViewPage<T extends DataTypes.StringRecord>(
|
|
|
274
370
|
pullToRefresh = true,
|
|
275
371
|
gridRef,
|
|
276
372
|
operationMessageHandler,
|
|
373
|
+
titleBar,
|
|
374
|
+
leftContainer,
|
|
375
|
+
leftContainerLines = 3,
|
|
376
|
+
leftContainerProps = {},
|
|
277
377
|
...rest
|
|
278
378
|
} = props;
|
|
279
379
|
|
|
380
|
+
// Current breakpoint
|
|
381
|
+
const bp = useCurrentBreakpoint();
|
|
382
|
+
|
|
280
383
|
// Data
|
|
281
384
|
const [data, setData] = React.useState<T>();
|
|
282
385
|
|
|
@@ -286,6 +389,10 @@ export function ViewPage<T extends DataTypes.StringRecord>(
|
|
|
286
389
|
// Container
|
|
287
390
|
const pullContainer = "#page-container";
|
|
288
391
|
|
|
392
|
+
// Left container
|
|
393
|
+
const { size = ViewPageSize.smallLine, ...leftContainerPropsRest } =
|
|
394
|
+
leftContainerProps;
|
|
395
|
+
|
|
289
396
|
// Load data
|
|
290
397
|
const refresh = React.useCallback(async () => {
|
|
291
398
|
const result = await loadData();
|
|
@@ -293,6 +400,76 @@ export function ViewPage<T extends DataTypes.StringRecord>(
|
|
|
293
400
|
setData(result);
|
|
294
401
|
}, [loadData]);
|
|
295
402
|
|
|
403
|
+
// Create fields
|
|
404
|
+
const fieldIndexRef = React.useRef(0);
|
|
405
|
+
const createFields = React.useCallback(
|
|
406
|
+
(data: T, maxItems: number = 0) => {
|
|
407
|
+
let validItems = 0;
|
|
408
|
+
const items: React.ReactNode[] = [];
|
|
409
|
+
let i: number = fieldIndexRef.current;
|
|
410
|
+
for (; i < fields.length; i++) {
|
|
411
|
+
const field = fields[i];
|
|
412
|
+
let oneSize: ViewPageItemSize;
|
|
413
|
+
let oneItem: React.ReactNode;
|
|
414
|
+
if (typeof field === "function") {
|
|
415
|
+
// Most flexible way, do whatever you want
|
|
416
|
+
const createdResult = field(data, refresh);
|
|
417
|
+
if (createdResult == null || createdResult === "") continue;
|
|
418
|
+
if (Array.isArray(createdResult)) {
|
|
419
|
+
const [created, size] = createdResult;
|
|
420
|
+
oneSize = size;
|
|
421
|
+
oneItem = created;
|
|
422
|
+
} else {
|
|
423
|
+
oneSize = ViewPageSize.line;
|
|
424
|
+
oneItem = createdResult;
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
const [itemData, itemLabel, gridProps, size] = getItemField(
|
|
428
|
+
app,
|
|
429
|
+
field,
|
|
430
|
+
data
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// Some callback function may return '' instead of undefined
|
|
434
|
+
if (itemData == null || itemData === "") continue;
|
|
435
|
+
|
|
436
|
+
oneSize = size;
|
|
437
|
+
oneItem = (
|
|
438
|
+
<ViewPageGridItem
|
|
439
|
+
{...gridProps}
|
|
440
|
+
key={i}
|
|
441
|
+
data={itemData}
|
|
442
|
+
label={itemLabel}
|
|
443
|
+
/>
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Max lines
|
|
448
|
+
if (maxItems > 0) {
|
|
449
|
+
const itemSize = getItemSize(bp, oneSize);
|
|
450
|
+
if (maxItems < validItems + itemSize) {
|
|
451
|
+
fieldIndexRef.current = i;
|
|
452
|
+
break;
|
|
453
|
+
} else {
|
|
454
|
+
items.push(oneItem);
|
|
455
|
+
validItems += itemSize;
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
items.push(oneItem);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (maxItems === 0) {
|
|
463
|
+
fieldIndexRef.current = 0;
|
|
464
|
+
} else {
|
|
465
|
+
fieldIndexRef.current = i;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return items;
|
|
469
|
+
},
|
|
470
|
+
[app, refresh, fields, data, bp]
|
|
471
|
+
);
|
|
472
|
+
|
|
296
473
|
React.useEffect(() => {
|
|
297
474
|
const refreshHandler: RefreshHandler = async () => {
|
|
298
475
|
await refresh();
|
|
@@ -332,44 +509,44 @@ export function ViewPage<T extends DataTypes.StringRecord>(
|
|
|
332
509
|
}
|
|
333
510
|
/>
|
|
334
511
|
)}
|
|
512
|
+
{titleBar && titleBar(data)}
|
|
335
513
|
<Grid2
|
|
336
514
|
container
|
|
337
515
|
justifyContent="left"
|
|
338
|
-
spacing={spacing}
|
|
339
516
|
className="ET-ViewPage"
|
|
340
517
|
ref={gridRef}
|
|
518
|
+
spacing={spacing}
|
|
341
519
|
sx={{
|
|
342
520
|
".MuiTypography-subtitle2": {
|
|
343
521
|
fontWeight: "bold"
|
|
344
522
|
}
|
|
345
523
|
}}
|
|
346
524
|
>
|
|
347
|
-
{
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
})}
|
|
525
|
+
{leftContainer && (
|
|
526
|
+
<React.Fragment>
|
|
527
|
+
<Grid2
|
|
528
|
+
container
|
|
529
|
+
className="ET-ViewPage-LeftContainer"
|
|
530
|
+
spacing={spacing}
|
|
531
|
+
size={size}
|
|
532
|
+
{...leftContainerPropsRest}
|
|
533
|
+
>
|
|
534
|
+
{leftContainer(data)}
|
|
535
|
+
</Grid2>
|
|
536
|
+
<Grid2
|
|
537
|
+
container
|
|
538
|
+
className="ET-ViewPage-LeftOthers"
|
|
539
|
+
spacing={spacing}
|
|
540
|
+
size={ViewPageSize.matchSize(size)}
|
|
541
|
+
>
|
|
542
|
+
{createFields(
|
|
543
|
+
data,
|
|
544
|
+
leftContainerLines * (12 - getItemSize(bp, size))
|
|
545
|
+
)}
|
|
546
|
+
</Grid2>
|
|
547
|
+
</React.Fragment>
|
|
548
|
+
)}
|
|
549
|
+
{createFields(data)}
|
|
373
550
|
</Grid2>
|
|
374
551
|
{actions !== null && (
|
|
375
552
|
<Stack
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Breakpoint, useMediaQuery, useTheme } from "@mui/material";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook to get the current breakpoint
|
|
5
|
+
* @returns The current breakpoint
|
|
6
|
+
*/
|
|
7
|
+
export function useCurrentBreakpoint(): Breakpoint {
|
|
8
|
+
const theme = useTheme();
|
|
9
|
+
const items: (Breakpoint | null)[] = [
|
|
10
|
+
useMediaQuery(theme.breakpoints.down("xs")) ? "xs" : null,
|
|
11
|
+
useMediaQuery(theme.breakpoints.between("xs", "sm")) ? "sm" : null,
|
|
12
|
+
useMediaQuery(theme.breakpoints.between("sm", "md")) ? "md" : null,
|
|
13
|
+
useMediaQuery(theme.breakpoints.between("md", "lg")) ? "lg" : null,
|
|
14
|
+
useMediaQuery(theme.breakpoints.up("lg")) ? "xl" : null
|
|
15
|
+
];
|
|
16
|
+
return items.find((item) => item != null) ?? "lg";
|
|
17
|
+
}
|