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:
commit
9e4650cbb6
32 changed files with 550 additions and 236 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
|
||||
(defn preconj
|
||||
[coll elem]
|
||||
(assert (vector? coll))
|
||||
(assert (or (vector? coll) (nil? coll)))
|
||||
(into [elem] coll))
|
||||
|
||||
(defn enumerate
|
||||
|
|
|
@ -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))}))))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=""
|
||||
|
|
BIN
frontend/resources/images/deco-news-left.png
Normal file
BIN
frontend/resources/images/deco-news-left.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
frontend/resources/images/deco-news-right.png
Normal file
BIN
frontend/resources/images/deco-news-right.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
frontend/resources/images/deco-newsletter.png
Normal file
BIN
frontend/resources/images/deco-newsletter.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)))))))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
47
frontend/src/app/main/ui/onboarding/newsletter.cljs
Normal file
47
frontend/src/app/main/ui/onboarding/newsletter.cljs
Normal 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"}]]]))
|
|
@ -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 {}]]])
|
||||
|
|
|
@ -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]]])
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue