@nocobase/client-v2 2.2.0-beta.15 → 2.2.0-beta.17

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 (93) hide show
  1. package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -2
  2. package/es/flow/actions/linkageRules.d.ts +25 -0
  3. package/es/flow/components/FieldAssignValueInput.d.ts +4 -30
  4. package/es/flow/components/FlowRoute.d.ts +2 -2
  5. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  6. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  7. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  8. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  9. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  10. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  11. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  12. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  13. package/es/flow/models/blocks/form/FormBlockModel.d.ts +6 -0
  14. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  15. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  16. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  17. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  18. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  19. package/es/index.d.ts +1 -0
  20. package/es/index.mjs +105 -111
  21. package/es/ui-operation/index.d.ts +15 -0
  22. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  23. package/lib/index.js +108 -114
  24. package/lib/locale/languageCodes.js +1 -0
  25. package/package.json +8 -7
  26. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
  27. package/src/components/form/ScanInput/CodeScanner.tsx +61 -34
  28. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  29. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +114 -18
  30. package/src/components/form/ScanInput/useCodeScanner.ts +166 -23
  31. package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
  32. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  33. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  34. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  35. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  36. package/src/flow/actions/linkageRules.tsx +51 -13
  37. package/src/flow/common/Markdown/style.ts +4 -0
  38. package/src/flow/components/BlockItemCard.tsx +2 -2
  39. package/src/flow/components/FieldAssignValueInput.tsx +38 -621
  40. package/src/flow/components/FlowRoute.tsx +71 -29
  41. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  42. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  43. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  44. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  45. package/src/flow/components/field-value-variable/index.ts +12 -0
  46. package/src/flow/index.ts +2 -0
  47. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  48. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  49. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  50. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  51. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  52. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  53. package/src/flow/models/base/CollectionBlockModel.tsx +15 -2
  54. package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
  55. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
  56. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  57. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  58. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  59. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +60 -0
  60. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  61. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
  62. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
  63. package/src/flow/models/blocks/form/FormBlockModel.tsx +4 -0
  64. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  65. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
  66. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  67. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  68. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  69. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  70. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  71. package/src/flow/models/blocks/table/TableBlockModel.tsx +3 -1
  72. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  73. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  74. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  75. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  76. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
  77. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  78. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  79. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +46 -0
  80. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  81. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  82. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  83. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  84. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +1 -0
  85. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +20 -2
  86. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  87. package/src/index.ts +1 -0
  88. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  89. package/src/layout-manager/LayoutRoute.tsx +2 -1
  90. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  91. package/src/locale/languageCodes.ts +1 -0
  92. package/src/ui-operation/index.ts +33 -0
  93. package/src/ui-operation/ui-operation-codec.ts +54 -0
@@ -22,6 +22,11 @@ import { applyUpdateRecordAction } from './UpdateRecordActionUtils';
22
22
  // import { RemoteFlowModelRenderer } from '../../FlowPage';
23
23
 
24
24
  const SETTINGS_FLOW_KEY = 'assignSettings';
25
+ const AFTER_SUCCESS_DEFAULT_PARAMS = {
26
+ successMessage: tExpr('Saved successfully'),
27
+ manualClose: false,
28
+ actionAfterSuccess: 'stay',
29
+ };
25
30
 
26
31
  function Info() {
27
32
  const ctx = useFlowSettingsContext();
@@ -101,6 +106,10 @@ UpdateRecordActionModel.registerFlow({
101
106
  tipComponent: Info,
102
107
  validateBeforeSave: true,
103
108
  }),
109
+ afterSuccess: {
110
+ use: 'afterSuccess',
111
+ defaultParams: AFTER_SUCCESS_DEFAULT_PARAMS,
112
+ },
104
113
  },
105
114
  });
106
115
 
@@ -113,7 +122,15 @@ UpdateRecordActionModel.registerFlow({
113
122
  return getAssignFieldValuesDefaultParams(ctx, SETTINGS_FLOW_KEY);
114
123
  },
115
124
  async handler(ctx, params) {
116
- await applyUpdateRecordAction(ctx, params, { settingsFlowKey: SETTINGS_FLOW_KEY });
125
+ const updated = await applyUpdateRecordAction(ctx, params, { settingsFlowKey: SETTINGS_FLOW_KEY });
126
+ if (!updated) {
127
+ return;
128
+ }
129
+ const savedAfterSuccess = ctx.model.getStepParams(SETTINGS_FLOW_KEY, 'afterSuccess') || {};
130
+ await ctx.runAction('afterSuccess', {
131
+ ...AFTER_SUCCESS_DEFAULT_PARAMS,
132
+ ...savedAfterSuccess,
133
+ });
117
134
  },
118
135
  },
119
136
  },
@@ -8,9 +8,15 @@
8
8
  */
9
9
 
10
10
  import { MultiRecordResource, SingleRecordResource } from '@nocobase/flow-engine';
11
+ import type { AxiosRequestConfig } from 'axios';
11
12
  import { dispatchEventDeep } from '../../utils';
12
13
  import { resolveAssignFieldValues } from '../blocks/assign-form/assignFieldValuesFlow';
13
14
 
15
+ type UpdateRecordActionParams = {
16
+ assignedValues?: unknown;
17
+ requestConfig?: AxiosRequestConfig;
18
+ };
19
+
14
20
  export async function refreshLinkageRulesAfterUpdate(ctx: any) {
15
21
  const blockModel = ctx?.blockModel || ctx?.model?.context?.blockModel || ctx?.model;
16
22
  const actionModel = ctx?.model;
@@ -48,11 +54,11 @@ export async function refreshLinkageRulesAfterUpdate(ctx: any) {
48
54
 
49
55
  export async function applyUpdateRecordAction(
50
56
  ctx: any,
51
- params: any,
57
+ params: UpdateRecordActionParams,
52
58
  options?: {
53
59
  settingsFlowKey?: string;
54
60
  },
55
- ) {
61
+ ): Promise<boolean> {
56
62
  const settingsFlowKey = options?.settingsFlowKey || 'assignSettings';
57
63
 
58
64
  // 统一接入二次确认:如果启用则弹窗;未配置时默认不启用
@@ -62,25 +68,31 @@ export async function applyUpdateRecordAction(
62
68
 
63
69
  const assignedValues = await resolveAssignFieldValues(ctx, params?.assignedValues, 'UpdateRecordAction');
64
70
  if (!assignedValues) {
65
- return;
71
+ return false;
66
72
  }
67
73
 
68
74
  if (!assignedValues || typeof assignedValues !== 'object' || !Object.keys(assignedValues).length) {
69
75
  ctx.message.warning(ctx.t('No assigned fields configured'));
70
- return;
76
+ return false;
71
77
  }
72
78
  const collection = ctx.collection?.name;
73
79
  const filterByTk = ctx.collection?.getFilterByTK?.(ctx.record);
74
80
  if (!collection || typeof filterByTk === 'undefined' || filterByTk === null) {
75
81
  ctx.message.error(ctx.t('Record is required to perform this action'));
76
- return;
82
+ return false;
77
83
  }
84
+ let updated = false;
78
85
  if (ctx.resource instanceof SingleRecordResource) {
79
86
  await ctx.resource.save(assignedValues, params.requestConfig);
87
+ updated = true;
80
88
  } else if (ctx.resource instanceof MultiRecordResource) {
81
89
  await ctx.resource.update(filterByTk, assignedValues, params.requestConfig);
90
+ updated = true;
91
+ }
92
+ if (!updated) {
93
+ return false;
82
94
  }
83
95
 
84
96
  await refreshLinkageRulesAfterUpdate(ctx);
85
- ctx.message.success(ctx.t('Saved successfully'));
97
+ return true;
86
98
  }
@@ -8,8 +8,9 @@
8
8
  */
9
9
 
10
10
  import { beforeEach, describe, expect, it, vi } from 'vitest';
11
- import { MultiRecordResource } from '@nocobase/flow-engine';
11
+ import { FlowEngine, MultiRecordResource } from '@nocobase/flow-engine';
12
12
  import { applyUpdateRecordAction } from '../UpdateRecordActionUtils';
13
+ import { UpdateRecordActionModel } from '../UpdateRecordActionModel';
13
14
  import { dispatchEventDeep } from '../../../utils';
14
15
 
15
16
  vi.mock('../../../utils', () => ({
@@ -21,7 +22,7 @@ describe('UpdateRecordActionModel apply action', () => {
21
22
  vi.clearAllMocks();
22
23
  });
23
24
 
24
- it('dispatches paginationChange for action and block after successful update', async () => {
25
+ it('dispatches paginationChange and returns success after updating the record', async () => {
25
26
  const resource: any = Object.create(MultiRecordResource.prototype);
26
27
  resource.update = vi.fn(async () => ({}));
27
28
  resource.refresh = vi.fn(async () => {});
@@ -52,12 +53,13 @@ describe('UpdateRecordActionModel apply action', () => {
52
53
  t: (value: string) => value,
53
54
  };
54
55
 
55
- await applyUpdateRecordAction(ctx, {
56
+ const updated = await applyUpdateRecordAction(ctx, {
56
57
  assignedValues: {
57
58
  marital_status: '已婚',
58
59
  },
59
60
  });
60
61
 
62
+ expect(updated).toBe(true);
61
63
  expect(resource.update).toHaveBeenCalledWith(1, { marital_status: '已婚' }, undefined);
62
64
  expect(resource.refresh).not.toHaveBeenCalled();
63
65
 
@@ -67,6 +69,76 @@ describe('UpdateRecordActionModel apply action', () => {
67
69
  expect(paginationCalls.length).toBeGreaterThan(0);
68
70
  expect(paginationCalls.some(([model]: [any]) => model === ctx.model)).toBe(true);
69
71
  expect(paginationCalls.some(([model]: [any]) => model === blockModel)).toBe(true);
70
- expect(ctx.message.success).toHaveBeenCalledWith('Saved successfully');
72
+ expect(ctx.message.success).not.toHaveBeenCalled();
73
+ });
74
+
75
+ it('runs the configured after-success action only after a successful update', async () => {
76
+ const engine = new FlowEngine();
77
+ const action = new UpdateRecordActionModel({ uid: 'update-record-action', flowEngine: engine } as any);
78
+ action.setStepParams('assignSettings', 'confirm', { enable: false });
79
+ action.setStepParams('assignSettings', 'afterSuccess', {
80
+ successMessage: 'Record updated',
81
+ });
82
+
83
+ const resource: any = Object.create(MultiRecordResource.prototype);
84
+ resource.update = vi.fn(async () => ({}));
85
+ const blockModel: any = { uid: 'details-block' };
86
+ const runAction = vi.fn(async () => {});
87
+ const ctx: any = {
88
+ model: action,
89
+ blockModel,
90
+ runAction,
91
+ collection: {
92
+ name: 'users',
93
+ getFilterByTK: vi.fn(() => 1),
94
+ },
95
+ record: { id: 1 },
96
+ resource,
97
+ message: {
98
+ success: vi.fn(),
99
+ warning: vi.fn(),
100
+ error: vi.fn(),
101
+ },
102
+ t: (value: string) => value,
103
+ };
104
+ const handler = action.getFlow('apply')?.getStep('apply')?.serialize().handler;
105
+
106
+ await handler(ctx, { assignedValues: { status: 'active' } });
107
+
108
+ expect(runAction).toHaveBeenNthCalledWith(1, 'confirm', { enable: false });
109
+ expect(runAction).toHaveBeenNthCalledWith(2, 'afterSuccess', {
110
+ successMessage: 'Record updated',
111
+ manualClose: false,
112
+ actionAfterSuccess: 'stay',
113
+ });
114
+ });
115
+
116
+ it('does not run the after-success action when no fields are assigned', async () => {
117
+ const engine = new FlowEngine();
118
+ const action = new UpdateRecordActionModel({ uid: 'update-record-action-empty', flowEngine: engine } as any);
119
+ const runAction = vi.fn(async () => {});
120
+ const ctx: any = {
121
+ model: action,
122
+ runAction,
123
+ collection: {
124
+ name: 'users',
125
+ getFilterByTK: vi.fn(() => 1),
126
+ },
127
+ record: { id: 1 },
128
+ resource: Object.create(MultiRecordResource.prototype),
129
+ message: {
130
+ success: vi.fn(),
131
+ warning: vi.fn(),
132
+ error: vi.fn(),
133
+ },
134
+ t: (value: string) => value,
135
+ };
136
+ const handler = action.getFlow('apply')?.getStep('apply')?.serialize().handler;
137
+
138
+ await handler(ctx, { assignedValues: {} });
139
+
140
+ expect(runAction).toHaveBeenCalledTimes(1);
141
+ expect(runAction).toHaveBeenCalledWith('confirm', { enable: false });
142
+ expect(ctx.message.warning).toHaveBeenCalledWith('No assigned fields configured');
71
143
  });
72
144
  });
@@ -7,7 +7,8 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { DefaultStructure, FlowModel, tExpr, useFlowModel } from '@nocobase/flow-engine';
10
+ import { FlowModel, tExpr, useFlowModel } from '@nocobase/flow-engine';
11
+ import type { DefaultStructure, FlowModelOptions } from '@nocobase/flow-engine';
11
12
  import { Icon } from '../../../components';
12
13
  import { Button, Tooltip } from 'antd';
13
14
  import type { ButtonProps } from 'antd/es/button';
@@ -87,8 +88,10 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
87
88
  return null;
88
89
  }
89
90
 
90
- onInit(options: any): void {
91
+ onInit(options: FlowModelOptions<T>): void {
91
92
  super.onInit(options);
93
+ // Action disabled state is computed at runtime and must never be restored from persisted props.
94
+ delete this.props.disabled;
92
95
  this.context.defineProperty('actionName', {
93
96
  get: () => {
94
97
  return this.getAclActionName();
@@ -97,6 +100,12 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
97
100
  });
98
101
  }
99
102
 
103
+ serialize() {
104
+ const data = super.serialize();
105
+ delete data.props.disabled;
106
+ return data;
107
+ }
108
+
100
109
  getInputArgs() {
101
110
  const inputArgs: Record<string, any> = {};
102
111
  const defaultKeys: string[] = [];
@@ -14,8 +14,10 @@ import {
14
14
  FlowSettingsButton,
15
15
  buildSubModelGroups,
16
16
  buildSubModelItems,
17
+ type FlowModel,
17
18
  type SubModelItem,
18
19
  type SubModelItemsType,
20
+ VIEW_ACTIVATED_EVENT,
19
21
  } from '@nocobase/flow-engine';
20
22
  import React from 'react';
21
23
  import { FilterManager } from '../blocks/filter-manager/FilterManager';
@@ -24,6 +26,8 @@ import { GridModel } from './GridModel';
24
26
  const SELECT_SCENE_ALLOWED_OTHER_BLOCK_MODELS = ['JSBlockModel', 'IframeBlockModel', 'MarkdownBlockModel'] as const;
25
27
 
26
28
  export class BlockGridModel extends GridModel {
29
+ private viewActivatedListener?: () => void;
30
+
27
31
  dragOverlayConfig: DragOverlayConfig = {
28
32
  // 列内插入
29
33
  columnInsert: {
@@ -50,6 +54,28 @@ export class BlockGridModel extends GridModel {
50
54
  });
51
55
  }
52
56
 
57
+ onMount() {
58
+ super.onMount();
59
+ if (this.context.view?.inputArgs?.scene !== 'select' || this.viewActivatedListener) {
60
+ return;
61
+ }
62
+
63
+ this.viewActivatedListener = () => {
64
+ this.mapSubModels('items', (item) => {
65
+ (item as FlowModel & { onActive?: () => void }).onActive?.();
66
+ });
67
+ };
68
+ this.flowEngine.emitter.on(VIEW_ACTIVATED_EVENT, this.viewActivatedListener);
69
+ }
70
+
71
+ protected onUnmount() {
72
+ if (this.viewActivatedListener) {
73
+ this.flowEngine.emitter.off(VIEW_ACTIVATED_EVENT, this.viewActivatedListener);
74
+ this.viewActivatedListener = undefined;
75
+ }
76
+ super.onUnmount();
77
+ }
78
+
53
79
  get subModelBaseClasses() {
54
80
  const inputArgs = this.context.view?.inputArgs ?? {};
55
81
  if (inputArgs.collectionName && !inputArgs.filterByTk) {
@@ -82,6 +82,15 @@ export class CollectionBlockModel<T = DefaultStructure> extends DataBlockModel<T
82
82
  const engine = this.context.engine as FlowEngine;
83
83
  const currentVersion = this.getDirtyTrackingVersion(engine, dataSourceKey, resource, params);
84
84
 
85
+ if (resource instanceof MultiRecordResource && this.getDataLoadingMode() === 'manual' && !this.hasActiveFilters()) {
86
+ resource.setData([]);
87
+ resource.setMeta({ count: 0, hasNext: false });
88
+ resource.setPage(1);
89
+ resource.loading = false;
90
+ this.lastSeenDirtyVersion = currentVersion;
91
+ return;
92
+ }
93
+
85
94
  if (forceRefresh) {
86
95
  if (this.dirtyRefreshing) return;
87
96
  this.dirtyRefreshing = true;
@@ -569,9 +578,13 @@ export class CollectionBlockModel<T = DefaultStructure> extends DataBlockModel<T
569
578
  }
570
579
  if (fieldPath.includes('.')) {
571
580
  // 关系数据
581
+ const collection = this.collection;
582
+ if (!collection) {
583
+ return;
584
+ }
572
585
  const [field1, field2] = fieldPath.split('.');
573
586
  const associationField = this.context.dataSourceManager.getCollectionField(
574
- `${this.collection.dataSourceKey}.${this.collection.name}.${field1}`,
587
+ `${collection.dataSourceKey}.${collection.name}.${field1}`,
575
588
  ) as CollectionField;
576
589
  if (!associationField) {
577
590
  return;
@@ -583,7 +596,7 @@ export class CollectionBlockModel<T = DefaultStructure> extends DataBlockModel<T
583
596
  }
584
597
 
585
598
  const collectionField = this.context.dataSourceManager.getCollectionField(
586
- `${this.collection.dataSourceKey}.${targetCollectionName}.${field2}`,
599
+ `${collection.dataSourceKey}.${targetCollectionName}.${field2}`,
587
600
  ) as CollectionField;
588
601
 
589
602
  if (collectionField && collectionField.isAssociationField()) {
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { FlowModel, FlowModelRenderer, observable, type ParamObject, tExpr } from '@nocobase/flow-engine';
11
11
  import { Icon, type NocoBaseDesktopRoute } from '../../../../flow-compat';
12
+ import type { RouteRepository } from '../../../../RouteRepository';
12
13
  import { useRequest } from 'ahooks';
13
14
  import _ from 'lodash';
14
15
  import React from 'react';
@@ -71,6 +72,25 @@ function isLinkageRulesStepParams(value: unknown): value is LinkageRulesStepPara
71
72
  return isRecord(value);
72
73
  }
73
74
 
75
+ type RefreshableRouteRepository = Pick<RouteRepository, 'refreshAccessible'>;
76
+
77
+ const routeRefreshQueues = new WeakMap<RefreshableRouteRepository, Promise<unknown>>();
78
+
79
+ async function refreshRouteRepository(repository: RefreshableRouteRepository) {
80
+ const previous = routeRefreshQueues.get(repository) || Promise.resolve();
81
+ const current = previous.catch(() => undefined).then(() => repository.refreshAccessible());
82
+
83
+ routeRefreshQueues.set(repository, current);
84
+
85
+ try {
86
+ await current;
87
+ } finally {
88
+ if (routeRefreshQueues.get(repository) === current) {
89
+ routeRefreshQueues.delete(repository);
90
+ }
91
+ }
92
+ }
93
+
74
94
  export class BasePageTabModel extends FlowModel<{
75
95
  subModels: {
76
96
  grid: BlockGridModel;
@@ -282,23 +302,36 @@ export class RootPageTabModel extends BasePageTabModel {
282
302
  async save() {
283
303
  const json = this.serialize();
284
304
  const documentTitle = this.stepParams?.pageTabSettings?.tab?.documentTitle;
285
- const response = await this.context.api.request({
286
- method: 'post',
287
- url: 'desktopRoutes:updateOrCreate',
288
- params: {
289
- filterKeys: ['schemaUid'],
305
+ const route = this.props.route || {};
306
+ const persisted = route.id != null;
307
+ const currentRoute =
308
+ persisted && route.schemaUid ? this.context.routeRepository?.getRouteBySchemaUid?.(route.schemaUid) : undefined;
309
+ const data = {
310
+ ...(persisted ? { schemaUid: route.schemaUid } : route),
311
+ title: this.getTabTitle(''),
312
+ icon: this.getTabIcon(),
313
+ options: {
314
+ ...(currentRoute ? currentRoute.options : route.options),
315
+ flowRegistry: json.flowRegistry,
316
+ documentTitle,
290
317
  },
291
- data: {
292
- ...this.props.route,
293
- title: this.getTabTitle(''),
294
- icon: this.getTabIcon(),
295
- options: {
296
- ...(this.props.route?.options || {}),
297
- flowRegistry: json.flowRegistry,
298
- documentTitle,
299
- },
300
- },
301
- });
318
+ };
319
+ const response = await this.context.api.request(
320
+ persisted
321
+ ? {
322
+ method: 'post',
323
+ url: `desktopRoutes:update?filter[id]=${route.id}`,
324
+ data,
325
+ }
326
+ : {
327
+ method: 'post',
328
+ url: 'desktopRoutes:updateOrCreate',
329
+ params: {
330
+ filterKeys: ['schemaUid'],
331
+ },
332
+ data,
333
+ },
334
+ );
302
335
  const persistedRoute = normalizePersistedRoute(response?.data?.data);
303
336
 
304
337
  // 新建 tab 首次保存后需要立即拿到持久化 route id,拖拽排序会直接依赖它。
@@ -312,6 +345,20 @@ export class RootPageTabModel extends BasePageTabModel {
312
345
  },
313
346
  });
314
347
  }
348
+
349
+ // 后续保存会从 RouteRepository 读取扩展 options,当前写入完成后必须先同步最新路由快照。
350
+ try {
351
+ if (this.context.routeRepository?.refreshAccessible) {
352
+ await refreshRouteRepository(this.context.routeRepository);
353
+ } else {
354
+ await this.context.refreshDesktopRoutes?.();
355
+ }
356
+ } catch (error) {
357
+ this.context.logger?.warn?.(
358
+ { err: error },
359
+ '[client-v2] Failed to refresh desktop routes after saving a page tab',
360
+ );
361
+ }
315
362
  }
316
363
 
317
364
  private buildAnchorPayload(latestAnchor: PersistedFlowModelAnchor | undefined, linkageRules: LinkageRulesStepParams) {
@@ -359,8 +406,11 @@ export class RootPageTabModel extends BasePageTabModel {
359
406
  throw new Error('Route repository is unavailable while persisting the page tab FlowModel marker.');
360
407
  }
361
408
 
409
+ const currentRoute = route.schemaUid
410
+ ? this.context.routeRepository.getRouteBySchemaUid?.(route.schemaUid)
411
+ : undefined;
362
412
  const nextOptions = {
363
- ...(route.options || {}),
413
+ ...(currentRoute?.options || route.options || {}),
364
414
  };
365
415
  if (hasRules) {
366
416
  nextOptions.hasPersistedPageTabFlowModel = true;