diff --git a/backend/resources/emails/change-email/en.mustache b/backend/resources/emails/change-email/en.mustache
new file mode 100644
index 000000000..efae9b35f
--- /dev/null
+++ b/backend/resources/emails/change-email/en.mustache
@@ -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
diff --git a/backend/resources/emails/password-recovery/en.mustache b/backend/resources/emails/password-recovery/en.mustache
index df68f236a..c08997c51 100644
--- a/backend/resources/emails/password-recovery/en.mustache
+++ b/backend/resources/emails/password-recovery/en.mustache
@@ -1,42 +1,18 @@
-- begin :subject
-Password recovery.
+Password reset.
-- end
-- begin :body-text
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
-
--- begin :body-html
-
-
-
-
- title
- {{> ../partials/inline_style }}
-
-
-
-
-
-
-
-
-
-
- TODO
- {{ token }}
-
-
-
-
-
- {{> ../partials/en/footer }}
-
-
--- end
\ No newline at end of file
diff --git a/backend/resources/emails/register/en.mustache b/backend/resources/emails/register/en.mustache
index 70c8fd99f..23992f874 100644
--- a/backend/resources/emails/register/en.mustache
+++ b/backend/resources/emails/register/en.mustache
@@ -1,41 +1,15 @@
-- begin :subject
-Welcome to UXBOX.
+Verify email.
-- end
-- begin :body-text
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.
--- end
+{{ publicUrl }}/#/auth/verify-token?token={{token}}
--- begin :body-html
-
-
-
-
- title
- {{> ../partials/inline_style }}
-
-
-
-
-
-
-
-
-
-
- Hello {{name}}!
- Welcome to UXBOX.
- UXBOX team.
-
-
-
-
-
- {{> ../partials/en/footer }}
-
-
+Enjoy!
+The UXBOX team.
-- end
\ No newline at end of file
diff --git a/backend/resources/migrations/0002.users.sql b/backend/resources/migrations/0002.users.sql
index cbd34cff1..6a58e4187 100644
--- a/backend/resources/migrations/0002.users.sql
+++ b/backend/resources/migrations/0002.users.sql
@@ -6,7 +6,10 @@ CREATE TABLE profile (
deleted_at timestamptz NULL,
fullname text NOT NULL DEFAULT '',
+
email text NOT NULL,
+ pending_email text NULL,
+
photo 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 (
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();
-
-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 (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
diff --git a/backend/resources/migrations/0007.remove_version.sql b/backend/resources/migrations/0007.remove-version.sql
similarity index 100%
rename from backend/resources/migrations/0007.remove_version.sql
rename to backend/resources/migrations/0007.remove-version.sql
diff --git a/backend/resources/migrations/0008.generic-token.sql b/backend/resources/migrations/0008.generic-token.sql
new file mode 100644
index 000000000..113917d0e
--- /dev/null
+++ b/backend/resources/migrations/0008.generic-token.sql
@@ -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
+);
diff --git a/backend/src/uxbox/config.clj b/backend/src/uxbox/config.clj
index 0d160fb62..409108cb2 100644
--- a/backend/src/uxbox/config.clj
+++ b/backend/src/uxbox/config.clj
@@ -26,6 +26,8 @@
:database-username "uxbox"
:database-password "uxbox"
+ :public-url "http://localhost:3449"
+
:redis-uri "redis://redis/0"
:media-directory "resources/public/media"
:assets-directory "resources/public/static"
@@ -67,11 +69,13 @@
(s/def ::registration-enabled ::us/boolean)
(s/def ::registration-domain-whitelist ::us/string)
(s/def ::debug-humanize-transit ::us/boolean)
+(s/def ::public-url ::us/string)
(s/def ::config
(s/keys :opt-un [::http-server-cors
::http-server-debug
::http-server-port
+ ::public-url
::database-username
::database-password
::database-uri
diff --git a/backend/src/uxbox/emails.clj b/backend/src/uxbox/emails.clj
index 2f7aeac72..883ac4327 100644
--- a/backend/src/uxbox/emails.clj
+++ b/backend/src/uxbox/emails.clj
@@ -62,3 +62,11 @@
(def password-recovery
"A password recovery notification email."
(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))
diff --git a/backend/src/uxbox/http/handlers.clj b/backend/src/uxbox/http/handlers.clj
index 225eb46d4..6a3170db6 100644
--- a/backend/src/uxbox/http/handlers.clj
+++ b/backend/src/uxbox/http/handlers.clj
@@ -18,6 +18,7 @@
#{:create-demo-profile
:logout
:profile
+ :verify-profile-token
:recover-profile
:register-profile
:request-profile-recovery
@@ -50,8 +51,17 @@
(:profile-id req) (assoc :profile-id (:profile-id req)))]
(if (or (:profile-id req)
(contains? unauthorized-services type))
- {:status 200
- :body (sm/handle (with-meta data {:req req}))}
+ (let [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
:body {:type :authentication
:code :unauthorized}})))
@@ -68,11 +78,11 @@
(defn logout-handler
[req]
- (some-> (get-in req [:cookies "auth-token"])
+ (some-> (get-in req [:cookies "auth-token" :value])
(uuid/uuid)
(session/delete))
- {:status 204
- :cookies {"auth-token" nil}
+ {:status 200
+ :cookies {"auth-token" {:value "" :max-age -1}}
:body ""})
(defn echo-handler
diff --git a/backend/src/uxbox/migrations.clj b/backend/src/uxbox/migrations.clj
index ea1d09225..62ae24c2f 100644
--- a/backend/src/uxbox/migrations.clj
+++ b/backend/src/uxbox/migrations.clj
@@ -34,8 +34,11 @@
:name "0006-presence"
:fn (mg/resource "migrations/0006.presence.sql")}
{:desc "Remove version"
- :name "0007.remove_version"
- :fn (mg/resource "migrations/0007.remove_version.sql")}]})
+ :name "0007-remove-version"
+ :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
diff --git a/backend/src/uxbox/services/mutations/profile.clj b/backend/src/uxbox/services/mutations/profile.clj
index e880bb77a..6861be038 100644
--- a/backend/src/uxbox/services/mutations/profile.clj
+++ b/backend/src/uxbox/services/mutations/profile.clj
@@ -46,6 +46,12 @@
(s/def ::old-password ::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
@@ -86,15 +92,6 @@
;; --- 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
[conn {:keys [id fullname lang theme] :as params}]
(db/update! conn :profile
@@ -117,7 +114,7 @@
(defn- validate-password!
[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))]
(when-not (:valid result)
(ex/raise :type :validation
@@ -179,14 +176,10 @@
(defn- update-profile-photo
[conn profile-id path]
- (let [sql "update profile set photo=$1
- where id=$2
- and deleted_at is null
- returning id"]
- (db/update! conn :profile
- {:photo (str path)}
- {:id profile-id})
- nil))
+ (db/update! conn :profile
+ {:photo (str path)}
+ {:id profile-id})
+ nil)
;; --- Mutation: Register Profile
@@ -211,36 +204,44 @@
[params]
(when-not (:registration-enabled cfg/config)
(ex/raise :type :restriction
- :code :registration-disabled))
+ :code ::registration-disabled))
+
(when-not (email-domain-in-whitelist? (:registration-domain-whitelist cfg/config)
(:email params))
(ex/raise :type :validation
:code ::email-domain-is-not-allowed))
+
(db/with-atomic [conn db/pool]
(check-profile-existence! conn params)
- (let [profile (register-profile conn params)]
- ;; TODO: send a correct link for email verification
- (let [data {:to (:email params)
- :name (:fullname params)}]
- (emails/send! conn emails/register data)
- profile))))
+ (let [profile (register-profile conn params)
+ token (-> (sodi.prng/random-bytes 32)
+ (sodi.util/bytes->b64s))
+ payload {:type :verify-email
+ :profile-id (:id profile)
+ :email (:email profile)}]
-(def ^:private sql:insert-profile
- "insert into profile (id, fullname, email, password, photo, is_demo)
- values ($1, $2, $3, $4, '', $5) returning *")
+ (db/insert! conn :generic-token
+ {:token token
+ :valid-until (dt/plus (dt/now)
+ (dt/duration {:days 30}))
+ :content (blob/encode payload)})
-(def ^:private sql:insert-email
- "insert into profile_email (profile_id, email, is_main)
- values ($1, $2, true)")
+ (emails/send! conn emails/register
+ {:to (:email profile)
+ :name (:fullname profile)
+ :public-url (:public-url cfg/config)
+ :token token})
+ profile)))
(def ^:private sql:profile-existence
"select exists (select * from profile
- where email = $1
+ where email = ?
and deleted_at is null) as val")
(defn- check-profile-existence!
[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)
(ex/raise :type :validation
:code ::email-already-exists))
@@ -256,68 +257,192 @@
(db/insert! conn :profile
{:id id
:fullname fullname
- :email email
+ :email (str/lower email)
+ :pending-email (if demo? nil email)
:photo ""
:password password
: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
[conn params]
(let [prof (create-profile conn params)
- _ (create-profile-email conn prof)
-
team (mt.teams/create-team conn {:profile-id (:id prof)
:name "Default"
: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)
:team-id (:id team)
:name "Drafts"
- :default? true})
- _ (mt.projects/create-project-profile conn {:project-id (:id proj)
- :profile-id (:id prof)})
- ]
+ :default? true})]
+ (mt.teams/create-team-profile conn {:team-id (:id team)
+ :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)
{:default-team (:id team)
: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
(s/def ::request-profile-recovery
(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
[{:keys [email] :as params}]
(letfn [(create-recovery-token [conn {:keys [id] :as profile}]
- (let [token (-> (sodi.prng/random-bytes 32)
- (sodi.util/bytes->b64s))
- sql sql:insert-recovery-token]
- (db/insert! conn :password-recovery-token
- {:profile-id id
- :token token})
+ (let [token (-> (sodi.prng/random-bytes 32)
+ (sodi.util/bytes->b64s))
+ payload {:type :password-recovery-token
+ :profile-id id}]
+ (db/insert! conn :generic-token
+ {:token token
+ :valid-until (dt/plus (dt/now) (dt/duration {:hours 24}))
+ :content (blob/encode payload)})
(assoc profile :token token)))
+
(send-email-notification [conn profile]
(emails/send! conn emails/password-recovery
{:to (:email profile)
+ :public-url (:public-url cfg/config)
:token (:token profile)
- :name (:fullname profile)})
- nil)]
+ :name (:fullname profile)}))]
+
(db/with-atomic [conn db/pool]
(let [profile (->> (retrieve-profile-by-email conn email)
(create-recovery-token conn))]
- (send-email-notification conn profile)))))
+ (send-email-notification conn profile)
+ nil))))
;; --- Mutation: Recover Profile
@@ -326,27 +451,30 @@
(s/def ::recover-profile
(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
[{:keys [token password]}]
(letfn [(validate-token [conn token]
- (let [sql "delete from password_recovery_token
- where token=$1 returning *"
- sql "select * from password_recovery_token
- where token=$1"]
- (-> {:token token}
- (db/get-by-params conn :password-recovery-token)
- (:profile-id))))
+ (let [{:keys [token content]}
+ (-> (db/get-by-params conn :generic-token {:token token})
+ (decode-token-row))]
+ (when (not= (:type content) :password-recovery-token)
+ (ex/raise :type :validation
+ :code :invalid-token))
+ (:profile-id content)))
+
(update-password [conn profile-id]
- (let [sql "update profile set password=$2 where id=$1"
- pwd (sodi.pwhash/derive password)]
- (db/update! conn :profile {:password pwd} {:id profile-id})
- nil))]
+ (let [pwd (sodi.pwhash/derive password)]
+ (db/update! conn :profile {:password pwd} {:id profile-id})))
+
+ (delete-token [conn token]
+ (db/delete! conn :generic-token {:token token}))]
+
+
(db/with-atomic [conn db/pool]
- (-> (validate-token conn token)
- (update-password conn)))))
+ (->> (validate-token conn token)
+ (update-password conn))
+ (delete-token conn token)
+ nil)))
;; --- Mutation: Delete Profile
@@ -391,6 +519,6 @@
(let [rows (db/exec! conn [sql:teams-ownership-check profile-id])]
(when-not (empty? rows)
(ex/raise :type :validation
- :code :owner-teams-with-people
+ :code ::owner-teams-with-people
:hint "The user need to transfer ownership of owned teams."
:context {:teams (mapv :team-id rows)}))))
diff --git a/backend/src/uxbox/services/queries/profile.clj b/backend/src/uxbox/services/queries/profile.clj
index 55737a2a2..abfac1c2d 100644
--- a/backend/src/uxbox/services/queries/profile.clj
+++ b/backend/src/uxbox/services/queries/profile.clj
@@ -73,8 +73,7 @@
(defn retrieve-profile-data
[conn id]
- (let [sql "select * from profile where id=? and deleted_at is null"]
- (db/exec-one! conn [sql id])))
+ (db/get-by-id conn :profile id))
(defn retrieve-profile
[conn id]
@@ -93,4 +92,4 @@
(defn strip-private-attrs
"Only selects a publicy visible profile attrs."
[o]
- (select-keys o [:id :fullname :lang :email :created-at :photo :theme :photo-uri]))
+ (dissoc o :password :deleted-at))
diff --git a/backend/src/uxbox/util/template.clj b/backend/src/uxbox/util/template.clj
index 36b4de119..4260682bd 100644
--- a/backend/src/uxbox/util/template.clj
+++ b/backend/src/uxbox/util/template.clj
@@ -11,6 +11,7 @@
[clojure.tools.logging :as log]
[clojure.walk :as walk]
[clojure.java.io :as io]
+ [cuerdas.core :as str]
[uxbox.common.exceptions :as ex])
(:import
java.io.StringReader
@@ -26,7 +27,7 @@
(walk/postwalk (fn [x]
(cond
(instance? clojure.lang.Named x)
- (name x)
+ (str/camel (name x))
(instance? clojure.lang.MapEntry x)
x
diff --git a/backend/src/uxbox/util/time.clj b/backend/src/uxbox/util/time.clj
index 994177427..a0afc53de 100644
--- a/backend/src/uxbox/util/time.clj
+++ b/backend/src/uxbox/util/time.clj
@@ -17,6 +17,7 @@
java.time.OffsetDateTime
java.time.Duration
java.util.Date
+ java.time.temporal.TemporalAmount
org.apache.logging.log4j.core.util.CronExpression))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -32,6 +33,10 @@
[]
(Instant/now))
+(defn plus
+ [d ta]
+ (.plus d ^TemporalAmount ta))
+
(defn- obj->duration
[{:keys [days minutes seconds hours nanos millis]}]
(cond-> (Duration/ofMillis (if (int? millis) ^long millis 0))
diff --git a/docs/02-Frontend-Developer-Guide.md b/docs/02-Frontend-Developer-Guide.md
index 446351e26..f5795b5f1 100644
--- a/docs/02-Frontend-Developer-Guide.md
+++ b/docs/02-Frontend-Developer-Guide.md
@@ -5,10 +5,11 @@ application.
## Access to clojure from javascript console
-The uxbox namespace of the main application is exported, so that is accessible from
-javascript console in Chrome developer tools. Object names and data types are converted
-to javascript style. For example you can emit the event to reset zoom level by typing
-this at the console (there is autocompletion for help):
+The uxbox namespace of the main application is exported, so that is
+accessible from javascript console in Chrome developer tools. Object
+names and data types are converted to javascript style. For example
+you can emit the event to reset zoom level by typing this at the
+console (there is autocompletion for help):
```javascript
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
-Debugging a problem in the viewport algorithms for grouping and rotating
-is difficult. We have set a visual debug mode that displays some
-annotations on screen, to help understanding what's happening.
+Debugging a problem in the viewport algorithms for grouping and
+rotating is difficult. We have set a visual debug mode that displays
+some annotations on screen, to help understanding what's happening.
To activate it, open the javascript console and type
+
```javascript
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
+
```javascript
uxbox.util.debug.debug_all()
uxbox.util.debug.debug_none()
@@ -34,8 +39,8 @@ uxbox.util.debug.debug_none()
## Debug state and objects
-There are also some useful functions to visualize the global state or any
-complex object. To use them from clojure:
+There are also some useful functions to visualize the global state or
+any complex object. To use them from clojure:
```clojure
(ns uxbox.util.debug)
diff --git a/frontend/deps.edn b/frontend/deps.edn
index db4f79961..55ea41dfb 100644
--- a/frontend/deps.edn
+++ b/frontend/deps.edn
@@ -1,11 +1,11 @@
{:paths ["src" "vendor" "resources" "../common"]
: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"}
com.cognitect/transit-cljs {:mvn/version "0.8.264"}
- environ/environ {:mvn/version "1.1.0"}
- metosin/reitit-core {:mvn/version "0.4.2"}
+ environ/environ {:mvn/version "1.2.0"}
+ metosin/reitit-core {:mvn/version "0.5.1"}
expound/expound {:mvn/version "0.8.4"}
danlentz/clj-uuid {:mvn/version "0.1.9"}
@@ -17,7 +17,7 @@
funcool/okulary {:mvn/version "2020.04.14-0"}
funcool/potok {:mvn/version "2.8.0-SNAPSHOT"}
funcool/promesa {:mvn/version "5.1.0"}
- funcool/rumext {:mvn/version "2020.05.04-0"}
+ funcool/rumext {:mvn/version "2020.05.22-1"}
}
:aliases
{:dev
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 1e62dd2c6..37a1e50de 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -29,14 +29,14 @@
"integrity": "sha512-QzVKww91fJv/KzARJBS/Im5GS2A8iE64E1HxOed72EmYOvPLG4PBw77QCIUjFl7VwWB3G/SVrxsHedJD/wtn1A=="
},
"@types/lodash": {
- "version": "4.14.150",
- "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.150.tgz",
- "integrity": "sha512-kMNLM5JBcasgYscD9x/Gvr6lTAv2NVgsKtet/hm93qMyf/D1pt+7jeEZklKJKxMVmXjxbRVQQGfqDSfipYCO6w=="
+ "version": "4.14.152",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.152.tgz",
+ "integrity": "sha512-Vwf9YF2x1GE3WNeUMjT5bTHa2DqgUo87ocdgTScupY2JclZ5Nn7W2RLM/N0+oreexUk8uaVugR81NnTY/jNNXg=="
},
"@types/q": {
- "version": "1.5.2",
- "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz",
- "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==",
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz",
+ "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==",
"dev": true
},
"ajv": {
@@ -256,6 +256,14 @@
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"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": {
@@ -352,18 +360,18 @@
"dev": true
},
"autoprefixer": {
- "version": "9.7.6",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.6.tgz",
- "integrity": "sha512-F7cYpbN7uVVhACZTeeIeealwdGM6wMtfWARVLTy5xmKtgVdBNJvbDRoCK3YO1orcs7gv/KwYlb3iXwu9Ug9BkQ==",
+ "version": "9.8.0",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.0.tgz",
+ "integrity": "sha512-D96ZiIHXbDmU02dBaemyAg53ez+6F5yZmapmgKcjm35yEe1uVDYI8hGW3VYoGRaG290ZFf91YxHrR518vC0u/A==",
"dev": true,
"requires": {
- "browserslist": "^4.11.1",
- "caniuse-lite": "^1.0.30001039",
+ "browserslist": "^4.12.0",
+ "caniuse-lite": "^1.0.30001061",
"chalk": "^2.4.2",
"normalize-range": "^0.1.2",
"num2fraction": "^1.2.2",
- "postcss": "^7.0.27",
- "postcss-value-parser": "^4.0.3"
+ "postcss": "^7.0.30",
+ "postcss-value-parser": "^4.1.0"
}
},
"aws-sign2": {
@@ -483,15 +491,25 @@
"integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz",
"integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ="
},
"bn.js": {
- "version": "4.11.8",
- "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
- "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.1.tgz",
+ "integrity": "sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA==",
"dev": true
},
"boolbase": {
@@ -602,21 +620,50 @@
"requires": {
"bn.js": "^4.1.0",
"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": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
- "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.0.tgz",
+ "integrity": "sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA==",
"dev": true,
"requires": {
- "bn.js": "^4.1.1",
- "browserify-rsa": "^4.0.0",
- "create-hash": "^1.1.0",
- "create-hmac": "^1.1.2",
- "elliptic": "^6.0.0",
- "inherits": "^2.0.1",
- "parse-asn1": "^5.0.0"
+ "bn.js": "^5.1.1",
+ "browserify-rsa": "^4.0.1",
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "elliptic": "^6.5.2",
+ "inherits": "^2.0.4",
+ "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": {
@@ -718,9 +765,9 @@
"dev": true
},
"caniuse-lite": {
- "version": "1.0.30001048",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001048.tgz",
- "integrity": "sha512-g1iSHKVxornw0K8LG9LLdf+Fxnv7T1Z+mMsf0/YYLclQX4Cd522Ap0Lrw6NFqHgezit78dtyWxzlV2Xfc7vgRg==",
+ "version": "1.0.30001062",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001062.tgz",
+ "integrity": "sha512-ei9ZqeOnN7edDrb24QfJ0OZicpEbsWxv7WusOiQGz/f2SfvBgHHbOEwBJ8HKGVSyx8Z6ndPjxzR6m0NQq+0bfw==",
"dev": true
},
"caseless": {
@@ -749,6 +796,7 @@
"anymatch": "^2.0.0",
"async-each": "^1.0.1",
"braces": "^2.3.2",
+ "fsevents": "^1.2.7",
"glob-parent": "^3.1.0",
"inherits": "^2.0.3",
"is-binary-path": "^1.0.0",
@@ -1073,6 +1121,14 @@
"requires": {
"bn.js": "^4.1.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": {
@@ -1233,9 +1289,9 @@
}
},
"date-fns": {
- "version": "2.12.0",
- "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.12.0.tgz",
- "integrity": "sha512-qJgn99xxKnFgB1qL4jpxU7Q2t0LOn1p8KMIveef3UZD7kqjT3tpFNNdXJelEHhE+rUgffriXriw/sOSU+cS1Hw=="
+ "version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.14.0.tgz",
+ "integrity": "sha512-1zD+68jhFgDIM0rF05rcwYO8cExdNqxjq4xP1QKM60Q45mnO6zaMWB4tOzrIr4M4GSLntsKeE4c9Bdl2jhL/yw=="
},
"dateformat": {
"version": "3.0.3",
@@ -1391,6 +1447,14 @@
"bn.js": "^4.1.0",
"miller-rabin": "^4.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": {
@@ -1488,9 +1552,9 @@
}
},
"electron-to-chromium": {
- "version": "1.3.426",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.426.tgz",
- "integrity": "sha512-sdQ7CXQbFflKY5CU63ra+kIYq9F7d1OqI33856qJZxTrwo0sLASdmoRl9lWpGrQDS9Nk/RFliQWd3PPDrZ+Meg==",
+ "version": "1.3.446",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
+ "integrity": "sha512-CLQaFuvkKqR9FD2G3cJrr1fV7DRMXiAKWLP2F8cxtvvtzAS7Tubt0kF47/m+uE61kiT+I7ZEn7HqLnmWdOhmuA==",
"dev": true
},
"elliptic": {
@@ -1506,6 +1570,14 @@
"inherits": "^2.0.1",
"minimalistic-assert": "^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": {
@@ -1527,9 +1599,9 @@
}
},
"entities": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
- "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.2.tgz",
+ "integrity": "sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw==",
"dev": true
},
"env-variable": {
@@ -1917,6 +1989,13 @@
"integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==",
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@@ -2081,6 +2160,17 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -2254,9 +2344,9 @@
},
"dependencies": {
"gulp-cli": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.0.tgz",
- "integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==",
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.1.tgz",
+ "integrity": "sha512-yEMxrXqY8mJFlaauFQxNrCpzWJThu0sH1sqlToaTOT063Hub9s/Nt2C+GSLe6feQ/IMWrHvGOOsyES7CQc9O+A==",
"dev": true,
"requires": {
"ansi-colors": "^1.0.1",
@@ -2488,9 +2578,9 @@
}
},
"safe-buffer": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
- "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==",
+ "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
}
}
@@ -3362,6 +3452,14 @@
"requires": {
"bn.js": "^4.0.0",
"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": {
@@ -3545,6 +3643,13 @@
"integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==",
"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": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@@ -3616,9 +3721,9 @@
}
},
"node-releases": {
- "version": "1.1.53",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.53.tgz",
- "integrity": "sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ==",
+ "version": "1.1.56",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.56.tgz",
+ "integrity": "sha512-EVo605FhWLygH8a64TjgpjyHYOihkxECwX1bHHr8tETJKWEiWS2YJjPbvsX2jFjnjTNEgBCmk9mLjKG1Mf11cw==",
"dev": true
},
"normalize-package-data": {
@@ -4141,9 +4246,9 @@
"dev": true
},
"postcss": {
- "version": "7.0.27",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.27.tgz",
- "integrity": "sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==",
+ "version": "7.0.30",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.30.tgz",
+ "integrity": "sha512-nu/0m+NtIzoubO+xdAlwZl/u5S5vi/y6BCsoL8D+8IxsD3XvBS8X4YEADNIVXKVuQvduiucnRv+vPIqj56EGMQ==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
@@ -4226,6 +4331,14 @@
"parse-asn1": "^5.0.0",
"randombytes": "^2.0.1",
"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": {
@@ -4314,9 +4427,9 @@
}
},
"react-color": {
- "version": "2.18.0",
- "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.18.0.tgz",
- "integrity": "sha512-FyVeU1kQiSokWc8NPz22azl1ezLpJdUyTbWL0LPUpcuuYDrZ/Y1veOk9rRK5B3pMlyDGvTk4f4KJhlkIQNRjEA==",
+ "version": "2.18.1",
+ "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.18.1.tgz",
+ "integrity": "sha512-X5XpyJS6ncplZs74ak0JJoqPi+33Nzpv5RYWWxn17bslih+X7OlgmfpmGC1fNvdkK7/SGWYf1JJdn7D2n5gSuQ==",
"requires": {
"@icons/material": "^0.2.4",
"lodash": "^4.17.11",
@@ -4488,9 +4601,9 @@
"dev": true
},
"replace-ext": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz",
- "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz",
+ "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==",
"dev": true
},
"replace-homedir": {
@@ -4749,9 +4862,9 @@
}
},
"shadow-cljs": {
- "version": "2.8.109",
- "resolved": "https://registry.npmjs.org/shadow-cljs/-/shadow-cljs-2.8.109.tgz",
- "integrity": "sha512-xUN5kBYgyk2OVv3Gz9/dxJdDNoImskYg6VNLpHkubCG46Q1Lv9tymd11Hyekka6WWk24QCNSVIyPta82txZGfQ==",
+ "version": "2.9.8",
+ "resolved": "https://registry.npmjs.org/shadow-cljs/-/shadow-cljs-2.9.8.tgz",
+ "integrity": "sha512-pZQT6hbTnT2CLN2lrp5bV9vglYd4hdlIqPqEateOZGmy+2RHYI6BLd2Zbx96hjnTaWcsSx3H9nv/B4nOGD6eDA==",
"dev": true,
"requires": {
"node-libs-browser": "^2.0.0",
@@ -4878,9 +4991,9 @@
}
},
"slate": {
- "version": "0.57.2",
- "resolved": "https://registry.npmjs.org/slate/-/slate-0.57.2.tgz",
- "integrity": "sha512-qxx9iwNmN3fn13hPbwh1p65aNLCgpHMMK/XXLX7rBVv+GT2UFys9tU8OK6FyUF/lU2uEJ++sScDu8cHjzwLefw==",
+ "version": "0.58.1",
+ "resolved": "https://registry.npmjs.org/slate/-/slate-0.58.1.tgz",
+ "integrity": "sha512-2Vj1jfzHQ/X4t23iKaWoEw09iuIo1oYIsl2tZjZTEl61VNwFEIZkjzI5yuyGS4x0QnUMbNtMoOCeJQx8HxHvdw==",
"requires": {
"@types/esrever": "^0.2.0",
"esrever": "^0.2.0",
@@ -4890,9 +5003,9 @@
}
},
"slate-react": {
- "version": "0.57.2",
- "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.57.2.tgz",
- "integrity": "sha512-fg91E7XISMnFfoHB8vPbbaKoTDpaTfE+iwnG9i6EzbIfwNysz8XvLDpRW3XExm1ZtAfhEKB3Um8nPMtGaugVRg==",
+ "version": "0.58.1",
+ "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.58.1.tgz",
+ "integrity": "sha512-y94fhdUYjCFsZiN0vEMo9pxL+HA9U8RH2E4w5LxjA2ZVFk2htf8rRZC+6sq5OHBrVjp2Tw09EJbMQhrahqrtew==",
"requires": {
"@types/is-hotkey": "^0.1.1",
"@types/lodash": "^4.14.149",
@@ -5078,9 +5191,9 @@
"dev": true
},
"spdx-expression-parse": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
- "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
"dev": true,
"requires": {
"spdx-exceptions": "^2.1.0",
@@ -5733,9 +5846,9 @@
"dev": true
},
"tslib": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
- "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA=="
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
},
"tty-browserify": {
"version": "0.0.0",
diff --git a/frontend/package.json b/frontend/package.json
index 9a24c4ced..0855a243d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -13,7 +13,7 @@
],
"scripts": {},
"devDependencies": {
- "autoprefixer": "^9.7.6",
+ "autoprefixer": "^9.8.0",
"clean-css": "^4.2.3",
"gulp": "4.0.2",
"gulp-gzip": "^1.4.2",
@@ -22,21 +22,21 @@
"gulp-rename": "^2.0.0",
"gulp-svg-sprite": "^1.5.0",
"mkdirp": "^1.0.4",
- "postcss": "^7.0.27",
+ "postcss": "^7.0.30",
"rimraf": "^3.0.0",
"sass": "^1.26.0",
- "shadow-cljs": "^2.8.96"
+ "shadow-cljs": "^2.9.7"
},
"dependencies": {
- "date-fns": "^2.12.0",
+ "date-fns": "^2.13.0",
"mousetrap": "^1.6.5",
"randomcolor": "^0.5.4",
"react": "^16.13.1",
- "react-color": "^2.18.0",
+ "react-color": "^2.18.1",
"react-dom": "^16.13.1",
"rxjs": "^7.0.0-beta.0",
- "slate": "^0.57.1",
- "slate-react": "^0.57.1",
+ "slate": "^0.58.1",
+ "slate-react": "^0.58.1",
"source-map-support": "^0.5.16",
"tdigest": "^0.1.1",
"xregexp": "^4.3.0"
diff --git a/frontend/resources/images/icons/at.svg b/frontend/resources/images/icons/at.svg
new file mode 100644
index 000000000..c9bb6aada
--- /dev/null
+++ b/frontend/resources/images/icons/at.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/resources/images/icons/logout.svg b/frontend/resources/images/icons/logout.svg
new file mode 100644
index 000000000..b5ae8c421
--- /dev/null
+++ b/frontend/resources/images/icons/logout.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json
index f39842a84..4183c8d8a 100644
--- a/frontend/resources/locales.json
+++ b/frontend/resources/locales.json
@@ -1,6 +1,192 @@
{
+ "auth.already-have-account" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:111" ],
+ "translations" : {
+ "en" : "Already have an account?",
+ "fr" : "Vous avez déjà un compte ?"
+ }
+ },
+ "auth.confirm-password-label" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/recovery.cljs:78" ],
+ "translations" : {
+ "en" : "Confirm password",
+ "fr" : "Confirmez mot de passe"
+ }
+ },
+ "auth.create-demo-profile" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:90", "src/uxbox/main/ui/auth/register.cljs:120" ],
+ "translations" : {
+ "en" : "Create demo account",
+ "fr" : null
+ }
+ },
+ "auth.create-demo-profile-label" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:87", "src/uxbox/main/ui/auth/register.cljs:117" ],
+ "translations" : {
+ "en" : "Just wanna try it?"
+ }
+ },
+ "auth.email-label" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:55", "src/uxbox/main/ui/auth/register.cljs:86", "src/uxbox/main/ui/auth/recovery_request.cljs:47" ],
+ "translations" : {
+ "en" : "Email",
+ "fr" : "adresse email"
+ }
+ },
+ "auth.forgot-password" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:78" ],
+ "translations" : {
+ "en" : "Forgot your password?",
+ "fr" : "Mot de passe oublié ?"
+ }
+ },
+ "auth.fullname-label" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:80" ],
+ "translations" : {
+ "en" : "Full Name",
+ "fr" : "Nom complet"
+ }
+ },
+ "auth.go-back-to-login" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/recovery_request.cljs:67" ],
+ "translations" : {
+ "en" : "Go back!",
+ "fr" : "Retour!"
+ }
+ },
+ "auth.goodbye-title" : {
+ "used-in" : [ "src/uxbox/main/ui/auth.cljs:33" ],
+ "translations" : {
+ "en" : "Goodbye!"
+ }
+ },
+ "auth.login-here" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:114" ],
+ "translations" : {
+ "en" : "Login here"
+ }
+ },
+ "auth.login-submit-label" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:63" ],
+ "translations" : {
+ "en" : "Sign in",
+ "fr" : "Se connecter"
+ }
+ },
+ "auth.login-subtitle" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:70" ],
+ "translations" : {
+ "en" : "Enter your details below"
+ }
+ },
+ "auth.login-title" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:69" ],
+ "translations" : {
+ "en" : "Great to see you again!"
+ }
+ },
+ "auth.new-password-label" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/recovery.cljs:74" ],
+ "translations" : {
+ "en" : "Type a new password",
+ "fr" : null
+ }
+ },
+ "auth.notifications.invalid-token-error" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/recovery.cljs:50" ],
+ "translations" : {
+ "en" : "The recovery token is invalid.",
+ "fr" : "Le jeton de récupération n'est pas valide."
+ }
+ },
+ "auth.notifications.password-changed-succesfully" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/recovery.cljs:54" ],
+ "translations" : {
+ "en" : "Password successfully changed"
+ }
+ },
+ "auth.notifications.recovery-token-sent" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/recovery_request.cljs:34" ],
+ "translations" : {
+ "en" : "Password recovery link sent to your inbox.",
+ "fr" : "Lien de récupération de mot de passe envoyé."
+ }
+ },
+ "auth.password-label" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:61", "src/uxbox/main/ui/auth/register.cljs:90" ],
+ "translations" : {
+ "en" : "Password",
+ "fr" : "Mot de passe"
+ }
+ },
+ "auth.password-length-hint" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:89" ],
+ "translations" : {
+ "en" : "At least 8 characters"
+ }
+ },
+ "auth.recovery-request-submit-label" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/recovery_request.cljs:51" ],
+ "translations" : {
+ "en" : "Recover Password"
+ }
+ },
+ "auth.recovery-request-subtitle" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/recovery_request.cljs:60" ],
+ "translations" : {
+ "en" : "We'll send you an email with instructions"
+ }
+ },
+ "auth.recovery-request-title" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/recovery_request.cljs:59" ],
+ "translations" : {
+ "en" : "Forgot your password?"
+ }
+ },
+ "auth.recovery-submit-label" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/recovery.cljs:81" ],
+ "translations" : {
+ "en" : "Change your password"
+ }
+ },
+ "auth.register" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:84" ],
+ "translations" : {
+ "en" : "Sign up here"
+ }
+ },
+ "auth.register-label" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:81" ],
+ "translations" : {
+ "en" : "No account yet?"
+ }
+ },
+ "auth.register-submit-label" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:94" ],
+ "translations" : {
+ "en" : "Create an account"
+ }
+ },
+ "auth.register-subtitle" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:103" ],
+ "translations" : {
+ "en" : "It's free, it's Open Source"
+ }
+ },
+ "auth.register-title" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:102" ],
+ "translations" : {
+ "en" : "Create an account"
+ }
+ },
+ "auth.sidebar-tagline" : {
+ "used-in" : [ "src/uxbox/main/ui/auth.cljs:44" ],
+ "translations" : {
+ "en" : "The open-source solution for design and prototyping."
+ }
+ },
"dashboard.grid.delete" : {
- "used-in" : [ "src/uxbox/main/ui/dashboard/grid.cljs:102", "src/uxbox/main/ui/dashboard/project.cljs:62" ],
+ "used-in" : [ "src/uxbox/main/ui/dashboard/project.cljs:62", "src/uxbox/main/ui/dashboard/grid.cljs:102" ],
"translations" : {
"en" : "Delete"
}
@@ -18,7 +204,7 @@
}
},
"dashboard.grid.rename" : {
- "used-in" : [ "src/uxbox/main/ui/dashboard/grid.cljs:101", "src/uxbox/main/ui/dashboard/project.cljs:61" ],
+ "used-in" : [ "src/uxbox/main/ui/dashboard/project.cljs:61", "src/uxbox/main/ui/dashboard/grid.cljs:101" ],
"translations" : {
"en" : "Rename"
}
@@ -320,40 +506,86 @@
}
},
"errors.api.form.registration-disabled" : {
- "used-in" : [ "src/uxbox/main/ui/profile/register.cljs:37" ],
"translations" : {
- "en" : "The registration is currently disabled.",
- "fr" : "L'enregistrement est actuellement désactivé."
- }
+ "en" : null,
+ "fr" : null
+ },
+ "unused" : true
},
"errors.api.form.unexpected-error" : {
- "used-in" : [ "src/uxbox/main/ui/profile/register.cljs:44" ],
"translations" : {
- "en" : "An unexpected error occurred.",
- "fr" : "Une erreur inattendue c'est produite"
- }
+ "en" : null,
+ "fr" : null
+ },
+ "unused" : true
},
"errors.auth.unauthorized" : {
- "used-in" : [ "src/uxbox/main/data/auth.cljs:57" ],
+ "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:37" ],
"translations" : {
"en" : "Username or password seems to be wrong.",
"fr" : "Le nom d'utilisateur ou le mot de passe semble être faux."
}
},
+ "errors.email-already-exists" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:38", "src/uxbox/main/ui/auth.cljs:84" ],
+ "translations" : {
+ "en" : "Email already used"
+ }
+ },
"errors.generic" : {
- "used-in" : [ "src/uxbox/main/ui.cljs:178" ],
+ "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:37", "src/uxbox/main/ui/auth.cljs:88", "src/uxbox/main/ui.cljs:179" ],
"translations" : {
"en" : "Something wrong has happened.",
"fr" : "Quelque chose c'est mal passé."
}
},
"errors.network" : {
- "used-in" : [ "src/uxbox/main/ui.cljs:172" ],
+ "used-in" : [ "src/uxbox/main/ui.cljs:173" ],
"translations" : {
"en" : "Unable to connect to backend server.",
"fr" : "Impossible de se connecter au serveur principal."
}
},
+ "errors.password-invalid-confirmation" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:59" ],
+ "translations" : {
+ "en" : null,
+ "fr" : null
+ }
+ },
+ "errors.password-too-short" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:62" ],
+ "translations" : {
+ "en" : "Password should at least be 8 characters"
+ }
+ },
+ "errors.registration-disabled" : {
+ "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:52" ],
+ "translations" : {
+ "en" : "The registration is currently disabled.",
+ "fr" : "L'enregistrement est actuellement désactivé."
+ }
+ },
+ "errors.unexpected-error" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:44", "src/uxbox/main/ui/auth/register.cljs:58" ],
+ "translations" : {
+ "en" : "An unexpected error occurred.",
+ "fr" : "Une erreur inattendue c'est produite"
+ }
+ },
+ "errors.wrong-old-password" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:28" ],
+ "translations" : {
+ "en" : "Old password is incorrect"
+ }
+ },
+ "generic.error" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:31" ],
+ "translations" : {
+ "en" : null,
+ "fr" : null
+ }
+ },
"header.sitemap" : {
"used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:67" ],
"translations" : {
@@ -396,299 +628,270 @@
"fr" : "Envoyer un fichier"
}
},
- "login.create-demo-profile" : {
- "used-in" : [ "src/uxbox/main/ui/login.cljs:91" ],
- "translations" : {
- "en" : "Create demo account",
- "fr" : null
- }
- },
- "login.create-demo-profile-description" : {
- "used-in" : [ "src/uxbox/main/ui/login.cljs:90" ],
- "translations" : {
- "en" : "Will be deleted in 24 hours since creation",
- "fr" : null
- }
- },
- "login.email" : {
- "used-in" : [ "src/uxbox/main/ui/login.cljs:62" ],
- "translations" : {
- "en" : "Email",
- "fr" : "adresse email"
- }
- },
- "login.forgot-password" : {
- "used-in" : [ "src/uxbox/main/ui/login.cljs:84" ],
- "translations" : {
- "en" : "Forgot your password?",
- "fr" : "Mot de passe oublié ?"
- }
- },
- "login.password" : {
- "used-in" : [ "src/uxbox/main/ui/login.cljs:71" ],
- "translations" : {
- "en" : "Password",
- "fr" : "Mot de passe"
- }
- },
- "login.register" : {
- "used-in" : [ "src/uxbox/main/ui/login.cljs:87" ],
- "translations" : {
- "en" : "Don't have an account?",
- "fr" : "Vous n'avez pas de compte ?"
- }
- },
- "login.submit" : {
- "used-in" : [ "src/uxbox/main/ui/login.cljs:78" ],
- "translations" : {
- "en" : "Sign in",
- "fr" : "Se connecter"
- }
- },
"modal.create-color.new-color" : {
"used-in" : [ "src/uxbox/main/ui/dashboard/library.cljs:48" ],
"translations" : {
"en" : "New Color"
}
},
- "profile.recovery.email" : {
- "used-in" : [ "src/uxbox/main/ui/profile/recovery_request.cljs:54" ],
- "translations" : {
- "en" : "Email Address",
- "fr" : "adresse email"
- }
- },
"profile.recovery.go-to-login" : {
- "used-in" : [ "src/uxbox/main/ui/profile/recovery.cljs:81", "src/uxbox/main/ui/profile/recovery_request.cljs:65" ],
+ "used-in" : [ "src/uxbox/main/ui/auth/recovery.cljs:97" ],
"translations" : {
- "en" : "Go back!",
- "fr" : "Retour!"
- }
- },
- "profile.recovery.invalid-token" : {
- "used-in" : [ "src/uxbox/main/ui/profile/recovery.cljs:45" ],
- "translations" : {
- "en" : "The recovery token is invalid.",
- "fr" : "Le jeton de récupération n'est pas valide."
- }
- },
- "profile.recovery.password" : {
- "used-in" : [ "src/uxbox/main/ui/profile/recovery.cljs:70" ],
- "translations" : {
- "en" : "Type a new password",
+ "en" : null,
"fr" : null
}
},
- "profile.recovery.password-changed" : {
- "used-in" : [ "src/uxbox/main/ui/profile/recovery.cljs:40" ],
+ "settings.cancel-and-keep-my-account" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/delete_account.cljs:42" ],
"translations" : {
- "en" : "Password successfully changed",
- "fr" : "TODO"
+ "en" : "Cancel and keep my account"
}
},
- "profile.recovery.recovery-token-sent" : {
- "used-in" : [ "src/uxbox/main/ui/profile/recovery_request.cljs:39" ],
+ "settings.change-email-label" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:73" ],
"translations" : {
- "en" : "Password recovery link sent to your inbox.",
- "fr" : "Lien de récupération de mot de passe envoyé."
+ "en" : "Change email"
}
},
- "profile.recovery.submit-recover" : {
- "used-in" : [ "src/uxbox/main/ui/profile/recovery.cljs:76" ],
+ "settings.change-email-submit-label" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:76" ],
"translations" : {
- "en" : "Change your password",
- "fr" : null
+ "en" : "Change email"
}
},
- "profile.recovery.submit-request" : {
- "used-in" : [ "src/uxbox/main/ui/profile/recovery_request.cljs:60" ],
+ "settings.change-email-title" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:56" ],
"translations" : {
- "en" : "Recover Password",
- "fr" : null
+ "en" : "Change your email"
}
},
- "profile.recovery.token" : {
- "used-in" : [ "src/uxbox/main/ui/profile/recovery.cljs:61" ],
+ "settings.close-modal-label" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:92" ],
"translations" : {
- "en" : "Recovery token (sent by email)",
- "fr" : null
+ "en" : "Close"
}
},
- "profile.register.already-have-account" : {
- "used-in" : [ "src/uxbox/main/ui/profile/register.cljs:110" ],
+ "settings.confirm-email-label" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:73" ],
"translations" : {
- "en" : "Already have an account?",
- "fr" : "Vous avez déjà un compte ?"
+ "en" : "Verify new email"
}
},
- "profile.register.email" : {
- "used-in" : [ "src/uxbox/main/ui/profile/register.cljs:80" ],
- "translations" : {
- "en" : "Your email",
- "fr" : "Votre adresse email"
- }
- },
- "profile.register.fullname" : {
- "used-in" : [ "src/uxbox/main/ui/profile/register.cljs:65" ],
- "translations" : {
- "en" : "Full Name",
- "fr" : "Nom complet"
- }
- },
- "profile.register.get-started" : {
- "used-in" : [ "src/uxbox/main/ui/profile/register.cljs:106" ],
- "translations" : {
- "en" : "Get started",
- "fr" : "Commencer"
- }
- },
- "profile.register.password" : {
- "used-in" : [ "src/uxbox/main/ui/profile/register.cljs:94" ],
- "translations" : {
- "en" : "Password",
- "fr" : "Mot de passe"
- }
- },
- "settings.notifications.description" : {
- "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:19" ],
- "translations" : {
- "en" : "Get a roll up of prototype changes in your inbox.",
- "fr" : "Obtenez un résumé des modifications apportées aux prototypes à votre adresse email."
- }
- },
- "settings.notifications.every-day" : {
- "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:38", "src/uxbox/main/ui/settings/notifications.cljs:38" ],
- "translations" : {
- "en" : "Every day",
- "fr" : "Chaque jour"
- }
- },
- "settings.notifications.every-hour" : {
- "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:32", "src/uxbox/main/ui/settings/notifications.cljs:32" ],
- "translations" : {
- "en" : "Every hour",
- "fr" : "Chaque heure"
- }
- },
- "settings.notifications.none" : {
- "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:26", "src/uxbox/main/ui/settings/notifications.cljs:26" ],
- "translations" : {
- "en" : "None",
- "fr" : "Aucune"
- }
- },
- "settings.notifications.notifications-saved" : {
- "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:18" ],
- "translations" : {
- "en" : "Notifications preferences saved successfully!",
- "fr" : "Préférences de notifications enregistrées avec succès !"
- }
- },
- "settings.password" : {
- "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:36" ],
- "translations" : {
- "en" : "PASSWORD",
- "fr" : "MOT DE PASSE"
- }
- },
- "settings.password.change-password" : {
- "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:64" ],
- "translations" : {
- "en" : "Change password",
- "fr" : "Changement de mot de passe"
- }
- },
- "settings.password.confirm-password" : {
- "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:94" ],
+ "settings.confirm-password-label" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:91" ],
"translations" : {
"en" : "Confirm password",
"fr" : "Confirmez mot de passe"
}
},
- "settings.password.new-password" : {
- "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:83" ],
+ "settings.delete-account-info" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/delete_account.cljs:32" ],
+ "translations" : {
+ "en" : "By removing your account you’ll lose all your current projects and archives."
+ }
+ },
+ "settings.delete-account-title" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/delete_account.cljs:28" ],
+ "translations" : {
+ "en" : "Are you sure you want to delete your account?"
+ }
+ },
+ "settings.email-label" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:67" ],
+ "translations" : {
+ "en" : "Email"
+ }
+ },
+ "settings.fullname-label" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:60" ],
+ "translations" : {
+ "en" : "Your name",
+ "fr" : "Votre nom complet"
+ }
+ },
+ "settings.language-change-title" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/options.cljs:50" ],
+ "translations" : {
+ "en" : "Language"
+ }
+ },
+ "settings.language-label" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/options.cljs:54" ],
+ "translations" : {
+ "en" : "Select UI language"
+ }
+ },
+ "settings.new-email-label" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:69" ],
+ "translations" : {
+ "en" : "New email"
+ }
+ },
+ "settings.new-password-label" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:86" ],
"translations" : {
"en" : "New password",
"fr" : "Nouveau mot de passe"
}
},
- "settings.password.old-password" : {
- "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:72" ],
+ "settings.notifications.description" : {
"translations" : {
- "en" : "Old password",
- "fr" : "Ancien mot de passe"
+ "en" : null,
+ "fr" : null
+ },
+ "unused" : true
+ },
+ "settings.notifications.email-changed-successfully" : {
+ "used-in" : [ "src/uxbox/main/ui/auth.cljs:64" ],
+ "translations" : {
+ "en" : "Your email address has been updated successfully"
}
},
- "settings.password.password-saved" : {
+ "settings.notifications.email-verified-successfully" : {
+ "used-in" : [ "src/uxbox/main/ui/auth.cljs:57" ],
+ "translations" : {
+ "en" : "Your email address has been verified successfully"
+ }
+ },
+ "settings.notifications.every-day" : {
+ "translations" : {
+ "en" : null,
+ "fr" : null
+ },
+ "unused" : true
+ },
+ "settings.notifications.every-hour" : {
+ "translations" : {
+ "en" : null,
+ "fr" : null
+ },
+ "unused" : true
+ },
+ "settings.notifications.none" : {
+ "translations" : {
+ "en" : null,
+ "fr" : null
+ },
+ "unused" : true
+ },
+ "settings.notifications.notifications-saved" : {
+ "translations" : {
+ "en" : null,
+ "fr" : null
+ },
+ "unused" : true
+ },
+ "settings.notifications.password-saved" : {
"used-in" : [ "src/uxbox/main/ui/settings/password.cljs:36" ],
"translations" : {
"en" : "Password saved successfully!",
"fr" : "Mot de passe enregistré avec succès !"
}
},
- "settings.profile" : {
- "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:32" ],
+ "settings.notifications.profile-deletion-not-allowed" : {
"translations" : {
- "en" : "PROFILE",
- "fr" : "PROFIL"
- }
+ "en" : null,
+ "fr" : null
+ },
+ "used-in" : [ "src/uxbox/main/data/auth.cljs:121" ]
},
- "settings.profile.lang" : {
- "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:88" ],
- "translations" : {
- "en" : "Default language",
- "fr" : "Langue par défaut"
- }
- },
- "settings.profile.profile-saved" : {
- "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:48" ],
+ "settings.notifications.profile-saved" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/options.cljs:37", "src/uxbox/main/ui/settings/profile.cljs:42" ],
"translations" : {
"en" : "Profile saved successfully!",
"fr" : "Profil enregistré avec succès !"
}
},
- "settings.profile.section-basic-data" : {
- "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:62" ],
+ "settings.old-password-label" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:81" ],
"translations" : {
- "en" : "Name, username and email",
- "fr" : "Nom, nom d'utilisateur et adresse email"
+ "en" : "Old password",
+ "fr" : "Ancien mot de passe"
}
},
- "settings.profile.section-theme-data" : {
- "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:97" ],
+ "settings.options" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:52" ],
"translations" : {
- "en" : "Default theme",
- "fr" : "Thème par défaut"
+ "en" : "OPTIONS"
}
},
- "settings.profile.your-avatar" : {
- "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:142" ],
+ "settings.password" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:47" ],
"translations" : {
- "en" : "Your avatar",
- "fr" : "Votre avatar"
+ "en" : "PASSWORD",
+ "fr" : "MOT DE PASSE"
}
},
- "settings.profile.your-email" : {
- "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:83" ],
+ "settings.password-change-title" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:76" ],
"translations" : {
- "en" : null,
- "fr" : null
+ "en" : "Change password",
+ "fr" : "Changement de mot de passe"
}
},
- "settings.profile.your-name" : {
- "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:71" ],
+ "settings.profile" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:42" ],
"translations" : {
- "en" : "Your name",
- "fr" : "Votre nom complet"
+ "en" : "PROFILE",
+ "fr" : "PROFIL"
}
},
- "settings.update-settings" : {
- "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:42", "src/uxbox/main/ui/settings/password.cljs:102", "src/uxbox/main/ui/settings/profile.cljs:109" ],
+ "settings.profile-submit-label" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/options.cljs:65", "src/uxbox/main/ui/settings/password.cljs:94", "src/uxbox/main/ui/settings/profile.cljs:92" ],
"translations" : {
"en" : "Update settings",
"fr" : "Mettre à jour les paramètres"
}
},
+ "settings.remove-account-label" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:97" ],
+ "translations" : {
+ "en" : "Want to remove your account?"
+ }
+ },
+ "settings.teams" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:57" ],
+ "translations" : {
+ "en" : "TEAMS"
+ }
+ },
+ "settings.theme-change-title" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/options.cljs:58" ],
+ "translations" : {
+ "en" : "UI theme"
+ }
+ },
+ "settings.theme-label" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/options.cljs:59" ],
+ "translations" : {
+ "en" : "Select theme"
+ }
+ },
+ "settings.update-photo-label" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:119" ],
+ "translations" : {
+ "en" : "UPDATE"
+ }
+ },
+ "settings.update-settings" : {
+ "translations" : {
+ "en" : null,
+ "fr" : null
+ },
+ "unused" : true
+ },
+ "settings.verification-sent-title" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:81" ],
+ "translations" : {
+ "en" : "Verification email sent"
+ }
+ },
+ "settings.yes-delete-my-account" : {
+ "used-in" : [ "src/uxbox/main/ui/settings/delete_account.cljs:39" ],
+ "translations" : {
+ "en" : "Yes, delete my account"
+ }
+ },
"viewer.empty-state" : {
"used-in" : [ "src/uxbox/main/ui/viewer.cljs:44" ],
"translations" : {
@@ -822,49 +1025,49 @@
}
},
"workspace.header.menu.disable-dynamic-alignment" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:113" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:119" ],
"translations" : {
"en" : "Disable dynamic alignment"
}
},
"workspace.header.menu.disable-snap-grid" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:89" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:91" ],
"translations" : {
"en" : "Disable snap to grid"
}
},
"workspace.header.menu.enable-dynamic-alignment" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:114" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:120" ],
"translations" : {
"en" : "Enable dynamic aligment"
}
},
"workspace.header.menu.enable-snap-grid" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:90" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:92" ],
"translations" : {
"en" : "Snap to grid"
}
},
"workspace.header.menu.hide-grid" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:83" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:84" ],
"translations" : {
"en" : "Hide grid"
}
},
"workspace.header.menu.hide-layers" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:95" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:98" ],
"translations" : {
"en" : "Hide layers"
}
},
"workspace.header.menu.hide-libraries" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:107" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:112" ],
"translations" : {
"en" : "Hide libraries"
}
},
"workspace.header.menu.hide-palette" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:101" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:105" ],
"translations" : {
"en" : "Hide color palette"
}
@@ -876,25 +1079,25 @@
}
},
"workspace.header.menu.show-grid" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:84" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:85" ],
"translations" : {
"en" : "Show grid"
}
},
"workspace.header.menu.show-layers" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:96" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:99" ],
"translations" : {
"en" : "Show layers"
}
},
"workspace.header.menu.show-libraries" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:108" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:113" ],
"translations" : {
"en" : "Show libraries"
}
},
"workspace.header.menu.show-palette" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:102" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:106" ],
"translations" : {
"en" : "Show color palette"
}
@@ -906,7 +1109,7 @@
}
},
"workspace.header.viewer" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:151" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:158" ],
"translations" : {
"en" : "View mode (Ctrl + P)",
"fr" : "Mode visualisation (Ctrl + P)"
@@ -1111,121 +1314,121 @@
}
},
"workspace.options.grid.column" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:132" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:134" ],
"translations" : {
"en" : "Columns"
}
},
"workspace.options.grid.params.columns" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:171" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:175" ],
"translations" : {
"en" : "Columns"
}
},
"workspace.options.grid.params.gutter" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:202" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:208" ],
"translations" : {
"en" : "Gutter"
}
},
"workspace.options.grid.params.height" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:194" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:199" ],
"translations" : {
"en" : "Height"
}
},
"workspace.options.grid.params.margin" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:207" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:214" ],
"translations" : {
"en" : "Margin"
}
},
"workspace.options.grid.params.rows" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:163" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:166" ],
"translations" : {
"en" : "Rows"
}
},
"workspace.options.grid.params.set-default" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:219" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:227" ],
"translations" : {
"en" : "Set as default"
}
},
"workspace.options.grid.params.size" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:156" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:159" ],
"translations" : {
"en" : "Size"
}
},
"workspace.options.grid.params.type" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:179" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:184" ],
"translations" : {
"en" : "Type"
}
},
"workspace.options.grid.params.type.bottom" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:187" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:192" ],
"translations" : {
"en" : "Bottom"
}
},
"workspace.options.grid.params.type.center" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:185" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:190" ],
"translations" : {
"en" : "Center"
}
},
"workspace.options.grid.params.type.left" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:184" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:189" ],
"translations" : {
"en" : "Left"
}
},
"workspace.options.grid.params.type.right" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:188" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:193" ],
"translations" : {
"en" : "Right"
}
},
"workspace.options.grid.params.type.stretch" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:181" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:186" ],
"translations" : {
"en" : "Stretch"
}
},
"workspace.options.grid.params.type.top" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:183" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:188" ],
"translations" : {
"en" : "Top"
}
},
"workspace.options.grid.params.use-default" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:217" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:225" ],
"translations" : {
"en" : "Use default"
}
},
"workspace.options.grid.params.width" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:195" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:200" ],
"translations" : {
"en" : "Width"
}
},
"workspace.options.grid.row" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:133" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:135" ],
"translations" : {
"en" : "Rows"
}
},
"workspace.options.grid.square" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:131" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:133" ],
"translations" : {
"en" : "Square"
}
},
"workspace.options.grid.title" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:231" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:239" ],
"translations" : {
"en" : "Grid & Layouts"
}
@@ -1514,7 +1717,7 @@
}
},
"workspace.viewport.click-to-close-path" : {
- "used-in" : [ "src/uxbox/main/ui/workspace/drawarea.cljs:357" ],
+ "used-in" : [ "src/uxbox/main/ui/workspace/drawarea.cljs:363" ],
"translations" : {
"en" : "Click to close the path"
}
diff --git a/frontend/resources/styles/common/base.scss b/frontend/resources/styles/common/base.scss
index e42defc32..1d7d21886 100644
--- a/frontend/resources/styles/common/base.scss
+++ b/frontend/resources/styles/common/base.scss
@@ -55,10 +55,10 @@ svg {
a {
cursor: pointer;
- color: $color-primary;
+ color: $color-primary-dark;
&:hover {
- color: $color-primary-dark;
+ color: $color-primary;
}
}
diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss
index 245db4e8d..e3409d409 100644
--- a/frontend/resources/styles/main-default.scss
+++ b/frontend/resources/styles/main-default.scss
@@ -41,39 +41,41 @@
// 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/workspace';
-@import 'main/partials/workspace-header';
-@import 'main/partials/workspace-libraries';
-@import 'main/partials/tool-bar';
+@import 'main/partials/modal';
@import 'main/partials/project-bar';
@import 'main/partials/sidebar';
-@import 'main/partials/sidebar-tools';
@import 'main/partials/sidebar-align-options';
+@import 'main/partials/sidebar-document-history';
@import 'main/partials/sidebar-element-options';
@import 'main/partials/sidebar-icons';
@import 'main/partials/sidebar-interactions';
@import 'main/partials/sidebar-layers';
@import 'main/partials/sidebar-sitemap';
-@import 'main/partials/sidebar-document-history';
-@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/sidebar-tools';
@import 'main/partials/tab-container';
-@import "main/partials/zoom-widget";
-@import "main/partials/viewer-header";
-@import "main/partials/viewer-thumbnails";
-@import "main/partials/viewer";
-@import "main/partials/messages";
-@import "main/partials/texts";
+@import 'main/partials/tool-bar';
+@import 'main/partials/user-settings';
+@import 'main/partials/workspace';
+@import 'main/partials/workspace-header';
+@import 'main/partials/workspace-libraries';
diff --git a/frontend/resources/styles/main/layouts/login.scss b/frontend/resources/styles/main/layouts/login.scss
index b5e4a5d4e..aaf9fa39d 100644
--- a/frontend/resources/styles/main/layouts/login.scss
+++ b/frontend/resources/styles/main/layouts/login.scss
@@ -5,115 +5,51 @@
// Copyright (c) 2015-2020 Andrey Antukh
// Copyright (c) 2015-2020 Juan de la Cruz
-.login {
- align-items: center;
- background-color: $color-gray-40;
- background-image: url("/images/login-bg.jpg");
- background-position: center;
- background-repeat: no-repeat;
- background-size: cover;
- display: flex;
+// TODO: rename to auth.scss
+
+.auth {
+ display: grid;
+ grid-template-rows: auto;
+ grid-template-columns: 388px auto;
+}
+
+.auth-sidebar {
+ grid-column: 1 / span 1;
height: 100vh;
- justify-content: center;
- position: relative;
- width: 100%;
- .login-body {
- align-items: center;
- display: flex;
- flex-direction: column;
-
- svg {
- 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;
- }
-
- }
+ display: flex;
+ padding-top: 100px;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ background-color:#2C233E;
+ .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;
+ }
}
diff --git a/frontend/resources/styles/main/layouts/main-layout.scss b/frontend/resources/styles/main/layouts/main-layout.scss
index b15a9bd4e..b9b62ef03 100644
--- a/frontend/resources/styles/main/layouts/main-layout.scss
+++ b/frontend/resources/styles/main/layouts/main-layout.scss
@@ -31,3 +31,15 @@
.dashboard-content {
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;
+ }
+}
+
diff --git a/frontend/resources/styles/main/partials/forms.scss b/frontend/resources/styles/main/partials/forms.scss
index b5e1a04e6..9ae5d45fc 100644
--- a/frontend/resources/styles/main/partials/forms.scss
+++ b/frontend/resources/styles/main/partials/forms.scss
@@ -15,3 +15,265 @@ textarea {
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;
+ }
+ }
+}
diff --git a/frontend/resources/styles/main/partials/login.scss b/frontend/resources/styles/main/partials/login.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss
new file mode 100644
index 000000000..9ed66e6a0
--- /dev/null
+++ b/frontend/resources/styles/main/partials/modal.scss
@@ -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;
+ }
+
+ }
+}
diff --git a/frontend/resources/styles/main/partials/user-settings.scss b/frontend/resources/styles/main/partials/user-settings.scss
index f31b47a73..c06d0fc0b 100644
--- a/frontend/resources/styles/main/partials/user-settings.scss
+++ b/frontend/resources/styles/main/partials/user-settings.scss
@@ -1,70 +1,164 @@
.settings-content {
- .main-logo {
- position: fixed;
- left: 0;
- top:0;
- width: 40px;
- height: 40px;
- z-index: 12;
- cursor: pointer;
- }
-
- nav {
+ header {
display: flex;
- left: 0;
- width: 100%;
- justify-content: center;
+ flex-direction: column;
+ height: 160px;
+ background-color: $color-white;
- .nav-item {
- margin: 0 $size-6;
- color: $color-gray-30;
- text-transform: uppercase;
- border-bottom: 1px solid transparent;
+ .secondary-menu {
+ display: flex;
+ justify-content: space-between;
+ height: 40px;
+ font-size: $fs14;
+ color: $color-gray-60;
- &:hover {
- color: $color-black;
+ .icon {
+ display: flex;
+ align-items: center;
}
- &.current {
- color: $color-black;
- border-bottom: 1px solid $color-primary;
+ .left {
+ margin-left: 30px;
+ 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 {
- color: $color-black;
- font-size: $fs15;
- 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;
+ .settings-profile {
+ .forms-container {
+ margin-top: 80px;
+ }
}
.avatar-form {
- align-items: center;
+ flex-basis: 168px;
+ height: 100vh;
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 {
border-radius: 50%;
flex-shrink: 0;
@@ -72,7 +166,18 @@
margin-right: $medium;
width: 120px;
}
+ }
+ .options-form,
+ .password-form {
+ display: flex;
+ flex-direction: column;
+ flex-basis: 368px;
+
+ h2 {
+ font-size: $fs14;
+ font-weight: normal;
+ margin-bottom: $medium;
+ }
}
}
-
diff --git a/frontend/src/uxbox/main.cljs b/frontend/src/uxbox/main.cljs
index 4eccb6d62..c7107ec4a 100644
--- a/frontend/src/uxbox/main.cljs
+++ b/frontend/src/uxbox/main.cljs
@@ -41,7 +41,7 @@
(and (or (= path "")
(nil? match))
(not authed?))
- (st/emit! (rt/nav :login))
+ (st/emit! (rt/nav :auth-login))
(and (nil? match) authed?)
(st/emit! (rt/nav :dashboard-team {:team-id (:default-team-id profile)}))
diff --git a/frontend/src/uxbox/main/data/auth.cljs b/frontend/src/uxbox/main/data/auth.cljs
index a16d62c56..f31af56c9 100644
--- a/frontend/src/uxbox/main/data/auth.cljs
+++ b/frontend/src/uxbox/main/data/auth.cljs
@@ -51,14 +51,18 @@
ptk/WatchEvent
(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
- :scope "webapp"}
- on-error #(rx/of (dm/error (tr "errors.auth.unauthorized")))]
+ :scope "webapp"}]
(->> (rp/mutation :login params)
- (rx/map logged-in)
- (rx/catch rp/client-error? on-error))))))
-
+ (rx/tap on-success)
+ (rx/catch (fn [err]
+ (on-error err)
+ (rx/empty)))
+ (rx/map logged-in))))))
;; --- Logout
(def clear-user-data
@@ -81,8 +85,8 @@
(ptk/reify ::logout
ptk/WatchEvent
(watch [_ state stream]
- (rx/of (rt/nav :login)
- clear-user-data))))
+ (rx/of clear-user-data
+ (rt/nav :auth-login)))))
;; --- Register
@@ -93,18 +97,37 @@
(defn register
"Create a register event instance."
- [data on-error]
+ [data]
(s/assert ::register data)
- (s/assert fn? on-error)
(ptk/reify ::register
ptk/WatchEvent
(watch [_ state stream]
- (letfn [(handle-error [{payload :payload}]
- (on-error payload)
- (rx/empty))]
+ (let [{:keys [on-error on-success]
+ :or {on-error identity
+ on-success identity}} (meta data)]
(->> (rp/mutation :register-profile data)
- (rx/map (fn [_] (login data)))
- (rx/catch rp/client-error? handle-error))))))
+ (rx/tap on-success)
+ (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
@@ -112,38 +135,43 @@
(s/keys :req-un [::email]))
(defn request-profile-recovery
- [data on-success]
+ [data]
(us/verify ::recovery-request data)
- (us/verify fn? on-success)
(ptk/reify ::request-profile-recovery
ptk/WatchEvent
(watch [_ state stream]
- (letfn [(on-error [{payload :payload}]
- (rx/empty))]
+ (let [{:keys [on-error on-success]
+ :or {on-error identity
+ on-success identity}} (meta data)]
+
(->> (rp/mutation :request-profile-recovery data)
(rx/tap on-success)
- (rx/catch rp/client-error? on-error))))))
+ (rx/catch (fn [err]
+ (on-error err)
+ (rx/empty))))))))
+
;; --- Recovery (Password)
(s/def ::token string?)
-(s/def ::on-error fn?)
-(s/def ::on-success fn?)
-
(s/def ::recover-profile
- (s/keys :req-un [::password ::token ::on-error ::on-success]))
+ (s/keys :req-un [::password ::token]))
(defn recover-profile
- [{:keys [token password on-error on-success] :as data}]
+ [{:keys [token password] :as data}]
(us/verify ::recover-profile data)
(ptk/reify ::recover-profile
ptk/WatchEvent
(watch [_ state stream]
- (->> (rp/mutation :recover-profile {:token token :password password})
- (rx/tap on-success)
- (rx/catch (fn [err]
- (on-error)
- (rx/empty)))))))
+ (let [{:keys [on-error on-success]
+ :or {on-error identity
+ on-success identity}} (meta data)]
+ (->> (rp/mutation :recover-profile data)
+ (rx/tap on-success)
+ (rx/catch (fn [err]
+ (on-error)
+ (rx/empty))))))))
+
;; --- Create Demo Profile
diff --git a/frontend/src/uxbox/main/data/dashboard.cljs b/frontend/src/uxbox/main/data/dashboard.cljs
index 0b366e9e3..c15148679 100644
--- a/frontend/src/uxbox/main/data/dashboard.cljs
+++ b/frontend/src/uxbox/main/data/dashboard.cljs
@@ -143,7 +143,7 @@
(->> (rp/query :projects-by-team {:team-id team-id})
(rx/map projects-fetched)
(rx/catch (fn [error]
- (rx/of (rt/nav' :not-authorized))))))))
+ (rx/of (rt/nav' :auth-login))))))))
(defn projects-fetched
[projects]
@@ -212,7 +212,7 @@
(->> (rp/query :recent-files params)
(rx/map recent-files-fetched)
(rx/catch (fn [e]
- (rx/of (rt/nav' :not-authorized)))))))))
+ (rx/of (rt/nav' :auth-login)))))))))
(defn recent-files-fetched
[recent-files]
diff --git a/frontend/src/uxbox/main/data/messages.cljs b/frontend/src/uxbox/main/data/messages.cljs
index fb5738206..2b2f50c9d 100644
--- a/frontend/src/uxbox/main/data/messages.cljs
+++ b/frontend/src/uxbox/main/data/messages.cljs
@@ -61,3 +61,9 @@
(show {:content message
:type :info
:timeout timeout}))
+
+(defn success
+ [message & {:keys [timeout] :or {timeout 3000}}]
+ (show {:content message
+ :type :info
+ :timeout timeout}))
diff --git a/frontend/src/uxbox/main/data/users.cljs b/frontend/src/uxbox/main/data/users.cljs
index 32b0a509e..836717539 100644
--- a/frontend/src/uxbox/main/data/users.cljs
+++ b/frontend/src/uxbox/main/data/users.cljs
@@ -13,6 +13,7 @@
[uxbox.common.spec :as us]
[uxbox.config :as cfg]
[uxbox.main.repo :as rp]
+ [uxbox.util.router :as rt]
[uxbox.util.i18n :as i18n :refer [tr]]
[uxbox.util.storage :refer [storage]]
[uxbox.util.avatars :as avatars]
@@ -74,7 +75,11 @@
ptk/WatchEvent
(watch [_ state s]
(->> (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
@@ -91,9 +96,35 @@
(rx/empty))]
(->> (rp/mutation :update-profile data)
(rx/do on-success)
- (rx/map profile-fetched)
+ (rx/map (constantly fetch-profile))
(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)
(s/def ::update-password
@@ -107,15 +138,16 @@
(ptk/reify ::update-password
ptk/WatchEvent
(watch [_ state s]
- (let [mdata (meta data)
- on-success (:on-success mdata identity)
- on-error (:on-error mdata identity)
+ (let [{:keys [on-error on-success]
+ :or {on-error identity
+ on-success identity}} (meta data)
params {:old-password (:password-old data)
:password (:password-1 data)}]
(->> (rp/mutation :update-profile-password params)
- (rx/catch rp/client-error? #(do (on-error (:payload %))
- (rx/empty)))
- (rx/do on-success)
+ (rx/tap on-success)
+ (rx/catch (fn [err]
+ (on-error err)
+ (rx/empty)))
(rx/ignore))))))
diff --git a/frontend/src/uxbox/main/repo.cljs b/frontend/src/uxbox/main/repo.cljs
index e6702eef2..f7974b249 100644
--- a/frontend/src/uxbox/main/repo.cljs
+++ b/frontend/src/uxbox/main/repo.cljs
@@ -95,7 +95,7 @@
(defmethod mutation :logout
[id params]
(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))))
(def client-error? http/client-error?)
diff --git a/frontend/src/uxbox/main/ui.cljs b/frontend/src/uxbox/main/ui.cljs
index 61375c4d5..95d371313 100644
--- a/frontend/src/uxbox/main/ui.cljs
+++ b/frontend/src/uxbox/main/ui.cljs
@@ -21,11 +21,8 @@
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[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.profile.recovery :refer [profile-recovery-page]]
- [uxbox.main.ui.profile.recovery-request :refer [profile-recovery-request-page]]
- [uxbox.main.ui.profile.register :refer [profile-register-page]]
+ [uxbox.main.ui.auth :refer [auth verify-token]]
[uxbox.main.ui.settings :as settings]
[uxbox.main.ui.viewer :refer [viewer-page]]
[uxbox.main.ui.workspace :as workspace]
@@ -35,14 +32,18 @@
;; --- Routes
(def routes
- [["/login" :login]
- ["/register" :profile-register]
- ["/recovery/request" :profile-recovery-request]
- ["/recovery" :profile-recovery]
+ [["/auth"
+ ["/login" :auth-login]
+ ["/register" :auth-register]
+ ["/recovery/request" :auth-recovery-request]
+ ["/recovery" :auth-recovery]
+ ["/verify-token" :auth-verify-token]
+ ["/goodbye" :auth-goodbye]]
["/settings"
["/profile" :settings-profile]
- ["/password" :settings-password]]
+ ["/password" :settings-password]
+ ["/options" :settings-options]]
["/view/:page-id" :viewer]
["/not-found" :not-found]
@@ -84,20 +85,20 @@
{::mf/wrap [#(mf/catch % {:fallback app-error})]}
[{:keys [route] :as props}]
(case (get-in route [:data :name])
- :login
- [:& login-page]
- :profile-register
- [:& profile-register-page]
+ (:auth-login
+ :auth-register
+ :auth-goodbye
+ :auth-recovery-request
+ :auth-recovery)
+ [:& auth {:route route}]
- :profile-recovery-request
- [:& profile-recovery-request-page]
-
- :profile-recovery
- [:& profile-recovery-page]
+ :auth-verify-token
+ [:& verify-token {:route route}]
(:settings-profile
- :settings-password)
+ :settings-password
+ :settings-options)
[:& settings/settings {:route route}]
:debug-icons-preview
diff --git a/frontend/src/uxbox/main/ui/auth.cljs b/frontend/src/uxbox/main/ui/auth.cljs
new file mode 100644
index 000000000..0a5748349
--- /dev/null
+++ b/frontend/src/uxbox/main/ui/auth.cljs
@@ -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]))
diff --git a/frontend/src/uxbox/main/ui/auth/login.cljs b/frontend/src/uxbox/main/ui/auth/login.cljs
new file mode 100644
index 000000000..30001a13b
--- /dev/null
+++ b/frontend/src/uxbox/main/ui/auth/login.cljs
@@ -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")]]]]])
diff --git a/frontend/src/uxbox/main/ui/auth/recovery.cljs b/frontend/src/uxbox/main/ui/auth/recovery.cljs
new file mode 100644
index 000000000..a021e5e8c
--- /dev/null
+++ b/frontend/src/uxbox/main/ui/auth/recovery.cljs
@@ -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")]]]]])
+
diff --git a/frontend/src/uxbox/main/ui/auth/recovery_request.cljs b/frontend/src/uxbox/main/ui/auth/recovery_request.cljs
new file mode 100644
index 000000000..f7b8b0803
--- /dev/null
+++ b/frontend/src/uxbox/main/ui/auth/recovery_request.cljs
@@ -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")]]]]])
diff --git a/frontend/src/uxbox/main/ui/auth/register.cljs b/frontend/src/uxbox/main/ui/auth/register.cljs
new file mode 100644
index 000000000..d51650314
--- /dev/null
+++ b/frontend/src/uxbox/main/ui/auth/register.cljs
@@ -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")]]]]])
diff --git a/frontend/src/uxbox/main/ui/components/forms.cljs b/frontend/src/uxbox/main/ui/components/forms.cljs
new file mode 100644
index 000000000..c00d8668f
--- /dev/null
+++ b/frontend/src/uxbox/main/ui/components/forms.cljs
@@ -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]]))
+
+
+
diff --git a/frontend/src/uxbox/main/ui/icons.cljs b/frontend/src/uxbox/main/ui/icons.cljs
index 85f11ea14..ca1e58fda 100644
--- a/frontend/src/uxbox/main/ui/icons.cljs
+++ b/frontend/src/uxbox/main/ui/icons.cljs
@@ -22,6 +22,7 @@
(def arrow-end (icon-xref :arrow-end))
(def arrow-slide (icon-xref :arrow-slide))
(def artboard (icon-xref :artboard))
+(def at (icon-xref :at))
(def auto-fix (icon-xref :auto-fix))
(def auto-height (icon-xref :auto-height))
(def auto-width (icon-xref :auto-width))
@@ -60,6 +61,7 @@
(def lock (icon-xref :lock))
(def lock-open (icon-xref :lock-open))
(def logo (icon-xref :uxbox-logo))
+(def logout (icon-xref :logout))
(def logo-icon (icon-xref :uxbox-logo-icon))
(def lowercase (icon-xref :lowercase))
(def mail (icon-xref :mail))
diff --git a/frontend/src/uxbox/main/ui/login.cljs b/frontend/src/uxbox/main/ui/login.cljs
deleted file mode 100644
index d1743e4c4..000000000
--- a/frontend/src/uxbox/main/ui/login.cljs
+++ /dev/null
@@ -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
-;; Copyright (c) 2015-2020 Juan de la Cruz
-
-(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]]])
diff --git a/frontend/src/uxbox/main/ui/modal.cljs b/frontend/src/uxbox/main/ui/modal.cljs
index 369df3aa4..cecb9b61b 100644
--- a/frontend/src/uxbox/main/ui/modal.cljs
+++ b/frontend/src/uxbox/main/ui/modal.cljs
@@ -27,13 +27,12 @@
(defn- on-parent-clicked
[event parent-ref]
- (dom/stop-propagation event)
- (dom/prevent-default event)
(let [parent (mf/ref-val parent-ref)
current (dom/get-target event)]
(when (dom/equals? parent current)
- (reset! state nil)
- #_(st/emit! (udl/hide-lightbox)))))
+ (dom/stop-propagation event)
+ (dom/prevent-default event)
+ (reset! state nil))))
(mf/defc modal-wrapper
[{:keys [component props]}]
@@ -46,7 +45,8 @@
parent-ref (mf/use-ref nil)]
[:div.lightbox {:class classes
:ref parent-ref
- :on-click #(on-parent-clicked % parent-ref)}
+ :on-click #(on-parent-clicked % parent-ref)
+ }
(mf/element component props)]))
(mf/defc modal
diff --git a/frontend/src/uxbox/main/ui/profile/recovery.cljs b/frontend/src/uxbox/main/ui/profile/recovery.cljs
deleted file mode 100644
index 9b4ca3c4b..000000000
--- a/frontend/src/uxbox/main/ui/profile/recovery.cljs
+++ /dev/null
@@ -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
-;; Copyright (c) 2015-2020 Juan de la Cruz
-
-(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]]])
diff --git a/frontend/src/uxbox/main/ui/profile/recovery_request.cljs b/frontend/src/uxbox/main/ui/profile/recovery_request.cljs
deleted file mode 100644
index 17abdf54c..000000000
--- a/frontend/src/uxbox/main/ui/profile/recovery_request.cljs
+++ /dev/null
@@ -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
-;; Copyright (c) 2015-2020 Juan de la Cruz
-
-(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]]])
diff --git a/frontend/src/uxbox/main/ui/profile/register.cljs b/frontend/src/uxbox/main/ui/profile/register.cljs
deleted file mode 100644
index 1e7fd0185..000000000
--- a/frontend/src/uxbox/main/ui/profile/register.cljs
+++ /dev/null
@@ -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
-;; Copyright (c) 2015-2017 Juan de la Cruz
-
-(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]]])
diff --git a/frontend/src/uxbox/main/ui/settings.cljs b/frontend/src/uxbox/main/ui/settings.cljs
index 6cb275bcb..3d9bcf99e 100644
--- a/frontend/src/uxbox/main/ui/settings.cljs
+++ b/frontend/src/uxbox/main/ui/settings.cljs
@@ -2,8 +2,10 @@
;; 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-2016 Andrey Antukh
-;; Copyright (c) 2015-2016 Juan de la Cruz
+;; 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
(:require
@@ -18,6 +20,7 @@
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.settings.header :refer [header]]
[uxbox.main.ui.settings.password :refer [password-page]]
+ [uxbox.main.ui.settings.options :refer [options-page]]
[uxbox.main.ui.settings.profile :refer [profile-page]]))
(mf/defc settings
@@ -30,7 +33,8 @@
[:& header {:section section :profile profile}]
(case section
:settings-profile (mf/element profile-page)
- :settings-password (mf/element password-page))]]))
+ :settings-password (mf/element password-page)
+ :settings-options (mf/element options-page))]]))
diff --git a/frontend/src/uxbox/main/ui/settings/change_email.cljs b/frontend/src/uxbox/main/ui/settings/change_email.cljs
new file mode 100644
index 000000000..8a433abad
--- /dev/null
+++ b/frontend/src/uxbox/main/ui/settings/change_email.cljs
@@ -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 "We’ll 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}])]))
diff --git a/frontend/src/uxbox/main/ui/settings/delete_account.cljs b/frontend/src/uxbox/main/ui/settings/delete_account.cljs
new file mode 100644
index 000000000..4ebfafd11
--- /dev/null
+++ b/frontend/src/uxbox/main/ui/settings/delete_account.cljs
@@ -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")]]]]))
diff --git a/frontend/src/uxbox/main/ui/settings/header.cljs b/frontend/src/uxbox/main/ui/settings/header.cljs
index e279d8e74..6a6565349 100644
--- a/frontend/src/uxbox/main/ui/settings/header.cljs
+++ b/frontend/src/uxbox/main/ui/settings/header.cljs
@@ -16,22 +16,60 @@
(mf/defc header
[{:keys [section profile] :as props}]
- (let [profile? (= section :settings-profile)
+ (let [profile? (= section :settings-profile)
password? (= section :settings-password)
- locale (i18n/use-locale)
- team-id (:default-team-id profile)]
- [:header
- [: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")]]]]))
+ options? (= section :settings-options)
+
+ team-id (:default-team-id profile)
+ go-back #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))
+ logout #(st/emit! da/logout)
+
+ locale (mf/deref i18n/locale)
+ team-id (:default-team-id profile)]
+ [:header
+ [:section.secondary-menu
+ [:div.left {:on-click go-back}
+ [:span.icon i/arrow-slide]
+ [:span.label "Dashboard"]]
+ [:div.right {:on-click logout}
+ [:span.label "Log out"]
+ [: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")]]]]))
diff --git a/frontend/src/uxbox/main/ui/settings/notifications.cljs b/frontend/src/uxbox/main/ui/settings/notifications.cljs
deleted file mode 100644
index 3662c836f..000000000
--- a/frontend/src/uxbox/main/ui/settings/notifications.cljs
+++ /dev/null
@@ -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
-;; Copyright (c) 2016 Juan de la Cruz
-
-(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")}]
- ]])
diff --git a/frontend/src/uxbox/main/ui/settings/options.cljs b/frontend/src/uxbox/main/ui/settings/options.cljs
new file mode 100644
index 000000000..bf10f7d04
--- /dev/null
+++ b/frontend/src/uxbox/main/ui/settings/options.cljs
@@ -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}]]]))
diff --git a/frontend/src/uxbox/main/ui/settings/password.cljs b/frontend/src/uxbox/main/ui/settings/password.cljs
index f0ee7f357..fa3ac3b66 100644
--- a/frontend/src/uxbox/main/ui/settings/password.cljs
+++ b/frontend/src/uxbox/main/ui/settings/password.cljs
@@ -5,8 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
-;; Copyright (c) 2016-2020 Andrey Antukh
-;; Copyright (c) 2016-2020 Juan de la Cruz
+;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.password
(:require
@@ -15,40 +14,52 @@
[uxbox.main.ui.icons :as i]
[uxbox.main.data.users :as udu]
[uxbox.main.data.messages :as dm]
+ [uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.store :as st]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
- [uxbox.util.i18n :refer [tr]]))
+ [uxbox.util.i18n :as i18n :refer [t tr]]))
(defn- on-error
[form 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]
- {: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
- [event form]
+ [form event]
(dom/prevent-default event)
- (let [data (:clean-data form)
- mdata {:on-success #(st/emit! (dm/info (tr "settings.password.password-saved")))
- :on-error #(on-error form %)}]
- (st/emit! (udu/update-password (with-meta data mdata)))))
+ (let [params (with-meta (:clean-data form)
+ {:on-success (partial on-success form)
+ :on-error (partial on-error form)})]
+ (st/emit! (udu/update-password params))))
(s/def ::password-1 ::fm/not-empty-string)
(s/def ::password-2 ::fm/not-empty-string)
(s/def ::password-old ::fm/not-empty-string)
-(defn password-equality
+(defn- password-equality
[data]
(let [password-1 (:password-1 data)
password-2 (:password-2 data)]
- (when (and password-1 password-2
- (not= password-1 password-2))
- {:password-2 {:code ::password-not-equal
- :message "profile.password.not-equal"}})))
+
+ (cond-> {}
+ (and password-1 password-2
+ (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/keys :req-un [::password-1
@@ -56,54 +67,37 @@
::password-old]))
(mf/defc password-form
- [props]
- (let [{:keys [data] :as form} (fm/use-form2 :spec ::password-form
- :validators [password-equality]
- :initial {})]
- [:form.password-form {:on-submit #(on-submit % form)}
- [:span.settings-label (tr "settings.password.change-password")]
- [:input.input-text
- {: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")}]
+ [{:keys [locale] :as props}]
+ [:& form {:class "password-form"
+ :on-submit on-submit
+ :spec ::password-form
+ :validators [password-equality]
+ :initial {}}
+ [:h2 (t locale "settings.password-change-title")]
- [:& 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
- {:type "password"
- :name "password-1"
- :value (:password-1 data "")
- :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")}]
+ [:& input
+ {:type "password"
+ :name :password-1
+ :label (t locale "settings.new-password-label")}]
- [:& fm/field-error {:form form :field :password-1}]
+ [:& input
+ {:type "password"
+ :name :password-2
+ :label (t locale "settings.confirm-password-label")}]
- [:input.input-text
- {:type "password"
- :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")}]]))
+ [:& submit-button
+ {:label (t locale "settings.profile-submit-label")}]])
;; --- Password Page
(mf/defc password-page
[props]
- [:section.settings-password
- [:& password-form]])
+ (let [locale (mf/deref i18n/locale)]
+ [:section.settings-password.generic-form
+ [:div.forms-container
+ [:& password-form {:locale locale}]]]))
diff --git a/frontend/src/uxbox/main/ui/settings/profile.cljs b/frontend/src/uxbox/main/ui/settings/profile.cljs
index 583319af1..146c4a646 100644
--- a/frontend/src/uxbox/main/ui/settings/profile.cljs
+++ b/frontend/src/uxbox/main/ui/settings/profile.cljs
@@ -2,8 +2,10 @@
;; 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-2017 Andrey Antukh
-;; Copyright (c) 2016-2017 Juan de la Cruz
+;; 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.profile
(:require
@@ -13,6 +15,10 @@
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[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.store :as st]
[uxbox.main.refs :as refs]
@@ -21,8 +27,6 @@
[uxbox.util.i18n :as i18n :refer [tr t]]))
(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 ::profile-form
@@ -30,22 +34,12 @@
(defn- on-error
[error form]
- (case (:code error)
- :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"})))
+ (st/emit! (dm/error (tr "errors.generic"))))
(defn- on-submit
- [event form]
- (dom/prevent-default event)
+ [form event]
(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)]
(st/emit! (udu/update-profile (with-meta data
{:on-success on-success
@@ -54,64 +48,58 @@
;; --- Profile Form
(mf/defc profile-form
- [props]
- (let [locale (i18n/use-locale)
- form (fm/use-form ::profile-form #(deref refs/profile))
- data (:data form)]
- [:form.profile-form {:on-submit #(on-submit % form)}
- [:span.settings-label (t locale "settings.profile.section-basic-data")]
-
- [:input.input-text
+ [{:keys [locale] :as props}]
+ (let [prof (mf/deref refs/profile)]
+ [:& form {:on-submit on-submit
+ :class "profile-form"
+ :spec ::profile-form
+ :initial prof}
+ [:& input
{:type "text"
- :name "fullname"
- :class (fm/error-class form :fullname)
- :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}]
+ :name :fullname
+ :label (t locale "settings.fullname-label")}]
- [:input.input-text
+ [:& input
{:type "email"
- :name "email"
- :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 (t locale "settings.profile.your-email")}]
- [:& fm/field-error {:form form
- :type #{::api}
- :field :email}]
+ :name :email
+ :disabled true
+ :help-icon i/at
+ :label (t locale "settings.email-label")}]
- [:span.settings-label (t locale "settings.profile.lang")]
- [:select.input-select {:value (:lang data)
- :name "lang"
- :class (fm/error-class form :lang)
- :on-blur (fm/on-input-blur form :lang)
- :on-change (fm/on-input-change form :lang)}
- [:option {:value "en"} "English"]
- [:option {:value "fr"} "Français"]]
+ (cond
+ (nil? (:pending-email prof))
+ [:div.change-email
+ [:a {:on-click #(modal/show! change-email-modal {})}
+ (t locale "settings.change-email-label")]]
- [:span.user-settings-label (tr "settings.profile.section-theme-data")]
- [:select.input-select {:value (:theme data)
- :name "theme"
- :class (fm/error-class form :theme)
- :on-blur (fm/on-input-blur form :theme)
- :on-change (fm/on-input-change form :theme)}
- [:option {:value "light"} "Default"]]
+ (not= (:pending-email prof) (:email prof))
+ [:span.featured-note
+ [:span.icon i/trash]
+ [:span.text
+ [:span "There is a pending change of your email to "]
+ [:strong (:pending-email prof)]
+ [:span "."] [:br]
+ [:a {:on-click #(st/emit! udu/cancel-email-change)}
+ "Dismiss"]]]
- [:input.btn-primary.btn-large
- {:type "submit"
- :class (when-not (:valid form) "btn-disabled")
- :disabled (not (:valid form))
- :value (t locale "settings.update-settings")}]]))
+ :else
+ [:span.featured-note.warning
+ [:span.text
+ [:span "There is a pending email validation."]]])
+
+
+ [:& 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
(mf/defc profile-photo-form
- [props]
+ [{:keys [locale] :as props}]
(let [profile (mf/deref refs/profile)
photo (:photo-uri profile)
photo (if (or (str/empty? photo) (nil? photo))
@@ -127,10 +115,12 @@
(st/emit! (udu/update-photo {:file file}))
(dom/clean-value! target)))]
[:form.avatar-form
- [:img {:src photo}]
- [:input {:type "file"
- :value ""
- :on-change on-change}]]))
+ [:div.image-change-field
+ [:span.update-overlay (t locale "settings.update-photo-label")]
+ [:img {:src photo}]
+ [:input {:type "file"
+ :value ""
+ :on-change on-change}]]]))
;; --- Profile Page
@@ -138,7 +128,7 @@
{::mf/wrap-props false}
[props]
(let [locale (i18n/use-locale)]
- [:section.settings-profile
- [:span.settings-label (t locale "settings.profile.your-avatar")]
- [:& profile-photo-form]
- [:& profile-form]]))
+ [:section.settings-profile.generic-form
+ [:div.forms-container
+ [:& profile-photo-form {:locale locale}]
+ [:& profile-form {:locale locale}]]]))
diff --git a/frontend/src/uxbox/util/dom.cljs b/frontend/src/uxbox/util/dom.cljs
index 64c85d787..a0c882b07 100644
--- a/frontend/src/uxbox/util/dom.cljs
+++ b/frontend/src/uxbox/util/dom.cljs
@@ -35,7 +35,7 @@
[& params]
(assert (even? (count params)))
(str/join " " (reduce (fn [acc [k v]]
- (if (true? v)
+ (if (true? (boolean v))
(conj acc (name k))
acc))
[]
diff --git a/frontend/src/uxbox/util/forms.cljs b/frontend/src/uxbox/util/forms.cljs
index 7e77d11e1..5cf722758 100644
--- a/frontend/src/uxbox/util/forms.cljs
+++ b/frontend/src/uxbox/util/forms.cljs
@@ -50,44 +50,25 @@
:else acc))
(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]}]
(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)
+
+ cleaned (s/conform spec (:data state))
+ problems (when (= ::s/invalid cleaned)
(::s/problems (s/explain-data spec (:data state))))
- errors (merge (reduce interpret-problem {} problems)
- (when (not= clean-data ::s/invalid)
+ errors (merge (reduce interpret-problem {} problems)
(reduce (fn [errors vf]
- (merge errors (vf clean-data)))
- {} validators))
- (:errors state))]
+ (merge errors (vf (:data state))))
+ {} validators)
+ (:errors state))]
(-> (assoc state
:errors errors
- :clean-data (when (not= clean-data ::s/invalid) clean-data)
+ :clean-data (when (not= cleaned ::s/invalid) cleaned)
:valid (and (empty? errors)
- (not= clean-data ::s/invalid)))
+ (not= cleaned ::s/invalid)))
(impl-mutator update-state))))
(defn on-input-change
diff --git a/frontend/src/uxbox/util/object.cljs b/frontend/src/uxbox/util/object.cljs
index 61d6527c1..8e0f2a596 100644
--- a/frontend/src/uxbox/util/object.cljs
+++ b/frontend/src/uxbox/util/object.cljs
@@ -9,8 +9,11 @@
(ns uxbox.util.object
"A collection of helpers for work with javascript objects."
- (:refer-clojure :exclude [get get-in assoc!])
- (:require [goog.object :as gobj]))
+ (:refer-clojure :exclude [set! get get-in assoc!])
+ (:require
+ [cuerdas.core :as str]
+ [goog.object :as gobj]
+ ["lodash/omit" :as omit]))
(defn get
([obj k]
@@ -32,6 +35,14 @@
(rest keys)
(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!
([a b]
(js/Object.assign a b))
@@ -42,3 +53,13 @@
[obj key value]
(unchecked-set obj key value)
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))