Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2024-12-10 10:46:38 +01:00
commit bdb777516e
17 changed files with 456 additions and 175 deletions

View file

@ -552,18 +552,17 @@
(defn clone-template
[{:keys [template-id project-id] :as params}]
(dm/assert! (uuid? project-id))
(ptk/reify ::clone-template
ev/Event
(-data [_]
{:template-id template-id
:project-id project-id})
{:template-id template-id})
ptk/WatchEvent
(watch [_ _ _]
(watch [_ state _]
(let [{:keys [on-success on-error]
: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
:template-id template-id})
(rx/tap (fn [event]

View file

@ -67,6 +67,7 @@
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh]
[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.transforms :as dwt]
[app.main.data.workspace.undo :as dwu]
@ -85,6 +86,7 @@
[app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]]
[app.util.storage :as storage]
[app.util.text.content :as tc]
[app.util.timers :as tm]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
@ -1389,6 +1391,7 @@
(rx/ignore))))))))))
(declare ^:private paste-transit)
(declare ^:private paste-html-text)
(declare ^:private paste-text)
(declare ^:private paste-image)
(declare ^:private paste-svg-text)
@ -1456,6 +1459,7 @@
(let [pdata (wapi/read-from-paste-event event)
image-data (some-> pdata wapi/extract-images)
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))]
(cond
(and (string? text-data) (re-find #"<svg\s" text-data))
@ -1468,6 +1472,9 @@
(coll? transit-data)
(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)
(rx/of (paste-text text-data))
@ -1821,6 +1828,34 @@
:else
(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
[text]
(dm/assert! (string? text))

View file

@ -37,6 +37,8 @@
;; -- 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 set-editor-root! editor.v2/setRoot)
(def ^function get-editor-root editor.v2/getRoot)

View file

@ -13,7 +13,6 @@
[app.main.data.exports.files :as fexp]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
@ -57,9 +56,8 @@
(mf/defc file-menu*
{::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 (boolean? show) "missing `show` prop")
(assert (fn? on-edit) "missing `on-edit` prop")
(assert (fn? on-menu-close) "missing `on-menu-close` prop")
(assert (boolean? navigate) "missing `navigate` prop")
@ -74,12 +72,11 @@
multi? (> file-count 1)
current-team-id (mf/use-ctx ctx/current-team-id)
teams (mf/use-state nil)
default-team (-> (mf/deref refs/teams)
(get current-team-id))
teams* (mf/use-state nil)
teams (deref teams*)
current-team (or (get @teams current-team-id) default-team)
other-teams (remove #(= (:id %) current-team-id) (vals @teams))
current-team (get teams current-team-id)
other-teams (remove #(= (:id %) current-team-id) (vals teams))
current-projects (remove #(= (:id %) (:project-id file))
(:projects current-team))
@ -208,142 +205,134 @@
on-export-standard-files
(mf/use-fn
(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
mounted-ref (mf/use-ref true)]
(mf/with-effect []
(->> (rp/cmd! :get-all-projects)
(rx/map group-by-team)
(rx/subs! #(reset! teams* %))))
(mf/use-effect
(mf/deps show)
(fn []
(when show
(->> (rp/cmd! :get-all-projects)
(rx/map group-by-team)
(rx/subs! #(when (mf/ref-val mounted-ref)
(reset! teams %)))))))
(let [sub-options
(concat
(for [project current-projects]
{:name (get-project-name project)
:id (get-project-id project)
:handler (on-move (:id current-team)
(:id project))})
(when (seq other-teams)
[{:name (tr "dashboard.move-to-other-team")
:id "move-to-other-team"
:options
(for [team other-teams]
{:name (get-team-name team)
:id (get-project-id team)
:options
(for [sub-project (:projects team)]
{:name (get-project-name sub-project)
:id (get-project-id sub-project)
:handler (on-move (:id team)
(:id sub-project))})})}]))
(when current-team
(let [sub-options
(concat
(for [project current-projects]
{:name (get-project-name project)
:id (get-project-id project)
:handler (on-move (:id current-team)
(:id project))})
(when (seq other-teams)
[{:name (tr "dashboard.move-to-other-team")
:id "move-to-other-team"
:options
(for [team other-teams]
{:name (get-team-name team)
:id (get-project-id team)
:options
(for [sub-project (:projects team)]
{:name (get-project-name sub-project)
:id (get-project-id sub-project)
:handler (on-move (:id team)
(:id sub-project))})})}]))
options
(if multi?
[(when can-edit
{:name (tr "dashboard.duplicate-multi" file-count)
:id "duplicate-multi"
:handler on-duplicate})
options
(if multi?
[(when can-edit
{:name (tr "dashboard.duplicate-multi" file-count)
:id "duplicate-multi"
:handler on-duplicate})
(when (and (or (seq current-projects) (seq other-teams)) can-edit)
{:name (tr "dashboard.move-to-multi" file-count)
:id "file-move-multi"
:options sub-options})
(when (and (or (seq current-projects) (seq other-teams)) can-edit)
{:name (tr "dashboard.move-to-multi" file-count)
:id "file-move-multi"
:options sub-options})
(when-not (contains? cf/flags :export-file-v3)
{:name (tr "dashboard.export-binary-multi" file-count)
:id "file-binary-export-multi"
:handler on-export-binary-files})
(when-not (contains? cf/flags :export-file-v3)
{:name (tr "dashboard.export-binary-multi" file-count)
:id "file-binary-export-multi"
:handler on-export-binary-files})
(when (contains? cf/flags :export-file-v3)
{:name (tr "dashboard.export-binary-multi" file-count)
:id "file-binary-export-multi"
:handler on-export-binary-files-v3})
(when (contains? cf/flags :export-file-v3)
{:name (tr "dashboard.export-binary-multi" file-count)
:id "file-binary-export-multi"
:handler on-export-binary-files-v3})
(when-not (contains? cf/flags :export-file-v3)
{:name (tr "dashboard.export-standard-multi" file-count)
:id "file-standard-export-multi"
:handler on-export-standard-files})
(when-not (contains? cf/flags :export-file-v3)
{:name (tr "dashboard.export-standard-multi" file-count)
:id "file-standard-export-multi"
:handler on-export-standard-files})
(when (and (:is-shared file) can-edit)
{:name (tr "labels.unpublish-multi-files" file-count)
:id "file-unpublish-multi"
:handler on-del-shared})
(when (and (:is-shared file) can-edit)
{:name (tr "labels.unpublish-multi-files" file-count)
:id "file-unpublish-multi"
:handler on-del-shared})
(when (and (not is-lib-page?) can-edit)
{:name :separator}
{:name (tr "labels.delete-multi-files" file-count)
:id "file-delete-multi"
:handler on-delete})]
(when (and (not is-lib-page?) can-edit)
{:name :separator}
{:name (tr "labels.delete-multi-files" file-count)
:id "file-delete-multi"
:handler on-delete})]
[{:name (tr "dashboard.open-in-new-tab")
:id "file-open-new-tab"
:handler on-new-tab}
(when (and (not is-search-page?) can-edit)
{:name (tr "labels.rename")
:id "file-rename"
:handler on-edit})
[{:name (tr "dashboard.open-in-new-tab")
:id "file-open-new-tab"
:handler on-new-tab}
(when (and (not is-search-page?) can-edit)
{:name (tr "labels.rename")
:id "file-rename"
:handler on-edit})
(when (and (not is-search-page?) can-edit)
{:name (tr "dashboard.duplicate")
:id "file-duplicate"
:handler on-duplicate})
(when (and (not is-search-page?) can-edit)
{:name (tr "dashboard.duplicate")
:id "file-duplicate"
:handler on-duplicate})
(when (and (not is-lib-page?)
(not is-search-page?)
(or (seq current-projects) (seq other-teams))
can-edit)
{:name (tr "dashboard.move-to")
:id "file-move-to"
:options sub-options})
(when (and (not is-lib-page?)
(not is-search-page?)
(or (seq current-projects) (seq other-teams))
can-edit)
{:name (tr "dashboard.move-to")
:id "file-move-to"
:options sub-options})
(when (and (not is-search-page?)
can-edit)
(if (:is-shared file)
{:name (tr "dashboard.unpublish-shared")
:id "file-del-shared"
:handler on-del-shared}
{:name (tr "dashboard.add-shared")
:id "file-add-shared"
:handler on-add-shared}))
(when (and (not is-search-page?)
can-edit)
(if (:is-shared file)
{:name (tr "dashboard.unpublish-shared")
:id "file-del-shared"
:handler on-del-shared}
{:name (tr "dashboard.add-shared")
:id "file-add-shared"
:handler on-add-shared}))
{:name :separator}
{:name :separator}
(when-not (contains? cf/flags :export-file-v3)
{:name (tr "dashboard.download-binary-file")
:id "download-binary-file"
:handler on-export-binary-files})
(when-not (contains? cf/flags :export-file-v3)
{:name (tr "dashboard.download-binary-file")
:id "download-binary-file"
:handler on-export-binary-files})
(when (contains? cf/flags :export-file-v3)
{:name (tr "dashboard.download-binary-file")
:id "download-binary-file"
:handler on-export-binary-files-v3})
(when (contains? cf/flags :export-file-v3)
{:name (tr "dashboard.download-binary-file")
:id "download-binary-file"
:handler on-export-binary-files-v3})
(when-not (contains? cf/flags :export-file-v3)
{:name (tr "dashboard.download-standard-file")
:id "download-standard-file"
:handler on-export-standard-files})
(when-not (contains? cf/flags :export-file-v3)
{:name (tr "dashboard.download-standard-file")
:id "download-standard-file"
:handler on-export-standard-files})
(when (and (not is-lib-page?) (not is-search-page?) can-edit)
{:name :separator})
(when (and (not is-lib-page?) (not is-search-page?) can-edit)
{:name :separator})
(when (and (not is-lib-page?) (not is-search-page?) can-edit)
{:name (tr "labels.delete")
:id "file-delete"
:handler on-delete})])]
(when (and (not is-lib-page?) (not is-search-page?) can-edit)
{:name (tr "labels.delete")
:id "file-delete"
:handler on-delete})])]
[:> context-menu*
{:on-close on-menu-close
:show show
:fixed (or (not= top 0) (not= left 0))
:min-width true
:top top
:left left
:options options
:origin parent-id}]))))
[:> context-menu*
{:on-close on-menu-close
:fixed (or (not= top 0) (not= left 0))
:show true
:min-width true
:top top
:left left
:options options
:origin parent-id}])))

View file

@ -409,7 +409,6 @@
;; so the menu can be handled
[:div {:style {:pointer-events "all"}}
[:> file-menu* {:files (vals selected-files)
:show (:menu-open dashboard-local)
:left (+ 24 (:x (:menu-pos dashboard-local)))
:top (:y (:menu-pos dashboard-local))
:can-edit can-edit

View file

@ -15,9 +15,11 @@
[app.common.types.typographies-list :as ctyl]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.dashboard :as dd]
[app.main.data.modal :as modal]
[app.main.data.profile :as du]
[app.main.data.team :as dtm]
[app.main.data.notifications :as ntf]
[app.main.data.workspace.colors :as mdc]
[app.main.data.workspace.libraries :as dwl]
[app.main.refs :as refs]
@ -34,6 +36,7 @@
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.strings :refer [matches-search]]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.v2 :as mf]))
@ -85,8 +88,10 @@
(conj (tr "workspace.libraries.typography" typography-count))))
"\u00A0")))
(mf/defc describe-library-blocks
[{:keys [components-count graphics-count colors-count typography-count] :as props}]
(mf/defc describe-library-blocks*
{::mf/props :obj
::mf/private true}
[{:keys [components-count graphics-count colors-count typography-count]}]
[:*
(when (pos? components-count)
[:li {:class (stl/css :element-count)}
@ -104,10 +109,47 @@
[:li {:class (stl/css :element-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/props :obj
::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 "")
search-term (deref search-term*)
library-ref (mf/with-memo [file-id]
@ -139,6 +181,11 @@
(->> (vals linked-libraries)
(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
(mf/use-fn
(fn [event]
@ -216,10 +263,10 @@
[:div {:class (stl/css :item-content)}
[:div {:class (stl/css :item-name)} (tr "workspace.libraries.file-library")]
[:ul {:class (stl/css :item-contents)}
[:& describe-library-blocks {:components-count (count components)
:graphics-count (count media)
:colors-count (count colors)
:typography-count (count typographies)}]]]
[:> describe-library-blocks* {:components-count (count components)
:graphics-count (count media)
:colors-count (count colors)
:typography-count (count typographies)}]]]
(if ^boolean is-shared
[:input {:class (stl/css :item-unpublish)
:type "button"
@ -241,10 +288,10 @@
graphics-count (count (dm/get-in library [:data :media] []))
colors-count (count (dm/get-in library [:data :colors] []))
typography-count (count (dm/get-in library [:data :typographies] []))]
[:& describe-library-blocks {:components-count components-count
:graphics-count graphics-count
:colors-count colors-count
:typography-count typography-count}])]]
[:> describe-library-blocks* {:components-count components-count
:graphics-count graphics-count
:colors-count colors-count
:typography-count typography-count}])]]
[:button {:class (stl/css :item-button)
:type "button"
@ -275,10 +322,10 @@
graphics-count (dm/get-in library [:library-summary :media :count] 0)
colors-count (dm/get-in library [:library-summary :colors :count] 0)
typography-count (dm/get-in library [:library-summary :typographies :count] 0)]
[:& describe-library-blocks {:components-count components-count
:graphics-count graphics-count
:colors-count colors-count
:typography-count typography-count}])]]
[:> describe-library-blocks* {:components-count components-count
:graphics-count graphics-count
:colors-count colors-count
:typography-count typography-count}])]]
[:button {:class (stl/css :item-button-shared)
:data-library-id (dm/str id)
:title (tr "workspace.libraries.shared-library-btn")
@ -291,6 +338,21 @@
(nil? shared-libraries)
(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)
[:*
[:span {:class (stl/css :empty-state-icon)}

View file

@ -126,6 +126,7 @@
@include flexCenter;
width: $s-20;
padding: 0 0 0 $s-8;
svg {
@extend .button-icon-small;
stroke: var(--icon-foreground);
@ -231,6 +232,7 @@
padding: $s-8 $s-24;
margin-inline-end: $s-2;
border-radius: $br-8;
&:disabled {
@extend .button-disabled;
}
@ -333,3 +335,62 @@
text-decoration: underline;
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;
}

View file

@ -145,6 +145,10 @@
(not= (.-tagName ^js target) "INPUT")) ;; an editable control
(.. ^js event getBrowserEvent -clipboardData))))
(defn extract-html-text
[clipboard-data]
(.getData clipboard-data "text/html"))
(defn extract-text
[clipboard-data]
(.getData clipboard-data "text"))