valtech-components 2.0.996 → 2.0.997
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm2022/lib/components/organisms/notification-preferences-view/notification-preferences-view.component.mjs +667 -0
- package/esm2022/lib/components/organisms/notification-preferences-view/notification-preferences-view.i18n.mjs +71 -0
- package/esm2022/lib/components/organisms/notification-preferences-view/notification-preferences.routes.mjs +31 -0
- package/esm2022/lib/components/organisms/notification-preferences-view/types.mjs +2 -0
- package/esm2022/lib/components/organisms/organization-view/organization-view.component.mjs +1 -76
- package/esm2022/lib/components/organisms/organization-view/organization-view.i18n.mjs +1 -5
- package/esm2022/lib/components/organisms/permissions-view/permissions-view.component.mjs +511 -0
- package/esm2022/lib/components/organisms/permissions-view/permissions-view.i18n.mjs +116 -0
- package/esm2022/lib/components/organisms/permissions-view/permissions.routes.mjs +29 -0
- package/esm2022/lib/components/organisms/permissions-view/types.mjs +2 -0
- package/esm2022/lib/components/organisms/settings-hub/settings.routes.mjs +12 -1
- package/esm2022/lib/services/auth/auth.service.mjs +1 -14
- package/esm2022/lib/services/auth/types.mjs +1 -1
- package/esm2022/lib/services/org/org.service.mjs +1 -8
- package/esm2022/lib/services/org/permission-catalog.service.mjs +33 -0
- package/esm2022/lib/services/org/types.mjs +1 -1
- package/esm2022/lib/version.mjs +2 -2
- package/esm2022/public-api.mjs +8 -4
- package/fesm2022/valtech-components.mjs +1484 -1003
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/organisms/notification-preferences-view/notification-preferences-view.component.d.ts +146 -0
- package/lib/components/organisms/notification-preferences-view/notification-preferences-view.i18n.d.ts +11 -0
- package/lib/components/organisms/notification-preferences-view/notification-preferences.routes.d.ts +26 -0
- package/lib/components/organisms/notification-preferences-view/types.d.ts +36 -0
- package/lib/components/organisms/organization-view/organization-view.component.d.ts +0 -5
- package/lib/components/organisms/permissions-view/permissions-view.component.d.ts +71 -0
- package/lib/components/organisms/permissions-view/permissions-view.i18n.d.ts +9 -0
- package/lib/components/organisms/permissions-view/permissions.routes.d.ts +24 -0
- package/lib/components/organisms/permissions-view/types.d.ts +26 -0
- package/lib/components/organisms/settings-hub/settings.routes.d.ts +15 -0
- package/lib/services/auth/auth.service.d.ts +1 -7
- package/lib/services/auth/types.d.ts +0 -23
- package/lib/services/org/org.service.d.ts +1 -6
- package/lib/services/org/permission-catalog.service.d.ts +34 -0
- package/lib/services/org/types.d.ts +0 -30
- package/lib/version.d.ts +1 -1
- package/package.json +1 -1
- package/public-api.d.ts +8 -5
- package/esm2022/lib/components/organisms/api-keys-modal/api-keys-modal.component.mjs +0 -385
- package/esm2022/lib/components/organisms/api-keys-modal/api-keys-modal.i18n.mjs +0 -63
- package/esm2022/lib/components/organisms/member-import-modal/member-import-modal.component.mjs +0 -313
- package/esm2022/lib/components/organisms/member-import-modal/member-import-modal.i18n.mjs +0 -63
- package/esm2022/lib/services/apikeys/api-keys.service.mjs +0 -46
- package/esm2022/lib/services/apikeys/types.mjs +0 -7
- package/lib/components/organisms/api-keys-modal/api-keys-modal.component.d.ts +0 -44
- package/lib/components/organisms/api-keys-modal/api-keys-modal.i18n.d.ts +0 -6
- package/lib/components/organisms/member-import-modal/member-import-modal.component.d.ts +0 -47
- package/lib/components/organisms/member-import-modal/member-import-modal.i18n.d.ts +0 -6
- package/lib/services/apikeys/api-keys.service.d.ts +0 -25
- package/lib/services/apikeys/types.d.ts +0 -46
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { inject, InjectionToken, makeEnvironmentProviders, APP_INITIALIZER, ErrorHandler, Injectable, DestroyRef, Inject, signal, computed, PLATFORM_ID, isDevMode, Optional, runInInjectionContext, effect, Pipe, TemplateRef, ViewContainerRef, input, Directive, EventEmitter, Component, Input, Output, ChangeDetectionStrategy, HostBinding, HostListener, NgZone, ViewChild, ChangeDetectorRef, output, ContentChild, ElementRef, viewChild, untracked, Injector, isSignal, ViewEncapsulation } from '@angular/core';
|
|
3
|
-
import { BehaviorSubject, throwError, Subject, map, distinctUntilChanged, filter as filter$1, take as take$1, firstValueFrom, of, from, EMPTY, Observable, interval, debounceTime, switchMap as switchMap$1, catchError as catchError$1, takeUntil, isObservable, race, timer,
|
|
3
|
+
import { BehaviorSubject, throwError, Subject, map, distinctUntilChanged, filter as filter$1, take as take$1, firstValueFrom, of, from, EMPTY, Observable, interval, debounceTime, switchMap as switchMap$1, catchError as catchError$1, takeUntil, isObservable, race, timer, forkJoin, shareReplay } from 'rxjs';
|
|
4
4
|
import { catchError, switchMap, finalize, filter, take, map as map$1, tap, distinctUntilChanged as distinctUntilChanged$1, retry, timeout, debounceTime as debounceTime$1, takeUntil as takeUntil$1 } from 'rxjs/operators';
|
|
5
5
|
import * as i1$3 from '@angular/common/http';
|
|
6
6
|
import { provideHttpClient, withInterceptors, HttpClient, HttpErrorResponse } from '@angular/common/http';
|
|
@@ -54,7 +54,7 @@ import 'prismjs/components/prism-json';
|
|
|
54
54
|
* Current version of valtech-components.
|
|
55
55
|
* This is automatically updated during the publish process.
|
|
56
56
|
*/
|
|
57
|
-
const VERSION = '2.0.
|
|
57
|
+
const VERSION = '2.0.997';
|
|
58
58
|
|
|
59
59
|
// Control de estado de refresco (singleton a nivel de módulo)
|
|
60
60
|
let isRefreshing = false;
|
|
@@ -8658,19 +8658,6 @@ class AuthService {
|
|
|
8658
8658
|
.post(`${this.baseUrl}/reset-password`, request)
|
|
8659
8659
|
.pipe(catchError(error => this.handleAuthError(error)));
|
|
8660
8660
|
}
|
|
8661
|
-
/**
|
|
8662
|
-
* Activa una cuenta pre-aprovisionada por una organización (ADR-023): define
|
|
8663
|
-
* la contraseña con el token del enlace de email. En éxito el backend devuelve
|
|
8664
|
-
* tokens de auto-signin → el usuario queda logueado (mismo flujo que verify-email).
|
|
8665
|
-
*/
|
|
8666
|
-
activateAccount(request) {
|
|
8667
|
-
this.stateService.clearError();
|
|
8668
|
-
return this.http.post(`${this.baseUrl}/activate`, request).pipe(tap(response => {
|
|
8669
|
-
if (response.activated && response.accessToken) {
|
|
8670
|
-
this.handleSuccessfulAuth(response);
|
|
8671
|
-
}
|
|
8672
|
-
}), catchError(error => this.handleAuthError(error)));
|
|
8673
|
-
}
|
|
8674
8661
|
/**
|
|
8675
8662
|
* Cambia la contraseña del usuario autenticado.
|
|
8676
8663
|
* Requiere la contraseña actual para verificación.
|
|
@@ -43401,62 +43388,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
43401
43388
|
type: Input
|
|
43402
43389
|
}] } });
|
|
43403
43390
|
|
|
43404
|
-
/**
|
|
43405
|
-
* Helper para montar el bloque completo de Settings en una app del factory: el
|
|
43406
|
-
* HUB de navegación (`val-settings-hub`) en el index + las vistas promovidas
|
|
43407
|
-
* (Perfil, Preferencias) + las rutas propias de la app (`extraRoutes`).
|
|
43408
|
-
*
|
|
43409
|
-
* Pensado para apps nuevas (greenfield) que adoptan el set entero. showcase NO
|
|
43410
|
-
* lo usa hoy (preserva su `settings.routes.ts` por la semántica de sus muchas
|
|
43411
|
-
* secciones no promovidas — ver guía de promoción), pero el provider debe
|
|
43412
|
-
* existir y compilar para el resto de apps.
|
|
43413
|
-
*
|
|
43414
|
-
* @example
|
|
43415
|
-
* ```ts
|
|
43416
|
-
* // app.routes.ts
|
|
43417
|
-
* import { provideValtechSettingsRoutes } from 'valtech-components';
|
|
43418
|
-
*
|
|
43419
|
-
* export const routes: Routes = [
|
|
43420
|
-
* {
|
|
43421
|
-
* path: 'app',
|
|
43422
|
-
* children: [
|
|
43423
|
-
* ...provideValtechSettingsRoutes({
|
|
43424
|
-
* hubConfig: { onLogout: () => auth.logoutWithConfirmation() },
|
|
43425
|
-
* extraRoutes: [
|
|
43426
|
-
* { path: 'security', loadComponent: () => ... },
|
|
43427
|
-
* ],
|
|
43428
|
-
* }),
|
|
43429
|
-
* ],
|
|
43430
|
-
* },
|
|
43431
|
-
* ];
|
|
43432
|
-
* ```
|
|
43433
|
-
*/
|
|
43434
|
-
function provideValtechSettingsRoutes(opts) {
|
|
43435
|
-
const basePath = opts?.basePath ?? 'settings';
|
|
43436
|
-
const includeProfile = opts?.includeProfile ?? true;
|
|
43437
|
-
const includePreferences = opts?.includePreferences ?? true;
|
|
43438
|
-
return [
|
|
43439
|
-
{
|
|
43440
|
-
path: basePath,
|
|
43441
|
-
children: [
|
|
43442
|
-
{
|
|
43443
|
-
path: '',
|
|
43444
|
-
component: SettingsHubComponent,
|
|
43445
|
-
data: { settingsHubConfig: opts?.hubConfig },
|
|
43446
|
-
},
|
|
43447
|
-
...(includeProfile ? provideValtechProfileRoutes({ config: opts?.profileConfig }) : []),
|
|
43448
|
-
...(includePreferences
|
|
43449
|
-
? provideValtechPreferencesRoutes({
|
|
43450
|
-
path: opts?.preferencesPath ?? 'preferences',
|
|
43451
|
-
config: opts?.preferencesConfig,
|
|
43452
|
-
})
|
|
43453
|
-
: []),
|
|
43454
|
-
...(opts?.extraRoutes ?? []),
|
|
43455
|
-
],
|
|
43456
|
-
},
|
|
43457
|
-
];
|
|
43458
|
-
}
|
|
43459
|
-
|
|
43460
43391
|
addIcons({ shieldCheckmarkOutline, checkmarkCircleOutline });
|
|
43461
43392
|
/**
|
|
43462
43393
|
* `val-login-attempt-modal` — modal de confirmación de intento de inicio de sesión.
|
|
@@ -45416,13 +45347,6 @@ class OrgService {
|
|
|
45416
45347
|
inviteUser(orgId, req) {
|
|
45417
45348
|
return this.http.post(`${this.baseUrl}/${orgId}/invite`, req);
|
|
45418
45349
|
}
|
|
45419
|
-
/**
|
|
45420
|
-
* Crea/asigna miembros en masa (ADR-023). Resultado por-fila (partial success):
|
|
45421
|
-
* `created` (pre-aprovisionado), `assigned` (ya existía), `skipped`, `error`.
|
|
45422
|
-
*/
|
|
45423
|
-
importMembers(orgId, req) {
|
|
45424
|
-
return this.http.post(`${this.baseUrl}/${orgId}/members/import`, req);
|
|
45425
|
-
}
|
|
45426
45350
|
leaveOrg(orgId) {
|
|
45427
45351
|
return this.http.post(`${this.baseUrl}/${orgId}/leave`, {});
|
|
45428
45352
|
}
|
|
@@ -48907,849 +48831,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
48907
48831
|
type: Input
|
|
48908
48832
|
}] } });
|
|
48909
48833
|
|
|
48910
|
-
/**
|
|
48911
|
-
* Defaults i18n (es/en) embebidos en `val-member-import-modal` (ADR-023).
|
|
48912
|
-
* Auto-registrados si el consumer no proveyó el namespace `Settings.ImportModal`.
|
|
48913
|
-
*/
|
|
48914
|
-
const MEMBER_IMPORT_MODAL_I18N = {
|
|
48915
|
-
es: {
|
|
48916
|
-
title: 'Importar miembros',
|
|
48917
|
-
hint: 'Pega un CSV con columnas: email, nombre, rol (una fila por persona).',
|
|
48918
|
-
csvPlaceholder: 'email,nombre,rol\nana@x.com,Ana,editor\nbeto@x.com,Beto,viewer',
|
|
48919
|
-
rolesAvailable: 'Roles disponibles:',
|
|
48920
|
-
onConflictLabel: 'Si el email ya existe',
|
|
48921
|
-
conflictAssign: 'Asignar el rol',
|
|
48922
|
-
conflictSkip: 'Omitir',
|
|
48923
|
-
conflictError: 'Marcar error',
|
|
48924
|
-
sendActivation: 'Enviar email de activación a las cuentas nuevas',
|
|
48925
|
-
submit: 'Importar',
|
|
48926
|
-
submitting: 'Importando...',
|
|
48927
|
-
parseError: 'No se pudo interpretar el CSV. Revisa el formato.',
|
|
48928
|
-
noRows: 'Pega al menos una fila válida (email y rol).',
|
|
48929
|
-
summary: 'Resumen',
|
|
48930
|
-
created: 'creados',
|
|
48931
|
-
assigned: 'asignados',
|
|
48932
|
-
skipped: 'omitidos',
|
|
48933
|
-
failed: 'con error',
|
|
48934
|
-
colEmail: 'Email',
|
|
48935
|
-
colStatus: 'Estado',
|
|
48936
|
-
colDetail: 'Detalle',
|
|
48937
|
-
activationSent: 'activación enviada',
|
|
48938
|
-
activationPending: 'activación pendiente',
|
|
48939
|
-
again: 'Importar otro lote',
|
|
48940
|
-
close: 'Cerrar',
|
|
48941
|
-
genericError: 'No se pudo completar la importación. Intenta de nuevo.',
|
|
48942
|
-
},
|
|
48943
|
-
en: {
|
|
48944
|
-
title: 'Import members',
|
|
48945
|
-
hint: 'Paste a CSV with columns: email, name, role (one row per person).',
|
|
48946
|
-
csvPlaceholder: 'email,name,role\nana@x.com,Ana,editor\nbeto@x.com,Beto,viewer',
|
|
48947
|
-
rolesAvailable: 'Available roles:',
|
|
48948
|
-
onConflictLabel: 'If the email already exists',
|
|
48949
|
-
conflictAssign: 'Assign the role',
|
|
48950
|
-
conflictSkip: 'Skip',
|
|
48951
|
-
conflictError: 'Mark as error',
|
|
48952
|
-
sendActivation: 'Send activation email to new accounts',
|
|
48953
|
-
submit: 'Import',
|
|
48954
|
-
submitting: 'Importing...',
|
|
48955
|
-
parseError: "Couldn't parse the CSV. Check the format.",
|
|
48956
|
-
noRows: 'Paste at least one valid row (email and role).',
|
|
48957
|
-
summary: 'Summary',
|
|
48958
|
-
created: 'created',
|
|
48959
|
-
assigned: 'assigned',
|
|
48960
|
-
skipped: 'skipped',
|
|
48961
|
-
failed: 'failed',
|
|
48962
|
-
colEmail: 'Email',
|
|
48963
|
-
colStatus: 'Status',
|
|
48964
|
-
colDetail: 'Detail',
|
|
48965
|
-
activationSent: 'activation sent',
|
|
48966
|
-
activationPending: 'activation pending',
|
|
48967
|
-
again: 'Import another batch',
|
|
48968
|
-
close: 'Close',
|
|
48969
|
-
genericError: "Couldn't complete the import. Try again.",
|
|
48970
|
-
},
|
|
48971
|
-
};
|
|
48972
|
-
|
|
48973
|
-
const DEFAULT_NAMESPACE$3 = 'Settings.ImportModal';
|
|
48974
|
-
/**
|
|
48975
|
-
* `val-member-import-modal` — panel de carga masiva de miembros (ADR-023 fase 6).
|
|
48976
|
-
*
|
|
48977
|
-
* El admin pega un CSV (`email,nombre,rol`); el modal lo parsea a filas y llama
|
|
48978
|
-
* `OrgService.importMembers`. Muestra el resultado por-fila (partial success):
|
|
48979
|
-
* `created` / `assigned` / `skipped` / `error`. Header canónico (Regla #5).
|
|
48980
|
-
*
|
|
48981
|
-
* Auto-registra defaults i18n (es/en) en `Settings.ImportModal`.
|
|
48982
|
-
*/
|
|
48983
|
-
class MemberImportModalComponent {
|
|
48984
|
-
constructor() {
|
|
48985
|
-
this.i18n = inject(I18nService);
|
|
48986
|
-
this.orgService = inject(OrgService);
|
|
48987
|
-
/** Org destino del import. */
|
|
48988
|
-
this.orgId = '';
|
|
48989
|
-
/** Nombres de roles disponibles (para el hint). Opcional. */
|
|
48990
|
-
this.availableRoles = [];
|
|
48991
|
-
/** Namespace i18n. */
|
|
48992
|
-
this.i18nNamespace = DEFAULT_NAMESPACE$3;
|
|
48993
|
-
this.csv = '';
|
|
48994
|
-
this.onConflict = 'assignRole';
|
|
48995
|
-
this.sendActivation = true;
|
|
48996
|
-
this.submitting = signal(false);
|
|
48997
|
-
this.errorMsg = signal('');
|
|
48998
|
-
this.results = signal(null);
|
|
48999
|
-
this.rolesHint = computed(() => this.availableRoles.length ? `${this.t('rolesAvailable')} ${this.availableRoles.join(', ')}` : '');
|
|
49000
|
-
this.summaryLabel = computed(() => {
|
|
49001
|
-
const s = this.results()?.summary;
|
|
49002
|
-
if (!s)
|
|
49003
|
-
return this.t('summary');
|
|
49004
|
-
return `${this.t('summary')}: ${s.created} ${this.t('created')} · ${s.assigned} ${this.t('assigned')} · ${s.skipped} ${this.t('skipped')} · ${s.failed} ${this.t('failed')}`;
|
|
49005
|
-
});
|
|
49006
|
-
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$3)) {
|
|
49007
|
-
this.i18n.registerContent(DEFAULT_NAMESPACE$3, MEMBER_IMPORT_MODAL_I18N);
|
|
49008
|
-
}
|
|
49009
|
-
}
|
|
49010
|
-
ngOnInit() {
|
|
49011
|
-
if (!this.i18n.hasNamespace(this.i18nNamespace)) {
|
|
49012
|
-
this.i18n.registerContent(this.i18nNamespace, MEMBER_IMPORT_MODAL_I18N);
|
|
49013
|
-
}
|
|
49014
|
-
}
|
|
49015
|
-
/** Parsea el CSV pegado a filas {email,name,roleName}. Tolera header. */
|
|
49016
|
-
parseCsv() {
|
|
49017
|
-
const rows = [];
|
|
49018
|
-
for (const raw of this.csv.split(/\r?\n/)) {
|
|
49019
|
-
const line = raw.trim();
|
|
49020
|
-
if (!line)
|
|
49021
|
-
continue;
|
|
49022
|
-
const cols = line.split(',').map(c => c.trim());
|
|
49023
|
-
const email = (cols[0] ?? '').toLowerCase();
|
|
49024
|
-
// Saltar header (primera col "email") o líneas sin @.
|
|
49025
|
-
if (!email || !email.includes('@'))
|
|
49026
|
-
continue;
|
|
49027
|
-
rows.push({ email, name: cols[1] ?? '', roleName: cols[2] ?? '' });
|
|
49028
|
-
}
|
|
49029
|
-
return rows;
|
|
49030
|
-
}
|
|
49031
|
-
submit() {
|
|
49032
|
-
this.errorMsg.set('');
|
|
49033
|
-
const users = this.parseCsv();
|
|
49034
|
-
if (users.length === 0) {
|
|
49035
|
-
this.errorMsg.set(this.t('noRows'));
|
|
49036
|
-
return;
|
|
49037
|
-
}
|
|
49038
|
-
if (users.some(u => !u.roleName)) {
|
|
49039
|
-
this.errorMsg.set(this.t('parseError'));
|
|
49040
|
-
return;
|
|
49041
|
-
}
|
|
49042
|
-
this.submitting.set(true);
|
|
49043
|
-
this.orgService
|
|
49044
|
-
.importMembers(this.orgId, {
|
|
49045
|
-
users,
|
|
49046
|
-
onConflict: this.onConflict,
|
|
49047
|
-
sendActivation: this.sendActivation,
|
|
49048
|
-
})
|
|
49049
|
-
.subscribe({
|
|
49050
|
-
next: res => {
|
|
49051
|
-
this.submitting.set(false);
|
|
49052
|
-
this.results.set(res);
|
|
49053
|
-
// Refrescar lista del consumer si hubo altas/asignaciones efectivas.
|
|
49054
|
-
if (res.summary.created + res.summary.assigned > 0) {
|
|
49055
|
-
this.onSuccess?.();
|
|
49056
|
-
}
|
|
49057
|
-
},
|
|
49058
|
-
error: err => {
|
|
49059
|
-
this.submitting.set(false);
|
|
49060
|
-
this.errorMsg.set(err?.error?.message ?? this.t('genericError'));
|
|
49061
|
-
},
|
|
49062
|
-
});
|
|
49063
|
-
}
|
|
49064
|
-
statusLabel(r) {
|
|
49065
|
-
const base = this.t(r.status === 'error' ? 'failed' : r.status);
|
|
49066
|
-
if (r.status === 'created' && r.activation) {
|
|
49067
|
-
return `${base} · ${this.t(r.activation === 'sent' ? 'activationSent' : 'activationPending')}`;
|
|
49068
|
-
}
|
|
49069
|
-
return base;
|
|
49070
|
-
}
|
|
49071
|
-
reset() {
|
|
49072
|
-
this.csv = '';
|
|
49073
|
-
this.errorMsg.set('');
|
|
49074
|
-
this.results.set(null);
|
|
49075
|
-
}
|
|
49076
|
-
dismiss() {
|
|
49077
|
-
this._modalRef?.dismiss(null, 'cancel');
|
|
49078
|
-
}
|
|
49079
|
-
t(key) {
|
|
49080
|
-
return this.i18n.t(key, this.i18nNamespace);
|
|
49081
|
-
}
|
|
49082
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MemberImportModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
49083
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: MemberImportModalComponent, isStandalone: true, selector: "val-member-import-modal", inputs: { orgId: "orgId", availableRoles: "availableRoles", onSuccess: "onSuccess", _modalRef: "_modalRef", i18nNamespace: "i18nNamespace" }, ngImport: i0, template: `
|
|
49084
|
-
<ion-header>
|
|
49085
|
-
<ion-toolbar>
|
|
49086
|
-
<ion-buttons slot="end">
|
|
49087
|
-
<ion-button fill="clear" color="dark" (click)="dismiss()">
|
|
49088
|
-
<strong>{{ t('close') }}</strong>
|
|
49089
|
-
</ion-button>
|
|
49090
|
-
</ion-buttons>
|
|
49091
|
-
</ion-toolbar>
|
|
49092
|
-
</ion-header>
|
|
49093
|
-
|
|
49094
|
-
<ion-content class="ion-padding">
|
|
49095
|
-
<val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
|
|
49096
|
-
|
|
49097
|
-
@if (!results()) {
|
|
49098
|
-
<val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
|
|
49099
|
-
|
|
49100
|
-
@if (rolesHint()) {
|
|
49101
|
-
<val-text [props]="{ content: rolesHint(), size: 'small', color: 'medium', bold: false }" />
|
|
49102
|
-
}
|
|
49103
|
-
|
|
49104
|
-
<ion-textarea
|
|
49105
|
-
class="import-csv"
|
|
49106
|
-
[autoGrow]="true"
|
|
49107
|
-
[rows]="6"
|
|
49108
|
-
[placeholder]="t('csvPlaceholder')"
|
|
49109
|
-
[(ngModel)]="csv"
|
|
49110
|
-
/>
|
|
49111
|
-
|
|
49112
|
-
<ion-select
|
|
49113
|
-
class="import-conflict"
|
|
49114
|
-
label="{{ t('onConflictLabel') }}"
|
|
49115
|
-
labelPlacement="stacked"
|
|
49116
|
-
interface="popover"
|
|
49117
|
-
[(ngModel)]="onConflict"
|
|
49118
|
-
>
|
|
49119
|
-
<ion-select-option value="assignRole">{{ t('conflictAssign') }}</ion-select-option>
|
|
49120
|
-
<ion-select-option value="skip">{{ t('conflictSkip') }}</ion-select-option>
|
|
49121
|
-
<ion-select-option value="error">{{ t('conflictError') }}</ion-select-option>
|
|
49122
|
-
</ion-select>
|
|
49123
|
-
|
|
49124
|
-
<ion-checkbox class="import-activation" labelPlacement="end" [(ngModel)]="sendActivation">
|
|
49125
|
-
{{ t('sendActivation') }}
|
|
49126
|
-
</ion-checkbox>
|
|
49127
|
-
|
|
49128
|
-
@if (errorMsg()) {
|
|
49129
|
-
<p class="import-error">{{ errorMsg() }}</p>
|
|
49130
|
-
}
|
|
49131
|
-
|
|
49132
|
-
<div class="import-actions">
|
|
49133
|
-
<ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
|
|
49134
|
-
@if (submitting()) {
|
|
49135
|
-
<ion-spinner name="dots" />
|
|
49136
|
-
{{ t('submitting') }}
|
|
49137
|
-
} @else {
|
|
49138
|
-
{{ t('submit') }}
|
|
49139
|
-
}
|
|
49140
|
-
</ion-button>
|
|
49141
|
-
</div>
|
|
49142
|
-
} @else {
|
|
49143
|
-
<!-- Resultados -->
|
|
49144
|
-
<val-title [props]="{ content: summaryLabel(), size: 'large', color: '', bold: true }" />
|
|
49145
|
-
<div class="import-results">
|
|
49146
|
-
@for (r of results()!.results; track r.email) {
|
|
49147
|
-
<div class="import-row import-row--{{ r.status }}">
|
|
49148
|
-
<span class="import-row__email">{{ r.email }}</span>
|
|
49149
|
-
<span class="import-row__status">{{ statusLabel(r) }}</span>
|
|
49150
|
-
@if (r.reason) {
|
|
49151
|
-
<span class="import-row__reason">{{ r.reason }}</span>
|
|
49152
|
-
}
|
|
49153
|
-
</div>
|
|
49154
|
-
}
|
|
49155
|
-
</div>
|
|
49156
|
-
<div class="import-actions">
|
|
49157
|
-
<ion-button expand="block" fill="outline" color="dark" (click)="reset()">
|
|
49158
|
-
{{ t('again') }}
|
|
49159
|
-
</ion-button>
|
|
49160
|
-
</div>
|
|
49161
|
-
}
|
|
49162
|
-
</ion-content>
|
|
49163
|
-
`, isInline: true, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:12px}.import-csv{--background: var(--ion-color-light);border-radius:10px;margin-bottom:16px;font-family:monospace;font-size:13px}.import-conflict{margin-bottom:12px}.import-activation{display:block;margin-bottom:16px;font-size:14px}.import-error{margin:0 0 12px;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.import-actions{margin-top:8px}.import-results{display:flex;flex-direction:column;gap:6px;margin:12px 0}.import-row{display:grid;grid-template-columns:1fr auto;grid-row-gap:2px;align-items:center;padding:10px 12px;border-radius:8px;border-left:3px solid var(--ion-color-medium);background:var(--ion-color-light);font-size:14px}.import-row--created{border-left-color:var(--ion-color-success, #2dd36f)}.import-row--assigned{border-left-color:var(--ion-color-primary, #7026df)}.import-row--skipped{border-left-color:var(--ion-color-medium, #92949c)}.import-row--error{border-left-color:var(--ion-color-danger, #c0392b)}.import-row__email{font-weight:600;color:var(--ion-color-dark)}.import-row__status{text-align:right;color:var(--ion-color-medium-shade, #6b6e76)}.import-row__reason{grid-column:1 / -1;font-size:12px;color:var(--ion-color-danger, #c0392b)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$7.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$7.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }, { kind: "component", type: IonSelect, selector: "ion-select", inputs: ["cancelText", "color", "compareWith", "disabled", "errorText", "expandedIcon", "fill", "helperText", "interface", "interfaceOptions", "justify", "label", "labelPlacement", "mode", "multiple", "name", "okText", "placeholder", "selectedText", "shape", "toggleIcon", "value"] }, { kind: "component", type: IonSelectOption, selector: "ion-select-option", inputs: ["disabled", "value"] }, { kind: "component", type: IonCheckbox, selector: "ion-checkbox", inputs: ["checked", "color", "disabled", "errorText", "helperText", "indeterminate", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }] }); }
|
|
49164
|
-
}
|
|
49165
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MemberImportModalComponent, decorators: [{
|
|
49166
|
-
type: Component,
|
|
49167
|
-
args: [{ selector: 'val-member-import-modal', standalone: true, imports: [
|
|
49168
|
-
FormsModule,
|
|
49169
|
-
IonHeader,
|
|
49170
|
-
IonToolbar,
|
|
49171
|
-
IonButtons,
|
|
49172
|
-
IonButton,
|
|
49173
|
-
IonContent,
|
|
49174
|
-
IonTextarea,
|
|
49175
|
-
IonSelect,
|
|
49176
|
-
IonSelectOption,
|
|
49177
|
-
IonCheckbox,
|
|
49178
|
-
IonSpinner,
|
|
49179
|
-
DisplayComponent,
|
|
49180
|
-
TextComponent,
|
|
49181
|
-
TitleComponent,
|
|
49182
|
-
], template: `
|
|
49183
|
-
<ion-header>
|
|
49184
|
-
<ion-toolbar>
|
|
49185
|
-
<ion-buttons slot="end">
|
|
49186
|
-
<ion-button fill="clear" color="dark" (click)="dismiss()">
|
|
49187
|
-
<strong>{{ t('close') }}</strong>
|
|
49188
|
-
</ion-button>
|
|
49189
|
-
</ion-buttons>
|
|
49190
|
-
</ion-toolbar>
|
|
49191
|
-
</ion-header>
|
|
49192
|
-
|
|
49193
|
-
<ion-content class="ion-padding">
|
|
49194
|
-
<val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
|
|
49195
|
-
|
|
49196
|
-
@if (!results()) {
|
|
49197
|
-
<val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
|
|
49198
|
-
|
|
49199
|
-
@if (rolesHint()) {
|
|
49200
|
-
<val-text [props]="{ content: rolesHint(), size: 'small', color: 'medium', bold: false }" />
|
|
49201
|
-
}
|
|
49202
|
-
|
|
49203
|
-
<ion-textarea
|
|
49204
|
-
class="import-csv"
|
|
49205
|
-
[autoGrow]="true"
|
|
49206
|
-
[rows]="6"
|
|
49207
|
-
[placeholder]="t('csvPlaceholder')"
|
|
49208
|
-
[(ngModel)]="csv"
|
|
49209
|
-
/>
|
|
49210
|
-
|
|
49211
|
-
<ion-select
|
|
49212
|
-
class="import-conflict"
|
|
49213
|
-
label="{{ t('onConflictLabel') }}"
|
|
49214
|
-
labelPlacement="stacked"
|
|
49215
|
-
interface="popover"
|
|
49216
|
-
[(ngModel)]="onConflict"
|
|
49217
|
-
>
|
|
49218
|
-
<ion-select-option value="assignRole">{{ t('conflictAssign') }}</ion-select-option>
|
|
49219
|
-
<ion-select-option value="skip">{{ t('conflictSkip') }}</ion-select-option>
|
|
49220
|
-
<ion-select-option value="error">{{ t('conflictError') }}</ion-select-option>
|
|
49221
|
-
</ion-select>
|
|
49222
|
-
|
|
49223
|
-
<ion-checkbox class="import-activation" labelPlacement="end" [(ngModel)]="sendActivation">
|
|
49224
|
-
{{ t('sendActivation') }}
|
|
49225
|
-
</ion-checkbox>
|
|
49226
|
-
|
|
49227
|
-
@if (errorMsg()) {
|
|
49228
|
-
<p class="import-error">{{ errorMsg() }}</p>
|
|
49229
|
-
}
|
|
49230
|
-
|
|
49231
|
-
<div class="import-actions">
|
|
49232
|
-
<ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
|
|
49233
|
-
@if (submitting()) {
|
|
49234
|
-
<ion-spinner name="dots" />
|
|
49235
|
-
{{ t('submitting') }}
|
|
49236
|
-
} @else {
|
|
49237
|
-
{{ t('submit') }}
|
|
49238
|
-
}
|
|
49239
|
-
</ion-button>
|
|
49240
|
-
</div>
|
|
49241
|
-
} @else {
|
|
49242
|
-
<!-- Resultados -->
|
|
49243
|
-
<val-title [props]="{ content: summaryLabel(), size: 'large', color: '', bold: true }" />
|
|
49244
|
-
<div class="import-results">
|
|
49245
|
-
@for (r of results()!.results; track r.email) {
|
|
49246
|
-
<div class="import-row import-row--{{ r.status }}">
|
|
49247
|
-
<span class="import-row__email">{{ r.email }}</span>
|
|
49248
|
-
<span class="import-row__status">{{ statusLabel(r) }}</span>
|
|
49249
|
-
@if (r.reason) {
|
|
49250
|
-
<span class="import-row__reason">{{ r.reason }}</span>
|
|
49251
|
-
}
|
|
49252
|
-
</div>
|
|
49253
|
-
}
|
|
49254
|
-
</div>
|
|
49255
|
-
<div class="import-actions">
|
|
49256
|
-
<ion-button expand="block" fill="outline" color="dark" (click)="reset()">
|
|
49257
|
-
{{ t('again') }}
|
|
49258
|
-
</ion-button>
|
|
49259
|
-
</div>
|
|
49260
|
-
}
|
|
49261
|
-
</ion-content>
|
|
49262
|
-
`, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:12px}.import-csv{--background: var(--ion-color-light);border-radius:10px;margin-bottom:16px;font-family:monospace;font-size:13px}.import-conflict{margin-bottom:12px}.import-activation{display:block;margin-bottom:16px;font-size:14px}.import-error{margin:0 0 12px;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.import-actions{margin-top:8px}.import-results{display:flex;flex-direction:column;gap:6px;margin:12px 0}.import-row{display:grid;grid-template-columns:1fr auto;grid-row-gap:2px;align-items:center;padding:10px 12px;border-radius:8px;border-left:3px solid var(--ion-color-medium);background:var(--ion-color-light);font-size:14px}.import-row--created{border-left-color:var(--ion-color-success, #2dd36f)}.import-row--assigned{border-left-color:var(--ion-color-primary, #7026df)}.import-row--skipped{border-left-color:var(--ion-color-medium, #92949c)}.import-row--error{border-left-color:var(--ion-color-danger, #c0392b)}.import-row__email{font-weight:600;color:var(--ion-color-dark)}.import-row__status{text-align:right;color:var(--ion-color-medium-shade, #6b6e76)}.import-row__reason{grid-column:1 / -1;font-size:12px;color:var(--ion-color-danger, #c0392b)}\n"] }]
|
|
49263
|
-
}], ctorParameters: () => [], propDecorators: { orgId: [{
|
|
49264
|
-
type: Input
|
|
49265
|
-
}], availableRoles: [{
|
|
49266
|
-
type: Input
|
|
49267
|
-
}], onSuccess: [{
|
|
49268
|
-
type: Input
|
|
49269
|
-
}], _modalRef: [{
|
|
49270
|
-
type: Input
|
|
49271
|
-
}], i18nNamespace: [{
|
|
49272
|
-
type: Input
|
|
49273
|
-
}] } });
|
|
49274
|
-
|
|
49275
|
-
/**
|
|
49276
|
-
* Cliente HTTP de Client API Keys org-scopeadas (ADR-023). La org activa se
|
|
49277
|
-
* resuelve en backend desde el JWT, así que el path no la incluye.
|
|
49278
|
-
*/
|
|
49279
|
-
class ApiKeyService {
|
|
49280
|
-
constructor(config, http) {
|
|
49281
|
-
this.config = config;
|
|
49282
|
-
this.http = http;
|
|
49283
|
-
}
|
|
49284
|
-
get baseUrl() {
|
|
49285
|
-
return `${this.config.apiUrl}/org/api-keys`;
|
|
49286
|
-
}
|
|
49287
|
-
/** Lista las API keys de la org activa. */
|
|
49288
|
-
list() {
|
|
49289
|
-
return this.http.get(`${this.baseUrl}/`).pipe(map$1(r => r.keys ?? []));
|
|
49290
|
-
}
|
|
49291
|
-
/** Crea una API key. La respuesta incluye el secreto (se muestra UNA vez). */
|
|
49292
|
-
create(req) {
|
|
49293
|
-
return this.http.post(`${this.baseUrl}/`, req);
|
|
49294
|
-
}
|
|
49295
|
-
/** Revoca (elimina) una API key por id. */
|
|
49296
|
-
revoke(keyId) {
|
|
49297
|
-
return this.http.delete(`${this.baseUrl}/${keyId}`);
|
|
49298
|
-
}
|
|
49299
|
-
/** Catálogo de permisos asignables a una key. */
|
|
49300
|
-
availablePermissions() {
|
|
49301
|
-
return this.http
|
|
49302
|
-
.get(`${this.baseUrl}/permissions`)
|
|
49303
|
-
.pipe(map$1(r => r.permissions ?? []));
|
|
49304
|
-
}
|
|
49305
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeyService, deps: [{ token: VALTECH_AUTH_CONFIG }, { token: i1$3.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
49306
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeyService, providedIn: 'root' }); }
|
|
49307
|
-
}
|
|
49308
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeyService, decorators: [{
|
|
49309
|
-
type: Injectable,
|
|
49310
|
-
args: [{ providedIn: 'root' }]
|
|
49311
|
-
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
49312
|
-
type: Inject,
|
|
49313
|
-
args: [VALTECH_AUTH_CONFIG]
|
|
49314
|
-
}] }, { type: i1$3.HttpClient }] });
|
|
49315
|
-
|
|
49316
|
-
/**
|
|
49317
|
-
* Defaults i18n (es/en) embebidos en `val-api-keys-modal` (ADR-023).
|
|
49318
|
-
* Auto-registrados si el consumer no proveyó el namespace `Settings.ApiKeysModal`.
|
|
49319
|
-
*/
|
|
49320
|
-
const API_KEYS_MODAL_I18N = {
|
|
49321
|
-
es: {
|
|
49322
|
-
title: 'Client API Keys',
|
|
49323
|
-
hint: 'Llaves M2M org-scopeadas para integraciones (p. ej. importar miembros).',
|
|
49324
|
-
empty: 'Aún no hay API keys en esta organización.',
|
|
49325
|
-
newKey: 'Nueva API key',
|
|
49326
|
-
nameLabel: 'Nombre',
|
|
49327
|
-
namePlaceholder: 'Ej: Integración RRHH',
|
|
49328
|
-
permissionsLabel: 'Permisos',
|
|
49329
|
-
expiresLabel: 'Expira en (días, opcional)',
|
|
49330
|
-
create: 'Crear key',
|
|
49331
|
-
creating: 'Creando...',
|
|
49332
|
-
cancel: 'Cancelar',
|
|
49333
|
-
revoke: 'Revocar',
|
|
49334
|
-
revokeConfirm: '¿Revocar esta API key? Las integraciones que la usen dejarán de funcionar.',
|
|
49335
|
-
secretTitle: 'Copia tu API key ahora',
|
|
49336
|
-
secretWarning: 'Este secreto se muestra una sola vez. Guárdalo en un lugar seguro.',
|
|
49337
|
-
copy: 'Copiar',
|
|
49338
|
-
copied: 'Copiada',
|
|
49339
|
-
done: 'Listo',
|
|
49340
|
-
statusActive: 'activa',
|
|
49341
|
-
lastUsed: 'Último uso',
|
|
49342
|
-
never: 'nunca',
|
|
49343
|
-
permsCount: 'permisos',
|
|
49344
|
-
noName: 'El nombre es obligatorio.',
|
|
49345
|
-
noPerms: 'Selecciona al menos un permiso.',
|
|
49346
|
-
close: 'Cerrar',
|
|
49347
|
-
genericError: 'No se pudo completar la operación. Intenta de nuevo.',
|
|
49348
|
-
},
|
|
49349
|
-
en: {
|
|
49350
|
-
title: 'Client API Keys',
|
|
49351
|
-
hint: 'Org-scoped M2M keys for integrations (e.g. member import).',
|
|
49352
|
-
empty: 'No API keys in this organization yet.',
|
|
49353
|
-
newKey: 'New API key',
|
|
49354
|
-
nameLabel: 'Name',
|
|
49355
|
-
namePlaceholder: 'E.g: HR integration',
|
|
49356
|
-
permissionsLabel: 'Permissions',
|
|
49357
|
-
expiresLabel: 'Expires in (days, optional)',
|
|
49358
|
-
create: 'Create key',
|
|
49359
|
-
creating: 'Creating...',
|
|
49360
|
-
cancel: 'Cancel',
|
|
49361
|
-
revoke: 'Revoke',
|
|
49362
|
-
revokeConfirm: 'Revoke this API key? Integrations using it will stop working.',
|
|
49363
|
-
secretTitle: 'Copy your API key now',
|
|
49364
|
-
secretWarning: 'This secret is shown only once. Store it somewhere safe.',
|
|
49365
|
-
copy: 'Copy',
|
|
49366
|
-
copied: 'Copied',
|
|
49367
|
-
done: 'Done',
|
|
49368
|
-
statusActive: 'active',
|
|
49369
|
-
lastUsed: 'Last used',
|
|
49370
|
-
never: 'never',
|
|
49371
|
-
permsCount: 'permissions',
|
|
49372
|
-
noName: 'Name is required.',
|
|
49373
|
-
noPerms: 'Select at least one permission.',
|
|
49374
|
-
close: 'Close',
|
|
49375
|
-
genericError: "Couldn't complete the operation. Try again.",
|
|
49376
|
-
},
|
|
49377
|
-
};
|
|
49378
|
-
|
|
49379
|
-
const DEFAULT_NAMESPACE$2 = 'Settings.ApiKeysModal';
|
|
49380
|
-
/**
|
|
49381
|
-
* `val-api-keys-modal` — gestión de Client API Keys org-scopeadas (ADR-023 fase 6).
|
|
49382
|
-
*
|
|
49383
|
-
* Lista las keys de la org, permite crear (mostrando el secreto UNA vez con copia)
|
|
49384
|
-
* y revocar. Pensado para habilitar integraciones M2M del import de miembros, pero
|
|
49385
|
-
* sirve a cualquier permiso del catálogo. Header canónico (Regla #5).
|
|
49386
|
-
*/
|
|
49387
|
-
class ApiKeysModalComponent {
|
|
49388
|
-
constructor() {
|
|
49389
|
-
this.i18n = inject(I18nService);
|
|
49390
|
-
this.apiKeys = inject(ApiKeyService);
|
|
49391
|
-
/** Namespace i18n. */
|
|
49392
|
-
this.i18nNamespace = DEFAULT_NAMESPACE$2;
|
|
49393
|
-
this.keys = signal([]);
|
|
49394
|
-
this.availablePermissions = signal([]);
|
|
49395
|
-
this.loading = signal(true);
|
|
49396
|
-
this.showCreate = signal(false);
|
|
49397
|
-
this.submitting = signal(false);
|
|
49398
|
-
this.errorMsg = signal('');
|
|
49399
|
-
this.secret = signal(null);
|
|
49400
|
-
this.copied = signal(false);
|
|
49401
|
-
this.name = '';
|
|
49402
|
-
this.expiresInDays = null;
|
|
49403
|
-
this.selectedPerms = signal(new Set());
|
|
49404
|
-
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$2)) {
|
|
49405
|
-
this.i18n.registerContent(DEFAULT_NAMESPACE$2, API_KEYS_MODAL_I18N);
|
|
49406
|
-
}
|
|
49407
|
-
}
|
|
49408
|
-
ngOnInit() {
|
|
49409
|
-
if (!this.i18n.hasNamespace(this.i18nNamespace)) {
|
|
49410
|
-
this.i18n.registerContent(this.i18nNamespace, API_KEYS_MODAL_I18N);
|
|
49411
|
-
}
|
|
49412
|
-
this.refresh();
|
|
49413
|
-
this.apiKeys.availablePermissions().subscribe({
|
|
49414
|
-
next: perms => this.availablePermissions.set(perms),
|
|
49415
|
-
error: () => { },
|
|
49416
|
-
});
|
|
49417
|
-
}
|
|
49418
|
-
refresh() {
|
|
49419
|
-
this.loading.set(true);
|
|
49420
|
-
this.apiKeys.list().subscribe({
|
|
49421
|
-
next: keys => {
|
|
49422
|
-
this.keys.set(keys);
|
|
49423
|
-
this.loading.set(false);
|
|
49424
|
-
},
|
|
49425
|
-
error: () => this.loading.set(false),
|
|
49426
|
-
});
|
|
49427
|
-
}
|
|
49428
|
-
openCreate() {
|
|
49429
|
-
this.errorMsg.set('');
|
|
49430
|
-
this.name = '';
|
|
49431
|
-
this.expiresInDays = null;
|
|
49432
|
-
this.selectedPerms.set(new Set());
|
|
49433
|
-
this.showCreate.set(true);
|
|
49434
|
-
}
|
|
49435
|
-
togglePerm(perm, ev) {
|
|
49436
|
-
const checked = ev.detail?.checked ?? false;
|
|
49437
|
-
const next = new Set(this.selectedPerms());
|
|
49438
|
-
if (checked) {
|
|
49439
|
-
next.add(perm);
|
|
49440
|
-
}
|
|
49441
|
-
else {
|
|
49442
|
-
next.delete(perm);
|
|
49443
|
-
}
|
|
49444
|
-
this.selectedPerms.set(next);
|
|
49445
|
-
}
|
|
49446
|
-
submit() {
|
|
49447
|
-
this.errorMsg.set('');
|
|
49448
|
-
if (!this.name.trim()) {
|
|
49449
|
-
this.errorMsg.set(this.t('noName'));
|
|
49450
|
-
return;
|
|
49451
|
-
}
|
|
49452
|
-
const permissions = Array.from(this.selectedPerms());
|
|
49453
|
-
if (permissions.length === 0) {
|
|
49454
|
-
this.errorMsg.set(this.t('noPerms'));
|
|
49455
|
-
return;
|
|
49456
|
-
}
|
|
49457
|
-
this.submitting.set(true);
|
|
49458
|
-
this.apiKeys
|
|
49459
|
-
.create({
|
|
49460
|
-
name: this.name.trim(),
|
|
49461
|
-
permissions,
|
|
49462
|
-
expiresInDays: this.expiresInDays && this.expiresInDays > 0 ? this.expiresInDays : undefined,
|
|
49463
|
-
})
|
|
49464
|
-
.subscribe({
|
|
49465
|
-
next: res => {
|
|
49466
|
-
this.submitting.set(false);
|
|
49467
|
-
this.showCreate.set(false);
|
|
49468
|
-
this.secret.set(res.key);
|
|
49469
|
-
},
|
|
49470
|
-
error: err => {
|
|
49471
|
-
this.submitting.set(false);
|
|
49472
|
-
this.errorMsg.set(err?.error?.message ?? this.t('genericError'));
|
|
49473
|
-
},
|
|
49474
|
-
});
|
|
49475
|
-
}
|
|
49476
|
-
revoke(key) {
|
|
49477
|
-
if (!confirm(this.t('revokeConfirm')))
|
|
49478
|
-
return;
|
|
49479
|
-
this.apiKeys.revoke(key.keyId).subscribe({
|
|
49480
|
-
next: () => this.refresh(),
|
|
49481
|
-
error: err => this.errorMsg.set(err?.error?.message ?? this.t('genericError')),
|
|
49482
|
-
});
|
|
49483
|
-
}
|
|
49484
|
-
async copy(secret) {
|
|
49485
|
-
try {
|
|
49486
|
-
await navigator.clipboard.writeText(secret);
|
|
49487
|
-
this.copied.set(true);
|
|
49488
|
-
}
|
|
49489
|
-
catch {
|
|
49490
|
-
// clipboard no disponible — el user puede seleccionar el texto manualmente
|
|
49491
|
-
}
|
|
49492
|
-
}
|
|
49493
|
-
finishSecret() {
|
|
49494
|
-
this.secret.set(null);
|
|
49495
|
-
this.copied.set(false);
|
|
49496
|
-
this.refresh();
|
|
49497
|
-
}
|
|
49498
|
-
dismiss() {
|
|
49499
|
-
this._modalRef?.dismiss(null, 'cancel');
|
|
49500
|
-
}
|
|
49501
|
-
t(key) {
|
|
49502
|
-
return this.i18n.t(key, this.i18nNamespace);
|
|
49503
|
-
}
|
|
49504
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeysModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
49505
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ApiKeysModalComponent, isStandalone: true, selector: "val-api-keys-modal", inputs: { _modalRef: "_modalRef", i18nNamespace: "i18nNamespace" }, ngImport: i0, template: `
|
|
49506
|
-
<ion-header>
|
|
49507
|
-
<ion-toolbar>
|
|
49508
|
-
<ion-buttons slot="end">
|
|
49509
|
-
<ion-button fill="clear" color="dark" (click)="dismiss()">
|
|
49510
|
-
<strong>{{ t('close') }}</strong>
|
|
49511
|
-
</ion-button>
|
|
49512
|
-
</ion-buttons>
|
|
49513
|
-
</ion-toolbar>
|
|
49514
|
-
</ion-header>
|
|
49515
|
-
|
|
49516
|
-
<ion-content class="ion-padding">
|
|
49517
|
-
<val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
|
|
49518
|
-
<val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
|
|
49519
|
-
|
|
49520
|
-
@if (secret(); as s) {
|
|
49521
|
-
<!-- Secreto recién creado: mostrar UNA vez -->
|
|
49522
|
-
<div class="apikey-secret">
|
|
49523
|
-
<val-title [props]="{ content: t('secretTitle'), size: 'large', color: '', bold: true }" />
|
|
49524
|
-
<p class="apikey-secret__warn">{{ t('secretWarning') }}</p>
|
|
49525
|
-
<div class="apikey-secret__box">
|
|
49526
|
-
<code>{{ s.secret }}</code>
|
|
49527
|
-
</div>
|
|
49528
|
-
<div class="apikey-actions">
|
|
49529
|
-
<ion-button expand="block" color="dark" (click)="copy(s.secret)">
|
|
49530
|
-
{{ copied() ? t('copied') : t('copy') }}
|
|
49531
|
-
</ion-button>
|
|
49532
|
-
<ion-button expand="block" fill="outline" color="dark" (click)="finishSecret()">
|
|
49533
|
-
{{ t('done') }}
|
|
49534
|
-
</ion-button>
|
|
49535
|
-
</div>
|
|
49536
|
-
</div>
|
|
49537
|
-
} @else {
|
|
49538
|
-
@if (loading()) {
|
|
49539
|
-
<ion-spinner name="dots" />
|
|
49540
|
-
} @else {
|
|
49541
|
-
<!-- Lista -->
|
|
49542
|
-
@if (keys().length === 0) {
|
|
49543
|
-
<val-text [props]="{ content: t('empty'), size: 'medium', color: 'medium', bold: false }" />
|
|
49544
|
-
} @else {
|
|
49545
|
-
<div class="apikey-list">
|
|
49546
|
-
@for (k of keys(); track k.keyId) {
|
|
49547
|
-
<div class="apikey-row">
|
|
49548
|
-
<div class="apikey-row__main">
|
|
49549
|
-
<span class="apikey-row__name">{{ k.name }}</span>
|
|
49550
|
-
<span class="apikey-row__meta">
|
|
49551
|
-
{{ k.permissions.length }} {{ t('permsCount') }} · {{ t('lastUsed') }}:
|
|
49552
|
-
{{ k.lastUsedAt || t('never') }}
|
|
49553
|
-
</span>
|
|
49554
|
-
</div>
|
|
49555
|
-
<ion-button fill="clear" color="danger" size="small" (click)="revoke(k)">
|
|
49556
|
-
{{ t('revoke') }}
|
|
49557
|
-
</ion-button>
|
|
49558
|
-
</div>
|
|
49559
|
-
}
|
|
49560
|
-
</div>
|
|
49561
|
-
}
|
|
49562
|
-
|
|
49563
|
-
@if (errorMsg()) {
|
|
49564
|
-
<p class="apikey-error">{{ errorMsg() }}</p>
|
|
49565
|
-
}
|
|
49566
|
-
|
|
49567
|
-
<!-- Crear -->
|
|
49568
|
-
@if (showCreate()) {
|
|
49569
|
-
<div class="apikey-create">
|
|
49570
|
-
<ion-input
|
|
49571
|
-
label="{{ t('nameLabel') }}"
|
|
49572
|
-
labelPlacement="stacked"
|
|
49573
|
-
[placeholder]="t('namePlaceholder')"
|
|
49574
|
-
[(ngModel)]="name"
|
|
49575
|
-
/>
|
|
49576
|
-
<p class="apikey-create__label">{{ t('permissionsLabel') }}</p>
|
|
49577
|
-
<div class="apikey-perms">
|
|
49578
|
-
@for (p of availablePermissions(); track p) {
|
|
49579
|
-
<ion-checkbox
|
|
49580
|
-
labelPlacement="end"
|
|
49581
|
-
[checked]="selectedPerms().has(p)"
|
|
49582
|
-
(ionChange)="togglePerm(p, $event)"
|
|
49583
|
-
>
|
|
49584
|
-
{{ p }}
|
|
49585
|
-
</ion-checkbox>
|
|
49586
|
-
}
|
|
49587
|
-
</div>
|
|
49588
|
-
<ion-input
|
|
49589
|
-
label="{{ t('expiresLabel') }}"
|
|
49590
|
-
labelPlacement="stacked"
|
|
49591
|
-
type="number"
|
|
49592
|
-
[(ngModel)]="expiresInDays"
|
|
49593
|
-
/>
|
|
49594
|
-
<div class="apikey-actions">
|
|
49595
|
-
<ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
|
|
49596
|
-
@if (submitting()) {
|
|
49597
|
-
<ion-spinner name="dots" /> {{ t('creating') }}
|
|
49598
|
-
} @else {
|
|
49599
|
-
{{ t('create') }}
|
|
49600
|
-
}
|
|
49601
|
-
</ion-button>
|
|
49602
|
-
<ion-button expand="block" fill="clear" color="medium" (click)="showCreate.set(false)">
|
|
49603
|
-
{{ t('cancel') }}
|
|
49604
|
-
</ion-button>
|
|
49605
|
-
</div>
|
|
49606
|
-
</div>
|
|
49607
|
-
} @else {
|
|
49608
|
-
<div class="apikey-actions">
|
|
49609
|
-
<ion-button expand="block" color="primary" (click)="openCreate()">
|
|
49610
|
-
{{ t('newKey') }}
|
|
49611
|
-
</ion-button>
|
|
49612
|
-
</div>
|
|
49613
|
-
}
|
|
49614
|
-
}
|
|
49615
|
-
}
|
|
49616
|
-
</ion-content>
|
|
49617
|
-
`, isInline: true, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:12px}.apikey-list{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}.apikey-row{display:flex;align-items:center;justify-content:space-between;padding:12px;border-radius:10px;border:1.5px solid var(--ion-color-light)}.apikey-row__main{display:flex;flex-direction:column}.apikey-row__name{font-weight:600;color:var(--ion-color-dark)}.apikey-row__meta{font-size:12px;color:var(--ion-color-medium-shade, #6b6e76)}.apikey-create{margin-top:8px;padding:16px;border-radius:12px;background:var(--ion-color-light)}.apikey-create__label{margin:12px 0 4px;font-size:13px;font-weight:600;color:var(--ion-color-dark)}.apikey-perms{display:flex;flex-direction:column;gap:6px;margin-bottom:12px;font-size:13px}.apikey-actions{margin-top:16px;display:flex;flex-direction:column;gap:8px}.apikey-error{margin:8px 0;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.apikey-secret__warn{font-size:.875rem;color:var(--ion-color-warning-shade, #b88600);margin:0 0 12px}.apikey-secret__box{padding:12px;border-radius:8px;background:var(--ion-color-dark);color:#fff;word-break:break-all;font-family:monospace;font-size:13px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$7.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$7.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonCheckbox, selector: "ion-checkbox", inputs: ["checked", "color", "disabled", "errorText", "helperText", "indeterminate", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }] }); }
|
|
49618
|
-
}
|
|
49619
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeysModalComponent, decorators: [{
|
|
49620
|
-
type: Component,
|
|
49621
|
-
args: [{ selector: 'val-api-keys-modal', standalone: true, imports: [
|
|
49622
|
-
FormsModule,
|
|
49623
|
-
IonHeader,
|
|
49624
|
-
IonToolbar,
|
|
49625
|
-
IonButtons,
|
|
49626
|
-
IonButton,
|
|
49627
|
-
IonContent,
|
|
49628
|
-
IonInput,
|
|
49629
|
-
IonCheckbox,
|
|
49630
|
-
IonSpinner,
|
|
49631
|
-
DisplayComponent,
|
|
49632
|
-
TextComponent,
|
|
49633
|
-
TitleComponent,
|
|
49634
|
-
], template: `
|
|
49635
|
-
<ion-header>
|
|
49636
|
-
<ion-toolbar>
|
|
49637
|
-
<ion-buttons slot="end">
|
|
49638
|
-
<ion-button fill="clear" color="dark" (click)="dismiss()">
|
|
49639
|
-
<strong>{{ t('close') }}</strong>
|
|
49640
|
-
</ion-button>
|
|
49641
|
-
</ion-buttons>
|
|
49642
|
-
</ion-toolbar>
|
|
49643
|
-
</ion-header>
|
|
49644
|
-
|
|
49645
|
-
<ion-content class="ion-padding">
|
|
49646
|
-
<val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
|
|
49647
|
-
<val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
|
|
49648
|
-
|
|
49649
|
-
@if (secret(); as s) {
|
|
49650
|
-
<!-- Secreto recién creado: mostrar UNA vez -->
|
|
49651
|
-
<div class="apikey-secret">
|
|
49652
|
-
<val-title [props]="{ content: t('secretTitle'), size: 'large', color: '', bold: true }" />
|
|
49653
|
-
<p class="apikey-secret__warn">{{ t('secretWarning') }}</p>
|
|
49654
|
-
<div class="apikey-secret__box">
|
|
49655
|
-
<code>{{ s.secret }}</code>
|
|
49656
|
-
</div>
|
|
49657
|
-
<div class="apikey-actions">
|
|
49658
|
-
<ion-button expand="block" color="dark" (click)="copy(s.secret)">
|
|
49659
|
-
{{ copied() ? t('copied') : t('copy') }}
|
|
49660
|
-
</ion-button>
|
|
49661
|
-
<ion-button expand="block" fill="outline" color="dark" (click)="finishSecret()">
|
|
49662
|
-
{{ t('done') }}
|
|
49663
|
-
</ion-button>
|
|
49664
|
-
</div>
|
|
49665
|
-
</div>
|
|
49666
|
-
} @else {
|
|
49667
|
-
@if (loading()) {
|
|
49668
|
-
<ion-spinner name="dots" />
|
|
49669
|
-
} @else {
|
|
49670
|
-
<!-- Lista -->
|
|
49671
|
-
@if (keys().length === 0) {
|
|
49672
|
-
<val-text [props]="{ content: t('empty'), size: 'medium', color: 'medium', bold: false }" />
|
|
49673
|
-
} @else {
|
|
49674
|
-
<div class="apikey-list">
|
|
49675
|
-
@for (k of keys(); track k.keyId) {
|
|
49676
|
-
<div class="apikey-row">
|
|
49677
|
-
<div class="apikey-row__main">
|
|
49678
|
-
<span class="apikey-row__name">{{ k.name }}</span>
|
|
49679
|
-
<span class="apikey-row__meta">
|
|
49680
|
-
{{ k.permissions.length }} {{ t('permsCount') }} · {{ t('lastUsed') }}:
|
|
49681
|
-
{{ k.lastUsedAt || t('never') }}
|
|
49682
|
-
</span>
|
|
49683
|
-
</div>
|
|
49684
|
-
<ion-button fill="clear" color="danger" size="small" (click)="revoke(k)">
|
|
49685
|
-
{{ t('revoke') }}
|
|
49686
|
-
</ion-button>
|
|
49687
|
-
</div>
|
|
49688
|
-
}
|
|
49689
|
-
</div>
|
|
49690
|
-
}
|
|
49691
|
-
|
|
49692
|
-
@if (errorMsg()) {
|
|
49693
|
-
<p class="apikey-error">{{ errorMsg() }}</p>
|
|
49694
|
-
}
|
|
49695
|
-
|
|
49696
|
-
<!-- Crear -->
|
|
49697
|
-
@if (showCreate()) {
|
|
49698
|
-
<div class="apikey-create">
|
|
49699
|
-
<ion-input
|
|
49700
|
-
label="{{ t('nameLabel') }}"
|
|
49701
|
-
labelPlacement="stacked"
|
|
49702
|
-
[placeholder]="t('namePlaceholder')"
|
|
49703
|
-
[(ngModel)]="name"
|
|
49704
|
-
/>
|
|
49705
|
-
<p class="apikey-create__label">{{ t('permissionsLabel') }}</p>
|
|
49706
|
-
<div class="apikey-perms">
|
|
49707
|
-
@for (p of availablePermissions(); track p) {
|
|
49708
|
-
<ion-checkbox
|
|
49709
|
-
labelPlacement="end"
|
|
49710
|
-
[checked]="selectedPerms().has(p)"
|
|
49711
|
-
(ionChange)="togglePerm(p, $event)"
|
|
49712
|
-
>
|
|
49713
|
-
{{ p }}
|
|
49714
|
-
</ion-checkbox>
|
|
49715
|
-
}
|
|
49716
|
-
</div>
|
|
49717
|
-
<ion-input
|
|
49718
|
-
label="{{ t('expiresLabel') }}"
|
|
49719
|
-
labelPlacement="stacked"
|
|
49720
|
-
type="number"
|
|
49721
|
-
[(ngModel)]="expiresInDays"
|
|
49722
|
-
/>
|
|
49723
|
-
<div class="apikey-actions">
|
|
49724
|
-
<ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
|
|
49725
|
-
@if (submitting()) {
|
|
49726
|
-
<ion-spinner name="dots" /> {{ t('creating') }}
|
|
49727
|
-
} @else {
|
|
49728
|
-
{{ t('create') }}
|
|
49729
|
-
}
|
|
49730
|
-
</ion-button>
|
|
49731
|
-
<ion-button expand="block" fill="clear" color="medium" (click)="showCreate.set(false)">
|
|
49732
|
-
{{ t('cancel') }}
|
|
49733
|
-
</ion-button>
|
|
49734
|
-
</div>
|
|
49735
|
-
</div>
|
|
49736
|
-
} @else {
|
|
49737
|
-
<div class="apikey-actions">
|
|
49738
|
-
<ion-button expand="block" color="primary" (click)="openCreate()">
|
|
49739
|
-
{{ t('newKey') }}
|
|
49740
|
-
</ion-button>
|
|
49741
|
-
</div>
|
|
49742
|
-
}
|
|
49743
|
-
}
|
|
49744
|
-
}
|
|
49745
|
-
</ion-content>
|
|
49746
|
-
`, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:12px}.apikey-list{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}.apikey-row{display:flex;align-items:center;justify-content:space-between;padding:12px;border-radius:10px;border:1.5px solid var(--ion-color-light)}.apikey-row__main{display:flex;flex-direction:column}.apikey-row__name{font-weight:600;color:var(--ion-color-dark)}.apikey-row__meta{font-size:12px;color:var(--ion-color-medium-shade, #6b6e76)}.apikey-create{margin-top:8px;padding:16px;border-radius:12px;background:var(--ion-color-light)}.apikey-create__label{margin:12px 0 4px;font-size:13px;font-weight:600;color:var(--ion-color-dark)}.apikey-perms{display:flex;flex-direction:column;gap:6px;margin-bottom:12px;font-size:13px}.apikey-actions{margin-top:16px;display:flex;flex-direction:column;gap:8px}.apikey-error{margin:8px 0;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.apikey-secret__warn{font-size:.875rem;color:var(--ion-color-warning-shade, #b88600);margin:0 0 12px}.apikey-secret__box{padding:12px;border-radius:8px;background:var(--ion-color-dark);color:#fff;word-break:break-all;font-family:monospace;font-size:13px}\n"] }]
|
|
49747
|
-
}], ctorParameters: () => [], propDecorators: { _modalRef: [{
|
|
49748
|
-
type: Input
|
|
49749
|
-
}], i18nNamespace: [{
|
|
49750
|
-
type: Input
|
|
49751
|
-
}] } });
|
|
49752
|
-
|
|
49753
48834
|
/**
|
|
49754
48835
|
* Defaults i18n (es/en) embebidos en `val-organization-view`. Auto-registrados en
|
|
49755
48836
|
* el constructor del componente si el consumer no proveyó el namespace
|
|
@@ -49805,8 +48886,6 @@ const ORGANIZATION_VIEW_I18N = {
|
|
|
49805
48886
|
inviteCtaTitle: '¿Deseas agregar a alguien?',
|
|
49806
48887
|
inviteCtaHint: 'Invita a usuarios a unirse a tu organización.',
|
|
49807
48888
|
inviteOpen: 'Invitar',
|
|
49808
|
-
importOpen: 'Importar',
|
|
49809
|
-
apiKeysOpen: 'API Keys',
|
|
49810
48889
|
viewMemberCta: 'Ver miembro',
|
|
49811
48890
|
roleViewer: 'Visor',
|
|
49812
48891
|
roleEditor: 'Editor',
|
|
@@ -49876,8 +48955,6 @@ const ORGANIZATION_VIEW_I18N = {
|
|
|
49876
48955
|
inviteCtaTitle: 'Want to add someone?',
|
|
49877
48956
|
inviteCtaHint: 'Invite users to join your organization.',
|
|
49878
48957
|
inviteOpen: 'Invite',
|
|
49879
|
-
importOpen: 'Import',
|
|
49880
|
-
apiKeysOpen: 'API Keys',
|
|
49881
48958
|
viewMemberCta: 'View member',
|
|
49882
48959
|
roleViewer: 'Viewer',
|
|
49883
48960
|
roleEditor: 'Editor',
|
|
@@ -49902,7 +48979,7 @@ const ORGANIZATION_VIEW_I18N = {
|
|
|
49902
48979
|
},
|
|
49903
48980
|
};
|
|
49904
48981
|
|
|
49905
|
-
const DEFAULT_NAMESPACE$
|
|
48982
|
+
const DEFAULT_NAMESPACE$3 = 'Settings.Organization';
|
|
49906
48983
|
const DEFAULT_VIEW_PERMISSIONS_ROUTE = '/app/settings/permissions';
|
|
49907
48984
|
const DEFAULT_LEAVE_REDIRECT_ROUTE = '/app/settings/account';
|
|
49908
48985
|
/**
|
|
@@ -49959,7 +49036,7 @@ class OrganizationViewComponent {
|
|
|
49959
49036
|
showInvite: merged.showInvite ?? true,
|
|
49960
49037
|
showTransferOwnership: merged.showTransferOwnership ?? true,
|
|
49961
49038
|
showLeave: merged.showLeave ?? true,
|
|
49962
|
-
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$
|
|
49039
|
+
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$3,
|
|
49963
49040
|
viewPermissionsRoute: merged.viewPermissionsRoute ?? DEFAULT_VIEW_PERMISSIONS_ROUTE,
|
|
49964
49041
|
leaveRedirectRoute: merged.leaveRedirectRoute ?? DEFAULT_LEAVE_REDIRECT_ROUTE,
|
|
49965
49042
|
onOrgEdited: merged.onOrgEdited,
|
|
@@ -49991,22 +49068,6 @@ class OrganizationViewComponent {
|
|
|
49991
49068
|
this.members = signal([]);
|
|
49992
49069
|
this.membersLoading = signal(false);
|
|
49993
49070
|
this.membersLoadError = signal(null);
|
|
49994
|
-
this.membersErrorState = computed(() => {
|
|
49995
|
-
this.i18n.lang();
|
|
49996
|
-
const err = this.membersLoadError();
|
|
49997
|
-
if (!err)
|
|
49998
|
-
return null;
|
|
49999
|
-
return createErrorStateProps(err, {
|
|
50000
|
-
title: { offline: this.tt('offlineTitle'), error: this.tt('errorTitle') },
|
|
50001
|
-
description: {
|
|
50002
|
-
offline: this.tt('offlineHint'),
|
|
50003
|
-
error: interpretError(err).message,
|
|
50004
|
-
},
|
|
50005
|
-
retryLabel: this.tt('retry'),
|
|
50006
|
-
onRetry: () => this.loadMembers(),
|
|
50007
|
-
retrying: this.membersLoading(),
|
|
50008
|
-
});
|
|
50009
|
-
});
|
|
50010
49071
|
this.membersNextToken = signal(null);
|
|
50011
49072
|
this.loadingMoreMembers = signal(false);
|
|
50012
49073
|
this.showAllMembers = signal(false);
|
|
@@ -50093,22 +49154,6 @@ class OrganizationViewComponent {
|
|
|
50093
49154
|
shape: 'round',
|
|
50094
49155
|
type: 'button',
|
|
50095
49156
|
}));
|
|
50096
|
-
this.openImportButtonProps = computed(() => ({
|
|
50097
|
-
text: this.tt('importOpen'),
|
|
50098
|
-
color: 'dark',
|
|
50099
|
-
fill: 'outline',
|
|
50100
|
-
size: 'default',
|
|
50101
|
-
shape: 'round',
|
|
50102
|
-
type: 'button',
|
|
50103
|
-
}));
|
|
50104
|
-
this.openApiKeysButtonProps = computed(() => ({
|
|
50105
|
-
text: this.tt('apiKeysOpen'),
|
|
50106
|
-
color: 'dark',
|
|
50107
|
-
fill: 'clear',
|
|
50108
|
-
size: 'default',
|
|
50109
|
-
shape: 'round',
|
|
50110
|
-
type: 'button',
|
|
50111
|
-
}));
|
|
50112
49157
|
/** Guarda para detectar el primer disparo del effect (evita doble carga). */
|
|
50113
49158
|
this.lastLoadedOrgId = null;
|
|
50114
49159
|
const ns = this.ns;
|
|
@@ -50209,36 +49254,6 @@ class OrganizationViewComponent {
|
|
|
50209
49254
|
backdropDismiss: true,
|
|
50210
49255
|
});
|
|
50211
49256
|
}
|
|
50212
|
-
onOpenImport() {
|
|
50213
|
-
void this.modalService.open({
|
|
50214
|
-
component: MemberImportModalComponent,
|
|
50215
|
-
componentProps: {
|
|
50216
|
-
orgId: this.activeOrgId(),
|
|
50217
|
-
availableRoles: this.availableRoles().map(r => r.name),
|
|
50218
|
-
onSuccess: () => {
|
|
50219
|
-
this.loadMembers();
|
|
50220
|
-
this.resolvedConfig().onMemberInvited?.();
|
|
50221
|
-
},
|
|
50222
|
-
},
|
|
50223
|
-
breakpoints: {
|
|
50224
|
-
breakpoints: [0, 0.92],
|
|
50225
|
-
initialBreakpoint: 0.92,
|
|
50226
|
-
showHandle: true,
|
|
50227
|
-
},
|
|
50228
|
-
backdropDismiss: true,
|
|
50229
|
-
});
|
|
50230
|
-
}
|
|
50231
|
-
onOpenApiKeys() {
|
|
50232
|
-
void this.modalService.open({
|
|
50233
|
-
component: ApiKeysModalComponent,
|
|
50234
|
-
breakpoints: {
|
|
50235
|
-
breakpoints: [0, 0.92],
|
|
50236
|
-
initialBreakpoint: 0.92,
|
|
50237
|
-
showHandle: true,
|
|
50238
|
-
},
|
|
50239
|
-
backdropDismiss: true,
|
|
50240
|
-
});
|
|
50241
|
-
}
|
|
50242
49257
|
onViewPermissions() {
|
|
50243
49258
|
this.nav.navigateByUrl(this.resolvedConfig().viewPermissionsRoute);
|
|
50244
49259
|
}
|
|
@@ -50320,7 +49335,6 @@ class OrganizationViewComponent {
|
|
|
50320
49335
|
if (!orgId)
|
|
50321
49336
|
return Promise.resolve();
|
|
50322
49337
|
this.membersLoading.set(true);
|
|
50323
|
-
this.membersLoadError.set(null);
|
|
50324
49338
|
this.membersNextToken.set(null);
|
|
50325
49339
|
this.showAllMembers.set(false);
|
|
50326
49340
|
return new Promise(resolve => {
|
|
@@ -50531,8 +49545,6 @@ class OrganizationViewComponent {
|
|
|
50531
49545
|
<div class="section-body">
|
|
50532
49546
|
@if (membersLoading()) {
|
|
50533
49547
|
<val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
|
|
50534
|
-
} @else if (membersErrorState()) {
|
|
50535
|
-
<val-empty-state [props]="membersErrorState()!" />
|
|
50536
49548
|
} @else if (members().length === 0) {
|
|
50537
49549
|
<val-text
|
|
50538
49550
|
[props]="{
|
|
@@ -50625,8 +49637,6 @@ class OrganizationViewComponent {
|
|
|
50625
49637
|
/>
|
|
50626
49638
|
</div>
|
|
50627
49639
|
<val-button [props]="openInviteButtonProps()" (click)="onOpenInvite()" />
|
|
50628
|
-
<val-button [props]="openImportButtonProps()" (click)="onOpenImport()" />
|
|
50629
|
-
<val-button [props]="openApiKeysButtonProps()" (click)="onOpenApiKeys()" />
|
|
50630
49640
|
</div>
|
|
50631
49641
|
</section>
|
|
50632
49642
|
}
|
|
@@ -50735,8 +49745,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
50735
49745
|
ButtonComponent,
|
|
50736
49746
|
MemberCardComponent,
|
|
50737
49747
|
TransferOwnershipModalComponent,
|
|
50738
|
-
MemberImportModalComponent,
|
|
50739
|
-
ApiKeysModalComponent,
|
|
50740
49748
|
], template: `
|
|
50741
49749
|
<div class="page">
|
|
50742
49750
|
<header class="page-header">
|
|
@@ -50815,8 +49823,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
50815
49823
|
<div class="section-body">
|
|
50816
49824
|
@if (membersLoading()) {
|
|
50817
49825
|
<val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
|
|
50818
|
-
} @else if (membersErrorState()) {
|
|
50819
|
-
<val-empty-state [props]="membersErrorState()!" />
|
|
50820
49826
|
} @else if (members().length === 0) {
|
|
50821
49827
|
<val-text
|
|
50822
49828
|
[props]="{
|
|
@@ -50909,8 +49915,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
50909
49915
|
/>
|
|
50910
49916
|
</div>
|
|
50911
49917
|
<val-button [props]="openInviteButtonProps()" (click)="onOpenInvite()" />
|
|
50912
|
-
<val-button [props]="openImportButtonProps()" (click)="onOpenImport()" />
|
|
50913
|
-
<val-button [props]="openApiKeysButtonProps()" (click)="onOpenApiKeys()" />
|
|
50914
49918
|
</div>
|
|
50915
49919
|
</section>
|
|
50916
49920
|
}
|
|
@@ -51039,6 +50043,70 @@ function provideValtechOrganizationRoutes(opts) {
|
|
|
51039
50043
|
];
|
|
51040
50044
|
}
|
|
51041
50045
|
|
|
50046
|
+
/**
|
|
50047
|
+
* Helper para montar el bloque completo de Settings en una app del factory: el
|
|
50048
|
+
* HUB de navegación (`val-settings-hub`) en el index + las vistas promovidas
|
|
50049
|
+
* (Perfil, Preferencias) + las rutas propias de la app (`extraRoutes`).
|
|
50050
|
+
*
|
|
50051
|
+
* Pensado para apps nuevas (greenfield) que adoptan el set entero. showcase NO
|
|
50052
|
+
* lo usa hoy (preserva su `settings.routes.ts` por la semántica de sus muchas
|
|
50053
|
+
* secciones no promovidas — ver guía de promoción), pero el provider debe
|
|
50054
|
+
* existir y compilar para el resto de apps.
|
|
50055
|
+
*
|
|
50056
|
+
* @example
|
|
50057
|
+
* ```ts
|
|
50058
|
+
* // app.routes.ts
|
|
50059
|
+
* import { provideValtechSettingsRoutes } from 'valtech-components';
|
|
50060
|
+
*
|
|
50061
|
+
* export const routes: Routes = [
|
|
50062
|
+
* {
|
|
50063
|
+
* path: 'app',
|
|
50064
|
+
* children: [
|
|
50065
|
+
* ...provideValtechSettingsRoutes({
|
|
50066
|
+
* hubConfig: { onLogout: () => auth.logoutWithConfirmation() },
|
|
50067
|
+
* extraRoutes: [
|
|
50068
|
+
* { path: 'security', loadComponent: () => ... },
|
|
50069
|
+
* ],
|
|
50070
|
+
* }),
|
|
50071
|
+
* ],
|
|
50072
|
+
* },
|
|
50073
|
+
* ];
|
|
50074
|
+
* ```
|
|
50075
|
+
*/
|
|
50076
|
+
function provideValtechSettingsRoutes(opts) {
|
|
50077
|
+
const basePath = opts?.basePath ?? 'settings';
|
|
50078
|
+
const includeProfile = opts?.includeProfile ?? true;
|
|
50079
|
+
const includePreferences = opts?.includePreferences ?? true;
|
|
50080
|
+
const includeSecurity = opts?.includeSecurity ?? true;
|
|
50081
|
+
const includeAccount = opts?.includeAccount ?? true;
|
|
50082
|
+
const includeOrganization = opts?.includeOrganization ?? true;
|
|
50083
|
+
return [
|
|
50084
|
+
{
|
|
50085
|
+
path: basePath,
|
|
50086
|
+
children: [
|
|
50087
|
+
{
|
|
50088
|
+
path: '',
|
|
50089
|
+
component: SettingsHubComponent,
|
|
50090
|
+
data: { settingsHubConfig: opts?.hubConfig },
|
|
50091
|
+
},
|
|
50092
|
+
...(includeProfile ? provideValtechProfileRoutes({ config: opts?.profileConfig }) : []),
|
|
50093
|
+
...(includePreferences
|
|
50094
|
+
? provideValtechPreferencesRoutes({
|
|
50095
|
+
path: opts?.preferencesPath ?? 'preferences',
|
|
50096
|
+
config: opts?.preferencesConfig,
|
|
50097
|
+
})
|
|
50098
|
+
: []),
|
|
50099
|
+
...(includeSecurity ? provideValtechSecurityRoutes({ config: opts?.securityConfig }) : []),
|
|
50100
|
+
...(includeAccount ? provideValtechAccountRoutes({ config: opts?.accountConfig }) : []),
|
|
50101
|
+
...(includeOrganization
|
|
50102
|
+
? provideValtechOrganizationRoutes({ config: opts?.organizationConfig })
|
|
50103
|
+
: []),
|
|
50104
|
+
...(opts?.extraRoutes ?? []),
|
|
50105
|
+
],
|
|
50106
|
+
},
|
|
50107
|
+
];
|
|
50108
|
+
}
|
|
50109
|
+
|
|
51042
50110
|
/**
|
|
51043
50111
|
* Refreshable Stream
|
|
51044
50112
|
*
|
|
@@ -51247,7 +50315,7 @@ addIcons({
|
|
|
51247
50315
|
openOutline,
|
|
51248
50316
|
personOutline,
|
|
51249
50317
|
});
|
|
51250
|
-
const DEFAULT_NAMESPACE = 'Notifications';
|
|
50318
|
+
const DEFAULT_NAMESPACE$2 = 'Notifications';
|
|
51251
50319
|
const DEFAULT_SETTINGS_PATH = '/app/settings/notifications';
|
|
51252
50320
|
const DEFAULT_PAGE_SIZE = 10;
|
|
51253
50321
|
const DEFAULT_MAX_LOAD = 50;
|
|
@@ -51307,7 +50375,7 @@ class NotificationsViewComponent {
|
|
|
51307
50375
|
showPushOffCta: merged.showPushOffCta ?? true,
|
|
51308
50376
|
enableMarkAll: merged.enableMarkAll ?? true,
|
|
51309
50377
|
settingsNotificationsPath: merged.settingsNotificationsPath ?? DEFAULT_SETTINGS_PATH,
|
|
51310
|
-
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
|
|
50378
|
+
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$2,
|
|
51311
50379
|
pageSize: merged.pageSize ?? DEFAULT_PAGE_SIZE,
|
|
51312
50380
|
maxLoad: merged.maxLoad ?? DEFAULT_MAX_LOAD,
|
|
51313
50381
|
onNotificationOpen: merged.onNotificationOpen,
|
|
@@ -51975,6 +51043,1419 @@ function provideValtechNotificationsRoutes(opts) {
|
|
|
51975
51043
|
];
|
|
51976
51044
|
}
|
|
51977
51045
|
|
|
51046
|
+
/**
|
|
51047
|
+
* Defaults i18n (es/en) embebidos en `val-notification-preferences-view`.
|
|
51048
|
+
* Auto-registrados en el constructor del componente si el consumer no proveyó
|
|
51049
|
+
* el namespace (`Settings.Notifications` por default). SOLO incluye las keys que
|
|
51050
|
+
* la vista usa.
|
|
51051
|
+
*
|
|
51052
|
+
* Portado del override `Settings.Notifications` (showcase) bajo el proceso de
|
|
51053
|
+
* ADR-021.
|
|
51054
|
+
*/
|
|
51055
|
+
const NOTIFICATION_PREFERENCES_VIEW_I18N = {
|
|
51056
|
+
es: {
|
|
51057
|
+
pageTitle: 'Notificaciones',
|
|
51058
|
+
pageDescription: 'Configura como quieres recibir notificaciones',
|
|
51059
|
+
// Control principal (UX simplificado)
|
|
51060
|
+
simpleTitle: 'Notificaciones push en este dispositivo',
|
|
51061
|
+
simpleDescription: 'Recibe alertas en tiempo real incluso con la app cerrada.',
|
|
51062
|
+
enabling: 'Activando…',
|
|
51063
|
+
disabling: 'Desactivando…',
|
|
51064
|
+
enabled: 'Activadas',
|
|
51065
|
+
errorGeneric: 'No pudimos completar la configuración. Verifica los permisos del navegador y reintenta.',
|
|
51066
|
+
retryCta: 'Reintentar',
|
|
51067
|
+
// Mensajes contextuales
|
|
51068
|
+
permissionUnsupported: 'Tu navegador no soporta notificaciones push.',
|
|
51069
|
+
permissionDeniedInfo: 'Tu navegador esta bloqueando las notificaciones. Para habilitarlas, ve a la configuracion del sitio y permitelas manualmente.',
|
|
51070
|
+
// iOS PWA install
|
|
51071
|
+
iosInstallTitle: 'En iOS, instala la app para recibir notificaciones',
|
|
51072
|
+
iosInstallHint: 'Las notificaciones push en iOS (iPhone/iPad) solo funcionan cuando la app está instalada en la pantalla de inicio. Edge, Chrome y Firefox en iOS comparten esta limitación.',
|
|
51073
|
+
iosInstallStep1: 'Abre este sitio en Safari (no en Chrome/Edge).',
|
|
51074
|
+
iosInstallStep2: 'Toca el botón Compartir (□ con flecha hacia arriba).',
|
|
51075
|
+
iosInstallStep3: "Elige 'Añadir a pantalla de inicio'.",
|
|
51076
|
+
iosInstallStep4: 'Abre la app desde el ícono recién creado y vuelve aquí.',
|
|
51077
|
+
// Detalles técnicos (expandible)
|
|
51078
|
+
detailsShow: 'Ver detalles técnicos',
|
|
51079
|
+
detailsHide: 'Ocultar detalles',
|
|
51080
|
+
detailsToken: 'Token',
|
|
51081
|
+
detailsNone: 'Sin registrar',
|
|
51082
|
+
diagnosticsPermission: 'Permiso del navegador',
|
|
51083
|
+
diagnosticsSupported: 'Soporte del navegador',
|
|
51084
|
+
// Toasts
|
|
51085
|
+
enableSuccess: 'Notificaciones habilitadas',
|
|
51086
|
+
disableSuccess: 'Notificaciones deshabilitadas',
|
|
51087
|
+
},
|
|
51088
|
+
en: {
|
|
51089
|
+
pageTitle: 'Notifications',
|
|
51090
|
+
pageDescription: 'Configure how you want to receive notifications',
|
|
51091
|
+
simpleTitle: 'Push notifications on this device',
|
|
51092
|
+
simpleDescription: 'Get real-time alerts even when the app is closed.',
|
|
51093
|
+
enabling: 'Enabling…',
|
|
51094
|
+
disabling: 'Disabling…',
|
|
51095
|
+
enabled: 'Enabled',
|
|
51096
|
+
errorGeneric: "We couldn't complete the setup. Check your browser permissions and retry.",
|
|
51097
|
+
retryCta: 'Retry',
|
|
51098
|
+
permissionUnsupported: 'Your browser does not support push notifications.',
|
|
51099
|
+
permissionDeniedInfo: 'Your browser is blocking notifications. To enable them, go to site settings and allow them manually.',
|
|
51100
|
+
iosInstallTitle: 'On iOS, install the app to receive notifications',
|
|
51101
|
+
iosInstallHint: 'Push notifications on iOS (iPhone/iPad) only work when the app is installed on the Home Screen. Edge, Chrome and Firefox on iOS share this limitation.',
|
|
51102
|
+
iosInstallStep1: 'Open this site in Safari (not Chrome/Edge).',
|
|
51103
|
+
iosInstallStep2: 'Tap the Share button (□ with an up arrow).',
|
|
51104
|
+
iosInstallStep3: "Choose 'Add to Home Screen'.",
|
|
51105
|
+
iosInstallStep4: 'Open the app from the new icon and come back here.',
|
|
51106
|
+
detailsShow: 'View technical details',
|
|
51107
|
+
detailsHide: 'Hide details',
|
|
51108
|
+
detailsToken: 'Token',
|
|
51109
|
+
detailsNone: 'Not registered',
|
|
51110
|
+
diagnosticsPermission: 'Browser permission',
|
|
51111
|
+
diagnosticsSupported: 'Browser support',
|
|
51112
|
+
enableSuccess: 'Notifications enabled',
|
|
51113
|
+
disableSuccess: 'Notifications disabled',
|
|
51114
|
+
},
|
|
51115
|
+
};
|
|
51116
|
+
|
|
51117
|
+
addIcons({
|
|
51118
|
+
alertCircleOutline,
|
|
51119
|
+
chevronDownOutline,
|
|
51120
|
+
chevronUpOutline,
|
|
51121
|
+
closeCircleOutline,
|
|
51122
|
+
notificationsOffOutline,
|
|
51123
|
+
notificationsOutline,
|
|
51124
|
+
refreshOutline,
|
|
51125
|
+
});
|
|
51126
|
+
const DEFAULT_NAMESPACE$1 = 'Settings.Notifications';
|
|
51127
|
+
/**
|
|
51128
|
+
* `val-notification-preferences-view` — **preferencias de notificaciones push**
|
|
51129
|
+
* (FCM) full-feature autocontenida (organism). Promovida desde `showcase` bajo el
|
|
51130
|
+
* proceso de ADR-021.
|
|
51131
|
+
*
|
|
51132
|
+
* Separada de `val-notifications-view` (el inbox / feed). Aquí el user **activa o
|
|
51133
|
+
* desactiva** las notificaciones push en este dispositivo.
|
|
51134
|
+
*
|
|
51135
|
+
* UX simplificado: UN solo toggle que ejecuta toda la secuencia (pedir permiso →
|
|
51136
|
+
* SW ready → getToken → registrar en backend → set master pref) vía
|
|
51137
|
+
* `MessagingService.enable()`. El registro del device en el backend va como
|
|
51138
|
+
* callback (la lib no depende de `AuthService` para evitar ciclos). Mensajes
|
|
51139
|
+
* contextuales y botón "Reintentar" aparecen solo cuando aplican.
|
|
51140
|
+
*
|
|
51141
|
+
* NO renderiza ion-content — vive dentro de val-page-wrapper. `ActivatedRoute` se
|
|
51142
|
+
* inyecta `{ optional: true }` → funciona sin router (embebida / Storybook).
|
|
51143
|
+
*/
|
|
51144
|
+
class NotificationPreferencesViewComponent {
|
|
51145
|
+
/** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
|
|
51146
|
+
get ns() {
|
|
51147
|
+
return this.resolvedConfig().i18nNamespace;
|
|
51148
|
+
}
|
|
51149
|
+
// ---------------------------------------------------------------------------
|
|
51150
|
+
// Lifecycle
|
|
51151
|
+
// ---------------------------------------------------------------------------
|
|
51152
|
+
constructor() {
|
|
51153
|
+
this.nav = inject(NavigationService);
|
|
51154
|
+
this.i18n = inject(I18nService);
|
|
51155
|
+
this.toast = inject(ToastService);
|
|
51156
|
+
this.auth = inject(AuthService);
|
|
51157
|
+
this.messaging = inject(MessagingService);
|
|
51158
|
+
this.pageRefresh = inject(PageRefreshService);
|
|
51159
|
+
this.prefs = inject(PreferencesService);
|
|
51160
|
+
// `ActivatedRoute` opcional → la vista funciona embebida / en Storybook sin
|
|
51161
|
+
// router (solo lo usa para leer el route data de config como fallback).
|
|
51162
|
+
this.route = inject(ActivatedRoute, { optional: true });
|
|
51163
|
+
this.resolvedConfig = computed(() => {
|
|
51164
|
+
const fromRoute = (this.route?.snapshot.data['notificationPreferencesConfig'] ??
|
|
51165
|
+
{});
|
|
51166
|
+
const merged = { ...fromRoute, ...(this.config ?? {}) };
|
|
51167
|
+
return {
|
|
51168
|
+
showDetails: merged.showDetails ?? true,
|
|
51169
|
+
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$1,
|
|
51170
|
+
onPushEnabled: merged.onPushEnabled,
|
|
51171
|
+
onPushDisabled: merged.onPushDisabled,
|
|
51172
|
+
};
|
|
51173
|
+
});
|
|
51174
|
+
/** Operación en curso (gobierna spinner del toggle + label "Activando…"). */
|
|
51175
|
+
this._busy = signal(null);
|
|
51176
|
+
/** True si el último intento falló — gobierna estado `error` + retry button. */
|
|
51177
|
+
this._lastFailed = signal(false);
|
|
51178
|
+
/** True mientras se re-obtiene el token FCM en background (no afecta la UI). */
|
|
51179
|
+
this._repairingToken = false;
|
|
51180
|
+
/** Toggle del expand de detalles técnicos. */
|
|
51181
|
+
this.showDetails = signal(false);
|
|
51182
|
+
this.permission = signal('default');
|
|
51183
|
+
this.isSupported = signal(false);
|
|
51184
|
+
this.currentToken = signal(null);
|
|
51185
|
+
this.needsIOSInstall = signal(this.detectNeedsIOSInstall());
|
|
51186
|
+
/** FormControl del toggle (sync-eado con `isEnabled` via effect). */
|
|
51187
|
+
this.toggleControl = new FormControl(false, {
|
|
51188
|
+
nonNullable: true,
|
|
51189
|
+
});
|
|
51190
|
+
/** Handler del listener `focus` — referencia guardada para poder removerlo. */
|
|
51191
|
+
this.onWindowFocus = () => this.scheduleBrowserRefresh();
|
|
51192
|
+
/** Timer del debounce del refresh disparado por `focus`. */
|
|
51193
|
+
this.focusDebounceTimer = null;
|
|
51194
|
+
/** Ventana de debounce (ms) para el refresh disparado por `focus`. */
|
|
51195
|
+
this.FOCUS_DEBOUNCE_MS = 500;
|
|
51196
|
+
// ---------------------------------------------------------------------------
|
|
51197
|
+
// Estado derivado
|
|
51198
|
+
// ---------------------------------------------------------------------------
|
|
51199
|
+
this.pageTitle = computed(() => this.tt('pageTitle'));
|
|
51200
|
+
this.pageDescription = computed(() => this.tt('pageDescription'));
|
|
51201
|
+
/**
|
|
51202
|
+
* Estado efectivo del toggle: "el usuario tiene push activo".
|
|
51203
|
+
*
|
|
51204
|
+
* Se deriva SOLO de la decisión del usuario (`master`) y del permiso del SO
|
|
51205
|
+
* (`permission`). El token FCM NO forma parte del estado de UI: vive en
|
|
51206
|
+
* memoria, se pierde en cada recarga/suspensión de la PWA y produciría flicker.
|
|
51207
|
+
* Si falta token con `master && granted`, es un detalle reparable en
|
|
51208
|
+
* background (ver `repairTokenIfNeeded`), no un cambio del toggle.
|
|
51209
|
+
*/
|
|
51210
|
+
this.isEnabled = computed(() => this.prefs.notificationsMaster() && this.permission() === 'granted');
|
|
51211
|
+
this.state = computed(() => {
|
|
51212
|
+
if (this.needsIOSInstall())
|
|
51213
|
+
return 'needs-install';
|
|
51214
|
+
if (this.permission() === 'unsupported' || !this.isSupported()) {
|
|
51215
|
+
// iOS standalone aún puede dar isSupported false temprano antes de init —
|
|
51216
|
+
// solo bloqueamos si el browser explícitamente no soporta APIs.
|
|
51217
|
+
if (this.permission() === 'unsupported')
|
|
51218
|
+
return 'unsupported';
|
|
51219
|
+
}
|
|
51220
|
+
const busy = this._busy();
|
|
51221
|
+
if (busy === 'enabling')
|
|
51222
|
+
return 'enabling';
|
|
51223
|
+
if (busy === 'disabling')
|
|
51224
|
+
return 'disabling';
|
|
51225
|
+
if (this.isEnabled())
|
|
51226
|
+
return 'enabled';
|
|
51227
|
+
if (this.permission() === 'denied')
|
|
51228
|
+
return 'blocked';
|
|
51229
|
+
if (this._lastFailed())
|
|
51230
|
+
return 'error';
|
|
51231
|
+
return 'idle';
|
|
51232
|
+
});
|
|
51233
|
+
this.busyLabel = computed(() => {
|
|
51234
|
+
const s = this.state();
|
|
51235
|
+
if (s === 'enabling')
|
|
51236
|
+
return this.tt('enabling');
|
|
51237
|
+
if (s === 'disabling')
|
|
51238
|
+
return this.tt('disabling');
|
|
51239
|
+
return null;
|
|
51240
|
+
});
|
|
51241
|
+
this.contextMessage = computed(() => {
|
|
51242
|
+
switch (this.state()) {
|
|
51243
|
+
case 'unsupported':
|
|
51244
|
+
return this.tt('permissionUnsupported');
|
|
51245
|
+
case 'blocked':
|
|
51246
|
+
return this.tt('permissionDeniedInfo');
|
|
51247
|
+
case 'error':
|
|
51248
|
+
return this.tt('errorGeneric');
|
|
51249
|
+
default:
|
|
51250
|
+
return null;
|
|
51251
|
+
}
|
|
51252
|
+
});
|
|
51253
|
+
this.alertVariant = computed(() => {
|
|
51254
|
+
const s = this.state();
|
|
51255
|
+
if (s === 'error' || s === 'blocked')
|
|
51256
|
+
return 'warning';
|
|
51257
|
+
if (s === 'unsupported')
|
|
51258
|
+
return 'danger';
|
|
51259
|
+
return 'info';
|
|
51260
|
+
});
|
|
51261
|
+
this.alertIcon = computed(() => {
|
|
51262
|
+
const s = this.state();
|
|
51263
|
+
if (s === 'unsupported')
|
|
51264
|
+
return 'close-circle-outline';
|
|
51265
|
+
return 'alert-circle-outline';
|
|
51266
|
+
});
|
|
51267
|
+
this.toggleProps = computed(() => ({
|
|
51268
|
+
control: this.toggleControl,
|
|
51269
|
+
color: 'primary',
|
|
51270
|
+
disabled: this._busy() !== null ||
|
|
51271
|
+
this.state() === 'unsupported' ||
|
|
51272
|
+
this.state() === 'needs-install' ||
|
|
51273
|
+
this.state() === 'blocked',
|
|
51274
|
+
}));
|
|
51275
|
+
this.retryButtonProps = computed(() => ({
|
|
51276
|
+
text: this.tt('retryCta'),
|
|
51277
|
+
color: 'dark',
|
|
51278
|
+
fill: 'outline',
|
|
51279
|
+
size: 'default',
|
|
51280
|
+
shape: 'round',
|
|
51281
|
+
type: 'button',
|
|
51282
|
+
icon: { name: 'refresh-outline', slot: 'start' },
|
|
51283
|
+
state: this._busy() !== null ? ComponentStates.WORKING : ComponentStates.ENABLED,
|
|
51284
|
+
}));
|
|
51285
|
+
this.tokenPreview = computed(() => {
|
|
51286
|
+
const t = this.currentToken();
|
|
51287
|
+
if (!t)
|
|
51288
|
+
return null;
|
|
51289
|
+
return `${t.slice(0, 22)}…${t.slice(-6)}`;
|
|
51290
|
+
});
|
|
51291
|
+
const ns = this.ns;
|
|
51292
|
+
if (!this.i18n.hasNamespace(ns)) {
|
|
51293
|
+
this.i18n.registerContent(ns, NOTIFICATION_PREFERENCES_VIEW_I18N);
|
|
51294
|
+
}
|
|
51295
|
+
this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
|
|
51296
|
+
void this.refreshBrowserState();
|
|
51297
|
+
// Re-aplicar header en cambio de idioma.
|
|
51298
|
+
effect(() => {
|
|
51299
|
+
this.i18n.lang();
|
|
51300
|
+
this.nav.setBackHeader('pageTitle', this.ns, { withMenu: true });
|
|
51301
|
+
}, { allowSignalWrites: true });
|
|
51302
|
+
// Sync FormControl ←→ estado efectivo. Cuando el estado cambia (cambio de
|
|
51303
|
+
// permission por el browser, o snapshot Firestore de `master`), el toggle se
|
|
51304
|
+
// re-alinea con la realidad.
|
|
51305
|
+
//
|
|
51306
|
+
// IMPORTANTE: se ignoran los cambios mientras hay una operación en curso
|
|
51307
|
+
// (`_busy() !== null`). Durante `onEnable`/`onDisable` las señales fuente se
|
|
51308
|
+
// actualizan en momentos distintos y producirían estados intermedios →
|
|
51309
|
+
// flicker visible. El toggle se re-alinea UNA sola vez en el `finally`.
|
|
51310
|
+
effect(() => {
|
|
51311
|
+
const eff = this.isEnabled();
|
|
51312
|
+
if (this._busy() !== null)
|
|
51313
|
+
return;
|
|
51314
|
+
if (this.toggleControl.value !== eff) {
|
|
51315
|
+
this.toggleControl.setValue(eff, { emitEvent: false });
|
|
51316
|
+
}
|
|
51317
|
+
}, { allowSignalWrites: true });
|
|
51318
|
+
// Interceptar click del user: el toggle ya cambió visualmente, ejecutar
|
|
51319
|
+
// la acción correspondiente.
|
|
51320
|
+
this.toggleControl.valueChanges.subscribe(v => {
|
|
51321
|
+
if (this._busy() !== null)
|
|
51322
|
+
return;
|
|
51323
|
+
if (v && !this.isEnabled()) {
|
|
51324
|
+
void this.onEnable();
|
|
51325
|
+
}
|
|
51326
|
+
else if (!v && this.isEnabled()) {
|
|
51327
|
+
void this.onDisable();
|
|
51328
|
+
}
|
|
51329
|
+
});
|
|
51330
|
+
// Detectar regreso a la pestaña — el user pudo haber cambiado permisos
|
|
51331
|
+
// desde la config del browser. El refresh va con debounce para no
|
|
51332
|
+
// dispararse en ráfagas (en PWA mobile `focus` salta con cada cambio de
|
|
51333
|
+
// app / aparición del teclado). El listener se remueve en ngOnDestroy.
|
|
51334
|
+
if (typeof window !== 'undefined') {
|
|
51335
|
+
window.addEventListener('focus', this.onWindowFocus);
|
|
51336
|
+
}
|
|
51337
|
+
}
|
|
51338
|
+
/**
|
|
51339
|
+
* Registra el pull-to-refresh estándar. Aunque esta vista no muestra una
|
|
51340
|
+
* lista remota, sí refleja estado mutable del browser (permiso, token,
|
|
51341
|
+
* soporte FCM) que puede cambiar fuera de la app — el refresh re-evalúa todo.
|
|
51342
|
+
*
|
|
51343
|
+
* Se usa `ngOnInit`/`ngOnDestroy` (NO `ionViewWillEnter`) porque
|
|
51344
|
+
* `val-page-wrapper` usa `<router-outlet>` plano — los hooks `ionView*` solo
|
|
51345
|
+
* disparan dentro de `<ion-router-outlet>`.
|
|
51346
|
+
*/
|
|
51347
|
+
ngOnInit() {
|
|
51348
|
+
this.pageRefresh.register(() => this.refreshBrowserState());
|
|
51349
|
+
}
|
|
51350
|
+
ngOnDestroy() {
|
|
51351
|
+
this.pageRefresh.unregister();
|
|
51352
|
+
if (typeof window !== 'undefined') {
|
|
51353
|
+
window.removeEventListener('focus', this.onWindowFocus);
|
|
51354
|
+
}
|
|
51355
|
+
if (this.focusDebounceTimer !== null) {
|
|
51356
|
+
clearTimeout(this.focusDebounceTimer);
|
|
51357
|
+
this.focusDebounceTimer = null;
|
|
51358
|
+
}
|
|
51359
|
+
}
|
|
51360
|
+
/** Refresca el estado del browser con debounce — evita ráfagas de `focus`. */
|
|
51361
|
+
scheduleBrowserRefresh() {
|
|
51362
|
+
if (this.focusDebounceTimer !== null) {
|
|
51363
|
+
clearTimeout(this.focusDebounceTimer);
|
|
51364
|
+
}
|
|
51365
|
+
this.focusDebounceTimer = setTimeout(() => {
|
|
51366
|
+
this.focusDebounceTimer = null;
|
|
51367
|
+
void this.refreshBrowserState();
|
|
51368
|
+
}, this.FOCUS_DEBOUNCE_MS);
|
|
51369
|
+
}
|
|
51370
|
+
// ---------------------------------------------------------------------------
|
|
51371
|
+
// Actions
|
|
51372
|
+
// ---------------------------------------------------------------------------
|
|
51373
|
+
/**
|
|
51374
|
+
* Secuencia completa de activación.
|
|
51375
|
+
*
|
|
51376
|
+
* Toda la mecánica (permission → SW ready → getToken con retry → watchdog de
|
|
51377
|
+
* auto-reload) vive en `MessagingService.enable()` de la lib. El paso de
|
|
51378
|
+
* registro del device en el backend se pasa como callback (`registerDevice`)
|
|
51379
|
+
* porque la lib no puede depender de `AuthService` sin crear un ciclo.
|
|
51380
|
+
*
|
|
51381
|
+
* `enable()` no hace throw: resuelve con un `EnablePushResult` descriptivo.
|
|
51382
|
+
*/
|
|
51383
|
+
async onEnable() {
|
|
51384
|
+
this._busy.set('enabling');
|
|
51385
|
+
this._lastFailed.set(false);
|
|
51386
|
+
try {
|
|
51387
|
+
const result = await this.messaging.enable({
|
|
51388
|
+
registerDevice: token => this.auth.registerDevice(token).then(r => r.registered),
|
|
51389
|
+
});
|
|
51390
|
+
if (result.status === 'enabled') {
|
|
51391
|
+
// Persistir flag master si aún no estaba activo.
|
|
51392
|
+
if (!this.prefs.notificationsMaster()) {
|
|
51393
|
+
await this.prefs.setNotificationsMaster(true);
|
|
51394
|
+
}
|
|
51395
|
+
this.toast.show({
|
|
51396
|
+
message: this.tt('enableSuccess'),
|
|
51397
|
+
duration: 2000,
|
|
51398
|
+
color: 'dark',
|
|
51399
|
+
position: 'top',
|
|
51400
|
+
});
|
|
51401
|
+
this.resolvedConfig().onPushEnabled?.();
|
|
51402
|
+
}
|
|
51403
|
+
else if (result.status === 'timeout' && result.reloaded) {
|
|
51404
|
+
// La lib disparó el auto-reload — la página está por recargarse.
|
|
51405
|
+
return;
|
|
51406
|
+
}
|
|
51407
|
+
else {
|
|
51408
|
+
// denied / unsupported / error / timeout sin reload → fallo recuperable.
|
|
51409
|
+
this._lastFailed.set(true);
|
|
51410
|
+
this.toast.show({
|
|
51411
|
+
message: this.tt('errorGeneric'),
|
|
51412
|
+
duration: 3000,
|
|
51413
|
+
color: 'dark',
|
|
51414
|
+
position: 'top',
|
|
51415
|
+
});
|
|
51416
|
+
}
|
|
51417
|
+
}
|
|
51418
|
+
catch {
|
|
51419
|
+
// `enable()` no hace throw, pero el callback de registro sí puede.
|
|
51420
|
+
this._lastFailed.set(true);
|
|
51421
|
+
this.toast.show({
|
|
51422
|
+
message: this.tt('errorGeneric'),
|
|
51423
|
+
duration: 3000,
|
|
51424
|
+
color: 'dark',
|
|
51425
|
+
position: 'top',
|
|
51426
|
+
});
|
|
51427
|
+
}
|
|
51428
|
+
finally {
|
|
51429
|
+
// Único refresh final: re-alinea el toggle UNA sola vez con el estado real.
|
|
51430
|
+
await this.refreshBrowserState();
|
|
51431
|
+
this._busy.set(null);
|
|
51432
|
+
}
|
|
51433
|
+
}
|
|
51434
|
+
async onDisable() {
|
|
51435
|
+
this._busy.set('disabling');
|
|
51436
|
+
this._lastFailed.set(false);
|
|
51437
|
+
try {
|
|
51438
|
+
await this.auth.disableNotifications();
|
|
51439
|
+
if (this.prefs.notificationsMaster()) {
|
|
51440
|
+
await this.prefs.setNotificationsMaster(false);
|
|
51441
|
+
}
|
|
51442
|
+
this.toast.show({
|
|
51443
|
+
message: this.tt('disableSuccess'),
|
|
51444
|
+
duration: 2000,
|
|
51445
|
+
color: 'dark',
|
|
51446
|
+
position: 'top',
|
|
51447
|
+
});
|
|
51448
|
+
this.resolvedConfig().onPushDisabled?.();
|
|
51449
|
+
}
|
|
51450
|
+
catch {
|
|
51451
|
+
this._lastFailed.set(true);
|
|
51452
|
+
}
|
|
51453
|
+
finally {
|
|
51454
|
+
await this.refreshBrowserState();
|
|
51455
|
+
this._busy.set(null);
|
|
51456
|
+
}
|
|
51457
|
+
}
|
|
51458
|
+
/** Reintentar la secuencia completa de activación, sin importar el estado actual. */
|
|
51459
|
+
async onRetry() {
|
|
51460
|
+
this._lastFailed.set(false);
|
|
51461
|
+
await this.onEnable();
|
|
51462
|
+
}
|
|
51463
|
+
toggleDetails() {
|
|
51464
|
+
this.showDetails.update(v => !v);
|
|
51465
|
+
}
|
|
51466
|
+
// ---------------------------------------------------------------------------
|
|
51467
|
+
// Privados
|
|
51468
|
+
// ---------------------------------------------------------------------------
|
|
51469
|
+
async refreshBrowserState() {
|
|
51470
|
+
this.isSupported.set(await this.messaging.isSupported());
|
|
51471
|
+
this.permission.set(this.auth.getNotificationPermissionState());
|
|
51472
|
+
this.currentToken.set(this.messaging.currentToken ?? null);
|
|
51473
|
+
void this.repairTokenIfNeeded();
|
|
51474
|
+
}
|
|
51475
|
+
/**
|
|
51476
|
+
* Repara el token FCM en background.
|
|
51477
|
+
*
|
|
51478
|
+
* Si el usuario tiene push activo (`master && granted`) pero no hay token en
|
|
51479
|
+
* memoria — caso típico tras un cold start de la PWA — re-obtenemos el token
|
|
51480
|
+
* y re-registramos el device sin tocar el toggle ni mostrar un error
|
|
51481
|
+
* alarmista. El token es un detalle de implementación reparable, no parte del
|
|
51482
|
+
* estado de UI (ver `isEnabled`).
|
|
51483
|
+
*/
|
|
51484
|
+
async repairTokenIfNeeded() {
|
|
51485
|
+
if (this._busy() !== null)
|
|
51486
|
+
return;
|
|
51487
|
+
if (this._repairingToken)
|
|
51488
|
+
return;
|
|
51489
|
+
if (!this.isEnabled())
|
|
51490
|
+
return;
|
|
51491
|
+
if (this.currentToken())
|
|
51492
|
+
return;
|
|
51493
|
+
this._repairingToken = true;
|
|
51494
|
+
try {
|
|
51495
|
+
const token = await this.messaging.getToken();
|
|
51496
|
+
if (token) {
|
|
51497
|
+
this.currentToken.set(token);
|
|
51498
|
+
await this.auth.registerDevice(token);
|
|
51499
|
+
}
|
|
51500
|
+
}
|
|
51501
|
+
catch {
|
|
51502
|
+
// Reparación best-effort: no degradamos el toggle ni mostramos error.
|
|
51503
|
+
}
|
|
51504
|
+
finally {
|
|
51505
|
+
this._repairingToken = false;
|
|
51506
|
+
}
|
|
51507
|
+
}
|
|
51508
|
+
/**
|
|
51509
|
+
* iOS browser sin estar instalado como PWA — push web no disponible.
|
|
51510
|
+
* Aplica a Safari y a Edge/Chrome/Firefox en iOS (todos WebKit por mandato Apple).
|
|
51511
|
+
*/
|
|
51512
|
+
detectNeedsIOSInstall() {
|
|
51513
|
+
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
51514
|
+
return false;
|
|
51515
|
+
}
|
|
51516
|
+
const ua = navigator.userAgent || '';
|
|
51517
|
+
const isIOS = /iPad|iPhone|iPod/.test(ua);
|
|
51518
|
+
if (!isIOS)
|
|
51519
|
+
return false;
|
|
51520
|
+
const isStandalone = window.matchMedia?.('(display-mode: standalone)').matches ||
|
|
51521
|
+
navigator.standalone === true;
|
|
51522
|
+
return !isStandalone;
|
|
51523
|
+
}
|
|
51524
|
+
tt(key) {
|
|
51525
|
+
return this.i18n.t(key, this.ns);
|
|
51526
|
+
}
|
|
51527
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NotificationPreferencesViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
51528
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: NotificationPreferencesViewComponent, isStandalone: true, selector: "val-notification-preferences-view", inputs: { config: "config" }, ngImport: i0, template: `
|
|
51529
|
+
<div class="page">
|
|
51530
|
+
<header class="page-header">
|
|
51531
|
+
<val-display [props]="{ size: 'small', color: 'dark', content: pageTitle() }" />
|
|
51532
|
+
<val-title
|
|
51533
|
+
[props]="{
|
|
51534
|
+
size: 'large',
|
|
51535
|
+
color: 'dark',
|
|
51536
|
+
bold: false,
|
|
51537
|
+
content: pageDescription(),
|
|
51538
|
+
}"
|
|
51539
|
+
/>
|
|
51540
|
+
</header>
|
|
51541
|
+
|
|
51542
|
+
@if (state() === 'needs-install') {
|
|
51543
|
+
<section class="settings-section">
|
|
51544
|
+
<val-title
|
|
51545
|
+
[props]="{
|
|
51546
|
+
size: 'medium',
|
|
51547
|
+
color: 'dark',
|
|
51548
|
+
bold: true,
|
|
51549
|
+
content: tt('iosInstallTitle'),
|
|
51550
|
+
}"
|
|
51551
|
+
/>
|
|
51552
|
+
<div class="section-body">
|
|
51553
|
+
<val-text
|
|
51554
|
+
[props]="{
|
|
51555
|
+
size: 'small',
|
|
51556
|
+
color: 'medium',
|
|
51557
|
+
bold: false,
|
|
51558
|
+
content: tt('iosInstallHint'),
|
|
51559
|
+
}"
|
|
51560
|
+
/>
|
|
51561
|
+
<ol class="install-steps">
|
|
51562
|
+
<li>{{ tt('iosInstallStep1') }}</li>
|
|
51563
|
+
<li>{{ tt('iosInstallStep2') }}</li>
|
|
51564
|
+
<li>{{ tt('iosInstallStep3') }}</li>
|
|
51565
|
+
<li>{{ tt('iosInstallStep4') }}</li>
|
|
51566
|
+
</ol>
|
|
51567
|
+
</div>
|
|
51568
|
+
</section>
|
|
51569
|
+
}
|
|
51570
|
+
|
|
51571
|
+
<section class="settings-section">
|
|
51572
|
+
<div class="notif-row">
|
|
51573
|
+
<div class="notif-row__icon">
|
|
51574
|
+
<ion-icon name="notifications-outline" aria-hidden="true" />
|
|
51575
|
+
</div>
|
|
51576
|
+
<div class="notif-row__body">
|
|
51577
|
+
<val-title
|
|
51578
|
+
[props]="{
|
|
51579
|
+
size: 'medium',
|
|
51580
|
+
color: 'dark',
|
|
51581
|
+
bold: true,
|
|
51582
|
+
content: tt('simpleTitle'),
|
|
51583
|
+
}"
|
|
51584
|
+
/>
|
|
51585
|
+
<val-text
|
|
51586
|
+
[props]="{
|
|
51587
|
+
size: 'small',
|
|
51588
|
+
color: 'medium',
|
|
51589
|
+
bold: false,
|
|
51590
|
+
content: tt('simpleDescription'),
|
|
51591
|
+
}"
|
|
51592
|
+
/>
|
|
51593
|
+
@if (busyLabel(); as label) {
|
|
51594
|
+
<span class="notif-row__busy">{{ label }}</span>
|
|
51595
|
+
}
|
|
51596
|
+
</div>
|
|
51597
|
+
<div class="notif-row__control">
|
|
51598
|
+
<val-toggle-input [props]="toggleProps()" />
|
|
51599
|
+
</div>
|
|
51600
|
+
</div>
|
|
51601
|
+
|
|
51602
|
+
@if (contextMessage(); as msg) {
|
|
51603
|
+
<div class="alert" [class]="'alert--' + alertVariant()">
|
|
51604
|
+
<ion-icon [name]="alertIcon()" aria-hidden="true" />
|
|
51605
|
+
<span>{{ msg }}</span>
|
|
51606
|
+
</div>
|
|
51607
|
+
}
|
|
51608
|
+
|
|
51609
|
+
@if (state() === 'error') {
|
|
51610
|
+
<div class="row-actions">
|
|
51611
|
+
<val-button [props]="retryButtonProps()" (click)="onRetry()" />
|
|
51612
|
+
</div>
|
|
51613
|
+
}
|
|
51614
|
+
</section>
|
|
51615
|
+
|
|
51616
|
+
@if (resolvedConfig().showDetails && state() !== 'needs-install' && state() !== 'unsupported') {
|
|
51617
|
+
<section class="settings-section">
|
|
51618
|
+
<button class="details-toggle" type="button" (click)="toggleDetails()">
|
|
51619
|
+
<span>{{ showDetails() ? tt('detailsHide') : tt('detailsShow') }}</span>
|
|
51620
|
+
<ion-icon [name]="showDetails() ? 'chevron-up-outline' : 'chevron-down-outline'" aria-hidden="true" />
|
|
51621
|
+
</button>
|
|
51622
|
+
@if (showDetails()) {
|
|
51623
|
+
<dl class="details">
|
|
51624
|
+
<div class="details__row">
|
|
51625
|
+
<dt>{{ tt('detailsToken') }}</dt>
|
|
51626
|
+
<dd>{{ tokenPreview() ?? tt('detailsNone') }}</dd>
|
|
51627
|
+
</div>
|
|
51628
|
+
<div class="details__row">
|
|
51629
|
+
<dt>{{ tt('diagnosticsPermission') }}</dt>
|
|
51630
|
+
<dd>{{ permission() }}</dd>
|
|
51631
|
+
</div>
|
|
51632
|
+
<div class="details__row">
|
|
51633
|
+
<dt>{{ tt('diagnosticsSupported') }}</dt>
|
|
51634
|
+
<dd>{{ isSupported() ? '✓' : '✗' }}</dd>
|
|
51635
|
+
</div>
|
|
51636
|
+
</dl>
|
|
51637
|
+
}
|
|
51638
|
+
</section>
|
|
51639
|
+
}
|
|
51640
|
+
</div>
|
|
51641
|
+
`, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:8px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-body{margin-top:16px;display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.notif-row{display:grid;grid-template-columns:44px 1fr auto;gap:16px;align-items:center}.notif-row__icon{width:44px;height:44px;border-radius:50%;background:var(--ion-color-light, rgba(0, 0, 0, .06));display:flex;align-items:center;justify-content:center}.notif-row__icon ion-icon{font-size:22px;color:var(--ion-color-dark, #1a1a1a)}.notif-row__body{display:flex;flex-direction:column;gap:4px;min-width:0}.notif-row__busy{font-size:12px;color:var(--ion-color-medium, #92949c);font-style:italic;margin-top:4px}.notif-row__control{flex-shrink:0}.alert{display:flex;gap:10px;align-items:flex-start;margin-top:12px;padding:12px 14px;border-radius:10px;font-size:13px;line-height:1.45;background:var(--ion-color-light, rgba(0, 0, 0, .04));color:var(--ion-color-dark, #1a1a1a)}.alert ion-icon{font-size:18px;flex-shrink:0;margin-top:1px}.alert--warning ion-icon{color:var(--ion-color-warning-shade, #c79e08)}.alert--danger ion-icon{color:var(--ion-color-danger-shade, #cf3c4f)}.alert--info ion-icon{color:var(--ion-color-medium, #6b6b6b)}.install-steps{margin:0;padding-left:20px;font-size:13px;line-height:1.6;color:var(--ion-color-dark, #1a1a1a)}.install-steps li+li{margin-top:4px}.details-toggle{display:flex;align-items:center;gap:6px;background:none;border:0;padding:0;font-size:13px;color:var(--ion-color-medium, #6b6b6b);cursor:pointer}.details-toggle ion-icon{font-size:14px}.details{margin:12px 0 0;display:flex;flex-direction:column;gap:6px}.details__row{display:grid;grid-template-columns:140px 1fr;gap:12px;font-size:12px;align-items:baseline}.details__row dt{margin:0;color:var(--ion-color-medium, #6b6b6b);text-transform:uppercase;letter-spacing:.4px;font-weight:500}.details__row dd{margin:0;color:var(--ion-color-dark, #1a1a1a);font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;word-break:break-all}\n"], dependencies: [{ kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: ToggleInputComponent, selector: "val-toggle-input", inputs: ["preset", "props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }] }); }
|
|
51642
|
+
}
|
|
51643
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NotificationPreferencesViewComponent, decorators: [{
|
|
51644
|
+
type: Component,
|
|
51645
|
+
args: [{ selector: 'val-notification-preferences-view', standalone: true, imports: [IonIcon, ToggleInputComponent, ButtonComponent, DisplayComponent, TitleComponent, TextComponent], template: `
|
|
51646
|
+
<div class="page">
|
|
51647
|
+
<header class="page-header">
|
|
51648
|
+
<val-display [props]="{ size: 'small', color: 'dark', content: pageTitle() }" />
|
|
51649
|
+
<val-title
|
|
51650
|
+
[props]="{
|
|
51651
|
+
size: 'large',
|
|
51652
|
+
color: 'dark',
|
|
51653
|
+
bold: false,
|
|
51654
|
+
content: pageDescription(),
|
|
51655
|
+
}"
|
|
51656
|
+
/>
|
|
51657
|
+
</header>
|
|
51658
|
+
|
|
51659
|
+
@if (state() === 'needs-install') {
|
|
51660
|
+
<section class="settings-section">
|
|
51661
|
+
<val-title
|
|
51662
|
+
[props]="{
|
|
51663
|
+
size: 'medium',
|
|
51664
|
+
color: 'dark',
|
|
51665
|
+
bold: true,
|
|
51666
|
+
content: tt('iosInstallTitle'),
|
|
51667
|
+
}"
|
|
51668
|
+
/>
|
|
51669
|
+
<div class="section-body">
|
|
51670
|
+
<val-text
|
|
51671
|
+
[props]="{
|
|
51672
|
+
size: 'small',
|
|
51673
|
+
color: 'medium',
|
|
51674
|
+
bold: false,
|
|
51675
|
+
content: tt('iosInstallHint'),
|
|
51676
|
+
}"
|
|
51677
|
+
/>
|
|
51678
|
+
<ol class="install-steps">
|
|
51679
|
+
<li>{{ tt('iosInstallStep1') }}</li>
|
|
51680
|
+
<li>{{ tt('iosInstallStep2') }}</li>
|
|
51681
|
+
<li>{{ tt('iosInstallStep3') }}</li>
|
|
51682
|
+
<li>{{ tt('iosInstallStep4') }}</li>
|
|
51683
|
+
</ol>
|
|
51684
|
+
</div>
|
|
51685
|
+
</section>
|
|
51686
|
+
}
|
|
51687
|
+
|
|
51688
|
+
<section class="settings-section">
|
|
51689
|
+
<div class="notif-row">
|
|
51690
|
+
<div class="notif-row__icon">
|
|
51691
|
+
<ion-icon name="notifications-outline" aria-hidden="true" />
|
|
51692
|
+
</div>
|
|
51693
|
+
<div class="notif-row__body">
|
|
51694
|
+
<val-title
|
|
51695
|
+
[props]="{
|
|
51696
|
+
size: 'medium',
|
|
51697
|
+
color: 'dark',
|
|
51698
|
+
bold: true,
|
|
51699
|
+
content: tt('simpleTitle'),
|
|
51700
|
+
}"
|
|
51701
|
+
/>
|
|
51702
|
+
<val-text
|
|
51703
|
+
[props]="{
|
|
51704
|
+
size: 'small',
|
|
51705
|
+
color: 'medium',
|
|
51706
|
+
bold: false,
|
|
51707
|
+
content: tt('simpleDescription'),
|
|
51708
|
+
}"
|
|
51709
|
+
/>
|
|
51710
|
+
@if (busyLabel(); as label) {
|
|
51711
|
+
<span class="notif-row__busy">{{ label }}</span>
|
|
51712
|
+
}
|
|
51713
|
+
</div>
|
|
51714
|
+
<div class="notif-row__control">
|
|
51715
|
+
<val-toggle-input [props]="toggleProps()" />
|
|
51716
|
+
</div>
|
|
51717
|
+
</div>
|
|
51718
|
+
|
|
51719
|
+
@if (contextMessage(); as msg) {
|
|
51720
|
+
<div class="alert" [class]="'alert--' + alertVariant()">
|
|
51721
|
+
<ion-icon [name]="alertIcon()" aria-hidden="true" />
|
|
51722
|
+
<span>{{ msg }}</span>
|
|
51723
|
+
</div>
|
|
51724
|
+
}
|
|
51725
|
+
|
|
51726
|
+
@if (state() === 'error') {
|
|
51727
|
+
<div class="row-actions">
|
|
51728
|
+
<val-button [props]="retryButtonProps()" (click)="onRetry()" />
|
|
51729
|
+
</div>
|
|
51730
|
+
}
|
|
51731
|
+
</section>
|
|
51732
|
+
|
|
51733
|
+
@if (resolvedConfig().showDetails && state() !== 'needs-install' && state() !== 'unsupported') {
|
|
51734
|
+
<section class="settings-section">
|
|
51735
|
+
<button class="details-toggle" type="button" (click)="toggleDetails()">
|
|
51736
|
+
<span>{{ showDetails() ? tt('detailsHide') : tt('detailsShow') }}</span>
|
|
51737
|
+
<ion-icon [name]="showDetails() ? 'chevron-up-outline' : 'chevron-down-outline'" aria-hidden="true" />
|
|
51738
|
+
</button>
|
|
51739
|
+
@if (showDetails()) {
|
|
51740
|
+
<dl class="details">
|
|
51741
|
+
<div class="details__row">
|
|
51742
|
+
<dt>{{ tt('detailsToken') }}</dt>
|
|
51743
|
+
<dd>{{ tokenPreview() ?? tt('detailsNone') }}</dd>
|
|
51744
|
+
</div>
|
|
51745
|
+
<div class="details__row">
|
|
51746
|
+
<dt>{{ tt('diagnosticsPermission') }}</dt>
|
|
51747
|
+
<dd>{{ permission() }}</dd>
|
|
51748
|
+
</div>
|
|
51749
|
+
<div class="details__row">
|
|
51750
|
+
<dt>{{ tt('diagnosticsSupported') }}</dt>
|
|
51751
|
+
<dd>{{ isSupported() ? '✓' : '✗' }}</dd>
|
|
51752
|
+
</div>
|
|
51753
|
+
</dl>
|
|
51754
|
+
}
|
|
51755
|
+
</section>
|
|
51756
|
+
}
|
|
51757
|
+
</div>
|
|
51758
|
+
`, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:8px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-body{margin-top:16px;display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.notif-row{display:grid;grid-template-columns:44px 1fr auto;gap:16px;align-items:center}.notif-row__icon{width:44px;height:44px;border-radius:50%;background:var(--ion-color-light, rgba(0, 0, 0, .06));display:flex;align-items:center;justify-content:center}.notif-row__icon ion-icon{font-size:22px;color:var(--ion-color-dark, #1a1a1a)}.notif-row__body{display:flex;flex-direction:column;gap:4px;min-width:0}.notif-row__busy{font-size:12px;color:var(--ion-color-medium, #92949c);font-style:italic;margin-top:4px}.notif-row__control{flex-shrink:0}.alert{display:flex;gap:10px;align-items:flex-start;margin-top:12px;padding:12px 14px;border-radius:10px;font-size:13px;line-height:1.45;background:var(--ion-color-light, rgba(0, 0, 0, .04));color:var(--ion-color-dark, #1a1a1a)}.alert ion-icon{font-size:18px;flex-shrink:0;margin-top:1px}.alert--warning ion-icon{color:var(--ion-color-warning-shade, #c79e08)}.alert--danger ion-icon{color:var(--ion-color-danger-shade, #cf3c4f)}.alert--info ion-icon{color:var(--ion-color-medium, #6b6b6b)}.install-steps{margin:0;padding-left:20px;font-size:13px;line-height:1.6;color:var(--ion-color-dark, #1a1a1a)}.install-steps li+li{margin-top:4px}.details-toggle{display:flex;align-items:center;gap:6px;background:none;border:0;padding:0;font-size:13px;color:var(--ion-color-medium, #6b6b6b);cursor:pointer}.details-toggle ion-icon{font-size:14px}.details{margin:12px 0 0;display:flex;flex-direction:column;gap:6px}.details__row{display:grid;grid-template-columns:140px 1fr;gap:12px;font-size:12px;align-items:baseline}.details__row dt{margin:0;color:var(--ion-color-medium, #6b6b6b);text-transform:uppercase;letter-spacing:.4px;font-weight:500}.details__row dd{margin:0;color:var(--ion-color-dark, #1a1a1a);font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;word-break:break-all}\n"] }]
|
|
51759
|
+
}], ctorParameters: () => [], propDecorators: { config: [{
|
|
51760
|
+
type: Input
|
|
51761
|
+
}] } });
|
|
51762
|
+
|
|
51763
|
+
/**
|
|
51764
|
+
* Helper para montar las **preferencias de notificaciones push**
|
|
51765
|
+
* (`val-notification-preferences-view`) como ruta en una app del factory. El
|
|
51766
|
+
* `config` se pasa por route `data` (`notificationPreferencesConfig`) y el
|
|
51767
|
+
* componente lo lee como fallback de su `@Input() config`.
|
|
51768
|
+
*
|
|
51769
|
+
* No confundir con `provideValtechNotificationsRoutes` (el inbox / feed).
|
|
51770
|
+
*
|
|
51771
|
+
* @example
|
|
51772
|
+
* ```ts
|
|
51773
|
+
* // app.routes.ts (típicamente bajo settings)
|
|
51774
|
+
* export const settingsRoutes: Routes = [
|
|
51775
|
+
* ...provideValtechNotificationPreferencesRoutes(), // → notifications
|
|
51776
|
+
* ];
|
|
51777
|
+
*
|
|
51778
|
+
* // con config acotada:
|
|
51779
|
+
* ...provideValtechNotificationPreferencesRoutes({ config: { showDetails: false } }),
|
|
51780
|
+
* ...provideValtechNotificationPreferencesRoutes({ path: 'push' }),
|
|
51781
|
+
* ```
|
|
51782
|
+
*/
|
|
51783
|
+
function provideValtechNotificationPreferencesRoutes(opts) {
|
|
51784
|
+
return [
|
|
51785
|
+
{
|
|
51786
|
+
path: opts?.path ?? 'notifications',
|
|
51787
|
+
component: NotificationPreferencesViewComponent,
|
|
51788
|
+
data: { notificationPreferencesConfig: opts?.config },
|
|
51789
|
+
},
|
|
51790
|
+
];
|
|
51791
|
+
}
|
|
51792
|
+
|
|
51793
|
+
/**
|
|
51794
|
+
* Consume el catálogo de permisos del backend (`GET /org/{orgId}/permissions-catalog`).
|
|
51795
|
+
* El backend es la fuente de verdad de qué permisos existen y de su alcance
|
|
51796
|
+
* (globales de plataforma vs. propios de una app). La respuesta incluye el
|
|
51797
|
+
* `appId` del request para que la vista distinga la app actual.
|
|
51798
|
+
*
|
|
51799
|
+
* Promovido desde `showcase` a la lib bajo el proceso de ADR-021. Usa
|
|
51800
|
+
* `VALTECH_AUTH_CONFIG.apiUrl` (como `OrgService`) en vez del `environment` de la
|
|
51801
|
+
* app — la lib no importa el environment del consumer.
|
|
51802
|
+
*/
|
|
51803
|
+
class PermissionCatalogService {
|
|
51804
|
+
constructor(config, http) {
|
|
51805
|
+
this.config = config;
|
|
51806
|
+
this.http = http;
|
|
51807
|
+
}
|
|
51808
|
+
getCatalog(orgId) {
|
|
51809
|
+
return this.http.get(`${this.config.apiUrl}/org/${orgId}/permissions-catalog`);
|
|
51810
|
+
}
|
|
51811
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionCatalogService, deps: [{ token: VALTECH_AUTH_CONFIG }, { token: i1$3.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
51812
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionCatalogService, providedIn: 'root' }); }
|
|
51813
|
+
}
|
|
51814
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionCatalogService, decorators: [{
|
|
51815
|
+
type: Injectable,
|
|
51816
|
+
args: [{ providedIn: 'root' }]
|
|
51817
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
51818
|
+
type: Inject,
|
|
51819
|
+
args: [VALTECH_AUTH_CONFIG]
|
|
51820
|
+
}] }, { type: i1$3.HttpClient }] });
|
|
51821
|
+
|
|
51822
|
+
/**
|
|
51823
|
+
* Defaults i18n (es/en) embebidos en `val-permissions-view`. Auto-registrados en
|
|
51824
|
+
* el constructor del componente si el consumer no proveyó el namespace
|
|
51825
|
+
* (`Settings.Permissions` por default). SOLO incluye las keys que la vista usa.
|
|
51826
|
+
*
|
|
51827
|
+
* Portado EXACTO de `PERMISSIONS_I18N` (showcase) bajo el proceso de ADR-021.
|
|
51828
|
+
*/
|
|
51829
|
+
const PERMISSIONS_VIEW_I18N = {
|
|
51830
|
+
es: {
|
|
51831
|
+
pageTitle: 'Permisos',
|
|
51832
|
+
pageDescription: 'Roles y permisos de esta app en la organización activa',
|
|
51833
|
+
systemRolesTitle: 'Roles del sistema',
|
|
51834
|
+
systemRolesHint: 'Predefinidos por la plataforma. No editables.',
|
|
51835
|
+
customRolesTitle: 'Roles personalizados',
|
|
51836
|
+
customRolesHint: 'Definidos por tu organización.',
|
|
51837
|
+
systemBadge: 'Sistema',
|
|
51838
|
+
noCustomRoles: 'Esta organización no tiene roles personalizados.',
|
|
51839
|
+
noPermissions: 'Sin permisos asignados.',
|
|
51840
|
+
// Grupos de alcance
|
|
51841
|
+
groupApp: 'Permisos de la app',
|
|
51842
|
+
groupPlatform: 'Permisos de plataforma',
|
|
51843
|
+
groupOther: 'Otras apps',
|
|
51844
|
+
// Roles del sistema
|
|
51845
|
+
roleViewer: 'Visor',
|
|
51846
|
+
roleEditor: 'Editor',
|
|
51847
|
+
roleAdmin: 'Administrador',
|
|
51848
|
+
roleSuperAdmin: 'Super Administrador',
|
|
51849
|
+
roleBingoAdmin: 'Bingo · Administrador',
|
|
51850
|
+
roleBingoTesorero: 'Bingo · Tesorero',
|
|
51851
|
+
roleBingoTaquilla: 'Bingo · Taquilla',
|
|
51852
|
+
roleBingoOperator: 'Bingo · Operador',
|
|
51853
|
+
// Apps
|
|
51854
|
+
app_showcase: 'Showcase',
|
|
51855
|
+
app_bingo: 'Bingo',
|
|
51856
|
+
// Recursos
|
|
51857
|
+
permUsers: 'Usuarios',
|
|
51858
|
+
permRoles: 'Roles',
|
|
51859
|
+
permRbac: 'Control de acceso',
|
|
51860
|
+
permApps: 'Apps',
|
|
51861
|
+
'permApi-keys': 'API keys',
|
|
51862
|
+
'permEmail-logs': 'Logs de correo',
|
|
51863
|
+
'permSms-logs': 'Logs de SMS',
|
|
51864
|
+
permDocuments: 'Documentos',
|
|
51865
|
+
permMedia: 'Media',
|
|
51866
|
+
permTemplates: 'Plantillas',
|
|
51867
|
+
permRequests: 'Solicitudes',
|
|
51868
|
+
permBingo: 'Bingo',
|
|
51869
|
+
// Acciones
|
|
51870
|
+
permRead: 'Leer',
|
|
51871
|
+
permWrite: 'Escribir',
|
|
51872
|
+
permCreate: 'Crear',
|
|
51873
|
+
permDelete: 'Eliminar',
|
|
51874
|
+
permManage: 'Gestionar',
|
|
51875
|
+
permEdit: 'Editar',
|
|
51876
|
+
permSell: 'Vender',
|
|
51877
|
+
permCheckin: 'Canjear',
|
|
51878
|
+
permSettle: 'Rendir',
|
|
51879
|
+
permExport: 'Exportar',
|
|
51880
|
+
permOperate: 'Operar',
|
|
51881
|
+
permAll: 'Todo',
|
|
51882
|
+
// Loading / error
|
|
51883
|
+
errorTitle: 'Error al cargar permisos',
|
|
51884
|
+
retry: 'Reintentar',
|
|
51885
|
+
},
|
|
51886
|
+
en: {
|
|
51887
|
+
pageTitle: 'Permissions',
|
|
51888
|
+
pageDescription: 'Roles and permissions of this app in the active organization',
|
|
51889
|
+
systemRolesTitle: 'System roles',
|
|
51890
|
+
systemRolesHint: 'Defined by the platform. Not editable.',
|
|
51891
|
+
customRolesTitle: 'Custom roles',
|
|
51892
|
+
customRolesHint: 'Defined by your organization.',
|
|
51893
|
+
systemBadge: 'System',
|
|
51894
|
+
noCustomRoles: 'This organization has no custom roles.',
|
|
51895
|
+
noPermissions: 'No permissions assigned.',
|
|
51896
|
+
groupApp: 'App permissions',
|
|
51897
|
+
groupPlatform: 'Platform permissions',
|
|
51898
|
+
groupOther: 'Other apps',
|
|
51899
|
+
roleViewer: 'Viewer',
|
|
51900
|
+
roleEditor: 'Editor',
|
|
51901
|
+
roleAdmin: 'Administrator',
|
|
51902
|
+
roleSuperAdmin: 'Super Administrator',
|
|
51903
|
+
roleBingoAdmin: 'Bingo · Administrator',
|
|
51904
|
+
roleBingoTesorero: 'Bingo · Treasurer',
|
|
51905
|
+
roleBingoTaquilla: 'Bingo · Box office',
|
|
51906
|
+
roleBingoOperator: 'Bingo · Operator',
|
|
51907
|
+
app_showcase: 'Showcase',
|
|
51908
|
+
app_bingo: 'Bingo',
|
|
51909
|
+
permUsers: 'Users',
|
|
51910
|
+
permRoles: 'Roles',
|
|
51911
|
+
permRbac: 'Access control',
|
|
51912
|
+
permApps: 'Apps',
|
|
51913
|
+
'permApi-keys': 'API keys',
|
|
51914
|
+
'permEmail-logs': 'Email logs',
|
|
51915
|
+
'permSms-logs': 'SMS logs',
|
|
51916
|
+
permDocuments: 'Documents',
|
|
51917
|
+
permMedia: 'Media',
|
|
51918
|
+
permTemplates: 'Templates',
|
|
51919
|
+
permRequests: 'Requests',
|
|
51920
|
+
permBingo: 'Bingo',
|
|
51921
|
+
permRead: 'Read',
|
|
51922
|
+
permWrite: 'Write',
|
|
51923
|
+
permCreate: 'Create',
|
|
51924
|
+
permDelete: 'Delete',
|
|
51925
|
+
permManage: 'Manage',
|
|
51926
|
+
permEdit: 'Edit',
|
|
51927
|
+
permSell: 'Sell',
|
|
51928
|
+
permCheckin: 'Check-in',
|
|
51929
|
+
permSettle: 'Settle',
|
|
51930
|
+
permExport: 'Export',
|
|
51931
|
+
permOperate: 'Operate',
|
|
51932
|
+
permAll: 'All',
|
|
51933
|
+
errorTitle: 'Error loading permissions',
|
|
51934
|
+
retry: 'Retry',
|
|
51935
|
+
},
|
|
51936
|
+
};
|
|
51937
|
+
|
|
51938
|
+
const DEFAULT_NAMESPACE = 'Settings.Permissions';
|
|
51939
|
+
/**
|
|
51940
|
+
* `val-permissions-view` — vista Permisos full-feature autocontenida (organism).
|
|
51941
|
+
* Visor RBAC de solo lectura: muestra los roles del sistema (Visor/Editor/Admin +
|
|
51942
|
+
* roles de plataforma) y los roles personalizados de la org activa, con el
|
|
51943
|
+
* catálogo de permisos agrupado por alcance (app actual / plataforma / otras apps).
|
|
51944
|
+
* Promovida desde `showcase` bajo el proceso de ADR-021.
|
|
51945
|
+
*
|
|
51946
|
+
* **RBAC interno** (como `organization-view`): lee la org activa vía
|
|
51947
|
+
* `AuthService.user().activeOrgId`. La vista NO muta nada — el control de quién
|
|
51948
|
+
* puede verla es responsabilidad del guard de ruta del consumer; no hay gating
|
|
51949
|
+
* fino interno ni se expone como config.
|
|
51950
|
+
*
|
|
51951
|
+
* Datos: `forkJoin` de `OrgService.listOrgRoles(orgId)` (roles de la org) +
|
|
51952
|
+
* `PermissionCatalogService.getCatalog(orgId)` (catálogo + appId actual). El
|
|
51953
|
+
* catálogo define el alcance (`scope`/`appId`) de cada recurso para agrupar.
|
|
51954
|
+
*
|
|
51955
|
+
* NO renderiza ion-content — vive dentro de val-page-wrapper. `ActivatedRoute` se
|
|
51956
|
+
* inyecta `{ optional: true }` solo para leer el route data de config. Auto-registra
|
|
51957
|
+
* sus defaults i18n (es/en) si el consumer no proveyó el namespace (default
|
|
51958
|
+
* `Settings.Permissions`).
|
|
51959
|
+
*/
|
|
51960
|
+
class PermissionsViewComponent {
|
|
51961
|
+
/** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
|
|
51962
|
+
get ns() {
|
|
51963
|
+
return this.resolvedConfig().i18nNamespace;
|
|
51964
|
+
}
|
|
51965
|
+
constructor() {
|
|
51966
|
+
this.i18n = inject(I18nService);
|
|
51967
|
+
this.orgService = inject(OrgService);
|
|
51968
|
+
this.catalog = inject(PermissionCatalogService);
|
|
51969
|
+
this.auth = inject(AuthService);
|
|
51970
|
+
this.nav = inject(NavigationService);
|
|
51971
|
+
this.route = inject(ActivatedRoute, { optional: true });
|
|
51972
|
+
this.resolvedConfig = computed(() => {
|
|
51973
|
+
const fromRoute = (this.route?.snapshot.data['permissionsConfig'] ?? {});
|
|
51974
|
+
const merged = { ...fromRoute, ...(this.config ?? {}) };
|
|
51975
|
+
return {
|
|
51976
|
+
showSystemRoles: merged.showSystemRoles ?? true,
|
|
51977
|
+
showCustomRoles: merged.showCustomRoles ?? true,
|
|
51978
|
+
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
|
|
51979
|
+
};
|
|
51980
|
+
});
|
|
51981
|
+
this.loading = signal(false);
|
|
51982
|
+
this.loadError = signal(null);
|
|
51983
|
+
this.roles = signal([]);
|
|
51984
|
+
this.systemRoles = computed(() => this.roles().filter(r => r.isSystem));
|
|
51985
|
+
this.customRoles = computed(() => this.roles().filter(r => !r.isSystem));
|
|
51986
|
+
this.activeOrgId = computed(() => {
|
|
51987
|
+
const u = this.auth.user();
|
|
51988
|
+
return u?.activeOrgId ?? u?.activeOrg ?? '';
|
|
51989
|
+
});
|
|
51990
|
+
this.retryButtonProps = computed(() => ({
|
|
51991
|
+
text: this.tt('retry'),
|
|
51992
|
+
color: 'dark',
|
|
51993
|
+
fill: 'outline',
|
|
51994
|
+
size: 'default',
|
|
51995
|
+
type: 'button',
|
|
51996
|
+
state: this.loading() ? ComponentStates.DISABLED : ComponentStates.ENABLED,
|
|
51997
|
+
}));
|
|
51998
|
+
const ns = this.ns;
|
|
51999
|
+
if (!this.i18n.hasNamespace(ns)) {
|
|
52000
|
+
this.i18n.registerContent(ns, PERMISSIONS_VIEW_I18N);
|
|
52001
|
+
}
|
|
52002
|
+
this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
|
|
52003
|
+
this.load();
|
|
52004
|
+
connectPageRefresh(() => this.load());
|
|
52005
|
+
}
|
|
52006
|
+
load() {
|
|
52007
|
+
const orgId = this.activeOrgId();
|
|
52008
|
+
if (!orgId)
|
|
52009
|
+
return;
|
|
52010
|
+
this.loading.set(true);
|
|
52011
|
+
this.loadError.set(null);
|
|
52012
|
+
forkJoin({
|
|
52013
|
+
roles: this.orgService.listOrgRoles(orgId),
|
|
52014
|
+
catalog: this.catalog.getCatalog(orgId),
|
|
52015
|
+
}).subscribe({
|
|
52016
|
+
next: ({ roles: rawRoles, catalog }) => {
|
|
52017
|
+
const byResource = new Map();
|
|
52018
|
+
for (const r of catalog.permissions)
|
|
52019
|
+
byResource.set(r.resource, r);
|
|
52020
|
+
const currentAppId = catalog.appId;
|
|
52021
|
+
this.roles.set((rawRoles ?? []).map((r) => this.buildRoleView({
|
|
52022
|
+
id: r.id,
|
|
52023
|
+
name: r.name,
|
|
52024
|
+
description: r.description,
|
|
52025
|
+
permissions: r.permissions ?? [],
|
|
52026
|
+
isSystem: r.isSystem ?? false,
|
|
52027
|
+
}, byResource, currentAppId)));
|
|
52028
|
+
this.loading.set(false);
|
|
52029
|
+
},
|
|
52030
|
+
error: err => {
|
|
52031
|
+
this.loadError.set(err);
|
|
52032
|
+
this.loading.set(false);
|
|
52033
|
+
},
|
|
52034
|
+
});
|
|
52035
|
+
}
|
|
52036
|
+
buildRoleView(role, byResource, currentAppId) {
|
|
52037
|
+
const appPerms = [];
|
|
52038
|
+
const platformPerms = [];
|
|
52039
|
+
const otherByApp = new Map();
|
|
52040
|
+
for (const perm of role.permissions) {
|
|
52041
|
+
const [resource, action] = perm.split(':');
|
|
52042
|
+
const label = this.permLabel(resource, action);
|
|
52043
|
+
const entry = byResource.get(resource);
|
|
52044
|
+
if (entry?.scope === 'app') {
|
|
52045
|
+
if (entry.appId === currentAppId) {
|
|
52046
|
+
appPerms.push(label);
|
|
52047
|
+
}
|
|
52048
|
+
else {
|
|
52049
|
+
const appId = entry.appId ?? '?';
|
|
52050
|
+
const arr = otherByApp.get(appId) ?? [];
|
|
52051
|
+
arr.push(label);
|
|
52052
|
+
otherByApp.set(appId, arr);
|
|
52053
|
+
}
|
|
52054
|
+
}
|
|
52055
|
+
else {
|
|
52056
|
+
// global o desconocido → plataforma
|
|
52057
|
+
platformPerms.push(label);
|
|
52058
|
+
}
|
|
52059
|
+
}
|
|
52060
|
+
const groups = [];
|
|
52061
|
+
if (appPerms.length) {
|
|
52062
|
+
groups.push({ key: 'app', label: this.tt('groupApp'), perms: appPerms });
|
|
52063
|
+
}
|
|
52064
|
+
if (platformPerms.length) {
|
|
52065
|
+
groups.push({
|
|
52066
|
+
key: 'platform',
|
|
52067
|
+
label: this.tt('groupPlatform'),
|
|
52068
|
+
perms: platformPerms,
|
|
52069
|
+
});
|
|
52070
|
+
}
|
|
52071
|
+
for (const [appId, perms] of otherByApp) {
|
|
52072
|
+
groups.push({
|
|
52073
|
+
key: `other:${appId}`,
|
|
52074
|
+
label: `${this.tt('groupOther')}: ${this.appLabel(appId)}`,
|
|
52075
|
+
perms,
|
|
52076
|
+
});
|
|
52077
|
+
}
|
|
52078
|
+
return {
|
|
52079
|
+
id: role.id,
|
|
52080
|
+
name: role.name,
|
|
52081
|
+
description: role.description,
|
|
52082
|
+
isSystem: role.isSystem,
|
|
52083
|
+
groups,
|
|
52084
|
+
hasPerms: groups.length > 0,
|
|
52085
|
+
};
|
|
52086
|
+
}
|
|
52087
|
+
tt(key) {
|
|
52088
|
+
return this.i18n.t(key, this.ns);
|
|
52089
|
+
}
|
|
52090
|
+
appLabel(appId) {
|
|
52091
|
+
const key = `app_${appId}`;
|
|
52092
|
+
const label = this.tt(key);
|
|
52093
|
+
return label === key ? appId : label;
|
|
52094
|
+
}
|
|
52095
|
+
roleLabel(name) {
|
|
52096
|
+
const map = {
|
|
52097
|
+
viewer: 'roleViewer',
|
|
52098
|
+
editor: 'roleEditor',
|
|
52099
|
+
admin: 'roleAdmin',
|
|
52100
|
+
super_admin: 'roleSuperAdmin',
|
|
52101
|
+
bingo_admin: 'roleBingoAdmin',
|
|
52102
|
+
bingo_tesorero: 'roleBingoTesorero',
|
|
52103
|
+
bingo_taquilla: 'roleBingoTaquilla',
|
|
52104
|
+
bingo_operator: 'roleBingoOperator',
|
|
52105
|
+
};
|
|
52106
|
+
return this.tt(map[name] ?? name);
|
|
52107
|
+
}
|
|
52108
|
+
permLabel(resource, action) {
|
|
52109
|
+
if (resource === '*' && action === '*')
|
|
52110
|
+
return this.tt('permAll');
|
|
52111
|
+
const resKey = resource === '*' ? 'permAll' : `perm${resource.charAt(0).toUpperCase()}${resource.slice(1)}`;
|
|
52112
|
+
const actKey = action === '*' ? 'permAll' : `perm${action.charAt(0).toUpperCase()}${action.slice(1)}`;
|
|
52113
|
+
const res = this.tt(resKey);
|
|
52114
|
+
const act = this.tt(actKey);
|
|
52115
|
+
return `${res}: ${act}`;
|
|
52116
|
+
}
|
|
52117
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
52118
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: PermissionsViewComponent, isStandalone: true, selector: "val-permissions-view", inputs: { config: "config" }, ngImport: i0, template: `
|
|
52119
|
+
<div class="page">
|
|
52120
|
+
<header class="page-header">
|
|
52121
|
+
<val-display [props]="{ size: 'small', color: 'dark', content: tt('pageTitle') }" />
|
|
52122
|
+
<val-title
|
|
52123
|
+
[props]="{
|
|
52124
|
+
size: 'large',
|
|
52125
|
+
color: 'dark',
|
|
52126
|
+
bold: false,
|
|
52127
|
+
content: tt('pageDescription'),
|
|
52128
|
+
}"
|
|
52129
|
+
/>
|
|
52130
|
+
</header>
|
|
52131
|
+
|
|
52132
|
+
@if (loading()) {
|
|
52133
|
+
<div class="spinner-row">
|
|
52134
|
+
<ion-spinner name="crescent" />
|
|
52135
|
+
</div>
|
|
52136
|
+
} @else if (loadError()) {
|
|
52137
|
+
<div class="error-row">
|
|
52138
|
+
<val-text
|
|
52139
|
+
[props]="{
|
|
52140
|
+
content: tt('errorTitle'),
|
|
52141
|
+
size: 'medium',
|
|
52142
|
+
color: 'medium',
|
|
52143
|
+
bold: false,
|
|
52144
|
+
}"
|
|
52145
|
+
/>
|
|
52146
|
+
<val-button [props]="retryButtonProps()" (click)="load()" />
|
|
52147
|
+
</div>
|
|
52148
|
+
} @else {
|
|
52149
|
+
@if (resolvedConfig().showSystemRoles) {
|
|
52150
|
+
<section class="perm-section">
|
|
52151
|
+
<div class="perm-section__header">
|
|
52152
|
+
<val-title
|
|
52153
|
+
[props]="{
|
|
52154
|
+
size: 'medium',
|
|
52155
|
+
color: 'dark',
|
|
52156
|
+
bold: true,
|
|
52157
|
+
content: tt('systemRolesTitle'),
|
|
52158
|
+
}"
|
|
52159
|
+
/>
|
|
52160
|
+
<val-text
|
|
52161
|
+
[props]="{
|
|
52162
|
+
content: tt('systemRolesHint'),
|
|
52163
|
+
size: 'medium',
|
|
52164
|
+
color: 'medium',
|
|
52165
|
+
bold: false,
|
|
52166
|
+
}"
|
|
52167
|
+
/>
|
|
52168
|
+
</div>
|
|
52169
|
+
<div class="role-list">
|
|
52170
|
+
@for (role of systemRoles(); track role.id) {
|
|
52171
|
+
<div class="role-card">
|
|
52172
|
+
<div class="role-card__header">
|
|
52173
|
+
<span class="role-card__name">{{ roleLabel(role.name) }}</span>
|
|
52174
|
+
<val-pill
|
|
52175
|
+
[props]="{
|
|
52176
|
+
label: tt('systemBadge'),
|
|
52177
|
+
color: 'primary',
|
|
52178
|
+
weight: 'bold',
|
|
52179
|
+
}"
|
|
52180
|
+
/>
|
|
52181
|
+
</div>
|
|
52182
|
+
@if (role.description) {
|
|
52183
|
+
<span class="role-card__desc">{{ role.description }}</span>
|
|
52184
|
+
}
|
|
52185
|
+
@if (!role.hasPerms) {
|
|
52186
|
+
<div class="perm-chips">
|
|
52187
|
+
<span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
|
|
52188
|
+
</div>
|
|
52189
|
+
} @else {
|
|
52190
|
+
@for (g of role.groups; track g.key) {
|
|
52191
|
+
<div class="perm-group">
|
|
52192
|
+
<span class="perm-group__label">{{ g.label }}</span>
|
|
52193
|
+
<div class="perm-chips">
|
|
52194
|
+
@for (p of g.perms; track p) {
|
|
52195
|
+
<span class="perm-chip">{{ p }}</span>
|
|
52196
|
+
}
|
|
52197
|
+
</div>
|
|
52198
|
+
</div>
|
|
52199
|
+
}
|
|
52200
|
+
}
|
|
52201
|
+
</div>
|
|
52202
|
+
}
|
|
52203
|
+
</div>
|
|
52204
|
+
</section>
|
|
52205
|
+
}
|
|
52206
|
+
|
|
52207
|
+
@if (resolvedConfig().showCustomRoles) {
|
|
52208
|
+
<section class="perm-section">
|
|
52209
|
+
<div class="perm-section__header">
|
|
52210
|
+
<val-title
|
|
52211
|
+
[props]="{
|
|
52212
|
+
size: 'medium',
|
|
52213
|
+
color: 'dark',
|
|
52214
|
+
bold: true,
|
|
52215
|
+
content: tt('customRolesTitle'),
|
|
52216
|
+
}"
|
|
52217
|
+
/>
|
|
52218
|
+
<val-text
|
|
52219
|
+
[props]="{
|
|
52220
|
+
content: tt('customRolesHint'),
|
|
52221
|
+
size: 'medium',
|
|
52222
|
+
color: 'medium',
|
|
52223
|
+
bold: false,
|
|
52224
|
+
}"
|
|
52225
|
+
/>
|
|
52226
|
+
</div>
|
|
52227
|
+
@if (customRoles().length === 0) {
|
|
52228
|
+
<val-text
|
|
52229
|
+
[props]="{
|
|
52230
|
+
content: tt('noCustomRoles'),
|
|
52231
|
+
size: 'medium',
|
|
52232
|
+
color: 'medium',
|
|
52233
|
+
bold: false,
|
|
52234
|
+
}"
|
|
52235
|
+
/>
|
|
52236
|
+
} @else {
|
|
52237
|
+
<div class="role-list">
|
|
52238
|
+
@for (role of customRoles(); track role.id) {
|
|
52239
|
+
<div class="role-card">
|
|
52240
|
+
<div class="role-card__header">
|
|
52241
|
+
<span class="role-card__name">{{ role.name }}</span>
|
|
52242
|
+
</div>
|
|
52243
|
+
@if (role.description) {
|
|
52244
|
+
<span class="role-card__desc">{{ role.description }}</span>
|
|
52245
|
+
}
|
|
52246
|
+
@if (!role.hasPerms) {
|
|
52247
|
+
<div class="perm-chips">
|
|
52248
|
+
<span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
|
|
52249
|
+
</div>
|
|
52250
|
+
} @else {
|
|
52251
|
+
@for (g of role.groups; track g.key) {
|
|
52252
|
+
<div class="perm-group">
|
|
52253
|
+
<span class="perm-group__label">{{ g.label }}</span>
|
|
52254
|
+
<div class="perm-chips">
|
|
52255
|
+
@for (p of g.perms; track p) {
|
|
52256
|
+
<span class="perm-chip">{{ p }}</span>
|
|
52257
|
+
}
|
|
52258
|
+
</div>
|
|
52259
|
+
</div>
|
|
52260
|
+
}
|
|
52261
|
+
}
|
|
52262
|
+
</div>
|
|
52263
|
+
}
|
|
52264
|
+
</div>
|
|
52265
|
+
}
|
|
52266
|
+
</section>
|
|
52267
|
+
}
|
|
52268
|
+
}
|
|
52269
|
+
</div>
|
|
52270
|
+
`, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:24px}.spinner-row,.error-row{display:flex;flex-direction:column;align-items:center;gap:16px;padding:32px 0}.perm-section{margin-bottom:32px}.perm-section__header{margin-bottom:12px}.role-list{display:flex;flex-direction:column;gap:10px}.role-card{border:1.5px solid var(--ion-color-light);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}.role-card__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.role-card__name{font-size:15px;font-weight:600;color:var(--ion-color-dark)}.role-card__desc{font-size:13px;color:var(--ion-color-medium);line-height:1.4}.perm-group{display:flex;flex-direction:column;gap:6px}.perm-group__label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--ion-color-medium)}.perm-chips{display:flex;flex-wrap:wrap;gap:6px}.perm-chip{font-size:12px;padding:3px 10px;border-radius:20px;background:var(--ion-color-light);color:var(--ion-color-dark)}.perm-chip--empty{color:var(--ion-color-medium);background:transparent;padding-left:0}\n"], dependencies: [{ kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: PillComponent, selector: "val-pill", inputs: ["preset", "props"], outputs: ["pillClick", "pillAction"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }] }); }
|
|
52271
|
+
}
|
|
52272
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsViewComponent, decorators: [{
|
|
52273
|
+
type: Component,
|
|
52274
|
+
args: [{ selector: 'val-permissions-view', standalone: true, imports: [IonSpinner, DisplayComponent, PillComponent, TitleComponent, TextComponent, ButtonComponent], template: `
|
|
52275
|
+
<div class="page">
|
|
52276
|
+
<header class="page-header">
|
|
52277
|
+
<val-display [props]="{ size: 'small', color: 'dark', content: tt('pageTitle') }" />
|
|
52278
|
+
<val-title
|
|
52279
|
+
[props]="{
|
|
52280
|
+
size: 'large',
|
|
52281
|
+
color: 'dark',
|
|
52282
|
+
bold: false,
|
|
52283
|
+
content: tt('pageDescription'),
|
|
52284
|
+
}"
|
|
52285
|
+
/>
|
|
52286
|
+
</header>
|
|
52287
|
+
|
|
52288
|
+
@if (loading()) {
|
|
52289
|
+
<div class="spinner-row">
|
|
52290
|
+
<ion-spinner name="crescent" />
|
|
52291
|
+
</div>
|
|
52292
|
+
} @else if (loadError()) {
|
|
52293
|
+
<div class="error-row">
|
|
52294
|
+
<val-text
|
|
52295
|
+
[props]="{
|
|
52296
|
+
content: tt('errorTitle'),
|
|
52297
|
+
size: 'medium',
|
|
52298
|
+
color: 'medium',
|
|
52299
|
+
bold: false,
|
|
52300
|
+
}"
|
|
52301
|
+
/>
|
|
52302
|
+
<val-button [props]="retryButtonProps()" (click)="load()" />
|
|
52303
|
+
</div>
|
|
52304
|
+
} @else {
|
|
52305
|
+
@if (resolvedConfig().showSystemRoles) {
|
|
52306
|
+
<section class="perm-section">
|
|
52307
|
+
<div class="perm-section__header">
|
|
52308
|
+
<val-title
|
|
52309
|
+
[props]="{
|
|
52310
|
+
size: 'medium',
|
|
52311
|
+
color: 'dark',
|
|
52312
|
+
bold: true,
|
|
52313
|
+
content: tt('systemRolesTitle'),
|
|
52314
|
+
}"
|
|
52315
|
+
/>
|
|
52316
|
+
<val-text
|
|
52317
|
+
[props]="{
|
|
52318
|
+
content: tt('systemRolesHint'),
|
|
52319
|
+
size: 'medium',
|
|
52320
|
+
color: 'medium',
|
|
52321
|
+
bold: false,
|
|
52322
|
+
}"
|
|
52323
|
+
/>
|
|
52324
|
+
</div>
|
|
52325
|
+
<div class="role-list">
|
|
52326
|
+
@for (role of systemRoles(); track role.id) {
|
|
52327
|
+
<div class="role-card">
|
|
52328
|
+
<div class="role-card__header">
|
|
52329
|
+
<span class="role-card__name">{{ roleLabel(role.name) }}</span>
|
|
52330
|
+
<val-pill
|
|
52331
|
+
[props]="{
|
|
52332
|
+
label: tt('systemBadge'),
|
|
52333
|
+
color: 'primary',
|
|
52334
|
+
weight: 'bold',
|
|
52335
|
+
}"
|
|
52336
|
+
/>
|
|
52337
|
+
</div>
|
|
52338
|
+
@if (role.description) {
|
|
52339
|
+
<span class="role-card__desc">{{ role.description }}</span>
|
|
52340
|
+
}
|
|
52341
|
+
@if (!role.hasPerms) {
|
|
52342
|
+
<div class="perm-chips">
|
|
52343
|
+
<span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
|
|
52344
|
+
</div>
|
|
52345
|
+
} @else {
|
|
52346
|
+
@for (g of role.groups; track g.key) {
|
|
52347
|
+
<div class="perm-group">
|
|
52348
|
+
<span class="perm-group__label">{{ g.label }}</span>
|
|
52349
|
+
<div class="perm-chips">
|
|
52350
|
+
@for (p of g.perms; track p) {
|
|
52351
|
+
<span class="perm-chip">{{ p }}</span>
|
|
52352
|
+
}
|
|
52353
|
+
</div>
|
|
52354
|
+
</div>
|
|
52355
|
+
}
|
|
52356
|
+
}
|
|
52357
|
+
</div>
|
|
52358
|
+
}
|
|
52359
|
+
</div>
|
|
52360
|
+
</section>
|
|
52361
|
+
}
|
|
52362
|
+
|
|
52363
|
+
@if (resolvedConfig().showCustomRoles) {
|
|
52364
|
+
<section class="perm-section">
|
|
52365
|
+
<div class="perm-section__header">
|
|
52366
|
+
<val-title
|
|
52367
|
+
[props]="{
|
|
52368
|
+
size: 'medium',
|
|
52369
|
+
color: 'dark',
|
|
52370
|
+
bold: true,
|
|
52371
|
+
content: tt('customRolesTitle'),
|
|
52372
|
+
}"
|
|
52373
|
+
/>
|
|
52374
|
+
<val-text
|
|
52375
|
+
[props]="{
|
|
52376
|
+
content: tt('customRolesHint'),
|
|
52377
|
+
size: 'medium',
|
|
52378
|
+
color: 'medium',
|
|
52379
|
+
bold: false,
|
|
52380
|
+
}"
|
|
52381
|
+
/>
|
|
52382
|
+
</div>
|
|
52383
|
+
@if (customRoles().length === 0) {
|
|
52384
|
+
<val-text
|
|
52385
|
+
[props]="{
|
|
52386
|
+
content: tt('noCustomRoles'),
|
|
52387
|
+
size: 'medium',
|
|
52388
|
+
color: 'medium',
|
|
52389
|
+
bold: false,
|
|
52390
|
+
}"
|
|
52391
|
+
/>
|
|
52392
|
+
} @else {
|
|
52393
|
+
<div class="role-list">
|
|
52394
|
+
@for (role of customRoles(); track role.id) {
|
|
52395
|
+
<div class="role-card">
|
|
52396
|
+
<div class="role-card__header">
|
|
52397
|
+
<span class="role-card__name">{{ role.name }}</span>
|
|
52398
|
+
</div>
|
|
52399
|
+
@if (role.description) {
|
|
52400
|
+
<span class="role-card__desc">{{ role.description }}</span>
|
|
52401
|
+
}
|
|
52402
|
+
@if (!role.hasPerms) {
|
|
52403
|
+
<div class="perm-chips">
|
|
52404
|
+
<span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
|
|
52405
|
+
</div>
|
|
52406
|
+
} @else {
|
|
52407
|
+
@for (g of role.groups; track g.key) {
|
|
52408
|
+
<div class="perm-group">
|
|
52409
|
+
<span class="perm-group__label">{{ g.label }}</span>
|
|
52410
|
+
<div class="perm-chips">
|
|
52411
|
+
@for (p of g.perms; track p) {
|
|
52412
|
+
<span class="perm-chip">{{ p }}</span>
|
|
52413
|
+
}
|
|
52414
|
+
</div>
|
|
52415
|
+
</div>
|
|
52416
|
+
}
|
|
52417
|
+
}
|
|
52418
|
+
</div>
|
|
52419
|
+
}
|
|
52420
|
+
</div>
|
|
52421
|
+
}
|
|
52422
|
+
</section>
|
|
52423
|
+
}
|
|
52424
|
+
}
|
|
52425
|
+
</div>
|
|
52426
|
+
`, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:24px}.spinner-row,.error-row{display:flex;flex-direction:column;align-items:center;gap:16px;padding:32px 0}.perm-section{margin-bottom:32px}.perm-section__header{margin-bottom:12px}.role-list{display:flex;flex-direction:column;gap:10px}.role-card{border:1.5px solid var(--ion-color-light);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}.role-card__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.role-card__name{font-size:15px;font-weight:600;color:var(--ion-color-dark)}.role-card__desc{font-size:13px;color:var(--ion-color-medium);line-height:1.4}.perm-group{display:flex;flex-direction:column;gap:6px}.perm-group__label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--ion-color-medium)}.perm-chips{display:flex;flex-wrap:wrap;gap:6px}.perm-chip{font-size:12px;padding:3px 10px;border-radius:20px;background:var(--ion-color-light);color:var(--ion-color-dark)}.perm-chip--empty{color:var(--ion-color-medium);background:transparent;padding-left:0}\n"] }]
|
|
52427
|
+
}], ctorParameters: () => [], propDecorators: { config: [{
|
|
52428
|
+
type: Input
|
|
52429
|
+
}] } });
|
|
52430
|
+
|
|
52431
|
+
/**
|
|
52432
|
+
* Helper para montar la vista Permisos (`val-permissions-view`) como ruta en una
|
|
52433
|
+
* app del factory. El `config` se pasa por route `data` (`permissionsConfig`) y el
|
|
52434
|
+
* componente lo lee como fallback de su `@Input() config`.
|
|
52435
|
+
*
|
|
52436
|
+
* @example
|
|
52437
|
+
* ```ts
|
|
52438
|
+
* // settings.routes.ts
|
|
52439
|
+
* export const settingsRoutes: Routes = [
|
|
52440
|
+
* ...provideValtechPermissionsRoutes(), // → /app/settings/permissions
|
|
52441
|
+
* { path: 'preferences', loadComponent: () => ... },
|
|
52442
|
+
* ];
|
|
52443
|
+
*
|
|
52444
|
+
* // con config acotada:
|
|
52445
|
+
* ...provideValtechPermissionsRoutes({ config: { showCustomRoles: false } }),
|
|
52446
|
+
* ...provideValtechPermissionsRoutes({ path: 'roles' }),
|
|
52447
|
+
* ```
|
|
52448
|
+
*/
|
|
52449
|
+
function provideValtechPermissionsRoutes(opts) {
|
|
52450
|
+
return [
|
|
52451
|
+
{
|
|
52452
|
+
path: opts?.path ?? 'permissions',
|
|
52453
|
+
component: PermissionsViewComponent,
|
|
52454
|
+
data: { permissionsConfig: opts?.config },
|
|
52455
|
+
},
|
|
52456
|
+
];
|
|
52457
|
+
}
|
|
52458
|
+
|
|
51978
52459
|
/** Built-in label sets for the three platform locales. */
|
|
51979
52460
|
const CALLOUT_LABELS = {
|
|
51980
52461
|
es: {
|
|
@@ -62181,5 +62662,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
62181
62662
|
* Generated bundle index. Do not edit.
|
|
62182
62663
|
*/
|
|
62183
62664
|
|
|
62184
|
-
export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, ApiKeyService, ApiKeysModalComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CONTENT_CARD_DEFAULTS, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentCardComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, CookieBannerComponent, CountdownComponent, CreateOrgModalComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DataTableComponent, DateInputComponent, DateRangeInputComponent, DebugConsoleComponent, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, EmailInputComponent, EmptyStateComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FontSizeOption, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HasPermissionDirective, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INFO_CARD_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoCardComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, InviteMemberModalComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MemberImportModalComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestService, RightsFooterComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SETTINGS_SECTIONS_CATALOG, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SearchHeaderComponent, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VERSION, ValtechErrorService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildSettingsCards, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideSplashScreen, provideValtechAccountRoutes, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor$1 as resolveColor, resolveInputDefaultValue, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
|
|
62665
|
+
export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CONTENT_CARD_DEFAULTS, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentCardComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, CookieBannerComponent, CountdownComponent, CreateOrgModalComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DataTableComponent, DateInputComponent, DateRangeInputComponent, DebugConsoleComponent, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, EmailInputComponent, EmptyStateComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FontSizeOption, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HasPermissionDirective, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INFO_CARD_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoCardComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, InviteMemberModalComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationPreferencesViewComponent, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PermissionCatalogService, PermissionsViewComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestService, RightsFooterComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SETTINGS_SECTIONS_CATALOG, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SearchHeaderComponent, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VERSION, ValtechErrorService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildSettingsCards, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideSplashScreen, provideValtechAccountRoutes, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor$1 as resolveColor, resolveInputDefaultValue, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
|
|
62185
62666
|
//# sourceMappingURL=valtech-components.mjs.map
|