@nocobase/client-v2 2.3.0-beta.9 → 2.4.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/RouteRepository.d.ts +6 -0
- package/es/acl/ACLProvider.d.ts +10 -0
- package/es/acl/aclCheckReadiness.d.ts +11 -0
- package/es/authRedirect.d.ts +11 -2
- package/es/components/AttachmentUpload.d.ts +51 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +10 -1
- package/es/flow/internal/utils/quickEditDataScope.d.ts +48 -0
- package/es/flow/internal/utils/quickEditViewContainer.d.ts +27 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +1 -0
- package/es/index.d.ts +4 -1
- package/es/index.mjs +129 -118
- package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
- package/es/nocobase-buildin-plugin/index.d.ts +10 -0
- package/es/settings-app/SettingsApplication.d.ts +18 -0
- package/es/settings-app/SettingsBrand.d.ts +16 -0
- package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
- package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
- package/es/settings-app/SettingsGroupNav.d.ts +18 -0
- package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
- package/es/settings-app/SettingsRouterManager.d.ts +15 -0
- package/es/settings-app/SettingsSearch.d.ts +17 -0
- package/es/settings-app/SettingsShell.d.ts +10 -0
- package/es/settings-app/isSettingsApp.d.ts +18 -0
- package/es/settings-app/runtimePaths.d.ts +18 -0
- package/es/settings-app/settingsDocumentPath.d.ts +14 -0
- package/es/settings-app/settingsTheme.d.ts +87 -0
- package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
- package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
- package/es/settings-center/groups.d.ts +66 -0
- package/es/settings-center/useSettingsGroups.d.ts +36 -0
- package/es/settings-center/useSettingsSearch.d.ts +35 -0
- package/es/settings-center/utils.d.ts +24 -0
- package/es/theme/index.d.ts +1 -0
- package/es/theme/itemActive.d.ts +24 -0
- package/es/theme/type.d.ts +9 -0
- package/lib/index.js +133 -122
- package/package.json +7 -7
- package/src/BaseApplication.tsx +2 -0
- package/src/RouteRepository.ts +14 -0
- package/src/__tests__/RouteRepository.test.ts +51 -0
- package/src/__tests__/app.test.tsx +135 -3
- package/src/__tests__/authRedirect.test.ts +154 -0
- package/src/__tests__/browserChecker.test.ts +172 -6
- package/src/__tests__/mockSettingsApplication.ts +29 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +81 -8
- package/src/__tests__/plugin-manager.test.tsx +7 -6
- package/src/__tests__/settings-application.test.ts +164 -0
- package/src/__tests__/settings-auth-routes.test.ts +71 -0
- package/src/__tests__/settings-center.test.tsx +168 -130
- package/src/__tests__/settings-layout-root.test.tsx +600 -0
- package/src/__tests__/settings-runtime-paths.test.ts +99 -0
- package/src/__tests__/settings-search-shortcut.test.tsx +321 -0
- package/src/__tests__/settings-shell.test.tsx +261 -0
- package/src/acl/ACLProvider.tsx +53 -3
- package/src/acl/aclCheckReadiness.ts +44 -0
- package/src/authRedirect.ts +85 -8
- package/src/components/AppComponents.tsx +16 -7
- package/src/components/AttachmentUpload.tsx +275 -0
- package/src/components/index.ts +1 -0
- package/src/flow/actions/__tests__/validation.test.ts +48 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -56
- package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
- package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +104 -20
- package/src/flow/components/filter/VariableFilterItem.tsx +14 -4
- package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
- package/src/flow/internal/utils/__tests__/quickEditDataScope.test.ts +156 -0
- package/src/flow/internal/utils/__tests__/quickEditViewContainer.test.ts +52 -0
- package/src/flow/internal/utils/quickEditDataScope.tsx +177 -0
- package/src/flow/internal/utils/quickEditViewContainer.ts +46 -0
- package/src/flow/models/actions/JSItemActionModel.tsx +19 -3
- package/src/flow/models/actions/__tests__/JSItemActionModel.test.tsx +47 -1
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +45 -11
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +120 -0
- package/src/flow/models/blocks/js-block/JSBlock.tsx +5 -1
- package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +23 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +31 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +64 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +7 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +17 -10
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +6 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.disabled.test.tsx +90 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordSelectFieldModel.quickCreatePopup.test.tsx +183 -0
- package/src/flow/models/fields/SelectFieldModel.tsx +5 -0
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +29 -1
- package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
- package/src/index.ts +11 -1
- package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
- package/src/nocobase-buildin-plugin/index.tsx +112 -71
- package/src/settings-app/SettingsApplication.ts +31 -0
- package/src/settings-app/SettingsBrand.tsx +108 -0
- package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
- package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
- package/src/settings-app/SettingsGroupNav.tsx +124 -0
- package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
- package/src/settings-app/SettingsRouterManager.ts +92 -0
- package/src/settings-app/SettingsSearch.tsx +343 -0
- package/src/settings-app/SettingsShell.tsx +184 -0
- package/src/settings-app/isSettingsApp.ts +21 -0
- package/src/settings-app/runtimePaths.ts +103 -0
- package/src/settings-app/settingsDocumentPath.ts +66 -0
- package/src/settings-app/settingsTheme.ts +369 -0
- package/src/settings-app/useSettingsThemeConfig.ts +100 -0
- package/src/settings-center/AdminSettingsLayout.tsx +154 -169
- package/src/settings-center/SystemSettingsPage.tsx +4 -170
- package/src/settings-center/__tests__/groups.test.ts +25 -0
- package/src/settings-center/groups.ts +108 -0
- package/src/settings-center/useSettingsGroups.ts +105 -0
- package/src/settings-center/useSettingsSearch.ts +173 -0
- package/src/settings-center/utils.tsx +50 -1
- package/src/theme/index.tsx +1 -0
- package/src/theme/itemActive.ts +31 -0
- package/src/theme/type.ts +10 -0
|
@@ -8,13 +8,59 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import { render, screen, waitFor } from '@nocobase/test/client';
|
|
11
|
+
import { act, render, screen, waitFor } from '@nocobase/test/client';
|
|
12
12
|
import { App, ConfigProvider } from 'antd';
|
|
13
13
|
import { describe, expect, it } from 'vitest';
|
|
14
14
|
import { FlowEngine, FlowEngineProvider, FlowModelRenderer } from '@nocobase/flow-engine';
|
|
15
15
|
import { JSItemActionModel } from '../JSItemActionModel';
|
|
16
16
|
|
|
17
17
|
describe('JSItemActionModel', () => {
|
|
18
|
+
it('keeps empty items reachable in configuration mode without replacing rendered content', async () => {
|
|
19
|
+
const engine = new FlowEngine();
|
|
20
|
+
engine.registerModels({ JSItemActionModel });
|
|
21
|
+
const model = engine.createModel<JSItemActionModel>({ use: 'JSItemActionModel' });
|
|
22
|
+
await engine.flowSettings.disable();
|
|
23
|
+
|
|
24
|
+
render(model.render());
|
|
25
|
+
const host = model.context.ref.current;
|
|
26
|
+
expect(host).toBeInstanceOf(HTMLDivElement);
|
|
27
|
+
expect(host.style.minWidth).toBe('');
|
|
28
|
+
expect(host.textContent).toBe('');
|
|
29
|
+
|
|
30
|
+
await act(async () => {
|
|
31
|
+
await engine.flowSettings.enable();
|
|
32
|
+
});
|
|
33
|
+
await waitFor(() => expect(host.style.minWidth).toBe('40px'));
|
|
34
|
+
|
|
35
|
+
const content = document.createElement('button');
|
|
36
|
+
content.textContent = 'Custom action';
|
|
37
|
+
host.appendChild(content);
|
|
38
|
+
|
|
39
|
+
await act(async () => {
|
|
40
|
+
await engine.flowSettings.disable();
|
|
41
|
+
});
|
|
42
|
+
await waitFor(() => expect(host.style.minWidth).toBe(''));
|
|
43
|
+
expect(model.context.ref.current).toBe(host);
|
|
44
|
+
expect(host.firstElementChild).toBe(content);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('keeps hidden empty items configurable and removes them outside configuration mode', async () => {
|
|
48
|
+
const engine = new FlowEngine();
|
|
49
|
+
engine.registerModels({ JSItemActionModel });
|
|
50
|
+
const model = engine.createModel<JSItemActionModel>({ use: 'JSItemActionModel' });
|
|
51
|
+
model.setHidden(true);
|
|
52
|
+
await engine.flowSettings.enable();
|
|
53
|
+
|
|
54
|
+
const { container } = render(model.render());
|
|
55
|
+
expect(model.context.ref.current.style.minWidth).toBe('40px');
|
|
56
|
+
expect(model.context.ref.current.style.opacity).toBe('0.3');
|
|
57
|
+
|
|
58
|
+
await act(async () => {
|
|
59
|
+
await engine.flowSettings.disable();
|
|
60
|
+
});
|
|
61
|
+
await waitFor(() => expect(container).toBeEmptyDOMElement());
|
|
62
|
+
});
|
|
63
|
+
|
|
18
64
|
it('renders collection-level custom content', async () => {
|
|
19
65
|
const engine = new FlowEngine();
|
|
20
66
|
engine.registerModels({ JSItemActionModel });
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from '@nocobase/flow-engine';
|
|
24
24
|
import { Button, Form, Skeleton, Space } from 'antd';
|
|
25
25
|
import React from 'react';
|
|
26
|
+
import { findDataScopeStep, getQuickEditDataScopeFilter } from '../../../internal/utils/quickEditDataScope';
|
|
26
27
|
import { FieldModel } from '../../base/FieldModel';
|
|
27
28
|
import { FormComponent } from './FormBlockModel';
|
|
28
29
|
import { FormItemModel } from './FormItemModel';
|
|
@@ -36,6 +37,14 @@ const QUICK_EDIT_MOBILE_CONTENT_PADDING = '8px var(--nb-mobile-page-tabs-content
|
|
|
36
37
|
const QUICK_EDIT_MOBILE_ACTIONS_PADDING =
|
|
37
38
|
'8px var(--nb-mobile-page-tabs-content-padding, 12px) calc(80px + env(safe-area-inset-bottom, 0px))';
|
|
38
39
|
const QUICK_EDIT_MOBILE_MEDIA_QUERY = '(max-width: 768px)';
|
|
40
|
+
// Dropdown-style editors open upwards inside the desktop popover so the option list does not cover the Submit and
|
|
41
|
+
// Cancel buttons rendered right below the field. antd still flips the list downwards when there is no room above.
|
|
42
|
+
const QUICK_EDIT_DROPDOWN_UP_MODELS = [
|
|
43
|
+
'SelectFieldModel',
|
|
44
|
+
'RecordSelectFieldModel',
|
|
45
|
+
'CascadeSelectFieldModel',
|
|
46
|
+
'CascadeSelectListFieldModel',
|
|
47
|
+
];
|
|
39
48
|
|
|
40
49
|
type QuickEditViewBeforeClosePayload = {
|
|
41
50
|
result?: unknown;
|
|
@@ -159,6 +168,7 @@ export class QuickEditFormModel extends FlowModel {
|
|
|
159
168
|
__onSubmitSuccess;
|
|
160
169
|
_fieldProps: any;
|
|
161
170
|
_onOk: any;
|
|
171
|
+
_dataScopeFilter: Record<string, any> | undefined;
|
|
162
172
|
|
|
163
173
|
get form() {
|
|
164
174
|
return this.context.form;
|
|
@@ -212,6 +222,7 @@ export class QuickEditFormModel extends FlowModel {
|
|
|
212
222
|
},
|
|
213
223
|
sourceFieldModel ? { delegate: sourceFieldModel.context } : undefined,
|
|
214
224
|
) as QuickEditFormModel;
|
|
225
|
+
model._dataScopeFilter = getQuickEditDataScopeFilter(sourceFieldModel);
|
|
215
226
|
|
|
216
227
|
const bodyStyles = {
|
|
217
228
|
maxHeight: QUICK_EDIT_POPOVER_MAX_HEIGHT,
|
|
@@ -410,6 +421,13 @@ QuickEditFormModel.registerFlow({
|
|
|
410
421
|
resource.setDataSourceKey(dataSourceKey);
|
|
411
422
|
resource.setResourceName(collectionName);
|
|
412
423
|
ctx.model.resource = resource;
|
|
424
|
+
// Seed the edited row before dispatching the field model's flows. An association data scope resolves
|
|
425
|
+
// `ctx.record` while the field renders, and so does the foreign key filter that one-to-one and one-to-many
|
|
426
|
+
// editors build, so the record has to be in place first.
|
|
427
|
+
if (ctx.inputArgs.filterByTk || ctx.inputArgs.record) {
|
|
428
|
+
resource.setFilterByTk(ctx.inputArgs.filterByTk);
|
|
429
|
+
resource.setData(ctx.inputArgs.record);
|
|
430
|
+
}
|
|
413
431
|
const collectionField = ctx.model.collection.getField(fieldPath) as CollectionField;
|
|
414
432
|
if (collectionField) {
|
|
415
433
|
const binding = FormItemModel.getDefaultBindingByField(ctx, collectionField);
|
|
@@ -417,24 +435,42 @@ QuickEditFormModel.registerFlow({
|
|
|
417
435
|
return;
|
|
418
436
|
}
|
|
419
437
|
const use = binding.modelName;
|
|
438
|
+
const stepParams: Record<string, Record<string, Record<string, unknown>>> = {
|
|
439
|
+
fieldSettings: {
|
|
440
|
+
init: {
|
|
441
|
+
dataSourceKey,
|
|
442
|
+
collectionName,
|
|
443
|
+
fieldPath,
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
const dataScopeFilter = ctx.model._dataScopeFilter;
|
|
448
|
+
if (dataScopeFilter) {
|
|
449
|
+
const dataScopeStep = findDataScopeStep(ctx.engine.getModelClass(use));
|
|
450
|
+
if (dataScopeStep) {
|
|
451
|
+
stepParams[dataScopeStep.flowKey] = {
|
|
452
|
+
...stepParams[dataScopeStep.flowKey],
|
|
453
|
+
[dataScopeStep.stepKey]: { filter: dataScopeFilter },
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
420
457
|
const fieldModel = ctx.model.addSubModel<FieldModel>('fields', {
|
|
421
458
|
use,
|
|
422
459
|
props:
|
|
423
460
|
typeof binding.defaultProps === 'function'
|
|
424
461
|
? binding.defaultProps(ctx, collectionField)
|
|
425
462
|
: binding.defaultProps,
|
|
426
|
-
stepParams
|
|
427
|
-
fieldSettings: {
|
|
428
|
-
init: {
|
|
429
|
-
dataSourceKey,
|
|
430
|
-
collectionName,
|
|
431
|
-
fieldPath,
|
|
432
|
-
},
|
|
433
|
-
},
|
|
434
|
-
},
|
|
463
|
+
stepParams,
|
|
435
464
|
});
|
|
436
465
|
fieldModel.setProps(getQuickEditFieldProps(collectionField, ctx.model._fieldProps));
|
|
437
466
|
fieldModel.setProps({ sourceFieldModelUid: ctx.inputArgs.sourceFieldModelUid });
|
|
467
|
+
if (
|
|
468
|
+
!ctx.model.context.isMobileLayout &&
|
|
469
|
+
QUICK_EDIT_DROPDOWN_UP_MODELS.includes(use) &&
|
|
470
|
+
fieldModel.props.placement == null
|
|
471
|
+
) {
|
|
472
|
+
fieldModel.setProps({ placement: 'topLeft' });
|
|
473
|
+
}
|
|
438
474
|
ctx.model.context.defineProperty('collectionField', {
|
|
439
475
|
get: () => collectionField,
|
|
440
476
|
});
|
|
@@ -442,8 +478,6 @@ QuickEditFormModel.registerFlow({
|
|
|
442
478
|
await fieldModel.dispatchEvent('beforeRender');
|
|
443
479
|
}
|
|
444
480
|
if (ctx.inputArgs.filterByTk || ctx.inputArgs.record) {
|
|
445
|
-
resource.setFilterByTk(ctx.inputArgs.filterByTk);
|
|
446
|
-
resource.setData(ctx.inputArgs.record);
|
|
447
481
|
ctx.model.form?.setFieldsValue(resource.getData());
|
|
448
482
|
}
|
|
449
483
|
},
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
11
|
import { FlowEngine, FlowModel, SingleRecordResource } from '@nocobase/flow-engine';
|
|
12
|
+
import '../../../index';
|
|
12
13
|
import { QuickEditFormModel } from '../QuickEditFormModel';
|
|
14
|
+
import { dataScope } from '../../../../actions/dataScope';
|
|
15
|
+
import { RecordSelectFieldModel } from '../../../fields/AssociationFieldModel/RecordSelectFieldModel';
|
|
16
|
+
import { SelectFieldModel } from '../../../fields/SelectFieldModel';
|
|
13
17
|
|
|
14
18
|
describe('QuickEditFormModel - quick edit save triggers API (regression)', () => {
|
|
15
19
|
let engine: FlowEngine;
|
|
@@ -55,6 +59,54 @@ describe('QuickEditFormModel - quick edit save triggers API (regression)', () =>
|
|
|
55
59
|
});
|
|
56
60
|
};
|
|
57
61
|
|
|
62
|
+
const addMemberCollections = () => {
|
|
63
|
+
const ds = engine.context.dataSourceManager.getDataSource('main');
|
|
64
|
+
ds.addCollection({
|
|
65
|
+
name: 'departments',
|
|
66
|
+
filterTargetKey: 'id',
|
|
67
|
+
titleField: 'name',
|
|
68
|
+
fields: [
|
|
69
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
70
|
+
{ name: 'name', type: 'string', interface: 'input', uiSchema: { title: 'Name' } },
|
|
71
|
+
{ name: 'level', type: 'string', interface: 'input', uiSchema: { title: 'Level' } },
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
ds.addCollection({
|
|
75
|
+
name: 'members',
|
|
76
|
+
filterTargetKey: 'id',
|
|
77
|
+
fields: [
|
|
78
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
79
|
+
{ name: 'level', type: 'string', interface: 'input', uiSchema: { title: 'Level' } },
|
|
80
|
+
{
|
|
81
|
+
name: 'department',
|
|
82
|
+
type: 'belongsTo',
|
|
83
|
+
interface: 'm2o',
|
|
84
|
+
target: 'departments',
|
|
85
|
+
targetKey: 'id',
|
|
86
|
+
uiSchema: { title: 'Department' },
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const openQuickEditForAssociation = async (dataScopeFilter?: Record<string, unknown>) => {
|
|
93
|
+
engine.registerModels({ QuickEditFormModel, RecordSelectFieldModel });
|
|
94
|
+
engine.registerActions({ dataScope });
|
|
95
|
+
addMemberCollections();
|
|
96
|
+
const model = engine.createModel<QuickEditFormModel>({
|
|
97
|
+
use: QuickEditFormModel,
|
|
98
|
+
uid: `qe-association-${dataScopeFilter ? 'scoped' : 'plain'}`,
|
|
99
|
+
stepParams: {
|
|
100
|
+
quickEditFormSettings: {
|
|
101
|
+
init: { dataSourceKey: 'main', collectionName: 'members', fieldPath: 'department' },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
model._dataScopeFilter = dataScopeFilter;
|
|
106
|
+
await model.applyFlow('quickEditFormSettings', { filterByTk: 7, record: { id: 7, level: 'senior' } });
|
|
107
|
+
return model;
|
|
108
|
+
};
|
|
109
|
+
|
|
58
110
|
it('uses source field context when opening quick edit', async () => {
|
|
59
111
|
engine.registerModels({ QuickEditFormModel });
|
|
60
112
|
engine.context.defineProperty('pageActive', { value: { value: false } });
|
|
@@ -414,4 +466,72 @@ describe('QuickEditFormModel - quick edit save triggers API (regression)', () =>
|
|
|
414
466
|
|
|
415
467
|
expect(api.request).toHaveBeenCalledTimes(1);
|
|
416
468
|
});
|
|
469
|
+
|
|
470
|
+
it('opens dropdown editors upwards in the desktop popover so they do not cover the actions', async () => {
|
|
471
|
+
engine.registerModels({ QuickEditFormModel, SelectFieldModel });
|
|
472
|
+
const ds = engine.context.dataSourceManager.getDataSource('main');
|
|
473
|
+
ds.addCollection({
|
|
474
|
+
name: 'users',
|
|
475
|
+
fields: [
|
|
476
|
+
{
|
|
477
|
+
name: 'tags',
|
|
478
|
+
type: 'array',
|
|
479
|
+
interface: 'multipleSelect',
|
|
480
|
+
uiSchema: { title: 'Tags', enum: [{ label: 'A', value: 'a' }] },
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const desktopModel = engine.createModel<QuickEditFormModel>({
|
|
486
|
+
use: QuickEditFormModel,
|
|
487
|
+
uid: 'qe-desktop',
|
|
488
|
+
stepParams: {
|
|
489
|
+
quickEditFormSettings: {
|
|
490
|
+
init: { dataSourceKey: 'main', collectionName: 'users', fieldPath: 'tags' },
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
await desktopModel.applyFlow('quickEditFormSettings', { record: {} });
|
|
495
|
+
const desktopField = desktopModel.subModels.fields[0];
|
|
496
|
+
expect(desktopField.use).toBe('SelectFieldModel');
|
|
497
|
+
expect(desktopField.props.placement).toBe('topLeft');
|
|
498
|
+
|
|
499
|
+
const mobileModel = engine.createModel<QuickEditFormModel>({
|
|
500
|
+
use: QuickEditFormModel,
|
|
501
|
+
uid: 'qe-mobile',
|
|
502
|
+
stepParams: {
|
|
503
|
+
quickEditFormSettings: {
|
|
504
|
+
init: { dataSourceKey: 'main', collectionName: 'users', fieldPath: 'tags' },
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
mobileModel.context.defineProperty('isMobileLayout', { value: true });
|
|
509
|
+
await mobileModel.applyFlow('quickEditFormSettings', { record: {} });
|
|
510
|
+
expect(mobileModel.subModels.fields[0].props.placement).toBeUndefined();
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('hands the column data scope to the association editor built for the popover', async () => {
|
|
514
|
+
const filter = { logic: '$and', items: [{ path: 'level', operator: 'eq', value: '{{ctx.record.level}}' }] };
|
|
515
|
+
|
|
516
|
+
const model = await openQuickEditForAssociation(filter);
|
|
517
|
+
|
|
518
|
+
const fieldModel = model.subModels.fields[0];
|
|
519
|
+
expect(fieldModel.use).toBe('RecordSelectFieldModel');
|
|
520
|
+
expect(fieldModel.getStepParams('selectSettings', 'dataScope')).toEqual({ filter });
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('leaves the association editor unscoped when the column has no data scope', async () => {
|
|
524
|
+
const model = await openQuickEditForAssociation();
|
|
525
|
+
|
|
526
|
+
expect(model.subModels.fields[0].getStepParams('selectSettings', 'dataScope')).toBeUndefined();
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('seeds the edited row before the editor flows run so record variables resolve', async () => {
|
|
530
|
+
const filter = { logic: '$and', items: [{ path: 'level', operator: 'eq', value: '{{ctx.record.level}}' }] };
|
|
531
|
+
|
|
532
|
+
const model = await openQuickEditForAssociation(filter);
|
|
533
|
+
|
|
534
|
+
expect(model.resource.getData()).toMatchObject({ id: 7, level: 'senior' });
|
|
535
|
+
expect(JSON.stringify(model.subModels.fields[0].resource.getFilter() ?? null)).toContain('senior');
|
|
536
|
+
});
|
|
417
537
|
});
|
|
@@ -152,6 +152,7 @@ const JSBlockPlainHost = ({
|
|
|
152
152
|
afterContent,
|
|
153
153
|
contentRef,
|
|
154
154
|
marginBlock,
|
|
155
|
+
minHeight,
|
|
155
156
|
...rest
|
|
156
157
|
}: React.HTMLAttributes<HTMLDivElement> & {
|
|
157
158
|
uid: string;
|
|
@@ -161,6 +162,7 @@ const JSBlockPlainHost = ({
|
|
|
161
162
|
afterContent?: React.ReactNode;
|
|
162
163
|
contentRef: React.RefObject<HTMLDivElement>;
|
|
163
164
|
marginBlock: number;
|
|
165
|
+
minHeight: number;
|
|
164
166
|
}) => {
|
|
165
167
|
const hostRef = React.useRef<HTMLDivElement | null>(null);
|
|
166
168
|
const resolvedHeight = usePlainHostHeight({ height, heightMode, hostRef, marginBlock });
|
|
@@ -175,7 +177,7 @@ const JSBlockPlainHost = ({
|
|
|
175
177
|
display: 'flex',
|
|
176
178
|
flexDirection: 'column',
|
|
177
179
|
height: resolvedHeight ?? undefined,
|
|
178
|
-
minHeight
|
|
180
|
+
minHeight,
|
|
179
181
|
overflow: 'auto',
|
|
180
182
|
...(style || {}),
|
|
181
183
|
}}
|
|
@@ -228,6 +230,8 @@ export class JSBlockModel extends BlockModel {
|
|
|
228
230
|
afterContent={afterContent}
|
|
229
231
|
contentRef={this.context.ref}
|
|
230
232
|
marginBlock={this.context.themeToken?.marginBlock ?? 0}
|
|
233
|
+
// Keep empty blocks reachable by the settings overlay while the UI editor is active.
|
|
234
|
+
minHeight={this.context.flowSettingsEnabled ? this.context.themeToken?.controlHeightLG ?? 40 : 0}
|
|
231
235
|
/>
|
|
232
236
|
);
|
|
233
237
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { App, ConfigProvider } from 'antd';
|
|
12
|
-
import { render } from '@nocobase/test/client';
|
|
12
|
+
import { act, render, waitFor } from '@nocobase/test/client';
|
|
13
13
|
import { describe, expect, it, vi } from 'vitest';
|
|
14
14
|
import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
|
15
15
|
import { JSBlockModel } from '../JSBlock';
|
|
@@ -78,6 +78,28 @@ describe('JSBlockModel', () => {
|
|
|
78
78
|
expect(model.context.ref.current).toBe(host?.firstElementChild);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
it('keeps an empty cardless block reachable only while the UI editor is active', async () => {
|
|
82
|
+
const { engine, model } = createJSBlock('js-block-empty-without-card', false, { style: undefined });
|
|
83
|
+
await engine.flowSettings.disable();
|
|
84
|
+
const { container } = renderBlock(engine, model);
|
|
85
|
+
const host = container.querySelector('#model-js-block-empty-without-card') as HTMLElement;
|
|
86
|
+
const content = model.context.ref.current;
|
|
87
|
+
|
|
88
|
+
expect(host.style.minHeight).toBe('0');
|
|
89
|
+
await act(async () => {
|
|
90
|
+
await engine.flowSettings.enable();
|
|
91
|
+
});
|
|
92
|
+
await waitFor(() => expect(host.style.minHeight).toBe('40px'));
|
|
93
|
+
expect(model.context.ref.current).toBe(content);
|
|
94
|
+
expect(container.querySelector('.ant-card')).toBeNull();
|
|
95
|
+
|
|
96
|
+
await act(async () => {
|
|
97
|
+
await engine.flowSettings.disable();
|
|
98
|
+
});
|
|
99
|
+
await waitFor(() => expect(host.style.minHeight).toBe('0'));
|
|
100
|
+
expect(model.context.ref.current).toBe(content);
|
|
101
|
+
});
|
|
102
|
+
|
|
81
103
|
it('keeps cardless specified-height content scrollable inside the plain host', () => {
|
|
82
104
|
const { engine, model } = createJSBlock('js-block-without-card-height', false, {
|
|
83
105
|
heightMode: 'specifyValue',
|
|
@@ -37,6 +37,7 @@ import { getRowKey } from './utils';
|
|
|
37
37
|
import { getSavedAssociationTitleField, getTableColumnSortField } from './sortUtils';
|
|
38
38
|
import { getFieldBindingUse, rebuildFieldSubModel } from '../../../internal/utils/rebuildFieldSubModel';
|
|
39
39
|
import { getSavedDateTimeFormatParams, resolveDateTimeDisplayProps } from '../../../utils/dateTimeDisplayProps';
|
|
40
|
+
import { QuickEditDataScopeInput, resolveQuickEditDataScopeStep } from '../../../internal/utils/quickEditDataScope';
|
|
40
41
|
|
|
41
42
|
export function FieldDeletePlaceholder(props: any) {
|
|
42
43
|
const { t } = useTranslation();
|
|
@@ -542,6 +543,36 @@ TableColumnModel.registerFlow({
|
|
|
542
543
|
ctx.model.setProps('editable', ctx.model.associationPathName ? false : params.editable);
|
|
543
544
|
},
|
|
544
545
|
},
|
|
546
|
+
quickEditDataScope: {
|
|
547
|
+
title: tExpr('Data scope'),
|
|
548
|
+
uiMode: {
|
|
549
|
+
type: 'dialog',
|
|
550
|
+
props: {
|
|
551
|
+
width: 800,
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
uiSchema: {
|
|
555
|
+
filter: {
|
|
556
|
+
type: 'object',
|
|
557
|
+
'x-decorator': 'FormItem',
|
|
558
|
+
'x-component': QuickEditDataScopeInput,
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
useRawParams: true,
|
|
562
|
+
defaultParams: {
|
|
563
|
+
filter: { logic: '$and', items: [] },
|
|
564
|
+
},
|
|
565
|
+
hideInSettings(ctx) {
|
|
566
|
+
if (ctx.model.associationPathName || !ctx.model.props.editable) {
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
return !resolveQuickEditDataScopeStep(ctx, ctx.collectionField);
|
|
570
|
+
},
|
|
571
|
+
handler() {
|
|
572
|
+
// Settings-only step. The filter is kept raw on the column and handed to the editor model that
|
|
573
|
+
// QuickEditFormModel builds when the popover opens, so there is nothing to apply to the column itself.
|
|
574
|
+
},
|
|
575
|
+
},
|
|
545
576
|
model: {
|
|
546
577
|
title: tExpr('Field component'),
|
|
547
578
|
use: 'displayFieldComponent',
|
|
@@ -9,8 +9,36 @@
|
|
|
9
9
|
|
|
10
10
|
import { FlowEngine } from '@nocobase/flow-engine';
|
|
11
11
|
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
// Prime the model module graph so the editable field bindings used by the quick edit data scope setting are registered.
|
|
13
|
+
import '../../../index';
|
|
14
|
+
import { RecordSelectFieldModel } from '../../../fields/AssociationFieldModel/RecordSelectFieldModel';
|
|
12
15
|
import { TableColumnModel } from '../TableColumnModel';
|
|
13
16
|
|
|
17
|
+
const createQuickEditDataScopeCtx = (
|
|
18
|
+
overrides: { associationPathName?: string; editable?: boolean; isAssociation?: boolean } = {},
|
|
19
|
+
) => {
|
|
20
|
+
const engine = new FlowEngine();
|
|
21
|
+
engine.registerModels({ RecordSelectFieldModel });
|
|
22
|
+
const { associationPathName, editable = true, isAssociation = true } = overrides;
|
|
23
|
+
return {
|
|
24
|
+
engine,
|
|
25
|
+
model: {
|
|
26
|
+
associationPathName,
|
|
27
|
+
props: { editable },
|
|
28
|
+
},
|
|
29
|
+
collectionField: {
|
|
30
|
+
interface: isAssociation ? 'm2o' : 'input',
|
|
31
|
+
isAssociationField: () => isAssociation,
|
|
32
|
+
targetCollection: isAssociation ? { template: 'general' } : undefined,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getQuickEditDataScopeStep = (engine: FlowEngine) => {
|
|
38
|
+
const model = new TableColumnModel({ uid: 'table-column-quick-edit-data-scope', flowEngine: engine } as any);
|
|
39
|
+
return model.getFlow('tableColumnSettings')?.steps?.quickEditDataScope as any;
|
|
40
|
+
};
|
|
41
|
+
|
|
14
42
|
describe('TableColumnModel sorter settings', () => {
|
|
15
43
|
it('clamps custom column width to the minimum value', () => {
|
|
16
44
|
const engine = new FlowEngine();
|
|
@@ -30,6 +58,42 @@ describe('TableColumnModel sorter settings', () => {
|
|
|
30
58
|
expect(setProps).toHaveBeenCalledWith('width', 10);
|
|
31
59
|
});
|
|
32
60
|
|
|
61
|
+
it('offers the quick edit data scope on an editable association column', async () => {
|
|
62
|
+
const engine = new FlowEngine();
|
|
63
|
+
const step = getQuickEditDataScopeStep(engine);
|
|
64
|
+
|
|
65
|
+
const hidden = await step.hideInSettings(createQuickEditDataScopeCtx());
|
|
66
|
+
|
|
67
|
+
expect(hidden).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('hides the quick edit data scope when quick edit is off', async () => {
|
|
71
|
+
const engine = new FlowEngine();
|
|
72
|
+
const step = getQuickEditDataScopeStep(engine);
|
|
73
|
+
|
|
74
|
+
const hidden = await step.hideInSettings(createQuickEditDataScopeCtx({ editable: false }));
|
|
75
|
+
|
|
76
|
+
expect(hidden).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('hides the quick edit data scope for non-association columns', async () => {
|
|
80
|
+
const engine = new FlowEngine();
|
|
81
|
+
const step = getQuickEditDataScopeStep(engine);
|
|
82
|
+
|
|
83
|
+
const hidden = await step.hideInSettings(createQuickEditDataScopeCtx({ isAssociation: false }));
|
|
84
|
+
|
|
85
|
+
expect(hidden).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('hides the quick edit data scope for relation path columns', async () => {
|
|
89
|
+
const engine = new FlowEngine();
|
|
90
|
+
const step = getQuickEditDataScopeStep(engine);
|
|
91
|
+
|
|
92
|
+
const hidden = await step.hideInSettings(createQuickEditDataScopeCtx({ associationPathName: 'department' }));
|
|
93
|
+
|
|
94
|
+
expect(hidden).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
33
97
|
it('hides quick edit setting for relation path columns added from association groups', async () => {
|
|
34
98
|
const engine = new FlowEngine();
|
|
35
99
|
const model = new TableColumnModel({ uid: 'table-column-relation-path-quick-edit', flowEngine: engine } as any);
|
|
@@ -25,6 +25,7 @@ import { arrayMove, SortableContext, verticalListSortingStrategy, useSortable }
|
|
|
25
25
|
import { CSS } from '@dnd-kit/utilities';
|
|
26
26
|
import { AssociationFieldModel } from './AssociationFieldModel';
|
|
27
27
|
import { transformNestedData } from '../ClickableFieldModel';
|
|
28
|
+
import { syncQuickEditPreventClose } from '../../../internal/utils/quickEditViewContainer';
|
|
28
29
|
|
|
29
30
|
type CascadeHydrateStatus = 'pending' | 'done';
|
|
30
31
|
|
|
@@ -373,7 +374,12 @@ export class CascadeSelectInnerFieldModel extends AssociationFieldModel {
|
|
|
373
374
|
this.setProps({ onPopupScroll: fn });
|
|
374
375
|
}
|
|
375
376
|
set onDropdownVisibleChange(fn) {
|
|
376
|
-
this.setProps({
|
|
377
|
+
this.setProps({
|
|
378
|
+
onDropdownVisibleChange: (visible: boolean) => {
|
|
379
|
+
syncQuickEditPreventClose(this, visible);
|
|
380
|
+
fn(visible);
|
|
381
|
+
},
|
|
382
|
+
});
|
|
377
383
|
}
|
|
378
384
|
set onSearch(fn) {
|
|
379
385
|
this.setProps({ onSearch: fn });
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
createCurrentRecordMetaFactory,
|
|
20
20
|
useFlowModel,
|
|
21
21
|
} from '@nocobase/flow-engine';
|
|
22
|
-
import { Table, Button, Space } from 'antd';
|
|
22
|
+
import { Table, Button, Space, theme } from 'antd';
|
|
23
23
|
import { uid } from '@formily/shared';
|
|
24
24
|
import classNames from 'classnames';
|
|
25
25
|
import { DragEndEvent } from '@dnd-kit/core';
|
|
@@ -190,6 +190,7 @@ const AddFieldColumn = ({ model }) => {
|
|
|
190
190
|
};
|
|
191
191
|
|
|
192
192
|
const DisplayTable = (props) => {
|
|
193
|
+
const { token } = theme.useToken();
|
|
193
194
|
const {
|
|
194
195
|
pageSize,
|
|
195
196
|
value,
|
|
@@ -269,7 +270,7 @@ const DisplayTable = (props) => {
|
|
|
269
270
|
},
|
|
270
271
|
},
|
|
271
272
|
...baseColumns,
|
|
272
|
-
{ key: 'empty' },
|
|
273
|
+
{ key: 'empty', render: () => <div style={{ minHeight: token.controlHeight }} /> },
|
|
273
274
|
].filter(Boolean),
|
|
274
275
|
) as any[];
|
|
275
276
|
|
|
@@ -283,7 +284,7 @@ const DisplayTable = (props) => {
|
|
|
283
284
|
}
|
|
284
285
|
|
|
285
286
|
return cols;
|
|
286
|
-
}, [baseColumns, enableIndexColumn, currentPage, currentPageSize, isConfigMode, model]);
|
|
287
|
+
}, [baseColumns, enableIndexColumn, currentPage, currentPageSize, isConfigMode, model, token.controlHeight]);
|
|
287
288
|
return (
|
|
288
289
|
<Table
|
|
289
290
|
tableLayout="fixed"
|
|
@@ -41,6 +41,7 @@ import { BlockSceneEnum } from '../../base/BlockModel';
|
|
|
41
41
|
import { ActionWithoutPermission } from '../../base/ActionModel';
|
|
42
42
|
import { EditFormModel } from '../../blocks';
|
|
43
43
|
import { hasAncestorModel } from './recordSelectSettingsUtils';
|
|
44
|
+
import { syncQuickEditPreventClose } from '../../../internal/utils/quickEditViewContainer';
|
|
44
45
|
|
|
45
46
|
function RemoteModelRenderer({ options }) {
|
|
46
47
|
const ctx = useFlowViewContext();
|
|
@@ -65,6 +66,11 @@ function RemoteModelRenderer({ options }) {
|
|
|
65
66
|
|
|
66
67
|
export function CreateContent({ model, toOne = false }) {
|
|
67
68
|
const ctx = useFlowContext();
|
|
69
|
+
// Quick-create popups isolate business context but must retain the field's configuration mode.
|
|
70
|
+
ctx.defineProperty('flowSettingsEnabled', {
|
|
71
|
+
get: () => model.context.flowSettingsEnabled,
|
|
72
|
+
cache: false,
|
|
73
|
+
});
|
|
68
74
|
const { Header, type } = ctx.view;
|
|
69
75
|
model._closeView = ctx.view.close;
|
|
70
76
|
return (
|
|
@@ -308,6 +314,7 @@ const LazySelect = (props: Readonly<LazySelectProps>) => {
|
|
|
308
314
|
}
|
|
309
315
|
setKeepOpenRuntime({ preventCloseOnSelect: false, open });
|
|
310
316
|
}
|
|
317
|
+
syncQuickEditPreventClose(model, open);
|
|
311
318
|
others.onDropdownVisibleChange?.(open);
|
|
312
319
|
}}
|
|
313
320
|
optionRender={({ data }) => {
|
|
@@ -874,6 +881,8 @@ RecordSelectFieldModel.registerFlow({
|
|
|
874
881
|
dataSourceKey: ctx.collection.dataSourceKey,
|
|
875
882
|
collectionName: ctx.collectionField?.target,
|
|
876
883
|
...(associationName && sourceId != null ? { associationName, sourceId } : {}),
|
|
884
|
+
// 上级记录暂无主键(如审批配置态表单)时不能传 associationName,否则弹窗内新建的区块会绑定到关联资源;改传 sourceAssociationName,仅用于展示“当前弹窗上级记录”变量
|
|
885
|
+
...(associationName && sourceId == null ? { sourceAssociationName: associationName } : {}),
|
|
877
886
|
collectionField: ctx.collectionField,
|
|
878
887
|
openerUids,
|
|
879
888
|
onChange: (e) => {
|