feat(modal): add confirm and confirm delete modals with tests
- Implemented ConfirmModalComponent and ConfirmDeleteModalComponent for confirmation dialogs. - Added modal service methods to open confirm and confirm delete modals. - Created corresponding HTML and SCSS files for both modal components. - Updated ReutilizablesTestPageComponent to utilize new modals. - Enhanced modal service tests to cover new functionality. - Refactored existing tests to accommodate changes in modal behavior and structure.
This commit is contained in:
@@ -5,8 +5,18 @@ import {
|
|||||||
BrowserTestingModule,
|
BrowserTestingModule,
|
||||||
platformBrowserTesting
|
platformBrowserTesting
|
||||||
} from '@angular/platform-browser/testing';
|
} from '@angular/platform-browser/testing';
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
import {
|
||||||
|
afterEach,
|
||||||
|
beforeAll,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
vi
|
||||||
|
} from 'vitest';
|
||||||
|
|
||||||
|
import { ConfirmDeleteModalComponent } from '../../shared/components/confirm-delete-modal/confirm-delete-modal.component';
|
||||||
|
import { ConfirmModalComponent } from '../../shared/components/confirm-modal/confirm-modal.component';
|
||||||
import {
|
import {
|
||||||
MODAL_DATA,
|
MODAL_DATA,
|
||||||
ModalRef,
|
ModalRef,
|
||||||
@@ -122,6 +132,59 @@ describe('ModalService', () => {
|
|||||||
expect(activeModal?.config.data).toEqual({ amount: 3 });
|
expect(activeModal?.config.data).toEqual({ amount: 3 });
|
||||||
expect(activeModal?.ref).toBe(ref);
|
expect(activeModal?.ref).toBe(ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('opens the standard confirm modal with default labels', () => {
|
||||||
|
const ref = service.openConfirm({
|
||||||
|
title: 'Confirmar compra',
|
||||||
|
content: 'Esto confirmara la compra actual.'
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeModal = service.activeModal();
|
||||||
|
|
||||||
|
expect(activeModal?.component).toBe(ConfirmModalComponent);
|
||||||
|
expect(activeModal?.ref).toBe(ref);
|
||||||
|
expect(activeModal?.config).toEqual({
|
||||||
|
title: 'Confirmar compra',
|
||||||
|
data: {
|
||||||
|
content: 'Esto confirmara la compra actual.',
|
||||||
|
confirmLabel: 'Confirmar',
|
||||||
|
cancelLabel: 'Cancelar'
|
||||||
|
},
|
||||||
|
size: 'md',
|
||||||
|
closeOnBackdrop: true,
|
||||||
|
closeOnEscape: true,
|
||||||
|
showCloseButton: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens the delete confirm modal preserving modal overrides', () => {
|
||||||
|
service.openConfirmDelete({
|
||||||
|
title: 'Eliminar producto',
|
||||||
|
content: 'Se eliminara el producto.',
|
||||||
|
confirmLabel: 'Eliminar',
|
||||||
|
cancelLabel: 'Conservar',
|
||||||
|
size: 'lg',
|
||||||
|
closeOnBackdrop: false,
|
||||||
|
closeOnEscape: false,
|
||||||
|
showCloseButton: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeModal = service.activeModal();
|
||||||
|
|
||||||
|
expect(activeModal?.component).toBe(ConfirmDeleteModalComponent);
|
||||||
|
expect(activeModal?.config).toEqual({
|
||||||
|
title: 'Eliminar producto',
|
||||||
|
data: {
|
||||||
|
content: 'Se eliminara el producto.',
|
||||||
|
confirmLabel: 'Eliminar',
|
||||||
|
cancelLabel: 'Conservar'
|
||||||
|
},
|
||||||
|
size: 'lg',
|
||||||
|
closeOnBackdrop: false,
|
||||||
|
closeOnEscape: false,
|
||||||
|
showCloseButton: false
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { InjectionToken, Injectable, Type, signal } from '@angular/core';
|
import { InjectionToken, Injectable, Type, signal } from '@angular/core';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
import { ConfirmDeleteModalComponent } from '../../shared/components/confirm-delete-modal/confirm-delete-modal.component';
|
||||||
|
import { ConfirmModalComponent } from '../../shared/components/confirm-modal/confirm-modal.component';
|
||||||
|
|
||||||
export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||||
export type ModalDismissReason =
|
export type ModalDismissReason =
|
||||||
@@ -28,6 +30,19 @@ export interface NormalizedModalConfig<TData = unknown>
|
|||||||
showCloseButton: boolean;
|
showCloseButton: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfirmModalData {
|
||||||
|
content: string;
|
||||||
|
confirmLabel: string;
|
||||||
|
cancelLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfirmModalConfig
|
||||||
|
extends Omit<ModalConfig<ConfirmModalData>, 'data'> {
|
||||||
|
content: string;
|
||||||
|
confirmLabel?: string;
|
||||||
|
cancelLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ActiveModalState<TResult = unknown, TData = unknown> {
|
export interface ActiveModalState<TResult = unknown, TData = unknown> {
|
||||||
component: Type<unknown>;
|
component: Type<unknown>;
|
||||||
config: NormalizedModalConfig<TData>;
|
config: NormalizedModalConfig<TData>;
|
||||||
@@ -46,6 +61,11 @@ const DEFAULT_MODAL_CONFIG: Pick<
|
|||||||
showCloseButton: true
|
showCloseButton: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_CONFIRM_MODAL_LABELS = {
|
||||||
|
confirmLabel: 'Confirmar',
|
||||||
|
cancelLabel: 'Cancelar'
|
||||||
|
} satisfies Pick<ConfirmModalData, 'confirmLabel' | 'cancelLabel'>;
|
||||||
|
|
||||||
export class ModalRef<TResult = unknown> {
|
export class ModalRef<TResult = unknown> {
|
||||||
private readonly afterClosedSubject = new Subject<TResult | undefined>();
|
private readonly afterClosedSubject = new Subject<TResult | undefined>();
|
||||||
private closed = false;
|
private closed = false;
|
||||||
@@ -115,6 +135,17 @@ export class ModalService {
|
|||||||
return ref;
|
return ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openConfirm(config: ConfirmModalConfig): ModalRef<boolean> {
|
||||||
|
return this.open(ConfirmModalComponent, this.buildConfirmModalConfig(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
openConfirmDelete(config: ConfirmModalConfig): ModalRef<boolean> {
|
||||||
|
return this.open(
|
||||||
|
ConfirmDeleteModalComponent,
|
||||||
|
this.buildConfirmModalConfig(config)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private close<TResult>(ref: ModalRef<TResult>, result?: TResult): void {
|
private close<TResult>(ref: ModalRef<TResult>, result?: TResult): void {
|
||||||
if (this.activeModalState()?.ref !== ref) {
|
if (this.activeModalState()?.ref !== ref) {
|
||||||
return;
|
return;
|
||||||
@@ -144,4 +175,24 @@ export class ModalService {
|
|||||||
...config
|
...config
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildConfirmModalConfig(
|
||||||
|
config: ConfirmModalConfig
|
||||||
|
): ModalConfig<ConfirmModalData> {
|
||||||
|
const {
|
||||||
|
content,
|
||||||
|
confirmLabel = DEFAULT_CONFIRM_MODAL_LABELS.confirmLabel,
|
||||||
|
cancelLabel = DEFAULT_CONFIRM_MODAL_LABELS.cancelLabel,
|
||||||
|
...modalConfig
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...modalConfig,
|
||||||
|
data: {
|
||||||
|
content,
|
||||||
|
confirmLabel,
|
||||||
|
cancelLabel
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,9 @@
|
|||||||
<app-button variant="secondary" [disabled]="true">Secondary</app-button>
|
<app-button variant="secondary" [disabled]="true">Secondary</app-button>
|
||||||
|
|
||||||
<app-button variant="danger" [disabled]="true">Danger</app-button>
|
<app-button variant="danger" [disabled]="true">Danger</app-button>
|
||||||
<app-button variant="danger-secondary" [disabled]="true">Danger secondary</app-button>
|
<app-button variant="danger-secondary" [disabled]="true">
|
||||||
|
Danger secondary
|
||||||
|
</app-button>
|
||||||
<app-button variant="cancel" [disabled]="true">Cancel</app-button>
|
<app-button variant="cancel" [disabled]="true">Cancel</app-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,29 +48,49 @@
|
|||||||
<h2>Toasts</h2>
|
<h2>Toasts</h2>
|
||||||
<div class="button-grid">
|
<div class="button-grid">
|
||||||
<app-button (click)="triggerToast('info')">Trigger Info</app-button>
|
<app-button (click)="triggerToast('info')">Trigger Info</app-button>
|
||||||
<app-button variant="secondary" (click)="triggerToast('success')">Trigger Success</app-button>
|
<app-button variant="secondary" (click)="triggerToast('success')">
|
||||||
<app-button variant="danger" (click)="triggerToast('danger')">Trigger Danger</app-button>
|
Trigger Success
|
||||||
|
</app-button>
|
||||||
|
<app-button variant="danger" (click)="triggerToast('danger')">
|
||||||
|
Trigger Danger
|
||||||
|
</app-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<h2>Toasts Persistentes (No se van)</h2>
|
<h2>Toasts Persistentes (No se van)</h2>
|
||||||
<div class="button-grid">
|
<div class="button-grid">
|
||||||
<app-button (click)="triggerPersistentToast('info')">Info Persistente</app-button>
|
<app-button (click)="triggerPersistentToast('info')">
|
||||||
<app-button variant="secondary" (click)="triggerPersistentToast('success')">Success Persistente</app-button>
|
Info Persistente
|
||||||
<app-button variant="danger" (click)="triggerPersistentToast('danger')">Danger Persistente</app-button>
|
</app-button>
|
||||||
|
<app-button
|
||||||
|
variant="secondary"
|
||||||
|
(click)="triggerPersistentToast('success')"
|
||||||
|
>
|
||||||
|
Success Persistente
|
||||||
|
</app-button>
|
||||||
|
<app-button variant="danger" (click)="triggerPersistentToast('danger')">
|
||||||
|
Danger Persistente
|
||||||
|
</app-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<h2>Modal</h2>
|
<h2>Modal</h2>
|
||||||
<p class="text-muted mb-0">
|
<p class="text-muted mb-0">
|
||||||
Demo del modal global con servicio, host y devolución de resultado.
|
Demo del modal global con servicio, host y devolucion de resultado.
|
||||||
</p>
|
</p>
|
||||||
<div class="button-grid">
|
<div class="button-grid">
|
||||||
<app-button (click)="openBasicModal()">Abrir modal simple</app-button>
|
<app-button (click)="openBasicModal()">Abrir confirm modal</app-button>
|
||||||
<app-button variant="secondary" (click)="openLockedModal()">Abrir modal bloqueado</app-button>
|
<app-button variant="danger" (click)="openConfirmDeleteModal()">
|
||||||
<app-button variant="danger-secondary" (click)="openWideModal()">Abrir modal ancho</app-button>
|
Abrir confirm delete
|
||||||
|
</app-button>
|
||||||
|
<app-button variant="secondary" (click)="openLockedModal()">
|
||||||
|
Abrir modal bloqueado
|
||||||
|
</app-button>
|
||||||
|
<app-button variant="danger-secondary" (click)="openWideModal()">
|
||||||
|
Abrir modal ancho
|
||||||
|
</app-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-showcase__result" data-testid="modal-last-result">
|
<div class="modal-showcase__result" data-testid="modal-last-result">
|
||||||
{{ lastModalResult }}
|
{{ lastModalResult }}
|
||||||
@@ -81,11 +103,12 @@
|
|||||||
<div class="icon-button-showcase">
|
<div class="icon-button-showcase">
|
||||||
<h2>Icon Buttons</h2>
|
<h2>Icon Buttons</h2>
|
||||||
<p class="text-muted mb-4">
|
<p class="text-muted mb-4">
|
||||||
Botones de ícono pequeños con color base <code>#666666</code>, deshabilitados con <code>#A0A0A0</code> y estados hover/active con color de marca (a excepción de trash que usa danger).
|
Botones de icono pequenos con color base <code>#666666</code>,
|
||||||
|
deshabilitados con <code>#A0A0A0</code> y estados hover/active con color
|
||||||
|
de marca (a excepcion de trash que usa danger).
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="icon-button-showcase__grid">
|
<div class="icon-button-showcase__grid">
|
||||||
<!-- Disabled Row -->
|
|
||||||
<div class="icon-button-showcase__row">
|
<div class="icon-button-showcase__row">
|
||||||
<span class="icon-button-showcase__label">Disabled</span>
|
<span class="icon-button-showcase__label">Disabled</span>
|
||||||
<div class="icon-button-showcase__items">
|
<div class="icon-button-showcase__items">
|
||||||
@@ -99,7 +122,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Normal Row -->
|
|
||||||
<div class="icon-button-showcase__row">
|
<div class="icon-button-showcase__row">
|
||||||
<span class="icon-button-showcase__label">Normal</span>
|
<span class="icon-button-showcase__label">Normal</span>
|
||||||
<div class="icon-button-showcase__items">
|
<div class="icon-button-showcase__items">
|
||||||
@@ -113,7 +135,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hover/Active Row -->
|
|
||||||
<div class="icon-button-showcase__row">
|
<div class="icon-button-showcase__row">
|
||||||
<span class="icon-button-showcase__label">Active / Hover</span>
|
<span class="icon-button-showcase__label">Active / Hover</span>
|
||||||
<div class="icon-button-showcase__items">
|
<div class="icon-button-showcase__items">
|
||||||
@@ -136,7 +157,6 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="icon-button-showcase__grid">
|
<div class="icon-button-showcase__grid">
|
||||||
<!-- Disabled Row -->
|
|
||||||
<div class="icon-button-showcase__row">
|
<div class="icon-button-showcase__row">
|
||||||
<span class="icon-button-showcase__label">Disabled</span>
|
<span class="icon-button-showcase__label">Disabled</span>
|
||||||
<div class="icon-button-showcase__items">
|
<div class="icon-button-showcase__items">
|
||||||
@@ -145,7 +165,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Normal Row -->
|
|
||||||
<div class="icon-button-showcase__row">
|
<div class="icon-button-showcase__row">
|
||||||
<span class="icon-button-showcase__label">Normal</span>
|
<span class="icon-button-showcase__label">Normal</span>
|
||||||
<div class="icon-button-showcase__items">
|
<div class="icon-button-showcase__items">
|
||||||
@@ -154,7 +173,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hover/Active Row -->
|
|
||||||
<div class="icon-button-showcase__row">
|
<div class="icon-button-showcase__row">
|
||||||
<span class="icon-button-showcase__label">Active / Hover</span>
|
<span class="icon-button-showcase__label">Active / Hover</span>
|
||||||
<div class="icon-button-showcase__items">
|
<div class="icon-button-showcase__items">
|
||||||
@@ -292,7 +310,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-button variant="secondary" (click)="clearFile()">Limpiar archivo</app-button>
|
<app-button variant="secondary" (click)="clearFile()">
|
||||||
|
Limpiar archivo
|
||||||
|
</app-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -301,7 +321,8 @@
|
|||||||
<div class="paginator-showcase">
|
<div class="paginator-showcase">
|
||||||
<h2>Paginator</h2>
|
<h2>Paginator</h2>
|
||||||
<p class="text-muted mb-0">
|
<p class="text-muted mb-0">
|
||||||
Componente shared para navegar por paginas con primera, anterior, siguiente y ultima.
|
Componente shared para navegar por paginas con primera, anterior,
|
||||||
|
siguiente y ultima.
|
||||||
</p>
|
</p>
|
||||||
<app-paginator
|
<app-paginator
|
||||||
[page]="currentPage"
|
[page]="currentPage"
|
||||||
@@ -322,7 +343,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!cartVisible) {
|
@if (!cartVisible) {
|
||||||
<app-button variant="secondary" (click)="showCart()">Volver a mostrar carrito</app-button>
|
<app-button variant="secondary" (click)="showCart()">
|
||||||
|
Volver a mostrar carrito
|
||||||
|
</app-button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -346,16 +369,19 @@
|
|||||||
<section class="component-demo card shadow-sm mt-4 configuracion-tennant">
|
<section class="component-demo card shadow-sm mt-4 configuracion-tennant">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<span class="eyebrow">Tenant Configuration</span>
|
<span class="eyebrow">Tenant Configuration</span>
|
||||||
<h2 class="mb-4">Configuración Tenant</h2>
|
<h2 class="mb-4">Configuracion Tenant</h2>
|
||||||
|
|
||||||
<!-- Logos section -->
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-6 mb-3 mb-md-0">
|
<div class="col-md-6 mb-3 mb-md-0">
|
||||||
<div class="tenant-logo-card">
|
<div class="tenant-logo-card">
|
||||||
<div class="tenant-logo-card__label">Logo del Header</div>
|
<div class="tenant-logo-card__label">Logo del Header</div>
|
||||||
<div class="tenant-logo-card__container header-bg">
|
<div class="tenant-logo-card__container header-bg">
|
||||||
@if (tenant()?.header_logo) {
|
@if (tenant()?.header_logo) {
|
||||||
<img [src]="tenant()?.header_logo" alt="Header Logo" class="tenant-logo-card__img" />
|
<img
|
||||||
|
[src]="tenant()?.header_logo"
|
||||||
|
alt="Header Logo"
|
||||||
|
class="tenant-logo-card__img"
|
||||||
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="tenant-logo-card__placeholder">
|
<div class="tenant-logo-card__placeholder">
|
||||||
<i class="fa-solid fa-image mb-2"></i>
|
<i class="fa-solid fa-image mb-2"></i>
|
||||||
@@ -371,7 +397,11 @@
|
|||||||
<div class="tenant-logo-card__label">Logo del Footer</div>
|
<div class="tenant-logo-card__label">Logo del Footer</div>
|
||||||
<div class="tenant-logo-card__container footer-bg">
|
<div class="tenant-logo-card__container footer-bg">
|
||||||
@if (tenant()?.footer_logo) {
|
@if (tenant()?.footer_logo) {
|
||||||
<img [src]="tenant()?.footer_logo" alt="Footer Logo" class="tenant-logo-card__img" />
|
<img
|
||||||
|
[src]="tenant()?.footer_logo"
|
||||||
|
alt="Footer Logo"
|
||||||
|
class="tenant-logo-card__img"
|
||||||
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="tenant-logo-card__placeholder">
|
<div class="tenant-logo-card__placeholder">
|
||||||
<i class="fa-solid fa-image mb-2"></i>
|
<i class="fa-solid fa-image mb-2"></i>
|
||||||
@@ -385,66 +415,95 @@
|
|||||||
|
|
||||||
<hr class="section-divider" />
|
<hr class="section-divider" />
|
||||||
|
|
||||||
<!-- Colors section -->
|
|
||||||
<div class="colors-section">
|
<div class="colors-section">
|
||||||
<h3 class="mb-3">Colores de Marca</h3>
|
<h3 class="mb-3">Colores de Marca</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<div class="color-swatch-card shadow-sm">
|
<div class="color-swatch-card shadow-sm">
|
||||||
<div class="color-swatch-card__preview" [style.background-color]="'var(--tenant-primary)'"></div>
|
<div
|
||||||
|
class="color-swatch-card__preview"
|
||||||
|
[style.background-color]="'var(--tenant-primary)'"
|
||||||
|
></div>
|
||||||
<div class="color-swatch-card__body">
|
<div class="color-swatch-card__body">
|
||||||
<span class="color-swatch-card__title">Primario</span>
|
<span class="color-swatch-card__title">Primario</span>
|
||||||
<code class="color-swatch-card__value">{{ tenant()?.primary_color || 'Default' }}</code>
|
<code class="color-swatch-card__value">
|
||||||
|
{{ tenant()?.primary_color || 'Default' }}
|
||||||
|
</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<div class="color-swatch-card shadow-sm">
|
<div class="color-swatch-card shadow-sm">
|
||||||
<div class="color-swatch-card__preview" [style.background-color]="'var(--tenant-secondary)'"></div>
|
<div
|
||||||
|
class="color-swatch-card__preview"
|
||||||
|
[style.background-color]="'var(--tenant-secondary)'"
|
||||||
|
></div>
|
||||||
<div class="color-swatch-card__body">
|
<div class="color-swatch-card__body">
|
||||||
<span class="color-swatch-card__title">Secundario</span>
|
<span class="color-swatch-card__title">Secundario</span>
|
||||||
<code class="color-swatch-card__value">{{ tenant()?.secondary_color || 'Default' }}</code>
|
<code class="color-swatch-card__value">
|
||||||
|
{{ tenant()?.secondary_color || 'Default' }}
|
||||||
|
</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<div class="color-swatch-card shadow-sm">
|
<div class="color-swatch-card shadow-sm">
|
||||||
<div class="color-swatch-card__preview" [style.background-color]="'var(--tenant-danger)'"></div>
|
<div
|
||||||
|
class="color-swatch-card__preview"
|
||||||
|
[style.background-color]="'var(--tenant-danger)'"
|
||||||
|
></div>
|
||||||
<div class="color-swatch-card__body">
|
<div class="color-swatch-card__body">
|
||||||
<span class="color-swatch-card__title">Danger</span>
|
<span class="color-swatch-card__title">Danger</span>
|
||||||
<code class="color-swatch-card__value">{{ tenant()?.danger_color || 'Default' }}</code>
|
<code class="color-swatch-card__value">
|
||||||
|
{{ tenant()?.danger_color || 'Default' }}
|
||||||
|
</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<div class="color-swatch-card shadow-sm">
|
<div class="color-swatch-card shadow-sm">
|
||||||
<div class="color-swatch-card__preview" [style.background-color]="'var(--tenant-success)'"></div>
|
<div
|
||||||
|
class="color-swatch-card__preview"
|
||||||
|
[style.background-color]="'var(--tenant-success)'"
|
||||||
|
></div>
|
||||||
<div class="color-swatch-card__body">
|
<div class="color-swatch-card__body">
|
||||||
<span class="color-swatch-card__title">Success</span>
|
<span class="color-swatch-card__title">Success</span>
|
||||||
<code class="color-swatch-card__value">{{ tenant()?.success_color || 'Default' }}</code>
|
<code class="color-swatch-card__value">
|
||||||
|
{{ tenant()?.success_color || 'Default' }}
|
||||||
|
</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<div class="color-swatch-card shadow-sm">
|
<div class="color-swatch-card shadow-sm">
|
||||||
<div class="color-swatch-card__preview" [style.background-color]="'var(--tenant-header-bg)'"></div>
|
<div
|
||||||
|
class="color-swatch-card__preview"
|
||||||
|
[style.background-color]="'var(--tenant-header-bg)'"
|
||||||
|
></div>
|
||||||
<div class="color-swatch-card__body">
|
<div class="color-swatch-card__body">
|
||||||
<span class="color-swatch-card__title">Header BG</span>
|
<span class="color-swatch-card__title">Header BG</span>
|
||||||
<code class="color-swatch-card__value">{{ tenant()?.header_bg_color || 'Default' }}</code>
|
<code class="color-swatch-card__value">
|
||||||
|
{{ tenant()?.header_bg_color || 'Default' }}
|
||||||
|
</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<div class="color-swatch-card shadow-sm">
|
<div class="color-swatch-card shadow-sm">
|
||||||
<div class="color-swatch-card__preview" [style.background-color]="'var(--tenant-footer-bg)'"></div>
|
<div
|
||||||
|
class="color-swatch-card__preview"
|
||||||
|
[style.background-color]="'var(--tenant-footer-bg)'"
|
||||||
|
></div>
|
||||||
<div class="color-swatch-card__body">
|
<div class="color-swatch-card__body">
|
||||||
<span class="color-swatch-card__title">Footer BG</span>
|
<span class="color-swatch-card__title">Footer BG</span>
|
||||||
<code class="color-swatch-card__value">{{ tenant()?.footer_bg_color || 'Default' }}</code>
|
<code class="color-swatch-card__value">
|
||||||
|
{{ tenant()?.footer_bg_color || 'Default' }}
|
||||||
|
</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -457,9 +516,10 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<span class="eyebrow">Reusable Component</span>
|
<span class="eyebrow">Reusable Component</span>
|
||||||
<h2 class="mb-4">Tarjetas de Producto (ProductCard)</h2>
|
<h2 class="mb-4">Tarjetas de Producto (ProductCard)</h2>
|
||||||
|
|
||||||
<p class="text-muted mb-4">
|
<p class="text-muted mb-4">
|
||||||
Las tarjetas se adaptan al tamaño del contenedor padre. A continuación se presentan dentro de una grilla de Bootstrap.
|
Las tarjetas se adaptan al tamano del contenedor padre. A continuacion se
|
||||||
|
presentan dentro de una grilla de Bootstrap.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -479,10 +539,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<app-store-section title="PRODUCTOS" class="component-demo store-section-demo mt-5">
|
<app-store-section
|
||||||
|
title="PRODUCTOS"
|
||||||
|
class="component-demo store-section-demo mt-5"
|
||||||
|
>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@for (product of testProducts; track product.title) {
|
@for (product of testProducts; track product.title) {
|
||||||
<div class="col-12 col-md-6 col-lg-4 mb-4 d-flex align-items-stretch store-section-demo__item">
|
<div
|
||||||
|
class="col-12 col-md-6 col-lg-4 mb-4 d-flex align-items-stretch store-section-demo__item"
|
||||||
|
>
|
||||||
<app-product-card
|
<app-product-card
|
||||||
[imageUrl]="product.imageUrl"
|
[imageUrl]="product.imageUrl"
|
||||||
[title]="product.title"
|
[title]="product.title"
|
||||||
@@ -496,4 +561,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</app-store-section>
|
</app-store-section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { signal } from '@angular/core';
|
import { signal } from '@angular/core';
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed, getTestBed } from '@angular/core/testing';
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import {
|
||||||
import { ReutilizablesTestPageComponent } from './reutilizables-test-page.component';
|
BrowserTestingModule,
|
||||||
|
platformBrowserTesting
|
||||||
|
} from '@angular/platform-browser/testing';
|
||||||
|
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { ModalService } from '../../../../core/services/modal.service';
|
import { ModalService } from '../../../../core/services/modal.service';
|
||||||
import { Tenant } from '../../../../core/services/tenant.interface';
|
import { Tenant } from '../../../../core/services/tenant.interface';
|
||||||
import { TenantService } from '../../../../core/services/tenant.service';
|
import { TenantService } from '../../../../core/services/tenant.service';
|
||||||
import { ToastService } from '../../../../core/services/toast.service';
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
|
import { ReutilizablesTestPageComponent } from './reutilizables-test-page.component';
|
||||||
|
|
||||||
const tenant: Tenant = {
|
const tenant: Tenant = {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -42,18 +47,39 @@ function createToastServiceStub() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createModalRefStub() {
|
||||||
|
return {
|
||||||
|
afterClosed$: {
|
||||||
|
subscribe: vi.fn()
|
||||||
|
},
|
||||||
|
dismissReason: vi.fn().mockReturnValue(null)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function createModalServiceStub() {
|
function createModalServiceStub() {
|
||||||
return {
|
return {
|
||||||
open: vi.fn().mockReturnValue({
|
open: vi.fn().mockReturnValue(createModalRefStub()),
|
||||||
afterClosed$: {
|
openConfirm: vi.fn().mockReturnValue(createModalRefStub()),
|
||||||
subscribe: vi.fn()
|
openConfirmDelete: vi.fn().mockReturnValue(createModalRefStub())
|
||||||
},
|
|
||||||
dismissReason: vi.fn().mockReturnValue(null)
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('ReutilizablesTestPageComponent', () => {
|
describe('ReutilizablesTestPageComponent', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
try {
|
||||||
|
getTestBed().initTestEnvironment(
|
||||||
|
BrowserTestingModule,
|
||||||
|
platformBrowserTesting()
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Test environment may already be initialized by another setup entrypoint.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
});
|
||||||
|
|
||||||
it('renders the tenant configuration section with logo sources and swatch colors', async () => {
|
it('renders the tenant configuration section with logo sources and swatch colors', async () => {
|
||||||
const modalServiceStub = createModalServiceStub();
|
const modalServiceStub = createModalServiceStub();
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
@@ -78,20 +104,20 @@ describe('ReutilizablesTestPageComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const element = fixture.nativeElement as HTMLElement;
|
const element = fixture.nativeElement as HTMLElement;
|
||||||
|
|
||||||
// Check section title
|
|
||||||
const sectionTitle = element.querySelector('.configuracion-tennant h2');
|
const sectionTitle = element.querySelector('.configuracion-tennant h2');
|
||||||
expect(sectionTitle?.textContent).toContain('Configuración Tenant');
|
|
||||||
|
|
||||||
// Check header logo source
|
expect(sectionTitle?.textContent).toContain('Configuracion Tenant');
|
||||||
const headerImg = element.querySelector('.tenant-logo-card img[alt="Header Logo"]') as HTMLImageElement;
|
|
||||||
|
const headerImg = element.querySelector(
|
||||||
|
'.tenant-logo-card img[alt="Header Logo"]'
|
||||||
|
) as HTMLImageElement;
|
||||||
expect(headerImg?.src).toBe(tenant.header_logo);
|
expect(headerImg?.src).toBe(tenant.header_logo);
|
||||||
|
|
||||||
// Check footer logo source
|
const footerImg = element.querySelector(
|
||||||
const footerImg = element.querySelector('.tenant-logo-card img[alt="Footer Logo"]') as HTMLImageElement;
|
'.tenant-logo-card img[alt="Footer Logo"]'
|
||||||
|
) as HTMLImageElement;
|
||||||
expect(footerImg?.src).toBe(tenant.footer_logo);
|
expect(footerImg?.src).toBe(tenant.footer_logo);
|
||||||
|
|
||||||
// Check color hex values
|
|
||||||
expect(element.textContent).toContain(tenant.primary_color);
|
expect(element.textContent).toContain(tenant.primary_color);
|
||||||
expect(element.textContent).toContain(tenant.secondary_color);
|
expect(element.textContent).toContain(tenant.secondary_color);
|
||||||
expect(element.textContent).toContain(tenant.danger_color);
|
expect(element.textContent).toContain(tenant.danger_color);
|
||||||
@@ -124,10 +150,12 @@ describe('ReutilizablesTestPageComponent', () => {
|
|||||||
const element = fixture.nativeElement as HTMLElement;
|
const element = fixture.nativeElement as HTMLElement;
|
||||||
|
|
||||||
expect(element.querySelector('app-paginator')).not.toBeNull();
|
expect(element.querySelector('app-paginator')).not.toBeNull();
|
||||||
expect(element.querySelector('[data-testid="paginator-status"]')?.textContent?.trim()).toBe('1/8');
|
expect(
|
||||||
|
element.querySelector('[data-testid="paginator-status"]')?.textContent?.trim()
|
||||||
|
).toBe('1/8');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the modal showcase and opens the basic modal from the demo button', async () => {
|
it('renders the modal showcase and opens the confirm demos from the buttons', async () => {
|
||||||
const modalServiceStub = createModalServiceStub();
|
const modalServiceStub = createModalServiceStub();
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ReutilizablesTestPageComponent],
|
imports: [ReutilizablesTestPageComponent],
|
||||||
@@ -151,15 +179,24 @@ describe('ReutilizablesTestPageComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const element = fixture.nativeElement as HTMLElement;
|
const element = fixture.nativeElement as HTMLElement;
|
||||||
const modalButtons = Array.from(element.querySelectorAll('.button-group app-button button'))
|
const buttons = Array.from(
|
||||||
.filter((button) => button.textContent?.includes('Abrir modal'));
|
element.querySelectorAll('.button-group app-button button')
|
||||||
|
|
||||||
expect(element.querySelector('[data-testid="modal-last-result"]')?.textContent).toContain(
|
|
||||||
'Todavía no se abrió ningún modal.'
|
|
||||||
);
|
);
|
||||||
|
const confirmButton = buttons.find((button) =>
|
||||||
|
button.textContent?.includes('Abrir confirm modal')
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
const deleteButton = buttons.find((button) =>
|
||||||
|
button.textContent?.includes('Abrir confirm delete')
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
(modalButtons[0] as HTMLButtonElement).click();
|
expect(
|
||||||
|
element.querySelector('[data-testid="modal-last-result"]')?.textContent
|
||||||
|
).toContain('Todavia no se abrio ningun modal.');
|
||||||
|
|
||||||
expect(modalServiceStub.open).toHaveBeenCalled();
|
confirmButton.click();
|
||||||
|
deleteButton.click();
|
||||||
|
|
||||||
|
expect(modalServiceStub.openConfirm).toHaveBeenCalled();
|
||||||
|
expect(modalServiceStub.openConfirmDelete).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,55 +1,19 @@
|
|||||||
import { Component, inject } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { ButtonComponent } from '../../../../shared/components/button/button.component';
|
import { ButtonComponent } from '../../../../shared/components/button/button.component';
|
||||||
import { CartComponent, CartItemMock } from '../../../../shared/components/cart/cart.component';
|
import {
|
||||||
|
CartComponent,
|
||||||
|
CartItemMock
|
||||||
|
} from '../../../../shared/components/cart/cart.component';
|
||||||
|
import { CartIconComponent } from '../../../../shared/components/cart-icon/cart-icon.component';
|
||||||
|
import { IconButtonComponent } from '../../../../shared/components/icon-button/icon-button.component';
|
||||||
import { InputComponent } from '../../../../shared/components/input/input.component';
|
import { InputComponent } from '../../../../shared/components/input/input.component';
|
||||||
import { PaginatorComponent } from '../../../../shared/components/paginator/paginator.component';
|
import { PaginatorComponent } from '../../../../shared/components/paginator/paginator.component';
|
||||||
import { ProductCardComponent } from '../../../../shared/components/product-card/product-card.component';
|
import { ProductCardComponent } from '../../../../shared/components/product-card/product-card.component';
|
||||||
import { StoreSectionComponent } from '../../../../shared/components/store-section/store-section.component';
|
import { StoreSectionComponent } from '../../../../shared/components/store-section/store-section.component';
|
||||||
import { IconButtonComponent } from '../../../../shared/components/icon-button/icon-button.component';
|
import { ModalService } from '../../../../core/services/modal.service';
|
||||||
import { CartIconComponent } from '../../../../shared/components/cart-icon/cart-icon.component';
|
|
||||||
import {
|
|
||||||
MODAL_DATA,
|
|
||||||
ModalRef,
|
|
||||||
ModalService
|
|
||||||
} from '../../../../core/services/modal.service';
|
|
||||||
import { TenantService } from '../../../../core/services/tenant.service';
|
import { TenantService } from '../../../../core/services/tenant.service';
|
||||||
import { ToastService } from '../../../../core/services/toast.service';
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
|
|
||||||
interface ModalDemoData {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
confirmLabel: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-modal-demo-content',
|
|
||||||
imports: [ButtonComponent],
|
|
||||||
template: `
|
|
||||||
<div class="modal-demo-content">
|
|
||||||
<p class="modal-demo-content__eyebrow">Demo modal</p>
|
|
||||||
<h3 class="modal-demo-content__title">{{ data.title }}</h3>
|
|
||||||
<p class="modal-demo-content__description">{{ data.description }}</p>
|
|
||||||
|
|
||||||
<div class="modal-demo-content__actions">
|
|
||||||
<app-button variant="secondary" (click)="dismiss()">Cancelar</app-button>
|
|
||||||
<app-button (click)="confirm()">{{ data.confirmLabel }}</app-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
export class ModalDemoContentComponent {
|
|
||||||
readonly data = inject<ModalDemoData>(MODAL_DATA);
|
|
||||||
private readonly modalRef = inject<ModalRef<string>>(ModalRef);
|
|
||||||
|
|
||||||
protected confirm(): void {
|
|
||||||
this.modalRef.close(`confirm:${this.data.title}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected dismiss(): void {
|
|
||||||
this.modalRef.dismiss('programmatic');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-reutilizables-test-page',
|
selector: 'app-reutilizables-test-page',
|
||||||
imports: [
|
imports: [
|
||||||
@@ -87,13 +51,13 @@ export class ReutilizablesTestPageComponent {
|
|||||||
protected editableDisabled = true;
|
protected editableDisabled = true;
|
||||||
protected currentPage = 1;
|
protected currentPage = 1;
|
||||||
protected cartVisible = true;
|
protected cartVisible = true;
|
||||||
protected lastModalResult = 'Todavía no se abrió ningún modal.';
|
protected lastModalResult = 'Todavia no se abrio ningun modal.';
|
||||||
protected readonly paginatorTotalPages = 8;
|
protected readonly paginatorTotalPages = 8;
|
||||||
protected readonly cartBackgroundColor = '#ffffff';
|
protected readonly cartBackgroundColor = '#ffffff';
|
||||||
|
|
||||||
protected readonly testProducts = [
|
protected readonly testProducts = [
|
||||||
{
|
{
|
||||||
title: 'Pantalón recto de scuba negro',
|
title: 'Pantalon recto de scuba negro',
|
||||||
originalPrice: 95000,
|
originalPrice: 95000,
|
||||||
discount: 20,
|
discount: 20,
|
||||||
transferPrice: 74800,
|
transferPrice: 74800,
|
||||||
@@ -107,7 +71,7 @@ export class ReutilizablesTestPageComponent {
|
|||||||
imageUrl: '/images/zapatillas-running.png'
|
imageUrl: '/images/zapatillas-running.png'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Remera básica blanca (Sin Descuento)',
|
title: 'Remera basica blanca (Sin Descuento)',
|
||||||
originalPrice: 32000,
|
originalPrice: 32000,
|
||||||
discount: null,
|
discount: null,
|
||||||
transferPrice: 30000,
|
transferPrice: 30000,
|
||||||
@@ -118,7 +82,7 @@ export class ReutilizablesTestPageComponent {
|
|||||||
protected readonly cartMockItems: CartItemMock[] = [
|
protected readonly cartMockItems: CartItemMock[] = [
|
||||||
{
|
{
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
product: 'Calza térmica unisex con proceso sense y cintura con mayor agarre',
|
product: 'Calza termica unisex con proceso sense y cintura con mayor agarre',
|
||||||
originalPrice: 95000,
|
originalPrice: 95000,
|
||||||
discountedPrice: 76000,
|
discountedPrice: 76000,
|
||||||
discountPercentage: 20,
|
discountPercentage: 20,
|
||||||
@@ -130,7 +94,7 @@ export class ReutilizablesTestPageComponent {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
product: 'Pantalón recto de scuba negro',
|
product: 'Pantalon recto de scuba negro',
|
||||||
originalPrice: null,
|
originalPrice: null,
|
||||||
discountedPrice: 89000,
|
discountedPrice: 89000,
|
||||||
discountPercentage: null,
|
discountPercentage: null,
|
||||||
@@ -186,83 +150,98 @@ export class ReutilizablesTestPageComponent {
|
|||||||
|
|
||||||
protected hideCart(): void {
|
protected hideCart(): void {
|
||||||
this.cartVisible = false;
|
this.cartVisible = false;
|
||||||
this.toastService.info('Se disparó el evento de cerrado del carrito mock.');
|
this.toastService.info('Se disparo el evento de cerrado del carrito mock.');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected triggerToast(type: 'success' | 'danger' | 'info'): void {
|
protected triggerToast(type: 'success' | 'danger' | 'info'): void {
|
||||||
if (type === 'success') {
|
if (type === 'success') {
|
||||||
this.toastService.success('¡Operación exitosa! El producto se ha guardado correctamente.');
|
this.toastService.success(
|
||||||
|
'Operacion exitosa. El producto se ha guardado correctamente.'
|
||||||
|
);
|
||||||
} else if (type === 'danger') {
|
} else if (type === 'danger') {
|
||||||
this.toastService.danger('¡Alerta de error! Hubo un problema al procesar la solicitud.');
|
this.toastService.danger(
|
||||||
|
'Alerta de error. Hubo un problema al procesar la solicitud.'
|
||||||
|
);
|
||||||
} else if (type === 'info') {
|
} else if (type === 'info') {
|
||||||
this.toastService.info('Información del sistema: Hay una nueva actualización disponible.');
|
this.toastService.info(
|
||||||
|
'Informacion del sistema: Hay una nueva actualizacion disponible.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected triggerPersistentToast(type: 'success' | 'danger' | 'info'): void {
|
protected triggerPersistentToast(type: 'success' | 'danger' | 'info'): void {
|
||||||
if (type === 'success') {
|
if (type === 'success') {
|
||||||
this.toastService.success('¡Éxito persistente! Esta alerta no se cerrará sola.', 0);
|
this.toastService.success(
|
||||||
|
'Exito persistente. Esta alerta no se cerrara sola.',
|
||||||
|
0
|
||||||
|
);
|
||||||
} else if (type === 'danger') {
|
} else if (type === 'danger') {
|
||||||
this.toastService.danger('¡Error persistente! Por favor, atienda este problema y ciérrelo manualmente.', 0);
|
this.toastService.danger(
|
||||||
|
'Error persistente. Por favor, atienda este problema y cierrelo manualmente.',
|
||||||
|
0
|
||||||
|
);
|
||||||
} else if (type === 'info') {
|
} else if (type === 'info') {
|
||||||
this.toastService.info('Información persistente: Mantendremos este aviso en pantalla.', 0);
|
this.toastService.info(
|
||||||
|
'Informacion persistente: Mantendremos este aviso en pantalla.',
|
||||||
|
0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected openBasicModal(): void {
|
protected openBasicModal(): void {
|
||||||
this.openDemoModal({
|
this.openConfirmModal({
|
||||||
title: 'Modal simple',
|
title: 'Confirmar accion',
|
||||||
description: 'Caso base para verificar apertura, cierre y devolución de resultado.',
|
content:
|
||||||
|
'Caso base para verificar apertura, cierre y devolucion de resultado.',
|
||||||
confirmLabel: 'Confirmar'
|
confirmLabel: 'Confirmar'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected openConfirmDeleteModal(): void {
|
||||||
|
const ref = this.modalService.openConfirmDelete({
|
||||||
|
title: 'Eliminar producto',
|
||||||
|
content:
|
||||||
|
'Esta accion eliminara el producto del catalogo. Podras volver a crearlo manualmente.',
|
||||||
|
confirmLabel: 'Eliminar',
|
||||||
|
cancelLabel: 'Conservar'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.handleModalResult(ref);
|
||||||
|
}
|
||||||
|
|
||||||
protected openLockedModal(): void {
|
protected openLockedModal(): void {
|
||||||
this.openDemoModal(
|
this.openConfirmModal({
|
||||||
{
|
title: 'Modal bloqueado',
|
||||||
title: 'Modal bloqueado',
|
content:
|
||||||
description: 'Este modal no se cierra tocando el backdrop ni con la tecla Escape.',
|
'Este modal no se cierra tocando el backdrop ni con la tecla Escape.',
|
||||||
confirmLabel: 'Entendido'
|
confirmLabel: 'Entendido',
|
||||||
},
|
closeOnBackdrop: false,
|
||||||
{
|
closeOnEscape: false
|
||||||
closeOnBackdrop: false,
|
});
|
||||||
closeOnEscape: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected openWideModal(): void {
|
protected openWideModal(): void {
|
||||||
this.openDemoModal(
|
this.openConfirmModal({
|
||||||
{
|
title: 'Modal ancho',
|
||||||
title: 'Modal ancho',
|
content: 'Demuestra una variante visual mas amplia para contenido mas pesado.',
|
||||||
description: 'Demuestra una variante visual más amplia para contenido más pesado.',
|
confirmLabel: 'Seguir',
|
||||||
confirmLabel: 'Seguir'
|
size: 'xl'
|
||||||
},
|
});
|
||||||
{
|
|
||||||
size: 'xl'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private openDemoModal(
|
private openConfirmModal(config: Parameters<ModalService['openConfirm']>[0]): void {
|
||||||
data: ModalDemoData,
|
const ref = this.modalService.openConfirm(config);
|
||||||
overrides: Partial<{
|
|
||||||
size: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
||||||
closeOnBackdrop: boolean;
|
|
||||||
closeOnEscape: boolean;
|
|
||||||
}> = {}
|
|
||||||
): void {
|
|
||||||
const ref = this.modalService.open(ModalDemoContentComponent, {
|
|
||||||
title: data.title,
|
|
||||||
data,
|
|
||||||
...overrides
|
|
||||||
});
|
|
||||||
|
|
||||||
|
this.handleModalResult(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleModalResult(ref: ReturnType<ModalService['openConfirm']>): void {
|
||||||
ref.afterClosed$.subscribe((result) => {
|
ref.afterClosed$.subscribe((result) => {
|
||||||
const dismissReason = ref.dismissReason();
|
const dismissReason = ref.dismissReason();
|
||||||
this.lastModalResult = result
|
this.lastModalResult =
|
||||||
? `Resultado: ${result}`
|
typeof result === 'boolean'
|
||||||
: `Cerrado sin resultado${dismissReason ? ` (${dismissReason})` : ''}.`;
|
? `Resultado: ${result}`
|
||||||
|
: `Cerrado sin resultado${dismissReason ? ` (${dismissReason})` : ''}.`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<div class="confirm-modal">
|
||||||
|
<p class="confirm-modal__content">{{ data.content }}</p>
|
||||||
|
|
||||||
|
<div class="confirm-modal__actions">
|
||||||
|
<app-button variant="danger-secondary" (click)="cancel()">
|
||||||
|
{{ data.cancelLabel }}
|
||||||
|
</app-button>
|
||||||
|
<app-button variant="danger" (click)="confirm()">
|
||||||
|
{{ data.confirmLabel }}
|
||||||
|
</app-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.confirm-modal {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
justify-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-modal__content {
|
||||||
|
margin: 0;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-modal__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import '@angular/compiler';
|
||||||
|
import { TestBed, getTestBed } from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
BrowserTestingModule,
|
||||||
|
platformBrowserTesting
|
||||||
|
} from '@angular/platform-browser/testing';
|
||||||
|
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConfirmModalData,
|
||||||
|
MODAL_DATA,
|
||||||
|
ModalRef
|
||||||
|
} from '../../../core/services/modal.service';
|
||||||
|
import { ConfirmDeleteModalComponent } from './confirm-delete-modal.component';
|
||||||
|
|
||||||
|
describe('ConfirmDeleteModalComponent', () => {
|
||||||
|
const data: ConfirmModalData = {
|
||||||
|
content: 'Se eliminara el elemento seleccionado.',
|
||||||
|
confirmLabel: 'Eliminar',
|
||||||
|
cancelLabel: 'Cancelar'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
try {
|
||||||
|
getTestBed().initTestEnvironment(
|
||||||
|
BrowserTestingModule,
|
||||||
|
platformBrowserTesting()
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Test environment may already be initialized by another setup entrypoint.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the configured content and uses danger for the confirm action', async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ConfirmDeleteModalComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: MODAL_DATA,
|
||||||
|
useValue: data
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ModalRef,
|
||||||
|
useValue: {
|
||||||
|
close: vi.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(ConfirmDeleteModalComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const element = fixture.nativeElement as HTMLElement;
|
||||||
|
const buttons = element.querySelectorAll('button');
|
||||||
|
|
||||||
|
expect(element.textContent).toContain(data.content);
|
||||||
|
expect(element.textContent).toContain(data.confirmLabel);
|
||||||
|
expect(buttons[1].className).toContain('btn-danger');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes with false on cancel and true on confirm', async () => {
|
||||||
|
const closeSpy = vi.fn();
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ConfirmDeleteModalComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: MODAL_DATA,
|
||||||
|
useValue: data
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ModalRef,
|
||||||
|
useValue: {
|
||||||
|
close: closeSpy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(ConfirmDeleteModalComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const buttons = fixture.nativeElement.querySelectorAll('button');
|
||||||
|
|
||||||
|
buttons[0].click();
|
||||||
|
buttons[1].click();
|
||||||
|
|
||||||
|
expect(closeSpy).toHaveBeenNthCalledWith(1, false);
|
||||||
|
expect(closeSpy).toHaveBeenNthCalledWith(2, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConfirmModalData,
|
||||||
|
MODAL_DATA,
|
||||||
|
ModalRef
|
||||||
|
} from '../../../core/services/modal.service';
|
||||||
|
import { ButtonComponent } from '../button/button.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-confirm-delete-modal',
|
||||||
|
imports: [ButtonComponent],
|
||||||
|
templateUrl: './confirm-delete-modal.component.html',
|
||||||
|
styleUrl: './confirm-delete-modal.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class ConfirmDeleteModalComponent {
|
||||||
|
protected readonly data = inject<ConfirmModalData>(MODAL_DATA);
|
||||||
|
private readonly modalRef = inject<ModalRef<boolean>>(ModalRef);
|
||||||
|
|
||||||
|
protected cancel(): void {
|
||||||
|
this.modalRef.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected confirm(): void {
|
||||||
|
this.modalRef.close(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<div class="confirm-modal">
|
||||||
|
<p class="confirm-modal__content">{{ data.content }}</p>
|
||||||
|
|
||||||
|
<div class="confirm-modal__actions">
|
||||||
|
<app-button variant="secondary" (click)="cancel()">
|
||||||
|
{{ data.cancelLabel }}
|
||||||
|
</app-button>
|
||||||
|
<app-button (click)="confirm()">
|
||||||
|
{{ data.confirmLabel }}
|
||||||
|
</app-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.confirm-modal {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
justify-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-modal__content {
|
||||||
|
margin: 0;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-modal__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import '@angular/compiler';
|
||||||
|
import { TestBed, getTestBed } from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
BrowserTestingModule,
|
||||||
|
platformBrowserTesting
|
||||||
|
} from '@angular/platform-browser/testing';
|
||||||
|
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConfirmModalData,
|
||||||
|
MODAL_DATA,
|
||||||
|
ModalRef
|
||||||
|
} from '../../../core/services/modal.service';
|
||||||
|
import { ConfirmModalComponent } from './confirm-modal.component';
|
||||||
|
|
||||||
|
describe('ConfirmModalComponent', () => {
|
||||||
|
const data: ConfirmModalData = {
|
||||||
|
content: 'Se confirmara la operacion seleccionada.',
|
||||||
|
confirmLabel: 'Aceptar',
|
||||||
|
cancelLabel: 'Volver'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
try {
|
||||||
|
getTestBed().initTestEnvironment(
|
||||||
|
BrowserTestingModule,
|
||||||
|
platformBrowserTesting()
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Test environment may already be initialized by another setup entrypoint.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the configured content and labels', async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ConfirmModalComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: MODAL_DATA,
|
||||||
|
useValue: data
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ModalRef,
|
||||||
|
useValue: {
|
||||||
|
close: vi.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(ConfirmModalComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const element = fixture.nativeElement as HTMLElement;
|
||||||
|
|
||||||
|
expect(element.textContent).toContain(data.content);
|
||||||
|
expect(element.textContent).toContain(data.confirmLabel);
|
||||||
|
expect(element.textContent).toContain(data.cancelLabel);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes with false on cancel and true on confirm', async () => {
|
||||||
|
const closeSpy = vi.fn();
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ConfirmModalComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: MODAL_DATA,
|
||||||
|
useValue: data
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ModalRef,
|
||||||
|
useValue: {
|
||||||
|
close: closeSpy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(ConfirmModalComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const buttons = fixture.nativeElement.querySelectorAll('button');
|
||||||
|
|
||||||
|
buttons[0].click();
|
||||||
|
buttons[1].click();
|
||||||
|
|
||||||
|
expect(closeSpy).toHaveBeenNthCalledWith(1, false);
|
||||||
|
expect(closeSpy).toHaveBeenNthCalledWith(2, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConfirmModalData,
|
||||||
|
MODAL_DATA,
|
||||||
|
ModalRef
|
||||||
|
} from '../../../core/services/modal.service';
|
||||||
|
import { ButtonComponent } from '../button/button.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-confirm-modal',
|
||||||
|
imports: [ButtonComponent],
|
||||||
|
templateUrl: './confirm-modal.component.html',
|
||||||
|
styleUrl: './confirm-modal.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class ConfirmModalComponent {
|
||||||
|
protected readonly data = inject<ConfirmModalData>(MODAL_DATA);
|
||||||
|
private readonly modalRef = inject<ModalRef<boolean>>(ModalRef);
|
||||||
|
|
||||||
|
protected cancel(): void {
|
||||||
|
this.modalRef.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected confirm(): void {
|
||||||
|
this.modalRef.close(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,16 +49,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-shell__header {
|
.modal-shell__header {
|
||||||
|
position: relative;
|
||||||
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 1.25rem 1.25rem 0.75rem;
|
padding: 1.25rem 1.25rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-shell__title {
|
.modal-shell__title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
font-size: clamp(1.15rem, 1rem + 0.4vw, 1.5rem);
|
font-size: clamp(1.15rem, 1rem + 0.4vw, 1.5rem);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
color: #202020;
|
color: #202020;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-shell__header .btn-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.25rem;
|
||||||
|
right: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-shell__body {
|
.modal-shell__body {
|
||||||
|
|||||||
Reference in New Issue
Block a user