diff --git a/frontend/src/app/main/data/workspace/pages.cljs b/frontend/src/app/main/data/workspace/pages.cljs index 9ef4a20f19..37d12730b8 100644 --- a/frontend/src/app/main/data/workspace/pages.cljs +++ b/frontend/src/app/main/data/workspace/pages.cljs @@ -88,6 +88,7 @@ (let [page (dsh/lookup-page state file-id page-id) uris (into #{} xf:collect-file-media (:objects page))] (rx/merge + (rx/of (ptk/data-event ::initialized page-id)) (->> (rx/from uris) (rx/map #(http/fetch-data-uri % false)) (rx/ignore)) diff --git a/frontend/src/app/plugins/api.cljs b/frontend/src/app/plugins/api.cljs index 8c0d07f316..6fe55c6415 100644 --- a/frontend/src/app/plugins/api.cljs +++ b/frontend/src/app/plugins/api.cljs @@ -27,6 +27,7 @@ [app.main.data.workspace.colors :as dwc] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.media :as dwm] + [app.main.data.workspace.pages :as dwpg] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.variants :as dwv] [app.main.data.workspace.wasm-text :as dwwt] @@ -54,7 +55,8 @@ [app.util.object :as obj] [app.util.theme :as theme] [beicon.v2.core :as rx] - [cuerdas.core :as str])) + [cuerdas.core :as str] + [potok.v2.core :as ptk])) ;; ;; PLUGINS PUBLIC API - The plugins will able to access this functions @@ -571,11 +573,20 @@ (let [id (cond (page/page-proxy? page) (obj/get page "$id") (string? page) (uuid/parse* page) - :else nil) - new-window (if (boolean? new-window) new-window false)] + :else nil)] (if (nil? id) (u/not-valid plugin-id :openPage "Expected a Page object or a page UUID string") - (st/emit! (dcm/go-to-workspace :page-id id ::rt/new-window new-window))))) + (if (true? new-window) + (do (st/emit! (dcm/go-to-workspace :page-id id ::rt/new-window true)) + (js/Promise.resolve nil)) + (js/Promise. + (fn [resolve _] + (->> st/stream + (rx/filter (ptk/type? ::dwpg/initialized)) + (rx/filter #(= (deref %) id)) + (rx/take 1) + (rx/subs! #(resolve nil))) + (st/emit! (dcm/go-to-workspace :page-id id)))))))) :alignHorizontal (fn [shapes direction] diff --git a/frontend/src/app/plugins/page.cljs b/frontend/src/app/plugins/page.cljs index 39e0938880..57e37e4a5c 100644 --- a/frontend/src/app/plugins/page.cljs +++ b/frontend/src/app/plugins/page.cljs @@ -19,6 +19,7 @@ [app.main.data.workspace :as dw] [app.main.data.workspace.guides :as dwgu] [app.main.data.workspace.interactions :as dwi] + [app.main.data.workspace.pages :as dwpg] [app.main.repo :as rp] [app.main.router :as-alias rt] [app.main.store :as st] @@ -32,7 +33,8 @@ [app.plugins.utils :as u] [app.util.object :as obj] [beicon.v2.core :as rx] - [cuerdas.core :as str])) + [cuerdas.core :as str] + [potok.v2.core :as ptk])) (declare page-proxy) @@ -269,9 +271,19 @@ (not (r/check-permission plugin-id "content:read")) (u/not-valid plugin-id :openPage "Plugin doesn't have 'content:read' permission") + (true? new-window) + (do (st/emit! (dcm/go-to-workspace :page-id id ::rt/new-window true)) + (js/Promise.resolve nil)) + :else - (let [new-window (if (boolean? new-window) new-window false)] - (st/emit! (dcm/go-to-workspace :page-id id ::rt/new-window new-window))))) + (js/Promise. + (fn [resolve _] + (->> st/stream + (rx/filter (ptk/type? ::dwpg/initialized)) + (rx/filter #(= (deref %) id)) + (rx/take 1) + (rx/subs! #(resolve nil))) + (st/emit! (dcm/go-to-workspace :page-id id)))))) :createFlow (fn [name frame] diff --git a/frontend/src/app/util/browser_history.js b/frontend/src/app/util/browser_history.js index 52831614b3..3bfdcb49ec 100644 --- a/frontend/src/app/util/browser_history.js +++ b/frontend/src/app/util/browser_history.js @@ -40,7 +40,7 @@ goog.scope(function() { }; self.set_token_BANG_ = function(instance, token) { - instance.setToken(token); + instance?.setToken(token); } self.replace_token_BANG_ = function(instance, token) { diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 2b94869245..b80a385ad5 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -842,10 +842,11 @@ ([uri name] (open-new-window uri name "noopener,noreferrer")) ([uri name features] - (when-let [new-window (.open js/window (str uri) name features)] - (when (not= name "_blank") - (when-let [location (.-location new-window)] - (.reload location)))))) + (when (exists? js/window) + (when-let [new-window (.open js/window (str uri) name features)] + (when (not= name "_blank") + (when-let [location (.-location new-window)] + (.reload location))))))) (defn browser-back [] diff --git a/frontend/test/frontend_tests/plugins/page_test.cljs b/frontend/test/frontend_tests/plugins/page_test.cljs new file mode 100644 index 0000000000..496465ff10 --- /dev/null +++ b/frontend/test/frontend_tests/plugins/page_test.cljs @@ -0,0 +1,94 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC Sucursal en España SL + +(ns frontend-tests.plugins.page-test + (:require + [app.common.test-helpers.files :as cthf] + [app.main.data.workspace.pages :as dwpg] + [app.main.store :as st] + [app.plugins.api :as api] + [app.util.object :as obj] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.state :as ths] + [potok.v2.core :as ptk])) + +(defn- setup + "Creates a file with two pages (page1 as current) and a plugin context." + [] + (let [file (-> (cthf/sample-file :file1 :page-label :page1) + (cthf/add-sample-page :page2) + (cthf/switch-to-page :page1)) + store (ths/setup-store file) + _ (set! st/state store) + _ (set! st/stream (ptk/input-stream store)) + context (api/create-context "00000000-0000-0000-0000-000000000000")] + {:file file :store store :context context})) + +(defn- mock-page-initialized + "Simulates the two effects of initialize-page* without routing: + updates current-page-id in state, then emits the public ::dwpg/initialized event." + [store page-id] + (ptk/emit! store #(assoc % :current-page-id page-id)) + (ptk/emit! store (ptk/data-event ::dwpg/initialized page-id))) + +(t/deftest test-open-page-returns-promise + (let [{:keys [context]} (setup) + ^js pages (.. context -currentFile -pages) + ^js page2 (aget pages 1)] + (t/is (instance? js/Promise (.openPage context page2))))) + +(t/deftest test-open-page-new-window-returns-promise + (let [{:keys [context]} (setup) + ^js pages (.. context -currentFile -pages) + ^js page2 (aget pages 1)] + (t/is (instance? js/Promise (.openPage context page2 true))))) + +(t/deftest test-open-page-invalid-arg-returns-nil + (let [{:keys [context]} (setup)] + (t/is (nil? (.openPage context "not-a-page"))))) + +(t/deftest test-open-page-resolves-when-page-changes + (t/async done + (let [{:keys [store context]} (setup) + ^js pages (.. context -currentFile -pages) + ^js page2 (aget pages 1) + page2-id (obj/get page2 "$id")] + + (-> (.openPage context page2) + (.then (fn [_] + (t/is (= (:current-page-id @store) page2-id)) + (done)))) + + (mock-page-initialized store page2-id)))) + +(t/deftest test-open-page-does-not-resolve-for-wrong-page + ;; Promise should not resolve when a different page is initialized + (t/async done + (let [{:keys [store context]} (setup) + ^js pages (.. context -currentFile -pages) + ^js page1 (aget pages 0) + ^js page2 (aget pages 1) + page1-id (obj/get page1 "$id") + page2-id (obj/get page2 "$id") + resolved? (atom false)] + + (-> (.openPage context page2) + (.then (fn [_] (reset! resolved? true)))) + + ;; Initialize page1 (wrong page) — promise should not resolve + (mock-page-initialized store page1-id) + + ;; Give microtasks a chance to run, then verify promise is still pending + (js/setTimeout + (fn [] + (t/is (not @resolved?)) + ;; Now initialize the correct page and confirm it resolves + (-> (.openPage context page2) + (.then (fn [_] + (t/is (= (:current-page-id @store) page2-id)) + (done)))) + (mock-page-initialized store page2-id)) + 0)))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index e96ceece01..5f0c6c0659 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -25,6 +25,7 @@ [frontend-tests.logic.pasting-in-containers-test] [frontend-tests.main-errors-test] [frontend-tests.plugins.context-shapes-test] + [frontend-tests.plugins.page-test] [frontend-tests.plugins.parser-test] [frontend-tests.plugins.tokens-test] [frontend-tests.plugins.utils-test] @@ -74,6 +75,7 @@ frontend-tests.logic.groups-test frontend-tests.logic.pasting-in-containers-test frontend-tests.plugins.context-shapes-test + frontend-tests.plugins.page-test frontend-tests.plugins.parser-test frontend-tests.plugins.tokens-test frontend-tests.plugins.utils-test diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index 614a77407b..c9c48444c6 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -3,6 +3,7 @@ - **plugins-runtime**: Added `version` field that returns the current version - **plugins-runtime**: Added optional parameter `throwOnError` to `penpot.ui.sendMessage` (default false, backwards-compatible) - **plugin-types**: Added a flags subcontexts with the flag `naturalChildrenOrdering` +- **plugin-types**: `penpot.openPage()` now returns `Promise` and should be awaited before performing operations on the new page - **plugin-types**: Fix penpot.openPage() to navigate in same tab by default - **plugin-types:** Change `LibraryComponent.isVariant()` return type to type guard `this is LibraryVariantComponent` - **plugin-types**: Added `createVariantFromComponents` diff --git a/plugins/libs/plugin-types/index.d.ts b/plugins/libs/plugin-types/index.d.ts index 7293964814..18d8eafcfa 100644 --- a/plugins/libs/plugin-types/index.d.ts +++ b/plugins/libs/plugin-types/index.d.ts @@ -1279,10 +1279,10 @@ export interface Context { * * @example * ```js - * context.openPage(page); + * await context.openPage(page); * ``` */ - openPage(page: Page | string, newWindow?: boolean): void; + openPage(page: Page | string, newWindow?: boolean): Promise; /** * Aligning will move all the selected layers to a position relative to one diff --git a/plugins/libs/plugins-runtime/src/lib/api/index.ts b/plugins/libs/plugins-runtime/src/lib/api/index.ts index 885967a2f1..6faa7b1925 100644 --- a/plugins/libs/plugins-runtime/src/lib/api/index.ts +++ b/plugins/libs/plugins-runtime/src/lib/api/index.ts @@ -348,9 +348,9 @@ export function createApi( return plugin.context.createPage(); }, - openPage(page: Page | string, newWindow?: boolean): void { + openPage(page: Page | string, newWindow?: boolean): Promise { checkPermission('content:read'); - plugin.context.openPage(page, newWindow ?? false); + return plugin.context.openPage(page, newWindow ?? false); }, alignHorizontal(