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:
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))