@nocobase/client-v2 2.2.0-beta.9 → 2.3.0-alpha.1
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/es/APIClient.d.ts +1 -3
- package/es/BaseApplication.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
- package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
- package/es/components/category-tabs/index.d.ts +9 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -1
- package/es/components/form/TypedVariableInput.d.ts +31 -5
- package/es/components/form/filter/CollectionFilter.d.ts +2 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
- package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
- package/es/flow/components/RunJSValueEditor.d.ts +1 -0
- package/es/flow/components/filter/FilterGroup.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
- package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
- package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
- package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.d.ts +4 -0
- package/es/index.mjs +153 -118
- package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
- package/es/utils/index.d.ts +1 -0
- package/lib/index.js +162 -127
- package/lib/locale/languageCodes.js +2 -1
- package/package.json +8 -7
- package/src/APIClient.ts +1 -10
- package/src/Application.tsx +4 -0
- package/src/BaseApplication.tsx +11 -6
- package/src/__tests__/app.test.tsx +73 -0
- package/src/__tests__/browserChecker.test.ts +61 -0
- package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +28 -0
- package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
- package/src/collection-manager/field-validation.ts +1 -1
- package/src/components/README.md +7 -1
- package/src/components/README.zh-CN.md +6 -1
- package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
- package/src/components/category-tabs/index.ts +10 -0
- package/src/components/form/JsonTextArea.tsx +4 -0
- package/src/components/form/ScanInput/CodeScanner.tsx +23 -6
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +180 -3
- package/src/components/form/ScanInput/useCodeScanner.ts +170 -5
- package/src/components/form/TypedVariableInput.tsx +452 -101
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
- package/src/components/form/filter/CollectionFilter.tsx +4 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
- package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
- package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/form/filter/useFilterActionProps.ts +37 -6
- package/src/components/index.ts +1 -0
- package/src/flow/FlowPage.tsx +9 -1
- package/src/flow/__tests__/FlowPage.test.tsx +50 -3
- package/src/flow/__tests__/FlowRoute.test.tsx +2 -2
- package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +86 -11
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
- package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +91 -20
- package/src/flow/components/FlowRoute.tsx +7 -3
- package/src/flow/components/RunJSValueEditor.tsx +9 -1
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
- package/src/flow/components/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
- package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
- package/src/flow/index.ts +1 -0
- package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
- package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
- package/src/flow/models/base/PageModel/PageTabModel.tsx +184 -3
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +572 -1
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -0
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +34 -0
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
- package/src/flow-compat/fieldValidationConstants.ts +1 -1
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/index.ts +4 -0
- package/src/locale/languageCodes.ts +2 -1
- package/src/nocobase-buildin-plugin/index.tsx +12 -0
- package/src/utils/getRouteRuntimeVersion.ts +185 -0
- package/src/utils/index.tsx +1 -0
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Html5Qrcode, Html5QrcodeScannerState, Html5QrcodeSupportedFormats } from 'html5-qrcode';
|
|
11
|
+
import jsQR from 'jsqr';
|
|
11
12
|
import { useCallback, useEffect, useState } from 'react';
|
|
12
13
|
import type { CodeFormatsToSupport } from './types';
|
|
13
14
|
|
|
@@ -24,8 +25,30 @@ type UseCodeScannerOptions = {
|
|
|
24
25
|
onScannerSizeChanged?: (size: ScannerSize) => void;
|
|
25
26
|
onScanSuccess: (text: string) => void;
|
|
26
27
|
onScanFailure?: () => void;
|
|
28
|
+
onCameraStartFailure?: (error: unknown) => void;
|
|
27
29
|
};
|
|
28
30
|
|
|
31
|
+
type ImageDataVariant = {
|
|
32
|
+
imageData: ImageData;
|
|
33
|
+
maxSize: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type JsQRImageTransform = {
|
|
37
|
+
contrast?: number;
|
|
38
|
+
threshold?: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const QR_SCAN_IMAGE_SIZES = [3200, 2400, 1600, 1000];
|
|
42
|
+
const QR_SCAN_IMAGE_TRANSFORMS: JsQRImageTransform[] = [
|
|
43
|
+
{},
|
|
44
|
+
{ contrast: 3, threshold: 105 },
|
|
45
|
+
{ contrast: 2, threshold: 105 },
|
|
46
|
+
{ contrast: 3, threshold: 120 },
|
|
47
|
+
{ contrast: 2, threshold: 120 },
|
|
48
|
+
{ contrast: 3, threshold: 90 },
|
|
49
|
+
{ contrast: 4, threshold: 105 },
|
|
50
|
+
];
|
|
51
|
+
|
|
29
52
|
export const DEFAULT_CODE_FORMATS: CodeFormatsToSupport = [
|
|
30
53
|
Html5QrcodeSupportedFormats.QR_CODE,
|
|
31
54
|
Html5QrcodeSupportedFormats.CODE_128,
|
|
@@ -48,6 +71,13 @@ export function getCodeScanBoxSize(width: number, height: number) {
|
|
|
48
71
|
};
|
|
49
72
|
}
|
|
50
73
|
|
|
74
|
+
function clampCodeScanBoxSize(scanBoxSize: ScannerSize, width: number, height: number) {
|
|
75
|
+
return {
|
|
76
|
+
width: Math.min(scanBoxSize.width, width),
|
|
77
|
+
height: Math.min(scanBoxSize.height, height),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
51
81
|
async function stopScanner(scanner?: Html5Qrcode, options: { clear?: boolean } = {}) {
|
|
52
82
|
if (!scanner) {
|
|
53
83
|
return;
|
|
@@ -62,6 +92,126 @@ async function stopScanner(scanner?: Html5Qrcode, options: { clear?: boolean } =
|
|
|
62
92
|
}
|
|
63
93
|
}
|
|
64
94
|
|
|
95
|
+
function isSafariBrowser() {
|
|
96
|
+
const { userAgent, vendor } = navigator;
|
|
97
|
+
return /Apple/i.test(vendor) && /Safari/i.test(userAgent) && !/CriOS|FxiOS|EdgiOS|Chrome/i.test(userAgent);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function loadImage(file: File) {
|
|
101
|
+
return new Promise<HTMLImageElement>((resolve, reject) => {
|
|
102
|
+
const image = new Image();
|
|
103
|
+
const url = URL.createObjectURL(file);
|
|
104
|
+
const cleanup = () => URL.revokeObjectURL(url);
|
|
105
|
+
|
|
106
|
+
image.onload = () => {
|
|
107
|
+
cleanup();
|
|
108
|
+
resolve(image);
|
|
109
|
+
};
|
|
110
|
+
image.onerror = (event) => {
|
|
111
|
+
cleanup();
|
|
112
|
+
reject(event);
|
|
113
|
+
};
|
|
114
|
+
image.onabort = (event) => {
|
|
115
|
+
cleanup();
|
|
116
|
+
reject(event);
|
|
117
|
+
};
|
|
118
|
+
image.src = url;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getScanImageSizes(naturalWidth: number, naturalHeight: number) {
|
|
123
|
+
const maxSize = Math.max(naturalWidth, naturalHeight);
|
|
124
|
+
const cappedMaxSize = Math.min(maxSize, QR_SCAN_IMAGE_SIZES[0]);
|
|
125
|
+
return Array.from(new Set([cappedMaxSize, ...QR_SCAN_IMAGE_SIZES.filter((size) => size < cappedMaxSize)])).sort(
|
|
126
|
+
(a, b) => b - a,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getImageDataFromImage(image: HTMLImageElement, maxSize: number): ImageDataVariant | undefined {
|
|
131
|
+
const naturalWidth = image.naturalWidth || image.width;
|
|
132
|
+
const naturalHeight = image.naturalHeight || image.height;
|
|
133
|
+
|
|
134
|
+
const scale = Math.min(1, maxSize / Math.max(naturalWidth, naturalHeight));
|
|
135
|
+
const width = Math.max(1, Math.round(naturalWidth * scale));
|
|
136
|
+
const height = Math.max(1, Math.round(naturalHeight * scale));
|
|
137
|
+
const canvas = document.createElement('canvas');
|
|
138
|
+
canvas.width = width;
|
|
139
|
+
canvas.height = height;
|
|
140
|
+
const context = canvas.getContext('2d');
|
|
141
|
+
if (!context) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
context.drawImage(image, 0, 0, width, height);
|
|
146
|
+
return {
|
|
147
|
+
imageData: context.getImageData(0, 0, width, height),
|
|
148
|
+
maxSize,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function getImageDataVariants(file: File) {
|
|
153
|
+
const image = await loadImage(file);
|
|
154
|
+
const naturalWidth = image.naturalWidth || image.width;
|
|
155
|
+
const naturalHeight = image.naturalHeight || image.height;
|
|
156
|
+
|
|
157
|
+
if (!naturalWidth || !naturalHeight) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return getScanImageSizes(naturalWidth, naturalHeight)
|
|
162
|
+
.map((maxSize) => getImageDataFromImage(image, maxSize))
|
|
163
|
+
.filter((variant): variant is ImageDataVariant => !!variant);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getTransformedImageData(imageData: ImageData, transform: JsQRImageTransform) {
|
|
167
|
+
const { contrast = 1, threshold } = transform;
|
|
168
|
+
if (contrast === 1 && threshold == null) {
|
|
169
|
+
return imageData.data;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const data = new Uint8ClampedArray(imageData.data);
|
|
173
|
+
for (let index = 0; index < data.length; index += 4) {
|
|
174
|
+
let luminance = data[index] * 0.299 + data[index + 1] * 0.587 + data[index + 2] * 0.114;
|
|
175
|
+
luminance = Math.max(0, Math.min(255, (luminance - 128) * contrast + 128));
|
|
176
|
+
if (threshold != null) {
|
|
177
|
+
luminance = luminance < threshold ? 0 : 255;
|
|
178
|
+
}
|
|
179
|
+
data[index] = luminance;
|
|
180
|
+
data[index + 1] = luminance;
|
|
181
|
+
data[index + 2] = luminance;
|
|
182
|
+
}
|
|
183
|
+
return data;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function scanFileWithJsQR(file: File, formatsToSupport?: CodeFormatsToSupport) {
|
|
187
|
+
const formats = formatsToSupport?.length ? formatsToSupport : DEFAULT_CODE_FORMATS;
|
|
188
|
+
if (!formats.includes(Html5QrcodeSupportedFormats.QR_CODE)) {
|
|
189
|
+
throw new Error('QR_CODE is not included in the requested formats');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const variants = await getImageDataVariants(file);
|
|
193
|
+
if (!variants?.length) {
|
|
194
|
+
throw new Error('Failed to prepare uploaded image for QR decoding');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const { imageData } of variants) {
|
|
198
|
+
for (const transform of QR_SCAN_IMAGE_TRANSFORMS) {
|
|
199
|
+
const data = getTransformedImageData(imageData, transform);
|
|
200
|
+
const qrCode = jsQR(data, imageData.width, imageData.height, { inversionAttempts: 'attemptBoth' });
|
|
201
|
+
if (qrCode?.data) {
|
|
202
|
+
return qrCode.data;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
throw new Error('No QR code decoded by jsQR');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function shouldScanQrWithJsQR(formatsToSupport?: CodeFormatsToSupport) {
|
|
211
|
+
const formats = formatsToSupport?.length ? formatsToSupport : DEFAULT_CODE_FORMATS;
|
|
212
|
+
return formats.includes(Html5QrcodeSupportedFormats.QR_CODE);
|
|
213
|
+
}
|
|
214
|
+
|
|
65
215
|
export function useCodeScanner({
|
|
66
216
|
enabled,
|
|
67
217
|
elementId,
|
|
@@ -70,6 +220,7 @@ export function useCodeScanner({
|
|
|
70
220
|
onScannerSizeChanged,
|
|
71
221
|
onScanSuccess,
|
|
72
222
|
onScanFailure,
|
|
223
|
+
onCameraStartFailure,
|
|
73
224
|
}: UseCodeScannerOptions) {
|
|
74
225
|
const [scanner, setScanner] = useState<Html5Qrcode>();
|
|
75
226
|
|
|
@@ -81,7 +232,7 @@ export function useCodeScanner({
|
|
|
81
232
|
fps: 10,
|
|
82
233
|
qrbox(width, height) {
|
|
83
234
|
onScannerSizeChanged?.({ width, height });
|
|
84
|
-
return scanBoxSize ?? getCodeScanBoxSize(width, height);
|
|
235
|
+
return clampCodeScanBoxSize(scanBoxSize ?? getCodeScanBoxSize(width, height), width, height);
|
|
85
236
|
},
|
|
86
237
|
},
|
|
87
238
|
(decodedText) => {
|
|
@@ -95,6 +246,16 @@ export function useCodeScanner({
|
|
|
95
246
|
|
|
96
247
|
const startScanFile = useCallback(
|
|
97
248
|
async (file: File) => {
|
|
249
|
+
if (isSafariBrowser() && shouldScanQrWithJsQR(formatsToSupport)) {
|
|
250
|
+
try {
|
|
251
|
+
const decodedText = await scanFileWithJsQR(file, formatsToSupport);
|
|
252
|
+
onScanSuccess(decodedText);
|
|
253
|
+
return;
|
|
254
|
+
} catch {
|
|
255
|
+
// Fall through to html5-qrcode so barcode uploads still work in Safari.
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
98
259
|
if (!scanner) {
|
|
99
260
|
return;
|
|
100
261
|
}
|
|
@@ -103,12 +264,12 @@ export function useCodeScanner({
|
|
|
103
264
|
try {
|
|
104
265
|
const result = await scanner.scanFileV2(file, false);
|
|
105
266
|
onScanSuccess(result.decodedText);
|
|
106
|
-
} catch
|
|
267
|
+
} catch {
|
|
107
268
|
onScanFailure?.();
|
|
108
269
|
await startScanCamera(scanner);
|
|
109
270
|
}
|
|
110
271
|
},
|
|
111
|
-
[onScanFailure, onScanSuccess, scanner, startScanCamera],
|
|
272
|
+
[formatsToSupport, onScanFailure, onScanSuccess, scanner, startScanCamera],
|
|
112
273
|
);
|
|
113
274
|
|
|
114
275
|
useEffect(() => {
|
|
@@ -121,14 +282,18 @@ export function useCodeScanner({
|
|
|
121
282
|
verbose: false,
|
|
122
283
|
});
|
|
123
284
|
setScanner(scannerInstance);
|
|
124
|
-
startScanCamera(scannerInstance).catch(() => {
|
|
285
|
+
startScanCamera(scannerInstance).catch((error: unknown) => {
|
|
286
|
+
if (onCameraStartFailure) {
|
|
287
|
+
onCameraStartFailure(error);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
125
290
|
onScanFailure?.();
|
|
126
291
|
});
|
|
127
292
|
|
|
128
293
|
return () => {
|
|
129
294
|
stopScanner(scannerInstance, { clear: true }).catch(() => undefined);
|
|
130
295
|
};
|
|
131
|
-
}, [elementId, enabled, formatsToSupport, onScanFailure, startScanCamera]);
|
|
296
|
+
}, [elementId, enabled, formatsToSupport, onCameraStartFailure, onScanFailure, startScanCamera]);
|
|
132
297
|
|
|
133
298
|
return {
|
|
134
299
|
startScanFile,
|