diff --git a/CHANGES.md b/CHANGES.md index 6304ec2b43..241f593a73 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -70,6 +70,7 @@ - Fix opacity mixed value [Taiga #13960](https://tree.taiga.io/project/penpot/issue/13960) - Fix gap input throwing an error [Github #8984](https://github.com/penpot/penpot/pull/8984) - Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990) +- Allow deleting the profile avatar after uploading [Github #9067](https://github.com/penpot/penpot/issues/9067) ## 2.15.0 (Unreleased) diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index cfc4a1c66e..2199df39ea 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -314,6 +314,25 @@ (climit/invoke! generate-thumbnail file))] (sto/put-object! storage params))) +;; --- MUTATION: Delete Photo + +(sv/defmethod ::delete-profile-photo + {::doc/added "2.16" + ::sm/params [:map] + ::sm/result :nil + ::db/transaction true} + [{:keys [::db/conn ::sto/storage]} {:keys [::rpc/profile-id]}] + (let [profile (get-profile conn profile-id ::db/for-update true)] + (when-let [id (:photo-id profile)] + (sto/touch-object! storage id)) + + (db/update! conn :profile + {:photo-id nil} + {:id profile-id} + {::db/return-keys false}) + + nil)) + ;; --- MUTATION: Request Email Change (declare ^:private request-email-change!) diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index 1cdf16a99f..d4cfedf871 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -125,7 +125,20 @@ out (th/command! data)] ;; (th/print-result! out) - (t/is (nil? (:error out))))))) + (t/is (nil? (:error out))))) + + (t/testing "delete photo clears photo-id" + (let [data {::th/type :delete-profile-photo + ::rpc/profile-id (:id profile)} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + (let [data {::th/type :get-profile + ::rpc/profile-id (:id profile)} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:photo-id (:result out)))))))) (t/deftest profile-deletion-1 (let [prof (th/create-profile* 1) diff --git a/frontend/src/app/main/data/profile.cljs b/frontend/src/app/main/data/profile.cljs index 66ded6fc8b..2260a734f8 100644 --- a/frontend/src/app/main/data/profile.cljs +++ b/frontend/src/app/main/data/profile.cljs @@ -348,6 +348,23 @@ (rx/map (constantly (refresh-profile))) (rx/catch on-error)))))) +(def delete-photo + (ptk/reify ::delete-photo + ev/Event + (-data [_] {}) + + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:profile :photo-id] nil)) + + ptk/WatchEvent + (watch [_ _ _] + (->> (rp/cmd! :delete-profile-photo {}) + (rx/map (constantly (refresh-profile))) + (rx/catch (fn [cause] + (js/console.error "delete-photo failed" cause) + (rx/of (refresh-profile)))))))) + (defn fetch-file-comments-users [{:keys [team-id]}] (assert (uuid? team-id) "expected a valid uuid for `team-id`") diff --git a/frontend/src/app/main/ui/settings/profile.cljs b/frontend/src/app/main/ui/settings/profile.cljs index 763ee3c836..be2665337d 100644 --- a/frontend/src/app/main/ui/settings/profile.cljs +++ b/frontend/src/app/main/ui/settings/profile.cljs @@ -16,6 +16,7 @@ [app.main.store :as st] [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.forms :as fm] + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) @@ -92,6 +93,7 @@ [] (let [input-ref (mf/use-ref nil) profile (mf/deref refs/profile) + has-photo? (some? (:photo-id profile)) photo (mf/with-memo [profile] @@ -103,13 +105,32 @@ on-file-selected (fn [file] - (st/emit! (du/update-photo file)))] + (st/emit! (du/update-photo file))) + + on-delete-click + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (st/emit! (modal/show + {:type :confirm + :title (tr "labels.delete-profile-photo.title") + :message (tr "labels.delete-profile-photo.message") + :accept-label (tr "labels.delete") + :on-accept (fn [_] (st/emit! du/delete-photo))}))))] [:form {:class (stl/css :avatar-form)} [:div {:class (stl/css :image-change-field)} [:span {:class (stl/css :update-overlay) :on-click on-image-click} (tr "labels.update")] [:img {:src photo}] + (when has-photo? + [:button {:type "button" + :class (stl/css :delete-overlay) + :title (tr "labels.delete") + :aria-label (tr "labels.delete") + :on-click on-delete-click + :data-testid "profile-image-delete"} + [:> icon* {:icon-id i/delete :size "m"}]]) [:& file-uploader {:accept "image/jpeg,image/png" :multi false :ref input-ref diff --git a/frontend/src/app/main/ui/settings/profile.scss b/frontend/src/app/main/ui/settings/profile.scss index 115c33864a..ce9d3b3b0f 100644 --- a/frontend/src/app/main/ui/settings/profile.scss +++ b/frontend/src/app/main/ui/settings/profile.scss @@ -280,6 +280,31 @@ form.avatar-form { z-index: $z-index-modal; } + .delete-overlay { + position: absolute; + top: $s-4; + inset-inline-end: $s-4; + display: flex; + align-items: center; + justify-content: center; + width: $s-32; + height: $s-32; + padding: 0; + border: none; + border-radius: 50%; + background: var(--color-background-primary); + color: var(--color-foreground-primary); + cursor: pointer; + opacity: 0; + transition: opacity 0.15s ease-in-out; + z-index: calc(#{$z-index-modal} + 1); + + &:hover { + background: var(--color-background-quaternary); + color: var(--color-accent-primary); + } + } + input[type="file"] { width: 100%; height: 100%; @@ -294,6 +319,10 @@ form.avatar-form { .update-overlay { opacity: 0.8; } + + .delete-overlay { + opacity: 1; + } } } diff --git a/frontend/translations/en.po b/frontend/translations/en.po index f86456267e..9b0bbc2ad7 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -2594,6 +2594,12 @@ msgstr "Delete %s files" msgid "labels.deleted" msgstr "Deleted" +msgid "labels.delete-profile-photo.title" +msgstr "Delete profile photo" + +msgid "labels.delete-profile-photo.message" +msgstr "Are you sure you want to delete your profile photo?" + #: src/app/main/ui/onboarding/questions.cljs:86 msgid "labels.developer" msgstr "Development"