Files
api/web/components/Notifications/Sidebar.vue
Eli Bosley 345e83bfb0 feat: upgrade nuxt-custom-elements (#1461)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added new modal dialogs and UI components, including activation steps,
OS update feedback, and expanded notification management.
* Introduced a plugin to configure internationalization, state
management, and Apollo client support in web components.
* Added a new Log Viewer page with a streamlined interface for viewing
logs.

* **Improvements**
* Centralized Pinia state management by consolidating all stores to use
a shared global Pinia instance.
* Simplified component templates by removing redundant
internationalization host wrappers.
* Enhanced ESLint configuration with stricter rules and global variable
declarations.
* Refined custom element build process to prevent jQuery conflicts and
optimize minification.
* Updated component imports and templates for consistent structure and
maintainability.
* Streamlined log viewer dropdowns using simplified select components
with improved formatting.
* Improved notification sidebar with filtering by importance and modular
components.
* Replaced legacy notification popups with new UI components and added
automatic root session creation for localhost requests.
* Updated OS version display and user profile UI with refined styling
and component usage.

* **Bug Fixes**
* Fixed component tag capitalization and improved type annotations
across components.

* **Chores**
* Updated development dependencies including ESLint plugins and build
tools.
* Removed deprecated log viewer patch class and cleaned up related test
fixtures.
  * Removed unused imports and simplified Apollo client setup.
* Cleaned up test mocks and removed obsolete i18n host component tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210730229632804

---------

Co-authored-by: Pujit Mehrotra <pujit@lime-technology.com>
Co-authored-by: Zack Spear <zackspear@users.noreply.github.com>
2025-07-08 10:05:39 -04:00

206 lines
6.9 KiB
Vue

<script setup lang="ts">
import { computed, ref } from 'vue';
import { useMutation, useQuery, useSubscription } from '@vue/apollo-composable';
import {
Button,
Select,
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@unraid/ui';
import { useTrackLatestSeenNotification } from '~/composables/api/use-notifications';
import { useFragment } from '~/composables/gql';
import { NotificationImportance as Importance, NotificationType } from '~/composables/gql/graphql';
import {
archiveAllNotifications,
deleteArchivedNotifications,
NOTIFICATION_FRAGMENT,
notificationsOverview,
resetOverview,
} from './graphql/notification.query';
import {
notificationAddedSubscription,
notificationOverviewSubscription,
} from './graphql/notification.subscription';
import NotificationsIndicator from './Indicator.vue';
import NotificationsList from './List.vue';
const { mutate: archiveAll, loading: loadingArchiveAll } = useMutation(archiveAllNotifications);
const { mutate: deleteArchives, loading: loadingDeleteAll } = useMutation(deleteArchivedNotifications);
const { mutate: recalculateOverview } = useMutation(resetOverview);
const importance = ref<Importance | undefined>(undefined);
const filterItems = [
{ type: 'label' as const, label: 'Notification Types' },
{ label: 'All Types', value: 'all' },
{ label: 'Alert', value: Importance.ALERT },
{ label: 'Info', value: Importance.INFO },
{ label: 'Warning', value: Importance.WARNING },
];
const confirmAndArchiveAll = async () => {
if (confirm('This will archive all notifications on your Unraid server. Continue?')) {
await archiveAll();
}
};
const confirmAndDeleteArchives = async () => {
if (
confirm(
'This will permanently delete all archived notifications currently on your Unraid server. Continue?'
)
) {
await deleteArchives();
}
};
const { result, subscribeToMore } = useQuery(notificationsOverview);
subscribeToMore({
document: notificationOverviewSubscription,
updateQuery: (prev, { subscriptionData }) => {
const snapshot = structuredClone(prev);
snapshot.notifications.overview = subscriptionData.data.notificationsOverview;
return snapshot;
},
});
const { latestNotificationTimestamp, haveSeenNotifications } = useTrackLatestSeenNotification();
const { onResult: onNotificationAdded } = useSubscription(notificationAddedSubscription);
onNotificationAdded(({ data }) => {
if (!data) return;
const notif = useFragment(NOTIFICATION_FRAGMENT, data.notificationAdded);
if (notif.type !== NotificationType.UNREAD) return;
if (notif.timestamp) {
latestNotificationTimestamp.value = notif.timestamp;
}
// probably smart to leave this log outside the if-block for the initial release
console.log('incoming notification', notif);
if (!globalThis.toast) {
return;
}
const funcMapping: Record<Importance, (typeof globalThis)['toast']['info' | 'error' | 'warning']> = {
[Importance.ALERT]: globalThis.toast.error,
[Importance.WARNING]: globalThis.toast.warning,
[Importance.INFO]: globalThis.toast.info,
};
const toast = funcMapping[notif.importance];
const createOpener = () => ({ label: 'Open', onClick: () => location.assign(notif.link as string) });
requestAnimationFrame(() =>
toast(notif.title, {
description: notif.subject,
action: notif.link ? createOpener() : undefined,
})
);
});
const overview = computed(() => {
if (!result.value) {
return;
}
return result.value.notifications.overview;
});
/** This recalculates the archived count due to notifications going to archived + unread when they are in an Unread state. */
const readArchivedCount = computed(() => {
if (!overview.value) return 0;
const { archive, unread } = overview.value;
return Math.max(0, archive.total - unread.total);
});
const prepareToViewNotifications = () => {
void recalculateOverview();
};
</script>
<template>
<Sheet>
<SheetTrigger @click="prepareToViewNotifications">
<span class="sr-only">Notifications</span>
<NotificationsIndicator :overview="overview" :seen="haveSeenNotifications" />
</SheetTrigger>
<SheetContent
side="right"
class="w-full max-w-[100vw] sm:max-w-[540px] max-h-screen h-screen min-h-screen px-0 flex flex-col gap-5 pb-0"
>
<div class="relative flex flex-col h-full w-full">
<SheetHeader class="ml-1 px-6 items-baseline gap-1 pb-2">
<SheetTitle class="text-2xl">Notifications</SheetTitle>
<a href="/Settings/Notifications">
<Button variant="link" size="sm" class="p-0 h-auto">Edit Settings</Button>
</a>
</SheetHeader>
<Tabs
default-value="unread"
class="flex flex-1 flex-col min-h-0"
aria-label="Notification filters"
>
<div class="flex flex-row justify-between items-center flex-wrap gap-5 px-6">
<TabsList class="flex" aria-label="Filter notifications by status">
<TabsTrigger value="unread">
Unread <span v-if="overview">({{ overview.unread.total }})</span>
</TabsTrigger>
<TabsTrigger value="archived">
Archived
<span v-if="overview">({{ readArchivedCount }})</span>
</TabsTrigger>
</TabsList>
<TabsContent value="unread" class="flex-col items-end">
<Button
:disabled="loadingArchiveAll"
variant="link"
size="sm"
class="text-foreground hover:text-destructive transition-none"
@click="confirmAndArchiveAll"
>
Archive All
</Button>
</TabsContent>
<TabsContent value="archived" class="flex-col items-end">
<Button
:disabled="loadingDeleteAll"
variant="link"
size="sm"
class="text-foreground hover:text-destructive transition-none"
@click="confirmAndDeleteArchives"
>
Delete All
</Button>
</TabsContent>
<Select
:items="filterItems"
placeholder="Filter By"
class="h-auto"
@update:model-value="
(val: unknown) => {
const strVal = String(val);
importance = strVal === 'all' || !strVal ? undefined : (strVal as Importance);
}
"
></Select>
</div>
<TabsContent value="unread" class="flex-col flex-1 min-h-0">
<NotificationsList :importance="importance" :type="NotificationType.UNREAD" />
</TabsContent>
<TabsContent value="archived" class="flex-col flex-1 min-h-0">
<NotificationsList :importance="importance" :type="NotificationType.ARCHIVE" />
</TabsContent>
</Tabs>
</div>
</SheetContent>
</Sheet>
</template>