@nocobase/flow-engine 2.2.0-alpha.1 → 2.2.0-alpha.11

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 (115) hide show
  1. package/lib/JSRunner.d.ts +1 -0
  2. package/lib/JSRunner.js +110 -20
  3. package/lib/acl/Acl.d.ts +2 -1
  4. package/lib/acl/Acl.js +28 -0
  5. package/lib/components/FieldModelRenderer.js +44 -31
  6. package/lib/components/FlowContextSelector.js +55 -12
  7. package/lib/components/FormItem.js +11 -7
  8. package/lib/components/MobilePopup.js +39 -10
  9. package/lib/components/MobilePopup.style.js +27 -6
  10. package/lib/components/dnd/index.js +9 -2
  11. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +86 -32
  12. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +20 -0
  13. package/lib/components/subModel/LazyDropdown.js +41 -26
  14. package/lib/components/variables/VariableHybridInput.d.ts +9 -0
  15. package/lib/components/variables/VariableHybridInput.js +146 -17
  16. package/lib/components/variables/VariableInput.js +19 -7
  17. package/lib/components/variables/VariableTag.js +48 -36
  18. package/lib/components/variables/types.d.ts +21 -0
  19. package/lib/flowContext.d.ts +1 -1
  20. package/lib/flowContext.js +97 -42
  21. package/lib/flowEngine.js +6 -0
  22. package/lib/flowI18n.js +3 -3
  23. package/lib/flowSettings.d.ts +5 -1
  24. package/lib/flowSettings.js +70 -0
  25. package/lib/locale/en-US.json +3 -0
  26. package/lib/locale/index.d.ts +6 -0
  27. package/lib/locale/zh-CN.json +3 -0
  28. package/lib/resources/apiResource.js +2 -1
  29. package/lib/resources/baseRecordResource.js +6 -17
  30. package/lib/resources/multiRecordResource.js +13 -3
  31. package/lib/resources/singleRecordResource.js +7 -2
  32. package/lib/runjs-context/helpers.js +12 -5
  33. package/lib/types.d.ts +15 -1
  34. package/lib/types.js +1 -0
  35. package/lib/utils/dataSourceDirty.d.ts +20 -0
  36. package/lib/utils/dataSourceDirty.js +139 -0
  37. package/lib/utils/dirtyAwareApiClient.d.ts +11 -0
  38. package/lib/utils/dirtyAwareApiClient.js +632 -0
  39. package/lib/utils/index.d.ts +1 -1
  40. package/lib/utils/index.js +11 -11
  41. package/lib/utils/loadedPageCache.d.ts +1 -0
  42. package/lib/utils/loadedPageCache.js +6 -0
  43. package/lib/utils/openViewRouteState.d.ts +28 -0
  44. package/lib/utils/openViewRouteState.js +125 -0
  45. package/lib/utils/parsePathnameToViewParams.d.ts +3 -0
  46. package/lib/utils/parsePathnameToViewParams.js +18 -1
  47. package/lib/utils/resolveRunJSObjectValues.js +3 -2
  48. package/lib/utils/runjsModuleLoader.js +0 -30
  49. package/lib/views/ViewNavigation.js +5 -0
  50. package/package.json +4 -4
  51. package/src/JSRunner.ts +112 -25
  52. package/src/__tests__/JSRunner.test.ts +4 -5
  53. package/src/__tests__/flowContext.test.ts +154 -0
  54. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +51 -0
  55. package/src/__tests__/flowI18n.test.ts +11 -0
  56. package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
  57. package/src/__tests__/flowSettings.test.ts +72 -0
  58. package/src/__tests__/runjsRuntimeFeatures.test.ts +15 -2
  59. package/src/__tests__/viewScopedFlowEngine.test.ts +72 -6
  60. package/src/acl/Acl.tsx +36 -1
  61. package/src/acl/__tests__/Acl.test.tsx +70 -0
  62. package/src/components/FieldModelRenderer.tsx +50 -36
  63. package/src/components/FlowContextSelector.tsx +66 -11
  64. package/src/components/FormItem.tsx +12 -7
  65. package/src/components/MobilePopup.style.ts +34 -7
  66. package/src/components/MobilePopup.tsx +42 -10
  67. package/src/components/__tests__/FieldModelRenderer.test.tsx +165 -0
  68. package/src/components/__tests__/FormItem.test.tsx +17 -2
  69. package/src/components/__tests__/MobilePopup.style.test.tsx +103 -0
  70. package/src/components/__tests__/MobilePopup.test.tsx +150 -0
  71. package/src/components/dnd/index.tsx +11 -2
  72. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +105 -35
  73. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +381 -12
  74. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +28 -0
  75. package/src/components/subModel/LazyDropdown.tsx +44 -26
  76. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +85 -2
  77. package/src/components/variables/VariableHybridInput.tsx +185 -14
  78. package/src/components/variables/VariableInput.tsx +32 -7
  79. package/src/components/variables/VariableTag.tsx +51 -37
  80. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +60 -3
  81. package/src/components/variables/__tests__/VariableHybridInput.test.tsx +212 -0
  82. package/src/components/variables/__tests__/VariableInput.test.tsx +202 -6
  83. package/src/components/variables/__tests__/VariableTag.test.tsx +80 -0
  84. package/src/components/variables/types.ts +21 -0
  85. package/src/flowContext.ts +104 -36
  86. package/src/flowEngine.ts +6 -0
  87. package/src/flowI18n.ts +8 -3
  88. package/src/flowSettings.ts +85 -1
  89. package/src/locale/__tests__/index.test.ts +21 -0
  90. package/src/locale/en-US.json +3 -0
  91. package/src/locale/zh-CN.json +3 -0
  92. package/src/resources/apiResource.ts +2 -1
  93. package/src/resources/baseRecordResource.ts +6 -23
  94. package/src/resources/multiRecordResource.ts +13 -3
  95. package/src/resources/singleRecordResource.ts +6 -1
  96. package/src/runjs-context/helpers.ts +12 -6
  97. package/src/types.ts +16 -0
  98. package/src/utils/__tests__/dirtyAwareApiClient.test.ts +713 -0
  99. package/src/utils/__tests__/openViewRouteState.test.ts +40 -0
  100. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +36 -0
  101. package/src/utils/dataSourceDirty.ts +126 -0
  102. package/src/utils/dirtyAwareApiClient.ts +742 -0
  103. package/src/utils/index.ts +10 -9
  104. package/src/utils/loadedPageCache.ts +7 -0
  105. package/src/utils/openViewRouteState.ts +107 -0
  106. package/src/utils/parsePathnameToViewParams.ts +23 -1
  107. package/src/utils/resolveRunJSObjectValues.ts +5 -2
  108. package/src/utils/runjsModuleLoader.ts +0 -32
  109. package/src/views/ViewNavigation.ts +6 -1
  110. package/src/views/__tests__/ViewNavigation.test.ts +15 -0
  111. package/lib/utils/safeGlobals.d.ts +0 -28
  112. package/lib/utils/safeGlobals.js +0 -367
  113. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +0 -38
  114. package/src/utils/__tests__/safeGlobals.test.ts +0 -106
  115. package/src/utils/safeGlobals.ts +0 -406
@@ -0,0 +1,40 @@
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 { decodeOpenViewRouteState, encodeOpenViewRouteState, isOpenViewRouteStateToken } from '../openViewRouteState';
11
+
12
+ describe('openViewRouteState', () => {
13
+ it('encodes route state as an 8-letter view-bound token', () => {
14
+ const token = encodeOpenViewRouteState('popup', { mode: 'dialog', size: 'large' });
15
+
16
+ expect(token).toMatch(/^[A-Za-z]{8}$/);
17
+ expect(isOpenViewRouteStateToken(token)).toBe(true);
18
+ expect(decodeOpenViewRouteState('popup', token)).toEqual({ mode: 'dialog', size: 'large' });
19
+ expect(decodeOpenViewRouteState('other-popup', token)).toBeUndefined();
20
+ });
21
+
22
+ it('can encode mode without leaking a default size', () => {
23
+ const token = encodeOpenViewRouteState('popup', { mode: 'embed' });
24
+
25
+ expect(token).toMatch(/^[A-Za-z]{8}$/);
26
+ expect(decodeOpenViewRouteState('popup', token)).toEqual({ mode: 'embed' });
27
+ });
28
+
29
+ it('generates distinct tokens for supported route state combinations', () => {
30
+ const modes = [undefined, 'drawer', 'dialog', 'embed'] as const;
31
+ const sizes = [undefined, 'small', 'medium', 'large'] as const;
32
+ const tokens = modes.flatMap((mode) =>
33
+ sizes
34
+ .map((size) => encodeOpenViewRouteState('popup', { mode, size }))
35
+ .filter((token): token is string => !!token),
36
+ );
37
+
38
+ expect(new Set(tokens).size).toBe(tokens.length);
39
+ });
40
+ });
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { parsePathnameToViewParams } from '../parsePathnameToViewParams';
11
+ import { encodeOpenViewRouteState } from '../openViewRouteState';
11
12
 
12
13
  describe('parsePathnameToViewParams', () => {
13
14
  test('should return single view param for basic admin path', () => {
@@ -48,6 +49,41 @@ describe('parsePathnameToViewParams', () => {
48
49
  expect(result).toEqual([{ viewUid: 'xxx' }, { viewUid: 'yyy', filterByTk: '1', sourceId: '1' }]);
49
50
  });
50
51
 
52
+ test('should parse RunJS openView route state without losing route params', () => {
53
+ const token = encodeOpenViewRouteState('yyy', { mode: 'dialog', size: 'large' });
54
+ if (!token) {
55
+ throw new Error('Expected openView route state token.');
56
+ }
57
+ const result = parsePathnameToViewParams(`/admin/xxx/view/yyy/opts/${token}/filterbytk/1/sourceid/2`);
58
+
59
+ expect(result).toEqual([
60
+ { viewUid: 'xxx' },
61
+ {
62
+ viewUid: 'yyy',
63
+ openViewRouteState: { mode: 'dialog', size: 'large' },
64
+ filterByTk: '1',
65
+ sourceId: '2',
66
+ },
67
+ ]);
68
+ });
69
+
70
+ test('should ignore invalid RunJS openView opts and keep following params', () => {
71
+ const result = parsePathnameToViewParams('/admin/xxx/view/yyy/opts/AbCdEfGh/filterbytk/1');
72
+
73
+ expect(result).toEqual([{ viewUid: 'xxx' }, { viewUid: 'yyy', filterByTk: '1' }]);
74
+ });
75
+
76
+ test('should not parse bare RunJS openView route state token segments', () => {
77
+ const token = encodeOpenViewRouteState('yyy', { mode: 'dialog', size: 'large' });
78
+ if (!token) {
79
+ throw new Error('Expected openView route state token.');
80
+ }
81
+ const result = parsePathnameToViewParams(`/admin/xxx/view/yyy/${token}/filterbytk/1`);
82
+
83
+ expect(result[1]).toMatchObject({ viewUid: 'yyy' });
84
+ expect(result[1]?.openViewRouteState).toBeUndefined();
85
+ });
86
+
51
87
  test('should handle multiple views with different filterByTk and sourceId', () => {
52
88
  const result = parsePathnameToViewParams(
53
89
  '/admin/xxx/view/yyy/filterbytk/1/sourceid/1/view/zzz/filterbytk/2/sourceid/2',
@@ -0,0 +1,126 @@
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 type { FlowEngine } from '../flowEngine';
11
+ import { DATA_SOURCE_DIRTY_EVENT } from '../views/viewEvents';
12
+
13
+ type MarkDataSourceDirtyOptions = {
14
+ engine?: FlowEngine;
15
+ dataSourceKey?: unknown;
16
+ resourceName?: unknown;
17
+ includePreviousEngines?: boolean;
18
+ };
19
+
20
+ export function getHeaderValue(headers: unknown, name: string): unknown {
21
+ if (!headers || typeof headers !== 'object') {
22
+ return undefined;
23
+ }
24
+
25
+ const maybeHeaders = headers as { get?: (key: string) => unknown };
26
+ if (typeof maybeHeaders.get === 'function') {
27
+ const value = maybeHeaders.get(name);
28
+ if (value != null && value !== '') {
29
+ return value;
30
+ }
31
+ }
32
+
33
+ const lowerName = name.toLowerCase();
34
+ for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {
35
+ if (key.toLowerCase() === lowerName) {
36
+ return value;
37
+ }
38
+ }
39
+
40
+ return undefined;
41
+ }
42
+
43
+ export function getDataSourceKeyFromHeaders(headers: unknown): string {
44
+ const value = getHeaderValue(headers, 'x-data-source');
45
+ if (Array.isArray(value)) {
46
+ return String(value[0] || 'main');
47
+ }
48
+ return value == null || value === '' ? 'main' : String(value);
49
+ }
50
+
51
+ export function getAffectedResourceNames(resourceName: unknown): string[] {
52
+ const name = String(resourceName || '').trim();
53
+ if (!name) {
54
+ return [];
55
+ }
56
+
57
+ const names = new Set<string>([name]);
58
+ if (name.includes('.')) {
59
+ names.add(name.split('.')[0]);
60
+ }
61
+ return Array.from(names);
62
+ }
63
+
64
+ function getDirtyTargetEngines(engine: FlowEngine, includePreviousEngines?: boolean): FlowEngine[] {
65
+ if (!includePreviousEngines) {
66
+ return [engine];
67
+ }
68
+
69
+ const engines: FlowEngine[] = [];
70
+ const seen = new Set<FlowEngine>();
71
+ let current: FlowEngine | undefined = engine;
72
+ let guard = 0;
73
+
74
+ while (current && guard++ < 50) {
75
+ if (!seen.has(current)) {
76
+ engines.push(current);
77
+ seen.add(current);
78
+ }
79
+ current = current.previousEngine;
80
+ }
81
+
82
+ return engines;
83
+ }
84
+
85
+ export function markDataSourceDirty(options: MarkDataSourceDirtyOptions): string[] {
86
+ const { engine, resourceName, includePreviousEngines } = options;
87
+ if (!engine?.markDataSourceDirty) {
88
+ return [];
89
+ }
90
+
91
+ const resourceNames = getAffectedResourceNames(resourceName);
92
+ if (!resourceNames.length) {
93
+ return [];
94
+ }
95
+
96
+ const dataSourceKey = String(options.dataSourceKey || 'main');
97
+ const targetEngines = getDirtyTargetEngines(engine, includePreviousEngines);
98
+ const beforeVersions = new Map<FlowEngine, Map<string, number>>();
99
+
100
+ for (const targetEngine of targetEngines) {
101
+ const versions = new Map<string, number>();
102
+ beforeVersions.set(targetEngine, versions);
103
+ for (const name of resourceNames) {
104
+ versions.set(name, targetEngine.getDataSourceDirtyVersion?.(dataSourceKey, name) || 0);
105
+ }
106
+ }
107
+
108
+ for (const targetEngine of targetEngines) {
109
+ const versions = beforeVersions.get(targetEngine);
110
+ for (const name of resourceNames) {
111
+ const before = versions?.get(name) || 0;
112
+ const current = targetEngine.getDataSourceDirtyVersion?.(dataSourceKey, name) || 0;
113
+ if (current !== before) {
114
+ continue;
115
+ }
116
+ targetEngine.markDataSourceDirty(dataSourceKey, name);
117
+ }
118
+ }
119
+
120
+ engine.emitter?.emit?.(DATA_SOURCE_DIRTY_EVENT, {
121
+ dataSourceKey,
122
+ resourceNames,
123
+ });
124
+
125
+ return resourceNames;
126
+ }