diff --git a/frontend/src/lib/email/EmailVerificationStatus.svelte b/frontend/src/lib/email/EmailVerificationStatus.svelte
index d977d148c..65eb64b61 100644
--- a/frontend/src/lib/email/EmailVerificationStatus.svelte
+++ b/frontend/src/lib/email/EmailVerificationStatus.svelte
@@ -1,9 +1,9 @@
diff --git a/frontend/src/lib/notify/Notify.svelte b/frontend/src/lib/notify/Notify.svelte
index 39dc05c2b..1d0bd4ca7 100644
--- a/frontend/src/lib/notify/Notify.svelte
+++ b/frontend/src/lib/notify/Notify.svelte
@@ -1,8 +1,10 @@
{#if $notifications.length > 0}
diff --git a/frontend/src/lib/notify/index.ts b/frontend/src/lib/notify/index.ts
index a4519a796..bed372ffc 100644
--- a/frontend/src/lib/notify/index.ts
+++ b/frontend/src/lib/notify/index.ts
@@ -1,6 +1,7 @@
-import { readonly, writable } from 'svelte/store';
+import { writable, type Readable, type Writable } from 'svelte/store';
import { Duration } from '$lib/util/time';
+import { defineContext } from '$lib/util/context';
export interface Notification {
message: string;
@@ -8,36 +9,39 @@ export interface Notification {
duration: number;
}
-const _notifications = writable([]);
-// _notifications.set([{ message: 'Test notification', duration: 4 }, { message: 'Test notification', duration: 4 }])
+export const { use: useNotifications, init: initNotificationService } =
+ defineContext(() => new NotificationService(writable([])));
-export const notifications = readonly(_notifications);
+export class NotificationService {
-export function notifySuccess(
- message: string,
- duration = Duration.Default,
-): void {
- addNotification({ message, duration });
-}
+ get notifications(): Readable {
+ return this._notifications;
+ }
-export function notifyWarning( // in case we need them to be different colors in the future this is its own function
- message: string,
- duration = Duration.Default,
-): void {
- addNotification({ message, duration, category: 'alert-warning' });
-}
+ constructor(private readonly _notifications: Writable) {
+ // _notifications.set([{ message: 'Test notification', duration: 4 }, { message: 'Test notification', duration: 4 }])
+ }
-function addNotification(notification: Notification): void {
- _notifications.update((currentNotifications) => [...currentNotifications, notification]);
- setTimeout(() => {
- removeNotification(notification);
- }, notification.duration);
-}
+ notifySuccess = (message: string, duration = Duration.Default): void => {
+ this.addNotification({ message, duration });
+ }
-export function removeNotification(notification: Notification): void {
- _notifications.update((currentNotifications) => currentNotifications.filter((n: Notification) => n !== notification));
-}
+ notifyWarning = (message: string, duration = Duration.Default): void => {
+ this.addNotification({ message, duration, category: 'alert-warning' });
+ }
+
+ removeNotification = (notification: Notification): void => {
+ this._notifications.update((currentNotifications) => currentNotifications.filter((n: Notification) => n !== notification));
+ }
+
+ removeAllNotifications = (): void => {
+ this._notifications.set([]);
+ }
-export function removeAllNotifications(): void {
- _notifications.set([]);
+ private addNotification(notification: Notification): void {
+ this._notifications.update((currentNotifications) => [...currentNotifications, notification]);
+ setTimeout(() => {
+ this.removeNotification(notification);
+ }, notification.duration);
+ }
}
diff --git a/frontend/src/lib/user.ts b/frontend/src/lib/user.ts
index a4145c7b3..de69c82e1 100644
--- a/frontend/src/lib/user.ts
+++ b/frontend/src/lib/user.ts
@@ -1,7 +1,6 @@
import { browser } from '$app/environment'
import { redirect, type Cookies } from '@sveltejs/kit'
import jwtDecode from 'jwt-decode'
-import { removeAllNotifications } from './notify'
import { deleteCookie, getCookie } from './util/cookies'
import {hash} from '$lib/util/hash';
import { ensureErrorIsTraced } from './otel'
@@ -135,7 +134,6 @@ function jwtToUser(user: JwtTokenUser): LexAuthUser {
export function logout(cookies?: Cookies): void {
cookies && deleteCookie('.LexBoxAuth', cookies);
- removeAllNotifications();
if (browser && window.location.pathname !== '/login') {
throw redirect(307, '/login');
}
diff --git a/frontend/src/lib/util/context.ts b/frontend/src/lib/util/context.ts
index 72ce2c5aa..606b5bcfa 100644
--- a/frontend/src/lib/util/context.ts
+++ b/frontend/src/lib/util/context.ts
@@ -1,16 +1,25 @@
import { getContext, setContext } from 'svelte';
-interface ContextDefinition {
+interface ContextConfig {
+ key: string | symbol,
+ onInit?: (value: T) => void
+}
+
+interface ContextDefinition {
use: () => T;
- init: (value: T) => T;
+ init: (...args: P) => T;
}
-export function defineContext(key: string | symbol = Symbol(), onInit?: (value: T) => void): ContextDefinition {
+export function defineContext(
+ initializer: (...args: P) => T,
+ { key = Symbol(), onInit }: Partial> = {},
+): ContextDefinition {
return {
use(): T {
return getContext(key);
},
- init(value: T): T {
+ init(...args: P): T {
+ const value = initializer(...args);
setContext(key, value);
onInit?.(value);
return value;
diff --git a/frontend/src/routes/(authenticated)/admin/+page.svelte b/frontend/src/routes/(authenticated)/admin/+page.svelte
index 933c7ee40..9abbbe114 100644
--- a/frontend/src/routes/(authenticated)/admin/+page.svelte
+++ b/frontend/src/routes/(authenticated)/admin/+page.svelte
@@ -6,7 +6,7 @@
import type { PageData } from './$types';
import DeleteUserModal from '$lib/components/DeleteUserModal.svelte';
import EditUserAccount from './EditUserAccount.svelte';
- import { notifySuccess, notifyWarning } from '$lib/notify';
+ import { useNotifications } from '$lib/notify';
import { DialogResponse } from '$lib/components/modals';
import { Duration } from '$lib/util/time';
import { Icon } from '$lib/icons';
@@ -21,6 +21,8 @@
$: allProjects = data.projects;
$: userData = data.users;
+ const { notifySuccess, notifyWarning } = useNotifications();
+
const queryParams = getSearchParams({
userSearch: queryParam.string(''),
showDeletedProjects: queryParam.boolean(false),
diff --git a/frontend/src/routes/(authenticated)/admin/ProjectTable.svelte b/frontend/src/routes/(authenticated)/admin/ProjectTable.svelte
index ad6e80e5a..e7d94f086 100644
--- a/frontend/src/routes/(authenticated)/admin/ProjectTable.svelte
+++ b/frontend/src/routes/(authenticated)/admin/ProjectTable.svelte
@@ -4,7 +4,7 @@ import {getProjectTypeI18nKey, ProjectTypeIcon} from '$lib/components/ProjectTyp
import {_FILTER_PAGE_SIZE, type AdminSearchParams, type Project} from './+page';
import {_deleteProject} from '$lib/gql/mutations';
import {DialogResponse} from '$lib/components/modals';
-import {notifyWarning} from '$lib/notify';
+import { useNotifications } from '$lib/notify';
import ConfirmDeleteModal from '$lib/components/modals/ConfirmDeleteModal.svelte';
import Dropdown from '$lib/components/Dropdown.svelte';
import TrashIcon from '$lib/icons/TrashIcon.svelte';
@@ -25,6 +25,8 @@ export let queryParams: QueryParams;
$: filters = queryParams.queryParamValues;
$: defaultFilters = queryParams.defaultQueryParamValues;
+const { notifyWarning } = useNotifications();
+
const projectFilterKeys = new Set(['projectSearch', 'projectType', 'showDeletedProjects', 'userEmail'] as const) satisfies Set;
let projectFilterLimit = _FILTER_PAGE_SIZE;
let hasActiveProjectFilter: boolean;
diff --git a/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte b/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte
index 930e2b6c0..8188c0621 100644
--- a/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte
+++ b/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte
@@ -14,7 +14,7 @@
import AddProjectMember from './AddProjectMember.svelte';
import ChangeMemberRoleModal from './ChangeMemberRoleModal.svelte';
import { CircleArrowIcon, TrashIcon } from '$lib/icons';
- import { notifySuccess, notifyWarning } from '$lib/notify';
+ import { useNotifications } from '$lib/notify';
import { DialogResponse } from '$lib/components/modals';
import type { ErrorMessage } from '$lib/forms';
import ResetProjectModal from './ResetProjectModal.svelte';
@@ -45,6 +45,8 @@
? `http://hg.${$page.url.host}/${data.code}`
: `https://hg-${$page.url.host.replace('depot', 'forge')}/${data.code}`;
+ const { notifySuccess, notifyWarning } = useNotifications();
+
let changeMemberRoleModal: ChangeMemberRoleModal;
async function changeMemberRole(projectUser: ProjectUser): Promise {
const { response } = await changeMemberRoleModal.open({
diff --git a/frontend/src/routes/(authenticated)/project/[project_code]/AddProjectMember.svelte b/frontend/src/routes/(authenticated)/project/[project_code]/AddProjectMember.svelte
index 9d5fae3b2..eef516e7e 100644
--- a/frontend/src/routes/(authenticated)/project/[project_code]/AddProjectMember.svelte
+++ b/frontend/src/routes/(authenticated)/project/[project_code]/AddProjectMember.svelte
@@ -7,7 +7,7 @@
import t from '$lib/i18n';
import { z } from 'zod';
import { _addProjectMember } from './+page';
- import { notifySuccess } from '$lib/notify';
+ import { useNotifications } from '$lib/notify';
export let projectId: string;
const schema = z.object({
@@ -17,6 +17,8 @@
let formModal: FormModal;
$: form = formModal?.form();
+ const { notifySuccess } = useNotifications();
+
async function openModal(): Promise {
const { response, formState } = await formModal.open(async () => {
const { error } = await _addProjectMember({
diff --git a/frontend/src/routes/(authenticated)/project/create/+page.svelte b/frontend/src/routes/(authenticated)/project/create/+page.svelte
index a7ea4816f..315e543f2 100644
--- a/frontend/src/routes/(authenticated)/project/create/+page.svelte
+++ b/frontend/src/routes/(authenticated)/project/create/+page.svelte
@@ -11,10 +11,13 @@
import { z } from 'zod';
import { _createProject } from './+page';
import AdminContent from '$lib/layout/AdminContent.svelte';
- import { notifySuccess } from '$lib/notify';
+ import { useNotifications } from '$lib/notify';
import { Duration } from '$lib/util/time';
export let data;
+
+ const { notifySuccess } = useNotifications();
+
const formSchema = z.object({
name: z.string().min(1, $t('project.create.name_missing')),
description: z.string().min(1, $t('project.create.description_missing')),
diff --git a/frontend/src/routes/(authenticated)/resetPassword/+page.svelte b/frontend/src/routes/(authenticated)/resetPassword/+page.svelte
index 97107aa3a..3057d4c4c 100644
--- a/frontend/src/routes/(authenticated)/resetPassword/+page.svelte
+++ b/frontend/src/routes/(authenticated)/resetPassword/+page.svelte
@@ -5,12 +5,14 @@
import Page from '$lib/layout/Page.svelte';
import { hash } from '$lib/util/hash';
import { z } from 'zod';
- import { notifySuccess } from '$lib/notify';
+ import { useNotifications } from '$lib/notify';
import type { PageData } from './$types';
import { passwordFormRules } from '$lib/forms/utils';
export let data: PageData;
+ const { notifySuccess } = useNotifications();
+
const formSchema = z.object({
password: passwordFormRules($t),
});
diff --git a/frontend/src/routes/(authenticated)/user/+page.svelte b/frontend/src/routes/(authenticated)/user/+page.svelte
index ebb08210d..173ebd5d3 100644
--- a/frontend/src/routes/(authenticated)/user/+page.svelte
+++ b/frontend/src/routes/(authenticated)/user/+page.svelte
@@ -4,7 +4,7 @@
import t from '$lib/i18n';
import { Page } from '$lib/layout';
import { _changeUserAccountData } from './+page';
- import { notifySuccess, notifyWarning } from '$lib/notify';
+ import { useNotifications } from '$lib/notify';
import z from 'zod';
import { goto } from '$app/navigation';
import DeleteUserModal from '$lib/components/DeleteUserModal.svelte';
@@ -23,6 +23,8 @@
const requestedEmail = useRequestedEmail();
$: if (data.emailResult) emailResult.set(data.emailResult);
+ const { notifySuccess, notifyWarning } = useNotifications();
+
async function openDeleteModal(): Promise {
let { response } = await deleteModal.open(user);
if (response == DialogResponse.Submit) {
diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte
index eff79487b..aef53e2fa 100644
--- a/frontend/src/routes/+layout.svelte
+++ b/frontend/src/routes/+layout.svelte
@@ -6,8 +6,7 @@
import type { LayoutData } from './$types';
import Notify from '$lib/notify/Notify.svelte';
import { Footer } from '$lib/layout';
- import { writable } from 'svelte/store';
- import { notifyWarning } from '$lib/notify';
+ import { initNotificationService } from '$lib/notify';
import { Duration } from '$lib/util/time';
import { browser } from '$app/environment';
import t from '$lib/i18n';
@@ -16,7 +15,9 @@
export let data: LayoutData;
const { page, updated } = getStores();
- const error = initErrorStore(writable($page.error));
+ const { notifyWarning } = initNotificationService();
+
+ const error = initErrorStore($page.error);
$: $error = $page.error;
$: {
if (browser && $updated) {