@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
@@ -0,0 +1,92 @@
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 { render, screen, waitFor } from '@testing-library/react';
11
+ import React from 'react';
12
+ import { MemoryRouter } from 'react-router-dom';
13
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
14
+ import {
15
+ FLOW_SETTINGS_PREFERENCE_STORAGE_KEY,
16
+ writeFlowSettingsPreference,
17
+ } from '../../flow/admin-shell/admin-layout/flowSettingsPreference';
18
+ import { ACLRolesCheckProvider } from '../ACLProvider';
19
+
20
+ const mocks = vi.hoisted(() => ({
21
+ app: undefined as unknown,
22
+ }));
23
+
24
+ vi.mock('../../hooks/useApp', () => ({
25
+ useApp: () => mocks.app,
26
+ }));
27
+
28
+ describe('ACLRolesCheckProvider', () => {
29
+ beforeEach(() => {
30
+ window.localStorage.removeItem(FLOW_SETTINGS_PREFERENCE_STORAGE_KEY);
31
+ });
32
+
33
+ it('exits UI editing mode when the current role cannot configure pages', async () => {
34
+ const disable = vi.fn(async () => undefined);
35
+ const setRole = vi.fn();
36
+ const aclContext: Record<string, unknown> = {};
37
+ const appContext = {
38
+ acl: undefined,
39
+ defineProperty(name: string, descriptor: PropertyDescriptor) {
40
+ Object.defineProperty(this, name, descriptor);
41
+ },
42
+ ...aclContext,
43
+ };
44
+
45
+ mocks.app = {
46
+ apiClient: {
47
+ auth: {
48
+ role: 'member',
49
+ setRole,
50
+ },
51
+ request: vi.fn(async () => ({
52
+ data: {
53
+ data: {
54
+ role: 'viewer',
55
+ snippets: [],
56
+ },
57
+ meta: {},
58
+ },
59
+ })),
60
+ },
61
+ context: appContext,
62
+ flowEngine: {
63
+ flowSettings: {
64
+ disable,
65
+ },
66
+ },
67
+ pluginSettingsManager: {
68
+ setAclSnippets: vi.fn(),
69
+ },
70
+ renderComponent: () => <div>Loading</div>,
71
+ router: {
72
+ isSkippedAuthCheckRoute: () => false,
73
+ },
74
+ };
75
+ writeFlowSettingsPreference(true);
76
+
77
+ render(
78
+ <MemoryRouter initialEntries={['/admin']}>
79
+ <ACLRolesCheckProvider>
80
+ <div>Page content</div>
81
+ </ACLRolesCheckProvider>
82
+ </MemoryRouter>,
83
+ );
84
+
85
+ expect(await screen.findByText('Page content')).toBeInTheDocument();
86
+ await waitFor(() => {
87
+ expect(disable).toHaveBeenCalledTimes(1);
88
+ });
89
+ expect(window.localStorage.getItem(FLOW_SETTINGS_PREFERENCE_STORAGE_KEY)).toBe('0');
90
+ expect(setRole).toHaveBeenCalledWith('viewer');
91
+ });
92
+ });
@@ -0,0 +1,52 @@
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 { describe, expect, it } from 'vitest';
11
+ import { getCoreFieldConfigureState, reverseFieldConfigureItems } from '../field-configure';
12
+
13
+ describe('inverse relationship configuration', () => {
14
+ it.each(['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'])(
15
+ 'keeps the %s inverse relationship type disabled when creating or editing a field',
16
+ (type) => {
17
+ for (const createOnly of [true, false]) {
18
+ for (const showReverseFieldConfig of [true, false]) {
19
+ const values = {
20
+ autoCreateReverseField: true,
21
+ reverseField: { type, ...(createOnly ? {} : { key: 'existing-reverse-field' }) },
22
+ };
23
+
24
+ expect(
25
+ getCoreFieldConfigureState('reverseField.type', values, { createOnly, showReverseFieldConfig }),
26
+ ).toEqual({
27
+ disabled: true,
28
+ hidden: !showReverseFieldConfig,
29
+ });
30
+ }
31
+ }
32
+ },
33
+ );
34
+
35
+ it.each(['reverseField.name', 'reverseField.uiSchema.title'])(
36
+ 'allows editing %s only when the inverse configuration is visible',
37
+ (name) => {
38
+ for (const showReverseFieldConfig of [true, false]) {
39
+ expect(getCoreFieldConfigureState(name, {}, { showReverseFieldConfig })).toEqual({
40
+ disabled: !showReverseFieldConfig,
41
+ hidden: !showReverseFieldConfig,
42
+ });
43
+ }
44
+ },
45
+ );
46
+
47
+ it('also disables the inverse relationship type in explicit configure items', () => {
48
+ const item = reverseFieldConfigureItems().find(({ name }) => name === 'reverseField.type');
49
+
50
+ expect(item?.disabled).toBe(true);
51
+ });
52
+ });
@@ -271,7 +271,7 @@ export function getCoreFieldConfigureState(
271
271
 
272
272
  if (name.startsWith('reverseField.')) {
273
273
  return {
274
- disabled: !context.showReverseFieldConfig,
274
+ disabled: name === 'reverseField.type' || !context.showReverseFieldConfig,
275
275
  hidden: !context.showReverseFieldConfig,
276
276
  };
277
277
  }
@@ -514,7 +514,7 @@ export function reverseFieldConfigureItems(): FieldConfigureItem[] {
514
514
  { label: "{{t('BelongsToMany')}}", value: 'belongsToMany' },
515
515
  ],
516
516
  hidden: ({ context, values }) => !context.showReverseFieldConfig && !get(values, 'autoCreateReverseField'),
517
- disabled: ({ context }) => !context.showReverseFieldConfig,
517
+ disabled: true,
518
518
  },
519
519
  {
520
520
  name: 'reverseField.uiSchema.title',
@@ -11,7 +11,7 @@ import { FileImageOutlined, LeftOutlined } from '@ant-design/icons';
11
11
  import { css } from '@emotion/css';
12
12
  import { Button, message, theme } from 'antd';
13
13
  import { Html5Qrcode } from 'html5-qrcode';
14
- import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
14
+ import React, { useCallback, useEffect, useId, useRef, useState } from 'react';
15
15
  import ReactDOM from 'react-dom';
16
16
  import { useTranslation } from 'react-i18next';
17
17
  import { ScanBox } from './ScanBox';
@@ -26,6 +26,14 @@ type CodeScannerProps = {
26
26
  };
27
27
 
28
28
  const MAX_CODE_IMAGE_SIZE = 10 * 1024 * 1024;
29
+ const SCANNER_RENDER_WIDTH = 1280;
30
+
31
+ function getViewportSize() {
32
+ return {
33
+ width: Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0),
34
+ height: Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0),
35
+ };
36
+ }
29
37
 
30
38
  function CodeScannerContent({ visible, formatsToSupport, onClose, onScanSuccess }: CodeScannerProps) {
31
39
  const { t } = useTranslation();
@@ -33,17 +41,17 @@ function CodeScannerContent({ visible, formatsToSupport, onClose, onScanSuccess
33
41
  const inputId = useId().replace(/:/g, '');
34
42
  const scannerElementId = `code-scanner-${inputId}`;
35
43
  const imgUploaderRef = useRef<HTMLInputElement>(null);
44
+ const scannerViewportRef = useRef<HTMLDivElement>(null);
36
45
  const [cameraAvailable, setCameraAvailable] = useState(false);
46
+ const [scannerSize, setScannerSize] = useState({ width: 0, height: 0 });
47
+ const [viewport, setViewport] = useState(getViewportSize);
37
48
 
38
- const viewport = useMemo(() => {
39
- const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
40
- const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
41
- return { width, height };
42
- }, []);
43
- const scanBoxSize = useMemo(
44
- () => getCodeScanBoxSize(viewport.width, viewport.height),
45
- [viewport.height, viewport.width],
46
- );
49
+ const previewScale = scannerSize.width
50
+ ? Math.max(viewport.width / scannerSize.width, viewport.height / scannerSize.height)
51
+ : Math.min(1, viewport.width / SCANNER_RENDER_WIDTH);
52
+ const visibleScanBoxSize = scannerSize.width
53
+ ? getCodeScanBoxSize(viewport.width, viewport.height)
54
+ : { width: 0, height: 0 };
47
55
 
48
56
  const showScanFailure = useCallback(() => {
49
57
  message.error(t('Code recognition failed, please scan again'));
@@ -76,12 +84,19 @@ function CodeScannerContent({ visible, formatsToSupport, onClose, onScanSuccess
76
84
  enabled: visible && cameraAvailable,
77
85
  elementId: scannerElementId,
78
86
  formatsToSupport,
79
- scanBoxSize,
87
+ onScannerSizeChanged: setScannerSize,
88
+ scanViewportRef: scannerViewportRef,
80
89
  onScanSuccess: handleScanSuccess,
81
90
  onScanFailure: showScanFailure,
82
91
  onCameraStartFailure: showCameraStartFailure,
83
92
  });
84
93
 
94
+ useEffect(() => {
95
+ const handleResize = () => setViewport(getViewportSize());
96
+ window.addEventListener('resize', handleResize);
97
+ return () => window.removeEventListener('resize', handleResize);
98
+ }, []);
99
+
85
100
  useEffect(() => {
86
101
  if (!visible) {
87
102
  return;
@@ -152,18 +167,22 @@ function CodeScannerContent({ visible, formatsToSupport, onClose, onScanSuccess
152
167
  inset: 0;
153
168
  overflow: hidden;
154
169
  `;
170
+ const scannerSurfaceClass = css`
171
+ position: absolute;
172
+ top: 50%;
173
+ left: 50%;
174
+ transform-origin: center;
175
+ `;
155
176
  const scannerClass = css`
156
177
  position: relative;
157
178
  width: 100%;
158
- height: 100%;
159
- overflow: hidden;
179
+ line-height: 0;
160
180
 
161
181
  video {
162
- position: absolute !important;
163
- inset: 0 !important;
164
182
  width: 100% !important;
165
- height: 100% !important;
166
- object-fit: cover !important;
183
+ height: auto !important;
184
+ max-width: none !important;
185
+ display: block !important;
167
186
  }
168
187
 
169
188
  #qr-shaded-region {
@@ -190,27 +209,37 @@ function CodeScannerContent({ visible, formatsToSupport, onClose, onScanSuccess
190
209
 
191
210
  return (
192
211
  <div className={rootClass}>
193
- <div className={scannerWrapperClass}>
194
- <div id={scannerElementId} className={scannerClass} />
212
+ <div ref={scannerViewportRef} className={scannerWrapperClass}>
213
+ <div
214
+ className={scannerSurfaceClass}
215
+ style={{
216
+ width: `${SCANNER_RENDER_WIDTH}px`,
217
+ transform: `translate(-50%, -50%) scale(${previewScale})`,
218
+ }}
219
+ >
220
+ <div id={scannerElementId} className={scannerClass} />
221
+ </div>
195
222
  </div>
223
+ {cameraAvailable && scannerSize.width > 0 && (
224
+ <ScanBox
225
+ style={{
226
+ position: 'fixed',
227
+ top: `${(viewport.height - visibleScanBoxSize.height) / 2}px`,
228
+ left: `${(viewport.width - visibleScanBoxSize.width) / 2}px`,
229
+ width: `${visibleScanBoxSize.width}px`,
230
+ height: `${visibleScanBoxSize.height}px`,
231
+ }}
232
+ />
233
+ )}
234
+ <Button
235
+ aria-label={t('Close')}
236
+ className={closeButtonClass}
237
+ icon={<LeftOutlined />}
238
+ type="text"
239
+ onClick={onClose}
240
+ />
196
241
  {cameraAvailable && (
197
242
  <>
198
- <ScanBox
199
- style={{
200
- position: 'fixed',
201
- top: `${(viewport.height - scanBoxSize.height) / 2}px`,
202
- left: `${(viewport.width - scanBoxSize.width) / 2}px`,
203
- width: `${scanBoxSize.width}px`,
204
- height: `${scanBoxSize.height}px`,
205
- }}
206
- />
207
- <Button
208
- aria-label={t('Close')}
209
- className={closeButtonClass}
210
- icon={<LeftOutlined />}
211
- type="text"
212
- onClick={onClose}
213
- />
214
243
  <Button className={albumClass} icon={<FileImageOutlined />} type="text" onClick={handleImageButtonClick}>
215
244
  {t('Album')}
216
245
  </Button>
@@ -0,0 +1,107 @@
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 { act, render, waitFor } from '@testing-library/react';
11
+ import React from 'react';
12
+ import { afterEach, describe, expect, it, vi } from 'vitest';
13
+ import { CodeScanner } from '../CodeScanner';
14
+
15
+ const mocks = vi.hoisted(() => ({
16
+ getCameras: vi.fn().mockResolvedValue([{ id: 'rear-camera' }]),
17
+ scanViewport: null as HTMLElement | null,
18
+ }));
19
+
20
+ vi.mock('html5-qrcode', () => ({
21
+ Html5Qrcode: {
22
+ getCameras: mocks.getCameras,
23
+ },
24
+ Html5QrcodeScannerState: {
25
+ PAUSED: 3,
26
+ SCANNING: 2,
27
+ },
28
+ Html5QrcodeSupportedFormats: {
29
+ CODABAR: 2,
30
+ CODE_128: 5,
31
+ CODE_39: 3,
32
+ CODE_93: 4,
33
+ DATA_MATRIX: 6,
34
+ EAN_13: 9,
35
+ EAN_8: 10,
36
+ ITF: 8,
37
+ PDF_417: 11,
38
+ QR_CODE: 0,
39
+ UPC_A: 14,
40
+ UPC_E: 15,
41
+ },
42
+ }));
43
+
44
+ vi.mock('react-i18next', () => ({
45
+ useTranslation: () => ({ t: (key: string) => key }),
46
+ }));
47
+
48
+ vi.mock('../useCodeScanner', async (importOriginal) => {
49
+ const actual = await importOriginal<typeof import('../useCodeScanner')>();
50
+ const ReactModule = await import('react');
51
+
52
+ return {
53
+ ...actual,
54
+ useCodeScanner: ({
55
+ enabled,
56
+ onScannerSizeChanged,
57
+ scanViewportRef,
58
+ }: {
59
+ enabled: boolean;
60
+ onScannerSizeChanged?: (size: { width: number; height: number }) => void;
61
+ scanViewportRef?: { current: HTMLElement | null };
62
+ }) => {
63
+ ReactModule.useEffect(() => {
64
+ if (enabled) {
65
+ mocks.scanViewport = scanViewportRef?.current || null;
66
+ onScannerSizeChanged?.({ width: 1280, height: 720 });
67
+ }
68
+ }, [enabled, onScannerSizeChanged, scanViewportRef]);
69
+
70
+ return { startScanFile: vi.fn() };
71
+ },
72
+ };
73
+ });
74
+
75
+ describe('CodeScanner', () => {
76
+ afterEach(() => {
77
+ mocks.scanViewport = null;
78
+ vi.restoreAllMocks();
79
+ });
80
+
81
+ it('keeps the camera preview covering portrait and landscape viewports', async () => {
82
+ let viewportWidth = 390;
83
+ let viewportHeight = 844;
84
+ vi.spyOn(window, 'innerWidth', 'get').mockImplementation(() => viewportWidth);
85
+ vi.spyOn(window, 'innerHeight', 'get').mockImplementation(() => viewportHeight);
86
+
87
+ render(<CodeScanner visible onClose={() => undefined} onScanSuccess={() => undefined} />);
88
+
89
+ await waitFor(() => expect(document.getElementById('code-scan-box')).toBeInTheDocument());
90
+ expect(mocks.scanViewport).toBeInstanceOf(HTMLElement);
91
+
92
+ const scannerElement = document.querySelector<HTMLElement>('[id^="code-scanner-"]');
93
+ const scannerSurface = scannerElement?.parentElement;
94
+ const scanBox = document.getElementById('code-scan-box');
95
+ expect(scannerSurface?.style.transform).toBe(`translate(-50%, -50%) scale(${844 / 720})`);
96
+ expect(scanBox).toHaveStyle({ width: '351px', height: '540px' });
97
+
98
+ viewportWidth = 844;
99
+ viewportHeight = 390;
100
+ act(() => window.dispatchEvent(new Event('resize')));
101
+
102
+ await waitFor(() => {
103
+ expect(scannerSurface?.style.transform).toBe(`translate(-50%, -50%) scale(${844 / 1280})`);
104
+ expect(scanBox).toHaveStyle({ width: '759px', height: '273px' });
105
+ });
106
+ });
107
+ });