feat: Adds support for logo in portal settings page [CW-2585] (#8354)

This commit is contained in:
Nithin David Thomas
2023-11-18 09:28:27 +05:30
committed by GitHub
parent 7380f0e7ce
commit 0af27a2387
9 changed files with 158 additions and 22 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,21 +14,24 @@
>
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
<div class="mb-4">
<label>
{{ $t('HELP_CENTER.PORTAL.ADD.LOGO.LABEL') }}
</label>
<div class="flex items-center flex-row">
<thumbnail :username="name" size="56px" variant="square" />
<woot-button
v-if="false"
class="ml-3"
variant="smooth"
color-scheme="secondary"
icon="upload"
size="small"
>
{{ $t('HELP_CENTER.PORTAL.ADD.LOGO.UPLOAD_BUTTON') }}
</woot-button>
<woot-avatar-uploader
ref="imageUpload"
:label="$t('HELP_CENTER.PORTAL.ADD.LOGO.LABEL')"
:src="logoUrl"
@change="onFileChange"
/>
<div v-if="showDeleteButton" class="avatar-delete-btn">
<woot-button
type="button"
color-scheme="alert"
variant="hollow"
size="small"
@click="deleteAvatar"
>
{{ $t('PROFILE_SETTINGS.DELETE_AVATAR') }}
</woot-button>
</div>
</div>
<p
class="mt-1 mb-0 text-xs text-slate-600 dark:text-slate-400 not-italic"
@@ -87,17 +90,20 @@
<script>
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')
);
}
},
},
};
</script>

View File

@@ -7,6 +7,7 @@
$t('HELP_CENTER.PORTAL.EDIT.EDIT_BASIC_INFO.BUTTON_TEXT')
"
@submit="updatePortalSettings"
@delete-logo="deleteLogo"
/>
</template>
@@ -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);
}
},
},
};
</script>

View File

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

View File

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

View File

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