@nocobase/client-v2 2.1.31 → 2.2.0-alpha.10
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/APIClient.d.ts +1 -3
- package/es/BaseApplication.d.ts +2 -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/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
- package/es/components/category-tabs/index.d.ts +9 -0
- package/es/components/form/TypedVariableInput.d.ts +31 -5
- package/es/components/form/filter/CollectionFilter.d.ts +2 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
- package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
- package/es/components/index.d.ts +2 -0
- package/es/entry-actions/EntryActionManager.d.ts +24 -0
- package/es/entry-actions/index.d.ts +9 -0
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +2 -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 +3 -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/FieldAssignExactDatePicker.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
- package/es/flow/components/RunJSValueEditor.d.ts +1 -0
- package/es/flow/components/filter/FilterGroup.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/base/ActionModelCore.d.ts +2 -0
- package/es/flow/models/base/GridModel.d.ts +1 -1
- package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
- package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
- package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +9 -0
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.d.ts +7 -1
- package/es/index.mjs +223 -135
- package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
- package/es/utils/index.d.ts +1 -0
- package/lib/index.js +220 -132
- package/package.json +7 -7
- package/src/APIClient.ts +1 -10
- package/src/Application.tsx +4 -0
- package/src/BaseApplication.tsx +2 -0
- 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 +18 -0
- package/src/__tests__/authRedirect.test.ts +17 -0
- package/src/__tests__/browserChecker.test.ts +61 -0
- package/src/__tests__/exports.test.ts +16 -0
- package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +28 -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/components/README.md +7 -1
- package/src/components/README.zh-CN.md +6 -1
- package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
- package/src/components/category-tabs/index.ts +10 -0
- package/src/components/form/JsonTextArea.tsx +4 -0
- package/src/components/form/TypedVariableInput.tsx +452 -101
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
- package/src/components/form/filter/CollectionFilter.tsx +4 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
- package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
- package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/form/filter/useFilterActionProps.ts +37 -6
- package/src/components/index.ts +2 -0
- package/src/entry-actions/EntryActionManager.ts +76 -0
- package/src/entry-actions/index.ts +10 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +1084 -145
- package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
- package/src/flow/actions/customVariable.tsx +1 -2
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +65 -8
- package/src/flow/actions/runjs.tsx +2 -14
- package/src/flow/admin-shell/BaseLayoutModel.tsx +13 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +46 -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/__tests__/AdminLayoutComponent.test.tsx +435 -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 +77 -0
- 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/components/DefaultValue.tsx +1 -2
- package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +76 -9
- package/src/flow/components/FlowRoute.tsx +185 -28
- package/src/flow/components/RunJSValueEditor.tsx +9 -1
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +84 -2
- 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/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
- package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
- package/src/flow/index.ts +1 -0
- 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 +5 -0
- package/src/flow/models/base/GridModel.tsx +38 -7
- package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
- package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
- package/src/flow/models/base/PageModel/PageTabModel.tsx +186 -3
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -14
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +586 -1
- package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -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 +1 -2
- package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
- package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +87 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +2 -2
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +91 -0
- package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
- package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
- 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/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +168 -10
- package/src/flow-compat/Popover.tsx +43 -4
- package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/index.ts +13 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
- package/src/nocobase-buildin-plugin/index.tsx +19 -3
- package/src/settings-center/AdminSettingsLayout.tsx +10 -2
- package/src/settings-center/SystemSettingsPage.tsx +0 -1
- package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
- package/src/settings-center/utils.tsx +0 -6
- package/src/utils/getRouteRuntimeVersion.ts +185 -0
- package/src/utils/index.tsx +1 -0
|
@@ -180,11 +180,21 @@ const AddFieldColumn = ({ model }: { model: TableBlockModel }) => {
|
|
|
180
180
|
].filter(Boolean)}
|
|
181
181
|
keepDropdownOpen
|
|
182
182
|
>
|
|
183
|
-
|
|
183
|
+
{renderTableSettingsButton(model, 'Fields')}
|
|
184
184
|
</AddSubModelButton>
|
|
185
185
|
);
|
|
186
186
|
};
|
|
187
187
|
|
|
188
|
+
function renderTableSettingsButton(model: TableBlockModel, title: string) {
|
|
189
|
+
const label = model.translate(title);
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<FlowSettingsButton aria-label={label} icon={<SettingOutlined />}>
|
|
193
|
+
{model.context.isMobileLayout ? null : label}
|
|
194
|
+
</FlowSettingsButton>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
188
198
|
type CustomTableBlockModelClassesEnum = {
|
|
189
199
|
CollectionActionGroupModel?: string;
|
|
190
200
|
RecordActionGroupModel?: string;
|
|
@@ -268,7 +278,7 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
|
|
|
268
278
|
cols.push({
|
|
269
279
|
key: 'addColumn',
|
|
270
280
|
fixed: 'right',
|
|
271
|
-
width: 100,
|
|
281
|
+
...(this.context.isMobileLayout ? {} : { width: 100 }),
|
|
272
282
|
title: <AddFieldColumn model={this} />,
|
|
273
283
|
} as any);
|
|
274
284
|
}
|
|
@@ -520,7 +530,7 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
|
|
|
520
530
|
subModelBaseClass={this.getModelClassName('CollectionActionGroupModel')}
|
|
521
531
|
subModelKey="actions"
|
|
522
532
|
>
|
|
523
|
-
|
|
533
|
+
{renderTableSettingsButton(this, 'Actions')}
|
|
524
534
|
</AddSubModelButton>
|
|
525
535
|
);
|
|
526
536
|
}
|
|
@@ -19,11 +19,16 @@ const capturedDroppableUids: string[] = [];
|
|
|
19
19
|
const capturedRendererProps: any[] = [];
|
|
20
20
|
let latestOnDragEnd: ((event: any) => void) | undefined;
|
|
21
21
|
const capturedDndProviders: Array<{ persist: boolean }> = [];
|
|
22
|
+
const capturedAddSubModelButtonProps: any[] = [];
|
|
22
23
|
|
|
23
24
|
vi.mock('@nocobase/flow-engine', async () => {
|
|
24
25
|
const actual = await vi.importActual<any>('@nocobase/flow-engine');
|
|
25
26
|
return {
|
|
26
27
|
...actual,
|
|
28
|
+
AddSubModelButton: (props) => {
|
|
29
|
+
capturedAddSubModelButtonProps.push(props);
|
|
30
|
+
return <button data-testid="add-sub-model-button">{props.children}</button>;
|
|
31
|
+
},
|
|
27
32
|
DndProvider: ({ children, onDragEnd, persist = true, ...restProps }) => {
|
|
28
33
|
const engine = actual.useFlowEngine();
|
|
29
34
|
const handleDragEnd = (event: any) => {
|
|
@@ -50,6 +55,17 @@ vi.mock('@nocobase/flow-engine', async () => {
|
|
|
50
55
|
const ActualFlowModelRenderer = actual.FlowModelRenderer;
|
|
51
56
|
return <ActualFlowModelRenderer {...props} />;
|
|
52
57
|
},
|
|
58
|
+
FlowsFloatContextMenu: ({ children, extraToolbarItems = [], model }) => {
|
|
59
|
+
return (
|
|
60
|
+
<div data-test-flow-context-menu>
|
|
61
|
+
{children}
|
|
62
|
+
{extraToolbarItems.map((item) => {
|
|
63
|
+
const Component = item.component;
|
|
64
|
+
return <Component key={item.key} model={model} />;
|
|
65
|
+
})}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
},
|
|
53
69
|
};
|
|
54
70
|
});
|
|
55
71
|
|
|
@@ -221,6 +237,7 @@ describe('TableActionsColumnModel: drag integration', () => {
|
|
|
221
237
|
capturedDroppableUids.length = 0;
|
|
222
238
|
capturedRendererProps.length = 0;
|
|
223
239
|
capturedDndProviders.length = 0;
|
|
240
|
+
capturedAddSubModelButtonProps.length = 0;
|
|
224
241
|
latestOnDragEnd = undefined;
|
|
225
242
|
});
|
|
226
243
|
|
|
@@ -233,6 +250,7 @@ describe('TableActionsColumnModel: drag integration', () => {
|
|
|
233
250
|
props: { width: 200, title: 'Actions' },
|
|
234
251
|
subModels: { actions: [{ use: 'TestViewActionModel' }, { use: 'TestViewActionModel' }] },
|
|
235
252
|
});
|
|
253
|
+
actionsCol.context.defineMethod('getModelClassName', (className: string) => className);
|
|
236
254
|
|
|
237
255
|
const colProps = actionsCol.getColumnProps();
|
|
238
256
|
const record = { id: 1, phone: '000000' } as any;
|
|
@@ -335,4 +353,40 @@ describe('TableActionsColumnModel: drag integration', () => {
|
|
|
335
353
|
|
|
336
354
|
expect(moveModelSpy).toHaveBeenCalledWith(firstUid, secondUid, { persist: true });
|
|
337
355
|
});
|
|
356
|
+
|
|
357
|
+
it('disables icon-only mode when adding row actions without icons', async () => {
|
|
358
|
+
const engine = new FlowEngine();
|
|
359
|
+
engine.registerModels({ TableActionsColumnModel, TestViewActionModel });
|
|
360
|
+
|
|
361
|
+
const actionsCol = engine.createModel<TableActionsColumnModel>({
|
|
362
|
+
use: 'TableActionsColumnModel',
|
|
363
|
+
props: { width: 200, title: 'Actions' },
|
|
364
|
+
});
|
|
365
|
+
actionsCol.context.defineMethod('getModelClassName', (className: string) => className);
|
|
366
|
+
|
|
367
|
+
render(
|
|
368
|
+
<FlowEngineProvider engine={engine}>
|
|
369
|
+
<ConfigProvider>
|
|
370
|
+
<App>{actionsCol.getColumnProps().title as React.ReactNode}</App>
|
|
371
|
+
</ConfigProvider>
|
|
372
|
+
</FlowEngineProvider>,
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const addButtonProps = capturedAddSubModelButtonProps.find(
|
|
376
|
+
(props) => props.subModelKey === 'actions' && props.subModelBaseClass === 'RecordActionGroupModel',
|
|
377
|
+
);
|
|
378
|
+
expect(addButtonProps).toBeDefined();
|
|
379
|
+
expect(addButtonProps.afterSubModelInit).toBeTypeOf('function');
|
|
380
|
+
|
|
381
|
+
const actionModel = {
|
|
382
|
+
setStepParams: vi.fn(),
|
|
383
|
+
};
|
|
384
|
+
await addButtonProps.afterSubModelInit(actionModel);
|
|
385
|
+
|
|
386
|
+
expect(actionModel.setStepParams).toHaveBeenCalledWith('buttonSettings', 'general', {
|
|
387
|
+
type: 'link',
|
|
388
|
+
icon: null,
|
|
389
|
+
iconOnly: false,
|
|
390
|
+
});
|
|
391
|
+
});
|
|
338
392
|
});
|
|
@@ -0,0 +1,78 @@
|
|
|
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 { FlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import '@nocobase/client';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import { TableBlockModel } from '../TableBlockModel';
|
|
15
|
+
|
|
16
|
+
function createTableModel(options: { isMobileLayout?: boolean; flowSettingsEnabled?: boolean } = {}) {
|
|
17
|
+
const engine = new FlowEngine();
|
|
18
|
+
engine.registerModels({ TableBlockModel });
|
|
19
|
+
engine.context.defineProperty('isMobileLayout', { value: !!options.isMobileLayout });
|
|
20
|
+
engine.context.defineProperty('flowSettingsEnabled', { value: !!options.flowSettingsEnabled });
|
|
21
|
+
|
|
22
|
+
const ds = engine.dataSourceManager.getDataSource('main');
|
|
23
|
+
ds.addCollection({
|
|
24
|
+
name: 'posts',
|
|
25
|
+
filterTargetKey: 'id',
|
|
26
|
+
fields: [
|
|
27
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
28
|
+
{ name: 'title', type: 'string', interface: 'input' },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return engine.createModel<TableBlockModel>({
|
|
33
|
+
uid: `posts-table-${options.isMobileLayout ? 'mobile' : 'desktop'}`,
|
|
34
|
+
use: 'TableBlockModel',
|
|
35
|
+
stepParams: {
|
|
36
|
+
resourceSettings: {
|
|
37
|
+
init: {
|
|
38
|
+
dataSourceKey: 'main',
|
|
39
|
+
collectionName: 'posts',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getSettingsButton(element: React.ReactElement) {
|
|
47
|
+
return element.props.children as React.ReactElement;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('TableBlockModel mobile settings buttons', () => {
|
|
51
|
+
it('renders table actions and fields settings as icon-only buttons in mobile layouts', () => {
|
|
52
|
+
const model = createTableModel({ isMobileLayout: true, flowSettingsEnabled: true });
|
|
53
|
+
const actionButton = getSettingsButton(model.renderConfigureActions() as React.ReactElement);
|
|
54
|
+
const addColumn = model.getColumns().find((column) => column.key === 'addColumn');
|
|
55
|
+
const fieldsButton = getSettingsButton(
|
|
56
|
+
(addColumn?.title as React.ReactElement).type({ model }) as React.ReactElement,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(actionButton.props.children).toBeNull();
|
|
60
|
+
expect(actionButton.props['aria-label']).toBe('Actions');
|
|
61
|
+
expect(fieldsButton.props.children).toBeNull();
|
|
62
|
+
expect(fieldsButton.props['aria-label']).toBe('Fields');
|
|
63
|
+
expect(addColumn?.width).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('keeps table actions and fields settings labels outside mobile layouts', () => {
|
|
67
|
+
const model = createTableModel({ flowSettingsEnabled: true });
|
|
68
|
+
const actionButton = getSettingsButton(model.renderConfigureActions() as React.ReactElement);
|
|
69
|
+
const addColumn = model.getColumns().find((column) => column.key === 'addColumn');
|
|
70
|
+
const fieldsButton = getSettingsButton(
|
|
71
|
+
(addColumn?.title as React.ReactElement).type({ model }) as React.ReactElement,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(actionButton.props.children).toBe('Actions');
|
|
75
|
+
expect(fieldsButton.props.children).toBe('Fields');
|
|
76
|
+
expect(addColumn?.width).toBe(100);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { ElementProxy, tExpr
|
|
10
|
+
import { ElementProxy, tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import React, { useEffect, useMemo, useRef } from 'react';
|
|
12
12
|
import { Input } from 'antd';
|
|
13
13
|
import { FieldModel } from '../base/FieldModel';
|
|
@@ -398,16 +398,7 @@ JSEditableFieldModel.registerFlow({
|
|
|
398
398
|
get: () => isReadOnlyMode(ctx.model),
|
|
399
399
|
cache: false,
|
|
400
400
|
});
|
|
401
|
-
|
|
402
|
-
await ctx.runjs(
|
|
403
|
-
code,
|
|
404
|
-
{
|
|
405
|
-
window: createSafeWindow({ navigator }),
|
|
406
|
-
document: createSafeDocument(),
|
|
407
|
-
navigator,
|
|
408
|
-
},
|
|
409
|
-
{ version },
|
|
410
|
-
);
|
|
401
|
+
await ctx.runjs(code, undefined, { version });
|
|
411
402
|
});
|
|
412
403
|
},
|
|
413
404
|
},
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { ElementProxy, tExpr
|
|
10
|
+
import { ElementProxy, tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import React, { useEffect } from 'react';
|
|
12
12
|
import { FieldModel } from '../base/FieldModel';
|
|
13
13
|
import { resolveRunJsParams } from '../utils/resolveRunJsParams';
|
|
@@ -211,12 +211,7 @@ JSFieldModel.registerFlow({
|
|
|
211
211
|
get: () => ctx.model.props?.value,
|
|
212
212
|
cache: false,
|
|
213
213
|
});
|
|
214
|
-
|
|
215
|
-
await ctx.runjs(
|
|
216
|
-
code,
|
|
217
|
-
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
218
|
-
{ version },
|
|
219
|
-
);
|
|
214
|
+
await ctx.runjs(code, undefined, { version });
|
|
220
215
|
});
|
|
221
216
|
},
|
|
222
217
|
},
|
|
@@ -7,14 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
ElementProxy,
|
|
12
|
-
FormItem,
|
|
13
|
-
createSafeDocument,
|
|
14
|
-
createSafeWindow,
|
|
15
|
-
createSafeNavigator,
|
|
16
|
-
tExpr,
|
|
17
|
-
} from '@nocobase/flow-engine';
|
|
10
|
+
import { ElementProxy, FormItem, tExpr } from '@nocobase/flow-engine';
|
|
18
11
|
import React from 'react';
|
|
19
12
|
import { CodeEditor } from '../../components/code-editor';
|
|
20
13
|
import { CommonItemModel } from '../base/CommonItemModel';
|
|
@@ -150,12 +143,7 @@ ctx.render(<JsItem />);
|
|
|
150
143
|
ctx.defineProperty('element', {
|
|
151
144
|
get: () => new ElementProxy(element),
|
|
152
145
|
});
|
|
153
|
-
|
|
154
|
-
await ctx.runjs(
|
|
155
|
-
code,
|
|
156
|
-
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
157
|
-
{ version },
|
|
158
|
-
);
|
|
146
|
+
await ctx.runjs(code, undefined, { version });
|
|
159
147
|
});
|
|
160
148
|
},
|
|
161
149
|
},
|
|
@@ -48,12 +48,12 @@ export class VariableFieldFormModel extends FlowModel {
|
|
|
48
48
|
<FormProvider form={this.form}>
|
|
49
49
|
<FormLayout layout={'vertical'}>
|
|
50
50
|
{this.mapSubModels('fields', (field) => {
|
|
51
|
-
//
|
|
51
|
+
// Avoid conflicting with the DOM nodeName property when rendering the temporary input.
|
|
52
52
|
const init = field?.getStepParams?.('fieldSettings', 'init') || {};
|
|
53
53
|
const fp = init?.fieldPath as string | undefined;
|
|
54
54
|
if (fp) {
|
|
55
|
-
const namePath = fp
|
|
56
|
-
const toSet:
|
|
55
|
+
const namePath = (fp === 'nodeName' ? '__nb_variable_field_nodeName' : fp).split('.');
|
|
56
|
+
const toSet: Record<string, unknown> = {};
|
|
57
57
|
if (!field?.props?.id) toSet.id = namePath;
|
|
58
58
|
if (!field?.props?.name) toSet.name = namePath;
|
|
59
59
|
if (Object.keys(toSet).length) field?.setProps?.(toSet);
|
|
@@ -0,0 +1,51 @@
|
|
|
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 { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
|
11
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import { InputFieldModel } from '../InputFieldModel';
|
|
15
|
+
import { VariableFieldFormModel } from '../VariableFieldFormModel';
|
|
16
|
+
|
|
17
|
+
describe('VariableFieldFormModel', () => {
|
|
18
|
+
it('uses DOM-safe name and id for temporary controls created from reserved form property names', async () => {
|
|
19
|
+
const engine = new FlowEngine();
|
|
20
|
+
engine.registerModels({ InputFieldModel, VariableFieldFormModel });
|
|
21
|
+
const model = engine.createModel<VariableFieldFormModel>({
|
|
22
|
+
use: 'VariableFieldFormModel',
|
|
23
|
+
subModels: {
|
|
24
|
+
fields: [
|
|
25
|
+
{
|
|
26
|
+
use: 'InputFieldModel',
|
|
27
|
+
stepParams: {
|
|
28
|
+
fieldSettings: {
|
|
29
|
+
init: {
|
|
30
|
+
fieldPath: 'nodeName',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const field = model.subModels.fields[0];
|
|
39
|
+
|
|
40
|
+
render(<FlowEngineProvider engine={engine}>{model.render()}</FlowEngineProvider>);
|
|
41
|
+
|
|
42
|
+
const input = await screen.findByRole('textbox');
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(field.props.name).toEqual(['__nb_variable_field_nodeName']);
|
|
45
|
+
});
|
|
46
|
+
expect(input).toHaveAttribute('name', '__nb_variable_field_nodeName');
|
|
47
|
+
expect(input).toHaveAttribute('id', '__nb_variable_field_nodeName');
|
|
48
|
+
expect(field.props.id).toEqual(['__nb_variable_field_nodeName']);
|
|
49
|
+
expect(field.getStepParams('fieldSettings', 'init')).toEqual({ fieldPath: 'nodeName' });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -14,9 +14,11 @@ import { Button, Dropdown, theme, Tooltip, type ButtonProps, type MenuProps } fr
|
|
|
14
14
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
15
15
|
import { useHotkeys } from 'react-hotkeys-hook';
|
|
16
16
|
import { useTranslation } from 'react-i18next';
|
|
17
|
-
import { Link } from 'react-router-dom';
|
|
17
|
+
import { Link, useLocation } from 'react-router-dom';
|
|
18
18
|
import { useACLRoleContext } from '../../../acl';
|
|
19
|
+
import type { BaseApplication } from '../../../BaseApplication';
|
|
19
20
|
import type { PluginSettingsPageType } from '../../../PluginSettingsManager';
|
|
21
|
+
import { shouldOpenAdminRouteInNewWindow } from '../../../RouterManager';
|
|
20
22
|
import { useApp } from '../../../hooks/useApp';
|
|
21
23
|
import {
|
|
22
24
|
filterRenderableSettings,
|
|
@@ -64,6 +66,163 @@ const topbarActionTriggerClassName = css`
|
|
|
64
66
|
height: 100%;
|
|
65
67
|
`;
|
|
66
68
|
|
|
69
|
+
type TopbarSettingsAppLike = Pick<
|
|
70
|
+
BaseApplication<any>,
|
|
71
|
+
'name' | 'router' | 'getPublicPath' | 'getHref' | 'layoutManager'
|
|
72
|
+
>;
|
|
73
|
+
|
|
74
|
+
const normalizeTopbarPath = (pathname?: string) => {
|
|
75
|
+
const trimmed = pathname?.trim();
|
|
76
|
+
if (!trimmed || trimmed === '/') {
|
|
77
|
+
return '/';
|
|
78
|
+
}
|
|
79
|
+
return `/${trimmed.replace(/^\/+/, '')}`;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const normalizeTopbarBasePath = (pathname?: string) => {
|
|
83
|
+
const normalized = normalizeTopbarPath(pathname).replace(/\/+$/, '');
|
|
84
|
+
return normalized === '' || normalized === '/' ? '' : normalized;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getTopbarRouterBasePath = (app: TopbarSettingsAppLike | undefined) => {
|
|
88
|
+
return app?.router?.getBasename?.() || app?.router?.basename || app?.getPublicPath?.() || '';
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const stripTopbarRouterBasePath = (pathname: string, basename?: string) => {
|
|
92
|
+
const normalizedPath = normalizeTopbarPath(pathname);
|
|
93
|
+
const normalizedBase = normalizeTopbarBasePath(basename);
|
|
94
|
+
|
|
95
|
+
if (!normalizedBase) {
|
|
96
|
+
return normalizedPath;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (normalizedPath === normalizedBase) {
|
|
100
|
+
return '/';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (normalizedPath.startsWith(`${normalizedBase}/`)) {
|
|
104
|
+
return normalizeTopbarPath(normalizedPath.slice(normalizedBase.length));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return normalizedPath;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const getTopbarAppPath = (pathname: string) => {
|
|
111
|
+
const normalizedPath = normalizeTopbarPath(pathname);
|
|
112
|
+
const match = /^(.*?\/(?:apps|_app)\/[^/]+)(?=\/|$)/.exec(normalizedPath);
|
|
113
|
+
const appPath = match?.[1] || '';
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
appPath,
|
|
117
|
+
routePath: appPath ? normalizeTopbarPath(normalizedPath.slice(appPath.length)) : normalizedPath,
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const getTopbarContextAppPath = (app: TopbarSettingsAppLike | undefined, basename?: string) => {
|
|
122
|
+
const basenameAppPath = getTopbarAppPath(basename || '').appPath;
|
|
123
|
+
if (basenameAppPath) {
|
|
124
|
+
return basenameAppPath;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const publicPathAppPath = getTopbarAppPath(app?.getPublicPath?.() || '').appPath;
|
|
128
|
+
if (publicPathAppPath) {
|
|
129
|
+
return publicPathAppPath;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (app?.name && app.name !== 'main' && app.getHref) {
|
|
133
|
+
return normalizeTopbarBasePath(app.getHref('/'));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return '';
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const prependTopbarAppPath = (pathname: string, appPath: string) => {
|
|
140
|
+
const normalizedPath = normalizeTopbarPath(pathname);
|
|
141
|
+
|
|
142
|
+
if (!appPath || normalizedPath === appPath || normalizedPath.startsWith(`${appPath}/`)) {
|
|
143
|
+
return normalizedPath;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return `${appPath}${normalizedPath}`;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const isAdminRuntimePath = (pathname: string) => {
|
|
150
|
+
return pathname === '/admin' || pathname.startsWith('/admin/');
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const getTopbarAdminRoutePath = (app: TopbarSettingsAppLike | undefined) => {
|
|
154
|
+
try {
|
|
155
|
+
return app?.layoutManager?.getLayout?.('admin')?.routePath;
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const buildTopbarDocumentHref = (targetPath: string, basename?: string) => {
|
|
162
|
+
const normalizedTarget = normalizeTopbarPath(targetPath);
|
|
163
|
+
const normalizedBase = normalizeTopbarBasePath(basename);
|
|
164
|
+
|
|
165
|
+
if (!normalizedBase || normalizedTarget === normalizedBase || normalizedTarget.startsWith(`${normalizedBase}/`)) {
|
|
166
|
+
return normalizedTarget;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return `${normalizedBase}${normalizedTarget}`;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
function TopbarExternalSettingsLabel(props: { title: React.ReactNode; link: string }) {
|
|
173
|
+
return (
|
|
174
|
+
<div
|
|
175
|
+
onClick={() => {
|
|
176
|
+
window.open(props.link, '_blank', 'noopener,noreferrer');
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
179
|
+
{props.title}
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function TopbarInternalSettingsLabel(props: { title: React.ReactNode; path?: string }) {
|
|
185
|
+
const app = useApp();
|
|
186
|
+
const location = useLocation();
|
|
187
|
+
const targetPath = props.path || '/admin/settings';
|
|
188
|
+
const basename = getTopbarRouterBasePath(app);
|
|
189
|
+
const currentPath = stripTopbarRouterBasePath(location.pathname, basename);
|
|
190
|
+
const currentLocationAppPath = getTopbarAppPath(currentPath);
|
|
191
|
+
const currentAppPath = currentLocationAppPath.appPath || getTopbarContextAppPath(app, basename);
|
|
192
|
+
const currentRoutePath = currentLocationAppPath.appPath ? currentLocationAppPath.routePath : currentPath;
|
|
193
|
+
const targetPathInCurrentApp = prependTopbarAppPath(stripTopbarRouterBasePath(targetPath, basename), currentAppPath);
|
|
194
|
+
|
|
195
|
+
if (currentAppPath) {
|
|
196
|
+
const href = buildTopbarDocumentHref(targetPathInCurrentApp, basename);
|
|
197
|
+
const shouldOpenInNewWindow = shouldOpenAdminRouteInNewWindow({
|
|
198
|
+
currentPathname: currentPath,
|
|
199
|
+
targetPathname: targetPathInCurrentApp,
|
|
200
|
+
basePath: basename,
|
|
201
|
+
adminRoutePath: getTopbarAdminRoutePath(app),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<a
|
|
206
|
+
href={href}
|
|
207
|
+
target={shouldOpenInNewWindow ? '_blank' : undefined}
|
|
208
|
+
rel={shouldOpenInNewWindow ? 'noopener noreferrer' : undefined}
|
|
209
|
+
>
|
|
210
|
+
{props.title}
|
|
211
|
+
</a>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (isAdminRuntimePath(currentRoutePath)) {
|
|
216
|
+
return <Link to={targetPathInCurrentApp}>{props.title}</Link>;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<a href={buildTopbarDocumentHref(targetPath, basename)} target="_blank" rel="noopener noreferrer">
|
|
221
|
+
{props.title}
|
|
222
|
+
</a>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
67
226
|
/**
|
|
68
227
|
* 将 settings 顶部菜单转换成右上角 dropdown items。
|
|
69
228
|
*
|
|
@@ -112,15 +271,9 @@ export function getTopbarPluginSettingsItems(options: {
|
|
|
112
271
|
title: targetTitle,
|
|
113
272
|
icon: item.icon,
|
|
114
273
|
label: targetLink ? (
|
|
115
|
-
<
|
|
116
|
-
onClick={() => {
|
|
117
|
-
window.open(targetLink, '_blank', 'noopener,noreferrer');
|
|
118
|
-
}}
|
|
119
|
-
>
|
|
120
|
-
{targetTitle}
|
|
121
|
-
</div>
|
|
274
|
+
<TopbarExternalSettingsLabel title={targetTitle} link={targetLink} />
|
|
122
275
|
) : (
|
|
123
|
-
<
|
|
276
|
+
<TopbarInternalSettingsLabel title={targetTitle} path={targetPath} />
|
|
124
277
|
),
|
|
125
278
|
};
|
|
126
279
|
});
|
|
@@ -131,7 +284,12 @@ export function getTopbarPluginSettingsItems(options: {
|
|
|
131
284
|
items.push({
|
|
132
285
|
key: pluginManagerSetting.key,
|
|
133
286
|
icon: pluginManagerSetting.icon || <ApiOutlined />,
|
|
134
|
-
label:
|
|
287
|
+
label: (
|
|
288
|
+
<TopbarInternalSettingsLabel
|
|
289
|
+
title={pluginManagerSetting.title || t('Plugin manager')}
|
|
290
|
+
path={pluginManagerSetting.path}
|
|
291
|
+
/>
|
|
292
|
+
),
|
|
135
293
|
});
|
|
136
294
|
}
|
|
137
295
|
|
|
@@ -8,15 +8,54 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Popover as AntdPopover, type PopoverProps } from 'antd';
|
|
11
|
-
import React, { useCallback, useRef } from 'react';
|
|
11
|
+
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
|
12
12
|
import { getZIndex, useZIndexContext, zIndexContext } from './zIndexContext';
|
|
13
13
|
|
|
14
14
|
export const ICON_POPUP_Z_INDEX = 2000;
|
|
15
15
|
|
|
16
|
+
function parseZIndex(value?: string | null) {
|
|
17
|
+
if (!value || value === 'auto') {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const zIndex = Number(value);
|
|
22
|
+
return Number.isFinite(zIndex) ? zIndex : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getElementZIndex(element: HTMLElement) {
|
|
26
|
+
return parseZIndex(window.getComputedStyle(element).zIndex) ?? parseZIndex(element.style.zIndex);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getContainingPopupZIndex(element: HTMLElement | null) {
|
|
30
|
+
if (!element || typeof window === 'undefined') {
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let current: HTMLElement | null = element;
|
|
35
|
+
let maxZIndex = 0;
|
|
36
|
+
|
|
37
|
+
while (current && current !== document.documentElement) {
|
|
38
|
+
const zIndex = getElementZIndex(current);
|
|
39
|
+
if (typeof zIndex === 'number' && zIndex > maxZIndex) {
|
|
40
|
+
maxZIndex = zIndex;
|
|
41
|
+
}
|
|
42
|
+
current = current.parentElement;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return maxZIndex;
|
|
46
|
+
}
|
|
47
|
+
|
|
16
48
|
export const StablePopover = (props: PopoverProps) => {
|
|
17
49
|
const parentZIndex = useZIndexContext();
|
|
18
|
-
const
|
|
19
|
-
const target = useRef(null);
|
|
50
|
+
const [containingPopupZIndex, setContainingPopupZIndex] = useState(parentZIndex);
|
|
51
|
+
const target = useRef<EventTarget | null>(null);
|
|
52
|
+
const triggerRef = useRef<HTMLDivElement | null>(null);
|
|
53
|
+
const zIndex = getZIndex('drawer', Math.max(parentZIndex, containingPopupZIndex), 1);
|
|
54
|
+
|
|
55
|
+
useLayoutEffect(() => {
|
|
56
|
+
const nextZIndex = Math.max(parentZIndex, getContainingPopupZIndex(triggerRef.current));
|
|
57
|
+
setContainingPopupZIndex((previousZIndex) => (previousZIndex === nextZIndex ? previousZIndex : nextZIndex));
|
|
58
|
+
}, [parentZIndex]);
|
|
20
59
|
|
|
21
60
|
const avoidClose = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
22
61
|
target.current = e.target;
|
|
@@ -36,7 +75,7 @@ export const StablePopover = (props: PopoverProps) => {
|
|
|
36
75
|
);
|
|
37
76
|
|
|
38
77
|
return (
|
|
39
|
-
<div className="popover-with-stop-propagation" onClick={avoidClose}>
|
|
78
|
+
<div ref={triggerRef} className="popover-with-stop-propagation" onClick={avoidClose}>
|
|
40
79
|
<zIndexContext.Provider value={zIndex}>
|
|
41
80
|
<AntdPopover {...props} zIndex={zIndex} onOpenChange={onOpenChange} />
|
|
42
81
|
</zIndexContext.Provider>
|
|
@@ -0,0 +1,34 @@
|
|
|
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 { describe, expect, it } from 'vitest';
|
|
13
|
+
import { StablePopover } from '../Popover';
|
|
14
|
+
|
|
15
|
+
describe('StablePopover', () => {
|
|
16
|
+
it('renders above the containing popup layer', async () => {
|
|
17
|
+
render(
|
|
18
|
+
<div style={{ position: 'relative', zIndex: 5000 }}>
|
|
19
|
+
<StablePopover open content={<div>Icon picker popup</div>}>
|
|
20
|
+
<button type="button">Select icon</button>
|
|
21
|
+
</StablePopover>
|
|
22
|
+
</div>,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(screen.getByText('Icon picker popup')).toBeInTheDocument();
|
|
26
|
+
|
|
27
|
+
await waitFor(() => {
|
|
28
|
+
const popover = document.querySelector<HTMLElement>('.ant-popover');
|
|
29
|
+
|
|
30
|
+
expect(popover).toBeInTheDocument();
|
|
31
|
+
expect(Number(popover?.style.zIndex)).toBeGreaterThan(5000);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|