@nocobase/client-v2 2.1.0-beta.29 → 2.1.0-beta.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/BaseApplication.d.ts +1 -0
- package/es/PluginManager.d.ts +1 -0
- package/es/components/form/DrawerFormLayout.d.ts +49 -0
- package/es/components/form/EnvVariableInput.d.ts +42 -0
- package/es/components/form/FileSizeInput.d.ts +27 -0
- package/es/components/form/createFormRegistry.d.ts +33 -0
- package/es/components/form/index.d.ts +13 -0
- package/es/components/index.d.ts +1 -1
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +4 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +5 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +1 -0
- package/es/flow/internal/utils/enumOptionsUtils.d.ts +5 -0
- package/es/flow/models/actions/AssociationActionUtils.d.ts +5 -0
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/filter-manager/FilterManager.d.ts +5 -1
- package/es/flow/models/blocks/table/TableSelectModel.d.ts +8 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +3 -0
- package/es/flow/models/fields/DisplayTitleFieldModel.d.ts +1 -1
- package/es/flow/models/utils/displayValueUtils.d.ts +12 -0
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.mjs +122 -106
- package/es/utils/remotePlugins.d.ts +0 -4
- package/lib/index.js +121 -105
- package/package.json +6 -5
- package/src/BaseApplication.tsx +14 -8
- package/src/PluginManager.ts +1 -0
- package/src/__tests__/app.test.tsx +28 -1
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +67 -46
- package/src/__tests__/remotePlugins.test.ts +29 -18
- package/src/__tests__/settings-center.test.tsx +30 -0
- package/src/components/form/DrawerFormLayout.tsx +103 -0
- package/src/components/form/EnvVariableInput.tsx +126 -0
- package/src/components/form/FileSizeInput.tsx +105 -0
- package/src/components/form/createFormRegistry.ts +60 -0
- package/src/components/form/index.tsx +14 -0
- package/src/components/index.ts +1 -1
- package/src/flow/__tests__/FlowRoute.test.tsx +4 -5
- package/src/flow/actions/__tests__/actionLinkageRules.race.repro.test.ts +199 -0
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +92 -13
- package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +6 -1
- package/src/flow/actions/__tests__/linkageRules.menu.test.ts +90 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +476 -1
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +316 -280
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +2 -8
- package/src/flow/actions/setTargetDataScope.tsx +32 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +8 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +70 -12
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +26 -87
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +11 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +5 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +292 -31
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +50 -12
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +77 -56
- package/src/flow/components/AdminLayout.tsx +2 -2
- package/src/flow/components/FieldAssignRulesEditor.tsx +2 -0
- package/src/flow/components/FlowRoute.tsx +17 -4
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
- package/src/flow/components/filter/LinkageFilterItem.tsx +9 -2
- package/src/flow/components/filter/VariableFilterItem.tsx +2 -6
- package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +71 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -0
- package/src/flow/internal/utils/__tests__/enumOptionsUtils.test.ts +10 -1
- package/src/flow/internal/utils/enumOptionsUtils.ts +29 -0
- package/src/flow/models/actions/AssociateActionModel.tsx +2 -2
- package/src/flow/models/actions/AssociationActionUtils.ts +14 -0
- package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +63 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +7 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +33 -9
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +53 -13
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +63 -3
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +33 -1
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +66 -2
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +270 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +8 -5
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +30 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +6 -1
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +6 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +11 -6
- package/src/flow/models/blocks/table/TableColumnModel.tsx +3 -0
- package/src/flow/models/blocks/table/TableSelectModel.tsx +36 -26
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowClick.test.ts +69 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +96 -1
- package/src/flow/models/blocks/table/__tests__/TableSelectModel.test.ts +41 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +7 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +4 -1
- package/src/flow/models/fields/ClickableFieldModel.tsx +9 -4
- package/src/flow/models/fields/DisplayTitleFieldModel.tsx +12 -4
- package/src/flow/models/fields/SelectFieldModel.tsx +31 -1
- package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +23 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +2 -1
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +7 -0
- package/src/flow/models/utils/displayValueUtils.ts +57 -0
- package/src/flow/system-settings/useSystemSettings.tsx +36 -1
- package/src/utils/globalDeps.ts +2 -0
- package/src/utils/remotePlugins.ts +7 -27
|
@@ -243,6 +243,125 @@ describe('FilterManager', () => {
|
|
|
243
243
|
});
|
|
244
244
|
});
|
|
245
245
|
|
|
246
|
+
describe('prepareFiltersForTarget', () => {
|
|
247
|
+
it('should prepare filter form initial values before target refresh', async () => {
|
|
248
|
+
(filterManager as any).filterConfigs = [
|
|
249
|
+
{
|
|
250
|
+
filterId: 'filter1',
|
|
251
|
+
targetId: 'target1',
|
|
252
|
+
filterPaths: ['name'],
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
const prepareInitialFilterValues = vi.fn().mockResolvedValue(undefined);
|
|
256
|
+
const filterBlockModel = { prepareInitialFilterValues };
|
|
257
|
+
const filterModel = {
|
|
258
|
+
context: {
|
|
259
|
+
blockModel: filterBlockModel,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
(mockFlowModel.flowEngine.getModel as any).mockImplementation((uid: string) => {
|
|
264
|
+
if (uid === 'filter1') return filterModel;
|
|
265
|
+
return null;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const prepared = await filterManager.prepareFiltersForTarget('target1');
|
|
269
|
+
|
|
270
|
+
expect(prepareInitialFilterValues).toHaveBeenCalledTimes(1);
|
|
271
|
+
expect(prepared.has(filterBlockModel)).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should skip marking a filter block when initial values are not ready', async () => {
|
|
275
|
+
(filterManager as any).filterConfigs = [
|
|
276
|
+
{
|
|
277
|
+
filterId: 'filter1',
|
|
278
|
+
targetId: 'target1',
|
|
279
|
+
filterPaths: ['name'],
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
const filterBlockModel = { prepareInitialFilterValues: vi.fn().mockResolvedValue(false) };
|
|
283
|
+
const filterModel = {
|
|
284
|
+
context: {
|
|
285
|
+
blockModel: filterBlockModel,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
(mockFlowModel.flowEngine.getModel as any).mockImplementation((uid: string) => {
|
|
290
|
+
if (uid === 'filter1') return filterModel;
|
|
291
|
+
return null;
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const prepared = await filterManager.prepareFiltersForTarget('target1');
|
|
295
|
+
|
|
296
|
+
expect(prepared.has(filterBlockModel)).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should continue preparing other filter blocks when one fails', async () => {
|
|
300
|
+
(filterManager as any).filterConfigs = [
|
|
301
|
+
{
|
|
302
|
+
filterId: 'filter1',
|
|
303
|
+
targetId: 'target1',
|
|
304
|
+
filterPaths: ['name'],
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
filterId: 'filter2',
|
|
308
|
+
targetId: 'target1',
|
|
309
|
+
filterPaths: ['title'],
|
|
310
|
+
},
|
|
311
|
+
];
|
|
312
|
+
const failingBlockModel = { prepareInitialFilterValues: vi.fn().mockRejectedValue(new Error('bad default')) };
|
|
313
|
+
const preparedBlockModel = { prepareInitialFilterValues: vi.fn().mockResolvedValue(true) };
|
|
314
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
315
|
+
|
|
316
|
+
(mockFlowModel.flowEngine.getModel as any).mockImplementation((uid: string) => {
|
|
317
|
+
if (uid === 'filter1') return { context: { blockModel: failingBlockModel } };
|
|
318
|
+
if (uid === 'filter2') return { context: { blockModel: preparedBlockModel } };
|
|
319
|
+
return null;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const prepared = await filterManager.prepareFiltersForTarget('target1');
|
|
323
|
+
|
|
324
|
+
expect(prepared.has(failingBlockModel)).toBe(false);
|
|
325
|
+
expect(prepared.has(preparedBlockModel)).toBe(true);
|
|
326
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
327
|
+
errorSpy.mockRestore();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should keep error isolation when a filter block rejects a non-error value', async () => {
|
|
331
|
+
(filterManager as any).filterConfigs = [
|
|
332
|
+
{
|
|
333
|
+
filterId: 'filter1',
|
|
334
|
+
targetId: 'target1',
|
|
335
|
+
filterPaths: ['name'],
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
filterId: 'filter2',
|
|
339
|
+
targetId: 'target1',
|
|
340
|
+
filterPaths: ['title'],
|
|
341
|
+
},
|
|
342
|
+
];
|
|
343
|
+
const failingBlockModel = { prepareInitialFilterValues: vi.fn().mockRejectedValue('bad default') };
|
|
344
|
+
const preparedBlockModel = { prepareInitialFilterValues: vi.fn().mockResolvedValue(true) };
|
|
345
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
346
|
+
|
|
347
|
+
(mockFlowModel.flowEngine.getModel as any).mockImplementation((uid: string) => {
|
|
348
|
+
if (uid === 'filter1') return { context: { blockModel: failingBlockModel } };
|
|
349
|
+
if (uid === 'filter2') return { context: { blockModel: preparedBlockModel } };
|
|
350
|
+
return null;
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const prepared = await filterManager.prepareFiltersForTarget('target1');
|
|
355
|
+
|
|
356
|
+
expect(prepared.has(failingBlockModel)).toBe(false);
|
|
357
|
+
expect(prepared.has(preparedBlockModel)).toBe(true);
|
|
358
|
+
expect(errorSpy).toHaveBeenCalledWith('Failed to prepare filter defaults for target "target1": bad default');
|
|
359
|
+
} finally {
|
|
360
|
+
errorSpy.mockRestore();
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
246
365
|
describe('addFilterConfig', () => {
|
|
247
366
|
it('should add a new filter config successfully', async () => {
|
|
248
367
|
const filterConfig = {
|
|
@@ -496,6 +615,157 @@ describe('FilterManager', () => {
|
|
|
496
615
|
expect(mockTargetModel2.resource.refresh).toHaveBeenCalledTimes(1);
|
|
497
616
|
});
|
|
498
617
|
|
|
618
|
+
it('should wait for target resource idle before refreshing', async () => {
|
|
619
|
+
vi.useFakeTimers();
|
|
620
|
+
(filterManager as any).filterConfigs = [
|
|
621
|
+
{
|
|
622
|
+
filterId: 'filter-1',
|
|
623
|
+
targetId: 'target-1',
|
|
624
|
+
filterPaths: ['name'],
|
|
625
|
+
operator: '$eq',
|
|
626
|
+
},
|
|
627
|
+
];
|
|
628
|
+
|
|
629
|
+
const resource = {
|
|
630
|
+
loading: true,
|
|
631
|
+
addFilterGroup: vi.fn(),
|
|
632
|
+
removeFilterGroup: vi.fn(),
|
|
633
|
+
refresh: vi.fn().mockImplementation(() => {
|
|
634
|
+
expect(resource.loading).toBe(false);
|
|
635
|
+
return Promise.resolve();
|
|
636
|
+
}),
|
|
637
|
+
};
|
|
638
|
+
const mockTargetModel = {
|
|
639
|
+
resource,
|
|
640
|
+
setFilterActive: vi.fn(),
|
|
641
|
+
getDataLoadingMode: vi.fn().mockReturnValue('auto'),
|
|
642
|
+
};
|
|
643
|
+
const mockFilterModel = {
|
|
644
|
+
getFilterValue: vi.fn().mockReturnValue('test-value'),
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
(mockFlowModel.flowEngine.getModel as any).mockImplementation((uid: string) => {
|
|
648
|
+
if (uid === 'target-1') return mockTargetModel;
|
|
649
|
+
if (uid === 'filter-1') return mockFilterModel;
|
|
650
|
+
return null;
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const refreshPromise = filterManager.refreshTargetsByFilter('filter-1');
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
await vi.advanceTimersByTimeAsync(16);
|
|
657
|
+
expect(resource.refresh).not.toHaveBeenCalled();
|
|
658
|
+
|
|
659
|
+
resource.loading = false;
|
|
660
|
+
await vi.advanceTimersByTimeAsync(16);
|
|
661
|
+
await refreshPromise;
|
|
662
|
+
|
|
663
|
+
expect(resource.refresh).toHaveBeenCalledTimes(1);
|
|
664
|
+
} finally {
|
|
665
|
+
vi.useRealTimers();
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('should not refresh before a slow target resource becomes idle', async () => {
|
|
670
|
+
vi.useFakeTimers();
|
|
671
|
+
(filterManager as any).filterConfigs = [
|
|
672
|
+
{
|
|
673
|
+
filterId: 'filter-1',
|
|
674
|
+
targetId: 'target-1',
|
|
675
|
+
filterPaths: ['name'],
|
|
676
|
+
operator: '$eq',
|
|
677
|
+
},
|
|
678
|
+
];
|
|
679
|
+
|
|
680
|
+
const resource = {
|
|
681
|
+
loading: true,
|
|
682
|
+
addFilterGroup: vi.fn(),
|
|
683
|
+
removeFilterGroup: vi.fn(),
|
|
684
|
+
refresh: vi.fn().mockResolvedValue(undefined),
|
|
685
|
+
};
|
|
686
|
+
const mockTargetModel = {
|
|
687
|
+
resource,
|
|
688
|
+
setFilterActive: vi.fn(),
|
|
689
|
+
getDataLoadingMode: vi.fn().mockReturnValue('auto'),
|
|
690
|
+
};
|
|
691
|
+
const mockFilterModel = {
|
|
692
|
+
getFilterValue: vi.fn().mockReturnValue('test-value'),
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
(mockFlowModel.flowEngine.getModel as any).mockImplementation((uid: string) => {
|
|
696
|
+
if (uid === 'target-1') return mockTargetModel;
|
|
697
|
+
if (uid === 'filter-1') return mockFilterModel;
|
|
698
|
+
return null;
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
const refreshPromise = filterManager.refreshTargetsByFilter('filter-1');
|
|
702
|
+
|
|
703
|
+
try {
|
|
704
|
+
await vi.advanceTimersByTimeAsync(2100);
|
|
705
|
+
|
|
706
|
+
expect(resource.refresh).not.toHaveBeenCalled();
|
|
707
|
+
|
|
708
|
+
resource.loading = false;
|
|
709
|
+
await vi.advanceTimersByTimeAsync(16);
|
|
710
|
+
await refreshPromise;
|
|
711
|
+
|
|
712
|
+
expect(resource.refresh).toHaveBeenCalledTimes(1);
|
|
713
|
+
} finally {
|
|
714
|
+
vi.useRealTimers();
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('should skip excluded target ids when refreshing', async () => {
|
|
719
|
+
(filterManager as any).filterConfigs = [
|
|
720
|
+
{
|
|
721
|
+
filterId: 'filter-1',
|
|
722
|
+
targetId: 'target-1',
|
|
723
|
+
filterPaths: ['name'],
|
|
724
|
+
operator: '$eq',
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
filterId: 'filter-1',
|
|
728
|
+
targetId: 'target-2',
|
|
729
|
+
filterPaths: ['name'],
|
|
730
|
+
operator: '$eq',
|
|
731
|
+
},
|
|
732
|
+
];
|
|
733
|
+
|
|
734
|
+
const mockTargetModel1 = {
|
|
735
|
+
resource: {
|
|
736
|
+
addFilterGroup: vi.fn(),
|
|
737
|
+
removeFilterGroup: vi.fn(),
|
|
738
|
+
refresh: vi.fn().mockResolvedValue(undefined),
|
|
739
|
+
},
|
|
740
|
+
setFilterActive: vi.fn(),
|
|
741
|
+
getDataLoadingMode: vi.fn().mockReturnValue('auto'),
|
|
742
|
+
};
|
|
743
|
+
const mockTargetModel2 = {
|
|
744
|
+
resource: {
|
|
745
|
+
addFilterGroup: vi.fn(),
|
|
746
|
+
removeFilterGroup: vi.fn(),
|
|
747
|
+
refresh: vi.fn().mockResolvedValue(undefined),
|
|
748
|
+
},
|
|
749
|
+
setFilterActive: vi.fn(),
|
|
750
|
+
getDataLoadingMode: vi.fn().mockReturnValue('auto'),
|
|
751
|
+
};
|
|
752
|
+
const mockFilterModel = {
|
|
753
|
+
getFilterValue: vi.fn().mockReturnValue('test-value'),
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
(mockFlowModel.flowEngine.getModel as any).mockImplementation((uid: string) => {
|
|
757
|
+
if (uid === 'target-1') return mockTargetModel1;
|
|
758
|
+
if (uid === 'target-2') return mockTargetModel2;
|
|
759
|
+
if (uid === 'filter-1') return mockFilterModel;
|
|
760
|
+
return null;
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
await filterManager.refreshTargetsByFilter('filter-1', { excludeTargetIds: new Set(['target-1']) });
|
|
764
|
+
|
|
765
|
+
expect(mockTargetModel1.resource.refresh).not.toHaveBeenCalled();
|
|
766
|
+
expect(mockTargetModel2.resource.refresh).toHaveBeenCalledTimes(1);
|
|
767
|
+
});
|
|
768
|
+
|
|
499
769
|
it('should normalize array operators when the target field is scalar', async () => {
|
|
500
770
|
const filterConfigs = [
|
|
501
771
|
{
|
|
@@ -699,11 +699,14 @@ FormBlockModel.registerFlow({
|
|
|
699
699
|
linkageRules: {
|
|
700
700
|
use: 'fieldLinkageRules',
|
|
701
701
|
afterParamsSave(ctx) {
|
|
702
|
-
//
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
702
|
+
// FlowSettings 保存后还会触发一次 beforeRender/rerender,
|
|
703
|
+
// 这里延迟到该刷新之后再重放联动规则,避免字段组件被 beforeRender 恢复为原始属性。
|
|
704
|
+
setTimeout(() => {
|
|
705
|
+
ctx.model.applyFlow('eventSettings', {
|
|
706
|
+
changedValues: {},
|
|
707
|
+
allValues: ctx.form?.getFieldsValue(true),
|
|
708
|
+
});
|
|
709
|
+
}, 0);
|
|
707
710
|
},
|
|
708
711
|
},
|
|
709
712
|
},
|
|
@@ -14,6 +14,11 @@ import { FlowEngine, FlowModel, SingleRecordResource } from '@nocobase/flow-engi
|
|
|
14
14
|
// 直接从 models 聚合导入,避免局部文件相互引用顺序导致的循环依赖
|
|
15
15
|
import { FormBlockContent, FormBlockModel, FormComponent } from '../../../..';
|
|
16
16
|
import { Form } from 'antd';
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.useRealTimers();
|
|
20
|
+
});
|
|
21
|
+
|
|
17
22
|
// -----------------------------
|
|
18
23
|
// Helpers
|
|
19
24
|
// -----------------------------
|
|
@@ -319,6 +324,31 @@ describe('FormBlockModel (form/formValues injection & server resolve anchors)',
|
|
|
319
324
|
expect(flows.has('eventSettings')).toBe(true);
|
|
320
325
|
});
|
|
321
326
|
|
|
327
|
+
it('replays linkage rules after the settings rerender tick', async () => {
|
|
328
|
+
vi.useFakeTimers();
|
|
329
|
+
const engine = new FlowEngine();
|
|
330
|
+
const TestFormModel = await createTestFormModelSubclass();
|
|
331
|
+
const model = new TestFormModel({ uid: 'form-linkage-save', flowEngine: engine } as any);
|
|
332
|
+
const applyFlow = vi.spyOn(model, 'applyFlow').mockResolvedValue(undefined as any);
|
|
333
|
+
const flow = model.getFlow('eventSettings') as any;
|
|
334
|
+
const afterParamsSave = flow?.steps?.linkageRules?.afterParamsSave;
|
|
335
|
+
const ctx: any = {
|
|
336
|
+
model,
|
|
337
|
+
form: {
|
|
338
|
+
getFieldsValue: vi.fn(() => ({ status: 'draft' })),
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
afterParamsSave(ctx);
|
|
343
|
+
|
|
344
|
+
expect(applyFlow).not.toHaveBeenCalled();
|
|
345
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
346
|
+
expect(applyFlow).toHaveBeenCalledWith('eventSettings', {
|
|
347
|
+
changedValues: {},
|
|
348
|
+
allValues: { status: 'draft' },
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
322
352
|
it('delegates layout/assignRules/linkageRules stepParams to grid model', async () => {
|
|
323
353
|
const model = await setupFormModel();
|
|
324
354
|
const engine = model.flowEngine as FlowEngine;
|
|
@@ -1508,16 +1508,21 @@ export class RuleEngine {
|
|
|
1508
1508
|
private getRowTargetKey(baseCtx: any, rowPath: NamePath): string | string[] {
|
|
1509
1509
|
let collection = this.getRootCollection() || this.getCollectionFromContext(baseCtx);
|
|
1510
1510
|
let field: any;
|
|
1511
|
+
let lastAssociationField: any;
|
|
1511
1512
|
for (const seg of rowPath) {
|
|
1512
1513
|
if (typeof seg === 'number') continue;
|
|
1513
1514
|
if (typeof seg !== 'string' || !seg || !collection?.getField) break;
|
|
1514
1515
|
|
|
1515
1516
|
field = collection?.getField?.(seg);
|
|
1516
1517
|
if (!field?.isAssociationField?.()) break;
|
|
1518
|
+
lastAssociationField = field;
|
|
1517
1519
|
collection = field?.targetCollection;
|
|
1518
1520
|
}
|
|
1519
1521
|
|
|
1520
|
-
const raw =
|
|
1522
|
+
const raw =
|
|
1523
|
+
lastAssociationField?.targetCollection?.filterTargetKey ??
|
|
1524
|
+
lastAssociationField?.targetCollection?.filterByTk ??
|
|
1525
|
+
lastAssociationField?.targetKey;
|
|
1521
1526
|
if (Array.isArray(raw)) {
|
|
1522
1527
|
const keys = raw.filter((key): key is string => typeof key === 'string' && !!key);
|
|
1523
1528
|
return keys.length ? keys : 'id';
|
|
@@ -394,16 +394,21 @@ export class FormValueRuntime {
|
|
|
394
394
|
private getArrayItemTargetKey(arrayPath?: NamePath): string | string[] {
|
|
395
395
|
let collection = this.model?.context?.collection;
|
|
396
396
|
let field: any;
|
|
397
|
+
let lastAssociationField: any;
|
|
397
398
|
for (const seg of arrayPath || []) {
|
|
398
399
|
if (typeof seg === 'number') continue;
|
|
399
400
|
if (typeof seg !== 'string' || !collection?.getField) break;
|
|
400
401
|
|
|
401
402
|
field = collection?.getField?.(seg);
|
|
402
403
|
if (!field?.isAssociationField?.()) break;
|
|
404
|
+
lastAssociationField = field;
|
|
403
405
|
collection = field?.targetCollection;
|
|
404
406
|
}
|
|
405
407
|
|
|
406
|
-
const raw =
|
|
408
|
+
const raw =
|
|
409
|
+
lastAssociationField?.targetCollection?.filterTargetKey ??
|
|
410
|
+
lastAssociationField?.targetCollection?.filterByTk ??
|
|
411
|
+
lastAssociationField?.targetKey;
|
|
407
412
|
if (Array.isArray(raw)) {
|
|
408
413
|
const keys = raw.filter((key): key is string => typeof key === 'string' && !!key);
|
|
409
414
|
return keys.length ? keys : 'id';
|
|
@@ -932,12 +932,12 @@ const HighPerformanceTable = React.memo(
|
|
|
932
932
|
|
|
933
933
|
return {
|
|
934
934
|
onClick: async (event) => {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
935
|
+
const selected = highlightedRowKey !== rowKey;
|
|
936
|
+
defineClickedRowRecordVariable(model, selected ? record : null);
|
|
937
|
+
try {
|
|
938
|
+
await model.dispatchEvent('rowClick', { record, rowIndex, event, selected });
|
|
939
|
+
} finally {
|
|
938
940
|
removeClickedRowRecordVariable(model);
|
|
939
|
-
} else {
|
|
940
|
-
await model.dispatchEvent('rowClick', { record, rowIndex, event });
|
|
941
941
|
}
|
|
942
942
|
},
|
|
943
943
|
rowIndex,
|
|
@@ -1072,7 +1072,12 @@ TableBlockModel.registerEvents({
|
|
|
1072
1072
|
|
|
1073
1073
|
const model = ctx.model as TableBlockModel;
|
|
1074
1074
|
const rowKey = getRowKey(ctx.inputArgs.record, model.collection.filterTargetKey);
|
|
1075
|
-
|
|
1075
|
+
const selected = ctx.inputArgs.selected;
|
|
1076
|
+
if (selected === true) {
|
|
1077
|
+
model.highlightRow(ctx.inputArgs.record);
|
|
1078
|
+
} else if (selected === false) {
|
|
1079
|
+
model.clearHighlight();
|
|
1080
|
+
} else if (model.props.highlightedRowKey !== rowKey) {
|
|
1076
1081
|
model.highlightRow(ctx.inputArgs.record);
|
|
1077
1082
|
} else {
|
|
1078
1083
|
model.clearHighlight();
|
|
@@ -535,6 +535,9 @@ TableColumnModel.registerFlow({
|
|
|
535
535
|
fieldModel.setStepParams('fieldSettings', 'init', fieldSettingsInit);
|
|
536
536
|
await fieldModel.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
537
537
|
}
|
|
538
|
+
if (targetUse) {
|
|
539
|
+
ctx.model.setStepParams('tableColumnSettings', 'model', { use: targetUse });
|
|
540
|
+
}
|
|
538
541
|
ctx.model.setProps(targetCollectionField.getComponentProps());
|
|
539
542
|
},
|
|
540
543
|
defaultParams: (ctx: any) => {
|
|
@@ -12,45 +12,55 @@ import { castArray } from 'lodash';
|
|
|
12
12
|
import { BlockSceneEnum } from '../../base/BlockModel';
|
|
13
13
|
import { TableBlockModel } from './TableBlockModel';
|
|
14
14
|
|
|
15
|
+
export function getAssociationSelectForeignKeyFilter(collectionField: any) {
|
|
16
|
+
const isOToAny = ['oho', 'o2m'].includes(collectionField?.interface);
|
|
17
|
+
const foreignKey = collectionField?.foreignKey;
|
|
18
|
+
if (!isOToAny || !foreignKey) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
[foreignKey]: {
|
|
23
|
+
$is: null,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getAssociationSelectAssociatedRecordsFilter(collection: any, associatedRecords: any[] = []) {
|
|
29
|
+
const targetKey = collection?.filterTargetKey || 'id';
|
|
30
|
+
const filterKeys = associatedRecords
|
|
31
|
+
.map((record) => record?.[targetKey])
|
|
32
|
+
.filter((value) => value !== undefined && value !== null && value !== '');
|
|
33
|
+
|
|
34
|
+
if (!filterKeys.length) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
[`${targetKey}.$ne`]: filterKeys,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
15
43
|
export class TableSelectModel extends TableBlockModel {
|
|
16
44
|
static scene = BlockSceneEnum.select;
|
|
17
45
|
rowSelectionProps: any = observable.deep({});
|
|
18
46
|
onInit(options: any) {
|
|
19
47
|
super.onInit(options);
|
|
20
48
|
const collectionField = this.context.view.inputArgs.collectionField || {};
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
if (isOToAny) {
|
|
24
|
-
const foreignKey = collectionField.foreignKey;
|
|
49
|
+
const foreignKeyFilter = getAssociationSelectForeignKeyFilter(collectionField);
|
|
50
|
+
if (foreignKeyFilter) {
|
|
25
51
|
const filterGroupKey = `${this.uid}-${collectionField.name}`;
|
|
26
|
-
|
|
27
|
-
if (sourceId != null) {
|
|
28
|
-
this.resource.addFilterGroup(filterGroupKey, {
|
|
29
|
-
$or: [{ [foreignKey]: { $is: null } }, { [foreignKey]: { $eq: sourceId } }],
|
|
30
|
-
});
|
|
31
|
-
} else {
|
|
32
|
-
this.resource.addFilterGroup(filterGroupKey, {
|
|
33
|
-
[foreignKey]: {
|
|
34
|
-
$is: null,
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
}
|
|
52
|
+
this.resource.addFilterGroup(filterGroupKey, foreignKeyFilter);
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
Object.assign(this.rowSelectionProps, this.context.view.inputArgs.rowSelectionProps || {});
|
|
42
56
|
|
|
43
57
|
const getSelectedRows = this.rowSelectionProps?.defaultSelectedRows;
|
|
44
58
|
const selectData = typeof getSelectedRows === 'function' ? getSelectedRows() : getSelectedRows;
|
|
45
|
-
const data = (selectData
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.filter(Boolean);
|
|
51
|
-
this.resource.addFilterGroup(`${this.uid}-select`, {
|
|
52
|
-
[`${this.collection.filterTargetKey}.$ne`]: filterKeys,
|
|
53
|
-
});
|
|
59
|
+
const data = [...castArray(selectData || []), ...castArray(this.context.view.inputArgs.associatedRecords || [])];
|
|
60
|
+
const associatedRecordsFilter = getAssociationSelectAssociatedRecordsFilter(this.collection, data);
|
|
61
|
+
if (associatedRecordsFilter) {
|
|
62
|
+
this.resource.addFilterGroup(`${this.uid}-select`, associatedRecordsFilter);
|
|
63
|
+
}
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
66
|
|
|
@@ -0,0 +1,69 @@
|
|
|
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 } from 'vitest';
|
|
12
|
+
import '@nocobase/client';
|
|
13
|
+
import { TableBlockModel } from '../TableBlockModel';
|
|
14
|
+
|
|
15
|
+
function createTableModel() {
|
|
16
|
+
const engine = new FlowEngine();
|
|
17
|
+
engine.registerModels({ TableBlockModel });
|
|
18
|
+
|
|
19
|
+
const ds = engine.dataSourceManager.getDataSource('main');
|
|
20
|
+
ds.addCollection({
|
|
21
|
+
name: 'posts',
|
|
22
|
+
filterTargetKey: 'id',
|
|
23
|
+
fields: [
|
|
24
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
25
|
+
{ name: 'title', type: 'string', interface: 'input' },
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return engine.createModel<TableBlockModel>({
|
|
30
|
+
uid: 'posts-table',
|
|
31
|
+
use: 'TableBlockModel',
|
|
32
|
+
stepParams: {
|
|
33
|
+
resourceSettings: {
|
|
34
|
+
init: {
|
|
35
|
+
dataSourceKey: 'main',
|
|
36
|
+
collectionName: 'posts',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('TableBlockModel rowClick event', () => {
|
|
44
|
+
it('highlights the clicked row when selected is true', async () => {
|
|
45
|
+
const model = createTableModel();
|
|
46
|
+
const record = { id: 1, title: 'first post' };
|
|
47
|
+
const rowClick = model.getEvent('rowClick');
|
|
48
|
+
|
|
49
|
+
await rowClick?.handler({ model, inputArgs: { record, selected: true } } as any, {
|
|
50
|
+
condition: { logic: '$and', items: [] },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(model.props.highlightedRowKey).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('clears the highlighted row when selected is false', async () => {
|
|
57
|
+
const model = createTableModel();
|
|
58
|
+
const record = { id: 1, title: 'first post' };
|
|
59
|
+
const rowClick = model.getEvent('rowClick');
|
|
60
|
+
|
|
61
|
+
model.highlightRow(record);
|
|
62
|
+
|
|
63
|
+
await rowClick?.handler({ model, inputArgs: { record, selected: false } } as any, {
|
|
64
|
+
condition: { logic: '$and', items: [] },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(model.props.highlightedRowKey).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
});
|