@nocobase/client-v2 2.3.0-alpha.1 → 2.3.0-beta.11

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 (173) hide show
  1. package/es/APIClient.d.ts +12 -0
  2. package/es/components/form/ScanInput/useCodeScanner.d.ts +12 -2
  3. package/es/components/form/ScanInput/zxingWasmDecoder.d.ts +9 -0
  4. package/es/flow/actions/linkageRules.d.ts +25 -0
  5. package/es/flow/components/FieldAssignValueInput.d.ts +6 -30
  6. package/es/flow/components/FlowRoute.d.ts +2 -2
  7. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  8. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +32 -0
  9. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  10. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  11. package/es/flow/components/filter/VariableFilterItem.d.ts +7 -0
  12. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  13. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  14. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  15. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  16. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  17. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  18. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  19. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  20. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  21. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  22. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  23. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  24. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  25. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  26. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  27. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  28. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  29. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  30. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  31. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  32. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  33. package/es/flow-compat/passwordUtils.d.ts +1 -1
  34. package/es/index.d.ts +1 -0
  35. package/es/index.mjs +148 -145
  36. package/es/ui-operation/index.d.ts +15 -0
  37. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  38. package/lib/index.js +170 -167
  39. package/lib/locale/languageCodes.js +1 -0
  40. package/package.json +10 -8
  41. package/src/APIClient.ts +92 -0
  42. package/src/Application.tsx +3 -1
  43. package/src/__tests__/APIClient.test.tsx +58 -0
  44. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
  45. package/src/__tests__/settings-center.test.tsx +270 -6
  46. package/src/acl/ACLProvider.tsx +2 -0
  47. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  48. package/src/collection-manager/__tests__/field-configure.test.ts +52 -0
  49. package/src/collection-manager/field-configure.ts +2 -2
  50. package/src/components/form/ScanInput/CodeScanner.tsx +64 -35
  51. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +107 -0
  52. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +294 -24
  53. package/src/components/form/ScanInput/__tests__/zxingWasmDecoder.test.ts +78 -0
  54. package/src/components/form/ScanInput/useCodeScanner.ts +281 -23
  55. package/src/components/form/ScanInput/zxingWasmDecoder.ts +64 -0
  56. package/src/components/form/filter/CollectionFilterItem.tsx +4 -2
  57. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +17 -0
  58. package/src/components/form/table/Table.tsx +81 -6
  59. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  60. package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
  61. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  62. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  63. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  64. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  65. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  66. package/src/flow/actions/__tests__/validation.test.ts +48 -0
  67. package/src/flow/actions/linkageRules.tsx +59 -16
  68. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  69. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  70. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
  71. package/src/flow/common/Markdown/style.ts +4 -0
  72. package/src/flow/components/BlockItemCard.tsx +2 -2
  73. package/src/flow/components/FieldAssignValueInput.tsx +42 -621
  74. package/src/flow/components/FlowRoute.tsx +71 -29
  75. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  76. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +335 -0
  77. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +389 -0
  78. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  79. package/src/flow/components/field-value-variable/index.ts +12 -0
  80. package/src/flow/components/filter/VariableFilterItem.tsx +23 -6
  81. package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
  82. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
  83. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  84. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  85. package/src/flow/index.ts +2 -0
  86. package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
  87. package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
  88. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  89. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  90. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  91. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  92. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  93. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  94. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  95. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  96. package/src/flow/models/base/GridModel.tsx +0 -1
  97. package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
  98. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
  99. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  100. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  101. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  102. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  103. package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
  104. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +36 -50
  105. package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
  106. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  107. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  108. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
  109. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  110. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
  111. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  112. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  113. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
  114. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
  115. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  116. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  117. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
  118. package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
  119. package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
  120. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  121. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  122. package/src/flow/models/blocks/form/FormBlockModel.tsx +73 -5
  123. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  124. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +153 -2
  125. package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
  126. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  127. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  128. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  129. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  130. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  131. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  132. package/src/flow/models/blocks/table/TableBlockModel.tsx +28 -2
  133. package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
  134. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  135. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  136. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  137. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  138. package/src/flow/models/blocks/table/utils.ts +7 -0
  139. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  140. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +12 -3
  141. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  142. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  143. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +29 -3
  144. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
  145. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +5 -1
  146. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +191 -3
  147. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  148. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  149. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  150. package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
  151. package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
  152. package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
  153. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  154. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  155. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  156. package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
  157. package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
  158. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  159. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  160. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  161. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
  162. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
  163. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  164. package/src/flow/models/topbar/TopbarActionModel.tsx +5 -5
  165. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  166. package/src/index.ts +1 -0
  167. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  168. package/src/layout-manager/LayoutRoute.tsx +2 -1
  169. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  170. package/src/locale/languageCodes.ts +1 -0
  171. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  172. package/src/ui-operation/index.ts +33 -0
  173. package/src/ui-operation/ui-operation-codec.ts +54 -0
@@ -9,8 +9,10 @@
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
+ import type { RefObject } from 'react';
13
14
  import type { CodeFormatsToSupport } from './types';
15
+ import { decodeQrCodeWithZxingWasm } from './zxingWasmDecoder';
14
16
 
15
17
  type ScannerSize = {
16
18
  width: number;
@@ -21,8 +23,8 @@ type UseCodeScannerOptions = {
21
23
  enabled: boolean;
22
24
  elementId: string;
23
25
  formatsToSupport?: CodeFormatsToSupport;
24
- scanBoxSize?: ScannerSize;
25
26
  onScannerSizeChanged?: (size: ScannerSize) => void;
27
+ scanViewportRef?: RefObject<HTMLElement | null>;
26
28
  onScanSuccess: (text: string) => void;
27
29
  onScanFailure?: () => void;
28
30
  onCameraStartFailure?: (error: unknown) => void;
@@ -38,7 +40,31 @@ type JsQRImageTransform = {
38
40
  threshold?: number;
39
41
  };
40
42
 
43
+ type QrFrameCaptureLimits = {
44
+ maxHeight: number;
45
+ maxPixels: number;
46
+ maxWidth: number;
47
+ };
48
+
49
+ type FocusMediaTrackCapabilities = MediaTrackCapabilities & {
50
+ focusMode?: string[];
51
+ };
52
+
53
+ type FocusMediaTrackConstraintSet = MediaTrackConstraintSet & {
54
+ focusMode?: string;
55
+ };
56
+
41
57
  const QR_SCAN_IMAGE_SIZES = [3200, 2400, 1600, 1000];
58
+ const LIVE_QR_SCAN_INTERVAL = 120;
59
+ const IOS_ZXING_SCAN_INTERVAL = 200;
60
+ const LIVE_QR_SCAN_MAX_WIDTH = 960;
61
+ const LIVE_QR_SCAN_MAX_HEIGHT = 540;
62
+ const LIVE_QR_SCAN_MAX_PIXELS = LIVE_QR_SCAN_MAX_WIDTH * 420;
63
+ const IOS_ZXING_SCAN_LIMITS: QrFrameCaptureLimits = {
64
+ maxHeight: 720,
65
+ maxPixels: 720 * 960,
66
+ maxWidth: 1280,
67
+ };
42
68
  const QR_SCAN_IMAGE_TRANSFORMS: JsQRImageTransform[] = [
43
69
  {},
44
70
  { contrast: 3, threshold: 105 },
@@ -66,8 +92,170 @@ export const DEFAULT_CODE_FORMATS: CodeFormatsToSupport = [
66
92
 
67
93
  export function getCodeScanBoxSize(width: number, height: number) {
68
94
  return {
69
- width: Math.floor(Math.min(width * 0.82, 520)),
70
- height: Math.floor(Math.min(height * 0.32, 240)),
95
+ width: Math.floor(Math.min((width * 90) / 100, 1152)),
96
+ height: Math.floor(Math.min((height * 70) / 100, 540)),
97
+ };
98
+ }
99
+
100
+ export function isIOSBrowser() {
101
+ return (
102
+ /iPad|iPhone|iPod/i.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
103
+ );
104
+ }
105
+
106
+ function getVisibleVideoFrameRegion(video: HTMLVideoElement, scanViewport: Element) {
107
+ const rect = video.getBoundingClientRect();
108
+ const viewportRect = scanViewport.getBoundingClientRect();
109
+ const visibleLeft = Math.max(viewportRect.left, rect.left);
110
+ const visibleTop = Math.max(viewportRect.top, rect.top);
111
+ const visibleRight = Math.min(viewportRect.right, rect.right);
112
+ const visibleBottom = Math.min(viewportRect.bottom, rect.bottom);
113
+ if (!rect.width || !rect.height || visibleRight <= visibleLeft || visibleBottom <= visibleTop) {
114
+ return;
115
+ }
116
+
117
+ const sourceX = Math.max(0, Math.floor(((visibleLeft - rect.left) / rect.width) * video.videoWidth));
118
+ const sourceY = Math.max(0, Math.floor(((visibleTop - rect.top) / rect.height) * video.videoHeight));
119
+ return {
120
+ x: sourceX,
121
+ y: sourceY,
122
+ width: Math.min(
123
+ video.videoWidth - sourceX,
124
+ Math.ceil(((visibleRight - visibleLeft) / rect.width) * video.videoWidth),
125
+ ),
126
+ height: Math.min(
127
+ video.videoHeight - sourceY,
128
+ Math.ceil(((visibleBottom - visibleTop) / rect.height) * video.videoHeight),
129
+ ),
130
+ };
131
+ }
132
+
133
+ export function getQrVideoFrameImageData(
134
+ video: HTMLVideoElement,
135
+ canvas: HTMLCanvasElement,
136
+ scanViewport?: Element,
137
+ limits: QrFrameCaptureLimits = {
138
+ maxHeight: LIVE_QR_SCAN_MAX_HEIGHT,
139
+ maxPixels: LIVE_QR_SCAN_MAX_PIXELS,
140
+ maxWidth: LIVE_QR_SCAN_MAX_WIDTH,
141
+ },
142
+ ) {
143
+ if (!video.videoWidth || !video.videoHeight || video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
144
+ return;
145
+ }
146
+
147
+ const viewfinderWidth = video.clientWidth || video.videoWidth;
148
+ const viewfinderHeight = video.clientHeight || video.videoHeight;
149
+ const scanBoxSize = getCodeScanBoxSize(viewfinderWidth, viewfinderHeight);
150
+ const scanBoxRegion = {
151
+ width: Math.min(video.videoWidth, Math.floor(scanBoxSize.width * (video.videoWidth / viewfinderWidth))),
152
+ height: Math.min(video.videoHeight, Math.floor(scanBoxSize.height * (video.videoHeight / viewfinderHeight))),
153
+ };
154
+ const sourceRegion = scanViewport
155
+ ? getVisibleVideoFrameRegion(video, scanViewport)
156
+ : {
157
+ x: Math.floor((video.videoWidth - scanBoxRegion.width) / 2),
158
+ y: Math.floor((video.videoHeight - scanBoxRegion.height) / 2),
159
+ ...scanBoxRegion,
160
+ };
161
+ if (!sourceRegion) {
162
+ return;
163
+ }
164
+
165
+ const maxWidth = scanViewport && sourceRegion.height > sourceRegion.width ? limits.maxHeight : limits.maxWidth;
166
+ const maxHeight = scanViewport && sourceRegion.height > sourceRegion.width ? limits.maxWidth : limits.maxHeight;
167
+ const pixelScale = scanViewport ? Math.sqrt(limits.maxPixels / (sourceRegion.width * sourceRegion.height)) : 1;
168
+ const targetScale = Math.min(1, maxWidth / sourceRegion.width, maxHeight / sourceRegion.height, pixelScale);
169
+ const targetWidth = Math.max(1, Math.floor(sourceRegion.width * targetScale));
170
+ const targetHeight = Math.max(1, Math.floor(sourceRegion.height * targetScale));
171
+ if (canvas.width !== targetWidth) {
172
+ canvas.width = targetWidth;
173
+ }
174
+ if (canvas.height !== targetHeight) {
175
+ canvas.height = targetHeight;
176
+ }
177
+ const context = canvas.getContext('2d', { willReadFrequently: true });
178
+ if (!context) {
179
+ return;
180
+ }
181
+
182
+ context.drawImage(
183
+ video,
184
+ sourceRegion.x,
185
+ sourceRegion.y,
186
+ sourceRegion.width,
187
+ sourceRegion.height,
188
+ 0,
189
+ 0,
190
+ targetWidth,
191
+ targetHeight,
192
+ );
193
+ return context.getImageData(0, 0, targetWidth, targetHeight);
194
+ }
195
+
196
+ export function scanQrVideoFrame(video: HTMLVideoElement, canvas: HTMLCanvasElement, scanViewport?: Element) {
197
+ const imageData = getQrVideoFrameImageData(video, canvas, scanViewport);
198
+ if (!imageData) {
199
+ return;
200
+ }
201
+ return jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: 'dontInvert' })?.data;
202
+ }
203
+
204
+ export async function scanQrVideoFrameWithZxingWasm(
205
+ video: HTMLVideoElement,
206
+ canvas: HTMLCanvasElement,
207
+ scanViewport: Element,
208
+ ) {
209
+ const imageData = getQrVideoFrameImageData(video, canvas, scanViewport, IOS_ZXING_SCAN_LIMITS);
210
+ if (!imageData) {
211
+ return;
212
+ }
213
+ return decodeQrCodeWithZxingWasm(imageData);
214
+ }
215
+
216
+ function startLiveQrScan(
217
+ elementId: string,
218
+ onScanSuccess: (text: string) => void,
219
+ scanViewportRef?: RefObject<HTMLElement | null>,
220
+ ) {
221
+ const canvas = document.createElement('canvas');
222
+ const useVisiblePreview = isIOSBrowser();
223
+ let timer: number | undefined;
224
+ let stopped = false;
225
+
226
+ const scan = async () => {
227
+ if (stopped) {
228
+ return;
229
+ }
230
+ const video = document.getElementById(elementId)?.querySelector('video');
231
+ const scanViewport = useVisiblePreview ? scanViewportRef?.current : undefined;
232
+ if (video) {
233
+ let decodedText: string | undefined;
234
+ try {
235
+ decodedText =
236
+ useVisiblePreview && scanViewport
237
+ ? await scanQrVideoFrameWithZxingWasm(video, canvas, scanViewport)
238
+ : scanQrVideoFrame(video, canvas);
239
+ } catch {
240
+ // Keep the existing scanners active when the optional WASM decoder cannot load or decode a frame.
241
+ }
242
+ if (decodedText && !stopped) {
243
+ stopped = true;
244
+ onScanSuccess(decodedText);
245
+ return;
246
+ }
247
+ }
248
+ if (!stopped) {
249
+ timer = window.setTimeout(scan, useVisiblePreview ? IOS_ZXING_SCAN_INTERVAL : LIVE_QR_SCAN_INTERVAL);
250
+ }
251
+ };
252
+
253
+ timer = window.setTimeout(scan, 0);
254
+ return () => {
255
+ stopped = true;
256
+ if (timer !== undefined) {
257
+ window.clearTimeout(timer);
258
+ }
71
259
  };
72
260
  }
73
261
 
@@ -92,6 +280,19 @@ async function stopScanner(scanner?: Html5Qrcode, options: { clear?: boolean } =
92
280
  }
93
281
  }
94
282
 
283
+ async function enableContinuousFocus(scanner: Html5Qrcode) {
284
+ try {
285
+ const capabilities = scanner.getRunningTrackCapabilities() as FocusMediaTrackCapabilities;
286
+ if (!capabilities.focusMode?.includes('continuous')) {
287
+ return;
288
+ }
289
+ const focusConstraints: FocusMediaTrackConstraintSet = { focusMode: 'continuous' };
290
+ await scanner.applyVideoConstraints({ advanced: [focusConstraints] });
291
+ } catch {
292
+ // Some browsers expose incomplete camera capability APIs. Scanning should continue without explicit focus control.
293
+ }
294
+ }
295
+
95
296
  function isSafariBrowser() {
96
297
  const { userAgent, vendor } = navigator;
97
298
  return /Apple/i.test(vendor) && /Safari/i.test(userAgent) && !/CriOS|FxiOS|EdgiOS|Chrome/i.test(userAgent);
@@ -216,40 +417,96 @@ export function useCodeScanner({
216
417
  enabled,
217
418
  elementId,
218
419
  formatsToSupport,
219
- scanBoxSize,
220
420
  onScannerSizeChanged,
421
+ scanViewportRef,
221
422
  onScanSuccess,
222
423
  onScanFailure,
223
424
  onCameraStartFailure,
224
425
  }: UseCodeScannerOptions) {
225
426
  const [scanner, setScanner] = useState<Html5Qrcode>();
427
+ const liveQrScanStopRef = useRef<() => void>();
428
+ const scanSucceededRef = useRef(false);
429
+ const scanSessionRef = useRef(0);
430
+
431
+ const stopLiveQrScan = useCallback(() => {
432
+ liveQrScanStopRef.current?.();
433
+ liveQrScanStopRef.current = undefined;
434
+ }, []);
435
+
436
+ const cancelActiveScan = useCallback(() => {
437
+ scanSessionRef.current += 1;
438
+ stopLiveQrScan();
439
+ }, [stopLiveQrScan]);
440
+
441
+ const reportScanSuccess = useCallback(
442
+ (text: string) => {
443
+ if (scanSucceededRef.current) {
444
+ return;
445
+ }
446
+ scanSucceededRef.current = true;
447
+ stopLiveQrScan();
448
+ onScanSuccess(text);
449
+ },
450
+ [onScanSuccess, stopLiveQrScan],
451
+ );
226
452
 
227
453
  const startScanCamera = useCallback(
228
454
  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);
455
+ cancelActiveScan();
456
+ const scanSession = scanSessionRef.current;
457
+ scanSucceededRef.current = false;
458
+ try {
459
+ await scannerInstance.start(
460
+ { facingMode: 'environment' },
461
+ {
462
+ fps: 8,
463
+ disableFlip: false,
464
+ videoConstraints: {
465
+ facingMode: { ideal: 'environment' },
466
+ width: { ideal: 1920 },
467
+ height: { ideal: 1080 },
468
+ frameRate: { ideal: 30 },
469
+ },
470
+ qrbox(width, height) {
471
+ onScannerSizeChanged?.({ width, height });
472
+ return clampCodeScanBoxSize(getCodeScanBoxSize(width, height), width, height);
473
+ },
474
+ },
475
+ (decodedText) => {
476
+ reportScanSuccess(decodedText);
236
477
  },
237
- },
238
- (decodedText) => {
239
- onScanSuccess(decodedText);
240
- },
241
- undefined,
242
- );
478
+ undefined,
479
+ );
480
+ } catch (error) {
481
+ if (scanSession !== scanSessionRef.current) {
482
+ return;
483
+ }
484
+ throw error;
485
+ }
486
+ if (scanSession !== scanSessionRef.current) {
487
+ try {
488
+ await stopScanner(scannerInstance, { clear: true });
489
+ } catch {
490
+ // The scanner may already have been cleared by the canceled session cleanup.
491
+ }
492
+ return;
493
+ }
494
+ if (shouldScanQrWithJsQR(formatsToSupport)) {
495
+ liveQrScanStopRef.current = startLiveQrScan(elementId, reportScanSuccess, scanViewportRef);
496
+ }
497
+ await enableContinuousFocus(scannerInstance);
243
498
  },
244
- [onScanSuccess, onScannerSizeChanged, scanBoxSize],
499
+ [cancelActiveScan, elementId, formatsToSupport, onScannerSizeChanged, reportScanSuccess, scanViewportRef],
245
500
  );
246
501
 
247
502
  const startScanFile = useCallback(
248
503
  async (file: File) => {
504
+ cancelActiveScan();
505
+ scanSucceededRef.current = false;
249
506
  if (isSafariBrowser() && shouldScanQrWithJsQR(formatsToSupport)) {
250
507
  try {
251
508
  const decodedText = await scanFileWithJsQR(file, formatsToSupport);
252
- onScanSuccess(decodedText);
509
+ reportScanSuccess(decodedText);
253
510
  return;
254
511
  } catch {
255
512
  // Fall through to html5-qrcode so barcode uploads still work in Safari.
@@ -263,13 +520,13 @@ export function useCodeScanner({
263
520
  await stopScanner(scanner);
264
521
  try {
265
522
  const result = await scanner.scanFileV2(file, false);
266
- onScanSuccess(result.decodedText);
523
+ reportScanSuccess(result.decodedText);
267
524
  } catch {
268
525
  onScanFailure?.();
269
526
  await startScanCamera(scanner);
270
527
  }
271
528
  },
272
- [formatsToSupport, onScanFailure, onScanSuccess, scanner, startScanCamera],
529
+ [cancelActiveScan, formatsToSupport, onScanFailure, reportScanSuccess, scanner, startScanCamera],
273
530
  );
274
531
 
275
532
  useEffect(() => {
@@ -291,9 +548,10 @@ export function useCodeScanner({
291
548
  });
292
549
 
293
550
  return () => {
551
+ cancelActiveScan();
294
552
  stopScanner(scannerInstance, { clear: true }).catch(() => undefined);
295
553
  };
296
- }, [elementId, enabled, formatsToSupport, onCameraStartFailure, onScanFailure, startScanCamera]);
554
+ }, [cancelActiveScan, elementId, enabled, formatsToSupport, onCameraStartFailure, onScanFailure, startScanCamera]);
297
555
 
298
556
  return {
299
557
  startScanFile,
@@ -0,0 +1,64 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ let decoderPromise: Promise<typeof import('zxing-wasm/reader')> | undefined;
11
+
12
+ async function loadDecoder() {
13
+ if (!decoderPromise) {
14
+ decoderPromise = Promise.all([import('zxing-wasm/reader'), import('zxing-wasm/reader/zxing_reader.wasm')])
15
+ .then(async ([decoder, { default: wasmUrl }]) => {
16
+ try {
17
+ await decoder.prepareZXingModule({
18
+ fireImmediately: true,
19
+ overrides: { locateFile: () => wasmUrl },
20
+ });
21
+ } catch (error) {
22
+ decoder.purgeZXingModule();
23
+ throw error;
24
+ }
25
+ return decoder;
26
+ })
27
+ .catch((error: unknown) => {
28
+ decoderPromise = undefined;
29
+ throw error;
30
+ });
31
+ }
32
+ return decoderPromise;
33
+ }
34
+
35
+ export async function decodeQrCodeWithZxingWasm(imageData: ImageData) {
36
+ const decoder = await loadDecoder();
37
+ const results = await decoder.readBarcodes(imageData, {
38
+ binarizer: 'GlobalHistogram',
39
+ downscaleThreshold: 300,
40
+ formats: ['QRCode'],
41
+ maxNumberOfSymbols: 1,
42
+ tryDenoise: true,
43
+ tryDownscale: true,
44
+ tryHarder: true,
45
+ tryInvert: true,
46
+ tryRotate: true,
47
+ });
48
+ if (results[0]?.text) {
49
+ return results[0].text;
50
+ }
51
+
52
+ const fallbackResults = await decoder.readBarcodes(imageData, {
53
+ binarizer: 'LocalAverage',
54
+ downscaleThreshold: 300,
55
+ formats: ['QRCode'],
56
+ maxNumberOfSymbols: 1,
57
+ tryDenoise: true,
58
+ tryDownscale: true,
59
+ tryHarder: true,
60
+ tryInvert: true,
61
+ tryRotate: true,
62
+ });
63
+ return fallbackResults[0]?.text;
64
+ }
@@ -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;