From fced22bc60afbbe3ee24ccf490fdbda438a3475d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 3 Nov 2021 13:10:49 +0100 Subject: [PATCH] :tada: Add new onboarding flow. --- backend/src/app/rpc/mutations/profile.clj | 22 +- backend/src/app/rpc/mutations/teams.clj | 137 +++++--- backend/src/app/rpc/queries/projects.clj | 10 +- backend/test/app/services_projects_test.clj | 16 +- backend/test/app/services_teams_test.clj | 2 +- backend/test/app/test_helpers.clj | 24 +- frontend/resources/images/on-solo-hover.svg | 1 + frontend/resources/images/on-solo.svg | 1 + frontend/resources/images/on-teamup-hover.svg | 1 + frontend/resources/images/on-teamup.svg | 1 + frontend/resources/images/ph-file.svg | 1 + frontend/resources/images/ph-left.svg | 1 + frontend/resources/images/ph-right.svg | 1 + .../styles/main/partials/dashboard-grid.scss | 19 ++ .../resources/styles/main/partials/modal.scss | 298 ++++++++++++++++-- frontend/src/app/main/data/dashboard.cljs | 22 ++ frontend/src/app/main/refs.cljs | 6 - frontend/src/app/main/ui.cljs | 5 +- frontend/src/app/main/ui/dashboard.cljs | 4 +- frontend/src/app/main/ui/dashboard/files.cljs | 2 +- frontend/src/app/main/ui/dashboard/grid.cljs | 31 +- .../app/main/ui/dashboard/placeholder.cljs | 34 ++ .../app/main/ui/dashboard/project_menu.cljs | 1 - .../src/app/main/ui/dashboard/projects.cljs | 12 +- frontend/src/app/main/ui/modal.cljs | 5 +- frontend/src/app/main/ui/onboarding.cljs | 236 ++++++++++++-- frontend/src/app/worker/import.cljs | 2 +- frontend/translations/en.po | 57 ++-- 28 files changed, 743 insertions(+), 209 deletions(-) create mode 100644 frontend/resources/images/on-solo-hover.svg create mode 100644 frontend/resources/images/on-solo.svg create mode 100644 frontend/resources/images/on-teamup-hover.svg create mode 100644 frontend/resources/images/on-teamup.svg create mode 100644 frontend/resources/images/ph-file.svg create mode 100644 frontend/resources/images/ph-left.svg create mode 100644 frontend/resources/images/ph-right.svg create mode 100644 frontend/src/app/main/ui/dashboard/placeholder.cljs 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" +