From 8acc9af1f5908cd959a5373b7a88007d085f55b3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 21 Mar 2022 15:00:50 +0100 Subject: [PATCH 01/10] :paperclip: Add more events instrumentation --- backend/src/app/http/oauth.clj | 13 +- backend/src/app/loggers/audit.clj | 14 ++ backend/src/app/rpc/mutations/profile.clj | 3 +- backend/src/app/rpc/mutations/teams.clj | 61 ++--- backend/test/app/services_teams_test.clj | 7 +- frontend/src/app/main/data/dashboard.cljs | 14 +- frontend/src/app/main/data/events.cljs | 3 +- frontend/src/app/main/data/users.cljs | 23 -- frontend/src/app/main/ui/auth/register.cljs | 16 +- frontend/src/app/main/ui/dashboard/team.cljs | 251 +++++++++++-------- 10 files changed, 216 insertions(+), 189 deletions(-) diff --git a/backend/src/app/http/oauth.clj b/backend/src/app/http/oauth.clj index 4e27485293..26c2f80c43 100644 --- a/backend/src/app/http/oauth.clj +++ b/backend/src/app/http/oauth.clj @@ -180,17 +180,6 @@ ;; --- HTTP HANDLERS -(defn extract-utm-props - "Extracts additional data from user params." - [params] - (reduce-kv (fn [params k v] - (let [sk (name k)] - (cond-> params - (str/starts-with? sk "utm_") - (assoc (->> sk str/kebab (keyword "penpot")) v)))) - {} - params)) - (defn- retrieve-profile [{:keys [pool executor] :as cfg} info] (px/with-dispatch executor @@ -252,7 +241,7 @@ (defn- auth-handler [{:keys [tokens] :as cfg} {:keys [params] :as request} respond raise] (try - (let [props (extract-utm-props params) + (let [props (audit/extract-utm-params params) state (tokens :generate {:iss :oauth :invitation-token (:invitation-token params) diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index 952c4d640d..8b5a8c2ec7 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -34,6 +34,20 @@ (yrq/get-header request "x-real-ip") (yrq/remote-addr request))) +(defn extract-utm-params + "Extracts additional data from params and namespace them under + `penpot` ns." + [params] + (letfn [(process-param [params k v] + (let [sk (d/name k)] + (cond-> params + (str/starts-with? sk "utm_") + (assoc (->> sk str/kebab (keyword "penpot")) v) + + (str/starts-with? sk "mtm_") + (assoc (->> sk str/kebab (keyword "penpot")) v))))] + (reduce-kv process-param {} params))) + (defn profile->props [profile] (-> profile diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index fbc59b8b7f..5834b0588e 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -12,7 +12,6 @@ [app.config :as cf] [app.db :as db] [app.emails :as eml] - [app.http.oauth :refer [extract-utm-props]] [app.loggers.audit :as audit] [app.media :as media] [app.rpc.mutations.teams :as teams] @@ -223,7 +222,7 @@ [conn params] (let [id (or (:id params) (uuid/next)) - props (-> (extract-utm-props params) + props (-> (audit/extract-utm-params params) (merge (:props params)) (db/tjson)) diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj index 77fa0b9e07..494204ca8d 100644 --- a/backend/src/app/rpc/mutations/teams.clj +++ b/backend/src/app/rpc/mutations/teams.clj @@ -13,6 +13,7 @@ [app.config :as cf] [app.db :as db] [app.emails :as eml] + [app.loggers.audit :as audit] [app.media :as media] [app.rpc.mutations.projects :as projects] [app.rpc.permissions :as perms] @@ -357,14 +358,14 @@ :opt-un [::email ::emails])) (sv/defmethod ::invite-team-member + "A rpc call that allow to send a single or multiple invitations to + join the team." [{:keys [pool] :as cfg} {:keys [profile-id team-id email emails role] :as params}] (db/with-atomic [conn pool] (let [perms (teams/get-permissions conn profile-id team-id) profile (db/get-by-id conn :profile profile-id) team (db/get-by-id conn :team team-id) - emails (or emails #{}) - emails (if email (conj emails email) emails) - ] + emails (cond-> (or emails #{}) (string? email) (conj email))] (when-not (:is-admin perms) (ex/raise :type :validation @@ -385,7 +386,9 @@ :profile profile :role role)) ) - nil))) + + (with-meta {} + {::audit/props {:invitations (count emails)}})))) (def sql:upsert-team-invitation "insert into team_invitation(team_id, email_to, role, valid_until) @@ -395,19 +398,19 @@ (defn- create-team-invitation [{:keys [conn tokens team profile role email] :as cfg}] - (let [member (profile/retrieve-profile-data-by-email conn email) + (let [member (profile/retrieve-profile-data-by-email conn email) token-exp (dt/in-future "48h") - itoken (tokens :generate - {:iss :team-invitation - :exp token-exp - :profile-id (:id profile) - :role role - :team-id (:id team) - :member-email (:email member email) - :member-id (:id member)}) - ptoken (tokens :generate-predefined - {:iss :profile-identity - :profile-id (:id profile)})] + itoken (tokens :generate + {:iss :team-invitation + :exp token-exp + :profile-id (:id profile) + :role role + :team-id (:id team) + :member-email (:email member email) + :member-id (:id member)}) + ptoken (tokens :generate-predefined + {:iss :profile-identity + :profile-id (:id profile)})] (when (and member (not (eml/allow-send-emails? conn member))) (ex/raise :type :validation @@ -443,21 +446,14 @@ (s/and ::create-team (s/keys :req-un [::emails ::role]))) (sv/defmethod ::create-team-and-invite-members - [{:keys [pool audit] :as cfg} {:keys [profile-id emails role] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id emails role] :as params}] (db/with-atomic [conn pool] - (let [team (create-team conn params) - profile (db/get-by-id conn :profile profile-id)] + (let [team (create-team conn params) + audit-fn (:audit cfg) + profile (db/get-by-id conn :profile profile-id)] ;; Create invitations for all provided emails. (doseq [email emails] - (audit :cmd :submit - :type "mutation" - :name "create-team-invitation" - :profile-id profile-id - :props {:email email - :role role - :profile-id profile-id}) - (create-team-invitation (assoc cfg :conn conn @@ -465,8 +461,17 @@ :profile profile :email email :role role))) - team))) + (with-meta team + {:before-complete + #(audit-fn :cmd :submit + :type "mutation" + :name "invite-team-member" + :profile-id profile-id + :props {:emails emails + :role role + :profile-id profile-id + :invitations (count emails)})})))) ;; --- Mutation: Update invitation role diff --git a/backend/test/app/services_teams_test.clj b/backend/test/app/services_teams_test.clj index fa233ce713..61199fbf39 100644 --- a/backend/test/app/services_teams_test.clj +++ b/backend/test/app/services_teams_test.clj @@ -44,16 +44,15 @@ ;; (th/print-result! out) - (t/is (nil? (:result out))) + (t/is (= {} (:result out))) (t/is (= 1 (:call-count (deref mock)))) (t/is (= 1 (:num invitation)))) - ;; invite internal user without complaints (th/reset-mock! mock) (let [data (assoc data :email (:email profile2)) out (th/mutation! data)] - (t/is (nil? (:result out))) + (t/is (= {} (:result out))) (t/is (= 1 (:call-count (deref mock))))) ;; invite user with complaint @@ -61,7 +60,7 @@ (th/reset-mock! mock) (let [data (assoc data :email "foo@bar.com") out (th/mutation! data)] - (t/is (nil? (:result out))) + (t/is (= {} (:result out))) (t/is (= 1 (:call-count (deref mock))))) ;; invite user with bounce diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 1a495e871c..ce9f7982d0 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -426,21 +426,21 @@ (rx/tap #(tm/schedule on-success)) (rx/catch on-error)))))) -(defn invite-team-member - [{:keys [emails role] :as params}] +(defn invite-team-members + [{:keys [emails role team-id resend?] :as params}] (us/assert ::us/set-of-emails emails) (us/assert ::us/keyword role) - (ptk/reify ::invite-team-member + (us/assert ::us/uuid team-id) + (ptk/reify ::invite-team-members IDeref - (-deref [_] {:role role}) + (-deref [_] {:role role :team-id team-id :resend? resend?}) ptk/WatchEvent - (watch [_ state _] + (watch [_ _ _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params) - team-id (:current-team-id state) - params (assoc params :team-id team-id)] + params (dissoc params :resend?)] (->> (rp/mutation! :invite-team-member params) (rx/tap on-success) (rx/catch on-error)))))) diff --git a/frontend/src/app/main/data/events.cljs b/frontend/src/app/main/data/events.cljs index bcb6dc5483..7adf4c86cb 100644 --- a/frontend/src/app/main/data/events.cljs +++ b/frontend/src/app/main/data/events.cljs @@ -85,7 +85,7 @@ (derive :app.main.data.dashboard/delete-team-member ::generic-action) (derive :app.main.data.dashboard/duplicate-project ::generic-action) (derive :app.main.data.dashboard/file-created ::generic-action) -(derive :app.main.data.dashboard/invite-team-member ::generic-action) +(derive :app.main.data.dashboard/invite-team-members ::generic-action) (derive :app.main.data.dashboard/leave-team ::generic-action) (derive :app.main.data.dashboard/move-files ::generic-action) (derive :app.main.data.dashboard/move-project ::generic-action) @@ -113,6 +113,7 @@ (derive :app.main.data.workspace.persistence/attach-library ::generic-action) (derive :app.main.data.workspace.persistence/detach-library ::generic-action) (derive :app.main.data.workspace.persistence/set-file-shard ::generic-action) +(derive :app.main.data.workspace.selection/toggle-focus-mode ::generic-action) (derive :app.main.data.workspace/create-page ::generic-action) (derive :app.main.data.workspace/set-workspace-layout ::generic-action) (derive :app.main.data.workspace/toggle-layout-flag ::generic-action) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index a13b371166..cd5ca770e9 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -293,29 +293,6 @@ (rx/catch (constantly (rx/of 1))) (rx/map #(logged-out params))))))) -;; --- EVENT: register - -(s/def ::register - (s/keys :req-un [::fullname ::password ::email])) - -(defn register - "Create a register event instance." - [data] - (s/assert ::register data) - (ptk/reify ::register - ptk/WatchEvent - (watch [_ _ _] - (let [{:keys [on-error on-success] - :or {on-error identity - on-success identity}} (meta data)] - (->> (rp/mutation :register-profile data) - (rx/tap on-success) - (rx/catch on-error)))) - - ptk/EffectEvent - (effect [_ _ _] - (swap! storage dissoc ::redirect-to)))) - ;; --- Update Profile (defn update-profile diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 9cb0ed2938..739b7ad1da 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -68,8 +68,8 @@ (st/emit! (dm/error (tr "errors.generic"))))) (defn- handle-prepare-register-success - [_form {:keys [token] :as result}] - (st/emit! (rt/nav :auth-register-validate {} {:token token}))) + [_ params] + (st/emit! (rt/nav :auth-register-validate {} params))) (mf/defc register-form [{:keys [params] :as props}] @@ -83,8 +83,9 @@ (mf/use-callback (fn [form _event] (reset! submitted? true) - (let [params (:clean-data @form)] - (->> (rp/mutation :prepare-register-profile params) + (let [cdata (:clean-data @form)] + (->> (rp/mutation :prepare-register-profile cdata) + (rx/map #(merge % params)) (rx/finalize #(reset! submitted? false)) (rx/subs (partial handle-prepare-register-success form) (partial handle-prepare-register-error form)))))) @@ -160,13 +161,6 @@ (defn- handle-register-error [form error] (case (:code error) - :registration-disabled - (st/emit! (dm/error (tr "errors.registration-disabled"))) - - :email-has-permanent-bounces - (let [email (get @form [:data :email])] - (st/emit! (dm/error (tr "errors.email-has-permanent-bounces" email)))) - :email-already-exists (swap! form assoc-in [:errors :email] {:message "errors.email-already-exists"}) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index ca0e2502b8..905879eff3 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -7,10 +7,11 @@ (ns app.main.ui.dashboard.team (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.spec :as us] [app.config :as cfg] [app.main.data.dashboard :as dd] - [app.main.data.messages :as dm] + [app.main.data.messages :as msg] [app.main.data.modal :as modal] [app.main.data.users :as du] [app.main.refs :as refs] @@ -27,15 +28,14 @@ [cljs.spec.alpha :as s] [rumext.alpha :as mf])) -;; TEAM SECTION - (mf/defc header {::mf/wrap [mf/memo]} [{:keys [section team] :as props}] - (let [go-members (st/emitf (dd/go-to-team-members)) - go-settings (st/emitf (dd/go-to-team-settings)) - go-invitations (st/emitf (dd/go-to-team-invitations)) - invite-member (st/emitf (modal/show {:type ::invite-member :team team})) + (let [go-members (mf/use-fn #(st/emit! (dd/go-to-team-members))) + go-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings))) + go-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations))) + invite-member (mf/use-fn #(st/emit! (modal/show {:type :invite-members :team team}))) + members-section? (= section :dashboard-team-members) settings-section? (= section :dashboard-team-settings) invitations-section? (= section :dashboard-team-invitations) @@ -62,12 +62,16 @@ (tr "dashboard.invite-profile")] [:div.blank-space])]])) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; INVITATIONS MODAL +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (defn get-available-roles [permissions] (->> [{:value "editor" :label (tr "labels.editor")} (when (:is-admin permissions) {:value "admin" :label (tr "labels.admin")}) - ;; Temporarily disabled viewer role + ;; Temporarily disabled viewer roles ;; https://tree.taiga.io/project/uxboxproject/issue/1083 ;; {:value "viewer" :label (tr "labels.viewer")} ] @@ -75,31 +79,34 @@ (s/def ::emails (s/and ::us/set-of-emails d/not-empty?)) (s/def ::role ::us/keyword) -(s/def ::invite-member-form - (s/keys :req-un [::role ::emails])) +(s/def ::team-id ::us/uuid) -(mf/defc invite-member-modal +(s/def ::invite-member-form + (s/keys :req-un [::role ::emails ::team-id])) + +(mf/defc invite-members-modal {::mf/register modal/components - ::mf/register-as ::invite-member} + ::mf/register-as :invite-members} [{:keys [team]}] (let [perms (:permissions team) roles (mf/use-memo (mf/deps perms) #(get-available-roles perms)) - initial (mf/use-memo (constantly {:role "editor"})) + initial (mf/use-memo (constantly {:role "editor" :team-id (:id team)})) form (fm/use-form :spec ::invite-member-form :initial initial) error-text (mf/use-state "") on-success - (st/emitf (dm/success (tr "notifications.invitation-email-sent")) - (modal/hide) - (dd/fetch-team-invitations)) + (fn [] + (st/emit! (msg/success (tr "notifications.invitation-email-sent")) + (modal/hide) + (dd/fetch-team-invitations))) on-error (fn [{:keys [type code] :as error}] (cond (and (= :validation type) (= :profile-is-muted code)) - (st/emit! (dm/error (tr "errors.profile-is-muted")) + (st/emit! (msg/error (tr "errors.profile-is-muted")) (modal/hide)) (and (= :validation type) @@ -108,7 +115,7 @@ (swap! error-text (tr "errors.email-spam-or-permanent-bounces" (:email error))) :else - (st/emit! (dm/error (tr "errors.generic")) + (st/emit! (msg/error (tr "errors.generic")) (modal/hide)))) on-submit @@ -116,9 +123,9 @@ (let [params (:clean-data @form) mdata {:on-success (partial on-success form) :on-error (partial on-error form)}] - (st/emit! (dd/invite-team-member (with-meta params mdata)) + (st/emit! (dd/invite-team-members (with-meta params mdata)) (dd/fetch-team-invitations))))] - + [:div.modal.dashboard-invite-modal.form-container [:& fm/form {:on-submit on-submit :form form} [:div.title @@ -141,7 +148,9 @@ [:div.action-buttons [:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept")}]]]])) -;; TEAM MEMBERS SECTION +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; MEMBERS SECTION +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (mf/defc member-info [{:keys [member profile] :as props}] (let [is-you? (= (:id profile) (:id member))] @@ -210,101 +219,126 @@ {::mf/wrap [mf/memo]} [{:keys [team member members profile] :as props}] - (let [set-role - (fn [role] - (let [params {:member-id (:id member) :role role}] - (st/emit! (dd/update-team-member-role params)))) - owner? (get-in team [:permissions :is-owner]) + (let [owner? (dm/get-in team [:permissions :is-owner]) + set-role + (mf/use-fn + (mf/deps member) + (fn [role] + (let [params {:member-id (:id member) :role role}] + (st/emit! (dd/update-team-member-role params))))) - set-owner-fn (partial set-role :owner) - set-admin (partial set-role :admin) - set-editor (partial set-role :editor) + + set-owner-fn (mf/use-fn (mf/deps set-role) (partial set-role :owner)) + set-admin (mf/use-fn (mf/deps set-role) (partial set-role :admin)) + set-editor (mf/use-fn (mf/deps set-role) (partial set-role :editor)) ;; set-viewer (partial set-role :viewer) set-owner - (fn [member] - (st/emit! (modal/show - {:type :confirm - :title (tr "modals.promote-owner-confirm.title") - :message (tr "modals.promote-owner-confirm.message" (:name member)) - :scd-message (tr "modals.promote-owner-confirm.hint") - :accept-label (tr "modals.promote-owner-confirm.accept") - :on-accept set-owner-fn - :accept-style :primary}))) + (mf/use-fn + (mf/deps set-owner-fn member) + (fn [member] + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.promote-owner-confirm.title") + :message (tr "modals.promote-owner-confirm.message" (:name member)) + :scd-message (tr "modals.promote-owner-confirm.hint") + :accept-label (tr "modals.promote-owner-confirm.accept") + :on-accept set-owner-fn + :accept-style :primary})))) delete-member-fn - (st/emitf (dd/delete-team-member {:member-id (:id member)})) + (mf/use-fn + (mf/deps member) + (fn [] (st/emit! (dd/delete-team-member {:member-id (:id member)})))) on-success - (fn [] - (st/emit! (dd/go-to-projects (:default-team-id profile)) - (modal/hide) - (du/fetch-teams))) + (mf/use-fn + (mf/deps profile) + (fn [] + (st/emit! (dd/go-to-projects (:default-team-id profile)) + (modal/hide) + (du/fetch-teams)))) on-error - (fn [{:keys [code] :as error}] - (condp = code + (mf/use-fn + (fn [{:keys [code] :as error}] + (condp = code - :no-enough-members-for-leave - (rx/of (dm/error (tr "errors.team-leave.insufficient-members"))) + :no-enough-members-for-leave + (rx/of (msg/error (tr "errors.team-leave.insufficient-members"))) - :member-does-not-exist - (rx/of (dm/error (tr "errors.team-leave.member-does-not-exists"))) + :member-does-not-exist + (rx/of (msg/error (tr "errors.team-leave.member-does-not-exists"))) - :owner-cant-leave-team - (rx/of (dm/error (tr "errors.team-leave.owner-cant-leave"))) + :owner-cant-leave-team + (rx/of (msg/error (tr "errors.team-leave.owner-cant-leave"))) - (rx/throw error))) + (rx/throw error)))) delete-fn - (fn [] - (st/emit! (dd/delete-team (with-meta team {:on-success on-success - :on-error on-error})))) + (mf/use-fn + (mf/deps team on-success on-error) + (fn [] + (st/emit! (dd/delete-team (with-meta team {:on-success on-success + :on-error on-error}))))) leave-fn - (fn [member-id] - (let [params (cond-> {} (uuid? member-id) (assoc :reassign-to member-id))] - (st/emit! (dd/leave-team (with-meta params - {:on-success on-success - :on-error on-error}))))) + (mf/use-fn + (mf/deps on-success on-error) + (fn [member-id] + (let [params (cond-> {} (uuid? member-id) (assoc :reassign-to member-id))] + (st/emit! (dd/leave-team (with-meta params + {:on-success on-success + :on-error on-error})))))) leave-and-close - (st/emitf (modal/show - {:type :confirm - :title (tr "modals.leave-confirm.title") - :message (tr "modals.leave-and-close-confirm.message" (:name team)) - :scd-message (tr "modals.leave-and-close-confirm.hint") - :accept-label (tr "modals.leave-confirm.accept") - :on-accept delete-fn})) + (mf/use-fn + (mf/deps delete-fn) + (fn [] + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.leave-confirm.title") + :message (tr "modals.leave-and-close-confirm.message" (:name team)) + :scd-message (tr "modals.leave-and-close-confirm.hint") + :accept-label (tr "modals.leave-confirm.accept") + :on-accept delete-fn})))) change-owner-and-leave - (fn [] - (st/emit! (dd/fetch-team-members) - (modal/show - {:type :leave-and-reassign - :profile profile - :team team - :accept leave-fn}))) + (mf/use-fn + (mf/deps profile team leave-fn) + (fn [] + (st/emit! (dd/fetch-team-members) + (modal/show + {:type :leave-and-reassign + :profile profile + :team team + :accept leave-fn})))) leave - (st/emitf (modal/show - {:type :confirm - :title (tr "modals.leave-confirm.title") - :message (tr "modals.leave-confirm.message") - :accept-label (tr "modals.leave-confirm.accept") - :on-accept leave-fn})) + (mf/use-fn + (mf/deps leave-fn) + (fn [] + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.leave-confirm.title") + :message (tr "modals.leave-confirm.message") + :accept-label (tr "modals.leave-confirm.accept") + :on-accept leave-fn})))) preset-leave (cond (= 1 (count members)) leave-and-close (= true owner?) change-owner-and-leave :else leave) delete - (st/emitf (modal/show - {:type :confirm - :title (tr "modals.delete-team-member-confirm.title") - :message (tr "modals.delete-team-member-confirm.message") - :accept-label (tr "modals.delete-team-member-confirm.accept") - :on-accept delete-member-fn}))] + (mf/use-fn + (mf/deps delete-member-fn) + (fn [] + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.delete-team-member-confirm.title") + :message (tr "modals.delete-team-member-confirm.message") + :accept-label (tr "modals.delete-team-member-confirm.accept") + :on-accept delete-member-fn}))))] [:div.table-row [:div.table-field.name @@ -361,7 +395,9 @@ :team team :members-map members-map}]]])) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; INVITATIONS SECTION +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (mf/defc invitation-role-selector [{:keys [can-invite? role status change-to-admin change-to-editor] :as props}] @@ -418,7 +454,7 @@ :pending) on-success - #(st/emit! (dm/success (tr "notifications.invitation-email-sent")) + #(st/emit! (msg/success (tr "notifications.invitation-email-sent")) (modal/hide) (dd/fetch-team-invitations)) @@ -428,18 +464,18 @@ (cond (and (= :validation type) (= :profile-is-muted code)) - (dm/error (tr "errors.profile-is-muted")) + (msg/error (tr "errors.profile-is-muted")) (and (= :validation type) (= :member-is-muted code)) - (dm/error (tr "errors.member-is-muted")) + (msg/error (tr "errors.member-is-muted")) (and (= :validation type) (= :email-has-permanent-bounces code)) - (dm/error (tr "errors.email-has-permanent-bounces" email)) + (msg/error (tr "errors.email-has-permanent-bounces" email)) :else - (dm/error (tr "errors.generic")))) + (msg/error (tr "errors.generic")))) change-rol (fn [role] @@ -455,20 +491,31 @@ resend-invitation (fn [] - (let [params {:email email :team-id (:id team) :role invitation-role} + (let [params {:email email + :team-id (:id team) + :resend? true + :role invitation-role} mdata {:on-success on-success :on-error (partial on-error email)}] - (st/emit! (dd/invite-team-member (with-meta params mdata))) - (st/emit! (dd/fetch-team-invitations))))] + (st/emit! (dd/invite-team-members (with-meta params mdata)) + (dd/fetch-team-invitations))))] [:div.table-row [:div.table-field.mail email] - [:div.table-field.roles [:& invitation-role-selector {:can-invite? can-invite? - :role invitation-role - :status status - :change-to-editor (partial change-rol :editor) - :change-to-admin (partial change-rol :admin)}]] - [:div.table-field.status [:& invitation-status-badge {:status status}]] - [:div.table-field.actions [:& invitation-actions {:can-modify? can-invite? :delete delete-invitation :resend resend-invitation}]]])) + [:div.table-field.roles + [:& invitation-role-selector + {:can-invite? can-invite? + :role invitation-role + :status status + :change-to-editor (partial change-rol :editor) + :change-to-admin (partial change-rol :admin)}]] + + [:div.table-field.status + [:& invitation-status-badge {:status status}]] + [:div.table-field.actions + [:& invitation-actions + {:can-modify? can-invite? + :delete delete-invitation + :resend resend-invitation}]]])) (mf/defc empty-invitation-table [can-invite?] [:div.empty-invitations @@ -513,7 +560,9 @@ [:& invitation-section {:team team :invitations invitations}]]])) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SETTINGS SECTION +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (mf/defc team-settings-page [{:keys [team] :as props}] From 89e2f4a4815474327b0acce4b81e1bdc0830e646 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 16 Mar 2022 21:51:02 +0100 Subject: [PATCH 02/10] :bug: Fix crash on iOS when displaying viewer --- CHANGES.md | 1 + frontend/src/app/main/ui/viewer.cljs | 2 +- frontend/src/app/util/dom.cljs | 9 ++++++--- frontend/src/app/util/webapi.cljs | 14 +++++++++----- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6873529d94..2492e416f5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -45,6 +45,7 @@ - Fix ellipsis in long page names [Taiga #2962](https://tree.taiga.io/project/penpot/issue/2962) - Fix color palette animation [Taiga #2852](https://tree.taiga.io/project/penpot/issue/2852) - Fix display code icon on preview hover [Taiga #2838](https://tree.taiga.io/project/penpot/us/2838) +- Fix crash on iOS when displaying viewer [#1522](https://github.com/penpot/penpot/issues/1522) ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index 58cd5d4073..a7c3e860d0 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -152,7 +152,7 @@ (mf/deps fullscreen?) (fn [] ;; Trigger dom fullscreen depending on our state - (let [wrapper (dom/get-element "viewer-layout") + (let [wrapper (dom/get-element "viewer-layout") fullscreen-dom? (dom/fullscreen?)] (when (not= fullscreen? fullscreen-dom?) (if fullscreen? diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index a08ac9a681..1357cf2a12 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -7,14 +7,16 @@ (ns app.util.dom (:require [app.common.data.macros :as dm] - [app.common.exceptions :as ex] [app.common.geom.point :as gpt] + [app.common.logging :as log] [app.util.globals :as globals] [app.util.object :as obj] [cuerdas.core :as str] [goog.dom :as dom] [promesa.core :as p])) +(log/set-level! :warn) + ;; --- Deprecated methods (defn event->inner-text @@ -306,8 +308,9 @@ (boolean (.-fullscreenElement globals/document)) :else - (ex/raise :type :not-supported - :hint "seems like the current browser does not support fullscreen api."))) + (do + (log/error :msg "Seems like the current browser does not support fullscreen api.") + false))) (defn ^boolean blob? [^js v] diff --git a/frontend/src/app/util/webapi.cljs b/frontend/src/app/util/webapi.cljs index 643d6d618f..b56ac91a7a 100644 --- a/frontend/src/app/util/webapi.cljs +++ b/frontend/src/app/util/webapi.cljs @@ -8,11 +8,13 @@ "HTML5 web api helpers." (:require [app.common.data :as d] - [app.common.exceptions :as ex] + [app.common.logging :as log] [app.util.object :as obj] [beicon.core :as rx] [cuerdas.core :as str])) +(log/set-level! :warn) + (defn- file-reader [f] (rx/create @@ -114,8 +116,9 @@ (.webkitRequestFullscreen el) :else - (ex/raise :type :not-supported - :hint "seems like the current browser does not support fullscreen api."))) + (do + (log/error :msg "Seems like the current browser does not support fullscreen api.") + false))) (defn exit-fullscreen [] @@ -127,8 +130,9 @@ (.webkitExitFullscreen js/document) :else - (ex/raise :type :not-supported - :hint "seems like the current browser does not support fullscreen api."))) + (do + (log/error :msg "Seems like the current browser does not support fullscreen api.") + false))) (defn observe-resize [node] From a4d362d43d9f351f1ab32278a900f7333ced7775 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 17 Mar 2022 15:12:05 +0100 Subject: [PATCH 03/10] :bug: Fix problem when importing a SVG with text --- CHANGES.md | 1 + frontend/src/app/main/ui/shapes/attrs.cljs | 15 ++++--- .../src/app/main/ui/workspace/shapes.cljs | 33 ++++++-------- .../app/main/ui/workspace/shapes/svg_raw.cljs | 9 ++-- frontend/src/app/util/svg.cljs | 43 +++++++++++++++++++ 5 files changed, 69 insertions(+), 32 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2492e416f5..66178ed012 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -46,6 +46,7 @@ - Fix color palette animation [Taiga #2852](https://tree.taiga.io/project/penpot/issue/2852) - Fix display code icon on preview hover [Taiga #2838](https://tree.taiga.io/project/penpot/us/2838) - Fix crash on iOS when displaying viewer [#1522](https://github.com/penpot/penpot/issues/1522) +- Fix problem when importing a SVG with text [#1532](https://github.com/penpot/penpot/issues/1532) ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index b6e18d8625..d555f574ea 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -98,14 +98,8 @@ (contains? shape :fill-color) {:fill (:fill-color shape)} - ;; If contains svg-attrs the origin is svg. If it's not svg origin - ;; we setup the default fill as transparent (instead of black) - (and (not (contains? shape :svg-attrs)) - (not (#{:svg-raw :group} (:type shape)))) - {:fill "none"} - :else - {}) + {:fill "none"}) fill-attrs (cond-> fill-attrs (contains? shape :fill-opacity) @@ -212,6 +206,13 @@ (obj/set! "fill" (obj/get svg-styles "fill")) (obj/set! "fillOpacity" (obj/get svg-styles "fillOpacity"))) + ;; If contains svg-attrs the origin is svg. If it's not svg origin + ;; we setup the default fill as transparent (instead of black) + (and (contains? shape :svg-attrs) + (#{:svg-raw :group} (:type shape)) + (empty? (:fills shape))) + styles + :else (add-fill styles (d/without-nils (get-in shape [:fills 0])) render-id 0))] diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index 359e2c56f9..aa1ad49906 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -69,31 +69,24 @@ ::mf/wrap-props false} [props] (let [shape (obj/get props "shape") - opts #js {:shape shape} - - svg-element? (and (= (:type shape) :svg-raw) - (not= :svg (get-in shape [:content :tag])))] + opts #js {:shape shape}] (when (and (some? shape) (not (:hidden shape))) [:* - (if-not svg-element? - (case (:type shape) - :path [:> path/path-wrapper opts] - :text [:> text/text-wrapper opts] - :group [:> group-wrapper opts] - :rect [:> rect-wrapper opts] - :image [:> image-wrapper opts] - :circle [:> circle-wrapper opts] - :svg-raw [:> svg-raw-wrapper opts] - :bool [:> bool-wrapper opts] + (case (:type shape) + :path [:> path/path-wrapper opts] + :text [:> text/text-wrapper opts] + :group [:> group-wrapper opts] + :rect [:> rect-wrapper opts] + :image [:> image-wrapper opts] + :circle [:> circle-wrapper opts] + :svg-raw [:> svg-raw-wrapper opts] + :bool [:> bool-wrapper opts] - ;; Only used when drawing a new frame. - :frame [:> frame-wrapper opts] + ;; Only used when drawing a new frame. + :frame [:> frame-wrapper opts] - nil) - - ;; Don't wrap svg elements inside a otherwise some can break - [:> svg-raw-wrapper opts]) + nil) (when (debug? :bounding-boxes) [:> bounding-box opts])]))) diff --git a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs index 22919126a5..7a3d7b1a41 100644 --- a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs @@ -9,6 +9,7 @@ [app.main.refs :as refs] [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.svg-raw :as svg-raw] + [app.util.svg :as usvg] [rumext.alpha :as mf])) (defn svg-raw-wrapper-factory @@ -20,11 +21,9 @@ [props] (let [shape (unchecked-get props "shape") childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape))) - childs (mf/deref childs-ref)] - - - (if (or (= (get-in shape [:content :tag]) :svg) - (and (contains? shape :svg-attrs) (map? (:content shape)))) + childs (mf/deref childs-ref) + svg-tag (get-in shape [:content :tag])] + (if (contains? usvg/svg-group-safe-tags svg-tag) [:> shape-container {:shape shape} [:& svg-raw-shape {:shape shape :childs childs}]] diff --git a/frontend/src/app/util/svg.cljs b/frontend/src/app/util/svg.cljs index 549798bf88..f7a7296a38 100644 --- a/frontend/src/app/util/svg.cljs +++ b/frontend/src/app/util/svg.cljs @@ -458,6 +458,49 @@ :feTile :feTurbulence}) +;; By spec: https://www.w3.org/TR/SVG11/single-page.html#struct-GElement +(defonce svg-group-safe-tags + #{:animate + :animateColor + :animateMotion + :animateTransform + :set + :desc + :metadata + :title + :circle + :ellipse + :line + :path + :polygon + :polyline + :rect + :defs + :g + :svg + :symbol + :use + :linearGradient + :radialGradient + :a + :altGlyphDef + :clipPath + :color-profile + :cursor + :filter + :font + :font-face + :foreignObject + :image + :marker + :mask + :pattern + :script + :style + :switch + :text + :view}) + ;; Props not supported by react we need to keep them lowercase (defonce non-react-props #{:mask-type}) From eaa6ea80e608d3f20fc951ea74abe2c8de4ffeda Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 17 Mar 2022 15:12:16 +0100 Subject: [PATCH 04/10] :bug: Fix problem when adding shadows to imported text --- CHANGES.md | 1 + frontend/src/app/main/ui/shapes/filters.cljs | 23 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 66178ed012..ce87874e83 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -47,6 +47,7 @@ - Fix display code icon on preview hover [Taiga #2838](https://tree.taiga.io/project/penpot/us/2838) - Fix crash on iOS when displaying viewer [#1522](https://github.com/penpot/penpot/issues/1522) - Fix problem when importing a SVG with text [#1532](https://github.com/penpot/penpot/issues/1532) +- Fix problem when adding shadows to imported text [#Taiga 3057](https://tree.taiga.io/project/penpot/issue/3057) ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index eadd1750d0..a1370b8877 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -175,7 +175,7 @@ (if svg-root? ;; When is a raw-svg but not the root we use the whole svg as bound for the filter. Is the maximum ;; we're allowed to display - {:x 0 :y 0 :width width :height height} + {:x x :y y :width width :height height} ;; Otherwise we calculate the bound (let [filter-bounds (->> filters @@ -224,15 +224,14 @@ filter-y (/ (- (:y bounds) (:y selrect) padding) (:height selrect)) filter-width (/ (+ (:width bounds) (* 2 padding)) (:width selrect)) filter-height (/ (+ (:height bounds) (* 2 padding)) (:height selrect))] - [:* - (when (> (count filters) 2) - [:filter {:id filter-id - :x filter-x - :y filter-y - :width filter-width - :height filter-height - :filterUnits "objectBoundingBox" - :color-interpolation-filters "sRGB"} - (for [entry filters] - [:& filter-entry {:entry entry}])])])) + (when (> (count filters) 2) + [:filter {:id filter-id + :x filter-x + :y filter-y + :width filter-width + :height filter-height + :filterUnits "objectBoundingBox" + :color-interpolation-filters "sRGB"} + (for [entry filters] + [:& filter-entry {:entry entry}])]))) From 444567faac6b7817df9d9f0d8a3887aa35c62491 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 17 Mar 2022 16:21:00 +0100 Subject: [PATCH 05/10] :bug: Fix problem when importing SVG's with uses with overriding properties --- CHANGES.md | 1 + .../app/main/data/workspace/svg_upload.cljs | 36 +++++++++++++------ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ce87874e83..5cc7644952 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -48,6 +48,7 @@ - Fix crash on iOS when displaying viewer [#1522](https://github.com/penpot/penpot/issues/1522) - Fix problem when importing a SVG with text [#1532](https://github.com/penpot/penpot/issues/1532) - Fix problem when adding shadows to imported text [#Taiga 3057](https://tree.taiga.io/project/penpot/issue/3057) +- Fix problem when importing SVG's with uses with overriding properties [#Taiga 2884](https://tree.taiga.io/project/penpot/issue/2884) ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index a11af44650..bd6a8d715a 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -88,12 +88,12 @@ (get-in shape [:svg-attrs :fill-opacity]) (-> (update :svg-attrs dissoc :fill-opacity) (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :fill-opacity]) - (d/parse-double)))) + (d/parse-double)))) (get-in shape [:svg-attrs :style :fill-opacity]) (-> (update-in [:svg-attrs :style] dissoc :fill-opacity) (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :style :fill-opacity]) - (d/parse-double)))))) + (d/parse-double)))))) (defn setup-stroke [shape] (let [stroke-linecap (-> (or (get-in shape [:svg-attrs :stroke-linecap]) @@ -105,11 +105,25 @@ (cond-> shape (uc/color? (str/trim (get-in shape [:svg-attrs :stroke]))) (-> (update :svg-attrs dissoc :stroke) - (assoc-in [:strokes 0 :stroke-color] (get-in shape [:svg-attrs :stroke]))) + (assoc-in [:strokes 0 :stroke-color] (-> (get-in shape [:svg-attrs :stroke]) + (str/trim) + (uc/parse-color)))) (uc/color? (str/trim (get-in shape [:svg-attrs :style :stroke]))) (-> (update-in [:svg-attrs :style] dissoc :stroke) - (assoc-in [:strokes 0 :stroke-color] (get-in shape [:svg-attrs :style :stroke]))) + (assoc-in [:strokes 0 :stroke-color] (-> (get-in shape [:svg-attrs :style :stroke]) + (str/trim) + (uc/parse-color)))) + + (get-in shape [:svg-attrs :stroke-opacity]) + (-> (update :svg-attrs dissoc :stroke-opacity) + (assoc-in [:strokes 0 :stroke-opacity] (-> (get-in shape [:svg-attrs :stroke-opacity]) + (d/parse-double)))) + + (get-in shape [:svg-attrs :style :stroke-opacity]) + (-> (update-in [:svg-attrs :style] dissoc :stroke-opacity) + (assoc-in [:fills 0 :stroke-opacity] (-> (get-in shape [:svg-attrs :style :stroke-opacity]) + (d/parse-double)))) (get-in shape [:svg-attrs :stroke-width]) (-> (update :svg-attrs dissoc :stroke-width) @@ -123,14 +137,13 @@ (and stroke-linecap (= (:type shape) :path)) (-> (update-in [:svg-attrs :style] dissoc :stroke-linecap) - (cond-> - (#{:round :square} stroke-linecap) + (cond-> (#{:round :square} stroke-linecap) (assoc :stroke-cap-start stroke-linecap :stroke-cap-end stroke-linecap))))] - (if (d/any-key? (get-in [:strokes 0] shape) :stroke-color :stroke-opacity :stroke-width :stroke-cap-start :stroke-cap-end) - (assoc-in shape [:strokes 0 :stroke-style] :svg) - shape))) + (cond-> shape + (d/any-key? (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] (cond-> shape @@ -352,8 +365,9 @@ use-tag? (and (= :use tag) (contains? defs href-id))] (if use-tag? - (let [use-data (get defs href-id) - + (let [;; Merge the data of the use definition with the properties passed as attributes + use-data (-> (get defs href-id) + (update :attrs #(d/deep-merge % (dissoc attrs :xlink:href :href)))) displacement (gpt/point (d/parse-double (:x attrs "0")) (d/parse-double (:y attrs "0"))) disp-matrix (str (gmt/translate-matrix displacement)) element-data (-> element-data From 928128ba2dfb3c10458d2b450bf0e0a2a13e7916 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 17 Mar 2022 16:27:07 +0100 Subject: [PATCH 06/10] :bug: Fix problem when changing page while editing text --- .../src/app/main/ui/workspace/shapes/text/editor.cljs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 52139e778b..4918d52acc 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -235,11 +235,12 @@ (defn translate-point-from-viewport "Translate a point in the viewport into client coordinates" [pt viewport zoom] - (let [vbox (.. ^js viewport -viewBox -baseVal) - box (gpt/point (.-x vbox) (.-y vbox)) - zoom (gpt/point zoom)] - (-> (gpt/subtract pt box) - (gpt/multiply zoom)))) + (when (some? viewport) + (let [vbox (.. ^js viewport -viewBox -baseVal) + box (gpt/point (.-x vbox) (.-y vbox)) + zoom (gpt/point zoom)] + (-> (gpt/subtract pt box) + (gpt/multiply zoom))))) (mf/defc text-editor-viewport {::mf/wrap-props false} From e6f8269c0b472673a0b1fb80d4e186a9ad266877 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 18 Mar 2022 16:16:33 +0100 Subject: [PATCH 07/10] :bug: Fix problem with inconsistency with border-radius --- CHANGES.md | 4 + common/src/app/common/geom/shapes.cljc | 5 ++ .../src/app/common/geom/shapes/corners.cljc | 46 +++++++++++ .../src/app/common/path/shapes_to_path.cljc | 79 +++++++++++-------- frontend/src/app/main/ui/shapes/attrs.cljs | 68 +++++----------- 5 files changed, 124 insertions(+), 78 deletions(-) create mode 100644 common/src/app/common/geom/shapes/corners.cljc diff --git a/CHANGES.md b/CHANGES.md index 5cc7644952..cab75cb40b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,9 @@ ## 1.13.0-beta ### :boom: Breaking changes + +- We've changed the behaviour of the border-radius so it works as CSS that [has some limits](https://www.w3.org/TR/css-backgrounds-3/#corner-overlap). + ### :sparkles: New features - Exporting big files flow [Taiga #2218](https://tree.taiga.io/project/penpot/us/2218) @@ -49,6 +52,7 @@ - Fix problem when importing a SVG with text [#1532](https://github.com/penpot/penpot/issues/1532) - Fix problem when adding shadows to imported text [#Taiga 3057](https://tree.taiga.io/project/penpot/issue/3057) - Fix problem when importing SVG's with uses with overriding properties [#Taiga 2884](https://tree.taiga.io/project/penpot/issue/2884) +- Fix inconsistency with radius in SVG an CSS [#1587](https://github.com/penpot/penpot/issues/1587) ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index 75fe17862a..d60ffdc231 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -11,6 +11,7 @@ [app.common.geom.shapes.bool :as gsb] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.constraints :as gct] + [app.common.geom.shapes.corners :as gsc] [app.common.geom.shapes.intersect :as gin] [app.common.geom.shapes.path :as gsp] [app.common.geom.shapes.rect :as gpr] @@ -153,3 +154,7 @@ ;; Constraints (dm/export gct/default-constraints-h) (dm/export gct/default-constraints-v) + +;; Corners +(dm/export gsc/shape-corners-1) +(dm/export gsc/shape-corners-4) diff --git a/common/src/app/common/geom/shapes/corners.cljc b/common/src/app/common/geom/shapes/corners.cljc new file mode 100644 index 0000000000..16c6d17cd3 --- /dev/null +++ b/common/src/app/common/geom/shapes/corners.cljc @@ -0,0 +1,46 @@ +;; 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) UXBOX Labs SL + +(ns app.common.geom.shapes.corners) + +(defn fix-radius + ;; https://www.w3.org/TR/css-backgrounds-3/#corner-overlap + ;; + ;; > Corner curves must not overlap: When the sum of any two adjacent border radii exceeds the size of the border box, + ;; > UAs must proportionally reduce the used values of all border radii until none of them overlap. + ;; + ;; > The algorithm for reducing radii is as follows: Let f = min(Li/Si), where i ∈ {top, right, bottom, left}, Si is + ;; > the sum of the two corresponding radii of the corners on side i, and Ltop = Lbottom = the width of the box, and + ;; > Lleft = Lright = the height of the box. If f < 1, then all corner radii are reduced by multiplying them by f. + ([width height r] + (let [f (min (/ width (* 2 r)) + (/ height (* 2 r)))] + (if (< f 1) + (* r f) + r))) + + ([width height r1 r2 r3 r4] + (let [f (min (/ width (+ r1 r2)) + (/ height (+ r2 r3)) + (/ width (+ r3 r4)) + (/ height (+ r4 r1)))] + (if (< f 1) + [(* r1 f) (* r2 f) (* r3 f) (* r4 f)] + [r1 r2 r3 r4])))) + +(defn shape-corners-1 + "Retrieve the effective value for the corner given a single value for corner." + [{:keys [width height rx] :as shape}] + (if (some? rx) + (fix-radius width height rx) + 0)) + +(defn shape-corners-4 + "Retrieve the effective value for the corner given four values for the corners." + [{:keys [width height r1 r2 r3 r4]}] + (if (and (some? r1) (some? r2) (some? r3) (some? r4)) + (fix-radius width height r1 r2 r3 r4) + [r1 r2 r3 r4])) diff --git a/common/src/app/common/path/shapes_to_path.cljc b/common/src/app/common/path/shapes_to_path.cljc index 4a297f7f0b..bb4e439c3d 100644 --- a/common/src/app/common/path/shapes_to_path.cljc +++ b/common/src/app/common/path/shapes_to_path.cljc @@ -11,9 +11,11 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes.common :as gsc] + [app.common.geom.shapes.corners :as gso] [app.common.geom.shapes.path :as gsp] [app.common.path.bool :as pb] - [app.common.path.commands :as pc])) + [app.common.path.commands :as pc] + [app.common.spec.radius :as ctr])) (def ^:const bezier-circle-c 0.551915024494) @@ -93,7 +95,7 @@ (defn circle->path "Creates the bezier curves to approximate a circle shape" - [x y width height] + [{:keys [x y width height]}] (let [mx (+ x (/ width 2)) my (+ y (/ height 2)) ex (+ x width) @@ -116,35 +118,50 @@ (pc/make-curve-to p4 (assoc p3 :x c1x) (assoc p4 :y c2y)) (pc/make-curve-to p1 (assoc p4 :y c1y) (assoc p1 :x c1x))])) +(defn draw-rounded-rect-path + ([x y width height r] + (draw-rounded-rect-path x y width height r r r r)) + + ([x y width height r1 r2 r3 r4] + (let [p1 (gpt/point x (+ y r1)) + p2 (gpt/point (+ x r1) y) + + p3 (gpt/point (+ width x (- r2)) y) + p4 (gpt/point (+ width x) (+ y r2)) + + p5 (gpt/point (+ width x) (+ height y (- r3))) + p6 (gpt/point (+ width x (- r3)) (+ height y)) + + p7 (gpt/point (+ x r4) (+ height y)) + p8 (gpt/point x (+ height y (- r4)))] + (-> [] + (conj (pc/make-move-to p1)) + (cond-> (not= p1 p2) + (conj (make-corner-arc p1 p2 :top-left r1))) + (conj (pc/make-line-to p3)) + (cond-> (not= p3 p4) + (conj (make-corner-arc p3 p4 :top-right r2))) + (conj (pc/make-line-to p5)) + (cond-> (not= p5 p6) + (conj (make-corner-arc p5 p6 :bottom-right r3))) + (conj (pc/make-line-to p7)) + (cond-> (not= p7 p8) + (conj (make-corner-arc p7 p8 :bottom-left r4))) + (conj (pc/make-line-to p1)))))) + (defn rect->path "Creates a bezier curve that approximates a rounded corner rectangle" - [x y width height r1 r2 r3 r4 rx] - (let [[r1 r2 r3 r4] (->> [r1 r2 r3 r4] (mapv #(or % rx 0))) - p1 (gpt/point x (+ y r1)) - p2 (gpt/point (+ x r1) y) + [{:keys [x y width height] :as shape}] + (case (ctr/radius-mode shape) + :radius-1 + (let [radius (gso/shape-corners-1 shape)] + (draw-rounded-rect-path x y width height radius)) - p3 (gpt/point (+ width x (- r2)) y) - p4 (gpt/point (+ width x) (+ y r2)) + :radius-4 + (let [[r1 r2 r3 r4] (gso/shape-corners-4 shape)] + (draw-rounded-rect-path x y width height r1 r2 r3 r4)) - p5 (gpt/point (+ width x) (+ height y (- r3))) - p6 (gpt/point (+ width x (- r3)) (+ height y)) - - p7 (gpt/point (+ x r4) (+ height y)) - p8 (gpt/point x (+ height y (- r4)))] - (-> [] - (conj (pc/make-move-to p1)) - (cond-> (not= p1 p2) - (conj (make-corner-arc p1 p2 :top-left r1))) - (conj (pc/make-line-to p3)) - (cond-> (not= p3 p4) - (conj (make-corner-arc p3 p4 :top-right r2))) - (conj (pc/make-line-to p5)) - (cond-> (not= p5 p6) - (conj (make-corner-arc p5 p6 :bottom-right r3))) - (conj (pc/make-line-to p7)) - (cond-> (not= p7 p8) - (conj (make-corner-arc p7 p8 :bottom-left r4))) - (conj (pc/make-line-to p1))))) + [])) (declare convert-to-path) @@ -192,9 +209,9 @@ "Transforms the given shape to a path" ([shape] (convert-to-path shape {})) - ([{:keys [type x y width height r1 r2 r3 r4 rx metadata] :as shape} objects] + ([{:keys [type metadata] :as shape} objects] (assert (map? objects)) - (case (:type shape) + (case type :group (group-to-path shape objects) @@ -204,8 +221,8 @@ (:rect :circle :image :text) (let [new-content (case type - :circle (circle->path x y width height) - #_:else (rect->path x y width height r1 r2 r3 r4 rx)) + :circle (circle->path shape) + #_:else (rect->path shape)) ;; Apply the transforms that had the shape transform (:transform shape) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index d555f574ea..dd76b8a6a8 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -7,6 +7,8 @@ (ns app.main.ui.shapes.attrs (:require [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.geom.shapes :as gsh] [app.common.spec.radius :as ctr] [app.common.spec.shape :refer [stroke-caps-line stroke-caps-marker]] [app.main.ui.context :as muc] @@ -26,58 +28,30 @@ (->> values (map #(+ % width)) (str/join ",")))) -(defn- truncate-side - [shape ra-attr rb-attr dimension-attr] - (let [ra (ra-attr shape) - rb (rb-attr shape) - dimension (dimension-attr shape)] - (if (<= (+ ra rb) dimension) - [ra rb] - [(/ (* ra dimension) (+ ra rb)) - (/ (* rb dimension) (+ ra rb))]))) -(defn- truncate-radius - [shape] - (let [[r-top-left r-top-right] - (truncate-side shape :r1 :r2 :width) - - [r-right-top r-right-bottom] - (truncate-side shape :r2 :r3 :height) - - [r-bottom-right r-bottom-left] - (truncate-side shape :r3 :r4 :width) - - [r-left-bottom r-left-top] - (truncate-side shape :r4 :r1 :height)] - - [(min r-top-left r-left-top) - (min r-top-right r-right-top) - (min r-right-bottom r-bottom-right) - (min r-bottom-left r-left-bottom)])) - -(defn add-border-radius [attrs shape] +(defn add-border-radius [attrs {:keys [x y width height] :as shape}] (case (ctr/radius-mode shape) - :radius-1 - (obj/merge! attrs #js {:rx (:rx shape 0) - :ry (:ry shape 0)}) + (let [radius (gsh/shape-corners-1 shape)] + (obj/merge! attrs #js {:rx radius :ry radius})) :radius-4 - (let [[r1 r2 r3 r4] (truncate-radius shape) - top (- (:width shape) r1 r2) - right (- (:height shape) r2 r3) - bottom (- (:width shape) r3 r4) - left (- (:height shape) r4 r1)] - (obj/merge! attrs #js {:d (str "M" (+ (:x shape) r1) "," (:y shape) " " - "h" top " " - "a" r2 "," r2 " 0 0 1 " r2 "," r2 " " - "v" right " " - "a" r3 "," r3 " 0 0 1 " (- r3) "," r3 " " - "h" (- bottom) " " - "a" r4 "," r4 " 0 0 1 " (- r4) "," (- r4) " " - "v" (- left) " " - "a" r1 "," r1 " 0 0 1 " r1 "," (- r1) " " - "z")})) + (let [[r1 r2 r3 r4] (gsh/shape-corners-4 shape) + top (- width r1 r2) + right (- height r2 r3) + bottom (- width r3 r4) + left (- height r4 r1)] + (obj/merge! attrs #js {:d (dm/str + "M" (+ x r1) "," y " " + "h" top " " + "a" r2 "," r2 " 0 0 1 " r2 "," r2 " " + "v" right " " + "a" r3 "," r3 " 0 0 1 " (- r3) "," r3 " " + "h" (- bottom) " " + "a" r4 "," r4 " 0 0 1 " (- r4) "," (- r4) " " + "v" (- left) " " + "a" r1 "," r1 " 0 0 1 " r1 "," (- r1) " " + "z")})) attrs)) (defn add-fill From 118b4367e777836544c951ca97d46d9f7e6ed91f Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 21 Mar 2022 21:34:46 +0100 Subject: [PATCH 08/10] :bug: Parametrized render to embed objects. Fix problem with fonts when exporting to SVG --- exporter/src/app/handlers/resources.cljs | 1 + exporter/src/app/renderer/svg.cljs | 1 + frontend/src/app/main/fonts.cljs | 5 +++-- frontend/src/app/main/ui.cljs | 2 ++ frontend/src/app/main/ui/render.cljs | 9 +++++---- frontend/src/app/render.cljs | 8 +++++--- 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/exporter/src/app/handlers/resources.cljs b/exporter/src/app/handlers/resources.cljs index e934fdcebf..6fa3ddb1a9 100644 --- a/exporter/src/app/handlers/resources.cljs +++ b/exporter/src/app/handlers/resources.cljs @@ -26,6 +26,7 @@ (defn- get-mtype [type] + (case (d/name type) "zip" "application/zip" "pdf" "application/pdf" diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index 20d883d24c..4a965ad4da 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -327,6 +327,7 @@ :page-id page-id :object-id object-id :render-texts true + :embed true :route "render-object"} uri (-> (or uri (cf/get :public-uri)) diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs index 94187a5616..38f5ab6b0d 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -226,7 +226,7 @@ font-style: %(style)s; font-weight: %(weight)s; font-display: block; - src: url(/fonts/%(family)s-%(suffix)s.woff) format('woff'); + src: url(%(baseurl)sfonts/%(family)s-%(suffix)s.woff) format('woff'); } ") @@ -262,7 +262,8 @@ :else (let [{:keys [weight style suffix] :as variant} (d/seek #(= (:id %) font-variant-id) variants) - font-data {:family family + font-data {:baseurl (str cf/public-uri) + :family family :style style :suffix (or suffix font-variant-id) :weight weight}] diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 2fa0102c0e..11a76dbbf3 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -115,10 +115,12 @@ (let [file-id (uuid (get-in route [:path-params :file-id])) page-id (uuid (get-in route [:path-params :page-id])) object-id (uuid (get-in route [:path-params :object-id])) + embed? (= (get-in route [:query-params :embed]) "true") render-texts (get-in route [:query-params :render-texts])] [:& render/render-object {:file-id file-id :page-id page-id :object-id object-id + :embed? embed? :render-texts? (and (some? render-texts) (= render-texts "true"))}])) :render-sprite diff --git a/frontend/src/app/main/ui/render.cljs b/frontend/src/app/main/ui/render.cljs index 3b34dee801..2b5edf267d 100644 --- a/frontend/src/app/main/ui/render.cljs +++ b/frontend/src/app/main/ui/render.cljs @@ -52,8 +52,8 @@ (mf/defc object-svg {::mf/wrap [mf/memo]} - [{:keys [objects object-id zoom render-texts?] - :or {zoom 1} + [{:keys [objects object-id zoom render-texts? embed?] + :or {zoom 1 embed? false} :as props}] (let [object (get objects object-id) frame-id (if (= :frame (:type object)) @@ -106,7 +106,7 @@ {:size (str (mth/ceil width) "px " (mth/ceil height) "px")})) - [:& (mf/provider embed/context) {:value false} + [:& (mf/provider embed/context) {:value embed?} [:svg {:id "screenshot" :view-box vbox :width width @@ -152,7 +152,7 @@ objects)) (mf/defc render-object - [{:keys [file-id page-id object-id render-texts?] :as props}] + [{:keys [file-id page-id object-id render-texts? embed?] :as props}] (let [objects (mf/use-state nil)] (mf/with-effect [file-id page-id object-id] @@ -171,6 +171,7 @@ (when @objects [:& object-svg {:objects @objects :object-id object-id + :embed? embed? :render-texts? render-texts? :zoom 1}]))) diff --git a/frontend/src/app/render.cljs b/frontend/src/app/render.cljs index b8681028fc..d119796f80 100644 --- a/frontend/src/app/render.cljs +++ b/frontend/src/app/render.cljs @@ -60,17 +60,19 @@ (s/def ::file-id ::us/uuid) (s/def ::object-id ::us/uuid) (s/def ::render-text ::us/boolean) +(s/def ::embed ::us/boolean) (s/def ::render-object-params (s/keys :req-un [::file-id ::page-id ::object-id] - :opt-un [::render-text])) + :opt-un [::render-text ::embed])) (defn- render-object [params] - (let [{:keys [page-id file-id object-id render-texts]} (us/conform ::render-object-params params)] + (let [{:keys [page-id file-id object-id render-texts embed]} (us/conform ::render-object-params params)] (mf/html [:& render/render-object {:file-id file-id :page-id page-id :object-id object-id - :render-texts? (and (some? render-texts) (= render-texts "true"))}]))) + :embed? embed + :render-texts? render-texts}]))) From 6008dc12d38b32849bc87e6c17154da6265968d6 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 21 Mar 2022 21:57:00 +0100 Subject: [PATCH 09/10] :bug: Fix clickable area in layers --- CHANGES.md | 1 + frontend/resources/styles/main/partials/sidebar-layers.scss | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index cab75cb40b..36c6349e11 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -53,6 +53,7 @@ - Fix problem when adding shadows to imported text [#Taiga 3057](https://tree.taiga.io/project/penpot/issue/3057) - Fix problem when importing SVG's with uses with overriding properties [#Taiga 2884](https://tree.taiga.io/project/penpot/issue/2884) - Fix inconsistency with radius in SVG an CSS [#1587](https://github.com/penpot/penpot/issues/1587) +- Fix clickable area in layers [#1680](https://github.com/penpot/penpot/issues/1680) ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) diff --git a/frontend/resources/styles/main/partials/sidebar-layers.scss b/frontend/resources/styles/main/partials/sidebar-layers.scss index 3f8e90146b..2b67b255d3 100644 --- a/frontend/resources/styles/main/partials/sidebar-layers.scss +++ b/frontend/resources/styles/main/partials/sidebar-layers.scss @@ -215,6 +215,7 @@ span.element-name { overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap; + flex: 1; } .element-actions { From f2d1a4190ae1012b3c6ec8e8ad445e92c04de6be Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 21 Mar 2022 22:58:37 +0100 Subject: [PATCH 10/10] :sparkles: Don't stop SVG import when an image cannot be imported --- CHANGES.md | 1 + .../app/main/data/workspace/svg_upload.cljs | 97 ++++++++++--------- frontend/src/app/main/ui/shapes/attrs.cljs | 10 +- .../src/app/main/ui/shapes/custom_stroke.cljs | 27 +++++- 4 files changed, 88 insertions(+), 47 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 36c6349e11..53b0bdb971 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -37,6 +37,7 @@ - Add the ability to specify the attr for retrieve the email on OIDC integration [#1460](https://github.com/penpot/penpot/issues/1460) - Allow registration with invitation token when registration is disabled - Add the ability to disable standard, password login [Taiga #2999](https://tree.taiga.io/project/penpot/us/2999) +- Don't stop SVG import when an image cannot be imported [#1531](https://github.com/penpot/penpot/issues/1531) ### :bug: Bugs fixed diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index bd6a8d715a..6998bce8ef 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -70,30 +70,32 @@ :else (str tag)))) (defn setup-fill [shape] - (cond-> shape - ;; Color present as attribute - (uc/color? (str/trim (get-in shape [:svg-attrs :fill]))) - (-> (update :svg-attrs dissoc :fill) - (assoc-in [:fills 0 :fill-color] (-> (get-in shape [:svg-attrs :fill]) - (str/trim) - (uc/parse-color)))) + (if (some? (:fills shape)) + shape + (cond-> shape + ;; Color present as attribute + (uc/color? (str/trim (get-in shape [:svg-attrs :fill]))) + (-> (update :svg-attrs dissoc :fill) + (assoc-in [:fills 0 :fill-color] (-> (get-in shape [:svg-attrs :fill]) + (str/trim) + (uc/parse-color)))) - ;; Color present as style - (uc/color? (str/trim (get-in shape [:svg-attrs :style :fill]))) - (-> (update-in [:svg-attrs :style] dissoc :fill) - (assoc-in [:fills 0 :fill-color] (-> (get-in shape [:svg-attrs :style :fill]) - (str/trim) - (uc/parse-color)))) + ;; Color present as style + (uc/color? (str/trim (get-in shape [:svg-attrs :style :fill]))) + (-> (update-in [:svg-attrs :style] dissoc :fill) + (assoc-in [:fills 0 :fill-color] (-> (get-in shape [:svg-attrs :style :fill]) + (str/trim) + (uc/parse-color)))) - (get-in shape [:svg-attrs :fill-opacity]) - (-> (update :svg-attrs dissoc :fill-opacity) - (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :fill-opacity]) - (d/parse-double)))) + (get-in shape [:svg-attrs :fill-opacity]) + (-> (update :svg-attrs dissoc :fill-opacity) + (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :fill-opacity]) + (d/parse-double)))) - (get-in shape [:svg-attrs :style :fill-opacity]) - (-> (update-in [:svg-attrs :style] dissoc :fill-opacity) - (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :style :fill-opacity]) - (d/parse-double)))))) + (get-in shape [:svg-attrs :style :fill-opacity]) + (-> (update-in [:svg-attrs :style] dissoc :fill-opacity) + (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :style :fill-opacity]) + (d/parse-double))))))) (defn setup-stroke [shape] (let [stroke-linecap (-> (or (get-in shape [:svg-attrs :stroke-linecap]) @@ -337,18 +339,20 @@ (update :y - (:y origin))) rect-metadata (calculate-rect-metadata rect-data transform)] - (-> {:id (uuid/next) - :type :image - :name name - :frame-id frame-id - :metadata {:width (:width image-data) - :height (:height image-data) - :mtype (:mtype image-data) - :id (:id image-data)}} - (merge rect-metadata) - (assoc :svg-viewbox (select-keys rect [:x :y :width :height])) - (assoc :svg-attrs (dissoc attrs :x :y :width :height :xlink:href))))) + (when (some? image-data) + (-> {:id (uuid/next) + :type :image + :name name + :frame-id frame-id + :metadata {:width (:width image-data) + :height (:height image-data) + :mtype (:mtype image-data) + :id (:id image-data)}} + + (merge rect-metadata) + (assoc :svg-viewbox (select-keys rect [:x :y :width :height])) + (assoc :svg-attrs (dissoc attrs :x :y :width :height :xlink:href)))))) (defn parse-svg-element [frame-id svg-data element-data unames] (let [{:keys [tag attrs]} element-data @@ -389,21 +393,21 @@ :polygon (create-path-shape name frame-id svg-data (-> element-data usvg/polygon->path)) :line (create-path-shape name frame-id svg-data (-> element-data usvg/line->path)) :image (create-image-shape name frame-id svg-data element-data) - #_other (create-raw-svg name frame-id svg-data element-data))) + #_other (create-raw-svg name frame-id svg-data element-data)))] + (when (some? shape) + (let [shape (assoc shape :fills []) + shape (assoc shape :strokes []) - shape (assoc shape :fills []) - shape (assoc shape :strokes []) + shape (when (some? shape) + (-> shape + (assoc :svg-defs (select-keys (:defs svg-data) references)) + (setup-fill) + (setup-stroke))) - shape (when (some? shape) - (-> shape - (assoc :svg-defs (select-keys (:defs svg-data) references)) - (setup-fill) - (setup-stroke))) - - children (cond->> (:content element-data) - (or (= tag :g) (= tag :svg)) - (mapv #(usvg/inherit-attributes attrs %)))] - [shape children])))) + children (cond->> (:content element-data) + (or (= tag :g) (= tag :svg)) + (mapv #(usvg/inherit-attributes attrs %)))] + [shape children])))))) (defn add-svg-child-changes [page-id objects selected frame-id parent-id svg-data [unames changes] [index data]] (let [[shape children] (parse-svg-element frame-id svg-data data unames)] @@ -448,6 +452,9 @@ (->> (rp/mutation! (if (contains? uri-data :content) :upload-file-media-object :create-file-media-object-from-url) uri-data) + ;; When the image uploaded fail we skip the shape + ;; returning `nil` will afterward not create the shape. + (rx/catch #(rx/of nil)) (rx/map #(vector (:url uri-data) %))))) (rx/reduce (fn [acc [url image]] (assoc acc url image)) {}) (rx/map #(create-svg-shapes (assoc svg-data :image-data %) position)))))) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index dd76b8a6a8..2a0f3a129b 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -180,6 +180,11 @@ (obj/set! "fill" (obj/get svg-styles "fill")) (obj/set! "fillOpacity" (obj/get svg-styles "fillOpacity"))) + (obj/contains? svg-attrs "fill") + (-> styles + (obj/set! "fill" (obj/get svg-attrs "fill")) + (obj/set! "fillOpacity" (obj/get svg-attrs "fillOpacity"))) + ;; If contains svg-attrs the origin is svg. If it's not svg origin ;; we setup the default fill as transparent (instead of black) (and (contains? shape :svg-attrs) @@ -187,8 +192,11 @@ (empty? (:fills shape))) styles + (d/not-empty? (:fills shape)) + (add-fill styles (d/without-nils (get-in shape [:fills 0])) render-id 0) + :else - (add-fill styles (d/without-nils (get-in shape [:fills 0])) render-id 0))] + styles)] (-> props (obj/merge! svg-attrs) diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index fa99695b8a..c9f8d1f705 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -328,7 +328,13 @@ props (cond-> props (d/not-empty? (:shadow shape)) - (obj/set! "filter" (dm/fmt "url(#filter_%)" render-id)))] + (obj/set! "filter" (dm/fmt "url(#filter_%)" render-id))) + + svg-defs (:svg-defs shape {}) + svg-attrs (:svg-attrs shape {}) + + [svg-attrs svg-styles] + (attrs/extract-svg-attrs render-id svg-defs svg-attrs)] (cond url-fill? @@ -339,6 +345,25 @@ (obj/without ["fill" "fillOpacity"])))] (obj/set! props "fill" (dm/fmt "url(#fill-0-%)" render-id))) + (obj/contains? svg-styles "fill") + (let [style + (-> (obj/get props "style") + (obj/clone) + (obj/set! "fill" (obj/get svg-styles "fill")) + (obj/set! "fillOpacity" (obj/get svg-styles "fillOpacity")))] + (-> props + (obj/set! "style" style))) + + (obj/contains? svg-attrs "fill") + (let [style + (-> (obj/get props "style") + (obj/clone) + (obj/set! "fill" (obj/get svg-attrs "fill")) + (obj/set! "fillOpacity" (obj/get svg-attrs "fillOpacity")))] + (-> props + (obj/set! "style" style))) + + (d/not-empty? (:fills shape)) (let [fill-props (attrs/extract-fill-attrs (get-in shape [:fills 0]) render-id 0)