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 => ({

View File

@@ -1097,6 +1097,8 @@ export const en: Translations = {
qrCode: 'QR Code',
bankCardPayment: 'Bank card payment',
guest: 'Guest',
cancel: 'Cancel',
confirm: 'Confirm',
},
location: {
allRegions: 'All regions',

View File

@@ -1097,6 +1097,8 @@ export const hy: Translations = {
qrCode: 'QR կոդ',
bankCardPayment: 'Վճարում բանկային քարտով',
guest: 'Հյուր',
cancel: 'Չեղարկել',
confirm: 'Հաստատել',
},
location: {
allRegions: 'Բոլոր տարածաշրջանները',

View File

@@ -1097,6 +1097,8 @@ export const ru: Translations = {
qrCode: 'QR-код',
bankCardPayment: 'Оплата банковской картой',
guest: 'Гость',
cancel: 'Отмена',
confirm: 'Подтвердить',
},
location: {
allRegions: 'Все регионы',

View File

@@ -1096,6 +1096,8 @@ export interface Translations {
qrCode: string;
bankCardPayment: string;
guest: string;
cancel: string;
confirm: string;
};
location: {
allRegions: string;

View File

@@ -293,4 +293,13 @@
<app-telegram-login />
<app-confirm-dialog
[open]="clearCartConfirmOpen()"
[titleText]="'cart.confirmClear' | translate"
[message]="'cart.confirmClear' | translate"
[destructive]="true"
(confirmed)="confirmClearCart()"
(cancelled)="clearCartConfirmOpen.set(false)"
/>

View File

@@ -19,6 +19,8 @@ import { EmptyStateComponent } from '../../shared/ui/empty-state/empty-state.com
import { ButtonComponent } from '../../shared/ui/button/button.component';
import { ConfigService } from '../../core/config/config.service';
import { TenantResolverService } from '../../core/config/tenant-resolver.service';
import { UserNotificationService } from '../../features/website/user-experience/services/user-notification.service';
import { ConfirmDialogComponent } from '../../shared/ui/confirm-dialog/confirm-dialog.component';
type PaymentMethod = 'qr' | 'card';
@@ -27,7 +29,7 @@ const MODAL_FOCUSABLE_SELECTOR =
@Component({
selector: 'app-cart',
imports: [DecimalPipe, RouterLink, FormsModule, DeliverySelectorComponent, TelegramLoginComponent, LangRoutePipe, TranslatePipe, IconComponent, EmptyStateComponent, ButtonComponent],
imports: [DecimalPipe, RouterLink, FormsModule, DeliverySelectorComponent, TelegramLoginComponent, LangRoutePipe, TranslatePipe, IconComponent, EmptyStateComponent, ButtonComponent, ConfirmDialogComponent],
templateUrl: './cart.component.html',
styleUrls: ['./cart.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@@ -44,6 +46,7 @@ export class CartComponent implements OnDestroy {
private i18n = inject(TranslateService);
private authService = inject(AuthService);
private notifications = inject(UserNotificationService);
isAuthenticated = this.authService.isAuthenticated;
@@ -240,10 +243,15 @@ export class CartComponent implements OnDestroy {
document.addEventListener('touchend', cleanup);
}
readonly clearCartConfirmOpen = signal(false);
clearCart(): void {
if (confirm(this.i18n.t('cart.confirmClear'))) {
this.cartService.clearCart();
this.clearCartConfirmOpen.set(true);
}
confirmClearCart(): void {
this.cartService.clearCart();
this.clearCartConfirmOpen.set(false);
}
readonly getMainImage = getMainImage;
@@ -262,12 +270,12 @@ export class CartComponent implements OnDestroy {
checkout(paymentMethod: PaymentMethod): void {
if (!this.allRequiredDeliveriesSelected()) {
alert(this.i18n.t('cart.deliveryRequired'));
this.notifications.show(this.i18n.t('cart.deliveryRequired'), 'warning');
return;
}
if (!this.termsAccepted) {
alert(this.i18n.t('cart.acceptTerms'));
this.notifications.show(this.i18n.t('cart.acceptTerms'), 'warning');
return;
}
this.openPaymentPopup(paymentMethod);
@@ -615,8 +623,7 @@ export class CartComponent implements OnDestroy {
this.apiService.submitPurchaseEmail(emailData).subscribe({
next: () => {
this.emailSubmitting.set(false);
// Show success message
alert(this.i18n.t('cart.emailSuccess'));
this.notifications.show(this.i18n.t('cart.emailSuccess'), 'success');
// Close popup and redirect to home page
setTimeout(() => {
this.closePaymentPopup();
@@ -627,7 +634,7 @@ export class CartComponent implements OnDestroy {
error: (err) => {
console.error('Error submitting email:', err);
this.emailSubmitting.set(false);
alert(this.i18n.t('cart.emailError'));
this.notifications.show(this.i18n.t('cart.emailError'), 'warning');
}
});
}

View File

@@ -0,0 +1,11 @@
<app-dialog [open]="open()" [titleText]="titleText()" size="sm" (closed)="cancelled.emit()">
<p class="app-confirm-dialog__message">{{ message() }}</p>
<div class="app-confirm-dialog__actions">
<app-button variant="secondary" (click)="cancelled.emit()">
{{ cancelLabel() ?? ('common.cancel' | translate) }}
</app-button>
<app-button [variant]="destructive() ? 'danger' : 'primary'" (click)="confirmed.emit()">
{{ confirmLabel() ?? ('common.confirm' | translate) }}
</app-button>
</div>
</app-dialog>

View File

@@ -0,0 +1,10 @@
.app-confirm-dialog__message {
margin: 0 0 var(--space-lg, 1.5rem);
color: var(--text-secondary);
}
.app-confirm-dialog__actions {
display: flex;
justify-content: flex-end;
gap: var(--space-sm, 0.5rem);
}

View File

@@ -0,0 +1,24 @@
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
import { DialogComponent } from '../dialog/dialog.component';
import { ButtonComponent } from '../button/button.component';
import { TranslatePipe } from '../../../i18n/translate.pipe';
@Component({
selector: 'app-confirm-dialog',
standalone: true,
imports: [DialogComponent, ButtonComponent, TranslatePipe],
templateUrl: './confirm-dialog.component.html',
styleUrl: './confirm-dialog.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ConfirmDialogComponent {
readonly open = input(false);
readonly titleText = input<string | null>(null);
readonly message = input<string>('');
readonly confirmLabel = input<string | null>(null);
readonly cancelLabel = input<string | null>(null);
readonly destructive = input(false);
readonly confirmed = output<void>();
readonly cancelled = output<void>();
}