@nocobase/client-v2 2.1.0-beta.43 → 2.1.0-beta.45

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.
Files changed (91) hide show
  1. package/es/BaseApplication.d.ts +9 -0
  2. package/es/authRedirect.d.ts +12 -0
  3. package/es/collection-manager/filter-operators/index.d.ts +1 -0
  4. package/es/components/form/filter/CollectionFilterItem.d.ts +5 -1
  5. package/es/components/form/filter/FilterValueInput.d.ts +5 -0
  6. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  7. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +11 -0
  8. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  9. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +3 -2
  10. package/es/flow/components/ConditionBuilder.d.ts +1 -0
  11. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  12. package/es/flow/components/FieldAssignValueInput.d.ts +1 -0
  13. package/es/flow/components/fieldAssignOptions.d.ts +10 -0
  14. package/es/flow/components/filter/LinkageFilterItem.d.ts +1 -0
  15. package/es/flow/components/filter/VariableFilterItem.d.ts +1 -0
  16. package/es/flow/components/filter/metaTreeAssociationDepth.d.ts +13 -0
  17. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +26 -3
  18. package/es/flow/models/base/AssociationFieldGroupModel.d.ts +2 -33
  19. package/es/flow/models/blocks/filter-manager/utils.d.ts +23 -1
  20. package/es/flow/models/blocks/table/TableColumnModel.d.ts +2 -0
  21. package/es/index.mjs +179 -101
  22. package/lib/index.js +187 -109
  23. package/package.json +7 -7
  24. package/src/BaseApplication.tsx +13 -0
  25. package/src/__tests__/browserChecker.test.ts +103 -0
  26. package/src/__tests__/dataSourceRuntime.test.tsx +22 -1
  27. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +5 -0
  28. package/src/authRedirect.ts +43 -0
  29. package/src/collection-field-interface/CollectionFieldInterface.ts +14 -7
  30. package/src/collection-manager/filter-operators/index.ts +29 -2
  31. package/src/components/form/filter/CollectionFilterItem.tsx +10 -2
  32. package/src/components/form/filter/FilterValueInput.tsx +27 -2
  33. package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +39 -0
  34. package/src/flow/__tests__/FlowRoute.test.tsx +8 -1
  35. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +369 -2
  36. package/src/flow/actions/dataScope.tsx +8 -1
  37. package/src/flow/actions/linkageRules.tsx +56 -1
  38. package/src/flow/actions/setTargetDataScope.tsx +1 -1
  39. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +31 -3
  40. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +15 -1
  41. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +139 -0
  42. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +75 -1
  43. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  44. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +11 -0
  45. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -4
  46. package/src/flow/admin-shell/admin-layout/useApplications.tsx +34 -1
  47. package/src/flow/common/Markdown/Edit.tsx +32 -4
  48. package/src/flow/components/ConditionBuilder.tsx +12 -2
  49. package/src/flow/components/DefaultValue.tsx +14 -8
  50. package/src/flow/components/FieldAssignRulesEditor.tsx +37 -46
  51. package/src/flow/components/FieldAssignValueInput.tsx +14 -14
  52. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +112 -1
  53. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +71 -1
  54. package/src/flow/components/fieldAssignOptions.ts +23 -6
  55. package/src/flow/components/filter/LinkageFilterItem.tsx +26 -12
  56. package/src/flow/components/filter/VariableFilterItem.tsx +32 -13
  57. package/src/flow/components/filter/__tests__/fieldsToOptions.test.ts +48 -0
  58. package/src/flow/components/filter/__tests__/metaTreeAssociationDepth.test.ts +60 -0
  59. package/src/flow/components/filter/fieldsToOptions.ts +9 -4
  60. package/src/flow/components/filter/metaTreeAssociationDepth.ts +69 -0
  61. package/src/flow/internal/utils/operatorSchemaHelper.ts +112 -5
  62. package/src/flow/models/actions/FilterActionModel.tsx +15 -3
  63. package/src/flow/models/base/AssociationFieldGroupModel.tsx +20 -3
  64. package/src/flow/models/base/GridModel.tsx +1 -1
  65. package/src/flow/models/base/PageModel/ChildPageModel.tsx +5 -1
  66. package/src/flow/models/base/PageModel/__tests__/ChildPageModel.test.ts +12 -0
  67. package/src/flow/models/base/__tests__/AssociationFieldGroupModel.test.ts +86 -0
  68. package/src/flow/models/base/__tests__/GridModel.render.test.tsx +53 -0
  69. package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +11 -0
  70. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +14 -12
  71. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +6 -1
  72. package/src/flow/models/blocks/filter-manager/__tests__/getDefaultOperator.test.ts +35 -1
  73. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +148 -0
  74. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/defaultOperator.test.tsx +85 -0
  75. package/src/flow/models/blocks/filter-manager/flow-actions/connectFields.tsx +48 -6
  76. package/src/flow/models/blocks/filter-manager/flow-actions/customizeFilterRender.tsx +14 -44
  77. package/src/flow/models/blocks/filter-manager/flow-actions/defaultOperator.tsx +3 -5
  78. package/src/flow/models/blocks/filter-manager/flow-actions/operatorComponentProps.tsx +2 -12
  79. package/src/flow/models/blocks/filter-manager/utils.ts +143 -4
  80. package/src/flow/models/blocks/form/FormAssociationItemModel.tsx +9 -0
  81. package/src/flow/models/blocks/form/FormItemModel.tsx +9 -0
  82. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  83. package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +27 -2
  84. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +29 -0
  85. package/src/flow/models/blocks/form/value-runtime/runtime.ts +5 -1
  86. package/src/flow/models/blocks/table/TableColumnModel.tsx +15 -5
  87. package/src/flow/models/blocks/table/TableCustomColumnModel.tsx +2 -2
  88. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +18 -0
  89. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +6 -7
  90. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +5 -3
  91. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +70 -0
@@ -9,9 +9,15 @@
9
9
 
10
10
  import React from 'react';
11
11
  import { fireEvent, render, screen } from '@testing-library/react';
12
- import { FlowSettingsContextProvider } from '@nocobase/flow-engine';
12
+ import { FlowEngine, FlowModel, FlowSettingsContextProvider } from '@nocobase/flow-engine';
13
13
  import { describe, expect, it, vi } from 'vitest';
14
- import { fieldLinkageRules, linkageSetFieldProps, subFormLinkageSetFieldProps } from '../linkageRules';
14
+ import {
15
+ fieldLinkageRules,
16
+ linkageAssignField,
17
+ linkageSetFieldProps,
18
+ subFormFieldLinkageRules,
19
+ subFormLinkageSetFieldProps,
20
+ } from '../linkageRules';
15
21
 
16
22
  const createSubFormFieldModel = ({
17
23
  uid,
@@ -335,6 +341,110 @@ describe('subFormLinkageSetFieldProps action', () => {
335
341
  expect(row0CollectedModel.hidden).toBe(false);
336
342
  expect(row1CollectedModel.hidden).toBe(true);
337
343
  });
344
+
345
+ it('should clear the current row value when a subform list field is hidden without reserving value', async () => {
346
+ const setFormValues = vi.fn(async () => undefined);
347
+ const form = {
348
+ getFieldValue: vi.fn((path: Array<string | number>) => {
349
+ if (JSON.stringify(path) === JSON.stringify(['items', 0, 'a'])) {
350
+ return 'row value';
351
+ }
352
+ return undefined;
353
+ }),
354
+ setFieldValue: vi.fn(),
355
+ };
356
+ const engine = new FlowEngine();
357
+
358
+ const rowGridFork = new FlowModel({ uid: 'row-grid-fork', flowEngine: engine }) as any;
359
+ rowGridFork.hidden = false;
360
+ rowGridFork.context.defineProperty('fieldKey', { value: ['items:0'] });
361
+ rowGridFork.context.defineProperty('fieldIndex', { value: ['items:0'] });
362
+ rowGridFork.context.defineProperty('form', { value: form });
363
+ rowGridFork.context.defineProperty('setFormValues', { value: setFormValues });
364
+ rowGridFork.context.defineProperty('app', {
365
+ value: {
366
+ jsonLogic: {
367
+ apply: () => true,
368
+ },
369
+ },
370
+ });
371
+ rowGridFork.getAction = vi.fn((name: string) => {
372
+ if (name === 'subFormLinkageSetFieldProps') {
373
+ return subFormLinkageSetFieldProps;
374
+ }
375
+ });
376
+
377
+ const targetFieldFork: any = {
378
+ uid: 'field-a',
379
+ isFork: true,
380
+ hidden: false,
381
+ props: {},
382
+ context: {
383
+ fieldIndex: ['items:0'],
384
+ },
385
+ getStepParams: vi.fn((flowKey: string, stepKey: string) => {
386
+ if (flowKey === 'fieldSettings' && stepKey === 'init') {
387
+ return { fieldPath: 'a' };
388
+ }
389
+ }),
390
+ setProps(key: any, value?: any) {
391
+ if (typeof key === 'string') {
392
+ this.props[key] = value;
393
+ } else {
394
+ this.props = { ...this.props, ...key };
395
+ }
396
+ },
397
+ };
398
+
399
+ const formItemModel: any = {
400
+ uid: 'field-a',
401
+ getFork: vi.fn((key: string) => (key === 'items:0:field-a' ? targetFieldFork : undefined)),
402
+ };
403
+
404
+ engine.getModel = vi.fn((uid: string) => (uid === 'field-a' ? formItemModel : undefined)) as any;
405
+
406
+ await subFormFieldLinkageRules.handler(
407
+ {
408
+ model: {
409
+ hidden: false,
410
+ subModels: {
411
+ grid: {
412
+ forks: [rowGridFork],
413
+ },
414
+ },
415
+ },
416
+ flowKey: 'eventSettings',
417
+ } as any,
418
+ {
419
+ value: [
420
+ {
421
+ key: 'rule-1',
422
+ enable: true,
423
+ condition: { logic: '$and', items: [] },
424
+ actions: [
425
+ {
426
+ name: 'subFormLinkageSetFieldProps',
427
+ params: {
428
+ value: {
429
+ fields: ['field-a'],
430
+ state: 'hidden',
431
+ },
432
+ },
433
+ },
434
+ ],
435
+ },
436
+ ],
437
+ },
438
+ );
439
+
440
+ expect(form.getFieldValue).toHaveBeenCalledWith(['items', 0, 'a']);
441
+ expect(targetFieldFork.hidden).toBe(true);
442
+ expect(setFormValues).toHaveBeenCalledWith(
443
+ [{ path: ['items', 0, 'a'], value: undefined }],
444
+ expect.objectContaining({ source: 'linkage' }),
445
+ );
446
+ expect(form.setFieldValue).not.toHaveBeenCalled();
447
+ });
338
448
  });
339
449
 
340
450
  describe('linkageSetFieldProps action', () => {
@@ -541,6 +651,263 @@ describe('linkageSetFieldProps action', () => {
541
651
  expect(form.setFieldValue).not.toHaveBeenCalled();
542
652
  });
543
653
 
654
+ it('should clear form value when a field is hidden without reserving value', async () => {
655
+ const setFormValues = vi.fn(async () => undefined);
656
+ const form = {
657
+ getFieldValue: vi.fn(() => '123'),
658
+ setFieldValue: vi.fn(),
659
+ };
660
+ const fieldModel: any = {
661
+ uid: 'name-field',
662
+ hidden: false,
663
+ context: { form },
664
+ props: {
665
+ label: 'Name',
666
+ },
667
+ getStepParams: vi.fn((flowKey: string, stepKey: string) => {
668
+ if (flowKey === 'fieldSettings' && stepKey === 'init') {
669
+ return { fieldPath: 'name' };
670
+ }
671
+ }),
672
+ setProps(key: any, value?: any) {
673
+ if (typeof key === 'string') {
674
+ this.props[key] = value;
675
+ } else {
676
+ this.props = { ...this.props, ...key };
677
+ }
678
+ },
679
+ };
680
+ const ctx: any = {
681
+ app: {
682
+ jsonLogic: {
683
+ apply: vi.fn(() => true),
684
+ },
685
+ },
686
+ model: {
687
+ context: { form },
688
+ subModels: {
689
+ grid: {
690
+ subModels: {
691
+ items: [fieldModel],
692
+ },
693
+ },
694
+ },
695
+ },
696
+ setFormValues,
697
+ getAction: (name: string) => (name === 'linkageSetFieldProps' ? linkageSetFieldProps : null),
698
+ resolveJsonTemplate: vi.fn(async (value) => value),
699
+ };
700
+
701
+ await fieldLinkageRules.handler(ctx, {
702
+ value: [
703
+ {
704
+ key: 'rule-1',
705
+ enable: true,
706
+ condition: { logic: '$and', items: [] },
707
+ actions: [
708
+ {
709
+ key: 'action-1',
710
+ name: 'linkageSetFieldProps',
711
+ params: {
712
+ value: {
713
+ fields: ['name-field'],
714
+ state: 'hidden',
715
+ },
716
+ },
717
+ },
718
+ ],
719
+ },
720
+ ],
721
+ });
722
+
723
+ expect(fieldModel.hidden).toBe(true);
724
+ expect(setFormValues).toHaveBeenCalledWith(
725
+ [{ path: ['name'], value: undefined }],
726
+ expect.objectContaining({ source: 'linkage' }),
727
+ );
728
+ expect(form.setFieldValue).not.toHaveBeenCalled();
729
+ });
730
+
731
+ it('should keep hidden clear after same-round assignment for the same field', async () => {
732
+ const formValues: { name?: string } = {};
733
+ const setFormValues = vi.fn(async (patches: Array<{ path: Array<string | number>; value: string | undefined }>) => {
734
+ for (const patch of patches) {
735
+ if (patch.path.length === 1 && patch.path[0] === 'name') {
736
+ formValues.name = patch.value;
737
+ }
738
+ }
739
+ });
740
+ const form = {
741
+ getFieldValue: vi.fn((path: Array<string | number>) => {
742
+ if (path.length === 1 && path[0] === 'name') {
743
+ return formValues.name;
744
+ }
745
+ return undefined;
746
+ }),
747
+ setFieldValue: vi.fn(),
748
+ };
749
+ const fieldModel: any = {
750
+ uid: 'name-field',
751
+ hidden: false,
752
+ context: { form },
753
+ props: {
754
+ label: 'Name',
755
+ },
756
+ getStepParams: vi.fn((flowKey: string, stepKey: string) => {
757
+ if (flowKey === 'fieldSettings' && stepKey === 'init') {
758
+ return { fieldPath: 'name' };
759
+ }
760
+ }),
761
+ setProps(key: any, value?: any) {
762
+ if (typeof key === 'string') {
763
+ this.props[key] = value;
764
+ } else {
765
+ this.props = { ...this.props, ...key };
766
+ }
767
+ },
768
+ };
769
+ const ctx: any = {
770
+ app: {
771
+ jsonLogic: {
772
+ apply: vi.fn(() => true),
773
+ },
774
+ },
775
+ model: {
776
+ context: { form },
777
+ subModels: {
778
+ grid: {
779
+ subModels: {
780
+ items: [fieldModel],
781
+ },
782
+ },
783
+ },
784
+ },
785
+ setFormValues,
786
+ getAction: (name: string) => {
787
+ if (name === 'linkageSetFieldProps') return linkageSetFieldProps;
788
+ if (name === 'linkageAssignField') return linkageAssignField;
789
+ return null;
790
+ },
791
+ resolveJsonTemplate: vi.fn(async (value) => value),
792
+ };
793
+
794
+ await fieldLinkageRules.handler(ctx, {
795
+ value: [
796
+ {
797
+ key: 'rule-1',
798
+ enable: true,
799
+ condition: { logic: '$and', items: [] },
800
+ actions: [
801
+ {
802
+ key: 'action-1',
803
+ name: 'linkageSetFieldProps',
804
+ params: {
805
+ value: {
806
+ fields: ['name-field'],
807
+ state: 'hidden',
808
+ },
809
+ },
810
+ },
811
+ {
812
+ key: 'action-2',
813
+ name: 'linkageAssignField',
814
+ params: {
815
+ value: [
816
+ {
817
+ key: 'assign-1',
818
+ enable: true,
819
+ targetPath: 'name',
820
+ value: 'assigned',
821
+ },
822
+ ],
823
+ },
824
+ },
825
+ ],
826
+ },
827
+ ],
828
+ });
829
+
830
+ expect(fieldModel.hidden).toBe(true);
831
+ expect(formValues.name).toBeUndefined();
832
+ expect(setFormValues).not.toHaveBeenCalled();
833
+ expect(form.setFieldValue).not.toHaveBeenCalled();
834
+ });
835
+
836
+ it('should not clear form value when a field is hidden with reserved value', async () => {
837
+ const setFormValues = vi.fn(async () => undefined);
838
+ const form = {
839
+ getFieldValue: vi.fn(() => '123'),
840
+ setFieldValue: vi.fn(),
841
+ };
842
+ const fieldModel: any = {
843
+ uid: 'name-field',
844
+ hidden: false,
845
+ context: { form },
846
+ props: {
847
+ label: 'Name',
848
+ },
849
+ getStepParams: vi.fn((flowKey: string, stepKey: string) => {
850
+ if (flowKey === 'fieldSettings' && stepKey === 'init') {
851
+ return { fieldPath: 'name' };
852
+ }
853
+ }),
854
+ setProps(key: any, value?: any) {
855
+ if (typeof key === 'string') {
856
+ this.props[key] = value;
857
+ } else {
858
+ this.props = { ...this.props, ...key };
859
+ }
860
+ },
861
+ };
862
+ const ctx: any = {
863
+ app: {
864
+ jsonLogic: {
865
+ apply: vi.fn(() => true),
866
+ },
867
+ },
868
+ model: {
869
+ context: { form },
870
+ subModels: {
871
+ grid: {
872
+ subModels: {
873
+ items: [fieldModel],
874
+ },
875
+ },
876
+ },
877
+ },
878
+ setFormValues,
879
+ getAction: (name: string) => (name === 'linkageSetFieldProps' ? linkageSetFieldProps : null),
880
+ resolveJsonTemplate: vi.fn(async (value) => value),
881
+ };
882
+
883
+ await fieldLinkageRules.handler(ctx, {
884
+ value: [
885
+ {
886
+ key: 'rule-1',
887
+ enable: true,
888
+ condition: { logic: '$and', items: [] },
889
+ actions: [
890
+ {
891
+ key: 'action-1',
892
+ name: 'linkageSetFieldProps',
893
+ params: {
894
+ value: {
895
+ fields: ['name-field'],
896
+ state: 'hiddenReservedValue',
897
+ },
898
+ },
899
+ },
900
+ ],
901
+ },
902
+ ],
903
+ });
904
+
905
+ expect(fieldModel.hidden).toBe(false);
906
+ expect(fieldModel.props.hidden).toBe(true);
907
+ expect(setFormValues).not.toHaveBeenCalled();
908
+ expect(form.setFieldValue).not.toHaveBeenCalled();
909
+ });
910
+
544
911
  it('should keep all options selectable after options were limited once', async () => {
545
912
  const fieldComponentModel: any = {
546
913
  uid: 'status-field-component',
@@ -33,7 +33,14 @@ export const dataScope = defineAction({
33
33
  return (
34
34
  <FilterGroup
35
35
  value={props.value}
36
- FilterItem={(p) => <VariableFilterItem {...p} model={flowContext.model} rightAsVariable />}
36
+ FilterItem={(p) => (
37
+ <VariableFilterItem
38
+ {...p}
39
+ model={flowContext.model}
40
+ rightAsVariable
41
+ maxAssociationFieldDepth={flowContext.model instanceof FieldModel ? undefined : 2}
42
+ />
43
+ )}
37
44
  />
38
45
  );
39
46
  },
@@ -1606,7 +1606,9 @@ const LinkageRulesUI = observer(
1606
1606
  <div style={{ paddingLeft: 12 }}>
1607
1607
  <FilterGroup
1608
1608
  value={rule.condition}
1609
- FilterItem={(props) => <LinkageFilterItem model={ctx.model} value={props.value} />}
1609
+ FilterItem={(props) => (
1610
+ <LinkageFilterItem model={ctx.model} value={props.value} maxAssociationFieldDepth={2} />
1611
+ )}
1610
1612
  />
1611
1613
  </div>
1612
1614
  </div>
@@ -1766,6 +1768,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1766
1768
  const allModels: FlowModel[] = ctx.model.__allModels || (ctx.model.__allModels = []);
1767
1769
  const modelsToApply = new Set<FlowModel>(allModels);
1768
1770
  const patchPropsByModel = new Map<FlowModel, any>();
1771
+ const clearValueOnHiddenModelUids = new Set<string>();
1769
1772
  const directValuePatches: Array<{ path: Array<string | number>; value: any; whenEmpty?: boolean }> = [];
1770
1773
  const rootCollection = getCollectionFromModel((ctx.model as any)?.context?.blockModel ?? ctx.model);
1771
1774
  const isSafeToWriteAssociationSubpath = (namePath: any): boolean => {
@@ -1876,6 +1879,16 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1876
1879
  ...(whenEmpty ? { whenEmpty: true } : {}),
1877
1880
  });
1878
1881
  };
1882
+ const removePendingFormValuePatches = (path: any) => {
1883
+ const resolvedPath = resolveNamePathForPatch(path);
1884
+ if (!resolvedPath) return;
1885
+ const resolvedPathKey = namePathToPathKey(resolvedPath);
1886
+ for (let i = directValuePatches.length - 1; i >= 0; i--) {
1887
+ if (namePathToPathKey(directValuePatches[i].path) === resolvedPathKey) {
1888
+ directValuePatches.splice(i, 1);
1889
+ }
1890
+ }
1891
+ };
1879
1892
 
1880
1893
  const getModelTargetPathForPatch = (model: any): string | null => {
1881
1894
  if (!model || typeof model !== 'object') return null;
@@ -1971,6 +1984,23 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1971
1984
 
1972
1985
  return out;
1973
1986
  };
1987
+ const getModelTargetPathForHiddenClear = (model: any): string | Array<string | number> | null => {
1988
+ const fieldPathArray = normalizeNamePathForKey(model?.context?.fieldPathArray);
1989
+ const targetPath = getModelTargetPathForPatch(model);
1990
+ if (fieldPathArray) {
1991
+ const targetPathLastString = targetPath
1992
+ ? ([...parsePathString(targetPath)].reverse().find((seg) => typeof seg === 'string') as string | undefined)
1993
+ : undefined;
1994
+ const fieldPathArrayLastString = [...fieldPathArray].reverse().find((seg) => typeof seg === 'string');
1995
+ if (!targetPathLastString || targetPathLastString === fieldPathArrayLastString) {
1996
+ return fieldPathArray;
1997
+ }
1998
+ }
1999
+
2000
+ if (!targetPath) return null;
2001
+
2002
+ return resolveIndexedRelativePath(targetPath, model?.context?.fieldIndex) || targetPath;
2003
+ };
1974
2004
  const getModelTargetPathKeys = (model: any): Set<string> => {
1975
2005
  const keys = new Set<string>();
1976
2006
  const fieldPathArray = normalizeNamePathForKey(model?.context?.fieldPathArray);
@@ -2058,6 +2088,13 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2058
2088
  ...props,
2059
2089
  });
2060
2090
 
2091
+ if (
2092
+ (action.name === 'linkageSetFieldProps' || action.name === 'subFormLinkageSetFieldProps') &&
2093
+ props?.hiddenModel === true
2094
+ ) {
2095
+ clearValueOnHiddenModelUids.add(model?.uid || String(model));
2096
+ }
2097
+
2061
2098
  if (allModels.indexOf(model) === -1) {
2062
2099
  allModels.push(model);
2063
2100
  }
@@ -2145,6 +2182,24 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2145
2182
  }
2146
2183
  }
2147
2184
 
2185
+ if (
2186
+ clearValueOnHiddenModelUids.has(uid) &&
2187
+ Object.prototype.hasOwnProperty.call(patchProps, 'hiddenModel') &&
2188
+ patchProps.hiddenModel === true
2189
+ ) {
2190
+ const targetPath = getModelTargetPathForHiddenClear(model);
2191
+ if (!targetPath) {
2192
+ console.warn('[linkageRules] Skip clearing hidden field value due to missing target path', {
2193
+ flowKey: ctx.flowKey,
2194
+ modelUid: ctx.model?.uid,
2195
+ targetUid: model?.uid,
2196
+ });
2197
+ } else {
2198
+ removePendingFormValuePatches(targetPath);
2199
+ addFormValuePatch({ path: targetPath, value: undefined });
2200
+ }
2201
+ }
2202
+
2148
2203
  model.__props = null;
2149
2204
  });
2150
2205
  hiddenStatePatches.forEach(({ model, hidden }) => {
@@ -135,7 +135,7 @@ function SetTargetDataScope(props) {
135
135
  return (
136
136
  <FilterGroup
137
137
  value={props.value}
138
- FilterItem={(p) => <VariableFilterItem {...p} model={targetModel} rightAsVariable />}
138
+ FilterItem={(p) => <VariableFilterItem {...p} model={targetModel} rightAsVariable maxAssociationFieldDepth={2} />}
139
139
  />
140
140
  );
141
141
  }
@@ -42,6 +42,7 @@ import {
42
42
  FLOW_SETTINGS_PREFERENCE_STORAGE_KEY,
43
43
  readFlowSettingsPreference,
44
44
  } from './flowSettingsPreference';
45
+ import { useAppListRender } from './AppListRender';
45
46
 
46
47
  const className1 = css`
47
48
  height: var(--nb-header-height);
@@ -391,7 +392,8 @@ export const AdminLayoutComponent = observer((props: any) => {
391
392
  );
392
393
  const designable = !isMobileSider && preferredFlowSettingsEnabled;
393
394
  const { styles } = useHeaderStyle();
394
- const { Component: AppsComponent } = useApplications();
395
+ const { appList } = useApplications();
396
+ const appListRender = useAppListRender();
395
397
  const flowSettingsSyncRef = useRef(0);
396
398
  const desiredFlowSettingsEnabledRef = useRef(false);
397
399
  const handleLayoutContentElementChange = useCallback(
@@ -601,6 +603,31 @@ export const AdminLayoutComponent = observer((props: any) => {
601
603
  overflowedIndicatorPopupClassName: styles.headerPopup,
602
604
  };
603
605
  }, [styles.headerPopup]);
606
+ const appSwitcherStyle = useMemo(
607
+ () => css`
608
+ .ant-layout-header.ant-pro-layout-header,
609
+ .ant-pro-top-nav-header {
610
+ z-index: 101 !important;
611
+ }
612
+
613
+ .ant-pro-layout-apps-popover {
614
+ z-index: 2000 !important;
615
+ }
616
+
617
+ .ant-pro-layout-apps-icon {
618
+ color: ${customToken.colorTextHeaderMenu || 'rgba(255, 255, 255, 0.85)'};
619
+ }
620
+
621
+ .ant-pro-layout-apps-icon:hover,
622
+ .ant-pro-layout-apps-icon-active {
623
+ color: ${customToken.colorTextHeaderMenuHover ||
624
+ customToken.colorTextHeaderMenu ||
625
+ 'rgba(255, 255, 255, 0.85)'};
626
+ background: ${customToken.colorBgHeaderMenuHover || 'rgba(255, 255, 255, 0.12)'};
627
+ }
628
+ `,
629
+ [customToken.colorBgHeaderMenuHover, customToken.colorTextHeaderMenu, customToken.colorTextHeaderMenuHover],
630
+ );
604
631
 
605
632
  const closeMobileMenu = useCallback(() => {
606
633
  if (!isMobileSider) {
@@ -634,16 +661,17 @@ export const AdminLayoutComponent = observer((props: any) => {
634
661
  {...props}
635
662
  contentStyle={contentStyle}
636
663
  siderWidth={customToken.siderWidth || 200}
637
- className={resetStyle}
664
+ className={`${resetStyle} ${appSwitcherStyle}`}
638
665
  location={location}
639
666
  route={route}
640
667
  actionsRender={props.actionsRender || actionsRender}
641
668
  logo={
642
669
  <div style={{ display: 'flex', alignItems: 'center' }}>
643
- {AppsComponent && <AppsComponent />}
644
670
  <NocoBaseLogo />
645
671
  </div>
646
672
  }
673
+ appList={appList}
674
+ appListRender={appListRender}
647
675
  title={''}
648
676
  layout="mix"
649
677
  splitMenus
@@ -55,6 +55,15 @@ const insertPositionToMethod = {
55
55
  afterEnd: 'insertAfter',
56
56
  } as const;
57
57
 
58
+ const omitMenuRuntimeProps = (props: Record<string, unknown> = {}) => {
59
+ const persistableProps = { ...props };
60
+ delete persistableProps.item;
61
+ delete persistableProps.dom;
62
+ delete persistableProps.options;
63
+ delete persistableProps.renderType;
64
+ return persistableProps;
65
+ };
66
+
58
67
  export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStructure> {
59
68
  private creationPersisted = false;
60
69
  private persistedStateHydrated = false;
@@ -261,6 +270,12 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
261
270
  return currentFlowCount > 0 || initialFlowCount > 0;
262
271
  }
263
272
 
273
+ serialize(): ReturnType<FlowModel['serialize']> {
274
+ const data = super.serialize();
275
+ data.props = omitMenuRuntimeProps(data.props);
276
+ return data;
277
+ }
278
+
264
279
  setHidden(value: boolean) {
265
280
  const previous = this.hidden;
266
281
  super.setHidden(value);
@@ -698,7 +713,6 @@ AdminLayoutMenuItemModel.registerFlow({
698
713
  AdminLayoutMenuItemModel.registerFlow({
699
714
  key: 'menuSettings',
700
715
  title: 'Menu settings',
701
- manual: true,
702
716
  steps: {
703
717
  edit: {
704
718
  title: 'Edit',