@steedos-labs/plugin-workflow 3.0.39 → 3.0.41

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 (39) hide show
  1. package/designer/dist/amis-renderer/amis-renderer.css +1 -1
  2. package/designer/dist/amis-renderer/amis-renderer.js +1 -1
  3. package/designer/dist/assets/index-7cMOmCg4.css +1 -0
  4. package/designer/dist/assets/index-DbYFInYv.js +943 -0
  5. package/designer/dist/index.html +2 -2
  6. package/main/default/applications/approve_workflow.app.yml +3 -160
  7. package/main/default/applications/desktop.app.yml +21 -0
  8. package/main/default/client/socket.client.js +2 -5
  9. package/main/default/manager/handlers_manager.js +5 -2
  10. package/main/default/manager/instance_number_rules.js +1 -1
  11. package/main/default/manager/uuflow_manager.js +20 -2
  12. package/main/default/objects/instance_tasks/listviews/inbox.listview.yml +1 -1
  13. package/main/default/objects/instance_tasks/listviews/outbox.listview.yml +1 -1
  14. package/main/default/objects/instances/buttons/instance_delete.button.yml +7 -1
  15. package/main/default/objects/instances/buttons/instance_new.button.yml +2 -2
  16. package/main/default/objects/instances/listviews/completed.listview.yml +1 -1
  17. package/main/default/objects/instances/listviews/draft.listview.yml +1 -1
  18. package/main/default/objects/instances/listviews/monitor.listview.yml +1 -1
  19. package/main/default/objects/instances/listviews/pending.listview.yml +1 -1
  20. package/main/default/pages/flow_selector.page.amis.json +2 -2
  21. package/main/default/pages/flow_selector_mobile.page.amis.json +2 -2
  22. package/main/default/pages/page_instance_print.page.amis.json +11 -1
  23. package/main/default/routes/am.router.js +3 -1
  24. package/main/default/routes/api_auto_number.router.js +166 -23
  25. package/main/default/routes/api_workflow_ai_form_design.router.js +116 -16
  26. package/main/default/routes/api_workflow_ai_form_design_stream.router.js +115 -17
  27. package/main/default/routes/api_workflow_box_filter.router.js +2 -2
  28. package/main/default/routes/api_workflow_nav.router.js +1 -0
  29. package/main/default/services/flows.service.js +20 -24
  30. package/main/default/services/instance.service.js +6 -4
  31. package/main/default/test/test_badge_draft.js +12 -26
  32. package/main/default/test/test_badge_update.js +10 -54
  33. package/package.json +1 -1
  34. package/package.service.js +14 -0
  35. package/public/amis-renderer/amis-renderer.css +1 -1
  36. package/public/amis-renderer/amis-renderer.js +1 -1
  37. package/public/workflow/index.css +10 -279
  38. package/designer/dist/assets/index-CxYuhf9v.js +0 -757
  39. package/designer/dist/assets/index-Dve-EwQO.css +0 -1
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/api/workflow/designer-v2/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>designer</title>
8
- <script type="module" crossorigin src="/api/workflow/designer-v2/assets/index-CxYuhf9v.js"></script>
9
- <link rel="stylesheet" crossorigin href="/api/workflow/designer-v2/assets/index-Dve-EwQO.css">
8
+ <script type="module" crossorigin src="/api/workflow/designer-v2/assets/index-DbYFInYv.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/api/workflow/designer-v2/assets/index-7cMOmCg4.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
@@ -23,166 +23,9 @@ nav_schema: {
23
23
  "ignoreNavSchema": true
24
24
  },
25
25
  {
26
- "type": "wrapper",
27
- "size": "none",
28
- "className": "instances-sidebar-wrapper mt-1 bg-white",
29
- "body": [
30
- {
31
- "type": "button",
32
- "label": "刷新",
33
- "className": "instance-nav-reload hidden",
34
- "onEvent": {
35
- "click": {
36
- "actions": [
37
- {
38
- "actionType": "reload",
39
- "componentId": "u:instanceNav"
40
- }
41
- ]
42
- }
43
- }
44
- },
45
- {
46
- "type": "button",
47
- "label": "刷新树",
48
- "className": "instance-tree-reload hidden",
49
- "onEvent": {
50
- "click": {
51
- "actions": [
52
- {
53
- "actionType": "custom",
54
- "script": "try{ var opts = window.__latestNavOptions; var data = window.__latestNavData; if(!Array.isArray(opts)){ console.log('[tree-reload] no global options'); return; } opts = JSON.parse(JSON.stringify(opts)); var tree = window.__navTreeComponent; if(!tree || !tree.props || !tree.props.formItem){ console.log('[tree-reload] no saved tree ref, trying amisScoped'); var s = window.amisScoped || (document.querySelector('[data-amisScoped]') && document.querySelector('[data-amisScoped]').__amisScoped); if(s){ tree = s.getComponentById('u:instanceNavTree'); } } if(tree && tree.props && tree.props.formItem){ tree.props.formItem.setOptions(opts, undefined, data || {}); console.log('[tree-reload] setOptions done, count:', opts.length); if(tree.forceUpdate){ tree.forceUpdate(); console.log('[tree-reload] forceUpdate called'); } } else { console.log('[tree-reload] FAIL: cannot find tree component'); } }catch(e){ console.warn('[tree-reload error]', e); }"
55
- }
56
- ]
57
- }
58
- }
59
- },
60
- {
61
- "type": "service",
62
- "id": "u:instanceNav",
63
- "className": "bg-white",
64
- "onEvent": {
65
- "@data.changed.instances": {
66
- "actions": [
67
- {
68
- "actionType": "reload"
69
- }
70
- ]
71
- },
72
- "fetchInited": {
73
- "actions": [
74
- {
75
- "actionType": "custom",
76
- "script": "try{ var s = event.context && event.context.scoped; if(!s){ var amisScoped = window.amisScoped || (document.querySelector('[data-amisScoped]') && document.querySelector('[data-amisScoped]').__amisScoped); if(amisScoped){ s = amisScoped; } } if(s){ var tree = s.getComponentById('u:instanceNavTree'); if(tree && tree.props && tree.props.formItem){ window.__navTreeComponent = tree; var opts = event.data && event.data.options; if(Array.isArray(opts)){ opts = JSON.parse(JSON.stringify(opts)); tree.props.formItem.setOptions(opts, undefined, event.data); if(tree.forceUpdate){ setTimeout(function(){ tree.forceUpdate(); }, 100); } } } } var _navComp = s && s.getComponentById('u:instanceNav'); if(_navComp && _navComp.props && _navComp.props.store){ window.__instanceNavStore = _navComp.props.store; } if(!window._instanceNavRouteListenerAdded){ window._instanceNavRouteListenerAdded = true; window._wasOnApproveWorkflow = true; window.addEventListener('message', function(evt){ if(!evt.data || !evt.data.type) return; if(evt.data.type === 'ROUTE_CHANGE'){ var isOnApprove = window.location.pathname.indexOf('/app/approve_workflow') === 0; var wasOn = window._wasOnApproveWorkflow; window._wasOnApproveWorkflow = isOnApprove; if(!isOnApprove) return; var isPop = evt.data.navigationType === 'POP'; if(wasOn && !isPop) return; clearTimeout(window._instanceNavReloadTimer); window._instanceNavReloadTimer = setTimeout(function(){ var _val = window.location.pathname + decodeURIComponent(window.location.search); _val = _val.replace(new RegExp('/view/(?!none)[^?#/]+'), '/view/none'); var opts = window.__latestNavOptions; if(opts){ var _fn = function(o,v){ for(var i=0;i<o.length;i++){ if(o[i].value===v) return v; if(o[i].children){ var f=_fn(o[i].children,v); if(f) return f; } } return null; }; var _fl = function(o,sid,sobj){ for(var i=0;i<o.length;i++){ var v=o[i].value; if(v){ var m1=v.match(/side_listview_id=([^&]*)/); var m2=v.match(/side_object=([^&]*)/); if(m1&&m2&&m1[1]===sid&&m2[1]===sobj) return v; } if(o[i].children){ var f=_fl(o[i].children,sid,sobj); if(f) return f; } } return null; }; if(!_fn(opts,_val)){ var lm=_val.match(/side_listview_id=([^&]*)/); var om=_val.match(/side_object=([^&]*)/); if(lm&&om){ var pm=_fl(opts,lm[1],om[1]); if(pm) _val=pm; } } } var store = window.__instanceNavStore; if(store && store.updateData){ store.updateData({ value: _val, _ts: Date.now() }, undefined, false); } var _tree = window.__navTreeComponent; if(_tree && _tree.props && _tree.props.setPrinstineValue){ _tree.props.setPrinstineValue(_val); } }, 300); } }); } }catch(e){ console.warn('[fetchInited error]', e); }"
77
- }
78
- ]
79
- }
80
- },
81
- "body": [
82
- {
83
- "type": "input-tree",
84
- "name": "instanceNavTree",
85
- "treeContainerClassName": "h-full bg-white",
86
- "className": "instance-box-tree h-full w-full p-0 bg-white",
87
- "id": "u:instanceNavTree",
88
- "stacked": true,
89
- "multiple": false,
90
- "enableNodePath": false,
91
- "hideRoot": true,
92
- "showIcon": true,
93
- "initiallyOpen": false,
94
- "virtualThreshold": 100000,
95
- "value": "${value}",
96
- "size": "md",
97
- "onEvent": {
98
- "change": {
99
- "actions": [
100
- {
101
- "actionType": "setValue",
102
- "componentId": "instances_list_service",
103
- "args": {
104
- "value": {
105
- "isFlowDataDone": false
106
- }
107
- }
108
- },
109
- {
110
- "actionType": "custom",
111
- "script": "//获取上一次的flowId和categoryId\nconst lastFlowId = event.data.flowId;\nconst lastCategoryId = event.data.categoryId;\n//从value中获取最新的flowId\nvar flowIdRegex = /&flowId=([^&]+)/;\nvar flowIdMatch = event.data.value.match(flowIdRegex);\nconst flowId = flowIdMatch && flowIdMatch.length > 0 ? flowIdMatch[1] : \"\";\n//从value中获取最新的categoryId\nvar categoryIdRegex = /&categoryId=([^&]+)/;\nvar categoryIdMatch = event.data.value.match(categoryIdRegex);\nconst categoryId = categoryIdMatch && categoryIdMatch.length > 0 ? categoryIdMatch[1] : \"\";\n//获取上一次的listname和最新的listname\nconst lastListName = event.data.listName;\nconst listName = event.data.value.split('?')[0].split('instances/grid/')[1];\n//切换流程时清除过滤条件\nif (lastListName == \"monitor\" && listName == \"monitor\" && (flowId != lastFlowId || categoryId != lastCategoryId)) {\n listViewPropsStoreKey = window.location.pathname + \"/crud\";\n sessionStorage.removeItem(listViewPropsStoreKey);\n sessionStorage.removeItem(listViewPropsStoreKey + \"/query\");\n}"
112
- },
113
- {
114
- "actionType": "setValue",
115
- "componentId": "instances_list_service",
116
- "args": {
117
- "value": {
118
- "additionalFilters": [
119
- "${event.data.options.name}",
120
- "=",
121
- "${event.data.options.value}"
122
- ]
123
- }
124
- },
125
- "expression": "${event.data.options.level>=10}"
126
- },
127
- {
128
- "args": {
129
- "link": "${event.data.value}",
130
- "blank": false
131
- },
132
- "actionType": "link"
133
- }
134
- ]
135
- }
136
- },
137
- "menuTpl": {
138
- "type": "wrapper",
139
- "className": "flex items-center py-1.5 px-3 m-0 rounded-md transition-colors duration-150",
140
- "body": [
141
- {
142
- "type": "tpl",
143
- "className": "flex-1 leading-6 truncate instance-menu-label",
144
- "tpl": "${label}",
145
- "id": "u:9dee51f00db4"
146
- },
147
- {
148
- "type": "tpl",
149
- "className": "ml-auto",
150
- "tpl": "",
151
- "badge": {
152
- "className": "h-0",
153
- "offset": [
154
- -5,
155
- 0
156
- ],
157
- "mode": "text",
158
- "text": "${tag | toInt}",
159
- "overflowCount": 999
160
- },
161
- "id": "u:2329cd1fecc2"
162
- }
163
- ],
164
- "id": "u:545154bcc334"
165
- },
166
- "unfoldedLevel": 2,
167
- "source": "${options}"
168
- }
169
- ],
170
- "api": {
171
- "method": "get",
172
- "url": "${context.rootUrl}/api/${appId}/workflow/nav",
173
- "headers": {
174
- "Authorization": "Bearer ${context.tenantId},${context.authToken}"
175
- },
176
- "trackExpression": "false",
177
- "messages": {},
178
- "adaptor": "var _val = window.location.pathname + decodeURIComponent(window.location.search); _val = _val.replace(new RegExp('/view/(?!none)[^?#/]+'), '/view/none'); console.log('[adaptor] computed value:', _val); function _findNode(opts, val){ for(var i=0;i<opts.length;i++){ if(opts[i].value===val) return opts[i].value; if(opts[i].children){ var f=_findNode(opts[i].children,val); if(f) return f; } } return null; } function _findByListview(opts, sid, sobj){ for(var i=0;i<opts.length;i++){ var v=opts[i].value; if(v){ var m1=v.match(/side_listview_id=([^&]*)/); var m2=v.match(/side_object=([^&]*)/); if(m1&&m2&&m1[1]===sid&&m2[1]===sobj){ return v; } } if(opts[i].children){ var f=_findByListview(opts[i].children,sid,sobj); if(f) return f; } } return null; } if(payload.data.options){ var exactMatch=_findNode(payload.data.options, _val); if(!exactMatch){ console.log('[adaptor] no exact match, trying partial match'); var lm=_val.match(/side_listview_id=([^&]*)/); var om=_val.match(/side_object=([^&]*)/); if(lm&&om){ var partialMatch=_findByListview(payload.data.options, lm[1], om[1]); if(partialMatch){ console.log('[adaptor] partial match found:', partialMatch); _val=partialMatch; } else { console.log('[adaptor] no partial match either, listview='+lm[1]+' object='+om[1]); } } } else { console.log('[adaptor] exact match found'); } } payload.data.value = _val; payload.data._ts = Date.now(); if(payload.data.options){ payload.data.options = JSON.parse(JSON.stringify(payload.data.options)); window.__latestNavOptions = payload.data.options; window.__latestNavData = payload.data; console.log('[adaptor] saved options to global, count:', payload.data.options.length); } return payload;"
179
- },
180
- "messages": {},
181
- "dsType": "api"
182
- }
183
- ],
184
- "isFixedHeight": false,
185
- "isFixedWidth": false
26
+ "type": "approval-tree-menu",
27
+ "apiUrl": "/api/approve_workflow/workflow/nav",
28
+ "navigateMode": "router"
186
29
  }
187
30
  ],
188
31
  "isFixedHeight": false,
@@ -0,0 +1,21 @@
1
+ code: desktop
2
+ enable_nav_schema: true
3
+ nav_schema: {
4
+ "type": "wrapper",
5
+ "size": "none",
6
+ "body": [
7
+ {
8
+ "type": "steedos-app-menu",
9
+ "stacked": true,
10
+ "appId": "approve_workflow",
11
+ "ignoreNavSchema": true
12
+ },
13
+ {
14
+ "type": "approval-tree-menu",
15
+ "apiUrl": "/api/approve_workflow/workflow/nav",
16
+ "navigateMode": "router"
17
+ }
18
+ ],
19
+ "isFixedHeight": false,
20
+ "isFixedWidth": false
21
+ }
@@ -45,11 +45,7 @@ window.waitForThing(window, 'socket').then(()=>{
45
45
  };
46
46
  if (isFirstRun === false && shouldReloadView()) {
47
47
  window.$(".list-view-btn-reload").click()
48
- clearTimeout(window.__navReloadTimer)
49
- clearTimeout(window.__navRetryTimer)
50
- window.__navReloadTimer = setTimeout(function(){ window.$(".instance-nav-reload").click() }, 500)
51
- // Retry: reload tree only (no API request) to pick up data already in Service store
52
- window.__navRetryTimer = setTimeout(function(){ window.$(".instance-tree-reload").click() }, 2000)
48
+ window.postMessage({ type: "approval-tree-menu:reload" }, "*")
53
49
  }
54
50
  isFirstRun = false;
55
51
  });
@@ -62,6 +58,7 @@ window.waitForThing(window, 'socket').then(()=>{
62
58
  };
63
59
  if (shouldReloadView()) {
64
60
  window.$(".list-view-btn-reload").click()
61
+ window.postMessage({ type: "approval-tree-menu:reload" }, "*")
65
62
  }
66
63
  });
67
64
 
@@ -159,7 +159,11 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
159
159
 
160
160
  // 拟稿时, 可以设定后续每个步骤的处理人 #1926
161
161
  if (instance.step_approve && !_.isEmpty(instance.step_approve[`${step_id}_options`])) {
162
- return instance.step_approve[`${step_id}_options`];
162
+ const res = instance.step_approve[`${step_id}_options`];
163
+
164
+ if(res != null && res.length > 0 && res[0] != null){
165
+ return res;
166
+ }
163
167
  }
164
168
 
165
169
  let approve_users = [];
@@ -195,7 +199,6 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
195
199
  current_step = _.find(current_steps, function (step) {
196
200
  return step._id === step_id;
197
201
  });
198
-
199
202
  if (current_step.step_type === "condition") {
200
203
  const unfinished_trace = _.find(instance.traces, function (trace) {
201
204
  return trace.is_finished === false;
@@ -19,7 +19,7 @@ module.exports = {
19
19
  name: name
20
20
  });
21
21
  if (!numberRules) {
22
- throw new Meteor.Error('error!', `${name}`);
22
+ throw new Error('error!', `${name}`);
23
23
  }
24
24
  date = new Date();
25
25
  context = {};
@@ -1585,7 +1585,7 @@ UUFlowManager.workflow_engine = async function (approve_from_client, current_use
1585
1585
  };
1586
1586
 
1587
1587
  // Skip processed constants and helpers
1588
- const SKIP_DESCRIPTION = '滑步跳过:已在之前步骤处理过';
1588
+ const SKIP_DESCRIPTION = ''; //滑步跳过:已在之前步骤处理过
1589
1589
 
1590
1590
  function isNormalApprove(approve) {
1591
1591
  return !approve.type || approve.type === 'draft' || approve.type === 'reassign';
@@ -1662,7 +1662,7 @@ UUFlowManager.handleSkipProcessed = async function (instance_id, flow, maxDepth
1662
1662
  return;
1663
1663
  }
1664
1664
 
1665
- const next_step_id = lines[0].to_step;
1665
+ let next_step_id = lines[0].to_step;
1666
1666
  let next_step;
1667
1667
  try {
1668
1668
  next_step = UUFlowManager.getStep(instance, flow, next_step_id);
@@ -1670,6 +1670,24 @@ UUFlowManager.handleSkipProcessed = async function (instance_id, flow, maxDepth
1670
1670
  return;
1671
1671
  }
1672
1672
 
1673
+ // If the next step is a condition node, evaluate it server-side to find the real next step
1674
+ // Condition nodes should never be "stopped at" during slide step; we must resolve through them.
1675
+ let conditionDepth = 10;
1676
+ while (next_step.step_type === 'condition' && conditionDepth-- > 0) {
1677
+ console.log(`[workflow/engine] [handleSkipProcessed] Evaluating condition node "${next_step.name}"`);
1678
+ const resolvedStepIds = await UUFlowManager.getNextSteps(instance, flow, next_step, '');
1679
+ if (!resolvedStepIds || resolvedStepIds.length === 0) {
1680
+ console.warn('[workflow/engine] [handleSkipProcessed] Condition node evaluated to no next steps');
1681
+ return;
1682
+ }
1683
+ next_step_id = resolvedStepIds[0];
1684
+ try {
1685
+ next_step = UUFlowManager.getStep(instance, flow, next_step_id);
1686
+ } catch (e) {
1687
+ return;
1688
+ }
1689
+ }
1690
+
1673
1691
  // Mark current trace as finished with all approves as skipped
1674
1692
  const setObj = {};
1675
1693
  setObj[`traces.${traceIdx}.is_finished`] = true;
@@ -19,7 +19,7 @@ mobile_columns:
19
19
  filter_scope: space
20
20
  filters: !!js/function |
21
21
  function(filters, data){
22
- var result = Steedos.authRequest(`/api/workflow/v2/\${data.listName}/filter?app=\${data.appId}`, {
22
+ var result = Steedos.authRequest(`/api/workflow/v2/\${data.listName}/filter?app=\${data.appId}&additionalFilters=\${data.additionalFilters || ""}`, {
23
23
  type: 'get', async: false
24
24
  });
25
25
  return result.filter;
@@ -18,7 +18,7 @@ mobile_columns:
18
18
  filter_scope: space
19
19
  filters: !!js/function |
20
20
  function(filters, data){
21
- var result = Steedos.authRequest(`/api/workflow/v2/\${data.listName}/filter?app=\${data.appId}`, {
21
+ var result = Steedos.authRequest(`/api/workflow/v2/\${data.listName}/filter?app=\${data.appId}&additionalFilters=\${data.additionalFilters || ""}`, {
22
22
  type: 'get', async: false
23
23
  });
24
24
  return result.filter;
@@ -29,13 +29,19 @@ amis_schema: |-
29
29
  "actionType": "custom",
30
30
  "script": "SteedosWorkflow.Instance.changed=false; navigate('/app/approve_workflow/instances/view/none?side_object=instances&side_listview_id=draft&additionalFilters=&flowId=&categoryId=')"
31
31
  },
32
+ {
33
+ "actionType": "wait",
34
+ "args": {
35
+ "time": 500
36
+ }
37
+ },
32
38
  {
33
39
  "actionType": "custom",
34
40
  "script":"window.$('.list-view-btn-reload').click()"
35
41
  },
36
42
  {
37
43
  "actionType": "custom",
38
- "script":"window.$('.instance-nav-reload').click()"
44
+ "script":"window.postMessage({ type: 'approval-tree-menu:reload' }, '*')"
39
45
  }
40
46
  ],
41
47
  "weight": 0
@@ -68,7 +68,7 @@ amis_schema: |-
68
68
  {
69
69
  "actionType": "wait",
70
70
  "args": {
71
- "time": 100
71
+ "time": 500
72
72
  }
73
73
  },
74
74
  {
@@ -77,7 +77,7 @@ amis_schema: |-
77
77
  },
78
78
  {
79
79
  "actionType": "custom",
80
- "script":"window.$('.instance-nav-reload').click()"
80
+ "script":"window.postMessage({ type: 'approval-tree-menu:reload' }, '*')"
81
81
  }
82
82
  ]
83
83
  }
@@ -18,7 +18,7 @@ mobile_columns:
18
18
  filter_scope: space
19
19
  filters: !!js/function |
20
20
  function(filters, data){
21
- var result = Steedos.authRequest(`/api/workflow/v2/\${data.listName}/filter?app=\${data.appId}`, {
21
+ var result = Steedos.authRequest(`/api/workflow/v2/\${data.listName}/filter?app=\${data.appId}&additionalFilters=\${data.additionalFilters || ""}`, {
22
22
  type: 'get', async: false
23
23
  });
24
24
  return result.filter;
@@ -9,7 +9,7 @@ columns:
9
9
  filter_scope: space
10
10
  filters: !!js/function |
11
11
  function(filters, data){
12
- var result = Steedos.authRequest(`/api/workflow/v2/\${data.listName}/filter?app=\${data.appId}`, {
12
+ var result = Steedos.authRequest(`/api/workflow/v2/\${data.listName}/filter?app=\${data.appId}&additionalFilters=\${data.additionalFilters || ""}`, {
13
13
  type: 'get', async: false
14
14
  });
15
15
  return result.filter;
@@ -18,7 +18,7 @@ mobile_columns:
18
18
  filter_scope: space
19
19
  filters: !!js/function |
20
20
  function(filters, data){
21
- var result = Steedos.authRequest(`/api/workflow/v2/\${data.listName}/filter?app=\${data.appId}`, {
21
+ var result = Steedos.authRequest(`/api/workflow/v2/\${data.listName}/filter?app=\${data.appId}&additionalFilters=\${data.additionalFilters || ""}`, {
22
22
  type: 'get', async: false
23
23
  });
24
24
  return result.filter;
@@ -18,7 +18,7 @@ mobile_columns:
18
18
  filter_scope: space
19
19
  filters: !!js/function |
20
20
  function(filters, data){
21
- var result = Steedos.authRequest(`/api/workflow/v2/\${data.listName}/filter?app=\${data.appId}`, {
21
+ var result = Steedos.authRequest(`/api/workflow/v2/\${data.listName}/filter?app=\${data.appId}&additionalFilters=\${data.additionalFilters || ""}`, {
22
22
  type: 'get', async: false
23
23
  });
24
24
  return result.filter;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "type": "liquid",
3
- "template": "<style>\n @keyframes fadeUpSpring {\n 0% { opacity: 0; transform: translateY(10px); }\n 100% { opacity: 1; transform: translateY(0); }\n }\n \n /* Make scrollbars standardized and visible */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n ::-webkit-scrollbar-thumb {\n background-color: rgba(0, 0, 0, 0.25); /* Darker for visibility on gray bg */\n border-radius: 4px;\n border: 2px solid transparent; /* Creates padding effect */\n background-clip: content-box;\n }\n ::-webkit-scrollbar-thumb:hover {\n background-color: rgba(0, 0, 0, 0.4);\n }\n\n /* \n Fix outer modal scrollbar - SAFER VERSION \n Only apply these aggressive overrides (no padding, hidden overflow)\n to the specific modal that contains our component (identified by #steedosFlowSelectorSidebarList).\n This prevents breaking other stacked modals like 'Confirm Dialogs'.\n */\n .antd-Modal-body:has(#steedosFlowSelectorSidebarList) {\n overflow: hidden !important;\n padding: 0 !important; /* Optional: maximize space */\n display: flex;\n flex-direction: column;\n }\n\n /* Ensure the AMIS container fills height if needed */\n .antd-Service, .liquid-amis-container {\n height: 100%;\n }\n</style>\n\n<!-- Main Container: Fixed Height 70vh. -->\n<div class=\"flex h-[70vh] max-h-[800px] w-full overflow-hidden font-sans text-gray-900 bg-white\" style=\"min-height: 0;\">\n\n <!-- Left Sidebar -->\n <!-- flex-col, h-full, overflow-hidden -->\n <div class=\"flex flex-col w-[260px] h-full border-r border-gray-200 bg-[#F2F2F7] shrink-0 overflow-hidden\">\n <!-- Header -->\n <div class=\"shrink-0 pt-4 pb-2 px-3\">\n <div class=\"relative group\">\n <div class=\"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5 text-gray-500\">\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n </svg>\n </div>\n <input type=\"text\" id=\"searchInput\" placeholder=\"搜索流程名称...\" class=\"w-full rounded-[10px] border-none bg-[#767680]/10 py-1.5 pl-9 pr-3 text-[14px] text-gray-900 placeholder:text-gray-500 outline-none transition-all duration-200 focus:bg-white focus:shadow-sm focus:ring-2 focus:ring-blue-500/20\">\n </div>\n </div>\n \n <!-- List Container -->\n <!-- min-h-0 is CRITICAL for flex child scrolling -->\n <div class=\"flex-1 min-h-0 overflow-y-auto px-2 pb-4 space-y-0.5 scroll-smooth\" id=\"steedosFlowSelectorSidebarList\">\n </div>\n </div>\n\n <!-- Right Content -->\n <!-- flex-1 fills remaining width -->\n <div class=\"flex-1 h-full relative bg-white overflow-hidden\">\n <!-- Absolute inset-0 locks the scroll container size -->\n <div id=\"mainContentScroll\" class=\"absolute inset-0 overflow-y-auto scroll-smooth p-6\">\n <div id=\"contentContainer\" class=\"w-full h-auto min-h-full\">\n <div class=\"flex h-full w-full flex-col items-center justify-center pt-20\">\n <div class=\"inline-flex items-center gap-2 text-gray-400 text-sm animate-pulse\">\n <svg class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path>\n </svg>\n <span>正在加载资源...</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n\n<script>\n const WorkflowService = {\n apiBase: \"\", \n getHeaders: function() { return { 'Content-Type': 'application/json' }; },\n getData: async function() {\n try {\n const appId = (typeof data !== 'undefined' && data.context && data.context.app_id) ? data.context.app_id : \"\";\n const url = this.apiBase + \"/service/api/flows/getList?action=new&appId=\" + encodeURIComponent(appId);\n const res = await fetch(url, { headers: this.getHeaders() });\n const treeData = await res.json();\n const categories = [];\n const parsedFlows = [];\n if (Array.isArray(treeData)) {\n treeData.forEach(cat => {\n categories.push({ _id: cat._id, name: cat.name });\n if (Array.isArray(cat.flows)) {\n cat.flows.forEach(f => {\n parsedFlows.push({\n id: f._id, name: f.name, categoryId: cat._id, categoryName: cat.name || \"其他流程\" \n });\n });\n }\n });\n }\n return { categories: categories, flows: parsedFlows };\n } catch (e) { \n console.error(\"WorkflowService Error:\", e);\n return { categories: [], flows: [] }; \n }\n },\n getFavorites: function() {\n const saved = localStorage.getItem('steedos_fav_ids');\n return saved ? JSON.parse(saved) : [];\n },\n toggleFavorite: function(flowId, isFav) {\n let favs = this.getFavorites();\n if (isFav) { if (!favs.includes(flowId)) favs.push(flowId); } \n else { favs = favs.filter(id => id !== flowId); }\n localStorage.setItem('steedos_fav_ids', JSON.stringify(favs));\n return favs;\n }\n };\n\n const AppState = { allFlows: [], categories: [], favorites: [] };\n const sidebarEl = document.getElementById('steedosFlowSelectorSidebarList');\n const contentEl = document.getElementById('contentContainer');\n const searchInput = document.getElementById('searchInput');\n\n async function init() {\n try {\n const data = await WorkflowService.getData();\n AppState.allFlows = data.flows;\n AppState.categories = data.categories;\n AppState.favorites = WorkflowService.getFavorites();\n renderUI();\n } catch (e) {\n contentEl.innerHTML = `<div class=\"text-gray-400 text-sm\">加载失败,请检查网络</div>`;\n }\n }\n\n function renderUI(filterText = \"\") {\n sidebarEl.innerHTML = \"\";\n contentEl.innerHTML = \"\";\n\n const isSearching = filterText.length > 0;\n let groups = [];\n\n const favFlows = AppState.allFlows.filter(f => \n AppState.favorites.includes(f.id) && \n (isSearching ? f.name.includes(filterText) : true)\n );\n if (favFlows.length > 0) {\n groups.push({ id: 'fav', name: \"我的收藏\", items: favFlows, isFav: true });\n }\n\n AppState.categories.forEach(cat => {\n const items = AppState.allFlows.filter(f => \n f.categoryId === cat._id &&\n (isSearching ? f.name.includes(filterText) : true)\n );\n if (items.length > 0) {\n groups.push({ id: cat._id, name: cat.name, items: items, isFav: false });\n }\n });\n\n const otherItems = AppState.allFlows.filter(f => \n !AppState.categories.find(c => c._id === f.categoryId) &&\n (isSearching ? f.name.includes(filterText) : true)\n );\n if (otherItems.length > 0) {\n groups.push({ id: 'other', name: \"其他流程\", items: otherItems, isFav: false });\n }\n\n if (groups.length === 0) {\n contentEl.innerHTML = `<div class=\"animate-[fadeUpSpring_0.5s_ease-out] text-center pt-20\"><div class=\"text-gray-200 text-7xl mb-4\">∅</div><div class=\"text-gray-400 text-sm\">未找到匹配流程</div></div>`;\n return;\n }\n\n const contentFragment = document.createDocumentFragment();\n groups.forEach((group, index) => {\n const groupId = `group-\\${group.id}`;\n const navItem = document.createElement('div');\n let navBase = \"group flex cursor-pointer items-center justify-between rounded-md px-3 py-2 text-[14px] transition-all duration-200 ease-out select-none\";\n let activeClass = \"bg-[#007AFF] text-white shadow-sm font-medium\";\n let inactiveClass = \"text-gray-700 hover:bg-black/5 active:bg-black/10\";\n \n navItem.className = `\\${navBase} \\${index === 0 ? activeClass : inactiveClass}`;\n const badgeClass = index === 0 ? \"text-white/80\" : \"text-gray-400 group-hover:text-gray-500\";\n \n navItem.innerHTML = `<span class=\"truncate\">\\${group.isFav ? '★ ' : ''}\\${group.name}</span><span class=\"\\${badgeClass} text-[12px] font-medium transition-colors\">\\${group.items.length}</span>`;\n \n navItem.onclick = () => {\n Array.from(sidebarEl.children).forEach(el => {\n el.className = `\\${navBase} \\${inactiveClass}`;\n el.querySelector('span:last-child').className = \"text-gray-400 group-hover:text-gray-500 text-[12px] font-medium transition-colors\";\n });\n navItem.className = `\\${navBase} \\${activeClass}`;\n navItem.querySelector('span:last-child').className = \"text-white/80 text-[12px] font-medium transition-colors\";\n \n const target = document.getElementById(groupId);\n const container = document.getElementById('mainContentScroll');\n if(target && container) {\n const targetTop = target.getBoundingClientRect().top; \n const containerTop = container.getBoundingClientRect().top; \n container.scrollTo({ top: container.scrollTop + targetTop - containerTop - 16, behavior: 'smooth' });\n }\n };\n sidebarEl.appendChild(navItem);\n\n const section = document.createElement('div');\n section.id = groupId;\n section.className = \"mb-10\";\n const headerColor = group.isFav ? 'text-amber-500' : 'text-gray-900';\n section.innerHTML = `<div class=\"sticky top-0 z-20 mb-4 bg-white pb-2 text-xl font-bold tracking-tight text-left border-b border-gray-100 \\${headerColor}\">\\${group.name}</div>`;\n\n const grid = document.createElement('div');\n grid.className = 'grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-4';\n\n const gridFragment = document.createDocumentFragment();\n group.items.forEach((flow, i) => {\n const isFav = AppState.favorites.includes(flow.id);\n const colorMap = ['bg-blue-50 text-blue-600', 'bg-orange-50 text-orange-600', 'bg-emerald-50 text-emerald-600', 'bg-indigo-50 text-indigo-600'];\n const colorClass = colorMap[(flow.name.length + i) % 4];\n const firstChar = flow.name.replace(/【.*?】/g, '').charAt(0) || flow.name.charAt(0);\n const card = document.createElement('div');\n card.className = 'group relative flex h-auto min-h-[72px] cursor-pointer items-center rounded-2xl border border-gray-100 bg-white p-3 text-left shadow-[0_2px_8px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.02] transition-[transform,box-shadow] duration-200 ease-out hover:-translate-y-1 hover:border-gray-200 hover:shadow-[0_12px_24px_rgba(0,0,0,0.08)] active:scale-[0.98] active:bg-gray-50' + (i < 12 ? ' animate-[fadeUpSpring_0.4s_cubic-bezier(0.16,1,0.3,1)_forwards]' : '');\n if (i < 12) { card.style.animationDelay = (i * 0.03) + 's'; card.style.opacity = '0'; }\n const iconClass = isFav ? 'text-yellow-400 fill-current' : 'text-gray-300 group-hover/btn:text-gray-400 fill-none stroke-current stroke-[1.5]';\n const btnBgClass = isFav ? 'opacity-100 hover:scale-110' : 'opacity-0 group-hover:opacity-100 hover:bg-gray-100 hover:scale-110';\n\n card.innerHTML = `\n <div class=\"star-btn group/btn absolute right-2 top-1/2 -translate-y-1/2 z-20 flex h-8 w-8 items-center justify-center rounded-full transition-all duration-200 \\${btnBgClass}\" title=\"\\${isFav ? '取消收藏' : '加入收藏'}\">\n <svg class=\"h-5 w-5 transition-colors duration-300 \\${iconClass}\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z\" />\n </svg>\n </div>\n <div class=\"mr-4 flex h-11 w-11 shrink-0 items-center justify-center rounded-xl text-[16px] font-bold \\${colorClass}\">\\${firstChar}</div>\n <div class=\"flex-1 pr-8 text-[15px] font-medium text-gray-900 line-clamp-3 leading-relaxed tracking-tight\" title=\"\\${flow.name}\">\\${flow.name}</div>\n `;\n card.onclick = () => {\n if (card.dataset.loading === 'true') return;\n card.dataset.loading = 'true';\n card.style.pointerEvents = 'none';\n card.style.opacity = '0.6';\n card.innerHTML = '<div class=\"flex items-center justify-center w-full gap-2 text-gray-400 text-sm\"><svg class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path></svg><span>正在创建...</span></div>';\n setTimeout(() => {\n data._scoped.doAction([\n { \"actionType\": \"broadcast\", \"args\": { \"eventName\": \"flows.selected\" }, \"data\": { \"value\": flow.id } }\n ])\n }, 50);\n };\n const starBtn = card.querySelector('.star-btn');\n const starIcon = starBtn.querySelector('svg');\n starBtn.onclick = (e) => {\n e.stopPropagation();\n const newFavState = !starBtn.classList.contains('active-fav');\n if (newFavState) {\n starBtn.classList.add('active-fav', 'opacity-100');\n starBtn.classList.add('animate-[starPop_0.4s_ease-out]');\n starIcon.setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-yellow-400 fill-current');\n } else {\n starBtn.classList.remove('active-fav', 'opacity-100');\n starBtn.classList.remove('animate-[starPop_0.4s_ease-out]');\n starIcon.setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-gray-300 group-hover/btn:text-gray-400 fill-none stroke-current stroke-[1.5]');\n }\n AppState.favorites = WorkflowService.toggleFavorite(flow.id, newFavState);\n setTimeout(() => renderUI(searchInput.value), 300);\n };\n if (isFav) starBtn.classList.add('active-fav');\n gridFragment.appendChild(card);\n });\n grid.appendChild(gridFragment);\n section.appendChild(grid);\n contentFragment.appendChild(section);\n });\n contentEl.appendChild(contentFragment);\n }\n\n let _searchTimer = null;\n searchInput.addEventListener('input', (e) => {\n clearTimeout(_searchTimer);\n _searchTimer = setTimeout(() => renderUI(e.target.value.trim()), 300);\n });\n init();\n</script>\n",
3
+ "template": "<style>\n @keyframes fadeUpSpring {\n 0% { opacity: 0; transform: translateY(10px); }\n 100% { opacity: 1; transform: translateY(0); }\n }\n \n /* Make scrollbars standardized and visible */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n ::-webkit-scrollbar-thumb {\n background-color: rgba(0, 0, 0, 0.25); /* Darker for visibility on gray bg */\n border-radius: 4px;\n border: 2px solid transparent; /* Creates padding effect */\n background-clip: content-box;\n }\n ::-webkit-scrollbar-thumb:hover {\n background-color: rgba(0, 0, 0, 0.4);\n }\n\n /* \n Fix outer modal scrollbar - SAFER VERSION \n Only apply these aggressive overrides (no padding, hidden overflow)\n to the specific modal that contains our component (identified by #steedosFlowSelectorSidebarList).\n This prevents breaking other stacked modals like 'Confirm Dialogs'.\n */\n .antd-Modal-body:has(#steedosFlowSelectorSidebarList) {\n overflow: hidden !important;\n padding: 0 !important; /* Optional: maximize space */\n display: flex;\n flex-direction: column;\n }\n\n /* Ensure the AMIS container fills height if needed */\n .antd-Service, .liquid-amis-container {\n height: 100%;\n }\n</style>\n\n<!-- Main Container: Fixed Height 70vh. -->\n<div class=\"flex h-[70vh] max-h-[800px] w-full overflow-hidden font-sans text-gray-900 bg-white\" style=\"min-height: 0;\">\n\n <!-- Left Sidebar -->\n <!-- flex-col, h-full, overflow-hidden -->\n <div class=\"flex flex-col w-[260px] h-full border-r border-gray-200 bg-[#F2F2F7] shrink-0 overflow-hidden\">\n <!-- Header -->\n <div class=\"shrink-0 pt-4 pb-2 px-3\">\n <div class=\"relative group\">\n <div class=\"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5 text-gray-500\">\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n </svg>\n </div>\n <input type=\"text\" id=\"searchInput\" placeholder=\"搜索流程名称...\" class=\"w-full rounded-[10px] border-none bg-[#767680]/10 py-1.5 pl-9 pr-3 text-[14px] text-gray-900 placeholder:text-gray-500 outline-none transition-all duration-200 focus:bg-white focus:shadow-sm focus:ring-2 focus:ring-blue-500/20\">\n </div>\n </div>\n \n <!-- List Container -->\n <!-- min-h-0 is CRITICAL for flex child scrolling -->\n <div class=\"flex-1 min-h-0 overflow-y-auto px-2 pb-4 space-y-0.5 scroll-smooth\" id=\"steedosFlowSelectorSidebarList\">\n </div>\n </div>\n\n <!-- Right Content -->\n <!-- flex-1 fills remaining width -->\n <div class=\"flex-1 h-full relative bg-white overflow-hidden\">\n <!-- Absolute inset-0 locks the scroll container size -->\n <div id=\"mainContentScroll\" class=\"absolute inset-0 overflow-y-auto scroll-smooth p-6\">\n <div id=\"contentContainer\" class=\"w-full h-auto min-h-full\">\n <div class=\"flex h-full w-full flex-col items-center justify-center pt-20\">\n <div class=\"inline-flex items-center gap-2 text-gray-400 text-sm animate-pulse\">\n <svg class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path>\n </svg>\n <span>正在加载资源...</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n\n<script>\n const WorkflowService = {\n apiBase: \"\",\n getHeaders: function() { return { 'Content-Type': 'application/json' }; },\n getData: async function() {\n try {\n const appId = (typeof data !== 'undefined' && data.context && data.context.app_id) ? data.context.app_id : \"\";\n const url = this.apiBase + \"/service/api/flows/getList?action=new&appId=\" + encodeURIComponent(appId);\n const res = await fetch(url, { headers: this.getHeaders() });\n const treeData = await res.json();\n const categories = [];\n const parsedFlows = [];\n if (Array.isArray(treeData)) {\n treeData.forEach(cat => {\n categories.push({ _id: cat._id, name: cat.name });\n if (Array.isArray(cat.flows)) {\n cat.flows.forEach(f => {\n parsedFlows.push({\n id: f._id, name: f.name, categoryId: cat._id, categoryName: cat.name || \"其他流程\"\n });\n });\n }\n });\n }\n return { categories: categories, flows: parsedFlows };\n } catch (e) {\n console.error(\"WorkflowService Error:\", e);\n return { categories: [], flows: [] };\n }\n },\n getFavorites: function() {\n const saved = localStorage.getItem('steedos_fav_ids');\n return saved ? JSON.parse(saved) : [];\n },\n toggleFavorite: function(flowId, isFav) {\n let favs = this.getFavorites();\n if (isFav) { if (!favs.includes(flowId)) favs.push(flowId); }\n else { favs = favs.filter(id => id !== flowId); }\n localStorage.setItem('steedos_fav_ids', JSON.stringify(favs));\n return favs;\n }\n };\n\n const AppState = { allFlows: [], categories: [], favorites: [] };\n\n function escapeHtml(str) {\n return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;').replace(/'/g, '&#39;');\n }\n const sidebarEl = document.getElementById('steedosFlowSelectorSidebarList');\n const contentEl = document.getElementById('contentContainer');\n const searchInput = document.getElementById('searchInput');\n\n // Optimization 2: lazy rendering state\n let _observer = null;\n let _currentGroups = [];\n\n // Render cards into a placeholder element for a single group (lazy)\n var ESTIMATED_CARD_HEIGHT_PX = 88;\n var LAZY_LOAD_MARGIN = '300px 0px';\n\n function fillCards(group, placeholder) {\n if (placeholder.dataset.rendered) return;\n placeholder.dataset.rendered = 'true';\n placeholder.style.minHeight = '';\n const gridFragment = document.createDocumentFragment();\n group.items.forEach(function(flow, i) {\n const isFav = AppState.favorites.includes(flow.id);\n const colorMap = ['bg-blue-50 text-blue-600', 'bg-orange-50 text-orange-600', 'bg-emerald-50 text-emerald-600', 'bg-indigo-50 text-indigo-600'];\n const colorClass = colorMap[(flow.name.length + i) % 4];\n const firstChar = flow.name.replace(/【.*?】/g, '').charAt(0) || flow.name.charAt(0);\n const card = document.createElement('div');\n card.className = 'group relative flex h-auto min-h-[72px] cursor-pointer items-center rounded-2xl border border-gray-100 bg-white p-3 text-left shadow-[0_2px_8px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.02] transition-[transform,box-shadow] duration-200 ease-out hover:-translate-y-1 hover:border-gray-200 hover:shadow-[0_12px_24px_rgba(0,0,0,0.08)] active:scale-[0.98] active:bg-gray-50' + (i < 12 ? ' animate-[fadeUpSpring_0.4s_cubic-bezier(0.16,1,0.3,1)_forwards]' : '');\n if (i < 12) { card.style.animationDelay = (i * 0.03) + 's'; card.style.opacity = '0'; }\n // Optimization 3: data-flow-id for event delegation\n card.dataset.flowId = flow.id;\n const iconClass = isFav ? 'text-yellow-400 fill-current' : 'text-gray-300 group-hover/btn:text-gray-400 fill-none stroke-current stroke-[1.5]';\n const btnBgClass = isFav ? 'opacity-100 hover:scale-110' : 'opacity-0 group-hover:opacity-100 hover:bg-gray-100 hover:scale-110';\n card.innerHTML = '<div class=\"star-btn group/btn absolute right-2 top-1/2 -translate-y-1/2 z-20 flex h-8 w-8 items-center justify-center rounded-full transition-all duration-200 ' + btnBgClass + (isFav ? ' active-fav' : '') + '\" title=\"' + (isFav ? '取消收藏' : '加入收藏') + '\" data-flow-id=\"' + flow.id + '\"><svg class=\"h-5 w-5 transition-colors duration-300 ' + iconClass + '\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z\" /></svg></div><div class=\"mr-4 flex h-11 w-11 shrink-0 items-center justify-center rounded-xl text-[16px] font-bold ' + colorClass + '\">' + escapeHtml(firstChar) + '</div><div class=\"flex-1 pr-8 text-[15px] font-medium text-gray-900 line-clamp-3 leading-relaxed tracking-tight\" title=\"' + escapeHtml(flow.name) + '\">' + escapeHtml(flow.name) + '</div>';\n gridFragment.appendChild(card);\n });\n placeholder.appendChild(gridFragment);\n }\n\n // Optimization 3: single delegated click listener — handles both star-btn and card clicks\n contentEl.addEventListener('click', function(e) {\n const starBtn = e.target.closest('.star-btn');\n if (starBtn) {\n const flowId = starBtn.dataset.flowId;\n const isFav = AppState.favorites.includes(flowId);\n const newFavState = !isFav;\n if (newFavState) {\n starBtn.classList.add('active-fav', 'opacity-100');\n starBtn.querySelector('svg').setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-yellow-400 fill-current');\n starBtn.setAttribute('title', '取消收藏');\n } else {\n starBtn.classList.remove('active-fav', 'opacity-100');\n starBtn.querySelector('svg').setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-gray-300 group-hover/btn:text-gray-400 fill-none stroke-current stroke-[1.5]');\n starBtn.setAttribute('title', '加入收藏');\n }\n AppState.favorites = WorkflowService.toggleFavorite(flowId, newFavState);\n setTimeout(function() { renderUI(searchInput.value); }, 300);\n return;\n }\n const card = e.target.closest('[data-flow-id]');\n if (card && !card.classList.contains('star-btn')) {\n if (card.dataset.loading === 'true') return;\n card.dataset.loading = 'true';\n card.style.pointerEvents = 'none';\n card.style.opacity = '0.6';\n card.innerHTML = '<div class=\"flex items-center justify-center w-full gap-2 text-gray-400 text-sm\"><svg class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path></svg><span>正在创建...</span></div>';\n setTimeout(function() {\n data._scoped.doAction([\n { \"actionType\": \"broadcast\", \"args\": { \"eventName\": \"flows.selected\" }, \"data\": { \"value\": card.dataset.flowId } }\n ]);\n }, 50);\n }\n });\n\n async function init() {\n try {\n const result = await WorkflowService.getData();\n AppState.allFlows = result.flows;\n AppState.categories = result.categories;\n AppState.favorites = WorkflowService.getFavorites();\n renderUI();\n } catch (e) {\n contentEl.innerHTML = '<div class=\"text-gray-400 text-sm\">加载失败,请检查网络</div>';\n }\n }\n\n function renderUI(filterText) {\n filterText = filterText || \"\";\n // Disconnect previous IntersectionObserver before rebuilding DOM\n if (_observer) { _observer.disconnect(); _observer = null; }\n sidebarEl.innerHTML = \"\";\n contentEl.innerHTML = \"\";\n\n const isSearching = filterText.length > 0;\n const groups = [];\n\n const favFlows = AppState.allFlows.filter(function(f) {\n return AppState.favorites.includes(f.id) && (isSearching ? f.name.toLowerCase().includes(filterText.toLowerCase()) : true);\n });\n if (favFlows.length > 0) {\n groups.push({ id: 'fav', name: \"我的收藏\", items: favFlows, isFav: true });\n }\n\n AppState.categories.forEach(function(cat) {\n const items = AppState.allFlows.filter(function(f) {\n return f.categoryId === cat._id && (isSearching ? f.name.toLowerCase().includes(filterText.toLowerCase()) : true);\n });\n if (items.length > 0) {\n groups.push({ id: cat._id, name: cat.name, items: items, isFav: false });\n }\n });\n\n const otherItems = AppState.allFlows.filter(function(f) {\n return !AppState.categories.find(function(c) { return c._id === f.categoryId; }) &&\n (isSearching ? f.name.toLowerCase().includes(filterText.toLowerCase()) : true);\n });\n if (otherItems.length > 0) {\n groups.push({ id: 'other', name: \"其他流程\", items: otherItems, isFav: false });\n }\n\n if (groups.length === 0) {\n contentEl.innerHTML = '<div class=\"animate-[fadeUpSpring_0.5s_ease-out] text-center pt-20\"><div class=\"text-gray-200 text-7xl mb-4\">∅</div><div class=\"text-gray-400 text-sm\">未找到匹配流程</div></div>';\n return;\n }\n\n _currentGroups = groups;\n const contentFragment = document.createDocumentFragment();\n const navBase = \"group flex cursor-pointer items-center justify-between rounded-md px-3 py-2 text-[14px] transition-all duration-200 ease-out select-none\";\n const activeClass = \"bg-[#007AFF] text-white shadow-sm font-medium\";\n const inactiveClass = \"text-gray-700 hover:bg-black/5 active:bg-black/10\";\n\n groups.forEach(function(group, index) {\n const groupId = 'group-' + group.id;\n const navItem = document.createElement('div');\n navItem.className = navBase + ' ' + (index === 0 ? activeClass : inactiveClass);\n const badgeClass = index === 0 ? \"text-white/80\" : \"text-gray-400 group-hover:text-gray-500\";\n navItem.innerHTML = '<span class=\"truncate\">' + (group.isFav ? '★ ' : '') + group.name + '</span><span class=\"' + badgeClass + ' text-[12px] font-medium transition-colors\">' + group.items.length + '</span>';\n\n navItem.onclick = (function(grp, gId) {\n return function() {\n Array.from(sidebarEl.children).forEach(function(el) {\n el.className = navBase + ' ' + inactiveClass;\n el.querySelector('span:last-child').className = \"text-gray-400 group-hover:text-gray-500 text-[12px] font-medium transition-colors\";\n });\n navItem.className = navBase + ' ' + activeClass;\n navItem.querySelector('span:last-child').className = \"text-white/80 text-[12px] font-medium transition-colors\";\n const section = document.getElementById(gId);\n if (section) {\n // Optimization 2: force-render target section before scrolling to avoid blank placeholder\n const ph = section.querySelector('.card-placeholder');\n if (ph && !ph.dataset.rendered) {\n fillCards(grp, ph);\n if (_observer) _observer.unobserve(ph);\n }\n const container = document.getElementById('mainContentScroll');\n if (container) {\n const targetTop = section.getBoundingClientRect().top;\n const containerTop = container.getBoundingClientRect().top;\n container.scrollTo({ top: container.scrollTop + targetTop - containerTop - 16, behavior: 'smooth' });\n }\n }\n };\n })(group, groupId);\n sidebarEl.appendChild(navItem);\n\n // Optimization 2: render section header + placeholder (cards filled lazily)\n const section = document.createElement('div');\n section.id = groupId;\n section.className = \"mb-10\";\n const headerColor = group.isFav ? 'text-amber-500' : 'text-gray-900';\n section.innerHTML = '<div class=\"sticky top-0 z-20 mb-4 bg-white pb-2 text-xl font-bold tracking-tight text-left border-b border-gray-100 ' + headerColor + '\">' + group.name + '</div>';\n const placeholder = document.createElement('div');\n placeholder.className = 'card-placeholder grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-4';\n // Pre-size the placeholder to avoid layout shift while cards aren't rendered yet\n placeholder.style.minHeight = (Math.ceil(group.items.length / 4) * ESTIMATED_CARD_HEIGHT_PX) + 'px';\n section.appendChild(placeholder);\n contentFragment.appendChild(section);\n });\n\n contentEl.appendChild(contentFragment);\n\n // Optimization 2: set up IntersectionObserver to fill cards as groups scroll into view\n const scrollContainer = document.getElementById('mainContentScroll');\n _observer = new IntersectionObserver(function(entries) {\n entries.forEach(function(entry) {\n if (entry.isIntersecting && entry.target.isConnected && !entry.target.dataset.rendered) {\n const ph = entry.target;\n const section = ph.parentElement;\n if (section && section.id) {\n const gId = section.id.replace('group-', '');\n const grp = _currentGroups.find(function(g) { return String(g.id) === gId; });\n if (grp) {\n fillCards(grp, ph);\n if (_observer) _observer.unobserve(ph);\n }\n }\n }\n });\n }, { root: scrollContainer, rootMargin: LAZY_LOAD_MARGIN });\n\n contentEl.querySelectorAll('.card-placeholder').forEach(function(p) {\n _observer.observe(p);\n });\n }\n\n // Optimization 4: Chinese IME compositionstart/compositionend prevents stutter during composition\n let _isComposing = false;\n let _searchTimer = null;\n searchInput.addEventListener('compositionstart', function() { _isComposing = true; });\n searchInput.addEventListener('compositionend', function(e) {\n _isComposing = false;\n clearTimeout(_searchTimer);\n renderUI(e.target.value.trim());\n });\n searchInput.addEventListener('input', function(e) {\n if (_isComposing) return;\n clearTimeout(_searchTimer);\n _searchTimer = setTimeout(function() { renderUI(e.target.value.trim()); }, 300);\n });\n\n init();\n</script>\n\n",
4
4
  "className": "h-full"
5
- }
5
+ }