@nocobase/client-v2 2.1.0-alpha.24 → 2.1.0-alpha.25
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/flow/models/base/BlockGridModel.d.ts +2 -1
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +5 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +12 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/rowIdentity.d.ts +18 -0
- package/es/index.mjs +70 -70
- package/lib/index.js +71 -71
- package/package.json +5 -5
- package/src/flow/models/base/BlockGridModel.tsx +48 -2
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneAddBlock.test.ts +83 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +293 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/subtable-nested.test.ts +3 -2
- package/src/flow/models/blocks/form/value-runtime/rules.ts +66 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +285 -12
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +46 -22
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/rowIdentity.ts +70 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/SubTableRowIdentity.test.ts +45 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.1.0-alpha.
|
|
3
|
+
"version": "2.1.0-alpha.25",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"@formily/antd-v5": "1.2.3",
|
|
25
25
|
"@formily/react": "^2.2.27",
|
|
26
26
|
"@formily/shared": "^2.2.27",
|
|
27
|
-
"@nocobase/flow-engine": "2.1.0-alpha.
|
|
28
|
-
"@nocobase/sdk": "2.1.0-alpha.
|
|
29
|
-
"@nocobase/shared": "2.1.0-alpha.
|
|
27
|
+
"@nocobase/flow-engine": "2.1.0-alpha.25",
|
|
28
|
+
"@nocobase/sdk": "2.1.0-alpha.25",
|
|
29
|
+
"@nocobase/shared": "2.1.0-alpha.25",
|
|
30
30
|
"antd": "5.24.2",
|
|
31
31
|
"classnames": "^2.3.1",
|
|
32
32
|
"dayjs": "^1.11.10",
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"react-i18next": "^11.15.1",
|
|
36
36
|
"react-router-dom": "^6.30.1"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "63e4aaa625f3108fe41238e85bb13dee37fe1f48"
|
|
39
39
|
}
|
|
@@ -8,11 +8,21 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { PlusOutlined } from '@ant-design/icons';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
AddSubModelButton,
|
|
13
|
+
DragOverlayConfig,
|
|
14
|
+
FlowSettingsButton,
|
|
15
|
+
buildSubModelGroups,
|
|
16
|
+
buildSubModelItems,
|
|
17
|
+
type SubModelItem,
|
|
18
|
+
type SubModelItemsType,
|
|
19
|
+
} from '@nocobase/flow-engine';
|
|
12
20
|
import React from 'react';
|
|
13
21
|
import { FilterManager } from '../blocks/filter-manager/FilterManager';
|
|
14
22
|
import { GridModel } from './GridModel';
|
|
15
23
|
|
|
24
|
+
const SELECT_SCENE_ALLOWED_OTHER_BLOCK_MODELS = ['JSBlockModel', 'IframeBlockModel', 'MarkdownBlockModel'] as const;
|
|
25
|
+
|
|
16
26
|
export class BlockGridModel extends GridModel {
|
|
17
27
|
dragOverlayConfig: DragOverlayConfig = {
|
|
18
28
|
// 列内插入
|
|
@@ -53,6 +63,36 @@ export class BlockGridModel extends GridModel {
|
|
|
53
63
|
return this.context.filterManager;
|
|
54
64
|
}
|
|
55
65
|
|
|
66
|
+
get addBlockItems(): SubModelItemsType | undefined {
|
|
67
|
+
if (this.context.view?.inputArgs?.scene !== 'select') {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return async (ctx) => {
|
|
72
|
+
const items = await buildSubModelGroups(['DataBlockModel', 'FilterBlockModel'])(ctx);
|
|
73
|
+
const allowedOtherBlockModels = SELECT_SCENE_ALLOWED_OTHER_BLOCK_MODELS.filter((modelName) =>
|
|
74
|
+
Boolean(ctx.engine.getModelClass(modelName)),
|
|
75
|
+
);
|
|
76
|
+
const otherBlockItems = (
|
|
77
|
+
await Promise.all(allowedOtherBlockModels.map((modelName) => buildSubModelItems(modelName)(ctx)))
|
|
78
|
+
)
|
|
79
|
+
.flat()
|
|
80
|
+
.sort((a, b) => (a.sort ?? 1000) - (b.sort ?? 1000));
|
|
81
|
+
|
|
82
|
+
if (otherBlockItems.length > 0) {
|
|
83
|
+
items.push({
|
|
84
|
+
key: 'select-scene-other-blocks',
|
|
85
|
+
type: 'group',
|
|
86
|
+
label: ctx.t('Other blocks'),
|
|
87
|
+
sort: 1000,
|
|
88
|
+
children: otherBlockItems,
|
|
89
|
+
} satisfies SubModelItem);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return items;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
56
96
|
serialize() {
|
|
57
97
|
const data = super.serialize();
|
|
58
98
|
data['filterManager'] = this.filterManager.getFilterConfigs();
|
|
@@ -60,8 +100,14 @@ export class BlockGridModel extends GridModel {
|
|
|
60
100
|
}
|
|
61
101
|
|
|
62
102
|
renderAddSubModelButton() {
|
|
103
|
+
const items = this.addBlockItems;
|
|
63
104
|
return (
|
|
64
|
-
<AddSubModelButton
|
|
105
|
+
<AddSubModelButton
|
|
106
|
+
model={this}
|
|
107
|
+
subModelKey="items"
|
|
108
|
+
items={items}
|
|
109
|
+
subModelBaseClasses={items ? undefined : this.subModelBaseClasses}
|
|
110
|
+
>
|
|
65
111
|
<FlowSettingsButton icon={<PlusOutlined />} data-flow-add-block>
|
|
66
112
|
{this.context.t('Add block')}
|
|
67
113
|
</FlowSettingsButton>
|
|
@@ -0,0 +1,83 @@
|
|
|
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, FlowModel } from '@nocobase/flow-engine';
|
|
11
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
12
|
+
import { BlockGridModel } from '../BlockGridModel';
|
|
13
|
+
|
|
14
|
+
class BlockModel extends FlowModel {}
|
|
15
|
+
BlockModel.define({ label: 'Other blocks' });
|
|
16
|
+
|
|
17
|
+
class DataBlockModel extends BlockModel {}
|
|
18
|
+
DataBlockModel.define({ label: 'Data blocks' });
|
|
19
|
+
|
|
20
|
+
class FilterBlockModel extends BlockModel {}
|
|
21
|
+
FilterBlockModel.define({ label: 'Filter blocks' });
|
|
22
|
+
|
|
23
|
+
class SelectTableBlockModel extends DataBlockModel {}
|
|
24
|
+
SelectTableBlockModel.define({ label: 'Table', children: false });
|
|
25
|
+
|
|
26
|
+
class SelectFilterBlockModel extends FilterBlockModel {}
|
|
27
|
+
SelectFilterBlockModel.define({ label: 'Filter', children: false });
|
|
28
|
+
|
|
29
|
+
class JSBlockModel extends BlockModel {}
|
|
30
|
+
JSBlockModel.define({ label: 'JS block' });
|
|
31
|
+
|
|
32
|
+
class IframeBlockModel extends BlockModel {}
|
|
33
|
+
IframeBlockModel.define({ label: 'Iframe' });
|
|
34
|
+
|
|
35
|
+
class MarkdownBlockModel extends BlockModel {}
|
|
36
|
+
MarkdownBlockModel.define({ label: 'Markdown' });
|
|
37
|
+
|
|
38
|
+
class ActionPanelBlockModel extends BlockModel {}
|
|
39
|
+
ActionPanelBlockModel.define({ label: 'Action panel' });
|
|
40
|
+
|
|
41
|
+
class ReferenceBlockModel extends BlockModel {}
|
|
42
|
+
ReferenceBlockModel.define({ label: 'Block template' });
|
|
43
|
+
|
|
44
|
+
describe('BlockGridModel - select scene add block menu', () => {
|
|
45
|
+
let engine: FlowEngine;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
engine = new FlowEngine();
|
|
49
|
+
engine.registerModels({
|
|
50
|
+
BlockModel,
|
|
51
|
+
DataBlockModel,
|
|
52
|
+
FilterBlockModel,
|
|
53
|
+
BlockGridModel,
|
|
54
|
+
SelectTableBlockModel,
|
|
55
|
+
SelectFilterBlockModel,
|
|
56
|
+
JSBlockModel,
|
|
57
|
+
IframeBlockModel,
|
|
58
|
+
MarkdownBlockModel,
|
|
59
|
+
ActionPanelBlockModel,
|
|
60
|
+
ReferenceBlockModel,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('keeps only JS, iframe and markdown under other blocks in select scene', async () => {
|
|
65
|
+
engine.context.defineProperty('view', { value: { inputArgs: { scene: 'select' } } });
|
|
66
|
+
const model = engine.createModel<BlockGridModel>({ use: 'BlockGridModel' });
|
|
67
|
+
|
|
68
|
+
const itemsSource = model.addBlockItems;
|
|
69
|
+
expect(typeof itemsSource).toBe('function');
|
|
70
|
+
|
|
71
|
+
const items = await itemsSource!(model.context);
|
|
72
|
+
const otherBlocks = items.find((item) => item.key === 'select-scene-other-blocks');
|
|
73
|
+
|
|
74
|
+
expect(otherBlocks).toBeTruthy();
|
|
75
|
+
expect(otherBlocks?.type).toBe('group');
|
|
76
|
+
expect(Array.isArray(otherBlocks?.children)).toBe(true);
|
|
77
|
+
|
|
78
|
+
const childKeys = (otherBlocks?.children as any[]).map((item) => item.key);
|
|
79
|
+
expect(childKeys).toEqual(['JSBlockModel', 'IframeBlockModel', 'MarkdownBlockModel']);
|
|
80
|
+
expect(childKeys).not.toContain('ActionPanelBlockModel');
|
|
81
|
+
expect(childKeys).not.toContain('ReferenceBlockModel');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -1706,6 +1706,299 @@ describe('FormValueRuntime (form assign rules)', () => {
|
|
|
1706
1706
|
expect(formStub.getFieldValue(['users', 0, 'name'])).toBe('');
|
|
1707
1707
|
});
|
|
1708
1708
|
|
|
1709
|
+
it('clears explicit state for deleted to-many rows even when allValues is stale', () => {
|
|
1710
|
+
const engineEmitter = new EventEmitter();
|
|
1711
|
+
const blockEmitter = new EventEmitter();
|
|
1712
|
+
const formStub = createFormStub({
|
|
1713
|
+
roles: [{ title: 'Z', __is_new__: true }],
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1716
|
+
const blockModel: any = {
|
|
1717
|
+
uid: 'form-assign-default-create-readd-row',
|
|
1718
|
+
flowEngine: { emitter: engineEmitter },
|
|
1719
|
+
emitter: blockEmitter,
|
|
1720
|
+
dispatchEvent: vi.fn(),
|
|
1721
|
+
getAclActionName: () => 'create',
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
|
|
1725
|
+
runtime.mount({ sync: true });
|
|
1726
|
+
|
|
1727
|
+
lodashSet((formStub as any).__store, ['roles', 0, 'title'], 'custom');
|
|
1728
|
+
runtime.handleFormFieldsChange([{ name: ['roles', 0, 'title'], touched: true } as any]);
|
|
1729
|
+
expect((runtime as any).findExplicitHit('roles[0].title')).toBe('roles[0].title');
|
|
1730
|
+
|
|
1731
|
+
const staleAllValues = { roles: [{ title: 'custom', __is_new__: true }] };
|
|
1732
|
+
lodashSet((formStub as any).__store, ['roles'], []);
|
|
1733
|
+
runtime.handleFormValuesChange({ roles: [] }, staleAllValues);
|
|
1734
|
+
|
|
1735
|
+
expect((runtime as any).findExplicitHit('roles[0].title')).toBeNull();
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
it('moves explicit state with filterTargetKey identified to-many rows after deleting a preceding row', () => {
|
|
1739
|
+
const engineEmitter = new EventEmitter();
|
|
1740
|
+
const blockEmitter = new EventEmitter();
|
|
1741
|
+
const formStub = createFormStub({
|
|
1742
|
+
roles: [
|
|
1743
|
+
{ code: 'admin', title: 'same-title' },
|
|
1744
|
+
{ code: 'editor', title: 'custom' },
|
|
1745
|
+
],
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
const blockModel: any = {
|
|
1749
|
+
uid: 'form-assign-default-create-shift-row',
|
|
1750
|
+
flowEngine: { emitter: engineEmitter },
|
|
1751
|
+
emitter: blockEmitter,
|
|
1752
|
+
dispatchEvent: vi.fn(),
|
|
1753
|
+
getAclActionName: () => 'create',
|
|
1754
|
+
};
|
|
1755
|
+
|
|
1756
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
|
|
1757
|
+
runtime.mount({ sync: true });
|
|
1758
|
+
|
|
1759
|
+
const blockCtx = createFieldContext(runtime);
|
|
1760
|
+
const roleRowCollection: any = { getField: () => null, filterTargetKey: 'code' };
|
|
1761
|
+
const rolesField: any = { type: 'hasMany', isAssociationField: () => true, targetCollection: roleRowCollection };
|
|
1762
|
+
const rootCollection: any = { getField: (name: string) => (name === 'roles' ? rolesField : null) };
|
|
1763
|
+
blockCtx.defineProperty('collection', { value: rootCollection });
|
|
1764
|
+
blockModel.context = blockCtx;
|
|
1765
|
+
|
|
1766
|
+
lodashSet((formStub as any).__store, ['roles', 1, 'title'], 'same-title');
|
|
1767
|
+
runtime.handleFormFieldsChange([{ name: ['roles', 1, 'title'], touched: true } as any]);
|
|
1768
|
+
expect((runtime as any).findExplicitHit('roles[1].title')).toBe('roles[1].title');
|
|
1769
|
+
|
|
1770
|
+
const shiftedRow = formStub.getFieldValue(['roles', 1]);
|
|
1771
|
+
lodashSet((formStub as any).__store, ['roles'], [shiftedRow]);
|
|
1772
|
+
runtime.handleFormValuesChange({ roles: [shiftedRow] }, formStub.getFieldsValue());
|
|
1773
|
+
|
|
1774
|
+
expect((runtime as any).findExplicitHit('roles[1].title')).toBeNull();
|
|
1775
|
+
expect((runtime as any).findExplicitHit('roles[0].title')).toBe('roles[0].title');
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
it('moves observable binding writes with filterTargetKey identified rows after deleting a preceding row', async () => {
|
|
1779
|
+
const engineEmitter = new EventEmitter();
|
|
1780
|
+
const blockEmitter = new EventEmitter();
|
|
1781
|
+
const formStub = createFormStub({
|
|
1782
|
+
roles: [{ code: 'admin' }, { code: 'editor' }],
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
const blockModel: any = {
|
|
1786
|
+
uid: 'form-assign-default-observable-shift-row',
|
|
1787
|
+
flowEngine: { emitter: engineEmitter },
|
|
1788
|
+
emitter: blockEmitter,
|
|
1789
|
+
dispatchEvent: vi.fn(),
|
|
1790
|
+
getAclActionName: () => 'create',
|
|
1791
|
+
};
|
|
1792
|
+
|
|
1793
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
|
|
1794
|
+
runtime.mount({ sync: true });
|
|
1795
|
+
|
|
1796
|
+
const blockCtx = createFieldContext(runtime);
|
|
1797
|
+
const roleRowCollection: any = { getField: () => null, filterTargetKey: 'code' };
|
|
1798
|
+
const rolesField: any = { type: 'hasMany', isAssociationField: () => true, targetCollection: roleRowCollection };
|
|
1799
|
+
const rootCollection: any = { getField: (name: string) => (name === 'roles' ? rolesField : null) };
|
|
1800
|
+
blockCtx.defineProperty('collection', { value: rootCollection });
|
|
1801
|
+
blockCtx.defineProperty('fieldIndex', { value: ['roles:1'] });
|
|
1802
|
+
blockModel.context = blockCtx;
|
|
1803
|
+
|
|
1804
|
+
const defaultMeta = observable({ label: 'Editor' });
|
|
1805
|
+
await runtime.setFormValues(blockCtx, [{ path: 'roles.meta', value: defaultMeta }], {
|
|
1806
|
+
source: 'linkage',
|
|
1807
|
+
markExplicit: false,
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
expect(formStub.getFieldValue(['roles', 1, 'meta'])).toEqual({ label: 'Editor' });
|
|
1811
|
+
|
|
1812
|
+
const shiftedRow = formStub.getFieldValue(['roles', 1]);
|
|
1813
|
+
lodashSet((formStub as any).__store, ['roles'], [shiftedRow]);
|
|
1814
|
+
runtime.handleFormValuesChange({ roles: [shiftedRow] }, formStub.getFieldsValue());
|
|
1815
|
+
|
|
1816
|
+
defaultMeta.label = 'Updated';
|
|
1817
|
+
|
|
1818
|
+
await waitFor(() => expect(formStub.getFieldValue(['roles', 0, 'meta'])).toEqual({ label: 'Updated' }));
|
|
1819
|
+
expect(formStub.getFieldValue(['roles', 1])).toBeUndefined();
|
|
1820
|
+
});
|
|
1821
|
+
|
|
1822
|
+
it('skips stale to-many row rules after the row index is out of bounds', async () => {
|
|
1823
|
+
const engineEmitter = new EventEmitter();
|
|
1824
|
+
const blockEmitter = new EventEmitter();
|
|
1825
|
+
const formStub = createFormStub({
|
|
1826
|
+
roles: [{ title: 'Z', __is_new__: true, __index__: 'row-1' }],
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
const blockModel: any = {
|
|
1830
|
+
uid: 'form-assign-default-stale-row',
|
|
1831
|
+
flowEngine: { emitter: engineEmitter },
|
|
1832
|
+
emitter: blockEmitter,
|
|
1833
|
+
dispatchEvent: vi.fn(),
|
|
1834
|
+
getAclActionName: () => 'create',
|
|
1835
|
+
};
|
|
1836
|
+
|
|
1837
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
|
|
1838
|
+
runtime.mount({ sync: true });
|
|
1839
|
+
|
|
1840
|
+
const blockCtx = createFieldContext(runtime);
|
|
1841
|
+
const roleRowCollection: any = { getField: () => null };
|
|
1842
|
+
const rolesField: any = { type: 'hasMany', isAssociationField: () => true, targetCollection: roleRowCollection };
|
|
1843
|
+
const rootCollection: any = { getField: (name: string) => (name === 'roles' ? rolesField : null) };
|
|
1844
|
+
blockCtx.defineProperty('collection', { value: rootCollection });
|
|
1845
|
+
blockModel.context = blockCtx;
|
|
1846
|
+
|
|
1847
|
+
const masterModel: any = {
|
|
1848
|
+
uid: 'roles.title',
|
|
1849
|
+
subModels: { field: {} },
|
|
1850
|
+
getStepParams(flowKey: string, stepKey: string) {
|
|
1851
|
+
if (flowKey === 'fieldSettings' && stepKey === 'init') {
|
|
1852
|
+
return { fieldPath: 'roles.title' };
|
|
1853
|
+
}
|
|
1854
|
+
return undefined;
|
|
1855
|
+
},
|
|
1856
|
+
};
|
|
1857
|
+
const masterCtx = createFieldContext(runtime);
|
|
1858
|
+
masterCtx.defineProperty('blockModel', { value: blockModel });
|
|
1859
|
+
masterCtx.defineProperty('model', { value: masterModel });
|
|
1860
|
+
masterModel.context = masterCtx;
|
|
1861
|
+
|
|
1862
|
+
const staleRow: any = {
|
|
1863
|
+
uid: 'roles.title',
|
|
1864
|
+
isFork: true,
|
|
1865
|
+
forkId: 'roles:stale',
|
|
1866
|
+
subModels: { field: {} },
|
|
1867
|
+
getStepParams(flowKey: string, stepKey: string) {
|
|
1868
|
+
if (flowKey === 'fieldSettings' && stepKey === 'init') {
|
|
1869
|
+
return { fieldPath: 'roles.title' };
|
|
1870
|
+
}
|
|
1871
|
+
return undefined;
|
|
1872
|
+
},
|
|
1873
|
+
};
|
|
1874
|
+
const staleCtx = createFieldContext(runtime);
|
|
1875
|
+
staleCtx.defineProperty('blockModel', { value: blockModel });
|
|
1876
|
+
staleCtx.defineProperty('fieldIndex', { value: ['roles:1'] });
|
|
1877
|
+
staleCtx.defineProperty('item', {
|
|
1878
|
+
value: {
|
|
1879
|
+
index: 1,
|
|
1880
|
+
length: 1,
|
|
1881
|
+
__is_new__: true,
|
|
1882
|
+
value: { title: 'Z', __is_new__: true, __index__: 'row-1' },
|
|
1883
|
+
},
|
|
1884
|
+
});
|
|
1885
|
+
staleCtx.defineProperty('model', { value: staleRow });
|
|
1886
|
+
staleRow.context = staleCtx;
|
|
1887
|
+
masterModel.forks = new Set([staleRow]);
|
|
1888
|
+
|
|
1889
|
+
blockCtx.defineProperty('engine', {
|
|
1890
|
+
value: {
|
|
1891
|
+
forEachModel: (cb: any) => {
|
|
1892
|
+
cb(masterModel);
|
|
1893
|
+
},
|
|
1894
|
+
},
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
runtime.syncAssignRules([
|
|
1898
|
+
{
|
|
1899
|
+
key: 'r1',
|
|
1900
|
+
enable: true,
|
|
1901
|
+
targetPath: 'roles.title',
|
|
1902
|
+
mode: 'default',
|
|
1903
|
+
condition: { logic: '$and', items: [] },
|
|
1904
|
+
value: 'Z',
|
|
1905
|
+
},
|
|
1906
|
+
]);
|
|
1907
|
+
|
|
1908
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1909
|
+
expect(formStub.getFieldValue(['roles'])).toHaveLength(1);
|
|
1910
|
+
expect(formStub.getFieldValue(['roles', 1])).toBeUndefined();
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
it('skips to-many row rules when filterTargetKey identity does not match the current row at that index', async () => {
|
|
1914
|
+
const engineEmitter = new EventEmitter();
|
|
1915
|
+
const blockEmitter = new EventEmitter();
|
|
1916
|
+
const formStub = createFormStub({
|
|
1917
|
+
roles: [{ code: 'admin', title: '' }],
|
|
1918
|
+
});
|
|
1919
|
+
|
|
1920
|
+
const blockModel: any = {
|
|
1921
|
+
uid: 'form-assign-default-mismatched-row',
|
|
1922
|
+
flowEngine: { emitter: engineEmitter },
|
|
1923
|
+
emitter: blockEmitter,
|
|
1924
|
+
dispatchEvent: vi.fn(),
|
|
1925
|
+
getAclActionName: () => 'create',
|
|
1926
|
+
};
|
|
1927
|
+
|
|
1928
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
|
|
1929
|
+
runtime.mount({ sync: true });
|
|
1930
|
+
|
|
1931
|
+
const blockCtx = createFieldContext(runtime);
|
|
1932
|
+
const roleRowCollection: any = { getField: () => null, filterTargetKey: 'code' };
|
|
1933
|
+
const rolesField: any = { type: 'hasMany', isAssociationField: () => true, targetCollection: roleRowCollection };
|
|
1934
|
+
const rootCollection: any = { getField: (name: string) => (name === 'roles' ? rolesField : null) };
|
|
1935
|
+
blockCtx.defineProperty('collection', { value: rootCollection });
|
|
1936
|
+
blockModel.context = blockCtx;
|
|
1937
|
+
|
|
1938
|
+
const masterModel: any = {
|
|
1939
|
+
uid: 'roles.title',
|
|
1940
|
+
subModels: { field: {} },
|
|
1941
|
+
getStepParams(flowKey: string, stepKey: string) {
|
|
1942
|
+
if (flowKey === 'fieldSettings' && stepKey === 'init') {
|
|
1943
|
+
return { fieldPath: 'roles.title' };
|
|
1944
|
+
}
|
|
1945
|
+
return undefined;
|
|
1946
|
+
},
|
|
1947
|
+
};
|
|
1948
|
+
const masterCtx = createFieldContext(runtime);
|
|
1949
|
+
masterCtx.defineProperty('blockModel', { value: blockModel });
|
|
1950
|
+
masterCtx.defineProperty('model', { value: masterModel });
|
|
1951
|
+
masterModel.context = masterCtx;
|
|
1952
|
+
|
|
1953
|
+
const mismatchedRow: any = {
|
|
1954
|
+
uid: 'roles.title',
|
|
1955
|
+
isFork: true,
|
|
1956
|
+
forkId: 'roles:mismatched',
|
|
1957
|
+
subModels: { field: {} },
|
|
1958
|
+
getStepParams(flowKey: string, stepKey: string) {
|
|
1959
|
+
if (flowKey === 'fieldSettings' && stepKey === 'init') {
|
|
1960
|
+
return { fieldPath: 'roles.title' };
|
|
1961
|
+
}
|
|
1962
|
+
return undefined;
|
|
1963
|
+
},
|
|
1964
|
+
};
|
|
1965
|
+
const rowCtx = createFieldContext(runtime);
|
|
1966
|
+
rowCtx.defineProperty('blockModel', { value: blockModel });
|
|
1967
|
+
rowCtx.defineProperty('fieldIndex', { value: ['roles:0'] });
|
|
1968
|
+
rowCtx.defineProperty('item', {
|
|
1969
|
+
value: {
|
|
1970
|
+
index: 0,
|
|
1971
|
+
length: 1,
|
|
1972
|
+
value: { code: 'editor', title: 'custom' },
|
|
1973
|
+
},
|
|
1974
|
+
});
|
|
1975
|
+
rowCtx.defineProperty('model', { value: mismatchedRow });
|
|
1976
|
+
mismatchedRow.context = rowCtx;
|
|
1977
|
+
masterModel.forks = new Set([mismatchedRow]);
|
|
1978
|
+
|
|
1979
|
+
blockCtx.defineProperty('engine', {
|
|
1980
|
+
value: {
|
|
1981
|
+
forEachModel: (cb: any) => {
|
|
1982
|
+
cb(masterModel);
|
|
1983
|
+
},
|
|
1984
|
+
},
|
|
1985
|
+
});
|
|
1986
|
+
|
|
1987
|
+
runtime.syncAssignRules([
|
|
1988
|
+
{
|
|
1989
|
+
key: 'r1',
|
|
1990
|
+
enable: true,
|
|
1991
|
+
targetPath: 'roles.title',
|
|
1992
|
+
mode: 'default',
|
|
1993
|
+
condition: { logic: '$and', items: [] },
|
|
1994
|
+
value: 'Z',
|
|
1995
|
+
},
|
|
1996
|
+
]);
|
|
1997
|
+
|
|
1998
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1999
|
+
expect(formStub.getFieldValue(['roles', 0, 'title'])).toBe('');
|
|
2000
|
+
});
|
|
2001
|
+
|
|
1709
2002
|
it('keeps default-disabled for edited to-many leaf when onValuesChange only provides top-level path', async () => {
|
|
1710
2003
|
const engineEmitter = new EventEmitter();
|
|
1711
2004
|
const blockEmitter = new EventEmitter();
|
|
@@ -82,8 +82,9 @@ describe('SubTableColumnModel (nested subform)', () => {
|
|
|
82
82
|
parentFieldIndex: ['users:0'],
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
const rowFork = column.
|
|
86
|
-
expect(rowFork
|
|
85
|
+
const [rowFork] = Array.from(column.forks ?? []);
|
|
86
|
+
expect(rowFork).toBeDefined();
|
|
87
|
+
expect((rowFork as any)?.context?.fieldIndex).toEqual(['users:0', 'roles:1']);
|
|
87
88
|
|
|
88
89
|
runtime.syncAssignRules([
|
|
89
90
|
{
|
|
@@ -17,6 +17,7 @@ import { namePathToPathKey, parsePathString, pathKeyToNamePath } from './path';
|
|
|
17
17
|
import type { FormAssignRuleItem, FormValueWriteMeta, NamePath, Patch, SetOptions, ValueSource } from './types';
|
|
18
18
|
import { createTxId, isEmptyValue } from './utils';
|
|
19
19
|
import { isToManyAssociationField } from '../../../../internal/utils/modelUtils';
|
|
20
|
+
import { getSubTableRowIdentity } from '../../../fields/AssociationFieldModel/SubTableFieldModel/rowIdentity';
|
|
20
21
|
|
|
21
22
|
/** Symbol to indicate rule value resolution should be skipped */
|
|
22
23
|
const SKIP_RULE_VALUE = Symbol('SKIP_RULE_VALUE');
|
|
@@ -51,6 +52,7 @@ type RuntimeRule = {
|
|
|
51
52
|
|
|
52
53
|
type ObservableBinding = {
|
|
53
54
|
source: ValueSource;
|
|
55
|
+
pathKey: string;
|
|
54
56
|
dispose: () => void;
|
|
55
57
|
};
|
|
56
58
|
|
|
@@ -1172,7 +1174,6 @@ export class RuleEngine {
|
|
|
1172
1174
|
|
|
1173
1175
|
const ruleContext = this.prepareRuleContext(rule);
|
|
1174
1176
|
const { baseCtx, targetNamePath, targetKey, clearDeps, disposeBinding } = ruleContext;
|
|
1175
|
-
|
|
1176
1177
|
if (!this.shouldRunRule(rule, targetNamePath, targetKey, baseCtx)) {
|
|
1177
1178
|
clearDeps(state);
|
|
1178
1179
|
disposeBinding();
|
|
@@ -1487,18 +1488,59 @@ export class RuleEngine {
|
|
|
1487
1488
|
// 编辑态默认值规则:
|
|
1488
1489
|
// - 顶层编辑表单:不应用默认值
|
|
1489
1490
|
// - 子表单:仅对“新增行/新增对象”(__is_new__ = true)应用默认值
|
|
1490
|
-
|
|
1491
|
+
return this.getItemContext(baseCtx)?.__is_new__ === true;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
private isStaleToManyItemContext(baseCtx: any): boolean {
|
|
1495
|
+
const item = this.getItemContext(baseCtx);
|
|
1496
|
+
if (!Number.isFinite(item?.index) || !Number.isFinite(item?.length)) return false;
|
|
1497
|
+
return item.index < 0 || item.index >= item.length;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
private getItemContext(baseCtx: any) {
|
|
1491
1501
|
try {
|
|
1492
|
-
|
|
1502
|
+
return baseCtx?.item;
|
|
1493
1503
|
} catch {
|
|
1494
|
-
|
|
1504
|
+
return undefined;
|
|
1495
1505
|
}
|
|
1506
|
+
}
|
|
1496
1507
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1508
|
+
private getRowTargetKey(baseCtx: any, rowPath: NamePath): string | string[] {
|
|
1509
|
+
let collection = this.getRootCollection() || this.getCollectionFromContext(baseCtx);
|
|
1510
|
+
let field: any;
|
|
1511
|
+
for (const seg of rowPath) {
|
|
1512
|
+
if (typeof seg === 'number') continue;
|
|
1513
|
+
if (typeof seg !== 'string' || !seg || !collection?.getField) break;
|
|
1514
|
+
|
|
1515
|
+
field = collection?.getField?.(seg);
|
|
1516
|
+
if (!field?.isAssociationField?.()) break;
|
|
1517
|
+
collection = field?.targetCollection;
|
|
1499
1518
|
}
|
|
1500
1519
|
|
|
1501
|
-
|
|
1520
|
+
const raw = field?.targetCollection?.filterTargetKey ?? field?.targetCollection?.filterByTk ?? field?.targetKey;
|
|
1521
|
+
if (Array.isArray(raw)) {
|
|
1522
|
+
const keys = raw.filter((key): key is string => typeof key === 'string' && !!key);
|
|
1523
|
+
return keys.length ? keys : 'id';
|
|
1524
|
+
}
|
|
1525
|
+
return typeof raw === 'string' && raw ? raw : 'id';
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
private isMismatchedToManyItemContext(baseCtx: any, targetNamePath: NamePath): boolean {
|
|
1529
|
+
const item = this.getItemContext(baseCtx);
|
|
1530
|
+
if (!item) return false;
|
|
1531
|
+
|
|
1532
|
+
for (let i = targetNamePath.length - 1; i >= 0; i--) {
|
|
1533
|
+
if (typeof targetNamePath[i] !== 'number') continue;
|
|
1534
|
+
|
|
1535
|
+
const rowPath = targetNamePath.slice(0, i + 1);
|
|
1536
|
+
const targetKey = this.getRowTargetKey(baseCtx, rowPath);
|
|
1537
|
+
const currentRow = this.options.getFormValueAtPath(rowPath);
|
|
1538
|
+
const currentIdentity = getSubTableRowIdentity(currentRow, targetKey);
|
|
1539
|
+
const itemIdentity = getSubTableRowIdentity(item.value, targetKey);
|
|
1540
|
+
return !!currentIdentity && !!itemIdentity && currentIdentity !== itemIdentity;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
return false;
|
|
1502
1544
|
}
|
|
1503
1545
|
|
|
1504
1546
|
private shouldRunRule(
|
|
@@ -1509,6 +1551,8 @@ export class RuleEngine {
|
|
|
1509
1551
|
): boolean {
|
|
1510
1552
|
if (!rule.getEnabled()) return false;
|
|
1511
1553
|
if (!targetNamePath || !targetKey) return false;
|
|
1554
|
+
if (this.isStaleToManyItemContext(baseCtx)) return false;
|
|
1555
|
+
if (this.isMismatchedToManyItemContext(baseCtx, targetNamePath)) return false;
|
|
1512
1556
|
if (rule.source === 'default') {
|
|
1513
1557
|
if (!this.shouldApplyDefaultRuleInCurrentState(baseCtx)) return false;
|
|
1514
1558
|
if (this.options.findExplicitHit(targetKey)) return false;
|
|
@@ -1808,6 +1852,19 @@ export class RuleEngine {
|
|
|
1808
1852
|
}
|
|
1809
1853
|
})();
|
|
1810
1854
|
|
|
1855
|
+
// Row/grid rules resolve target paths through fieldIndex (for example `roles.title`
|
|
1856
|
+
// -> `roles[1].title`). When a row is deleted or reordered, the rule must reschedule
|
|
1857
|
+
// even if its value/condition does not reference ctx.item directly.
|
|
1858
|
+
const fieldIndex = baseCtx?.model?.context?.fieldIndex ?? baseCtx?.fieldIndex;
|
|
1859
|
+
const shouldWatchFieldIndex = Array.isArray(fieldIndex) && fieldIndex.some((it) => typeof it === 'string');
|
|
1860
|
+
if (shouldWatchFieldIndex) {
|
|
1861
|
+
const fieldIndexDisposer = reaction(
|
|
1862
|
+
() => this.getFieldIndexSignature(baseCtx),
|
|
1863
|
+
() => this.scheduleRule(rule.id),
|
|
1864
|
+
);
|
|
1865
|
+
state.depDisposers.push(fieldIndexDisposer);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1811
1868
|
for (const depKey of deps) {
|
|
1812
1869
|
if (depKey === 'fv:*') {
|
|
1813
1870
|
continue;
|
|
@@ -1841,14 +1898,9 @@ export class RuleEngine {
|
|
|
1841
1898
|
const subPath = sep >= 0 ? rest.slice(sep + 1) : '';
|
|
1842
1899
|
const depPath = subPath ? (parsePathString(subPath).filter((seg) => typeof seg !== 'object') as NamePath) : [];
|
|
1843
1900
|
|
|
1844
|
-
// 特殊变量:item 为 RuleEngine 注入的计算属性(不直接存在于 baseCtx
|
|
1901
|
+
// 特殊变量:item 为 RuleEngine 注入的计算属性(不直接存在于 baseCtx 上)。
|
|
1902
|
+
// fieldIndex 的变化已统一在上面监听,这里只补 item 自身取值的依赖。
|
|
1845
1903
|
if (varName === 'item') {
|
|
1846
|
-
const fieldIndexDisposer = reaction(
|
|
1847
|
-
() => this.getFieldIndexSignature(baseCtx),
|
|
1848
|
-
() => this.scheduleRule(rule.id),
|
|
1849
|
-
);
|
|
1850
|
-
state.depDisposers.push(fieldIndexDisposer);
|
|
1851
|
-
|
|
1852
1904
|
if (depPath.length) {
|
|
1853
1905
|
const trackingFormValues = this.options.createTrackingFormValues({ deps: new Set(), wildcard: false });
|
|
1854
1906
|
const itemValueDisposer = reaction(
|