wsb-daemon 0.275.7622

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
<!--
  Deleting a local branch in one repository.

  The branches are read from the repository rather than typed: a mistyped name
  is a refusal at best and the wrong branch at worst. The one that is checked
  out is named and left out of the list — git will not delete it and neither
  will this, and saying so where the names are beats a refusal after the click.

  Unmerged work needs force, stated as what it is: git's refusal is the only
  thing standing between a branch nobody merged and losing what is on it.
-->
<script lang="ts">
  import { onMount } from "svelte";
  import { git } from "../../../stores/git.svelte.js";
  import AppIcon from "../../layout/AppIcon.svelte";

  let {
    repoPath,
    onclose,
  }: {
    repoPath?: string;
    onclose: () => void;
  } = $props();

  let branches = $state<string[]>([]);
  let current = $state<string | null>(null);
  let selected = $state("");
  let force = $state(false);
  let loading = $state(true);
  let error = $state<string | null>(null);

  onMount(async () => {
    try {
      const answer = await git.getBranches(repoPath);
      branches = answer.branches;
      current = answer.current;
      selected = branches.find((branch) => branch !== answer.current) ?? "";
    } catch (failure) {
      error = failure instanceof Error ? failure.message : String(failure);
    } finally {
      loading = false;
    }
  });

  const deletable = $derived(branches.filter((branch) => branch !== current));

  function remove() {
    if (!selected) return;
    if (git.deleteBranch(selected, repoPath, force)) onclose();
  }

  function handleKeydown(event: KeyboardEvent) {
    if (event.key === "Escape") onclose();
  }
</script>

<div class="modal-overlay" onclick={onclose} onkeydown={handleKeydown} role="presentation">
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
  <div
    class="modal-panel"
    onclick={(event) => event.stopPropagation()}
    onkeydown={handleKeydown}
    role="dialog"
    aria-label="Delete branch"
    tabindex="0"
  >
    <div class="modal-header">
      <h2 class="modal-title">Delete branch</h2>
      <button class="modal-close" onclick={onclose} aria-label="Close">
        <AppIcon name="x" size={14} />
      </button>
    </div>
    <div class="modal-body">
      {#if error}
        <div class="modal-error">{error}</div>
      {/if}

      {#if loading}
        <p class="empty">Reading branches…</p>
      {:else if deletable.length === 0}
        <p class="empty">
          This repository has only {current ?? "the branch it is on"}, and the branch that is
          checked out cannot be deleted from under itself.
        </p>
      {:else}
        <label class="field">
          <span class="field-label">Branch</span>
          <select class="field-input" bind:value={selected}>
            {#each deletable as branch (branch)}
              <option value={branch}>{branch}</option>
            {/each}
          </select>
        </label>
        {#if current}
          <p class="empty">On {current} — that one stays.</p>
        {/if}
        <label class="check">
          <input type="checkbox" bind:checked={force} />
          delete even if its work is not merged anywhere
        </label>
      {/if}
    </div>
    <div class="modal-footer">
      <button class="secondary" onclick={onclose}>Cancel</button>
      <button class="danger" disabled={!selected} onclick={remove}>
        Delete {selected}
      </button>
    </div>
  </div>
</div>

<style>
  .modal-body {
    display: flex;
    flex-direction: column;
    gap: var(--wb-spacing-8);
  }

  .modal-footer {
    display: flex;
    justify-content: flex-end;
    gap: var(--wb-spacing-6);
    padding: var(--wb-spacing-10) var(--wb-spacing-14) var(--wb-spacing-12);
  }
</style>