valtech-components 2.0.903 → 2.0.905
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/molecules/content-reaction/content-reaction.component.mjs +4 -3
- package/esm2022/lib/services/auth/auth.service.mjs +27 -23
- package/esm2022/lib/services/auth/device.service.mjs +4 -4
- package/esm2022/lib/services/auth/handoff.service.mjs +3 -3
- package/esm2022/lib/services/auth/notification-action.service.mjs +3 -3
- package/esm2022/lib/services/auth/oauth.service.mjs +7 -7
- package/esm2022/lib/services/auth/session.service.mjs +2 -2
- package/esm2022/lib/services/auth/storage.service.mjs +7 -4
- package/esm2022/lib/services/auth/sync.service.mjs +10 -7
- package/esm2022/lib/services/feedback/feedback.service.mjs +17 -12
- package/esm2022/lib/version.mjs +2 -2
- package/fesm2022/valtech-components.mjs +72 -56
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/molecules/content-reaction/content-reaction.component.d.ts +1 -0
- package/lib/services/feedback/feedback.service.d.ts +2 -0
- package/lib/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, Injectable, makeEnvironmentProviders, APP_INITIALIZER, inject, EventEmitter, Component, Input, Output, input, computed, ChangeDetectionStrategy, HostBinding, PLATFORM_ID, Inject, HostListener, Pipe, effect, ViewChild, ChangeDetectorRef, ContentChild, ErrorHandler, DestroyRef, InjectionToken, Optional, runInInjectionContext, output, Injector, TemplateRef, ViewContainerRef, isSignal, Directive, ElementRef, ViewEncapsulation } from '@angular/core';
|
|
2
|
+
import { signal, Injectable, makeEnvironmentProviders, APP_INITIALIZER, inject, EventEmitter, Component, Input, Output, input, computed, ChangeDetectionStrategy, HostBinding, PLATFORM_ID, Inject, HostListener, Pipe, effect, ViewChild, ChangeDetectorRef, ContentChild, ErrorHandler, DestroyRef, InjectionToken, isDevMode, Optional, runInInjectionContext, output, Injector, TemplateRef, ViewContainerRef, isSignal, Directive, ElementRef, ViewEncapsulation } from '@angular/core';
|
|
3
3
|
import * as i2$1 from '@ionic/angular/standalone';
|
|
4
4
|
import { IonAvatar, IonCard, IonIcon, IonButton, IonSpinner, IonText, IonModal, IonHeader, IonToolbar, IonContent, IonButtons, IonTitle, IonProgressBar, IonSkeletonText, IonFab, IonFabButton, IonFabList, IonLabel, IonCardContent, IonCardHeader, IonCardTitle, IonCardSubtitle, IonCheckbox, IonTextarea, IonDatetime, IonDatetimeButton, IonInput, IonSelect, IonSelectOption, IonPopover, IonList, IonItem, IonRadioGroup, IonRadio, IonRange, IonSearchbar, IonSegment, IonSegmentButton, IonToggle, IonAccordion, IonAccordionGroup, IonTabBar, IonTabButton, IonBadge, IonBreadcrumb, IonBreadcrumbs, IonChip, IonNote, ToastController as ToastController$1, IonCol, IonRow, IonRefresher, IonRefresherContent, IonRippleEffect, AlertController, IonMenuButton, IonFooter, IonListHeader, IonInfiniteScroll, IonInfiniteScrollContent, IonGrid, MenuController, IonMenu, IonMenuToggle, IonSplitPane } from '@ionic/angular/standalone';
|
|
5
5
|
import * as i1 from '@angular/common';
|
|
@@ -53,7 +53,7 @@ import 'prismjs/components/prism-json';
|
|
|
53
53
|
* Current version of valtech-components.
|
|
54
54
|
* This is automatically updated during the publish process.
|
|
55
55
|
*/
|
|
56
|
-
const VERSION = '2.0.
|
|
56
|
+
const VERSION = '2.0.905';
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Servicio para gestionar presets de componentes.
|
|
@@ -21912,7 +21912,10 @@ class AuthStorageService {
|
|
|
21912
21912
|
constructor(config, platformId) {
|
|
21913
21913
|
this.config = config;
|
|
21914
21914
|
this.isBrowser = isPlatformBrowser(platformId);
|
|
21915
|
-
|
|
21915
|
+
if (!this.config && isDevMode()) {
|
|
21916
|
+
console.warn('[valtech-components] AuthStorageService: provideValtechAuth() no está en main.ts.');
|
|
21917
|
+
}
|
|
21918
|
+
const prefix = this.config?.storagePrefix || 'valtech_auth_';
|
|
21916
21919
|
this.keys = {
|
|
21917
21920
|
ACCESS_TOKEN: `${prefix}access_token`,
|
|
21918
21921
|
REFRESH_TOKEN: `${prefix}refresh_token`,
|
|
@@ -22049,7 +22052,7 @@ class AuthStorageService {
|
|
|
22049
22052
|
if (!this.isBrowser)
|
|
22050
22053
|
return;
|
|
22051
22054
|
try {
|
|
22052
|
-
Object.values(this.keys).forEach(
|
|
22055
|
+
Object.values(this.keys).forEach(key => localStorage.removeItem(key));
|
|
22053
22056
|
}
|
|
22054
22057
|
catch (e) {
|
|
22055
22058
|
console.warn('[ValtechAuth] Error limpiando storage:', e);
|
|
@@ -22092,7 +22095,10 @@ class AuthSyncService {
|
|
|
22092
22095
|
/** Observable de eventos de sincronización */
|
|
22093
22096
|
this.onEvent$ = this.eventSubject.asObservable();
|
|
22094
22097
|
this.isBrowser = isPlatformBrowser(platformId);
|
|
22095
|
-
|
|
22098
|
+
if (!this.config && isDevMode()) {
|
|
22099
|
+
console.warn('[valtech-components] AuthSyncService: provideValtechAuth() no está en main.ts.');
|
|
22100
|
+
}
|
|
22101
|
+
const prefix = this.config?.storagePrefix || 'valtech_auth_';
|
|
22096
22102
|
this.channelName = `${prefix}sync_channel`;
|
|
22097
22103
|
}
|
|
22098
22104
|
/**
|
|
@@ -22100,7 +22106,7 @@ class AuthSyncService {
|
|
|
22100
22106
|
* SSR-noop — sync de pestañas solo aplica en browser.
|
|
22101
22107
|
*/
|
|
22102
22108
|
start() {
|
|
22103
|
-
if (!this.config
|
|
22109
|
+
if (!this.config?.enableTabSync || !this.isBrowser) {
|
|
22104
22110
|
return;
|
|
22105
22111
|
}
|
|
22106
22112
|
// Intentar usar BroadcastChannel API (mejor rendimiento)
|
|
@@ -22129,7 +22135,7 @@ class AuthSyncService {
|
|
|
22129
22135
|
* Envía un evento a otras pestañas.
|
|
22130
22136
|
*/
|
|
22131
22137
|
broadcast(event) {
|
|
22132
|
-
if (!this.config
|
|
22138
|
+
if (!this.config?.enableTabSync || !this.isBrowser) {
|
|
22133
22139
|
return;
|
|
22134
22140
|
}
|
|
22135
22141
|
const fullEvent = {
|
|
@@ -22173,7 +22179,7 @@ class AuthSyncService {
|
|
|
22173
22179
|
* Inicializa fallback con storage events.
|
|
22174
22180
|
*/
|
|
22175
22181
|
initStorageEvents() {
|
|
22176
|
-
const storageKey = `${this.config
|
|
22182
|
+
const storageKey = `${this.config?.storagePrefix}sync_event`;
|
|
22177
22183
|
this.storageListener = (event) => {
|
|
22178
22184
|
if (event.key === storageKey && event.newValue) {
|
|
22179
22185
|
try {
|
|
@@ -22191,7 +22197,7 @@ class AuthSyncService {
|
|
|
22191
22197
|
* Envía evento via localStorage (fallback).
|
|
22192
22198
|
*/
|
|
22193
22199
|
broadcastViaStorage(event) {
|
|
22194
|
-
const storageKey = `${this.config
|
|
22200
|
+
const storageKey = `${this.config?.storagePrefix}sync_event`;
|
|
22195
22201
|
try {
|
|
22196
22202
|
// Escribir y luego limpiar para permitir múltiples eventos del mismo tipo
|
|
22197
22203
|
localStorage.setItem(storageKey, JSON.stringify(event));
|
|
@@ -25273,7 +25279,7 @@ class OAuthService {
|
|
|
25273
25279
|
return new Observable(observer => {
|
|
25274
25280
|
// Construir URL de inicio
|
|
25275
25281
|
const redirectUri = `${window.location.origin}/auth/oauth/callback`;
|
|
25276
|
-
const startUrl = `${this.config
|
|
25282
|
+
const startUrl = `${this.config?.apiUrl}/v2/auth/oauth/${provider}/start?redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
25277
25283
|
// Abrir popup centrado
|
|
25278
25284
|
const width = 500;
|
|
25279
25285
|
const height = 600;
|
|
@@ -25378,7 +25384,7 @@ class OAuthService {
|
|
|
25378
25384
|
startLinkFlow(provider) {
|
|
25379
25385
|
return new Observable(observer => {
|
|
25380
25386
|
const redirectUri = `${window.location.origin}/auth/oauth/callback`;
|
|
25381
|
-
const startUrl = `${this.config
|
|
25387
|
+
const startUrl = `${this.config?.apiUrl}/v2/auth/oauth/link/${provider}/start?redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
25382
25388
|
const width = 500;
|
|
25383
25389
|
const height = 600;
|
|
25384
25390
|
const left = window.screenX + (window.outerWidth - width) / 2;
|
|
@@ -25429,7 +25435,7 @@ class OAuthService {
|
|
|
25429
25435
|
*/
|
|
25430
25436
|
getLinkedProviders() {
|
|
25431
25437
|
return this.http
|
|
25432
|
-
.get(`${this.config
|
|
25438
|
+
.get(`${this.config?.apiUrl}/v2/auth/oauth/providers`)
|
|
25433
25439
|
.pipe(catchError(error => throwError(() => ({
|
|
25434
25440
|
code: error.error?.code || 'FETCH_ERROR',
|
|
25435
25441
|
message: error.error?.message || 'Error al obtener proveedores vinculados',
|
|
@@ -25440,7 +25446,7 @@ class OAuthService {
|
|
|
25440
25446
|
*/
|
|
25441
25447
|
unlinkProvider(provider) {
|
|
25442
25448
|
return this.http
|
|
25443
|
-
.post(`${this.config
|
|
25449
|
+
.post(`${this.config?.apiUrl}/v2/auth/oauth/unlink`, { provider })
|
|
25444
25450
|
.pipe(catchError(error => throwError(() => ({
|
|
25445
25451
|
code: error.error?.code || 'UNLINK_ERROR',
|
|
25446
25452
|
message: error.error?.message || 'Error al desvincular proveedor',
|
|
@@ -25451,7 +25457,7 @@ class OAuthService {
|
|
|
25451
25457
|
*/
|
|
25452
25458
|
setPassword(password) {
|
|
25453
25459
|
return this.http
|
|
25454
|
-
.post(`${this.config
|
|
25460
|
+
.post(`${this.config?.apiUrl}/v2/auth/oauth/set-password`, { password })
|
|
25455
25461
|
.pipe(catchError(error => throwError(() => ({
|
|
25456
25462
|
code: error.error?.code || 'SET_PASSWORD_ERROR',
|
|
25457
25463
|
message: error.error?.message || 'Error al establecer contraseña',
|
|
@@ -25462,7 +25468,7 @@ class OAuthService {
|
|
|
25462
25468
|
*/
|
|
25463
25469
|
hasPassword() {
|
|
25464
25470
|
return this.http
|
|
25465
|
-
.get(`${this.config
|
|
25471
|
+
.get(`${this.config?.apiUrl}/v2/auth/oauth/has-password`)
|
|
25466
25472
|
.pipe(catchError(error => throwError(() => ({
|
|
25467
25473
|
code: error.error?.code || 'CHECK_PASSWORD_ERROR',
|
|
25468
25474
|
message: error.error?.message || 'Error al verificar contraseña',
|
|
@@ -25800,6 +25806,10 @@ class AuthService {
|
|
|
25800
25806
|
this.mfaPending = this.stateService.mfaPending;
|
|
25801
25807
|
/** Error actual */
|
|
25802
25808
|
this.error = this.stateService.error;
|
|
25809
|
+
if (!this.config && isDevMode()) {
|
|
25810
|
+
console.warn("[valtech-components] AuthService: provideValtechAuth() no está en main.ts. " +
|
|
25811
|
+
"Login, registro y sesión no funcionarán.");
|
|
25812
|
+
}
|
|
25803
25813
|
}
|
|
25804
25814
|
// =============================================
|
|
25805
25815
|
// INICIALIZACIÓN
|
|
@@ -25873,7 +25883,7 @@ class AuthService {
|
|
|
25873
25883
|
}
|
|
25874
25884
|
}
|
|
25875
25885
|
// 5. Iniciar sincronización entre pestañas
|
|
25876
|
-
if (this.config
|
|
25886
|
+
if (this.config?.enableTabSync) {
|
|
25877
25887
|
this.syncService.start();
|
|
25878
25888
|
this.syncSubscription = this.syncService.onEvent$.subscribe(event => this.handleSyncEvent(event));
|
|
25879
25889
|
}
|
|
@@ -26106,7 +26116,7 @@ class AuthService {
|
|
|
26106
26116
|
this.stateService.updateRefreshToken(response.refreshToken); // NUEVO: guardar refresh rotado
|
|
26107
26117
|
this.storageService.saveAccessToken(response.accessToken, expiresAt);
|
|
26108
26118
|
this.storageService.saveRefreshToken(response.refreshToken); // NUEVO: persistir refresh rotado
|
|
26109
|
-
if (this.config
|
|
26119
|
+
if (this.config?.enableFirebaseIntegration && response.firebaseToken) {
|
|
26110
26120
|
this.signInWithFirebase(response.firebaseToken);
|
|
26111
26121
|
}
|
|
26112
26122
|
this.startRefreshTimer();
|
|
@@ -26192,7 +26202,7 @@ class AuthService {
|
|
|
26192
26202
|
this.signOutFirebase();
|
|
26193
26203
|
this.clearState();
|
|
26194
26204
|
this.syncService.broadcast({ type: 'LOGOUT' });
|
|
26195
|
-
this.router.navigate([this.config
|
|
26205
|
+
this.router.navigate([this.config?.loginRoute]);
|
|
26196
26206
|
}
|
|
26197
26207
|
// =============================================
|
|
26198
26208
|
// MFA SETUP (usuario autenticado)
|
|
@@ -26407,7 +26417,7 @@ class AuthService {
|
|
|
26407
26417
|
this.signOutFirebase();
|
|
26408
26418
|
this.clearState();
|
|
26409
26419
|
this.syncService.broadcast({ type: 'LOGOUT' });
|
|
26410
|
-
this.router.navigate([this.config
|
|
26420
|
+
this.router.navigate([this.config?.loginRoute]);
|
|
26411
26421
|
}
|
|
26412
26422
|
}), catchError(error => this.handleAuthError(error)));
|
|
26413
26423
|
}
|
|
@@ -26484,11 +26494,11 @@ class AuthService {
|
|
|
26484
26494
|
// PRIVATE METHODS
|
|
26485
26495
|
// =============================================
|
|
26486
26496
|
get baseUrl() {
|
|
26487
|
-
return `${this.config
|
|
26497
|
+
return `${this.config?.apiUrl}${this.config?.authPrefix}`;
|
|
26488
26498
|
}
|
|
26489
26499
|
/** Base for /v2/users endpoints (separate from /v2/auth). */
|
|
26490
26500
|
get usersBaseUrl() {
|
|
26491
|
-
return `${this.config
|
|
26501
|
+
return `${this.config?.apiUrl}/v2/users`;
|
|
26492
26502
|
}
|
|
26493
26503
|
handleSuccessfulAuth(response) {
|
|
26494
26504
|
const expiresAt = Date.now() + response.expiresIn * 1000;
|
|
@@ -26521,18 +26531,18 @@ class AuthService {
|
|
|
26521
26531
|
this.syncService.broadcast({ type: 'LOGIN' });
|
|
26522
26532
|
// Integración con Firebase
|
|
26523
26533
|
console.log('[ValtechAuth] handleSuccessfulAuth - Firebase check:', {
|
|
26524
|
-
enableFirebaseIntegration: this.config
|
|
26534
|
+
enableFirebaseIntegration: this.config?.enableFirebaseIntegration,
|
|
26525
26535
|
hasFirebaseTokenKey: 'firebaseToken' in response,
|
|
26526
26536
|
firebaseTokenValue: !!response.firebaseToken,
|
|
26527
26537
|
firebaseTokenLength: response.firebaseToken?.length || 0,
|
|
26528
26538
|
});
|
|
26529
|
-
if (this.config
|
|
26539
|
+
if (this.config?.enableFirebaseIntegration &&
|
|
26530
26540
|
'firebaseToken' in response &&
|
|
26531
26541
|
response.firebaseToken) {
|
|
26532
26542
|
console.log('[ValtechAuth] Calling signInWithFirebase with token length:', response.firebaseToken.length);
|
|
26533
26543
|
this.signInWithFirebase(response.firebaseToken);
|
|
26534
26544
|
}
|
|
26535
|
-
else if (this.config
|
|
26545
|
+
else if (this.config?.enableFirebaseIntegration) {
|
|
26536
26546
|
// El login NO trajo firebaseToken (ej. flujo OAuth que lo perdió). En vez
|
|
26537
26547
|
// de quedar sin sesión de Firebase, recurrimos al fallback self-healing:
|
|
26538
26548
|
// /refresh sí devuelve un firebaseToken confiable.
|
|
@@ -26543,7 +26553,7 @@ class AuthService {
|
|
|
26543
26553
|
console.log('[ValtechAuth] Firebase signin skipped - integración Firebase desactivada');
|
|
26544
26554
|
}
|
|
26545
26555
|
// Registro automático de dispositivo para push notifications
|
|
26546
|
-
if (this.config
|
|
26556
|
+
if (this.config?.enableDeviceRegistration) {
|
|
26547
26557
|
this.registerDeviceIfNeeded(); // fire-and-forget
|
|
26548
26558
|
}
|
|
26549
26559
|
// Sincronizar idioma del usuario con i18n
|
|
@@ -26595,7 +26605,7 @@ class AuthService {
|
|
|
26595
26605
|
const state = this.stateService.state();
|
|
26596
26606
|
if (!state.expiresAt)
|
|
26597
26607
|
return;
|
|
26598
|
-
const refreshBeforeMs = (this.config
|
|
26608
|
+
const refreshBeforeMs = (this.config?.refreshBeforeExpiry || 60) * 1000;
|
|
26599
26609
|
const refreshAt = state.expiresAt - refreshBeforeMs;
|
|
26600
26610
|
const delay = refreshAt - Date.now();
|
|
26601
26611
|
if (delay > 0) {
|
|
@@ -26640,7 +26650,7 @@ class AuthService {
|
|
|
26640
26650
|
case 'LOGOUT':
|
|
26641
26651
|
this.stateService.reset();
|
|
26642
26652
|
this.stopRefreshTimer();
|
|
26643
|
-
this.router.navigate([this.config
|
|
26653
|
+
this.router.navigate([this.config?.loginRoute]);
|
|
26644
26654
|
break;
|
|
26645
26655
|
case 'PERMISSIONS_UPDATE': {
|
|
26646
26656
|
const perms = this.storageService.loadPermissions();
|
|
@@ -26686,7 +26696,7 @@ class AuthService {
|
|
|
26686
26696
|
}
|
|
26687
26697
|
}
|
|
26688
26698
|
async signOutFirebase() {
|
|
26689
|
-
if (!this.config
|
|
26699
|
+
if (!this.config?.enableFirebaseIntegration)
|
|
26690
26700
|
return;
|
|
26691
26701
|
try {
|
|
26692
26702
|
if (this.firebaseService) {
|
|
@@ -26717,7 +26727,7 @@ class AuthService {
|
|
|
26717
26727
|
* Fire-and-forget: se invoca con `void` para no demorar `initialize()`.
|
|
26718
26728
|
*/
|
|
26719
26729
|
async ensureFirebaseSessionOnBootstrap() {
|
|
26720
|
-
if (!this.config
|
|
26730
|
+
if (!this.config?.enableFirebaseIntegration || !this.firebaseService)
|
|
26721
26731
|
return;
|
|
26722
26732
|
// Dar a la persistencia IndexedDB propia de Firebase Auth una oportunidad
|
|
26723
26733
|
// de auto-restaurarse (navegador normal). En una PWA standalone de iOS
|
|
@@ -26743,7 +26753,7 @@ class AuthService {
|
|
|
26743
26753
|
* desde el debug-console qué disparó el fallback.
|
|
26744
26754
|
*/
|
|
26745
26755
|
async reestablishFirebaseViaRefresh(reason) {
|
|
26746
|
-
if (!this.config
|
|
26756
|
+
if (!this.config?.enableFirebaseIntegration || !this.firebaseService)
|
|
26747
26757
|
return;
|
|
26748
26758
|
console.log(`[FBAuth] fallback — re-estableciendo Firebase vía /refresh (motivo: ${reason})`);
|
|
26749
26759
|
try {
|
|
@@ -26816,7 +26826,7 @@ class AuthService {
|
|
|
26816
26826
|
if (token) {
|
|
26817
26827
|
// Eliminar del backend
|
|
26818
26828
|
await firstValueFrom(this.http
|
|
26819
|
-
.request('DELETE', `${this.config
|
|
26829
|
+
.request('DELETE', `${this.config?.apiUrl}/v2/users/me/devices/by-token`, {
|
|
26820
26830
|
body: { token },
|
|
26821
26831
|
})
|
|
26822
26832
|
.pipe(catchError(() => of(null))));
|
|
@@ -26886,7 +26896,7 @@ class AuthService {
|
|
|
26886
26896
|
}
|
|
26887
26897
|
// Registrar en backend
|
|
26888
26898
|
const { platform, browser, os } = this.detectPlatformInfo();
|
|
26889
|
-
const response = await firstValueFrom(this.http.post(`${this.config
|
|
26899
|
+
const response = await firstValueFrom(this.http.post(`${this.config?.apiUrl}/v2/users/me/devices`, {
|
|
26890
26900
|
token,
|
|
26891
26901
|
platform,
|
|
26892
26902
|
browser,
|
|
@@ -26909,7 +26919,7 @@ class AuthService {
|
|
|
26909
26919
|
*/
|
|
26910
26920
|
async registerDeviceIfNeeded() {
|
|
26911
26921
|
// Solo si está habilitado y messaging está disponible
|
|
26912
|
-
if (!this.config
|
|
26922
|
+
if (!this.config?.enableDeviceRegistration || !this.messagingService) {
|
|
26913
26923
|
return false;
|
|
26914
26924
|
}
|
|
26915
26925
|
try {
|
|
@@ -26929,7 +26939,7 @@ class AuthService {
|
|
|
26929
26939
|
}
|
|
26930
26940
|
// Registrar en backend
|
|
26931
26941
|
const { platform, browser, os } = this.detectPlatformInfo();
|
|
26932
|
-
await firstValueFrom(this.http.post(`${this.config
|
|
26942
|
+
await firstValueFrom(this.http.post(`${this.config?.apiUrl}/v2/users/me/devices`, {
|
|
26933
26943
|
token,
|
|
26934
26944
|
platform,
|
|
26935
26945
|
browser,
|
|
@@ -26948,7 +26958,7 @@ class AuthService {
|
|
|
26948
26958
|
* Elimina el dispositivo del backend y borra el token FCM.
|
|
26949
26959
|
*/
|
|
26950
26960
|
async unregisterDevice() {
|
|
26951
|
-
if (!this.config
|
|
26961
|
+
if (!this.config?.enableDeviceRegistration || !this.messagingService) {
|
|
26952
26962
|
return;
|
|
26953
26963
|
}
|
|
26954
26964
|
try {
|
|
@@ -26956,7 +26966,7 @@ class AuthService {
|
|
|
26956
26966
|
if (token) {
|
|
26957
26967
|
// Delete from backend (fire and forget)
|
|
26958
26968
|
this.http
|
|
26959
|
-
.request('DELETE', `${this.config
|
|
26969
|
+
.request('DELETE', `${this.config?.apiUrl}/v2/users/me/devices/by-token`, {
|
|
26960
26970
|
body: { token },
|
|
26961
26971
|
})
|
|
26962
26972
|
.pipe(catchError(() => of(null)))
|
|
@@ -28123,17 +28133,22 @@ function provideValtechFeedback(config) {
|
|
|
28123
28133
|
*/
|
|
28124
28134
|
class FeedbackService {
|
|
28125
28135
|
constructor() {
|
|
28126
|
-
this.config = inject(VALTECH_FEEDBACK_CONFIG);
|
|
28136
|
+
this.config = inject(VALTECH_FEEDBACK_CONFIG, { optional: true });
|
|
28127
28137
|
this.http = inject(HttpClient);
|
|
28128
28138
|
this.firestore = inject(FirestoreService, { optional: true });
|
|
28129
28139
|
this.storage = inject(StorageService, { optional: true });
|
|
28130
28140
|
this.auth = inject(AuthService, { optional: true });
|
|
28141
|
+
this.isConfigured = signal(!!this.config);
|
|
28142
|
+
if (!this.config && isDevMode()) {
|
|
28143
|
+
console.warn('[valtech-components] FeedbackService: provideValtechFeedback() no está en main.ts. ' +
|
|
28144
|
+
'Las reacciones y el feedback no funcionarán.');
|
|
28145
|
+
}
|
|
28131
28146
|
}
|
|
28132
28147
|
/**
|
|
28133
28148
|
* URL base para endpoints de feedback.
|
|
28134
28149
|
*/
|
|
28135
28150
|
get baseUrl() {
|
|
28136
|
-
return `${this.config
|
|
28151
|
+
return `${this.config?.apiUrl}${this.config?.feedbackPrefix}`;
|
|
28137
28152
|
}
|
|
28138
28153
|
/**
|
|
28139
28154
|
* Captura el contexto del dispositivo automáticamente.
|
|
@@ -28167,7 +28182,7 @@ class FeedbackService {
|
|
|
28167
28182
|
attachments,
|
|
28168
28183
|
contentRef,
|
|
28169
28184
|
deviceContext: this.captureDeviceContext(),
|
|
28170
|
-
appId: this.config
|
|
28185
|
+
appId: this.config?.appId,
|
|
28171
28186
|
};
|
|
28172
28187
|
return this.http.post(this.baseUrl, request);
|
|
28173
28188
|
}
|
|
@@ -28197,15 +28212,15 @@ class FeedbackService {
|
|
|
28197
28212
|
*/
|
|
28198
28213
|
validateFile(file) {
|
|
28199
28214
|
// Verificar tamaño
|
|
28200
|
-
if (file.size > this.config
|
|
28201
|
-
const maxSizeMB = Math.round(this.config
|
|
28215
|
+
if (file.size > this.config?.maxFileSize) {
|
|
28216
|
+
const maxSizeMB = Math.round(this.config?.maxFileSize / (1024 * 1024));
|
|
28202
28217
|
return {
|
|
28203
28218
|
valid: false,
|
|
28204
28219
|
error: `El archivo excede el tamaño máximo de ${maxSizeMB}MB`,
|
|
28205
28220
|
};
|
|
28206
28221
|
}
|
|
28207
28222
|
// Verificar tipo
|
|
28208
|
-
const allowedTypes = this.config
|
|
28223
|
+
const allowedTypes = this.config?.allowedFileTypes || [];
|
|
28209
28224
|
const isAllowed = allowedTypes.some(pattern => {
|
|
28210
28225
|
if (pattern.endsWith('/*')) {
|
|
28211
28226
|
const baseType = pattern.replace('/*', '');
|
|
@@ -28367,7 +28382,7 @@ class FeedbackService {
|
|
|
28367
28382
|
}
|
|
28368
28383
|
// 2. Fallback: llamar API (solo si hay usuario autenticado)
|
|
28369
28384
|
const params = new URLSearchParams({
|
|
28370
|
-
appId: this.config
|
|
28385
|
+
appId: this.config?.appId,
|
|
28371
28386
|
entityType,
|
|
28372
28387
|
entityId,
|
|
28373
28388
|
});
|
|
@@ -28397,7 +28412,7 @@ class FeedbackService {
|
|
|
28397
28412
|
reactionValue: value,
|
|
28398
28413
|
description: comment || '',
|
|
28399
28414
|
deviceContext: this.captureDeviceContext(),
|
|
28400
|
-
appId: this.config
|
|
28415
|
+
appId: this.config?.appId,
|
|
28401
28416
|
};
|
|
28402
28417
|
return firstValueFrom(this.http.post(this.baseUrl, request));
|
|
28403
28418
|
}
|
|
@@ -28426,7 +28441,7 @@ class FeedbackService {
|
|
|
28426
28441
|
reactionValue: value,
|
|
28427
28442
|
description: comment || '',
|
|
28428
28443
|
deviceContext: this.captureDeviceContext(),
|
|
28429
|
-
appId: this.config
|
|
28444
|
+
appId: this.config?.appId,
|
|
28430
28445
|
};
|
|
28431
28446
|
return firstValueFrom(this.http.post(`${this.baseUrl}/anonymous`, request));
|
|
28432
28447
|
}
|
|
@@ -28474,7 +28489,7 @@ class FeedbackService {
|
|
|
28474
28489
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FeedbackService, decorators: [{
|
|
28475
28490
|
type: Injectable,
|
|
28476
28491
|
args: [{ providedIn: 'root' }]
|
|
28477
|
-
}] });
|
|
28492
|
+
}], ctorParameters: () => [] });
|
|
28478
28493
|
|
|
28479
28494
|
/**
|
|
28480
28495
|
* Configuración por defecto de tipos de feedback.
|
|
@@ -30199,7 +30214,7 @@ class DeviceService {
|
|
|
30199
30214
|
this.http = http;
|
|
30200
30215
|
}
|
|
30201
30216
|
get baseUrl() {
|
|
30202
|
-
return `${this.config
|
|
30217
|
+
return `${this.config?.apiUrl}/v2/users/me/devices`;
|
|
30203
30218
|
}
|
|
30204
30219
|
/**
|
|
30205
30220
|
* Lista todos los dispositivos registrados del usuario.
|
|
@@ -30254,7 +30269,7 @@ class DeviceService {
|
|
|
30254
30269
|
* ```
|
|
30255
30270
|
*/
|
|
30256
30271
|
validateAction(token) {
|
|
30257
|
-
return this.http.post(`${this.config
|
|
30272
|
+
return this.http.post(`${this.config?.apiUrl}/v2/actions/validate`, { token });
|
|
30258
30273
|
}
|
|
30259
30274
|
/**
|
|
30260
30275
|
* Ejecuta una acción de dispositivo desde un token de email.
|
|
@@ -30277,7 +30292,7 @@ class DeviceService {
|
|
|
30277
30292
|
*/
|
|
30278
30293
|
executeAction(token) {
|
|
30279
30294
|
// Usa el endpoint unificado de acciones
|
|
30280
|
-
return this.http.post(`${this.config
|
|
30295
|
+
return this.http.post(`${this.config?.apiUrl}/v2/actions/execute`, { token }).pipe(map$1(response => ({
|
|
30281
30296
|
operationId: response.operationId,
|
|
30282
30297
|
success: response.success,
|
|
30283
30298
|
message: response.message,
|
|
@@ -30336,7 +30351,7 @@ class SessionService {
|
|
|
30336
30351
|
this.http = http;
|
|
30337
30352
|
}
|
|
30338
30353
|
get baseUrl() {
|
|
30339
|
-
return `${this.config
|
|
30354
|
+
return `${this.config?.apiUrl}/v2/users/me/sessions`;
|
|
30340
30355
|
}
|
|
30341
30356
|
/**
|
|
30342
30357
|
* Lista todas las sesiones activas del usuario.
|
|
@@ -30659,7 +30674,7 @@ class HandoffService {
|
|
|
30659
30674
|
const tokenParam = options.tokenParam ?? HANDOFF_TOKEN_PARAM;
|
|
30660
30675
|
const routeParam = options.routeParam ?? HANDOFF_ROUTE_PARAM;
|
|
30661
30676
|
const defaultRoute = options.defaultRoute ?? '/';
|
|
30662
|
-
const errorRoute = options.errorRoute ?? this.config
|
|
30677
|
+
const errorRoute = options.errorRoute ?? this.config?.loginRoute ?? '/login';
|
|
30663
30678
|
const params = new URLSearchParams(window.location.search);
|
|
30664
30679
|
const token = params.get(tokenParam);
|
|
30665
30680
|
if (!token) {
|
|
@@ -30689,7 +30704,7 @@ class HandoffService {
|
|
|
30689
30704
|
});
|
|
30690
30705
|
}
|
|
30691
30706
|
get baseUrl() {
|
|
30692
|
-
return `${this.config
|
|
30707
|
+
return `${this.config?.apiUrl}${this.config?.authPrefix}`;
|
|
30693
30708
|
}
|
|
30694
30709
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HandoffService, deps: [{ token: VALTECH_AUTH_CONFIG, optional: true }, { token: i1$8.HttpClient }, { token: AuthService }, { token: i1$1.Router }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
30695
30710
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HandoffService, providedIn: 'root' }); }
|
|
@@ -30877,7 +30892,7 @@ class NotificationActionService {
|
|
|
30877
30892
|
if (!route) {
|
|
30878
30893
|
return 'no-action-route';
|
|
30879
30894
|
}
|
|
30880
|
-
const currentApp = this.config
|
|
30895
|
+
const currentApp = this.config?.appId;
|
|
30881
30896
|
const targetApp = notif.appId;
|
|
30882
30897
|
// 2) Cross-app: handoff + full redirect
|
|
30883
30898
|
//
|
|
@@ -30887,7 +30902,7 @@ class NotificationActionService {
|
|
|
30887
30902
|
if (targetApp && currentApp && targetApp !== currentApp) {
|
|
30888
30903
|
try {
|
|
30889
30904
|
const resp = await firstValueFrom(this.handoff.createHandoff({ targetAppId: targetApp, route }));
|
|
30890
|
-
const baseUrl = resp.targetBaseUrl ?? this.config
|
|
30905
|
+
const baseUrl = resp.targetBaseUrl ?? this.config?.appUrls?.[targetApp];
|
|
30891
30906
|
if (!baseUrl) {
|
|
30892
30907
|
console.warn(`[NotificationAction] No baseUrl for app '${targetApp}' — backend did not return targetBaseUrl and no appUrls fallback configured`);
|
|
30893
30908
|
return 'cross-app-unconfigured';
|
|
@@ -46072,6 +46087,7 @@ class ContentReactionComponent {
|
|
|
46072
46087
|
this.toast = inject(ToastService);
|
|
46073
46088
|
this.i18n = inject(I18nService);
|
|
46074
46089
|
this.auth = inject(AuthService, { optional: true });
|
|
46090
|
+
this.isConfigured = computed(() => this.feedbackService.isConfigured());
|
|
46075
46091
|
this.props = {};
|
|
46076
46092
|
this.reactionSubmit = new EventEmitter();
|
|
46077
46093
|
this.reactionChange = new EventEmitter();
|
|
@@ -46249,11 +46265,11 @@ class ContentReactionComponent {
|
|
|
46249
46265
|
return this.i18n.t(key, 'ContentReaction') || translations[key] || key;
|
|
46250
46266
|
}
|
|
46251
46267
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContentReactionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
46252
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ContentReactionComponent, isStandalone: true, selector: "val-content-reaction", inputs: { props: "props" }, outputs: { reactionSubmit: "reactionSubmit", reactionChange: "reactionChange" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"content-reaction\" [class.disabled]=\"resolvedProps().disabled\" [class.readonly]=\"resolvedProps().readonly\">\n <!-- Loading inicial -->\n @if (state().isLoading && !state().selectedValue) {\n <div class=\"loading-container\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <!-- Pregunta -->\n <p class=\"question\">{{ resolvedProps().question }}</p>\n\n <!-- Emojis -->\n <div class=\"emoji-container\">\n @for (value of activeReactionValues(); track value; let i = $index) {\n <button\n type=\"button\"\n class=\"emoji-button\"\n [class.selected]=\"isSelected(value)\"\n [class.negative]=\"value === 'negative' && isSelected(value)\"\n [class.neutral]=\"value === 'neutral' && isSelected(value)\"\n [class.positive]=\"value === 'positive' && isSelected(value)\"\n [attr.aria-label]=\"getEmojiLabel(i)\"\n [attr.aria-pressed]=\"isSelected(value)\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction(value)\"\n >\n <span class=\"emoji\">{{ getEmoji(i) }}</span>\n </button>\n }\n </div>\n\n <!-- Campo de comentario (solo si hay selecci\u00F3n) -->\n @if (showCommentField()) {\n <div class=\"comment-section\">\n <ion-textarea\n [value]=\"state().comment\"\n [placeholder]=\"resolvedProps().commentPlaceholder\"\n [maxlength]=\"resolvedProps().maxCommentLength\"\n [disabled]=\"resolvedProps().disabled\"\n [rows]=\"3\"\n class=\"comment-textarea\"\n (ionInput)=\"updateComment($event)\"\n ></ion-textarea>\n <span class=\"char-count\"> {{ state().comment.length }}/{{ resolvedProps().maxCommentLength }} </span>\n </div>\n }\n\n <!-- Bot\u00F3n de env\u00EDo -->\n @if (state().selectedValue && !state().isSubmitted) {\n <ion-button expand=\"block\" [disabled]=\"!canSubmit()\" (click)=\"submitReaction()\" class=\"submit-button\">\n @if (state().isLoading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ state().hadPreviousReaction ? t('update') : t('submit') }} }\n </ion-button>\n }\n\n <!-- Mensaje de confirmaci\u00F3n -->\n @if (state().isSubmitted) {\n <p class=\"submitted-message\">{{ t('thankYou') }}</p>\n }\n\n <!-- Error -->\n @if (state().error) {\n <p class=\"error-message\">{{ state().error }}</p>\n } }\n</div>\n", styles: [":host{display:block}.content-reaction{padding:16px;text-align:center}.content-reaction.disabled{opacity:.6;pointer-events:none}.content-reaction.readonly{pointer-events:none}.question{font-size:16px;font-weight:500;color:var(--ion-color-dark);margin:0 0 16px}.emoji-container{display:flex;justify-content:center;gap:24px;margin-bottom:20px}.emoji-button{background:transparent;border:2px solid var(--ion-color-light-shade);border-radius:50%;width:64px;height:64px;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;justify-content:center;padding:0}.emoji-button .emoji{font-size:32px;transition:transform .3s cubic-bezier(.4,0,.2,1);line-height:1}.emoji-button:hover:not(:disabled){transform:scale(1.1);border-color:var(--ion-color-medium)}.emoji-button:focus{outline:2px solid var(--ion-color-primary);outline-offset:2px}.emoji-button.selected{transform:scale(1.2)}.emoji-button.selected .emoji{transform:scale(1.1)}.emoji-button.selected.negative{border-color:var(--ion-color-danger);background:var(--ion-color-danger-tint);box-shadow:0 4px 12px rgba(var(--ion-color-danger-rgb),.3)}.emoji-button.selected.neutral{border-color:var(--ion-color-warning);background:var(--ion-color-warning-tint);box-shadow:0 4px 12px rgba(var(--ion-color-warning-rgb),.3)}.emoji-button.selected.positive{border-color:var(--ion-color-success);background:var(--ion-color-success-tint);box-shadow:0 4px 12px rgba(var(--ion-color-success-rgb),.3)}.emoji-button:disabled{cursor:not-allowed;opacity:.5}.comment-section{margin-top:16px;animation:slideIn .3s ease-out}.comment-section .comment-textarea{--background: var(--ion-color-light);--border-radius: 8px;--padding-start: 12px;--padding-end: 12px;width:100%}.comment-section .char-count{display:block;text-align:right;font-size:12px;color:var(--ion-color-medium);margin-top:4px}.submit-button{margin-top:16px;--border-radius: 8px}.submitted-message{margin-top:16px;color:var(--ion-color-success);font-weight:500;animation:fadeIn .3s ease-out}.error-message{margin-top:8px;color:var(--ion-color-danger);font-size:14px}.loading-container{display:flex;justify-content:center;padding:20px}@keyframes slideIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { 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: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { 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"] }] }); }
|
|
46268
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ContentReactionComponent, isStandalone: true, selector: "val-content-reaction", inputs: { props: "props" }, outputs: { reactionSubmit: "reactionSubmit", reactionChange: "reactionChange" }, usesOnChanges: true, ngImport: i0, template: "@if (isConfigured()) {\n<div class=\"content-reaction\" [class.disabled]=\"resolvedProps().disabled\" [class.readonly]=\"resolvedProps().readonly\">\n <!-- Loading inicial -->\n @if (state().isLoading && !state().selectedValue) {\n <div class=\"loading-container\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <!-- Pregunta -->\n <p class=\"question\">{{ resolvedProps().question }}</p>\n\n <!-- Emojis -->\n <div class=\"emoji-container\">\n @for (value of activeReactionValues(); track value; let i = $index) {\n <button\n type=\"button\"\n class=\"emoji-button\"\n [class.selected]=\"isSelected(value)\"\n [class.negative]=\"value === 'negative' && isSelected(value)\"\n [class.neutral]=\"value === 'neutral' && isSelected(value)\"\n [class.positive]=\"value === 'positive' && isSelected(value)\"\n [attr.aria-label]=\"getEmojiLabel(i)\"\n [attr.aria-pressed]=\"isSelected(value)\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction(value)\"\n >\n <span class=\"emoji\">{{ getEmoji(i) }}</span>\n </button>\n }\n </div>\n\n <!-- Campo de comentario (solo si hay selecci\u00F3n) -->\n @if (showCommentField()) {\n <div class=\"comment-section\">\n <ion-textarea\n [value]=\"state().comment\"\n [placeholder]=\"resolvedProps().commentPlaceholder\"\n [maxlength]=\"resolvedProps().maxCommentLength\"\n [disabled]=\"resolvedProps().disabled\"\n [rows]=\"3\"\n class=\"comment-textarea\"\n (ionInput)=\"updateComment($event)\"\n ></ion-textarea>\n <span class=\"char-count\"> {{ state().comment.length }}/{{ resolvedProps().maxCommentLength }} </span>\n </div>\n }\n\n <!-- Bot\u00F3n de env\u00EDo -->\n @if (state().selectedValue && !state().isSubmitted) {\n <ion-button expand=\"block\" [disabled]=\"!canSubmit()\" (click)=\"submitReaction()\" class=\"submit-button\">\n @if (state().isLoading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ state().hadPreviousReaction ? t('update') : t('submit') }} }\n </ion-button>\n }\n\n <!-- Mensaje de confirmaci\u00F3n -->\n @if (state().isSubmitted) {\n <p class=\"submitted-message\">{{ t('thankYou') }}</p>\n }\n\n <!-- Error -->\n @if (state().error) {\n <p class=\"error-message\">{{ state().error }}</p>\n } }\n</div>\n}\n", styles: [":host{display:block}.content-reaction{padding:16px;text-align:center}.content-reaction.disabled{opacity:.6;pointer-events:none}.content-reaction.readonly{pointer-events:none}.question{font-size:16px;font-weight:500;color:var(--ion-color-dark);margin:0 0 16px}.emoji-container{display:flex;justify-content:center;gap:24px;margin-bottom:20px}.emoji-button{background:transparent;border:2px solid var(--ion-color-light-shade);border-radius:50%;width:64px;height:64px;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;justify-content:center;padding:0}.emoji-button .emoji{font-size:32px;transition:transform .3s cubic-bezier(.4,0,.2,1);line-height:1}.emoji-button:hover:not(:disabled){transform:scale(1.1);border-color:var(--ion-color-medium)}.emoji-button:focus{outline:2px solid var(--ion-color-primary);outline-offset:2px}.emoji-button.selected{transform:scale(1.2)}.emoji-button.selected .emoji{transform:scale(1.1)}.emoji-button.selected.negative{border-color:var(--ion-color-danger);background:var(--ion-color-danger-tint);box-shadow:0 4px 12px rgba(var(--ion-color-danger-rgb),.3)}.emoji-button.selected.neutral{border-color:var(--ion-color-warning);background:var(--ion-color-warning-tint);box-shadow:0 4px 12px rgba(var(--ion-color-warning-rgb),.3)}.emoji-button.selected.positive{border-color:var(--ion-color-success);background:var(--ion-color-success-tint);box-shadow:0 4px 12px rgba(var(--ion-color-success-rgb),.3)}.emoji-button:disabled{cursor:not-allowed;opacity:.5}.comment-section{margin-top:16px;animation:slideIn .3s ease-out}.comment-section .comment-textarea{--background: var(--ion-color-light);--border-radius: 8px;--padding-start: 12px;--padding-end: 12px;width:100%}.comment-section .char-count{display:block;text-align:right;font-size:12px;color:var(--ion-color-medium);margin-top:4px}.submit-button{margin-top:16px;--border-radius: 8px}.submitted-message{margin-top:16px;color:var(--ion-color-success);font-weight:500;animation:fadeIn .3s ease-out}.error-message{margin-top:8px;color:var(--ion-color-danger);font-size:14px}.loading-container{display:flex;justify-content:center;padding:20px}@keyframes slideIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { 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: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { 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"] }] }); }
|
|
46253
46269
|
}
|
|
46254
46270
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContentReactionComponent, decorators: [{
|
|
46255
46271
|
type: Component,
|
|
46256
|
-
args: [{ selector: 'val-content-reaction', standalone: true, imports: [CommonModule, FormsModule, IonButton, IonSpinner, IonTextarea], template: "<div class=\"content-reaction\" [class.disabled]=\"resolvedProps().disabled\" [class.readonly]=\"resolvedProps().readonly\">\n <!-- Loading inicial -->\n @if (state().isLoading && !state().selectedValue) {\n <div class=\"loading-container\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <!-- Pregunta -->\n <p class=\"question\">{{ resolvedProps().question }}</p>\n\n <!-- Emojis -->\n <div class=\"emoji-container\">\n @for (value of activeReactionValues(); track value; let i = $index) {\n <button\n type=\"button\"\n class=\"emoji-button\"\n [class.selected]=\"isSelected(value)\"\n [class.negative]=\"value === 'negative' && isSelected(value)\"\n [class.neutral]=\"value === 'neutral' && isSelected(value)\"\n [class.positive]=\"value === 'positive' && isSelected(value)\"\n [attr.aria-label]=\"getEmojiLabel(i)\"\n [attr.aria-pressed]=\"isSelected(value)\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction(value)\"\n >\n <span class=\"emoji\">{{ getEmoji(i) }}</span>\n </button>\n }\n </div>\n\n <!-- Campo de comentario (solo si hay selecci\u00F3n) -->\n @if (showCommentField()) {\n <div class=\"comment-section\">\n <ion-textarea\n [value]=\"state().comment\"\n [placeholder]=\"resolvedProps().commentPlaceholder\"\n [maxlength]=\"resolvedProps().maxCommentLength\"\n [disabled]=\"resolvedProps().disabled\"\n [rows]=\"3\"\n class=\"comment-textarea\"\n (ionInput)=\"updateComment($event)\"\n ></ion-textarea>\n <span class=\"char-count\"> {{ state().comment.length }}/{{ resolvedProps().maxCommentLength }} </span>\n </div>\n }\n\n <!-- Bot\u00F3n de env\u00EDo -->\n @if (state().selectedValue && !state().isSubmitted) {\n <ion-button expand=\"block\" [disabled]=\"!canSubmit()\" (click)=\"submitReaction()\" class=\"submit-button\">\n @if (state().isLoading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ state().hadPreviousReaction ? t('update') : t('submit') }} }\n </ion-button>\n }\n\n <!-- Mensaje de confirmaci\u00F3n -->\n @if (state().isSubmitted) {\n <p class=\"submitted-message\">{{ t('thankYou') }}</p>\n }\n\n <!-- Error -->\n @if (state().error) {\n <p class=\"error-message\">{{ state().error }}</p>\n } }\n</div>\n", styles: [":host{display:block}.content-reaction{padding:16px;text-align:center}.content-reaction.disabled{opacity:.6;pointer-events:none}.content-reaction.readonly{pointer-events:none}.question{font-size:16px;font-weight:500;color:var(--ion-color-dark);margin:0 0 16px}.emoji-container{display:flex;justify-content:center;gap:24px;margin-bottom:20px}.emoji-button{background:transparent;border:2px solid var(--ion-color-light-shade);border-radius:50%;width:64px;height:64px;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;justify-content:center;padding:0}.emoji-button .emoji{font-size:32px;transition:transform .3s cubic-bezier(.4,0,.2,1);line-height:1}.emoji-button:hover:not(:disabled){transform:scale(1.1);border-color:var(--ion-color-medium)}.emoji-button:focus{outline:2px solid var(--ion-color-primary);outline-offset:2px}.emoji-button.selected{transform:scale(1.2)}.emoji-button.selected .emoji{transform:scale(1.1)}.emoji-button.selected.negative{border-color:var(--ion-color-danger);background:var(--ion-color-danger-tint);box-shadow:0 4px 12px rgba(var(--ion-color-danger-rgb),.3)}.emoji-button.selected.neutral{border-color:var(--ion-color-warning);background:var(--ion-color-warning-tint);box-shadow:0 4px 12px rgba(var(--ion-color-warning-rgb),.3)}.emoji-button.selected.positive{border-color:var(--ion-color-success);background:var(--ion-color-success-tint);box-shadow:0 4px 12px rgba(var(--ion-color-success-rgb),.3)}.emoji-button:disabled{cursor:not-allowed;opacity:.5}.comment-section{margin-top:16px;animation:slideIn .3s ease-out}.comment-section .comment-textarea{--background: var(--ion-color-light);--border-radius: 8px;--padding-start: 12px;--padding-end: 12px;width:100%}.comment-section .char-count{display:block;text-align:right;font-size:12px;color:var(--ion-color-medium);margin-top:4px}.submit-button{margin-top:16px;--border-radius: 8px}.submitted-message{margin-top:16px;color:var(--ion-color-success);font-weight:500;animation:fadeIn .3s ease-out}.error-message{margin-top:8px;color:var(--ion-color-danger);font-size:14px}.loading-container{display:flex;justify-content:center;padding:20px}@keyframes slideIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"] }]
|
|
46272
|
+
args: [{ selector: 'val-content-reaction', standalone: true, imports: [CommonModule, FormsModule, IonButton, IonSpinner, IonTextarea], template: "@if (isConfigured()) {\n<div class=\"content-reaction\" [class.disabled]=\"resolvedProps().disabled\" [class.readonly]=\"resolvedProps().readonly\">\n <!-- Loading inicial -->\n @if (state().isLoading && !state().selectedValue) {\n <div class=\"loading-container\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <!-- Pregunta -->\n <p class=\"question\">{{ resolvedProps().question }}</p>\n\n <!-- Emojis -->\n <div class=\"emoji-container\">\n @for (value of activeReactionValues(); track value; let i = $index) {\n <button\n type=\"button\"\n class=\"emoji-button\"\n [class.selected]=\"isSelected(value)\"\n [class.negative]=\"value === 'negative' && isSelected(value)\"\n [class.neutral]=\"value === 'neutral' && isSelected(value)\"\n [class.positive]=\"value === 'positive' && isSelected(value)\"\n [attr.aria-label]=\"getEmojiLabel(i)\"\n [attr.aria-pressed]=\"isSelected(value)\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction(value)\"\n >\n <span class=\"emoji\">{{ getEmoji(i) }}</span>\n </button>\n }\n </div>\n\n <!-- Campo de comentario (solo si hay selecci\u00F3n) -->\n @if (showCommentField()) {\n <div class=\"comment-section\">\n <ion-textarea\n [value]=\"state().comment\"\n [placeholder]=\"resolvedProps().commentPlaceholder\"\n [maxlength]=\"resolvedProps().maxCommentLength\"\n [disabled]=\"resolvedProps().disabled\"\n [rows]=\"3\"\n class=\"comment-textarea\"\n (ionInput)=\"updateComment($event)\"\n ></ion-textarea>\n <span class=\"char-count\"> {{ state().comment.length }}/{{ resolvedProps().maxCommentLength }} </span>\n </div>\n }\n\n <!-- Bot\u00F3n de env\u00EDo -->\n @if (state().selectedValue && !state().isSubmitted) {\n <ion-button expand=\"block\" [disabled]=\"!canSubmit()\" (click)=\"submitReaction()\" class=\"submit-button\">\n @if (state().isLoading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ state().hadPreviousReaction ? t('update') : t('submit') }} }\n </ion-button>\n }\n\n <!-- Mensaje de confirmaci\u00F3n -->\n @if (state().isSubmitted) {\n <p class=\"submitted-message\">{{ t('thankYou') }}</p>\n }\n\n <!-- Error -->\n @if (state().error) {\n <p class=\"error-message\">{{ state().error }}</p>\n } }\n</div>\n}\n", styles: [":host{display:block}.content-reaction{padding:16px;text-align:center}.content-reaction.disabled{opacity:.6;pointer-events:none}.content-reaction.readonly{pointer-events:none}.question{font-size:16px;font-weight:500;color:var(--ion-color-dark);margin:0 0 16px}.emoji-container{display:flex;justify-content:center;gap:24px;margin-bottom:20px}.emoji-button{background:transparent;border:2px solid var(--ion-color-light-shade);border-radius:50%;width:64px;height:64px;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;justify-content:center;padding:0}.emoji-button .emoji{font-size:32px;transition:transform .3s cubic-bezier(.4,0,.2,1);line-height:1}.emoji-button:hover:not(:disabled){transform:scale(1.1);border-color:var(--ion-color-medium)}.emoji-button:focus{outline:2px solid var(--ion-color-primary);outline-offset:2px}.emoji-button.selected{transform:scale(1.2)}.emoji-button.selected .emoji{transform:scale(1.1)}.emoji-button.selected.negative{border-color:var(--ion-color-danger);background:var(--ion-color-danger-tint);box-shadow:0 4px 12px rgba(var(--ion-color-danger-rgb),.3)}.emoji-button.selected.neutral{border-color:var(--ion-color-warning);background:var(--ion-color-warning-tint);box-shadow:0 4px 12px rgba(var(--ion-color-warning-rgb),.3)}.emoji-button.selected.positive{border-color:var(--ion-color-success);background:var(--ion-color-success-tint);box-shadow:0 4px 12px rgba(var(--ion-color-success-rgb),.3)}.emoji-button:disabled{cursor:not-allowed;opacity:.5}.comment-section{margin-top:16px;animation:slideIn .3s ease-out}.comment-section .comment-textarea{--background: var(--ion-color-light);--border-radius: 8px;--padding-start: 12px;--padding-end: 12px;width:100%}.comment-section .char-count{display:block;text-align:right;font-size:12px;color:var(--ion-color-medium);margin-top:4px}.submit-button{margin-top:16px;--border-radius: 8px}.submitted-message{margin-top:16px;color:var(--ion-color-success);font-weight:500;animation:fadeIn .3s ease-out}.error-message{margin-top:8px;color:var(--ion-color-danger);font-size:14px}.loading-container{display:flex;justify-content:center;padding:20px}@keyframes slideIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"] }]
|
|
46257
46273
|
}], propDecorators: { props: [{
|
|
46258
46274
|
type: Input
|
|
46259
46275
|
}], reactionSubmit: [{
|