@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.
- package/designer/dist/amis-renderer/amis-renderer.css +1 -1
- package/designer/dist/amis-renderer/amis-renderer.js +1 -1
- package/designer/dist/assets/index-7cMOmCg4.css +1 -0
- package/designer/dist/assets/index-DbYFInYv.js +943 -0
- package/designer/dist/index.html +2 -2
- package/main/default/applications/approve_workflow.app.yml +3 -160
- package/main/default/applications/desktop.app.yml +21 -0
- package/main/default/client/socket.client.js +2 -5
- package/main/default/manager/handlers_manager.js +5 -2
- package/main/default/manager/instance_number_rules.js +1 -1
- package/main/default/manager/uuflow_manager.js +20 -2
- package/main/default/objects/instance_tasks/listviews/inbox.listview.yml +1 -1
- package/main/default/objects/instance_tasks/listviews/outbox.listview.yml +1 -1
- package/main/default/objects/instances/buttons/instance_delete.button.yml +7 -1
- package/main/default/objects/instances/buttons/instance_new.button.yml +2 -2
- package/main/default/objects/instances/listviews/completed.listview.yml +1 -1
- package/main/default/objects/instances/listviews/draft.listview.yml +1 -1
- package/main/default/objects/instances/listviews/monitor.listview.yml +1 -1
- package/main/default/objects/instances/listviews/pending.listview.yml +1 -1
- package/main/default/pages/flow_selector.page.amis.json +2 -2
- package/main/default/pages/flow_selector_mobile.page.amis.json +2 -2
- package/main/default/pages/page_instance_print.page.amis.json +11 -1
- package/main/default/routes/am.router.js +3 -1
- package/main/default/routes/api_auto_number.router.js +166 -23
- package/main/default/routes/api_workflow_ai_form_design.router.js +116 -16
- package/main/default/routes/api_workflow_ai_form_design_stream.router.js +115 -17
- package/main/default/routes/api_workflow_box_filter.router.js +2 -2
- package/main/default/routes/api_workflow_nav.router.js +1 -0
- package/main/default/services/flows.service.js +20 -24
- package/main/default/services/instance.service.js +6 -4
- package/main/default/test/test_badge_draft.js +12 -26
- package/main/default/test/test_badge_update.js +10 -54
- package/package.json +1 -1
- package/package.service.js +14 -0
- package/public/amis-renderer/amis-renderer.css +1 -1
- package/public/amis-renderer/amis-renderer.js +1 -1
- package/public/workflow/index.css +10 -279
- package/designer/dist/assets/index-CxYuhf9v.js +0 -757
- package/designer/dist/assets/index-Dve-EwQO.css +0 -1
package/designer/dist/index.html
CHANGED
|
@@ -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-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/api/workflow/designer-v2/assets/index-
|
|
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": "
|
|
27
|
-
"
|
|
28
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
-
|
|
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
|
|
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":
|
|
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
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"').replace(/'/g, ''');\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
|
+
}
|