mirror of
https://github.com/penpot/penpot.git
synced 2026-08-01 10:56:20 +00:00
🐛 Fix shape export failures when export name is nil or empty (#10852)
Use cuerdas blank-name handling directly when normalizing frontend export payloads. Replace nil or blank export names with the object-id string in request-simple-export, request-multiple-export, clipboard export, and plugin direct export payloads so that the backend always receives a valid name. Add focused frontend tests for nil/blank name normalization and normalized request params. AI-assisted-by: nex-n2-pro
This commit is contained in:
parent
fadb3124a0
commit
040080749b
@ -20,10 +20,27 @@
|
|||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.websocket :as ws]
|
[app.util.websocket :as ws]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
|
[cuerdas.core :as str]
|
||||||
[potok.v2.core :as ptk]))
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
(def default-timeout 5000)
|
(def default-timeout 5000)
|
||||||
|
|
||||||
|
(defn normalize-export
|
||||||
|
[{:keys [object-id name] :as export}]
|
||||||
|
(assoc export :name (if (str/blank? name)
|
||||||
|
(str object-id)
|
||||||
|
name)))
|
||||||
|
|
||||||
|
(defn- normalize-exports
|
||||||
|
[exports]
|
||||||
|
(mapv normalize-export exports))
|
||||||
|
|
||||||
|
(defn- normalize-export-shapes-params
|
||||||
|
[{:keys [exports] :as params}]
|
||||||
|
(cond-> params
|
||||||
|
(seq exports)
|
||||||
|
(assoc :exports (normalize-exports exports))))
|
||||||
|
|
||||||
(defn toggle-detail-visibililty
|
(defn toggle-detail-visibililty
|
||||||
[]
|
[]
|
||||||
(ptk/reify ::toggle-detail-visibililty
|
(ptk/reify ::toggle-detail-visibililty
|
||||||
@ -181,104 +198,106 @@
|
|||||||
|
|
||||||
(defn request-simple-export
|
(defn request-simple-export
|
||||||
[{:keys [export]}]
|
[{:keys [export]}]
|
||||||
(ptk/reify ::request-simple-export
|
(let [export (normalize-export export)]
|
||||||
ptk/UpdateEvent
|
(ptk/reify ::request-simple-export
|
||||||
(update [_ state]
|
ptk/UpdateEvent
|
||||||
(cond-> state
|
(update [_ state]
|
||||||
(not (use-wasm-export? state export))
|
(cond-> state
|
||||||
(update :export assoc :in-progress true :id uuid/zero)))
|
(not (use-wasm-export? state export))
|
||||||
|
(update :export assoc :in-progress true :id uuid/zero)))
|
||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(if (use-wasm-export? state export)
|
(if (use-wasm-export? state export)
|
||||||
(do
|
(do
|
||||||
(case (:type export)
|
(case (:type export)
|
||||||
:pdf (wasm.exports/export-pdf export)
|
:pdf (wasm.exports/export-pdf export)
|
||||||
(wasm.exports/export-image export))
|
(wasm.exports/export-image export))
|
||||||
(rx/empty))
|
(rx/empty))
|
||||||
(let [profile-id (:profile-id state)
|
(let [profile-id (:profile-id state)
|
||||||
params {:exports [export]
|
params (normalize-export-shapes-params {:exports [export]
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:cmd :export-shapes
|
:cmd :export-shapes
|
||||||
:wait true
|
:wait true
|
||||||
:is-wasm (wasm-export-enabled? state)}]
|
:is-wasm (wasm-export-enabled? state)})]
|
||||||
(rx/concat
|
(rx/concat
|
||||||
(dwp/force-persist-and-wait 400)
|
(dwp/force-persist-and-wait 400)
|
||||||
|
|
||||||
(->> (rp/cmd! :export params)
|
(->> (rp/cmd! :export params)
|
||||||
(rx/map (fn [{:keys [filename mtype uri]}]
|
(rx/map (fn [{:keys [filename mtype uri]}]
|
||||||
(dom/trigger-download-uri filename mtype uri)
|
(dom/trigger-download-uri filename mtype uri)
|
||||||
(clear-export-state uuid/zero)))
|
(clear-export-state uuid/zero)))
|
||||||
(rx/catch (fn [cause]
|
(rx/catch (fn [cause]
|
||||||
(rx/concat
|
(rx/concat
|
||||||
(rx/of (clear-export-state uuid/zero))
|
(rx/of (clear-export-state uuid/zero))
|
||||||
(rx/throw cause)))))))))))
|
(rx/throw cause))))))))))))
|
||||||
|
|
||||||
(defn request-multiple-export
|
(defn request-multiple-export
|
||||||
[{:keys [exports cmd name]
|
[{:keys [exports cmd name]
|
||||||
:or {cmd :export-shapes}
|
:or {cmd :export-shapes}
|
||||||
:as params}]
|
:as params}]
|
||||||
(ptk/reify ::request-multiple-export
|
(let [exports (normalize-exports exports)]
|
||||||
ptk/WatchEvent
|
(ptk/reify ::request-multiple-export
|
||||||
(watch [_ state _]
|
ptk/WatchEvent
|
||||||
(let [resource-id (volatile! nil)
|
(watch [_ state _]
|
||||||
profile-id (:profile-id state)
|
(let [resource-id (volatile! nil)
|
||||||
ws-conn (:ws-conn state)
|
profile-id (:profile-id state)
|
||||||
params (cond->
|
ws-conn (:ws-conn state)
|
||||||
{:exports exports
|
params (cond->
|
||||||
:cmd cmd
|
{:exports exports
|
||||||
:profile-id profile-id
|
:cmd cmd
|
||||||
:force-multiple true
|
:profile-id profile-id
|
||||||
:is-wasm (wasm-export-enabled? state)}
|
:force-multiple true
|
||||||
(some? name)
|
:is-wasm (wasm-export-enabled? state)}
|
||||||
(assoc :name name))
|
(some? name)
|
||||||
|
(assoc :name name))
|
||||||
|
|
||||||
progress-stream
|
progress-stream
|
||||||
(->> (ws/get-rcv-stream ws-conn)
|
(->> (ws/get-rcv-stream ws-conn)
|
||||||
(rx/filter ws/message-event?)
|
(rx/filter ws/message-event?)
|
||||||
(rx/map :payload)
|
(rx/map :payload)
|
||||||
(rx/filter #(= :export-update (:type %)))
|
(rx/filter #(= :export-update (:type %)))
|
||||||
(rx/filter #(= @resource-id (:resource-id %)))
|
(rx/filter #(= @resource-id (:resource-id %)))
|
||||||
(rx/share))
|
(rx/share))
|
||||||
|
|
||||||
stopper
|
stopper
|
||||||
(rx/filter #(or (= "ended" (:status %))
|
(rx/filter #(or (= "ended" (:status %))
|
||||||
(= "error" (:status %)))
|
(= "error" (:status %)))
|
||||||
progress-stream)]
|
progress-stream)]
|
||||||
|
|
||||||
(swap! st/ongoing-tasks conj :export)
|
(swap! st/ongoing-tasks conj :export)
|
||||||
|
|
||||||
(rx/merge
|
(rx/merge
|
||||||
;; Force that all data is persisted; best effort.
|
;; Force that all data is persisted; best effort.
|
||||||
(rx/of ::dwp/force-persist)
|
(rx/of ::dwp/force-persist)
|
||||||
|
|
||||||
;; Launch the exportation process and stores the resource id
|
;; Launch the exportation process and stores the resource id
|
||||||
;; locally.
|
;; locally.
|
||||||
(->> (rp/cmd! :export params)
|
(->> (rp/cmd! :export params)
|
||||||
(rx/map (fn [{:keys [id] :as resource}]
|
(rx/map (fn [{:keys [id] :as resource}]
|
||||||
(vreset! resource-id id)
|
(vreset! resource-id id)
|
||||||
(initialize-export-status exports cmd resource))))
|
(initialize-export-status exports cmd resource))))
|
||||||
|
|
||||||
;; We proceed to update the export state with incoming
|
;; We proceed to update the export state with incoming
|
||||||
;; progress updates. We delay the stopper for give some time
|
;; progress updates. We delay the stopper for give some time
|
||||||
;; to update the status with ended or errored status before
|
;; to update the status with ended or errored status before
|
||||||
;; close the stream.
|
;; close the stream.
|
||||||
(->> progress-stream
|
(->> progress-stream
|
||||||
(rx/map update-export-status)
|
(rx/map update-export-status)
|
||||||
(rx/take-until (rx/delay 500 stopper))
|
(rx/take-until (rx/delay 500 stopper))
|
||||||
(rx/finalize (fn []
|
(rx/finalize (fn []
|
||||||
(swap! st/ongoing-tasks disj :export))))
|
(swap! st/ongoing-tasks disj :export))))
|
||||||
|
|
||||||
;; We hide need to hide the ui elements of the export after
|
;; We hide need to hide the ui elements of the export after
|
||||||
;; some interval. We also delay a little bit more the stopper
|
;; some interval. We also delay a little bit more the stopper
|
||||||
;; for ensure that after some security time, the stream is
|
;; for ensure that after some security time, the stream is
|
||||||
;; completely closed.
|
;; completely closed.
|
||||||
(->> progress-stream
|
(->> progress-stream
|
||||||
(rx/filter #(= "ended" (:status %)))
|
(rx/filter #(= "ended" (:status %)))
|
||||||
(rx/take 1)
|
(rx/take 1)
|
||||||
(rx/delay default-timeout)
|
(rx/delay default-timeout)
|
||||||
(rx/map #(clear-export-state @resource-id))
|
(rx/map #(clear-export-state @resource-id))
|
||||||
(rx/take-until (rx/delay 6000 stopper))))))))
|
(rx/take-until (rx/delay 6000 stopper)))))))))
|
||||||
|
|
||||||
(defn request-export
|
(defn request-export
|
||||||
[{:keys [exports] :as params}]
|
[{:keys [exports] :as params}]
|
||||||
|
|||||||
@ -34,6 +34,7 @@
|
|||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.main.data.changes :as dch]
|
[app.main.data.changes :as dch]
|
||||||
[app.main.data.event :as ev]
|
[app.main.data.event :as ev]
|
||||||
|
[app.main.data.exports.assets :as de]
|
||||||
[app.main.data.exports.wasm :as wasm.exports]
|
[app.main.data.exports.wasm :as wasm.exports]
|
||||||
[app.main.data.helpers :as dsh]
|
[app.main.data.helpers :as dsh]
|
||||||
[app.main.data.notifications :as ntf]
|
[app.main.data.notifications :as ntf]
|
||||||
@ -1147,16 +1148,16 @@
|
|||||||
page-id (:current-page-id state)
|
page-id (:current-page-id state)
|
||||||
selected (first (dsh/lookup-selected state))
|
selected (first (dsh/lookup-selected state))
|
||||||
|
|
||||||
export {:file-id file-id
|
export (de/normalize-export {:file-id file-id
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:object-id selected
|
:object-id selected
|
||||||
;; webp would be preferrable, but PNG is the most supported image MIME type by clipboard APIs.
|
;; webp would be preferrable, but PNG is the most supported image MIME type by clipboard APIs.
|
||||||
:type :png
|
:type :png
|
||||||
;; Always use 2 to ensure good enough quality for wireframes.
|
;; Always use 2 to ensure good enough quality for wireframes.
|
||||||
:scale 2
|
:scale 2
|
||||||
:suffix ""
|
:suffix ""
|
||||||
:enabled true
|
:enabled true
|
||||||
:name ""}
|
:name ""})
|
||||||
|
|
||||||
;; Create a deferred promise immediately, before any async operations.
|
;; Create a deferred promise immediately, before any async operations.
|
||||||
;; Registering the clipboard write NOW preserves the user-gesture security
|
;; Registering the clipboard write NOW preserves the user-gesture security
|
||||||
|
|||||||
@ -34,6 +34,7 @@
|
|||||||
[app.common.types.text :as txt]
|
[app.common.types.text :as txt]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
|
[app.main.data.exports.assets :as de]
|
||||||
[app.main.data.exports.wasm :as wasm.exports]
|
[app.main.data.exports.wasm :as wasm.exports]
|
||||||
[app.main.data.persistence :as dwp]
|
[app.main.data.persistence :as dwp]
|
||||||
[app.main.data.plugins :as dp]
|
[app.main.data.plugins :as dp]
|
||||||
@ -1547,13 +1548,13 @@
|
|||||||
:profile-id (:profile-id @st/state)
|
:profile-id (:profile-id @st/state)
|
||||||
:wait true
|
:wait true
|
||||||
:is-wasm false
|
:is-wasm false
|
||||||
:exports [{:file-id file-id
|
:exports [(de/normalize-export {:file-id file-id
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:object-id id
|
:object-id id
|
||||||
:name (:name shape)
|
:name (:name shape)
|
||||||
:type (:type value :png)
|
:type (:type value :png)
|
||||||
:suffix (:suffix value "")
|
:suffix (:suffix value "")
|
||||||
:scale (:scale value 1)}]}]
|
:scale (:scale value 1)})]}]
|
||||||
(js/Promise.
|
(js/Promise.
|
||||||
(fn [resolve reject]
|
(fn [resolve reject]
|
||||||
;; The exporter renders the file from its persisted
|
;; The exporter renders the file from its persisted
|
||||||
|
|||||||
99
frontend/test/frontend_tests/data/exports_assets_test.cljs
Normal file
99
frontend/test/frontend_tests/data/exports_assets_test.cljs
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
;; 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.data.exports-assets-test
|
||||||
|
(:require
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.main.data.exports.assets :as de]
|
||||||
|
[app.main.data.persistence :as dwp]
|
||||||
|
[app.main.repo :as repo]
|
||||||
|
[app.main.store :as st]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.websocket :as ws]
|
||||||
|
[beicon.v2.core :as rx]
|
||||||
|
[cljs.test :as t :include-macros true]
|
||||||
|
[frontend-tests.helpers.events :as the]
|
||||||
|
[frontend-tests.helpers.mock :as mock]
|
||||||
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
|
(def ^:private export {:id (uuid/next)
|
||||||
|
:object-id (uuid/next)
|
||||||
|
:type :png
|
||||||
|
:suffix ""
|
||||||
|
:scale 1})
|
||||||
|
|
||||||
|
(defn- export-with-name
|
||||||
|
[name]
|
||||||
|
(merge export {:name name}))
|
||||||
|
|
||||||
|
(defn- test-state
|
||||||
|
[]
|
||||||
|
{:profile-id (:id export)
|
||||||
|
:ws-conn nil})
|
||||||
|
|
||||||
|
(t/deftest normalize-export-preserves-existing-name
|
||||||
|
(t/is (= (export-with-name "Layer 1")
|
||||||
|
(de/normalize-export (export-with-name "Layer 1")))))
|
||||||
|
|
||||||
|
(t/deftest normalize-export-replaces-nil-name-with-object-id
|
||||||
|
(t/is (= (export-with-name (str (:object-id export)))
|
||||||
|
(de/normalize-export (assoc export :name nil)))))
|
||||||
|
|
||||||
|
(t/deftest normalize-export-replaces-empty-name-with-object-id
|
||||||
|
(t/is (= (export-with-name (str (:object-id export)))
|
||||||
|
(de/normalize-export (assoc export :name "")))))
|
||||||
|
|
||||||
|
(t/deftest request-simple-export-sends-normalized-export
|
||||||
|
(t/async done
|
||||||
|
(let [export (export-with-name "")
|
||||||
|
observed (atom nil)]
|
||||||
|
(mock/with-mocks {repo/cmd! (mock/stub (fn [_ params]
|
||||||
|
(reset! observed params)
|
||||||
|
(rx/of {:filename "export.png"
|
||||||
|
:mtype "image/png"
|
||||||
|
:uri "blob:export"})))
|
||||||
|
dwp/force-persist-and-wait (mock/stub (fn [_] (rx/of ::force-persisted)))
|
||||||
|
dom/trigger-download-uri (mock/stub (fn [& _] nil))}
|
||||||
|
(fn [done']
|
||||||
|
(let [completed (fn [_state]
|
||||||
|
(t/is (= (export-with-name (str (:object-id export)))
|
||||||
|
(-> @observed :exports first))))]
|
||||||
|
(ptk/emit! (the/prepare-store (test-state) done' completed)
|
||||||
|
(de/request-simple-export {:export export})
|
||||||
|
:the/end)))
|
||||||
|
done))))
|
||||||
|
|
||||||
|
(t/deftest request-multiple-export-sends-normalized-enabled-exports
|
||||||
|
(t/async done
|
||||||
|
(let [exports [{:id "enabled-1"
|
||||||
|
:object-id "enabled-1"
|
||||||
|
:shape {:id "enabled-1"}
|
||||||
|
:type :png
|
||||||
|
:suffix ""
|
||||||
|
:scale 1
|
||||||
|
:enabled true
|
||||||
|
:name ""}]
|
||||||
|
observed (atom nil)]
|
||||||
|
(mock/with-mocks {repo/cmd! (mock/stub (fn [_ params]
|
||||||
|
(reset! observed params)
|
||||||
|
(rx/of {:id (:id export)})))
|
||||||
|
ws/get-rcv-stream (mock/stub (fn [_] (rx/empty)))
|
||||||
|
st/ongoing-tasks (atom #{})}
|
||||||
|
(fn [done']
|
||||||
|
(let [completed (fn [_state]
|
||||||
|
(t/is (= [{:id "enabled-1"
|
||||||
|
:object-id "enabled-1"
|
||||||
|
:shape {:id "enabled-1"}
|
||||||
|
:type :png
|
||||||
|
:suffix ""
|
||||||
|
:scale 1
|
||||||
|
:enabled true
|
||||||
|
:name "enabled-1"}]
|
||||||
|
(:exports @observed))))]
|
||||||
|
(ptk/emit! (the/prepare-store (test-state) done' completed)
|
||||||
|
(de/request-multiple-export {:exports exports})
|
||||||
|
:the/end)))
|
||||||
|
done))))
|
||||||
@ -7,6 +7,7 @@
|
|||||||
[frontend-tests.basic-shapes-test]
|
[frontend-tests.basic-shapes-test]
|
||||||
[frontend-tests.code-gen-style-test]
|
[frontend-tests.code-gen-style-test]
|
||||||
[frontend-tests.copy-as-svg-test]
|
[frontend-tests.copy-as-svg-test]
|
||||||
|
[frontend-tests.data.exports-assets-test]
|
||||||
[frontend-tests.data.nitrate-test]
|
[frontend-tests.data.nitrate-test]
|
||||||
[frontend-tests.data.repo-test]
|
[frontend-tests.data.repo-test]
|
||||||
[frontend-tests.data.store-test]
|
[frontend-tests.data.store-test]
|
||||||
@ -88,6 +89,7 @@
|
|||||||
'frontend-tests.data.nitrate-test
|
'frontend-tests.data.nitrate-test
|
||||||
'frontend-tests.data.repo-test
|
'frontend-tests.data.repo-test
|
||||||
'frontend-tests.data.store-test
|
'frontend-tests.data.store-test
|
||||||
|
'frontend-tests.data.exports-assets-test
|
||||||
'frontend-tests.errors-test
|
'frontend-tests.errors-test
|
||||||
'frontend-tests.main-errors-test
|
'frontend-tests.main-errors-test
|
||||||
'frontend-tests.data.uploads-test
|
'frontend-tests.data.uploads-test
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user