@steedos-labs/plugin-workflow 3.0.24 → 3.0.26

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.
@@ -4265,7 +4265,6 @@ UUFlowManager.draft_save_instance = async function (ins, userId) {
4265
4265
  submitter: 1,
4266
4266
  traces: 1,
4267
4267
  form: 1,
4268
- form_version: 1,
4269
4268
  flow_version: 1,
4270
4269
  space: 1,
4271
4270
  flow: 1
@@ -4344,65 +4343,6 @@ UUFlowManager.draft_save_instance = async function (ins, userId) {
4344
4343
  }
4345
4344
  }
4346
4345
 
4347
- // Convert date/datetime field values from ISO strings to Date objects
4348
- // Fetch form to check field types and get name formula
4349
- // For draft save, always use current form version (not historys which could be very large)
4350
- const form = await db.forms.findOne(
4351
- { _id: form_id },
4352
- { projection: { 'current._id': 1, 'current.fields': 1, 'current.name_forumla': 1 } }
4353
- );
4354
-
4355
- // Draft save always uses current form version
4356
- const formVersion = form?.current;
4357
-
4358
- // Convert date strings to Date objects for proper MongoDB storage
4359
- if (formVersion && formVersion.fields && values) {
4360
- const convertFieldValue = (field, value) => {
4361
- if (!value) return value;
4362
-
4363
- // Handle date and datetime fields - convert ISO strings to Date objects
4364
- if (field.type === 'date' || field.type === 'datetime') {
4365
- // Check if value is a string (ISO date format from frontend)
4366
- if (typeof value === 'string') {
4367
- const dateObj = new Date(value);
4368
- // Validate it's a valid date
4369
- if (!isNaN(dateObj.getTime())) {
4370
- return dateObj;
4371
- }
4372
- }
4373
- }
4374
- // Handle table fields (subforms)
4375
- else if (field.type === 'table' && Array.isArray(value) && field.fields) {
4376
- return value.map(row => {
4377
- const convertedRow = {...row};
4378
- field.fields.forEach(subField => {
4379
- if (row[subField.code] !== undefined) {
4380
- convertedRow[subField.code] = convertFieldValue(subField, row[subField.code]);
4381
- }
4382
- });
4383
- return convertedRow;
4384
- });
4385
- }
4386
-
4387
- return value;
4388
- };
4389
-
4390
- // Process all form fields
4391
- formVersion.fields.forEach(field => {
4392
- if (field.type === 'section' && field.fields) {
4393
- // Process section's child fields
4394
- field.fields.forEach(sectionField => {
4395
- if (values[sectionField.code] !== undefined) {
4396
- values[sectionField.code] = convertFieldValue(sectionField, values[sectionField.code]);
4397
- }
4398
- });
4399
- } else if (values[field.code] !== undefined) {
4400
- // Process regular fields
4401
- values[field.code] = convertFieldValue(field, values[field.code]);
4402
- }
4403
- });
4404
- }
4405
-
4406
4346
  setObj[key_str + 'values'] = values;
4407
4347
  setObj[key_str + 'description'] = description;
4408
4348
  setObj[key_str + 'judge'] = 'submitted';
@@ -4413,6 +4353,11 @@ UUFlowManager.draft_save_instance = async function (ins, userId) {
4413
4353
  }
4414
4354
 
4415
4355
  // Calculate instance name
4356
+ const form = await db.forms.findOne(
4357
+ { _id: form_id },
4358
+ { projection: { 'current.name_forumla': 1 } }
4359
+ );
4360
+
4416
4361
  if (form?.current?.name_forumla) {
4417
4362
  setObj.name = await UUFlowManager.getInstanceName(ins, values);
4418
4363
  if(result !== 'upgraded'){
@@ -21,8 +21,11 @@ amis_schema: |-
21
21
  {
22
22
  "type": "service",
23
23
  "dsType": "api",
24
+ "data": {
25
+ "isMobile": "${window:innerWidth <= 768}"
26
+ },
24
27
  "schemaApi": {
25
- "url": "/api/v6/functions/pages/schema?pageId=flow_selector",
28
+ "url": "${isMobile ? '/api/v6/functions/pages/schema?pageId=flow_selector_mobile' : '/api/v6/functions/pages/schema?pageId=flow_selector'}",
26
29
  "method": "get"
27
30
  },
28
31
  "initFetchSchema": true,
@@ -21,8 +21,11 @@ amis_schema: |-
21
21
  {
22
22
  "type": "service",
23
23
  "dsType": "api",
24
+ "data": {
25
+ "isMobile": "${window:innerWidth <= 768}"
26
+ },
24
27
  "schemaApi": {
25
- "url": "/api/v6/functions/pages/schema?pageId=flow_selector",
28
+ "url": "${isMobile ? '/api/v6/functions/pages/schema?pageId=flow_selector_mobile' : '/api/v6/functions/pages/schema?pageId=flow_selector'}",
26
29
  "method": "get"
27
30
  },
28
31
  "initFetchSchema": true,
@@ -83,6 +83,17 @@ amis_schema: |-
83
83
  },
84
84
  "actionType": "ajax"
85
85
  },
86
+ {
87
+ "actionType": "broadcast",
88
+ "args": {
89
+ "eventName": "@data.@instanceDetail.changed"
90
+ },
91
+ "data": {}
92
+ },
93
+ {
94
+ "actionType": "custom",
95
+ "script": "$('.steedos-workflow-reload-btn').trigger('click')"
96
+ },
86
97
  {
87
98
  "componentId": "",
88
99
  "args": {},
@@ -0,0 +1,5 @@
1
+ {
2
+ "type": "liquid",
3
+ "template": "<style>\n @keyframes fadeUpSpring {\n 0% { opacity: 0; transform: translateY(10px); }\n 100% { opacity: 1; transform: translateY(0); }\n }\n \n /* Mobile-optimized scrollbars */\n ::-webkit-scrollbar {\n width: 4px;\n height: 4px;\n }\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n ::-webkit-scrollbar-thumb {\n background-color: rgba(0, 0, 0, 0.2);\n border-radius: 4px;\n }\n\n /* Force full screen modal for mobile */\n .antd-Modal:has(#steedosFlowSelectorMobile) {\n top: 0 !important;\n margin: 0 !important;\n padding: 0 !important;\n max-width: 100vw !important;\n width: 100vw !important;\n height: 100vh !important;\n height: 100dvh !important;\n }\n\n .antd-Modal-content:has(#steedosFlowSelectorMobile) {\n height: 100vh !important;\n height: 100dvh !important;\n padding-bottom: 0 !important;\n display: flex !important;\n flex-direction: column !important;\n border-radius: 0 !important;\n overflow: hidden !important;\n }\n\n .antd-Modal-body:has(#steedosFlowSelectorMobile) {\n margin: 0 !important;\n flex: 1 !important;\n height: auto !important;\n overflow: hidden !important;\n padding: 0 !important;\n display: flex !important;\n flex-direction: column !important;\n }\n \n .antd-Service, .liquid-amis-container {\n display: flex; /* Ensure flex propagation */\n flex-direction: column;\n height: 100%;\n overflow: hidden; /* Prevent double scrollbars */\n }\n\n /* Mobile category selector styles */\n .mobile-category-pill {\n -webkit-tap-highlight-color: transparent;\n touch-action: manipulation;\n }\n\n /* Flow card animations */\n @keyframes flowCardSlideIn {\n 0% {\n opacity: 0;\n transform: translateY(20px);\n }\n 100% {\n opacity: 1;\n transform: translateY(0);\n }\n }\n</style>\n\n<!-- Main Mobile Container -->\n<div id=\"steedosFlowSelectorMobile\" class=\"flex flex-col h-full w-full bg-[#F5F5F7] font-sans text-gray-900 overflow-hidden\">\n \n <!-- Top Header Section -->\n <div class=\"shrink-0 bg-white border-b border-gray-200 shadow-sm pt-4\">\n \n <!-- Search Bar -->\n <div class=\"px-4 pb-3\">\n <div class=\"relative\">\n <div class=\"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-gray-400\">\n <svg class=\"h-5 w-5\" 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 \n type=\"text\" \n id=\"mobileSearchInput\" \n placeholder=\"搜索流程...\" \n class=\"w-full rounded-xl border border-gray-200 bg-gray-50 py-2.5 pl-10 pr-3 text-base text-gray-900 placeholder:text-gray-400 outline-none transition-all duration-200 focus:bg-white focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20\"\n >\n </div>\n </div>\n \n <!-- Category Tabs - Horizontal Scroll -->\n <div class=\"overflow-x-auto scrollbar-hide pb-2\">\n <div id=\"mobileCategoryTabs\" class=\"flex gap-2 px-4 min-w-full\">\n <!-- Categories will be inserted here -->\n </div>\n </div>\n </div>\n \n <!-- Main Content Area -->\n <!-- Added min-h-0 and webkit-overflow-scrolling for better mobile scrolling -->\n <div class=\"flex-1 overflow-y-auto scroll-smooth px-4 py-4 min-h-0\" style=\"-webkit-overflow-scrolling: touch;\">\n <div id=\"mobileContentContainer\" class=\"space-y-3 pb-safe\">\n <div class=\"flex flex-col items-center justify-center pt-20 text-center\">\n <div class=\"inline-flex items-center gap-2 text-gray-400 text-sm animate-pulse\">\n <svg class=\"animate-spin h-5 w-5\" 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\n<script>\n const MobileWorkflowService = {\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 // Use relative path directly to avoid cross-origin issues if apiBase is mixed up\n const url = \"/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, \n name: f.name, \n categoryId: cat._id, \n categoryName: cat.name || \"其他流程\" \n });\n });\n }\n });\n }\n return { categories: categories, flows: parsedFlows };\n } catch (e) { \n console.error(\"MobileWorkflowService 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) { \n if (!favs.includes(flowId)) favs.push(flowId); \n } else { \n favs = favs.filter(id => id !== flowId); \n }\n localStorage.setItem('steedos_fav_ids', JSON.stringify(favs));\n return favs;\n }\n };\n\n const MobileAppState = { \n allFlows: [], \n categories: [], \n favorites: [],\n currentCategory: 'all',\n /* Virtualization State */\n filteredFlows: [],\n renderedCount: 0,\n batchSize: 50\n };\n \n const tabsEl = document.getElementById('mobileCategoryTabs');\n // Important: ContentEl is the scroll host\n // listEl is the container for items\n let contentEl, listEl, searchInput;\n\n function initElements() {\n // Find elements scoped to our container to avoid collisions if multiple instances\n const wrapper = document.getElementById('steedosFlowSelectorMobile');\n if (!wrapper) return false;\n \n searchInput = document.getElementById('mobileSearchInput');\n // The scroll container is the div with class 'overflow-y-auto'\n contentEl = wrapper.querySelector('.overflow-y-auto');\n listEl = document.getElementById('mobileContentContainer');\n return true;\n }\n\n async function initMobile() {\n // Wait for DOM to be ready\n if (!initElements()) {\n setTimeout(initMobile, 100);\n return;\n }\n\n try {\n const data = await MobileWorkflowService.getData();\n MobileAppState.allFlows = data.flows;\n MobileAppState.categories = data.categories;\n MobileAppState.favorites = MobileWorkflowService.getFavorites();\n \n initObserver();\n renderMobileTabs();\n renderMobileContent();\n \n // Search init\n searchInput.addEventListener('input', (e) => {\n renderMobileContent(e.target.value.trim());\n });\n\n } catch (e) {\n console.error('Init error:', e);\n if (listEl) listEl.innerHTML = `<div class=\"text-center pt-20 text-gray-400 text-sm\">加载失败,请检查网络连接</div>`;\n }\n }\n\n let observer;\n function initObserver() {\n if (!contentEl) return;\n \n const options = {\n root: contentEl, // Observe intersection with scroll container\n rootMargin: '200px',\n threshold: 0.1\n };\n \n observer = new IntersectionObserver((entries) => {\n entries.forEach(entry => {\n if (entry.isIntersecting) {\n renderNextBatch();\n }\n });\n }, options);\n }\n\n function renderMobileTabs(filterText = \"\") {\n if (!tabsEl) return;\n \n // Save scroll position\n const currentScroll = tabsEl.scrollLeft;\n tabsEl.innerHTML = \"\";\n \n // Filter flows by search text to determine counts\n let searchedFlows = MobileAppState.allFlows;\n if (filterText && filterText.length > 0) {\n searchedFlows = searchedFlows.filter(f => f.name.includes(filterText));\n }\n \n // All category\n const allCount = searchedFlows.length;\n const isAllActive = MobileAppState.currentCategory === 'all';\n // Only show count on 'all' if we are filtering, same as PC? PC usually shows (Total). Let's show (count).\n const allTab = createTab('all', `全部 (${allCount})`, isAllActive);\n tabsEl.appendChild(allTab);\n \n // Favorites\n // For favorites, we filter the ALREADY SEARCHED flows to see if they are in favorites\n const favCount = searchedFlows.filter(f => MobileAppState.favorites.includes(f.id)).length;\n if (favCount > 0) {\n const isFavActive = MobileAppState.currentCategory === 'fav';\n const favTab = createTab('fav', `★ 收藏 (${favCount})`, isFavActive);\n tabsEl.appendChild(favTab);\n }\n \n // Category tabs\n MobileAppState.categories.forEach(cat => {\n const count = searchedFlows.filter(f => f.categoryId === cat._id).length;\n if (count > 0) {\n const isActive = MobileAppState.currentCategory === cat._id;\n const tab = createTab(cat._id, `${cat.name} (${count})`, isActive);\n tabsEl.appendChild(tab);\n }\n });\n \n // Restore scroll position\n tabsEl.scrollLeft = currentScroll;\n }\n\n function createTab(catId, label, isActive) {\n const tab = document.createElement('button');\n const baseClass = \"mobile-category-pill shrink-0 whitespace-nowrap rounded-full px-4 py-2 text-sm font-medium transition-all duration-200\";\n const activeClass = \"bg-blue-500 text-white shadow-md\";\n const inactiveClass = \"bg-white text-gray-700 border border-gray-200 active:bg-gray-50\";\n \n tab.className = `${baseClass} ${isActive ? activeClass : inactiveClass}`;\n tab.textContent = label;\n tab.dataset.categoryId = catId;\n \n tab.onclick = () => {\n MobileAppState.currentCategory = catId;\n // Pass current search text to preserve filter\n renderMobileContent(document.getElementById('mobileSearchInput').value.trim());\n };\n \n return tab;\n }\n\n function renderMobileContent(filterText = \"\") {\n if (!contentEl || !listEl) return;\n \n const isSearching = filterText.length > 0;\n let flows = MobileAppState.allFlows;\n \n // 1. First filter by search text\n if (isSearching) {\n flows = flows.filter(f => f.name.includes(filterText));\n }\n\n // 2. Then filter by category\n if (MobileAppState.currentCategory === 'fav') {\n flows = flows.filter(f => MobileAppState.favorites.includes(f.id));\n } else if (MobileAppState.currentCategory !== 'all') {\n flows = flows.filter(f => f.categoryId === MobileAppState.currentCategory);\n }\n \n const flowsToShow = flows;\n \n // Update tabs to reflect these search results\n renderMobileTabs(filterText);\n \n listEl.innerHTML = \"\";\n contentEl.scrollTop = 0;\n \n if (flowsToShow.length === 0) {\n listEl.innerHTML = `\n <div class=\"flex flex-col items-center justify-center pt-20 text-center\">\n <div class=\"text-gray-300 text-7xl mb-4\">∅</div>\n <div class=\"text-gray-400 text-sm\">${isSearching ? '未找到匹配的流程' : '此分类暂无流程'}</div>\\n </div>\n `;\n return;\n }\n \n MobileAppState.filteredFlows = flowsToShow;\n MobileAppState.renderedCount = 0;\n \n renderNextBatch();\n updateSentinel();\n }\n\n function renderNextBatch() {\n const { filteredFlows, renderedCount, batchSize } = MobileAppState;\n const total = filteredFlows.length;\n if (renderedCount >= total) return;\n \n const nextBatch = filteredFlows.slice(renderedCount, renderedCount + batchSize);\n const fragment = document.createDocumentFragment();\n \n nextBatch.forEach((flow, i) => {\n const index = renderedCount + i;\n const card = createMobileFlowCard(flow, index);\n \n // Remove animation for later batches to avoid scroll lag appearance\n if (renderedCount > 0) {\n card.style.animationDelay = '0s';\n }\n \n fragment.appendChild(card);\n });\n \n // Remove sentinel if exists\n const sentinel = document.getElementById('scroll-sentinel');\n if (sentinel) sentinel.remove();\n \n listEl.appendChild(fragment);\n MobileAppState.renderedCount += nextBatch.length;\n \n updateSentinel();\n }\n\n function updateSentinel() {\n if (MobileAppState.renderedCount < MobileAppState.filteredFlows.length) {\n const sentinel = document.createElement('div');\n sentinel.id = 'scroll-sentinel';\n sentinel.className = 'flex justify-center p-4';\n sentinel.innerHTML = '<span class=\"text-gray-400 text-xs\">加载更多...</span>';\n listEl.appendChild(sentinel);\n if (observer) observer.observe(sentinel);\n }\n }\n\n function createMobileFlowCard(flow, index) {\n const isFav = MobileAppState.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 'bg-purple-50 text-purple-600',\n 'bg-pink-50 text-pink-600'\n ];\n const colorClass = colorMap[(flow.name.length + index) % colorMap.length];\n const firstChar = flow.name.replace(/【.*?】/g, '').charAt(0) || flow.name.charAt(0);\n \n const card = document.createElement('div');\n card.className = 'relative flex items-center gap-3 rounded-2xl border border-gray-200 bg-white p-4 shadow-sm transition-all duration-200 active:scale-[0.97] active:bg-gray-50';\n card.style.animation = 'flowCardSlideIn 0.4s ease-out forwards';\n card.style.animationDelay = `${Math.min(index * 0.05, 0.5)}s`;\n card.style.opacity = '0';\n \n const iconClass = isFav ? 'text-yellow-400 fill-current' : 'text-gray-300 fill-none stroke-current stroke-[1.5]';\n \n card.innerHTML = `\n <div class=\"flex h-14 w-14 shrink-0 items-center justify-center rounded-xl text-lg font-bold ${colorClass}\">\n ${firstChar}\n </div>\n <div class=\"flex-1 min-w-0\">\n <div class=\"text-base font-medium text-gray-900 leading-snug line-clamp-3\" title=\"${flow.name}\">\n ${flow.name}\n </div>\n </div>\n <button class=\"star-btn shrink-0 flex h-10 w-10 items-center justify-center rounded-full transition-all duration-200 active:scale-90 ${isFav ? 'bg-yellow-50' : 'active:bg-gray-100'}\" title=\"${isFav ? '取消收藏' : '收藏'}\">\n <svg class=\"h-6 w-6 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 </button>\\n `;\n \n // Click card to select flow\n card.onclick = (e) => {\n if (!e.target.closest('.star-btn')) {\n setTimeout(() => {\n data._scoped.doAction([\n { \n \"actionType\": \"broadcast\", \n \"args\": { \"eventName\": \"flows.selected\" }, \n \"data\": { \"value\": flow.id } \n }\n ]);\n }, 50);\n }\n };\n \n // Star button to toggle favorite\n const starBtn = card.querySelector('.star-btn');\n const starIcon = starBtn.querySelector('svg');\n starBtn.onclick = (e) => {\n e.stopPropagation();\n const newFavState = !isFav;\n MobileAppState.favorites = MobileWorkflowService.toggleFavorite(flow.id, newFavState);\n \n // Update UI\n if (newFavState) {\n starBtn.classList.add('bg-yellow-50');\n starBtn.classList.remove('active:bg-gray-100');\n starIcon.setAttribute('class', 'h-6 w-6 transition-colors duration-300 text-yellow-400 fill-current');\n } else {\n starBtn.classList.remove('bg-yellow-50');\n starBtn.classList.add('active:bg-gray-100');\n starIcon.setAttribute('class', 'h-6 w-6 transition-colors duration-300 text-gray-300 fill-none stroke-current stroke-[1.5]');\n }\n \n // Refresh tabs and content after short delay\n setTimeout(() => {\n renderMobileTabs();\n renderMobileContent(searchInput.value.trim());\n }, 300);\n };\n \n return card;\n }\n\n // Initialize\n initMobile();\n</script>",
4
+ "className": "h-full"
5
+ }
@@ -0,0 +1,7 @@
1
+ name: flow_selector_mobile
2
+ is_active: true
3
+ label: 流程选择(移动端)
4
+ pageAssignments: []
5
+ render_engine: amis
6
+ type: app
7
+ widgets: []
@@ -214,7 +214,7 @@
214
214
  "data": {},
215
215
  "id": "u:d37465183f56",
216
216
  "className": "steedos-instance-related-view-wrapper flex justify-center",
217
- "bodyClassName": "p-0 flex flex-1 overflow-hidden h-full",
217
+ "bodyClassName": "p-0 flex flex-1 h-full",
218
218
  "name": "amis-root-workflow",
219
219
  "initApi": {
220
220
  "url": "/api/workflow/v2/instance/${recordId}/permission",
@@ -302,6 +302,12 @@
302
302
  ".steedos-amis-instance-view-content": {
303
303
  "max-width": "none !important"
304
304
  },
305
+ ".steedos-instance-print-wrapper .liquid-amis-container": {
306
+ "overflow": "visible !important"
307
+ },
308
+ ".steedos-instance-print-wrapper .instance-template .table-page-body": {
309
+ "width": "100%"
310
+ },
305
311
  ".simplify-traces .step-type-start":{
306
312
  "display": "none !important"
307
313
  },
@@ -87,6 +87,10 @@ router.post('/api/workflow/v2/nextStepUsers', requireAuthentication, async funct
87
87
 
88
88
  if (applicant)
89
89
  nextStepUsers = await WorkflowManager.getRoleUsersByOrgsAndRoles(spaceId, applicant.organizations, approveRoleIds);
90
+ if(!nextStepUsers || nextStepUsers.length < 1){
91
+ const roles = await WorkflowManager.getRoles(instance.space, approveRoleIds);
92
+ error = t('next_step_users_not_found.applicant_role', {role_name: _.join(_.map(roles, 'name'), ', ')}, userSession.language)
93
+ }
90
94
  break;
91
95
  case 'hrRole':
92
96
  var approveHrRoleIds = nextStep.approver_hr_roles;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steedos-labs/plugin-workflow",
3
- "version": "3.0.24",
3
+ "version": "3.0.26",
4
4
  "main": "package.service.js",
5
5
  "license": "MIT",
6
6
  "scripts": {
@@ -529,6 +529,13 @@ tbody .color-priority-muted *{
529
529
  box-shadow: none;
530
530
  }
531
531
 
532
+ .steedos-instance-style-table .antd-RadiosControl{
533
+ margin-left: 0.5rem !important;
534
+ }
535
+
536
+ .steedos-instance-style-table .antd-CheckboxesControl{
537
+ margin-left: 0.5rem !important;
538
+ }
532
539
 
533
540
  .instances-sidebar .steedos-listview-change-button > button > .fa-caret-down{
534
541
  display: none;
@@ -556,6 +563,12 @@ tbody .color-priority-muted *{
556
563
  height: calc(100vh - 101px);
557
564
  }
558
565
 
566
+ @media (max-width: 768px){
567
+ .steedos-instance-detail-wrapper{
568
+ height: calc(100vh - 64px);
569
+ }
570
+ }
571
+
559
572
  .steedos-amis-instance-view-body .antd-Panel-title{
560
573
  font-size: 14px;
561
574
  font-weight: 500;
@@ -627,6 +640,25 @@ tbody .color-priority-muted *{
627
640
  border: none !important;
628
641
  }
629
642
 
643
+ .steedos-instance-style-table .instance-form .td-childfield {
644
+ padding: 0 !important;
645
+ border: 1px solid black !important;
646
+ }
647
+ .steedos-instance-style-table .instance-form .td-childfield .antd-Table{
648
+ margin-bottom: 0px !important;
649
+ }
650
+
651
+ .steedos-instance-style-table .instance-form .td-childfield .antd-Table .antd-Table-operationCell {
652
+ border: none !important;
653
+ }
654
+
655
+ .steedos-instance-style-table .instance-form .td-childfield .antd-Table td:first-child {
656
+ border-right: none !important;
657
+ }
658
+ .steedos-instance-style-table .antd-Select-value{
659
+ white-space: unset !important;
660
+ }
661
+
630
662
 
631
663
  /* 1. 基础设置:合并边框并去掉表格容器的外边框 */
632
664
  .instance-form .antd-Table-table {
@@ -751,6 +783,14 @@ tbody .color-priority-muted *{
751
783
  }
752
784
  }
753
785
 
786
+ .antd-Combo-form .antd-Form-row .antd-Form-col{
787
+ margin-bottom: 0.75rem !important;
788
+ }
789
+ .antd-Combo-form .antd-Form-row .antd-Form-col .antd-Form-rowInner > div{
790
+ flex: 1;
791
+ }
792
+
793
+
754
794
  /* .steedos-instance-detail-wrapper{
755
795
  height: auto !important;
756
796
  }
@@ -28,6 +28,22 @@ function getFieldLabel(fieldDef, fieldName) {
28
28
  return fieldName;
29
29
  }
30
30
 
31
+ const getArgumentsList = (func)=>{
32
+ let funcString;
33
+ if (typeof func === 'function') {
34
+ funcString = func.toString();
35
+ } else {
36
+ funcString = func;
37
+ }
38
+ const regExp = /function\s*\w*\(([\s\S]*?)\)/;
39
+ if (regExp.test(funcString)) {
40
+ const argList = RegExp.$1.split(',');
41
+ return argList.map(arg => arg.replace(/\s/g, ''));
42
+ } else {
43
+ return [];
44
+ }
45
+ }
46
+
31
47
  function fieldToAmis(fieldName, fieldDef, schema, isColumn = false, isPrint = false) {
32
48
  // Handle section type special case
33
49
  if (fieldDef && fieldDef.type === 'section') {
@@ -85,6 +101,126 @@ function fieldToAmis(fieldName, fieldDef, schema, isColumn = false, isPrint = fa
85
101
  case 'checkbox': type = 'checkboxes'; break;
86
102
  case 'select': type = 'select'; break;
87
103
  case 'lookup': type = 'select'; break; // Simplified for now
104
+ case 'odata':
105
+ type = 'select';
106
+ const field = fieldDef;
107
+ const inTable = isColumn;
108
+ const argsName = getArgumentsList(field.filters);
109
+ let labelField = field.formula ? field.formula.substr(1, field.formula.length - 2) : 'name';
110
+ if(labelField.indexOf(".") > -1){
111
+ labelField = labelField.substr(labelField.indexOf(".") + 1);
112
+ }
113
+ config.multiple = field.is_multiselect;
114
+
115
+ // Compress script to single line to avoid JSON parsing issues with control characters
116
+ const adaptorScript = `
117
+ return (async () => {
118
+ let options = _.map(payload.value, (item)=>{
119
+ const value = item;
120
+ item["@label"] = item["${labelField}"];
121
+ delete item['@odata.editLink'];
122
+ delete item['@odata.etag'];
123
+ delete item['@odata.id'];
124
+ return {
125
+ label: item["@label"],
126
+ value: value
127
+ }
128
+ });
129
+
130
+ if(api.selectedIds && api.selectedIds.length > 0){
131
+ const loadedIds = options.map(function(opt){ return opt.value._id; });
132
+ const missingIds = api.selectedIds.filter(function(id){ return !loadedIds.includes(id); });
133
+ if(missingIds.length > 0){
134
+ const baseUrl = api.baseUrl;
135
+ const idFilter = missingIds.map(function(id){ return "_id eq '" + id + "'"; }).join(" or ");
136
+ const fetchUrl = baseUrl + (baseUrl.indexOf('?') > -1 ? '&' : '?') + "$filter=" + idFilter;
137
+ try {
138
+ const response = await fetch(fetchUrl, {});
139
+ const data = await response.json();
140
+ if (data && data.value) {
141
+ const missingOptions = _.map(data.value, (item) => {
142
+ const value = item;
143
+ item["@label"] = item["${labelField}"];
144
+ delete item['@odata.editLink'];
145
+ delete item['@odata.etag'];
146
+ delete item['@odata.id'];
147
+ return { label: item["@label"], value: value };
148
+ });
149
+ options.unshift(...missingOptions);
150
+ }
151
+ } catch (e) {
152
+ console.error("Failed to fetch missing values", e);
153
+ }
154
+ }
155
+ }
156
+ payload.data = { options: options };
157
+ return payload;
158
+ })();
159
+ `.replace(/\s+/g, ' ');
160
+
161
+ const filtersStr = field.filters ? _.replace(field.filters, /_.pluck/g, '_.map') : '';
162
+ const requestAdaptorScript = `
163
+ const filters = \`${filtersStr}\`;
164
+ let url = \`${field.url}\`;
165
+ api.baseUrl = url;
166
+ if(filters){
167
+ let joinKey = url.indexOf('?') > 0 ? '&' : '?';
168
+ let _filter = [];
169
+ if(filters.startsWith('function(') || filters.startsWith('function (')){
170
+ const argsName = ${JSON.stringify(argsName)};
171
+ const fun = eval('_fun='+filters);
172
+ const funArgs = [];
173
+ for(const item of argsName){
174
+ funArgs.push(context[item]);
175
+ }
176
+ _filter = fun.apply({}, funArgs);
177
+ }else{
178
+ _filter = filters;
179
+ }
180
+
181
+ const val = context.value || _.get(context, '${field.code}');
182
+ let ids = [];
183
+ if(val){
184
+ const values = Array.isArray(val) ? val : [val];
185
+ ids = values.map((v) => v && (v._id || v)).filter((v) => typeof v === 'string');
186
+ }
187
+
188
+ ids = _.uniq(ids);
189
+ api.selectedIds = ids;
190
+
191
+ if(context.term){
192
+ _filter = \`(\${_filter}) and contains(name, '\${context.term}')\`;
193
+ }
194
+ joinKey = url.indexOf('?') > 0 ? '&' : '?';
195
+ api.url = url + joinKey + "$filter=" + _filter;
196
+ }else{
197
+ api.url = url;
198
+ }
199
+ api.query = {};
200
+ return api;
201
+ `.replace(/\s+/g, ' ');
202
+
203
+ config.autoComplete = {
204
+ url: field.url.indexOf('?') < 0 ? `${field.url}?term=\${term}` : `${field.url}&term=\${term}`,
205
+ method: "get",
206
+ dataType: "json",
207
+ adaptor: adaptorScript,
208
+ requestAdaptor: requestAdaptorScript
209
+ };
210
+
211
+ // Only add trackExpression if there are dependent fields (argsName)
212
+ if (argsName && argsName.length > 0) {
213
+ // Use simpler tracking without |json to avoid potential newline issues in stringified objects
214
+ config.autoComplete.trackExpression = _.join(_.map(argsName, (item) => `\${${item}}`), '-');
215
+ }
216
+
217
+ config.source = _.cloneDeep(config.autoComplete);
218
+ // trackExpression is needed in source to react to dependency changes
219
+
220
+ if(!inTable){
221
+ config.searchable = true;
222
+ }
223
+ break;
88
224
  case 'table':
89
225
  type = 'steedos-input-table';
90
226
  config.columnsTogglable = false;
@@ -287,7 +423,13 @@ function convertTemplate(content, fields, isPrint) {
287
423
  });
288
424
 
289
425
  // --- Sub-phase 2c: Convert afFieldLabelText ---
290
- newContent = newContent.replace(/\{\{afFieldLabelText\s+name=["'](.+?)["']\}\}/g, '$1');
426
+ newContent = newContent.replace(/\{\{afFieldLabelText\s+name=["'](.+?)["']\}\}/g, (match, name) => {
427
+ const fieldDef = findFieldDef(fields, name);
428
+ if (fieldDef && fieldDef.is_required) {
429
+ return `${name}<span class="antd-Form-star" style="display: \${record.step.permissions['${name}'] == 'editable' ? 'inline' : 'none'}">*</span>`;
430
+ }
431
+ return name;
432
+ });
291
433
 
292
434
  // --- Sub-phase 2d: Convert instanceSignText ---
293
435
  newContent = newContent.replace(/\{\{>\s*instanceSignText\s+([\s\S]+?)\}\}/g, (match, args) => {
@@ -334,6 +476,14 @@ function convertTemplate(content, fields, isPrint) {
334
476
  return `{% amis %}\n${JSON.stringify(amisSchema, null, 2)}\n{% endamis %}`;
335
477
  });
336
478
 
479
+ // --- Sub-phase 2e: Add header for tr-child-table ---
480
+ // Extract dynamic title from td-childfield-xxx class if present
481
+ newContent = newContent.replace(/(<tr[^>]*class\s*=\s*["'][^"']*tr-child-table[^"']*["'][^>]*>[\s\S]*?class\s*=\s*["'][^"']*?td-childfield-)([^\s"']+)/gi,
482
+ (match, beforeName, name) => {
483
+ return `<tr><td colspan="10" style="background:#f1f1f1" class="font-bold">${name}</td></tr>${beforeName}${name}`;
484
+ }
485
+ );
486
+
337
487
  return newContent;
338
488
  }
339
489