fix(ui): replace native confirm()/alert() with shared dialogs and toasts

New app-confirm-dialog (wraps existing app-dialog + app-button) replaces
every native confirm() across media library bulk-delete, static pages
editor (delete/bulk-delete), builder save-bar (publish/reset-draft),
project-editor-page (reset-section), homepage/languages/widgets sections
(remove block/language/widget), and cart (clear-cart).

Cart's native alert() calls (delivery/terms validation, email send
success/failure) now route through the existing UserNotificationService
toast pipeline instead.

No native confirm()/alert()/prompt() remain in production UI.
This commit is contained in:
sdarbinyan
2026-07-26 00:08:00 +04:00
parent a670ca994f
commit 6c6fa00ccf
23 changed files with 251 additions and 41 deletions

View File

@@ -127,4 +127,17 @@
<app-button variant="danger" (click)="proceedDelete()">{{ 'mediaLibrary.confirm' | translate }}</app-button>
</div>
</app-dialog>
<app-dialog
[open]="bulkDeleteConfirmOpen()"
[titleText]="'mediaLibrary.deleteConfirmTitle' | translate"
size="sm"
(closed)="bulkDeleteConfirmOpen.set(false)"
>
<p>{{ 'mediaLibrary.bulkDeleteConfirm' | translate }}</p>
<div class="media-page__dialog-actions">
<app-button variant="secondary" (click)="bulkDeleteConfirmOpen.set(false)">{{ 'mediaLibrary.cancel' | translate }}</app-button>
<app-button variant="danger" (click)="proceedBulkDelete()">{{ 'mediaLibrary.confirm' | translate }}</app-button>
</div>
</app-dialog>
</section>

View File

@@ -49,6 +49,7 @@ export class MediaLibraryPageComponent implements OnInit {
@ViewChild('fileInput') private fileInput?: ElementRef<HTMLInputElement>;
protected readonly pendingDelete = signal<MediaAsset | null>(null);
protected readonly bulkDeleteConfirmOpen = signal(false);
protected readonly detailsAsset = signal<MediaAsset | null>(null);
protected readonly dragActive = signal(false);
protected readonly totalPages = () => Math.max(1, Math.ceil(this.facade.total() / PAGE_SIZE));
@@ -212,10 +213,15 @@ export class MediaLibraryPageComponent implements OnInit {
return this.facade.items().filter(a => this.facade.isSelected(a.id));
}
protected async bulkDelete(): Promise<void> {
protected bulkDelete(): void {
if (this.selectedAssets().length === 0) return;
this.bulkDeleteConfirmOpen.set(true);
}
protected async proceedBulkDelete(): Promise<void> {
const ids = this.selectedAssets().map(a => a.id);
this.bulkDeleteConfirmOpen.set(false);
if (ids.length === 0) return;
if (!confirm(this.translate.t('mediaLibrary.bulkDeleteConfirm'))) return;
await this.facade.bulkDelete(ids);
}

View File

@@ -94,4 +94,22 @@
}
<app-media-picker [open]="mediaPickerOpen" (selected)="onImagePicked($event)" (closed)="mediaPickerOpen = false" />
<app-confirm-dialog
[open]="!!pendingDeletePageId()"
[titleText]="'staticPages.confirmDeletePage' | translate"
[message]="'staticPages.confirmDeletePage' | translate"
[destructive]="true"
(confirmed)="confirmDeletePage()"
(cancelled)="pendingDeletePageId.set(null)"
/>
<app-confirm-dialog
[open]="bulkDeleteConfirmOpen()"
[titleText]="'staticPages.confirmBulkDelete' | translate"
[message]="'staticPages.confirmBulkDelete' | translate"
[destructive]="true"
(confirmed)="confirmBulkDelete()"
(cancelled)="bulkDeleteConfirmOpen.set(false)"
/>
</app-section-card>

View File

@@ -20,6 +20,7 @@ import { StaticPagePreviewComponent } from './static-page-preview/static-page-pr
import { ContentDashboardComponent } from './content-dashboard/content-dashboard.component';
import { PageCardComponent, PageSeoStatus } from './page-card/page-card.component';
import { PageEditorComponent, PageMediaField } from './page-editor/page-editor.component';
import { ConfirmDialogComponent } from '../../../shared/ui/confirm-dialog/confirm-dialog.component';
type StatusFilter = 'all' | ContentPageStatus;
const ALL_LOCALES_FILTER = 'all';
@@ -42,6 +43,7 @@ const ALL_LOCALES_FILTER = 'all';
ContentDashboardComponent,
PageCardComponent,
PageEditorComponent,
ConfirmDialogComponent,
],
templateUrl: './static-pages-editor.component.html',
styleUrls: ['../../project-editor/sections/section.shared.scss', './static-pages-editor.component.scss'],
@@ -259,10 +261,16 @@ export class StaticPagesEditorComponent {
this.persist([...this.allPages(), clone]);
}
readonly pendingDeletePageId = signal<string | null>(null);
deletePage(id: string): void {
if (!confirm(this.translate.t('staticPages.confirmDeletePage'))) {
return;
}
this.pendingDeletePageId.set(id);
}
confirmDeletePage(): void {
const id = this.pendingDeletePageId();
this.pendingDeletePageId.set(null);
if (!id) return;
this.persist(this.allPages().filter(page => page.id !== id));
this.deselect(id);
if (this.editingPageId() === id) {
@@ -378,10 +386,14 @@ export class StaticPagesEditorComponent {
this.selectedIds.set(new Set());
}
readonly bulkDeleteConfirmOpen = signal(false);
bulkDelete(): void {
if (!confirm(this.translate.t('staticPages.confirmBulkDelete'))) {
return;
}
this.bulkDeleteConfirmOpen.set(true);
}
confirmBulkDelete(): void {
this.bulkDeleteConfirmOpen.set(false);
const ids = this.selectedIds();
this.persist(this.allPages().filter(page => !ids.has(page.id)));
this.clearSelection();

View File

@@ -28,3 +28,20 @@
<app-button variant="primary" size="sm" [disabled]="hasBlockingIssues()" (click)="publish()">{{ 'builder.publish' | translate }}</app-button>
</div>
</div>
<app-confirm-dialog
[open]="publishConfirmOpen()"
[titleText]="'builder.confirmPublish' | translate"
[message]="'builder.confirmPublish' | translate"
(confirmed)="confirmPublish()"
(cancelled)="publishConfirmOpen.set(false)"
/>
<app-confirm-dialog
[open]="resetDraftConfirmOpen()"
[titleText]="'builder.confirmResetDraft' | translate"
[message]="'builder.confirmResetDraft' | translate"
[destructive]="true"
(confirmed)="confirmResetDraft()"
(cancelled)="resetDraftConfirmOpen.set(false)"
/>

View File

@@ -1,13 +1,14 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { TranslatePipe } from '../../../../i18n/translate.pipe';
import { TranslateService } from '../../../../i18n/translate.service';
import { ProjectEditorFacade } from '../../facade/project-editor.facade';
import { ButtonComponent } from '../../../../shared/ui/button/button.component';
import { ConfirmDialogComponent } from '../../../../shared/ui/confirm-dialog/confirm-dialog.component';
@Component({
selector: 'app-project-editor-save-bar',
standalone: true,
imports: [TranslatePipe, ButtonComponent],
imports: [TranslatePipe, ButtonComponent, ConfirmDialogComponent],
templateUrl: './project-editor-save-bar.component.html',
styleUrls: ['./project-editor-save-bar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@@ -36,16 +37,25 @@ export class ProjectEditorSaveBarComponent {
this.facade.save();
}
readonly publishConfirmOpen = signal(false);
readonly resetDraftConfirmOpen = signal(false);
publish(): void {
if (confirm(this.translate.t('builder.confirmPublish'))) {
this.facade.publish();
}
this.publishConfirmOpen.set(true);
}
confirmPublish(): void {
this.publishConfirmOpen.set(false);
this.facade.publish();
}
resetDraft(): void {
if (confirm(this.translate.t('builder.confirmResetDraft'))) {
this.facade.resetDraft();
}
this.resetDraftConfirmOpen.set(true);
}
confirmResetDraft(): void {
this.resetDraftConfirmOpen.set(false);
this.facade.resetDraft();
}
formatSavedAt(timestamp: number): string {

View File

@@ -107,3 +107,12 @@
}
</main>
</div>
<app-confirm-dialog
[open]="resetSectionConfirmOpen()"
[titleText]="'builder.confirmResetSection' | translate"
[message]="'builder.confirmResetSection' | translate"
[destructive]="true"
(confirmed)="confirmResetActiveSection()"
(cancelled)="resetSectionConfirmOpen.set(false)"
/>

View File

@@ -18,6 +18,7 @@ import { ProjectEditorPreviewSectionComponent } from '../sections/preview-sectio
import { TranslatePipe } from '../../../i18n/translate.pipe';
import { TranslateService } from '../../../i18n/translate.service';
import { LanguageService } from '../../../services/language.service';
import { ConfirmDialogComponent } from '../../../shared/ui/confirm-dialog/confirm-dialog.component';
import { IconComponent } from '../../../shared/ui/icon/icon.component';
import { ButtonComponent } from '../../../shared/ui/button/button.component';
import { StaticPagesEditorComponent } from '../../content-management/components/static-pages-editor.component';
@@ -66,6 +67,7 @@ const SECTION_LABEL_KEYS: Record<ProjectEditorSectionId, string> = {
ProjectEditorSaveBarComponent,
IconComponent,
ButtonComponent,
ConfirmDialogComponent,
],
templateUrl: './project-editor-page.component.html',
styleUrls: ['./project-editor-page.component.scss'],
@@ -114,10 +116,15 @@ export class ProjectEditorPageComponent {
});
}
readonly resetSectionConfirmOpen = signal(false);
resetActiveSection(): void {
if (confirm(this.translate.t('builder.confirmResetSection'))) {
this.facade.resetSection(this.activeSection());
}
this.resetSectionConfirmOpen.set(true);
}
confirmResetActiveSection(): void {
this.resetSectionConfirmOpen.set(false);
this.facade.resetSection(this.activeSection());
}
toggleHelp(): void {

View File

@@ -82,4 +82,13 @@
</div>
</div>
</app-section-card>
<app-confirm-dialog
[open]="!!pendingRemoveBlockId()"
[titleText]="'builder.blockRemoveConfirm' | translate"
[message]="'builder.blockRemoveConfirm' | translate"
[destructive]="true"
(confirmed)="confirmRemoveBlock()"
(cancelled)="pendingRemoveBlockId.set(null)"
/>
}

View File

@@ -1,5 +1,5 @@
import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ProjectEditorFacade } from '../facade/project-editor.facade';
import { TranslatePipe } from '../../../i18n/translate.pipe';
@@ -12,6 +12,7 @@ import { HomepageOverviewComponent } from './homepage/homepage-overview.componen
import { SectionConfig } from '../../../shared/models/config';
import { IconComponent } from '../../../shared/ui/icon/icon.component';
import { AppIconName } from '../../../shared/ui/icon/icon-registry';
import { ConfirmDialogComponent } from '../../../shared/ui/confirm-dialog/confirm-dialog.component';
export interface LayoutStrategyOption {
value: 'stack' | 'grid' | 'hero' | 'carousel' | 'split';
@@ -44,7 +45,7 @@ const BLOCK_BY_TYPE = new Map(BLOCK_CATALOG.map(entry => [entry.type, entry]));
@Component({
selector: 'app-project-editor-homepage-section',
standalone: true,
imports: [DragDropModule, FormsModule, TranslatePipe, InputComponent, SectionCardComponent, ToggleComponent, EmptyStateComponent, HomepageOverviewComponent, IconComponent],
imports: [DragDropModule, FormsModule, TranslatePipe, InputComponent, SectionCardComponent, ToggleComponent, EmptyStateComponent, HomepageOverviewComponent, IconComponent, ConfirmDialogComponent],
templateUrl: './homepage-section.component.html',
styleUrls: ['./section.shared.scss', './homepage-section.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@@ -164,10 +165,16 @@ export class ProjectEditorHomepageSectionComponent {
this.replaceSections([...this.sections(), copy]);
}
readonly pendingRemoveBlockId = signal<string | null>(null);
removeBlock(sectionId: string): void {
if (!confirm(this.translate.t('builder.blockRemoveConfirm'))) {
return;
}
this.pendingRemoveBlockId.set(sectionId);
}
confirmRemoveBlock(): void {
const sectionId = this.pendingRemoveBlockId();
this.pendingRemoveBlockId.set(null);
if (!sectionId) return;
this.replaceSections(this.sections().filter(section => section.id !== sectionId));
}

View File

@@ -38,3 +38,12 @@
}
</div>
</app-section-card>
<app-confirm-dialog
[open]="!!pendingRemoveLocale()"
[titleText]="'builder.confirmRemoveLanguage' | translate"
[message]="'builder.confirmRemoveLanguage' | translate"
[destructive]="true"
(confirmed)="confirmRemoveLocale()"
(cancelled)="pendingRemoveLocale.set(null)"
/>

View File

@@ -7,11 +7,12 @@ import { ButtonComponent } from '../../../shared/ui/button/button.component';
import { InputComponent } from '../../../shared/ui/input/input.component';
import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component';
import { LocaleTabsComponent } from '../../../shared/ui/locale-tabs/locale-tabs.component';
import { ConfirmDialogComponent } from '../../../shared/ui/confirm-dialog/confirm-dialog.component';
@Component({
selector: 'app-project-editor-languages-section',
standalone: true,
imports: [FormsModule, TranslatePipe, ButtonComponent, InputComponent, SectionCardComponent, LocaleTabsComponent],
imports: [FormsModule, TranslatePipe, ButtonComponent, InputComponent, SectionCardComponent, LocaleTabsComponent, ConfirmDialogComponent],
templateUrl: './languages-section.component.html',
styleUrls: ['./section.shared.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@@ -58,10 +59,17 @@ export class ProjectEditorLanguagesSectionComponent {
this.addLocaleError.set(null);
}
readonly pendingRemoveLocale = signal<string | null>(null);
removeLocale(code: string): void {
if (confirm(this.translate.t('builder.confirmRemoveLanguage'))) {
this.facade.removeLocale(code);
}
this.pendingRemoveLocale.set(code);
}
confirmRemoveLocale(): void {
const code = this.pendingRemoveLocale();
this.pendingRemoveLocale.set(null);
if (!code) return;
this.facade.removeLocale(code);
}
setDefault(code: string): void {

View File

@@ -108,4 +108,13 @@
}
</div>
</app-section-card>
<app-confirm-dialog
[open]="!!pendingRemoveWidgetId()"
[titleText]="'builder.widgetRemoveConfirm' | translate"
[message]="'builder.widgetRemoveConfirm' | translate"
[destructive]="true"
(confirmed)="confirmRemoveWidget()"
(cancelled)="pendingRemoveWidgetId.set(null)"
/>
}

View File

@@ -11,6 +11,7 @@ import { EmptyStateComponent } from '../../../shared/ui/empty-state/empty-state.
import { WidgetConfig } from '../../../shared/models/config';
import { IconComponent } from '../../../shared/ui/icon/icon.component';
import { AppIconName } from '../../../shared/ui/icon/icon-registry';
import { ConfirmDialogComponent } from '../../../shared/ui/confirm-dialog/confirm-dialog.component';
interface HeroSlideDraft {
title: string;
@@ -32,7 +33,7 @@ const WIDGET_LABEL_KEYS: Record<string, string> = {
@Component({
selector: 'app-project-editor-widgets-section',
standalone: true,
imports: [FormsModule, TranslatePipe, InputComponent, ButtonComponent, SectionCardComponent, ToggleComponent, EmptyStateComponent, IconComponent],
imports: [FormsModule, TranslatePipe, InputComponent, ButtonComponent, SectionCardComponent, ToggleComponent, EmptyStateComponent, IconComponent, ConfirmDialogComponent],
templateUrl: './widgets-section.component.html',
styleUrls: ['./section.shared.scss', './widgets-section.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@@ -108,10 +109,16 @@ export class ProjectEditorWidgetsSectionComponent {
}));
}
readonly pendingRemoveWidgetId = signal<string | null>(null);
removeWidget(widgetId: string): void {
if (!confirm(this.translate.t('builder.widgetRemoveConfirm'))) {
return;
}
this.pendingRemoveWidgetId.set(widgetId);
}
confirmRemoveWidget(): void {
const widgetId = this.pendingRemoveWidgetId();
this.pendingRemoveWidgetId.set(null);
if (!widgetId) return;
this.facade.updateBootstrap(current => ({
...current,
pages: current.pages.map(page => ({