@steedos-labs/plugin-workflow 3.0.13 → 3.0.15

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/README.md CHANGED
@@ -6,4 +6,27 @@
6
6
  * @Description:
7
7
  -->
8
8
  ## 功能说明
9
- - 审批王服务,包含 流程触发器
9
+ - 审批王服务,包含 流程触发器
10
+
11
+ ## 环境变量配置
12
+
13
+ ### STEEDOS_WORKFLOW_ENABLE_CATEGORY_FILTER
14
+ - **说明**: 控制是否在"待处理"、"监控箱"视图以及左侧导航菜单中按流程分类进行筛选和显示
15
+ - **默认行为**: 启用分类筛选(当环境变量未设置或设置为除 'false' 以外的任何值时)
16
+ - **可选值**:
17
+ - 不设置或设置为除 `'false'` 以外的任何值: 启用按分类筛选,根据应用(app)显示对应分类的流程
18
+ - `'false'` (字符串): 禁用按分类筛选,显示所有流程
19
+ - **使用场景**: 国产化优化,简化流程展示逻辑
20
+ - **影响范围**:
21
+ - 待处理(Pending)和监控箱(Monitor Box)视图的数据筛选
22
+ - 左侧导航菜单结构:禁用时仅显示box层级(待办箱、监控箱等),不显示分类和流程子菜单
23
+
24
+ **示例配置**:
25
+ ```bash
26
+ # 禁用分类筛选
27
+ STEEDOS_WORKFLOW_ENABLE_CATEGORY_FILTER=false
28
+
29
+ # 启用分类筛选(以下任一方式)
30
+ # STEEDOS_WORKFLOW_ENABLE_CATEGORY_FILTER=true
31
+ # 或者不设置该环境变量(默认启用)
32
+ ```
@@ -8,6 +8,7 @@ mobile: true
8
8
  sort: 10
9
9
  # tabs:
10
10
  # - object_instance_tasks
11
+ default_tab: object_instance_tasks
11
12
 
12
13
  showSidebar: true
13
14
  enable_nav_schema: true
@@ -24,12 +25,12 @@ nav_schema: {
24
25
  {
25
26
  "type": "wrapper",
26
27
  "size": "none",
27
- "className": "instances-sidebar-wrapper mt-1",
28
+ "className": "instances-sidebar-wrapper mt-1 bg-white",
28
29
  "body": [
29
30
  {
30
31
  "type": "service",
31
32
  "id": "u:instanceNav",
32
- "className": "bg-none",
33
+ "className": "bg-white",
33
34
  "onEvent": {
34
35
  "@data.changed.instances": {
35
36
  "actions": [
@@ -58,8 +59,8 @@ nav_schema: {
58
59
  {
59
60
  "type": "input-tree",
60
61
  "name": "tree",
61
- "treeContainerClassName": "h-full",
62
- "className": "instance-box-tree h-full w-full p-0",
62
+ "treeContainerClassName": "h-full bg-white",
63
+ "className": "instance-box-tree h-full w-full p-0 bg-white",
63
64
  "id": "u:9f3dd961ca12",
64
65
  "stacked": true,
65
66
  "multiple": false,
@@ -112,23 +113,23 @@ nav_schema: {
112
113
  },
113
114
  "menuTpl": {
114
115
  "type": "wrapper",
115
- "className": "flex flex-row p-0 m-0",
116
+ "className": "flex items-center py-1.5 px-3 m-0 rounded-md transition-colors duration-150",
116
117
  "body": [
117
118
  {
118
119
  "type": "tpl",
119
- "className": "flex-1 w-6/12",
120
+ "className": "flex-1 leading-6 truncate instance-menu-label",
120
121
  "tpl": "${label}",
121
122
  "id": "u:9dee51f00db4"
122
123
  },
123
124
  {
124
125
  "type": "tpl",
125
- "className": "-mx-11 ",
126
+ "className": "ml-auto",
126
127
  "tpl": "",
127
128
  "badge": {
128
129
  "className": "h-0",
129
130
  "offset": [
130
- -20,
131
- 12
131
+ -5,
132
+ 0
132
133
  ],
133
134
  "mode": "text",
134
135
  "text": "${tag | toInt}",
@@ -605,6 +605,7 @@ UUFlowManager.calculateConditionWithAmis = function (values, condition_str) {
605
605
  * @returns {Boolean} Condition result
606
606
  */
607
607
  UUFlowManager.calculateCondition = function (values, condition_str) {
608
+ // console.log('calculateCondition', values, condition_str);
608
609
  try {
609
610
  const __values = values;
610
611
 
@@ -817,10 +818,7 @@ UUFlowManager.initFormulaValues = async function (instance, values) {
817
818
  __values["applicant"] = {
818
819
  roles: await UUFlowManager.getUserRoles(instance.applicant, instance.space),
819
820
  name: instance.applicant_name,
820
- organization: {
821
- fullname: instance.applicant_organization_fullname,
822
- name: instance.applicant_organization_name
823
- },
821
+ organization: await UUFlowManager.getOrganization(instance.applicant_organization),
824
822
  id: instance.applicant
825
823
  };
826
824
 
@@ -23,9 +23,7 @@ module.exports = {
23
23
  },
24
24
  design_form_layoutVisible: function(object_name, record_id, record_permissions, data) {
25
25
  var record = data && data.record;
26
- if (!Steedos.isSpaceAdmin()) {
27
- return false
28
- }
26
+
29
27
  if (!record) {
30
28
  return false;
31
29
  }
@@ -60,6 +60,9 @@ fields:
60
60
  readonly: true
61
61
  visible_on: "{{global.mode !='read' ? false : true}}"
62
62
  name: current_no
63
+ company_id:
64
+ label: Main Division
65
+ visible_on: "{{true}}"
63
66
  description:
64
67
  label: Description
65
68
  type: textarea
@@ -77,8 +80,6 @@ fields:
77
80
  type: textarea
78
81
  is_wide: true
79
82
  name: help_text
80
- company_id:
81
- label: Main Division
82
83
  created_by:
83
84
  label: Created by
84
85
  sort_no: 9999
@@ -906,7 +907,7 @@ permission_set:
906
907
  allowEdit: false
907
908
  allowRead: true
908
909
  modifyAllRecords: false
909
- viewAllRecords: true
910
+ viewAllRecords: false
910
911
  admin:
911
912
  allowCreate: true
912
913
  allowDelete: false
@@ -19,15 +19,15 @@ amis_schema: |-
19
19
  "title": "${'CustomLabels.instance_action_new_dialog_title' | t}",
20
20
  "body": [
21
21
  {
22
- "type": "steedos-select-flow",
23
- "id": "instanceNewFlowSelect",
24
- "showIcon": true,
25
- "showRadio": false,
26
- "onlyLeaf": true,
27
- "name": "flow",
28
- "action": "new",
22
+ "type": "service",
23
+ "dsType": "api",
24
+ "schemaApi": {
25
+ "url": "/api/v6/functions/pages/schema?pageId=flow_selector",
26
+ "method": "get"
27
+ },
28
+ "initFetchSchema": true,
29
29
  "onEvent": {
30
- "change": {
30
+ "flows.selected": {
31
31
  "weight": 0,
32
32
  "actions": [
33
33
  {
@@ -71,7 +71,7 @@ amis_schema: |-
71
71
  "closeOnEsc": true,
72
72
  "closeOnOutside": false,
73
73
  "showCloseButton": true,
74
- "size": "lg",
74
+ "size": "xl",
75
75
  "actions": false
76
76
  }
77
77
  }
@@ -100,7 +100,7 @@ amis_schema: |-
100
100
  ]
101
101
  }
102
102
  },
103
- "hiddenOn": "${${listName}!==\"draft\"}"
103
+ "hiddenOn": "${listName!==\"draft\" || display===\"split\"}"
104
104
  }
105
105
  ],
106
106
  "regions": [
@@ -1,5 +1,5 @@
1
1
  {
2
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>",
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=\"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\" 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 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/95 pb-2 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 = ['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-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 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-2 leading-relaxed tracking-tight\" title=\"\\${flow.name}\">\\${flow.name}</div>\n `;\n card.onclick = () => {\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 grid.appendChild(card);\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>\n",
4
4
  "className": "h-full"
5
5
  }
@@ -26,7 +26,7 @@
26
26
  "schemaApi": {
27
27
  "method": "get",
28
28
  "url": "/api/health_check?trace=${recordId}",
29
- "adaptor": "const result = {data: {'type':'wrapper','className':'p-0 h-full','body':[{'type':'steedos-instance-detail','id':'u:40052b3812c1','label':'Instance Detail','instanceId':context.recordId,'boxName':context.side_listview_id}],'id':'u:829a40757f0a'}};console.log('result===>', result); return result;"
29
+ "adaptor": "const urlParams = new URLSearchParams(location.search); const sideListViewId = urlParams.get('side_listview_id'); const result = {data: {'type':'wrapper','className':'p-0 h-full','body':[{'type':'steedos-instance-detail','id':'u:40052b3812c1','label':'Instance Detail','instanceId':context.recordId,'boxName':sideListViewId}],'id':'u:829a40757f0a'}};return result;"
30
30
  },
31
31
  "body": {},
32
32
  "className": "h-full",
@@ -19,6 +19,11 @@ router.post('/api/workflow/v2/instance/upgrade', requireAuthentication, async fu
19
19
  'flow','form','applicant_name','applicant_organization','applicant_organization_fullname','applicant_organization_name',
20
20
  'code', 'flow_version', 'form_version', 'submit_date', 'flow_name', 'code', 'traces', 'state'
21
21
  ]});
22
+ if(!record){
23
+ return res.status(200).send({
24
+ instance: false
25
+ });
26
+ }
22
27
  if(record.state !== 'draft'){
23
28
  return res.status(200).send({
24
29
  instance: true
@@ -56,54 +56,67 @@ const getCategoriesInbox = async (userSession, req, currentUrl) => {
56
56
  fields: ['_id', 'flow', 'category','flow_name','category_name']
57
57
  }, userSession)
58
58
 
59
+ // Check environment variable to control category filtering
60
+ // Default is enabled (when not set or set to any value except 'false')
61
+ // Only the string 'false' will disable the filtering
62
+ const enableCategoryFilter = process.env.STEEDOS_WORKFLOW_ENABLE_CATEGORY_FILTER !== 'false';
63
+
59
64
  const output = [];
60
- const categoryGroups = lodash.groupBy(data, 'category_name');
61
- lodash.each(categoryGroups, (v, k)=>{
62
- let categoryBadge = 0;
63
- const flowGroups = lodash.groupBy(v, 'flow_name');
64
- const flows = [];
65
- const categoryValue = `/app/${appId}/instance_tasks/view/none?side_object=instance_tasks&side_listview_id=inbox&additionalFilters=['category','=',${v[0].category?"'" + v[0].category + "'":v[0].category}]&flowId=&categoryId=${v[0].category}`;
66
- let categoryIsUnfolded = false;
67
- lodash.each(flowGroups, (v2, k2)=>{
68
- const flowValue = `/app/${appId}/instance_tasks/view/none?side_object=instance_tasks&side_listview_id=inbox&additionalFilters=['flow','=','${v2[0].flow}']&flowId=${v2[0].flow}&categoryId=${v[0].category}`;
69
- let flowIsUnfolded = false;
70
- if(currentUrl == flowValue){
71
- flowIsUnfolded = true;
65
+
66
+ if (!enableCategoryFilter) {
67
+ // When category filtering is disabled, return empty array (no sub-navigation)
68
+ // Only box-level items will be shown
69
+ } else {
70
+ // Original logic with category grouping
71
+ const categoryGroups = lodash.groupBy(data, 'category_name');
72
+ lodash.each(categoryGroups, (v, k)=>{
73
+ let categoryBadge = 0;
74
+ const flowGroups = lodash.groupBy(v, 'flow_name');
75
+ const flows = [];
76
+ const categoryValue = `/app/${appId}/instance_tasks/view/none?side_object=instance_tasks&side_listview_id=inbox&additionalFilters=['category','=',${v[0].category?"'" + v[0].category + "'":v[0].category}]&flowId=&categoryId=${v[0].category}`;
77
+ let categoryIsUnfolded = false;
78
+ lodash.each(flowGroups, (v2, k2)=>{
79
+ const flowValue = `/app/${appId}/instance_tasks/view/none?side_object=instance_tasks&side_listview_id=inbox&additionalFilters=['flow','=','${v2[0].flow}']&flowId=${v2[0].flow}&categoryId=${v[0].category}`;
80
+ let flowIsUnfolded = false;
81
+ if(currentUrl == flowValue){
82
+ flowIsUnfolded = true;
83
+ categoryIsUnfolded = true;
84
+ }
85
+ categoryBadge += v2.length
86
+ flows.push({
87
+ label: k2,
88
+ flow_name: k2,
89
+ tag:v2.length,
90
+ options:{
91
+ level:3,
92
+ value: v2[0].flow,
93
+ name: 'flow',
94
+ to: flowValue,
95
+ },
96
+ value: flowValue,
97
+ unfolded: flowIsUnfolded
98
+ })
99
+ })
100
+ if(currentUrl == categoryValue){
72
101
  categoryIsUnfolded = true;
73
102
  }
74
- categoryBadge += v2.length
75
- flows.push({
76
- label: k2,
77
- flow_name: k2,
78
- tag:v2.length,
79
- options:{
80
- level:3,
81
- value: v2[0].flow,
82
- name: 'flow',
83
- to: flowValue,
103
+ output.push({
104
+ label: k == 'null' || k == 'undefined' || !k ? "未分类" : k,
105
+ children: flows,
106
+ category_name: k == 'null' || k == 'undefined' || !k ? "未分类" : k,
107
+ tag: v.length,
108
+ options: {
109
+ level: 2,
110
+ value: v[0].category,
111
+ name: 'category',
112
+ to: categoryValue,
84
113
  },
85
- value: flowValue,
86
- unfolded: flowIsUnfolded
114
+ value: categoryValue,
115
+ unfolded: categoryIsUnfolded
87
116
  })
88
117
  })
89
- if(currentUrl == categoryValue){
90
- categoryIsUnfolded = true;
91
- }
92
- output.push({
93
- label: k == 'null' || k == 'undefined' || !k ? "未分类" : k,
94
- children: flows,
95
- category_name: k == 'null' || k == 'undefined' || !k ? "未分类" : k,
96
- tag: v.length,
97
- options: {
98
- level: 2,
99
- value: v[0].category,
100
- name: 'category',
101
- to: categoryValue,
102
- },
103
- value: categoryValue,
104
- unfolded: categoryIsUnfolded
105
- })
106
- })
118
+ }
119
+
107
120
  return {
108
121
  schema: output,
109
122
  count: data.length,
@@ -121,119 +134,130 @@ const getCategoriesMonitor = async (userSession, req, currentUrl) => {
121
134
  let output = [];
122
135
  let monitorIsUnfolded = false;
123
136
 
137
+ // Check environment variable to control category filtering
138
+ // Default is enabled (when not set or set to any value except 'false')
139
+ // Only the string 'false' will disable the filtering
140
+ const enableCategoryFilter = process.env.STEEDOS_WORKFLOW_ENABLE_CATEGORY_FILTER !== 'false';
141
+
124
142
  try {
125
- // const sa = new Date().getTime();
126
- const apps = await objectql.getObject('apps').find({filters: ['space', '=', userSession.spaceId], fields: ['_id', 'code']});
127
- // console.log(`find apps`, new Date().getTime() - sa)
128
- const appsMap = new Map(Object.entries(lodash.keyBy(apps, '_id')));
129
- // const sc = new Date().getTime();
130
- const categories = await objectql.getObject('categories').find({filters: ["space", "=", `${userSession.spaceId}`], sort: "sort_no desc"})
131
- // console.log(`find categories`, new Date().getTime() - sc)
132
- for (const item of categories) {
133
- if(item.app){
134
- item.app__expand = appsMap.get(item.app)
135
- }else{
136
- item.app__expand = {}
143
+ let flows = [];
144
+
145
+ if (!enableCategoryFilter) {
146
+ // When category filtering is disabled, return empty array (no sub-navigation)
147
+ // Only box-level items will be shown
148
+ // Still need to check permissions for hasFlowsPer (used to control monitor box visibility)
149
+ if (!hasFlowsPer) {
150
+ const flowIds = await WorkflowManager.getMyAdminOrMonitorFlows(userSession.spaceId, userSession.userId);
151
+ hasFlowsPer = flowIds && flowIds.length > 0;
152
+ }
153
+ } else {
154
+ // Original logic with category grouping
155
+ const apps = await objectql.getObject('apps').find({filters: ['space', '=', userSession.spaceId], fields: ['_id', 'code']});
156
+ const appsMap = new Map(Object.entries(lodash.keyBy(apps, '_id')));
157
+ const categories = await objectql.getObject('categories').find({filters: ["space", "=", `${userSession.spaceId}`], sort: "sort_no desc"})
158
+ for (const item of categories) {
159
+ if (item.app) {
160
+ item.app__expand = appsMap.get(item.app)
161
+ } else {
162
+ item.app__expand = {}
163
+ }
137
164
  }
138
- }
139
165
 
140
- let currentAppCategories = [];
141
- if(appId == "approve_workflow"){
142
- currentAppCategories = categories;
143
- }else{
144
- currentAppCategories = lodash.filter(categories, (category) => {
145
- if(category.app__expand?.code == appId) return true;
146
- else return false;
147
- })
148
- if(currentAppCategories.length == 0) {
149
- //如果没有任何分类绑定该app,则该app显示所有分类(该规则为审批王规则)
166
+ let currentAppCategories = [];
167
+ if (appId == "approve_workflow") {
150
168
  currentAppCategories = categories;
169
+ } else {
170
+ currentAppCategories = lodash.filter(categories, (category) => {
171
+ if (category.app__expand?.code == appId) return true;
172
+ else return false;
173
+ })
174
+ if (currentAppCategories.length == 0) {
175
+ //如果没有任何分类绑定该app,则该app显示所有分类(该规则为审批王规则)
176
+ currentAppCategories = categories;
177
+ }
151
178
  }
152
- }
153
- let categoriesIds = lodash.map(currentAppCategories, '_id');
154
- // console.log(`getCategoriesMonitor categoriesIds`, categoriesIds)
155
- // console.log(`getCategoriesMonitor hasFlowsPer`, hasFlowsPer)
156
- let flows = [];
157
- if (!hasFlowsPer) {
158
- const flowIds = await WorkflowManager.getMyAdminOrMonitorFlows(userSession.spaceId, userSession.userId);
159
- hasFlowsPer = flowIds && flowIds.length > 0;
160
- if (hasFlowsPer) {
179
+ let categoriesIds = lodash.map(currentAppCategories, '_id');
180
+
181
+ if (!hasFlowsPer) {
182
+ const flowIds = await WorkflowManager.getMyAdminOrMonitorFlows(userSession.spaceId, userSession.userId);
183
+ hasFlowsPer = flowIds && flowIds.length > 0;
184
+ if (hasFlowsPer) {
185
+ flows = await objectql.getObject('flows').find({
186
+ filters: [["_id","in", flowIds],"and",["category","in", categoriesIds], "and", ["state", "=", "enabled"]],
187
+ fields: ['_id', 'name', 'category', 'sort_no'],
188
+ sort:"sort_no desc"
189
+ });
190
+ }
191
+ } else {
161
192
  flows = await objectql.getObject('flows').find({
162
- filters: [["_id","in", flowIds],"and",["category","in", categoriesIds], "and", ["state", "=", "enabled"]],
193
+ filters:[["space", "=", userSession.spaceId],["category","in", categoriesIds],["state", "=", "enabled"]],
163
194
  fields: ['_id', 'name', 'category', 'sort_no'],
164
195
  sort:"sort_no desc"
165
- });
196
+ })
166
197
  }
167
- } else {
168
- // const s1 = new Date().getTime();
169
- flows = await objectql.getObject('flows').find({
170
- filters:[["space", "=", userSession.spaceId],["category","in", categoriesIds],["state", "=", "enabled"]],
171
- fields: ['_id', 'name', 'category', 'sort_no'],
172
- sort:"sort_no desc"
173
- })
174
- // console.log(`find flows`, new Date().getTime() - s1)
175
- }
176
- if (flows.length > 0) {
177
- const categoriesMap = new Map(Object.entries(lodash.keyBy(categories, '_id')));
198
+
199
+ if (flows.length > 0) {
200
+ const categoriesMap = new Map(Object.entries(lodash.keyBy(categories, '_id')));
178
201
 
179
- for (const item of flows) {
180
- if(item.category){
181
- item.category__expand = categoriesMap.get(item.category)
182
- }else{
183
- item.category__expand = {}
202
+ for (const item of flows) {
203
+ if (item.category) {
204
+ item.category__expand = categoriesMap.get(item.category)
205
+ } else {
206
+ item.category__expand = {}
207
+ }
184
208
  }
185
- }
186
209
 
187
- const categoryGroups = lodash.groupBy(flows, 'category__expand.name');
188
- lodash.each(categoryGroups, (v, k) => {
189
- const flowGroups = lodash.groupBy(v, 'name');
190
- const flows = [];
191
- const categoryValue = `/app/${appId}/instances/view/none?side_object=instances&side_listview_id=monitor&additionalFilters=['category','=',${v[0].category__expand?"'" + v[0].category__expand._id + "'":null}]&flowId=&categoryId=${v[0].category__expand && v[0].category__expand._id}`;
192
- let categoryIsUnfolded = false;
193
- lodash.each(flowGroups, (v2, k2) => {
194
- const flowValue = `/app/${appId}/instances/view/none?side_object=instances&side_listview_id=monitor&additionalFilters=['flow','=','${v2[0]._id}']&flowId=${v2[0]._id}&categoryId=${v[0].category__expand && v[0].category__expand._id}`;
195
- let flowIsUnfolded = false;
196
- if(currentUrl == flowValue){
197
- flowIsUnfolded = true;
210
+ const categoryGroups = lodash.groupBy(flows, 'category__expand.name');
211
+ lodash.each(categoryGroups, (v, k) => {
212
+ const flowGroups = lodash.groupBy(v, 'name');
213
+ const flows = [];
214
+ const categoryValue = `/app/${appId}/instances/view/none?side_object=instances&side_listview_id=monitor&additionalFilters=['category','=',${v[0].category__expand?"'" + v[0].category__expand._id + "'":null}]&flowId=&categoryId=${v[0].category__expand && v[0].category__expand._id}`;
215
+ let categoryIsUnfolded = false;
216
+ lodash.each(flowGroups, (v2, k2) => {
217
+ const flowValue = `/app/${appId}/instances/view/none?side_object=instances&side_listview_id=monitor&additionalFilters=['flow','=','${v2[0]._id}']&flowId=${v2[0]._id}&categoryId=${v[0].category__expand && v[0].category__expand._id}`;
218
+ let flowIsUnfolded = false;
219
+ if (currentUrl == flowValue) {
220
+ flowIsUnfolded = true;
221
+ categoryIsUnfolded = true;
222
+ monitorIsUnfolded = true;
223
+ }
224
+ flows.push({
225
+ label: k2,
226
+ flow_name: k2,
227
+ options: {
228
+ level: 3,
229
+ value: v2[0]._id,
230
+ name: 'flow',
231
+ to: flowValue,
232
+ },
233
+ value: flowValue,
234
+ unfolded: flowIsUnfolded
235
+ })
236
+ })
237
+ if (currentUrl == categoryValue) {
198
238
  categoryIsUnfolded = true;
199
239
  monitorIsUnfolded = true;
200
240
  }
201
- flows.push({
202
- label: k2,
203
- flow_name: k2,
241
+ output.push({
242
+ label: k == 'null' || k == 'undefined' || !k? "未分类" : k,
243
+ children: flows,
244
+ category_name: k == 'null' || k == 'undefined' || !k ? "未分类" : k,
204
245
  options: {
205
- level: 3,
206
- value: v2[0]._id,
207
- name: 'flow',
208
- to: flowValue,
246
+ level: 2,
247
+ value: v[0].category__expand && v[0].category__expand._id,
248
+ name: 'category',
249
+ to: categoryValue,
209
250
  },
210
- value: flowValue,
211
- unfolded: flowIsUnfolded
251
+ value: categoryValue,
252
+ unfolded: categoryIsUnfolded
212
253
  })
213
254
  })
214
- if(currentUrl == categoryValue){
215
- categoryIsUnfolded = true;
216
- monitorIsUnfolded = true;
217
- }
218
- output.push({
219
- label: k == 'null' || k == 'undefined' || !k? "未分类" : k,
220
- children: flows,
221
- category_name: k == 'null' || k == 'undefined' || !k ? "未分类" : k,
222
- options: {
223
- level: 2,
224
- value: v[0].category__expand && v[0].category__expand._id,
225
- name: 'category',
226
- to: categoryValue,
227
- },
228
- value: categoryValue,
229
- unfolded: categoryIsUnfolded
230
- })
231
- })
232
- output = lodash.sortBy(output, [function (o) {
233
- return lodash.findIndex(categories, (e) => {
234
- return e._id == o.options.value;
235
- });
236
- }]);
255
+ output = lodash.sortBy(output, [function (o) {
256
+ return lodash.findIndex(categories, (e) => {
257
+ return e._id == o.options.value;
258
+ });
259
+ }]);
260
+ }
237
261
  }
238
262
  } catch (error) {
239
263
  console.log(error)
@@ -302,7 +302,22 @@
302
302
  tpl.type = "input-text";
303
303
  }
304
304
  if(field.formula){
305
- tpl.value = `$${field.formula}`;
305
+ if(field.formula.startsWith('{') && (field.formula.endsWith('}') || field.formula.indexOf("}") > 0)){
306
+ // {申请人姓名}.organization.fullname 转换为 ${申请人姓名.organization.fullname}
307
+ // {申请人姓名.organization.fullname} 转换为 ${申请人姓名.organization.fullname}
308
+ if(field.formula.indexOf("}.") > 0){
309
+ // {申请人姓名}.organization.fullname
310
+ let formula = field.formula;
311
+ formula = formula.substring(1, formula.indexOf("}"));
312
+ tpl.value = `\${${formula}${field.formula.substring( field.formula.indexOf("}") + 1 )}}`;
313
+ } else {
314
+ // {申请人姓名.organization.fullname}
315
+ tpl.value = `$${field.formula}`;
316
+ }
317
+ }else{
318
+ // 替换掉所有双引号"
319
+ tpl.value = field.formula.replace(/"/g, '');
320
+ }
306
321
  }
307
322
  break;
308
323
  case "number":
@@ -1,8 +1,8 @@
1
1
  /*
2
2
  * @Author: baozhoutao@steedos.com
3
3
  * @Date: 2023-01-14 11:31:56
4
- * @LastEditors: baozhoutao@steedos.com
5
- * @LastEditTime: 2023-10-18 09:50:42
4
+ * @LastEditors: 殷亮辉 yinlianghui@hotoa.com
5
+ * @LastEditTime: 2026-01-15 13:28:24
6
6
  * @Description:
7
7
  */
8
8
  const objectql = require("@steedos/objectql");
@@ -31,6 +31,7 @@ module.exports = {
31
31
  },
32
32
  actions: {
33
33
  flows__getList: {
34
+ rest: "GET /getList",
34
35
  graphql: {
35
36
  query: `
36
37
  #按权限获取flows数据
@@ -283,7 +283,16 @@ module.exports = {
283
283
  },
284
284
  getAppCategoriesIds: {
285
285
  async handler(appId) {
286
- if(appId == "approve_workflow"){
286
+ // Check environment variable to control category filtering
287
+ // Default is enabled (when not set or set to any value except 'false')
288
+ // Only the string 'false' will disable the filtering
289
+ const enableCategoryFilter = process.env.STEEDOS_WORKFLOW_ENABLE_CATEGORY_FILTER !== 'false';
290
+
291
+ if (!enableCategoryFilter) {
292
+ return [];
293
+ }
294
+
295
+ if (appId == "approve_workflow") {
287
296
  return [];
288
297
  }
289
298
  const categories = await objectql.getObject('categories').directFind({ filters: [['app', '=', appId]] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steedos-labs/plugin-workflow",
3
- "version": "3.0.13",
3
+ "version": "3.0.15",
4
4
  "main": "package.service.js",
5
5
  "license": "MIT",
6
6
  "scripts": {
@@ -52,7 +52,27 @@
52
52
 
53
53
  .instance-box-tree .antd-TreeControl{
54
54
  padding: 0;
55
+ background: #ffffff;
56
+ }
57
+
58
+ /* Remove default tree styling to allow custom styling */
59
+ .instance-box-tree .antd-Tree-itemLabel-item{
60
+ padding: 0 !important;
61
+ margin: 0 !important;
62
+ display: flex !important;
63
+ align-items: center !important;
64
+ width: 100%; /* Ensure it spans the width */
55
65
  }
66
+
67
+ /* Ensure the label container is also flex centered and full width */
68
+ .instance-box-tree .antd-Tree-itemLabel {
69
+ display: flex !important;
70
+ align-items: center !important;
71
+ width: 100%;
72
+ padding-right: 4px;
73
+ }
74
+
75
+ /* Tree item wrapper - removed margin to allow full-row background */
56
76
  /* .instance-box-tree .antd-TreeControl{
57
77
  padding: 0;
58
78
  border-bottom: 0px;
@@ -78,17 +98,42 @@
78
98
  .instance-box-tree .antd-Tree-itemIcon{
79
99
  margin-right: 4px;
80
100
  } */
101
+ /* Fix Badge layout: force it to flow naturally in flex layout to display full content for large numbers like 9999 */
102
+ /* Also ensures it stays aligned to the right of the text label without overlapping */
103
+ .instance-box-tree .antd-Badge {
104
+ height: auto !important;
105
+ width: auto !important;
106
+ position: static !important;
107
+ transform: none !important;
108
+ flex-shrink: 0;
109
+ margin-left: 4px;
110
+ display: flex;
111
+ align-items: center;
112
+ }
113
+
81
114
  .instance-box-tree .antd-Badge-text{
82
- background: #D3D6DD;
115
+ background: #e5e7eb;
116
+ color: #4b5563;
117
+ font-size: 11px;
118
+ padding: 0px 6px;
119
+ border-radius: 10px;
120
+ font-weight: 500;
121
+ line-height: 18px;
122
+ min-width: 18px;
123
+ display: inline-block;
124
+ text-align: center;
125
+ position: static !important;
126
+ transform: none !important;
83
127
  }
84
128
  .instance-box-tree .antd-Tree-item:nth-child(1) .antd-Badge-text{
85
- background: #DF4D46;
129
+ background: #ef4444;
130
+ color: #ffffff;
86
131
  }
87
132
  /* .instance-box-tree .antd-Tree-itemLabel-item{
88
133
  height: 1.6rem;
89
134
  } */
90
135
  .instance-box-tree .antd-TplField span{
91
- width: 85%;
136
+ /* width: 85%; */
92
137
  display: block;
93
138
  overflow: hidden;
94
139
  white-space: nowrap;
@@ -115,12 +160,157 @@
115
160
  border: none !important;
116
161
  }
117
162
 
118
- .instance-box-tree .antd-Tree-item .is-checked{
119
- background-color: rgba(21,137,238,.1);
163
+ /* Base styling for all menu labels - must be specific enough */
164
+ .instance-box-tree .antd-Tree .instance-menu-label {
165
+ font-weight: 400;
166
+ color: #6b7280;
167
+ }
168
+
169
+ /* Tree item spacing for proper layout */
170
+ .instance-box-tree .antd-Tree-item {
171
+ margin: 2px 8px;
172
+ }
173
+
174
+ /* Selected state styling - Using robust pseudo-element strategy for cross-browser support */
175
+
176
+ /* 1. Reset positions to ensure clean background layering */
177
+ .instance-box-tree .antd-Tree-item {
178
+ margin: 2px 8px; /* Maintain standard margin */
179
+ position: relative !important; /* Context for absolute positioning */
180
+ z-index: 0; /* Create stacking context so -1 doesn't go behind the white container */
181
+ }
182
+
183
+ /* 2. Remove ANY positioning from the label so it doesn't trap the pseudo-element */
184
+ .instance-box-tree .antd-Tree-itemLabel.is-checked {
185
+ background-color: transparent !important;
186
+ position: static !important;
120
187
  }
121
188
 
122
- .instance-box-tree .antd-Tree-item .is-checked .antd-Tree-itemLabel-item{
123
- background-color: unset;
189
+ /* 3. Helper to make label static if checked is on item */
190
+ .instance-box-tree .antd-Tree-item.is-checked > .antd-Tree-itemLabel {
191
+ background-color: transparent !important;
192
+ position: static !important;
193
+ }
194
+
195
+ /* 4. The background pseudo-element */
196
+ .instance-box-tree .antd-Tree-itemLabel.is-checked::before,
197
+ .instance-box-tree .antd-Tree-item.is-checked > .antd-Tree-itemLabel::before {
198
+ content: "";
199
+ position: absolute;
200
+ left: 0;
201
+ right: 0;
202
+ top: 0; /* Cover full height (margin handles the spacing) */
203
+ bottom: 0;
204
+ background-color: var(--color-brand-50, #dbeafe);
205
+ border-radius: 6px;
206
+ z-index: -1; /* Behind text */
207
+ }
208
+
209
+ .instance-box-tree .antd-Tree-itemLabel.is-checked .instance-menu-label,
210
+ .instance-box-tree .antd-Tree-item.is-checked .instance-menu-label {
211
+ color: var(--Menu-light-fontColor-onHover, #1e40af) !important;
212
+ }
213
+
214
+ .instance-box-tree .antd-Tree-itemLabel.is-checked .antd-Tree-itemIcon,
215
+ .instance-box-tree .antd-Tree-item.is-checked .antd-Tree-itemIcon {
216
+ color: var(--Menu-light-fontColor-onHover, #1e40af) !important;
217
+ }
218
+
219
+ /* Hover state - entire row with light gray background */
220
+ .instance-box-tree .antd-Tree-item:hover {
221
+ background-color: #f3f4f6;
222
+ border-radius: 6px;
223
+ margin: 2px 8px;
224
+ }
225
+
226
+ .instance-box-tree .antd-Tree-item.is-checked:hover {
227
+ background-color: var(--color-brand-50, #dbeafe) !important;
228
+ }
229
+
230
+ /* Group title styling - larger and bolder */
231
+ .instance-box-tree .antd-Tree .antd-Tree-item[aria-level="1"] .instance-menu-label {
232
+ font-size: 13px !important;
233
+ font-weight: 600;
234
+ color: #374151;
235
+ }
236
+ --isL
237
+ /* Child item (non-leaf) styling - normal size and weight */
238
+ .instance-box-tree .antd-Tree .antd-Tree-item[aria-level="2"]:not(.is-leaf) .instance-menu-label {
239
+ font-size: 13px !important;
240
+ font-weight: 400;
241
+ color: #6b7280;
242
+ }
243
+
244
+ /* Leaf node styling - darker and larger for prominence */
245
+ .instance-box-tree .antd-Tree .antd-Tree-item--isLeaf .instance-menu-label {
246
+ font-size: 14px !important;
247
+ font-weight: 600 !important;
248
+ color: rgb(71 85 105 / var(--tw-text-opacity, 1)) !important;
249
+ }
250
+
251
+ /* Override leaf node color when selected */
252
+ .instance-box-tree .antd-Tree .antd-Tree-item--isLeaf .antd-Tree-itemLabel.is-checked .instance-menu-label {
253
+ color: var(--Menu-light-fontColor-onHover, #1e40af) !important;
254
+ font-weight: 600 !important;
255
+ }
256
+
257
+ /* Fix alignment and spacing for the custom menu template wrapper */
258
+ .instance-box-tree .antd-Tree-itemText > div {
259
+ padding-top: 0px !important; /* Reduced from 4px to fix "text too low" */
260
+ padding-bottom: 0px !important;
261
+ padding-right: 0px !important; /* Reduced to move content closer to right edge */
262
+ line-height: 24px; /* Explicit line height to ensure centering */
263
+ display: flex;
264
+ align-items: center;
265
+ }
266
+
267
+ /* Ensure the text container takes available width */
268
+ .instance-box-tree .antd-Tree-itemText {
269
+ flex: 1;
270
+ display: flex;
271
+ align-items: center;
272
+ position: relative; /* Ensure z-index works if needed */
273
+ z-index: 1;
274
+ min-width: 0; /* Enable truncation */
275
+ }
276
+
277
+ /* Icon styling */
278
+ .instance-box-tree .antd-Tree-itemIcon{
279
+ color: #6b7280;
280
+ margin-right: 0px;
281
+ }
282
+
283
+ /* Hide icons for leaf nodes at level >= 3 (final menu items within category groups) */
284
+ .instance-box-tree .antd-Tree .antd-Tree-item--isLeaf[style*="calc(2 *"] .antd-Tree-itemIcon,
285
+ .instance-box-tree .antd-Tree .antd-Tree-item--isLeaf[style*="calc(3 *"] .antd-Tree-itemIcon,
286
+ .instance-box-tree .antd-Tree .antd-Tree-item--isLeaf[style*="calc(4 *"] .antd-Tree-itemIcon,
287
+ .instance-box-tree .antd-Tree .antd-Tree-item--isLeaf[style*="calc(5 *"] .antd-Tree-itemIcon {
288
+ display: none;
289
+ margin-right: 0px;
290
+ }
291
+
292
+ /* Reduce left padding for leaf nodes to compensate for hidden icon */
293
+ .instance-box-tree .antd-Tree .antd-Tree-item--isLeaf .antd-Tree-itemLabel{
294
+ padding-left: 0;
295
+ }
296
+
297
+ /* Hide arrow placeholder for leaf nodes at level >= 3 to align text left without shifting container */
298
+ .instance-box-tree .antd-Tree .antd-Tree-item--isLeaf[style*="calc(2 *"] .antd-Tree-itemArrowPlaceholder,
299
+ .instance-box-tree .antd-Tree .antd-Tree-item--isLeaf[style*="calc(3 *"] .antd-Tree-itemArrowPlaceholder,
300
+ .instance-box-tree .antd-Tree .antd-Tree-item--isLeaf[style*="calc(4 *"] .antd-Tree-itemArrowPlaceholder,
301
+ .instance-box-tree .antd-Tree .antd-Tree-item--isLeaf[style*="calc(5 *"] .antd-Tree-itemArrowPlaceholder {
302
+ width: 22px;
303
+ }
304
+
305
+ /* Arrow styling */
306
+ .instance-box-tree .antd-Tree-itemArrow{
307
+ color: #9ca3af;
308
+ transition: transform 0.2s ease;
309
+ margin-right: 4px;
310
+ }
311
+
312
+ .instance-box-tree .antd-Tree-item.is-expanded > .antd-Tree-itemArrow{
313
+ transform: rotate(90deg);
124
314
  }
125
315
 
126
316
 
@@ -360,7 +550,7 @@ tbody .color-priority-muted *{
360
550
  }
361
551
 
362
552
  .steedos-instance-detail-wrapper{
363
- height: calc(100vh - 84px);
553
+ height: calc(100vh - 101px);
364
554
  }
365
555
 
366
556
  .steedos-amis-instance-view-body .antd-Panel-title{
@@ -372,10 +562,27 @@ tbody .color-priority-muted *{
372
562
  z-index: 900;
373
563
  }
374
564
 
375
- /* .instances-sidebar-wrapper{
376
- overflow-y: auto;
377
- height: calc(100vh - 50px);
378
- } */
565
+ /* Sidebar wrapper - no scrolling as parent handles it */
566
+ .instances-sidebar-wrapper{
567
+ /* Removed overflow and height - parent layer handles scrolling */
568
+ }
569
+
570
+ .instances-sidebar-wrapper::-webkit-scrollbar {
571
+ width: 6px;
572
+ }
573
+
574
+ .instances-sidebar-wrapper::-webkit-scrollbar-track {
575
+ background: transparent;
576
+ }
577
+
578
+ .instances-sidebar-wrapper::-webkit-scrollbar-thumb {
579
+ background: #d1d5db;
580
+ border-radius: 3px;
581
+ }
582
+
583
+ .instances-sidebar-wrapper::-webkit-scrollbar-thumb:hover {
584
+ background: #9ca3af;
585
+ }
379
586
 
380
587
  .steedos-instance-related-view-wrapper{
381
588
  .antd-Page-header{
@@ -383,6 +590,12 @@ tbody .color-priority-muted *{
383
590
  }
384
591
  }
385
592
 
593
+ .instance-form pre{
594
+ white-space: pre-wrap; /* 保留空白符,允许换行 */
595
+ word-wrap: break-word; /* 长单词或URL换行 */
596
+ overflow-wrap: break-word; /* 更现代的属性 */
597
+ }
598
+
386
599
 
387
600
  /* 公共打印隐藏样式 */
388
601
  @media print {
@@ -408,4 +621,19 @@ tbody .color-priority-muted *{
408
621
  .font-normal{
409
622
  border: none !important;
410
623
  }
411
- }
624
+ }
625
+
626
+ /* .steedos-instance-detail-wrapper{
627
+ height: auto !important;
628
+ }
629
+
630
+ .steedos-amis-instance-view-body{
631
+ height: auto !important;
632
+ }
633
+ .creator-content-wrapper{
634
+ overflow: unset !important;
635
+ }
636
+
637
+ body{
638
+ overflow: auto !important;
639
+ } */