0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-01 01:21:21 -05:00

♻️ Initial profile and auth refactor.

This commit is contained in:
Andrey Antukh 2020-05-22 13:48:21 +02:00
parent d0defe5d93
commit 7d5f9c1078
59 changed files with 2712 additions and 1407 deletions

View file

@ -0,0 +1,19 @@
-- begin :subject
Email change.
-- end
-- begin :body-text
Hello {{name}}!
We received a request to change your current email to {{ pendingEmail }}.
Click to the link below to confirm the change:
{{ publicUrl }}/#/auth/verify-token?token={{token}}
If you received this email by mistake, please consider changing your password
for security reasons.
Enjoy!
The UXBOX team.
-- end

View file

@ -1,42 +1,18 @@
-- begin :subject -- begin :subject
Password recovery. Password reset.
-- end -- end
-- begin :body-text -- begin :body-text
Hello {{name}}! Hello {{name}}!
You have requested a password recovery. We received a request to reset your password. Click the link below to choose a
new one:
The token is: {{ publicUrl }}/#/auth/recovery?token={{token}}
{{ token }} If you received this email by mistake, you can safely ignore it. Your password
won't be changed.
Enjoy!
The UXBOX team.
-- end -- end
-- begin :body-html
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta content="width=device-width" name="viewport" />
<title>title</title>
{{> ../partials/inline_style }}
</head>
<body bgcolor="#f6f6f6" cz-shortcut-listen="true">
<table class="body-wrap">
<tbody>
<tr>
<td></td>
<td bgcolor="#FFFFFF" class="container">
<div class="logo">
<img alt="UXBOX" src="{{#static}}images/email/logo.png{{/static}}" />
</div>
<p>TODO</p>
<p>{{ token }}</p>
</td>
<td></td>
</tr>
</tbody>
</table>
{{> ../partials/en/footer }}
</body>
</html>
-- end

View file

@ -1,41 +1,15 @@
-- begin :subject -- begin :subject
Welcome to UXBOX. Verify email.
-- end -- end
-- begin :body-text -- begin :body-text
Hello {{name}}! Hello {{name}}!
Welcome to UXBOX. Thanks for signing up for your UXBOX account! Please verify your email using the
link below adn get started building mockups and prototypes today!
UXBOX team. {{ publicUrl }}/#/auth/verify-token?token={{token}}
-- end
-- begin :body-html Enjoy!
<html> The UXBOX team.
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta content="width=device-width" name="viewport" />
<title>title</title>
{{> ../partials/inline_style }}
</head>
<body bgcolor="#f6f6f6" cz-shortcut-listen="true">
<table class="body-wrap">
<tbody>
<tr>
<td></td>
<td bgcolor="#FFFFFF" class="container">
<div class="logo">
<img alt="UXBOX" src="{{#static}}images/email/logo.png{{/static}}" />
</div>
<p>Hello {{name}}!</p>
<p>Welcome to UXBOX.</p>
<p>UXBOX team.</p>
</td>
<td></td>
</tr>
</tbody>
</table>
{{> ../partials/en/footer }}
</body>
</html>
-- end -- end

View file

@ -6,7 +6,10 @@ CREATE TABLE profile (
deleted_at timestamptz NULL, deleted_at timestamptz NULL,
fullname text NOT NULL DEFAULT '', fullname text NOT NULL DEFAULT '',
email text NOT NULL, email text NOT NULL,
pending_email text NULL,
photo text NOT NULL, photo text NOT NULL,
password text NOT NULL, password text NOT NULL,
@ -33,26 +36,6 @@ VALUES ('00000000-0000-0000-0000-000000000000'::uuid,
CREATE TABLE profile_email (
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
verified_at timestamptz NULL DEFAULT NULL,
email text NOT NULL,
is_main boolean NOT NULL DEFAULT false,
is_verified boolean NOT NULL DEFAULT false
);
CREATE INDEX profile_email__profile_id__idx
ON profile_email (profile_id);
CREATE UNIQUE INDEX profile_email__email__idx
ON profile_email (email);
CREATE TABLE team ( CREATE TABLE team (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
@ -121,19 +104,6 @@ BEFORE UPDATE ON profile_attr
FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); FOR EACH ROW EXECUTE PROCEDURE update_modified_at();
CREATE TABLE password_recovery_token (
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
token text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
used_at timestamptz NULL,
PRIMARY KEY (profile_id, token)
);
CREATE TABLE session ( CREATE TABLE session (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),

View file

@ -0,0 +1,6 @@
CREATE TABLE generic_token (
token text PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
valid_until timestamptz NOT NULL,
content bytea NOT NULL
);

View file

@ -26,6 +26,8 @@
:database-username "uxbox" :database-username "uxbox"
:database-password "uxbox" :database-password "uxbox"
:public-url "http://localhost:3449"
:redis-uri "redis://redis/0" :redis-uri "redis://redis/0"
:media-directory "resources/public/media" :media-directory "resources/public/media"
:assets-directory "resources/public/static" :assets-directory "resources/public/static"
@ -67,11 +69,13 @@
(s/def ::registration-enabled ::us/boolean) (s/def ::registration-enabled ::us/boolean)
(s/def ::registration-domain-whitelist ::us/string) (s/def ::registration-domain-whitelist ::us/string)
(s/def ::debug-humanize-transit ::us/boolean) (s/def ::debug-humanize-transit ::us/boolean)
(s/def ::public-url ::us/string)
(s/def ::config (s/def ::config
(s/keys :opt-un [::http-server-cors (s/keys :opt-un [::http-server-cors
::http-server-debug ::http-server-debug
::http-server-port ::http-server-port
::public-url
::database-username ::database-username
::database-password ::database-password
::database-uri ::database-uri

View file

@ -62,3 +62,11 @@
(def password-recovery (def password-recovery
"A password recovery notification email." "A password recovery notification email."
(emails/build ::password-recovery default-context)) (emails/build ::password-recovery default-context))
(s/def ::pending-email ::us/string)
(s/def ::change-email
(s/keys :req-un [::name ::pending-email ::token]))
(def change-email
"Password change confirmation email"
(emails/build ::change-email default-context))

View file

@ -18,6 +18,7 @@
#{:create-demo-profile #{:create-demo-profile
:logout :logout
:profile :profile
:verify-profile-token
:recover-profile :recover-profile
:register-profile :register-profile
:request-profile-recovery :request-profile-recovery
@ -50,8 +51,17 @@
(:profile-id req) (assoc :profile-id (:profile-id req)))] (:profile-id req) (assoc :profile-id (:profile-id req)))]
(if (or (:profile-id req) (if (or (:profile-id req)
(contains? unauthorized-services type)) (contains? unauthorized-services type))
{:status 200 (let [body (sm/handle (with-meta data {:req req}))]
:body (sm/handle (with-meta data {:req req}))} (if (= type :delete-profile)
(do
(some-> (get-in req [:cookies "auth-token" :value])
(uuid/uuid)
(session/delete))
{:status 204
:cookies {"auth-token" {:value "" :max-age -1}}
:body ""})
{:status 200
:body body}))
{:status 403 {:status 403
:body {:type :authentication :body {:type :authentication
:code :unauthorized}}))) :code :unauthorized}})))
@ -68,11 +78,11 @@
(defn logout-handler (defn logout-handler
[req] [req]
(some-> (get-in req [:cookies "auth-token"]) (some-> (get-in req [:cookies "auth-token" :value])
(uuid/uuid) (uuid/uuid)
(session/delete)) (session/delete))
{:status 204 {:status 200
:cookies {"auth-token" nil} :cookies {"auth-token" {:value "" :max-age -1}}
:body ""}) :body ""})
(defn echo-handler (defn echo-handler

View file

@ -34,8 +34,11 @@
:name "0006-presence" :name "0006-presence"
:fn (mg/resource "migrations/0006.presence.sql")} :fn (mg/resource "migrations/0006.presence.sql")}
{:desc "Remove version" {:desc "Remove version"
:name "0007.remove_version" :name "0007-remove-version"
:fn (mg/resource "migrations/0007.remove_version.sql")}]}) :fn (mg/resource "migrations/0007.remove-version.sql")}]})
{:desc "Initial generic token tables"
:name "0008-generic-token"
:fn (mg/resource "migrations/0007.generic-token.sql")}]})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Entry point ;; Entry point

View file

@ -46,6 +46,12 @@
(s/def ::old-password ::us/string) (s/def ::old-password ::us/string)
(s/def ::theme ::us/string) (s/def ::theme ::us/string)
(defn decode-token-row
[{:keys [content] :as row}]
(when row
(cond-> row
content (assoc :content (blob/decode content)))))
;; --- Mutation: Login ;; --- Mutation: Login
@ -86,15 +92,6 @@
;; --- Mutation: Update Profile (own) ;; --- Mutation: Update Profile (own)
(def ^:private sql:update-profile
"update profile
set fullname = $2,
lang = $3,
theme = $4
where id = $1
and deleted_at is null
returning *")
(defn- update-profile (defn- update-profile
[conn {:keys [id fullname lang theme] :as params}] [conn {:keys [id fullname lang theme] :as params}]
(db/update! conn :profile (db/update! conn :profile
@ -117,7 +114,7 @@
(defn- validate-password! (defn- validate-password!
[conn {:keys [profile-id old-password] :as params}] [conn {:keys [profile-id old-password] :as params}]
(let [profile (profile/retrieve-profile conn profile-id) (let [profile (profile/retrieve-profile-data conn profile-id)
result (sodi.pwhash/verify old-password (:password profile))] result (sodi.pwhash/verify old-password (:password profile))]
(when-not (:valid result) (when-not (:valid result)
(ex/raise :type :validation (ex/raise :type :validation
@ -179,14 +176,10 @@
(defn- update-profile-photo (defn- update-profile-photo
[conn profile-id path] [conn profile-id path]
(let [sql "update profile set photo=$1 (db/update! conn :profile
where id=$2 {:photo (str path)}
and deleted_at is null {:id profile-id})
returning id"] nil)
(db/update! conn :profile
{:photo (str path)}
{:id profile-id})
nil))
;; --- Mutation: Register Profile ;; --- Mutation: Register Profile
@ -211,36 +204,44 @@
[params] [params]
(when-not (:registration-enabled cfg/config) (when-not (:registration-enabled cfg/config)
(ex/raise :type :restriction (ex/raise :type :restriction
:code :registration-disabled)) :code ::registration-disabled))
(when-not (email-domain-in-whitelist? (:registration-domain-whitelist cfg/config) (when-not (email-domain-in-whitelist? (:registration-domain-whitelist cfg/config)
(:email params)) (:email params))
(ex/raise :type :validation (ex/raise :type :validation
:code ::email-domain-is-not-allowed)) :code ::email-domain-is-not-allowed))
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(check-profile-existence! conn params) (check-profile-existence! conn params)
(let [profile (register-profile conn params)] (let [profile (register-profile conn params)
;; TODO: send a correct link for email verification token (-> (sodi.prng/random-bytes 32)
(let [data {:to (:email params) (sodi.util/bytes->b64s))
:name (:fullname params)}] payload {:type :verify-email
(emails/send! conn emails/register data) :profile-id (:id profile)
profile)))) :email (:email profile)}]
(def ^:private sql:insert-profile (db/insert! conn :generic-token
"insert into profile (id, fullname, email, password, photo, is_demo) {:token token
values ($1, $2, $3, $4, '', $5) returning *") :valid-until (dt/plus (dt/now)
(dt/duration {:days 30}))
:content (blob/encode payload)})
(def ^:private sql:insert-email (emails/send! conn emails/register
"insert into profile_email (profile_id, email, is_main) {:to (:email profile)
values ($1, $2, true)") :name (:fullname profile)
:public-url (:public-url cfg/config)
:token token})
profile)))
(def ^:private sql:profile-existence (def ^:private sql:profile-existence
"select exists (select * from profile "select exists (select * from profile
where email = $1 where email = ?
and deleted_at is null) as val") and deleted_at is null) as val")
(defn- check-profile-existence! (defn- check-profile-existence!
[conn {:keys [email] :as params}] [conn {:keys [email] :as params}]
(let [result (db/exec-one! conn [sql:profile-existence email])] (let [email (str/lower email)
result (db/exec-one! conn [sql:profile-existence email])]
(when (:val result) (when (:val result)
(ex/raise :type :validation (ex/raise :type :validation
:code ::email-already-exists)) :code ::email-already-exists))
@ -256,68 +257,192 @@
(db/insert! conn :profile (db/insert! conn :profile
{:id id {:id id
:fullname fullname :fullname fullname
:email email :email (str/lower email)
:pending-email (if demo? nil email)
:photo "" :photo ""
:password password :password password
:is-demo demo?}))) :is-demo demo?})))
(defn- create-profile-email
[conn {:keys [id email] :as profile}]
(db/insert! conn :profile-email
{:profile-id id
:email email
:is-main true}))
(defn register-profile (defn register-profile
[conn params] [conn params]
(let [prof (create-profile conn params) (let [prof (create-profile conn params)
_ (create-profile-email conn prof)
team (mt.teams/create-team conn {:profile-id (:id prof) team (mt.teams/create-team conn {:profile-id (:id prof)
:name "Default" :name "Default"
:default? true}) :default? true})
_ (mt.teams/create-team-profile conn {:team-id (:id team)
:profile-id (:id prof)})
proj (mt.projects/create-project conn {:profile-id (:id prof) proj (mt.projects/create-project conn {:profile-id (:id prof)
:team-id (:id team) :team-id (:id team)
:name "Drafts" :name "Drafts"
:default? true}) :default? true})]
_ (mt.projects/create-project-profile conn {:project-id (:id proj) (mt.teams/create-team-profile conn {:team-id (:id team)
:profile-id (:id prof)}) :profile-id (:id prof)})
] (mt.projects/create-project-profile conn {:project-id (:id proj)
:profile-id (:id prof)})
;; TODO: rename to -default-team-id...
(merge (profile/strip-private-attrs prof) (merge (profile/strip-private-attrs prof)
{:default-team (:id team) {:default-team (:id team)
:default-project (:id proj)}))) :default-project (:id proj)})))
;; --- Mutation: Request Email Change
(declare select-profile-for-update)
(s/def ::request-email-change
(s/keys :req-un [::email]))
(sm/defmutation ::request-email-change
[{:keys [profile-id email] :as params}]
(db/with-atomic [conn db/pool]
(let [email (str/lower email)
profile (select-profile-for-update conn profile-id)
token (-> (sodi.prng/random-bytes 32)
(sodi.util/bytes->b64s))
payload {:type :change-email
:profile-id profile-id
:email email}]
(when (not= email (:email profile))
(check-profile-existence! conn params))
(db/update! conn :profile
{:pending-email email}
{:id profile-id})
(db/insert! conn :generic-token
{:token token
:valid-until (dt/plus (dt/now)
(dt/duration {:hours 48}))
:content (blob/encode payload)})
(emails/send! conn emails/change-email
{:to (:email profile)
:name (:fullname profile)
:public-url (:public-url cfg/config)
:pending-email email
:token token})
nil)))
(defn- select-profile-for-update
[conn id]
(db/get-by-id conn :profile id {:for-update true}))
;; --- Mutation: Verify Profile Token
;; Generic mutation for perform token based verification for auth
;; domain.
(declare retrieve-token)
(s/def ::verify-profile-token
(s/keys :req-un [::token]))
(sm/defmutation ::verify-profile-token
[{:keys [token] :as params}]
(letfn [(handle-email-change [conn token]
(let [profile (select-profile-for-update conn (:profile-id token))]
(when (not= (:email token)
(:pending-email profile))
(ex/raise :type :validation
:code ::email-does-not-match))
(check-profile-existence! conn {:email (:pending-email profile)})
(db/update! conn :profile
{:pending-email nil
:email (:pending-email profile)}
{:id (:id profile)})
token))
(handle-email-verify [conn token]
(let [profile (select-profile-for-update conn (:profile-id token))]
(when (or (not= (:email profile)
(:pending-email profile))
(not= (:email profile)
(:email token)))
(ex/raise :type :validation
:code ::invalid-token))
(db/update! conn :profile
{:pending-email nil}
{:id (:id profile)})
token))]
(db/with-atomic [conn db/pool]
(let [token (retrieve-token conn token)]
(db/delete! conn :generic-token {:token (:token params)})
;; Validate the token expiration
(when (> (inst-ms (dt/now))
(inst-ms (:valid-until token)))
(ex/raise :type :validation
:code ::invalid-token))
(case (:type token)
:change-email (handle-email-change conn token)
:verify-email (handle-email-verify conn token)
(ex/raise :type :validation
:code ::invalid-token))))))
(defn- retrieve-token
[conn token]
(let [row (-> (db/get-by-params conn :generic-token {:token token})
(decode-token-row))]
(when-not row
(ex/raise :type :validation
:code ::invalid-token))
(-> row
(dissoc :content)
(merge (:content row)))))
;; --- Mutation: Cancel Email Change
(s/def ::cancel-email-change
(s/keys :req-un [::profile-id]))
(sm/defmutation ::cancel-email-change
[{:keys [profile-id] :as params}]
(db/with-atomic [conn db/pool]
(let [profile (select-profile-for-update conn profile-id)]
(when (= (:email profile)
(:pending-email profile))
(ex/raise :type :validation
:code ::unexpected-request))
(db/update! conn :profile {:pending-email nil} {:id profile-id})
nil)))
;; --- Mutation: Request Profile Recovery ;; --- Mutation: Request Profile Recovery
(s/def ::request-profile-recovery (s/def ::request-profile-recovery
(s/keys :req-un [::email])) (s/keys :req-un [::email]))
(def sql:insert-recovery-token
"insert into password_recovery_token (profile_id, token) values ($1, $2)")
(sm/defmutation ::request-profile-recovery (sm/defmutation ::request-profile-recovery
[{:keys [email] :as params}] [{:keys [email] :as params}]
(letfn [(create-recovery-token [conn {:keys [id] :as profile}] (letfn [(create-recovery-token [conn {:keys [id] :as profile}]
(let [token (-> (sodi.prng/random-bytes 32) (let [token (-> (sodi.prng/random-bytes 32)
(sodi.util/bytes->b64s)) (sodi.util/bytes->b64s))
sql sql:insert-recovery-token] payload {:type :password-recovery-token
(db/insert! conn :password-recovery-token :profile-id id}]
{:profile-id id (db/insert! conn :generic-token
:token token}) {:token token
:valid-until (dt/plus (dt/now) (dt/duration {:hours 24}))
:content (blob/encode payload)})
(assoc profile :token token))) (assoc profile :token token)))
(send-email-notification [conn profile] (send-email-notification [conn profile]
(emails/send! conn emails/password-recovery (emails/send! conn emails/password-recovery
{:to (:email profile) {:to (:email profile)
:public-url (:public-url cfg/config)
:token (:token profile) :token (:token profile)
:name (:fullname profile)}) :name (:fullname profile)}))]
nil)]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(let [profile (->> (retrieve-profile-by-email conn email) (let [profile (->> (retrieve-profile-by-email conn email)
(create-recovery-token conn))] (create-recovery-token conn))]
(send-email-notification conn profile))))) (send-email-notification conn profile)
nil))))
;; --- Mutation: Recover Profile ;; --- Mutation: Recover Profile
@ -326,27 +451,30 @@
(s/def ::recover-profile (s/def ::recover-profile
(s/keys :req-un [::token ::password])) (s/keys :req-un [::token ::password]))
(def sql:remove-recovery-token
"delete from password_recovery_token where profile_id=$1 and token=$2")
(sm/defmutation ::recover-profile (sm/defmutation ::recover-profile
[{:keys [token password]}] [{:keys [token password]}]
(letfn [(validate-token [conn token] (letfn [(validate-token [conn token]
(let [sql "delete from password_recovery_token (let [{:keys [token content]}
where token=$1 returning *" (-> (db/get-by-params conn :generic-token {:token token})
sql "select * from password_recovery_token (decode-token-row))]
where token=$1"] (when (not= (:type content) :password-recovery-token)
(-> {:token token} (ex/raise :type :validation
(db/get-by-params conn :password-recovery-token) :code :invalid-token))
(:profile-id)))) (:profile-id content)))
(update-password [conn profile-id] (update-password [conn profile-id]
(let [sql "update profile set password=$2 where id=$1" (let [pwd (sodi.pwhash/derive password)]
pwd (sodi.pwhash/derive password)] (db/update! conn :profile {:password pwd} {:id profile-id})))
(db/update! conn :profile {:password pwd} {:id profile-id})
nil))] (delete-token [conn token]
(db/delete! conn :generic-token {:token token}))]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(-> (validate-token conn token) (->> (validate-token conn token)
(update-password conn))))) (update-password conn))
(delete-token conn token)
nil)))
;; --- Mutation: Delete Profile ;; --- Mutation: Delete Profile
@ -391,6 +519,6 @@
(let [rows (db/exec! conn [sql:teams-ownership-check profile-id])] (let [rows (db/exec! conn [sql:teams-ownership-check profile-id])]
(when-not (empty? rows) (when-not (empty? rows)
(ex/raise :type :validation (ex/raise :type :validation
:code :owner-teams-with-people :code ::owner-teams-with-people
:hint "The user need to transfer ownership of owned teams." :hint "The user need to transfer ownership of owned teams."
:context {:teams (mapv :team-id rows)})))) :context {:teams (mapv :team-id rows)}))))

View file

@ -73,8 +73,7 @@
(defn retrieve-profile-data (defn retrieve-profile-data
[conn id] [conn id]
(let [sql "select * from profile where id=? and deleted_at is null"] (db/get-by-id conn :profile id))
(db/exec-one! conn [sql id])))
(defn retrieve-profile (defn retrieve-profile
[conn id] [conn id]
@ -93,4 +92,4 @@
(defn strip-private-attrs (defn strip-private-attrs
"Only selects a publicy visible profile attrs." "Only selects a publicy visible profile attrs."
[o] [o]
(select-keys o [:id :fullname :lang :email :created-at :photo :theme :photo-uri])) (dissoc o :password :deleted-at))

View file

@ -11,6 +11,7 @@
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[clojure.walk :as walk] [clojure.walk :as walk]
[clojure.java.io :as io] [clojure.java.io :as io]
[cuerdas.core :as str]
[uxbox.common.exceptions :as ex]) [uxbox.common.exceptions :as ex])
(:import (:import
java.io.StringReader java.io.StringReader
@ -26,7 +27,7 @@
(walk/postwalk (fn [x] (walk/postwalk (fn [x]
(cond (cond
(instance? clojure.lang.Named x) (instance? clojure.lang.Named x)
(name x) (str/camel (name x))
(instance? clojure.lang.MapEntry x) (instance? clojure.lang.MapEntry x)
x x

View file

@ -17,6 +17,7 @@
java.time.OffsetDateTime java.time.OffsetDateTime
java.time.Duration java.time.Duration
java.util.Date java.util.Date
java.time.temporal.TemporalAmount
org.apache.logging.log4j.core.util.CronExpression)) org.apache.logging.log4j.core.util.CronExpression))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -32,6 +33,10 @@
[] []
(Instant/now)) (Instant/now))
(defn plus
[d ta]
(.plus d ^TemporalAmount ta))
(defn- obj->duration (defn- obj->duration
[{:keys [days minutes seconds hours nanos millis]}] [{:keys [days minutes seconds hours nanos millis]}]
(cond-> (Duration/ofMillis (if (int? millis) ^long millis 0)) (cond-> (Duration/ofMillis (if (int? millis) ^long millis 0))

View file

@ -5,10 +5,11 @@ application.
## Access to clojure from javascript console ## Access to clojure from javascript console
The uxbox namespace of the main application is exported, so that is accessible from The uxbox namespace of the main application is exported, so that is
javascript console in Chrome developer tools. Object names and data types are converted accessible from javascript console in Chrome developer tools. Object
to javascript style. For example you can emit the event to reset zoom level by typing names and data types are converted to javascript style. For example
this at the console (there is autocompletion for help): you can emit the event to reset zoom level by typing this at the
console (there is autocompletion for help):
```javascript ```javascript
uxbox.main.store.emit_BANG_(uxbox.main.data.workspace.reset_zoom) uxbox.main.store.emit_BANG_(uxbox.main.data.workspace.reset_zoom)
@ -16,17 +17,21 @@ uxbox.main.store.emit_BANG_(uxbox.main.data.workspace.reset_zoom)
## Visual debug mode and utilities ## Visual debug mode and utilities
Debugging a problem in the viewport algorithms for grouping and rotating Debugging a problem in the viewport algorithms for grouping and
is difficult. We have set a visual debug mode that displays some rotating is difficult. We have set a visual debug mode that displays
annotations on screen, to help understanding what's happening. some annotations on screen, to help understanding what's happening.
To activate it, open the javascript console and type To activate it, open the javascript console and type
```javascript ```javascript
uxbox.util.debug.toggle_debug("option") uxbox.util.debug.toggle_debug("option")
``` ```
Current options are `bounding-boxes`, `group`, `events` and `rotation-handler`.
Current options are `bounding-boxes`, `group`, `events` and
`rotation-handler`.
You can also activate or deactivate all visual aids with You can also activate or deactivate all visual aids with
```javascript ```javascript
uxbox.util.debug.debug_all() uxbox.util.debug.debug_all()
uxbox.util.debug.debug_none() uxbox.util.debug.debug_none()
@ -34,8 +39,8 @@ uxbox.util.debug.debug_none()
## Debug state and objects ## Debug state and objects
There are also some useful functions to visualize the global state or any There are also some useful functions to visualize the global state or
complex object. To use them from clojure: any complex object. To use them from clojure:
```clojure ```clojure
(ns uxbox.util.debug) (ns uxbox.util.debug)

View file

@ -1,11 +1,11 @@
{:paths ["src" "vendor" "resources" "../common"] {:paths ["src" "vendor" "resources" "../common"]
:deps :deps
{org.clojure/clojurescript {:mvn/version "1.10.753"} {org.clojure/clojurescript {:mvn/version "1.10.764"}
org.clojure/clojure {:mvn/version "1.10.1"} org.clojure/clojure {:mvn/version "1.10.1"}
com.cognitect/transit-cljs {:mvn/version "0.8.264"} com.cognitect/transit-cljs {:mvn/version "0.8.264"}
environ/environ {:mvn/version "1.1.0"} environ/environ {:mvn/version "1.2.0"}
metosin/reitit-core {:mvn/version "0.4.2"} metosin/reitit-core {:mvn/version "0.5.1"}
expound/expound {:mvn/version "0.8.4"} expound/expound {:mvn/version "0.8.4"}
danlentz/clj-uuid {:mvn/version "0.1.9"} danlentz/clj-uuid {:mvn/version "0.1.9"}
@ -17,7 +17,7 @@
funcool/okulary {:mvn/version "2020.04.14-0"} funcool/okulary {:mvn/version "2020.04.14-0"}
funcool/potok {:mvn/version "2.8.0-SNAPSHOT"} funcool/potok {:mvn/version "2.8.0-SNAPSHOT"}
funcool/promesa {:mvn/version "5.1.0"} funcool/promesa {:mvn/version "5.1.0"}
funcool/rumext {:mvn/version "2020.05.04-0"} funcool/rumext {:mvn/version "2020.05.22-1"}
} }
:aliases :aliases
{:dev {:dev

View file

@ -29,14 +29,14 @@
"integrity": "sha512-QzVKww91fJv/KzARJBS/Im5GS2A8iE64E1HxOed72EmYOvPLG4PBw77QCIUjFl7VwWB3G/SVrxsHedJD/wtn1A==" "integrity": "sha512-QzVKww91fJv/KzARJBS/Im5GS2A8iE64E1HxOed72EmYOvPLG4PBw77QCIUjFl7VwWB3G/SVrxsHedJD/wtn1A=="
}, },
"@types/lodash": { "@types/lodash": {
"version": "4.14.150", "version": "4.14.152",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.150.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.152.tgz",
"integrity": "sha512-kMNLM5JBcasgYscD9x/Gvr6lTAv2NVgsKtet/hm93qMyf/D1pt+7jeEZklKJKxMVmXjxbRVQQGfqDSfipYCO6w==" "integrity": "sha512-Vwf9YF2x1GE3WNeUMjT5bTHa2DqgUo87ocdgTScupY2JclZ5Nn7W2RLM/N0+oreexUk8uaVugR81NnTY/jNNXg=="
}, },
"@types/q": { "@types/q": {
"version": "1.5.2", "version": "1.5.4",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz",
"integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==",
"dev": true "dev": true
}, },
"ajv": { "ajv": {
@ -256,6 +256,14 @@
"bn.js": "^4.0.0", "bn.js": "^4.0.0",
"inherits": "^2.0.1", "inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0" "minimalistic-assert": "^1.0.0"
},
"dependencies": {
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
}
} }
}, },
"assert": { "assert": {
@ -352,18 +360,18 @@
"dev": true "dev": true
}, },
"autoprefixer": { "autoprefixer": {
"version": "9.7.6", "version": "9.8.0",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.6.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.0.tgz",
"integrity": "sha512-F7cYpbN7uVVhACZTeeIeealwdGM6wMtfWARVLTy5xmKtgVdBNJvbDRoCK3YO1orcs7gv/KwYlb3iXwu9Ug9BkQ==", "integrity": "sha512-D96ZiIHXbDmU02dBaemyAg53ez+6F5yZmapmgKcjm35yEe1uVDYI8hGW3VYoGRaG290ZFf91YxHrR518vC0u/A==",
"dev": true, "dev": true,
"requires": { "requires": {
"browserslist": "^4.11.1", "browserslist": "^4.12.0",
"caniuse-lite": "^1.0.30001039", "caniuse-lite": "^1.0.30001061",
"chalk": "^2.4.2", "chalk": "^2.4.2",
"normalize-range": "^0.1.2", "normalize-range": "^0.1.2",
"num2fraction": "^1.2.2", "num2fraction": "^1.2.2",
"postcss": "^7.0.27", "postcss": "^7.0.30",
"postcss-value-parser": "^4.0.3" "postcss-value-parser": "^4.1.0"
} }
}, },
"aws-sign2": { "aws-sign2": {
@ -483,15 +491,25 @@
"integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
"dev": true "dev": true
}, },
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bintrees": { "bintrees": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz",
"integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ="
}, },
"bn.js": { "bn.js": {
"version": "4.11.8", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.1.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", "integrity": "sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA==",
"dev": true "dev": true
}, },
"boolbase": { "boolbase": {
@ -602,21 +620,50 @@
"requires": { "requires": {
"bn.js": "^4.1.0", "bn.js": "^4.1.0",
"randombytes": "^2.0.1" "randombytes": "^2.0.1"
},
"dependencies": {
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
}
} }
}, },
"browserify-sign": { "browserify-sign": {
"version": "4.0.4", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.0.tgz",
"integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", "integrity": "sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA==",
"dev": true, "dev": true,
"requires": { "requires": {
"bn.js": "^4.1.1", "bn.js": "^5.1.1",
"browserify-rsa": "^4.0.0", "browserify-rsa": "^4.0.1",
"create-hash": "^1.1.0", "create-hash": "^1.2.0",
"create-hmac": "^1.1.2", "create-hmac": "^1.1.7",
"elliptic": "^6.0.0", "elliptic": "^6.5.2",
"inherits": "^2.0.1", "inherits": "^2.0.4",
"parse-asn1": "^5.0.0" "parse-asn1": "^5.1.5",
"readable-stream": "^3.6.0",
"safe-buffer": "^5.2.0"
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
}
} }
}, },
"browserify-zlib": { "browserify-zlib": {
@ -718,9 +765,9 @@
"dev": true "dev": true
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001048", "version": "1.0.30001062",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001048.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001062.tgz",
"integrity": "sha512-g1iSHKVxornw0K8LG9LLdf+Fxnv7T1Z+mMsf0/YYLclQX4Cd522Ap0Lrw6NFqHgezit78dtyWxzlV2Xfc7vgRg==", "integrity": "sha512-ei9ZqeOnN7edDrb24QfJ0OZicpEbsWxv7WusOiQGz/f2SfvBgHHbOEwBJ8HKGVSyx8Z6ndPjxzR6m0NQq+0bfw==",
"dev": true "dev": true
}, },
"caseless": { "caseless": {
@ -749,6 +796,7 @@
"anymatch": "^2.0.0", "anymatch": "^2.0.0",
"async-each": "^1.0.1", "async-each": "^1.0.1",
"braces": "^2.3.2", "braces": "^2.3.2",
"fsevents": "^1.2.7",
"glob-parent": "^3.1.0", "glob-parent": "^3.1.0",
"inherits": "^2.0.3", "inherits": "^2.0.3",
"is-binary-path": "^1.0.0", "is-binary-path": "^1.0.0",
@ -1073,6 +1121,14 @@
"requires": { "requires": {
"bn.js": "^4.1.0", "bn.js": "^4.1.0",
"elliptic": "^6.0.0" "elliptic": "^6.0.0"
},
"dependencies": {
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
}
} }
}, },
"create-hash": { "create-hash": {
@ -1233,9 +1289,9 @@
} }
}, },
"date-fns": { "date-fns": {
"version": "2.12.0", "version": "2.14.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.12.0.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.14.0.tgz",
"integrity": "sha512-qJgn99xxKnFgB1qL4jpxU7Q2t0LOn1p8KMIveef3UZD7kqjT3tpFNNdXJelEHhE+rUgffriXriw/sOSU+cS1Hw==" "integrity": "sha512-1zD+68jhFgDIM0rF05rcwYO8cExdNqxjq4xP1QKM60Q45mnO6zaMWB4tOzrIr4M4GSLntsKeE4c9Bdl2jhL/yw=="
}, },
"dateformat": { "dateformat": {
"version": "3.0.3", "version": "3.0.3",
@ -1391,6 +1447,14 @@
"bn.js": "^4.1.0", "bn.js": "^4.1.0",
"miller-rabin": "^4.0.0", "miller-rabin": "^4.0.0",
"randombytes": "^2.0.0" "randombytes": "^2.0.0"
},
"dependencies": {
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
}
} }
}, },
"direction": { "direction": {
@ -1488,9 +1552,9 @@
} }
}, },
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.426", "version": "1.3.446",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.426.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
"integrity": "sha512-sdQ7CXQbFflKY5CU63ra+kIYq9F7d1OqI33856qJZxTrwo0sLASdmoRl9lWpGrQDS9Nk/RFliQWd3PPDrZ+Meg==", "integrity": "sha512-CLQaFuvkKqR9FD2G3cJrr1fV7DRMXiAKWLP2F8cxtvvtzAS7Tubt0kF47/m+uE61kiT+I7ZEn7HqLnmWdOhmuA==",
"dev": true "dev": true
}, },
"elliptic": { "elliptic": {
@ -1506,6 +1570,14 @@
"inherits": "^2.0.1", "inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0", "minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0" "minimalistic-crypto-utils": "^1.0.0"
},
"dependencies": {
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
}
} }
}, },
"enabled": { "enabled": {
@ -1527,9 +1599,9 @@
} }
}, },
"entities": { "entities": {
"version": "2.0.0", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.2.tgz",
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==", "integrity": "sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw==",
"dev": true "dev": true
}, },
"env-variable": { "env-variable": {
@ -1917,6 +1989,13 @@
"integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==", "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==",
"dev": true "dev": true
}, },
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"optional": true
},
"fill-range": { "fill-range": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@ -2081,6 +2160,17 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true "dev": true
}, },
"fsevents": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"dev": true,
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
},
"function-bind": { "function-bind": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@ -2254,9 +2344,9 @@
}, },
"dependencies": { "dependencies": {
"gulp-cli": { "gulp-cli": {
"version": "2.2.0", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.0.tgz", "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.1.tgz",
"integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==", "integrity": "sha512-yEMxrXqY8mJFlaauFQxNrCpzWJThu0sH1sqlToaTOT063Hub9s/Nt2C+GSLe6feQ/IMWrHvGOOsyES7CQc9O+A==",
"dev": true, "dev": true,
"requires": { "requires": {
"ansi-colors": "^1.0.1", "ansi-colors": "^1.0.1",
@ -2488,9 +2578,9 @@
} }
}, },
"safe-buffer": { "safe-buffer": {
"version": "5.2.0", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true "dev": true
} }
} }
@ -3362,6 +3452,14 @@
"requires": { "requires": {
"bn.js": "^4.0.0", "bn.js": "^4.0.0",
"brorand": "^1.0.1" "brorand": "^1.0.1"
},
"dependencies": {
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
}
} }
}, },
"mime-db": { "mime-db": {
@ -3545,6 +3643,13 @@
"integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==",
"dev": true "dev": true
}, },
"nan": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==",
"dev": true,
"optional": true
},
"nanomatch": { "nanomatch": {
"version": "1.2.13", "version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@ -3616,9 +3721,9 @@
} }
}, },
"node-releases": { "node-releases": {
"version": "1.1.53", "version": "1.1.56",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.53.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.56.tgz",
"integrity": "sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ==", "integrity": "sha512-EVo605FhWLygH8a64TjgpjyHYOihkxECwX1bHHr8tETJKWEiWS2YJjPbvsX2jFjnjTNEgBCmk9mLjKG1Mf11cw==",
"dev": true "dev": true
}, },
"normalize-package-data": { "normalize-package-data": {
@ -4141,9 +4246,9 @@
"dev": true "dev": true
}, },
"postcss": { "postcss": {
"version": "7.0.27", "version": "7.0.30",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.27.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.30.tgz",
"integrity": "sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==", "integrity": "sha512-nu/0m+NtIzoubO+xdAlwZl/u5S5vi/y6BCsoL8D+8IxsD3XvBS8X4YEADNIVXKVuQvduiucnRv+vPIqj56EGMQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "^2.4.2", "chalk": "^2.4.2",
@ -4226,6 +4331,14 @@
"parse-asn1": "^5.0.0", "parse-asn1": "^5.0.0",
"randombytes": "^2.0.1", "randombytes": "^2.0.1",
"safe-buffer": "^5.1.2" "safe-buffer": "^5.1.2"
},
"dependencies": {
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
}
} }
}, },
"pump": { "pump": {
@ -4314,9 +4427,9 @@
} }
}, },
"react-color": { "react-color": {
"version": "2.18.0", "version": "2.18.1",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.18.0.tgz", "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.18.1.tgz",
"integrity": "sha512-FyVeU1kQiSokWc8NPz22azl1ezLpJdUyTbWL0LPUpcuuYDrZ/Y1veOk9rRK5B3pMlyDGvTk4f4KJhlkIQNRjEA==", "integrity": "sha512-X5XpyJS6ncplZs74ak0JJoqPi+33Nzpv5RYWWxn17bslih+X7OlgmfpmGC1fNvdkK7/SGWYf1JJdn7D2n5gSuQ==",
"requires": { "requires": {
"@icons/material": "^0.2.4", "@icons/material": "^0.2.4",
"lodash": "^4.17.11", "lodash": "^4.17.11",
@ -4488,9 +4601,9 @@
"dev": true "dev": true
}, },
"replace-ext": { "replace-ext": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz",
"integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==",
"dev": true "dev": true
}, },
"replace-homedir": { "replace-homedir": {
@ -4749,9 +4862,9 @@
} }
}, },
"shadow-cljs": { "shadow-cljs": {
"version": "2.8.109", "version": "2.9.8",
"resolved": "https://registry.npmjs.org/shadow-cljs/-/shadow-cljs-2.8.109.tgz", "resolved": "https://registry.npmjs.org/shadow-cljs/-/shadow-cljs-2.9.8.tgz",
"integrity": "sha512-xUN5kBYgyk2OVv3Gz9/dxJdDNoImskYg6VNLpHkubCG46Q1Lv9tymd11Hyekka6WWk24QCNSVIyPta82txZGfQ==", "integrity": "sha512-pZQT6hbTnT2CLN2lrp5bV9vglYd4hdlIqPqEateOZGmy+2RHYI6BLd2Zbx96hjnTaWcsSx3H9nv/B4nOGD6eDA==",
"dev": true, "dev": true,
"requires": { "requires": {
"node-libs-browser": "^2.0.0", "node-libs-browser": "^2.0.0",
@ -4878,9 +4991,9 @@
} }
}, },
"slate": { "slate": {
"version": "0.57.2", "version": "0.58.1",
"resolved": "https://registry.npmjs.org/slate/-/slate-0.57.2.tgz", "resolved": "https://registry.npmjs.org/slate/-/slate-0.58.1.tgz",
"integrity": "sha512-qxx9iwNmN3fn13hPbwh1p65aNLCgpHMMK/XXLX7rBVv+GT2UFys9tU8OK6FyUF/lU2uEJ++sScDu8cHjzwLefw==", "integrity": "sha512-2Vj1jfzHQ/X4t23iKaWoEw09iuIo1oYIsl2tZjZTEl61VNwFEIZkjzI5yuyGS4x0QnUMbNtMoOCeJQx8HxHvdw==",
"requires": { "requires": {
"@types/esrever": "^0.2.0", "@types/esrever": "^0.2.0",
"esrever": "^0.2.0", "esrever": "^0.2.0",
@ -4890,9 +5003,9 @@
} }
}, },
"slate-react": { "slate-react": {
"version": "0.57.2", "version": "0.58.1",
"resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.57.2.tgz", "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.58.1.tgz",
"integrity": "sha512-fg91E7XISMnFfoHB8vPbbaKoTDpaTfE+iwnG9i6EzbIfwNysz8XvLDpRW3XExm1ZtAfhEKB3Um8nPMtGaugVRg==", "integrity": "sha512-y94fhdUYjCFsZiN0vEMo9pxL+HA9U8RH2E4w5LxjA2ZVFk2htf8rRZC+6sq5OHBrVjp2Tw09EJbMQhrahqrtew==",
"requires": { "requires": {
"@types/is-hotkey": "^0.1.1", "@types/is-hotkey": "^0.1.1",
"@types/lodash": "^4.14.149", "@types/lodash": "^4.14.149",
@ -5078,9 +5191,9 @@
"dev": true "dev": true
}, },
"spdx-expression-parse": { "spdx-expression-parse": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
"integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"spdx-exceptions": "^2.1.0", "spdx-exceptions": "^2.1.0",
@ -5733,9 +5846,9 @@
"dev": true "dev": true
}, },
"tslib": { "tslib": {
"version": "1.11.1", "version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
"integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
}, },
"tty-browserify": { "tty-browserify": {
"version": "0.0.0", "version": "0.0.0",

View file

@ -13,7 +13,7 @@
], ],
"scripts": {}, "scripts": {},
"devDependencies": { "devDependencies": {
"autoprefixer": "^9.7.6", "autoprefixer": "^9.8.0",
"clean-css": "^4.2.3", "clean-css": "^4.2.3",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-gzip": "^1.4.2", "gulp-gzip": "^1.4.2",
@ -22,21 +22,21 @@
"gulp-rename": "^2.0.0", "gulp-rename": "^2.0.0",
"gulp-svg-sprite": "^1.5.0", "gulp-svg-sprite": "^1.5.0",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"postcss": "^7.0.27", "postcss": "^7.0.30",
"rimraf": "^3.0.0", "rimraf": "^3.0.0",
"sass": "^1.26.0", "sass": "^1.26.0",
"shadow-cljs": "^2.8.96" "shadow-cljs": "^2.9.7"
}, },
"dependencies": { "dependencies": {
"date-fns": "^2.12.0", "date-fns": "^2.13.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"randomcolor": "^0.5.4", "randomcolor": "^0.5.4",
"react": "^16.13.1", "react": "^16.13.1",
"react-color": "^2.18.0", "react-color": "^2.18.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"rxjs": "^7.0.0-beta.0", "rxjs": "^7.0.0-beta.0",
"slate": "^0.57.1", "slate": "^0.58.1",
"slate-react": "^0.57.1", "slate-react": "^0.58.1",
"source-map-support": "^0.5.16", "source-map-support": "^0.5.16",
"tdigest": "^0.1.1", "tdigest": "^0.1.1",
"xregexp": "^4.3.0" "xregexp": "^4.3.0"

View file

@ -0,0 +1,3 @@
<svg width="500" height="500" viewBox="0 0 495 500">
<path d="M175.754 251.669q0 39.766 19.744 62.569 19.744 22.525 54.227 22.525 34.204 0 53.67-22.803 19.745-22.803 19.745-62.291 0-38.933-20.023-61.736-20.022-23.081-53.948-23.081-33.649 0-53.671 22.803-19.744 22.803-19.744 62.014zm151.557 83.147q-16.685 21.413-38.376 31.702-21.413 10.011-50.056 10.011-47.83 0-77.864-34.482-29.755-34.761-29.755-90.378 0-55.618 30.033-90.379 30.033-34.76 77.586-34.76 28.643 0 50.334 10.567 21.69 10.289 38.098 31.424v-36.43h39.766v204.672q40.601-6.118 63.404-36.985 23.081-31.146 23.081-80.368 0-29.755-8.898-55.895-8.621-26.14-26.419-48.387-28.92-36.43-70.634-55.617-41.435-19.467-90.378-19.467-34.205 0-65.628 9.177-31.424 8.9-58.12 26.697-43.66 28.365-68.41 74.527Q40.603 196.329 40.603 250q0 44.216 15.851 82.87 16.13 38.654 46.44 68.131 29.2 28.921 67.576 43.938 38.376 15.295 82.036 15.295 35.873 0 70.356-12.236 34.76-11.958 63.681-34.483l25.028 30.868q-34.76 26.974-75.917 41.156Q294.774 500 252.506 500q-51.446 0-97.053-18.354-45.606-18.075-81.201-52.836-35.595-34.761-54.227-80.367Q1.393 302.558 1.393 250q0-50.612 18.91-96.496 18.91-45.884 53.949-80.645 35.873-35.317 82.87-53.95Q204.118 0 256.677 0q58.954 0 109.288 24.194 50.612 24.193 84.816 68.687 20.857 27.252 31.702 59.232 11.124 31.98 11.124 66.185 0 73.137-44.216 115.406-44.216 42.27-122.08 43.938z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,3 @@
<svg width="500" height="500">
<path d="M142.771 278.2l247.992.6-33.635 35.2-34.136 35.3 18.775 20 20.08 18.5L500 249.5 363.253 112.2l-21.285 17.4-18.876 20 34.137 35.3 33.534 35-247.992.7v57.5zM265.562 500H.702L0 499.2V.8L.703 0h264.86v45H45.18v410h220.381z"/>
</svg>

After

Width:  |  Height:  |  Size: 271 B

File diff suppressed because it is too large Load diff

View file

@ -55,10 +55,10 @@ svg {
a { a {
cursor: pointer; cursor: pointer;
color: $color-primary; color: $color-primary-dark;
&:hover { &:hover {
color: $color-primary-dark; color: $color-primary;
} }
} }

View file

@ -41,39 +41,41 @@
// Partials // Partials
//################################################# //#################################################
@import "main/partials/login";
@import "main/partials/messages";
@import "main/partials/texts";
@import "main/partials/viewer";
@import "main/partials/viewer-header";
@import "main/partials/viewer-thumbnails";
@import "main/partials/zoom-widget";
@import 'main/partials/activity-bar';
@import 'main/partials/color-palette';
@import 'main/partials/colorpicker';
@import 'main/partials/context-menu';
@import 'main/partials/dashboard-bar';
@import 'main/partials/dashboard-grid';
@import 'main/partials/debug-icons-preview';
@import 'main/partials/editable-label';
@import 'main/partials/forms';
@import 'main/partials/left-toolbar';
@import 'main/partials/library-bar';
@import 'main/partials/lightbox';
@import 'main/partials/loader';
@import 'main/partials/main-bar'; @import 'main/partials/main-bar';
@import 'main/partials/workspace'; @import 'main/partials/modal';
@import 'main/partials/workspace-header';
@import 'main/partials/workspace-libraries';
@import 'main/partials/tool-bar';
@import 'main/partials/project-bar'; @import 'main/partials/project-bar';
@import 'main/partials/sidebar'; @import 'main/partials/sidebar';
@import 'main/partials/sidebar-tools';
@import 'main/partials/sidebar-align-options'; @import 'main/partials/sidebar-align-options';
@import 'main/partials/sidebar-document-history';
@import 'main/partials/sidebar-element-options'; @import 'main/partials/sidebar-element-options';
@import 'main/partials/sidebar-icons'; @import 'main/partials/sidebar-icons';
@import 'main/partials/sidebar-interactions'; @import 'main/partials/sidebar-interactions';
@import 'main/partials/sidebar-layers'; @import 'main/partials/sidebar-layers';
@import 'main/partials/sidebar-sitemap'; @import 'main/partials/sidebar-sitemap';
@import 'main/partials/sidebar-document-history'; @import 'main/partials/sidebar-tools';
@import 'main/partials/left-toolbar';
@import 'main/partials/dashboard-bar';
@import 'main/partials/dashboard-grid';
@import 'main/partials/user-settings';
@import 'main/partials/activity-bar';
@import 'main/partials/library-bar';
@import 'main/partials/lightbox';
@import 'main/partials/color-palette';
@import 'main/partials/colorpicker';
@import 'main/partials/forms';
@import 'main/partials/loader';
@import 'main/partials/context-menu';
@import 'main/partials/debug-icons-preview';
@import 'main/partials/editable-label';
@import 'main/partials/tab-container'; @import 'main/partials/tab-container';
@import "main/partials/zoom-widget"; @import 'main/partials/tool-bar';
@import "main/partials/viewer-header"; @import 'main/partials/user-settings';
@import "main/partials/viewer-thumbnails"; @import 'main/partials/workspace';
@import "main/partials/viewer"; @import 'main/partials/workspace-header';
@import "main/partials/messages"; @import 'main/partials/workspace-libraries';
@import "main/partials/texts";

View file

@ -5,115 +5,51 @@
// Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz> // Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
// Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com> // Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
.login { // TODO: rename to auth.scss
align-items: center;
background-color: $color-gray-40; .auth {
background-image: url("/images/login-bg.jpg"); display: grid;
background-position: center; grid-template-rows: auto;
background-repeat: no-repeat; grid-template-columns: 388px auto;
background-size: cover; }
display: flex;
.auth-sidebar {
grid-column: 1 / span 1;
height: 100vh; height: 100vh;
justify-content: center;
position: relative;
width: 100%;
.login-body { display: flex;
align-items: center; padding-top: 100px;
display: flex; flex-direction: column;
flex-direction: column; align-items: center;
justify-content: flex-start;
svg { background-color:#2C233E;
fill: $color-black;
height: 70px;
margin-bottom: $x-big;
width: 200px;
@include animation(.1s,1.5s,fadeInDown);
}
.login-content {
display: flex;
flex-direction: column;
width: 320px;
@include animation(1s,1s,fadeIn);
.input-text {
background-color: transparent;
border-color: $color-black;
color: $color-black;
font-size: $fs16;
margin-bottom: $big*2;
@include placeholder {
color: $color-gray-30;
}
&:hover {
@include placeholder {
color: $color-black;
}
}
&:focus {
background-color: $color-white;
border-color: $color-gray-10;
}
&.success {
background-color: $color-success-light;
color: $color-success-dark;
@include placeholder {
color: $color-white;
}
}
&.error {
background-color: rgba(234,35,35,.3);
color: red;
@include placeholder {
color: $color-white;
}
}
}
.input-checkbox {
margin: $big 0;
label {
color: $color-gray-20;
}
}
.login-links {
display: flex;
font-size: $fs13;
justify-content: space-between;
margin-top: $medium;
a {
color: $color-black;
text-align: center;
&:hover {
color: $color-primary-dark;
}
}
}
.btn-secondary {
margin-top: 5rem;
}
}
.tagline {
text-align: center;
width: 280px;
font-size: $fs24;
margin-top: 25px;
color: white;
} }
.logo {
svg {
fill: white;
width: 280px;
height: 80px;
}
}
}
.auth-content {
grid-column: 2 / span 1;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: $color-white;
.form-container {
width: 368px;
}
} }

View file

@ -31,3 +31,15 @@
.dashboard-content { .dashboard-content {
background-color: lighten($color-gray-10, 5%); background-color: lighten($color-gray-10, 5%);
} }
.verify-token {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
svg#loader-pencil {
fill: $color-gray-50;
}
}

View file

@ -15,3 +15,265 @@ textarea {
color: $color-danger; color: $color-danger;
} }
} }
.featured-note {
display: flex;
align-items: center;
justify-content: center;
font-size: $fs11;
padding: 10px;
margin-bottom: 25px;
background-color: rgba(#59B9E2, 0.05);
color: $color-gray-60;
.icon {
display: flex;
padding: 10px;
svg {
width: 16px;
height: 16px;
}
}
&.warning {
color: $color-danger;
}
}
.generic-form {
display: flex;
justify-content: center;
.forms-container {
display: flex;
margin-top: 40px;
width: 536px;
justify-content: center;
}
form {
display: flex;
flex-direction: column;
// flex-basis: 368px;
}
h1 {
font-size: $fs36;
color: #2C233E;
margin-bottom: 20px;
}
.subtitle {
font-size: $fs24;
color: #2C233E;
margin-bottom: 20px;
}
h2 {
font-size: $fs14;
color: $color-gray-60;
// height: 40px;
display: flex;
align-items: center;
}
a {
text-decoration: underline;
}
.links {
font-size: $fs11;
}
.link-entry {
font-size: $fs12;
color: $color-gray-40;
margin-bottom: 10px;
}
.link-entry a {
font-size: $fs12;
color: $color-primary-dark;
}
}
.custom-input {
display: flex;
flex-direction: column;
margin-bottom: 20px;
label {
font-size: $fs10;
color: $color-gray-30;
}
input {
color: $color-gray-60;
font-size: $fs12;
width: 100%;
border: 0px;
padding: 0px;
margin: 0px;
background-color: transparent;
}
.input-container {
display: flex;
flex-direction: row;
background-color: $color-white;
border-radius: 2px;
border: 1px solid $color-gray-20;
height: 40px;
padding-left: 15px;
padding-right: 15px;
&.invalid {
border-color: $color-danger;
label {
color: $color-danger;
}
}
&.valid {
border-color: $color-success;
}
&.focus {
border-color: $color-gray-60;
}
&.disabled {
background-color: lighten($color-gray-10, 5%);
user-select: none;
}
}
.hint {
padding: 4px;
font-size: $fs10;
}
.error {
color: $color-danger;
padding: 4px;
font-size: $fs10;
}
.main-content {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding-top: 6px;
padding-bottom: 6px;
}
.help-icon {
display: flex;
justify-content: center;
align-items: center;
padding-left: 10px;
svg {
fill: $color-gray-30;
width: 15px;
height: 15px;
}
}
}
.custom-select {
display: flex;
flex-direction: column;
margin-bottom: $big;
position: relative;
label {
font-size: $fs10;
color: $color-gray-30;
}
select {
cursor: pointer;
font-size: $fs12;
border: 0px;
opacity: 0;
z-index: 10;
padding: 0px;
margin: 0px;
background-color: transparent;
position: absolute;
width: calc(100% - 1px);
height: 100%;
padding: 15px;
}
.main-content {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding-top: 6px;
padding-bottom: 6px;
}
.input-container {
display: flex;
flex-direction: row;
background-color: $color-white;
border-radius: 2px;
border: 1px solid $color-gray-20;
height: 40px;
padding-left: 15px;
padding-right: 15px;
&.invalid {
border-color: $color-danger;
label {
color: $color-danger;
}
}
&.valid {
border-color: $color-success;
}
&.focus {
border-color: $color-gray-60;
}
&.disabled {
background-color: $color-gray-10;
user-select: none;
}
}
.value {
color: $color-gray-60;
font-size: $fs12;
width: 100%;
border: 0px;
padding: 0px;
margin: 0px;
}
.icon {
display: flex;
justify-content: center;
align-items: center;
padding-left: 10px;
svg {
fill: $color-gray-30;
transform: rotate(90deg);
width: 15px;
height: 15px;
}
}
}

View file

@ -0,0 +1,55 @@
.generic-modal {
background-color: $color-white;
width: 565px;
display: flex;
position: relative;
.close {
cursor: pointer;
position: absolute;
right: 16px;
top: 16px;
svg {
width: 16px;
height: 16px;
transform: rotate(45deg);
}
}
.modal-content {
display: flex;
flex-grow: 1;
flex-direction: column;
padding: 100px;
}
.button-row {
display: flex;
justify-content: space-between;
> button {
font-size: $fs13;
}
> button:not(:first-child) {
margin-left: 25px;
}
}
}
.change-email-modal {
h2 {
font-size: $fs14;
margin-bottom: 20px;
}
.confirmation {
.btn-primary {
margin-bottom: 30px;
}
.featured-note .icon svg {
fill: $color-success;
}
}
}

View file

@ -1,70 +1,164 @@
.settings-content { .settings-content {
.main-logo { header {
position: fixed;
left: 0;
top:0;
width: 40px;
height: 40px;
z-index: 12;
cursor: pointer;
}
nav {
display: flex; display: flex;
left: 0; flex-direction: column;
width: 100%; height: 160px;
justify-content: center; background-color: $color-white;
.nav-item { .secondary-menu {
margin: 0 $size-6; display: flex;
color: $color-gray-30; justify-content: space-between;
text-transform: uppercase; height: 40px;
border-bottom: 1px solid transparent; font-size: $fs14;
color: $color-gray-60;
&:hover { .icon {
color: $color-black; display: flex;
align-items: center;
} }
&.current { .left {
color: $color-black; margin-left: 30px;
border-bottom: 1px solid $color-primary; display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
.label {
margin-left: 15px;
}
svg {
fill: $color-gray-60;
width: 14px;
height: 14px;
transform: rotate(180deg);
}
}
.right {
align-items: center;
cursor: pointer;
display: flex;
justify-content: center;
margin-right: 30px;
.label {
color: $color-primary-dark;
margin-right: 15px;
}
svg {
fill: $color-primary-dark;
width: 14px;
height: 14px;
}
&:hover {
.label {
color: $color-danger;
}
svg {
fill: $color-danger;
}
}
}
}
h1 {
align-items: top;
color: $color-gray-60;
display: flex;
flex-grow: 1;
font-size: $fs24;
font-weight: normal;
justify-content: center;
}
nav {
display: flex;
justify-content: center;
height: 40px;
.nav-item {
align-items: center;
color: $color-gray-40;
display: flex;
flex-basis: 140px;
justify-content: center;
&.current {
border-bottom: 3px solid $color-primary;
}
} }
} }
} }
}
.settings-profile,
.settings-password {
display: flex;
flex-direction: column;
margin: 0 auto;
width: 500px;
.settings-label { .settings-profile {
color: $color-black; .forms-container {
font-size: $fs15; margin-top: 80px;
margin: $x-big 0 $x-small 0; }
padding: $medium 0;
}
.input-text {
color: $color-gray-60;
}
.btn-primary {
margin-top: $medium;
}
.profile-form,
.password-form {
display: flex;
flex-direction: column;
} }
.avatar-form { .avatar-form {
align-items: center; flex-basis: 168px;
height: 100vh;
display: flex; display: flex;
position: relative;
.image-change-field {
position: relative;
width: 120px;
height: 120px;
.update-overlay {
opacity: 0;
cursor: pointer;
position: absolute;
width: 121px;
height: 121px;
border-radius: 50%;
font-size: $fs24;
color: $color-white;
line-height: 120px;
text-align: center;
background: $color-primary-dark;
z-index: 14;
}
input[type=file] {
width: 120px;
height: 120px;
position: absolute;
opacity: 0;
cursor: pointer;
top: 0;
z-index: 15;
}
&:hover {
img {display: none;}
.update-overlay {opacity: 1};
}
}
}
.profile-form {
flex-grow: 1;
display: flex;
flex-direction: column;
.change-email {
display: flex;
flex-direction: row;
font-size: $fs12;
color: $color-primary-dark;
justify-content: flex-end;
margin-bottom: 20px;
}
}
.avatar-form {
img { img {
border-radius: 50%; border-radius: 50%;
flex-shrink: 0; flex-shrink: 0;
@ -72,7 +166,18 @@
margin-right: $medium; margin-right: $medium;
width: 120px; width: 120px;
} }
}
.options-form,
.password-form {
display: flex;
flex-direction: column;
flex-basis: 368px;
h2 {
font-size: $fs14;
font-weight: normal;
margin-bottom: $medium;
}
} }
} }

View file

@ -41,7 +41,7 @@
(and (or (= path "") (and (or (= path "")
(nil? match)) (nil? match))
(not authed?)) (not authed?))
(st/emit! (rt/nav :login)) (st/emit! (rt/nav :auth-login))
(and (nil? match) authed?) (and (nil? match) authed?)
(st/emit! (rt/nav :dashboard-team {:team-id (:default-team-id profile)})) (st/emit! (rt/nav :dashboard-team {:team-id (:default-team-id profile)}))

View file

@ -51,14 +51,18 @@
ptk/WatchEvent ptk/WatchEvent
(watch [this state s] (watch [this state s]
(let [params {:email email (let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)
params {:email email
:password password :password password
:scope "webapp"} :scope "webapp"}]
on-error #(rx/of (dm/error (tr "errors.auth.unauthorized")))]
(->> (rp/mutation :login params) (->> (rp/mutation :login params)
(rx/map logged-in) (rx/tap on-success)
(rx/catch rp/client-error? on-error)))))) (rx/catch (fn [err]
(on-error err)
(rx/empty)))
(rx/map logged-in))))))
;; --- Logout ;; --- Logout
(def clear-user-data (def clear-user-data
@ -81,8 +85,8 @@
(ptk/reify ::logout (ptk/reify ::logout
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(rx/of (rt/nav :login) (rx/of clear-user-data
clear-user-data)))) (rt/nav :auth-login)))))
;; --- Register ;; --- Register
@ -93,18 +97,37 @@
(defn register (defn register
"Create a register event instance." "Create a register event instance."
[data on-error] [data]
(s/assert ::register data) (s/assert ::register data)
(s/assert fn? on-error)
(ptk/reify ::register (ptk/reify ::register
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(letfn [(handle-error [{payload :payload}] (let [{:keys [on-error on-success]
(on-error payload) :or {on-error identity
(rx/empty))] on-success identity}} (meta data)]
(->> (rp/mutation :register-profile data) (->> (rp/mutation :register-profile data)
(rx/map (fn [_] (login data))) (rx/tap on-success)
(rx/catch rp/client-error? handle-error)))))) (rx/map #(login data))
(rx/catch (fn [err]
(on-error err)
(rx/empty))))))))
;; --- Request Account Deletion
(def request-account-deletion
(letfn [(on-error [{:keys [code] :as error}]
(if (= :uxbox.services.mutations.profile/owner-teams-with-people code)
(let [msg (tr "settings.notifications.profile-deletion-not-allowed")]
(rx/of (dm/error msg)))
(rx/empty)))]
(ptk/reify ::request-account-deletion
ptk/WatchEvent
(watch [_ state stream]
(rx/concat
(->> (rp/mutation :delete-profile {})
(rx/map #(rt/nav :auth-goodbye))
(rx/catch on-error)))))))
;; --- Recovery Request ;; --- Recovery Request
@ -112,38 +135,43 @@
(s/keys :req-un [::email])) (s/keys :req-un [::email]))
(defn request-profile-recovery (defn request-profile-recovery
[data on-success] [data]
(us/verify ::recovery-request data) (us/verify ::recovery-request data)
(us/verify fn? on-success)
(ptk/reify ::request-profile-recovery (ptk/reify ::request-profile-recovery
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(letfn [(on-error [{payload :payload}] (let [{:keys [on-error on-success]
(rx/empty))] :or {on-error identity
on-success identity}} (meta data)]
(->> (rp/mutation :request-profile-recovery data) (->> (rp/mutation :request-profile-recovery data)
(rx/tap on-success) (rx/tap on-success)
(rx/catch rp/client-error? on-error)))))) (rx/catch (fn [err]
(on-error err)
(rx/empty))))))))
;; --- Recovery (Password) ;; --- Recovery (Password)
(s/def ::token string?) (s/def ::token string?)
(s/def ::on-error fn?)
(s/def ::on-success fn?)
(s/def ::recover-profile (s/def ::recover-profile
(s/keys :req-un [::password ::token ::on-error ::on-success])) (s/keys :req-un [::password ::token]))
(defn recover-profile (defn recover-profile
[{:keys [token password on-error on-success] :as data}] [{:keys [token password] :as data}]
(us/verify ::recover-profile data) (us/verify ::recover-profile data)
(ptk/reify ::recover-profile (ptk/reify ::recover-profile
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(->> (rp/mutation :recover-profile {:token token :password password}) (let [{:keys [on-error on-success]
(rx/tap on-success) :or {on-error identity
(rx/catch (fn [err] on-success identity}} (meta data)]
(on-error) (->> (rp/mutation :recover-profile data)
(rx/empty))))))) (rx/tap on-success)
(rx/catch (fn [err]
(on-error)
(rx/empty))))))))
;; --- Create Demo Profile ;; --- Create Demo Profile

View file

@ -143,7 +143,7 @@
(->> (rp/query :projects-by-team {:team-id team-id}) (->> (rp/query :projects-by-team {:team-id team-id})
(rx/map projects-fetched) (rx/map projects-fetched)
(rx/catch (fn [error] (rx/catch (fn [error]
(rx/of (rt/nav' :not-authorized)))))))) (rx/of (rt/nav' :auth-login))))))))
(defn projects-fetched (defn projects-fetched
[projects] [projects]
@ -212,7 +212,7 @@
(->> (rp/query :recent-files params) (->> (rp/query :recent-files params)
(rx/map recent-files-fetched) (rx/map recent-files-fetched)
(rx/catch (fn [e] (rx/catch (fn [e]
(rx/of (rt/nav' :not-authorized))))))))) (rx/of (rt/nav' :auth-login)))))))))
(defn recent-files-fetched (defn recent-files-fetched
[recent-files] [recent-files]

View file

@ -61,3 +61,9 @@
(show {:content message (show {:content message
:type :info :type :info
:timeout timeout})) :timeout timeout}))
(defn success
[message & {:keys [timeout] :or {timeout 3000}}]
(show {:content message
:type :info
:timeout timeout}))

View file

@ -13,6 +13,7 @@
[uxbox.common.spec :as us] [uxbox.common.spec :as us]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.main.repo :as rp] [uxbox.main.repo :as rp]
[uxbox.util.router :as rt]
[uxbox.util.i18n :as i18n :refer [tr]] [uxbox.util.i18n :as i18n :refer [tr]]
[uxbox.util.storage :refer [storage]] [uxbox.util.storage :refer [storage]]
[uxbox.util.avatars :as avatars] [uxbox.util.avatars :as avatars]
@ -74,7 +75,11 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state s] (watch [_ state s]
(->> (rp/query! :profile) (->> (rp/query! :profile)
(rx/map profile-fetched))))) (rx/map profile-fetched)
(rx/catch (fn [error]
(if (= (:type error) :not-found)
(rx/of (rt/nav :auth-login))
(rx/empty))))))))
;; --- Update Profile ;; --- Update Profile
@ -91,9 +96,35 @@
(rx/empty))] (rx/empty))]
(->> (rp/mutation :update-profile data) (->> (rp/mutation :update-profile data)
(rx/do on-success) (rx/do on-success)
(rx/map profile-fetched) (rx/map (constantly fetch-profile))
(rx/catch rp/client-error? handle-error)))))) (rx/catch rp/client-error? handle-error))))))
;; --- Request Email Change
(defn request-email-change
[{:keys [email] :as data}]
(ptk/reify ::request-email-change
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)]
(->> (rp/mutation :request-email-change data)
(rx/tap on-success)
(rx/map (constantly fetch-profile))
(rx/catch (fn [err]
(on-error err)
(rx/empty))))))))
;; --- Cancel Email Change
(def cancel-email-change
(ptk/reify ::cancel-email-change
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/mutation :cancel-email-change {})
(rx/map (constantly fetch-profile))))))
;; --- Update Password (Form) ;; --- Update Password (Form)
(s/def ::update-password (s/def ::update-password
@ -107,15 +138,16 @@
(ptk/reify ::update-password (ptk/reify ::update-password
ptk/WatchEvent ptk/WatchEvent
(watch [_ state s] (watch [_ state s]
(let [mdata (meta data) (let [{:keys [on-error on-success]
on-success (:on-success mdata identity) :or {on-error identity
on-error (:on-error mdata identity) on-success identity}} (meta data)
params {:old-password (:password-old data) params {:old-password (:password-old data)
:password (:password-1 data)}] :password (:password-1 data)}]
(->> (rp/mutation :update-profile-password params) (->> (rp/mutation :update-profile-password params)
(rx/catch rp/client-error? #(do (on-error (:payload %)) (rx/tap on-success)
(rx/empty))) (rx/catch (fn [err]
(rx/do on-success) (on-error err)
(rx/empty)))
(rx/ignore)))))) (rx/ignore))))))

View file

@ -95,7 +95,7 @@
(defmethod mutation :logout (defmethod mutation :logout
[id params] [id params]
(let [url (str url "/api/logout")] (let [url (str url "/api/logout")]
(->> (http/send! {:method :post :url url :body params :auth false}) (->> (http/send! {:method :post :url url :body params})
(rx/mapcat handle-response)))) (rx/mapcat handle-response))))
(def client-error? http/client-error?) (def client-error? http/client-error?)

View file

@ -21,11 +21,8 @@
[uxbox.main.refs :as refs] [uxbox.main.refs :as refs]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.main.ui.dashboard :refer [dashboard]] [uxbox.main.ui.dashboard :refer [dashboard]]
[uxbox.main.ui.login :refer [login-page]]
[uxbox.main.ui.static :refer [not-found-page not-authorized-page]] [uxbox.main.ui.static :refer [not-found-page not-authorized-page]]
[uxbox.main.ui.profile.recovery :refer [profile-recovery-page]] [uxbox.main.ui.auth :refer [auth verify-token]]
[uxbox.main.ui.profile.recovery-request :refer [profile-recovery-request-page]]
[uxbox.main.ui.profile.register :refer [profile-register-page]]
[uxbox.main.ui.settings :as settings] [uxbox.main.ui.settings :as settings]
[uxbox.main.ui.viewer :refer [viewer-page]] [uxbox.main.ui.viewer :refer [viewer-page]]
[uxbox.main.ui.workspace :as workspace] [uxbox.main.ui.workspace :as workspace]
@ -35,14 +32,18 @@
;; --- Routes ;; --- Routes
(def routes (def routes
[["/login" :login] [["/auth"
["/register" :profile-register] ["/login" :auth-login]
["/recovery/request" :profile-recovery-request] ["/register" :auth-register]
["/recovery" :profile-recovery] ["/recovery/request" :auth-recovery-request]
["/recovery" :auth-recovery]
["/verify-token" :auth-verify-token]
["/goodbye" :auth-goodbye]]
["/settings" ["/settings"
["/profile" :settings-profile] ["/profile" :settings-profile]
["/password" :settings-password]] ["/password" :settings-password]
["/options" :settings-options]]
["/view/:page-id" :viewer] ["/view/:page-id" :viewer]
["/not-found" :not-found] ["/not-found" :not-found]
@ -84,20 +85,20 @@
{::mf/wrap [#(mf/catch % {:fallback app-error})]} {::mf/wrap [#(mf/catch % {:fallback app-error})]}
[{:keys [route] :as props}] [{:keys [route] :as props}]
(case (get-in route [:data :name]) (case (get-in route [:data :name])
:login
[:& login-page]
:profile-register (:auth-login
[:& profile-register-page] :auth-register
:auth-goodbye
:auth-recovery-request
:auth-recovery)
[:& auth {:route route}]
:profile-recovery-request :auth-verify-token
[:& profile-recovery-request-page] [:& verify-token {:route route}]
:profile-recovery
[:& profile-recovery-page]
(:settings-profile (:settings-profile
:settings-password) :settings-password
:settings-options)
[:& settings/settings {:route route}] [:& settings/settings {:route route}]
:debug-icons-preview :debug-icons-preview

View file

@ -0,0 +1,93 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth
(:require
[cljs.spec.alpha :as s]
[beicon.core :as rx]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.users :as du]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.auth.login :refer [login-page]]
[uxbox.main.ui.auth.recovery :refer [recovery-page]]
[uxbox.main.ui.auth.recovery-request :refer [recovery-request-page]]
[uxbox.main.ui.auth.register :refer [register-page]]
[uxbox.main.repo :as rp]
[uxbox.util.timers :as ts]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [tr t]]
[uxbox.util.router :as rt]))
(mf/defc goodbye-page
[{:keys [locale] :as props}]
[:div.goodbay
[:h1 (t locale "auth.goodbye-title")]])
(mf/defc auth
[{:keys [route] :as props}]
(let [section (get-in route [:data :name])
locale (mf/deref i18n/locale)]
[:*
[:& messages]
[:div.auth
[:section.auth-sidebar
[:a.logo i/logo]
[:span.tagline (t locale "auth.sidebar-tagline")]]
[:section.auth-content
(case section
:auth-register [:& register-page {:locale locale}]
:auth-login [:& login-page {:locale locale}]
:auth-goodbye [:& goodbye-page {:locale locale}]
:auth-recovery-request [:& recovery-request-page {:locale locale}]
:auth-recovery [:& recovery-page {:locale locale
:params (:query-params route)}])]]]))
(defn- handle-email-verified
[data]
(let [msg (tr "settings.notifications.email-verified-successfully")]
(ts/schedule 100 #(st/emit! (dm/success msg)))
(st/emit! (rt/nav :settings-profile)
du/fetch-profile)))
(defn- handle-email-changed
[data]
(let [msg (tr "settings.notifications.email-changed-successfully")]
(ts/schedule 100 #(st/emit! (dm/success msg)))
(st/emit! (rt/nav :settings-profile)
du/fetch-profile)))
(mf/defc verify-token
[{:keys [route] :as props}]
(let [token (get-in route [:query-params :token])]
(mf/use-effect
(fn []
(->> (rp/mutation :verify-profile-token {:token token})
(rx/subs
(fn [response]
(case (:type response)
:verify-email (handle-email-verified response)
:change-email (handle-email-changed response)
nil))
(fn [error]
(case (:code error)
:uxbox.services.mutations.profile/email-already-exists
(let [msg (tr "errors.email-already-exists")]
(ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :settings-profile)))
(let [msg (tr "errors.generic")]
(ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :settings-profile)))))))))
[:div.verify-token
i/loader-pencil]))

View file

@ -0,0 +1,86 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth.login
(:require
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]
[uxbox.common.spec :as us]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as da]
[uxbox.main.store :as st]
[uxbox.main.data.messages :as dm]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr t]]
[uxbox.util.router :as rt]))
(s/def ::email ::us/email)
(s/def ::password ::us/not-empty-string)
(s/def ::login-form
(s/keys :req-un [::email ::password]))
(defn- on-error
[form error]
(st/emit! (dm/error (tr "errors.auth.unauthorized"))))
(defn- on-submit
[form event]
(let [params (with-meta (:clean-data form)
{:on-error (partial on-error form)})]
(st/emit! (da/login params))))
(mf/defc login-form
[{:keys [locale] :as props}]
[:& form {:on-submit on-submit
:spec ::login-form
:initial {}}
[:& input
{:name :email
:type "text"
:tab-index "2"
:help-icon i/at
:label (t locale "auth.email-label")}]
[:& input
{:type "password"
:name :password
:tab-index "3"
:help-icon i/eye
:label (t locale "auth.password-label")}]
[:& submit-button
{:label (t locale "auth.login-submit-label")}]])
(mf/defc login-page
[{:keys [locale] :as props}]
[:div.generic-form.login-form
[:div.form-container
[:h1 (t locale "auth.login-title")]
[:div.subtitle (t locale "auth.login-subtitle")]
[:& login-form {:locale locale}]
[:div.links
[:div.link-entry
[:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))
:tab-index "5"}
(t locale "auth.forgot-password")]]
[:div.link-entry
[:span (t locale "auth.register-label") " "]
[:a {:on-click #(st/emit! (rt/nav :auth-register))
:tab-index "6"}
(t locale "auth.register")]]
[:div.link-entry
[:span (t locale "auth.create-demo-profile-label") " "]
[:a {:on-click #(st/emit! da/create-demo-profile)
:tab-index "6"}
(t locale "auth.create-demo-profile")]]]]])

View file

@ -0,0 +1,98 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth.recovery
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t tr]]
[uxbox.util.router :as rt]))
(s/def ::password-1 ::fm/not-empty-string)
(s/def ::password-2 ::fm/not-empty-string)
(s/def ::token ::fm/not-empty-string)
(s/def ::recovery-form
(s/keys :req-un [::password-1
::password-2]))
(defn- password-equality
[data]
(let [password-1 (:password-1 data)
password-2 (:password-2 data)]
(cond-> {}
(and password-1 password-2
(not= password-1 password-2))
(assoc :password-2 {:message "errors.password-invalid-confirmation"})
(and password-1 (> 8 (count password-1)))
(assoc :password-1 {:message "errors.password-too-short"}))))
(defn- on-error
[form error]
(st/emit! (dm/error (tr "auth.notifications.invalid-token-error"))))
(defn- on-success
[_]
(st/emit! (dm/info (tr "auth.notifications.password-changed-succesfully"))
(rt/nav :auth-login)))
(defn- on-submit
[form event]
(let [params (with-meta {:token (get-in form [:clean-data :token])
:password (get-in form [:clean-data :password-2])}
{:on-error (partial on-error form)
:on-success (partial on-success form)})]
(st/emit! (uda/recover-profile params))))
(mf/defc recovery-form
[{:keys [locale params] :as props}]
[:& form {:on-submit on-submit
:spec ::recovery-form
:validators [password-equality]
:initial params}
[:& input {:type "password"
:name :password-1
:label (t locale "auth.new-password-label")}]
[:& input {:type "password"
:name :password-2
:label (t locale "auth.confirm-password-label")}]
[:& submit-button
{:label (t locale "auth.recovery-submit-label")}]])
;; --- Recovery Request Page
(mf/defc recovery-page
[{:keys [locale params] :as props}]
[:section.generic-form
[:div.form-container
[:h1 "Forgot your password?"]
[:div.subtitle "Please enter your new password"]
[:& recovery-form {:locale locale :params params}]
[:div.links
[:div.link-entry
[:a {:on-click #(st/emit! (rt/nav :auth-login))}
(t locale "profile.recovery.go-to-login")]]]]])

View file

@ -0,0 +1,68 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth.recovery-request
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [tr t]]
[uxbox.util.router :as rt]))
(s/def ::email ::us/email)
(s/def ::recovery-request-form (s/keys :req-un [::email]))
(defn- on-submit
[form event]
(let [on-success #(st/emit! (dm/info (tr "auth.notifications.recovery-token-sent"))
(rt/nav :auth-login))
params (with-meta (:clean-data form)
{:on-success on-success})]
(st/emit! (uda/request-profile-recovery params))))
(mf/defc recovery-form
[{:keys [locale] :as props}]
[:& form {:on-submit on-submit
:spec ::recovery-request-form
:initial {}}
[:& input {:name :email
:label (t locale "auth.email-label")
:help-icon i/at
:type "text"}]
[:& submit-button
{:label (t locale "auth.recovery-request-submit-label")}]])
;; --- Recovery Request Page
(mf/defc recovery-request-page
[{:keys [locale] :as props}]
[:section.generic-form
[:div.form-container
[:h1 (t locale "auth.recovery-request-title")]
[:div.subtitle (t locale "auth.recovery-request-subtitle")]
[:& recovery-form {:locale locale}]
[:div.links
[:div.link-entry
[:a {:on-click #(st/emit! (rt/nav :auth-login))}
(t locale "auth.go-back-to-login")]]]]])

View file

@ -0,0 +1,120 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth.register
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.config :as cfg]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as uda]
[uxbox.main.store :as st]
[uxbox.main.data.auth :as da]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr t]]
[uxbox.util.router :as rt]))
(mf/defc demo-warning
[_]
[:div.featured-note.warning
[:span
[:strong "WARNING: "]
"This is a " [:strong "demo"] " service, "
[:strong "DO NOT USE"] " for real work, "
" the projects will be periodicaly wiped."]])
(s/def ::fullname ::fm/not-empty-string)
(s/def ::password ::fm/not-empty-string)
(s/def ::email ::fm/email)
(s/def ::register-form
(s/keys :req-un [::password
::fullname
::email]))
(defn- on-error
[form error]
(case (:code error)
:uxbox.services.mutations.profile/registration-disabled
(st/emit! (tr "errors.registration-disabled"))
:uxbox.services.mutations.profile/email-already-exists
(swap! form assoc-in [:errors :email]
{:message "errors.email-already-exists"})
(st/emit! (tr "errors.unexpected-error"))))
(defn- validate
[data]
(let [password (:password data)]
(when (> 8 (count password))
{:password {:message "errors.password-too-short"}})))
(defn- on-submit
[form event]
(let [data (with-meta (:clean-data form)
{:on-error (partial on-error form)})]
(st/emit! (uda/register data))))
(mf/defc register-form
[{:keys [locale] :as props}]
[:& form {:on-submit on-submit
:spec ::register-form
:validators [validate]
:initial {}}
[:& input {:name :fullname
:tab-index "1"
:label (t locale "auth.fullname-label")
:type "text"}]
[:& input {:type "email"
:name :email
:tab-index "2"
:help-icon i/at
:label (t locale "auth.email-label")}]
[:& input {:name :password
:tab-index "3"
:hint (t locale "auth.password-length-hint")
:label (t locale "auth.password-label")
:type "password"}]
[:& submit-button
{:label (t locale "auth.register-submit-label")}]])
;; --- Register Page
(mf/defc register-page
[{:keys [locale] :as props}]
[:section.generic-form
[:div.form-container
[:h1 (t locale "auth.register-title")]
[:div.subtitle (t locale "auth.register-subtitle")]
(when cfg/demo-warning
[:& demo-warning])
[:& register-form {:locale locale}]
[:div.links
[:div.link-entry
[:span (t locale "auth.already-have-account") " "]
[:a {:on-click #(st/emit! (rt/nav :auth-login))
:tab-index "4"}
(t locale "auth.login-here")]]
[:div.link-entry
[:span (t locale "auth.create-demo-profile-label") " "]
[:a {:on-click #(st/emit! da/create-demo-profile)
:tab-index "5"}
(t locale "auth.create-demo-profile")]]]]])

View file

@ -0,0 +1,150 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.components.forms
(:require
[rumext.alpha :as mf]
[cuerdas.core :as str]
[uxbox.common.data :as d]
[uxbox.main.ui.icons :as i]
[uxbox.util.object :as obj]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t]]
["react" :as react]
[uxbox.util.dom :as dom]))
(def form-ctx (mf/create-context nil))
(mf/defc input
[{:keys [type label help-icon disabled name form hint] :as props}]
(let [form (mf/use-ctx form-ctx)
type' (mf/use-state type)
focus? (mf/use-state false)
locale (mf/deref i18n/locale)
touched? (get-in form [:touched name])
error (get-in form [:errors name])
klass (dom/classnames
:focus @focus?
:valid (and touched? (not error))
:invalid (and touched? error)
:disabled disabled)
swap-text-password
(fn []
(swap! type' (fn [type]
(if (= "password" type)
"text"
"password"))))
on-focus #(reset! focus? true)
on-change (fm/on-input-change form name)
on-blur
(fn [event]
(reset! focus? false)
(when-not (get-in form [:touched name])
(swap! form assoc-in [:touched name] true)))
value (get-in form [:data name] "")
props (-> props
(dissoc :help-icon :form)
(assoc :value value
:on-focus on-focus
:on-blur on-blur
:placeholder label
:on-change on-change
:type @type')
(obj/clj->props))]
[:div.custom-input
[:div.input-container {:class klass}
[:div.main-content
(when-not (str/empty? value)
[:label label])
[:> :input props]]
[:div.help-icon
{:style {:cursor "pointer"}
:on-click (when (= "password" type)
swap-text-password)}
(cond
(and (= type "password")
(= @type' "password"))
i/eye
(and (= type "password")
(= @type' "text"))
i/eye-closed
:else
help-icon)]]
(cond
(and touched? (:message error))
[:span.error (t locale (:message error))]
(string? hint)
[:span.hint hint])]))
(mf/defc select
[{:keys [options label name form default]
:or {default ""}}]
(let [form (mf/use-ctx form-ctx)
value (get-in form [:data name] default)
cvalue (d/seek #(= value (:value %)) options)
on-change (fm/on-input-change form name)]
[:div.custom-select
[:select {:value value
:on-change on-change}
(for [item options]
[:option {:key (:value item) :value (:value item)} (:label item)])]
[:div.input-container
[:div.main-content
[:label label]
[:span.value (:label cvalue "")]]
[:div.icon
i/arrow-slide]]]))
(mf/defc submit-button
[{:keys [label form] :as props}]
(let [form (mf/use-ctx form-ctx)]
[:input.btn-primary.btn-large
{:name "submit"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value label
:type "submit"}]))
(mf/defc form
[{:keys [on-submit spec validators initial children class] :as props}]
(let [frm (fm/use-form :spec spec
:validators validators
:initial initial)]
(mf/use-effect
(mf/deps initial)
(fn []
(if (fn? initial)
(swap! frm update :data merge (initial))
(swap! frm update :data merge initial))))
[:& (mf/provider form-ctx) {:value frm}
[:form {:class class
:on-submit (fn [event]
(dom/prevent-default event)
(on-submit frm event))}
children]]))

View file

@ -22,6 +22,7 @@
(def arrow-end (icon-xref :arrow-end)) (def arrow-end (icon-xref :arrow-end))
(def arrow-slide (icon-xref :arrow-slide)) (def arrow-slide (icon-xref :arrow-slide))
(def artboard (icon-xref :artboard)) (def artboard (icon-xref :artboard))
(def at (icon-xref :at))
(def auto-fix (icon-xref :auto-fix)) (def auto-fix (icon-xref :auto-fix))
(def auto-height (icon-xref :auto-height)) (def auto-height (icon-xref :auto-height))
(def auto-width (icon-xref :auto-width)) (def auto-width (icon-xref :auto-width))
@ -60,6 +61,7 @@
(def lock (icon-xref :lock)) (def lock (icon-xref :lock))
(def lock-open (icon-xref :lock-open)) (def lock-open (icon-xref :lock-open))
(def logo (icon-xref :uxbox-logo)) (def logo (icon-xref :uxbox-logo))
(def logout (icon-xref :logout))
(def logo-icon (icon-xref :uxbox-logo-icon)) (def logo-icon (icon-xref :uxbox-logo-icon))
(def lowercase (icon-xref :lowercase)) (def lowercase (icon-xref :lowercase))
(def mail (icon-xref :mail)) (def mail (icon-xref :mail))

View file

@ -1,99 +0,0 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.login
(:require
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]
[uxbox.common.spec :as us]
[uxbox.main.ui.icons :as i]
[uxbox.config :as cfg]
[uxbox.main.data.auth :as da]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr]]
[uxbox.util.router :as rt]))
(s/def ::email ::us/email)
(s/def ::password ::us/not-empty-string)
(s/def ::login-form
(s/keys :req-un [::email ::password]))
(defn- on-submit
[event form]
(dom/prevent-default event)
(let [{:keys [email password]} (:clean-data form)]
(st/emit! (da/login {:email email :password password}))))
(mf/defc demo-warning
[_]
[:div.message-inline
[:p
[:strong "WARNING: "]
"This is a " [:strong "demo"] " service, "
[:strong "DO NOT USE"] " for real work, "
" the projects will be periodicaly wiped."]])
(mf/defc login-form
[]
(let [{:keys [data] :as form} (fm/use-form ::login-form {})]
[:form {:on-submit #(on-submit % form)}
[:div.login-content
(when cfg/demo-warning
[:& demo-warning])
[:input.input-text
{:name "email"
:tab-index "2"
:value (:email data "")
:class (fm/error-class form :email)
:on-blur (fm/on-input-blur form :email)
:on-change (fm/on-input-change form :email)
:placeholder (tr "login.email")
:type "text"}]
[:input.input-text
{:name "password"
:tab-index "3"
:value (:password data "")
:class (fm/error-class form :password)
:on-blur (fm/on-input-blur form :password)
:on-change (fm/on-input-change form :password)
:placeholder (tr "login.password")
:type "password"}]
[:input.btn-primary.btn-large
{:name "login"
:tab-index "4"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (tr "login.submit")
:type "submit"}]
[:div.login-links
[:a {:on-click #(st/emit! (rt/nav :profile-recovery-request))
:tab-index "5"}
(tr "login.forgot-password")]
[:a {:on-click #(st/emit! (rt/nav :profile-register))
:tab-index "6"}
(tr "login.register")]]
[:a.btn-secondary.btn-small {:on-click #(st/emit! da/create-demo-profile)
:tab-index "7"
:title (tr "login.create-demo-profile-description")}
(tr "login.create-demo-profile")]]]))
(mf/defc login-page
[]
[:div.login
[:div.login-body
[:& messages]
[:a i/logo]
[:& login-form]]])

View file

@ -27,13 +27,12 @@
(defn- on-parent-clicked (defn- on-parent-clicked
[event parent-ref] [event parent-ref]
(dom/stop-propagation event)
(dom/prevent-default event)
(let [parent (mf/ref-val parent-ref) (let [parent (mf/ref-val parent-ref)
current (dom/get-target event)] current (dom/get-target event)]
(when (dom/equals? parent current) (when (dom/equals? parent current)
(reset! state nil) (dom/stop-propagation event)
#_(st/emit! (udl/hide-lightbox))))) (dom/prevent-default event)
(reset! state nil))))
(mf/defc modal-wrapper (mf/defc modal-wrapper
[{:keys [component props]}] [{:keys [component props]}]
@ -46,7 +45,8 @@
parent-ref (mf/use-ref nil)] parent-ref (mf/use-ref nil)]
[:div.lightbox {:class classes [:div.lightbox {:class classes
:ref parent-ref :ref parent-ref
:on-click #(on-parent-clicked % parent-ref)} :on-click #(on-parent-clicked % parent-ref)
}
(mf/element component props)])) (mf/element component props)]))
(mf/defc modal (mf/defc modal

View file

@ -1,91 +0,0 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.profile.recovery
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t]]
[uxbox.util.router :as rt]))
(s/def ::token ::us/not-empty-string)
(s/def ::password ::us/not-empty-string)
(s/def ::recovery-form (s/keys :req-un [::token ::password]))
(mf/defc recovery-form
[]
(let [{:keys [data] :as form} (fm/use-form ::recovery-form {})
locale (i18n/use-locale)
on-success
(fn []
(st/emit! (dm/info (t locale "profile.recovery.password-changed"))
(rt/nav :login)))
on-error
(fn []
(st/emit! (dm/error (t locale "profile.recovery.invalid-token"))))
on-submit
(fn [event]
(dom/prevent-default event)
(st/emit! (uda/recover-profile (assoc (:clean-data form)
:on-error on-error
:on-success on-success))))]
[:form {:on-submit on-submit}
[:div.login-content
[:input.input-text
{:name "token"
:value (:token data "")
:class (fm/error-class form :token)
:on-blur (fm/on-input-blur form :token)
:on-change (fm/on-input-change form :token)
:placeholder (t locale "profile.recovery.token")
:auto-complete "off"
:type "text"}]
[:input.input-text
{:name "password"
:value (:password data "")
:class (fm/error-class form :password)
:on-blur (fm/on-input-blur form :password)
:on-change (fm/on-input-change form :password)
:placeholder (t locale "profile.recovery.password")
:type "password"}]
[:input.btn-primary
{:name "recover"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (t locale "profile.recovery.submit-recover")
:type "submit"}]
[:div.login-links
[:a {:on-click #(st/emit! (rt/nav :login))}
(t locale "profile.recovery.go-to-login")]]]]))
;; --- Recovery Request Page
(mf/defc profile-recovery-page
[]
[:div.login
[:div.login-body
[:& messages]
[:a i/logo]
[:& recovery-form]]])

View file

@ -1,75 +0,0 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.profile.recovery-request
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t]]
[uxbox.util.router :as rt]))
(s/def ::email ::us/email)
(s/def ::recovery-request-form (s/keys :req-un [::email]))
(mf/defc recovery-form
[]
(let [{:keys [data] :as form} (fm/use-form ::recovery-request-form {})
locale (i18n/use-locale)
on-success
(fn []
(st/emit! (dm/info (t locale "profile.recovery.recovery-token-sent"))
(rt/nav :profile-recovery)))
on-submit
(fn [event]
(dom/prevent-default event)
(st/emit! (uda/request-profile-recovery (:clean-data form) on-success)))]
[:form {:on-submit on-submit}
[:div.login-content
[:input.input-text
{:name "email"
:value (:email data "")
:class (fm/error-class form :email)
:on-blur (fm/on-input-blur form :email)
:on-change (fm/on-input-change form :email)
:placeholder (t locale "profile.recovery.email")
:type "text"}]
[:input.btn-primary
{:name "login"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (t locale "profile.recovery.submit-request")
:type "submit"}]
[:div.login-links
[:a {:on-click #(st/emit! (rt/nav :login))}
(t locale "profile.recovery.go-to-login")]]]]))
;; --- Recovery Request Page
(mf/defc profile-recovery-request-page
[]
[:div.login
[:div.login-body
[:& messages]
[:a i/logo]
[:& recovery-form]]])

View file

@ -1,120 +0,0 @@
;; 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) 2015-2017 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.profile.register
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as uda]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr]]
[uxbox.util.router :as rt]))
(s/def ::fullname ::fm/not-empty-string)
(s/def ::password ::fm/not-empty-string)
(s/def ::email ::fm/email)
(s/def ::register-form
(s/keys :req-un [::password
::fullname
::email]))
(defn- on-error
[error form]
(case (:code error)
:uxbox.services.users/registration-disabled
(st/emit! (tr "errors.api.form.registration-disabled"))
:uxbox.services.users/email-already-exists
(swap! form assoc-in [:errors :email]
{:type ::api
:message "errors.api.form.email-already-exists"})
(st/emit! (tr "errors.api.form.unexpected-error"))))
(defn- on-submit
[event form]
(dom/prevent-default event)
(let [data (:clean-data form)
on-error #(on-error % form)]
(st/emit! (uda/register data on-error))))
(mf/defc register-form
[props]
(let [{:keys [data] :as form} (fm/use-form ::register-form {})]
[:form {:on-submit #(on-submit % form)}
[:div.login-content
[:input.input-text
{:name "fullname"
:tab-index "1"
:value (:fullname data "")
:class (fm/error-class form :fullname)
:on-blur (fm/on-input-blur form :fullname)
:on-change (fm/on-input-change form :fullname)
:placeholder (tr "profile.register.fullname")
:type "text"}]
[:& fm/field-error {:form form
:type #{::api}
:field :fullname}]
[:input.input-text
{:type "email"
:name "email"
:tab-index "3"
:class (fm/error-class form :email)
:on-blur (fm/on-input-blur form :email)
:on-change (fm/on-input-change form :email)
:value (:email data "")
:placeholder (tr "profile.register.email")}]
[:& fm/field-error {:form form
:type #{::api}
:field :email}]
[:input.input-text
{:name "password"
:tab-index "4"
:value (:password data "")
:class (fm/error-class form :password)
:on-blur (fm/on-input-blur form :password)
:on-change (fm/on-input-change form :password)
:placeholder (tr "profile.register.password")
:type "password"}]
[:& fm/field-error {:form form
:type #{::api}
:field :email}]
[:input.btn-primary
{:type "submit"
:tab-index "5"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (tr "profile.register.get-started")}]
[:div.login-links
[:a {:on-click #(st/emit! (rt/nav :login))}
(tr "profile.register.already-have-account")]]]]))
;; --- Register Page
(mf/defc profile-register-page
[props]
[:div.login
[:div.login-body
[:& messages]
[:a i/logo]
[:& register-form]]])

View file

@ -2,8 +2,10 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; 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/. ;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;; ;;
;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz> ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com> ;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings (ns uxbox.main.ui.settings
(:require (:require
@ -18,6 +20,7 @@
[uxbox.main.ui.messages :refer [messages]] [uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.settings.header :refer [header]] [uxbox.main.ui.settings.header :refer [header]]
[uxbox.main.ui.settings.password :refer [password-page]] [uxbox.main.ui.settings.password :refer [password-page]]
[uxbox.main.ui.settings.options :refer [options-page]]
[uxbox.main.ui.settings.profile :refer [profile-page]])) [uxbox.main.ui.settings.profile :refer [profile-page]]))
(mf/defc settings (mf/defc settings
@ -30,7 +33,8 @@
[:& header {:section section :profile profile}] [:& header {:section section :profile profile}]
(case section (case section
:settings-profile (mf/element profile-page) :settings-profile (mf/element profile-page)
:settings-password (mf/element password-page))]])) :settings-password (mf/element password-page)
:settings-options (mf/element options-page))]]))

View file

@ -0,0 +1,102 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.change-email
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as da]
[uxbox.main.data.users :as du]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.refs :as refs]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.main.ui.modal :as modal]
[uxbox.util.i18n :as i18n :refer [tr t]]))
(s/def ::email-1 ::fm/email)
(s/def ::email-2 ::fm/email)
(s/def ::email-change-form
(s/keys :req-un [::email-1 ::email-2]))
(defn- on-error
[form error]
(cond
(= (:code error) :uxbox.services.mutations.profile/email-already-exists)
(swap! form (fn [data]
(let [error {:message (tr "errors.email-already-exists")}]
(-> data
(assoc-in [:errors :email-1] error)
(assoc-in [:errors :email-2] error)))))
:else
(let [msg (tr "errors.unexpected-error")]
(st/emit! (dm/error msg)))))
(defn- on-submit
[form event]
(let [data (with-meta {:email (get-in form [:clean-data :email-1])}
{:on-error (partial on-error form)})]
(st/emit! (du/request-email-change data))))
(mf/defc change-email-form
[{:keys [locale profile] :as props}]
[:section.modal-content.generic-form
[:h2 (t locale "settings.change-email-title")]
[:span.featured-note
[:span.text
[:span "Well send you an email to your current email "]
[:strong (:email profile)]
[:span " to verify your identity."]]]
[:& form {:on-submit on-submit
:spec ::email-change-form
:initial {}}
[:& input {:type "text"
:name :email-1
:label (t locale "settings.new-email-label")}]
[:& input {:type "text"
:name :email-2
:label (t locale "settings.confirm-email-label")}]
[:& submit-button
{:label (t locale "settings.change-email-submit-label")}]]])
(mf/defc change-email-confirmation
[{:keys [locale profile] :as locale}]
[:section.modal-content.generic-form.confirmation
[:h2 (t locale "settings.verification-sent-title")]
[:span.featured-note
[:span.icon i/trash]
[:span.text
[:span (str/format "We have sent you an email to “")]
[:strong (:email profile)]
[:span "” Please follow the instructions to verify the email."]]]
[:button.btn-primary.btn-large
{:on-click #(modal/hide!)}
(t locale "settings.close-modal-label")]])
(mf/defc change-email-modal
[props]
(let [locale (mf/deref i18n/locale)
profile (mf/deref refs/profile)]
[:section.generic-modal.change-email-modal
[:span.close {:on-click #(modal/hide!)} i/close]
(if (:pending-email profile)
[:& change-email-confirmation {:locale locale :profile profile}]
[:& change-email-form {:locale locale :profile profile}])]))

View file

@ -0,0 +1,42 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.delete-account
(:require
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as da]
[uxbox.main.data.users :as du]
[uxbox.main.store :as st]
[uxbox.main.ui.modal :as modal]
[uxbox.util.i18n :as i18n :refer [tr t]]))
(mf/defc delete-account-modal
[props]
(let [locale (mf/deref i18n/locale)]
[:section.generic-modal.change-email-modal
[:span.close {:on-click #(modal/hide!)} i/close]
[:section.modal-content.generic-form
[:h2 (t locale "settings.delete-account-title")]
[:span.featured-note
[:span.text
[:span (t locale "settings.delete-account-info")]]]
[:div.button-row
[:button.btn-warning.btn-large
{:on-click #(do
(modal/hide!)
(st/emit! da/request-account-deletion))}
(t locale "settings.yes-delete-my-account")]
[:button.btn-secondary.btn-large
{:on-click #(modal/hide!)}
(t locale "settings.cancel-and-keep-my-account")]]]]))

View file

@ -16,22 +16,60 @@
(mf/defc header (mf/defc header
[{:keys [section profile] :as props}] [{:keys [section profile] :as props}]
(let [profile? (= section :settings-profile) (let [profile? (= section :settings-profile)
password? (= section :settings-password) password? (= section :settings-password)
locale (i18n/use-locale) options? (= section :settings-options)
team-id (:default-team-id profile)]
[:header team-id (:default-team-id profile)
[:div.main-logo go-back #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))
{:on-click #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))} logout #(st/emit! da/logout)
i/logo-icon]
[:section.main-bar locale (mf/deref i18n/locale)
[:nav team-id (:default-team-id profile)]
[:a.nav-item [:header
{:class (when profile? "current") [:section.secondary-menu
:on-click #(st/emit! (rt/nav :settings-profile))} [:div.left {:on-click go-back}
(t locale "settings.profile")] [:span.icon i/arrow-slide]
[:a.nav-item [:span.label "Dashboard"]]
{:class (when password? "current") [:div.right {:on-click logout}
:on-click #(st/emit! (rt/nav :settings-password))} [:span.label "Log out"]
(t locale "settings.password")]]]])) [:span.icon i/logout]]]
[:h1 "Your account"]
[:nav
[:a.nav-item
{:class (when profile? "current")
:on-click #(st/emit! (rt/nav :settings-profile))}
(t locale "settings.profile")]
[:a.nav-item
{:class (when password? "current")
:on-click #(st/emit! (rt/nav :settings-password))}
(t locale "settings.password")]
[:a.nav-item
{:class (when options? "current")
:on-click #(st/emit! (rt/nav :settings-options))}
(t locale "settings.options")]
[:a.nav-item
{:class "foobar"
:on-click #(st/emit! (rt/nav :settings-profile))}
(t locale "settings.teams")]]]))
;; [:div.main-logo
;; {:on-click #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))}
;; i/logo-icon]
;; [:section.main-bar
;; [:nav
;; [:a.nav-item
;; {:class (when profile? "current")
;; :on-click #(st/emit! (rt/nav :settings-profile))}
;; (t locale "settings.profile")]
;; [:a.nav-item
;; {:class (when password? "current")
;; :on-click #(st/emit! (rt/nav :settings-password))}
;; (t locale "settings.password")]]]]))

View file

@ -1,43 +0,0 @@
;; 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) 2016 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.settings.notifications
(:require
[cuerdas.core :as str]
[rumext.alpha :as mf]
[uxbox.util.i18n :refer [tr]]))
(mf/defc notifications-page
[]
[:section.dashboard-content.user-settings
[:section.user-settings-content
[:span.user-settings-label (tr "settings.notifications.notifications-saved")]
[:p (tr "settings.notifications.description")]
[:div.input-radio.radio-primary
[:input {:type "radio"
:id "notification-1"
:name "notification"
:value "none"}]
[:label {:for "notification-1"
:value (tr "settings.notifications.none")} (tr "settings.notifications.none")]
[:input {:type "radio"
:id "notification-2"
:name "notification"
:value "every-hour"}]
[:label {:for "notification-2"
:value (tr "settings.notifications.every-hour")} (tr "settings.notifications.every-hour")]
[:input {:type "radio"
:id "notification-3"
:name "notification"
:value "every-day"}]
[:label {:for "notification-3"
:value (tr "settings.notifications.every-day")} (tr "settings.notifications.every-day")]]
[:input.btn-primary {:type "submit"
:class "btn-disabled"
:disabled true
:value (tr "settings.update-settings")}]
]])

View file

@ -0,0 +1,75 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.options
(:require
[rumext.alpha :as mf]
[cljs.spec.alpha :as s]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.users :as udu]
[uxbox.main.data.messages :as dm]
[uxbox.main.ui.components.forms :refer [select submit-button form]]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t tr]]))
(s/def ::lang (s/nilable ::fm/not-empty-string))
(s/def ::theme (s/nilable ::fm/not-empty-string))
(s/def ::options-form
(s/keys :opt-un [::lang ::theme]))
(defn- on-error
[form error])
(defn- on-submit
[form event]
(dom/prevent-default event)
(let [data (:clean-data form)
on-success #(st/emit! (dm/info (tr "settings.notifications.profile-saved")))
on-error #(on-error % form)]
(st/emit! (udu/update-profile (with-meta data
{:on-success on-success
:on-error on-error})))))
(mf/defc options-form
[{:keys [locale profile] :as props}]
[:& form {:class "options-form"
:on-submit on-submit
:spec ::options-form
:initial profile}
[:h2 (t locale "settings.language-change-title")]
[:& select {:options [{:label "English" :value "en"}
{:label "Français" :value "fr"}]
:label (t locale "settings.language-label")
:default "en"
:name :lang}]
[:h2 (t locale "settings.theme-change-title")]
[:& select {:label (t locale "settings.theme-label")
:name :theme
:default "default"
:options [{:label "Default" :value "default"}]}]
[:& submit-button
{:label (t locale "settings.profile-submit-label")}]])
;; --- Password Page
(mf/defc options-page
[props]
(let [locale (mf/deref i18n/locale)
profile (mf/deref refs/profile)]
[:section.settings-options.generic-form
[:div.forms-container
[:& options-form {:locale locale :profile profile}]]]))

View file

@ -5,8 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; defined by the Mozilla Public License, v. 2.0.
;; ;;
;; Copyright (c) 2016-2020 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2020 UXBOX Labs SL
;; Copyright (c) 2016-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.settings.password (ns uxbox.main.ui.settings.password
(:require (:require
@ -15,40 +14,52 @@
[uxbox.main.ui.icons :as i] [uxbox.main.ui.icons :as i]
[uxbox.main.data.users :as udu] [uxbox.main.data.users :as udu]
[uxbox.main.data.messages :as dm] [uxbox.main.data.messages :as dm]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.forms :as fm] [uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr]])) [uxbox.util.i18n :as i18n :refer [t tr]]))
(defn- on-error (defn- on-error
[form error] [form error]
(case (:code error) (case (:code error)
:uxbox.services.users/old-password-not-match :uxbox.services.mutations.profile/old-password-not-match
(swap! form assoc-in [:errors :password-old] (swap! form assoc-in [:errors :password-old]
{:type ::api :message "settings.password.wrong-old-password"}) {:message (tr "errors.wrong-old-password")})
:else (throw (ex-info "unexpected" {:error error})))) :else
(let [msg (tr "generic.error")]
(st/emit! (dm/error msg)))))
(defn- on-success
[form]
(let [msg (tr "settings.notifications.password-saved")]
(st/emit! (dm/info msg))))
(defn- on-submit (defn- on-submit
[event form] [form event]
(dom/prevent-default event) (dom/prevent-default event)
(let [data (:clean-data form) (let [params (with-meta (:clean-data form)
mdata {:on-success #(st/emit! (dm/info (tr "settings.password.password-saved"))) {:on-success (partial on-success form)
:on-error #(on-error form %)}] :on-error (partial on-error form)})]
(st/emit! (udu/update-password (with-meta data mdata))))) (st/emit! (udu/update-password params))))
(s/def ::password-1 ::fm/not-empty-string) (s/def ::password-1 ::fm/not-empty-string)
(s/def ::password-2 ::fm/not-empty-string) (s/def ::password-2 ::fm/not-empty-string)
(s/def ::password-old ::fm/not-empty-string) (s/def ::password-old ::fm/not-empty-string)
(defn password-equality (defn- password-equality
[data] [data]
(let [password-1 (:password-1 data) (let [password-1 (:password-1 data)
password-2 (:password-2 data)] password-2 (:password-2 data)]
(when (and password-1 password-2
(not= password-1 password-2)) (cond-> {}
{:password-2 {:code ::password-not-equal (and password-1 password-2
:message "profile.password.not-equal"}}))) (not= password-1 password-2))
(assoc :password-2 {:message (tr "errors.password-invalid-confirmation")})
(and password-1 (> 8 (count password-1)))
(assoc :password-1 {:message (tr "errors.password-too-short")}))))
(s/def ::password-form (s/def ::password-form
(s/keys :req-un [::password-1 (s/keys :req-un [::password-1
@ -56,54 +67,37 @@
::password-old])) ::password-old]))
(mf/defc password-form (mf/defc password-form
[props] [{:keys [locale] :as props}]
(let [{:keys [data] :as form} (fm/use-form2 :spec ::password-form [:& form {:class "password-form"
:validators [password-equality] :on-submit on-submit
:initial {})] :spec ::password-form
[:form.password-form {:on-submit #(on-submit % form)} :validators [password-equality]
[:span.settings-label (tr "settings.password.change-password")] :initial {}}
[:input.input-text [:h2 (t locale "settings.password-change-title")]
{:type "password"
:name "password-old"
:value (:password-old data "")
:class (fm/error-class form :password-old)
:on-blur (fm/on-input-blur form :password-old)
:on-change (fm/on-input-change form :password-old)
:placeholder (tr "settings.password.old-password")}]
[:& fm/field-error {:form form :field :password-old :type ::api}] [:& input
{:type "password"
:name :password-old
:label (t locale "settings.old-password-label")}]
[:input.input-text [:& input
{:type "password" {:type "password"
:name "password-1" :name :password-1
:value (:password-1 data "") :label (t locale "settings.new-password-label")}]
:class (fm/error-class form :password-1)
:on-blur (fm/on-input-blur form :password-1)
:on-change (fm/on-input-change form :password-1)
:placeholder (tr "settings.password.new-password")}]
[:& fm/field-error {:form form :field :password-1}] [:& input
{:type "password"
:name :password-2
:label (t locale "settings.confirm-password-label")}]
[:input.input-text [:& submit-button
{:type "password" {:label (t locale "settings.profile-submit-label")}]])
:name "password-2"
:value (:password-2 data "")
:class (fm/error-class form :password-2)
:on-blur (fm/on-input-blur form :password-2)
:on-change (fm/on-input-change form :password-2)
:placeholder (tr "settings.password.confirm-password")}]
[:& fm/field-error {:form form :field :password-2}]
[:input.btn-primary.btn-large
{:type "submit"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (tr "settings.update-settings")}]]))
;; --- Password Page ;; --- Password Page
(mf/defc password-page (mf/defc password-page
[props] [props]
[:section.settings-password (let [locale (mf/deref i18n/locale)]
[:& password-form]]) [:section.settings-password.generic-form
[:div.forms-container
[:& password-form {:locale locale}]]]))

View file

@ -2,8 +2,10 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; 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/. ;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;; ;;
;; Copyright (c) 2016-2017 Andrey Antukh <niwi@niwi.nz> ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; Copyright (c) 2016-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com> ;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.profile (ns uxbox.main.ui.settings.profile
(:require (:require
@ -13,6 +15,10 @@
[rumext.alpha :as mf] [rumext.alpha :as mf]
[uxbox.main.ui.icons :as i] [uxbox.main.ui.icons :as i]
[uxbox.main.data.users :as udu] [uxbox.main.data.users :as udu]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.ui.settings.change-email :refer [change-email-modal]]
[uxbox.main.ui.settings.delete-account :refer [delete-account-modal]]
[uxbox.main.ui.modal :as modal]
[uxbox.main.data.messages :as dm] [uxbox.main.data.messages :as dm]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.main.refs :as refs] [uxbox.main.refs :as refs]
@ -21,8 +27,6 @@
[uxbox.util.i18n :as i18n :refer [tr t]])) [uxbox.util.i18n :as i18n :refer [tr t]]))
(s/def ::fullname ::fm/not-empty-string) (s/def ::fullname ::fm/not-empty-string)
(s/def ::lang (s/nilable ::fm/not-empty-string))
(s/def ::theme ::fm/not-empty-string)
(s/def ::email ::fm/email) (s/def ::email ::fm/email)
(s/def ::profile-form (s/def ::profile-form
@ -30,22 +34,12 @@
(defn- on-error (defn- on-error
[error form] [error form]
(case (:code error) (st/emit! (dm/error (tr "errors.generic"))))
:uxbox.services.users/email-already-exists
(swap! form assoc-in [:errors :email]
{:type ::api
:message "errors.api.form.email-already-exists"})
:uxbox.services.users/username-already-exists
(swap! form assoc-in [:errors :username]
{:type ::api
:message "errors.api.form.username-already-exists"})))
(defn- on-submit (defn- on-submit
[event form] [form event]
(dom/prevent-default event)
(let [data (:clean-data form) (let [data (:clean-data form)
on-success #(st/emit! (dm/info (tr "settings.profile.profile-saved"))) on-success #(st/emit! (dm/info (tr "settings.notifications.profile-saved")))
on-error #(on-error % form)] on-error #(on-error % form)]
(st/emit! (udu/update-profile (with-meta data (st/emit! (udu/update-profile (with-meta data
{:on-success on-success {:on-success on-success
@ -54,64 +48,58 @@
;; --- Profile Form ;; --- Profile Form
(mf/defc profile-form (mf/defc profile-form
[props] [{:keys [locale] :as props}]
(let [locale (i18n/use-locale) (let [prof (mf/deref refs/profile)]
form (fm/use-form ::profile-form #(deref refs/profile)) [:& form {:on-submit on-submit
data (:data form)] :class "profile-form"
[:form.profile-form {:on-submit #(on-submit % form)} :spec ::profile-form
[:span.settings-label (t locale "settings.profile.section-basic-data")] :initial prof}
[:& input
[:input.input-text
{:type "text" {:type "text"
:name "fullname" :name :fullname
:class (fm/error-class form :fullname) :label (t locale "settings.fullname-label")}]
:on-blur (fm/on-input-blur form :fullname)
:on-change (fm/on-input-change form :fullname)
:value (:fullname data "")
:placeholder (t locale "settings.profile.your-name")}]
[:& fm/field-error {:form form
:type #{::api}
:field :fullname}]
[:input.input-text [:& input
{:type "email" {:type "email"
:name "email" :name :email
:class (fm/error-class form :email) :disabled true
:on-blur (fm/on-input-blur form :email) :help-icon i/at
:on-change (fm/on-input-change form :email) :label (t locale "settings.email-label")}]
:value (:email data "")
:placeholder (t locale "settings.profile.your-email")}]
[:& fm/field-error {:form form
:type #{::api}
:field :email}]
[:span.settings-label (t locale "settings.profile.lang")] (cond
[:select.input-select {:value (:lang data) (nil? (:pending-email prof))
:name "lang" [:div.change-email
:class (fm/error-class form :lang) [:a {:on-click #(modal/show! change-email-modal {})}
:on-blur (fm/on-input-blur form :lang) (t locale "settings.change-email-label")]]
:on-change (fm/on-input-change form :lang)}
[:option {:value "en"} "English"]
[:option {:value "fr"} "Français"]]
[:span.user-settings-label (tr "settings.profile.section-theme-data")] (not= (:pending-email prof) (:email prof))
[:select.input-select {:value (:theme data) [:span.featured-note
:name "theme" [:span.icon i/trash]
:class (fm/error-class form :theme) [:span.text
:on-blur (fm/on-input-blur form :theme) [:span "There is a pending change of your email to "]
:on-change (fm/on-input-change form :theme)} [:strong (:pending-email prof)]
[:option {:value "light"} "Default"]] [:span "."] [:br]
[:a {:on-click #(st/emit! udu/cancel-email-change)}
"Dismiss"]]]
[:input.btn-primary.btn-large :else
{:type "submit" [:span.featured-note.warning
:class (when-not (:valid form) "btn-disabled") [:span.text
:disabled (not (:valid form)) [:span "There is a pending email validation."]]])
:value (t locale "settings.update-settings")}]]))
[:& submit-button
{:label (t locale "settings.profile-submit-label")}]
[:div.links
[:div.link-item
[:a {:on-click #(modal/show! delete-account-modal {})}
(t locale "settings.remove-account-label")]]]]))
;; --- Profile Photo Form ;; --- Profile Photo Form
(mf/defc profile-photo-form (mf/defc profile-photo-form
[props] [{:keys [locale] :as props}]
(let [profile (mf/deref refs/profile) (let [profile (mf/deref refs/profile)
photo (:photo-uri profile) photo (:photo-uri profile)
photo (if (or (str/empty? photo) (nil? photo)) photo (if (or (str/empty? photo) (nil? photo))
@ -127,10 +115,12 @@
(st/emit! (udu/update-photo {:file file})) (st/emit! (udu/update-photo {:file file}))
(dom/clean-value! target)))] (dom/clean-value! target)))]
[:form.avatar-form [:form.avatar-form
[:img {:src photo}] [:div.image-change-field
[:input {:type "file" [:span.update-overlay (t locale "settings.update-photo-label")]
:value "" [:img {:src photo}]
:on-change on-change}]])) [:input {:type "file"
:value ""
:on-change on-change}]]]))
;; --- Profile Page ;; --- Profile Page
@ -138,7 +128,7 @@
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [locale (i18n/use-locale)] (let [locale (i18n/use-locale)]
[:section.settings-profile [:section.settings-profile.generic-form
[:span.settings-label (t locale "settings.profile.your-avatar")] [:div.forms-container
[:& profile-photo-form] [:& profile-photo-form {:locale locale}]
[:& profile-form]])) [:& profile-form {:locale locale}]]]))

View file

@ -35,7 +35,7 @@
[& params] [& params]
(assert (even? (count params))) (assert (even? (count params)))
(str/join " " (reduce (fn [acc [k v]] (str/join " " (reduce (fn [acc [k v]]
(if (true? v) (if (true? (boolean v))
(conj acc (name k)) (conj acc (name k))
acc)) acc))
[] []

View file

@ -50,44 +50,25 @@
:else acc)) :else acc))
(defn use-form (defn use-form
[spec initial]
(let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial)
:errors {}
:touched {}})
clean-data (s/conform spec (:data state))
problems (when (= ::s/invalid clean-data)
(::s/problems (s/explain-data spec (:data state))))
errors (merge (reduce interpret-problem {} problems)
(:errors state))]
(-> (assoc state
:errors errors
:clean-data (when (not= clean-data ::s/invalid) clean-data)
:valid (and (empty? errors)
(not= clean-data ::s/invalid)))
(impl-mutator update-state))))
(defn use-form2
[& {:keys [spec validators initial]}] [& {:keys [spec validators initial]}]
(let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial) (let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial)
:errors {} :errors {}
:touched {}}) :touched {}})
clean-data (s/conform spec (:data state))
problems (when (= ::s/invalid clean-data) cleaned (s/conform spec (:data state))
problems (when (= ::s/invalid cleaned)
(::s/problems (s/explain-data spec (:data state)))) (::s/problems (s/explain-data spec (:data state))))
errors (merge (reduce interpret-problem {} problems) errors (merge (reduce interpret-problem {} problems)
(when (not= clean-data ::s/invalid)
(reduce (fn [errors vf] (reduce (fn [errors vf]
(merge errors (vf clean-data))) (merge errors (vf (:data state))))
{} validators)) {} validators)
(:errors state))] (:errors state))]
(-> (assoc state (-> (assoc state
:errors errors :errors errors
:clean-data (when (not= clean-data ::s/invalid) clean-data) :clean-data (when (not= cleaned ::s/invalid) cleaned)
:valid (and (empty? errors) :valid (and (empty? errors)
(not= clean-data ::s/invalid))) (not= cleaned ::s/invalid)))
(impl-mutator update-state)))) (impl-mutator update-state))))
(defn on-input-change (defn on-input-change

View file

@ -9,8 +9,11 @@
(ns uxbox.util.object (ns uxbox.util.object
"A collection of helpers for work with javascript objects." "A collection of helpers for work with javascript objects."
(:refer-clojure :exclude [get get-in assoc!]) (:refer-clojure :exclude [set! get get-in assoc!])
(:require [goog.object :as gobj])) (:require
[cuerdas.core :as str]
[goog.object :as gobj]
["lodash/omit" :as omit]))
(defn get (defn get
([obj k] ([obj k]
@ -32,6 +35,14 @@
(rest keys) (rest keys)
(unchecked-get res key)))))) (unchecked-get res key))))))
(defn without
[obj keys]
(let [keys (cond
(vector? keys) (into-array keys)
(array? keys) keys
:else (throw (js/Error. "unexpected input")))]
(omit obj keys)))
(defn merge! (defn merge!
([a b] ([a b]
(js/Object.assign a b)) (js/Object.assign a b))
@ -42,3 +53,13 @@
[obj key value] [obj key value]
(unchecked-set obj key value) (unchecked-set obj key value)
obj) obj)
(defn- props-key-fn
[key]
(if (or (= key :class) (= key :class-name))
"className"
(str/camel (name key))))
(defn clj->props
[props]
(clj->js props :keyword-fn props-key-fn))