@steedos-labs/plugin-workflow 3.0.11 → 3.0.13
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/AI_PLUGIN_GUIDE.md +939 -0
- package/main/default/applications/approve_workflow.app.yml +159 -3
- package/main/default/manager/handlers_manager.js +1 -1
- package/main/default/manager/instance_number_rules.js +84 -0
- package/main/default/manager/instance_tasks_manager.js +79 -1
- package/main/default/manager/uuflow_manager.js +60 -45
- package/main/default/objects/flows/flows.object.yml +1 -4
- package/main/default/objects/instances/buttons/instance_delete.button.yml +4 -24
- package/main/default/objects/instances/buttons/instance_new.button.yml +11 -11
- package/main/default/objects/instances/buttons/instance_reassign.button.yml +5 -4
- package/main/default/objects/instances/buttons/instance_save.button.yml +6 -8
- package/main/default/pages/flow_selector.page.amis.json +5 -0
- package/main/default/pages/flow_selector.page.yml +7 -0
- package/main/default/pages/instance_detail.page.amis.json +1 -99
- package/main/default/pages/instance_tasks_detail.page.amis.json +0 -100
- package/main/default/pages/page_instance_view.page.amis.json +1 -1
- package/main/default/routes/api_auto_number.router.js +233 -0
- package/main/default/routes/api_files.router.js +21 -0
- package/main/default/routes/api_have_read.router.js +20 -2
- package/main/default/routes/api_workflow_chart.router.js +23 -3
- package/main/default/routes/api_workflow_next_step.router.js +135 -30
- package/main/default/routes/api_workflow_next_step_users.router.js +63 -10
- package/main/default/routes/nextStepUsers.router.js +3 -3
- package/main/default/services/instance.service.js +1 -9
- package/package.json +1 -1
- package/public/workflow/index.css +12 -4
- package/main/default/pages/instance_tasks_list.page.amis.json +0 -330
- package/main/default/pages/instance_tasks_list.page.yml +0 -12
- package/main/default/pages/instances_list.page.amis.json +0 -327
- package/main/default/pages/instances_list.page.yml +0 -12
|
@@ -18,10 +18,7 @@ amis_schema: |-
|
|
|
18
18
|
"method": "post",
|
|
19
19
|
"sendOn": "",
|
|
20
20
|
"requestAdaptor": "var _SteedosUI$getRef$get, _approveValues$next_s;\nconst formValues = context._scoped.getComponentById(\"instance_form\").getValues();const _formValues = JSON.parse(JSON.stringify(formValues)); if(_formValues){delete _formValues.__applicant} \nconst approveValues = (_SteedosUI$getRef$get = context._scoped.getComponentById(\"instance_approval\")) === null || _SteedosUI$getRef$get === void 0 ? void 0 : _SteedosUI$getRef$get.getValues();\nlet nextUsers = approveValues === null || approveValues === void 0 ? void 0 : approveValues.next_users;\nif (_.isString(nextUsers)) {\n nextUsers = [approveValues.next_users];\n}\nconst instance = context.record;\nconst body = {\n instance: {\n _id: instance._id,\n applicant: formValues.applicant.user,\n submitter: formValues.submitter,\n traces: [{\n _id: instance.trace._id,\n step: instance.step._id,\n approves: [{\n _id: instance.approve._id,\n next_steps: [{\n step: approveValues === null || approveValues === void 0 || (_approveValues$next_s = approveValues.next_step) === null || _approveValues$next_s === void 0 ? void 0 : _approveValues$next_s._id,\n users: nextUsers\n }],\n description: approveValues === null || approveValues === void 0 ? void 0 : approveValues.suggestion,\n values: _formValues\n }]\n }]\n }\n};\napi.data = body;\nreturn api;",
|
|
21
|
-
"adaptor": "window.SteedosWorkflow.Instance.changed = false; if (payload.instance == \"upgraded\") { window.setTimeout(function(){
|
|
22
|
-
"headers": {
|
|
23
|
-
"Authorization": "Bearer ${context.tenantId},${context.authToken}"
|
|
24
|
-
},
|
|
21
|
+
"adaptor": "window.SteedosWorkflow.Instance.changed = false; if (payload.instance == \"upgraded\") { window.setTimeout(function(){ $('.steedos-workflow-reload-btn').trigger('click'); }, 2000); return {...payload, status: 1, msg: t('instance_action_instance_save_msg_upgraded')}; } \n return payload.instance != false ? {data: payload, status: 0, msg: t('instance_action_instance_save_msg_success')} : {...payload, status: 1, msg: t('instance_action_instance_save_msg_failed')};",
|
|
25
22
|
"data": {
|
|
26
23
|
"&": "$$"
|
|
27
24
|
}
|
|
@@ -33,14 +30,11 @@ amis_schema: |-
|
|
|
33
30
|
{
|
|
34
31
|
"args": {
|
|
35
32
|
"api": {
|
|
36
|
-
"url": "
|
|
33
|
+
"url": "/api/workflow/v2/approve/save",
|
|
37
34
|
"method": "post",
|
|
38
35
|
"sendOn": "",
|
|
39
36
|
"requestAdaptor": "var _SteedosUI$getRef$get, _approveValues$next_s;\nconst formValues = context._scoped.getComponentById(\"instance_form\").getValues();\nconst approveValues = (_SteedosUI$getRef$get = context._scoped.getComponentById(\"instance_approval\")) === null || _SteedosUI$getRef$get === void 0 ? void 0 : _SteedosUI$getRef$get.getValues();\nlet nextUsers = approveValues === null || approveValues === void 0 ? void 0 : approveValues.next_users;\nif (_.isString(nextUsers)) {\n nextUsers = [approveValues.next_users];\n}\nconst instance = context.record;\nconst body = {\n approve: {\n id: instance.approve._id,\n instance: instance._id,\n trace: instance.trace._id,\n next_steps: [{\n step: approveValues === null || approveValues === void 0 || (_approveValues$next_s = approveValues.next_step) === null || _approveValues$next_s === void 0 ? void 0 : _approveValues$next_s._id,\n users: nextUsers\n }],\n description: approveValues === null || approveValues === void 0 ? void 0 : approveValues.suggestion,\n judge: approveValues === null || approveValues === void 0 ? void 0 : approveValues.judge,\n values: formValues\n }\n};\napi.data = body;\nreturn api;",
|
|
40
37
|
"adaptor": "window.SteedosWorkflow.Instance.changed = false; return payload.instance ? {...payload, status: 0, msg: t('instance_action_instance_save_msg_success')} : {...payload, status: 1, msg: t('instance_action_instance_save_msg_failed')};",
|
|
41
|
-
"headers": {
|
|
42
|
-
"Authorization": "Bearer ${context.tenantId},${context.authToken}"
|
|
43
|
-
},
|
|
44
38
|
"data": {
|
|
45
39
|
"&": "$$"
|
|
46
40
|
}
|
|
@@ -48,6 +42,10 @@ amis_schema: |-
|
|
|
48
42
|
},
|
|
49
43
|
"actionType": "ajax",
|
|
50
44
|
"expression": "record.box != 'draft' && record.state != 'draft'"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"actionType": "custom",
|
|
48
|
+
"script": "if(event.data.instance && event.data.instance.name){doAction({actionType: 'setValue',componentId: 'u:instancePage',args: { value: {title: event.data.instance.name} }});}"
|
|
51
49
|
}
|
|
52
50
|
],
|
|
53
51
|
"weight": 0
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "liquid",
|
|
3
|
+
"template": "<style>\n /* 动效 */\n @keyframes fadeUpSpring {\n 0% {\n opacity: 0;\n transform: translateY(10px);\n }\n\n 100% {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n @keyframes starPop {\n 0% {\n transform: scale(1);\n }\n\n 40% {\n transform: scale(1.35) rotate(15deg);\n }\n\n 100% {\n transform: scale(1) rotate(0);\n }\n }\n\n .no-scrollbar::-webkit-scrollbar {\n display: none;\n }\n\n .no-scrollbar {\n -ms-overflow-style: none;\n scrollbar-width: none;\n }\n\n .line-clamp-2 {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n }\n</style>\n\n<div class=\"flex h-full w-full flex-row items-stretch overflow-hidden bg-white font-sans text-gray-900 antialiased\">\n\n <div class=\"flex h-full w-[260px] shrink-0 flex-col border-r border-gray-200/80 bg-[#F2F2F7] z-20\">\n\n <div class=\"shrink-0 px-3 pt-4 pb-2\">\n <div class=\"px-2 mb-3 text-2xl font-bold tracking-tight text-black\">流程</div>\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\"\n 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=\"搜索\"\n 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 <div class=\"flex-1 overflow-y-auto px-2 pb-4 no-scrollbar space-y-0.5\" id=\"sidebarList\">\n </div>\n </div>\n\n <div class=\"relative flex-1 h-full overflow-y-auto scroll-smooth bg-white z-10\" id=\"mainContent\">\n <div id=\"contentContainer\" class=\"flex h-full w-full flex-col items-center justify-center\">\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\n<script>\n // --- Service Layer ---\n const WorkflowService = {\n apiBase: \"\", \n \n getHeaders: function() { return { 'Content-Type': 'application/json' }; },\n \n getCategories: async function() {\n try {\n const url = `\\${this.apiBase}/api/v6/data/categories?skip=0&top=100&sort=sort_no&fields=name`;\n const res = await fetch(url, { headers: this.getHeaders() });\n const json = await res.json();\n return json.data || [];\n } catch (e) { return []; }\n },\n \n getFlows: async function() {\n try {\n const filters = JSON.stringify([[\"perms.users_can_add\",\"=\",data.context.userId], \"or\", [\"perms.orgs_can_add\",\"in\",data.context.user.organizations_parents]]);\n const url = `\\${this.apiBase}/api/v6/data/flows?skip=0&top=5000&sort=sort_no&fields=name%2Ccategory&filters=\\${encodeURIComponent(filters)}`;\n const res = await fetch(url, { headers: this.getHeaders() });\n const json = await res.json();\n return json.data || [];\n } catch (e) { return []; }\n },\n \n getData: async function() {\n const [categories, flows] = await Promise.all([this.getCategories(), this.getFlows()]);\n const categoryMap = {};\n categories.forEach(c => categoryMap[c._id] = c.name);\n const mappedFlows = flows.map(f => ({\n id: f._id, name: f.name, categoryId: f.category,\n categoryName: categoryMap[f.category] || \"其他流程\" \n }));\n return { categories: categories, flows: mappedFlows };\n },\n \n getFavorites: function() {\n const saved = localStorage.getItem('steedos_fav_ids');\n return saved ? JSON.parse(saved) : [];\n },\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 // --- UI Controller ---\n const AppState = { allFlows: [], categories: [], favorites: [] };\n const sidebarEl = document.getElementById('sidebarList');\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 contentEl.className = \"block w-full h-full pt-4 px-8 pb-10\";\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.className = \"flex h-full w-full flex-col items-center justify-center\";\n contentEl.innerHTML = `<div class=\"animate-[fadeUpSpring_0.5s_ease-out] text-center\"><div class=\"text-gray-200 text-7xl mb-4\">∅</div><div class=\"text-gray-400 text-sm\">未找到匹配流程</div></div>`;\n return;\n }\n\n groups.forEach((group, index) => {\n const groupId = `group-\\${group.id}`;\n \n // Sidebar Item\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 = `\n <span class=\"truncate\">\\${group.isFav ? '★ ' : ''}\\${group.name}</span>\n <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 document.getElementById(groupId)?.scrollIntoView({ behavior: 'smooth', block: 'start' });\n };\n sidebarEl.appendChild(navItem);\n\n // Content Header\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/95 py-3 text-xl font-bold tracking-tight backdrop-blur-xl 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 group.items.forEach((flow, i) => {\n const isFav = AppState.favorites.includes(flow.id);\n const colorMap = [\n 'bg-blue-50 text-blue-600',\n 'bg-orange-50 text-orange-600',\n 'bg-emerald-50 text-emerald-600',\n 'bg-indigo-50 text-indigo-600'\n ];\n const colorClass = colorMap[(flow.name.length + i) % 4];\n const firstChar = flow.name.replace(/【.*?】/g, '').charAt(0) || flow.name.charAt(0);\n\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-all duration-300 ease-out animate-[fadeUpSpring_0.6s_cubic-bezier(0.16,1,0.3,1)_forwards] 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';\n card.style.animationDelay = `\\${Math.min(i * 0.04, 0.6)}s`;\n card.style.opacity = '0'; \n \n const iconClass = isFav \n ? 'text-yellow-400 fill-current' \n : 'text-gray-300 group-hover/btn:text-gray-400 fill-none stroke-current stroke-[1.5]';\n const btnBgClass = isFav\n ? 'opacity-100 hover:scale-110'\n : 'opacity-0 group-hover:opacity-100 hover:bg-gray-100 hover:scale-110';\n\n // 修改点: 添加 top-1/2 -translate-y-1/2 实现绝对垂直居中\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 \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-2 leading-relaxed tracking-tight\" title=\"\\${flow.name}\">\\${flow.name}</div>\n `;\n\n card.onclick = () => {\n setTimeout(() => {\n console.log(\"选中的流程ID:\", flow.id);\n //alert(`准备发起: \\${data.context.user.name}`);\n data._scoped.doAction([\n {\n \"actionType\": \"broadcast\",\n \"args\": {\n \"eventName\": \"flows.selected\"\n },\n \"data\": {\n \"value\": flow.id\n }\n }\n ])\n }, 50);\n };\n\n const starBtn = card.querySelector('.star-btn');\n const starIcon = starBtn.querySelector('svg');\n \n starBtn.onclick = (e) => {\n e.stopPropagation();\n const newFavState = !starBtn.classList.contains('active-fav');\n \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\n AppState.favorites = WorkflowService.toggleFavorite(flow.id, newFavState);\n setTimeout(() => renderUI(searchInput.value), 300);\n };\n \n if (isFav) starBtn.classList.add('active-fav');\n\n grid.appendChild(card);\n });\n\n section.appendChild(grid);\n contentEl.appendChild(section);\n });\n }\n\n searchInput.addEventListener('input', (e) => renderUI(e.target.value.trim()));\n init();\n</script>",
|
|
4
|
+
"className": "h-full"
|
|
5
|
+
}
|
|
@@ -1,104 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "page",
|
|
3
|
-
"body": [
|
|
4
|
-
"type": "wrapper",
|
|
5
|
-
"visibleOn": "${AND(display != 'split', ${window:innerWidth > 768},!!!_inDrawer)}",
|
|
6
|
-
"className": "bg-white p-0 flex-shrink-0 min-w-[240px] lg:order-first lg:flex lg:flex-col rounded shadow my-4 ml-4 mr-1 my-4 sm:rounded",
|
|
7
|
-
"body": [{
|
|
8
|
-
"type": "service",
|
|
9
|
-
"className": "w-full h-full",
|
|
10
|
-
"onEvent": {
|
|
11
|
-
"@data.changed.steedos_keyvalues": {
|
|
12
|
-
"actions": [{
|
|
13
|
-
"actionType": "reload"
|
|
14
|
-
}]
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
"body": [
|
|
18
|
-
{
|
|
19
|
-
"type": "button",
|
|
20
|
-
"label": "刷新",
|
|
21
|
-
"className": "instance-nav-reload hidden",
|
|
22
|
-
"onEvent": {
|
|
23
|
-
"click": {
|
|
24
|
-
"actions": [{
|
|
25
|
-
"actionType": "reload",
|
|
26
|
-
"componentId": "u:instanceNav"
|
|
27
|
-
}]
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
"type": "input-tree",
|
|
33
|
-
"treeContainerClassName": "h-full",
|
|
34
|
-
"name": "tree",
|
|
35
|
-
"className": "instance-box-tree w-full",
|
|
36
|
-
"id": "u:9f3dd961ca12",
|
|
37
|
-
"stacked": true,
|
|
38
|
-
"multiple": false,
|
|
39
|
-
"enableNodePath": false,
|
|
40
|
-
"hideRoot": true,
|
|
41
|
-
"showIcon": true,
|
|
42
|
-
"initiallyOpen": false,
|
|
43
|
-
"virtualThreshold": 100000,
|
|
44
|
-
"value": "/app/${appId}/${objectName}/grid/${listName}",
|
|
45
|
-
"size": "md",
|
|
46
|
-
"onEvent": {
|
|
47
|
-
"change": {
|
|
48
|
-
"actions": [{
|
|
49
|
-
"args": {
|
|
50
|
-
"link": "${event.data.value}",
|
|
51
|
-
"blank": false
|
|
52
|
-
},
|
|
53
|
-
"actionType": "link"
|
|
54
|
-
}]
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
"menuTpl": {
|
|
58
|
-
"type": "wrapper",
|
|
59
|
-
"className": "flex flex-row p-0 m-0",
|
|
60
|
-
"body": [{
|
|
61
|
-
"type": "tpl",
|
|
62
|
-
"className": "flex-1 w-6/12",
|
|
63
|
-
"tpl": "${label}",
|
|
64
|
-
"id": "u:8147616a7b50"
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
"type": "tpl",
|
|
68
|
-
"className": "-mx-11 ",
|
|
69
|
-
"tpl": "",
|
|
70
|
-
"badge": {
|
|
71
|
-
"className": "h-0",
|
|
72
|
-
"offset": [
|
|
73
|
-
-20,
|
|
74
|
-
12
|
|
75
|
-
],
|
|
76
|
-
"mode": "text",
|
|
77
|
-
"text": "${tag | toInt}",
|
|
78
|
-
"overflowCount": 999
|
|
79
|
-
},
|
|
80
|
-
"id": "u:94f078e62361"
|
|
81
|
-
}
|
|
82
|
-
],
|
|
83
|
-
"id": "u:decad07ad26b"
|
|
84
|
-
},
|
|
85
|
-
"unfoldedLevel": 2,
|
|
86
|
-
"source": "${options}"
|
|
87
|
-
}],
|
|
88
|
-
"id": "u:instanceNav",
|
|
89
|
-
"api": {
|
|
90
|
-
"method": "get",
|
|
91
|
-
"url": "${context.rootUrl}/api/${appId}/workflow/nav",
|
|
92
|
-
"headers": {
|
|
93
|
-
"Authorization": "Bearer ${context.tenantId},${context.authToken}"
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
"dsType": "api"
|
|
97
|
-
}],
|
|
98
|
-
"mobile": {
|
|
99
|
-
"visibleOn": "false"
|
|
100
|
-
}
|
|
101
|
-
},
|
|
3
|
+
"body": [
|
|
102
4
|
{
|
|
103
5
|
"type": "button",
|
|
104
6
|
"label": "刷新",
|
|
@@ -1,106 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "page",
|
|
3
3
|
"body": [
|
|
4
|
-
{
|
|
5
|
-
"type": "wrapper",
|
|
6
|
-
"visibleOn": "${AND(display != 'split', ${window:innerWidth > 768})}",
|
|
7
|
-
"className": "bg-white p-0 flex-shrink-0 min-w-[240px] lg:order-first lg:flex lg:flex-col rounded shadow my-4 ml-4 mr-1 my-4 sm:rounded",
|
|
8
|
-
"body": [
|
|
9
|
-
{
|
|
10
|
-
"type": "service",
|
|
11
|
-
"className": "w-full h-full",
|
|
12
|
-
"onEvent": {
|
|
13
|
-
"@data.changed.steedos_keyvalues": {
|
|
14
|
-
"actions": [
|
|
15
|
-
{
|
|
16
|
-
"actionType": "reload"
|
|
17
|
-
}
|
|
18
|
-
]
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
"body": [
|
|
22
|
-
{
|
|
23
|
-
"type": "button",
|
|
24
|
-
"label": "刷新",
|
|
25
|
-
"className": "instance-nav-reload hidden",
|
|
26
|
-
"onEvent": {
|
|
27
|
-
"click": {
|
|
28
|
-
"actions": [{
|
|
29
|
-
"actionType": "reload",
|
|
30
|
-
"componentId": "u:instanceNav"
|
|
31
|
-
}]
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"type": "input-tree",
|
|
37
|
-
"treeContainerClassName": "h-full",
|
|
38
|
-
"name": "tree",
|
|
39
|
-
"className": "instance-box-tree w-full",
|
|
40
|
-
"id": "u:9f3dd961ca12",
|
|
41
|
-
"stacked": true,
|
|
42
|
-
"multiple": false,
|
|
43
|
-
"enableNodePath": false,
|
|
44
|
-
"hideRoot": true,
|
|
45
|
-
"showIcon": true,
|
|
46
|
-
"initiallyOpen": false,
|
|
47
|
-
"virtualThreshold": 100000,
|
|
48
|
-
"value":"/app/${appId}/${objectName}/grid/${listName}",
|
|
49
|
-
"size": "md",
|
|
50
|
-
"onEvent": {
|
|
51
|
-
"change": {
|
|
52
|
-
"actions": [
|
|
53
|
-
{
|
|
54
|
-
"args": {
|
|
55
|
-
"link": "${event.data.value}",
|
|
56
|
-
"blank": false
|
|
57
|
-
},
|
|
58
|
-
"actionType": "link"
|
|
59
|
-
}
|
|
60
|
-
]
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
"menuTpl": {
|
|
64
|
-
"type": "wrapper",
|
|
65
|
-
"className": "flex flex-row p-0 m-0",
|
|
66
|
-
"body": [
|
|
67
|
-
{
|
|
68
|
-
"type": "tpl",
|
|
69
|
-
"className": "flex-1 w-6/12",
|
|
70
|
-
"tpl": "${label}"
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
"type": "tpl",
|
|
74
|
-
"className": "-mx-11 ",
|
|
75
|
-
"tpl": "",
|
|
76
|
-
"badge": {
|
|
77
|
-
"className": "h-0",
|
|
78
|
-
"offset": [
|
|
79
|
-
-20,
|
|
80
|
-
12
|
|
81
|
-
],
|
|
82
|
-
"mode": "text",
|
|
83
|
-
"text": "${tag}",
|
|
84
|
-
"overflowCount": 999
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
]
|
|
88
|
-
},
|
|
89
|
-
"unfoldedLevel": 2,
|
|
90
|
-
"source": "${options}"
|
|
91
|
-
}
|
|
92
|
-
],
|
|
93
|
-
"id": "u:instanceNav",
|
|
94
|
-
"api": {
|
|
95
|
-
"method": "get",
|
|
96
|
-
"url": "${context.rootUrl}/api/${appId}/workflow/nav",
|
|
97
|
-
"headers": {
|
|
98
|
-
"Authorization": "Bearer ${context.tenantId},${context.authToken}"
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
]
|
|
103
|
-
},
|
|
104
4
|
{
|
|
105
5
|
"type": "wrapper",
|
|
106
6
|
"className": "steedos-instance-detail-wrapper m-0 p-0 flex-1 focus:outline-none lg:order-last sm:m-4 shadow sm:rounded",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"bodyClassName": "p-0 flex flex-1 overflow-hidden h-full",
|
|
39
39
|
"name": "amis-root-workflow",
|
|
40
40
|
"initApi": {
|
|
41
|
-
"url": "
|
|
41
|
+
"url": "/api/workflow/v2/instance/${recordId}/permission",
|
|
42
42
|
"method": "get",
|
|
43
43
|
"data": {},
|
|
44
44
|
"requestAdaptor": "",
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const { instanceNumberBuilder } = require("../manager/instance_number_rules");
|
|
4
|
+
const objectql = require('@steedos/objectql');
|
|
5
|
+
const _ = require('lodash');
|
|
6
|
+
const UUFlowManager = require('../manager/uuflow_manager');
|
|
7
|
+
const { requireAuthentication } = require("@steedos/auth");
|
|
8
|
+
|
|
9
|
+
// 获取用户当前的 approve
|
|
10
|
+
const getUserApprove = ({ instance, userId }) => {
|
|
11
|
+
const currentTrace = _.find(instance.traces, (trace) => {
|
|
12
|
+
return trace.is_finished != true;
|
|
13
|
+
});
|
|
14
|
+
let currentApprove = null;
|
|
15
|
+
if (currentTrace) {
|
|
16
|
+
currentApprove = _.find(currentTrace.approves, (approve) => {
|
|
17
|
+
return approve.is_finished != true && approve.handler == userId;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//传阅的approve返回最新一条
|
|
22
|
+
if (!currentApprove || currentApprove.type == "cc") {
|
|
23
|
+
// 当前是传阅
|
|
24
|
+
_.each(instance.traces, function (t) {
|
|
25
|
+
_.each(t.approves, function (a) {
|
|
26
|
+
if (a.type == "cc" && a.handler == userId && a.is_finished == false) {
|
|
27
|
+
currentApprove = a;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!currentApprove) return;
|
|
34
|
+
|
|
35
|
+
if (currentApprove._id) {
|
|
36
|
+
currentApprove.id = currentApprove._id;
|
|
37
|
+
}
|
|
38
|
+
return currentApprove;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// 获取可编辑的自动编号字段
|
|
42
|
+
const getEditableAutoNumberFields = async (step, formId, formVersion) => {
|
|
43
|
+
const permissions = step.permissions || {};
|
|
44
|
+
const form = await UUFlowManager.getForm(formId);
|
|
45
|
+
const formV = await UUFlowManager.getFormVersion(form, formVersion);
|
|
46
|
+
|
|
47
|
+
const autoNumberFields = [];
|
|
48
|
+
|
|
49
|
+
// 检查字段是否是自动编号字段(通过 formula 判断)
|
|
50
|
+
const isAutoNumberField = (field) => {
|
|
51
|
+
return field.default_value && typeof field.default_value === 'string' && field.default_value.trim().startsWith('auto_number(');
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// 从 formula 中提取自动编号规则名称
|
|
55
|
+
const extractAutoNumberName = (formula) => {
|
|
56
|
+
// 匹配 auto_number("xxx") 或 auto_number('xxx') 或 auto_number(xxx)
|
|
57
|
+
const match = formula.match(/auto_number\s*\(\s*['"]?([^'"()]+?)['"]?\s*\)/);
|
|
58
|
+
return match ? match[1].trim() : null;
|
|
59
|
+
};
|
|
60
|
+
for (const field of formV.fields || []) {
|
|
61
|
+
|
|
62
|
+
if (field.type === "section") {
|
|
63
|
+
// 处理 section 中的字段
|
|
64
|
+
for (const sectionField of field.fields || []) {
|
|
65
|
+
if (isAutoNumberField(sectionField) && permissions[sectionField.code] === "editable") {
|
|
66
|
+
const autoNumberName = extractAutoNumberName(sectionField.default_value);
|
|
67
|
+
if (autoNumberName) {
|
|
68
|
+
autoNumberFields.push({
|
|
69
|
+
code: sectionField.code,
|
|
70
|
+
auto_number_name: autoNumberName
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
// 处理普通字段
|
|
77
|
+
if (isAutoNumberField(field) && permissions[field.code] === "editable") {
|
|
78
|
+
const autoNumberName = extractAutoNumberName(field.default_value);
|
|
79
|
+
if (autoNumberName) {
|
|
80
|
+
autoNumberFields.push({
|
|
81
|
+
code: field.code,
|
|
82
|
+
auto_number_name: autoNumberName
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return autoNumberFields;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
router.post('/api/workflow/v2/auto_number', requireAuthentication, async function (req, res) {
|
|
93
|
+
try {
|
|
94
|
+
const userSession = req.user;
|
|
95
|
+
const userId = userSession.userId;
|
|
96
|
+
const { instanceId } = req.body;
|
|
97
|
+
|
|
98
|
+
if (!instanceId) {
|
|
99
|
+
return res.status(200).send({
|
|
100
|
+
error: '缺少必填参数: instanceId'
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 获取 instance 信息
|
|
105
|
+
const instancesCollection = await objectql.getObject('instances');
|
|
106
|
+
const instance = await instancesCollection.findOne(instanceId);
|
|
107
|
+
|
|
108
|
+
if (!instance) {
|
|
109
|
+
return res.status(200).send({
|
|
110
|
+
error: '未找到该实例'
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 获取当前用户的 approve
|
|
115
|
+
const currentApprove = getUserApprove({ instance, userId });
|
|
116
|
+
|
|
117
|
+
if (!currentApprove) {
|
|
118
|
+
return res.status(200).send({
|
|
119
|
+
error: '未找到当前用户的审批节点'
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 如果是传阅类型,则不处理自动编号
|
|
124
|
+
if (currentApprove.type === 'cc') {
|
|
125
|
+
return res.status(200).send({
|
|
126
|
+
success: true,
|
|
127
|
+
message: '传阅节点不处理自动编号'
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 查找当前 trace 和 approve 的位置
|
|
132
|
+
let traceIndex = -1;
|
|
133
|
+
let approveIndex = -1;
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < instance.traces.length; i++) {
|
|
136
|
+
const trace = instance.traces[i];
|
|
137
|
+
if (trace.approves && trace.approves.length > 0) {
|
|
138
|
+
const index = trace.approves.findIndex(app => app._id === currentApprove._id);
|
|
139
|
+
if (index !== -1) {
|
|
140
|
+
traceIndex = i;
|
|
141
|
+
approveIndex = index;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (traceIndex === -1 || approveIndex === -1) {
|
|
148
|
+
return res.status(200).send({
|
|
149
|
+
error: '未找到对应的 trace 和 approve'
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const currentTrace = instance.traces[traceIndex];
|
|
154
|
+
|
|
155
|
+
// 获取当前 step
|
|
156
|
+
const flowsCollection = await objectql.getObject('flows');
|
|
157
|
+
const flow = await flowsCollection.findOne(instance.flow);
|
|
158
|
+
|
|
159
|
+
if (!flow) {
|
|
160
|
+
return res.status(200).send({
|
|
161
|
+
error: '未找到对应的流程'
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let step = null;
|
|
166
|
+
if (flow.current && flow.current.steps) {
|
|
167
|
+
step = flow.current.steps.find(s => s._id === currentTrace.step);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!step && flow.historys) {
|
|
171
|
+
for (const h of flow.historys) {
|
|
172
|
+
step = h.steps.find(s => s._id === currentTrace.step);
|
|
173
|
+
if (step) break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!step) {
|
|
178
|
+
return res.status(200).send({
|
|
179
|
+
error: '未找到对应的步骤'
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 获取可编辑的自动编号字段
|
|
184
|
+
const autoNumberFields = await getEditableAutoNumberFields(step, instance.form, instance.form_version);
|
|
185
|
+
|
|
186
|
+
if (autoNumberFields.length === 0) {
|
|
187
|
+
return res.status(200).send({
|
|
188
|
+
success: true,
|
|
189
|
+
message: '当前步骤没有可编辑的自动编号字段'
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 为每个自动编号字段生成编号
|
|
194
|
+
const result = {};
|
|
195
|
+
const updateObj = {};
|
|
196
|
+
|
|
197
|
+
for (const field of autoNumberFields) {
|
|
198
|
+
// 检查字段是否已有值
|
|
199
|
+
if (currentApprove.values && currentApprove.values[field.code]) {
|
|
200
|
+
result[field.code] = currentApprove.values[field.code];
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 生成自动编号
|
|
205
|
+
const autoNumber = await instanceNumberBuilder(instance.space, field.auto_number_name);
|
|
206
|
+
|
|
207
|
+
if (autoNumber && autoNumber._error) {
|
|
208
|
+
throw autoNumber._error;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
result[field.code] = autoNumber;
|
|
212
|
+
updateObj[`traces.${traceIndex}.approves.${approveIndex}.values.${field.code}`] = autoNumber;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 更新 approve 的 values
|
|
216
|
+
if (Object.keys(updateObj).length > 0) {
|
|
217
|
+
await instancesCollection.update(instanceId, updateObj);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
res.status(200).send({
|
|
221
|
+
success: true,
|
|
222
|
+
fields: result
|
|
223
|
+
});
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error(error);
|
|
226
|
+
res.status(200).send({
|
|
227
|
+
error: error.message
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
exports.default = router;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const { requireAuthentication } = require("@steedos/auth");
|
|
4
|
+
const { getObject } = require('@steedos/objectql')
|
|
5
|
+
const _ = require('lodash');
|
|
6
|
+
|
|
7
|
+
router.delete('/api/workflow/v2/attachment/:instanceId/:id', requireAuthentication, async function (req, res) {
|
|
8
|
+
try {
|
|
9
|
+
const { instanceId, id} = req.params;
|
|
10
|
+
await getObject('cfs_instances_filerecord').delete(id);
|
|
11
|
+
res.status(200).send({
|
|
12
|
+
status: 0
|
|
13
|
+
})
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error(error);
|
|
16
|
+
res.status(200).send({
|
|
17
|
+
error: error.message
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
exports.default = router;
|
|
@@ -30,6 +30,8 @@ router.post('/api/workflow/v2/set_have_read', requireAuthentication, async funct
|
|
|
30
30
|
success: true
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
|
|
33
35
|
const db = {
|
|
34
36
|
instances: await getCollection('instances'),
|
|
35
37
|
instance_tasks: await getCollection('instance_tasks')
|
|
@@ -40,7 +42,8 @@ router.post('/api/workflow/v2/set_have_read', requireAuthentication, async funct
|
|
|
40
42
|
fields: {
|
|
41
43
|
"instance": 1,
|
|
42
44
|
"is_read": 1,
|
|
43
|
-
"type": 1
|
|
45
|
+
"type": 1,
|
|
46
|
+
space: 1
|
|
44
47
|
}
|
|
45
48
|
});
|
|
46
49
|
if(!instance_task){
|
|
@@ -48,6 +51,21 @@ router.post('/api/workflow/v2/set_have_read', requireAuthentication, async funct
|
|
|
48
51
|
success: true
|
|
49
52
|
});
|
|
50
53
|
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// 更新当前用户相关的通知为已读
|
|
57
|
+
const notificationsObj = objectql.getObject('notifications');
|
|
58
|
+
await notificationsObj.directUpdateMany([
|
|
59
|
+
['owner', '=', userId],
|
|
60
|
+
['is_read', '!=', true],
|
|
61
|
+
['related_to.o', '=', 'instances'],
|
|
62
|
+
['related_to.ids', '=', instance_task.instance]
|
|
63
|
+
], {
|
|
64
|
+
is_read: true
|
|
65
|
+
});
|
|
66
|
+
console.log(`b6-microservice.broadcast`, {name: '$notification.users', data: {tenantId: instance_task.space, users: [userId], message: null}});
|
|
67
|
+
await broker.call('b6-microservice.broadcast', {name: '$notification.users', data: {tenantId: instance_task.space, users: [userId], message: null}})
|
|
68
|
+
|
|
51
69
|
if (instance_task.is_read){
|
|
52
70
|
return res.status(200).send({
|
|
53
71
|
success: true
|
|
@@ -63,7 +81,7 @@ router.post('/api/workflow/v2/set_have_read', requireAuthentication, async funct
|
|
|
63
81
|
}
|
|
64
82
|
else{
|
|
65
83
|
await set_approve_have_read(ins._id, myApprove.trace, myApprove._id, { userId });
|
|
66
|
-
}
|
|
84
|
+
}
|
|
67
85
|
return res.status(200).send({
|
|
68
86
|
success: true
|
|
69
87
|
});
|