@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.
- package/main/default/manager/uuflow_manager.js +5 -60
- package/main/default/objects/instance_tasks/buttons/instance_new.button.yml +4 -1
- package/main/default/objects/instances/buttons/instance_new.button.yml +4 -1
- package/main/default/objects/instances/buttons/instance_return.button.yml +11 -0
- package/main/default/pages/flow_selector_mobile.page.amis.json +5 -0
- package/main/default/pages/flow_selector_mobile.page.yml +7 -0
- package/main/default/pages/page_instance_print.page.amis.json +7 -1
- package/main/default/routes/api_workflow_next_step_users.router.js +4 -0
- package/package.json +1 -1
- package/public/workflow/index.css +40 -0
- package/src/util/templateConverter.js +151 -1
|
@@ -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
|
+
}
|
|
@@ -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
|
|
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
|
@@ -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,
|
|
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
|
|