diff --git a/app/integrations/asana.py b/app/integrations/asana.py index 38f5459..b73ccdd 100644 --- a/app/integrations/asana.py +++ b/app/integrations/asana.py @@ -269,19 +269,102 @@ class AsanaConnector(BaseConnector): "name": "workspace_gid", "type": "string", "label": "Workspace GID", + "required": True, + "placeholder": "1234567890", "description": "Asana workspace GID to sync with", + "help": "Find your workspace GID in Asana workspace settings or API", + }, + { + "name": "project_gids", + "type": "text", + "label": "Project GIDs", + "required": False, + "placeholder": "1234567890, 9876543210", + "description": "Comma-separated list of specific project GIDs to sync (leave empty to sync all)", + "help": "Optional: Limit sync to specific projects", }, { "name": "sync_direction", "type": "select", "label": "Sync Direction", "options": [ - {"value": "asana_to_timetracker", "label": "Asana → TimeTracker"}, - {"value": "timetracker_to_asana", "label": "TimeTracker → Asana"}, - {"value": "bidirectional", "label": "Bidirectional"}, + {"value": "asana_to_timetracker", "label": "Asana → TimeTracker (Import only)"}, + {"value": "timetracker_to_asana", "label": "TimeTracker → Asana (Export only)"}, + {"value": "bidirectional", "label": "Bidirectional (Two-way sync)"}, ], "default": "asana_to_timetracker", + "description": "Choose how data flows between Asana and TimeTracker", + }, + { + "name": "sync_items", + "type": "array", + "label": "Items to Sync", + "options": [ + {"value": "projects", "label": "Projects"}, + {"value": "tasks", "label": "Tasks"}, + {"value": "subtasks", "label": "Subtasks"}, + ], + "default": ["projects", "tasks"], + "description": "Select which items to synchronize", + }, + { + "name": "auto_sync", + "type": "boolean", + "label": "Auto Sync", + "default": False, + "description": "Automatically sync when changes are detected", + }, + { + "name": "sync_interval", + "type": "select", + "label": "Sync Schedule", + "options": [ + {"value": "manual", "label": "Manual only"}, + {"value": "hourly", "label": "Every hour"}, + {"value": "daily", "label": "Daily"}, + ], + "default": "manual", + "description": "How often to automatically sync data", + }, + { + "name": "sync_completed", + "type": "boolean", + "label": "Sync Completed Tasks", + "default": False, + "description": "Include completed tasks in sync", + }, + { + "name": "status_mapping", + "type": "json", + "label": "Status Mapping", + "placeholder": '{"completed": "completed", "incomplete": "todo"}', + "description": "Map Asana task completion status to TimeTracker statuses (JSON format)", + "help": "Customize how Asana task statuses map to TimeTracker task statuses", }, ], "required": ["workspace_gid"], + "sections": [ + { + "title": "Workspace Settings", + "description": "Configure your Asana workspace", + "fields": ["workspace_gid", "project_gids"], + }, + { + "title": "Sync Settings", + "description": "Configure what and how to sync", + "fields": ["sync_direction", "sync_items", "sync_completed", "auto_sync", "sync_interval"], + }, + { + "title": "Data Mapping", + "description": "Customize how data translates between Asana and TimeTracker", + "fields": ["status_mapping"], + }, + ], + "sync_settings": { + "enabled": True, + "auto_sync": False, + "sync_interval": "manual", + "sync_direction": "asana_to_timetracker", + "sync_items": ["projects", "tasks"], + }, } diff --git a/app/integrations/base.py b/app/integrations/base.py index 87c569b..8a61389 100644 --- a/app/integrations/base.py +++ b/app/integrations/base.py @@ -140,9 +140,51 @@ class BaseConnector(ABC): Get configuration schema for this connector. Returns: - Dict describing configuration fields + Dict describing configuration fields with structure: + { + "fields": [ + { + "name": "field_name", + "type": "string|number|boolean|select|array|json|password|url|text", + "label": "Display Label", + "description": "Help text", + "placeholder": "Placeholder text", + "default": default_value, + "required": True/False, + "options": [{"value": "val", "label": "Label"}] for select, + "help": "Additional help text", + "validation": {"min": 1, "max": 100} for numbers, + } + ], + "required": ["field_name"], + "sections": [ + { + "title": "Section Title", + "description": "Section description", + "fields": ["field_name1", "field_name2"] + } + ], + "sync_settings": { + "enabled": True/False, + "auto_sync": True/False, + "sync_interval": "hourly|daily|weekly|manual", + "sync_direction": "provider_to_timetracker|timetracker_to_provider|bidirectional", + "sync_items": ["tasks", "projects", "time_entries"], + } + } """ - return {"fields": [], "required": []} + return { + "fields": [], + "required": [], + "sections": [], + "sync_settings": { + "enabled": True, + "auto_sync": False, + "sync_interval": "manual", + "sync_direction": "provider_to_timetracker", + "sync_items": [], + } + } def validate_config(self, config: Dict[str, Any]) -> Dict[str, Any]: """ @@ -154,4 +196,104 @@ class BaseConnector(ABC): Returns: Dict with 'valid' (bool) and 'errors' (list) """ - return {"valid": True, "errors": []} + schema = self.get_config_schema() + errors = [] + + # Check required fields + required_fields = schema.get("required", []) + for field_name in required_fields: + if field_name not in config or not config[field_name]: + # Find field label for error message + field_label = field_name + for field in schema.get("fields", []): + if field.get("name") == field_name: + field_label = field.get("label", field_name) + break + errors.append(f"{field_label} is required") + + # Validate field types and constraints + for field in schema.get("fields", []): + field_name = field.get("name") + if field_name not in config: + continue + + value = config[field_name] + field_type = field.get("type", "string") + + # Type validation + if field_type == "number" and value is not None: + try: + float(value) + # Check min/max if specified + validation = field.get("validation", {}) + if "min" in validation and float(value) < validation["min"]: + errors.append(f"{field.get('label', field_name)} must be at least {validation['min']}") + if "max" in validation and float(value) > validation["max"]: + errors.append(f"{field.get('label', field_name)} must be at most {validation['max']}") + except (ValueError, TypeError): + errors.append(f"{field.get('label', field_name)} must be a number") + elif field_type == "boolean" and value is not None: + if not isinstance(value, bool) and value not in ("true", "false", "1", "0", "on", "off", ""): + errors.append(f"{field.get('label', field_name)} must be a boolean") + elif field_type == "url" and value: + try: + from urllib.parse import urlparse + parsed = urlparse(value) + if not parsed.scheme or not parsed.netloc: + errors.append(f"{field.get('label', field_name)} must be a valid URL") + except Exception: + errors.append(f"{field.get('label', field_name)} must be a valid URL") + elif field_type == "json" and value: + try: + import json + if isinstance(value, str): + json.loads(value) + except json.JSONDecodeError: + errors.append(f"{field.get('label', field_name)} must be valid JSON") + + return {"valid": len(errors) == 0, "errors": errors} + + def get_sync_settings(self) -> Dict[str, Any]: + """ + Get current sync settings from integration config. + + Returns: + Dict with sync settings + """ + if not self.integration or not self.integration.config: + schema = self.get_config_schema() + return schema.get("sync_settings", {}) + + config = self.integration.config + schema = self.get_config_schema() + default_sync_settings = schema.get("sync_settings", {}) + + return { + "enabled": config.get("sync_enabled", default_sync_settings.get("enabled", True)), + "auto_sync": config.get("auto_sync", default_sync_settings.get("auto_sync", False)), + "sync_interval": config.get("sync_interval", default_sync_settings.get("sync_interval", "manual")), + "sync_direction": config.get("sync_direction", default_sync_settings.get("sync_direction", "provider_to_timetracker")), + "sync_items": config.get("sync_items", default_sync_settings.get("sync_items", [])), + } + + def get_field_mappings(self) -> Dict[str, str]: + """ + Get field mappings for data translation. + + Returns: + Dict mapping provider fields to TimeTracker fields + """ + if not self.integration or not self.integration.config: + return {} + return self.integration.config.get("field_mappings", {}) + + def get_status_mappings(self) -> Dict[str, str]: + """ + Get status mappings for data translation. + + Returns: + Dict mapping provider statuses to TimeTracker statuses + """ + if not self.integration or not self.integration.config: + return {} + return self.integration.config.get("status_mappings", {}) diff --git a/app/integrations/caldav_calendar.py b/app/integrations/caldav_calendar.py index 28b2af1..2b582cb 100644 --- a/app/integrations/caldav_calendar.py +++ b/app/integrations/caldav_calendar.py @@ -896,5 +896,134 @@ class CalDAVCalendarConnector(BaseConnector): cal.add_component(event) return cal.to_ical().decode('utf-8') + + def get_config_schema(self) -> Dict[str, Any]: + """Get configuration schema.""" + return { + "fields": [ + { + "name": "server_url", + "type": "url", + "label": "Server URL", + "required": False, + "placeholder": "https://mail.example.com/dav", + "description": "CalDAV server base URL (optional if calendar_url is provided)", + "help": "Base URL of your CalDAV server (e.g., https://mail.example.com/dav)", + }, + { + "name": "calendar_url", + "type": "url", + "label": "Calendar URL", + "required": False, + "placeholder": "https://mail.example.com/dav/user/calendar/", + "description": "Full URL to the calendar collection (ends with /)", + "help": "Direct URL to your calendar. Must end with a forward slash (/).", + }, + { + "name": "calendar_name", + "type": "string", + "label": "Calendar Name", + "required": False, + "placeholder": "My Calendar", + "description": "Display name for the calendar (optional)", + }, + { + "name": "sync_direction", + "type": "select", + "label": "Sync Direction", + "options": [ + {"value": "calendar_to_time_tracker", "label": "Calendar → TimeTracker (Import only)"}, + {"value": "time_tracker_to_calendar", "label": "TimeTracker → Calendar (Export only)"}, + {"value": "bidirectional", "label": "Bidirectional (Two-way sync)"}, + ], + "default": "calendar_to_time_tracker", + "description": "Choose how data flows between CalDAV calendar and TimeTracker", + }, + { + "name": "sync_items", + "type": "array", + "label": "Items to Sync", + "options": [ + {"value": "time_entries", "label": "Time Entries"}, + {"value": "events", "label": "Calendar Events"}, + ], + "default": ["events"], + "description": "Select which items to synchronize", + }, + { + "name": "default_project_id", + "type": "number", + "label": "Default Project", + "required": True, + "description": "Default project to assign imported calendar events to", + "help": "Required for importing calendar events as time entries", + }, + { + "name": "lookback_days", + "type": "number", + "label": "Lookback Days", + "default": 90, + "validation": {"min": 1, "max": 365}, + "description": "Number of days in the past to sync (1-365)", + "help": "How far back to sync calendar events", + }, + { + "name": "lookahead_days", + "type": "number", + "label": "Lookahead Days", + "default": 7, + "validation": {"min": 1, "max": 365}, + "description": "Number of days in the future to sync (1-365)", + "help": "How far ahead to sync calendar events", + }, + { + "name": "verify_ssl", + "type": "boolean", + "label": "Verify SSL Certificate", + "default": True, + "description": "Verify SSL certificate when connecting to CalDAV server", + "help": "Disable only if using a self-signed certificate", + }, + { + "name": "auto_sync", + "type": "boolean", + "label": "Auto Sync", + "default": False, + "description": "Automatically sync on a schedule", + }, + { + "name": "sync_interval", + "type": "select", + "label": "Sync Schedule", + "options": [ + {"value": "manual", "label": "Manual only"}, + {"value": "hourly", "label": "Every hour"}, + {"value": "daily", "label": "Daily"}, + ], + "default": "manual", + "description": "How often to automatically sync data", + }, + ], + "required": ["default_project_id"], + "sections": [ + { + "title": "Connection Settings", + "description": "Configure your CalDAV server connection", + "fields": ["server_url", "calendar_url", "calendar_name", "verify_ssl"], + }, + { + "title": "Sync Settings", + "description": "Configure what and how to sync", + "fields": ["sync_direction", "sync_items", "default_project_id", "lookback_days", "lookahead_days", "auto_sync", "sync_interval"], + }, + ], + "sync_settings": { + "enabled": True, + "auto_sync": False, + "sync_interval": "manual", + "sync_direction": "calendar_to_time_tracker", + "sync_items": ["events"], + }, + } diff --git a/app/integrations/github.py b/app/integrations/github.py index e266c2e..d14a097 100644 --- a/app/integrations/github.py +++ b/app/integrations/github.py @@ -433,14 +433,71 @@ class GitHubConnector(BaseConnector): "type": "text", "required": False, "placeholder": "owner/repo1, owner/repo2", - "help": "Comma-separated list of repositories to sync", + "help": "Comma-separated list of repositories to sync (e.g., 'octocat/Hello-World, owner/repo'). Leave empty to sync all accessible repositories.", + "description": "Which GitHub repositories to sync", + }, + { + "name": "sync_direction", + "type": "select", + "label": "Sync Direction", + "options": [ + {"value": "github_to_timetracker", "label": "GitHub → TimeTracker (Import only)"}, + {"value": "timetracker_to_github", "label": "TimeTracker → GitHub (Export only)"}, + {"value": "bidirectional", "label": "Bidirectional (Two-way sync)"}, + ], + "default": "github_to_timetracker", + "description": "Choose how data flows between GitHub and TimeTracker", + }, + { + "name": "sync_items", + "type": "array", + "label": "Items to Sync", + "options": [ + {"value": "issues", "label": "Issues"}, + {"value": "pull_requests", "label": "Pull Requests"}, + {"value": "projects", "label": "Projects (Repositories)"}, + ], + "default": ["issues"], + "description": "Select which items to synchronize", + }, + { + "name": "issue_states", + "type": "array", + "label": "Issue States to Sync", + "options": [ + {"value": "open", "label": "Open Issues"}, + {"value": "closed", "label": "Closed Issues"}, + {"value": "all", "label": "All Issues"}, + ], + "default": ["open"], + "description": "Which issue states to include in sync", }, { "name": "auto_sync", "type": "boolean", "label": "Auto Sync", + "default": False, + "description": "Automatically sync when webhooks are received from GitHub", + }, + { + "name": "sync_interval", + "type": "select", + "label": "Sync Schedule", + "options": [ + {"value": "manual", "label": "Manual only"}, + {"value": "hourly", "label": "Every hour"}, + {"value": "daily", "label": "Daily"}, + {"value": "weekly", "label": "Weekly"}, + ], + "default": "manual", + "description": "How often to automatically sync data", + }, + { + "name": "create_projects", + "type": "boolean", + "label": "Create Projects", "default": True, - "description": "Automatically sync when webhooks are received", + "description": "Automatically create projects in TimeTracker from GitHub repositories", }, { "name": "webhook_secret", @@ -448,8 +505,33 @@ class GitHubConnector(BaseConnector): "type": "password", "required": False, "placeholder": "Enter webhook secret from GitHub", - "help": "Secret token for verifying webhook signatures (configure in GitHub webhook settings)", + "help": "Secret token for verifying webhook signatures. Configure this in your GitHub repository webhook settings.", + "description": "Security token for webhook verification", }, ], "required": [], + "sections": [ + { + "title": "Repository Settings", + "description": "Configure which repositories to sync", + "fields": ["repositories", "create_projects"], + }, + { + "title": "Sync Settings", + "description": "Configure what and how to sync", + "fields": ["sync_direction", "sync_items", "issue_states", "auto_sync", "sync_interval"], + }, + { + "title": "Webhook Settings", + "description": "Configure webhook security", + "fields": ["webhook_secret"], + }, + ], + "sync_settings": { + "enabled": True, + "auto_sync": False, + "sync_interval": "manual", + "sync_direction": "github_to_timetracker", + "sync_items": ["issues"], + }, } diff --git a/app/integrations/gitlab.py b/app/integrations/gitlab.py index a4fe7c9..ff0b176 100644 --- a/app/integrations/gitlab.py +++ b/app/integrations/gitlab.py @@ -236,21 +236,109 @@ class GitLabConnector(BaseConnector): "fields": [ { "name": "repository_ids", - "type": "array", + "type": "text", "label": "Repository IDs", - "description": "GitLab project IDs to sync (leave empty to sync all accessible projects)", + "required": False, + "placeholder": "123456, 789012", + "description": "Comma-separated list of GitLab project IDs to sync (leave empty to sync all accessible projects)", + "help": "Find project IDs in GitLab project settings or API. Leave empty to sync all projects you have access to.", }, { "name": "sync_direction", "type": "select", "label": "Sync Direction", "options": [ - {"value": "gitlab_to_timetracker", "label": "GitLab → TimeTracker"}, - {"value": "timetracker_to_gitlab", "label": "TimeTracker → GitLab"}, - {"value": "bidirectional", "label": "Bidirectional"}, + {"value": "gitlab_to_timetracker", "label": "GitLab → TimeTracker (Import only)"}, + {"value": "timetracker_to_gitlab", "label": "TimeTracker → GitLab (Export only)"}, + {"value": "bidirectional", "label": "Bidirectional (Two-way sync)"}, ], "default": "gitlab_to_timetracker", + "description": "Choose how data flows between GitLab and TimeTracker", + }, + { + "name": "sync_items", + "type": "array", + "label": "Items to Sync", + "options": [ + {"value": "issues", "label": "Issues"}, + {"value": "merge_requests", "label": "Merge Requests"}, + {"value": "projects", "label": "Projects"}, + ], + "default": ["issues"], + "description": "Select which items to synchronize", + }, + { + "name": "issue_states", + "type": "array", + "label": "Issue States to Sync", + "options": [ + {"value": "opened", "label": "Open Issues"}, + {"value": "closed", "label": "Closed Issues"}, + {"value": "all", "label": "All Issues"}, + ], + "default": ["opened"], + "description": "Which issue states to include in sync", + }, + { + "name": "auto_sync", + "type": "boolean", + "label": "Auto Sync", + "default": False, + "description": "Automatically sync when webhooks are received from GitLab", + }, + { + "name": "sync_interval", + "type": "select", + "label": "Sync Schedule", + "options": [ + {"value": "manual", "label": "Manual only"}, + {"value": "hourly", "label": "Every hour"}, + {"value": "daily", "label": "Daily"}, + {"value": "weekly", "label": "Weekly"}, + ], + "default": "manual", + "description": "How often to automatically sync data", + }, + { + "name": "create_projects", + "type": "boolean", + "label": "Create Projects", + "default": True, + "description": "Automatically create projects in TimeTracker from GitLab projects", + }, + { + "name": "webhook_secret", + "label": "Webhook Secret", + "type": "password", + "required": False, + "placeholder": "Enter webhook secret from GitLab", + "help": "Secret token for verifying webhook signatures. Configure this in your GitLab project webhook settings.", + "description": "Security token for webhook verification", }, ], "required": [], + "sections": [ + { + "title": "Repository Settings", + "description": "Configure which GitLab projects to sync", + "fields": ["repository_ids", "create_projects"], + }, + { + "title": "Sync Settings", + "description": "Configure what and how to sync", + "fields": ["sync_direction", "sync_items", "issue_states", "auto_sync", "sync_interval"], + }, + { + "title": "Webhook Settings", + "description": "Configure webhook security", + "fields": ["webhook_secret"], + }, + ], + "sync_settings": { + "enabled": True, + "auto_sync": False, + "sync_interval": "manual", + "sync_direction": "gitlab_to_timetracker", + "sync_items": ["issues"], + }, } diff --git a/app/integrations/google_calendar.py b/app/integrations/google_calendar.py index 715ef4d..3ed62aa 100644 --- a/app/integrations/google_calendar.py +++ b/app/integrations/google_calendar.py @@ -474,18 +474,33 @@ class GoogleCalendarConnector(BaseConnector): "type": "string", "label": "Calendar ID", "default": "primary", + "required": False, + "placeholder": "primary", "description": "Google Calendar ID to sync with (default: primary)", + "help": "Use 'primary' for your main calendar, or enter a specific calendar ID from Google Calendar settings", }, { "name": "sync_direction", "type": "select", "label": "Sync Direction", "options": [ - {"value": "time_tracker_to_calendar", "label": "TimeTracker → Calendar"}, - {"value": "calendar_to_time_tracker", "label": "Calendar → TimeTracker"}, - {"value": "bidirectional", "label": "Bidirectional"}, + {"value": "time_tracker_to_calendar", "label": "TimeTracker → Calendar (Export only)"}, + {"value": "calendar_to_time_tracker", "label": "Calendar → TimeTracker (Import only)"}, + {"value": "bidirectional", "label": "Bidirectional (Two-way sync)"}, ], "default": "time_tracker_to_calendar", + "description": "Choose how data flows between Google Calendar and TimeTracker", + }, + { + "name": "sync_items", + "type": "array", + "label": "Items to Sync", + "options": [ + {"value": "time_entries", "label": "Time Entries"}, + {"value": "events", "label": "Calendar Events"}, + ], + "default": ["time_entries"], + "description": "Select which items to synchronize", }, { "name": "auto_sync", @@ -494,6 +509,69 @@ class GoogleCalendarConnector(BaseConnector): "default": True, "description": "Automatically sync when time entries are created/updated", }, + { + "name": "sync_interval", + "type": "select", + "label": "Sync Schedule", + "options": [ + {"value": "manual", "label": "Manual only"}, + {"value": "hourly", "label": "Every hour"}, + {"value": "daily", "label": "Daily"}, + ], + "default": "hourly", + "description": "How often to automatically sync data", + }, + { + "name": "event_title_format", + "type": "text", + "label": "Event Title Format", + "default": "{project} - {task}", + "placeholder": "{project} - {task}", + "description": "Format for calendar event titles. Use {project}, {task}, {notes} as placeholders", + "help": "Customize how time entries appear as calendar events", + }, + { + "name": "sync_past_days", + "type": "number", + "label": "Sync Past Days", + "default": 90, + "validation": {"min": 1, "max": 365}, + "description": "Number of days in the past to sync (1-365)", + "help": "How far back to sync calendar events", + }, + { + "name": "sync_future_days", + "type": "number", + "label": "Sync Future Days", + "default": 30, + "validation": {"min": 1, "max": 365}, + "description": "Number of days in the future to sync (1-365)", + "help": "How far ahead to sync calendar events", + }, ], "required": [], + "sections": [ + { + "title": "Calendar Settings", + "description": "Configure your Google Calendar connection", + "fields": ["calendar_id"], + }, + { + "title": "Sync Settings", + "description": "Configure what and how to sync", + "fields": ["sync_direction", "sync_items", "auto_sync", "sync_interval", "sync_past_days", "sync_future_days"], + }, + { + "title": "Display Settings", + "description": "Customize how events appear in the calendar", + "fields": ["event_title_format"], + }, + ], + "sync_settings": { + "enabled": True, + "auto_sync": True, + "sync_interval": "hourly", + "sync_direction": "time_tracker_to_calendar", + "sync_items": ["time_entries"], + }, } diff --git a/app/integrations/jira.py b/app/integrations/jira.py index c0fcb0b..ab1da3e 100644 --- a/app/integrations/jira.py +++ b/app/integrations/jira.py @@ -253,6 +253,12 @@ class JiraConnector(BaseConnector): def _map_jira_status(self, jira_status: str) -> str: """Map Jira status to TimeTracker task status.""" + # Check for custom status mapping in config + status_mapping = self.get_status_mappings() + if status_mapping and jira_status in status_mapping: + return status_mapping[jira_status] + + # Default mapping status_map = { "To Do": "todo", "In Progress": "in_progress", @@ -299,22 +305,109 @@ class JiraConnector(BaseConnector): "type": "url", "required": True, "placeholder": "https://your-domain.atlassian.net", + "description": "Your Jira instance URL", + "help": "Enter your Jira Cloud or Server URL", }, { "name": "jql", "label": "JQL Query", "type": "text", "required": False, - "placeholder": "assignee = currentUser() AND status != Done", - "help": "Jira Query Language query to filter issues to sync", + "placeholder": "assignee = currentUser() AND status != Done ORDER BY updated DESC", + "help": "Jira Query Language query to filter issues to sync. Leave empty to sync all assigned issues.", + "description": "Filter which issues to sync from Jira", + }, + { + "name": "sync_direction", + "type": "select", + "label": "Sync Direction", + "options": [ + {"value": "jira_to_timetracker", "label": "Jira → TimeTracker (Import only)"}, + {"value": "timetracker_to_jira", "label": "TimeTracker → Jira (Export only)"}, + {"value": "bidirectional", "label": "Bidirectional (Two-way sync)"}, + ], + "default": "jira_to_timetracker", + "description": "Choose how data flows between Jira and TimeTracker", + }, + { + "name": "sync_items", + "type": "array", + "label": "Items to Sync", + "options": [ + {"value": "issues", "label": "Issues (Tasks)"}, + {"value": "projects", "label": "Projects"}, + {"value": "time_entries", "label": "Time Entries"}, + ], + "default": ["issues"], + "description": "Select which items to synchronize", }, { "name": "auto_sync", "type": "boolean", "label": "Auto Sync", + "default": False, + "description": "Automatically sync when webhooks are received from Jira", + }, + { + "name": "sync_interval", + "type": "select", + "label": "Sync Schedule", + "options": [ + {"value": "manual", "label": "Manual only"}, + {"value": "hourly", "label": "Every hour"}, + {"value": "daily", "label": "Daily"}, + {"value": "weekly", "label": "Weekly"}, + ], + "default": "manual", + "description": "How often to automatically sync data", + }, + { + "name": "create_projects", + "type": "boolean", + "label": "Create Projects", "default": True, - "description": "Automatically sync when webhooks are received", + "description": "Automatically create projects in TimeTracker from Jira projects", + }, + { + "name": "status_mapping", + "type": "json", + "label": "Status Mapping", + "placeholder": '{"To Do": "todo", "In Progress": "in_progress", "Done": "completed"}', + "description": "Map Jira statuses to TimeTracker statuses (JSON format)", + "help": "Customize how Jira issue statuses map to TimeTracker task statuses", + }, + { + "name": "field_mapping", + "type": "json", + "label": "Field Mapping", + "placeholder": '{"summary": "name", "description": "description", "assignee": "user_id"}', + "description": "Map Jira fields to TimeTracker fields (JSON format)", + "help": "Customize how Jira issue fields map to TimeTracker task fields", }, ], "required": ["jira_url"], + "sections": [ + { + "title": "Connection Settings", + "description": "Configure your Jira connection", + "fields": ["jira_url", "jql"], + }, + { + "title": "Sync Settings", + "description": "Configure what and how to sync", + "fields": ["sync_direction", "sync_items", "auto_sync", "sync_interval", "create_projects"], + }, + { + "title": "Data Mapping", + "description": "Customize how data translates between Jira and TimeTracker", + "fields": ["status_mapping", "field_mapping"], + }, + ], + "sync_settings": { + "enabled": True, + "auto_sync": False, + "sync_interval": "manual", + "sync_direction": "jira_to_timetracker", + "sync_items": ["issues"], + }, } diff --git a/app/integrations/microsoft_teams.py b/app/integrations/microsoft_teams.py index 548ea25..c61d05e 100644 --- a/app/integrations/microsoft_teams.py +++ b/app/integrations/microsoft_teams.py @@ -252,20 +252,113 @@ class MicrosoftTeamsConnector(BaseConnector): "name": "default_channel_id", "type": "string", "label": "Default Channel ID", + "required": False, + "placeholder": "19:channel-id@thread.tacv2", "description": "Default Teams channel ID for notifications", + "help": "Find channel ID in Teams channel settings or API", + }, + { + "name": "sync_direction", + "type": "select", + "label": "Sync Direction", + "options": [ + {"value": "teams_to_timetracker", "label": "Teams → TimeTracker (Import only)"}, + {"value": "timetracker_to_teams", "label": "TimeTracker → Teams (Export only)"}, + {"value": "bidirectional", "label": "Bidirectional (Two-way sync)"}, + ], + "default": "timetracker_to_teams", + "description": "Choose how data flows between Microsoft Teams and TimeTracker", + }, + { + "name": "sync_items", + "type": "array", + "label": "Items to Sync", + "options": [ + {"value": "channels", "label": "Channels"}, + {"value": "teams", "label": "Teams"}, + {"value": "messages", "label": "Messages (as tasks)"}, + ], + "default": [], + "description": "Select which items to synchronize", }, { "name": "notify_on_time_entry_start", "type": "boolean", "label": "Notify on Time Entry Start", "default": False, + "description": "Send Teams notification when a time entry starts", + }, + { + "name": "notify_on_time_entry_complete", + "type": "boolean", + "label": "Notify on Time Entry Complete", + "default": False, + "description": "Send Teams notification when a time entry is completed", + }, + { + "name": "notify_on_task_complete", + "type": "boolean", + "label": "Notify on Task Complete", + "default": False, + "description": "Send Teams notification when a task is completed", }, { "name": "notify_on_invoice_sent", "type": "boolean", "label": "Notify on Invoice Sent", "default": True, + "description": "Send Teams notification when an invoice is sent", + }, + { + "name": "notify_on_project_create", + "type": "boolean", + "label": "Notify on Project Create", + "default": False, + "description": "Send Teams notification when a project is created", + }, + { + "name": "auto_sync", + "type": "boolean", + "label": "Auto Sync", + "default": False, + "description": "Automatically sync when webhooks are received from Teams", + }, + { + "name": "sync_interval", + "type": "select", + "label": "Sync Schedule", + "options": [ + {"value": "manual", "label": "Manual only"}, + {"value": "hourly", "label": "Every hour"}, + {"value": "daily", "label": "Daily"}, + ], + "default": "manual", + "description": "How often to automatically sync data", }, ], "required": [], + "sections": [ + { + "title": "Channel Settings", + "description": "Configure Teams channel for notifications", + "fields": ["default_channel_id"], + }, + { + "title": "Sync Settings", + "description": "Configure what and how to sync", + "fields": ["sync_direction", "sync_items", "auto_sync", "sync_interval"], + }, + { + "title": "Notification Settings", + "description": "Configure when to send Teams notifications", + "fields": ["notify_on_time_entry_start", "notify_on_time_entry_complete", "notify_on_task_complete", "notify_on_invoice_sent", "notify_on_project_create"], + }, + ], + "sync_settings": { + "enabled": True, + "auto_sync": False, + "sync_interval": "manual", + "sync_direction": "timetracker_to_teams", + "sync_items": [], + }, } diff --git a/app/integrations/outlook_calendar.py b/app/integrations/outlook_calendar.py index 63386f4..93e9aa1 100644 --- a/app/integrations/outlook_calendar.py +++ b/app/integrations/outlook_calendar.py @@ -352,18 +352,33 @@ class OutlookCalendarConnector(BaseConnector): "type": "string", "label": "Calendar ID", "default": "calendar", + "required": False, + "placeholder": "calendar", "description": "Outlook Calendar ID to sync with (default: 'calendar' for primary calendar)", + "help": "Use 'calendar' for your primary calendar, or enter a specific calendar ID from Outlook settings", }, { "name": "sync_direction", "type": "select", "label": "Sync Direction", "options": [ - {"value": "time_tracker_to_calendar", "label": "TimeTracker → Calendar"}, - {"value": "calendar_to_time_tracker", "label": "Calendar → TimeTracker"}, - {"value": "bidirectional", "label": "Bidirectional"}, + {"value": "time_tracker_to_calendar", "label": "TimeTracker → Calendar (Export only)"}, + {"value": "calendar_to_time_tracker", "label": "Calendar → TimeTracker (Import only)"}, + {"value": "bidirectional", "label": "Bidirectional (Two-way sync)"}, ], "default": "time_tracker_to_calendar", + "description": "Choose how data flows between Outlook Calendar and TimeTracker", + }, + { + "name": "sync_items", + "type": "array", + "label": "Items to Sync", + "options": [ + {"value": "time_entries", "label": "Time Entries"}, + {"value": "events", "label": "Calendar Events"}, + ], + "default": ["time_entries"], + "description": "Select which items to synchronize", }, { "name": "auto_sync", @@ -372,6 +387,69 @@ class OutlookCalendarConnector(BaseConnector): "default": True, "description": "Automatically sync when time entries are created/updated", }, + { + "name": "sync_interval", + "type": "select", + "label": "Sync Schedule", + "options": [ + {"value": "manual", "label": "Manual only"}, + {"value": "hourly", "label": "Every hour"}, + {"value": "daily", "label": "Daily"}, + ], + "default": "hourly", + "description": "How often to automatically sync data", + }, + { + "name": "event_title_format", + "type": "text", + "label": "Event Title Format", + "default": "{project} - {task}", + "placeholder": "{project} - {task}", + "description": "Format for calendar event titles. Use {project}, {task}, {notes} as placeholders", + "help": "Customize how time entries appear as calendar events", + }, + { + "name": "sync_past_days", + "type": "number", + "label": "Sync Past Days", + "default": 90, + "validation": {"min": 1, "max": 365}, + "description": "Number of days in the past to sync (1-365)", + "help": "How far back to sync calendar events", + }, + { + "name": "sync_future_days", + "type": "number", + "label": "Sync Future Days", + "default": 30, + "validation": {"min": 1, "max": 365}, + "description": "Number of days in the future to sync (1-365)", + "help": "How far ahead to sync calendar events", + }, ], "required": [], + "sections": [ + { + "title": "Calendar Settings", + "description": "Configure your Outlook Calendar connection", + "fields": ["calendar_id"], + }, + { + "title": "Sync Settings", + "description": "Configure what and how to sync", + "fields": ["sync_direction", "sync_items", "auto_sync", "sync_interval", "sync_past_days", "sync_future_days"], + }, + { + "title": "Display Settings", + "description": "Customize how events appear in the calendar", + "fields": ["event_title_format"], + }, + ], + "sync_settings": { + "enabled": True, + "auto_sync": True, + "sync_interval": "hourly", + "sync_direction": "time_tracker_to_calendar", + "sync_items": ["time_entries"], + }, } diff --git a/app/integrations/quickbooks.py b/app/integrations/quickbooks.py index 892d0a9..d87a5ea 100644 --- a/app/integrations/quickbooks.py +++ b/app/integrations/quickbooks.py @@ -615,7 +615,10 @@ class QuickBooksConnector(BaseConnector): "name": "realm_id", "type": "string", "label": "Company ID (Realm ID)", + "required": True, + "placeholder": "123456789", "description": "QuickBooks company ID (realm ID)", + "help": "Find your company ID in QuickBooks after connecting. It's automatically set during OAuth.", }, { "name": "use_sandbox", @@ -624,33 +627,112 @@ class QuickBooksConnector(BaseConnector): "default": True, "description": "Use QuickBooks sandbox environment for testing", }, - {"name": "sync_invoices", "type": "boolean", "label": "Sync Invoices", "default": True}, - {"name": "sync_expenses", "type": "boolean", "label": "Sync Expenses", "default": True}, + { + "name": "sync_direction", + "type": "select", + "label": "Sync Direction", + "options": [ + {"value": "quickbooks_to_timetracker", "label": "QuickBooks → TimeTracker (Import only)"}, + {"value": "timetracker_to_quickbooks", "label": "TimeTracker → QuickBooks (Export only)"}, + {"value": "bidirectional", "label": "Bidirectional (Two-way sync)"}, + ], + "default": "timetracker_to_quickbooks", + "description": "Choose how data flows between QuickBooks and TimeTracker", + }, + { + "name": "sync_items", + "type": "array", + "label": "Items to Sync", + "options": [ + {"value": "invoices", "label": "Invoices"}, + {"value": "expenses", "label": "Expenses"}, + {"value": "payments", "label": "Payments"}, + {"value": "customers", "label": "Customers"}, + ], + "default": ["invoices", "expenses"], + "description": "Select which items to synchronize", + }, + {"name": "sync_invoices", "type": "boolean", "label": "Sync Invoices", "default": True, "description": "Enable invoice synchronization"}, + {"name": "sync_expenses", "type": "boolean", "label": "Sync Expenses", "default": True, "description": "Enable expense synchronization"}, + { + "name": "auto_sync", + "type": "boolean", + "label": "Auto Sync", + "default": False, + "description": "Automatically sync when invoices or expenses are created/updated", + }, + { + "name": "sync_interval", + "type": "select", + "label": "Sync Schedule", + "options": [ + {"value": "manual", "label": "Manual only"}, + {"value": "hourly", "label": "Every hour"}, + {"value": "daily", "label": "Daily"}, + ], + "default": "manual", + "description": "How often to automatically sync data", + }, { "name": "default_expense_account_id", "type": "string", "label": "Default Expense Account ID", - "description": "QuickBooks account ID to use for expenses when no mapping is configured", + "required": False, "default": "1", + "description": "QuickBooks account ID to use for expenses when no mapping is configured", + "help": "Find account IDs in QuickBooks Chart of Accounts", }, { "name": "customer_mappings", "type": "json", "label": "Customer Mappings", - "description": "JSON mapping of TimeTracker client IDs to QuickBooks customer IDs (e.g., {\"1\": \"qb_customer_id_123\"})", + "required": False, + "placeholder": '{"1": "qb_customer_id_123", "2": "qb_customer_id_456"}', + "description": "JSON mapping of TimeTracker client IDs to QuickBooks customer IDs", + "help": "Map your TimeTracker clients to QuickBooks customers. Format: {\"timetracker_client_id\": \"quickbooks_customer_id\"}", }, { "name": "item_mappings", "type": "json", "label": "Item Mappings", + "required": False, + "placeholder": '{"service_1": "qb_item_id_123"}', "description": "JSON mapping of TimeTracker invoice items to QuickBooks items", + "help": "Map your TimeTracker services/products to QuickBooks items", }, { "name": "account_mappings", "type": "json", "label": "Account Mappings", + "required": False, + "placeholder": '{"expense_category_1": "qb_account_id_123"}', "description": "JSON mapping of TimeTracker expense category IDs to QuickBooks account IDs", + "help": "Map your TimeTracker expense categories to QuickBooks accounts", }, ], "required": ["realm_id"], + "sections": [ + { + "title": "Connection Settings", + "description": "Configure your QuickBooks connection", + "fields": ["realm_id", "use_sandbox"], + }, + { + "title": "Sync Settings", + "description": "Configure what and how to sync", + "fields": ["sync_direction", "sync_items", "sync_invoices", "sync_expenses", "auto_sync", "sync_interval"], + }, + { + "title": "Data Mapping", + "description": "Map TimeTracker data to QuickBooks", + "fields": ["default_expense_account_id", "customer_mappings", "item_mappings", "account_mappings"], + }, + ], + "sync_settings": { + "enabled": True, + "auto_sync": False, + "sync_interval": "manual", + "sync_direction": "timetracker_to_quickbooks", + "sync_items": ["invoices", "expenses"], + }, } diff --git a/app/integrations/slack.py b/app/integrations/slack.py index d2bf603..d966c5a 100644 --- a/app/integrations/slack.py +++ b/app/integrations/slack.py @@ -267,3 +267,96 @@ class SlackConnector(BaseConnector): return {"success": True, "message": "Message sent successfully"} else: return {"success": False, "message": f"Slack API error: {data.get('error', 'Unknown error')}"} + + def get_config_schema(self) -> Dict[str, Any]: + """Get configuration schema.""" + return { + "fields": [ + { + "name": "sync_direction", + "type": "select", + "label": "Sync Direction", + "options": [ + {"value": "slack_to_timetracker", "label": "Slack → TimeTracker (Import only)"}, + {"value": "timetracker_to_slack", "label": "TimeTracker → Slack (Export only)"}, + {"value": "bidirectional", "label": "Bidirectional (Two-way sync)"}, + ], + "default": "slack_to_timetracker", + "description": "Choose how data flows between Slack and TimeTracker", + }, + { + "name": "sync_items", + "type": "array", + "label": "Items to Sync", + "options": [ + {"value": "channels", "label": "Channels"}, + {"value": "users", "label": "Users"}, + {"value": "messages", "label": "Messages (as tasks)"}, + ], + "default": ["channels", "users"], + "description": "Select which items to synchronize", + }, + { + "name": "notification_channel", + "type": "text", + "label": "Notification Channel", + "required": False, + "placeholder": "#general or channel-id", + "help": "Channel ID or name where TimeTracker notifications will be sent", + "description": "Default channel for notifications", + }, + { + "name": "auto_sync", + "type": "boolean", + "label": "Auto Sync", + "default": False, + "description": "Automatically sync when webhooks are received from Slack", + }, + { + "name": "sync_interval", + "type": "select", + "label": "Sync Schedule", + "options": [ + {"value": "manual", "label": "Manual only"}, + {"value": "hourly", "label": "Every hour"}, + {"value": "daily", "label": "Daily"}, + ], + "default": "manual", + "description": "How often to automatically sync data", + }, + { + "name": "notify_on_time_entry", + "type": "boolean", + "label": "Notify on Time Entry", + "default": False, + "description": "Send Slack notifications when time entries are created", + }, + { + "name": "notify_on_task_complete", + "type": "boolean", + "label": "Notify on Task Complete", + "default": False, + "description": "Send Slack notifications when tasks are completed", + }, + ], + "required": [], + "sections": [ + { + "title": "Sync Settings", + "description": "Configure what and how to sync", + "fields": ["sync_direction", "sync_items", "auto_sync", "sync_interval"], + }, + { + "title": "Notification Settings", + "description": "Configure Slack notifications", + "fields": ["notification_channel", "notify_on_time_entry", "notify_on_task_complete"], + }, + ], + "sync_settings": { + "enabled": True, + "auto_sync": False, + "sync_interval": "manual", + "sync_direction": "slack_to_timetracker", + "sync_items": ["channels", "users"], + }, + } diff --git a/app/integrations/trello.py b/app/integrations/trello.py index ec322ff..3d8ff0f 100644 --- a/app/integrations/trello.py +++ b/app/integrations/trello.py @@ -435,21 +435,95 @@ class TrelloConnector(BaseConnector): "fields": [ { "name": "board_ids", - "type": "array", + "type": "text", "label": "Board IDs", - "description": "Trello board IDs to sync (leave empty to sync all)", + "required": False, + "placeholder": "board-id-1, board-id-2", + "description": "Comma-separated list of Trello board IDs to sync (leave empty to sync all)", + "help": "Find board IDs in Trello board URLs or API. Leave empty to sync all accessible boards.", }, { "name": "sync_direction", "type": "select", "label": "Sync Direction", "options": [ - {"value": "trello_to_timetracker", "label": "Trello → TimeTracker"}, - {"value": "timetracker_to_trello", "label": "TimeTracker → Trello"}, - {"value": "bidirectional", "label": "Bidirectional"}, + {"value": "trello_to_timetracker", "label": "Trello → TimeTracker (Import only)"}, + {"value": "timetracker_to_trello", "label": "TimeTracker → Trello (Export only)"}, + {"value": "bidirectional", "label": "Bidirectional (Two-way sync)"}, ], "default": "trello_to_timetracker", + "description": "Choose how data flows between Trello and TimeTracker", + }, + { + "name": "sync_items", + "type": "array", + "label": "Items to Sync", + "options": [ + {"value": "boards", "label": "Boards (Projects)"}, + {"value": "cards", "label": "Cards (Tasks)"}, + {"value": "lists", "label": "Lists"}, + ], + "default": ["boards", "cards"], + "description": "Select which items to synchronize", + }, + { + "name": "auto_sync", + "type": "boolean", + "label": "Auto Sync", + "default": False, + "description": "Automatically sync when webhooks are received from Trello", + }, + { + "name": "sync_interval", + "type": "select", + "label": "Sync Schedule", + "options": [ + {"value": "manual", "label": "Manual only"}, + {"value": "hourly", "label": "Every hour"}, + {"value": "daily", "label": "Daily"}, + ], + "default": "manual", + "description": "How often to automatically sync data", + }, + { + "name": "list_status_mapping", + "type": "json", + "label": "List to Status Mapping", + "placeholder": '{"To Do": "todo", "In Progress": "in_progress", "Done": "completed"}', + "description": "Map Trello list names to TimeTracker task statuses (JSON format)", + "help": "Customize how Trello list names map to TimeTracker task statuses", + }, + { + "name": "sync_archived", + "type": "boolean", + "label": "Sync Archived Items", + "default": False, + "description": "Include archived boards and cards in sync", }, ], "required": [], + "sections": [ + { + "title": "Board Settings", + "description": "Configure which Trello boards to sync", + "fields": ["board_ids", "sync_archived"], + }, + { + "title": "Sync Settings", + "description": "Configure what and how to sync", + "fields": ["sync_direction", "sync_items", "auto_sync", "sync_interval"], + }, + { + "title": "Data Mapping", + "description": "Customize how data translates between Trello and TimeTracker", + "fields": ["list_status_mapping"], + }, + ], + "sync_settings": { + "enabled": True, + "auto_sync": False, + "sync_interval": "manual", + "sync_direction": "trello_to_timetracker", + "sync_items": ["boards", "cards"], + }, } diff --git a/app/integrations/xero.py b/app/integrations/xero.py index 9354faa..3a89180 100644 --- a/app/integrations/xero.py +++ b/app/integrations/xero.py @@ -357,35 +357,117 @@ class XeroConnector(BaseConnector): "name": "tenant_id", "type": "string", "label": "Tenant ID", + "required": True, + "placeholder": "tenant-uuid-123", "description": "Xero organisation tenant ID", + "help": "Find your tenant ID in Xero after connecting. It's automatically set during OAuth.", + }, + { + "name": "sync_direction", + "type": "select", + "label": "Sync Direction", + "options": [ + {"value": "xero_to_timetracker", "label": "Xero → TimeTracker (Import only)"}, + {"value": "timetracker_to_xero", "label": "TimeTracker → Xero (Export only)"}, + {"value": "bidirectional", "label": "Bidirectional (Two-way sync)"}, + ], + "default": "timetracker_to_xero", + "description": "Choose how data flows between Xero and TimeTracker", + }, + { + "name": "sync_items", + "type": "array", + "label": "Items to Sync", + "options": [ + {"value": "invoices", "label": "Invoices"}, + {"value": "expenses", "label": "Expenses"}, + {"value": "payments", "label": "Payments"}, + {"value": "contacts", "label": "Contacts"}, + ], + "default": ["invoices", "expenses"], + "description": "Select which items to synchronize", + }, + {"name": "sync_invoices", "type": "boolean", "label": "Sync Invoices", "default": True, "description": "Enable invoice synchronization"}, + {"name": "sync_expenses", "type": "boolean", "label": "Sync Expenses", "default": True, "description": "Enable expense synchronization"}, + { + "name": "auto_sync", + "type": "boolean", + "label": "Auto Sync", + "default": False, + "description": "Automatically sync when invoices or expenses are created/updated", + }, + { + "name": "sync_interval", + "type": "select", + "label": "Sync Schedule", + "options": [ + {"value": "manual", "label": "Manual only"}, + {"value": "hourly", "label": "Every hour"}, + {"value": "daily", "label": "Daily"}, + ], + "default": "manual", + "description": "How often to automatically sync data", }, - {"name": "sync_invoices", "type": "boolean", "label": "Sync Invoices", "default": True}, - {"name": "sync_expenses", "type": "boolean", "label": "Sync Expenses", "default": True}, { "name": "default_expense_account_code", "type": "string", "label": "Default Expense Account Code", - "description": "Xero account code to use for expenses when no mapping is configured", + "required": False, "default": "200", + "description": "Xero account code to use for expenses when no mapping is configured", + "help": "Find account codes in Xero Chart of Accounts", }, { "name": "contact_mappings", "type": "json", "label": "Contact Mappings", - "description": "JSON mapping of TimeTracker client IDs to Xero Contact IDs (e.g., {\"1\": \"contact-uuid-123\"})", + "required": False, + "placeholder": '{"1": "contact-uuid-123", "2": "contact-uuid-456"}', + "description": "JSON mapping of TimeTracker client IDs to Xero Contact IDs", + "help": "Map your TimeTracker clients to Xero contacts. Format: {\"timetracker_client_id\": \"xero_contact_id\"}", }, { "name": "item_mappings", "type": "json", "label": "Item Mappings", + "required": False, + "placeholder": '{"service_1": "item_code_123"}', "description": "JSON mapping of TimeTracker invoice items to Xero item codes", + "help": "Map your TimeTracker services/products to Xero items", }, { "name": "account_mappings", "type": "json", "label": "Account Mappings", + "required": False, + "placeholder": '{"expense_category_1": "account_code_200"}', "description": "JSON mapping of TimeTracker expense category IDs to Xero account codes", + "help": "Map your TimeTracker expense categories to Xero accounts", }, ], "required": ["tenant_id"], + "sections": [ + { + "title": "Connection Settings", + "description": "Configure your Xero connection", + "fields": ["tenant_id"], + }, + { + "title": "Sync Settings", + "description": "Configure what and how to sync", + "fields": ["sync_direction", "sync_items", "sync_invoices", "sync_expenses", "auto_sync", "sync_interval"], + }, + { + "title": "Data Mapping", + "description": "Map TimeTracker data to Xero", + "fields": ["default_expense_account_code", "contact_mappings", "item_mappings", "account_mappings"], + }, + ], + "sync_settings": { + "enabled": True, + "auto_sync": False, + "sync_interval": "manual", + "sync_direction": "timetracker_to_xero", + "sync_items": ["invoices", "expenses"], + }, } diff --git a/app/routes/integrations.py b/app/routes/integrations.py index 89ca49c..0d87c33 100644 --- a/app/routes/integrations.py +++ b/app/routes/integrations.py @@ -386,6 +386,30 @@ def manage_integration(provider): if field_type == "boolean": # Checkboxes: present = True, absent = False value = field_name in request.form + elif field_type == "array": + # Array fields - get all selected values + values = request.form.getlist(field_name) + value = values if values else field.get("default", []) + elif field_type == "select": + # Select fields - single value + value = request.form.get(field_name, "").strip() + if not value: + value = field.get("default") + elif field_type == "number": + # Number fields - convert to int/float + value_str = request.form.get(field_name, "").strip() + if value_str: + try: + # Try int first, then float + if "." in value_str: + value = float(value_str) + else: + value = int(value_str) + except ValueError: + flash(_("Invalid number for field %(field)s", field=field.get("label", field_name)), "error") + continue + else: + value = field.get("default") elif field_type == "json": # JSON fields - parse if provided value_str = request.form.get(field_name, "").strip() @@ -399,17 +423,19 @@ def manage_integration(provider): else: value = None else: - # String/number/url fields + # String/url/text fields value = request.form.get(field_name, "").strip() if not value and field.get("required", False): flash(_("Field %(field)s is required", field=field.get("label", field_name)), "error") continue + if not value: + value = field.get("default") - # Only update if value is provided or it's a boolean (always set) + # Only update if value is provided or it's a boolean/array (always set) if value is not None and value != "": integration_to_update.config[field_name] = value - elif field_type == "boolean": - # Always set boolean fields + elif field_type in ("boolean", "array"): + # Always set boolean and array fields integration_to_update.config[field_name] = value # Ensure config is marked as modified @@ -458,6 +484,25 @@ def manage_integration(provider): display_name = getattr(connector_class, "display_name", None) or provider.replace("_", " ").title() description = getattr(connector_class, "description", None) or "" + # Get config schema from connector + config_schema = {} + current_config = {} + active_integration = integration if integration else user_integration + + if active_integration: + current_config = active_integration.config or {} + if connector: + try: + config_schema = connector.get_config_schema() + except Exception as e: + logger.warning(f"Could not get config schema for {provider}: {e}") + elif connector_class and hasattr(connector_class, "get_config_schema"): + try: + temp_connector = connector_class(active_integration, None) + config_schema = temp_connector.get_config_schema() + except Exception as e: + logger.warning(f"Could not get config schema for {provider}: {e}") + return render_template( "integrations/manage.html", provider=provider, @@ -466,11 +511,14 @@ def manage_integration(provider): connector_error=connector_error, integration=integration, user_integration=user_integration, + active_integration=active_integration, credentials=credentials, current_creds=current_creds, display_name=display_name, description=description, is_global=is_global, + config_schema=config_schema, + current_config=current_config, ) diff --git a/app/templates/integrations/manage.html b/app/templates/integrations/manage.html index 1590ca3..fbe307e 100644 --- a/app/templates/integrations/manage.html +++ b/app/templates/integrations/manage.html @@ -173,71 +173,242 @@
+ {{ _('Configure sync settings, data mappings, and other integration options.') }} +
+