diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index a4cb7fedab..8096051d40 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -140,6 +140,13 @@ (u/query-string->map (:query uri)))}))) +(defn- legacy-query-token + "Legacy root links (e.g. `#?template=`) keep their params in a + bare `#?…` fragment. Return them as a query token, or nil." + [hash] + (when (str/starts-with? hash "#?") + (subs hash 1))) + (defn- store-session-params [{:keys [template plugin]}] (binding [storage/*sync* true] @@ -226,7 +233,8 @@ (let [location (.-location js/document) location-path (dm/str (.-origin location) (.-pathname location)) valid-location? (= location-path (dm/str cf/public-uri)) - legacy-hash (.-hash location)] + legacy-hash (.-hash location) + legacy-token (legacy-query-token legacy-hash)] (cond (not valid-location?) @@ -242,6 +250,10 @@ (st/emit! (rt/nav name params {::rt/replace true})) (on-query-navigate router token send-event-info?)) + ;; TODO(next-version): delete with `legacy-routes`. + (some? legacy-token) + (on-query-navigate router legacy-token send-event-info?) + :else (on-query-navigate router token send-event-info?)))) @@ -251,7 +263,10 @@ (subs token 1) (or token "")) query-params (u/query-string->map token-query) - empty-token? (str/blank? token-query) + ;; Without a `screen` the token targets the app root; its params + ;; (e.g. `template`) are forwarded to the dashboard or kept for + ;; after login. + root? (nil? (rt/get-query-param query-params :screen)) match (rt/match router token)] (if (some? match) (handle-sso-error-and-navigate match send-event-info? (rt/get-current-href)) @@ -261,9 +276,13 @@ ;; on invitations workflows (and probably other cases). (->> (rp/cmd! :get-profile) (rx/mapcat (fn [profile] - (->> (rp/cmd! :get-teams {}) - (rx/map (fn [teams] - (assoc profile ::teams (into #{} (map :id) teams))))))) + ;; get-teams requires a session; anonymous users + ;; only need their params kept for after login. + (if (= (:id profile) uuid/zero) + (rx/of profile) + (->> (rp/cmd! :get-teams {}) + (rx/map (fn [teams] + (assoc profile ::teams (into #{} (map :id) teams)))))))) (rx/subs! (fn [{:keys [id ::teams] :as profile}] (cond (= id uuid/zero) @@ -271,7 +290,7 @@ (store-session-params query-params) (st/emit! (rt/nav :auth-login))) - empty-token? + root? (let [default-team-id (:default-team-id profile) last-team-id (dtm/get-last-team-id) team-id (if (contains? teams last-team-id) diff --git a/frontend/test/frontend_tests/ui/routes_test.cljs b/frontend/test/frontend_tests/ui/routes_test.cljs index e85246d954..68ef15c5d1 100644 --- a/frontend/test/frontend_tests/ui/routes_test.cljs +++ b/frontend/test/frontend_tests/ui/routes_test.cljs @@ -9,9 +9,11 @@ [app.common.time :as ct] [app.common.uuid :as uuid] [app.config :as cf] + [app.main.data.team :as dtm] [app.main.repo :as rp] [app.main.store :as st] [app.main.ui.routes :as routes] + [app.util.storage :as storage] [beicon.v2.core :as rx] [cljs.test :as t :include-macros true] [frontend-tests.helpers.mock :as mock])) @@ -95,3 +97,61 @@ (t/is (= 2 (count @events))) (done')) done)))) + +;; Template links (`#?template=`) predate query-string routing: the +;; params live in a bare `#?…` fragment and there is no `screen` (#12127). + +(def ^:private template-url + "https://penpot.github.io/penpot-files/tutorial-for-beginners%20v.2.0.penpot") + +(def ^:private template-param + "The query param as the router decodes it." + "https://penpot.github.io/penpot-files/tutorial-for-beginners v.2.0.penpot") + +(t/deftest legacy-query-hash-becomes-a-query-token + (t/is (= (str "?template=" template-url) + (#'routes/legacy-query-token (str "#?template=" template-url)))) + (t/is (nil? (#'routes/legacy-query-token "#/workspace?team-id=1"))) + (t/is (nil? (#'routes/legacy-query-token "")))) + +(t/deftest query-without-screen-goes-to-dashboard-with-its-params + (t/async done + (let [team-id (uuid/next) + events (atom [])] + (mock/with-mocks + {rp/cmd! (mock/stub + (fn [command _] + (case command + :get-profile (rx/of {:id (uuid/next) :default-team-id team-id}) + :get-teams (rx/of [{:id team-id}])))) + dtm/get-last-team-id (mock/stub (fn [] nil)) + st/emit! (mock/stub (fn [& emitted] (swap! events into emitted)))} + (fn [done'] + (#'routes/on-query-navigate #{:dashboard-recent} (str "?template=" template-url) false) + (let [{:keys [id params]} (some-> (first @events) deref)] + (t/is (= :dashboard-recent id)) + (t/is (= template-param (:template params))) + (t/is (= team-id (:team-id params)))) + (done')) + done)))) + +(t/deftest query-without-screen-keeps-template-for-after-login + (t/async done + (let [events (atom [])] + (mock/with-mocks + {rp/cmd! (mock/stub + (fn [command _] + (case command + :get-profile (rx/of {:id uuid/zero}) + ;; The backend requires a session for get-teams. + :get-teams (rx/throw (ex-info "authentication required" + {:type :authentication}))))) + st/emit! (mock/stub (fn [& emitted] (swap! events into emitted)))} + (fn [done'] + (#'routes/on-query-navigate #{:auth-login} (str "?template=" template-url) false) + (t/is (= template-param (:template @storage/session))) + (t/is (= :auth-login (:id (some-> (first @events) deref)))) + (binding [storage/*sync* true] + (swap! storage/session dissoc :template)) + (done')) + done))))