@nocobase/client-v2 2.1.0-beta.34 → 2.1.0-beta.35
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/BaseApplication.d.ts +6 -0
- package/es/PluginManager.d.ts +2 -0
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +9 -1
- package/es/index.mjs +77 -77
- package/es/json-logic/globalOperators.d.ts +11 -0
- package/es/utils/globalDeps.d.ts +7 -0
- package/lib/index.js +91 -91
- package/package.json +7 -6
- package/src/BaseApplication.tsx +8 -0
- package/src/PluginManager.ts +2 -0
- package/src/__tests__/app.test.tsx +8 -0
- package/src/__tests__/remotePlugins.test.ts +148 -0
- package/src/css-variable/CSSVariableProvider.tsx +1 -1
- package/src/flow/actions/filterFormDefaultValues.tsx +1 -2
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +329 -5
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +337 -0
- package/src/json-logic/globalOperators.js +731 -0
- package/src/utils/globalDeps.ts +45 -29
- package/src/utils/remotePlugins.ts +107 -6
|
@@ -7,10 +7,97 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { render, waitFor } from '@testing-library/react';
|
|
12
|
+
import { FlowEngine } from '@nocobase/flow-engine';
|
|
10
13
|
import { describe, expect, it, vi } from 'vitest';
|
|
11
14
|
import { filterFormDefaultValues } from '../../../../actions/filterFormDefaultValues';
|
|
12
15
|
import { FilterFormBlockModel } from '../FilterFormBlockModel';
|
|
13
16
|
|
|
17
|
+
function resolveTemplateValue(raw: any, values: Record<string, any>): any {
|
|
18
|
+
if (typeof raw === 'string') {
|
|
19
|
+
const matched = raw.match(/^\{\{\s*ctx\.formValues\.([^}]+?)\s*\}\}$/);
|
|
20
|
+
return matched ? values[matched[1]] : raw;
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(raw)) {
|
|
23
|
+
return raw.map((item) => resolveTemplateValue(item, values));
|
|
24
|
+
}
|
|
25
|
+
if (raw && typeof raw === 'object') {
|
|
26
|
+
return Object.fromEntries(Object.entries(raw).map(([key, value]) => [key, resolveTemplateValue(value, values)]));
|
|
27
|
+
}
|
|
28
|
+
return raw;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createFilterFormDefaultValuesModel(rules: any[], initialValues: Record<string, any> = {}) {
|
|
32
|
+
const values = { ...initialValues };
|
|
33
|
+
const createItem = (fieldPath: string, uid: string) => ({
|
|
34
|
+
uid,
|
|
35
|
+
fieldPath,
|
|
36
|
+
props: { name: `${fieldPath}_${uid}` },
|
|
37
|
+
getProps() {
|
|
38
|
+
return this.props;
|
|
39
|
+
},
|
|
40
|
+
getStepParams(flowKey: string, stepKey: string) {
|
|
41
|
+
if (flowKey === 'fieldSettings' && stepKey === 'init') {
|
|
42
|
+
return { fieldPath };
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
},
|
|
46
|
+
subModels: {
|
|
47
|
+
field: {},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const model = {
|
|
51
|
+
defaultValuesRefreshSeq: 0,
|
|
52
|
+
lastDefaultValueByFieldName: new Map<string, any>(),
|
|
53
|
+
form: {
|
|
54
|
+
getFieldsValue: () => ({ ...values }),
|
|
55
|
+
getFieldValue: (name: string) => values[name],
|
|
56
|
+
setFieldValue: (name: string, value: any) => {
|
|
57
|
+
values[name] = value;
|
|
58
|
+
},
|
|
59
|
+
setFieldsValue: (next: Record<string, any>) => {
|
|
60
|
+
Object.assign(values, next);
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
context: {
|
|
64
|
+
resolveJsonTemplate: vi.fn((raw) => resolveTemplateValue(raw, values)),
|
|
65
|
+
app: {
|
|
66
|
+
jsonLogic: {
|
|
67
|
+
apply: vi.fn((logic: Record<string, any[]>) => {
|
|
68
|
+
const [[operator, args]] = Object.entries(logic);
|
|
69
|
+
if (operator === '$eq') return args[0] === args[1];
|
|
70
|
+
return true;
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
subModels: {
|
|
76
|
+
grid: {
|
|
77
|
+
subModels: {
|
|
78
|
+
items: [createItem('nickname', 'nick'), createItem('username', 'user')],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
getStepParams: vi.fn((flowKey: string, stepKey: string) => {
|
|
83
|
+
if (flowKey === 'formFilterBlockModelSettings' && stepKey === 'defaultValues') {
|
|
84
|
+
return { value: rules };
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}),
|
|
88
|
+
canApplyFormDefaultValue: (FilterFormBlockModel.prototype as any).canApplyFormDefaultValue,
|
|
89
|
+
matchDefaultValueCondition: (FilterFormBlockModel.prototype as any).matchDefaultValueCondition,
|
|
90
|
+
applyFormDefaultValues: FilterFormBlockModel.prototype.applyFormDefaultValues,
|
|
91
|
+
handleFilterFormValuesChange: (FilterFormBlockModel.prototype as any).handleFilterFormValuesChange,
|
|
92
|
+
dispatchEvent: vi.fn(),
|
|
93
|
+
emitter: {
|
|
94
|
+
emit: vi.fn(),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return { model, values };
|
|
99
|
+
}
|
|
100
|
+
|
|
14
101
|
describe('filter-form defaultValues wiring', () => {
|
|
15
102
|
it('loads action and model modules', () => {
|
|
16
103
|
expect(filterFormDefaultValues).toBeTruthy();
|
|
@@ -48,4 +135,254 @@ describe('filter-form defaultValues wiring', () => {
|
|
|
48
135
|
expect(model.initialDefaultsPromise).toBeUndefined();
|
|
49
136
|
expect(model.applyFormDefaultValues).not.toHaveBeenCalled();
|
|
50
137
|
});
|
|
138
|
+
|
|
139
|
+
it('exposes current form values in the filter form variable meta tree', async () => {
|
|
140
|
+
const engine = new FlowEngine();
|
|
141
|
+
|
|
142
|
+
const dataSource = engine.context.dataSourceManager.getDataSource('main');
|
|
143
|
+
dataSource.addCollection({
|
|
144
|
+
name: 'users',
|
|
145
|
+
filterTargetKey: ['id', 'tenantId'],
|
|
146
|
+
fields: [
|
|
147
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
148
|
+
{ name: 'tenantId', type: 'string', interface: 'text' },
|
|
149
|
+
{ name: 'name', type: 'string', interface: 'text' },
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
dataSource.addCollection({
|
|
153
|
+
name: 'departments',
|
|
154
|
+
filterTargetKey: 'id',
|
|
155
|
+
fields: [
|
|
156
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
157
|
+
{ name: 'name', type: 'string', interface: 'text' },
|
|
158
|
+
{ name: 'owner', type: 'belongsTo', target: 'users', interface: 'm2o' },
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
dataSource.addCollection({
|
|
162
|
+
name: 'tasks',
|
|
163
|
+
filterTargetKey: 'id',
|
|
164
|
+
fields: [
|
|
165
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
166
|
+
{ name: 'title', type: 'string', interface: 'text' },
|
|
167
|
+
{ name: 'department', type: 'belongsTo', target: 'departments', interface: 'm2o' },
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
engine.registerModels({ FilterFormBlockModel });
|
|
172
|
+
const model = engine.createModel<FilterFormBlockModel>({
|
|
173
|
+
use: 'FilterFormBlockModel',
|
|
174
|
+
uid: 'filter-form-current-form',
|
|
175
|
+
subModels: {
|
|
176
|
+
grid: {
|
|
177
|
+
subModels: {
|
|
178
|
+
items: [],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
} as any);
|
|
183
|
+
|
|
184
|
+
function HookCaller() {
|
|
185
|
+
model.useHooksBeforeRender();
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
render(React.createElement(HookCaller));
|
|
190
|
+
|
|
191
|
+
const store = {
|
|
192
|
+
title: 'bug',
|
|
193
|
+
'department_department-filter': 1,
|
|
194
|
+
'department.owner_owner-filter': { id: 7, tenantId: 'tenant-a' },
|
|
195
|
+
};
|
|
196
|
+
const fakeForm = {
|
|
197
|
+
getFieldsValue: () => ({ ...store }),
|
|
198
|
+
};
|
|
199
|
+
model.context.defineProperty('form', { value: fakeForm });
|
|
200
|
+
model.subModels.grid.subModels.items = [
|
|
201
|
+
{
|
|
202
|
+
uid: 'department-filter',
|
|
203
|
+
fieldPath: 'department',
|
|
204
|
+
props: { name: 'department_department-filter' },
|
|
205
|
+
subModels: {
|
|
206
|
+
field: {
|
|
207
|
+
context: {
|
|
208
|
+
collectionField: dataSource.getCollection('tasks').getField('department'),
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
uid: 'owner-filter',
|
|
215
|
+
fieldPath: 'department.owner',
|
|
216
|
+
props: { name: 'department.owner_owner-filter' },
|
|
217
|
+
subModels: {
|
|
218
|
+
field: {
|
|
219
|
+
context: {
|
|
220
|
+
collectionField: dataSource.getCollection('departments').getField('owner'),
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
expect((model.context as any).formValues).toMatchObject({
|
|
228
|
+
...store,
|
|
229
|
+
department: {
|
|
230
|
+
'owner_owner-filter': { id: 7, tenantId: 'tenant-a' },
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const options = (model.context as any).getPropertyOptions('formValues');
|
|
235
|
+
const meta = await options.meta();
|
|
236
|
+
const properties = await meta.properties();
|
|
237
|
+
const metaTree = await (model.context as any).getPropertyMetaTree();
|
|
238
|
+
|
|
239
|
+
expect(options.resolveOnServer('department_department-filter')).toBe(false);
|
|
240
|
+
expect(options.resolveOnServer('department_department-filter.name')).toBe(true);
|
|
241
|
+
expect(options.resolveOnServer('department_department-filter[0].name')).toBe(true);
|
|
242
|
+
expect(options.resolveOnServer('department.owner_owner-filter.name')).toBe(true);
|
|
243
|
+
expect(options.serverOnlyWhenContextParams).toBe(true);
|
|
244
|
+
expect(meta.title).toBe('Current form');
|
|
245
|
+
expect(properties.department.properties['owner_owner-filter'].title).toBe('owner');
|
|
246
|
+
expect(metaTree).toEqual(
|
|
247
|
+
expect.arrayContaining([
|
|
248
|
+
expect.objectContaining({
|
|
249
|
+
name: 'formValues',
|
|
250
|
+
title: 'Current form',
|
|
251
|
+
}),
|
|
252
|
+
]),
|
|
253
|
+
);
|
|
254
|
+
expect(await meta.buildVariablesParams(model.context)).toMatchObject({
|
|
255
|
+
'department_department-filter': { collection: 'departments', dataSourceKey: 'main', filterByTk: 1 },
|
|
256
|
+
department: {
|
|
257
|
+
'owner_owner-filter': {
|
|
258
|
+
collection: 'users',
|
|
259
|
+
dataSourceKey: 'main',
|
|
260
|
+
filterByTk: { id: 7, tenantId: 'tenant-a' },
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('refreshes default values that depend on current filter form values', async () => {
|
|
267
|
+
const { model, values } = createFilterFormDefaultValuesModel(
|
|
268
|
+
[
|
|
269
|
+
{
|
|
270
|
+
key: 'username-default',
|
|
271
|
+
enable: true,
|
|
272
|
+
targetPath: 'username',
|
|
273
|
+
mode: 'default',
|
|
274
|
+
value: '{{ ctx.formValues.nickname_nick }}',
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
{ nickname_nick: 'Alice' },
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
|
|
281
|
+
expect(values.username_user).toBe('Alice');
|
|
282
|
+
|
|
283
|
+
values.nickname_nick = 'Bob';
|
|
284
|
+
model.defaultValuesRefreshSeq += 1;
|
|
285
|
+
await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any, {
|
|
286
|
+
refreshSeq: model.defaultValuesRefreshSeq,
|
|
287
|
+
});
|
|
288
|
+
expect(values.username_user).toBe('Bob');
|
|
289
|
+
|
|
290
|
+
values.username_user = 'Manual';
|
|
291
|
+
values.nickname_nick = 'Carol';
|
|
292
|
+
model.defaultValuesRefreshSeq += 1;
|
|
293
|
+
await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any, {
|
|
294
|
+
refreshSeq: model.defaultValuesRefreshSeq,
|
|
295
|
+
});
|
|
296
|
+
expect(values.username_user).toBe('Manual');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('applies fixed values even when the target filter field already has a value', async () => {
|
|
300
|
+
const { model, values } = createFilterFormDefaultValuesModel(
|
|
301
|
+
[
|
|
302
|
+
{
|
|
303
|
+
key: 'username-fixed',
|
|
304
|
+
enable: true,
|
|
305
|
+
targetPath: 'username',
|
|
306
|
+
mode: 'assign',
|
|
307
|
+
value: '{{ ctx.formValues.nickname_nick }}',
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
{ nickname_nick: 'Bob', username_user: 'Manual' },
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
|
|
314
|
+
|
|
315
|
+
expect(values.username_user).toBe('Bob');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('skips filter form field values when the rule condition does not match', async () => {
|
|
319
|
+
const { model, values } = createFilterFormDefaultValuesModel(
|
|
320
|
+
[
|
|
321
|
+
{
|
|
322
|
+
key: 'username-condition',
|
|
323
|
+
enable: true,
|
|
324
|
+
targetPath: 'username',
|
|
325
|
+
mode: 'assign',
|
|
326
|
+
condition: {
|
|
327
|
+
logic: '$and',
|
|
328
|
+
items: [{ path: '{{ ctx.formValues.nickname_nick }}', operator: '$eq', value: 'allow' }],
|
|
329
|
+
},
|
|
330
|
+
value: 'Matched',
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
{ nickname_nick: 'deny' },
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
|
|
337
|
+
expect(values.username_user).toBeUndefined();
|
|
338
|
+
|
|
339
|
+
values.nickname_nick = 'allow';
|
|
340
|
+
await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
|
|
341
|
+
expect(values.username_user).toBe('Matched');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('emits formValuesChange with final values after applying dependent field values', async () => {
|
|
345
|
+
const { model, values } = createFilterFormDefaultValuesModel(
|
|
346
|
+
[
|
|
347
|
+
{
|
|
348
|
+
key: 'username-fixed',
|
|
349
|
+
enable: true,
|
|
350
|
+
targetPath: 'username',
|
|
351
|
+
mode: 'assign',
|
|
352
|
+
value: '{{ ctx.formValues.nickname_nick }}',
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
{ nickname_nick: 'Bob' },
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
(model as any).handleFilterFormValuesChange({ nickname_nick: 'Bob' }, { nickname_nick: 'Bob' });
|
|
359
|
+
|
|
360
|
+
await waitFor(() => {
|
|
361
|
+
expect(values.username_user).toBe('Bob');
|
|
362
|
+
expect(model.dispatchEvent).toHaveBeenCalledWith(
|
|
363
|
+
'formValuesChange',
|
|
364
|
+
{
|
|
365
|
+
changedValues: {
|
|
366
|
+
nickname_nick: 'Bob',
|
|
367
|
+
username_user: 'Bob',
|
|
368
|
+
},
|
|
369
|
+
allValues: {
|
|
370
|
+
nickname_nick: 'Bob',
|
|
371
|
+
username_user: 'Bob',
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
{ debounce: true },
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
expect(model.emitter.emit).toHaveBeenCalledWith('formValuesChange', {
|
|
378
|
+
changedValues: {
|
|
379
|
+
nickname_nick: 'Bob',
|
|
380
|
+
username_user: 'Bob',
|
|
381
|
+
},
|
|
382
|
+
allValues: {
|
|
383
|
+
nickname_nick: 'Bob',
|
|
384
|
+
username_user: 'Bob',
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
});
|
|
51
388
|
});
|