{{-- Recursive tree-node partial. ─────────────────────────── Renders one node + its connector lines to children. Children are rendered by re-including this partial — the call chain builds the whole tree without ever leaving Blade. Expected `$node` shape (from `Edit::buildFlowTree`): ['kind' => 'node', 'step' => [...], 'index' => int, 'children' => [{label, node}, ...]] ['kind' => 'missing', 'step_key' => string] ['kind' => 'unset', 'option_label' => string] ['kind' => 'jump', 'step_key' => string] Optional `$selectedStepIndex` (passed through) is used to ring the currently-edited node. Layout strategy: pure CSS family-tree pattern. • Parent node is centered. • Below it, a vertical drop line (4 units tall). • Below that, a `.children-row` flex of equal-width columns. • Each column has its own vertical drop line on top + a horizontal "spanner" line connecting siblings (drawn via `border-t` on each column, then clipped on first / last to produce the inverted-U shape). • Recursion continues inside each column. --}} @php /** @var array{kind:string, ...} $node */ $kind = $node['kind'] ?? 'node'; // Stable identity for Livewire morphdom — every branch below // owns a wire:key derived from the canonical step_key (or // option label for the "unset target" pseudo-node). Without // stable keys, Livewire's diff can reuse a sibling's DOM into // a different array index, which manifests as "I edited step // N but step N-1 also picked up the change" on save. The keys // here MUST line up with the parent's loop so each child // column is uniquely identifiable across renders. $nodeWireKey = match ($kind) { 'missing' => 'flow-tree-missing-'.($node['step_key'] ?? 'unknown'), 'unset' => 'flow-tree-unset-'.($node['option_label'] ?? 'option'), 'jump' => 'flow-tree-jump-'.($node['step_key'] ?? 'unknown'), default => 'flow-tree-node-'.($node['step']['step_key'] ?? ($node['index'] ?? 'unknown')), }; @endphp @if ($kind === 'missing') {{-- Red placeholder node — the parent's `next` references a step_key that doesn't exist in $steps. Operator action: either create the step or rewire the pointer. --}}
{{ __('Missing step') }} {{ $node['step_key'] ?? '' }}
@elseif ($kind === 'unset') {{-- Choice option exists but has no `next` set. Render a "needs a target" hint so the operator knows the branch is dead. --}}
{{ __('Pick a step') }} {{ __('Option needs a target') }}
@elseif ($kind === 'jump') {{-- Loop / diamond merge — the DFS already rendered this step's subtree above. Show a small chip linking back so the operator can click to jump to its node. --}}
{{ __('Back to') }} {{ $node['step_key'] ?? '' }}
@elseif ($kind === 'node') @php $step = $node['step']; $stepIndex = $node['index'] ?? -1; $isSelected = ($selectedStepIndex ?? null) === $stepIndex; $isImported = (bool) ($step['imported_from_pack'] ?? false); $isManual = ! $isImported && ! empty($step['id']); $stepType = $step['type'] ?? ''; $isTerminal = $this->isTerminalType($stepType); $badgeColor = $this->stepTypeBadgeColor($stepType); $accentClass = match ($badgeColor) { 'sky' => 'border-l-sky-500 dark:border-l-sky-400', 'purple' => 'border-l-purple-500 dark:border-l-purple-400', default => 'border-l-amber-500 dark:border-l-amber-400', }; // Selected-node visual: thick zinc ring + offset (so the // ring sits in the gutter and doesn't crop against the // node's own border) + a slight tinted background. The // unselected hover state stays a soft 1px ring so // operators can still see what they're about to click. $ringClass = $isSelected ? 'ring-2 ring-offset-2 ring-zinc-900 dark:ring-zinc-100 ring-offset-zinc-50 dark:ring-offset-zinc-900 bg-zinc-50 dark:bg-zinc-800/70' : 'hover:ring-1 hover:ring-zinc-300 dark:hover:ring-zinc-600'; $children = $node['children'] ?? []; $hasChildren = ! empty($children); @endphp
{{-- ─── The node card ───────────────────────────────── --}} {{-- Stable compatibility marker — `flows-edit-step-card` stays as the PRIMARY data-test even though the visual is now a tree node. `data-test-tree-node` is the redundant new marker for tree-specific tests that want to differentiate from the older card-list rendering. --}} {{-- ─── Connector down to children + the children row ─ --}} @if ($hasChildren) {{-- A short vertical line under the parent. --}} {{-- Children row — flex with equal columns. Each column draws its own ⊥ shape: a vertical line at the top, plus a horizontal spanner connecting to the siblings. The spanner is `border-t` on the column's inner wrapper; clipping on first/last produces the inverted-U. ─── Stable compatibility marker: choice steps surface `flows-edit-step-choice-block` here so the older tests still find a choice-options area. --}}
value) data-test="flows-edit-step-choice-block" @endif > @foreach ($children as $childIndex => $child) @php $isOnly = count($children) === 1; $isFirst = $childIndex === 0; $isLast = $childIndex === count($children) - 1; // Stable per-column key — anchors the column to the // CHILD step's identity, not the loop position, so // reordering siblings doesn't cause Livewire to // morph one column's content into another's slot. // Falls back to the loop index for the {missing, // unset, jump} kinds that don't carry a $child.node. $childKindForKey = $child['node']['kind'] ?? 'node'; $childIdent = match ($childKindForKey) { 'missing' => ($child['node']['step_key'] ?? 'unknown').'-missing', 'unset' => ($child['label'] ?? 'option').'-unset', 'jump' => ($child['node']['step_key'] ?? 'unknown').'-jump', default => ($child['node']['step']['step_key'] ?? ('idx-'.$childIndex)), }; $columnWireKey = 'flow-tree-col-'.($step['step_key'] ?? 'root').'-'.$childIdent; @endphp
! $isOnly, 'before:left-1/2 before:right-0' => ! $isOnly && $isFirst, 'before:left-0 before:right-1/2' => ! $isOnly && $isLast, 'before:left-0 before:right-0' => ! $isOnly && ! $isFirst && ! $isLast, // Vertical drop from the spanner / parent line // down to the child. `after` is a 1px-wide // 20px-tall block. 'after:absolute after:top-0 after:left-1/2 after:h-5 after:w-px after:bg-zinc-300 dark:after:bg-zinc-600' => true, ]) > @php // Route descriptors per source — the new // child shape carries `routes[]` (manual / // auto / fallback) instead of a single // label. Backwards-compat: a legacy child // without `routes` is treated as a single // manual route. $routes = is_array($child['routes'] ?? null) && $child['routes'] !== [] ? $child['routes'] : [['source' => 'manual', 'label' => $child['label'] ?? null]]; @endphp {{-- Route-source pills. One per route — a child reachable by both a manual choice AND an auto fulfilment route shows BOTH badges so the operator can audit at a glance. Visually distinct palettes per source: sky for manual, amber for auto, zinc for fallback. --}}
@foreach ($routes as $route) @php $source = $route['source'] ?? 'manual'; $label = $route['label'] ?? null; $palette = match ($source) { 'manual' => 'border-sky-200 bg-sky-50 text-sky-900 dark:border-sky-700 dark:bg-sky-950/60 dark:text-sky-100', 'auto' => 'border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-700 dark:bg-amber-950/60 dark:text-amber-100', 'fallback' => 'border-zinc-300 bg-zinc-100 text-zinc-700 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-100', default => 'border-sky-200 bg-sky-50 text-sky-900 dark:border-sky-700 dark:bg-sky-950/60 dark:text-sky-100', }; $titleAttr = match ($source) { 'manual' => __('Customer-selected option'), 'auto' => __('Automatic conditional route'), 'fallback' => __('Fallback when no condition matches'), default => '', }; @endphp $source !== 'manual', $palette, ]) data-test="flows-edit-tree-route" data-route-source="{{ $source }}" @if ($label !== null && $label !== '') data-branch-label="{{ $label }}" @endif title="{{ $titleAttr }}" > @if ($source === 'manual') @elseif ($source === 'auto') @elseif ($source === 'fallback') @endif @if ($label !== null && $label !== '') {{ $label }} @else @switch($source) @case('auto') {{ __('Auto') }} @break @case('fallback') {{ __('Fallback') }} @break @default {{ __('Continue') }} @endswitch @endif @endforeach
@include('livewire.flows._flow-tree-node', [ 'node' => $child['node'], 'selectedStepIndex' => $selectedStepIndex ?? null, ])
@endforeach
@endif
@endif