@nocobase/client-v2 2.2.0-beta.15 → 2.2.0-beta.16
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 +1 -0
- package/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- 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/table/TableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/index.mjs +87 -84
- package/lib/index.js +94 -91
- 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/linkageRules.tsx +26 -6
- package/src/flow/common/Markdown/style.ts +4 -0
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/components/FlowRoute.tsx +71 -29
- package/src/flow/index.ts +2 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- 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 +6 -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/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/js-block/JSBlock.tsx +1 -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__/TableBlockModel.quickEditRefresh.test.ts +12 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -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/utils/dataScopeFormValueClear.ts +4 -3
- package/src/locale/languageCodes.ts +1 -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 = {
|
|
@@ -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;
|
|
@@ -1500,13 +1506,26 @@ async function resolveLinkageRulesParamsPreservingRunJsScripts(ctx: FlowContext,
|
|
|
1500
1506
|
}
|
|
1501
1507
|
|
|
1502
1508
|
const LinkageRulesUI = observer(
|
|
1503
|
-
(props: {
|
|
1504
|
-
|
|
1509
|
+
(props: {
|
|
1510
|
+
readonly value: LinkageRule[];
|
|
1511
|
+
onChange?: (value: LinkageRule[]) => void;
|
|
1512
|
+
supportedActions: string[];
|
|
1513
|
+
title?: string;
|
|
1514
|
+
}) => {
|
|
1515
|
+
const { value: rules = [], onChange, supportedActions } = props;
|
|
1505
1516
|
const ctx = useFlowContext();
|
|
1506
1517
|
const flowEngine = useFlowEngine();
|
|
1507
1518
|
const t = ctx.model.translate.bind(ctx.model);
|
|
1508
1519
|
const assignPriorityTip = t('Assignment takes precedence over form field assignment');
|
|
1509
1520
|
|
|
1521
|
+
const replaceRules = (updater: (nextRules: LinkageRule[]) => void) => {
|
|
1522
|
+
if (onChange) {
|
|
1523
|
+
onChange(updateLinkageRules(rules, updater));
|
|
1524
|
+
} else {
|
|
1525
|
+
updater(rules);
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
|
|
1510
1529
|
// 创建新规则的默认值
|
|
1511
1530
|
const createNewRule = (): LinkageRule => ({
|
|
1512
1531
|
key: uid(),
|
|
@@ -1523,7 +1542,7 @@ const LinkageRulesUI = observer(
|
|
|
1523
1542
|
|
|
1524
1543
|
// 删除规则
|
|
1525
1544
|
const handleDeleteRule = (index: number) => {
|
|
1526
|
-
|
|
1545
|
+
replaceRules((nextRules) => nextRules.splice(index, 1));
|
|
1527
1546
|
};
|
|
1528
1547
|
|
|
1529
1548
|
// 上移规则
|
|
@@ -1562,7 +1581,9 @@ const LinkageRulesUI = observer(
|
|
|
1562
1581
|
|
|
1563
1582
|
// 切换规则启用状态
|
|
1564
1583
|
const handleToggleEnable = (index: number, enable: boolean) => {
|
|
1565
|
-
|
|
1584
|
+
replaceRules((nextRules) => {
|
|
1585
|
+
nextRules[index].enable = enable;
|
|
1586
|
+
});
|
|
1566
1587
|
};
|
|
1567
1588
|
|
|
1568
1589
|
// 获取可用的动作类型
|
|
@@ -2222,7 +2243,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2222
2243
|
}
|
|
2223
2244
|
|
|
2224
2245
|
rememberOriginalProp(key, model.props?.[key]);
|
|
2225
|
-
if (key === 'hiddenText') {
|
|
2246
|
+
if (key === 'hiddenText' && normalizedProps[key]) {
|
|
2226
2247
|
rememberOriginalProp('title', model.props?.title);
|
|
2227
2248
|
}
|
|
2228
2249
|
if (key === 'required') {
|
|
@@ -2286,7 +2307,6 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2286
2307
|
const newProps = { ...model.__originalProps, ...patchProps };
|
|
2287
2308
|
const prevHidden = !!model.hidden;
|
|
2288
2309
|
const nextHidden = !!newProps.hiddenModel;
|
|
2289
|
-
|
|
2290
2310
|
model.setProps(_.omit(newProps, ['hiddenModel', 'value', 'hiddenText', LINKAGE_ASSIGN_MODE_PROP]));
|
|
2291
2311
|
syncFieldOptionsToForks(model, patchProps);
|
|
2292
2312
|
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: {
|
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { type FlowEngine, useFlowContext, useFlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import { Button, Result } from 'antd';
|
|
11
12
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
12
|
-
import {
|
|
13
|
+
import { useTranslation } from 'react-i18next';
|
|
13
14
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
15
|
+
import { getModernClientPrefix, stripModernClientPrefix } from '../../authRedirect';
|
|
14
16
|
import { useApp } from '../../hooks/useApp';
|
|
15
17
|
import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../flow-compat';
|
|
16
18
|
import {
|
|
@@ -22,12 +24,14 @@ import { getLayoutModel, type BaseLayoutModel } from '../admin-shell/BaseLayoutM
|
|
|
22
24
|
import { useLayoutRoutePage } from '../admin-shell/useLayoutRoutePage';
|
|
23
25
|
import { AppNotFound } from '../../components';
|
|
24
26
|
import { useKeepAlive } from '../../components/KeepAlive';
|
|
27
|
+
import { registerDeviceTypeContext } from '../internal/registerDeviceTypeContext';
|
|
25
28
|
|
|
26
29
|
type FlowRouteGuardState = {
|
|
27
30
|
pageUid?: string;
|
|
28
31
|
pending: boolean;
|
|
29
32
|
allowBridge: boolean;
|
|
30
33
|
notFound: boolean;
|
|
34
|
+
legacyPageUnsupported?: boolean;
|
|
31
35
|
};
|
|
32
36
|
|
|
33
37
|
export type LegacyPageBehavior = 'redirect' | 'notFound' | 'bridge';
|
|
@@ -156,31 +160,9 @@ const BridgeFlowRoute = ({
|
|
|
156
160
|
const layoutContentRef = useRef<HTMLDivElement>(null);
|
|
157
161
|
|
|
158
162
|
useEffect(() => {
|
|
159
|
-
flowEngine.context.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
meta: {
|
|
163
|
-
type: 'string',
|
|
164
|
-
title: flowEngine.translate('Current device type'),
|
|
165
|
-
interface: 'select',
|
|
166
|
-
uiSchema: {
|
|
167
|
-
enum: [
|
|
168
|
-
{ label: flowEngine.translate('Computer'), value: 'computer' },
|
|
169
|
-
{ label: flowEngine.translate('Mobile'), value: 'mobile' },
|
|
170
|
-
{ label: flowEngine.translate('Tablet'), value: 'tablet' },
|
|
171
|
-
{ label: flowEngine.translate('SmartTv'), value: 'smarttv' },
|
|
172
|
-
{ label: flowEngine.translate('Console'), value: 'console' },
|
|
173
|
-
{ label: flowEngine.translate('Wearable'), value: 'wearable' },
|
|
174
|
-
{ label: flowEngine.translate('Embedded'), value: 'embedded' },
|
|
175
|
-
],
|
|
176
|
-
'x-component': 'Select',
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
info: {
|
|
180
|
-
description: 'Current device type (computer/mobile/tablet/...).',
|
|
181
|
-
detail: 'string',
|
|
182
|
-
},
|
|
183
|
-
});
|
|
163
|
+
if (!flowEngine.context.getPropertyOptions('deviceType')) {
|
|
164
|
+
registerDeviceTypeContext(flowEngine);
|
|
165
|
+
}
|
|
184
166
|
}, [flowEngine]);
|
|
185
167
|
|
|
186
168
|
useLayoutRoutePage({
|
|
@@ -195,11 +177,58 @@ const BridgeFlowRoute = ({
|
|
|
195
177
|
return <div ref={layoutContentRef} />;
|
|
196
178
|
};
|
|
197
179
|
|
|
180
|
+
type RouteLocation = {
|
|
181
|
+
pathname: string;
|
|
182
|
+
search: string;
|
|
183
|
+
hash: string;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const getLegacyPageHref = (app: { getPublicPath: () => string }, location: RouteLocation) => {
|
|
187
|
+
const modernPublicPath = app.getPublicPath();
|
|
188
|
+
const browserLocationMatchesPublicPath = window.location.pathname.startsWith(modernPublicPath);
|
|
189
|
+
const currentLocation = browserLocationMatchesPublicPath ? window.location : location;
|
|
190
|
+
const pathWithinModernClient = currentLocation.pathname.startsWith(modernPublicPath)
|
|
191
|
+
? currentLocation.pathname.slice(modernPublicPath.length)
|
|
192
|
+
: currentLocation.pathname.replace(/^\/+/, '');
|
|
193
|
+
return `${stripModernClientPrefix(modernPublicPath)}${pathWithinModernClient}${currentLocation.search}${
|
|
194
|
+
currentLocation.hash
|
|
195
|
+
}`;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const LegacyPageUnsupported = ({
|
|
199
|
+
app,
|
|
200
|
+
location,
|
|
201
|
+
}: {
|
|
202
|
+
app: { getPublicPath: () => string };
|
|
203
|
+
location: RouteLocation;
|
|
204
|
+
}) => {
|
|
205
|
+
const { t } = useTranslation();
|
|
206
|
+
const modernClientPath = `/${getModernClientPrefix()}/`;
|
|
207
|
+
const withModernClientPath = (message: string) => message.replaceAll('{{modernClientPath}}', modernClientPath);
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<Result
|
|
211
|
+
status="warning"
|
|
212
|
+
title={withModernClientPath(t('This page is not supported in the {{modernClientPath}} branch'))}
|
|
213
|
+
subTitle={withModernClientPath(
|
|
214
|
+
t(
|
|
215
|
+
'The {{modernClientPath}} branch only supports new pages. This page is a legacy page. Please open it from the original entry.',
|
|
216
|
+
),
|
|
217
|
+
)}
|
|
218
|
+
extra={
|
|
219
|
+
<Button href={getLegacyPageHref(app, location)} type="primary">
|
|
220
|
+
{t('Open from the original entry')}
|
|
221
|
+
</Button>
|
|
222
|
+
}
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
};
|
|
226
|
+
|
|
198
227
|
/**
|
|
199
228
|
* 管理后台动态页面路由组件。
|
|
200
229
|
*
|
|
201
|
-
* 负责读取当前路由页面 UID
|
|
202
|
-
*
|
|
230
|
+
* 负责读取当前路由页面 UID,并把页面生命周期桥接到 AdminLayout host model。
|
|
231
|
+
* 设备变量通常由 PluginFlowEngine 共享初始化提供;独立渲染时会在挂载后补充注册。
|
|
203
232
|
*
|
|
204
233
|
* @example
|
|
205
234
|
* ```tsx
|
|
@@ -377,7 +406,13 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
377
406
|
|
|
378
407
|
if (target.reason === 'unsupportedV2Runtime') {
|
|
379
408
|
if (active && requestId === requestIdRef.current) {
|
|
380
|
-
setGuardState({
|
|
409
|
+
setGuardState({
|
|
410
|
+
pageUid,
|
|
411
|
+
pending: false,
|
|
412
|
+
allowBridge: false,
|
|
413
|
+
notFound: false,
|
|
414
|
+
legacyPageUnsupported: true,
|
|
415
|
+
});
|
|
381
416
|
}
|
|
382
417
|
return;
|
|
383
418
|
}
|
|
@@ -414,6 +449,10 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
414
449
|
return <AppNotFound />;
|
|
415
450
|
}
|
|
416
451
|
|
|
452
|
+
if (guardState.legacyPageUnsupported) {
|
|
453
|
+
return <LegacyPageUnsupported app={app} location={location} />;
|
|
454
|
+
}
|
|
455
|
+
|
|
417
456
|
if (!guardState.allowBridge) {
|
|
418
457
|
return null;
|
|
419
458
|
}
|
|
@@ -421,11 +460,14 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
421
460
|
return <BridgeFlowRoute pageUid={pageUid} active={active} getLayoutModel={getLayoutModel} />;
|
|
422
461
|
}, [
|
|
423
462
|
active,
|
|
463
|
+
app,
|
|
424
464
|
getLayoutModel,
|
|
425
465
|
guardState.allowBridge,
|
|
466
|
+
guardState.legacyPageUnsupported,
|
|
426
467
|
guardState.notFound,
|
|
427
468
|
guardState.pageUid,
|
|
428
469
|
guardState.pending,
|
|
470
|
+
location,
|
|
429
471
|
pageUid,
|
|
430
472
|
]);
|
|
431
473
|
|