@nocobase/client-v2 2.2.0-beta.1 → 2.2.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.
- package/es/BaseApplication.d.ts +3 -0
- package/es/PluginSettingsManager.d.ts +33 -0
- package/es/RouteRepository.d.ts +29 -6
- package/es/RouterManager.d.ts +15 -0
- package/es/authRedirect.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
- package/es/components/form/JsonTextArea.d.ts +2 -1
- package/es/components/form/VariableJsonTextArea.d.ts +19 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/components/index.d.ts +1 -0
- package/es/entry-actions/EntryActionManager.d.ts +24 -0
- package/es/entry-actions/index.d.ts +9 -0
- package/es/flow/actions/afterSuccess.d.ts +8 -1
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
- package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
- package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
- package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
- package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
- package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/models/base/ActionModelCore.d.ts +2 -0
- package/es/flow/models/base/GridModel.d.ts +1 -1
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
- package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
- package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
- package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
- package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
- package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
- package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
- package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
- package/es/flow/routeTransientInputArgs.d.ts +14 -0
- package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
- package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
- package/es/flow/utils/formValueDeps.d.ts +32 -0
- package/es/flow-compat/FieldValidation.d.ts +2 -1
- package/es/index.d.ts +4 -1
- package/es/index.mjs +315 -167
- package/es/utils/markdownSanitize.d.ts +11 -0
- package/lib/index.js +315 -167
- package/package.json +8 -7
- package/src/BaseApplication.tsx +12 -5
- package/src/PluginSettingsManager.ts +53 -0
- package/src/RouteRepository.ts +151 -24
- package/src/RouterManager.tsx +142 -1
- package/src/__tests__/PluginSettingsManager.test.ts +13 -0
- package/src/__tests__/RouteRepository.test.ts +239 -0
- package/src/__tests__/RouterManager.test.ts +125 -0
- package/src/__tests__/app.test.tsx +55 -0
- package/src/__tests__/authRedirect.test.ts +17 -0
- package/src/__tests__/exports.test.ts +16 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
- package/src/__tests__/plugin-manager.test.tsx +44 -2
- package/src/__tests__/settings-center.test.tsx +40 -1
- package/src/authRedirect.ts +38 -0
- package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
- package/src/collection-manager/field-configure.ts +1 -1
- package/src/collection-manager/interfaces/id.ts +1 -1
- package/src/collection-manager/interfaces/m2m.tsx +2 -2
- package/src/collection-manager/interfaces/m2o.tsx +2 -2
- package/src/collection-manager/interfaces/nanoid.ts +1 -1
- package/src/collection-manager/interfaces/o2m.tsx +2 -2
- package/src/collection-manager/interfaces/obo.tsx +2 -2
- package/src/collection-manager/interfaces/oho.tsx +2 -2
- package/src/collection-manager/interfaces/properties/index.ts +2 -2
- package/src/collection-manager/interfaces/uuid.ts +1 -1
- package/src/components/AppComponents.tsx +19 -3
- package/src/components/form/JsonTextArea.tsx +21 -11
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +145 -2
- package/src/components/form/ScanInput/useCodeScanner.ts +154 -2
- package/src/components/form/VariableJsonTextArea.tsx +175 -0
- package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
- package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/index.ts +1 -0
- package/src/entry-actions/EntryActionManager.ts +76 -0
- package/src/entry-actions/index.ts +10 -0
- package/src/flow/FlowPage.tsx +38 -3
- package/src/flow/__tests__/FlowPage.test.tsx +201 -27
- package/src/flow/__tests__/FlowRoute.test.tsx +1088 -105
- package/src/flow/__tests__/getKey.test.ts +7 -0
- package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
- package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +120 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
- package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
- package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
- package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +164 -0
- package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
- package/src/flow/actions/afterSuccess.tsx +142 -3
- package/src/flow/actions/customVariable.tsx +1 -2
- package/src/flow/actions/dataScopeFilter.ts +20 -1
- package/src/flow/actions/dateTimeFormat.tsx +42 -28
- package/src/flow/actions/index.ts +1 -1
- package/src/flow/actions/linkageRules.tsx +123 -35
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
- package/src/flow/actions/openView.tsx +59 -5
- package/src/flow/actions/runjs.tsx +2 -14
- package/src/flow/actions/validation.tsx +62 -30
- package/src/flow/admin-shell/BaseLayoutModel.tsx +149 -3
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +187 -42
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1020 -110
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +74 -13
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +486 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +64 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
- package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
- package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
- package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +518 -2
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +214 -2
- package/src/flow/admin-shell/admin-layout/index.ts +1 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
- package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
- package/src/flow/common/Markdown/Display.tsx +14 -3
- package/src/flow/common/Markdown/Edit.tsx +19 -7
- package/src/flow/components/DefaultValue.tsx +1 -2
- package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
- package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
- package/src/flow/components/FieldAssignValueInput.tsx +15 -11
- package/src/flow/components/FlowRoute.tsx +186 -25
- package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +134 -0
- package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
- package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
- package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
- package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
- package/src/flow/components/fieldAssignOptions.ts +155 -27
- package/src/flow/flows/editMarkdownFlow.tsx +1 -1
- package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
- package/src/flow/index.ts +1 -1
- package/src/flow/internal/components/Markdown/util.ts +2 -1
- package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
- package/src/flow/models/actions/JSActionModel.tsx +2 -7
- package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
- package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
- package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
- package/src/flow/models/base/ActionModel.tsx +21 -8
- package/src/flow/models/base/ActionModelCore.tsx +11 -4
- package/src/flow/models/base/GridModel.tsx +38 -7
- package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
- package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
- package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +32 -3
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
- package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +111 -0
- package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
- package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
- package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
- package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
- package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
- package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
- package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
- package/src/flow/models/blocks/form/submitHandler.ts +17 -3
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
- package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
- package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
- package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
- package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
- package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +38 -14
- package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
- package/src/flow/models/blocks/table/TableColumnModel.tsx +42 -5
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +210 -1
- package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
- package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
- package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
- package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +30 -6
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +61 -17
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
- package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
- package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
- package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
- package/src/flow/models/fields/InputFieldModel.tsx +40 -2
- package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
- package/src/flow/models/fields/JSFieldModel.tsx +2 -7
- package/src/flow/models/fields/JSItemModel.tsx +2 -14
- package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
- package/src/flow/models/fields/TextareaFieldModel.tsx +72 -3
- package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +131 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
- package/src/flow/models/topbar/TopbarActionModel.tsx +168 -10
- package/src/flow/resolveViewParamsToViewList.tsx +11 -3
- package/src/flow/routeTransientInputArgs.ts +27 -0
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +300 -0
- package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
- package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
- package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
- package/src/flow/utils/formValueDeps.ts +170 -0
- package/src/flow-compat/FieldValidation.tsx +122 -60
- package/src/flow-compat/Popover.tsx +43 -4
- package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
- package/src/index.ts +10 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
- package/src/nocobase-buildin-plugin/index.tsx +51 -15
- package/src/settings-center/AdminSettingsLayout.tsx +10 -2
- package/src/settings-center/SystemSettingsPage.tsx +1 -2
- package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
- package/src/settings-center/plugin-manager/index.tsx +3 -0
- package/src/settings-center/utils.tsx +0 -6
- package/src/utils/markdownSanitize.ts +88 -0
|
@@ -18,6 +18,19 @@ class TestAclPlugin extends Plugin {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
class TestSettingsLinkPlugin extends Plugin {
|
|
22
|
+
async load() {
|
|
23
|
+
this.app.pluginSettingsManager.addMenuItem({ key: 'shared-settings', title: 'Shared settings' });
|
|
24
|
+
this.app.pluginSettingsManager.addPageTabItem({
|
|
25
|
+
menuKey: 'shared-settings',
|
|
26
|
+
key: 'target',
|
|
27
|
+
title: 'Target settings',
|
|
28
|
+
Component: () => <div>Target settings page</div>,
|
|
29
|
+
});
|
|
30
|
+
this.app.pluginSettingsManager.setPluginSettingsLink('demo-plugin', 'shared-settings.target');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
type MockClientApplication = ReturnType<typeof createMockClient>;
|
|
22
35
|
|
|
23
36
|
const renderApp = (app: MockClientApplication) => {
|
|
@@ -35,9 +48,9 @@ const waitForGetRequests = async (app: MockClientApplication, urls: string[]) =>
|
|
|
35
48
|
);
|
|
36
49
|
};
|
|
37
50
|
|
|
38
|
-
const setupApp = (pmList: any[]) => {
|
|
51
|
+
const setupApp = (pmList: any[], plugins: Array<typeof Plugin> = []) => {
|
|
39
52
|
const app = createMockClient({
|
|
40
|
-
plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
|
|
53
|
+
plugins: [NocoBaseBuildInPlugin, TestAclPlugin, ...plugins],
|
|
41
54
|
router: { type: 'memory', initialEntries: ['/admin/settings/plugin-manager'] },
|
|
42
55
|
});
|
|
43
56
|
|
|
@@ -174,4 +187,33 @@ describe('plugin-manager page', () => {
|
|
|
174
187
|
expect(removeCall?.params).toMatchObject({ filterByTk: 'demo-plugin' });
|
|
175
188
|
});
|
|
176
189
|
});
|
|
190
|
+
|
|
191
|
+
it('shows Settings for a plugin linked to a different settings page', async () => {
|
|
192
|
+
const app = setupApp(
|
|
193
|
+
[
|
|
194
|
+
{
|
|
195
|
+
name: 'demo-plugin',
|
|
196
|
+
packageName: '@nocobase/demo-plugin',
|
|
197
|
+
displayName: 'Demo plugin',
|
|
198
|
+
description: 'A demo',
|
|
199
|
+
enabled: true,
|
|
200
|
+
builtIn: false,
|
|
201
|
+
removable: false,
|
|
202
|
+
version: '0.1.0',
|
|
203
|
+
isCompatible: true,
|
|
204
|
+
keywords: [],
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
[TestSettingsLinkPlugin],
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
renderApp(app);
|
|
211
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check', 'pm:list']);
|
|
212
|
+
|
|
213
|
+
const card = await screen.findByRole('button', { name: 'Demo plugin' });
|
|
214
|
+
const settingsLink = within(card.closest('.ant-card') as HTMLElement).getByText('Settings');
|
|
215
|
+
fireEvent.click(settingsLink);
|
|
216
|
+
|
|
217
|
+
expect(await screen.findByText('Target settings page')).toBeInTheDocument();
|
|
218
|
+
});
|
|
177
219
|
});
|
|
@@ -14,7 +14,7 @@ import { message } from 'antd';
|
|
|
14
14
|
import { AdminSettingsLayoutModel as ClientV2AdminSettingsLayoutModel } from '../settings-center';
|
|
15
15
|
import { AdminSettingsLayoutModel as ClientV1AdminSettingsLayoutModel } from '../../../client/src/pm/AdminSettingsLayoutModel';
|
|
16
16
|
import { NocoBaseBuildInPlugin } from '../nocobase-buildin-plugin';
|
|
17
|
-
import { matchSettingsRoute } from '../settings-center/utils';
|
|
17
|
+
import { matchSettingsRoute, sortTopLevelSettings } from '../settings-center/utils';
|
|
18
18
|
|
|
19
19
|
class TestAclPlugin extends Plugin {
|
|
20
20
|
async load() {
|
|
@@ -168,6 +168,12 @@ describe('settings center', () => {
|
|
|
168
168
|
});
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
+
it('should sort system-settings with other top-level settings by normal ordering', () => {
|
|
172
|
+
const settings = [{ name: 'system-settings' }, { name: 'api-keys' }, { name: 'backups' }] as any;
|
|
173
|
+
|
|
174
|
+
expect(sortTopLevelSettings(settings).map((item) => item.name)).toEqual(['api-keys', 'backups', 'system-settings']);
|
|
175
|
+
});
|
|
176
|
+
|
|
171
177
|
it('should redirect /admin/settings to system-settings by default', async () => {
|
|
172
178
|
const app = createMockClient({
|
|
173
179
|
plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
|
|
@@ -354,6 +360,39 @@ describe('settings center', () => {
|
|
|
354
360
|
expect(screen.getByRole('menuitem', { name: 'Menu ACL Demo' })).toBeInTheDocument();
|
|
355
361
|
});
|
|
356
362
|
|
|
363
|
+
it('should allow the settings sidebar menu to scroll independently', async () => {
|
|
364
|
+
class ManySettingsPlugin extends Plugin {
|
|
365
|
+
async load() {
|
|
366
|
+
for (let index = 0; index < 30; index += 1) {
|
|
367
|
+
this.pluginSettingsManager.addMenuItem({
|
|
368
|
+
key: `scroll-demo-${index}`,
|
|
369
|
+
title: `Scroll demo ${index}`,
|
|
370
|
+
});
|
|
371
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
372
|
+
menuKey: `scroll-demo-${index}`,
|
|
373
|
+
key: 'index',
|
|
374
|
+
title: `Scroll demo ${index}`,
|
|
375
|
+
Component: () => <div>{`Scroll demo page ${index}`}</div>,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const app = createMockClient({
|
|
382
|
+
plugins: [NocoBaseBuildInPlugin, TestAclPlugin, ManySettingsPlugin],
|
|
383
|
+
router: { type: 'memory', initialEntries: ['/admin/settings/scroll-demo-29'] },
|
|
384
|
+
});
|
|
385
|
+
mockAdminRuntime(app);
|
|
386
|
+
|
|
387
|
+
await renderApp(app);
|
|
388
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
389
|
+
|
|
390
|
+
expect(await screen.findByText('Scroll demo page 29')).toBeInTheDocument();
|
|
391
|
+
|
|
392
|
+
const sidebar = screen.getByRole('menuitem', { name: 'Scroll demo 29' }).closest('.ant-layout-sider');
|
|
393
|
+
expect(sidebar).toHaveStyle({ overflowY: 'auto' });
|
|
394
|
+
});
|
|
395
|
+
|
|
357
396
|
it('should save system settings through systemSettings:put', async () => {
|
|
358
397
|
const app = createMockClient({
|
|
359
398
|
plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
|
package/src/authRedirect.ts
CHANGED
|
@@ -204,6 +204,44 @@ function getDefaultV2AdminRedirectPath(app: AppLike) {
|
|
|
204
204
|
return joinRootRelativePath(getV2EffectiveBasePath(app), '/admin');
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
function isSafeRootRelativePath(value?: string | null) {
|
|
208
|
+
return !!value && value.startsWith('/') && !value.startsWith('//') && !value.startsWith('/\\');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function preserveTrailingSlash(originalPathname: string, value: string) {
|
|
212
|
+
if (originalPathname !== '/' && originalPathname.endsWith('/') && !value.endsWith('/')) {
|
|
213
|
+
return `${value}/`;
|
|
214
|
+
}
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function normalizeV2RedirectPath(app: AppLike, target?: string | null, fallbackPath = '/admin/') {
|
|
219
|
+
// In a v2 sub-app, publicPath can be `/v/` while the active router
|
|
220
|
+
// basename is `/v/apps/a/`. Redirects must resolve under the basename.
|
|
221
|
+
const basePath = trimTrailingSlashes(getV2EffectiveBasePath(app)) || '/';
|
|
222
|
+
const fallbackTarget = isSafeRootRelativePath(fallbackPath) ? fallbackPath : '/admin/';
|
|
223
|
+
const rawTarget = isSafeRootRelativePath(target) ? target : fallbackTarget;
|
|
224
|
+
let { pathname, search, hash } = splitPathLike(rawTarget);
|
|
225
|
+
let normalizedPathname = normalizePathname(pathname);
|
|
226
|
+
|
|
227
|
+
// Already under the current v2 runtime, e.g. `/v/apps/a/admin/`.
|
|
228
|
+
if (basePath === '/' || normalizedPathname === basePath || normalizedPathname.startsWith(`${basePath}/`)) {
|
|
229
|
+
return `${preserveTrailingSlash(pathname, normalizedPathname)}${normalizeSearch(search)}${normalizeHash(hash)}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const publicPath = trimTrailingSlashes(getV2PublicPath(app)) || '/';
|
|
233
|
+
// Under v2 publicPath but outside the current basename, e.g. `/v/admin/`
|
|
234
|
+
// inside `/v/apps/a/`; use fallback so it lands on `/v/apps/a/admin/`.
|
|
235
|
+
if (publicPath !== '/' && (normalizedPathname === publicPath || normalizedPathname.startsWith(`${publicPath}/`))) {
|
|
236
|
+
({ pathname, search, hash } = splitPathLike(fallbackTarget));
|
|
237
|
+
normalizedPathname = normalizePathname(pathname);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Basename-relative target, e.g. `/admin/`, becomes `/v/apps/a/admin/`.
|
|
241
|
+
const joinedPathname = preserveTrailingSlash(pathname, joinRootRelativePath(basePath, normalizedPathname));
|
|
242
|
+
return `${joinedPathname}${normalizeSearch(search)}${normalizeHash(hash)}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
207
245
|
/**
|
|
208
246
|
* 将当前 v2 页面地址转换为根相对 redirect 路径。
|
|
209
247
|
*
|
|
@@ -115,6 +115,7 @@ export abstract class CollectionFieldInterface {
|
|
|
115
115
|
titleUsable?: boolean;
|
|
116
116
|
usePathOptions?(field: any): any;
|
|
117
117
|
hidden?: boolean;
|
|
118
|
+
creatable?: boolean;
|
|
118
119
|
|
|
119
120
|
addComponentOption(componentOption: CollectionFieldInterfaceComponentOption) {
|
|
120
121
|
if (!this.componentOptions) {
|
|
@@ -530,7 +530,7 @@ export function reverseFieldConfigureItems(): FieldConfigureItem[] {
|
|
|
530
530
|
component: 'Input',
|
|
531
531
|
required: true,
|
|
532
532
|
description:
|
|
533
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
533
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
534
534
|
hidden: ({ context, values }) => !context.showReverseFieldConfig && !get(values, 'autoCreateReverseField'),
|
|
535
535
|
disabled: ({ context }) => !context.showReverseFieldConfig,
|
|
536
536
|
},
|
|
@@ -48,7 +48,7 @@ export class IdFieldInterface extends CollectionFieldInterface {
|
|
|
48
48
|
'x-decorator': 'FormItem',
|
|
49
49
|
'x-component': 'Input',
|
|
50
50
|
description:
|
|
51
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
51
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
52
52
|
},
|
|
53
53
|
};
|
|
54
54
|
filterable = {
|
|
@@ -158,7 +158,7 @@ export class M2MFieldInterface extends CollectionFieldInterface {
|
|
|
158
158
|
required: true,
|
|
159
159
|
default: '{{ useNewId("f_") }}',
|
|
160
160
|
description:
|
|
161
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
161
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
162
162
|
'x-decorator': 'FormItem',
|
|
163
163
|
'x-component': 'ForeignKey',
|
|
164
164
|
'x-validator': 'uid',
|
|
@@ -192,7 +192,7 @@ export class M2MFieldInterface extends CollectionFieldInterface {
|
|
|
192
192
|
required: true,
|
|
193
193
|
default: '{{ useNewId("f_") }}',
|
|
194
194
|
description:
|
|
195
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
195
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
196
196
|
'x-decorator': 'FormItem',
|
|
197
197
|
'x-component': 'ForeignKey',
|
|
198
198
|
'x-validator': 'uid',
|
|
@@ -69,7 +69,7 @@ export class M2OFieldInterface extends CollectionFieldInterface {
|
|
|
69
69
|
'x-decorator': 'FormItem',
|
|
70
70
|
'x-component': 'Input',
|
|
71
71
|
description:
|
|
72
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
72
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
73
73
|
},
|
|
74
74
|
type: relationshipType,
|
|
75
75
|
grid: {
|
|
@@ -123,7 +123,7 @@ export class M2OFieldInterface extends CollectionFieldInterface {
|
|
|
123
123
|
required: true,
|
|
124
124
|
default: '{{ useNewId("f_") }}',
|
|
125
125
|
description:
|
|
126
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
126
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
127
127
|
'x-decorator': 'FormItem',
|
|
128
128
|
'x-component': 'ForeignKey',
|
|
129
129
|
'x-validator': 'uid',
|
|
@@ -42,7 +42,7 @@ export class NanoidFieldInterface extends CollectionFieldInterface {
|
|
|
42
42
|
'x-component': 'Input',
|
|
43
43
|
'x-disabled': '{{ !createOnly }}',
|
|
44
44
|
description:
|
|
45
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
45
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
46
46
|
},
|
|
47
47
|
customAlphabet: {
|
|
48
48
|
type: 'string',
|
|
@@ -68,7 +68,7 @@ export class O2MFieldInterface extends CollectionFieldInterface {
|
|
|
68
68
|
'x-decorator': 'FormItem',
|
|
69
69
|
'x-component': 'Input',
|
|
70
70
|
description:
|
|
71
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
71
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
72
72
|
},
|
|
73
73
|
type: relationshipType,
|
|
74
74
|
grid: {
|
|
@@ -135,7 +135,7 @@ export class O2MFieldInterface extends CollectionFieldInterface {
|
|
|
135
135
|
required: true,
|
|
136
136
|
default: '{{ useNewId("f_") }}',
|
|
137
137
|
description:
|
|
138
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
138
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
139
139
|
'x-decorator': 'FormItem',
|
|
140
140
|
'x-component': 'ForeignKey',
|
|
141
141
|
'x-validator': 'uid',
|
|
@@ -55,7 +55,7 @@ export class OBOFieldInterface extends CollectionFieldInterface {
|
|
|
55
55
|
'x-decorator': 'FormItem',
|
|
56
56
|
'x-component': 'Input',
|
|
57
57
|
description:
|
|
58
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
58
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
59
59
|
},
|
|
60
60
|
type: relationshipType,
|
|
61
61
|
grid: {
|
|
@@ -109,7 +109,7 @@ export class OBOFieldInterface extends CollectionFieldInterface {
|
|
|
109
109
|
required: true,
|
|
110
110
|
default: '{{ useNewId("f_") }}',
|
|
111
111
|
description:
|
|
112
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
112
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
113
113
|
'x-decorator': 'FormItem',
|
|
114
114
|
'x-component': 'ForeignKey',
|
|
115
115
|
'x-validator': 'uid',
|
|
@@ -55,7 +55,7 @@ export class OHOFieldInterface extends CollectionFieldInterface {
|
|
|
55
55
|
'x-decorator': 'FormItem',
|
|
56
56
|
'x-component': 'Input',
|
|
57
57
|
description:
|
|
58
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
58
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
59
59
|
},
|
|
60
60
|
type: relationshipType,
|
|
61
61
|
grid: {
|
|
@@ -123,7 +123,7 @@ export class OHOFieldInterface extends CollectionFieldInterface {
|
|
|
123
123
|
required: true,
|
|
124
124
|
default: '{{ useNewId("f_") }}',
|
|
125
125
|
description:
|
|
126
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
126
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
127
127
|
'x-decorator': 'FormItem',
|
|
128
128
|
'x-component': 'ForeignKey',
|
|
129
129
|
'x-validator': 'uid',
|
|
@@ -141,7 +141,7 @@ export const reverseFieldProperties: Record<string, ISchema> = {
|
|
|
141
141
|
'x-validator': 'uid',
|
|
142
142
|
'x-disabled': '{{ !showReverseFieldConfig }}',
|
|
143
143
|
description:
|
|
144
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
144
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
145
145
|
},
|
|
146
146
|
},
|
|
147
147
|
},
|
|
@@ -399,7 +399,7 @@ export const defaultProps = {
|
|
|
399
399
|
'x-component': 'Input',
|
|
400
400
|
'x-validator': 'uid',
|
|
401
401
|
description:
|
|
402
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
402
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
403
403
|
},
|
|
404
404
|
};
|
|
405
405
|
|
|
@@ -46,7 +46,7 @@ export class UUIDFieldInterface extends CollectionFieldInterface {
|
|
|
46
46
|
'x-component': 'Input',
|
|
47
47
|
'x-disabled': '{{ !createOnly }}',
|
|
48
48
|
description:
|
|
49
|
-
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
49
|
+
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
|
|
50
50
|
},
|
|
51
51
|
autoFill,
|
|
52
52
|
layout: {
|
|
@@ -21,7 +21,13 @@ import type { Application } from '../Application';
|
|
|
21
21
|
interface AppErrorPayload {
|
|
22
22
|
code?: string;
|
|
23
23
|
message?: string;
|
|
24
|
-
command?: {
|
|
24
|
+
command?: {
|
|
25
|
+
name: string;
|
|
26
|
+
components?: {
|
|
27
|
+
maintaining?: string;
|
|
28
|
+
maintainingDialog?: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
25
31
|
[key: string]: any;
|
|
26
32
|
}
|
|
27
33
|
|
|
@@ -188,7 +194,12 @@ export const AppError: FC<{ error: Error & { title?: string }; app: Application
|
|
|
188
194
|
);
|
|
189
195
|
|
|
190
196
|
export const AppMaintaining: FC<{ app: Application; error: Error }> = observer(
|
|
191
|
-
({ app }) => {
|
|
197
|
+
({ app, error }) => {
|
|
198
|
+
const component = (error as AppErrorPayload | undefined)?.command?.components?.maintaining;
|
|
199
|
+
if (component) {
|
|
200
|
+
return app.renderComponent(component, { app, error });
|
|
201
|
+
}
|
|
202
|
+
|
|
192
203
|
const { icon, status, title, subTitle } = getProps(app);
|
|
193
204
|
return (
|
|
194
205
|
<div>
|
|
@@ -211,7 +222,12 @@ export const AppMaintaining: FC<{ app: Application; error: Error }> = observer(
|
|
|
211
222
|
);
|
|
212
223
|
|
|
213
224
|
export const AppMaintainingDialog: FC<{ app: Application; error: Error }> = observer(
|
|
214
|
-
({ app }) => {
|
|
225
|
+
({ app, error }) => {
|
|
226
|
+
const component = (error as AppErrorPayload | undefined)?.command?.components?.maintainingDialog;
|
|
227
|
+
if (component) {
|
|
228
|
+
return app.renderComponent(component, { app, error });
|
|
229
|
+
}
|
|
230
|
+
|
|
215
231
|
const { icon, status, title, subTitle } = getProps(app);
|
|
216
232
|
return (
|
|
217
233
|
<Modal open={true} footer={null} closable={false}>
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
import { css, cx } from '@emotion/css';
|
|
11
11
|
import { Input, Typography } from 'antd';
|
|
12
12
|
import type { TextAreaProps } from 'antd/es/input';
|
|
13
|
+
import type { TextAreaRef } from 'antd/es/input/TextArea';
|
|
13
14
|
import JSON5 from 'json5';
|
|
14
|
-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
15
|
+
import React, { useCallback, useEffect, useMemo, useState, type ChangeEvent, type FocusEvent } from 'react';
|
|
15
16
|
|
|
16
17
|
export interface JsonTextAreaProps extends Omit<TextAreaProps, 'value' | 'onChange'> {
|
|
17
18
|
value?: unknown;
|
|
@@ -36,14 +37,14 @@ function stringifyJsonValue(value: unknown, json: typeof JSON | typeof JSON5, sp
|
|
|
36
37
|
json.parse(value);
|
|
37
38
|
return value;
|
|
38
39
|
} catch {
|
|
39
|
-
return json.stringify(value, undefined, space);
|
|
40
|
+
return json.stringify(value, undefined, space) ?? '';
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
return json.stringify(value, undefined, space);
|
|
44
|
+
return json.stringify(value, undefined, space) ?? '';
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
const JsonTextAreaComponent = React.forwardRef<TextAreaRef, JsonTextAreaProps>((props, ref) => {
|
|
47
48
|
const {
|
|
48
49
|
value,
|
|
49
50
|
onChange,
|
|
@@ -74,11 +75,8 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
|
|
|
74
75
|
[json],
|
|
75
76
|
);
|
|
76
77
|
|
|
77
|
-
const
|
|
78
|
-
(
|
|
79
|
-
const nextText = event.target.value;
|
|
80
|
-
setText(nextText);
|
|
81
|
-
|
|
78
|
+
const validateText = useCallback(
|
|
79
|
+
(nextText: string) => {
|
|
82
80
|
try {
|
|
83
81
|
parseText(nextText);
|
|
84
82
|
setError(undefined);
|
|
@@ -89,12 +87,21 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
|
|
|
89
87
|
[parseText],
|
|
90
88
|
);
|
|
91
89
|
|
|
90
|
+
const handleChange = useCallback(
|
|
91
|
+
(event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
92
|
+
const nextText = event.target.value;
|
|
93
|
+
setText(nextText);
|
|
94
|
+
validateText(nextText);
|
|
95
|
+
},
|
|
96
|
+
[validateText],
|
|
97
|
+
);
|
|
98
|
+
|
|
92
99
|
const handleBlur = useCallback(
|
|
93
|
-
(event:
|
|
100
|
+
(event: FocusEvent<HTMLTextAreaElement>) => {
|
|
94
101
|
try {
|
|
95
102
|
const parsed = parseText(event.target.value);
|
|
96
103
|
setError(undefined);
|
|
97
|
-
setText(parsed == null ? '' : json.stringify(parsed, undefined, space));
|
|
104
|
+
setText(parsed == null ? '' : json.stringify(parsed, undefined, space) ?? '');
|
|
98
105
|
onChange?.(parsed);
|
|
99
106
|
} catch (err) {
|
|
100
107
|
setError(err instanceof Error ? err.message : String(err));
|
|
@@ -111,6 +118,7 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
|
|
|
111
118
|
<>
|
|
112
119
|
<Input.TextArea
|
|
113
120
|
{...textAreaProps}
|
|
121
|
+
ref={ref}
|
|
114
122
|
value={text}
|
|
115
123
|
onChange={handleChange}
|
|
116
124
|
onBlur={handleBlur}
|
|
@@ -126,4 +134,6 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
|
|
|
126
134
|
);
|
|
127
135
|
});
|
|
128
136
|
|
|
137
|
+
export const JsonTextArea = React.memo(JsonTextAreaComponent);
|
|
138
|
+
|
|
129
139
|
JsonTextArea.displayName = 'JsonTextArea';
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { render, waitFor } from '@testing-library/react';
|
|
10
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
11
11
|
import React, { useCallback } from 'react';
|
|
12
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
13
|
import { DEFAULT_CODE_FORMATS, getCodeScanBoxSize, useCodeScanner } from '../useCodeScanner';
|
|
14
14
|
|
|
15
15
|
type MockScannerInstance = {
|
|
@@ -44,6 +44,12 @@ const mocks = vi.hoisted(() => {
|
|
|
44
44
|
};
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
const jsQrMocks = vi.hoisted(() => {
|
|
48
|
+
return {
|
|
49
|
+
default: vi.fn(),
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
47
53
|
vi.mock('html5-qrcode', () => ({
|
|
48
54
|
Html5Qrcode: mocks.Html5Qrcode,
|
|
49
55
|
Html5QrcodeScannerState: {
|
|
@@ -66,6 +72,8 @@ vi.mock('html5-qrcode', () => ({
|
|
|
66
72
|
},
|
|
67
73
|
}));
|
|
68
74
|
|
|
75
|
+
vi.mock('jsqr', () => jsQrMocks);
|
|
76
|
+
|
|
69
77
|
function ScannerHost({ scanBoxSize }: { scanBoxSize?: { width: number; height: number } } = {}) {
|
|
70
78
|
const handleScanSuccess = useCallback(() => undefined, []);
|
|
71
79
|
|
|
@@ -79,9 +87,74 @@ function ScannerHost({ scanBoxSize }: { scanBoxSize?: { width: number; height: n
|
|
|
79
87
|
return <div id="scanner" />;
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
function FileScannerHost({ onScanSuccess }: { onScanSuccess: (text: string) => void }) {
|
|
91
|
+
const { startScanFile } = useCodeScanner({
|
|
92
|
+
elementId: 'scanner',
|
|
93
|
+
enabled: true,
|
|
94
|
+
onScanSuccess,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<>
|
|
99
|
+
<div id="scanner" />
|
|
100
|
+
<button onClick={() => startScanFile(new File(['qr'], 'qr.png', { type: 'image/png' }))}>scan file</button>
|
|
101
|
+
</>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function stubSafariBrowser() {
|
|
106
|
+
vi.spyOn(window.navigator, 'vendor', 'get').mockReturnValue('Apple Computer, Inc.');
|
|
107
|
+
vi.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue(
|
|
108
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function stubImageElement(size: { width: number; height: number } = { width: 600, height: 400 }) {
|
|
113
|
+
vi.stubGlobal(
|
|
114
|
+
'Image',
|
|
115
|
+
class MockImage {
|
|
116
|
+
onabort: ((event: Event) => void) | null = null;
|
|
117
|
+
onerror: ((event: Event) => void) | null = null;
|
|
118
|
+
onload: (() => void) | null = null;
|
|
119
|
+
height = size.height;
|
|
120
|
+
naturalHeight = size.height;
|
|
121
|
+
naturalWidth = size.width;
|
|
122
|
+
width = size.width;
|
|
123
|
+
|
|
124
|
+
set src(_value: string) {
|
|
125
|
+
this.onload?.();
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
vi.stubGlobal('URL', {
|
|
130
|
+
...URL,
|
|
131
|
+
createObjectURL: vi.fn(() => 'blob:qr'),
|
|
132
|
+
revokeObjectURL: vi.fn(),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function stubCanvas() {
|
|
137
|
+
const canvasContext = {
|
|
138
|
+
drawImage: vi.fn(),
|
|
139
|
+
getImageData: vi.fn((_x: number, _y: number, width: number, height: number) => ({
|
|
140
|
+
data: new Uint8ClampedArray(width * height * 4),
|
|
141
|
+
height,
|
|
142
|
+
width,
|
|
143
|
+
})),
|
|
144
|
+
} as unknown as CanvasRenderingContext2D;
|
|
145
|
+
|
|
146
|
+
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(canvasContext);
|
|
147
|
+
}
|
|
148
|
+
|
|
82
149
|
describe('useCodeScanner', () => {
|
|
83
150
|
beforeEach(() => {
|
|
84
151
|
vi.clearAllMocks();
|
|
152
|
+
jsQrMocks.default.mockReturnValue(undefined);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
afterEach(() => {
|
|
156
|
+
vi.unstubAllGlobals();
|
|
157
|
+
vi.restoreAllMocks();
|
|
85
158
|
});
|
|
86
159
|
|
|
87
160
|
it('starts scanning with QR code and barcode formats by default', async () => {
|
|
@@ -120,4 +193,74 @@ describe('useCodeScanner', () => {
|
|
|
120
193
|
|
|
121
194
|
expect(config?.qrbox?.(1200, 844)).toEqual({ width: 319, height: 240 });
|
|
122
195
|
});
|
|
196
|
+
|
|
197
|
+
it('uses jsQR first for Safari uploaded QR images', async () => {
|
|
198
|
+
const handleScanSuccess = vi.fn();
|
|
199
|
+
jsQrMocks.default.mockReturnValueOnce({ data: 'JSQR-CODE' });
|
|
200
|
+
stubSafariBrowser();
|
|
201
|
+
stubImageElement();
|
|
202
|
+
stubCanvas();
|
|
203
|
+
|
|
204
|
+
render(<FileScannerHost onScanSuccess={handleScanSuccess} />);
|
|
205
|
+
await waitFor(() => expect(mocks.start).toHaveBeenCalled());
|
|
206
|
+
|
|
207
|
+
fireEvent.click(screen.getByText('scan file'));
|
|
208
|
+
|
|
209
|
+
await waitFor(() => expect(handleScanSuccess).toHaveBeenCalledWith('JSQR-CODE'));
|
|
210
|
+
expect(jsQrMocks.default).toHaveBeenCalledWith(expect.any(Uint8ClampedArray), 600, 400, {
|
|
211
|
+
inversionAttempts: 'attemptBoth',
|
|
212
|
+
});
|
|
213
|
+
expect(mocks.scanFileV2).not.toHaveBeenCalled();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('falls back to html5-qrcode for Safari uploaded files when jsQR does not decode a QR code', async () => {
|
|
217
|
+
const handleScanSuccess = vi.fn();
|
|
218
|
+
mocks.scanFileV2.mockResolvedValueOnce({ decodedText: 'BARCODE-CODE' });
|
|
219
|
+
stubSafariBrowser();
|
|
220
|
+
stubImageElement();
|
|
221
|
+
stubCanvas();
|
|
222
|
+
|
|
223
|
+
render(<FileScannerHost onScanSuccess={handleScanSuccess} />);
|
|
224
|
+
await waitFor(() => expect(mocks.start).toHaveBeenCalled());
|
|
225
|
+
|
|
226
|
+
fireEvent.click(screen.getByText('scan file'));
|
|
227
|
+
|
|
228
|
+
await waitFor(() => expect(handleScanSuccess).toHaveBeenCalledWith('BARCODE-CODE'));
|
|
229
|
+
expect(jsQrMocks.default).toHaveBeenCalled();
|
|
230
|
+
expect(mocks.scanFileV2).toHaveBeenCalled();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('tries multiple image scales before falling back from Safari jsQR scanning', async () => {
|
|
234
|
+
const handleScanSuccess = vi.fn();
|
|
235
|
+
Array.from({ length: 8 }).forEach((_, index) => {
|
|
236
|
+
jsQrMocks.default.mockReturnValueOnce(index === 7 ? { data: 'SCALED-CODE' } : undefined);
|
|
237
|
+
});
|
|
238
|
+
stubSafariBrowser();
|
|
239
|
+
stubImageElement({ width: 4000, height: 3000 });
|
|
240
|
+
stubCanvas();
|
|
241
|
+
|
|
242
|
+
render(<FileScannerHost onScanSuccess={handleScanSuccess} />);
|
|
243
|
+
await waitFor(() => expect(mocks.start).toHaveBeenCalled());
|
|
244
|
+
|
|
245
|
+
fireEvent.click(screen.getByText('scan file'));
|
|
246
|
+
|
|
247
|
+
await waitFor(() => expect(handleScanSuccess).toHaveBeenCalledWith('SCALED-CODE'));
|
|
248
|
+
expect(jsQrMocks.default).toHaveBeenCalledTimes(8);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('tries enhanced grayscale QR images for blurry Safari uploads', async () => {
|
|
252
|
+
const handleScanSuccess = vi.fn();
|
|
253
|
+
jsQrMocks.default.mockReturnValueOnce(undefined).mockReturnValueOnce({ data: 'ENHANCED-CODE' });
|
|
254
|
+
stubSafariBrowser();
|
|
255
|
+
stubImageElement();
|
|
256
|
+
stubCanvas();
|
|
257
|
+
|
|
258
|
+
render(<FileScannerHost onScanSuccess={handleScanSuccess} />);
|
|
259
|
+
await waitFor(() => expect(mocks.start).toHaveBeenCalled());
|
|
260
|
+
|
|
261
|
+
fireEvent.click(screen.getByText('scan file'));
|
|
262
|
+
|
|
263
|
+
await waitFor(() => expect(handleScanSuccess).toHaveBeenCalledWith('ENHANCED-CODE'));
|
|
264
|
+
expect(jsQrMocks.default).toHaveBeenCalledTimes(2);
|
|
265
|
+
});
|
|
123
266
|
});
|