🐛 Add ability to delete uploaded profile avatar (#9068)

Fixes #9067. Adds a delete button that appears on hover over an
uploaded profile photo; clicking it opens a confirm modal and, on
accept, clears the stored photo so the generated fallback avatar is
shown again. A new :delete-profile-photo RPC schedules the old
storage object for garbage collection and sets photo-id to null.

Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
moorsecopers99 2026-04-21 20:19:30 +03:00 committed by GitHub
parent bb91c06390
commit 95b2d7b083
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 108 additions and 2 deletions

View File

@ -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)

View File

@ -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!)

View File

@ -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)

View File

@ -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`")

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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"