From 20d6e5b5e7ddf9e2cd6ea9dd33b44e11bd6c67af Mon Sep 17 00:00:00 2001 From: Yuchan Lee Date: Fri, 19 Jul 2024 21:11:57 +0900 Subject: [PATCH] Show notifications on registration failure (#51) --- .../docker-compose.devcontainer.yaml | 3 +- src/components/Config.svelte | 5 +- src/components/Config.test.ts | 2 +- src/components/Message.svelte | 42 ++++++++++++++++ src/components/Message.test.ts | 9 ++++ src/lib/messages.ts | 48 +++++++++++++++++++ src/pages/options/Page.svelte | 17 +++++-- 7 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 src/components/Message.svelte create mode 100644 src/components/Message.test.ts create mode 100644 src/lib/messages.ts diff --git a/.devcontainer/docker-compose.devcontainer.yaml b/.devcontainer/docker-compose.devcontainer.yaml index ccaae78..d356c19 100644 --- a/.devcontainer/docker-compose.devcontainer.yaml +++ b/.devcontainer/docker-compose.devcontainer.yaml @@ -7,11 +7,12 @@ services: volumes: - ..:/workspace environment: - DISPLAY: novnc:0.0 + DISPLAY: ${DISPLAY:-novnc:0.0} healthcheck: disable: true cap_add: - SYS_ADMIN + shm_size: 2gb novnc: image: theasp/novnc:latest diff --git a/src/components/Config.svelte b/src/components/Config.svelte index 6cc5394..248c3e3 100644 --- a/src/components/Config.svelte +++ b/src/components/Config.svelte @@ -4,6 +4,7 @@ import P from 'flowbite-svelte/P.svelte'; import { get } from 'svelte/store'; import Eye from '~/components/Eye.svelte'; + import { putMessage } from '~/lib/messages'; import { launchWebAuthFlow as _launchWebAuthFlow } from '~/lib/raindrop/auth'; import { accessToken, clientID, clientSecret, refreshToken } from '~/lib/settings'; @@ -20,9 +21,10 @@ }); accessToken.set(result.accessToken); refreshToken.set(result.refreshToken); + putMessage({ type: 'success', message: 'Successfully authorized app.' }); } catch (err) { - // TODO: Show error message as modal or toast console.error('Failed to authorize app:', err); + putMessage({ type: 'error', message: String(err) }); } }; @@ -80,7 +82,6 @@ classDiv="w-full mr-2" id="access-token" type={showAccessToken ? 'text' : 'password'} - disabled bind:value={$accessToken} > Access Token diff --git a/src/components/Config.test.ts b/src/components/Config.test.ts index e24e5ba..e460e54 100644 --- a/src/components/Config.test.ts +++ b/src/components/Config.test.ts @@ -71,7 +71,7 @@ describe('access token', () => { const { getByTestId } = render(Config); const input = getByTestId('access-token/input') as HTMLInputElement; expect(input).toBeTruthy(); - expect(input.getAttributeNames()).toContain('disabled'); + expect(input.getAttributeNames()).not.toContain('disabled'); }); it('is masked by default and able to toggle it', async () => { diff --git a/src/components/Message.svelte b/src/components/Message.svelte new file mode 100644 index 0000000..eaec8bc --- /dev/null +++ b/src/components/Message.svelte @@ -0,0 +1,42 @@ + + +{#if message} + {@const color = messageMapping[message.type].color} + {@const icon = messageMapping[message.type].icon} +
+ dismissMessage(message.id)}> + + + + {message.message} + +
+{/if} diff --git a/src/components/Message.test.ts b/src/components/Message.test.ts new file mode 100644 index 0000000..28a3627 --- /dev/null +++ b/src/components/Message.test.ts @@ -0,0 +1,9 @@ +// @vitest-environment jsdom +import { render } from '@testing-library/svelte'; +import { expect, it } from 'vitest'; +import Message from './Message.svelte'; + +it('renders OK', () => { + const { container } = render(Message); + expect(container).toBeTruthy(); +}); diff --git a/src/lib/messages.ts b/src/lib/messages.ts new file mode 100644 index 0000000..ed9e663 --- /dev/null +++ b/src/lib/messages.ts @@ -0,0 +1,48 @@ +import { writable } from 'svelte/store'; + +export type Message = { + id?: string; + type: 'success' | 'info' | 'error'; + message: string; +}; + +export type MessageBox = { + [id: string]: Message; +}; + +export const messageBox = writable({}); + +/** + * Put message to message box. + * @param message Message to put. If `.id` is not set, random generated ID will be used. + * @returns ID of message. + */ +export function putMessage(message: Message): string { + const id = message.id ?? Math.random().toString(36).substring(2); + messageBox.update((box) => { + box[id] = { id, ...message }; + return box; + }); + return id; +} + +/** + * Dismiss message with ID. + * @param id ID of message. + */ +export function dismissMessage(id?: string) { + if (!id) return; + + messageBox.update((box) => { + delete box[id]; + return box; + }); +} + +/* c8 ignore start */ +if (import.meta.vitest) { + const { describe } = import.meta.vitest; + + describe.todo('To Do'); +} +/* c8 ignore stop */ diff --git a/src/pages/options/Page.svelte b/src/pages/options/Page.svelte index e57ddac..220920d 100644 --- a/src/pages/options/Page.svelte +++ b/src/pages/options/Page.svelte @@ -9,40 +9,49 @@ import About from '~/components/About.svelte'; import Bookmarks from '~/components/Bookmarks.svelte'; import Config from '~/components/Config.svelte'; + import Message from '~/components/Message.svelte'; import TryIt from '~/components/TryIt.svelte'; + import { messageBox } from '~/lib/messages';
- + Bookmarks
- + Try It
- + Settings
- + About
+ + +
+ {#each Object.entries($messageBox) as [id, message] (id)} + + {/each} +