From 7d5f9c1078813d51cc216a5950074d9992d85501 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 22 May 2020 13:48:21 +0200 Subject: [PATCH] :recycle: Initial profile and auth refactor. --- .../resources/emails/change-email/en.mustache | 19 + .../emails/password-recovery/en.mustache | 42 +- backend/resources/emails/register/en.mustache | 38 +- backend/resources/migrations/0002.users.sql | 36 +- ...ve_version.sql => 0007.remove-version.sql} | 0 .../migrations/0008.generic-token.sql | 6 + backend/src/uxbox/config.clj | 4 + backend/src/uxbox/emails.clj | 8 + backend/src/uxbox/http/handlers.clj | 20 +- backend/src/uxbox/migrations.clj | 7 +- .../src/uxbox/services/mutations/profile.clj | 286 +++++-- .../src/uxbox/services/queries/profile.clj | 5 +- backend/src/uxbox/util/template.clj | 3 +- backend/src/uxbox/util/time.clj | 5 + docs/02-Frontend-Developer-Guide.md | 25 +- frontend/deps.edn | 8 +- frontend/package-lock.json | 255 ++++-- frontend/package.json | 14 +- frontend/resources/images/icons/at.svg | 3 + frontend/resources/images/icons/logout.svg | 3 + frontend/resources/locales.json | 733 +++++++++++------- frontend/resources/styles/common/base.scss | 4 +- frontend/resources/styles/main-default.scss | 54 +- .../resources/styles/main/layouts/login.scss | 148 +--- .../styles/main/layouts/main-layout.scss | 12 + .../resources/styles/main/partials/forms.scss | 262 +++++++ .../resources/styles/main/partials/login.scss | 0 .../resources/styles/main/partials/modal.scss | 55 ++ .../styles/main/partials/user-settings.scss | 209 +++-- frontend/src/uxbox/main.cljs | 2 +- frontend/src/uxbox/main/data/auth.cljs | 88 ++- frontend/src/uxbox/main/data/dashboard.cljs | 4 +- frontend/src/uxbox/main/data/messages.cljs | 6 + frontend/src/uxbox/main/data/users.cljs | 48 +- frontend/src/uxbox/main/repo.cljs | 2 +- frontend/src/uxbox/main/ui.cljs | 39 +- frontend/src/uxbox/main/ui/auth.cljs | 93 +++ frontend/src/uxbox/main/ui/auth/login.cljs | 86 ++ frontend/src/uxbox/main/ui/auth/recovery.cljs | 98 +++ .../uxbox/main/ui/auth/recovery_request.cljs | 68 ++ frontend/src/uxbox/main/ui/auth/register.cljs | 120 +++ .../src/uxbox/main/ui/components/forms.cljs | 150 ++++ frontend/src/uxbox/main/ui/icons.cljs | 2 + frontend/src/uxbox/main/ui/login.cljs | 99 --- frontend/src/uxbox/main/ui/modal.cljs | 10 +- .../src/uxbox/main/ui/profile/recovery.cljs | 91 --- .../main/ui/profile/recovery_request.cljs | 75 -- .../src/uxbox/main/ui/profile/register.cljs | 120 --- frontend/src/uxbox/main/ui/settings.cljs | 10 +- .../uxbox/main/ui/settings/change_email.cljs | 102 +++ .../main/ui/settings/delete_account.cljs | 42 + .../src/uxbox/main/ui/settings/header.cljs | 72 +- .../uxbox/main/ui/settings/notifications.cljs | 43 - .../src/uxbox/main/ui/settings/options.cljs | 75 ++ .../src/uxbox/main/ui/settings/password.cljs | 110 ++- .../src/uxbox/main/ui/settings/profile.cljs | 136 ++-- frontend/src/uxbox/util/dom.cljs | 2 +- frontend/src/uxbox/util/forms.cljs | 37 +- frontend/src/uxbox/util/object.cljs | 25 +- 59 files changed, 2712 insertions(+), 1407 deletions(-) create mode 100644 backend/resources/emails/change-email/en.mustache rename backend/resources/migrations/{0007.remove_version.sql => 0007.remove-version.sql} (100%) create mode 100644 backend/resources/migrations/0008.generic-token.sql create mode 100644 frontend/resources/images/icons/at.svg create mode 100644 frontend/resources/images/icons/logout.svg create mode 100644 frontend/resources/styles/main/partials/login.scss create mode 100644 frontend/resources/styles/main/partials/modal.scss create mode 100644 frontend/src/uxbox/main/ui/auth.cljs create mode 100644 frontend/src/uxbox/main/ui/auth/login.cljs create mode 100644 frontend/src/uxbox/main/ui/auth/recovery.cljs create mode 100644 frontend/src/uxbox/main/ui/auth/recovery_request.cljs create mode 100644 frontend/src/uxbox/main/ui/auth/register.cljs create mode 100644 frontend/src/uxbox/main/ui/components/forms.cljs delete mode 100644 frontend/src/uxbox/main/ui/login.cljs delete mode 100644 frontend/src/uxbox/main/ui/profile/recovery.cljs delete mode 100644 frontend/src/uxbox/main/ui/profile/recovery_request.cljs delete mode 100644 frontend/src/uxbox/main/ui/profile/register.cljs create mode 100644 frontend/src/uxbox/main/ui/settings/change_email.cljs create mode 100644 frontend/src/uxbox/main/ui/settings/delete_account.cljs delete mode 100644 frontend/src/uxbox/main/ui/settings/notifications.cljs create mode 100644 frontend/src/uxbox/main/ui/settings/options.cljs 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))