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) {