swarm-ui: rewire a notification flow by drag or keyboard
The flow editor can now change a flow's shape on the canvas, not only through the inspector's fields: - drag a box onto another to move it and its branch there; a drop the save would refuse shows the target dashed red and gives the reason; - a palette of node kinds adds a node below the picked box on click, or below any box it is dragged onto; it offers what the controller lists; - arrow keys walk the tree, M picks a box up and Enter drops it under the focused one, Alt+Up/Down reorder a node among its siblings, Delete removes a branch; focus follows the edit; - each box with siblings says when it runs, and the inspector can move it earlier or later, since the router runs children in edge order; - request_reviewer is a second leaf kind beside todo, with a reviewer field; - leaving with unsaved edits asks first, also for in-app links and the back button (a router hook runs navigation past a page's guard).
This commit is contained in:
parent
e01770c056
commit
68e289fff5
16 changed files with 1072 additions and 133 deletions
|
|
@ -0,0 +1,88 @@
|
|||
// Stops a page with unsaved edits from losing them without asking.
|
||||
//
|
||||
// Closing or reloading the tab gets the browser's own prompt. A move inside the
|
||||
// app goes past the nav guard, and the browser back button is caught with a
|
||||
// spare history entry: while there are edits the page pushes a copy of its own
|
||||
// URL, so back lands on the same page and the prompt can be shown.
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { navigate } from "wouter-preact/use-browser-location";
|
||||
import { setNavGuard } from "../../shell/navGuard.js";
|
||||
|
||||
// `to` is where the person was headed, or null for the back button.
|
||||
interface Pending {
|
||||
to: string | null;
|
||||
}
|
||||
|
||||
export function useLeaveGuard(dirty: boolean): {
|
||||
pending: boolean;
|
||||
leave: () => void;
|
||||
stay: () => void;
|
||||
// Lets the page's own navigation through, such as after deleting the flow.
|
||||
release: () => void;
|
||||
} {
|
||||
const [pending, setPending] = useState<Pending | null>(null);
|
||||
const spare = useRef(false);
|
||||
const released = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dirty) return;
|
||||
const warn = (e: BeforeUnloadEvent) => e.preventDefault();
|
||||
window.addEventListener("beforeunload", warn);
|
||||
return () => window.removeEventListener("beforeunload", warn);
|
||||
}, [dirty]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dirty) return;
|
||||
setNavGuard((to) => {
|
||||
if (released.current) return true;
|
||||
setPending({ to });
|
||||
return false;
|
||||
});
|
||||
return () => setNavGuard(null);
|
||||
}, [dirty]);
|
||||
|
||||
useEffect(() => {
|
||||
const onPop = () => {
|
||||
if (!spare.current) return;
|
||||
if (dirty && !released.current) {
|
||||
// Back landed on the page again; the spare entry is used up.
|
||||
spare.current = false;
|
||||
setPending({ to: null });
|
||||
} else {
|
||||
// Nothing left to protect: go on to where back was meant to go.
|
||||
spare.current = false;
|
||||
history.back();
|
||||
}
|
||||
};
|
||||
window.addEventListener("popstate", onPop);
|
||||
if (dirty && !spare.current) {
|
||||
history.pushState(history.state, "", location.href);
|
||||
spare.current = true;
|
||||
}
|
||||
return () => window.removeEventListener("popstate", onPop);
|
||||
}, [dirty]);
|
||||
|
||||
const stay = useCallback(() => {
|
||||
if (pending?.to === null && !spare.current) {
|
||||
history.pushState(history.state, "", location.href);
|
||||
spare.current = true;
|
||||
}
|
||||
setPending(null);
|
||||
}, [pending]);
|
||||
|
||||
const leave = useCallback(() => {
|
||||
released.current = true;
|
||||
setNavGuard(null);
|
||||
const to = pending?.to;
|
||||
setPending(null);
|
||||
if (to === null) history.back();
|
||||
else if (to !== undefined) navigate(to);
|
||||
}, [pending]);
|
||||
|
||||
const release = useCallback(() => {
|
||||
released.current = true;
|
||||
setNavGuard(null);
|
||||
}, []);
|
||||
|
||||
return { pending: pending !== null, leave, stay, release };
|
||||
}
|
||||
Loading…
Reference in a new issue