Merge pull request #7511 from penpot/marina-improve-users-give-feedback

 Improve the way users give us feedback
This commit is contained in:
Marina López
2025-10-17 10:19:34 +02:00
committed by GitHub
18 changed files with 449 additions and 193 deletions

View File

@@ -8,38 +8,41 @@
<body> <body>
<p> <p>
<strong>Feedback from:</strong><br /> <strong>Feedback from:</strong><br />
{% if profile %} <span>
<span> <span>Name: </span>
<span>Name: </span> <span><code>{{profile.fullname|abbreviate:25}}</code></span>
<span><code>{{profile.fullname|abbreviate:25}}</code></span> </span>
</span> <br />
<br /> <span>
<span>Email: </span>
<span> <span>{{profile.email}}</span>
<span>Email: </span> </span>
<span>{{profile.email}}</span> <br />
</span> <span>
<br /> <span>ID: </span>
<span><code>{{profile.id}}</code></span>
<span> </span>
<span>ID: </span>
<span><code>{{profile.id}}</code></span>
</span>
{% else %}
<span>
<span>Email: </span>
<span>{{profile.email}}</span>
</span>
{% endif %}
</p> </p>
<p> <p>
<strong>Subject:</strong><br /> <strong>Subject:</strong><br />
<span>{{subject|abbreviate:300}}</span> <span>{{feedback-subject|abbreviate:300}}</span>
</p> </p>
<p>
<strong>Type:</strong><br />
<span>{{feedback-type|abbreviate:300}}</span>
</p>
{% if feedback-error-href %}
<p>
<strong>Error HREF:</strong><br />
<span>{{feedback-error-href|abbreviate:500}}</span>
</p>
{% endif %}
<p> <p>
<strong>Message:</strong><br /> <strong>Message:</strong><br />
{{content|linebreaks-br|safe}} {{feedback-content|linebreaks-br}}
</p> </p>
</body> </body>
</html> </html>

View File

@@ -1 +1 @@
[PENPOT FEEDBACK]: {{subject}} [PENPOT FEEDBACK]: {{feedback-subject}}

View File

@@ -1,9 +1,10 @@
{% if profile %} From: {{profile.fullname}} <{{profile.email}}> / {{profile.id}}
Feedback profile: {{profile.fullname}} <{{profile.email}}> / {{profile.id}} Subject: {{feedback-subject}}
{% else %} Type: {{feedback-type}}
Feedback from: {{email}} {%- if feedback-error-href %}
{% endif %} HREF: {{feedback-error-href}}
{% endif -%}
Subject: {{subject}} Message:
{{content}} {{feedback-content}}

View File

@@ -20,6 +20,7 @@ export PENPOT_FLAGS="\
enable-audit-log \ enable-audit-log \
enable-transit-readable-response \ enable-transit-readable-response \
enable-demo-users \ enable-demo-users \
enable-user-feedback \
disable-secure-session-cookies \ disable-secure-session-cookies \
enable-smtp \ enable-smtp \
enable-prepl-server \ enable-prepl-server \
@@ -46,6 +47,8 @@ export PENPOT_MEDIA_MAX_FILE_SIZE=104857600
# Setup default multipart upload size to 300MiB # Setup default multipart upload size to 300MiB
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=314572800 export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=314572800
export PENPOT_USER_FEEDBACK_DESTINATION="support@example.com"
export AWS_ACCESS_KEY_ID=penpot-devenv export AWS_ACCESS_KEY_ID=penpot-devenv
export AWS_SECRET_ACCESS_KEY=penpot-devenv export AWS_SECRET_ACCESS_KEY=penpot-devenv
export PENPOT_OBJECTS_STORAGE_BACKEND=s3 export PENPOT_OBJECTS_STORAGE_BACKEND=s3

View File

@@ -7,6 +7,7 @@
(ns app.email (ns app.email
"Main api for send emails." "Main api for send emails."
(:require (:require
[app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
@@ -93,36 +94,44 @@
headers))) headers)))
(defn- assign-body (defn- assign-body
[^MimeMessage mmsg {:keys [body charset] :or {charset "utf-8"}}] [^MimeMessage mmsg {:keys [body charset attachments] :or {charset "utf-8"}}]
(let [mpart (MimeMultipart. "mixed")] (let [mixed-mpart (MimeMultipart. "mixed")]
(cond (cond
(string? body) (string? body)
(let [bpart (MimeBodyPart.)] (let [text-part (MimeBodyPart.)]
(.setContent bpart ^String body (str "text/plain; charset=" charset)) (.setText text-part ^String body ^String charset)
(.addBodyPart mpart bpart)) (.addBodyPart mixed-mpart text-part))
(vector? body)
(let [mmp (MimeMultipart. "alternative")
mbp (MimeBodyPart.)]
(.addBodyPart mpart mbp)
(.setContent mbp mmp)
(doseq [item body]
(let [mbp (MimeBodyPart.)]
(.setContent mbp
^String (:content item)
^String (str (:type item "text/plain") "; charset=" charset))
(.addBodyPart mmp mbp))))
(map? body) (map? body)
(let [bpart (MimeBodyPart.)] (let [content-part (MimeBodyPart.)
(.setContent bpart alternative-mpart (MimeMultipart. "alternative")]
^String (:content body)
^String (str (:type body "text/plain") "; charset=" charset)) (when-let [content (get body "text/html")]
(.addBodyPart mpart bpart)) (let [html-part (MimeBodyPart.)]
(.setContent html-part ^String content
(str "text/html; charset=" charset))
(.addBodyPart alternative-mpart html-part)))
(when-let [content (get body "text/plain")]
(let [text-part (MimeBodyPart.)]
(.setText text-part ^String content ^String charset)
(.addBodyPart alternative-mpart text-part)))
(.setContent content-part alternative-mpart)
(.addBodyPart mixed-mpart content-part))
:else :else
(throw (ex-info "Unsupported type" {:body body}))) (throw (IllegalArgumentException. "invalid email body provided")))
(.setContent mmsg mpart)
(doseq [[name content] attachments]
(prn "attachment" name)
(let [attachment-part (MimeBodyPart.)]
(.setFileName attachment-part ^String name)
(.setContent attachment-part ^String content (str "text/plain; charset=" charset))
(.addBodyPart mixed-mpart attachment-part)))
(.setContent mmsg mixed-mpart)
mmsg)) mmsg))
(defn- opts->props (defn- opts->props
@@ -210,24 +219,26 @@
(ex/raise :type :internal (ex/raise :type :internal
:code :missing-email-templates)) :code :missing-email-templates))
{:subject subj {:subject subj
:body (into :body (d/without-nils
[{:type "text/plain" {"text/plain" text
:content text}] "text/html" html})}))
(when html
[{:type "text/html"
:content html}]))}))
(def ^:private schema:context (def ^:private schema:params
[:map [:map {:title "Email Params"}
[:to [:or ::sm/email [::sm/vec ::sm/email]]] [:to [:or ::sm/email [::sm/vec ::sm/email]]]
[:reply-to {:optional true} ::sm/email] [:reply-to {:optional true} ::sm/email]
[:from {:optional true} ::sm/email] [:from {:optional true} ::sm/email]
[:lang {:optional true} ::sm/text] [:lang {:optional true} ::sm/text]
[:subject {:optional true} ::sm/text]
[:priority {:optional true} [:enum :high :low]] [:priority {:optional true} [:enum :high :low]]
[:extra-data {:optional true} ::sm/text]]) [:extra-data {:optional true} ::sm/text]
[:body {:optional true}
[:or :string [:map-of :string :string]]]
[:attachments {:optional true}
[:map-of :string :string]]])
(def ^:private check-context (def ^:private check-params
(sm/check-fn schema:context)) (sm/check-fn schema:params))
(defn template-factory (defn template-factory
[& {:keys [id schema]}] [& {:keys [id schema]}]
@@ -235,9 +246,9 @@
(let [check-fn (if schema (let [check-fn (if schema
(sm/check-fn schema) (sm/check-fn schema)
(constantly nil))] (constantly nil))]
(fn [context] (fn [params]
(let [context (-> context check-context check-fn) (let [params (-> params check-params check-fn)
email (build-email-template id context)] email (build-email-template id params)]
(when-not email (when-not email
(ex/raise :type :internal (ex/raise :type :internal
:code :email-template-does-not-exists :code :email-template-does-not-exists
@@ -245,35 +256,40 @@
:template-id id)) :template-id id))
(cond-> (assoc email :id (name id)) (cond-> (assoc email :id (name id))
(:extra-data context) (:extra-data params)
(assoc :extra-data (:extra-data context)) (assoc :extra-data (:extra-data params))
(:from context) (seq (:attachments params))
(assoc :from (:from context)) (assoc :attachments (:attachments params))
(:reply-to context) (:from params)
(assoc :reply-to (:reply-to context)) (assoc :from (:from params))
(:to context) (:reply-to params)
(assoc :to (:to context))))))) (assoc :reply-to (:reply-to params))
(:to params)
(assoc :to (:to params)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC HIGH-LEVEL API ;; PUBLIC HIGH-LEVEL API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn render (defn render
[email-factory context] [email-factory params]
(email-factory context)) (email-factory params))
(defn send! (defn send!
"Schedule an already defined email to be sent using asynchronously "Schedule an already defined email to be sent using asynchronously
using worker task." using worker task."
[{:keys [::conn ::factory] :as context}] [{:keys [::conn ::factory] :as params}]
(assert (db/connectable? conn) "expected a valid database connection or pool") (assert (db/connectable? conn) "expected a valid database connection or pool")
(let [email (if factory (let [email (if factory
(factory context) (factory params)
(dissoc context ::conn))] (-> params
(dissoc params)
(check-params)))]
(wrk/submit! {::wrk/task :sendmail (wrk/submit! {::wrk/task :sendmail
::wrk/delay 0 ::wrk/delay 0
::wrk/max-retries 4 ::wrk/max-retries 4
@@ -343,8 +359,10 @@
(def ^:private schema:feedback (def ^:private schema:feedback
[:map [:map
[:subject ::sm/text] [:feedback-subject ::sm/text]
[:content ::sm/text]]) [:feedback-type ::sm/text]
[:feedback-content ::sm/text]
[:profile :map]])
(def user-feedback (def user-feedback
"A profile feedback email." "A profile feedback email."

View File

@@ -7,6 +7,7 @@
(ns app.rpc.commands.feedback (ns app.rpc.commands.feedback
"A general purpose feedback module." "A general purpose feedback module."
(:require (:require
[app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.config :as cf] [app.config :as cf]
@@ -21,8 +22,11 @@
(def ^:private schema:send-user-feedback (def ^:private schema:send-user-feedback
[:map {:title "send-user-feedback"} [:map {:title "send-user-feedback"}
[:subject [:string {:max 400}]] [:subject [:string {:max 500}]]
[:content [:string {:max 2500}]]]) [:content [:string {:max 2500}]]
[:type {:optional true} :string]
[:error-href {:optional true} [:string {:max 2500}]]
[:error-report {:optional true} :string]])
(sv/defmethod ::send-user-feedback (sv/defmethod ::send-user-feedback
{::doc/added "1.18" {::doc/added "1.18"
@@ -39,16 +43,26 @@
(defn- send-user-feedback! (defn- send-user-feedback!
[pool profile params] [pool profile params]
(let [dest (or (cf/get :user-feedback-destination) (let [destination
;; LEGACY (or (cf/get :user-feedback-destination)
(cf/get :feedback-destination))] ;; LEGACY
(cf/get :feedback-destination))
attachments
(d/without-nils
{"error-report.txt" (:error-report params)})]
(eml/send! {::eml/conn pool (eml/send! {::eml/conn pool
::eml/factory eml/user-feedback ::eml/factory eml/user-feedback
:from dest :from (cf/get :smtp-default-from)
:to dest :to destination
:profile profile
:reply-to (:email profile) :reply-to (:email profile)
:email (:email profile) :email (:email profile)
:subject (:subject params) :attachments attachments
:content (:content params)})
:feedback-subject (:subject params)
:feedback-type (:type params "not-specified")
:feedback-content (:content params)
:feedback-error-href (:error-href params)
:profile profile})
nil)) nil))

View File

@@ -22,4 +22,4 @@
(t/is (contains? result :body)) (t/is (contains? result :body))
(t/is (contains? result :to)) (t/is (contains? result :to))
#_(t/is (contains? result :reply-to)) #_(t/is (contains? result :reply-to))
(t/is (vector? (:body result))))) (t/is (map? (:body result)))))

View File

@@ -25,6 +25,9 @@
;; From app.main.data.workspace we can use directly because it causes a circular dependency ;; From app.main.data.workspace we can use directly because it causes a circular dependency
(def reload-file nil) (def reload-file nil)
;; Will contain the latest error report assigned
(def last-report nil)
(defn- print-data! (defn- print-data!
[data] [data]
(-> data (-> data

View File

@@ -40,16 +40,16 @@
(def verify-token-page (def verify-token-page
(mf/lazy-component app.main.ui.auth.verify-token/verify-token)) (mf/lazy-component app.main.ui.auth.verify-token/verify-token))
(def viewer-page (def viewer-page*
(mf/lazy-component app.main.ui.viewer/viewer*)) (mf/lazy-component app.main.ui.viewer/viewer*))
(def dashboard-page (def dashboard-page*
(mf/lazy-component app.main.ui.dashboard/dashboard*)) (mf/lazy-component app.main.ui.dashboard/dashboard*))
(def settings-page (def settings-page*
(mf/lazy-component app.main.ui.settings/settings)) (mf/lazy-component app.main.ui.settings/settings*))
(def workspace-page (def workspace-page*
(mf/lazy-component app.main.ui.workspace/workspace*)) (mf/lazy-component app.main.ui.workspace/workspace*))
(mf/defc workspace-legacy-redirect* (mf/defc workspace-legacy-redirect*
@@ -197,7 +197,13 @@
:settings-subscription :settings-subscription
:settings-access-tokens :settings-access-tokens
:settings-notifications) :settings-notifications)
[:? [:& settings-page {:route route}]] (let [params (get params :query)
error-report-id (some-> params :error-report-id uuid/parse*)]
[:? [:> settings-page*
{:route route
:type (get params :type)
:error-report-id error-report-id
:error-href (get params :error-href)}]])
:debug-icons-preview :debug-icons-preview
(when *assert* (when *assert*
@@ -239,13 +245,13 @@
[:& release-notes-modal {:version (:main cf/version)}]) [:& release-notes-modal {:version (:main cf/version)}])
[:> team-container* {:team-id team-id} [:> team-container* {:team-id team-id}
[:> dashboard-page {:profile profile [:> dashboard-page* {:profile profile
:section section :section section
:team-id team-id :team-id team-id
:search-term search-term :search-term search-term
:plugin-url plugin-url :plugin-url plugin-url
:project-id project-id :project-id project-id
:template template}]]]) :template template}]]])
:workspace :workspace
(let [params (get params :query) (let [params (get params :query)
@@ -266,11 +272,11 @@
[:& release-notes-modal {:version (:main cf/version)}])) [:& release-notes-modal {:version (:main cf/version)}]))
[:> team-container* {:team-id team-id} [:> team-container* {:team-id team-id}
[:> workspace-page {:team-id team-id [:> workspace-page* {:team-id team-id
:file-id file-id :file-id file-id
:page-id page-id :page-id page-id
:layout-name layout :layout-name layout
:key file-id}]]]) :key file-id}]]])
:viewer :viewer
(let [params (get params :query) (let [params (get params :query)
@@ -287,7 +293,7 @@
share (:share params)] share (:share params)]
[:? {} [:? {}
[:> viewer-page [:> viewer-page*
{:page-id page-id {:page-id page-id
:file-id file-id :file-id file-id
:frame-id frame-id :frame-id frame-id
@@ -369,7 +375,7 @@
[:& (mf/provider ctx/current-profile) {:value profile} [:& (mf/provider ctx/current-profile) {:value profile}
(if edata (if edata
[:> static/exception-page* {:data edata :route route}] [:> static/exception-page* {:data edata :route route}]
[:> error-boundary* {:fallback static/internal-error*} [:> error-boundary* {:fallback static/exception-page*}
[:> notifications/current-notification*] [:> notifications/current-notification*]
(when route (when route
[:> page* {:route route :profile profile}])])]])) [:> page* {:route route :profile profile}])])]]))

View File

@@ -16,7 +16,7 @@
[app.main.ui.settings.access-tokens :refer [access-tokens-page]] [app.main.ui.settings.access-tokens :refer [access-tokens-page]]
[app.main.ui.settings.change-email] [app.main.ui.settings.change-email]
[app.main.ui.settings.delete-account] [app.main.ui.settings.delete-account]
[app.main.ui.settings.feedback :refer [feedback-page]] [app.main.ui.settings.feedback :refer [feedback-page*]]
[app.main.ui.settings.notifications :refer [notifications-page*]] [app.main.ui.settings.notifications :refer [notifications-page*]]
[app.main.ui.settings.options :refer [options-page]] [app.main.ui.settings.options :refer [options-page]]
[app.main.ui.settings.password :refer [password-page]] [app.main.ui.settings.password :refer [password-page]]
@@ -33,8 +33,8 @@
[:div {:class (stl/css :dashboard-title)} [:div {:class (stl/css :dashboard-title)}
[:h1 {:data-testid "account-title"} (tr "dashboard.your-account-title")]]]) [:h1 {:data-testid "account-title"} (tr "dashboard.your-account-title")]]])
(mf/defc settings (mf/defc settings*
[{:keys [route] :as props}] [{:keys [route type error-report-id error-href]}]
(let [section (get-in route [:data :name]) (let [section (get-in route [:data :name])
profile (mf/deref refs/profile)] profile (mf/deref refs/profile)]
@@ -60,7 +60,9 @@
[:& profile-page] [:& profile-page]
:settings-feedback :settings-feedback
[:& feedback-page] [:> feedback-page* {:type type
:error-report-id error-report-id
:error-href error-href}]
:settings-password :settings-password
[:& password-page] [:& password-page]

View File

@@ -8,28 +8,63 @@
"Feedback form." "Feedback form."
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data :as d]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.main.data.notifications :as ntf] [app.main.data.notifications :as ntf]
[app.main.errors :as errors]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.forms :as fm] [app.main.ui.components.forms :as fm]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.timers :as tm]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def ^:private schema:feedback-form (def ^:private schema:feedback-form
[:map {:title "FeedbackForm"} [:map {:title "FeedbackForm"}
[:subject [::sm/text {:max 250}]] [:subject [::sm/text {:max 250}]]
[:content [::sm/text {:max 5000}]]]) [:type [:string {:max 250}]]
[:content [::sm/text {:max 5000}]]
[:error-report {:optional true} ::sm/text]
[:error-href {:optional true} [::sm/text {:max 2048}]]])
(mf/defc feedback-form (mf/defc feedback-form*
{::mf/private true} {::mf/private true}
[] [{:keys [error-report type error-href]}]
(let [profile (mf/deref refs/profile) (let [profile (mf/deref refs/profile)
form (fm/use-form :schema schema:feedback-form)
loading (mf/use-state false) initial
(mf/with-memo [error-href error-report]
(d/without-nils
{:subject ""
:type (d/nilv type "")
:content ""
:error-href error-href
:error-report error-report}))
form
(fm/use-form :schema schema:feedback-form
:initial initial)
loading
(mf/use-state false)
report
(mf/with-memo [error-report]
(wapi/create-blob error-report "text/plain"))
on-download
(mf/use-fn
(mf/deps report)
(fn [event]
(dom/prevent-default event)
(let [uri (wapi/create-uri report)]
(dom/trigger-download-uri "report" "text/plain" uri)
(tm/schedule-on-idle #(wapi/revoke-uri uri)))))
on-succes on-succes
(mf/use-fn (mf/use-fn
@@ -57,24 +92,47 @@
(->> (rp/cmd! :send-user-feedback data) (->> (rp/cmd! :send-user-feedback data)
(rx/subs! on-succes on-error)))))] (rx/subs! on-succes on-error)))))]
[:& fm/form {:class (stl/css :feedback-form) [:& fm/form {:class (stl/css :feedback-form)
:on-submit on-submit :on-submit on-submit
:form form} :form form}
;; --- Feedback section ;; --- Feedback section
[:h2 {:class (stl/css :field-title)} (tr "feedback.title")] [:h2 {:class (stl/css :field-title :feedback-title)} (tr "feedback.title-contact-us")]
[:p {:class (stl/css :field-text)} (tr "feedback.subtitle")] [:p {:class (stl/css :field-text :feedback-title)} (tr "feedback.subtitle")]
[:div {:class (stl/css :fields-row)} [:div {:class (stl/css :fields-row)}
[:& fm/input {:label (tr "feedback.subject") [:& fm/input {:label (tr "feedback.subject")
:name :subject :name :subject
:show-success? true}]] :show-success? true}]]
[:div {:class (stl/css :fields-row)}
[:label {:class (stl/css :field-label)} (tr "feedback.type")]
[:& fm/select {:label (tr "feedback.type")
:name :type
:options [{:label (tr "feedback.type.idea") :value "idea"}
{:label (tr "feedback.type.issue") :value "issue"}
{:label (tr "feedback.type.doubt") :value "doubt"}]}]]
[:div {:class (stl/css :fields-row :description)} [:div {:class (stl/css :fields-row :description)}
[:& fm/textarea [:& fm/textarea
{:label (tr "feedback.description") {:class (stl/css :feedback-description)
:label (tr "feedback.description")
:name :content :name :content
:placeholder (tr "feedback.description-placeholder")
:rows 5}]] :rows 5}]]
[:div {:class (stl/css :fields-row)}
[:p {:class (stl/css :field-text)} (tr "feedback.penpot.link")]
[:& fm/input {:label ""
:name :error-href
:placeholder "https://penpot.app/"
:show-success? true}]
(when report
[:a {:class (stl/css :link :download-button) :on-click on-download}
(tr "labels.download" "report.txt")])]
[:> fm/submit-button* [:> fm/submit-button*
{:label (if @loading (tr "labels.sending") (tr "labels.send")) {:label (if @loading (tr "labels.sending") (tr "labels.send"))
:class (stl/css :feedback-button-link) :class (stl/css :feedback-button-link)
@@ -82,30 +140,30 @@
[:hr] [:hr]
[:h2 {:class (stl/css :field-title)} (tr "feedback.discourse-title")] [:h2 {:class (stl/css :feedback-title)} (tr "feedback.other-ways-contact")]
[:p {:class (stl/css :field-text)} (tr "feedback.discourse-subtitle1")]
[:a
{:class (stl/css :feedback-button-link)
:href "https://community.penpot.app"
:target "_blank"}
(tr "feedback.discourse-go-to")]
[:hr]
[:h2 {:class (stl/css :field-title)} (tr "feedback.twitter-title")] [:a {:class (stl/css :link)
[:p {:class (stl/css :field-text)} (tr "feedback.twitter-subtitle1")] :href "https://community.penpot.app"
:target "_blank"}
(tr "feedback.discourse-title")]
[:p {:class (stl/css :field-text :bottom-margin)} (tr "feedback.discourse-subtitle1")]
[:a [:a {:class (stl/css :link)
{:class (stl/css :feedback-button-link) :href "https://x.com/penpotapp"
:href "https://twitter.com/penpotapp" :target "_blank"}
:target "_blank"} (tr "feedback.twitter-title")]
(tr "feedback.twitter-go-to")]])) [:p {:class (stl/css :field-text)} (tr "feedback.twitter-subtitle1")]]))
(mf/defc feedback-page (mf/defc feedback-page*
[] [{:keys [error-report-id] :as props}]
(mf/with-effect [] (mf/with-effect []
(dom/set-html-title (tr "title.settings.feedback"))) (dom/set-html-title (tr "title.settings.feedback")))
[:div {:class (stl/css :dashboard-settings)} (let [report (when (= error-report-id (:id errors/last-report))
[:div {:class (stl/css :form-container)} (:content errors/last-report))
[:& feedback-form]]]) props (mf/spread-props props {:error-report report})]
[:div {:class (stl/css :dashboard-settings)}
[:div {:class (stl/css :form-container)}
[:> feedback-form* props]]]))

View File

@@ -6,24 +6,74 @@
@use "common/refactor/common-refactor" as *; @use "common/refactor/common-refactor" as *;
@use "./profile"; @use "./profile";
@use "../ds/typography.scss" as t;
@use "../ds/_borders.scss" as b;
@use "../ds/_sizes.scss" as *;
@use "../ds/_utils.scss" as *;
@use "../ds/spacing.scss" as *;
.feedback-form { .feedback-form {
textarea { .fields-row {
border-radius: $br-8; margin-block-end: $sz-32;
padding: $br-12; }
.feedback-description {
@include t.use-typography("body-medium");
border-radius: b.$br-8;
padding: var(--sp-m);
background-color: var(--color-background-tertiary); background-color: var(--color-background-tertiary);
color: var(--color-foreground-primary); color: var(--color-foreground-primary);
border: none; border: none;
::placeholder { ::placeholder {
color: var(--color-background-disabled); color: var(--input-placeholder-color);
} }
&:focus { &:focus {
outline: $s-1 solid var(--color-accent-primary); outline: b.$b-1 solid var(--color-accent-primary);
} }
} }
}
.feedback-button-link { .field-label {
@extend .button-primary; @include t.use-typography("headline-small");
block-size: $sz-32;
color: var(--color-foreground-primary);
margin-block-end: var(--sp-l);
}
.feedback-button-link {
@extend .button-primary;
margin-block-end: px2rem(72);
}
.feedback-title {
margin-block-end: var(--sp-xxxl);
}
.field-text {
@include t.use-typography("body-medium");
}
.bottom-margin {
margin-block-end: var(--sp-xxxl);
}
.link {
@include t.use-typography("headline-small");
color: var(--color-accent-tertiary);
margin-block-end: var(--sp-s);
}
.download-button {
@include t.use-typography("body-small");
color: var(--color-foreground-primary);
text-transform: lowercase;
border: b.$b-1 solid var(--color-background-quaternary);
margin-block-start: var(--sp-s);
padding: var(--sp-s);
border-radius: b.$br-8;
block-size: $sz-32;
display: inline-block;
min-inline-size: $sz-160;
text-align: center;
}
} }

View File

@@ -11,7 +11,7 @@
width: 100%; width: 100%;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
a:not(.button-primary) { a:not(.button-primary):not(.link) {
color: var(--color-foreground-secondary); color: var(--color-foreground-secondary);
} }
} }

View File

@@ -133,7 +133,7 @@
:settings-item true) :settings-item true)
:on-click go-settings-feedback} :on-click go-settings-feedback}
feedback-icon feedback-icon
[:span {:class (stl/css :element-title)} (tr "labels.give-feedback")]])]]])) [:span {:class (stl/css :element-title)} (tr "labels.contact-us")]])]]]))
(mf/defc sidebar (mf/defc sidebar
{::mf/wrap [mf/memo] {::mf/wrap [mf/memo]

View File

@@ -11,9 +11,11 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.pprint :as pp] [app.common.pprint :as pp]
[app.common.uri :as u] [app.common.uri :as u]
[app.common.uuid :as uuid]
[app.main.data.auth :refer [is-authenticated?]] [app.main.data.auth :refer [is-authenticated?]]
[app.main.data.common :as dcm] [app.main.data.common :as dcm]
[app.main.data.event :as ev] [app.main.data.event :as ev]
[app.main.errors :as errors]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.router :as rt] [app.main.router :as rt]
@@ -22,12 +24,14 @@
[app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]] [app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]]
[app.main.ui.auth.register :as register] [app.main.ui.auth.register :as register]
[app.main.ui.dashboard.sidebar :refer [sidebar*]] [app.main.ui.dashboard.sidebar :refer [sidebar*]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]] [app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]]
[app.main.ui.icons :as deprecated-icon] [app.main.ui.icons :as deprecated-icon]
[app.main.ui.viewer.header :as viewer.header] [app.main.ui.viewer.header :as viewer.header]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.timers :as tm]
[app.util.webapi :as wapi] [app.util.webapi :as wapi]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
@@ -353,6 +357,19 @@
(let [report-uri (mf/use-ref nil) (let [report-uri (mf/use-ref nil)
on-reset (or on-reset #(st/emit! (rt/assign-exception nil))) on-reset (or on-reset #(st/emit! (rt/assign-exception nil)))
support-contact-click
(mf/use-fn
(mf/deps on-reset report)
(fn []
(tm/schedule on-reset)
(let [error-report-id (uuid/next)
error-href (rt/get-current-href)]
(set! errors/last-report {:id error-report-id :content report})
(st/emit!
(rt/nav :settings-feedback {:type "issue"
:error-report-id error-report-id
:error-href error-href})))))
on-download on-download
(mf/use-fn (mf/use-fn
(fn [event] (fn [event]
@@ -362,6 +379,7 @@
(mf/with-effect [report] (mf/with-effect [report]
(when (some? report) (when (some? report)
(set! errors/last-report report)
(let [report (wapi/create-blob report "text/plain") (let [report (wapi/create-blob report "text/plain")
uri (wapi/create-uri report)] uri (wapi/create-uri report)]
(mf/set-ref-val! report-uri uri) (mf/set-ref-val! report-uri uri)
@@ -370,11 +388,23 @@
[:> error-container* {} [:> error-container* {}
[:div {:class (stl/css :main-message)} (tr "labels.internal-error.main-message")] [:div {:class (stl/css :main-message)} (tr "labels.internal-error.main-message")]
[:div {:class (stl/css :desc-message)} (tr "labels.internal-error.desc-message")]
[:div {:class (stl/css :desc-message)}
[:p {:class (stl/css :desc-text)} (tr "labels.internal-error.desc-message-first")]
[:p {:class (stl/css :desc-text)} (tr "labels.internal-error.desc-message-second")]]
(when (some? report) (when (some? report)
[:a {:on-click on-download} "Download report.txt"]) [:a {:class (stl/css :download-link) :on-click on-download} (tr "labels.download" "report.txt")])
[:div {:class (stl/css :sign-info)}
[:button {:on-click on-reset} (tr "labels.retry")]]])) [:div {:class (stl/css :buttons-container)}
[:> button* {:variant "secondary"
:type "button"
:class (stl/css :support-btn)
:on-click support-contact-click} (tr "labels.contact-support")]
[:> button* {:variant "primary"
:type "button"
:class (stl/css :retry-btn)
:on-click on-reset} (tr "labels.retry")]]]))
(defn- load-info (defn- load-info
"Load exception page info" "Load exception page info"
@@ -422,6 +452,7 @@
:path (get route :path) :path (get route :path)
:report report :report report
:params params})))) :params params}))))
(case type (case type
:not-found :not-found
[:> not-found* {}] [:> not-found* {}]

View File

@@ -5,6 +5,7 @@
// Copyright (c) KALEIDOS INC // Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated; @use "refactor/common-refactor.scss" as deprecated;
@use "./ds/typography.scss" as t;
.exception-layout { .exception-layout {
width: 100%; width: 100%;
@@ -133,15 +134,27 @@
} }
.main-message { .main-message {
@include deprecated.bigTitleTipography; @include t.use-typography("title-large");
color: var(--color-foreground-primary); color: var(--color-foreground-primary);
} }
.desc-message { .desc-message {
@include deprecated.bigTitleTipography; @include t.use-typography("title-large");
color: var(--color-foreground-secondary); color: var(--color-foreground-secondary);
} }
.desc-text {
@include t.use-typography("title-large");
color: var(--color-foreground-secondary);
margin-block-end: 0;
}
.download-link {
@include t.use-typography("code-font");
color: var(--color-foreground-primary);
text-transform: lowercase;
}
.sign-info { .sign-info {
text-align: center; text-align: center;
@@ -333,5 +346,13 @@
.login-container { .login-container {
width: 100%; width: 100%;
background-color: red; background-color: var(--color-background-error);
}
.buttons-container {
display: flex;
.retry-btn {
margin-inline-start: var(--sp-xxl);
}
} }

View File

@@ -1559,9 +1559,8 @@ msgstr "Old password is incorrect"
msgid "feedback.description" msgid "feedback.description"
msgstr "Description" msgstr "Description"
#: src/app/main/ui/settings/feedback.cljs:92 msgid "feedback.description-placeholder"
msgid "feedback.discourse-go-to" msgstr "Please describe the reason of your feedback"
msgstr "Go to Penpot forum"
#: src/app/main/ui/settings/feedback.cljs:86 #: src/app/main/ui/settings/feedback.cljs:86
msgid "feedback.discourse-subtitle1" msgid "feedback.discourse-subtitle1"
@@ -1569,6 +1568,9 @@ msgstr ""
"We're happy to have you here. If you need help, please search before you " "We're happy to have you here. If you need help, please search before you "
"post." "post."
msgid "feedback.other-ways-contact"
msgstr "Other ways to contact us"
#: src/app/main/ui/settings/feedback.cljs:85 #: src/app/main/ui/settings/feedback.cljs:85
msgid "feedback.discourse-title" msgid "feedback.discourse-title"
msgstr "Penpot community" msgstr "Penpot community"
@@ -1577,6 +1579,21 @@ msgstr "Penpot community"
msgid "feedback.subject" msgid "feedback.subject"
msgstr "Subject" msgstr "Subject"
msgid "feedback.type"
msgstr "Type"
msgid "feedback.type.idea"
msgstr "Idea"
msgid "feedback.type.issue"
msgstr "Issue"
msgid "feedback.type.doubt"
msgstr "Doubt"
msgid "feedback.penpot.link"
msgstr "If the feedback is something related with a file or a project, add the penpot link in here:"
#: src/app/main/ui/settings/feedback.cljs:66 #: src/app/main/ui/settings/feedback.cljs:66
msgid "feedback.subtitle" msgid "feedback.subtitle"
msgstr "" msgstr ""
@@ -1584,12 +1601,8 @@ msgstr ""
"idea or a doubt. A member of our team will respond as soon as possible." "idea or a doubt. A member of our team will respond as soon as possible."
#: src/app/main/ui/settings/feedback.cljs:65 #: src/app/main/ui/settings/feedback.cljs:65
msgid "feedback.title" msgid "feedback.title-contact-us"
msgstr "Email" msgstr "Contact us"
#: src/app/main/ui/settings/feedback.cljs:102
msgid "feedback.twitter-go-to"
msgstr "Go to X"
#: src/app/main/ui/settings/feedback.cljs:96 #: src/app/main/ui/settings/feedback.cljs:96
msgid "feedback.twitter-subtitle1" msgid "feedback.twitter-subtitle1"
@@ -2217,6 +2230,9 @@ msgstr "Github repository"
msgid "labels.give-feedback" msgid "labels.give-feedback"
msgstr "Give feedback" msgstr "Give feedback"
msgid "labels.contact-us"
msgstr "Contact us"
#: src/app/main/ui/auth/recovery_request.cljs:104, src/app/main/ui/auth/register.cljs:359, src/app/main/ui/static.cljs:170, src/app/main/ui/viewer/login.cljs:111 #: src/app/main/ui/auth/recovery_request.cljs:104, src/app/main/ui/auth/register.cljs:359, src/app/main/ui/static.cljs:170, src/app/main/ui/viewer/login.cljs:111
msgid "labels.go-back" msgid "labels.go-back"
msgstr "Go back" msgstr "Go back"
@@ -2254,10 +2270,11 @@ msgid "labels.installed-fonts"
msgstr "Installed fonts" msgstr "Installed fonts"
#: src/app/main/ui/static.cljs:373 #: src/app/main/ui/static.cljs:373
msgid "labels.internal-error.desc-message" msgid "labels.internal-error.desc-message-first"
msgstr "" msgstr "Something bad happened."
"Something bad happened. Please retry the operation and if the problem "
"persists, contact support." msgid "labels.internal-error.desc-message-second"
msgstr "You can retry the operation or contact support to report the error."
#: src/app/main/ui/static.cljs:372 #: src/app/main/ui/static.cljs:372
msgid "labels.internal-error.main-message" msgid "labels.internal-error.main-message"
@@ -2525,6 +2542,12 @@ msgstr "Restore"
msgid "labels.retry" msgid "labels.retry"
msgstr "Retry" msgstr "Retry"
msgid "labels.download"
msgstr "Download %s"
msgid "labels.contact-support"
msgstr "Contact support"
#: src/app/main/ui/dashboard/team.cljs:513, src/app/main/ui/dashboard/team.cljs:945 #: src/app/main/ui/dashboard/team.cljs:513, src/app/main/ui/dashboard/team.cljs:945
msgid "labels.role" msgid "labels.role"
msgstr "Role" msgstr "Role"

View File

@@ -1556,9 +1556,8 @@ msgstr "La contraseña anterior no es correcta"
msgid "feedback.description" msgid "feedback.description"
msgstr "Descripción" msgstr "Descripción"
#: src/app/main/ui/settings/feedback.cljs:92 msgid "feedback.description-placeholder"
msgid "feedback.discourse-go-to" msgstr "Describe el motivo de tu comentario"
msgstr "Ir al foro de Penpot"
#: src/app/main/ui/settings/feedback.cljs:86 #: src/app/main/ui/settings/feedback.cljs:86
msgid "feedback.discourse-subtitle1" msgid "feedback.discourse-subtitle1"
@@ -1566,6 +1565,9 @@ msgstr ""
"Estamos encantados de tenerte por aquí. Si necesitas ayuda, busca, escribe " "Estamos encantados de tenerte por aquí. Si necesitas ayuda, busca, escribe "
"o pregunta lo que necesites." "o pregunta lo que necesites."
msgid "feedback.other-ways-contact"
msgstr "Otras formas de contactarnos"
#: src/app/main/ui/settings/feedback.cljs:85 #: src/app/main/ui/settings/feedback.cljs:85
msgid "feedback.discourse-title" msgid "feedback.discourse-title"
msgstr "Comunidad de Penpot" msgstr "Comunidad de Penpot"
@@ -1574,6 +1576,21 @@ msgstr "Comunidad de Penpot"
msgid "feedback.subject" msgid "feedback.subject"
msgstr "Asunto" msgstr "Asunto"
msgid "feedback.type"
msgstr "Tipo"
msgid "feedback.type.idea"
msgstr "Idea"
msgid "feedback.type.issue"
msgstr "Problema"
msgid "feedback.type.doubt"
msgstr "Duda"
msgid "feedback.penpot.link"
msgstr "Si el comentario está relacionado con un archivo o un proyecto, añade aquí el enlace de Penpot:"
#: src/app/main/ui/settings/feedback.cljs:66 #: src/app/main/ui/settings/feedback.cljs:66
msgid "feedback.subtitle" msgid "feedback.subtitle"
msgstr "" msgstr ""
@@ -1582,12 +1599,8 @@ msgstr ""
"pronto como sea posible." "pronto como sea posible."
#: src/app/main/ui/settings/feedback.cljs:65 #: src/app/main/ui/settings/feedback.cljs:65
msgid "feedback.title" msgid "feedback.title-contact-us"
msgstr "Correo electrónico" msgstr "Contáctanos"
#: src/app/main/ui/settings/feedback.cljs:102
msgid "feedback.twitter-go-to"
msgstr "Ir a X"
#: src/app/main/ui/settings/feedback.cljs:96 #: src/app/main/ui/settings/feedback.cljs:96
msgid "feedback.twitter-subtitle1" msgid "feedback.twitter-subtitle1"
@@ -2195,6 +2208,9 @@ msgstr "Repositorio de Github"
msgid "labels.give-feedback" msgid "labels.give-feedback"
msgstr "Danos tu opinión" msgstr "Danos tu opinión"
msgid "labels.contact-us"
msgstr "Contáctanos"
#: src/app/main/ui/auth/recovery_request.cljs:104, src/app/main/ui/auth/register.cljs:359, src/app/main/ui/static.cljs:170, src/app/main/ui/viewer/login.cljs:111 #: src/app/main/ui/auth/recovery_request.cljs:104, src/app/main/ui/auth/register.cljs:359, src/app/main/ui/static.cljs:170, src/app/main/ui/viewer/login.cljs:111
msgid "labels.go-back" msgid "labels.go-back"
msgstr "Volver" msgstr "Volver"
@@ -2232,10 +2248,11 @@ msgid "labels.installed-fonts"
msgstr "Fuentes instaladas" msgstr "Fuentes instaladas"
#: src/app/main/ui/static.cljs:373 #: src/app/main/ui/static.cljs:373
msgid "labels.internal-error.desc-message" msgid "labels.internal-error.desc-message-first"
msgstr "" msgstr "Ha ocurrido algo extraño."
"Ha ocurrido algo extraño. Por favor, reintenta la operación, y si el "
"problema persiste, contacta con el servicio técnico." msgid "labels.internal-error.desc-message-second"
msgstr "Puedes reintentar la operación o contacta con soporte para reportar el error."
#: src/app/main/ui/static.cljs:372 #: src/app/main/ui/static.cljs:372
msgid "labels.internal-error.main-message" msgid "labels.internal-error.main-message"
@@ -2495,6 +2512,12 @@ msgstr "Restaurar"
msgid "labels.retry" msgid "labels.retry"
msgstr "Reintentar" msgstr "Reintentar"
msgid "labels.download"
msgstr "Descargar %s"
msgid "labels.contact-support"
msgstr "Contacta con soporte"
#: src/app/main/ui/dashboard/team.cljs:513, src/app/main/ui/dashboard/team.cljs:945 #: src/app/main/ui/dashboard/team.cljs:513, src/app/main/ui/dashboard/team.cljs:945
msgid "labels.role" msgid "labels.role"
msgstr "Rol" msgstr "Rol"