fix: Installation name not showing (#12096)

This commit is contained in:
Sivin Varghese
2025-08-06 13:11:22 +05:30
committed by GitHub
parent 855dd590ab
commit d5286c9535
23 changed files with 215 additions and 127 deletions

View File

@@ -1,9 +1,5 @@
<script>
/* eslint no-console: 0 */
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
export default {
mixins: [globalConfigMixin],
props: {
items: {
type: Array,

View File

@@ -3,7 +3,7 @@ import WidgetHead from './WidgetHead.vue';
import WidgetBody from './WidgetBody.vue';
import WidgetFooter from './WidgetFooter.vue';
import InputRadioGroup from 'dashboard/routes/dashboard/settings/inbox/components/InputRadioGroup.vue';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import { useBranding } from 'shared/composables/useBranding';
import { mapGetters } from 'vuex';
export default {
@@ -14,7 +14,6 @@ export default {
WidgetFooter,
InputRadioGroup,
},
mixins: [globalConfigMixin],
props: {
welcomeHeading: {
type: String,
@@ -57,6 +56,12 @@ export default {
default: '',
},
},
setup() {
const { replaceInstallationName } = useBranding();
return {
replaceInstallationName,
};
},
data() {
return {
widgetScreens: [
@@ -159,9 +164,8 @@ export default {
/>
<span>
{{
useInstallationName(
$t('INBOX_MGMT.WIDGET_BUILDER.BRANDING_TEXT'),
globalConfig.installationName
replaceInstallationName(
$t('INBOX_MGMT.WIDGET_BUILDER.BRANDING_TEXT')
)
}}
</span>

View File

@@ -3,14 +3,19 @@ import ChannelItem from 'dashboard/components/widgets/ChannelItem.vue';
import router from '../../../index';
import PageHeader from '../SettingsSubPageHeader.vue';
import { mapGetters } from 'vuex';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import { useBranding } from 'shared/composables/useBranding';
export default {
components: {
ChannelItem,
PageHeader,
},
mixins: [globalConfigMixin],
setup() {
const { replaceInstallationName } = useBranding();
return {
replaceInstallationName,
};
},
data() {
return {
enabledFeatures: {},
@@ -69,12 +74,7 @@ export default {
<PageHeader
class="max-w-4xl"
:header-title="$t('INBOX_MGMT.ADD.AUTH.TITLE')"
:header-content="
useInstallationName(
$t('INBOX_MGMT.ADD.AUTH.DESC'),
globalConfig.installationName
)
"
:header-content="replaceInstallationName($t('INBOX_MGMT.ADD.AUTH.DESC'))"
/>
<div
class="grid max-w-3xl grid-cols-2 mx-0 mt-6 sm:grid-cols-3 lg:grid-cols-4"

View File

@@ -1,9 +1,14 @@
<script>
import { mapGetters } from 'vuex';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import { useBranding } from 'shared/composables/useBranding';
export default {
mixins: [globalConfigMixin],
setup() {
const { replaceInstallationName } = useBranding();
return {
replaceInstallationName,
};
},
computed: {
...mapGetters({
globalConfig: 'globalConfig/get',
@@ -29,10 +34,7 @@ export default {
items() {
return this.createFlowSteps.map(item => ({
...item,
body: this.useInstallationName(
item.body,
this.globalConfig.installationName
),
body: this.replaceInstallationName(item.body),
}));
},
},

View File

@@ -6,11 +6,11 @@ import { useAlert } from 'dashboard/composables';
import { useAccount } from 'dashboard/composables/useAccount';
import { required } from '@vuelidate/validators';
import LoadingState from 'dashboard/components/widgets/LoadingState.vue';
import { mapGetters } from 'vuex';
import ChannelApi from '../../../../../api/channels';
import PageHeader from '../../SettingsSubPageHeader.vue';
import router from '../../../../index';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import { useBranding } from 'shared/composables/useBranding';
import NextButton from 'dashboard/components-next/button/Button.vue';
import { loadScript } from 'dashboard/helper/DOMHelpers';
@@ -22,11 +22,12 @@ export default {
PageHeader,
NextButton,
},
mixins: [globalConfigMixin],
setup() {
const { accountId } = useAccount();
const { replaceInstallationName } = useBranding();
return {
accountId,
replaceInstallationName,
v$: useVuelidate(),
};
},
@@ -66,9 +67,6 @@ export default {
getSelectablePages() {
return this.pageList.filter(item => !item.exists);
},
...mapGetters({
globalConfig: 'globalConfig/get',
}),
},
mounted() {
@@ -223,12 +221,7 @@ export default {
/>
</a>
<p class="py-6">
{{
useInstallationName(
$t('INBOX_MGMT.ADD.FB.HELP'),
globalConfig.installationName
)
}}
{{ replaceInstallationName($t('INBOX_MGMT.ADD.FB.HELP')) }}
</p>
</div>
<div v-else>
@@ -249,10 +242,7 @@ export default {
<PageHeader
:header-title="$t('INBOX_MGMT.ADD.DETAILS.TITLE')"
:header-content="
useInstallationName(
$t('INBOX_MGMT.ADD.DETAILS.DESC'),
globalConfig.installationName
)
replaceInstallationName($t('INBOX_MGMT.ADD.DETAILS.DESC'))
"
/>
</div>

View File

@@ -1,11 +1,9 @@
<script>
import { useVuelidate } from '@vuelidate/core';
import { useAccount } from 'dashboard/composables/useAccount';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import instagramClient from 'dashboard/api/channel/instagramClient';
export default {
mixins: [globalConfigMixin],
setup() {
const { accountId } = useAccount();
return {

View File

@@ -3,7 +3,6 @@ import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import DashboardAppModal from './DashboardAppModal.vue';
import DashboardAppsRow from './DashboardAppsRow.vue';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import BaseSettingsHeader from '../../components/BaseSettingsHeader.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
@@ -14,7 +13,6 @@ export default {
DashboardAppsRow,
NextButton,
},
mixins: [globalConfigMixin],
data() {
return {
loading: {},

View File

@@ -1,12 +1,14 @@
<script setup>
import { useStoreGetters, useStore } from 'dashboard/composables/store';
import { computed, onMounted } from 'vue';
import { useBranding } from 'shared/composables/useBranding';
import IntegrationItem from './IntegrationItem.vue';
import SettingsLayout from '../SettingsLayout.vue';
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
const store = useStore();
const getters = useStoreGetters();
const { replaceInstallationName } = useBranding();
const uiFlags = getters['integrations/getUIFlags'];
@@ -27,7 +29,9 @@ onMounted(() => {
<template #header>
<BaseSettingsHeader
:title="$t('INTEGRATION_SETTINGS.HEADER')"
:description="$t('INTEGRATION_SETTINGS.DESCRIPTION')"
:description="
replaceInstallationName($t('INTEGRATION_SETTINGS.DESCRIPTION'))
"
:link-text="$t('INTEGRATION_SETTINGS.LEARN_MORE')"
feature-name="integrations"
/>

View File

@@ -5,7 +5,7 @@ import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { frontendURL } from '../../../../helper/URLHelper';
import { useAlert } from 'dashboard/composables';
import { useInstallationName } from 'shared/mixins/globalConfigMixin';
import { useBranding } from 'shared/composables/useBranding';
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
import Button from 'dashboard/components-next/button/Button.vue';
@@ -26,11 +26,11 @@ const props = defineProps({
const { t } = useI18n();
const store = useStore();
const router = useRouter();
const { replaceInstallationName } = useBranding();
const dialogRef = ref(null);
const accountId = computed(() => store.getters.getCurrentAccountId);
const globalConfig = computed(() => store.getters['globalConfig/get']);
const openDeletePopup = () => {
if (dialogRef.value) {
@@ -82,12 +82,7 @@ const confirmDeletion = () => {
{{ integrationName }}
</h3>
<p class="text-n-slate-11 text-sm leading-6">
{{
useInstallationName(
integrationDescription,
globalConfig.installationName
)
}}
{{ replaceInstallationName(integrationDescription) }}
</p>
</div>
</div>

View File

@@ -3,7 +3,7 @@ import { computed } from 'vue';
import { useStoreGetters } from 'dashboard/composables/store';
import { useI18n } from 'vue-i18n';
import { frontendURL } from 'dashboard/helper/URLHelper';
import { useInstallationName } from 'shared/mixins/globalConfigMixin';
import { useBranding } from 'shared/composables/useBranding';
import Button from 'dashboard/components-next/button/Button.vue';
@@ -28,9 +28,9 @@ const props = defineProps({
const getters = useStoreGetters();
const accountId = getters.getCurrentAccountId;
const globalConfig = getters['globalConfig/get'];
const { t } = useI18n();
const { replaceInstallationName } = useBranding();
const integrationStatus = computed(() =>
props.enabled
@@ -80,7 +80,7 @@ const actionURL = computed(() =>
</router-link>
</div>
<p class="text-n-slate-11">
{{ useInstallationName(description, globalConfig.installationName) }}
{{ replaceInstallationName(description) }}
</p>
</div>
</div>

View File

@@ -1,5 +1,4 @@
<script>
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import Integration from './Integration.vue';
import IntegrationHelpText from './IntegrationHelpText.vue';
@@ -8,8 +7,6 @@ export default {
Integration,
IntegrationHelpText,
},
mixins: [globalConfigMixin],
props: {
integrationId: {
type: [String, Number],

View File

@@ -3,7 +3,7 @@ import { ref, computed } from 'vue';
import { useStore } from 'vuex';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import { useInstallationName } from 'shared/mixins/globalConfigMixin';
import { useBranding } from 'shared/composables/useBranding';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import Button from 'dashboard/components-next/button/Button.vue';
@@ -18,6 +18,7 @@ const store = useStore();
const { t } = useI18n();
const { formatMessage } = useMessageFormatter();
const { replaceInstallationName } = useBranding();
const selectedChannelId = ref('');
const availableChannels = ref([]);
@@ -29,16 +30,9 @@ const errorDescription = computed(() => {
? t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.DESCRIPTION')
: t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.EXPIRED');
});
const globalConfig = computed(() => store.getters['globalConfig/get']);
const formattedErrorMessage = computed(() => {
return formatMessage(
useInstallationName(
errorDescription.value,
globalConfig.value.installationName
),
false
);
return formatMessage(replaceInstallationName(errorDescription.value), false);
});
const fetchChannels = async () => {

View File

@@ -1,6 +1,7 @@
<script>
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useBranding } from 'shared/composables/useBranding';
import NextButton from 'dashboard/components-next/button/Button.vue';
import NewWebhook from './NewWebHook.vue';
import EditWebhook from './EditWebHook.vue';
@@ -17,6 +18,10 @@ export default {
EditWebhook,
WebhookRow,
},
setup() {
const { replaceInstallationName } = useBranding();
return { replaceInstallationName };
},
data() {
return {
loading: {},
@@ -99,7 +104,7 @@ export default {
<BaseSettingsHeader
v-if="integration.name"
:title="integration.name"
:description="integration.description"
:description="replaceInstallationName(integration.description)"
:link-text="$t('INTEGRATION_SETTINGS.WEBHOOK.LEARN_MORE')"
feature-name="webhook"
:back-button-label="$t('INTEGRATION_SETTINGS.HEADER')"

View File

@@ -1,21 +1,25 @@
<script>
import { useAlert } from 'dashboard/composables';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import { useBranding } from 'shared/composables/useBranding';
import { mapGetters } from 'vuex';
import WebhookForm from './WebhookForm.vue';
export default {
components: { WebhookForm },
mixins: [globalConfigMixin],
props: {
onClose: {
type: Function,
required: true,
},
},
setup() {
const { replaceInstallationName } = useBranding();
return {
replaceInstallationName,
};
},
computed: {
...mapGetters({
globalConfig: 'globalConfig/get',
uiFlags: 'webhooks/getUIFlags',
}),
},
@@ -43,10 +47,7 @@ export default {
<woot-modal-header
:header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.TITLE')"
:header-content="
useInstallationName(
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.DESC'),
globalConfig.installationName
)
replaceInstallationName($t('INTEGRATION_SETTINGS.WEBHOOK.FORM.DESC'))
"
/>
<WebhookForm

View File

@@ -3,10 +3,10 @@ import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useFontSize } from 'dashboard/composables/useFontSize';
import { useBranding } from 'shared/composables/useBranding';
import { clearCookiesOnLogout } from 'dashboard/store/utils/api.js';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import UserProfilePicture from './UserProfilePicture.vue';
import UserBasicDetails from './UserBasicDetails.vue';
import MessageSignature from './MessageSignature.vue';
@@ -37,16 +37,17 @@ export default {
AudioNotifications,
AccessToken,
},
mixins: [globalConfigMixin],
setup() {
const { isEditorHotKeyEnabled, updateUISettings } = useUISettings();
const { currentFontSize, updateFontSize } = useFontSize();
const { replaceInstallationName } = useBranding();
return {
currentFontSize,
updateFontSize,
isEditorHotKeyEnabled,
updateUISettings,
replaceInstallationName,
};
},
data() {
@@ -215,7 +216,11 @@ export default {
</div>
<FormSection
:title="$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.TITLE')"
:description="$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.NOTE')"
:description="
replaceInstallationName(
$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.NOTE')
)
"
>
<FontSize
:value="currentFontSize"
@@ -288,10 +293,7 @@ export default {
<FormSection
:title="$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.TITLE')"
:description="
useInstallationName(
$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.NOTE'),
globalConfig.installationName
)
replaceInstallationName($t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.NOTE'))
"
>
<AccessToken

View File

@@ -1,5 +1,5 @@
<script>
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import { useBranding } from 'shared/composables/useBranding';
const {
LOGO_THUMBNAIL: logoThumbnail,
@@ -8,13 +8,18 @@ const {
} = window.globalConfig || {};
export default {
mixins: [globalConfigMixin],
props: {
disableBranding: {
type: Boolean,
default: false,
},
},
setup() {
const { replaceInstallationName } = useBranding();
return {
replaceInstallationName,
};
},
data() {
return {
globalConfig: {
@@ -61,7 +66,7 @@ export default {
:src="globalConfig.logoThumbnail"
/>
<span>
{{ useInstallationName($t('POWERED_BY'), globalConfig.brandName) }}
{{ replaceInstallationName($t('POWERED_BY')) }}
</span>
</a>
</div>

View File

@@ -0,0 +1,96 @@
import { useBranding } from '../useBranding';
import { useMapGetter } from 'dashboard/composables/store.js';
// Mock the store composable
vi.mock('dashboard/composables/store.js', () => ({
useMapGetter: vi.fn(),
}));
describe('useBranding', () => {
let mockGlobalConfig;
beforeEach(() => {
mockGlobalConfig = {
value: {
installationName: 'MyCompany',
},
};
useMapGetter.mockReturnValue(mockGlobalConfig);
});
afterEach(() => {
vi.clearAllMocks();
});
describe('replaceInstallationName', () => {
it('should replace "Chatwoot" with installation name when both text and installation name are provided', () => {
const { replaceInstallationName } = useBranding();
const result = replaceInstallationName('Welcome to Chatwoot');
expect(result).toBe('Welcome to MyCompany');
});
it('should replace multiple occurrences of "Chatwoot"', () => {
const { replaceInstallationName } = useBranding();
const result = replaceInstallationName(
'Chatwoot is great! Use Chatwoot today.'
);
expect(result).toBe('MyCompany is great! Use MyCompany today.');
});
it('should return original text when installation name is not provided', () => {
mockGlobalConfig.value = {};
const { replaceInstallationName } = useBranding();
const result = replaceInstallationName('Welcome to Chatwoot');
expect(result).toBe('Welcome to Chatwoot');
});
it('should return original text when globalConfig is not available', () => {
mockGlobalConfig.value = undefined;
const { replaceInstallationName } = useBranding();
const result = replaceInstallationName('Welcome to Chatwoot');
expect(result).toBe('Welcome to Chatwoot');
});
it('should return original text when text is empty or null', () => {
const { replaceInstallationName } = useBranding();
expect(replaceInstallationName('')).toBe('');
expect(replaceInstallationName(null)).toBe(null);
expect(replaceInstallationName(undefined)).toBe(undefined);
});
it('should handle text without "Chatwoot" gracefully', () => {
const { replaceInstallationName } = useBranding();
const result = replaceInstallationName('Welcome to our platform');
expect(result).toBe('Welcome to our platform');
});
it('should be case-sensitive for "Chatwoot"', () => {
const { replaceInstallationName } = useBranding();
const result = replaceInstallationName(
'Welcome to chatwoot and CHATWOOT'
);
expect(result).toBe('Welcome to chatwoot and CHATWOOT');
});
it('should handle special characters in installation name', () => {
mockGlobalConfig.value = {
installationName: 'My-Company & Co.',
};
const { replaceInstallationName } = useBranding();
const result = replaceInstallationName('Welcome to Chatwoot');
expect(result).toBe('Welcome to My-Company & Co.');
});
});
});

View File

@@ -0,0 +1,26 @@
/**
* Composable for branding-related utilities
* Provides methods to customize text with installation-specific branding
*/
import { useMapGetter } from 'dashboard/composables/store.js';
export function useBranding() {
const globalConfig = useMapGetter('globalConfig/get');
/**
* Replaces "Chatwoot" in text with the installation name from global config
* @param {string} text - The text to process
* @returns {string} - Text with "Chatwoot" replaced by installation name
*/
const replaceInstallationName = text => {
if (!text) return text;
const installationName = globalConfig.value?.installationName;
if (!installationName) return text;
return text.replace(/Chatwoot/g, installationName);
};
return {
replaceInstallationName,
};
}

View File

@@ -1,12 +0,0 @@
export const useInstallationName = (str, installationName) => {
if (str && installationName) {
return str.replace(/Chatwoot/g, installationName);
}
return str;
};
export default {
methods: {
useInstallationName,
},
};

View File

@@ -1,18 +1,17 @@
<script>
import { useVuelidate } from '@vuelidate/core';
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { required, minLength, email } from '@vuelidate/validators';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import { useBranding } from 'shared/composables/useBranding';
import FormInput from '../../../../components/Form/Input.vue';
import { resetPassword } from '../../../../api/auth';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: { FormInput, NextButton },
mixins: [globalConfigMixin],
setup() {
return { v$: useVuelidate() };
const { replaceInstallationName } = useBranding();
return { v$: useVuelidate(), replaceInstallationName };
},
data() {
return {
@@ -24,9 +23,6 @@ export default {
error: '',
};
},
computed: {
...mapGetters({ globalConfig: 'globalConfig/get' }),
},
validations() {
return {
credentials: {
@@ -82,12 +78,7 @@ export default {
<p
class="mb-4 text-sm font-normal leading-6 tracking-normal text-n-slate-11"
>
{{
useInstallationName(
$t('RESET_PASSWORD.DESCRIPTION'),
globalConfig.installationName
)
}}
{{ replaceInstallationName($t('RESET_PASSWORD.DESCRIPTION')) }}
</p>
<div class="space-y-5">
<FormInput

View File

@@ -1,6 +1,6 @@
<script>
import { mapGetters } from 'vuex';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import { useBranding } from 'shared/composables/useBranding';
import SignupForm from './components/Signup/Form.vue';
import Testimonials from './components/Testimonials/Index.vue';
import Spinner from 'shared/components/Spinner.vue';
@@ -11,7 +11,10 @@ export default {
Spinner,
Testimonials,
},
mixins: [globalConfigMixin],
setup() {
const { replaceInstallationName } = useBranding();
return { replaceInstallationName };
},
data() {
return { isLoading: false };
},
@@ -61,12 +64,7 @@ export default {
<div class="px-1 text-sm text-n-slate-12">
<span>{{ $t('REGISTER.HAVE_AN_ACCOUNT') }}</span>
<router-link class="text-link text-n-brand" to="/app/login">
{{
useInstallationName(
$t('LOGIN.TITLE'),
globalConfig.installationName
)
}}
{{ replaceInstallationName($t('LOGIN.TITLE')) }}
</router-link>
</div>
</div>

View File

@@ -3,7 +3,6 @@ import { useVuelidate } from '@vuelidate/core';
import { required, minLength, email } from '@vuelidate/validators';
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import { DEFAULT_REDIRECT_URL } from 'dashboard/constants/globals';
import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
import FormInput from '../../../../../components/Form/Input.vue';
@@ -20,7 +19,6 @@ export default {
NextButton,
VueHcaptcha,
},
mixins: [globalConfigMixin],
setup() {
return { v$: useVuelidate() };
},

View File

@@ -8,8 +8,7 @@ import { required, email } from '@vuelidate/validators';
import { useVuelidate } from '@vuelidate/core';
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
import SessionStorage from 'shared/helpers/sessionStorage';
// mixins
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import { useBranding } from 'shared/composables/useBranding';
// components
import FormInput from '../../components/Form/Input.vue';
@@ -31,7 +30,6 @@ export default {
Spinner,
NextButton,
},
mixins: [globalConfigMixin],
props: {
ssoAuthToken: { type: String, default: '' },
ssoAccountId: { type: String, default: '' },
@@ -40,7 +38,11 @@ export default {
authError: { type: String, default: '' },
},
setup() {
return { v$: useVuelidate() };
const { replaceInstallationName } = useBranding();
return {
replaceInstallationName,
v$: useVuelidate(),
};
},
data() {
return {
@@ -182,9 +184,7 @@ export default {
class="hidden w-auto h-8 mx-auto dark:block"
/>
<h2 class="mt-6 text-3xl font-medium text-center text-n-slate-12">
{{
useInstallationName($t('LOGIN.TITLE'), globalConfig.installationName)
}}
{{ replaceInstallationName($t('LOGIN.TITLE')) }}
</h2>
<p v-if="showSignupLink" class="mt-3 text-sm text-center text-n-slate-11">
{{ $t('COMMON.OR') }}