diff --git a/CHANGES.md b/CHANGES.md index 8019f51f52..247d756936 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,15 +1,43 @@ # CHANGELOG # + + + ## :rocket: Next ### :sparkles: New features ### :bug: Bugs fixed - ### :arrow_up: Deps updates ### :boom: Breaking changes ### :heart: Community contributions by (Thank you!) + +## 1.7.2-alpha + +### :sparkles: New features + +- Add many improvements to text tool. + +### :bug: Bugs fixed + +- Add scroll bar to Teams menu [Taiga #1894](https://tree.taiga.io/project/penpot/issue/1894). +- Fix repeated names when duplicating artboards or groups [Taiga #1892](https://tree.taiga.io/project/penpot/issue/1892). +- Fix properly messages lifecycle on navigate. +- Fix handling repeated names on duplicate object trees. +- Fix group naming on group creation. +- Fix some issues in svg transformation. + +### :arrow_up: Deps updates + +- Update frontend build tooling. + +### :boom: Breaking changes +### :heart: Community contributions by (Thank you!) + +- soultipsy [#1100](https://github.com/penpot/penpot/pull/1100) + + ## 1.7.1-alpha ### :bug: Bugs fixed diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index f7c587d487..27df0e7785 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -58,7 +58,7 @@ :srepl-host "127.0.0.1" :srepl-port 6062 - :assets-storage-backend :fs + :assets-storage-backend :assets-fs :storage-assets-fs-directory "assets" :feedback-destination "info@example.com" diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index a3fbc02c62..248a125dd2 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -136,7 +136,9 @@ ["/webhooks" ["/sns" {:post (:sns-webhook cfg)}]] - ["/api" {:middleware [[middleware/etag] + ["/api" {:middleware [ + ;; Temporary disabled + #_[middleware/etag] [middleware/format-response-body] [middleware/params] [middleware/multipart-params] diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index b979b8431a..42babf3dc8 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -85,17 +85,22 @@ (.close ^java.io.OutputStream output-stream)))))) (defn- impl-format-response-body - [response request] + [response _request] (let [body (:body response) opts {:type :json-verbose}] (cond (coll? body) (-> response (update :headers assoc "content-type" "application/transit+json") - (assoc :body - (if (= :post (:request-method request)) - (transit-streamable-body body opts) - (t/encode body opts)))) + (assoc :body (transit-streamable-body body opts))) + + ;; ;; Temporary disabled + ;; (-> response + ;; (update :headers assoc "content-type" "application/transit+json") + ;; (assoc :body + ;; (if (= :post (:request-method request)) + ;; (transit-streamable-body body opts) + ;; (t/encode body opts)))) (nil? body) (assoc response :status 204 :body "") diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index dc43559107..e882f71658 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -322,15 +322,17 @@ :app.storage/storage {:pool (ig/ref :app.db/pool) :executor (ig/ref :app.worker/executor) - :backend (cf/get :assets-storage-backend :assets-fs) - :backends {:assets-s3 (ig/ref [::assets :app.storage.s3/backend]) + + :backends { + :assets-s3 (ig/ref [::assets :app.storage.s3/backend]) :assets-db (ig/ref [::assets :app.storage.db/backend]) :assets-fs (ig/ref [::assets :app.storage.fs/backend]) - :s3 (ig/ref [::assets :app.storage.s3/backend]) - :db (ig/ref [::assets :app.storage.db/backend]) - :fs (ig/ref [::assets :app.storage.fs/backend]) :tmp (ig/ref [::tmp :app.storage.fs/backend]) - :fdata-s3 (ig/ref [::fdata :app.storage.s3/backend])}} + :fdata-s3 (ig/ref [::fdata :app.storage.s3/backend]) + + ;; keep this for backward compatibility + :s3 (ig/ref [::assets :app.storage.s3/backend]) + :fs (ig/ref [::assets :app.storage.fs/backend])}} [::fdata :app.storage.s3/backend] {:region (cf/get :storage-fdata-s3-region) diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index b2aeefa184..59019558d6 100644 --- a/backend/src/app/media.clj +++ b/backend/src/app/media.clj @@ -11,6 +11,7 @@ [app.common.exceptions :as ex] [app.common.media :as cm] [app.common.spec :as us] + [app.config :as cf] [app.rlimits :as rlm] [app.rpc.queries.svg :as svg] [buddy.core.bytes :as bb] @@ -28,10 +29,6 @@ org.im4java.core.IMOperation org.im4java.core.Info)) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; --- Utility functions -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - (s/def ::image-content-type cm/valid-image-types) (s/def ::font-content-type cm/valid-font-types) @@ -330,3 +327,17 @@ (= stype :ttf) (-> (assoc "font/otf" (ttf->otf sfnt)) (assoc "font/ttf" sfnt))))))))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Utility functions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn configure-assets-storage + "Given storage map, returns a storage configured with the apropriate + backend for assets." + [storage conn] + (-> storage + (assoc :conn conn) + (assoc :backend (cf/get :assets-storage-backend :assets-fs)))) + diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj index 3c266268d3..6436f0fc92 100644 --- a/backend/src/app/rpc/mutations/files.clj +++ b/backend/src/app/rpc/mutations/files.clj @@ -11,7 +11,6 @@ [app.common.pages.migrations :as pmg] [app.common.spec :as us] [app.common.uuid :as uuid] - [app.config :as cf] [app.db :as db] [app.rpc.permissions :as perms] [app.rpc.queries.files :as files] @@ -288,7 +287,7 @@ (defn- delete-from-storage [{:keys [storage] :as cfg} file] - (when-let [backend (simpl/resolve-backend storage (cf/get :fdata-storage-backend))] + (when-let [backend (simpl/resolve-backend storage (:data-backend file))] (simpl/del-object backend file))) (defn- update-file diff --git a/backend/src/app/rpc/mutations/fonts.clj b/backend/src/app/rpc/mutations/fonts.clj index 9037550f13..fd3291e6d3 100644 --- a/backend/src/app/rpc/mutations/fonts.clj +++ b/backend/src/app/rpc/mutations/fonts.clj @@ -47,7 +47,8 @@ (defn create-font-variant [{:keys [conn storage] :as cfg} {:keys [data] :as params}] (let [data (media/run cfg {:cmd :generate-fonts :input data :rlimit :font}) - storage (assoc storage :conn conn) + storage (media/configure-assets-storage storage conn) + otf (when-let [fdata (get data "font/otf")] (sto/put-object storage {:content (sto/content fdata) :content-type "font/otf"})) diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj index 3467522f1d..bfe876df1e 100644 --- a/backend/src/app/rpc/mutations/media.clj +++ b/backend/src/app/rpc/mutations/media.clj @@ -32,7 +32,6 @@ (s/def ::file-id ::us/uuid) (s/def ::team-id ::us/uuid) - ;; --- Create File Media object (upload) (declare create-file-media-object) @@ -94,10 +93,9 @@ (defn create-file-media-object [{:keys [conn storage] :as cfg} {:keys [id file-id is-local name content] :as params}] (media/validate-media-type (:content-type content)) - (let [storage (assoc storage :conn conn) + (let [storage (media/configure-assets-storage storage conn) source-path (fs/path (:tempfile content)) source-mtype (:content-type content) - source-info (media/run cfg {:cmd :info :input {:path source-path :mtype source-mtype}}) thumb (when (and (not (svg-image? source-info)) diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index 17c52869a7..cc1dce0799 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -9,7 +9,7 @@ [app.common.exceptions :as ex] [app.common.spec :as us] [app.common.uuid :as uuid] - [app.config :as cfg] + [app.config :as cf] [app.db :as db] [app.emails :as eml] [app.http.oauth :refer [extract-props]] @@ -99,11 +99,11 @@ (sv/defmethod ::prepare-register-profile {:auth false} [{:keys [pool tokens] :as cfg} params] - (when-not (cfg/get :registration-enabled) + (when-not (cf/get :registration-enabled) (ex/raise :type :restriction :code :registration-disabled)) - (when-let [domains (cfg/get :registration-domain-whitelist)] + (when-let [domains (cf/get :registration-domain-whitelist)] (when-not (email-domain-in-whitelist? domains (:email params)) (ex/raise :type :validation :code :email-domain-is-not-allowed))) @@ -402,6 +402,7 @@ {:password (derive-password password)} {:id id})) + ;; --- MUTATION: Update Photo (declare update-profile-photo) @@ -416,11 +417,13 @@ [{:keys [pool storage] :as cfg} {:keys [profile-id file] :as params}] (db/with-atomic [conn pool] (media/validate-media-type (:content-type file) #{"image/jpeg" "image/png" "image/webp"}) + (media/run cfg {:cmd :info :input {:path (:tempfile file) + :mtype (:content-type file)}}) + (let [profile (db/get-by-id conn :profile profile-id) - _ (media/run cfg {:cmd :info :input {:path (:tempfile file) - :mtype (:content-type file)}}) - photo (teams/upload-photo cfg params) - storage (assoc storage :conn conn)] + storage (media/configure-assets-storage storage conn) + cfg (assoc cfg :storage storage) + photo (teams/upload-photo cfg params)] ;; Schedule deletion of old photo (when-let [id (:photo-id profile)] @@ -453,7 +456,7 @@ params (assoc params :profile profile :email (str/lower email))] - (if (cfg/get :smtp-enabled) + (if (cf/get :smtp-enabled) (request-email-change cfg params) (change-email-inmediatelly cfg params))))) diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj index 3b71c43c48..d0eae6dee5 100644 --- a/backend/src/app/rpc/mutations/teams.clj +++ b/backend/src/app/rpc/mutations/teams.clj @@ -251,10 +251,12 @@ (db/with-atomic [conn pool] (teams/check-edition-permissions! conn profile-id team-id) (media/validate-media-type (:content-type file) #{"image/jpeg" "image/png" "image/webp"}) + (media/run cfg {:cmd :info :input {:path (:tempfile file) + :mtype (:content-type file)}}) (let [team (teams/retrieve-team conn profile-id team-id) - _ (media/run cfg {:cmd :info :input {:path (:tempfile file) - :mtype (:content-type file)}}) + storage (media/configure-assets-storage storage conn) + cfg (assoc cfg :storage storage) photo (upload-photo cfg params)] ;; Schedule deletion of old photo @@ -263,8 +265,8 @@ ;; Save new photo (db/update! conn :team - {:photo-id (:id photo)} - {:id team-id}) + {:photo-id (:id photo)} + {:id team-id}) (assoc team :photo-id (:id photo))))) diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 74938feb23..55f536d5f3 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -9,7 +9,6 @@ [app.common.pages.migrations :as pmg] [app.common.spec :as us] [app.common.uuid :as uuid] - [app.config :as cf] [app.db :as db] [app.rpc.permissions :as perms] [app.rpc.queries.projects :as projects] @@ -175,7 +174,7 @@ (defn- retrieve-data* [{:keys [storage] :as cfg} file] - (when-let [backend (simpl/resolve-backend storage (cf/get :fdata-storage-backend))] + (when-let [backend (simpl/resolve-backend storage (:data-backend file))] (simpl/get-object-bytes backend file))) (defn retrieve-data diff --git a/backend/src/app/storage.clj b/backend/src/app/storage.clj index f8a923a2de..e6acb07763 100644 --- a/backend/src/app/storage.clj +++ b/backend/src/app/storage.clj @@ -28,8 +28,6 @@ ;; Storage Module State ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::backend ::us/keyword) - (s/def ::s3 ::ss3/backend) (s/def ::fs ::sfs/backend) (s/def ::db ::sdb/backend) @@ -42,7 +40,7 @@ :db ::sdb/backend)))) (defmethod ig/pre-init-spec ::storage [_] - (s/keys :req-un [::backend ::wrk/executor ::db/pool ::backends])) + (s/keys :req-un [::wrk/executor ::db/pool ::backends])) (defmethod ig/prep-key ::storage [_ {:keys [backends] :as cfg}] @@ -55,7 +53,7 @@ (assoc :backends (d/without-nils backends)))) (s/def ::storage - (s/keys :req-un [::backends ::wrk/executor ::db/pool ::backend])) + (s/keys :req-un [::backends ::wrk/executor ::db/pool])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Database Objects diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index 1172de76e2..997edc170e 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -15,6 +15,20 @@ [app.common.pages.spec :as spec] [app.common.spec :as us])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Specific helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- without-obj + "Clear collection from specified obj and without nil values." + [coll o] + (into [] (filter #(not= % o)) coll)) + +(defn vec-without-nils + [coll] + (into [] (remove nil?) coll)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Page Transformation Changes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -54,45 +68,50 @@ (assoc data :options (d/dissoc-in (:options data) path))))))) (defmethod process-change :add-obj - [data {:keys [id obj page-id component-id frame-id parent-id - index ignore-touched]}] - (letfn [(update-fn [data] - (let [parent-id (or parent-id frame-id) - objects (:objects data) - obj (assoc obj - :frame-id frame-id - :parent-id parent-id - :id id)] - (if (and (or (nil? parent-id) (contains? objects parent-id)) - (or (nil? frame-id) (contains? objects frame-id))) - (-> data - (update :objects assoc id obj) - (update-in [:objects parent-id :shapes] - (fn [shapes] - (let [shapes (or shapes [])] - (cond - (some #{id} shapes) - shapes + [data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]}] + (letfn [(update-parent-shapes [shapes] + ;; Ensure that shapes is always a vector. + (let [shapes (into [] shapes)] + (cond + (some #{id} shapes) + shapes - (nil? index) - (if (= :frame (:type obj)) - (d/concat [id] shapes) - (conj shapes id)) + (nil? index) + (if (= :frame (:type obj)) + (into [id] shapes) + (conj shapes id)) - :else - (cph/insert-at-index shapes index [id]))))) + :else + (cph/insert-at-index shapes index [id])))) + + (update-parent [parent] + (-> parent + (update :shapes update-parent-shapes) + (update :shapes vec-without-nils) + (cond-> (and (:shape-ref parent) + (not= (:id parent) frame-id) + (not ignore-touched)) + (-> (update :touched cph/set-touched-group :shapes-group) + (dissoc :remote-synced?))))) + + (update-objects [objects parent-id] + (if (and (or (nil? parent-id) (contains? objects parent-id)) + (or (nil? frame-id) (contains? objects frame-id))) + (-> objects + (assoc id (-> obj + (assoc :frame-id frame-id) + (assoc :parent-id parent-id) + (assoc :id id))) + (update parent-id update-parent)) + objects)) + + (update-container [data] + (let [parent-id (or parent-id frame-id)] + (update data :objects update-objects parent-id)))] - (cond-> (and (:shape-ref (get-in data [:objects parent-id])) - (not= parent-id frame-id) - (not ignore-touched)) - (-> - (update-in [:objects parent-id :touched] - cph/set-touched-group :shapes-group) - (d/dissoc-in [:objects parent-id :remote-synced?])))) - data)))] (if page-id - (d/update-in-when data [:pages-index page-id] update-fn) - (d/update-in-when data [:components component-id] update-fn)))) + (d/update-in-when data [:pages-index page-id] update-container) + (d/update-in-when data [:components component-id] update-container)))) (defmethod process-change :mod-obj [data {:keys [id page-id component-id operations]}] @@ -107,32 +126,27 @@ (defmethod process-change :del-obj [data {:keys [page-id component-id id ignore-touched]}] - (letfn [(delete-object [objects id] + (letfn [(delete-from-parent [parent] + (let [parent (update parent :shapes without-obj id)] + (cond-> parent + (and (:shape-ref parent) + (not ignore-touched)) + (-> (update :touched cph/set-touched-group :shapes-group) + (dissoc :remote-synced?))))) + + (delete-from-objects [objects] (if-let [target (get objects id)] - (let [parent-id (cph/get-parent id objects) - frame-id (:frame-id target) - parent (get objects parent-id) - objects (dissoc objects id)] - (cond-> objects - (and (not= parent-id frame-id) - (#{:group :svg-raw} (:type parent))) - (update-in [parent-id :shapes] (fn [s] (filterv #(not= % id) s))) - - (and (:shape-ref parent) (not ignore-touched)) - (-> - (update-in [parent-id :touched] cph/set-touched-group :shapes-group) - (d/dissoc-in [parent-id :remote-synced?])) - - (contains? objects frame-id) - (update-in [frame-id :shapes] (fn [s] (filterv #(not= % id) s))) - - (seq (:shapes target)) ; Recursive delete all - ; dependend objects - (as-> $ (reduce delete-object $ (:shapes target))))) + (let [parent-id (or (:parent-id target) + (:frame-id target)) + children (cph/get-children id objects)] + (-> (reduce dissoc objects children) + (dissoc id) + (d/update-when parent-id delete-from-parent))) objects))] + (if page-id - (d/update-in-when data [:pages-index page-id :objects] delete-object id) - (d/update-in-when data [:components component-id :objects] delete-object id)))) + (d/update-in-when data [:pages-index page-id :objects] delete-from-objects) + (d/update-in-when data [:components component-id :objects] delete-from-objects)))) ;; reg-objects operation "regenerates" the geometry and selrect of the parent groups (defmethod process-change :reg-objects @@ -191,25 +205,24 @@ (insert-items prev-shapes index shapes) ;; For masked groups, the first shape is the mask ;; and it cannot be moved. - (let [mask-id (first prev-shapes) - other-ids (rest prev-shapes) - not-mask-shapes (strip-id shapes mask-id) - new-index (if (nil? index) nil (max (dec index) 0)) - new-shapes (insert-items other-ids new-index not-mask-shapes)] + (let [mask-id (first prev-shapes) + other-ids (rest prev-shapes) + not-mask-shapes (without-obj shapes mask-id) + new-index (if (nil? index) nil (max (dec index) 0)) + new-shapes (insert-items other-ids new-index not-mask-shapes)] (d/concat [mask-id] new-shapes)))) - (strip-id [coll id] - (filterv #(not= % id) coll)) - (add-to-parent [parent index shapes] - (cond-> parent - true - (update :shapes check-insert-items parent index shapes) - - (and (:shape-ref parent) (= (:type parent) :group) (not ignore-touched)) - (-> - (update :touched cph/set-touched-group :shapes-group) - (dissoc :remote-synced?)))) + (let [parent (-> parent + (update :shapes check-insert-items parent index shapes) + ;; We need to ensure that no `nil` in the + ;; shapes list after adding all the + ;; incoming shapes to the parent. + (update :shapes vec-without-nils))] + (cond-> parent + (and (:shape-ref parent) (= (:type parent) :group) (not ignore-touched)) + (-> (update :touched cph/set-touched-group :shapes-group) + (dissoc :remote-synced?))))) (remove-from-old-parent [cpindex objects shape-id] (let [prev-parent-id (get cpindex shape-id)] @@ -217,22 +230,19 @@ ;; the new destination target parent id. (if (= prev-parent-id parent-id) objects - (let [sid shape-id - pid prev-parent-id - obj (get objects pid) + (let [sid shape-id + pid prev-parent-id + obj (get objects pid) component? (and (:shape-ref obj) (= (:type obj) :group) (not ignore-touched))] (-> objects - (d/update-in-when [pid :shapes] strip-id sid) - - (cond-> component? - (d/update-when - pid - #(-> % - (update :touched cph/set-touched-group :shapes-group) - (dissoc :remote-synced?))))))))) + (d/update-in-when [pid :shapes] without-obj sid) + (d/update-in-when [pid :shapes] vec-without-nils) + (cond-> component? (d/update-when pid #(-> % + (update :touched cph/set-touched-group :shapes-group) + (dissoc :remote-synced?))))))))) (update-parent-id [objects id] (-> objects @@ -240,8 +250,7 @@ ;; Updates the frame-id references that might be outdated (assign-frame-id [frame-id objects id] - (let [objects (-> objects - (d/update-when id assoc :frame-id frame-id)) + (let [objects (d/update-when objects id assoc :frame-id frame-id) obj (get objects id)] (cond-> objects ;; If we moving frame, the parent frame is the root diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 37d7febeea..6b30e3a9e3 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -103,16 +103,16 @@ children's order will be breadth first." [id objects] - (loop [result (transient []) + (loop [result (transient []) pending (transient []) - next id] + next id] (let [children (get-in objects [next :shapes] []) [result pending] ;; Iterate through children and add them to the result ;; also add them in pending to check for their children (loop [result result pending pending - current (first children) + current (first children) children (rest children)] (if current (recur (conj! result current) @@ -214,7 +214,7 @@ (if (some #{id} acc) acc (conj acc id))) - prev-ids + (vec prev-ids) ids)) (defn select-toplevel-shapes diff --git a/common/src/app/common/text.cljc b/common/src/app/common/text.cljc index 9f184e7e9d..f4c450f1a1 100644 --- a/common/src/app/common/text.cljc +++ b/common/src/app/common/text.cljc @@ -64,7 +64,8 @@ (defn ^boolean is-text-node? [node] - (string? (:text node))) + (and (string? (:text node)) + (not= (:text node) ""))) (defn ^boolean is-paragraph-node? [node] diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 5508f5a7da..e6deb917ca 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -3,10 +3,10 @@ LABEL maintainer="Andrey Antukh " ARG DEBIAN_FRONTEND=noninteractive -ENV NODE_VERSION=v14.17.2 \ - CLOJURE_VERSION=1.10.3.882 \ +ENV NODE_VERSION=v14.17.3 \ + CLOJURE_VERSION=1.10.3.929 \ CLJKONDO_VERSION=2021.06.18 \ - BABASHKA_VERSION=0.4.6 \ + BABASHKA_VERSION=0.5.0 \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 diff --git a/frontend/deps.edn b/frontend/deps.edn index 2695bf8460..7467b3e658 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -23,7 +23,7 @@ :dev {:extra-deps - {thheller/shadow-cljs {:mvn/version "2.15.1"}}} + {thheller/shadow-cljs {:mvn/version "2.15.2"}}} :shadow-cljs {:main-opts ["-m" "shadow.cljs.devtools.cli"]} diff --git a/frontend/package.json b/frontend/package.json index e5f371b46f..13261d45a2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,7 +33,7 @@ "postcss-clean": "^1.2.2", "rimraf": "^3.0.0", "sass": "^1.35.1", - "shadow-cljs": "2.15.1" + "shadow-cljs": "2.15.2" }, "dependencies": { "date-fns": "^2.22.1", diff --git a/frontend/resources/styles/main/partials/dashboard-sidebar.scss b/frontend/resources/styles/main/partials/dashboard-sidebar.scss index 2c4a0b8d15..2e90557595 100644 --- a/frontend/resources/styles/main/partials/dashboard-sidebar.scss +++ b/frontend/resources/styles/main/partials/dashboard-sidebar.scss @@ -38,6 +38,7 @@ z-index: 12; max-height: 30rem; min-width: 230px; + overflow-y: auto; } .options-dropdown { diff --git a/frontend/src/app/main/data/messages.cljs b/frontend/src/app/main/data/messages.cljs index 1a834fa3c6..acd1564306 100644 --- a/frontend/src/app/main/data/messages.cljs +++ b/frontend/src/app/main/data/messages.cljs @@ -52,11 +52,17 @@ ptk/WatchEvent (watch [_ _ stream] - (when (:timeout data) - (let [stoper (rx/filter (ptk/type? ::show) stream)] - (->> (rx/of hide) - (rx/delay (:timeout data)) - (rx/take-until stoper))))))) + (rx/merge + (let [stoper (rx/filter (ptk/type? ::hide) stream)] + (->> stream + (rx/filter (ptk/type? :app.util.router/navigate)) + (rx/map (constantly hide)) + (rx/take-until stoper))) + (when (:timeout data) + (let [stoper (rx/filter (ptk/type? ::show) stream)] + (->> (rx/of hide) + (rx/delay (:timeout data)) + (rx/take-until stoper)))))))) (def hide (ptk/reify ::hide diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 7c0a57c7d9..8428c09102 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -204,10 +204,19 @@ ptk/UpdateEvent (update [_ state] (dissoc state + :current-file-id + :current-project-id + :current-team-id + :workspace-data + :workspace-editor-state :workspace-file - :workspace-project + :workspace-libraries :workspace-media-objects - :workspace-persistence)) + :workspace-persistence + :workspace-presence + :workspace-project + :workspace-project + :workspace-undo)) ptk/WatchEvent (watch [_ _ _] @@ -242,9 +251,10 @@ (update [_ state] (let [page-id (or page-id (get-in state [:workspace-data :pages 0])) local (-> (:workspace-local state) - (dissoc :edition) - (dissoc :edit-path) - (dissoc :selected))] + (dissoc + :edition + :edit-path + :selected))] (-> state (assoc-in [:workspace-cache page-id] local) (dissoc :current-page-id :workspace-local :trimmed-page :workspace-drawing)))))) @@ -1474,8 +1484,8 @@ (= :frame (get-in objects [(first selected) :type]))))) (defn- paste-shape - [{:keys [selected objects images] :as data} in-viewport?] - (letfn [;; Given a file-id and img (part generated by the + [{:keys [selected objects images] :as data} in-viewport?] ;; TODO: perhaps rename 'objects' to 'shapes', because it contains only + (letfn [;; Given a file-id and img (part generated by the ;; the shapes to paste, not the whole page tree of shapes ;; copy-selected event), uploads the new media. (upload-media [file-id imgpart] (->> (http/send! {:uri (:file-data imgpart) @@ -1583,7 +1593,7 @@ page-id (:current-page-id state) unames (-> (wsh/lookup-page-objects state page-id) - (dwc/retrieve-used-names)) + (dwc/retrieve-used-names)) ;; TODO: move this calculation inside prepare-duplcate-changes? rchanges (->> (dws/prepare-duplicate-changes objects page-id unames selected delta) (mapv (partial process-rchange media-idx)) diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 5d6d7c00f3..b76dce9749 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -400,67 +400,74 @@ (into (d/ordered-set) empty-parents-xform all-parents) mk-del-obj-xf - (map (fn [id] - {:type :del-obj - :page-id page-id - :id id})) + (comp (filter (partial contains? objects)) + (map (fn [id] + {:type :del-obj + :page-id page-id + :id id}))) mk-add-obj-xf - (map (fn [id] - (let [item (get objects id)] - {:type :add-obj - :id (:id item) - :page-id page-id - :index (cp/position-on-parent id objects) - :frame-id (:frame-id item) - :parent-id (:parent-id item) - :obj item}))) + (comp (filter (partial contains? objects)) + (map (fn [id] + (let [item (get objects id)] + {:type :add-obj + :id (:id item) + :page-id page-id + :index (cp/position-on-parent id objects) + :frame-id (:frame-id item) + :parent-id (:parent-id item) + :obj item})))) mk-mod-touched-xf - (map (fn [id] - (let [parent (get objects id)] - {:type :mod-obj - :page-id page-id - :id (:id parent) - :operations [{:type :set-touched - :touched (:touched parent)}]}))) + (comp (filter (partial contains? objects)) + (map (fn [id] + (let [parent (get objects id)] + {:type :mod-obj + :page-id page-id + :id (:id parent) + :operations [{:type :set-touched + :touched (:touched parent)}]})))) mk-mod-int-del-xf - (map (fn [obj] - {:type :mod-obj - :page-id page-id - :id (:id obj) - :operations [{:type :set - :attr :interactions - :val (vec (remove (fn [interaction] - (contains? ids (:destination interaction))) - (:interactions obj)))}]})) + (comp (filter some?) + (map (fn [obj] + {:type :mod-obj + :page-id page-id + :id (:id obj) + :operations [{:type :set + :attr :interactions + :val (vec (remove (fn [interaction] + (contains? ids (:destination interaction))) + (:interactions obj)))}]}))) mk-mod-int-add-xf - (map (fn [obj] - {:type :mod-obj - :page-id page-id - :id (:id obj) - :operations [{:type :set - :attr :interactions - :val (:interactions obj)}]})) + (comp (filter some?) + (map (fn [obj] + {:type :mod-obj + :page-id page-id + :id (:id obj) + :operations [{:type :set + :attr :interactions + :val (:interactions obj)}]}))) mk-mod-unmask-xf - (map (fn [id] - {:type :mod-obj - :page-id page-id - :id id - :operations [{:type :set - :attr :masked-group? - :val false}]})) + (comp (filter (partial contains? objects)) + (map (fn [id] + {:type :mod-obj + :page-id page-id + :id id + :operations [{:type :set + :attr :masked-group? + :val false}]}))) mk-mod-mask-xf - (map (fn [id] - {:type :mod-obj - :page-id page-id - :id id - :operations [{:type :set - :attr :masked-group? - :val true}]})) + (comp (filter (partial contains? objects)) + (map (fn [id] + {:type :mod-obj + :page-id page-id + :id id + :operations [{:type :set + :attr :masked-group? + :val true}]}))) rchanges (-> [] diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index fc88141b31..db26140a8d 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -23,21 +23,14 @@ (map #(assoc % ::index (cp/position-on-parent (:id %) objects))) (sort-by ::index))) -(defn- make-group - [shapes prefix keep-name] - (let [selrect (gsh/selection-rect shapes) - frame-id (-> shapes first :frame-id) - group-name (if (and keep-name - (= (count shapes) 1) - (= (:type (first shapes)) :group)) - (:name (first shapes)) - (name (gensym prefix)))] ; TODO: we should something like in new shapes - (-> (cp/make-minimal-group frame-id selrect group-name) - (gsh/setup selrect) - (assoc :shapes (mapv :id shapes))))) +(defn- get-empty-groups-after-group-creation + "An auxiliar function that finds and returns a set of ids that + corresponds to groups that should be deleted after a group creation. -(defn get-empty-groups - "Retrieve emtpy groups after group creation" + The corner case happens when you selects two (or more) shapes that + belongs each one to different groups, and after creating the new + group, one (or many) groups can become empty because they have had a + single shape which is moved to the created group." [objects parent-id shapes] (let [ids (cp/clean-loops objects (into #{} (map :id) shapes)) parents (->> ids @@ -71,56 +64,67 @@ result))))))) (defn prepare-create-group - [objects page-id shapes prefix keep-name] - (let [group (make-group shapes prefix keep-name) - frame-id (:frame-id (first shapes)) + [objects page-id shapes base-name keep-name?] + (let [frame-id (:frame-id (first shapes)) parent-id (:parent-id (first shapes)) - rchanges [{:type :add-obj - :id (:id group) - :page-id page-id - :frame-id frame-id - :parent-id parent-id - :obj group - :index (::index (first shapes))} + gname (if (and keep-name? + (= (count shapes) 1) + (= (:type (first shapes)) :group)) + (:name (first shapes)) + (-> (dwc/retrieve-used-names objects) + (dwc/generate-unique-name base-name))) - {:type :mov-objects - :page-id page-id - :parent-id (:id group) - :shapes (mapv :id shapes)}] + selrect (gsh/selection-rect shapes) + group (-> (cp/make-minimal-group frame-id selrect gname) + (gsh/setup selrect) + (assoc :shapes (mapv :id shapes))) - uchanges (-> (mapv - (fn [obj] - {:type :mov-objects - :page-id page-id - :parent-id (:parent-id obj) - :index (::index obj) - :shapes [(:id obj)]}) shapes) - (conj - {:type :del-obj - :id (:id group) - :page-id page-id})) + rchanges [{:type :add-obj + :id (:id group) + :page-id page-id + :frame-id frame-id + :parent-id parent-id + :obj group + :index (::index (first shapes))} - ids-to-delete (get-empty-groups objects parent-id shapes) + {:type :mov-objects + :page-id page-id + :parent-id (:id group) + :shapes (mapv :id shapes)}] + + uchanges (-> (mapv (fn [obj] + {:type :mov-objects + :page-id page-id + :parent-id (:parent-id obj) + :index (::index obj) + :shapes [(:id obj)]}) + shapes) + (conj {:type :del-obj + :id (:id group) + :page-id page-id})) + + ;; Look at the `get-empty-groups-after-group-creation` + ;; doctring to understand the real purpuse of this code + ids-to-delete (get-empty-groups-after-group-creation objects parent-id shapes) delete-group (fn [changes id] - (-> changes - (conj {:type :del-obj - :id id - :page-id page-id}))) + (conj changes {:type :del-obj + :id id + :page-id page-id})) add-deleted-group (fn [changes id] (let [obj (-> (get objects id) - (d/without-keys [:shapes]))] - - (d/concat [{:type :add-obj - :id id - :page-id page-id - :frame-id (:frame-id obj) - :parent-id (:parent-id obj) - :obj obj - :index (::index obj)}] changes))) + (dissoc :shapes))] + (into [{:type :add-obj + :id id + :page-id page-id + :frame-id (:frame-id obj) + :parent-id (:parent-id obj) + :obj obj + :index (::index obj)}] + changes))) rchanges (->> ids-to-delete (reduce delete-group rchanges)) @@ -178,7 +182,7 @@ shapes (shapes-for-grouping objects selected)] (when-not (empty? shapes) (let [[group rchanges uchanges] - (prepare-create-group objects page-id shapes "Group-" false)] + (prepare-create-group objects page-id shapes "Group" false)] (rx/of (dch/commit-changes {:redo-changes rchanges :undo-changes uchanges :origin it}) @@ -217,7 +221,7 @@ (if (and (= (count shapes) 1) (= (:type (first shapes)) :group)) [(first shapes) [] []] - (prepare-create-group objects page-id shapes "Group-" true)) + (prepare-create-group objects page-id shapes "Group" true)) rchanges (d/concat rchanges [{:type :mod-obj diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index b267fb3885..77b15d1b9d 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -382,7 +382,7 @@ page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - unames (atom (dwc/retrieve-used-names objects)) + unames (volatile! (dwc/retrieve-used-names objects)) frame-id (cp/frame-id-by-position objects (gpt/add orig-pos delta)) @@ -391,7 +391,7 @@ (let [new-name (dwc/generate-unique-name @unames (:name new-shape))] (when (nil? (:parent-id original-shape)) - (swap! unames conj new-name)) + (vswap! unames conj new-name)) (cond-> new-shape true @@ -594,7 +594,7 @@ ptk/UpdateEvent (update [_ state] (if (not= library-id (:current-file-id state)) - (assoc-in state [:workspace-libraries library-id :synced-at] (dt/now)) + (d/assoc-in-when state [:workspace-libraries library-id :synced-at] (dt/now)) state)) ptk/WatchEvent diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index 788b2453a0..16115953e6 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -129,7 +129,7 @@ (if (and (= (count shapes) 1) (= (:type (first shapes)) :group)) [(first shapes) [] []] - (dwg/prepare-create-group objects page-id shapes "Component-" true)) + (dwg/prepare-create-group objects page-id shapes "Component" true)) [new-shape new-shapes updated-shapes] (make-component-shape group objects file-id) @@ -204,6 +204,7 @@ "Generate changes to remove the links between a shape and all its children with a component." [shape-id container] + (log/debug :msg "Detach instance" :shape-id shape-id :container (:id container)) (let [shapes (cp/get-object-with-children shape-id (:objects container)) rchanges (mapv (fn [obj] (make-change @@ -646,89 +647,92 @@ :shape (str (:name shape-inst)) :component (:name component)) - (let [omit-touched? (not reset?) - clear-remote-synced? (and initial-root? reset?) - set-remote-synced? (and (not initial-root?) reset?) + (if (nil? shape-main) + ;; This should not occur, but protect against it in any case + (generate-detach-instance (:id shape-inst) container) + (let [omit-touched? (not reset?) + clear-remote-synced? (and initial-root? reset?) + set-remote-synced? (and (not initial-root?) reset?) - [rchanges uchanges] - (concat-changes - (update-attrs shape-inst - shape-main - root-inst - root-main - container - omit-touched?) + [rchanges uchanges] (concat-changes - (if reset? - (change-touched shape-inst - shape-main - container - {:reset-touched? true}) - empty-changes) + (update-attrs shape-inst + shape-main + root-inst + root-main + container + omit-touched?) (concat-changes - (if clear-remote-synced? - (change-remote-synced shape-inst container nil) + (if reset? + (change-touched shape-inst + shape-main + container + {:reset-touched? true}) empty-changes) - (if set-remote-synced? - (change-remote-synced shape-inst container true) - empty-changes)))) + (concat-changes + (if clear-remote-synced? + (change-remote-synced shape-inst container nil) + empty-changes) + (if set-remote-synced? + (change-remote-synced shape-inst container true) + empty-changes)))) - children-inst (mapv #(cp/get-shape container %) - (:shapes shape-inst)) - children-main (mapv #(cp/get-shape component %) - (:shapes shape-main)) + children-inst (mapv #(cp/get-shape container %) + (:shapes shape-inst)) + children-main (mapv #(cp/get-shape component %) + (:shapes shape-main)) - only-inst (fn [child-inst] - (when-not (and omit-touched? - (contains? (:touched shape-inst) - :shapes-group)) - (remove-shape child-inst - container - omit-touched?))) + only-inst (fn [child-inst] + (when-not (and omit-touched? + (contains? (:touched shape-inst) + :shapes-group)) + (remove-shape child-inst + container + omit-touched?))) - only-main (fn [child-main] - (when-not (and omit-touched? - (contains? (:touched shape-inst) - :shapes-group)) - (add-shape-to-instance child-main - (d/index-of children-main - child-main) - component - container - root-inst - root-main - omit-touched? - set-remote-synced?))) + only-main (fn [child-main] + (when-not (and omit-touched? + (contains? (:touched shape-inst) + :shapes-group)) + (add-shape-to-instance child-main + (d/index-of children-main + child-main) + component + container + root-inst + root-main + omit-touched? + set-remote-synced?))) - both (fn [child-inst child-main] - (generate-sync-shape-direct-recursive container - child-inst - component - child-main - root-inst - root-main - reset? - initial-root?)) + both (fn [child-inst child-main] + (generate-sync-shape-direct-recursive container + child-inst + component + child-main + root-inst + root-main + reset? + initial-root?)) - moved (fn [child-inst child-main] - (move-shape - child-inst - (d/index-of children-inst child-inst) - (d/index-of children-main child-main) - container - omit-touched?)) + moved (fn [child-inst child-main] + (move-shape + child-inst + (d/index-of children-inst child-inst) + (d/index-of children-main child-main) + container + omit-touched?)) - [child-rchanges child-uchanges] - (compare-children children-inst - children-main - only-inst - only-main - both - moved - false)] + [child-rchanges child-uchanges] + (compare-children children-inst + children-main + only-inst + only-main + both + moved + false)] - [(d/concat rchanges child-rchanges) - (d/concat uchanges child-uchanges)])) + [(d/concat rchanges child-rchanges) + (d/concat uchanges child-uchanges)]))) (defn generate-sync-shape-inverse "Generate changes to update the component a shape is linked to, from @@ -764,96 +768,99 @@ :shape (str (:name shape-inst)) :component (:name component)) - (let [component-container (cp/make-container component :component) + (if (nil? shape-main) + ;; This should not occur, but protect against it in any case + empty-changes + (let [component-container (cp/make-container component :component) - omit-touched? false - set-remote-synced? (not initial-root?) - clear-remote-synced? initial-root? + omit-touched? false + set-remote-synced? (not initial-root?) + clear-remote-synced? initial-root? - [rchanges uchanges] - (concat-changes - (update-attrs shape-main - shape-inst - root-main - root-inst - component-container - omit-touched?) + [rchanges uchanges] (concat-changes - (change-touched shape-inst - shape-main - container - {:reset-touched? true}) + (update-attrs shape-main + shape-inst + root-main + root-inst + component-container + omit-touched?) (concat-changes - (change-touched shape-main - shape-inst - component-container - {:copy-touched? true}) + (change-touched shape-inst + shape-main + container + {:reset-touched? true}) (concat-changes - (if clear-remote-synced? - (change-remote-synced shape-inst container nil) - empty-changes) - (if set-remote-synced? - (change-remote-synced shape-inst container true) - empty-changes))))) + (change-touched shape-main + shape-inst + component-container + {:copy-touched? true}) + (concat-changes + (if clear-remote-synced? + (change-remote-synced shape-inst container nil) + empty-changes) + (if set-remote-synced? + (change-remote-synced shape-inst container true) + empty-changes))))) - children-inst (mapv #(cp/get-shape container %) - (:shapes shape-inst)) - children-main (mapv #(cp/get-shape component %) - (:shapes shape-main)) + children-inst (mapv #(cp/get-shape container %) + (:shapes shape-inst)) + children-main (mapv #(cp/get-shape component %) + (:shapes shape-main)) - only-inst (fn [child-inst] - (add-shape-to-main child-inst - (d/index-of children-inst - child-inst) - component - container - root-inst - root-main)) + only-inst (fn [child-inst] + (add-shape-to-main child-inst + (d/index-of children-inst + child-inst) + component + container + root-inst + root-main)) - only-main (fn [child-main] - (remove-shape child-main - component-container - false)) + only-main (fn [child-main] + (remove-shape child-main + component-container + false)) - both (fn [child-inst child-main] - (generate-sync-shape-inverse-recursive container - child-inst - component - child-main - root-inst - root-main - initial-root?)) + both (fn [child-inst child-main] + (generate-sync-shape-inverse-recursive container + child-inst + component + child-main + root-inst + root-main + initial-root?)) - moved (fn [child-inst child-main] - (move-shape - child-main - (d/index-of children-main child-main) - (d/index-of children-inst child-inst) - component-container - false)) + moved (fn [child-inst child-main] + (move-shape + child-main + (d/index-of children-main child-main) + (d/index-of children-inst child-inst) + component-container + false)) - [child-rchanges child-uchanges] - (compare-children children-inst - children-main - only-inst - only-main - both - moved - true) + [child-rchanges child-uchanges] + (compare-children children-inst + children-main + only-inst + only-main + both + moved + true) - ;; The inverse sync may be made on a component that is inside a - ;; remote library. We need to separate changes that are from - ;; local and remote files. - check-local (fn [change] - (cond-> change - (= (:id change) (:id shape-inst)) - (assoc :local-change? true))) + ;; The inverse sync may be made on a component that is inside a + ;; remote library. We need to separate changes that are from + ;; local and remote files. + check-local (fn [change] + (cond-> change + (= (:id change) (:id shape-inst)) + (assoc :local-change? true))) - rchanges (mapv check-local rchanges) - uchanges (mapv check-local uchanges)] + rchanges (mapv check-local rchanges) + uchanges (mapv check-local uchanges)] - [(d/concat rchanges child-rchanges) - (d/concat uchanges child-uchanges)])) + [(d/concat rchanges child-rchanges) + (d/concat uchanges child-uchanges)]))) ; ---- Operation generation helpers ---- diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 76609c41a9..9471a58287 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -326,13 +326,16 @@ (defn unlink-file-from-library [file-id library-id] (ptk/reify ::unlink-file-from-library + ptk/UpdateEvent + (update [_ state] + (d/dissoc-in state [:workspace-libraries library-id])) + ptk/WatchEvent (watch [_ _ _] - (let [unlinked #(d/dissoc-in % [:workspace-libraries library-id]) - params {:file-id file-id - :library-id library-id}] + (let [params {:file-id file-id + :library-id library-id}] (->> (rp/mutation :unlink-file-from-library params) - (rx/map (constantly unlinked))))))) + (rx/ignore)))))) ;; --- Upload File Media objects diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 386e8f3b9f..c04bfbedf6 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -257,8 +257,6 @@ (declare prepare-duplicate-frame-change) (declare prepare-duplicate-shape-change) -(def ^:private change->name #(get-in % [:obj :name])) - (defn update-indices "Fixes the indices for a set of changes after a duplication. We need to fix the indices to take into the account the movement of indices. @@ -290,19 +288,19 @@ "Prepare objects to paste: generate new id, give them unique names, move to the position of mouse pointer, and find in what frame they fit." - [objects page-id names ids delta] - (loop [names names - ids (seq ids) - chgs []] - (if ids - (let [id (first ids) - result (prepare-duplicate-change objects page-id names id delta) - result (if (vector? result) result [result])] - (recur - (into names (map change->name) result) - (next ids) - (into chgs result))) - chgs))) + [objects page-id unames ids delta] + (let [unames (volatile! unames) + update-unames! (fn [new-name] (vswap! unames conj new-name))] + (loop [ids (seq ids) + chgs []] + (if ids + (let [id (first ids) + result (prepare-duplicate-change objects page-id unames update-unames! id delta) + result (if (vector? result) result [result])] + (recur + (next ids) + (into chgs result))) + chgs)))) (defn duplicate-changes-update-indices "Parses the change set when duplicating to set-up the appropiate indices" @@ -317,32 +315,32 @@ (-> changes (update-indices index-map)))) (defn- prepare-duplicate-change - [objects page-id names id delta] + [objects page-id unames update-unames! id delta] (let [obj (get objects id)] (if (= :frame (:type obj)) - (prepare-duplicate-frame-change objects page-id names obj delta) - (prepare-duplicate-shape-change objects page-id names obj delta (:frame-id obj) (:parent-id obj))))) + (prepare-duplicate-frame-change objects page-id unames update-unames! obj delta) + (prepare-duplicate-shape-change objects page-id unames update-unames! obj delta (:frame-id obj) (:parent-id obj))))) (defn- prepare-duplicate-shape-change - [objects page-id names obj delta frame-id parent-id] + [objects page-id unames update-unames! obj delta frame-id parent-id] (when (some? obj) (let [id (uuid/next) - name (dwc/generate-unique-name names (:name obj)) + name (dwc/generate-unique-name @unames (:name obj)) + _ (update-unames! name) + renamed-obj (assoc obj :id id :name name) moved-obj (geom/move renamed-obj delta) parent-id (or parent-id frame-id) children-changes - (loop [names names - result [] + (loop [result [] cid (first (:shapes obj)) cids (rest (:shapes obj))] (if (nil? cid) result (let [obj (get objects cid) - changes (prepare-duplicate-shape-change objects page-id names obj delta frame-id id)] + changes (prepare-duplicate-shape-change objects page-id unames update-unames! obj delta frame-id id)] (recur - (into names (map change->name changes)) (into result changes) (first cids) (rest cids))))) @@ -361,11 +359,13 @@ children-changes)))) (defn- prepare-duplicate-frame-change - [objects page-id names obj delta] + [objects page-id unames update-unames! obj delta] (let [frame-id (uuid/next) - frame-name (dwc/generate-unique-name names (:name obj)) + frame-name (dwc/generate-unique-name @unames (:name obj)) + _ (update-unames! frame-name) + sch (->> (map #(get objects %) (:shapes obj)) - (mapcat #(prepare-duplicate-shape-change objects page-id names % delta frame-id frame-id))) + (mapcat #(prepare-duplicate-shape-change objects page-id unames update-unames! % delta frame-id frame-id))) frame (-> obj (assoc :id frame-id) diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index 048e4bb965..49942812a2 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -7,10 +7,13 @@ (ns app.main.data.workspace.svg-upload (:require [app.common.data :as d] + [app.common.exceptions :as ex] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] + [app.common.math :as mth] [app.common.pages :as cp] + [app.common.spec :refer [max-safe-int min-safe-int]] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] @@ -28,13 +31,36 @@ (defonce default-circle {:r 0 :cx 0 :cy 0}) (defonce default-image {:x 0 :y 0 :width 1 :height 1}) +(defn- assert-valid-num [attr num] + (when (or (nil? num) + (mth/nan? num) + (not (mth/finite? num)) + (>= num max-safe-int ) + (<= num min-safe-int)) + (ex/raise (str (d/name attr) " attribute invalid: " num))) + + ;; If the number is between 0-1 we round to 1 (same in negative form + (cond + (and (> num 0) (< num 1)) 1 + (and (< num 0) (> num -1)) -1 + :else num)) + +(defn- assert-valid-pos-num [attr num] + (let [num (assert-valid-num attr num)] + (when (< num 0) + (ex/raise (str (d/name attr) " attribute invalid: " num))) + num)) + (defn- svg-dimensions [data] (let [width (get-in data [:attrs :width] 100) height (get-in data [:attrs :height] 100) viewbox (get-in data [:attrs :viewBox] (str "0 0 " width " " height)) - [x y width height] (->> (str/split viewbox " ") + [x y width height] (->> (str/split viewbox #"\s+") (map d/parse-double))] - [x y width height])) + [(assert-valid-num :x x) + (assert-valid-num :y y) + (assert-valid-pos-num :width width) + (assert-valid-pos-num :height height)])) (defn tag->name "Given a tag returns its layer name" @@ -467,4 +493,6 @@ (dwc/select-shapes (d/ordered-set root-id)))) (catch :default e - (.error js/console "Error upload" e)))))) + (.error js/console "Error SVG" e) + (rx/throw {:type :svg-parser + :data e})))))) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index f928c01cec..841bf01819 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -67,19 +67,24 @@ (rx/of (dch/update-shapes [id] #(assoc % :content content)) (dwu/commit-undo-transaction))))) - (rx/of (dws/deselect-shape id) - (dwc/delete-shapes #{id}))))))) + + (when (some? id) + (rx/of (dws/deselect-shape id) + (dwc/delete-shapes #{id})))))))) (defn initialize-editor-state [{:keys [id content] :as shape} decorator] (ptk/reify ::initialize-editor-state ptk/UpdateEvent (update [_ state] - (update-in state [:workspace-editor-state id] - (fn [_] - (ted/create-editor-state - (some->> content ted/import-content) - decorator)))) + (let [text-state (some->> content ted/import-content) + attrs (get-in state [:workspace-local :defaults :font]) + + editor (cond-> (ted/create-editor-state text-state decorator) + (and (nil? content) (some? attrs)) + (ted/update-editor-current-block-data attrs))] + (-> state + (assoc-in [:workspace-editor-state id] editor)))) ptk/WatchEvent (watch [_ _ stream] @@ -101,6 +106,13 @@ (update [_ state] (d/update-in-when state [:workspace-editor-state id] ted/editor-select-all)))) +(defn cursor-to-end + [{:keys [id] :as shape}] + (ptk/reify ::cursor-to-end + ptk/UpdateEvent + (update [_ state] + (d/update-in-when state [:workspace-editor-state id] ted/cursor-to-end)))) + ;; --- Helpers (defn- shape-current-values @@ -193,8 +205,11 @@ (when-not (some? (get-in state [:workspace-editor-state id])) (let [objects (wsh/lookup-page-objects state) shape (get objects id) + update-node? (fn [node] + (or (txt/is-text-node? node) + (txt/is-paragraph-node? node))) - update-fn #(update-shape % txt/is-text-node? attrs/merge attrs) + update-fn #(update-shape % update-node? attrs/merge attrs) shape-ids (cond (= (:type shape) :text) [id] (= (:type shape) :group) (cp/get-children id objects))] (rx/of (dch/update-shapes shape-ids update-fn))))))) @@ -309,3 +324,14 @@ (rx/race resize-batch change-page) (rx/of #(dissoc % ::handling-texts)))) (rx/empty)))))) + +(defn save-font + [data] + (ptk/reify ::save-font + ptk/UpdateEvent + (update [_ state] + (let [multiple? (->> data vals (d/seek #(= % :multiple)))] + (cond-> state + (not multiple?) + (assoc-in [:workspace-local :defaults :font] data)))))) + diff --git a/frontend/src/app/main/ui/components/editable_select.cljs b/frontend/src/app/main/ui/components/editable_select.cljs index 40c13e7702..127e0e403e 100644 --- a/frontend/src/app/main/ui/components/editable_select.cljs +++ b/frontend/src/app/main/ui/components/editable_select.cljs @@ -14,19 +14,23 @@ [app.util.timers :as timers] [rumext.alpha :as mf])) -(mf/defc editable-select [{:keys [value type options class on-change placeholder]}] +(mf/defc editable-select [{:keys [value type options class on-change placeholder on-blur]}] (let [state (mf/use-state {:id (uuid/next) :is-open? false :current-value value :top nil :left nil :bottom nil}) + + emit-blur? (mf/use-ref nil) + open-dropdown #(swap! state assoc :is-open? true) close-dropdown #(swap! state assoc :is-open? false) select-item (fn [value] (fn [_] (swap! state assoc :current-value value) - (when on-change (on-change value)))) + (when on-change (on-change value)) + (when on-blur (on-blur)))) as-key-value (fn [item] (if (map? item) [(:value item) (:label item)] [item item])) @@ -55,21 +59,38 @@ assoc :left left :top top - :bottom bottom))))))] + :bottom bottom)))))) + + handle-focus + (mf/use-callback + (fn [] + (mf/set-ref-val! emit-blur? false))) + + handle-blur + (mf/use-callback + (fn [] + (mf/set-ref-val! emit-blur? true) + (timers/schedule + 200 + (fn [] + (when (and on-blur (mf/ref-val emit-blur?)) (on-blur))))))] (mf/use-effect - (mf/deps value) - #(reset! state {:current-value value})) + (mf/deps value (:current-value @state)) + #(when (not= value (:current-value @state)) + (reset! state {:current-value value}))) (mf/use-effect - (mf/deps options) - #(reset! state {:is-open? false - :current-value value})) + (mf/deps (:is-open? @state)) + (fn [] + (mf/set-ref-val! emit-blur? (not (:is-open? @state))))) [:div.editable-select {:class class :ref on-node-load} [:input.input-text {:value (or (-> @state :current-value value->label) "") :on-change handle-change-input + :on-focus handle-focus + :on-blur handle-blur :placeholder placeholder :type type}] [:span.dropdown-button {:on-click open-dropdown} i/arrow-down] diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 9d247e13fd..ec76c8f610 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -47,8 +47,8 @@ text-align (:text-align data "start") grow-type (:grow-type shape) - base #js {:fontSize (str (:font-size txt/default-text-attrs) "px") - :lineHeight (:line-height txt/default-text-attrs) + base #js {:fontSize (str (:font-size data (:font-size txt/default-text-attrs)) "px") + :lineHeight (:line-height data (:line-height txt/default-text-attrs)) :margin "inherit"}] (cond-> base (some? line-height) (obj/set! "lineHeight" line-height) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 2086412dfb..a4e3f03558 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -8,6 +8,7 @@ (:require [app.main.data.messages :as dm] [app.main.data.workspace :as dw] + [app.main.data.workspace.persistence :as dwp] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.context :as ctx] @@ -25,6 +26,7 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.object :as obj] + [app.util.timers :as ts] [okulary.core :as l] [rumext.alpha :as mf])) @@ -122,7 +124,10 @@ (mf/deps project-id file-id) (fn [] (st/emit! (dw/initialize-file project-id file-id)) - (st/emitf (dw/finalize-file project-id file-id)))) + (fn [] + ;; Schedule to 100ms so we can do the update before the file is finalized + (st/emit! ::dwp/force-persist) + (ts/schedule 100 (st/emitf (dw/finalize-file project-id file-id)))))) (mf/use-effect (fn [] diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index f48d73b454..b9aa0dbc65 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -75,7 +75,11 @@ (mf/use-callback (mf/deps file) #(st/emit! (dw/link-file-to-library (:id file) %))) unlink-library - (mf/use-callback (mf/deps file) #(st/emit! (dw/unlink-file-from-library (:id file) %)))] + (mf/use-callback + (mf/deps file) + (fn [library-id] + (st/emit! (dw/unlink-file-from-library (:id file) library-id) + (dwl/sync-file (:id file) library-id))))] [:* [:div.section [:div.section-title (tr "workspace.libraries.in-this-file")] diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs index 2859344226..db9d70aad0 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.shapes.text.editor (:require ["draft-js" :as draft] + [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.text :as txt] [app.main.data.workspace :as dw] @@ -32,7 +33,7 @@ (let [bprops (obj/get props "blockProps") data (obj/get bprops "data") style (sts/generate-paragraph-styles (obj/get bprops "shape") - (obj/get bprops "data")) + (obj/get bprops "data")) dir (:text-direction data "auto")] @@ -56,12 +57,36 @@ :shape shape}} nil))) +(defn styles-fn [styles content] + (if (= (.getText content) "") + (-> (.getData content) + (.toJS) + (js->clj :keywordize-keys true) + (sts/generate-text-styles)) + (-> (txt/styles-to-attrs styles) + (sts/generate-text-styles)))) + (def default-decorator (ted/create-decorator "PENPOT_SELECTION" selection-component)) (def empty-editor-state (ted/create-editor-state nil default-decorator)) +(defn get-content-changes + [old-state state] + (let [old-blocks (js->clj (.toJS (.getBlockMap (.getCurrentContent ^js old-state))) + :keywordize-keys false) + new-blocks (js->clj (.toJS (.getBlockMap (.getCurrentContent ^js state))) + + :keywordize-keys false)] + (->> old-blocks + (d/mapm + (fn [bkey bstate] + {:old (get bstate "text") + :new (get-in new-blocks [bkey "text"])})) + (filter #(contains? new-blocks (first %))) + (into {})))) + (mf/defc text-shape-edit-html {::mf/wrap [mf/memo] ::mf/wrap-props false @@ -106,10 +131,35 @@ (fn [_] (reset! blured false))) + prev-value (mf/use-ref state) + + ;; Effect that keeps updated the `prev-value` reference + _ (mf/use-effect + (mf/deps state) + #(mf/set-ref-val! prev-value state)) + + handle-change + (mf/use-callback + (fn [state] + (let [old-state (mf/ref-val prev-value)] + (if (and (some? state) (some? old-state)) + (let [block-states (get-content-changes old-state state) + + block-to-add-styles + (->> block-states + (filter + (fn [[_ v]] + (and (not= (:old v) (:new v)) + (= (:old v) "")))) + (mapv first))] + (ted/apply-block-styles-to-content state block-to-add-styles)) + state)))) + on-change (mf/use-callback (fn [val] - (let [val (if (true? @blured) + (let [val (handle-change val) + val (if (true? @blured) (ted/add-editor-blur-selection val) (ted/remove-editor-blur-selection val))] (st/emit! (dwt/update-editor-state shape val))))) @@ -124,9 +174,27 @@ handle-return (mf/use-callback (fn [_ state] - (st/emit! (dwt/update-editor-state shape (ted/editor-split-block state))) + (let [style (ted/get-editor-current-inline-styles state) + state (-> (ted/insert-text state "\n" style) + (handle-change))] + (st/emit! (dwt/update-editor-state shape state))) "handled")) - ] + + on-click + (mf/use-callback + (fn [event] + (when (dom/class? (dom/get-target event) "DraftEditor-root") + (st/emit! (dwt/cursor-to-end shape))) + (st/emit! (dwt/focus-editor)))) + + handle-pasted-text + (fn [text _ _] + (let [style (ted/get-editor-current-inline-styles state) + state (-> (ted/insert-text state text style) + (handle-change))] + (st/emit! (dwt/update-editor-state shape state))) + + "handled")] (mf/use-layout-effect on-mount) @@ -135,7 +203,7 @@ :style {:cursor cur/text :width (:width shape) :height (:height shape)} - :on-click (st/emitf (dwt/focus-editor)) + :on-click on-click :class (dom/classnames :align-top (= (:vertical-align content "top") "top") :align-center (= (:vertical-align content) "center") @@ -146,9 +214,8 @@ :on-focus on-focus :handle-return handle-return :strip-pasted-styles true - :custom-style-fn (fn [styles _] - (-> (txt/styles-to-attrs styles) - (sts/generate-text-styles))) + :handle-pasted-text handle-pasted-text + :custom-style-fn styles-fn :block-renderer-fn #(render-block % shape) :ref on-editor :editor-state state}]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index d9f9ec455b..5f55a9ab2e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -20,6 +20,7 @@ [app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry typography-options]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] + [app.util.timers :as tm] [cuerdas.core :as str] [rumext.alpha :as mf])) @@ -80,11 +81,12 @@ (def attrs (d/concat #{} shape-attrs root-attrs paragraph-attrs text-attrs)) (mf/defc text-align-options - [{:keys [values on-change] :as props}] + [{:keys [values on-change on-blur] :as props}] (let [{:keys [text-align]} values handle-change (fn [_ new-align] - (on-change {:text-align new-align}))] + (on-change {:text-align new-align}) + (when (some? on-blur) (on-blur)))] ;; --- Align [:div.align-icons @@ -110,10 +112,12 @@ i/text-align-justify]])) (mf/defc text-direction-options - [{:keys [values on-change] :as props}] + [{:keys [values on-change on-blur] :as props}] (let [direction (:text-direction values) - handle-change (fn [_ val] - (on-change {:text-direction val}))] + handle-change + (fn [_ val] + (on-change {:text-direction val}) + (when (some? on-blur) (on-blur)))] ;; --- Align [:div.align-icons [:span.tooltip.tooltip-bottom-left @@ -128,12 +132,13 @@ i/text-direction-rtl]])) (mf/defc vertical-align - [{:keys [values on-change] :as props}] + [{:keys [values on-change on-blur] :as props}] (let [{:keys [vertical-align]} values vertical-align (or vertical-align "top") handle-change (fn [_ new-align] - (on-change {:vertical-align new-align}))] + (on-change {:vertical-align new-align}) + (when (some? on-blur) (on-blur)))] [:div.align-icons [:span.tooltip.tooltip-bottom-left @@ -153,11 +158,12 @@ i/align-bottom]])) (mf/defc grow-options - [{:keys [ids values] :as props}] + [{:keys [ids values on-blur] :as props}] (let [grow-type (:grow-type values) handle-change-grow (fn [_ grow-type] - (st/emit! (dch/update-shapes ids #(assoc % :grow-type grow-type))))] + (st/emit! (dch/update-shapes ids #(assoc % :grow-type grow-type))) + (when (some? on-blur) (on-blur)))] [:div.align-icons [:span.tooltip.tooltip-bottom @@ -177,11 +183,12 @@ i/auto-height]])) (mf/defc text-decoration-options - [{:keys [values on-change] :as props}] + [{:keys [values on-change on-blur] :as props}] (let [text-decoration (or (:text-decoration values) "none") handle-change (fn [_ type] - (on-change {:text-decoration type}))] + (on-change {:text-decoration type}) + (when (some? on-blur) (on-blur)))] [:div.align-icons [:span.tooltip.tooltip-bottom {:alt (tr "workspace.options.text-options.none") @@ -220,7 +227,11 @@ emit-update! (mf/use-callback + (mf/deps values) (fn [id attrs] + (st/emit! (dwt/save-font (-> (merge txt/default-text-attrs values attrs) + (select-keys text-attrs)))) + (let [attrs (select-keys attrs root-attrs)] (when-not (empty? attrs) (st/emit! (dwt/update-root-attrs {:id id :attrs attrs})))) @@ -235,7 +246,7 @@ on-change (mf/use-callback - (mf/deps ids) + (mf/deps ids emit-update!) (fn [attrs] (run! #(emit-update! % attrs) ids))) @@ -287,7 +298,15 @@ opts #js {:ids ids :values values - :on-change on-change}] + :on-change on-change + :on-blur + (fn [] + (tm/schedule + 100 + (fn [] + (when (not= "INPUT" (-> (dom/get-active) (dom/get-tag-name))) + (let [node (dom/get-element-by-class "public-DraftEditor-content")] + (dom/focus! node))))))}] [:div.element-set [:div.element-set-title diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index ca5e77631a..6af685f846 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -230,7 +230,7 @@ :current? (= (:id font) (:id selected))}]))) (mf/defc font-options - [{:keys [values on-change] :as props}] + [{:keys [values on-change on-blur] :as props}] (let [{:keys [font-id font-size font-variant-id]} values font-id (or font-id (:font-id txt/default-text-attrs)) @@ -271,18 +271,26 @@ :font-family (:family font) :font-variant-id new-variant-id :font-weight (:weight variant) - :font-style (:style variant)})))) + :font-style (:style variant)}) + (dom/blur! (dom/get-target event))))) on-font-select (mf/use-callback (mf/deps change-font) (fn [font*] (when (not= font font*) - (change-font (:id font*))))) + (change-font (:id font*))) + + (when (some? on-blur) + (on-blur)))) on-font-selector-close (mf/use-callback - #(reset! open-selector? false))] + (fn [] + (reset! open-selector? false) + (when (some? on-blur) + (on-blur)) + ))] [:* (when @open-selector? @@ -314,12 +322,14 @@ :options size-options :type "number" :placeholder "--" - :on-change on-font-size-change}]) + :on-change on-font-size-change + :on-blur on-blur}]) [:select.input-select.variant-option {:disabled (= font-id :multiple) :value (attr->string font-variant-id) - :on-change on-font-variant-change} + :on-change on-font-variant-change + :on-blur on-blur} (when (or (= font-id :multiple) (= font-variant-id :multiple)) [:option {:value ""} "--"]) (for [variant (:variants font)] @@ -329,7 +339,7 @@ (mf/defc spacing-options - [{:keys [values on-change] :as props}] + [{:keys [values on-change on-blur] :as props}] (let [{:keys [line-height letter-spacing]} values @@ -353,7 +363,8 @@ :max "200" :value (attr->string line-height) :placeholder (tr "settings.multiple") - :on-change #(handle-change % :line-height)}]] + :on-change #(handle-change % :line-height) + :on-blur on-blur}]] [:div.input-icon [:span.icon-before.tooltip.tooltip-bottom @@ -366,18 +377,21 @@ :max "200" :value (attr->string letter-spacing) :placeholder (tr "settings.multiple") - :on-change #(handle-change % :letter-spacing)}]]])) + :on-change #(handle-change % :letter-spacing) + :on-blur on-blur}]]])) (mf/defc text-transform-options - [{:keys [values on-change] :as props}] + [{:keys [values on-change on-blur] :as props}] (let [text-transform (or (:text-transform values) "none") handle-change (fn [_ type] - (on-change {:text-transform type}))] + (on-change {:text-transform type}) + (when (some? on-blur) (on-blur)))] [:div.align-icons [:span.tooltip.tooltip-bottom {:alt (tr "workspace.options.text-options.none") :class (dom/classnames :current (= "none" text-transform)) + :on-focus #(dom/prevent-default %) :on-click #(handle-change % "none")} i/minus] [:span.tooltip.tooltip-bottom @@ -397,11 +411,12 @@ i/titlecase]])) (mf/defc typography-options - [{:keys [ids editor values on-change]}] + [{:keys [ids editor values on-change on-blur]}] (let [opts #js {:editor editor :ids ids :values values - :on-change on-change}] + :on-change on-change + :on-blur on-blur}] [:div.element-set-content [:> font-options opts] [:div.row-flex diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs index 2989834322..a8eccfa4d5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs @@ -11,7 +11,7 @@ [app.main.refs :as refs] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] - [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-menu fill-attrs]] [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] @@ -42,6 +42,7 @@ text-values (d/merge (select-keys shape [:grow-type]) + (select-keys shape fill-attrs) (dwt/current-root-values {:shape shape :attrs root-attrs}) diff --git a/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs b/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs index f6d85e32cf..1aef9a82f7 100644 --- a/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs @@ -108,14 +108,14 @@ ;; Draw Mode [:div.viewport-actions-entry.tooltip.tooltip-bottom {:class (when (= edit-mode :draw) "is-toggled") - :alt (tr "workspace.path.actions.move-nodes" (sc/get-tooltip :move-nodes)) + :alt (tr "workspace.path.actions.draw-nodes" (sc/get-tooltip :draw-nodes)) :on-click on-select-draw-mode} i/pen] ;; Edit mode [:div.viewport-actions-entry.tooltip.tooltip-bottom {:class (when (= edit-mode :move) "is-toggled") - :alt (tr "workspace.path.actions.draw-nodes" (sc/get-tooltip :draw-nodes)) + :alt (tr "workspace.path.actions.move-nodes" (sc/get-tooltip :move-nodes)) :on-click on-select-edit-mode} i/pointer-inner]] diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 1fe2dce1db..9ca6c54a11 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -95,10 +95,12 @@ on-mouse-down (mf/use-callback (mf/deps (:id frame) on-frame-select) - (fn [event] - (dom/prevent-default event) - (dom/stop-propagation event) - (on-frame-select event (:id frame)))) + (fn [bevent] + (let [event (.-nativeEvent bevent)] + (when (= 1 (.-which event)) + (dom/prevent-default event) + (dom/stop-propagation event) + (on-frame-select event (:id frame)))))) on-double-click (mf/use-callback diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index f05f2a1a5f..9cf48d13de 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -98,7 +98,7 @@ (defn get-attribute "Extract the value of one attribute of a dom node." [node attr-name] - (.getAttribute node attr-name)) + (.getAttribute ^js node attr-name)) (def get-target-val (comp get-value get-target)) @@ -223,11 +223,13 @@ (defn focus! [node] - (.focus node)) + (when (some? node) + (.focus node))) (defn blur! [node] - (.blur node)) + (when (some? node) + (.blur node))) (defn fullscreen? [] @@ -291,8 +293,11 @@ (defn get-user-agent [] (.-userAgent globals/navigator)) +(defn get-active [] + (.-activeElement globals/document)) + (defn active? [node] - (= (.-activeElement globals/document) node)) + (= (get-active) node)) (defn get-data [^js node ^string attr] (.getAttribute node (str "data-" attr))) @@ -377,5 +382,5 @@ (trigger-download-uri filename mtype uri))) (defn left-mouse? [bevent] - (let [event (.-nativeEvent bevent)] + (let [event (.-nativeEvent ^js bevent)] (= 1 (.-which event)))) diff --git a/frontend/src/app/util/text_editor.cljs b/frontend/src/app/util/text_editor.cljs index 370f17f2de..06738d20ef 100644 --- a/frontend/src/app/util/text_editor.cljs +++ b/frontend/src/app/util/text_editor.cljs @@ -70,8 +70,11 @@ (defn get-editor-current-inline-styles [state] - (-> (.getCurrentInlineStyle ^js state) - (txt/styles-to-attrs))) + (if (impl/isCurrentEmpty state) + (let [block (impl/getCurrentBlock state)] + (get-editor-block-data block)) + (-> (.getCurrentInlineStyle ^js state) + (txt/styles-to-attrs)))) (defn update-editor-current-block-data [state attrs] @@ -79,7 +82,18 @@ (defn update-editor-current-inline-styles [state attrs] - (impl/applyInlineStyle state (txt/attrs-to-styles attrs))) + (let [update-blocks + (fn [state block-key] + (if (empty? (impl/getBlockContent state block-key)) + (impl/updateBlockData state block-key (clj->js attrs)) + + (let [attrs (-> (impl/getInlineStyle state block-key 0) + (txt/styles-to-attrs))] + (impl/updateBlockData state block-key (clj->js attrs))))) + + state (impl/applyInlineStyle state (txt/attrs-to-styles attrs)) + selected (impl/getSelectedBlocks state)] + (reduce update-blocks state selected))) (defn editor-split-block [state] @@ -92,3 +106,27 @@ (defn remove-editor-blur-selection [state] (impl/removeBlurSelectionEntity state)) + +(defn cursor-to-end + [state] + (impl/cursorToEnd state)) + +(defn apply-block-styles-to-content + [state blocks] + (if (empty? blocks) + state + (let [selection (impl/getSelection state) + redfn + (fn [state bkey] + (let [attrs (-> (impl/getBlockData state bkey) + (js->clj :keywordize-keys true))] + (-> state + (impl/selectBlock bkey) + (impl/applyInlineStyle (txt/attrs-to-styles attrs)))))] + (as-> state $ + (reduce redfn $ blocks) + (impl/setSelection $ selection))))) + +(defn insert-text [state text attrs] + (let [style (txt/attrs-to-styles attrs)] + (impl/insertText state text (clj->js attrs) (clj->js style)))) diff --git a/frontend/src/app/util/text_editor_impl.js b/frontend/src/app/util/text_editor_impl.js index 9255ec621b..92b01533bf 100644 --- a/frontend/src/app/util/text_editor_impl.js +++ b/frontend/src/app/util/text_editor_impl.js @@ -9,19 +9,39 @@ 'use strict'; import { + BlockMapBuilder, CharacterMetadata, - EditorState, CompositeDecorator, + EditorState, + Modifier, + RichTextEditorUtil, SelectionState, - Modifier } from "draft-js"; -import {Map} from "immutable"; +import DraftPasteProcessor from 'draft-js/lib/DraftPasteProcessor'; +import {Map, OrderedSet} from "immutable"; function isDefined(v) { return v !== undefined && v !== null; } +function mergeBlockData(block, newData) { + let data = block.getData(); + + for (let key of Object.keys(newData)) { + const oldVal = data.get(key); + if (oldVal === newData[key]) { + data = data.delete(key); + } else { + data = data.set(key, newData[key]); + } + } + + return block.mergeDeep({ + data: data + }); +} + export function createEditorState(content, decorator) { if (content === null) { return EditorState.createEmpty(decorator); @@ -56,6 +76,18 @@ function getSelectAllSelection(state) { }); } +function getCursorInEndPosition(state) { + const content = state.getCurrentContent(); + const lastBlock = content.getBlockMap().last(); + + return new SelectionState({ + "anchorKey": lastBlock.getKey(), + "anchorOffset": lastBlock.getLength(), + "focusKey": lastBlock.getKey(), + "focusOffset": lastBlock.getLength() + }); +} + export function selectAll(state) { return EditorState.forceSelection(state, getSelectAllSelection(state)); } @@ -83,43 +115,38 @@ export function updateCurrentBlockData(state, attrs) { let content = state.getCurrentContent(); content = modifySelectedBlocks(content, selection, (block) => { - let data = block.getData(); - for (let key of Object.keys(attrs)) { - const oldVal = data.get(key); - if (oldVal === attrs[key]) { - data = data.delete(key); - } else { - data = data.set(key, attrs[key]); - } - } - - return block.merge({ - data: data - }); + return mergeBlockData(block, attrs); }); return EditorState.push(state, content, "change-block-data"); } export function applyInlineStyle(state, styles) { - const selection = state.getSelection(); + const userSelection = state.getSelection(); + let selection = userSelection; + + if (selection.isCollapsed()) { + selection = getSelectAllSelection(state); + } + + let result = state; let content = null; for (let style of styles) { const [p, k, v] = style.split("$$$"); const prefix = [p, k, ""].join("$$$"); - content = state.getCurrentContent(); + content = result.getCurrentContent(); content = removeInlineStylePrefix(content, selection, prefix); if (v !== "z:null") { content = Modifier.applyInlineStyle(content, selection, style); } - state = EditorState.push(state, content, "change-inline-style"); + result = EditorState.push(result, content, "change-inline-style"); } - return state; + return EditorState.acceptSelection(result, userSelection); } export function splitBlockPreservingData(state) { @@ -209,3 +236,143 @@ export function removeInlineStylePrefix(contentState, selectionState, stylePrefi return block.set("characterList", chars); }); } + +export function cursorToEnd(state) { + const newSelection = getCursorInEndPosition(state); + const selection = state.getSelection(); + + let content = state.getCurrentContent(); + content = Modifier.applyEntity(content, newSelection, null); + + state = EditorState.forceSelection(state, newSelection); + state = EditorState.push(state, content, "apply-entity"); + + return state; +} + +export function isCurrentEmpty(state) { + const selection = state.getSelection(); + + if (!selection.isCollapsed()) { + return false; + } + + const blockKey = selection.getStartKey(); + const content = state.getCurrentContent(); + + const block = content.getBlockForKey(blockKey); + + return block.getText() === ""; +} + +/* + Returns the block keys between a selection +*/ +export function getSelectedBlocks(state) { + const selection = state.getSelection(); + const startKey = selection.getStartKey(); + const endKey = selection.getEndKey(); + const content = state.getCurrentContent(); + const result = [ startKey ]; + + let currentKey = startKey; + + while (currentKey !== endKey) { + const currentBlock = content.getBlockAfter(currentKey); + currentKey = currentBlock.getKey(); + result.push(currentKey); + } + + return result; +} + +export function getBlockContent(state, blockKey) { + const content = state.getCurrentContent(); + const block = content.getBlockForKey(blockKey); + return block.getText(); +} + +export function getBlockData(state, blockKey) { + const content = state.getCurrentContent(); + const block = content.getBlockForKey(blockKey); + return block && block.getData().toJS(); +} + +export function updateBlockData(state, blockKey, data) { + const userSelection = state.getSelection(); + const content = state.getCurrentContent(); + const block = content.getBlockForKey(blockKey); + const newBlock = mergeBlockData(block, data); + + const blockData = newBlock.getData(); + + const newContent = Modifier.setBlockData( + state.getCurrentContent(), + SelectionState.createEmpty(blockKey), + blockData + ); + + const result = EditorState.push(state, newContent, 'change-block-data'); + return EditorState.acceptSelection(result, userSelection); +} + +export function getSelection(state) { + return state.getSelection(); +} + +export function setSelection(state, selection) { + return EditorState.acceptSelection(state, selection); +} + +export function selectBlock(state, blockKey) { + const block = state.getCurrentContent().getBlockForKey(blockKey); + const length = block.getText().length; + const selection = SelectionState.createEmpty(blockKey).merge({ + focusOffset: length + }); + return EditorState.acceptSelection(state, selection); +} + +export function getInlineStyle(state, blockKey, offset) { + const content = state.getCurrentContent(); + const block = content.getBlockForKey(blockKey); + return block.getInlineStyleAt(offset).toJS(); +} + +const NEWLINE_REGEX = /\r\n?|\n/g; + +function splitTextIntoTextBlocks(text) { + return text.split(NEWLINE_REGEX); +} + +export function insertText(state, text, attrs, inlineStyles) { + const blocks = splitTextIntoTextBlocks(text); + + const character = CharacterMetadata.create({style: OrderedSet(inlineStyles)}); + + let blockArray = DraftPasteProcessor.processText( + blocks, + character, + "unstyled", + ); + + blockArray = blockArray.map((b) => { + if (b.getText() === "") { + return mergeBlockData(b, attrs) + } + return b; + }); + + const fragment = BlockMapBuilder.createFromArray(blockArray); + const content = state.getCurrentContent(); + const selection = state.getSelection(); + + const newContent = Modifier.replaceWithFragment( + content, + selection, + fragment + ); + + const resultSelection = SelectionState.createEmpty(selection.getStartKey()); + return EditorState.push(state, newContent, 'insert-fragment'); +} diff --git a/frontend/test/app/components_basic_test.cljs b/frontend/test/app/components_basic_test.cljs index 702b36cadd..e4ecfca84e 100644 --- a/frontend/test/app/components_basic_test.cljs +++ b/frontend/test/app/components_basic_test.cljs @@ -89,11 +89,11 @@ ;; NOTE: the group name depends on having executed ;; the previous test. - (t/is (= (:name group) "Component-2")) + (t/is (= (:name group) "Component-1")) (t/is (= (:name shape1) "Rect 1")) (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:name component) "Component-2")) - (t/is (= (:name c-group) "Component-2")) + (t/is (= (:name component) "Component-1")) + (t/is (= (:name c-group) "Component-1")) (t/is (= (:name c-shape1) "Rect 1")) (t/is (= (:name c-shape2) "Rect 2")) @@ -139,11 +139,11 @@ (t/is (= (:name shape1) "Rect 1")) (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:name group) "Group-3")) - (t/is (= (:name component) "Group-3")) + (t/is (= (:name group) "Group-1")) + (t/is (= (:name component) "Group-1")) (t/is (= (:name c-shape1) "Rect 1")) (t/is (= (:name c-shape2) "Rect 2")) - (t/is (= (:name c-group) "Group-3")) + (t/is (= (:name c-group) "Group-1")) (thl/is-from-file group file)))) @@ -234,7 +234,7 @@ new-component-id)] (t/is (= (:name component2) - "Component-6"))))) + "Component-2"))))) (rx/subs done @@ -322,9 +322,9 @@ (t/is (not= (:id instance1) (:id instance2))) (t/is (= (:id component) component-id)) - (t/is (= (:name instance2) "Component-8")) + (t/is (= (:name instance2) "Component-2")) (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-instance2) "Component-7")) + (t/is (= (:name c-instance2) "Component-1")) (t/is (= (:name c-shape2) "Rect 1"))))) (rx/subs diff --git a/frontend/test/app/test_helpers/pages.cljs b/frontend/test/app/test_helpers/pages.cljs index 4ea7265aac..d4f0ab03b1 100644 --- a/frontend/test/app/test_helpers/pages.cljs +++ b/frontend/test/app/test_helpers/pages.cljs @@ -10,6 +10,7 @@ [app.common.pages :as cp] [app.common.pages.helpers :as cph] [app.main.data.workspace :as dw] + [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.libraries-helpers :as dwlh])) @@ -79,7 +80,7 @@ :obj shape}])))) (defn group-shapes - ([state label ids] (group-shapes state label ids "Group-")) + ([state label ids] (group-shapes state label ids "Group")) ([state label ids prefix] (let [page (current-page state) shapes (dwg/shapes-for-grouping (:objects page) ids)] @@ -94,9 +95,9 @@ (defn make-component [state label ids] - (let [page (current-page state) - objects (wsh/lookup-page-objects state page-id) - shapes (dwg/shapes-for-grouping objects ids) + (let [page (current-page state) + objects (wsh/lookup-page-objects state (:id page)) + shapes (dwg/shapes-for-grouping objects ids) [group rchanges uchanges] (dwlh/generate-add-component shapes diff --git a/frontend/yarn.lock b/frontend/yarn.lock index a0ac4205e6..ca1890d131 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4258,10 +4258,10 @@ shadow-cljs-jar@1.3.2: resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b" integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg== -shadow-cljs@2.15.1: - version "2.15.1" - resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.15.1.tgz#9f4b642efafeb84480396f46af2a8e59351d5986" - integrity sha512-X0ueBJksdBg5FIuFOFguyZtAP9gzZZI6lmednxQ/eOsN9tGhpTXh5Y8/7lGzkfIFXxONe9THZx4f2m4JX5jBYA== +shadow-cljs@2.15.2: + version "2.15.2" + resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.15.2.tgz#195ad2cc45d3334920e629721f06c6d63802b1ac" + integrity sha512-WPlSMkGgbU5b2nrt+Y1A1TsPs5Rip/JvCxGG2t2Pvzo+pLJ+RcpkZgAxjNQNNA7VYWEh5Pqwyvq5KzQ+0LMsxw== dependencies: node-libs-browser "^2.2.1" readline-sync "^1.4.7" diff --git a/version.txt b/version.txt index 075c573674..e6bff99f22 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.7.1-alpha +1.7.2-alpha