zebflow 0.10.2

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
import { cx, useEffect, useState } from "zeb/react";
import { requestJson } from "@/components/lib/http";
import Button from "@/components/ui/button";
import Field from "@/components/ui/field";
import Input from "@/components/ui/input";
import { Select, SelectOption } from "@/components/ui/select";
import SettingsSection from "@/pages/project-studio/settings/components/settings-section";

/** The working branch, and switching it. */
export default function GitBranchPanel({ owner, project }) {
  const branchesUrl = `/api/projects/${owner}/${project}/git/branches`;
  const [current, setCurrent] = useState("");
  const [branches, setBranches] = useState([]);
  const [selected, setSelected] = useState("");
  const [newBranch, setNewBranch] = useState("");
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState("");
  const [msgTone, setMsgTone] = useState("info");

  async function fetchBranches() {
    try {
      const data = await requestJson(branchesUrl);
      if (!data) return;
      setCurrent(data.current ?? "");
      setBranches(Array.isArray(data.branches) ? data.branches : []);
      setSelected(data.current ?? "");
    } catch (_) {}
  }

  useEffect(() => { fetchBranches(); }, []);

  async function handleSwitch() {
    if (!selected || selected === current) return;
    setBusy(true); setMsg("");
    try {
      await requestJson(branchesUrl, { method: "POST", body: JSON.stringify({ branch: selected, create: false }) });
      setMsg(`Switched to ${selected}.`); setMsgTone("ok");
      await fetchBranches();
    } catch (e) { setMsg((e as any)?.message || "Failed"); setMsgTone("error"); }
    setBusy(false);
  }

  async function handleCreate() {
    const name = newBranch.trim();
    if (!name) return;
    setBusy(true); setMsg("");
    try {
      await requestJson(branchesUrl, { method: "POST", body: JSON.stringify({ branch: name, create: true }) });
      setMsg(`Branch "${name}" created and checked out.`); setMsgTone("ok");
      setNewBranch("");
      await fetchBranches();
    } catch (e) { setMsg((e as any)?.message || "Failed"); setMsgTone("error"); }
    setBusy(false);
  }

  return (
    <SettingsSection
      id="git"
      title="Git Branches"
      description="Switch or create local branches for this project."
      tag={current || null}
    >

      {branches.length > 1 && (
        <div className="flex items-end gap-[0.65rem] mb-[0.65rem]">
          <Field label="Switch to branch" className="flex-1">
            <Select value={selected} onChange={(e) => setSelected(e.target.value)}>
              {branches.map((b) => <SelectOption key={b} value={b}>{b}</SelectOption>)}
            </Select>
          </Field>
          <Button size="sm" variant="outline" onClick={handleSwitch} disabled={busy || selected === current}>
            Switch
          </Button>
        </div>
      )}

      <div className="flex items-end gap-[0.65rem]">
        <Field label="Create new branch" className="flex-1">
          <Input
            placeholder="branch-name"
            value={newBranch}
            onInput={(e) => setNewBranch(e.currentTarget.value)}
          />
        </Field>
        <Button size="sm" variant="primary" onClick={handleCreate} disabled={busy || !newBranch.trim()}>
          Create
        </Button>
      </div>

      {msg && (
        <p className={cx("text-[0.72rem] mt-[0.5rem]", msgTone === "ok" ? "text-primary" : msgTone === "error" ? "text-red-400" : "text-muted-foreground")}>
          {msg}
        </p>
      )}
    </SettingsSection>
  );
}