mirror of
https://github.com/penpot/penpot.git
synced 2025-06-07 09:11:41 +02:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
bdb777516e
17 changed files with 456 additions and 175 deletions
|
@ -60,15 +60,25 @@
|
||||||
(media/validate-media-type! content)
|
(media/validate-media-type! content)
|
||||||
(media/validate-media-size! content)
|
(media/validate-media-size! content)
|
||||||
|
|
||||||
(db/run! cfg (fn [cfg]
|
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||||
(let [object (create-file-media-object cfg params)
|
;; We get the minimal file for proper checking if
|
||||||
props {:name (:name params)
|
;; file is not already deleted
|
||||||
|
(let [_ (files/get-minimal-file conn file-id)
|
||||||
|
mobj (create-file-media-object cfg params)]
|
||||||
|
|
||||||
|
(db/update! conn :file
|
||||||
|
{:modified-at (dt/now)
|
||||||
|
:has-media-trimmed false}
|
||||||
|
{:id file-id}
|
||||||
|
{::db/return-keys false})
|
||||||
|
|
||||||
|
(with-meta mobj
|
||||||
|
{::audit/replace-props
|
||||||
|
{:name (:name params)
|
||||||
:file-id file-id
|
:file-id file-id
|
||||||
:is-local (:is-local params)
|
:is-local (:is-local params)
|
||||||
:size (:size content)
|
:size (:size content)
|
||||||
:mtype (:mtype content)}]
|
:mtype (:mtype content)}})))))
|
||||||
(with-meta object
|
|
||||||
{::audit/replace-props props})))))
|
|
||||||
|
|
||||||
(defn- big-enough-for-thumbnail?
|
(defn- big-enough-for-thumbnail?
|
||||||
"Checks if the provided image info is big enough for
|
"Checks if the provided image info is big enough for
|
||||||
|
@ -142,20 +152,14 @@
|
||||||
:always
|
:always
|
||||||
(assoc ::image (process-main-image info)))))
|
(assoc ::image (process-main-image info)))))
|
||||||
|
|
||||||
(defn create-file-media-object
|
(defn- create-file-media-object
|
||||||
[{:keys [::sto/storage ::db/conn ::wrk/executor]}
|
[{:keys [::sto/storage ::db/conn ::wrk/executor] :as cfg}
|
||||||
{:keys [id file-id is-local name content]}]
|
{:keys [id file-id is-local name content]}]
|
||||||
|
|
||||||
(let [result (px/invoke! executor (partial process-image content))
|
(let [result (px/invoke! executor (partial process-image content))
|
||||||
image (sto/put-object! storage (::image result))
|
image (sto/put-object! storage (::image result))
|
||||||
thumb (when-let [params (::thumb result)]
|
thumb (when-let [params (::thumb result)]
|
||||||
(sto/put-object! storage params))]
|
(sto/put-object! storage params))]
|
||||||
|
|
||||||
(db/update! conn :file
|
|
||||||
{:modified-at (dt/now)
|
|
||||||
:has-media-trimmed false}
|
|
||||||
{:id file-id})
|
|
||||||
|
|
||||||
(db/exec-one! conn [sql:create-file-media-object
|
(db/exec-one! conn [sql:create-file-media-object
|
||||||
(or id (uuid/next))
|
(or id (uuid/next))
|
||||||
file-id is-local name
|
file-id is-local name
|
||||||
|
@ -182,7 +186,18 @@
|
||||||
::sm/params schema:create-file-media-object-from-url}
|
::sm/params schema:create-file-media-object-from-url}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(files/check-edition-permissions! pool profile-id file-id)
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
(create-file-media-object-from-url cfg (assoc params :profile-id profile-id)))
|
;; We get the minimal file for proper checking if file is not
|
||||||
|
;; already deleted
|
||||||
|
(let [_ (files/get-minimal-file cfg file-id)
|
||||||
|
mobj (create-file-media-object-from-url cfg (assoc params :profile-id profile-id))]
|
||||||
|
|
||||||
|
(db/update! pool :file
|
||||||
|
{:modified-at (dt/now)
|
||||||
|
:has-media-trimmed false}
|
||||||
|
{:id file-id}
|
||||||
|
{::db/return-keys false})
|
||||||
|
|
||||||
|
mobj))
|
||||||
|
|
||||||
(defn download-image
|
(defn download-image
|
||||||
[{:keys [::http/client]} uri]
|
[{:keys [::http/client]} uri]
|
||||||
|
|
|
@ -422,7 +422,9 @@
|
||||||
:deleted-at deleted-at
|
:deleted-at deleted-at
|
||||||
:id profile-id}})
|
:id profile-id}})
|
||||||
|
|
||||||
(rph/with-transform {} (session/delete-fn cfg)))))
|
|
||||||
|
(-> (rph/wrap nil)
|
||||||
|
(rph/with-transform (session/delete-fn cfg))))))
|
||||||
|
|
||||||
|
|
||||||
;; --- HELPERS
|
;; --- HELPERS
|
||||||
|
@ -431,8 +433,11 @@
|
||||||
"WITH owner_teams AS (
|
"WITH owner_teams AS (
|
||||||
SELECT tpr.team_id AS id
|
SELECT tpr.team_id AS id
|
||||||
FROM team_profile_rel AS tpr
|
FROM team_profile_rel AS tpr
|
||||||
|
JOIN team AS t ON (t.id = tpr.team_id)
|
||||||
WHERE tpr.is_owner IS TRUE
|
WHERE tpr.is_owner IS TRUE
|
||||||
AND tpr.profile_id = ?
|
AND tpr.profile_id = ?
|
||||||
|
AND (t.deleted_at IS NULL OR
|
||||||
|
t.deleted_at > now())
|
||||||
)
|
)
|
||||||
SELECT tpr.team_id AS id,
|
SELECT tpr.team_id AS id,
|
||||||
count(tpr.profile_id) - 1 AS participants
|
count(tpr.profile_id) - 1 AS participants
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
|
[app.util.time :as dt]
|
||||||
[backend-tests.helpers :as th]
|
[backend-tests.helpers :as th]
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
[datoteka.fs :as fs]))
|
[datoteka.fs :as fs]))
|
||||||
|
@ -245,3 +246,35 @@
|
||||||
(t/is (= "image/jpeg" (:mtype result)))
|
(t/is (= "image/jpeg" (:mtype result)))
|
||||||
(t/is (uuid? (:media-id result)))
|
(t/is (uuid? (:media-id result)))
|
||||||
(t/is (uuid? (:thumbnail-id result))))))
|
(t/is (uuid? (:thumbnail-id result))))))
|
||||||
|
|
||||||
|
|
||||||
|
(t/deftest media-object-upload-command-when-file-is-deleted
|
||||||
|
(let [prof (th/create-profile* 1)
|
||||||
|
proj (th/create-project* 1 {:profile-id (:id prof)
|
||||||
|
:team-id (:default-team-id prof)})
|
||||||
|
file (th/create-file* 1 {:profile-id (:id prof)
|
||||||
|
:project-id (:default-project-id prof)
|
||||||
|
:is-shared false})
|
||||||
|
|
||||||
|
_ (th/db-update! :file
|
||||||
|
{:deleted-at (dt/now)}
|
||||||
|
{:id (:id file)})
|
||||||
|
|
||||||
|
mfile {:filename "sample.jpg"
|
||||||
|
:path (th/tempfile "backend_tests/test_files/sample.jpg")
|
||||||
|
:mtype "image/jpeg"
|
||||||
|
:size 312043}
|
||||||
|
|
||||||
|
params {::th/type :upload-file-media-object
|
||||||
|
::rpc/profile-id (:id prof)
|
||||||
|
:file-id (:id file)
|
||||||
|
:is-local true
|
||||||
|
:name "testfile"
|
||||||
|
:content mfile}
|
||||||
|
|
||||||
|
out (th/command! params)]
|
||||||
|
|
||||||
|
(let [error (:error out)
|
||||||
|
error-data (ex-data error)]
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (= (:type error-data) :not-found)))))
|
||||||
|
|
|
@ -203,7 +203,24 @@
|
||||||
edata (ex-data error)]
|
edata (ex-data error)]
|
||||||
(t/is (th/ex-info? error))
|
(t/is (th/ex-info? error))
|
||||||
(t/is (= (:type edata) :validation))
|
(t/is (= (:type edata) :validation))
|
||||||
(t/is (= (:code edata) :owner-teams-with-people))))))
|
(t/is (= (:code edata) :owner-teams-with-people)))
|
||||||
|
|
||||||
|
(let [params {::th/type :delete-team
|
||||||
|
::rpc/profile-id (:id prof1)
|
||||||
|
:id (:id team1)}
|
||||||
|
out (th/command! params)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
|
||||||
|
(let [team (th/db-get :team {:id (:id team1)} {::db/remove-deleted false})]
|
||||||
|
(t/is (dt/instant? (:deleted-at team)))))
|
||||||
|
|
||||||
|
;; Request profile to be deleted
|
||||||
|
(let [params {::th/type :delete-profile
|
||||||
|
::rpc/profile-id (:id prof1)}
|
||||||
|
out (th/command! params)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:result out)))
|
||||||
|
(t/is (nil? (:error out)))))))
|
||||||
|
|
||||||
(t/deftest profile-deletion-3
|
(t/deftest profile-deletion-3
|
||||||
(let [prof1 (th/create-profile* 1)
|
(let [prof1 (th/create-profile* 1)
|
||||||
|
@ -291,7 +308,7 @@
|
||||||
out (th/command! params)]
|
out (th/command! params)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
|
|
||||||
(t/is (= {} (:result out)))
|
(t/is (nil? (:result out)))
|
||||||
(t/is (nil? (:error out))))
|
(t/is (nil? (:error out))))
|
||||||
|
|
||||||
;; query files after profile soft deletion
|
;; query files after profile soft deletion
|
||||||
|
@ -336,7 +353,7 @@
|
||||||
::rpc/profile-id (:id prof1)}
|
::rpc/profile-id (:id prof1)}
|
||||||
out (th/command! params)]
|
out (th/command! params)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (= {} (:result out)))
|
(t/is (nil? (:result out)))
|
||||||
(t/is (nil? (:error out))))
|
(t/is (nil? (:error out))))
|
||||||
|
|
||||||
(th/run-pending-tasks!)
|
(th/run-pending-tasks!)
|
||||||
|
|
|
@ -552,18 +552,17 @@
|
||||||
|
|
||||||
(defn clone-template
|
(defn clone-template
|
||||||
[{:keys [template-id project-id] :as params}]
|
[{:keys [template-id project-id] :as params}]
|
||||||
(dm/assert! (uuid? project-id))
|
|
||||||
(ptk/reify ::clone-template
|
(ptk/reify ::clone-template
|
||||||
ev/Event
|
ev/Event
|
||||||
(-data [_]
|
(-data [_]
|
||||||
{:template-id template-id
|
{:template-id template-id})
|
||||||
:project-id project-id})
|
|
||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ state _]
|
||||||
(let [{:keys [on-success on-error]
|
(let [{:keys [on-success on-error]
|
||||||
:or {on-success identity
|
:or {on-success identity
|
||||||
on-error rx/throw}} (meta params)]
|
on-error rx/throw}} (meta params)
|
||||||
|
project-id (or project-id (:current-project-id state))]
|
||||||
(->> (rp/cmd! ::sse/clone-template {:project-id project-id
|
(->> (rp/cmd! ::sse/clone-template {:project-id project-id
|
||||||
:template-id template-id})
|
:template-id template-id})
|
||||||
(rx/tap (fn [event]
|
(rx/tap (fn [event]
|
||||||
|
|
|
@ -67,6 +67,7 @@
|
||||||
[app.main.data.workspace.shape-layout :as dwsl]
|
[app.main.data.workspace.shape-layout :as dwsl]
|
||||||
[app.main.data.workspace.shapes :as dwsh]
|
[app.main.data.workspace.shapes :as dwsh]
|
||||||
[app.main.data.workspace.state-helpers :as wsh]
|
[app.main.data.workspace.state-helpers :as wsh]
|
||||||
|
[app.main.data.workspace.texts :as dwtxt]
|
||||||
[app.main.data.workspace.thumbnails :as dwth]
|
[app.main.data.workspace.thumbnails :as dwth]
|
||||||
[app.main.data.workspace.transforms :as dwt]
|
[app.main.data.workspace.transforms :as dwt]
|
||||||
[app.main.data.workspace.undo :as dwu]
|
[app.main.data.workspace.undo :as dwu]
|
||||||
|
@ -85,6 +86,7 @@
|
||||||
[app.util.http :as http]
|
[app.util.http :as http]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[app.util.storage :as storage]
|
[app.util.storage :as storage]
|
||||||
|
[app.util.text.content :as tc]
|
||||||
[app.util.timers :as tm]
|
[app.util.timers :as tm]
|
||||||
[app.util.webapi :as wapi]
|
[app.util.webapi :as wapi]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
|
@ -1389,6 +1391,7 @@
|
||||||
(rx/ignore))))))))))
|
(rx/ignore))))))))))
|
||||||
|
|
||||||
(declare ^:private paste-transit)
|
(declare ^:private paste-transit)
|
||||||
|
(declare ^:private paste-html-text)
|
||||||
(declare ^:private paste-text)
|
(declare ^:private paste-text)
|
||||||
(declare ^:private paste-image)
|
(declare ^:private paste-image)
|
||||||
(declare ^:private paste-svg-text)
|
(declare ^:private paste-svg-text)
|
||||||
|
@ -1456,6 +1459,7 @@
|
||||||
(let [pdata (wapi/read-from-paste-event event)
|
(let [pdata (wapi/read-from-paste-event event)
|
||||||
image-data (some-> pdata wapi/extract-images)
|
image-data (some-> pdata wapi/extract-images)
|
||||||
text-data (some-> pdata wapi/extract-text)
|
text-data (some-> pdata wapi/extract-text)
|
||||||
|
html-data (some-> pdata wapi/extract-html-text)
|
||||||
transit-data (ex/ignoring (some-> text-data t/decode-str))]
|
transit-data (ex/ignoring (some-> text-data t/decode-str))]
|
||||||
(cond
|
(cond
|
||||||
(and (string? text-data) (re-find #"<svg\s" text-data))
|
(and (string? text-data) (re-find #"<svg\s" text-data))
|
||||||
|
@ -1468,6 +1472,9 @@
|
||||||
(coll? transit-data)
|
(coll? transit-data)
|
||||||
(rx/of (paste-transit (assoc transit-data :in-viewport in-viewport?)))
|
(rx/of (paste-transit (assoc transit-data :in-viewport in-viewport?)))
|
||||||
|
|
||||||
|
(string? html-data)
|
||||||
|
(rx/of (paste-html-text html-data text-data))
|
||||||
|
|
||||||
(string? text-data)
|
(string? text-data)
|
||||||
(rx/of (paste-text text-data))
|
(rx/of (paste-text text-data))
|
||||||
|
|
||||||
|
@ -1821,6 +1828,34 @@
|
||||||
:else
|
:else
|
||||||
(deref ms/mouse-position)))
|
(deref ms/mouse-position)))
|
||||||
|
|
||||||
|
(defn- paste-html-text
|
||||||
|
[html text]
|
||||||
|
(dm/assert! (string? html))
|
||||||
|
(ptk/reify ::paste-html-text
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [root (dwtxt/create-root-from-html html)
|
||||||
|
content (tc/dom->cljs root)
|
||||||
|
|
||||||
|
id (uuid/next)
|
||||||
|
width (max 8 (min (* 7 (count text)) 700))
|
||||||
|
height 16
|
||||||
|
{:keys [x y]} (calculate-paste-position state)
|
||||||
|
|
||||||
|
shape {:id id
|
||||||
|
:type :text
|
||||||
|
:name (txt/generate-shape-name text)
|
||||||
|
:x x
|
||||||
|
:y y
|
||||||
|
:width width
|
||||||
|
:height height
|
||||||
|
:grow-type (if (> (count text) 100) :auto-height :auto-width)
|
||||||
|
:content content}
|
||||||
|
undo-id (js/Symbol)]
|
||||||
|
(rx/of (dwu/start-undo-transaction undo-id)
|
||||||
|
(dwsh/create-and-add-shape :text x y shape)
|
||||||
|
(dwu/commit-undo-transaction undo-id))))))
|
||||||
|
|
||||||
(defn- paste-text
|
(defn- paste-text
|
||||||
[text]
|
[text]
|
||||||
(dm/assert! (string? text))
|
(dm/assert! (string? text))
|
||||||
|
|
|
@ -37,6 +37,8 @@
|
||||||
|
|
||||||
;; -- V2 Editor Helpers
|
;; -- V2 Editor Helpers
|
||||||
|
|
||||||
|
(def ^function create-root-from-string editor.v2/createRootFromString)
|
||||||
|
(def ^function create-root-from-html editor.v2/createRootFromHTML)
|
||||||
(def ^function create-editor editor.v2/create)
|
(def ^function create-editor editor.v2/create)
|
||||||
(def ^function set-editor-root! editor.v2/setRoot)
|
(def ^function set-editor-root! editor.v2/setRoot)
|
||||||
(def ^function get-editor-root editor.v2/getRoot)
|
(def ^function get-editor-root editor.v2/getRoot)
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
[app.main.data.exports.files :as fexp]
|
[app.main.data.exports.files :as fexp]
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.data.notifications :as ntf]
|
[app.main.data.notifications :as ntf]
|
||||||
[app.main.refs :as refs]
|
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
[app.main.router :as rt]
|
[app.main.router :as rt]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
|
@ -57,9 +56,8 @@
|
||||||
|
|
||||||
(mf/defc file-menu*
|
(mf/defc file-menu*
|
||||||
{::mf/props :obj}
|
{::mf/props :obj}
|
||||||
[{:keys [files show on-edit on-menu-close top left navigate origin parent-id can-edit]}]
|
[{:keys [files on-edit on-menu-close top left navigate origin parent-id can-edit]}]
|
||||||
(assert (seq files) "missing `files` prop")
|
(assert (seq files) "missing `files` prop")
|
||||||
(assert (boolean? show) "missing `show` prop")
|
|
||||||
(assert (fn? on-edit) "missing `on-edit` prop")
|
(assert (fn? on-edit) "missing `on-edit` prop")
|
||||||
(assert (fn? on-menu-close) "missing `on-menu-close` prop")
|
(assert (fn? on-menu-close) "missing `on-menu-close` prop")
|
||||||
(assert (boolean? navigate) "missing `navigate` prop")
|
(assert (boolean? navigate) "missing `navigate` prop")
|
||||||
|
@ -74,12 +72,11 @@
|
||||||
multi? (> file-count 1)
|
multi? (> file-count 1)
|
||||||
|
|
||||||
current-team-id (mf/use-ctx ctx/current-team-id)
|
current-team-id (mf/use-ctx ctx/current-team-id)
|
||||||
teams (mf/use-state nil)
|
teams* (mf/use-state nil)
|
||||||
default-team (-> (mf/deref refs/teams)
|
teams (deref teams*)
|
||||||
(get current-team-id))
|
|
||||||
|
|
||||||
current-team (or (get @teams current-team-id) default-team)
|
current-team (get teams current-team-id)
|
||||||
other-teams (remove #(= (:id %) current-team-id) (vals @teams))
|
other-teams (remove #(= (:id %) current-team-id) (vals teams))
|
||||||
current-projects (remove #(= (:id %) (:project-id file))
|
current-projects (remove #(= (:id %) (:project-id file))
|
||||||
(:projects current-team))
|
(:projects current-team))
|
||||||
|
|
||||||
|
@ -208,21 +205,13 @@
|
||||||
on-export-standard-files
|
on-export-standard-files
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-export-files)
|
(mf/deps on-export-files)
|
||||||
(partial on-export-files :legacy-zip))
|
(partial on-export-files :legacy-zip))]
|
||||||
|
|
||||||
;; NOTE: this is used for detect if component is still mounted
|
(mf/with-effect []
|
||||||
mounted-ref (mf/use-ref true)]
|
|
||||||
|
|
||||||
(mf/use-effect
|
|
||||||
(mf/deps show)
|
|
||||||
(fn []
|
|
||||||
(when show
|
|
||||||
(->> (rp/cmd! :get-all-projects)
|
(->> (rp/cmd! :get-all-projects)
|
||||||
(rx/map group-by-team)
|
(rx/map group-by-team)
|
||||||
(rx/subs! #(when (mf/ref-val mounted-ref)
|
(rx/subs! #(reset! teams* %))))
|
||||||
(reset! teams %)))))))
|
|
||||||
|
|
||||||
(when current-team
|
|
||||||
(let [sub-options
|
(let [sub-options
|
||||||
(concat
|
(concat
|
||||||
(for [project current-projects]
|
(for [project current-projects]
|
||||||
|
@ -340,10 +329,10 @@
|
||||||
|
|
||||||
[:> context-menu*
|
[:> context-menu*
|
||||||
{:on-close on-menu-close
|
{:on-close on-menu-close
|
||||||
:show show
|
|
||||||
:fixed (or (not= top 0) (not= left 0))
|
:fixed (or (not= top 0) (not= left 0))
|
||||||
|
:show true
|
||||||
:min-width true
|
:min-width true
|
||||||
:top top
|
:top top
|
||||||
:left left
|
:left left
|
||||||
:options options
|
:options options
|
||||||
:origin parent-id}]))))
|
:origin parent-id}])))
|
||||||
|
|
|
@ -409,7 +409,6 @@
|
||||||
;; so the menu can be handled
|
;; so the menu can be handled
|
||||||
[:div {:style {:pointer-events "all"}}
|
[:div {:style {:pointer-events "all"}}
|
||||||
[:> file-menu* {:files (vals selected-files)
|
[:> file-menu* {:files (vals selected-files)
|
||||||
:show (:menu-open dashboard-local)
|
|
||||||
:left (+ 24 (:x (:menu-pos dashboard-local)))
|
:left (+ 24 (:x (:menu-pos dashboard-local)))
|
||||||
:top (:y (:menu-pos dashboard-local))
|
:top (:y (:menu-pos dashboard-local))
|
||||||
:can-edit can-edit
|
:can-edit can-edit
|
||||||
|
|
|
@ -15,9 +15,11 @@
|
||||||
[app.common.types.typographies-list :as ctyl]
|
[app.common.types.typographies-list :as ctyl]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
|
[app.main.data.dashboard :as dd]
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.data.profile :as du]
|
[app.main.data.profile :as du]
|
||||||
[app.main.data.team :as dtm]
|
[app.main.data.team :as dtm]
|
||||||
|
[app.main.data.notifications :as ntf]
|
||||||
[app.main.data.workspace.colors :as mdc]
|
[app.main.data.workspace.colors :as mdc]
|
||||||
[app.main.data.workspace.libraries :as dwl]
|
[app.main.data.workspace.libraries :as dwl]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
|
@ -34,6 +36,7 @@
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[app.util.strings :refer [matches-search]]
|
[app.util.strings :refer [matches-search]]
|
||||||
|
[beicon.v2.core :as rx]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[okulary.core :as l]
|
[okulary.core :as l]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
@ -85,8 +88,10 @@
|
||||||
(conj (tr "workspace.libraries.typography" typography-count))))
|
(conj (tr "workspace.libraries.typography" typography-count))))
|
||||||
"\u00A0")))
|
"\u00A0")))
|
||||||
|
|
||||||
(mf/defc describe-library-blocks
|
(mf/defc describe-library-blocks*
|
||||||
[{:keys [components-count graphics-count colors-count typography-count] :as props}]
|
{::mf/props :obj
|
||||||
|
::mf/private true}
|
||||||
|
[{:keys [components-count graphics-count colors-count typography-count]}]
|
||||||
[:*
|
[:*
|
||||||
(when (pos? components-count)
|
(when (pos? components-count)
|
||||||
[:li {:class (stl/css :element-count)}
|
[:li {:class (stl/css :element-count)}
|
||||||
|
@ -104,10 +109,47 @@
|
||||||
[:li {:class (stl/css :element-count)}
|
[:li {:class (stl/css :element-count)}
|
||||||
(tr "workspace.libraries.typography" typography-count)])])
|
(tr "workspace.libraries.typography" typography-count)])])
|
||||||
|
|
||||||
|
(mf/defc sample-library-entry*
|
||||||
|
{::mf/props :obj
|
||||||
|
::mf/private true}
|
||||||
|
[{:keys [library importing]}]
|
||||||
|
(let [id (:id library)
|
||||||
|
importing? (deref importing)
|
||||||
|
|
||||||
|
on-error
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [_]
|
||||||
|
(reset! importing nil)
|
||||||
|
(rx/of (ntf/error (tr "dashboard.libraries-and-templates.import-error")))))
|
||||||
|
|
||||||
|
on-success
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [_]
|
||||||
|
(st/emit! (dtm/fetch-shared-files))))
|
||||||
|
|
||||||
|
import-library
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [_]
|
||||||
|
(reset! importing id)
|
||||||
|
(st/emit! (dd/clone-template
|
||||||
|
(with-meta {:template-id id}
|
||||||
|
{:on-success on-success
|
||||||
|
:on-error on-error})))))]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :sample-library-item)
|
||||||
|
:key (dm/str id)}
|
||||||
|
[:div {:class (stl/css :sample-library-item-name)} (:name library)]
|
||||||
|
[:input {:class (stl/css-case :sample-library-button true
|
||||||
|
:sample-library-add (nil? importing?)
|
||||||
|
:sample-library-adding (some? importing?))
|
||||||
|
:type "button"
|
||||||
|
:value (if (= importing? id) (tr "labels.adding") (tr "labels.add"))
|
||||||
|
:on-click import-library}]]))
|
||||||
|
|
||||||
(mf/defc libraries-tab*
|
(mf/defc libraries-tab*
|
||||||
{::mf/props :obj
|
{::mf/props :obj
|
||||||
::mf/private true}
|
::mf/private true}
|
||||||
[{:keys [file-id is-shared linked-libraries shared-libraries]}]
|
[{:keys [file-id team-id is-shared linked-libraries shared-libraries]}]
|
||||||
(let [search-term* (mf/use-state "")
|
(let [search-term* (mf/use-state "")
|
||||||
search-term (deref search-term*)
|
search-term (deref search-term*)
|
||||||
library-ref (mf/with-memo [file-id]
|
library-ref (mf/with-memo [file-id]
|
||||||
|
@ -139,6 +181,11 @@
|
||||||
(->> (vals linked-libraries)
|
(->> (vals linked-libraries)
|
||||||
(sort-by (comp str/lower :name))))
|
(sort-by (comp str/lower :name))))
|
||||||
|
|
||||||
|
importing* (mf/use-state nil)
|
||||||
|
sample-libraries [{:id "penpot-design-system", :name "Design system example"}
|
||||||
|
{:id "wireframing-kit", :name "Wireframe library"}
|
||||||
|
{:id "whiteboarding-kit", :name "Whiteboarding Kit"}]
|
||||||
|
|
||||||
change-search-term
|
change-search-term
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [event]
|
(fn [event]
|
||||||
|
@ -216,7 +263,7 @@
|
||||||
[:div {:class (stl/css :item-content)}
|
[:div {:class (stl/css :item-content)}
|
||||||
[:div {:class (stl/css :item-name)} (tr "workspace.libraries.file-library")]
|
[:div {:class (stl/css :item-name)} (tr "workspace.libraries.file-library")]
|
||||||
[:ul {:class (stl/css :item-contents)}
|
[:ul {:class (stl/css :item-contents)}
|
||||||
[:& describe-library-blocks {:components-count (count components)
|
[:> describe-library-blocks* {:components-count (count components)
|
||||||
:graphics-count (count media)
|
:graphics-count (count media)
|
||||||
:colors-count (count colors)
|
:colors-count (count colors)
|
||||||
:typography-count (count typographies)}]]]
|
:typography-count (count typographies)}]]]
|
||||||
|
@ -241,7 +288,7 @@
|
||||||
graphics-count (count (dm/get-in library [:data :media] []))
|
graphics-count (count (dm/get-in library [:data :media] []))
|
||||||
colors-count (count (dm/get-in library [:data :colors] []))
|
colors-count (count (dm/get-in library [:data :colors] []))
|
||||||
typography-count (count (dm/get-in library [:data :typographies] []))]
|
typography-count (count (dm/get-in library [:data :typographies] []))]
|
||||||
[:& describe-library-blocks {:components-count components-count
|
[:> describe-library-blocks* {:components-count components-count
|
||||||
:graphics-count graphics-count
|
:graphics-count graphics-count
|
||||||
:colors-count colors-count
|
:colors-count colors-count
|
||||||
:typography-count typography-count}])]]
|
:typography-count typography-count}])]]
|
||||||
|
@ -275,7 +322,7 @@
|
||||||
graphics-count (dm/get-in library [:library-summary :media :count] 0)
|
graphics-count (dm/get-in library [:library-summary :media :count] 0)
|
||||||
colors-count (dm/get-in library [:library-summary :colors :count] 0)
|
colors-count (dm/get-in library [:library-summary :colors :count] 0)
|
||||||
typography-count (dm/get-in library [:library-summary :typographies :count] 0)]
|
typography-count (dm/get-in library [:library-summary :typographies :count] 0)]
|
||||||
[:& describe-library-blocks {:components-count components-count
|
[:> describe-library-blocks* {:components-count components-count
|
||||||
:graphics-count graphics-count
|
:graphics-count graphics-count
|
||||||
:colors-count colors-count
|
:colors-count colors-count
|
||||||
:typography-count typography-count}])]]
|
:typography-count typography-count}])]]
|
||||||
|
@ -291,6 +338,21 @@
|
||||||
(nil? shared-libraries)
|
(nil? shared-libraries)
|
||||||
(tr "workspace.libraries.loading")
|
(tr "workspace.libraries.loading")
|
||||||
|
|
||||||
|
(and (str/empty? search-term) (cf/external-feature-flag "templates-03" "test"))
|
||||||
|
[:*
|
||||||
|
[:div {:class (stl/css :sample-libraries-info)}
|
||||||
|
(tr "workspace.libraries.empty.no-libraries")
|
||||||
|
[:a {:target "_blank"
|
||||||
|
:class (stl/css :sample-libraries-link)
|
||||||
|
:href "https://penpot.app/libraries-templates"}
|
||||||
|
(tr "workspace.libraries.empty.some-templates")]]
|
||||||
|
[:div {:class (stl/css :sample-libraries-container)}
|
||||||
|
(tr "workspace.libraries.empty.add-some")
|
||||||
|
(for [library sample-libraries]
|
||||||
|
[:> sample-library-entry*
|
||||||
|
{:library library
|
||||||
|
:importing importing*}])]]
|
||||||
|
|
||||||
(str/empty? search-term)
|
(str/empty? search-term)
|
||||||
[:*
|
[:*
|
||||||
[:span {:class (stl/css :empty-state-icon)}
|
[:span {:class (stl/css :empty-state-icon)}
|
||||||
|
|
|
@ -126,6 +126,7 @@
|
||||||
@include flexCenter;
|
@include flexCenter;
|
||||||
width: $s-20;
|
width: $s-20;
|
||||||
padding: 0 0 0 $s-8;
|
padding: 0 0 0 $s-8;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
@extend .button-icon-small;
|
@extend .button-icon-small;
|
||||||
stroke: var(--icon-foreground);
|
stroke: var(--icon-foreground);
|
||||||
|
@ -231,6 +232,7 @@
|
||||||
padding: $s-8 $s-24;
|
padding: $s-8 $s-24;
|
||||||
margin-inline-end: $s-2;
|
margin-inline-end: $s-2;
|
||||||
border-radius: $br-8;
|
border-radius: $br-8;
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
@extend .button-disabled;
|
@extend .button-disabled;
|
||||||
}
|
}
|
||||||
|
@ -333,3 +335,62 @@
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
font-weight: $fw400;
|
font-weight: $fw400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sample-libraries-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: $fs-12;
|
||||||
|
margin: $s-32;
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-libraries-link {
|
||||||
|
color: var(--color-accent-primary);
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: $fw400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-libraries-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: $fs-12;
|
||||||
|
width: 100%;
|
||||||
|
align-items: start;
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-library-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: $s-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-library-item-name {
|
||||||
|
font-size: $fs-14;
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: $s-232;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-library-add {
|
||||||
|
@extend .button-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-library-adding {
|
||||||
|
@extend .button-disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-library-button {
|
||||||
|
@include uppercaseTitleTipography;
|
||||||
|
height: $s-32;
|
||||||
|
width: $s-80;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: $br-8;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
|
@ -145,6 +145,10 @@
|
||||||
(not= (.-tagName ^js target) "INPUT")) ;; an editable control
|
(not= (.-tagName ^js target) "INPUT")) ;; an editable control
|
||||||
(.. ^js event getBrowserEvent -clipboardData))))
|
(.. ^js event getBrowserEvent -clipboardData))))
|
||||||
|
|
||||||
|
(defn extract-html-text
|
||||||
|
[clipboard-data]
|
||||||
|
(.getData clipboard-data "text/html"))
|
||||||
|
|
||||||
(defn extract-text
|
(defn extract-text
|
||||||
[clipboard-data]
|
[clipboard-data]
|
||||||
(.getData clipboard-data "text"))
|
(.getData clipboard-data "text"))
|
||||||
|
|
|
@ -12,6 +12,7 @@ import ChangeController from "./controllers/ChangeController.js";
|
||||||
import SelectionController from "./controllers/SelectionController.js";
|
import SelectionController from "./controllers/SelectionController.js";
|
||||||
import { createSelectionImposterFromClientRects } from "./selection/Imposter.js";
|
import { createSelectionImposterFromClientRects } from "./selection/Imposter.js";
|
||||||
import { addEventListeners, removeEventListeners } from "./Event.js";
|
import { addEventListeners, removeEventListeners } from "./Event.js";
|
||||||
|
import { mapContentFragmentFromHTML, mapContentFragmentFromString } from "./content/dom/Content.js";
|
||||||
import { createRoot, createEmptyRoot } from "./content/dom/Root.js";
|
import { createRoot, createEmptyRoot } from "./content/dom/Root.js";
|
||||||
import { createParagraph } from "./content/dom/Paragraph.js";
|
import { createParagraph } from "./content/dom/Paragraph.js";
|
||||||
import { createEmptyInline, createInline } from "./content/dom/Inline.js";
|
import { createEmptyInline, createInline } from "./content/dom/Inline.js";
|
||||||
|
@ -501,6 +502,20 @@ export class TextEditor extends EventTarget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createRootFromHTML(html) {
|
||||||
|
const fragment = mapContentFragmentFromHTML(html);
|
||||||
|
const root = createRoot([]);
|
||||||
|
root.replaceChildren(fragment);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRootFromString(string) {
|
||||||
|
const fragment = mapContentFragmentFromString(string);
|
||||||
|
const root = createRoot([]);
|
||||||
|
root.replaceChild(fragment);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
export function isEditor(instance) {
|
export function isEditor(instance) {
|
||||||
return (instance instanceof TextEditor);
|
return (instance instanceof TextEditor);
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,17 @@ function getInertElement() {
|
||||||
return inertElement;
|
return inertElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a default declaration.
|
||||||
|
*
|
||||||
|
* @returns {CSSStyleDeclaration}
|
||||||
|
*/
|
||||||
|
function getStyleDefaultsDeclaration() {
|
||||||
|
const element = getInertElement();
|
||||||
|
resetInertElement();
|
||||||
|
return element.style;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes the styles of an element the same way `window.getComputedStyle` does.
|
* Computes the styles of an element the same way `window.getComputedStyle` does.
|
||||||
*
|
*
|
||||||
|
@ -115,22 +126,26 @@ export function getComputedStyle(element) {
|
||||||
* CSS properties like `font-family` or some CSS variables.
|
* CSS properties like `font-family` or some CSS variables.
|
||||||
*
|
*
|
||||||
* @param {Node} node
|
* @param {Node} node
|
||||||
* @param {CSSStyleDeclaration} styleDefaults
|
* @param {CSSStyleDeclaration} [styleDefaults]
|
||||||
* @returns {CSSStyleDeclaration}
|
* @returns {CSSStyleDeclaration}
|
||||||
*/
|
*/
|
||||||
export function normalizeStyles(node, styleDefaults) {
|
export function normalizeStyles(node, styleDefaults = getStyleDefaultsDeclaration()) {
|
||||||
const styleDeclaration = mergeStyleDeclarations(
|
const styleDeclaration = mergeStyleDeclarations(
|
||||||
styleDefaults,
|
styleDefaults,
|
||||||
getComputedStyle(node.parentElement)
|
getComputedStyle(node.parentElement)
|
||||||
);
|
);
|
||||||
|
|
||||||
// If there's a color property, we should convert it to
|
// If there's a color property, we should convert it to
|
||||||
// a --fills CSS variable property.
|
// a --fills CSS variable property.
|
||||||
const fills = styleDeclaration.getPropertyValue("--fills");
|
const fills = styleDeclaration.getPropertyValue("--fills");
|
||||||
const color = styleDeclaration.getPropertyValue("color");
|
const color = styleDeclaration.getPropertyValue("color");
|
||||||
if (color && !fills) {
|
if (color) {
|
||||||
styleDeclaration.removeProperty("color");
|
styleDeclaration.removeProperty("color");
|
||||||
styleDeclaration.setProperty("--fills", getFills(color));
|
styleDeclaration.setProperty("--fills", getFills(color));
|
||||||
|
} else {
|
||||||
|
styleDeclaration.setProperty("--fills", fills);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's a font-family property and not a --font-id, then
|
// If there's a font-family property and not a --font-id, then
|
||||||
// we remove the font-family because it will not work.
|
// we remove the font-family because it will not work.
|
||||||
const fontFamily = styleDeclaration.getPropertyValue("font-family");
|
const fontFamily = styleDeclaration.getPropertyValue("font-family");
|
||||||
|
@ -145,8 +160,15 @@ export function normalizeStyles(node, styleDefaults) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineHeight = styleDeclaration.getPropertyValue("line-height");
|
const lineHeight = styleDeclaration.getPropertyValue("line-height");
|
||||||
if (!lineHeight || lineHeight === "") {
|
if (!lineHeight || lineHeight === "" || !lineHeight.endsWith("px")) {
|
||||||
|
// TODO: Podríamos convertir unidades en decimales.
|
||||||
styleDeclaration.setProperty("line-height", DEFAULT_LINE_HEIGHT);
|
styleDeclaration.setProperty("line-height", DEFAULT_LINE_HEIGHT);
|
||||||
|
} else if (lineHeight.endsWith("px")) {
|
||||||
|
const fontSize = styleDeclaration.getPropertyValue("font-size");
|
||||||
|
styleDeclaration.setProperty(
|
||||||
|
"line-height",
|
||||||
|
parseFloat(lineHeight) / parseFloat(fontSize),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return styleDeclaration
|
return styleDeclaration
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { expect, describe, test } from "vitest";
|
import { expect, describe, test } from "vitest";
|
||||||
import TextEditor from "../TextEditor.js";
|
|
||||||
import {
|
import {
|
||||||
createEmptyParagraph,
|
createEmptyParagraph,
|
||||||
createParagraph,
|
createParagraph,
|
||||||
|
|
|
@ -1565,6 +1565,9 @@ msgstr "Active"
|
||||||
msgid "labels.add"
|
msgid "labels.add"
|
||||||
msgstr "Add"
|
msgstr "Add"
|
||||||
|
|
||||||
|
msgid "labels.adding"
|
||||||
|
msgstr "Adding..."
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/fonts.cljs:176
|
#: src/app/main/ui/dashboard/fonts.cljs:176
|
||||||
msgid "labels.add-custom-font"
|
msgid "labels.add-custom-font"
|
||||||
msgstr "Add custom font"
|
msgstr "Add custom font"
|
||||||
|
@ -4594,6 +4597,15 @@ msgstr "see all changes"
|
||||||
msgid "workspace.libraries.updates"
|
msgid "workspace.libraries.updates"
|
||||||
msgstr "UPDATES"
|
msgstr "UPDATES"
|
||||||
|
|
||||||
|
msgid "workspace.libraries.empty.no-libraries"
|
||||||
|
msgstr "There are no Shared Libraries at you team, you can look for"
|
||||||
|
|
||||||
|
msgid "workspace.libraries.empty.some-templates"
|
||||||
|
msgstr "some templates in here"
|
||||||
|
|
||||||
|
msgid "workspace.libraries.empty.add-some"
|
||||||
|
msgstr "Or add some of these to try:"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:745
|
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:745
|
||||||
msgid "workspace.options.add-interaction"
|
msgid "workspace.options.add-interaction"
|
||||||
msgstr "Click the + button to add interactions."
|
msgstr "Click the + button to add interactions."
|
||||||
|
|
|
@ -1571,6 +1571,9 @@ msgstr "Activo"
|
||||||
msgid "labels.add"
|
msgid "labels.add"
|
||||||
msgstr "Añadir"
|
msgstr "Añadir"
|
||||||
|
|
||||||
|
msgid "labels.adding"
|
||||||
|
msgstr "Añadiendo..."
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/fonts.cljs:176
|
#: src/app/main/ui/dashboard/fonts.cljs:176
|
||||||
msgid "labels.add-custom-font"
|
msgid "labels.add-custom-font"
|
||||||
msgstr "Añadir fuente personalizada"
|
msgstr "Añadir fuente personalizada"
|
||||||
|
@ -4595,6 +4598,15 @@ msgstr "ver todos los cambios"
|
||||||
msgid "workspace.libraries.updates"
|
msgid "workspace.libraries.updates"
|
||||||
msgstr "ACTUALIZACIONES"
|
msgstr "ACTUALIZACIONES"
|
||||||
|
|
||||||
|
msgid "workspace.libraries.empty.no-libraries"
|
||||||
|
msgstr "No hay Biblioteacas Compartidas en tu equipo, puedes buscar"
|
||||||
|
|
||||||
|
msgid "workspace.libraries.empty.some-templates"
|
||||||
|
msgstr "algunas plantillas aquí"
|
||||||
|
|
||||||
|
msgid "workspace.libraries.empty.add-some"
|
||||||
|
msgstr "O añadir algunas de éstas para probar:"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:745
|
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:745
|
||||||
msgid "workspace.options.add-interaction"
|
msgid "workspace.options.add-interaction"
|
||||||
msgstr "Pulsa el botón + para añadir interacciones."
|
msgstr "Pulsa el botón + para añadir interacciones."
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue