@steedos-labs/plugin-workflow 3.0.25 â 3.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/designer/README.md +110 -0
- package/main/default/objects/flows/flows.object.yml +18 -0
- 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.page.amis.json +1 -1
- 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/am.router.js +28 -0
- package/main/default/routes/api_workflow_ai_design.router.js +184 -0
- package/main/default/routes/api_workflow_next_step_users.router.js +4 -0
- package/main/default/routes/designer-v2.router.js +62 -0
- package/package.json +13 -1
- package/public/workflow/index.css +40 -0
- package/src/util/templateConverter.js +151 -1
- package/AI_PLUGIN_GUIDE.md +0 -939
- package/APPROVAL_COMMENTS_OPERATIONS.md +0 -673
- package/APPROVAL_COMMENTS_UPGRADE.md +0 -854
- package/README_TEMPLATE.md +0 -222
- package/convert-templates.js +0 -51
- package/run.js +0 -388
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Workflow Designer (React + shadcn/ui)
|
|
2
|
+
|
|
3
|
+
Modern workflow designer built with React 18, TypeScript, and shadcn/ui.
|
|
4
|
+
|
|
5
|
+
## ð Quick Start
|
|
6
|
+
|
|
7
|
+
### Development
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
npm run dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Open http://localhost:5173/
|
|
15
|
+
|
|
16
|
+
### Production Build
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm run build
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Built files will be in `dist/` directory.
|
|
23
|
+
|
|
24
|
+
## ð ïļ Tech Stack
|
|
25
|
+
|
|
26
|
+
- **React 19** - UI framework
|
|
27
|
+
- **TypeScript** - Type safety
|
|
28
|
+
- **Vite** - Build tool
|
|
29
|
+
- **Tailwind CSS** - Styling
|
|
30
|
+
- **shadcn/ui** - UI components
|
|
31
|
+
- **Zustand** - State management
|
|
32
|
+
- **Axios** - HTTP client
|
|
33
|
+
|
|
34
|
+
## ð Project Structure
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
designer/
|
|
38
|
+
âââ src/
|
|
39
|
+
â âââ components/
|
|
40
|
+
â â âââ ui/ # shadcn/ui components
|
|
41
|
+
â â âââ layout/ # Layout components
|
|
42
|
+
â â âââ workflow/ # Workflow-specific (TODO)
|
|
43
|
+
â â âââ form/ # Form designer (TODO)
|
|
44
|
+
â âââ lib/ # Utilities
|
|
45
|
+
â âââ pages/ # Page components
|
|
46
|
+
â â âââ WorkflowDesigner.tsx
|
|
47
|
+
â âââ stores/ # Zustand stores (TODO)
|
|
48
|
+
â âââ types/ # TypeScript types (TODO)
|
|
49
|
+
â âââ App.tsx
|
|
50
|
+
â âââ main.tsx
|
|
51
|
+
âââ public/
|
|
52
|
+
âââ dist/ # Build output
|
|
53
|
+
âââ package.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## ð Backend Integration
|
|
57
|
+
|
|
58
|
+
The new designer is served via:
|
|
59
|
+
- **Development**: http://localhost:5173/
|
|
60
|
+
- **Production**: /api/workflow/designer-v2
|
|
61
|
+
|
|
62
|
+
Backend route: `main/default/routes/designer-v2.router.js`
|
|
63
|
+
|
|
64
|
+
## ð Features
|
|
65
|
+
|
|
66
|
+
### Implemented â
|
|
67
|
+
- Modern UI with shadcn/ui
|
|
68
|
+
- Responsive layout
|
|
69
|
+
- TypeScript type safety
|
|
70
|
+
- Basic component structure
|
|
71
|
+
- Development server
|
|
72
|
+
- Production build
|
|
73
|
+
|
|
74
|
+
### TODO ð§
|
|
75
|
+
- Workflow canvas with React Flow
|
|
76
|
+
- Form designer components
|
|
77
|
+
- State management with Zustand
|
|
78
|
+
- API integration
|
|
79
|
+
- Internationalization (i18n)
|
|
80
|
+
- Unit tests
|
|
81
|
+
- More shadcn/ui components
|
|
82
|
+
|
|
83
|
+
## ðĻ Customization
|
|
84
|
+
|
|
85
|
+
### Tailwind Configuration
|
|
86
|
+
|
|
87
|
+
Edit `tailwind.config.js` to customize theme.
|
|
88
|
+
|
|
89
|
+
### Path Aliases
|
|
90
|
+
|
|
91
|
+
TypeScript path aliases are configured in `tsconfig.app.json`:
|
|
92
|
+
- `@/` maps to `./src/`
|
|
93
|
+
|
|
94
|
+
## ðĶ Scripts
|
|
95
|
+
|
|
96
|
+
- `npm run dev` - Start development server
|
|
97
|
+
- `npm run build` - Build for production
|
|
98
|
+
- `npm run lint` - Run ESLint
|
|
99
|
+
- `npm run preview` - Preview production build
|
|
100
|
+
|
|
101
|
+
## ð Related Documentation
|
|
102
|
+
|
|
103
|
+
See parent directory for comprehensive planning docs:
|
|
104
|
+
- [Refactoring Plan](../DESIGNER_REFACTOR_PLAN.md)
|
|
105
|
+
- [URL Structure](../URL_STRUCTURE.md)
|
|
106
|
+
- [Quick Start Guide](../QUICK_START_GUIDE.md)
|
|
107
|
+
|
|
108
|
+
## ð License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -769,6 +769,24 @@ actions:
|
|
|
769
769
|
const iframe_url = `/api/workflow/designer?url=${url}&title=${title}`;
|
|
770
770
|
return Steedos.openWindow(Steedos.absoluteUrl(iframe_url));
|
|
771
771
|
}
|
|
772
|
+
designFlowV2:
|
|
773
|
+
label: æĩįĻčŪūčŪĄåĻV2(BETA)
|
|
774
|
+
visible: !<tag:yaml.org,2002:js/function> |-
|
|
775
|
+
function (object_name, record_id, record_permissions) {
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
'on': record_more
|
|
779
|
+
todo: !<tag:yaml.org,2002:js/function> |-
|
|
780
|
+
function (object_name, record_id, record_permissions, data) {
|
|
781
|
+
const flow = data && data.record;
|
|
782
|
+
const space = Builder.settings.context.user.spaceId;
|
|
783
|
+
const companyId = Builder.settings.context.user.company_id;
|
|
784
|
+
let url = `/api/workflow/designer-v2?flowId=${flow._id}`;
|
|
785
|
+
if (companyId && !Builder.settings.context.user.is_space_admin) {
|
|
786
|
+
url = url + `&companyId=${companyId}`;
|
|
787
|
+
}
|
|
788
|
+
return Steedos.openWindow(url);
|
|
789
|
+
}
|
|
772
790
|
enableFlow:
|
|
773
791
|
label: Enable Flow
|
|
774
792
|
visible: !<tag:yaml.org,2002:js/function> |-
|
|
@@ -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": {},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "liquid",
|
|
3
|
-
"template": "<style>\n @keyframes fadeUpSpring {\n 0% { opacity: 0; transform: translateY(10px); }\n 100% { opacity: 1; transform: translateY(0); }\n }\n \n /* Make scrollbars standardized and visible */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n ::-webkit-scrollbar-thumb {\n background-color: rgba(0, 0, 0, 0.25); /* Darker for visibility on gray bg */\n border-radius: 4px;\n border: 2px solid transparent; /* Creates padding effect */\n background-clip: content-box;\n }\n ::-webkit-scrollbar-thumb:hover {\n background-color: rgba(0, 0, 0, 0.4);\n }\n\n /* \n Fix outer modal scrollbar - SAFER VERSION \n Only apply these aggressive overrides (no padding, hidden overflow)\n to the specific modal that contains our component (identified by #steedosFlowSelectorSidebarList).\n This prevents breaking other stacked modals like 'Confirm Dialogs'.\n */\n .antd-Modal-body:has(#steedosFlowSelectorSidebarList) {\n overflow: hidden !important;\n padding: 0 !important; /* Optional: maximize space */\n display: flex;\n flex-direction: column;\n }\n\n /* Ensure the AMIS container fills height if needed */\n .antd-Service, .liquid-amis-container {\n height: 100%;\n }\n</style>\n\n<!-- Main Container: Fixed Height 70vh. -->\n<div class=\"flex h-[70vh] max-h-[800px] w-full overflow-hidden font-sans text-gray-900 bg-white\" style=\"min-height: 0;\">\n\n <!-- Left Sidebar -->\n <!-- flex-col, h-full, overflow-hidden -->\n <div class=\"flex flex-col w-[260px] h-full border-r border-gray-200 bg-[#F2F2F7] shrink-0 overflow-hidden\">\n <!-- Header -->\n <div class=\"shrink-0 pt-4 pb-2 px-3\">\n <div class=\"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-3 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",
|
|
3
|
+
"template": "<style>\n @keyframes fadeUpSpring {\n 0% { opacity: 0; transform: translateY(10px); }\n 100% { opacity: 1; transform: translateY(0); }\n }\n \n /* Make scrollbars standardized and visible */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n ::-webkit-scrollbar-thumb {\n background-color: rgba(0, 0, 0, 0.25); /* Darker for visibility on gray bg */\n border-radius: 4px;\n border: 2px solid transparent; /* Creates padding effect */\n background-clip: content-box;\n }\n ::-webkit-scrollbar-thumb:hover {\n background-color: rgba(0, 0, 0, 0.4);\n }\n\n /* \n Fix outer modal scrollbar - SAFER VERSION \n Only apply these aggressive overrides (no padding, hidden overflow)\n to the specific modal that contains our component (identified by #steedosFlowSelectorSidebarList).\n This prevents breaking other stacked modals like 'Confirm Dialogs'.\n */\n .antd-Modal-body:has(#steedosFlowSelectorSidebarList) {\n overflow: hidden !important;\n padding: 0 !important; /* Optional: maximize space */\n display: flex;\n flex-direction: column;\n }\n\n /* Ensure the AMIS container fills height if needed */\n .antd-Service, .liquid-amis-container {\n height: 100%;\n }\n</style>\n\n<!-- Main Container: Fixed Height 70vh. -->\n<div class=\"flex h-[70vh] max-h-[800px] w-full overflow-hidden font-sans text-gray-900 bg-white\" style=\"min-height: 0;\">\n\n <!-- Left Sidebar -->\n <!-- flex-col, h-full, overflow-hidden -->\n <div class=\"flex flex-col w-[260px] h-full border-r border-gray-200 bg-[#F2F2F7] shrink-0 overflow-hidden\">\n <!-- Header -->\n <div class=\"shrink-0 pt-4 pb-2 px-3\">\n <div class=\"relative group\">\n <div class=\"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5 text-gray-500\">\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n </svg>\n </div>\n <input type=\"text\" id=\"searchInput\" placeholder=\"æįīĒæĩįĻåį§°...\" class=\"w-full rounded-[10px] border-none bg-[#767680]/10 py-1.5 pl-9 pr-3 text-[14px] text-gray-900 placeholder:text-gray-500 outline-none transition-all duration-200 focus:bg-white focus:shadow-sm focus:ring-2 focus:ring-blue-500/20\">\n </div>\n </div>\n \n <!-- List Container -->\n <!-- min-h-0 is CRITICAL for flex child scrolling -->\n <div class=\"flex-1 min-h-0 overflow-y-auto px-2 pb-4 space-y-0.5 scroll-smooth\" id=\"steedosFlowSelectorSidebarList\">\n </div>\n </div>\n\n <!-- Right Content -->\n <!-- flex-1 fills remaining width -->\n <div class=\"flex-1 h-full relative bg-white overflow-hidden\">\n <!-- Absolute inset-0 locks the scroll container size -->\n <div id=\"mainContentScroll\" class=\"absolute inset-0 overflow-y-auto scroll-smooth p-6\">\n <div id=\"contentContainer\" class=\"w-full h-auto min-h-full\">\n <div class=\"flex h-full w-full flex-col items-center justify-center pt-20\">\n <div class=\"inline-flex items-center gap-2 text-gray-400 text-sm animate-pulse\">\n <svg class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path>\n </svg>\n <span>æĢåĻå č――čĩæš...</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n\n<script>\n const WorkflowService = {\n apiBase: \"\", \n getHeaders: function() { return { 'Content-Type': 'application/json' }; },\n getData: async function() {\n try {\n const appId = (typeof data !== 'undefined' && data.context && data.context.app_id) ? data.context.app_id : \"\";\n const url = this.apiBase + \"/service/api/flows/getList?action=new&appId=\" + encodeURIComponent(appId);\n const res = await fetch(url, { headers: this.getHeaders() });\n const treeData = await res.json();\n const categories = [];\n const parsedFlows = [];\n if (Array.isArray(treeData)) {\n treeData.forEach(cat => {\n categories.push({ _id: cat._id, name: cat.name });\n if (Array.isArray(cat.flows)) {\n cat.flows.forEach(f => {\n parsedFlows.push({\n id: f._id, name: f.name, categoryId: cat._id, categoryName: cat.name || \"å
ķäŧæĩįĻ\" \n });\n });\n }\n });\n }\n return { categories: categories, flows: parsedFlows };\n } catch (e) { \n console.error(\"WorkflowService Error:\", e);\n return { categories: [], flows: [] }; \n }\n },\n getFavorites: function() {\n const saved = localStorage.getItem('steedos_fav_ids');\n return saved ? JSON.parse(saved) : [];\n },\n toggleFavorite: function(flowId, isFav) {\n let favs = this.getFavorites();\n if (isFav) { if (!favs.includes(flowId)) favs.push(flowId); } \n else { favs = favs.filter(id => id !== flowId); }\n localStorage.setItem('steedos_fav_ids', JSON.stringify(favs));\n return favs;\n }\n };\n\n const AppState = { allFlows: [], categories: [], favorites: [] };\n const sidebarEl = document.getElementById('steedosFlowSelectorSidebarList');\n const contentEl = document.getElementById('contentContainer');\n const searchInput = document.getElementById('searchInput');\n\n async function init() {\n try {\n const data = await WorkflowService.getData();\n AppState.allFlows = data.flows;\n AppState.categories = data.categories;\n AppState.favorites = WorkflowService.getFavorites();\n renderUI();\n } catch (e) {\n contentEl.innerHTML = `<div class=\"text-gray-400 text-sm\">å č――åĪąčīĨïžčŊ·æĢæĨį―įŧ</div>`;\n }\n }\n\n function renderUI(filterText = \"\") {\n sidebarEl.innerHTML = \"\";\n contentEl.innerHTML = \"\";\n\n const isSearching = filterText.length > 0;\n let groups = [];\n\n const favFlows = AppState.allFlows.filter(f => \n AppState.favorites.includes(f.id) && \n (isSearching ? f.name.includes(filterText) : true)\n );\n if (favFlows.length > 0) {\n groups.push({ id: 'fav', name: \"æįæķč\", items: favFlows, isFav: true });\n }\n\n AppState.categories.forEach(cat => {\n const items = AppState.allFlows.filter(f => \n f.categoryId === cat._id &&\n (isSearching ? f.name.includes(filterText) : true)\n );\n if (items.length > 0) {\n groups.push({ id: cat._id, name: cat.name, items: items, isFav: false });\n }\n });\n\n const otherItems = AppState.allFlows.filter(f => \n !AppState.categories.find(c => c._id === f.categoryId) &&\n (isSearching ? f.name.includes(filterText) : true)\n );\n if (otherItems.length > 0) {\n groups.push({ id: 'other', name: \"å
ķäŧæĩįĻ\", items: otherItems, isFav: false });\n }\n\n if (groups.length === 0) {\n contentEl.innerHTML = `<div class=\"animate-[fadeUpSpring_0.5s_ease-out] text-center pt-20\"><div class=\"text-gray-200 text-7xl mb-4\">â
</div><div class=\"text-gray-400 text-sm\">æŠæūå°åđé
æĩįĻ</div></div>`;\n return;\n }\n\n const contentFragment = document.createDocumentFragment();\n groups.forEach((group, index) => {\n const groupId = `group-\\${group.id}`;\n const navItem = document.createElement('div');\n let navBase = \"group flex cursor-pointer items-center justify-between rounded-md px-3 py-2 text-[14px] transition-all duration-200 ease-out select-none\";\n let activeClass = \"bg-[#007AFF] text-white shadow-sm font-medium\";\n let inactiveClass = \"text-gray-700 hover:bg-black/5 active:bg-black/10\";\n \n navItem.className = `\\${navBase} \\${index === 0 ? activeClass : inactiveClass}`;\n const badgeClass = index === 0 ? \"text-white/80\" : \"text-gray-400 group-hover:text-gray-500\";\n \n navItem.innerHTML = `<span class=\"truncate\">\\${group.isFav ? 'â
' : ''}\\${group.name}</span><span class=\"\\${badgeClass} text-[12px] font-medium transition-colors\">\\${group.items.length}</span>`;\n \n navItem.onclick = () => {\n Array.from(sidebarEl.children).forEach(el => {\n el.className = `\\${navBase} \\${inactiveClass}`;\n el.querySelector('span:last-child').className = \"text-gray-400 group-hover:text-gray-500 text-[12px] font-medium transition-colors\";\n });\n navItem.className = `\\${navBase} \\${activeClass}`;\n navItem.querySelector('span:last-child').className = \"text-white/80 text-[12px] font-medium transition-colors\";\n \n const target = document.getElementById(groupId);\n const container = document.getElementById('mainContentScroll');\n if(target && container) {\n const targetTop = target.getBoundingClientRect().top; \n const containerTop = container.getBoundingClientRect().top; \n container.scrollTo({ top: container.scrollTop + targetTop - containerTop - 16, behavior: 'smooth' });\n }\n };\n sidebarEl.appendChild(navItem);\n\n const section = document.createElement('div');\n section.id = groupId;\n section.className = \"mb-10\";\n const headerColor = group.isFav ? 'text-amber-500' : 'text-gray-900';\n section.innerHTML = `<div class=\"sticky top-0 z-20 mb-4 bg-white pb-2 text-xl font-bold tracking-tight text-left border-b border-gray-100 \\${headerColor}\">\\${group.name}</div>`;\n\n const grid = document.createElement('div');\n grid.className = 'grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-4';\n\n const gridFragment = document.createDocumentFragment();\n group.items.forEach((flow, i) => {\n const isFav = AppState.favorites.includes(flow.id);\n const colorMap = ['bg-blue-50 text-blue-600', 'bg-orange-50 text-orange-600', 'bg-emerald-50 text-emerald-600', 'bg-indigo-50 text-indigo-600'];\n const colorClass = colorMap[(flow.name.length + i) % 4];\n const firstChar = flow.name.replace(/ã.*?ã/g, '').charAt(0) || flow.name.charAt(0);\n const card = document.createElement('div');\n card.className = 'group relative flex h-auto min-h-[72px] cursor-pointer items-center rounded-2xl border border-gray-100 bg-white p-3 text-left shadow-[0_2px_8px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.02] transition-[transform,box-shadow] duration-200 ease-out hover:-translate-y-1 hover:border-gray-200 hover:shadow-[0_12px_24px_rgba(0,0,0,0.08)] active:scale-[0.98] active:bg-gray-50' + (i < 12 ? ' animate-[fadeUpSpring_0.4s_cubic-bezier(0.16,1,0.3,1)_forwards]' : '');\n if (i < 12) { card.style.animationDelay = (i * 0.03) + 's'; card.style.opacity = '0'; }\n const iconClass = isFav ? 'text-yellow-400 fill-current' : 'text-gray-300 group-hover/btn:text-gray-400 fill-none stroke-current stroke-[1.5]';\n const btnBgClass = isFav ? 'opacity-100 hover:scale-110' : 'opacity-0 group-hover:opacity-100 hover:bg-gray-100 hover:scale-110';\n\n card.innerHTML = `\n <div class=\"star-btn group/btn absolute right-2 top-1/2 -translate-y-1/2 z-20 flex h-8 w-8 items-center justify-center rounded-full transition-all duration-200 \\${btnBgClass}\" title=\"\\${isFav ? 'åæķæķč' : 'å å
Ĩæķč'}\">\n <svg class=\"h-5 w-5 transition-colors duration-300 \\${iconClass}\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z\" />\n </svg>\n </div>\n <div class=\"mr-4 flex h-11 w-11 shrink-0 items-center justify-center rounded-xl text-[16px] font-bold \\${colorClass}\">\\${firstChar}</div>\n <div class=\"flex-1 pr-8 text-[15px] font-medium text-gray-900 line-clamp-3 leading-relaxed tracking-tight\" title=\"\\${flow.name}\">\\${flow.name}</div>\n `;\n card.onclick = () => {\n if (card.dataset.loading === 'true') return;\n card.dataset.loading = 'true';\n card.style.pointerEvents = 'none';\n card.style.opacity = '0.6';\n card.innerHTML = '<div class=\"flex items-center justify-center w-full gap-2 text-gray-400 text-sm\"><svg class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path></svg><span>æĢåĻååŧš...</span></div>';\n setTimeout(() => {\n data._scoped.doAction([\n { \"actionType\": \"broadcast\", \"args\": { \"eventName\": \"flows.selected\" }, \"data\": { \"value\": flow.id } }\n ])\n }, 50);\n };\n const starBtn = card.querySelector('.star-btn');\n const starIcon = starBtn.querySelector('svg');\n starBtn.onclick = (e) => {\n e.stopPropagation();\n const newFavState = !starBtn.classList.contains('active-fav');\n if (newFavState) {\n starBtn.classList.add('active-fav', 'opacity-100');\n starBtn.classList.add('animate-[starPop_0.4s_ease-out]');\n starIcon.setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-yellow-400 fill-current');\n } else {\n starBtn.classList.remove('active-fav', 'opacity-100');\n starBtn.classList.remove('animate-[starPop_0.4s_ease-out]');\n starIcon.setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-gray-300 group-hover/btn:text-gray-400 fill-none stroke-current stroke-[1.5]');\n }\n AppState.favorites = WorkflowService.toggleFavorite(flow.id, newFavState);\n setTimeout(() => renderUI(searchInput.value), 300);\n };\n if (isFav) starBtn.classList.add('active-fav');\n gridFragment.appendChild(card);\n });\n grid.appendChild(gridFragment);\n section.appendChild(grid);\n contentFragment.appendChild(section);\n });\n contentEl.appendChild(contentFragment);\n }\n\n let _searchTimer = null;\n searchInput.addEventListener('input', (e) => {\n clearTimeout(_searchTimer);\n _searchTimer = setTimeout(() => renderUI(e.target.value.trim()), 300);\n });\n init();\n</script>\n",
|
|
4
4
|
"className": "h-full"
|
|
5
5
|
}
|
|
@@ -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
|
},
|
|
@@ -81,6 +81,34 @@ router.get('/am/designer/startup', async function (req, res) {
|
|
|
81
81
|
}
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
+
// startup v2 - lightweight: only return the specified flow and its form
|
|
85
|
+
router.get('/am/designer/startup/v2', async function (req, res) {
|
|
86
|
+
try {
|
|
87
|
+
let flowId = req.query.flowId;
|
|
88
|
+
if (!flowId) {
|
|
89
|
+
return res.status(400).send({ status: 'error', message: 'flowId is required' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let flowCollection = await getCollection('flows');
|
|
93
|
+
let formCollection = await getCollection('forms');
|
|
94
|
+
|
|
95
|
+
let flow = await flowCollection.findOne({ _id: flowId }, { projection: { historys: 0 } });
|
|
96
|
+
if (!flow) {
|
|
97
|
+
return res.status(404).send({ status: 'error', message: 'Flow not found' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let form = await formCollection.findOne({ _id: flow.form }, { projection: { historys: 0 } });
|
|
101
|
+
|
|
102
|
+
res.send({
|
|
103
|
+
flow,
|
|
104
|
+
form,
|
|
105
|
+
sync_token: new Date().getTime() / 1000
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
res.status(500).send(error.message);
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
84
112
|
// čĄĻå
|
|
85
113
|
router.post('/am/forms', async function (req, res) {
|
|
86
114
|
try {
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const steedosAuth = require('@steedos/auth');
|
|
4
|
+
const axios = require('axios');
|
|
5
|
+
const bodyParser = require('body-parser');
|
|
6
|
+
|
|
7
|
+
router.use(bodyParser.json({ limit: '2mb' }));
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* AI Workflow Designer endpoint
|
|
11
|
+
*
|
|
12
|
+
* Accepts natural language instructions and current workflow steps,
|
|
13
|
+
* calls an OpenAI-compatible LLM API to generate modified steps.
|
|
14
|
+
*
|
|
15
|
+
* Environment variables:
|
|
16
|
+
* WORKFLOW_AI_API_KEY - API key for the LLM provider
|
|
17
|
+
* WORKFLOW_AI_BASE_URL - Base URL (default: https://api.openai.com/v1)
|
|
18
|
+
* WORKFLOW_AI_MODEL - Model name (default: gpt-4o)
|
|
19
|
+
*/
|
|
20
|
+
router.post('/am/ai/design', async function auth(req, res, next) {
|
|
21
|
+
try {
|
|
22
|
+
const result = await steedosAuth.auth(req, res);
|
|
23
|
+
if (result && result.userId) {
|
|
24
|
+
req.user = result;
|
|
25
|
+
next();
|
|
26
|
+
} else {
|
|
27
|
+
res.status(401).json({ error: 'čŊ·å
įŧå―' });
|
|
28
|
+
}
|
|
29
|
+
} catch (e) {
|
|
30
|
+
res.status(401).json({ error: 'čŪĪčŊåĪąčīĨ' });
|
|
31
|
+
}
|
|
32
|
+
}, async function (req, res) {
|
|
33
|
+
try {
|
|
34
|
+
const apiKey = process.env.WORKFLOW_AI_API_KEY;
|
|
35
|
+
if (!apiKey) {
|
|
36
|
+
return res.status(500).json({ error: 'čŊ·å
é
į―ŪįŊåĒåé WORKFLOW_AI_API_KEY' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const baseURL = process.env.WORKFLOW_AI_BASE_URL || 'https://api.openai.com/v1';
|
|
40
|
+
const model = process.env.WORKFLOW_AI_MODEL || 'gpt-4o';
|
|
41
|
+
|
|
42
|
+
const { steps, formFields, userRequest } = req.body;
|
|
43
|
+
|
|
44
|
+
if (!userRequest) {
|
|
45
|
+
return res.status(400).json({ error: 'čŊ·čūå
Ĩéæąæčŋ°' });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Build form fields description
|
|
49
|
+
const fieldsDesc = (formFields || [])
|
|
50
|
+
.filter(f => f.code)
|
|
51
|
+
.map(f => ` - ${f.name || f.code} (įžį : ${f.code}, įąŧå: ${f.type || 'text'})`)
|
|
52
|
+
.join('\n');
|
|
53
|
+
|
|
54
|
+
const systemPrompt = `ä― æŊäļäļŠäļäļįåŪĄæđæĩįĻčŪūčŪĄåļãä― įäŧŧåĄæŊæ đæŪįĻæ·įčŠįķčŊčĻæčŋ°ïžäŋŪæđåŪĄæđæĩįĻįæĨéŠĪåŪäđïžJSON æ žåžïžã
|
|
55
|
+
|
|
56
|
+
## æĨéŠĪæ°æŪįŧæ
|
|
57
|
+
|
|
58
|
+
æŊäļŠæĨéŠĪ(step)æŊäļäļŠ JSON åŊđ蹥ïžåŋ
éĄŧå
åŦäŧĨäļåæŪĩïž
|
|
59
|
+
- _id: åįŽĶäļēïžåŊäļæ čŊãå·ēææĨéŠĪäŋįå _idïžæ°æĨéŠĪä―ŋįĻ "step-" + å°ååæŊæ°åéæšäļēïžåĶ "step-a1b2c3d4"ïž
|
|
60
|
+
- name: åįŽĶäļēïžæĨéŠĪåį§°
|
|
61
|
+
- step_type: åįŽĶäļēïžįąŧåïžåŠč―æŊäŧĨäļäđäļïž
|
|
62
|
+
- "start" â åžå§čįđïžæĩįĻåŋ
éĄŧæäļäŧ
æäļäļŠïž
|
|
63
|
+
- "end" â įŧæčįđïžæĩįĻåŋ
éĄŧæäļäŧ
æäļäļŠïž
|
|
64
|
+
- "submit" â åĄŦåæĨéŠĪ
|
|
65
|
+
- "sign" â åŪĄæđæĨéŠĪ
|
|
66
|
+
- "counterSign" â äžįūæĨéŠĪïžåĪäššåæķåŪĄæđïž
|
|
67
|
+
- "condition" â æĄäŧķåæŊčįđ
|
|
68
|
+
- deal_type: åįŽĶäļē, åĪįäšščšŦäŧ―ïžsubmit/sign/counterSign åŋ
åĄŦïžïžåŊéåžïž
|
|
69
|
+
- "pickupAtRuntime" â åŪĄæđæķæåŪ
|
|
70
|
+
- "specifyUser" â æåŪäššå
|
|
71
|
+
- "applicantSuperior" â įģčŊ·äššįäļįš§
|
|
72
|
+
- "applicant" â įģčŊ·äšš
|
|
73
|
+
- "hrRole" â æåŪč§čē
|
|
74
|
+
- "applicantRole" â æåŪåŪĄæđåēä―
|
|
75
|
+
- posx, posy: æ°åïžčįđä―į―Ūãįšĩåäŧäļå°äļæåïžåžå§čįđ posx=200,posy=40ïžäđåæŊæĨ y åĒå įšĶ120
|
|
76
|
+
- timeout_hours: æ°åïžčķ
æķå°æķæ°ïžéŧčŪĪ 168
|
|
77
|
+
- lines: æ°įŧïžäŧå―åæĨéŠĪåšåįčŋįšŋïžæŊæĄčŋįšŋå
åŦïž
|
|
78
|
+
- _id: åįŽĶäļēïžčŋįšŋåŊäļæ čŊïžįĻ "line-" + éæšäļē
|
|
79
|
+
- name: åįŽĶäļēïžčŋįšŋåį§°ïžåŊįĐšïž
|
|
80
|
+
- to_step: åįŽĶäļēïžįŪæ æĨéŠĪį _id
|
|
81
|
+
- state: åįŽĶäļēïž"submitted"ïžæäšĪïž/ "approved"ïžæ ļåïž/ "rejected"ïžéĐģåïž
|
|
82
|
+
- condition: åįŽĶäļēïžæĄäŧķčĄĻčūūåžïžäŧ
æĄäŧķčįđįåšįšŋéčĶïžïžæ žåžåĶ {åæŪĩįžį } > 10000
|
|
83
|
+
|
|
84
|
+
## č§å
|
|
85
|
+
1. æĩįĻåŋ
éĄŧæäļäŧ
æäļäļŠ start åäļäļŠ end čįđ
|
|
86
|
+
2. æææĨéŠĪåŋ
éĄŧéčŋ lines äšįļčŋéïžäļč―æåĪįŦčįđ
|
|
87
|
+
3. äŧ start åžå§åŋ
éĄŧč―æįŧå°čūū end
|
|
88
|
+
4. åŪĄæđæĨéŠĪ(sign)éåļļæäļĪæĄåšįšŋïžapprovedïžæ ļåïžå°äļäļæĨïžrejectedïžéĐģåïžåå°åĄŦåæäļäļæĨ
|
|
89
|
+
5. æĄäŧķčįđ(condition)æåĪæĄåšįšŋïžæŊæĄåļĶäļåį condition čĄĻčūūåž
|
|
90
|
+
6. äļčĶæ·ŧå notifyãconcurrentStartãconcurrentEnd įąŧåįæĨéŠĪ
|
|
91
|
+
7. äŋŪæđæķå°―åŊč―äŋįįĻæ·å·ēææĨéŠĪį _id åčŪūį―ŪïžåŠæđéčĶæđįéĻå
|
|
92
|
+
8. čŋååŪæīį steps æ°įŧ
|
|
93
|
+
|
|
94
|
+
## čūåšæ žåž
|
|
95
|
+
įīæĨčŋå JSON æ°įŧïžäļčĶæäŧŧä―å
ķäŧæåãč§Ģéãmarkdown æ čŪ°ã`;
|
|
96
|
+
|
|
97
|
+
const userContent = `## å―åčĄĻååæŪĩ
|
|
98
|
+
${fieldsDesc || 'ïžæ åæŪĩäŋĄæŊïž'}
|
|
99
|
+
|
|
100
|
+
## å―åæĩįĻæĨéŠĪ
|
|
101
|
+
${JSON.stringify(steps || [], null, 2)}
|
|
102
|
+
|
|
103
|
+
## įĻæ·éæą
|
|
104
|
+
${userRequest}
|
|
105
|
+
|
|
106
|
+
čŊ·čŋåäŋŪæđåįåŪæī steps JSON æ°įŧã`;
|
|
107
|
+
|
|
108
|
+
const response = await axios.post(`${baseURL}/chat/completions`, {
|
|
109
|
+
model,
|
|
110
|
+
messages: [
|
|
111
|
+
{ role: 'system', content: systemPrompt },
|
|
112
|
+
{ role: 'user', content: userContent },
|
|
113
|
+
],
|
|
114
|
+
temperature: 0.3,
|
|
115
|
+
max_tokens: 8000,
|
|
116
|
+
}, {
|
|
117
|
+
headers: {
|
|
118
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
119
|
+
'Content-Type': 'application/json',
|
|
120
|
+
},
|
|
121
|
+
timeout: 120000,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const content = response.data?.choices?.[0]?.message?.content;
|
|
125
|
+
if (!content) {
|
|
126
|
+
return res.status(500).json({ error: 'AI æŠčŋåææå
åŪđ' });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Try to parse the JSON from the response
|
|
130
|
+
let stepsResult;
|
|
131
|
+
try {
|
|
132
|
+
let text = content.trim();
|
|
133
|
+
// Strip markdown code block if present
|
|
134
|
+
if (text.startsWith('```json')) text = text.slice(7);
|
|
135
|
+
else if (text.startsWith('```')) text = text.slice(3);
|
|
136
|
+
if (text.endsWith('```')) text = text.slice(0, -3);
|
|
137
|
+
text = text.trim();
|
|
138
|
+
|
|
139
|
+
// Find JSON array
|
|
140
|
+
const arrStart = text.indexOf('[');
|
|
141
|
+
const arrEnd = text.lastIndexOf(']');
|
|
142
|
+
if (arrStart !== -1 && arrEnd > arrStart) {
|
|
143
|
+
stepsResult = JSON.parse(text.substring(arrStart, arrEnd + 1));
|
|
144
|
+
} else {
|
|
145
|
+
stepsResult = JSON.parse(text);
|
|
146
|
+
}
|
|
147
|
+
} catch (parseErr) {
|
|
148
|
+
return res.status(500).json({
|
|
149
|
+
error: 'AI čŋåå
åŪđæ æģč§Ģæäļš JSON',
|
|
150
|
+
raw: content,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!Array.isArray(stepsResult)) {
|
|
155
|
+
return res.status(500).json({ error: 'AI čŋåįäļæŊæĨéŠĪæ°įŧ' });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Basic validation
|
|
159
|
+
const hasStart = stepsResult.some(s => s.step_type === 'start');
|
|
160
|
+
const hasEnd = stepsResult.some(s => s.step_type === 'end');
|
|
161
|
+
if (!hasStart || !hasEnd) {
|
|
162
|
+
return res.status(500).json({ error: 'AI čŋåįæĩįĻįžšå°åžå§æįŧæčįđ' });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return res.json({ steps: stepsResult });
|
|
166
|
+
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('AI workflow design error:', error?.response?.data || error?.message || error);
|
|
169
|
+
if (error.response) {
|
|
170
|
+
const status = error.response.status;
|
|
171
|
+
const data = error.response.data;
|
|
172
|
+
if (status === 401 || status === 403) {
|
|
173
|
+
return res.status(500).json({ error: 'AI API åŊéĨæ æææéäļčķģïžčŊ·æĢæĨ WORKFLOW_AI_API_KEY' });
|
|
174
|
+
}
|
|
175
|
+
if (status === 429) {
|
|
176
|
+
return res.status(500).json({ error: 'AI API čŊ·æąéĒįčķ
éïžčŊ·įĻåéčŊ' });
|
|
177
|
+
}
|
|
178
|
+
return res.status(500).json({ error: data?.error?.message || 'AI API č°įĻåĪąčīĨ' });
|
|
179
|
+
}
|
|
180
|
+
return res.status(500).json({ error: error.message || 'AI æåĄč°įĻåĪąčīĨ' });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
exports.default = router;
|
|
@@ -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;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const router = express.Router();
|
|
4
|
+
const { requireAuthentication } = require("@steedos/auth");
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* New React-based workflow designer
|
|
9
|
+
* URL: /api/workflow/designer-v2
|
|
10
|
+
*/
|
|
11
|
+
router.get('/api/workflow/designer-v2', requireAuthentication, async (req, res) => {
|
|
12
|
+
try {
|
|
13
|
+
// In development, proxy to Vite dev server
|
|
14
|
+
if (process.env.NODE_ENV === 'development') {
|
|
15
|
+
return res.redirect('http://localhost:5173');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// In production, serve built React app
|
|
19
|
+
const designerPath = path.join(__dirname, '../../../designer/dist/index.html');
|
|
20
|
+
res.sendFile(designerPath);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
res.status(500).send({
|
|
23
|
+
errors: [{ errorMessage: e.message }]
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* New designer startup API
|
|
30
|
+
* URL: /api/designer-v2/startup
|
|
31
|
+
*/
|
|
32
|
+
router.get('/api/designer-v2/startup', requireAuthentication, async (req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const { getObject } = require('@steedos/objectql');
|
|
35
|
+
|
|
36
|
+
// Get workspace data
|
|
37
|
+
const space = await getObject("spaces").findOne(req.user.spaceId);
|
|
38
|
+
|
|
39
|
+
// Return initialization data
|
|
40
|
+
res.json({
|
|
41
|
+
user: {
|
|
42
|
+
userId: req.user.userId,
|
|
43
|
+
name: req.user.name,
|
|
44
|
+
spaceId: req.user.spaceId
|
|
45
|
+
},
|
|
46
|
+
space: space,
|
|
47
|
+
message: 'New designer startup API - ready for implementation'
|
|
48
|
+
});
|
|
49
|
+
} catch (e) {
|
|
50
|
+
res.status(500).send({
|
|
51
|
+
errors: [{ errorMessage: e.message }]
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Serve static assets for production (js, css, images, etc.)
|
|
58
|
+
*/
|
|
59
|
+
const designerDistPath = path.join(__dirname, '../../../designer/dist');
|
|
60
|
+
router.use('/api/workflow/designer-v2', express.static(designerDistPath));
|
|
61
|
+
|
|
62
|
+
exports.default = router;
|