From 9ad5e9e0da523177160f7d032bd90bc70f62d64c Mon Sep 17 00:00:00 2001 From: Mamerto Fabian Jr Date: Sun, 29 Dec 2024 12:47:41 +0800 Subject: [PATCH 1/2] feat: enhance GitHub repository management in GitHubSettings component - Added repository search functionality with dropdown selection for user repositories. - Implemented loading state and error handling for fetching repositories from GitHub. - Improved user experience with real-time feedback on repository existence and creation prompts. - Updated GitHubService to include a method for listing user repositories, supporting both user and organization endpoints. - Enhanced UI with loading indicators and contextual messages for repository input. These changes significantly improve the usability and functionality of the GitHubSettings component, allowing users to easily manage and select repositories. --- src/lib/components/GitHubSettings.svelte | 186 +++++++++++++++++++++-- src/services/GitHubService.ts | 48 +++++- 2 files changed, 221 insertions(+), 13 deletions(-) diff --git a/src/lib/components/GitHubSettings.svelte b/src/lib/components/GitHubSettings.svelte index 3dccacd..fc0f7c9 100644 --- a/src/lib/components/GitHubSettings.svelte +++ b/src/lib/components/GitHubSettings.svelte @@ -12,6 +12,8 @@ ChevronDown, Check, X, + Search, + Loader2, } from "lucide-svelte"; import { onMount } from 'svelte'; import { GitHubService } from "../../services/GitHubService"; @@ -37,11 +39,108 @@ let isTokenValid: boolean | null = null; let tokenValidationTimeout: number; let validationError: string | null = null; + let repositories: Array<{ + name: string; + description: string | null; + html_url: string; + private: boolean; + created_at: string; + updated_at: string; + language: string | null; + }> = []; + let isLoadingRepos = false; + let showRepoDropdown = false; + let repoSearchQuery = ""; + let repoInputFocused = false; + let repoExists = false; + let selectedIndex = -1; - onMount(() => { + $: filteredRepos = repositories + .filter(repo => + repo.name.toLowerCase().includes(repoSearchQuery.toLowerCase()) || + (repo.description && repo.description.toLowerCase().includes(repoSearchQuery.toLowerCase())) + ) + .slice(0, 10); + + $: if (repoName) { + repoExists = repositories.some(repo => repo.name.toLowerCase() === repoName.toLowerCase()); + } + + async function loadRepositories() { + if (!githubToken || !repoOwner || !isTokenValid) return; + + try { + isLoadingRepos = true; + const githubService = new GitHubService(githubToken); + repositories = await githubService.listUserRepositories(repoOwner); + } catch (error) { + console.error('Error loading repositories:', error); + repositories = []; + } finally { + isLoadingRepos = false; + } + } + + function handleRepoInput() { + repoSearchQuery = repoName; + onInput(); + } + + function selectRepo(repo: typeof repositories[0]) { + repoName = repo.name; + showRepoDropdown = false; + repoSearchQuery = repo.name; + onInput(); + } + + function handleRepoKeydown(event: KeyboardEvent) { + if (!showRepoDropdown) return; + + switch (event.key) { + case 'ArrowDown': + event.preventDefault(); + selectedIndex = Math.min(selectedIndex + 1, filteredRepos.length - 1); + break; + case 'ArrowUp': + event.preventDefault(); + selectedIndex = Math.max(selectedIndex - 1, -1); + break; + case 'Enter': + event.preventDefault(); + if (selectedIndex >= 0 && filteredRepos[selectedIndex]) { + selectRepo(filteredRepos[selectedIndex]); + } + break; + case 'Escape': + event.preventDefault(); + showRepoDropdown = false; + break; + } + } + + function handleRepoFocus() { + repoInputFocused = true; + showRepoDropdown = true; + repoSearchQuery = repoName; + } + + function handleRepoBlur() { + repoInputFocused = false; + // Delay hiding dropdown to allow click events to register + setTimeout(() => { + showRepoDropdown = false; + }, 200); + } + + onMount(async () => { chrome.storage.local.get(['showNewUserGuide'], (result) => { showNewUserGuide = result.showNewUserGuide ?? true; }); + + // If we have initial valid settings, validate and load repos + if (githubToken && repoOwner) { + await validateSettings(); + } }); function toggleNewUserGuide() { @@ -63,6 +162,11 @@ const result = await githubService.validateTokenAndUser(repoOwner); isTokenValid = result.isValid; validationError = result.error || null; + + // Load repositories after successful validation + if (result.isValid) { + await loadRepositories(); + } } catch (error) { console.error('Error validating settings:', error); isTokenValid = false; @@ -238,14 +342,75 @@ {/if} - +
+
+ +
+ {#if isLoadingRepos} + + {:else} + + {/if} +
+
+ {#if showRepoDropdown && (filteredRepos.length > 0 || !repoExists)} +
+
    + {#each filteredRepos as repo, i} +
  • + +
  • + {/each} + {#if !repoExists} +
  • + {#if repoName.length > 0} +

    💡If the repository "{repoName}" doesn't exist, it will be created automatically.

    +

    ✨ If it's a private repository, you can still enter it manually even if it's not visible in the list.

    + {:else} +

    Enter a repository name (new or private) or select from your public repositories

    + {/if} +
  • + {/if} +
+
+ {/if} +
+ {#if repoExists} +

+ ℹ️ Using existing repository +

+ {:else if repoName} +

+ ✨ A new repository will be created if it doesn't exist yet. +

+

+ ⚠️ You can push to private repositories, but loading it into Bolt will fail. +

+ {/if}
@@ -262,6 +427,9 @@ class="bg-slate-800 border-slate-700 text-slate-200 placeholder:text-slate-500" />
+

+ 💡 If the branch doesn't exist, it will be created automatically from the default branch. +