mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-12 07:19:49 -05:00
feat: comprehensive integration setup and configuration system
Enhanced all integrations with complete setup procedures, authentication flows, and comprehensive configuration management. Base Connector Enhancements: - Extended get_config_schema() with sections, sync settings, and validation - Added validate_config() with type checking and constraints - Added helper methods: get_sync_settings(), get_field_mappings(), get_status_mappings() Integration Configuration Schemas: - All integrations now have complete config schemas with organized sections - Support for sync direction (Import/Export/Bidirectional) - Sync scheduling options (Manual/Hourly/Daily/Weekly) - Data mapping configuration (status mappings, field mappings) - Field types: string, number, boolean, select, array, json, url, text, password - Comprehensive help text and descriptions for all fields Enhanced Integrations: - Jira: JQL queries, status mapping, project auto-creation - GitHub: Repository selection, issue state filtering, webhook security - GitLab: Project selection, issue filtering, webhook configuration - Slack: Channel selection, notification triggers - Asana: Workspace/project selection, completion status sync - Trello: Board selection, list-to-status mapping - Microsoft Teams: Channel configuration, notification settings - QuickBooks: Customer/item/account mappings, sandbox mode - Xero: Contact/item/account mappings - Google Calendar: Event formatting, date range controls - Outlook Calendar: Event formatting, date range controls - CalDAV: Server discovery, SSL verification, lookback/lookahead UI Enhancements: - Section-based configuration display - Support for all field types (select, array, number, json, boolean) - Improved help text and descriptions - Better visual organization and validation Route Enhancements: - Config schema passed to template - Form processing for all field types - Proper default value handling - Validation error messages This provides a complete, user-friendly integration setup experience with one-button OAuth connections, configurable sync settings, and data translation capabilities.
This commit is contained in:
@@ -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"],
|
||||
},
|
||||
}
|
||||
|
||||
+145
-3
@@ -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", {})
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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": [],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -173,71 +173,242 @@
|
||||
<h2 class="text-lg font-semibold mb-4">
|
||||
<i class="fas fa-cog mr-2"></i>{{ _('Integration Configuration') }}
|
||||
</h2>
|
||||
<p class="text-sm text-text-muted-light dark:text-text-muted-dark mb-6">
|
||||
{{ _('Configure sync settings, data mappings, and other integration options.') }}
|
||||
</p>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="action" value="update_config">
|
||||
|
||||
<div class="space-y-4">
|
||||
{% for field in config_schema.fields %}
|
||||
{% set field_name = field.name %}
|
||||
{% set field_type = field.get('type', 'string') %}
|
||||
{% set field_value = current_config.get(field_name, field.get('default', '')) %}
|
||||
|
||||
<div>
|
||||
<label for="config_{{ field_name }}" class="block text-sm font-medium mb-2">
|
||||
{{ field.get('label', field_name) }}
|
||||
{% if field.get('required', False) %}
|
||||
<span class="text-red-500">*</span>
|
||||
{% if config_schema.get('sections') %}
|
||||
<!-- Display fields organized by sections -->
|
||||
{% for section in config_schema.sections %}
|
||||
<div class="mb-8 last:mb-0">
|
||||
<div class="border-b border-border-light dark:border-border-dark pb-2 mb-4">
|
||||
<h3 class="text-md font-semibold text-text-light dark:text-text-dark">
|
||||
{{ section.get('title', 'Settings') }}
|
||||
</h3>
|
||||
{% if section.get('description') %}
|
||||
<p class="text-sm text-text-muted-light dark:text-text-muted-dark mt-1">
|
||||
{{ section.get('description') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</label>
|
||||
|
||||
{% if field_type == 'boolean' %}
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
{% if field_value %}checked{% endif %}
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary">
|
||||
<label for="config_{{ field_name }}" class="ml-2 text-sm text-text-muted-light dark:text-text-muted-dark">
|
||||
{{ field.get('description', '') }}
|
||||
</label>
|
||||
</div>
|
||||
{% elif field_type == 'text' or field_type == 'textarea' %}
|
||||
<textarea
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
rows="{% if field_type == 'textarea' %}4{% else %}2{% endif %}"
|
||||
class="w-full px-3 py-2 border border-border-light dark:border-border-dark rounded-lg bg-card-light dark:bg-card-dark"
|
||||
placeholder="{{ field.get('placeholder', '') }}"
|
||||
{% if field.get('required', False) %}required{% endif %}>{{ field_value }}</textarea>
|
||||
{% elif field_type == 'json' %}
|
||||
<textarea
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
rows="4"
|
||||
class="w-full px-3 py-2 border border-border-light dark:border-border-dark rounded-lg bg-card-light dark:bg-card-dark font-mono text-sm"
|
||||
placeholder='{{ field.get('placeholder', '{}') }}'
|
||||
{% if field.get('required', False) %}required{% endif %}>{% if field_value %}{{ field_value|tojson }}{% endif %}</textarea>
|
||||
{% else %}
|
||||
<input type="{{ field_type }}"
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
value="{{ field_value }}"
|
||||
class="w-full px-3 py-2 border border-border-light dark:border-border-dark rounded-lg bg-card-light dark:bg-card-dark"
|
||||
placeholder="{{ field.get('placeholder', '') }}"
|
||||
{% if field.get('required', False) %}required{% endif %}>
|
||||
{% endif %}
|
||||
|
||||
{% if field.get('help') or field.get('description') %}
|
||||
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">
|
||||
{{ field.get('help', field.get('description', '')) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="space-y-4">
|
||||
{% for field_name in section.get('fields', []) %}
|
||||
{% set field = config_schema.fields|selectattr('name', 'equalto', field_name)|first %}
|
||||
{% if field %}
|
||||
{% set field_type = field.get('type', 'string') %}
|
||||
{% set field_value = current_config.get(field_name, field.get('default', '')) %}
|
||||
|
||||
<div>
|
||||
<label for="config_{{ field_name }}" class="block text-sm font-medium mb-2">
|
||||
{{ field.get('label', field_name) }}
|
||||
{% if field.get('required', False) %}
|
||||
<span class="text-red-500">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
|
||||
{% if field_type == 'boolean' %}
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
{% if field_value %}checked{% endif %}
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary">
|
||||
<label for="config_{{ field_name }}" class="ml-2 text-sm text-text-muted-light dark:text-text-muted-dark">
|
||||
{{ field.get('description', '') }}
|
||||
</label>
|
||||
</div>
|
||||
{% elif field_type == 'select' %}
|
||||
<select name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
class="w-full px-3 py-2 border border-border-light dark:border-border-dark rounded-lg bg-card-light dark:bg-card-dark"
|
||||
{% if field.get('required', False) %}required{% endif %}>
|
||||
{% for option in field.get('options', []) %}
|
||||
<option value="{{ option.get('value', option.get('label', '')) }}"
|
||||
{% if field_value == option.get('value', option.get('label', '')) or (not field_value and option.get('value', option.get('label', '')) == field.get('default')) %}selected{% endif %}>
|
||||
{{ option.get('label', option.get('value', '')) }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% elif field_type == 'array' %}
|
||||
<div class="space-y-2">
|
||||
{% for option in field.get('options', []) %}
|
||||
{% set option_value = option.get('value', option.get('label', '')) %}
|
||||
{% set is_selected = field_value and option_value in field_value %}
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}_{{ loop.index }}"
|
||||
value="{{ option_value }}"
|
||||
{% if is_selected or (not field_value and option_value in field.get('default', [])) %}checked{% endif %}
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary">
|
||||
<label for="config_{{ field_name }}_{{ loop.index }}" class="ml-2 text-sm text-text-light dark:text-text-dark">
|
||||
{{ option.get('label', option_value) }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif field_type == 'number' %}
|
||||
<input type="number"
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
value="{{ field_value }}"
|
||||
{% if field.get('validation') %}
|
||||
{% if field.validation.get('min') is not none %}min="{{ field.validation.min }}"{% endif %}
|
||||
{% if field.validation.get('max') is not none %}max="{{ field.validation.max }}"{% endif %}
|
||||
{% endif %}
|
||||
class="w-full px-3 py-2 border border-border-light dark:border-border-dark rounded-lg bg-card-light dark:bg-card-dark"
|
||||
placeholder="{{ field.get('placeholder', '') }}"
|
||||
{% if field.get('required', False) %}required{% endif %}>
|
||||
{% elif field_type == 'text' or field_type == 'textarea' %}
|
||||
<textarea
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
rows="{% if field_type == 'textarea' %}4{% else %}2{% endif %}"
|
||||
class="w-full px-3 py-2 border border-border-light dark:border-border-dark rounded-lg bg-card-light dark:bg-card-dark"
|
||||
placeholder="{{ field.get('placeholder', '') }}"
|
||||
{% if field.get('required', False) %}required{% endif %}>{{ field_value }}</textarea>
|
||||
{% elif field_type == 'json' %}
|
||||
<textarea
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
rows="6"
|
||||
class="w-full px-3 py-2 border border-border-light dark:border-border-dark rounded-lg bg-card-light dark:bg-card-dark font-mono text-sm"
|
||||
placeholder="{{ field.get('placeholder', '{}') }}"
|
||||
{% if field.get('required', False) %}required{% endif %}>{% if field_value %}{{ field_value|tojson|safe }}{% endif %}</textarea>
|
||||
{% else %}
|
||||
<input type="{{ field_type }}"
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
value="{{ field_value }}"
|
||||
class="w-full px-3 py-2 border border-border-light dark:border-border-dark rounded-lg bg-card-light dark:bg-card-dark"
|
||||
placeholder="{{ field.get('placeholder', '') }}"
|
||||
{% if field.get('required', False) %}required{% endif %}>
|
||||
{% endif %}
|
||||
|
||||
{% if field.get('help') or field.get('description') %}
|
||||
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
{{ field.get('help', field.get('description', '')) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Fallback: Display all fields without sections -->
|
||||
<div class="space-y-4">
|
||||
{% for field in config_schema.fields %}
|
||||
{% set field_name = field.name %}
|
||||
{% set field_type = field.get('type', 'string') %}
|
||||
{% set field_value = current_config.get(field_name, field.get('default', '')) %}
|
||||
|
||||
<div>
|
||||
<label for="config_{{ field_name }}" class="block text-sm font-medium mb-2">
|
||||
{{ field.get('label', field_name) }}
|
||||
{% if field.get('required', False) %}
|
||||
<span class="text-red-500">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
|
||||
{% if field_type == 'boolean' %}
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
{% if field_value %}checked{% endif %}
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary">
|
||||
<label for="config_{{ field_name }}" class="ml-2 text-sm text-text-muted-light dark:text-text-muted-dark">
|
||||
{{ field.get('description', '') }}
|
||||
</label>
|
||||
</div>
|
||||
{% elif field_type == 'select' %}
|
||||
<select name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
class="w-full px-3 py-2 border border-border-light dark:border-border-dark rounded-lg bg-card-light dark:bg-card-dark"
|
||||
{% if field.get('required', False) %}required{% endif %}>
|
||||
{% for option in field.get('options', []) %}
|
||||
<option value="{{ option.get('value', option.get('label', '')) }}"
|
||||
{% if field_value == option.get('value', option.get('label', '')) or (not field_value and option.get('value', option.get('label', '')) == field.get('default')) %}selected{% endif %}>
|
||||
{{ option.get('label', option.get('value', '')) }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% elif field_type == 'array' %}
|
||||
<div class="space-y-2">
|
||||
{% for option in field.get('options', []) %}
|
||||
{% set option_value = option.get('value', option.get('label', '')) %}
|
||||
{% set is_selected = field_value and option_value in field_value %}
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}_{{ loop.index }}"
|
||||
value="{{ option_value }}"
|
||||
{% if is_selected or (not field_value and option_value in field.get('default', [])) %}checked{% endif %}
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary">
|
||||
<label for="config_{{ field_name }}_{{ loop.index }}" class="ml-2 text-sm text-text-light dark:text-text-dark">
|
||||
{{ option.get('label', option_value) }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif field_type == 'number' %}
|
||||
<input type="number"
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
value="{{ field_value }}"
|
||||
{% if field.get('validation') %}
|
||||
{% if field.validation.get('min') is not none %}min="{{ field.validation.min }}"{% endif %}
|
||||
{% if field.validation.get('max') is not none %}max="{{ field.validation.max }}"{% endif %}
|
||||
{% endif %}
|
||||
class="w-full px-3 py-2 border border-border-light dark:border-border-dark rounded-lg bg-card-light dark:bg-card-dark"
|
||||
placeholder="{{ field.get('placeholder', '') }}"
|
||||
{% if field.get('required', False) %}required{% endif %}>
|
||||
{% elif field_type == 'text' or field_type == 'textarea' %}
|
||||
<textarea
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
rows="{% if field_type == 'textarea' %}4{% else %}2{% endif %}"
|
||||
class="w-full px-3 py-2 border border-border-light dark:border-border-dark rounded-lg bg-card-light dark:bg-card-dark"
|
||||
placeholder="{{ field.get('placeholder', '') }}"
|
||||
{% if field.get('required', False) %}required{% endif %}>{{ field_value }}</textarea>
|
||||
{% elif field_type == 'json' %}
|
||||
<textarea
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
rows="6"
|
||||
class="w-full px-3 py-2 border border-border-light dark:border-border-dark rounded-lg bg-card-light dark:bg-card-dark font-mono text-sm"
|
||||
placeholder="{{ field.get('placeholder', '{}') }}"
|
||||
{% if field.get('required', False) %}required{% endif %}>{% if field_value %}{{ field_value|tojson|safe }}{% endif %}</textarea>
|
||||
{% else %}
|
||||
<input type="{{ field_type }}"
|
||||
name="{{ field_name }}"
|
||||
id="config_{{ field_name }}"
|
||||
value="{{ field_value }}"
|
||||
class="w-full px-3 py-2 border border-border-light dark:border-border-dark rounded-lg bg-card-light dark:bg-card-dark"
|
||||
placeholder="{{ field.get('placeholder', '') }}"
|
||||
{% if field.get('required', False) %}required{% endif %}>
|
||||
{% endif %}
|
||||
|
||||
{% if field.get('help') or field.get('description') %}
|
||||
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
{{ field.get('help', field.get('description', '')) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="mt-6 pt-6 border-t border-border-light dark:border-border-dark">
|
||||
<button type="submit" class="bg-primary text-white px-6 py-2 rounded-lg hover:bg-primary/90 transition-colors">
|
||||
<i class="fas fa-save mr-2"></i>{{ _('Save Configuration') }}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user