0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-21 06:02:32 -05:00

🎉 Add user feedback module.

This commit is contained in:
Andrey Antukh 2021-02-08 22:39:11 +01:00 committed by Hirunatan
parent 1cb18ad7cb
commit c1a139fc51
20 changed files with 431 additions and 59 deletions

View file

@ -1,5 +1,6 @@
{:lint-as {potok.core/reify clojure.core/reify
promesa.core/let clojure.core/let
rumext.alpha/defc clojure.core/defn
app.db/with-atomic clojure.core/with-open}
:output
{:exclude-files ["data_readers.clj"]}

View file

@ -0,0 +1 @@
[FEEDBACK]: From {{ profile.email }}

View file

@ -0,0 +1,7 @@
Feedback from: {{profile.fullname}} <{{profile.email}}>
Profile ID: {{profile.id}}
Subject: {{subject}}
{{content}}

View file

@ -40,6 +40,9 @@
:storage-s3-region :eu-central-1
:storage-s3-bucket "penpot-devenv-assets-pre"
:feedback-destination "info@example.com"
:feedback-enabled false
:assets-path "/internal/assets/"
:rlimits-password 10
@ -93,7 +96,11 @@
(s/def ::media-directory ::us/string)
(s/def ::asserts-enabled ::us/boolean)
(s/def ::feedback-enabled ::us/boolean)
(s/def ::feedback-destination ::us/string)
(s/def ::error-report-webhook ::us/string)
(s/def ::smtp-enabled ::us/boolean)
(s/def ::smtp-default-reply-to ::us/string)
(s/def ::smtp-default-from ::us/string)
@ -156,6 +163,8 @@
::database-username
::default-blob-version
::error-report-webhook
::feedback-enabled
::feedback-destination
::github-client-id
::github-client-secret
::gitlab-base-uri

View file

@ -43,6 +43,16 @@
;; --- Emails
(s/def ::subject ::us/string)
(s/def ::content ::us/string)
(s/def ::feedback
(s/keys :req-un [::subject ::content]))
(def feedback
"A profile feedback email."
(emails/template-factory ::feedback default-context))
(s/def ::name ::us/string)
(s/def ::register
(s/keys :req-un [::name]))

View file

@ -126,6 +126,7 @@
'app.rpc.mutations.projects
'app.rpc.mutations.viewer
'app.rpc.mutations.teams
'app.rpc.mutations.feedback
'app.rpc.mutations.verify-token)
(map (partial process-method cfg))
(into {}))))

View file

@ -0,0 +1,41 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2021 UXBOX Labs SL
(ns app.rpc.mutations.feedback
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.config :as cfg]
[app.db :as db]
[app.emails :as emails]
[app.rpc.queries.profile :as profile]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
(s/def ::subject ::us/string)
(s/def ::content ::us/string)
(s/def ::send-profile-feedback
(s/keys :req-un [::profile-id ::subject ::content]))
(sv/defmethod ::send-profile-feedback
[{:keys [pool] :as cfg} {:keys [profile-id subject content] :as params}]
(when-not (:feedback-enabled cfg/config)
(ex/raise :type :validation
:code :feedback-disabled
:hint "feedback module is disabled"))
(db/with-atomic [conn pool]
(let [profile (profile/retrieve-profile-data conn profile-id)]
(emails/send! conn emails/feedback
{:to (:feedback-destination cfg/config)
:profile profile
:subject subject
:content content})
nil)))

View file

@ -9,6 +9,7 @@
(ns app.util.emails
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.util.template :as tmpl]
@ -196,15 +197,17 @@
text (render-email-template-part :txt id context)
html (render-email-template-part :html id context)]
(when (or (not subj)
(not text)
(not html))
(and (not text)
(not html)))
(ex/raise :type :internal
:code :missing-email-templates))
{:subject subj
:body [{:type "text/plain"
:content text}
{:type "text/html"
:content html}]}))
:body (d/concat
[{:type "text/plain"
:content text}]
(when html
[{:type "text/html"
:content html}]))}))
(s/def ::priority #{:high :low})
(s/def ::to (s/or :sigle ::us/email

View file

@ -974,6 +974,78 @@
"es" : "La contraseña anterior no es correcta"
}
},
"feedback.chat-start" : {
"used-in" : [ "src/app/main/ui/settings/feedback.cljs:112" ],
"translations" : {
"en" : "Join the chat",
"es" : "Unirse al chat"
}
},
"feedback.chat-subtitle" : {
"used-in" : [ "src/app/main/ui/settings/feedback.cljs:109" ],
"translations" : {
"en" : "Feeling like talking? Chat with us at Gitter",
"es" : "¿Deseas conversar? Entra al nuestro chat de la comunidad en Gitter"
}
},
"feedback.description" : {
"used-in" : [ "src/app/main/ui/settings/feedback.cljs:88" ],
"translations" : {
"en" : "Description",
"es" : "Descripción"
}
},
"feedback.discussions-go-to" : {
"used-in" : [ "src/app/main/ui/settings/feedback.cljs:104" ],
"translations" : {
"en" : "Go to discussions",
"es" : "Ir a las discussiones"
}
},
"feedback.discussions-subtitle1" : {
"used-in" : [ "src/app/main/ui/settings/feedback.cljs:99" ],
"translations" : {
"en" : "Join Penpot team collaborative communication forum.",
"es" : "Entra al foro colaborativo de Penpot"
}
},
"feedback.discussions-subtitle2" : {
"used-in" : [ "src/app/main/ui/settings/feedback.cljs:100" ],
"translations" : {
"en" : "You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.",
"es" : ""
}
},
"feedback.discussions-title" : {
"used-in" : [ "src/app/main/ui/settings/feedback.cljs:98" ],
"translations" : {
"en" : "Team discussions",
"es" : ""
}
},
"feedback.subject" : {
"used-in" : [ "src/app/main/ui/settings/feedback.cljs:84" ],
"translations" : {
"en" : "Subject",
"es" : "Asunto"
}
},
"feedback.subtitle" : {
"used-in" : [ "src/app/main/ui/settings/feedback.cljs:81" ],
"translations" : {
"en" : "Please describe the reason of your email, specifying if is an issue, an idea or a doubt. A member of our team will respond as soon as possible.",
"es" : ""
}
},
"feedback.title" : {
"used-in" : [ "src/app/main/ui/settings/feedback.cljs:80" ],
"translations" : {
"en" : "Email",
"fr" : "Adresse email",
"ru" : "Email",
"es" : "Correo electrónico"
}
},
"generic.error" : {
"used-in" : [ "src/app/main/ui/settings/password.cljs:31" ],
"translations" : {
@ -1605,7 +1677,7 @@
"es" : "Correo electrónico"
}
},
"labels.feedback" : {
"labels.give-feedback" : {
"used-in" : [ "src/app/main/ui/workspace/header.cljs:231", "src/app/main/ui/dashboard/sidebar.cljs:471" ],
"translations" : {
"en" : "Give feedback",
@ -1823,6 +1895,37 @@
"es" : "Cargo"
}
},
"labels.send" : {
"used-in" : [ "src/app/main/ui/settings/feedback.cljs:93" ],
"translations" : {
"en" : "Send",
"es" : "Enviar"
}
},
"labels.sending" : {
"used-in" : [ "src/app/main/ui/settings/feedback.cljs:93" ],
"translations" : {
"en" : "Sending...",
"es" : "Enviando..."
}
},
"labels.feedback-disabled" : {
"used-in" : [ "src/app/main/ui/settings/feedback.cljs" ],
"translations" : {
"en" : "Feedback disabled",
"es" : "El modulo de recepción de opiniones esta deshabilitado."
}
},
"labels.feedback-sent" : {
"used-in" : [ "src/app/main/ui/settings/feedback.cljs" ],
"translations" : {
"en" : "Feedback sent",
"es" : "Opinión enviada"
}
},
"labels.service-unavailable.desc-message" : {
"used-in" : [ "src/app/main/ui/static.cljs:75" ],
"translations" : {

View file

@ -20,6 +20,7 @@
min-width: 25px;
padding: 0 1rem;
transition: all .4s;
text-decoration: none !important;
svg {
height: 15px;
width: 15px;

View file

@ -102,6 +102,14 @@ textarea {
text-decoration: underline;
}
p {
color: $color-gray-60;
}
hr {
border-color: $color-gray-20;
}
.links {
display: flex;
font-size: $fs14;
@ -131,7 +139,8 @@ textarea {
flex-direction: column;
position: relative;
input {
input,
textarea {
background-color: $color-white;
border-radius: 2px;
border: 1px solid $color-gray-20;
@ -143,6 +152,13 @@ textarea {
width: 100%;
}
textarea {
height: auto;
font-size: $fs14;
font-family: "worksans", sans-serif;
padding-top: 20px;
}
// Makes the background for autocomplete white
input:-webkit-autofill,
input:-webkit-autofill:hover,

View file

@ -67,6 +67,7 @@
(def default-language "en")
(def demo-warning (obj/get global "penpotDemoWarning" false))
(def feedback-enabled (obj/get global "penpotFeedbackEnabled" false))
(def allow-demo-users (obj/get global "penpotAllowDemoUsers" true))
(def google-client-id (obj/get global "penpotGoogleClientID" nil))
(def gitlab-client-id (obj/get global "penpotGitlabClientID" nil))

View file

@ -59,17 +59,18 @@
(def routes
[["/auth"
["/login" :auth-login]
["/register" :auth-register]
["/login" :auth-login]
["/register" :auth-register]
["/register/success" :auth-register-success]
["/recovery/request" :auth-recovery-request]
["/recovery" :auth-recovery]
["/verify-token" :auth-verify-token]]
["/recovery" :auth-recovery]
["/verify-token" :auth-verify-token]]
["/settings"
["/profile" :settings-profile]
["/profile" :settings-profile]
["/password" :settings-password]
["/options" :settings-options]]
["/feedback" :settings-feedback]
["/options" :settings-options]]
["/view/:file-id/:page-id"
{:name :viewer
@ -89,11 +90,11 @@
["/render-object/:file-id/:page-id/:object-id" :render-object]
["/dashboard/team/:team-id"
["/members" :dashboard-team-members]
["/settings" :dashboard-team-settings]
["/projects" :dashboard-projects]
["/search" :dashboard-search]
["/libraries" :dashboard-libraries]
["/members" :dashboard-team-members]
["/settings" :dashboard-team-settings]
["/projects" :dashboard-projects]
["/search" :dashboard-search]
["/libraries" :dashboard-libraries]
["/projects/:project-id" :dashboard-files]]
["/workspace/:project-id/:file-id" :workspace]])
@ -121,7 +122,8 @@
(:settings-profile
:settings-password
:settings-options)
:settings-options
:settings-feedback)
[:& settings/settings {:route route}]
:debug-icons-preview

View file

@ -15,7 +15,7 @@
[app.main.ui.icons :as i]
[app.util.object :as obj]
[app.util.forms :as fm]
[app.util.i18n :as i18n :refer [t]]
[app.util.i18n :as i18n :refer [t tr]]
["react" :as react]
[app.util.dom :as dom]))
@ -28,7 +28,6 @@
type' (mf/use-state type)
focus? (mf/use-state false)
locale (mf/deref i18n/locale)
touched? (get-in @form [:touched name])
error (get-in @form [:errors name])
@ -94,7 +93,59 @@
help-icon'])
(cond
(and touched? (:message error))
[:span.error (t locale (:message error))]
[:span.error (tr (:message error))]
(string? hint)
[:span.hint hint])]]))
(mf/defc textarea
[{:keys [label disabled name form hint trim] :as props}]
(let [form (or form (mf/use-ctx form-ctx))
type' (mf/use-state type)
focus? (mf/use-state false)
touched? (get-in @form [:touched name])
error (get-in @form [:errors name])
value (get-in @form [:data name] "")
klass (dom/classnames
:focus @focus?
:valid (and touched? (not error))
:invalid (and touched? error)
:disabled disabled
;; :empty (str/empty? value)
)
on-focus #(reset! focus? true)
on-change (fm/on-input-change form name trim)
on-blur
(fn [event]
(reset! focus? false)
(when-not (get-in @form [:touched name])
(swap! form assoc-in [:touched name] true)))
props (-> props
(dissoc :help-icon :form :trim)
(assoc :value value
:on-focus on-focus
:on-blur on-blur
;; :placeholder label
:on-change on-change
:type @type')
(obj/clj->props))]
[:div.custom-input
{:class klass}
[:*
[:label label]
[:> :textarea props]
(cond
(and touched? (:message error))
[:span.error (tr (:message error))]
(string? hint)
[:span.hint hint])]]))

View file

@ -466,10 +466,12 @@
[:li {:on-click (partial on-click (da/logout))}
[:span.icon i/exit]
[:span.text (t locale "labels.logout")]]
[:li.feedback {:on-click #(.open js/window "https://github.com/penpot/penpot/discussions" "_blank")}
[:span.icon i/msg-info]
[:span.text (t locale "labels.feedback")]
[:span.primary-badge "ALPHA"]]]]]
(when cfg/feedback-enabled
[:li.feedback {:on-click (partial on-click :settings-feedback)}
[:span.icon i/msg-info]
[:span.text (t locale "labels.give-feedback")]
[:span.primary-badge "ALPHA"]])]]]
(when (and team profile)
[:& comments-section {:profile profile

View file

@ -5,30 +5,31 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
;; Copyright (c) 2020-2021 UXBOX Labs SL
(ns app.main.ui.settings
(:require
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.settings.options :refer [options-page]]
[app.main.ui.settings.feedback :refer [feedback-page]]
[app.main.ui.settings.password :refer [password-page]]
[app.main.ui.settings.profile :refer [profile-page]]
[app.main.ui.settings.sidebar :refer [sidebar]]
[app.main.ui.settings.change-email]
[app.main.ui.settings.delete-account]
[app.util.i18n :as i18n :refer [t]]
[app.util.i18n :as i18n :refer [tr]]
[rumext.alpha :as mf]))
(mf/defc header
{::mf/wrap [mf/memo]}
[{:keys [locale] :as props}]
[]
(let [logout (constantly nil)]
[:header.dashboard-header
[:div.dashboard-title
[:h1 (t locale "dashboard.your-account-title")]]
[:h1 (tr "dashboard.your-account-title")]]
[:a.btn-secondary.btn-small {:on-click logout}
(t locale "labels.logout")]]))
(tr "labels.logout")]]))
(mf/defc settings
[{:keys [route] :as props}]
@ -41,12 +42,15 @@
:section section}]
[:div.dashboard-content
[:& header {:locale locale}]
[:& header]
[:section.dashboard-container
(case section
:settings-profile
[:& profile-page {:locale locale}]
:settings-feedback
[:& feedback-page]
:settings-password
[:& password-page {:locale locale}]

View file

@ -0,0 +1,120 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.settings.feedback
"Feedback form."
(:require
[app.common.spec :as us]
[app.main.data.messages :as dm]
[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.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[beicon.core :as rx]
[app.main.repo :as rp]
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
(s/def ::content ::us/not-empty-string)
(s/def ::subject ::us/not-empty-string)
(s/def ::feedback-form
(s/keys :req-un [::subject ::content]))
(defn- on-error
[form error]
(st/emit! (dm/error (tr "errors.generic"))))
(defn- on-success
[form]
(st/emit! (dm/success (tr "notifications.profile-saved"))))
(mf/defc options-form
[]
(let [profile (mf/deref refs/profile)
form (fm/use-form :spec ::feedback-form)
loading (mf/use-state false)
on-succes
(mf/use-callback
(mf/deps profile)
(fn [event]
(st/emit! (dm/success (tr "labels.feedback-sent")))
(swap! form assoc :data {} :touched {} :errors {})))
on-error
(mf/use-callback
(mf/deps profile)
(fn [{:keys [code] :as error}]
(reset! loading false)
(if (= code :feedbck-disabled)
(st/emit! (dm/error (tr "labels.feedback-disabled")))
(st/emit! (dm/error (tr "errors.generic"))))))
on-submit
(mf/use-callback
(mf/deps profile)
(fn [form event]
(reset! loading true)
(let [data (:clean-data @form)]
(prn "on-submit" data)
(->> (rp/mutation! :send-profile-feedback data)
(rx/subs on-succes on-error #(reset! loading false))))))]
[:& fm/form {:class "feedback-form"
:on-submit on-submit
:form form}
;; --- Feedback section
[:h2 (tr "feedback.title")]
[:p (tr "feedback.subtitle")]
[:div.fields-row
[:& fm/input {:label (tr "feedback.subject")
:name :subject}]]
[:div.fields-row
[:& fm/textarea
{:label (tr "feedback.description")
:name :content
:rows 5}]]
[:& fm/submit-button
{:label (if @loading (tr "labels.sending") (tr "labels.send"))
:disabled @loading}]
[:hr]
[:h2 (tr "feedback.discussions-title")]
[:p (tr "feedback.discussions-subtitle1")]
[:p (tr "feedback.discussions-subtitle2")]
[:a.btn-secondary.btn-large
{:href "https://github.com/penpot/penpot/discussions" :target "_blank"}
(tr "feedback.discussions-go-to")]
[:hr]
[:h2 "Gitter"]
[:p (tr "feedback.chat-subtitle")]
[:a.btn-secondary.btn-large
{:href "https://gitter.im/penpot/community" :target "_blank"}
(tr "feedback.chat-start")]
]))
(mf/defc feedback-page
[]
[:div.dashboard-settings
[:div.form-container
[:& options-form]]])

View file

@ -9,31 +9,20 @@
(ns app.main.ui.settings.sidebar
(:require
[app.common.spec :as us]
[app.main.data.auth :as da]
[app.main.data.messages :as dm]
[app.main.refs :as refs]
[app.config :as cfg]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.forms :as fm]
[app.main.ui.dashboard.sidebar :refer [profile-section]]
[app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [t tr]]
[app.util.object :as obj]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.time :as dt]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[goog.functions :as f]
[okulary.core :as l]
[rumext.alpha :as mf]))
(mf/defc sidebar-content
[{:keys [locale profile section] :as props}]
[{:keys [profile section] :as props}]
(let [profile? (= section :settings-profile)
password? (= section :settings-password)
options? (= section :settings-options)
feedback? (= section :settings-feedback)
go-dashboard
(mf/use-callback
@ -45,6 +34,11 @@
(mf/deps profile)
(st/emitf (rt/nav :settings-profile)))
go-settings-feedback
(mf/use-callback
(mf/deps profile)
(st/emitf (rt/nav :settings-feedback)))
go-settings-password
(mf/use-callback
(mf/deps profile)
@ -59,7 +53,7 @@
[:div.sidebar-content-section
[:div.back-to-dashboard {:on-click go-dashboard}
[:span.icon i/arrow-down]
[:span.text (t locale "labels.dashboard")]]]
[:span.text (tr "labels.dashboard")]]]
[:hr]
[:div.sidebar-content-section
@ -67,25 +61,30 @@
[:li {:class (when profile? "current")
:on-click go-settings-profile}
i/user
[:span.element-title (t locale "labels.profile")]]
[:span.element-title (tr "labels.profile")]]
[:li {:class (when password? "current")
:on-click go-settings-password}
i/lock
[:span.element-title (t locale "labels.password")]]
[:span.element-title (tr "labels.password")]]
[:li {:class (when options? "current")
:on-click go-settings-options}
i/tree
[:span.element-title (t locale "labels.settings")]]]]]))
[:span.element-title (tr "labels.settings")]]
(when cfg/feedback-enabled
[:li {:class (when feedback? "current")
:on-click go-settings-feedback}
i/msg-info
[:span.element-title (tr "labels.give-feedback")]])]]]))
(mf/defc sidebar
{::mf/wrap [mf/memo]}
[{:keys [profile locale section]}]
[:div.dashboard-sidebar.settings
[:div.sidebar-inside
[:& sidebar-content {:locale locale
:profile profile
[:& sidebar-content {:profile profile
:section section}]
[:& profile-section {:profile profile
:locale locale}]]])

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
;; Copyright (c) 2020-2021 UXBOX Labs SL
(ns app.main.ui.workspace.header
(:require
@ -227,9 +227,10 @@
[:li {:on-click on-add-shared}
[:span (tr "dashboard.add-shared")]])
[:li.feedback {:on-click #(.open js/window "https://github.com/penpot/penpot/discussions" "_blank")}
[:span (tr "labels.feedback")]
[:span.primary-badge "ALPHA"]]
(when cfg/feedback-enabled
[:li.feedback {:on-click (st/emitf (rt/nav :settings-feedback))}
[:span (tr "labels.give-feedback")]
[:span.primary-badge "ALPHA"]])
]]]))
;; --- Header Component

View file

@ -57,7 +57,6 @@
form))
(defn- wrap-update-fn
[f {:keys [spec validators]}]
(fn [& args]