Files
iptvnator/apps/web/src/app/settings/settings-unsaved-changes-dialog.component.ts
T
4grayandClaude Opus 5.5 dd8293e7cf perf(web): make the app root and settings components OnPush
Plan item C6 step 3 for apps/web: the fifteen Eager components switch to
OnPush, among them the app root and the update notification panel that
the idle audit found re-rendering on every idle tick. Their template
state is signals from the settings facades, signal inputs and the shared
reactive settings form.

The checklist flagged the backup import, which patches the form from a
detached file input with no template event. A new spec patches only a
value, which changes no form status, and confirms the OnPush general
section still shows the new theme; it guards that path for the zoneless
flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:16:42 +02:00

105 lines
3.7 KiB
TypeScript

import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
/** What the user picked; closing via backdrop/Esc yields `undefined` = stay. */
export type SettingsUnsavedChangesChoice = 'save' | 'discard';
export interface SettingsUnsavedChangesDialogData {
/** Save-and-leave is offered only while the form can actually be saved. */
canSave: boolean;
}
/**
* Asked once, when the user is about to leave the settings area with staged
* edits. Section switches never show this — the shared form survives them.
* "Cancel" (stay) carries the initial focus so Enter is always the safe
* choice. Labels are single verbs so the row fits one line in every locale.
*/
@Component({
imports: [MatButtonModule, MatDialogModule, MatIconModule, TranslateModule],
styles: [
`
.unsaved-dialog__invalid-hint {
display: flex;
align-items: center;
gap: 8px;
margin: 12px 0 0;
color: var(--mat-sys-error);
font-size: 0.86rem;
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
/* Phone breakpoint: one full-width action per row, in DOM order,
instead of letting a long translation wrap the row unevenly. */
@media (max-width: 640px) {
.unsaved-dialog__actions {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.unsaved-dialog__actions
.mat-mdc-button-base
+ .mat-mdc-button-base {
margin: 0;
}
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h2 mat-dialog-title>
{{ 'SETTINGS.UNSAVED_DIALOG_TITLE' | translate }}
</h2>
<mat-dialog-content class="mat-typography">
<p>{{ 'SETTINGS.UNSAVED_DIALOG_MESSAGE' | translate }}</p>
@if (!data.canSave) {
<p class="unsaved-dialog__invalid-hint">
<mat-icon>error_outline</mat-icon>
<span>{{
'SETTINGS.UNSAVED_DIALOG_INVALID_HINT' | translate
}}</span>
</p>
}
</mat-dialog-content>
<mat-dialog-actions align="end" class="unsaved-dialog__actions">
<button
mat-button
mat-dialog-close
cdkFocusInitial
data-test-id="unsaved-dialog-stay"
>
{{ 'CANCEL' | translate }}
</button>
<button
mat-button
class="app-destructive-button"
[mat-dialog-close]="'discard'"
data-test-id="unsaved-dialog-discard"
>
{{ 'SETTINGS.UNSAVED_DIALOG_DISCARD' | translate }}
</button>
<button
mat-flat-button
color="primary"
[disabled]="!data.canSave"
[mat-dialog-close]="'save'"
data-test-id="unsaved-dialog-save"
>
{{ 'SETTINGS.UNSAVED_DIALOG_SAVE' | translate }}
</button>
</mat-dialog-actions>
`,
})
export class SettingsUnsavedChangesDialogComponent {
readonly data = inject<SettingsUnsavedChangesDialogData>(MAT_DIALOG_DATA);
}