From dc2e2c081edfd5e52b257d0a19e5781290f41cda Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 9 Jun 2026 16:44:40 +0200 Subject: [PATCH] :bug: Fix openPage plugin problem --- .../src/app/main/data/workspace/pages.cljs | 1 + frontend/src/app/plugins/api.cljs | 18 +++- frontend/src/app/plugins/page.cljs | 17 +++- .../frontend_tests/plugins/page_test.cljs | 93 +++++++++++++++++++ frontend/test/frontend_tests/runner.cljs | 2 + plugins/CHANGELOG.md | 1 + plugins/libs/plugin-types/index.d.ts | 4 +- .../libs/plugins-runtime/src/lib/api/index.ts | 4 +- 8 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 frontend/test/frontend_tests/plugins/page_test.cljs diff --git a/frontend/src/app/main/data/workspace/pages.cljs b/frontend/src/app/main/data/workspace/pages.cljs index 9ef4a20f19..c4a4452d2b 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 :page-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..374ee5df44 100644 --- a/frontend/src/app/plugins/api.cljs +++ b/frontend/src/app/plugins/api.cljs @@ -54,7 +54,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 +572,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? :page-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..7e15c00404 100644 --- a/frontend/src/app/plugins/page.cljs +++ b/frontend/src/app/plugins/page.cljs @@ -32,7 +32,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 +270,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? :page-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/test/frontend_tests/plugins/page_test.cljs b/frontend/test/frontend_tests/plugins/page_test.cljs new file mode 100644 index 0000000000..365157401c --- /dev/null +++ b/frontend/test/frontend_tests/plugins/page_test.cljs @@ -0,0 +1,93 @@ +;; 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.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 :page-initialized event." + [store page-id] + (ptk/emit! store #(assoc % :current-page-id page-id)) + (ptk/emit! store (ptk/data-event :page-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(