@nocobase/client-v2 2.1.0-alpha.26 → 2.1.0-alpha.27

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 (45) hide show
  1. package/es/BaseApplication.d.ts +1 -0
  2. package/es/flow/actions/dataScopeFilter.d.ts +9 -0
  3. package/es/flow/components/Grid/index.d.ts +5 -3
  4. package/es/flow/components/code-editor/types.d.ts +1 -0
  5. package/es/flow/internal/utils/rebuildFieldSubModel.d.ts +2 -1
  6. package/es/flow/models/base/GridModel.d.ts +19 -2
  7. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  8. package/es/flow/models/fields/JSFieldModel.d.ts +5 -0
  9. package/es/index.mjs +83 -83
  10. package/lib/index.js +83 -83
  11. package/package.json +5 -5
  12. package/src/BaseApplication.tsx +4 -0
  13. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +158 -0
  14. package/src/flow/actions/dataScope.tsx +6 -4
  15. package/src/flow/actions/dataScopeFilter.ts +70 -0
  16. package/src/flow/actions/setTargetDataScope.tsx +6 -5
  17. package/src/flow/components/Grid/index.tsx +66 -20
  18. package/src/flow/components/code-editor/__tests__/linter.test.ts +18 -0
  19. package/src/flow/components/code-editor/__tests__/runjsDiagnostics.test.ts +23 -0
  20. package/src/flow/components/code-editor/index.tsx +18 -17
  21. package/src/flow/components/code-editor/linter.ts +222 -158
  22. package/src/flow/components/code-editor/runjsDiagnostics.ts +161 -97
  23. package/src/flow/components/code-editor/types.ts +1 -0
  24. package/src/flow/internal/utils/__tests__/rebuildFieldSubModel.test.ts +77 -2
  25. package/src/flow/internal/utils/rebuildFieldSubModel.ts +21 -5
  26. package/src/flow/models/base/BlockGridModel.tsx +2 -2
  27. package/src/flow/models/base/GridModel.tsx +428 -195
  28. package/src/flow/models/base/__tests__/BlockGridModel.dragOverlayConfig.test.ts +44 -0
  29. package/src/flow/models/base/__tests__/GridModel.computeOverlayRect.test.ts +29 -0
  30. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +181 -2
  31. package/src/flow/models/base/__tests__/GridModel.resizeLayout.test.ts +124 -0
  32. package/src/flow/models/base/__tests__/GridModel.visibleLayout.test.ts +55 -15
  33. package/src/flow/models/blocks/details/DetailsGridModel.tsx +6 -6
  34. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +9 -5
  35. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +54 -14
  36. package/src/flow/models/blocks/filter-form/__tests__/FilterFormBlockModel.cleanup.test.ts +138 -0
  37. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +45 -0
  38. package/src/flow/models/blocks/form/FormGridModel.tsx +6 -6
  39. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +22 -0
  40. package/src/flow/models/blocks/table/JSColumnModel.tsx +30 -2
  41. package/src/flow/models/blocks/table/TableBlockModel.tsx +8 -1
  42. package/src/flow/models/blocks/table/TableColumnModel.tsx +1 -0
  43. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +51 -0
  44. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +49 -0
  45. package/src/flow/models/fields/JSFieldModel.tsx +54 -14
@@ -27,6 +27,50 @@ import {
27
27
  shouldPreprocessRunJSTemplates,
28
28
  } from '@nocobase/flow-engine';
29
29
 
30
+ const acornWalkBase = {
31
+ ...(acornWalk as any).base,
32
+ JSXElement(node: any, state: any, callback: any) {
33
+ callback(node.openingElement, state);
34
+ for (const child of node.children || []) callback(child, state);
35
+ if (node.closingElement) callback(node.closingElement, state);
36
+ },
37
+ JSXFragment(node: any, state: any, callback: any) {
38
+ callback(node.openingFragment, state);
39
+ for (const child of node.children || []) callback(child, state);
40
+ callback(node.closingFragment, state);
41
+ },
42
+ JSXOpeningElement(node: any, state: any, callback: any) {
43
+ callback(node.name, state);
44
+ for (const attribute of node.attributes || []) callback(attribute, state);
45
+ },
46
+ JSXClosingElement(node: any, state: any, callback: any) {
47
+ callback(node.name, state);
48
+ },
49
+ JSXAttribute(node: any, state: any, callback: any) {
50
+ callback(node.name, state);
51
+ if (node.value) callback(node.value, state);
52
+ },
53
+ JSXExpressionContainer(node: any, state: any, callback: any) {
54
+ callback(node.expression, state);
55
+ },
56
+ JSXSpreadAttribute(node: any, state: any, callback: any) {
57
+ callback(node.argument, state);
58
+ },
59
+ JSXMemberExpression(node: any, state: any, callback: any) {
60
+ callback(node.object, state);
61
+ callback(node.property, state);
62
+ },
63
+ JSXNamespacedName(node: any, state: any, callback: any) {
64
+ callback(node.namespace, state);
65
+ callback(node.name, state);
66
+ },
67
+ JSXIdentifier() {},
68
+ JSXText() {},
69
+ JSXEmptyExpression() {},
70
+ JSXOpeningFragment() {},
71
+ JSXClosingFragment() {},
72
+ };
73
+
30
74
  export type RunJSIssue = {
31
75
  type: 'lint' | 'runtime';
32
76
  message: string;
@@ -615,55 +659,63 @@ function collectHeuristicIssues(code: string): RunJSIssue[] {
615
659
 
616
660
  // Collect declared identifiers (very coarse, best-effort).
617
661
  try {
618
- acornWalk.full(ast, (node: any) => {
619
- switch (node?.type) {
620
- case 'VariableDeclarator':
621
- addPatternIds(node.id);
622
- break;
623
- case 'FunctionDeclaration':
624
- addId(node.id);
625
- (node.params || []).forEach(addPatternIds);
626
- break;
627
- case 'FunctionExpression':
628
- addId(node.id);
629
- (node.params || []).forEach(addPatternIds);
630
- break;
631
- case 'ArrowFunctionExpression':
632
- (node.params || []).forEach(addPatternIds);
633
- break;
634
- case 'CatchClause':
635
- addPatternIds((node as any).param);
636
- break;
637
- case 'ClassDeclaration':
638
- addId(node.id);
639
- break;
640
- default:
641
- break;
642
- }
643
- });
662
+ acornWalk.full(
663
+ ast,
664
+ (node: any) => {
665
+ switch (node?.type) {
666
+ case 'VariableDeclarator':
667
+ addPatternIds(node.id);
668
+ break;
669
+ case 'FunctionDeclaration':
670
+ addId(node.id);
671
+ (node.params || []).forEach(addPatternIds);
672
+ break;
673
+ case 'FunctionExpression':
674
+ addId(node.id);
675
+ (node.params || []).forEach(addPatternIds);
676
+ break;
677
+ case 'ArrowFunctionExpression':
678
+ (node.params || []).forEach(addPatternIds);
679
+ break;
680
+ case 'CatchClause':
681
+ addPatternIds((node as any).param);
682
+ break;
683
+ case 'ClassDeclaration':
684
+ addId(node.id);
685
+ break;
686
+ default:
687
+ break;
688
+ }
689
+ },
690
+ acornWalkBase,
691
+ );
644
692
  } catch (_) {
645
693
  // ignore
646
694
  }
647
695
 
648
696
  // 1) Non-callable call: 123(), 'x'(), (1+2)(), ({})()
649
697
  try {
650
- acornWalk.full(ast, (node: any) => {
651
- if (!node || typeof node.type !== 'string') return;
652
- if (node.type !== 'CallExpression') return;
653
- const callee = node.callee;
654
- const isCallableLike =
655
- callee &&
656
- (callee.type === 'Identifier' ||
657
- callee.type === 'MemberExpression' ||
658
- callee.type === 'FunctionExpression' ||
659
- callee.type === 'ArrowFunctionExpression' ||
660
- callee.type === 'CallExpression' ||
661
- callee.type === 'ChainExpression');
662
- if (!isCallableLike) {
663
- const pos = (callee as any)?.start ?? node.start ?? 0;
664
- pushAtPos(pos, 'no-noncallable-call', 'This expression is not callable.');
665
- }
666
- });
698
+ acornWalk.full(
699
+ ast,
700
+ (node: any) => {
701
+ if (!node || typeof node.type !== 'string') return;
702
+ if (node.type !== 'CallExpression') return;
703
+ const callee = node.callee;
704
+ const isCallableLike =
705
+ callee &&
706
+ (callee.type === 'Identifier' ||
707
+ callee.type === 'MemberExpression' ||
708
+ callee.type === 'FunctionExpression' ||
709
+ callee.type === 'ArrowFunctionExpression' ||
710
+ callee.type === 'CallExpression' ||
711
+ callee.type === 'ChainExpression');
712
+ if (!isCallableLike) {
713
+ const pos = (callee as any)?.start ?? node.start ?? 0;
714
+ pushAtPos(pos, 'no-noncallable-call', 'This expression is not callable.');
715
+ }
716
+ },
717
+ acornWalkBase,
718
+ );
667
719
  } catch (_) {
668
720
  // ignore
669
721
  }
@@ -673,37 +725,45 @@ function collectHeuristicIssues(code: string): RunJSIssue[] {
673
725
  try {
674
726
  const reported = new Set<string>();
675
727
  const allowedShort = new Set<string>(['t']);
676
- acornWalk.full(ast, (node: any) => {
677
- if (!node || typeof node.type !== 'string') return;
678
- if (node.type !== 'CallExpression') return;
679
- let callee = node.callee;
680
- if (callee?.type === 'ChainExpression') callee = callee.expression;
681
- if (!callee || callee.type !== 'MemberExpression') return;
682
- const obj = callee.object;
683
- if (!obj || obj.type !== 'Identifier' || obj.name !== 'ctx') return;
684
-
685
- let name: string | null = null;
686
- if (!callee.computed && callee.property?.type === 'Identifier') {
687
- name = callee.property.name;
688
- } else if (callee.computed && callee.property?.type === 'Literal' && typeof callee.property.value === 'string') {
689
- name = callee.property.value;
690
- }
728
+ acornWalk.full(
729
+ ast,
730
+ (node: any) => {
731
+ if (!node || typeof node.type !== 'string') return;
732
+ if (node.type !== 'CallExpression') return;
733
+ let callee = node.callee;
734
+ if (callee?.type === 'ChainExpression') callee = callee.expression;
735
+ if (!callee || callee.type !== 'MemberExpression') return;
736
+ const obj = callee.object;
737
+ if (!obj || obj.type !== 'Identifier' || obj.name !== 'ctx') return;
738
+
739
+ let name: string | null = null;
740
+ if (!callee.computed && callee.property?.type === 'Identifier') {
741
+ name = callee.property.name;
742
+ } else if (
743
+ callee.computed &&
744
+ callee.property?.type === 'Literal' &&
745
+ typeof callee.property.value === 'string'
746
+ ) {
747
+ name = callee.property.value;
748
+ }
691
749
 
692
- if (!name || typeof name !== 'string') return;
693
- const normalized = name.trim();
694
- if (!normalized || normalized.startsWith('_')) return;
695
- if (normalized.length > 2) return;
696
- if (allowedShort.has(normalized)) return;
697
- if (reported.has(normalized)) return;
698
-
699
- const pos = (callee.property as any)?.start ?? callee.start ?? node.start ?? 0;
700
- pushAtPos(
701
- pos,
702
- 'possible-undefined-ctx-member-call',
703
- `Possible undefined ctx method call: ctx.${normalized}(). This may be a typo or not available in the current ctx API.`,
704
- );
705
- reported.add(normalized);
706
- });
750
+ if (!name || typeof name !== 'string') return;
751
+ const normalized = name.trim();
752
+ if (!normalized || normalized.startsWith('_')) return;
753
+ if (normalized.length > 2) return;
754
+ if (allowedShort.has(normalized)) return;
755
+ if (reported.has(normalized)) return;
756
+
757
+ const pos = (callee.property as any)?.start ?? callee.start ?? node.start ?? 0;
758
+ pushAtPos(
759
+ pos,
760
+ 'possible-undefined-ctx-member-call',
761
+ `Possible undefined ctx method call: ctx.${normalized}(). This may be a typo or not available in the current ctx API.`,
762
+ );
763
+ reported.add(normalized);
764
+ },
765
+ acornWalkBase,
766
+ );
707
767
  } catch (_) {
708
768
  // ignore
709
769
  }
@@ -711,31 +771,35 @@ function collectHeuristicIssues(code: string): RunJSIssue[] {
711
771
  // 2) Possible undefined variable (exclude declarations and property keys)
712
772
  try {
713
773
  const reported = new Set<string>();
714
- acornWalk.ancestor(ast, {
715
- Identifier(node: any, ancestors: any[]) {
716
- const name = node?.name;
717
- if (!name || declared.has(name) || reported.has(name)) return;
718
- const parent = ancestors[ancestors.length - 2];
719
- if (!parent) return;
720
- if (
721
- (parent.type === 'VariableDeclarator' && parent.id === node) ||
722
- (parent.type === 'FunctionDeclaration' && parent.id === node) ||
723
- (parent.type === 'FunctionExpression' && parent.id === node) ||
724
- (parent.type === 'ClassDeclaration' && parent.id === node) ||
725
- (parent.type === 'ClassExpression' && parent.id === node) ||
726
- (parent.type === 'Property' && parent.key === node && parent.computed !== true) ||
727
- (parent.type === 'MemberExpression' && parent.property === node && parent.computed !== true) ||
728
- (parent.type === 'LabeledStatement' && parent.label === node) ||
729
- (parent.type === 'BreakStatement' && parent.label === node) ||
730
- (parent.type === 'ContinueStatement' && parent.label === node)
731
- ) {
732
- return;
733
- }
734
- const pos = (node as any).start ?? 0;
735
- pushAtPos(pos, 'possible-undefined-variable', `Possible undefined variable: ${name}`);
736
- reported.add(name);
774
+ acornWalk.ancestor(
775
+ ast,
776
+ {
777
+ Identifier(node: any, ancestors: any[]) {
778
+ const name = node?.name;
779
+ if (!name || declared.has(name) || reported.has(name)) return;
780
+ const parent = ancestors[ancestors.length - 2];
781
+ if (!parent) return;
782
+ if (
783
+ (parent.type === 'VariableDeclarator' && parent.id === node) ||
784
+ (parent.type === 'FunctionDeclaration' && parent.id === node) ||
785
+ (parent.type === 'FunctionExpression' && parent.id === node) ||
786
+ (parent.type === 'ClassDeclaration' && parent.id === node) ||
787
+ (parent.type === 'ClassExpression' && parent.id === node) ||
788
+ (parent.type === 'Property' && parent.key === node && parent.computed !== true) ||
789
+ (parent.type === 'MemberExpression' && parent.property === node && parent.computed !== true) ||
790
+ (parent.type === 'LabeledStatement' && parent.label === node) ||
791
+ (parent.type === 'BreakStatement' && parent.label === node) ||
792
+ (parent.type === 'ContinueStatement' && parent.label === node)
793
+ ) {
794
+ return;
795
+ }
796
+ const pos = (node as any).start ?? 0;
797
+ pushAtPos(pos, 'possible-undefined-variable', `Possible undefined variable: ${name}`);
798
+ reported.add(name);
799
+ },
737
800
  },
738
- });
801
+ acornWalkBase,
802
+ );
739
803
  } catch (_) {
740
804
  // ignore
741
805
  }
@@ -17,6 +17,7 @@ import { RunLog } from './hooks/useCodeRunner';
17
17
  export interface EditorRef {
18
18
  write(document: string): void;
19
19
  read(): string;
20
+ run?(): Promise<unknown>;
20
21
  buttonGroupHeight?: number;
21
22
  snippetEntries: SnippetEntry[];
22
23
  logs: RunLog[];
@@ -35,7 +35,7 @@ describe('rebuildFieldSubModel', () => {
35
35
  });
36
36
  });
37
37
 
38
- test('rebuilds field with same uid and updates binding use', async () => {
38
+ test('rebuilds field with same uid and direct target field model use', async () => {
39
39
  const staleClickHandler = () => null;
40
40
  const parent = engine.createModel<DummyParentModel>({
41
41
  use: DummyParentModel,
@@ -66,7 +66,8 @@ describe('rebuildFieldSubModel', () => {
66
66
 
67
67
  expect(rebuilt).toBeInstanceOf(DummyTargetFieldModel);
68
68
  expect(rebuilt.uid).toBe('field-1');
69
- expect(getFieldBindingUse(rebuilt)).toBe('DummyTargetFieldModel');
69
+ expect(getFieldBindingUse(rebuilt)).toBeUndefined();
70
+ expect(rebuilt.use).toBe('DummyTargetFieldModel');
70
71
  expect(rebuilt.props).toMatchObject({ added: 'yes', pattern: 'readPretty' });
71
72
  expect((rebuilt.props as any).onClick).toBeUndefined();
72
73
 
@@ -106,4 +107,78 @@ describe('rebuildFieldSubModel', () => {
106
107
  expect(Array.isArray(cols)).toBe(true);
107
108
  expect(cols.map((c) => c.uid)).toEqual(['col-1', 'col-2']);
108
109
  });
110
+
111
+ test('preserves compatible step params when rebuilding with the same field model use', async () => {
112
+ const parent = engine.createModel<DummyParentModel>({
113
+ use: DummyParentModel,
114
+ uid: 'parent-3',
115
+ subModels: {
116
+ field: {
117
+ use: DummyTargetFieldModel,
118
+ uid: 'field-3',
119
+ stepParams: {
120
+ fieldSettings: { init: { initKey: true } },
121
+ displayFieldSettings: {
122
+ overflowMode: {
123
+ overflowMode: true,
124
+ },
125
+ },
126
+ } as any,
127
+ },
128
+ },
129
+ });
130
+
131
+ await rebuildFieldSubModel({
132
+ parentModel: parent,
133
+ targetUse: 'DummyTargetFieldModel',
134
+ fieldSettingsInit: { fieldPath: 'title' },
135
+ });
136
+
137
+ const rebuilt = parent.subModels.field as DummyTargetFieldModel;
138
+ expect(rebuilt.stepParams).toEqual({
139
+ fieldSettings: {
140
+ init: { fieldPath: 'title' },
141
+ },
142
+ displayFieldSettings: {
143
+ overflowMode: {
144
+ overflowMode: true,
145
+ },
146
+ },
147
+ });
148
+ });
149
+
150
+ test('drops incompatible step params when rebuilding to a different field model use', async () => {
151
+ const parent = engine.createModel<DummyParentModel>({
152
+ use: DummyParentModel,
153
+ uid: 'parent-4',
154
+ subModels: {
155
+ field: {
156
+ use: FieldModel,
157
+ uid: 'field-4',
158
+ stepParams: {
159
+ fieldBinding: { use: 'FieldModel' },
160
+ fieldSettings: { init: { initKey: true } },
161
+ numberSettings: {
162
+ format: {
163
+ separator: '0,0.00',
164
+ },
165
+ },
166
+ } as any,
167
+ },
168
+ },
169
+ });
170
+
171
+ await rebuildFieldSubModel({
172
+ parentModel: parent,
173
+ targetUse: 'DummyTargetFieldModel',
174
+ fieldSettingsInit: { fieldPath: 'title' },
175
+ });
176
+
177
+ const rebuilt = parent.subModels.field as DummyTargetFieldModel;
178
+ expect(rebuilt.stepParams).toEqual({
179
+ fieldSettings: {
180
+ init: { fieldPath: 'title' },
181
+ },
182
+ });
183
+ });
109
184
  });
@@ -10,8 +10,9 @@
10
10
  /**
11
11
  * 通用的字段子模型重建工具:
12
12
  * - 保留原有 uid
13
- * - 通过 FieldModel 入口 + fieldBinding.use 动态选择目标字段类
13
+ * - 直接重建为目标字段类,保持与 defineChildren 初始创建逻辑一致
14
14
  * - 支持同步父项模式(pattern)
15
+ * - 同一字段模型类型下保留已有字段设置;切换到其他字段模型类型时丢弃不兼容设置
15
16
  * - 重建后触发 beforeRender(useCache: false)
16
17
  */
17
18
  import { FieldModel } from '../../models/base/FieldModel';
@@ -39,6 +40,16 @@ type RebuildOptions = {
39
40
  fieldSettingsInit?: unknown;
40
41
  };
41
42
 
43
+ function normalizeModelUse(value: unknown): string | undefined {
44
+ if (typeof value === 'string') {
45
+ return value;
46
+ }
47
+ if (typeof value === 'function' && value.name) {
48
+ return value.name;
49
+ }
50
+ return undefined;
51
+ }
52
+
42
53
  export function getFieldBindingUse(fieldModel?: FieldModel): string | undefined {
43
54
  const bindingUse = (fieldModel?.stepParams as FieldStepParams | undefined)?.fieldBinding?.use;
44
55
  return typeof bindingUse === 'string' ? bindingUse : undefined;
@@ -61,13 +72,18 @@ export async function rebuildFieldSubModel({
61
72
  delete prevSubModels[key];
62
73
  }
63
74
  }
64
- const prevStepParams: FieldStepParams = (fieldModel?.stepParams as FieldStepParams) || {};
75
+ const currentUse = normalizeModelUse(getFieldBindingUse(fieldModel) || fieldModel?.use);
76
+ const shouldPreserveStepParams = currentUse === targetUse;
77
+ const prevStepParams: FieldStepParams = shouldPreserveStepParams
78
+ ? (fieldModel?.stepParams as FieldStepParams) || {}
79
+ : {};
65
80
  const nextFieldSettingsInit = fieldSettingsInit ?? parentModel.getFieldSettingsInitParams?.();
81
+ const { fieldBinding: _fieldBinding, ...restStepParams } = prevStepParams;
66
82
 
67
83
  const nextStepParams: FieldStepParams = {
68
- ...prevStepParams,
69
- fieldBinding: { ...prevStepParams.fieldBinding, use: targetUse },
84
+ ...restStepParams,
70
85
  fieldSettings: {
86
+ ...(restStepParams.fieldSettings || {}),
71
87
  init: nextFieldSettingsInit,
72
88
  },
73
89
  };
@@ -81,7 +97,7 @@ export async function rebuildFieldSubModel({
81
97
 
82
98
  const subModel = parentModel.setSubModel('field', {
83
99
  uid: fieldUid,
84
- use: FieldModel,
100
+ use: targetUse,
85
101
  props: { ...(defaultProps || {}), ...(pattern ? { pattern } : {}) },
86
102
  stepParams: nextStepParams as StepParams,
87
103
  // Preserve existing subModels (e.g. SubTable columns) so switching field component back and forth
@@ -27,8 +27,8 @@ export class BlockGridModel extends GridModel {
27
27
  dragOverlayConfig: DragOverlayConfig = {
28
28
  // 列内插入
29
29
  columnInsert: {
30
- before: { offsetTop: -24 },
31
- after: { offsetTop: 24 },
30
+ before: { offsetTop: -12 },
31
+ after: { offsetTop: 12 },
32
32
  },
33
33
  // 列边缘
34
34
  columnEdge: {