|
16 | 16 |
|
17 | 17 | <script lang="ts"> |
18 | 18 | import { t } from '$lib/i18n/index.svelte'; |
19 | | - import type { Node } from '$lib/tasks/model'; |
| 19 | + import { |
| 20 | + addSwitchBranch, |
| 21 | + removeSwitchBranch, |
| 22 | + renameSwitchBranch, |
| 23 | + updateSwitchBranchCondition, |
| 24 | + updateSwitchBranchTarget, |
| 25 | + } from '$lib/tasks/actions'; |
| 26 | + import type { NamedWorkflow, Node, SwitchNode } from '$lib/tasks/model'; |
20 | 27 |
|
21 | 28 | import CommonFields from './inspector/CommonFields.svelte'; |
22 | 29 | import { getNodeEditor } from './node-editors/registry'; |
|
37 | 44 | // Navigation into subgraphs |
38 | 45 | onenternode?: (nodeId: string) => void; // loop body |
39 | 46 | onenterbranch?: (nodeId: string, branchId: string) => void; // switch/fork/try |
40 | | - // Branch management (switch / fork) |
| 47 | + // Branch management (fork only — switch uses onupdate directly) |
41 | 48 | onaddbranch?: (nodeId: string) => void; |
42 | 49 | onremovebranch?: (nodeId: string, branchId: string) => void; |
43 | 50 | // Try-specific: add catchGraph section |
44 | 51 | onaddcatch?: (nodeId: string) => void; |
| 52 | + // Workflow list for switch branch target selection |
| 53 | + workflows?: NamedWorkflow[]; |
| 54 | + currentWorkflowName?: string; |
45 | 55 | } |
46 | 56 |
|
47 | 57 | let { |
|
57 | 67 | onaddbranch, |
58 | 68 | onremovebranch, |
59 | 69 | onaddcatch, |
| 70 | + workflows = [], |
| 71 | + currentWorkflowName = '', |
60 | 72 | }: Props = $props(); |
61 | 73 |
|
62 | 74 | // --------------------------------------------------------------------------- |
|
65 | 77 |
|
66 | 78 | const NodeEditor = $derived(node ? getNodeEditor(node) : null); |
67 | 79 |
|
| 80 | + // Narrowed switch node — used by the switch branch editing section. |
| 81 | + const switchNode = $derived( |
| 82 | + node !== null && node.type === 'switch' ? (node as SwitchNode) : null, |
| 83 | + ); |
| 84 | +
|
| 85 | + // Available target workflows: all except the current one. |
| 86 | + const targetWorkflows = $derived( |
| 87 | + workflows.filter((wf) => wf.name !== currentWorkflowName), |
| 88 | + ); |
| 89 | +
|
68 | 90 | // --------------------------------------------------------------------------- |
69 | 91 | // Helpers |
70 | 92 | // --------------------------------------------------------------------------- |
|
78 | 100 | if (!node) return; |
79 | 101 | onupdate?.({ ...node, name: value }); |
80 | 102 | } |
| 103 | +
|
| 104 | + // --------------------------------------------------------------------------- |
| 105 | + // Switch branch helpers — call onupdate with the full updated SwitchNode. |
| 106 | + // --------------------------------------------------------------------------- |
| 107 | +
|
| 108 | + function handleSwitchUpdate(updated: SwitchNode): void { |
| 109 | + onupdate?.(updated); |
| 110 | + } |
81 | 111 | </script> |
82 | 112 |
|
83 | 113 | <aside class="inspector"> |
|
114 | 144 | </section> |
115 | 145 |
|
116 | 146 | <!-- ------------------------------------------------------------------- |
117 | | - TaskSpecificSection: dynamically loaded editor from registry. |
118 | | - For task nodes this replaces the "coming soon" hint. |
119 | | - For structural nodes this supplements the branch/section navigation. |
| 147 | + Switch branch editing — full per-branch form with name, condition, |
| 148 | + target workflow, and inline validation. |
120 | 149 | -------------------------------------------------------------------- --> |
121 | 150 |
|
122 | | - {#if node.type === 'switch' || node.type === 'fork'} |
| 151 | + {#if switchNode} |
| 152 | + {@const hasDefault = switchNode.branches.some( |
| 153 | + (b) => b.condition === undefined, |
| 154 | + )} |
| 155 | + <section class="inspector-branches"> |
| 156 | + <h3 class="inspector-section-title"> |
| 157 | + {t('inspector.switch.branches')} |
| 158 | + </h3> |
| 159 | + |
| 160 | + {#each switchNode.branches as branch (branch.id)} |
| 161 | + {@const isDefault = branch.condition === undefined} |
| 162 | + {@const isDuplicate = |
| 163 | + switchNode.branches.filter((b) => b.label === branch.label).length > |
| 164 | + 1} |
| 165 | + {@const emptyCondition = !isDefault && branch.condition === ''} |
| 166 | + {@const noTarget = !branch.thenWorkflowName} |
| 167 | + |
| 168 | + <div class="switch-branch-card" data-testid="switch-branch-card"> |
| 169 | + <div class="switch-branch-card-header"> |
| 170 | + {#if isDefault} |
| 171 | + <span class="switch-default-badge" |
| 172 | + >{t('inspector.switch.defaultBranch')}</span |
| 173 | + > |
| 174 | + {:else} |
| 175 | + <span></span> |
| 176 | + {/if} |
| 177 | + {#if switchNode.branches.length > 1} |
| 178 | + <button |
| 179 | + class="branch-remove-btn" |
| 180 | + type="button" |
| 181 | + aria-label={t('inspector.removeBranch', { |
| 182 | + label: branch.label, |
| 183 | + })} |
| 184 | + onclick={() => |
| 185 | + handleSwitchUpdate( |
| 186 | + removeSwitchBranch(switchNode, branch.id), |
| 187 | + )} |
| 188 | + > |
| 189 | + ✕ |
| 190 | + </button> |
| 191 | + {/if} |
| 192 | + </div> |
| 193 | + |
| 194 | + <label class="field-label" for="branch-name-{branch.id}"> |
| 195 | + {t('inspector.switch.branchName')} |
| 196 | + </label> |
| 197 | + <input |
| 198 | + id="branch-name-{branch.id}" |
| 199 | + class="text-input" |
| 200 | + type="text" |
| 201 | + aria-label={t('inspector.switch.branchName')} |
| 202 | + value={branch.label} |
| 203 | + oninput={(e) => |
| 204 | + handleSwitchUpdate( |
| 205 | + renameSwitchBranch( |
| 206 | + switchNode, |
| 207 | + branch.id, |
| 208 | + e.currentTarget.value, |
| 209 | + ), |
| 210 | + )} |
| 211 | + /> |
| 212 | + {#if isDuplicate} |
| 213 | + <p class="field-warning"> |
| 214 | + {t('inspector.switch.errorDuplicateName')} |
| 215 | + </p> |
| 216 | + {/if} |
| 217 | + |
| 218 | + {#if !isDefault} |
| 219 | + <label class="field-label" for="branch-cond-{branch.id}"> |
| 220 | + {t('inspector.switch.condition')} |
| 221 | + </label> |
| 222 | + <input |
| 223 | + id="branch-cond-{branch.id}" |
| 224 | + class="text-input text-input--mono" |
| 225 | + type="text" |
| 226 | + aria-label={t('inspector.switch.condition')} |
| 227 | + value={branch.condition ?? ''} |
| 228 | + oninput={(e) => |
| 229 | + handleSwitchUpdate( |
| 230 | + updateSwitchBranchCondition( |
| 231 | + switchNode, |
| 232 | + branch.id, |
| 233 | + e.currentTarget.value, |
| 234 | + ), |
| 235 | + )} |
| 236 | + /> |
| 237 | + {#if emptyCondition} |
| 238 | + <p class="field-warning"> |
| 239 | + {t('inspector.switch.errorEmptyCondition')} |
| 240 | + </p> |
| 241 | + {/if} |
| 242 | + {/if} |
| 243 | + |
| 244 | + <label class="field-label" for="branch-target-{branch.id}"> |
| 245 | + {t('inspector.switch.targetWorkflow')} |
| 246 | + </label> |
| 247 | + <select |
| 248 | + id="branch-target-{branch.id}" |
| 249 | + class="select-input" |
| 250 | + aria-label={t('inspector.switch.targetWorkflow')} |
| 251 | + value={branch.thenWorkflowName ?? ''} |
| 252 | + onchange={(e) => |
| 253 | + handleSwitchUpdate( |
| 254 | + updateSwitchBranchTarget( |
| 255 | + switchNode, |
| 256 | + branch.id, |
| 257 | + e.currentTarget.value || undefined, |
| 258 | + ), |
| 259 | + )} |
| 260 | + > |
| 261 | + <option value="">—</option> |
| 262 | + {#each targetWorkflows as wf (wf.id)} |
| 263 | + <option value={wf.name}>{wf.name}</option> |
| 264 | + {/each} |
| 265 | + </select> |
| 266 | + {#if noTarget} |
| 267 | + <p class="field-warning"> |
| 268 | + {t('inspector.switch.errorNoTarget')} |
| 269 | + </p> |
| 270 | + {/if} |
| 271 | + </div> |
| 272 | + {/each} |
| 273 | + |
| 274 | + <div class="switch-add-btns"> |
| 275 | + <button |
| 276 | + class="branch-add-btn" |
| 277 | + type="button" |
| 278 | + onclick={() => |
| 279 | + handleSwitchUpdate(addSwitchBranch(switchNode, 'new-branch', ''))} |
| 280 | + > |
| 281 | + {t('inspector.switch.addBranch')} |
| 282 | + </button> |
| 283 | + {#if !hasDefault} |
| 284 | + <button |
| 285 | + class="branch-add-btn" |
| 286 | + type="button" |
| 287 | + onclick={() => |
| 288 | + handleSwitchUpdate( |
| 289 | + addSwitchBranch(switchNode, 'default', undefined), |
| 290 | + )} |
| 291 | + > |
| 292 | + {t('inspector.switch.addDefaultBranch')} |
| 293 | + </button> |
| 294 | + {/if} |
| 295 | + </div> |
| 296 | + </section> |
| 297 | + {:else if node.type === 'fork'} |
123 | 298 | <section class="inspector-branches"> |
124 | 299 | <h3 class="inspector-section-title">{t('inspector.branches')}</h3> |
125 | 300 | <ul class="branch-list" role="list"> |
|
343 | 518 | letter-spacing: 0.04em; |
344 | 519 | } |
345 | 520 |
|
| 521 | + /* ------------------------------------------------------------------------- |
| 522 | + Switch branch cards |
| 523 | + ------------------------------------------------------------------------- */ |
| 524 | +
|
| 525 | + .switch-branch-card { |
| 526 | + border: 1px solid #e5e7eb; |
| 527 | + border-radius: 6px; |
| 528 | + padding: 0.5rem 0.6rem; |
| 529 | + margin-bottom: 0.5rem; |
| 530 | + display: flex; |
| 531 | + flex-direction: column; |
| 532 | + gap: 0.2rem; |
| 533 | + } |
| 534 | +
|
| 535 | + .switch-branch-card-header { |
| 536 | + display: flex; |
| 537 | + align-items: center; |
| 538 | + justify-content: space-between; |
| 539 | + margin-bottom: 0.2rem; |
| 540 | + } |
| 541 | +
|
| 542 | + .switch-default-badge { |
| 543 | + font-size: 0.68rem; |
| 544 | + font-weight: 600; |
| 545 | + text-transform: uppercase; |
| 546 | + letter-spacing: 0.04em; |
| 547 | + color: #7c3aed; |
| 548 | + background: #f3f0ff; |
| 549 | + border-radius: 3px; |
| 550 | + padding: 1px 5px; |
| 551 | + } |
| 552 | +
|
| 553 | + .text-input { |
| 554 | + width: 100%; |
| 555 | + padding: 0.25rem 0.4rem; |
| 556 | + border: 1px solid #ddd; |
| 557 | + border-radius: 4px; |
| 558 | + font-size: 0.78rem; |
| 559 | + font-family: inherit; |
| 560 | + color: #111; |
| 561 | + background: #fff; |
| 562 | + box-sizing: border-box; |
| 563 | + margin-bottom: 0.2rem; |
| 564 | + } |
| 565 | +
|
| 566 | + .text-input--mono { |
| 567 | + font-family: monospace; |
| 568 | + } |
| 569 | +
|
| 570 | + .text-input:focus { |
| 571 | + outline: none; |
| 572 | + border-color: #1a56cc; |
| 573 | + box-shadow: 0 0 0 2px rgba(26, 86, 204, 0.15); |
| 574 | + } |
| 575 | +
|
| 576 | + .select-input { |
| 577 | + width: 100%; |
| 578 | + padding: 0.25rem 0.4rem; |
| 579 | + border: 1px solid #ddd; |
| 580 | + border-radius: 4px; |
| 581 | + font-size: 0.78rem; |
| 582 | + font-family: inherit; |
| 583 | + color: #111; |
| 584 | + background: #fff; |
| 585 | + box-sizing: border-box; |
| 586 | + margin-bottom: 0.2rem; |
| 587 | + } |
| 588 | +
|
| 589 | + .select-input:focus { |
| 590 | + outline: none; |
| 591 | + border-color: #1a56cc; |
| 592 | + box-shadow: 0 0 0 2px rgba(26, 86, 204, 0.15); |
| 593 | + } |
| 594 | +
|
| 595 | + .field-warning { |
| 596 | + margin: 0 0 0.2rem; |
| 597 | + font-size: 0.7rem; |
| 598 | + color: #b45309; |
| 599 | + } |
| 600 | +
|
| 601 | + .switch-add-btns { |
| 602 | + display: flex; |
| 603 | + flex-direction: column; |
| 604 | + gap: 0.375rem; |
| 605 | + margin-top: 0.25rem; |
| 606 | + } |
| 607 | +
|
| 608 | + /* ------------------------------------------------------------------------- |
| 609 | + Fork branch list (unchanged) |
| 610 | + ------------------------------------------------------------------------- */ |
| 611 | +
|
346 | 612 | .branch-list { |
347 | 613 | list-style: none; |
348 | 614 | margin: 0 0 0.5rem; |
|
0 commit comments