diff --git a/CHANGES.md b/CHANGES.md index df5ae4b936..a159aa4579 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) + ## 1.13.0-beta ### :boom: Breaking changes @@ -46,6 +47,9 @@ ### :bug: Bugs fixed +- Fix send to back several shapes at a time [Taiga #3077](https://tree.taiga.io/project/penpot/issue/3077) +- Fix duplicate multi selected elements [Taiga #3155](https://tree.taiga.io/project/penpot/issue/3155) +- Fix add fills to artboard modify children [Taiga #3151](https://tree.taiga.io/project/penpot/issue/3151) - Avoid numeric inputs to allow big numbers [Taiga #2858](https://tree.taiga.io/project/penpot/issue/2858) - Fix component contex menu size [Taiga #2480](https://tree.taiga.io/project/penpot/issue/2480) - Add shadow to artboard make it lose the fill [Taiga #3139](https://tree.taiga.io/project/penpot/issue/3139) diff --git a/backend/scripts/repl b/backend/scripts/repl index ed84823ae6..86eaccb801 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -1,5 +1,9 @@ #!/usr/bin/env bash +export PENPOT_HOST=devenv +export PENPOT_TENANT=dev +export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-secure-session-cookies enable-audit-log enable-cors enable-transit-readable-response enable-demo-users" + # export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot" # export PENPOT_DATABASE_USERNAME="penpot" # export PENPOT_DATABASE_PASSWORD="penpot" @@ -8,7 +12,6 @@ # export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot_pre" # export PENPOT_DATABASE_USERNAME="penpot_pre" # export PENPOT_DATABASE_PASSWORD="penpot_pre" -export PENPOT_FLAGS="enable-asserts enable-audit-log $PENPOT_FLAGS" # Initialize MINIO config # mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin diff --git a/backend/scripts/start-dev b/backend/scripts/start-dev index 87f1a78a78..3a7c6f0396 100755 --- a/backend/scripts/start-dev +++ b/backend/scripts/start-dev @@ -1,6 +1,8 @@ #!/usr/bin/env bash -export PENPOT_FLAGS="$PENPOT_FLAGS enable-asserts" +export PENPOT_HOST=devenv +export PENPOT_TENANT=dev +export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-cors enable-transit-readable-response enable-demo-users" set -ex diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index c0e3ea5acd..26f65c6ece 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -312,8 +312,7 @@ ::tenant])) (def default-flags - [:enable-backend-asserts - :enable-backend-api-doc + [:enable-backend-api-doc :enable-secure-session-cookies]) (defn- parse-flags diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 6a88a10c48..455964fdb7 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -209,6 +209,9 @@ {:cron #app/cron "0 0 0 * * ?" ;; daily :task :tasks-gc} + {:cron #app/cron "0 30 */3,23 * * ?" + :task :telemetry} + (when (cf/get :fdata-storage-backed) {:cron #app/cron "0 0 * * * ?" ;; hourly :task :file-offload}) @@ -219,12 +222,7 @@ (when (contains? cf/flags :audit-log-gc) {:cron #app/cron "0 0 0 * * ?" ;; daily - :task :audit-log-gc}) - - (when (or (contains? cf/flags :telemetry) - (cf/get :telemetry-enabled)) - {:cron #app/cron "0 30 */3,23 * * ?" - :task :telemetry})]} + :task :audit-log-gc})]} :app.worker/registry {:metrics (ig/ref :app.metrics/metrics) diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index 5834b0588e..855a6ef324 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -6,6 +6,7 @@ (ns app.rpc.mutations.profile (:require + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] [app.common.uuid :as uuid] @@ -30,7 +31,7 @@ (s/def ::email ::us/email) (s/def ::fullname ::us/not-empty-string) -(s/def ::lang (s/nilable ::us/not-empty-string)) +(s/def ::lang ::us/string) (s/def ::path ::us/string) (s/def ::profile-id ::us/uuid) (s/def ::password ::us/not-empty-string) @@ -342,27 +343,41 @@ ;; --- MUTATION: Update Profile (own) -(defn- update-profile - [conn {:keys [id fullname lang theme] :as params}] - (let [profile (db/update! conn :profile - {:fullname fullname - :lang lang - :theme theme} - {:id id})] - (-> profile - (profile/decode-profile-row) - (profile/strip-private-attrs)))) - - +(s/def ::newsletter-subscribed ::us/boolean) (s/def ::update-profile - (s/keys :req-un [::id ::fullname] - :opt-un [::lang ::theme])) + (s/keys :req-un [::fullname ::profile-id] + :opt-un [::lang ::theme ::newsletter-subscribed])) (sv/defmethod ::update-profile - [{:keys [pool] :as cfg} params] + [{:keys [pool] :as cfg} {:keys [profile-id fullname lang theme newsletter-subscribed] :as params}] (db/with-atomic [conn pool] - (let [profile (update-profile conn params)] - (with-meta profile + ;; NOTE: we need to retrieve the profile independently if we use + ;; it or not for explicit locking and avoid concurrent updates of + ;; the same row/object. + (let [profile (-> (db/get-by-id conn :profile profile-id {:for-update true}) + (profile/decode-profile-row)) + + ;; Update the profile map with direct params + profile (-> profile + (assoc :fullname fullname) + (assoc :lang lang) + (assoc :theme theme)) + + ;; Update profile props if the indirect prop is coming in + ;; the params map and update the profile props data + ;; acordingly. + profile (cond-> profile + (some? newsletter-subscribed) + (update :props assoc :newsletter-subscribed newsletter-subscribed))] + + (db/update! conn :profile + {:fullname fullname + :lang lang + :theme theme + :props (db/tjson (:props profile))} + {:id profile-id}) + + (with-meta (-> profile profile/strip-private-attrs d/without-nils) {::audit/props (audit/profile->props profile)})))) ;; --- MUTATION: Update Password diff --git a/backend/src/app/tasks/telemetry.clj b/backend/src/app/tasks/telemetry.clj index 11c1411576..cf45eeeba0 100644 --- a/backend/src/app/tasks/telemetry.clj +++ b/backend/src/app/tasks/telemetry.clj @@ -12,7 +12,7 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] - [app.config :as cfg] + [app.config :as cf] [app.db :as db] [app.util.async :refer [thread-sleep]] [app.util.json :as json] @@ -25,6 +25,7 @@ (declare get-stats) (declare send!) +(declare get-subscriptions) (s/def ::http-client fn?) (s/def ::version ::us/string) @@ -38,18 +39,39 @@ (defmethod ig/init-key ::handler [_ {:keys [pool sprops version] :as cfg}] - (fn [{:keys [send?] :or {send? true}}] - ;; Sleep randomly between 0 to 10s - (when send? - (thread-sleep (rand-int 10000))) + (fn [{:keys [send? enabled?] :or {send? true enabled? false}}] + (let [subs (get-subscriptions pool) + enabled? (or enabled? + (contains? cf/flags :telemetry) + (cf/get :telemetry-enabled)) - (let [instance-id (:instance-id sprops) - stats (-> (get-stats pool version) - (assoc :instance-id instance-id))] - (when send? - (send! cfg stats)) + data {:subscriptions subs + :version version + :instance-id (:instance-id sprops)}] + (cond + ;; If we have telemetry enabled, then proceed the normal + ;; operation. + enabled? + (let [data (merge data (get-stats pool))] + (when send? + (thread-sleep (rand-int 10000)) + (send! cfg data)) + data) - stats))) + ;; If we have telemetry disabled, but there are users that are + ;; explicitly checked the newsletter subscription on the + ;; onboarding dialog or the profile section, then proceed to + ;; send a limited telemetry data, that consists in the list of + ;; subscribed emails and the running penpot version. + (seq subs) + (do + (when send? + (thread-sleep (rand-int 10000)) + (send! cfg data)) + data) + + :else + data)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; IMPL @@ -68,6 +90,12 @@ :response-status (:status response) :response-body (:body response))))) +(defn- get-subscriptions + [conn] + (let [sql "select email from profile where props->>'~:newsletter-subscribed' = 'true'"] + (->> (db/exec! conn [sql]) + (mapv :email)))) + (defn- retrieve-num-teams [conn] (-> (db/exec-one! conn ["select count(*) as count from team;"]) :count)) @@ -166,12 +194,11 @@ :user-tz (System/getProperty "user.timezone")})) (defn get-stats - [conn version] - (let [referer (if (cfg/get :telemetry-with-taiga) + [conn] + (let [referer (if (cf/get :telemetry-with-taiga) "taiga" - (cfg/get :telemetry-referer))] - (-> {:version version - :referer referer + (cf/get :telemetry-referer))] + (-> {:referer referer :total-teams (retrieve-num-teams conn) :total-projects (retrieve-num-projects conn) :total-files (retrieve-num-files conn) diff --git a/backend/test/app/tasks_telemetry_test.clj b/backend/test/app/tasks_telemetry_test.clj index 3178755546..60012716f6 100644 --- a/backend/test/app/tasks_telemetry_test.clj +++ b/backend/test/app/tasks_telemetry_test.clj @@ -21,13 +21,16 @@ (with-mocks [mock {:target 'app.tasks.telemetry/send! :return nil}] (let [task-fn (-> th/*system* :app.worker/registry :telemetry) - prof (th/create-profile* 1 {:is-active true})] + prof (th/create-profile* 1 {:is-active true + :props {:newsletter-subscribed true}})] ;; run the task - (task-fn nil) + (task-fn {:send? true :enabled? true}) (t/is (:called? @mock)) (let [[_ data] (-> @mock :call-args)] + (t/is (contains? data :subscriptions)) + (t/is (= [(:email prof)] (get data :subscriptions))) (t/is (contains? data :total-fonts)) (t/is (contains? data :total-users)) (t/is (contains? data :total-projects)) diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index a67869186d..872539945b 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -101,7 +101,7 @@ (defn preconj [coll elem] - (assert (vector? coll)) + (assert (or (vector? coll) (nil? coll))) (into [elem] coll)) (defn enumerate diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc index 96d9562c17..8ffc38e134 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/pages/changes_builder.cljc @@ -124,35 +124,35 @@ [changes id name] (-> changes (update :redo-changes conj {:type :add-page :id id :name name}) - (update :undo-changes conj {:type :del-page :id id}) + (update :undo-changes d/preconj {:type :del-page :id id}) (apply-changes-local))) (defn add-page [changes id page] (-> changes (update :redo-changes conj {:type :add-page :id id :page page}) - (update :undo-changes conj {:type :del-page :id id}) + (update :undo-changes d/preconj {:type :del-page :id id}) (apply-changes-local))) (defn mod-page [changes page new-name] (-> changes (update :redo-changes conj {:type :mod-page :id (:id page) :name new-name}) - (update :undo-changes conj {:type :mod-page :id (:id page) :name (:name page)}) + (update :undo-changes d/preconj {:type :mod-page :id (:id page) :name (:name page)}) (apply-changes-local))) (defn del-page [changes page] (-> changes (update :redo-changes conj {:type :del-page :id (:id page)}) - (update :undo-changes conj {:type :add-page :id (:id page) :page page}) + (update :undo-changes d/preconj {:type :add-page :id (:id page) :page page}) (apply-changes-local))) (defn move-page [changes page-id index prev-index] (-> changes (update :redo-changes conj {:type :mov-page :id page-id :index index}) - (update :undo-changes conj {:type :mov-page :id page-id :index prev-index}) + (update :undo-changes d/preconj {:type :mov-page :id page-id :index prev-index}) (apply-changes-local))) (defn set-page-option @@ -167,10 +167,10 @@ :page-id page-id :option option-key :value option-val}) - (update :undo-changes conj {:type :set-option - :page-id page-id - :option option-key - :value old-val}) + (update :undo-changes d/preconj {:type :set-option + :page-id page-id + :option option-key + :value old-val}) (apply-changes-local)))) (defn update-page-option @@ -186,10 +186,10 @@ :page-id page-id :option option-key :value new-val}) - (update :undo-changes conj {:type :set-option - :page-id page-id - :option option-key - :value old-val}) + (update :undo-changes d/preconj {:type :set-option + :page-id page-id + :option option-key + :value old-val}) (apply-changes-local)))) ;; Shape tree changes @@ -280,8 +280,8 @@ (update :rops conj {:type :set :attr attr :val new-val :ignore-geometry ignore-geometry? :ignore-touched ignore-touched}) - (update :uops conj {:type :set :attr attr :val old-val - :ignore-touched true}))))) + (update :uops d/preconj {:type :set :attr attr :val old-val + :ignore-touched true}))))) update-shape (fn [changes id] @@ -297,7 +297,7 @@ uops (cond-> uops (seq uops) - (conj {:type :set-touched :touched (:touched old-obj)})) + (d/preconj {:type :set-touched :touched (:touched old-obj)})) change (cond-> {:type :mod-obj :id id} @@ -404,7 +404,7 @@ operations (-> operations (update :rops conj {:type :set :attr attr :val new-val :ignore-touched true}) - (update :uops conj {:type :set :attr attr :val old-val :ignore-touched true}))))) + (update :uops d/preconj {:type :set :attr attr :val old-val :ignore-touched true}))))) resize-parent (fn [changes parent] @@ -452,7 +452,7 @@ [changes color] (-> changes (update :redo-changes conj {:type :add-color :color color}) - (update :undo-changes conj {:type :del-color :id (:id color)}) + (update :undo-changes d/preconj {:type :del-color :id (:id color)}) (apply-changes-local))) (defn update-color @@ -462,7 +462,7 @@ prev-color (get-in library-data [:colors (:id color)])] (-> changes (update :redo-changes conj {:type :mod-color :color color}) - (update :undo-changes conj {:type :mod-color :color prev-color}) + (update :undo-changes d/preconj {:type :mod-color :color prev-color}) (apply-changes-local)))) (defn delete-color @@ -472,14 +472,14 @@ prev-color (get-in library-data [:colors color-id])] (-> changes (update :redo-changes conj {:type :del-color :id color-id}) - (update :undo-changes conj {:type :add-color :color prev-color}) + (update :undo-changes d/preconj {:type :add-color :color prev-color}) (apply-changes-local)))) (defn add-media [changes object] (-> changes (update :redo-changes conj {:type :add-media :object object}) - (update :undo-changes conj {:type :del-media :id (:id object)}) + (update :undo-changes d/preconj {:type :del-media :id (:id object)}) (apply-changes-local))) (defn update-media @@ -489,7 +489,7 @@ prev-object (get-in library-data [:media (:id object)])] (-> changes (update :redo-changes conj {:type :mod-media :object object}) - (update :undo-changes conj {:type :mod-media :object prev-object}) + (update :undo-changes d/preconj {:type :mod-media :object prev-object}) (apply-changes-local)))) (defn delete-media @@ -499,14 +499,14 @@ prev-object (get-in library-data [:media id])] (-> changes (update :redo-changes conj {:type :del-media :id id}) - (update :undo-changes conj {:type :add-media :object prev-object}) + (update :undo-changes d/preconj {:type :add-media :object prev-object}) (apply-changes-local)))) (defn add-typography [changes typography] (-> changes (update :redo-changes conj {:type :add-typography :typography typography}) - (update :undo-changes conj {:type :del-typography :id (:id typography)}) + (update :undo-changes d/preconj {:type :del-typography :id (:id typography)}) (apply-changes-local))) (defn update-typography @@ -516,7 +516,7 @@ prev-typography (get-in library-data [:typographies (:id typography)])] (-> changes (update :redo-changes conj {:type :mod-typography :typography typography}) - (update :undo-changes conj {:type :mod-typography :typography prev-typography}) + (update :undo-changes d/preconj {:type :mod-typography :typography prev-typography}) (apply-changes-local)))) (defn delete-typography @@ -526,7 +526,7 @@ prev-typography (get-in library-data [:typographies typography-id])] (-> changes (update :redo-changes conj {:type :del-typography :id typography-id}) - (update :undo-changes conj {:type :add-typography :typography prev-typography}) + (update :undo-changes d/preconj {:type :add-typography :typography prev-typography}) (apply-changes-local)))) (defn add-component @@ -569,8 +569,8 @@ (update :undo-changes (fn [undo-changes] (-> undo-changes - (conj {:type :del-component - :id id}) + (d/preconj {:type :del-component + :id id}) (into (comp (map :id) (map lookupf) (map mk-change)) @@ -590,11 +590,11 @@ :name (:name new-component) :path (:path new-component) :objects (:objects new-component)}) - (update :undo-changes conj {:type :mod-component - :id id - :name (:name prev-component) - :path (:path prev-component) - :objects (:objects prev-component)})) + (update :undo-changes d/preconj {:type :mod-component + :id id + :name (:name prev-component) + :path (:path prev-component) + :objects (:objects prev-component)})) changes))) (defn delete-component @@ -605,9 +605,9 @@ (-> changes (update :redo-changes conj {:type :del-component :id id}) - (update :undo-changes conj {:type :add-component - :id id - :name (:name prev-component) - :path (:path prev-component) - :shapes (vals (:objects prev-component))})))) + (update :undo-changes d/preconj {:type :add-component + :id id + :name (:name prev-component) + :path (:path prev-component) + :shapes (vals (:objects prev-component))})))) diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml index fcb5a03510..2772b6b116 100644 --- a/docker/devenv/docker-compose.yaml +++ b/docker/devenv/docker-compose.yaml @@ -52,7 +52,6 @@ services: - PENPOT_SMTP_PASSWORD= - PENPOT_SMTP_SSL=false - PENPOT_SMTP_TLS=false - - PENPOT_FLAGS="enable-cors enable-insecure-register enable-audit-log disable-secure-session-cookies" # LDAP setup - PENPOT_LDAP_HOST=ldap @@ -119,7 +118,6 @@ services: - PENPOT_SMTP_PASSWORD= - PENPOT_SMTP_SSL=false - PENPOT_SMTP_TLS=false - - PENPOT_FLAGS="enable-cors enable-audit-log" # LDAP setup - PENPOT_LDAP_HOST=ldap diff --git a/docker/images/config.env b/docker/images/config.env index d6e4686405..484b900e43 100644 --- a/docker/images/config.env +++ b/docker/images/config.env @@ -40,7 +40,7 @@ PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com # Feature flags. Right now they are only affect frontend, but in # future release they will affect to both backend and frontend. -PENPOT_FLAGS="enable-registration enable-demo-users" +PENPOT_FLAGS="enable-registration" # Comma separated list of allowed domains to register. Empty to allow all. # PENPOT_REGISTRATION_DOMAIN_WHITELIST="" diff --git a/frontend/resources/images/deco-news-left.png b/frontend/resources/images/deco-news-left.png new file mode 100644 index 0000000000..a705b26401 Binary files /dev/null and b/frontend/resources/images/deco-news-left.png differ diff --git a/frontend/resources/images/deco-news-right.png b/frontend/resources/images/deco-news-right.png new file mode 100644 index 0000000000..f8feec2614 Binary files /dev/null and b/frontend/resources/images/deco-news-right.png differ diff --git a/frontend/resources/images/deco-newsletter.png b/frontend/resources/images/deco-newsletter.png new file mode 100644 index 0000000000..d322c62b82 Binary files /dev/null and b/frontend/resources/images/deco-newsletter.png differ diff --git a/frontend/resources/styles/main/partials/dashboard-settings.scss b/frontend/resources/styles/main/partials/dashboard-settings.scss index c5c19597b9..d45d1661ee 100644 --- a/frontend/resources/styles/main/partials/dashboard-settings.scss +++ b/frontend/resources/styles/main/partials/dashboard-settings.scss @@ -109,6 +109,38 @@ flex-direction: column; max-width: 368px; width: 100%; + + .newsletter-subs { + border-bottom: 1px solid $color-gray-20; + border-top: 1px solid $color-gray-20; + padding: 30px 0; + margin-bottom: 31px; + + .newsletter-title { + font-family: "worksans", sans-serif; + color: $color-gray-30; + font-size: $fs14; + } + + label { + font-family: "worksans", sans-serif; + color: $color-gray-60; + font-size: $fs12; + margin-right: -17px; + margin-bottom: 13px; + } + + .info { + font-family: "worksans", sans-serif; + color: $color-gray-30; + font-size: $fs12; + margin-bottom: 8px; + } + + .input-checkbox label { + align-items: flex-start; + } + } } .options-form, diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index 26f352471a..b5a8f3ce27 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -996,6 +996,57 @@ } } } + + &.newsletter { + padding: $size-5 0 0 0; + flex-direction: column; + min-width: 555px; + .modal-top { + padding: 87px 40px 0 40px; + color: $color-gray-60; + display: flex; + flex-direction: column; + + h1 { + font-family: sourcesanspro; + font-weight: bold; + font-size: $fs36; + margin-bottom: 0.75rem; + } + + p { + font-family: sourcesanspro; + font-weight: 500; + font-size: $fs16; + margin-bottom: 1.5rem; + } + } + + .modal-bottom { + margin: 0 32px; + padding: 32px 0; + color: $color-gray-60; + display: flex; + flex-direction: column; + border-top: 1px solid $color-gray-10; + + p { + font-family: "worksans", sans-serif; + text-align: left; + color: $color-gray-30; + } + } + + .modal-footer { + padding: 17px; + display: flex; + justify-content: flex-end; + + .btn-secondary { + margin-right: 16px; + } + } + } } .deco { @@ -1004,6 +1055,23 @@ top: -18px; width: 60px; + &.top { + width: 183px; + top: -106px; + left: 161px; + } + + &.newsletter-right { + left: 515px; + top: 50px; + } + + &.newsletter-left { + width: 26px; + left: -15px; + top: -15px; + } + &.right { left: 590px; top: 0; diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 456f163a78..96cd3baaf1 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -54,11 +54,14 @@ :browser :webworker)) +(def default-flags + [:enable-newsletter-subscription]) + (defn- parse-flags [global] (let [flags (obj/get global "penpotFlags" "") flags (sequence (map keyword) (str/words flags))] - (flags/parse flags/default flags))) + (flags/parse flags/default default-flags flags))) (defn- parse-version [global] diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index cd5ca770e9..1eaa37d847 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -303,8 +303,8 @@ (watch [_ _ stream] (let [mdata (meta data) on-success (:on-success mdata identity) - on-error (:on-error mdata #(rx/throw %))] - (->> (rp/mutation :update-profile data) + on-error (:on-error mdata rx/throw)] + (->> (rp/mutation :update-profile (dissoc data :props)) (rx/catch on-error) (rx/mapcat (fn [_] @@ -392,7 +392,6 @@ (->> (rp/mutation :update-profile-props {:props props}) (rx/map (constantly (fetch-profile))))))))) - (defn mark-questions-as-answered [] (ptk/reify ::mark-questions-as-answered diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index fbf2017608..4f0e74ecb4 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -578,19 +578,20 @@ (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - selected-shapes (->> (wsh/lookup-selected state) - (map (d/getf objects))) + selected-ids (wsh/lookup-selected state) + selected-shapes (map (d/getf objects) selected-ids) move-shape (fn [changes shape] (let [parent (get objects (:parent-id shape)) sibling-ids (:shapes parent) current-index (d/index-of sibling-ids (:id shape)) + index-in-selection (d/index-of selected-ids (:id shape)) new-index (case loc :top (count sibling-ids) :down (max 0 (- current-index 1)) :up (min (count sibling-ids) (+ (inc current-index) 1)) - :bottom 0)] + :bottom index-in-selection)] (pcb/change-parent changes (:id parent) [shape] @@ -631,11 +632,11 @@ (pcb/update-shapes shapes-to-detach (fn [shape] (assoc shape :component-id nil - :component-file nil - :component-root? nil - :remote-synced? nil - :shape-ref nil - :touched nil))) + :component-file nil + :component-root? nil + :remote-synced? nil + :shape-ref nil + :touched nil))) ; Make non root a component moved inside another one (pcb/update-shapes shapes-to-deroot diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index dad3d08d9f..c93412fdbf 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -8,12 +8,14 @@ (:require [app.common.colors :as clr] [app.common.data :as d] + [app.common.pages.helpers :as cph] [app.main.data.modal :as md] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.layout :as layout] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.texts :as dwt] [app.main.repo :as rp] + [app.util.color :as uc] [beicon.core :as rx] [potok.core :as ptk])) @@ -179,12 +181,12 @@ (ptk/reify ::change-fill ptk/WatchEvent (watch [_ state _] - (let [change (fn [shape attrs] + (let [change-fn (fn [shape attrs] (-> shape (cond-> (not (contains? shape :fills)) (assoc :fills [])) (assoc-in [:fills position] (into {} attrs))))] - (transform-fill state ids color change))))) + (transform-fill state ids color change-fn))))) (defn change-fill-and-clear [ids color] @@ -419,3 +421,24 @@ :fill (change-fill [(:shape-id shape)] new-color (:index shape)) :stroke (change-stroke [(:shape-id shape)] new-color (:index shape)) :content (dwt/update-text-with-function (:shape-id shape) (partial change-text-color old-color new-color (:index shape)))))))))) + +(defn apply-color-from-palette + [color is-alt?] + (ptk/reify ::apply-color-from-palette + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + selected (->> (wsh/lookup-selected state) + (cph/clean-loops objects)) + selected-obj (keep (d/getf objects) selected) + select-shapes-for-color (fn [shape objects] + (let [shapes (case (:type shape) + :group (cph/get-children objects (:id shape)) + [shape])] + (->> shapes + (remove cph/group-shape?) + (map :id)))) + ids (mapcat #(select-shapes-for-color % objects) selected-obj)] + (if is-alt? + (rx/of (change-stroke ids (merge uc/empty-color color) 0)) + (rx/of (change-fill ids (merge uc/empty-color color) 0))))))) diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index f0ee45cd97..a018111e9f 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -357,13 +357,13 @@ :operations [{:type :set :attr :content :val new-content}]})) - (update :undo-changes conj (make-change - container - {:type :mod-obj - :id (:id shape) - :operations [{:type :set - :attr :content - :val old-content}]})))] + (update :undo-changes d/preconj (make-change + container + {:type :mod-obj + :id (:id shape) + :operations [{:type :set + :attr :content + :val old-content}]})))] (if (= new-content old-content) changes changes'))) @@ -915,7 +915,7 @@ (assoc :frame-id (:frame-id shape'))))))) del-obj-change (fn [changes shape'] - (update changes :undo-changes conj + (update changes :undo-changes d/preconj (make-change container {:type :del-obj @@ -994,7 +994,7 @@ :val (:touched shape')}]})) del-obj-change (fn [changes shape'] - (update changes :undo-changes conj + (update changes :undo-changes d/preconj {:type :del-obj :id (:id shape') :page-id (:id page) @@ -1021,7 +1021,7 @@ add-undo-change (fn [changes id] (let [shape' (get objects id)] - (update changes :undo-changes conj + (update changes :undo-changes d/preconj (make-change container (as-> {:type :add-obj @@ -1073,13 +1073,13 @@ :shapes [(:id shape)] :index index-after :ignore-touched true})) - (update :undo-changes conj (make-change - container - {:type :mov-objects - :parent-id (:parent-id shape) - :shapes [(:id shape)] - :index index-before - :ignore-touched true})))] + (update :undo-changes d/preconj (make-change + container + {:type :mov-objects + :parent-id (:parent-id shape) + :shapes [(:id shape)] + :index index-before + :ignore-touched true})))] (if (and (cph/touched-group? parent :shapes-group) omit-touched?) changes @@ -1114,13 +1114,13 @@ :operations [{:type :set-touched :touched new-touched}]})) - (update :undo-changes conj (make-change - container - {:type :mod-obj - :id (:id dest-shape) - :operations - [{:type :set-touched - :touched (:touched dest-shape)}]}))))))) + (update :undo-changes d/preconj (make-change + container + {:type :mod-obj + :id (:id dest-shape) + :operations + [{:type :set-touched + :touched (:touched dest-shape)}]}))))))) (defn- change-remote-synced [changes shape container remote-synced?] @@ -1139,13 +1139,13 @@ :operations [{:type :set-remote-synced :remote-synced? remote-synced?}]})) - (update :undo-changes conj (make-change - container - {:type :mod-obj - :id (:id shape) - :operations - [{:type :set-remote-synced - :remote-synced? (:remote-synced? shape)}]})))))) + (update :undo-changes d/preconj (make-change + container + {:type :mod-obj + :id (:id shape) + :operations + [{:type :set-remote-synced + :remote-synced? (:remote-synced? shape)}]})))))) (defn- update-attrs "The main function that implements the attribute sync algorithm. Copy @@ -1191,11 +1191,11 @@ container {:type :reg-objects :shapes all-parents})) - (update :undo-changes conj (make-change - container - {:type :mod-obj - :id (:id dest-shape) - :operations uoperations})) + (update :undo-changes d/preconj (make-change + container + {:type :mod-obj + :id (:id dest-shape) + :operations uoperations})) (update :undo-changes conj (make-change container {:type :reg-objects @@ -1222,7 +1222,7 @@ uoperations) (recur (next attrs) (conj roperations roperation) - (conj uoperations uoperation))))))))) + (d/preconj uoperations uoperation))))))))) (defn- reposition-shape [shape origin-root dest-root] diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 73fe955188..ff79bcb7e7 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -483,30 +483,30 @@ (when (or (not move-delta?) (nil? (get-in state [:workspace-local :transform]))) (let [page (wsh/lookup-page state) objects (:objects page) - selected (wsh/lookup-selected state) - delta (if (and move-delta? (= (count selected) 1)) - (let [obj (get objects (first selected))] - (calc-duplicate-delta obj state objects)) - (gpt/point 0 0)) + selected (wsh/lookup-selected state)] + (when (seq selected) + (let [obj (get objects (first selected)) + delta (if move-delta? + (calc-duplicate-delta obj state objects) + (gpt/point 0 0)) - changes (->> (prepare-duplicate-changes objects page selected delta it) - (duplicate-changes-update-indices objects selected)) + changes (->> (prepare-duplicate-changes objects page selected delta it) + (duplicate-changes-update-indices objects selected)) - id-original (when (= (count selected) 1) (first selected)) + id-original (first selected) - selected (->> changes - :redo-changes - (filter #(= (:type %) :add-obj)) - (filter #(selected (:old-id %))) - (map #(get-in % [:obj :id])) - (into (d/ordered-set))) + selected (->> changes + :redo-changes + (filter #(= (:type %) :add-obj)) + (filter #(selected (:old-id %))) + (map #(get-in % [:obj :id])) + (into (d/ordered-set))) - id-duplicated (when (= (count selected) 1) (first selected))] - - ;; Warning: This order is important for the focus mode. - (rx/of (dch/commit-changes changes) - (select-shapes selected) - (memorize-duplicated id-original id-duplicated))))))) + id-duplicated (first selected)] + ;; Warning: This order is important for the focus mode. + (rx/of (dch/commit-changes changes) + (select-shapes selected) + (memorize-duplicated id-original id-duplicated))))))))) (defn change-hover-state [id value] diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index 3c61dbce5f..ec49222ae4 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -59,15 +59,15 @@ klass (str more-classes " " (dom/classnames - :focus @focus? - :valid (and touched? (not error)) - :invalid (and touched? error) - :disabled disabled - :empty (and is-text? (str/empty? value)) - :with-icon (not (nil? help-icon')) - :custom-input is-text? - :input-radio is-radio? - :input-checkbox is-checkbox?)) + :focus @focus? + :valid (and touched? (not error)) + :invalid (and touched? error) + :disabled disabled + :empty (and is-text? (str/empty? value)) + :with-icon (not (nil? help-icon')) + :custom-input is-text? + :input-radio is-radio? + :input-checkbox is-checkbox?)) swap-text-password (fn [] @@ -78,7 +78,7 @@ on-focus #(reset! focus? true) on-change (fn [event] - (let [value (-> event dom/get-target dom/get-input-value)] + (let [value (-> event dom/get-target dom/get-input-value)] (fm/on-input-change form input-name value trim))) on-blur @@ -87,16 +87,23 @@ (when-not (get-in @form [:touched input-name]) (swap! form assoc-in [:touched input-name] true))) + on-click + (fn [_] + (when-not (get-in @form [:touched input-name]) + (swap! form assoc-in [:touched input-name] true))) + props (-> props (dissoc :help-icon :form :trim :children) (assoc :id (name input-name) :value value :auto-focus auto-focus? + :on-click (when (or is-radio? is-checkbox?) on-click) :on-focus on-focus :on-blur on-blur :placeholder label :on-change on-change :type @type') + (cond-> (and value is-checkbox?) (assoc :default-checked value)) (obj/clj->props))] [:div @@ -210,7 +217,7 @@ (let [form (or form (mf/use-ctx form-ctx))] [:input.btn-primary.btn-large {:name "submit" - :class (when-not (:valid @form) "btn-disabled") + :class (when (or (not (:valid @form)) (true? disabled)) "btn-disabled") :disabled (or (not (:valid @form)) (true? disabled)) :on-click on-click :value label diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs index 76fb137557..1f904576a6 100644 --- a/frontend/src/app/main/ui/onboarding.cljs +++ b/frontend/src/app/main/ui/onboarding.cljs @@ -10,6 +10,7 @@ [app.main.data.modal :as modal] [app.main.data.users :as du] [app.main.store :as st] + [app.main.ui.onboarding.newsletter] [app.main.ui.onboarding.questions] [app.main.ui.onboarding.team-choice] [app.main.ui.onboarding.templates] @@ -134,8 +135,10 @@ [:p (tr "onboarding.slide.3.desc1")] [:p (tr "onboarding.slide.3.desc2")]] [:div.modal-navigation - [:button.btn-secondary {:on-click skip - :data-test "slide-3-btn"} (tr "labels.start")] + [:button.btn-secondary + {:on-click skip + :data-test "slide-3-btn"} + (tr "labels.start")] [:& rc/navigation-bullets {:slide slide :navigate navigate @@ -149,23 +152,23 @@ klass (mf/use-state "fadeInDown") navigate - (mf/use-callback #(reset! slide %)) + (mf/use-fn #(reset! slide %)) skip - (mf/use-callback - (st/emitf (modal/hide) - (modal/show {:type :onboarding-choice}) - (du/mark-onboarding-as-viewed)))] + (mf/use-fn + #(st/emit! (modal/hide) + (if (contains? @cf/flags :newsletter-subscription) + (modal/show {:type :onboarding-newsletter-modal}) + (modal/show {:type :onboarding-choice})) + (du/mark-onboarding-as-viewed)))] - (mf/use-layout-effect - (mf/deps @slide) - (fn [] - (when (not= :start @slide) - (reset! klass "fadeIn")) - (let [sem (tm/schedule 300 #(reset! klass nil))] - (fn [] - (reset! klass nil) - (tm/dispose! sem))))) + (mf/with-effect [@slide] + (when (not= :start @slide) + (reset! klass "fadeIn")) + (let [sem (tm/schedule 300 #(reset! klass nil))] + (fn [] + (reset! klass nil) + (tm/dispose! sem)))) [:div.modal-overlay [:div.animated {:class @klass} diff --git a/frontend/src/app/main/ui/onboarding/newsletter.cljs b/frontend/src/app/main/ui/onboarding/newsletter.cljs new file mode 100644 index 0000000000..bf72d5639f --- /dev/null +++ b/frontend/src/app/main/ui/onboarding/newsletter.cljs @@ -0,0 +1,47 @@ +;; 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.main.ui.onboarding.newsletter + (:require + [app.main.data.messages :as dm] + [app.main.data.modal :as modal] + [app.main.data.users :as du] + [app.main.store :as st] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) + +(mf/defc onboarding-newsletter-modal + {::mf/register modal/components + ::mf/register-as :onboarding-newsletter-modal} + [] + (let [message (tr "onboarding.newsletter.acceptance-message") + accept + (mf/use-callback + (fn [] + (st/emit! (dm/success message) + (modal/show {:type :onboarding-choice}) + (du/update-profile-props {:newsletter-subscribed true})))) + + decline + (mf/use-callback + (fn [] + (st/emit! (modal/show {:type :onboarding-choice}) + (du/update-profile-props {:newsletter-subscribed false}))))] + + [:div.modal-overlay + [:div.modal-container.onboarding.newsletter.animated.fadeInUp + [:div.modal-top + [:h1.newsletter-title {:data-test "onboarding-newsletter-title"} (tr "onboarding.newsletter.title")] + [:p (tr "onboarding.newsletter.desc")]] + [:div.modal-bottom + [:p (tr "onboarding.newsletter.privacy1") [:a {:target "_blank" :href "https://penpot.app/privacy.html"} (tr "onboarding.newsletter.policy")]] + [:p (tr "onboarding.newsletter.privacy2")]] + [:div.modal-footer + [:button.btn-secondary {:on-click decline} (tr "onboarding.newsletter.decline")] + [:button.btn-primary {:on-click accept} (tr "onboarding.newsletter.accept")]] + [:img.deco.top {:src "images/deco-newsletter.png" :border "0"}] + [:img.deco.newsletter-left {:src "images/deco-news-left.png" :border "0"}] + [:img.deco.newsletter-right {:src "images/deco-news-right.png" :border "0"}]]])) diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs index 1793d5bfb2..a79e0000ec 100644 --- a/frontend/src/app/main/ui/settings/options.cljs +++ b/frontend/src/app/main/ui/settings/options.cljs @@ -13,7 +13,7 @@ [app.main.store :as st] [app.main.ui.components.forms :as fm] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t tr]] + [app.util.i18n :as i18n :refer [tr]] [cljs.spec.alpha :as s] [rumext.alpha :as mf])) @@ -30,51 +30,51 @@ (defn- on-submit [form _event] (let [data (:clean-data @form) - data (cond-> data - (empty? (:lang data)) - (assoc :lang nil)) mdata {:on-success (partial on-success form)}] (st/emit! (du/update-profile (with-meta data mdata))))) (mf/defc options-form - [{:keys [locale] :as props}] + [] (let [profile (mf/deref refs/profile) + initial (mf/with-memo [profile] + (update profile :lang #(or % ""))) form (fm/use-form :spec ::options-form - :initial profile)] + :initial initial)] + [:& fm/form {:class "options-form" :on-submit on-submit :form form} - [:h2 (t locale "labels.language")] + [:h2 (tr "labels.language")] [:div.fields-row - [:& fm/select {:options (into [{:label "Auto (browser)" :value "default"}] + [:& fm/select {:options (into [{:label "Auto (browser)" :value ""}] i18n/supported-locales) - :label (t locale "dashboard.select-ui-language") + :label (tr "dashboard.select-ui-language") :default "" :name :lang :data-test "setting-lang"}]] - + ;; TODO: Do not show as long as we only have one theme - #_[:h2 (t locale "dashboard.theme-change")] + #_[:h2 (tr "dashboard.theme-change")] #_[:div.fields-row - [:& fm/select {:label (t locale "dashboard.select-ui-theme") + [:& fm/select {:label (tr "dashboard.select-ui-theme") :name :theme :default "default" :options [{:label "Default" :value "default"}] :data-test "theme-lang"}]] [:& fm/submit-button - {:label (t locale "dashboard.update-settings") + {:label (tr "dashboard.update-settings") :data-test "submit-lang-change"}]])) ;; --- Password Page (mf/defc options-page - [{:keys [locale]}] + [] (mf/use-effect #(dom/set-html-title (tr "title.settings.options"))) [:div.dashboard-settings [:div.form-container {:data-test "settings-form"} - [:& options-form {:locale locale}]]]) + [:& options-form {}]]]) diff --git a/frontend/src/app/main/ui/settings/profile.cljs b/frontend/src/app/main/ui/settings/profile.cljs index e92a71eaa6..2e549ef71e 100644 --- a/frontend/src/app/main/ui/settings/profile.cljs +++ b/frontend/src/app/main/ui/settings/profile.cljs @@ -7,7 +7,7 @@ (ns app.main.ui.settings.profile (:require [app.common.spec :as us] - [app.config :as cfg] + [app.config :as cf] [app.main.data.messages :as dm] [app.main.data.modal :as modal] [app.main.data.users :as du] @@ -17,7 +17,7 @@ [app.main.ui.components.forms :as fm] [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [tr t]] + [app.util.i18n :as i18n :refer [tr]] [cljs.spec.alpha :as s] [rumext.alpha :as mf])) @@ -40,10 +40,13 @@ ;; --- Profile Form (mf/defc profile-form - [{:keys [locale] :as props}] + [] (let [profile (mf/deref refs/profile) - form (fm/use-form :spec ::profile-form - :initial profile)] + initial (mf/with-memo [profile] + (let [subscribed? (-> profile :props :newsletter-subscribed)] + (assoc profile :newsletter-subscribed subscribed?))) + form (fm/use-form :spec ::profile-form :initial initial)] + [:& fm/form {:on-submit on-submit :form form :class "profile-form"} @@ -51,7 +54,7 @@ [:& fm/input {:type "text" :name :fullname - :label (t locale "dashboard.your-name")}]] + :label (tr "dashboard.your-name")}]] [:div.fields-row [:& fm/input @@ -59,29 +62,40 @@ :name :email :disabled true :help-icon i/at - :label (t locale "dashboard.your-email")}] + :label (tr "dashboard.your-email")}] [:div.options [:div.change-email [:a {:on-click #(modal/show! :change-email {})} - (t locale "dashboard.change-email")]]]] + (tr "dashboard.change-email")]]]] + + (when (contains? @cf/flags :newsletter-subscription) + [:div.newsletter-subs + [:p.newsletter-title (tr "dashboard.newsletter-title")] + [:& fm/input {:name :newsletter-subscribed + :class "check-primary" + :type "checkbox" + :label (tr "dashboard.newsletter-msg")}] + [:p.info (tr "onboarding.newsletter.privacy1") + [:a {:target "_blank" :href "https://penpot.app/privacy.html"} (tr "onboarding.newsletter.policy")]] + [:p.info (tr "onboarding.newsletter.privacy2")]]) [:& fm/submit-button - {:label (t locale "dashboard.update-settings")}] + {:label (tr "dashboard.save-settings") + :disabled (empty? (:touched @form))}] [:div.links [:div.link-item [:a {:on-click #(modal/show! :delete-account {}) :data-test "remove-acount-btn"} - (t locale "dashboard.remove-account")]]]])) + (tr "dashboard.remove-account")]]]])) ;; --- Profile Photo Form -(mf/defc profile-photo-form - [{:keys [locale] :as props}] - (let [file-input (mf/use-ref nil) - profile (mf/deref refs/profile) - photo (cfg/resolve-profile-photo-url profile) +(mf/defc profile-photo-form [] + (let [file-input (mf/use-ref nil) + profile (mf/deref refs/profile) + photo (cf/resolve-profile-photo-url profile) on-image-click #(dom/click (mf/ref-val file-input)) on-file-selected @@ -90,7 +104,7 @@ [:form.avatar-form [:div.image-change-field - [:span.update-overlay {:on-click on-image-click} (t locale "labels.update")] + [:span.update-overlay {:on-click on-image-click} (tr "labels.update")] [:img {:src photo}] [:& file-uploader {:accept "image/jpeg,image/png" :multi false @@ -100,14 +114,11 @@ ;; --- Profile Page -(mf/defc profile-page - [{:keys [locale]}] - - (mf/use-effect - #(dom/set-html-title (tr "title.settings.profile"))) - +(mf/defc profile-page [] + (mf/with-effect [] + (dom/set-html-title (tr "title.settings.profile"))) [:div.dashboard-settings [:div.form-container.two-columns - [:& profile-photo-form {:locale locale}] - [:& profile-form {:locale locale}]]]) + [:& profile-photo-form] + [:& profile-form]]]) diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index 28522282a5..193ba39e73 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -330,9 +330,10 @@ props (cond-> props (or ;; There are any shadows - (and (d/not-empty? (:shadow shape)) (not (cph/frame-shape? shape))) + (and (seq (->> (:shadow shape) (remove :hidden))) (not (cph/frame-shape? shape))) + ;; There are no strokes and a blur - (and (some? (:blur shape)) (not (cph/frame-shape? shape)) (empty? (:strokes shape)))) + (and (:blur shape) (-> shape :blur :hidden not) (not (cph/frame-shape? shape)) (empty? (:strokes shape)))) (obj/set! "filter" (dm/fmt "url(#filter_%)" render-id))) svg-defs (:svg-defs shape {}) @@ -415,7 +416,7 @@ stroke-props (-> (obj/new) (obj/set! "id" (dm/fmt "strokes-%" (:id shape))) (cond-> - (and (some? (:blur shape)) (not (cph/frame-shape? shape))) + (and (and (:blur shape) (-> shape :blur :hidden not)) (not (cph/frame-shape? shape))) (obj/set! "filter" (dm/fmt "url(#filter_blur_%)" render-id))))] [:* (when diff --git a/frontend/src/app/main/ui/workspace/colorpalette.cljs b/frontend/src/app/main/ui/workspace/colorpalette.cljs index 64eb5bc8fa..c468632309 100644 --- a/frontend/src/app/main/ui/workspace/colorpalette.cljs +++ b/frontend/src/app/main/ui/workspace/colorpalette.cljs @@ -13,7 +13,6 @@ [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.hooks.resize :refer [use-resize-hook]] [app.main.ui.icons :as i] - [app.util.color :as uc] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [app.util.keyboard :as kbd] @@ -40,12 +39,9 @@ ;; --- Components (mf/defc palette-item [{:keys [color]}] - (let [ids-with-children (map :id (mf/deref refs/selected-shapes-with-children)) - select-color + (let [select-color (fn [event] - (if (kbd/alt? event) - (st/emit! (mdc/change-stroke ids-with-children (merge uc/empty-color color) 0)) - (st/emit! (mdc/change-fill ids-with-children (merge uc/empty-color color) 0))))] + (st/emit! (mdc/apply-color-from-palette color (kbd/alt? event))))] [:div.color-cell {:on-click select-color} [:& cb/color-bullet {:color color}] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 971395c86b..a41e9a7153 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -581,7 +581,19 @@ msgstr "Search results" msgid "dashboard.type-something" msgstr "Type to search results" -#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.save-settings" +msgstr "Save settings" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.newsletter-title" +msgstr "Newsletter subscription" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.newsletter-msg" +msgstr "Send me news, product updates and recommendations about Penpot." + +#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Update settings" @@ -1855,6 +1867,30 @@ msgstr "" msgid "onboarding.welcome.title" msgstr "Welcome to Penpot" +msgid "onboarding.newsletter.title" +msgstr "Want to receive Penpot news?" + +msgid "onboarding.newsletter.desc" +msgstr "Subscribe to our newsletter to stay up to date with product development progress and news." + +msgid "onboarding.newsletter.privacy1" +msgstr "Because we care about privacy, here's our " + +msgid "onboarding.newsletter.policy" +msgstr "Privacy Policy." + +msgid "onboarding.newsletter.privacy2" +msgstr "We will only send relevant emails to you. You can unsubscribe at any time in your user profile or via the unsubscribe link in any of our newsletters." + +msgid "onboarding.newsletter.accept" +msgstr "Yes, subscribe" + +msgid "onboarding.newsletter.decline" +msgstr "No, thanks" + +msgid "onboarding.newsletter.acceptance-message" +msgstr "Your subscription request has been sent, we will send you an email to confirm it." + #: src/app/main/ui/auth/recovery.cljs msgid "profile.recovery.go-to-login" msgstr "Go to login" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 6b3091e857..a64cf4c5c1 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -587,7 +587,20 @@ msgstr "Resultados de búsqueda" msgid "dashboard.type-something" msgstr "Escribe algo para buscar" -#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.save-settings" +msgstr "Guardar opciones" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.newsletter-title" +msgstr "Suscripción a newsletter" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.newsletter-msg" +msgstr "Envíame noticias, actualizaciones de producto y recomendaciones sobre Penpot." + + +#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Actualizar opciones" @@ -1876,6 +1889,31 @@ msgstr "" msgid "onboarding.welcome.title" msgstr "Te damos la bienvenida a Penpot" +msgid "onboarding.newsletter.title" +msgstr "¿Quieres recibir noticias sobre Penpot?" + +msgid "onboarding.newsletter.desc" +msgstr "Suscríbete a nuestra newsletter para estar al día de los progresos del producto y noticias." + +msgid "onboarding.newsletter.privacy1" +msgstr "Porque nos importa la privacidad, aquí puedes ver nuestra " + +msgid "onboarding.newsletter.policy" +msgstr "Política de Privacidad." + +msgid "onboarding.newsletter.privacy2" +msgstr "Sólo te enviaremos emails relevantes para ti. Puedes desuscribirte en cualquier momento desde tu perfil o usando el vínculo de desuscripción en cualquiera de nuestras newsletters." + +msgid "onboarding.newsletter.accept" +msgstr "Si, suscribirme" + +msgid "onboarding.newsletter.decline" +msgstr "No, gracias" + +msgid "onboarding.newsletter.acceptance-message" +msgstr "Tu solicitud de suscripción ha sido enviada, te haremos una confirmación a tu email" + + #: src/app/main/ui/auth/recovery.cljs msgid "profile.recovery.go-to-login" msgstr "Ir al login"