Compare commits
24 commits
main
...
feat/grafi
| Author | SHA1 | Date | |
|---|---|---|---|
| 60ff00802c | |||
| b5084c028e | |||
| 587dad5cf9 | |||
| 0fd05f638f | |||
| aa61e00af6 | |||
| 7e2ffb42fe | |||
| dd26ea132d | |||
| 50a3df5c18 | |||
| e7f2272c30 | |||
| ef9955257e | |||
| 6890a38546 | |||
| 590178b8f2 | |||
| e3c93dc220 | |||
| 600e0c87dc | |||
| 9e36075f0e | |||
| 3a7a34a4f3 | |||
| c13489e232 | |||
| 4bf6677bc5 | |||
| 8860f49714 | |||
| 74dc7b85f8 | |||
| 66a7a6fa56 | |||
| 294803e66c | |||
| ae630201ba | |||
| 7fb96451a5 |
501 changed files with 26695 additions and 13420 deletions
144
.cursor/plans/implement_rbac_roles_page_8dd9fac6.plan.md
Normal file
144
.cursor/plans/implement_rbac_roles_page_8dd9fac6.plan.md
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
# Implement RBAC Roles Page
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Implement the RBAC roles admin page following the exact pattern used in `mandates.ts`. This includes creating the API file, custom hook for state management, updating the page configuration with CreateButton header button, and adding translations in all three languages (German, English, French).
|
||||||
|
|
||||||
|
## Files to Create/Modify
|
||||||
|
|
||||||
|
### 1. Create API File: `frontend_nyla/src/api/roleApi.ts`
|
||||||
|
- Follow the pattern from `mandateApi.ts`
|
||||||
|
- Implement all required endpoints:
|
||||||
|
- `fetchRoles()` - GET /api/rbac/roles (with pagination support)
|
||||||
|
- `fetchRoleById()` - GET /api/rbac/roles/{roleId}
|
||||||
|
- `fetchRoleOptions()` - GET /api/rbac/roles/options
|
||||||
|
- `createRole()` - POST /api/rbac/roles
|
||||||
|
- `updateRole()` - PUT /api/rbac/roles/{roleId}
|
||||||
|
- `deleteRole()` - DELETE /api/rbac/roles/{roleId}
|
||||||
|
- Include TypeScript types: `Role`, `RoleUpdateData`, `PaginationParams`, `PaginatedResponse`
|
||||||
|
|
||||||
|
### 2. Create Hook: `frontend_nyla/src/hooks/useAdminRbacRoles.ts`
|
||||||
|
- Follow the exact pattern from `useAdminMandates.ts`
|
||||||
|
- Create two hooks:
|
||||||
|
- `useRbacRoles()` - Main hook for data fetching and state management
|
||||||
|
- Fetch roles with pagination support
|
||||||
|
- Fetch attributes from `/api/attributes/Role` using `fetchAttributes(request, 'Role')`
|
||||||
|
- Fetch permissions using `checkPermission('DATA', 'Role')`
|
||||||
|
- Implement `generateEditFieldsFromAttributes()` using `attributeTypeMapper` utilities
|
||||||
|
- Implement `generateCreateFieldsFromAttributes()` using `attributeTypeMapper` utilities
|
||||||
|
- Implement `ensureAttributesLoaded()` for EditActionButton
|
||||||
|
- Implement optimistic updates (`removeOptimistically`, `updateOptimistically`)
|
||||||
|
- Return pagination info, attributes, permissions, and all required functions
|
||||||
|
- `useRbacRoleOperations()` - Operations hook for CRUD
|
||||||
|
- `handleRoleDelete()` - Delete with loading state tracking
|
||||||
|
- `handleRoleCreate()` - Create with error handling
|
||||||
|
- `handleRoleUpdate()` - Update with error handling
|
||||||
|
- Track loading states in Sets (deletingRoles, editingRoles, creatingRole)
|
||||||
|
- Return error states (deleteError, createError, updateError)
|
||||||
|
|
||||||
|
### 3. Update Page Configuration: `frontend_nyla/src/core/PageManager/data/pages/admin/rbac-role.ts`
|
||||||
|
- Follow the exact structure from `mandates.ts`
|
||||||
|
- Import `FaPlus` from `react-icons/fa` for the create button icon
|
||||||
|
- Create `createRbacRolesHook()` factory function that:
|
||||||
|
- Uses `useRbacRoles()` and `useRbacRoleOperations()`
|
||||||
|
- Converts attributes to columns using `attributesToColumns()` helper
|
||||||
|
- Implements `handleDeleteSingle` and `handleDeleteMultiple` callbacks
|
||||||
|
- Returns all required data for FormGeneratorTable
|
||||||
|
- Update `rbacRolePageData`:
|
||||||
|
- Add header button with `FaPlus` icon for creating roles (following mandates.ts pattern):
|
||||||
|
```typescript
|
||||||
|
headerButtons: [
|
||||||
|
{
|
||||||
|
id: 'add-role',
|
||||||
|
label: 'admin.rbac-role.new_button',
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'md',
|
||||||
|
icon: FaPlus,
|
||||||
|
formConfig: {
|
||||||
|
fields: [], // Empty array - fields will be generated dynamically from attributes
|
||||||
|
popupTitle: 'admin.rbac-role.modal.create.title',
|
||||||
|
popupSize: 'medium',
|
||||||
|
createOperationName: 'handleRoleCreate',
|
||||||
|
successMessage: 'admin.rbac-role.create.success',
|
||||||
|
errorMessage: 'admin.rbac-role.create.error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
- Add table content section with:
|
||||||
|
- `hookFactory: createRbacRolesHook`
|
||||||
|
- Action buttons: edit and delete (following mandates pattern)
|
||||||
|
- Configure edit button with `fetchItemFunctionName: 'fetchRoleById'`
|
||||||
|
- Configure delete button with proper operation names
|
||||||
|
- Add permission-based disabled logic
|
||||||
|
- Keep existing privilege checker (sysadmin only)
|
||||||
|
|
||||||
|
### 4. Update Translations: All three locale files
|
||||||
|
- **German (`frontend_nyla/src/locales/de.ts`)**: Add missing translations after line 756:
|
||||||
|
- `'admin.rbac-role.new_button': 'Rolle hinzufügen'`
|
||||||
|
- `'admin.rbac-role.action.edit': 'Bearbeiten'`
|
||||||
|
- `'admin.rbac-role.action.delete': 'Löschen'`
|
||||||
|
- `'admin.rbac-role.modal.create.title': 'Neue Rolle erstellen'`
|
||||||
|
- `'admin.rbac-role.create.success': 'Rolle erfolgreich erstellt'`
|
||||||
|
- `'admin.rbac-role.create.error': 'Fehler beim Erstellen der Rolle'`
|
||||||
|
|
||||||
|
- **English (`frontend_nyla/src/locales/en.ts`)**: Add missing translations after line 756:
|
||||||
|
- `'admin.rbac-role.new_button': 'Add Role'`
|
||||||
|
- `'admin.rbac-role.action.edit': 'Edit'`
|
||||||
|
- `'admin.rbac-role.action.delete': 'Delete'`
|
||||||
|
- `'admin.rbac-role.modal.create.title': 'Create New Role'`
|
||||||
|
- `'admin.rbac-role.create.success': 'Role created successfully'`
|
||||||
|
- `'admin.rbac-role.create.error': 'Error creating role'`
|
||||||
|
|
||||||
|
- **French (`frontend_nyla/src/locales/fr.ts`)**: Add missing translations after line 756:
|
||||||
|
- `'admin.rbac-role.new_button': 'Ajouter un rôle'`
|
||||||
|
- `'admin.rbac-role.action.edit': 'Modifier'`
|
||||||
|
- `'admin.rbac-role.action.delete': 'Supprimer'`
|
||||||
|
- `'admin.rbac-role.modal.create.title': 'Créer un nouveau rôle'`
|
||||||
|
- `'admin.rbac-role.create.success': 'Rôle créé avec succès'`
|
||||||
|
- `'admin.rbac-role.create.error': 'Erreur lors de la création du rôle'`
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### API File Structure
|
||||||
|
- Use `ApiRequestFunction` type from `useApi`
|
||||||
|
- Support pagination parameters (page, pageSize, sort, filters, search)
|
||||||
|
- Handle both paginated and non-paginated responses
|
||||||
|
- Use `/api/rbac/roles` as base URL
|
||||||
|
- Use `/api/attributes/Role` for attributes endpoint
|
||||||
|
|
||||||
|
### Hook Pattern
|
||||||
|
- Use `useApiRequest` hook for API calls
|
||||||
|
- Use `usePermissions` hook for permission checking
|
||||||
|
- Use `getUserDataCache()` to check authentication before fetching
|
||||||
|
- Implement attribute type mapping using utilities from `attributeTypeMapper.ts`:
|
||||||
|
- `isCheckboxType()`, `isSelectType()`, `isMultiselectType()`, `isDateTimeType()`, `isTextareaType()`
|
||||||
|
- Filter out non-editable fields (id, readonly fields, etc.)
|
||||||
|
- Handle options arrays and option references
|
||||||
|
|
||||||
|
### Page Configuration Pattern
|
||||||
|
- Use `attributesToColumns()` helper to convert attributes to column config
|
||||||
|
- Disable filtering for date/timestamp fields using `isDateTimeType()`
|
||||||
|
- Configure action buttons with proper field mappings and operation names
|
||||||
|
- Use permission-based disabled logic for buttons
|
||||||
|
- Set `entityType: 'Role'` for EditActionButton
|
||||||
|
- Add header button using CreateButton component pattern (via formConfig in headerButtons)
|
||||||
|
|
||||||
|
## Key Dependencies
|
||||||
|
- `useApiRequest` from `hooks/useApi`
|
||||||
|
- `usePermissions` from `hooks/usePermissions`
|
||||||
|
- `fetchAttributes` from `api/attributesApi`
|
||||||
|
- `attributeTypeMapper` utilities from `utils/attributeTypeMapper`
|
||||||
|
- `FormGeneratorTable` component
|
||||||
|
- `EditActionButton` and `DeleteActionButton` components
|
||||||
|
- `CreateButton` component (rendered via PageRenderer from headerButtons formConfig)
|
||||||
|
- `FaPlus` icon from `react-icons/fa`
|
||||||
|
|
||||||
|
## Testing Considerations
|
||||||
|
- Verify all API endpoints are called correctly
|
||||||
|
- Ensure attributes are fetched from `/api/attributes/Role`
|
||||||
|
- Verify permission checks work correctly
|
||||||
|
- Test create, edit, delete operations
|
||||||
|
- Verify optimistic updates work
|
||||||
|
- Check that date/timestamp fields are not filterable
|
||||||
|
- Verify CreateButton appears in header and opens create modal
|
||||||
|
- Verify translations work in all three languages
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
name: Deploy Nyla Frontend to Integration
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- int
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
SERVER_HOST: porta-int.poweron.swiss
|
|
||||||
SERVER_USER: ubuntu
|
|
||||||
APP_DIR: /srv/nyla/current
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Setup SSH
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
printf '%s\n' "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
|
|
||||||
chmod 600 ~/.ssh/deploy_key
|
|
||||||
echo "StrictHostKeyChecking=no" >> ~/.ssh/config
|
|
||||||
echo "UserKnownHostsFile=/dev/null" >> ~/.ssh/config
|
|
||||||
|
|
||||||
- name: Deploy
|
|
||||||
run: |
|
|
||||||
ssh -i ~/.ssh/deploy_key ${{ env.SERVER_USER }}@${{ env.SERVER_HOST }} "
|
|
||||||
set -e
|
|
||||||
cd ${{ env.APP_DIR }}
|
|
||||||
git remote set-url origin ssh://git@git.poweron.swiss:2222/PowerOn/ui-nyla.git
|
|
||||||
git fetch origin int
|
|
||||||
git reset --hard origin/int
|
|
||||||
cp config/env-int.env .env
|
|
||||||
rm -f config/env-*.env
|
|
||||||
npm ci
|
|
||||||
npm run build:int
|
|
||||||
"
|
|
||||||
|
|
||||||
- name: Health Check
|
|
||||||
run: |
|
|
||||||
HTTP_STATUS=$(curl -sk -o /dev/null -w "%{http_code}" \
|
|
||||||
https://${{ env.SERVER_HOST }}/ || echo "000")
|
|
||||||
if [ "$HTTP_STATUS" = "200" ]; then
|
|
||||||
echo "Health check passed! (HTTP $HTTP_STATUS)"
|
|
||||||
else
|
|
||||||
echo "Health check returned HTTP $HTTP_STATUS"
|
|
||||||
fi
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
name: Deploy Nyla Frontend to Production
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
SERVER_HOST: porta.poweron.swiss
|
|
||||||
SERVER_USER: ubuntu
|
|
||||||
APP_DIR: /srv/nyla/current
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Setup SSH
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
printf '%s\n' "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
|
|
||||||
chmod 600 ~/.ssh/deploy_key
|
|
||||||
echo "StrictHostKeyChecking=no" >> ~/.ssh/config
|
|
||||||
echo "UserKnownHostsFile=/dev/null" >> ~/.ssh/config
|
|
||||||
|
|
||||||
- name: Deploy
|
|
||||||
run: |
|
|
||||||
ssh -i ~/.ssh/deploy_key ${{ env.SERVER_USER }}@${{ env.SERVER_HOST }} "
|
|
||||||
set -e
|
|
||||||
cd ${{ env.APP_DIR }}
|
|
||||||
git remote set-url origin ssh://git@git.poweron.swiss:2222/PowerOn/ui-nyla.git
|
|
||||||
git fetch origin main
|
|
||||||
git reset --hard origin/main
|
|
||||||
cp config/env-prod.env .env
|
|
||||||
rm -f config/env-*.env
|
|
||||||
npm ci
|
|
||||||
npm run build:prod
|
|
||||||
"
|
|
||||||
|
|
||||||
- name: Health Check
|
|
||||||
run: |
|
|
||||||
HTTP_STATUS=$(curl -sk -o /dev/null -w "%{http_code}" \
|
|
||||||
https://${{ env.SERVER_HOST }}/ || echo "000")
|
|
||||||
if [ "$HTTP_STATUS" = "200" ]; then
|
|
||||||
echo "Health check passed! (HTTP $HTTP_STATUS)"
|
|
||||||
else
|
|
||||||
echo "Health check returned HTTP $HTTP_STATUS"
|
|
||||||
fi
|
|
||||||
71
.github/workflows/poweron_nyla_int.yml
vendored
Normal file
71
.github/workflows/poweron_nyla_int.yml
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
name: Deploy Nyla Frontend to Integration
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- int
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Cancel in-progress runs when a new run is triggered (saves logs/storage)
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Copy integration environment file
|
||||||
|
run: |
|
||||||
|
cp config/env-poweron-nyla-int.env .env
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm install express
|
||||||
|
|
||||||
|
- name: Build React app for integration
|
||||||
|
run: npm run build:int
|
||||||
|
|
||||||
|
- name: Prepare deployment package
|
||||||
|
run: |
|
||||||
|
# Create deployment package with build files and necessary configs
|
||||||
|
mkdir deploy
|
||||||
|
cp -r dist/* deploy/
|
||||||
|
# Create a simple server.js for serving the app
|
||||||
|
echo "const express = require('express');" > deploy/server.js
|
||||||
|
echo "const path = require('path');" >> deploy/server.js
|
||||||
|
echo "const app = express();" >> deploy/server.js
|
||||||
|
echo "app.use(express.static(path.join(__dirname)));" >> deploy/server.js
|
||||||
|
echo "app.get('/*', function(req, res) { res.sendFile(path.join(__dirname, 'index.html')); });" >> deploy/server.js
|
||||||
|
echo "const port = process.env.PORT || 8080;" >> deploy/server.js
|
||||||
|
echo "app.listen(port, () => console.log('Server running on port', port));" >> deploy/server.js
|
||||||
|
# Create a new package.json for deployment
|
||||||
|
echo '{
|
||||||
|
"name": "frontend-int",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2"
|
||||||
|
}
|
||||||
|
}' > deploy/package.json
|
||||||
|
|
||||||
|
- name: 'Deploy to Azure Web App'
|
||||||
|
uses: azure/webapps-deploy@v3
|
||||||
|
with:
|
||||||
|
app-name: 'poweron-nyla-int'
|
||||||
|
publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_POWERON_NYLA_INT }}
|
||||||
|
package: ./deploy
|
||||||
71
.github/workflows/poweron_nyla_main.yml
vendored
Normal file
71
.github/workflows/poweron_nyla_main.yml
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
name: Deploy Nyla Frontend to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Cancel in-progress runs when a new run is triggered (saves logs/storage)
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Copy production environment file
|
||||||
|
run: |
|
||||||
|
cp config/env-poweron-nyla-prod.env .env
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm install express
|
||||||
|
|
||||||
|
- name: Build React app for production
|
||||||
|
run: npm run build:prod
|
||||||
|
|
||||||
|
- name: Prepare deployment package
|
||||||
|
run: |
|
||||||
|
# Create deployment package with build files and necessary configs
|
||||||
|
mkdir deploy
|
||||||
|
cp -r dist/* deploy/
|
||||||
|
# Create a simple server.js for serving the app
|
||||||
|
echo "const express = require('express');" > deploy/server.js
|
||||||
|
echo "const path = require('path');" >> deploy/server.js
|
||||||
|
echo "const app = express();" >> deploy/server.js
|
||||||
|
echo "app.use(express.static(path.join(__dirname)));" >> deploy/server.js
|
||||||
|
echo "app.get('/*', function(req, res) { res.sendFile(path.join(__dirname, 'index.html')); });" >> deploy/server.js
|
||||||
|
echo "const port = process.env.PORT || 8080;" >> deploy/server.js
|
||||||
|
echo "app.listen(port, () => console.log('Server running on port', port));" >> deploy/server.js
|
||||||
|
# Create a new package.json for deployment
|
||||||
|
echo '{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2"
|
||||||
|
}
|
||||||
|
}' > deploy/package.json
|
||||||
|
|
||||||
|
- name: 'Deploy to Azure Web App'
|
||||||
|
uses: azure/webapps-deploy@v3
|
||||||
|
with:
|
||||||
|
app-name: 'poweron-nyla'
|
||||||
|
publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_POWERON_NYLA }}
|
||||||
|
package: ./deploy
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -31,7 +31,4 @@ dist-ssr
|
||||||
.cursorignore
|
.cursorignore
|
||||||
|
|
||||||
# Keep environment files in config/ (naming: env-<workflow>.env)
|
# Keep environment files in config/ (naming: env-<workflow>.env)
|
||||||
!config/env-*.env
|
!config/env-*.env
|
||||||
|
|
||||||
tsc-errors.txt
|
|
||||||
scripts/i18n_missing_report.md
|
|
||||||
190
config/config.ts
190
config/config.ts
|
|
@ -1,24 +1,178 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* Configuration — reads mandatory env vars set by .env (copied from config/env-*.env by CI).
|
* Simple Configuration Service
|
||||||
*
|
* Centralized access to environment variables with fallbacks
|
||||||
* NO silent fallbacks for critical values.
|
|
||||||
* If VITE_API_BASE_URL is missing the app fails loudly at startup.
|
|
||||||
*
|
|
||||||
* Vite replaces import.meta.env.VITE_* statically at build time.
|
|
||||||
* Dynamic access via import.meta.env[key] does NOT work in production builds.
|
|
||||||
* Therefore each variable must be accessed with its literal property name.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const _apiBaseUrl: string = import.meta.env.VITE_API_BASE_URL;
|
// API Configuration
|
||||||
|
export const getApiBaseUrl = (): string => {
|
||||||
|
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
|
||||||
|
};
|
||||||
|
|
||||||
if (!_apiBaseUrl) {
|
export const getApiTimeout = (): number => {
|
||||||
throw new Error(
|
return parseInt(import.meta.env.VITE_API_TIMEOUT || '10000');
|
||||||
'Missing required env variable: VITE_API_BASE_URL. Ensure .env is present (cp config/env-<env>.env .env).'
|
};
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getApiBaseUrl = (): string => _apiBaseUrl;
|
// App Configuration
|
||||||
|
export const getAppName = (): string => {
|
||||||
|
return import.meta.env.VITE_APP_NAME || 'PowerOn';
|
||||||
|
};
|
||||||
|
|
||||||
export const getAppName = (): string => import.meta.env.VITE_APP_NAME || 'PowerOn';
|
export const getAppVersion = (): string => {
|
||||||
|
return import.meta.env.VITE_APP_VERSION || '0.0.0';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAppEnvironment = (): string => {
|
||||||
|
return import.meta.env.VITE_APP_ENVIRONMENT || 'dev';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Environment Detection
|
||||||
|
export const isDevelopment = (): boolean => {
|
||||||
|
return import.meta.env.MODE === 'development' || getAppEnvironment() === 'dev';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isProduction = (): boolean => {
|
||||||
|
return import.meta.env.MODE === 'production' || getAppEnvironment() === 'prod';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isIntegration = (): boolean => {
|
||||||
|
return getAppEnvironment() === 'int';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debug Configuration
|
||||||
|
export const isDebugMode = (): boolean => {
|
||||||
|
return import.meta.env.VITE_DEBUG === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLogLevel = (): string => {
|
||||||
|
return import.meta.env.VITE_LOG_LEVEL || 'info';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isConsoleLogsEnabled = (): boolean => {
|
||||||
|
return import.meta.env.VITE_ENABLE_CONSOLE_LOGS === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Microsoft Authentication
|
||||||
|
export const getMicrosoftClientId = (): string | undefined => {
|
||||||
|
return import.meta.env.VITE_MICROSOFT_CLIENT_ID;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMicrosoftTenantId = (): string | undefined => {
|
||||||
|
return import.meta.env.VITE_MICROSOFT_TENANT_ID;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEntraClientSecret = (): string | undefined => {
|
||||||
|
return import.meta.env.VITE_ENTRA_CLIENT_SECRET;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEntraAuthority = (): string | undefined => {
|
||||||
|
return import.meta.env.VITE_ENTRA_AUTHORITY;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEntraRedirectPath = (): string | undefined => {
|
||||||
|
return import.meta.env.VITE_ENTRA_REDIRECT_PATH;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEntraRedirectUri = (): string | undefined => {
|
||||||
|
return import.meta.env.VITE_ENTRA_REDIRECT_URI;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Feature Flags (if needed in the future)
|
||||||
|
export const isFeatureEnabled = (feature: string): boolean => {
|
||||||
|
const envKey = `VITE_ENABLE_${feature.toUpperCase()}`;
|
||||||
|
return import.meta.env[envKey] === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Analytics and Monitoring
|
||||||
|
export const isAnalyticsEnabled = (): boolean => {
|
||||||
|
return import.meta.env.VITE_ENABLE_ANALYTICS === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isErrorReportingEnabled = (): boolean => {
|
||||||
|
return import.meta.env.VITE_ENABLE_ERROR_REPORTING === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isPerformanceMonitoringEnabled = (): boolean => {
|
||||||
|
return import.meta.env.VITE_ENABLE_PERFORMANCE_MONITORING === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Development Server (for dev environment)
|
||||||
|
export const getDevServerPort = (): number => {
|
||||||
|
return parseInt(import.meta.env.VITE_DEV_SERVER_PORT || '5176');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDevServerHost = (): string => {
|
||||||
|
return import.meta.env.VITE_DEV_SERVER_HOST || 'localhost';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isDevServerHttps = (): boolean => {
|
||||||
|
return import.meta.env.VITE_DEV_SERVER_HTTPS === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Security Configuration
|
||||||
|
export const isHttpsEnabled = (): boolean => {
|
||||||
|
return import.meta.env.VITE_ENABLE_HTTPS === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isCspEnabled = (): boolean => {
|
||||||
|
return import.meta.env.VITE_ENABLE_CSP === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test Configuration
|
||||||
|
export const isMockDataEnabled = (): boolean => {
|
||||||
|
return import.meta.env.VITE_ENABLE_MOCK_DATA === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isTestMode = (): boolean => {
|
||||||
|
return import.meta.env.VITE_ENABLE_TEST_MODE === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convenience object for easy destructuring
|
||||||
|
export const config = {
|
||||||
|
// API
|
||||||
|
getApiBaseUrl,
|
||||||
|
getApiTimeout,
|
||||||
|
|
||||||
|
// App
|
||||||
|
getAppName,
|
||||||
|
getAppVersion,
|
||||||
|
getAppEnvironment,
|
||||||
|
|
||||||
|
// Environment
|
||||||
|
isDevelopment,
|
||||||
|
isProduction,
|
||||||
|
isIntegration,
|
||||||
|
|
||||||
|
// Debug
|
||||||
|
isDebugMode,
|
||||||
|
getLogLevel,
|
||||||
|
isConsoleLogsEnabled,
|
||||||
|
|
||||||
|
// Microsoft Auth
|
||||||
|
getMicrosoftClientId,
|
||||||
|
getMicrosoftTenantId,
|
||||||
|
getEntraClientSecret,
|
||||||
|
getEntraAuthority,
|
||||||
|
getEntraRedirectPath,
|
||||||
|
getEntraRedirectUri,
|
||||||
|
|
||||||
|
// Features
|
||||||
|
isFeatureEnabled,
|
||||||
|
|
||||||
|
// Analytics
|
||||||
|
isAnalyticsEnabled,
|
||||||
|
isErrorReportingEnabled,
|
||||||
|
isPerformanceMonitoringEnabled,
|
||||||
|
|
||||||
|
// Dev Server
|
||||||
|
getDevServerPort,
|
||||||
|
getDevServerHost,
|
||||||
|
isDevServerHttps,
|
||||||
|
|
||||||
|
// Security
|
||||||
|
isHttpsEnabled,
|
||||||
|
isCspEnabled,
|
||||||
|
|
||||||
|
// Test
|
||||||
|
isMockDataEnabled,
|
||||||
|
isTestMode,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,5 @@
|
||||||
# Consumed by: Vite build (title) + SPA runtime (getApiBaseUrl / getAppName)
|
# Consumed by: Vite build (title) + SPA runtime (getApiBaseUrl / getAppName)
|
||||||
# Auth and secrets live on the gateway — never in frontend env.
|
# Auth and secrets live on the gateway — never in frontend env.
|
||||||
|
|
||||||
VITE_API_BASE_URL="http://localhost:8000"
|
VITE_API_BASE_URL="http://localhost:8000/"
|
||||||
VITE_APP_NAME=PowerOn Nyla dev
|
VITE_APP_NAME=PowerOn Nyla dev
|
||||||
|
|
@ -2,5 +2,5 @@
|
||||||
# Consumed by: Vite build (title) + SPA runtime (getApiBaseUrl / getAppName)
|
# Consumed by: Vite build (title) + SPA runtime (getApiBaseUrl / getAppName)
|
||||||
# Auth and secrets live on the gateway — never in frontend env.
|
# Auth and secrets live on the gateway — never in frontend env.
|
||||||
|
|
||||||
VITE_API_BASE_URL=https://api-int.poweron.swiss
|
VITE_API_BASE_URL=https://gateway-int.poweron.swiss
|
||||||
VITE_APP_NAME=Poweron Nyla int
|
VITE_APP_NAME=Poweron Nyla int
|
||||||
|
|
@ -2,5 +2,5 @@
|
||||||
# Consumed by: Vite build (title) + SPA runtime (getApiBaseUrl / getAppName)
|
# Consumed by: Vite build (title) + SPA runtime (getApiBaseUrl / getAppName)
|
||||||
# Auth and secrets live on the gateway — never in frontend env.
|
# Auth and secrets live on the gateway — never in frontend env.
|
||||||
|
|
||||||
VITE_API_BASE_URL=https://api.poweron.swiss
|
VITE_API_BASE_URL=https://gateway-prod.poweron.swiss
|
||||||
VITE_APP_NAME=PowerOn Nyla
|
VITE_APP_NAME=PowerOn Nyla
|
||||||
|
|
@ -1,3 +1,12 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
// Export simple configuration service
|
||||||
// All rights reserved.
|
export * from './config';
|
||||||
export { getApiBaseUrl, getAppName } from './config';
|
|
||||||
|
// Re-export commonly used functions
|
||||||
|
export {
|
||||||
|
getApiBaseUrl,
|
||||||
|
getAppName,
|
||||||
|
isDevelopment,
|
||||||
|
isProduction,
|
||||||
|
isDebugMode,
|
||||||
|
config
|
||||||
|
} from './config';
|
||||||
|
|
|
||||||
2
env.d.ts
vendored
2
env.d.ts
vendored
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
|
|
|
||||||
|
|
@ -185,10 +185,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p class="legal-meta" style="margin-bottom: 0.75rem; color: #64748b; font-size: 0.85rem;">Company & legal details · May 2026</p>
|
<p>© 2025 PowerOn AI Platform. All rights reserved.</p>
|
||||||
<p><strong>PowerOn AG</strong> · Birmensdorferstrasse 94 · CH-8003 Zürich · Switzerland</p>
|
|
||||||
<p><a href="mailto:p.motsch@poweron.swiss">p.motsch@poweron.swiss</a></p>
|
|
||||||
<p style="margin-top: 1rem;">© 2026 PowerOn. All rights reserved.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="last-updated">
|
<div class="last-updated">
|
||||||
<strong>Last Updated:</strong> May 2026
|
<strong>Last Updated:</strong> August 2025
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-section">
|
<div class="content-section">
|
||||||
|
|
@ -272,13 +272,8 @@
|
||||||
<h2>Contact Us</h2>
|
<h2>Contact Us</h2>
|
||||||
<p>If you have any questions about this Privacy Policy or our data practices, please contact us:</p>
|
<p>If you have any questions about this Privacy Policy or our data practices, please contact us:</p>
|
||||||
<div class="highlight-box">
|
<div class="highlight-box">
|
||||||
<p><strong>Email:</strong> <a href="mailto:p.motsch@poweron.swiss">p.motsch@poweron.swiss</a></p>
|
<p><strong>Email:</strong> privacy@poweron-ai.com</p>
|
||||||
<p><strong>Address:</strong><br>
|
<p><strong>Address:</strong> PowerOn AI Platform, Privacy Team</p>
|
||||||
PowerOn AG<br>
|
|
||||||
Birmensdorferstrasse 94<br>
|
|
||||||
CH-8003 Zürich<br>
|
|
||||||
Switzerland
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -288,7 +283,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p>© 2026 PowerOn. All rights reserved.</p>
|
<p>© 2025 PowerOn AI Platform. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="last-updated">
|
<div class="last-updated">
|
||||||
<strong>Last Updated:</strong> May 2026
|
<strong>Last Updated:</strong> August 2025
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-section">
|
<div class="content-section">
|
||||||
|
|
@ -315,13 +315,8 @@
|
||||||
<h2>Contact Information</h2>
|
<h2>Contact Information</h2>
|
||||||
<p>If you have any questions about these Terms of Service, please contact us:</p>
|
<p>If you have any questions about these Terms of Service, please contact us:</p>
|
||||||
<div class="highlight-box">
|
<div class="highlight-box">
|
||||||
<p><strong>Email:</strong> <a href="mailto:p.motsch@poweron.swiss">p.motsch@poweron.swiss</a></p>
|
<p><strong>Email:</strong> legal@poweron-ai.com</p>
|
||||||
<p><strong>Address:</strong><br>
|
<p><strong>Address:</strong> PowerOn AI Platform, Legal Department</p>
|
||||||
PowerOn AG<br>
|
|
||||||
Birmensdorferstrasse 94<br>
|
|
||||||
CH-8003 Zürich<br>
|
|
||||||
Switzerland
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -331,7 +326,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p>© 2026 PowerOn. All rights reserved.</p>
|
<p>© 2025 PowerOn AI Platform. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
15027
scripts/i18n_missing_report.md
Normal file
15027
scripts/i18n_missing_report.md
Normal file
File diff suppressed because it is too large
Load diff
27
src/App.tsx
27
src/App.tsx
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* App.tsx
|
* App.tsx
|
||||||
*
|
*
|
||||||
|
|
@ -41,12 +39,11 @@ import { GDPRPage } from './pages/GDPR';
|
||||||
import StorePage from './pages/Store';
|
import StorePage from './pages/Store';
|
||||||
import { IntegrationsOverviewPage } from './pages/IntegrationsOverviewPage';
|
import { IntegrationsOverviewPage } from './pages/IntegrationsOverviewPage';
|
||||||
import { FeatureViewPage } from './pages/FeatureView';
|
import { FeatureViewPage } from './pages/FeatureView';
|
||||||
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminLogsPage, AdminDemoConfigPage } from './pages/admin';
|
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminLogsPage, AdminDemoConfigPage, AdminDatabaseHealthPage } from './pages/admin';
|
||||||
import { AdminMandateWizardPage, AdminInvitationWizardPage } from './pages/admin/wizards';
|
import { AdminMandateWizardPage, AdminInvitationWizardPage } from './pages/admin/wizards';
|
||||||
import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
|
import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
|
||||||
import { BillingDataView, BillingAdmin, BillingMandateView, AdminSubscriptionsPage } from './pages/billing';
|
import { BillingDataView, BillingAdmin, BillingMandateView, AdminSubscriptionsPage } from './pages/billing';
|
||||||
import { WorkflowAutomationPage } from './pages/workflowAutomation/WorkflowAutomationHubPage';
|
import { AutomationsDashboardPage } from './pages/AutomationsDashboardPage';
|
||||||
import { RagInventoryPage } from './pages/RagInventoryPage';
|
|
||||||
import { ComplianceAuditPage } from './pages/ComplianceAuditPage';
|
import { ComplianceAuditPage } from './pages/ComplianceAuditPage';
|
||||||
function App() {
|
function App() {
|
||||||
// Load saved theme preference and set app name on app mount
|
// Load saved theme preference and set app name on app mount
|
||||||
|
|
@ -126,20 +123,15 @@ function App() {
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* ============================================== */}
|
{/* ============================================== */}
|
||||||
{/* WORKFLOW AUTOMATION (System-Komponente) */}
|
{/* AUTOMATIONS DASHBOARD */}
|
||||||
{/* ============================================== */}
|
{/* ============================================== */}
|
||||||
<Route path="workflow-automation" element={<WorkflowAutomationPage />} />
|
<Route path="automations" element={<AutomationsDashboardPage />} />
|
||||||
|
|
||||||
{/* ============================================== */}
|
|
||||||
{/* RAG INVENTORY */}
|
|
||||||
{/* ============================================== */}
|
|
||||||
<Route path="rag-inventory" element={<RagInventoryPage />} />
|
|
||||||
|
|
||||||
{/* Legacy top-level routes – redirect to dashboard (migrated to feature-instance routes) */}
|
{/* Legacy top-level routes – redirect to dashboard (migrated to feature-instance routes) */}
|
||||||
|
<Route path="chatbot" element={<Navigate to="/" replace />} />
|
||||||
<Route path="pek" element={<Navigate to="/" replace />} />
|
<Route path="pek" element={<Navigate to="/" replace />} />
|
||||||
<Route path="speech" element={<Navigate to="/" replace />} />
|
<Route path="speech" element={<Navigate to="/" replace />} />
|
||||||
|
|
||||||
|
|
||||||
{/* ============================================== */}
|
{/* ============================================== */}
|
||||||
{/* FEATURE-INSTANZ ROUTES */}
|
{/* FEATURE-INSTANZ ROUTES */}
|
||||||
{/* /mandates/:mandateId/:featureCode/:instanceId */}
|
{/* /mandates/:mandateId/:featureCode/:instanceId */}
|
||||||
|
|
@ -173,8 +165,13 @@ function App() {
|
||||||
<Route path="templates" element={<FeatureViewPage view="templates" />} />
|
<Route path="templates" element={<FeatureViewPage view="templates" />} />
|
||||||
<Route path="logs" element={<FeatureViewPage view="logs" />} />
|
<Route path="logs" element={<FeatureViewPage view="logs" />} />
|
||||||
|
|
||||||
{/* Workspace Editor */}
|
{/* Workspace + Automation2 Editor */}
|
||||||
<Route path="editor" element={<FeatureViewPage view="editor" />} />
|
<Route path="editor" element={<FeatureViewPage view="editor" />} />
|
||||||
|
<Route path="rag-insights" element={<FeatureViewPage view="rag-insights" />} />
|
||||||
|
|
||||||
|
{/* Automation2: legacy workflows URL → editor */}
|
||||||
|
<Route path="workflows" element={<Navigate to="../editor" replace />} />
|
||||||
|
<Route path="workflows-tasks" element={<FeatureViewPage view="workflows-tasks" />} />
|
||||||
|
|
||||||
{/* Teams Bot Feature Views */}
|
{/* Teams Bot Feature Views */}
|
||||||
<Route path="sessions" element={<FeatureViewPage view="sessions" />} />
|
<Route path="sessions" element={<FeatureViewPage view="sessions" />} />
|
||||||
|
|
@ -221,7 +218,7 @@ function App() {
|
||||||
<Route path="subscriptions" element={<AdminSubscriptionsPage />} />
|
<Route path="subscriptions" element={<AdminSubscriptionsPage />} />
|
||||||
<Route path="logs" element={<AdminLogsPage />} />
|
<Route path="logs" element={<AdminLogsPage />} />
|
||||||
<Route path="languages" element={null} />
|
<Route path="languages" element={null} />
|
||||||
<Route path="database-health" element={null} />
|
<Route path="database-health" element={<AdminDatabaseHealthPage />} />
|
||||||
<Route path="demo-config" element={<AdminDemoConfigPage />} />
|
<Route path="demo-config" element={<AdminDemoConfigPage />} />
|
||||||
<Route path="mandate-wizard" element={<AdminMandateWizardPage />} />
|
<Route path="mandate-wizard" element={<AdminMandateWizardPage />} />
|
||||||
<Route path="invitation-wizard" element={<AdminInvitationWizardPage />} />
|
<Route path="invitation-wizard" element={<AdminInvitationWizardPage />} />
|
||||||
|
|
|
||||||
64
src/api.ts
64
src/api.ts
|
|
@ -1,10 +1,25 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
// api.ts
|
// api.ts
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { addCSRFTokenToHeaders, getCSRFToken, generateAndStoreCSRFToken } from './utils/csrfUtils';
|
import { addCSRFTokenToHeaders, getCSRFToken, generateAndStoreCSRFToken } from './utils/csrfUtils';
|
||||||
import { clearUserDataCache, getUserDataCache } from './utils/userCache';
|
import { clearUserDataCache, getUserDataCache } from './utils/userCache';
|
||||||
|
|
||||||
|
// Utility function to resolve hostname to IP address
|
||||||
|
const resolveHostnameToIP = async (hostname: string): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
// For localhost, return as is
|
||||||
|
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||||
|
return hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For production domains, we can't directly resolve IP due to CORS
|
||||||
|
// But we can show the hostname which is more useful anyway
|
||||||
|
return hostname;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not resolve hostname to IP:', error);
|
||||||
|
return hostname;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract mandate/instance context from current URL.
|
* Extract mandate/instance context from current URL.
|
||||||
* URL pattern: /mandates/:mandateId/:featureCode/:instanceId/...
|
* URL pattern: /mandates/:mandateId/:featureCode/:instanceId/...
|
||||||
|
|
@ -29,25 +44,52 @@ const getContextFromUrl = (): { mandateId?: string; instanceId?: string } => {
|
||||||
|
|
||||||
import { getApiBaseUrl } from '../config/config';
|
import { getApiBaseUrl } from '../config/config';
|
||||||
|
|
||||||
const _baseUrl = getApiBaseUrl();
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
console.log(`[api] Backend: ${_baseUrl} | env: ${import.meta.env.MODE}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: _baseUrl,
|
baseURL: getApiBaseUrl(),
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
|
// FastAPI expects repeat-style array query params (``?ids=1&ids=2``).
|
||||||
|
// Axios v1.x default would render ``?ids[]=1&ids[]=2``, which FastAPI
|
||||||
|
// silently drops -- e.g. ``trackerIds`` filters on the Redmine stats
|
||||||
|
// endpoint never reach the route. Setting ``indexes: null`` switches
|
||||||
|
// the URLSearchParams visitor to repeat format. Applies globally so
|
||||||
|
// every endpoint with array query params gets it for free.
|
||||||
paramsSerializer: { indexes: null },
|
paramsSerializer: { indexes: null },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add a request interceptor to add the auth token, context headers
|
// Add a request interceptor to add the auth token, context headers, and log backend IP
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
async (config) => {
|
async (config) => {
|
||||||
// Add auth token if available (otherwise httpOnly cookies are used automatically)
|
// Log backend information
|
||||||
|
const backendUrl = config.baseURL || getApiBaseUrl();
|
||||||
|
console.log(`🌐 Communicating with backend: ${backendUrl}`);
|
||||||
|
|
||||||
|
// Try to resolve and log the IP address
|
||||||
|
if (backendUrl) {
|
||||||
|
try {
|
||||||
|
const url = new URL(backendUrl);
|
||||||
|
const hostname = url.hostname;
|
||||||
|
const resolvedIP = await resolveHostnameToIP(hostname);
|
||||||
|
|
||||||
|
console.log(`📍 Backend hostname: ${hostname}`);
|
||||||
|
console.log(`🔗 Full backend URL: ${backendUrl}`);
|
||||||
|
console.log(`🌍 Resolved address: ${resolvedIP}`);
|
||||||
|
|
||||||
|
// Log environment info
|
||||||
|
console.log(`🏗️ Environment: ${import.meta.env.MODE}`);
|
||||||
|
console.log(`⚙️ API Base URL: ${getApiBaseUrl()}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not parse backend URL:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for auth token in localStorage and add to headers
|
||||||
const authToken = localStorage.getItem('authToken');
|
const authToken = localStorage.getItem('authToken');
|
||||||
if (authToken && config.headers) {
|
if (authToken && config.headers) {
|
||||||
config.headers.Authorization = `Bearer ${authToken}`;
|
config.headers.Authorization = `Bearer ${authToken}`;
|
||||||
|
console.log('🔑 Using Bearer token for authentication');
|
||||||
|
} else {
|
||||||
|
// Fallback: httpOnly cookies
|
||||||
|
console.log('🍪 Using httpOnly cookies for authentication (automatic)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send app language to backend so i18n labels match the UI
|
// Send app language to backend so i18n labels match the UI
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { ApiRequestOptions } from '../hooks/useApi';
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
import type { AttributeType } from '../utils/attributeTypeMapper';
|
import type { AttributeType } from '../utils/attributeTypeMapper';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { ApiRequestOptions } from '../hooks/useApi';
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { addCSRFTokenToHeaders } from '../utils/csrfUtils';
|
import { addCSRFTokenToHeaders } from '../utils/csrfUtils';
|
||||||
|
|
@ -14,30 +12,14 @@ export interface LoginRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
type: 'local_auth_success' | 'mfa_required' | 'mfa_setup_required';
|
type: 'local_auth_success';
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
tokenType?: string;
|
tokenType?: string;
|
||||||
authenticationAuthority?: string;
|
authenticationAuthority?: string;
|
||||||
mfaToken?: string;
|
|
||||||
provisioningUri?: string;
|
|
||||||
label?: any;
|
label?: any;
|
||||||
fieldLabels?: any;
|
fieldLabels?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MfaVerifyRequest {
|
|
||||||
token: string;
|
|
||||||
code: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MfaSetupResponse {
|
|
||||||
provisioningUri: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MfaStatusResponse {
|
|
||||||
mfaEnabled: boolean;
|
|
||||||
mfaRequired: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RegisterData {
|
export interface RegisterData {
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|
@ -334,36 +316,3 @@ export async function logoutApi(): Promise<void> {
|
||||||
await api.post('/api/local/logout');
|
await api.post('/api/local/logout');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// MFA API FUNCTIONS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export async function mfaVerifyApi(data: MfaVerifyRequest): Promise<LoginResponse> {
|
|
||||||
const response = await api.post<LoginResponse>('/api/mfa/verify', data);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function mfaSetupApi(): Promise<MfaSetupResponse> {
|
|
||||||
const response = await api.post<MfaSetupResponse>('/api/mfa/setup');
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function mfaConfirmApi(code: string, token?: string): Promise<{ mfaEnabled: boolean }> {
|
|
||||||
if (token) {
|
|
||||||
const response = await api.post<{ mfaEnabled: boolean }>('/api/mfa/confirm', { code, token });
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
const response = await api.post<{ mfaEnabled: boolean }>('/api/mfa/confirm-authenticated', { code });
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function mfaStatusApi(): Promise<MfaStatusResponse> {
|
|
||||||
const response = await api.get<MfaStatusResponse>('/api/mfa/status');
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function mfaDisableApi(code: string): Promise<{ mfaEnabled: boolean }> {
|
|
||||||
const response = await api.post<{ mfaEnabled: boolean }>('/api/mfa/disable', { code });
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { ApiRequestOptions } from '../hooks/useApi';
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
329
src/api/chatbotApi.ts
Normal file
329
src/api/chatbotApi.ts
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
import api from '../api';
|
||||||
|
import { addCSRFTokenToHeaders, getCSRFToken, generateAndStoreCSRFToken } from '../utils/csrfUtils';
|
||||||
|
import { Message } from '../components/UiComponents/Messages/MessagesTypes';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TYPES & INTERFACES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface UserInputRequest {
|
||||||
|
input: string;
|
||||||
|
workflowId?: string;
|
||||||
|
files?: Array<{ id: string; name: string }>;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatbotWorkflow {
|
||||||
|
id: string;
|
||||||
|
mandateId?: string; // Optional - not in ChatbotConversation
|
||||||
|
featureInstanceId?: string; // From ChatbotConversation
|
||||||
|
status: string;
|
||||||
|
name?: string;
|
||||||
|
currentRound?: number;
|
||||||
|
currentTask?: number;
|
||||||
|
currentAction?: number;
|
||||||
|
startedAt?: number;
|
||||||
|
lastActivity?: number;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StartChatbotRequest {
|
||||||
|
prompt: string;
|
||||||
|
listFileId?: string[];
|
||||||
|
userLanguage?: string;
|
||||||
|
workflowId?: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StartChatbotResponse extends ChatbotWorkflow {
|
||||||
|
// Workflow object returned from start endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatDataItem {
|
||||||
|
type: 'message' | 'log' | 'stat' | 'document' | 'stopped' | 'status' | 'chunk';
|
||||||
|
createdAt?: number;
|
||||||
|
item?: Message | any;
|
||||||
|
label?: string; // For status events
|
||||||
|
content?: string; // For chunk events (token-by-token streaming)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type for the request function passed to API functions
|
||||||
|
export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<any>;
|
||||||
|
|
||||||
|
// Type for SSE event handler
|
||||||
|
export type SSEEventHandler = (item: ChatDataItem) => void;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// API REQUEST FUNCTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new chatbot workflow or continue an existing one with SSE streaming
|
||||||
|
* Endpoint: POST /api/chatbot/{instanceId}/start/stream
|
||||||
|
*
|
||||||
|
* @param instanceId - Feature Instance ID
|
||||||
|
* @param requestBody - Request body with prompt and optional workflowId
|
||||||
|
* @param onEvent - Callback function called for each SSE event
|
||||||
|
* @param onError - Optional error callback
|
||||||
|
* @param onComplete - Optional completion callback
|
||||||
|
* @returns Promise that resolves when stream completes
|
||||||
|
*/
|
||||||
|
export async function startChatbotStreamApi(
|
||||||
|
instanceId: string,
|
||||||
|
requestBody: StartChatbotRequest,
|
||||||
|
onEvent: SSEEventHandler,
|
||||||
|
onError?: (error: Error) => void,
|
||||||
|
onComplete?: () => void
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Prepare request body
|
||||||
|
console.log('[startChatbotStreamApi] instanceId:', instanceId);
|
||||||
|
console.log('[startChatbotStreamApi] requestBody received:', JSON.stringify(requestBody, null, 2));
|
||||||
|
|
||||||
|
const body: any = {
|
||||||
|
prompt: requestBody.prompt,
|
||||||
|
...(requestBody.listFileId && requestBody.listFileId.length > 0 && { listFileId: requestBody.listFileId }),
|
||||||
|
...(requestBody.userLanguage && { userLanguage: requestBody.userLanguage }),
|
||||||
|
...(requestBody.metadata && { metadata: requestBody.metadata })
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[startChatbotStreamApi] body being sent:', JSON.stringify(body, null, 2));
|
||||||
|
|
||||||
|
// Add workflowId to query params if provided
|
||||||
|
const url = requestBody.workflowId
|
||||||
|
? `/api/chatbot/${instanceId}/start/stream?workflowId=${encodeURIComponent(requestBody.workflowId)}`
|
||||||
|
: `/api/chatbot/${instanceId}/start/stream`;
|
||||||
|
|
||||||
|
// Get base URL from api instance
|
||||||
|
const baseURL = api.defaults.baseURL || '';
|
||||||
|
const fullURL = baseURL + url;
|
||||||
|
|
||||||
|
// Prepare headers with authentication and CSRF token
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add auth token if available
|
||||||
|
const authToken = localStorage.getItem('authToken');
|
||||||
|
if (authToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${authToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add CSRF token for POST requests
|
||||||
|
if (!getCSRFToken()) {
|
||||||
|
generateAndStoreCSRFToken();
|
||||||
|
}
|
||||||
|
addCSRFTokenToHeaders(headers);
|
||||||
|
|
||||||
|
// Use fetch for SSE streaming (POST with body)
|
||||||
|
const response = await fetch(fullURL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
credentials: 'include' // Include cookies for authentication
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error('Response body is null');
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode chunk and add to buffer
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
// Process complete SSE messages
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
try {
|
||||||
|
const jsonStr = line.slice(6); // Remove 'data: ' prefix
|
||||||
|
if (jsonStr.trim()) {
|
||||||
|
const item: ChatDataItem = JSON.parse(jsonStr);
|
||||||
|
console.log('[SSE] Received event:', item.type, item);
|
||||||
|
onEvent(item);
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn('Failed to parse SSE event:', line, parseError);
|
||||||
|
}
|
||||||
|
} else if (line.startsWith(':')) {
|
||||||
|
// Comment/keepalive line, ignore
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process any remaining buffer content
|
||||||
|
if (buffer.trim()) {
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
try {
|
||||||
|
const jsonStr = line.slice(6);
|
||||||
|
if (jsonStr.trim()) {
|
||||||
|
const item: ChatDataItem = JSON.parse(jsonStr);
|
||||||
|
onEvent(item);
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn('Failed to parse SSE event:', line, parseError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error in startChatbotStreamApi:', error);
|
||||||
|
if (onError) {
|
||||||
|
onError(error instanceof Error ? error : new Error(String(error)));
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop a running chatbot workflow
|
||||||
|
* Endpoint: POST /api/chatbot/{instanceId}/stop/{workflowId}
|
||||||
|
*/
|
||||||
|
export async function stopChatbotApi(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
instanceId: string,
|
||||||
|
workflowId: string
|
||||||
|
): Promise<ChatbotWorkflow> {
|
||||||
|
console.log('[stopChatbotApi] Calling stop endpoint:', `/api/chatbot/${instanceId}/stop/${workflowId}`, { instanceId, workflowId });
|
||||||
|
const data = await request({
|
||||||
|
url: `/api/chatbot/${instanceId}/stop/${workflowId}`,
|
||||||
|
method: 'post'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[stopChatbotApi] Stop response:', data);
|
||||||
|
return data as ChatbotWorkflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get chatbot threads/workflows
|
||||||
|
* Endpoint: GET /api/chatbot/{instanceId}/threads
|
||||||
|
*/
|
||||||
|
export async function getChatbotThreadsApi(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
instanceId: string,
|
||||||
|
pagination?: { page?: number; pageSize?: number }
|
||||||
|
): Promise<{ items: ChatbotWorkflow[]; metadata: any }> {
|
||||||
|
const paginationParam = pagination ? JSON.stringify(pagination) : undefined;
|
||||||
|
const requestParams = paginationParam
|
||||||
|
? { pagination: paginationParam }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
console.log(`[getChatbotThreadsApi] instanceId: ${instanceId}, params:`, requestParams);
|
||||||
|
|
||||||
|
const data = await request({
|
||||||
|
url: `/api/chatbot/${instanceId}/threads`,
|
||||||
|
method: 'get',
|
||||||
|
params: requestParams
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
console.log(`[getChatbotThreadsApi] Full response:`, JSON.stringify(data, null, 2));
|
||||||
|
console.log(`[getChatbotThreadsApi] Response structure:`, {
|
||||||
|
hasItems: !!data.items,
|
||||||
|
itemsLength: Array.isArray(data.items) ? data.items.length : 'not an array',
|
||||||
|
hasMetadata: !!data.metadata,
|
||||||
|
metadataKeys: data.metadata ? Object.keys(data.metadata) : []
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: Array.isArray(data.items) ? data.items : [],
|
||||||
|
metadata: data.pagination ?? data.metadata ?? {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific chatbot thread/workflow with its chat data
|
||||||
|
* Endpoint: GET /api/chatbot/{instanceId}/threads?workflowId={id}
|
||||||
|
*
|
||||||
|
* Backend returns: { workflow: ChatbotWorkflow, chatData: { items: ChatDataItem[] } }
|
||||||
|
*
|
||||||
|
* @param request - API request function
|
||||||
|
* @param instanceId - Feature Instance ID
|
||||||
|
* @param workflowId - ID of the workflow to fetch
|
||||||
|
* @returns Object containing workflow details and chatData with items array
|
||||||
|
*/
|
||||||
|
export async function getChatbotThreadApi(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
instanceId: string,
|
||||||
|
workflowId: string
|
||||||
|
): Promise<{ workflow: ChatbotWorkflow; chatData: { items: ChatDataItem[] } }> {
|
||||||
|
console.log(`[getChatbotThreadApi] instanceId: ${instanceId}, workflowId: ${workflowId}`);
|
||||||
|
|
||||||
|
const data = await request({
|
||||||
|
url: `/api/chatbot/${instanceId}/threads`,
|
||||||
|
method: 'get',
|
||||||
|
params: { workflowId }
|
||||||
|
}) as { workflow: ChatbotWorkflow; chatData: { items: ChatDataItem[] } };
|
||||||
|
|
||||||
|
console.log(`[getChatbotThreadApi] Full response for workflowId ${workflowId}:`, JSON.stringify(data, null, 2));
|
||||||
|
console.log(`[getChatbotThreadApi] Response structure:`, {
|
||||||
|
hasWorkflow: !!data.workflow,
|
||||||
|
workflowKeys: data.workflow ? Object.keys(data.workflow) : [],
|
||||||
|
hasChatData: !!data.chatData,
|
||||||
|
hasItems: !!data.chatData?.items,
|
||||||
|
chatDataKeys: data.chatData ? Object.keys(data.chatData) : [],
|
||||||
|
itemsLength: Array.isArray(data.chatData?.items) ? data.chatData.items.length : 'not an array',
|
||||||
|
chatDataTypes: Array.isArray(data.chatData?.items) ? data.chatData.items.map((item: ChatDataItem) => item?.type).filter(Boolean) : []
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
workflow: data.workflow,
|
||||||
|
chatData: data.chatData || { items: [] }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a chatbot workflow
|
||||||
|
* Endpoint: DELETE /api/chatbot/{instanceId}/{workflowId}
|
||||||
|
*
|
||||||
|
* @param request - API request function
|
||||||
|
* @param instanceId - Feature Instance ID
|
||||||
|
* @param workflowId - ID of the workflow to delete
|
||||||
|
* @returns Success status
|
||||||
|
*/
|
||||||
|
export async function deleteChatbotWorkflowApi(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
instanceId: string,
|
||||||
|
workflowId: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await request({
|
||||||
|
url: `/api/chatbot/${instanceId}/${workflowId}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error deleting chatbot workflow:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
|
||||||
* ClickUp API — ClickUp-specific functions for the workflow automation flow editor.
|
|
||||||
*
|
|
||||||
* Extracted from the legacy workflowApi.ts re-export shim so each integration
|
|
||||||
* lives in its own module.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ApiRequestFunction } from './workflowAutomationApi';
|
|
||||||
|
|
||||||
function _encodedConnectionId(connectionId: string): string {
|
|
||||||
return encodeURIComponent(connectionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ClickUp GET /task/{taskId} — list.id for resolving list-scoped fields when only task id is known. */
|
|
||||||
export async function fetchClickupTask(
|
|
||||||
request: ApiRequestFunction,
|
|
||||||
connectionId: string,
|
|
||||||
taskId: string
|
|
||||||
): Promise<Record<string, unknown>> {
|
|
||||||
const data = await request({
|
|
||||||
url: `/api/clickup/${_encodedConnectionId(connectionId)}/tasks/${encodeURIComponent(taskId)}`,
|
|
||||||
method: 'get',
|
|
||||||
});
|
|
||||||
return data && typeof data === 'object' ? (data as Record<string, unknown>) : {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ClickUp list metadata (statuses, etc.) — GET /api/clickup/{connectionId}/lists/{listId}. */
|
|
||||||
export async function fetchClickupList(
|
|
||||||
request: ApiRequestFunction,
|
|
||||||
connectionId: string,
|
|
||||||
listId: string
|
|
||||||
): Promise<Record<string, unknown>> {
|
|
||||||
const data = await request({
|
|
||||||
url: `/api/clickup/${_encodedConnectionId(connectionId)}/lists/${encodeURIComponent(listId)}`,
|
|
||||||
method: 'get',
|
|
||||||
});
|
|
||||||
return data && typeof data === 'object' ? (data as Record<string, unknown>) : {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ClickUp workspace/team (members for assignees) — GET /api/clickup/{connectionId}/teams/{teamId}. */
|
|
||||||
export async function fetchClickupTeam(
|
|
||||||
request: ApiRequestFunction,
|
|
||||||
connectionId: string,
|
|
||||||
teamId: string
|
|
||||||
): Promise<Record<string, unknown>> {
|
|
||||||
const data = await request({
|
|
||||||
url: `/api/clickup/${_encodedConnectionId(connectionId)}/teams/${encodeURIComponent(teamId)}`,
|
|
||||||
method: 'get',
|
|
||||||
});
|
|
||||||
return data && typeof data === 'object' ? (data as Record<string, unknown>) : {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ClickUp list custom fields (GET /api/clickup/{connectionId}/lists/{listId}/fields). */
|
|
||||||
export async function fetchClickupListFields(
|
|
||||||
request: ApiRequestFunction,
|
|
||||||
connectionId: string,
|
|
||||||
listId: string
|
|
||||||
): Promise<{ fields?: unknown[] } & Record<string, unknown>> {
|
|
||||||
const data = await request({
|
|
||||||
url: `/api/clickup/${_encodedConnectionId(connectionId)}/lists/${encodeURIComponent(listId)}/fields`,
|
|
||||||
method: 'get',
|
|
||||||
});
|
|
||||||
return (data && typeof data === 'object' ? data : {}) as { fields?: unknown[] } & Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ClickUp GET /list/{id}/task page (tasks in a list for relationship dropdowns). */
|
|
||||||
export interface ClickupListTaskItem {
|
|
||||||
id?: string;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchClickupListTasks(
|
|
||||||
request: ApiRequestFunction,
|
|
||||||
connectionId: string,
|
|
||||||
listId: string,
|
|
||||||
options?: { page?: number; includeClosed?: boolean }
|
|
||||||
): Promise<
|
|
||||||
{ tasks?: ClickupListTaskItem[]; last_page?: boolean } & Record<string, unknown>
|
|
||||||
> {
|
|
||||||
const data = await request({
|
|
||||||
url: `/api/clickup/${_encodedConnectionId(connectionId)}/lists/${encodeURIComponent(listId)}/tasks`,
|
|
||||||
method: 'get',
|
|
||||||
params: {
|
|
||||||
page: options?.page ?? 0,
|
|
||||||
include_closed: options?.includeClosed ?? false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return (data && typeof data === 'object' ? data : {}) as {
|
|
||||||
tasks?: ClickupListTaskItem[];
|
|
||||||
last_page?: boolean;
|
|
||||||
} & Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Paginated tasks in a list — for ClickUp relationship dropdowns and input.form „ClickUp-Aufgabe". */
|
|
||||||
export async function loadClickupListTasksForDropdown(
|
|
||||||
request: ApiRequestFunction,
|
|
||||||
connectionId: string,
|
|
||||||
listId: string
|
|
||||||
): Promise<Array<{ id: string; name: string }>> {
|
|
||||||
const acc: Array<{ id: string; name: string }> = [];
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const maxPages = 12;
|
|
||||||
const pageSizeHint = 100;
|
|
||||||
for (let page = 0; page < maxPages; page++) {
|
|
||||||
const data = await fetchClickupListTasks(request, connectionId, listId, {
|
|
||||||
page,
|
|
||||||
includeClosed: false,
|
|
||||||
});
|
|
||||||
if (data && typeof data === 'object' && 'error' in data && (data as { error?: unknown }).error) {
|
|
||||||
const err = (data as { error?: unknown }).error;
|
|
||||||
const body = (data as { body?: string }).body;
|
|
||||||
throw new Error(
|
|
||||||
typeof err === 'string' ? err + (body ? `: ${body.slice(0, 200)}` : '') : 'ClickUp API error'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const tasks = Array.isArray(data.tasks) ? data.tasks : [];
|
|
||||||
for (const t of tasks) {
|
|
||||||
const id = t?.id != null ? String(t.id) : '';
|
|
||||||
if (!id || seen.has(id)) continue;
|
|
||||||
seen.add(id);
|
|
||||||
acc.push({ id, name: String(t.name ?? id) });
|
|
||||||
}
|
|
||||||
const rawLast = (data as Record<string, unknown>).last_page;
|
|
||||||
const last =
|
|
||||||
rawLast === true ||
|
|
||||||
rawLast === 'true' ||
|
|
||||||
tasks.length === 0 ||
|
|
||||||
tasks.length < pageSizeHint;
|
|
||||||
if (last) break;
|
|
||||||
}
|
|
||||||
acc.sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { addCSRFTokenToHeaders, getCSRFToken, generateAndStoreCSRFToken } from '../utils/csrfUtils';
|
import { addCSRFTokenToHeaders, getCSRFToken, generateAndStoreCSRFToken } from '../utils/csrfUtils';
|
||||||
import { ApiRequestOptions } from '../hooks/useApi';
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { ApiRequestOptions } from '../hooks/useApi';
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -8,11 +6,17 @@ import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
export interface KnowledgePreferences {
|
export interface KnowledgePreferences {
|
||||||
schemaVersion?: number;
|
schemaVersion?: number;
|
||||||
|
neutralizeBeforeEmbed?: boolean;
|
||||||
mailContentDepth?: 'metadata' | 'snippet' | 'full';
|
mailContentDepth?: 'metadata' | 'snippet' | 'full';
|
||||||
mailIndexAttachments?: boolean;
|
mailIndexAttachments?: boolean;
|
||||||
filesIndexBinaries?: boolean;
|
filesIndexBinaries?: boolean;
|
||||||
|
mimeAllowlist?: string[];
|
||||||
clickupScope?: 'titles' | 'title_description' | 'with_comments';
|
clickupScope?: 'titles' | 'title_description' | 'with_comments';
|
||||||
clickupIndexAttachments?: boolean;
|
clickupIndexAttachments?: boolean;
|
||||||
|
surfaceToggles?: {
|
||||||
|
google?: { gmail?: boolean; drive?: boolean };
|
||||||
|
msft?: { sharepoint?: boolean; outlook?: boolean };
|
||||||
|
};
|
||||||
maxAgeDays?: number;
|
maxAgeDays?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -288,210 +292,3 @@ export async function submitInfomaniakToken(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// RAG KNOWLEDGE CONSENT & CONTROL
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export async function patchKnowledgeConsent(
|
|
||||||
request: ApiRequestFunction,
|
|
||||||
connectionId: string,
|
|
||||||
enabled: boolean
|
|
||||||
): Promise<{ connectionId: string; knowledgeIngestionEnabled: boolean; purged?: any; cancelledJobs?: number; bootstrapEnqueued?: boolean }> {
|
|
||||||
return await request({
|
|
||||||
url: `/api/connections/${connectionId}/knowledge-consent`,
|
|
||||||
method: 'patch',
|
|
||||||
data: { enabled }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function patchKnowledgePreferences(
|
|
||||||
request: ApiRequestFunction,
|
|
||||||
connectionId: string,
|
|
||||||
preferences: KnowledgePreferences
|
|
||||||
): Promise<{ connectionId: string; knowledgePreferences: KnowledgePreferences; updated: boolean }> {
|
|
||||||
return await request({
|
|
||||||
url: `/api/connections/${connectionId}/knowledge-preferences`,
|
|
||||||
method: 'patch',
|
|
||||||
data: { preferences }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function postKnowledgeStop(
|
|
||||||
request: ApiRequestFunction,
|
|
||||||
connectionId: string
|
|
||||||
): Promise<{ connectionId: string; cancelled: number }> {
|
|
||||||
return await request({
|
|
||||||
url: `/api/connections/${connectionId}/knowledge-stop`,
|
|
||||||
method: 'post'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RagLimits {
|
|
||||||
maxItems?: number;
|
|
||||||
maxBytes?: number;
|
|
||||||
maxFileSize?: number;
|
|
||||||
maxDepth?: number;
|
|
||||||
// ClickUp variant
|
|
||||||
maxTasks?: number;
|
|
||||||
maxWorkspaces?: number;
|
|
||||||
maxListsPerWorkspace?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DataSourceSettings {
|
|
||||||
ragLimits?: RagLimits;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CostEstimate {
|
|
||||||
estimatedTokens: number;
|
|
||||||
estimatedChf: number;
|
|
||||||
basis: {
|
|
||||||
kind: string;
|
|
||||||
limits: Record<string, number>;
|
|
||||||
assumptions: Record<string, any>;
|
|
||||||
notes: string;
|
|
||||||
};
|
|
||||||
sourceId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function patchDataSourceSettings(
|
|
||||||
request: ApiRequestFunction,
|
|
||||||
dataSourceId: string,
|
|
||||||
settings: DataSourceSettings
|
|
||||||
): Promise<{ sourceId: string; settings: DataSourceSettings; updated: boolean }> {
|
|
||||||
return await request({
|
|
||||||
url: `/api/datasources/${dataSourceId}/settings`,
|
|
||||||
method: 'patch',
|
|
||||||
data: { settings }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDataSourceCostEstimate(
|
|
||||||
request: ApiRequestFunction,
|
|
||||||
dataSourceId: string
|
|
||||||
): Promise<CostEstimate> {
|
|
||||||
return await request({
|
|
||||||
url: `/api/datasources/${dataSourceId}/cost-estimate`,
|
|
||||||
method: 'get'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flag toggles (neutralize / scope / ragIndexEnabled) now go through the
|
|
||||||
// generic UDB endpoint POST /api/udb/node/{key}/flag/{flag}; see
|
|
||||||
// `UdbSourcesProvider` and the wiki UDB reference page.
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// RAG INVENTORY
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export interface RagDataSourceDto {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
path: string;
|
|
||||||
sourceType: string;
|
|
||||||
/** Three-state inherit semantics on backend; UI reads as effective boolean from RAG inventory aggregator. */
|
|
||||||
ragIndexEnabled: boolean | null;
|
|
||||||
neutralize: boolean | null;
|
|
||||||
lastIndexed: number | null;
|
|
||||||
/** Distinct files indexed for this DataSource (one row per source document). */
|
|
||||||
fileCount: number;
|
|
||||||
/** Embedding-sized text fragments (one per ContentChunk row, ~400 tokens each). */
|
|
||||||
chunkCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RagConnectionDto {
|
|
||||||
id: string;
|
|
||||||
authority: string;
|
|
||||||
externalEmail: string;
|
|
||||||
knowledgeIngestionEnabled: boolean;
|
|
||||||
preferences: KnowledgePreferences;
|
|
||||||
dataSources: RagDataSourceDto[];
|
|
||||||
totalFiles: number;
|
|
||||||
totalChunks: number;
|
|
||||||
runningJobs: {
|
|
||||||
jobId: string;
|
|
||||||
progress: number;
|
|
||||||
/** Already translated server-side. */
|
|
||||||
progressMessage: string;
|
|
||||||
}[];
|
|
||||||
lastError?: { jobId: string; errorMessage: string; finishedAt: number | null } | null;
|
|
||||||
lastSuccess?: {
|
|
||||||
jobId: string;
|
|
||||||
finishedAt: number | null;
|
|
||||||
indexed: number;
|
|
||||||
skippedDuplicate: number;
|
|
||||||
skippedPolicy: number;
|
|
||||||
failed: number;
|
|
||||||
durationMs: number;
|
|
||||||
/** Name of the first budget that bit (e.g. "maxBytes", "maxItems", "maxTasks"); null if walk completed naturally. */
|
|
||||||
stoppedAtLimit?: string | null;
|
|
||||||
/** Effective limits used by the walker, for showing the value next to the limit name. */
|
|
||||||
limits?: Record<string, number>;
|
|
||||||
bytesProcessed?: number;
|
|
||||||
} | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RagFeatureDataSourceDto {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
tableName: string;
|
|
||||||
featureCode: string;
|
|
||||||
ragIndexEnabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RagFeatureInstanceDto {
|
|
||||||
featureInstanceId: string;
|
|
||||||
featureCode: string;
|
|
||||||
label: string;
|
|
||||||
mandateId: string;
|
|
||||||
fileCount: number;
|
|
||||||
chunkCount: number;
|
|
||||||
statusCounts: Record<string, number>;
|
|
||||||
dataSources: RagFeatureDataSourceDto[];
|
|
||||||
ragEnabled: boolean;
|
|
||||||
runningJobs?: {
|
|
||||||
jobId: string;
|
|
||||||
progress: number;
|
|
||||||
progressMessage: string;
|
|
||||||
}[];
|
|
||||||
lastError?: { jobId: string; errorMessage: string; finishedAt: number | null } | null;
|
|
||||||
lastSuccess?: {
|
|
||||||
jobId: string;
|
|
||||||
finishedAt: number | null;
|
|
||||||
indexed: number;
|
|
||||||
skippedDuplicate: number;
|
|
||||||
failed: number;
|
|
||||||
} | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RagInventoryDto {
|
|
||||||
connections: RagConnectionDto[];
|
|
||||||
featureInstances?: RagFeatureInstanceDto[];
|
|
||||||
totals: { files: number; chunks: number; bytes?: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RagActiveJobDto {
|
|
||||||
jobId: string;
|
|
||||||
connectionId: string;
|
|
||||||
connectionLabel?: string;
|
|
||||||
jobType: string;
|
|
||||||
progress: number | null;
|
|
||||||
/** Already translated server-side. */
|
|
||||||
progressMessage: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getRagInventoryMe(request: ApiRequestFunction): Promise<RagInventoryDto> {
|
|
||||||
return await request({ url: '/api/rag/inventory/me', method: 'get' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getRagInventoryMandate(request: ApiRequestFunction): Promise<RagInventoryDto> {
|
|
||||||
return await request({ url: '/api/rag/inventory/mandate', method: 'get' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getRagInventoryPlatform(request: ApiRequestFunction): Promise<any> {
|
|
||||||
return await request({ url: '/api/rag/inventory/platform', method: 'get' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getRagActiveJobs(request: ApiRequestFunction): Promise<RagActiveJobDto[]> {
|
|
||||||
return await request({ url: '/api/rag/inventory/jobs', method: 'get' });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* Features API
|
* Features API
|
||||||
*
|
*
|
||||||
|
|
@ -16,6 +14,8 @@ import type {
|
||||||
InstancePermissions,
|
InstancePermissions,
|
||||||
AccessLevel,
|
AccessLevel,
|
||||||
} from '../types/mandate';
|
} from '../types/mandate';
|
||||||
|
import { mandateDisplayLabel } from '../utils/mandateDisplayUtils';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// MOCK DATA (Temporär bis Backend bereit)
|
// MOCK DATA (Temporär bis Backend bereit)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -172,11 +172,56 @@ export async function fetchMyFeatures(): Promise<FeaturesMyResponse> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('📡 featuresApi: Fetching /api/features/my');
|
||||||
const response = await api.get<FeaturesMyResponse>('/api/features/my');
|
const response = await api.get<FeaturesMyResponse>('/api/features/my');
|
||||||
|
|
||||||
// Get the actual data (response.data contains the FeaturesMyResponse)
|
// Get the actual data (response.data contains the FeaturesMyResponse)
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
|
// DEBUG: Log all chatbot instances and their permissions
|
||||||
|
console.log('🔍 [DEBUG] featuresApi: Full response received', {
|
||||||
|
response,
|
||||||
|
data,
|
||||||
|
hasMandates: !!data?.mandates,
|
||||||
|
mandateCount: data?.mandates?.length || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data?.mandates) {
|
||||||
|
data.mandates.forEach(mandate => {
|
||||||
|
mandate.features.forEach(feature => {
|
||||||
|
if (feature.code === 'chatbot') {
|
||||||
|
console.log('🔍 [DEBUG] featuresApi: Found chatbot feature', {
|
||||||
|
mandateId: mandate.id,
|
||||||
|
mandateName: mandateDisplayLabel(mandate),
|
||||||
|
featureCode: feature.code,
|
||||||
|
instanceCount: feature.instances.length,
|
||||||
|
});
|
||||||
|
feature.instances.forEach(instance => {
|
||||||
|
console.log('🔍 [DEBUG] featuresApi: Chatbot Instance Details:', {
|
||||||
|
instanceId: instance.id,
|
||||||
|
instanceLabel: instance.instanceLabel,
|
||||||
|
featureCode: instance.featureCode,
|
||||||
|
userRoles: instance.userRoles,
|
||||||
|
permissions: instance.permissions,
|
||||||
|
views: instance.permissions?.views,
|
||||||
|
viewKeys: instance.permissions?.views ? Object.keys(instance.permissions.views) : [],
|
||||||
|
hasConversationsView: instance.permissions?.views?.['chatbot-conversations'] ||
|
||||||
|
instance.permissions?.views?.['ui.feature.chatbot.conversations'] ||
|
||||||
|
instance.permissions?.views?.['_all'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ featuresApi: Loaded features:', {
|
||||||
|
mandateCount: data?.mandates?.length || 0,
|
||||||
|
totalInstances: data?.mandates
|
||||||
|
?.flatMap(m => m.features)
|
||||||
|
?.flatMap(f => f.instances)
|
||||||
|
?.length || 0,
|
||||||
|
});
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ featuresApi: Error fetching features:', error);
|
console.error('❌ featuresApi: Error fetching features:', error);
|
||||||
|
|
@ -194,6 +239,7 @@ export async function fetchAvailableFeatures(): Promise<MandateFeature[]> {
|
||||||
return [
|
return [
|
||||||
{ code: 'trustee', label: 'Treuhand', icon: 'briefcase', instances: [] },
|
{ code: 'trustee', label: 'Treuhand', icon: 'briefcase', instances: [] },
|
||||||
{ code: 'chatworkflow', label: 'Workflow', icon: 'play_circle', instances: [] },
|
{ code: 'chatworkflow', label: 'Workflow', icon: 'play_circle', instances: [] },
|
||||||
|
{ code: 'chatbot', label: 'Chatbot', icon: 'chat', instances: [] },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { ApiRequestOptions } from '../hooks/useApi';
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -38,7 +36,6 @@ export interface PaginationParams {
|
||||||
search?: string;
|
search?: string;
|
||||||
viewKey?: string;
|
viewKey?: string;
|
||||||
groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>;
|
groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>;
|
||||||
owner?: 'all' | 'me' | 'shared';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaginatedResponse<T> {
|
export interface PaginatedResponse<T> {
|
||||||
|
|
@ -112,7 +109,6 @@ export async function fetchFiles(
|
||||||
if (params.search) paginationObj.search = params.search;
|
if (params.search) paginationObj.search = params.search;
|
||||||
if (params.viewKey) paginationObj.viewKey = params.viewKey;
|
if (params.viewKey) paginationObj.viewKey = params.viewKey;
|
||||||
if (params.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels;
|
if (params.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels;
|
||||||
if (params.owner) requestParams.owner = params.owner;
|
|
||||||
|
|
||||||
if (Object.keys(paginationObj).length > 0) {
|
if (Object.keys(paginationObj).length > 0) {
|
||||||
requestParams.pagination = JSON.stringify(paginationObj);
|
requestParams.pagination = JSON.stringify(paginationObj);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { ApiRequestOptions } from '../hooks/useApi';
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* Neutralization API
|
* Neutralization API
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { ApiRequestOptions } from '../hooks/useApi';
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { ApiRequestOptions } from '../hooks/useApi';
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { ApiRequestOptions } from '../hooks/useApi';
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import type { ApiRequestOptions } from '../hooks/useApi';
|
import type { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* Redmine API
|
* Redmine API
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { ApiRequestOptions } from '../hooks/useApi';
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* Store API
|
* Store API
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { ApiRequestOptions } from '../hooks/useApi';
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
export interface TableListViewRow {
|
export interface TableListViewRow {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import type { VoiceOption } from './voiceCatalogApi';
|
import type { VoiceOption } from './voiceCatalogApi';
|
||||||
|
|
||||||
|
|
@ -73,7 +71,6 @@ export interface TeamsbotConfig {
|
||||||
triggerCooldownSeconds: number;
|
triggerCooldownSeconds: number;
|
||||||
contextWindowSegments: number;
|
contextWindowSegments: number;
|
||||||
debugMode?: boolean;
|
debugMode?: boolean;
|
||||||
avatarFileId?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TeamsbotSessionStats {
|
export interface TeamsbotSessionStats {
|
||||||
|
|
@ -87,7 +84,6 @@ export interface TeamsbotSessionStats {
|
||||||
export interface StartSessionRequest {
|
export interface StartSessionRequest {
|
||||||
meetingLink: string;
|
meetingLink: string;
|
||||||
botName?: string;
|
botName?: string;
|
||||||
moduleId?: string;
|
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
joinMode?: TeamsbotJoinMode;
|
joinMode?: TeamsbotJoinMode;
|
||||||
sessionContext?: string;
|
sessionContext?: string;
|
||||||
|
|
@ -106,7 +102,6 @@ export interface ConfigUpdateRequest {
|
||||||
triggerCooldownSeconds?: number;
|
triggerCooldownSeconds?: number;
|
||||||
contextWindowSegments?: number;
|
contextWindowSegments?: number;
|
||||||
debugMode?: boolean;
|
debugMode?: boolean;
|
||||||
avatarFileId?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Voice option type re-exported from the central voice catalog API
|
// Voice option type re-exported from the central voice catalog API
|
||||||
|
|
@ -467,13 +462,6 @@ export function createSessionStream(instanceId: string, sessionId: string): Even
|
||||||
return new EventSource(url, { withCredentials: true });
|
return new EventSource(url, { withCredentials: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** SSE dashboard stream: periodic { type: 'dashboardState', sessions, modules } */
|
|
||||||
export function createDashboardStream(instanceId: string): EventSource {
|
|
||||||
const baseUrl = api.defaults.baseURL || '';
|
|
||||||
const url = `${baseUrl}/api/teamsbot/${instanceId}/dashboard/stream`;
|
|
||||||
return new EventSource(url, { withCredentials: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Debug Screenshots (SysAdmin only)
|
// Debug Screenshots (SysAdmin only)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
@ -604,9 +592,6 @@ export interface MeetingModule {
|
||||||
defaultDirectorPrompts?: string;
|
defaultDirectorPrompts?: string;
|
||||||
goals?: string;
|
goals?: string;
|
||||||
kpiTargets?: string;
|
kpiTargets?: string;
|
||||||
defaultMeetingLink?: string;
|
|
||||||
defaultBotName?: string;
|
|
||||||
defaultAvatarFileId?: string;
|
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -617,7 +602,6 @@ export async function listModules(instanceId: string): Promise<MeetingModule[]>
|
||||||
|
|
||||||
export async function createModule(instanceId: string, body: {
|
export async function createModule(instanceId: string, body: {
|
||||||
title: string; seriesType?: string; defaultBotId?: string; goals?: string; kpiTargets?: string;
|
title: string; seriesType?: string; defaultBotId?: string; goals?: string; kpiTargets?: string;
|
||||||
defaultMeetingLink?: string; defaultBotName?: string; defaultAvatarFileId?: string;
|
|
||||||
}): Promise<MeetingModule> {
|
}): Promise<MeetingModule> {
|
||||||
const response = await api.post(`/api/teamsbot/${instanceId}/modules`, body);
|
const response = await api.post(`/api/teamsbot/${instanceId}/modules`, body);
|
||||||
return response.data?.module;
|
return response.data?.module;
|
||||||
|
|
@ -636,31 +620,3 @@ export async function updateModule(instanceId: string, moduleId: string, body: P
|
||||||
export async function deleteModule(instanceId: string, moduleId: string): Promise<void> {
|
export async function deleteModule(instanceId: string, moduleId: string): Promise<void> {
|
||||||
await api.delete(`/api/teamsbot/${instanceId}/modules/${moduleId}`);
|
await api.delete(`/api/teamsbot/${instanceId}/modules/${moduleId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MediaFileInfo {
|
|
||||||
id: string;
|
|
||||||
fileName: string;
|
|
||||||
mimeType: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listMediaFiles(): Promise<MediaFileInfo[]> {
|
|
||||||
const response = await api.get('/api/files/list', {
|
|
||||||
params: { pagination: JSON.stringify({ pageSize: 500 }) },
|
|
||||||
});
|
|
||||||
const data = response.data;
|
|
||||||
let items: any[];
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
items = data;
|
|
||||||
} else if (Array.isArray(data?.items)) {
|
|
||||||
items = data.items;
|
|
||||||
} else {
|
|
||||||
console.warn('[listMediaFiles] unexpected response shape:', Object.keys(data || {}));
|
|
||||||
items = [];
|
|
||||||
}
|
|
||||||
const filtered = items.filter((f: any) => {
|
|
||||||
const mime = (f.mimeType || '').toLowerCase();
|
|
||||||
return mime.startsWith('image/') || mime.startsWith('video/');
|
|
||||||
});
|
|
||||||
console.log(`[listMediaFiles] ${items.length} total files, ${filtered.length} media files`);
|
|
||||||
return filtered.map((f: any) => ({ id: f.id, fileName: f.fileName, mimeType: f.mimeType }));
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* Trustee API
|
* Trustee API
|
||||||
*
|
*
|
||||||
|
|
@ -866,14 +864,7 @@ export async function syncPositionsToAccounting(
|
||||||
request: ApiRequestFunction,
|
request: ApiRequestFunction,
|
||||||
instanceId: string,
|
instanceId: string,
|
||||||
positionIds: string[],
|
positionIds: string[],
|
||||||
opts?: {
|
opts?: { pollMs?: number; onProgress?: (progress: number, message?: string | null) => void }
|
||||||
pollMs?: number;
|
|
||||||
/**
|
|
||||||
* `message` is already translated server-side by the job route handler
|
|
||||||
* (`resolveJobMessage`). Render it 1:1; never feed it through `t()`.
|
|
||||||
*/
|
|
||||||
onProgress?: (progress: number, message?: string | null) => void;
|
|
||||||
}
|
|
||||||
): Promise<{ total: number; success: number; skipped?: number; errors: number; results: any[] }> {
|
): Promise<{ total: number; success: number; skipped?: number; errors: number; results: any[] }> {
|
||||||
const submission = await request({
|
const submission = await request({
|
||||||
url: `${_getTrusteeBaseUrl(instanceId)}/accounting/sync`,
|
url: `${_getTrusteeBaseUrl(instanceId)}/accounting/sync`,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { ApiRequestOptions } from '../hooks/useApi';
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* Voice / Language Catalog API.
|
* Voice / Language Catalog API.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* AccessLevelSelect
|
* AccessLevelSelect
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* AccessRulesEditor
|
* AccessRulesEditor
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* AccessRulesTable
|
* AccessRulesTable
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* AccessRules Components
|
* AccessRules Components
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -73,12 +73,13 @@
|
||||||
|
|
||||||
/* Connector grid (Step 0) */
|
/* Connector grid (Step 0) */
|
||||||
.connectorGrid {
|
.connectorGrid {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connectorCard {
|
.connectorCard {
|
||||||
|
flex: 1 1 140px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -446,22 +447,6 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.patInput {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid var(--border-color, #e0e0e0);
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-family: monospace;
|
|
||||||
margin: 12px 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.patInput:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--primary, #2563eb);
|
|
||||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark theme */
|
/* Dark theme */
|
||||||
:global(.dark-theme) .connectorCard {
|
:global(.dark-theme) .connectorCard {
|
||||||
background: var(--surface-color);
|
background: var(--surface-color);
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,153 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* AddConnectionWizard
|
* AddConnectionWizard
|
||||||
*
|
*
|
||||||
* Streamlined multi-step modal for adding a new connector.
|
* Multi-step modal for adding a new connector with optional knowledge
|
||||||
* Steps are connector-type-aware:
|
* ingestion consent and per-connection preferences (§2.6).
|
||||||
* Base: Connector → Consent → Connect
|
*
|
||||||
* Microsoft: Connector → Consent → Admin Consent (optional) → Connect
|
* Steps:
|
||||||
* Infomaniak: Connector → Consent → PAT Input → (done)
|
* 0 — Connector wählen
|
||||||
|
* 1 — Consent (Wissensdatenbank Ja/Nein)
|
||||||
|
* 2 — Präferenzen (nur wenn Ja)
|
||||||
|
* 3 — Zusammenfassung + OAuth starten
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Modal } from '../UiComponents/Modal/Modal';
|
import { Modal } from '../UiComponents/Modal/Modal';
|
||||||
import { FaGoogle, FaMicrosoft, FaTasks, FaCloud, FaCheck, FaArrowRight, FaShieldAlt } from 'react-icons/fa';
|
import { FaGoogle, FaMicrosoft, FaTasks, FaDatabase, FaShieldAlt, FaCheck, FaArrowRight, FaInfoCircle } from 'react-icons/fa';
|
||||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
import type { KnowledgePreferences } from '../../api/connectionApi';
|
||||||
import styles from './AddConnectionWizard.module.css';
|
import styles from './AddConnectionWizard.module.css';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export type ConnectorType = 'google' | 'msft' | 'clickup' | 'infomaniak';
|
export type ConnectorType = 'google' | 'msft' | 'clickup';
|
||||||
|
|
||||||
type StepId = 'connector' | 'consent' | 'msftAdminConsent' | 'infomaniakPat' | 'connect';
|
|
||||||
|
|
||||||
interface WizardState {
|
interface WizardState {
|
||||||
currentStep: StepId;
|
step: 0 | 1 | 2 | 3;
|
||||||
connector: ConnectorType | null;
|
connector: ConnectorType | null;
|
||||||
knowledgeEnabled: boolean;
|
knowledgeEnabled: boolean;
|
||||||
infomaniakToken: string;
|
prefs: KnowledgePreferences;
|
||||||
adminConsentDone: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_PREFS: KnowledgePreferences = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
neutralizeBeforeEmbed: false,
|
||||||
|
mailContentDepth: 'full',
|
||||||
|
mailIndexAttachments: false,
|
||||||
|
filesIndexBinaries: true,
|
||||||
|
clickupScope: 'title_description',
|
||||||
|
clickupIndexAttachments: false,
|
||||||
|
maxAgeDays: 90,
|
||||||
|
};
|
||||||
|
|
||||||
const CONNECTOR_LABELS: Record<ConnectorType, string> = {
|
const CONNECTOR_LABELS: Record<ConnectorType, string> = {
|
||||||
google: 'Google',
|
google: 'Google',
|
||||||
msft: 'Microsoft 365',
|
msft: 'Microsoft 365',
|
||||||
clickup: 'ClickUp',
|
clickup: 'ClickUp',
|
||||||
infomaniak: 'Infomaniak',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CONNECTOR_ICONS: Record<ConnectorType, React.ReactNode> = {
|
const CONNECTOR_ICONS: Record<ConnectorType, React.ReactNode> = {
|
||||||
google: <FaGoogle style={{ color: '#4285f4' }} />,
|
google: <FaGoogle style={{ color: '#4285f4' }} />,
|
||||||
msft: <FaMicrosoft style={{ color: '#00a4ef' }} />,
|
msft: <FaMicrosoft style={{ color: '#00a4ef' }} />,
|
||||||
clickup: <FaTasks style={{ color: '#7b68ee' }} />,
|
clickup: <FaTasks style={{ color: '#7b68ee' }} />,
|
||||||
infomaniak: <FaCloud style={{ color: '#0098db' }} />,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function _getSteps(connector: ConnectorType | null): StepId[] {
|
// ---------------------------------------------------------------------------
|
||||||
if (connector === 'msft') return ['connector', 'consent', 'msftAdminConsent', 'connect'];
|
// Cost estimate helper
|
||||||
if (connector === 'infomaniak') return ['connector', 'consent', 'infomaniakPat'];
|
// ---------------------------------------------------------------------------
|
||||||
return ['connector', 'consent', 'connect'];
|
|
||||||
|
/**
|
||||||
|
* Returns a cost estimate broken into two lines:
|
||||||
|
*
|
||||||
|
* 1. Embedding (OpenAI text-embedding-3-small, $0.02 / 1M tokens) — always tiny.
|
||||||
|
* 2. Neutralization (Private LLM / qwen2.5 on-premise, CHF 0.01 per LLM call)
|
||||||
|
* — this is the DOMINANT cost when enabled. One call per email/task for
|
||||||
|
* short content; several calls for long threads or files.
|
||||||
|
*
|
||||||
|
* Numbers are conservative ranges. Subsequent syncs are cheaper because
|
||||||
|
* unchanged content is deduplicated before any LLM/embedding call.
|
||||||
|
*/
|
||||||
|
function computeCostEstimate(
|
||||||
|
connector: ConnectorType | null,
|
||||||
|
prefs: KnowledgePreferences,
|
||||||
|
): {
|
||||||
|
embeddingLow: string;
|
||||||
|
embeddingHigh: string;
|
||||||
|
neutralizationLow: string | null;
|
||||||
|
neutralizationHigh: string | null;
|
||||||
|
note: string;
|
||||||
|
} | null {
|
||||||
|
if (!connector) return null;
|
||||||
|
|
||||||
|
// ---- Embedding (OpenAI, USD) ----
|
||||||
|
const EMBED_USD_PER_M = 0.02;
|
||||||
|
const tokensPerMail: Record<string, number> = { metadata: 30, snippet: 120, full: 500 };
|
||||||
|
const depth = prefs.mailContentDepth ?? 'full';
|
||||||
|
const maxAge = prefs.maxAgeDays ?? 90;
|
||||||
|
const mailCount = Math.min(500, Math.round((maxAge / 90) * 500));
|
||||||
|
const taskCount = Math.min(500, Math.round((maxAge / 90) * 300));
|
||||||
|
|
||||||
|
let embedLowTokens = 0;
|
||||||
|
let embedHighTokens = 0;
|
||||||
|
|
||||||
|
if (connector === 'google' || connector === 'msft') {
|
||||||
|
const mailTokens = mailCount * tokensPerMail[depth];
|
||||||
|
embedLowTokens += mailTokens * 0.6;
|
||||||
|
embedHighTokens += mailTokens * 1.5 + 500_000; // Drive/SharePoint
|
||||||
|
if (prefs.mailIndexAttachments) embedHighTokens += 200_000;
|
||||||
|
} else if (connector === 'clickup') {
|
||||||
|
const scope = prefs.clickupScope ?? 'title_description';
|
||||||
|
const tpt = scope === 'titles' ? 30 : scope === 'title_description' ? 200 : 400;
|
||||||
|
embedLowTokens += taskCount * tpt * 0.6;
|
||||||
|
embedHighTokens += taskCount * tpt * 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fmtUsd = (tokens: number) => {
|
||||||
|
const usd = (tokens / 1_000_000) * EMBED_USD_PER_M;
|
||||||
|
if (usd < 0.001) return '< 0.01 $';
|
||||||
|
if (usd < 0.10) return `~${usd.toFixed(3)} $`;
|
||||||
|
return `~${usd.toFixed(2)} $`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Neutralization (Private LLM, CHF 0.01/call) ----
|
||||||
|
// Each item (email / task / file) = 1 LLM call for short content,
|
||||||
|
// 2-4 for long threads/documents.
|
||||||
|
const NEUT_CHF_PER_CALL = 0.01;
|
||||||
|
let neutLow: string | null = null;
|
||||||
|
let neutHigh: string | null = null;
|
||||||
|
|
||||||
|
if (prefs.neutralizeBeforeEmbed) {
|
||||||
|
let lowCalls = 0;
|
||||||
|
let highCalls = 0;
|
||||||
|
|
||||||
|
if (connector === 'google' || connector === 'msft') {
|
||||||
|
lowCalls += mailCount * 1; // 1 call / short email
|
||||||
|
highCalls += mailCount * 3; // up to 3 calls / long thread
|
||||||
|
lowCalls += 20; // Drive/SharePoint files (low)
|
||||||
|
highCalls += 200; // Drive/SharePoint files (high, large PDFs)
|
||||||
|
} else if (connector === 'clickup') {
|
||||||
|
lowCalls += taskCount * 1;
|
||||||
|
highCalls += taskCount * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fmtChf = (calls: number) => {
|
||||||
|
const chf = calls * NEUT_CHF_PER_CALL;
|
||||||
|
if (chf < 0.01) return '< 0.01 CHF';
|
||||||
|
return `~${chf.toFixed(2)} CHF`;
|
||||||
|
};
|
||||||
|
|
||||||
|
neutLow = fmtChf(lowCalls);
|
||||||
|
neutHigh = fmtChf(highCalls);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
embeddingLow: fmtUsd(embedLowTokens),
|
||||||
|
embeddingHigh: fmtUsd(embedHighTokens),
|
||||||
|
neutralizationLow: neutLow,
|
||||||
|
neutralizationHigh: neutHigh,
|
||||||
|
note: 'Einmalig beim ersten Sync. Folge-Syncs kosten weniger (nur neue Inhalte).',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -59,9 +157,11 @@ function _getSteps(connector: ConnectorType | null): StepId[] {
|
||||||
interface AddConnectionWizardProps {
|
interface AddConnectionWizardProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onConnect: (type: ConnectorType, knowledgeEnabled: boolean) => Promise<void>;
|
onConnect: (
|
||||||
onInfomaniakConnect?: (token: string, knowledgeEnabled: boolean) => Promise<void>;
|
type: ConnectorType,
|
||||||
onMsftAdminConsent?: () => void;
|
knowledgeEnabled: boolean,
|
||||||
|
prefs: KnowledgePreferences | null,
|
||||||
|
) => Promise<void>;
|
||||||
isConnecting?: boolean;
|
isConnecting?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,93 +173,84 @@ export const AddConnectionWizard: React.FC<AddConnectionWizardProps> = ({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
onConnect,
|
onConnect,
|
||||||
onInfomaniakConnect,
|
|
||||||
onMsftAdminConsent,
|
|
||||||
isConnecting = false,
|
isConnecting = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
|
||||||
|
|
||||||
const [state, setState] = useState<WizardState>({
|
const [state, setState] = useState<WizardState>({
|
||||||
currentStep: 'connector',
|
step: 0,
|
||||||
connector: null,
|
connector: null,
|
||||||
knowledgeEnabled: false,
|
knowledgeEnabled: false,
|
||||||
infomaniakToken: '',
|
prefs: { ...DEFAULT_PREFS },
|
||||||
adminConsentDone: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const reset = () =>
|
const reset = () =>
|
||||||
setState({ currentStep: 'connector', connector: null, knowledgeEnabled: false, infomaniakToken: '', adminConsentDone: false });
|
setState({ step: 0, connector: null, knowledgeEnabled: false, prefs: { ...DEFAULT_PREFS } });
|
||||||
|
|
||||||
const handleClose = () => { reset(); onClose(); };
|
const handleClose = () => {
|
||||||
|
|
||||||
const steps = _getSteps(state.connector);
|
|
||||||
const stepIndex = steps.indexOf(state.currentStep);
|
|
||||||
|
|
||||||
const goNext = () => {
|
|
||||||
const nextIdx = stepIndex + 1;
|
|
||||||
if (nextIdx < steps.length) {
|
|
||||||
setState(s => ({ ...s, currentStep: steps[nextIdx] }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const goBack = () => {
|
|
||||||
const prevIdx = stepIndex - 1;
|
|
||||||
if (prevIdx >= 0) {
|
|
||||||
setState(s => ({ ...s, currentStep: steps[prevIdx] }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectConnector = (c: ConnectorType) => {
|
|
||||||
setState(s => ({ ...s, connector: c, currentStep: 'consent' }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const setConsent = (enabled: boolean) => {
|
|
||||||
setState(s => ({ ...s, knowledgeEnabled: enabled }));
|
|
||||||
goNext();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFinalConnect = async () => {
|
|
||||||
if (!state.connector) return;
|
|
||||||
if (state.connector === 'infomaniak' && onInfomaniakConnect) {
|
|
||||||
await onInfomaniakConnect(state.infomaniakToken, state.knowledgeEnabled);
|
|
||||||
} else {
|
|
||||||
await onConnect(state.connector, state.knowledgeEnabled);
|
|
||||||
}
|
|
||||||
reset();
|
reset();
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setStep = (step: WizardState['step']) => setState(s => ({ ...s, step }));
|
||||||
|
const setConnector = (connector: ConnectorType) =>
|
||||||
|
setState(s => ({ ...s, connector, step: 1 }));
|
||||||
|
const setKnowledgeEnabled = (v: boolean) =>
|
||||||
|
setState(s => ({ ...s, knowledgeEnabled: v, step: v ? 2 : 3 }));
|
||||||
|
const updatePref = <K extends keyof KnowledgePreferences>(key: K, value: KnowledgePreferences[K]) =>
|
||||||
|
setState(s => ({ ...s, prefs: { ...s.prefs, [key]: value } }));
|
||||||
|
|
||||||
|
const handleConnect = async () => {
|
||||||
|
if (!state.connector) return;
|
||||||
|
await onConnect(
|
||||||
|
state.connector,
|
||||||
|
state.knowledgeEnabled,
|
||||||
|
state.knowledgeEnabled ? state.prefs : null,
|
||||||
|
);
|
||||||
|
reset();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const visibleSteps = state.knowledgeEnabled
|
||||||
|
? [0, 1, 2, 3]
|
||||||
|
: [0, 1, 3];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={open} onClose={handleClose} title={t('Verbindung hinzufügen')} size="md" closeOnEscape>
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
title="Verbindung hinzufügen"
|
||||||
|
size="md"
|
||||||
|
closeOnEscape
|
||||||
|
>
|
||||||
{/* Stepper */}
|
{/* Stepper */}
|
||||||
<div className={styles.stepper}>
|
<div className={styles.stepper}>
|
||||||
{steps.map((s, i) => (
|
{[0, 1, 2, 3].map(i => (
|
||||||
<div
|
<div
|
||||||
key={s}
|
key={i}
|
||||||
className={[
|
className={[
|
||||||
styles.stepDot,
|
styles.stepDot,
|
||||||
stepIndex === i ? styles.stepDotActive : '',
|
state.step === i ? styles.stepDotActive : '',
|
||||||
stepIndex > i ? styles.stepDotDone : '',
|
state.step > i ? styles.stepDotDone : '',
|
||||||
|
!visibleSteps.includes(i) ? styles.stepDotHidden : '',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
{stepIndex > i ? <FaCheck size={10} /> : i + 1}
|
{state.step > i ? <FaCheck size={10} /> : i + 1}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{/* ---- Step: Connector ---- */}
|
{/* ---- Step 0: Connector ---- */}
|
||||||
{state.currentStep === 'connector' && (
|
{state.step === 0 && (
|
||||||
<div className={styles.stepContent}>
|
<div className={styles.stepContent}>
|
||||||
<h3 className={styles.stepTitle}>{t('Anbieter wählen')}</h3>
|
<h3 className={styles.stepTitle}>Anbieter wählen</h3>
|
||||||
<p className={styles.stepHint}>{t('Welchen Dienst möchtest du verbinden?')}</p>
|
<p className={styles.stepHint}>Welchen Dienst möchtest du verbinden?</p>
|
||||||
<div className={styles.connectorGrid}>
|
<div className={styles.connectorGrid}>
|
||||||
{(['google', 'msft', 'clickup', 'infomaniak'] as ConnectorType[]).map(type => (
|
{(['google', 'msft', 'clickup'] as ConnectorType[]).map(type => (
|
||||||
<button
|
<button
|
||||||
key={type}
|
key={type}
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.connectorCard}
|
className={styles.connectorCard}
|
||||||
onClick={() => selectConnector(type)}
|
onClick={() => setConnector(type)}
|
||||||
>
|
>
|
||||||
<span className={styles.connectorIcon}>{CONNECTOR_ICONS[type]}</span>
|
<span className={styles.connectorIcon}>{CONNECTOR_ICONS[type]}</span>
|
||||||
<span className={styles.connectorLabel}>{CONNECTOR_LABELS[type]}</span>
|
<span className={styles.connectorLabel}>{CONNECTOR_LABELS[type]}</span>
|
||||||
|
|
@ -169,119 +260,253 @@ export const AddConnectionWizard: React.FC<AddConnectionWizardProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ---- Step: Consent ---- */}
|
{/* ---- Step 1: Consent ---- */}
|
||||||
{state.currentStep === 'consent' && (
|
{state.step === 1 && (
|
||||||
<div className={styles.stepContent}>
|
<div className={styles.stepContent}>
|
||||||
<h3 className={styles.stepTitle}>{t('Wissensdatenbank')}</h3>
|
<div className={styles.consentIcon}><FaDatabase size={32} /></div>
|
||||||
|
<h3 className={styles.stepTitle}>Wissensdatenbank</h3>
|
||||||
<p className={styles.stepBody}>
|
<p className={styles.stepBody}>
|
||||||
{t('Möchtest du Inhalte aus dieser Verbindung in deine persönliche Wissensdatenbank aufnehmen, damit die KI beim Antworten auf Informationen aus {provider} zurückgreifen kann?', { provider: state.connector ? CONNECTOR_LABELS[state.connector] : t('diesem Dienst') })}
|
Möchtest du Inhalte aus dieser Verbindung in deine persönliche
|
||||||
|
Wissensdatenbank aufnehmen, damit die KI beim Antworten auf Informationen
|
||||||
|
aus{' '}
|
||||||
|
{state.connector ? CONNECTOR_LABELS[state.connector] : 'diesem Dienst'}{' '}
|
||||||
|
zurückgreifen kann?
|
||||||
</p>
|
</p>
|
||||||
<p className={styles.stepHint}>
|
<p className={styles.stepHint}>
|
||||||
{t('Du kannst dies später jederzeit in der UDB pro Datenquelle steuern.')}
|
Du kannst diese Einstellung später in den Verbindungsdetails ändern.
|
||||||
</p>
|
|
||||||
<div className={styles.consentButtons}>
|
|
||||||
<button type="button" className={styles.consentButtonYes} onClick={() => setConsent(true)}>
|
|
||||||
<FaCheck /> {t('Ja, aktivieren')}
|
|
||||||
</button>
|
|
||||||
<button type="button" className={styles.consentButtonNo} onClick={() => setConsent(false)}>
|
|
||||||
{t('Nein, überspringen')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className={styles.stepNavLeft}>
|
|
||||||
<button type="button" className={styles.navBack} onClick={goBack}>{t('Zurück')}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ---- Step: MSFT Admin Consent ---- */}
|
|
||||||
{state.currentStep === 'msftAdminConsent' && (
|
|
||||||
<div className={styles.stepContent}>
|
|
||||||
<div style={{ textAlign: 'center', marginBottom: 16 }}>
|
|
||||||
<FaShieldAlt size={32} style={{ color: '#00a4ef' }} />
|
|
||||||
</div>
|
|
||||||
<h3 className={styles.stepTitle}>{t('Organisations-Zustimmung (optional)')}</h3>
|
|
||||||
<p className={styles.stepBody}>
|
|
||||||
{t('Falls du Mandant-Administrator bist, kannst du jetzt für deine ganze Organisation zustimmen. So müssen andere Benutzer nicht einzeln bestätigen.')}
|
|
||||||
</p>
|
|
||||||
<p className={styles.stepHint}>
|
|
||||||
{t('Wenn du kein Admin bist oder dies später tun möchtest, überspringe diesen Schritt.')}
|
|
||||||
</p>
|
</p>
|
||||||
<div className={styles.consentButtons}>
|
<div className={styles.consentButtons}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.consentButtonYes}
|
className={styles.consentButtonYes}
|
||||||
onClick={() => { onMsftAdminConsent?.(); setState(s => ({ ...s, adminConsentDone: true })); goNext(); }}
|
onClick={() => setKnowledgeEnabled(true)}
|
||||||
>
|
>
|
||||||
<FaShieldAlt /> {t('Admin-Zustimmung erteilen')}
|
<FaCheck /> Ja, aufnehmen
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className={styles.consentButtonNo} onClick={goNext}>
|
<button
|
||||||
{t('Überspringen')}
|
type="button"
|
||||||
|
className={styles.consentButtonNo}
|
||||||
|
onClick={() => setKnowledgeEnabled(false)}
|
||||||
|
>
|
||||||
|
Nein, überspringen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.stepNavLeft}>
|
<div className={styles.stepNavLeft}>
|
||||||
<button type="button" className={styles.navBack} onClick={goBack}>{t('Zurück')}</button>
|
<button type="button" className={styles.navBack} onClick={() => setStep(0)}>
|
||||||
</div>
|
Zurück
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ---- Step: Infomaniak PAT ---- */}
|
|
||||||
{state.currentStep === 'infomaniakPat' && (
|
|
||||||
<div className={styles.stepContent}>
|
|
||||||
<h3 className={styles.stepTitle}>{t('Infomaniak Personal Access Token')}</h3>
|
|
||||||
<p className={styles.stepBody}>
|
|
||||||
{t('Erstelle einen Personal Access Token in deinem Infomaniak-Konto und füge ihn hier ein.')}
|
|
||||||
</p>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder="pat_..."
|
|
||||||
value={state.infomaniakToken}
|
|
||||||
onChange={e => setState(s => ({ ...s, infomaniakToken: e.target.value }))}
|
|
||||||
className={styles.patInput}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<div className={styles.stepNav}>
|
|
||||||
<button type="button" className={styles.navBack} onClick={goBack}>{t('Zurück')}</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.navConnect}
|
|
||||||
onClick={handleFinalConnect}
|
|
||||||
disabled={isConnecting || !state.infomaniakToken.trim()}
|
|
||||||
>
|
|
||||||
{isConnecting ? t('Verbinden…') : t('Verbinden')}
|
|
||||||
{!isConnecting && <FaArrowRight size={12} />}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ---- Step: Connect ---- */}
|
{/* ---- Step 2: Preferences ---- */}
|
||||||
{state.currentStep === 'connect' && (
|
{state.step === 2 && (
|
||||||
<div className={styles.stepContent}>
|
<div className={styles.stepContent}>
|
||||||
<h3 className={styles.stepTitle}>{t('Verbindung herstellen')}</h3>
|
<h3 className={styles.stepTitle}>Einstellungen</h3>
|
||||||
|
<p className={styles.stepHint}>
|
||||||
|
Steuere, welche Inhalte und in welcher Form sie indexiert werden.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className={styles.prefGroup}>
|
||||||
|
<label className={styles.prefLabel}>
|
||||||
|
<FaShieldAlt className={styles.prefIcon} />
|
||||||
|
Anonymisierung vor dem Indexieren
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!state.prefs.neutralizeBeforeEmbed}
|
||||||
|
onChange={e => updatePref('neutralizeBeforeEmbed', e.target.checked)}
|
||||||
|
className={styles.prefCheck}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p className={styles.prefHint}>
|
||||||
|
Persönliche Daten (Namen, E-Mail-Adressen) werden vor dem Speichern ersetzt.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(state.connector === 'google' || state.connector === 'msft') && (
|
||||||
|
<>
|
||||||
|
<div className={styles.prefGroup}>
|
||||||
|
<label className={styles.prefLabelRow}>
|
||||||
|
E-Mail-Inhalt
|
||||||
|
<select
|
||||||
|
value={state.prefs.mailContentDepth ?? 'full'}
|
||||||
|
onChange={e => updatePref('mailContentDepth', e.target.value as any)}
|
||||||
|
className={styles.prefSelect}
|
||||||
|
>
|
||||||
|
<option value="metadata">Nur Metadaten (Betreff, Absender, Datum)</option>
|
||||||
|
<option value="snippet">Vorschautext (ca. 250 Zeichen)</option>
|
||||||
|
<option value="full">Vollständiger Text</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={styles.prefGroup}>
|
||||||
|
<label className={styles.prefLabel}>
|
||||||
|
E-Mail-Anhänge indexieren
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!state.prefs.mailIndexAttachments}
|
||||||
|
onChange={e => updatePref('mailIndexAttachments', e.target.checked)}
|
||||||
|
className={styles.prefCheck}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state.connector === 'clickup' && (
|
||||||
|
<div className={styles.prefGroup}>
|
||||||
|
<label className={styles.prefLabelRow}>
|
||||||
|
Aufgaben-Inhalt
|
||||||
|
<select
|
||||||
|
value={state.prefs.clickupScope ?? 'title_description'}
|
||||||
|
onChange={e => updatePref('clickupScope', e.target.value as any)}
|
||||||
|
className={styles.prefSelect}
|
||||||
|
>
|
||||||
|
<option value="titles">Nur Aufgabentitel</option>
|
||||||
|
<option value="title_description">Titel + Beschreibung</option>
|
||||||
|
<option value="with_comments">Titel + Beschreibung + Kommentare</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.prefGroup}>
|
||||||
|
<label className={styles.prefLabelRow}>
|
||||||
|
Zeitfenster (Tage)
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={3650}
|
||||||
|
value={state.prefs.maxAgeDays ?? 90}
|
||||||
|
onChange={e => updatePref('maxAgeDays', parseInt(e.target.value, 10) || 0)}
|
||||||
|
className={styles.prefNumber}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p className={styles.prefHint}>0 = kein Limit</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.stepNav}>
|
||||||
|
<button type="button" className={styles.navBack} onClick={() => setStep(1)}>
|
||||||
|
Zurück
|
||||||
|
</button>
|
||||||
|
<button type="button" className={styles.navNext} onClick={() => setStep(3)}>
|
||||||
|
Weiter <FaArrowRight size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ---- Step 3: Summary ---- */}
|
||||||
|
{state.step === 3 && (
|
||||||
|
<div className={styles.stepContent}>
|
||||||
|
<h3 className={styles.stepTitle}>Zusammenfassung</h3>
|
||||||
<div className={styles.summary}>
|
<div className={styles.summary}>
|
||||||
<div className={styles.summaryRow}>
|
<div className={styles.summaryRow}>
|
||||||
<span className={styles.summaryKey}>{t('Anbieter')}</span>
|
<span className={styles.summaryKey}>Anbieter</span>
|
||||||
<span className={styles.summaryVal}>
|
<span className={styles.summaryVal}>
|
||||||
{state.connector && CONNECTOR_ICONS[state.connector]}
|
{CONNECTOR_ICONS[state.connector!]}
|
||||||
{state.connector ? CONNECTOR_LABELS[state.connector] : '—'}
|
{state.connector ? CONNECTOR_LABELS[state.connector] : '—'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.summaryRow}>
|
<div className={styles.summaryRow}>
|
||||||
<span className={styles.summaryKey}>{t('Wissensdatenbank')}</span>
|
<span className={styles.summaryKey}>Wissensdatenbank</span>
|
||||||
<span className={styles.summaryVal}>
|
<span className={styles.summaryVal}>
|
||||||
{state.knowledgeEnabled ? t('Aktiv') : t('Nicht aktiv')}
|
{state.knowledgeEnabled ? '✓ Aktiv' : '✗ Nicht aktiv'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{state.knowledgeEnabled && (
|
||||||
|
<>
|
||||||
|
<div className={styles.summaryRow}>
|
||||||
|
<span className={styles.summaryKey}>Anonymisierung</span>
|
||||||
|
<span className={styles.summaryVal}>
|
||||||
|
{state.prefs.neutralizeBeforeEmbed ? 'Ja' : 'Nein'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{(state.connector === 'google' || state.connector === 'msft') && (
|
||||||
|
<div className={styles.summaryRow}>
|
||||||
|
<span className={styles.summaryKey}>E-Mail-Tiefe</span>
|
||||||
|
<span className={styles.summaryVal}>
|
||||||
|
{{ metadata: 'Nur Metadaten', snippet: 'Vorschautext', full: 'Volltext' }[
|
||||||
|
state.prefs.mailContentDepth ?? 'full'
|
||||||
|
] ?? state.prefs.mailContentDepth}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{state.connector === 'clickup' && (
|
||||||
|
<div className={styles.summaryRow}>
|
||||||
|
<span className={styles.summaryKey}>Aufgaben-Inhalt</span>
|
||||||
|
<span className={styles.summaryVal}>
|
||||||
|
{{
|
||||||
|
titles: 'Nur Titel',
|
||||||
|
title_description: 'Titel + Beschreibung',
|
||||||
|
with_comments: 'Titel + Beschreibung + Kommentare',
|
||||||
|
}[state.prefs.clickupScope ?? 'title_description'] ?? state.prefs.clickupScope}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.summaryRow}>
|
||||||
|
<span className={styles.summaryKey}>Zeitfenster</span>
|
||||||
|
<span className={styles.summaryVal}>
|
||||||
|
{state.prefs.maxAgeDays ? `${state.prefs.maxAgeDays} Tage` : 'Unbegrenzt'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Cost estimate — only shown when knowledge ingestion is enabled */}
|
||||||
|
{state.knowledgeEnabled && (() => {
|
||||||
|
const est = computeCostEstimate(state.connector, state.prefs);
|
||||||
|
if (!est) return null;
|
||||||
|
return (
|
||||||
|
<div className={styles.costHint}>
|
||||||
|
<FaInfoCircle className={styles.costHintIcon} />
|
||||||
|
<div>
|
||||||
|
<span className={styles.costHintTitle}>Geschätzte Kosten (erster Sync)</span>
|
||||||
|
<table className={styles.costTable}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td className={styles.costLabel}>Embedding</td>
|
||||||
|
<td className={styles.costVal}>
|
||||||
|
{est.embeddingLow} – {est.embeddingHigh}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{est.neutralizationLow && (
|
||||||
|
<tr className={styles.costRowNeut}>
|
||||||
|
<td className={styles.costLabel}>Anonymisierung (Private LLM)</td>
|
||||||
|
<td className={styles.costVal}>
|
||||||
|
{est.neutralizationLow} – {est.neutralizationHigh}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{est.neutralizationLow && (
|
||||||
|
<span className={styles.costHintWarn}>
|
||||||
|
⚠ Anonymisierung ist der Hauptkostentreiber (CHF 0.01 pro LLM-Aufruf, on-premise).
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className={styles.costHintNote}>{est.note}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
<div className={styles.stepNav}>
|
<div className={styles.stepNav}>
|
||||||
<button type="button" className={styles.navBack} onClick={goBack}>{t('Zurück')}</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.navBack}
|
||||||
|
onClick={() => setStep(state.knowledgeEnabled ? 2 : 1)}
|
||||||
|
>
|
||||||
|
Zurück
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.navConnect}
|
className={styles.navConnect}
|
||||||
onClick={handleFinalConnect}
|
onClick={handleConnect}
|
||||||
disabled={isConnecting}
|
disabled={isConnecting}
|
||||||
>
|
>
|
||||||
{isConnecting ? t('Verbinden…') : t('Mit {provider} verbinden', { provider: state.connector ? CONNECTOR_LABELS[state.connector] : '…' })}
|
{isConnecting ? 'Verbinden…' : `Mit ${state.connector ? CONNECTOR_LABELS[state.connector] : '…'} verbinden`}
|
||||||
{!isConnecting && <FaArrowRight size={12} />}
|
{!isConnecting && <FaArrowRight size={12} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* ChatInput -- Shared chat input component.
|
* ChatInput -- Shared chat input component.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* ChatMessageList -- Shared chat message display component.
|
* ChatMessageList -- Shared chat message display component.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
export { ChatMessageList } from './ChatMessageList';
|
export { ChatMessageList } from './ChatMessageList';
|
||||||
export type { ChatMessage } from './ChatMessageList';
|
export type { ChatMessage } from './ChatMessageList';
|
||||||
export { ChatInput } from './ChatInput';
|
export { ChatInput } from './ChatInput';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { IoIosDownload, IoIosCopy } from 'react-icons/io';
|
import { IoIosDownload, IoIosCopy } from 'react-icons/io';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { IoIosDownload } from 'react-icons/io';
|
import { IoIosDownload } from 'react-icons/io';
|
||||||
import { Popup, PopupAction } from '../UiComponents/Popup/Popup';
|
import { Popup, PopupAction } from '../UiComponents/Popup/Popup';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
export { ContentPreview } from './ContentPreview';
|
export { ContentPreview } from './ContentPreview';
|
||||||
export type { ContentPreviewProps } from './ContentPreview';
|
export type { ContentPreviewProps } from './ContentPreview';
|
||||||
export { UrlContentPreview } from './UrlContentPreview';
|
export { UrlContentPreview } from './UrlContentPreview';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import styles from '../ContentPreview.module.css';
|
import styles from '../ContentPreview.module.css';
|
||||||
|
|
||||||
interface ApplicationRendererProps {
|
interface ApplicationRendererProps {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
import styles from '../ContentPreview.module.css';
|
import styles from '../ContentPreview.module.css';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import styles from '../ContentPreview.module.css';
|
import styles from '../ContentPreview.module.css';
|
||||||
|
|
||||||
interface HtmlRendererProps {
|
interface HtmlRendererProps {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import styles from '../ContentPreview.module.css';
|
import styles from '../ContentPreview.module.css';
|
||||||
|
|
||||||
interface ImageRendererProps {
|
interface ImageRendererProps {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
import styles from '../ContentPreview.module.css';
|
import styles from '../ContentPreview.module.css';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
import styles from '../ContentPreview.module.css';
|
import styles from '../ContentPreview.module.css';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { IoIosWarning } from 'react-icons/io';
|
import { IoIosWarning } from 'react-icons/io';
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
import styles from '../ContentPreview.module.css';
|
import styles from '../ContentPreview.module.css';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import styles from '../ContentPreview.module.css';
|
import styles from '../ContentPreview.module.css';
|
||||||
|
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
import styles from '../ContentPreview.module.css';
|
import styles from '../ContentPreview.module.css';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { renderAsync } from 'docx-preview';
|
import { renderAsync } from 'docx-preview';
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
export { JsonRenderer } from './JsonRenderer';
|
export { JsonRenderer } from './JsonRenderer';
|
||||||
export { ImageRenderer } from './ImageRenderer';
|
export { ImageRenderer } from './ImageRenderer';
|
||||||
export { TextRenderer } from './TextRenderer';
|
export { TextRenderer } from './TextRenderer';
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* Workflow Flow Editor - Data flow context for Data Picker and DynamicValueField.
|
* Automation2 Flow Editor - Data flow context for Data Picker and DynamicValueField.
|
||||||
* Extended with portTypeCatalog and systemVariables for the Typed Port System.
|
* Extended with portTypeCatalog and systemVariables for the Typed Port System.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createContext, useContext, useMemo } from 'react';
|
import React, { createContext, useContext, useMemo } from 'react';
|
||||||
import type { CanvasNode, CanvasConnection } from '../editor/FlowCanvas';
|
import type { CanvasNode, CanvasConnection } from '../editor/FlowCanvas';
|
||||||
import { getAvailableSources } from '../nodes/shared/dataFlowGraph';
|
import { getAvailableSources } from '../nodes/shared/dataFlowGraph';
|
||||||
import type { ApiRequestFunction, ConditionOperatorDef, FormFieldType, NodeType, PortField, PortSchema, SystemVariable } from '../../../api/workflowAutomationApi';
|
import type { ApiRequestFunction, ConditionOperatorDef, FormFieldType, NodeType, PortField, PortSchema, SystemVariable } from '../../../api/workflowApi';
|
||||||
|
|
||||||
export interface WorkflowDataFlowContextValue {
|
export interface Automation2DataFlowContextValue {
|
||||||
currentNodeId: string;
|
currentNodeId: string;
|
||||||
nodes: CanvasNode[];
|
nodes: CanvasNode[];
|
||||||
connections: CanvasConnection[];
|
connections: CanvasConnection[];
|
||||||
|
|
@ -32,13 +30,13 @@ export interface WorkflowDataFlowContextValue {
|
||||||
parseGraphDefinedSchema: (parameterKey: string) => PortSchema | null;
|
parseGraphDefinedSchema: (parameterKey: string) => PortSchema | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WorkflowDataFlowContext = createContext<WorkflowDataFlowContextValue | null>(null);
|
const Automation2DataFlowContext = createContext<Automation2DataFlowContextValue | null>(null);
|
||||||
|
|
||||||
export function useWorkflowDataFlow(): WorkflowDataFlowContextValue | null {
|
export function useAutomation2DataFlow(): Automation2DataFlowContextValue | null {
|
||||||
return useContext(WorkflowDataFlowContext);
|
return useContext(Automation2DataFlowContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkflowDataFlowProviderProps {
|
interface Automation2DataFlowProviderProps {
|
||||||
node: CanvasNode | null;
|
node: CanvasNode | null;
|
||||||
nodes: CanvasNode[];
|
nodes: CanvasNode[];
|
||||||
connections: CanvasConnection[];
|
connections: CanvasConnection[];
|
||||||
|
|
@ -54,7 +52,7 @@ interface WorkflowDataFlowProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkflowDataFlowProvider: React.FC<WorkflowDataFlowProviderProps> = ({
|
export const Automation2DataFlowProvider: React.FC<Automation2DataFlowProviderProps> = ({
|
||||||
node,
|
node,
|
||||||
nodes,
|
nodes,
|
||||||
connections,
|
connections,
|
||||||
|
|
@ -69,7 +67,7 @@ export const WorkflowDataFlowProvider: React.FC<WorkflowDataFlowProviderProps> =
|
||||||
request,
|
request,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const value = useMemo((): WorkflowDataFlowContextValue | null => {
|
const value = useMemo((): Automation2DataFlowContextValue | null => {
|
||||||
if (!node) return null;
|
if (!node) return null;
|
||||||
const formTypeToPort: Record<string, string> = Object.fromEntries(
|
const formTypeToPort: Record<string, string> = Object.fromEntries(
|
||||||
formFieldTypes.map((f) => [f.id, f.portType])
|
formFieldTypes.map((f) => [f.id, f.portType])
|
||||||
|
|
@ -137,8 +135,8 @@ export const WorkflowDataFlowProvider: React.FC<WorkflowDataFlowProviderProps> =
|
||||||
}, [node, nodes, connections, nodeOutputsPreview, nodeTypes, language, portTypeCatalog, systemVariables, formFieldTypes, conditionOperatorCatalog, instanceId, request]);
|
}, [node, nodes, connections, nodeOutputsPreview, nodeTypes, language, portTypeCatalog, systemVariables, formFieldTypes, conditionOperatorCatalog, instanceId, request]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkflowDataFlowContext.Provider value={value}>
|
<Automation2DataFlowContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
</WorkflowDataFlowContext.Provider>
|
</Automation2DataFlowContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Workflow Flow Editor Styles
|
* Automation2 Flow Editor Styles
|
||||||
* Sidebar with node list + canvas area.
|
* Sidebar with node list + canvas area.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -255,7 +255,6 @@
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border-bottom: 1px solid var(--border-color, #e0e0e0);
|
border-bottom: 1px solid var(--border-color, #e0e0e0);
|
||||||
background: var(--bg-primary, #fff);
|
background: var(--bg-primary, #fff);
|
||||||
overflow: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvasHeaderToolbar {
|
.canvasHeaderToolbar {
|
||||||
|
|
@ -389,8 +388,8 @@
|
||||||
|
|
||||||
/* Closed <select> width must not follow the longest option label. */
|
/* Closed <select> width must not follow the longest option label. */
|
||||||
.canvasHeaderWorkflowSelect {
|
.canvasHeaderWorkflowSelect {
|
||||||
flex: 0 1 12.5rem;
|
flex: 0 0 auto;
|
||||||
min-width: 8rem;
|
width: 12.5rem;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 0.31rem 0.45rem;
|
padding: 0.31rem 0.45rem;
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
|
|
@ -402,61 +401,11 @@
|
||||||
color: var(--text-primary, #333);
|
color: var(--text-primary, #333);
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvasHeaderTitleBlock {
|
.canvasHeaderIconBtn {
|
||||||
flex: 1 1 auto;
|
padding: 6px !important;
|
||||||
min-width: 0;
|
min-width: 30px !important;
|
||||||
display: flex;
|
min-height: 30px !important;
|
||||||
align-items: center;
|
box-sizing: border-box !important;
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvasHeaderTitle,
|
|
||||||
.canvasHeaderTitle input {
|
|
||||||
margin: 0;
|
|
||||||
min-width: 0;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary, #1a1a1a);
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvasHeaderTitle {
|
|
||||||
line-height: 1.2;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvasHeaderTitleMuted {
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 500;
|
|
||||||
opacity: 0.65;
|
|
||||||
color: var(--text-secondary, #666);
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvasHeaderTitle input {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
padding: 0.25rem 0.4rem;
|
|
||||||
border: 1px solid var(--primary-color, #007bff);
|
|
||||||
border-radius: 4px;
|
|
||||||
outline: none;
|
|
||||||
background: var(--bg-primary, #fff);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvasHeaderActionPanel {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 0.4rem;
|
|
||||||
padding: 0.35rem 0.5rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border-color, #e0e0e0);
|
|
||||||
background: var(--bg-secondary, #f8f9fa);
|
|
||||||
flex: 0 1 auto;
|
|
||||||
max-width: 100%;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvasHeaderSplitPair :global(.button + .button) {
|
.canvasHeaderSplitPair :global(.button + .button) {
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* WorkflowFlowEditor
|
* Automation2FlowEditor
|
||||||
*
|
*
|
||||||
* n8n-style flow builder with backend-driven node list and categories.
|
* n8n-style flow builder with backend-driven node list and categories.
|
||||||
* Start nodes come from the API (category `start`); invocations are synced on the server from the graph.
|
* Start nodes come from the API (category `start`); invocations are synced on the server from the graph.
|
||||||
|
|
@ -25,16 +23,15 @@ import {
|
||||||
createTemplateFromWorkflow,
|
createTemplateFromWorkflow,
|
||||||
copyTemplate,
|
copyTemplate,
|
||||||
importWorkflowFromFile,
|
importWorkflowFromFile,
|
||||||
WORKFLOW_FILE_EXTENSION,
|
|
||||||
type NodeType,
|
type NodeType,
|
||||||
type NodeTypeCategory,
|
type NodeTypeCategory,
|
||||||
type WorkflowGraph,
|
type Automation2Graph,
|
||||||
type WorkflowDefinition,
|
type Automation2Workflow,
|
||||||
type ExecuteGraphResponse,
|
type ExecuteGraphResponse,
|
||||||
type WorkflowEntryPoint,
|
type WorkflowEntryPoint,
|
||||||
type AutoVersion,
|
type AutoVersion,
|
||||||
type AutoTemplateScope,
|
type AutoTemplateScope,
|
||||||
} from '../../../api/workflowAutomationApi';
|
} from '../../../api/workflowApi';
|
||||||
import {
|
import {
|
||||||
FlowCanvas,
|
FlowCanvas,
|
||||||
type CanvasNode,
|
type CanvasNode,
|
||||||
|
|
@ -52,7 +49,7 @@ import { fromApiGraph, toApiGraph, switchOutputCountFromCases, trimConnectionsFo
|
||||||
import { buildNodeOutputsPreview, setPortTypeCatalog as setRegistryCatalog } from '../nodes/shared/outputPreviewRegistry';
|
import { buildNodeOutputsPreview, setPortTypeCatalog as setRegistryCatalog } from '../nodes/shared/outputPreviewRegistry';
|
||||||
import { findGraphErrors } from '../nodes/shared/paramValidation';
|
import { findGraphErrors } from '../nodes/shared/paramValidation';
|
||||||
import { getLabel as getParamLabel } from '../nodes/shared/utils';
|
import { getLabel as getParamLabel } from '../nodes/shared/utils';
|
||||||
import { WorkflowDataFlowProvider } from '../context/WorkflowDataFlowContext';
|
import { Automation2DataFlowProvider } from '../context/Automation2DataFlowContext';
|
||||||
import { usePrompt } from '../../../hooks/usePrompt';
|
import { usePrompt } from '../../../hooks/usePrompt';
|
||||||
import { EditorChatPanel } from './EditorChatPanel';
|
import { EditorChatPanel } from './EditorChatPanel';
|
||||||
import type { PendingFile, EditorDataSource, EditorFeatureDataSource } from './EditorChatPanel';
|
import type { PendingFile, EditorDataSource, EditorFeatureDataSource } from './EditorChatPanel';
|
||||||
|
|
@ -60,12 +57,12 @@ import { EditorWorkflowChatList } from './EditorWorkflowChatList';
|
||||||
import { RunTracingPanel } from './RunTracingPanel';
|
import { RunTracingPanel } from './RunTracingPanel';
|
||||||
import { UnifiedDataBar } from '../../../components/UnifiedDataBar';
|
import { UnifiedDataBar } from '../../../components/UnifiedDataBar';
|
||||||
import type { UdbContext, UdbTab } from '../../../components/UnifiedDataBar';
|
import type { UdbContext, UdbTab } from '../../../components/UnifiedDataBar';
|
||||||
import styles from './WorkflowFlowEditor.module.css';
|
import styles from './Automation2FlowEditor.module.css';
|
||||||
|
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
|
|
||||||
const LOG = '[WorkflowEditor]';
|
const LOG = '[Automation2]';
|
||||||
|
|
||||||
const CANVAS_HISTORY_MAX = 50;
|
const CANVAS_HISTORY_MAX = 50;
|
||||||
|
|
||||||
|
|
@ -81,7 +78,7 @@ function cloneCanvasSnapshot(nodes: CanvasNode[], connections: CanvasConnection[
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkflowFlowEditorProps {
|
interface Automation2FlowEditorProps {
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
mandateId?: string;
|
mandateId?: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
|
|
@ -95,7 +92,7 @@ interface WorkflowFlowEditorProps {
|
||||||
onSourcesChanged?: () => void;
|
onSourcesChanged?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instanceId,
|
export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ instanceId,
|
||||||
mandateId,
|
mandateId,
|
||||||
language = 'de',
|
language = 'de',
|
||||||
initialWorkflowId,
|
initialWorkflowId,
|
||||||
|
|
@ -113,9 +110,9 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
const [categories, setCategories] = useState<NodeTypeCategory[]>([]);
|
const [categories, setCategories] = useState<NodeTypeCategory[]>([]);
|
||||||
const [portTypeCatalog, setPortTypeCatalog] = useState<Record<string, unknown>>({});
|
const [portTypeCatalog, setPortTypeCatalog] = useState<Record<string, unknown>>({});
|
||||||
const [systemVariables, setSystemVariables] = useState<Record<string, unknown>>({});
|
const [systemVariables, setSystemVariables] = useState<Record<string, unknown>>({});
|
||||||
const [formFieldTypes, setFormFieldTypes] = useState<import('../../../api/workflowAutomationApi').FormFieldType[]>([]);
|
const [formFieldTypes, setFormFieldTypes] = useState<import('../../../api/workflowApi').FormFieldType[]>([]);
|
||||||
const [conditionOperatorCatalog, setConditionOperatorCatalog] = useState<
|
const [conditionOperatorCatalog, setConditionOperatorCatalog] = useState<
|
||||||
Record<string, import('../../../api/workflowAutomationApi').ConditionOperatorDef[]>
|
Record<string, import('../../../api/workflowApi').ConditionOperatorDef[]>
|
||||||
>({});
|
>({});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
@ -140,7 +137,7 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
const [canvasStickyNotes, setCanvasStickyNotes] = useState<CanvasStickyNote[]>([]);
|
const [canvasStickyNotes, setCanvasStickyNotes] = useState<CanvasStickyNote[]>([]);
|
||||||
const [executing, setExecuting] = useState(false);
|
const [executing, setExecuting] = useState(false);
|
||||||
const [executeResult, setExecuteResult] = useState<ExecuteGraphResponse | null>(null);
|
const [executeResult, setExecuteResult] = useState<ExecuteGraphResponse | null>(null);
|
||||||
const [workflows, setWorkflows] = useState<WorkflowDefinition[]>([]);
|
const [workflows, setWorkflows] = useState<Automation2Workflow[]>([]);
|
||||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
||||||
const [selectedNode, setSelectedNode] = useState<CanvasNode | null>(null);
|
const [selectedNode, setSelectedNode] = useState<CanvasNode | null>(null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
@ -156,12 +153,11 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
instanceId,
|
instanceId,
|
||||||
mandateId: mandateId || '',
|
mandateId: mandateId || '',
|
||||||
featureInstanceId: instanceId,
|
featureInstanceId: instanceId,
|
||||||
surface: 'workflowAutomation',
|
surface: 'graphEditor',
|
||||||
}), [instanceId, mandateId]);
|
}), [instanceId, mandateId]);
|
||||||
const [versions, setVersions] = useState<AutoVersion[]>([]);
|
const [versions, setVersions] = useState<AutoVersion[]>([]);
|
||||||
const [currentVersionId, setCurrentVersionId] = useState<string | null>(null);
|
const [currentVersionId, setCurrentVersionId] = useState<string | null>(null);
|
||||||
const [versionLoading, setVersionLoading] = useState(false);
|
const [versionLoading, setVersionLoading] = useState(false);
|
||||||
const didBootstrapEmptyCanvasRef = useRef(false);
|
|
||||||
|
|
||||||
const [targetFeatureInstanceId, setTargetFeatureInstanceId] = useState<string | null>(instanceId);
|
const [targetFeatureInstanceId, setTargetFeatureInstanceId] = useState<string | null>(instanceId);
|
||||||
|
|
||||||
|
|
@ -304,7 +300,7 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
|
|
||||||
const applyGraphWithSync = useCallback(
|
const applyGraphWithSync = useCallback(
|
||||||
(
|
(
|
||||||
graph: WorkflowGraph | null | undefined,
|
graph: Automation2Graph | null | undefined,
|
||||||
wfInvocations: WorkflowEntryPoint[] | undefined,
|
wfInvocations: WorkflowEntryPoint[] | undefined,
|
||||||
opts?: { skipHistory?: boolean }
|
opts?: { skipHistory?: boolean }
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -312,7 +308,7 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
pushCanvasHistoryPastFromCurrent();
|
pushCanvasHistoryPastFromCurrent();
|
||||||
}
|
}
|
||||||
setInvocations(wfInvocations ?? []);
|
setInvocations(wfInvocations ?? []);
|
||||||
const g: WorkflowGraph = graph ?? { nodes: [], connections: [] };
|
const g: Automation2Graph = graph ?? { nodes: [], connections: [] };
|
||||||
const { nodes, connections } = fromApiGraph(g, nodeTypes);
|
const { nodes, connections } = fromApiGraph(g, nodeTypes);
|
||||||
setCanvasNodes(nodes);
|
setCanvasNodes(nodes);
|
||||||
setCanvasConnections(connections);
|
setCanvasConnections(connections);
|
||||||
|
|
@ -321,7 +317,7 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFromApiGraph = useCallback(
|
const handleFromApiGraph = useCallback(
|
||||||
(graph: WorkflowGraph, wfInvocations?: WorkflowEntryPoint[]) => {
|
(graph: Automation2Graph, wfInvocations?: WorkflowEntryPoint[]) => {
|
||||||
applyGraphWithSync(graph, wfInvocations);
|
applyGraphWithSync(graph, wfInvocations);
|
||||||
},
|
},
|
||||||
[applyGraphWithSync]
|
[applyGraphWithSync]
|
||||||
|
|
@ -357,7 +353,7 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
setExecuteResult(null);
|
setExecuteResult(null);
|
||||||
try {
|
try {
|
||||||
const ep = currentWorkflowId ? invocations[0]?.id : undefined;
|
const ep = currentWorkflowId ? invocations[0]?.id : undefined;
|
||||||
const result = await executeGraph(request, graph, currentWorkflowId ?? undefined, {
|
const result = await executeGraph(request, instanceId, graph, currentWorkflowId ?? undefined, {
|
||||||
...(ep ? { entryPointId: ep } : {}),
|
...(ep ? { entryPointId: ep } : {}),
|
||||||
});
|
});
|
||||||
setExecuteResult(result);
|
setExecuteResult(result);
|
||||||
|
|
@ -406,7 +402,7 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
if (currentWorkflowId) {
|
if (currentWorkflowId) {
|
||||||
const updated = await updateWorkflow(request, currentWorkflowId, {
|
const updated = await updateWorkflow(request, instanceId, currentWorkflowId, {
|
||||||
graph,
|
graph,
|
||||||
invocations,
|
invocations,
|
||||||
targetFeatureInstanceId,
|
targetFeatureInstanceId,
|
||||||
|
|
@ -423,12 +419,11 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const created = await createWorkflow(request, {
|
const created = await createWorkflow(request, instanceId, {
|
||||||
label: label.trim() || t('Neuer Workflow'),
|
label: label.trim() || t('Neuer Workflow'),
|
||||||
graph,
|
graph,
|
||||||
invocations,
|
invocations,
|
||||||
targetFeatureInstanceId,
|
targetFeatureInstanceId,
|
||||||
mandateId,
|
|
||||||
});
|
});
|
||||||
setCurrentWorkflowId(created.id);
|
setCurrentWorkflowId(created.id);
|
||||||
setInvocations(created.invocations ?? []);
|
setInvocations(created.invocations ?? []);
|
||||||
|
|
@ -440,12 +435,12 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
}, [request, mandateId, canvasNodes, canvasConnections, currentWorkflowId, promptInput, invocations, t, nodeErrors, targetFeatureInstanceId, hasCanvasStartNode]);
|
}, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, promptInput, invocations, t, nodeErrors, targetFeatureInstanceId, hasCanvasStartNode]);
|
||||||
|
|
||||||
const handleLoad = useCallback(
|
const handleLoad = useCallback(
|
||||||
async (workflowId: string) => {
|
async (workflowId: string) => {
|
||||||
try {
|
try {
|
||||||
const wf = await fetchWorkflow(request, workflowId);
|
const wf = await fetchWorkflow(request, instanceId, workflowId);
|
||||||
if (wf.graph) {
|
if (wf.graph) {
|
||||||
handleFromApiGraph(wf.graph, wf.invocations);
|
handleFromApiGraph(wf.graph, wf.invocations);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -467,7 +462,7 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
setExecuteResult(null);
|
setExecuteResult(null);
|
||||||
applyGraphWithSync({ nodes: [], connections: [] }, []);
|
applyGraphWithSync({ nodes: [], connections: [] }, []);
|
||||||
try {
|
try {
|
||||||
const result = await fetchWorkflows(request);
|
const result = await fetchWorkflows(request, instanceId);
|
||||||
setWorkflows(Array.isArray(result) ? result : result.items);
|
setWorkflows(Array.isArray(result) ? result : result.items);
|
||||||
} catch (refreshErr) {
|
} catch (refreshErr) {
|
||||||
console.error(`${LOG} workflows refresh failed`, refreshErr);
|
console.error(`${LOG} workflows refresh failed`, refreshErr);
|
||||||
|
|
@ -480,7 +475,7 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[request, handleFromApiGraph, applyGraphWithSync, t]
|
[request, instanceId, handleFromApiGraph, applyGraphWithSync, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleWorkflowSelect = useCallback(
|
const handleWorkflowSelect = useCallback(
|
||||||
|
|
@ -548,10 +543,11 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadNodeTypes = useCallback(async () => {
|
const loadNodeTypes = useCallback(async () => {
|
||||||
|
if (!instanceId) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const data = await fetchNodeTypes(request, mandateId || '', language);
|
const data = await fetchNodeTypes(request, instanceId, language);
|
||||||
setNodeTypes(data.nodeTypes);
|
setNodeTypes(data.nodeTypes);
|
||||||
setCategories(data.categories);
|
setCategories(data.categories);
|
||||||
if (data.portTypeCatalog) {
|
if (data.portTypeCatalog) {
|
||||||
|
|
@ -568,16 +564,17 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [language, request]);
|
}, [instanceId, language, request]);
|
||||||
|
|
||||||
const loadWorkflows = useCallback(async () => {
|
const loadWorkflows = useCallback(async () => {
|
||||||
|
if (!instanceId) return;
|
||||||
try {
|
try {
|
||||||
const result = await fetchWorkflows(request, { mandateId: mandateId || undefined });
|
const result = await fetchWorkflows(request, instanceId);
|
||||||
setWorkflows(Array.isArray(result) ? result : result.items);
|
setWorkflows(Array.isArray(result) ? result : result.items);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`${LOG} loadWorkflows failed`, e);
|
console.error(`${LOG} loadWorkflows failed`, e);
|
||||||
}
|
}
|
||||||
}, [request, mandateId]);
|
}, [instanceId, request]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadNodeTypes();
|
loadNodeTypes();
|
||||||
|
|
@ -601,22 +598,8 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading || nodeTypes.length === 0) return;
|
if (loading || nodeTypes.length === 0) return;
|
||||||
if (currentWorkflowId || initialWorkflowId) {
|
if (currentWorkflowId || initialWorkflowId) return;
|
||||||
didBootstrapEmptyCanvasRef.current = false;
|
if (canvasNodes.length > 0) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (didBootstrapEmptyCanvasRef.current) return;
|
|
||||||
didBootstrapEmptyCanvasRef.current = true;
|
|
||||||
if (canvasNodes.length === 0 && canvasConnections.length === 0 && invocations.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.debug(`${LOG} bootstrapping empty canvas`, {
|
|
||||||
currentWorkflowId,
|
|
||||||
initialWorkflowId,
|
|
||||||
canvasNodes: canvasNodes.length,
|
|
||||||
canvasConnections: canvasConnections.length,
|
|
||||||
invocations: invocations.length,
|
|
||||||
});
|
|
||||||
applyGraphWithSync({ nodes: [], connections: [] }, [], {
|
applyGraphWithSync({ nodes: [], connections: [] }, [], {
|
||||||
skipHistory: true,
|
skipHistory: true,
|
||||||
});
|
});
|
||||||
|
|
@ -626,9 +609,8 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
currentWorkflowId,
|
currentWorkflowId,
|
||||||
initialWorkflowId,
|
initialWorkflowId,
|
||||||
canvasNodes.length,
|
canvasNodes.length,
|
||||||
canvasConnections.length,
|
|
||||||
invocations.length,
|
|
||||||
applyGraphWithSync,
|
applyGraphWithSync,
|
||||||
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const toggleCategory = useCallback((id: string) => {
|
const toggleCategory = useCallback((id: string) => {
|
||||||
|
|
@ -667,17 +649,17 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadVersions = useCallback(async () => {
|
const loadVersions = useCallback(async () => {
|
||||||
if (!currentWorkflowId) {
|
if (!instanceId || !currentWorkflowId) {
|
||||||
setVersions([]);
|
setVersions([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const v = await fetchVersions(request, currentWorkflowId);
|
const v = await fetchVersions(request, instanceId, currentWorkflowId);
|
||||||
setVersions(v);
|
setVersions(v);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`${LOG} loadVersions failed`, e);
|
console.error(`${LOG} loadVersions failed`, e);
|
||||||
}
|
}
|
||||||
}, [currentWorkflowId, request]);
|
}, [instanceId, currentWorkflowId, request]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadVersions();
|
loadVersions();
|
||||||
|
|
@ -698,9 +680,10 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
|
|
||||||
const handlePublishVersion = useCallback(
|
const handlePublishVersion = useCallback(
|
||||||
async (versionId: string) => {
|
async (versionId: string) => {
|
||||||
|
if (!instanceId) return;
|
||||||
setVersionLoading(true);
|
setVersionLoading(true);
|
||||||
try {
|
try {
|
||||||
await publishVersion(request, versionId);
|
await publishVersion(request, instanceId, versionId);
|
||||||
await loadVersions();
|
await loadVersions();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) });
|
setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) });
|
||||||
|
|
@ -708,14 +691,15 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
setVersionLoading(false);
|
setVersionLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[request, loadVersions]
|
[request, instanceId, loadVersions]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleUnpublishVersion = useCallback(
|
const handleUnpublishVersion = useCallback(
|
||||||
async (versionId: string) => {
|
async (versionId: string) => {
|
||||||
|
if (!instanceId) return;
|
||||||
setVersionLoading(true);
|
setVersionLoading(true);
|
||||||
try {
|
try {
|
||||||
await unpublishVersion(request, versionId);
|
await unpublishVersion(request, instanceId, versionId);
|
||||||
await loadVersions();
|
await loadVersions();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) });
|
setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) });
|
||||||
|
|
@ -723,14 +707,15 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
setVersionLoading(false);
|
setVersionLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[request, loadVersions]
|
[request, instanceId, loadVersions]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleArchiveVersion = useCallback(
|
const handleArchiveVersion = useCallback(
|
||||||
async (versionId: string) => {
|
async (versionId: string) => {
|
||||||
|
if (!instanceId) return;
|
||||||
setVersionLoading(true);
|
setVersionLoading(true);
|
||||||
try {
|
try {
|
||||||
await archiveVersion(request, versionId);
|
await archiveVersion(request, instanceId, versionId);
|
||||||
await loadVersions();
|
await loadVersions();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) });
|
setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) });
|
||||||
|
|
@ -738,14 +723,14 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
setVersionLoading(false);
|
setVersionLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[request, loadVersions]
|
[request, instanceId, loadVersions]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateDraft = useCallback(async () => {
|
const handleCreateDraft = useCallback(async () => {
|
||||||
if (!currentWorkflowId) return;
|
if (!instanceId || !currentWorkflowId) return;
|
||||||
setVersionLoading(true);
|
setVersionLoading(true);
|
||||||
try {
|
try {
|
||||||
const draft = await createDraftVersion(request, currentWorkflowId);
|
const draft = await createDraftVersion(request, instanceId, currentWorkflowId);
|
||||||
await loadVersions();
|
await loadVersions();
|
||||||
setCurrentVersionId(draft.id);
|
setCurrentVersionId(draft.id);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
|
|
@ -753,16 +738,16 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
} finally {
|
} finally {
|
||||||
setVersionLoading(false);
|
setVersionLoading(false);
|
||||||
}
|
}
|
||||||
}, [request, currentWorkflowId, loadVersions]);
|
}, [request, instanceId, currentWorkflowId, loadVersions]);
|
||||||
|
|
||||||
// Template: save current workflow as template
|
// Template: save current workflow as template
|
||||||
const [templateSaving, setTemplateSaving] = useState(false);
|
const [templateSaving, setTemplateSaving] = useState(false);
|
||||||
const handleSaveAsTemplate = useCallback(
|
const handleSaveAsTemplate = useCallback(
|
||||||
async (scope: AutoTemplateScope) => {
|
async (scope: AutoTemplateScope) => {
|
||||||
if (!currentWorkflowId) return;
|
if (!instanceId || !currentWorkflowId) return;
|
||||||
setTemplateSaving(true);
|
setTemplateSaving(true);
|
||||||
try {
|
try {
|
||||||
await createTemplateFromWorkflow(request, currentWorkflowId, scope);
|
await createTemplateFromWorkflow(request, instanceId, currentWorkflowId, scope);
|
||||||
setExecuteResult({ success: true, error: undefined } as unknown as ExecuteGraphResponse);
|
setExecuteResult({ success: true, error: undefined } as unknown as ExecuteGraphResponse);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) });
|
setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) });
|
||||||
|
|
@ -770,15 +755,16 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
setTemplateSaving(false);
|
setTemplateSaving(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[request, currentWorkflowId]
|
[request, instanceId, currentWorkflowId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Template: new workflow from template
|
// Template: new workflow from template
|
||||||
const [templatePickerOpen, setTemplatePickerOpen] = useState(false);
|
const [templatePickerOpen, setTemplatePickerOpen] = useState(false);
|
||||||
const handleNewFromTemplate = useCallback(
|
const handleNewFromTemplate = useCallback(
|
||||||
async (templateId: string) => {
|
async (templateId: string) => {
|
||||||
|
if (!instanceId) return;
|
||||||
try {
|
try {
|
||||||
const wf = await copyTemplate(request, templateId);
|
const wf = await copyTemplate(request, instanceId, templateId);
|
||||||
setWorkflows((prev) => [...prev, wf]);
|
setWorkflows((prev) => [...prev, wf]);
|
||||||
setCurrentWorkflowId(wf.id);
|
setCurrentWorkflowId(wf.id);
|
||||||
if (wf.graph) handleFromApiGraph(wf.graph, wf.invocations);
|
if (wf.graph) handleFromApiGraph(wf.graph, wf.invocations);
|
||||||
|
|
@ -787,7 +773,7 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) });
|
setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[request, handleFromApiGraph]
|
[request, instanceId, handleFromApiGraph]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -945,20 +931,12 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
activeTab={udbTab as UdbTab}
|
activeTab={udbTab as UdbTab}
|
||||||
onTabChange={(tab) => setUdbTab(tab as LeftTab)}
|
onTabChange={(tab) => setUdbTab(tab as LeftTab)}
|
||||||
hideTabs={['chats']}
|
hideTabs={['chats']}
|
||||||
onFileSelect={async (fileId, fileName) => {
|
onFileSelect={onFileSelect}
|
||||||
if (fileName?.toLowerCase().endsWith(WORKFLOW_FILE_EXTENSION)) {
|
|
||||||
try {
|
|
||||||
const result = await importWorkflowFromFile(request, { fileId });
|
|
||||||
await loadWorkflows();
|
|
||||||
if (result?.workflow?.id) handleWorkflowSelect(result.workflow.id);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[workflowAutomation] workflow file import failed', e);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onFileSelect?.(fileId, fileName);
|
|
||||||
}}
|
|
||||||
onSourcesChanged={onSourcesChanged}
|
onSourcesChanged={onSourcesChanged}
|
||||||
|
onWorkflowImportedFromFile={async (workflowId) => {
|
||||||
|
await loadWorkflows();
|
||||||
|
handleWorkflowSelect(workflowId);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1030,12 +1008,12 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
stickyNotes={canvasStickyNotes}
|
stickyNotes={canvasStickyNotes}
|
||||||
onStickyNotesChange={setCanvasStickyNotes}
|
onStickyNotesChange={setCanvasStickyNotes}
|
||||||
onExternalDrop={async (mime, payload) => {
|
onExternalDrop={async (mime, payload) => {
|
||||||
if (mime !== 'application/json+workflow') return false;
|
if (mime !== 'application/json+workflow' || !instanceId) return false;
|
||||||
const p = payload as { files?: Array<{ id: string }> } | undefined;
|
const p = payload as { files?: Array<{ id: string }> } | undefined;
|
||||||
const fileId = p?.files?.[0]?.id;
|
const fileId = p?.files?.[0]?.id;
|
||||||
if (!fileId) return false;
|
if (!fileId) return false;
|
||||||
try {
|
try {
|
||||||
const result = await importWorkflowFromFile(request, { fileId });
|
const result = await importWorkflowFromFile(request, instanceId, { fileId });
|
||||||
await loadWorkflows();
|
await loadWorkflows();
|
||||||
if (result?.workflow?.id) handleWorkflowSelect(result.workflow.id);
|
if (result?.workflow?.id) handleWorkflowSelect(result.workflow.id);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1048,7 +1026,7 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
</div>
|
</div>
|
||||||
{configurableSelected && selectedNode && (
|
{configurableSelected && selectedNode && (
|
||||||
<div className={styles.nodeConfigPanelWrap} data-suppress-flow-node-hotkeys="">
|
<div className={styles.nodeConfigPanelWrap} data-suppress-flow-node-hotkeys="">
|
||||||
<WorkflowDataFlowProvider
|
<Automation2DataFlowProvider
|
||||||
node={selectedNode}
|
node={selectedNode}
|
||||||
nodes={canvasNodes}
|
nodes={canvasNodes}
|
||||||
connections={canvasConnections}
|
connections={canvasConnections}
|
||||||
|
|
@ -1073,7 +1051,7 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
request={request}
|
request={request}
|
||||||
verboseSchema={verboseSchema}
|
verboseSchema={verboseSchema}
|
||||||
/>
|
/>
|
||||||
</WorkflowDataFlowProvider>
|
</Automation2DataFlowProvider>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1128,4 +1106,4 @@ export const WorkflowFlowEditor: React.FC<WorkflowFlowEditorProps> = ({ instance
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WorkflowFlowEditor;
|
export default Automation2FlowEditor;
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* CanvasHeader - Workflow controls, version selector, and execute result.
|
* CanvasHeader - Workflow controls, version selector, and execute result.
|
||||||
*/
|
*/
|
||||||
|
|
@ -25,11 +23,12 @@ import {
|
||||||
HiOutlineArrowUturnRight,
|
HiOutlineArrowUturnRight,
|
||||||
HiOutlineTrash,
|
HiOutlineTrash,
|
||||||
HiOutlineDocumentDuplicate,
|
HiOutlineDocumentDuplicate,
|
||||||
|
HiOutlineArrowLongRight,
|
||||||
HiOutlineChatBubbleLeftEllipsis,
|
HiOutlineChatBubbleLeftEllipsis,
|
||||||
HiOutlineSquares2X2,
|
HiOutlineSquares2X2,
|
||||||
} from 'react-icons/hi2';
|
} from 'react-icons/hi2';
|
||||||
import type { WorkflowDefinition, ExecuteGraphResponse, AutoVersion, AutoTemplateScope } from '../../../api/workflowAutomationApi';
|
import type { Automation2Workflow, ExecuteGraphResponse, AutoVersion, AutoTemplateScope } from '../../../api/workflowApi';
|
||||||
import styles from './WorkflowFlowEditor.module.css';
|
import styles from './Automation2FlowEditor.module.css';
|
||||||
|
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
import { getUserDataCache } from '../../../utils/userCache';
|
import { getUserDataCache } from '../../../utils/userCache';
|
||||||
|
|
@ -62,7 +61,7 @@ export interface CanvasHeaderCanvasEditProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CanvasHeaderProps {
|
interface CanvasHeaderProps {
|
||||||
workflows: WorkflowDefinition[];
|
workflows: Automation2Workflow[];
|
||||||
currentWorkflowId: string | null;
|
currentWorkflowId: string | null;
|
||||||
onWorkflowSelect: (workflowId: string | null) => void;
|
onWorkflowSelect: (workflowId: string | null) => void;
|
||||||
onNew: () => void;
|
onNew: () => void;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* EditorChatPanel
|
* EditorChatPanel
|
||||||
*
|
*
|
||||||
* AI Chat sidebar for the WorkflowAutomation editor.
|
* AI Chat sidebar for the GraphicalEditor.
|
||||||
* Streams responses via SSE (same pattern as Workspace chat).
|
* Streams responses via SSE (same pattern as Workspace chat).
|
||||||
* File & data-source attachment UX mirrors WorkspaceInput:
|
* File & data-source attachment UX mirrors WorkspaceInput:
|
||||||
* - Files: drag & drop from FilesTab (UDB) onto input area, or click in UDB
|
* - Files: drag & drop from FilesTab (UDB) onto input area, or click in UDB
|
||||||
|
|
@ -89,7 +87,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
|
||||||
|
|
||||||
// Load persisted chat history from the backend whenever the workflow changes.
|
// Load persisted chat history from the backend whenever the workflow changes.
|
||||||
// The chat is stored in `ChatWorkflow.linkedWorkflowId == workflowId` and is
|
// The chat is stored in `ChatWorkflow.linkedWorkflowId == workflowId` and is
|
||||||
// returned by `GET /api/workflow-automation/{workflowId}/chat/messages`.
|
// returned by `GET /api/workflows/{instanceId}/{workflowId}/chat/messages`.
|
||||||
// For an unsaved workflow (workflowId == null) we just clear the panel.
|
// For an unsaved workflow (workflowId == null) we just clear the panel.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workflowId) {
|
if (!workflowId) {
|
||||||
|
|
@ -101,7 +99,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
|
||||||
setHistoryLoading(true);
|
setHistoryLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await api.get<PersistedEditorChatResponse>(
|
const res = await api.get<PersistedEditorChatResponse>(
|
||||||
`/api/workflow-automation/${workflowId}/chat/messages`,
|
`/api/workflows/${instanceId}/${workflowId}/chat/messages`,
|
||||||
);
|
);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const persisted = (res.data?.messages || []).map((m): ChatMessage => ({
|
const persisted = (res.data?.messages || []).map((m): ChatMessage => ({
|
||||||
|
|
@ -168,7 +166,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
|
||||||
|
|
||||||
const baseURL = api.defaults.baseURL || '';
|
const baseURL = api.defaults.baseURL || '';
|
||||||
const cleanup = startSseStream({
|
const cleanup = startSseStream({
|
||||||
url: `${baseURL}/api/workflow-automation/${workflowId}/chat/stream`,
|
url: `${baseURL}/api/workflows/${instanceId}/${workflowId}/chat/stream`,
|
||||||
body,
|
body,
|
||||||
handlers: {
|
handlers: {
|
||||||
onChunk: (event) => {
|
onChunk: (event) => {
|
||||||
|
|
@ -229,7 +227,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
|
||||||
: m));
|
: m));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await api.post(`/api/workflow-automation/${workflowId}/chat/stop`);
|
await api.post(`/api/workflows/${instanceId}/${workflowId}/chat/stop`);
|
||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
abortRef.current?.();
|
abortRef.current?.();
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* EditorWorkflowChatList
|
* EditorWorkflowChatList
|
||||||
*
|
*
|
||||||
* UDB "Chats" tab content for the WorkflowAutomation editor: each AutoWorkflow
|
* UDB "Chats" tab content for the GraphicalEditor: each AutoWorkflow is treated
|
||||||
* is treated as one editor chat session. Lists workflows already loaded by the
|
* as one editor chat session. Lists workflows already loaded by the parent
|
||||||
* parent editor (no extra fetch), supports search and "+ Neu" to start a fresh
|
* editor (no extra fetch), supports search and "+ Neu" to start a fresh
|
||||||
* workflow chat. Mirrors the spirit of the Workspace ChatsTab but uses
|
* workflow chat. Mirrors the spirit of the Workspace ChatsTab but uses
|
||||||
* WorkflowAutomation data instead of the workspace endpoint.
|
* GraphicalEditor data instead of the workspace endpoint.
|
||||||
*/
|
*/
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import type { WorkflowDefinition } from '../../../api/workflowAutomationApi';
|
import type { Automation2Workflow } from '../../../api/workflowApi';
|
||||||
|
|
||||||
interface EditorWorkflowChatListProps {
|
interface EditorWorkflowChatListProps {
|
||||||
workflows: WorkflowDefinition[];
|
workflows: Automation2Workflow[];
|
||||||
currentWorkflowId: string | null;
|
currentWorkflowId: string | null;
|
||||||
onSelect: (workflowId: string | null) => void;
|
onSelect: (workflowId: string | null) => void;
|
||||||
onNew: () => void;
|
onNew: () => void;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* FlowCanvas - Workflow graph canvas with nodes and connection lines.
|
* FlowCanvas - Workflow graph canvas with nodes and connection lines.
|
||||||
* Nodes have 4 connection handles (one per side), drag nodes to add, connect with arrows.
|
* Nodes have 4 connection handles (one per side), drag nodes to add, connect with arrows.
|
||||||
|
|
@ -15,15 +13,13 @@ import React, {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import type { GraphDefinedSchemaRef, NodeType } from '../../../api/workflowAutomationApi';
|
import type { GraphDefinedSchemaRef, NodeType } from '../../../api/workflowApi';
|
||||||
import styles from './WorkflowFlowEditor.module.css';
|
import styles from './Automation2FlowEditor.module.css';
|
||||||
|
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
import { AiBadge } from '../nodes/shared/AiBadge';
|
import { AiBadge } from '../nodes/shared/AiBadge';
|
||||||
import { switchOutputLabel } from '../nodes/shared/graphUtils';
|
import { switchOutputLabel } from '../nodes/shared/graphUtils';
|
||||||
|
|
||||||
const LOG = '[FlowCanvas]';
|
|
||||||
|
|
||||||
export interface CanvasNode {
|
export interface CanvasNode {
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
|
@ -846,8 +842,6 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
|
||||||
|
|
||||||
const onHistoryCheckpointRef = useRef(onHistoryCheckpoint);
|
const onHistoryCheckpointRef = useRef(onHistoryCheckpoint);
|
||||||
onHistoryCheckpointRef.current = onHistoryCheckpoint;
|
onHistoryCheckpointRef.current = onHistoryCheckpoint;
|
||||||
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
||||||
onSelectionChangeRef.current = onSelectionChange;
|
|
||||||
|
|
||||||
const emitHistoryCheckpoint = useCallback(() => {
|
const emitHistoryCheckpoint = useCallback(() => {
|
||||||
onHistoryCheckpointRef.current?.();
|
onHistoryCheckpointRef.current?.();
|
||||||
|
|
@ -1025,19 +1019,12 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const lastEmittedSelectionRef = useRef<{ nodeId: string | null; signature: string | null }>({
|
|
||||||
nodeId: null,
|
|
||||||
signature: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const node = selectedNodeId ? nodes.find((n) => n.id === selectedNodeId) ?? null : null;
|
if (onSelectionChange) {
|
||||||
const signature = node ? JSON.stringify(node) : null;
|
const node = selectedNodeId ? nodes.find((n) => n.id === selectedNodeId) ?? null : null;
|
||||||
const last = lastEmittedSelectionRef.current;
|
onSelectionChange(node);
|
||||||
if (last.nodeId === selectedNodeId && last.signature === signature) return;
|
}
|
||||||
lastEmittedSelectionRef.current = { nodeId: selectedNodeId, signature };
|
}, [selectedNodeId, nodes, onSelectionChange]);
|
||||||
onSelectionChangeRef.current?.(node);
|
|
||||||
}, [selectedNodeId, nodes]);
|
|
||||||
|
|
||||||
const handleConnectionClick = useCallback((e: React.MouseEvent, connId: string) => {
|
const handleConnectionClick = useCallback((e: React.MouseEvent, connId: string) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -1101,11 +1088,6 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
|
||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback(
|
||||||
async (e: React.DragEvent) => {
|
async (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.debug(`${LOG} drop received`, {
|
|
||||||
types: Array.from(e.dataTransfer.types),
|
|
||||||
clientX: e.clientX,
|
|
||||||
clientY: e.clientY,
|
|
||||||
});
|
|
||||||
// 1) externe Drop-Targets (z. B. ``application/json+workflow`` aus UDB-FilesTab)
|
// 1) externe Drop-Targets (z. B. ``application/json+workflow`` aus UDB-FilesTab)
|
||||||
if (onExternalDrop) {
|
if (onExternalDrop) {
|
||||||
const reservedMimes = new Set([
|
const reservedMimes = new Set([
|
||||||
|
|
@ -1131,35 +1113,16 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
|
||||||
}
|
}
|
||||||
// 2) Standard: Node-Type aus der NodeSidebar
|
// 2) Standard: Node-Type aus der NodeSidebar
|
||||||
const raw = e.dataTransfer.getData('application/json');
|
const raw = e.dataTransfer.getData('application/json');
|
||||||
if (!raw || !containerRef.current) {
|
if (!raw || !containerRef.current) return;
|
||||||
console.debug(`${LOG} drop ignored`, {
|
|
||||||
hasRaw: Boolean(raw),
|
|
||||||
hasContainer: Boolean(containerRef.current),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const { type } = JSON.parse(raw);
|
const { type } = JSON.parse(raw);
|
||||||
const el = containerRef.current;
|
const el = containerRef.current;
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
const x = (e.clientX - rect.left - panOffset.x) / zoom - NODE_WIDTH / 2;
|
const x = (e.clientX - rect.left - panOffset.x) / zoom - NODE_WIDTH / 2;
|
||||||
const y = (e.clientY - rect.top - panOffset.y) / zoom - NODE_HEIGHT / 2;
|
const y = (e.clientY - rect.top - panOffset.y) / zoom - NODE_HEIGHT / 2;
|
||||||
console.debug(`${LOG} placing node from drop`, {
|
|
||||||
type,
|
|
||||||
raw,
|
|
||||||
dropX: x,
|
|
||||||
dropY: y,
|
|
||||||
panOffset,
|
|
||||||
zoom,
|
|
||||||
});
|
|
||||||
onDropNodeType(type, Math.max(0, x), Math.max(0, y));
|
onDropNodeType(type, Math.max(0, x), Math.max(0, y));
|
||||||
emitHistoryCheckpoint();
|
emitHistoryCheckpoint();
|
||||||
} catch (error) {
|
} catch (_) {}
|
||||||
console.debug(`${LOG} drop parse failed`, {
|
|
||||||
raw,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[onDropNodeType, onExternalDrop, panOffset, zoom, emitHistoryCheckpoint]
|
[onDropNodeType, onExternalDrop, panOffset, zoom, emitHistoryCheckpoint]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* NodeConfigPanel - Generic parameter renderer for all node types.
|
* NodeConfigPanel - Generic parameter renderer for all node types.
|
||||||
* Renders each parameter using FRONTEND_TYPE_RENDERERS based on frontendType.
|
* Renders each parameter using FRONTEND_TYPE_RENDERERS based on frontendType.
|
||||||
|
|
@ -7,15 +5,14 @@
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||||
import type { CanvasNode } from './FlowCanvas';
|
import type { CanvasNode } from './FlowCanvas';
|
||||||
import type { GraphDefinedSchemaRef, NodeType, NodeTypeParameter, PortSchema } from '../../../api/workflowAutomationApi';
|
import type { GraphDefinedSchemaRef, NodeType, NodeTypeParameter, PortSchema } from '../../../api/workflowApi';
|
||||||
import type { ApiRequestFunction } from '../../../api/workflowAutomationApi';
|
import type { ApiRequestFunction } from '../../../api/workflowApi';
|
||||||
import { getLabel } from '../nodes/shared/utils';
|
import { getLabel } from '../nodes/shared/utils';
|
||||||
import { FRONTEND_TYPE_RENDERERS } from '../nodes/frontendTypeRenderers';
|
import { FRONTEND_TYPE_RENDERERS } from '../nodes/frontendTypeRenderers';
|
||||||
import { ContextBuilderRenderer } from '../nodes/frontendTypeRenderers/ContextBuilderRenderer';
|
|
||||||
import { RequiredAttributePicker } from '../nodes/shared/RequiredAttributePicker';
|
import { RequiredAttributePicker } from '../nodes/shared/RequiredAttributePicker';
|
||||||
import { findRequiredErrors } from '../nodes/shared/paramValidation';
|
import { findRequiredErrors } from '../nodes/shared/paramValidation';
|
||||||
import { useWorkflowDataFlow } from '../context/WorkflowDataFlowContext';
|
import { useAutomation2DataFlow } from '../context/Automation2DataFlowContext';
|
||||||
import styles from './WorkflowFlowEditor.module.css';
|
import styles from './Automation2FlowEditor.module.css';
|
||||||
|
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
import { AccordionList } from '../../UiComponents/AccordionList';
|
import { AccordionList } from '../../UiComponents/AccordionList';
|
||||||
|
|
@ -212,7 +209,7 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
|
||||||
[onParametersChange]
|
[onParametersChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
const dataFlow = useWorkflowDataFlow();
|
const dataFlow = useAutomation2DataFlow();
|
||||||
const portTypeCatalog: Record<string, PortSchema> = (dataFlow?.portTypeCatalog as Record<string, PortSchema> | undefined) ?? {};
|
const portTypeCatalog: Record<string, PortSchema> = (dataFlow?.portTypeCatalog as Record<string, PortSchema> | undefined) ?? {};
|
||||||
|
|
||||||
// Phase-4 Schicht-4 — Pflicht-Params zuerst sortieren, damit der User
|
// Phase-4 Schicht-4 — Pflicht-Params zuerst sortieren, damit der User
|
||||||
|
|
@ -256,7 +253,6 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
|
||||||
|
|
||||||
for (const param of sortedParameters) {
|
for (const param of sortedParameters) {
|
||||||
if (param.frontendType === 'hidden') continue;
|
if (param.frontendType === 'hidden') continue;
|
||||||
if (param.name === 'context') continue;
|
|
||||||
if (CONTEXT_EXTRACT_CHUNK_SET.has(param.name)) continue;
|
if (CONTEXT_EXTRACT_CHUNK_SET.has(param.name)) continue;
|
||||||
if (!parameterVisibleForFrontendOptions(param, params, nodeType)) continue;
|
if (!parameterVisibleForFrontendOptions(param, params, nodeType)) continue;
|
||||||
|
|
||||||
|
|
@ -382,15 +378,6 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
|
||||||
t,
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const extractContentContextParam = useMemo((): NodeTypeParameter | null => {
|
|
||||||
if (!node || !nodeType || node.type !== CONTEXT_EXTRACT_CONTENT_NODE_TYPE) return null;
|
|
||||||
const param = sortedParameters.find((p) => p.name === 'context') ?? null;
|
|
||||||
if (!param) return null;
|
|
||||||
if (param.frontendType === 'hidden') return null;
|
|
||||||
if (!parameterVisibleForFrontendOptions(param, params, nodeType)) return null;
|
|
||||||
return param;
|
|
||||||
}, [node, nodeType, sortedParameters, params]);
|
|
||||||
|
|
||||||
if (!node || !nodeType) return null;
|
if (!node || !nodeType) return null;
|
||||||
|
|
||||||
const isTrigger = node.type.startsWith('trigger.');
|
const isTrigger = node.type.startsWith('trigger.');
|
||||||
|
|
@ -496,71 +483,11 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{extractContentAccordionItems !== null ? (
|
{extractContentAccordionItems !== null ? (
|
||||||
<>
|
<AccordionList<string>
|
||||||
{extractContentContextParam ? (
|
key={`${node.id}-extract-accordion`}
|
||||||
<div
|
defaultOpenId={null}
|
||||||
key={`${node.id}-${extractContentContextParam.name}`}
|
items={extractContentAccordionItems}
|
||||||
style={{ marginBottom: 8, minWidth: 0, maxWidth: '100%' }}
|
/>
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 6,
|
|
||||||
marginBottom: 2,
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
minWidth: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{extractContentContextParam.required && (
|
|
||||||
<span
|
|
||||||
title={t('Pflichtfeld')}
|
|
||||||
style={{ color: 'var(--danger-color, #dc3545)', fontWeight: 700, flexShrink: 0 }}
|
|
||||||
>
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{verboseSchema && extractContentContextParam.type && (
|
|
||||||
<span
|
|
||||||
title={t('Parameter-Typ')}
|
|
||||||
style={{
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: 'var(--text-secondary)',
|
|
||||||
background: 'var(--bg-secondary)',
|
|
||||||
border: '1px solid var(--border-color)',
|
|
||||||
borderRadius: 4,
|
|
||||||
padding: '1px 6px',
|
|
||||||
maxWidth: '100%',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{extractContentContextParam.type}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<ContextBuilderRenderer
|
|
||||||
param={extractContentContextParam}
|
|
||||||
value={workflowParamUiValue(params, extractContentContextParam)}
|
|
||||||
onChange={(val: unknown) => updateParam(extractContentContextParam.name, val)}
|
|
||||||
allParams={params}
|
|
||||||
instanceId={instanceId}
|
|
||||||
request={request}
|
|
||||||
nodeType={node.type}
|
|
||||||
onPatchParams={patchParams}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{extractContentAccordionItems.length > 0 ? (
|
|
||||||
<AccordionList<string>
|
|
||||||
key={`${node.id}-extract-accordion`}
|
|
||||||
defaultOpenId={null}
|
|
||||||
items={extractContentAccordionItems}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
parameters.map((param: NodeTypeParameter) => {
|
parameters.map((param: NodeTypeParameter) => {
|
||||||
// Safety net: hidden params have no UI footprint at all — no row,
|
// Safety net: hidden params have no UI footprint at all — no row,
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* NodeListItem - Draggable node type item for the sidebar.
|
* NodeListItem - Draggable node type item for the sidebar.
|
||||||
* Used in both regular categories and I/O sub-groups.
|
* Used in both regular categories and I/O sub-groups.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { NodeType } from '../../../api/workflowAutomationApi';
|
import type { NodeType } from '../../../api/workflowApi';
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
import { getCategoryIcon } from '../nodes/shared/utils';
|
import { getCategoryIcon } from '../nodes/shared/utils';
|
||||||
import type { GetLabelFn } from '../nodes/shared/utils';
|
import type { GetLabelFn } from '../nodes/shared/utils';
|
||||||
import styles from './WorkflowFlowEditor.module.css';
|
import styles from './Automation2FlowEditor.module.css';
|
||||||
import { AiBadge } from '../nodes/shared/AiBadge';
|
import { AiBadge } from '../nodes/shared/AiBadge';
|
||||||
|
|
||||||
interface NodeListItemProps {
|
interface NodeListItemProps {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* NodeSidebar - Sidebar with searchable, collapsible node list.
|
* NodeSidebar - Sidebar with searchable, collapsible node list.
|
||||||
* Groups node types by category (start, input, flow, data, ai, email, sharepoint).
|
* Groups node types by category (start, input, flow, data, ai, email, sharepoint).
|
||||||
|
|
@ -7,11 +5,11 @@
|
||||||
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { FaChevronDown, FaChevronRight } from 'react-icons/fa';
|
import { FaChevronDown, FaChevronRight } from 'react-icons/fa';
|
||||||
import type { NodeType, NodeTypeCategory } from '../../../api/workflowAutomationApi';
|
import type { NodeType, NodeTypeCategory } from '../../../api/workflowApi';
|
||||||
import { CATEGORY_ORDER, HIDDEN_NODE_IDS } from '../nodes/shared/constants';
|
import { CATEGORY_ORDER, HIDDEN_NODE_IDS } from '../nodes/shared/constants';
|
||||||
import { getLabel } from '../nodes/shared/utils';
|
import { getLabel } from '../nodes/shared/utils';
|
||||||
import { NodeListItem } from './NodeListItem';
|
import { NodeListItem } from './NodeListItem';
|
||||||
import styles from './WorkflowFlowEditor.module.css';
|
import styles from './Automation2FlowEditor.module.css';
|
||||||
|
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* RunTracingPanel
|
* RunTracingPanel
|
||||||
*
|
*
|
||||||
|
|
@ -9,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useApiRequest } from '../../../hooks/useApi';
|
import { useApiRequest } from '../../../hooks/useApi';
|
||||||
import type { AutoStepLog } from '../../../api/workflowAutomationApi';
|
import type { AutoStepLog } from '../../../api/workflowApi';
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
|
|
@ -100,7 +98,7 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await request({
|
const data = await request({
|
||||||
url: `/api/workflow-automation/runs/${runId}/steps`,
|
url: `/api/workflows/${instanceId}/runs/${runId}/steps`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
});
|
||||||
setSteps(data?.steps || []);
|
setSteps(data?.steps || []);
|
||||||
|
|
@ -117,7 +115,7 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
|
||||||
loadSteps();
|
loadSteps();
|
||||||
|
|
||||||
const baseUrl = api.defaults.baseURL || '';
|
const baseUrl = api.defaults.baseURL || '';
|
||||||
const url = `${baseUrl}/api/workflow-automation/runs/${runId}/stream`;
|
const url = `${baseUrl}/api/workflows/${instanceId}/runs/${runId}/stream`;
|
||||||
const es = new EventSource(url, { withCredentials: true });
|
const es = new EventSource(url, { withCredentials: true });
|
||||||
eventSourceRef.current = es;
|
eventSourceRef.current = es;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* TemplatePicker - modal to browse and select a workflow template for creating a new workflow.
|
* TemplatePicker - modal to browse and select a workflow template for creating a new workflow.
|
||||||
*/
|
*/
|
||||||
|
|
@ -11,8 +9,8 @@ import {
|
||||||
type AutoWorkflowTemplate,
|
type AutoWorkflowTemplate,
|
||||||
type AutoTemplateScope,
|
type AutoTemplateScope,
|
||||||
type ApiRequestFunction,
|
type ApiRequestFunction,
|
||||||
} from '../../../api/workflowAutomationApi';
|
} from '../../../api/workflowApi';
|
||||||
import styles from './WorkflowFlowEditor.module.css';
|
import styles from './Automation2FlowEditor.module.css';
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
interface TemplatePickerProps {
|
interface TemplatePickerProps {
|
||||||
|
|
@ -52,7 +50,7 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const scope = activeScope === 'all' ? undefined : activeScope;
|
const scope = activeScope === 'all' ? undefined : activeScope;
|
||||||
const result = await fetchTemplates(request, scope);
|
const result = await fetchTemplates(request, instanceId, scope);
|
||||||
setTemplates(Array.isArray(result) ? result : result.items);
|
setTemplates(Array.isArray(result) ? result : result.items);
|
||||||
} catch {
|
} catch {
|
||||||
setTemplates([]);
|
setTemplates([]);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
export { Automation2FlowEditor, Automation2FlowEditor as FlowEditor } from './editor/Automation2FlowEditor';
|
||||||
// All rights reserved.
|
|
||||||
export { WorkflowFlowEditor, WorkflowFlowEditor as FlowEditor } from './editor/WorkflowFlowEditor';
|
|
||||||
export type { PendingFile, EditorDataSource, EditorFeatureDataSource } from './editor/EditorChatPanel';
|
export type { PendingFile, EditorDataSource, EditorFeatureDataSource } from './editor/EditorChatPanel';
|
||||||
export { FlowCanvas, STICKY_NOTE_PALETTE, STICKY_NOTE_DEFAULT_COLOR_ID, STICKY_NOTE_DEFAULT_HEIGHT, getStickyNotePaletteEntry } from './editor/FlowCanvas';
|
export { FlowCanvas, STICKY_NOTE_PALETTE, STICKY_NOTE_DEFAULT_COLOR_ID, STICKY_NOTE_DEFAULT_HEIGHT, getStickyNotePaletteEntry } from './editor/FlowCanvas';
|
||||||
export type { CanvasNode, CanvasConnection, CanvasStickyNote, FlowCanvasHandle, FlowCanvasViewportEditState } from './editor/FlowCanvas';
|
export type { CanvasNode, CanvasConnection, CanvasStickyNote, FlowCanvasHandle, FlowCanvasViewportEditState } from './editor/FlowCanvas';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* One text field per option — the text the end user sees in the dropdown.
|
* One text field per option — the text the end user sees in the dropdown.
|
||||||
* Stored as { value, label } with the same string so payload and UI stay in sync.
|
* Stored as { value, label } with the same string so payload and UI stay in sync.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* Form node config - draggable fields, types, required toggle
|
* Form node config - draggable fields, types, required toggle
|
||||||
*/
|
*/
|
||||||
|
|
@ -8,8 +6,8 @@ import React from 'react';
|
||||||
import { FaGripVertical, FaTimes } from 'react-icons/fa';
|
import { FaGripVertical, FaTimes } from 'react-icons/fa';
|
||||||
import type { FormField, NodeConfigRendererProps } from '../shared/types';
|
import type { FormField, NodeConfigRendererProps } from '../shared/types';
|
||||||
import { FORM_FIELD_TYPES, FORM_FIELD_TYPE_LABELS } from '../../../../utils/attributeTypeMapper';
|
import { FORM_FIELD_TYPES, FORM_FIELD_TYPE_LABELS } from '../../../../utils/attributeTypeMapper';
|
||||||
import styles from '../../editor/WorkflowFlowEditor.module.css';
|
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||||
import { useWorkflowDataFlow } from '../../context/WorkflowDataFlowContext';
|
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||||
import { FormFieldOptionsEditor } from './FormFieldOptionsEditor';
|
import { FormFieldOptionsEditor } from './FormFieldOptionsEditor';
|
||||||
import {
|
import {
|
||||||
deriveFormFieldPayloadKey,
|
deriveFormFieldPayloadKey,
|
||||||
|
|
@ -21,7 +19,7 @@ import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const ctx = useWorkflowDataFlow();
|
const ctx = useAutomation2DataFlow();
|
||||||
const fieldTypeOptions = ctx?.formFieldTypes?.length
|
const fieldTypeOptions = ctx?.formFieldTypes?.length
|
||||||
? ctx.formFieldTypes
|
? ctx.formFieldTypes
|
||||||
: FORM_FIELD_TYPES.map((ft) => ({ id: ft, label: FORM_FIELD_TYPE_LABELS[ft] ?? ft, portType: 'str' }));
|
: FORM_FIELD_TYPES.map((ft) => ({ id: ft, label: FORM_FIELD_TYPE_LABELS[ft] ?? ft, portType: 'str' }));
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* Helpers for optional select/multiselect rows on workflow form field definitions.
|
* Helpers for optional select/multiselect rows on workflow form field definitions.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
export { FormNodeConfig } from './FormNodeConfig';
|
export { FormNodeConfig } from './FormNodeConfig';
|
||||||
export { FormFieldOptionsEditor } from './FormFieldOptionsEditor';
|
export { FormFieldOptionsEditor } from './FormFieldOptionsEditor';
|
||||||
export type { FormFieldOptionRow } from './formFieldOptionsUtils';
|
export type { FormFieldOptionRow } from './formFieldOptionsUtils';
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* Backend-driven case list for flow.switch (depends on value dataRef).
|
* Backend-driven case list for flow.switch (depends on value dataRef).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { FieldRendererProps } from './index';
|
import type { FieldRendererProps } from './index';
|
||||||
import { useWorkflowDataFlow } from '../../context/WorkflowDataFlowContext';
|
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||||
import { isRef, type DataRef } from '../shared/dataRef';
|
import { isRef, type DataRef } from '../shared/dataRef';
|
||||||
import { toApiGraph } from '../shared/graphUtils';
|
import { toApiGraph } from '../shared/graphUtils';
|
||||||
import { fetchConditionMeta, type ConditionOperatorDef } from '../../../../api/workflowAutomationApi';
|
import { fetchConditionMeta, type ConditionOperatorDef } from '../../../../api/workflowApi';
|
||||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
export interface SwitchCase {
|
export interface SwitchCase {
|
||||||
|
|
@ -118,7 +116,7 @@ export const CaseListEditor: React.FC<FieldRendererProps> = ({
|
||||||
allParams,
|
allParams,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const dataFlow = useWorkflowDataFlow();
|
const dataFlow = useAutomation2DataFlow();
|
||||||
const dependsOn =
|
const dependsOn =
|
||||||
param.frontendOptions && typeof param.frontendOptions === 'object'
|
param.frontendOptions && typeof param.frontendOptions === 'object'
|
||||||
? String((param.frontendOptions as Record<string, unknown>).dependsOn ?? 'value')
|
? String((param.frontendOptions as Record<string, unknown>).dependsOn ?? 'value')
|
||||||
|
|
@ -159,7 +157,7 @@ export const CaseListEditor: React.FC<FieldRendererProps> = ({
|
||||||
|
|
||||||
if (dataFlow?.instanceId && dataFlow.request) {
|
if (dataFlow?.instanceId && dataFlow.request) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetchConditionMeta(dataFlow.request, {
|
fetchConditionMeta(dataFlow.request, dataFlow.instanceId, {
|
||||||
graph: toApiGraph(dataFlow.nodes, dataFlow.connections),
|
graph: toApiGraph(dataFlow.nodes, dataFlow.connections),
|
||||||
nodeId: dataFlow.currentNodeId,
|
nodeId: dataFlow.currentNodeId,
|
||||||
ref: { type: 'ref', nodeId: ref.nodeId, path: ref.path },
|
ref: { type: 'ref', nodeId: ref.nodeId, path: ref.path },
|
||||||
|
|
|
||||||
|
|
@ -1,395 +0,0 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
|
||||||
* clickupList — hierarchical ClickUp list picker via connector browse API.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
|
||||||
import { fetchBrowse, type BrowseEntry } from '../../../../api/workflowAutomationApi';
|
|
||||||
import { fetchClickupList } from '../../../../api/clickupApi';
|
|
||||||
import type { FieldRendererProps } from './index';
|
|
||||||
import {
|
|
||||||
clickupBrowseParentPath,
|
|
||||||
formatListPickerValue,
|
|
||||||
isClickupContainerEntry,
|
|
||||||
isClickupListEntry,
|
|
||||||
parseClickupListPath,
|
|
||||||
resolveListPathFromValue,
|
|
||||||
} from './clickupPathUtils';
|
|
||||||
|
|
||||||
const CLICKUP_PURPLE = '#7B68EE';
|
|
||||||
|
|
||||||
const glassPanel: React.CSSProperties = {
|
|
||||||
marginTop: 6,
|
|
||||||
borderRadius: 10,
|
|
||||||
border: '1px solid rgba(123, 104, 238, 0.35)',
|
|
||||||
background: 'rgba(255, 255, 255, 0.72)',
|
|
||||||
backdropFilter: 'blur(10px)',
|
|
||||||
WebkitBackdropFilter: 'blur(10px)',
|
|
||||||
boxShadow:
|
|
||||||
'0 4px 24px rgba(123, 104, 238, 0.12), 0 0 0 1px rgba(255, 255, 255, 0.5) inset',
|
|
||||||
padding: 8,
|
|
||||||
};
|
|
||||||
|
|
||||||
const glassTrigger: React.CSSProperties = {
|
|
||||||
display: 'flex',
|
|
||||||
width: '100%',
|
|
||||||
alignItems: 'stretch',
|
|
||||||
borderRadius: 8,
|
|
||||||
border: '1px solid rgba(123, 104, 238, 0.4)',
|
|
||||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(123,104,238,0.08) 100%)',
|
|
||||||
boxShadow: '0 0 12px rgba(123, 104, 238, 0.15)',
|
|
||||||
overflow: 'hidden',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ClickUpListPicker: React.FC<FieldRendererProps> = ({
|
|
||||||
param,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
allParams,
|
|
||||||
instanceId,
|
|
||||||
request,
|
|
||||||
onPatchParams,
|
|
||||||
nodeType,
|
|
||||||
}) => {
|
|
||||||
const { t } = useLanguage();
|
|
||||||
const dependsOn = (param.frontendOptions?.dependsOn as string | undefined) || 'connectionReference';
|
|
||||||
const connectionReference = (allParams?.[dependsOn] as string | undefined) || '';
|
|
||||||
const hasConnection = !!connectionReference && typeof connectionReference === 'string';
|
|
||||||
|
|
||||||
const [panelOpen, setPanelOpen] = useState(false);
|
|
||||||
const [browsePath, setBrowsePath] = useState('/');
|
|
||||||
const [items, setItems] = useState<BrowseEntry[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [pickedLabel, setPickedLabel] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const strVal = typeof value === 'string' ? value : value != null ? String(value) : '';
|
|
||||||
|
|
||||||
const loadBrowse = useCallback(
|
|
||||||
async (path: string) => {
|
|
||||||
if (!request || !instanceId || !connectionReference) return;
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const res = await fetchBrowse(request, connectionReference, 'clickup', path);
|
|
||||||
setItems(res.items);
|
|
||||||
setBrowsePath(res.path || path);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
setItems([]);
|
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[request, instanceId, connectionReference],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!panelOpen || !hasConnection) return;
|
|
||||||
void loadBrowse(browsePath);
|
|
||||||
}, [panelOpen, hasConnection, browsePath, loadBrowse]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!strVal) {
|
|
||||||
setPickedLabel(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pathFromVal = resolveListPathFromValue(strVal, param.name);
|
|
||||||
if (pathFromVal) {
|
|
||||||
const parsed = parseClickupListPath(pathFromVal);
|
|
||||||
if (parsed.listId && request && connectionReference) {
|
|
||||||
let cancelled = false;
|
|
||||||
fetchClickupList(request, connectionReference, parsed.listId)
|
|
||||||
.then((data) => {
|
|
||||||
if (cancelled) return;
|
|
||||||
const name = typeof data.name === 'string' ? data.name : null;
|
|
||||||
setPickedLabel(name || parsed.listId || strVal);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) setPickedLabel(parsed.listId || strVal);
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
setPickedLabel(parsed.listId || strVal);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (param.name === 'listId' && strVal && request && connectionReference) {
|
|
||||||
let cancelled = false;
|
|
||||||
fetchClickupList(request, connectionReference, strVal)
|
|
||||||
.then((data) => {
|
|
||||||
if (cancelled) return;
|
|
||||||
setPickedLabel(typeof data.name === 'string' ? data.name : strVal);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) setPickedLabel(strVal);
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
setPickedLabel(strVal);
|
|
||||||
}, [strVal, param.name, request, connectionReference]);
|
|
||||||
|
|
||||||
const shouldPatchTeamId =
|
|
||||||
nodeType === 'clickup.searchTasks' || Object.prototype.hasOwnProperty.call(allParams ?? {}, 'teamId');
|
|
||||||
|
|
||||||
const selectList = useCallback(
|
|
||||||
(entry: BrowseEntry) => {
|
|
||||||
const listPath = entry.path;
|
|
||||||
const stored = formatListPickerValue(listPath, param.name);
|
|
||||||
const { teamId, listId } = parseClickupListPath(listPath);
|
|
||||||
|
|
||||||
if (shouldPatchTeamId && onPatchParams && teamId) {
|
|
||||||
const patch: Record<string, unknown> = { [param.name]: stored };
|
|
||||||
patch.teamId = teamId;
|
|
||||||
onPatchParams(patch);
|
|
||||||
} else {
|
|
||||||
onChange(stored);
|
|
||||||
}
|
|
||||||
|
|
||||||
setPickedLabel(entry.name || listId || stored);
|
|
||||||
setPanelOpen(false);
|
|
||||||
},
|
|
||||||
[param.name, shouldPatchTeamId, onPatchParams, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const navigateInto = useCallback((entry: BrowseEntry) => {
|
|
||||||
if (!isClickupContainerEntry(entry.metadata, entry.isFolder)) return;
|
|
||||||
setBrowsePath(entry.path);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const goUp = useCallback(() => {
|
|
||||||
setBrowsePath((p) => clickupBrowseParentPath(p));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const clearSelection = useCallback(() => {
|
|
||||||
if (shouldPatchTeamId && onPatchParams) {
|
|
||||||
const patch: Record<string, unknown> = { [param.name]: '' };
|
|
||||||
if (nodeType === 'clickup.searchTasks') {
|
|
||||||
patch.teamId = '';
|
|
||||||
}
|
|
||||||
onPatchParams(patch);
|
|
||||||
} else {
|
|
||||||
onChange('');
|
|
||||||
}
|
|
||||||
setPickedLabel(null);
|
|
||||||
}, [shouldPatchTeamId, onPatchParams, onChange, param.name, nodeType]);
|
|
||||||
|
|
||||||
const triggerLabel = strVal
|
|
||||||
? pickedLabel ?? '…'
|
|
||||||
: t('ClickUp-Liste wählen');
|
|
||||||
|
|
||||||
const breadcrumb =
|
|
||||||
browsePath === '/'
|
|
||||||
? t('Workspaces')
|
|
||||||
: browsePath.replace(/^\/team\//, '').replace(/\//g, ' › ');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ marginBottom: 8 }}>
|
|
||||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>
|
|
||||||
{param.description || param.name}
|
|
||||||
</label>
|
|
||||||
{!request || !instanceId ? (
|
|
||||||
<div style={{ fontSize: 11, color: '#888' }}>
|
|
||||||
{t('Listen-Browser nicht verfügbar (keine API-Anbindung).')}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div style={{ ...glassTrigger, opacity: hasConnection ? 1 : 0.55 }}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!hasConnection}
|
|
||||||
onClick={() => {
|
|
||||||
if (!hasConnection) return;
|
|
||||||
setPanelOpen((o) => {
|
|
||||||
if (!o) setBrowsePath('/');
|
|
||||||
return !o;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
gap: 8,
|
|
||||||
minWidth: 0,
|
|
||||||
padding: '8px 10px',
|
|
||||||
border: 'none',
|
|
||||||
background: 'transparent',
|
|
||||||
cursor: hasConnection ? 'pointer' : 'not-allowed',
|
|
||||||
fontSize: 12,
|
|
||||||
textAlign: 'left',
|
|
||||||
color: 'var(--text-primary, #334155)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 }}>
|
|
||||||
{hasConnection ? triggerLabel : t('Zuerst {field} wählen', { field: dependsOn })}
|
|
||||||
</span>
|
|
||||||
<span aria-hidden style={{ flexShrink: 0, fontSize: 10, opacity: 0.65 }}>
|
|
||||||
{panelOpen ? '▾' : '▸'}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{strVal ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title={t('Auswahl aufheben')}
|
|
||||||
aria-label={t('Auswahl aufheben')}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
clearSelection();
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
flexShrink: 0,
|
|
||||||
width: 36,
|
|
||||||
border: 'none',
|
|
||||||
borderLeft: '1px solid rgba(123, 104, 238, 0.25)',
|
|
||||||
background: 'transparent',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: 16,
|
|
||||||
lineHeight: 1,
|
|
||||||
color: 'var(--text-secondary, #64748b)',
|
|
||||||
padding: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{panelOpen && hasConnection && (
|
|
||||||
<div style={glassPanel}>
|
|
||||||
{error && (
|
|
||||||
<div style={{ fontSize: 11, color: '#c00', marginBottom: 6 }}>{error}</div>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
gap: 6,
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 6,
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{browsePath !== '/' && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={goUp}
|
|
||||||
style={{
|
|
||||||
padding: '3px 8px',
|
|
||||||
borderRadius: 6,
|
|
||||||
border: '1px solid rgba(123, 104, 238, 0.35)',
|
|
||||||
background: 'rgba(255,255,255,0.6)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: 11,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
↑ {t('Hoch')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => loadBrowse(browsePath)}
|
|
||||||
title={t('Neu laden')}
|
|
||||||
style={{
|
|
||||||
padding: '3px 8px',
|
|
||||||
borderRadius: 6,
|
|
||||||
border: '1px solid rgba(123, 104, 238, 0.35)',
|
|
||||||
background: 'rgba(255,255,255,0.6)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: 11,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
↻
|
|
||||||
</button>
|
|
||||||
<span style={{ fontSize: 11, color: '#555', flex: 1, minWidth: 0 }}>
|
|
||||||
{breadcrumb}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
maxHeight: 220,
|
|
||||||
overflow: 'auto',
|
|
||||||
borderRadius: 6,
|
|
||||||
border: '1px solid rgba(123, 104, 238, 0.2)',
|
|
||||||
background: 'rgba(255, 255, 255, 0.85)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loading && (
|
|
||||||
<div style={{ padding: 8, fontSize: 11, color: '#888' }}>{t('Lade')}</div>
|
|
||||||
)}
|
|
||||||
{!loading && items.length === 0 && (
|
|
||||||
<div style={{ padding: 8, fontSize: 11, color: '#888' }}>
|
|
||||||
{t('Keine Einträge')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!loading &&
|
|
||||||
items.map((item) => {
|
|
||||||
const isList = isClickupListEntry(item.metadata);
|
|
||||||
const canNavigate = isClickupContainerEntry(item.metadata, item.isFolder);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.path}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '4px 8px',
|
|
||||||
borderBottom: '1px solid rgba(123, 104, 238, 0.08)',
|
|
||||||
fontSize: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => {
|
|
||||||
if (isList) selectList(item);
|
|
||||||
else if (canNavigate) navigateInto(item);
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
||||||
e.preventDefault();
|
|
||||||
if (isList) selectList(item);
|
|
||||||
else if (canNavigate) navigateInto(item);
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
cursor: isList || canNavigate ? 'pointer' : 'default',
|
|
||||||
userSelect: 'none',
|
|
||||||
}}
|
|
||||||
title={isList ? t('Liste wählen') : canNavigate ? t('Öffnen') : undefined}
|
|
||||||
>
|
|
||||||
{isList ? '📋' : canNavigate ? '📁' : '·'} {item.name}
|
|
||||||
</span>
|
|
||||||
{isList && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => selectList(item)}
|
|
||||||
style={{
|
|
||||||
padding: '2px 8px',
|
|
||||||
borderRadius: 6,
|
|
||||||
border: `1px solid ${CLICKUP_PURPLE}`,
|
|
||||||
background: CLICKUP_PURPLE,
|
|
||||||
color: '#fff',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: 11,
|
|
||||||
boxShadow: '0 0 8px rgba(123, 104, 238, 0.45)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Wählen')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* Backend-driven condition editor for flow.ifElse (depends on Item dataRef).
|
* Backend-driven condition editor for flow.ifElse (depends on Item dataRef).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { FieldRendererProps } from './index';
|
import type { FieldRendererProps } from './index';
|
||||||
import { useWorkflowDataFlow } from '../../context/WorkflowDataFlowContext';
|
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||||
import { isRef, type DataRef } from '../shared/dataRef';
|
import { isRef, type DataRef } from '../shared/dataRef';
|
||||||
import { toApiGraph } from '../shared/graphUtils';
|
import { toApiGraph } from '../shared/graphUtils';
|
||||||
import { fetchConditionMeta, type ConditionOperatorDef } from '../../../../api/workflowAutomationApi';
|
import { fetchConditionMeta, type ConditionOperatorDef } from '../../../../api/workflowApi';
|
||||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
export interface StructuredCondition {
|
export interface StructuredCondition {
|
||||||
|
|
@ -43,7 +41,7 @@ export const ConditionEditor: React.FC<FieldRendererProps> = ({
|
||||||
allParams,
|
allParams,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const dataFlow = useWorkflowDataFlow();
|
const dataFlow = useAutomation2DataFlow();
|
||||||
const dependsOn =
|
const dependsOn =
|
||||||
param.frontendOptions && typeof param.frontendOptions === 'object'
|
param.frontendOptions && typeof param.frontendOptions === 'object'
|
||||||
? String((param.frontendOptions as Record<string, unknown>).dependsOn ?? 'Item')
|
? String((param.frontendOptions as Record<string, unknown>).dependsOn ?? 'Item')
|
||||||
|
|
@ -85,7 +83,7 @@ export const ConditionEditor: React.FC<FieldRendererProps> = ({
|
||||||
|
|
||||||
if (dataFlow?.instanceId && dataFlow.request) {
|
if (dataFlow?.instanceId && dataFlow.request) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetchConditionMeta(dataFlow.request, {
|
fetchConditionMeta(dataFlow.request, dataFlow.instanceId, {
|
||||||
graph: toApiGraph(dataFlow.nodes, dataFlow.connections),
|
graph: toApiGraph(dataFlow.nodes, dataFlow.connections),
|
||||||
nodeId: dataFlow.currentNodeId,
|
nodeId: dataFlow.currentNodeId,
|
||||||
ref: { type: 'ref', nodeId: ref.nodeId, path: ref.path },
|
ref: { type: 'ref', nodeId: ref.nodeId, path: ref.path },
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* One place to configure context.setContext rows: target key, then either
|
* One place to configure context.setContext rows: target key, then either
|
||||||
* upstream picker, a fixed literal, or a human task.
|
* upstream picker, a fixed literal, or a human task.
|
||||||
|
|
@ -7,7 +5,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||||
import { useWorkflowDataFlow } from '../../context/WorkflowDataFlowContext';
|
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||||
import { DataPicker } from '../shared/DataPicker';
|
import { DataPicker } from '../shared/DataPicker';
|
||||||
import { isRef, isSystemVar, type DataRef, type SystemVarRef } from '../shared/dataRef';
|
import { isRef, isSystemVar, type DataRef, type SystemVarRef } from '../shared/dataRef';
|
||||||
import type { FieldRendererProps } from './index';
|
import type { FieldRendererProps } from './index';
|
||||||
|
|
@ -176,7 +174,7 @@ const REMOVE_BTN: React.CSSProperties = {
|
||||||
|
|
||||||
export const ContextAssignmentsEditor: React.FC<FieldRendererProps> = ({ param, value, onChange, allParams }) => {
|
export const ContextAssignmentsEditor: React.FC<FieldRendererProps> = ({ param, value, onChange, allParams }) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const dataFlow = useWorkflowDataFlow();
|
const dataFlow = useAutomation2DataFlow();
|
||||||
const rows = normalizeRows(value, allParams);
|
const rows = normalizeRows(value, allParams);
|
||||||
const [pickerRow, setPickerRow] = React.useState<number | null>(null);
|
const [pickerRow, setPickerRow] = React.useState<number | null>(null);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* ContextBuilderRenderer — multi-select context binding for AI nodes.
|
* ContextBuilderRenderer — multi-select context binding for AI nodes.
|
||||||
*
|
*
|
||||||
|
|
@ -13,7 +11,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||||
import { useWorkflowDataFlow } from '../../context/WorkflowDataFlowContext';
|
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||||
import { DataPicker } from '../shared/DataPicker';
|
import { DataPicker } from '../shared/DataPicker';
|
||||||
import { isRef, type DataRef, type SystemVarRef } from '../shared/dataRef';
|
import { isRef, type DataRef, type SystemVarRef } from '../shared/dataRef';
|
||||||
import type { FieldRendererProps } from './index';
|
import type { FieldRendererProps } from './index';
|
||||||
|
|
@ -54,7 +52,7 @@ const REMOVE_BTN: React.CSSProperties = {
|
||||||
|
|
||||||
export const ContextBuilderRenderer: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
export const ContextBuilderRenderer: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const dataFlow = useWorkflowDataFlow();
|
const dataFlow = useAutomation2DataFlow();
|
||||||
const [pickerOpen, setPickerOpen] = React.useState(false);
|
const [pickerOpen, setPickerOpen] = React.useState(false);
|
||||||
const dragIndex = React.useRef<number | null>(null);
|
const dragIndex = React.useRef<number | null>(null);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* DataRefRenderer — Pick-not-Push attribute binding using the existing
|
* DataRefRenderer — Pick-not-Push attribute binding using the existing
|
||||||
* hierarchical DataPicker.
|
* hierarchical DataPicker.
|
||||||
|
|
@ -12,14 +10,14 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||||
import { useWorkflowDataFlow } from '../../context/WorkflowDataFlowContext';
|
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||||
import { DataPicker } from '../shared/DataPicker';
|
import { DataPicker } from '../shared/DataPicker';
|
||||||
import { isRef, type DataRef, type SystemVarRef } from '../shared/dataRef';
|
import { isRef, type DataRef, type SystemVarRef } from '../shared/dataRef';
|
||||||
import type { FieldRendererProps } from './index';
|
import type { FieldRendererProps } from './index';
|
||||||
|
|
||||||
export const DataRefRenderer: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
export const DataRefRenderer: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const dataFlow = useWorkflowDataFlow();
|
const dataFlow = useAutomation2DataFlow();
|
||||||
const [pickerOpen, setPickerOpen] = React.useState(false);
|
const [pickerOpen, setPickerOpen] = React.useState(false);
|
||||||
|
|
||||||
const currentRef = isRef(value) ? (value as DataRef) : null;
|
const currentRef = isRef(value) ? (value as DataRef) : null;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* FeatureInstancePicker — renderer for frontendType="featureInstance".
|
* FeatureInstancePicker — renderer for frontendType="featureInstance".
|
||||||
*
|
*
|
||||||
* Modeled on ConnectionPicker. Loads mandate-scoped FeatureInstances filtered
|
* Modeled on ConnectionPicker. Loads mandate-scoped FeatureInstances filtered
|
||||||
* by `frontendOptions.featureCode` (e.g. "trustee", "redmine") via
|
* by `frontendOptions.featureCode` (e.g. "trustee", "redmine") via
|
||||||
* GET /api/workflow-automation/options/feature.instance?featureCode=<code>
|
* GET /api/workflows/{instanceId}/options/feature.instance?featureCode=<code>
|
||||||
*
|
*
|
||||||
* Behavior matches the rest of the editor:
|
* Behavior matches the rest of the editor:
|
||||||
* - 0 results -> hint to create a feature instance for this mandate
|
* - 0 results -> hint to create a feature instance for this mandate
|
||||||
|
|
@ -44,7 +42,7 @@ export const FeatureInstancePicker: React.FC<FieldRendererProps> = ({
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
request({
|
request({
|
||||||
url: `/api/workflow-automation/options/feature.instance?featureCode=${encodeURIComponent(featureCode)}`,
|
url: `/api/workflows/${instanceId}/options/feature.instance?featureCode=${encodeURIComponent(featureCode)}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
})
|
})
|
||||||
.then((res: unknown) => {
|
.then((res: unknown) => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* TemplateTextarea — Freitext mit eingebetteten {{nodeId.path}} Tokens.
|
* TemplateTextarea — Freitext mit eingebetteten {{nodeId.path}} Tokens.
|
||||||
* Tokens werden zur Laufzeit von resolveParameterReferences aufgeloest (Gateway).
|
* Tokens werden zur Laufzeit von resolveParameterReferences aufgeloest (Gateway).
|
||||||
|
|
@ -7,11 +5,11 @@
|
||||||
|
|
||||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import type { FieldRendererProps } from './index';
|
import type { FieldRendererProps } from './index';
|
||||||
import { useWorkflowDataFlow } from '../../context/WorkflowDataFlowContext';
|
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||||
import { DataPicker } from '../shared/DataPicker';
|
import { DataPicker } from '../shared/DataPicker';
|
||||||
import { formatRefLabel, isRef, isSystemVar, type DataRef, type SystemVarRef } from '../shared/dataRef';
|
import { formatRefLabel, isRef, isSystemVar, type DataRef, type SystemVarRef } from '../shared/dataRef';
|
||||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||||
import styles from '../../editor/WorkflowFlowEditor.module.css';
|
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||||
|
|
||||||
const _TEMPLATE_TOKEN_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
|
const _TEMPLATE_TOKEN_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
|
||||||
|
|
||||||
|
|
@ -62,7 +60,7 @@ function _parseTokensInTemplate(
|
||||||
|
|
||||||
export const TemplateTextareaRenderer: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
export const TemplateTextareaRenderer: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const dataFlow = useWorkflowDataFlow();
|
const dataFlow = useAutomation2DataFlow();
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const [pickerOpen, setPickerOpen] = useState(false);
|
const [pickerOpen, setPickerOpen] = useState(false);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* userFileFolder — FormGeneratorTree embedded: combobox-style trigger + expandable tree.
|
* userFileFolder — FormGeneratorTree embedded: combobox-style trigger + expandable tree.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import {
|
|
||||||
clickupBrowseParentPath,
|
|
||||||
formatListPickerValue,
|
|
||||||
parseClickupListPath,
|
|
||||||
} from './clickupPathUtils';
|
|
||||||
|
|
||||||
describe('clickupPathUtils', () => {
|
|
||||||
it('parseClickupListPath extracts team and list ids', () => {
|
|
||||||
expect(parseClickupListPath('/team/abc/list/xyz')).toEqual({
|
|
||||||
teamId: 'abc',
|
|
||||||
listId: 'xyz',
|
|
||||||
});
|
|
||||||
expect(parseClickupListPath('')).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('formatListPickerValue stores path or listId by param name', () => {
|
|
||||||
const path = '/team/abc/list/xyz';
|
|
||||||
expect(formatListPickerValue(path, 'pathQuery')).toBe(path);
|
|
||||||
expect(formatListPickerValue(path, 'listId')).toBe('xyz');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clickupBrowseParentPath walks up hierarchy', () => {
|
|
||||||
expect(clickupBrowseParentPath('/')).toBe('/');
|
|
||||||
expect(clickupBrowseParentPath('/team/t1')).toBe('/');
|
|
||||||
expect(clickupBrowseParentPath('/team/t1/space/s1')).toBe('/team/t1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/** Parse virtual ClickUp list paths: /team/{teamId}/list/{listId} */
|
|
||||||
|
|
||||||
const LIST_PATH_RE = /^\/team\/([^/]+)\/list\/([^/]+)$/;
|
|
||||||
|
|
||||||
export function parseClickupListPath(path: string): { teamId?: string; listId?: string } {
|
|
||||||
const p = (path || '').trim();
|
|
||||||
const m = p.match(LIST_PATH_RE);
|
|
||||||
if (!m) return {};
|
|
||||||
return { teamId: m[1], listId: m[2] };
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Store path for pathQuery; raw list id for listId param. */
|
|
||||||
export function formatListPickerValue(listPath: string, paramName: string): string {
|
|
||||||
const { listId } = parseClickupListPath(listPath);
|
|
||||||
if (paramName === 'listId' && listId) return listId;
|
|
||||||
return listPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Resolve list path from stored value (path or legacy raw id). */
|
|
||||||
export function resolveListPathFromValue(value: string, paramName: string): string | null {
|
|
||||||
const v = (value || '').trim();
|
|
||||||
if (!v) return null;
|
|
||||||
if (LIST_PATH_RE.test(v)) return v;
|
|
||||||
if (paramName === 'listId' && /^[a-zA-Z0-9_-]+$/.test(v)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clickupBrowseParentPath(path: string): string {
|
|
||||||
const p = (path || '/').trim() || '/';
|
|
||||||
if (p === '/') return '/';
|
|
||||||
const folder = p.match(/^(\/team\/[^/]+\/space\/[^/]+)\/folder\/[^/]+$/);
|
|
||||||
if (folder) return folder[1];
|
|
||||||
const space = p.match(/^(\/team\/[^/]+)\/space\/[^/]+$/);
|
|
||||||
if (space) return space[1];
|
|
||||||
if (/^\/team\/[^/]+$/.test(p)) return '/';
|
|
||||||
const parts = p.split('/').filter(Boolean);
|
|
||||||
if (parts.length <= 1) return '/';
|
|
||||||
parts.pop();
|
|
||||||
return `/${parts.join('/')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cuTypeFromEntry(metadata?: Record<string, unknown>): string {
|
|
||||||
const t = metadata?.cuType;
|
|
||||||
return typeof t === 'string' ? t : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isClickupListEntry(metadata?: Record<string, unknown>): boolean {
|
|
||||||
return cuTypeFromEntry(metadata) === 'list';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isClickupContainerEntry(
|
|
||||||
metadata: Record<string, unknown> | undefined,
|
|
||||||
isFolder: boolean,
|
|
||||||
): boolean {
|
|
||||||
const cu = cuTypeFromEntry(metadata);
|
|
||||||
if (cu === 'list') return false;
|
|
||||||
if (cu === 'team' || cu === 'space' || cu === 'folder') return true;
|
|
||||||
return isFolder && cu !== 'task';
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* Generic FrontendType renderer registry.
|
* Generic FrontendType renderer registry.
|
||||||
* Maps frontendType strings to React components.
|
* Maps frontendType strings to React components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ComponentType } from 'react';
|
import type { ComponentType } from 'react';
|
||||||
import type { NodeTypeParameter } from '../../../../api/workflowAutomationApi';
|
import type { NodeTypeParameter } from '../../../../api/workflowApi';
|
||||||
import type { ApiRequestFunction } from '../../../../api/workflowAutomationApi';
|
import type { ApiRequestFunction } from '../../../../api/workflowApi';
|
||||||
import { FORM_FIELD_TYPES, FORM_FIELD_TYPE_LABELS } from '../../../../utils/attributeTypeMapper';
|
import { FORM_FIELD_TYPES, FORM_FIELD_TYPE_LABELS } from '../../../../utils/attributeTypeMapper';
|
||||||
import { useWorkflowDataFlow } from '../../context/WorkflowDataFlowContext';
|
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||||
import { FormFieldOptionsEditor } from '../form/FormFieldOptionsEditor';
|
import { FormFieldOptionsEditor } from '../form/FormFieldOptionsEditor';
|
||||||
import {
|
import {
|
||||||
deriveFormFieldPayloadKey,
|
deriveFormFieldPayloadKey,
|
||||||
|
|
@ -48,14 +46,13 @@ import {
|
||||||
|
|
||||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||||
import { toApiGraph } from '../shared/graphUtils';
|
import { toApiGraph } from '../shared/graphUtils';
|
||||||
import { postUpstreamPaths } from '../../../../api/workflowAutomationApi';
|
import { postUpstreamPaths } from '../../../../api/workflowApi';
|
||||||
import type { CanvasNode } from '../../editor/FlowCanvas';
|
import type { CanvasNode } from '../../editor/FlowCanvas';
|
||||||
import { DataRefRenderer } from './DataRefRenderer';
|
import { DataRefRenderer } from './DataRefRenderer';
|
||||||
import { ContextBuilderRenderer } from './ContextBuilderRenderer';
|
import { ContextBuilderRenderer } from './ContextBuilderRenderer';
|
||||||
import { ContextAssignmentsEditor } from './ContextAssignmentsEditor';
|
import { ContextAssignmentsEditor } from './ContextAssignmentsEditor';
|
||||||
import { FeatureInstancePicker } from './FeatureInstancePicker';
|
import { FeatureInstancePicker } from './FeatureInstancePicker';
|
||||||
import { UserFileFolderPicker } from './UserFileFolderPicker';
|
import { UserFileFolderPicker } from './UserFileFolderPicker';
|
||||||
import { ClickUpListPicker } from './ClickUpListPicker';
|
|
||||||
import { ConditionEditor } from './ConditionEditor';
|
import { ConditionEditor } from './ConditionEditor';
|
||||||
import { CaseListEditor } from './CaseListEditor';
|
import { CaseListEditor } from './CaseListEditor';
|
||||||
import { TemplateTextareaRenderer } from './TemplateTextareaRenderer';
|
import { TemplateTextareaRenderer } from './TemplateTextareaRenderer';
|
||||||
|
|
@ -302,7 +299,7 @@ const HiddenInput: React.FC<FieldRendererProps> = () => null;
|
||||||
|
|
||||||
const ConnectionPicker: React.FC<FieldRendererProps> = ({ param, value, onChange, instanceId, request }) => {
|
const ConnectionPicker: React.FC<FieldRendererProps> = ({ param, value, onChange, instanceId, request }) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const dataFlow = useWorkflowDataFlow();
|
const dataFlow = useAutomation2DataFlow();
|
||||||
const [connections, setConnections] = React.useState<Array<{ id: string; label: string }>>([]);
|
const [connections, setConnections] = React.useState<Array<{ id: string; label: string }>>([]);
|
||||||
const [loadError, setLoadError] = React.useState<string | null>(null);
|
const [loadError, setLoadError] = React.useState<string | null>(null);
|
||||||
const [upstreamBindOptions, setUpstreamBindOptions] = React.useState<Array<{ key: string; label: string; ref: unknown }>>([]);
|
const [upstreamBindOptions, setUpstreamBindOptions] = React.useState<Array<{ key: string; label: string; ref: unknown }>>([]);
|
||||||
|
|
@ -312,7 +309,7 @@ const ConnectionPicker: React.FC<FieldRendererProps> = ({ param, value, onChange
|
||||||
if (!instanceId || !request) return;
|
if (!instanceId || !request) return;
|
||||||
const qs = authority ? `?authority=${encodeURIComponent(authority)}` : '';
|
const qs = authority ? `?authority=${encodeURIComponent(authority)}` : '';
|
||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
request({ url: `/api/workflow-automation/options/user.connection${qs}`, method: 'get' })
|
request({ url: `/api/workflows/${instanceId}/options/user.connection${qs}`, method: 'get' })
|
||||||
.then((res: unknown) => {
|
.then((res: unknown) => {
|
||||||
const data = res as { options?: Array<{ value: string; label: string }> };
|
const data = res as { options?: Array<{ value: string; label: string }> };
|
||||||
setConnections((data?.options || []).map((o) => ({ id: o.value, label: o.label })));
|
setConnections((data?.options || []).map((o) => ({ id: o.value, label: o.label })));
|
||||||
|
|
@ -330,7 +327,7 @@ const ConnectionPicker: React.FC<FieldRendererProps> = ({ param, value, onChange
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const graph = toApiGraph(dataFlow.nodes as CanvasNode[], dataFlow.connections);
|
const graph = toApiGraph(dataFlow.nodes as CanvasNode[], dataFlow.connections);
|
||||||
postUpstreamPaths(request, graph, dataFlow.currentNodeId)
|
postUpstreamPaths(request, instanceId, graph, dataFlow.currentNodeId)
|
||||||
.then(({ paths }) => {
|
.then(({ paths }) => {
|
||||||
const opts = paths
|
const opts = paths
|
||||||
.filter(
|
.filter(
|
||||||
|
|
@ -645,7 +642,7 @@ const SharepointPathPicker: React.FC<FieldRendererProps> = ({ param, value, onCh
|
||||||
|
|
||||||
const FieldBuilderEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
const FieldBuilderEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const ctx = useWorkflowDataFlow();
|
const ctx = useAutomation2DataFlow();
|
||||||
const fieldTypeOptions = ctx?.formFieldTypes?.length
|
const fieldTypeOptions = ctx?.formFieldTypes?.length
|
||||||
? ctx.formFieldTypes
|
? ctx.formFieldTypes
|
||||||
: FORM_FIELD_TYPES.map((ft) => ({ id: ft, label: FORM_FIELD_TYPE_LABELS[ft] ?? ft, portType: 'str' }));
|
: FORM_FIELD_TYPES.map((ft) => ({ id: ft, label: FORM_FIELD_TYPE_LABELS[ft] ?? ft, portType: 'str' }));
|
||||||
|
|
@ -757,10 +754,7 @@ const FieldBuilderEditor: React.FC<FieldRendererProps> = ({ param, value, onChan
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const typeId = e.target.value;
|
const typeId = e.target.value;
|
||||||
const nextFields = [...(Array.isArray(f.fields) ? f.fields : [])];
|
const nextFields = [...(Array.isArray(f.fields) ? f.fields : [])];
|
||||||
const subRow: Record<string, unknown> = {
|
const subRow = { ...(nextFields[j] as Record<string, unknown>), type: typeId };
|
||||||
...(nextFields[j] as Record<string, unknown>),
|
|
||||||
type: typeId,
|
|
||||||
};
|
|
||||||
if (formFieldTypeHasConfigurableOptions(typeId)) {
|
if (formFieldTypeHasConfigurableOptions(typeId)) {
|
||||||
subRow.options = normalizeFormFieldOptions(subRow.options);
|
subRow.options = normalizeFormFieldOptions(subRow.options);
|
||||||
}
|
}
|
||||||
|
|
@ -1071,7 +1065,7 @@ export const FRONTEND_TYPE_RENDERERS: Record<string, FieldRendererComponent> = {
|
||||||
sharepointFolder: SharepointPathPicker,
|
sharepointFolder: SharepointPathPicker,
|
||||||
sharepointFile: SharepointPathPicker,
|
sharepointFile: SharepointPathPicker,
|
||||||
userFileFolder: UserFileFolderPicker,
|
userFileFolder: UserFileFolderPicker,
|
||||||
clickupList: ClickUpListPicker,
|
clickupList: FolderPicker,
|
||||||
clickupTask: FolderPicker,
|
clickupTask: FolderPicker,
|
||||||
caseList: CaseListEditor,
|
caseList: CaseListEditor,
|
||||||
fieldBuilder: FieldBuilderEditor,
|
fieldBuilder: FieldBuilderEditor,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,2 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
export { ConditionEditor as IfElseNodeConfig } from '../frontendTypeRenderers/ConditionEditor';
|
export { ConditionEditor as IfElseNodeConfig } from '../frontendTypeRenderers/ConditionEditor';
|
||||||
export type { StructuredCondition } from '../frontendTypeRenderers/ConditionEditor';
|
export type { StructuredCondition } from '../frontendTypeRenderers/ConditionEditor';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
/**
|
/**
|
||||||
* Loop node config - Datenquelle für Iteration mit benutzerfreundlichen Labels.
|
* Loop node config - Datenquelle für Iteration mit benutzerfreundlichen Labels.
|
||||||
* Z.B. für jedes Formularfeld, jede Datei aus Upload, jede E-Mail aus Suche.
|
* Z.B. für jedes Formularfeld, jede Datei aus Upload, jede E-Mail aus Suche.
|
||||||
|
|
@ -9,7 +7,7 @@ import React from 'react';
|
||||||
import type { NodeConfigRendererProps } from '../shared/types';
|
import type { NodeConfigRendererProps } from '../shared/types';
|
||||||
import { LoopItemsSelect } from '../shared/LoopItemsSelect';
|
import { LoopItemsSelect } from '../shared/LoopItemsSelect';
|
||||||
import { createValue, isRef, isValue } from '../shared/dataRef';
|
import { createValue, isRef, isValue } from '../shared/dataRef';
|
||||||
import styles from '../../editor/WorkflowFlowEditor.module.css';
|
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||||
|
|
||||||
export const LoopNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
export const LoopNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
||||||
const value = params.items;
|
const value = params.items;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
|
||||||
// All rights reserved.
|
|
||||||
export { LoopNodeConfig } from './LoopNodeConfig';
|
export { LoopNodeConfig } from './LoopNodeConfig';
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue