Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bce2dce
fetch all branches via pagination
HarshMN2345 Jun 11, 2026
80242cb
fetch all branches via pagination in all branch selectors
HarshMN2345 Jun 11, 2026
ae75d14
simplify branch fetch
HarshMN2345 Jun 11, 2026
81bf3b9
add custom branch selector with search and load more
HarshMN2345 Jun 11, 2026
1848991
replace ComboBox with BranchSelector in settings pages
HarshMN2345 Jun 11, 2026
253d222
redesign branch selector to match github style
HarshMN2345 Jun 11, 2026
586624f
simplify branch selector, add search hint
HarshMN2345 Jun 11, 2026
072523d
match combobox style
HarshMN2345 Jun 11, 2026
34023e5
Revert "match combobox style"
HarshMN2345 Jun 11, 2026
9a85432
replace branch ComboBox with BranchSelector in modals
HarshMN2345 Jun 11, 2026
79f6bbd
fix loading flags and cache invalidation
HarshMN2345 Jun 11, 2026
3e6c24d
replace ComboBox with BranchSelector in add-domain pages
HarshMN2345 Jun 11, 2026
961ad5f
remove unused imports
HarshMN2345 Jun 11, 2026
8a5fd4b
fix type errors in spatial column defaults
HarshMN2345 Jun 11, 2026
b67bfe5
fix on:select handler in branch selector modals
HarshMN2345 Jun 11, 2026
c13cf2e
fix dropdown clipping in modals using fixed positioning
HarshMN2345 Jun 11, 2026
49ecdd2
use Input.ComboBox for proper dialog portal support
HarshMN2345 Jun 11, 2026
65c0d99
use melt-ui createCombobox for proper dialog portal support
HarshMN2345 Jun 11, 2026
093b81a
fix infinite loading loop
HarshMN2345 Jun 11, 2026
b475567
fix infinite loading with loaded flag
HarshMN2345 Jun 11, 2026
ea912e5
fix Input.Base not found error
HarshMN2345 Jun 11, 2026
9fbba38
restore previous UI, use fixed positioning for dialog
HarshMN2345 Jun 11, 2026
d0bbd5b
keep dropdown inside container with position absolute
HarshMN2345 Jun 11, 2026
0816d8c
use portal action to escape modal overflow
HarshMN2345 Jun 11, 2026
4ef5087
remove dead branch fetch code from page loaders
HarshMN2345 Jun 11, 2026
5aba685
fix all type errors
HarshMN2345 Jun 11, 2026
9bb0282
format
HarshMN2345 Jun 11, 2026
ba14c79
remove getBranches loop, use providerBranch directly
HarshMN2345 Jun 11, 2026
922b6a1
format
HarshMN2345 Jun 11, 2026
4bd3723
fix portal class mismatch causing dropdown to close on search click
HarshMN2345 Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@5d0672f",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@82d2831",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3",
"@appwrite.io/pink-legacy": "^1.0.3",
Expand Down
354 changes: 354 additions & 0 deletions src/lib/components/git/branchSelector.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,354 @@
<script lang="ts">
import {
IconChevronDown,
IconChevronUp,
IconSearch,
IconX
} from '@appwrite.io/pink-icons-svelte';
import { Icon } from '@appwrite.io/pink-svelte';
import { Query } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/state';
import { createEventDispatcher, hasContext, tick } from 'svelte';

export let value = '';
export let installationId: string;
export let repositoryId: string;
export let label = 'Production branch';
export let placeholder = 'Select branch';

const dispatch = createEventDispatcher();
Comment thread
greptile-apps[bot] marked this conversation as resolved.
const inDialogGroup = hasContext('dialog-group');

let open = false;
let searchQuery = '';
let branches: string[] = [];
let searchResults: string[] = [];
let loading = false;
let loaded = false;
let searching = false;
let searchTimer: ReturnType<typeof setTimeout>;
let searchInput: HTMLInputElement;
let containerEl: HTMLDivElement;
let dropdownRect = { top: 0, left: 0, width: 0 };

function portal(node: HTMLElement) {
const target = inDialogGroup ? document.querySelector('dialog[open]') : document.body;
target?.appendChild(node);
return {
destroy() {
node.parentNode?.removeChild(node);
}
};
}

function updateRect() {
if (!containerEl) return;
const rect = containerEl.getBoundingClientRect();
dropdownRect = { top: rect.bottom + 4, left: rect.left, width: rect.width };
}

$: (installationId,
repositoryId,
(() => {
branches = [];
loaded = false;
})());

async function loadBranches() {
if (loading || loaded || !installationId || !repositoryId) return;
loading = true;
try {
const { branches: result } = await sdk
.forProject(page.params.region, page.params.project)
.vcs.listRepositoryBranches({
installationId,
providerRepositoryId: repositoryId,
queries: [Query.limit(100)]
});
branches = result.map((b) => b.name);
loaded = true;
} finally {
loading = false;
}
}

async function searchBranches(query: string) {
if (!query) {
searchResults = [];
searching = false;
return;
}
searching = true;
try {
const { branches: results } = await sdk
.forProject(page.params.region, page.params.project)
.vcs.listRepositoryBranches({
installationId,
providerRepositoryId: repositoryId,
search: query,
queries: [Query.limit(100)]
});
searchResults = results.map((b) => b.name);
} finally {
searching = false;
}
}
Comment on lines +76 to +96

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale search results from out-of-order responses

The 300 ms debounce reduces but does not eliminate the race: if request A (for "mai") is still in flight when request B (for "main") starts and finishes first, A's finally block will overwrite searchResults with the wrong data after B has already completed. searching is false at that point, so the UI silently shows stale results for the old query while searchQuery displays the newer value.

Fix by tracking a sequence counter and discarding responses that arrive out of order — increment before each call, capture the value, and skip the assignment if the counter has advanced by the time the response arrives.


function onSearchInput() {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => searchBranches(searchQuery), 300);
}

function select(branch: string) {
value = branch;
open = false;
searchQuery = '';
searchResults = [];
dispatch('select', branch);
}

async function toggle() {
open = !open;
if (open) {
updateRect();
loadBranches();
await tick();
searchInput?.focus();
} else {
searchQuery = '';
searchResults = [];
}
}

function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
open = false;
searchQuery = '';
searchResults = [];
}
}

function handleOutsideClick(e: MouseEvent) {
if (open && !containerEl.contains(e.target as Node)) {
const dropdown = document.querySelector('.branch-selector-portal');
if (dropdown && dropdown.contains(e.target as Node)) return;
open = false;
searchQuery = '';
searchResults = [];
}
}

$: displayBranches = searchQuery ? searchResults : branches;
</script>

<svelte:window on:click={handleOutsideClick} on:keydown={handleKeydown} />

<div class="branch-selector" bind:this={containerEl}>
{#if label}
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="label">{label}</label>
{/if}
<button type="button" class="trigger" class:open on:click={toggle}>
<span class="trigger-value" class:muted={!value}>{value || placeholder}</span>
<Icon icon={open ? IconChevronUp : IconChevronDown} size="m" />
</button>

{#if open}
<div
class="dropdown branch-selector-portal"
use:portal
style="position: fixed; top: {dropdownRect.top}px; left: {dropdownRect.left}px; width: {dropdownRect.width}px; z-index: 9001;">
Comment thread
greptile-apps[bot] marked this conversation as resolved.
<div class="search-header">
<Icon icon={IconSearch} size="s" />
<input
bind:this={searchInput}
bind:value={searchQuery}
on:input={onSearchInput}
type="text"
placeholder="Find a branch..."
autocomplete="off" />
{#if searchQuery}
<button
type="button"
class="clear-btn"
on:click={() => {
searchQuery = '';
searchResults = [];
}}>
<Icon icon={IconX} size="s" />
</button>
{/if}
</div>
<ul role="listbox" class="branch-list">
{#if loading}
<li class="state-item">Loading...</li>
{:else if searching}
<li class="state-item">Searching...</li>
{:else if displayBranches.length === 0 && searchQuery}
<li class="state-item">No branches found</li>
{:else if displayBranches.length === 0}
<li class="state-item">No branches available</li>
{:else}
{#each displayBranches as branch}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<li
role="option"
aria-selected={branch === value}
class:active={branch === value}
on:click={() => select(branch)}>
{branch}
</li>
{/each}
{#if !searchQuery}
<li class="hint-item">Type to search all branches</li>
{/if}
{/if}
</ul>
</div>
{/if}
</div>

<style>
.branch-selector {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
gap: var(--space-2);
overflow: visible;
}

.label {
font-size: var(--font-size-s);
font-weight: 500;
color: var(--fgcolor-neutral-primary);
}

.trigger {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: var(--space-3) var(--space-6);
border: var(--border-width-s) solid var(--border-neutral);
border-radius: var(--border-radius-s);
background: var(--bgcolor-neutral-default);
cursor: pointer;
font-size: var(--font-size-s);
color: var(--fgcolor-neutral-primary);
transition: border-color 0.15s ease;
line-height: 140%;
}

.trigger:hover {
border-color: var(--border-focus);
}
.trigger.open {
outline: var(--border-width-l) solid var(--border-focus);
border-color: var(--border-focus);
}

.trigger-value {
flex: 1;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.trigger-value.muted {
color: var(--fgcolor-neutral-tertiary);
}

.dropdown {
background: var(--bgcolor-neutral-primary);
border: var(--border-width-s) solid var(--border-neutral);
border-radius: var(--border-radius-m);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
overflow: hidden;
}

.search-header {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-4) var(--space-5);
border-bottom: var(--border-width-s) solid var(--border-neutral);
}

.search-header input {
flex: 1;
border: none;
background: transparent;
font-size: var(--font-size-s);
color: var(--fgcolor-neutral-primary);
outline: none;
}

.search-header input::placeholder {
color: var(--fgcolor-neutral-tertiary);
}

.clear-btn {
display: flex;
align-items: center;
background: none;
border: none;
cursor: pointer;
padding: 0;
color: var(--fgcolor-neutral-tertiary);
}

.clear-btn:hover {
color: var(--fgcolor-neutral-primary);
}

.branch-list {
max-height: 300px;
overflow-y: auto;
padding: var(--space-2) 0;
list-style: none;
margin: 0;
}

.branch-list li {
padding: var(--space-2) var(--space-5);
font-size: var(--font-size-s);
color: var(--fgcolor-neutral-secondary);
cursor: pointer;
user-select: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.branch-list li:hover,
.branch-list li.active {
background: var(--overlay-neutral-hover);
color: var(--fgcolor-neutral-primary);
}
.branch-list li.active {
font-weight: 500;
}

.state-item {
color: var(--fgcolor-neutral-tertiary) !important;
cursor: default !important;
}
.state-item:hover {
background: transparent !important;
}

.hint-item {
font-size: var(--font-size-xs);
color: var(--fgcolor-neutral-tertiary);
cursor: default;
border-top: var(--border-width-s) solid var(--border-neutral);
margin-top: var(--space-1);
padding-top: var(--space-2);
}

.hint-item:hover {
background: transparent !important;
}
</style>
1 change: 1 addition & 0 deletions src/lib/components/git/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export { default as DeploymentSource } from './deploymentSource.svelte';
export { default as DeploymentDomains } from './deploymentDomains.svelte';
export { default as ConnectBehaviour } from './connectBehaviour.svelte';
export { default as ProductionBranchFieldset } from './productionBranchFieldset.svelte';
export { default as BranchSelector } from './branchSelector.svelte';
export { default as RepositoryCard } from './repositoryCard.svelte';
export { default as ConnectRepoModal } from './connectRepoModal.svelte';
Loading