@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.
Files changed (148) hide show
  1. package/es/APIClient.d.ts +12 -0
  2. package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -2
  3. package/es/flow/actions/linkageRules.d.ts +25 -0
  4. package/es/flow/components/FieldAssignValueInput.d.ts +4 -30
  5. package/es/flow/components/FlowRoute.d.ts +2 -2
  6. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  7. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  8. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  9. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  10. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  11. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  12. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  13. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  14. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  15. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  16. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  17. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  18. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  19. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  20. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  21. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  22. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  23. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  24. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  25. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  26. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  27. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  28. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  29. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  30. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  31. package/es/flow-compat/passwordUtils.d.ts +1 -1
  32. package/es/index.d.ts +1 -0
  33. package/es/index.mjs +147 -144
  34. package/es/ui-operation/index.d.ts +15 -0
  35. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  36. package/lib/index.js +170 -167
  37. package/lib/locale/languageCodes.js +1 -0
  38. package/package.json +8 -7
  39. package/src/APIClient.ts +92 -0
  40. package/src/Application.tsx +3 -1
  41. package/src/__tests__/APIClient.test.tsx +58 -0
  42. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
  43. package/src/__tests__/settings-center.test.tsx +270 -6
  44. package/src/acl/ACLProvider.tsx +2 -0
  45. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  46. package/src/components/form/ScanInput/CodeScanner.tsx +61 -34
  47. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  48. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +114 -18
  49. package/src/components/form/ScanInput/useCodeScanner.ts +166 -23
  50. package/src/components/form/filter/CollectionFilterItem.tsx +4 -2
  51. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +17 -0
  52. package/src/components/form/table/Table.tsx +81 -6
  53. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  54. package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
  55. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  56. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  57. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  58. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  59. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  60. package/src/flow/actions/linkageRules.tsx +59 -16
  61. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  62. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  63. package/src/flow/common/Markdown/style.ts +4 -0
  64. package/src/flow/components/BlockItemCard.tsx +2 -2
  65. package/src/flow/components/FieldAssignValueInput.tsx +38 -621
  66. package/src/flow/components/FlowRoute.tsx +71 -29
  67. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  68. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  69. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  70. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  71. package/src/flow/components/field-value-variable/index.ts +12 -0
  72. package/src/flow/components/filter/VariableFilterItem.tsx +12 -3
  73. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
  74. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  75. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  76. package/src/flow/index.ts +2 -0
  77. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  78. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  79. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  80. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  81. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  82. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  83. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  84. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  85. package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
  86. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
  87. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  88. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  89. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  90. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  91. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  92. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  93. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  94. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
  95. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  96. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
  97. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  98. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  99. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
  100. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  101. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  102. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
  103. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  104. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  105. package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
  106. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  107. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
  108. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  109. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  110. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  111. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  112. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  113. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  114. package/src/flow/models/blocks/table/TableBlockModel.tsx +28 -2
  115. package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
  116. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  117. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  118. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  119. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  120. package/src/flow/models/blocks/table/utils.ts +7 -0
  121. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  122. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
  123. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  124. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  125. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
  126. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
  127. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
  128. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  129. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  130. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  131. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  132. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  133. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  134. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  135. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  136. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  137. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
  138. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
  139. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  140. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  141. package/src/index.ts +1 -0
  142. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  143. package/src/layout-manager/LayoutRoute.tsx +2 -1
  144. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  145. package/src/locale/languageCodes.ts +1 -0
  146. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  147. package/src/ui-operation/index.ts +33 -0
  148. 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 * 0.82, 520)),
70
- height: Math.floor(Math.min(height * 0.32, 240)),
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
- await scannerInstance.start(
230
- { facingMode: 'environment' },
231
- {
232
- fps: 10,
233
- qrbox(width, height) {
234
- onScannerSizeChanged?.({ width, height });
235
- return clampCodeScanBoxSize(scanBoxSize ?? getCodeScanBoxSize(width, height), width, height);
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
- (decodedText) => {
239
- onScanSuccess(decodedText);
240
- },
241
- undefined,
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
- [onScanSuccess, onScannerSizeChanged, scanBoxSize],
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
- onScanSuccess(decodedText);
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
- onScanSuccess(result.decodedText);
408
+ reportScanSuccess(result.decodedText);
267
409
  } catch {
268
410
  onScanFailure?.();
269
411
  await startScanCamera(scanner);
270
412
  }
271
413
  },
272
- [formatsToSupport, onScanFailure, onScanSuccess, scanner, startScanCamera],
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: (string | number)[]) => {
134
- const path = next.map(String);
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
- if (!showStandaloneHandleColumn) return baseColumns;
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, ...baseColumns];
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;