feat: Adds support for logo in portal settings page [CW-2585] (#8354)
This commit is contained in:
committed by
GitHub
parent
7380f0e7ce
commit
0af27a2387
@@ -17,6 +17,10 @@ class PortalsAPI extends ApiClient {
|
|||||||
deletePortal(portalSlug) {
|
deletePortal(portalSlug) {
|
||||||
return axios.delete(`${this.url}/${portalSlug}`);
|
return axios.delete(`${this.url}/${portalSlug}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteLogo(portalSlug) {
|
||||||
|
return axios.delete(`${this.url}/${portalSlug}/logo`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PortalsAPI;
|
export default PortalsAPI;
|
||||||
|
|||||||
@@ -96,3 +96,15 @@ export const getArticleSearchURL = ({
|
|||||||
|
|
||||||
return `${host}/${portalSlug}/articles?${queryParams.toString()}`;
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
isValidURL,
|
isValidURL,
|
||||||
conversationListPageURL,
|
conversationListPageURL,
|
||||||
getArticleSearchURL,
|
getArticleSearchURL,
|
||||||
|
hasValidAvatarUrl,
|
||||||
} from '../URLHelper';
|
} from '../URLHelper';
|
||||||
|
|
||||||
describe('#URL Helpers', () => {
|
describe('#URL Helpers', () => {
|
||||||
@@ -164,4 +165,29 @@ describe('#URL Helpers', () => {
|
|||||||
expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -223,7 +223,10 @@
|
|||||||
"LOGO": {
|
"LOGO": {
|
||||||
"LABEL": "Logo",
|
"LABEL": "Logo",
|
||||||
"UPLOAD_BUTTON": "Upload 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": {
|
"NAME": {
|
||||||
"LABEL": "Name",
|
"LABEL": "Name",
|
||||||
|
|||||||
@@ -14,21 +14,24 @@
|
|||||||
>
|
>
|
||||||
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
|
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label>
|
|
||||||
{{ $t('HELP_CENTER.PORTAL.ADD.LOGO.LABEL') }}
|
|
||||||
</label>
|
|
||||||
<div class="flex items-center flex-row">
|
<div class="flex items-center flex-row">
|
||||||
<thumbnail :username="name" size="56px" variant="square" />
|
<woot-avatar-uploader
|
||||||
<woot-button
|
ref="imageUpload"
|
||||||
v-if="false"
|
:label="$t('HELP_CENTER.PORTAL.ADD.LOGO.LABEL')"
|
||||||
class="ml-3"
|
:src="logoUrl"
|
||||||
variant="smooth"
|
@change="onFileChange"
|
||||||
color-scheme="secondary"
|
/>
|
||||||
icon="upload"
|
<div v-if="showDeleteButton" class="avatar-delete-btn">
|
||||||
size="small"
|
<woot-button
|
||||||
>
|
type="button"
|
||||||
{{ $t('HELP_CENTER.PORTAL.ADD.LOGO.UPLOAD_BUTTON') }}
|
color-scheme="alert"
|
||||||
</woot-button>
|
variant="hollow"
|
||||||
|
size="small"
|
||||||
|
@click="deleteAvatar"
|
||||||
|
>
|
||||||
|
{{ $t('PROFILE_SETTINGS.DELETE_AVATAR') }}
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
class="mt-1 mb-0 text-xs text-slate-600 dark:text-slate-400 not-italic"
|
class="mt-1 mb-0 text-xs text-slate-600 dark:text-slate-400 not-italic"
|
||||||
@@ -87,17 +90,20 @@
|
|||||||
<script>
|
<script>
|
||||||
import { required, minLength } from 'vuelidate/lib/validators';
|
import { required, minLength } from 'vuelidate/lib/validators';
|
||||||
import { isDomain } from 'shared/helpers/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 { convertToCategorySlug } from 'dashboard/helper/commons.js';
|
||||||
import { buildPortalURL } from 'dashboard/helper/portalHelper';
|
import { buildPortalURL } from 'dashboard/helper/portalHelper';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
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 { EXAMPLE_URL } = wootConstants;
|
||||||
|
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
mixins: [alertMixin],
|
||||||
thumbnail,
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
portal: {
|
portal: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -118,6 +124,10 @@ export default {
|
|||||||
slug: '',
|
slug: '',
|
||||||
domain: '',
|
domain: '',
|
||||||
alertMessage: '',
|
alertMessage: '',
|
||||||
|
|
||||||
|
// Logouploader keys
|
||||||
|
avatarBlobId: '',
|
||||||
|
logoUrl: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
validations: {
|
validations: {
|
||||||
@@ -159,6 +169,9 @@ export default {
|
|||||||
exampleURL: EXAMPLE_URL,
|
exampleURL: EXAMPLE_URL,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
showDeleteButton() {
|
||||||
|
return hasValidAvatarUrl(this.logoUrl);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const portal = this.portal || {};
|
const portal = this.portal || {};
|
||||||
@@ -166,6 +179,13 @@ export default {
|
|||||||
this.slug = portal.slug || '';
|
this.slug = portal.slug || '';
|
||||||
this.domain = portal.custom_domain || '';
|
this.domain = portal.custom_domain || '';
|
||||||
this.alertMessage = '';
|
this.alertMessage = '';
|
||||||
|
if (portal.logo) {
|
||||||
|
const {
|
||||||
|
logo: { file_url: logoURL, blob_id: blobId },
|
||||||
|
} = portal;
|
||||||
|
this.logoUrl = logoURL;
|
||||||
|
this.avatarBlobId = blobId;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onNameChange() {
|
onNameChange() {
|
||||||
@@ -181,9 +201,44 @@ export default {
|
|||||||
name: this.name,
|
name: this.name,
|
||||||
slug: this.slug,
|
slug: this.slug,
|
||||||
custom_domain: this.domain,
|
custom_domain: this.domain,
|
||||||
|
blob_id: this.avatarBlobId || null,
|
||||||
};
|
};
|
||||||
this.$emit('submit', portal);
|
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>
|
</script>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
$t('HELP_CENTER.PORTAL.EDIT.EDIT_BASIC_INFO.BUTTON_TEXT')
|
$t('HELP_CENTER.PORTAL.EDIT.EDIT_BASIC_INFO.BUTTON_TEXT')
|
||||||
"
|
"
|
||||||
@submit="updatePortalSettings"
|
@submit="updatePortalSettings"
|
||||||
|
@delete-logo="deleteLogo"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -70,6 +71,19 @@ export default {
|
|||||||
this.showAlert(this.alertMessage);
|
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>
|
</script>
|
||||||
|
|||||||
@@ -40,9 +40,12 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async createPortal(portal) {
|
async createPortal(portal) {
|
||||||
try {
|
try {
|
||||||
|
const { blob_id: blobId } = portal;
|
||||||
await this.$store.dispatch('portals/create', {
|
await this.$store.dispatch('portals/create', {
|
||||||
portal,
|
portal,
|
||||||
|
blob_id: blobId,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.alertMessage = this.$t(
|
this.alertMessage = this.$t(
|
||||||
'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_BASIC'
|
'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_BASIC'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -128,6 +128,7 @@
|
|||||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { clearCookiesOnLogout } from '../../../../store/utils/api';
|
import { clearCookiesOnLogout } from '../../../../store/utils/api';
|
||||||
|
import { hasValidAvatarUrl } from 'dashboard/helper/URLHelper';
|
||||||
import NotificationSettings from './NotificationSettings.vue';
|
import NotificationSettings from './NotificationSettings.vue';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import ChangePassword from './ChangePassword.vue';
|
import ChangePassword from './ChangePassword.vue';
|
||||||
@@ -198,6 +199,9 @@ export default {
|
|||||||
currentUserId: 'getCurrentUserID',
|
currentUserId: 'getCurrentUserID',
|
||||||
globalConfig: 'globalConfig/get',
|
globalConfig: 'globalConfig/get',
|
||||||
}),
|
}),
|
||||||
|
showDeleteButton() {
|
||||||
|
return hasValidAvatarUrl(this.avatarUrl);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentUserId(newCurrentUserId, prevCurrentUserId) {
|
currentUserId(newCurrentUserId, prevCurrentUserId) {
|
||||||
@@ -265,9 +269,6 @@ export default {
|
|||||||
this.showAlert(this.$t('PROFILE_SETTINGS.AVATAR_DELETE_FAILED'));
|
this.showAlert(this.$t('PROFILE_SETTINGS.AVATAR_DELETE_FAILED'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showDeleteButton() {
|
|
||||||
return this.avatarUrl && !this.avatarUrl.includes('www.gravatar.com');
|
|
||||||
},
|
|
||||||
toggleEditorMessageKey(key) {
|
toggleEditorMessageKey(key) {
|
||||||
this.updateUISettings({ editor_message_key: key });
|
this.updateUISettings({ editor_message_key: key });
|
||||||
this.showAlert(
|
this.showAlert(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PortalAPI from 'dashboard/api/helpCenter/portals';
|
|||||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||||
import { types } from './mutations';
|
import { types } from './mutations';
|
||||||
const portalAPIs = new PortalAPI();
|
const portalAPIs = new PortalAPI();
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
index: async ({ commit }) => {
|
index: async ({ commit }) => {
|
||||||
try {
|
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) => {
|
updatePortal: async ({ commit }, portal) => {
|
||||||
commit(types.UPDATE_PORTAL_ENTRY, portal);
|
commit(types.UPDATE_PORTAL_ENTRY, portal);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user