mirror of
https://github.com/penpot/penpot.git
synced 2026-09-10 22:19:19 +00:00
🐛 Fix link-preview replaceState guard and cover navigated effect
Compare the mirrored href against the path-relative current location so the no-op guard actually skips unchanged URLs. Read context ids through get-query-param so repeated keys resolve to the last value. Cover the navigated effect with stubbed-globals tests (write on change, skip when mirrored, strip stale context) and surface enable-link-preview in the configuration flags list. AI-assisted-by: muse-spark-1.3-contributor Signed-off-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
parent
3bc46a21dc
commit
db84b77c7e
@ -730,6 +730,9 @@ __Since version 2.0.0__
|
|||||||
- <code class="language-bash">enable-webhooks</code>: enables webhooks. More detail about this configuration in [webhooks section][6].
|
- <code class="language-bash">enable-webhooks</code>: enables webhooks. More detail about this configuration in [webhooks section][6].
|
||||||
- <code class="language-bash">enable-access-tokens</code>: enables access tokens. More detail about this configuration in [access tokens section][7].
|
- <code class="language-bash">enable-access-tokens</code>: enables access tokens. More detail about this configuration in [access tokens section][7].
|
||||||
- <code class="language-bash">disable-google-fonts-provider</code>: disables the google fonts provider.
|
- <code class="language-bash">disable-google-fonts-provider</code>: disables the google fonts provider.
|
||||||
|
- <code class="language-bash">enable-link-preview</code>: enables Open Graph link previews for shared links.
|
||||||
|
File names and dashboard thumbnails become readable by anyone holding the link, so only enable
|
||||||
|
it if you accept that trade-off. More detail in the [link previews page][9].
|
||||||
|
|
||||||
[1]: /technical-guide/getting-started#configure-penpot-with-elestio
|
[1]: /technical-guide/getting-started#configure-penpot-with-elestio
|
||||||
[2]: /technical-guide/getting-started#configure-penpot-with-docker
|
[2]: /technical-guide/getting-started#configure-penpot-with-docker
|
||||||
@ -739,3 +742,4 @@ __Since version 2.0.0__
|
|||||||
[6]: /technical-guide/integration/#webhooks
|
[6]: /technical-guide/integration/#webhooks
|
||||||
[7]: /technical-guide/integration/#access-tokens
|
[7]: /technical-guide/integration/#access-tokens
|
||||||
[8]: /mcp/
|
[8]: /mcp/
|
||||||
|
[9]: /technical-guide/developer/subsystems/link-preview/
|
||||||
|
|||||||
@ -225,7 +225,9 @@ indexing these preview pages, and responses are marked non-cacheable.
|
|||||||
1. Make sure the devenv nginx picked up the config (restart the devenv, or
|
1. Make sure the devenv nginx picked up the config (restart the devenv, or
|
||||||
`nginx -s reload` inside the container, if it predates these changes).
|
`nginx -s reload` inside the container, if it predates these changes).
|
||||||
|
|
||||||
2. Enable the flag before starting the backend REPL:
|
2. The flag already ships enabled in devenv via `backend/scripts/_env`, so
|
||||||
|
no export is needed there; outside devenv, enable it before starting
|
||||||
|
the backend:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export PENPOT_FLAGS="$PENPOT_FLAGS enable-link-preview"
|
export PENPOT_FLAGS="$PENPOT_FLAGS enable-link-preview"
|
||||||
|
|||||||
@ -65,6 +65,16 @@
|
|||||||
|
|
||||||
;; --- Navigate (Event)
|
;; --- Navigate (Event)
|
||||||
|
|
||||||
|
(defn get-query-param
|
||||||
|
"Safely extracts a scalar value for a query param key from a params
|
||||||
|
map. When the same key appears multiple times in a URL,
|
||||||
|
query-string->map returns a vector for that key; this function
|
||||||
|
always returns a single (last) element in that case, so downstream
|
||||||
|
consumers such as parse-long always receive a plain string or nil."
|
||||||
|
[params k]
|
||||||
|
(let [v (get params k)]
|
||||||
|
(if (sequential? v) (peek v) v)))
|
||||||
|
|
||||||
(defn match->context-params
|
(defn match->context-params
|
||||||
"Extract the params that give sharing context to the current URL.
|
"Extract the params that give sharing context to the current URL.
|
||||||
|
|
||||||
@ -74,9 +84,12 @@
|
|||||||
[match]
|
[match]
|
||||||
(let [path-params (dm/get-in match [:params :path])
|
(let [path-params (dm/get-in match [:params :path])
|
||||||
query-params (get match :query-params)
|
query-params (get match :query-params)
|
||||||
file-id (or (get query-params :file-id) (get path-params :file-id))
|
file-id (or (get-query-param query-params :file-id)
|
||||||
team-id (or (get query-params :team-id) (get path-params :team-id))
|
(get-query-param path-params :file-id))
|
||||||
project-id (or (get query-params :project-id) (get path-params :project-id))]
|
team-id (or (get-query-param query-params :team-id)
|
||||||
|
(get-query-param path-params :team-id))
|
||||||
|
project-id (or (get-query-param query-params :project-id)
|
||||||
|
(get-query-param path-params :project-id))]
|
||||||
(cond
|
(cond
|
||||||
(some? file-id) {:file-id file-id}
|
(some? file-id) {:file-id file-id}
|
||||||
(some? project-id) {:team-id team-id :project-id project-id}
|
(some? project-id) {:team-id team-id :project-id project-id}
|
||||||
@ -110,11 +123,14 @@
|
|||||||
(u/map->query-string))
|
(u/map->query-string))
|
||||||
href (dm/str (.-pathname globals/location)
|
href (dm/str (.-pathname globals/location)
|
||||||
(if (some? query) (dm/str "?" query) "")
|
(if (some? query) (dm/str "?" query) "")
|
||||||
|
(.-hash globals/location))
|
||||||
|
current (dm/str (.-pathname globals/location)
|
||||||
|
(.-search globals/location)
|
||||||
(.-hash globals/location))]
|
(.-hash globals/location))]
|
||||||
;; The pre-fragment query string is owned by this mirroring: skip
|
;; The pre-fragment query string is owned by this mirroring: skip
|
||||||
;; the write when nothing changed to avoid URL churn and dropping
|
;; the write when nothing changed to avoid URL churn and dropping
|
||||||
;; unrelated params set by other code.
|
;; unrelated params set by other code. Both sides are path-relative.
|
||||||
(when (not= href (.-href globals/location))
|
(when (not= href current)
|
||||||
(.replaceState js/history nil "" href))))))
|
(.replaceState js/history nil "" href))))))
|
||||||
|
|
||||||
(defn navigate
|
(defn navigate
|
||||||
@ -165,16 +181,6 @@
|
|||||||
[state]
|
[state]
|
||||||
(dm/get-in state [:route :params :query]))
|
(dm/get-in state [:route :params :query]))
|
||||||
|
|
||||||
(defn get-query-param
|
|
||||||
"Safely extracts a scalar value for a query param key from a params
|
|
||||||
map. When the same key appears multiple times in a URL,
|
|
||||||
query-string->map returns a vector for that key; this function
|
|
||||||
always returns a single (last) element in that case, so downstream
|
|
||||||
consumers such as parse-long always receive a plain string or nil."
|
|
||||||
[params k]
|
|
||||||
(let [v (get params k)]
|
|
||||||
(if (sequential? v) (peek v) v)))
|
|
||||||
|
|
||||||
(defn nav-back
|
(defn nav-back
|
||||||
[]
|
[]
|
||||||
(ptk/reify ::nav-back
|
(ptk/reify ::nav-back
|
||||||
|
|||||||
@ -7,7 +7,9 @@
|
|||||||
(ns frontend-tests.router-test
|
(ns frontend-tests.router-test
|
||||||
(:require
|
(:require
|
||||||
[app.main.router :as rt]
|
[app.main.router :as rt]
|
||||||
[cljs.test :as t :include-macros true]))
|
[app.util.globals :as globals]
|
||||||
|
[cljs.test :as t :include-macros true]
|
||||||
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
(t/deftest match-context-params-file-link
|
(t/deftest match-context-params-file-link
|
||||||
;; Workspace and viewer links only mirror the file-id.
|
;; Workspace and viewer links only mirror the file-id.
|
||||||
@ -55,3 +57,66 @@
|
|||||||
:file-id "file-1"}}]
|
:file-id "file-1"}}]
|
||||||
(t/is (= {:file-id "file-1"}
|
(t/is (= {:file-id "file-1"}
|
||||||
(rt/match->context-params match)))))
|
(rt/match->context-params match)))))
|
||||||
|
|
||||||
|
(t/deftest match-context-params-repeated-key
|
||||||
|
;; A repeated query key arrives as a vector; the last value wins.
|
||||||
|
(let [match {:query-params {:file-id ["file-old" "file-1"]}}]
|
||||||
|
(t/is (= {:file-id "file-1"}
|
||||||
|
(rt/match->context-params match)))))
|
||||||
|
|
||||||
|
(defn- with-stubbed-browser
|
||||||
|
"Run `thunk` with the `globals/location` mock props and a recording
|
||||||
|
`js/history.replaceState`. Restores both originals afterwards: the
|
||||||
|
location mock is shared across tests and `js/history` may not exist
|
||||||
|
in the test environment at all."
|
||||||
|
[{:keys [pathname search hash href]} replace-calls thunk]
|
||||||
|
(let [loc globals/location
|
||||||
|
old-pathname (.-pathname loc)
|
||||||
|
old-search (.-search loc)
|
||||||
|
old-hash (.-hash loc)
|
||||||
|
old-href (.-href loc)
|
||||||
|
old-history (.-history js/globalThis)]
|
||||||
|
(set! (.-pathname loc) pathname)
|
||||||
|
(set! (.-search loc) search)
|
||||||
|
(set! (.-hash loc) hash)
|
||||||
|
(set! (.-href loc) href)
|
||||||
|
(set! (.-history js/globalThis)
|
||||||
|
#js {:replaceState (fn [_ _ url] (swap! replace-calls conj url))})
|
||||||
|
(try
|
||||||
|
(thunk)
|
||||||
|
(finally
|
||||||
|
(set! (.-pathname loc) old-pathname)
|
||||||
|
(set! (.-search loc) old-search)
|
||||||
|
(set! (.-hash loc) old-hash)
|
||||||
|
(set! (.-href loc) old-href)
|
||||||
|
(set! (.-history js/globalThis) old-history)))))
|
||||||
|
|
||||||
|
(t/deftest navigated-mirrors-context-on-change
|
||||||
|
;; New context in the match triggers exactly one mirrored write.
|
||||||
|
(let [calls (atom [])]
|
||||||
|
(with-stubbed-browser
|
||||||
|
{:pathname "/" :search "" :hash "#/workspace?file-id=file-1" :href "http://localhost/"}
|
||||||
|
calls
|
||||||
|
(fn []
|
||||||
|
(ptk/effect (rt/navigated {:query-params {:file-id "file-1"}} false) nil nil)
|
||||||
|
(t/is (= ["/?file-id=file-1#/workspace?file-id=file-1"] @calls))))))
|
||||||
|
|
||||||
|
(t/deftest navigated-skips-write-when-mirrored
|
||||||
|
;; When the URL already carries the mirrored context, nothing is written.
|
||||||
|
(let [calls (atom [])]
|
||||||
|
(with-stubbed-browser
|
||||||
|
{:pathname "/" :search "?file-id=file-1" :hash "#/workspace?file-id=file-1" :href "http://localhost/?file-id=file-1#/workspace?file-id=file-1"}
|
||||||
|
calls
|
||||||
|
(fn []
|
||||||
|
(ptk/effect (rt/navigated {:query-params {:file-id "file-1"}} false) nil nil)
|
||||||
|
(t/is (= [] @calls))))))
|
||||||
|
|
||||||
|
(t/deftest navigated-strips-stale-context
|
||||||
|
;; A stale pre-fragment query is replaced with the current context.
|
||||||
|
(let [calls (atom [])]
|
||||||
|
(with-stubbed-browser
|
||||||
|
{:pathname "/" :search "?file-id=old" :hash "#/dashboard/recent?team-id=team-1" :href "http://localhost/?file-id=old#/dashboard/recent?team-id=team-1"}
|
||||||
|
calls
|
||||||
|
(fn []
|
||||||
|
(ptk/effect (rt/navigated {:query-params {:team-id "team-1"}} false) nil nil)
|
||||||
|
(t/is (= ["/?team-id=team-1#/dashboard/recent?team-id=team-1"] @calls))))))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user