diff --git a/backend/deps.edn b/backend/deps.edn index 0b4e5180a4..5bf32ab958 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -21,8 +21,8 @@ java-http-clj/java-http-clj {:mvn/version "0.4.3"} funcool/yetti - {:git/tag "v9.15" - :git/sha "aa9b967" + {:git/tag "v9.16" + :git/sha "7df3e08" :git/url "https://github.com/funcool/yetti.git" :exclusions [org.slf4j/slf4j-api]} diff --git a/backend/resources/app/templates/error-report.v3.tmpl b/backend/resources/app/templates/error-report.v3.tmpl index 216c4d3ebb..b36a97e499 100644 --- a/backend/resources/app/templates/error-report.v3.tmpl +++ b/backend/resources/app/templates/error-report.v3.tmpl @@ -71,7 +71,7 @@ penpot - error report v2 {{id}} {% if value %}
-
VALIDATION VALUE:
+
VALUE:
{{value}}
diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index b4ff0c8ab2..da99552935 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -264,7 +264,7 @@ content->>'~:hint' AS hint FROM server_error_report ORDER BY created_at DESC - LIMIT 100") + LIMIT 200") (defn error-list-handler [{:keys [::db/pool]} request] diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index 3829abfaa7..24f9891d35 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -10,6 +10,7 @@ [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.schema :as sm] + [app.config :as cf] [app.http :as-alias http] [app.http.access-token :as-alias actoken] [app.http.session :as-alias session] @@ -30,14 +31,14 @@ (let [claims (-> {} (into (::session/token-claims request)) (into (::actoken/token-claims request)))] - {:path (:path request) - :method (:method request) - :params (:params request) - :ip-addr (parse-client-ip request) - :user-agent (yrq/get-header request "user-agent") - :profile-id (:uid claims) - :version (or (yrq/get-header request "x-frontend-version") - "unknown")})) + {:request/path (:path request) + :request/method (:method request) + :request/params (:params request) + :request/user-agent (yrq/get-header request "user-agent") + :request/ip-addr (parse-client-ip request) + :request/profile-id (:uid claims) + :version/frontend (or (yrq/get-header request "x-frontend-version") "unknown") + :version/backend (:full cf/version)})) (defmulti handle-exception (fn [err & _rest] diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index e11a60d04d..f71f9da952 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -22,9 +22,10 @@ (:import com.fasterxml.jackson.core.JsonParseException com.fasterxml.jackson.core.io.JsonEOFException + com.fasterxml.jackson.databind.exc.MismatchedInputException io.undertow.server.RequestTooBigException - java.io.OutputStream - java.io.InputStream)) + java.io.InputStream + java.io.OutputStream)) (set! *warn-on-reflection* true) @@ -78,11 +79,13 @@ (or (instance? JsonEOFException cause) - (instance? JsonParseException cause)) + (instance? JsonParseException cause) + (instance? MismatchedInputException cause)) (raise (ex/error :type :validation :code :malformed-json :hint (ex-message cause) :cause cause)) + :else (raise cause)))] @@ -118,7 +121,9 @@ (t/write! tw data))) (catch java.io.IOException _) (catch Throwable cause - (l/error :hint "unexpected error on encoding response" :cause cause)) + (binding [l/*context* {:value data}] + (l/error :hint "unexpected error on encoding response" + :cause cause))) (finally (.close ^OutputStream output-stream)))))) @@ -131,8 +136,9 @@ (catch java.io.IOException _) (catch Throwable cause - (l/error :hint "unexpected error on encoding response" - :cause cause)) + (binding [l/*context* {:value data}] + (l/error :hint "unexpected error on encoding response" + :cause cause))) (finally (.close ^OutputStream output-stream)))))) diff --git a/backend/src/app/loggers/database.clj b/backend/src/app/loggers/database.clj index 5fbee80e98..fac70eac65 100644 --- a/backend/src/app/loggers/database.clj +++ b/backend/src/app/loggers/database.clj @@ -40,35 +40,33 @@ [{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}] (us/assert! ::l/record record) - (let [data (ex-data cause)] + (let [data (ex-data cause) + ctx (-> context + (assoc :tenant (cf/get :tenant)) + (assoc :host (cf/get :host)) + (assoc :public-uri (cf/get :public-uri)) + (assoc :logger/name logger) + (assoc :logger/level level) + (dissoc :request/params :value :params :data))] (merge - {:context (-> context - (assoc :tenant (cf/get :tenant)) - (assoc :host (cf/get :host)) - (assoc :public-uri (cf/get :public-uri)) - (assoc :version (:full cf/version)) - (assoc :logger-name logger) - (assoc :logger-level level) - (dissoc :params) - (pp/pprint-str :width 200)) - - :props (pp/pprint-str props :width 200) + {:context (-> (into (sorted-map) ctx) + (pp/pprint-str :width 200 :length 50 :level 10)) + :props (pp/pprint-str props :width 200 :length 50) :hint (or (ex-message cause) @message) :trace (ex/format-throwable cause :data? false :explain? false :header? false :summary? false)} - (when-let [params (:params context)] + (when-let [params (or (:request/params context) (:params context))] {:params (pp/pprint-str params :width 200)}) + (when-let [value (:value context)] + {:value (pp/pprint-str value :width 200 :length 50 :level 10)}) + (when-let [data (some-> data (dissoc ::s/problems ::s/value ::s/spec ::sm/explain :hint))] {:data (pp/pprint-str data :width 200)}) - (when-let [value (-> data ::sm/explain :value)] - {:value (pp/pprint-str value :width 200)}) - - (when-let [explain (ex/explain data)] + (when-let [explain (ex/explain data {:level 10 :length 50})] {:explain explain})))) - (defn error-record? [{:keys [::l/level ::l/cause]}] (and (= :error level) diff --git a/backend/src/app/loggers/mattermost.clj b/backend/src/app/loggers/mattermost.clj index 93db476a79..8a2117e23c 100644 --- a/backend/src/app/loggers/mattermost.clj +++ b/backend/src/app/loggers/mattermost.clj @@ -30,7 +30,9 @@ "```\n" "- host: `" (:host report) "`\n" "- tenant: `" (:tenant report) "`\n" - "- version: `" (:version report) "`\n" + "- request-path: `" (:request-path report) "`\n" + "- frontend-version: `" (:frontend-version report) "`\n" + "- backend-version: `" (:backend-version report) "`\n" "\n" "Trace:\n" (:trace report) @@ -50,13 +52,15 @@ (defn record->report [{:keys [::l/context ::l/id ::l/cause] :as record}] (us/assert! ::l/record record) - {:id id - :tenant (cf/get :tenant) - :host (cf/get :host) - :public-uri (cf/get :public-uri) - :version (:full cf/version) - :profile-id (:profile-id context) - :trace (ex/format-throwable cause :detail? false :header? false)}) + {:id id + :tenant (cf/get :tenant) + :host (cf/get :host) + :public-uri (cf/get :public-uri) + :backend-version (or (:version/backend context) (:full cf/version)) + :frontend-version (:version/frontend context) + :profile-id (:request/profile-id context) + :request-path (:request/path context) + :trace (ex/format-throwable cause :detail? false :header? false)}) (defn handle-event [cfg record] diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index ecaeae7c3d..d5f6f02713 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -327,6 +327,9 @@ {:name "0105-mod-file-change-table" :fn (mg/resource "app/migrations/sql/0105-mod-file-change-table.sql")} + {:name "0105-mod-server-error-report-table" + :fn (mg/resource "app/migrations/sql/0105-mod-server-error-report-table.sql")} + ]) (defn apply-migrations! diff --git a/backend/src/app/migrations/sql/0105-mod-server-error-report-table.sql b/backend/src/app/migrations/sql/0105-mod-server-error-report-table.sql new file mode 100644 index 0000000000..3609cece9f --- /dev/null +++ b/backend/src/app/migrations/sql/0105-mod-server-error-report-table.sql @@ -0,0 +1,2 @@ +CREATE INDEX server_error_report__created_at__idx + ON server_error_report ( created_at ); diff --git a/backend/src/app/worker.clj b/backend/src/app/worker.clj index e11c68d2a3..05c27f7f8e 100644 --- a/backend/src/app/worker.clj +++ b/backend/src/app/worker.clj @@ -489,16 +489,8 @@ (l/error :hint "worker: unhandled exception" :cause cause)))))) (defn- get-error-context - [error item] - (let [data (ex-data error)] - (merge - {:hint (ex-message error) - :spec-problems (some->> data ::s/problems (take 10) seq vec) - :spec-value (some->> data ::s/value) - :data (some-> data (dissoc ::s/problems ::s/value ::s/spec)) - :params item} - (when-let [explain (ex/explain data)] - {:spec-explain explain})))) + [_ item] + {:params item}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; CRON @@ -597,10 +589,10 @@ (catch InterruptedException _ (l/debug :hint "cron: task interrupted" :task-id id)) (catch Throwable cause - (l/error :hint "cron: unhandled exception on running task" - ::l/context (get-error-context cause task) - :task-id id - :cause cause)) + (binding [l/*context* (get-error-context cause task)] + (l/error :hint "cron: unhandled exception on running task" + :task-id id + :cause cause))) (finally (when-not (px/interrupted? :current) (schedule-cron-task cfg task)))))) diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index 3b440e756b..12f9ba8287 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -93,6 +93,13 @@ [:component-id {:optional true} ::sm/uuid] [:ignore-touched {:optional true} :boolean]]] + [:fix-obj + [:map {:title "FixObjChange"} + [:type [:= :fix-obj]] + [:id ::sm/uuid] + [:page-id {:optional true} ::sm/uuid] + [:component-id {:optional true} ::sm/uuid]]] + [:mov-objects [:map {:title "MovObjectsChange"} [:type [:= :mov-objects]] @@ -392,6 +399,12 @@ (d/update-in-when data [:pages-index page-id] ctst/delete-shape id ignore-touched) (d/update-in-when data [:components component-id] ctst/delete-shape id ignore-touched))) +(defmethod process-change :fix-obj + [data {:keys [page-id component-id] :as params}] + (if page-id + (d/update-in-when data [:pages-index page-id] ctst/fix-shape-children params) + (d/update-in-when data [:components component-id] ctst/fix-shape-children params))) + ;; FIXME: remove, seems like this method is already unused ;; reg-objects operation "regenerates" the geometry and selrect of the parent groups (defmethod process-change :reg-objects diff --git a/common/src/app/common/spec.cljc b/common/src/app/common/spec.cljc index acb3c698b4..5a8c23a385 100644 --- a/common/src/app/common/spec.cljc +++ b/common/src/app/common/spec.cljc @@ -29,8 +29,10 @@ (def uuid-rx #"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$") -(def max-safe-int (int 1e6)) -(def min-safe-int (int -1e6)) +;; Integer/MAX_VALUE +(def max-safe-int 2147483647) +;; Integer/MIN_VALUE +(def min-safe-int -2147483648) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; DEFAULT SPECS diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index aa8441facf..3d6c268c4c 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -103,7 +103,7 @@ [:map {:title "Fill"} [:fill-color {:optional true} ::ctc/rgb-color] [:fill-opacity {:optional true} ::sm/safe-number] - [:fill-color-gradient {:optional true} ::ctc/gradient] + [:fill-color-gradient {:optional true} [:maybe ::ctc/gradient]] [:fill-color-ref-file {:optional true} [:maybe ::sm/uuid]] [:fill-color-ref-id {:optional true} [:maybe ::sm/uuid]]]) @@ -201,7 +201,7 @@ (sm/def! ::group-attrs [:map {:title "GroupAttrs"} [:type [:= :group]] - [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]]) + [:shapes {:optional true} [:maybe [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]]]) (sm/def! ::frame-attrs [:map {:title "FrameAttrs"} @@ -214,18 +214,19 @@ (sm/def! ::bool-attrs [:map {:title "BoolAttrs"} [:type [:= :bool]] - [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]] + [:shapes {:optional true} [:maybe [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]] ;; FIXME: improve this schema [:bool-type :keyword] - ;; FIXME: improve this schema [:bool-content [:vector {:gen/max 2} [:map [:command :keyword] [:relative {:optional true} :boolean] - [:params [:map-of {:gen/max 5} :keyword ::sm/safe-number]]]]]]) + [:prev-pos {:optional true} ::gpt/point] + [:params {:optional true} + [:map-of {:gen/max 5} :keyword ::sm/safe-number]]]]]]) (sm/def! ::rect-attrs [:map {:title "RectAttrs"} @@ -252,7 +253,12 @@ (sm/def! ::path-attrs [:map {:title "PathAttrs"} [:type [:= :path]] + [:x {:optional true} [:maybe ::sm/safe-number]] + [:y {:optional true} [:maybe ::sm/safe-number]] + [:width {:optional true} [:maybe ::sm/safe-number]] + [:height {:optional true} [:maybe ::sm/safe-number]] [:content + {:optional true} [:vector [:map [:command :keyword] diff --git a/common/src/app/common/types/shape/text.cljc b/common/src/app/common/types/shape/text.cljc index 89851ba8e4..dff8759561 100644 --- a/common/src/app/common/types/shape/text.cljc +++ b/common/src/app/common/types/shape/text.cljc @@ -21,44 +21,46 @@ [:type [:= "root"]] [:key {:optional true} :string] [:children - [:vector {:min 1 :gen/max 2 :gen/min 1} - [:map - [:type [:= "paragraph-set"]] - [:key {:optional true} :string] - [:children - [:vector {:min 1 :gen/max 2 :gen/min 1} - [:map - [:type [:= "paragraph"]] - [:key {:optional true} :string] - [:fills {:optional true} - [:maybe - [:vector {:gen/max 2} ::shape/fill]]] - [:font-family {:optional true} :string] - [:font-size {:optional true} :string] - [:font-style {:optional true} :string] - [:font-weight {:optional true} :string] - [:direction {:optional true} :string] - [:text-decoration {:optional true} :string] - [:text-transform {:optional true} :string] - [:typography-ref-id {:optional true} [:maybe ::sm/uuid]] - [:typography-ref-file {:optional true} [:maybe ::sm/uuid]] - [:children - [:vector {:min 1 :gen/max 2 :gen/min 1} - [:map - [:text :string] - [:key {:optional true} :string] - [:fills {:optional true} - [:maybe - [:vector {:gen/max 2} ::shape/fill]]] - [:font-family {:optional true} :string] - [:font-size {:optional true} :string] - [:font-style {:optional true} :string] - [:font-weight {:optional true} :string] - [:direction {:optional true} :string] - [:text-decoration {:optional true} :string] - [:text-transform {:optional true} :string] - [:typography-ref-id {:optional true} [:maybe ::sm/uuid]] - [:typography-ref-file {:optional true} [:maybe ::sm/uuid]]]]]]]]]]]]) + {:optional true} + [:maybe + [:vector {:min 1 :gen/max 2 :gen/min 1} + [:map + [:type [:= "paragraph-set"]] + [:key {:optional true} :string] + [:children + [:vector {:min 1 :gen/max 2 :gen/min 1} + [:map + [:type [:= "paragraph"]] + [:key {:optional true} :string] + [:fills {:optional true} + [:maybe + [:vector {:gen/max 2} ::shape/fill]]] + [:font-family {:optional true} :string] + [:font-size {:optional true} :string] + [:font-style {:optional true} :string] + [:font-weight {:optional true} :string] + [:direction {:optional true} :string] + [:text-decoration {:optional true} :string] + [:text-transform {:optional true} :string] + [:typography-ref-id {:optional true} [:maybe ::sm/uuid]] + [:typography-ref-file {:optional true} [:maybe ::sm/uuid]] + [:children + [:vector {:min 1 :gen/max 2 :gen/min 1} + [:map + [:text :string] + [:key {:optional true} :string] + [:fills {:optional true} + [:maybe + [:vector {:gen/max 2} ::shape/fill]]] + [:font-family {:optional true} :string] + [:font-size {:optional true} :string] + [:font-style {:optional true} :string] + [:font-weight {:optional true} :string] + [:direction {:optional true} :string] + [:text-decoration {:optional true} :string] + [:text-transform {:optional true} :string] + [:typography-ref-id {:optional true} [:maybe ::sm/uuid]] + [:typography-ref-file {:optional true} [:maybe ::sm/uuid]]]]]]]]]]]]]) diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc index afb33201cb..98995347e8 100644 --- a/common/src/app/common/types/shape_tree.cljc +++ b/common/src/app/common/types/shape_tree.cljc @@ -90,16 +90,24 @@ (delete-from-objects [objects] (if-let [target (get objects shape-id)] - (let [parent-id (or (:parent-id target) - (:frame-id target)) - children-ids (cph/get-children-ids objects shape-id)] - (-> (reduce dissoc objects children-ids) - (dissoc shape-id) + (let [parent-id (or (:parent-id target) + (:frame-id target)) + children-ids (cph/get-children-ids objects shape-id)] + (-> (reduce dissoc objects (cons shape-id children-ids)) (d/update-when parent-id delete-from-parent))) objects))] (update container :objects delete-from-objects)))) +(defn fix-shape-children + "Checks and fix the children relations of the shape. If a children does not + exists on the objects tree, it will be removed from shape." + [{:keys [objects] :as container} {:keys [id] :as params}] + (let [contains? (partial contains? objects)] + (d/update-in-when container [:objects id :shapes] + (fn [shapes] + (into [] (filter contains?) shapes))))) + (defn get-frames "Retrieves all frame objects as vector" ([objects] (get-frames objects nil)) @@ -349,6 +357,7 @@ (some? force-id) force-id keep-ids? (:id object) :else (uuid/next))] + (loop [child-ids (seq (:shapes object)) new-direct-children [] new-children [] diff --git a/docker/images/Dockerfile.exporter b/docker/images/Dockerfile.exporter index 7bc3f89d22..9ddd9ecc78 100644 --- a/docker/images/Dockerfile.exporter +++ b/docker/images/Dockerfile.exporter @@ -104,7 +104,7 @@ WORKDIR /opt/penpot/exporter USER penpot:penpot RUN set -ex; \ - yarn; \ - yarn run playwright install chromium; + yarn --network-timeout 1000000; \ + yarn --network-timeout 1000000 run playwright install chromium; CMD ["node", "app.js"] diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 0de1233fb5..8018565128 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -50,6 +50,23 @@ (mf/mount (mf/element ui/app) (dom/get-element "app")) (mf/mount (mf/element modal) (dom/get-element "modal"))) +(defn- initialize-profile + "Event used mainly on application bootstrap; it fetches the profile + and if and only if the fetched profile corresponds to an + authenticated user; proceed to fetch teams." + [stream] + (rx/merge + (rx/of (du/fetch-profile)) + (->> stream + (rx/filter (ptk/type? ::profile-fetched)) + (rx/take 1) + (rx/map deref) + (rx/mapcat (fn [profile] + (if (du/is-authenticated? profile) + (rx/of (du/fetch-teams)) + (rx/empty)))) + (rx/observe-on :async)))) + (defn initialize [] (ptk/reify ::initialize @@ -61,14 +78,19 @@ (watch [_ _ stream] (rx/merge (rx/of (ev/initialize) - (feat/initialize) - (du/initialize-profile)) + (feat/initialize)) + (initialize-profile stream) + + ;; Once profile is fetched, initialize all penpot application + ;; routes (->> stream (rx/filter du/profile-fetched?) (rx/take 1) (rx/map #(rt/init-routes))) + ;; Once profile fetched and the current user is authenticated, + ;; proceed to initialize the websockets connection. (->> stream (rx/filter du/profile-fetched?) (rx/map deref) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 92d2e8bb18..06ff506423 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -117,28 +117,6 @@ (->> (rp/cmd! :get-profile) (rx/map profile-fetched))))) -;; --- EVENT: INITIALIZE PROFILE - -(defn initialize-profile - "Event used mainly on application bootstrap; it fetches the profile - and if and only if the fetched profile corresponds to an - authenticated user; proceed to fetch teams." - [] - (ptk/reify ::initialize-profile - ptk/WatchEvent - (watch [_ _ stream] - (rx/merge - (rx/of (fetch-profile)) - (->> stream - (rx/filter (ptk/type? ::profile-fetched)) - (rx/take 1) - (rx/map deref) - (rx/mapcat (fn [profile] - (if (= uuid/zero (:id profile)) - (rx/empty) - (rx/of (fetch-teams))))) - (rx/observe-on :async)))))) - ;; --- EVENT: login (defn- logged-in @@ -164,7 +142,8 @@ (when (is-authenticated? profile) (->> (rx/of (profile-fetched profile) (fetch-teams) - (get-redirect-event)) + (get-redirect-event) + (ws/initialize)) (rx/observe-on :async))))))) (declare login-from-register) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 0b4df68a53..8bd067454e 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -46,6 +46,7 @@ [app.main.data.workspace.drawing.common :as dwdc] [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.fix-bool-contents :as fbc] + [app.main.data.workspace.fix-broken-shape-links :as fbs] [app.main.data.workspace.fix-deleted-fonts :as fdf] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.guides :as dwgu] @@ -132,8 +133,10 @@ has-graphics? (-> file :media seq) components-v2 (features/active-feature? state :components-v2)] (rx/merge - (rx/of (fbc/fix-bool-contents)) - (rx/of (fdf/fix-deleted-fonts)) + (rx/of (fbc/fix-bool-contents) + (fdf/fix-deleted-fonts) + (fbs/fix-broken-shapes)) + (if (and has-graphics? components-v2) (rx/of (remove-graphics (:id file) (:name file))) (rx/empty))))))) @@ -1832,9 +1835,14 @@ detach? (or (foreign-instance? shape paste-objects state) (and (ctk/in-component-copy-not-root? shape) (not= (:id component-shape) - (:id component-shape-parent))))] + (:id component-shape-parent)))) + assign-shapes? (and (or (cph/group-shape? shape) + (cph/bool-shape? shape)) + (nil? (:shapes shape)))] (-> shape (assoc :frame-id frame-id :parent-id parent-id) + (cond-> assign-shapes? + (assoc :shapes [])) (cond-> detach? ;; this is used later, if the paste needs to create a new component from the detached shape (-> (assoc :saved-component-root (:component-root shape)) diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index fb637eb24c..ad48c0a3a7 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.exceptions :as ex] [app.common.logging :as log] [app.common.pages :as cp] [app.common.pages.changes :as cpc] @@ -206,6 +207,7 @@ path (if (= file-id current-file-id) [:workspace-data] [:workspace-libraries file-id :data])] + (try (dm/assert! "expect valid vector of changes" @@ -218,7 +220,11 @@ (ctst/update-object-indices page-id)))) (catch :default err - (log/error :js/error err) + (when-let [data (ex-data err)] + (js/console.log (ex/explain data))) + + (when (ex/error? err) + (js/console.log (.-stack ^js err))) (vreset! error err) state)))) diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index fbd330b2dd..32de9cf96c 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -353,9 +353,12 @@ (-> state (assoc-in [:workspace-global :picking-color?] true) (assoc ::md/modal {:id (random-uuid) - :data {:color colors/black :opacity 1} :type :colorpicker - :props {:on-change handle-change-color} + :props {:data {:color colors/black + :opacity 1} + :disable-opacity false + :disable-gradient false + :on-change handle-change-color} :allow-click-outside true}))))))) (defn color-att->text diff --git a/frontend/src/app/main/data/workspace/fix_broken_shape_links.cljs b/frontend/src/app/main/data/workspace/fix_broken_shape_links.cljs new file mode 100644 index 0000000000..e547f0c033 --- /dev/null +++ b/frontend/src/app/main/data/workspace/fix_broken_shape_links.cljs @@ -0,0 +1,55 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.data.workspace.fix-broken-shape-links + (:require + [app.main.data.workspace.changes :as dch] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defn- generate-changes + [attr {:keys [objects id] :as container}] + (let [base {:type :fix-obj attr id} + contains? (partial contains? objects) + xform (comp + ;; FIXME: Ensure all obj have id field (this is needed + ;; because some bug adds an ephimeral shape with id ZERO, + ;; with a single attr `:shapes` having a vector of ids + ;; pointing to not existing shapes). That happens on + ;; components. THIS IS A WORKAOURD + (map (fn [[id obj]] + (if (some? (:id obj)) + obj + (assoc obj :id id)))) + + ;; Remove all valid shapes + (remove (fn [obj] + (every? contains? (:shapes obj)))) + + (map (fn [obj] + (assoc base :id (:id obj)))))] + + (sequence xform objects))) + +(defn fix-broken-shapes + [] + (ptk/reify ::fix-broken-shape-links + ptk/WatchEvent + (watch [it state _] + (let [data (get state :workspace-data) + changes (concat + (mapcat (partial generate-changes :page-id) + (vals (:pages-index data))) + (mapcat (partial generate-changes :component-id) + (vals (:components data))))] + + (if (seq changes) + (rx/of (dch/commit-changes + {:origin it + :redo-changes (vec changes) + :undo-changes [] + :save-undo? false})) + (rx/empty)))))) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 3f6bab8404..c7ca8b7ea2 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -419,6 +419,8 @@ :else (let [frame? (cph/frame-shape? obj) + group? (cph/group-shape? obj) + bool? (cph/bool-shape? obj) new-id (ids-map (:id obj)) parent-id (or parent-id frame-id) name (:name obj) @@ -437,9 +439,15 @@ :name name :parent-id parent-id :frame-id frame-id) + (dissoc :shapes :main-instance? :use-for-thumbnail?) + + (cond-> + (or group? bool?) + (assoc :shapes [])) + (gsh/move delta) (d/update-when :interactions #(ctsi/remap-interactions % ids-map objects)) diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 6e57a9026b..37d52b9dcd 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -252,7 +252,7 @@ (let [all-ids (into empty-parents ids) contains? (partial contains? all-ids) xform (comp (map lookup) - (filter cph/group-shape?) + (filter #(or (cph/group-shape? %) (cph/bool-shape? %))) (remove #(->> (:shapes %) (remove contains?) seq)) (map :id)) parents (into #{} xform all-parents)] diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index 52c2a60293..2b404d95ae 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -116,56 +116,64 @@ (assoc-in [:fills 0 :fill-opacity] (-> (dm/get-in shape [:svg-attrs :style :fill-opacity]) (d/parse-double 1))))))) -(defn setup-stroke [shape] - (let [stroke-linecap (-> (or (dm/get-in shape [:svg-attrs :stroke-linecap]) - (dm/get-in shape [:svg-attrs :style :stroke-linecap])) - ((d/nilf str/trim)) - ((d/nilf keyword))) - color-attr (str/trim (dm/get-in shape [:svg-attrs :stroke])) - color-attr (if (= color-attr "currentColor") clr/black color-attr) - color-style (str/trim (dm/get-in shape [:svg-attrs :style :stroke])) - color-style (if (= color-style "currentColor") clr/black color-style) +(defn- setup-stroke + [shape] + (let [attrs (get shape :svg-attrs) + style (get attrs :style) - shape - (cond-> shape - ;; Color present as attribute - (uc/color? color-attr) - (-> (update :svg-attrs dissoc :stroke) - (assoc-in [:strokes 0 :stroke-color] (uc/parse-color color-attr))) + stroke (or (str/trim (:stroke attrs)) + (str/trim (:stroke style))) - ;; Color present as style - (uc/color? color-style) - (-> (update-in [:svg-attrs :style] dissoc :stroke) - (assoc-in [:strokes 0 :stroke-color] (uc/parse-color color-style))) + color (cond + (= stroke "currentColor") clr/black + (= stroke "none") nil + :else (uc/parse-color stroke)) - (dm/get-in shape [:svg-attrs :stroke-opacity]) - (-> (update :svg-attrs dissoc :stroke-opacity) - (assoc-in [:strokes 0 :stroke-opacity] (-> (dm/get-in shape [:svg-attrs :stroke-opacity]) - (d/parse-double 1)))) + opacity (when (some? color) + (d/parse-double + (or (:stroke-opacity attrs) + (:stroke-opacity style)) + 1)) - (dm/get-in shape [:svg-attrs :style :stroke-opacity]) - (-> (update-in [:svg-attrs :style] dissoc :stroke-opacity) - (assoc-in [:strokes 0 :stroke-opacity] (-> (dm/get-in shape [:svg-attrs :style :stroke-opacity]) - (d/parse-double 1)))) + width (when (some? color) + (d/parse-double + (or (:stroke-width attrs) + (:stroke-width style)) + 1)) - (dm/get-in shape [:svg-attrs :stroke-width]) - (-> (update :svg-attrs dissoc :stroke-width) - (assoc-in [:strokes 0 :stroke-width] (-> (dm/get-in shape [:svg-attrs :stroke-width]) - (d/parse-double)))) + linecap (or (get attrs :stroke-linecap) + (get style :stroke-linecap)) + linecap (some-> linecap str/trim keyword) - (dm/get-in shape [:svg-attrs :style :stroke-width]) - (-> (update-in [:svg-attrs :style] dissoc :stroke-width) - (assoc-in [:strokes 0 :stroke-width] (-> (dm/get-in shape [:svg-attrs :style :stroke-width]) - (d/parse-double)))) + attrs (-> attrs + (dissoc :stroke) + (dissoc :stroke-width) + (dissoc :stroke-opacity) + (update :style (fn [style] + (-> style + (dissoc :stroke) + (dissoc :stroke-linecap) + (dissoc :stroke-width) + (dissoc :stroke-opacity)))))] - (and stroke-linecap (= (:type shape) :path)) - (-> (update-in [:svg-attrs :style] dissoc :stroke-linecap) - (cond-> (#{:round :square} stroke-linecap) - (assoc :stroke-cap-start stroke-linecap - :stroke-cap-end stroke-linecap))))] + (cond-> (assoc shape :svg-attrs attrs) + (some? color) + (assoc-in [:strokes 0 :stroke-color] color) - (cond-> shape - (d/any-key? (dm/get-in shape [:strokes 0]) :stroke-color :stroke-opacity :stroke-width :stroke-cap-start :stroke-cap-end) + (and (some? color) (some? opacity)) + (assoc-in [:strokes 0 :stroke-opacity] opacity) + + (and (some? color) (some? width)) + (assoc-in [:strokes 0 :stroke-width] width) + + (and (some? linecap) (= (:type shape) :path) + (or (= linecap :round) (= linecap :square))) + (assoc :stroke-cap-start linecap + :stroke-cap-end linecap) + + (d/any-key? (dm/get-in shape [:strokes 0]) + :stroke-color :stroke-opacity :stroke-width + :stroke-cap-start :stroke-cap-end) (assoc-in [:strokes 0 :stroke-style] :svg)))) (defn setup-opacity [shape] diff --git a/frontend/src/app/main/ui/hooks/resize.cljs b/frontend/src/app/main/ui/hooks/resize.cljs index 6dd334678e..a5ad1f9bec 100644 --- a/frontend/src/app/main/ui/hooks/resize.cljs +++ b/frontend/src/app/main/ui/hooks/resize.cljs @@ -115,7 +115,7 @@ (mf/set-ref-val! prev-val-ref node))))] - (mf/with-effect [] + (mf/with-layout-effect [] ;; On dismount we need to disconnect the current observer (fn [] (when-let [observer (mf/ref-val observer-ref)] diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index d736e2cf44..8ea32ef4ec 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -89,7 +89,7 @@ (obj/merge! attrs (clj->js fill-attrs))))) (defn add-stroke [attrs stroke-data render-id index] - (let [stroke-style (:stroke-style stroke-data :none) + (let [stroke-style (:stroke-style stroke-data :solid) stroke-color-gradient-id (str "stroke-color-gradient_" render-id "_" index) stroke-width (:stroke-width stroke-data 1)] (if (not= stroke-style :none) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs index 74ad4399ac..53ec895026 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs @@ -29,7 +29,7 @@ (defn- event-type-names [] {:click (tr "workspace.options.interaction-on-click") - ; TODO: need more UX research + ;; TODO: need more UX research ;; :mouse-over (tr "workspace.options.interaction-while-hovering") ;; :mouse-press (tr "workspace.options.interaction-while-pressing") :mouse-enter (tr "workspace.options.interaction-mouse-enter") @@ -315,11 +315,12 @@ (when @extended-open? [:div.element-set-content - ; Trigger select + ;; Trigger select [:div.interactions-element.separator [:span.element-set-subtitle.wide (tr "workspace.options.interaction-trigger")] [:select.input-select - {:value (str (:event-type interaction)) + {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element + :value (str (:event-type interaction)) :on-change change-event-type} (for [[value name] (event-type-names)] (when-not (and (= value :after-delay) @@ -327,7 +328,7 @@ [:option {:key (dm/str value) :value (dm/str value)} name]))]] - ; Delay + ;; Delay (when (ctsi/has-delay interaction) [:div.interactions-element [:span.element-set-subtitle.wide (tr "workspace.options.interaction-delay")] @@ -338,22 +339,24 @@ :title (tr "workspace.options.interaction-ms")}] [:span.after (tr "workspace.options.interaction-ms")]]]) - ; Action select + ;; Action select [:div.interactions-element.separator [:span.element-set-subtitle.wide (tr "workspace.options.interaction-action")] [:select.input-select - {:value (str (:action-type interaction)) + {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element + :value (str (:action-type interaction)) :on-change change-action-type} (for [[value name] (action-type-names)] [:option {:key (dm/str "action-" value) :value (str value)} name])]] - ; Destination + ;; Destination (when (ctsi/has-destination interaction) [:div.interactions-element [:span.element-set-subtitle.wide (tr "workspace.options.interaction-destination")] [:select.input-select - {:value (str (:destination interaction)) + {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element + :value (str (:destination interaction)) :on-change change-destination} (if (= (:action-type interaction) :close-overlay) [:option {:value ""} (tr "workspace.options.interaction-self")] @@ -364,7 +367,7 @@ [:option {:key (dm/str "destination-" (:id frame)) :value (str (:id frame))} (:name frame)]))]]) - ; Preserve scroll + ;; Preserve scroll (when (ctsi/has-preserve-scroll interaction) [:div.interactions-element [:div.input-checkbox @@ -375,7 +378,7 @@ [:label {:for (str "preserve-" index)} (tr "workspace.options.interaction-preserve-scroll")]]]) - ; URL + ;; URL (when (ctsi/has-url interaction) [:div.interactions-element [:span.element-set-subtitle.wide (tr "workspace.options.interaction-url")] @@ -386,11 +389,12 @@ (when (ctsi/has-overlay-opts interaction) [:* - ; Overlay position relative-to (select) + ;; Overlay position relative-to (select) [:div.interactions-element [:span.element-set-subtitle.wide (tr "workspace.options.interaction-relative-to")] [:select.input-select - {:value (str (:position-relative-to interaction)) + {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element + :value (str (:position-relative-to interaction)) :on-change change-position-relative-to} (when (not= (:overlay-pos-type interaction) :manual) [:* @@ -401,16 +405,17 @@ [:option {:key (dm/str "position-relative-to-" (:id shape)) :value (str (:id shape))} (:name shape) " (" (tr "workspace.options.interaction-self") ")"]]] - ; Overlay position (select) + ;; Overlay position (select) [:div.interactions-element [:span.element-set-subtitle.wide (tr "workspace.options.interaction-position")] [:select.input-select - {:value (str (:overlay-pos-type interaction)) + {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element + :value (str (:overlay-pos-type interaction)) :on-change (partial change-overlay-pos-type (:id shape))} (for [[value name] (overlay-pos-type-names)] [:option {:value (str value)} name])]] - ; Overlay position (buttons) + ;; Overlay position (buttons) [:div.interactions-element.interactions-pos-buttons [:div.element-set-actions-button {:class (dom/classnames :active (= overlay-pos-type :center)) @@ -441,7 +446,7 @@ :on-click #(toggle-overlay-pos-type :bottom-center)} i/position-bottom-center]] - ; Overlay click outside + ;; Overlay click outside [:div.interactions-element [:div.input-checkbox [:input {:type "checkbox" @@ -451,7 +456,7 @@ [:label {:for (str "close-" index)} (tr "workspace.options.interaction-close-outside")]]] - ; Overlay background + ;; Overlay background [:div.interactions-element [:div.input-checkbox [:input {:type "checkbox" @@ -463,17 +468,18 @@ (when (ctsi/has-animation? interaction) [:* - ; Animation select + ;; Animation select [:div.interactions-element.separator [:span.element-set-subtitle.wide (tr "workspace.options.interaction-animation")] [:select.input-select - {:value (str (-> interaction :animation :animation-type)) + {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element + :value (str (-> interaction :animation :animation-type)) :on-change change-animation-type} [:option {:value ""} (tr "workspace.options.interaction-animation-none")] (for [[value name] (animation-type-names interaction)] [:option {:value (str value)} name])]] - ; Direction + ;; Direction (when (ctsi/has-way? interaction) [:div.interactions-element.interactions-way-buttons [:div.input-radio @@ -493,7 +499,7 @@ :on-change change-way}] [:label {:for "way-out"} (tr "workspace.options.interaction-out")]]]) - ; Direction + ;; Direction (when (ctsi/has-direction? interaction) [:div.interactions-element.interactions-direction-buttons [:div.element-set-actions-button @@ -513,7 +519,7 @@ :on-click #(change-direction :up)} i/animate-up]]) - ; Duration + ;; Duration (when (ctsi/has-duration? interaction) [:div.interactions-element [:span.element-set-subtitle.wide (tr "workspace.options.interaction-duration")] @@ -524,12 +530,13 @@ :title (tr "workspace.options.interaction-ms")}] [:span.after (tr "workspace.options.interaction-ms")]]]) - ; Easing + ;; Easing (when (ctsi/has-easing? interaction) [:div.interactions-element [:span.element-set-subtitle.wide (tr "workspace.options.interaction-easing")] [:select.input-select - {:value (str (-> interaction :animation :easing)) + {:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element + :value (str (-> interaction :animation :easing)) :on-change change-easing} (for [[value name] (easing-names)] [:option {:value (str value)} name])] @@ -541,7 +548,7 @@ :ease-out i/easing-ease-out :ease-in-out i/easing-ease-in-out)]]) - ; Offset effect + ;; Offset effect (when (ctsi/has-offset-effect? interaction) [:div.interactions-element [:div.input-checkbox diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index a2e27596c7..f311ef2835 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -325,7 +325,7 @@ layout-container-ids layout-container-values layout-item-ids layout-item-values] (mf/use-memo - (mf/deps objects-no-measures) + (mf/deps shapes objects-no-measures) (fn [] (into [] diff --git a/frontend/src/app/util/color.cljs b/frontend/src/app/util/color.cljs index 30f49882e3..5b8ca29fdd 100644 --- a/frontend/src/app/util/color.cljs +++ b/frontend/src/app/util/color.cljs @@ -8,6 +8,7 @@ "Color conversion utils." (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.util.i18n :as i18n :refer [tr]] [app.util.object :as obj] [app.util.strings :as ust] @@ -176,14 +177,16 @@ (= id :multiple) (= file-id :multiple))) -(defn color? [^string color-str] - (and (not (nil? color-str)) - (seq color-str) - (gcolor/isValidColor color-str))) +(defn color? + [color] + (and (string? color) + (gcolor/isValidColor color))) -(defn parse-color [^string color-str] - (let [result (gcolor/parse color-str)] - (str (.-hex ^js result)))) +(defn parse-color + [color] + (when (color? color) + (let [result (gcolor/parse color)] + (dm/str (.-hex ^js result))))) (def color-names (obj/get-keys ^js gcolor/names))