@nocobase/client-v2 2.3.0-alpha.1 → 2.3.0-beta.3
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 +12 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -2
- package/es/flow/actions/linkageRules.d.ts +25 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +4 -30
- package/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
- package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
- package/es/flow/components/field-value-variable/index.d.ts +11 -0
- package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
- package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
- package/es/flow/models/base/ActionModelCore.d.ts +4 -2
- package/es/flow/models/base/BlockGridModel.d.ts +3 -0
- package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
- package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
- package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
- package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
- package/es/flow/models/blocks/table/utils.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
- package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +147 -144
- package/es/ui-operation/index.d.ts +15 -0
- package/es/ui-operation/ui-operation-codec.d.ts +10 -0
- package/lib/index.js +170 -167
- package/lib/locale/languageCodes.js +1 -0
- package/package.json +8 -7
- package/src/APIClient.ts +92 -0
- package/src/Application.tsx +3 -1
- package/src/__tests__/APIClient.test.tsx +58 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
- package/src/__tests__/settings-center.test.tsx +270 -6
- package/src/acl/ACLProvider.tsx +2 -0
- package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
- package/src/components/form/ScanInput/CodeScanner.tsx +61 -34
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +114 -18
- package/src/components/form/ScanInput/useCodeScanner.ts +166 -23
- package/src/components/form/filter/CollectionFilterItem.tsx +4 -2
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +17 -0
- package/src/components/form/table/Table.tsx +81 -6
- package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
- package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
- package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
- package/src/flow/actions/linkageRules.tsx +59 -16
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
- package/src/flow/common/Markdown/style.ts +4 -0
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/components/FieldAssignValueInput.tsx +38 -621
- package/src/flow/components/FlowRoute.tsx +71 -29
- package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
- package/src/flow/components/field-value-variable/dateValue.ts +223 -0
- package/src/flow/components/field-value-variable/index.ts +12 -0
- package/src/flow/components/filter/VariableFilterItem.tsx +12 -3
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
- package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
- package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
- package/src/flow/index.ts +2 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
- package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
- package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
- package/src/flow/models/base/ActionModelCore.tsx +11 -2
- package/src/flow/models/base/BlockGridModel.tsx +26 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
- package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
- package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
- package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
- package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
- package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
- package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
- package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +28 -2
- package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
- package/src/flow/models/blocks/table/utils.ts +7 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
- package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
- package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
- package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
- package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
- package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
- package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
- package/src/index.ts +1 -0
- package/src/layout-manager/LayoutContentRoute.tsx +2 -1
- package/src/layout-manager/LayoutRoute.tsx +2 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
- package/src/locale/languageCodes.ts +1 -0
- package/src/settings-center/AdminSettingsLayout.tsx +23 -2
- package/src/ui-operation/index.ts +33 -0
- package/src/ui-operation/ui-operation-codec.ts +54 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { Html5Qrcode, Html5QrcodeScannerState, Html5QrcodeSupportedFormats } from 'html5-qrcode';
|
|
11
11
|
import jsQR from 'jsqr';
|
|
12
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
12
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
13
13
|
import type { CodeFormatsToSupport } from './types';
|
|
14
14
|
|
|
15
15
|
type ScannerSize = {
|
|
@@ -21,7 +21,6 @@ type UseCodeScannerOptions = {
|
|
|
21
21
|
enabled: boolean;
|
|
22
22
|
elementId: string;
|
|
23
23
|
formatsToSupport?: CodeFormatsToSupport;
|
|
24
|
-
scanBoxSize?: ScannerSize;
|
|
25
24
|
onScannerSizeChanged?: (size: ScannerSize) => void;
|
|
26
25
|
onScanSuccess: (text: string) => void;
|
|
27
26
|
onScanFailure?: () => void;
|
|
@@ -38,7 +37,18 @@ type JsQRImageTransform = {
|
|
|
38
37
|
threshold?: number;
|
|
39
38
|
};
|
|
40
39
|
|
|
40
|
+
type FocusMediaTrackCapabilities = MediaTrackCapabilities & {
|
|
41
|
+
focusMode?: string[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type FocusMediaTrackConstraintSet = MediaTrackConstraintSet & {
|
|
45
|
+
focusMode?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
41
48
|
const QR_SCAN_IMAGE_SIZES = [3200, 2400, 1600, 1000];
|
|
49
|
+
const LIVE_QR_SCAN_INTERVAL = 120;
|
|
50
|
+
const LIVE_QR_SCAN_MAX_WIDTH = 960;
|
|
51
|
+
const LIVE_QR_SCAN_MAX_HEIGHT = 540;
|
|
42
52
|
const QR_SCAN_IMAGE_TRANSFORMS: JsQRImageTransform[] = [
|
|
43
53
|
{},
|
|
44
54
|
{ contrast: 3, threshold: 105 },
|
|
@@ -66,8 +76,72 @@ export const DEFAULT_CODE_FORMATS: CodeFormatsToSupport = [
|
|
|
66
76
|
|
|
67
77
|
export function getCodeScanBoxSize(width: number, height: number) {
|
|
68
78
|
return {
|
|
69
|
-
width: Math.floor(Math.min(width *
|
|
70
|
-
height: Math.floor(Math.min(height *
|
|
79
|
+
width: Math.floor(Math.min((width * 90) / 100, 1152)),
|
|
80
|
+
height: Math.floor(Math.min((height * 70) / 100, 540)),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function scanQrVideoFrame(video: HTMLVideoElement, canvas: HTMLCanvasElement) {
|
|
85
|
+
if (!video.videoWidth || !video.videoHeight || video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const viewfinderWidth = video.clientWidth || video.videoWidth;
|
|
90
|
+
const viewfinderHeight = video.clientHeight || video.videoHeight;
|
|
91
|
+
const scanBoxSize = getCodeScanBoxSize(viewfinderWidth, viewfinderHeight);
|
|
92
|
+
const sourceWidth = Math.min(video.videoWidth, Math.floor(scanBoxSize.width * (video.videoWidth / viewfinderWidth)));
|
|
93
|
+
const sourceHeight = Math.min(
|
|
94
|
+
video.videoHeight,
|
|
95
|
+
Math.floor(scanBoxSize.height * (video.videoHeight / viewfinderHeight)),
|
|
96
|
+
);
|
|
97
|
+
const sourceX = Math.floor((video.videoWidth - sourceWidth) / 2);
|
|
98
|
+
const sourceY = Math.floor((video.videoHeight - sourceHeight) / 2);
|
|
99
|
+
const targetScale = Math.min(1, LIVE_QR_SCAN_MAX_WIDTH / sourceWidth, LIVE_QR_SCAN_MAX_HEIGHT / sourceHeight);
|
|
100
|
+
const targetWidth = Math.max(1, Math.floor(sourceWidth * targetScale));
|
|
101
|
+
const targetHeight = Math.max(1, Math.floor(sourceHeight * targetScale));
|
|
102
|
+
if (canvas.width !== targetWidth) {
|
|
103
|
+
canvas.width = targetWidth;
|
|
104
|
+
}
|
|
105
|
+
if (canvas.height !== targetHeight) {
|
|
106
|
+
canvas.height = targetHeight;
|
|
107
|
+
}
|
|
108
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
109
|
+
if (!context) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
context.drawImage(video, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, targetWidth, targetHeight);
|
|
114
|
+
const imageData = context.getImageData(0, 0, targetWidth, targetHeight);
|
|
115
|
+
return jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: 'dontInvert' })?.data;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function startLiveQrScan(elementId: string, onScanSuccess: (text: string) => void) {
|
|
119
|
+
const canvas = document.createElement('canvas');
|
|
120
|
+
let timer: number | undefined;
|
|
121
|
+
let stopped = false;
|
|
122
|
+
|
|
123
|
+
const scan = () => {
|
|
124
|
+
if (stopped) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const video = document.getElementById(elementId)?.querySelector('video');
|
|
128
|
+
if (video) {
|
|
129
|
+
const decodedText = scanQrVideoFrame(video, canvas);
|
|
130
|
+
if (decodedText) {
|
|
131
|
+
stopped = true;
|
|
132
|
+
onScanSuccess(decodedText);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
timer = window.setTimeout(scan, LIVE_QR_SCAN_INTERVAL);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
timer = window.setTimeout(scan, 0);
|
|
140
|
+
return () => {
|
|
141
|
+
stopped = true;
|
|
142
|
+
if (timer !== undefined) {
|
|
143
|
+
window.clearTimeout(timer);
|
|
144
|
+
}
|
|
71
145
|
};
|
|
72
146
|
}
|
|
73
147
|
|
|
@@ -92,6 +166,19 @@ async function stopScanner(scanner?: Html5Qrcode, options: { clear?: boolean } =
|
|
|
92
166
|
}
|
|
93
167
|
}
|
|
94
168
|
|
|
169
|
+
async function enableContinuousFocus(scanner: Html5Qrcode) {
|
|
170
|
+
try {
|
|
171
|
+
const capabilities = scanner.getRunningTrackCapabilities() as FocusMediaTrackCapabilities;
|
|
172
|
+
if (!capabilities.focusMode?.includes('continuous')) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const focusConstraints: FocusMediaTrackConstraintSet = { focusMode: 'continuous' };
|
|
176
|
+
await scanner.applyVideoConstraints({ advanced: [focusConstraints] });
|
|
177
|
+
} catch {
|
|
178
|
+
// Some browsers expose incomplete camera capability APIs. Scanning should continue without explicit focus control.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
95
182
|
function isSafariBrowser() {
|
|
96
183
|
const { userAgent, vendor } = navigator;
|
|
97
184
|
return /Apple/i.test(vendor) && /Safari/i.test(userAgent) && !/CriOS|FxiOS|EdgiOS|Chrome/i.test(userAgent);
|
|
@@ -216,40 +303,95 @@ export function useCodeScanner({
|
|
|
216
303
|
enabled,
|
|
217
304
|
elementId,
|
|
218
305
|
formatsToSupport,
|
|
219
|
-
scanBoxSize,
|
|
220
306
|
onScannerSizeChanged,
|
|
221
307
|
onScanSuccess,
|
|
222
308
|
onScanFailure,
|
|
223
309
|
onCameraStartFailure,
|
|
224
310
|
}: UseCodeScannerOptions) {
|
|
225
311
|
const [scanner, setScanner] = useState<Html5Qrcode>();
|
|
312
|
+
const liveQrScanStopRef = useRef<() => void>();
|
|
313
|
+
const scanSucceededRef = useRef(false);
|
|
314
|
+
const scanSessionRef = useRef(0);
|
|
315
|
+
|
|
316
|
+
const stopLiveQrScan = useCallback(() => {
|
|
317
|
+
liveQrScanStopRef.current?.();
|
|
318
|
+
liveQrScanStopRef.current = undefined;
|
|
319
|
+
}, []);
|
|
320
|
+
|
|
321
|
+
const cancelActiveScan = useCallback(() => {
|
|
322
|
+
scanSessionRef.current += 1;
|
|
323
|
+
stopLiveQrScan();
|
|
324
|
+
}, [stopLiveQrScan]);
|
|
325
|
+
|
|
326
|
+
const reportScanSuccess = useCallback(
|
|
327
|
+
(text: string) => {
|
|
328
|
+
if (scanSucceededRef.current) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
scanSucceededRef.current = true;
|
|
332
|
+
stopLiveQrScan();
|
|
333
|
+
onScanSuccess(text);
|
|
334
|
+
},
|
|
335
|
+
[onScanSuccess, stopLiveQrScan],
|
|
336
|
+
);
|
|
226
337
|
|
|
227
338
|
const startScanCamera = useCallback(
|
|
228
339
|
async (scannerInstance: Html5Qrcode) => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
340
|
+
cancelActiveScan();
|
|
341
|
+
const scanSession = scanSessionRef.current;
|
|
342
|
+
scanSucceededRef.current = false;
|
|
343
|
+
try {
|
|
344
|
+
await scannerInstance.start(
|
|
345
|
+
{ facingMode: 'environment' },
|
|
346
|
+
{
|
|
347
|
+
fps: 8,
|
|
348
|
+
disableFlip: false,
|
|
349
|
+
videoConstraints: {
|
|
350
|
+
facingMode: { ideal: 'environment' },
|
|
351
|
+
width: { ideal: 1920 },
|
|
352
|
+
height: { ideal: 1080 },
|
|
353
|
+
frameRate: { ideal: 30 },
|
|
354
|
+
},
|
|
355
|
+
qrbox(width, height) {
|
|
356
|
+
onScannerSizeChanged?.({ width, height });
|
|
357
|
+
return clampCodeScanBoxSize(getCodeScanBoxSize(width, height), width, height);
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
(decodedText) => {
|
|
361
|
+
reportScanSuccess(decodedText);
|
|
236
362
|
},
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
363
|
+
undefined,
|
|
364
|
+
);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (scanSession !== scanSessionRef.current) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
if (scanSession !== scanSessionRef.current) {
|
|
372
|
+
try {
|
|
373
|
+
await stopScanner(scannerInstance, { clear: true });
|
|
374
|
+
} catch {
|
|
375
|
+
// The scanner may already have been cleared by the canceled session cleanup.
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (shouldScanQrWithJsQR(formatsToSupport)) {
|
|
380
|
+
liveQrScanStopRef.current = startLiveQrScan(elementId, reportScanSuccess);
|
|
381
|
+
}
|
|
382
|
+
await enableContinuousFocus(scannerInstance);
|
|
243
383
|
},
|
|
244
|
-
[
|
|
384
|
+
[cancelActiveScan, elementId, formatsToSupport, onScannerSizeChanged, reportScanSuccess],
|
|
245
385
|
);
|
|
246
386
|
|
|
247
387
|
const startScanFile = useCallback(
|
|
248
388
|
async (file: File) => {
|
|
389
|
+
cancelActiveScan();
|
|
390
|
+
scanSucceededRef.current = false;
|
|
249
391
|
if (isSafariBrowser() && shouldScanQrWithJsQR(formatsToSupport)) {
|
|
250
392
|
try {
|
|
251
393
|
const decodedText = await scanFileWithJsQR(file, formatsToSupport);
|
|
252
|
-
|
|
394
|
+
reportScanSuccess(decodedText);
|
|
253
395
|
return;
|
|
254
396
|
} catch {
|
|
255
397
|
// Fall through to html5-qrcode so barcode uploads still work in Safari.
|
|
@@ -263,13 +405,13 @@ export function useCodeScanner({
|
|
|
263
405
|
await stopScanner(scanner);
|
|
264
406
|
try {
|
|
265
407
|
const result = await scanner.scanFileV2(file, false);
|
|
266
|
-
|
|
408
|
+
reportScanSuccess(result.decodedText);
|
|
267
409
|
} catch {
|
|
268
410
|
onScanFailure?.();
|
|
269
411
|
await startScanCamera(scanner);
|
|
270
412
|
}
|
|
271
413
|
},
|
|
272
|
-
[formatsToSupport, onScanFailure,
|
|
414
|
+
[cancelActiveScan, formatsToSupport, onScanFailure, reportScanSuccess, scanner, startScanCamera],
|
|
273
415
|
);
|
|
274
416
|
|
|
275
417
|
useEffect(() => {
|
|
@@ -291,9 +433,10 @@ export function useCodeScanner({
|
|
|
291
433
|
});
|
|
292
434
|
|
|
293
435
|
return () => {
|
|
436
|
+
cancelActiveScan();
|
|
294
437
|
stopScanner(scannerInstance, { clear: true }).catch(() => undefined);
|
|
295
438
|
};
|
|
296
|
-
}, [elementId, enabled, formatsToSupport, onCameraStartFailure, onScanFailure, startScanCamera]);
|
|
439
|
+
}, [cancelActiveScan, elementId, enabled, formatsToSupport, onCameraStartFailure, onScanFailure, startScanCamera]);
|
|
297
440
|
|
|
298
441
|
return {
|
|
299
442
|
startScanFile,
|
|
@@ -130,8 +130,10 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
|
|
|
130
130
|
[operatorOptions, operator],
|
|
131
131
|
);
|
|
132
132
|
|
|
133
|
-
const handleFieldChange = (next
|
|
134
|
-
|
|
133
|
+
const handleFieldChange = (next?: (string | number)[]) => {
|
|
134
|
+
// rc-cascader emits `undefined` when its clear icon is clicked in single-select mode, despite its public type
|
|
135
|
+
// declaring an array. Treat that runtime value as an empty path so clearing the field also resets the row.
|
|
136
|
+
const path = (next ?? []).map(String);
|
|
135
137
|
props.value.path = path.join('.');
|
|
136
138
|
const leaf = findOptionByPath(options, path);
|
|
137
139
|
props.value.operator = leaf?.operators?.[0]?.value || '';
|
|
@@ -104,6 +104,23 @@ describe('CollectionFilterItem', () => {
|
|
|
104
104
|
});
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
it('clears the filter row when the selected field is cleared', async () => {
|
|
108
|
+
const value = observable({ path: 'username', operator: '$eq', value: 'alice' });
|
|
109
|
+
const collection = buildStubCollection([{ name: 'username', title: 'Username' }]);
|
|
110
|
+
|
|
111
|
+
const { container } = render(<CollectionFilterItem value={value} collection={collection} />);
|
|
112
|
+
|
|
113
|
+
const clear = container.querySelector('.ant-select-clear');
|
|
114
|
+
if (!clear) throw new Error('expected Cascader clear control to be rendered');
|
|
115
|
+
fireEvent.mouseDown(clear);
|
|
116
|
+
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
expect(value.path).toBe('');
|
|
119
|
+
expect(value.operator).toBe('');
|
|
120
|
+
expect(value.value).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
107
124
|
it('clears value when the operator changes', async () => {
|
|
108
125
|
const value = observable({ path: 'username', operator: '$eq', value: 'alice' });
|
|
109
126
|
const collection = buildStubCollection([{ name: 'username', title: 'Username' }]);
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import { DragOverlay, type DragEndEvent, type DragStartEvent } from '@dnd-kit/core';
|
|
11
11
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
12
|
-
import { cx } from '@emotion/css';
|
|
12
|
+
import { css, cx, injectGlobal } from '@emotion/css';
|
|
13
13
|
import { DndProvider } from '@nocobase/flow-engine';
|
|
14
14
|
import { useMemoizedFn } from 'ahooks';
|
|
15
|
-
import { Table as AntdTable, type TableProps as AntdTableProps } from 'antd';
|
|
16
|
-
import type { ColumnsType, ColumnType, GetRowKey } from 'antd/es/table/interface';
|
|
15
|
+
import { Table as AntdTable, theme, type TableProps as AntdTableProps } from 'antd';
|
|
16
|
+
import type { ColumnsType, ColumnGroupType, ColumnType, GetRowKey } from 'antd/es/table/interface';
|
|
17
17
|
import type { RenderedCell } from 'rc-table/lib/interface';
|
|
18
18
|
import React, { useMemo, useState } from 'react';
|
|
19
19
|
import { SortableRow, SortHandle } from './dnd/SortableRow';
|
|
@@ -24,6 +24,20 @@ import { readRowKey, snapshotSourceRow, type RowKey, type RowSnapshot } from './
|
|
|
24
24
|
|
|
25
25
|
type RowSelectionRenderCellResult<RecordType> = React.ReactNode | RenderedCell<RecordType>;
|
|
26
26
|
|
|
27
|
+
const DEFAULT_COLUMN_CONTENT_CLASS_NAME = 'nb-table-default-column-content';
|
|
28
|
+
const DEFAULT_COLUMN_CONTENT_MAX_WIDTH_PROPERTY = '--nb-table-default-column-content-max-width';
|
|
29
|
+
|
|
30
|
+
// Zero specificity lets caller classes remain authoritative regardless of stylesheet insertion order. The max-width
|
|
31
|
+
// value itself comes from a token-derived private CSS variable set by each Table instance.
|
|
32
|
+
injectGlobal`
|
|
33
|
+
:where(.${DEFAULT_COLUMN_CONTENT_CLASS_NAME}) {
|
|
34
|
+
max-width: var(${DEFAULT_COLUMN_CONTENT_MAX_WIDTH_PROPERTY});
|
|
35
|
+
white-space: normal;
|
|
36
|
+
overflow-wrap: anywhere;
|
|
37
|
+
word-break: break-word;
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
27
41
|
/**
|
|
28
42
|
* Default initial page size for `Table`. Exposed so consumers can seed their
|
|
29
43
|
* controlled `pageSize` state with the same value the component would use if
|
|
@@ -50,6 +64,54 @@ function isRenderedCell<RecordType>(value: unknown): value is RenderedCell<Recor
|
|
|
50
64
|
return typeof value === 'object' && value !== null && !React.isValidElement(value) && 'children' in value;
|
|
51
65
|
}
|
|
52
66
|
|
|
67
|
+
function isColumnGroup<RecordType>(
|
|
68
|
+
column: ColumnGroupType<RecordType> | ColumnType<RecordType>,
|
|
69
|
+
): column is ColumnGroupType<RecordType> {
|
|
70
|
+
return 'children' in column && Array.isArray(column.children) && column.children.length > 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function addDefaultColumnContentClassName<RecordType>(
|
|
74
|
+
columns: ColumnsType<RecordType>,
|
|
75
|
+
defaultClassName: string,
|
|
76
|
+
): ColumnsType<RecordType> {
|
|
77
|
+
return columns.map((column) => {
|
|
78
|
+
if (column === AntdTable.EXPAND_COLUMN || column === AntdTable.SELECTION_COLUMN) {
|
|
79
|
+
return column;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (isColumnGroup(column)) {
|
|
83
|
+
return {
|
|
84
|
+
...column,
|
|
85
|
+
children: addDefaultColumnContentClassName(column.children, defaultClassName),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (column.width !== undefined || column.ellipsis || column.fixed) {
|
|
90
|
+
return column;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const originalOnCell = column.onCell;
|
|
94
|
+
const originalOnHeaderCell = column.onHeaderCell;
|
|
95
|
+
return {
|
|
96
|
+
...column,
|
|
97
|
+
onCell: (record, index) => {
|
|
98
|
+
const cellProps = originalOnCell?.(record, index) ?? {};
|
|
99
|
+
return {
|
|
100
|
+
...cellProps,
|
|
101
|
+
className: cx(defaultClassName, cellProps.className),
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
onHeaderCell: (headerColumn) => {
|
|
105
|
+
const cellProps = originalOnHeaderCell?.(headerColumn) ?? {};
|
|
106
|
+
return {
|
|
107
|
+
...cellProps,
|
|
108
|
+
className: cx(defaultClassName, cellProps.className),
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
53
115
|
export interface TableProps<RecordType extends object = any> extends AntdTableProps<RecordType> {
|
|
54
116
|
/**
|
|
55
117
|
* Required so drag-sort, hover-swap and row identity work. Accepts the same
|
|
@@ -108,6 +170,7 @@ export interface TableProps<RecordType extends object = any> extends AntdTablePr
|
|
|
108
170
|
* plus the index swap, so it is safe as the default table on any page.
|
|
109
171
|
*/
|
|
110
172
|
export function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
|
|
173
|
+
const { token } = theme.useToken();
|
|
111
174
|
const {
|
|
112
175
|
rowKey,
|
|
113
176
|
showIndex = true,
|
|
@@ -143,6 +206,17 @@ export function Table<RecordType extends object = any>(props: TableProps<RecordT
|
|
|
143
206
|
const showHandleInSelection = isDraggable && showSortHandle && !!rowSelection;
|
|
144
207
|
const showStandaloneHandleColumn = isDraggable && showSortHandle && !rowSelection;
|
|
145
208
|
|
|
209
|
+
const defaultColumnContentClassName = useMemo(
|
|
210
|
+
() =>
|
|
211
|
+
cx(
|
|
212
|
+
DEFAULT_COLUMN_CONTENT_CLASS_NAME,
|
|
213
|
+
css`
|
|
214
|
+
${DEFAULT_COLUMN_CONTENT_MAX_WIDTH_PROPERTY}: ${token.screenXS - token.paddingXL * 3 + token.padding * 2}px;
|
|
215
|
+
`,
|
|
216
|
+
),
|
|
217
|
+
[token.padding, token.paddingXL, token.screenXS],
|
|
218
|
+
);
|
|
219
|
+
|
|
146
220
|
const itemKeys = useMemo<string[]>(() => {
|
|
147
221
|
if (!isDraggable || !dataSource) return [];
|
|
148
222
|
return dataSource
|
|
@@ -191,15 +265,16 @@ export function Table<RecordType extends object = any>(props: TableProps<RecordT
|
|
|
191
265
|
// selection cell — see `augmentedRowSelection` below.
|
|
192
266
|
const augmentedColumns = useMemo<ColumnsType<RecordType>>(() => {
|
|
193
267
|
const baseColumns: ColumnsType<RecordType> = columns ?? [];
|
|
194
|
-
|
|
268
|
+
const styledColumns = addDefaultColumnContentClassName(baseColumns, defaultColumnContentClassName);
|
|
269
|
+
if (!showStandaloneHandleColumn) return styledColumns;
|
|
195
270
|
const handleColumn: ColumnType<RecordType> = {
|
|
196
271
|
key: '__sort__',
|
|
197
272
|
width: sortHandleColumnWidth,
|
|
198
273
|
align: 'center',
|
|
199
274
|
render: () => <SortHandle />,
|
|
200
275
|
};
|
|
201
|
-
return [handleColumn, ...
|
|
202
|
-
}, [columns, showStandaloneHandleColumn, sortHandleColumnWidth]);
|
|
276
|
+
return [handleColumn, ...styledColumns];
|
|
277
|
+
}, [columns, defaultColumnContentClassName, showStandaloneHandleColumn, sortHandleColumnWidth]);
|
|
203
278
|
|
|
204
279
|
const augmentedRowSelection = useMemo(() => {
|
|
205
280
|
if (!rowSelection) return rowSelection;
|