This commit is contained in:
2026-09-15 01:12:50 +08:00
parent 2db889ad89
commit 1a75503105
99 changed files with 4168 additions and 726 deletions
@@ -94,11 +94,12 @@ with everything nested under it — the wrapper yields nothing while its flag is
own command is always a list, so the <a class="xref" href="mission-root-panel.html">Mission branch root</a> <em>is</em> a
list, and every nested one is the same shape one level down.</p>
<p><code>List</code> is an ordinary catalog kind — category Flow — so a list is added the way any command is, and
adding one grows a sub-tree: the node's children are its own entries, and its panel embeds the same
entry-list editor the branch root uses, scoped to the nested list.</p>
adding one grows a sub-tree: the node's children are its own entries, edited on their own Control
Tree rows, and the list's own panel holds only its optional title, beneath the Disabled Caption
while the command is unticked.</p>
<p>This page covers what a list is, how its title reads, and what moving commands in and out of one
costs. The entry-list editor itself — Add Command, the row actions, the three drag landings — is
documented once, on <a class="xref" href="mission-root-panel.html">Mission Root Panel</a>.</p>
costs. The Control Tree row affordances every list shares — Add Command, the row buttons, the three
drag landings — are documented once, on <a class="xref" href="mission-root-panel.html">Mission Root Panel</a>.</p>
<h2 id="key-models">Key Models</h2>
<ul>
<li><strong>Primary</strong>: <a class="xref" href="../../../api/Hi.SessionCommands.ListCommand.html">ListCommand</a> — <a class="xref" href="../../../api/Hi.SessionCommands.ListCommand.html#Hi_SessionCommands_ListCommand_Title">Title</a>
@@ -121,72 +122,92 @@ and <a class="xref" href="../../../api/Hi.SessionCommands.ListCommand.html#Hi_Se
<ul>
<li>List Entry Panel
<ul>
<li>Control Bar
<li>Disabled Caption
<ul>
<li>Up / Down / Duplicate / Delete — labelled buttons for the operations that rewrite the
<strong>parent</strong> list. Delete asks for confirmation in a dialog naming the command.</li>
<li>At the bar's left, a caption while the command is disabled: skipped during play, still
editable.</li>
<li>Present only while the command is disabled: skipped during play, still editable.</li>
</ul>
</li>
<li>Title (optional) Input</li>
<li>Embedded Entry-List Editor
<ul>
<li>This list's own entries, scoped to the node's path.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>The Title input holds the <strong>raw</strong> title — empty when unset — while the label the row and the tree
show is composed by the engine: <code>List</code> when the title is blank, <code>List [title]</code> when it is not. A
<p>Nothing else is on the panel. The list's entries are the node's children in the tree, and the
operations that rewrite the <strong>parent</strong> list — Move up, Move down, Duplicate, Delete — are the
buttons on the list's own tree row, revealed on hover and on the selected row, and always shown on
a touch or pen device, where there is no hover; Delete asks for confirmation in a dialog naming the
command. The <code>+</code> on that row adds a command into this list.</p>
<p>The Title input holds the <strong>raw</strong> title — empty when unset — while the label the tree row shows is
composed by the engine: <code>List</code> when the title is blank, <code>List [title]</code> when it is not. A
titled list still says what it is. A whitespace-only title counts as unset and is never written into
the project file, so it cannot come back as a changed title on the next load.</p>
<p>Keystrokes debounce into one title save. The pending save is flushed before anything that shifts the
entry paths — a structural operation, a duplicate, a selection change — and cancelled on delete and
on unmount, so a late write cannot land on whichever command slid into the old path. Once it is
saved the tree label refreshes in place rather than by rebuilding the branch, which would remount
the open editor mid-edit.</p>
the project file: it reloads as the empty title, and the label reads <code>List</code> both before and after,
so a save cannot come back as a different label on the next load.</p>
<p>Keystrokes debounce into one title save. The same dirty gate flushes the pending save — and awaits a
save already on the wire — on two occasions: before every Control Tree row op, whose rebuild
remounts the panel and can shift the entry paths, and before a selection change, which switches
the panel away; unmount cancels it, so a late write cannot land on whichever command slides into
the old path. A flush whose save fails refuses the op or the switch, and a refused op says so in a
notice; a save that was already on the wire when the gate ran is only waited for — its failure
raises a notice of its own, and the op then proceeds regardless, leaving the title unsaved. Once
it is saved the tree label refreshes in place rather than by rebuilding the branch, which would
remount the open editor mid-edit.</p>
<p>The root list is the one list without a title input: the branch root reads <code>Mission</code>.</p>
<h2 id="enabling-and-what-a-disabled-list-skips">Enabling, and What a Disabled List Skips</h2>
<p>The enable switch is the checkbox on the command's own Control Tree item, not a control in the entry
row. It decides only whether the command <strong>runs</strong>: while it is clear the row and the tree node dim,
and the editor stays open and fully editable. Unticking a list dims its whole sub-tree — the dim
rides ancestor propagation in the tree while each nested entry keeps its own flag, which is exactly
what the run does: a disabled entry is skipped with everything under it.</p>
<p>The enable switch is the checkbox on the command's own Control Tree row, not a control on its panel
and not one of the row's buttons. It decides only whether the command <strong>runs</strong>: while it is clear
the tree row dims, and the editor stays open and fully editable. The checkboxes are read-only while
a mission row op, or a tick's own writes, are in flight. Unticking a list dims its whole sub-tree —
the dim rides ancestor propagation in the tree while each nested entry keeps its own flag, which is
exactly what the run does: a disabled entry is skipped with everything under it.</p>
<h2 id="paths">Paths</h2>
<p>A command node's key is its Mission API path — <code>&quot;0&quot;</code>, <code>&quot;1&quot;</code>, nested <code>&quot;0.2&quot;</code> for entry 2 inside the
list at index 0. Every structural change re-mints them: deleting entry 2 slides the next command
into path <code>&quot;2&quot;</code>. Each branch build therefore stamps its nodes, so the editor panel remounts onto the
new path instead of going on showing the command that used to live there.</p>
new path instead of going on showing a command the path no longer names. The host carries its own
index-based ids — the expansion set, the Add focus and the selection — through each row op, so they
follow the rows rather than the slots: a selection riding on the acted-on row follows it — the
stepped or dropped row at its new index, or the clone a Duplicate makes — except on Delete, where
no row remains to follow: it lands on the nearest remaining entry of that list, the index clamped
to the new count, or on the owning list node once the list is empty, where Add Command waits. Any
other selection keeps its own re-indexed row.</p>
<h2 id="moving-commands-in-and-out">Moving Commands In and Out</h2>
<ul>
<li><strong>Into a list</strong> — drag a row onto the middle band of a list row; that row's outer quarters still
reorder around it.</li>
<li><strong>Out of a list</strong> — drag onto the drop-out zone of a nested list's editor, which is visible only
while a row inside that editor is being dragged. The entry lands right after the list command
itself in the owning list.</li>
<li><strong>Into a list</strong> — drag a command's tree row — the whole row drags; the handle icon at its left
only marks it as draggable — onto the middle band of a list row; that row's outer quarters still
reorder around it. The Mission root row is all middle band: a drop anywhere on it lands in the
root list. Either way the entry lands at that list's tail.</li>
<li><strong>Out of a list</strong> — drop the row on the edge of a command row in another list, before or after it;
the lower quarter of the list command's own row lands the entry right after the list in the
owning list.</li>
</ul>
<p>Both are the same reparent call. The server resolves the source list and the target list to object
references before it mutates either, so the index shift the removal causes cannot misroute the
insert, and it refuses to move a list into itself or into one of its own descendants. That guard
compares identity rather than path-string prefixes, because <code>&quot;02&quot;</code>, <code>&quot;+2&quot;</code> and <code>&quot; 2&quot;</code> all parse to
index 2.</p>
<p>Which call a landing makes depends on whether the entry changes list, not on where the pointer
sat. A landing that changes the list — the middle band of a list the entry is not in, or an edge in
another list — is the reparent call; a landing that keeps the list — an edge in the entry's own
list, or the middle band of the list row (the Mission root row included) that already holds it,
which sends it to the tail — is the reorder call, sent as the list's whole new order. The server
resolves the source list and the target list to object references before it mutates either, so the
index shift the removal causes cannot misroute the insert, and it refuses to move a list into itself
or into one of its own descendants. That guard compares identity rather than path-string prefixes,
because <code>&quot;02&quot;</code>, <code>&quot;+2&quot;</code> and <code>&quot; 2&quot;</code> all parse to index 2.</p>
<p>Two more properties of a list follow from the same API:</p>
<ul>
<li><strong>A new command lands at the end</strong> of the list it was added to, root or nested. Nothing is pinned;
the order is entirely the user's, changed with the row's up / down buttons or by dragging.</li>
<li><strong>A new command lands right after the focused command</strong> when that command is a direct entry of
the list the <code>+</code> belongs to, root or nested, and at that list's tail otherwise. The focus is one
for the whole Mission tree — the last selected or last added command, whichever list it sits in
— so each list row's <code>+</code>, scoped to its own list, ignores a focus that lies in another list.
Nothing is pinned; the order is entirely the user's, changed with the row's Move up / Move down
buttons or by dragging.</li>
<li><strong>Duplicate is a deep copy.</strong> The entry is cloned through the same XML round-trip the project file
uses, so a nested list copies with its whole sub-tree, and the clone lands right after the source.</li>
</ul>
<p>Dragging serves rearrangement only: a drag carrying text or files is ignored, because the row's drag
handling returns as soon as the drag did not start on a row. To bring several program files in at
once, open a <a class="xref" href="NcFileCommand-panel.html">Program File</a> command's picker and pick them
<p>Dragging serves rearrangement only: a row accepts a drop only while a mission row is being dragged,
so a drag carrying text or files keeps the browser's no-drop cursor. To bring several program files
in at once, open a <a class="xref" href="NcFileCommand-panel.html">Program File</a> command's picker and pick them
together — the first pick lands on that command and each further pick becomes another Program File
command right after it, in pick order, inside the same list at any depth.</p>
<h2 id="which-commands-a-list-can-hold">Which Commands a List Can Hold</h2>
<p>The addable set is the server's command catalog: every engine command carrying <code>[CommandCatalog]</code>,
reflected into the picker Add Command opens, which searches by display label, kind key and alias.
reflected into the picker Add Command opens, which searches by display label, kind key and alias
and takes one pick per opening.
<code>List</code> is one of those commands, which is what makes nesting an ordinary act rather than a special
case. The frontend contributes an icon per kind, and its own label where it has one; the server
supplies the localized label otherwise, so a newly attributed engine command becomes addable with no
@@ -196,50 +217,74 @@ creation. General Config is the one such command with an editor of its own: it i
settings bundle, so a project file storing one loads as the individual setting commands it stands
for and saving never writes the bundle back — editable when a project still constructs one through
the API, and absent from the catalog.</p>
<p>Selecting an entry row selects that command's tree node, and the command's editor opens on that
node — the kind's own panel when one exists, otherwise a generic editor built from the scalar fields
the command declares. Selection is one row at a time: there is no multi-selection here, and every
action acts on the row that carries it. Because the server composes every entry label, a command
carrying no title of its own still reads as its localized display name.</p>
<p>There is no second column inside this editor: the entry list is a single column in the tree's editor
pane. The draggable vertical divider on screen belongs to the Execution page, between the left dock
— the Control Tree over the editor panel — and the central area; a second divider inside that dock
drags the height between the tree and the editor.</p>
<p>Selecting a command's tree row opens that command's editor on its node — the kind's own panel when
one exists, otherwise a generic editor built from the scalar fields the command declares. Selection
is one row at a time: there is no multi-selection here, and every row button acts on the row that
carries it, whether or not that row is the selected one. Because the server composes every entry
label, a command carrying no title of its own still reads as its localized display name.</p>
<p>A list's entries never appear in the editor pane: they are the node's children in the Control Tree,
and the pane below the tree shows only the selected command's panel. The left-most of the Execution
page's three draggable vertical dividers sits between the left dock — the Control Tree over the
editor panel — and the central area; the divider inside that dock drags the height between the tree
and the editor.</p>
<h2 id="source-code-path">Source Code Path</h2>
<p>See <a class="xref" href="../../index.html">HiNC App Anatomy</a> for git repository links.</p>
<h3 id="web-application">Web Application</h3>
<ul>
<li><code>wwwroot-src/src/components/controlTree/MissionCommandSlavePanel.vue</code> — a list entry's panel: the
control bar, the optional Title input with its debounce / flush / cancel discipline, the embedded
entry-list editor of a list entry, and the bespoke-or-generic editor choice for every other kind.</li>
<li><code>wwwroot-src/src/components/controlTree/MissionRootPanel.vue</code> — the entry-list editor embedded
here and serving the branch root: the rows and their actions, the three drag landings, and the
drop-out zone shown only during a drag inside a nested editor.</li>
disabled caption, the optional Title input with its debounce / flush / cancel discipline and the
dirty gate it answers to the host, and the bespoke-or-generic editor choice for every other kind.</li>
<li><code>wwwroot-src/src/components/controlTree/ControlTreePanel.vue</code> — the tree rows: the draggable row
with its handle icon, the Move up / Move down / Duplicate / Delete buttons on every command row
(revealed on hover and on the selected row, always shown on a touch or pen device), the <code>+</code>
on every list row (the Mission root included) and the Add Command dialog it opens, and the
drop-indicator styles.</li>
<li><code>wwwroot-src/src/components/controlTree/useMissionTreeDrag.ts</code> — the drag bookkeeping between
mission rows: which rows drag, which receive, and the before / after / into landing read from the
pointer's position on the row.</li>
<li><code>wwwroot-src/src/components/controlTree/MissionRootPanel.vue</code> — the branch root's panel, the one
without a title input: the command count and the run-order hint.</li>
<li><code>wwwroot-src/src/components/controlTree/AddCommandDialog.vue</code> — the search-first catalog picker;
one pick per opening.</li>
<li><code>wwwroot-src/src/components/controlTree/missionItemTypes.ts</code> — the child builders that recurse
into a list's own entries, the per-kind editor override map, the kind and category icons, and the
per-build stamp that forces the panel remount.</li>
<li><code>wwwroot-src/src/components/controlTree/useControlTreeHost.ts</code> — which nodes show a checkbox, the
dim that flows down a disabled command's sub-tree, the enable write, and the in-place label
refresh after a title edit or a language switch.</li>
into a list's own entries, the per-kind editor override map, and the per-build stamp that forces
the panel remount.</li>
<li><code>wwwroot-src/src/components/controlTree/missionKindIcons.ts</code> — the kind and category icons the
picker shows.</li>
<li><code>wwwroot-src/src/components/controlTree/useControlTreeHost.ts</code> — the five row ops — add, drag
move, step, duplicate, delete — through one <code>missionRowOp</code> helper (the busy gate, the open panel's
dirty flush, the API call, the scoped rebuild, the post-op selection), the insert-after-focus rule
scoped to the <code>+</code>'s own list, and the index translation that carries the expansion set, the Add
focus and the selection through each op; which nodes show a checkbox, the dim that flows down a
disabled command's sub-tree, the enable write, and the in-place label refresh after a title edit
or a language switch.</li>
<li><code>wwwroot-src/src/api/mission.ts</code> — the typed wrapper over the Mission API and the path convention
(<code>root</code> or empty = the root list, <code>0.2</code> = a nested entry).</li>
(<code>root</code> or empty = the root list, <code>0.2</code> = a nested entry); <code>addEntry</code> takes the optional insert
index and resolves to the new entry's path.</li>
<li><code>wwwroot-src/src/components/mission/GenericCommandPanel.vue</code> — the editor a catalog kind gets when
it ships no bespoke panel, built from the command's declared scalar fields.</li>
<li><code>wwwroot-src/src/components/mission/NcFileCommandPanel.vue</code> — the multi-pick file dialog: the
first pick lands on the command, each further pick becomes another Program File command right
after it.</li>
<li><code>wwwroot-src/src/pages/ExecutionPage.vue</code> — the page-level splitter between the left dock and the
central area.</li>
<li><code>wwwroot-src/src/i18n/en/tree.ts</code> — the <code>tree.mission.*</code> strings: the kind names, the drop-out
zone, the run-order hint, the disabled hint and the operation labels.</li>
<li><code>wwwroot-src/src/pages/ExecutionPage.vue</code> — the page-level splitters; the left-most parts the left
dock from the central area.</li>
<li><code>wwwroot-src/src/components/controlTree/ControlTreeDock.vue</code> — the left dock's two rows and the
height divider between the tree and the editor.</li>
<li><code>wwwroot-src/src/i18n/en/tree.ts</code> — the <code>tree.mission.*</code> strings: the kind names, the run-order
hint that names the row affordances, the disabled hint, the cancelled-op notice and the operation
labels.</li>
<li><code>wwwroot-src/src/i18n/en/common.ts</code> — the row buttons' titles and the Title (optional) label.</li>
<li><code>wwwroot-src/src/i18n/en/dialog.ts</code> — the Delete Command confirmation.</li>
<li><code>Missions/MissionController.cs</code> — the entry lifecycle: <code>GET list-command/entries</code> (recursive),
<code>POST list-command/entries</code> and <code>POST list-command/entries/{path}</code> (append at the tail of the root
or of a nested list), <code>DELETE list-command/entries/{path}</code>,
<code>POST list-command/entries/{path}/move</code>, <code>PUT list-command/reorder</code>,
<code>POST list-command/entries/{path}/duplicate</code> (the XML-round-trip deep clone),
<code>POST list-command/entries/{path}/reparent</code> (drag-into-list and drop-out, with the self/descendant
guard), <code>PUT commands/{path}/listcommand</code> for the title, and <code>PUT commands/{path}/enabled</code> for an
entry's enable flag. <code>GetCommandTitle</code> composes every entry label the tree shows.</li>
<code>POST list-command/entries</code> and <code>POST list-command/entries/{path}</code> (into the root or a nested
list, at an optional <code>insertIndex</code> clamped to the list and otherwise at its tail, answering the
new entry's index and path), <code>DELETE list-command/entries/{path}</code>,
<code>POST list-command/entries/{path}/move</code> (the row's Move up / Move down), <code>PUT list-command/reorder</code>
(a drop that keeps the owning list), <code>POST list-command/entries/{path}/duplicate</code> (the
XML-round-trip deep clone), <code>POST list-command/entries/{path}/reparent</code> (a drop that changes the
owning list, with the self/descendant guard), <code>PUT commands/{path}/listcommand</code> for the title, and
<code>PUT commands/{path}/enabled</code> for an entry's enable flag. <code>GetCommandTitle</code> composes every entry
label the tree shows.</li>
<li><code>Missions/MissionCommandCatalog.cs</code> — reflects the <code>[CommandCatalog]</code> commands into the addable
set and constructs the picked kind.</li>
</ul>
@@ -260,7 +305,7 @@ a loaded project expands a stored one into the split setting commands.</li>
</ul>
<h2 id="see-also">See Also</h2>
<ul>
<li><a class="xref" href="mission-root-panel.html">Mission Root Panel</a> — the entry-list editor this command embeds, documented once</li>
<li><a class="xref" href="mission-root-panel.html">Mission Root Panel</a> — the branch root, and the Control Tree row affordances every list shares, documented once</li>
<li><a href="../../../manual/run/building-a-mission.html">Building a Mission</a> — the task this command serves: grouping a mission and moving entries between groups</li>
<li><a href="index.html">Mission</a> — the rest of the command panels</li>
</ul>