From 62d674c8e28f7a628d4573a430ad9815adf74551 Mon Sep 17 00:00:00 2001 From: Allan Lasser Date: Tue, 26 Nov 2024 09:14:48 -0500 Subject: [PATCH 1/5] Fix Toaster to top center Fix permanent toast demo; you need to provide `null` for lifespan --- src/lib/components/common/Toast.svelte | 2 +- src/lib/components/layouts/Toaster.svelte | 11 ++++++----- .../components/layouts/stories/Toaster.stories.svelte | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/lib/components/common/Toast.svelte b/src/lib/components/common/Toast.svelte index 6ec44ea76..9b5d8e711 100644 --- a/src/lib/components/common/Toast.svelte +++ b/src/lib/components/common/Toast.svelte @@ -66,7 +66,7 @@ on:mouseenter={cancel} on:mouseleave={reset} role="dialog" - transition:fly={{ duration: 750, easing: quintOut, y: 20 }} + transition:fly={{ duration: 750, easing: quintOut, y: -20 }} >
diff --git a/src/lib/components/layouts/Toaster.svelte b/src/lib/components/layouts/Toaster.svelte index 0dca92089..a05b66de6 100644 --- a/src/lib/components/layouts/Toaster.svelte +++ b/src/lib/components/layouts/Toaster.svelte @@ -1,12 +1,12 @@
@@ -49,7 +62,7 @@ }} > {#if typeof toast.contents === "string"} - {@html toast.contents} + {toast.contents} {:else} {/if} diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 000000000..95c5829a7 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1 @@ +export { load } from "sveltekit-flash-message/server"; From 0228c645197d9c0a28257a0769059ea28c2e08e1 Mon Sep 17 00:00:00 2001 From: Allan Lasser Date: Tue, 26 Nov 2024 09:36:43 -0500 Subject: [PATCH 5/5] Dispatch toasts, mostly from server actions - Sets toast messages in server actions - Uses client toasts for uploads --- src/langs/json/en.json | 14 +++++--- .../components/forms/DocumentUpload.svelte | 9 ++++- src/lib/components/forms/EditData.svelte | 5 +++ src/lib/components/forms/UserFeedback.svelte | 5 --- .../forms/stories/UserFeedback.stories.svelte | 5 --- src/lib/components/layouts/Toaster.svelte | 13 ------- src/routes/(app)/+page.server.ts | 11 +++++- .../add-ons/[owner]/[repo]/+page.server.ts | 4 +++ .../[owner]/[repo]/[event]/+page.server.ts | 3 ++ src/routes/(app)/documents/+page.server.ts | 22 ++++++++++++ .../documents/[id]-[slug]/+page.server.ts | 34 ++++++++++++++++--- src/routes/(app)/projects/+page.server.ts | 3 ++ .../projects/[id]-[slug]/+page.server.ts | 26 ++++++++++++-- src/routes/+layout.svelte | 15 +++++++- 14 files changed, 132 insertions(+), 37 deletions(-) diff --git a/src/langs/json/en.json b/src/langs/json/en.json index ff6681be9..aa5b46037 100644 --- a/src/langs/json/en.json +++ b/src/langs/json/en.json @@ -144,7 +144,9 @@ "uploading": "Uploading", "processing": "Processing", "done": "Done" - } + }, + "success": "Upload complete. Your documents will process in the background.", + "error": "Error during upload. See upload list for more details." }, "authSection": { "help": { @@ -407,7 +409,8 @@ "many": "Editing {n, plural, one {one document} other {# documents}}. This will add the same values to all selected documents.", "allowedHTML": "Limited HTML can be used for formatting. See API documentation for details.", "save": "Save", - "cancel": "Cancel" + "cancel": "Cancel", + "success": "Metadata saved" }, "annotate": { "title": "Annotate Document", @@ -433,6 +436,7 @@ "undo": "Undo", "cancel": "Discard", "cancelWarning": "You will lose unsaved redactions. Are you sure you want to cancel?", + "success": "Redactions saved", "error": "Error" }, "sections": { @@ -451,6 +455,7 @@ "continue": "Do you wish to continue?", "confirm": "Delete", "cancel": "Cancel", + "success": "{n, plural, one {Document deleted} other {Documents deleted}}", "error": "Error", "none": "No documents selected. Close this form and select at least one document first." }, @@ -490,11 +495,12 @@ "viewsource": "View Source", "selectionHelp": "Select individual documents or run a search for the documents you want, for example “+project:mueller-docs-200005”.", "selectionLearnMore": "Learn more about how Add-Ons work", - "editing": "Editing a previously dispatched addon", + "editing": "Editing a previously dispatched Add-On", "instructions": "Instructions", "dispatch": "Dispatch", "history": "History", - "noHistory": "You have not run this add-on before" + "noHistory": "You have not run this Add-On before", + "success": "Add-On dispatched" }, "addonBrowserDialog": { "title": "Browse Add-Ons", diff --git a/src/lib/components/forms/DocumentUpload.svelte b/src/lib/components/forms/DocumentUpload.svelte index 0e9c52422..e11a55e4b 100644 --- a/src/lib/components/forms/DocumentUpload.svelte +++ b/src/lib/components/forms/DocumentUpload.svelte @@ -70,6 +70,7 @@ progress through the three-part upload process. import { getCsrfToken } from "$lib/utils/api"; import { getCurrentUser } from "$lib/utils/permissions"; import { getProcessLoader } from "../processing/ProcessContext.svelte"; + import { toast } from "../layouts/Toaster.svelte"; export let files: File[] = getFilesToUpload(); export let projects: Project[] = []; @@ -202,6 +203,7 @@ progress through the three-part upload process. // errors are handled within each promise, so we can just wait for all to be settled await Promise.allSettled(promises); + toast($_("uploadDialog.success"), { status: "success" }); loading = false; } @@ -228,6 +230,7 @@ progress through the three-part upload process. // bail here on error, console.error to report this to sentry if (error) { + toast($_("uploadDialog.error"), { status: "error" }); return console.error(error); } @@ -239,7 +242,7 @@ progress through the three-part upload process. message: "API returned invalid document. Please try again.", }, }; - + toast($_("uploadDialog.error"), { status: "error" }); return; } @@ -255,6 +258,7 @@ progress through the three-part upload process. }; STATUS[id].error = error; + toast($_("uploadDialog.error"), { status: "error" }); return console.error(error); } @@ -263,6 +267,7 @@ progress through the three-part upload process. status: 500, message: "Invalid presigned URL", }; + toast($_("uploadDialog.error"), { status: "error" }); return; } @@ -276,6 +281,7 @@ progress through the three-part upload process. status: 500, message: "Upload failed", }; + toast($_("uploadDialog.error"), { status: "error" }); return; } @@ -289,6 +295,7 @@ progress through the three-part upload process. )); if (error) { + toast($_("uploadDialog.error"), { status: "error" }); STATUS[id].error = error; } // trigger process load request diff --git a/src/lib/components/forms/EditData.svelte b/src/lib/components/forms/EditData.svelte index 578fc72d6..cc40df73c 100644 --- a/src/lib/components/forms/EditData.svelte +++ b/src/lib/components/forms/EditData.svelte @@ -11,6 +11,7 @@ import KeyValue from "$lib/components/inputs/KeyValue.svelte"; import { canonicalUrl } from "$lib/api/documents"; + import { toast } from "../layouts/Toaster.svelte"; export let document: Document; @@ -53,6 +54,10 @@ // `update` is a function which triggers the default logic that would be triggered if this callback wasn't set dispatch("close"); update(result); + if (result.type === "success") { + // do something + toast($_("edit.success"), { status: "success" }); + } }; } diff --git a/src/lib/components/forms/UserFeedback.svelte b/src/lib/components/forms/UserFeedback.svelte index a87f1a293..6912d3886 100644 --- a/src/lib/components/forms/UserFeedback.svelte +++ b/src/lib/components/forms/UserFeedback.svelte @@ -7,7 +7,6 @@ import Button from "../common/Button.svelte"; import Flex from "../common/Flex.svelte"; import Avatar from "../accounts/Avatar.svelte"; - import { toast } from "../layouts/Toaster.svelte"; import { APP_URL } from "@/config/config"; import { getUserName } from "$lib/api/accounts"; @@ -54,13 +53,9 @@ return async ({ result }) => { if (result.type === "success") { status = "success"; - toast($_("feedback.success"), { - status: "success", - }); dispatch("close"); } else if (result.type === "failure") { status = "error"; - toast(result.error, { status: "error" }); } }; } diff --git a/src/lib/components/forms/stories/UserFeedback.stories.svelte b/src/lib/components/forms/stories/UserFeedback.stories.svelte index c91f94ef4..4606896a6 100644 --- a/src/lib/components/forms/stories/UserFeedback.stories.svelte +++ b/src/lib/components/forms/stories/UserFeedback.stories.svelte @@ -3,7 +3,6 @@ import UserFeedbackForm from "../UserFeedback.svelte"; import { me } from "@/test/fixtures/accounts"; import { feedback } from "@/test/handlers/feedback"; - import Toaster from "../../layouts/Toaster.svelte"; export const meta = { title: "Forms / User Feedback", @@ -20,27 +19,23 @@
-
-
-
-
diff --git a/src/lib/components/layouts/Toaster.svelte b/src/lib/components/layouts/Toaster.svelte index 760dc4693..0bd2a88f2 100644 --- a/src/lib/components/layouts/Toaster.svelte +++ b/src/lib/components/layouts/Toaster.svelte @@ -32,20 +32,7 @@
diff --git a/src/routes/(app)/+page.server.ts b/src/routes/(app)/+page.server.ts index 594b3b3dc..c3be430ab 100644 --- a/src/routes/(app)/+page.server.ts +++ b/src/routes/(app)/+page.server.ts @@ -2,9 +2,10 @@ import { _ } from "svelte-i18n"; import { createFeedback, type Feedback } from "@/lib/api/feedback"; import type { Actions } from "./$types"; import { fail } from "@sveltejs/kit"; +import { setFlash } from "sveltekit-flash-message/server"; export const actions = { - feedback: async ({ request, fetch }) => { + feedback: async ({ request, cookies, fetch }) => { const data = await request.formData(); // POST form data to baserow const feedback: Feedback = { @@ -14,8 +15,16 @@ export const actions = { }; try { await createFeedback(feedback, fetch); + setFlash( + { + message: "Feedback recieved, thanks for using DocumentCloud!", + status: "success", + }, + cookies, + ); return { success: true }; } catch (e) { + setFlash({ message: e.message, status: "error" }, cookies); fail(500, { message: String(e) }); } }, diff --git a/src/routes/(app)/add-ons/[owner]/[repo]/+page.server.ts b/src/routes/(app)/add-ons/[owner]/[repo]/+page.server.ts index 3a0fbf052..cc4d66734 100644 --- a/src/routes/(app)/add-ons/[owner]/[repo]/+page.server.ts +++ b/src/routes/(app)/add-ons/[owner]/[repo]/+page.server.ts @@ -4,6 +4,7 @@ import { fail } from "@sveltejs/kit"; import { CSRF_COOKIE_NAME } from "@/config/config.js"; import { getAddon, buildPayload, dispatch } from "$lib/api/addons"; +import { setFlash } from "sveltekit-flash-message/server"; export const actions = { async dispatch({ cookies, fetch, request, params }) { @@ -33,10 +34,13 @@ export const actions = { const { data, error } = await dispatch(payload, csrf_token, fetch); if (error) { + setFlash({ message: error.message, status: "error" }, cookies); return fail(error.status, { ...error }); } const type = payload.event ? "event" : "run"; + const message = payload.event ? "Event scheduled" : "Run dispatched"; + setFlash({ message, status: "success" }, cookies); return { type, [type]: data, diff --git a/src/routes/(app)/add-ons/[owner]/[repo]/[event]/+page.server.ts b/src/routes/(app)/add-ons/[owner]/[repo]/[event]/+page.server.ts index 0e6a25d51..08d941f74 100644 --- a/src/routes/(app)/add-ons/[owner]/[repo]/[event]/+page.server.ts +++ b/src/routes/(app)/add-ons/[owner]/[repo]/[event]/+page.server.ts @@ -4,6 +4,7 @@ import { fail } from "@sveltejs/kit"; import { CSRF_COOKIE_NAME } from "@/config/config.js"; import { getEvent, buildPayload, update } from "$lib/api/addons"; +import { setFlash } from "sveltekit-flash-message/server"; export const actions = { async update({ cookies, fetch, params, request }) { @@ -35,9 +36,11 @@ export const actions = { const { data, error } = await update(id, payload, csrf_token, fetch); if (error) { + setFlash({ message: error.message, status: "error" }, cookies); return fail(error.status, { ...error }); } + setFlash({ message: "Event updated", status: "success" }, cookies); return { type: "event", event: data, diff --git a/src/routes/(app)/documents/+page.server.ts b/src/routes/(app)/documents/+page.server.ts index a72e32933..8084b1bc5 100644 --- a/src/routes/(app)/documents/+page.server.ts +++ b/src/routes/(app)/documents/+page.server.ts @@ -5,6 +5,7 @@ import { fail } from "@sveltejs/kit"; import { CSRF_COOKIE_NAME } from "@/config/config.js"; import { destroy_many, edit_many, add_tags } from "$lib/api/documents"; +import { setFlash } from "sveltekit-flash-message/server"; export function load({ cookies }) { const csrf_token = cookies.get(CSRF_COOKIE_NAME); @@ -51,9 +52,21 @@ export const actions = { const errors = results.filter((r) => r.error); if (errors.length) { + errors.forEach((r) => { + if (r.error) { + setFlash({ message: r.error.message, status: "error" }, cookies); + } + }); return fail(400, { errors }); } + setFlash( + { + message: `Data saved to ${results.filter((r) => !r.error).length} documents`, + status: "success", + }, + cookies, + ); return { success: true, }; @@ -71,9 +84,12 @@ export const actions = { const { error } = await destroy_many(ids, csrf_token, fetch); if (error) { + setFlash({ message: error.message, status: "error" }, cookies); return fail(error.status, { ...error }); } + const message = ids.length === 1 ? "Document deleted" : "Documents deleted"; + setFlash({ message, status: "success" }, cookies); return { success: true, count: ids.length, @@ -100,9 +116,15 @@ export const actions = { const { error } = await edit_many(docs, csrf_token, fetch); if (error) { + setFlash({ message: error.message, status: "error" }, cookies); return fail(error.status, { ...error }); } + const message = + ids.length === 1 + ? "Saved edits to one document" + : `Saved edits to ${ids.length} documents`; + setFlash({ message, status: "success" }, cookies); return { success: true, count: ids.length, diff --git a/src/routes/(app)/documents/[id]-[slug]/+page.server.ts b/src/routes/(app)/documents/[id]-[slug]/+page.server.ts index 248c6f1f2..3f9f2327d 100644 --- a/src/routes/(app)/documents/[id]-[slug]/+page.server.ts +++ b/src/routes/(app)/documents/[id]-[slug]/+page.server.ts @@ -1,7 +1,8 @@ import type { Actions } from "./$types"; import type { Access, Document, Note } from "$lib/api/types"; -import { fail, redirect } from "@sveltejs/kit"; +import { fail } from "@sveltejs/kit"; +import { setFlash, redirect } from "sveltekit-flash-message/server"; import { CSRF_COOKIE_NAME } from "@/config/config.js"; import { destroy, edit, redact } from "$lib/api/documents"; @@ -54,12 +55,13 @@ export const actions = { ); if (error) { + setFlash({ message: error.message, status: "error" }, cookies); return fail(error.status, { message: error.message, error: error.errors, }); } - + setFlash({ message: "Data saved", status: "success" }, cookies); return { success: true, document, @@ -79,10 +81,16 @@ export const actions = { const { error } = await destroy(id, csrf_token, fetch); if (error) { + setFlash({ message: error.message, status: "error" }, cookies); return fail(error.status, { message: error.message }); } - return redirect(302, "/documents/"); + return redirect( + 302, + "/documents/", + { message: "Document deleted", status: "success" }, + cookies, + ); }, async edit({ cookies, fetch, request, params }) { @@ -93,7 +101,7 @@ export const actions = { const form = await request.formData(); const { id } = params; - const update: Partial = Object.fromEntries(form); + const update: Partial = Object.fromEntries(form.entries()); // noindex is a boolean so needs special treatment update.noindex = form.get("noindex") === "on"; @@ -101,12 +109,14 @@ export const actions = { const { data: document, error } = await edit(id, update, csrf_token, fetch); if (error) { + setFlash({ message: error.message, status: "error" }, cookies); return fail(error.status, { message: error.message, errors: error.errors, }); } + setFlash({ message: "Metadata saved", status: "success" }, cookies); return { success: true, document, @@ -127,14 +137,22 @@ export const actions = { // probably the API is down if (!resp) { + setFlash( + { message: "Redactions failed to save.", status: "error" }, + cookies, + ); return fail(500, { error: "Something went wrong." }); } // something else broke if (isErrorCode(resp.status)) { + setFlash( + { message: "Redactions failed to save.", status: "error" }, + cookies, + ); return fail(resp.status, await resp.json()); } - + setFlash({ message: "Redactions saved", status: "success" }, cookies); return { success: true, redactions: await resp.json(), // this should be the same as above @@ -171,12 +189,14 @@ export const actions = { ); if (error) { + setFlash({ message: error.message, status: "error" }, cookies); return fail(error.status, { message: error.message, errors: error.errors, }); } + setFlash({ message: "Note created", status: "success" }, cookies); return { success: true, note: created, @@ -208,12 +228,14 @@ export const actions = { ); if (error) { + setFlash({ message: error.message, status: "error" }, cookies); return fail(error.status, { message: error.message, errors: error.errors, }); } + setFlash({ message: "Note updated", status: "success" }, cookies); return { success: true, note: updated, @@ -234,9 +256,11 @@ export const actions = { const { error } = await notes.remove(params.id, note_id, csrf_token, fetch); if (error) { + setFlash({ message: error.message, status: "error" }, cookies); return fail(error.status, { message: error.message }); } + setFlash({ message: "Note deleted", status: "success" }, cookies); return { success: true, }; diff --git a/src/routes/(app)/projects/+page.server.ts b/src/routes/(app)/projects/+page.server.ts index 998e643d0..25511505a 100644 --- a/src/routes/(app)/projects/+page.server.ts +++ b/src/routes/(app)/projects/+page.server.ts @@ -4,6 +4,7 @@ import { fail } from "@sveltejs/kit"; import { CSRF_COOKIE_NAME } from "@/config/config.js"; import * as projects from "$lib/api/projects"; +import { setFlash } from "sveltekit-flash-message/server"; export function load({ cookies }) { const csrf_token = cookies.get(CSRF_COOKIE_NAME); @@ -30,9 +31,11 @@ export const actions = { try { const created = await projects.create(project, csrf_token, fetch); + setFlash({ message: "Project created", status: "success" }, cookies); return { success: true, project: created }; } catch (error) { // todo: return better errors + setFlash({ message: error.message, status: "error" }, cookies); return fail(400, { error }); } }, diff --git a/src/routes/(app)/projects/[id]-[slug]/+page.server.ts b/src/routes/(app)/projects/[id]-[slug]/+page.server.ts index 6aba2be38..747b114ad 100644 --- a/src/routes/(app)/projects/[id]-[slug]/+page.server.ts +++ b/src/routes/(app)/projects/[id]-[slug]/+page.server.ts @@ -1,7 +1,8 @@ import type { Actions } from "./$types"; import type { Project, ProjectAccess } from "$lib/api/types"; -import { fail, redirect } from "@sveltejs/kit"; +import { fail } from "@sveltejs/kit"; +import { redirect, setFlash } from "sveltekit-flash-message/server"; import { CSRF_COOKIE_NAME } from "@/config/config.js"; import * as collaborators from "$lib/api/collaborators"; import * as projects from "$lib/api/projects"; @@ -33,10 +34,16 @@ export const actions = { const { error } = await projects.destroy(project_id, csrf_token, fetch); if (error) { + setFlash({ message: error.message, status: "error" }, cookies); return fail(error.status, { message: error.message }); } - return redirect(302, "/projects/"); + return redirect( + 302, + "/projects/", + { message: "Project deleted", status: "success" }, + cookies, + ); }, /** @@ -65,9 +72,11 @@ export const actions = { ); if (error) { + setFlash({ message: error.message, status: "error" }, cookies); return fail(error.status, { message: error.message }); } + setFlash({ message: "Edits saved", status: "success" }, cookies); return { success: true, project: updated }; }, @@ -93,9 +102,11 @@ export const actions = { ); if (error) { + setFlash({ message: error.message, status: "error" }, cookies); return fail(error.status, { ...error }); } + setFlash({ message: "Invitation sent", status: "success" }, cookies); return { user, }; @@ -130,9 +141,14 @@ export const actions = { ); if (error) { + setFlash({ message: error.message, status: "error" }, cookies); return fail(error.status, { ...error }); } + setFlash( + { message: "Updated collaborator settings", status: "success" }, + cookies, + ); return { user: data, }; @@ -164,7 +180,13 @@ export const actions = { ); if (error) { + setFlash({ message: error.message, status: "error" }, cookies); return fail(error.status, { ...error }); } + + setFlash( + { message: "Collaborator removed from project", status: "success" }, + cookies, + ); }, } satisfies Actions; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 81aa820ae..19bad86ff 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,17 +1,30 @@