fix(ui): destructive confirmations, verb labels and provider icons

Confirmations: ConfirmDialogData.confirmLabel is required, so no dialog can
fall back to "Yes"/"No"; the dismiss defaults to "Cancel" and
`tone: 'destructive'` styles the confirm with .app-destructive-button. Every
caller names its action ("Remove playlist", "Clear", "Refresh playlist",
"Cancel download" with a "Close" dismiss). The confirm button has the
confirm-dialog-confirm test id and drops its no-op color="primary".

The no-op `warn` color input becomes .app-destructive-button on the EPG
mapping, playlist item, error view, EPG/reset settings, delete-all and source
cleanup buttons, and on the unsaved-changes dialog's Discard.

Provider icons come from SOURCE_TYPE_ICONS in shared/interfaces (Xtream
cloud, Stalker cast, M3U playlist_play / link / description / subject) in the
add dialog, auto-import, empty state, playlist switcher, playlist rows,
dashboard source rail, command palette, Sources filters and both reset
summaries. Stalker no longer borrows the Dashboard icon, and Xtream no longer
shares a glyph with M3U URL playlists.

The playlist error view removed a playlist through the stale
PlaylistActions.removePlaylist: it dropped the playlist from state before the
delete ran, swallowed failures, skipped the source activity guard and showed
no toast. It now uses PlaylistDeleteActionService like every other removal,
commits only a completed delete, toasts and goes home. The unused action and
its effect are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-01 18:59:23 +02:00
1 parent 572034f3be
commit 082c56608b
50 files changed
+584 -103

No files matched your search

@@ -59,7 +59,11 @@
<mat-dialog-actions align="end">
@if (currentMapping()) {
<button mat-button color="warn" (click)="removeMapping()">
<button
mat-button
class="app-destructive-button"
(click)="removeMapping()"
>
{{ 'EPG_MAPPING_DIALOG.REMOVE' | translate }}
</button>
}
@@ -52,3 +52,49 @@ describe('ConfirmDialogComponent actions', () => {
}
);
});
describe('ConfirmDialogComponent labels and tone', () => {
async function render(data: Record<string, unknown>) {
await TestBed.configureTestingModule({
imports: [
ConfirmDialogComponent,
NoopAnimationsModule,
TranslateModule.forRoot(),
],
providers: [
{
provide: MAT_DIALOG_DATA,
useValue: {
title: 'Remove playlist',
message: 'Delete it?',
confirmLabel: 'Remove playlist',
onConfirm: jest.fn(),
...data,
},
},
{ provide: MatDialogRef, useValue: { close: jest.fn() } },
],
}).compileComponents();
const fixture = TestBed.createComponent(ConfirmDialogComponent);
fixture.detectChanges();
const buttons = fixture.nativeElement.querySelectorAll(
'button'
) as NodeListOf<HTMLButtonElement>;
return { cancel: buttons[0], confirm: buttons[1] };
}
it('names the action instead of answering "Yes", and cancels by default', async () => {
const { cancel, confirm } = await render({});
expect(confirm.textContent?.trim()).toBe('Remove playlist');
// No translations are loaded, so the key is the rendered text.
expect(cancel.textContent?.trim()).toBe('CANCEL');
expect(confirm.classList).not.toContain('app-destructive-button');
});
it('styles destructive confirmations as destructive', async () => {
const { confirm } = await render({ tone: 'destructive' });
expect(confirm.classList).toContain('app-destructive-button');
});
});
@@ -3,12 +3,25 @@ import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
/**
* `destructive` renders the confirm action with the app-wide error styling
* (`.app-destructive-button`). Use it whenever confirming removes or discards
* user data; Material's `warn` color input has no effect with the M3 theme.
*/
export type ConfirmDialogTone = 'default' | 'destructive';
export interface ConfirmDialogData {
title: string;
message: string;
width?: string;
confirmLabel?: string;
/**
* Translated verb that names the action ("Remove playlist", "Clear").
* Required so a confirmation never falls back to an ambiguous "Yes".
*/
confirmLabel: string;
/** Translated dismiss label; defaults to "Cancel". */
cancelLabel?: string;
tone?: ConfirmDialogTone;
/** Run the action in the dialog; only Close/backdrop/Escape dismiss it. */
keepOpenOnConfirm?: boolean;
onConfirm: () => void;
@@ -27,23 +40,25 @@ export interface ConfirmDialogData {
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close cdkFocusInitial>
{{ dialogData.cancelLabel || 'NO' | translate }}
{{ dialogData.cancelLabel || ('CANCEL' | translate) }}
</button>
@if (dialogData.keepOpenOnConfirm) {
<button
mat-flat-button
data-test-id="confirm-dialog-confirm"
[class.app-destructive-button]="isDestructive"
(click)="dialogData.onConfirm()"
color="primary"
>
{{ dialogData.confirmLabel || 'YES' | translate }}
{{ dialogData.confirmLabel }}
</button>
} @else {
<button
mat-flat-button
data-test-id="confirm-dialog-confirm"
[class.app-destructive-button]="isDestructive"
[mat-dialog-close]="true"
color="primary"
>
{{ dialogData.confirmLabel || 'YES' | translate }}
{{ dialogData.confirmLabel }}
</button>
}
</mat-dialog-actions>
@@ -56,4 +71,8 @@ export class ConfirmDialogComponent {
constructor() {
this.dialogData = this.data;
}
get isDestructive(): boolean {
return this.dialogData.tone === 'destructive';
}
}