fix(386): address cold-review findings on the DetailPanel
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 11s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m16s
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 5s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m16s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 17m48s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 20m16s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 11s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m16s
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 5s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m16s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 17m48s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 20m16s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Cold adversarial review (no blockers, 3 real Mediums): - Query&size "Order" row ignored the Shuffle toggle (dead ?? fallback showing the static axis order); now reflects shuffleOn, consistent with the subtitle. - Clearing Name/Number to '' flagged the row Edited + armed the unsaved-changes guard, but the payload reverted to the proposal default — the two "edited" derivations disagreed. overrideEdited now ignores an empty (inherited) value. - runPreview did not clear per-channel overrides, so edits (incl. pinned numbers) silently carried into a re-generated batch → collision risk. Fresh preview now resets overrides/detailKey/logo cache. Tests: empty-name-not-Edited + re-preview-clears-overrides. Live-E2E (local instance, seeded TV library): preview → Configure → toggle Shuffle → Create verified end-to-end; the created channel's schedule shows PlaybackOrder=Shuffle (overridden) vs SeasonEpisode (axis default), confirming the per-channel override flows UI → SPA → backend → playout. Refs #386
This commit is contained in:
@@ -302,4 +302,41 @@ describe('AutoTuneScreen', () => {
|
||||
const comedy = body.channels.find((c: { value: string }) => c.value === 'Comedy');
|
||||
expect(comedy).toEqual({ axis: 'TvGenre', value: 'Comedy', name: 'Comedy', number: '502' });
|
||||
});
|
||||
|
||||
it('does not flag a row Edited when the name is typed then cleared back to empty', async () => {
|
||||
mockAutoTuneApi();
|
||||
await openPreview();
|
||||
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Configure' })[0]);
|
||||
await screen.findByRole('dialog', { name: 'The Office' });
|
||||
|
||||
const nameInput = screen.getByDisplayValue('The Office');
|
||||
fireEvent.change(nameInput, { target: { value: 'Renamed' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
|
||||
expect(screen.getByText('Edited')).toBeInTheDocument();
|
||||
|
||||
// Reopen and clear the field — inheriting the default is not an edit.
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Configure' })[0]);
|
||||
fireEvent.change(screen.getByDisplayValue('Renamed'), { target: { value: '' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
|
||||
expect(screen.queryByText('Edited')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('drops per-channel edits when the preview is re-run', async () => {
|
||||
mockAutoTuneApi();
|
||||
await openPreview();
|
||||
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Configure' })[0]);
|
||||
await screen.findByRole('dialog', { name: 'The Office' });
|
||||
fireEvent.change(screen.getByDisplayValue('The Office'), { target: { value: 'The Office (US)' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
|
||||
expect(screen.getByText('Edited')).toBeInTheDocument();
|
||||
|
||||
// Back to Configure, re-run Preview → a fresh batch starts with no carried-over overrides.
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Back' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Preview channels/ }));
|
||||
await screen.findByText('TV Shows');
|
||||
expect(screen.queryByText('Edited')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('checkbox', { name: 'The Office' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,8 +128,11 @@ function overrideEdited(override: ChannelOverride | undefined, proposal: AutoTun
|
||||
return false;
|
||||
}
|
||||
return Boolean(
|
||||
(override.name != null && override.name.trim() !== proposal.name) ||
|
||||
(override.number != null && override.number.trim() !== proposal.number) ||
|
||||
// A cleared field (trimmed to '') is NOT an edit — it inherits the proposal default, the same
|
||||
// value buildChannels submits (`override.name?.trim() || proposal.name`). Only a non-empty value
|
||||
// that differs counts, so the "Edited" badge + dirty guard match what actually gets created.
|
||||
(override.name != null && override.name.trim() !== '' && override.name.trim() !== proposal.name) ||
|
||||
(override.number != null && override.number.trim() !== '' && override.number.trim() !== proposal.number) ||
|
||||
override.templateId ||
|
||||
override.logoFile ||
|
||||
override.playbackOrder != null ||
|
||||
@@ -249,6 +252,12 @@ export function AutoTuneScreen() {
|
||||
const id = ++previewSeqRef.current;
|
||||
setStep('preview');
|
||||
setPreviewState({ status: 'loading' });
|
||||
// A fresh preview is a clean slate: drop any per-channel edits from a prior batch. They are
|
||||
// keyed by axis+value and would silently re-attach to the re-enumerated proposals — including a
|
||||
// pinned channel number that no longer fits the server's recomputed numbering (collision risk).
|
||||
setOverrides({});
|
||||
setDetailKey(null);
|
||||
uploadedLogosRef.current = new Map();
|
||||
previewAutoTune({
|
||||
axes: AXES.filter((axis) => axisIds.has(axis.id)).map((axis) => axis.id),
|
||||
minItems: Math.max(1, Number.parseInt(minItems, 10) || 1),
|
||||
@@ -1224,7 +1233,6 @@ function DetailPanel({
|
||||
const effectiveStreaming = effectiveValue('streamingMode', template, override.advanced) as StreamingMode | undefined;
|
||||
const streamingLabel = effectiveStreaming ? (STREAMING_MODE_LABELS[effectiveStreaming] ?? effectiveStreaming) : '—';
|
||||
|
||||
const axisMeta = AXES.find((axis) => axis.id === proposal.axis);
|
||||
const batchTemplateName = templates.find((candidate) => String(candidate.id) === batchTemplateId)?.name ?? 'batch template';
|
||||
|
||||
const overrideCount = Object.keys(override.advanced).length;
|
||||
@@ -1362,7 +1370,7 @@ function DetailPanel({
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection label="Query & size">
|
||||
<ReadRow label="Order" value={axisMeta?.order ?? (shuffleOn ? 'Shuffled' : 'Chronological')} />
|
||||
<ReadRow label="Order" value={shuffleOn ? 'Shuffled' : 'In order'} />
|
||||
<ReadRow label="Estimated items" value={<span style={monoStyle}>{proposal.itemCount}</span>} />
|
||||
<ReadRow label="Streaming mode" value={streamingLabel} />
|
||||
</DetailSection>
|
||||
|
||||
Reference in New Issue
Block a user