@nocobase/client-v2 3.0.0-alpha.6 → 3.0.0-alpha.8
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/actions/linkageRules.d.ts +24 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +4 -30
- 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/models/actions/UpdateRecordActionUtils.d.ts +8 -2
- 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/index.mjs +106 -106
- package/lib/index.js +97 -97
- package/package.json +7 -7
- package/src/__tests__/app.test.tsx +122 -3
- package/src/__tests__/browserChecker.test.ts +37 -0
- package/src/__tests__/settings-shell.test.tsx +27 -1
- package/src/components/AppComponents.tsx +16 -7
- package/src/components/form/table/Table.tsx +81 -6
- package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/linkageRules.tsx +25 -7
- package/src/flow/components/FieldAssignValueInput.tsx +38 -621
- 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/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/blocks/assign-form/AssignFormItemModel.tsx +28 -53
- 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/table/JSColumnModel.tsx +10 -1
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
- 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/mobile-components/MobileLazySelect.tsx +1 -0
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +20 -2
- package/src/settings-app/SettingsShell.tsx +19 -4
|
@@ -532,6 +532,43 @@ describe('FormValueRuntime (default rules)', () => {
|
|
|
532
532
|
});
|
|
533
533
|
|
|
534
534
|
describe('FormValueRuntime (form assign rules)', () => {
|
|
535
|
+
it('uses the form grid as the variable contract owner', async () => {
|
|
536
|
+
const engineEmitter = new EventEmitter();
|
|
537
|
+
const formStub = createFormStub({ b: 'licensed' });
|
|
538
|
+
const onResolveJsonTemplate = vi.fn();
|
|
539
|
+
const blockModel: any = {
|
|
540
|
+
uid: 'form-contract-owner',
|
|
541
|
+
subModels: { grid: { uid: 'form-contract-owner-grid' } },
|
|
542
|
+
flowEngine: { emitter: engineEmitter },
|
|
543
|
+
emitter: new EventEmitter(),
|
|
544
|
+
dispatchEvent: vi.fn(),
|
|
545
|
+
getAclActionName: () => 'create',
|
|
546
|
+
};
|
|
547
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
|
|
548
|
+
const blockCtx = createFieldContext(runtime);
|
|
549
|
+
const resolveJsonTemplate = blockCtx.resolveJsonTemplate.bind(blockCtx);
|
|
550
|
+
blockCtx.defineMethod('resolveJsonTemplate', async (template: unknown, options?: unknown) => {
|
|
551
|
+
onResolveJsonTemplate(options);
|
|
552
|
+
return resolveJsonTemplate(template, options);
|
|
553
|
+
});
|
|
554
|
+
blockModel.context = blockCtx;
|
|
555
|
+
runtime.mount({ sync: true });
|
|
556
|
+
|
|
557
|
+
runtime.syncAssignRules([
|
|
558
|
+
{
|
|
559
|
+
key: 'license-version',
|
|
560
|
+
enable: true,
|
|
561
|
+
targetPath: 'a',
|
|
562
|
+
mode: 'assign',
|
|
563
|
+
condition: { logic: '$and', items: [] },
|
|
564
|
+
value: '__B__',
|
|
565
|
+
},
|
|
566
|
+
]);
|
|
567
|
+
|
|
568
|
+
await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('licensed'));
|
|
569
|
+
expect(onResolveJsonTemplate).toHaveBeenCalledWith({ contractModelUid: 'form-contract-owner-grid' });
|
|
570
|
+
});
|
|
571
|
+
|
|
535
572
|
it('migrates block-level rule to field instance on mount and restores on unmount', async () => {
|
|
536
573
|
const engineEmitter = new EventEmitter();
|
|
537
574
|
const blockEmitter = new EventEmitter();
|
|
@@ -4086,6 +4123,404 @@ describe('FormValueRuntime (form assign rules)', () => {
|
|
|
4086
4123
|
expect(formStub.getFieldValue(['users', 0, 'user', 'id'])).toBe(1);
|
|
4087
4124
|
});
|
|
4088
4125
|
|
|
4126
|
+
it('assigns a to-one association from a sibling association in the same to-many row', async () => {
|
|
4127
|
+
const engineEmitter = new EventEmitter();
|
|
4128
|
+
const blockEmitter = new EventEmitter();
|
|
4129
|
+
const secondary = { id: 2, name: 'Secondary' };
|
|
4130
|
+
const formStub = createFormStub({ users: [{ user: null, secondaryUser: null }] });
|
|
4131
|
+
|
|
4132
|
+
const blockModel: any = {
|
|
4133
|
+
uid: 'form-assign-assoc-to-many-sibling',
|
|
4134
|
+
flowEngine: { emitter: engineEmitter },
|
|
4135
|
+
emitter: blockEmitter,
|
|
4136
|
+
dispatchEvent: vi.fn(),
|
|
4137
|
+
getAclActionName: () => 'create',
|
|
4138
|
+
};
|
|
4139
|
+
|
|
4140
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
|
|
4141
|
+
runtime.mount({ sync: true });
|
|
4142
|
+
|
|
4143
|
+
const blockCtx = createFieldContext(runtime);
|
|
4144
|
+
const userCollection: any = { getField: () => null };
|
|
4145
|
+
const userField: any = { isAssociationField: () => true, type: 'belongsTo', targetCollection: userCollection };
|
|
4146
|
+
const usersItemCollection: any = {
|
|
4147
|
+
getField: (name: string) => (name === 'user' || name === 'secondaryUser' ? userField : null),
|
|
4148
|
+
};
|
|
4149
|
+
const usersField: any = { isAssociationField: () => true, type: 'hasMany', targetCollection: usersItemCollection };
|
|
4150
|
+
const collection: any = { getField: (name: string) => (name === 'users' ? usersField : null) };
|
|
4151
|
+
blockCtx.defineProperty('collection', { value: collection });
|
|
4152
|
+
|
|
4153
|
+
const rowModel: any = {
|
|
4154
|
+
uid: 'users.user:users:0',
|
|
4155
|
+
isFork: true,
|
|
4156
|
+
forkId: 'users:0',
|
|
4157
|
+
subModels: { field: { context: { collectionField: userField } } },
|
|
4158
|
+
getStepParams(flowKey: string, stepKey: string) {
|
|
4159
|
+
if (flowKey === 'fieldSettings' && stepKey === 'init') return { fieldPath: 'users.user' };
|
|
4160
|
+
return undefined;
|
|
4161
|
+
},
|
|
4162
|
+
};
|
|
4163
|
+
const rowCtx = createFieldContext(runtime);
|
|
4164
|
+
rowCtx.defineProperty('blockModel', { value: blockModel });
|
|
4165
|
+
rowCtx.defineProperty('collection', { value: collection });
|
|
4166
|
+
rowCtx.defineProperty('fieldIndex', { value: ['users:0'] });
|
|
4167
|
+
rowCtx.defineProperty('model', { value: rowModel });
|
|
4168
|
+
rowModel.context = rowCtx;
|
|
4169
|
+
|
|
4170
|
+
blockCtx.defineProperty('engine', {
|
|
4171
|
+
value: { forEachModel: (callback: (model: unknown) => void) => callback(rowModel) },
|
|
4172
|
+
});
|
|
4173
|
+
blockModel.context = blockCtx;
|
|
4174
|
+
|
|
4175
|
+
runtime.syncAssignRules([
|
|
4176
|
+
{
|
|
4177
|
+
key: 'r1',
|
|
4178
|
+
enable: true,
|
|
4179
|
+
targetPath: 'users.user',
|
|
4180
|
+
mode: 'assign',
|
|
4181
|
+
condition: { logic: '$and', items: [] },
|
|
4182
|
+
value: '{{ ctx.item.parentItem.value.secondaryUser }}',
|
|
4183
|
+
},
|
|
4184
|
+
]);
|
|
4185
|
+
|
|
4186
|
+
await runtime.setFormValues(blockCtx, [{ path: ['users', 0, 'secondaryUser'], value: secondary }], {
|
|
4187
|
+
source: 'user',
|
|
4188
|
+
});
|
|
4189
|
+
await waitFor(() => expect(formStub.getFieldValue(['users', 0, 'user'])).toEqual(secondary));
|
|
4190
|
+
|
|
4191
|
+
await runtime.setFormValues(blockCtx, [{ path: ['users', 0, 'user'], value: { id: 3, name: 'Manual' } }], {
|
|
4192
|
+
source: 'user',
|
|
4193
|
+
});
|
|
4194
|
+
await waitFor(() => expect(formStub.getFieldValue(['users', 0, 'user'])).toEqual(secondary));
|
|
4195
|
+
|
|
4196
|
+
const nextSecondary = { id: 4, name: 'Next secondary' };
|
|
4197
|
+
await runtime.setFormValues(blockCtx, [{ path: ['users', 0, 'secondaryUser'], value: nextSecondary }], {
|
|
4198
|
+
source: 'user',
|
|
4199
|
+
});
|
|
4200
|
+
await waitFor(() => expect(formStub.getFieldValue(['users', 0, 'user'])).toEqual(nextSecondary));
|
|
4201
|
+
});
|
|
4202
|
+
|
|
4203
|
+
it('materializes assigned proxies so later to-many rows continue updating', async () => {
|
|
4204
|
+
const engineEmitter = new EventEmitter();
|
|
4205
|
+
const blockEmitter = new EventEmitter();
|
|
4206
|
+
const initialSecondary = { id: 2, name: 'Initial secondary', roleCodes: ['admin'] };
|
|
4207
|
+
const nextSecondary = { id: 4, name: 'Next secondary', roleCodes: ['editor'] };
|
|
4208
|
+
type UserRow = {
|
|
4209
|
+
user: unknown;
|
|
4210
|
+
secondaryUser: typeof initialSecondary | null;
|
|
4211
|
+
};
|
|
4212
|
+
const store: { users: UserRow[] } = {
|
|
4213
|
+
users: [{ user: null, secondaryUser: initialSecondary }],
|
|
4214
|
+
};
|
|
4215
|
+
type FormNamePath = Parameters<FormInstance['getFieldValue']>[0];
|
|
4216
|
+
const formStub = {
|
|
4217
|
+
getFieldValue: (namePath: FormNamePath) => lodashGet(store, namePath),
|
|
4218
|
+
setFieldValue: (namePath: FormNamePath, value: unknown) => lodashSet(store, namePath, value),
|
|
4219
|
+
// Match Ant Form: the outer store is plain, but nested values may still be reactive proxies.
|
|
4220
|
+
getFieldsValue: () => store,
|
|
4221
|
+
setFieldsValue: (patch: Partial<typeof store>) => lodashMerge(store, patch),
|
|
4222
|
+
};
|
|
4223
|
+
|
|
4224
|
+
const blockModel = {
|
|
4225
|
+
uid: 'form-assign-assoc-to-many-later-row-reactive-proxy',
|
|
4226
|
+
flowEngine: { emitter: engineEmitter },
|
|
4227
|
+
emitter: blockEmitter,
|
|
4228
|
+
dispatchEvent: vi.fn(),
|
|
4229
|
+
getAclActionName: () => 'create',
|
|
4230
|
+
context: undefined as unknown,
|
|
4231
|
+
};
|
|
4232
|
+
|
|
4233
|
+
const runtime = new FormValueRuntime({
|
|
4234
|
+
model: blockModel as unknown as ConstructorParameters<typeof FormValueRuntime>[0]['model'],
|
|
4235
|
+
getForm: () => formStub as unknown as FormInstance,
|
|
4236
|
+
});
|
|
4237
|
+
runtime.mount({ sync: true });
|
|
4238
|
+
|
|
4239
|
+
const blockCtx = createFieldContext(runtime);
|
|
4240
|
+
const userCollection = { getField: () => null };
|
|
4241
|
+
const userField = { isAssociationField: () => true, type: 'belongsTo', targetCollection: userCollection };
|
|
4242
|
+
const usersItemCollection = {
|
|
4243
|
+
getField: (name: string) => (name === 'user' || name === 'secondaryUser' ? userField : null),
|
|
4244
|
+
};
|
|
4245
|
+
const usersField = { isAssociationField: () => true, type: 'hasMany', targetCollection: usersItemCollection };
|
|
4246
|
+
const collection = { getField: (name: string) => (name === 'users' ? usersField : null) };
|
|
4247
|
+
blockCtx.defineProperty('collection', { value: collection });
|
|
4248
|
+
|
|
4249
|
+
const createRowModel = (index: number) => {
|
|
4250
|
+
const forkId = `users:${index}`;
|
|
4251
|
+
const rowCtx = createFieldContext(runtime);
|
|
4252
|
+
const rowModel = {
|
|
4253
|
+
uid: `users.user:${forkId}`,
|
|
4254
|
+
isFork: true,
|
|
4255
|
+
forkId,
|
|
4256
|
+
subModels: { field: { context: { collectionField: userField } } },
|
|
4257
|
+
getStepParams(flowKey: string, stepKey: string) {
|
|
4258
|
+
if (flowKey === 'fieldSettings' && stepKey === 'init') return { fieldPath: 'users.user' };
|
|
4259
|
+
return undefined;
|
|
4260
|
+
},
|
|
4261
|
+
context: rowCtx,
|
|
4262
|
+
};
|
|
4263
|
+
rowCtx.defineProperty('blockModel', { value: blockModel });
|
|
4264
|
+
rowCtx.defineProperty('collection', { value: collection });
|
|
4265
|
+
rowCtx.defineProperty('fieldIndex', { value: [forkId] });
|
|
4266
|
+
rowCtx.defineProperty('model', { value: rowModel });
|
|
4267
|
+
return rowModel;
|
|
4268
|
+
};
|
|
4269
|
+
const rowModels = [createRowModel(0)];
|
|
4270
|
+
|
|
4271
|
+
blockCtx.defineProperty('engine', {
|
|
4272
|
+
value: { forEachModel: (callback: (model: unknown) => void) => rowModels.forEach(callback) },
|
|
4273
|
+
});
|
|
4274
|
+
blockModel.context = blockCtx;
|
|
4275
|
+
|
|
4276
|
+
runtime.syncAssignRules([
|
|
4277
|
+
{
|
|
4278
|
+
key: 'user-from-row-sibling',
|
|
4279
|
+
enable: true,
|
|
4280
|
+
targetPath: 'users.user',
|
|
4281
|
+
mode: 'assign',
|
|
4282
|
+
condition: { logic: '$and', items: [] },
|
|
4283
|
+
value: '{{ ctx.item.parentItem.value.secondaryUser }}',
|
|
4284
|
+
},
|
|
4285
|
+
]);
|
|
4286
|
+
|
|
4287
|
+
await waitFor(() => expect(formStub.getFieldValue(['users', 0, 'user'])).toEqual(initialSecondary));
|
|
4288
|
+
const assignedUser = formStub.getFieldValue(['users', 0, 'user']);
|
|
4289
|
+
const assignedRoleCodes = formStub.getFieldValue(['users', 0, 'user', 'roleCodes']);
|
|
4290
|
+
expect(assignedUser.constructor).toBe(Object);
|
|
4291
|
+
expect(Array.isArray(assignedRoleCodes)).toBe(true);
|
|
4292
|
+
expect(assignedRoleCodes.constructor).toBe(Array);
|
|
4293
|
+
|
|
4294
|
+
const secondRow: UserRow = { user: null, secondaryUser: null };
|
|
4295
|
+
store.users.push(secondRow);
|
|
4296
|
+
const addedRows: Array<UserRow | undefined> = new Array(2);
|
|
4297
|
+
addedRows[1] = secondRow;
|
|
4298
|
+
expect(() => runtime.handleFormValuesChange({ users: addedRows }, store)).not.toThrow();
|
|
4299
|
+
|
|
4300
|
+
const row1 = createRowModel(1);
|
|
4301
|
+
rowModels.push(row1);
|
|
4302
|
+
engineEmitter.emit('model:mounted', { model: row1 });
|
|
4303
|
+
|
|
4304
|
+
secondRow.secondaryUser = nextSecondary;
|
|
4305
|
+
const changedRows: Array<Partial<UserRow> | undefined> = new Array(2);
|
|
4306
|
+
changedRows[1] = { secondaryUser: nextSecondary };
|
|
4307
|
+
expect(0 in changedRows).toBe(false);
|
|
4308
|
+
|
|
4309
|
+
expect(() => runtime.handleFormValuesChange({ users: changedRows }, store)).not.toThrow();
|
|
4310
|
+
await waitFor(() => expect(formStub.getFieldValue(['users', 1, 'user'])).toEqual(nextSecondary));
|
|
4311
|
+
expect(formStub.getFieldValue(['users', 0, 'user'])).toEqual(initialSecondary);
|
|
4312
|
+
});
|
|
4313
|
+
|
|
4314
|
+
it('assigns a to-many association from a sibling association in the same to-many row', async () => {
|
|
4315
|
+
const engineEmitter = new EventEmitter();
|
|
4316
|
+
const blockEmitter = new EventEmitter();
|
|
4317
|
+
const initialRoles = [{ id: 2, name: 'Initial role' }];
|
|
4318
|
+
const formStub = createFormStub({ users: [{ roles: [], secondaryRoles: [] }] });
|
|
4319
|
+
|
|
4320
|
+
const blockModel: any = {
|
|
4321
|
+
uid: 'form-assign-to-many-assoc-from-to-many-row-sibling',
|
|
4322
|
+
flowEngine: { emitter: engineEmitter },
|
|
4323
|
+
emitter: blockEmitter,
|
|
4324
|
+
dispatchEvent: vi.fn(),
|
|
4325
|
+
getAclActionName: () => 'create',
|
|
4326
|
+
};
|
|
4327
|
+
|
|
4328
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
|
|
4329
|
+
runtime.mount({ sync: true });
|
|
4330
|
+
|
|
4331
|
+
const blockCtx = createFieldContext(runtime);
|
|
4332
|
+
const roleCollection: any = { filterTargetKey: 'id', getField: () => null };
|
|
4333
|
+
const rolesField: any = {
|
|
4334
|
+
isAssociationField: () => true,
|
|
4335
|
+
type: 'belongsToMany',
|
|
4336
|
+
targetCollection: roleCollection,
|
|
4337
|
+
};
|
|
4338
|
+
const usersItemCollection: any = {
|
|
4339
|
+
getField: (name: string) => (name === 'roles' || name === 'secondaryRoles' ? rolesField : null),
|
|
4340
|
+
};
|
|
4341
|
+
const usersField: any = { isAssociationField: () => true, type: 'hasMany', targetCollection: usersItemCollection };
|
|
4342
|
+
const collection: any = { getField: (name: string) => (name === 'users' ? usersField : null) };
|
|
4343
|
+
blockCtx.defineProperty('collection', { value: collection });
|
|
4344
|
+
|
|
4345
|
+
const rowModel: any = {
|
|
4346
|
+
uid: 'users.roles:users:0',
|
|
4347
|
+
isFork: true,
|
|
4348
|
+
forkId: 'users:0',
|
|
4349
|
+
subModels: { field: { context: { collectionField: rolesField } } },
|
|
4350
|
+
getStepParams(flowKey: string, stepKey: string) {
|
|
4351
|
+
if (flowKey === 'fieldSettings' && stepKey === 'init') return { fieldPath: 'users.roles' };
|
|
4352
|
+
return undefined;
|
|
4353
|
+
},
|
|
4354
|
+
};
|
|
4355
|
+
const rowCtx = createFieldContext(runtime);
|
|
4356
|
+
rowCtx.defineProperty('blockModel', { value: blockModel });
|
|
4357
|
+
rowCtx.defineProperty('collection', { value: collection });
|
|
4358
|
+
rowCtx.defineProperty('fieldIndex', { value: ['users:0'] });
|
|
4359
|
+
rowCtx.defineProperty('model', { value: rowModel });
|
|
4360
|
+
rowModel.context = rowCtx;
|
|
4361
|
+
|
|
4362
|
+
blockCtx.defineProperty('engine', {
|
|
4363
|
+
value: { forEachModel: (callback: (model: unknown) => void) => callback(rowModel) },
|
|
4364
|
+
});
|
|
4365
|
+
blockModel.context = blockCtx;
|
|
4366
|
+
|
|
4367
|
+
runtime.syncAssignRules([
|
|
4368
|
+
{
|
|
4369
|
+
key: 'roles-from-row-sibling',
|
|
4370
|
+
enable: true,
|
|
4371
|
+
targetPath: 'users.roles',
|
|
4372
|
+
mode: 'assign',
|
|
4373
|
+
condition: { logic: '$and', items: [] },
|
|
4374
|
+
value: '{{ ctx.item.parentItem.value.secondaryRoles }}',
|
|
4375
|
+
},
|
|
4376
|
+
]);
|
|
4377
|
+
|
|
4378
|
+
await runtime.setFormValues(blockCtx, [{ path: ['users', 0, 'secondaryRoles'], value: initialRoles }], {
|
|
4379
|
+
source: 'user',
|
|
4380
|
+
});
|
|
4381
|
+
await waitFor(() => expect(formStub.getFieldValue(['users', 0, 'roles'])).toEqual(initialRoles));
|
|
4382
|
+
|
|
4383
|
+
const nextRoles = [{ id: 3, name: 'Next role' }];
|
|
4384
|
+
await runtime.setFormValues(blockCtx, [{ path: ['users', 0, 'secondaryRoles'], value: nextRoles }], {
|
|
4385
|
+
source: 'user',
|
|
4386
|
+
});
|
|
4387
|
+
await waitFor(() => expect(formStub.getFieldValue(['users', 0, 'roles'])).toEqual(nextRoles));
|
|
4388
|
+
});
|
|
4389
|
+
|
|
4390
|
+
it('preserves the PopupSubTable outer item chain for a top-level to-one association target', async () => {
|
|
4391
|
+
const engineEmitter = new EventEmitter();
|
|
4392
|
+
const blockEmitter = new EventEmitter();
|
|
4393
|
+
const initialAssignee = { id: 2, name: 'Initial assignee' };
|
|
4394
|
+
const nextAssignee = { id: 3, name: 'Next assignee' };
|
|
4395
|
+
const outerValue = observable({ defaultAssignee: initialAssignee });
|
|
4396
|
+
const formStub = createFormStub({ assignee: null });
|
|
4397
|
+
|
|
4398
|
+
const blockModel: any = {
|
|
4399
|
+
uid: 'popup-sub-table-form-assign-parent-item',
|
|
4400
|
+
flowEngine: { emitter: engineEmitter },
|
|
4401
|
+
emitter: blockEmitter,
|
|
4402
|
+
dispatchEvent: vi.fn(),
|
|
4403
|
+
getAclActionName: () => 'create',
|
|
4404
|
+
};
|
|
4405
|
+
|
|
4406
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
|
|
4407
|
+
runtime.mount({ sync: true });
|
|
4408
|
+
|
|
4409
|
+
const blockCtx = createFieldContext(runtime);
|
|
4410
|
+
const userCollection: any = { filterTargetKey: 'id', getField: () => null };
|
|
4411
|
+
const assigneeField: any = {
|
|
4412
|
+
isAssociationField: () => true,
|
|
4413
|
+
type: 'belongsTo',
|
|
4414
|
+
targetCollection: userCollection,
|
|
4415
|
+
};
|
|
4416
|
+
const collection: any = { getField: (name: string) => (name === 'assignee' ? assigneeField : null) };
|
|
4417
|
+
const outerItem = { index: 0, length: 1, value: outerValue };
|
|
4418
|
+
blockCtx.defineProperty('collection', { value: collection });
|
|
4419
|
+
blockCtx.defineProperty('item', {
|
|
4420
|
+
get: () => ({
|
|
4421
|
+
index: 1,
|
|
4422
|
+
length: 3,
|
|
4423
|
+
__is_new__: true,
|
|
4424
|
+
__is_stored__: false,
|
|
4425
|
+
value: blockCtx.formValues,
|
|
4426
|
+
parentItem: outerItem,
|
|
4427
|
+
}),
|
|
4428
|
+
cache: false,
|
|
4429
|
+
});
|
|
4430
|
+
blockModel.context = blockCtx;
|
|
4431
|
+
|
|
4432
|
+
runtime.syncAssignRules([
|
|
4433
|
+
{
|
|
4434
|
+
key: 'assignee-from-popup-parent',
|
|
4435
|
+
enable: true,
|
|
4436
|
+
targetPath: 'assignee',
|
|
4437
|
+
mode: 'assign',
|
|
4438
|
+
condition: {
|
|
4439
|
+
logic: '$and',
|
|
4440
|
+
items: [
|
|
4441
|
+
{ path: '{{ ctx.item.parentItem.index }}', operator: '$eq', value: 1 },
|
|
4442
|
+
{ path: '{{ ctx.item.parentItem.length }}', operator: '$eq', value: 3 },
|
|
4443
|
+
{ path: '{{ ctx.item.parentItem.__is_new__ }}', operator: '$eq', value: true },
|
|
4444
|
+
{ path: '{{ ctx.item.parentItem.__is_stored__ }}', operator: '$eq', value: false },
|
|
4445
|
+
],
|
|
4446
|
+
},
|
|
4447
|
+
value: '{{ ctx.item.parentItem.parentItem.value.defaultAssignee }}',
|
|
4448
|
+
},
|
|
4449
|
+
]);
|
|
4450
|
+
|
|
4451
|
+
await waitFor(() => expect(formStub.getFieldValue(['assignee'])).toEqual(initialAssignee));
|
|
4452
|
+
|
|
4453
|
+
outerValue.defaultAssignee = nextAssignee;
|
|
4454
|
+
await waitFor(() => expect(formStub.getFieldValue(['assignee'])).toEqual(nextAssignee));
|
|
4455
|
+
});
|
|
4456
|
+
|
|
4457
|
+
it('preserves the PopupSubTable outer item chain for a top-level to-many association target', async () => {
|
|
4458
|
+
const engineEmitter = new EventEmitter();
|
|
4459
|
+
const blockEmitter = new EventEmitter();
|
|
4460
|
+
const initialReviewers = [{ id: 2, name: 'Initial reviewer' }];
|
|
4461
|
+
const nextReviewers = [{ id: 3, name: 'Next reviewer' }];
|
|
4462
|
+
const outerValue = observable({ defaultReviewers: initialReviewers });
|
|
4463
|
+
const formStub = createFormStub({ reviewers: [] });
|
|
4464
|
+
|
|
4465
|
+
const blockModel: any = {
|
|
4466
|
+
uid: 'popup-sub-table-form-assign-to-many-parent-item',
|
|
4467
|
+
flowEngine: { emitter: engineEmitter },
|
|
4468
|
+
emitter: blockEmitter,
|
|
4469
|
+
dispatchEvent: vi.fn(),
|
|
4470
|
+
getAclActionName: () => 'create',
|
|
4471
|
+
};
|
|
4472
|
+
|
|
4473
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
|
|
4474
|
+
runtime.mount({ sync: true });
|
|
4475
|
+
|
|
4476
|
+
const blockCtx = createFieldContext(runtime);
|
|
4477
|
+
const userCollection: any = { filterTargetKey: 'id', getField: () => null };
|
|
4478
|
+
const reviewersField: any = {
|
|
4479
|
+
isAssociationField: () => true,
|
|
4480
|
+
type: 'belongsToMany',
|
|
4481
|
+
targetCollection: userCollection,
|
|
4482
|
+
};
|
|
4483
|
+
const collection: any = { getField: (name: string) => (name === 'reviewers' ? reviewersField : null) };
|
|
4484
|
+
const outerItem = { index: 0, length: 1, value: outerValue };
|
|
4485
|
+
blockCtx.defineProperty('collection', { value: collection });
|
|
4486
|
+
blockCtx.defineProperty('item', {
|
|
4487
|
+
get: () => ({
|
|
4488
|
+
index: 1,
|
|
4489
|
+
length: 3,
|
|
4490
|
+
__is_new__: true,
|
|
4491
|
+
__is_stored__: false,
|
|
4492
|
+
value: blockCtx.formValues,
|
|
4493
|
+
parentItem: outerItem,
|
|
4494
|
+
}),
|
|
4495
|
+
cache: false,
|
|
4496
|
+
});
|
|
4497
|
+
blockModel.context = blockCtx;
|
|
4498
|
+
|
|
4499
|
+
runtime.syncAssignRules([
|
|
4500
|
+
{
|
|
4501
|
+
key: 'reviewers-from-popup-parent',
|
|
4502
|
+
enable: true,
|
|
4503
|
+
targetPath: 'reviewers',
|
|
4504
|
+
mode: 'assign',
|
|
4505
|
+
condition: {
|
|
4506
|
+
logic: '$and',
|
|
4507
|
+
items: [
|
|
4508
|
+
{ path: '{{ ctx.item.parentItem.index }}', operator: '$eq', value: 1 },
|
|
4509
|
+
{ path: '{{ ctx.item.parentItem.length }}', operator: '$eq', value: 3 },
|
|
4510
|
+
{ path: '{{ ctx.item.parentItem.__is_new__ }}', operator: '$eq', value: true },
|
|
4511
|
+
{ path: '{{ ctx.item.parentItem.__is_stored__ }}', operator: '$eq', value: false },
|
|
4512
|
+
],
|
|
4513
|
+
},
|
|
4514
|
+
value: '{{ ctx.item.parentItem.parentItem.value.defaultReviewers }}',
|
|
4515
|
+
},
|
|
4516
|
+
]);
|
|
4517
|
+
|
|
4518
|
+
await waitFor(() => expect(formStub.getFieldValue(['reviewers'])).toEqual(initialReviewers));
|
|
4519
|
+
|
|
4520
|
+
outerValue.defaultReviewers = nextReviewers;
|
|
4521
|
+
await waitFor(() => expect(formStub.getFieldValue(['reviewers'])).toEqual(nextReviewers));
|
|
4522
|
+
});
|
|
4523
|
+
|
|
4089
4524
|
it('does not write to to-many nested path without row index', async () => {
|
|
4090
4525
|
const engineEmitter = new EventEmitter();
|
|
4091
4526
|
const blockEmitter = new EventEmitter();
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { isObservable, reaction, toJS } from '@formily/reactive';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
FlowContext,
|
|
13
|
+
FlowModel,
|
|
14
|
+
isRunJSValue,
|
|
15
|
+
normalizeRunJSValue,
|
|
16
|
+
type ResolveJsonTemplateOptions,
|
|
17
|
+
} from '@nocobase/flow-engine';
|
|
12
18
|
import { getValuesByPath } from '@nocobase/shared';
|
|
13
19
|
import _ from 'lodash';
|
|
14
20
|
import { dayjs } from '@nocobase/utils/client';
|
|
@@ -49,6 +55,7 @@ type RuntimeRule = {
|
|
|
49
55
|
getValue: () => any;
|
|
50
56
|
getCondition?: () => any;
|
|
51
57
|
getContext: () => any;
|
|
58
|
+
getContractModelUid?: () => string | undefined;
|
|
52
59
|
};
|
|
53
60
|
|
|
54
61
|
type ObservableBinding = {
|
|
@@ -83,8 +90,18 @@ type ToManyAggregateSourceInfo = {
|
|
|
83
90
|
lastWrite?: FormValueWriteMeta;
|
|
84
91
|
};
|
|
85
92
|
|
|
93
|
+
type RuntimeItemChain = {
|
|
94
|
+
index?: number;
|
|
95
|
+
length?: number;
|
|
96
|
+
__is_new__?: boolean;
|
|
97
|
+
__is_stored__?: boolean;
|
|
98
|
+
value: unknown;
|
|
99
|
+
parentItem?: RuntimeItemChain;
|
|
100
|
+
};
|
|
101
|
+
|
|
86
102
|
export type RuleEngineOptions = {
|
|
87
103
|
getBlockModelUid: () => string;
|
|
104
|
+
getAssignRulesModelUid: () => string | undefined;
|
|
88
105
|
getActionName: () => string | undefined;
|
|
89
106
|
getBlockContext: () => any;
|
|
90
107
|
getEngine: () => any;
|
|
@@ -208,6 +225,7 @@ export class RuleEngine {
|
|
|
208
225
|
getValue: () => template?.value,
|
|
209
226
|
getCondition: () => template?.condition,
|
|
210
227
|
getContext: () => this.options.getBlockContext(),
|
|
228
|
+
getContractModelUid: this.options.getAssignRulesModelUid,
|
|
211
229
|
};
|
|
212
230
|
|
|
213
231
|
this.rules.set(id, { rule, state: { deps: new Set(), depDisposers: [], runSeq: 0, scheduledAtWriteSeq: 0 } });
|
|
@@ -545,6 +563,7 @@ export class RuleEngine {
|
|
|
545
563
|
getValue: () => template?.value,
|
|
546
564
|
getCondition: () => template?.condition,
|
|
547
565
|
getContext: () => this.options.getBlockContext(),
|
|
566
|
+
getContractModelUid: this.options.getAssignRulesModelUid,
|
|
548
567
|
};
|
|
549
568
|
|
|
550
569
|
this.rules.set(id, { rule, state: { deps: new Set(), depDisposers: [], runSeq: 0, scheduledAtWriteSeq: 0 } });
|
|
@@ -913,6 +932,7 @@ export class RuleEngine {
|
|
|
913
932
|
getValue: () => template?.value,
|
|
914
933
|
getCondition: () => template?.condition,
|
|
915
934
|
getContext: () => model?.context,
|
|
935
|
+
getContractModelUid: this.options.getAssignRulesModelUid,
|
|
916
936
|
};
|
|
917
937
|
|
|
918
938
|
this.rules.set(id, { rule, state: { deps: new Set(), depDisposers: [], runSeq: 0, scheduledAtWriteSeq: 0 } });
|
|
@@ -1662,7 +1682,7 @@ export class RuleEngine {
|
|
|
1662
1682
|
}
|
|
1663
1683
|
}
|
|
1664
1684
|
|
|
1665
|
-
const evalCtx = this.createRuleEvaluationContext(baseCtx, collector, targetNamePath);
|
|
1685
|
+
const evalCtx = this.createRuleEvaluationContext(baseCtx, collector, targetNamePath, rule.getContractModelUid?.());
|
|
1666
1686
|
return { collector, evalCtx, rawValue, isRunJS };
|
|
1667
1687
|
}
|
|
1668
1688
|
|
|
@@ -2001,7 +2021,12 @@ export class RuleEngine {
|
|
|
2001
2021
|
return canOverwrite;
|
|
2002
2022
|
}
|
|
2003
2023
|
|
|
2004
|
-
private createRuleEvaluationContext(
|
|
2024
|
+
private createRuleEvaluationContext(
|
|
2025
|
+
baseCtx: any,
|
|
2026
|
+
collector: DepCollector,
|
|
2027
|
+
targetNamePath: NamePath | null,
|
|
2028
|
+
contractModelUid?: string,
|
|
2029
|
+
) {
|
|
2005
2030
|
const trackingFormValues = this.options.createTrackingFormValues(collector);
|
|
2006
2031
|
const ctx: any = new FlowContext();
|
|
2007
2032
|
try {
|
|
@@ -2021,12 +2046,19 @@ export class RuleEngine {
|
|
|
2021
2046
|
}
|
|
2022
2047
|
|
|
2023
2048
|
const delegatedResolveJsonTemplate =
|
|
2024
|
-
typeof ctx.resolveJsonTemplate === 'function'
|
|
2049
|
+
typeof ctx.resolveJsonTemplate === 'function'
|
|
2050
|
+
? (ctx.resolveJsonTemplate.bind(ctx) as (
|
|
2051
|
+
template: unknown,
|
|
2052
|
+
options?: ResolveJsonTemplateOptions,
|
|
2053
|
+
) => Promise<unknown>)
|
|
2054
|
+
: undefined;
|
|
2025
2055
|
ctx.defineMethod('resolveJsonTemplate', async (template: unknown) => {
|
|
2026
2056
|
const tokenStore = this.createLocalTemplateTokenStore();
|
|
2027
2057
|
const localResolved = this.resolveLocalFormValuesTemplates(baseCtx, template, collector, tokenStore);
|
|
2028
2058
|
const nextTemplate = localResolved.matched ? localResolved.value : template;
|
|
2029
|
-
const resolved = delegatedResolveJsonTemplate
|
|
2059
|
+
const resolved = delegatedResolveJsonTemplate
|
|
2060
|
+
? await delegatedResolveJsonTemplate(nextTemplate, contractModelUid ? { contractModelUid } : undefined)
|
|
2061
|
+
: nextTemplate;
|
|
2030
2062
|
return this.restoreLocalTemplateTokens(resolved, tokenStore);
|
|
2031
2063
|
});
|
|
2032
2064
|
|
|
@@ -2038,7 +2070,7 @@ export class RuleEngine {
|
|
|
2038
2070
|
// - parentItem:上级项(同结构,可链式 parentItem.parentItem...)
|
|
2039
2071
|
// 计算顺序:
|
|
2040
2072
|
// 1) 优先按 targetNamePath 从 formValues 构建“关联链 item”
|
|
2041
|
-
// 2)
|
|
2073
|
+
// 2) 若无法构建(例如目标字段是顶层非关联字段),回退到上游显式注入的 baseCtx.item
|
|
2042
2074
|
// (如 PopupSubTable 新增弹窗传入的 parentItem 链)
|
|
2043
2075
|
let itemCached: any;
|
|
2044
2076
|
let itemCachedReady = false;
|
|
@@ -2209,9 +2241,23 @@ export class RuleEngine {
|
|
|
2209
2241
|
parentItem,
|
|
2210
2242
|
};
|
|
2211
2243
|
};
|
|
2212
|
-
const
|
|
2213
|
-
|
|
2214
|
-
|
|
2244
|
+
const blockCtx = this.options.getBlockContext();
|
|
2245
|
+
const formRootItem = (() => {
|
|
2246
|
+
try {
|
|
2247
|
+
const item = blockCtx?.item as RuntimeItemChain | undefined;
|
|
2248
|
+
if (!item || typeof item !== 'object') return undefined;
|
|
2249
|
+
return item.value === blockCtx?.formValues ? item : undefined;
|
|
2250
|
+
} catch {
|
|
2251
|
+
return undefined;
|
|
2252
|
+
}
|
|
2253
|
+
})();
|
|
2254
|
+
const defaultRoot = {
|
|
2255
|
+
...buildNode(trackingFormValues, formRootItem?.index, formRootItem?.length, formRootItem?.parentItem),
|
|
2256
|
+
__is_new__: formRootItem?.__is_new__ ?? trackingFormValues?.__is_new__,
|
|
2257
|
+
__is_stored__: formRootItem?.__is_stored__ ?? trackingFormValues?.__is_stored__,
|
|
2258
|
+
};
|
|
2259
|
+
// item 用于“关系字段目标或其子路径”场景;
|
|
2260
|
+
// 顶层非关联字段/非关联嵌套对象字段应使用 formValues。
|
|
2215
2261
|
if (!targetNamePath || !Array.isArray(targetNamePath) || !targetNamePath.length) return undefined;
|
|
2216
2262
|
if (!rootCollection?.getField) return undefined;
|
|
2217
2263
|
|
|
@@ -2219,7 +2265,9 @@ export class RuleEngine {
|
|
|
2219
2265
|
const prefix: NamePath = [];
|
|
2220
2266
|
let collection = rootCollection;
|
|
2221
2267
|
|
|
2222
|
-
|
|
2268
|
+
// The target itself can be an association. Keep that final association in the item chain so
|
|
2269
|
+
// ctx.item.parentItem continues to point at the containing row instead of skipping directly to the form root.
|
|
2270
|
+
for (let i = 0; i < targetNamePath.length; i++) {
|
|
2223
2271
|
const seg = targetNamePath[i];
|
|
2224
2272
|
if (typeof seg !== 'string') break;
|
|
2225
2273
|
|
|
@@ -2231,9 +2279,12 @@ export class RuleEngine {
|
|
|
2231
2279
|
|
|
2232
2280
|
if (toMany) {
|
|
2233
2281
|
const next = targetNamePath[i + 1];
|
|
2234
|
-
if (typeof next
|
|
2235
|
-
|
|
2236
|
-
|
|
2282
|
+
if (typeof next === 'number') {
|
|
2283
|
+
prefix.push(next);
|
|
2284
|
+
i += 1;
|
|
2285
|
+
} else if (i !== targetNamePath.length - 1) {
|
|
2286
|
+
break;
|
|
2287
|
+
}
|
|
2237
2288
|
}
|
|
2238
2289
|
|
|
2239
2290
|
const targetCollection = field?.targetCollection;
|
|
@@ -2248,9 +2299,11 @@ export class RuleEngine {
|
|
|
2248
2299
|
const assocEntry = assocEntries[idx];
|
|
2249
2300
|
const value = _.get(trackingFormValues, assocEntry.path);
|
|
2250
2301
|
const lastSeg = assocEntry.path[assocEntry.path.length - 1];
|
|
2251
|
-
const
|
|
2302
|
+
const isIndexedToManyItem = assocEntry.toMany && typeof lastSeg === 'number';
|
|
2303
|
+
const index = isIndexedToManyItem ? lastSeg : undefined;
|
|
2252
2304
|
const length = (() => {
|
|
2253
2305
|
if (!assocEntry.toMany) return undefined;
|
|
2306
|
+
if (!isIndexedToManyItem) return Array.isArray(value) ? value.length : undefined;
|
|
2254
2307
|
// assocEntry.path: [..., associationKey, rowIndex]
|
|
2255
2308
|
const listPath = assocEntry.path.slice(0, -1);
|
|
2256
2309
|
const list = _.get(trackingFormValues, listPath);
|