@nocobase/flow-engine 2.1.0-beta.9 → 2.1.1

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 (215) hide show
  1. package/lib/FlowContextProvider.d.ts +5 -1
  2. package/lib/FlowContextProvider.js +9 -2
  3. package/lib/components/FieldModelRenderer.js +2 -2
  4. package/lib/components/FlowModelRenderer.d.ts +3 -1
  5. package/lib/components/FlowModelRenderer.js +12 -6
  6. package/lib/components/FormItem.d.ts +6 -0
  7. package/lib/components/FormItem.js +11 -3
  8. package/lib/components/MobilePopup.js +6 -5
  9. package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
  10. package/lib/components/dnd/gridDragPlanner.js +607 -19
  11. package/lib/components/dnd/index.d.ts +31 -2
  12. package/lib/components/dnd/index.js +244 -23
  13. package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
  14. package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
  15. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
  16. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +152 -42
  17. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
  18. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
  19. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
  20. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
  21. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
  22. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
  23. package/lib/components/subModel/AddSubModelButton.js +12 -1
  24. package/lib/components/subModel/LazyDropdown.js +301 -52
  25. package/lib/components/subModel/index.d.ts +1 -0
  26. package/lib/components/subModel/index.js +19 -0
  27. package/lib/components/subModel/utils.d.ts +2 -1
  28. package/lib/components/subModel/utils.js +15 -5
  29. package/lib/components/variables/VariableHybridInput.d.ts +27 -0
  30. package/lib/components/variables/VariableHybridInput.js +499 -0
  31. package/lib/components/variables/index.d.ts +2 -0
  32. package/lib/components/variables/index.js +3 -0
  33. package/lib/data-source/index.d.ts +84 -0
  34. package/lib/data-source/index.js +269 -7
  35. package/lib/executor/FlowExecutor.js +6 -3
  36. package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
  37. package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
  38. package/lib/flow-registry/index.d.ts +1 -0
  39. package/lib/flow-registry/index.js +3 -1
  40. package/lib/flowContext.d.ts +9 -1
  41. package/lib/flowContext.js +77 -6
  42. package/lib/flowEngine.d.ts +136 -4
  43. package/lib/flowEngine.js +429 -51
  44. package/lib/flowI18n.js +2 -1
  45. package/lib/flowSettings.d.ts +14 -6
  46. package/lib/flowSettings.js +34 -6
  47. package/lib/index.d.ts +2 -0
  48. package/lib/index.js +7 -0
  49. package/lib/lazy-helper.d.ts +14 -0
  50. package/lib/lazy-helper.js +71 -0
  51. package/lib/locale/en-US.json +1 -0
  52. package/lib/locale/index.d.ts +2 -0
  53. package/lib/locale/zh-CN.json +1 -0
  54. package/lib/models/DisplayItemModel.d.ts +1 -1
  55. package/lib/models/EditableItemModel.d.ts +1 -1
  56. package/lib/models/FilterableItemModel.d.ts +1 -1
  57. package/lib/models/flowModel.d.ts +13 -10
  58. package/lib/models/flowModel.js +126 -34
  59. package/lib/provider.js +38 -23
  60. package/lib/reactive/observer.js +46 -16
  61. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
  62. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
  63. package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
  64. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
  65. package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
  66. package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
  67. package/lib/runjs-context/contexts/base.js +464 -29
  68. package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
  69. package/lib/runjs-context/contexts/elementDoc.js +152 -0
  70. package/lib/runjs-context/setup.js +1 -0
  71. package/lib/runjs-context/snippets/index.js +13 -2
  72. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
  73. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
  74. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
  75. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
  76. package/lib/types.d.ts +50 -2
  77. package/lib/types.js +1 -0
  78. package/lib/utils/createCollectionContextMeta.js +6 -2
  79. package/lib/utils/index.d.ts +3 -2
  80. package/lib/utils/index.js +7 -0
  81. package/lib/utils/loadedPageCache.d.ts +24 -0
  82. package/lib/utils/loadedPageCache.js +139 -0
  83. package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
  84. package/lib/utils/parsePathnameToViewParams.js +28 -4
  85. package/lib/utils/randomId.d.ts +39 -0
  86. package/lib/utils/randomId.js +45 -0
  87. package/lib/utils/runjsTemplateCompat.js +1 -1
  88. package/lib/utils/runjsValue.js +41 -11
  89. package/lib/utils/schema-utils.d.ts +7 -1
  90. package/lib/utils/schema-utils.js +19 -0
  91. package/lib/views/FlowView.d.ts +7 -1
  92. package/lib/views/FlowView.js +11 -1
  93. package/lib/views/PageComponent.js +8 -6
  94. package/lib/views/ViewNavigation.d.ts +12 -2
  95. package/lib/views/ViewNavigation.js +28 -9
  96. package/lib/views/createViewMeta.js +114 -50
  97. package/lib/views/inheritLayoutContext.d.ts +10 -0
  98. package/lib/views/inheritLayoutContext.js +50 -0
  99. package/lib/views/runViewBeforeClose.d.ts +10 -0
  100. package/lib/views/runViewBeforeClose.js +45 -0
  101. package/lib/views/useDialog.d.ts +2 -1
  102. package/lib/views/useDialog.js +12 -3
  103. package/lib/views/useDrawer.d.ts +2 -1
  104. package/lib/views/useDrawer.js +12 -3
  105. package/lib/views/usePage.d.ts +5 -11
  106. package/lib/views/usePage.js +304 -144
  107. package/package.json +5 -4
  108. package/src/FlowContextProvider.tsx +9 -1
  109. package/src/__tests__/createViewMeta.popup.test.ts +115 -1
  110. package/src/__tests__/flow-engine.test.ts +166 -0
  111. package/src/__tests__/flowContext.test.ts +105 -1
  112. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  113. package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
  114. package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
  115. package/src/__tests__/flowSettings.test.ts +94 -15
  116. package/src/__tests__/objectVariable.test.ts +24 -0
  117. package/src/__tests__/provider.test.tsx +24 -2
  118. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  119. package/src/__tests__/runjsContext.test.ts +21 -0
  120. package/src/__tests__/runjsContextImplementations.test.ts +9 -2
  121. package/src/__tests__/runjsContextRuntime.test.ts +2 -0
  122. package/src/__tests__/runjsLocales.test.ts +6 -5
  123. package/src/__tests__/runjsSnippets.test.ts +21 -0
  124. package/src/__tests__/viewScopedFlowEngine.test.ts +136 -3
  125. package/src/components/FieldModelRenderer.tsx +2 -1
  126. package/src/components/FlowModelRenderer.tsx +18 -6
  127. package/src/components/FormItem.tsx +7 -1
  128. package/src/components/MobilePopup.tsx +4 -2
  129. package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
  130. package/src/components/__tests__/FormItem.test.tsx +25 -0
  131. package/src/components/__tests__/dnd.test.ts +44 -0
  132. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
  133. package/src/components/__tests__/gridDragPlanner.test.ts +472 -5
  134. package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
  135. package/src/components/dnd/gridDragPlanner.ts +750 -17
  136. package/src/components/dnd/index.tsx +305 -28
  137. package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
  138. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +178 -48
  139. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
  140. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +344 -8
  141. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
  142. package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
  143. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
  144. package/src/components/subModel/AddSubModelButton.tsx +16 -2
  145. package/src/components/subModel/LazyDropdown.tsx +341 -56
  146. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +524 -38
  147. package/src/components/subModel/__tests__/utils.test.ts +24 -0
  148. package/src/components/subModel/index.ts +1 -0
  149. package/src/components/subModel/utils.ts +13 -2
  150. package/src/components/variables/VariableHybridInput.tsx +531 -0
  151. package/src/components/variables/index.ts +2 -0
  152. package/src/data-source/__tests__/collection.test.ts +41 -2
  153. package/src/data-source/__tests__/index.test.ts +69 -2
  154. package/src/data-source/index.ts +332 -8
  155. package/src/executor/FlowExecutor.ts +6 -3
  156. package/src/executor/__tests__/flowExecutor.test.ts +57 -0
  157. package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
  158. package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
  159. package/src/flow-registry/index.ts +1 -0
  160. package/src/flowContext.ts +85 -6
  161. package/src/flowEngine.ts +484 -45
  162. package/src/flowI18n.ts +2 -1
  163. package/src/flowSettings.ts +40 -6
  164. package/src/index.ts +2 -0
  165. package/src/lazy-helper.tsx +57 -0
  166. package/src/locale/en-US.json +1 -0
  167. package/src/locale/zh-CN.json +1 -0
  168. package/src/models/DisplayItemModel.tsx +1 -1
  169. package/src/models/EditableItemModel.tsx +1 -1
  170. package/src/models/FilterableItemModel.tsx +1 -1
  171. package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
  172. package/src/models/__tests__/flowModel.test.ts +65 -37
  173. package/src/models/flowModel.tsx +184 -65
  174. package/src/provider.tsx +41 -25
  175. package/src/reactive/__tests__/observer.test.tsx +82 -0
  176. package/src/reactive/observer.tsx +87 -25
  177. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
  178. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
  179. package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
  180. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
  181. package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
  182. package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
  183. package/src/runjs-context/contexts/base.ts +467 -31
  184. package/src/runjs-context/contexts/elementDoc.ts +130 -0
  185. package/src/runjs-context/setup.ts +1 -0
  186. package/src/runjs-context/snippets/index.ts +12 -1
  187. package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
  188. package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
  189. package/src/types.ts +62 -0
  190. package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
  191. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +21 -0
  192. package/src/utils/__tests__/runjsValue.test.ts +11 -0
  193. package/src/utils/__tests__/utils.test.ts +62 -0
  194. package/src/utils/createCollectionContextMeta.ts +6 -2
  195. package/src/utils/index.ts +5 -1
  196. package/src/utils/loadedPageCache.ts +147 -0
  197. package/src/utils/parsePathnameToViewParams.ts +45 -5
  198. package/src/utils/randomId.ts +48 -0
  199. package/src/utils/runjsTemplateCompat.ts +1 -1
  200. package/src/utils/runjsValue.ts +50 -11
  201. package/src/utils/schema-utils.ts +30 -1
  202. package/src/views/FlowView.tsx +22 -2
  203. package/src/views/PageComponent.tsx +7 -4
  204. package/src/views/ViewNavigation.ts +46 -9
  205. package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
  206. package/src/views/__tests__/ViewNavigation.test.ts +52 -0
  207. package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
  208. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  209. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +12 -12
  210. package/src/views/createViewMeta.ts +106 -34
  211. package/src/views/inheritLayoutContext.ts +26 -0
  212. package/src/views/runViewBeforeClose.ts +19 -0
  213. package/src/views/useDialog.tsx +13 -3
  214. package/src/views/useDrawer.tsx +13 -3
  215. package/src/views/usePage.tsx +367 -180
@@ -0,0 +1,130 @@
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 { RunJSDocPropertyDoc } from '../../flowContext';
11
+
12
+ const elementProperties: Record<string, RunJSDocPropertyDoc> = {
13
+ innerHTML: 'Sanitized inner HTML string.',
14
+ outerHTML: 'Sanitized outer HTML string.',
15
+ textContent: 'Text content.',
16
+ style: 'Inline style declaration.',
17
+ classList: 'DOMTokenList for CSS classes.',
18
+ appendChild: {
19
+ type: 'function',
20
+ description: 'Append a DOM node or text.',
21
+ detail: '(child: Node | string) => void',
22
+ completion: { insertText: `ctx.element.appendChild('text')` },
23
+ },
24
+ setAttribute: {
25
+ type: 'function',
26
+ description: 'Set an HTML attribute.',
27
+ detail: '(name: string, value: string) => void',
28
+ completion: { insertText: `ctx.element.setAttribute('data-key', 'value')` },
29
+ },
30
+ getAttribute: {
31
+ type: 'function',
32
+ description: 'Read an HTML attribute.',
33
+ detail: '(name: string) => string | null',
34
+ completion: { insertText: `ctx.element.getAttribute('data-key')` },
35
+ },
36
+ querySelector: {
37
+ type: 'function',
38
+ description: 'Find the first matching descendant.',
39
+ detail: '(selectors: string) => Element | null',
40
+ completion: { insertText: `ctx.element.querySelector('.selector')` },
41
+ },
42
+ querySelectorAll: {
43
+ type: 'function',
44
+ description: 'Find all matching descendants.',
45
+ detail: '(selectors: string) => NodeListOf<Element>',
46
+ completion: { insertText: `ctx.element.querySelectorAll('.selector')` },
47
+ },
48
+ addEventListener: {
49
+ type: 'function',
50
+ description: 'Attach an event listener to the element.',
51
+ detail: '(type: string, listener: EventListener) => void',
52
+ completion: { insertText: `ctx.element.addEventListener('click', (event) => {})` },
53
+ },
54
+ removeEventListener: {
55
+ type: 'function',
56
+ description: 'Remove an event listener from the element.',
57
+ detail: '(type: string, listener: EventListener) => void',
58
+ completion: { insertText: `ctx.element.removeEventListener('click', handler)` },
59
+ },
60
+ };
61
+
62
+ const zhCNElementProperties: Record<string, RunJSDocPropertyDoc> = {
63
+ innerHTML: '已消毒的 innerHTML 字符串。',
64
+ outerHTML: '已消毒的 outerHTML 字符串。',
65
+ textContent: '文本内容。',
66
+ style: '内联样式声明。',
67
+ classList: 'CSS class 列表。',
68
+ appendChild: {
69
+ type: 'function',
70
+ description: '追加 DOM 节点或文本。',
71
+ detail: '(child: Node | string) => void',
72
+ completion: { insertText: `ctx.element.appendChild('text')` },
73
+ },
74
+ setAttribute: {
75
+ type: 'function',
76
+ description: '设置 HTML 属性。',
77
+ detail: '(name: string, value: string) => void',
78
+ completion: { insertText: `ctx.element.setAttribute('data-key', 'value')` },
79
+ },
80
+ getAttribute: {
81
+ type: 'function',
82
+ description: '读取 HTML 属性。',
83
+ detail: '(name: string) => string | null',
84
+ completion: { insertText: `ctx.element.getAttribute('data-key')` },
85
+ },
86
+ querySelector: {
87
+ type: 'function',
88
+ description: '查询第一个匹配的后代元素。',
89
+ detail: '(selectors: string) => Element | null',
90
+ completion: { insertText: `ctx.element.querySelector('.selector')` },
91
+ },
92
+ querySelectorAll: {
93
+ type: 'function',
94
+ description: '查询所有匹配的后代元素。',
95
+ detail: '(selectors: string) => NodeListOf<Element>',
96
+ completion: { insertText: `ctx.element.querySelectorAll('.selector')` },
97
+ },
98
+ addEventListener: {
99
+ type: 'function',
100
+ description: '给元素添加事件监听。',
101
+ detail: '(type: string, listener: EventListener) => void',
102
+ completion: { insertText: `ctx.element.addEventListener('click', (event) => {})` },
103
+ },
104
+ removeEventListener: {
105
+ type: 'function',
106
+ description: '移除元素事件监听。',
107
+ detail: '(type: string, listener: EventListener) => void',
108
+ completion: { insertText: `ctx.element.removeEventListener('click', handler)` },
109
+ },
110
+ };
111
+
112
+ export function createElementPropertyDoc(
113
+ description = 'Current DOM container for this RunJS context. Usually an ElementProxy.',
114
+ ): RunJSDocPropertyDoc {
115
+ return {
116
+ description,
117
+ detail: 'HTMLElement | ElementProxy',
118
+ properties: elementProperties,
119
+ };
120
+ }
121
+
122
+ export function createZhCNElementPropertyDoc(
123
+ description = '当前 RunJS 上下文的 DOM 容器,通常为 ElementProxy。',
124
+ ): RunJSDocPropertyDoc {
125
+ return {
126
+ description,
127
+ detail: 'HTMLElement | ElementProxy',
128
+ properties: zhCNElementProperties,
129
+ };
130
+ }
@@ -47,6 +47,7 @@ export async function setupRunJSContexts() {
47
47
  RunJSContextRegistry.register(version, 'JSFieldModel', JSFieldRunJSContext, { scenes: ['detail'] });
48
48
  RunJSContextRegistry.register(version, 'JSEditableFieldModel', JSEditableFieldRunJSContext, { scenes: ['form'] });
49
49
  RunJSContextRegistry.register(version, 'JSItemModel', JSItemRunJSContext, { scenes: ['form'] });
50
+ RunJSContextRegistry.register(version, 'JSItemActionModel', JSItemRunJSContext, { scenes: ['table'] });
50
51
  RunJSContextRegistry.register(version, 'JSColumnModel', JSColumnRunJSContext, { scenes: ['table'] });
51
52
  RunJSContextRegistry.register(version, 'FormJSFieldItemModel', FormJSFieldItemRunJSContext, { scenes: ['form'] });
52
53
  RunJSContextRegistry.register(version, 'JSRecordActionModel', JSRecordActionRunJSContext, { scenes: ['table'] });
@@ -49,6 +49,7 @@ const snippets: Record<string, RunJSSnippetLoader | undefined> = {
49
49
  'scene/detail/status-tag': () => import('./scene/detail/status-tag.snippet'),
50
50
  'scene/detail/relative-time': () => import('./scene/detail/relative-time.snippet'),
51
51
  'scene/detail/percentage-bar': () => import('./scene/detail/percentage-bar.snippet'),
52
+ 'scene/detail/set-field-style': () => import('./scene/detail/set-field-style.snippet'),
52
53
  // scene/form
53
54
  'scene/form/render-basic': () => import('./scene/form/render-basic.snippet'),
54
55
  'scene/form/set-field-value': () => import('./scene/form/set-field-value.snippet'),
@@ -67,6 +68,7 @@ const snippets: Record<string, RunJSSnippetLoader | undefined> = {
67
68
  'scene/table/iterate-selected-rows': () => import('./scene/table/iterate-selected-rows.snippet'),
68
69
  'scene/table/destroy-selected': () => import('./scene/table/destroy-selected.snippet'),
69
70
  'scene/table/export-selected-json': () => import('./scene/table/export-selected-json.snippet'),
71
+ 'scene/table/set-cell-style': () => import('./scene/table/set-cell-style.snippet'),
70
72
  };
71
73
 
72
74
  export default snippets;
@@ -125,10 +127,19 @@ function normalizeScenes(def: any, key: string): string[] {
125
127
  return [];
126
128
  }
127
129
 
130
+ function normalizeSceneGroup(scene: string): string {
131
+ const mapping: Record<string, string> = {
132
+ detailFieldEvent: 'detail',
133
+ tableFieldEvent: 'table',
134
+ formFieldEvent: 'form',
135
+ };
136
+ return mapping[scene] || scene;
137
+ }
138
+
128
139
  function computeGroups(def: any, key: string): string[] {
129
140
  const scenes = normalizeScenes(def, key);
130
141
  if (scenes.length) {
131
- return scenes.map((scene) => `scene/${scene}`);
142
+ return Array.from(new Set(scenes.map((scene) => `scene/${normalizeSceneGroup(scene)}`)));
132
143
  }
133
144
  const parts = key.split('/');
134
145
  if (!parts.length) return [];
@@ -0,0 +1,30 @@
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 { SnippetModule } from '../../types';
11
+ const snippet: SnippetModule = {
12
+ contexts: ['*'],
13
+ scenes: ['detailFieldEvent', 'formFieldEvent'],
14
+ prefix: 'sn-item-style',
15
+ label: 'Set form item/details item style',
16
+ description: 'Customize form item and details item container styles',
17
+ locales: {
18
+ 'zh-CN': {
19
+ label: '设置表单项/详情项样式',
20
+ description: '自定义表单项和详情项容器样式',
21
+ },
22
+ },
23
+ content: `
24
+ ctx.model.props.style = {
25
+ background: 'red',
26
+ };
27
+ `,
28
+ };
29
+
30
+ export default snippet;
@@ -0,0 +1,34 @@
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 { SnippetModule } from '../../types';
11
+ const snippet: SnippetModule = {
12
+ contexts: ['*'],
13
+ scenes: ['tableFieldEvent'],
14
+ prefix: 'sn-table-cell-style',
15
+ label: 'Set table cell style',
16
+ description: 'Customize table field cell styles with onCell',
17
+ locales: {
18
+ 'zh-CN': {
19
+ label: '表格字段样式设置',
20
+ description: '通过 onCell 自定义表格字段单元格样式',
21
+ },
22
+ },
23
+ content: `
24
+ ctx.model.props.onCell = (record, rowIndex) => {
25
+ return {
26
+ style: {
27
+ background: 'red',
28
+ },
29
+ };
30
+ };
31
+ `,
32
+ };
33
+
34
+ export default snippet;
package/src/types.ts CHANGED
@@ -145,6 +145,8 @@ export enum ActionScene {
145
145
  ACTION_LINKAGE_RULES,
146
146
  /** 动态事件流可用 */
147
147
  DYNAMIC_EVENT_FLOW,
148
+ /** 菜单项联动规则可用 */
149
+ MENU_LINKAGE_RULES,
148
150
  }
149
151
 
150
152
  /**
@@ -213,6 +215,7 @@ export interface ActionDefinition<TModel extends FlowModel = FlowModel, TCtx ext
213
215
  */
214
216
  export type FlowEventName =
215
217
  | 'click'
218
+ | 'close'
216
219
  | 'submit'
217
220
  | 'reset'
218
221
  | 'remove'
@@ -387,6 +390,65 @@ export interface CreateModelOptions {
387
390
  delegateToParent?: boolean;
388
391
  [key: string]: any; // 允许额外的自定义选项
389
392
  }
393
+
394
+ /**
395
+ * FlowModel loader result.
396
+ * Supports returning the model constructor directly, a default export, or a module object containing the named export.
397
+ */
398
+ export type FlowModelLoaderResult =
399
+ | ModelConstructor
400
+ | {
401
+ default?: ModelConstructor;
402
+ [key: string]: unknown;
403
+ }
404
+ | Record<string, unknown>;
405
+
406
+ /**
407
+ * FlowModel loader function.
408
+ */
409
+ export type FlowModelLoader = () => Promise<FlowModelLoaderResult>;
410
+
411
+ /**
412
+ * FlowModel loader entry (normalized internal form).
413
+ */
414
+ export interface FlowModelLoaderEntry {
415
+ loader: FlowModelLoader;
416
+ extends?: string[];
417
+ // meta?: Partial<FlowModelMeta>;
418
+ // scenes?: string[];
419
+ }
420
+
421
+ /**
422
+ * FlowModel loader input (user-facing form for registerModelLoaders).
423
+ * The `extends` field accepts flexible formats that will be normalized to `string[]` at registration time.
424
+ */
425
+ export interface FlowModelLoaderInput {
426
+ loader: FlowModelLoader;
427
+ extends?: string | ModelConstructor | (string | ModelConstructor)[];
428
+ }
429
+
430
+ /**
431
+ * FlowModel loader entry map (normalized internal form).
432
+ */
433
+ export type FlowModelLoaderMap = Record<string, FlowModelLoaderEntry>;
434
+
435
+ /**
436
+ * FlowModel loader input map (user-facing form for registerModelLoaders).
437
+ */
438
+ export type FlowModelLoaderInputMap = Record<string, FlowModelLoaderInput>;
439
+
440
+ /**
441
+ * Batch ensure result.
442
+ */
443
+ export interface EnsureBatchResult {
444
+ requested: string[];
445
+ loaded: string[];
446
+ failed: Array<{
447
+ name: string;
448
+ error?: unknown;
449
+ }>;
450
+ }
451
+
390
452
  export interface IFlowModelRepository<T extends FlowModel = FlowModel> {
391
453
  findOne(query: Record<string, any>): Promise<Record<string, any> | null>;
392
454
  save(model: T, options?: { onlyStepParams?: boolean }): Promise<Record<string, any>>;
@@ -26,6 +26,7 @@ describe('createCollectionContextMeta', () => {
26
26
  { name: 'id', type: 'integer', interface: 'number', filterable: true },
27
27
  { name: 'email', type: 'string', interface: 'text', filterable: true },
28
28
  { name: 'nickname', type: 'string', interface: 'text' }, // 未声明 filterable
29
+ { name: 'rawUserPayload', type: 'json', filterable: true },
29
30
  ],
30
31
  });
31
32
 
@@ -34,6 +35,7 @@ describe('createCollectionContextMeta', () => {
34
35
  fields: [
35
36
  { name: 'title', type: 'string', interface: 'text', filterable: true },
36
37
  { name: 'author', type: 'belongsTo', target: 'users', interface: 'm2o', filterable: true },
38
+ { name: 'rawPostPayload', type: 'json', filterable: true },
37
39
  ],
38
40
  });
39
41
 
@@ -44,8 +46,54 @@ describe('createCollectionContextMeta', () => {
44
46
  const authorMeta: any = props?.author;
45
47
  const authorFields = await authorMeta?.properties?.();
46
48
 
49
+ expect(props).toHaveProperty('title');
50
+ expect(props).toHaveProperty('author');
51
+ expect(props).not.toHaveProperty('rawPostPayload');
47
52
  expect(authorFields).toBeTruthy();
48
53
  expect(authorFields).toHaveProperty('email');
49
54
  expect(authorFields).not.toHaveProperty('nickname');
55
+ expect(authorFields).not.toHaveProperty('rawUserPayload');
56
+ });
57
+
58
+ it('keeps interfaced non-filterable fields but hides fields without interface when includeNonFilterable is true', async () => {
59
+ const engine = new FlowEngine();
60
+ const dm = engine.dataSourceManager as any;
61
+ dm.collectionFieldInterfaceManager = new CollectionFieldInterfaceManager([], {}, dm);
62
+ engine.context.defineProperty('app', { value: { dataSourceManager: dm } });
63
+ const ds = dm.getDataSource('main')!;
64
+
65
+ ds.addCollection({
66
+ name: 'users',
67
+ fields: [
68
+ { name: 'id', type: 'integer', interface: 'number', filterable: true },
69
+ { name: 'email', type: 'string', interface: 'text', filterable: true },
70
+ { name: 'nickname', type: 'string', interface: 'text' },
71
+ { name: 'rawUserPayload', type: 'json', filterable: true },
72
+ ],
73
+ });
74
+
75
+ ds.addCollection({
76
+ name: 'posts',
77
+ fields: [
78
+ { name: 'title', type: 'string', interface: 'text', filterable: true },
79
+ { name: 'internalName', type: 'string', interface: 'text' },
80
+ { name: 'rawPostPayload', type: 'json', filterable: true },
81
+ { name: 'author', type: 'belongsTo', target: 'users', interface: 'm2o', filterable: true },
82
+ ],
83
+ });
84
+
85
+ const posts = ds.getCollection('posts')!;
86
+ const metaFactory = createCollectionContextMeta(posts, 'Posts', true);
87
+ const meta = await metaFactory();
88
+ const props = await (meta?.properties as any)?.();
89
+ const authorFields = await props?.author?.properties?.();
90
+
91
+ expect(props).toHaveProperty('title');
92
+ expect(props).toHaveProperty('internalName');
93
+ expect(props).toHaveProperty('author');
94
+ expect(props).not.toHaveProperty('rawPostPayload');
95
+ expect(authorFields).toHaveProperty('email');
96
+ expect(authorFields).toHaveProperty('nickname');
97
+ expect(authorFields).not.toHaveProperty('rawUserPayload');
50
98
  });
51
99
  });
@@ -102,6 +102,27 @@ describe('parsePathnameToViewParams', () => {
102
102
  expect(result).toEqual([{ viewUid: 'xxx', tabUid: 'yyy' }]);
103
103
  });
104
104
 
105
+ test('should parse custom root prefix', () => {
106
+ const result = parsePathnameToViewParams('/embed/xxx/tab/yyy/view/zzz', { rootPrefix: 'embed' });
107
+ expect(result).toEqual([{ viewUid: 'xxx', tabUid: 'yyy' }, { viewUid: 'zzz' }]);
108
+ });
109
+
110
+ test('should parse pathname by basePath', () => {
111
+ const result = parsePathnameToViewParams('/embed/xxx/tab/yyy/view/zzz', { basePath: '/embed' });
112
+ expect(result).toEqual([{ viewUid: 'xxx', tabUid: 'yyy' }, { viewUid: 'zzz' }]);
113
+ });
114
+
115
+ test('should parse pathname by nested basePath', () => {
116
+ const result = parsePathnameToViewParams('/admin/settings/public-forms/xxx/view/zzz', {
117
+ basePath: '/admin/settings/public-forms',
118
+ });
119
+ expect(result).toEqual([{ viewUid: 'xxx' }, { viewUid: 'zzz' }]);
120
+ });
121
+
122
+ test('should keep admin as default root prefix', () => {
123
+ expect(parsePathnameToViewParams('/embed/xxx')).toEqual([]);
124
+ });
125
+
105
126
  test('should parse filterByTk from key-value encoded segment into object', () => {
106
127
  const kv = encodeURIComponent('id=1&tenant=ac');
107
128
  const path = `/admin/xxx/filterbytk/${kv}`;
@@ -41,4 +41,15 @@ describe('runjsValue utils', () => {
41
41
  expect(out.someVar).toContain('');
42
42
  expect(out.user).toContain('name');
43
43
  });
44
+
45
+ it('extractUsedVariablePathsFromRunJS: extracts ctx.getVar string paths', () => {
46
+ const code = `
47
+ const phone = await ctx.getVar('ctx.item.value.phone');
48
+ const assignee = await ctx.getVar("ctx.user.profile.name");
49
+ return [phone, assignee];
50
+ `;
51
+ const out = extractUsedVariablePathsFromRunJS(code);
52
+ expect(out.item).toContain('value.phone');
53
+ expect(out.user).toContain('profile.name');
54
+ });
44
55
  });
@@ -12,6 +12,7 @@ import {
12
12
  getT,
13
13
  isInheritedFrom,
14
14
  resolveDefaultParams,
15
+ shouldHideEventInSettings,
15
16
  resolveStepUiSchema,
16
17
  resolveStepDisabledInSettings,
17
18
  shouldHideStepInSettings,
@@ -27,6 +28,7 @@ import type {
27
28
  FlowDefinitionOptions,
28
29
  ActionDefinition,
29
30
  DeepPartial,
31
+ EventDefinition,
30
32
  ModelConstructor,
31
33
  StepParams,
32
34
  StepDefinition,
@@ -1002,6 +1004,66 @@ describe('Utils', () => {
1002
1004
  });
1003
1005
  });
1004
1006
 
1007
+ // ==================== shouldHideEventInSettings() FUNCTION ====================
1008
+ describe('shouldHideEventInSettings()', () => {
1009
+ let mockFlow: any;
1010
+ let mockEvent: EventDefinition;
1011
+
1012
+ beforeEach(() => {
1013
+ mockFlow = {
1014
+ key: 'testFlow',
1015
+ title: 'Test Flow',
1016
+ steps: {},
1017
+ };
1018
+
1019
+ mockEvent = {
1020
+ name: 'close',
1021
+ title: 'Close',
1022
+ handler: vi.fn(),
1023
+ };
1024
+ });
1025
+
1026
+ test('returns true for static hideInSettings=true', async () => {
1027
+ mockEvent.hideInSettings = true;
1028
+
1029
+ const result = await shouldHideEventInSettings(mockModel, mockFlow, mockEvent);
1030
+
1031
+ expect(result).toBe(true);
1032
+ });
1033
+
1034
+ test('returns false for static hideInSettings=false', async () => {
1035
+ mockEvent.hideInSettings = false;
1036
+
1037
+ const result = await shouldHideEventInSettings(mockModel, mockFlow, mockEvent);
1038
+
1039
+ expect(result).toBe(false);
1040
+ });
1041
+
1042
+ test('evaluates function hideInSettings with FlowRuntimeContext and can read ctx.view.preventClose', async () => {
1043
+ mockModel.context.defineProperty('view', { value: { preventClose: true } });
1044
+ const hideFn = vi.fn().mockImplementation((ctx) => !!ctx.view?.preventClose);
1045
+ mockEvent.hideInSettings = hideFn as any;
1046
+
1047
+ const result = await shouldHideEventInSettings(mockModel, mockFlow, mockEvent);
1048
+
1049
+ expect(hideFn).toHaveBeenCalledTimes(1);
1050
+ const ctx = hideFn.mock.calls[0][0] as FlowRuntimeContext;
1051
+ expect(ctx).toBeInstanceOf(FlowRuntimeContext);
1052
+ expect(result).toBe(true);
1053
+ });
1054
+
1055
+ test('returns false and logs warning when event hideInSettings throws', async () => {
1056
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1057
+ mockEvent.hideInSettings = vi.fn().mockRejectedValue(new Error('boom')) as any;
1058
+
1059
+ const result = await shouldHideEventInSettings(mockModel, mockFlow, mockEvent);
1060
+
1061
+ expect(consoleSpy).toHaveBeenCalled();
1062
+ expect(result).toBe(false);
1063
+ consoleSpy.mockRestore();
1064
+ });
1065
+ });
1066
+
1005
1067
  // ==================== shouldHideStepInSettings() FUNCTION ====================
1006
1068
  describe('shouldHideStepInSettings()', () => {
1007
1069
  let mockFlow: any;
@@ -14,6 +14,10 @@ import type { PropertyMetaFactory } from '../flowContext';
14
14
  const RELATION_FIELD_TYPES = ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany', 'belongsToArray'] as const;
15
15
  const NUMERIC_FIELD_TYPES = ['integer', 'float', 'double', 'decimal'] as const;
16
16
 
17
+ function shouldShowFieldInMeta(field: CollectionField, includeNonFilterable?: boolean) {
18
+ return Boolean(field.interface && (includeNonFilterable || field.filterable));
19
+ }
20
+
17
21
  /**
18
22
  * 创建字段的完整元数据(统一处理关联和非关联字段)
19
23
  */
@@ -36,7 +40,7 @@ function createFieldMetadata(field: CollectionField, includeNonFilterable?: bool
36
40
  properties: async () => {
37
41
  const subProperties: Record<string, any> = {};
38
42
  targetCollection.fields.forEach((subField) => {
39
- if (includeNonFilterable || subField.filterable) {
43
+ if (shouldShowFieldInMeta(subField, includeNonFilterable)) {
40
44
  subProperties[subField.name] = createFieldMetadata(subField, includeNonFilterable);
41
45
  }
42
46
  });
@@ -114,7 +118,7 @@ export function createCollectionContextMeta(
114
118
 
115
119
  // 添加所有字段
116
120
  collection.fields.forEach((field) => {
117
- if (includeNonFilterable || field.filterable) {
121
+ if (shouldShowFieldInMeta(field, includeNonFilterable)) {
118
122
  properties[field.name] = createFieldMetadata(field, includeNonFilterable);
119
123
  }
120
124
  });
@@ -22,7 +22,7 @@ export {
22
22
  export { escapeT, getT, tExpr } from './translation';
23
23
 
24
24
  // 异常类
25
- export { FlowCancelSaveException, FlowExitException } from './exceptions';
25
+ export { FlowCancelSaveException, FlowExitAllException, FlowExitException } from './exceptions';
26
26
 
27
27
  // 流程定义相关
28
28
  export { defineAction } from './flow-definitions';
@@ -39,6 +39,7 @@ export {
39
39
  resolveStepUiSchema,
40
40
  resolveStepDisabledInSettings,
41
41
  resolveUiMode,
42
+ shouldHideEventInSettings,
42
43
  shouldHideStepInSettings,
43
44
  } from './schema-utils';
44
45
 
@@ -103,3 +104,6 @@ export { isBeforeRenderFlow } from './flows';
103
104
 
104
105
  // Module URL resolver
105
106
  export { resolveModuleUrl, isCssFile } from './resolveModuleUrl';
107
+
108
+ // Random base36 identifier with optional semantic prefix
109
+ export { randomId } from './randomId';