diff --git a/web/src/lib/components/collections/CreateCollectionModal.svelte b/web/src/lib/components/collections/CreateCollectionModal.svelte index 9ec81639f..2da76e778 100644 --- a/web/src/lib/components/collections/CreateCollectionModal.svelte +++ b/web/src/lib/components/collections/CreateCollectionModal.svelte @@ -10,6 +10,7 @@ coerceDefault, fieldFromDef, isSafeDoneFieldKey, + keptAbandoned, typeSupportsDefault, validateFieldKey, type EditableField @@ -316,6 +317,8 @@ ) { const terms = f.terminalOptions.filter((t) => def.options!.includes(t)); if (terms.length > 0) def.terminal_options = terms; + const ab = keptAbandoned(f.abandonedOptions, terms); + if (ab.length > 0) def.abandoned_options = ab; } // Advanced controls (T3 / TASK-596). Only emit when set so // payloads stay compact and round-trip with existing schemas. diff --git a/web/src/lib/components/collections/EditCollectionModal.svelte b/web/src/lib/components/collections/EditCollectionModal.svelte index 106c2f54b..6eee51bed 100644 --- a/web/src/lib/components/collections/EditCollectionModal.svelte +++ b/web/src/lib/components/collections/EditCollectionModal.svelte @@ -12,6 +12,7 @@ coerceDefault, defaultsEqual, isSafeDoneFieldKey, + keptAbandoned, typeSupportsDefault, validateFieldKey, type EditableField @@ -316,6 +317,7 @@ options: f.options ? [...f.options] : [], originalOptions: f.options ? [...f.options] : [], terminalOptions: f.terminal_options ? [...f.terminal_options] : [], + abandonedOptions: f.abandoned_options ? [...f.abandoned_options] : [], required: f.required, computed: f.computed, collection: f.collection, @@ -541,6 +543,8 @@ ) { const terms = f.terminalOptions.filter((t) => def.options!.includes(t)); if (terms.length > 0) def.terminal_options = terms; + const ab = keptAbandoned(f.abandonedOptions, terms); + if (ab.length > 0) def.abandoned_options = ab; } // Gate type-specific advanced values by the current type so // stale hidden values from a previous type (e.g. a number @@ -619,6 +623,8 @@ ) { const terms = f.terminalOptions.filter((t) => def.options!.includes(t)); if (terms.length > 0) def.terminal_options = terms; + const ab = keptAbandoned(f.abandonedOptions, terms); + if (ab.length > 0) def.abandoned_options = ab; } // Advanced controls (T3 / TASK-596). Only emit when set so // payloads stay compact and round-trip with existing schemas. diff --git a/web/src/lib/components/collections/FieldEditor.svelte b/web/src/lib/components/collections/FieldEditor.svelte index 5cc42dedd..c0ebd9246 100644 --- a/web/src/lib/components/collections/FieldEditor.svelte +++ b/web/src/lib/components/collections/FieldEditor.svelte @@ -105,6 +105,24 @@ field.keyTouched = true; } + // Markings are keyed by option VALUE, so an edited option must carry its + // terminal and abandoned marks to the new value (codex round 1 on #1468): + // a plain bind left them on the old value, which the save then filtered + // out, silently un-marking a renamed "cancelled" and counting it shipped. + function renameOption(optIndex: number, next: string) { + const prev = field.options[optIndex]; + field.options[optIndex] = next; + if (prev === next) return; + for (const marks of [field.terminalOptions, field.abandonedOptions]) { + const at = marks.indexOf(prev); + if (at < 0) continue; + // Another row may still hold the old value; its marking stays. + if (field.options.includes(prev)) continue; + if (marks.includes(next)) marks.splice(at, 1); + else marks[at] = next; + } + } + function removeOption(optIndex: number) { field.options.splice(optIndex, 1); } @@ -117,6 +135,11 @@ const idx = field.terminalOptions.indexOf(option); if (idx >= 0) { field.terminalOptions.splice(idx, 1); + // An option that no longer closes an item cannot close it without + // delivering either: the server refuses abandoned_options outside + // terminal_options (BUG-2347). + const ab = field.abandonedOptions.indexOf(option); + if (ab >= 0) field.abandonedOptions.splice(ab, 1); } else { field.terminalOptions.push(option); } @@ -126,6 +149,23 @@ return field.terminalOptions.includes(option); } + // "Counts as abandoned" (BUG-2347): a terminal option that closes an item + // WITHOUT delivering it (cancelled, rejected, overturned…). The changelog, + // standup and report throughput leave those items out. Offered only on a + // terminal option. + function toggleAbandoned(option: string) { + const idx = field.abandonedOptions.indexOf(option); + if (idx >= 0) { + field.abandonedOptions.splice(idx, 1); + } else { + field.abandonedOptions.push(option); + } + } + + function isAbandoned(option: string): boolean { + return field.abandonedOptions.includes(option); + } + // ── Advanced section ───────────────────────────────────────────────────── // Contains: required, default value, suffix (number only), relation target. // Auto-expanded when the field already has any advanced value configured so @@ -330,6 +370,11 @@ Saved {/if} + Abandoned? {#if !isActiveDoneField} @@ -348,7 +393,8 @@ renameOption(oi, e.currentTarget.value)} placeholder="option name" /> + {#if showsTerminalColumn} + {#if isTerminal(field.options[oi])} + + {:else} + + {/if} + {/if}