0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-08 16:00:19 -05:00

Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2022-04-04 23:18:29 +02:00
commit 9e4650cbb6
32 changed files with 550 additions and 236 deletions

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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

View file

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

View file

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

View file

@ -101,7 +101,7 @@
(defn preconj
[coll elem]
(assert (vector? coll))
(assert (or (vector? coll) (nil? coll)))
(into [elem] coll))
(defn enumerate

View file

@ -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))}))))

View file

@ -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

View file

@ -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=""

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -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,

View file

@ -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;

View file

@ -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]

View file

@ -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

View file

@ -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

View file

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

View file

@ -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]

View file

@ -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]

View file

@ -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

View file

@ -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}

View file

@ -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"}]]]))

View file

@ -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 {}]]])

View file

@ -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]]])

View file

@ -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

View file

@ -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}]

View file

@ -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"

View file

@ -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"