Merge branch 'feat/saml-controllers' into feat/saml-ui
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
verifyServiceWorkerExistence,
|
||||
} from './helper/pushHelper';
|
||||
import ReconnectService from 'dashboard/helper/ReconnectService';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
@@ -38,12 +39,14 @@ export default {
|
||||
const { accountId } = useAccount();
|
||||
// Use the font size composable (it automatically sets up the watcher)
|
||||
const { currentFontSize } = useFontSize();
|
||||
const { uiSettings } = useUISettings();
|
||||
|
||||
return {
|
||||
router,
|
||||
store,
|
||||
currentAccountId: accountId,
|
||||
currentFontSize,
|
||||
uiSettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -88,7 +91,10 @@ export default {
|
||||
mounted() {
|
||||
this.initializeColorTheme();
|
||||
this.listenToThemeChanges();
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
// If user locale is set, use it; otherwise use account locale
|
||||
this.setLocale(
|
||||
this.uiSettings?.locale || window.chatwootConfig.selectedLocale
|
||||
);
|
||||
},
|
||||
unmounted() {
|
||||
if (this.reconnectService) {
|
||||
@@ -114,7 +120,8 @@ export default {
|
||||
const { locale, latest_chatwoot_version: latestChatwootVersion } =
|
||||
this.getAccount(this.currentAccountId);
|
||||
const { pubsub_token: pubsubToken } = this.currentUser || {};
|
||||
this.setLocale(locale);
|
||||
// If user locale is set, use it; otherwise use account locale
|
||||
this.setLocale(this.uiSettings?.locale || locale);
|
||||
this.latestChatwootVersion = latestChatwootVersion;
|
||||
vueActionCable.init(this.store, pubsubToken);
|
||||
this.reconnectService = new ReconnectService(this.store, this.router);
|
||||
|
||||
@@ -8,6 +8,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: REPLY_EDITOR_MODES.REPLY,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['toggleMode']);
|
||||
@@ -20,9 +24,12 @@ const privateModeSize = useElementSize(wootEditorPrivateMode);
|
||||
|
||||
/**
|
||||
* Computed boolean indicating if the editor is in private note mode
|
||||
* When disabled, always show NOTE mode regardless of actual mode prop
|
||||
* @type {ComputedRef<boolean>}
|
||||
*/
|
||||
const isPrivate = computed(() => props.mode === REPLY_EDITOR_MODES.NOTE);
|
||||
const isPrivate = computed(() => {
|
||||
return props.disabled || props.mode === REPLY_EDITOR_MODES.NOTE;
|
||||
});
|
||||
|
||||
/**
|
||||
* Computes the width of the sliding background chip in pixels
|
||||
@@ -53,6 +60,10 @@ const translateValue = computed(() => {
|
||||
<template>
|
||||
<button
|
||||
class="flex items-center w-auto h-8 p-1 transition-all border rounded-full bg-n-alpha-2 group relative duration-300 ease-in-out z-0"
|
||||
:disabled="disabled"
|
||||
:class="{
|
||||
'cursor-not-allowed': disabled,
|
||||
}"
|
||||
@click="$emit('toggleMode')"
|
||||
>
|
||||
<div ref="wootEditorReplyMode" class="flex items-center gap-1 px-2 z-20">
|
||||
@@ -62,7 +73,10 @@ const translateValue = computed(() => {
|
||||
{{ $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') }}
|
||||
</div>
|
||||
<div
|
||||
class="absolute shadow-sm rounded-full h-6 w-[var(--chip-width)] transition-all duration-300 ease-in-out translate-x-[var(--translate-x)] rtl:translate-x-[var(--rtl-translate-x)] bg-n-solid-1"
|
||||
class="absolute shadow-sm rounded-full h-6 w-[var(--chip-width)] ease-in-out translate-x-[var(--translate-x)] rtl:translate-x-[var(--rtl-translate-x)] bg-n-solid-1"
|
||||
:class="{
|
||||
'transition-all duration-300': !disabled,
|
||||
}"
|
||||
:style="{
|
||||
'--chip-width': width,
|
||||
'--translate-x': translateValue,
|
||||
|
||||
@@ -10,7 +10,6 @@ import { getAllowedFileTypesByChannel } from '@chatwoot/utils';
|
||||
import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
|
||||
import VideoCallButton from '../VideoCallButton.vue';
|
||||
import AIAssistanceButton from '../AIAssistanceButton.vue';
|
||||
import { REPLY_EDITOR_MODES } from './constants';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
import { mapGetters } from 'vuex';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
@@ -20,9 +19,9 @@ export default {
|
||||
components: { NextButton, FileUpload, VideoCallButton, AIAssistanceButton },
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
default: REPLY_EDITOR_MODES.REPLY,
|
||||
isNote: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onSend: {
|
||||
type: Function,
|
||||
@@ -168,9 +167,6 @@ export default {
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
uiFlags: 'integrations/getUIFlags',
|
||||
}),
|
||||
isNote() {
|
||||
return this.mode === REPLY_EDITOR_MODES.NOTE;
|
||||
},
|
||||
wrapClass() {
|
||||
return {
|
||||
'is-note-mode': this.isNote,
|
||||
|
||||
@@ -15,6 +15,10 @@ export default {
|
||||
type: String,
|
||||
default: REPLY_EDITOR_MODES.REPLY,
|
||||
},
|
||||
isReplyRestricted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isMessageLengthReachingThreshold: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
@@ -30,6 +34,7 @@ export default {
|
||||
emit('setReplyMode', mode);
|
||||
};
|
||||
const handleReplyClick = () => {
|
||||
if (props.isReplyRestricted) return;
|
||||
setReplyMode(REPLY_EDITOR_MODES.REPLY);
|
||||
};
|
||||
const handleNoteClick = () => {
|
||||
@@ -88,6 +93,7 @@ export default {
|
||||
<div class="flex justify-between h-[3.25rem] gap-2 ltr:pl-3 rtl:pr-3">
|
||||
<EditorModeToggle
|
||||
:mode="mode"
|
||||
:disabled="isReplyRestricted"
|
||||
class="mt-3"
|
||||
@toggle-mode="handleModeToggle"
|
||||
/>
|
||||
|
||||
@@ -170,6 +170,9 @@ export default {
|
||||
}
|
||||
return true;
|
||||
},
|
||||
isReplyRestricted() {
|
||||
return !this.currentChat?.can_reply && !this.isAWhatsAppChannel;
|
||||
},
|
||||
inboxId() {
|
||||
return this.currentChat.inbox_id;
|
||||
},
|
||||
@@ -1070,6 +1073,7 @@ export default {
|
||||
<div ref="replyEditor" class="reply-box" :class="replyBoxClass">
|
||||
<ReplyTopPanel
|
||||
:mode="replyType"
|
||||
:is-reply-restricted="isReplyRestricted"
|
||||
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
|
||||
:characters-remaining="charactersRemaining"
|
||||
:popout-reply-box="popOutReplyBox"
|
||||
@@ -1180,7 +1184,7 @@ export default {
|
||||
:is-on-private-note="isOnPrivateNote"
|
||||
:is-recording-audio="isRecordingAudio"
|
||||
:is-send-disabled="isReplyButtonDisabled"
|
||||
:mode="replyType"
|
||||
:is-note="isPrivate"
|
||||
:on-file-upload="onFileUpload"
|
||||
:on-send="onSendReply"
|
||||
:conversation-type="conversationType"
|
||||
|
||||
@@ -43,18 +43,22 @@ describe('useFontSize', () => {
|
||||
|
||||
it('returns fontSizeOptions with correct structure', () => {
|
||||
const { fontSizeOptions } = useFontSize();
|
||||
expect(fontSizeOptions).toHaveLength(5);
|
||||
expect(fontSizeOptions[0]).toHaveProperty('value');
|
||||
expect(fontSizeOptions[0]).toHaveProperty('label');
|
||||
expect(fontSizeOptions.value).toHaveLength(5);
|
||||
expect(fontSizeOptions.value[0]).toHaveProperty('value');
|
||||
expect(fontSizeOptions.value[0]).toHaveProperty('label');
|
||||
|
||||
// Check specific options
|
||||
expect(fontSizeOptions.find(option => option.value === '16px')).toEqual({
|
||||
expect(
|
||||
fontSizeOptions.value.find(option => option.value === '16px')
|
||||
).toEqual({
|
||||
value: '16px',
|
||||
label:
|
||||
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.DEFAULT',
|
||||
});
|
||||
|
||||
expect(fontSizeOptions.find(option => option.value === '14px')).toEqual({
|
||||
expect(
|
||||
fontSizeOptions.value.find(option => option.value === '14px')
|
||||
).toEqual({
|
||||
value: '14px',
|
||||
label:
|
||||
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.SMALLER',
|
||||
@@ -143,12 +147,12 @@ describe('useFontSize', () => {
|
||||
const { fontSizeOptions } = useFontSize();
|
||||
|
||||
// Check that translation is applied
|
||||
expect(fontSizeOptions.find(option => option.value === '14px').label).toBe(
|
||||
'Smaller'
|
||||
);
|
||||
expect(fontSizeOptions.find(option => option.value === '16px').label).toBe(
|
||||
'Default'
|
||||
);
|
||||
expect(
|
||||
fontSizeOptions.value.find(option => option.value === '14px').label
|
||||
).toBe('Smaller');
|
||||
expect(
|
||||
fontSizeOptions.value.find(option => option.value === '16px').label
|
||||
).toBe('Default');
|
||||
|
||||
// Verify translation function was called with correct keys
|
||||
expect(mockTranslate).toHaveBeenCalledWith(
|
||||
|
||||
@@ -77,8 +77,8 @@ export const useFontSize = () => {
|
||||
* Font size options for select dropdown
|
||||
* @type {Array<{value: string, label: string}>}
|
||||
*/
|
||||
const fontSizeOptions = FONT_SIZE_NAMES.map(name =>
|
||||
createFontSizeOption(t, name)
|
||||
const fontSizeOptions = computed(() =>
|
||||
FONT_SIZE_NAMES.map(name => createFontSizeOption(t, name))
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,6 +51,13 @@
|
||||
"LARGER": "Larger",
|
||||
"EXTRA_LARGE": "Extra Large"
|
||||
}
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"TITLE": "Preferred Language",
|
||||
"NOTE": "Choose the language you want to use.",
|
||||
"UPDATE_SUCCESS": "Your Language settings have been updated successfully",
|
||||
"UPDATE_ERROR": "There is an error while updating the language settings, please try again",
|
||||
"USE_ACCOUNT_DEFAULT": "Use account default"
|
||||
}
|
||||
},
|
||||
"MESSAGE_SIGNATURE_SECTION": {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import WithLabel from 'v3/components/Form/WithLabel.vue';
|
||||
import NextInput from 'next/input/Input.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
@@ -33,12 +32,12 @@ export default {
|
||||
NextInput,
|
||||
},
|
||||
setup() {
|
||||
const { updateUISettings } = useUISettings();
|
||||
const { updateUISettings, uiSettings } = useUISettings();
|
||||
const { enabledLanguages } = useConfig();
|
||||
const { accountId } = useAccount();
|
||||
const v$ = useVuelidate();
|
||||
|
||||
return { updateUISettings, v$, enabledLanguages, accountId };
|
||||
return { updateUISettings, uiSettings, v$, enabledLanguages, accountId };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -112,7 +111,7 @@ export default {
|
||||
const { name, locale, id, domain, support_email, features } =
|
||||
this.getAccount(this.accountId);
|
||||
|
||||
this.$root.$i18n.locale = locale;
|
||||
this.$root.$i18n.locale = this.uiSettings?.locale || locale;
|
||||
this.name = name;
|
||||
this.locale = locale;
|
||||
this.id = id;
|
||||
@@ -137,21 +136,19 @@ export default {
|
||||
domain: this.domain,
|
||||
support_email: this.supportEmail,
|
||||
});
|
||||
this.$root.$i18n.locale = this.locale;
|
||||
// If user locale is set, update the locale with user locale
|
||||
if (this.uiSettings?.locale) {
|
||||
this.$root.$i18n.locale = this.uiSettings?.locale;
|
||||
} else {
|
||||
// If user locale is not set, update the locale with account locale
|
||||
this.$root.$i18n.locale = this.locale;
|
||||
}
|
||||
this.getAccount(this.id).locale = this.locale;
|
||||
this.updateDirectionView(this.locale);
|
||||
useAlert(this.$t('GENERAL_SETTINGS.UPDATE.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(this.$t('GENERAL_SETTINGS.UPDATE.ERROR'));
|
||||
}
|
||||
},
|
||||
|
||||
updateDirectionView(locale) {
|
||||
const isRTLSupported = getLanguageDirection(locale);
|
||||
this.updateUISettings({
|
||||
rtl_view: isRTLSupported,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -34,6 +34,7 @@ const {
|
||||
isAWhatsAppChannel,
|
||||
isAFacebookInbox,
|
||||
isATelegramChannel,
|
||||
isATwilioWhatsAppChannel,
|
||||
} = useInbox(route.params.inbox_id);
|
||||
|
||||
const hasDuplicateInstagramInbox = computed(() => {
|
||||
@@ -168,7 +169,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full col-span-6 p-6 overflow-auto">
|
||||
<div class="overflow-auto col-span-6 p-6 w-full h-full">
|
||||
<DuplicateInboxBanner
|
||||
v-if="hasDuplicateInstagramInbox"
|
||||
:content="$t('INBOX_MGMT.ADD.INSTAGRAM.NEW_INBOX_SUGGESTION')"
|
||||
@@ -187,7 +188,7 @@ onMounted(() => {
|
||||
</div>
|
||||
<div class="w-[50%] max-w-[50%] ml-[25%]">
|
||||
<woot-code
|
||||
v-if="isATwilioChannel"
|
||||
v-if="isATwilioWhatsAppChannel"
|
||||
lang="html"
|
||||
:script="currentInbox.callback_webhook_url"
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@ import UserProfilePicture from './UserProfilePicture.vue';
|
||||
import UserBasicDetails from './UserBasicDetails.vue';
|
||||
import MessageSignature from './MessageSignature.vue';
|
||||
import FontSize from './FontSize.vue';
|
||||
import UserLanguageSelect from './UserLanguageSelect.vue';
|
||||
import HotKeyCard from './HotKeyCard.vue';
|
||||
import ChangePassword from './ChangePassword.vue';
|
||||
import NotificationPreferences from './NotificationPreferences.vue';
|
||||
@@ -28,6 +29,7 @@ export default {
|
||||
MessageSignature,
|
||||
FormSection,
|
||||
FontSize,
|
||||
UserLanguageSelect,
|
||||
UserProfilePicture,
|
||||
Policy,
|
||||
UserBasicDetails,
|
||||
@@ -230,6 +232,12 @@ export default {
|
||||
"
|
||||
@change="updateFontSize"
|
||||
/>
|
||||
<UserLanguageSelect
|
||||
:label="$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.LANGUAGE.TITLE')"
|
||||
:description="
|
||||
$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.LANGUAGE.NOTE')
|
||||
"
|
||||
/>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
:title="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.TITLE')"
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
import FormSelect from 'v3/components/Form/Select.vue';
|
||||
|
||||
defineProps({
|
||||
label: { type: String, default: '' },
|
||||
description: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const { updateUISettings, uiSettings } = useUISettings();
|
||||
const { enabledLanguages } = useConfig();
|
||||
const { currentAccount } = useAccount();
|
||||
|
||||
const currentLanguage = computed(() => uiSettings.value?.locale ?? '');
|
||||
|
||||
const languageOptions = computed(() => [
|
||||
{
|
||||
name: t(
|
||||
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.LANGUAGE.USE_ACCOUNT_DEFAULT'
|
||||
),
|
||||
iso_639_1_code: '',
|
||||
},
|
||||
...(enabledLanguages ?? []),
|
||||
]);
|
||||
|
||||
const updateLanguage = async languageCode => {
|
||||
try {
|
||||
if (!languageCode) {
|
||||
// Clear preference to use account default
|
||||
await updateUISettings({ locale: null });
|
||||
locale.value = currentAccount.value.locale;
|
||||
useAlert(
|
||||
t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.LANGUAGE.UPDATE_SUCCESS')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const valid = (enabledLanguages || []).some(
|
||||
l => l.iso_639_1_code === languageCode
|
||||
);
|
||||
if (!valid) {
|
||||
throw new Error(`Invalid language code: ${languageCode}`);
|
||||
}
|
||||
|
||||
await updateUISettings({ locale: languageCode });
|
||||
// Apply immediately if the user explicitly chose a preference
|
||||
locale.value = languageCode;
|
||||
|
||||
useAlert(
|
||||
t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.LANGUAGE.UPDATE_SUCCESS')
|
||||
);
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.LANGUAGE.UPDATE_ERROR')
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const selectedValue = computed({
|
||||
get: () => currentLanguage.value,
|
||||
set: value => {
|
||||
updateLanguage(value);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-2 justify-between w-full items-start">
|
||||
<div>
|
||||
<label class="text-n-gray-12 font-medium leading-6 text-sm">
|
||||
{{ label }}
|
||||
</label>
|
||||
<p class="text-n-gray-11">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<FormSelect
|
||||
v-model="selectedValue"
|
||||
name="language"
|
||||
spacing="compact"
|
||||
class="min-w-28 mt-px"
|
||||
:options="languageOptions"
|
||||
label=""
|
||||
>
|
||||
<option
|
||||
v-for="option in languageOptions"
|
||||
:key="option.iso_639_1_code || 'default'"
|
||||
:value="option.iso_639_1_code"
|
||||
:selected="option.iso_639_1_code === selectedValue"
|
||||
>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</FormSelect>
|
||||
</div>
|
||||
</template>
|
||||
@@ -28,12 +28,16 @@ export const getters = {
|
||||
getUIFlags($state) {
|
||||
return $state.uiFlags;
|
||||
},
|
||||
isRTL: ($state, _, rootState) => {
|
||||
const accountId = rootState.route?.params?.accountId;
|
||||
if (!accountId) return false;
|
||||
isRTL: ($state, _getters, rootState, rootGetters) => {
|
||||
const accountId = Number(rootState.route?.params?.accountId);
|
||||
const userLocale = rootGetters?.getUISettings?.locale;
|
||||
const accountLocale =
|
||||
accountId && findRecordById($state, accountId)?.locale;
|
||||
|
||||
const { locale } = findRecordById($state, Number(accountId));
|
||||
return locale ? getLanguageDirection(locale) : false;
|
||||
// Prefer user locale; fallback to account locale
|
||||
const effectiveLocale = userLocale ?? accountLocale;
|
||||
|
||||
return effectiveLocale ? getLanguageDirection(effectiveLocale) : false;
|
||||
},
|
||||
isTrialAccount: $state => id => {
|
||||
const account = findRecordById($state, id);
|
||||
|
||||
@@ -49,35 +49,74 @@ describe('#getters', () => {
|
||||
});
|
||||
|
||||
describe('isRTL', () => {
|
||||
it('returns false when accountId is not present', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('returns false when accountId is not present and userLocale is not set', () => {
|
||||
const state = { records: [accountData] };
|
||||
const rootState = { route: { params: {} } };
|
||||
expect(getters.isRTL({}, null, rootState)).toBe(false);
|
||||
const rootGetters = {};
|
||||
|
||||
expect(getters.isRTL(state, null, rootState, rootGetters)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for RTL language', () => {
|
||||
const state = {
|
||||
records: [{ id: 1, locale: 'ar' }],
|
||||
};
|
||||
const rootState = { route: { params: { accountId: '1' } } };
|
||||
vi.spyOn(languageHelpers, 'getLanguageDirection').mockReturnValue(true);
|
||||
expect(getters.isRTL(state, null, rootState)).toBe(true);
|
||||
it('uses userLocale when present (no accountId)', () => {
|
||||
const state = { records: [accountData] };
|
||||
const rootState = { route: { params: {} } };
|
||||
const rootGetters = { getUISettings: { locale: 'ar' } };
|
||||
const spy = vi
|
||||
.spyOn(languageHelpers, 'getLanguageDirection')
|
||||
.mockReturnValue(true);
|
||||
|
||||
expect(getters.isRTL(state, null, rootState, rootGetters)).toBe(true);
|
||||
expect(spy).toHaveBeenCalledWith('ar');
|
||||
});
|
||||
|
||||
it('returns false for LTR language', () => {
|
||||
const state = {
|
||||
records: [{ id: 1, locale: 'en' }],
|
||||
};
|
||||
it('prefers userLocale over account locale when both are present', () => {
|
||||
const state = { records: [{ id: 1, locale: 'en' }] };
|
||||
const rootState = { route: { params: { accountId: '1' } } };
|
||||
vi.spyOn(languageHelpers, 'getLanguageDirection').mockReturnValue(false);
|
||||
expect(getters.isRTL(state, null, rootState)).toBe(false);
|
||||
const rootGetters = { getUISettings: { locale: 'ar' } };
|
||||
const spy = vi
|
||||
.spyOn(languageHelpers, 'getLanguageDirection')
|
||||
.mockReturnValue(true);
|
||||
|
||||
expect(getters.isRTL(state, null, rootState, rootGetters)).toBe(true);
|
||||
expect(spy).toHaveBeenCalledWith('ar');
|
||||
});
|
||||
|
||||
it('returns false when account is not found', () => {
|
||||
const state = {
|
||||
records: [],
|
||||
};
|
||||
it('falls back to account locale when userLocale is not provided', () => {
|
||||
const state = { records: [{ id: 1, locale: 'ar' }] };
|
||||
const rootState = { route: { params: { accountId: '1' } } };
|
||||
expect(getters.isRTL(state, null, rootState)).toBe(false);
|
||||
const rootGetters = {};
|
||||
const spy = vi
|
||||
.spyOn(languageHelpers, 'getLanguageDirection')
|
||||
.mockReturnValue(true);
|
||||
|
||||
expect(getters.isRTL(state, null, rootState, rootGetters)).toBe(true);
|
||||
expect(spy).toHaveBeenCalledWith('ar');
|
||||
});
|
||||
|
||||
it('returns false for LTR language when userLocale is provided', () => {
|
||||
const state = { records: [{ id: 1, locale: 'en' }] };
|
||||
const rootState = { route: { params: { accountId: '1' } } };
|
||||
const rootGetters = { getUISettings: { locale: 'en' } };
|
||||
const spy = vi
|
||||
.spyOn(languageHelpers, 'getLanguageDirection')
|
||||
.mockReturnValue(false);
|
||||
|
||||
expect(getters.isRTL(state, null, rootState, rootGetters)).toBe(false);
|
||||
expect(spy).toHaveBeenCalledWith('en');
|
||||
});
|
||||
|
||||
it('returns false when accountId present but user locale is null', () => {
|
||||
const state = { records: [{ id: 1, locale: 'en' }] };
|
||||
const rootState = { route: { params: { accountId: '1' } } };
|
||||
const rootGetters = { getUISettings: { locale: null } };
|
||||
const spy = vi.spyOn(languageHelpers, 'getLanguageDirection');
|
||||
|
||||
expect(getters.isRTL(state, null, rootState, rootGetters)).toBe(false);
|
||||
expect(spy).toHaveBeenCalledWith('en');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user