diff --git a/backend/resources/app/onboarding.edn b/backend/resources/app/onboarding.edn index a6449f5fd..07a11859d 100644 --- a/backend/resources/app/onboarding.edn +++ b/backend/resources/app/onboarding.edn @@ -36,4 +36,7 @@ :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Open-Color-Scheme.penpot"} {:id "flex-layout-playground" :name "Flex Layout Playground" - :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Flex-Layout-Playground.penpot"}] + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Flex-Layout-Playground.penpot"} + {:id "welcome" + :name "Welcome" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/welcome.penpot"}] diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index 3adac1f55..1ed3fa364 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -27,9 +27,11 @@ [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.setup :as-alias setup] + [app.setup.welcome-file :refer [create-welcome-file]] [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] + [app.worker :as wrk] [cuerdas.core :as str])) (def schema:password @@ -241,6 +243,7 @@ params (d/without-nils params) token (tokens/generate (::setup/props cfg) params)] + (with-meta {:token token} {::audit/profile-id uuid/zero}))) @@ -350,7 +353,7 @@ :extra-data ptoken}))) (defn register-profile - [{:keys [::db/conn] :as cfg} {:keys [token fullname theme] :as params}] + [{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [token fullname theme] :as params}] (let [theme (when (= theme "light") theme) claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register}) params (-> claims @@ -380,8 +383,13 @@ invitation (when-let [token (:invitation-token params)] (tokens/verify (::setup/props cfg) {:token token :iss :team-invitation})) - props (audit/profile->props profile)] + props (audit/profile->props profile) + create-welcome-file-when-needed + (fn [] + (when (:create-welcome-file params) + (let [cfg (dissoc cfg ::db/conn)] + (wrk/submit! executor (create-welcome-file cfg profile)))))] (cond ;; When profile is blocked, we just ignore it and return plain data (:is-blocked profile) @@ -418,6 +426,7 @@ (if (:is-active profile) (-> (profile/strip-private-attrs profile) (rph/with-transform (session/create-fn cfg (:id profile))) + (rph/with-defer create-welcome-file-when-needed) (rph/with-meta {::audit/replace-props props ::audit/context {:action "login"} @@ -427,10 +436,12 @@ (when-not (eml/has-reports? conn (:email profile)) (send-email-verification! cfg profile)) - (rph/with-meta {:email (:email profile)} - {::audit/replace-props props - ::audit/context {:action "email-verification"} - ::audit/profile-id (:id profile)}))) + (-> {:email (:email profile)} + (rph/with-defer create-welcome-file-when-needed) + (rph/with-meta + {::audit/replace-props props + ::audit/context {:action "email-verification"} + ::audit/profile-id (:id profile)})))) :else (let [elapsed? (elapsed-verify-threshold? profile) @@ -462,7 +473,8 @@ [:map {:title "register-profile"} [:token schema:token] [:fullname [::sm/word-string {:max 100}]] - [:theme {:optional true} [:string {:max 10}]]]) + [:theme {:optional true} [:string {:max 10}]] + [:create-welcome-file {:optional true} :boolean]]) (sv/defmethod ::register-profile {::rpc/auth false diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index e90b255fc..30d0d3460 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -396,8 +396,8 @@ ;; --- COMMAND: Clone Template -(defn- clone-template - [cfg {:keys [project-id ::rpc/profile-id] :as params} template] +(defn clone-template + [cfg {:keys [project-id profile-id] :as params} template] (db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}] ;; NOTE: the importation process performs some operations that ;; are not very friendly with virtual threads, and for avoid @@ -416,6 +416,7 @@ (doseq [file-id result] (let [props (assoc props :id file-id) event (-> (audit/event-from-rpc-params params) + (assoc ::audit/profile-id profile-id) (assoc ::audit/name "create-file") (assoc ::audit/props props))] (audit/submit! cfg event)))) @@ -437,7 +438,8 @@ [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}] (let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]}) _ (teams/check-edition-permissions! pool profile-id (:team-id project)) - template (tmpl/get-template-stream cfg template-id)] + template (tmpl/get-template-stream cfg template-id) + params (assoc params :profile-id profile-id)] (when-not template (ex/raise :type :not-found diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index ce302571e..3108fcbb2 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -360,27 +360,31 @@ [:map {:title "update-profile-props"} [:props [:map-of :keyword :any]]])) +(defn update-profile-props + [{:keys [::db/conn] :as cfg} profile-id props] + (let [profile (get-profile conn profile-id ::sql/for-update true) + props (reduce-kv (fn [props k v] + ;; We don't accept namespaced keys + (if (simple-ident? k) + (if (nil? v) + (dissoc props k) + (assoc props k v)) + props)) + (:props profile) + props)] + + (db/update! conn :profile + {:props (db/tjson props)} + {:id profile-id}) + + (filter-props props))) + (sv/defmethod ::update-profile-props {::doc/added "1.0" ::sm/params schema:update-profile-props} - [{:keys [::db/pool]} {:keys [::rpc/profile-id props]}] - (db/with-atomic [conn pool] - (let [profile (get-profile conn profile-id ::sql/for-update true) - props (reduce-kv (fn [props k v] - ;; We don't accept namespaced keys - (if (simple-ident? k) - (if (nil? v) - (dissoc props k) - (assoc props k v)) - props)) - (:props profile) - props)] - - (db/update! conn :profile - {:props (db/tjson props)} - {:id profile-id}) - - (filter-props props)))) + [cfg {:keys [::rpc/profile-id props]}] + (db/tx-run! cfg (fn [cfg] + (update-profile-props cfg profile-id props)))) ;; --- MUTATION: Delete Profile diff --git a/backend/src/app/setup/welcome_file.clj b/backend/src/app/setup/welcome_file.clj new file mode 100644 index 000000000..8de4acaa7 --- /dev/null +++ b/backend/src/app/setup/welcome_file.clj @@ -0,0 +1,64 @@ +;; 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) KALEIDOS INC + +(ns app.setup.welcome-file + (:require + [app.common.logging :as l] + [app.db :as db] + [app.rpc :as-alias rpc] + [app.rpc.climit :as-alias climit] + [app.rpc.commands.files-update :as fupdate] + [app.rpc.commands.management :as management] + [app.rpc.commands.profile :as profile] + [app.rpc.doc :as-alias doc] + [app.setup :as-alias setup] + [app.setup.templates :as tmpl] + [app.worker :as-alias wrk])) + +(def ^:private page-id #uuid "2c6952ee-d00e-8160-8004-d2250b7210cb") +(def ^:private shape-id #uuid "765e9f82-c44e-802e-8004-d72a10b7b445") + +(def ^:private update-path + [:data :pages-index page-id :objects shape-id + :content :children 0 :children 0 :children 0]) + +(def ^:private sql:mark-file-object-thumbnails-deleted + "UPDATE file_tagged_object_thumbnail + SET deleted_at = now() + WHERE file_id = ?") + +(def ^:private sql:mark-file-thumbnail-deleted + "UPDATE file_thumbnail + SET deleted_at = now() + WHERE file_id = ?") + +(defn- update-welcome-shape + [_ file name] + (let [text (str "Welcome to Penpot, " name "!")] + (-> file + (update-in update-path assoc :text text) + (update-in [:data :pages-index page-id :objects shape-id] assoc :name "Welcome to Penpot!") + (update-in [:data :pages-index page-id :objects shape-id] dissoc :position-data)))) + +(defn create-welcome-file + [cfg {:keys [id fullname] :as profile}] + (try + (let [cfg (dissoc cfg ::db/conn) + params {:profile-id (:id profile) + :project-id (:default-project-id profile)} + template-stream (tmpl/get-template-stream cfg "welcome") + file-id (-> (management/clone-template cfg params template-stream) + first)] + + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (fupdate/update-file! cfg file-id update-welcome-shape fullname) + (profile/update-profile-props cfg id {:welcome-file-id file-id}) + (db/exec-one! conn [sql:mark-file-object-thumbnails-deleted file-id]) + (db/exec-one! conn [sql:mark-file-thumbnail-deleted file-id])))) + + (catch Throwable cause + (l/error :hint "unexpected error on create welcome file " :cause cause)))) + diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 1a61a0644..c59141867 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -25,6 +25,8 @@ [beicon.v2.core :as rx] [potok.v2.core :as ptk])) +(declare update-profile-props) + ;; --- SCHEMAS (def ^:private @@ -152,9 +154,15 @@ profile. The profile can proceed from standard login or from accepting invitation, or third party auth signup or singin." [profile] - (letfn [(get-redirect-event [] - (let [team-id (get-current-team-id profile)] - (rt/nav' :dashboard-projects {:team-id team-id})))] + (letfn [(get-redirect-events [] + (let [team-id (get-current-team-id profile) + welcome-file-id (get-in profile [:props :welcome-file-id])] + (if (some? welcome-file-id) + (rx/of + (rt/nav' :workspace {:project-id (:default-project-id profile) + :file-id welcome-file-id}) + (update-profile-props {:welcome-file-id nil})) + (rx/of (rt/nav' :dashboard-projects {:team-id team-id})))))] (ptk/reify ::logged-in ev/Event @@ -171,10 +179,11 @@ ptk/WatchEvent (watch [_ _ _] (when (is-authenticated? profile) - (->> (rx/of (profile-fetched profile) - (fetch-teams) - (get-redirect-event) - (ws/initialize)) + (->> (rx/concat + (rx/of (profile-fetched profile) + (fetch-teams) + (ws/initialize)) + (get-redirect-events)) (rx/observe-on :async))))))) (declare login-from-register) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 50cd947e6..8a53010de 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -44,7 +44,30 @@ (mf/defc main-page {::mf/props :obj} [{:keys [route profile]}] - (let [{:keys [data params]} route] + (let [{:keys [data params]} route + props (get profile :props) + show-question-modal? + (and (contains? cf/flags :onboarding) + (not (:onboarding-viewed props)) + (not (contains? props :onboarding-questions))) + + show-newsletter-modal? + (and (contains? cf/flags :onboarding) + (not (:onboarding-viewed props)) + (not (contains? props :newsletter-updates)) + (contains? props :onboarding-questions)) + + show-team-modal? + (and (contains? cf/flags :onboarding) + (not (:onboarding-viewed props)) + (not (contains? props :onboarding-team-id)) + (contains? props :newsletter-updates)) + + show-release-modal? + (and (contains? cf/flags :onboarding) + (:onboarding-viewed props) + (not= (:release-notes-viewed props) (:main cf/version)) + (not= "0.0" (:main cf/version)))] [:& (mf/provider ctx/current-route) {:value route} (case (:name data) (:auth-login @@ -84,42 +107,19 @@ #_[:& app.main.ui.onboarding/onboarding-templates-modal] #_[:& app.main.ui.onboarding/onboarding-modal] #_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal] - (when-let [props (get profile :props)] - (let [show-question-modal? - (and (contains? cf/flags :onboarding) - (not (:onboarding-viewed props)) - (not (contains? props :onboarding-questions))) - show-newsletter-modal? - (and (contains? cf/flags :onboarding) - (not (:onboarding-viewed props)) - (not (contains? props :newsletter-updates)) - (contains? props :onboarding-questions)) + (cond + show-question-modal? + [:& questions-modal] - show-team-modal? - (and (contains? cf/flags :onboarding) - (not (:onboarding-viewed props)) - (not (contains? props :onboarding-team-id)) - (contains? props :newsletter-updates)) + show-newsletter-modal? + [:& onboarding-newsletter] - show-release-modal? - (and (contains? cf/flags :onboarding) - (:onboarding-viewed props) - (not= (:release-notes-viewed props) (:main cf/version)) - (not= "0.0" (:main cf/version)))] + show-team-modal? + [:& onboarding-team-modal {:go-to-team? true}] - (cond - show-question-modal? - [:& questions-modal] - - show-newsletter-modal? - [:& onboarding-newsletter] - - show-team-modal? - [:& onboarding-team-modal] - - show-release-modal? - [:& release-notes-modal {:version (:main cf/version)}]))) + show-release-modal? + [:& release-notes-modal {:version (:main cf/version)}]) [:& dashboard-page {:route route :profile profile}]] :viewer @@ -154,6 +154,20 @@ page-id (some-> params :query :page-id uuid) layout (some-> params :query :layout keyword)] [:? {} + (when (cf/external-feature-flag "onboarding-03" "test") + (cond + show-question-modal? + [:& questions-modal] + + show-newsletter-modal? + [:& onboarding-newsletter] + + show-team-modal? + [:& onboarding-team-modal {:go-to-team? false}] + + show-release-modal? + [:& release-notes-modal {:version (:main cf/version)}])) + [:& workspace-page {:project-id project-id :file-id file-id :page-id page-id diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 192a96635..8c3a8a6da 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -39,7 +39,8 @@ form (fm/use-form :schema schema:register-form :initial initial) - submitted? (mf/use-state false) + submitted? + (mf/use-state false) on-error (mf/use-fn @@ -176,7 +177,9 @@ ::mf/private true} [{:keys [params on-success-callback]}] (let [form (fm/use-form :schema schema:register-validate-form :initial params) - submitted? (mf/use-state false) + + submitted? + (mf/use-state false) on-success (mf/use-fn @@ -208,7 +211,13 @@ (mf/deps on-success on-error) (fn [form _] (reset! submitted? true) - (let [params (:clean-data @form)] + (let [create-welcome-file? + (cf/external-feature-flag "onboarding-03" "test") + + params + (cond-> (:clean-data @form) + create-welcome-file? (assoc :create-welcome-file true))] + (->> (rp/cmd! :register-profile params) (rx/finalize #(reset! submitted? false)) (rx/subs! on-success on-error)))))] diff --git a/frontend/src/app/main/ui/dashboard/templates.cljs b/frontend/src/app/main/ui/dashboard/templates.cljs index 8927ff053..1d6e07998 100644 --- a/frontend/src/app/main/ui/dashboard/templates.cljs +++ b/frontend/src/app/main/ui/dashboard/templates.cljs @@ -168,7 +168,9 @@ [{:keys [default-project-id profile project-id team-id]}] (let [templates (mf/deref builtin-templates) templates (mf/with-memo [templates] - (filterv #(not= (:id %) "tutorial-for-beginners") templates)) + (filterv #(and + (not= (:id %) "welcome") + (not= (:id %) "tutorial-for-beginners")) templates)) route (mf/deref refs/route) route-name (get-in route [:data :name]) diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs index e18d11ab1..79bcc98a9 100644 --- a/frontend/src/app/main/ui/onboarding/team_choice.cljs +++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs @@ -66,7 +66,7 @@ (mf/defc team-form-step-2 {::mf/props :obj} - [{:keys [name on-back]}] + [{:keys [name on-back go-to-team?]}] (let [initial (mf/use-memo #(do {:role "editor" :name name})) @@ -85,7 +85,8 @@ (let [team-id (:id response)] (st/emit! (du/update-profile-props {:onboarding-team-id team-id :onboarding-viewed true}) - (rt/nav :dashboard-projects {:team-id team-id}))))) + (when go-to-team? + (rt/nav :dashboard-projects {:team-id team-id})))))) on-error (mf/use-fn @@ -240,7 +241,7 @@ (mf/defc onboarding-team-modal {::mf/props :obj} - [] + [{:keys [go-to-team?]}] (let [name* (mf/use-state nil) name (deref name*) @@ -262,6 +263,6 @@ [:& left-sidebar] [:div {:class (stl/css :separator)}] (if name - [:& team-form-step-2 {:name name :on-back on-back}] + [:& team-form-step-2 {:name name :on-back on-back :go-to-team? go-to-team?}] [:& team-form-step-1 {:on-submit on-submit}])]]))