mirror of
https://github.com/logseq/logseq.git
synced 2026-05-23 12:14:06 +00:00
enhance: use graph identity in URLs (#12693)
* enhance: use graph identity in URLs * fix: resolve graph identity URLs * fix: satisfy graph url lint * fix: resolve graph id from hash urls * fix: preserve tab graph on reload * fix: initialize tab graph before render * enhance: open graph in another tab * fix: open graph tabs by graph id * fix: open local graph tabs by registry id * fix: avoid duplicate rtc graph id * fix: open electron graph window on shift click * fix: address graph identity review
This commit is contained in:
3
deps/common/.carve/config.edn
vendored
3
deps/common/.carve/config.edn
vendored
@@ -11,5 +11,6 @@
|
||||
logseq.common.cognito-config
|
||||
logseq.common.config
|
||||
logseq.common.defkeywords
|
||||
logseq.common.graph-dir]
|
||||
logseq.common.graph-dir
|
||||
logseq.common.graph-registry]
|
||||
:report {:format :ignore}}
|
||||
|
||||
72
deps/common/src/logseq/common/graph_registry.cljc
vendored
Normal file
72
deps/common/src/logseq/common/graph_registry.cljc
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
(ns logseq.common.graph-registry
|
||||
"Graph registry normalization and lookup helpers."
|
||||
(:require [clojure.string :as string]))
|
||||
|
||||
(def ^:private db-version-prefix
|
||||
"logseq_db_")
|
||||
|
||||
(defn- present-string?
|
||||
[s]
|
||||
(and (string? s) (not (string/blank? s))))
|
||||
|
||||
(defn normalize-entry
|
||||
[entry]
|
||||
(let [local-graph-id (:local-graph-id entry)
|
||||
graph-id (or (when (present-string? (:graph-id entry)) (:graph-id entry))
|
||||
(when (present-string? local-graph-id) local-graph-id))]
|
||||
(when-not graph-id
|
||||
(throw (ex-info "Missing graph identity"
|
||||
{:entry entry})))
|
||||
(-> entry
|
||||
(assoc :graph-id graph-id)
|
||||
(dissoc :rtc-graph-id))))
|
||||
|
||||
(defn ^:api upsert-entry
|
||||
[registry entry]
|
||||
(let [entry' (normalize-entry entry)]
|
||||
(->> (or registry [])
|
||||
(remove #(or (= (:graph-id entry') (:graph-id %))
|
||||
(and (present-string? (:repo entry'))
|
||||
(= (:repo entry') (:repo %)))
|
||||
(and (present-string? (:local-graph-id entry'))
|
||||
(= (:local-graph-id entry') (:local-graph-id %)))))
|
||||
(cons entry')
|
||||
vec)))
|
||||
|
||||
(defn- normalize-comparable
|
||||
[s]
|
||||
(some-> s str string/lower-case))
|
||||
|
||||
(defn- canonical-repo
|
||||
[s]
|
||||
(when (seq s)
|
||||
(let [s (str s)
|
||||
stripped (loop [name' s]
|
||||
(if (string/starts-with? name' db-version-prefix)
|
||||
(recur (subs name' (count db-version-prefix)))
|
||||
name'))]
|
||||
(str db-version-prefix stripped))))
|
||||
|
||||
(defn- identifier-match?
|
||||
[entry graph-identifier]
|
||||
(let [identifier (normalize-comparable graph-identifier)
|
||||
repo (normalize-comparable (:repo entry))
|
||||
graph-name (normalize-comparable (:graph-name entry))
|
||||
graph-id (normalize-comparable (:graph-id entry))
|
||||
canonical-repo-name (normalize-comparable (canonical-repo graph-identifier))]
|
||||
(or (= identifier repo)
|
||||
(= identifier graph-name)
|
||||
(= identifier graph-id)
|
||||
(= canonical-repo-name repo))))
|
||||
|
||||
(defn ^:api resolve-target
|
||||
[registry {:keys [graph-id graph-identifier]}]
|
||||
(cond
|
||||
(present-string? graph-id)
|
||||
(some #(when (= graph-id (:graph-id %)) %) registry)
|
||||
|
||||
(present-string? graph-identifier)
|
||||
(some #(when (identifier-match? % graph-identifier) %) registry)
|
||||
|
||||
:else
|
||||
nil))
|
||||
195
docs/adr/0017-url-target-graph-resolution.md
Normal file
195
docs/adr/0017-url-target-graph-resolution.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# ADR 0017: URL Target Graph Resolution
|
||||
|
||||
Date: 2026-05-20
|
||||
Status: Accepted
|
||||
|
||||
## Context
|
||||
Logseq URLs can point at a graph, page, block, or file. Today the graph target is
|
||||
not carried through one coherent flow.
|
||||
|
||||
The custom protocol path is handled in Electron main:
|
||||
- `logseq://graph/<graph>` and `logseq://new-window/<graph>` are parsed in
|
||||
`electron.url/local-url-handler`.
|
||||
- Electron resolves `<graph>` through `electron.handler/get-graph-name`.
|
||||
- If no window is already associated with that graph, Electron asks the current
|
||||
renderer to run `openNewWindowOfGraph`.
|
||||
- The renderer then opens a new Electron window with `#/?graph=<repo>`.
|
||||
|
||||
This depends on renderer IPC timing and renderer-local storage. On first app
|
||||
launch, the initial deeplink is handled soon after the main window is created,
|
||||
while the renderer listener can still be unavailable. When the message is lost,
|
||||
the newly opened app falls back to the stored `:git/current-repo`, so the URL
|
||||
appears to always open the current graph.
|
||||
|
||||
The web app has a related weakness. `frontend.state` only reads `:graph` from
|
||||
`frontend.util/parse-params` during initial state creation, and that parser only
|
||||
understands hash fragments shaped like `#/?graph=...`. Page/block web URLs and
|
||||
later hash changes can route inside the currently opened graph instead of first
|
||||
switching to the graph named by the URL.
|
||||
|
||||
Graph names are not stable identifiers. Users can rename graphs, and two linked
|
||||
graphs can share the same short basename. URLs that are meant to survive
|
||||
renames, browser sessions, and device handoffs need to identify the graph by id.
|
||||
|
||||
## Decision
|
||||
Make URL graph selection an explicit target object and process it before route
|
||||
navigation in the renderer, with a small persisted graph registry used to turn
|
||||
stable graph ids into local repos.
|
||||
|
||||
1. Introduce a shared URL target model for Electron protocol URLs, web URLs, and
|
||||
mobile URLs:
|
||||
- `:graph-id` - canonical graph id from the URL.
|
||||
- `:graph-identifier` - compatibility alias, such as a short graph name or
|
||||
full DB repo name.
|
||||
- `:repo` - resolved full local repo name, for example `logseq_db_work`.
|
||||
- `:route` - one of graph home, page, block, or file.
|
||||
- `:open-mode` - current window or new window.
|
||||
- `:source` - protocol URL, web URL, startup argv, or second instance.
|
||||
2. Use `:graph-id` as the canonical URL identity. For remote graphs,
|
||||
`:graph-id` is the remote graph UUID. For local-only graphs, `:graph-id` is
|
||||
the local graph UUID. Generated URLs should include `graph-id` whenever the
|
||||
graph id is known.
|
||||
3. Resolve URL graph targets through one graph resolver that accepts, in order:
|
||||
- graph ids from the local graph registry;
|
||||
- full DB repo names, such as `logseq_db_work`;
|
||||
- short local graph names, such as `work`;
|
||||
- existing canonicalized DB repo names.
|
||||
4. Treat an unresolved graph id as an unresolved URL target, not as a successful
|
||||
match for the current graph. Mobile reports this to the user. Web startup
|
||||
currently logs the unresolved target and continues with the tab-local or
|
||||
stored graph so the app can still boot.
|
||||
5. For Electron custom protocol URLs, the main process owns URL intake and
|
||||
graph-name/graph-id resolution. This branch keeps the existing
|
||||
renderer-mediated window creation path for custom protocol URLs and teaches
|
||||
it to resolve graph ids through the registry.
|
||||
6. For web and mobile URLs, the renderer owns URL intake and graph resolution.
|
||||
It must switch to the target graph before applying the route target.
|
||||
7. Once the target graph is loaded, apply the route target exactly once.
|
||||
8. Do not store URL graph targets in `:git/current-repo` until the graph switch
|
||||
succeeds.
|
||||
9. Use `sessionStorage` for tab-local graph identity on web reloads that do not
|
||||
carry a URL graph target. This preserves each browser tab's graph without
|
||||
appending `graph-id` to every internal route.
|
||||
|
||||
## Supported URL Shapes
|
||||
Canonical generated web URLs use graph ids:
|
||||
- `https://logseq.com/?graph-id=<graph-uuid>`
|
||||
- `https://logseq.com/page/<page-uuid>?graph-id=<graph-uuid>`
|
||||
- `https://logseq.com/block/<block-uuid>?graph-id=<graph-uuid>`
|
||||
|
||||
Electron protocol URLs accept graph ids through the same graph identifier slot:
|
||||
- `logseq://graph/<graph-uuid>`
|
||||
- `logseq://graph/<graph-uuid>?page=<page-name-or-uuid>`
|
||||
- `logseq://graph/<graph-uuid>?block-id=<block-uuid>`
|
||||
- `logseq://new-window/<graph-uuid>?page=<page-name-or-uuid>`
|
||||
|
||||
Compatibility URLs remain accepted:
|
||||
- `logseq://graph/<graph-name>`
|
||||
- `logseq://graph/<graph-name>?page=<page-name>`
|
||||
- `logseq://graph/<graph-name>?block-id=<uuid>`
|
||||
- `logseq://graph/<graph-name>?file=<file>`
|
||||
- `https://logseq.com/#/?graph=<graph-name>`
|
||||
- `https://logseq.com/#/page/<page-name>?graph=<graph-name>`
|
||||
|
||||
Generated web URLs should use path routes instead of hash routes. The app may
|
||||
continue accepting hash-routed URLs during migration, but new share/copy-link
|
||||
helpers should emit path URLs with `graph-id`.
|
||||
|
||||
## Graph Registry
|
||||
URL graph-id resolution must not open every graph database during startup. Keep a
|
||||
small graph registry outside each graph DB.
|
||||
|
||||
1. Web and mobile store the graph registry in IndexedDB.
|
||||
2. Electron stores the graph registry in
|
||||
`(.getPath app "home")/.logseq/graphs.edn`.
|
||||
3. Registry entries include at least:
|
||||
- `:graph-id`
|
||||
- `:repo`
|
||||
- `:graph-name`
|
||||
- `:local-graph-id`
|
||||
- `:updated-at`
|
||||
4. Do not persist a separate `:rtc-graph-id` in the registry. For remote graphs,
|
||||
`:graph-id` is the remote graph UUID. For local-only graphs, `:graph-id` is
|
||||
the local graph UUID.
|
||||
5. Update the registry whenever a graph is created, opened, imported,
|
||||
downloaded, uploaded to RTC, renamed, unlinked, or deleted.
|
||||
6. If a registry lookup fails for a graph id, the app may run an explicit repair
|
||||
path that enumerates local graph storage and opens graph DBs to rebuild the
|
||||
registry. This repair path must not be the normal URL-open path.
|
||||
|
||||
## Electron Runtime Design
|
||||
1. Store the registry at `(.getPath app "home")/.logseq/graphs.edn`.
|
||||
2. Resolve Electron protocol graph identifiers through that registry before
|
||||
falling back to existing repo/name lookup.
|
||||
3. For shift-click in the graph list, open a new Electron window through the
|
||||
existing `openNewWindow` IPC using the resolved repo. Electron windows still
|
||||
use repo identity for window creation.
|
||||
4. Keep a per-window pending target in Electron state only for work that cannot
|
||||
be represented safely in the URL, such as a file redirect that requires graph
|
||||
readiness. Avoid a single global `:window/once-graph-ready` callback because
|
||||
concurrent windows can overwrite each other.
|
||||
5. When the renderer reports `graphReady`, Electron should look up and consume
|
||||
the pending target for that specific window id.
|
||||
6. Existing error notifications for missing or unknown graphs remain, but they
|
||||
should be sent to the selected/fallback window after graph resolution fails.
|
||||
7. Direct main-process creation of route-aware graph windows remains follow-up
|
||||
work; it should replace the renderer-mediated `openNewWindowOfGraph` path.
|
||||
|
||||
## Web Runtime Design
|
||||
1. Add a parser that can read graph targets from path URLs and compatibility
|
||||
hash URLs.
|
||||
2. During frontend startup:
|
||||
- parse the URL target;
|
||||
- resolve `:graph-id` through the IndexedDB graph registry;
|
||||
- resolve compatibility `:graph-identifier` against the known graph list when
|
||||
no `:graph-id` is present;
|
||||
- initialize from the resolved URL target when it resolves;
|
||||
- otherwise use the tab-local `sessionStorage` graph target, then the stored
|
||||
`:git/current-repo`, then the first linked repo.
|
||||
3. During startup, apply resolved page/block URL targets after the target graph
|
||||
is restored. Later in-app route changes with graph targets are follow-up
|
||||
work.
|
||||
4. During route generation:
|
||||
- generated page routes include `graph-id` when the current graph id is
|
||||
available, but startup/render-phase redirects tolerate a temporarily
|
||||
unavailable graph id instead of crashing.
|
||||
5. Generated web links from `frontend.util.url` should include `graph-id` on the
|
||||
actual route.
|
||||
6. Web "open in another tab" and shift-click from All graphs open `#/?graph-id=`
|
||||
URLs for existing local graphs.
|
||||
|
||||
## Mobile Runtime Design
|
||||
1. Use the same URL target parser and resolver as the web runtime.
|
||||
2. Store and resolve the graph registry from IndexedDB.
|
||||
3. Treat mobile app intents and universal links as URL sources that produce the
|
||||
same target model.
|
||||
|
||||
## Consequences
|
||||
- Resolvable graph-id URLs open the requested graph instead of silently using the
|
||||
last current graph.
|
||||
- Electron protocol URLs can resolve graph ids via the registry, but route-aware
|
||||
direct window creation remains follow-up work.
|
||||
- Web graph URLs become route-aware, so page and block links can switch graph
|
||||
before navigating during startup and mobile deeplink handling.
|
||||
- Web and mobile graph-id resolution is fast because it reads the graph registry
|
||||
instead of scanning OPFS graph directories and opening graph databases.
|
||||
- Electron graph-id resolution is fast because it reads
|
||||
`(.getPath app "home")/.logseq/graphs.edn` instead of opening each graph
|
||||
database.
|
||||
- Bare web reloads preserve the tab's graph through `sessionStorage`, so two
|
||||
tabs can remain on different graphs even when one tab changes the global
|
||||
current graph.
|
||||
- The implementation needs focused tests around URL parsing, graph resolution,
|
||||
first-launch Electron deeplinks, second-instance deeplinks, and web/mobile
|
||||
route changes.
|
||||
|
||||
## Follow-up Work
|
||||
1. Add Electron main tests for first-launch and second-instance deeplinks.
|
||||
2. Add renderer tests proving that web URLs switch graph before page/block
|
||||
navigation after graph switches triggered by later route changes, not only
|
||||
startup URLs.
|
||||
3. Add graph registry read/write tests for IndexedDB and the Electron
|
||||
`(.getPath app "home")/.logseq/graphs.edn` registry.
|
||||
4. Replace `:window/once-graph-ready` with per-window pending targets.
|
||||
5. Replace Electron custom protocol's renderer-mediated `openNewWindowOfGraph`
|
||||
path with direct main-process route-aware window creation.
|
||||
@@ -3,13 +3,18 @@
|
||||
["fs-extra" :as ^js fs]
|
||||
["path" :as ^js node-path]
|
||||
[cljs.reader :as reader]
|
||||
[electron.logger :as logger]))
|
||||
[electron.logger :as logger]
|
||||
[logseq.common.graph-registry :as graph-registry]))
|
||||
|
||||
;; FIXME: move configs.edn to where it should be
|
||||
(defonce dot-root (.join node-path (.getPath app "home") ".logseq"))
|
||||
(defonce cfg-root (.getPath app "userData"))
|
||||
(defonce cfg-path (.join node-path cfg-root "configs.edn"))
|
||||
|
||||
(defn graph-registry-path
|
||||
[]
|
||||
(.join node-path dot-root "graphs.edn"))
|
||||
|
||||
(defn- ensure-cfg
|
||||
[]
|
||||
(try
|
||||
@@ -40,3 +45,33 @@
|
||||
(defn get-config
|
||||
[]
|
||||
(ensure-cfg))
|
||||
|
||||
(defn- read-edn-file
|
||||
[path]
|
||||
(try
|
||||
(.ensureFileSync fs path)
|
||||
(let [body (.toString (.readFileSync fs path))]
|
||||
(if (seq body) (reader/read-string body) []))
|
||||
(catch :default e
|
||||
(logger/error :graph-registry-read-error e)
|
||||
[])))
|
||||
|
||||
(defn read-graph-registry
|
||||
[]
|
||||
(read-edn-file (graph-registry-path)))
|
||||
|
||||
(defn write-graph-registry!
|
||||
[registry]
|
||||
(try
|
||||
(.ensureDirSync fs dot-root)
|
||||
(.writeFileSync fs (graph-registry-path) (pr-str (vec registry)))
|
||||
(vec registry)
|
||||
(catch :default e
|
||||
(logger/error :graph-registry-write-error e)
|
||||
nil)))
|
||||
|
||||
(defn upsert-graph-registry-entry!
|
||||
[entry]
|
||||
(let [registry (read-graph-registry)
|
||||
registry' (graph-registry/upsert-entry registry entry)]
|
||||
(write-graph-registry! registry')))
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
[logseq.cli.common :as cli-common]
|
||||
[logseq.common.config :as common-config]
|
||||
[logseq.common.graph :as common-graph]
|
||||
[logseq.common.graph-registry :as graph-registry]
|
||||
[logseq.db.sqlite.util :as sqlite-util]
|
||||
[promesa.core :as p]))
|
||||
|
||||
@@ -206,18 +207,24 @@
|
||||
"Given a graph's name of string, returns the graph's fullname. For example, given
|
||||
`cat`, returns `logseq_db_cat`. Returns `nil` if no such graph exists."
|
||||
[graph-identifier]
|
||||
(when-let [repo (canonical-repo graph-identifier)]
|
||||
(let [graph-name (common-config/strip-leading-db-version-prefix repo)]
|
||||
(->> (get-graphs)
|
||||
(some #(when (or
|
||||
(= (utils/normalize-lc %) (utils/normalize-lc repo))
|
||||
(string/ends-with? (utils/normalize-lc %)
|
||||
(str "/" (utils/normalize-lc graph-name))))
|
||||
%))))))
|
||||
(or (:repo (graph-registry/resolve-target
|
||||
(cfgs/read-graph-registry)
|
||||
{:graph-identifier graph-identifier}))
|
||||
(when-let [repo (canonical-repo graph-identifier)]
|
||||
(let [graph-name (common-config/strip-leading-db-version-prefix repo)]
|
||||
(->> (get-graphs)
|
||||
(some #(when (or
|
||||
(= (utils/normalize-lc %) (utils/normalize-lc repo))
|
||||
(string/ends-with? (utils/normalize-lc %)
|
||||
(str "/" (utils/normalize-lc graph-name))))
|
||||
%)))))))
|
||||
|
||||
(defmethod handle :getGraphs [_window [_]]
|
||||
(get-graphs))
|
||||
|
||||
(defmethod handle :upsertGraphRegistryEntry [_window [_ entry]]
|
||||
(cfgs/upsert-graph-registry-entry! entry))
|
||||
|
||||
(defmethod handle :deleteGraph [_window [_ graph]]
|
||||
(when-let [repo (canonical-repo graph)]
|
||||
(p/let [_ (db-worker/release-repo! repo)]
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[frontend.handler.common.developer :as dev-common-handler]
|
||||
[frontend.handler.comments :as comments-handler]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.handler.graph :as graph-handler]
|
||||
[frontend.handler.notification :as notification]
|
||||
[frontend.handler.property :as property-handler]
|
||||
[frontend.handler.property.util :as pu]
|
||||
@@ -271,9 +272,10 @@
|
||||
(shui/dropdown-menu-item
|
||||
{:key "Copy block URL"
|
||||
:on-click (fn [_e]
|
||||
(let [current-repo (state/get-current-repo)
|
||||
tap-f (fn [block-id]
|
||||
(url-util/get-logseq-graph-uuid-url nil current-repo block-id))]
|
||||
(let [tap-f (fn [block-id]
|
||||
(url-util/get-logseq-web-block-url config/app-website
|
||||
(graph-handler/current-graph-id)
|
||||
block-id))]
|
||||
(editor-handler/copy-block-ref! block-id tap-f)))}
|
||||
(t :block/copy-url)))
|
||||
|
||||
|
||||
@@ -125,6 +125,34 @@
|
||||
(p/then (fn []
|
||||
(repo-handler/remove-repo! repo))))))
|
||||
|
||||
(defn graph-open-new-window-target
|
||||
[{:keys [url] :as repo}]
|
||||
(when-let [graph-id (:graph-id (graph/repo-summary->registry-entry repo))]
|
||||
{:repo url
|
||||
:graph-id graph-id}))
|
||||
|
||||
(defn- <graph-open-new-window-target
|
||||
[{:keys [url] :as repo}]
|
||||
(if-let [target (graph-open-new-window-target repo)]
|
||||
(p/resolved target)
|
||||
(p/let [registry (graph/<get-graph-registry)
|
||||
entry (graph/resolve-registry-target registry {:graph-identifier url})]
|
||||
(when entry
|
||||
{:repo (:repo entry)
|
||||
:graph-id (:graph-id entry)}))))
|
||||
|
||||
(defn open-in-another-tab-action?
|
||||
[{:keys [root]}]
|
||||
(boolean (and util/web-platform? root)))
|
||||
|
||||
(defn open-graph-in-another-tab!
|
||||
[repo]
|
||||
(if-let [target (graph-open-new-window-target repo)]
|
||||
(state/pub-event! [:graph/open-new-window target])
|
||||
(p/let [target (<graph-open-new-window-target repo)]
|
||||
(when target
|
||||
(state/pub-event! [:graph/open-new-window target])))))
|
||||
|
||||
(rum/defc ^:large-vars/cleanup-todo repos-inner
|
||||
"Graph list in `All graphs` page"
|
||||
[repos]
|
||||
@@ -171,6 +199,13 @@
|
||||
(ui/icon "dots" {:size 15})))
|
||||
(shui/dropdown-menu-content
|
||||
{:align "end"}
|
||||
(when (open-in-another-tab-action? repo)
|
||||
(shui/dropdown-menu-item
|
||||
{:key "open-in-another-tab"
|
||||
:class "open-in-another-tab-menu-item"
|
||||
:on-click #(open-graph-in-another-tab! repo)}
|
||||
(t :graph/open-in-another-tab-action)))
|
||||
|
||||
(when root
|
||||
(shui/dropdown-menu-item
|
||||
{:key "delete-locally"
|
||||
@@ -330,9 +365,14 @@
|
||||
(when (and ready-for-use? (not downloading?))
|
||||
(when-let [on-click (:on-click opts)]
|
||||
(on-click e))
|
||||
(if (and (gobj/get e "shiftKey")
|
||||
(not (and rtc-graph? remote?)))
|
||||
(state/pub-event! [:graph/open-new-window url])
|
||||
(if (and (gobj/get e "shiftKey") (:root graph))
|
||||
(if (util/electron?)
|
||||
(state/pub-event! [:graph/open-new-window url])
|
||||
(if-let [target (graph-open-new-window-target graph)]
|
||||
(state/pub-event! [:graph/open-new-window target])
|
||||
(p/let [target (<graph-open-new-window-target graph)]
|
||||
(when target
|
||||
(state/pub-event! [:graph/open-new-window target])))))
|
||||
(cond
|
||||
;; exists locally?
|
||||
(or (:root graph) (not rtc-graph?))
|
||||
|
||||
44
src/main/frontend/graph_tab.cljs
Normal file
44
src/main/frontend/graph_tab.cljs
Normal file
@@ -0,0 +1,44 @@
|
||||
(ns frontend.graph-tab
|
||||
"Tab-local graph context helpers."
|
||||
(:require [clojure.string :as string]
|
||||
[goog.object :as gobj]))
|
||||
|
||||
(def graph-id-key
|
||||
"ls-tab-graph-id")
|
||||
|
||||
(def repo-key
|
||||
"ls-tab-repo")
|
||||
|
||||
(defn- session-storage
|
||||
[]
|
||||
(gobj/get js/globalThis "sessionStorage"))
|
||||
|
||||
(defn get-tab-graph-id
|
||||
[]
|
||||
(when-let [storage (session-storage)]
|
||||
(let [graph-id (.getItem storage graph-id-key)]
|
||||
(when-not (string/blank? graph-id)
|
||||
graph-id))))
|
||||
|
||||
(defn get-tab-repo
|
||||
[]
|
||||
(when-let [storage (session-storage)]
|
||||
(let [repo (.getItem storage repo-key)]
|
||||
(when-not (string/blank? repo)
|
||||
repo))))
|
||||
|
||||
(defn get-tab-graph
|
||||
[]
|
||||
(let [repo (get-tab-repo)
|
||||
graph-id (get-tab-graph-id)]
|
||||
(when (or repo graph-id)
|
||||
{:repo repo
|
||||
:graph-id graph-id})))
|
||||
|
||||
(defn set-tab-graph!
|
||||
[repo graph-id]
|
||||
(when-let [storage (session-storage)]
|
||||
(when-not (string/blank? repo)
|
||||
(.setItem storage repo-key repo))
|
||||
(when-not (string/blank? graph-id)
|
||||
(.setItem storage graph-id-key graph-id))))
|
||||
@@ -21,12 +21,14 @@
|
||||
[frontend.handler.events.export]
|
||||
[frontend.handler.events.rtc]
|
||||
[frontend.handler.events.ui]
|
||||
[frontend.handler.graph :as graph-handler]
|
||||
[frontend.handler.global-config :as global-config-handler]
|
||||
[frontend.handler.page :as page-handler]
|
||||
[frontend.handler.plugin :as plugin-handler]
|
||||
[frontend.handler.plugin-config :as plugin-config-handler]
|
||||
[frontend.handler.repo :as repo-handler]
|
||||
[frontend.handler.repo-config :as repo-config-handler]
|
||||
[frontend.handler.route :as route-handler]
|
||||
[frontend.handler.ui :as ui-handler]
|
||||
[frontend.handler.user :as user-handler]
|
||||
[frontend.common.idb :as idb]
|
||||
@@ -36,6 +38,7 @@
|
||||
[frontend.persist-db :as persist-db]
|
||||
[frontend.state :as state]
|
||||
[frontend.util :as util]
|
||||
[frontend.util.url :as url-util]
|
||||
[goog.object :as gobj]
|
||||
[lambdaisland.glogi :as log]
|
||||
[promesa.core :as p]))
|
||||
@@ -57,7 +60,9 @@
|
||||
(defn restore-and-setup!
|
||||
[repo]
|
||||
(when repo
|
||||
(-> (p/let [_ (db-restore/restore-graph! repo)]
|
||||
(-> (p/let [_ (db-restore/restore-graph! repo)
|
||||
_ (graph-handler/<upsert-current-graph-registry!)]
|
||||
(graph-handler/remember-current-graph-id-in-tab!)
|
||||
(repo-config-handler/start {:repo repo}))
|
||||
(p/then
|
||||
(fn []
|
||||
@@ -120,6 +125,26 @@
|
||||
(p/let [info (ipc/ipc :system/info)]
|
||||
(state/set-state! :system/info (bean/->clj info)))))
|
||||
|
||||
(defn- current-url-target
|
||||
[]
|
||||
(try
|
||||
(url-util/parse-web-url-target (.-href js/window.location))
|
||||
(catch js/Error e
|
||||
(log/warn :url-target/parse-failed e)
|
||||
nil)))
|
||||
|
||||
(defn- apply-url-target-route!
|
||||
[url-target]
|
||||
(let [{:keys [to page-id block-id]} (:route url-target)]
|
||||
(case to
|
||||
:page
|
||||
(route-handler/redirect-to-page! page-id)
|
||||
|
||||
:block
|
||||
(route-handler/redirect-to-page! block-id)
|
||||
|
||||
nil)))
|
||||
|
||||
(defn start!
|
||||
[render]
|
||||
(let [t1 (util/time-ms)]
|
||||
@@ -154,10 +179,26 @@
|
||||
repos (repo-handler/get-repos)
|
||||
_ (state/set-repos! repos)
|
||||
_ (mobile-util/hide-splash) ;; hide splash as early as ui is stable
|
||||
repo (or (state/get-current-repo) (:url (first repos)))
|
||||
url-target (current-url-target)
|
||||
registry (graph-handler/<get-graph-registry)
|
||||
target-repo (when (seq url-target)
|
||||
(:repo (graph-handler/resolve-registry-target
|
||||
(concat registry
|
||||
(graph-handler/registry-from-repo-summaries repos))
|
||||
url-target)))
|
||||
_ (when (and (seq (:graph-id url-target)) (nil? target-repo))
|
||||
(log/warn :url-target/unresolved-graph-id url-target))
|
||||
repo (graph-handler/resolve-startup-repo
|
||||
registry
|
||||
repos
|
||||
url-target
|
||||
(graph-handler/get-tab-graph)
|
||||
(state/get-current-repo))
|
||||
_ (if (empty? repos)
|
||||
(repo-handler/new-db! config/demo-repo)
|
||||
(restore-and-setup! repo))]
|
||||
(restore-and-setup! repo))
|
||||
_ (when target-repo
|
||||
(apply-url-target-route! url-target))]
|
||||
(set-network-watcher!)
|
||||
(when (mobile-util/native-platform?)
|
||||
(state/restore-mobile-theme!)))
|
||||
|
||||
@@ -105,6 +105,8 @@
|
||||
(p/do!
|
||||
(repo-handler/restore-and-setup-repo! graph)
|
||||
(graph-switch graph)
|
||||
(graph-handler/<upsert-current-graph-registry!)
|
||||
(graph-handler/remember-current-graph-id-in-tab!)
|
||||
(state/set-state! :sync-graph/init? false)
|
||||
(when (:rtc-download? opts)
|
||||
(repo-handler/refresh-repos!)
|
||||
@@ -128,8 +130,8 @@
|
||||
(let [t2 (t/now)]
|
||||
(log/info ::graph-switch-spent (- t2 t1))))))
|
||||
|
||||
(defmethod handle :graph/open-new-window [[_ev target-repo]]
|
||||
(ui-handler/open-new-window-or-tab! target-repo))
|
||||
(defmethod handle :graph/open-new-window [[_ev target]]
|
||||
(ui-handler/open-new-window-or-tab! target))
|
||||
|
||||
(defmethod handle :page/create [[_ page-name opts]]
|
||||
(if (= page-name (date/today))
|
||||
|
||||
@@ -1,7 +1,118 @@
|
||||
(ns frontend.handler.graph
|
||||
"Provides util handler fns for graph view"
|
||||
(:require [frontend.state :as state]
|
||||
[frontend.storage :as storage]))
|
||||
(:require [cljs-bean.core :as bean]
|
||||
[clojure.string :as string]
|
||||
[electron.ipc :as ipc]
|
||||
[frontend.common.idb :as idb]
|
||||
[frontend.db :as db]
|
||||
[frontend.graph-tab :as graph-tab]
|
||||
[frontend.state :as state]
|
||||
[frontend.storage :as storage]
|
||||
[frontend.util :as util]
|
||||
[logseq.common.config :as common-config]
|
||||
[logseq.common.graph-registry :as graph-registry]
|
||||
[logseq.db :as ldb]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def graph-registry-key
|
||||
"ls-graph-registry")
|
||||
|
||||
(def normalize-registry-entry graph-registry/normalize-entry)
|
||||
|
||||
(def resolve-registry-target graph-registry/resolve-target)
|
||||
|
||||
(def set-tab-graph! graph-tab/set-tab-graph!)
|
||||
(def get-tab-graph graph-tab/get-tab-graph)
|
||||
|
||||
(defn- storage-registry->clj
|
||||
[registry]
|
||||
(let [registry' (bean/->clj registry)]
|
||||
(if (sequential? registry')
|
||||
(vec registry')
|
||||
[])))
|
||||
|
||||
(defn <get-graph-registry
|
||||
[]
|
||||
(p/let [registry (idb/get-item graph-registry-key)]
|
||||
(storage-registry->clj registry)))
|
||||
|
||||
(defn <upsert-graph-registry-entry!
|
||||
[entry]
|
||||
(let [entry' (-> entry
|
||||
normalize-registry-entry
|
||||
(assoc :updated-at (js/Date.now)))]
|
||||
(if (util/electron?)
|
||||
(ipc/ipc "upsertGraphRegistryEntry" entry')
|
||||
(p/let [registry (<get-graph-registry)
|
||||
registry' (graph-registry/upsert-entry registry entry')]
|
||||
(idb/set-item! graph-registry-key (bean/->js registry'))))))
|
||||
|
||||
(defn repo-summary->registry-entry
|
||||
[{:keys [url GraphName GraphUUID metadata sync-meta] :as repo}]
|
||||
(when url
|
||||
(let [graph-id (some-> (or GraphUUID
|
||||
(:kv/value metadata)
|
||||
(second sync-meta))
|
||||
str)
|
||||
local-graph-id (some-> (:local-graph-id repo) str)]
|
||||
(when (or graph-id local-graph-id)
|
||||
(normalize-registry-entry
|
||||
{:repo url
|
||||
:graph-name (or GraphName
|
||||
(common-config/strip-leading-db-version-prefix url))
|
||||
:graph-id graph-id
|
||||
:local-graph-id local-graph-id})))))
|
||||
|
||||
(defn registry-from-repo-summaries
|
||||
[repos]
|
||||
(keep repo-summary->registry-entry repos))
|
||||
|
||||
(defn resolve-startup-repo
|
||||
[registry repos url-target tab-graph current-repo]
|
||||
(let [registry' (concat registry (registry-from-repo-summaries repos))
|
||||
tab-repo (:repo tab-graph)
|
||||
tab-graph-id (:graph-id tab-graph)
|
||||
repo-exists? (fn [repo]
|
||||
(some #(= repo (:url %)) repos))
|
||||
url-target-repo (:repo (resolve-registry-target registry' url-target))
|
||||
tab-target-repo (when (and (nil? url-target-repo)
|
||||
(string/blank? (:graph-id url-target)))
|
||||
(or (when (and (not (string/blank? tab-repo))
|
||||
(repo-exists? tab-repo))
|
||||
tab-repo)
|
||||
(:repo (resolve-registry-target
|
||||
registry'
|
||||
{:graph-id tab-graph-id}))))]
|
||||
(or url-target-repo
|
||||
tab-target-repo
|
||||
current-repo
|
||||
(:url (first repos)))))
|
||||
|
||||
(defn current-graph-id
|
||||
[]
|
||||
(when-let [repo (state/get-current-repo)]
|
||||
(when-let [db* (db/get-db repo)]
|
||||
(some-> (or (ldb/get-graph-rtc-uuid db*)
|
||||
(ldb/get-graph-local-uuid db*))
|
||||
str))))
|
||||
|
||||
(defn remember-current-graph-id-in-tab!
|
||||
[]
|
||||
(when-let [repo (state/get-current-repo)]
|
||||
(when-let [graph-id (current-graph-id)]
|
||||
(set-tab-graph! repo graph-id))))
|
||||
|
||||
(defn <upsert-current-graph-registry!
|
||||
[]
|
||||
(when-let [repo (state/get-current-repo)]
|
||||
(when-let [db* (db/get-db repo)]
|
||||
(<upsert-graph-registry-entry!
|
||||
{:repo repo
|
||||
:graph-name (common-config/strip-leading-db-version-prefix repo)
|
||||
:local-graph-id (some-> (ldb/get-graph-local-uuid db*) str)
|
||||
:graph-id (some-> (or (ldb/get-graph-rtc-uuid db*)
|
||||
(ldb/get-graph-local-uuid db*))
|
||||
str)}))))
|
||||
|
||||
(defn settle-metadata-to-local!
|
||||
[m]
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
[frontend.handler.notification :as notification]
|
||||
[frontend.handler.plugin :as plugin-handler]
|
||||
[frontend.handler.property :as property-handler]
|
||||
[frontend.handler.graph :as graph-handler]
|
||||
[frontend.handler.route :as route-handler]
|
||||
[frontend.modules.outliner.op :as outliner-op]
|
||||
[frontend.modules.outliner.ui :as ui-outliner-tx]
|
||||
@@ -313,5 +314,7 @@
|
||||
([page-uuid]
|
||||
(if page-uuid
|
||||
(util/copy-to-clipboard!
|
||||
(url-util/get-logseq-graph-page-url nil (state/get-current-repo) (str page-uuid)))
|
||||
(url-util/get-logseq-web-page-url config/app-website
|
||||
(graph-handler/current-graph-id)
|
||||
(str page-uuid)))
|
||||
(notification/show! (t :page/no-page-found-to-copy) :warning))))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
[frontend.db :as db]
|
||||
[frontend.db.model :as model]
|
||||
[frontend.extensions.pdf.utils :as pdf-utils]
|
||||
[frontend.handler.graph :as graph-handler]
|
||||
[frontend.handler.notification :as notification]
|
||||
[frontend.handler.property.util :as pu]
|
||||
[frontend.handler.recent :as recent-handler]
|
||||
@@ -69,6 +70,15 @@
|
||||
{:to :page
|
||||
:path-params {:name (str page-name)}}))
|
||||
|
||||
(defn- current-graph-query-params
|
||||
[]
|
||||
(when-let [graph-id (graph-handler/current-graph-id)]
|
||||
{:graph-id graph-id}))
|
||||
|
||||
(defn- merge-query-params
|
||||
[route params]
|
||||
(update route :query-params merge params))
|
||||
|
||||
(defn redirect-to-page!
|
||||
"`page-name` can be a block uuid or name, prefer to use uuid than name when possible"
|
||||
([page-name]
|
||||
@@ -90,17 +100,19 @@
|
||||
(when-let [db-id (:db/id page)]
|
||||
(recent-handler/add-page-to-recent! db-id click-from-recent?))
|
||||
(let [m (cond->
|
||||
(default-page-route (str page-name))
|
||||
(merge-query-params
|
||||
(default-page-route (str page-name))
|
||||
(current-graph-query-params))
|
||||
|
||||
block-id
|
||||
(assoc :query-params {:anchor (str "ls-block-" block-id)})
|
||||
(merge-query-params {:anchor (str "ls-block-" block-id)})
|
||||
|
||||
anchor
|
||||
(assoc :query-params {:anchor anchor})
|
||||
(merge-query-params {:anchor anchor})
|
||||
|
||||
(boolean? push)
|
||||
(assoc :push push))]
|
||||
(redirect! m)))))))))
|
||||
(redirect! m)))))))))
|
||||
|
||||
(defn built-in-page-title
|
||||
[page-name]
|
||||
|
||||
@@ -240,12 +240,21 @@
|
||||
(state/pub-event! [:modal/show-cards])))
|
||||
|
||||
(defn open-new-window-or-tab!
|
||||
"Open a new Electron window."
|
||||
[target-repo]
|
||||
(when target-repo
|
||||
(if (util/electron?)
|
||||
(ipc/ipc "openNewWindow" target-repo)
|
||||
(js/window.open (str config/app-website "#/?graph=" target-repo) "_blank"))))
|
||||
"Open a new Electron window or web tab."
|
||||
[target]
|
||||
(when target
|
||||
(let [{:keys [repo graph-id]} (if (map? target)
|
||||
target
|
||||
{:repo target})]
|
||||
(if (util/electron?)
|
||||
(ipc/ipc "openNewWindow" repo)
|
||||
(do
|
||||
(when-not (seq graph-id)
|
||||
(throw (js/Error. "Missing graph id")))
|
||||
(js/window.open (str config/app-website
|
||||
"#/?graph-id="
|
||||
(js/encodeURIComponent graph-id))
|
||||
"_blank"))))))
|
||||
|
||||
(defn toggle-show-empty-hidden-properties!
|
||||
[]
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[frontend.db.conn-state :as db-conn-state]
|
||||
[frontend.dicts :as dicts]
|
||||
[frontend.flows :as flows]
|
||||
[frontend.graph-tab :as graph-tab]
|
||||
[frontend.mobile.util :as mobile-util]
|
||||
[frontend.spec.storage :as storage-spec]
|
||||
[frontend.storage :as storage]
|
||||
@@ -77,7 +78,9 @@
|
||||
(defonce ^:large-vars/data-var state
|
||||
(let [document-mode? (or (storage/get :document/mode?) false)
|
||||
current-graph (let [url-graph (:graph (util/parse-params))
|
||||
graph (or url-graph (storage/get :git/current-repo))]
|
||||
graph (or url-graph
|
||||
(graph-tab/get-tab-repo)
|
||||
(storage/get :git/current-repo))]
|
||||
(when graph (ipc/ipc "setCurrentGraph" graph))
|
||||
graph)]
|
||||
(atom
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns frontend.util.url
|
||||
"Util fns related to protocol url"
|
||||
(:require [frontend.db.conn :as db-conn]))
|
||||
(:require [clojure.string :as string]
|
||||
[frontend.db.conn :as db-conn]))
|
||||
|
||||
;; Keep same as electron/electron.core
|
||||
(def LSP_SCHEME "logseq")
|
||||
@@ -52,14 +53,64 @@
|
||||
(str (get-logseq-graph-url host repo protocol?)
|
||||
"?block-id=" uuid)))
|
||||
|
||||
(defn get-logseq-graph-page-url
|
||||
"The URL represents an page in graph with pagename, for example:
|
||||
logseq://graph/abc?page=<page-name>
|
||||
Ensure repo and page-name are valid before hand.
|
||||
host: set to `nil` for local graph
|
||||
protocol?: if true, returns URL with protocol prefix"
|
||||
([host repo page-name]
|
||||
(get-logseq-graph-page-url host repo page-name true))
|
||||
([host repo page-name protocol?]
|
||||
(str (get-logseq-graph-url host repo protocol?)
|
||||
"?page=" (encode-param page-name))))
|
||||
(defn- strip-trailing-slash
|
||||
[s]
|
||||
(string/replace s #"/+$" ""))
|
||||
|
||||
(defn- required-url-part!
|
||||
[k v]
|
||||
(when-not (and (string? v) (not (string/blank? v)))
|
||||
(throw (js/Error. (str "Missing " (name k)))))
|
||||
v)
|
||||
|
||||
(defn get-logseq-web-page-url
|
||||
"Canonical web URL for a page. Page routes must always carry `graph-id`."
|
||||
[app-base-url graph-id page-id]
|
||||
(str (strip-trailing-slash (required-url-part! :app-base-url app-base-url))
|
||||
"/page/" (encode-param (required-url-part! :page-id page-id))
|
||||
"?graph-id=" (encode-param (required-url-part! :graph-id graph-id))))
|
||||
|
||||
(defn get-logseq-web-block-url
|
||||
"Canonical web URL for a block. Block routes must always carry `graph-id`."
|
||||
[app-base-url graph-id block-id]
|
||||
(str (strip-trailing-slash (required-url-part! :app-base-url app-base-url))
|
||||
"/block/" (encode-param (required-url-part! :block-id block-id))
|
||||
"?graph-id=" (encode-param (required-url-part! :graph-id graph-id))))
|
||||
|
||||
(defn- route-from-path-parts
|
||||
[path-parts]
|
||||
(case (first path-parts)
|
||||
"page" (when-let [page-id (second path-parts)]
|
||||
{:to :page
|
||||
:page-id (js/decodeURIComponent page-id)})
|
||||
"block" (when-let [block-id (second path-parts)]
|
||||
{:to :block
|
||||
:block-id (js/decodeURIComponent block-id)})
|
||||
nil))
|
||||
|
||||
(defn- path-parts
|
||||
[path]
|
||||
(->> (string/split path #"/")
|
||||
(remove string/blank?)
|
||||
vec))
|
||||
|
||||
(defn- hash-route-url
|
||||
[parsed-url]
|
||||
(let [hash (.-hash parsed-url)]
|
||||
(when (string/starts-with? hash "#/")
|
||||
(js/URL. (subs hash 1) "https://logseq.com"))))
|
||||
|
||||
(defn parse-web-url-target
|
||||
[url]
|
||||
(let [parsed-url (js/URL. url "https://logseq.com")
|
||||
hash-url (hash-route-url parsed-url)
|
||||
graph-id (or (some-> hash-url .-searchParams (.get "graph-id"))
|
||||
(.get (.-searchParams parsed-url) "graph-id"))
|
||||
route (or (some-> hash-url .-pathname path-parts route-from-path-parts)
|
||||
(route-from-path-parts (path-parts (.-pathname parsed-url))))]
|
||||
(cond-> {}
|
||||
(not (string/blank? graph-id))
|
||||
(assoc :graph-id graph-id)
|
||||
|
||||
route
|
||||
(assoc :route route))))
|
||||
|
||||
@@ -4,90 +4,134 @@
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.db.async :as db-async]
|
||||
[frontend.handler.graph :as graph-handler]
|
||||
[frontend.handler.notification :as notification]
|
||||
[frontend.handler.route :as route-handler]
|
||||
[frontend.mobile.intent :as intent]
|
||||
[frontend.state :as state]
|
||||
[frontend.util.text :as text-util]
|
||||
[frontend.util.url :as url-util]
|
||||
[goog :refer [Uri]]
|
||||
[logseq.common.util :as common-util]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def *link-to-another-graph (atom false))
|
||||
|
||||
(defn- redirect-url-target-route!
|
||||
[url-target link-to-another-graph?]
|
||||
(when-let [{:keys [to page-id block-id]} (:route url-target)]
|
||||
(js/setTimeout
|
||||
(fn []
|
||||
(case to
|
||||
:page
|
||||
(route-handler/redirect-to-page! page-id)
|
||||
|
||||
:block
|
||||
(route-handler/redirect-to-page! block-id)
|
||||
|
||||
nil)
|
||||
(reset! *link-to-another-graph false))
|
||||
(if link-to-another-graph?
|
||||
1000
|
||||
0))))
|
||||
|
||||
(defn- handle-web-url-target!
|
||||
[url-target]
|
||||
(when (seq url-target)
|
||||
(p/let [registry (graph-handler/<get-graph-registry)
|
||||
repos (->> (state/sub [:me :repos])
|
||||
(remove #(= (:url %) config/demo-repo)))
|
||||
target-repo (:repo (graph-handler/resolve-registry-target
|
||||
(concat registry
|
||||
(graph-handler/registry-from-repo-summaries repos))
|
||||
url-target))]
|
||||
(if target-repo
|
||||
(let [link-to-another-graph? (not= target-repo (state/get-current-repo))]
|
||||
(when link-to-another-graph?
|
||||
(state/pub-event! [:graph/switch target-repo])
|
||||
(reset! *link-to-another-graph true))
|
||||
(redirect-url-target-route! url-target link-to-another-graph?))
|
||||
(when-let [graph-id (:graph-id url-target)]
|
||||
(notification/show! (t :deeplink/open-graph-error graph-id) :error false))))))
|
||||
|
||||
(defn deeplink [url]
|
||||
(let [url (string/replace url "logseq.com/" "")
|
||||
^js/Uri parsed-url (.parse Uri url)
|
||||
hostname (.getDomain parsed-url)
|
||||
pathname (.getPath parsed-url)
|
||||
search-params (.getQueryData parsed-url)
|
||||
current-repo-url (state/get-current-repo)
|
||||
get-graph-name-fn #(-> (text-util/get-graph-name-from-path %)
|
||||
(string/split "/")
|
||||
last
|
||||
string/lower-case)
|
||||
current-graph-name (get-graph-name-fn current-repo-url)
|
||||
repos (->> (state/sub [:me :repos])
|
||||
(remove #(= (:url %) config/demo-repo))
|
||||
(map :url))
|
||||
repo-names (map #(get-graph-name-fn %) repos)]
|
||||
(cond
|
||||
(and (= hostname "mobile") (= pathname "/go/audio"))
|
||||
(state/pub-event! [:mobile/start-audio-record])
|
||||
(and (= hostname "mobile") (= pathname "/go/quick-add"))
|
||||
(state/pub-event! [:mobile/set-tab "capture"])
|
||||
(= hostname "graph")
|
||||
(let [graph-name (some-> pathname
|
||||
(string/replace "/" "")
|
||||
string/lower-case)
|
||||
[page-name block-uuid] (map #(.get search-params %)
|
||||
["page" "block-id"])]
|
||||
(let [url-target (url-util/parse-web-url-target url)]
|
||||
(if (seq url-target)
|
||||
(handle-web-url-target! url-target)
|
||||
(let [url (string/replace url "logseq.com/" "")
|
||||
^js/Uri parsed-url (.parse Uri url)
|
||||
hostname (.getDomain parsed-url)
|
||||
pathname (.getPath parsed-url)
|
||||
search-params (.getQueryData parsed-url)
|
||||
current-repo-url (state/get-current-repo)
|
||||
get-graph-name-fn #(-> (text-util/get-graph-name-from-path %)
|
||||
(string/split "/")
|
||||
last
|
||||
string/lower-case)
|
||||
current-graph-name (get-graph-name-fn current-repo-url)
|
||||
repos (->> (state/sub [:me :repos])
|
||||
(remove #(= (:url %) config/demo-repo))
|
||||
(map :url))
|
||||
repo-names (map #(get-graph-name-fn %) repos)]
|
||||
(cond
|
||||
(and (= hostname "mobile") (= pathname "/go/audio"))
|
||||
(state/pub-event! [:mobile/start-audio-record])
|
||||
|
||||
(when-not (string/blank? graph-name)
|
||||
(when-not (= graph-name current-graph-name)
|
||||
(let [graph-idx (.indexOf repo-names graph-name)
|
||||
graph-url (when (not= graph-idx -1)
|
||||
(nth repos graph-idx))]
|
||||
(if graph-url
|
||||
(do (state/pub-event! [:graph/switch graph-url])
|
||||
(reset! *link-to-another-graph true))
|
||||
(notification/show! (t :deeplink/open-graph-error graph-name) :error false))))
|
||||
(and (= hostname "mobile") (= pathname "/go/quick-add"))
|
||||
(state/pub-event! [:mobile/set-tab "capture"])
|
||||
|
||||
(when (or (= graph-name current-graph-name)
|
||||
@*link-to-another-graph)
|
||||
(js/setTimeout
|
||||
(fn []
|
||||
(cond
|
||||
page-name
|
||||
(p/let [block (db-async/<get-block (state/get-current-repo) page-name {:children? false})]
|
||||
(if block
|
||||
(route-handler/redirect-to-page! block-uuid)
|
||||
(notification/show! (t :deeplink/open-page-error page-name) :error false)))
|
||||
(= hostname "graph")
|
||||
(let [graph-name (some-> pathname
|
||||
(string/replace "/" "")
|
||||
string/lower-case)
|
||||
[page-name block-uuid] (map #(.get search-params %)
|
||||
["page" "block-id"])]
|
||||
|
||||
block-uuid
|
||||
(p/let [block (db-async/<get-block (state/get-current-repo) block-uuid {:children? false})]
|
||||
(if block
|
||||
(route-handler/redirect-to-page! block-uuid)
|
||||
(notification/show! (t :deeplink/open-block-error block-uuid) :error false)))
|
||||
(when-not (string/blank? graph-name)
|
||||
(when-not (= graph-name current-graph-name)
|
||||
(let [graph-idx (.indexOf repo-names graph-name)
|
||||
graph-url (when (not= graph-idx -1)
|
||||
(nth repos graph-idx))]
|
||||
(if graph-url
|
||||
(do (state/pub-event! [:graph/switch graph-url])
|
||||
(reset! *link-to-another-graph true))
|
||||
(notification/show! (t :deeplink/open-graph-error graph-name) :error false))))
|
||||
|
||||
:else
|
||||
nil)
|
||||
(reset! *link-to-another-graph false))
|
||||
(if @*link-to-another-graph
|
||||
1000
|
||||
0)))))
|
||||
(when (or (= graph-name current-graph-name)
|
||||
@*link-to-another-graph)
|
||||
(js/setTimeout
|
||||
(fn []
|
||||
(cond
|
||||
page-name
|
||||
(p/let [block (db-async/<get-block (state/get-current-repo) page-name {:children? false})]
|
||||
(if block
|
||||
(route-handler/redirect-to-page! block-uuid)
|
||||
(notification/show! (t :deeplink/open-page-error page-name) :error false)))
|
||||
|
||||
(= hostname "shared")
|
||||
(let [result (into {} (map (fn [key]
|
||||
[(keyword key) (.get search-params key)])
|
||||
["title" "url" "type" "payload"]))]
|
||||
(if (:payload result)
|
||||
(let [raw (common-util/safe-decode-uri-component (:payload result))
|
||||
payload (-> raw
|
||||
js/JSON.parse
|
||||
(js->clj :keywordize-keys true))]
|
||||
(intent/handle-payload payload))
|
||||
(intent/handle-result result)))
|
||||
block-uuid
|
||||
(p/let [block (db-async/<get-block (state/get-current-repo) block-uuid {:children? false})]
|
||||
(if block
|
||||
(route-handler/redirect-to-page! block-uuid)
|
||||
(notification/show! (t :deeplink/open-block-error block-uuid) :error false)))
|
||||
|
||||
:else
|
||||
nil)))
|
||||
:else
|
||||
nil)
|
||||
(reset! *link-to-another-graph false))
|
||||
(if @*link-to-another-graph
|
||||
1000
|
||||
0)))))
|
||||
|
||||
(= hostname "shared")
|
||||
(let [result (into {} (map (fn [key]
|
||||
[(keyword key) (.get search-params key)])
|
||||
["title" "url" "type" "payload"]))]
|
||||
(if (:payload result)
|
||||
(let [raw (common-util/safe-decode-uri-component (:payload result))
|
||||
payload (-> raw
|
||||
js/JSON.parse
|
||||
(js->clj :keywordize-keys true))]
|
||||
(intent/handle-payload payload))
|
||||
(intent/handle-result result)))
|
||||
|
||||
:else
|
||||
nil)))))
|
||||
|
||||
@@ -728,6 +728,7 @@
|
||||
:graph/no-selected-node "No graph node selected."
|
||||
:graph/node-count (fn [n] (str n " " (if (= 1 n) "node" "nodes")))
|
||||
:graph/open-folder-action "Open graph folder"
|
||||
:graph/open-in-another-tab-action "Open in another tab"
|
||||
:graph/open-selected-node "Open {1}"
|
||||
:graph/preparing "Preparing"
|
||||
:graph/refresh-remote-graphs "Refresh remote graphs"
|
||||
|
||||
@@ -724,6 +724,7 @@
|
||||
:graph/name-placeholder "你的知识库名称"
|
||||
:graph/no-selected-node "未选中图谱节点。"
|
||||
:graph/open-folder-action "打开图谱所在文件夹"
|
||||
:graph/open-in-another-tab-action "在另一个标签页中打开"
|
||||
:graph/open-selected-node "打开 {1}"
|
||||
:graph/preparing "加载中"
|
||||
:graph/refresh-remote-graphs "刷新远程知识库"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
[frontend.components.rtc.indicator :as rtc-indicator]
|
||||
[frontend.db :as db]
|
||||
[frontend.handler.db-based.sync :as rtc-handler]
|
||||
[frontend.handler.graph :as graph-handler]
|
||||
[frontend.handler.repo :as repo-handler]
|
||||
[frontend.handler.user :as user-handler]
|
||||
[frontend.state :as state]
|
||||
@@ -23,6 +24,60 @@
|
||||
user-handler/rtc-group? (constantly true)]
|
||||
(is (true? (repo/local-uploadable-graph? {:url "logseq_db_mobile"})))))
|
||||
|
||||
(deftest open-in-another-tab-action-is-web-only-for-existing-graphs-test
|
||||
(let [actionable-f (some-> (resolve 'frontend.components.repo/open-in-another-tab-action?) deref)]
|
||||
(is (fn? actionable-f) "All graphs should expose a web open-in-another-tab action predicate")
|
||||
(when actionable-f
|
||||
(with-redefs [util/web-platform? true]
|
||||
(is (true? (actionable-f {:url "logseq_db_demo"
|
||||
:root "/graphs/demo"
|
||||
:GraphUUID "graph-uuid"})))
|
||||
(is (true? (actionable-f {:url "logseq_db_demo"
|
||||
:root "/graphs/demo"})))
|
||||
(is (false? (actionable-f {:url "logseq_db_remote"}))))
|
||||
(with-redefs [util/web-platform? false]
|
||||
(is (false? (actionable-f {:url "logseq_db_demo"
|
||||
:root "/graphs/demo"
|
||||
:GraphUUID "graph-uuid"})))))))
|
||||
|
||||
(deftest open-graph-in-another-tab-publishes-graph-open-window-event-test
|
||||
(let [open-f (some-> (resolve 'frontend.components.repo/open-graph-in-another-tab!) deref)
|
||||
events (atom [])]
|
||||
(is (fn? open-f) "All graphs should expose a web open-in-another-tab action")
|
||||
(when open-f
|
||||
(with-redefs [state/pub-event! (fn [event]
|
||||
(swap! events conj event))]
|
||||
(open-f {:url "logseq_db_demo"
|
||||
:GraphUUID "graph-uuid"})
|
||||
(is (= [[:graph/open-new-window {:repo "logseq_db_demo"
|
||||
:graph-id "graph-uuid"}]]
|
||||
@events))))))
|
||||
|
||||
(deftest open-local-graph-in-another-tab-resolves-graph-id-from-registry-test
|
||||
(async done
|
||||
(let [open-f (some-> (resolve 'frontend.components.repo/open-graph-in-another-tab!) deref)
|
||||
events (atom [])]
|
||||
(is (fn? open-f) "All graphs should expose a web open-in-another-tab action")
|
||||
(if-not open-f
|
||||
(done)
|
||||
(-> (p/with-redefs [graph-handler/<get-graph-registry
|
||||
(fn []
|
||||
(p/resolved [{:repo "logseq_db_demo"
|
||||
:graph-name "demo"
|
||||
:graph-id "local-uuid"}]))
|
||||
state/pub-event! (fn [event]
|
||||
(swap! events conj event))]
|
||||
(open-f {:url "logseq_db_demo"
|
||||
:root "/graphs/demo"}))
|
||||
(p/then (fn []
|
||||
(is (= [[:graph/open-new-window {:repo "logseq_db_demo"
|
||||
:graph-id "local-uuid"}]]
|
||||
@events))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done))))))))
|
||||
|
||||
(deftest upload-local-graph-with-confirm-asks-before-upload-test
|
||||
(async done
|
||||
(let [upload-fn (some-> (resolve 'frontend.components.repo/upload-local-graph-with-confirm!) deref)
|
||||
@@ -337,3 +392,73 @@
|
||||
(swap! events conj event))]
|
||||
(on-click #js {})
|
||||
(is (empty? @events)))))
|
||||
|
||||
(deftest shift-click-opens-downloaded-remote-graph-in-new-tab-with-graph-id-test
|
||||
(let [events (atom [])
|
||||
links (#'repo/repos-dropdown-links
|
||||
[{:url "logseq_db_demo"
|
||||
:root "/graphs/demo"
|
||||
:remote? true
|
||||
:rtc-graph? true
|
||||
:GraphName "demo"
|
||||
:GraphUUID "graph-uuid"
|
||||
:GraphSchemaVersion "65"
|
||||
:graph-ready-for-use? true}]
|
||||
nil
|
||||
nil)
|
||||
on-click (get-in (first links) [:options :on-click])]
|
||||
(with-redefs [state/pub-event! (fn [event]
|
||||
(swap! events conj event))]
|
||||
(on-click #js {:shiftKey true})
|
||||
(is (= [[:graph/open-new-window {:repo "logseq_db_demo"
|
||||
:graph-id "graph-uuid"}]]
|
||||
@events)))))
|
||||
|
||||
(deftest shift-click-opens-local-graph-in-new-tab-with-registry-graph-id-test
|
||||
(async done
|
||||
(let [events (atom [])
|
||||
links (#'repo/repos-dropdown-links
|
||||
[{:url "logseq_db_demo"
|
||||
:root "/graphs/demo"
|
||||
:graph-ready-for-use? true}]
|
||||
nil
|
||||
nil)
|
||||
on-click (get-in (first links) [:options :on-click])]
|
||||
(-> (p/with-redefs [graph-handler/<get-graph-registry
|
||||
(fn []
|
||||
(p/resolved [{:repo "logseq_db_demo"
|
||||
:graph-name "demo"
|
||||
:graph-id "local-uuid"}]))
|
||||
state/pub-event! (fn [event]
|
||||
(swap! events conj event))]
|
||||
(on-click #js {:shiftKey true}))
|
||||
(p/then (fn []
|
||||
(is (= [[:graph/open-new-window {:repo "logseq_db_demo"
|
||||
:graph-id "local-uuid"}]]
|
||||
@events))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))
|
||||
|
||||
(deftest electron-shift-click-opens-local-graph-in-new-window-by-repo-test
|
||||
(let [events (atom [])
|
||||
registry-reads (atom 0)
|
||||
links (#'repo/repos-dropdown-links
|
||||
[{:url "logseq_db_demo"
|
||||
:root "/graphs/demo"
|
||||
:graph-ready-for-use? true}]
|
||||
nil
|
||||
nil)
|
||||
on-click (get-in (first links) [:options :on-click])]
|
||||
(with-redefs [util/electron? (constantly true)
|
||||
graph-handler/<get-graph-registry
|
||||
(fn []
|
||||
(swap! registry-reads inc)
|
||||
(p/resolved []))
|
||||
state/pub-event! (fn [event]
|
||||
(swap! events conj event))]
|
||||
(on-click #js {:shiftKey true})
|
||||
(is (= [[:graph/open-new-window "logseq_db_demo"]]
|
||||
@events))
|
||||
(is (zero? @registry-reads)))))
|
||||
|
||||
163
src/test/frontend/handler/graph_test.cljs
Normal file
163
src/test/frontend/handler/graph_test.cljs
Normal file
@@ -0,0 +1,163 @@
|
||||
(ns frontend.handler.graph-test
|
||||
(:require [cljs.test :refer [async deftest is testing]]
|
||||
[frontend.common.idb :as idb]
|
||||
[frontend.handler.graph]
|
||||
[frontend.state :as state]
|
||||
[logseq.common.graph-registry :as graph-registry]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(deftest graph-registry-key-is-indexeddb-compatible-test
|
||||
(let [registry-key (some-> (resolve 'frontend.handler.graph/graph-registry-key) deref)]
|
||||
(is (= "ls-graph-registry" registry-key))))
|
||||
|
||||
(deftest get-graph-registry-normalizes-indexeddb-js-values-test
|
||||
(async done
|
||||
(let [get-registry-f (some-> (resolve 'frontend.handler.graph/<get-graph-registry) deref)]
|
||||
(p/with-redefs [idb/get-item (fn [_]
|
||||
(p/resolved
|
||||
#js [#js {"repo" "logseq_db_work"
|
||||
"graph-name" "work"
|
||||
"graph-id" "remote-uuid"}]))]
|
||||
(-> (get-registry-f)
|
||||
(.then (fn [registry]
|
||||
(is (= [{:repo "logseq_db_work"
|
||||
:graph-name "work"
|
||||
:graph-id "remote-uuid"}]
|
||||
registry))
|
||||
(is (= "logseq_db_work"
|
||||
(:repo (graph-registry/resolve-target
|
||||
registry
|
||||
{:graph-id "remote-uuid"}))))
|
||||
(done)))
|
||||
(.catch (fn [e]
|
||||
(is false (str e))
|
||||
(done))))))))
|
||||
|
||||
(deftest remember-current-graph-id-in-tab-test
|
||||
(let [remember-f (some-> (resolve 'frontend.handler.graph/remember-current-graph-id-in-tab!) deref)
|
||||
stored-graph (atom nil)]
|
||||
(is (fn? remember-f) "Current graph id should be remembered for same-tab reloads")
|
||||
(when remember-f
|
||||
(p/with-redefs [frontend.handler.graph/current-graph-id (constantly "remote-uuid")
|
||||
state/get-current-repo (constantly "logseq_db_work")
|
||||
frontend.handler.graph/set-tab-graph! (fn [repo graph-id]
|
||||
(reset! stored-graph {:repo repo
|
||||
:graph-id graph-id}))]
|
||||
(remember-f)
|
||||
(is (= {:repo "logseq_db_work"
|
||||
:graph-id "remote-uuid"}
|
||||
@stored-graph))))))
|
||||
|
||||
(deftest resolve-startup-repo-prefers-tab-repo-before-global-current-test
|
||||
(let [resolve-f (some-> (resolve 'frontend.handler.graph/resolve-startup-repo) deref)]
|
||||
(is (fn? resolve-f) "Startup repo resolver should exist")
|
||||
(when resolve-f
|
||||
(is (= "logseq_db_tab"
|
||||
(resolve-f []
|
||||
[{:url "logseq_db_tab"}
|
||||
{:url "logseq_db_current"}]
|
||||
{}
|
||||
{:repo "logseq_db_tab"
|
||||
:graph-id "tab-uuid"}
|
||||
"logseq_db_current"))))))
|
||||
|
||||
(deftest resolve-startup-repo-prefers-url-graph-id-test
|
||||
(let [resolve-f (some-> (resolve 'frontend.handler.graph/resolve-startup-repo) deref)]
|
||||
(is (fn? resolve-f) "Startup repo resolver should exist")
|
||||
(when resolve-f
|
||||
(is (= "logseq_db_url"
|
||||
(resolve-f [{:repo "logseq_db_url"
|
||||
:graph-name "url"
|
||||
:graph-id "url-uuid"}
|
||||
{:repo "logseq_db_tab"
|
||||
:graph-name "tab"
|
||||
:graph-id "tab-uuid"}]
|
||||
[{:url "logseq_db_current"}]
|
||||
{:graph-id "url-uuid"}
|
||||
{:repo "logseq_db_tab"
|
||||
:graph-id "tab-uuid"}
|
||||
"logseq_db_current"))))))
|
||||
|
||||
(deftest resolve-startup-repo-uses-tab-graph-id-before-global-current-test
|
||||
(let [resolve-f (some-> (resolve 'frontend.handler.graph/resolve-startup-repo) deref)]
|
||||
(is (fn? resolve-f) "Startup repo resolver should exist")
|
||||
(when resolve-f
|
||||
(testing "refreshing a bare root URL keeps the tab's graph context"
|
||||
(is (= "logseq_db_tab"
|
||||
(resolve-f [{:repo "logseq_db_tab"
|
||||
:graph-name "tab"
|
||||
:graph-id "tab-uuid"}]
|
||||
[{:url "logseq_db_current"}]
|
||||
{}
|
||||
{:repo "logseq_db_tab"
|
||||
:graph-id "tab-uuid"}
|
||||
"logseq_db_current"))))
|
||||
(testing "global current graph remains the last fallback"
|
||||
(is (= "logseq_db_current"
|
||||
(resolve-f []
|
||||
[{:url "logseq_db_first"}]
|
||||
{}
|
||||
nil
|
||||
"logseq_db_current")))))))
|
||||
|
||||
(deftest normalize-registry-entry-prefers-remote-graph-id-test
|
||||
(let [normalize-f (some-> (resolve 'frontend.handler.graph/normalize-registry-entry) deref)]
|
||||
(is (fn? normalize-f) "Graph registry entry normalizer should exist")
|
||||
(when normalize-f
|
||||
(testing "remote graphs store graph-id without duplicating rtc-graph-id"
|
||||
(is (= {:repo "logseq_db_work"
|
||||
:graph-name "work"
|
||||
:local-graph-id "local-uuid"
|
||||
:graph-id "remote-uuid"}
|
||||
(select-keys
|
||||
(normalize-f {:repo "logseq_db_work"
|
||||
:graph-name "work"
|
||||
:local-graph-id "local-uuid"
|
||||
:graph-id "remote-uuid"})
|
||||
[:repo :graph-name :local-graph-id :rtc-graph-id :graph-id]))))
|
||||
(testing "local-only graphs use local graph uuid as canonical graph-id"
|
||||
(is (= "local-uuid"
|
||||
(:graph-id (normalize-f {:repo "logseq_db_local"
|
||||
:graph-name "local"
|
||||
:local-graph-id "local-uuid"})))))
|
||||
(testing "missing graph identity fails fast"
|
||||
(is (thrown? js/Error
|
||||
(normalize-f {:repo "logseq_db_broken"
|
||||
:graph-name "broken"})))))))
|
||||
|
||||
(deftest resolve-registry-target-prefers-graph-id-test
|
||||
(let [resolve-f (some-> (resolve 'frontend.handler.graph/resolve-registry-target) deref)]
|
||||
(is (fn? resolve-f) "Graph registry target resolver should exist")
|
||||
(when resolve-f
|
||||
(let [registry [{:repo "logseq_db_work"
|
||||
:graph-name "work"
|
||||
:graph-id "remote-uuid"}
|
||||
{:repo "logseq_db_other"
|
||||
:graph-name "work"
|
||||
:graph-id "other-uuid"}]]
|
||||
(is (= "logseq_db_work"
|
||||
(:repo (resolve-f registry {:graph-id "remote-uuid"}))))
|
||||
(is (= "logseq_db_other"
|
||||
(:repo (resolve-f registry {:graph-identifier "logseq_db_other"}))))
|
||||
(is (= "logseq_db_work"
|
||||
(:repo (resolve-f registry {:graph-identifier "remote-uuid"})))
|
||||
"Protocol URL graph identifiers can be canonical graph ids")
|
||||
(is (nil? (resolve-f registry {:graph-id "missing-uuid"})))))))
|
||||
|
||||
(deftest upsert-registry-entry-replaces-local-id-after-remote-id-exists-test
|
||||
(let [registry [{:repo "logseq_db_work"
|
||||
:graph-name "work"
|
||||
:local-graph-id "local-uuid"
|
||||
:graph-id "local-uuid"}]
|
||||
registry' (graph-registry/upsert-entry
|
||||
registry
|
||||
{:repo "logseq_db_work"
|
||||
:graph-name "work"
|
||||
:local-graph-id "local-uuid"
|
||||
:graph-id "remote-uuid"})]
|
||||
(is (= 1 (count registry')))
|
||||
(is (= "remote-uuid" (:graph-id (first registry'))))
|
||||
(is (not (contains? (first registry') :rtc-graph-id)))
|
||||
(is (nil? (graph-registry/resolve-target registry' {:graph-id "local-uuid"})))
|
||||
(is (= "logseq_db_work"
|
||||
(:repo (graph-registry/resolve-target registry' {:graph-id "remote-uuid"}))))))
|
||||
@@ -1,8 +1,11 @@
|
||||
(ns frontend.handler.route-test
|
||||
(:require [clojure.test :refer [deftest is use-fixtures testing]]
|
||||
[frontend.db :as db]
|
||||
[frontend.db.utils :as db-utils]
|
||||
[frontend.handler.graph :as graph-handler]
|
||||
[frontend.handler.route :as route-handler]
|
||||
[frontend.test.helper :as test-helper :refer [load-test-files]]))
|
||||
[frontend.test.helper :as test-helper :refer [load-test-files]]
|
||||
[reitit.frontend.easy :as rfe]))
|
||||
|
||||
(use-fixtures :each {:before test-helper/start-test-db!
|
||||
:after test-helper/destroy-test-db!})
|
||||
@@ -63,3 +66,37 @@
|
||||
(is (= {:to :page :path-params {:name "page name"}}
|
||||
(#'route-handler/default-page-route "Page name"))
|
||||
"Generates a case insensitive page link")))
|
||||
|
||||
(deftest redirect-to-page-includes-current-graph-id
|
||||
(testing "page routes always carry graph identity"
|
||||
(let [calls (atom [])]
|
||||
(with-redefs [db/get-page (constantly nil)
|
||||
db/get-alias-source-page (constantly nil)
|
||||
graph-handler/current-graph-id (constantly "graph-uuid")
|
||||
rfe/push-state (fn [& args] (swap! calls conj args))]
|
||||
(route-handler/redirect-to-page! "Page name")
|
||||
(is (= [[:page {:name "page name"} {:graph-id "graph-uuid"}]]
|
||||
@calls)))))
|
||||
|
||||
(testing "existing page route query params are preserved"
|
||||
(let [calls (atom [])]
|
||||
(with-redefs [db/get-page (constantly nil)
|
||||
db/get-alias-source-page (constantly nil)
|
||||
graph-handler/current-graph-id (constantly "graph-uuid")
|
||||
rfe/push-state (fn [& args] (swap! calls conj args))]
|
||||
(route-handler/redirect-to-page! "Page name" {:anchor "ls-block-block-uuid"})
|
||||
(is (= [[:page {:name "page name"}
|
||||
{:anchor "ls-block-block-uuid"
|
||||
:graph-id "graph-uuid"}]]
|
||||
@calls))))))
|
||||
|
||||
(deftest redirect-to-page-tolerates-missing-current-graph-id
|
||||
(testing "startup redirects do not crash before graph restoration finishes"
|
||||
(let [calls (atom [])]
|
||||
(with-redefs [db/get-page (constantly nil)
|
||||
db/get-alias-source-page (constantly nil)
|
||||
graph-handler/current-graph-id (constantly nil)
|
||||
rfe/push-state (fn [& args] (swap! calls conj args))]
|
||||
(route-handler/redirect-to-page! "Page name")
|
||||
(is (= [[:page {:name "page name"} nil]]
|
||||
@calls))))))
|
||||
|
||||
21
src/test/frontend/handler/ui_test.cljs
Normal file
21
src/test/frontend/handler/ui_test.cljs
Normal file
@@ -0,0 +1,21 @@
|
||||
(ns frontend.handler.ui-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[frontend.config :as config]
|
||||
[frontend.handler.ui :as ui-handler]
|
||||
[frontend.util :as util]))
|
||||
|
||||
(deftest open-new-window-or-tab-uses-graph-id-on-web-test
|
||||
(let [opened (atom [])
|
||||
original-open (.-open js/window)]
|
||||
(try
|
||||
(set! (.-open js/window)
|
||||
(fn [url target]
|
||||
(swap! opened conj [url target])))
|
||||
(with-redefs [util/electron? (constantly false)
|
||||
config/app-website "http://localhost:3001"]
|
||||
(ui-handler/open-new-window-or-tab! {:repo "logseq_db_demo"
|
||||
:graph-id "graph uuid"}))
|
||||
(is (= [["http://localhost:3001#/?graph-id=graph%20uuid" "_blank"]]
|
||||
@opened))
|
||||
(finally
|
||||
(set! (.-open js/window) original-open)))))
|
||||
38
src/test/frontend/util/url_test.cljs
Normal file
38
src/test/frontend/util/url_test.cljs
Normal file
@@ -0,0 +1,38 @@
|
||||
(ns frontend.util.url-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[frontend.util.url]))
|
||||
|
||||
(deftest web-page-url-requires-graph-id-test
|
||||
(let [page-url-f (some-> (resolve 'frontend.util.url/get-logseq-web-page-url) deref)]
|
||||
(is (fn? page-url-f) "Canonical page URL helper should exist")
|
||||
(when page-url-f
|
||||
(testing "page routes always include graph-id"
|
||||
(is (= "https://logseq.com/page/page-uuid?graph-id=remote-graph-uuid"
|
||||
(page-url-f "https://logseq.com" "remote-graph-uuid" "page-uuid"))))
|
||||
(testing "missing graph-id fails instead of generating ambiguous page URL"
|
||||
(is (thrown? js/Error
|
||||
(page-url-f "https://logseq.com" nil "page-uuid")))))))
|
||||
|
||||
(deftest web-block-url-requires-graph-id-test
|
||||
(let [block-url-f (some-> (resolve 'frontend.util.url/get-logseq-web-block-url) deref)]
|
||||
(is (fn? block-url-f) "Canonical block URL helper should exist")
|
||||
(when block-url-f
|
||||
(is (= "https://logseq.com/block/block-uuid?graph-id=remote-graph-uuid"
|
||||
(block-url-f "https://logseq.com" "remote-graph-uuid" "block-uuid"))))))
|
||||
|
||||
(deftest parse-web-url-target-reads-path-route-and-graph-id-test
|
||||
(let [parse-f (some-> (resolve 'frontend.util.url/parse-web-url-target) deref)]
|
||||
(is (fn? parse-f) "Web URL target parser should exist")
|
||||
(when parse-f
|
||||
(is (= {:graph-id "remote-graph-uuid"
|
||||
:route {:to :page
|
||||
:page-id "page-uuid"}}
|
||||
(parse-f "https://logseq.com/page/page-uuid?graph-id=remote-graph-uuid")))
|
||||
(is (= {:graph-id "remote-graph-uuid"
|
||||
:route {:to :block
|
||||
:block-id "block-uuid"}}
|
||||
(parse-f "https://logseq.com/block/block-uuid?graph-id=remote-graph-uuid")))
|
||||
(is (= {:graph-id "dc4b7cbd-65f7-4e76-9591-dcb3d14f11cf"
|
||||
:route {:to :page
|
||||
:page-id "00000001-2026-0520-0000-000000000000"}}
|
||||
(parse-f "http://localhost:3001/#/page/00000001-2026-0520-0000-000000000000?graph-id=dc4b7cbd-65f7-4e76-9591-dcb3d14f11cf"))))))
|
||||
61
src/test/mobile/deeplink_test.cljs
Normal file
61
src/test/mobile/deeplink_test.cljs
Normal file
@@ -0,0 +1,61 @@
|
||||
(ns mobile.deeplink-test
|
||||
(:require [cljs.test :refer [async deftest is testing]]
|
||||
[frontend.handler.graph :as graph-handler]
|
||||
[frontend.handler.route :as route-handler]
|
||||
[frontend.state :as state]
|
||||
[mobile.deeplink :as deeplink]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(deftest web-page-url-switches-to-graph-id-target
|
||||
(async done
|
||||
(testing "mobile universal web links resolve graph-id before routing"
|
||||
(let [events (atom [])
|
||||
redirects (atom [])]
|
||||
(p/with-redefs [graph-handler/<get-graph-registry
|
||||
(fn []
|
||||
(p/resolved [{:repo "logseq_db_target"
|
||||
:graph-name "target"
|
||||
:graph-id "remote-uuid"}]))
|
||||
state/get-current-repo (constantly "logseq_db_current")
|
||||
state/sub (fn [query]
|
||||
(case query
|
||||
[:me :repos] [{:url "logseq_db_target"}]
|
||||
nil))
|
||||
state/pub-event! (fn [event] (swap! events conj event))
|
||||
route-handler/redirect-to-page! (fn [page-id]
|
||||
(swap! redirects conj page-id))]
|
||||
(-> (deeplink/deeplink "https://logseq.com/page/page-uuid?graph-id=remote-uuid")
|
||||
(p/then (fn [] (p/delay 1010)))
|
||||
(p/then (fn []
|
||||
(is (= [[:graph/switch "logseq_db_target"]]
|
||||
@events))
|
||||
(is (= ["page-uuid"] @redirects))
|
||||
(done)))
|
||||
(p/catch (fn [e]
|
||||
(is false (str e))
|
||||
(done)))))))))
|
||||
|
||||
(deftest web-page-url-routes-within-current-graph
|
||||
(async done
|
||||
(testing "mobile universal web links route when graph-id is already current"
|
||||
(let [redirects (atom [])]
|
||||
(p/with-redefs [graph-handler/<get-graph-registry
|
||||
(fn []
|
||||
(p/resolved [{:repo "logseq_db_target"
|
||||
:graph-name "target"
|
||||
:graph-id "remote-uuid"}]))
|
||||
state/get-current-repo (constantly "logseq_db_target")
|
||||
state/sub (fn [query]
|
||||
(case query
|
||||
[:me :repos] [{:url "logseq_db_target"}]
|
||||
nil))
|
||||
route-handler/redirect-to-page! (fn [page-id]
|
||||
(swap! redirects conj page-id))]
|
||||
(-> (deeplink/deeplink "https://logseq.com/page/page-uuid?graph-id=remote-uuid")
|
||||
(p/then (fn [] (p/delay 5)))
|
||||
(p/then (fn []
|
||||
(is (= ["page-uuid"] @redirects))
|
||||
(done)))
|
||||
(p/catch (fn [e]
|
||||
(is false (str e))
|
||||
(done)))))))))
|
||||
Reference in New Issue
Block a user