@nocobase/client-v2 2.1.0-beta.47 → 2.1.0-beta.48

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 (26) hide show
  1. package/es/components/form/ScanInput/CodeScanner.d.ts +18 -0
  2. package/es/components/form/ScanInput/ScanBox.d.ts +12 -0
  3. package/es/components/form/ScanInput/ScanInput.d.ts +18 -0
  4. package/es/components/form/ScanInput/index.d.ts +11 -0
  5. package/es/components/form/ScanInput/types.d.ts +10 -0
  6. package/es/components/form/ScanInput/useCodeScanner.d.ts +31 -0
  7. package/es/components/form/index.d.ts +1 -0
  8. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +14 -0
  9. package/es/index.mjs +76 -36
  10. package/lib/index.js +111 -71
  11. package/package.json +8 -7
  12. package/src/components/form/ScanInput/CodeScanner.tsx +219 -0
  13. package/src/components/form/ScanInput/ScanBox.tsx +30 -0
  14. package/src/components/form/ScanInput/ScanInput.tsx +150 -0
  15. package/src/components/form/ScanInput/__tests__/ScanInput.test.tsx +119 -0
  16. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +123 -0
  17. package/src/components/form/ScanInput/index.ts +12 -0
  18. package/src/components/form/ScanInput/types.ts +12 -0
  19. package/src/components/form/ScanInput/useCodeScanner.ts +136 -0
  20. package/src/components/form/index.tsx +1 -0
  21. package/src/flow/actions/__tests__/actionLinkageRules.race.repro.test.ts +45 -0
  22. package/src/flow/actions/linkageRules.tsx +16 -4
  23. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +111 -26
  24. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +61 -0
  25. package/src/flow/models/fields/InputFieldModel.tsx +56 -1
  26. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +116 -0
@@ -0,0 +1,12 @@
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
+ import type { Html5QrcodeSupportedFormats } from 'html5-qrcode';
11
+
12
+ export type CodeFormatsToSupport = Html5QrcodeSupportedFormats[];
@@ -0,0 +1,136 @@
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
+ import { Html5Qrcode, Html5QrcodeScannerState, Html5QrcodeSupportedFormats } from 'html5-qrcode';
11
+ import { useCallback, useEffect, useState } from 'react';
12
+ import type { CodeFormatsToSupport } from './types';
13
+
14
+ type ScannerSize = {
15
+ width: number;
16
+ height: number;
17
+ };
18
+
19
+ type UseCodeScannerOptions = {
20
+ enabled: boolean;
21
+ elementId: string;
22
+ formatsToSupport?: CodeFormatsToSupport;
23
+ scanBoxSize?: ScannerSize;
24
+ onScannerSizeChanged?: (size: ScannerSize) => void;
25
+ onScanSuccess: (text: string) => void;
26
+ onScanFailure?: () => void;
27
+ };
28
+
29
+ export const DEFAULT_CODE_FORMATS: CodeFormatsToSupport = [
30
+ Html5QrcodeSupportedFormats.QR_CODE,
31
+ Html5QrcodeSupportedFormats.CODE_128,
32
+ Html5QrcodeSupportedFormats.CODE_39,
33
+ Html5QrcodeSupportedFormats.CODE_93,
34
+ Html5QrcodeSupportedFormats.CODABAR,
35
+ Html5QrcodeSupportedFormats.EAN_13,
36
+ Html5QrcodeSupportedFormats.EAN_8,
37
+ Html5QrcodeSupportedFormats.ITF,
38
+ Html5QrcodeSupportedFormats.UPC_A,
39
+ Html5QrcodeSupportedFormats.UPC_E,
40
+ Html5QrcodeSupportedFormats.DATA_MATRIX,
41
+ Html5QrcodeSupportedFormats.PDF_417,
42
+ ];
43
+
44
+ export function getCodeScanBoxSize(width: number, height: number) {
45
+ return {
46
+ width: Math.floor(Math.min(width * 0.82, 520)),
47
+ height: Math.floor(Math.min(height * 0.32, 240)),
48
+ };
49
+ }
50
+
51
+ async function stopScanner(scanner?: Html5Qrcode, options: { clear?: boolean } = {}) {
52
+ if (!scanner) {
53
+ return;
54
+ }
55
+
56
+ const state = scanner.getState();
57
+ if ([Html5QrcodeScannerState.SCANNING, Html5QrcodeScannerState.PAUSED].includes(state)) {
58
+ await scanner.stop();
59
+ }
60
+ if (options.clear) {
61
+ scanner.clear();
62
+ }
63
+ }
64
+
65
+ export function useCodeScanner({
66
+ enabled,
67
+ elementId,
68
+ formatsToSupport,
69
+ scanBoxSize,
70
+ onScannerSizeChanged,
71
+ onScanSuccess,
72
+ onScanFailure,
73
+ }: UseCodeScannerOptions) {
74
+ const [scanner, setScanner] = useState<Html5Qrcode>();
75
+
76
+ const startScanCamera = useCallback(
77
+ async (scannerInstance: Html5Qrcode) => {
78
+ await scannerInstance.start(
79
+ { facingMode: 'environment' },
80
+ {
81
+ fps: 10,
82
+ qrbox(width, height) {
83
+ onScannerSizeChanged?.({ width, height });
84
+ return scanBoxSize ?? getCodeScanBoxSize(width, height);
85
+ },
86
+ },
87
+ (decodedText) => {
88
+ onScanSuccess(decodedText);
89
+ },
90
+ undefined,
91
+ );
92
+ },
93
+ [onScanSuccess, onScannerSizeChanged, scanBoxSize],
94
+ );
95
+
96
+ const startScanFile = useCallback(
97
+ async (file: File) => {
98
+ if (!scanner) {
99
+ return;
100
+ }
101
+
102
+ await stopScanner(scanner);
103
+ try {
104
+ const result = await scanner.scanFileV2(file, false);
105
+ onScanSuccess(result.decodedText);
106
+ } catch (error) {
107
+ onScanFailure?.();
108
+ await startScanCamera(scanner);
109
+ }
110
+ },
111
+ [onScanFailure, onScanSuccess, scanner, startScanCamera],
112
+ );
113
+
114
+ useEffect(() => {
115
+ if (!enabled) {
116
+ return;
117
+ }
118
+
119
+ const scannerInstance = new Html5Qrcode(elementId, {
120
+ formatsToSupport: formatsToSupport?.length ? formatsToSupport : DEFAULT_CODE_FORMATS,
121
+ verbose: false,
122
+ });
123
+ setScanner(scannerInstance);
124
+ startScanCamera(scannerInstance).catch(() => {
125
+ onScanFailure?.();
126
+ });
127
+
128
+ return () => {
129
+ stopScanner(scannerInstance, { clear: true }).catch(() => undefined);
130
+ };
131
+ }, [elementId, enabled, formatsToSupport, onScanFailure, startScanCamera]);
132
+
133
+ return {
134
+ startScanFile,
135
+ };
136
+ }
@@ -16,5 +16,6 @@ export * from './FileSizeInput';
16
16
  export * from './JsonTextArea';
17
17
  export * from './PasswordInput';
18
18
  export * from './RemoteSelect';
19
+ export * from './ScanInput';
19
20
  export * from './TypedVariableInput';
20
21
  export * from './VariableInput';
@@ -10,6 +10,9 @@
10
10
  import { describe, expect, it, vi } from 'vitest';
11
11
  import { actionLinkageRules } from '../linkageRules';
12
12
 
13
+ class ActionModel {}
14
+ class PopupSubTableEditActionModel extends ActionModel {}
15
+
13
16
  function createActionModel() {
14
17
  const model: any = {
15
18
  uid: 'edit-action',
@@ -196,4 +199,46 @@ describe('actionLinkageRules props patch isolation', () => {
196
199
 
197
200
  expect(model.hidden).toBe(true);
198
201
  });
202
+
203
+ it('does not sync row action hidden state to the popup subtable field path', async () => {
204
+ const model = createActionModel();
205
+ Object.defineProperty(model, 'constructor', {
206
+ value: PopupSubTableEditActionModel,
207
+ });
208
+ model.isFork = true;
209
+ model.context = {
210
+ blockModel: { uid: 'form-block' },
211
+ fieldPathArray: ['org_o2m'],
212
+ };
213
+ const fieldModel: any = {
214
+ uid: 'org-o2m-field',
215
+ hidden: false,
216
+ context: {
217
+ blockModel: { uid: 'form-block' },
218
+ fieldPathArray: ['org_o2m'],
219
+ },
220
+ };
221
+ const { ctx } = createRuntime(model, { pauseHiddenAction: false });
222
+ ctx.engine = {
223
+ forEachModel: (visitor: (m: any) => void) => {
224
+ visitor(model);
225
+ visitor(fieldModel);
226
+ },
227
+ };
228
+
229
+ await actionLinkageRules.handler(
230
+ ctx,
231
+ createLinkageParams([
232
+ {
233
+ name: 'linkageSetActionProps',
234
+ params: {
235
+ value: 'hidden',
236
+ },
237
+ },
238
+ ]),
239
+ );
240
+
241
+ expect(model.hidden).toBe(true);
242
+ expect(fieldModel.hidden).toBe(false);
243
+ });
199
244
  });
@@ -106,6 +106,18 @@ const getLinkageScopeDepthFromContext = (ctx: FlowContext): number => {
106
106
  return getLinkageScopeDepthFromModel((ctx as any)?.model);
107
107
  };
108
108
 
109
+ const isActionFlowModel = (model: any): boolean => {
110
+ if (!model || typeof model !== 'object') return false;
111
+
112
+ let proto = model.constructor?.prototype;
113
+ while (proto) {
114
+ if (proto.constructor?.name === 'ActionModel') return true;
115
+ proto = Object.getPrototypeOf(proto);
116
+ }
117
+
118
+ return false;
119
+ };
120
+
109
121
  // 获取表单中所有字段的 model 实例的通用函数
110
122
  const getFormFields = (ctx: any) => {
111
123
  try {
@@ -2142,12 +2154,12 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2142
2154
  model.setProps(_.omit(newProps, ['hiddenModel', 'value', 'hiddenText']));
2143
2155
  syncFieldOptionsToForks(model, patchProps);
2144
2156
  if (typeof model.setHidden === 'function') {
2145
- model.setHidden(!!newProps.hiddenModel);
2157
+ model.setHidden(nextHidden);
2146
2158
  } else {
2147
2159
  model.hidden = nextHidden;
2148
- if (prevHidden !== nextHidden) {
2149
- hiddenStatePatches.push({ model, hidden: nextHidden });
2150
- }
2160
+ }
2161
+ if (prevHidden !== nextHidden && !isActionFlowModel(model)) {
2162
+ hiddenStatePatches.push({ model, hidden: nextHidden });
2151
2163
  }
2152
2164
 
2153
2165
  if (newProps.required === true) {
@@ -31,7 +31,90 @@ import {
31
31
  createRootItemChain,
32
32
  type ItemChain,
33
33
  } from './itemChain';
34
- import { buildOpenerUids, LabelByField } from './recordSelectShared';
34
+ import { buildOpenerUids, LabelByField, type AssociationFieldNames } from './recordSelectShared';
35
+
36
+ const MULTIPLE_ASSOCIATION_TYPES = ['belongsToMany', 'hasMany', 'belongsToArray'];
37
+
38
+ function isRecord(value: unknown): value is Record<string, unknown> {
39
+ return !!value && typeof value === 'object' && !Array.isArray(value);
40
+ }
41
+
42
+ export function canRecordPickerSelectMultiple(
43
+ collectionField: { type?: string } | null | undefined,
44
+ allowMultiple?: boolean,
45
+ ) {
46
+ return (
47
+ !!collectionField?.type && MULTIPLE_ASSOCIATION_TYPES.includes(collectionField.type) && allowMultiple !== false
48
+ );
49
+ }
50
+
51
+ export function shouldClearRecordPickerValueOnMultipleChange(
52
+ collectionField: { type?: string } | null | undefined,
53
+ previousAllowMultiple?: boolean,
54
+ nextAllowMultiple?: boolean,
55
+ ) {
56
+ return (
57
+ canRecordPickerSelectMultiple(collectionField, previousAllowMultiple) !==
58
+ canRecordPickerSelectMultiple(collectionField, nextAllowMultiple)
59
+ );
60
+ }
61
+
62
+ export function normalizeRecordPickerSelectedRows(value: unknown, allowMultiple: boolean) {
63
+ if (!value) {
64
+ return [];
65
+ }
66
+ if (allowMultiple) {
67
+ return Array.isArray(value) ? value : [value];
68
+ }
69
+ return Array.isArray(value) ? value.slice(0, 1) : [value];
70
+ }
71
+
72
+ export function getRecordPickerEmptyValue(allowMultiple: boolean) {
73
+ return allowMultiple ? [] : undefined;
74
+ }
75
+
76
+ export function getRecordPickerClearedValue() {
77
+ return undefined;
78
+ }
79
+
80
+ export function normalizeRecordPickerValue(value: unknown, fieldNames: AssociationFieldNames, allowMultiple: boolean) {
81
+ if (!value) {
82
+ return getRecordPickerEmptyValue(allowMultiple);
83
+ }
84
+ const toSelectItem = (item: Record<string, unknown>) => ({
85
+ ...item,
86
+ label: item[fieldNames.label],
87
+ value: item[fieldNames.value],
88
+ });
89
+ if (!allowMultiple) {
90
+ const item = Array.isArray(value) ? value[0] : value;
91
+ return isRecord(item) ? toSelectItem(item) : undefined;
92
+ }
93
+ if (!Array.isArray(value)) {
94
+ return [];
95
+ }
96
+ return value.filter(isRecord).map(toSelectItem);
97
+ }
98
+
99
+ function applyRecordPickerAllowMultiple(ctx: any, params: any, previousParams?: any) {
100
+ const shouldClearValue =
101
+ previousParams &&
102
+ shouldClearRecordPickerValueOnMultipleChange(
103
+ ctx.collectionField,
104
+ previousParams?.allowMultiple,
105
+ params?.allowMultiple,
106
+ );
107
+ const emptyValue = getRecordPickerClearedValue();
108
+
109
+ ctx.model.setProps({
110
+ allowMultiple: params?.allowMultiple,
111
+ ...(shouldClearValue ? { value: emptyValue } : {}),
112
+ });
113
+ if (shouldClearValue) {
114
+ ctx.model.selectedRows.value = emptyValue;
115
+ ctx.model.props.onChange?.(emptyValue);
116
+ }
117
+ }
35
118
 
36
119
  export function buildRecordPickerParentItemContext(ctx: any): {
37
120
  parentItem: ItemChain;
@@ -241,25 +324,10 @@ export function RecordPickerContent({ model, toOne = false }) {
241
324
  function RecordPickerField(props) {
242
325
  const { fieldNames, onClick, disabled } = props;
243
326
  const ctx = useFlowContext();
244
- const toOne = ['belongsTo', 'hasOne'].includes(ctx.collectionField.type);
327
+ const allowMultiple = canRecordPickerSelectMultiple(ctx.collectionField, props.allowMultiple);
245
328
  useEffect(() => {
246
329
  ctx.model.selectedRows.value = props.value;
247
- }, [props.value]);
248
- const normalizeValue = (value, fieldNames, toOne) => {
249
- if (!value) return toOne ? undefined : [];
250
- if (toOne) {
251
- return {
252
- ...value,
253
- label: value[fieldNames.label],
254
- value: value[fieldNames.value],
255
- };
256
- }
257
- return value.map((v) => ({
258
- ...v,
259
- label: v[fieldNames.label],
260
- value: v[fieldNames.value],
261
- }));
262
- };
330
+ }, [ctx.model.selectedRows, props.value]);
263
331
 
264
332
  return (
265
333
  <Select
@@ -270,12 +338,12 @@ function RecordPickerField(props) {
270
338
  onClick(e);
271
339
  }
272
340
  }}
273
- value={normalizeValue(props.value, fieldNames, toOne)}
341
+ value={normalizeRecordPickerValue(props.value, fieldNames, allowMultiple)}
274
342
  labelRender={(item) => {
275
343
  return <LabelByField option={item} fieldNames={fieldNames} />;
276
344
  }}
277
345
  labelInValue
278
- mode={toOne ? undefined : 'multiple'}
346
+ mode={allowMultiple ? 'multiple' : undefined}
279
347
  options={props.value}
280
348
  allowClear
281
349
  onChange={(newValue, option) => {
@@ -405,7 +473,7 @@ RecordPickerFieldModel.registerFlow({
405
473
  },
406
474
  handler(ctx, params) {
407
475
  const { onChange } = ctx.inputArgs;
408
- const toOne = ['belongsTo', 'hasOne'].includes(ctx.collectionField.type);
476
+ const allowMultiple = canRecordPickerSelectMultiple(ctx.collectionField, ctx.model.props.allowMultiple);
409
477
  const sizeToWidthMap: Record<string, any> = {
410
478
  drawer: {
411
479
  small: '30%',
@@ -441,9 +509,9 @@ RecordPickerFieldModel.registerFlow({
441
509
  currentItemValue: ctx.inputArgs.currentItemValue ?? {},
442
510
  }),
443
511
  rowSelectionProps: {
444
- type: toOne ? 'radio' : 'checkbox',
512
+ type: allowMultiple ? 'checkbox' : 'radio',
445
513
  defaultSelectedRows: () => {
446
- return ctx.model.props.value;
514
+ return normalizeRecordPickerSelectedRows(ctx.model.props.value, allowMultiple);
447
515
  },
448
516
  renderCell: undefined,
449
517
  selectedRowKeys: undefined,
@@ -452,8 +520,7 @@ RecordPickerFieldModel.registerFlow({
452
520
  const selectTable = selectBlockModel.findSubModel('items', (m) => {
453
521
  return m;
454
522
  });
455
- if (toOne) {
456
- // 单选
523
+ if (!allowMultiple) {
457
524
  ctx.model.selectedRows.value = selectedRows?.[0];
458
525
  onChange(ctx.model.selectedRows.value);
459
526
  ctx.model._closeView?.();
@@ -475,7 +542,7 @@ RecordPickerFieldModel.registerFlow({
475
542
  },
476
543
  },
477
544
  },
478
- content: () => <RecordPickerContent model={ctx.model} toOne={toOne} />,
545
+ content: () => <RecordPickerContent model={ctx.model} toOne={!allowMultiple} />,
479
546
  styles: {
480
547
  content: {
481
548
  padding: 0,
@@ -500,6 +567,24 @@ RecordPickerFieldModel.registerFlow({
500
567
  fieldNames: {
501
568
  use: 'titleField',
502
569
  },
570
+ allowMultiple: {
571
+ title: tExpr('Multiple'),
572
+ uiMode: { type: 'switch', key: 'allowMultiple' },
573
+ hideInSettings(ctx) {
574
+ return !canRecordPickerSelectMultiple(ctx.collectionField, true);
575
+ },
576
+ defaultParams(ctx) {
577
+ return {
578
+ allowMultiple: canRecordPickerSelectMultiple(ctx.collectionField, true),
579
+ };
580
+ },
581
+ afterParamsSave(ctx, params, previousParams) {
582
+ applyRecordPickerAllowMultiple(ctx, params, previousParams);
583
+ },
584
+ handler(ctx, params) {
585
+ applyRecordPickerAllowMultiple(ctx, params);
586
+ },
587
+ },
503
588
  },
504
589
  });
505
590
 
@@ -20,6 +20,14 @@ import {
20
20
  resolveRecordPersistenceState,
21
21
  } from '../itemChain';
22
22
  import { injectRecordPickerPopupContext } from '@nocobase/client-v2';
23
+ import {
24
+ canRecordPickerSelectMultiple,
25
+ getRecordPickerClearedValue,
26
+ getRecordPickerEmptyValue,
27
+ normalizeRecordPickerSelectedRows,
28
+ normalizeRecordPickerValue,
29
+ shouldClearRecordPickerValueOnMultipleChange,
30
+ } from '../RecordPickerFieldModel';
23
31
 
24
32
  function createMockCollection() {
25
33
  return {
@@ -32,6 +40,59 @@ function createMockCollection() {
32
40
  }
33
41
 
34
42
  describe('RecordPickerFieldModel item context', () => {
43
+ it('resolves popup select multiple mode for association fields', () => {
44
+ expect(canRecordPickerSelectMultiple({ type: 'belongsToMany' })).toBe(true);
45
+ expect(canRecordPickerSelectMultiple({ type: 'hasMany' })).toBe(true);
46
+ expect(canRecordPickerSelectMultiple({ type: 'belongsToArray' })).toBe(true);
47
+ expect(canRecordPickerSelectMultiple({ type: 'belongsToMany' }, false)).toBe(false);
48
+ expect(canRecordPickerSelectMultiple({ type: 'belongsTo' })).toBe(false);
49
+ expect(canRecordPickerSelectMultiple({ type: 'hasOne' })).toBe(false);
50
+ });
51
+
52
+ it('normalizes popup select selected rows according to multiple mode', () => {
53
+ const rows = [
54
+ { id: 1, name: 'A' },
55
+ { id: 2, name: 'B' },
56
+ ];
57
+
58
+ expect(normalizeRecordPickerSelectedRows(rows, true)).toEqual(rows);
59
+ expect(normalizeRecordPickerSelectedRows(rows, false)).toEqual([{ id: 1, name: 'A' }]);
60
+ expect(normalizeRecordPickerSelectedRows({ id: 1, name: 'A' }, false)).toEqual([{ id: 1, name: 'A' }]);
61
+ expect(normalizeRecordPickerSelectedRows(undefined, true)).toEqual([]);
62
+ });
63
+
64
+ it('normalizes popup select display value according to multiple mode', () => {
65
+ const fieldNames = { label: 'name', value: 'id' };
66
+ const rows = [
67
+ { id: 1, name: 'A' },
68
+ { id: 2, name: 'B' },
69
+ ];
70
+
71
+ expect(normalizeRecordPickerValue(rows, fieldNames, true)).toEqual([
72
+ { id: 1, name: 'A', label: 'A', value: 1 },
73
+ { id: 2, name: 'B', label: 'B', value: 2 },
74
+ ]);
75
+ expect(normalizeRecordPickerValue(rows, fieldNames, false)).toEqual({ id: 1, name: 'A', label: 'A', value: 1 });
76
+ expect(normalizeRecordPickerValue(undefined, fieldNames, true)).toEqual([]);
77
+ expect(normalizeRecordPickerValue(undefined, fieldNames, false)).toBeUndefined();
78
+ });
79
+
80
+ it('returns the empty popup select value for the current multiple mode', () => {
81
+ expect(getRecordPickerEmptyValue(true)).toEqual([]);
82
+ expect(getRecordPickerEmptyValue(false)).toBeUndefined();
83
+ });
84
+
85
+ it('clears popup select value to undefined when multiple mode changes', () => {
86
+ expect(getRecordPickerClearedValue()).toBeUndefined();
87
+ });
88
+
89
+ it('detects when changing multiple mode should clear popup select value', () => {
90
+ expect(shouldClearRecordPickerValueOnMultipleChange({ type: 'belongsToMany' }, true, false)).toBe(true);
91
+ expect(shouldClearRecordPickerValueOnMultipleChange({ type: 'belongsToMany' }, false, true)).toBe(true);
92
+ expect(shouldClearRecordPickerValueOnMultipleChange({ type: 'belongsToMany' }, true, true)).toBe(false);
93
+ expect(shouldClearRecordPickerValueOnMultipleChange({ type: 'belongsTo' }, true, false)).toBe(false);
94
+ });
95
+
35
96
  it('itemChain helpers: createParentItemAccessorsFromInputArgs works', () => {
36
97
  const inputArgs = {
37
98
  parentItem: { value: { id: 1 } },
@@ -12,10 +12,15 @@ import { Input } from 'antd';
12
12
  import React from 'react';
13
13
  import { customAlphabet as Alphabet } from 'nanoid';
14
14
  import { FieldModel } from '../base/FieldModel';
15
+ import { ScanInput } from '../../../components/form/ScanInput';
15
16
 
16
17
  export class InputFieldModel extends FieldModel {
17
18
  render() {
18
- return <Input {...this.props} />;
19
+ if (this.props.enableScan) {
20
+ return <ScanInput {...this.props} />;
21
+ }
22
+ const { enableScan, disableManualInput, ...inputProps } = this.props;
23
+ return <Input {...inputProps} />;
19
24
  }
20
25
  }
21
26
 
@@ -36,6 +41,56 @@ InputFieldModel.registerFlow({
36
41
  },
37
42
  });
38
43
 
44
+ InputFieldModel.registerFlow({
45
+ key: 'scanInputSettings',
46
+ sort: 800,
47
+ title: tExpr('Scan input settings'),
48
+ steps: {
49
+ enableScan: {
50
+ title: tExpr('Enable Scan'),
51
+ uiMode: { type: 'switch', key: 'enableScan' },
52
+ hideInSettings(ctx) {
53
+ return ctx.model.getProps().pattern === 'readPretty' || ctx.model.getProps().disabled;
54
+ },
55
+ defaultParams(ctx) {
56
+ return {
57
+ enableScan: !!ctx.model.props.enableScan,
58
+ };
59
+ },
60
+ handler(ctx, params) {
61
+ ctx.model.setProps({
62
+ enableScan: !!params.enableScan,
63
+ disableManualInput: params.enableScan ? ctx.model.props.disableManualInput : false,
64
+ });
65
+ },
66
+ },
67
+ disableManualInput: {
68
+ title: tExpr('Disable manual input'),
69
+ uiMode: { type: 'switch', key: 'disableManualInput' },
70
+ hideInSettings(ctx) {
71
+ const enableScanParams = ctx.model.getStepParams?.('scanInputSettings', 'enableScan') as
72
+ | { enableScan?: boolean }
73
+ | undefined;
74
+ const enableScan = Object.prototype.hasOwnProperty.call(enableScanParams || {}, 'enableScan')
75
+ ? !!enableScanParams?.enableScan
76
+ : !!ctx.model.props.enableScan;
77
+
78
+ return !enableScan || ctx.model.getProps().pattern === 'readPretty' || !!ctx.model.getProps().disabled;
79
+ },
80
+ defaultParams(ctx) {
81
+ return {
82
+ disableManualInput: !!ctx.model.props.disableManualInput,
83
+ };
84
+ },
85
+ handler(ctx, params) {
86
+ ctx.model.setProps({
87
+ disableManualInput: !!ctx.model.props.enableScan && !!params.disableManualInput,
88
+ });
89
+ },
90
+ },
91
+ },
92
+ });
93
+
39
94
  InputFieldModel.define({
40
95
  label: tExpr('Input'),
41
96
  });