@memberjunction/ng-explorer-core 6.2.0-edge.5 → 6.2.0-edge.7
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/README.md +7 -2
- package/dist/app-routing.module.d.ts.map +1 -1
- package/dist/app-routing.module.js +22 -4
- package/dist/app-routing.module.js.map +1 -1
- package/dist/generated/lazy-feature-config.d.ts +1 -1
- package/dist/generated/lazy-feature-config.d.ts.map +1 -1
- package/dist/generated/lazy-feature-config.js +16 -2
- package/dist/generated/lazy-feature-config.js.map +1 -1
- package/dist/lib/dashboard-preferences-dialog/dashboard-preferences-dialog.component.js +2 -2
- package/dist/lib/office/office-connect.component.d.ts +19 -0
- package/dist/lib/office/office-connect.component.d.ts.map +1 -0
- package/dist/lib/office/office-connect.component.js +80 -0
- package/dist/lib/office/office-connect.component.js.map +1 -0
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts +7 -0
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.js +20 -8
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/dashboard-class-resolution.d.ts +14 -0
- package/dist/lib/resource-wrappers/dashboard-class-resolution.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-class-resolution.js +21 -0
- package/dist/lib/resource-wrappers/dashboard-class-resolution.js.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/dashboard-resource.component.js +3 -2
- package/dist/lib/resource-wrappers/dashboard-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/record-form-mode.d.ts +44 -0
- package/dist/lib/resource-wrappers/record-form-mode.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/record-form-mode.js +47 -0
- package/dist/lib/resource-wrappers/record-form-mode.js.map +1 -0
- package/dist/lib/resource-wrappers/record-resource.component.d.ts +56 -1
- package/dist/lib/resource-wrappers/record-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/record-resource.component.js +124 -9
- package/dist/lib/resource-wrappers/record-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/standard-form-create.d.ts +4 -0
- package/dist/lib/resource-wrappers/standard-form-create.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/standard-form-create.js +6 -0
- package/dist/lib/resource-wrappers/standard-form-create.js.map +1 -0
- package/dist/lib/resource-wrappers/view-resource.component.d.ts +18 -1
- package/dist/lib/resource-wrappers/view-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/view-resource.component.js +105 -39
- package/dist/lib/resource-wrappers/view-resource.component.js.map +1 -1
- package/dist/lib/shell/components/dialogs/app-access-dialog.component.js +2 -2
- package/dist/lib/shell/components/record-open/record-origin-crumb.component.d.ts.map +1 -1
- package/dist/lib/shell/components/record-open/record-origin-crumb.component.js +12 -8
- package/dist/lib/shell/components/record-open/record-origin-crumb.component.js.map +1 -1
- package/dist/lib/shell/components/tabs/component-cache-manager.d.ts +6 -0
- package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -1
- package/dist/lib/shell/components/tabs/component-cache-manager.js +18 -3
- package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts +2 -0
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.js +38 -10
- package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
- package/dist/lib/shell/shell-chrome-policy.d.ts +6 -0
- package/dist/lib/shell/shell-chrome-policy.d.ts.map +1 -1
- package/dist/lib/shell/shell-chrome-policy.js +1 -0
- package/dist/lib/shell/shell-chrome-policy.js.map +1 -1
- package/dist/lib/shell/shell.component.d.ts +23 -0
- package/dist/lib/shell/shell.component.d.ts.map +1 -1
- package/dist/lib/shell/shell.component.js +283 -71
- package/dist/lib/shell/shell.component.js.map +1 -1
- package/dist/lib/single-list-detail/single-list-detail.component.js +2 -2
- package/dist/lib/single-list-detail/single-list-detail.component.js.map +1 -1
- package/dist/lib/single-record/single-record.component.d.ts +27 -2
- package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
- package/dist/lib/single-record/single-record.component.js +54 -8
- package/dist/lib/single-record/single-record.component.js.map +1 -1
- package/dist/lib/user-notifications/user-notifications.component.d.ts +24 -2
- package/dist/lib/user-notifications/user-notifications.component.d.ts.map +1 -1
- package/dist/lib/user-notifications/user-notifications.component.js +139 -24
- package/dist/lib/user-notifications/user-notifications.component.js.map +1 -1
- package/dist/lib/user-profile/user-profile.component.js +2 -2
- package/dist/module.d.ts +64 -63
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +3 -0
- package/dist/module.js.map +1 -1
- package/package.json +58 -57
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Component, ChangeDetectorRef, inject } from '@angular/core';
|
|
1
|
+
import { Component, ChangeDetectorRef, NgZone, inject } from '@angular/core';
|
|
2
2
|
import { SharedService } from '@memberjunction/ng-shared';
|
|
3
3
|
import { MJUserNotificationEntity, UserInfoEngine } from '@memberjunction/core-entities';
|
|
4
4
|
import { CompositeKey } from '@memberjunction/core';
|
|
@@ -368,6 +368,7 @@ export class UserNotificationsComponent extends BaseAngularComponent {
|
|
|
368
368
|
navigationService;
|
|
369
369
|
appManager;
|
|
370
370
|
cdr = inject(ChangeDetectorRef);
|
|
371
|
+
ngZone = inject(NgZone, { optional: true });
|
|
371
372
|
RadioSelected = 'All';
|
|
372
373
|
/** @deprecated Use {@link RadioSelected}. */
|
|
373
374
|
get radioSelected() {
|
|
@@ -436,13 +437,34 @@ export class UserNotificationsComponent extends BaseAngularComponent {
|
|
|
436
437
|
async ngOnInit() {
|
|
437
438
|
this.loadNotificationTypes();
|
|
438
439
|
// Refresh the snapshot relative-time labels between change-detection passes.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
440
|
+
// Run outside NgZone so the 60s timer only triggers Angular CD when a label actually changes.
|
|
441
|
+
const startTimer = () => {
|
|
442
|
+
this.relativeTimer = setInterval(() => {
|
|
443
|
+
let anyChanged = false;
|
|
444
|
+
for (const vm of this.vmCache.values()) {
|
|
445
|
+
const next = this.relativeTime(vm.N.__mj_CreatedAt);
|
|
446
|
+
if (vm.Relative !== next) {
|
|
447
|
+
vm.Relative = next;
|
|
448
|
+
anyChanged = true;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
this.groupsKey = ''; // day buckets can shift at midnight
|
|
452
|
+
if (anyChanged) {
|
|
453
|
+
if (this.ngZone) {
|
|
454
|
+
this.ngZone.run(() => this.cdr.markForCheck());
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
this.cdr.markForCheck();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}, RELATIVE_REFRESH_MS);
|
|
461
|
+
};
|
|
462
|
+
if (this.ngZone) {
|
|
463
|
+
this.ngZone.runOutsideAngular(startTimer);
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
startTimer();
|
|
467
|
+
}
|
|
446
468
|
}
|
|
447
469
|
ngAfterViewInit() {
|
|
448
470
|
this.SharedService.InvokeManualResize(); // make sure the notifications component is sized correctly
|
|
@@ -468,17 +490,38 @@ export class UserNotificationsComponent extends BaseAngularComponent {
|
|
|
468
490
|
// ========================================================================
|
|
469
491
|
// Data shaping (filter → view models → day groups)
|
|
470
492
|
// ========================================================================
|
|
493
|
+
notificationsToShowCache = [];
|
|
494
|
+
notificationsToShowKey = '';
|
|
495
|
+
unreadNotificationsCache = [];
|
|
496
|
+
unreadNotificationsKey = '';
|
|
497
|
+
readNotificationsCache = [];
|
|
498
|
+
readNotificationsKey = '';
|
|
499
|
+
computeAllNotificationsSignature() {
|
|
500
|
+
const all = this.AllNotifications;
|
|
501
|
+
let unreadCount = 0;
|
|
502
|
+
for (let i = 0; i < all.length; i++) {
|
|
503
|
+
if (all[i].Unread)
|
|
504
|
+
unreadCount++;
|
|
505
|
+
}
|
|
506
|
+
const baseKey = `${all.length}|${unreadCount}|${all[0]?.ID ?? ''}|${all[all.length - 1]?.ID ?? ''}`;
|
|
507
|
+
return { all, unreadCount, baseKey };
|
|
508
|
+
}
|
|
471
509
|
get NotificationsToShow() {
|
|
510
|
+
const { all, baseKey } = this.computeAllNotificationsSignature();
|
|
511
|
+
const key = `${baseKey}|${this.RadioSelected}|${this.SelectedTypeFilter ?? ''}|${this.CurrentFilter}`;
|
|
512
|
+
if (key === this.notificationsToShowKey) {
|
|
513
|
+
return this.notificationsToShowCache;
|
|
514
|
+
}
|
|
472
515
|
let temp = [];
|
|
473
516
|
switch (this.RadioSelected) {
|
|
474
517
|
case 'All':
|
|
475
|
-
temp =
|
|
518
|
+
temp = all;
|
|
476
519
|
break;
|
|
477
520
|
case 'Unread':
|
|
478
|
-
temp =
|
|
521
|
+
temp = all.filter(n => n.Unread);
|
|
479
522
|
break;
|
|
480
523
|
case 'Read':
|
|
481
|
-
temp =
|
|
524
|
+
temp = all.filter(n => !n.Unread);
|
|
482
525
|
break;
|
|
483
526
|
}
|
|
484
527
|
// Apply type filter if selected
|
|
@@ -486,24 +529,26 @@ export class UserNotificationsComponent extends BaseAngularComponent {
|
|
|
486
529
|
temp = temp.filter(n => UUIDsEqual(n.NotificationTypeID, this.SelectedTypeFilter));
|
|
487
530
|
}
|
|
488
531
|
// Apply text filter if it is not empty
|
|
489
|
-
|
|
532
|
+
const filterLower = this.CurrentFilter.trim().toLowerCase();
|
|
533
|
+
if (filterLower.length > 0) {
|
|
490
534
|
// check for inclusion of filter value in title or message
|
|
491
|
-
temp = temp.filter(n => n.Title?.toLowerCase().includes(
|
|
492
|
-
n.Message?.toLowerCase().includes(
|
|
535
|
+
temp = temp.filter(n => n.Title?.toLowerCase().includes(filterLower) ||
|
|
536
|
+
n.Message?.toLowerCase().includes(filterLower));
|
|
493
537
|
}
|
|
538
|
+
this.notificationsToShowKey = key;
|
|
539
|
+
this.notificationsToShowCache = temp;
|
|
494
540
|
return temp;
|
|
495
541
|
}
|
|
496
542
|
/**
|
|
497
543
|
* The filtered notifications as render-ready day groups. Memoized on a composite
|
|
498
|
-
* key of the inputs so the getter stays
|
|
499
|
-
* the (time-sensitive) VM snapshots never rebuild mid-pass.
|
|
544
|
+
* key of the inputs BEFORE filtering `AllNotifications`, so the getter stays $O(1)$
|
|
545
|
+
* across change-detection passes and the (time-sensitive) VM snapshots never rebuild mid-pass.
|
|
500
546
|
*/
|
|
501
547
|
get Groups() {
|
|
502
|
-
const
|
|
503
|
-
const
|
|
504
|
-
const key = `${list.length}|${unread}|${this.RadioSelected}|${this.SelectedTypeFilter}|${this.CurrentFilter}|${list[0]?.ID ?? ''}|${list[list.length - 1]?.ID ?? ''}`;
|
|
548
|
+
const { baseKey } = this.computeAllNotificationsSignature();
|
|
549
|
+
const key = `${baseKey}|${this.RadioSelected}|${this.SelectedTypeFilter ?? ''}|${this.CurrentFilter}`;
|
|
505
550
|
if (key !== this.groupsKey) {
|
|
506
|
-
this.groups = this.buildGroups(
|
|
551
|
+
this.groups = this.buildGroups(this.NotificationsToShow);
|
|
507
552
|
this.groupsKey = key;
|
|
508
553
|
}
|
|
509
554
|
return this.groups;
|
|
@@ -651,6 +696,9 @@ export class UserNotificationsComponent extends BaseAngularComponent {
|
|
|
651
696
|
if (typeName === 'meet-room') {
|
|
652
697
|
return true;
|
|
653
698
|
}
|
|
699
|
+
if (this.parseUserRoutineConfig(notification)) {
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
654
702
|
}
|
|
655
703
|
const info = this.NotificationUrl(notification);
|
|
656
704
|
return (info !== null && info.urlParts && info.urlParts.length > 0);
|
|
@@ -718,10 +766,20 @@ export class UserNotificationsComponent extends BaseAngularComponent {
|
|
|
718
766
|
return SharedService.UserNotifications;
|
|
719
767
|
}
|
|
720
768
|
get UnreadNotifications() {
|
|
721
|
-
|
|
769
|
+
const { all, baseKey } = this.computeAllNotificationsSignature();
|
|
770
|
+
if (baseKey !== this.unreadNotificationsKey) {
|
|
771
|
+
this.unreadNotificationsKey = baseKey;
|
|
772
|
+
this.unreadNotificationsCache = all.filter(n => n.Unread);
|
|
773
|
+
}
|
|
774
|
+
return this.unreadNotificationsCache;
|
|
722
775
|
}
|
|
723
776
|
get ReadNotifications() {
|
|
724
|
-
|
|
777
|
+
const { all, baseKey } = this.computeAllNotificationsSignature();
|
|
778
|
+
if (baseKey !== this.readNotificationsKey) {
|
|
779
|
+
this.readNotificationsKey = baseKey;
|
|
780
|
+
this.readNotificationsCache = all.filter(n => !n.Unread);
|
|
781
|
+
}
|
|
782
|
+
return this.readNotificationsCache;
|
|
725
783
|
}
|
|
726
784
|
SelectReadOption(option) {
|
|
727
785
|
this.RadioSelected = option;
|
|
@@ -824,6 +882,9 @@ export class UserNotificationsComponent extends BaseAngularComponent {
|
|
|
824
882
|
if (this.navigateToMeetRoom(notification)) {
|
|
825
883
|
return;
|
|
826
884
|
}
|
|
885
|
+
if (this.navigateToUserRoutine(notification)) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
827
888
|
if (this.navigateToHandoffConsole(notification)) {
|
|
828
889
|
return;
|
|
829
890
|
}
|
|
@@ -1009,9 +1070,63 @@ export class UserNotificationsComponent extends BaseAngularComponent {
|
|
|
1009
1070
|
navConfig['versionNumber'] = config.versionNumber;
|
|
1010
1071
|
if (config.taskId)
|
|
1011
1072
|
navConfig['taskId'] = config.taskId;
|
|
1012
|
-
|
|
1073
|
+
// The Chat tab is usually already open and gets reused; a reused tab reads its configuration
|
|
1074
|
+
// only at init, so the same values go in the query params, which it does react to.
|
|
1075
|
+
this.navigationService.OpenNavItemByName('Conversations', navConfig, chatApp.ID, { queryParams: { ...navConfig } });
|
|
1076
|
+
return true;
|
|
1077
|
+
}
|
|
1078
|
+
/** The routine-run config of a notification, or null when it is not one (or names no routine). */
|
|
1079
|
+
parseUserRoutineConfig(notification) {
|
|
1080
|
+
if (!notification.ResourceConfiguration || notification.ResourceConfiguration.trim().length === 0) {
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
const config = SafeJSONParse(notification.ResourceConfiguration);
|
|
1084
|
+
if (!config || config.type?.trim().toLowerCase() !== 'userroutine' || !config.routineId) {
|
|
1085
|
+
return null;
|
|
1086
|
+
}
|
|
1087
|
+
return config;
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Opens what a routine run produced: its reply, in the routine's own conversation (the
|
|
1091
|
+
* `{ type:'UserRoutine', routineId, runId, conversationId? }` ResourceConfiguration). That
|
|
1092
|
+
* conversation is application-scoped, so the chat sidebar never lists it; the click deep-links
|
|
1093
|
+
* into the Chat app's Conversations item. A notification written before the config carried the
|
|
1094
|
+
* conversation reads it off the routine; a routine with no conversation opens in the Routines
|
|
1095
|
+
* app. Returns false (not handled) when the notification is not a routine run.
|
|
1096
|
+
*/
|
|
1097
|
+
navigateToUserRoutine(notification) {
|
|
1098
|
+
const config = this.parseUserRoutineConfig(notification);
|
|
1099
|
+
if (!config) {
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
void this.openUserRoutineOutput(config);
|
|
1013
1103
|
return true;
|
|
1014
1104
|
}
|
|
1105
|
+
async openUserRoutineOutput(config) {
|
|
1106
|
+
const conversationId = config.conversationId ?? await this.loadRoutineConversationId(config.routineId);
|
|
1107
|
+
const chatApp = conversationId ? this.appManager.GetAppByName('Chat') : null;
|
|
1108
|
+
if (conversationId && chatApp) {
|
|
1109
|
+
// Query params as well as configuration: an already-open Chat tab is reused and reacts
|
|
1110
|
+
// only to a query-param change (its configuration is read once, at init).
|
|
1111
|
+
await this.navigationService.OpenNavItemByName('Conversations', { conversationId }, chatApp.ID, { queryParams: { conversationId } });
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
const routinesApp = this.appManager.GetAppByName('Routines');
|
|
1115
|
+
if (routinesApp) {
|
|
1116
|
+
await this.navigationService.OpenNavItemByName('My Routines', { routine: config.routineId }, routinesApp.ID);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
MJNotificationService.Instance.CreateSimpleNotification('The Routines app is not installed, so this routine cannot be opened', 'error', 4000);
|
|
1120
|
+
}
|
|
1121
|
+
/** The routine's ConversationID, or null when the routine cannot be read or has none. */
|
|
1122
|
+
async loadRoutineConversationId(routineId) {
|
|
1123
|
+
const p = this.ProviderToUse;
|
|
1124
|
+
const routine = await p.GetEntityObject('MJ: User Routines', p.CurrentUser);
|
|
1125
|
+
if (!await routine.Load(routineId)) {
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
return routine.ConversationID ?? null;
|
|
1129
|
+
}
|
|
1015
1130
|
GetNotificationType(typeId) {
|
|
1016
1131
|
if (!typeId)
|
|
1017
1132
|
return null;
|
|
@@ -1084,5 +1199,5 @@ export class UserNotificationsComponent extends BaseAngularComponent {
|
|
|
1084
1199
|
type: Component,
|
|
1085
1200
|
args: [{ standalone: false, selector: 'app-user-notifications', template: "<div class=\"notifications-container\">\n <!-- Header Section -->\n <div class=\"notifications-header\">\n <div class=\"header-top\">\n <div class=\"header-title\">\n <div class=\"title-icon\">\n <i class=\"fa-solid fa-bell\"></i>\n </div>\n <div class=\"title-text\">\n <h1>Notifications</h1>\n @if (UnreadNotifications.length > 0) {\n <span class=\"title-subtitle\">{{UnreadNotifications.length}} unread</span>\n } @else if (AllNotifications.length > 0) {\n <span class=\"title-subtitle\">All caught up</span>\n }\n </div>\n </div>\n\n @if (AllNotifications.length > 0) {\n <div class=\"header-actions\">\n @if (UnreadNotifications.length > 0) {\n <button class=\"action-btn action-btn-ghost\"\n type=\"button\"\n (click)=\"markAllAsRead()\"\n aria-label=\"Mark all notifications as read\">\n <i class=\"fa-solid fa-check-double\" aria-hidden=\"true\"></i>\n <span class=\"btn-text\">Mark All Read</span>\n </button>\n }\n @if (ReadNotifications.length > 0) {\n <button class=\"action-btn action-btn-secondary\"\n type=\"button\"\n (click)=\"markAllAsUnread()\"\n aria-label=\"Mark all notifications as unread\">\n <i class=\"fa-solid fa-envelope\" aria-hidden=\"true\"></i>\n <span class=\"btn-text\">Mark All Unread</span>\n </button>\n }\n </div>\n }\n </div>\n\n <!-- Filters Section -->\n @if (AllNotifications.length > 0) {\n <div class=\"filters-section\">\n <div class=\"filters-slider\">\n <div class=\"slider-content\">\n <!-- Read-status pills -->\n <div class=\"header-stats\" role=\"group\" aria-label=\"Filter notifications by read status\">\n <button class=\"stat-pill\"\n type=\"button\"\n [class.active]=\"radioSelected === 'All'\"\n (click)=\"selectReadOption('All')\"\n [attr.aria-pressed]=\"radioSelected === 'All'\"\n aria-label=\"Show all notifications\">\n <span class=\"stat-label\">All</span>\n <span class=\"stat-count\">{{AllNotifications.length}}</span>\n </button>\n\n @if (UnreadNotifications.length > 0 || ReadNotifications.length === 0) {\n <button class=\"stat-pill\"\n type=\"button\"\n [class.active]=\"radioSelected === 'Unread'\"\n [class.has-unread]=\"UnreadNotifications.length > 0\"\n (click)=\"selectReadOption('Unread')\"\n [attr.aria-pressed]=\"radioSelected === 'Unread'\"\n aria-label=\"Show unread notifications\">\n <span class=\"stat-label\">Unread</span>\n <span class=\"stat-count\" [class.unread-count]=\"UnreadNotifications.length > 0\">\n {{UnreadNotifications.length}}\n </span>\n </button>\n }\n\n @if (ReadNotifications.length > 0 || UnreadNotifications.length === 0) {\n <button class=\"stat-pill\"\n type=\"button\"\n [class.active]=\"radioSelected === 'Read'\"\n (click)=\"selectReadOption('Read')\"\n [attr.aria-pressed]=\"radioSelected === 'Read'\"\n aria-label=\"Show read notifications\">\n <span class=\"stat-label\">Read</span>\n <span class=\"stat-count\">{{ReadNotifications.length}}</span>\n </button>\n }\n </div>\n\n <!-- Type filter -->\n @if (notificationTypes.length > 0) {\n <div class=\"filter-item type-filter-slider\">\n <select class=\"type-select\"\n [(ngModel)]=\"selectedTypeFilter\"\n (change)=\"onTypeFilterChange(selectedTypeFilter)\"\n aria-label=\"Filter by notification type\">\n <option [value]=\"null\">All Types</option>\n @for (type of notificationTypes; track type.ID) {\n <option [value]=\"type.ID\">{{type.Name}}</option>\n }\n </select>\n </div>\n }\n </div>\n </div>\n\n <!-- Search -->\n <div class=\"search-section\">\n <div class=\"search-container\">\n <i class=\"fa-solid fa-magnifying-glass search-icon\" aria-hidden=\"true\"></i>\n <input type=\"text\"\n class=\"search-input\"\n placeholder=\"Search notifications...\"\n [value]=\"currentFilter\"\n (input)=\"onFilterChanged($any($event.target).value)\"\n aria-label=\"Search notifications\" />\n </div>\n </div>\n\n @if (NotificationsToShow.length !== AllNotifications.length) {\n <div class=\"filter-info\">\n <span class=\"filter-badge\">\n <i class=\"fa-solid fa-filter\" aria-hidden=\"true\"></i>\n Showing {{NotificationsToShow.length}} of {{AllNotifications.length}}\n </span>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Notifications Content -->\n <div class=\"notifications-content\">\n @if (NotificationsToShow.length === 0 && AllNotifications.length === 0) {\n <mj-empty-state\n Variant=\"success\"\n Icon=\"fa-solid fa-bell-slash\"\n Title=\"No Notifications\"\n Message=\"You're all caught up! New notifications will appear here.\" />\n } @else if (NotificationsToShow.length === 0) {\n <mj-empty-state\n Variant=\"no-results\"\n Icon=\"fa-solid fa-filter-circle-xmark\"\n Title=\"No Matching Notifications\"\n Message=\"Try adjusting your filters to see more notifications.\" />\n } @else {\n <!-- Day-grouped notification list -->\n <div class=\"notifications-list\">\n @for (group of Groups; track group.Key) {\n <div class=\"group-label\">{{group.Label}}</div>\n\n @for (vm of group.Items; track vm.N.ID) {\n <article class=\"notification-card\"\n [class.unread]=\"vm.N.Unread\"\n [class.expanded]=\"IsExpanded(vm)\"\n [class.clickable]=\"vm.Clickable\"\n role=\"article\"\n [attr.aria-label]=\"vm.N.Title\">\n\n <!-- Unread Indicator Bar -->\n <div class=\"card-indicator\" aria-hidden=\"true\"></div>\n\n <!-- Type Icon -->\n <div class=\"card-icon\" aria-hidden=\"true\">\n <i [class]=\"'fa-solid ' + getTypeIcon(vm.N)\"\n [style.color]=\"getTypeColor(vm.N)\"></i>\n </div>\n\n <!-- Content -->\n <div class=\"card-content\" (click)=\"notificationClicked(vm.N)\">\n <div class=\"card-header\">\n <h3 class=\"card-title\" [class.card-title-unread]=\"vm.N.Unread\">\n {{vm.N.Title}}\n </h3>\n <span class=\"card-time\" [title]=\"(vm.N.__mj_CreatedAt | date:'medium') ?? ''\">\n @if (vm.Relative) {\n {{vm.Relative}}\n } @else {\n <time [attr.datetime]=\"vm.N.__mj_CreatedAt\">{{vm.N.__mj_CreatedAt | date:'MMM d, h:mm a'}}</time>\n }\n </span>\n </div>\n\n <div class=\"card-meta\">\n <span class=\"card-type\">{{getTypeName(vm.N)}}</span>\n @if (vm.Clickable) {\n <span class=\"card-open-hint\">\n <i class=\"fa-solid fa-arrow-up-right-from-square\" aria-hidden=\"true\"></i> Open\n </span>\n }\n </div>\n\n @if (!IsExpanded(vm)) {\n <p class=\"card-message\">{{vm.Preview}}</p>\n } @else {\n <div class=\"card-rich\" (click)=\"$event.stopPropagation()\">\n @switch (vm.Kind) {\n @case ('html') {\n <!-- Angular sanitizes on [innerHTML] bind; chrome tags were stripped at classification -->\n <div class=\"card-rich-html\" [innerHTML]=\"vm.Body\"></div>\n }\n @case ('markdown') {\n <mj-markdown class=\"card-rich-md\" [data]=\"vm.Body\" [enableMermaid]=\"false\"></mj-markdown>\n }\n @default {\n <p class=\"card-message card-message-full\">{{vm.N.Message}}</p>\n }\n }\n </div>\n }\n\n @if (vm.Expandable) {\n <button class=\"card-expand-btn\" type=\"button\"\n (click)=\"ToggleExpanded(vm, $event)\"\n [attr.aria-expanded]=\"IsExpanded(vm)\">\n <i class=\"fa-solid\" [class.fa-chevron-down]=\"!IsExpanded(vm)\" [class.fa-chevron-up]=\"IsExpanded(vm)\" aria-hidden=\"true\"></i>\n {{ IsExpanded(vm) ? 'Show less' : 'Show more' }}\n </button>\n }\n </div>\n\n <!-- Actions -->\n <div class=\"card-actions\">\n @if (vm.N.Unread) {\n <button class=\"card-action-btn\"\n type=\"button\"\n (click)=\"markAsRead(vm.N, true, null)\"\n [attr.aria-label]=\"'Mark ' + vm.N.Title + ' as read'\"\n title=\"Mark as Read\">\n <i class=\"fa-solid fa-check\" aria-hidden=\"true\"></i>\n <span class=\"action-text\">Read</span>\n </button>\n } @else {\n <button class=\"card-action-btn\"\n type=\"button\"\n (click)=\"markAsRead(vm.N, false, null)\"\n [attr.aria-label]=\"'Mark ' + vm.N.Title + ' as unread'\"\n title=\"Mark as Unread\">\n <i class=\"fa-solid fa-envelope\" aria-hidden=\"true\"></i>\n <span class=\"action-text\">Unread</span>\n </button>\n }\n </div>\n </article>\n }\n }\n </div>\n }\n </div>\n</div>\n", styles: ["/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n User Notifications \u2014 day-grouped cards with rich (HTML/Markdown) content,\n expand/collapse, and token-driven theming (light + dark for free).\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n:host {\n display: block;\n height: 100%;\n}\n\n.notifications-container {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--mj-bg-page);\n}\n\n/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.notifications-header {\n flex-shrink: 0;\n padding: 20px 24px 12px 24px;\n background: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.header-top {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 14px;\n}\n\n.header-title {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n}\n\n.title-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n border-radius: var(--mj-radius-lg, 12px);\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-size: 16px;\n}\n\n.title-text {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.title-text h1 {\n margin: 0;\n font-size: 20px;\n font-weight: 700;\n color: var(--mj-text-primary);\n line-height: 1.2;\n}\n\n.title-subtitle {\n font-size: 12px;\n color: var(--mj-text-muted);\n}\n\n.header-actions {\n display: flex;\n gap: 8px;\n flex-shrink: 0;\n}\n\n.action-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 7px 12px;\n border-radius: var(--mj-radius-md, 8px);\n border: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n color: var(--mj-text-secondary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.15s, color 0.15s, border-color 0.15s;\n}\n\n.action-btn:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n.action-btn-ghost {\n border-color: color-mix(in srgb, var(--mj-brand-primary) 35%, transparent);\n color: var(--mj-brand-primary);\n}\n\n.action-btn-ghost:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n}\n\n/* \u2500\u2500 Filters \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.filters-section {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 10px;\n}\n\n.filters-slider {\n min-width: 0;\n}\n\n.slider-content {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n}\n\n.header-stats {\n display: inline-flex;\n gap: 4px;\n padding: 3px;\n border-radius: var(--mj-radius-full, 999px);\n background: var(--mj-bg-surface-sunken);\n}\n\n.stat-pill {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 5px 12px;\n border: none;\n border-radius: var(--mj-radius-full, 999px);\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.15s, color 0.15s;\n}\n\n.stat-pill:hover {\n color: var(--mj-text-primary);\n}\n\n.stat-pill.active {\n background: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n box-shadow: var(--mj-shadow-sm);\n}\n\n.stat-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 18px;\n height: 18px;\n padding: 0 5px;\n border-radius: var(--mj-radius-full, 999px);\n background: color-mix(in srgb, var(--mj-text-muted) 14%, transparent);\n color: var(--mj-text-secondary);\n font-size: 10px;\n font-weight: 700;\n}\n\n.stat-count.unread-count {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n color: var(--mj-brand-primary);\n}\n\n.type-select {\n padding: 7px 10px;\n border-radius: var(--mj-radius-md, 8px);\n border: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.search-section {\n flex: 1 1 220px;\n min-width: 180px;\n}\n\n.search-container {\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.search-icon {\n position: absolute;\n left: 12px;\n color: var(--mj-text-muted);\n font-size: 12px;\n pointer-events: none;\n}\n\n.search-input {\n width: 100%;\n padding: 8px 12px 8px 32px;\n border-radius: var(--mj-radius-md, 8px);\n border: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n font-size: 13px;\n}\n\n.search-input:focus {\n outline: none;\n border-color: var(--mj-border-focus);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 12%, transparent);\n}\n\n.filter-info {\n flex-basis: 100%;\n}\n\n.filter-badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 10px;\n border-radius: var(--mj-radius-full, 999px);\n background: color-mix(in srgb, var(--mj-status-info) 10%, var(--mj-bg-surface));\n color: var(--mj-status-info);\n font-size: 11px;\n font-weight: 600;\n}\n\n/* \u2500\u2500 Content / list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.notifications-content {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n padding: 16px 24px 32px 24px;\n}\n\n.notifications-list {\n display: flex;\n flex-direction: column;\n gap: 10px;\n max-width: 960px;\n margin: 0 auto;\n}\n\n.group-label {\n position: sticky;\n top: -16px;\n z-index: 1;\n padding: 10px 2px 4px 2px;\n background: var(--mj-bg-page);\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--mj-text-muted);\n}\n\n.group-label:not(:first-child) {\n margin-top: 8px;\n}\n\n/* \u2500\u2500 Card \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.notification-card {\n position: relative;\n display: flex;\n gap: 12px;\n padding: 14px 16px;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg, 12px);\n background: var(--mj-bg-surface);\n transition: border-color 0.15s, box-shadow 0.15s, background-color 0.15s;\n overflow: hidden;\n}\n\n.notification-card:hover {\n border-color: var(--mj-border-strong);\n box-shadow: var(--mj-shadow-sm);\n}\n\n.notification-card.unread {\n background: color-mix(in srgb, var(--mj-brand-primary) 3%, var(--mj-bg-surface));\n}\n\n.card-indicator {\n display: none;\n position: absolute;\n left: 0;\n top: 0;\n bottom: 0;\n width: 3px;\n background: var(--mj-brand-primary);\n border-radius: 0 2px 2px 0;\n}\n\n.notification-card.unread .card-indicator {\n display: block;\n}\n\n.card-icon {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n border-radius: var(--mj-radius-md, 10px);\n background: var(--mj-bg-surface-sunken);\n font-size: 14px;\n}\n\n.card-content {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.notification-card.clickable .card-content {\n cursor: pointer;\n}\n\n.card-header {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 12px;\n}\n\n.card-title {\n margin: 0;\n font-size: 14px;\n font-weight: 500;\n color: var(--mj-text-primary);\n overflow-wrap: anywhere;\n}\n\n.card-title-unread {\n font-weight: 700;\n}\n\n.card-time {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--mj-text-muted);\n white-space: nowrap;\n}\n\n.card-meta {\n display: flex;\n align-items: center;\n gap: 8px;\n margin: 3px 0 6px 0;\n}\n\n.card-type {\n display: inline-flex;\n padding: 2px 8px;\n border-radius: var(--mj-radius-full, 999px);\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-secondary);\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n}\n\n.card-open-hint {\n display: none;\n align-items: center;\n gap: 4px;\n font-size: 10px;\n font-weight: 600;\n color: var(--mj-brand-primary);\n}\n\n.notification-card.clickable:hover .card-open-hint {\n display: inline-flex;\n}\n\n.card-message {\n margin: 0;\n font-size: 13px;\n line-height: 1.5;\n color: var(--mj-text-secondary);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n\n.card-message-full {\n display: block;\n -webkit-line-clamp: unset;\n white-space: pre-wrap;\n}\n\n/* \u2500\u2500 Rich (expanded) content \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.card-rich {\n margin-top: 4px;\n padding: 12px 14px;\n border: 1px solid var(--mj-border-subtle);\n border-radius: var(--mj-radius-md, 8px);\n background: var(--mj-bg-surface-card);\n cursor: default;\n}\n\n/* Sanitized foreign markup: keep it inside the card no matter what it brought along */\n.card-rich-html {\n font-size: 13px;\n line-height: 1.55;\n color: var(--mj-text-primary);\n overflow-x: auto;\n}\n\n.card-rich-html img {\n max-width: 100%;\n height: auto;\n}\n\n.card-rich-html table {\n max-width: 100%;\n border-collapse: collapse;\n}\n\n.card-rich-html td,\n.card-rich-html th {\n padding: 4px 8px;\n border: 1px solid var(--mj-border-subtle);\n}\n\n.card-rich-html a {\n color: var(--mj-text-link);\n}\n\n.card-rich-html h1,\n.card-rich-html h2,\n.card-rich-html h3 {\n margin: 0.4em 0 0.3em 0;\n font-size: 1.05em;\n}\n\n.card-rich-md {\n display: block;\n font-size: 13px;\n}\n\n.card-expand-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-top: 8px;\n padding: 4px 10px;\n border: none;\n border-radius: var(--mj-radius-full, 999px);\n background: transparent;\n color: var(--mj-brand-primary);\n font-size: 11px;\n font-weight: 700;\n cursor: pointer;\n}\n\n.card-expand-btn:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n/* \u2500\u2500 Card actions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.card-actions {\n flex-shrink: 0;\n display: flex;\n align-items: flex-start;\n}\n\n.card-action-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 10px;\n border: 1px solid transparent;\n border-radius: var(--mj-radius-md, 8px);\n background: transparent;\n color: var(--mj-text-muted);\n font-size: 11px;\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.15s, color 0.15s;\n}\n\n.card-action-btn:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n border-color: var(--mj-border-default);\n}\n\n/* \u2500\u2500 Responsive \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n@media (max-width: 720px) {\n .notifications-header {\n padding: 14px 14px 10px 14px;\n }\n\n .notifications-content {\n padding: 12px 12px 24px 12px;\n }\n\n .btn-text,\n .action-text {\n display: none;\n }\n\n .card-header {\n flex-direction: column;\n gap: 2px;\n }\n\n .card-time {\n order: -1;\n }\n}\n"] }]
|
|
1086
1201
|
}], () => [{ type: i1.SharedService }, { type: i1.NavigationService }, { type: i2.ApplicationManager }], null); })();
|
|
1087
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(UserNotificationsComponent, { className: "UserNotificationsComponent", filePath: "src/lib/user-notifications/user-notifications.component.ts", lineNumber:
|
|
1202
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(UserNotificationsComponent, { className: "UserNotificationsComponent", filePath: "src/lib/user-notifications/user-notifications.component.ts", lineNumber: 104 }); })();
|
|
1088
1203
|
//# sourceMappingURL=user-notifications.component.js.map
|