Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug/remove notifications singleton #364

Merged
merged 2 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions frontend/src/lib/email/EmailVerificationStatus.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<script lang="ts" context="module">
import type { Writable } from 'svelte/store';
import { writable, type Writable } from 'svelte/store';
import { defineContext } from '$lib/util/context';

export const { use: useRequestedEmail, init: initRequestedEmail } = defineContext<Writable<string | null>>();
export const { use: useEmailResult, init: initEmailResult } = defineContext<Writable<EmailResult | null>>();
export const { use: useRequestedEmail, init: initRequestedEmail } = defineContext<Writable<string | null>>(() => writable());
export const { use: useEmailResult, init: initEmailResult } = defineContext<Writable<EmailResult | null>>(() => writable());
</script>

<script lang="ts">
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/lib/error/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { browser, dev } from '$app/environment';
import { isObject, isRedirect } from '$lib/util/types';

import type { Writable } from 'svelte/store';
import { writable, type Writable } from 'svelte/store';
import { defineContext } from '$lib/util/context';
import { ensureErrorIsTraced } from '$lib/otel';
import { getStores } from '$app/stores';

export const { use: useError, init: initErrorStore } =
defineContext<Writable<App.Error | null>>('ERROR_STORE_KEY', setupGlobalErrorHandlers);
defineContext<Writable<App.Error | null>, [App.Error | null]>(
(error: App.Error | null) => writable(error),
{ onInit: setupGlobalErrorHandlers });

//we can't just have a `dismiss` function because we need to be able to call it from the template
//but we can't use `error()` after a component is created, so we need to define a hook function which is called once
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/lib/layout/Layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import { beforeNavigate } from '$app/navigation';
import { page } from '$app/stores';
import type { LayoutData } from '../../routes/$types';
import { writable } from 'svelte/store';

let menuToggle = false;
$: data = $page.data as LayoutData;
Expand All @@ -30,8 +29,8 @@
});
beforeNavigate(() => close());

initRequestedEmail(writable());
initEmailResult(writable());
initRequestedEmail();
initEmailResult();
</script>

<svelte:window on:keydown={closeOnEscape} />
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/lib/notify/Notify.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<script lang="ts">
import { notifications, removeAllNotifications, removeNotification } from '.';
import t from '$lib/i18n';
import { BadgeButton } from '$lib/components/Badges';
import { slide, blur } from 'svelte/transition';
import { useNotifications } from '.';

const { notifications, removeAllNotifications, removeNotification } = useNotifications();
</script>

{#if $notifications.length > 0}
Expand Down
58 changes: 31 additions & 27 deletions frontend/src/lib/notify/index.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,47 @@
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;
category?: 'alert-warning';
duration: number;
}

const _notifications = writable<Notification[]>([]);
// _notifications.set([{ message: 'Test notification', duration: 4 }, { message: 'Test notification', duration: 4 }])
export const { use: useNotifications, init: initNotificationService } =
defineContext<NotificationService>(() => 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<Notification[]> {
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<Notification[]>) {
// _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);
}
}
2 changes: 0 additions & 2 deletions frontend/src/lib/user.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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');
}
Expand Down
17 changes: 13 additions & 4 deletions frontend/src/lib/util/context.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { getContext, setContext } from 'svelte';

interface ContextDefinition<T> {
interface ContextConfig<T> {
key: string | symbol,
onInit?: (value: T) => void
}

interface ContextDefinition<T, P extends unknown[]> {
use: () => T;
init: (value: T) => T;
init: (...args: P) => T;
}

export function defineContext<T>(key: string | symbol = Symbol(), onInit?: (value: T) => void): ContextDefinition<T> {
export function defineContext<T, P extends unknown[] = []>(
initializer: (...args: P) => T,
{ key = Symbol(), onInit }: Partial<ContextConfig<T>> = {},
): ContextDefinition<T, P> {
return {
use(): T {
return getContext<T>(key);
},
init(value: T): T {
init(...args: P): T {
const value = initializer(...args);
setContext(key, value);
onInit?.(value);
return value;
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/routes/(authenticated)/admin/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -21,6 +21,8 @@
$: allProjects = data.projects;
$: userData = data.users;

const { notifySuccess, notifyWarning } = useNotifications();

const queryParams = getSearchParams<AdminSearchParams>({
userSearch: queryParam.string<string>(''),
showDeletedProjects: queryParam.boolean<boolean>(false),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,6 +25,8 @@ export let queryParams: QueryParams<AdminSearchParams>;
$: filters = queryParams.queryParamValues;
$: defaultFilters = queryParams.defaultQueryParamValues;

const { notifyWarning } = useNotifications();

const projectFilterKeys = new Set(['projectSearch', 'projectType', 'showDeletedProjects', 'userEmail'] as const) satisfies Set<keyof AdminSearchParams>;
let projectFilterLimit = _FILTER_PAGE_SIZE;
let hasActiveProjectFilter: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<void> {
const { response } = await changeMemberRoleModal.open({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -17,6 +17,8 @@
let formModal: FormModal<typeof schema>;
$: form = formModal?.form();

const { notifySuccess } = useNotifications();

async function openModal(): Promise<void> {
const { response, formState } = await formModal.open(async () => {
const { error } = await _addProjectMember({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/routes/(authenticated)/user/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -23,6 +23,8 @@
const requestedEmail = useRequestedEmail();
$: if (data.emailResult) emailResult.set(data.emailResult);

const { notifySuccess, notifyWarning } = useNotifications();

async function openDeleteModal(): Promise<void> {
let { response } = await deleteModal.open(user);
if (response == DialogResponse.Submit) {
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) {
Expand Down