diff --git a/backend/src/app/rpc/commands/media.clj b/backend/src/app/rpc/commands/media.clj index 69265c27f..f4913edb2 100644 --- a/backend/src/app/rpc/commands/media.clj +++ b/backend/src/app/rpc/commands/media.clj @@ -60,15 +60,25 @@ (media/validate-media-type! content) (media/validate-media-size! content) - (db/run! cfg (fn [cfg] - (let [object (create-file-media-object cfg params) - props {:name (:name params) - :file-id file-id - :is-local (:is-local params) - :size (:size content) - :mtype (:mtype content)}] - (with-meta object - {::audit/replace-props props}))))) + (db/run! cfg (fn [{:keys [::db/conn] :as cfg}] + ;; We get the minimal file for proper checking if + ;; 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 + :is-local (:is-local params) + :size (:size content) + :mtype (:mtype content)}}))))) (defn- big-enough-for-thumbnail? "Checks if the provided image info is big enough for @@ -142,20 +152,14 @@ :always (assoc ::image (process-main-image info))))) -(defn create-file-media-object - [{:keys [::sto/storage ::db/conn ::wrk/executor]} +(defn- create-file-media-object + [{:keys [::sto/storage ::db/conn ::wrk/executor] :as cfg} {:keys [id file-id is-local name content]}] - (let [result (px/invoke! executor (partial process-image content)) image (sto/put-object! storage (::image result)) thumb (when-let [params (::thumb result)] (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 (or id (uuid/next)) file-id is-local name @@ -182,7 +186,18 @@ ::sm/params schema:create-file-media-object-from-url} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (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 [{:keys [::http/client]} uri] diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index 57034c461..7c7ca3339 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -422,7 +422,9 @@ :deleted-at deleted-at :id profile-id}}) - (rph/with-transform {} (session/delete-fn cfg))))) + + (-> (rph/wrap nil) + (rph/with-transform (session/delete-fn cfg)))))) ;; --- HELPERS @@ -431,8 +433,11 @@ "WITH owner_teams AS ( SELECT tpr.team_id AS id FROM team_profile_rel AS tpr + JOIN team AS t ON (t.id = tpr.team_id) WHERE tpr.is_owner IS TRUE AND tpr.profile_id = ? + AND (t.deleted_at IS NULL OR + t.deleted_at > now()) ) SELECT tpr.team_id AS id, count(tpr.profile_id) - 1 AS participants diff --git a/backend/test/backend_tests/rpc_media_test.clj b/backend/test/backend_tests/rpc_media_test.clj index 748c72683..3095a5c05 100644 --- a/backend/test/backend_tests/rpc_media_test.clj +++ b/backend/test/backend_tests/rpc_media_test.clj @@ -10,6 +10,7 @@ [app.db :as db] [app.rpc :as-alias rpc] [app.storage :as sto] + [app.util.time :as dt] [backend-tests.helpers :as th] [clojure.test :as t] [datoteka.fs :as fs])) @@ -245,3 +246,35 @@ (t/is (= "image/jpeg" (:mtype result))) (t/is (uuid? (:media-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))))) diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index 1bd49db48..47e58adba 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -203,7 +203,24 @@ edata (ex-data error)] (t/is (th/ex-info? error)) (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 (let [prof1 (th/create-profile* 1) @@ -291,7 +308,7 @@ out (th/command! params)] ;; (th/print-result! out) - (t/is (= {} (:result out))) + (t/is (nil? (:result out))) (t/is (nil? (:error out)))) ;; query files after profile soft deletion @@ -336,7 +353,7 @@ ::rpc/profile-id (:id prof1)} out (th/command! params)] ;; (th/print-result! out) - (t/is (= {} (:result out))) + (t/is (nil? (:result out))) (t/is (nil? (:error out)))) (th/run-pending-tasks!) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 4f606716c..49756a559 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -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] diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 5cbc687e7..bf802087d 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -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 #"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)) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 4128d1761..0ff5809be 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -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) diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index de4a7518c..f3ef69ca4 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -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}]))) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 13427d601..830d7caad 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index 741e4fb6b..b9ca96b88 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -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)} diff --git a/frontend/src/app/main/ui/workspace/libraries.scss b/frontend/src/app/main/ui/workspace/libraries.scss index 7c04d7b30..a1df227fe 100644 --- a/frontend/src/app/main/ui/workspace/libraries.scss +++ b/frontend/src/app/main/ui/workspace/libraries.scss @@ -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; +} diff --git a/frontend/src/app/util/webapi.cljs b/frontend/src/app/util/webapi.cljs index 2225a96db..722067fe7 100644 --- a/frontend/src/app/util/webapi.cljs +++ b/frontend/src/app/util/webapi.cljs @@ -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")) diff --git a/frontend/text-editor/src/editor/TextEditor.js b/frontend/text-editor/src/editor/TextEditor.js index 357496312..795731b9a 100644 --- a/frontend/text-editor/src/editor/TextEditor.js +++ b/frontend/text-editor/src/editor/TextEditor.js @@ -12,6 +12,7 @@ import ChangeController from "./controllers/ChangeController.js"; import SelectionController from "./controllers/SelectionController.js"; import { createSelectionImposterFromClientRects } from "./selection/Imposter.js"; import { addEventListeners, removeEventListeners } from "./Event.js"; +import { mapContentFragmentFromHTML, mapContentFragmentFromString } from "./content/dom/Content.js"; import { createRoot, createEmptyRoot } from "./content/dom/Root.js"; import { createParagraph } from "./content/dom/Paragraph.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) { return (instance instanceof TextEditor); } diff --git a/frontend/text-editor/src/editor/content/dom/Style.js b/frontend/text-editor/src/editor/content/dom/Style.js index dde094472..a4a883770 100644 --- a/frontend/text-editor/src/editor/content/dom/Style.js +++ b/frontend/text-editor/src/editor/content/dom/Style.js @@ -75,6 +75,17 @@ function getInertElement() { 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. * @@ -115,22 +126,26 @@ export function getComputedStyle(element) { * CSS properties like `font-family` or some CSS variables. * * @param {Node} node - * @param {CSSStyleDeclaration} styleDefaults + * @param {CSSStyleDeclaration} [styleDefaults] * @returns {CSSStyleDeclaration} */ -export function normalizeStyles(node, styleDefaults) { +export function normalizeStyles(node, styleDefaults = getStyleDefaultsDeclaration()) { const styleDeclaration = mergeStyleDeclarations( styleDefaults, getComputedStyle(node.parentElement) ); + // If there's a color property, we should convert it to // a --fills CSS variable property. const fills = styleDeclaration.getPropertyValue("--fills"); const color = styleDeclaration.getPropertyValue("color"); - if (color && !fills) { + if (color) { styleDeclaration.removeProperty("color"); styleDeclaration.setProperty("--fills", getFills(color)); + } else { + styleDeclaration.setProperty("--fills", fills); } + // If there's a font-family property and not a --font-id, then // we remove the font-family because it will not work. const fontFamily = styleDeclaration.getPropertyValue("font-family"); @@ -145,8 +160,15 @@ export function normalizeStyles(node, styleDefaults) { } 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); + } else if (lineHeight.endsWith("px")) { + const fontSize = styleDeclaration.getPropertyValue("font-size"); + styleDeclaration.setProperty( + "line-height", + parseFloat(lineHeight) / parseFloat(fontSize), + ); } return styleDeclaration } diff --git a/frontend/text-editor/src/editor/controllers/SelectionController.test.js b/frontend/text-editor/src/editor/controllers/SelectionController.test.js index 070475e44..786c9a18d 100644 --- a/frontend/text-editor/src/editor/controllers/SelectionController.test.js +++ b/frontend/text-editor/src/editor/controllers/SelectionController.test.js @@ -1,5 +1,4 @@ import { expect, describe, test } from "vitest"; -import TextEditor from "../TextEditor.js"; import { createEmptyParagraph, createParagraph, diff --git a/frontend/translations/en.po b/frontend/translations/en.po index ee4fc8376..d71585de4 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1565,6 +1565,9 @@ msgstr "Active" msgid "labels.add" msgstr "Add" +msgid "labels.adding" +msgstr "Adding..." + #: src/app/main/ui/dashboard/fonts.cljs:176 msgid "labels.add-custom-font" msgstr "Add custom font" @@ -4594,6 +4597,15 @@ msgstr "see all changes" msgid "workspace.libraries.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 msgid "workspace.options.add-interaction" msgstr "Click the + button to add interactions." diff --git a/frontend/translations/es.po b/frontend/translations/es.po index bce795a55..23691241e 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1571,6 +1571,9 @@ msgstr "Activo" msgid "labels.add" msgstr "Añadir" +msgid "labels.adding" +msgstr "Añadiendo..." + #: src/app/main/ui/dashboard/fonts.cljs:176 msgid "labels.add-custom-font" msgstr "Añadir fuente personalizada" @@ -4595,6 +4598,15 @@ msgstr "ver todos los cambios" msgid "workspace.libraries.updates" 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 msgid "workspace.options.add-interaction" msgstr "Pulsa el botón + para añadir interacciones."