// The branch panel: every line the story has taken, and the map of them.
//
// One request draws the whole panel. `fork_depth` says where a branch leaves
// its parent and `depth` says where it currently ends, so the shape is two
// numbers a row rather than a walk.
//
// Delete belongs here rather than in a later subphase because nothing prunes
// the tree on its own. This panel is the first place you can make a fork, so
// it has to be the first place you can unmake one.
import { useEffect, useState } from 'react'
import { api } from '../../../api'
import { BranchMap } from '../../../BranchMap'
import { branchLabel, headLineage, orderBranches, savePointsUnder } from '../../../branches'
function BranchPanel({ advId, refreshKey, onSwitched, onTreeChanged, onError }) {
const [branches, setBranches] = useState(null)
const [failed, setFailed] = useState(null)
const [busyId, setBusyId] = useState(null)
const [renaming, setRenaming] = useState(null) // { id, text }
const [confirming, setConfirming] = useState(null)
const [mapOpen, setMapOpen] = useState(false)
const [tick, setTick] = useState(0)
useEffect(() => {
let cancelled = false
setFailed(null)
api.listBranches(advId)
.then((list) => { if (!cancelled) setBranches(list) })
.catch((err) => { if (!cancelled) setFailed(err.message) })
return () => { cancelled = true }
}, [advId, refreshKey, tick])
// Answers whether it worked. Both callers keep an editor open on a refusal —
// a rename the server turned down must not take the typed name with it.
async function run(branchId, work) {
setBusyId(branchId)
try {
await work()
setTick((t) => t + 1)
// Deleting a branch takes its memories with it, and nothing else on the
// screen would hear about that — no turn is played, and the story on the
// current path does not change by a single action.
onTreeChanged()
return true
} catch (err) {
onError(err.message)
return false
} finally {
setBusyId(null)
}
}
// One copy of each operation. The list below and the map overlay both go
// through these, so a rule cannot hold in one view and not the other, and a
// failure is reported one way wherever it was asked for.
const switchTo = (b) => run(b.id, async () => onSwitched(await api.switchBranch(advId, b.id)))
const renameTo = (b, name) => run(b.id, () => api.renameBranch(advId, b.id, name))
const removeBranch = (b) => run(b.id, () => api.deleteBranch(advId, b.id))
const saveName = async (b) => { if (await renameTo(b, renaming.text)) setRenaming(null) }
const remove = async (b) => { if (await removeBranch(b)) setConfirming(null) }
if (failed) return
Couldn’t read the branches — {failed}
if (!branches) return
Reading the tree…
const lineage = headLineage(branches)
return (
{/* The list says which lines exist; the map says where they parted and
how much story each one is, which is the part a list cannot draw. */}
{branches.length === 1 && (
One thread so far. Retry a turn, then take an attempt the story moved
past — that is what makes a second one.
)}
{orderBranches(branches).map(({ branch, indent }) => {
const isRenaming = renaming?.id === branch.id
const isConfirming = confirming === branch.id
const busy = busyId === branch.id
// The server refuses to delete the line being read or any line it
// was forked from. The button said nothing about that and answered
// with a toast; it now says so before it is pressed.
const loadBearing = lineage.has(branch.id)
// A Save Point naming a moment on this branch, or on anything forked
// from it, keeps the branch alive: named moments last until the user
// deletes them, so the server refuses the deletion rather than taking
// them with it. Counted over the subtree, because deleting a branch
// takes its descendants.
const protecting = savePointsUnder(branches, branch.id)
return (
{branch.own_actions} of its own
{branch.parent_branch_id !== null && ` · forked at moment ${branch.fork_depth + 1}`}
{` · ends at ${branch.depth + 1}`}
{protecting > 0 &&
` · ${protecting === 1 ? '1 Save Point' : `${protecting} Save Points`} kept here`}
{isConfirming ? (
{/* Only reachable when no Save Point points into this
subtree — the button is disabled otherwise, and the server
refuses regardless. So this says what the deletion costs
without hedging about Save Points that cannot be at risk. */}
Delete this branch and everything forked from it? The story
on other paths, and every Save Point, is unaffected.
) : (
{!branch.is_head && (
)}
{isRenaming ? (
<>
>
) : (
)}
{/* The root holds the turns every other branch borrows, and
the server refuses it — so it is not offered. */}
{branch.parent_branch_id !== null && (
)}