diff --git a/backend/airweave/platform/configs/config.py b/backend/airweave/platform/configs/config.py index 2b184134a..0444f697f 100644 --- a/backend/airweave/platform/configs/config.py +++ b/backend/airweave/platform/configs/config.py @@ -1,6 +1,6 @@ """Configuration classes for platform components.""" -from typing import Optional +from typing import Any, Optional from pydantic import Field, field_validator @@ -58,14 +58,13 @@ class BitbucketConfig(SourceConfig): @field_validator("file_extensions", mode="before") @classmethod - def parse_file_extensions(cls, value): + def parse_file_extensions(cls, value: Any) -> list[str]: """Convert string input to list if needed.""" if isinstance(value, str): if not value.strip(): return [] - # Split by commas and strip whitespace return [ext.strip() for ext in value.split(",") if ext.strip()] - return value + return value # type: ignore[no-any-return] class BoxConfig(SourceConfig): @@ -150,9 +149,11 @@ class GitHubConfig(SourceConfig): repo_name: str = Field( title="Repository Name", - description="Repository to sync in owner/repo format (e.g., 'airweave-ai/airweave')", + description=( + "Repository to sync in owner/repo format (e.g., 'airweave-ai/airweave') " + "or full GitHub URL (e.g., 'https://github.com/airweave-ai/airweave')" + ), min_length=3, - pattern=r"^[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+$", ) branch: str = Field( default="", @@ -171,6 +172,18 @@ class GitHubConfig(SourceConfig): ), ) + @field_validator("repo_name", mode="before") + @classmethod + def normalize_repo_name(cls, v: str) -> str: + """Strip GitHub URL prefix if provided, normalizing to owner/repo format.""" + if isinstance(v, str): + v = v.strip().rstrip("/") + for prefix in ("https://github.com/", "http://github.com/"): + if v.lower().startswith(prefix): + v = v[len(prefix) :] + break + return v + @field_validator("repo_name") @classmethod def validate_repo_name(cls, v: str) -> str: @@ -259,21 +272,20 @@ class GmailConfig(SourceConfig): @field_validator("included_labels", "excluded_labels", "excluded_categories", mode="before") @classmethod - def parse_list_fields(cls, value): + def parse_list_fields(cls, value: Any) -> list[str]: """Convert comma-separated string to list if needed.""" if isinstance(value, str): if not value.strip(): return [] return [item.strip() for item in value.split(",") if item.strip()] - return value + return value # type: ignore[no-any-return] @field_validator("after_date") @classmethod - def validate_date_format(cls, value): + def validate_date_format(cls, value: Optional[str]) -> Optional[str]: """Validate date format and convert to YYYY/MM/DD.""" if not value: return value - # Accept both YYYY/MM/DD and YYYY-MM-DD formats return value.replace("-", "/") @@ -314,10 +326,10 @@ class GoogleDriveConfig(SourceConfig): @field_validator("include_patterns", mode="before") @classmethod - def _parse_include_patterns(cls, value): + def _parse_include_patterns(cls, value: Any) -> list[str]: if isinstance(value, str): return [p.strip() for p in value.split(",") if p.strip()] - return value + return value # type: ignore[no-any-return] class GoogleSlidesConfig(SourceConfig): @@ -450,21 +462,20 @@ class OutlookMailConfig(SourceConfig): @field_validator("included_folders", "excluded_folders", mode="before") @classmethod - def parse_list_fields(cls, value): + def parse_list_fields(cls, value: Any) -> list[str]: """Convert comma-separated string to list if needed.""" if isinstance(value, str): if not value.strip(): return [] return [item.strip() for item in value.split(",") if item.strip()] - return value + return value # type: ignore[no-any-return] @field_validator("after_date") @classmethod - def validate_date_format(cls, value): + def validate_date_format(cls, value: Optional[str]) -> Optional[str]: """Validate date format and convert to YYYY/MM/DD.""" if not value: return value - # Accept both YYYY/MM/DD and YYYY-MM-DD formats return value.replace("-", "/") @@ -506,7 +517,7 @@ class CTTIConfig(SourceConfig): @field_validator("limit", mode="before") @classmethod - def parse_limit(cls, value): + def parse_limit(cls, value: Any) -> int: """Convert string input to integer if needed.""" if isinstance(value, str): if not value.strip(): @@ -515,11 +526,11 @@ def parse_limit(cls, value): return int(value.strip()) except ValueError as e: raise ValueError("Limit must be a valid integer") from e - return value + return value # type: ignore[no-any-return] @field_validator("skip", mode="before") @classmethod - def parse_skip(cls, value): + def parse_skip(cls, value: Any) -> int: """Convert string input to integer if needed.""" if isinstance(value, str): if not value.strip(): @@ -537,7 +548,7 @@ def parse_skip(cls, value): if value < 0: raise ValueError("Skip must be non-negative") return int(value) - return value + return value # type: ignore[no-any-return] class SharePointConfig(SourceConfig): @@ -649,14 +660,14 @@ class SalesforceConfig(SourceConfig): @field_validator("instance_url", mode="before") @classmethod - def strip_https_prefix(cls, value): + def strip_https_prefix(cls, value: Any) -> Optional[str]: """Remove https:// or http:// prefix if present.""" if isinstance(value, str): if value.startswith("https://"): return value.replace("https://", "", 1) elif value.startswith("http://"): return value.replace("http://", "", 1) - return value + return value # type: ignore[no-any-return] class TodoistConfig(SourceConfig): @@ -800,7 +811,7 @@ class StubConfig(SourceConfig): mode="before", ) @classmethod - def parse_weight(cls, value): + def parse_weight(cls, value: Any) -> int: """Convert string input to integer if needed.""" if isinstance(value, str): if not value.strip(): @@ -809,7 +820,7 @@ def parse_weight(cls, value): return int(value.strip()) except ValueError as e: raise ValueError("Weight must be a valid integer") from e - return value + return value # type: ignore[no-any-return] class IncrementalStubConfig(SourceConfig): diff --git a/frontend/src/lib/validation/rules.ts b/frontend/src/lib/validation/rules.ts index 6c945014b..b836337cb 100644 --- a/frontend/src/lib/validation/rules.ts +++ b/frontend/src/lib/validation/rules.ts @@ -434,24 +434,32 @@ export const redirectUrlValidation: FieldValidation = { }; /** - * Repository name validation (owner/repo format) + * Repository name validation (owner/repo format or full GitHub URL) */ export const repoNameValidation: FieldValidation = { field: 'repo_name', debounceMs: 500, showOn: 'change', validate: (value: string): ValidationResult => { - const trimmed = value.trim(); + let trimmed = value.trim().replace(/\/+$/, ''); if (!trimmed) { return { isValid: true, severity: 'info' }; } + // Strip GitHub URL prefix if present + for (const prefix of ['https://github.com/', 'http://github.com/']) { + if (trimmed.toLowerCase().startsWith(prefix)) { + trimmed = trimmed.slice(prefix.length); + break; + } + } + // Must contain a slash if (!trimmed.includes('/')) { return { isValid: false, - hint: 'Repository must be in owner/repo format (e.g., airweave-ai/airweave)', + hint: 'Use owner/repo format (e.g., airweave-ai/airweave) or a full GitHub URL', severity: 'warning' }; } @@ -460,7 +468,7 @@ export const repoNameValidation: FieldValidation = { if (parts.length !== 2) { return { isValid: false, - hint: 'Repository must be in owner/repo format (e.g., airweave-ai/airweave)', + hint: 'Use owner/repo format (e.g., airweave-ai/airweave) or a full GitHub URL', severity: 'warning' }; }