Skip to content

New sandcastle title and dirty state tracking #12794

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: sandcastle-v2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/sandcastle/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sandcastle Reborn</title>
<title>Cesium Sandcastle</title>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<title>Cesium Sandcastle</title>
<title>CesiumJS Sandcastle</title>

I think, technically speaking, this should be the branding?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly? I was just matching what the current Sandcastle uses

<style>
/* Load fonts for itwin-ui */
@font-face {
Expand Down
82 changes: 72 additions & 10 deletions packages/sandcastle/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ const defaultHtmlCode = `<style>
`;

const GALLERY_BASE = __GALLERY_BASE_URL__;
const cesiumVersion = __CESIUM_VERSION__;
const versionString = __COMMIT_SHA__
? `Commit: ${__COMMIT_SHA__.substring(0, 7)} - ${cesiumVersion}`
: cesiumVersion;

type RightSideRef = {
toggleExpanded: () => void;
Expand Down Expand Up @@ -164,6 +168,7 @@ function AppBarButton({

export type SandcastleAction =
| { type: "reset" }
| { type: "resetDirty" }
| { type: "setCode"; code: string }
| { type: "setHtml"; html: string }
| { type: "runSandcastle" }
Expand All @@ -178,9 +183,6 @@ function App() {
const consoleCollapsedHeight = 26;
const [consoleExpanded, setConsoleExpanded] = useState(false);

const cesiumVersion = __CESIUM_VERSION__;
const versionString = __COMMIT_SHA__ ? `Commit: ${__COMMIT_SHA__}` : "";

const startOnEditor = !!(window.location.search || window.location.hash);
const [leftPanel, setLeftPanel] = useState<"editor" | "gallery">(
startOnEditor ? "editor" : "gallery",
Expand All @@ -196,6 +198,7 @@ function App() {
committedCode: string;
committedHtml: string;
runNumber: number;
dirty: boolean;
};

const initialState: CodeState = {
Expand All @@ -204,6 +207,7 @@ function App() {
committedCode: defaultJsCode,
committedHtml: defaultHtmlCode,
runNumber: 0,
dirty: false,
};

const [codeState, dispatch] = useReducer(function reducer(
Expand All @@ -218,12 +222,14 @@ function App() {
return {
...state,
code: action.code,
dirty: true,
};
}
case "setHtml": {
return {
...state,
html: action.html,
dirty: true,
};
}
case "runSandcastle": {
Expand All @@ -241,11 +247,46 @@ function App() {
committedCode: action.code ?? state.code,
committedHtml: action.html ?? state.html,
runNumber: state.runNumber + 1,
dirty: false,
};
}
case "resetDirty": {
return {
...state,
dirty: false,
};
}
}
}, initialState);

useEffect(() => {
const host = window.location.host;
let envString = "";
if (host.includes("localhost") && host !== "localhost:8080") {
// this helps differentiate tabs for local sandcastle development or other testing
envString = `${host.replace("localhost:", "")} `;
}

const baseTitle = "Cesium Sandcastle";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const baseTitle = "Cesium Sandcastle";
const baseTitle = "CesiumJS Sandcastle";

or possibly even just the following due to all the other info?

Suggested change
const baseTitle = "Cesium Sandcastle";
const baseTitle = "Sandcastle";

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want this to match whatever title we pick for the base page so the specific sandcastle title is just prepended. I'm open to dropping down to just "Sandcastle" but I wasn't sure about dropping the branding

const dirtyIndicator = codeState.dirty ? "*" : "";
if (title !== "") {
document.title = `${envString}${title}${dirtyIndicator} | ${baseTitle}`;
} else {
document.title = `${envString}${baseTitle}`;
}
Comment on lines +272 to +276
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Why shouldn't we show the dirty indicator on named sandcastles?
  2. Assuming we can, couldn't we simplify this to thew following?
Suggested change
if (title !== "") {
document.title = `${envString}${title}${dirtyIndicator} | ${baseTitle}`;
} else {
document.title = `${envString}${baseTitle}`;
}
document.title = `${envString}${title}${dirtyIndicator} | ${baseTitle}`;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check makes sure that we only add the title and the | to the page title when there's actually a title to prepend in front of the bar. The page starts with an empty title "" in state when it initially loads because we don't know what it will be yet.
If a specific gallery demo is not loaded then we replace it with "New Sandcastle" thus providing a title and it will show the dirty indicator.

}, [title, codeState.dirty]);

const confirmLeave = useCallback(() => {
if (!codeState.dirty) {
return true;
}

/* eslint-disable-next-line no-alert */
return window.confirm(
"You have unsaved changes. Are you sure you want to navigate away from this demo?",
);
Comment on lines +285 to +287
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is totally up-to-par with what we had before, but what do you think of propagating the current sandcastle to local storage or similar?

The goal would to be to persist state, like what happens when you reload a GitHub tab with an in-progress comment. Or maybe a slightly different workflow, like what happens if you leave and come back to https://geojson.io/.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think of propagating the current sandcastle to local storage or similar?

This (or similar) has been requested often and I do really want to find some solution to try and prevent lost work. However I think it should be a separate effort, this PR just tries to match the functionality of the existing sandcastle. This is already on the running task list but further down for lower priority.happy to re-evaluate if you think it's a "must have" before release.

My current thought is not to make it truly persist every page load but offer a "saved state" that we can suggest the user reload like jsfiddle does

image

}, [codeState.dirty]);

const [legacyIdMap, setLegacyIdMap] = useState<Record<string, string>>({});
const [galleryItems, setGalleryItems] = useState<GalleryItem[]>([]);
const [galleryLoaded, setGalleryLoaded] = useState(false);
Expand Down Expand Up @@ -277,6 +318,9 @@ function App() {
}

function resetSandcastle() {
if (!confirmLeave()) {
return;
}
dispatch({ type: "reset" });

window.history.pushState({}, "", getBaseUrl());
Expand All @@ -292,6 +336,7 @@ function App() {

const shareUrl = `${getBaseUrl()}#c=${base64String}`;
window.history.replaceState({}, "", shareUrl);
dispatch({ type: "resetDirty" });
}

function openStandalone() {
Expand Down Expand Up @@ -407,12 +452,27 @@ function App() {
useEffect(() => {
// Listen to browser forward/back navigation and try to load from URL
// this is necessary because of the pushState used for faster gallery loading
function pushStateListener() {
loadFromUrl();
function popStateListener() {
if (confirmLeave()) {
loadFromUrl();
}
}
window.addEventListener("popstate", pushStateListener);
return () => window.removeEventListener("popstate", pushStateListener);
}, [loadFromUrl]);
window.addEventListener("popstate", popStateListener);
return () => window.removeEventListener("popstate", popStateListener);
}, [loadFromUrl, confirmLeave]);

useEffect(() => {
// if the code has been edited listen for navigation away and warn
if (codeState.dirty) {
function beforeUnloadListener(e: BeforeUnloadEvent) {
e.preventDefault();
return ""; // modern browsers ignore the contents of this string
}
window.addEventListener("beforeunload", beforeUnloadListener);
return () =>
window.removeEventListener("beforeunload", beforeUnloadListener);
}
}, [codeState.dirty]);

return (
<Root
Expand Down Expand Up @@ -443,8 +503,7 @@ function App() {
</Button>
<div className="flex-spacer"></div>
<div className="version">
{versionString && <pre>{versionString.substring(0, 7)} - </pre>}
<pre>{cesiumVersion}</pre>
{versionString && <pre>{versionString}</pre>}
</div>
</header>
<div className="application-bar">
Expand Down Expand Up @@ -505,6 +564,9 @@ function App() {
hidden={leftPanel !== "gallery"}
galleryItems={galleryItems}
loadDemo={(item, switchToCode) => {
if (!confirmLeave()) {
return;
}
// Load the gallery item every time it's clicked
loadGalleryItem(item.id);

Expand Down
7 changes: 0 additions & 7 deletions packages/sandcastle/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@ import { createRoot } from "react-dom/client";
import "./reset.css"; // TODO: this may not be needed with itwin-ui
import App from "./App.tsx";

const host = window.location.host;
if (host.includes("localhost")) {
document.title = `${host.replace("localhost:", "")} ${document.title}`;
} else if (host.includes("ci")) {
document.title = `CI ${document.title}`;
}

createRoot(document.getElementById("app-container")!).render(
<StrictMode>
<App />
Expand Down
2 changes: 2 additions & 0 deletions packages/sandcastle/vite.config.ci.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineConfig, UserConfig } from "vite";
import { viteStaticCopy } from "vite-plugin-static-copy";
import { env } from "process";

import baseConfig, { cesiumPathReplace } from "./vite.config.ts";

Expand All @@ -22,6 +23,7 @@ export default defineConfig(() => {
...config.define,
__PAGE_BASE_URL__: JSON.stringify(process.env.BASE_URL),
__GALLERY_BASE_URL__: JSON.stringify(`${config.base}/gallery`),
__COMMIT_SHA__: JSON.stringify(env.GITHUB_SHA),
};

const copyPlugin = viteStaticCopy({
Expand Down