@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.
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ name: flow_selector_mobile
2
+ is_active: true
3
+ label: æĩįϋ选æ‹Đ(į§ŧåŠĻįŦŊ)
4
+ pageAssignments: []
5
+ render_engine: amis
6
+ type: app
7
+ widgets: []
@@ -214,7 +214,7 @@
214
214
  "data": {},
215
215
  "id": "u:d37465183f56",
216
216
  "className": "steedos-instance-related-view-wrapper flex justify-center",
217
- "bodyClassName": "p-0 flex flex-1 overflow-hidden h-full",
217
+ "bodyClassName": "p-0 flex flex-1 h-full",
218
218
  "name": "amis-root-workflow",
219
219
  "initApi": {
220
220
  "url": "/api/workflow/v2/instance/${recordId}/permission",
@@ -302,6 +302,12 @@
302
302
  ".steedos-amis-instance-view-content": {
303
303
  "max-width": "none !important"
304
304
  },
305
+ ".steedos-instance-print-wrapper .liquid-amis-container": {
306
+ "overflow": "visible !important"
307
+ },
308
+ ".steedos-instance-print-wrapper .instance-template .table-page-body": {
309
+ "width": "100%"
310
+ },
305
311
  ".simplify-traces .step-type-start":{
306
312
  "display": "none !important"
307
313
  },
@@ -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;