@nocobase/client-v2 3.0.0-alpha.7 → 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/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/index.mjs +101 -110
- package/lib/index.js +93 -102
- package/package.json +7 -7
- package/src/__tests__/app.test.tsx +15 -6
- package/src/components/AppComponents.tsx +4 -4
- 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/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/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/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/mobile-components/MobileLazySelect.tsx +1 -0
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +20 -2
|
@@ -10,23 +10,20 @@
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { Input } from 'antd';
|
|
12
12
|
import { define, observable } from '@formily/reactive';
|
|
13
|
-
import {
|
|
14
|
-
FlowModelRenderer,
|
|
15
|
-
FormItem,
|
|
16
|
-
VariableInput,
|
|
17
|
-
tExpr,
|
|
18
|
-
isVariableExpression,
|
|
19
|
-
parseValueToPath,
|
|
20
|
-
isRunJSValue,
|
|
21
|
-
EditableItemModel,
|
|
22
|
-
jioToJoiSchema,
|
|
23
|
-
} from '@nocobase/flow-engine';
|
|
13
|
+
import { FlowModelRenderer, FormItem, tExpr, EditableItemModel, jioToJoiSchema } from '@nocobase/flow-engine';
|
|
24
14
|
// 无需类型导入(避免未使用的类型)
|
|
25
15
|
import { FormItemModel } from '../form/FormItemModel';
|
|
26
16
|
import { EditFormModel } from '../form/EditFormModel';
|
|
27
17
|
import { customAlphabet as Alphabet } from 'nanoid';
|
|
28
18
|
import { ensureOptionsFromUiSchemaEnumIfAbsent } from '../../../internal/utils/enumOptionsUtils';
|
|
29
19
|
import { RunJSValueEditor } from '../../../components/RunJSValueEditor';
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_DATE_VARIABLE_COMPONENT_PROPS,
|
|
22
|
+
FieldValueVariableInput,
|
|
23
|
+
getFieldInterface,
|
|
24
|
+
isDateLikeField,
|
|
25
|
+
resolveDateVariableComponentProps,
|
|
26
|
+
} from '../../../components/field-value-variable';
|
|
30
27
|
|
|
31
28
|
type AssignFormTempOriginField = {
|
|
32
29
|
uid?: string;
|
|
@@ -285,7 +282,9 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
285
282
|
) : null;
|
|
286
283
|
};
|
|
287
284
|
|
|
288
|
-
const NullComponent: React.FC = () =>
|
|
285
|
+
const NullComponent: React.FC = () => (
|
|
286
|
+
<Input placeholder={`<${this.context.t?.('Null') ?? 'Null'}>`} readOnly style={{ width: '100%' }} />
|
|
287
|
+
);
|
|
289
288
|
|
|
290
289
|
const RunJSComponent: React.FC<any> = (inputProps: any) => {
|
|
291
290
|
return (
|
|
@@ -298,46 +297,18 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
298
297
|
);
|
|
299
298
|
};
|
|
300
299
|
|
|
301
|
-
const
|
|
302
|
-
renderInputComponent: (meta: any) => {
|
|
303
|
-
const firstPath = meta?.paths?.[0];
|
|
304
|
-
if (firstPath === 'constant') return ConstantValueEditor as any;
|
|
305
|
-
if (firstPath === 'null') return NullComponent as any;
|
|
306
|
-
if (firstPath === 'runjs') return RunJSComponent as any;
|
|
307
|
-
return undefined;
|
|
308
|
-
},
|
|
309
|
-
resolveValueFromPath: (item: any) => {
|
|
310
|
-
const firstPath = item?.paths?.[0];
|
|
311
|
-
if (firstPath === 'constant') return '';
|
|
312
|
-
if (firstPath === 'null') return null;
|
|
313
|
-
if (firstPath === 'runjs') return { code: '', version: 'v2' };
|
|
314
|
-
return undefined;
|
|
315
|
-
},
|
|
316
|
-
resolvePathFromValue: (currentValue: any) => {
|
|
317
|
-
if (currentValue === null) return ['null'];
|
|
318
|
-
if (isRunJSValue(currentValue)) return ['runjs'];
|
|
319
|
-
return isVariableExpression(currentValue) ? parseValueToPath(currentValue) : ['constant'];
|
|
320
|
-
},
|
|
321
|
-
} as any;
|
|
322
|
-
|
|
323
|
-
// 合并变量树:在最前面追加“常量/空值”两个选项
|
|
324
|
-
const mergedMetaTree = async () => {
|
|
300
|
+
const baseMetaTree = async () => {
|
|
325
301
|
const getTree = (this.context as any)?.getPropertyMetaTree;
|
|
326
|
-
|
|
327
|
-
return [
|
|
328
|
-
{
|
|
329
|
-
title: tExpr('Constant'),
|
|
330
|
-
name: 'constant',
|
|
331
|
-
type: 'string',
|
|
332
|
-
paths: ['constant'],
|
|
333
|
-
render: ConstantValueEditor,
|
|
334
|
-
},
|
|
335
|
-
{ title: tExpr('Null'), name: 'null', type: 'object', paths: ['null'], render: NullComponent },
|
|
336
|
-
{ title: tExpr('RunJS'), name: 'runjs', type: 'object', paths: ['runjs'], render: RunJSComponent },
|
|
337
|
-
...base,
|
|
338
|
-
];
|
|
302
|
+
return typeof getTree === 'function' ? await getTree() : [];
|
|
339
303
|
};
|
|
340
304
|
|
|
305
|
+
const targetCollectionField = collection?.getField?.(this.fieldPath);
|
|
306
|
+
const targetFieldInterface = getFieldInterface(targetCollectionField);
|
|
307
|
+
const dateLike = isDateLikeField(targetCollectionField, this.fieldPath?.split('.').slice(-1)[0]);
|
|
308
|
+
const dateComponentProps = dateLike
|
|
309
|
+
? resolveDateVariableComponentProps(targetCollectionField, targetFieldInterface)
|
|
310
|
+
: DEFAULT_DATE_VARIABLE_COMPONENT_PROPS;
|
|
311
|
+
|
|
341
312
|
// 计算 label:优先使用配置中的 label,其次集合字段标题,最后回退字段路径
|
|
342
313
|
let labelText = this.props?.label ?? this.fieldPath ?? '';
|
|
343
314
|
const cf = collection?.getField?.(this.fieldPath);
|
|
@@ -357,14 +328,18 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
357
328
|
const formValue = formBindingProps?.__assign_value__;
|
|
358
329
|
const mergedValue = typeof formValue === 'undefined' ? this.assignValue : formValue;
|
|
359
330
|
return (
|
|
360
|
-
<
|
|
331
|
+
<FieldValueVariableInput
|
|
361
332
|
value={mergedValue}
|
|
362
|
-
onChange={(v
|
|
333
|
+
onChange={(v) => {
|
|
363
334
|
this.assignValue = v;
|
|
364
335
|
formBindingProps?.__assign_trigger__?.(v);
|
|
365
336
|
}}
|
|
366
|
-
|
|
367
|
-
|
|
337
|
+
baseMetaTree={baseMetaTree}
|
|
338
|
+
constantComponent={ConstantValueEditor}
|
|
339
|
+
nullComponent={NullComponent}
|
|
340
|
+
runJSComponent={RunJSComponent}
|
|
341
|
+
isDateLikeField={dateLike}
|
|
342
|
+
dateComponentProps={dateComponentProps}
|
|
368
343
|
clearValue={''}
|
|
369
344
|
/>
|
|
370
345
|
);
|
|
@@ -77,6 +77,10 @@ export class FormBlockModel<
|
|
|
77
77
|
> extends CollectionBlockModel<T> {
|
|
78
78
|
formValueRuntime?: FormValueRuntime;
|
|
79
79
|
|
|
80
|
+
serialize() {
|
|
81
|
+
return { ...super.serialize(), variableContractType: { type: 'form', use: this.use } };
|
|
82
|
+
}
|
|
83
|
+
|
|
80
84
|
private userModifiedTopLevelFields = new Set<string>();
|
|
81
85
|
|
|
82
86
|
get form() {
|
|
@@ -21,6 +21,10 @@ export type DefaultFormGridStructure = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export class FormGridModel<T extends DefaultFormGridStructure = DefaultFormGridStructure> extends GridModel<T> {
|
|
24
|
+
serialize() {
|
|
25
|
+
return { ...super.serialize(), variableContractType: { type: 'formGrid', use: this.use } };
|
|
26
|
+
}
|
|
27
|
+
|
|
24
28
|
itemFallback = (<Skeleton.Input block size="small" style={{ marginBottom: '0.5rem' }} />);
|
|
25
29
|
itemSettingsMenuLevel = 2;
|
|
26
30
|
itemFlowSettings = {
|
|
@@ -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();
|