From 42c416e3cba5a2dcb9031fe677c349a47e4ece2e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 16 Oct 2025 13:43:09 +0200 Subject: [PATCH 1/6] :paperclip: Add user feedback defaults to backend scripts/_env --- backend/scripts/_env | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/scripts/_env b/backend/scripts/_env index aba7420c27..fb1d8d8e69 100644 --- a/backend/scripts/_env +++ b/backend/scripts/_env @@ -20,6 +20,7 @@ export PENPOT_FLAGS="\ enable-audit-log \ enable-transit-readable-response \ enable-demo-users \ + enable-user-feedback \ disable-secure-session-cookies \ enable-smtp \ enable-prepl-server \ @@ -46,6 +47,8 @@ export PENPOT_MEDIA_MAX_FILE_SIZE=104857600 # Setup default multipart upload size to 300MiB 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_SECRET_ACCESS_KEY=penpot-devenv export PENPOT_OBJECTS_STORAGE_BACKEND=s3 From e8e27c25c0f1e4995d70f911f1bf11c2e10daa02 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 16 Oct 2025 13:44:04 +0200 Subject: [PATCH 2/6] :lipstick: Fix naming on several components under ui ns Following the current naming convention --- frontend/src/app/main/ui.cljs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index f92ab7396c..f9f27e3f05 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -40,16 +40,16 @@ (def verify-token-page (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*)) -(def dashboard-page +(def dashboard-page* (mf/lazy-component app.main.ui.dashboard/dashboard*)) (def settings-page (mf/lazy-component app.main.ui.settings/settings)) -(def workspace-page +(def workspace-page* (mf/lazy-component app.main.ui.workspace/workspace*)) (mf/defc workspace-legacy-redirect* @@ -239,13 +239,13 @@ [:& release-notes-modal {:version (:main cf/version)}]) [:> team-container* {:team-id team-id} - [:> dashboard-page {:profile profile - :section section - :team-id team-id - :search-term search-term - :plugin-url plugin-url - :project-id project-id - :template template}]]]) + [:> dashboard-page* {:profile profile + :section section + :team-id team-id + :search-term search-term + :plugin-url plugin-url + :project-id project-id + :template template}]]]) :workspace (let [params (get params :query) @@ -266,11 +266,11 @@ [:& release-notes-modal {:version (:main cf/version)}])) [:> team-container* {:team-id team-id} - [:> workspace-page {:team-id team-id - :file-id file-id - :page-id page-id - :layout-name layout - :key file-id}]]]) + [:> workspace-page* {:team-id team-id + :file-id file-id + :page-id page-id + :layout-name layout + :key file-id}]]]) :viewer (let [params (get params :query) @@ -287,7 +287,7 @@ share (:share params)] [:? {} - [:> viewer-page + [:> viewer-page* {:page-id page-id :file-id file-id :frame-id frame-id From 95f58ffda598cde981401fcf8c6728d344e0174f Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 16 Oct 2025 13:46:07 +0200 Subject: [PATCH 3/6] :sparkles: Allow add attachements on emails --- backend/src/app/email.clj | 118 ++++++++++++++++++++++---------------- 1 file changed, 67 insertions(+), 51 deletions(-) diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index 75365fe75c..f21d88a935 100644 --- a/backend/src/app/email.clj +++ b/backend/src/app/email.clj @@ -7,6 +7,7 @@ (ns app.email "Main api for send emails." (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.logging :as l] @@ -93,36 +94,44 @@ headers))) (defn- assign-body - [^MimeMessage mmsg {:keys [body charset] :or {charset "utf-8"}}] - (let [mpart (MimeMultipart. "mixed")] + [^MimeMessage mmsg {:keys [body charset attachments] :or {charset "utf-8"}}] + (let [mixed-mpart (MimeMultipart. "mixed")] (cond (string? body) - (let [bpart (MimeBodyPart.)] - (.setContent bpart ^String body (str "text/plain; charset=" charset)) - (.addBodyPart mpart bpart)) - - (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)))) + (let [text-part (MimeBodyPart.)] + (.setText text-part ^String body ^String charset) + (.addBodyPart mixed-mpart text-part)) (map? body) - (let [bpart (MimeBodyPart.)] - (.setContent bpart - ^String (:content body) - ^String (str (:type body "text/plain") "; charset=" charset)) - (.addBodyPart mpart bpart)) + (let [content-part (MimeBodyPart.) + alternative-mpart (MimeMultipart. "alternative")] + + (when-let [content (get body "text/html")] + (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 - (throw (ex-info "Unsupported type" {:body body}))) - (.setContent mmsg mpart) + (throw (IllegalArgumentException. "invalid email body provided"))) + + (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)) (defn- opts->props @@ -210,24 +219,26 @@ (ex/raise :type :internal :code :missing-email-templates)) {:subject subj - :body (into - [{:type "text/plain" - :content text}] - (when html - [{:type "text/html" - :content html}]))})) + :body (d/without-nils + {"text/plain" text + "text/html" html})})) -(def ^:private schema:context - [:map +(def ^:private schema:params + [:map {:title "Email Params"} [:to [:or ::sm/email [::sm/vec ::sm/email]]] [:reply-to {:optional true} ::sm/email] [:from {:optional true} ::sm/email] [:lang {:optional true} ::sm/text] + [:subject {:optional true} ::sm/text] [: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 - (sm/check-fn schema:context)) +(def ^:private check-params + (sm/check-fn schema:params)) (defn template-factory [& {:keys [id schema]}] @@ -235,9 +246,9 @@ (let [check-fn (if schema (sm/check-fn schema) (constantly nil))] - (fn [context] - (let [context (-> context check-context check-fn) - email (build-email-template id context)] + (fn [params] + (let [params (-> params check-params check-fn) + email (build-email-template id params)] (when-not email (ex/raise :type :internal :code :email-template-does-not-exists @@ -245,35 +256,40 @@ :template-id id)) (cond-> (assoc email :id (name id)) - (:extra-data context) - (assoc :extra-data (:extra-data context)) + (:extra-data params) + (assoc :extra-data (:extra-data params)) - (:from context) - (assoc :from (:from context)) + (seq (:attachments params)) + (assoc :attachments (:attachments params)) - (:reply-to context) - (assoc :reply-to (:reply-to context)) + (:from params) + (assoc :from (:from params)) - (:to context) - (assoc :to (:to context))))))) + (:reply-to params) + (assoc :reply-to (:reply-to params)) + + (:to params) + (assoc :to (:to params))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; PUBLIC HIGH-LEVEL API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn render - [email-factory context] - (email-factory context)) + [email-factory params] + (email-factory params)) (defn send! "Schedule an already defined email to be sent using asynchronously using worker task." - [{:keys [::conn ::factory] :as context}] + [{:keys [::conn ::factory] :as params}] (assert (db/connectable? conn) "expected a valid database connection or pool") (let [email (if factory - (factory context) - (dissoc context ::conn))] + (factory params) + (-> params + (dissoc params) + (check-params)))] (wrk/submit! {::wrk/task :sendmail ::wrk/delay 0 ::wrk/max-retries 4 From a32f44a62c44831c55850d97b9ecb4208a0f26a7 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 16 Oct 2025 13:47:00 +0200 Subject: [PATCH 4/6] :bug: Use correct error boundary fallback on ui ns --- frontend/src/app/main/ui.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index f9f27e3f05..16a882f518 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -369,7 +369,7 @@ [:& (mf/provider ctx/current-profile) {:value profile} (if edata [:> static/exception-page* {:data edata :route route}] - [:> error-boundary* {:fallback static/internal-error*} + [:> error-boundary* {:fallback static/exception-page*} [:> notifications/current-notification*] (when route [:> page* {:route route :profile profile}])])]])) From 854ad5bb4dbbf9bc992bb3bd148a48d6f5c210f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Fri, 29 Aug 2025 14:07:55 +0200 Subject: [PATCH 5/6] :sparkles: Improve the way users give us feedback --- frontend/src/app/main/ui.cljs | 9 +- frontend/src/app/main/ui/settings.cljs | 6 +- .../src/app/main/ui/settings/feedback.cljs | 95 +++++++++++++------ .../src/app/main/ui/settings/feedback.scss | 66 +++++++++++-- .../src/app/main/ui/settings/profile.scss | 2 +- .../src/app/main/ui/settings/sidebar.cljs | 2 +- frontend/src/app/main/ui/static.cljs | 32 ++++++- frontend/src/app/main/ui/static.scss | 27 +++++- frontend/translations/en.po | 49 +++++++--- frontend/translations/es.po | 49 +++++++--- 10 files changed, 261 insertions(+), 76 deletions(-) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 16a882f518..b2eb1b065c 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -197,7 +197,14 @@ :settings-subscription :settings-access-tokens :settings-notifications) - [:? [:& settings-page {:route route}]] + (let [params (get params :query) + type (some-> params :type) + report-id (some-> params :report-id) + url-error (some-> params :url-error)] + [:? [:& settings-page {:route route + :type type + :report-id report-id + :url-error url-error}]]) :debug-icons-preview (when *assert* diff --git a/frontend/src/app/main/ui/settings.cljs b/frontend/src/app/main/ui/settings.cljs index 36cd8da7ba..28488e4578 100644 --- a/frontend/src/app/main/ui/settings.cljs +++ b/frontend/src/app/main/ui/settings.cljs @@ -34,7 +34,7 @@ [:h1 {:data-testid "account-title"} (tr "dashboard.your-account-title")]]]) (mf/defc settings - [{:keys [route] :as props}] + [{:keys [route type report-id url-error]}] (let [section (get-in route [:data :name]) profile (mf/deref refs/profile)] @@ -60,7 +60,9 @@ [:& profile-page] :settings-feedback - [:& feedback-page] + [:& feedback-page {:type type + :report-id report-id + :url-error url-error}] :settings-password [:& password-page] diff --git a/frontend/src/app/main/ui/settings/feedback.cljs b/frontend/src/app/main/ui/settings/feedback.cljs index 4555e2a57c..ae49270016 100644 --- a/frontend/src/app/main/ui/settings/feedback.cljs +++ b/frontend/src/app/main/ui/settings/feedback.cljs @@ -16,20 +16,37 @@ [app.main.ui.components.forms :as fm] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] + [app.util.webapi :as wapi] [beicon.v2.core :as rx] [rumext.v2 :as mf])) -(def ^:private schema:feedback-form +(defn schema:feedback-form [url-error] [:map {:title "FeedbackForm"} [:subject [::sm/text {:max 250}]] - [:content [::sm/text {:max 5000}]]]) + [:type [:string {:max 250}]] + [:content [::sm/text {:max 5000}]] + [:penpot-link [::sm/text {:max 2048 :value (or url-error "") :optional true}]]]) (mf/defc feedback-form {::mf/private true} - [] - (let [profile (mf/deref refs/profile) - form (fm/use-form :schema schema:feedback-form) - loading (mf/use-state false) + [{:keys [report type url-error]}] + (let [profile (mf/deref refs/profile) + initial (mf/with-memo [url-error] + {:subject "" + :type (or type "") + :content "" + :penpot-link url-error}) + form (fm/use-form :schema (schema:feedback-form url-error) :initial initial) + loading (mf/use-state false) + report (wapi/create-blob report "text/plain") + report-uri (wapi/create-uri report) + + on-download + (mf/use-fn + (mf/deps report) + (fn [event] + (dom/prevent-default event) + (dom/trigger-download-uri "report" "text/plain" report-uri))) on-succes (mf/use-fn @@ -62,19 +79,41 @@ :form form} ;; --- Feedback section - [:h2 {:class (stl/css :field-title)} (tr "feedback.title")] - [:p {:class (stl/css :field-text)} (tr "feedback.subtitle")] + [:h2 {:class (stl/css :field-title :feedback-title)} (tr "feedback.title-contact-us")] + [:p {:class (stl/css :field-text :feedback-title)} (tr "feedback.subtitle")] [:div {:class (stl/css :fields-row)} [:& fm/input {:label (tr "feedback.subject") :name :subject :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)} [:& fm/textarea - {:label (tr "feedback.description") + {:class (stl/css :feedback-description) + :label (tr "feedback.description") :name :content + :placeholder (tr "feedback.description-placeholder") :rows 5}]] + [:div {:class (stl/css :fields-row)} + [:p {:class (stl/css :field-text)} (tr "feedback.penpot.link")] + [:& fm/input {:label "" + :name :penpot-link + :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* {:label (if @loading (tr "labels.sending") (tr "labels.send")) :class (stl/css :feedback-button-link) @@ -82,30 +121,28 @@ [:hr] - [:h2 {:class (stl/css :field-title)} (tr "feedback.discourse-title")] - [:p {:class (stl/css :field-text)} (tr "feedback.discourse-subtitle1")] + [:h2 {:class (stl/css :feedback-title)} (tr "feedback.other-ways-contact")] - [: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")] - [:p {:class (stl/css :field-text)} (tr "feedback.twitter-subtitle1")] + [:a {:class (stl/css :link) + :href "https://community.penpot.app" + :target "_blank"} + (tr "feedback.discourse-title")] + [:p {:class (stl/css :field-text :bottom-margin)} (tr "feedback.discourse-subtitle1")] - [:a - {:class (stl/css :feedback-button-link) - :href "https://twitter.com/penpotapp" - :target "_blank"} - (tr "feedback.twitter-go-to")]])) + [:a {:class (stl/css :link) + :href "https://x.com/penpotapp" + :target "_blank"} + (tr "feedback.twitter-title")] + [:p {:class (stl/css :field-text)} (tr "feedback.twitter-subtitle1")]])) (mf/defc feedback-page - [] + [{:keys [type report-id url-error]}] (mf/with-effect [] (dom/set-html-title (tr "title.settings.feedback"))) - - [:div {:class (stl/css :dashboard-settings)} - [:div {:class (stl/css :form-container)} - [:& feedback-form]]]) + (let [report (.getItem js/localStorage report-id)] + [:div {:class (stl/css :dashboard-settings)} + [:div {:class (stl/css :form-container)} + [:& feedback-form {:report report + :type type + :url-error url-error}]]])) diff --git a/frontend/src/app/main/ui/settings/feedback.scss b/frontend/src/app/main/ui/settings/feedback.scss index b319db4ebf..ee91182b80 100644 --- a/frontend/src/app/main/ui/settings/feedback.scss +++ b/frontend/src/app/main/ui/settings/feedback.scss @@ -6,24 +6,74 @@ @use "common/refactor/common-refactor" as *; @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 { - textarea { - border-radius: $br-8; - padding: $br-12; + .fields-row { + margin-block-end: $sz-32; + } + + .feedback-description { + @include t.use-typography("body-medium"); + border-radius: b.$br-8; + padding: var(--sp-m); background-color: var(--color-background-tertiary); color: var(--color-foreground-primary); border: none; ::placeholder { - color: var(--color-background-disabled); + color: var(--input-placeholder-color); } &:focus { - outline: $s-1 solid var(--color-accent-primary); + outline: b.$b-1 solid var(--color-accent-primary); } } -} -.feedback-button-link { - @extend .button-primary; + .field-label { + @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; + } } diff --git a/frontend/src/app/main/ui/settings/profile.scss b/frontend/src/app/main/ui/settings/profile.scss index b08da057ca..4e8473b6e8 100644 --- a/frontend/src/app/main/ui/settings/profile.scss +++ b/frontend/src/app/main/ui/settings/profile.scss @@ -11,7 +11,7 @@ width: 100%; justify-content: center; align-items: center; - a:not(.button-primary) { + a:not(.button-primary):not(.link) { color: var(--color-foreground-secondary); } } diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs index 5ebec1e4bc..0808e2299d 100644 --- a/frontend/src/app/main/ui/settings/sidebar.cljs +++ b/frontend/src/app/main/ui/settings/sidebar.cljs @@ -133,7 +133,7 @@ :settings-item true) :on-click go-settings-feedback} 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/wrap [mf/memo] diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index 2f6da79171..3f18836686 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -22,6 +22,7 @@ [app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]] [app.main.ui.auth.register :as register] [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.raw-svg :refer [raw-svg*]] [app.main.ui.icons :as deprecated-icon] @@ -351,7 +352,16 @@ (mf/defc internal-error* [{:keys [on-reset report] :as props}] (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 + (fn [] + (let [report-id (str "report-" (random-uuid))] + (.setItem js/localStorage report-id report) + (st/emit! (rt/nav :settings-feedback {:type "issue" + :report-id report-id + :url-error (rt/get-current-href)}))))) on-download (mf/use-fn @@ -370,11 +380,23 @@ [:> error-container* {} [: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) - [:a {:on-click on-download} "Download report.txt"]) - [:div {:class (stl/css :sign-info)} - [:button {:on-click on-reset} (tr "labels.retry")]]])) + [:a {:class (stl/css :download-link) :on-click on-download} (tr "labels.download" "report.txt")]) + + [: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 "Load exception page info" diff --git a/frontend/src/app/main/ui/static.scss b/frontend/src/app/main/ui/static.scss index 77bbfbff5b..3f77cf7d38 100644 --- a/frontend/src/app/main/ui/static.scss +++ b/frontend/src/app/main/ui/static.scss @@ -5,6 +5,7 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; +@use "./ds/typography.scss" as t; .exception-layout { width: 100%; @@ -133,15 +134,27 @@ } .main-message { - @include deprecated.bigTitleTipography; + @include t.use-typography("title-large"); color: var(--color-foreground-primary); } .desc-message { - @include deprecated.bigTitleTipography; + @include t.use-typography("title-large"); 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 { text-align: center; @@ -333,5 +346,13 @@ .login-container { width: 100%; - background-color: red; + background-color: var(--color-background-error); +} + +.buttons-container { + display: flex; + + .retry-btn { + margin-inline-start: var(--sp-xxl); + } } diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 5bfc2bef3c..376cd0cf0d 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1559,9 +1559,8 @@ msgstr "Old password is incorrect" msgid "feedback.description" msgstr "Description" -#: src/app/main/ui/settings/feedback.cljs:92 -msgid "feedback.discourse-go-to" -msgstr "Go to Penpot forum" +msgid "feedback.description-placeholder" +msgstr "Please describe the reason of your feedback" #: src/app/main/ui/settings/feedback.cljs:86 msgid "feedback.discourse-subtitle1" @@ -1569,6 +1568,9 @@ msgstr "" "We're happy to have you here. If you need help, please search before you " "post." +msgid "feedback.other-ways-contact" +msgstr "Other ways to contact us" + #: src/app/main/ui/settings/feedback.cljs:85 msgid "feedback.discourse-title" msgstr "Penpot community" @@ -1577,6 +1579,21 @@ msgstr "Penpot community" msgid "feedback.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 msgid "feedback.subtitle" msgstr "" @@ -1584,12 +1601,8 @@ msgstr "" "idea or a doubt. A member of our team will respond as soon as possible." #: src/app/main/ui/settings/feedback.cljs:65 -msgid "feedback.title" -msgstr "Email" - -#: src/app/main/ui/settings/feedback.cljs:102 -msgid "feedback.twitter-go-to" -msgstr "Go to X" +msgid "feedback.title-contact-us" +msgstr "Contact us" #: src/app/main/ui/settings/feedback.cljs:96 msgid "feedback.twitter-subtitle1" @@ -2217,6 +2230,9 @@ msgstr "Github repository" msgid "labels.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 msgid "labels.go-back" msgstr "Go back" @@ -2254,10 +2270,11 @@ msgid "labels.installed-fonts" msgstr "Installed fonts" #: src/app/main/ui/static.cljs:373 -msgid "labels.internal-error.desc-message" -msgstr "" -"Something bad happened. Please retry the operation and if the problem " -"persists, contact support." +msgid "labels.internal-error.desc-message-first" +msgstr "Something bad happened." + +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 msgid "labels.internal-error.main-message" @@ -2525,6 +2542,12 @@ msgstr "Restore" msgid "labels.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 msgid "labels.role" msgstr "Role" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 6b31583b5a..a33be1b5c9 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1556,9 +1556,8 @@ msgstr "La contraseña anterior no es correcta" msgid "feedback.description" msgstr "Descripción" -#: src/app/main/ui/settings/feedback.cljs:92 -msgid "feedback.discourse-go-to" -msgstr "Ir al foro de Penpot" +msgid "feedback.description-placeholder" +msgstr "Describe el motivo de tu comentario" #: src/app/main/ui/settings/feedback.cljs:86 msgid "feedback.discourse-subtitle1" @@ -1566,6 +1565,9 @@ msgstr "" "Estamos encantados de tenerte por aquí. Si necesitas ayuda, busca, escribe " "o pregunta lo que necesites." +msgid "feedback.other-ways-contact" +msgstr "Otras formas de contactarnos" + #: src/app/main/ui/settings/feedback.cljs:85 msgid "feedback.discourse-title" msgstr "Comunidad de Penpot" @@ -1574,6 +1576,21 @@ msgstr "Comunidad de Penpot" msgid "feedback.subject" 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 msgid "feedback.subtitle" msgstr "" @@ -1582,12 +1599,8 @@ msgstr "" "pronto como sea posible." #: src/app/main/ui/settings/feedback.cljs:65 -msgid "feedback.title" -msgstr "Correo electrónico" - -#: src/app/main/ui/settings/feedback.cljs:102 -msgid "feedback.twitter-go-to" -msgstr "Ir a X" +msgid "feedback.title-contact-us" +msgstr "Contáctanos" #: src/app/main/ui/settings/feedback.cljs:96 msgid "feedback.twitter-subtitle1" @@ -2195,6 +2208,9 @@ msgstr "Repositorio de Github" msgid "labels.give-feedback" 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 msgid "labels.go-back" msgstr "Volver" @@ -2232,10 +2248,11 @@ msgid "labels.installed-fonts" msgstr "Fuentes instaladas" #: src/app/main/ui/static.cljs:373 -msgid "labels.internal-error.desc-message" -msgstr "" -"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-first" +msgstr "Ha ocurrido algo extraño." + +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 msgid "labels.internal-error.main-message" @@ -2495,6 +2512,12 @@ msgstr "Restaurar" msgid "labels.retry" 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 msgid "labels.role" msgstr "Rol" From fbbee98c3dc4a12e1c659316ed53a10aa19762a2 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 16 Oct 2025 13:47:43 +0200 Subject: [PATCH 6/6] :sparkles: Add proper backend integration of for new feedback form --- backend/resources/app/email/feedback/en.html | 53 ++++++++------- backend/resources/app/email/feedback/en.subj | 2 +- backend/resources/app/email/feedback/en.txt | 15 +++-- backend/src/app/email.clj | 6 +- backend/src/app/rpc/commands/feedback.clj | 34 +++++++--- .../test/backend_tests/email_sending_test.clj | 2 +- frontend/src/app/main/errors.cljs | 3 + frontend/src/app/main/ui.cljs | 19 +++--- frontend/src/app/main/ui/settings.cljs | 12 ++-- .../src/app/main/ui/settings/feedback.cljs | 67 ++++++++++++------- frontend/src/app/main/ui/static.cljs | 21 ++++-- 11 files changed, 143 insertions(+), 91 deletions(-) diff --git a/backend/resources/app/email/feedback/en.html b/backend/resources/app/email/feedback/en.html index 6de9cda624..742d4c3f66 100644 --- a/backend/resources/app/email/feedback/en.html +++ b/backend/resources/app/email/feedback/en.html @@ -8,38 +8,41 @@

Feedback from:
- {% if profile %} - - Name: - {{profile.fullname|abbreviate:25}} - -
- - - Email: - {{profile.email}} - -
- - - ID: - {{profile.id}} - - {% else %} - - Email: - {{profile.email}} - - {% endif %} + + Name: + {{profile.fullname|abbreviate:25}} + +
+ + Email: + {{profile.email}} + +
+ + ID: + {{profile.id}} +

Subject:
- {{subject|abbreviate:300}} + {{feedback-subject|abbreviate:300}}

+

+ Type:
+ {{feedback-type|abbreviate:300}} +

+ + {% if feedback-error-href %} +

+ Error HREF:
+ {{feedback-error-href|abbreviate:500}} +

+ {% endif %} +

Message:
- {{content|linebreaks-br|safe}} + {{feedback-content|linebreaks-br}}

diff --git a/backend/resources/app/email/feedback/en.subj b/backend/resources/app/email/feedback/en.subj index aa226f6df0..82bacc6648 100644 --- a/backend/resources/app/email/feedback/en.subj +++ b/backend/resources/app/email/feedback/en.subj @@ -1 +1 @@ -[PENPOT FEEDBACK]: {{subject}} +[PENPOT FEEDBACK]: {{feedback-subject}} diff --git a/backend/resources/app/email/feedback/en.txt b/backend/resources/app/email/feedback/en.txt index a60d380c8e..76a42e42cf 100644 --- a/backend/resources/app/email/feedback/en.txt +++ b/backend/resources/app/email/feedback/en.txt @@ -1,9 +1,10 @@ -{% if profile %} -Feedback profile: {{profile.fullname}} <{{profile.email}}> / {{profile.id}} -{% else %} -Feedback from: {{email}} -{% endif %} +From: {{profile.fullname}} <{{profile.email}}> / {{profile.id}} +Subject: {{feedback-subject}} +Type: {{feedback-type}} +{%- if feedback-error-href %} +HREF: {{feedback-error-href}} +{% endif -%} -Subject: {{subject}} +Message: -{{content}} +{{feedback-content}} diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index f21d88a935..91563b4d95 100644 --- a/backend/src/app/email.clj +++ b/backend/src/app/email.clj @@ -359,8 +359,10 @@ (def ^:private schema:feedback [:map - [:subject ::sm/text] - [:content ::sm/text]]) + [:feedback-subject ::sm/text] + [:feedback-type ::sm/text] + [:feedback-content ::sm/text] + [:profile :map]]) (def user-feedback "A profile feedback email." diff --git a/backend/src/app/rpc/commands/feedback.clj b/backend/src/app/rpc/commands/feedback.clj index e3525ded47..fea115d858 100644 --- a/backend/src/app/rpc/commands/feedback.clj +++ b/backend/src/app/rpc/commands/feedback.clj @@ -7,6 +7,7 @@ (ns app.rpc.commands.feedback "A general purpose feedback module." (:require + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.schema :as sm] [app.config :as cf] @@ -21,8 +22,11 @@ (def ^:private schema:send-user-feedback [:map {:title "send-user-feedback"} - [:subject [:string {:max 400}]] - [:content [:string {:max 2500}]]]) + [:subject [:string {:max 500}]] + [: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 {::doc/added "1.18" @@ -39,16 +43,26 @@ (defn- send-user-feedback! [pool profile params] - (let [dest (or (cf/get :user-feedback-destination) - ;; LEGACY - (cf/get :feedback-destination))] + (let [destination + (or (cf/get :user-feedback-destination) + ;; LEGACY + (cf/get :feedback-destination)) + + attachments + (d/without-nils + {"error-report.txt" (:error-report params)})] + (eml/send! {::eml/conn pool ::eml/factory eml/user-feedback - :from dest - :to dest - :profile profile + :from (cf/get :smtp-default-from) + :to destination :reply-to (:email profile) :email (:email profile) - :subject (:subject params) - :content (:content params)}) + :attachments attachments + + :feedback-subject (:subject params) + :feedback-type (:type params "not-specified") + :feedback-content (:content params) + :feedback-error-href (:error-href params) + :profile profile}) nil)) diff --git a/backend/test/backend_tests/email_sending_test.clj b/backend/test/backend_tests/email_sending_test.clj index a61825ae49..a6f66c7b3c 100644 --- a/backend/test/backend_tests/email_sending_test.clj +++ b/backend/test/backend_tests/email_sending_test.clj @@ -22,4 +22,4 @@ (t/is (contains? result :body)) (t/is (contains? result :to)) #_(t/is (contains? result :reply-to)) - (t/is (vector? (:body result))))) + (t/is (map? (:body result))))) diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 4f2b6009c8..ed8462058f 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -25,6 +25,9 @@ ;; From app.main.data.workspace we can use directly because it causes a circular dependency (def reload-file nil) +;; Will contain the latest error report assigned +(def last-report nil) + (defn- print-data! [data] (-> data diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index b2eb1b065c..bdb595cf2c 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -46,8 +46,8 @@ (def dashboard-page* (mf/lazy-component app.main.ui.dashboard/dashboard*)) -(def settings-page - (mf/lazy-component app.main.ui.settings/settings)) +(def settings-page* + (mf/lazy-component app.main.ui.settings/settings*)) (def workspace-page* (mf/lazy-component app.main.ui.workspace/workspace*)) @@ -197,14 +197,13 @@ :settings-subscription :settings-access-tokens :settings-notifications) - (let [params (get params :query) - type (some-> params :type) - report-id (some-> params :report-id) - url-error (some-> params :url-error)] - [:? [:& settings-page {:route route - :type type - :report-id report-id - :url-error url-error}]]) + (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 (when *assert* diff --git a/frontend/src/app/main/ui/settings.cljs b/frontend/src/app/main/ui/settings.cljs index 28488e4578..5935620068 100644 --- a/frontend/src/app/main/ui/settings.cljs +++ b/frontend/src/app/main/ui/settings.cljs @@ -16,7 +16,7 @@ [app.main.ui.settings.access-tokens :refer [access-tokens-page]] [app.main.ui.settings.change-email] [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.options :refer [options-page]] [app.main.ui.settings.password :refer [password-page]] @@ -33,8 +33,8 @@ [:div {:class (stl/css :dashboard-title)} [:h1 {:data-testid "account-title"} (tr "dashboard.your-account-title")]]]) -(mf/defc settings - [{:keys [route type report-id url-error]}] +(mf/defc settings* + [{:keys [route type error-report-id error-href]}] (let [section (get-in route [:data :name]) profile (mf/deref refs/profile)] @@ -60,9 +60,9 @@ [:& profile-page] :settings-feedback - [:& feedback-page {:type type - :report-id report-id - :url-error url-error}] + [:> feedback-page* {:type type + :error-report-id error-report-id + :error-href error-href}] :settings-password [:& password-page] diff --git a/frontend/src/app/main/ui/settings/feedback.cljs b/frontend/src/app/main/ui/settings/feedback.cljs index ae49270016..de25cde1e1 100644 --- a/frontend/src/app/main/ui/settings/feedback.cljs +++ b/frontend/src/app/main/ui/settings/feedback.cljs @@ -8,45 +8,63 @@ "Feedback form." (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] [app.common.schema :as sm] [app.main.data.notifications :as ntf] + [app.main.errors :as errors] [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] [app.main.ui.components.forms :as fm] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] + [app.util.timers :as tm] [app.util.webapi :as wapi] [beicon.v2.core :as rx] [rumext.v2 :as mf])) -(defn schema:feedback-form [url-error] +(def ^:private schema:feedback-form [:map {:title "FeedbackForm"} [:subject [::sm/text {:max 250}]] [:type [:string {:max 250}]] [:content [::sm/text {:max 5000}]] - [:penpot-link [::sm/text {:max 2048 :value (or url-error "") :optional true}]]]) + [: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} - [{:keys [report type url-error]}] - (let [profile (mf/deref refs/profile) - initial (mf/with-memo [url-error] - {:subject "" - :type (or type "") - :content "" - :penpot-link url-error}) - form (fm/use-form :schema (schema:feedback-form url-error) :initial initial) - loading (mf/use-state false) - report (wapi/create-blob report "text/plain") - report-uri (wapi/create-uri report) + [{:keys [error-report type error-href]}] + (let [profile (mf/deref refs/profile) + + 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) - (dom/trigger-download-uri "report" "text/plain" report-uri))) + (let [uri (wapi/create-uri report)] + (dom/trigger-download-uri "report" "text/plain" uri) + (tm/schedule-on-idle #(wapi/revoke-uri uri))))) + on-succes (mf/use-fn @@ -74,11 +92,12 @@ (->> (rp/cmd! :send-user-feedback data) (rx/subs! on-succes on-error)))))] + [:& fm/form {:class (stl/css :feedback-form) :on-submit on-submit :form form} - ;; --- Feedback section + ;; --- Feedback section [:h2 {:class (stl/css :field-title :feedback-title)} (tr "feedback.title-contact-us")] [:p {:class (stl/css :field-text :feedback-title)} (tr "feedback.subtitle")] @@ -106,7 +125,7 @@ [:div {:class (stl/css :fields-row)} [:p {:class (stl/css :field-text)} (tr "feedback.penpot.link")] [:& fm/input {:label "" - :name :penpot-link + :name :error-href :placeholder "https://penpot.app/" :show-success? true}] @@ -136,13 +155,15 @@ (tr "feedback.twitter-title")] [:p {:class (stl/css :field-text)} (tr "feedback.twitter-subtitle1")]])) -(mf/defc feedback-page - [{:keys [type report-id url-error]}] +(mf/defc feedback-page* + [{:keys [error-report-id] :as props}] (mf/with-effect [] (dom/set-html-title (tr "title.settings.feedback"))) - (let [report (.getItem js/localStorage report-id)] + + (let [report (when (= error-report-id (:id errors/last-report)) + (:content errors/last-report)) + props (mf/spread-props props {:error-report report})] + [:div {:class (stl/css :dashboard-settings)} [:div {:class (stl/css :form-container)} - [:& feedback-form {:report report - :type type - :url-error url-error}]]])) + [:> feedback-form* props]]])) diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index 3f18836686..f56688b7ef 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -11,9 +11,11 @@ [app.common.data :as d] [app.common.pprint :as pp] [app.common.uri :as u] + [app.common.uuid :as uuid] [app.main.data.auth :refer [is-authenticated?]] [app.main.data.common :as dcm] [app.main.data.event :as ev] + [app.main.errors :as errors] [app.main.refs :as refs] [app.main.repo :as rp] [app.main.router :as rt] @@ -29,6 +31,7 @@ [app.main.ui.viewer.header :as viewer.header] [app.util.dom :as dom] [app.util.i18n :refer [tr]] + [app.util.timers :as tm] [app.util.webapi :as wapi] [beicon.v2.core :as rx] [cuerdas.core :as str] @@ -352,16 +355,20 @@ (mf/defc internal-error* [{:keys [on-reset report] :as props}] (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 [] - (let [report-id (str "report-" (random-uuid))] - (.setItem js/localStorage report-id report) - (st/emit! (rt/nav :settings-feedback {:type "issue" - :report-id report-id - :url-error (rt/get-current-href)}))))) + (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 (mf/use-fn @@ -372,6 +379,7 @@ (mf/with-effect [report] (when (some? report) + (set! errors/last-report report) (let [report (wapi/create-blob report "text/plain") uri (wapi/create-uri report)] (mf/set-ref-val! report-uri uri) @@ -444,6 +452,7 @@ :path (get route :path) :report report :params params})))) + (case type :not-found [:> not-found* {}]