diff --git a/app/assets/javascripts/profile/edit/components/profile_edit_app.vue b/app/assets/javascripts/profile/edit/components/profile_edit_app.vue index 815b8742500d31d2e99026ce69bd701001490485..eedb5d7764e605cbf49300dce836438c9f7895bd 100644 --- a/app/assets/javascripts/profile/edit/components/profile_edit_app.vue +++ b/app/assets/javascripts/profile/edit/components/profile_edit_app.vue @@ -3,7 +3,6 @@ import { nextTick } from 'vue'; import { GlForm, GlButton } from '@gitlab/ui'; import { VARIANT_DANGER, VARIANT_INFO, createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; -import { readFileAsDataURL } from '~/lib/utils/file_utility'; import SetStatusForm from '~/set_status_modal/set_status_form.vue'; import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue'; import { isUserBusy, computedClearStatusAfterValue } from '~/set_status_modal/utils'; @@ -106,20 +105,12 @@ export default { this.updateProfileSettings = false; } }, - async syncHeaderAvatars() { - const dataURL = await readFileAsDataURL(this.avatarBlob); - - const elements = gon?.use_new_navigation - ? ['[data-testid="user-dropdown"] .gl-avatar'] - : ['.header-user-avatar', '.js-sidebar-user-avatar']; - - elements.forEach((selector) => { - const node = document.querySelector(selector); - if (!node) return; - - node.setAttribute('src', dataURL); - node.setAttribute('srcset', dataURL); - }); + syncHeaderAvatars() { + document.dispatchEvent( + new CustomEvent('userAvatar:update', { + detail: { url: URL.createObjectURL(this.avatarBlob) }, + }), + ); }, onBlobChange(blob) { this.avatarBlob = blob; diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index 4d3824f910ce7c72aec8df4f73fc07df68010ffb..16f0110a1afe1b2fce1c34c167ca797fab4b879e 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -89,12 +89,9 @@ export default class Profile { } updateHeaderAvatar() { - if (gon?.use_new_navigation) { - $('[data-testid="user-dropdown"] .gl-avatar').attr('src', this.avatarGlCrop.dataURL); - } else { - $('.header-user-avatar').attr('src', this.avatarGlCrop.dataURL); - $('.js-sidebar-user-avatar').attr('src', this.avatarGlCrop.dataURL); - } + const url = URL.createObjectURL(this.avatarGlCrop.getBlob()); + + document.dispatchEvent(new CustomEvent('userAvatar:update', { detail: { url } })); } setRepoRadio() { diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue index db769df873fd72a5bfe2326991aee7e0fda828e0..5dab74374dfaaac6924c892f5efee6fefe26d9f1 100644 --- a/app/assets/javascripts/super_sidebar/components/user_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue @@ -59,9 +59,13 @@ export default { data() { return { setStatusModalReady: false, + updatedAvatarUrl: null, }; }, computed: { + avatarUrl() { + return this.updatedAvatarUrl || this.data.avatar_url; + }, toggleText() { return sprintf(__('%{user} user’s menu'), { user: this.data.name }); }, @@ -190,7 +194,16 @@ export default { }; }, }, + mounted() { + document.addEventListener('userAvatar:update', this.updateAvatar); + }, + unmounted() { + document.removeEventListener('userAvatar:update', this.updateAvatar); + }, methods: { + updateAvatar(event) { + this.updatedAvatarUrl = event.detail?.url; + }, onShow() { this.initBuyCIMinsCallout(); }, @@ -240,7 +253,7 @@ export default { <gl-avatar :size="24" :entity-name="data.name" - :src="data.avatar_url" + :src="avatarUrl" aria-hidden="true" data-testid="user-avatar-content" /> diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb index 83eb7cb989e36f032b94b70077532b5a316f5ea9..5d121d9eeba9c9b06e4a4be21cb2d4bdc2c9aa2a 100644 --- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb +++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb @@ -18,8 +18,10 @@ wait_for_all_requests - data_uri = find('.avatar-image .gl-avatar')['src'] - within_testid('user-dropdown') { expect(find('.gl-avatar')['src']).to eq data_uri } + within_testid('user-dropdown') do + # We are setting a blob URL + expect(find('.gl-avatar')['src']).to start_with 'blob:' + end visit profile_path diff --git a/spec/frontend/profile/edit/components/profile_edit_app_spec.js b/spec/frontend/profile/edit/components/profile_edit_app_spec.js index 31a368aefa916b3ea99f03cfe054f61dc1929c43..39bf597352bd6b518c5adbbde86e48f16e46cce8 100644 --- a/spec/frontend/profile/edit/components/profile_edit_app_spec.js +++ b/spec/frontend/profile/edit/components/profile_edit_app_spec.js @@ -3,7 +3,6 @@ import MockAdapter from 'axios-mock-adapter'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { readFileAsDataURL } from '~/lib/utils/file_utility'; import axios from '~/lib/utils/axios_utils'; import ProfileEditApp from '~/profile/edit/components/profile_edit_app.vue'; import UserAvatar from '~/profile/edit/components/user_avatar.vue'; @@ -103,6 +102,8 @@ describe('Profile Edit App', () => { }); it('syncs header avatars', async () => { + jest.spyOn(document, 'dispatchEvent'); + jest.spyOn(URL, 'createObjectURL'); mockAxios.onPut(stubbedProfilePath).reply(200, { message: successMessage, }); @@ -112,7 +113,8 @@ describe('Profile Edit App', () => { await waitForPromises(); - expect(readFileAsDataURL).toHaveBeenCalledWith(mockAvatarFile); + expect(URL.createObjectURL).toHaveBeenCalledWith(mockAvatarFile); + expect(document.dispatchEvent).toHaveBeenCalledWith(new CustomEvent('userAvatar:update')); }); it('contains changes from the status form', async () => { diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js index ba675c8b3f514570629c02941fee03c52bdf3785..4af3247693be942108c497e9c60f5dcb1644703b 100644 --- a/spec/frontend/super_sidebar/components/user_menu_spec.js +++ b/spec/frontend/super_sidebar/components/user_menu_spec.js @@ -78,6 +78,20 @@ describe('UserMenu component', () => { }); }); + it('updates avatar url on custom avatar update event', async () => { + const url = `${userMenuMockData.avatar_url}-new-avatar`; + + document.dispatchEvent(new CustomEvent('userAvatar:update', { detail: { url } })); + await nextTick(); + + const avatar = toggle.findComponent(GlAvatar); + expect(avatar.exists()).toBe(true); + expect(avatar.props()).toMatchObject({ + entityName: userMenuMockData.name, + src: url, + }); + }); + it('renders screen reader text', () => { expect(toggle.find('.gl-sr-only').text()).toBe(`${userMenuMockData.name} user’s menu`); });