diff --git a/app/javascript/dashboard/api/helpCenter/portals.js b/app/javascript/dashboard/api/helpCenter/portals.js index 28220b0b5..7c6210dbd 100644 --- a/app/javascript/dashboard/api/helpCenter/portals.js +++ b/app/javascript/dashboard/api/helpCenter/portals.js @@ -17,6 +17,10 @@ class PortalsAPI extends ApiClient { deletePortal(portalSlug) { return axios.delete(`${this.url}/${portalSlug}`); } + + deleteLogo(portalSlug) { + return axios.delete(`${this.url}/${portalSlug}/logo`); + } } export default PortalsAPI; diff --git a/app/javascript/dashboard/helper/URLHelper.js b/app/javascript/dashboard/helper/URLHelper.js index 7ea8531d3..5464d1107 100644 --- a/app/javascript/dashboard/helper/URLHelper.js +++ b/app/javascript/dashboard/helper/URLHelper.js @@ -96,3 +96,15 @@ export const getArticleSearchURL = ({ return `${host}/${portalSlug}/articles?${queryParams.toString()}`; }; + +export const hasValidAvatarUrl = avatarUrl => { + try { + const { host: avatarUrlHost } = new URL(avatarUrl); + const isFromGravatar = ['www.gravatar.com', 'gravatar'].includes( + avatarUrlHost + ); + return avatarUrl && !isFromGravatar; + } catch (error) { + return false; + } +}; diff --git a/app/javascript/dashboard/helper/specs/URLHelper.spec.js b/app/javascript/dashboard/helper/specs/URLHelper.spec.js index 1b5814556..d137c367c 100644 --- a/app/javascript/dashboard/helper/specs/URLHelper.spec.js +++ b/app/javascript/dashboard/helper/specs/URLHelper.spec.js @@ -4,6 +4,7 @@ import { isValidURL, conversationListPageURL, getArticleSearchURL, + hasValidAvatarUrl, } from '../URLHelper'; describe('#URL Helpers', () => { @@ -164,4 +165,29 @@ describe('#URL Helpers', () => { expect(url).toBe('myurl.com/news/articles?page=1&locale=en'); }); }); + + describe('hasValidAvatarUrl', () => { + test('should return true for valid non-Gravatar URL', () => { + expect(hasValidAvatarUrl('https://chatwoot.com/avatar.jpg')).toBe(true); + }); + + test('should return false for a Gravatar URL (www.gravatar.com)', () => { + expect(hasValidAvatarUrl('https://www.gravatar.com/avatar.jpg')).toBe( + false + ); + }); + + test('should return false for a Gravatar URL (gravatar)', () => { + expect(hasValidAvatarUrl('https://gravatar/avatar.jpg')).toBe(false); + }); + + test('should handle invalid URL', () => { + expect(hasValidAvatarUrl('invalid-url')).toBe(false); // or expect an error, depending on function design + }); + + test('should return false for empty or undefined URL', () => { + expect(hasValidAvatarUrl('')).toBe(false); + expect(hasValidAvatarUrl()).toBe(false); + }); + }); }); diff --git a/app/javascript/dashboard/i18n/locale/en/helpCenter.json b/app/javascript/dashboard/i18n/locale/en/helpCenter.json index 43b419328..6d4a3eb8c 100644 --- a/app/javascript/dashboard/i18n/locale/en/helpCenter.json +++ b/app/javascript/dashboard/i18n/locale/en/helpCenter.json @@ -223,7 +223,10 @@ "LOGO": { "LABEL": "Logo", "UPLOAD_BUTTON": "Upload logo", - "HELP_TEXT": "This logo will be displayed on the portal header." + "HELP_TEXT": "This logo will be displayed on the portal header.", + "IMAGE_UPLOAD_SUCCESS": "Logo uploaded successfully", + "IMAGE_UPLOAD_ERROR": "Logo deleted successfully", + "IMAGE_DELETE_ERROR": "Error while deleting logo" }, "NAME": { "LABEL": "Name", diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsBasicForm.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsBasicForm.vue index c3d74eaab..7653d16a9 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsBasicForm.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsBasicForm.vue @@ -14,21 +14,24 @@ >
import { required, minLength } from 'vuelidate/lib/validators'; import { isDomain } from 'shared/helpers/Validators'; -import thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; + +import alertMixin from 'shared/mixins/alertMixin'; import { convertToCategorySlug } from 'dashboard/helper/commons.js'; import { buildPortalURL } from 'dashboard/helper/portalHelper'; import wootConstants from 'dashboard/constants/globals'; +import { hasValidAvatarUrl } from 'dashboard/helper/URLHelper'; +import { checkFileSizeLimit } from 'shared/helpers/FileHelper'; +import { uploadFile } from 'dashboard/helper/uploadHelper'; const { EXAMPLE_URL } = wootConstants; +const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB export default { - components: { - thumbnail, - }, + mixins: [alertMixin], props: { portal: { type: Object, @@ -118,6 +124,10 @@ export default { slug: '', domain: '', alertMessage: '', + + // Logouploader keys + avatarBlobId: '', + logoUrl: '', }; }, validations: { @@ -159,6 +169,9 @@ export default { exampleURL: EXAMPLE_URL, }); }, + showDeleteButton() { + return hasValidAvatarUrl(this.logoUrl); + }, }, mounted() { const portal = this.portal || {}; @@ -166,6 +179,13 @@ export default { this.slug = portal.slug || ''; this.domain = portal.custom_domain || ''; this.alertMessage = ''; + if (portal.logo) { + const { + logo: { file_url: logoURL, blob_id: blobId }, + } = portal; + this.logoUrl = logoURL; + this.avatarBlobId = blobId; + } }, methods: { onNameChange() { @@ -181,9 +201,44 @@ export default { name: this.name, slug: this.slug, custom_domain: this.domain, + blob_id: this.avatarBlobId || null, }; this.$emit('submit', portal); }, + async deleteAvatar() { + this.logoUrl = ''; + this.avatarBlobId = ''; + this.$emit('delete-logo'); + }, + onFileChange({ file }) { + if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) { + this.uploadLogoToStorage(file); + } else { + this.showAlert( + this.$t( + 'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR', + { + size: MAXIMUM_FILE_UPLOAD_SIZE, + } + ) + ); + } + + this.$refs.imageUpload.value = ''; + }, + async uploadLogoToStorage(file) { + try { + const { fileUrl, blobId } = await uploadFile(file); + if (fileUrl) { + this.logoUrl = fileUrl; + this.avatarBlobId = blobId; + } + } catch (error) { + this.showAlert( + this.$t('HELP_CENTER.PORTAL.ADD.LOGO.IMAGE_DELETE_ERROR') + ); + } + }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalBasic.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalBasic.vue index c6d9f8b15..8e3579f02 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalBasic.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalBasic.vue @@ -7,6 +7,7 @@ $t('HELP_CENTER.PORTAL.EDIT.EDIT_BASIC_INFO.BUTTON_TEXT') " @submit="updatePortalSettings" + @delete-logo="deleteLogo" /> @@ -70,6 +71,19 @@ export default { this.showAlert(this.alertMessage); } }, + async deleteLogo() { + try { + const portalSlug = this.lastPortalSlug; + await this.$store.dispatch('portals/deleteLogo', { + portalSlug, + }); + } catch (error) { + this.alertMessage = + error?.message || + this.$t('HELP_CENTER.PORTAL.ADD.LOGO.IMAGE_DELETE_ERROR'); + this.showAlert(this.alertMessage); + } + }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalDetails.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalDetails.vue index 98aaf544f..2857867bb 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalDetails.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalDetails.vue @@ -40,9 +40,12 @@ export default { methods: { async createPortal(portal) { try { + const { blob_id: blobId } = portal; await this.$store.dispatch('portals/create', { portal, + blob_id: blobId, }); + this.alertMessage = this.$t( 'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_BASIC' ); diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue index f44ceedfb..4a1c81bda 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue @@ -128,6 +128,7 @@ import { required, minLength, email } from 'vuelidate/lib/validators'; import { mapGetters } from 'vuex'; import { clearCookiesOnLogout } from '../../../../store/utils/api'; +import { hasValidAvatarUrl } from 'dashboard/helper/URLHelper'; import NotificationSettings from './NotificationSettings.vue'; import alertMixin from 'shared/mixins/alertMixin'; import ChangePassword from './ChangePassword.vue'; @@ -198,6 +199,9 @@ export default { currentUserId: 'getCurrentUserID', globalConfig: 'globalConfig/get', }), + showDeleteButton() { + return hasValidAvatarUrl(this.avatarUrl); + }, }, watch: { currentUserId(newCurrentUserId, prevCurrentUserId) { @@ -265,9 +269,6 @@ export default { this.showAlert(this.$t('PROFILE_SETTINGS.AVATAR_DELETE_FAILED')); } }, - showDeleteButton() { - return this.avatarUrl && !this.avatarUrl.includes('www.gravatar.com'); - }, toggleEditorMessageKey(key) { this.updateUISettings({ editor_message_key: key }); this.showAlert( diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/actions.js b/app/javascript/dashboard/store/modules/helpCenterPortals/actions.js index bfae85d27..60ccf9d25 100644 --- a/app/javascript/dashboard/store/modules/helpCenterPortals/actions.js +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/actions.js @@ -2,6 +2,7 @@ import PortalAPI from 'dashboard/api/helpCenter/portals'; import { throwErrorMessage } from 'dashboard/store/utils/api'; import { types } from './mutations'; const portalAPIs = new PortalAPI(); + export const actions = { index: async ({ commit }) => { try { @@ -89,6 +90,23 @@ export const actions = { } }, + deleteLogo: async ({ commit }, { portalSlug }) => { + commit(types.SET_HELP_PORTAL_UI_FLAG, { + uiFlags: { isUpdating: true }, + portalSlug, + }); + try { + await portalAPIs.deleteLogo(portalSlug); + } catch (error) { + throwErrorMessage(error); + } finally { + commit(types.SET_HELP_PORTAL_UI_FLAG, { + uiFlags: { isUpdating: false }, + portalSlug, + }); + } + }, + updatePortal: async ({ commit }, portal) => { commit(types.UPDATE_PORTAL_ENTRY, portal); },