diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj
index 906ec83ad..12b8e9d64 100644
--- a/backend/src/app/rpc/mutations/profile.clj
+++ b/backend/src/app/rpc/mutations/profile.clj
@@ -16,7 +16,6 @@
[app.loggers.audit :as audit]
[app.media :as media]
[app.metrics :as mtx]
- [app.rpc.mutations.projects :as projects]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.setup.initial-data :as sid]
@@ -256,28 +255,15 @@
:code :email-already-exists
:cause e)))))))
-
(defn create-profile-relations
[conn profile]
- (let [team (teams/create-team conn {:profile-id (:id profile)
- :name "Default"
- :is-default true})
- project (projects/create-project conn {:profile-id (:id profile)
- :team-id (:id team)
- :name "Drafts"
- :is-default true})
- params {:team-id (:id team)
- :profile-id (:id profile)
- :project-id (:id project)
- :role :owner}]
-
- (teams/create-team-role conn params)
- (projects/create-project-role conn params)
-
+ (let [team (teams/create-team conn {:profile-id (:id profile)
+ :name "Default"
+ :is-default true})]
(-> profile
(profile/strip-private-attrs)
(assoc :default-team-id (:id team))
- (assoc :default-project-id (:id project)))))
+ (assoc :default-project-id (:default-project-id team)))))
;; --- MUTATION: Login
diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj
index 625a13af6..0f44afb17 100644
--- a/backend/src/app/rpc/mutations/teams.clj
+++ b/backend/src/app/rpc/mutations/teams.clj
@@ -32,6 +32,7 @@
;; --- Mutation: Create Team
(declare create-team)
+(declare create-team-entry)
(declare create-team-role)
(declare create-team-default-project)
@@ -42,15 +43,21 @@
(sv/defmethod ::create-team
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
- (let [team (create-team conn params)
- params (assoc params
- :team-id (:id team)
- :role :owner)]
- (create-team-role conn params)
- (create-team-default-project conn params)
- team)))
+ (create-team conn params)))
(defn create-team
+ "This is a complete team creation process, it creates the team
+ object and all related objects (default role and default project)."
+ [conn params]
+ (let [team (create-team-entry conn params)
+ params (assoc params
+ :team-id (:id team)
+ :role :owner)
+ project (create-team-default-project conn params)]
+ (create-team-role conn params)
+ (assoc team :default-project-id (:id project))))
+
+(defn- create-team-entry
[conn {:keys [id name is-default] :as params}]
(let [id (or id (uuid/next))
is-default (if (boolean? is-default) is-default false)]
@@ -59,23 +66,24 @@
:name name
:is-default is-default})))
-(defn create-team-role
+(defn- create-team-role
[conn {:keys [team-id profile-id role] :as params}]
(let [params {:team-id team-id
:profile-id profile-id}]
(->> (perms/assign-role-flags params role)
(db/insert! conn :team-profile-rel))))
-(defn create-team-default-project
+(defn- create-team-default-project
[conn {:keys [team-id profile-id] :as params}]
(let [project {:id (uuid/next)
:team-id team-id
:name "Drafts"
- :is-default true}]
- (projects/create-project conn project)
+ :is-default true}
+ project (projects/create-project conn project)]
(projects/create-project-role conn {:project-id (:id project)
:profile-id profile-id
- :role :owner})))
+ :role :owner})
+ project))
;; --- Mutation: Update Team
@@ -293,28 +301,18 @@
;; --- Mutation: Invite Member
+(declare create-team-invitation)
+
(s/def ::email ::us/email)
(s/def ::invite-team-member
(s/keys :req-un [::profile-id ::team-id ::email ::role]))
(sv/defmethod ::invite-team-member
- [{:keys [pool tokens] :as cfg} {:keys [profile-id team-id email role] :as params}]
+ [{:keys [pool] :as cfg} {:keys [profile-id team-id email role] :as params}]
(db/with-atomic [conn pool]
(let [perms (teams/get-permissions conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id)
- member (profile/retrieve-profile-data-by-email conn email)
- team (db/get-by-id conn :team team-id)
- itoken (tokens :generate
- {:iss :team-invitation
- :exp (dt/in-future "48h")
- :profile-id (:id profile)
- :role role
- :team-id team-id
- :member-email (:email member email)
- :member-id (:id member)})
- ptoken (tokens :generate-predefined
- {:iss :profile-identity
- :profile-id (:id profile)})]
+ team (db/get-by-id conn :team team-id)]
(when-not (:is-admin perms)
(ex/raise :type :validation
@@ -326,24 +324,71 @@
:code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
- (when (and member (not (eml/allow-send-emails? conn member)))
- (ex/raise :type :validation
- :code :member-is-muted
- :hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
-
- ;; Secondly check if the invited member email is part of the
- ;; global spam/bounce report.
- (when (eml/has-bounce-reports? conn email)
- (ex/raise :type :validation
- :code :email-has-permanent-bounces
- :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
-
- (eml/send! {::eml/conn conn
- ::eml/factory eml/invite-to-team
- :public-uri (:public-uri cfg)
- :to email
- :invited-by (:fullname profile)
- :team (:name team)
- :token itoken
- :extra-data ptoken})
+ (create-team-invitation
+ (assoc cfg
+ :email email
+ :conn conn
+ :team team
+ :profile profile
+ :role role))
nil)))
+
+(defn- create-team-invitation
+ [{:keys [conn tokens team profile role email] :as cfg}]
+ (let [member (profile/retrieve-profile-data-by-email conn email)
+ itoken (tokens :generate
+ {:iss :team-invitation
+ :exp (dt/in-future "48h")
+ :profile-id (:id profile)
+ :role role
+ :team-id (:id team)
+ :member-email (:email member email)
+ :member-id (:id member)})
+ ptoken (tokens :generate-predefined
+ {:iss :profile-identity
+ :profile-id (:id profile)})]
+
+ (when (and member (not (eml/allow-send-emails? conn member)))
+ (ex/raise :type :validation
+ :code :member-is-muted
+ :hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
+
+ ;; Secondly check if the invited member email is part of the
+ ;; global spam/bounce report.
+ (when (eml/has-bounce-reports? conn email)
+ (ex/raise :type :validation
+ :code :email-has-permanent-bounces
+ :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
+
+ (eml/send! {::eml/conn conn
+ ::eml/factory eml/invite-to-team
+ :public-uri (:public-uri cfg)
+ :to email
+ :invited-by (:fullname profile)
+ :team (:name team)
+ :token itoken
+ :extra-data ptoken})))
+
+
+;; --- Mutation: Create Team & Invite Members
+
+(s/def ::emails ::us/set-of-emails)
+(s/def ::create-team-and-invite-members
+ (s/and ::create-team (s/keys :req-un [::emails ::role])))
+
+(sv/defmethod ::create-team-and-invite-members
+ [{:keys [pool] :as cfg} {:keys [profile-id emails role] :as params}]
+ (db/with-atomic [conn pool]
+ (let [team (create-team conn params)
+ profile (db/get-by-id conn :profile profile-id)]
+
+ ;; Create invitations for all provided emails.
+ (doseq [email emails]
+ (create-team-invitation
+ (assoc cfg
+ :conn conn
+ :team team
+ :profile profile
+ :email email
+ :role role)))
+ team)))
diff --git a/backend/src/app/rpc/queries/projects.clj b/backend/src/app/rpc/queries/projects.clj
index 47886784a..1e92869e4 100644
--- a/backend/src/app/rpc/queries/projects.clj
+++ b/backend/src/app/rpc/queries/projects.clj
@@ -79,12 +79,14 @@
where f.project_id = p.id
and deleted_at is null) as count
from project as p
+ inner join team as t on (t.id = p.team_id)
left join team_project_profile_rel as tpp
on (tpp.project_id = p.id and
tpp.team_id = p.team_id and
tpp.profile_id = ?)
where p.team_id = ?
and p.deleted_at is null
+ and t.deleted_at is null
order by p.modified_at desc")
(defn retrieve-projects
@@ -108,26 +110,26 @@
(def sql:all-projects
"select p1.*, t.name as team_name, t.is_default as is_default_team
from project as p1
- inner join team as t
- on t.id = p1.team_id
+ inner join team as t on (t.id = p1.team_id)
where t.id in (select team_id
from team_profile_rel as tpr
where tpr.profile_id = ?
and (tpr.can_edit = true or
tpr.is_owner = true or
tpr.is_admin = true))
+ and t.deleted_at is null
and p1.deleted_at is null
union
select p2.*, t.name as team_name, t.is_default as is_default_team
from project as p2
- inner join team as t
- on t.id = p2.team_id
+ inner join team as t on (t.id = p2.team_id)
where p2.id in (select project_id
from project_profile_rel as ppr
where ppr.profile_id = ?
and (ppr.can_edit = true or
ppr.is_owner = true or
ppr.is_admin = true))
+ and t.deleted_at is null
and p2.deleted_at is null
order by team_name, name;")
diff --git a/backend/test/app/services_projects_test.clj b/backend/test/app/services_projects_test.clj
index 5f23577f3..a59991e38 100644
--- a/backend/test/app/services_projects_test.clj
+++ b/backend/test/app/services_projects_test.clj
@@ -43,7 +43,7 @@
(t/is (nil? (:error out)))
(let [result (:result out)]
- (t/is (= 1 (count result)))
+ (t/is (= 2 (count result)))
(t/is project-id (get-in result [0 :id]))
(t/is (= "test project" (get-in result [0 :name])))))
@@ -55,15 +55,15 @@
(t/is (nil? (:error out)))
(let [result (:result out)]
- (t/is (= 2 (count result)))
+ (t/is (= 3 (count result)))
(t/is (not= project-id (get-in result [0 :id])))
(t/is (= "Drafts" (get-in result [0 :name])))
(t/is (= "Default" (get-in result [0 :team-name])))
(t/is (= true (get-in result [0 :is-default-team])))
- (t/is project-id (get-in result [1 :id]))
- (t/is (= "test project" (get-in result [1 :name])))
- (t/is (= "team1" (get-in result [1 :team-name])))
- (t/is (= false (get-in result [1 :is-default-team])))))
+ (t/is project-id (get-in result [2 :id]))
+ (t/is (= "test project" (get-in result [2 :name])))
+ (t/is (= "team1" (get-in result [2 :team-name])))
+ (t/is (= false (get-in result [2 :is-default-team])))))
;; rename project
(let [data {::th/type :rename-project
@@ -95,7 +95,7 @@
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
- ;; query a list of projects after delete"
+ ;; query a list of projects after delete
(let [data {::th/type :projects
:team-id (:id team)
:profile-id (:id profile)}
@@ -103,7 +103,7 @@
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
- (t/is (= 0 (count result)))))
+ (t/is (= 1 (count result)))))
))
(t/deftest permissions-checks-create-project
diff --git a/backend/test/app/services_teams_test.clj b/backend/test/app/services_teams_test.clj
index 6dd7470da..6e2aaeea7 100644
--- a/backend/test/app/services_teams_test.clj
+++ b/backend/test/app/services_teams_test.clj
@@ -130,7 +130,7 @@
(let [result (task {:max-age (dt/duration {:minutes 1})})]
(t/is (nil? result)))
- ;; query the list of projects of a after hard deletion
+ ;; query the list of projects after hard deletion
(let [data {::th/type :projects
:team-id (:id team)
:profile-id (:id profile1)}
diff --git a/backend/test/app/test_helpers.clj b/backend/test/app/test_helpers.clj
index 43334b883..f503e5d66 100644
--- a/backend/test/app/test_helpers.clj
+++ b/backend/test/app/test_helpers.clj
@@ -126,7 +126,8 @@
:password "123123"
:is-demo false}
params)]
- (->> (#'profile/create-profile conn params)
+ (->> params
+ (#'profile/create-profile conn)
(#'profile/create-profile-relations conn)))))
(defn create-project*
@@ -159,15 +160,10 @@
([i params] (create-team* *pool* i params))
([conn i {:keys [profile-id] :as params}]
(us/assert uuid? profile-id)
- (let [id (mk-uuid "team" i)
- team (#'teams/create-team conn {:id id
- :profile-id profile-id
- :name (str "team" i)})]
- (#'teams/create-team-role conn
- {:team-id id
- :profile-id profile-id
- :role :owner})
- team)))
+ (let [id (mk-uuid "team" i)]
+ (teams/create-team conn {:id id
+ :profile-id profile-id
+ :name (str "team" i)}))))
(defn create-file-media-object*
([params] (create-file-media-object* *pool* params))
@@ -350,3 +346,11 @@
(defn reset-mock!
[m]
(reset! m @(mk/make-mock {})))
+
+(defn pause
+ []
+ (let [^java.io.Console cnsl (System/console)]
+ (println "[waiting RETURN]")
+ (.readLine cnsl)
+ nil))
+
diff --git a/frontend/resources/images/on-solo-hover.svg b/frontend/resources/images/on-solo-hover.svg
new file mode 100644
index 000000000..d75553178
--- /dev/null
+++ b/frontend/resources/images/on-solo-hover.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/on-solo.svg b/frontend/resources/images/on-solo.svg
new file mode 100644
index 000000000..08d76020d
--- /dev/null
+++ b/frontend/resources/images/on-solo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/on-teamup-hover.svg b/frontend/resources/images/on-teamup-hover.svg
new file mode 100644
index 000000000..c012588a0
--- /dev/null
+++ b/frontend/resources/images/on-teamup-hover.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/on-teamup.svg b/frontend/resources/images/on-teamup.svg
new file mode 100644
index 000000000..10b85bf13
--- /dev/null
+++ b/frontend/resources/images/on-teamup.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/ph-file.svg b/frontend/resources/images/ph-file.svg
new file mode 100644
index 000000000..6859f17f5
--- /dev/null
+++ b/frontend/resources/images/ph-file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/ph-left.svg b/frontend/resources/images/ph-left.svg
new file mode 100644
index 000000000..7853a1b55
--- /dev/null
+++ b/frontend/resources/images/ph-left.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/ph-right.svg b/frontend/resources/images/ph-right.svg
new file mode 100644
index 000000000..6e2852f31
--- /dev/null
+++ b/frontend/resources/images/ph-right.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/resources/styles/main/partials/dashboard-grid.scss b/frontend/resources/styles/main/partials/dashboard-grid.scss
index 032bab70c..bbf80ec67 100644
--- a/frontend/resources/styles/main/partials/dashboard-grid.scss
+++ b/frontend/resources/styles/main/partials/dashboard-grid.scss
@@ -335,6 +335,20 @@
padding: 3rem;
justify-content: center;
+ &.drafts {
+ background-image: url("/images/ph-left.svg"), url("/images/ph-right.svg");
+ background-position: 15% bottom, 85% top;
+ background-repeat: no-repeat;
+ .text {
+ p {
+ max-width: 360px;
+ text-align: center;
+ font-size: $fs16;
+ }
+ }
+ }
+
+
svg {
width: 36px;
height: 36px;
@@ -346,5 +360,10 @@
color: $color-gray-30;
font-size: $fs16;
}
+
+ img.ph-files {
+ height: 150px;
+ margin-right: calc(100% - 148px);
+ }
}
diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss
index 27da8245d..bff16696c 100644
--- a/frontend/resources/styles/main/partials/modal.scss
+++ b/frontend/resources/styles/main/partials/modal.scss
@@ -63,7 +63,7 @@
display: flex;
flex-direction: column;
width: 448px;
- background-color: $color-dashboard;
+ background-color: $color-white;
.modal-header {
align-items: center;
@@ -705,7 +705,7 @@
background-color: $color-white;
box-shadow: 0 10px 10px rgba(0,0,0,.2);
display: flex;
- min-height: 370px;
+ min-height: 420px;
flex-direction: row;
font-family: "sourcesanspro", sans-serif;
min-width: 620px;
@@ -824,21 +824,93 @@
}
&.final {
+ // TODO: Juan revisa TODA esta parte
+
padding: $size-5 0 0 0;
+ flex-direction: column;
+
+ .modal-top {
+ padding-top: 40px;
+ color: $color-gray-60;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ h1 {
+ font-family: 'worksans', sans-serif;
+ font-weight: 700;
+ font-size: 27px;
+ margin-bottom: $size-3;
+ }
+ p {
+ font-family: 'worksans', sans-serif;
+ font-weight: 500;
+ font-size: $fs18;
+ }
+
+ }
+
+ .modal-columns {
+ display: flex;
+ margin: 17px;
+
+ .modal-left {
+ background-image: url("/images/on-solo.svg");
+ background-position: left top;
+ background-size: 11%;
+ }
+
+ .modal-left:hover {
+ background-image: url("/images/on-solo-hover.svg");
+ background-size: 15%;
+ }
+
+ .modal-right {
+ background-image: url("/images/on-teamup.svg");
+ background-position: right top;
+ background-size: 28%;
+ }
+
+ .modal-right:hover {
+ background-image: url("/images/on-teamup-hover.svg");
+ background-size: 32%;
+ }
+
+ .modal-right,
+ .modal-left {
+ background-repeat: no-repeat;
+ border-radius: $br-medium;
+ transition: all ease .3s;
+ &:hover {
+ background-color: $color-primary;
+ }
+ }
+ }
+
+ .modal-left {
+ margin-right: 35px;
+ }
.modal-left,
.modal-right {
+ justify-content: center;
align-items: center;
background-color: $color-white;
color: $color-black;
flex: 1;
flex-direction: column;
- overflow: visible;
- padding: $size-6 40px;
+ // overflow: visible;
+ // padding: $size-6 40px;
text-align: center;
+ border: 1px solid $color-gray-10;
+ border-radius: 2px;
+ min-height: 180px;
+ width: 233px;
+ cursor: pointer;
+
h2 {
- font-weight: 900;
+ font-weight: 700;
margin-bottom: $size-5;
font-size: $fs24;
}
@@ -847,12 +919,6 @@
font-size: $fs14;
}
- .btn-primary {
- margin-bottom: 0;
- margin-top: auto;
- width: 200px;
- }
-
img {
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.25);
border-radius: $br-medium;
@@ -861,26 +927,6 @@
width: 150px;
}
}
-
- .modal-left {
- border-right: 1px solid $color-gray-10;
-
- form {
- align-items: center;
- display: flex;
- flex-direction: column;
- margin-top: auto;
-
- .custom-input {
- margin-bottom: $size-4;
-
- input {
- width: 200px;
- }
- }
- }
- }
-
}
}
@@ -899,3 +945,193 @@
.relnotes .onboarding {
height: 420px;
}
+
+.onboarding-templates {
+ position: fixed;
+ top: 0;
+ right: 0;
+ width: 348px;
+ height: 100vh;
+
+ .modal-close-button {
+ width: 34px;
+ height: 34px;
+ margin-right: 13px;
+ margin-top: 13px;
+ svg {
+ width: 24px;
+ height: 24px;
+ }
+ }
+
+ .modal-header {
+ height: unset;
+ border-radius: unset;
+ justify-content: flex-end;
+ }
+
+ .modal-content {
+ border: 0px;
+ padding: 0px 25px;
+ background-color: $color-white;
+ flex-grow: 1;
+
+ p, h3 {
+ color: $color-gray-60;
+ text-align: center;
+ }
+
+ h3 {
+ font-size: $fs18;
+ font-weight: bold;
+ }
+
+ p {
+ font-size: $fs16;
+ }
+
+
+ .templates {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-top: 8%;
+ }
+
+ .template-item {
+ width: 275px;
+ border: 1px solid $color-gray-10;
+
+ display: flex;
+ flex-direction: column;
+ text-align: left;
+ border-radius: $br-small;
+
+ &:not(:last-child) {
+ margin-bottom: 22px;
+ }
+ }
+
+ .template-item-content {
+ // height: 144px;
+ flex-grow: 1;
+
+ img {
+ border-radius: $br-small $br-small 0 0;
+ }
+ }
+
+ .template-item-title {
+ padding: 6px 12px;
+ height: 64px;
+ border-top: 1px solid $color-gray-10;
+
+ .label {
+ color: $color-black;
+ padding: 0px 4px;
+ font-size: $fs16;
+ display: flex;
+ }
+
+ .action {
+ color: $color-primary-dark;
+ cursor: pointer;
+ font-size: $fs14;
+ font-weight: 600;
+ display: flex;
+ justify-content: flex-end;
+ margin-top: $size-2;
+ }
+
+ }
+ }
+}
+
+
+.onboarding-team {
+ display: flex;
+ min-width: 620px;
+ min-height: 420px;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+
+ .title {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 408px;
+
+ color: $color-gray-60;
+ h2 {
+ font-weight: 700;
+ padding-bottom: 10px;
+ }
+
+ p {
+ text-align: center;
+ font-size: $fs18;
+ }
+ }
+
+ form {
+ display: flex;
+ flex-direction: column;
+ margin-top: $size-6;
+
+ .buttons {
+ margin-top: 30px;
+ display: flex;
+ justify-content: flex-end;
+
+ > *:not(:last-child) {
+ margin-right: 13px;
+ }
+
+ input { margin-bottom: unset; }
+ input[type=submit] {
+ }
+
+ .btn-primary {
+ width: 117px;
+ }
+ }
+
+ .team-row {
+ .custom-input {
+ width: 459px;
+ }
+ }
+
+ .invite-row {
+ display: flex;
+ justify-content: space-between;
+
+ > *:not(:last-child) {
+ margin-right: 13px;
+ }
+
+ .custom-input {
+ width: 321px;
+ }
+
+ .custom-select {
+ width: 118px;
+ }
+ }
+
+ .skip-action {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 15px;
+ .action {
+ color: $color-primary-dark;
+ font-weight: 500;
+ font-size: $fs16;
+ cursor: pointer;
+ }
+ }
+
+ }
+}
diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs
index 7025dc5ea..e15b8d7fb 100644
--- a/frontend/src/app/main/data/dashboard.cljs
+++ b/frontend/src/app/main/data/dashboard.cljs
@@ -300,6 +300,28 @@
(rx/map team-created)
(rx/catch on-error))))))
+;; --- EVENT: create-team-with-invitations
+
+;; NOTE: right now, it only handles a single email, in a near future
+;; this will be changed to the ability to specify multiple emails.
+
+(defn create-team-with-invitations
+ [{:keys [name email role] :as params}]
+ (us/assert string? name)
+ (ptk/reify ::create-team-with-invitations
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [{:keys [on-success on-error]
+ :or {on-success identity
+ on-error rx/throw}} (meta params)
+ params {:name name
+ :emails #{email}
+ :role role}]
+ (->> (rp/mutation! :create-team-and-invite-members params)
+ (rx/tap on-success)
+ (rx/map team-created)
+ (rx/catch on-error))))))
+
;; --- EVENT: update-team
(defn update-team
diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs
index 3612d8490..adc3cfaf6 100644
--- a/frontend/src/app/main/refs.cljs
+++ b/frontend/src/app/main/refs.cljs
@@ -61,12 +61,6 @@
(def dashboard-search-result
(l/derived :dashboard-search-result st/state))
-(def dashboard-team
- (l/derived (fn [state]
- (let [team-id (:current-team-id state)]
- (get-in state [:teams team-id])))
- st/state))
-
(def dashboard-team-stats
(l/derived :dashboard-team-stats st/state))
diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs
index b005dc11c..545471615 100644
--- a/frontend/src/app/main/ui.cljs
+++ b/frontend/src/app/main/ui.cljs
@@ -72,7 +72,10 @@
:dashboard-team-settings)
[:*
#_[:div.modal-wrapper
- [:& app.main.ui.onboarding/release-notes-modal {:version "1.8"}]]
+ #_[:& app.main.ui.onboarding/onboarding-templates-modal]
+ #_[:& app.main.ui.onboarding/onboarding-modal]
+ #_[:& app.main.ui.onboarding/onboarding-team-modal]
+ ]
[:& dashboard {:route route}]]
:viewer
diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs
index 9d2f00178..1292c6eaf 100644
--- a/frontend/src/app/main/ui/dashboard.cljs
+++ b/frontend/src/app/main/ui/dashboard.cljs
@@ -104,11 +104,11 @@
(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)})))))))
+ (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-project-id) {:value project-id}
-
;; NOTE: dashboard events and other related functions assumes
;; that the team is a implicit context variable that is
;; available using react context or accessing
diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs
index 4ce733033..ef413c78c 100644
--- a/frontend/src/app/main/ui/dashboard/files.cljs
+++ b/frontend/src/app/main/ui/dashboard/files.cljs
@@ -120,6 +120,6 @@
[:*
[:& header {:team team :project project}]
[:section.dashboard-container
- [:& grid {:project-id (:id project)
+ [:& grid {:project project
:files files}]]]))
diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs
index 3bd48a1ce..f91e1de5c 100644
--- a/frontend/src/app/main/ui/dashboard/grid.cljs
+++ b/frontend/src/app/main/ui/dashboard/grid.cljs
@@ -15,6 +15,7 @@
[app.main.ui.dashboard.file-menu :refer [file-menu]]
[app.main.ui.dashboard.import :refer [use-import-file]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
+ [app.main.ui.dashboard.placeholder :refer [empty-placeholder loading-placeholder]]
[app.main.ui.icons :as i]
[app.main.worker :as wrk]
[app.util.dom :as dom]
@@ -195,24 +196,10 @@
:on-edit on-edit
:on-menu-close on-menu-close}])]]]))
-(mf/defc empty-placeholder
- [{:keys [dragging?] :as props}]
- (if-not dragging?
- [:div.grid-empty-placeholder
- [:div.icon i/file-html]
- [:div.text (tr "dashboard.empty-files")]]
- [:div.grid-row.no-wrap
- [:div.grid-item]]))
-
-(mf/defc loading-placeholder
- []
- [:div.grid-empty-placeholder
- [:div.icon i/loader]
- [:div.text (tr "dashboard.loading-files")]])
-
(mf/defc grid
- [{:keys [files project-id] :as props}]
- (let [dragging? (mf/use-state false)
+ [{:keys [files project] :as props}]
+ (let [dragging? (mf/use-state false)
+ project-id (:id project)
on-finish-import
(mf/use-callback
@@ -272,7 +259,7 @@
:navigate? true}])]
:else
- [:& empty-placeholder])]))
+ [:& empty-placeholder {:default? (:is-default project)}])]))
(mf/defc line-grid-row
[{:keys [files selected-files on-load-more dragging?] :as props}]
@@ -330,8 +317,11 @@
(tr "dashboard.show-all-files")]])]))
(mf/defc line-grid
- [{:keys [project-id team-id files on-load-more] :as props}]
+ [{:keys [project team files on-load-more] :as props}]
(let [dragging? (mf/use-state false)
+ project-id (:id project)
+ team-id (:id team)
+
selected-files (mf/deref refs/dashboard-selected-files)
selected-project (mf/deref refs/dashboard-selected-project)
@@ -413,5 +403,6 @@
:dragging? @dragging?}]
:else
- [:& empty-placeholder {:dragging? @dragging?}])]))
+ [:& empty-placeholder {:dragging? @dragging?
+ :default? (:is-default project)}])]))
diff --git a/frontend/src/app/main/ui/dashboard/placeholder.cljs b/frontend/src/app/main/ui/dashboard/placeholder.cljs
new file mode 100644
index 000000000..1df3839bd
--- /dev/null
+++ b/frontend/src/app/main/ui/dashboard/placeholder.cljs
@@ -0,0 +1,34 @@
+;; 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.dashboard.placeholder
+ (:require
+ [app.main.ui.icons :as i]
+ [app.util.i18n :as i18n :refer [tr]]
+ [rumext.alpha :as mf]))
+
+(mf/defc empty-placeholder
+ [{:keys [dragging? default?] :as props}]
+ (cond
+ (true? dragging?)
+ [:div.grid-row.no-wrap
+ [:div.grid-item]]
+
+ (true? default?)
+ [:div.grid-empty-placeholder.drafts
+ [:div.text
+ [:& i18n/tr-html {:label "dashboard.empty-placeholder-drafts"}]]]
+
+ :else
+ [:div.grid-empty-placeholder
+ [:img.ph-files {:src "images/ph-file.svg"}]]))
+
+(mf/defc loading-placeholder
+ []
+ [:div.grid-empty-placeholder
+ [:div.icon i/loader]
+ [:div.text (tr "dashboard.loading-files")]])
+
diff --git a/frontend/src/app/main/ui/dashboard/project_menu.cljs b/frontend/src/app/main/ui/dashboard/project_menu.cljs
index b3654ff59..e1820cc1a 100644
--- a/frontend/src/app/main/ui/dashboard/project_menu.cljs
+++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs
@@ -73,7 +73,6 @@
:accept-label (tr "modals.delete-project-confirm.accept")
:on-accept delete-fn}))
-
file-input (mf/use-ref nil)
on-import-files
diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs
index 7089ba721..c171aab38 100644
--- a/frontend/src/app/main/ui/dashboard/projects.cljs
+++ b/frontend/src/app/main/ui/dashboard/projects.cljs
@@ -33,10 +33,8 @@
(tr "dashboard.new-project")]]))
(mf/defc project-item
- [{:keys [project first? files] :as props}]
+ [{:keys [project first? team files] :as props}]
(let [locale (mf/deref i18n/locale)
-
- team-id (:team-id project)
file-count (or (:count project) 0)
dstate (mf/deref refs/dashboard-local)
@@ -145,9 +143,8 @@
i/actions]]
[:& line-grid
- {:project-id (:id project)
- :project project
- :team-id team-id
+ {:project project
+ :team team
:on-load-more on-nav
:files files}]]))
@@ -186,7 +183,8 @@
(filterv #(= id (:project-id %)))
(sort-by :modified-at #(compare %2 %1))))]
[:& project-item {:project project
- :files files
+ :team team
+ :files files
:first? (= project (first projects))
:key (:id project)}]))]])))
diff --git a/frontend/src/app/main/ui/modal.cljs b/frontend/src/app/main/ui/modal.cljs
index b07e3dd57..e4ae3e4bc 100644
--- a/frontend/src/app/main/ui/modal.cljs
+++ b/frontend/src/app/main/ui/modal.cljs
@@ -72,8 +72,9 @@
#(doseq [key keys]
(events/unlistenByKey key)))))
- [:div.modal-wrapper {:ref wrapper-ref}
- (mf/element (get components (:type data)) (:props data))]))
+ (when-let [component (get components (:type data))]
+ [:div.modal-wrapper {:ref wrapper-ref}
+ (mf/element component (:props data))])))
(def modal-ref
diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs
index c980f45c0..e0f6e1066 100644
--- a/frontend/src/app/main/ui/onboarding.cljs
+++ b/frontend/src/app/main/ui/onboarding.cljs
@@ -12,8 +12,10 @@
[app.main.data.messages :as dm]
[app.main.data.modal :as modal]
[app.main.data.users :as du]
+ [app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
+ [app.main.ui.icons :as i]
[app.main.ui.releases.common :as rc]
[app.main.ui.releases.v1-4]
[app.main.ui.releases.v1-5]
@@ -21,10 +23,13 @@
[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.object :as obj]
[app.util.router :as rt]
[app.util.timers :as tm]
+ [beicon.core :as rx]
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
@@ -159,7 +164,7 @@
skip
(mf/use-callback
(st/emitf (modal/hide)
- (modal/show {:type :onboarding-team})
+ (modal/show {:type :onboarding-choice})
(du/mark-onboarding-as-viewed)))]
(mf/use-layout-effect
@@ -187,57 +192,232 @@
(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 [close (mf/use-fn (st/emitf (modal/hide)))
- form (fm/use-form :spec ::team-form
+ (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]
- (st/emit! (modal/hide)
- (rt/nav :dashboard-projects {:team-id (:id 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."))))
- on-submit
+ ;; The SKIP branch only creates the team, without invitations
+ on-skip
(mf/use-callback
- (fn [form _event]
+ (fn [_]
(let [mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}
- params {:name (get-in @form [:clean-data :name])}]
- (st/emit! (dd/create-team (with-meta params mdata))))))]
+ 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.final.animated.fadeInUp
- [:div.modal-left
- [:img {:src "images/onboarding-team.jpg" :border "0" :alt (tr "onboarding.team.create.title")}]
- [:h2 (tr "onboarding.team.create.title")]
- [:p (tr "onboarding.team.create.desc1")]
+ [: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}
- [:& fm/input {:type "text"
- :name :name
- :label (tr "onboarding.team.create.input-placeholder")}]
+ [:& 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 "onboarding.team.create.button")}]]]
-
- [:div.modal-right
- [:img {:src "images/onboarding-start.jpg" :border "0" :alt (tr "onboarding.team.start.title")}]
- [:h2 (tr "onboarding.team.start.title")]
- [:p (tr "onboarding.team.start.desc1")]
- [:button.btn-primary.btn-large {:on-click close} (tr "onboarding.team.start.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-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}]]]]]))
+
;;; --- RELEASE NOTES MODAL
diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs
index d36ec13a6..04c9521f3 100644
--- a/frontend/src/app/worker/import.cljs
+++ b/frontend/src/app/worker/import.cljs
@@ -169,7 +169,7 @@
(rx/tap #(do (swap! current inc)
(progress! context
:upload-data @current total))))))
-
+
(rx/map first)
(rx/tap #(reset! revn (:revn %)))
(rx/ignore))
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index 691cc14e6..7297aa88b 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -250,6 +250,12 @@ msgstr "Duplicate %s files"
msgid "dashboard.empty-files"
msgstr "You still have no files here"
+
+#: src/app/main/ui/dashboard/grid.cljs
+#, markdown
+msgid "dashboard.empty-placeholder-drafts"
+msgstr "Oh no! You have no files jet! If you want to try with some templates go to [templates.penpot.app](https://penpot.app/libraries-templates.html)"
+
msgid "dashboard.export-frames"
msgstr "Export artboards to PDF..."
@@ -1533,6 +1539,13 @@ msgstr "Profile saved successfully!"
msgid "notifications.validation-email-sent"
msgstr "Verification email sent to %s. Check your email!"
+
+msgid "onboarding.templates.title"
+msgstr "Start designing"
+
+msgid "onboarding.templates.subtitle"
+msgid "Here are some templates."
+
msgid "onboarding.contrib.alt"
msgstr "Open Source"
@@ -1606,31 +1619,27 @@ msgstr ""
msgid "onboarding.slide.3.title"
msgstr "One shared source of truth"
-msgid "onboarding.team.create.button"
-msgstr "Create a team"
+msgid "onboarding.choice.fly-solo"
+msgstr "Fly solo"
-msgid "onboarding.team.create.desc1"
-msgstr ""
-"Are you working with someone? Create a team to work together on projects "
-"and share design assets."
+msgid "onboarding.choice.fly-solo-desc"
+msgstr "Jump away into Penpot and start designing by your own."
-msgid "onboarding.team.create.input-placeholder"
+msgid "onboarding.choice.team-up"
+msgstr "Team up"
+
+msgid "onboarding.team.skip-and-invite-later"
+msgstr "Skip and invite later"
+
+msgid "onboarding.choice.team-up-desc"
+msgstr "Are you working with someone? Create a team and invite people to work together on projects and share design assets."
+
+msgid "labels.next"
+msgstr "Next"
+
+msgid "onboarding.team-input-placeholder"
msgstr "Enter new team name"
-msgid "onboarding.team.create.title"
-msgstr "Create team"
-
-msgid "onboarding.team.start.button"
-msgstr "Start right away"
-
-msgid "onboarding.team.start.desc1"
-msgstr ""
-"Jump right away into Penpot and start designing by your own. You will still "
-"have the chance to create teams later."
-
-msgid "onboarding.team.start.title"
-msgstr "Start designing"
-
msgid "onboarding.welcome.alt"
msgstr "Penpot"
@@ -1642,6 +1651,9 @@ msgstr ""
"Penpot is still at development stage and there will be constant updates. We "
"hope you enjoy the first stable version."
+msgid "onboarding.welcome.desc3"
+msgstr "How do you want to start?"
+
msgid "onboarding.welcome.title"
msgstr "Welcome to Penpot!"
@@ -3187,4 +3199,5 @@ msgid "workspace.updates.update"
msgstr "Update"
msgid "workspace.viewport.click-to-close-path"
-msgstr "Click to close the path"
\ No newline at end of file
+msgstr "Click to close the path"
+