Skip to content

Commit

Permalink
feat: tool drawer for edit and live-reload buttons (#1164)
Browse files Browse the repository at this point in the history
* 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 (`<a href="...">`) 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 (c38596a).  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.
  • Loading branch information
dairiki committed Nov 12, 2023
1 parent beee7e0 commit 1255d0f
Show file tree
Hide file tree
Showing 33 changed files with 1,660 additions and 204 deletions.
20 changes: 17 additions & 3 deletions frontend/build.ts
Expand Up @@ -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",
Expand All @@ -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,
Expand All @@ -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.
Expand Down
41 changes: 31 additions & 10 deletions 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";
Expand All @@ -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<string | undefined>();
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 (
<div className="btn-group">
Expand Down Expand Up @@ -60,14 +81,14 @@ export default function GlobalActions(): JSX.Element {
>
<i className="fa fa-refresh fa-fw" />
</button>
<button
type="button"
<a
href={previewUrl}
className="btn btn-secondary border"
onClick={returnToWebsite}
title={trans("RETURN_TO_WEBSITE")}
onClick={returnToWebsite}
>
<i className="fa fa-eye fa-fw" />
</button>
</a>
<button
type="button"
className="btn btn-secondary border"
Expand Down
10 changes: 5 additions & 5 deletions frontend/js/main.tsx
Expand Up @@ -70,9 +70,9 @@ if (dash) {
},
);

root.render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
);
let app = <RouterProvider router={router} />;
if (process.env.NODE_ENV !== "production") {
app = <StrictMode>{app}</StrictMode>;
}
root.render(app);
}
91 changes: 91 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions frontend/package.json
Expand Up @@ -9,6 +9,8 @@
"@fontsource/roboto-slab": "^5.0.3"
},
"devDependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@types/jsdom": "^21.1.1",
"@types/mocha": "^10.0.1",
"@types/node": "^20.3.3",
Expand All @@ -26,6 +28,7 @@
"fast-glob": "^3.2.11",
"font-awesome": "^4.3.0",
"jsdom": "^22.1.0",
"lit": "^3.0.2",
"mocha": "^10.0.0",
"nyc": "^15.1.0",
"prettier": "^3.0.0",
Expand Down
29 changes: 29 additions & 0 deletions frontend/tooldrawer/components/_svg-icons/fontawesome.ts
@@ -0,0 +1,29 @@
/*
* Compute JSON.stringifiable object containing SVG markup for a
* select subset of font-awesome icons.
*
* We do this at compile time, to reduce our compiled bundle size.
* Using @fortawesome/fontawesome-svg-core at runtime pulls in a large
* chunk of code that more than doubles our bundle size.
*/
import { icon } from "@fortawesome/fontawesome-svg-core";
import { fas } from "@fortawesome/free-solid-svg-icons";

const iconNames = [
"faAngleLeft",
"faAngleRight",
"faFilePen",
"faRetweet",
"faThumbtack",
];

function svgFor(iconName: string) {
if (iconName in fas) {
return icon(fas[iconName]).html.join("");
}
throw new Error(`unknown icon name "${iconName}"`);
}

export default Object.fromEntries(
iconNames.map((name) => [name, svgFor(name)]),
);

0 comments on commit 1255d0f

Please sign in to comment.