From aeb1ac41dadc43b80acb182428f023da2c20013e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 5 Dec 2024 12:39:43 +0100 Subject: [PATCH 1/7] :bug: Prevent upload media objects to deleted files --- backend/src/app/rpc/commands/media.clj | 51 ++++++++++++------- backend/test/backend_tests/rpc_media_test.clj | 33 ++++++++++++ 2 files changed, 66 insertions(+), 18 deletions(-) diff --git a/backend/src/app/rpc/commands/media.clj b/backend/src/app/rpc/commands/media.clj index 69265c27fd..f4913edb25 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/test/backend_tests/rpc_media_test.clj b/backend/test/backend_tests/rpc_media_test.clj index 748c72683a..3095a5c050 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))))) From 2f79d71262da574144157a310df7548f34572290 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 9 Dec 2024 10:15:13 +0100 Subject: [PATCH 2/7] :bug: Fix incorrect event handling on file-menu Don't wait team to be present for open the menu, because with slow connection speed it can cause unexpected ux glitche showing menu when the component inner request is resoved --- .../src/app/main/ui/dashboard/file_menu.cljs | 243 +++++++++--------- frontend/src/app/main/ui/dashboard/grid.cljs | 1 - 2 files changed, 116 insertions(+), 128 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index 9a0926110c..d618eb2a5e 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.store :as st] [app.main.ui.components.context-menu-a11y :refer [context-menu*]] @@ -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)) @@ -207,142 +204,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 4c72b6852a..dd4992c2c4 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -406,7 +406,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 From a923d396030e82fdcb1df169ed008894bfc33078 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 9 Dec 2024 09:48:35 +0100 Subject: [PATCH 3/7] :bug: Fix incorrect teams query on profile deletion The current approach prevents profile deletion when there are some extra (soft)deleted teams where the profile is owner --- backend/src/app/rpc/commands/profile.clj | 7 +++++- .../test/backend_tests/rpc_profile_test.clj | 23 ++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index 57034c4613..7c7ca33399 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_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index 1bd49db485..47e58adba6 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!) From 4ef631fd6a774569f999ef9d4ae6cab404a64dcf Mon Sep 17 00:00:00 2001 From: AzazelN28 Date: Mon, 9 Dec 2024 16:07:44 +0100 Subject: [PATCH 4/7] :bug: Fix copy/paste issues --- frontend/src/app/main/data/workspace.cljs | 35 +++++++++++++++++++ .../src/app/main/data/workspace/texts.cljs | 2 ++ frontend/src/app/util/webapi.cljs | 4 +++ frontend/text-editor/src/editor/TextEditor.js | 15 ++++++++ .../src/editor/content/dom/Style.js | 30 +++++++++++++--- .../controllers/SelectionController.test.js | 1 - 6 files changed, 82 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index cf6092e03e..54ad7c5301 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -65,6 +65,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] @@ -83,6 +84,7 @@ [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [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] @@ -1639,6 +1641,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) @@ -1706,6 +1709,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 20e24611b8..1f7db4b1b3 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/util/webapi.cljs b/frontend/src/app/util/webapi.cljs index 2225a96dbc..722067fe77 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 3574963127..795731b9a4 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 dde094472d..a4a8837701 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 070475e44c..786c9a18de 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, From 257d72ee9d5e87397ccb7d599c00db42631806bf Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Wed, 4 Dec 2024 13:17:39 +0100 Subject: [PATCH 5/7] :sparkles: Add test AB for adding a few "Suggested" libraries --- .../src/app/main/ui/workspace/libraries.cljs | 63 ++++++++++++++++++- .../src/app/main/ui/workspace/libraries.scss | 61 ++++++++++++++++++ frontend/translations/en.po | 12 ++++ frontend/translations/es.po | 12 ++++ 4 files changed, 146 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index 26ae8ca013..732f70e0c8 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -15,7 +15,9 @@ [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.notifications :as ntf] [app.main.data.users :as du] [app.main.data.workspace.colors :as mdc] [app.main.data.workspace.libraries :as dwl] @@ -33,6 +35,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])) @@ -104,9 +107,45 @@ (tr "workspace.libraries.typography" typography-count)])]) +(mf/defc sample-library-entry + [{:keys [library project-id team-id importing] :as props}] + (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 + (mf/deps team-id) + (fn [_] + (st/emit! (dwl/fetch-shared-files {:team-id team-id})))) + + import-library + (mf/use-fn + (fn [_] + (reset! importing id) + (st/emit! (dd/clone-template + (with-meta {:project-id project-id + :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/wrap-props false} - [{:keys [file-id shared? linked-libraries shared-libraries]}] + [{:keys [file-id shared? linked-libraries shared-libraries team-id]}] (let [search-term* (mf/use-state "") search-term (deref search-term*) library-ref (mf/with-memo [file-id] @@ -138,6 +177,12 @@ (->> (vals linked-libraries) (sort-by (comp str/lower :name)))) + importing* (mf/use-state nil) + project-id (mf/deref refs/current-project-id) + 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] @@ -290,6 +335,19 @@ (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 :project-id project-id :team-id team-id :importing importing*}])]] + (str/empty? search-term) [:* [:span {:class (stl/css :empty-state-icon)} @@ -519,7 +577,8 @@ :content (mf/html [:& libraries-tab {:file-id file-id :shared? shared? :linked-libraries libraries - :shared-libraries shared-libraries}])} + :shared-libraries shared-libraries + :team-id team-id}])} #js {:label (tr "workspace.libraries.updates") :id "updates" diff --git a/frontend/src/app/main/ui/workspace/libraries.scss b/frontend/src/app/main/ui/workspace/libraries.scss index 7c04d7b309..a1df227fe5 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/translations/en.po b/frontend/translations/en.po index 0d42a8b064..2c0b4b84a7 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" @@ -4595,6 +4598,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 bce795a555..23691241e2 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." From ce1ba3f28f9b35fc1962c8a053676ce808615b85 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 10 Dec 2024 09:21:45 +0100 Subject: [PATCH 6/7] :lipstick: Fix sample-library-entry component syntax style --- .../src/app/main/ui/workspace/libraries.cljs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index 732f70e0c8..aaa42da7e7 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -107,8 +107,10 @@ (tr "workspace.libraries.typography" typography-count)])]) -(mf/defc sample-library-entry - [{:keys [library project-id team-id importing] :as props}] +(mf/defc sample-library-entry* + {::mf/props :obj + ::mf/private true} + [{:keys [library project-id team-id importing]}] (let [id (:id library) importing? (deref importing) @@ -137,7 +139,9 @@ [: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?)) + [: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}]])) @@ -346,7 +350,11 @@ [:div {:class (stl/css :sample-libraries-container)} (tr "workspace.libraries.empty.add-some") (for [library sample-libraries] - [:& sample-library-entry {:library library :project-id project-id :team-id team-id :importing importing*}])]] + [:> sample-library-entry* + {:library library + :project-id project-id + :team-id team-id + :importing importing*}])]] (str/empty? search-term) [:* From e5d8bc91fbaf3eb455ab1bffa7b4f50f94447732 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 10 Dec 2024 09:26:07 +0100 Subject: [PATCH 7/7] :lipstick: Fix describe-library-blocks component syntax decl style --- .../src/app/main/ui/workspace/libraries.cljs | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index aaa42da7e7..bd5d33232d 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -87,8 +87,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)} @@ -264,10 +266,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 shared? [:input {:class (stl/css :item-unpublish) :type "button" @@ -289,10 +291,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" @@ -323,10 +325,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")