@apptegy/nuxt-navigation 0.1.114 → 0.1.116

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/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=3.0.0"
5
5
  },
6
6
  "configKey": "@apptegy/nuxt-navigation",
7
- "version": "0.1.114",
7
+ "version": "0.1.116",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.1",
10
10
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { defineNuxtModule, createResolver, addComponentsDir, addImportsDir } from '@nuxt/kit';
2
2
 
3
- const module$1 = defineNuxtModule({
3
+ const module = defineNuxtModule({
4
4
  meta: {
5
5
  name: "@apptegy/nuxt-navigation",
6
6
  compatibility: {
@@ -22,4 +22,4 @@ const module$1 = defineNuxtModule({
22
22
  }
23
23
  });
24
24
 
25
- export { module$1 as default };
25
+ export { module as default };
@@ -148,12 +148,7 @@ import {
148
148
  } from "../../utils/notification-api";
149
149
  const props = defineProps({
150
150
  sticky: { type: Boolean, required: false },
151
- showNotifications: { type: Boolean, required: false, default: false },
152
- notificationsResponse: { type: [Object, null], required: false, default: null },
153
- notificationsLoading: { type: Boolean, required: false, default: false },
154
- notificationPanelOpen: { type: Boolean, required: false, default: false },
155
- activeTabId: { type: String, required: false, default: "" },
156
- activeFilterId: { type: String, required: false, default: "" },
151
+ notifications: { type: [Object, null], required: false, default: null },
157
152
  user: { type: Object, required: true },
158
153
  impersonatedUser: { type: Object, required: false },
159
154
  useLogoutEmit: { type: Boolean, required: false },
@@ -163,9 +158,7 @@ const nuxtApp = useNuxtApp();
163
158
  const emit = defineEmits([
164
159
  "selected-option",
165
160
  "user-logout",
166
- "update:notificationPanelOpen",
167
- "update:activeTabId",
168
- "update:activeFilterId",
161
+ "update:notifications",
169
162
  "notifications-clear-all",
170
163
  "notifications-dismiss",
171
164
  "notifications-select",
@@ -173,8 +166,13 @@ const emit = defineEmits([
173
166
  ]);
174
167
  const popoverRef = ref(null);
175
168
  const isMobilePanel = useMediaQuery("(max-width: 960px)");
176
- const panelOpen = ref(props.notificationPanelOpen);
177
169
  const isBellShaking = ref(false);
170
+ const showNotifications = computed(() => props.notifications != null);
171
+ const notificationsResponse = computed(() => props.notifications?.response ?? null);
172
+ const notificationsLoading = computed(() => props.notifications?.loading ?? false);
173
+ const panelOpen = ref(props.notifications?.panelOpen ?? false);
174
+ const activeTabId = ref(props.notifications?.activeTabId || "new");
175
+ const activeFilterId = ref(props.notifications?.activeFilterId || "all-types");
178
176
  const popoverPt = {
179
177
  content: {
180
178
  class: "notifications-popover__content",
@@ -188,10 +186,10 @@ const notificationDateLabels = computed(() => ({
188
186
  justNow: nuxtApp.$t("navigation.notificationsPanel.justNow")
189
187
  }));
190
188
  const notificationsPanel = computed(() => {
191
- if (!props.notificationsResponse) {
189
+ if (!notificationsResponse.value) {
192
190
  return null;
193
191
  }
194
- return mapNotificationsApiResponse(props.notificationsResponse, {
192
+ return mapNotificationsApiResponse(notificationsResponse.value, {
195
193
  locale: nuxtApp.$intl.locale,
196
194
  dateLabels: notificationDateLabels.value,
197
195
  retentionLabel: nuxtApp.$t("navigation.notificationsPanel.retentionLabel"),
@@ -202,10 +200,17 @@ const notificationsPanel = computed(() => {
202
200
  });
203
201
  });
204
202
  const notificationCount = computed(
205
- () => getNotificationsTotalCount(props.notificationsResponse)
203
+ () => getNotificationsTotalCount(notificationsResponse.value)
206
204
  );
207
- const activeTabId = ref(props.activeTabId || notificationsPanel.value?.tabs[0]?.id || "");
208
- const activeFilterId = ref(props.activeFilterId || notificationsPanel.value?.filters?.[0]?.id || "all-types");
205
+ function patchNotifications(patch) {
206
+ if (!props.notifications) {
207
+ return;
208
+ }
209
+ emit("update:notifications", {
210
+ ...props.notifications,
211
+ ...patch
212
+ });
213
+ }
209
214
  watch(
210
215
  notificationCount,
211
216
  (newCount, oldCount) => {
@@ -223,19 +228,21 @@ function onBellShakeEnd() {
223
228
  isBellShaking.value = false;
224
229
  }
225
230
  watch(
226
- () => props.notificationPanelOpen,
231
+ () => props.notifications?.panelOpen,
227
232
  (value) => {
228
- panelOpen.value = value;
233
+ if (value !== void 0) {
234
+ panelOpen.value = value;
235
+ }
229
236
  }
230
237
  );
231
238
  watch(panelOpen, (value) => {
232
- emit("update:notificationPanelOpen", value);
239
+ patchNotifications({ panelOpen: value });
233
240
  if (!value && !isMobilePanel.value) {
234
241
  popoverRef.value?.hide();
235
242
  }
236
243
  });
237
244
  watch(
238
- () => props.activeTabId,
245
+ () => props.notifications?.activeTabId,
239
246
  (value) => {
240
247
  if (value) {
241
248
  activeTabId.value = value;
@@ -243,7 +250,7 @@ watch(
243
250
  }
244
251
  );
245
252
  watch(
246
- () => props.activeFilterId,
253
+ () => props.notifications?.activeFilterId,
247
254
  (value) => {
248
255
  if (value) {
249
256
  activeFilterId.value = value;
@@ -279,11 +286,11 @@ function handleNotificationSelect(payload) {
279
286
  }
280
287
  function handleTabChange(id) {
281
288
  activeTabId.value = id;
282
- emit("update:activeTabId", id);
289
+ patchNotifications({ activeTabId: id });
283
290
  }
284
291
  function handleFilterChange(id) {
285
292
  activeFilterId.value = id;
286
- emit("update:activeFilterId", id);
293
+ patchNotifications({ activeFilterId: id });
287
294
  }
288
295
  </script>
289
296
 
@@ -1,12 +1,10 @@
1
- import type { INotificationsApiResponse, IUser } from '../../types/index.js';
1
+ import type { INotificationsHeaderModel, IUser } from '../../types/index.js';
2
2
  interface IHeaderProps {
3
3
  sticky?: boolean;
4
- showNotifications?: boolean;
5
- notificationsResponse?: INotificationsApiResponse | null;
6
- notificationsLoading?: boolean;
7
- notificationPanelOpen?: boolean;
8
- activeTabId?: string;
9
- activeFilterId?: string;
4
+ /**
5
+ * Host-owned notification center state. Pass `null`/`undefined` to hide the bell.
6
+ */
7
+ notifications?: INotificationsHeaderModel | null;
10
8
  }
11
9
  interface IUserDropdown {
12
10
  user: {
@@ -29,13 +27,24 @@ type __VLS_Slots = {} & {
29
27
  } & {
30
28
  notifications?: (props: typeof __VLS_15) => any;
31
29
  };
32
- declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, any, string, import("vue").PublicProps, any, {
33
- showNotifications: boolean;
34
- notificationsResponse: INotificationsApiResponse | null;
35
- notificationsLoading: boolean;
36
- notificationPanelOpen: boolean;
37
- activeTabId: string;
38
- activeFilterId: string;
30
+ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
31
+ "selected-option": (...args: any[]) => void;
32
+ "user-logout": (...args: any[]) => void;
33
+ "update:notifications": (...args: any[]) => void;
34
+ "notifications-clear-all": (...args: any[]) => void;
35
+ "notifications-dismiss": (...args: any[]) => void;
36
+ "notifications-select": (...args: any[]) => void;
37
+ "notification-click": (...args: any[]) => void;
38
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
39
+ "onSelected-option"?: ((...args: any[]) => any) | undefined;
40
+ "onUser-logout"?: ((...args: any[]) => any) | undefined;
41
+ "onUpdate:notifications"?: ((...args: any[]) => any) | undefined;
42
+ "onNotifications-clear-all"?: ((...args: any[]) => any) | undefined;
43
+ "onNotifications-dismiss"?: ((...args: any[]) => any) | undefined;
44
+ "onNotifications-select"?: ((...args: any[]) => any) | undefined;
45
+ "onNotification-click"?: ((...args: any[]) => any) | undefined;
46
+ }>, {
47
+ notifications: INotificationsHeaderModel | null;
39
48
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
40
49
  declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
41
50
  export default _default;
@@ -38,10 +38,6 @@
38
38
 
39
39
  <div class="notification-item__content">
40
40
  <p class="notification-item__message">
41
- <strong>{{ primaryActor.name }}</strong>
42
- <template v-if="additionalActorsLabel">
43
- {{ additionalActorsLabel }}
44
- </template>
45
41
  {{ item.actionText }}
46
42
  </p>
47
43
  <p class="notification-item__meta">
@@ -106,16 +102,6 @@ const groupBadgeLabel = computed(() => {
106
102
  const count = props.item.groupCount ?? Math.max(props.item.actors.length - 1, 0);
107
103
  return count > 0 ? `+${count}` : "";
108
104
  });
109
- const additionalActorsLabel = computed(() => {
110
- if (props.item.groupCount && props.item.groupCount > 0) {
111
- return ` and ${props.item.groupCount} others`;
112
- }
113
- if (props.item.actors.length <= 1) {
114
- return "";
115
- }
116
- const names = props.item.actors.slice(1).map((actor) => actor.name);
117
- return `, ${names.join(", ")}`;
118
- });
119
105
  function svgIcon(paths) {
120
106
  return h(
121
107
  "svg",
@@ -220,7 +206,7 @@ function handleDismiss(event) {
220
206
  text-align: left;
221
207
  color: inherit;
222
208
  }
223
- .notification-item__main:hover .notification-item__message strong {
209
+ .notification-item__main:hover .notification-item__message {
224
210
  color: var(--purple-60, #662e80);
225
211
  }
226
212
  .notification-item__main:focus-visible {
@@ -297,9 +283,6 @@ function handleDismiss(event) {
297
283
  line-height: 20px;
298
284
  color: var(--grey-90, #111827);
299
285
  }
300
- .notification-item__message strong {
301
- font-weight: 600;
302
- }
303
286
  .notification-item__meta {
304
287
  display: flex;
305
288
  flex-wrap: wrap;
@@ -3,10 +3,10 @@ type __VLS_Props = {
3
3
  item: INotificationItem;
4
4
  };
5
5
  declare const _default: import("vue").DefineComponent<__VLS_Props, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
- dismiss: (id: string) => any;
7
6
  select: (item: INotificationItem) => any;
7
+ dismiss: (id: string) => any;
8
8
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
9
- onDismiss?: ((id: string) => any) | undefined;
10
9
  onSelect?: ((item: INotificationItem) => any) | undefined;
10
+ onDismiss?: ((id: string) => any) | undefined;
11
11
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
12
12
  export default _default;
@@ -185,7 +185,7 @@ import {
185
185
  getVisibleFilters,
186
186
  isInBadge,
187
187
  notificationMatchesFilter,
188
- resolveNotificationFilters,
188
+ resolveStickyNotificationFilters,
189
189
  shouldShowFilterChips
190
190
  } from "../../utils/notifications";
191
191
  const props = defineProps({
@@ -200,6 +200,7 @@ const emit = defineEmits(["update:activeTabId", "update:activeFilterId", "clear-
200
200
  const filtersRef = ref();
201
201
  const activeTabId = ref(props.activeTabId || props.data.tabs[0]?.id || "");
202
202
  const activeFilterId = ref(props.activeFilterId || props.data.filters?.[0]?.id || "all-types");
203
+ const stickyFilters = ref([]);
203
204
  watch(
204
205
  () => props.activeTabId,
205
206
  (value) => {
@@ -230,19 +231,44 @@ const usesDynamicFilters = computed(() => !props.data.filters?.length);
230
231
  const activeTab = computed(
231
232
  () => props.data.tabs.find((tab) => tab.id === activeTabId.value)
232
233
  );
233
- const resolvedFilters = computed(
234
- () => resolveNotificationFilters(allNotifications.value, activeTab.value, props.data.filters)
234
+ function filterCatalogKey(filters) {
235
+ return filters.map((filter) => filter.id).join("|");
236
+ }
237
+ watch(
238
+ [allNotifications, activeTab, activeFilterId, () => props.data.filters],
239
+ () => {
240
+ const { nextStickyFilters } = resolveStickyNotificationFilters({
241
+ notifications: allNotifications.value,
242
+ tab: activeTab.value,
243
+ providedFilters: props.data.filters,
244
+ activeFilterId: activeFilterId.value,
245
+ stickyFilters: stickyFilters.value
246
+ });
247
+ if (filterCatalogKey(stickyFilters.value) !== filterCatalogKey(nextStickyFilters)) {
248
+ stickyFilters.value = nextStickyFilters;
249
+ }
250
+ },
251
+ { immediate: true, deep: true }
252
+ );
253
+ const displayFilters = computed(
254
+ () => resolveStickyNotificationFilters({
255
+ notifications: allNotifications.value,
256
+ tab: activeTab.value,
257
+ providedFilters: props.data.filters,
258
+ activeFilterId: activeFilterId.value,
259
+ stickyFilters: stickyFilters.value
260
+ }).filters
235
261
  );
236
262
  const visibleFilters = computed(() => {
237
263
  if (usesDynamicFilters.value) {
238
- return resolvedFilters.value;
264
+ return displayFilters.value;
239
265
  }
240
- return getVisibleFilters(allNotifications.value, resolvedFilters.value, activeTab.value);
266
+ return getVisibleFilters(allNotifications.value, displayFilters.value, activeTab.value);
241
267
  });
242
268
  const showFilterChips = computed(
243
269
  () => shouldShowFilterChips(
244
270
  allNotifications.value,
245
- resolvedFilters.value,
271
+ displayFilters.value,
246
272
  activeTab.value,
247
273
  usesDynamicFilters.value
248
274
  )
@@ -269,7 +295,7 @@ function formatCount(count) {
269
295
  return formatNotificationCount(count);
270
296
  }
271
297
  const activeFilter = computed(
272
- () => visibleFilters.value.find((filter) => filter.id === activeFilterId.value) || resolvedFilters.value.find((filter) => filter.id === activeFilterId.value)
298
+ () => visibleFilters.value.find((filter) => filter.id === activeFilterId.value) || displayFilters.value.find((filter) => filter.id === activeFilterId.value)
273
299
  );
274
300
  function showClearAll(groupIndex) {
275
301
  return groupIndex === 0 && (activeTabId.value === "new" || activeTab.value?.unreadOnly);
@@ -8,6 +8,11 @@ type __VLS_Props = {
8
8
  mobile?: boolean;
9
9
  };
10
10
  declare const _default: import("vue").DefineComponent<__VLS_Props, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
+ select: (payload: {
12
+ item: INotificationsPanelData["groups"][number]["notifications"][number];
13
+ tabId: string;
14
+ filterId: string;
15
+ }) => any;
11
16
  "update:activeTabId": (id: string) => any;
12
17
  "update:activeFilterId": (id: string) => any;
13
18
  "clear-all": (payload: {
@@ -20,13 +25,13 @@ declare const _default: import("vue").DefineComponent<__VLS_Props, void, {}, {},
20
25
  tabId: string;
21
26
  filterId: string;
22
27
  }) => any;
23
- select: (payload: {
28
+ close: () => any;
29
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
30
+ onSelect?: ((payload: {
24
31
  item: INotificationsPanelData["groups"][number]["notifications"][number];
25
32
  tabId: string;
26
33
  filterId: string;
27
- }) => any;
28
- close: () => any;
29
- }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
34
+ }) => any) | undefined;
30
35
  "onUpdate:activeTabId"?: ((id: string) => any) | undefined;
31
36
  "onUpdate:activeFilterId"?: ((id: string) => any) | undefined;
32
37
  "onClear-all"?: ((payload: {
@@ -39,11 +44,6 @@ declare const _default: import("vue").DefineComponent<__VLS_Props, void, {}, {},
39
44
  tabId: string;
40
45
  filterId: string;
41
46
  }) => any) | undefined;
42
- onSelect?: ((payload: {
43
- item: INotificationsPanelData["groups"][number]["notifications"][number];
44
- tabId: string;
45
- filterId: string;
46
- }) => any) | undefined;
47
47
  onClose?: (() => any) | undefined;
48
48
  }>, {
49
49
  loading: boolean;
@@ -1,5 +1,5 @@
1
1
  export declare function useSidebar(): {
2
- state: any;
2
+ state: import("vue").Ref<boolean, boolean>;
3
3
  toggleSidebar: () => void;
4
4
  sidebarWidth: import("vue").ComputedRef<"264px" | "0px">;
5
5
  };
@@ -6,14 +6,16 @@ export declare const NOTIFICATION_BADGE_COLOR = "#C84B31";
6
6
  export declare const NOTIFICATION_FILTER_CATEGORIES: Array<{
7
7
  id: string;
8
8
  label: string;
9
- types: NotificationType[];
9
+ kinds: string[];
10
10
  }>;
11
11
  /** @deprecated Use NOTIFICATION_FILTER_CATEGORIES */
12
12
  export declare const DEFAULT_NOTIFICATION_FILTERS: ({
13
13
  id: string;
14
14
  label: string;
15
- types: NotificationType[];
15
+ kinds: string[];
16
16
  } | {
17
17
  id: string;
18
18
  label: string;
19
19
  })[];
20
+ /** Resolve `filter[kind][]` values for a notification center filter pill id. */
21
+ export declare function getKindsForNotificationFilter(filterId: string | null | undefined): string[];
@@ -14,35 +14,42 @@ export const NOTIFICATION_FILTER_CATEGORIES = [
14
14
  {
15
15
  id: "announcements",
16
16
  label: "Announcements",
17
- types: ["announcement"]
17
+ kinds: ["announcement"]
18
18
  },
19
19
  {
20
20
  id: "messages",
21
21
  label: "Messages",
22
- types: ["message"]
22
+ kinds: ["message"]
23
23
  },
24
24
  {
25
- id: "comments-mentions",
26
- label: "Comments & Mentions",
27
- types: ["comment", "mention", "reply", "reaction"]
25
+ id: "comments-reactions",
26
+ label: "Comments & Reactions",
27
+ kinds: ["comment", "reaction"]
28
28
  },
29
29
  {
30
30
  id: "school-posts",
31
31
  label: "School Posts",
32
- types: ["school-post"]
32
+ kinds: ["school_post"]
33
33
  },
34
34
  {
35
35
  id: "system",
36
36
  label: "System",
37
- types: ["system"]
37
+ kinds: ["system"]
38
38
  },
39
39
  {
40
40
  id: "other",
41
41
  label: "Other",
42
- types: ["unknown"]
42
+ kinds: ["unknown"]
43
43
  }
44
44
  ];
45
45
  export const DEFAULT_NOTIFICATION_FILTERS = [
46
46
  { id: "all-types", label: "All types" },
47
47
  ...NOTIFICATION_FILTER_CATEGORIES
48
48
  ];
49
+ export function getKindsForNotificationFilter(filterId) {
50
+ if (!filterId || filterId === "all-types") {
51
+ return [];
52
+ }
53
+ const category = NOTIFICATION_FILTER_CATEGORIES.find((filter) => filter.id === filterId);
54
+ return category?.kinds ? [...category.kinds] : [];
55
+ }
@@ -11,4 +11,4 @@ export interface IUser {
11
11
  client: string;
12
12
  admin_email: string;
13
13
  }
14
- export type { NotificationType, NotificationApiState, INotificationApiActor, INotificationApiRecord, INotificationsPagination, INotificationsApiResponse, INotificationActor, INotificationItem, INotificationGroup, INotificationTab, INotificationFilter, INotificationsPanelData, } from './notifications.js';
14
+ export type { NotificationType, NotificationApiState, INotificationApiActor, INotificationApiRecord, INotificationsPagination, INotificationsApiResponse, INotificationActor, INotificationItem, INotificationGroup, INotificationTab, INotificationFilter, INotificationsPanelData, INotificationsHeaderModel, } from './notifications.js';
@@ -7,6 +7,11 @@ export interface INotificationApiActor {
7
7
  }
8
8
  export interface INotificationApiRecord {
9
9
  id: number | string;
10
+ /**
11
+ * Category used for filter pills / `filter[kind][]` (e.g. announcement, comment, reaction).
12
+ * Distinct from `source_record_type`, which describes the underlying record.
13
+ */
14
+ kind?: string | null;
10
15
  source_record_type: string;
11
16
  title: string;
12
17
  summary?: string;
@@ -33,7 +38,13 @@ export interface INotificationActor {
33
38
  }
34
39
  export interface INotificationItem {
35
40
  id: string;
41
+ /** Visual/icon type — derived from `source_record_type`. */
36
42
  type: NotificationType;
43
+ /**
44
+ * Filter category — derived from API `kind` (falls back to `type` when kind is absent).
45
+ * Used by filter pills and `filter[kind][]`.
46
+ */
47
+ kind: string;
37
48
  actors: INotificationActor[];
38
49
  actionText: string;
39
50
  timestampLabel: string;
@@ -67,7 +78,15 @@ export interface INotificationFilter {
67
78
  id: string;
68
79
  label: string;
69
80
  count?: number;
70
- /** Notification types included in this filter. Omit or leave empty to include all types. */
81
+ /**
82
+ * Notification kinds included in this filter (`filter[kind][]`).
83
+ * Omit or leave empty to include all kinds.
84
+ */
85
+ kinds?: string[];
86
+ /**
87
+ * @deprecated Prefer `kinds`. Kept for callers that still filter by
88
+ * `source_record_type`-derived `type`.
89
+ */
71
90
  types?: NotificationType[];
72
91
  }
73
92
  export interface INotificationsPanelData {
@@ -77,3 +96,11 @@ export interface INotificationsPanelData {
77
96
  filters?: INotificationFilter[];
78
97
  groups: INotificationGroup[];
79
98
  }
99
+ /** Host-owned notification center state bound to NavHeader via `v-model:notifications`. */
100
+ export interface INotificationsHeaderModel {
101
+ response: INotificationsApiResponse | null;
102
+ loading?: boolean;
103
+ panelOpen?: boolean;
104
+ activeTabId?: string;
105
+ activeFilterId?: string;
106
+ }
@@ -1,7 +1,10 @@
1
1
  import type { INotificationApiActor, INotificationApiRecord, INotificationGroup, INotificationItem, INotificationsApiResponse, INotificationsPanelData, INotificationTab, NotificationType } from '../types/index.js';
2
2
  import { type INotificationDateLabels } from './notification-dates.js';
3
3
  export declare const DEFAULT_NOTIFICATION_TABS: INotificationTab[];
4
+ /** Normalize kind values for filter matching and `filter[kind][]` query params. */
5
+ export declare function normalizeNotificationKind(value: string): string;
4
6
  export declare function mapSourceRecordType(sourceRecordType: string): NotificationType;
7
+ export declare function mapNotificationKind(kind: string | null | undefined, fallbackType: NotificationType): string;
5
8
  export declare function mapNotificationApiActor(actor: INotificationApiActor): {
6
9
  name: string;
7
10
  avatarUrl: string | undefined;
@@ -19,10 +19,19 @@ export const DEFAULT_NOTIFICATION_TABS = [
19
19
  { id: "new", label: "New", unreadOnly: true },
20
20
  { id: "all", label: "All" }
21
21
  ];
22
+ export function normalizeNotificationKind(value) {
23
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/-/g, "_");
24
+ }
22
25
  export function mapSourceRecordType(sourceRecordType) {
23
26
  const normalizedSourceRecordType = sourceRecordType.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
24
27
  return SOURCE_RECORD_TYPE_MAP[normalizedSourceRecordType] ?? "unknown";
25
28
  }
29
+ export function mapNotificationKind(kind, fallbackType) {
30
+ if (!kind) {
31
+ return normalizeNotificationKind(fallbackType);
32
+ }
33
+ return normalizeNotificationKind(kind) || normalizeNotificationKind(fallbackType);
34
+ }
26
35
  export function mapNotificationApiActor(actor) {
27
36
  const name = [actor.first_name, actor.last_name].filter(Boolean).join(" ").trim();
28
37
  return {
@@ -32,9 +41,11 @@ export function mapNotificationApiActor(actor) {
32
41
  }
33
42
  export function mapNotificationApiRecord(record, options) {
34
43
  const state = record.notification_state;
44
+ const type = mapSourceRecordType(record.source_record_type);
35
45
  return {
36
46
  id: String(record.id),
37
- type: mapSourceRecordType(record.source_record_type),
47
+ type,
48
+ kind: mapNotificationKind(record.kind, type),
38
49
  actors: (record.actors ?? []).map(mapNotificationApiActor),
39
50
  actionText: record.title,
40
51
  timestampLabel: formatNotificationTimeAgo(record.created_at, {
@@ -14,6 +14,20 @@ export declare function getScopedNotifications(notifications: INotificationItem[
14
14
  */
15
15
  export declare function buildFiltersFromNotifications(notifications: INotificationItem[], tab: INotificationTab | undefined): INotificationFilter[];
16
16
  export declare function resolveNotificationFilters(notifications: INotificationItem[], tab: INotificationTab | undefined, filters?: INotificationFilter[]): INotificationFilter[];
17
+ /**
18
+ * Keeps the last "all types" pill catalog while a kind filter is active, so chips
19
+ * don't collapse when results are already filtered (e.g. via `filter[kind][]`).
20
+ */
21
+ export declare function resolveStickyNotificationFilters({ notifications, tab, providedFilters, activeFilterId, stickyFilters, }: {
22
+ notifications: INotificationItem[];
23
+ tab: INotificationTab | undefined;
24
+ providedFilters?: INotificationFilter[];
25
+ activeFilterId: string;
26
+ stickyFilters: INotificationFilter[];
27
+ }): {
28
+ filters: INotificationFilter[];
29
+ nextStickyFilters: INotificationFilter[];
30
+ };
17
31
  export declare function filterHasContent(notifications: INotificationItem[], filter: INotificationFilter, tab: INotificationTab | undefined): boolean;
18
32
  export declare function getVisibleFilters(notifications: INotificationItem[], filters: INotificationFilter[], tab: INotificationTab | undefined): INotificationFilter[];
19
33
  export declare function shouldShowFilterChips(notifications: INotificationItem[], filters: INotificationFilter[], tab: INotificationTab | undefined, usesDynamicFilters?: boolean): boolean;
@@ -1,4 +1,8 @@
1
- import { NOTIFICATION_FILTER_CATEGORIES } from "../constants/notification-types.js";
1
+ import {
2
+ DEFAULT_NOTIFICATION_FILTERS,
3
+ NOTIFICATION_FILTER_CATEGORIES
4
+ } from "../constants/notification-types.js";
5
+ import { normalizeNotificationKind } from "./notification-api.js";
2
6
  export function flattenNotifications(groups) {
3
7
  return groups.flatMap((group) => group.notifications);
4
8
  }
@@ -23,11 +27,19 @@ export function getInBadgeNotifications(notifications) {
23
27
  export function getInBadgeCount(notifications) {
24
28
  return getInBadgeNotifications(notifications).length;
25
29
  }
30
+ function filterKinds(filter) {
31
+ if (filter.kinds?.length) {
32
+ return filter.kinds.map(normalizeNotificationKind);
33
+ }
34
+ return (filter.types ?? []).map((type) => normalizeNotificationKind(type));
35
+ }
26
36
  export function notificationMatchesFilter(notification, filter) {
27
- if (!filter.types?.length) {
37
+ const kinds = filterKinds(filter);
38
+ if (!kinds.length) {
28
39
  return true;
29
40
  }
30
- return filter.types.includes(notification.type);
41
+ const notificationKind = normalizeNotificationKind(notification.kind || notification.type);
42
+ return kinds.includes(notificationKind);
31
43
  }
32
44
  export function getTabBadgeCount(notifications, tab) {
33
45
  if (tab.unreadOnly || tab.id === "new") {
@@ -36,7 +48,7 @@ export function getTabBadgeCount(notifications, tab) {
36
48
  return 0;
37
49
  }
38
50
  export function getFilterBadgeCount(notifications, filter) {
39
- if (!filter.types?.length || filter.id === "all-types") {
51
+ if (filter.id === "all-types" || !filterKinds(filter).length) {
40
52
  return 0;
41
53
  }
42
54
  return getInBadgeNotifications(notifications).filter(
@@ -51,9 +63,13 @@ export function getScopedNotifications(notifications, tab) {
51
63
  }
52
64
  export function buildFiltersFromNotifications(notifications, tab) {
53
65
  const scoped = getScopedNotifications(notifications, tab);
54
- const typesPresent = new Set(scoped.map((notification) => notification.type));
66
+ const kindsPresent = new Set(
67
+ scoped.map(
68
+ (notification) => normalizeNotificationKind(notification.kind || notification.type)
69
+ )
70
+ );
55
71
  const categories = NOTIFICATION_FILTER_CATEGORIES.filter(
56
- (category) => category.types.some((type) => typesPresent.has(type))
72
+ (category) => category.kinds.some((kind) => kindsPresent.has(normalizeNotificationKind(kind)))
57
73
  );
58
74
  if (categories.length < 2) {
59
75
  return [];
@@ -69,6 +85,37 @@ export function resolveNotificationFilters(notifications, tab, filters) {
69
85
  }
70
86
  return buildFiltersFromNotifications(notifications, tab);
71
87
  }
88
+ export function resolveStickyNotificationFilters({
89
+ notifications,
90
+ tab,
91
+ providedFilters,
92
+ activeFilterId,
93
+ stickyFilters
94
+ }) {
95
+ if (providedFilters?.length) {
96
+ return {
97
+ filters: providedFilters,
98
+ nextStickyFilters: stickyFilters
99
+ };
100
+ }
101
+ const dynamicFilters = buildFiltersFromNotifications(notifications, tab);
102
+ let nextStickyFilters = stickyFilters;
103
+ if (activeFilterId === "all-types") {
104
+ nextStickyFilters = dynamicFilters;
105
+ } else if (!stickyFilters.length) {
106
+ nextStickyFilters = dynamicFilters.length ? dynamicFilters : [...DEFAULT_NOTIFICATION_FILTERS];
107
+ }
108
+ if (activeFilterId !== "all-types" && nextStickyFilters.length) {
109
+ return {
110
+ filters: nextStickyFilters,
111
+ nextStickyFilters
112
+ };
113
+ }
114
+ return {
115
+ filters: dynamicFilters.length ? dynamicFilters : nextStickyFilters,
116
+ nextStickyFilters
117
+ };
118
+ }
72
119
  export function filterHasContent(notifications, filter, tab) {
73
120
  const scoped = getScopedNotifications(notifications, tab);
74
121
  return scoped.some((notification) => notificationMatchesFilter(notification, filter));
@@ -76,7 +123,10 @@ export function filterHasContent(notifications, filter, tab) {
76
123
  export function getVisibleFilters(notifications, filters, tab) {
77
124
  return filters.filter((filter) => {
78
125
  if (filter.id === "other") {
79
- return notifications.some((notification) => notification.type === "unknown");
126
+ return notifications.some((notification) => {
127
+ const kind = normalizeNotificationKind(notification.kind || notification.type);
128
+ return kind === "unknown" || notification.type === "unknown";
129
+ });
80
130
  }
81
131
  return true;
82
132
  }).filter((filter) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apptegy/nuxt-navigation",
3
- "version": "0.1.114",
3
+ "version": "0.1.116",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -36,20 +36,19 @@
36
36
  "primevue": "^4.2.5"
37
37
  },
38
38
  "devDependencies": {
39
- "@nuxt/devtools": "^2.6.2",
40
- "@nuxt/eslint-config": "^1.6.0",
41
- "@nuxt/module-builder": "^1.0.1",
42
- "@nuxt/schema": "^3.17.0",
43
- "@nuxt/test-utils": "^3.17.0",
44
- "@types/node": "^22.0.0",
45
- "eslint": "^9.31.0",
46
- "nuxt": "^3.17.0",
39
+ "@nuxt/devtools": "catalog:",
40
+ "@nuxt/eslint-config": "catalog:",
41
+ "@nuxt/module-builder": "catalog:",
42
+ "@nuxt/schema": "catalog:",
43
+ "@nuxt/test-utils": "catalog:",
44
+ "@types/node": "catalog:",
45
+ "eslint": "catalog:",
46
+ "nuxt": "catalog:",
47
47
  "primevue": "^4.5.4",
48
- "sass": "^1.89.2",
49
- "typescript": "~5.8.0",
50
- "vitest": "^3.2.4",
51
- "vue": "^3.5.13",
52
- "vue-tsc": "^3.0.3"
53
- },
54
- "gitHead": "d57420f3128d73ad4ee181b2c437040434c0023c"
48
+ "sass": "catalog:",
49
+ "typescript": "catalog:",
50
+ "vitest": "catalog:",
51
+ "vue": "catalog:",
52
+ "vue-tsc": "catalog:"
53
+ }
55
54
  }