Merge branch 'main' into cliente/tagumedica
This commit is contained in:
commit
336f4c1875
5 files changed
+319
-18
No files matched your search
@@ -0,0 +1,62 @@
|
|||||||
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { Router, provideRouter } from '@angular/router';
|
||||||
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
import { MenuService } from './menu.service';
|
||||||
|
import { RecentScreens } from './recent-screens';
|
||||||
|
|
||||||
|
@Component({ template: '' })
|
||||||
|
class Page {}
|
||||||
|
|
||||||
|
const ROUTES = ['ventas/ordenes', 'ventas/entrega', 'inventario/lotes', 'compras/ordenes', 'compras/facturas', 'bancos/pagos-recibidos', 'finanzas/asientos'].map((path) => ({ path, component: Page }));
|
||||||
|
|
||||||
|
describe('RecentScreens', () => {
|
||||||
|
async function setup(allowed: string[] = ['*']) {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({ providers: [provideRouter(ROUTES), provideHttpClient(), provideHttpClientTesting()] });
|
||||||
|
const loaded = TestBed.inject(MenuService).ensureLoaded();
|
||||||
|
TestBed.inject(HttpTestingController).expectOne('/api/user/menu').flush({ allowedPaths: allowed });
|
||||||
|
await loaded;
|
||||||
|
return { recent: TestBed.inject(RecentScreens), router: TestBed.inject(Router) };
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => localStorage.clear());
|
||||||
|
|
||||||
|
it('anota las pantallas del menú que se abren, la más reciente primero, y las recuerda', async () => {
|
||||||
|
const { recent, router } = await setup();
|
||||||
|
await router.navigateByUrl('/ventas/ordenes');
|
||||||
|
await router.navigateByUrl('/inventario/lotes');
|
||||||
|
expect(recent.paths()).toEqual(['/inventario/lotes', '/ventas/ordenes']);
|
||||||
|
expect(JSON.parse(localStorage.getItem('erp_recent')!)).toEqual(['/inventario/lotes', '/ventas/ordenes']);
|
||||||
|
// otra sesión del navegador las recupera
|
||||||
|
const again = (await setup()).recent;
|
||||||
|
expect(again.paths()).toEqual(['/inventario/lotes', '/ventas/ordenes']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('una pantalla abierta de nuevo sube al primer lugar sin repetirse, y el detalle cuenta como su pantalla', async () => {
|
||||||
|
const { recent, router } = await setup();
|
||||||
|
await router.navigateByUrl('/ventas/ordenes');
|
||||||
|
await router.navigateByUrl('/inventario/lotes');
|
||||||
|
await router.navigateByUrl('/ventas/ordenes');
|
||||||
|
expect(recent.paths()).toEqual(['/ventas/ordenes', '/inventario/lotes']);
|
||||||
|
recent.record('/inventario/lotes/75437?x=1');
|
||||||
|
expect(recent.paths()).toEqual(['/inventario/lotes', '/ventas/ordenes']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('guarda solo las 5 últimas e ignora lo que no es del menú', async () => {
|
||||||
|
const { recent, router } = await setup();
|
||||||
|
for (const p of ['/ventas/ordenes', '/ventas/entrega', '/inventario/lotes', '/compras/ordenes', '/compras/facturas', '/bancos/pagos-recibidos']) await router.navigateByUrl(p);
|
||||||
|
expect(recent.paths()).toHaveLength(5);
|
||||||
|
expect(recent.paths()[0]).toBe('/bancos/pagos-recibidos');
|
||||||
|
expect(recent.paths()).not.toContain('/ventas/ordenes');
|
||||||
|
recent.record('/no-esta-en-el-menu');
|
||||||
|
expect(recent.paths()).toHaveLength(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('un valor dañado del navegador no rompe nada', async () => {
|
||||||
|
localStorage.setItem('erp_recent', '{no es json');
|
||||||
|
expect((await setup()).recent.paths()).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Injectable, inject, signal } from '@angular/core';
|
||||||
|
import { NavigationEnd, Router } from '@angular/router';
|
||||||
|
import { filter } from 'rxjs';
|
||||||
|
import { MenuService } from './menu.service';
|
||||||
|
|
||||||
|
const KEY = 'erp_recent';
|
||||||
|
const MAX = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Las últimas pantallas del menú que se abrieron (hasta 5, la más reciente primero), recordadas en el navegador. Se llenan
|
||||||
|
* solas al navegar; la bienvenida las ofrece como atajo. Solo cuentan las pantallas del menú del usuario.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class RecentScreens {
|
||||||
|
private readonly menu = inject(MenuService);
|
||||||
|
readonly paths = signal<readonly string[]>(this.read());
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
inject(Router)
|
||||||
|
.events.pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd))
|
||||||
|
.subscribe((e) => this.record(e.urlAfterRedirects));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Anota la pantalla del menú a la que pertenece la ruta (`/inventario/lotes/75437` cuenta como `/inventario/lotes`). */
|
||||||
|
record(url: string): void {
|
||||||
|
const path = url.split(/[?#]/)[0];
|
||||||
|
const item = this.menu.modules().flatMap((m) => m.items).find((i) => path === i.path || path.startsWith(`${i.path}/`));
|
||||||
|
if (!item || this.paths()[0] === item.path) return;
|
||||||
|
const next = [item.path, ...this.paths().filter((p) => p !== item.path)].slice(0, MAX);
|
||||||
|
this.paths.set(next);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(KEY, JSON.stringify(next));
|
||||||
|
} catch {
|
||||||
|
/* sin almacenamiento: vale para esta sesión */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private read(): string[] {
|
||||||
|
try {
|
||||||
|
const v: unknown = JSON.parse(localStorage.getItem(KEY) ?? '[]');
|
||||||
|
return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string').slice(0, MAX) : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
|
import { type ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { Router, provideRouter } from '@angular/router';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { SessionStore } from '../../core/auth/session.store';
|
||||||
|
import { MenuService } from '../../core/menu/menu.service';
|
||||||
|
import { HomePage } from './home.page';
|
||||||
|
|
||||||
|
describe('HomePage', () => {
|
||||||
|
let fixture: ComponentFixture<HomePage>;
|
||||||
|
const el = () => fixture.nativeElement as HTMLElement;
|
||||||
|
const tick = async () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
fixture.detectChanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
async function open(allowed: string[], recent: string[] = []) {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
localStorage.clear();
|
||||||
|
if (recent.length) localStorage.setItem('erp_recent', JSON.stringify(recent));
|
||||||
|
TestBed.configureTestingModule({ providers: [provideRouter([]), provideHttpClient(), provideHttpClientTesting()] });
|
||||||
|
const loaded = TestBed.inject(MenuService).ensureLoaded();
|
||||||
|
TestBed.inject(HttpTestingController).expectOne('/api/user/menu').flush({ allowedPaths: allowed });
|
||||||
|
await loaded;
|
||||||
|
fixture = TestBed.createComponent(HomePage);
|
||||||
|
await tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = async (text: string) => {
|
||||||
|
const input = el().querySelector('input.find') as HTMLInputElement;
|
||||||
|
input.value = text;
|
||||||
|
input.dispatchEvent(new Event('input'));
|
||||||
|
await tick();
|
||||||
|
return input;
|
||||||
|
};
|
||||||
|
const cards = () => [...el().querySelectorAll('.card')].map((c) => [c.querySelector('h2')?.textContent?.trim(), [...c.querySelectorAll('a')].map((a) => a.textContent?.trim())] as const);
|
||||||
|
|
||||||
|
beforeEach(() => TestBed.resetTestingModule());
|
||||||
|
|
||||||
|
it('muestra una tarjeta por módulo con sus pantallas en lista, solo las permitidas', async () => {
|
||||||
|
await open(['/ventas/ordenes', '/ventas/entrega', '/inventario/lotes']);
|
||||||
|
expect(cards()).toEqual([
|
||||||
|
['Ventas', ['Órdenes de venta', 'Entregas']],
|
||||||
|
['Inventario', ['Lotes']],
|
||||||
|
]);
|
||||||
|
expect(el().querySelector('.card a')?.getAttribute('href')).toBe('/ventas/ordenes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('el saludo lleva el nombre y la empresa', async () => {
|
||||||
|
await open(['*']);
|
||||||
|
TestBed.inject(SessionStore).set({ token: 't', user: { webUser: 'u', empId: 1, firstName: 'Jose', lastName: 'H' }, company: 'SBO_X', sessionTimeoutMinutes: 30 } as never);
|
||||||
|
await tick();
|
||||||
|
expect(el().querySelector('h1')?.textContent).toContain('Bienvenido, Jose');
|
||||||
|
expect(el().querySelector('.sub')?.textContent).toContain('SBO_X');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('el buscador filtra las pantallas de todos los módulos, sin tildes ni mayúsculas, y dice a qué módulo pertenecen', async () => {
|
||||||
|
await open(['*']);
|
||||||
|
await type('ORDENES');
|
||||||
|
const found = [...el().querySelectorAll('.found a')].map((a) => [a.querySelector('strong')?.textContent, a.querySelector('span')?.textContent]);
|
||||||
|
expect(found).toEqual([['Órdenes de venta', 'Ventas'], ['Órdenes de compra', 'Compras']]);
|
||||||
|
expect(el().querySelector('.cards')).toBeNull();
|
||||||
|
// varias palabras: todas deben estar (en la pantalla o en su módulo)
|
||||||
|
await type('compra orden');
|
||||||
|
expect([...el().querySelectorAll('.found a strong')].map((s) => s.textContent)).toEqual(['Órdenes de compra']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sin coincidencias lo dice, y vaciar el buscador (o Esc) devuelve las tarjetas', async () => {
|
||||||
|
await open(['*']);
|
||||||
|
const input = await type('zzzz');
|
||||||
|
expect(el().querySelector('.found .none')?.textContent).toContain('Ninguna pantalla coincide con «zzzz»');
|
||||||
|
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
||||||
|
await tick();
|
||||||
|
expect(el().querySelector('.cards')).not.toBeNull();
|
||||||
|
expect((el().querySelector('input.find') as HTMLInputElement).value).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Enter abre la primera coincidencia', async () => {
|
||||||
|
await open(['*']);
|
||||||
|
const navigate = vi.spyOn(TestBed.inject(Router), 'navigateByUrl').mockResolvedValue(true);
|
||||||
|
const input = await type('lotes');
|
||||||
|
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
||||||
|
expect(navigate).toHaveBeenCalledWith('/inventario/lotes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ofrece las pantallas recientes que el usuario todavía puede abrir', async () => {
|
||||||
|
await open(['/ventas/ordenes', '/inventario/lotes'], ['/inventario/lotes', '/compras/ordenes', '/ventas/ordenes']);
|
||||||
|
expect([...el().querySelectorAll('.recent a')].map((a) => a.textContent?.trim())).toEqual(['Lotes', 'Órdenes de venta']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sin recientes no muestra esa fila', async () => {
|
||||||
|
await open(['*']);
|
||||||
|
expect(el().querySelector('.recent')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,36 +1,129 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { Router, RouterLink } from '@angular/router';
|
||||||
import { SessionStore } from '../../core/auth/session.store';
|
import { SessionStore } from '../../core/auth/session.store';
|
||||||
import { MenuService } from '../../core/menu/menu.service';
|
import { MenuService } from '../../core/menu/menu.service';
|
||||||
|
import { RecentScreens } from '../../core/menu/recent-screens';
|
||||||
|
|
||||||
|
/** Sin tildes ni mayúsculas, para que «ordenes» encuentre «Órdenes de venta». */
|
||||||
|
const plain = (s: string): string => s.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bienvenida: un buscador de pantallas, las últimas que se abrieron y una tarjeta por módulo del menú con sus pantallas en
|
||||||
|
* lista. Solo lo que el usuario puede abrir.
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'sbo-home-page',
|
selector: 'sbo-home-page',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [RouterLink],
|
imports: [RouterLink],
|
||||||
template: `
|
template: `
|
||||||
<h1>Bienvenido, {{ session.user()?.firstName }}</h1>
|
<header class="top">
|
||||||
<p class="sub">{{ session.company() }}</p>
|
<div>
|
||||||
@for (m of menu.modules(); track m.id) {
|
<h1>Bienvenido, {{ session.user()?.firstName }}</h1>
|
||||||
<section>
|
<p class="sub">{{ session.company() }}</p>
|
||||||
<h2>{{ m.label }}</h2>
|
</div>
|
||||||
<div class="links">
|
<input
|
||||||
@for (i of m.items; track i.path) {
|
class="find"
|
||||||
<a [routerLink]="i.path">{{ i.label }}</a>
|
type="search"
|
||||||
}
|
placeholder="Buscar pantalla…"
|
||||||
</div>
|
aria-label="Buscar pantalla"
|
||||||
</section>
|
autocomplete="off"
|
||||||
|
[value]="query()"
|
||||||
|
(input)="query.set($any($event.target).value)"
|
||||||
|
(keydown.enter)="openFirst()"
|
||||||
|
(keydown.escape)="query.set('')"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
@if (query().trim()) {
|
||||||
|
<ul class="found" role="list" aria-label="Pantallas encontradas">
|
||||||
|
@for (r of found(); track r.path) {
|
||||||
|
<li><a [routerLink]="r.path"><strong>{{ r.label }}</strong> <span>{{ r.module }}</span></a></li>
|
||||||
|
} @empty {
|
||||||
|
<li class="none">Ninguna pantalla coincide con «{{ query().trim() }}».</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
} @else {
|
||||||
|
@if (recent().length) {
|
||||||
|
<section class="recent" aria-label="Recientes">
|
||||||
|
<h2>Recientes</h2>
|
||||||
|
<div class="chips">
|
||||||
|
@for (r of recent(); track r.path) {
|
||||||
|
<a [routerLink]="r.path" [title]="r.module">{{ r.label }}</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
<div class="cards">
|
||||||
|
@for (m of menu.modules(); track m.id) {
|
||||||
|
<section class="card">
|
||||||
|
<h2>{{ m.label }}</h2>
|
||||||
|
<ul role="list">
|
||||||
|
@for (i of m.items; track i.path) {
|
||||||
|
<li><a [routerLink]="i.path">{{ i.label }}</a></li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
|
:host { display: block; }
|
||||||
|
.top { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 8px 16px; margin-bottom: 14px; }
|
||||||
h1 { margin: 0; font-size: 18px; font-weight: 600; }
|
h1 { margin: 0; font-size: 18px; font-weight: 600; }
|
||||||
.sub { margin: 2px 0 16px; color: var(--quiet); }
|
.sub { margin: 2px 0 0; color: var(--quiet); }
|
||||||
h2 { margin: 12px 0 6px; font-size: 13px; color: var(--quiet); text-transform: uppercase; letter-spacing: .4px; }
|
.find { width: min(320px, 100%); height: 26px; padding: 1px 8px; background: var(--field); border: 1px solid var(--field-b); border-radius: var(--radius); }
|
||||||
.links { display: flex; flex-wrap: wrap; gap: 6px; }
|
.find:focus { border-color: var(--focus); outline: none; }
|
||||||
a { padding: 4px 12px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-lg); color: var(--ink); text-decoration: none; }
|
|
||||||
a:hover { background: var(--row-sel); border-color: var(--focus); }
|
h2 { margin: 0; font-size: 12px; font-weight: 600; letter-spacing: .4px; text-transform: uppercase; color: var(--quiet); }
|
||||||
|
.recent { margin-bottom: 14px; }
|
||||||
|
.recent h2 { margin-bottom: 4px; }
|
||||||
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
|
.chips a { padding: 3px 12px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-pill, 12px); color: var(--ink); text-decoration: none; }
|
||||||
|
.chips a:hover { background: var(--row-sel); border-color: var(--focus); }
|
||||||
|
|
||||||
|
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; align-items: start; }
|
||||||
|
.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-panel); overflow: hidden; }
|
||||||
|
.card h2 { padding: 6px 12px; background: var(--grid-head); border-bottom: 1px solid var(--line); color: var(--ink); }
|
||||||
|
.card ul, .found { list-style: none; margin: 0; padding: 0; }
|
||||||
|
.card li a { display: block; padding: 4px 12px; color: var(--ink); text-decoration: none; }
|
||||||
|
.card li a:hover { background: var(--row-sel); }
|
||||||
|
.card li + li { border-top: 1px solid var(--line-soft); }
|
||||||
|
|
||||||
|
.found { max-width: 560px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-panel); }
|
||||||
|
.found li + li { border-top: 1px solid var(--line-soft); }
|
||||||
|
.found a { display: flex; justify-content: space-between; gap: 12px; padding: 5px 12px; color: var(--ink); text-decoration: none; }
|
||||||
|
.found a:hover { background: var(--row-sel); }
|
||||||
|
.found span { color: var(--quiet); }
|
||||||
|
.found .none { padding: 10px 12px; color: var(--quiet); }
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
export class HomePage {
|
export class HomePage {
|
||||||
protected readonly session = inject(SessionStore);
|
protected readonly session = inject(SessionStore);
|
||||||
protected readonly menu = inject(MenuService);
|
protected readonly menu = inject(MenuService);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
private readonly recents = inject(RecentScreens);
|
||||||
|
|
||||||
|
protected readonly query = signal('');
|
||||||
|
|
||||||
|
/** Todas las pantallas que el usuario puede abrir, con su módulo. */
|
||||||
|
private readonly all = computed(() => this.menu.modules().flatMap((m) => m.items.map((i) => ({ path: i.path, label: i.label, module: m.label }))));
|
||||||
|
|
||||||
|
/** Las pantallas que coinciden con lo escrito (en el nombre de la pantalla o del módulo), sin tildes ni mayúsculas. */
|
||||||
|
protected readonly found = computed(() => {
|
||||||
|
const words = plain(this.query()).split(/\s+/).filter(Boolean);
|
||||||
|
return this.all().filter((r) => words.every((w) => plain(`${r.label} ${r.module}`).includes(w)));
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Las últimas pantallas abiertas que el usuario todavía puede ver. */
|
||||||
|
protected readonly recent = computed(() => {
|
||||||
|
const byPath = new Map(this.all().map((r) => [r.path, r]));
|
||||||
|
return this.recents.paths().flatMap((p) => byPath.get(p) ?? []);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Enter abre la primera coincidencia. */
|
||||||
|
protected openFirst(): void {
|
||||||
|
const first = this.found()[0];
|
||||||
|
if (first) void this.router.navigateByUrl(first.path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import { AccountSegmentsService } from '../core/sl/account-segments';
|
|||||||
import { AuthService } from '../core/auth/auth.service';
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
import { SessionStore } from '../core/auth/session.store';
|
import { SessionStore } from '../core/auth/session.store';
|
||||||
import { MenuService } from '../core/menu/menu.service';
|
import { MenuService } from '../core/menu/menu.service';
|
||||||
|
import { RecentScreens } from '../core/menu/recent-screens';
|
||||||
import { ThemeSwitcher } from '../core/theme/theme-switcher';
|
import { ThemeSwitcher } from '../core/theme/theme-switcher';
|
||||||
|
|
||||||
const SIDEBAR_KEY = 'erp_sidebar';
|
const SIDEBAR_KEY = 'erp_sidebar';
|
||||||
@@ -48,6 +49,8 @@ export class MainLayout {
|
|||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
this.reveal(router.url);
|
this.reveal(router.url);
|
||||||
router.events.pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd), takeUntilDestroyed()).subscribe((e) => this.reveal(e.urlAfterRedirects));
|
router.events.pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd), takeUntilDestroyed()).subscribe((e) => this.reveal(e.urlAfterRedirects));
|
||||||
|
// empieza a anotar las pantallas que se abren (las ofrece la bienvenida)
|
||||||
|
inject(RecentScreens);
|
||||||
void this.segments.ensureLoaded();
|
void this.segments.ensureLoaded();
|
||||||
void inject(CompanySettingsService).ensureLoaded();
|
void inject(CompanySettingsService).ensureLoaded();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user