feat(ui): add init repository dialog
Add a modal dialog to initialize a new Git repository, replacing the previous prompt-based flow. It integrates with the app state and invokes the initialization operation then opens the repository on confirmation. Also adds styles, keyboard handling, accessibility hints, a folder browser, initial-branch input, and German/English labels with busy state. - New dialog component with folder picker, branch input, and i18n. - Wire dialog open/close and confirm handler to init and open actions. - Add CSS rules for dialog layout, controls, and error/busy states.
This commit is contained in:
+23
-4
@@ -26,6 +26,7 @@
|
||||
import DiscardConfirmDialog from "./lib/components/DiscardConfirmDialog.svelte";
|
||||
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
||||
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
|
||||
import InitRepositoryDialog from "./lib/components/InitRepositoryDialog.svelte";
|
||||
import NewBranchDialog from "./lib/components/NewBranchDialog.svelte";
|
||||
import RenameBranchDialog from "./lib/components/RenameBranchDialog.svelte";
|
||||
import ReflogDialog from "./lib/components/ReflogDialog.svelte";
|
||||
@@ -313,6 +314,7 @@
|
||||
let repoStatusCache: Record<string, RepoTab> = {};
|
||||
let repoSearch = "";
|
||||
let cloneDialogOpen = false;
|
||||
let initRepositoryDialogOpen = false;
|
||||
let pullStrategy: PullStrategy = (localStorage.getItem("gitlite.pullStrategy") as PullStrategy) || "merge";
|
||||
let selectedRemote = localStorage.getItem("gitlite.selectedRemote") || "";
|
||||
let remoteActionForceWithLease = false;
|
||||
@@ -3926,10 +3928,17 @@
|
||||
}
|
||||
|
||||
async function initializeRepository() {
|
||||
const selected = await openDialog({ directory: true, multiple: false, title: "Choose an empty or existing folder" });
|
||||
if (typeof selected !== "string") return;
|
||||
const branch = window.prompt("Initial branch name", "main")?.trim(); if (!branch) return;
|
||||
await runOperation("Initializing repository", async () => { await initRepository(selected, branch); await openRepo(selected); });
|
||||
initRepositoryDialogOpen = true;
|
||||
}
|
||||
|
||||
async function confirmInitializeRepository(path: string, branch: string) {
|
||||
const selected = path.trim();
|
||||
if (!selected) return;
|
||||
await runOperation("Initializing repository", async () => {
|
||||
await initRepository(selected, branch);
|
||||
initRepositoryDialogOpen = false;
|
||||
await openRepo(selected);
|
||||
});
|
||||
}
|
||||
|
||||
async function revertHistoryCommit(commit: GitCommit) {
|
||||
@@ -6090,6 +6099,16 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Initialize repository dialog -->
|
||||
{#if initRepositoryDialogOpen}
|
||||
<InitRepositoryDialog
|
||||
isBusy={operation === "Initializing repository"}
|
||||
language={appLanguage}
|
||||
onInit={confirmInitializeRepository}
|
||||
onClose={() => { if (!isBusy) initRepositoryDialogOpen = false; }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Full-screen overlay while a repository is being opened -->
|
||||
{#if openingRepo}
|
||||
<RepoLoadingOverlay repoName={repoDisplayName} />
|
||||
|
||||
+35
@@ -3878,6 +3878,13 @@
|
||||
max-height: calc(100vh - 32px);
|
||||
overflow: auto;
|
||||
}
|
||||
.init-repository-dialog {
|
||||
display: block;
|
||||
width: min(540px, calc(100vw - 32px));
|
||||
height: auto;
|
||||
max-height: calc(100vh - 32px);
|
||||
overflow: auto;
|
||||
}
|
||||
.rename-branch-dialog {
|
||||
display: block;
|
||||
width: min(520px, calc(100vw - 32px));
|
||||
@@ -4172,6 +4179,34 @@
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
}
|
||||
.init-repository-header { padding: 16px 18px; }
|
||||
.init-repository-heading { display: flex; align-items: center; gap: 12px; }
|
||||
.init-repository-heading h2 { margin: 2px 0 0; color: var(--color-ink); font-size: 15px; line-height: 1.25; }
|
||||
.init-repository-icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid color-mix(in srgb, var(--color-accent) 34%, var(--color-border));
|
||||
border-radius: 9px;
|
||||
color: var(--color-accent);
|
||||
background: color-mix(in srgb, var(--color-accent) 10%, var(--color-surface-raised));
|
||||
}
|
||||
.init-repository-form { display: flex; flex-direction: column; gap: 16px; padding: 18px; }
|
||||
.init-repository-description { margin: 0; color: var(--color-ink-dim); font-size: 11.5px; line-height: 1.5; }
|
||||
.init-repository-field > span { font-size: 9.5px; }
|
||||
.init-repository-field > div { position: relative; align-items: center; }
|
||||
.init-repository-field > div > svg { position: absolute; left: 11px; z-index: 1; color: var(--color-ink-faint); pointer-events: none; }
|
||||
.init-repository-field > div > input { height: 36px; padding-left: 36px; font-family: var(--font-mono); font-size: 12px; }
|
||||
.init-repository-field small { color: var(--color-ink-faint); font-size: 9.5px; line-height: 1.4; }
|
||||
.init-repository-path-field { padding: 12px; border: 1px solid var(--color-border-subtle); border-radius: 9px; background: var(--color-surface-raised); }
|
||||
.init-repository-path-control { display: grid !important; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
|
||||
.init-repository-path-control > input { grid-column: 1; }
|
||||
.init-repository-browse { grid-column: 2; display: inline-flex; align-items: center; gap: 6px; height: 36px; font-size: 11.5px; white-space: nowrap; }
|
||||
.init-repository-error { color: var(--color-danger, #ff6b78) !important; }
|
||||
.init-repository-actions { padding-top: 2px; }
|
||||
.init-repository-actions > button { font-size: 11.5px; }
|
||||
.rename-branch-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
||||
import { Folder, FolderOpen, GitBranch, LoaderCircle, Plus, X } from "@lucide/svelte";
|
||||
import type { AppLanguage } from "../types";
|
||||
|
||||
interface Props {
|
||||
isBusy: boolean;
|
||||
language: AppLanguage;
|
||||
onInit: (path: string, branch: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
isBusy = false,
|
||||
language = "en",
|
||||
onInit = () => {},
|
||||
onClose = () => {},
|
||||
}: Props = $props();
|
||||
|
||||
let path = $state("");
|
||||
let branch = $state("main");
|
||||
let browseError = $state("");
|
||||
const isGerman = $derived(language === "de");
|
||||
const pathPlaceholder = navigator.userAgent.includes("Windows")
|
||||
? "C:\\Projects\\my-repository"
|
||||
: "/home/user/projects/my-repository";
|
||||
|
||||
function submit(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
const targetPath = path.trim();
|
||||
const branchName = branch.trim();
|
||||
if (!targetPath || !branchName || isBusy) return;
|
||||
onInit(targetPath, branchName);
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape" && !isBusy) onClose();
|
||||
}
|
||||
|
||||
async function chooseFolder() {
|
||||
if (isBusy) return;
|
||||
browseError = "";
|
||||
try {
|
||||
const selected = await openDialog({
|
||||
title: isGerman ? "Ordner für das neue Repository auswählen" : "Select folder for the new repository",
|
||||
directory: true,
|
||||
multiple: false,
|
||||
defaultPath: path.trim() || undefined,
|
||||
});
|
||||
if (typeof selected === "string") path = selected;
|
||||
} catch (error) {
|
||||
browseError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("keydown", handleKeydown);
|
||||
return () => window.removeEventListener("keydown", handleKeydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="dialog-backdrop app-chrome-backdrop" role="presentation">
|
||||
<div
|
||||
class="dialog init-repository-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="init-repository-title"
|
||||
tabindex="-1"
|
||||
>
|
||||
<header class="dialog-header init-repository-header">
|
||||
<div class="init-repository-heading">
|
||||
<span class="init-repository-icon" aria-hidden="true"><Plus size={18} /></span>
|
||||
<div>
|
||||
<span class="eyebrow">{isGerman ? "Neues Repository" : "New repository"}</span>
|
||||
<h2 id="init-repository-title">{isGerman ? "Repository initialisieren" : "Initialize repository"}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<button class="dialog-close" type="button" onclick={onClose} disabled={isBusy} title={isGerman ? "Schließen" : "Close"}>
|
||||
<X size={18} aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<form class="init-repository-form" onsubmit={submit}>
|
||||
<p class="init-repository-description">
|
||||
{isGerman
|
||||
? "GitLite richtet in diesem Ordner ein neues Git-Repository ein. Vorhandene Dateien bleiben unverändert."
|
||||
: "GitLite will create a new Git repository in this folder. Existing files will remain unchanged."}
|
||||
</p>
|
||||
|
||||
<label class="new-branch-field init-repository-field init-repository-path-field">
|
||||
<span>{isGerman ? "Zielordner" : "Repository folder"}</span>
|
||||
<div class="init-repository-path-control">
|
||||
<Folder size={16} aria-hidden="true" />
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
bind:value={path}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
placeholder={pathPlaceholder}
|
||||
disabled={isBusy}
|
||||
autofocus
|
||||
aria-describedby="init-repository-path-hint"
|
||||
/>
|
||||
<button class="btn-secondary init-repository-browse" type="button" onclick={chooseFolder} disabled={isBusy}>
|
||||
<FolderOpen size={15} aria-hidden="true" />
|
||||
{isGerman ? "Auswählen…" : "Browse…"}
|
||||
</button>
|
||||
</div>
|
||||
<small id="init-repository-path-hint">
|
||||
{isGerman ? "Der Ordner wird angelegt, falls er noch nicht existiert." : "The folder will be created if it does not exist."}
|
||||
</small>
|
||||
{#if browseError}<small class="init-repository-error" role="alert">{browseError}</small>{/if}
|
||||
</label>
|
||||
|
||||
<label class="new-branch-field init-repository-field">
|
||||
<span>{isGerman ? "Name des ersten Branches" : "Initial branch name"}</span>
|
||||
<div>
|
||||
<GitBranch size={16} aria-hidden="true" />
|
||||
<input
|
||||
bind:value={branch}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
placeholder="main"
|
||||
disabled={isBusy}
|
||||
aria-describedby="init-repository-branch-hint"
|
||||
/>
|
||||
</div>
|
||||
<small id="init-repository-branch-hint">
|
||||
{isGerman ? "Du kannst den Branch später jederzeit umbenennen." : "You can rename the branch at any time."}
|
||||
</small>
|
||||
</label>
|
||||
|
||||
<div class="new-branch-actions init-repository-actions">
|
||||
<button class="btn-secondary" type="button" onclick={onClose} disabled={isBusy}>
|
||||
{isGerman ? "Abbrechen" : "Cancel"}
|
||||
</button>
|
||||
<button class="btn-primary" type="submit" disabled={isBusy || path.trim().length === 0 || branch.trim().length === 0}>
|
||||
{#if isBusy}
|
||||
<LoaderCircle class="spin" size={16} aria-hidden="true" />
|
||||
{:else}
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
{/if}
|
||||
{isGerman ? "Repository erstellen" : "Create repository"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user