0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-23 23:18:48 -05:00

Merge pull request #5011 from penpot/palba-testab-start-workspace

A/B test start directly at the workspace
This commit is contained in:
Alejandro 2024-09-05 07:05:57 +02:00 committed by GitHub
commit f765cc8dbc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 197 additions and 77 deletions

View file

@ -36,4 +36,7 @@
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Open-Color-Scheme.penpot"} :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Open-Color-Scheme.penpot"}
{:id "flex-layout-playground" {:id "flex-layout-playground"
:name "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"}]

View file

@ -27,9 +27,11 @@
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph] [app.rpc.helpers :as rph]
[app.setup :as-alias setup] [app.setup :as-alias setup]
[app.setup.welcome-file :refer [create-welcome-file]]
[app.tokens :as tokens] [app.tokens :as tokens]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk]
[cuerdas.core :as str])) [cuerdas.core :as str]))
(def schema:password (def schema:password
@ -241,6 +243,7 @@
params (d/without-nils params) params (d/without-nils params)
token (tokens/generate (::setup/props cfg) params)] token (tokens/generate (::setup/props cfg) params)]
(with-meta {:token token} (with-meta {:token token}
{::audit/profile-id uuid/zero}))) {::audit/profile-id uuid/zero})))
@ -350,7 +353,7 @@
:extra-data ptoken}))) :extra-data ptoken})))
(defn register-profile (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) (let [theme (when (= theme "light") theme)
claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register}) claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register})
params (-> claims params (-> claims
@ -380,8 +383,13 @@
invitation (when-let [token (:invitation-token params)] invitation (when-let [token (:invitation-token params)]
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation})) (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 (cond
;; When profile is blocked, we just ignore it and return plain data ;; When profile is blocked, we just ignore it and return plain data
(:is-blocked profile) (:is-blocked profile)
@ -418,6 +426,7 @@
(if (:is-active profile) (if (:is-active profile)
(-> (profile/strip-private-attrs profile) (-> (profile/strip-private-attrs profile)
(rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-defer create-welcome-file-when-needed)
(rph/with-meta (rph/with-meta
{::audit/replace-props props {::audit/replace-props props
::audit/context {:action "login"} ::audit/context {:action "login"}
@ -427,10 +436,12 @@
(when-not (eml/has-reports? conn (:email profile)) (when-not (eml/has-reports? conn (:email profile))
(send-email-verification! cfg profile)) (send-email-verification! cfg profile))
(rph/with-meta {:email (:email profile)} (-> {:email (:email profile)}
{::audit/replace-props props (rph/with-defer create-welcome-file-when-needed)
::audit/context {:action "email-verification"} (rph/with-meta
::audit/profile-id (:id profile)}))) {::audit/replace-props props
::audit/context {:action "email-verification"}
::audit/profile-id (:id profile)}))))
:else :else
(let [elapsed? (elapsed-verify-threshold? profile) (let [elapsed? (elapsed-verify-threshold? profile)
@ -462,7 +473,8 @@
[:map {:title "register-profile"} [:map {:title "register-profile"}
[:token schema:token] [:token schema:token]
[:fullname [::sm/word-string {:max 100}]] [: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 (sv/defmethod ::register-profile
{::rpc/auth false {::rpc/auth false

View file

@ -396,8 +396,8 @@
;; --- COMMAND: Clone Template ;; --- COMMAND: Clone Template
(defn- clone-template (defn clone-template
[cfg {:keys [project-id ::rpc/profile-id] :as params} template] [cfg {:keys [project-id profile-id] :as params} template]
(db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}] (db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}]
;; NOTE: the importation process performs some operations that ;; NOTE: the importation process performs some operations that
;; are not very friendly with virtual threads, and for avoid ;; are not very friendly with virtual threads, and for avoid
@ -416,6 +416,7 @@
(doseq [file-id result] (doseq [file-id result]
(let [props (assoc props :id file-id) (let [props (assoc props :id file-id)
event (-> (audit/event-from-rpc-params params) event (-> (audit/event-from-rpc-params params)
(assoc ::audit/profile-id profile-id)
(assoc ::audit/name "create-file") (assoc ::audit/name "create-file")
(assoc ::audit/props props))] (assoc ::audit/props props))]
(audit/submit! cfg event)))) (audit/submit! cfg event))))
@ -437,7 +438,8 @@
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}] [{: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]}) (let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]})
_ (teams/check-edition-permissions! pool profile-id (:team-id project)) _ (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 (when-not template
(ex/raise :type :not-found (ex/raise :type :not-found

View file

@ -360,27 +360,31 @@
[:map {:title "update-profile-props"} [:map {:title "update-profile-props"}
[:props [:map-of :keyword :any]]])) [: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 (sv/defmethod ::update-profile-props
{::doc/added "1.0" {::doc/added "1.0"
::sm/params schema:update-profile-props} ::sm/params schema:update-profile-props}
[{:keys [::db/pool]} {:keys [::rpc/profile-id props]}] [cfg {:keys [::rpc/profile-id props]}]
(db/with-atomic [conn pool] (db/tx-run! cfg (fn [cfg]
(let [profile (get-profile conn profile-id ::sql/for-update true) (update-profile-props cfg profile-id props))))
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))))
;; --- MUTATION: Delete Profile ;; --- MUTATION: Delete Profile

View file

@ -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))))

View file

@ -25,6 +25,8 @@
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
(declare update-profile-props)
;; --- SCHEMAS ;; --- SCHEMAS
(def ^:private (def ^:private
@ -152,9 +154,15 @@
profile. The profile can proceed from standard login or from profile. The profile can proceed from standard login or from
accepting invitation, or third party auth signup or singin." accepting invitation, or third party auth signup or singin."
[profile] [profile]
(letfn [(get-redirect-event [] (letfn [(get-redirect-events []
(let [team-id (get-current-team-id profile)] (let [team-id (get-current-team-id profile)
(rt/nav' :dashboard-projects {:team-id team-id})))] 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 (ptk/reify ::logged-in
ev/Event ev/Event
@ -171,10 +179,11 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(when (is-authenticated? profile) (when (is-authenticated? profile)
(->> (rx/of (profile-fetched profile) (->> (rx/concat
(fetch-teams) (rx/of (profile-fetched profile)
(get-redirect-event) (fetch-teams)
(ws/initialize)) (ws/initialize))
(get-redirect-events))
(rx/observe-on :async))))))) (rx/observe-on :async)))))))
(declare login-from-register) (declare login-from-register)

View file

@ -44,7 +44,30 @@
(mf/defc main-page (mf/defc main-page
{::mf/props :obj} {::mf/props :obj}
[{:keys [route profile]}] [{: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} [:& (mf/provider ctx/current-route) {:value route}
(case (:name data) (case (:name data)
(:auth-login (:auth-login
@ -84,42 +107,19 @@
#_[:& app.main.ui.onboarding/onboarding-templates-modal] #_[:& app.main.ui.onboarding/onboarding-templates-modal]
#_[:& app.main.ui.onboarding/onboarding-modal] #_[:& app.main.ui.onboarding/onboarding-modal]
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-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? (cond
(and (contains? cf/flags :onboarding) show-question-modal?
(not (:onboarding-viewed props)) [:& questions-modal]
(not (contains? props :newsletter-updates))
(contains? props :onboarding-questions))
show-team-modal? show-newsletter-modal?
(and (contains? cf/flags :onboarding) [:& onboarding-newsletter]
(not (:onboarding-viewed props))
(not (contains? props :onboarding-team-id))
(contains? props :newsletter-updates))
show-release-modal? show-team-modal?
(and (contains? cf/flags :onboarding) [:& onboarding-team-modal {:go-to-team? true}]
(:onboarding-viewed props)
(not= (:release-notes-viewed props) (:main cf/version))
(not= "0.0" (:main cf/version)))]
(cond show-release-modal?
show-question-modal? [:& release-notes-modal {:version (:main cf/version)}])
[:& questions-modal]
show-newsletter-modal?
[:& onboarding-newsletter]
show-team-modal?
[:& onboarding-team-modal]
show-release-modal?
[:& release-notes-modal {:version (:main cf/version)}])))
[:& dashboard-page {:route route :profile profile}]] [:& dashboard-page {:route route :profile profile}]]
:viewer :viewer
@ -154,6 +154,20 @@
page-id (some-> params :query :page-id uuid) page-id (some-> params :query :page-id uuid)
layout (some-> params :query :layout keyword)] 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 [:& workspace-page {:project-id project-id
:file-id file-id :file-id file-id
:page-id page-id :page-id page-id

View file

@ -39,7 +39,8 @@
form (fm/use-form :schema schema:register-form form (fm/use-form :schema schema:register-form
:initial initial) :initial initial)
submitted? (mf/use-state false) submitted?
(mf/use-state false)
on-error on-error
(mf/use-fn (mf/use-fn
@ -176,7 +177,9 @@
::mf/private true} ::mf/private true}
[{:keys [params on-success-callback]}] [{:keys [params on-success-callback]}]
(let [form (fm/use-form :schema schema:register-validate-form :initial params) (let [form (fm/use-form :schema schema:register-validate-form :initial params)
submitted? (mf/use-state false)
submitted?
(mf/use-state false)
on-success on-success
(mf/use-fn (mf/use-fn
@ -208,7 +211,13 @@
(mf/deps on-success on-error) (mf/deps on-success on-error)
(fn [form _] (fn [form _]
(reset! submitted? true) (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) (->> (rp/cmd! :register-profile params)
(rx/finalize #(reset! submitted? false)) (rx/finalize #(reset! submitted? false))
(rx/subs! on-success on-error)))))] (rx/subs! on-success on-error)))))]

View file

@ -168,7 +168,9 @@
[{:keys [default-project-id profile project-id team-id]}] [{:keys [default-project-id profile project-id team-id]}]
(let [templates (mf/deref builtin-templates) (let [templates (mf/deref builtin-templates)
templates (mf/with-memo [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 (mf/deref refs/route)
route-name (get-in route [:data :name]) route-name (get-in route [:data :name])

View file

@ -66,7 +66,7 @@
(mf/defc team-form-step-2 (mf/defc team-form-step-2
{::mf/props :obj} {::mf/props :obj}
[{:keys [name on-back]}] [{:keys [name on-back go-to-team?]}]
(let [initial (mf/use-memo (let [initial (mf/use-memo
#(do {:role "editor" #(do {:role "editor"
:name name})) :name name}))
@ -85,7 +85,8 @@
(let [team-id (:id response)] (let [team-id (:id response)]
(st/emit! (du/update-profile-props {:onboarding-team-id team-id (st/emit! (du/update-profile-props {:onboarding-team-id team-id
:onboarding-viewed true}) :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 on-error
(mf/use-fn (mf/use-fn
@ -240,7 +241,7 @@
(mf/defc onboarding-team-modal (mf/defc onboarding-team-modal
{::mf/props :obj} {::mf/props :obj}
[] [{:keys [go-to-team?]}]
(let [name* (mf/use-state nil) (let [name* (mf/use-state nil)
name (deref name*) name (deref name*)
@ -262,6 +263,6 @@
[:& left-sidebar] [:& left-sidebar]
[:div {:class (stl/css :separator)}] [:div {:class (stl/css :separator)}]
(if name (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}])]])) [:& team-form-step-1 {:on-submit on-submit}])]]))