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/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 diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index 75365fe75c..91563b4d95 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 @@ -343,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 f92ab7396c..bdb595cf2c 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 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* @@ -197,7 +197,13 @@ :settings-subscription :settings-access-tokens :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 (when *assert* @@ -239,13 +245,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 +272,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 +293,7 @@ share (:share params)] [:? {} - [:> viewer-page + [:> viewer-page* {:page-id page-id :file-id file-id :frame-id frame-id @@ -369,7 +375,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}])])]])) diff --git a/frontend/src/app/main/ui/settings.cljs b/frontend/src/app/main/ui/settings.cljs index 36cd8da7ba..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] :as props}] +(mf/defc settings* + [{:keys [route type error-report-id error-href]}] (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 + :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 4555e2a57c..de25cde1e1 100644 --- a/frontend/src/app/main/ui/settings/feedback.cljs +++ b/frontend/src/app/main/ui/settings/feedback.cljs @@ -8,28 +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])) (def ^:private schema:feedback-form [:map {:title "FeedbackForm"} [: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} - [] + [{:keys [error-report type error-href]}] (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 (mf/use-fn @@ -57,24 +92,47 @@ (->> (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 - [:h2 {:class (stl/css :field-title)} (tr "feedback.title")] - [:p {:class (stl/css :field-text)} (tr "feedback.subtitle")] + ;; --- 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")] [: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 :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* {:label (if @loading (tr "labels.sending") (tr "labels.send")) :class (stl/css :feedback-button-link) @@ -82,30 +140,30 @@ [: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 - [] +(mf/defc feedback-page* + [{:keys [error-report-id] :as props}] (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 (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* props]]])) 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..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] @@ -22,12 +24,14 @@ [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] [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] @@ -353,6 +357,19 @@ (let [report-uri (mf/use-ref 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 (mf/use-fn (fn [event] @@ -362,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) @@ -370,11 +388,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" @@ -422,6 +452,7 @@ :path (get route :path) :report report :params params})))) + (case type :not-found [:> not-found* {}] 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"