@nocobase/client-v2 2.3.0-alpha.1 → 2.3.0-beta.11
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/APIClient.d.ts +12 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +12 -2
- package/es/components/form/ScanInput/zxingWasmDecoder.d.ts +9 -0
- package/es/flow/actions/linkageRules.d.ts +25 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +6 -30
- package/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +32 -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/components/filter/VariableFilterItem.d.ts +7 -0
- package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
- package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
- package/es/flow/models/base/ActionModelCore.d.ts +4 -2
- package/es/flow/models/base/BlockGridModel.d.ts +3 -0
- package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
- package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
- package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
- package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
- package/es/flow/models/blocks/table/utils.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
- package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +148 -145
- package/es/ui-operation/index.d.ts +15 -0
- package/es/ui-operation/ui-operation-codec.d.ts +10 -0
- package/lib/index.js +170 -167
- package/lib/locale/languageCodes.js +1 -0
- package/package.json +10 -8
- package/src/APIClient.ts +92 -0
- package/src/Application.tsx +3 -1
- package/src/__tests__/APIClient.test.tsx +58 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
- package/src/__tests__/settings-center.test.tsx +270 -6
- package/src/acl/ACLProvider.tsx +2 -0
- package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
- package/src/collection-manager/__tests__/field-configure.test.ts +52 -0
- package/src/collection-manager/field-configure.ts +2 -2
- package/src/components/form/ScanInput/CodeScanner.tsx +64 -35
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +107 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +294 -24
- package/src/components/form/ScanInput/__tests__/zxingWasmDecoder.test.ts +78 -0
- package/src/components/form/ScanInput/useCodeScanner.ts +281 -23
- package/src/components/form/ScanInput/zxingWasmDecoder.ts +64 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +4 -2
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +17 -0
- package/src/components/form/table/Table.tsx +81 -6
- package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
- package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
- package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
- package/src/flow/actions/__tests__/validation.test.ts +48 -0
- package/src/flow/actions/linkageRules.tsx +59 -16
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
- package/src/flow/common/Markdown/style.ts +4 -0
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/components/FieldAssignValueInput.tsx +42 -621
- package/src/flow/components/FlowRoute.tsx +71 -29
- package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +335 -0
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +389 -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/components/filter/VariableFilterItem.tsx +23 -6
- package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
- package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
- package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
- package/src/flow/index.ts +2 -0
- package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
- package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
- package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
- package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
- package/src/flow/models/base/ActionModelCore.tsx +11 -2
- package/src/flow/models/base/BlockGridModel.tsx +26 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
- package/src/flow/models/base/GridModel.tsx +0 -1
- package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
- package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +36 -50
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
- package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
- package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
- package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
- package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
- package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +73 -5
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +153 -2
- package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
- package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -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/blocks/js-block/JSBlock.tsx +1 -1
- package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +28 -2
- package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
- 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/blocks/table/utils.ts +7 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +12 -3
- 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/RecordPickerFieldModel.tsx +29 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +5 -1
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +191 -3
- package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -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/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
- package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
- package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
- package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
- package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
- package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
- package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
- package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
- package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
- package/src/flow/models/topbar/TopbarActionModel.tsx +5 -5
- package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
- package/src/index.ts +1 -0
- package/src/layout-manager/LayoutContentRoute.tsx +2 -1
- package/src/layout-manager/LayoutRoute.tsx +2 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
- package/src/locale/languageCodes.ts +1 -0
- package/src/settings-center/AdminSettingsLayout.tsx +23 -2
- package/src/ui-operation/index.ts +33 -0
- package/src/ui-operation/ui-operation-codec.ts +54 -0
|
@@ -77,6 +77,7 @@ async function setupFormModel() {
|
|
|
77
77
|
ds.addCollection({
|
|
78
78
|
name: 'levels',
|
|
79
79
|
filterTargetKey: 'id',
|
|
80
|
+
titleField: 'name',
|
|
80
81
|
fields: [
|
|
81
82
|
{ name: 'id', type: 'integer', interface: 'number' },
|
|
82
83
|
{ name: 'name', type: 'string', interface: 'text' },
|
|
@@ -465,11 +466,17 @@ describe('FormBlockModel (form/formValues injection & server resolve anchors)',
|
|
|
465
466
|
|
|
466
467
|
it('builds non-empty contextParams for ctx.formValues.* deep association path', async () => {
|
|
467
468
|
const model = await setupFormModel();
|
|
469
|
+
const sessionPayload = Buffer.from(JSON.stringify({ userId: 1, signInTime: 'form-record-slots' })).toString(
|
|
470
|
+
'base64url',
|
|
471
|
+
);
|
|
468
472
|
// 注入 api mock 到引擎上下文,拦截 variables:resolve 的请求
|
|
469
473
|
const api = {
|
|
474
|
+
auth: { token: `test.${sessionPayload}.sig` },
|
|
470
475
|
request: vi.fn(async (config: any) => {
|
|
471
|
-
const
|
|
472
|
-
const batch =
|
|
476
|
+
const requestValues = config?.data?.values || {};
|
|
477
|
+
const batch = requestValues.batch || [];
|
|
478
|
+
expect(batch[0]?.rd).toEqual(expect.any(String));
|
|
479
|
+
expect(batch[0]?.template).toEqual({ who: '{{ ctx.formValues.assignees.org.name }}' });
|
|
473
480
|
const cp = batch[0]?.contextParams || {};
|
|
474
481
|
const keys = Object.keys(cp).sort();
|
|
475
482
|
// 聚合为单键,不再使用索引键
|
|
@@ -543,6 +550,150 @@ describe('FormBlockModel (form/formValues injection & server resolve anchors)',
|
|
|
543
550
|
expect(api.request).toHaveBeenCalledTimes(1);
|
|
544
551
|
});
|
|
545
552
|
|
|
553
|
+
it('resolves a configured nested association from the server when the local value is only its key', async () => {
|
|
554
|
+
const model = await setupFormModel();
|
|
555
|
+
const api = {
|
|
556
|
+
request: vi.fn(async (config: any) => {
|
|
557
|
+
const batch = config?.data?.values?.batch || [];
|
|
558
|
+
const item = batch[0] || {};
|
|
559
|
+
expect(item.template).toEqual({ level: '{{ ctx.formValues.customer.level }}' });
|
|
560
|
+
expect(Object.keys(item.contextParams || {})).toEqual(['formValues.customer']);
|
|
561
|
+
expect(item.contextParams['formValues.customer']).toMatchObject({
|
|
562
|
+
collection: 'customers',
|
|
563
|
+
filterByTk: 9,
|
|
564
|
+
});
|
|
565
|
+
return {
|
|
566
|
+
data: {
|
|
567
|
+
data: {
|
|
568
|
+
results: [{ id: item.id, data: { level: { id: 'level-1', name: 'Level 1' } } }],
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
} as any;
|
|
572
|
+
}),
|
|
573
|
+
} as any;
|
|
574
|
+
(model.flowEngine.context as any).defineProperty('api', { value: api });
|
|
575
|
+
|
|
576
|
+
function HookCaller() {
|
|
577
|
+
model.useHooksBeforeRender();
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
render(React.createElement(HookCaller));
|
|
581
|
+
|
|
582
|
+
const mem: Record<string, any> = {};
|
|
583
|
+
const fakeForm = {
|
|
584
|
+
setFieldsValue: (values: Record<string, any>) => Object.assign(mem, values),
|
|
585
|
+
getFieldsValue: () => ({ ...mem }),
|
|
586
|
+
getFieldValue: (namePath: any) => getByPath(mem, namePath),
|
|
587
|
+
setFieldValue: (key: string, value: any) => (mem[key] = value),
|
|
588
|
+
};
|
|
589
|
+
(model.context as any).defineProperty('form', { value: fakeForm });
|
|
590
|
+
fakeForm.setFieldsValue({ customer: { id: 9, level: 'level-1' } });
|
|
591
|
+
mockFormGridEnabledFields(model, ['customer']);
|
|
592
|
+
|
|
593
|
+
const output = await (model.context as any).resolveJsonTemplate({
|
|
594
|
+
level: '{{ ctx.formValues.customer.level }}',
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
expect(api.request).toHaveBeenCalledTimes(1);
|
|
598
|
+
expect(output).toEqual({ level: { id: 'level-1', name: 'Level 1' } });
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('resolves a configured nested association when its local record does not include the title field', async () => {
|
|
602
|
+
const model = await setupFormModel();
|
|
603
|
+
const api = {
|
|
604
|
+
request: vi.fn(async (config: any) => {
|
|
605
|
+
const item = config?.data?.values?.batch?.[0] || {};
|
|
606
|
+
return {
|
|
607
|
+
data: {
|
|
608
|
+
data: {
|
|
609
|
+
results: [{ id: item.id, data: { level: { id: 'level-1', name: 'Level 1' } } }],
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
} as any;
|
|
613
|
+
}),
|
|
614
|
+
} as any;
|
|
615
|
+
(model.flowEngine.context as any).defineProperty('api', { value: api });
|
|
616
|
+
|
|
617
|
+
function HookCaller() {
|
|
618
|
+
model.useHooksBeforeRender();
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
render(React.createElement(HookCaller));
|
|
622
|
+
|
|
623
|
+
const mem: Record<string, any> = {};
|
|
624
|
+
const fakeForm = {
|
|
625
|
+
setFieldsValue: (values: Record<string, any>) => Object.assign(mem, values),
|
|
626
|
+
getFieldsValue: () => ({ ...mem }),
|
|
627
|
+
getFieldValue: (namePath: any) => getByPath(mem, namePath),
|
|
628
|
+
setFieldValue: (key: string, value: any) => (mem[key] = value),
|
|
629
|
+
};
|
|
630
|
+
(model.context as any).defineProperty('form', { value: fakeForm });
|
|
631
|
+
fakeForm.setFieldsValue({ customer: { id: 9, level: { id: 'level-1' } } });
|
|
632
|
+
mockFormGridEnabledFields(model, ['customer']);
|
|
633
|
+
|
|
634
|
+
const output = await (model.context as any).resolveJsonTemplate({
|
|
635
|
+
level: '{{ ctx.formValues.customer.level }}',
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
expect(api.request).toHaveBeenCalledTimes(1);
|
|
639
|
+
expect(output).toEqual({ level: { id: 'level-1', name: 'Level 1' } });
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('uses a configured nested association locally when its record is already loaded', async () => {
|
|
643
|
+
const model = await setupFormModel();
|
|
644
|
+
const api = { request: vi.fn(async () => ({ data: {} }) as any) } as any;
|
|
645
|
+
(model.flowEngine.context as any).defineProperty('api', { value: api });
|
|
646
|
+
|
|
647
|
+
function HookCaller() {
|
|
648
|
+
model.useHooksBeforeRender();
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
render(React.createElement(HookCaller));
|
|
652
|
+
|
|
653
|
+
const mem: Record<string, any> = {};
|
|
654
|
+
const fakeForm = {
|
|
655
|
+
setFieldsValue: (values: Record<string, any>) => Object.assign(mem, values),
|
|
656
|
+
getFieldsValue: () => ({ ...mem }),
|
|
657
|
+
getFieldValue: (namePath: any) => getByPath(mem, namePath),
|
|
658
|
+
setFieldValue: (key: string, value: any) => (mem[key] = value),
|
|
659
|
+
};
|
|
660
|
+
(model.context as any).defineProperty('form', { value: fakeForm });
|
|
661
|
+
const level = { id: 'level-1', name: 'Level 1' };
|
|
662
|
+
fakeForm.setFieldsValue({ customer: { id: 9, level } });
|
|
663
|
+
mockFormGridEnabledFields(model, ['customer']);
|
|
664
|
+
|
|
665
|
+
const output = await (model.context as any).resolveJsonTemplate({
|
|
666
|
+
level: '{{ ctx.formValues.customer.level }}',
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
expect(output).toEqual({ level });
|
|
670
|
+
expect(api.request).not.toHaveBeenCalled();
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('keeps indexed paths local when the nested association record is already loaded', async () => {
|
|
674
|
+
const model = await setupFormModel();
|
|
675
|
+
|
|
676
|
+
function HookCaller() {
|
|
677
|
+
model.useHooksBeforeRender();
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
render(React.createElement(HookCaller));
|
|
681
|
+
|
|
682
|
+
const mem: Record<string, any> = {};
|
|
683
|
+
const fakeForm = {
|
|
684
|
+
setFieldsValue: (values: Record<string, any>) => Object.assign(mem, values),
|
|
685
|
+
getFieldsValue: () => ({ ...mem }),
|
|
686
|
+
getFieldValue: (namePath: any) => getByPath(mem, namePath),
|
|
687
|
+
setFieldValue: (key: string, value: any) => (mem[key] = value),
|
|
688
|
+
};
|
|
689
|
+
(model.context as any).defineProperty('form', { value: fakeForm });
|
|
690
|
+
fakeForm.setFieldsValue({ assignees: [{ id: 3, org: { id: 1, name: 'Org 1' } }] });
|
|
691
|
+
mockFormGridEnabledFields(model, ['assignees']);
|
|
692
|
+
|
|
693
|
+
const options = (model.context as any).getPropertyOptions('formValues');
|
|
694
|
+
expect(options.resolveOnServer('assignees[0].org.name')).toBe(false);
|
|
695
|
+
});
|
|
696
|
+
|
|
546
697
|
it('configured toMany dot aggregation path uses local value and skips server', async () => {
|
|
547
698
|
const model = await setupFormModel();
|
|
548
699
|
|
|
@@ -0,0 +1,175 @@
|
|
|
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 React from 'react';
|
|
11
|
+
import { act, render, screen, waitFor, cleanup } from '@testing-library/react';
|
|
12
|
+
import { Form, Input } from 'antd';
|
|
13
|
+
import { afterEach, expect, it, vi } from 'vitest';
|
|
14
|
+
import { FlowEngine, SingleRecordResource } from '@nocobase/flow-engine';
|
|
15
|
+
import { generateFlowModelRdFromToken } from '@nocobase/utils/client';
|
|
16
|
+
import { CreateFormModel, EditFormModel, FormGridModel, FormItemModel, FormComponent } from '../../../..';
|
|
17
|
+
import { fieldLinkageRules, linkageAssignField } from '../../../../actions/linkageRules';
|
|
18
|
+
|
|
19
|
+
afterEach(cleanup);
|
|
20
|
+
|
|
21
|
+
it.each([
|
|
22
|
+
{ use: 'CreateFormModel', mode: 'default', expected: 'STAFF-001' },
|
|
23
|
+
{ use: 'EditFormModel', mode: 'default', expected: '' },
|
|
24
|
+
{ use: 'EditFormModel', mode: 'override', expected: 'STAFF-001' },
|
|
25
|
+
])('$use respects $mode linkage after a delayed popup variable response', async ({ use, mode, expected }) => {
|
|
26
|
+
const uid = `popup-${use}-${mode}`;
|
|
27
|
+
const engine = new FlowEngine();
|
|
28
|
+
engine.registerModels({ CreateFormModel, EditFormModel, FormGridModel, FormItemModel });
|
|
29
|
+
engine.registerActions({ fieldLinkageRules, linkageAssignField });
|
|
30
|
+
engine.context.dataSourceManager.getDataSource('main').addCollection({
|
|
31
|
+
name: 't1_user',
|
|
32
|
+
filterTargetKey: 'id',
|
|
33
|
+
fields: [
|
|
34
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
35
|
+
{ name: 'staffseq', type: 'string', interface: 'input' },
|
|
36
|
+
{ name: 'staffname', type: 'string', interface: 'input' },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
const form = engine.createModel<CreateFormModel | EditFormModel>({
|
|
40
|
+
uid,
|
|
41
|
+
use,
|
|
42
|
+
stepParams: { resourceSettings: { init: { dataSourceKey: 'main', collectionName: 't1_user', filterByTk: 1 } } },
|
|
43
|
+
subModels: {
|
|
44
|
+
grid: {
|
|
45
|
+
uid: `${uid}-grid`,
|
|
46
|
+
use: 'FormGridModel',
|
|
47
|
+
subModels: {
|
|
48
|
+
items: [
|
|
49
|
+
{
|
|
50
|
+
uid: `${uid}-staffname`,
|
|
51
|
+
use: 'FormItemModel',
|
|
52
|
+
props: { name: 'staffname' },
|
|
53
|
+
stepParams: {
|
|
54
|
+
fieldSettings: { init: { dataSourceKey: 'main', collectionName: 't1_user', fieldPath: 'staffname' } },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const configured = '{{ ctx.popup.record.staffseq }}';
|
|
63
|
+
const rules = {
|
|
64
|
+
value: [
|
|
65
|
+
{
|
|
66
|
+
key: 'rule',
|
|
67
|
+
title: 'Rule',
|
|
68
|
+
enable: true,
|
|
69
|
+
condition: { logic: '$and', items: [] },
|
|
70
|
+
actions: [
|
|
71
|
+
{
|
|
72
|
+
key: 'assign',
|
|
73
|
+
name: 'linkageAssignField',
|
|
74
|
+
params: {
|
|
75
|
+
value: [
|
|
76
|
+
{
|
|
77
|
+
key: 'field',
|
|
78
|
+
enable: true,
|
|
79
|
+
mode,
|
|
80
|
+
condition: { logic: '$and', items: [] },
|
|
81
|
+
targetPath: 'staffname',
|
|
82
|
+
value: configured,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
form.setStepParams('eventSettings', 'linkageRules', rules);
|
|
92
|
+
const saved = JSON.parse(JSON.stringify(form.serialize()));
|
|
93
|
+
expect(saved.subModels.grid.stepParams.eventSettings.linkageRules).toEqual(rules);
|
|
94
|
+
expect(saved.stepParams.eventSettings?.linkageRules).toBeUndefined();
|
|
95
|
+
|
|
96
|
+
const token = `test.${Buffer.from(JSON.stringify({ userId: 2, signInTime: 'member-form' })).toString('base64url')}.sig`;
|
|
97
|
+
type Item = { id: string; rd: string; template: unknown; contextParams: unknown };
|
|
98
|
+
let resolveResponse: (() => void) | undefined;
|
|
99
|
+
const responseReady = new Promise<void>((resolve) => {
|
|
100
|
+
resolveResponse = resolve;
|
|
101
|
+
});
|
|
102
|
+
const request = vi.fn(async ({ data }: { data: { values: { batch: Item[] } } }) => {
|
|
103
|
+
const batch = data.values.batch;
|
|
104
|
+
expect(batch[0].rd).toBe(generateFlowModelRdFromToken(form.uid, token));
|
|
105
|
+
expect(batch[0].contextParams).toEqual({
|
|
106
|
+
'popup.record': { collection: 't1_user', dataSourceKey: 'main', filterByTk: '1' },
|
|
107
|
+
});
|
|
108
|
+
await responseReady;
|
|
109
|
+
return {
|
|
110
|
+
data: {
|
|
111
|
+
data: {
|
|
112
|
+
results: batch.map((item) => ({
|
|
113
|
+
id: item.id,
|
|
114
|
+
data: JSON.parse(JSON.stringify(item.template).replaceAll(configured, 'STAFF-001')),
|
|
115
|
+
})),
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
engine.context.defineProperty('api', { value: { auth: { token, role: 'member' }, request } });
|
|
121
|
+
form.context.defineProperty('popup', {
|
|
122
|
+
value: { record: { id: 1 } },
|
|
123
|
+
resolveOnServer: true,
|
|
124
|
+
meta: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
buildVariablesParams: () => ({ record: { collection: 't1_user', dataSourceKey: 'main', filterByTk: '1' } }),
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
const resource = form.resource as SingleRecordResource;
|
|
130
|
+
resource.setData(use === 'EditFormModel' ? { id: 1, staffname: null } : {});
|
|
131
|
+
function View() {
|
|
132
|
+
form.useHooksBeforeRender();
|
|
133
|
+
return (
|
|
134
|
+
<FormComponent model={form}>
|
|
135
|
+
<Form.Item name="staffname">
|
|
136
|
+
<Input aria-label="staffname" />
|
|
137
|
+
</Form.Item>
|
|
138
|
+
</FormComponent>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const dispatchEvent = vi.spyOn(form, 'dispatchEvent');
|
|
142
|
+
const view = render(<View />);
|
|
143
|
+
try {
|
|
144
|
+
form.formValueRuntime?.mount({ sync: true });
|
|
145
|
+
// Simulate the field mounting, without involving the page layout and field-renderer plugins.
|
|
146
|
+
engine.emitter.emit('model:mounted', { model: form.subModels.grid.subModels.items[0] });
|
|
147
|
+
let pending: Promise<unknown> | undefined;
|
|
148
|
+
await act(async () => {
|
|
149
|
+
pending = form.applyFlow('eventSettings');
|
|
150
|
+
});
|
|
151
|
+
await waitFor(() => expect(request).toHaveBeenCalled());
|
|
152
|
+
await act(async () => {
|
|
153
|
+
resolveResponse?.();
|
|
154
|
+
await pending;
|
|
155
|
+
});
|
|
156
|
+
if (mode === 'default') {
|
|
157
|
+
// Resolving a default value must not overwrite an existing edit record.
|
|
158
|
+
expect(form.subModels.grid.subModels.items[0].props.initialValue).toBe('STAFF-001');
|
|
159
|
+
}
|
|
160
|
+
await waitFor(() => expect((screen.getByLabelText('staffname') as HTMLInputElement).value).toBe(expected));
|
|
161
|
+
expect(form.form.getFieldValue('staffname')).toBe(expected || null);
|
|
162
|
+
} finally {
|
|
163
|
+
resolveResponse?.();
|
|
164
|
+
await act(async () => {
|
|
165
|
+
view.unmount();
|
|
166
|
+
form.formValueRuntime?.dispose();
|
|
167
|
+
// Drain automatic linkage refreshes before another engine starts its variable batch.
|
|
168
|
+
await Promise.all(
|
|
169
|
+
dispatchEvent.mock.results.filter((result) => result.type === 'return').map((result) => result.value),
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
dispatchEvent.mockRestore();
|
|
173
|
+
engine.disposeScheduler();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
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 { CreateFormModel } from '../CreateFormModel';
|
|
13
|
+
import { EditFormModel } from '../EditFormModel';
|
|
14
|
+
import { FormActionModel } from '../FormActionModel';
|
|
15
|
+
|
|
16
|
+
function prepareFormModel<T extends CreateFormModel | EditFormModel>(model: T, publicFormRuntime = false) {
|
|
17
|
+
const engine = new FlowEngine();
|
|
18
|
+
engine.registerModels({ CreateFormModel, EditFormModel });
|
|
19
|
+
model.flowEngine = engine;
|
|
20
|
+
Object.defineProperty(model, 'context', {
|
|
21
|
+
value: {
|
|
22
|
+
publicFormRuntime,
|
|
23
|
+
message: { error: vi.fn() },
|
|
24
|
+
t: (value: string) => value,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return model;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createSubmitAction() {
|
|
31
|
+
const onClick = vi.fn();
|
|
32
|
+
const action = {
|
|
33
|
+
getFlow: vi.fn((key: string) => (key === 'submitSettings' ? {} : undefined)),
|
|
34
|
+
onClick,
|
|
35
|
+
} as unknown as FormActionModel;
|
|
36
|
+
return { action, onClick };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('FormBlockModel.submitFromRunJs', () => {
|
|
40
|
+
it('dispatches the first action with a submitSettings flow', () => {
|
|
41
|
+
const { action, onClick } = createSubmitAction();
|
|
42
|
+
const { action: secondAction, onClick: secondOnClick } = createSubmitAction();
|
|
43
|
+
const blockModel = prepareFormModel(
|
|
44
|
+
Object.assign(Object.create(CreateFormModel.prototype), {
|
|
45
|
+
mapSubModels: (_key: string, callback: (item: FormActionModel) => FormActionModel) => [
|
|
46
|
+
callback(action),
|
|
47
|
+
callback(secondAction),
|
|
48
|
+
],
|
|
49
|
+
}) as CreateFormModel,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
blockModel.submitFromRunJs();
|
|
53
|
+
|
|
54
|
+
expect(onClick).toHaveBeenCalledWith(undefined);
|
|
55
|
+
expect(secondOnClick).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not dispatch an edit submit action without an available record', () => {
|
|
59
|
+
const { action, onClick } = createSubmitAction();
|
|
60
|
+
const blockModel = prepareFormModel(
|
|
61
|
+
Object.assign(Object.create(EditFormModel.prototype), {
|
|
62
|
+
hasAvailableData: () => false,
|
|
63
|
+
mapSubModels: (_key: string, callback: (item: FormActionModel) => FormActionModel) => [callback(action)],
|
|
64
|
+
}) as EditFormModel,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
blockModel.submitFromRunJs();
|
|
68
|
+
|
|
69
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('falls back to the core form submit handler when no submit action exists', () => {
|
|
73
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
74
|
+
const blockModel = prepareFormModel(
|
|
75
|
+
Object.assign(Object.create(CreateFormModel.prototype), {
|
|
76
|
+
mapSubModels: vi.fn(() => []),
|
|
77
|
+
submit,
|
|
78
|
+
}) as CreateFormModel,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
blockModel.submitFromRunJs();
|
|
82
|
+
|
|
83
|
+
expect(submit).toHaveBeenCalledOnce();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('falls back to the core edit submit handler when a record exists', () => {
|
|
87
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
88
|
+
const blockModel = prepareFormModel(
|
|
89
|
+
Object.assign(Object.create(EditFormModel.prototype), {
|
|
90
|
+
hasAvailableData: () => true,
|
|
91
|
+
mapSubModels: vi.fn(() => []),
|
|
92
|
+
submit,
|
|
93
|
+
}) as EditFormModel,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
blockModel.submitFromRunJs();
|
|
97
|
+
|
|
98
|
+
expect(submit).toHaveBeenCalledOnce();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('uses the core fallback for specialized form subclasses', () => {
|
|
102
|
+
class SpecializedCreateFormModel extends CreateFormModel {}
|
|
103
|
+
|
|
104
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
105
|
+
const blockModel = prepareFormModel(
|
|
106
|
+
Object.assign(Object.create(SpecializedCreateFormModel.prototype), {
|
|
107
|
+
mapSubModels: vi.fn(() => []),
|
|
108
|
+
submit,
|
|
109
|
+
}) as SpecializedCreateFormModel,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
blockModel.submitFromRunJs();
|
|
113
|
+
|
|
114
|
+
expect(submit).toHaveBeenCalledOnce();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('does not use the core fallback for public forms without a submit action', () => {
|
|
118
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
119
|
+
const blockModel = prepareFormModel(
|
|
120
|
+
Object.assign(Object.create(CreateFormModel.prototype), {
|
|
121
|
+
mapSubModels: vi.fn(() => []),
|
|
122
|
+
submit,
|
|
123
|
+
}) as CreateFormModel,
|
|
124
|
+
true,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
blockModel.submitFromRunJs();
|
|
128
|
+
|
|
129
|
+
expect(submit).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
});
|