@nocobase/flow-engine 2.2.0-alpha.6 → 2.2.0-alpha.8

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.
@@ -31,8 +31,10 @@ __export(dirtyAwareApiClient_exports, {
31
31
  getDirtyAwareApiClient: () => getDirtyAwareApiClient
32
32
  });
33
33
  module.exports = __toCommonJS(dirtyAwareApiClient_exports);
34
+ var import_sdk = require("@nocobase/sdk");
34
35
  var import_dataSourceDirty = require("./dataSourceDirty");
35
36
  const SKIP_DATA_SOURCE_DIRTY = "__nocobaseSkipDataSourceDirty";
37
+ const DIRTY_DISPATCH_TOKEN = Symbol("nocobaseDirtyDispatchToken");
36
38
  const dirtyAwareApiClientCache = /* @__PURE__ */ new WeakMap();
37
39
  const dirtyAwareApiClientProxies = /* @__PURE__ */ new WeakSet();
38
40
  const MUTATING_RESOURCE_ACTIONS = [
@@ -288,6 +290,43 @@ function resolveDirtyResourceAction(options, context) {
288
290
  return parseDirtyResourceActionFromUrl(options == null ? void 0 : options.url, context);
289
291
  }
290
292
  __name(resolveDirtyResourceAction, "resolveDirtyResourceAction");
293
+ function getDirtyResourceActionDispatchKey(dirtyResourceAction, headers) {
294
+ if (!dirtyResourceAction) {
295
+ return void 0;
296
+ }
297
+ return JSON.stringify([
298
+ dirtyResourceAction.dataSourceKey || (0, import_dataSourceDirty.getDataSourceKeyFromHeaders)(headers),
299
+ dirtyResourceAction.resourceName,
300
+ dirtyResourceAction.actionName
301
+ ]);
302
+ }
303
+ __name(getDirtyResourceActionDispatchKey, "getDirtyResourceActionDispatchKey");
304
+ function getRequestDispatchKey(options, context) {
305
+ return getDirtyResourceActionDispatchKey(resolveDirtyResourceAction(options, context), options.headers);
306
+ }
307
+ __name(getRequestDispatchKey, "getRequestDispatchKey");
308
+ function getResourceDispatchKey(name, of, headers) {
309
+ const resourceName = String(name ?? "").trim();
310
+ if (!resourceName) {
311
+ return void 0;
312
+ }
313
+ return JSON.stringify([resourceName, String(of ?? ""), (0, import_dataSourceDirty.getDataSourceKeyFromHeaders)(headers)]);
314
+ }
315
+ __name(getResourceDispatchKey, "getResourceDispatchKey");
316
+ function getMatchingRequestToken(token, requestKey) {
317
+ if (!token || !requestKey || token.requestKey && token.requestKey !== requestKey) {
318
+ return void 0;
319
+ }
320
+ return token;
321
+ }
322
+ __name(getMatchingRequestToken, "getMatchingRequestToken");
323
+ function getMatchingResourceToken(token, resourceKey) {
324
+ if (!token || !resourceKey || token.resourceKey && token.resourceKey !== resourceKey) {
325
+ return void 0;
326
+ }
327
+ return token;
328
+ }
329
+ __name(getMatchingResourceToken, "getMatchingResourceToken");
291
330
  function markResourceActionDataSourceDirty(context, dirtyResourceAction, headers) {
292
331
  (0, import_dataSourceDirty.markDataSourceDirty)({
293
332
  engine: context.engine,
@@ -297,7 +336,19 @@ function markResourceActionDataSourceDirty(context, dirtyResourceAction, headers
297
336
  });
298
337
  }
299
338
  __name(markResourceActionDataSourceDirty, "markResourceActionDataSourceDirty");
300
- function createDirtyAwareResource(context, resource, resourceName, resourceOf, headers) {
339
+ function markResourceActionDataSourceDirtyOnce(token, context, dirtyResourceAction, headers) {
340
+ if (token.skip || token.marked || !dirtyResourceAction || !isMutatingResourceAction(dirtyResourceAction.actionName)) {
341
+ return;
342
+ }
343
+ token.marked = true;
344
+ markResourceActionDataSourceDirty(context, dirtyResourceAction, headers);
345
+ }
346
+ __name(markResourceActionDataSourceDirtyOnce, "markResourceActionDataSourceDirtyOnce");
347
+ function isObjectRecord(value) {
348
+ return !!value && typeof value === "object";
349
+ }
350
+ __name(isObjectRecord, "isObjectRecord");
351
+ function createDirtyAwareResource(context, resource, resourceName, resourceOf, headers, requestTokenStack, parentToken) {
301
352
  return new Proxy(resource, {
302
353
  get(target, prop, receiver) {
303
354
  const original = Reflect.get(target, prop, receiver);
@@ -306,10 +357,38 @@ function createDirtyAwareResource(context, resource, resourceName, resourceOf, h
306
357
  }
307
358
  const action = original;
308
359
  return async (...args) => {
309
- const result = await action(...args);
360
+ const actionOptions = isObjectRecord(args[1]) ? args[1] : void 0;
310
361
  const dirtyResourceAction = resolveDirtyResourceActionFromResource(resourceName, resourceOf, prop, context);
311
- if (dirtyResourceAction) {
312
- markResourceActionDataSourceDirty(context, dirtyResourceAction, headers);
362
+ const requestKey = getDirtyResourceActionDispatchKey(dirtyResourceAction, headers);
363
+ const resourceKey = getResourceDispatchKey(resourceName, resourceOf, headers);
364
+ const inheritedToken = getMatchingRequestToken(
365
+ actionOptions == null ? void 0 : actionOptions[DIRTY_DISPATCH_TOKEN],
366
+ requestKey
367
+ ) || getMatchingRequestToken(parentToken, requestKey);
368
+ const token = inheritedToken || { marked: false, skip: false };
369
+ const ownsToken = !inheritedToken;
370
+ token.requestKey ||= requestKey;
371
+ token.resourceKey ||= resourceKey;
372
+ if (actionOptions == null ? void 0 : actionOptions[SKIP_DATA_SOURCE_DIRTY]) {
373
+ token.skip = true;
374
+ }
375
+ const forwardedArgs = actionOptions || args[1] == null ? [
376
+ args[0],
377
+ {
378
+ ...actionOptions,
379
+ [DIRTY_DISPATCH_TOKEN]: token
380
+ }
381
+ ] : args;
382
+ let actionResult;
383
+ requestTokenStack.push({ key: requestKey, token });
384
+ try {
385
+ actionResult = Reflect.apply(action, receiver, forwardedArgs);
386
+ } finally {
387
+ requestTokenStack.pop();
388
+ }
389
+ const result = await actionResult;
390
+ if (ownsToken) {
391
+ markResourceActionDataSourceDirtyOnce(token, context, dirtyResourceAction, headers);
313
392
  }
314
393
  return result;
315
394
  };
@@ -318,31 +397,206 @@ function createDirtyAwareResource(context, resource, resourceName, resourceOf, h
318
397
  }
319
398
  __name(createDirtyAwareResource, "createDirtyAwareResource");
320
399
  function createDirtyAwareApiClient(api, context) {
400
+ const baseResource = api.resource;
401
+ const baseRequest = api.request;
402
+ const shouldUseResourceDispatchReceiver = baseResource === import_sdk.APIClient.prototype.resource;
403
+ const shouldUseRequestDispatchReceiver = baseRequest === import_sdk.APIClient.prototype.request;
404
+ const resourceTokenStack = [];
405
+ const requestTokenStack = [];
406
+ let hasResourceOverride = false;
407
+ let resourceOverride;
408
+ let hasRequestOverride = false;
409
+ let requestOverride;
410
+ const getCurrentResource = /* @__PURE__ */ __name(() => hasResourceOverride ? resourceOverride : resource, "getCurrentResource");
411
+ const getCurrentRequest = /* @__PURE__ */ __name(() => hasRequestOverride ? requestOverride : request, "getCurrentRequest");
412
+ const dispatchResource = /* @__PURE__ */ __name((token, args) => {
413
+ const resourceKey = getResourceDispatchKey(args[0], args[1], args[2]);
414
+ const activeToken = getMatchingResourceToken(token, resourceKey);
415
+ const shouldWrapActions = !!activeToken && hasResourceOverride;
416
+ if (activeToken) {
417
+ activeToken.resourceKey ||= resourceKey;
418
+ resourceTokenStack.push({ key: resourceKey, token: activeToken });
419
+ }
420
+ try {
421
+ const resourceInstance = Reflect.apply(getCurrentResource(), proxy, args);
422
+ if (!shouldWrapActions || !activeToken) {
423
+ return resourceInstance;
424
+ }
425
+ return new Proxy(resourceInstance, {
426
+ get(target, prop, receiver) {
427
+ const original = Reflect.get(target, prop, receiver);
428
+ if (typeof prop !== "string" || typeof original !== "function" || !isMutatingResourceAction(prop)) {
429
+ return original;
430
+ }
431
+ const action = original;
432
+ return (...actionArgs) => {
433
+ const actionOptions = isObjectRecord(actionArgs[1]) ? actionArgs[1] : void 0;
434
+ const forwardedArgs = actionOptions || actionArgs[1] == null ? [
435
+ actionArgs[0],
436
+ {
437
+ ...actionOptions,
438
+ [DIRTY_DISPATCH_TOKEN]: activeToken
439
+ }
440
+ ] : actionArgs;
441
+ resourceTokenStack.push({ key: resourceKey, token: activeToken });
442
+ requestTokenStack.push({ key: activeToken.requestKey, token: activeToken });
443
+ try {
444
+ return Reflect.apply(action, receiver, forwardedArgs);
445
+ } finally {
446
+ requestTokenStack.pop();
447
+ resourceTokenStack.pop();
448
+ }
449
+ };
450
+ }
451
+ });
452
+ } finally {
453
+ if (activeToken) {
454
+ resourceTokenStack.pop();
455
+ }
456
+ }
457
+ }, "dispatchResource");
458
+ const createDispatchReceiver = /* @__PURE__ */ __name((token) => {
459
+ const receiver = Object.create(api);
460
+ Object.defineProperties(receiver, {
461
+ request: {
462
+ configurable: true,
463
+ value: /* @__PURE__ */ __name((config) => {
464
+ const options = config;
465
+ const requestKey = getRequestDispatchKey(options, context);
466
+ const stackFrame = requestTokenStack.at(-1);
467
+ const activeToken = getMatchingRequestToken(options == null ? void 0 : options[DIRTY_DISPATCH_TOKEN], requestKey) || (requestKey && (stackFrame == null ? void 0 : stackFrame.key) === requestKey ? stackFrame.token : void 0) || getMatchingRequestToken(token, requestKey);
468
+ const { [DIRTY_DISPATCH_TOKEN]: _dirtyDispatchToken, ...cleanOptions } = options;
469
+ const configWithToken = activeToken ? { ...cleanOptions, [DIRTY_DISPATCH_TOKEN]: activeToken } : cleanOptions;
470
+ if (activeToken) {
471
+ activeToken.requestKey ||= requestKey;
472
+ requestTokenStack.push({ key: requestKey, token: activeToken });
473
+ }
474
+ try {
475
+ return Reflect.apply(getCurrentRequest(), proxy, [configWithToken]);
476
+ } finally {
477
+ if (activeToken) {
478
+ requestTokenStack.pop();
479
+ }
480
+ }
481
+ }, "value")
482
+ },
483
+ resource: {
484
+ configurable: true,
485
+ value: /* @__PURE__ */ __name((...args) => {
486
+ const resourceKey = getResourceDispatchKey(args[0], args[1], args[2]);
487
+ const stackFrame = resourceTokenStack.at(-1);
488
+ const activeToken = getMatchingResourceToken(token, resourceKey) || (resourceKey && (stackFrame == null ? void 0 : stackFrame.key) === resourceKey ? stackFrame.token : void 0);
489
+ return dispatchResource(activeToken, args);
490
+ }, "value")
491
+ }
492
+ });
493
+ return receiver;
494
+ }, "createDispatchReceiver");
321
495
  const resource = /* @__PURE__ */ __name((name, of, headers, cancel) => {
322
- const targetResource = api.resource(name, of, headers, cancel);
323
- return createDirtyAwareResource(context, targetResource, name, of, headers);
496
+ const resourceKey = getResourceDispatchKey(name, of, headers);
497
+ const stackFrame = resourceTokenStack.at(-1);
498
+ const parentToken = resourceKey && (stackFrame == null ? void 0 : stackFrame.key) === resourceKey ? stackFrame.token : void 0;
499
+ const receiver = createDispatchReceiver(parentToken);
500
+ const resourceInstance = Reflect.apply(baseResource, shouldUseResourceDispatchReceiver ? receiver : api, [
501
+ name,
502
+ of,
503
+ headers,
504
+ cancel
505
+ ]);
506
+ return createDirtyAwareResource(context, resourceInstance, name, of, headers, requestTokenStack, parentToken);
324
507
  }, "resource");
325
508
  const request = /* @__PURE__ */ __name((config) => {
326
509
  const options = config;
510
+ const requestKey = getRequestDispatchKey(options, context);
511
+ const stackFrame = requestTokenStack.at(-1);
512
+ const inheritedToken = getMatchingRequestToken(options == null ? void 0 : options[DIRTY_DISPATCH_TOKEN], requestKey) || (requestKey && (stackFrame == null ? void 0 : stackFrame.key) === requestKey ? stackFrame.token : void 0);
513
+ const token = inheritedToken || { marked: false, skip: false };
514
+ const ownsToken = !inheritedToken;
515
+ token.requestKey ||= requestKey;
516
+ if (typeof options.resource === "string") {
517
+ token.resourceKey ||= getResourceDispatchKey(options.resource, options.resourceOf, options.headers);
518
+ }
327
519
  const skipDataSourceDirty = options == null ? void 0 : options[SKIP_DATA_SOURCE_DIRTY];
328
- const dirtyResourceAction = skipDataSourceDirty ? void 0 : resolveDirtyResourceAction(options, context);
329
- const { [SKIP_DATA_SOURCE_DIRTY]: _skipDataSourceDirty, ...cleanConfig } = options;
330
- return api.request(cleanConfig).then((result) => {
331
- if (dirtyResourceAction && isMutatingResourceAction(dirtyResourceAction.actionName)) {
332
- markResourceActionDataSourceDirty(context, dirtyResourceAction, options.headers);
520
+ if (skipDataSourceDirty) {
521
+ token.skip = true;
522
+ }
523
+ const dirtyResourceAction = resolveDirtyResourceAction(options, context);
524
+ const {
525
+ [DIRTY_DISPATCH_TOKEN]: _dirtyDispatchToken,
526
+ [SKIP_DATA_SOURCE_DIRTY]: _skipDataSourceDirty,
527
+ ...cleanConfig
528
+ } = options;
529
+ const receiver = createDispatchReceiver(token);
530
+ return Reflect.apply(baseRequest, shouldUseRequestDispatchReceiver ? receiver : api, [cleanConfig]).then((result) => {
531
+ if (ownsToken) {
532
+ markResourceActionDataSourceDirtyOnce(token, context, dirtyResourceAction, options.headers);
333
533
  }
334
534
  return result;
335
535
  });
336
536
  }, "request");
537
+ const isLockedOwnProperty = /* @__PURE__ */ __name((target, prop) => {
538
+ const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
539
+ return !!descriptor && !descriptor.configurable;
540
+ }, "isLockedOwnProperty");
337
541
  const proxy = new Proxy(api, {
338
542
  get(target, prop, receiver) {
339
543
  if (prop === "resource") {
340
- return resource;
544
+ if (isLockedOwnProperty(target, prop)) {
545
+ return Reflect.get(target, prop, receiver);
546
+ }
547
+ return hasResourceOverride ? resourceOverride : resource;
341
548
  }
342
549
  if (prop === "request") {
343
- return request;
550
+ if (isLockedOwnProperty(target, prop)) {
551
+ return Reflect.get(target, prop, receiver);
552
+ }
553
+ return hasRequestOverride ? requestOverride : request;
344
554
  }
345
555
  return Reflect.get(target, prop, receiver);
556
+ },
557
+ set(target, prop, value, receiver) {
558
+ if (prop === "resource") {
559
+ const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
560
+ if (descriptor && (!descriptor.configurable || !Reflect.isExtensible(target))) {
561
+ return false;
562
+ }
563
+ hasResourceOverride = true;
564
+ resourceOverride = value;
565
+ return true;
566
+ }
567
+ if (prop === "request") {
568
+ const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
569
+ if (descriptor && (!descriptor.configurable || !Reflect.isExtensible(target))) {
570
+ return false;
571
+ }
572
+ hasRequestOverride = true;
573
+ requestOverride = value;
574
+ return true;
575
+ }
576
+ return Reflect.set(target, prop, value, receiver);
577
+ },
578
+ deleteProperty(target, prop) {
579
+ if (prop !== "resource" && prop !== "request") {
580
+ return Reflect.deleteProperty(target, prop);
581
+ }
582
+ const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
583
+ if (descriptor && (!descriptor.configurable || !Reflect.isExtensible(target))) {
584
+ return false;
585
+ }
586
+ if (prop === "resource") {
587
+ hasResourceOverride = false;
588
+ resourceOverride = void 0;
589
+ } else {
590
+ hasRequestOverride = false;
591
+ requestOverride = void 0;
592
+ }
593
+ return true;
594
+ },
595
+ defineProperty(target, prop, descriptor) {
596
+ if (prop === "resource" || prop === "request") {
597
+ return false;
598
+ }
599
+ return Reflect.defineProperty(target, prop, descriptor);
346
600
  }
347
601
  });
348
602
  dirtyAwareApiClientProxies.add(proxy);
@@ -17,6 +17,7 @@ type DirtyKeyOptions = {
17
17
  export declare const createLoadedPageCache: () => {
18
18
  getDirtyKeyForModel(model?: FlowModel | null, options?: DirtyKeyOptions): string | undefined;
19
19
  markDirty(key?: string): void;
20
+ markDirtyForOptions(options?: LoadedPageOptions): void;
20
21
  shouldBypass(options?: LoadedPageOptions, isFlowSettingsEnabled?: () => boolean): boolean;
21
22
  clear(options?: LoadedPageOptions): void;
22
23
  mountModelToParent: <T extends FlowModel<import("..").DefaultStructure> = FlowModel<import("..").DefaultStructure>>(model: T, forceReplace?: boolean) => T;
@@ -113,6 +113,12 @@ const createLoadedPageCache = /* @__PURE__ */ __name(() => {
113
113
  dirtyKeys.add(key);
114
114
  }
115
115
  },
116
+ markDirtyForOptions(options) {
117
+ const key = getLoadedPageKey(options);
118
+ if (key) {
119
+ dirtyKeys.add(key);
120
+ }
121
+ },
116
122
  shouldBypass(options, isFlowSettingsEnabled) {
117
123
  const key = getLoadedPageKey(options);
118
124
  if (!key || !dirtyKeys.has(key)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.2.0-alpha.6",
3
+ "version": "2.2.0-alpha.8",
4
4
  "private": false,
5
5
  "description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
6
6
  "main": "lib/index.js",
@@ -8,8 +8,8 @@
8
8
  "dependencies": {
9
9
  "@formily/antd-v5": "1.x",
10
10
  "@formily/reactive": "2.x",
11
- "@nocobase/sdk": "2.2.0-alpha.6",
12
- "@nocobase/shared": "2.2.0-alpha.6",
11
+ "@nocobase/sdk": "2.2.0-alpha.8",
12
+ "@nocobase/shared": "2.2.0-alpha.8",
13
13
  "ahooks": "^3.7.2",
14
14
  "axios": "^1.7.0",
15
15
  "dayjs": "^1.11.9",
@@ -37,5 +37,5 @@
37
37
  ],
38
38
  "author": "NocoBase Team",
39
39
  "license": "Apache-2.0",
40
- "gitHead": "f55b252811f6af01d08cd3814171867b70c8ef7a"
40
+ "gitHead": "810c81e5963966bda7ec03b6453a3a80fe60e027"
41
41
  }
@@ -340,9 +340,9 @@ describe('ViewScopedFlowEngine', () => {
340
340
  const repository = new DirtyPageRepository();
341
341
  root.setModelRepository(repository);
342
342
 
343
- class ParentModel extends FlowModel {}
344
- class PageModel extends FlowModel {}
345
343
  class BlockModel extends FlowModel {}
344
+ class PageModel extends FlowModel<{ parent?: FlowModel; subModels: { items: BlockModel[] } }> {}
345
+ class ParentModel extends FlowModel<{ parent?: FlowModel; subModels: { page?: PageModel } }> {}
346
346
  root.registerModels({ ParentModel, PageModel, BlockModel });
347
347
 
348
348
  const parent = root.createModel<ParentModel>({ use: 'ParentModel', uid: 'popup-action' });
@@ -357,7 +357,10 @@ describe('ViewScopedFlowEngine', () => {
357
357
  items: [{ use: 'BlockModel', uid: 'stale-block' }],
358
358
  },
359
359
  });
360
- const staleBlock = stalePage.findSubModel('items' as any, (item) => item.uid === 'stale-block') as FlowModel;
360
+ const staleBlock = stalePage.findSubModel('items', (item) => item.uid === 'stale-block');
361
+ if (!staleBlock) {
362
+ throw new Error('Expected stale block to be loaded');
363
+ }
361
364
  parent.setSubModel('page', stalePage);
362
365
  oldScoped.unlinkFromStack();
363
366
 
@@ -372,7 +375,7 @@ describe('ViewScopedFlowEngine', () => {
372
375
  },
373
376
  };
374
377
 
375
- root.flowSettings.enable();
378
+ await root.flowSettings.enable();
376
379
  await staleBlock.saveStepParams();
377
380
  root.flowSettings.disable();
378
381
  repository.findOneCalls = 0;
@@ -388,8 +391,8 @@ describe('ViewScopedFlowEngine', () => {
388
391
 
389
392
  expect(repository.findOneCalls).toBe(1);
390
393
  expect(loaded).not.toBe(stalePage);
391
- expect((parent.subModels as any).page).toBe(loaded);
392
- expect(loaded?.mapSubModels('items' as any, (item) => item.uid)).toEqual(['fresh-block']);
394
+ expect(parent.subModels.page).toBe(loaded);
395
+ expect(loaded?.mapSubModels('items', (item) => item.uid)).toEqual(['fresh-block']);
393
396
 
394
397
  repository.findOneCalls = 0;
395
398
  const nextRuntimeScoped = createViewScopedEngine(root);
@@ -405,6 +408,69 @@ describe('ViewScopedFlowEngine', () => {
405
408
  expect(loadedAgain?.uid).toBe('popup-page');
406
409
  });
407
410
 
411
+ it('reloads a page after it was loaded in flow settings mode', async () => {
412
+ const root = new FlowEngine();
413
+ const repository = new DirtyPageRepository();
414
+ root.setModelRepository(repository);
415
+
416
+ class ParentModel extends FlowModel {}
417
+ class PageModel extends FlowModel {}
418
+ class BlockModel extends FlowModel {}
419
+ root.registerModels({ ParentModel, PageModel, BlockModel });
420
+
421
+ const parent = root.createModel<ParentModel>({ use: 'ParentModel', uid: 'settings-popup-action' });
422
+ repository.data = {
423
+ use: 'PageModel',
424
+ uid: 'settings-popup-page',
425
+ parentId: parent.uid,
426
+ subKey: 'page',
427
+ subType: 'object',
428
+ subModels: {
429
+ items: [{ use: 'BlockModel', uid: 'stale-settings-block' }],
430
+ },
431
+ };
432
+
433
+ await root.flowSettings.enable();
434
+ const designScoped = createViewScopedEngine(root);
435
+ const designLoaded = await designScoped.loadOrCreateModel<PageModel>({
436
+ async: true,
437
+ parentId: parent.uid,
438
+ subKey: 'page',
439
+ subType: 'object',
440
+ use: 'PageModel',
441
+ });
442
+ expect(repository.findOneCalls).toBe(1);
443
+ expect(designLoaded?.mapSubModels('items', (item) => item.uid)).toEqual(['stale-settings-block']);
444
+ designScoped.unlinkFromStack();
445
+
446
+ repository.data = {
447
+ use: 'PageModel',
448
+ uid: 'settings-popup-page',
449
+ parentId: parent.uid,
450
+ subKey: 'page',
451
+ subType: 'object',
452
+ subModels: {
453
+ items: [{ use: 'BlockModel', uid: 'fresh-settings-block' }],
454
+ },
455
+ };
456
+ root.flowSettings.disable();
457
+ repository.findOneCalls = 0;
458
+
459
+ const runtimeScoped = createViewScopedEngine(root);
460
+ const runtimeLoaded = await runtimeScoped.loadOrCreateModel<PageModel>({
461
+ async: true,
462
+ parentId: parent.uid,
463
+ subKey: 'page',
464
+ subType: 'object',
465
+ use: 'PageModel',
466
+ });
467
+
468
+ expect(repository.findOneCalls).toBe(1);
469
+ expect(runtimeLoaded).not.toBe(designLoaded);
470
+ expect(parent.subModels.page).toBe(runtimeLoaded);
471
+ expect(runtimeLoaded?.mapSubModels('items', (item) => item.uid)).toEqual(['fresh-settings-block']);
472
+ });
473
+
408
474
  it('does not bypass loaded page cache after a non-config save', async () => {
409
475
  const root = new FlowEngine();
410
476
  const repository = new DirtyPageRepository();
@@ -292,11 +292,11 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
292
292
  const defaultChildren = useMemo(() => {
293
293
  const hasSelected = active ?? Boolean(currentPath && currentPath.length > 0);
294
294
  return (
295
- <Button type={hasSelected ? 'primary' : 'default'} style={defaultButtonStyle}>
295
+ <Button type={hasSelected ? 'primary' : 'default'} style={defaultButtonStyle} disabled={cascaderProps.disabled}>
296
296
  x
297
297
  </Button>
298
298
  );
299
- }, [active, currentPath]);
299
+ }, [active, cascaderProps.disabled, currentPath]);
300
300
 
301
301
  // 处理选择变化事件
302
302
  const handleChange = useCallback(
@@ -54,15 +54,20 @@ const formItemPropKeys: (keyof ExtendedFormItemProps)[] = [
54
54
  'showLabel',
55
55
  ];
56
56
 
57
+ const modelInternalPropKeys = ['globalSort'];
58
+
57
59
  export const FormItem = ({
58
60
  children,
59
61
  showLabel = true,
60
62
  labelWidth,
61
63
  ...rest
62
64
  }: ExtendedFormItemProps & ChildExtraProps) => {
65
+ const forwardedRest = Object.fromEntries(
66
+ Object.entries(rest).filter(([key]) => !modelInternalPropKeys.includes(key)),
67
+ ) as ExtendedFormItemProps & ChildExtraProps;
63
68
  // 过滤掉 Form.Item 专用 props,只保留要传给子组件的
64
69
  const childProps = Object.fromEntries(
65
- Object.entries(rest).filter(([key]) => !formItemPropKeys.includes(key as keyof ExtendedFormItemProps)),
70
+ Object.entries(forwardedRest).filter(([key]) => !formItemPropKeys.includes(key as keyof ExtendedFormItemProps)),
66
71
  );
67
72
 
68
73
  const processedChildren =
@@ -74,7 +79,7 @@ export const FormItem = ({
74
79
  }
75
80
  return child;
76
81
  });
77
- const { label, labelWrap, colon = true, layout } = rest;
82
+ const { label, labelWrap, colon = true, layout } = forwardedRest;
78
83
  const effectiveLabelWrap = !layout || layout === 'vertical' ? true : labelWrap;
79
84
  const labelColStyle =
80
85
  layout === 'vertical' ? { width: labelWidth, ...verticalFormItemLabelStyle } : { width: labelWidth };
@@ -122,17 +127,17 @@ export const FormItem = ({
122
127
  };
123
128
  return (
124
129
  <Form.Item
125
- {...rest}
126
- style={{ ...formItemStyle, ...rest.style }}
130
+ {...forwardedRest}
131
+ style={{ ...formItemStyle, ...forwardedRest.style }}
127
132
  labelCol={{ style: labelColStyle }}
128
133
  layout={layout}
129
134
  label={renderLabel()}
130
135
  colon={false}
131
- extra={rest.extra && <span style={{ whiteSpace: 'pre-wrap' }}>{rest.extra}</span>}
136
+ extra={forwardedRest.extra && <span style={{ whiteSpace: 'pre-wrap' }}>{forwardedRest.extra}</span>}
132
137
  tooltip={
133
- rest.tooltip &&
138
+ forwardedRest.tooltip &&
134
139
  ({
135
- title: rest.tooltip,
140
+ title: forwardedRest.tooltip,
136
141
  overlayInnerStyle: { whiteSpace: 'pre-line' },
137
142
  } as TooltipProps)
138
143
  }
@@ -160,7 +160,7 @@ export const useMobileActionDrawerStyle = genStyleHook('nb-mobile-action-drawer'
160
160
  borderBottom: `1px solid ${token.colorSplit}`,
161
161
  position: 'sticky',
162
162
  top: 0,
163
- backgroundColor: 'white',
163
+ backgroundColor: token.colorBgContainer,
164
164
  zIndex: 1000,
165
165
 
166
166
  // to match the button named 'Add block'
@@ -172,12 +172,23 @@ export const useMobileActionDrawerStyle = genStyleHook('nb-mobile-action-drawer'
172
172
  '.nb-mobile-action-drawer-placeholder': {
173
173
  display: 'inline-block',
174
174
  padding: 12,
175
+ flex: '0 0 auto',
175
176
  visibility: 'hidden',
176
177
  },
177
178
 
179
+ '.nb-mobile-action-drawer-title': {
180
+ flex: '1 1 auto',
181
+ minWidth: 0,
182
+ overflow: 'hidden',
183
+ textAlign: 'center',
184
+ textOverflow: 'ellipsis',
185
+ whiteSpace: 'nowrap',
186
+ },
187
+
178
188
  '.nb-mobile-action-drawer-close-icon': {
179
189
  display: 'inline-block',
180
190
  padding: 12,
191
+ flex: '0 0 auto',
181
192
  cursor: 'pointer',
182
193
  },
183
194
 
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { ConfigProvider } from 'antd';
11
- import React, { FC, ReactNode, useMemo } from 'react';
11
+ import React, { FC, ReactNode, useCallback, useMemo } from 'react';
12
12
  import { useMobileActionDrawerStyle } from './MobilePopup.style';
13
13
  import { useTranslation } from 'react-i18next';
14
14
  import { lazy } from '../lazy-helper';
@@ -31,11 +31,33 @@ export const MobilePopup: FC<MobilePopupProps> = (props) => {
31
31
  const { t } = useTranslation();
32
32
  const { componentCls, hashId } = useMobileActionDrawerStyle();
33
33
 
34
- const style = useMemo(() => {
34
+ const bodyStyles = (props as MobilePopupProps & { styles?: { body?: React.CSSProperties } }).styles?.body;
35
+ const popupStyle = useMemo(() => {
35
36
  return {
36
- minHeight,
37
+ minHeight: bodyStyles?.minHeight ?? minHeight,
38
+ height: bodyStyles?.height,
39
+ maxHeight: bodyStyles?.maxHeight,
37
40
  };
38
- }, [minHeight]);
41
+ }, [bodyStyles?.height, bodyStyles?.maxHeight, bodyStyles?.minHeight, minHeight]);
42
+
43
+ const bodyStyle = useMemo(() => {
44
+ return {
45
+ padding: 0,
46
+ ...bodyStyles,
47
+ };
48
+ }, [bodyStyles]);
49
+
50
+ const handleCloseKeyDown = useCallback(
51
+ (event: React.KeyboardEvent<HTMLSpanElement>) => {
52
+ if (event.key !== 'Enter' && event.key !== ' ') {
53
+ return;
54
+ }
55
+
56
+ event.preventDefault();
57
+ closePopup();
58
+ },
59
+ [closePopup],
60
+ );
39
61
 
40
62
  const theme = useMemo(() => {
41
63
  return {
@@ -57,11 +79,8 @@ export const MobilePopup: FC<MobilePopupProps> = (props) => {
57
79
  onClose={closePopup}
58
80
  onMaskClick={closePopup}
59
81
  bodyClassName="nb-mobile-action-drawer-body"
60
- bodyStyle={{
61
- padding: 0,
62
- }}
63
- maskStyle={style}
64
- style={style}
82
+ bodyStyle={bodyStyle}
83
+ style={popupStyle}
65
84
  destroyOnClose
66
85
  >
67
86
  <div className="nb-mobile-action-drawer-header">
@@ -69,13 +88,14 @@ export const MobilePopup: FC<MobilePopupProps> = (props) => {
69
88
  <span className="nb-mobile-action-drawer-placeholder">
70
89
  <CloseOutline />
71
90
  </span>
72
- <span>{title}</span>
91
+ <span className="nb-mobile-action-drawer-title">{title}</span>
73
92
  <span
74
93
  className="nb-mobile-action-drawer-close-icon"
75
94
  onClick={closePopup}
76
95
  role="button"
77
96
  tabIndex={0}
78
97
  aria-label={t('Close')}
98
+ onKeyDown={handleCloseKeyDown}
79
99
  >
80
100
  <CloseOutline />
81
101
  </span>