@nocobase/client-v2 2.1.0-beta.26 → 2.1.0-beta.29

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 (83) hide show
  1. package/es/components/form/JsonTextArea.d.ts +18 -0
  2. package/es/components/index.d.ts +1 -0
  3. package/es/flow/actions/dateRangeLimit.d.ts +9 -0
  4. package/es/flow/actions/index.d.ts +1 -0
  5. package/es/flow/components/code-editor/types.d.ts +1 -0
  6. package/es/flow/models/base/PageModel/PageModel.d.ts +4 -0
  7. package/es/flow/models/base/PageModel/RootPageModel.d.ts +9 -0
  8. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +15 -6
  9. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +7 -0
  10. package/es/flow/models/blocks/shared/filterOperators.d.ts +9 -0
  11. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +2 -0
  12. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +20 -0
  13. package/es/flow/models/fields/JSEditableFieldModel.d.ts +4 -0
  14. package/es/flow-compat/data.d.ts +9 -2
  15. package/es/flow-compat/index.d.ts +1 -1
  16. package/es/index.d.ts +1 -1
  17. package/es/index.mjs +97 -90
  18. package/lib/index.js +99 -92
  19. package/package.json +6 -5
  20. package/src/BaseApplication.tsx +1 -1
  21. package/src/__tests__/app.test.tsx +23 -6
  22. package/src/components/form/JsonTextArea.tsx +129 -0
  23. package/src/components/index.ts +1 -0
  24. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +478 -0
  25. package/src/flow/actions/__tests__/pattern.test.ts +190 -0
  26. package/src/flow/actions/dateRangeLimit.tsx +66 -0
  27. package/src/flow/actions/index.ts +1 -0
  28. package/src/flow/actions/linkageRules.tsx +117 -19
  29. package/src/flow/actions/openView.tsx +2 -1
  30. package/src/flow/actions/pattern.tsx +25 -2
  31. package/src/flow/actions/titleField.tsx +8 -3
  32. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +7 -1
  33. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +117 -0
  34. package/src/flow/components/FieldAssignValueInput.tsx +1 -0
  35. package/src/flow/components/code-editor/__tests__/linter.test.ts +18 -0
  36. package/src/flow/components/code-editor/__tests__/runjsDiagnostics.test.ts +23 -0
  37. package/src/flow/components/code-editor/index.tsx +18 -17
  38. package/src/flow/components/code-editor/linter.ts +222 -158
  39. package/src/flow/components/code-editor/runjsDiagnostics.ts +161 -97
  40. package/src/flow/components/code-editor/types.ts +1 -0
  41. package/src/flow/components/filter/LinkageFilterItem.tsx +6 -5
  42. package/src/flow/components/filter/VariableFilterItem.tsx +14 -13
  43. package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +33 -0
  44. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -5
  45. package/src/flow/internal/utils/__tests__/titleFieldQuickSync.test.ts +1 -0
  46. package/src/flow/internal/utils/titleFieldQuickSync.ts +2 -2
  47. package/src/flow/models/actions/FilterActionModel.tsx +17 -9
  48. package/src/flow/models/base/PageModel/PageModel.tsx +15 -3
  49. package/src/flow/models/base/PageModel/RootPageModel.tsx +37 -2
  50. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +73 -0
  51. package/src/flow/models/base/PageModel/__tests__/RootPageModel.test.ts +116 -0
  52. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +200 -36
  53. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +270 -1
  54. package/src/flow/models/blocks/filter-form/__tests__/customFieldOperators.test.tsx +23 -0
  55. package/src/flow/models/blocks/filter-form/customFieldOperators.ts +12 -1
  56. package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +22 -8
  57. package/src/flow/models/blocks/filter-form/fields/__tests__/FilterFormCustomFieldModel.recordSelect.test.tsx +18 -0
  58. package/src/flow/models/blocks/filter-manager/FilterManager.ts +51 -1
  59. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +75 -0
  60. package/src/flow/models/blocks/form/FormItemModel.tsx +48 -28
  61. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +167 -1
  62. package/src/flow/models/blocks/form/value-runtime/runtime.ts +103 -11
  63. package/src/flow/models/blocks/shared/filterOperators.ts +14 -0
  64. package/src/flow/models/blocks/table/TableBlockModel.tsx +19 -3
  65. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +27 -3
  66. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +47 -0
  67. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +42 -0
  68. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.refresh.test.tsx +122 -0
  69. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +2 -0
  70. package/src/flow/models/fields/ClickableFieldModel.tsx +21 -9
  71. package/src/flow/models/fields/DateTimeFieldModel/DateOnlyFieldModel.tsx +9 -0
  72. package/src/flow/models/fields/DateTimeFieldModel/DateTimeFieldModel.tsx +4 -0
  73. package/src/flow/models/fields/DateTimeFieldModel/DateTimeNoTzFieldModel.tsx +9 -0
  74. package/src/flow/models/fields/DateTimeFieldModel/DateTimeTzFieldModel.tsx +9 -0
  75. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +242 -0
  76. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +152 -0
  77. package/src/flow/models/fields/DividerItemModel.tsx +30 -15
  78. package/src/flow/models/fields/JSEditableFieldModel.tsx +110 -14
  79. package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +87 -0
  80. package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +210 -0
  81. package/src/flow-compat/data.ts +25 -3
  82. package/src/flow-compat/index.ts +7 -1
  83. package/src/index.ts +1 -1
@@ -0,0 +1,66 @@
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 { defineAction, tExpr } from '@nocobase/flow-engine';
11
+ import { FieldAssignValueInput } from '../components/FieldAssignValueInput';
12
+
13
+ function normalizeDateRangeValue(value: any) {
14
+ if (value === '' || value === null || typeof value === 'undefined') {
15
+ return undefined;
16
+ }
17
+ return value;
18
+ }
19
+
20
+ export const dateRangeLimit = defineAction({
21
+ name: 'dateRangeLimit',
22
+ title: tExpr('Date range limit'),
23
+ uiMode: {
24
+ type: 'dialog',
25
+ props: {
26
+ width: 720,
27
+ },
28
+ },
29
+ uiSchema(ctx: any) {
30
+ const targetPath = ctx.model.context?.collectionField?.name || '';
31
+ const dateLimitInputProps = {
32
+ targetPath,
33
+ enableDateVariableAsConstant: true,
34
+ };
35
+
36
+ return {
37
+ _minDate: {
38
+ type: 'string',
39
+ title: tExpr('MinDate'),
40
+ 'x-decorator': 'FormItem',
41
+ 'x-component': FieldAssignValueInput,
42
+ 'x-component-props': dateLimitInputProps,
43
+ },
44
+ _maxDate: {
45
+ type: 'string',
46
+ title: tExpr('MaxDate'),
47
+ 'x-decorator': 'FormItem',
48
+ 'x-component': FieldAssignValueInput,
49
+ 'x-component-props': dateLimitInputProps,
50
+ },
51
+ };
52
+ },
53
+ defaultParams(ctx: any) {
54
+ return {
55
+ _minDate: normalizeDateRangeValue(ctx.model.props?._minDate),
56
+ _maxDate: normalizeDateRangeValue(ctx.model.props?._maxDate),
57
+ };
58
+ },
59
+ useRawParams: true,
60
+ async handler(ctx: any, params) {
61
+ ctx.model.setProps({
62
+ _minDate: normalizeDateRangeValue(params?._minDate),
63
+ _maxDate: normalizeDateRangeValue(params?._maxDate),
64
+ });
65
+ },
66
+ });
@@ -20,6 +20,7 @@ export * from './refreshTargetBlocks';
20
20
  export * from './setTargetDataScope';
21
21
  export { titleField } from './titleField';
22
22
  export * from './dateTimeFormat';
23
+ export * from './dateRangeLimit';
23
24
  export * from './sortingRules';
24
25
  export * from './dataLoadingMode';
25
26
  export * from './renderMode';
@@ -51,11 +51,7 @@ import {
51
51
  getCollectionFromModel,
52
52
  isToManyAssociationField,
53
53
  } from '../internal/utils/modelUtils';
54
- import {
55
- namePathToPathKey,
56
- parsePathString,
57
- resolveDynamicNamePath,
58
- } from '../models/blocks/form/value-runtime/path';
54
+ import { namePathToPathKey, parsePathString, resolveDynamicNamePath } from '../models/blocks/form/value-runtime/path';
59
55
  import { ensureFormValueDrivenLinkageRefresh } from './linkageRulesFormValueRefresh';
60
56
 
61
57
  interface LinkageRule {
@@ -1759,7 +1755,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1759
1755
 
1760
1756
  const linkageRules: LinkageRule[] = params.value as LinkageRule[];
1761
1757
  const allModels: FlowModel[] = ctx.model.__allModels || (ctx.model.__allModels = []);
1762
- const directValuePatches: Array<{ path: any; value: any }> = [];
1758
+ const directValuePatches: Array<{ path: Array<string | number>; value: any; whenEmpty?: boolean }> = [];
1763
1759
  const rootCollection = getCollectionFromModel((ctx.model as any)?.context?.blockModel ?? ctx.model);
1764
1760
  const isSafeToWriteAssociationSubpath = (namePath: any): boolean => {
1765
1761
  if (!Array.isArray(namePath) || !namePath.length) return true;
@@ -1800,6 +1796,24 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1800
1796
  const fieldIndex = (ctx.model as any)?.context?.fieldIndex;
1801
1797
  return resolveDynamicNamePath(path, fieldIndex);
1802
1798
  };
1799
+ const getDefaultPatchRuntime = () => {
1800
+ const blockModel = (ctx.model as any)?.context?.blockModel ?? ctx.model;
1801
+ return blockModel?.formValueRuntime ?? (ctx as any)?.formValueRuntime;
1802
+ };
1803
+ const rememberAppliedDefaultPatches = (patches: typeof directValuePatches) => {
1804
+ const runtime = getDefaultPatchRuntime();
1805
+ if (typeof runtime?.recordDefaultValuePatch !== 'function') return;
1806
+
1807
+ const lastPatchByPathKey = new Map<string, (typeof directValuePatches)[number]>();
1808
+ for (const patch of patches) {
1809
+ lastPatchByPathKey.set(namePathToPathKey(patch.path), patch);
1810
+ }
1811
+
1812
+ for (const patch of lastPatchByPathKey.values()) {
1813
+ if (!patch.whenEmpty) continue;
1814
+ runtime.recordDefaultValuePatch(patch.path, patch.value);
1815
+ }
1816
+ };
1803
1817
  const addFormValuePatch = (patch: { path: any; value: any; whenEmpty?: boolean }) => {
1804
1818
  if (!patch) return;
1805
1819
  const path = (patch as any)?.path;
@@ -1823,20 +1837,33 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1823
1837
  return;
1824
1838
  }
1825
1839
  const whenEmpty = !!(patch as any)?.whenEmpty;
1840
+ const value = (patch as any)?.value;
1826
1841
  try {
1827
1842
  const form = ctx.model?.context?.form;
1828
1843
  const current = form?.getFieldValue?.(resolvedPath);
1829
- if (whenEmpty && typeof current !== 'undefined' && current !== null && current !== '') {
1830
- return;
1844
+ if (whenEmpty) {
1845
+ const runtime = getDefaultPatchRuntime();
1846
+ if (typeof runtime?.canApplyDefaultValuePatch === 'function') {
1847
+ const canApply = runtime.canApplyDefaultValuePatch(resolvedPath, value);
1848
+ if (!canApply) {
1849
+ return;
1850
+ }
1851
+ } else if (typeof current !== 'undefined' && current !== null && current !== '') {
1852
+ return;
1853
+ }
1831
1854
  }
1832
- if (_.isEqual(current, (patch as any)?.value)) {
1855
+ if (_.isEqual(current, value)) {
1833
1856
  return;
1834
1857
  }
1835
1858
  } catch {
1836
1859
  // ignore
1837
1860
  }
1838
1861
 
1839
- directValuePatches.push({ path: resolvedPath, value: (patch as any)?.value });
1862
+ directValuePatches.push({
1863
+ path: resolvedPath,
1864
+ value,
1865
+ ...(whenEmpty ? { whenEmpty: true } : {}),
1866
+ });
1840
1867
  };
1841
1868
 
1842
1869
  const getModelTargetPathForPatch = (model: any): string | null => {
@@ -2018,6 +2045,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2018
2045
  if (typeof directSetter === 'function') {
2019
2046
  try {
2020
2047
  await trySetFormValues(directSetter, directCtx, 'linkage');
2048
+ rememberAppliedDefaultPatches(allPatches);
2021
2049
  return;
2022
2050
  } catch (error) {
2023
2051
  console.warn('[linkageRules] Failed to set form values via setFormValues', {
@@ -2034,6 +2062,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2034
2062
  if (typeof blockSetter === 'function') {
2035
2063
  try {
2036
2064
  await trySetFormValues(blockSetter, blockCtx, 'linkage');
2065
+ rememberAppliedDefaultPatches(allPatches);
2037
2066
  return;
2038
2067
  } catch (error) {
2039
2068
  console.warn('[linkageRules] Failed to set form values via setFormValues', {
@@ -2063,6 +2092,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2063
2092
  console.warn('[linkageRules] Failed to set form field value (fallback setFieldValue)', { path }, error);
2064
2093
  }
2065
2094
  });
2095
+ rememberAppliedDefaultPatches(allPatches);
2066
2096
  };
2067
2097
 
2068
2098
  export const blockLinkageRules = defineAction({
@@ -2348,33 +2378,95 @@ export const fieldLinkageRules = defineAction({
2348
2378
  return;
2349
2379
  }
2350
2380
 
2351
- const getRowScopeKeyFromModel = (model: any): string | null => {
2381
+ const getRowFieldIndexInfoFromModel = (model: any) => {
2352
2382
  const fieldIndex = model?.context?.fieldIndex;
2353
2383
  const arr = Array.isArray(fieldIndex) ? fieldIndex : [];
2354
- if (!arr.length) return null;
2384
+ const normalized = arr.filter((it): it is string => typeof it === 'string');
2355
2385
  const entries: Array<{ name: string; index: number }> = [];
2356
- for (const it of arr) {
2357
- if (typeof it !== 'string') continue;
2386
+ const path: Array<string | number> = [];
2387
+ for (const it of normalized) {
2358
2388
  const [name, indexStr] = it.split(':');
2359
2389
  const index = Number(indexStr);
2360
2390
  if (!name || Number.isNaN(index)) continue;
2361
2391
  entries.push({ name, index });
2392
+ path.push(name, index);
2362
2393
  }
2394
+ return { normalized, entries, path };
2395
+ };
2396
+
2397
+ const getRowScopeKeyFromModel = (model: any): string | null => {
2398
+ const { entries } = getRowFieldIndexInfoFromModel(model);
2363
2399
  if (!entries.length) return null;
2364
2400
  const deepest = entries[entries.length - 1].name;
2365
2401
  const occurrence = entries.reduce((count, e) => (e.name === deepest ? count + 1 : count), 0);
2366
2402
  return `${deepest}#${occurrence}`;
2367
2403
  };
2368
2404
 
2405
+ const getRowFieldIndexKeyFromModel = (model: any): string | null => {
2406
+ const { normalized } = getRowFieldIndexInfoFromModel(model);
2407
+ if (!normalized.length) return null;
2408
+ return JSON.stringify(normalized);
2409
+ };
2410
+
2411
+ const getRowPathFromModel = (model: any): Array<string | number> | null => {
2412
+ const { path } = getRowFieldIndexInfoFromModel(model);
2413
+ return path.length ? path : null;
2414
+ };
2415
+
2416
+ const getFormForRowFork = (model: any) => {
2417
+ return (
2418
+ model?.context?.form ??
2419
+ model?.context?.blockModel?.context?.form ??
2420
+ ctx.model?.context?.form ??
2421
+ ctx.model?.context?.blockModel?.context?.form
2422
+ );
2423
+ };
2424
+
2425
+ const isRowForkMountedInCurrentValue = (model: any): boolean => {
2426
+ const rowPath = getRowPathFromModel(model);
2427
+ if (!rowPath) return true;
2428
+ const form = getFormForRowFork(model);
2429
+ if (!form || typeof form.getFieldValue !== 'function') return true;
2430
+ return typeof form.getFieldValue(rowPath as any) !== 'undefined';
2431
+ };
2432
+
2433
+ const hasRowItemContext = (model: any): boolean => {
2434
+ const itemOptions = model?.context?.getPropertyOptions?.('item');
2435
+ if (itemOptions) return true;
2436
+ return typeof model?.context?.item !== 'undefined';
2437
+ };
2438
+
2439
+ const hasSubTableRowMarker = (model: any): boolean => {
2440
+ const markerOptions = model?.context?.getPropertyOptions?.('subTableRowFork');
2441
+ if (markerOptions) return true;
2442
+ return (
2443
+ typeof model?.context?.subTableRowFork !== 'undefined' ||
2444
+ typeof model?.subTableRowFork !== 'undefined' ||
2445
+ model?.subTableRowFork === true
2446
+ );
2447
+ };
2448
+
2369
2449
  const isRowGridForkModel = (model: any): boolean => {
2370
2450
  if (!model || typeof model !== 'object') return false;
2371
2451
  if ((model as any)?.subModels?.field) return false;
2372
2452
  if (!(model as any)?.subModels?.items) return false;
2373
- return !!getRowScopeKeyFromModel(model);
2453
+ return !!getRowScopeKeyFromModel(model) && !!getRowFieldIndexKeyFromModel(model);
2454
+ };
2455
+
2456
+ const isSubTableRowForkModel = (model: any): boolean => {
2457
+ if (!model || typeof model !== 'object') return false;
2458
+ if (!hasSubTableRowMarker(model)) return false;
2459
+ if (!getRowScopeKeyFromModel(model) || !getRowFieldIndexKeyFromModel(model)) return false;
2460
+ return hasRowItemContext(model);
2461
+ };
2462
+
2463
+ const isRowScopedForkModel = (model: any): boolean => {
2464
+ return isRowGridForkModel(model) || isSubTableRowForkModel(model);
2374
2465
  };
2375
2466
 
2376
- const collectRowGridForksByKey = (): Map<string, FlowModel[]> => {
2467
+ const collectRowScopedForksByKey = (): Map<string, FlowModel[]> => {
2377
2468
  const out = new Map<string, FlowModel[]>();
2469
+ const seenByKey = new Map<string, Set<string>>();
2378
2470
  const engine = ctx.engine;
2379
2471
  if (!engine?.forEachModel) return out;
2380
2472
 
@@ -2383,9 +2475,15 @@ export const fieldLinkageRules = defineAction({
2383
2475
  if (!forks || typeof forks.forEach !== 'function') return;
2384
2476
  forks.forEach((fork: any) => {
2385
2477
  if (!fork || fork.disposed) return;
2386
- if (!isRowGridForkModel(fork)) return;
2478
+ if (!isRowScopedForkModel(fork)) return;
2479
+ if (!isRowForkMountedInCurrentValue(fork)) return;
2387
2480
  const rowScopeKey = getRowScopeKeyFromModel(fork);
2388
- if (!rowScopeKey) return;
2481
+ const fieldIndexKey = getRowFieldIndexKeyFromModel(fork);
2482
+ if (!rowScopeKey || !fieldIndexKey) return;
2483
+ const seen = seenByKey.get(rowScopeKey) || new Set<string>();
2484
+ if (seen.has(fieldIndexKey)) return;
2485
+ seen.add(fieldIndexKey);
2486
+ seenByKey.set(rowScopeKey, seen);
2389
2487
  const arr = out.get(rowScopeKey) || [];
2390
2488
  arr.push(fork as FlowModel);
2391
2489
  out.set(rowScopeKey, arr);
@@ -2396,7 +2494,7 @@ export const fieldLinkageRules = defineAction({
2396
2494
  };
2397
2495
 
2398
2496
  const runRowScoped = async (): Promise<boolean> => {
2399
- const forksByKey = collectRowGridForksByKey();
2497
+ const forksByKey = collectRowScopedForksByKey();
2400
2498
  let hasAnyRowFork = false;
2401
2499
  for (const [rowScopeKey, rowParams] of rowParamsByKey.entries()) {
2402
2500
  const forks = forksByKey.get(rowScopeKey) || [];
@@ -345,7 +345,8 @@ export const openView = defineAction({
345
345
  target: ctx.inputArgs.target || ctx.layoutContentElement,
346
346
  dataSourceKey: runtimeDataSourceKey ?? actionDefaults.dataSourceKey,
347
347
  collectionName: runtimeCollectionName ?? actionDefaults.collectionName,
348
- associationName: runtimeAssociationName ?? actionDefaults.associationName,
348
+ associationName:
349
+ typeof runtimeAssociationName !== 'undefined' ? runtimeAssociationName : actionDefaults.associationName,
349
350
  filterByTk: mergedFilterByTk,
350
351
  sourceId: mergedSourceId,
351
352
  tabUid: mergedTabUid,
@@ -7,9 +7,25 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { BindingOptions, defineAction, tExpr, DisplayItemModel } from '@nocobase/flow-engine';
10
+ import { defineAction, tExpr } from '@nocobase/flow-engine';
11
11
  import { DetailsItemModel } from '../models/blocks/details/DetailsItemModel';
12
- import { rebuildFieldSubModel } from '../internal/utils/rebuildFieldSubModel';
12
+ import { getFieldBindingUse, rebuildFieldSubModel } from '../internal/utils/rebuildFieldSubModel';
13
+
14
+ type PatternAwareFieldModelMeta = {
15
+ preserveOnPatternChange?: boolean;
16
+ };
17
+
18
+ type PatternAwareFieldModel = {
19
+ scheduleApplyJsSettings?: () => void;
20
+ };
21
+
22
+ function shouldPreserveFieldModelOnPatternChange(ctx: any) {
23
+ const fieldModel = ctx.model.subModels.field;
24
+ const fieldUse = getFieldBindingUse(fieldModel) ?? fieldModel?.use;
25
+ const ModelClass = typeof fieldUse === 'string' ? ctx.engine.getModelClass(fieldUse) : fieldUse;
26
+
27
+ return ((ModelClass?.meta as PatternAwareFieldModelMeta | undefined)?.preserveOnPatternChange ?? false) === true;
28
+ }
13
29
 
14
30
  export const pattern = defineAction({
15
31
  name: 'pattern',
@@ -56,6 +72,13 @@ export const pattern = defineAction({
56
72
  };
57
73
  },
58
74
  afterParamsSave: async (ctx: any, params, previousParams) => {
75
+ if (shouldPreserveFieldModelOnPatternChange(ctx)) {
76
+ if (params.pattern !== previousParams.pattern) {
77
+ (ctx.model.subModels.field as PatternAwareFieldModel | undefined)?.scheduleApplyJsSettings?.();
78
+ }
79
+ return;
80
+ }
81
+
59
82
  const targetCollection = ctx.collectionField.targetCollection;
60
83
  const targetCollectionTitleField = targetCollection?.getField(
61
84
  ctx.model.subModels.field.props?.fieldNames?.label || ctx.model.props.titleField,
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { defineAction, DisplayItemModel, FlowModelContext, tExpr } from '@nocobase/flow-engine';
11
- import { isTitleField } from '../../flow-compat';
11
+ import { getFlowFieldInterfaceOptions, isTitleFieldInterface } from '../../flow-compat';
12
12
 
13
13
  const normalizeFilterTargetKey = (filterTargetKey: any) => {
14
14
  if (typeof filterTargetKey === 'string') {
@@ -37,10 +37,15 @@ export const titleField = defineAction({
37
37
  title: tExpr('Title field'),
38
38
  uiMode: (ctx) => {
39
39
  const targetCollection = ctx.collectionField.targetCollection;
40
- const dataSourceManager = ctx.app.dataSourceManager;
40
+ const dataSourceManager =
41
+ ctx.dataSourceManager || ctx.model?.context?.dataSourceManager || ctx.app?.dataSourceManager;
41
42
  const targetFields = targetCollection?.getFields?.() ?? [];
42
43
  const options = targetFields
43
- .filter((field) => isTitleField(dataSourceManager, field.options))
44
+ .filter((field) =>
45
+ isTitleFieldInterface(
46
+ getFlowFieldInterfaceOptions(field.options?.interface || field.interface, dataSourceManager),
47
+ ),
48
+ )
44
49
  .map((field) => ({
45
50
  value: field.name,
46
51
  label: field?.title,
@@ -47,6 +47,8 @@ interface RouteLike {
47
47
  pathname?: string;
48
48
  }
49
49
 
50
+ const hasUsableSourceId = (sourceId: unknown) => sourceId !== undefined && sourceId !== null && String(sourceId) !== '';
51
+
50
52
  /**
51
53
  * 管理 admin 场景下每个 page 的 v2 视图栈编排。
52
54
  * 该协调器只负责状态机和开关视图,不直接绑定 React 生命周期。
@@ -264,6 +266,10 @@ export class AdminLayoutRouteCoordinator {
264
266
  const destroyRef = React.createRef<(result?: any, force?: boolean) => void>();
265
267
  const updateRef = React.createRef<(value: any) => void>();
266
268
  const openViewParams = getOpenViewStepParams(viewItem.model);
269
+ const associationName =
270
+ openViewParams?.associationName && !hasUsableSourceId(viewItem.params.sourceId)
271
+ ? null
272
+ : openViewParams?.associationName;
267
273
  const openerUids = viewList.slice(0, viewItem.index).map((item) => item.params.viewUid);
268
274
  const navigation = new ViewNavigation(
269
275
  this.flowEngine.context,
@@ -273,7 +279,7 @@ export class AdminLayoutRouteCoordinator {
273
279
  viewItem.model.dispatchEvent('click', {
274
280
  target: runtime.meta.layoutContentElement || this.layoutContentElement,
275
281
  collectionName: openViewParams?.collectionName,
276
- associationName: openViewParams?.associationName,
282
+ associationName,
277
283
  dataSourceKey: openViewParams?.dataSourceKey,
278
284
  destroyRef,
279
285
  updateRef,
@@ -0,0 +1,117 @@
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, beforeEach } from 'vitest';
12
+ import { getViewDiffAndUpdateHidden } from '../../getViewDiffAndUpdateHidden';
13
+ import { getOpenViewStepParams } from '../../flows/openViewFlow';
14
+ import { resolveViewParamsToViewList } from '../../resolveViewParamsToViewList';
15
+ import { AdminLayoutRouteCoordinator } from '../AdminLayoutRouteCoordinator';
16
+ import { RouteModel } from '../../models/base/RouteModel';
17
+
18
+ vi.mock('../../resolveViewParamsToViewList', () => ({
19
+ resolveViewParamsToViewList: vi.fn(),
20
+ updateViewListHidden: vi.fn(),
21
+ }));
22
+
23
+ vi.mock('../../getViewDiffAndUpdateHidden', () => ({
24
+ getViewDiffAndUpdateHidden: vi.fn(),
25
+ getKey: vi.fn((viewItem) => viewItem.params.viewUid),
26
+ }));
27
+
28
+ vi.mock('../../flows/openViewFlow', async (importOriginal) => {
29
+ const actual = await importOriginal();
30
+ return {
31
+ ...(actual as any),
32
+ getOpenViewStepParams: vi.fn(),
33
+ };
34
+ });
35
+
36
+ const mockResolveViewParamsToViewList = vi.mocked(resolveViewParamsToViewList);
37
+ const mockGetViewDiffAndUpdateHidden = vi.mocked(getViewDiffAndUpdateHidden);
38
+ const mockGetOpenViewStepParams = vi.mocked(getOpenViewStepParams);
39
+
40
+ function setupRouteReplay(viewParams: Record<string, any>) {
41
+ const engine = new FlowEngine();
42
+ engine.registerModels({ RouteModel });
43
+ engine.context.defineProperty('route', {
44
+ value: {
45
+ params: { name: 'test-route' },
46
+ pathname: '/admin/popup/filterbytk/member',
47
+ },
48
+ });
49
+ engine.context.defineProperty('routeRepository', {
50
+ value: {
51
+ getRouteBySchemaUid: vi.fn(() => ({})),
52
+ },
53
+ });
54
+
55
+ const dispatchEvent = vi.fn(() => Promise.resolve());
56
+ const viewItem = {
57
+ params: {
58
+ viewUid: 'popup',
59
+ filterByTk: 'member',
60
+ ...viewParams,
61
+ },
62
+ modelUid: 'popup',
63
+ model: { uid: 'popup', dispatchEvent } as any,
64
+ hidden: { value: false },
65
+ index: 0,
66
+ };
67
+
68
+ mockResolveViewParamsToViewList.mockReturnValue([viewItem]);
69
+ mockGetViewDiffAndUpdateHidden.mockReturnValue({
70
+ viewsToClose: [],
71
+ viewsToOpen: [viewItem],
72
+ });
73
+ mockGetOpenViewStepParams.mockReturnValue({
74
+ collectionName: 'roles',
75
+ associationName: 'users.roles',
76
+ dataSourceKey: 'main',
77
+ } as any);
78
+
79
+ const coordinator = new AdminLayoutRouteCoordinator(engine);
80
+ coordinator.registerPage('test-route', {
81
+ active: true,
82
+ layoutContentElement: document.createElement('div'),
83
+ });
84
+
85
+ return { dispatchEvent };
86
+ }
87
+
88
+ describe('AdminLayoutRouteCoordinator', () => {
89
+ beforeEach(() => {
90
+ vi.clearAllMocks();
91
+ });
92
+
93
+ it('drops configured associationName during route replay when sourceId is absent', () => {
94
+ const { dispatchEvent } = setupRouteReplay({});
95
+
96
+ expect(dispatchEvent.mock.calls[0][1]).toMatchObject({
97
+ collectionName: 'roles',
98
+ associationName: null,
99
+ dataSourceKey: 'main',
100
+ filterByTk: 'member',
101
+ triggerByRouter: true,
102
+ });
103
+ });
104
+
105
+ it('keeps configured associationName during route replay when sourceId is present', () => {
106
+ const { dispatchEvent } = setupRouteReplay({ sourceId: '1' });
107
+
108
+ expect(dispatchEvent.mock.calls[0][1]).toMatchObject({
109
+ collectionName: 'roles',
110
+ associationName: 'users.roles',
111
+ dataSourceKey: 'main',
112
+ filterByTk: 'member',
113
+ sourceId: '1',
114
+ triggerByRouter: true,
115
+ });
116
+ });
117
+ });
@@ -485,6 +485,7 @@ type AssignValueFieldSource = {
485
485
  const ASSIGN_VALUE_IGNORED_PROP_KEYS = new Set([
486
486
  'value',
487
487
  'defaultValue',
488
+ 'hidden',
488
489
  'onChange',
489
490
  'onClick',
490
491
  'open',
@@ -39,6 +39,24 @@ describe('code-editor linter', () => {
39
39
  );
40
40
  });
41
41
 
42
+ it('does not warn for callback parameters used inside JSX', () => {
43
+ const code = `
44
+ const columns = [
45
+ {
46
+ render: (roles, record) => (
47
+ <div>
48
+ {roles.map((role) => <Tag key={role.name}>{record.nickname || role.title}</Tag>)}
49
+ </div>
50
+ ),
51
+ },
52
+ ];
53
+ `;
54
+ const diags = computeDiagnosticsFromText(code);
55
+ expect(diags.some((d) => d.message.includes('Possible undefined variable: roles'))).toBe(false);
56
+ expect(diags.some((d) => d.message.includes('Possible undefined variable: record'))).toBe(false);
57
+ expect(diags.some((d) => d.message.includes('Possible undefined variable: role'))).toBe(false);
58
+ });
59
+
42
60
  it('reports non-callable expression warning', () => {
43
61
  const code = `(1+2)()`;
44
62
  const diags = computeDiagnosticsFromText(code);
@@ -38,6 +38,29 @@ describe('runjsDiagnostics', () => {
38
38
  expect(res.issues.some((i) => i.type === 'lint' && i.ruleId === 'no-noncallable-call')).toBe(true);
39
39
  });
40
40
 
41
+ it('does not report JSX callback parameters as undefined variables', async () => {
42
+ const ctx = createTestCtx();
43
+ const code = `
44
+ const { Tag } = ctx.libs.antd;
45
+ const columns = [
46
+ {
47
+ render: (roles, record) => (
48
+ <div>
49
+ {roles.map((role) => <Tag key={role.name}>{record.nickname || role.title}</Tag>)}
50
+ </div>
51
+ ),
52
+ },
53
+ ];
54
+ ctx.render(<div />);
55
+ `;
56
+ const res = await diagnoseRunJS(code, ctx);
57
+ expect(
58
+ res.issues.some(
59
+ (i) => i.type === 'lint' && i.ruleId === 'possible-undefined-variable' && /roles|record|role/.test(i.message),
60
+ ),
61
+ ).toBe(false);
62
+ });
63
+
41
64
  it('reports suspicious short ctx member call as a lint issue', async () => {
42
65
  const ctx = createTestCtx();
43
66
  const res = await diagnoseRunJS('ctx.fw();', ctx);
@@ -133,6 +133,19 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
133
133
  return createRunJSCompletionSource({ hostCtx, staticOptions: finalExtra });
134
134
  }, [hostCtx, finalExtra]);
135
135
 
136
+ const runCurrentCode = useCallback(async () => {
137
+ const code = viewRef.current?.state.doc.toString() || '';
138
+ clearDiagnostics(viewRef.current);
139
+ const res = await run(code);
140
+ if (!res?.success) {
141
+ const rawErr = res?.error;
142
+ const errText = res?.timeout ? tr('Execution timed out') : String(rawErr || tr('Unknown error'));
143
+ const pos = parseErrorLineColumn(rawErr);
144
+ if (pos && viewRef.current) markErrorAt(viewRef.current, pos.line, pos.column, errText);
145
+ }
146
+ return res;
147
+ }, [run, tr]);
148
+
136
149
  // JSX 转换支持暂时移除:直接按原样运行代码
137
150
 
138
151
  // 错误标注相关工具已提取至 errorHelpers.ts
@@ -153,6 +166,9 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
153
166
  const v = viewRef.current;
154
167
  return v ? v.state.doc.toString() : '';
155
168
  },
169
+ run() {
170
+ return Promise.resolve(undefined);
171
+ },
156
172
 
157
173
  buttonGroupHeight: 0,
158
174
  snippetEntries: [],
@@ -160,6 +176,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
160
176
  });
161
177
  extraEditorRef.current.snippetEntries = snippetEntries;
162
178
  extraEditorRef.current.logs = logs;
179
+ extraEditorRef.current.run = runCurrentCode;
163
180
 
164
181
  // snippet group display handled in SnippetsDrawer
165
182
 
@@ -210,23 +227,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
210
227
  {tr('Snippets')}
211
228
  </Button>
212
229
  <>
213
- <Button
214
- size="small"
215
- loading={running}
216
- onClick={async () => {
217
- const code = viewRef.current?.state.doc.toString() || '';
218
- clearDiagnostics(viewRef.current);
219
- const res = await run(code);
220
- if (!res?.success) {
221
- const rawErr = res?.error;
222
- const errText = res?.timeout
223
- ? tr('Execution timed out')
224
- : String(rawErr || tr('Unknown error'));
225
- const pos = parseErrorLineColumn(rawErr);
226
- if (pos && viewRef.current) markErrorAt(viewRef.current, pos.line, pos.column, errText);
227
- }
228
- }}
229
- >
230
+ <Button size="small" loading={running} onClick={runCurrentCode}>
230
231
  {tr('Run')}
231
232
  </Button>
232
233
  </>