From 5b6041624ae5d72f9d4cd9722241d7b91b6d6a7d Mon Sep 17 00:00:00 2001 From: Alonso Torres Date: Thu, 11 Jun 2026 13:33:48 +0200 Subject: [PATCH] :bug: Fix export presets not preserved in view mode inspect (#9972) * :bug: Fix export presets not preserved in view mode inspect * :bug: Changes after review --- .../ui/specs/viewer-inspect-exports.spec.js | 49 +++++++++++++++ frontend/src/app/main/data/viewer.cljs | 7 +++ frontend/src/app/main/ui/inspect/exports.cljs | 61 +++++++++++++------ 3 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 frontend/playwright/ui/specs/viewer-inspect-exports.spec.js diff --git a/frontend/playwright/ui/specs/viewer-inspect-exports.spec.js b/frontend/playwright/ui/specs/viewer-inspect-exports.spec.js new file mode 100644 index 0000000000..3e99476fe5 --- /dev/null +++ b/frontend/playwright/ui/specs/viewer-inspect-exports.spec.js @@ -0,0 +1,49 @@ +import { test, expect } from "@playwright/test"; +import { ViewerPage } from "../pages/ViewerPage"; + +test.beforeEach(async ({ page }) => { + await ViewerPage.init(page); +}); + +const multipleBoardsFileId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb0"; +const multipleBoardsPageId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb3"; + +test("[View mode] Export presets are preserved when navigating between boards in inspect mode", async ({ + page, +}) => { + const viewer = new ViewerPage(page); + await viewer.setupLoggedInUser(); + await viewer.setupFileWithMultipleBoards(); + + await viewer.goToViewer({ + fileId: multipleBoardsFileId, + pageId: multipleBoardsPageId, + }); + + // Enter inspect (code) mode + await viewer.showCode(); + + // Wait for the inspect panel to load + await page.waitForSelector(".main_ui_inspect_exports__add-export"); + + // Add an export preset via the "+" button in the Export section + const addExportButton = page.locator(".main_ui_inspect_exports__add-export"); + await addExportButton.click(); + + // Verify the "Export 1 element" button appears, confirming the preset was added + const exportButton = page.getByRole("button", { name: "Export 1 element" }); + await expect(exportButton).toBeVisible(); + + // Navigate to another board + const nextButton = page.getByRole("button", { name: "Next" }); + await nextButton.click(); + await expect(page).toHaveURL(/&index=1/); + + // Navigate back to the first board + const prevButton = page.locator(".main_ui_viewer__viewer-go-prev"); + await prevButton.click(); + await expect(page).toHaveURL(/&index=0/); + + // Export preset should still be visible after returning to the first board + await expect(exportButton).toBeVisible(); +}); diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index d05df4bd13..87b9c1007b 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -583,6 +583,13 @@ (update [_ state] (d/dissoc-in state [:viewer-local :nav-scroll])))) +(defn update-exports-cache + [shapes-key exports] + (ptk/reify ::update-exports-cache + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:inspect-exports-cache shapes-key] exports)))) + (defn complete-animation [] (ptk/reify ::complete-animation diff --git a/frontend/src/app/main/ui/inspect/exports.cljs b/frontend/src/app/main/ui/inspect/exports.cljs index 00da7925fe..d431a5aeeb 100644 --- a/frontend/src/app/main/ui/inspect/exports.cljs +++ b/frontend/src/app/main/ui/inspect/exports.cljs @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.main.data.exports.assets :as de] + [app.main.data.viewer :as dv] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.select :refer [select]] @@ -17,8 +18,12 @@ [app.util.dom :as dom] [app.util.i18n :refer [tr c]] [app.util.keyboard :as kbd] + [okulary.core :as l] [rumext.v2 :as mf])) +(def ^:private exports-cache-ref + (l/derived :inspect-exports-cache st/state)) + (mf/defc exports {::mf/wrap [#(mf/memo % =)]} [{:keys [shapes page-id file-id share-id type] :as props}] @@ -65,46 +70,58 @@ (de/request-export {:exports exports}) (de/export-shapes-event exports "viewer"))))) + shapes-key + (mf/use-memo (mf/deps shapes) #(vec (sort (map :id shapes)))) + add-export (mf/use-callback - (mf/deps shapes) + (mf/deps shapes exports) (fn [] (let [xspec {:type :png :suffix "" - :scale 1}] - (swap! exports conj xspec)))) + :scale 1} + new-exports (conj @exports xspec)] + (reset! exports new-exports) + (st/emit! (dv/update-exports-cache shapes-key new-exports))))) delete-export (mf/use-callback - (mf/deps shapes) + (mf/deps shapes exports) (fn [index] - (swap! exports (fn [exports] - (let [[before after] (split-at index exports)] - (d/concat-vec before (rest after))))))) + (let [new-exports (let [[before after] (split-at index @exports)] + (d/concat-vec before (rest after)))] + (reset! exports new-exports) + (st/emit! (dv/update-exports-cache shapes-key new-exports))))) on-scale-change (mf/use-callback - (mf/deps shapes) + (mf/deps shapes exports) (fn [index event] - (let [scale (d/parse-double event)] - (swap! exports assoc-in [index :scale] scale)))) + (let [scale (d/parse-double event) + new-exports (assoc-in @exports [index :scale] scale)] + (reset! exports new-exports) + (st/emit! (dv/update-exports-cache shapes-key new-exports))))) on-suffix-change (mf/use-callback - (mf/deps shapes) + (mf/deps shapes exports) (fn [event] (let [value (dom/get-target-val event) index (-> (dom/get-current-target event) (dom/get-data "value") - (d/parse-integer))] - (swap! exports assoc-in [index :suffix] value)))) + (d/parse-integer)) + new-exports (assoc-in @exports [index :suffix] value)] + (reset! exports new-exports) + (st/emit! (dv/update-exports-cache shapes-key new-exports))))) on-type-change (mf/use-callback - (mf/deps shapes) + (mf/deps shapes exports) (fn [index event] - (let [type (keyword event)] - (swap! exports assoc-in [index :type] type)))) + (let [type (keyword event) + new-exports (assoc-in @exports [index :type] type)] + (reset! exports new-exports) + (st/emit! (dv/update-exports-cache shapes-key new-exports))))) manage-key-down (mf/use-callback @@ -130,10 +147,14 @@ (mf/use-effect (mf/deps shapes) (fn [] - (reset! exports (-> (mapv #(:exports % []) shapes) - flatten - distinct - vec)))) + (let [shapes-key (vec (sort (map :id shapes))) + cached (get @exports-cache-ref shapes-key)] + (if (some? cached) + (reset! exports cached) + (reset! exports (->> shapes + (mapcat #(:exports % [])) + (distinct) + vec)))))) [:div {:class (stl/css :element-set)} [:div {:class (stl/css :element-title)} [:> title-bar* {:collapsable false