Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301<script lang="ts"> import { resolve } from "$app/paths"; import Download from "$icon/download"; import Lock from "$icon/lock"; import Search from "$icon/search"; import X from "$icon/x"; import Fuse from "fuse.js"; import { tick } from "svelte"; import type { GitHubAccount, GitHubRepo } from "#lib/github.ts"; import type { Draft } from "#lib/components/import/gitHubImportState.svelte.ts"; import Button from "#lib/components/ui/Button.svelte"; import Checkbox from "#lib/components/ui/Checkbox.svelte"; import LayerCard from "#lib/components/ui/LayerCard.svelte"; import LayerCardPrimary from "#lib/components/ui/LayerCardPrimary.svelte"; import FormStep from "#lib/components/ui/FormStep.svelte"; import Input from "#lib/components/ui/Input.svelte"; import Pagination from "#lib/components/ui/Pagination.svelte"; import { plural } from "#lib/format.ts"; import RepoRow, { repoTableGrid } from "#lib/components/import/RepoRow.svelte"; import { isMigratable } from "#lib/components/import/gitHubImportState.svelte.ts"; import { isMigrateDisabled, isRepoSelectGated } from "#lib/components/import/stepGating.ts";
interface Props { number: number; account: GitHubAccount | null; authorized?: boolean; repos: GitHubRepo[]; drafts: Record<number, Draft>; expanded: Set<number>; allSelected: boolean; migratableCount: number; knotOptions: readonly string[]; disabled: boolean; submitting?: boolean; onSelectAll: (checked: boolean) => void; onSelectRepo: (id: number, checked: boolean) => void; onToggleExpand: (id: number) => void; onUpdateDraft: (id: number, patch: Partial<Draft>) => void; onMigrate: () => void; }
let { number, account, authorized = false, repos, drafts, expanded, allSelected, migratableCount, knotOptions, disabled, submitting = false, onSelectAll, onSelectRepo, onToggleExpand, onUpdateDraft, onMigrate }: Props = $props();
const PAGE_SIZE = 10;
let page = $state(1); let searching = $state(false); let query = $state(""); let searchInput = $state<HTMLInputElement>(); // the page clearing a search returns to let lastTouched = $state<number>();
const touch = (id: number) => (lastTouched = id);
// matches the edited name and description, not just what github reported const fuse = $derived( new Fuse( repos.map((repo) => ({ repo, name: drafts[repo.id]?.name ?? repo.name, description: drafts[repo.id]?.description ?? repo.description ?? "" })), { keys: [{ name: "name", weight: 2 }, "description"], threshold: 0.4, ignoreLocation: true } ) ); const filtered = $derived( query.trim() ? fuse.search(query.trim()).map((result) => result.item.repo) : repos ); const pageCount = $derived(Math.ceil(filtered.length / PAGE_SIZE)); const visible = $derived(filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE));
const pageOf = (id: number | undefined) => { const index = id === undefined ? -1 : repos.findIndex((repo) => repo.id === id); return index < 0 ? 1 : Math.floor(index / PAGE_SIZE) + 1; };
const onQuery = (next: string) => { const cleared = query.trim() !== "" && next.trim() === ""; query = next; page = cleared ? pageOf(lastTouched) : 1; };
const openSearch = async () => { searching = true; await tick(); searchInput?.focus(); };
const closeSearch = () => { searching = false; onQuery(""); };
let searchButton = $state<HTMLAnchorElement | HTMLButtonElement>();
const dismissSearch = async () => { closeSearch(); await tick(); searchButton?.focus(); };
// the list can shrink under the current page, e.g. a new account loads $effect(() => { if (page > Math.max(pageCount, 1)) page = Math.max(pageCount, 1); });
const selectedCount = $derived(repos.filter((repo) => drafts[repo.id]?.selected).length); const privateCount = $derived( repos.filter( (repo) => repo.private && drafts[repo.id]?.selected && isMigratable(drafts[repo.id]?.migration) ).length );
const stepDisabled = $derived(isRepoSelectGated({ account, authorized, disabled })); const migrateDisabled = $derived( isMigrateDisabled({ account, authorized, migratableCount, knotCount: knotOptions.length, disabled }) );</script>
<FormStep step={number} title="Choose repositories" description="Pick the repositories to migrate. Edit one to change its name, description, or knot." class={stepDisabled ? "pointer-events-none opacity-50" : undefined}> {#if account} <LayerCard> {#if repos.length} <div class="{repoTableGrid} min-h-10 py-1 typography-paragraph-regular text-foreground-muted" > <Checkbox class="hit-area-x-3 hit-area-y-1.5" checked={allSelected} indeterminate={selectedCount > 0 && !allSelected} disabled={stepDisabled} onchange={() => onSelectAll(!allSelected)} aria-label={allSelected ? "Deselect all repositories" : "Select all repositories"} /> {#if searching} <Input bind:element={searchInput} class="-mr-2xs md:col-span-5" iconLeft={Search} value={query} placeholder="Search repositories" aria-label="Search repositories" oninput={(event) => onQuery(event.currentTarget.value)} onkeydown={(event) => { if (event.key === "Escape") void dismissSearch(); }} onblur={() => { if (!query.trim()) closeSearch(); }} /> <Button variant="ghost" square icon={X} aria-label="Clear search" onclick={dismissSearch} /> {:else} {#if selectedCount > 0} <span class="tabular-nums md:col-span-5" >{selectedCount}/{repos.length} selected</span > {:else} <span>Name</span> <span class="hidden md:block"></span> <span class="hidden md:block"></span> <span class="hidden md:block">Description</span> <span class="hidden md:block">Destination</span> {/if} <Button variant="ghost" square icon={Search} aria-label="Search repositories" bind:ref={searchButton} disabled={stepDisabled} onclick={openSearch} /> {/if} </div> {/if} <LayerCardPrimary padding="none"> {#each visible as repo (repo.id)} {@const draft = drafts[repo.id]} {@const locked = stepDisabled || !isMigratable(draft.migration)} <RepoRow {repo} name={draft.name} description={draft.description} knot={draft.knot} knots={knotOptions} selected={draft.selected} disabled={locked} editing={expanded.has(repo.id)} onToggle={() => { touch(repo.id); onSelectRepo(repo.id, !draft.selected); }} onEdit={() => { touch(repo.id); onToggleExpand(repo.id); }} onSave={(edit) => { touch(repo.id); onUpdateDraft(repo.id, edit); onToggleExpand(repo.id); }} language={repo.language ?? undefined} stars={repo.stars ?? undefined} /> {:else} <p class="p-sm typography-paragraph-small text-foreground-muted"> {query.trim() ? "No repositories match your search." : "No repositories found."} </p> {/each} </LayerCardPrimary> {#if pageCount > 1} <div class="flex justify-center py-1"> <Pagination bind:page total={pageCount} disabled={stepDisabled} /> </div> {/if} </LayerCard> {:else} <p class="typography-paragraph-small text-foreground-muted"> No repositories yet. Connect your GitHub account to list them. </p> {/if} {#if knotOptions.length === 0} <p class="mt-sm typography-paragraph-small text-foreground-muted"> No knots are available yet. <a class="underline" href={resolve("/settings/knots")} >Manage your knots</a >. </p> {/if} {#if privateCount > 0} <p class="mt-md flex items-center justify-end gap-xs typography-paragraph-small text-foreground-muted" > <Lock class="size-3.5 shrink-0" aria-hidden="true" /> {plural( privateCount, "1 private repository will be public on Tangled, and anyone can read it.", `${privateCount} private repositories will be public on Tangled, and anyone can read them.` )} </p> {/if} <div class="mt-md flex justify-end"> <Button variant="primary" icon={Download} class="tabular-nums" disabled={migrateDisabled} loading={submitting} onclick={onMigrate} > {migratableCount === 0 ? "Migrate repositories" : `Migrate ${migratableCount} ${plural(migratableCount, "repository", "repositories")}`} </Button> </div></FormStep>