diff --git a/client/src/App.tsx b/client/src/App.tsx index 275c2961..153fb034 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -16,7 +16,7 @@ import { codeAtom } from './editor/code-atoms' import { Menu } from './navigation/Navigation' import { mobileAtom, settingsAtom } from './settings/settings-atoms' import { lightThemes } from './settings/settings-types' -import { freshlyImportedCodeAtom } from './store/import-atoms' +import { importedCodeAtom } from './store/import-atoms' import { currentProjectAtom } from './store/project-atoms' import { screenWidthAtom } from './store/window-atoms' import { save } from './utils/SaveToFile' @@ -37,7 +37,7 @@ function App() { const [, setScreenWidth] = useAtom(screenWidthAtom) const [project] = useAtom(currentProjectAtom) const [code, setCode] = useAtom(codeAtom) - const [freshlyImportedCode] = useAtom(freshlyImportedCodeAtom) + const [importedCode] = useAtom(importedCodeAtom) const model = editor?.getModel() @@ -211,8 +211,8 @@ function App() { /** Set editor content to the code loaded from the URL */ useEffect(() => { - if (freshlyImportedCode && model) model.setValue(freshlyImportedCode) - }, [freshlyImportedCode, model]) + if (importedCode && model) model.setValue(importedCode) + }, [importedCode, model]) // Disable monaco context menu outside the editor useEffect(() => { diff --git a/client/src/editor/code-atoms.ts b/client/src/editor/code-atoms.ts index 16af6f54..bf873917 100644 --- a/client/src/editor/code-atoms.ts +++ b/client/src/editor/code-atoms.ts @@ -2,7 +2,7 @@ import { atom } from 'jotai' import LZString from 'lz-string' import { settingsAtom } from '../settings/settings-atoms' -import { freshlyImportedCodeAtom, importedCodeAtom, importUrlAtom } from '../store/import-atoms' +import { importedCodeAtom, importUrlAtom, importUrlBaseAtom } from '../store/import-atoms' import { urlArgsAtom, urlArgsStableAtom } from '../store/url-atoms' import { fixedEncodeURIComponent } from '../utils/UrlParsing' @@ -10,41 +10,47 @@ import { fixedEncodeURIComponent } from '../utils/UrlParsing' export const codeAtom = atom( (get) => { const urlArgs = get(urlArgsStableAtom) - if (urlArgs.code) { - return decodeURIComponent(urlArgs.code) + if (urlArgs.url) { + return get(importedCodeAtom) + } else if (urlArgs.code) { + return urlArgs.code } if (urlArgs.codez) { return LZString.decompressFromBase64(urlArgs.codez) + } else { + return '' } - return get(freshlyImportedCodeAtom) }, (get, set, code: string) => { const urlArgs = get(urlArgsAtom) + if (urlArgs.url) { + // store the import URL so we can display it later again + set(importUrlBaseAtom, urlArgs.url) + } if (code.length == 0) { // delete all url arguments if there is no code set(urlArgsAtom, { ...urlArgs, url: undefined, code: undefined, codez: undefined }) return } - if (urlArgs.url) { - // store freshly imported code and the URL so we can compare later - const freshlyImportedCode = get(freshlyImportedCodeAtom) - set(importedCodeAtom, freshlyImportedCode) - set(importUrlAtom, urlArgs.url) - } else { - // if the code matches previously imported code, display the URL instead - const importedCode = get(importedCodeAtom) - if (importedCode !== undefined && importedCode === code) { - const importUrl = get(importUrlAtom) - set(urlArgsAtom, { ...urlArgs, url: importUrl, code: undefined, codez: undefined }) - return - } - } - const compress = get(settingsAtom).compress - if (compress) { + const importedCode = get(importedCodeAtom) + const url = get(importUrlAtom) ?? '' + if (code == importedCode) { + set(urlArgsAtom, { + ...urlArgs, + url: fixedEncodeURIComponent(url), + code: undefined, + codez: undefined, + }) + } else if (get(settingsAtom).compress) { // LZ padds the string with trailing `=`, which mess up the argument parsing // and aren't needed for LZ encoding, so we remove them. const compressedCode = LZString.compressToBase64(code).replace(/=*$/, '') - set(urlArgsAtom, { ...urlArgs, url: undefined, code: undefined, codez: compressedCode }) + set(urlArgsAtom, { + ...urlArgs, + url: undefined, + code: undefined, + codez: fixedEncodeURIComponent(compressedCode), + }) } else { const encodedCode = fixedEncodeURIComponent(code) set(urlArgsAtom, { ...urlArgs, url: undefined, code: encodedCode, codez: undefined }) diff --git a/client/src/settings/SettingsPopup.tsx b/client/src/settings/SettingsPopup.tsx index 51997010..a386ab5d 100644 --- a/client/src/settings/SettingsPopup.tsx +++ b/client/src/settings/SettingsPopup.tsx @@ -6,7 +6,7 @@ import { useState } from 'react' import { Popup } from '../navigation/Popup' import { shallowEqualSubset } from '../utils/shallowEqual' -import { applySettingsAtom, settingsAtom } from './settings-atoms' +import { settingsAtom } from './settings-atoms' import type { MobileValues, Theme } from './settings-types' import { defaultSettings, Settings } from './settings-types' @@ -19,8 +19,7 @@ export function SettingsPopup({ handleClose: () => void closeNav: () => void }) { - const [settings, setSettings] = useAtom(settingsAtom) - const [, applySettings] = useAtom(applySettingsAtom) + const [settings, applySettings] = useAtom(settingsAtom) const [newSettings, setNewSettings] = useState(settings) function updateSetting(key: K, value: Settings[K]) { diff --git a/client/src/settings/settings-atoms.ts b/client/src/settings/settings-atoms.ts index e276b1e5..24f95a2e 100644 --- a/client/src/settings/settings-atoms.ts +++ b/client/src/settings/settings-atoms.ts @@ -33,35 +33,35 @@ const settingsBaseAtom = atom({ saved: false, inUrl: false, ...default * - current (local) state (base) * - default values (base) */ -export const settingsAtom = atom((get) => { - const base = get(settingsBaseAtom) - const store = cleanObject(get(settingsStoreAtom)) - const url = cleanObject(get(settingsUrlStableAtom)) - return { - ...base, - ...store, - ...url, - saved: Object.entries(store).length > 0, - inUrl: Object.entries(url).length > 0, - } as Settings -}) - -/** Set the new settings, and write them to browser storage or URL if desired */ -export const applySettingsAtom = atom(null, (get, set, val: Settings) => { - const { saved, inUrl, ...settingsToStore } = val +export const settingsAtom = atom( + (get) => { + const base = get(settingsBaseAtom) + const store = cleanObject(get(settingsStoreAtom)) + const url = cleanObject(get(settingsUrlStableAtom)) + return { + ...base, + ...store, + ...url, + saved: Object.entries(store).length > 0, + inUrl: Object.entries(url).length > 0, + } as Settings + }, + (get, set, val: Settings) => { + const { saved, inUrl, ...settingsToStore } = val - set(settingsBaseAtom, val) + set(settingsBaseAtom, val) - if (saved) { - set(settingsStoreAtom, settingsToStore) - } else { - localStorage.removeItem('lean4web:settings') - } + if (saved) { + set(settingsStoreAtom, settingsToStore) + } else { + localStorage.removeItem('lean4web:settings') + } - const newSearchParams = inUrl ? encodeSettingsToURL(settingsToStore) : new URLSearchParams() - const location = get(locationAtom) - set(locationAtom, { ...location, searchParams: newSearchParams }) -}) + const newSearchParams = inUrl ? encodeSettingsToURL(settingsToStore) : new URLSearchParams() + const location = get(locationAtom) + set(locationAtom, { ...location, searchParams: newSearchParams }) + }, +) /** Indicates whether mobile layout should be used */ export const mobileAtom = atom((get) => { diff --git a/client/src/store/import-atoms.ts b/client/src/store/import-atoms.ts index 0ae91372..c59d8344 100644 --- a/client/src/store/import-atoms.ts +++ b/client/src/store/import-atoms.ts @@ -1,7 +1,8 @@ import { atom } from 'jotai' import { atomWithQuery } from 'jotai-tanstack-query' -import { lookupUrl } from '../utils/UrlParsing' +import { fixedEncodeURIComponent, lookupUrl } from '../utils/UrlParsing' +import { currentProjectAtom } from './project-atoms' import { urlArgsAtom, urlArgsStableAtom } from './url-atoms' /** @@ -10,19 +11,29 @@ import { urlArgsAtom, urlArgsStableAtom } from './url-atoms' * This is needed for the comparison which puts the URL back into the location hash if * the current code matches the one from the import-URL. */ -export const importUrlAtom = atom() +export const importUrlBaseAtom = atom() /** - * Stores the imported code. * - * This is needed for the comparison which puts the URL back into the location hash if - * the current code matches the one from the import-URL. + * */ -export const importedCodeAtom = atom() +export const importUrlAtom = atom( + (get) => get(urlArgsStableAtom).url ?? get(importUrlBaseAtom), + (get, set, url: string) => { + const urlArgs = get(urlArgsStableAtom) + set(importUrlBaseAtom, url) + set(urlArgsAtom, { + ...urlArgs, + url: fixedEncodeURIComponent(url), + code: undefined, + codez: undefined, + }) + }, +) /** Query to fetch the code from the import URL */ -const freshlyImportedCodeQueryAtom = atomWithQuery((get) => { - const url = get(urlArgsStableAtom).url +const importedCodeQueryAtom = atomWithQuery((get) => { + const url = get(importUrlAtom) return { queryKey: ['importedCode', url], queryFn: async () => { @@ -31,13 +42,19 @@ const freshlyImportedCodeQueryAtom = atomWithQuery((get) => { const code = res.ok ? await res.text() : `Error: failed to load code from ${url}` return code }, - enabled: url !== undefined, + enabled: url != undefined, keepPreviousData: true, } }) -export const freshlyImportedCodeAtom = atom((get) => { - const { data } = get(freshlyImportedCodeQueryAtom) +/** + * Stores the imported code. + * + * This is needed for the comparison which puts the URL back into the location hash if + * the current code matches the one from the import-URL. + */ +export const importedCodeAtom = atom((get) => { + const { data } = get(importedCodeQueryAtom) return data }) @@ -49,13 +66,9 @@ export const freshlyImportedCodeAtom = atom((get) => { export const setImportUrlAndProjectAtom = atom( null, (get, set, val: { url: string; project?: string }) => { - const urlArgs = get(urlArgsStableAtom) - set(urlArgsAtom, { - ...urlArgs, - url: val.url, - project: val.project ?? urlArgs.project, - code: undefined, - codez: undefined, - }) + set(importUrlAtom, val.url) // TODO: should there be some decoding of the input? + if (val.project) { + set(currentProjectAtom, val.project) + } }, ) diff --git a/client/src/store/project-atoms.ts b/client/src/store/project-atoms.ts index 6c342cbc..4a4385c1 100644 --- a/client/src/store/project-atoms.ts +++ b/client/src/store/project-atoms.ts @@ -4,7 +4,7 @@ import { atomWithQuery } from 'jotai-tanstack-query' import { LeanWebProject } from '../api/project-types' import { urlArgsAtom, urlArgsStableAtom } from './url-atoms' -const projectsQueryAtom = atomWithQuery((get) => ({ +const projectsQueryAtom = atomWithQuery(() => ({ queryKey: ['projects'], queryFn: async () => { const res = await fetch(`/api/projects`) diff --git a/client/src/store/url-atoms.ts b/client/src/store/url-atoms.ts index f84792c1..7aef1d97 100644 --- a/client/src/store/url-atoms.ts +++ b/client/src/store/url-atoms.ts @@ -21,4 +21,4 @@ export const urlArgsAtom = atom( ) // Prevent updates unless there is a value change -export const urlArgsStableAtom = selectAtom(urlArgsAtom, (settings) => settings, shallowEqual) +export const urlArgsStableAtom = selectAtom(urlArgsAtom, (args) => args, shallowEqual) diff --git a/client/src/store/url-converters.ts b/client/src/store/url-converters.ts index 0c7a792a..e022286d 100644 --- a/client/src/store/url-converters.ts +++ b/client/src/store/url-converters.ts @@ -26,5 +26,6 @@ export function parseArgs(hash: string): UrlArgs { .split('&') .map((s) => s.split('=')) .filter((x) => x[0]) + .map(([key, val]) => [key, decodeURIComponent(val)]) return Object.fromEntries(args) } diff --git a/doc/Development.md b/doc/Development.md index d046522c..d105daa7 100644 --- a/doc/Development.md +++ b/doc/Development.md @@ -76,6 +76,7 @@ graph TD; %% setter importUrlAtom -.-> urlArgsAtom; codeAtom -.-> urlArgsAtom; + codeAtom -.-> importUrlBaseAtom; settingsAtom -.-> locationAtom; setImportUrlAndProjectAtom[/setImportUrlAndProjectAtom/] @@ -83,7 +84,6 @@ graph TD; currentProjectAtom <-.-> setImportUrlAndProjectAtom %% Styles - linkStyle 16 stroke: red; linkStyle 17 stroke: red; linkStyle 18 stroke: red; @@ -94,6 +94,7 @@ graph TD; linkStyle 23 stroke: red; linkStyle 24 stroke: red; linkStyle 25 stroke: red; + linkStyle 26 stroke: red; classDef query fill:#d0ebff,stroke:#1c7ed6,stroke-width:2px; classDef storage fill:#d3f9d8,stroke:#2b8a3e,stroke-width:2px; @@ -112,5 +113,4 @@ graph TD; %% Setter class setImportUrlAndProjectAtom setter; - class applySettingsAtom setter; ``` diff --git a/package-lock.json b/package-lock.json index c391de3a..762377bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "leanweb", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "leanweb", - "version": "0.2.0", + "version": "0.2.1", "workspaces": [ "client", "server"