@nocobase/client-v2 2.2.0-beta.15 → 2.2.0-beta.17
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/components/form/ScanInput/useCodeScanner.d.ts +2 -2
- package/es/flow/actions/linkageRules.d.ts +25 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +4 -30
- package/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
- package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
- package/es/flow/components/field-value-variable/index.d.ts +11 -0
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
- package/es/flow/models/base/ActionModelCore.d.ts +4 -2
- package/es/flow/models/base/BlockGridModel.d.ts +3 -0
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +6 -0
- package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
- package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/index.d.ts +1 -0
- package/es/index.mjs +105 -111
- package/es/ui-operation/index.d.ts +15 -0
- package/es/ui-operation/ui-operation-codec.d.ts +10 -0
- package/lib/index.js +108 -114
- package/lib/locale/languageCodes.js +1 -0
- package/package.json +8 -7
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
- package/src/components/form/ScanInput/CodeScanner.tsx +61 -34
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +114 -18
- package/src/components/form/ScanInput/useCodeScanner.ts +166 -23
- package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
- package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
- package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/linkageRules.tsx +51 -13
- package/src/flow/common/Markdown/style.ts +4 -0
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/components/FieldAssignValueInput.tsx +38 -621
- package/src/flow/components/FlowRoute.tsx +71 -29
- package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
- package/src/flow/components/field-value-variable/dateValue.ts +223 -0
- package/src/flow/components/field-value-variable/index.ts +12 -0
- package/src/flow/index.ts +2 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
- package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
- package/src/flow/models/base/ActionModelCore.tsx +11 -2
- package/src/flow/models/base/BlockGridModel.tsx +26 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +15 -2
- package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +60 -0
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
- package/src/flow/models/blocks/form/FormBlockModel.tsx +4 -0
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
- package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
- package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +3 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +46 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
- package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
- package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +1 -0
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +20 -2
- package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
- package/src/index.ts +1 -0
- package/src/layout-manager/LayoutContentRoute.tsx +2 -1
- package/src/layout-manager/LayoutRoute.tsx +2 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
- package/src/locale/languageCodes.ts +1 -0
- package/src/ui-operation/index.ts +33 -0
- package/src/ui-operation/ui-operation-codec.ts +54 -0
|
@@ -0,0 +1,314 @@
|
|
|
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 { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { ActionModel as ConfiguredActionModel } from '../../models/base/ActionModel';
|
|
13
|
+
import { ActionModel } from '../../models/base/ActionModelCore';
|
|
14
|
+
import { actionLinkageRules, linkageSetActionProps, updateLinkageRules } from '../linkageRules';
|
|
15
|
+
|
|
16
|
+
class TestActionModel extends ActionModel {}
|
|
17
|
+
|
|
18
|
+
describe('action linkage rules on action forks', () => {
|
|
19
|
+
it('does not snapshot unrelated master props when disabling an action', async () => {
|
|
20
|
+
const engine = new FlowEngine();
|
|
21
|
+
engine.registerModels({ TestActionModel });
|
|
22
|
+
const master = engine.createModel<TestActionModel>({
|
|
23
|
+
use: 'TestActionModel',
|
|
24
|
+
props: { title: 'Edit' },
|
|
25
|
+
});
|
|
26
|
+
const fork = master.createFork({ className: 'row-action' });
|
|
27
|
+
|
|
28
|
+
const ctx = {
|
|
29
|
+
flowKey: 'buttonSettings',
|
|
30
|
+
model: fork,
|
|
31
|
+
app: {
|
|
32
|
+
jsonLogic: {
|
|
33
|
+
apply: vi.fn(() => true),
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
t: (value: string) => value,
|
|
37
|
+
resolveJsonTemplate: vi.fn(async (value: unknown) => value),
|
|
38
|
+
getAction: (name: string) => {
|
|
39
|
+
if (name !== 'linkageSetActionProps') return undefined;
|
|
40
|
+
return {
|
|
41
|
+
handler: async (_ctx: unknown, params: { setProps: Function }) => {
|
|
42
|
+
params.setProps(fork, { disabled: true });
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
} as never;
|
|
47
|
+
|
|
48
|
+
await actionLinkageRules.handler(ctx, {
|
|
49
|
+
value: [
|
|
50
|
+
{
|
|
51
|
+
key: 'disable-edit',
|
|
52
|
+
enable: true,
|
|
53
|
+
condition: { logic: '$and', items: [] },
|
|
54
|
+
actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(fork.localProps.title).toBeUndefined();
|
|
60
|
+
expect(fork.__originalProps.title).toBeUndefined();
|
|
61
|
+
expect(fork.localProps.disabled).toBe(true);
|
|
62
|
+
|
|
63
|
+
master.setProps('title', 'Updated');
|
|
64
|
+
await actionLinkageRules.handler(ctx, {
|
|
65
|
+
value: [
|
|
66
|
+
{
|
|
67
|
+
key: 'disable-edit',
|
|
68
|
+
enable: true,
|
|
69
|
+
condition: { logic: '$and', items: [] },
|
|
70
|
+
actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(fork.getProps().title).toBe('Updated');
|
|
76
|
+
expect(fork.serialize().props).toMatchObject({ title: 'Updated' });
|
|
77
|
+
expect(fork.serialize().props).not.toHaveProperty('disabled');
|
|
78
|
+
|
|
79
|
+
const refreshedFork = master.createFork({ className: 'row-action' });
|
|
80
|
+
expect(refreshedFork.getProps().title).toBe('Updated');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it.each(['disable', 'delete'] as const)(
|
|
84
|
+
'restores a row action when linkage rules are %sd without persisting disabled',
|
|
85
|
+
async (change) => {
|
|
86
|
+
const engine = new FlowEngine();
|
|
87
|
+
engine.registerModels({ ConfiguredActionModel });
|
|
88
|
+
engine.registerActions({ actionLinkageRules, linkageSetActionProps });
|
|
89
|
+
|
|
90
|
+
let savedData: ReturnType<ConfiguredActionModel['serialize']> | undefined;
|
|
91
|
+
engine.setModelRepository({
|
|
92
|
+
save: async (model) => {
|
|
93
|
+
savedData = model.serialize();
|
|
94
|
+
return savedData;
|
|
95
|
+
},
|
|
96
|
+
} as Parameters<FlowEngine['setModelRepository']>[0]);
|
|
97
|
+
|
|
98
|
+
const enabledRules = [
|
|
99
|
+
{
|
|
100
|
+
key: 'disable-edit',
|
|
101
|
+
enable: true,
|
|
102
|
+
condition: { logic: '$and', items: [] },
|
|
103
|
+
actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
const master = engine.createModel<ConfiguredActionModel>({
|
|
107
|
+
use: 'ConfiguredActionModel',
|
|
108
|
+
props: { title: 'Edit' },
|
|
109
|
+
stepParams: {
|
|
110
|
+
buttonSettings: {
|
|
111
|
+
linkageRules: { value: enabledRules },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const fork = master.createFork({ className: 'row-action' });
|
|
116
|
+
|
|
117
|
+
await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
118
|
+
expect(fork.getProps().disabled).toBe(true);
|
|
119
|
+
|
|
120
|
+
const nextRules =
|
|
121
|
+
change === 'disable'
|
|
122
|
+
? updateLinkageRules(enabledRules, (rules) => {
|
|
123
|
+
rules[0].enable = false;
|
|
124
|
+
})
|
|
125
|
+
: [];
|
|
126
|
+
fork.setStepParams('buttonSettings', 'linkageRules', { value: nextRules });
|
|
127
|
+
await fork.saveStepParams();
|
|
128
|
+
await fork.rerender();
|
|
129
|
+
|
|
130
|
+
expect(savedData).toBeDefined();
|
|
131
|
+
expect(savedData?.props).not.toHaveProperty('disabled');
|
|
132
|
+
expect(savedData?.stepParams.buttonSettings.linkageRules.value).toEqual(nextRules);
|
|
133
|
+
expect(fork.getProps().disabled).toBeUndefined();
|
|
134
|
+
|
|
135
|
+
const reloadedEngine = new FlowEngine();
|
|
136
|
+
reloadedEngine.registerModels({ ConfiguredActionModel });
|
|
137
|
+
reloadedEngine.registerActions({ actionLinkageRules, linkageSetActionProps });
|
|
138
|
+
const reloadedMaster = reloadedEngine.createModel<ConfiguredActionModel>({
|
|
139
|
+
use: 'ConfiguredActionModel',
|
|
140
|
+
props: savedData?.props,
|
|
141
|
+
stepParams: savedData?.stepParams,
|
|
142
|
+
});
|
|
143
|
+
const reloadedFork = reloadedMaster.createFork({ className: 'row-action' });
|
|
144
|
+
|
|
145
|
+
await reloadedFork.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
146
|
+
expect(reloadedFork.getProps().disabled).toBeUndefined();
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
it.each(['disabled', 'deleted'] as const)(
|
|
151
|
+
'ignores a historical disabled prop after the linkage rule is %s',
|
|
152
|
+
async (state) => {
|
|
153
|
+
const engine = new FlowEngine();
|
|
154
|
+
engine.registerModels({ ConfiguredActionModel });
|
|
155
|
+
engine.registerActions({ actionLinkageRules, linkageSetActionProps });
|
|
156
|
+
const rules =
|
|
157
|
+
state === 'disabled'
|
|
158
|
+
? [
|
|
159
|
+
{
|
|
160
|
+
key: 'disable-edit',
|
|
161
|
+
enable: false,
|
|
162
|
+
condition: { logic: '$and', items: [] },
|
|
163
|
+
actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
|
|
164
|
+
},
|
|
165
|
+
]
|
|
166
|
+
: [];
|
|
167
|
+
const master = engine.createModel<ConfiguredActionModel>({
|
|
168
|
+
use: 'ConfiguredActionModel',
|
|
169
|
+
props: { title: 'Edit', disabled: true },
|
|
170
|
+
stepParams: {
|
|
171
|
+
buttonSettings: {
|
|
172
|
+
linkageRules: { value: rules },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
const fork = master.createFork({ className: 'row-action' });
|
|
177
|
+
|
|
178
|
+
expect(master.getProps()).not.toHaveProperty('disabled');
|
|
179
|
+
await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
180
|
+
|
|
181
|
+
expect(fork.getProps().disabled).toBeUndefined();
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
it('still applies an enabled linkage rule after dropping a historical disabled prop', async () => {
|
|
186
|
+
const engine = new FlowEngine();
|
|
187
|
+
engine.registerModels({ ConfiguredActionModel });
|
|
188
|
+
engine.registerActions({ actionLinkageRules, linkageSetActionProps });
|
|
189
|
+
const master = engine.createModel<ConfiguredActionModel>({
|
|
190
|
+
use: 'ConfiguredActionModel',
|
|
191
|
+
props: { title: 'Edit', disabled: true },
|
|
192
|
+
stepParams: {
|
|
193
|
+
buttonSettings: {
|
|
194
|
+
linkageRules: {
|
|
195
|
+
value: [
|
|
196
|
+
{
|
|
197
|
+
key: 'disable-edit',
|
|
198
|
+
enable: true,
|
|
199
|
+
condition: { logic: '$and', items: [] },
|
|
200
|
+
actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
const fork = master.createFork({ className: 'row-action' });
|
|
208
|
+
|
|
209
|
+
expect(master.getProps()).not.toHaveProperty('disabled');
|
|
210
|
+
await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
211
|
+
|
|
212
|
+
expect(fork.getProps().disabled).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('preserves a default disabled state as runtime-only state', async () => {
|
|
216
|
+
class DefaultDisabledActionModel extends ConfiguredActionModel {
|
|
217
|
+
defaultProps = { ...this.defaultProps, disabled: true };
|
|
218
|
+
}
|
|
219
|
+
const engine = new FlowEngine();
|
|
220
|
+
engine.registerModels({ DefaultDisabledActionModel });
|
|
221
|
+
engine.registerActions({ actionLinkageRules, linkageSetActionProps });
|
|
222
|
+
const master = engine.createModel<DefaultDisabledActionModel>({
|
|
223
|
+
use: 'DefaultDisabledActionModel',
|
|
224
|
+
props: { title: 'Edit' },
|
|
225
|
+
stepParams: {
|
|
226
|
+
buttonSettings: {
|
|
227
|
+
linkageRules: { value: [] },
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
const fork = master.createFork({ className: 'row-action' });
|
|
232
|
+
|
|
233
|
+
await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
234
|
+
|
|
235
|
+
expect(fork.getProps().disabled).toBe(true);
|
|
236
|
+
expect(fork.serialize().props).not.toHaveProperty('disabled');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('updates a disabled row action title through the real beforeRender flow', async () => {
|
|
240
|
+
const engine = new FlowEngine();
|
|
241
|
+
engine.registerModels({ ConfiguredActionModel });
|
|
242
|
+
engine.registerActions({ actionLinkageRules, linkageSetActionProps });
|
|
243
|
+
let savedData: ReturnType<ConfiguredActionModel['serialize']> | undefined;
|
|
244
|
+
engine.setModelRepository({
|
|
245
|
+
save: async (model) => {
|
|
246
|
+
savedData = model.serialize();
|
|
247
|
+
return savedData;
|
|
248
|
+
},
|
|
249
|
+
} as Parameters<FlowEngine['setModelRepository']>[0]);
|
|
250
|
+
const master = engine.createModel<ConfiguredActionModel>({
|
|
251
|
+
use: 'ConfiguredActionModel',
|
|
252
|
+
props: { title: 'Edit' },
|
|
253
|
+
stepParams: {
|
|
254
|
+
buttonSettings: {
|
|
255
|
+
general: { title: 'Edit' },
|
|
256
|
+
linkageRules: {
|
|
257
|
+
value: [
|
|
258
|
+
{
|
|
259
|
+
key: 'disable-edit',
|
|
260
|
+
enable: true,
|
|
261
|
+
condition: { logic: '$and', items: [] },
|
|
262
|
+
actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
const fork = master.createFork({ className: 'row-action' });
|
|
270
|
+
|
|
271
|
+
await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
272
|
+
expect(fork.getProps()).toMatchObject({ title: 'Edit', disabled: true });
|
|
273
|
+
|
|
274
|
+
fork.setStepParams('buttonSettings', 'general', { title: 'Updated' });
|
|
275
|
+
await fork.saveStepParams();
|
|
276
|
+
await fork.rerender();
|
|
277
|
+
|
|
278
|
+
expect(savedData?.props).not.toHaveProperty('disabled');
|
|
279
|
+
expect(savedData?.stepParams.buttonSettings.general.title).toBe('Updated');
|
|
280
|
+
expect(fork.getProps()).toMatchObject({ title: 'Updated', disabled: true });
|
|
281
|
+
|
|
282
|
+
const reloadedEngine = new FlowEngine();
|
|
283
|
+
reloadedEngine.registerModels({ ConfiguredActionModel });
|
|
284
|
+
reloadedEngine.registerActions({ actionLinkageRules, linkageSetActionProps });
|
|
285
|
+
const reloadedMaster = reloadedEngine.createModel<ConfiguredActionModel>({
|
|
286
|
+
use: 'ConfiguredActionModel',
|
|
287
|
+
props: savedData?.props,
|
|
288
|
+
stepParams: savedData?.stepParams,
|
|
289
|
+
});
|
|
290
|
+
const reloadedFork = reloadedMaster.createFork({ className: 'row-action' });
|
|
291
|
+
|
|
292
|
+
await reloadedFork.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
293
|
+
expect(reloadedFork.getProps()).toMatchObject({ title: 'Updated', disabled: true });
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('creates a new rules value when disabling a linkage rule', () => {
|
|
297
|
+
const rules = [
|
|
298
|
+
{
|
|
299
|
+
key: 'disable-edit',
|
|
300
|
+
title: 'Linkage rule',
|
|
301
|
+
enable: true,
|
|
302
|
+
condition: { logic: '$and', items: [] },
|
|
303
|
+
actions: [],
|
|
304
|
+
},
|
|
305
|
+
];
|
|
306
|
+
const nextRules = updateLinkageRules(rules, (next) => {
|
|
307
|
+
next[0].enable = false;
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
expect(nextRules).toEqual([expect.objectContaining({ key: 'disable-edit', enable: false })]);
|
|
311
|
+
expect(nextRules).not.toBe(rules);
|
|
312
|
+
expect(rules[0].enable).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -225,6 +225,108 @@ describe('ensureFormValueDrivenDataScopeClear', () => {
|
|
|
225
225
|
expect(onChange).toHaveBeenCalledWith(null);
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
+
it('does not clear a popup form field when data scope depends on the external parent item', () => {
|
|
229
|
+
const emitter = new EventEmitter();
|
|
230
|
+
const formBlock = {
|
|
231
|
+
uid: 'form-1',
|
|
232
|
+
disposed: false,
|
|
233
|
+
emitter,
|
|
234
|
+
context: {
|
|
235
|
+
form: {},
|
|
236
|
+
formValues: { staff_m2o: null },
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const onChange = vi.fn();
|
|
241
|
+
const model: any = {
|
|
242
|
+
disposed: false,
|
|
243
|
+
props: {
|
|
244
|
+
value: { id: 10 },
|
|
245
|
+
onChange,
|
|
246
|
+
},
|
|
247
|
+
context: {
|
|
248
|
+
blockModel: formBlock,
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const ctx: any = {
|
|
253
|
+
model,
|
|
254
|
+
flowKey: 'selectSettings',
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const filter = {
|
|
258
|
+
logic: '$and',
|
|
259
|
+
items: [{ path: 'orgId', operator: '$eq', value: '{{ ctx.item.parentItem.value.org_m2o.id }}' }],
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
ensureFormValueDrivenDataScopeClear(ctx, filter);
|
|
263
|
+
|
|
264
|
+
emitter.emit('formValuesChange', {
|
|
265
|
+
changedPaths: [['staff_m2o']],
|
|
266
|
+
allValues: { staff_m2o: { id: 10 } },
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('maps popup current item dependencies to the popup form root', () => {
|
|
273
|
+
const emitter = new EventEmitter();
|
|
274
|
+
const formBlock = {
|
|
275
|
+
uid: 'form-1',
|
|
276
|
+
disposed: false,
|
|
277
|
+
emitter,
|
|
278
|
+
context: {
|
|
279
|
+
form: {},
|
|
280
|
+
formValues: {
|
|
281
|
+
org_m2o: { id: 1 },
|
|
282
|
+
staff_m2o: { id: 10 },
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const onChange = vi.fn();
|
|
288
|
+
const model: any = {
|
|
289
|
+
disposed: false,
|
|
290
|
+
props: {
|
|
291
|
+
value: { id: 10 },
|
|
292
|
+
onChange,
|
|
293
|
+
},
|
|
294
|
+
context: {
|
|
295
|
+
blockModel: formBlock,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const ctx: any = {
|
|
300
|
+
model,
|
|
301
|
+
flowKey: 'selectSettings',
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const filter = {
|
|
305
|
+
logic: '$and',
|
|
306
|
+
items: [{ path: 'orgId', operator: '$eq', value: '{{ ctx.item.value.org_m2o.id }}' }],
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
ensureFormValueDrivenDataScopeClear(ctx, filter);
|
|
310
|
+
|
|
311
|
+
emitter.emit('formValuesChange', {
|
|
312
|
+
changedPaths: [['staff_m2o']],
|
|
313
|
+
allValues: {
|
|
314
|
+
org_m2o: { id: 1 },
|
|
315
|
+
staff_m2o: { id: 10 },
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
319
|
+
|
|
320
|
+
emitter.emit('formValuesChange', {
|
|
321
|
+
changedPaths: [['org_m2o']],
|
|
322
|
+
allValues: {
|
|
323
|
+
org_m2o: { id: 2 },
|
|
324
|
+
staff_m2o: { id: 10 },
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
expect(onChange).toHaveBeenCalledWith(null);
|
|
328
|
+
});
|
|
329
|
+
|
|
228
330
|
it('does not clear a row field when a sibling field changes but the item dependency value is unchanged', () => {
|
|
229
331
|
const emitter = new EventEmitter();
|
|
230
332
|
const formBlock = {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import { getActionLinkageStateOptions } from '../linkageRules';
|
|
12
|
+
|
|
13
|
+
describe('getActionLinkageStateOptions', () => {
|
|
14
|
+
const t = (key: string) => key;
|
|
15
|
+
|
|
16
|
+
it('returns all states for regular action models', () => {
|
|
17
|
+
expect(getActionLinkageStateOptions({}, t).map((option) => option.value)).toEqual([
|
|
18
|
+
'visible',
|
|
19
|
+
'hidden',
|
|
20
|
+
'hiddenText',
|
|
21
|
+
'enabled',
|
|
22
|
+
'disabled',
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('filters states declared unsupported by the action model', () => {
|
|
27
|
+
expect(
|
|
28
|
+
getActionLinkageStateOptions({ supportedActionLinkageStates: ['visible', 'hidden'] }, t).map(
|
|
29
|
+
(option) => option.value,
|
|
30
|
+
),
|
|
31
|
+
).toEqual(['visible', 'hidden']);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -68,6 +68,12 @@ interface LinkageRule {
|
|
|
68
68
|
}[];
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export function updateLinkageRules<T>(rules: T[], updater: (nextRules: T[]) => void): T[] {
|
|
72
|
+
const nextRules = _.cloneDeep(rules);
|
|
73
|
+
updater(nextRules);
|
|
74
|
+
return nextRules;
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
const previewValueForLog = (value: any) => {
|
|
72
78
|
if (value == null) return value;
|
|
73
79
|
const t = typeof value;
|
|
@@ -654,6 +660,30 @@ export const linkageSetBlockProps = defineAction({
|
|
|
654
660
|
},
|
|
655
661
|
});
|
|
656
662
|
|
|
663
|
+
const ACTION_LINKAGE_STATE_OPTIONS = [
|
|
664
|
+
{ label: 'Visible', value: 'visible' },
|
|
665
|
+
{ label: 'Hidden', value: 'hidden' },
|
|
666
|
+
{ label: 'Hidden text', value: 'hiddenText' },
|
|
667
|
+
{ label: 'Enabled', value: 'enabled' },
|
|
668
|
+
{ label: 'Disabled', value: 'disabled' },
|
|
669
|
+
] as const;
|
|
670
|
+
|
|
671
|
+
type ActionLinkageState = (typeof ACTION_LINKAGE_STATE_OPTIONS)[number]['value'];
|
|
672
|
+
|
|
673
|
+
export function getActionLinkageStateOptions(
|
|
674
|
+
model: { supportedActionLinkageStates?: readonly ActionLinkageState[] },
|
|
675
|
+
t: (key: string) => string,
|
|
676
|
+
) {
|
|
677
|
+
const supportedStates = model.supportedActionLinkageStates ? new Set(model.supportedActionLinkageStates) : undefined;
|
|
678
|
+
|
|
679
|
+
return ACTION_LINKAGE_STATE_OPTIONS.filter((option) => !supportedStates || supportedStates.has(option.value)).map(
|
|
680
|
+
(option) => ({
|
|
681
|
+
label: t(option.label),
|
|
682
|
+
value: option.value,
|
|
683
|
+
}),
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
657
687
|
export const linkageSetActionProps = defineAction({
|
|
658
688
|
name: 'linkageSetActionProps',
|
|
659
689
|
title: tExpr('Set button state'),
|
|
@@ -674,13 +704,7 @@ export const linkageSetActionProps = defineAction({
|
|
|
674
704
|
onChange={onChange}
|
|
675
705
|
placeholder={t('Please select state')}
|
|
676
706
|
style={{ width: '100%' }}
|
|
677
|
-
options={
|
|
678
|
-
{ label: t('Visible'), value: 'visible' },
|
|
679
|
-
{ label: t('Hidden'), value: 'hidden' },
|
|
680
|
-
{ label: t('Hidden text'), value: 'hiddenText' },
|
|
681
|
-
{ label: t('Enabled'), value: 'enabled' },
|
|
682
|
-
{ label: t('Disabled'), value: 'disabled' },
|
|
683
|
-
]}
|
|
707
|
+
options={getActionLinkageStateOptions(ctx.model, t)}
|
|
684
708
|
allowClear
|
|
685
709
|
/>
|
|
686
710
|
);
|
|
@@ -1500,13 +1524,26 @@ async function resolveLinkageRulesParamsPreservingRunJsScripts(ctx: FlowContext,
|
|
|
1500
1524
|
}
|
|
1501
1525
|
|
|
1502
1526
|
const LinkageRulesUI = observer(
|
|
1503
|
-
(props: {
|
|
1504
|
-
|
|
1527
|
+
(props: {
|
|
1528
|
+
readonly value: LinkageRule[];
|
|
1529
|
+
onChange?: (value: LinkageRule[]) => void;
|
|
1530
|
+
supportedActions: string[];
|
|
1531
|
+
title?: string;
|
|
1532
|
+
}) => {
|
|
1533
|
+
const { value: rules = [], onChange, supportedActions } = props;
|
|
1505
1534
|
const ctx = useFlowContext();
|
|
1506
1535
|
const flowEngine = useFlowEngine();
|
|
1507
1536
|
const t = ctx.model.translate.bind(ctx.model);
|
|
1508
1537
|
const assignPriorityTip = t('Assignment takes precedence over form field assignment');
|
|
1509
1538
|
|
|
1539
|
+
const replaceRules = (updater: (nextRules: LinkageRule[]) => void) => {
|
|
1540
|
+
if (onChange) {
|
|
1541
|
+
onChange(updateLinkageRules(rules, updater));
|
|
1542
|
+
} else {
|
|
1543
|
+
updater(rules);
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1510
1547
|
// 创建新规则的默认值
|
|
1511
1548
|
const createNewRule = (): LinkageRule => ({
|
|
1512
1549
|
key: uid(),
|
|
@@ -1523,7 +1560,7 @@ const LinkageRulesUI = observer(
|
|
|
1523
1560
|
|
|
1524
1561
|
// 删除规则
|
|
1525
1562
|
const handleDeleteRule = (index: number) => {
|
|
1526
|
-
|
|
1563
|
+
replaceRules((nextRules) => nextRules.splice(index, 1));
|
|
1527
1564
|
};
|
|
1528
1565
|
|
|
1529
1566
|
// 上移规则
|
|
@@ -1562,7 +1599,9 @@ const LinkageRulesUI = observer(
|
|
|
1562
1599
|
|
|
1563
1600
|
// 切换规则启用状态
|
|
1564
1601
|
const handleToggleEnable = (index: number, enable: boolean) => {
|
|
1565
|
-
|
|
1602
|
+
replaceRules((nextRules) => {
|
|
1603
|
+
nextRules[index].enable = enable;
|
|
1604
|
+
});
|
|
1566
1605
|
};
|
|
1567
1606
|
|
|
1568
1607
|
// 获取可用的动作类型
|
|
@@ -2222,7 +2261,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2222
2261
|
}
|
|
2223
2262
|
|
|
2224
2263
|
rememberOriginalProp(key, model.props?.[key]);
|
|
2225
|
-
if (key === 'hiddenText') {
|
|
2264
|
+
if (key === 'hiddenText' && normalizedProps[key]) {
|
|
2226
2265
|
rememberOriginalProp('title', model.props?.title);
|
|
2227
2266
|
}
|
|
2228
2267
|
if (key === 'required') {
|
|
@@ -2286,7 +2325,6 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2286
2325
|
const newProps = { ...model.__originalProps, ...patchProps };
|
|
2287
2326
|
const prevHidden = !!model.hidden;
|
|
2288
2327
|
const nextHidden = !!newProps.hiddenModel;
|
|
2289
|
-
|
|
2290
2328
|
model.setProps(_.omit(newProps, ['hiddenModel', 'value', 'hiddenText', LINKAGE_ASSIGN_MODE_PROP]));
|
|
2291
2329
|
syncFieldOptionsToForks(model, patchProps);
|
|
2292
2330
|
if (typeof model.setHidden === 'function') {
|
|
@@ -26,6 +26,10 @@ export default function useStyle() {
|
|
|
26
26
|
() => ({
|
|
27
27
|
[`.${COMPONENT_CLS}`]: {
|
|
28
28
|
'.vditor-reset': { fontSize: `${token.fontSize}px !important`, color: 'unset' },
|
|
29
|
+
'.vditor-reset h2': {
|
|
30
|
+
borderBottom: 'none !important',
|
|
31
|
+
boxShadow: 'none !important',
|
|
32
|
+
},
|
|
29
33
|
'.vditor': {
|
|
30
34
|
borderRadius: 8,
|
|
31
35
|
},
|
|
@@ -150,7 +150,7 @@ export const BlockItemCard = React.forwardRef(
|
|
|
150
150
|
) => {
|
|
151
151
|
const { t } = useTranslation();
|
|
152
152
|
const { token } = theme.useToken();
|
|
153
|
-
const { title: blockTitle, description, children, className, heightMode, ...rest } = props;
|
|
153
|
+
const { title: blockTitle, description, children, className, heightMode, style, ...rest } = props;
|
|
154
154
|
const cardRef = useRef<HTMLDivElement | null>(null);
|
|
155
155
|
const setCardRef = useCallback(
|
|
156
156
|
(node: HTMLDivElement | null) => {
|
|
@@ -185,7 +185,7 @@ export const BlockItemCard = React.forwardRef(
|
|
|
185
185
|
<Card
|
|
186
186
|
ref={setCardRef as any}
|
|
187
187
|
title={title}
|
|
188
|
-
style={{ display: 'flex', flexDirection: 'column', height: height }}
|
|
188
|
+
style={{ display: 'flex', flexDirection: 'column', ...style, height: height ?? style?.height }}
|
|
189
189
|
styles={{
|
|
190
190
|
body: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'auto' },
|
|
191
191
|
header: {
|