automation template/definition editor

This commit is contained in:
ValueOn AG 2026-02-03 23:42:27 +01:00
parent f31e10496a
commit edeaf64fa4
11 changed files with 419 additions and 132 deletions

View file

@ -12,7 +12,7 @@ Multi-Tenant Design:
import uuid
from typing import Optional, List
from enum import Enum
from pydantic import BaseModel, Field, EmailStr, field_validator
from pydantic import BaseModel, Field, EmailStr, field_validator, computed_field
from modules.shared.attributeUtils import registerModelLabels
from modules.shared.timeUtils import getUtcTimestamp
@ -114,6 +114,20 @@ class UserConnection(BaseModel):
{"value": "none", "label": {"en": "None", "fr": "Aucun"}},
]})
tokenExpiresAt: Optional[float] = Field(None, description="When the current token expires (UTC timestamp in seconds)", json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False})
@computed_field
@computed_field
@property
def connectionReference(self) -> str:
"""Generate connection reference string in format: connection:{authority}:{username}"""
return f"connection:{self.authority.value}:{self.externalUsername}"
@computed_field
@property
def displayLabel(self) -> str:
"""Human-readable label for display in dropdowns"""
authorityLabels = {"msft": "Microsoft", "google": "Google", "local": "Local"}
return f"{authorityLabels.get(self.authority.value, self.authority.value)}: {self.externalUsername}"
registerModelLabels(
@ -132,6 +146,8 @@ registerModelLabels(
"expiresAt": {"en": "Expires At", "de": "Läuft ab am", "fr": "Expire le"},
"tokenStatus": {"en": "Connection Status", "de": "Verbindungsstatus", "fr": "Statut de connexion"},
"tokenExpiresAt": {"en": "Expires At", "de": "Läuft ab am", "fr": "Expire le"},
"connectionReference": {"en": "Connection Reference", "de": "Verbindungsreferenz", "fr": "Référence de connexion"},
"displayLabel": {"en": "Display Label", "de": "Anzeigebezeichnung", "fr": "Libellé d'affichage"},
},
)

View file

@ -52,7 +52,7 @@ class AutomationObjects:
"""Initializes the database connection with proper configuration."""
# Get configuration values
dbHost = APP_CONFIG.get("DB_HOST", "_no_config_default_data")
dbDatabase = "poweron_app"
dbDatabase = "poweron_automation"
dbUser = APP_CONFIG.get("DB_USER")
dbPassword = APP_CONFIG.get("DB_PASSWORD_SECRET")
dbPort = int(APP_CONFIG.get("DB_PORT", 5432))

View file

@ -134,6 +134,100 @@ async def get_automation_attributes(
"""Get attribute definitions for AutomationDefinition model"""
return {"attributes": automationAttributes}
@router.get("/actions")
@limiter.limit("30/minute")
async def get_available_actions(
request: Request,
context: RequestContext = Depends(getRequestContext)
) -> JSONResponse:
"""
Get available workflow actions for template editor.
Returns action definitions with parameters and example JSON snippets.
"""
try:
from modules.workflows.processing.shared.methodDiscovery import methods, discoverMethods
from modules.services import getInterface as getServices
# Ensure methods are discovered (need a service center for discovery)
if not methods:
# Create a lightweight service center for method discovery
services = getServices(context.user, context.mandateId)
discoverMethods(services)
actionsList = []
processedMethods = set()
for methodName, methodInfo in methods.items():
# Skip short name aliases - only process full class names (MethodXxx)
if not methodName.startswith('Method'):
continue
shortName = methodName.replace('Method', '').lower()
# Skip if already processed
if shortName in processedMethods:
continue
processedMethods.add(shortName)
methodInstance = methodInfo.get('instance')
if not methodInstance:
continue
# Get actions from method instance
for actionName, actionDef in methodInstance._actions.items():
# Build action info
actionInfo = {
"method": shortName,
"action": actionName,
"actionId": actionDef.actionId if hasattr(actionDef, 'actionId') else f"{shortName}.{actionName}",
"description": actionDef.description if hasattr(actionDef, 'description') else "",
"category": actionDef.category if hasattr(actionDef, 'category') else "general",
"parameters": []
}
# Add parameters from WorkflowActionParameter
parametersDef = actionDef.parameters if hasattr(actionDef, 'parameters') else {}
for paramName, paramDef in parametersDef.items():
paramInfo = {
"name": paramName,
"type": paramDef.type if hasattr(paramDef, 'type') else "Any",
"frontendType": paramDef.frontendType.value if hasattr(paramDef, 'frontendType') and paramDef.frontendType else "text",
"required": paramDef.required if hasattr(paramDef, 'required') else False,
"default": paramDef.default if hasattr(paramDef, 'default') else None,
"description": paramDef.description if hasattr(paramDef, 'description') else "",
}
if hasattr(paramDef, 'frontendOptions') and paramDef.frontendOptions:
paramInfo["frontendOptions"] = paramDef.frontendOptions
actionInfo["parameters"].append(paramInfo)
# Build example JSON snippet for copy/paste
exampleParams = {}
for paramName, paramDef in parametersDef.items():
if hasattr(paramDef, 'required') and paramDef.required:
exampleParams[paramName] = f"{{{{KEY:{paramName}}}}}"
else:
default = paramDef.default if hasattr(paramDef, 'default') else None
exampleParams[paramName] = default or f"{{{{KEY:{paramName}}}}}"
actionInfo["exampleJson"] = {
"execMethod": shortName,
"execAction": actionName,
"execParameters": exampleParams,
"execResultLabel": f"{shortName}_{actionName}_result"
}
actionsList.append(actionInfo)
return JSONResponse(content={"actions": actionsList})
except Exception as e:
logger.error(f"Error getting available actions: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Error getting available actions: {str(e)}"
)
@router.get("/{automationId}", response_model=AutomationDefinition)
@limiter.limit("30/minute")
async def get_automation(
@ -290,99 +384,6 @@ async def execute_automation_route(
)
@router.get("/actions")
@limiter.limit("30/minute")
async def get_available_actions(
request: Request,
context: RequestContext = Depends(getRequestContext)
) -> JSONResponse:
"""
Get available workflow actions for template editor.
Returns action definitions with parameters and example JSON snippets.
"""
try:
from modules.workflows.processing.shared.methodDiscovery import methods, discoverMethods
from modules.services import getInterface as getServices
# Ensure methods are discovered (need a service center for discovery)
if not methods:
# Create a lightweight service center for method discovery
services = getServices(context.user, context.mandateId)
discoverMethods(services)
actionsList = []
processedMethods = set()
for methodName, methodInfo in methods.items():
# Skip short name aliases - only process full class names (MethodXxx)
if not methodName.startswith('Method'):
continue
shortName = methodName.replace('Method', '').lower()
# Skip if already processed
if shortName in processedMethods:
continue
processedMethods.add(shortName)
methodInstance = methodInfo.get('instance')
if not methodInstance:
continue
# Get actions from method instance
for actionName, actionDef in methodInstance._actions.items():
# Build action info
actionInfo = {
"method": shortName,
"action": actionName,
"actionId": actionDef.actionId if hasattr(actionDef, 'actionId') else f"{shortName}.{actionName}",
"description": actionDef.description if hasattr(actionDef, 'description') else "",
"category": actionDef.category if hasattr(actionDef, 'category') else "general",
"parameters": []
}
# Add parameters from WorkflowActionParameter
parametersDef = actionDef.parameters if hasattr(actionDef, 'parameters') else {}
for paramName, paramDef in parametersDef.items():
paramInfo = {
"name": paramName,
"type": paramDef.type if hasattr(paramDef, 'type') else "Any",
"frontendType": paramDef.frontendType.value if hasattr(paramDef, 'frontendType') and paramDef.frontendType else "text",
"required": paramDef.required if hasattr(paramDef, 'required') else False,
"default": paramDef.default if hasattr(paramDef, 'default') else None,
"description": paramDef.description if hasattr(paramDef, 'description') else "",
}
if hasattr(paramDef, 'frontendOptions') and paramDef.frontendOptions:
paramInfo["frontendOptions"] = paramDef.frontendOptions
actionInfo["parameters"].append(paramInfo)
# Build example JSON snippet for copy/paste
exampleParams = {}
for paramName, paramDef in parametersDef.items():
if hasattr(paramDef, 'required') and paramDef.required:
exampleParams[paramName] = f"{{{{KEY:{paramName}}}}}"
else:
default = paramDef.default if hasattr(paramDef, 'default') else None
exampleParams[paramName] = default or f"{{{{KEY:{paramName}}}}}"
actionInfo["exampleJson"] = {
"execMethod": shortName,
"execAction": actionName,
"execParameters": exampleParams,
"execResultLabel": f"{shortName}_{actionName}_result"
}
actionsList.append(actionInfo)
return JSONResponse(content={"actions": actionsList})
except Exception as e:
logger.error(f"Error getting available actions: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Error getting available actions: {str(e)}"
)
# =============================================================================
# AutomationTemplate Routes (DB-persistiert)
# =============================================================================

View file

@ -53,7 +53,7 @@ class InterfaceFeatureNeutralizer:
try:
# Use same database config pattern as other feature interfaces
dbHost = APP_CONFIG.get("DB_HOST", "localhost")
dbDatabase = APP_CONFIG.get("DB_DATABASE_NEUTRALIZATION", APP_CONFIG.get("DB_DATABASE", "poweron"))
dbDatabase = "poweron_neutralization"
dbUser = APP_CONFIG.get("DB_USER", "postgres")
dbPassword = APP_CONFIG.get("DB_PASSWORD_SECRET")
dbPort = int(APP_CONFIG.get("DB_PORT", 5432))

View file

@ -528,7 +528,9 @@ class TrusteePosition(BaseModel):
"frontend_hidden": True
}
)
# System attributes are automatically set by DatabaseConnector
# Allow extra fields like _createdAt from database
model_config = {"extra": "allow"}
registerModelLabels(

View file

@ -1178,10 +1178,12 @@ class TrusteeObjects:
featureCode=self.FEATURE_CODE
)
# Clean records (remove internal fields) - keep as dicts for filtering/sorting
# Clean records (remove internal fields except _createdAt) - keep as dicts for filtering/sorting
# Keep _createdAt for display in frontend
keepFields = {'_createdAt'}
cleanedRecords = []
for record in records:
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_") or k in keepFields}
cleanedRecords.append(cleanedRecord)
# Step 2: Apply filters (search and field filters)

View file

@ -74,31 +74,53 @@ def initBootstrap(db: DatabaseConnector) -> None:
initAutomationTemplates(db, adminUserId)
def initAutomationTemplates(db: DatabaseConnector, adminUserId: Optional[str] = None) -> None:
def initAutomationTemplates(dbApp: DatabaseConnector, adminUserId: Optional[str] = None) -> None:
"""
Seed initial automation templates from subAutomationTemplates.py.
Only runs if no templates exist yet (bootstrap).
Creates templates with _createdBy = admin user (SysAdmin privilege).
NOTE: AutomationTemplate lives in poweron_automation database, not poweron_app!
Args:
db: Database connector instance
dbApp: Database connector for poweron_app (used to get admin user if needed)
adminUserId: Admin user ID for _createdBy field
"""
import json
from modules.features.automation.subAutomationTemplates import AUTOMATION_TEMPLATES
from modules.features.automation.datamodelFeatureAutomation import AutomationTemplate
from modules.shared.configuration import APP_CONFIG
# Check if templates already exist
existing = db.getRecordset(AutomationTemplate)
# Create connector for poweron_automation database (where templates live)
dbHost = APP_CONFIG.get("DB_HOST", "_no_config_default_data")
dbDatabase = "poweron_automation"
dbUser = APP_CONFIG.get("DB_USER")
dbPassword = APP_CONFIG.get("DB_PASSWORD_SECRET")
dbPort = int(APP_CONFIG.get("DB_PORT", 5432))
dbAutomation = DatabaseConnector(
dbHost=dbHost,
dbDatabase=dbDatabase,
dbUser=dbUser,
dbPassword=dbPassword,
dbPort=dbPort,
userId=adminUserId,
)
dbAutomation.initDbSystem()
# Check if templates already exist in poweron_automation
existing = dbAutomation.getRecordset(AutomationTemplate)
if existing:
logger.info(f"Automation templates already seeded ({len(existing)} templates)")
return
# Get admin user ID if not provided
# Get admin user ID if not provided (from poweron_app)
if not adminUserId:
from modules.shared.configuration import APP_CONFIG
adminUsers = db.getRecordset(UserInDB, {"email": APP_CONFIG.ADMIN_EMAIL})
adminUsers = dbApp.getRecordset(UserInDB, {"email": APP_CONFIG.ADMIN_EMAIL})
adminUserId = adminUsers[0]["id"] if adminUsers else None
# Update context with admin user
if adminUserId:
dbAutomation.updateContext(adminUserId)
templates = AUTOMATION_TEMPLATES.get("sets", [])
createdCount = 0
@ -120,17 +142,13 @@ def initAutomationTemplates(db: DatabaseConnector, adminUserId: Optional[str] =
}
try:
# Update context to set _createdBy to admin
if adminUserId and hasattr(db, 'updateContext'):
db.updateContext(adminUserId)
db.recordCreate(AutomationTemplate, templateData)
dbAutomation.recordCreate(AutomationTemplate, templateData)
createdCount += 1
logger.debug(f"Created automation template: {overview}")
except Exception as e:
logger.error(f"Failed to create automation template '{overview}': {e}")
logger.info(f"Seeded {createdCount} automation templates")
logger.info(f"Seeded {createdCount} automation templates in poweron_automation database")
logger.info("System bootstrap completed")
@ -739,26 +757,36 @@ def _createTableSpecificRules(db: DatabaseConnector) -> None:
delete=AccessLevel.NONE,
))
# AutomationTemplate: Only MY-level access (user-owned)
for roleId in [adminId, userId]:
if roleId:
tableRules.append(AccessRule(
roleId=roleId,
context=AccessRuleContext.DATA,
item="data.automation.AutomationTemplate",
view=True,
read=AccessLevel.MY,
create=AccessLevel.MY,
update=AccessLevel.MY,
delete=AccessLevel.MY,
))
# AutomationTemplate: Admin sees ALL (system templates), User sees only MY
if adminId:
tableRules.append(AccessRule(
roleId=adminId,
context=AccessRuleContext.DATA,
item="data.automation.AutomationTemplate",
view=True,
read=AccessLevel.ALL, # SysAdmin sees all templates
create=AccessLevel.ALL,
update=AccessLevel.ALL,
delete=AccessLevel.ALL,
))
if userId:
tableRules.append(AccessRule(
roleId=userId,
context=AccessRuleContext.DATA,
item="data.automation.AutomationTemplate",
view=True,
read=AccessLevel.MY,
create=AccessLevel.MY,
update=AccessLevel.MY,
delete=AccessLevel.MY,
))
if viewerId:
tableRules.append(AccessRule(
roleId=viewerId,
context=AccessRuleContext.DATA,
item="data.automation.AutomationTemplate",
view=True,
read=AccessLevel.MY,
read=AccessLevel.ALL, # Viewer can see all templates (read-only)
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
@ -927,14 +955,20 @@ def _ensureDataContextRules(db: DatabaseConnector) -> None:
# Define tables that need rules (user-owned, no mandate context)
# Users can only manage their own records (MY-level access)
tablesNeedingRules = [
tablesNeedingMyRules = [
"data.chat.ChatWorkflow",
"data.automation.AutomationDefinition",
]
# Tables where admin sees ALL (system-wide templates)
tablesNeedingAllRulesForAdmin = [
"data.automation.AutomationTemplate",
]
missingRules = []
for objectKey in tablesNeedingRules:
# MY-level rules for user-owned tables
for objectKey in tablesNeedingMyRules:
# Admin: MY-level access (user-owned, no mandate context)
if adminId and (adminId, objectKey) not in existingCombinations:
missingRules.append(AccessRule(
@ -974,6 +1008,47 @@ def _ensureDataContextRules(db: DatabaseConnector) -> None:
delete=AccessLevel.NONE,
))
# ALL-level rules for admin on system templates
for objectKey in tablesNeedingAllRulesForAdmin:
# Admin: ALL-level access (sees all templates)
if adminId and (adminId, objectKey) not in existingCombinations:
missingRules.append(AccessRule(
roleId=adminId,
context=AccessRuleContext.DATA,
item=objectKey,
view=True,
read=AccessLevel.ALL,
create=AccessLevel.ALL,
update=AccessLevel.ALL,
delete=AccessLevel.ALL,
))
# User: MY-level access
if userId and (userId, objectKey) not in existingCombinations:
missingRules.append(AccessRule(
roleId=userId,
context=AccessRuleContext.DATA,
item=objectKey,
view=True,
read=AccessLevel.MY,
create=AccessLevel.MY,
update=AccessLevel.MY,
delete=AccessLevel.MY,
))
# Viewer: ALL read-only (can see all templates)
if viewerId and (viewerId, objectKey) not in existingCombinations:
missingRules.append(AccessRule(
roleId=viewerId,
context=AccessRuleContext.DATA,
item=objectKey,
view=True,
read=AccessLevel.ALL,
create=AccessLevel.NONE,
update=AccessLevel.NONE,
delete=AccessLevel.NONE,
))
# Create missing rules
if missingRules:
for rule in missingRules:
@ -981,6 +1056,44 @@ def _ensureDataContextRules(db: DatabaseConnector) -> None:
logger.info(f"Created {len(missingRules)} missing DATA context rules")
else:
logger.debug("All DATA context rules already exist")
# Update existing AutomationTemplate rules for admin/viewer to ALL access
_updateAutomationTemplateRulesToAll(db, adminId, viewerId)
def _updateAutomationTemplateRulesToAll(db: DatabaseConnector, adminId: Optional[str], viewerId: Optional[str]) -> None:
"""
Update existing AutomationTemplate RBAC rules from MY to ALL for admin and viewer.
This ensures sysadmins can see all templates (including system-seeded ones).
"""
if not adminId and not viewerId:
return
templateObjectKey = "data.automation.AutomationTemplate"
# Find existing rules for AutomationTemplate
existingRules = db.getRecordset(
AccessRule,
recordFilter={
"context": AccessRuleContext.DATA.value,
"item": templateObjectKey
}
)
updatedCount = 0
for rule in existingRules:
ruleId = rule.get("id")
roleId = rule.get("roleId")
currentReadLevel = rule.get("read")
# Update admin and viewer rules from MY to ALL
if roleId in [adminId, viewerId] and currentReadLevel == AccessLevel.MY.value:
db.recordModify(AccessRule, ruleId, {"read": AccessLevel.ALL.value})
updatedCount += 1
logger.debug(f"Updated AutomationTemplate rule {ruleId} for role {roleId} to ALL access")
if updatedCount > 0:
logger.info(f"Updated {updatedCount} AutomationTemplate RBAC rules to ALL access")
def _createResourceContextRules(db: DatabaseConnector) -> None:

View file

@ -39,6 +39,44 @@ def _getUserConnection(interface, connectionId: str, userId: str) -> Optional[Us
logger.error(f"Error getting user connection: {str(e)}")
return None
def _getUserConnectionByReference(interface, connectionReference: str, userId: str) -> Optional[UserConnection]:
"""
Get a user connection by reference string (format: connection:authority:username).
Args:
interface: Database interface
connectionReference: Reference string like 'connection:msft:user@email.com'
userId: User ID to verify ownership
Returns:
UserConnection if found and belongs to user, None otherwise
"""
try:
# Parse reference format: connection:{authority}:{username} [status:..., token:...]
# Remove state information if present
baseReference = connectionReference.split(' [')[0]
parts = baseReference.split(':')
if len(parts) < 3 or parts[0] != "connection":
logger.warning(f"Invalid connection reference format: {connectionReference}")
return None
authority = parts[1] # e.g., 'msft'
username = ':'.join(parts[2:]) # Handle usernames with colons
# Get user connections and find matching one
connections = interface.getUserConnections(userId)
for conn in connections:
connAuthority = conn.authority.value if hasattr(conn.authority, 'value') else str(conn.authority)
if connAuthority.lower() == authority.lower() and conn.externalUsername == username:
return conn
logger.debug(f"No connection found for reference: {connectionReference}")
return None
except Exception as e:
logger.error(f"Error getting user connection by reference: {str(e)}")
return None
@router.get("/{connectionId}/sites", response_model=List[Dict[str, Any]])
@limiter.limit("30/minute")
async def get_sharepoint_sites(
@ -251,3 +289,110 @@ def _extractSitePath(webUrl: str) -> str:
return "/sites/" + webUrl.split("/sites/")[1].split("/")[0]
return ""
# ============================================================================
# Universal folder-options endpoint (by connectionReference)
# ============================================================================
@router.get("/folder-options", response_model=List[Dict[str, Any]])
@limiter.limit("30/minute")
async def getSharepointFolderOptionsByReference(
request: Request,
connectionReference: str = Query(..., description="Connection reference string (e.g., 'connection:msft:user@email.com')"),
siteId: Optional[str] = Query(None, description="Specific site ID to browse (if omitted, returns sites only)"),
path: Optional[str] = Query(None, description="Folder path within site to browse"),
currentUser: User = Depends(getCurrentUser)
) -> List[Dict[str, Any]]:
"""
Get SharePoint folders formatted as dropdown options (universal endpoint).
Uses connectionReference instead of connectionId for easier integration.
Two modes:
1. If siteId is not provided: Returns list of sites (for site selection)
2. If siteId is provided: Returns folders within that site (optionally at specific path)
Args:
connectionReference: Connection reference string (e.g., 'connection:msft:user@email.com')
siteId: Optional site ID to browse folders within
path: Optional folder path within site
"""
try:
interface = getInterface(currentUser)
# Get the connection by reference
connection = _getUserConnectionByReference(interface, connectionReference, currentUser.id)
if not connection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Connection not found for reference: {connectionReference}"
)
# Verify it's a Microsoft connection
authority = connection.authority.value if hasattr(connection.authority, 'value') else str(connection.authority)
if authority.lower() != 'msft':
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Connection is not a Microsoft connection (authority: {authority})"
)
# Initialize services
services = getServices(currentUser, None)
# Set access token on SharePoint service
if not services.sharepoint.setAccessTokenFromConnection(connection):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Failed to set SharePoint access token. Connection may be expired or invalid."
)
# Mode 1: Return sites list if no siteId specified
if not siteId:
sites = await services.sharepoint.discoverSites()
return [
{
"type": "site",
"value": site.get("id"),
"label": site.get("displayName", "Unknown Site"),
"siteId": site.get("id"),
"siteName": site.get("displayName", "Unknown Site"),
"webUrl": site.get("webUrl", ""),
"path": _extractSitePath(site.get("webUrl", ""))
}
for site in sites
]
# Mode 2: Return folders within specific site
folderPath = path or ""
items = await services.sharepoint.listFolderContents(siteId, folderPath)
if not items:
return []
folderOptions = []
for item in items:
if item.get("type") == "folder":
folderName = item.get("name", "")
itemPath = f"{folderPath}/{folderName}" if folderPath else folderName
folderOptions.append({
"type": "folder",
"value": itemPath,
"label": folderName,
"siteId": siteId,
"folderName": folderName,
"path": itemPath,
"hasChildren": True # Assume folders may have children
})
return folderOptions
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting SharePoint folder options by reference: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting SharePoint folder options: {str(e)}"
)

View file

@ -106,7 +106,7 @@ def _getFeatureUiObjects(featureCode: str) -> List[Dict[str, Any]]:
from modules.features.trustee.mainTrustee import UI_OBJECTS
return UI_OBJECTS
elif featureCode == "realestate":
from modules.features.realEstate.mainRealEstate import UI_OBJECTS
from modules.features.realestate.mainRealEstate import UI_OBJECTS
return UI_OBJECTS
else:
logger.warning(f"Unknown feature code: {featureCode}")

View file

@ -54,9 +54,11 @@ class FrontendType(str, Enum):
WORKFLOW_ACTION = "workflowAction"
"""Workflow action selector - fetches available actions from workflow context"""
SHAREPOINT_FOLDER = "sharepointFolder"
"""SharePoint folder selector - requires connectionReference parameter in same action to load folders"""
# Additional custom types can be added here as needed
# Examples:
# SHAREPOINT_FOLDER = "sharepointFolder"
# OUTLOOK_FOLDER = "outlookFolder"
# JIRA_PROJECT = "jiraProject"
@ -66,6 +68,7 @@ CUSTOM_TYPE_OPTIONS_API: Dict[FrontendType, str] = {
FrontendType.USER_CONNECTION: "user.connection",
FrontendType.DOCUMENT_REFERENCE: "workflow.documentReference", # To be implemented
FrontendType.WORKFLOW_ACTION: "workflow.action", # To be implemented
FrontendType.SHAREPOINT_FOLDER: "sharepoint.folder", # Dynamic - requires connectionReference
}
# Mapping of custom types to their description
@ -85,6 +88,11 @@ CUSTOM_TYPE_DESCRIPTIONS: Dict[FrontendType, Dict[str, str]] = {
"fr": "Action de workflow",
"de": "Workflow-Aktion"
},
FrontendType.SHAREPOINT_FOLDER: {
"en": "SharePoint Folder",
"fr": "Dossier SharePoint",
"de": "SharePoint-Ordner"
},
}

View file

@ -109,7 +109,7 @@ class MethodSharepoint(MethodBase):
"pathQuery": WorkflowActionParameter(
name="pathQuery",
type="str",
frontendType=FrontendType.TEXT,
frontendType=FrontendType.SHAREPOINT_FOLDER,
required=False,
description="Direct path query if no documentList (e.g., /sites/SiteName/FolderPath)"
),
@ -146,7 +146,7 @@ class MethodSharepoint(MethodBase):
"pathQuery": WorkflowActionParameter(
name="pathQuery",
type="str",
frontendType=FrontendType.TEXT,
frontendType=FrontendType.SHAREPOINT_FOLDER,
required=False,
description="Direct upload target path if documentList doesn't contain findDocumentPath result (e.g., /sites/SiteName/FolderPath)"
)