Merge branch 'develop' into fix/next-message-bubbles

This commit is contained in:
Sivin Varghese
2025-03-10 21:52:34 +05:30
committed by GitHub
633 changed files with 9981 additions and 3530 deletions
+9 -1
View File
@@ -14,6 +14,7 @@ import WootSnackbarBox from './components/SnackbarContainer.vue';
import { setColorTheme } from './helper/themeHelper';
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
import { useAccount } from 'dashboard/composables/useAccount';
import { useFontSize } from 'dashboard/composables/useFontSize';
import {
registerSubscription,
verifyServiceWorkerExistence,
@@ -37,8 +38,15 @@ export default {
const router = useRouter();
const store = useStore();
const { accountId } = useAccount();
// Use the font size composable (it automatically sets up the watcher)
const { currentFontSize } = useFontSize();
return { router, store, currentAccountId: accountId };
return {
router,
store,
currentAccountId: accountId,
currentFontSize,
};
},
data() {
return {
@@ -0,0 +1,9 @@
import ApiClient from '../ApiClient';
class CaptainBulkActionsAPI extends ApiClient {
constructor() {
super('captain/bulk_actions', { accountScoped: true });
}
}
export default new CaptainBulkActionsAPI();
@@ -137,6 +137,10 @@ class ConversationApi extends ApiClient {
requestCopilot(conversationId, body) {
return axios.post(`${this.url}/${conversationId}/copilot`, body);
}
getInboxAssistant(conversationId) {
return axios.get(`${this.url}/${conversationId}/inbox_assistant`);
}
}
export default new ConversationApi();
@@ -82,7 +82,7 @@ input[type='url']:not(.reset-base) {
}
input[type='file'] {
@apply bg-white dark:bg-slate-800 leading-[1.15] mb-4;
@apply bg-white dark:bg-n-solid-1 leading-[1.15] mb-4;
}
// Select
@@ -141,11 +141,16 @@ code {
@apply text-xs border-0;
&.hljs {
@apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-50 rounded-lg p-5;
@apply bg-n-slate-3 dark:bg-n-solid-3 text-slate-800 dark:text-slate-50 rounded-lg p-5;
.hljs-number,
.hljs-string {
@apply text-red-800 dark:text-red-400;
}
.hljs-name,
.hljs-tag {
@apply text-n-slate-11;
}
}
}
@@ -22,7 +22,7 @@ const handleButtonClick = () => {
<template>
<section class="flex flex-col w-full h-full overflow-hidden bg-n-background">
<header class="sticky top-0 z-10 px-6 lg:px-0">
<div class="w-full max-w-[960px] mx-auto">
<div class="w-full max-w-[60rem] mx-auto">
<div class="flex items-center justify-between w-full h-20 gap-2">
<span class="text-xl font-medium text-n-slate-12">
{{ headerTitle }}
@@ -44,7 +44,7 @@ const handleButtonClick = () => {
</div>
</header>
<main class="flex-1 px-6 overflow-y-auto lg:px-0">
<div class="w-full max-w-[960px] mx-auto py-4">
<div class="w-full max-w-[60rem] mx-auto py-4">
<slot name="default" />
</div>
</main>
@@ -40,7 +40,7 @@ const handleSubmit = campaignDetails => {
<template>
<div
class="w-[400px] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-slate-50 dark:border-slate-900 shadow-md flex flex-col gap-6 max-h-[85vh] overflow-y-auto"
class="w-[25rem] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-slate-50 dark:border-slate-900 shadow-md flex flex-col gap-6 max-h-[85vh] overflow-y-auto"
>
<h3 class="text-base font-medium text-slate-900 dark:text-slate-50">
{{ t(`CAMPAIGN.LIVE_CHAT.CREATE.TITLE`) }}
@@ -39,7 +39,7 @@ const handleClose = () => emit('close');
<template>
<div
class="w-[400px] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-slate-50 dark:border-slate-900 shadow-md flex flex-col gap-6"
class="w-[25rem] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-slate-50 dark:border-slate-900 shadow-md flex flex-col gap-6"
>
<h3 class="text-base font-medium text-slate-900 dark:text-slate-50">
{{ t(`CAMPAIGN.SMS.CREATE.TITLE`) }}
@@ -4,6 +4,10 @@ defineProps({
type: String,
default: 'col',
},
selectable: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['click']);
@@ -18,10 +22,11 @@ const handleClick = () => {
class="flex flex-col w-full shadow outline-1 outline outline-n-container group/cardLayout rounded-2xl bg-n-solid-2"
>
<div
class="flex w-full gap-3 px-6 py-5"
:class="
layout === 'col' ? 'flex-col' : 'flex-row justify-between items-center'
"
class="flex w-full gap-3 py-5"
:class="[
layout === 'col' ? 'flex-col' : 'flex-row justify-between items-center',
selectable ? 'px-10 py-6' : 'px-6',
]"
@click="handleClick"
>
<slot />
@@ -65,8 +65,8 @@ const toggleBlock = () => {
<div
class="flex flex-col w-full h-full transition-all duration-300 ltr:2xl:ml-56 rtl:2xl:mr-56"
>
<header class="sticky top-0 z-10 px-6 xl:px-0">
<div class="w-full mx-auto max-w-[650px]">
<header class="sticky top-0 z-10 px-6 3xl:px-0">
<div class="w-full mx-auto max-w-[40.625rem]">
<div class="flex items-center justify-between w-full h-20 gap-2">
<Breadcrumb
:items="breadcrumbItems"
@@ -98,8 +98,8 @@ const toggleBlock = () => {
</div>
</div>
</header>
<main class="flex-1 px-6 overflow-y-auto xl:px-px">
<div class="w-full py-4 mx-auto max-w-[650px]">
<main class="flex-1 px-6 overflow-y-auto 3xl:px-px">
<div class="w-full py-4 mx-auto max-w-[40.625rem]">
<slot name="default" />
</div>
</main>
@@ -107,7 +107,7 @@ const toggleBlock = () => {
<div
v-if="slots.sidebar"
class="overflow-y-auto justify-end min-w-[200px] w-full py-6 max-w-[440px] border-l border-n-weak bg-n-solid-2"
class="overflow-y-auto justify-end min-w-52 w-full py-6 max-w-md border-l border-n-weak bg-n-solid-2"
>
<slot name="sidebar" />
</div>
@@ -60,7 +60,7 @@ const emit = defineEmits([
<template>
<header class="sticky top-0 z-10">
<div
class="flex items-center justify-between w-full h-20 px-6 gap-2 mx-auto max-w-[960px]"
class="flex items-center justify-between w-full h-20 px-6 gap-2 mx-auto max-w-[60rem]"
>
<span class="text-xl font-medium truncate text-n-slate-12">
{{ headerTitle }}
@@ -62,7 +62,7 @@ const activeFilterQueryData = computed(() => {
t('CONTACTS_LAYOUT.FILTER.ACTIVE_FILTERS.CLEAR_FILTERS')
"
:show-clear-button="!hasActiveSegments"
class="max-w-[960px] px-6"
class="max-w-[60rem] px-6"
@open-filter="emit('openFilter')"
@clear-filters="emit('clearFilters')"
/>
@@ -72,7 +72,7 @@ const openFilter = () => {
@clear-filters="emit('clearFilters')"
/>
<main class="flex-1 overflow-y-auto">
<div class="w-full mx-auto max-w-[960px]">
<div class="w-full mx-auto max-w-[60rem]">
<ContactsActiveFiltersPreview
v-if="
(hasAppliedFilters || !isNotSegmentView) &&
@@ -22,7 +22,7 @@ defineProps({
class="relative flex flex-col items-center justify-center w-full h-full overflow-hidden"
>
<div
class="relative w-full max-w-[960px] mx-auto overflow-hidden h-full max-h-[448px]"
class="relative w-full max-w-[60rem] mx-auto overflow-hidden h-full max-h-[28rem]"
>
<div
class="w-full h-full space-y-4 overflow-y-hidden opacity-50 pointer-events-none"
@@ -60,7 +60,7 @@ const togglePortalSwitcher = () => {
<template>
<section class="flex flex-col w-full h-full overflow-hidden bg-n-background">
<header class="sticky top-0 z-10 px-6 pb-3 lg:px-0">
<div class="w-full max-w-[960px] mx-auto lg:px-6">
<div class="w-full max-w-[60rem] mx-auto lg:px-6">
<div
v-if="showHeaderTitle"
class="flex items-center justify-start h-20 gap-2"
@@ -96,7 +96,7 @@ const togglePortalSwitcher = () => {
</div>
</header>
<main class="flex-1 px-6 overflow-y-auto lg:px-0">
<div class="w-full max-w-[960px] mx-auto py-3 lg:px-6">
<div class="w-full max-w-[60rem] mx-auto py-3 lg:px-6">
<slot name="content" />
</div>
</main>
@@ -52,7 +52,7 @@ onMounted(() => {
<template>
<div
class="flex flex-col absolute w-[400px] bg-n-alpha-3 outline outline-1 outline-n-container backdrop-blur-[100px] shadow-lg gap-6 rounded-xl p-6"
class="flex flex-col absolute w-[25rem] bg-n-alpha-3 outline outline-1 outline-n-container backdrop-blur-[100px] shadow-lg gap-6 rounded-xl p-6"
>
<div class="flex items-center justify-between">
<h3>
@@ -75,7 +75,7 @@ onMounted(() => {
<div>
<div class="flex justify-between w-full gap-4 py-2">
<label
class="text-sm font-medium whitespace-nowrap min-w-[100px] text-slate-900 dark:text-slate-50"
class="text-sm font-medium whitespace-nowrap min-w-[6.25rem] text-slate-900 dark:text-slate-50"
>
{{
t(
@@ -90,9 +90,9 @@ onMounted(() => {
'HELP_CENTER.EDIT_ARTICLE_PAGE.ARTICLE_PROPERTIES.META_DESCRIPTION_PLACEHOLDER'
)
"
class="w-[220px]"
class="w-[13.75rem]"
custom-text-area-wrapper-class="!p-0 !border-0 !rounded-none !bg-transparent transition-none"
custom-text-area-class="max-h-[150px]"
custom-text-area-class="max-h-[9.375rem]"
auto-height
min-height="3rem"
/>
@@ -108,12 +108,12 @@ onMounted(() => {
:label="
t('HELP_CENTER.EDIT_ARTICLE_PAGE.ARTICLE_PROPERTIES.META_TITLE')
"
custom-label-class="min-w-[120px]"
custom-label-class="min-w-[7.5rem]"
/>
</div>
<div class="flex justify-between w-full gap-3 py-2">
<label
class="text-sm font-medium whitespace-nowrap min-w-[120px] text-slate-900 dark:text-slate-50"
class="text-sm font-medium whitespace-nowrap min-w-[7.5rem] text-slate-900 dark:text-slate-50"
>
{{
t('HELP_CENTER.EDIT_ARTICLE_PAGE.ARTICLE_PROPERTIES.META_TAGS')
@@ -126,7 +126,7 @@ onMounted(() => {
'HELP_CENTER.EDIT_ARTICLE_PAGE.ARTICLE_PROPERTIES.META_TAGS_PLACEHOLDER'
)
"
class="w-[224px]"
class="w-[14rem]"
/>
</div>
</div>
@@ -90,7 +90,7 @@ const handleCategory = async formData => {
<template>
<div
class="w-[400px] absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-slate-50 dark:border-slate-900 shadow-md flex flex-col gap-6"
class="w-[25rem] absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-slate-50 dark:border-slate-900 shadow-md flex flex-col gap-6"
>
<h3 class="text-base font-medium text-slate-900 dark:text-slate-50">
{{
@@ -201,7 +201,7 @@ defineExpose({ state, isSubmitDisabled });
color="slate"
size="sm"
:icon="!state.icon ? 'i-lucide-smile-plus' : ''"
class="!h-[38px] !w-[38px] absolute top-[31px] !outline-none !rounded-[7px] border-0 ltr:left-px rtl:right-px ltr:!rounded-r-none rtl:!rounded-l-none"
class="!h-[2.4rem] !w-[2.375rem] absolute top-[1.94rem] !outline-none !rounded-[0.438rem] border-0 ltr:left-px rtl:right-px ltr:!rounded-r-none rtl:!rounded-l-none"
@click="isEmojiPickerOpen = !isEmojiPickerOpen"
/>
<EmojiInput
@@ -74,7 +74,7 @@ const handleDeletePortal = () => {
</div>
<div
v-else-if="activePortal"
class="flex flex-col w-full gap-4 max-w-[640px] pb-8"
class="flex flex-col w-full gap-4 max-w-[40rem] pb-8"
>
<PortalBaseSettings
:active-portal="activePortal"
@@ -92,7 +92,7 @@ const redirectToPortalHomePage = () => {
<template>
<div
class="pt-5 pb-3 bg-n-alpha-3 backdrop-blur-[100px] outline outline-n-container outline-1 z-50 absolute w-[440px] rounded-xl shadow-md flex flex-col gap-4"
class="pt-5 pb-3 bg-n-alpha-3 backdrop-blur-[100px] outline outline-n-container outline-1 z-50 absolute w-[27.5rem] rounded-xl shadow-md flex flex-col gap-4"
>
<div
class="flex items-center justify-between gap-4 px-6 pb-3 border-b border-n-alpha-2"
@@ -13,6 +13,7 @@ import {
createNewContact,
fetchContactableInboxes,
processContactableInboxes,
mergeInboxDetails,
} from 'dashboard/components-next/NewConversation/helpers/composeConversationHelper';
import ComposeNewConversationForm from 'dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue';
@@ -47,6 +48,7 @@ const currentUser = useMapGetter('getCurrentUser');
const globalConfig = useMapGetter('globalConfig/get');
const uiFlags = useMapGetter('contactConversations/getUIFlags');
const messageSignature = useMapGetter('getMessageSignature');
const inboxesList = useMapGetter('inboxes/getInboxes');
const sendWithSignature = computed(() =>
fetchSignatureFlagFromUISettings(targetInbox.value?.channelType)
@@ -104,7 +106,12 @@ const handleSelectedContact = async ({ value, action, ...rest }) => {
isFetchingInboxes.value = true;
try {
const contactableInboxes = await fetchContactableInboxes(contact.id);
selectedContact.value.contactInboxes = contactableInboxes;
// Merge the processed contactableInboxes with the inboxesList
selectedContact.value.contactInboxes = mergeInboxDetails(
contactableInboxes,
inboxesList.value
);
isFetchingInboxes.value = false;
} catch (error) {
isFetchingInboxes.value = false;
@@ -162,9 +169,12 @@ watch(
() => {
if (activeContact.value && props.contactId) {
const contactInboxes = activeContact.value?.contactInboxes || [];
// First process the contactable inboxes to get the right structure
const processedInboxes = processContactableInboxes(contactInboxes);
// Then Merge processedInboxes with the inboxes list
selectedContact.value = {
...activeContact.value,
contactInboxes: processContactableInboxes(contactInboxes),
contactInboxes: mergeInboxDetails(processedInboxes, inboxesList.value),
};
}
},
@@ -148,7 +148,7 @@ useKeyboardEvents(keyboardEvents);
<template>
<div
class="flex items-center justify-between w-full h-[52px] gap-2 px-4 py-3"
class="flex items-center justify-between w-full h-[3.25rem] gap-2 px-4 py-3"
>
<div class="flex items-center gap-2">
<WhatsAppOptions
@@ -47,10 +47,10 @@ const removeAttachment = id => {
<div
v-for="attachment in filteredImageAttachments"
:key="attachment.id"
class="relative group/image w-[72px] h-[72px]"
class="relative group/image w-[4.5rem] h-[4.5rem]"
>
<img
class="object-cover w-[72px] h-[72px] rounded-lg"
class="object-cover w-[4.5rem] h-[4.5rem] rounded-lg"
:src="attachment.thumb"
/>
<Button
@@ -69,7 +69,7 @@ const removeAttachment = id => {
<div
v-for="attachment in filteredNonImageAttachments"
:key="attachment.id"
class="max-w-[300px] inline-flex items-center h-8 min-w-0 bg-n-alpha-2 dark:bg-n-solid-3 rounded-lg gap-3 ltr:pl-3 rtl:pr-3 ltr:pr-2 rtl:pl-2"
class="max-w-[18.75rem] inline-flex items-center h-8 min-w-0 bg-n-alpha-2 dark:bg-n-solid-3 rounded-lg gap-3 ltr:pl-3 rtl:pr-3 ltr:pr-2 rtl:pl-2"
>
<span class="text-sm font-medium text-n-slate-11">
{{ fileNameWithEllipsis(attachment.resource) }}
@@ -265,7 +265,7 @@ const handleSendWhatsappMessage = async ({ message, templateParams }) => {
<template>
<div
class="w-[670px] mt-2 divide-y divide-n-strong overflow-visible transition-all duration-300 ease-in-out top-full justify-between flex flex-col bg-n-alpha-3 border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl"
class="w-[42rem] mt-2 divide-y divide-n-strong overflow-visible transition-all duration-300 ease-in-out top-full justify-between flex flex-col bg-n-alpha-3 border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl"
>
<ContactSelector
:contacts="contacts"
@@ -87,6 +87,21 @@ export const processContactableInboxes = inboxes => {
}));
};
export const mergeInboxDetails = (inboxesData, inboxesList = []) => {
if (!inboxesData || !inboxesData.length) {
return [];
}
return inboxesData.map(inboxData => {
const matchingInbox =
inboxesList.find(inbox => inbox.id === inboxData.id) || {};
return {
...camelcaseKeys(matchingInbox, { deep: true }),
...inboxData,
};
});
};
export const prepareAttachmentPayload = (
attachedFiles,
directUploadsEnabled
@@ -110,6 +110,153 @@ describe('composeConversationHelper', () => {
});
});
describe('mergeInboxDetails', () => {
it('returns empty array if inboxesData is empty or null', () => {
expect(helpers.mergeInboxDetails(null)).toEqual([]);
expect(helpers.mergeInboxDetails([])).toEqual([]);
expect(helpers.mergeInboxDetails(undefined)).toEqual([]);
});
it('merges inbox data with matching inboxes from the list', () => {
const inboxesData = [
{ id: 1, sourceId: 'source1' },
{ id: 2, sourceId: 'source2' },
];
const inboxesList = [
{
id: 1,
name: 'Inbox 1',
channel_type: 'Channel::Email',
channel_id: 10,
phone_number: null,
},
{
id: 2,
name: 'Inbox 2',
channel_type: 'Channel::Whatsapp',
channel_id: 20,
phone_number: '+1234567890',
},
{
id: 3,
name: 'Inbox 3',
channel_type: 'Channel::Api',
channel_id: 30,
phone_number: null,
},
];
const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
expect(result.length).toBe(2);
expect(result[0]).toMatchObject({
id: 1,
sourceId: 'source1',
name: 'Inbox 1',
channelType: 'Channel::Email',
channelId: 10,
phoneNumber: null,
});
expect(result[1]).toMatchObject({
id: 2,
sourceId: 'source2',
name: 'Inbox 2',
channelType: 'Channel::Whatsapp',
channelId: 20,
phoneNumber: '+1234567890',
});
});
it('handles inboxes not found in the list', () => {
const inboxesData = [
{ id: 1, sourceId: 'source1' },
{ id: 99, sourceId: 'source99' }, // This doesn't exist in inboxesList
];
const inboxesList = [
{
id: 1,
name: 'Inbox 1',
channel_type: 'Channel::Email',
},
];
const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
expect(result.length).toBe(2);
expect(result[0]).toMatchObject({
id: 1,
sourceId: 'source1',
name: 'Inbox 1',
channelType: 'Channel::Email',
});
expect(result[1]).toMatchObject({
id: 99,
sourceId: 'source99',
});
expect(result[1].name).toBeUndefined();
expect(result[1].channelType).toBeUndefined();
});
it('camelcases properties from inboxesList', () => {
const inboxesData = [{ id: 1, sourceId: 'source1' }];
const inboxesList = [
{
id: 1,
name: 'Inbox 1',
channel_type: 'Channel::Email',
avatar_url: 'https://example.com/avatar.png',
working_hours: [
{
day_of_week: 1,
closed_all_day: false,
},
],
},
];
const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
expect(result[0]).toMatchObject({
id: 1,
sourceId: 'source1',
name: 'Inbox 1',
channelType: 'Channel::Email',
avatarUrl: 'https://example.com/avatar.png',
});
expect(result[0].workingHours[0]).toMatchObject({
dayOfWeek: 1,
closedAllDay: false,
});
});
it('preserves original properties when they conflict with inboxesList', () => {
const inboxesData = [
{ id: 1, sourceId: 'source1', name: 'Original Name' },
];
const inboxesList = [
{
id: 1,
name: 'List Name',
channel_type: 'Channel::Email',
},
];
const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
expect(result[0].name).toBe('Original Name');
expect(result[0].channelType).toBe('Channel::Email');
});
});
describe('prepareAttachmentPayload', () => {
it('prepares direct upload files', () => {
const files = [{ blobSignedId: 'signed1' }];
@@ -99,45 +99,50 @@ const computedJustify = computed(() => {
const STYLE_CONFIG = {
colors: {
blue: {
solid: 'bg-n-brand text-white hover:brightness-110 outline-transparent',
solid:
'bg-n-brand text-white hover:enabled:brightness-110 outline-transparent',
faded:
'bg-n-brand/10 text-n-blue-text hover:bg-n-brand/20 outline-transparent',
'bg-n-brand/10 text-n-blue-text hover:enabled:bg-n-brand/20 outline-transparent',
outline: 'text-n-blue-text outline-n-blue-border',
ghost: 'text-n-blue-text hover:bg-n-alpha-2 outline-transparent',
link: 'text-n-blue-text hover:underline outline-transparent',
ghost: 'text-n-blue-text hover:enabled:bg-n-alpha-2 outline-transparent',
link: 'text-n-blue-text hover:enabled:underline outline-transparent',
},
ruby: {
solid: 'bg-n-ruby-9 text-white hover:bg-n-ruby-10 outline-transparent',
solid:
'bg-n-ruby-9 text-white hover:enabled:bg-n-ruby-10 outline-transparent',
faded:
'bg-n-ruby-9/10 text-n-ruby-11 hover:bg-n-ruby-9/20 outline-transparent',
outline: 'text-n-ruby-11 hover:bg-n-ruby-9/10 outline-n-ruby-8',
ghost: 'text-n-ruby-11 hover:bg-n-alpha-2 outline-transparent',
link: 'text-n-ruby-9 hover:underline outline-transparent',
'bg-n-ruby-9/10 text-n-ruby-11 hover:enabled:bg-n-ruby-9/20 outline-transparent',
outline: 'text-n-ruby-11 hover:enabled:bg-n-ruby-9/10 outline-n-ruby-8',
ghost: 'text-n-ruby-11 hover:enabled:bg-n-alpha-2 outline-transparent',
link: 'text-n-ruby-9 hover:enabled:underline outline-transparent',
},
amber: {
solid: 'bg-n-amber-9 text-white hover:bg-n-amber-10 outline-transparent',
solid:
'bg-n-amber-9 text-white hover:enabled:bg-n-amber-10 outline-transparent',
faded:
'bg-n-amber-9/10 text-n-slate-12 hover:bg-n-amber-9/20 outline-transparent',
outline: 'text-n-amber-11 hover:bg-n-amber-9/10 outline-n-amber-9',
link: 'text-n-amber-9 hover:underline outline-transparent',
ghost: 'text-n-amber-9 hover:bg-n-alpha-2 outline-transparent',
'bg-n-amber-9/10 text-n-slate-12 hover:enabled:bg-n-amber-9/20 outline-transparent',
outline:
'text-n-amber-11 hover:enabled:bg-n-amber-9/10 outline-n-amber-9',
link: 'text-n-amber-9 hover:enabled:underline outline-transparent',
ghost: 'text-n-amber-9 hover:enabled:bg-n-alpha-2 outline-transparent',
},
slate: {
solid:
'bg-n-solid-3 dark:hover:bg-n-solid-2 hover:bg-n-alpha-2 text-n-slate-12 outline-n-container',
'bg-n-solid-3 dark:hover:enabled:bg-n-solid-2 hover:enabled:bg-n-alpha-2 text-n-slate-12 outline-n-container',
faded:
'bg-n-slate-9/10 text-n-slate-12 hover:bg-n-slate-9/20 outline-transparent',
outline: 'text-n-slate-11 outline-n-strong hover:bg-n-slate-9/10',
link: 'text-n-slate-11 hover:text-n-slate-12 hover:underline outline-transparent',
ghost: 'text-n-slate-12 hover:bg-n-alpha-2 outline-transparent',
'bg-n-slate-9/10 text-n-slate-12 hover:enabled:bg-n-slate-9/20 outline-transparent',
outline: 'text-n-slate-11 outline-n-strong hover:enabled:bg-n-slate-9/10',
link: 'text-n-slate-11 hover:enabled:text-n-slate-12 hover:enabled:underline outline-transparent',
ghost: 'text-n-slate-12 hover:enabled:bg-n-alpha-2 outline-transparent',
},
teal: {
solid: 'bg-n-teal-9 text-white hover:bg-n-teal-10 outline-transparent',
solid:
'bg-n-teal-9 text-white hover:enabled:bg-n-teal-10 outline-transparent',
faded:
'bg-n-teal-9/10 text-n-slate-12 hover:bg-n-teal-9/20 outline-transparent',
outline: 'text-n-teal-11 hover:bg-n-teal-9/10 outline-n-teal-9',
link: 'text-n-teal-9 hover:underline outline-transparent',
ghost: 'text-n-teal-9 hover:bg-n-alpha-2 outline-transparent',
'bg-n-teal-9/10 text-n-slate-12 hover:enabled:bg-n-teal-9/20 outline-transparent',
outline: 'text-n-teal-11 hover:enabled:bg-n-teal-9/10 outline-n-teal-9',
link: 'text-n-teal-9 hover:enabled:underline outline-transparent',
ghost: 'text-n-teal-9 hover:enabled:bg-n-alpha-2 outline-transparent',
},
},
sizes: {
@@ -171,7 +176,7 @@ const STYLE_CONFIG = {
center: 'justify-center',
end: 'justify-end',
},
base: 'inline-flex items-center min-w-0 gap-2 transition-all duration-200 ease-in-out border-0 rounded-lg outline-1 outline disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50',
base: 'inline-flex items-center min-w-0 gap-2 transition-all duration-200 ease-in-out border-0 rounded-lg outline-1 outline disabled:opacity-50',
};
const variantClasses = computed(() => {
@@ -68,7 +68,7 @@ const handlePageChange = event => {
<template>
<section class="flex flex-col w-full h-full overflow-hidden bg-n-background">
<header class="sticky top-0 z-10 px-6 xl:px-0">
<div class="w-full max-w-[960px] mx-auto">
<div class="w-full max-w-[60rem] mx-auto">
<div
class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row"
>
@@ -96,7 +96,7 @@ const handlePageChange = event => {
</div>
</header>
<main class="flex-1 px-6 overflow-y-auto xl:px-0">
<div class="w-full max-w-[960px] mx-auto py-4">
<div class="w-full max-w-[60rem] mx-auto py-4">
<slot v-if="!showPaywall" name="controls" />
<div
v-if="isFetching"
@@ -7,6 +7,7 @@ import { dynamicTime } from 'shared/helpers/timeHelper';
import CardLayout from 'dashboard/components-next/CardLayout.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
import Policy from 'dashboard/components/policy.vue';
const props = defineProps({
@@ -46,14 +47,27 @@ const props = defineProps({
type: Number,
required: true,
},
isSelected: {
type: Boolean,
default: false,
},
selectable: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['action', 'navigate']);
const emit = defineEmits(['action', 'navigate', 'select', 'hover']);
const { t } = useI18n();
const [showActionsDropdown, toggleDropdown] = useToggle();
const modelValue = computed({
get: () => props.isSelected,
set: () => emit('select', props.id),
});
const statusAction = computed(() => {
if (props.status === 'pending') {
return [
@@ -102,8 +116,17 @@ const handleDocumentableClick = () => {
</script>
<template>
<CardLayout :class="{ 'rounded-md': compact }">
<div class="flex justify-between w-full gap-1">
<CardLayout
selectable
class="relative"
:class="{ 'rounded-md': compact }"
@mouseenter="emit('hover', true)"
@mouseleave="emit('hover', false)"
>
<div v-show="selectable" class="absolute top-7 ltr:left-4 rtl:right-4">
<Checkbox v-model="modelValue" />
</div>
<div class="flex relative justify-between w-full gap-1">
<span class="text-base text-n-slate-12 line-clamp-1">
{{ question }}
</span>
@@ -148,7 +171,7 @@ const handleDocumentableClick = () => {
v-if="documentable.type === 'Captain::Document'"
class="inline-flex items-center gap-1 truncate over"
>
<i class="i-ph-chat-circle-dots text-base" />
<i class="i-ph-files-light text-base" />
<span class="max-w-96 truncate" :title="documentable.name">
{{ documentable.name }}
</span>
@@ -0,0 +1,59 @@
<script setup>
import { ref, computed } from 'vue';
import { useStore } from 'dashboard/composables/store';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
const props = defineProps({
type: {
type: String,
required: true,
},
bulkIds: {
type: Object,
required: true,
},
});
const emit = defineEmits(['deleteSuccess']);
const { t } = useI18n();
const store = useStore();
const bulkDeleteDialogRef = ref(null);
const i18nKey = computed(() => props.type.toUpperCase());
const handleBulkDelete = async ids => {
if (!ids) return;
try {
await store.dispatch(
'captainBulkActions/handleBulkDelete',
Array.from(props.bulkIds)
);
emit('deleteSuccess');
useAlert(t(`CAPTAIN.${i18nKey.value}.BULK_DELETE.SUCCESS_MESSAGE`));
} catch (error) {
useAlert(t(`CAPTAIN.${i18nKey.value}.BULK_DELETE.ERROR_MESSAGE`));
}
};
const handleDialogConfirm = async () => {
await handleBulkDelete(Array.from(props.bulkIds));
bulkDeleteDialogRef.value?.close();
};
defineExpose({ dialogRef: bulkDeleteDialogRef });
</script>
<template>
<Dialog
ref="bulkDeleteDialogRef"
type="alert"
:title="t(`CAPTAIN.${i18nKey}.BULK_DELETE.TITLE`)"
:description="t(`CAPTAIN.${i18nKey}.BULK_DELETE.DESCRIPTION`)"
:confirm-button-label="t(`CAPTAIN.${i18nKey}.BULK_DELETE.CONFIRM`)"
@confirm="handleDialogConfirm"
/>
</template>
@@ -27,7 +27,7 @@ const openBilling = () => {
<template>
<div
class="w-full max-w-[960px] mx-auto h-full max-h-[448px] grid place-content-center"
class="w-full max-w-[60rem] mx-auto h-full max-h-[448px] grid place-content-center"
>
<BasePaywallModal
class="mx-auto"
@@ -0,0 +1,47 @@
<script setup>
import Checkbox from './Checkbox.vue';
import { ref } from 'vue';
const defaultValue = ref(false);
const isChecked = ref(false);
const checkedValue = ref(true);
const indeterminateValue = ref(true);
</script>
<template>
<Story title="Components/Checkbox" :layout="{ type: 'grid', width: '250px' }">
<Variant title="States">
<div class="p-2 space-y-4">
<div class="flex items-center justify-between gap-4">
<span>Default:</span>
<Checkbox v-model="defaultValue" />
</div>
<div class="flex items-center justify-between gap-4">
<span>Checked:</span>
<Checkbox v-model="checkedValue" />
</div>
<div class="flex items-center justify-between gap-4">
<span>Indeterminate:</span>
<Checkbox v-model="indeterminateValue" indeterminate />
</div>
<div class="flex items-center justify-between gap-4">
<span>Indeterminate disabled:</span>
<Checkbox v-model="indeterminateValue" indeterminate disabled />
</div>
<div class="flex items-center justify-between gap-4">
<span>Disabled:</span>
<Checkbox v-model="defaultValue" disabled />
</div>
<div class="flex items-center justify-between gap-4">
<span>Disabled Checked:</span>
<Checkbox v-model="isChecked" disabled />
</div>
</div>
</Variant>
</Story>
</template>
@@ -0,0 +1,63 @@
<script setup>
defineProps({
indeterminate: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['change']);
const modelValue = defineModel('modelValue', {
type: Boolean,
default: false,
});
const handleChange = event => {
modelValue.value = event.target.checked;
emit('change', event);
};
</script>
<template>
<div class="relative w-4 h-4">
<input
:checked="modelValue"
:indeterminate="indeterminate"
type="checkbox"
:disabled="disabled"
class="peer absolute inset-0 z-10 h-4 w-4 disabled:opacity-50 appearance-none rounded border border-n-slate-6 ring-transparent transition-all duration-200 checked:border-n-brand checked:bg-n-brand dark:border-gray-600 dark:checked:border-n-brand indeterminate:border-n-brand indeterminate:bg-n-brand hover:enabled:bg-n-blue-border cursor-pointer"
@change="handleChange"
/>
<!-- Checkmark SVG -->
<svg
viewBox="0 0 14 14"
fill="none"
class="pointer-events-none absolute w-3.5 h-3.5 z-20 stroke-white opacity-0 peer-checked:opacity-100 transition-opacity duration-200 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
>
<path
d="M3 8L6 11L11 3.5"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<!-- Minus/Indeterminate SVG -->
<svg
viewBox="0 0 14 14"
fill="none"
class="pointer-events-none absolute w-3.5 h-3.5 z-20 stroke-white opacity-0 peer-indeterminate:opacity-100 transition-opacity duration-200 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
>
<path
d="M3 7L11 7"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
</template>
@@ -7,6 +7,7 @@ import CopilotInput from './CopilotInput.vue';
import CopilotLoader from './CopilotLoader.vue';
import CopilotAgentMessage from './CopilotAgentMessage.vue';
import CopilotAssistantMessage from './CopilotAssistantMessage.vue';
import ToggleCopilotAssistant from './ToggleCopilotAssistant.vue';
import Icon from '../icon/Icon.vue';
const props = defineProps({
@@ -26,9 +27,17 @@ const props = defineProps({
type: String,
required: true,
},
assistants: {
type: Array,
default: () => [],
},
activeAssistant: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(['sendMessage', 'reset']);
const emit = defineEmits(['sendMessage', 'reset', 'setAssistant']);
const COPILOT_USER_ROLES = ['assistant', 'system'];
@@ -97,14 +106,18 @@ watch(
<CopilotLoader v-if="isCaptainTyping" />
</div>
<div>
<div v-if="!messages.length" class="flex-1 px-3 py-3 space-y-1">
<div
v-if="!messages.length"
class="h-full w-full flex items-center justify-center"
>
<div class="h-fit px-3 py-3 space-y-1">
<span class="text-xs text-n-slate-10">
{{ $t('COPILOT.TRY_THESE_PROMPTS') }}
</span>
<button
v-for="prompt in promptOptions"
:key="prompt"
:key="prompt.label"
class="px-2 py-1 rounded-md border border-n-weak bg-n-slate-2 text-n-slate-11 flex items-center gap-1"
@click="() => useSuggestion(prompt)"
>
@@ -112,7 +125,17 @@ watch(
<Icon icon="i-lucide-chevron-right" />
</button>
</div>
<div class="mx-3 mt-px mb-2 flex flex-col items-end flex-1">
</div>
<div class="mx-3 mt-px mb-2">
<div class="flex items-center gap-2 justify-between w-full mb-1">
<ToggleCopilotAssistant
v-if="assistants.length"
:assistants="assistants"
:active-assistant="activeAssistant"
@set-assistant="$event => emit('setAssistant', $event)"
/>
<div v-else />
<button
v-if="messages.length"
class="text-xs flex items-center gap-1 hover:underline"
@@ -121,8 +144,8 @@ watch(
<i class="i-lucide-refresh-ccw" />
<span>{{ $t('CAPTAIN.COPILOT.RESET') }}</span>
</button>
<CopilotInput class="mb-1 flex-1 w-full" @send="sendMessage" />
</div>
<CopilotInput class="mb-1 w-full" @send="sendMessage" />
</div>
</div>
</template>
@@ -0,0 +1,81 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import Button from 'dashboard/components-next/button/Button.vue';
import DropdownContainer from 'next/dropdown-menu/base/DropdownContainer.vue';
import DropdownSection from 'next/dropdown-menu/base/DropdownSection.vue';
import DropdownBody from 'next/dropdown-menu/base/DropdownBody.vue';
import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue';
const props = defineProps({
assistants: {
type: Array,
required: true,
},
activeAssistant: {
type: Object,
required: true,
},
});
const emit = defineEmits(['setAssistant']);
const { t } = useI18n();
const activeAssistantLabel = computed(() => {
return props.activeAssistant
? props.activeAssistant.name
: t('CAPTAIN.COPILOT.SELECT_ASSISTANT');
});
</script>
<template>
<div>
<DropdownContainer>
<template #trigger="{ toggle, isOpen }">
<Button
:label="activeAssistantLabel"
icon="i-woot-captain"
ghost
slate
xs
:class="{ 'bg-n-alpha-2': isOpen }"
@click="toggle"
/>
</template>
<DropdownBody class="bottom-9 min-w-64 z-50" strong>
<DropdownSection class="max-h-80 overflow-scroll">
<DropdownItem
v-for="assistant in assistants"
:key="assistant.id"
class="!items-start !gap-1 flex-col cursor-pointer"
@click="() => emit('setAssistant', assistant)"
>
<template #label>
<div class="flex gap-1 justify-between w-full">
<div class="items-start flex gap-1 flex-col">
<span class="text-n-slate-12 text-sm">
{{ assistant.name }}
</span>
<span class="line-clamp-2 text-n-slate-11 text-xs">
{{ assistant.description }}
</span>
</div>
<div
v-if="assistant.id === activeAssistant?.id"
class="flex items-center justify-center flex-shrink-0 w-4 h-4 rounded-full bg-n-slate-12 dark:bg-n-slate-11"
>
<i
class="i-lucide-check text-white dark:text-n-slate-1 size-3"
/>
</div>
</div>
</template>
</DropdownItem>
</DropdownSection>
</DropdownBody>
</DropdownContainer>
</div>
</template>
@@ -19,7 +19,7 @@ const beforeClass = computed(() => {
// Add extra blur layer only when strong prop is true, as a hack for Chrome's stacked backdrop-blur limitation
// https://issues.chromium.org/issues/40835530
return "before:content-['\x00A0'] before:absolute before:bottom-0 before:left-0 before:w-full before:h-full before:backdrop-contrast-70 before:backdrop-blur-sm before:z-0 [&>*]:relative";
return "before:content-['\x00A0'] before:absolute before:bottom-0 before:left-0 before:w-full before:h-full before:rounded-xl before:backdrop-contrast-70 before:backdrop-blur-sm before:z-0 [&>*]:relative";
});
</script>
@@ -2,7 +2,10 @@ import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useOperators } from './operators';
import { useMapGetter } from 'dashboard/composables/store.js';
import { buildAttributesFilterTypes } from './helper/filterHelper.js';
import {
buildAttributesFilterTypes,
CONTACT_ATTRIBUTES,
} from './helper/filterHelper.js';
import countries from 'shared/constants/countries.js';
/**
@@ -59,7 +62,11 @@ export function useContactFilterContext() {
* @type {import('vue').ComputedRef<FilterType[]>}
*/
const customFilterTypes = computed(() =>
buildAttributesFilterTypes(contactAttributes.value, getOperatorTypes)
buildAttributesFilterTypes(
contactAttributes.value,
getOperatorTypes,
'contact'
)
);
/**
@@ -67,8 +74,8 @@ export function useContactFilterContext() {
*/
const filterTypes = computed(() => [
{
attributeKey: 'name',
value: 'name',
attributeKey: CONTACT_ATTRIBUTES.NAME,
value: CONTACT_ATTRIBUTES.NAME,
attributeName: t('CONTACTS_LAYOUT.FILTER.NAME'),
label: t('CONTACTS_LAYOUT.FILTER.NAME'),
inputType: 'plainText',
@@ -77,8 +84,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'email',
value: 'email',
attributeKey: CONTACT_ATTRIBUTES.EMAIL,
value: CONTACT_ATTRIBUTES.EMAIL,
attributeName: t('CONTACTS_LAYOUT.FILTER.EMAIL'),
label: t('CONTACTS_LAYOUT.FILTER.EMAIL'),
inputType: 'plainText',
@@ -87,8 +94,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'phone_number',
value: 'phone_number',
attributeKey: CONTACT_ATTRIBUTES.PHONE_NUMBER,
value: CONTACT_ATTRIBUTES.PHONE_NUMBER,
attributeName: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'),
label: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'),
inputType: 'plainText',
@@ -97,8 +104,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'identifier',
value: 'identifier',
attributeKey: CONTACT_ATTRIBUTES.IDENTIFIER,
value: CONTACT_ATTRIBUTES.IDENTIFIER,
attributeName: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'),
label: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'),
inputType: 'plainText',
@@ -107,8 +114,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'country_code',
value: 'country_code',
attributeKey: CONTACT_ATTRIBUTES.COUNTRY_CODE,
value: CONTACT_ATTRIBUTES.COUNTRY_CODE,
attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
inputType: 'searchSelect',
@@ -118,8 +125,8 @@ export function useContactFilterContext() {
attributeModel: 'additional',
},
{
attributeKey: 'city',
value: 'city',
attributeKey: CONTACT_ATTRIBUTES.CITY,
value: CONTACT_ATTRIBUTES.CITY,
attributeName: t('CONTACTS_LAYOUT.FILTER.CITY'),
label: t('CONTACTS_LAYOUT.FILTER.CITY'),
inputType: 'plainText',
@@ -128,8 +135,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'created_at',
value: 'created_at',
attributeKey: CONTACT_ATTRIBUTES.CREATED_AT,
value: CONTACT_ATTRIBUTES.CREATED_AT,
attributeName: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'),
label: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'),
inputType: 'date',
@@ -138,8 +145,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'last_activity_at',
value: 'last_activity_at',
attributeKey: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT,
value: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT,
attributeName: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'),
label: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'),
inputType: 'date',
@@ -148,8 +155,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'referer',
value: 'referer',
attributeKey: CONTACT_ATTRIBUTES.REFERER,
value: CONTACT_ATTRIBUTES.REFERER,
attributeName: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'),
label: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'),
inputType: 'plainText',
@@ -158,8 +165,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'blocked',
value: 'blocked',
attributeKey: CONTACT_ATTRIBUTES.BLOCKED,
value: CONTACT_ATTRIBUTES.BLOCKED,
attributeName: t('CONTACTS_LAYOUT.FILTER.BLOCKED'),
label: t('CONTACTS_LAYOUT.FILTER.BLOCKED'),
inputType: 'searchSelect',
@@ -1,3 +1,35 @@
/**
* Standard attributes of the conversation model
*/
export const CONVERSATION_ATTRIBUTES = {
STATUS: 'status',
PRIORITY: 'priority',
ASSIGNEE_ID: 'assignee_id',
INBOX_ID: 'inbox_id',
TEAM_ID: 'team_id',
DISPLAY_ID: 'display_id',
CAMPAIGN_ID: 'campaign_id',
LABELS: 'labels',
BROWSER_LANGUAGE: 'browser_language',
COUNTRY_CODE: 'country_code',
REFERER: 'referer',
CREATED_AT: 'created_at',
LAST_ACTIVITY_AT: 'last_activity_at',
};
export const CONTACT_ATTRIBUTES = {
NAME: 'name',
EMAIL: 'email',
PHONE_NUMBER: 'phone_number',
IDENTIFIER: 'identifier',
COUNTRY_CODE: 'country_code',
CITY: 'city',
CREATED_AT: 'created_at',
LAST_ACTIVITY_AT: 'last_activity_at',
REFERER: 'referer',
BLOCKED: 'blocked',
};
/**
* Determines the input type for a custom attribute based on its key
* @param {string} key - The attribute display type key
@@ -20,24 +52,37 @@ export const getCustomAttributeInputType = key => {
/**
* Builds filter types for custom attributes
* This also removes any conflicting attributes
* @param {Array} attributes - The attributes array
* @param {Function} getOperatorTypes - Function to get operator types
* @returns {Array} Array of filter types
*/
export const buildAttributesFilterTypes = (attributes, getOperatorTypes) => {
return attributes.map(attr => ({
attributeKey: attr.attributeKey,
value: attr.attributeKey,
attributeName: attr.attributeDisplayName,
label: attr.attributeDisplayName,
inputType: getCustomAttributeInputType(attr.attributeDisplayType),
filterOperators: getOperatorTypes(attr.attributeDisplayType),
options:
attr.attributeDisplayType === 'list'
? attr.attributeValues.map(item => ({ id: item, name: item }))
: [],
attributeModel: 'customAttributes',
}));
export const buildAttributesFilterTypes = (
attributes,
getOperatorTypes,
filterModel = 'conversation'
) => {
const standardAttributes = Object.values(
filterModel === 'conversation'
? CONVERSATION_ATTRIBUTES
: CONTACT_ATTRIBUTES
);
return attributes
.filter(attr => !standardAttributes.includes(attr.attributeKey))
.map(attr => ({
attributeKey: attr.attributeKey,
value: attr.attributeKey,
attributeName: attr.attributeDisplayName,
label: attr.attributeDisplayName,
inputType: getCustomAttributeInputType(attr.attributeDisplayType),
filterOperators: getOperatorTypes(attr.attributeDisplayType),
options:
attr.attributeDisplayType === 'list'
? attr.attributeValues.map(item => ({ id: item, name: item }))
: [],
attributeModel: 'customAttributes',
}));
};
/**
@@ -3,7 +3,10 @@ import { useI18n } from 'vue-i18n';
import { useOperators } from './operators';
import { useMapGetter } from 'dashboard/composables/store.js';
import { useChannelIcon } from 'next/icon/provider';
import { buildAttributesFilterTypes } from './helper/filterHelper';
import {
buildAttributesFilterTypes,
CONVERSATION_ATTRIBUTES,
} from './helper/filterHelper';
import countries from 'shared/constants/countries.js';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
@@ -70,7 +73,11 @@ export function useConversationFilterContext() {
* @type {import('vue').ComputedRef<FilterType[]>}
*/
const customFilterTypes = computed(() =>
buildAttributesFilterTypes(conversationAttributes.value, getOperatorTypes)
buildAttributesFilterTypes(
conversationAttributes.value,
getOperatorTypes,
'conversation'
)
);
/**
@@ -78,8 +85,8 @@ export function useConversationFilterContext() {
*/
const filterTypes = computed(() => [
{
attributeKey: 'status',
value: 'status',
attributeKey: CONVERSATION_ATTRIBUTES.STATUS,
value: CONVERSATION_ATTRIBUTES.STATUS,
attributeName: t('FILTER.ATTRIBUTES.STATUS'),
label: t('FILTER.ATTRIBUTES.STATUS'),
inputType: 'multiSelect',
@@ -94,8 +101,24 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'assignee_id',
value: 'assignee_id',
attributeKey: CONVERSATION_ATTRIBUTES.PRIORITY,
value: CONVERSATION_ATTRIBUTES.PRIORITY,
attributeName: t('FILTER.ATTRIBUTES.PRIORITY'),
label: t('FILTER.ATTRIBUTES.PRIORITY'),
inputType: 'multiSelect',
options: ['low', 'medium', 'high', 'urgent'].map(id => {
return {
id,
name: t(`CONVERSATION.PRIORITY.OPTIONS.${id.toUpperCase()}`),
};
}),
dataType: 'text',
filterOperators: equalityOperators.value,
attributeModel: 'standard',
},
{
attributeKey: CONVERSATION_ATTRIBUTES.ASSIGNEE_ID,
value: CONVERSATION_ATTRIBUTES.ASSIGNEE_ID,
attributeName: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'),
label: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'),
inputType: 'searchSelect',
@@ -110,8 +133,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'inbox_id',
value: 'inbox_id',
attributeKey: CONVERSATION_ATTRIBUTES.INBOX_ID,
value: CONVERSATION_ATTRIBUTES.INBOX_ID,
attributeName: t('FILTER.ATTRIBUTES.INBOX_NAME'),
label: t('FILTER.ATTRIBUTES.INBOX_NAME'),
inputType: 'searchSelect',
@@ -126,8 +149,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'team_id',
value: 'team_id',
attributeKey: CONVERSATION_ATTRIBUTES.TEAM_ID,
value: CONVERSATION_ATTRIBUTES.TEAM_ID,
attributeName: t('FILTER.ATTRIBUTES.TEAM_NAME'),
label: t('FILTER.ATTRIBUTES.TEAM_NAME'),
inputType: 'searchSelect',
@@ -137,8 +160,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'display_id',
value: 'display_id',
attributeKey: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
value: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
attributeName: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'),
label: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'),
inputType: 'plainText',
@@ -147,8 +170,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'campaign_id',
value: 'campaign_id',
attributeKey: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID,
value: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID,
attributeName: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'),
label: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'),
inputType: 'searchSelect',
@@ -161,8 +184,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'labels',
value: 'labels',
attributeKey: CONVERSATION_ATTRIBUTES.LABELS,
value: CONVERSATION_ATTRIBUTES.LABELS,
attributeName: t('FILTER.ATTRIBUTES.LABELS'),
label: t('FILTER.ATTRIBUTES.LABELS'),
inputType: 'multiSelect',
@@ -185,8 +208,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'browser_language',
value: 'browser_language',
attributeKey: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE,
value: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE,
attributeName: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'),
label: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'),
inputType: 'searchSelect',
@@ -196,8 +219,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional',
},
{
attributeKey: 'country_code',
value: 'country_code',
attributeKey: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
value: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
inputType: 'searchSelect',
@@ -207,8 +230,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional',
},
{
attributeKey: 'referer',
value: 'referer',
attributeKey: CONVERSATION_ATTRIBUTES.REFERER,
value: CONVERSATION_ATTRIBUTES.REFERER,
attributeName: t('FILTER.ATTRIBUTES.REFERER_LINK'),
label: t('FILTER.ATTRIBUTES.REFERER_LINK'),
inputType: 'plainText',
@@ -217,8 +240,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional',
},
{
attributeKey: 'created_at',
value: 'created_at',
attributeKey: CONVERSATION_ATTRIBUTES.CREATED_AT,
value: CONVERSATION_ATTRIBUTES.CREATED_AT,
attributeName: t('FILTER.ATTRIBUTES.CREATED_AT'),
label: t('FILTER.ATTRIBUTES.CREATED_AT'),
inputType: 'date',
@@ -227,8 +250,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'last_activity_at',
value: 'last_activity_at',
attributeKey: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT,
value: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT,
attributeName: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'),
label: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'),
inputType: 'date',
@@ -26,7 +26,7 @@ const { t } = useI18n();
/>
</div>
<div
class="absolute bg-n-alpha-3 px-4 py-3 border rounded-xl border-n-strong text-n-slate-12 bottom-6 w-52 text-xs backdrop-blur-[100px] shadow-[0px_0px_24px_0px_rgba(0,0,0,0.12)] opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all"
class="absolute bg-n-alpha-3 px-4 py-3 border rounded-xl border-n-strong text-n-slate-12 bottom-6 w-52 text-xs backdrop-blur-[100px] shadow-[0px_0px_24px_0px_rgba(0,0,0,0.12)] opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all break-all"
:class="{
'ltr:left-0 rtl:right-0': orientation === ORIENTATION.LEFT,
'ltr:right-0 rtl:left-0': orientation === ORIENTATION.RIGHT,
@@ -102,6 +102,7 @@ const statusToShow = computed(() => {
if (isRead.value) return MESSAGE_STATUS.READ;
if (isDelivered.value) return MESSAGE_STATUS.DELIVERED;
if (isSent.value) return MESSAGE_STATUS.SENT;
if (status.value === MESSAGE_STATUS.FAILED) return MESSAGE_STATUS.FAILED;
return MESSAGE_STATUS.PROGRESS;
});
@@ -63,7 +63,7 @@ const pageInfo = computed(() => {
<template>
<div
class="flex justify-between h-12 w-full max-w-[957px] outline outline-n-container outline-1 -outline-offset-1 mx-auto bg-n-solid-2 rounded-xl py-2 ltr:pl-4 rtl:pr-4 ltr:pr-3 rtl:pl-3 items-center before:absolute before:inset-x-0 before:-top-4 before:bg-gradient-to-t before:from-n-background before:from-10% before:dark:from-0% before:to-transparent before:h-4 before:pointer-events-none"
class="flex justify-between h-12 w-full max-w-[calc(60rem-3px)] outline outline-n-container outline-1 -outline-offset-1 mx-auto bg-n-solid-2 rounded-xl py-2 ltr:pl-4 rtl:pr-4 ltr:pr-3 rtl:pl-3 items-center before:absolute before:inset-x-0 before:-top-4 before:bg-gradient-to-t before:from-n-background before:from-10% before:dark:from-0% before:to-transparent before:h-4 before:pointer-events-none"
>
<div class="flex items-center gap-3">
<span class="min-w-0 text-sm font-normal line-clamp-1 text-n-slate-11">
@@ -185,7 +185,7 @@ watch(
"
trailing-icon
:disabled="disabled"
class="!h-[30px] top-1 !px-2 outline-0 !outline-none !rounded-lg border-0 ltr:!rounded-r-none rtl:!rounded-l-none"
class="!h-[1.875rem] top-1 !px-2 outline-0 !outline-none !rounded-lg border-0 ltr:!rounded-r-none rtl:!rounded-l-none"
@click="toggleCountryDropdown"
>
<span
@@ -515,7 +515,7 @@ const menuItems = computed(() => {
<template>
<aside
class="w-[200px] bg-n-solid-2 rtl:border-l ltr:border-r border-n-weak h-screen flex flex-col text-sm pb-1"
class="w-[12.5rem] bg-n-solid-2 rtl:border-l ltr:border-r border-n-weak h-screen flex flex-col text-sm pb-1"
>
<section class="grid gap-2 mt-2 mb-4">
<div class="flex items-center min-w-0 gap-2 px-2">
@@ -31,7 +31,7 @@ const shouldRenderComponent = computed(() => {
:is="to ? 'router-link' : 'div'"
:to="to"
:title="label"
class="flex h-8 items-center gap-2 px-2 py-1 rounded-lg max-w-[151px] hover:bg-gradient-to-r from-transparent via-n-slate-3/70 to-n-slate-3/70 group"
class="flex h-8 items-center gap-2 px-2 py-1 rounded-lg max-w-[9.438rem] hover:bg-gradient-to-r from-transparent via-n-slate-3/70 to-n-slate-3/70 group"
:class="{
'n-blue-text bg-n-alpha-2 active': active,
}"
@@ -27,7 +27,7 @@ const updateValue = () => {
>
<span class="sr-only">{{ t('SWITCH.TOGGLE') }}</span>
<span
class="absolute top-px left-0.5 h-3 w-3 transform rounded-full shadow-sm transition-transform duration-200 ease-in-out"
class="absolute top-[0.07rem] left-0.5 h-3 w-3 transform rounded-full shadow-sm transition-transform duration-200 ease-in-out"
:class="
modelValue
? 'translate-x-2.5 bg-white'
@@ -353,10 +353,11 @@ function setFiltersFromUISettings() {
const { conversations_filter_by: filterBy = {} } = uiSettings.value;
const { status, order_by: orderBy } = filterBy;
activeStatus.value = status || wootConstants.STATUS_TYPE.OPEN;
activeSortBy.value =
Object.keys(wootConstants.SORT_BY_TYPE).find(
sortField => sortField === orderBy
) || wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC;
activeSortBy.value = Object.values(wootConstants.SORT_BY_TYPE).includes(
orderBy
)
? orderBy
: wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC;
}
function emitConversationLoaded() {
@@ -23,33 +23,34 @@ export default {
<template>
<div
class="ml-0 mr-0 flex py-8 w-full xl:w-3/4 flex-col xl:flex-row"
class="ml-0 mr-0 py-8 w-full"
:class="{
'border-b border-solid border-slate-50 dark:border-slate-700/30':
showBorder,
'border-b border-solid border-n-weak/60 dark:border-n-weak': showBorder,
}"
>
<div class="w-full xl:w-1/4 min-w-0 xl:max-w-[30%] pr-12">
<p
v-if="title"
class="text-base text-woot-500 dark:text-woot-500 mb-0 font-medium"
>
{{ title }}
</p>
<p
class="text-sm mb-2 text-slate-700 dark:text-slate-300 leading-5 tracking-normal mt-2"
>
<slot v-if="subTitle" name="subTitle">
{{ subTitle }}
</slot>
</p>
<p v-if="note">
<span class="font-semibold">{{ $t('INBOX_MGMT.NOTE') }}</span>
{{ note }}
</p>
</div>
<div class="w-full xl:w-1/2 min-w-0 xl:max-w-[50%]">
<slot />
<div class="grid grid-cols-1 lg:grid-cols-6 gap-6">
<div class="col-span-2 xl:col-span-1">
<p
v-if="title"
class="text-base text-woot-500 dark:text-woot-500 mb-0 font-medium"
>
{{ title }}
</p>
<p
class="text-sm mb-2 text-slate-700 dark:text-slate-300 leading-5 tracking-normal mt-2"
>
<slot v-if="subTitle" name="subTitle">
{{ subTitle }}
</slot>
</p>
<p v-if="note">
<span class="font-semibold">{{ $t('INBOX_MGMT.NOTE') }}</span>
{{ note }}
</p>
</div>
<div class="col-span-4 lg:col-span-4 2xl:col-span-3">
<slot />
</div>
</div>
</div>
</template>
@@ -1,8 +1,11 @@
<script setup>
import { ref, computed, onMounted, watchEffect } from 'vue';
import { useStore } from 'dashboard/composables/store';
import Copilot from 'dashboard/components-next/copilot/Copilot.vue';
import ConversationAPI from 'dashboard/api/inbox/conversation';
import { useMapGetter } from 'dashboard/composables/store';
import { ref } from 'vue';
import { useUISettings } from 'dashboard/composables/useUISettings';
const props = defineProps({
conversationId: {
type: [Number, String],
@@ -13,10 +16,44 @@ const props = defineProps({
required: true,
},
});
const currentUser = useMapGetter('getCurrentUser');
const messages = ref([]);
const store = useStore();
const currentUser = useMapGetter('getCurrentUser');
const assistants = useMapGetter('captainAssistants/getRecords');
const inboxAssistant = useMapGetter('getCopilotAssistant');
const { uiSettings, updateUISettings } = useUISettings();
const messages = ref([]);
const isCaptainTyping = ref(false);
const selectedAssistantId = ref(null);
const activeAssistant = computed(() => {
const preferredId = uiSettings.value.preferred_captain_assistant_id;
// If the user has selected a specific assistant, it takes first preference for Copilot.
if (preferredId) {
const preferredAssistant = assistants.value.find(a => a.id === preferredId);
// Return the preferred assistant if found, otherwise continue to next cases
if (preferredAssistant) return preferredAssistant;
}
// If the above is not available, the assistant connected to the inbox takes preference.
if (inboxAssistant.value) {
const inboxMatchedAssistant = assistants.value.find(
a => a.id === inboxAssistant.value.id
);
if (inboxMatchedAssistant) return inboxMatchedAssistant;
}
// If neither of the above is available, the first assistant in the account takes preference.
return assistants.value[0];
});
const setAssistant = async assistant => {
selectedAssistantId.value = assistant.id;
await updateUISettings({
preferred_captain_assistant_id: assistant.id,
});
};
const handleReset = () => {
messages.value = [];
@@ -42,7 +79,7 @@ const sendMessage = async message => {
}))
.slice(0, -1),
message,
assistant_id: 16,
assistant_id: selectedAssistantId.value,
}
);
messages.value.push({
@@ -57,6 +94,17 @@ const sendMessage = async message => {
isCaptainTyping.value = false;
}
};
onMounted(() => {
store.dispatch('captainAssistants/get');
});
watchEffect(() => {
if (props.conversationId) {
store.dispatch('getInboxCaptainAssistantById', props.conversationId);
selectedAssistantId.value = activeAssistant.value?.id;
}
});
</script>
<template>
@@ -65,6 +113,9 @@ const sendMessage = async message => {
:support-agent="currentUser"
:is-captain-typing="isCaptainTyping"
:conversation-inbox-type="conversationInboxType"
:assistants="assistants"
:active-assistant="activeAssistant"
@set-assistant="setAssistant"
@send-message="sendMessage"
@reset="handleReset"
/>
@@ -23,17 +23,16 @@ export default {
<template>
<div
class="flex flex-col min-w-[15rem] max-h-[21.25rem] max-w-[23.75rem] rounded-md border border-solid border-slate-75 dark:border-slate-600"
class="flex flex-col min-w-[15rem] max-h-[21.25rem] max-w-[23.75rem] rounded-md border border-solid border-n-strong"
:class="{
'bg-woot-25 dark:bg-slate-700 border border-solid border-woot-300 dark:border-woot-400':
'bg-woot-25 dark:bg-n-solid-2 border border-solid border-n-blue-border':
active,
}"
>
<div
class="flex justify-between items-center px-2 w-full h-10 bg-slate-50 dark:bg-slate-900 rounded-t-[5px] border-b border-solid border-slate-50 dark:border-slate-600"
class="flex justify-between items-center rounded-t-md px-2 w-full h-10 bg-slate-50 dark:bg-slate-900 border-b border-solid border-n-strong"
:class="{
'bg-woot-50 border-b border-solid border-woot-75 dark:border-woot-700':
active,
'bg-woot-50 border-b border-solid border-n-blue-border': active,
}"
>
<div class="flex items-center p-1 text-sm font-medium">{{ heading }}</div>
@@ -38,20 +38,20 @@ export default {
cursor: pointer;
display: flex;
flex-shrink: 0;
height: 19px;
height: 1.188rem;
position: relative;
transition-duration: 200ms;
transition-property: background-color;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
width: 34px;
width: 2.125rem;
&.active {
background-color: var(--w-500);
}
&.small {
width: 22px;
height: 14px;
width: 1.375rem;
height: 0.875rem;
span {
height: var(--space-one);
@@ -1,6 +1,6 @@
<script setup>
import { ref, provide, onMounted, computed } from 'vue';
import { useEventListener } from '@vueuse/core';
import { ref, useTemplateRef, provide, computed, watch } from 'vue';
import { useElementSize } from '@vueuse/core';
const props = defineProps({
index: {
@@ -19,6 +19,12 @@ const props = defineProps({
const emit = defineEmits(['change']);
const tabsContainer = useTemplateRef('tabsContainer');
const tabsList = useTemplateRef('tabsList');
const { width: containerWidth } = useElementSize(tabsContainer);
const { width: listWidth } = useElementSize(tabsList);
const hasScroll = ref(false);
const activeIndex = computed({
@@ -34,20 +40,16 @@ provide('updateActiveIndex', index => {
});
const computeScrollWidth = () => {
// TODO: use useElementSize from vueuse
const tabElement = document.querySelector('.tabs');
if (tabElement) {
hasScroll.value = tabElement.scrollWidth > tabElement.clientWidth;
if (tabsContainer.value && tabsList.value) {
hasScroll.value = tabsList.value.scrollWidth > tabsList.value.clientWidth;
}
};
const onScrollClick = direction => {
// TODO: use useElementSize from vueuse
const tabElement = document.querySelector('.tabs');
if (tabElement) {
let scrollPosition = tabElement.scrollLeft;
if (tabsContainer.value && tabsList.value) {
let scrollPosition = tabsList.value.scrollLeft;
scrollPosition += direction === 'left' ? -100 : 100;
tabElement.scrollTo({
tabsList.value.scrollTo({
top: 0,
left: scrollPosition,
behavior: 'smooth',
@@ -55,14 +57,19 @@ const onScrollClick = direction => {
}
};
useEventListener(window, 'resize', computeScrollWidth);
onMounted(() => {
computeScrollWidth();
});
// Watch for changes in element sizes with immediate execution
watch(
[containerWidth, listWidth],
() => {
computeScrollWidth();
},
{ immediate: true }
);
</script>
<template>
<div
ref="tabsContainer"
:class="{
'tabs--container--with-border': border,
'tabs--container--compact': isCompact,
@@ -76,7 +83,7 @@ onMounted(() => {
>
<fluent-icon icon="chevron-left" :size="16" />
</button>
<ul :class="{ 'tabs--with-scroll': hasScroll }" class="tabs">
<ul ref="tabsList" :class="{ 'tabs--with-scroll': hasScroll }" class="tabs">
<slot />
</ul>
<button
@@ -72,26 +72,26 @@ export default {
&.active {
h3 {
@apply text-woot-500 dark:text-woot-500;
@apply text-n-blue-text dark:text-n-blue-text;
}
.step {
@apply bg-woot-500 dark:bg-woot-500;
@apply bg-n-brand dark:bg-n-brand;
}
}
&.over {
&::after {
@apply bg-woot-500 dark:bg-woot-500;
@apply bg-n-brand dark:bg-n-brand;
}
.step {
@apply bg-woot-500 dark:bg-woot-500;
@apply bg-n-brand dark:bg-n-brand;
}
& + .item {
&::before {
@apply bg-woot-500 dark:bg-woot-500;
@apply bg-n-brand dark:bg-n-brand;
}
}
}
@@ -201,10 +201,10 @@ export default {
<style lang="scss" scoped>
.filter {
@apply bg-slate-50 dark:bg-slate-800 p-2 border border-solid border-slate-75 dark:border-slate-600 rounded-md mb-2;
@apply bg-n-slate-3 dark:bg-n-solid-3 p-2 border border-solid border-n-strong dark:border-n-strong rounded-md mb-2;
&.is-a-macro {
@apply mb-0 bg-white dark:bg-slate-700 p-0 border-0 rounded-none;
@apply mb-0 bg-n-slate-2 dark:bg-n-solid-3 p-0 border-0 rounded-none;
}
}
@@ -128,7 +128,7 @@ export default {
getInputErrorClass(errorMessage) {
return errorMessage
? 'bg-red-50 dark:bg-red-800/50 border-red-100 dark:border-red-700/50'
: 'bg-slate-50 dark:bg-slate-800 border-slate-75 dark:border-slate-700/50';
: 'bg-n-slate-3 dark:bg-n-solid-3 border-n-strong dark:border-n-strong';
},
},
};
@@ -14,7 +14,7 @@ export default {
</script>
<template>
<div class="pt-4 pb-0 px-8 border-b border-solid border-n-weak">
<div class="pt-4 pb-0 px-8 border-b border-solid border-n-weak/60">
<h2 class="text-2xl text-slate-800 dark:text-slate-100 mb-1 font-medium">
{{ headerTitle }}
</h2>
@@ -85,7 +85,7 @@ export default {
</script>
<template>
<div class="flex justify-between h-[52px] gap-2 ltr:pl-3 rtl:pr-3">
<div class="flex justify-between h-[3.25rem] gap-2 ltr:pl-3 rtl:pr-3">
<EditorModeToggle
:mode="mode"
class="mt-3"
@@ -411,7 +411,7 @@ export default {
}
.checkbox-wrapper {
@apply h-10 w-10 flex items-center justify-center rounded-full cursor-pointer mt-4;
@apply flex items-center justify-center rounded-full cursor-pointer mt-4;
input[type='checkbox'] {
@apply m-0 cursor-pointer;
@@ -53,7 +53,7 @@ const showCopilotTab = computed(() =>
<template>
<div
class="ltr:border-l rtl:border-r border-n-weak h-full overflow-hidden z-10 min-w-[320px] w-[320px] 2xl:min-w-96 2xl:w-96 flex flex-col bg-n-background"
class="ltr:border-l rtl:border-r border-n-weak h-full overflow-hidden z-10 w-80 min-w-80 2xl:min-w-96 2xl:w-96 flex flex-col bg-n-background"
>
<div v-if="showCopilotTab" class="p-2">
<TabBar
@@ -2,11 +2,13 @@
import { reactive, computed, onMounted, ref } from 'vue';
import { useVuelidate } from '@vuelidate/core';
import { useI18n } from 'vue-i18n';
import { useTrack } from 'dashboard/composables';
import { useAlert } from 'dashboard/composables';
import LinearAPI from 'dashboard/api/integrations/linear';
import validations from './validations';
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
import SearchableDropdown from './SearchableDropdown.vue';
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
const props = defineProps({
conversationId: {
@@ -45,7 +47,7 @@ const statusDesiredOrder = [
];
const isCreating = ref(false);
const inputStyles = { borderRadius: '12px', fontSize: '14px' };
const inputStyles = { borderRadius: '0.75rem', fontSize: '0.875rem' };
const formState = reactive({
title: '',
@@ -188,6 +190,7 @@ const createIssue = async () => {
const { id: issueId } = response.data;
await LinearAPI.link_issue(props.conversationId, issueId, props.title);
useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_SUCCESS'));
useTrack(LINEAR_EVENTS.CREATE_ISSUE);
onClose();
} catch (error) {
const errorMessage = parseLinearAPIErrorResponse(
@@ -209,7 +212,7 @@ onMounted(getTeams);
v-model="formState.title"
:class="{ error: v$.title.$error }"
class="w-full"
:styles="{ ...inputStyles, padding: '6px 12px' }"
:styles="{ ...inputStyles, padding: '0.375rem 0.75rem' }"
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.TITLE.LABEL')"
:placeholder="
$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.TITLE.PLACEHOLDER')
@@ -221,7 +224,7 @@ onMounted(getTeams);
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.DESCRIPTION.LABEL') }}
<textarea
v-model="formState.description"
:style="{ ...inputStyles, padding: '8px 12px' }"
:style="{ ...inputStyles, padding: '0.5rem 0.75rem' }"
rows="3"
class="text-sm"
:placeholder="
@@ -1,11 +1,13 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useTrack } from 'dashboard/composables';
import { useAlert } from 'dashboard/composables';
import LinearAPI from 'dashboard/api/integrations/linear';
import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue';
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
const props = defineProps({
conversationId: {
@@ -85,6 +87,7 @@ const linkIssue = async () => {
searchQuery.value = '';
issues.value = [];
onClose();
useTrack(LINEAR_EVENTS.LINK_ISSUE);
} catch (error) {
const errorMessage = parseLinearAPIErrorResponse(
error,
@@ -6,6 +6,8 @@ import { useI18n } from 'vue-i18n';
import LinearAPI from 'dashboard/api/integrations/linear';
import CreateOrLinkIssue from './CreateOrLinkIssue.vue';
import Issue from './Issue.vue';
import { useTrack } from 'dashboard/composables';
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
const props = defineProps({
@@ -56,6 +58,7 @@ const unlinkIssue = async linkId => {
try {
isUnlinking.value = true;
await LinearAPI.unlinkIssue(linkId);
useTrack(LINEAR_EVENTS.UNLINK_ISSUE);
linkedIssue.value = null;
useAlert(t('INTEGRATION_SETTINGS.LINEAR.UNLINK.SUCCESS'));
} catch (error) {
@@ -0,0 +1,175 @@
import { ref } from 'vue';
import { useFontSize } from '../useFontSize';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
// Mock dependencies
vi.mock('dashboard/composables/useUISettings');
vi.mock('dashboard/composables', () => ({
useAlert: vi.fn(message => message),
}));
vi.mock('vue-i18n');
// Mock requestAnimationFrame
global.requestAnimationFrame = vi.fn(cb => cb());
describe('useFontSize', () => {
const mockUISettings = ref({
font_size: '16px',
});
const mockUpdateUISettings = vi.fn().mockResolvedValue(undefined);
const mockTranslate = vi.fn(key => key);
beforeEach(() => {
vi.clearAllMocks();
// Setup mocks
useUISettings.mockReturnValue({
uiSettings: mockUISettings,
updateUISettings: mockUpdateUISettings,
});
useI18n.mockReturnValue({
t: mockTranslate,
});
// Reset DOM state
document.documentElement.style.removeProperty('font-size');
// Reset mockUISettings to default
mockUISettings.value = { font_size: '16px' };
});
it('returns fontSizeOptions with correct structure', () => {
const { fontSizeOptions } = useFontSize();
expect(fontSizeOptions).toHaveLength(6);
expect(fontSizeOptions[0]).toHaveProperty('value');
expect(fontSizeOptions[0]).toHaveProperty('label');
// Check specific options
expect(fontSizeOptions.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({
value: '14px',
label:
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.SMALLER',
});
expect(fontSizeOptions.find(option => option.value === '22px')).toEqual({
value: '22px',
label:
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.EXTRA_LARGE',
});
});
it('returns currentFontSize from UI settings', () => {
const { currentFontSize } = useFontSize();
expect(currentFontSize.value).toBe('16px');
mockUISettings.value.font_size = '18px';
expect(currentFontSize.value).toBe('18px');
});
it('applies font size to document root correctly based on pixel values', () => {
const { applyFontSize } = useFontSize();
applyFontSize('18px');
expect(document.documentElement.style.fontSize).toBe('18px');
applyFontSize('14px');
expect(document.documentElement.style.fontSize).toBe('14px');
applyFontSize('22px');
expect(document.documentElement.style.fontSize).toBe('22px');
applyFontSize('16px');
expect(document.documentElement.style.fontSize).toBe('16px');
});
it('updates UI settings and applies font size', async () => {
const { updateFontSize } = useFontSize();
await updateFontSize('20px');
expect(mockUpdateUISettings).toHaveBeenCalledWith({ font_size: '20px' });
expect(document.documentElement.style.fontSize).toBe('20px');
expect(useAlert).toHaveBeenCalledWith(
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.UPDATE_SUCCESS'
);
});
it('shows error alert when update fails', async () => {
mockUpdateUISettings.mockRejectedValueOnce(new Error('Update failed'));
const { updateFontSize } = useFontSize();
await updateFontSize('20px');
expect(useAlert).toHaveBeenCalledWith(
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.UPDATE_ERROR'
);
});
it('handles unknown font size values gracefully', () => {
const { applyFontSize } = useFontSize();
// Should not throw an error and should apply the default font size
applyFontSize('unknown-size');
expect(document.documentElement.style.fontSize).toBe('16px');
});
it('watches for UI settings changes and applies font size', async () => {
useFontSize();
// Initial font size should now be 16px instead of empty
expect(document.documentElement.style.fontSize).toBe('16px');
// Update UI settings
mockUISettings.value = { font_size: '18px' };
// Wait for next tick to let watchers fire
await Promise.resolve();
expect(document.documentElement.style.fontSize).toBe('18px');
});
it('translates font size option labels correctly', () => {
// Set up specific translation mapping
mockTranslate.mockImplementation(key => {
const translations = {
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.SMALLER':
'Smaller',
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.DEFAULT':
'Default',
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.EXTRA_LARGE':
'Extra Large',
};
return translations[key] || key;
});
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.find(option => option.value === '22px').label).toBe(
'Extra Large'
);
// Verify translation function was called with correct keys
expect(mockTranslate).toHaveBeenCalledWith(
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.SMALLER'
);
expect(mockTranslate).toHaveBeenCalledWith(
'PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.DEFAULT'
);
});
});
@@ -0,0 +1,140 @@
/**
* @file useFontSize.js
* @description A composable for managing font size settings throughout the application.
* This handles font size selection, application to the DOM, and persistence in user settings.
*/
import { computed, watch } from 'vue';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
/**
* Font size options with their pixel values
* @type {Object}
*/
const FONT_SIZE_OPTIONS = {
SMALLER: '14px',
SMALL: '15px',
DEFAULT: '16px',
LARGE: '18px',
LARGER: '20px',
EXTRA_LARGE: '22px',
};
/**
* Array of font size option keys
* @type {Array<string>}
*/
const FONT_SIZE_NAMES = Object.keys(FONT_SIZE_OPTIONS);
/**
* Get font size label translation key
*
* @param {string} name - Font size name
* @returns {string} Translation key
*/
const getFontSizeLabelKey = name =>
`PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.OPTIONS.${name}`;
/**
* Create font size option object
*
* @param {Function} t - Translation function
* @param {string} name - Font size name
* @returns {Object} Font size option with value and label
*/
const createFontSizeOption = (t, name) => ({
value: FONT_SIZE_OPTIONS[name],
label: t(getFontSizeLabelKey(name)),
});
/**
* Apply font size value to document root
*
* @param {string} pixelValue - Font size value in pixels
*/
const applyFontSizeToDOM = pixelValue => {
document.documentElement.style.setProperty(
'font-size',
pixelValue ?? FONT_SIZE_OPTIONS.DEFAULT
);
};
/**
* Font size management composable
*
* @returns {Object} Font size utilities and state
* @property {Array} fontSizeOptions - Array of font size options for select components
* @property {import('vue').ComputedRef<string>} currentFontSize - Current font size from UI settings
* @property {Function} applyFontSize - Function to apply font size to document
* @property {Function} updateFontSize - Function to update font size in settings with alert feedback
*/
export const useFontSize = () => {
const { uiSettings, updateUISettings } = useUISettings();
const { t } = useI18n();
/**
* Font size options for select dropdown
* @type {Array<{value: string, label: string}>}
*/
const fontSizeOptions = FONT_SIZE_NAMES.map(name =>
createFontSizeOption(t, name)
);
/**
* Current font size from UI settings
* @type {import('vue').ComputedRef<string>}
*/
const currentFontSize = computed(
() => uiSettings.value.font_size || FONT_SIZE_OPTIONS.DEFAULT
);
/**
* Apply font size to document root
* @param {string} pixelValue - Font size in pixels (e.g., '16px')
* @returns {void}
*/
const applyFontSize = pixelValue => {
// Use requestAnimationFrame for better performance
requestAnimationFrame(() => applyFontSizeToDOM(pixelValue));
};
/**
* Update font size in settings and apply to document
* Shows success/error alerts
* @param {string} pixelValue - Font size in pixels (e.g., '16px')
* @returns {Promise<void>}
*/
const updateFontSize = async pixelValue => {
try {
await updateUISettings({ font_size: pixelValue });
applyFontSize(pixelValue);
useAlert(
t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.UPDATE_SUCCESS')
);
} catch (error) {
useAlert(
t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.UPDATE_ERROR')
);
}
};
// Watch for changes to the font size in UI settings
watch(
() => uiSettings.value.font_size,
newSize => {
applyFontSize(newSize);
},
{ immediate: true }
);
return {
fontSizeOptions,
currentFontSize,
applyFontSize,
updateFontSize,
};
};
export default useFontSize;
@@ -124,3 +124,9 @@ export const SLA_EVENTS = Object.freeze({
UPDATE: 'Updated an SLA',
DELETED: 'Deleted an SLA',
});
export const LINEAR_EVENTS = Object.freeze({
CREATE_ISSUE: 'Created a linear issue',
LINK_ISSUE: 'Linked a linear issue',
UNLINK_ISSUE: 'Unlinked a linear issue',
});
@@ -9,9 +9,9 @@ export const setColorTheme = isOSOnDarkMode => {
selectedColorScheme === 'dark'
) {
document.body.classList.add('dark');
document.documentElement.setAttribute('style', 'color-scheme: dark;');
document.documentElement.style.setProperty('color-scheme', 'dark');
} else {
document.body.classList.remove('dark');
document.documentElement.setAttribute('style', 'color-scheme: light;');
document.documentElement.style.setProperty('color-scheme', 'light');
}
};
@@ -266,7 +266,7 @@
"ATTRIBUTE_WARNING": "Contact details of <strong>{primaryContactName}</strong> will be copied to <strong>{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
"ERROR_MESSAGE": "Something went wrong. Please try again later."
},
"FORM": {
"SUBMIT": " Merge contacts",
@@ -288,6 +288,8 @@
"SEARCH_PLACEHOLDER": "Search...",
"MESSAGE_BUTTON": "Message",
"SEND_MESSAGE": "Send message",
"BLOCK_CONTACT": "Block contact",
"UNBLOCK_CONTACT": "Unblock contact",
"BREADCRUMB": {
"CONTACTS": "Contacts"
},
@@ -302,6 +304,10 @@
"SUCCESS_MESSAGE": "Contact saved successfully",
"ERROR_MESSAGE": "Unable to save contact. Please try again later."
},
"BLOCK_SUCCESS_MESSAGE": "This contact is blocked successfully",
"BLOCK_ERROR_MESSAGE": "Unable to block contact. Please try again later.",
"UNBLOCK_SUCCESS_MESSAGE": "This contact is unblocked successfully",
"UNBLOCK_ERROR_MESSAGE": "Unable to unblock contact. Please try again later.",
"IMPORT_CONTACT": {
"TITLE": "Import contacts",
"DESCRIPTION": "Import contacts through a CSV file.",
@@ -117,6 +117,7 @@
"PENDING": "Mark as pending",
"RESOLVED": "Mark as resolved",
"MARK_AS_UNREAD": "Mark as unread",
"MARK_AS_READ": "Mark as read",
"REOPEN": "Reopen conversation",
"SNOOZE": {
"TITLE": "Snooze",
@@ -300,6 +300,12 @@
"TITLE": "Unlink",
"SUCCESS": "Issue unlinked successfully",
"ERROR": "There was an error unlinking the issue, please try again"
},
"DELETE": {
"TITLE": "Are you sure you want to delete the integration?",
"MESSAGE": "Are you sure you want to delete the integration?",
"CONFIRM": "Yes, delete",
"CANCEL": "Cancel"
}
}
},
@@ -310,7 +316,8 @@
"LOADER": "Captain is thinking",
"YOU": "You",
"USE": "Use this",
"RESET": "Reset"
"RESET": "Reset",
"SELECT_ASSISTANT": "Select Assistant"
},
"PAYWALL": {
"TITLE": "Upgrade to use Captain AI",
@@ -433,6 +440,20 @@
"DOCUMENTABLE": {
"CONVERSATION": "Conversation #{id}"
},
"SELECTED": "{count} selected",
"BULK_APPROVE_BUTTON": "Approve",
"BULK_DELETE_BUTTON": "Delete",
"BULK_APPROVE": {
"SUCCESS_MESSAGE": "FAQs approved successfully",
"ERROR_MESSAGE": "There was an error approving the FAQs, please try again."
},
"BULK_DELETE": {
"TITLE": "Delete FAQs?",
"DESCRIPTION": "Are you sure you want to delete the selected FAQs? This action cannot be undone.",
"CONFIRM": "Yes, delete all",
"SUCCESS_MESSAGE": "FAQs deleted successfully",
"ERROR_MESSAGE": "There was an error deleting the FAQs, please try again."
},
"DELETE": {
"TITLE": "Are you sure to delete the FAQ?",
"DESCRIPTION": "",
@@ -35,6 +35,24 @@
}
}
},
"INTERFACE_SECTION": {
"TITLE": "Interface",
"NOTE": "Customize the look and feel of your Chatwoot dashboard.",
"FONT_SIZE": {
"TITLE": "Font size",
"NOTE": "Adjust the text size across the dashboard based on your preference.",
"UPDATE_SUCCESS": "Your font settings have been updated successfully",
"UPDATE_ERROR": "There is an error while updating the font settings, please try again",
"OPTIONS": {
"SMALLER": "Smaller",
"SMALL": "Small",
"DEFAULT": "Default",
"LARGE": "Large",
"LARGER": "Larger",
"EXTRA_LARGE": "Extra Large"
}
}
},
"MESSAGE_SIGNATURE_SECTION": {
"TITLE": "Personal message signature",
"NOTE": "Create a unique message signature to appear at the end of every message you send from any inbox. You can also include an inline image, which is supported in live-chat, email, and API inboxes.",
@@ -389,11 +407,6 @@
"SWITCH_TO_PRIVATE_NOTE": "Switch to Private Note",
"SWITCH_TO_REPLY": "Switch to Reply",
"TOGGLE_SNOOZE_DROPDOWN": "Toggle snooze dropdown"
},
"KEYS": {
"WINDOWS_KEY_AND_COMMAND_KEY": "Win / ⌘",
"ALT_OR_OPTION_KEY": "Alt / ⌥",
"FORWARD_SLASH_KEY": "/"
}
}
}
@@ -266,7 +266,7 @@
"ATTRIBUTE_WARNING": "سيتم نسخ تفاصيل الاتصال من <strong>{primaryContactName}</strong> إلى <strong>{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "رسالة_خطأ"
"ERROR_MESSAGE": "Something went wrong. Please try again later."
},
"FORM": {
"SUBMIT": " دمج جهات الاتصال",
@@ -288,6 +288,8 @@
"SEARCH_PLACEHOLDER": "Search...",
"MESSAGE_BUTTON": "رسالة",
"SEND_MESSAGE": "إرسال الرسالة",
"BLOCK_CONTACT": "Block contact",
"UNBLOCK_CONTACT": "Unblock contact",
"BREADCRUMB": {
"CONTACTS": "جهات الاتصال"
},
@@ -302,6 +304,10 @@
"SUCCESS_MESSAGE": "تم حفظ جهة الاتصال بنجاح",
"ERROR_MESSAGE": "Unable to save contact. Please try again later."
},
"BLOCK_SUCCESS_MESSAGE": "This contact is blocked successfully",
"BLOCK_ERROR_MESSAGE": "Unable to block contact. Please try again later.",
"UNBLOCK_SUCCESS_MESSAGE": "تم إلغاء حجب جهة الاتصال بنجاح",
"UNBLOCK_ERROR_MESSAGE": "Unable to unblock contact. Please try again later.",
"IMPORT_CONTACT": {
"TITLE": "Import contacts",
"DESCRIPTION": "استيراد جهات الاتصال من خلال ملف CSV.",
@@ -117,6 +117,7 @@
"PENDING": "تحديد كمعلق",
"RESOLVED": "تحديد كمحلولة",
"MARK_AS_UNREAD": "وضع علامة كغير مقروء",
"MARK_AS_READ": "تحديد كمقروء",
"REOPEN": "إعادة فتح المحادثة",
"SNOOZE": {
"TITLE": "تأجيل",
@@ -300,6 +300,12 @@
"TITLE": "إلغاء الربط",
"SUCCESS": "تم إلغاء ربط المشكلة بنجاح",
"ERROR": "حدث خطأ أثناء إلغاء ربط المشكلة، الرجاء المحاولة مرة أخرى"
},
"DELETE": {
"TITLE": "Are you sure you want to delete the integration?",
"MESSAGE": "Are you sure you want to delete the integration?",
"CONFIRM": "نعم، احذف",
"CANCEL": "إلغاء"
}
}
},
@@ -310,7 +316,8 @@
"LOADER": "Captain is thinking",
"YOU": "أنت",
"USE": "Use this",
"RESET": "Reset"
"RESET": "Reset",
"SELECT_ASSISTANT": "Select Assistant"
},
"PAYWALL": {
"TITLE": "Upgrade to use Captain AI",
@@ -433,6 +440,20 @@
"DOCUMENTABLE": {
"CONVERSATION": "Conversation #{id}"
},
"SELECTED": "{count} selected",
"BULK_APPROVE_BUTTON": "Approve",
"BULK_DELETE_BUTTON": "حذف",
"BULK_APPROVE": {
"SUCCESS_MESSAGE": "FAQs approved successfully",
"ERROR_MESSAGE": "There was an error approving the FAQs, please try again."
},
"BULK_DELETE": {
"TITLE": "Delete FAQs?",
"DESCRIPTION": "Are you sure you want to delete the selected FAQs? This action cannot be undone.",
"CONFIRM": "Yes, delete all",
"SUCCESS_MESSAGE": "FAQs deleted successfully",
"ERROR_MESSAGE": "There was an error deleting the FAQs, please try again."
},
"DELETE": {
"TITLE": "Are you sure to delete the FAQ?",
"DESCRIPTION": "",
@@ -35,6 +35,24 @@
}
}
},
"INTERFACE_SECTION": {
"TITLE": "Interface",
"NOTE": "Customize the look and feel of your Chatwoot dashboard.",
"FONT_SIZE": {
"TITLE": "Font size",
"NOTE": "Adjust the text size across the dashboard based on your preference.",
"UPDATE_SUCCESS": "Your font settings have been updated successfully",
"UPDATE_ERROR": "There is an error while updating the font settings, please try again",
"OPTIONS": {
"SMALLER": "Smaller",
"SMALL": "Small",
"DEFAULT": "افتراضي",
"LARGE": "Large",
"LARGER": "Larger",
"EXTRA_LARGE": "Extra Large"
}
}
},
"MESSAGE_SIGNATURE_SECTION": {
"TITLE": "توقيع الرسالة الشخصية",
"NOTE": "إنشاء توقيع رسالة فريدة تظهر في نهاية كل رسالة ترسلها من أي صندوق وارد. يمكنك أيضًا تضمين صورة داخلية، مدعومة في الدردشة المباشرة، والبريد الإلكتروني، وصناديق API الواردة.",
@@ -389,11 +407,6 @@
"SWITCH_TO_PRIVATE_NOTE": "التبديل إلى الملاحظة الخاصة",
"SWITCH_TO_REPLY": "التبديل إلى الرد",
"TOGGLE_SNOOZE_DROPDOWN": "تبديل القائمة المنسدلة"
},
"KEYS": {
"WINDOWS_KEY_AND_COMMAND_KEY": "Win / ⌘\n",
"ALT_OR_OPTION_KEY": "Alt / ⌥",
"FORWARD_SLASH_KEY": "/"
}
}
}
@@ -266,7 +266,7 @@
"ATTRIBUTE_WARNING": "Contact details of <strong>{primaryContactName}</strong> will be copied to <strong>{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
"ERROR_MESSAGE": "Something went wrong. Please try again later."
},
"FORM": {
"SUBMIT": " Merge contacts",
@@ -288,6 +288,8 @@
"SEARCH_PLACEHOLDER": "Search...",
"MESSAGE_BUTTON": "Message",
"SEND_MESSAGE": "Send message",
"BLOCK_CONTACT": "Block contact",
"UNBLOCK_CONTACT": "Unblock contact",
"BREADCRUMB": {
"CONTACTS": "Contacts"
},
@@ -302,6 +304,10 @@
"SUCCESS_MESSAGE": "Contact saved successfully",
"ERROR_MESSAGE": "Unable to save contact. Please try again later."
},
"BLOCK_SUCCESS_MESSAGE": "This contact is blocked successfully",
"BLOCK_ERROR_MESSAGE": "Unable to block contact. Please try again later.",
"UNBLOCK_SUCCESS_MESSAGE": "This contact is unblocked successfully",
"UNBLOCK_ERROR_MESSAGE": "Unable to unblock contact. Please try again later.",
"IMPORT_CONTACT": {
"TITLE": "Import contacts",
"DESCRIPTION": "Import contacts through a CSV file.",
@@ -117,6 +117,7 @@
"PENDING": "Mark as pending",
"RESOLVED": "Mark as resolved",
"MARK_AS_UNREAD": "Mark as unread",
"MARK_AS_READ": "Mark as read",
"REOPEN": "Reopen conversation",
"SNOOZE": {
"TITLE": "Snooze",
@@ -300,6 +300,12 @@
"TITLE": "Unlink",
"SUCCESS": "Issue unlinked successfully",
"ERROR": "There was an error unlinking the issue, please try again"
},
"DELETE": {
"TITLE": "Are you sure you want to delete the integration?",
"MESSAGE": "Are you sure you want to delete the integration?",
"CONFIRM": "Yes, delete",
"CANCEL": "Cancel"
}
}
},
@@ -310,7 +316,8 @@
"LOADER": "Captain is thinking",
"YOU": "You",
"USE": "Use this",
"RESET": "Reset"
"RESET": "Reset",
"SELECT_ASSISTANT": "Select Assistant"
},
"PAYWALL": {
"TITLE": "Upgrade to use Captain AI",
@@ -433,6 +440,20 @@
"DOCUMENTABLE": {
"CONVERSATION": "Conversation #{id}"
},
"SELECTED": "{count} selected",
"BULK_APPROVE_BUTTON": "Approve",
"BULK_DELETE_BUTTON": "Delete",
"BULK_APPROVE": {
"SUCCESS_MESSAGE": "FAQs approved successfully",
"ERROR_MESSAGE": "There was an error approving the FAQs, please try again."
},
"BULK_DELETE": {
"TITLE": "Delete FAQs?",
"DESCRIPTION": "Are you sure you want to delete the selected FAQs? This action cannot be undone.",
"CONFIRM": "Yes, delete all",
"SUCCESS_MESSAGE": "FAQs deleted successfully",
"ERROR_MESSAGE": "There was an error deleting the FAQs, please try again."
},
"DELETE": {
"TITLE": "Are you sure to delete the FAQ?",
"DESCRIPTION": "",
@@ -35,6 +35,24 @@
}
}
},
"INTERFACE_SECTION": {
"TITLE": "Interface",
"NOTE": "Customize the look and feel of your Chatwoot dashboard.",
"FONT_SIZE": {
"TITLE": "Font size",
"NOTE": "Adjust the text size across the dashboard based on your preference.",
"UPDATE_SUCCESS": "Your font settings have been updated successfully",
"UPDATE_ERROR": "There is an error while updating the font settings, please try again",
"OPTIONS": {
"SMALLER": "Smaller",
"SMALL": "Small",
"DEFAULT": "Default",
"LARGE": "Large",
"LARGER": "Larger",
"EXTRA_LARGE": "Extra Large"
}
}
},
"MESSAGE_SIGNATURE_SECTION": {
"TITLE": "Personal message signature",
"NOTE": "Create a unique message signature to appear at the end of every message you send from any inbox. You can also include an inline image, which is supported in live-chat, email, and API inboxes.",
@@ -389,11 +407,6 @@
"SWITCH_TO_PRIVATE_NOTE": "Switch to Private Note",
"SWITCH_TO_REPLY": "Switch to Reply",
"TOGGLE_SNOOZE_DROPDOWN": "Toggle snooze dropdown"
},
"KEYS": {
"WINDOWS_KEY_AND_COMMAND_KEY": "Win / ⌘",
"ALT_OR_OPTION_KEY": "Alt / ⌥",
"FORWARD_SLASH_KEY": "/"
}
}
}
@@ -266,7 +266,7 @@
"ATTRIBUTE_WARNING": "Детайлите на контакт <strong>{primaryContactName}</strong> ще бъдат копирани в <strong>{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ГРЕШКА"
"ERROR_MESSAGE": "Something went wrong. Please try again later."
},
"FORM": {
"SUBMIT": " Обединяване на контакти",
@@ -288,6 +288,8 @@
"SEARCH_PLACEHOLDER": "Search...",
"MESSAGE_BUTTON": "Съобщение",
"SEND_MESSAGE": "Изпрати съобщение",
"BLOCK_CONTACT": "Block contact",
"UNBLOCK_CONTACT": "Unblock contact",
"BREADCRUMB": {
"CONTACTS": "Контакти"
},
@@ -302,6 +304,10 @@
"SUCCESS_MESSAGE": "Успешно запазване на контакта",
"ERROR_MESSAGE": "Unable to save contact. Please try again later."
},
"BLOCK_SUCCESS_MESSAGE": "This contact is blocked successfully",
"BLOCK_ERROR_MESSAGE": "Unable to block contact. Please try again later.",
"UNBLOCK_SUCCESS_MESSAGE": "This contact is unblocked successfully",
"UNBLOCK_ERROR_MESSAGE": "Unable to unblock contact. Please try again later.",
"IMPORT_CONTACT": {
"TITLE": "Import contacts",
"DESCRIPTION": "Внасяне на контакти чрез CSV файл.",
@@ -117,6 +117,7 @@
"PENDING": "Mark as pending",
"RESOLVED": "Mark as resolved",
"MARK_AS_UNREAD": "Mark as unread",
"MARK_AS_READ": "Mark as read",
"REOPEN": "Reopen conversation",
"SNOOZE": {
"TITLE": "Snooze",
@@ -300,6 +300,12 @@
"TITLE": "Unlink",
"SUCCESS": "Issue unlinked successfully",
"ERROR": "There was an error unlinking the issue, please try again"
},
"DELETE": {
"TITLE": "Are you sure you want to delete the integration?",
"MESSAGE": "Are you sure you want to delete the integration?",
"CONFIRM": "Yes, delete",
"CANCEL": "Отмени"
}
}
},
@@ -310,7 +316,8 @@
"LOADER": "Captain is thinking",
"YOU": "You",
"USE": "Use this",
"RESET": "Reset"
"RESET": "Reset",
"SELECT_ASSISTANT": "Select Assistant"
},
"PAYWALL": {
"TITLE": "Upgrade to use Captain AI",
@@ -433,6 +440,20 @@
"DOCUMENTABLE": {
"CONVERSATION": "Conversation #{id}"
},
"SELECTED": "{count} selected",
"BULK_APPROVE_BUTTON": "Approve",
"BULK_DELETE_BUTTON": "Изтрий",
"BULK_APPROVE": {
"SUCCESS_MESSAGE": "FAQs approved successfully",
"ERROR_MESSAGE": "There was an error approving the FAQs, please try again."
},
"BULK_DELETE": {
"TITLE": "Delete FAQs?",
"DESCRIPTION": "Are you sure you want to delete the selected FAQs? This action cannot be undone.",
"CONFIRM": "Yes, delete all",
"SUCCESS_MESSAGE": "FAQs deleted successfully",
"ERROR_MESSAGE": "There was an error deleting the FAQs, please try again."
},
"DELETE": {
"TITLE": "Are you sure to delete the FAQ?",
"DESCRIPTION": "",
@@ -35,6 +35,24 @@
}
}
},
"INTERFACE_SECTION": {
"TITLE": "Interface",
"NOTE": "Customize the look and feel of your Chatwoot dashboard.",
"FONT_SIZE": {
"TITLE": "Font size",
"NOTE": "Adjust the text size across the dashboard based on your preference.",
"UPDATE_SUCCESS": "Your font settings have been updated successfully",
"UPDATE_ERROR": "There is an error while updating the font settings, please try again",
"OPTIONS": {
"SMALLER": "Smaller",
"SMALL": "Small",
"DEFAULT": "Default",
"LARGE": "Large",
"LARGER": "Larger",
"EXTRA_LARGE": "Extra Large"
}
}
},
"MESSAGE_SIGNATURE_SECTION": {
"TITLE": "Personal message signature",
"NOTE": "Create a unique message signature to appear at the end of every message you send from any inbox. You can also include an inline image, which is supported in live-chat, email, and API inboxes.",
@@ -389,11 +407,6 @@
"SWITCH_TO_PRIVATE_NOTE": "Switch to Private Note",
"SWITCH_TO_REPLY": "Switch to Reply",
"TOGGLE_SNOOZE_DROPDOWN": "Toggle snooze dropdown"
},
"KEYS": {
"WINDOWS_KEY_AND_COMMAND_KEY": "Win / ⌘",
"ALT_OR_OPTION_KEY": "Alt / ⌥",
"FORWARD_SLASH_KEY": "/"
}
}
}
@@ -266,7 +266,7 @@
"ATTRIBUTE_WARNING": "Les dades de contacte de <strong>{primaryContactName}</strong> es copiaran a <strong>{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
"ERROR_MESSAGE": "Something went wrong. Please try again later."
},
"FORM": {
"SUBMIT": " Fusiona contactes",
@@ -288,6 +288,8 @@
"SEARCH_PLACEHOLDER": "Search...",
"MESSAGE_BUTTON": "Missatge",
"SEND_MESSAGE": "Envia missatge",
"BLOCK_CONTACT": "Block contact",
"UNBLOCK_CONTACT": "Unblock contact",
"BREADCRUMB": {
"CONTACTS": "Contactes"
},
@@ -302,6 +304,10 @@
"SUCCESS_MESSAGE": "Contacte guardat correctament",
"ERROR_MESSAGE": "Unable to save contact. Please try again later."
},
"BLOCK_SUCCESS_MESSAGE": "This contact is blocked successfully",
"BLOCK_ERROR_MESSAGE": "Unable to block contact. Please try again later.",
"UNBLOCK_SUCCESS_MESSAGE": "Aquest contacte s'ha desbloquejat correctament",
"UNBLOCK_ERROR_MESSAGE": "Unable to unblock contact. Please try again later.",
"IMPORT_CONTACT": {
"TITLE": "Import contacts",
"DESCRIPTION": "Importa contactes a través d'un fitxer CSV.",
@@ -117,6 +117,7 @@
"PENDING": "Marca com a pendent",
"RESOLVED": "Marca com a resolt",
"MARK_AS_UNREAD": "Marca com a no llegit",
"MARK_AS_READ": "Marca com a llegit",
"REOPEN": "Torna a obrir la conversa",
"SNOOZE": {
"TITLE": "Posposat",
@@ -300,6 +300,12 @@
"TITLE": "Desenllaça",
"SUCCESS": "S'ha desenllaçat la issue correctament",
"ERROR": "S'ha produït un error en desenllaçar la issue, torna-ho a provar"
},
"DELETE": {
"TITLE": "Are you sure you want to delete the integration?",
"MESSAGE": "Are you sure you want to delete the integration?",
"CONFIRM": "Sí, esborra",
"CANCEL": "Cancel·la"
}
}
},
@@ -310,7 +316,8 @@
"LOADER": "Captain is thinking",
"YOU": "Tu",
"USE": "Use this",
"RESET": "Reset"
"RESET": "Reset",
"SELECT_ASSISTANT": "Select Assistant"
},
"PAYWALL": {
"TITLE": "Upgrade to use Captain AI",
@@ -433,6 +440,20 @@
"DOCUMENTABLE": {
"CONVERSATION": "Conversation #{id}"
},
"SELECTED": "{count} selected",
"BULK_APPROVE_BUTTON": "Approve",
"BULK_DELETE_BUTTON": "Esborrar",
"BULK_APPROVE": {
"SUCCESS_MESSAGE": "FAQs approved successfully",
"ERROR_MESSAGE": "There was an error approving the FAQs, please try again."
},
"BULK_DELETE": {
"TITLE": "Delete FAQs?",
"DESCRIPTION": "Are you sure you want to delete the selected FAQs? This action cannot be undone.",
"CONFIRM": "Yes, delete all",
"SUCCESS_MESSAGE": "FAQs deleted successfully",
"ERROR_MESSAGE": "There was an error deleting the FAQs, please try again."
},
"DELETE": {
"TITLE": "Are you sure to delete the FAQ?",
"DESCRIPTION": "",
@@ -35,6 +35,24 @@
}
}
},
"INTERFACE_SECTION": {
"TITLE": "Interface",
"NOTE": "Customize the look and feel of your Chatwoot dashboard.",
"FONT_SIZE": {
"TITLE": "Font size",
"NOTE": "Adjust the text size across the dashboard based on your preference.",
"UPDATE_SUCCESS": "Your font settings have been updated successfully",
"UPDATE_ERROR": "There is an error while updating the font settings, please try again",
"OPTIONS": {
"SMALLER": "Smaller",
"SMALL": "Small",
"DEFAULT": "Per defecte",
"LARGE": "Large",
"LARGER": "Larger",
"EXTRA_LARGE": "Extra Large"
}
}
},
"MESSAGE_SIGNATURE_SECTION": {
"TITLE": "Signatura personal del missatge",
"NOTE": "Crea una signatura de missatge única per aparèixer al final de cada missatge que envieu des de qualsevol safata d'entrada. També pots incloure una imatge en línia, que és compatible amb el xat en directe, el correu electrònic i les bústies d'entrada de l'API.",
@@ -389,11 +407,6 @@
"SWITCH_TO_PRIVATE_NOTE": "Canvia a la nota privada",
"SWITCH_TO_REPLY": "Canvia a Respon",
"TOGGLE_SNOOZE_DROPDOWN": "Commuta el menú desplegable de posposar"
},
"KEYS": {
"WINDOWS_KEY_AND_COMMAND_KEY": "Win / ⌘",
"ALT_OR_OPTION_KEY": "Alt / ⌥",
"FORWARD_SLASH_KEY": "/"
}
}
}
@@ -266,7 +266,7 @@
"ATTRIBUTE_WARNING": "Contact details of <strong>{primaryContactName}</strong> will be copied to <strong>{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
"ERROR_MESSAGE": "Something went wrong. Please try again later."
},
"FORM": {
"SUBMIT": " Merge contacts",
@@ -288,6 +288,8 @@
"SEARCH_PLACEHOLDER": "Search...",
"MESSAGE_BUTTON": "Zpráva",
"SEND_MESSAGE": "Send message",
"BLOCK_CONTACT": "Block contact",
"UNBLOCK_CONTACT": "Unblock contact",
"BREADCRUMB": {
"CONTACTS": "Kontakty"
},
@@ -302,6 +304,10 @@
"SUCCESS_MESSAGE": "Kontakt byl úspěšně uložen",
"ERROR_MESSAGE": "Unable to save contact. Please try again later."
},
"BLOCK_SUCCESS_MESSAGE": "This contact is blocked successfully",
"BLOCK_ERROR_MESSAGE": "Unable to block contact. Please try again later.",
"UNBLOCK_SUCCESS_MESSAGE": "This contact is unblocked successfully",
"UNBLOCK_ERROR_MESSAGE": "Unable to unblock contact. Please try again later.",
"IMPORT_CONTACT": {
"TITLE": "Import contacts",
"DESCRIPTION": "Import contacts through a CSV file.",
@@ -117,6 +117,7 @@
"PENDING": "Označit jako nevyřízené",
"RESOLVED": "Označit jako vyřešené",
"MARK_AS_UNREAD": "Mark as unread",
"MARK_AS_READ": "Mark as read",
"REOPEN": "Znovu otevřít konverzaci",
"SNOOZE": {
"TITLE": "Odložit",
@@ -300,6 +300,12 @@
"TITLE": "Unlink",
"SUCCESS": "Issue unlinked successfully",
"ERROR": "There was an error unlinking the issue, please try again"
},
"DELETE": {
"TITLE": "Are you sure you want to delete the integration?",
"MESSAGE": "Are you sure you want to delete the integration?",
"CONFIRM": "Yes, delete",
"CANCEL": "Zrušit"
}
}
},
@@ -310,7 +316,8 @@
"LOADER": "Captain is thinking",
"YOU": "Vy",
"USE": "Use this",
"RESET": "Reset"
"RESET": "Reset",
"SELECT_ASSISTANT": "Select Assistant"
},
"PAYWALL": {
"TITLE": "Upgrade to use Captain AI",
@@ -433,6 +440,20 @@
"DOCUMENTABLE": {
"CONVERSATION": "Conversation #{id}"
},
"SELECTED": "{count} selected",
"BULK_APPROVE_BUTTON": "Approve",
"BULK_DELETE_BUTTON": "Vymazat",
"BULK_APPROVE": {
"SUCCESS_MESSAGE": "FAQs approved successfully",
"ERROR_MESSAGE": "There was an error approving the FAQs, please try again."
},
"BULK_DELETE": {
"TITLE": "Delete FAQs?",
"DESCRIPTION": "Are you sure you want to delete the selected FAQs? This action cannot be undone.",
"CONFIRM": "Yes, delete all",
"SUCCESS_MESSAGE": "FAQs deleted successfully",
"ERROR_MESSAGE": "There was an error deleting the FAQs, please try again."
},
"DELETE": {
"TITLE": "Are you sure to delete the FAQ?",
"DESCRIPTION": "",
@@ -35,6 +35,24 @@
}
}
},
"INTERFACE_SECTION": {
"TITLE": "Interface",
"NOTE": "Customize the look and feel of your Chatwoot dashboard.",
"FONT_SIZE": {
"TITLE": "Font size",
"NOTE": "Adjust the text size across the dashboard based on your preference.",
"UPDATE_SUCCESS": "Your font settings have been updated successfully",
"UPDATE_ERROR": "There is an error while updating the font settings, please try again",
"OPTIONS": {
"SMALLER": "Smaller",
"SMALL": "Small",
"DEFAULT": "Default",
"LARGE": "Large",
"LARGER": "Larger",
"EXTRA_LARGE": "Extra Large"
}
}
},
"MESSAGE_SIGNATURE_SECTION": {
"TITLE": "Personal message signature",
"NOTE": "Create a unique message signature to appear at the end of every message you send from any inbox. You can also include an inline image, which is supported in live-chat, email, and API inboxes.",
@@ -389,11 +407,6 @@
"SWITCH_TO_PRIVATE_NOTE": "Switch to Private Note",
"SWITCH_TO_REPLY": "Switch to Reply",
"TOGGLE_SNOOZE_DROPDOWN": "Toggle snooze dropdown"
},
"KEYS": {
"WINDOWS_KEY_AND_COMMAND_KEY": "Win / ⌘",
"ALT_OR_OPTION_KEY": "Alt / ⌥",
"FORWARD_SLASH_KEY": "/"
}
}
}
@@ -35,7 +35,7 @@
},
"API": {
"SUCCESS_MESSAGE": "Canned response added successfully.",
"ERROR_MESSAGE": "Kunne ikke oprette forbindelse til Woot Server, Prøv igen senere."
"ERROR_MESSAGE": "Kunne ikke oprette forbindelse til Woot server, Prøv igen senere."
}
},
"EDIT": {
@@ -9,7 +9,7 @@
"FAILED_TO_SEND": "Failed to send",
"TAB_HEADING": "Samtaler",
"MENTION_HEADING": "Omtaler",
"UNATTENDED_HEADING": "Unattet",
"UNATTENDED_HEADING": "Ubehandlet",
"SEARCH": {
"INPUT": "Søg efter Mennesker, Chats, Gemte svar .."
},
@@ -266,7 +266,7 @@
"ATTRIBUTE_WARNING": "Kontaktoplysninger på <strong>{primaryContactName}</strong> vil blive kopieret til <strong>{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "FEJL_MEDDELELSE"
"ERROR_MESSAGE": "Something went wrong. Please try again later."
},
"FORM": {
"SUBMIT": " Sammenflet kontakter",
@@ -288,6 +288,8 @@
"SEARCH_PLACEHOLDER": "Search...",
"MESSAGE_BUTTON": "Besked",
"SEND_MESSAGE": "Send besked",
"BLOCK_CONTACT": "Block contact",
"UNBLOCK_CONTACT": "Unblock contact",
"BREADCRUMB": {
"CONTACTS": "Kontakter"
},
@@ -302,6 +304,10 @@
"SUCCESS_MESSAGE": "Kontakt gemt med succes",
"ERROR_MESSAGE": "Unable to save contact. Please try again later."
},
"BLOCK_SUCCESS_MESSAGE": "This contact is blocked successfully",
"BLOCK_ERROR_MESSAGE": "Unable to block contact. Please try again later.",
"UNBLOCK_SUCCESS_MESSAGE": "This contact is unblocked successfully",
"UNBLOCK_ERROR_MESSAGE": "Unable to unblock contact. Please try again later.",
"IMPORT_CONTACT": {
"TITLE": "Import contacts",
"DESCRIPTION": "Importér kontakter via en CSV-fil.",
@@ -117,6 +117,7 @@
"PENDING": "Markér som afventende",
"RESOLVED": "Marker som løst",
"MARK_AS_UNREAD": "Marker som ulæst",
"MARK_AS_READ": "Mark as read",
"REOPEN": "Genåbn samtale",
"SNOOZE": {
"TITLE": "Udsæt",
@@ -300,6 +300,12 @@
"TITLE": "Unlink",
"SUCCESS": "Issue unlinked successfully",
"ERROR": "There was an error unlinking the issue, please try again"
},
"DELETE": {
"TITLE": "Are you sure you want to delete the integration?",
"MESSAGE": "Are you sure you want to delete the integration?",
"CONFIRM": "Yes, delete",
"CANCEL": "Annuller"
}
}
},
@@ -310,7 +316,8 @@
"LOADER": "Captain is thinking",
"YOU": "Dig",
"USE": "Use this",
"RESET": "Reset"
"RESET": "Reset",
"SELECT_ASSISTANT": "Select Assistant"
},
"PAYWALL": {
"TITLE": "Upgrade to use Captain AI",
@@ -433,6 +440,20 @@
"DOCUMENTABLE": {
"CONVERSATION": "Conversation #{id}"
},
"SELECTED": "{count} selected",
"BULK_APPROVE_BUTTON": "Approve",
"BULK_DELETE_BUTTON": "Slet",
"BULK_APPROVE": {
"SUCCESS_MESSAGE": "FAQs approved successfully",
"ERROR_MESSAGE": "There was an error approving the FAQs, please try again."
},
"BULK_DELETE": {
"TITLE": "Delete FAQs?",
"DESCRIPTION": "Are you sure you want to delete the selected FAQs? This action cannot be undone.",
"CONFIRM": "Yes, delete all",
"SUCCESS_MESSAGE": "FAQs deleted successfully",
"ERROR_MESSAGE": "There was an error deleting the FAQs, please try again."
},
"DELETE": {
"TITLE": "Are you sure to delete the FAQ?",
"DESCRIPTION": "",
@@ -35,6 +35,24 @@
}
}
},
"INTERFACE_SECTION": {
"TITLE": "Interface",
"NOTE": "Customize the look and feel of your Chatwoot dashboard.",
"FONT_SIZE": {
"TITLE": "Font size",
"NOTE": "Adjust the text size across the dashboard based on your preference.",
"UPDATE_SUCCESS": "Your font settings have been updated successfully",
"UPDATE_ERROR": "There is an error while updating the font settings, please try again",
"OPTIONS": {
"SMALLER": "Smaller",
"SMALL": "Small",
"DEFAULT": "Standard",
"LARGE": "Large",
"LARGER": "Larger",
"EXTRA_LARGE": "Extra Large"
}
}
},
"MESSAGE_SIGNATURE_SECTION": {
"TITLE": "Personlig beskedsignatur",
"NOTE": "Create a unique message signature to appear at the end of every message you send from any inbox. You can also include an inline image, which is supported in live-chat, email, and API inboxes.",
@@ -389,11 +407,6 @@
"SWITCH_TO_PRIVATE_NOTE": "Skift til privat note",
"SWITCH_TO_REPLY": "Skift til svar",
"TOGGLE_SNOOZE_DROPDOWN": "Skift snooze dropdown"
},
"KEYS": {
"WINDOWS_KEY_AND_COMMAND_KEY": "Vind / ¤",
"ALT_OR_OPTION_KEY": "Alt. / ¤",
"FORWARD_SLASH_KEY": "/"
}
}
}
@@ -19,7 +19,7 @@
"ACTIONS": "Aktionen",
"VERIFIED": "Verifiziert",
"VERIFICATION_PENDING": "Überprüfung ausstehend",
"AVAILABLE_CUSTOM_ROLE": "Available custom role permissions"
"AVAILABLE_CUSTOM_ROLE": "Verfügbare Berechtigungen der benutzerdefinierten Rolle"
},
"ADD": {
"TITLE": "Fügen Sie Ihrem Team einen Agenten hinzu",
@@ -78,7 +78,7 @@
},
"AGENT_AVAILABILITY": {
"LABEL": "Verfügbarkeit",
"PLACEHOLDER": "Bitte wählen Sie den Online Status",
"PLACEHOLDER": "Bitte wählen Sie Ihre Verfügbarkeit aus",
"ERROR": "Verfügbarkeit ist erforderlich"
},
"SUBMIT": "Agent bearbeiten"
@@ -1,7 +1,7 @@
{
"AUTOMATION": {
"HEADER": "Automatisierung",
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
"DESCRIPTION": "Mittels Automatisierung können bestehende Prozesse ersetzt und rationalisiert werden, die manuellen Aufwand erfordern, z. B. das Hinzufügen von Etiketten und die Zuweisung von Gesprächen an den am besten geeigneten Agenten. So kann sich das Team auf seine Stärken konzentrieren und gleichzeitig den Zeitaufwand für Routineaufgaben reduzieren.",
"LEARN_MORE": "Learn more about automation",
"HEADER_BTN_TXT": "Automatisierungsregel hinzufügen",
"LOADING": "Automatisierungsregeln abrufen",
@@ -6,7 +6,7 @@
"LIST": {
"404": "In dieser Gruppe existieren keine aktiven Gespräche."
},
"FAILED_TO_SEND": "Failed to send",
"FAILED_TO_SEND": "Fehler beim Senden",
"TAB_HEADING": "Gespräche",
"MENTION_HEADING": "Erwähnungen",
"UNATTENDED_HEADING": "Unbeaufsichtigt",
@@ -101,7 +101,7 @@
"CONTENT": "hat eine URL geteilt"
},
"contact": {
"CONTENT": "Shared contact"
"CONTENT": "Geteilter Kontakt"
}
},
"CHAT_SORT_BY_FILTER": {
@@ -134,6 +134,6 @@
"HIDE_QUOTED_TEXT": "Zitierten Text ausblenden",
"SHOW_QUOTED_TEXT": "Zitierten Text anzeigen",
"MESSAGE_READ": "Lesen",
"SENDING": "Sending"
"SENDING": "Sende"
}
}
@@ -266,7 +266,7 @@
"ATTRIBUTE_WARNING": "Details von Kontakt <strong>{primaryContactName}</strong> wird zu <strong>{parentContactName}</strong> kopiert."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
"ERROR_MESSAGE": "Etwas ist schiefgelaufen. Bitte später erneut versuchen."
},
"FORM": {
"SUBMIT": " Kontakte zusammenführen",
@@ -284,30 +284,36 @@
"CONTACTS_LAYOUT": {
"HEADER": {
"TITLE": "Kontakte",
"SEARCH_TITLE": "Search contacts",
"SEARCH_PLACEHOLDER": "Search...",
"SEARCH_TITLE": "Kontakte suchen",
"SEARCH_PLACEHOLDER": "Suchen...",
"MESSAGE_BUTTON": "Nachricht",
"SEND_MESSAGE": "Nachricht senden",
"BLOCK_CONTACT": "Kontakt blockieren",
"UNBLOCK_CONTACT": "Kontakt entsperren",
"BREADCRUMB": {
"CONTACTS": "Kontakte"
},
"ACTIONS": {
"CONTACT_CREATION": {
"ADD_CONTACT": "Add contact",
"EXPORT_CONTACT": "Export contacts",
"IMPORT_CONTACT": "Import contacts",
"SAVE_CONTACT": "Save contact",
"ADD_CONTACT": "Kontakt hinzufügen",
"EXPORT_CONTACT": "Kontakte exportieren",
"IMPORT_CONTACT": "Kontakte importieren",
"SAVE_CONTACT": "Kontakt speichern",
"EMAIL_ADDRESS_DUPLICATE": "Diese E-Mail-Adresse wird bereits für einen anderen Kontakt verwendet.",
"PHONE_NUMBER_DUPLICATE": "Diese Telefonnummer wird für einen anderen Kontakt verwendet.",
"SUCCESS_MESSAGE": "Kontakt erfolgreich gespeichert",
"ERROR_MESSAGE": "Unable to save contact. Please try again later."
"ERROR_MESSAGE": "Kontakt konnte nicht gespeichert werden. Bitte versuchen Sie es später erneut."
},
"BLOCK_SUCCESS_MESSAGE": "Dieser Kontakt wurde erfolgreich blockiert",
"BLOCK_ERROR_MESSAGE": "Kontakt konnte nicht blockiert werden. Bitte versuchen Sie es später erneut.",
"UNBLOCK_SUCCESS_MESSAGE": "Dieser Kontakt wurde erfolgreich entsperrt",
"UNBLOCK_ERROR_MESSAGE": "Kontakt konnte nicht entsperrt werden. Bitte versuchen Sie es später erneut.",
"IMPORT_CONTACT": {
"TITLE": "Import contacts",
"TITLE": "Kontakte importieren",
"DESCRIPTION": "Kontakte über CSV-Datei importieren.",
"DOWNLOAD_LABEL": "Ein CSV-Beispiel herunterladen.",
"LABEL": "CSV-Datei:",
"CHOOSE_FILE": "Choose file",
"CHOOSE_FILE": "Datei auswählen",
"CHANGE": "Ändern",
"CANCEL": "Stornieren",
"IMPORT": "Importieren",
@@ -315,8 +321,8 @@
"ERROR_MESSAGE": "Es ist ein Fehler aufgetreten, bitte versuchen Sie es erneut"
},
"EXPORT_CONTACT": {
"TITLE": "Export contacts",
"DESCRIPTION": "Quickly export a csv file with comprehensive details of your contacts",
"TITLE": "Kontakte exportieren",
"DESCRIPTION": "Exportieren Sie schnell eine CSV-Datei mit umfassenden Daten Ihrer Kontakte",
"CONFIRM": "Exportieren",
"SUCCESS_MESSAGE": "Export is in progress, You will be notified via email when export file is ready to dowanlod.",
"ERROR_MESSAGE": "Es ist ein Fehler aufgetreten, bitte versuchen Sie es erneut"
@@ -335,10 +341,10 @@
}
},
"ORDER": {
"LABEL": "Ordering",
"LABEL": "Bestellen",
"OPTIONS": {
"ASCENDING": "Ascending",
"DESCENDING": "Descending"
"ASCENDING": "Aufsteigend",
"DESCENDING": "Absteigend"
}
},
"FILTERS": {
@@ -346,24 +352,24 @@
"TITLE": "Möchten Sie diesen Filter speichern?",
"CONFIRM": "Filter speichern",
"LABEL": "Name",
"PLACEHOLDER": "Enter the name of the filter",
"ERROR": "Enter a valid name",
"SUCCESS_MESSAGE": "Filter saved successfully",
"ERROR_MESSAGE": "Unable to save filter. Please try again later."
"PLACEHOLDER": "Geben Sie einen Namen für diesen Filter ein",
"ERROR": "Gib einen gültigen Namen ein",
"SUCCESS_MESSAGE": "Filter erfolgreich gespeichert",
"ERROR_MESSAGE": "Filter konnte nicht gespeichert werden. Bitte versuchen Sie es später erneut."
},
"DELETE_SEGMENT": {
"TITLE": "Löschung bestätigen",
"DESCRIPTION": "Are you sure you want to delete this filter?",
"DESCRIPTION": "Möchten Sie diesen Filter wirklich löschen?",
"CONFIRM": "Ja, löschen",
"CANCEL": "Nein, abbrechen",
"SUCCESS_MESSAGE": "Filter deleted successfully",
"ERROR_MESSAGE": "Unable to delete filter. Please try again later."
"SUCCESS_MESSAGE": "Filter erfolgreich gelöscht",
"ERROR_MESSAGE": "Filter konnte nicht gelöscht werden. Bitte versuchen Sie es später erneut."
}
}
}
},
"PAGINATION_FOOTER": {
"SHOWING": "Showing {startItem} - {endItem} of {totalItems} contacts"
"SHOWING": "Zeige {startItem} - {endItem} von {totalItems} Kontakten"
},
"FILTER": {
"NAME": "Name",
@@ -380,87 +386,87 @@
"BLOCKED_FALSE": "Nein",
"BUTTONS": {
"CLEAR_FILTERS": "Filter zurücksetzen",
"UPDATE_SEGMENT": "Update segment",
"UPDATE_SEGMENT": "Segment aktualisieren",
"APPLY_FILTERS": "Filter übernehmen",
"ADD_FILTER": "Filter hinzufügen"
},
"TITLE": "Kontakte filtern",
"EDIT_SEGMENT": "Segment bearbeiten",
"SEGMENT": {
"LABEL": "Segment name",
"INPUT_PLACEHOLDER": "Enter the name of the segment"
"LABEL": "Segmentname",
"INPUT_PLACEHOLDER": "Geben Sie den Namen des Segments ein"
},
"ACTIVE_FILTERS": {
"MORE_FILTERS": "+ {count} more filters",
"MORE_FILTERS": "+ {count} weitere Filter",
"CLEAR_FILTERS": "Filter zurücksetzen"
}
},
"CARD": {
"OF": "of",
"OF": "von",
"VIEW_DETAILS": "Details anzeigen",
"EDIT_DETAILS_FORM": {
"TITLE": "Kontaktdetails bearbeiten",
"FORM": {
"FIRST_NAME": {
"PLACEHOLDER": "Enter the first name"
"PLACEHOLDER": "Vorname eingeben"
},
"LAST_NAME": {
"PLACEHOLDER": "Enter the last name"
"PLACEHOLDER": "Nachname eingeben"
},
"EMAIL_ADDRESS": {
"PLACEHOLDER": "Enter the email address",
"PLACEHOLDER": "E-Mail-Adresse eingeben",
"DUPLICATE": "Diese E-Mail-Adresse wird bereits für einen anderen Kontakt verwendet."
},
"PHONE_NUMBER": {
"PLACEHOLDER": "Enter the phone number",
"PLACEHOLDER": "Telefonnummer eingeben",
"DUPLICATE": "Diese Telefonnummer wird für einen anderen Kontakt verwendet."
},
"CITY": {
"PLACEHOLDER": "Geben Sie den Ortsnamen ein"
},
"COUNTRY": {
"PLACEHOLDER": "Select country"
"PLACEHOLDER": "Land auswählen"
},
"BIO": {
"PLACEHOLDER": "Enter the bio"
"PLACEHOLDER": "Biografie eingeben"
},
"COMPANY_NAME": {
"PLACEHOLDER": "Firmenname eingeben"
}
},
"UPDATE_BUTTON": "Update contact",
"SUCCESS_MESSAGE": "Contact updated successfully",
"ERROR_MESSAGE": "Unable to update contact. Please try again later."
"UPDATE_BUTTON": "Kontakt aktualisieren",
"SUCCESS_MESSAGE": "Kontakt erfolgreich aktualisiert",
"ERROR_MESSAGE": "Kontakt konnte nicht aktualisiert werden. Bitte versuche es später erneut."
},
"SOCIAL_MEDIA": {
"TITLE": "Edit social links",
"TITLE": "Social-Media Links bearbeiten",
"FORM": {
"FACEBOOK": {
"PLACEHOLDER": "Add Facebook"
"PLACEHOLDER": "Facebook hinzufügen"
},
"GITHUB": {
"PLACEHOLDER": "Add Github"
"PLACEHOLDER": "Github hinzufügen"
},
"INSTAGRAM": {
"PLACEHOLDER": "Add Instagram"
"PLACEHOLDER": "Instagram hinzufügen"
},
"LINKEDIN": {
"PLACEHOLDER": "Add LinkedIn"
"PLACEHOLDER": "LinkedIn hinzufügen"
},
"TWITTER": {
"PLACEHOLDER": "Add Twitter"
"PLACEHOLDER": "Twitter hinzufügen"
}
}
}
},
"DETAILS": {
"CREATED_AT": "Created {date}",
"LAST_ACTIVITY": "Last active {date}",
"DELETE_CONTACT_DESCRIPTION": "Permanently delete this contact. This action is irreversible",
"CREATED_AT": "Erstellt am {date}",
"LAST_ACTIVITY": "Zuletzt aktiv {date}",
"DELETE_CONTACT_DESCRIPTION": "Diesen Kontakt dauerhaft löschen. Diese Aktion kann nicht rückgängig gemacht werden",
"DELETE_CONTACT": "Kontakt löschen",
"DELETE_DIALOG": {
"TITLE": "Löschung bestätigen",
"DESCRIPTION": "Are you sure you want to delete this {contactName} contact?",
"DESCRIPTION": "Sind Sie sicher, dass Sie den Kontakt {contactName} löschen möchten?",
"CONFIRM": "Ja, löschen",
"API": {
"SUCCESS_MESSAGE": "Kontakt erfolgreich gelöscht",
@@ -469,29 +475,29 @@
},
"AVATAR": {
"UPLOAD": {
"ERROR_MESSAGE": "Could not upload avatar. Please try again later.",
"SUCCESS_MESSAGE": "Avatar uploaded successfully"
"ERROR_MESSAGE": "Avatar konnte nicht hochgeladen werden. Bitte versuche es später erneut.",
"SUCCESS_MESSAGE": "Avatar erfolgreich hochgeladen"
},
"DELETE": {
"SUCCESS_MESSAGE": "Avatar erfolgreich gelöscht",
"ERROR_MESSAGE": "Could not delete avatar. Please try again later."
"ERROR_MESSAGE": "Avatar konnte nicht gelöscht werden. Bitte versuche es später erneut."
}
}
},
"SIDEBAR": {
"TABS": {
"ATTRIBUTES": "Attribute",
"HISTORY": "History",
"HISTORY": "Verlauf",
"NOTES": "Notizen",
"MERGE": "Merge"
"MERGE": "Zusammenführen"
},
"HISTORY": {
"EMPTY_STATE": "Es sind keine vorherigen Gespräche mit diesem Kontakt verbunden"
},
"ATTRIBUTES": {
"SEARCH_PLACEHOLDER": "Search for attributes",
"UNUSED_ATTRIBUTES": "{count} Used attribute | {count} Unused attributes",
"EMPTY_STATE": "There are no contact custom attributes available in this account. You can create a custom attribute in settings.",
"SEARCH_PLACEHOLDER": "Nach Attributen suchen",
"UNUSED_ATTRIBUTES": "{count} verwendete Attribute | {count} ungenutzte Attribute",
"EMPTY_STATE": "Es gibt keine benutzerdefinierten Attribute für Kontakte in diesem Konto. Sie können ein eigenes Attribut in den Einstellungen erstellen.",
"YES": "Ja",
"NO": "Nein",
"TRIGGER": {
@@ -499,11 +505,11 @@
"INPUT": "Wert eintragen"
},
"VALIDATIONS": {
"INVALID_NUMBER": "Invalid number",
"INVALID_NUMBER": "Ungültige Nummer",
"REQUIRED": "Gültiger Wert ist erforderlich",
"INVALID_INPUT": "Invalid input",
"INVALID_INPUT": "Ungültige Eingabe",
"INVALID_URL": "Ungültige URL",
"INVALID_DATE": "Invalid date"
"INVALID_DATE": "Ungültiges Datum"
},
"NO_ATTRIBUTES": "Keine Attribute gefunden",
"API": {
@@ -515,16 +521,16 @@
},
"MERGE": {
"TITLE": "Kontakte zusammenführen",
"DESCRIPTION": "Combine two profiles into one, including all attributes and conversations. In case of conflict, the primary contacts attributes will take precedence.",
"DESCRIPTION": "Profile zusammenführen, um zwei Profile zu einem zu kombinieren, einschließlich aller Attribute und Gespräche. Im Falle eines Konflikts haben die Attribute des primären Kontakts Vorrang.",
"PRIMARY": "Hauptkontakt",
"PRIMARY_HELP_LABEL": "To be saved",
"PRIMARY_REQUIRED_ERROR": "Please select a contact to merge with before proceeding",
"PARENT": "To be merged",
"PRIMARY_HELP_LABEL": "Zu speichern",
"PRIMARY_REQUIRED_ERROR": "Bitte wähle einen Kontakt zum Zusammenführen aus, bevor du fortfährst",
"PARENT": "Zusammenzuführen",
"PARENT_HELP_LABEL": "Zu löschen",
"EMPTY_STATE": "No contacts found",
"PLACEHOLDER": "Search for primary contact",
"EMPTY_STATE": "Keine Kontakte gefunden",
"PLACEHOLDER": "Nach primärem Kontakt suchen",
"SEARCH_PLACEHOLDER": "Nach Kontakt suchen",
"SEARCH_ERROR_MESSAGE": "Could not search for contacts. Please try again later.",
"SEARCH_ERROR_MESSAGE": "Kontakte konnten nicht gesucht werden. Bitte versuchen Sie es später erneut.",
"SUCCESS_MESSAGE": "Kontakt erfolgreich zusammengeführt",
"ERROR_MESSAGE": "Kontakte konnten nicht zusammengeführt werden, bitte erneut versuchen!",
"IS_SEARCHING": "Suchen...",
@@ -537,62 +543,62 @@
"PLACEHOLDER": "Notiz hinzufügen",
"WROTE": "schrieb",
"YOU": "Sie",
"SAVE": "Save note",
"EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above."
"SAVE": "Notiz speichern",
"EMPTY_STATE": "Es gibt keine Notizen zu diesem Kontakt. Sie können eine Notiz hinzufügen, indem Sie diese in das obige Feld eingeben."
}
},
"EMPTY_STATE": {
"TITLE": "No contacts found in this account",
"SUBTITLE": "Start adding new contacts by clicking on the button below",
"BUTTON_LABEL": "Add contact",
"TITLE": "Keine Kontakte in diesem Konto gefunden",
"SUBTITLE": "Füge neue Kontakte hinzu, indem du auf den Button unten klickst",
"BUTTON_LABEL": "Kontakt hinzufügen",
"SEARCH_EMPTY_STATE_TITLE": "Keine Kontakte entsprechen Ihrer Suche 🔍",
"LIST_EMPTY_STATE_TITLE": "No contacts available in this view 📋"
"LIST_EMPTY_STATE_TITLE": "Keine Kontakte verfügbar in dieser Ansicht 📋"
}
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
"ERROR_MESSAGE": "We couldnt complete the search. Please try again."
"ERROR_MESSAGE": "Wir konnten die Suche nicht abschließen. Bitte versuch es erneut."
},
"FORM": {
"GO_TO_CONVERSATION": "Aussicht",
"SUCCESS_MESSAGE": "The message was sent successfully!",
"ERROR_MESSAGE": "An error occurred while creating the conversation. Please try again later.",
"NO_INBOX_ALERT": "There are no available inboxes to start a conversation with this contact.",
"GO_TO_CONVERSATION": "Anzeigen",
"SUCCESS_MESSAGE": "Die Nachricht wurde erfolgreich versendet!",
"ERROR_MESSAGE": "Beim Erstellen der Unterhaltung ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.",
"NO_INBOX_ALERT": "Es sind keine Posteingänge vorhanden, um eine Unterhaltung mit diesem Kontakt zu starten.",
"CONTACT_SELECTOR": {
"LABEL": "An:",
"TAG_INPUT_PLACEHOLDER": "Search for a contact with name, email or phone number",
"CONTACT_CREATING": "Creating contact..."
"TAG_INPUT_PLACEHOLDER": "Suche nach Kontakten mit Name, E-Mail oder Telefonnummer",
"CONTACT_CREATING": "Neuen Kontakt erstellen..."
},
"INBOX_SELECTOR": {
"LABEL": "Via:",
"BUTTON": "Show inboxes"
"BUTTON": "Posteingänge anzeigen"
},
"EMAIL_OPTIONS": {
"SUBJECT_LABEL": "Betreff :",
"SUBJECT_PLACEHOLDER": "Enter your email subject here",
"SUBJECT_PLACEHOLDER": "E-Mail Betreff hier eingeben",
"CC_LABEL": "Cc:",
"CC_PLACEHOLDER": "Search for a contact with their email address",
"CC_PLACEHOLDER": "Suche nach einem Kontakt mit seiner E-Mail-Adresse",
"BCC_LABEL": "Bcc:",
"BCC_PLACEHOLDER": "Search for a contact with their email address",
"BCC_PLACEHOLDER": "Suche nach einem Kontakt mit seiner E-Mail-Adresse",
"BCC_BUTTON": "Bcc"
},
"MESSAGE_EDITOR": {
"PLACEHOLDER": "Schreiben Sie Ihre Nachricht hier..."
},
"WHATSAPP_OPTIONS": {
"LABEL": "Select template",
"SEARCH_PLACEHOLDER": "Search templates",
"EMPTY_STATE": "No templates found",
"LABEL": "Vorlage auswählen",
"SEARCH_PLACEHOLDER": "Vorlagen suchen",
"EMPTY_STATE": "Keine Vorlagen gefunden",
"TEMPLATE_PARSER": {
"TEMPLATE_NAME": "WhatsApp template: {templateName}",
"TEMPLATE_NAME": "WhatsApp Template: {templateName}",
"VARIABLES": "Variablen",
"BACK": "Zurück",
"SEND_MESSAGE": "Nachricht senden"
}
},
"ACTION_BUTTONS": {
"DISCARD": "Discard",
"SEND": "Send ({keyCode})"
"DISCARD": "Verwerfen",
"SEND": "Senden ({keyCode})"
}
}
}
@@ -38,23 +38,23 @@
"REMOVE_SELECTION": "Auswahl entfernen",
"DOWNLOAD": "Herunterladen",
"UNKNOWN_FILE_TYPE": "Unbekannte Datei",
"SAVE_CONTACT": "Save Contact",
"NO_CONTENT": "No content to display",
"SAVE_CONTACT": "Kontakt speichern",
"NO_CONTENT": "Kein Inhalt zum Anzeigen",
"SHARED_ATTACHMENT": {
"CONTACT": "{sender} has shared a contact",
"LOCATION": "{sender} has shared a location",
"FILE": "{sender} has shared a file",
"CONTACT": "{sender} hat einen Kontakt geteilt",
"LOCATION": "{sender} hat einen Standort geteilt",
"FILE": "{sender} hat eine Datei geteilt",
"MEETING": "{sender} hat ein Meeting begonnen"
},
"UPLOADING_ATTACHMENTS": "Anhänge werden hochgeladen...",
"REPLIED_TO_STORY": "Auf deine Geschichte geantwortet",
"UNSUPPORTED_MESSAGE": "This message is unsupported. You can view this message on the Facebook / Instagram app.",
"UNSUPPORTED_MESSAGE": "Diese Nachricht wird nicht unterstützt. Sie können diese Nachricht in der Facebook/Instagram-App sehen.",
"UNSUPPORTED_MESSAGE_FACEBOOK": "Diese Nachricht wird nicht unterstützt. Sie können diese Nachricht in der Facebook-Messenger-App sehen.",
"UNSUPPORTED_MESSAGE_INSTAGRAM": "Diese Nachricht wird nicht unterstützt. Sie können diese Nachricht in der Instagram-App sehen.",
"SUCCESS_DELETE_MESSAGE": "Nachricht erfolgreich gelöscht",
"FAIL_DELETE_MESSSAGE": "Nachricht konnte nicht gelöscht werden! Versuchen Sie es erneut",
"NO_RESPONSE": "Keine Antwort",
"RESPONSE": "Response",
"RESPONSE": "Antwort",
"RATING_TITLE": "Bewertung",
"FEEDBACK_TITLE": "Feedback",
"REPLY_MESSAGE_NOT_FOUND": "Nachricht nicht verfügbar",
@@ -117,6 +117,7 @@
"PENDING": "Als ausstehend markieren",
"RESOLVED": "Als gelöst markieren",
"MARK_AS_UNREAD": "Als ungelesen markieren",
"MARK_AS_READ": "Als gelesen markieren",
"REOPEN": "Konversation wieder öffnen",
"SNOOZE": {
"TITLE": "Erinnern",
@@ -134,7 +135,7 @@
"FAILED": "Agent konnte nicht zugewiesen werden. Bitte versuche es erneut."
},
"LABEL_ASSIGNMENT": {
"SUCCESFUL": "Assigned label #{labelName} to conversation id {conversationId}",
"SUCCESFUL": "Label #{labelName} der Konversations-ID {conversationId} zugewiesen",
"FAILED": "Label konnte nicht zugewiesen werden. Bitte versuche es erneut."
},
"TEAM_ASSIGNMENT": {
@@ -323,7 +324,7 @@
"BCC": "Bcc",
"CC": "Cc",
"SUBJECT": "Betreff",
"EXPAND": "Expand email"
"EXPAND": "E-Mail erweitern"
},
"CONVERSATION_PARTICIPANTS": {
"SIDEBAR_MENU_TITLE": "Zugewiesen",
@@ -350,14 +351,14 @@
"NO_TRANSLATIONS_AVAILABLE": "Für diesen Inhalt sind keine Übersetzungen verfügbar"
},
"TYPING": {
"ONE": "{user} is typing",
"TWO": "{user} and {secondUser} are typing",
"MULTIPLE": "{user} and {count} others are typing"
"ONE": "{user} tippt",
"TWO": "{user} und {secondUser} tippen",
"MULTIPLE": "{user} und {count} andere tippen"
},
"COPILOT": {
"TRY_THESE_PROMPTS": "Try these prompts"
"TRY_THESE_PROMPTS": "Probiere diese Prompts"
},
"GALLERY_VIEW": {
"ERROR_DOWNLOADING": "Unable to download attachment. Please try again"
"ERROR_DOWNLOADING": "Anhang konnte nicht heruntergeladen werden. Bitte versuche es erneut"
}
}

Some files were not shown because too many files have changed in this diff Show More