🎉 Backport questions form integration.

Among other related that need to be ported.
This commit is contained in:
Andrey Antukh 2021-11-04 15:17:12 +01:00
parent a2d3616171
commit eb1bcfba83
45 changed files with 983 additions and 529 deletions

View file

@ -58,7 +58,9 @@
(assoc response :cookies {cookie-name {:path "/" (assoc response :cookies {cookie-name {:path "/"
:http-only true :http-only true
:value id :value id
:same-site (if cors? :none :strict) :same-site (cond (not secure?) :lax
cors? :none
:else :strict)
:secure secure?}}))) :secure secure?}})))
(defn- clear-cookies (defn- clear-cookies

View file

@ -36,7 +36,8 @@
:is-active true :is-active true
:deleted-at (dt/in-future cf/deletion-delay) :deleted-at (dt/in-future cf/deletion-delay)
:password password :password password
:props {:onboarding-viewed true}}] :props {}
}]
(when-not (contains? cf/flags :demo-users) (when-not (contains? cf/flags :demo-users)
(ex/raise :type :validation (ex/raise :type :validation

View file

@ -335,9 +335,9 @@
;; --- MUTATION: Logout ;; --- MUTATION: Logout
(s/def ::logout (s/def ::logout
(s/keys :req-un [::profile-id])) (s/keys :opt-un [::profile-id]))
(sv/defmethod ::logout (sv/defmethod ::logout {:auth false}
[{:keys [session] :as cfg} _] [{:keys [session] :as cfg} _]
(with-meta {} (with-meta {}
{:transform-response (:delete session)})) {:transform-response (:delete session)}))

View file

@ -104,24 +104,53 @@
;; --- Mutation: Leave Team ;; --- Mutation: Leave Team
(declare role->params)
(s/def ::reassign-to ::us/uuid)
(s/def ::leave-team (s/def ::leave-team
(s/keys :req-un [::profile-id ::id])) (s/keys :req-un [::profile-id ::id]
:opt-un [::reassign-to]))
(sv/defmethod ::leave-team (sv/defmethod ::leave-team
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] [{:keys [pool] :as cfg} {:keys [id profile-id reassign-to]}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [perms (teams/get-permissions conn profile-id id) (let [perms (teams/get-permissions conn profile-id id)
members (teams/retrieve-team-members conn id)] members (teams/retrieve-team-members conn id)]
(when (:is-owner perms) (cond
;; we can only proceed if there are more members in the team
;; besides the current profile
(<= (count members) 1)
(ex/raise :type :validation
:code :no-enough-members-for-leave
:context {:members (count members)})
;; if the `reassign-to` is filled and has a different value
;; than the current profile-id, we proceed to reassing the
;; owner role to profile identified by the `reassign-to`.
(and reassign-to (not= reassign-to profile-id))
(let [member (d/seek #(= reassign-to (:id %)) members)]
(when-not member
(ex/raise :type :not-found :code :member-does-not-exist))
;; unasign owner role to current profile
(db/update! conn :team-profile-rel
{:is-owner false}
{:team-id id
:profile-id profile-id})
;; assign owner role to new profile
(db/update! conn :team-profile-rel
(role->params :owner)
{:team-id id :profile-id reassign-to}))
;; and finally, if all other conditions does not match and the
;; current profile is owner, we dont allow it because there
;; must always be an owner.
(:is-owner perms)
(ex/raise :type :validation (ex/raise :type :validation
:code :owner-cant-leave-team :code :owner-cant-leave-team
:hint "reasing owner before leave")) :hint "releasing owner before leave"))
(when-not (> (count members) 1)
(ex/raise :type :validation
:code :cant-leave-team
:context {:members (count members)}))
(db/delete! conn :team-profile-rel (db/delete! conn :team-profile-rel
{:profile-id profile-id {:profile-id profile-id
@ -129,7 +158,6 @@
nil))) nil)))
;; --- Mutation: Delete Team ;; --- Mutation: Delete Team
(s/def ::delete-team (s/def ::delete-team
@ -156,7 +184,6 @@
;; --- Mutation: Team Update Role ;; --- Mutation: Team Update Role
(declare retrieve-team-member) (declare retrieve-team-member)
(declare role->params)
(s/def ::team-id ::us/uuid) (s/def ::team-id ::us/uuid)
(s/def ::member-id ::us/uuid) (s/def ::member-id ::us/uuid)

View file

@ -37,10 +37,15 @@
(sv/defmethod ::profile {:auth false} (sv/defmethod ::profile {:auth false}
[{:keys [pool] :as cfg} {:keys [profile-id] :as params}] [{:keys [pool] :as cfg} {:keys [profile-id] :as params}]
(if profile-id
(retrieve-profile pool profile-id) ;; We need to return the anonymous profile object in two cases, when
{:id uuid/zero ;; no profile-id is in session, and when db call raises not found. In all other
:fullname "Anonymous User"})) ;; cases we need to reraise the exception.
(or (ex/try*
#(some->> profile-id (retrieve-profile pool))
#(when (not= :not-found (:type (ex-data %))) (throw %)))
{:id uuid/zero
:fullname "Anonymous User"}))
(def ^:private sql:default-profile-team (def ^:private sql:default-profile-team
"select t.id, name "select t.id, name

View file

@ -21,8 +21,10 @@
tpr.is_admin, tpr.is_admin,
tpr.can_edit tpr.can_edit
from team_profile_rel as tpr from team_profile_rel as tpr
join team as t on (t.id = tpr.team_id)
where tpr.profile_id = ? where tpr.profile_id = ?
and tpr.team_id = ?") and tpr.team_id = ?
and t.deleted_at is null")
(defn get-permissions (defn get-permissions
[conn profile-id team-id] [conn profile-id team-id]

View file

@ -6,6 +6,7 @@
(ns app.services-profile-test (ns app.services-profile-test
(:require (:require
[app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.rpc.mutations.profile :as profile] [app.rpc.mutations.profile :as profile]
[app.test-helpers :as th] [app.test-helpers :as th]
@ -153,11 +154,8 @@
:profile-id (:id prof)} :profile-id (:id prof)}
out (th/query! params)] out (th/query! params)]
;; (th/print-result! out) ;; (th/print-result! out)
(let [error (:error out) (let [result (:result out)]
error-data (ex-data error)] (t/is (= uuid/zero (:id result)))))))
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found))))
))
(t/deftest registration-domain-whitelist (t/deftest registration-domain-whitelist
(let [whitelist #{"gmail.com" "hey.com" "ya.ru"}] (let [whitelist #{"gmail.com" "hey.com" "ya.ru"}]

View file

@ -33,7 +33,6 @@
:role :editor :role :editor
:profile-id (:id profile1)}] :profile-id (:id profile1)}]
;; invite external user without complaints ;; invite external user without complaints
(let [data (assoc data :email "foo@bar.com") (let [data (assoc data :email "foo@bar.com")
out (th/mutation! data)] out (th/mutation! data)]
@ -136,9 +135,10 @@
:profile-id (:id profile1)} :profile-id (:id profile1)}
out (th/query! data)] out (th/query! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (let [error (:error out)
(let [result (:result out)] error-data (ex-data error)]
(t/is (= 0 (count result))))) (t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found))))
;; run permanent deletion ;; run permanent deletion
(let [result (task {:max-age (dt/duration 0)})] (let [result (task {:max-age (dt/duration 0)})]

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -89,3 +89,4 @@
@import "main/partials/handoff"; @import "main/partials/handoff";
@import "main/partials/exception-page"; @import "main/partials/exception-page";
@import "main/partials/share-link"; @import "main/partials/share-link";
@import "main/partials/af-signup-questions";

View file

@ -0,0 +1,197 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) UXBOX Labs SL
.af-form {
background-color: $color-white;
color: $color-gray-60 !important;
max-width: 760px !important;
overflow-y: auto;
padding: 3rem;
width: 100% !important;
h1, h3 {
font-family: 'worksans', sans-serif !important;
margin-bottom: .8rem;
font-weight: 500 !important;
}
h1 {
font-size: $fs38;
}
strong {
font-weight: 500;
}
p, label {
font-family: 'worksans', sans-serif !important;
font-size: $fs14;
}
form {
max-width: 760px;
width: 100%;
}
button {
font-family: 'worksans', sans-serif !important;
}
.af-choice,
.af-choice-multiple {
display: flex;
flex-wrap: wrap;
}
.af-choice-option {
max-width: 33%;
width: 100%;
label {
font-family: 'worksans', sans-serif !important;
font-size: $fs14;
padding-left: 0;
}
}
.af-choice-multiple {
.af-choice-option {
max-width: 50%;
width: 100%;
}
}
.af-divider-block {
/* margin-bottom: 2rem; */
p {
&::after,
&::before {
border-color: transparent;
}
}
}
.af-dropdown-text,
.text {
font-family: 'worksans', sans-serif !important;
}
.af-step-next {
display: flex;
margin-top: 2rem;
}
.af-step-next button {
color: $color-black;
background-color: $color-primary;
max-width: 180px;
margin-left: auto;
}
.af-step-previous {
margin-top: -40px;
}
.af-step-button {
text-align: left;
}
.af-field-input {
margin: 0.5rem 0;
}
.af-choice-option input:checked+label:before,
.af-legal input:checked+label:before {
background-color: $color-primary;
}
.af-field-use_of_penpot .af-choice-option input:checked+label,
.af-field-previous_design_tool .af-choice-option input:checked+label {
&::before {
background-color: transparent;
border: 2px solid $color-primary;
}
}
.af-field-use_of_penpot .af-choice-option label {
padding-top: 6rem;
background-size: 120px;
min-height: 150px;
}
.af-field-use_of_penpot .af-choice-option:nth-child(1) label {
background-image: url("../images/form/use-for-1.jpg");
}
.af-field-use_of_penpot .af-choice-option:nth-child(2) label {
background-image: url("../images/form/use-for-2.jpg");
}
.af-field-use_of_penpot .af-choice-option:nth-child(3) label {
background-image: url("../images/form/use-for-3.jpg");
}
.af-field-use_of_penpot .af-choice-option:nth-child(4) label {
background-image: url("../images/form/use-for-4.jpg");
}
.af-field-use_of_penpot label,
.af-field-previous_design_tool label {
display: flex;
padding-top: 5rem;
justify-content: center;
background-size: 50px;
background-repeat: no-repeat;
background-position: center 1rem;
margin: 1rem !important;
min-height: 130px;
position: relative;
text-align: center;
&:hover {
background-color: transparent;
box-shadow: 0px 10px 20px rgba(0,0,0,.2);
}
&::before {
background-color: transparent;
border-radius: 4px;
min-width: 100%;
min-height: 100%;
position: absolute;
top: 0;
left: 0;
margin: 0;
}
&::after {
display: none !important;
}
}
.af-field-previous_design_tool .af-choice-option:nth-child(1) label {
background-image: url("../images/form/figma.png");
}
.af-field-previous_design_tool .af-choice-option:nth-child(2) label {
background-image: url("../images/form/sketch.png");
}
.af-field-previous_design_tool .af-choice-option:nth-child(3) label {
background-image: url("../images/form/adobe-xd.png");
}
.af-field-previous_design_tool .af-choice-option:nth-child(4) label {
background-image: url("../images/form/uxpin.png");
}
.af-field-previous_design_tool .af-choice-option:nth-child(5) label {
background-image: url("../images/form/invision.png");
}
.af-field-previous_design_tool .af-choice-option:nth-child(6) label {
background-image: url("../images/form/never-used.png");
}
}

View file

@ -12,6 +12,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
padding: 32px; padding: 32px;
z-index: 1000;
cursor: pointer; cursor: pointer;

View file

@ -859,23 +859,23 @@
background-position: left top; background-position: left top;
background-size: 11%; background-size: 11%;
} }
.modal-left:hover { .modal-left:hover {
background-image: url("/images/on-solo-hover.svg"); background-image: url("/images/on-solo-hover.svg");
background-size: 15%; background-size: 15%;
} }
.modal-right { .modal-right {
background-image: url("/images/on-teamup.svg"); background-image: url("/images/on-teamup.svg");
background-position: right top; background-position: right top;
background-size: 28%; background-size: 28%;
} }
.modal-right:hover { .modal-right:hover {
background-image: url("/images/on-teamup-hover.svg"); background-image: url("/images/on-teamup-hover.svg");
background-size: 32%; background-size: 32%;
} }
.modal-right, .modal-right,
.modal-left { .modal-left {
background-repeat: no-repeat; background-repeat: no-repeat;
@ -1001,17 +1001,17 @@
.template-item { .template-item {
width: 275px; width: 275px;
border: 1px solid $color-gray-10; border: 1px solid $color-gray-10;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
text-align: left; text-align: left;
border-radius: $br-small; border-radius: $br-small;
&:not(:last-child) { &:not(:last-child) {
margin-bottom: 22px; margin-bottom: 22px;
} }
} }
.template-item-content { .template-item-content {
// height: 144px; // height: 144px;
flex-grow: 1; flex-grow: 1;
@ -1020,7 +1020,7 @@
border-radius: $br-small $br-small 0 0; border-radius: $br-small $br-small 0 0;
} }
} }
.template-item-title { .template-item-title {
padding: 6px 12px; padding: 6px 12px;
height: 64px; height: 64px;
@ -1135,3 +1135,49 @@
} }
} }
.questions-form {
.modal-overlay {
z-index: 2001;
}
.modal-container {
background-image: url("../images/deco-left.png"), url("../images/deco-right.png");
background-repeat: no-repeat;
background-position: 10% 50px, 90% 50px;
background-size: 65px;
display: flex;
flex-direction: row;
height: 100vh;
justify-content: center;
width: 100vw;
.af-form {
--primary-color: #00C38B;
--input-background-color: #ffffff;
--label-font-size: $fs16;
--field-error-font-color: #E65244;
--message-success-font-color: #49D793;
--message-fail-font-color: #E65244;
--invalid-field-border-color: #E65244;
--dropdown-background-color: #ffffff;
--primary-font-color: #000;
--input-border-color: rgb(224, 230, 240);
--input-border-radius: 3px;
--button-border-radius: 3px;
--message-border-radius: 3px;
--checkbox-border-radius: 3px;
--dropdown-option-background-color: rgba(0,195,139,1);
--dropdown-option-active-background-color: rgba(0,138,98,1);
--invalid-field-background-color: rgba(238.51780000000002,205.7178,204.11780000000002,1);
--message-fail-background-color: rgba(238.51780000000002,205.7178,204.11780000000002,1);
--message-success-background-color: rgba(171,232,197,1);
}
}
.modal-overlay {
background-color: rgba(0,0,0,0.9);
}
}

View file

@ -78,6 +78,7 @@
(def translations (obj/get global "penpotTranslations")) (def translations (obj/get global "penpotTranslations"))
(def themes (obj/get global "penpotThemes")) (def themes (obj/get global "penpotThemes"))
(def sentry-dsn (obj/get global "penpotSentryDsn")) (def sentry-dsn (obj/get global "penpotSentryDsn"))
(def onboarding-form-id (obj/get global "penpotOnboardingQuestionsFormId"))
(def flags (atom (parse-flags global))) (def flags (atom (parse-flags global)))
(def version (atom (parse-version global))) (def version (atom (parse-version global)))

View file

@ -16,6 +16,7 @@
[app.main.repo :as rp] [app.main.repo :as rp]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt] [app.util.router :as rt]
[app.util.timers :as tm]
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[potok.core :as ptk])) [potok.core :as ptk]))
@ -60,6 +61,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare fetch-projects) (declare fetch-projects)
(declare fetch-team-members)
(defn initialize (defn initialize
[{:keys [id] :as params}] [{:keys [id] :as params}]
@ -84,6 +86,7 @@
(rx/merge (rx/merge
(ptk/watch (df/load-team-fonts id) state stream) (ptk/watch (df/load-team-fonts id) state stream)
(ptk/watch (fetch-projects) state stream) (ptk/watch (fetch-projects) state stream)
(ptk/watch (fetch-team-members) state stream)
(ptk/watch (du/fetch-teams) state stream) (ptk/watch (du/fetch-teams) state stream)
(ptk/watch (du/fetch-users {:team-id id}) state stream))))) (ptk/watch (du/fetch-users {:team-id id}) state stream)))))
@ -237,13 +240,14 @@
(update :dashboard-files d/merge files)))))) (update :dashboard-files d/merge files))))))
(defn fetch-recent-files (defn fetch-recent-files
[] ([] (fetch-recent-files nil))
(ptk/reify ::fetch-recent-files ([team-id]
ptk/WatchEvent (ptk/reify ::fetch-recent-files
(watch [_ state _] ptk/WatchEvent
(let [team-id (:current-team-id state)] (watch [_ state _]
(->> (rp/query :team-recent-files {:team-id team-id}) (let [team-id (or team-id (:current-team-id state))]
(rx/map recent-files-fetched)))))) (->> (rp/query :team-recent-files {:team-id team-id})
(rx/map recent-files-fetched)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Selection ;; Data Selection
@ -396,16 +400,13 @@
(let [{:keys [on-success on-error] (let [{:keys [on-success on-error]
:or {on-success identity :or {on-success identity
on-error rx/throw}} (meta params) on-error rx/throw}} (meta params)
team-id (:current-team-id state)] team-id (:current-team-id state)
(rx/concat params (cond-> {:id team-id}
(when (uuid? reassign-to) (uuid? reassign-to)
(->> (rp/mutation! :update-team-member-role {:team-id team-id (assoc :reassign-to reassign-to))]
:role :owner (->> (rp/mutation! :leave-team params)
:member-id reassign-to}) (rx/tap #(tm/schedule on-success))
(rx/ignore))) (rx/catch on-error))))))
(->> (rp/mutation! :leave-team {:id team-id})
(rx/tap on-success)
(rx/catch on-error)))))))
(defn invite-team-member (defn invite-team-member
[{:keys [email role] :as params}] [{:keys [email role] :as params}]

View file

@ -7,12 +7,12 @@
(ns app.main.data.users (ns app.main.data.users
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.main.data.events :as ev] [app.main.data.events :as ev]
[app.main.data.media :as di] [app.main.data.media :as di]
[app.main.data.modal :as modal]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.util.i18n :as i18n] [app.util.i18n :as i18n]
[app.util.router :as rt] [app.util.router :as rt]
@ -93,6 +93,8 @@
;; --- EVENT: fetch-profile ;; --- EVENT: fetch-profile
(declare logout)
(def profile-fetched? (def profile-fetched?
(ptk/type? ::profile-fetched)) (ptk/type? ::profile-fetched))
@ -105,18 +107,18 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (cond-> state
(assoc :profile-id id) (is-authenticated? profile)
(assoc :profile profile))) (-> (assoc :profile-id id)
(assoc :profile profile))))
ptk/EffectEvent ptk/EffectEvent
(effect [_ state _] (effect [_ state _]
(let [profile (:profile state)] (when-let [profile (:profile state)]
(when (not= uuid/zero (:id profile)) (swap! storage assoc :profile profile)
(swap! storage assoc :profile profile) (i18n/set-locale! (:lang profile))
(i18n/set-locale! (:lang profile)) (some-> (:theme profile)
(some-> (:theme profile) (theme/set-current-theme!))))))
(theme/set-current-theme!)))))))
(defn fetch-profile (defn fetch-profile
[] []
@ -145,55 +147,84 @@
(rx/mapcat (fn [profile] (rx/mapcat (fn [profile]
(if (= uuid/zero (:id profile)) (if (= uuid/zero (:id profile))
(rx/empty) (rx/empty)
(rx/of (fetch-teams)))))))))) (rx/of (fetch-teams)))))
(rx/observe-on :async))))))
;; --- EVENT: login ;; --- EVENT: login
(defn- logged-in (defn- logged-in
"This is the main event that is executed once we have logged in
profile. The profile can proceed from standard login or from
accepting invitation, or third party auth signup or singin."
[profile] [profile]
(ptk/reify ::logged-in (letfn [(get-redirect-event []
IDeref (let [team-id (:default-team-id profile)]
(-deref [_] profile) (rt/nav' :dashboard-projects {:team-id team-id})))]
ptk/WatchEvent (ptk/reify ::logged-in
(watch [_ _ _] IDeref
(let [team-id (get-current-team-id profile)] (-deref [_] profile)
(->> (rx/concat
(rx/of (profile-fetched profile)
(fetch-teams))
(->> (rx/of (rt/nav' :dashboard-projects {:team-id team-id})) ptk/WatchEvent
(rx/delay 1000)) (watch [_ _ _]
(when (is-authenticated? profile)
(when-not (get-in profile [:props :onboarding-viewed]) (->> (rx/of (profile-fetched profile)
(->> (rx/of (modal/show {:type :onboarding})) (fetch-teams)
(rx/delay 1000)))) (get-redirect-event))
(rx/observe-on :async)))))))
(rx/observe-on :async))))))
(s/def ::login-params (s/def ::login-params
(s/keys :req-un [::email ::password])) (s/keys :req-un [::email ::password]))
(declare login-from-register)
(defn login (defn login
[{:keys [email password] :as data}] [{:keys [email password] :as data}]
(us/verify ::login-params data) (us/verify ::login-params data)
(ptk/reify ::login (ptk/reify ::login
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ stream]
(let [{:keys [on-error on-success] (let [{:keys [on-error on-success]
:or {on-error rx/throw :or {on-error rx/throw
on-success identity}} (meta data) on-success identity}} (meta data)
params {:email email params {:email email
:password password :password password
:scope "webapp"}] :scope "webapp"}]
(->> (rx/timer 100)
(rx/mapcat #(rp/mutation :login params)) ;; NOTE: We can't take the profile value from login because
(rx/tap on-success) ;; there are cases when login is successfull but the cookie is
(rx/catch on-error) ;; not set properly (because of possible misconfiguration).
(rx/map (fn [profile] ;; So, we proceed to make an additional call to fetch the
(with-meta profile ;; profile, and ensure that cookie is set correctly. If
{::ev/source "login"}))) ;; profile fetch is successful, we mark the user logged in, if
(rx/map logged-in)))))) ;; the returned profile is an NOT authenticated profile, we
;; proceed to logout and show an error message.
(rx/merge
(->> (rp/mutation :login params)
(rx/map fetch-profile)
(rx/catch on-error))
(->> stream
(rx/filter profile-fetched?)
(rx/take 1)
(rx/map deref)
(rx/filter (complement is-authenticated?))
(rx/tap on-error)
(rx/map #(ex/raise :type :authentication))
(rx/observe-on :async))
(->> stream
(rx/filter profile-fetched?)
(rx/take 1)
(rx/map deref)
(rx/filter is-authenticated?)
(rx/map (fn [profile]
(with-meta profile
{::ev/source "login"})))
(rx/tap on-success)
(rx/map logged-in)
(rx/observe-on :async)))))))
(defn login-from-token (defn login-from-token
[{:keys [profile] :as tdata}] [{:keys [profile] :as tdata}]
@ -221,44 +252,46 @@
(rx/map (fn [profile] (rx/map (fn [profile]
(with-meta profile (with-meta profile
{::ev/source "register"}))) {::ev/source "register"})))
(rx/map logged-in)))))) (rx/map logged-in)
(rx/observe-on :async))))))
;; --- EVENT: logout ;; --- EVENT: logout
(defn logged-out (defn logged-out
[] ([] (logged-out {}))
(ptk/reify ::logged-out ([_params]
ptk/UpdateEvent (ptk/reify ::logged-out
(update [_ state] ptk/UpdateEvent
(select-keys state [:route :router :session-id :history])) (update [_ state]
(select-keys state [:route :router :session-id :history]))
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(rx/of (rt/nav :auth-login))) ;; NOTE: We need the `effect` of the current event to be
;; executed before the redirect.
(->> (rx/of (rt/nav :auth-login))
(rx/observe-on :async)))
ptk/EffectEvent ptk/EffectEvent
(effect [_ _ _] (effect [_ _ _]
(reset! storage {}) (reset! storage {})
(i18n/reset-locale)))) (i18n/reset-locale)))))
(defn logout (defn logout
[] ([] (logout {}))
(ptk/reify ::logout ([params]
ptk/WatchEvent (ptk/reify ::logout
(watch [_ _ _] ptk/WatchEvent
(->> (rp/mutation :logout) (watch [_ _ _]
(rx/delay-at-least 300) (->> (rp/mutation :logout)
(rx/catch (constantly (rx/of 1))) (rx/delay-at-least 300)
(rx/map logged-out))))) (rx/catch (constantly (rx/of 1)))
(rx/map #(logged-out params)))))))
;; --- EVENT: register ;; --- EVENT: register
;; TODO: remove
(s/def ::invitation-token ::us/not-empty-string)
(s/def ::register (s/def ::register
(s/keys :req-un [::fullname ::password ::email] (s/keys :req-un [::fullname ::password ::email]))
:opt-un [::invitation-token]))
(defn register (defn register
"Create a register event instance." "Create a register event instance."
@ -347,20 +380,33 @@
(rx/empty))) (rx/empty)))
(rx/ignore)))))) (rx/ignore))))))
(defn mark-onboarding-as-viewed (defn mark-onboarding-as-viewed
([] (mark-onboarding-as-viewed nil)) ([] (mark-onboarding-as-viewed nil))
([{:keys [version]}] ([{:keys [version]}]
(ptk/reify ::mark-oboarding-as-viewed (ptk/reify ::mark-oboarding-as-viewed
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ _ _]
(let [version (or version (:main @cf/version)) (let [version (or version (:main @cf/version))
props (-> (get-in state [:profile :props]) props {:onboarding-viewed true
(assoc :onboarding-viewed true) :release-notes-viewed version}]
(assoc :release-notes-viewed version))]
(->> (rp/mutation :update-profile-props {:props props}) (->> (rp/mutation :update-profile-props {:props props})
(rx/map (constantly (fetch-profile))))))))) (rx/map (constantly (fetch-profile)))))))))
(defn mark-questions-as-answered
[]
(ptk/reify ::mark-questions-as-answered
ptk/UpdateEvent
(update [_ state]
(update-in state [:profile :props] assoc :onboarding-questions-answered true))
ptk/WatchEvent
(watch [_ _ _]
(let [props {:onboarding-questions-answered true}]
(->> (rp/mutation :update-profile-props {:props props})
(rx/map (constantly (fetch-profile))))))))
;; --- Update Photo ;; --- Update Photo
(defn update-photo (defn update-photo

View file

@ -13,6 +13,7 @@
[app.main.data.users :as du] [app.main.data.users :as du]
[app.main.sentry :as sentry] [app.main.sentry :as sentry]
[app.main.store :as st] [app.main.store :as st]
[app.util.i18n :refer [tr]]
[app.util.router :as rt] [app.util.router :as rt]
[app.util.timers :as ts] [app.util.timers :as ts]
[cljs.pprint :refer [pprint]] [cljs.pprint :refer [pprint]]
@ -48,7 +49,9 @@
;; here and not in app.main.errors because of circular dependency. ;; here and not in app.main.errors because of circular dependency.
(defmethod ptk/handle-error :authentication (defmethod ptk/handle-error :authentication
[_] [_]
(ts/schedule (st/emitf (du/logout)))) (let [msg (tr "errors.auth.unable-to-login")]
(st/emit! (du/logout {:capture-redirect true}))
(ts/schedule 500 (st/emitf (dm/warn msg)))))
;; That are special case server-errors that should be treated ;; That are special case server-errors that should be treated

View file

@ -6,6 +6,7 @@
(ns app.main.ui (ns app.main.ui
(:require (:require
[app.config :as cf]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.auth :refer [auth]] [app.main.ui.auth :refer [auth]]
@ -17,6 +18,8 @@
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.messages :as msgs] [app.main.ui.messages :as msgs]
[app.main.ui.onboarding] [app.main.ui.onboarding]
[app.main.ui.onboarding.questions]
[app.main.ui.releases]
[app.main.ui.render :as render] [app.main.ui.render :as render]
[app.main.ui.settings :as settings] [app.main.ui.settings :as settings]
[app.main.ui.static :as static] [app.main.ui.static :as static]
@ -32,7 +35,7 @@
(mf/defc main-page (mf/defc main-page
{::mf/wrap [#(mf/catch % {:fallback on-main-error})]} {::mf/wrap [#(mf/catch % {:fallback on-main-error})]}
[{:keys [route] :as props}] [{:keys [route profile]}]
(let [{:keys [data params]} route] (let [{:keys [data params]} route]
[:& (mf/provider ctx/current-route) {:value route} [:& (mf/provider ctx/current-route) {:value route}
(case (:name data) (case (:name data)
@ -70,13 +73,32 @@
:dashboard-font-providers :dashboard-font-providers
:dashboard-team-members :dashboard-team-members
:dashboard-team-settings) :dashboard-team-settings)
[:* [:*
#_[:div.modal-wrapper #_[:div.modal-wrapper
#_[:& app.main.ui.onboarding/onboarding-templates-modal] #_[:& app.main.ui.onboarding/onboarding-templates-modal]
[:& app.main.ui.onboarding/onboarding-modal] #_[:& app.main.ui.onboarding/onboarding-modal]
#_[:& app.main.ui.onboarding/onboarding-team-modal] #_[:& app.main.ui.onboarding/onboarding-team-modal]
] ]
[:& dashboard {:route route}]] (when-let [props (some-> profile (get :props {}))]
(cond
(and cf/onboarding-form-id
(not (:onboarding-questions-answered props false))
(not (:onboarding-viewed props false)))
[:& app.main.ui.onboarding.questions/questions
{:profile profile
:form-id cf/onboarding-form-id}]
(not (:onboarding-viewed props))
[:& app.main.ui.onboarding/onboarding-modal {}]
(and (:onboarding-viewed props)
(not= (:release-notes-viewed props) (:main @cf/version))
(not= "0.0" (:main @cf/version)))
[:& app.main.ui.releases/release-notes-modal {}]))
[:& dashboard {:route route :profile profile}]]
:viewer :viewer
(let [{:keys [query-params path-params]} route (let [{:keys [query-params path-params]} route
@ -124,12 +146,14 @@
(mf/defc app (mf/defc app
[] []
(let [route (mf/deref refs/route) (let [route (mf/deref refs/route)
edata (mf/deref refs/exception)] edata (mf/deref refs/exception)
profile (mf/deref refs/profile)]
[:& (mf/provider ctx/current-route) {:value route} [:& (mf/provider ctx/current-route) {:value route}
(if edata [:& (mf/provider ctx/current-profile) {:value profile}
[:& static/exception-page {:data edata}] (if edata
[:* [:& static/exception-page {:data edata}]
[:& msgs/notifications] [:*
(when route [:& msgs/notifications]
[:& main-page {:route route}])])])) (when route
[:& main-page {:route route :profile profile}])])]]))

View file

@ -30,8 +30,7 @@
(mf/use-callback (mf/use-callback
(fn [_ _] (fn [_ _]
(reset! submitted false) (reset! submitted false)
(st/emit! (dm/info (tr "auth.notifications.recovery-token-sent")) (st/emit! (dm/info (tr "auth.notifications.recovery-token-sent")))))
(rt/nav :auth-login))))
on-error on-error
(mf/use-callback (mf/use-callback

View file

@ -15,8 +15,9 @@
;; for text shapes in the export process ;; for text shapes in the export process
(def text-plain-colors-ctx (mf/create-context false)) (def text-plain-colors-ctx (mf/create-context false))
(def current-route (mf/create-context nil)) (def current-route (mf/create-context nil))
(def current-team-id (mf/create-context nil)) (def current-profile (mf/create-context nil))
(def current-team-id (mf/create-context nil))
(def current-project-id (mf/create-context nil)) (def current-project-id (mf/create-context nil))
(def current-page-id (mf/create-context nil)) (def current-page-id (mf/create-context nil))
(def current-file-id (mf/create-context nil)) (def current-file-id (mf/create-context nil))

View file

@ -7,9 +7,7 @@
(ns app.main.ui.dashboard (ns app.main.ui.dashboard
(:require (:require
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cf]
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.data.modal :as modal]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
@ -22,7 +20,6 @@
[app.main.ui.dashboard.search :refer [search-page]] [app.main.ui.dashboard.search :refer [search-page]]
[app.main.ui.dashboard.sidebar :refer [sidebar]] [app.main.ui.dashboard.sidebar :refer [sidebar]]
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page]] [app.main.ui.dashboard.team :refer [team-settings-page team-members-page]]
[app.util.timers :as tm]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(defn ^boolean uuid-str? (defn ^boolean uuid-str?
@ -77,9 +74,8 @@
nil)]) nil)])
(mf/defc dashboard (mf/defc dashboard
[{:keys [route] :as props}] [{:keys [route profile] :as props}]
(let [profile (mf/deref refs/profile) (let [section (get-in route [:data :name])
section (get-in route [:data :name])
params (parse-params route) params (parse-params route)
project-id (:project-id params) project-id (:project-id params)
@ -94,18 +90,8 @@
(mf/use-effect (mf/use-effect
(mf/deps team-id) (mf/deps team-id)
(st/emitf (dd/initialize {:id team-id})))
(mf/use-effect
(mf/deps)
(fn [] (fn []
(let [props (:props profile) (st/emit! (dd/initialize {:id team-id}))))
version (:release-notes-viewed props)]
(when (and (:onboarding-viewed props)
(not= version (:main @cf/version))
(not= "0.0" (:main @cf/version)))
(tm/schedule 1000 #(st/emit! (modal/show {:type :release-notes
:version (:main @cf/version)})))))))
[:& (mf/provider ctx/current-team-id) {:value team-id} [:& (mf/provider ctx/current-team-id) {:value team-id}
[:& (mf/provider ctx/current-project-id) {:value project-id} [:& (mf/provider ctx/current-project-id) {:value project-id}

View file

@ -115,7 +115,7 @@
(st/emit! (dm/success (tr "dashboard.success-move-file")))) (st/emit! (dm/success (tr "dashboard.success-move-file"))))
(if (or navigate? (not= team-id current-team-id)) (if (or navigate? (not= team-id current-team-id))
(st/emit! (dd/go-to-files team-id project-id)) (st/emit! (dd/go-to-files team-id project-id))
(st/emit! (dd/fetch-recent-files) (st/emit! (dd/fetch-recent-files team-id)
(dd/clear-selected-files)))) (dd/clear-selected-files))))
on-move on-move

View file

@ -327,8 +327,9 @@
on-finish-import on-finish-import
(mf/use-callback (mf/use-callback
(mf/deps (:id team))
(fn [] (fn []
(st/emit! (dd/fetch-recent-files) (st/emit! (dd/fetch-recent-files (:id team))
(dd/clear-selected-files)))) (dd/clear-selected-files))))
import-files (use-import-file project-id on-finish-import) import-files (use-import-file project-id on-finish-import)
@ -366,7 +367,7 @@
on-drop-success on-drop-success
(fn [] (fn []
(st/emit! (dm/success (tr "dashboard.success-move-file")) (st/emit! (dm/success (tr "dashboard.success-move-file"))
(dd/fetch-recent-files) (dd/fetch-recent-files (:id team))
(dd/clear-selected-files))) (dd/clear-selected-files)))
on-drop on-drop

View file

@ -97,9 +97,10 @@
on-import on-import
(mf/use-callback (mf/use-callback
(mf/deps (:id project) (:id team))
(fn [] (fn []
(st/emit! (dd/fetch-files {:project-id (:id project)}) (st/emit! (dd/fetch-files {:project-id (:id project)})
(dd/fetch-recent-files) (dd/fetch-recent-files (:id team))
(dd/clear-selected-files))))] (dd/clear-selected-files))))]
[:div.dashboard-project-row {:class (when first? "first")} [:div.dashboard-project-row {:class (when first? "first")}
@ -163,15 +164,15 @@
(mf/use-effect (mf/use-effect
(mf/deps team) (mf/deps team)
(fn [] (fn []
(when team (let [tname (if (:is-default team)
(let [tname (if (:is-default team) (tr "dashboard.your-penpot")
(tr "dashboard.your-penpot") (:name team))]
(:name team))] (dom/set-html-title (tr "title.dashboard.projects" tname)))))
(dom/set-html-title (tr "title.dashboard.projects" tname))))))
(mf/use-effect (mf/use-effect
(mf/deps (:id team))
(fn [] (fn []
(st/emit! (dd/fetch-recent-files) (st/emit! (dd/fetch-recent-files (:id team))
(dd/clear-selected-files)))) (dd/clear-selected-files))))
(when (seq projects) (when (seq projects)

View file

@ -28,6 +28,7 @@
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.object :as obj] [app.util.object :as obj]
[app.util.router :as rt] [app.util.router :as rt]
[beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[goog.functions :as f] [goog.functions :as f]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
@ -287,27 +288,39 @@
members-map (mf/deref refs/dashboard-team-members) members-map (mf/deref refs/dashboard-team-members)
members (vals members-map) members (vals members-map)
on-rename-clicked on-success
(st/emitf (modal/show :team-form {:team team}))
on-leaved-success
(fn []
(st/emit! (modal/hide)
(du/fetch-teams)))
leave-fn
(st/emitf (dd/leave-team (with-meta {} {:on-success on-leaved-success})))
leave-and-reassign-fn
(fn [member-id]
(let [params {:reassign-to member-id}]
(st/emit! (dd/go-to-projects (:default-team-id profile))
(dd/leave-team (with-meta params {:on-success on-leaved-success})))))
delete-fn
(fn [] (fn []
(st/emit! (dd/go-to-projects (:default-team-id profile)) (st/emit! (dd/go-to-projects (:default-team-id profile))
(dd/delete-team (with-meta team {:on-success on-leaved-success})))) (modal/hide)
(du/fetch-teams)))
on-error
(fn [{:keys [code] :as error}]
(condp = code
:no-enough-members-for-leave
(rx/of (dm/error (tr "errors.team-leave.insufficient-members")))
:member-does-not-exist
(rx/of (dm/error (tr "errors.team-leave.member-does-not-exists")))
:owner-cant-leave-team
(rx/of (dm/error (tr "errors.team-leave.owner-cant-leave")))
(rx/throw error)))
leave-fn
(fn [member-id]
(let [params (cond-> {} (uuid? member-id) (assoc :reassign-to member-id))]
(st/emit! (dd/leave-team (with-meta params
{:on-success on-success
:on-error on-error})))))
delete-fn
(fn []
(st/emit! (dd/delete-team (with-meta team {:on-success on-success
:on-error on-error}))))
on-rename-clicked
(fn []
(st/emit! (modal/show :team-form {:team team})))
on-leave-clicked on-leave-clicked
(st/emitf (modal/show (st/emitf (modal/show
@ -324,7 +337,7 @@
{:type ::leave-and-reassign {:type ::leave-and-reassign
:profile profile :profile profile
:team team :team team
:accept leave-and-reassign-fn}))) :accept leave-fn})))
on-delete-clicked on-delete-clicked
(st/emitf (st/emitf
@ -501,7 +514,7 @@
[:li {:on-click (partial on-click :settings-password)} [:li {:on-click (partial on-click :settings-password)}
[:span.icon i/lock] [:span.icon i/lock]
[:span.text (tr "labels.password")]] [:span.text (tr "labels.password")]]
[:li {:on-click (partial on-click (du/logout))} [:li {:on-click #(on-click (du/logout) %)}
[:span.icon i/exit] [:span.icon i/exit]
[:span.text (tr "labels.logout")]] [:span.text (tr "labels.logout")]]

View file

@ -6,32 +6,16 @@
(ns app.main.ui.onboarding (ns app.main.ui.onboarding
(:require (:require
[app.common.spec :as us]
[app.config :as cf] [app.config :as cf]
[app.main.data.dashboard :as dd]
[app.main.data.messages :as dm]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.users :as du] [app.main.data.users :as du]
[app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.forms :as fm] [app.main.ui.onboarding.questions]
[app.main.ui.icons :as i] [app.main.ui.onboarding.team-choice]
[app.main.ui.onboarding.templates]
[app.main.ui.releases.common :as rc] [app.main.ui.releases.common :as rc]
[app.main.ui.releases.v1-10]
[app.main.ui.releases.v1-4]
[app.main.ui.releases.v1-5]
[app.main.ui.releases.v1-6]
[app.main.ui.releases.v1-7]
[app.main.ui.releases.v1-8]
[app.main.ui.releases.v1-9]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.object :as obj]
[app.util.router :as rt]
[app.util.timers :as tm] [app.util.timers :as tm]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
;; --- ONBOARDING LIGHTBOX ;; --- ONBOARDING LIGHTBOX
@ -189,297 +173,3 @@
:slide @slide :slide @slide
:navigate navigate :navigate navigate
:skip skip)))]])) :skip skip)))]]))
(s/def ::name ::us/not-empty-string)
(s/def ::team-form
(s/keys :req-un [::name]))
(mf/defc onboarding-choice-modal
{::mf/register modal/components
::mf/register-as :onboarding-choice}
[]
(let [;; When user choices the option of `fly solo`, we proceed to show
;; the onboarding templates modal.
on-fly-solo
(fn []
(tm/schedule 400 #(st/emit! (modal/show {:type :onboarding-templates}))))
;; When user choices the option of `team up`, we proceed to show
;; the team creation modal.
on-team-up
(fn []
(st/emit! (modal/show {:type :onboarding-team})))
]
[:div.modal-overlay
[:div.modal-container.onboarding.final.animated.fadeInUp
[:div.modal-top
[:h1 (tr "onboarding.choice.title")]
[:p (tr "onboarding.choice.desc")]]
[:div.modal-columns
[:div.modal-left
[:div.content-button {:on-click on-fly-solo}
[:h2 (tr "onboarding.choice.fly-solo")]
[:p (tr "onboarding.choice.fly-solo-desc")]]]
[:div.modal-right
[:div.content-button {:on-click on-team-up}
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
(mf/defc onboarding-team-modal
{::mf/register modal/components
::mf/register-as :onboarding-team}
[]
(let [form (fm/use-form :spec ::team-form
:initial {})
on-submit
(mf/use-callback
(fn [form _]
(let [tname (get-in @form [:clean-data :name])]
(st/emit! (modal/show {:type :onboarding-team-invitations :name tname})))))]
[:div.modal-overlay
[:div.modal-container.onboarding-team
[:div.title
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]
[:& fm/form {:form form
:on-submit on-submit}
[:div.team-row
[:& fm/input {:type "text"
:name :name
:label (tr "onboarding.team-input-placeholder")}]]
[:div.buttons
[:button.btn-secondary.btn-large
{:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
(tr "labels.cancel")]
[:& fm/submit-button
{:label (tr "labels.next")}]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
(defn get-available-roles
[]
[{:value "editor" :label (tr "labels.editor")}
{:value "admin" :label (tr "labels.admin")}])
(s/def ::email ::us/email)
(s/def ::role ::us/keyword)
(s/def ::invite-form
(s/keys :req-un [::role ::email]))
;; This is the final step of team creation, consists in provide a
;; shortcut for invite users.
(mf/defc onboarding-team-invitations-modal
{::mf/register modal/components
::mf/register-as :onboarding-team-invitations}
[{:keys [name] :as props}]
(let [initial (mf/use-memo (constantly
{:role "editor"
:name name}))
form (fm/use-form :spec ::invite-form
:initial initial)
roles (mf/use-memo #(get-available-roles))
on-success
(mf/use-callback
(fn [_form response]
(let [project-id (:default-project-id response)
team-id (:id response)]
(st/emit!
(modal/hide)
(rt/nav :dashboard-projects {:team-id team-id}))
(tm/schedule 400 #(st/emit!
(modal/show {:type :onboarding-templates
:project-id project-id}))))))
on-error
(mf/use-callback
(fn [_form _response]
(st/emit! (dm/error "Error on creating team."))))
;; The SKIP branch only creates the team, without invitations
on-skip
(mf/use-callback
(fn [_]
(let [mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}
params {:name name}]
(st/emit! (dd/create-team (with-meta params mdata))))))
;; The SUBMIT branch creates the team with the invitations
on-submit
(mf/use-callback
(fn [form _]
(let [mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}
params (:clean-data @form)]
(st/emit! (dd/create-team-with-invitations (with-meta params mdata))))))]
[:div.modal-overlay
[:div.modal-container.onboarding-team
[:div.title
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]
[:& fm/form {:form form
:on-submit on-submit}
[:div.invite-row
[:& fm/input {:name :email
:label (tr "labels.email")}]
[:& fm/select {:name :role
:options roles}]]
[:div.buttons
[:button.btn-secondary.btn-large
{:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
(tr "labels.cancel")]
[:& fm/submit-button
{:label (tr "labels.create")}]]
[:div.skip-action
{:on-click on-skip}
[:div.action "Skip and invite later"]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
(mf/defc template-item
[{:keys [name path image project-id]}]
(let [downloading? (mf/use-state false)
link (str (assoc cf/public-uri :path path))
on-finish-import
(fn []
(st/emit! (dd/fetch-files {:project-id project-id})
(dd/fetch-recent-files)
(dd/clear-selected-files)))
open-import-modal
(fn [file]
(st/emit! (modal/show
{:type :import
:project-id project-id
:files [file]
:on-finish-import on-finish-import})))
on-click
(fn []
(reset! downloading? true)
(->> (http/send! {:method :get :uri link :response-type :blob :mode :no-cors})
(rx/subs (fn [{:keys [body] :as response}]
(open-import-modal {:name name :uri (dom/create-uri body)}))
(fn [error]
(js/console.log "error" error))
(fn []
(reset! downloading? false)))))
]
[:div.template-item
[:div.template-item-content
[:img {:src image}]]
[:div.template-item-title
[:div.label name]
(if @downloading?
[:div.action "Fetching..."]
[:div.action {:on-click on-click} "+ Add to drafts"])]]))
(mf/defc onboarding-templates-modal
{::mf/register modal/components
::mf/register-as :onboarding-templates}
;; NOTE: the project usually comes empty, it only comes fullfilled
;; when a user creates a new team just after signup.
[{:keys [project-id] :as props}]
(let [close-fn (mf/use-callback #(st/emit! (modal/hide)))
profile (mf/deref refs/profile)
project-id (or project-id (:default-project-id profile))]
[:div.modal-overlay
[:div.modal-container.onboarding-templates
[:div.modal-header
[:div.modal-close-button
{:on-click close-fn} i/close]]
[:div.modal-content
[:h3 (tr "onboarding.templates.title")]
[:p (tr "onboarding.templates.subtitle")]
[:div.templates
[:& template-item
{:path "/github/penpot-files/Penpot-Design-system.penpot"
:image "https://penpot.app/images/libraries/cover-ds-penpot.jpg"
:name "Penpot Design System"
:project-id project-id}]
[:& template-item
{:path "/github/penpot-files/Material-Design-Kit.penpot"
:image "https://penpot.app/images/libraries/cover-material.jpg"
:name "Material Design Kit"
:project-id project-id}]]]]]))
;;; --- RELEASE NOTES MODAL
(mf/defc release-notes
[{:keys [version] :as props}]
(let [slide (mf/use-state :start)
klass (mf/use-state "fadeInDown")
navigate
(mf/use-callback #(reset! slide %))
next
(mf/use-callback
(mf/deps slide)
(fn []
(if (= @slide :start)
(navigate 0)
(navigate (inc @slide)))))
finish
(mf/use-callback
(st/emitf (modal/hide)
(du/mark-onboarding-as-viewed {:version version})))
]
(mf/use-effect
(mf/deps)
(fn []
(st/emitf (du/mark-onboarding-as-viewed {:version version}))))
(mf/use-layout-effect
(mf/deps @slide)
(fn []
(when (not= :start @slide)
(reset! klass "fadeIn"))
(let [sem (tm/schedule 300 #(reset! klass nil))]
(fn []
(reset! klass nil)
(tm/dispose! sem)))))
(rc/render-release-notes
{:next next
:navigate navigate
:finish finish
:klass klass
:slide slide
:version version})))
(mf/defc release-notes-modal
{::mf/wrap-props false
::mf/register modal/components
::mf/register-as :release-notes}
[props]
(let [versions (methods rc/render-release-notes)
version (obj/get props "version")]
(when (contains? versions version)
[:div.relnotes
[:> release-notes props]])))
(defmethod rc/render-release-notes "0.0"
[params]
(rc/render-release-notes (assoc params :version "1.10")))

View file

@ -0,0 +1,48 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.onboarding.questions
"External form for onboarding questions."
(:require
[app.main.data.users :as du]
[app.main.store :as st]
[app.util.dom :as dom]
[goog.events :as ev]
[promesa.core :as p]
[rumext.alpha :as mf]))
(defn load-arengu-sdk
[container-ref email form-id]
(letfn [(on-init []
(when-let [container (mf/ref-val container-ref)]
(-> (.embed js/ArenguForms form-id container)
(p/then (fn [form]
(.setHiddenField ^js form "email" email))))))
(on-submit-success [_event]
(st/emit! (du/mark-questions-as-answered)))
]
(let [script (dom/create-element "script")
head (unchecked-get js/document "head")
lkey1 (ev/listen js/document "af-submitForm-success" on-submit-success)]
(unchecked-set script "src" "https://sdk.arengu.com/forms.js")
(unchecked-set script "onload" on-init)
(dom/append-child! head script)
(fn []
(ev/unlistenByKey lkey1)))))
(mf/defc questions
[{:keys [profile form-id]}]
(let [container (mf/use-ref)]
(mf/use-effect (partial load-arengu-sdk container (:email profile) form-id))
[:div.modal-wrapper.questions-form
[:div.modal-overlay
[:div.modal-container {:ref container}]]]))

View file

@ -0,0 +1,181 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.onboarding.team-choice
(:require
[app.common.spec :as us]
[app.main.data.dashboard :as dd]
[app.main.data.messages :as dm]
[app.main.data.modal :as modal]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.timers :as tm]
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
(s/def ::name ::us/not-empty-string)
(s/def ::team-form
(s/keys :req-un [::name]))
(mf/defc onboarding-choice-modal
{::mf/register modal/components
::mf/register-as :onboarding-choice}
[]
(let [;; When user choices the option of `fly solo`, we proceed to show
;; the onboarding templates modal.
on-fly-solo
(fn []
(tm/schedule 400 #(st/emit! (modal/show {:type :onboarding-templates}))))
;; When user choices the option of `team up`, we proceed to show
;; the team creation modal.
on-team-up
(fn []
(st/emit! (modal/show {:type :onboarding-team})))
]
[:div.modal-overlay
[:div.modal-container.onboarding.final.animated.fadeInUp
[:div.modal-top
[:h1 (tr "onboarding.welcome.title")]
[:p (tr "onboarding.welcome.desc3")]]
[:div.modal-columns
[:div.modal-left
[:div.content-button {:on-click on-fly-solo}
[:h2 (tr "onboarding.choice.fly-solo")]
[:p (tr "onboarding.choice.fly-solo-desc")]]]
[:div.modal-right
[:div.content-button {:on-click on-team-up}
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
(mf/defc onboarding-team-modal
{::mf/register modal/components
::mf/register-as :onboarding-team}
[]
(let [form (fm/use-form :spec ::team-form
:initial {})
on-submit
(mf/use-callback
(fn [form _]
(let [tname (get-in @form [:clean-data :name])]
(st/emit! (modal/show {:type :onboarding-team-invitations :name tname})))))]
[:div.modal-overlay
[:div.modal-container.onboarding-team
[:div.title
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]
[:& fm/form {:form form
:on-submit on-submit}
[:div.team-row
[:& fm/input {:type "text"
:name :name
:label (tr "onboarding.team-input-placeholder")}]]
[:div.buttons
[:button.btn-secondary.btn-large
{:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
(tr "labels.cancel")]
[:& fm/submit-button
{:label (tr "labels.next")}]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
(defn get-available-roles
[]
[{:value "editor" :label (tr "labels.editor")}
{:value "admin" :label (tr "labels.admin")}])
(s/def ::email ::us/email)
(s/def ::role ::us/keyword)
(s/def ::invite-form
(s/keys :req-un [::role ::email]))
;; This is the final step of team creation, consists in provide a
;; shortcut for invite users.
(mf/defc onboarding-team-invitations-modal
{::mf/register modal/components
::mf/register-as :onboarding-team-invitations}
[{:keys [name] :as props}]
(let [initial (mf/use-memo (constantly
{:role "editor"
:name name}))
form (fm/use-form :spec ::invite-form
:initial initial)
roles (mf/use-memo #(get-available-roles))
on-success
(mf/use-callback
(fn [_form response]
(let [project-id (:default-project-id response)
team-id (:id response)]
(st/emit!
(modal/hide)
(rt/nav :dashboard-projects {:team-id team-id}))
(tm/schedule 400 #(st/emit!
(modal/show {:type :onboarding-templates
:project-id project-id}))))))
on-error
(mf/use-callback
(fn [_form _response]
(st/emit! (dm/error "Error on creating team."))))
;; The SKIP branch only creates the team, without invitations
on-skip
(mf/use-callback
(fn [_]
(let [mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}
params {:name name}]
(st/emit! (dd/create-team (with-meta params mdata))))))
;; The SUBMIT branch creates the team with the invitations
on-submit
(mf/use-callback
(fn [form _]
(let [mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}
params (:clean-data @form)]
(st/emit! (dd/create-team-with-invitations (with-meta params mdata))))))]
[:div.modal-overlay
[:div.modal-container.onboarding-team
[:div.title
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]
[:& fm/form {:form form
:on-submit on-submit}
[:div.invite-row
[:& fm/input {:name :email
:label (tr "labels.email")}]
[:& fm/select {:name :role
:options roles}]]
[:div.buttons
[:button.btn-secondary.btn-large
{:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
(tr "labels.cancel")]
[:& fm/submit-button
{:label (tr "labels.create")}]]
[:div.skip-action
{:on-click on-skip}
[:div.action "Skip and invite later"]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))

View file

@ -0,0 +1,88 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.onboarding.templates
(:require
[app.config :as cf]
[app.main.data.dashboard :as dd]
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]]
[beicon.core :as rx]
[rumext.alpha :as mf]))
(mf/defc template-item
[{:keys [name path image project-id]}]
(let [downloading? (mf/use-state false)
link (str (assoc cf/public-uri :path path))
on-finish-import
(fn []
(st/emit! (dd/fetch-recent-files)))
open-import-modal
(fn [file]
(st/emit! (modal/show
{:type :import
:project-id project-id
:files [file]
:on-finish-import on-finish-import})))
on-click
(fn []
(reset! downloading? true)
(->> (http/send! {:method :get :uri link :response-type :blob :mode :no-cors})
(rx/subs (fn [{:keys [body] :as response}]
(open-import-modal {:name name :uri (dom/create-uri body)}))
(fn [error]
(js/console.log "error" error))
(fn []
(reset! downloading? false)))))
]
[:div.template-item
[:div.template-item-content
[:img {:src image}]]
[:div.template-item-title
[:div.label name]
(if @downloading?
[:div.action "Fetching..."]
[:div.action {:on-click on-click} "+ Add to drafts"])]]))
(mf/defc onboarding-templates-modal
{::mf/wrap-props false
::mf/register modal/components
::mf/register-as :onboarding-templates}
;; NOTE: the project usually comes empty, it only comes fullfilled
;; when a user creates a new team just after signup.
[{:keys [project-id] :as props}]
(let [close-fn (mf/use-callback #(st/emit! (modal/hide)))
profile (mf/deref refs/profile)
project-id (or project-id (:default-project-id profile))]
[:div.modal-overlay
[:div.modal-container.onboarding-templates
[:div.modal-header
[:div.modal-close-button
{:on-click close-fn} i/close]]
[:div.modal-content
[:h3 (tr "onboarding.templates.title")]
[:p (tr "onboarding.templates.subtitle")]
[:div.templates
[:& template-item
{:path "/github/penpot-files/Penpot-Design-system.penpot"
:image "https://penpot.app/images/libraries/cover-ds-penpot.jpg"
:name "Penpot Design System"
:project-id project-id}]
[:& template-item
{:path "/github/penpot-files/Material-Design-Kit.penpot"
:image "https://penpot.app/images/libraries/cover-material.jpg"
:name "Material Design Kit"
:project-id project-id}]]]]]))

View file

@ -0,0 +1,83 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.releases
(:require
[app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.store :as st]
[app.main.ui.releases.common :as rc]
[app.main.ui.releases.v1-4]
[app.main.ui.releases.v1-5]
[app.main.ui.releases.v1-6]
[app.main.ui.releases.v1-7]
[app.main.ui.releases.v1-8]
[app.main.ui.releases.v1-9]
[app.util.object :as obj]
[app.util.timers :as tm]
[rumext.alpha :as mf]))
;;; --- RELEASE NOTES MODAL
(mf/defc release-notes
[{:keys [version] :as props}]
(let [slide (mf/use-state :start)
klass (mf/use-state "fadeInDown")
navigate
(mf/use-callback #(reset! slide %))
next
(mf/use-callback
(mf/deps slide)
(fn []
(if (= @slide :start)
(navigate 0)
(navigate (inc @slide)))))
finish
(mf/use-callback
(st/emitf (modal/hide)
(du/mark-onboarding-as-viewed {:version version})))
]
(mf/use-effect
(mf/deps)
(fn []
(st/emitf (du/mark-onboarding-as-viewed {:version version}))))
(mf/use-layout-effect
(mf/deps @slide)
(fn []
(when (not= :start @slide)
(reset! klass "fadeIn"))
(let [sem (tm/schedule 300 #(reset! klass nil))]
(fn []
(reset! klass nil)
(tm/dispose! sem)))))
(rc/render-release-notes
{:next next
:navigate navigate
:finish finish
:klass klass
:slide slide
:version version})))
(mf/defc release-notes-modal
{::mf/wrap-props false
::mf/register modal/components
::mf/register-as :release-notes}
[props]
(let [versions (methods rc/render-release-notes)
version (obj/get props "version")]
(when (contains? versions version)
[:div.relnotes
[:> release-notes props]])))
(defmethod rc/render-release-notes "0.0"
[params]
(rc/render-release-notes (assoc params :version "1.10")))

View file

@ -6,10 +6,9 @@
(ns app.main.ui.static (ns app.main.ui.static
(:require (:require
[app.main.data.users :as du]
[app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.globals :as globals]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.object :as obj] [app.util.object :as obj]
[app.util.router :as rt] [app.util.router :as rt]
@ -19,14 +18,7 @@
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [children (obj/get props "children") (let [children (obj/get props "children")
on-click (mf/use-callback on-click (mf/use-callback #(set! (.-href globals/location) ""))]
(fn []
(let [profile (deref refs/profile)]
(if (du/is-authenticated? profile)
(let [team-id (du/get-current-team-id profile)]
(st/emit! (rt/nav :dashboard-projects {:team-id team-id})))
(st/emit! (rt/nav :auth-login {}))))))]
[:section.exception-layout [:section.exception-layout
[:div.exception-header [:div.exception-header
{:on-click on-click} {:on-click on-click}

View file

@ -166,7 +166,7 @@
(defn append-child! (defn append-child!
[el child] [el child]
(.appendChild el child)) (.appendChild ^js el child))
(defn get-first-child (defn get-first-child
[el] [el]

View file

@ -37,10 +37,16 @@
[& {:keys [initial] :as opts}] [& {:keys [initial] :as opts}]
(let [state (mf/useState 0) (let [state (mf/useState 0)
render (aget state 1) render (aget state 1)
state-ref (mf/use-ref {:data (if (fn? initial) (initial) initial)
:errors {} get-state (mf/use-callback
:touched {}}) (mf/deps initial)
form (mf/use-memo #(create-form-mutator state-ref render opts))] (fn []
{:data (if (fn? initial) (initial) initial)
:errors {}
:touched {}}))
state-ref (mf/use-ref (get-state))
form (mf/use-memo (mf/deps initial) #(create-form-mutator state-ref render get-state opts))]
(mf/use-effect (mf/use-effect
(mf/deps initial) (mf/deps initial)
@ -72,7 +78,7 @@
(not= cleaned ::s/invalid)))))) (not= cleaned ::s/invalid))))))
(defn- create-form-mutator (defn- create-form-mutator
[state-ref render opts] [state-ref render get-state opts]
(reify (reify
IDeref IDeref
(-deref [_] (-deref [_]
@ -80,7 +86,9 @@
IReset IReset
(-reset! [it new-value] (-reset! [it new-value]
(mf/set-ref-val! state-ref new-value) (if (nil? new-value)
(mf/set-ref-val! state-ref (get-state))
(mf/set-ref-val! state-ref new-value))
(render inc)) (render inc))
ISwap ISwap

View file

@ -88,6 +88,7 @@
:credentials credentials :credentials credentials
:referrerPolicy "no-referrer" :referrerPolicy "no-referrer"
:signal signal}] :signal signal}]
(-> (js/fetch (str uri) params) (-> (js/fetch (str uri) params)
(p/then (fn [response] (p/then (fn [response]
(vreset! abortable? false) (vreset! abortable? false)

View file

@ -19,17 +19,16 @@
;; --- Router API ;; --- Router API
(defn map->Match
[data]
(r/map->Match data))
(defn resolve (defn resolve
([router id] (resolve router id {} {})) ([router id] (resolve router id {} {}))
([router id path-params] (resolve router id path-params {})) ([router id path-params] (resolve router id path-params {}))
([router id path-params query-params] ([router id path-params query-params]
(when-let [match (r/match-by-name router id path-params)] (when-let [match (r/match-by-name router id path-params)]
(if (empty? query-params) (r/match->path match query-params))))
(r/match->path match)
(let [query (u/map->query-string query-params)]
(-> (u/uri (r/match->path match))
(assoc :query query)
(str)))))))
(defn create (defn create
[routes] [routes]
@ -161,7 +160,3 @@
(e/unlistenByKey key))))) (e/unlistenByKey key)))))
(rx/take-until stoper) (rx/take-until stoper)
(rx/subs #(on-change router %))))))) (rx/subs #(on-change router %)))))))

View file

@ -3245,4 +3245,16 @@ msgid "workspace.updates.update"
msgstr "Update" msgstr "Update"
msgid "workspace.viewport.click-to-close-path" msgid "workspace.viewport.click-to-close-path"
msgstr "Click to close the path" msgstr "Click to close the path"
msgid "errors.team-leave.member-does-not-exists"
msgstr "The member you try to assign does not exist."
msgid "errors.team-leave.owner-cant-leave"
msgstr "Owner can't leave team, you must reassign the owner role."
msgid "errors.team-leave.insufficient-members"
msgstr "Insufficient members to leave team, you probably want to delete it."
msgid "errors.auth.unable-to-login"
msgstr "Looks like you are not authenticated or session expired."