From 1255d0fbbeac93a490067f83a5f82346a0a043cb Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Sun, 12 Nov 2023 08:59:33 -0800 Subject: [PATCH] feat: tool drawer for edit and live-reload buttons (#1164) * feat: tool drawer for edit and live-reload buttons This replaces the *edit pencil* on HTML artifacts served by the admin server with "tool drawer". The tool drawer contains an edit button (which takes the place of the edit pencil), as well as a toggle button that can be used to disable live-reload (see #1027) for the window. If the tool drawer is obscuring part of the HTML page that one would like to look at or click on, the drawer may be dragged vertically to reposition it. It may also be (mostly) hidden off screen to minimize its visual impact. * feat(admin gui): use link for "Return to Website" button Using a real link (``) instead of a button with onclick handler allows the user to, e.g., open a new window with a live-preview by middle or right clicking on the button. Anyway, since the "button" leads to a new URL outside of the admin app, it's the right thing to do. E.g. see https://css-tricks.com/buttons-vs-links/. * perf: do not use React's StrictMode in production Strict mode causes everything to be rendered twice. (I noticed this because GlobalActions was firing off two request for /previewinfo for every page change.) This is not particularly efficient, and is probably not appropriate for the production build. * revert: remove --no-reload cli option The `--no-reload` option was add in PR #1027 (c38596a0). It disables the live-reload feature globally. Since this PR adds the ability to disable live-reload on a per-window basis, the global option no longer seems necessary. --- frontend/build.ts | 20 +- frontend/js/header/GlobalActions.tsx | 41 ++- frontend/js/main.tsx | 10 +- frontend/package-lock.json | 91 +++++++ frontend/package.json | 3 + .../components/_svg-icons/fontawesome.ts | 29 ++ .../tooldrawer/components/checkbox-button.ts | 104 +++++++ .../controllers/window-event-listener.ts | 66 +++++ frontend/tooldrawer/components/css/button.ts | 43 +++ .../components/css/visually-hidden.ts | 13 + frontend/tooldrawer/components/drag-handle.ts | 153 +++++++++++ frontend/tooldrawer/components/drawer.ts | 253 ++++++++++++++++++ frontend/tooldrawer/components/icon.ts | 56 ++++ frontend/tooldrawer/components/link-button.ts | 43 +++ .../components/livereload-widget.ts | 157 +++++++++++ frontend/tooldrawer/components/widget.ts | 62 +++++ frontend/tooldrawer/index.ts | 87 ++++++ frontend/tooldrawer/lib/condition.ts | 23 ++ frontend/tooldrawer/lib/livereloader.ts | 90 +++++++ frontend/tooldrawer/lib/marshall.ts | 86 ++++++ frontend/tooldrawer/lib/server-sent-events.ts | 39 +++ frontend/tooldrawer/livereload-worker.ts | 82 ++++++ frontend/tooldrawer/tsconfig.json | 26 ++ lektor/admin/modules/livereload.py | 12 - lektor/admin/modules/serve.py | 74 +++-- lektor/admin/templates/edit-button.html | 40 --- lektor/admin/templates/livereload-worker.js | 41 --- lektor/admin/templates/livereload.html | 32 --- lektor/admin/templates/tooldrawer.html | 10 + lektor/admin/webui.py | 5 +- lektor/cli.py | 13 +- lektor/devserver.py | 2 - tests/admin/test_serve.py | 58 ++-- 33 files changed, 1660 insertions(+), 204 deletions(-) create mode 100644 frontend/tooldrawer/components/_svg-icons/fontawesome.ts create mode 100644 frontend/tooldrawer/components/checkbox-button.ts create mode 100644 frontend/tooldrawer/components/controllers/window-event-listener.ts create mode 100644 frontend/tooldrawer/components/css/button.ts create mode 100644 frontend/tooldrawer/components/css/visually-hidden.ts create mode 100644 frontend/tooldrawer/components/drag-handle.ts create mode 100644 frontend/tooldrawer/components/drawer.ts create mode 100644 frontend/tooldrawer/components/icon.ts create mode 100644 frontend/tooldrawer/components/link-button.ts create mode 100644 frontend/tooldrawer/components/livereload-widget.ts create mode 100644 frontend/tooldrawer/components/widget.ts create mode 100644 frontend/tooldrawer/index.ts create mode 100644 frontend/tooldrawer/lib/condition.ts create mode 100644 frontend/tooldrawer/lib/livereloader.ts create mode 100644 frontend/tooldrawer/lib/marshall.ts create mode 100644 frontend/tooldrawer/lib/server-sent-events.ts create mode 100644 frontend/tooldrawer/livereload-worker.ts create mode 100644 frontend/tooldrawer/tsconfig.json delete mode 100644 lektor/admin/templates/edit-button.html delete mode 100644 lektor/admin/templates/livereload-worker.js delete mode 100644 lektor/admin/templates/livereload.html create mode 100644 lektor/admin/templates/tooldrawer.html diff --git a/frontend/build.ts b/frontend/build.ts index 53d3022b6..d5ebd6eb5 100644 --- a/frontend/build.ts +++ b/frontend/build.ts @@ -10,6 +10,9 @@ import { compile } from "sass"; import { compilerOptions } from "./tsconfig.json"; +// Optimization: compute fontawesome SVG at compile-time to minimize bundle size +import SVG_ICONS_FONTAWESOME from "./tooldrawer/components/_svg-icons/fontawesome"; + // A simple esbuild plugin to compile sass. const sassPlugin: Plugin = { name: "sass", @@ -36,8 +39,16 @@ const sassPlugin: Plugin = { */ async function runBuild(dev: boolean) { const ctx = await context({ - entryPoints: [join(__dirname, "js", "main.tsx")], - outfile: join(__dirname, "..", "lektor", "admin", "static", "app.js"), + entryPoints: { + app: join(__dirname, "js", "main.tsx"), + tooldrawer: join(__dirname, "tooldrawer"), + "livereload-worker": join( + __dirname, + "tooldrawer", + "livereload-worker.ts", + ), + }, + outdir: join(__dirname, "..", "lektor", "admin", "static"), format: "iife", bundle: true, target: compilerOptions.target, @@ -55,7 +66,10 @@ async function runBuild(dev: boolean) { sourcesContent: dev, // The following options differ between dev and prod builds. // For prod builds, we want to use React's prod build and minify. - define: { "process.env.NODE_ENV": dev ? '"development"' : '"production"' }, + define: { + "process.env.NODE_ENV": dev ? '"development"' : '"production"', + SVG_ICONS_FONTAWESOME: JSON.stringify(SVG_ICONS_FONTAWESOME), + }, // Only minify syntax (like DCE and not by removing whitespace and renaming // identifiers). This keeps the bundle size a bit larger but still very // readable. diff --git a/frontend/js/header/GlobalActions.tsx b/frontend/js/header/GlobalActions.tsx index 33b69ba6c..57b9c4d20 100644 --- a/frontend/js/header/GlobalActions.tsx +++ b/frontend/js/header/GlobalActions.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useRecord } from "../context/record-context"; import { getCanonicalUrl } from "../utils"; import { get } from "../fetch"; @@ -23,16 +23,37 @@ const preferences = () => { export default function GlobalActions(): JSX.Element { const record = useRecord(); + // Fetch previewURL so that we can use a link instead of button + // with onclick handler for the preview button. + // This allows one to, e.g., open a preview in a new window by + // right-clicking on the button. + const [previewUrl, setPreviewUrl] = useState(); + const fetchPreviewUrl = useMemo(async () => { + try { + const { url } = await get("/previewinfo", record); + const canonicalUrl = getCanonicalUrl(url ?? "/"); + setPreviewUrl(canonicalUrl); + return canonicalUrl; + } catch (err) { + showErrorDialog(err); + } + }, [record]); + useEffect(() => { return setShortcutHandler(ShortcutAction.Search, findFiles); }, []); const returnToWebsite = useCallback(() => { - get("/previewinfo", record).then(({ url }) => { - window.location.href = - url === null ? getCanonicalUrl("/") : getCanonicalUrl(url); - }, showErrorDialog); - }, [record]); + if (previewUrl === null) { + // href has not yet been set on the link button + // wait for /previewinfo fetch to complete... + fetchPreviewUrl + .then((href) => { + window.location.href = href; + }) + .catch(showErrorDialog); + } + }, [previewUrl, fetchPreviewUrl]); return (
@@ -60,14 +81,14 @@ export default function GlobalActions(): JSX.Element { > - +