Compare commits

..
12 Commits
658 changed files with 489 additions and 29509 deletions
+1 -1
View File
@@ -1 +1 @@
4.13.0
4.12.1
@@ -103,7 +103,6 @@ const showPagination = computed(() => {
<ContactsActiveFiltersPreview
v-if="showActiveFiltersPreview"
:active-segment="activeSegment"
class="mb-1"
@clear-filters="emit('clearFilters')"
@open-filter="openFilter"
/>
@@ -57,7 +57,7 @@ useKeyboardEvents(keyboardEvents);
<template>
<ButtonGroup
class="flex flex-col justify-center items-center absolute top-36 xl:top-24 ltr:right-2 rtl:left-2 bg-n-solid-2/90 backdrop-blur-lg border border-n-weak/50 rounded-full gap-1.5 p-1.5 shadow-sm transition-shadow duration-200 hover:shadow !z-20"
class="flex flex-col justify-center items-center absolute top-36 xl:top-24 ltr:right-2 rtl:left-2 bg-n-solid-2/90 backdrop-blur-lg border border-n-weak/50 rounded-full gap-1.5 p-1.5 shadow-sm transition-shadow duration-200 hover:shadow"
>
<Button
v-tooltip.top="$t('CONVERSATION.SIDEBAR.CONTACT')"
@@ -1,5 +1,5 @@
<script setup>
import { ref, computed, watch } from 'vue';
import { computed, ref } from 'vue';
import { debounce } from '@chatwoot/utils';
import { useI18n } from 'vue-i18n';
import { ARTICLE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
@@ -27,6 +27,7 @@ const props = defineProps({
const emit = defineEmits([
'saveArticle',
'saveArticleAsync',
'goBack',
'setAuthor',
'setCategory',
@@ -38,37 +39,42 @@ const { t } = useI18n();
const isNewArticle = computed(() => !props.article?.id);
const localTitle = ref(props.article?.title ?? '');
const localContent = ref(props.article?.content ?? '');
const saveAndSync = value => {
emit('saveArticle', value);
};
// Sync local state when navigating to a different article or on initial fetch
watch(
() => props.article?.id,
newId => {
if (newId) {
localTitle.value = props.article?.title ?? '';
localContent.value = props.article?.content ?? '';
}
}
// this will only send the data to the backend
// but will not update the local state preventing unnecessary re-renders
// since the data is already saved and we keep the editor text as the source of truth
const quickSave = debounce(
value => emit('saveArticleAsync', value),
400,
false
);
const debouncedSave = debounce(value => emit('saveArticle', value), 500, false);
// 2.5 seconds is enough to know that the user has stopped typing and is taking a pause
// so we can save the data to the backend and retrieve the updated data
// this will update the local state with response data
// Only use to save for existing articles
const saveAndSyncDebounced = debounce(saveAndSync, 2500, false);
const handleSave = value => {
if (isNewArticle.value) return;
debouncedSave(value);
quickSave(value);
saveAndSyncDebounced(value);
};
const articleTitle = computed({
get: () => localTitle.value,
get: () => props.article.title,
set: value => {
localTitle.value = value;
handleSave({ title: value });
},
});
const localContent = ref(props.article.content || '');
const articleContent = computed({
get: () => localContent.value,
get: () => props.article.content,
set: content => {
localContent.value = content;
handleSave({ content });
@@ -126,7 +132,7 @@ const handleCreateArticle = event => {
/>
<ArticleEditorControls
:article="article"
@save-article="values => emit('saveArticle', values)"
@save-article="saveAndSync"
@set-author="setAuthorId"
@set-category="setCategoryId"
/>
@@ -161,12 +167,8 @@ const handleCreateArticle = event => {
}
.editor-root .has-selection {
.ProseMirror-menubar:not(:has(*)) {
display: none !important;
}
.ProseMirror-menubar {
@apply rounded-lg !px-3 !py-1.5 z-50 bg-n-background items-center gap-4 ml-0 mb-0 shadow-md outline outline-1 outline-n-weak;
@apply h-8 rounded-lg !px-2 z-50 bg-n-solid-3 items-center gap-4 ml-0 mb-0 shadow-md outline outline-1 outline-n-weak;
display: flex;
top: var(--selection-top, auto) !important;
left: var(--selection-left, 0) !important;
@@ -174,10 +176,15 @@ const handleCreateArticle = event => {
position: absolute !important;
.ProseMirror-menuitem {
@apply ltr:mr-0 rtl:ml-0 size-4 flex items-center;
@apply mr-0;
.ProseMirror-icon {
@apply p-0.5 flex-shrink-0 ltr:mr-2 rtl:ml-2;
@apply p-0 mt-0 !mr-0;
svg {
width: 20px !important;
height: 20px !important;
}
}
}
@@ -1,5 +1,5 @@
<script setup>
import { computed, useSlots } from 'vue';
import { computed } from 'vue';
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
import Button from 'dashboard/components-next/button/Button.vue';
@@ -30,40 +30,23 @@ const modelValue = defineModel({
});
const selectedCount = computed(() => modelValue.value.size);
const visibleItemIds = computed(() => props.allItems.map(item => item.id));
const visibleItemCount = computed(() => visibleItemIds.value.length);
const selectedVisibleCount = computed(
() => visibleItemIds.value.filter(id => modelValue.value.has(id)).length
);
const totalCount = computed(() => props.allItems.length);
const hasSelected = computed(() => selectedCount.value > 0);
const isIndeterminate = computed(
() =>
selectedVisibleCount.value > 0 &&
selectedVisibleCount.value < visibleItemCount.value
() => hasSelected.value && selectedCount.value < totalCount.value
);
const allSelected = computed(
() =>
visibleItemCount.value > 0 &&
selectedVisibleCount.value === visibleItemCount.value
() => totalCount.value > 0 && selectedCount.value === totalCount.value
);
const slots = useSlots();
const hasSecondaryActions = computed(() => Boolean(slots['secondary-actions']));
const bulkCheckboxState = computed({
get: () => allSelected.value,
set: shouldSelectAll => {
if (!visibleItemCount.value) {
return;
}
const updatedSelection = new Set(modelValue.value);
if (shouldSelectAll) {
visibleItemIds.value.forEach(id => updatedSelection.add(id));
} else {
visibleItemIds.value.forEach(id => updatedSelection.delete(id));
}
modelValue.value = updatedSelection;
const newSelectedIds = shouldSelectAll
? new Set(props.allItems.map(item => item.id))
: new Set();
modelValue.value = newSelectedIds;
},
});
</script>
@@ -80,7 +63,7 @@ const bulkCheckboxState = computed({
v-if="hasSelected"
class="flex items-center gap-3 py-1 ltr:pl-3 rtl:pr-3 ltr:pr-4 rtl:pl-4 rounded-lg bg-n-solid-2 outline outline-1 outline-n-container shadow"
>
<div class="flex items-center gap-3 min-w-0">
<div class="flex items-center gap-3">
<div class="flex items-center gap-1.5 min-w-0">
<Checkbox
v-model="bulkCheckboxState"
@@ -95,23 +78,21 @@ const bulkCheckboxState = computed({
<span class="text-sm text-n-slate-10 truncate tabular-nums">
{{ selectedCountLabel }}
</span>
<div class="h-4 w-px bg-n-strong" />
<slot name="secondary-actions" />
</div>
<div class="flex items-center gap-3">
<slot v-if="hasSecondaryActions" name="secondary-actions" />
<div v-if="hasSecondaryActions" class="h-4 w-px bg-n-strong" />
<div class="flex items-center gap-3">
<slot name="actions" :selected-count="selectedCount">
<Button
:label="deleteLabel"
sm
ruby
ghost
class="!px-1.5"
icon="i-lucide-trash"
@click="emit('bulkDelete')"
/>
</slot>
</div>
<slot name="actions" :selected-count="selectedCount">
<Button
:label="deleteLabel"
sm
ruby
ghost
class="!px-1.5"
icon="i-lucide-trash"
@click="emit('bulkDelete')"
/>
</slot>
</div>
</div>
<div v-else class="flex items-center gap-3">
@@ -226,10 +226,4 @@ const handleSeeOriginal = () => {
}
}
}
// Email clients (Gmail, Outlook) hardcode dir="ltr" on wrapper elements.
// In RTL apps this forces email content LTR regardless of actual text.
[dir='rtl'] .letter-render [dir='ltr'] {
direction: inherit;
}
</style>
@@ -250,12 +250,6 @@ const menuItems = computed(() => {
activeOn: ['conversation_through_mentions'],
to: accountScopedRoute('conversation_mentions'),
},
{
name: 'Participating',
label: t('SIDEBAR.PARTICIPATING_CONVERSATIONS'),
activeOn: ['conversation_through_participating'],
to: accountScopedRoute('conversation_participating'),
},
{
name: 'Unattended',
activeOn: ['conversation_through_unattended'],
@@ -158,11 +158,9 @@ const activeChild = computed(() => {
return rankedPage ?? activeOnPages[0];
}
return navigableChildren.value.find(child => {
if (!child.to) return false;
const childPath = resolvePath(child.to);
return route.path === childPath || route.path.startsWith(`${childPath}/`);
});
return navigableChildren.value.find(
child => child.to && route.path.startsWith(resolvePath(child.to))
);
});
const hasActiveChild = computed(() => {
@@ -56,7 +56,6 @@ import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHe
import { conversationListPageURL } from '../helper/URLHelper';
import {
isOnMentionsView,
isOnParticipatingView,
isOnUnattendedView,
} from '../store/modules/conversations/helpers/actionHelpers';
import {
@@ -114,7 +113,6 @@ const chatLists = useMapGetter('getFilteredConversations');
const mineChatsList = useMapGetter('getMineChats');
const allChatList = useMapGetter('getAllStatusChats');
const unAssignedChatsList = useMapGetter('getUnAssignedChats');
const participatingChatsList = useMapGetter('getParticipatingChats');
const chatListLoading = useMapGetter('getChatListLoadingStatus');
const activeInbox = useMapGetter('getSelectedInbox');
const conversationStats = useMapGetter('conversationStats/getStats');
@@ -298,15 +296,13 @@ const pageTitle = computed(() => {
if (props.label) {
return `#${props.label}`;
}
if (props.conversationType === wootConstants.CONVERSATION_TYPE.MENTION) {
if (props.conversationType === 'mention') {
return t('CHAT_LIST.MENTION_HEADING');
}
if (
props.conversationType === wootConstants.CONVERSATION_TYPE.PARTICIPATING
) {
if (props.conversationType === 'participating') {
return t('CONVERSATION_PARTICIPANTS.SIDEBAR_MENU_TITLE');
}
if (props.conversationType === wootConstants.CONVERSATION_TYPE.UNATTENDED) {
if (props.conversationType === 'unattended') {
return t('CHAT_LIST.UNATTENDED_HEADING');
}
if (hasActiveFolders.value) {
@@ -315,30 +311,12 @@ const pageTitle = computed(() => {
return t('CHAT_LIST.TAB_HEADING');
});
function filterByAssigneeTab(conversations) {
if (activeAssigneeTab.value === wootConstants.ASSIGNEE_TYPE.ME) {
return conversations.filter(
c => c.meta?.assignee?.id === currentUser.value?.id
);
}
if (activeAssigneeTab.value === wootConstants.ASSIGNEE_TYPE.UNASSIGNED) {
return conversations.filter(c => !c.meta?.assignee);
}
return [...conversations];
}
const conversationList = computed(() => {
let localConversationList = [];
if (!hasAppliedFiltersOrActiveFolders.value) {
const filters = conversationFilters.value;
if (
props.conversationType === wootConstants.CONVERSATION_TYPE.PARTICIPATING
) {
localConversationList = filterByAssigneeTab(
participatingChatsList.value(filters)
);
} else if (activeAssigneeTab.value === 'me') {
if (activeAssigneeTab.value === 'me') {
localConversationList = [...mineChatsList.value(filters)];
} else if (activeAssigneeTab.value === 'unassigned') {
localConversationList = [...unAssignedChatsList.value(filters)];
@@ -659,11 +637,9 @@ function redirectToConversationList() {
let conversationType = '';
if (isOnMentionsView({ route: { name } })) {
conversationType = wootConstants.CONVERSATION_TYPE.MENTION;
} else if (isOnParticipatingView({ route: { name } })) {
conversationType = wootConstants.CONVERSATION_TYPE.PARTICIPATING;
conversationType = 'mention';
} else if (isOnUnattendedView({ route: { name } })) {
conversationType = wootConstants.CONVERSATION_TYPE.UNATTENDED;
conversationType = 'unattended';
}
router.push(
conversationListPageURL({
@@ -27,6 +27,10 @@ const props = defineProps({
type: Boolean,
default: true,
},
isPopout: {
type: Boolean,
default: false,
},
});
const emit = defineEmits([
@@ -204,7 +208,10 @@ onMounted(() => {
<template>
<div class="space-y-2 mb-4">
<div class="overflow-y-auto max-h-56">
<div
class="overflow-y-auto"
:class="{ 'max-h-96': isPopout, 'max-h-56': !isPopout }"
>
<p
v-dompurify-html="formatMessage(generatedContent, false)"
class="text-n-iris-12 text-sm prose-sm font-normal !mb-4"
@@ -984,32 +984,7 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
}
.ProseMirror-woot-style {
@apply overflow-auto;
}
.ProseMirror-woot-style:not(
:where(.resizable-editor-wrapper .ProseMirror-woot-style)
) {
@apply min-h-[5rem] max-h-[7.5rem];
}
// Resizable editor wrapper styles
.resizable-editor-wrapper {
.ProseMirror-woot-style {
min-height: clamp(
var(--editor-min-allowed, var(--editor-min-height, 5rem)),
var(--editor-height, var(--editor-min-height, 5rem)),
var(--editor-max-allowed, var(--editor-max-height, 7.5rem))
);
max-height: clamp(
var(--editor-min-allowed, var(--editor-min-height, 5rem)),
var(--editor-height, var(--editor-min-height, 5rem)),
var(--editor-max-allowed, var(--editor-max-height, 7.5rem))
);
transition:
min-height var(--editor-height-transition, 180ms ease),
max-height var(--editor-height-transition, 180ms ease);
}
@apply overflow-auto min-h-[5rem] max-h-[7.5rem];
}
.ProseMirror-prompt-backdrop::backdrop {
@@ -8,22 +8,13 @@ import {
EditorState,
Selection,
} from '@chatwoot/prosemirror-schema';
import {
suggestionsPlugin,
triggerCharacters,
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
import imagePastePlugin from '@chatwoot/prosemirror-schema/src/plugins/image';
import { toggleMark } from 'prosemirror-commands';
import { wrapInList } from 'prosemirror-schema-list';
import { toggleBlockType } from '@chatwoot/prosemirror-schema/src/menu/common';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import SlashCommandMenu from './SlashCommandMenu.vue';
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
const SLASH_MENU_OFFSET = 4;
const createState = (
content,
placeholder,
@@ -49,7 +40,6 @@ let editorView = null;
let state;
export default {
components: { SlashCommandMenu },
mixins: [keyboardEventListenerMixins],
props: {
modelValue: { type: String, default: '' },
@@ -72,15 +62,8 @@ export default {
},
data() {
return {
plugins: [
imagePastePlugin(this.handleImageUpload),
this.createSlashPlugin(),
],
plugins: [imagePastePlugin(this.handleImageUpload)],
isTextSelected: false, // Tracks text selection and prevents unnecessary re-renders on mouse selection
showSlashMenu: false,
slashSearchTerm: '',
slashRange: null,
slashMenuPosition: null,
};
},
watch: {
@@ -112,126 +95,6 @@ export default {
}
},
methods: {
createSlashPlugin() {
return suggestionsPlugin({
matcher: triggerCharacters('/', 0),
suggestionClass: '',
onEnter: args => {
this.showSlashMenu = true;
this.slashRange = args.range;
this.slashSearchTerm = args.text || '';
this.updateSlashMenuPosition(args.range.from);
return false;
},
onChange: args => {
this.slashRange = args.range;
this.slashSearchTerm = args.text;
return false;
},
onExit: () => {
this.slashSearchTerm = '';
this.showSlashMenu = false;
this.slashMenuPosition = null;
return false;
},
onKeyDown: ({ event }) => {
return (
event.keyCode === 13 &&
this.showSlashMenu &&
this.$refs.slashMenu?.hasItems
);
},
});
},
updateSlashMenuPosition(pos) {
if (!editorView) return;
const coords = editorView.coordsAtPos(pos);
const editorRect = this.$refs.editor.getBoundingClientRect();
const isRtl = getComputedStyle(this.$refs.editor).direction === 'rtl';
this.slashMenuPosition = {
top: coords.bottom - editorRect.top + SLASH_MENU_OFFSET,
...(isRtl
? { right: editorRect.right - coords.right }
: { left: coords.left - editorRect.left }),
};
},
removeSlashTriggerText() {
if (!editorView || !this.slashRange) return;
const { from, to } = this.slashRange;
editorView.dispatch(editorView.state.tr.delete(from, to));
state = editorView.state;
},
executeSlashCommand(actionKey) {
if (!editorView) return;
this.removeSlashTriggerText();
const { schema } = editorView.state;
const commandMap = {
strong: () =>
toggleMark(schema.marks.strong)(
editorView.state,
editorView.dispatch
),
em: () =>
toggleMark(schema.marks.em)(editorView.state, editorView.dispatch),
strike: () =>
toggleMark(schema.marks.strike)(
editorView.state,
editorView.dispatch
),
code: () =>
toggleMark(schema.marks.code)(editorView.state, editorView.dispatch),
h1: () =>
toggleBlockType(schema.nodes.heading, { level: 1 })(
editorView.state,
editorView.dispatch
),
h2: () =>
toggleBlockType(schema.nodes.heading, { level: 2 })(
editorView.state,
editorView.dispatch
),
h3: () =>
toggleBlockType(schema.nodes.heading, { level: 3 })(
editorView.state,
editorView.dispatch
),
bulletList: () =>
wrapInList(schema.nodes.bullet_list)(
editorView.state,
editorView.dispatch
),
orderedList: () =>
wrapInList(schema.nodes.ordered_list)(
editorView.state,
editorView.dispatch
),
insertTable: () => {
const { table, table_row, table_header, table_cell, paragraph } =
schema.nodes;
const headerCells = [0, 1, 2].map(() =>
table_header.createAndFill(null, paragraph.create())
);
const dataCells = [0, 1, 2].map(() =>
table_cell.createAndFill(null, paragraph.create())
);
const headerRow = table_row.create(null, headerCells);
const dataRow = table_row.create(null, dataCells);
const tableNode = table.create(null, [headerRow, dataRow]);
const tr = editorView.state.tr.replaceSelectionWith(tableNode);
editorView.dispatch(tr.scrollIntoView());
},
};
const command = commandMap[actionKey];
if (command) {
command();
state = editorView.state;
this.emitOnChange();
editorView.focus();
}
},
contentFromEditor() {
if (editorView) {
return ArticleMarkdownSerializer.serialize(editorView.state.doc);
@@ -399,8 +262,7 @@ export default {
// Get the editor's width
const editorWidth = editor.offsetWidth;
const menubar = editor.querySelector('.ProseMirror-menubar');
const menubarWidth = menubar ? menubar.scrollWidth : 480;
const menubarWidth = 480; // Menubar width (adjust as needed (px))
// Get the end position of the selection
const { bottom: endBottom, right: endRight } = editorView.coordsAtPos(to);
@@ -428,15 +290,7 @@ export default {
<template>
<div>
<div class="editor-root editor--article relative">
<SlashCommandMenu
v-if="showSlashMenu"
ref="slashMenu"
:search-key="slashSearchTerm"
:enabled-menu-options="enabledMenuOptions"
:position="slashMenuPosition"
@select-action="executeSlashCommand"
/>
<div class="editor-root editor--article">
<input
ref="imageUploadInput"
type="file"
@@ -54,7 +54,7 @@ export default {
default: undefined,
},
},
emits: ['setReplyMode', 'toggleEditorSize', 'executeCopilotAction'],
emits: ['setReplyMode', 'togglePopout', 'executeCopilotAction'],
setup(props, { emit }) {
const setReplyMode = mode => {
emit('setReplyMode', mode);
@@ -189,7 +189,7 @@ export default {
class="text-n-slate-11"
sm
icon="i-lucide-maximize-2"
@click="$emit('toggleEditorSize')"
@click="$emit('togglePopout')"
/>
</div>
</div>
@@ -1,180 +0,0 @@
<script setup>
import { ref, computed, watch, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useKeyboardNavigableList } from 'dashboard/composables/useKeyboardNavigableList';
import Icon from 'dashboard/components-next/icon/Icon.vue';
const props = defineProps({
searchKey: {
type: String,
default: '',
},
enabledMenuOptions: {
type: Array,
default: () => [],
},
position: {
type: Object,
default: null,
},
});
const emit = defineEmits(['selectAction']);
const { t } = useI18n();
const EDITOR_ACTIONS = [
{
value: 'h1',
labelKey: 'SLASH_COMMANDS.HEADING_1',
icon: 'i-lucide-heading-1',
menuKey: 'h1',
},
{
value: 'h2',
labelKey: 'SLASH_COMMANDS.HEADING_2',
icon: 'i-lucide-heading-2',
menuKey: 'h2',
},
{
value: 'h3',
labelKey: 'SLASH_COMMANDS.HEADING_3',
icon: 'i-lucide-heading-3',
menuKey: 'h3',
},
{
value: 'strong',
labelKey: 'SLASH_COMMANDS.BOLD',
icon: 'i-lucide-bold',
menuKey: 'strong',
},
{
value: 'em',
labelKey: 'SLASH_COMMANDS.ITALIC',
icon: 'i-lucide-italic',
menuKey: 'em',
},
{
value: 'insertTable',
labelKey: 'SLASH_COMMANDS.TABLE',
icon: 'i-lucide-table',
menuKey: 'insertTable',
},
{
value: 'strike',
labelKey: 'SLASH_COMMANDS.STRIKETHROUGH',
icon: 'i-lucide-strikethrough',
menuKey: 'strike',
},
{
value: 'code',
labelKey: 'SLASH_COMMANDS.CODE',
icon: 'i-lucide-code',
menuKey: 'code',
},
{
value: 'bulletList',
labelKey: 'SLASH_COMMANDS.BULLET_LIST',
icon: 'i-lucide-list',
menuKey: 'bulletList',
},
{
value: 'orderedList',
labelKey: 'SLASH_COMMANDS.ORDERED_LIST',
icon: 'i-lucide-list-ordered',
menuKey: 'orderedList',
},
];
const listContainerRef = ref(null);
const selectedIndex = ref(0);
const items = computed(() => {
const search = props.searchKey.toLowerCase();
return EDITOR_ACTIONS.filter(action => {
if (!props.enabledMenuOptions.includes(action.menuKey)) return false;
if (!search) return true;
return t(action.labelKey).toLowerCase().includes(search);
});
});
const hasItems = computed(() => items.value.length > 0);
const menuStyle = computed(() => {
if (!props.position) return {};
const style = { top: `${props.position.top}px` };
if (props.position.right != null) {
style.right = `${props.position.right}px`;
} else {
style.left = `${props.position.left}px`;
}
return style;
});
const adjustScroll = () => {
nextTick(() => {
const container = listContainerRef.value;
if (!container) return;
const el = container.querySelector(`#slash-item-${selectedIndex.value}`);
if (el) {
el.scrollIntoView({ block: 'nearest', behavior: 'auto' });
}
});
};
const onSelect = () => {
const item = items.value[selectedIndex.value];
if (item) emit('selectAction', item.value);
};
useKeyboardNavigableList({
items,
onSelect,
adjustScroll,
selectedIndex,
});
// Reset selection when filtered items change
watch(items, () => {
selectedIndex.value = 0;
});
const onHover = index => {
selectedIndex.value = index;
};
const onItemClick = index => {
selectedIndex.value = index;
onSelect();
};
defineExpose({ hasItems });
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<div
v-if="hasItems"
ref="listContainerRef"
class="bg-n-alpha-3 backdrop-blur-[100px] outline outline-1 outline-n-container absolute rounded-xl z-50 flex flex-col min-w-[10rem] shadow-lg p-2 overflow-auto max-h-[15rem]"
:style="menuStyle"
>
<button
v-for="(item, index) in items"
:id="`slash-item-${index}`"
:key="item.value"
type="button"
class="inline-flex items-center justify-start w-full h-8 min-w-0 gap-2 px-2 py-1.5 border-0 rounded-lg text-n-slate-12 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-2"
:class="{
'bg-n-alpha-1 dark:bg-n-alpha-2': index === selectedIndex,
}"
@mouseover="onHover(index)"
@click="onItemClick(index)"
>
<Icon :icon="item.icon" class="flex-shrink-0 size-3.5" />
<span class="min-w-0 text-sm truncate">
{{ t(item.labelKey) }}
</span>
</button>
</div>
</template>
@@ -45,7 +45,6 @@ const backButtonUrl = computed(() => {
const conversationTypeMap = {
conversation_through_mentions: 'mention',
conversation_through_participating: 'participating',
conversation_through_unattended: 'unattended',
};
return conversationListPageURL({
@@ -16,6 +16,10 @@ defineProps({
type: String,
default: '',
},
isPopout: {
type: Boolean,
default: false,
},
});
const emit = defineEmits([
@@ -65,6 +69,7 @@ const onSend = () => {
:generated-content="generatedContent"
:min-height="4"
:enabled-menu-options="[]"
:is-popout="isPopout"
@focus="onFocus"
@blur="onBlur"
@clear-selection="clearEditorSelection"
@@ -1,7 +1,7 @@
<script>
import { ref, provide, useTemplateRef } from 'vue';
import { useElementSize } from '@vueuse/core';
import { ref, provide } from 'vue';
// composable
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import { useLabelSuggestions } from 'dashboard/composables/useLabelSuggestions';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
@@ -11,7 +11,6 @@ import MessageList from 'next/message/MessageList.vue';
import ConversationLabelSuggestion from './conversation/LabelSuggestion.vue';
import Banner from 'dashboard/components/ui/Banner.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import ResizableEditorWrapper from './ResizableEditorWrapper.vue';
// stores and apis
import { mapGetters } from 'vuex';
@@ -44,16 +43,21 @@ export default {
Banner,
ConversationLabelSuggestion,
Spinner,
ResizableEditorWrapper,
},
mixins: [inboxMixin],
setup() {
const isPopOutReplyBox = ref(false);
const conversationPanelRef = ref(null);
const resizableEditorWrapperRef = ref(null);
const messagesViewRef = useTemplateRef('messagesViewRef');
const topBannerRef = useTemplateRef('topBannerRef');
const { height: containerHeight } = useElementSize(messagesViewRef);
const { height: topBannerHeight } = useElementSize(topBannerRef);
const keyboardEvents = {
Escape: {
action: () => {
isPopOutReplyBox.value = false;
},
},
};
useKeyboardEvents(keyboardEvents);
const {
captainTasksEnabled,
@@ -64,15 +68,11 @@ export default {
provide('contextMenuElementTarget', conversationPanelRef);
return {
isPopOutReplyBox,
captainTasksEnabled,
getLabelSuggestions,
isLabelSuggestionFeatureEnabled,
conversationPanelRef,
resizableEditorWrapperRef,
messagesViewRef,
topBannerRef,
containerHeight,
topBannerHeight,
};
},
data() {
@@ -254,7 +254,6 @@ export default {
this.fetchAllAttachmentsFromCurrentChat();
this.fetchSuggestions();
this.messageSentSinceOpened = false;
this.resetReplyEditorHeight();
},
},
@@ -438,37 +437,26 @@ export default {
const payload = useSnakeCase(message);
await this.$store.dispatch('sendMessageWithData', payload);
},
toggleReplyEditorSize() {
this.resizableEditorWrapperRef?.toggleEditorExpand?.();
},
resetReplyEditorHeight() {
this.resizableEditorWrapperRef?.resetEditorHeight?.();
},
},
};
</script>
<template>
<div
ref="messagesViewRef"
class="flex flex-col justify-between flex-grow h-full min-w-0 m-0"
>
<div ref="topBannerRef">
<Banner
v-if="!currentChat.can_reply"
color-scheme="alert"
class="mx-2 mt-2 overflow-hidden rounded-lg"
:banner-message="replyWindowBannerMessage"
:href-link="replyWindowLink"
:href-link-text="replyWindowLinkText"
/>
<Banner
v-else-if="hasDuplicateInstagramInbox"
color-scheme="alert"
class="mx-2 mt-2 overflow-hidden rounded-lg"
:banner-message="$t('CONVERSATION.OLD_INSTAGRAM_INBOX_REPLY_BANNER')"
/>
</div>
<div class="flex flex-col justify-between flex-grow h-full min-w-0 m-0">
<Banner
v-if="!currentChat.can_reply"
color-scheme="alert"
class="mx-2 mt-2 overflow-hidden rounded-lg"
:banner-message="replyWindowBannerMessage"
:href-link="replyWindowLink"
:href-link-text="replyWindowLinkText"
/>
<Banner
v-else-if="hasDuplicateInstagramInbox"
color-scheme="alert"
class="mx-2 mt-2 overflow-hidden rounded-lg"
:banner-message="$t('CONVERSATION.OLD_INSTAGRAM_INBOX_REPLY_BANNER')"
/>
<MessageList
ref="conversationPanelRef"
class="conversation-panel flex-shrink flex-grow basis-px flex flex-col overflow-y-auto relative h-full m-0 pb-4"
@@ -510,7 +498,13 @@ export default {
/>
</template>
</MessageList>
<div class="flex relative flex-col bg-n-surface-1">
<div
class="flex relative flex-col"
:class="{
'modal-mask': isPopOutReplyBox,
'bg-n-surface-1': !isPopOutReplyBox,
}"
>
<div
v-if="isAnyoneTyping"
class="absolute flex items-center w-full h-0 -top-7"
@@ -526,12 +520,42 @@ export default {
/>
</div>
</div>
<ResizableEditorWrapper
ref="resizableEditorWrapperRef"
:container-height="Math.max(0, containerHeight - topBannerHeight)"
>
<ReplyBox @toggle-editor-size="toggleReplyEditorSize" />
</ResizableEditorWrapper>
<ReplyBox
:pop-out-reply-box="isPopOutReplyBox"
@update:pop-out-reply-box="isPopOutReplyBox = $event"
/>
</div>
</div>
</template>
<style scoped lang="scss">
.modal-mask {
@apply fixed;
&::v-deep {
.ProseMirror-woot-style {
@apply max-h-[25rem];
}
.reply-box {
@apply border border-n-weak max-w-[75rem] w-[70%];
&.is-private {
@apply dark:border-n-amber-3/30 border-n-amber-12/5;
}
}
.reply-box .reply-box__top {
@apply relative min-h-[27.5rem];
}
.reply-box__top .input {
@apply min-h-[27.5rem];
}
.emoji-dialog {
@apply absolute ltr:left-auto rtl:right-auto bottom-1;
}
}
}
</style>
@@ -81,7 +81,13 @@ export default {
CopilotReplyBottomPanel,
},
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
emits: ['toggleEditorSize'],
props: {
popOutReplyBox: {
type: Boolean,
default: false,
},
},
emits: ['update:popOutReplyBox'],
setup() {
const {
uiSettings,
@@ -791,6 +797,7 @@ export default {
this.clearMessage();
this.hideEmojiPicker();
this.$emit('update:popOutReplyBox', false);
}
},
sendMessageAsMultipleMessages(message, copilotAcceptedMessage = '') {
@@ -1210,9 +1217,8 @@ export default {
file => !file?.isRecordedAudio
);
},
toggleEditorSize() {
this.$emit('toggleEditorSize');
this.$nextTick(() => this.messageEditor?.focusEditorInputField());
togglePopout() {
this.$emit('update:popOutReplyBox', !this.popOutReplyBox);
},
onSubmitCopilotReply() {
const acceptedMessage = this.copilot.accept();
@@ -1238,8 +1244,9 @@ export default {
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
:characters-remaining="charactersRemaining"
:editor-content="message"
:popout-reply-box="popOutReplyBox"
@set-reply-mode="setReplyMode"
@toggle-editor-size="toggleEditorSize"
@toggle-popout="togglePopout"
@toggle-copilot="copilot.toggleEditor"
@execute-copilot-action="executeCopilotAction"
/>
@@ -1268,7 +1275,7 @@ export default {
v-if="showEmojiPicker"
v-on-clickaway="hideEmojiPicker"
:class="{
'emoji-dialog--expanded': isOnExpandedLayout,
'emoji-dialog--expanded': isOnExpandedLayout || popOutReplyBox,
}"
:on-click="addIntoEditor"
/>
@@ -1292,6 +1299,7 @@ export default {
:show-copilot-editor="copilot.showEditor.value"
:is-generating-content="copilot.isGenerating.value"
:generated-content="copilot.generatedContent.value"
:is-popout="popOutReplyBox"
:placeholder="$t('CONVERSATION.FOOTER.COPILOT_MSG_INPUT')"
@focus="onFocus"
@blur="onBlur"
@@ -1,154 +0,0 @@
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, useTemplateRef } from 'vue';
import { useEventListener } from '@vueuse/core';
import { emitter } from 'shared/helpers/mitt';
import { BUS_EVENTS } from 'shared/constants/busEvents';
const props = defineProps({
containerHeight: { type: Number, default: 0 },
});
const DEFAULT_HEIGHT = 120;
const MIN_HEIGHT = 80;
const MIN_MESSAGES_HEIGHT = 200;
const EXPAND_RATIO = 0.5;
const RESET_DELAY_MS = 120;
const wrapperRef = useTemplateRef('wrapperRef');
const surroundingHeight = ref(0);
const editorHeight = ref(DEFAULT_HEIGHT);
const isResizing = ref(false);
const startY = ref(0);
const startHeight = ref(0);
let resetTimeoutId = null;
const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
// Measure height of elements surrounding the editor (top panel, email fields, bottom panel)
const measureSurroundingHeight = () => {
if (wrapperRef.value) {
surroundingHeight.value = Math.max(
0,
wrapperRef.value.offsetHeight - editorHeight.value
);
}
};
const isContainerReady = computed(() => props.containerHeight > 0);
const sizeBounds = computed(() => {
const h = props.containerHeight;
const s = surroundingHeight.value;
const max = Math.max(MIN_HEIGHT, h - MIN_MESSAGES_HEIGHT - s);
const expanded = clamp(Math.floor(h * EXPAND_RATIO - s / 2), MIN_HEIGHT, max);
return {
min: MIN_HEIGHT,
max: isContainerReady.value ? max : DEFAULT_HEIGHT,
expanded,
default: clamp(DEFAULT_HEIGHT, MIN_HEIGHT, max),
};
});
const clampToBounds = val =>
clamp(val, sizeBounds.value.min, sizeBounds.value.max);
const clearDragStyles = () => {
Object.assign(document.body.style, { cursor: '', userSelect: '' });
};
const getClientY = e => (e.touches ? e.touches[0].clientY : e.clientY);
const onResizeStart = event => {
editorHeight.value = clampToBounds(editorHeight.value);
measureSurroundingHeight();
isResizing.value = true;
startY.value = getClientY(event);
startHeight.value = clampToBounds(editorHeight.value);
editorHeight.value = startHeight.value;
Object.assign(document.body.style, {
cursor: 'row-resize',
userSelect: 'none',
});
};
const onResizeMove = event => {
if (!isResizing.value) return;
if (event.touches) event.preventDefault();
editorHeight.value = clampToBounds(
startHeight.value + startY.value - getClientY(event)
);
};
const onResizeEnd = () => {
if (!isResizing.value) return;
isResizing.value = false;
clearDragStyles();
};
const resetEditorHeight = () => {
editorHeight.value = sizeBounds.value.default;
};
const toggleEditorExpand = () => {
editorHeight.value = clampToBounds(editorHeight.value);
measureSurroundingHeight();
const { expanded, max, default: defaultHeight } = sizeBounds.value;
const isExpanded = editorHeight.value > defaultHeight;
// If expanded is too close to default, use max so the toggle is always noticeable
const target = expanded - defaultHeight < 100 ? max : expanded;
editorHeight.value = isExpanded ? defaultHeight : target;
};
const handleMessageSent = () => {
clearTimeout(resetTimeoutId);
resetTimeoutId = setTimeout(resetEditorHeight, RESET_DELAY_MS);
};
onMounted(() => {
emitter.on(BUS_EVENTS.MESSAGE_SENT, handleMessageSent);
});
onBeforeUnmount(() => {
emitter.off(BUS_EVENTS.MESSAGE_SENT, handleMessageSent);
clearTimeout(resetTimeoutId);
if (isResizing.value) {
isResizing.value = false;
clearDragStyles();
}
});
useEventListener(document, 'mousemove', onResizeMove);
useEventListener(document, 'mouseup', onResizeEnd);
useEventListener(document, 'touchmove', onResizeMove, { passive: false });
useEventListener(document, 'touchend', onResizeEnd);
useEventListener(document, 'touchcancel', onResizeEnd);
useEventListener(window, 'blur', onResizeEnd);
defineExpose({ toggleEditorExpand, resetEditorHeight });
</script>
<template>
<div
ref="wrapperRef"
class="relative resizable-editor-wrapper"
:style="{
'--editor-height': editorHeight + 'px',
'--editor-min-allowed': sizeBounds.min + 'px',
'--editor-max-allowed': sizeBounds.max + 'px',
'--editor-height-transition': isResizing ? 'none' : '180ms ease',
}"
>
<div
class="group absolute inset-x-0 -top-4 z-10 flex h-4 cursor-row-resize select-none items-center justify-center bg-gradient-to-b from-transparent from-10% dark:to-n-surface-1/80 to-n-surface-1/90 backdrop-blur-[0.01875rem]"
@mousedown="onResizeStart"
@touchstart.prevent="onResizeStart"
@dblclick="resetEditorHeight"
>
<div
class="w-8 h-0.5 mt-1 rounded-full bg-n-slate-6 group-hover:bg-n-slate-8 transition-all duration-200 motion-safe:group-hover:animate-bounce"
:class="{ 'bg-n-slate-8 animate-bounce': isResizing }"
/>
</div>
<slot />
</div>
</template>
@@ -92,9 +92,7 @@ describe('useAutomation', () => {
case 'assign_team':
return teams;
case 'assign_agent':
return options.addNoneToListFn
? options.addNoneToListFn(options.agents)
: options.agents;
return agents;
case 'send_email_to_team':
return teams;
case 'send_message':
@@ -242,11 +240,7 @@ describe('useAutomation', () => {
expect(getActionDropdownValues('add_label')).toEqual(labels);
expect(getActionDropdownValues('assign_team')).toEqual(teams);
expect(getActionDropdownValues('assign_agent')).toEqual([
{ id: 'nil', name: 'AUTOMATION.NONE_OPTION' },
{ id: 'last_responding_agent', name: 'AUTOMATION.LAST_RESPONDING_AGENT' },
...agents,
]);
expect(getActionDropdownValues('assign_agent')).toEqual(agents);
expect(getActionDropdownValues('send_email_to_team')).toEqual(teams);
expect(getActionDropdownValues('send_message')).toEqual([]);
expect(getActionDropdownValues('add_sla')).toEqual(slaPolicies);
@@ -16,20 +16,7 @@ describe('useEditableAutomation', () => {
return [];
}),
getActionDropdownValues: vi.fn(actionName => {
if (actionName === 'assign_agent') {
return [
{ id: 'nil', name: 'None' },
{
id: 'last_responding_agent',
name: 'Last Responding Agent',
},
{ id: 1, name: 'Agent 1' },
];
}
return [];
}),
getActionDropdownValues: vi.fn(),
});
});
@@ -64,35 +51,4 @@ describe('useEditableAutomation', () => {
},
]);
});
it('rehydrates last responding agent as a selected action option', () => {
const automation = {
event_name: 'conversation_created',
conditions: [],
actions: [
{
action_name: 'assign_agent',
action_params: ['last_responding_agent'],
},
],
};
const automationActionTypes = [
{ key: 'assign_agent', inputType: 'search_select' },
];
const { formatAutomation } = useEditableAutomation();
const result = formatAutomation(automation, [], {}, automationActionTypes);
expect(result.actions).toEqual([
{
action_name: 'assign_agent',
action_params: [
{
id: 'last_responding_agent',
name: 'Last Responding Agent',
},
],
},
]);
});
});
@@ -119,30 +119,24 @@ describe('useMacros', () => {
const { getMacroDropdownValues } = useMacros();
expect(getMacroDropdownValues('add_label')).toHaveLength(mockLabels.length);
expect(getMacroDropdownValues('assign_team')).toHaveLength(
mockTeams.length + 1
); // +1 for "None"
mockTeams.length
);
expect(getMacroDropdownValues('assign_agent')).toHaveLength(
mockAgents.length + 2
); // +2 for "None" and "Self"
mockAgents.length + 1
); // +1 for "Self"
});
it('returns teams with "None" option for assign_team and teams only for send_email_to_team', () => {
it('returns teams for assign_team and send_email_to_team types', () => {
const { getMacroDropdownValues } = useMacros();
const assignTeamResult = getMacroDropdownValues('assign_team');
expect(assignTeamResult[0]).toEqual({
id: 'nil',
name: 'AUTOMATION.NONE_OPTION',
});
expect(assignTeamResult.slice(1)).toEqual(mockTeams);
expect(getMacroDropdownValues('assign_team')).toEqual(mockTeams);
expect(getMacroDropdownValues('send_email_to_team')).toEqual(mockTeams);
});
it('returns agents with "None" and "Self" options for assign_agent type', () => {
it('returns agents with "Self" option for assign_agent type', () => {
const { getMacroDropdownValues } = useMacros();
const result = getMacroDropdownValues('assign_agent');
expect(result[0]).toEqual({ id: 'nil', name: 'AUTOMATION.NONE_OPTION' });
expect(result[1]).toEqual({ id: 'self', name: 'Self' });
expect(result.slice(2)).toEqual(mockAgents);
expect(result[0]).toEqual({ id: 'self', name: 'Self' });
expect(result.slice(1)).toEqual(mockAgents);
});
it('returns formatted labels for add_label and remove_label types', () => {
@@ -178,11 +172,8 @@ describe('useMacros', () => {
const { getMacroDropdownValues } = useMacros();
expect(getMacroDropdownValues('add_label')).toEqual([]);
expect(getMacroDropdownValues('assign_team')).toEqual([
{ id: 'nil', name: 'AUTOMATION.NONE_OPTION' },
]);
expect(getMacroDropdownValues('assign_team')).toEqual([]);
expect(getMacroDropdownValues('assign_agent')).toEqual([
{ id: 'nil', name: 'AUTOMATION.NONE_OPTION' },
{ id: 'self', name: 'Self' },
]);
});
@@ -121,19 +121,8 @@ export default function useAutomationValues() {
* @returns {Array} An array of action dropdown values.
*/
const getActionDropdownValues = type => {
let agentsList = agents.value;
if (type === 'assign_agent') {
agentsList = [
{
id: 'last_responding_agent',
name: t('AUTOMATION.LAST_RESPONDING_AGENT'),
},
...agentsList,
];
}
return getActionOptions({
agents: agentsList,
agents: agents.value,
labels: labels.value,
teams: teams.value,
slaPolicies: slaPolicies.value,
@@ -12,14 +12,11 @@ import { useKeyboardEvents } from './useKeyboardEvents';
/**
* Wrap the action in a function that calls the action and prevents the default event behavior.
* Only prevents default when items are available to navigate.
* @param {Function} action - The action to be called.
* @param {import('vue').Ref<Array>} items - A ref to the array of selectable items.
* @returns {{action: Function, allowOnFocusedInput: boolean}} An object containing the action and a flag to allow the event on focused input.
*/
const createAction = (action, items) => ({
const createAction = action => ({
action: e => {
if (!items.value?.length) return;
action();
e.preventDefault();
},
@@ -41,14 +38,15 @@ const createKeyboardEvents = (
items
) => {
const events = {
ArrowUp: createAction(moveSelectionUp, items),
'Control+KeyP': createAction(moveSelectionUp, items),
ArrowDown: createAction(moveSelectionDown, items),
'Control+KeyN': createAction(moveSelectionDown, items),
ArrowUp: createAction(moveSelectionUp),
'Control+KeyP': createAction(moveSelectionUp),
ArrowDown: createAction(moveSelectionDown),
'Control+KeyN': createAction(moveSelectionDown),
};
// Adds an event handler for the Enter key if the onSelect function is provided.
if (typeof onSelect === 'function') {
events.Enter = createAction(onSelect, items);
events.Enter = createAction(() => items.value?.length > 0 && onSelect());
}
return events;
@@ -15,11 +15,6 @@ export const useMacros = () => {
const teams = computed(() => getters['teams/getTeams'].value);
const agents = computed(() => getters['agents/getVerifiedAgents'].value);
const withNoneOption = options => [
{ id: 'nil', name: t('AUTOMATION.NONE_OPTION') },
...(options || []),
];
/**
* Get dropdown values based on the specified type
* @param {string} type - The type of dropdown values to retrieve
@@ -28,15 +23,10 @@ export const useMacros = () => {
const getMacroDropdownValues = type => {
switch (type) {
case 'assign_team':
return withNoneOption(teams.value);
case 'send_email_to_team':
return teams.value;
case 'assign_agent':
return [
...withNoneOption(),
{ id: 'self', name: 'Self' },
...agents.value,
];
return [{ id: 'self', name: 'Self' }, ...agents.value];
case 'add_label':
case 'remove_label':
return labels.value.map(i => ({
@@ -172,7 +172,6 @@ export const FORMATTING = {
export const ARTICLE_EDITOR_MENU_OPTIONS = [
'strong',
'em',
'strike',
'link',
'undo',
'redo',
@@ -183,7 +182,6 @@ export const ARTICLE_EDITOR_MENU_OPTIONS = [
'h3',
'imageUpload',
'code',
'insertTable',
];
/**
@@ -12,11 +12,6 @@ export default {
SNOOZED: 'snoozed',
ALL: 'all',
},
CONVERSATION_TYPE: {
MENTION: 'mention',
PARTICIPATING: 'participating',
UNATTENDED: 'unattended',
},
SORT_BY_TYPE: {
LAST_ACTIVITY_AT_ASC: 'last_activity_at_asc',
LAST_ACTIVITY_AT_DESC: 'last_activity_at_desc',
@@ -51,7 +51,6 @@ export const conversationListPageURL = ({
} else if (conversationType) {
const urlMap = {
mention: 'mentions/conversations',
participating: 'participating/conversations',
unattended: 'unattended/conversations',
};
url = `accounts/${accountId}/${urlMap[conversationType]}`;
@@ -40,15 +40,6 @@ describe('#URL Helpers', () => {
'/app/accounts/1/custom_view/1'
);
});
it('should return url to participating conversations', () => {
expect(
conversationListPageURL({
accountId: 1,
conversationType: 'participating',
})
).toBe('/app/accounts/1/participating/conversations');
});
});
describe('conversationUrl', () => {
it('should return direct conversation URL if activeInbox is nil', () => {
@@ -45,10 +45,6 @@ describe('#resolveTeamIds', () => {
const resolvedTeams = '⚙️ sales team, 🤷‍♂️ fayaz';
expect(resolveTeamIds(teams, [1, 2])).toEqual(resolvedTeams);
});
it('resolves nil as None', () => {
expect(resolveTeamIds(teams, ['nil'])).toEqual('None');
});
});
describe('#resolveLabels', () => {
@@ -63,10 +59,6 @@ describe('#resolveAgents', () => {
const resolvedAgents = 'John Doe';
expect(resolveAgents(agents, [1])).toEqual(resolvedAgents);
});
it('resolves nil and self values', () => {
expect(resolveAgents(agents, ['nil', 'self'])).toEqual('None, Self');
});
});
describe('#getFileName', () => {
@@ -125,7 +125,6 @@ const validateSingleAction = action => {
'mute_conversation',
'snooze_conversation',
'resolve_conversation',
'remove_assigned_agent',
'remove_assigned_team',
'open_conversation',
'pending_conversation',
@@ -63,16 +63,6 @@
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "ምስጢሩን ወደ ክሊፕቦርድ ቅዳ",
"COPY_SUCCESS": "ምስጢሩ ወደ ክሊፕቦርድ ተቀድሷል",
"TOGGLE": "የምስጢሩን ማየት አሳይ/ደብቅ",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "ተጠናቀቀ",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "Access Token",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,8 +140,6 @@
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
@@ -171,7 +169,6 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"PRIVATE_NOTE": "የግል ማስታወሻ",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Email",
"INBOX": "Inbox",
@@ -52,17 +52,5 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,7 +20,6 @@
"CALL": "ደውል",
"CALL_INITIATED": "ለእውነተኛው እውቀት መደወል እየተከናወነ ነው…",
"CALL_FAILED": "ጥሪውን መጀመር አልቻልንም። እባክዎ ደግመው ይሞክሩ።.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "የድምፅ ኢንቦክስ ይምረጡ"
},
@@ -86,14 +86,6 @@
"PLACEHOLDER": "እባክዎ የWebhook URLዎን ያስገቡ",
"ERROR": "እባክዎ ትክክለኛ አድራሻ ያስገቡ"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "ምስጢሩን ወደ ክሊፕቦርድ ቅዳ",
"COPY_SUCCESS": "ምስጢሩ ወደ ክሊፕቦርድ ተቀድሷል",
"TOGGLE": "የምስጢሩን ማየት አሳይ/ደብቅ",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "የድር ጣቢያ ዶሜን",
"PLACEHOLDER": "የድር ጣቢያዎን ዶሜን ያስገቡ (ለምሳሌ፡ acme.com)"
@@ -837,18 +837,6 @@
"SUCCESS_MESSAGE": "ብልጽግና ተጠቃሚ መሣሪያ ተሰርዟል",
"ERROR_MESSAGE": "ብልጽግና መሣሪያውን ማስወገድ አልተሳካም"
},
"PAYWALL": {
"TITLE": "Upgrade to use tools with Captain",
"AVAILABLE_ON": "Captain Tools are only available in Business and Enterprise plans. Please upgrade to Business plan to use the feature.",
"UPGRADE_PROMPT": "",
"UPGRADE_NOW": "Open billing",
"CANCEL_ANYTIME": ""
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain Tools are only available in the paid plans.",
"UPGRADE_PROMPT": "Please upgrade to a paid plan to use this feature.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"TEST": {
"BUTTON": "Test connection",
"SUCCESS": "Endpoint returned HTTP {status}",
@@ -93,7 +93,6 @@
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Mute Conversation",
@@ -68,8 +68,7 @@
"API_SUCCESS": "ፊርማው በተሳካ ሁኔታ ተቀምጧል",
"IMAGE_UPLOAD_ERROR": "ምስሉን ማስገባት አልተቻለም! እባክዎ እንደገና ይሞክሩ",
"IMAGE_UPLOAD_SUCCESS": "ምስሉ በተሳካ ሁኔታ ታክሏል። እባክዎ ማስቀመጫ ለማስቀመጥ ላይ ይጫኑ",
"IMAGE_UPLOAD_SIZE_ERROR": "የምስል መጠን ከ{size}MB በታች መሆን አለበት",
"INLINE_IMAGE_WARNING": "Pasted inline images were removed. Please use the image upload button to add images to your signature."
"IMAGE_UPLOAD_SIZE_ERROR": "የምስል መጠን ከ{size}MB በታች መሆን አለበት"
},
"MESSAGE_SIGNATURE": {
"LABEL": "የመልእክት ፊርማ",
@@ -45,13 +45,6 @@
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"SUBMIT": "Create account",
"HAVE_AN_ACCOUNT": "Already have an account?",
"VERIFY_EMAIL": {
"TITLE": "Check your inbox",
"DESCRIPTION": "We sent a verification link to {email}. Click the link to verify your email and get started.",
"RESEND": "የማረጋገጫ ኢሜል እንደገና ላክ",
"RESEND_SUCCESS": "Verification email sent. Please check your inbox.",
"RESEND_ERROR": "Could not send verification email. Please try again."
}
"HAVE_AN_ACCOUNT": "Already have an account?"
}
}
@@ -63,16 +63,6 @@
"ERROR_MESSAGE": "تعذر تحديث الروبوت. يرجى المحاولة مرة أخرى."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "Done",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "رمز المصادقة",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,8 +140,6 @@
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
@@ -171,7 +169,6 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"PRIVATE_NOTE": "إضافة ملاحظة خاصة",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "البريد الإلكتروني",
"INBOX": "صندوق الوارد",
@@ -52,17 +52,5 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,7 +20,6 @@
"CALL": "Call",
"CALL_INITIATED": "جار الاتصال بجهة الاتصال…",
"CALL_FAILED": "تعذر بدء المكالمة. الرجاء المحاولة مرة أخرى.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -86,14 +86,6 @@
"PLACEHOLDER": "Please enter your Webhook URL",
"ERROR": "الرجاء إدخال عنوان URL صالح"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "نطاق الموقع",
"PLACEHOLDER": "أدخل نطاق موقعك الإلكتروني (مثال: acme.com)"
@@ -837,18 +837,6 @@
"SUCCESS_MESSAGE": "Custom tool deleted successfully",
"ERROR_MESSAGE": "Failed to delete custom tool"
},
"PAYWALL": {
"TITLE": "Upgrade to use tools with Captain",
"AVAILABLE_ON": "Captain Tools are only available in Business and Enterprise plans. Please upgrade to Business plan to use the feature.",
"UPGRADE_PROMPT": "",
"UPGRADE_NOW": "فتح الفواتير",
"CANCEL_ANYTIME": ""
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain Tools are only available in the paid plans.",
"UPGRADE_PROMPT": "Please upgrade to a paid plan to use this feature.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"TEST": {
"BUTTON": "Test connection",
"SUCCESS": "Endpoint returned HTTP {status}",
@@ -93,7 +93,6 @@
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "كتم المحادثة",
@@ -68,8 +68,7 @@
"API_SUCCESS": "تم حفظ التوقيع بنجاح",
"IMAGE_UPLOAD_ERROR": "تعذر رفع الصورة! حاول مرة أخرى",
"IMAGE_UPLOAD_SUCCESS": "تم إضافة الصورة بنجاح. الرجاء النقر على الحفظ لحفظ التوقيع",
"IMAGE_UPLOAD_SIZE_ERROR": "حجم الصورة يجب أن يكون أقل من {size}MB",
"INLINE_IMAGE_WARNING": "Pasted inline images were removed. Please use the image upload button to add images to your signature."
"IMAGE_UPLOAD_SIZE_ERROR": "حجم الصورة يجب أن يكون أقل من {size}MB"
},
"MESSAGE_SIGNATURE": {
"LABEL": "توقيع الرسالة",
@@ -45,13 +45,6 @@
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
},
"SUBMIT": "إرسال",
"HAVE_AN_ACCOUNT": "هل لديك حساب مسبق؟",
"VERIFY_EMAIL": {
"TITLE": "Check your inbox",
"DESCRIPTION": "We sent a verification link to {email}. Click the link to verify your email and get started.",
"RESEND": "إعادة إرسال رسالة التحقق",
"RESEND_SUCCESS": "Verification email sent. Please check your inbox.",
"RESEND_ERROR": "Could not send verification email. Please try again."
}
"HAVE_AN_ACCOUNT": "هل لديك حساب مسبق؟"
}
}
@@ -63,16 +63,6 @@
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "Hazır",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "Access Token",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,8 +140,6 @@
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
@@ -171,7 +169,6 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"PRIVATE_NOTE": "Şəxsi Qeyd",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Email",
"INBOX": "Inbox",
@@ -52,17 +52,5 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,7 +20,6 @@
"CALL": "Zəng et",
"CALL_INITIATED": "Calling the contact…",
"CALL_FAILED": "Unable to start the call. Please try again.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -698,7 +698,7 @@
"EDIT_ARTICLE": {
"MORE_PROPERTIES": "More properties",
"UNCATEGORIZED": "Uncategorized",
"EDITOR_PLACEHOLDER": "Write your content here. Type '/' for formatting options."
"EDITOR_PLACEHOLDER": "Write something..."
},
"ARTICLE_PROPERTIES": {
"ARTICLE_PROPERTIES": "Article properties",
@@ -86,14 +86,6 @@
"PLACEHOLDER": "Zəhmət olmasa Webhook URL-ni daxil edin",
"ERROR": "Zəhmət olmasa düzgün URL daxil edin"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "Veb sayt Domeni",
"PLACEHOLDER": "Veb sayt domeninizi daxil edin (məsələn: acme.com)"
@@ -837,18 +837,6 @@
"SUCCESS_MESSAGE": "Xüsusi alət uğurla silindi",
"ERROR_MESSAGE": "Xüsusi alət silinmədi"
},
"PAYWALL": {
"TITLE": "Upgrade to use tools with Captain",
"AVAILABLE_ON": "Captain Tools are only available in Business and Enterprise plans. Please upgrade to Business plan to use the feature.",
"UPGRADE_PROMPT": "",
"UPGRADE_NOW": "Open billing",
"CANCEL_ANYTIME": ""
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain Tools are only available in the paid plans.",
"UPGRADE_PROMPT": "Please upgrade to a paid plan to use this feature.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"TEST": {
"BUTTON": "Bağlantını yoxla",
"SUCCESS": "Endpoint HTTP {status} qaytardı",
@@ -93,7 +93,6 @@
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Mute Conversation",
@@ -68,8 +68,7 @@
"API_SUCCESS": "İmza uğurla yadda saxlanıldı",
"IMAGE_UPLOAD_ERROR": "Şəkil yüklənmədi! Yenidən cəhd edin",
"IMAGE_UPLOAD_SUCCESS": "Şəkil uğurla əlavə edildi. İmzanı saxlamaq üçün zəhmət olmasa Saxla düyməsini basın",
"IMAGE_UPLOAD_SIZE_ERROR": "Şəkil ölçüsü {size}MB-dan az olmalıdır",
"INLINE_IMAGE_WARNING": "Pasted inline images were removed. Please use the image upload button to add images to your signature."
"IMAGE_UPLOAD_SIZE_ERROR": "Şəkil ölçüsü {size}MB-dan az olmalıdır"
},
"MESSAGE_SIGNATURE": {
"LABEL": "Mesaj imzası",
@@ -45,13 +45,6 @@
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"SUBMIT": "Create account",
"HAVE_AN_ACCOUNT": "Already have an account?",
"VERIFY_EMAIL": {
"TITLE": "Check your inbox",
"DESCRIPTION": "We sent a verification link to {email}. Click the link to verify your email and get started.",
"RESEND": "Təsdiq e-poçtunu yenidən göndər",
"RESEND_SUCCESS": "Verification email sent. Please check your inbox.",
"RESEND_ERROR": "Could not send verification email. Please try again."
}
"HAVE_AN_ACCOUNT": "Already have an account?"
}
}
@@ -63,16 +63,6 @@
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "Done",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "Access Token",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,8 +140,6 @@
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
@@ -171,7 +169,6 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"PRIVATE_NOTE": "Private Note",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Email",
"INBOX": "Входяща кутия",
@@ -52,17 +52,5 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,7 +20,6 @@
"CALL": "Call",
"CALL_INITIATED": "Calling the contact…",
"CALL_FAILED": "Unable to start the call. Please try again.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -86,14 +86,6 @@
"PLACEHOLDER": "Please enter your Webhook URL",
"ERROR": "Please enter a valid URL"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "Website Domain",
"PLACEHOLDER": "Enter your website domain (eg: acme.com)"
@@ -837,18 +837,6 @@
"SUCCESS_MESSAGE": "Custom tool deleted successfully",
"ERROR_MESSAGE": "Failed to delete custom tool"
},
"PAYWALL": {
"TITLE": "Upgrade to use tools with Captain",
"AVAILABLE_ON": "Captain Tools are only available in Business and Enterprise plans. Please upgrade to Business plan to use the feature.",
"UPGRADE_PROMPT": "",
"UPGRADE_NOW": "Open billing",
"CANCEL_ANYTIME": ""
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain Tools are only available in the paid plans.",
"UPGRADE_PROMPT": "Please upgrade to a paid plan to use this feature.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"TEST": {
"BUTTON": "Test connection",
"SUCCESS": "Endpoint returned HTTP {status}",
@@ -93,7 +93,6 @@
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Заглушаване на разговора",
@@ -68,8 +68,7 @@
"API_SUCCESS": "Signature saved successfully",
"IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again",
"IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature",
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB",
"INLINE_IMAGE_WARNING": "Pasted inline images were removed. Please use the image upload button to add images to your signature."
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB"
},
"MESSAGE_SIGNATURE": {
"LABEL": "Message Signature",
@@ -45,13 +45,6 @@
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно"
},
"SUBMIT": "Create account",
"HAVE_AN_ACCOUNT": "Already have an account?",
"VERIFY_EMAIL": {
"TITLE": "Check your inbox",
"DESCRIPTION": "We sent a verification link to {email}. Click the link to verify your email and get started.",
"RESEND": "Resend verification email",
"RESEND_SUCCESS": "Verification email sent. Please check your inbox.",
"RESEND_ERROR": "Could not send verification email. Please try again."
}
"HAVE_AN_ACCOUNT": "Already have an account?"
}
}
@@ -63,16 +63,6 @@
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "গোপন কোড ক্লিপবোর্ডে কপি করুন",
"COPY_SUCCESS": "গোপন কোড ক্লিপবোর্ডে কপি হয়েছে",
"TOGGLE": "গোপন কোডের দৃশ্যমানতা টগল করুন",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "সম্পন্ন",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "Access Token",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,8 +140,6 @@
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
@@ -171,7 +169,6 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"PRIVATE_NOTE": "ব্যক্তিগত নোট",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Email",
"INBOX": "Inbox",
@@ -52,17 +52,5 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,7 +20,6 @@
"CALL": "Call",
"CALL_INITIATED": "Calling the contact…",
"CALL_FAILED": "Unable to start the call. Please try again.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -86,14 +86,6 @@
"PLACEHOLDER": "অনুগ্রহ করে আপনার Webhook URL লিখুন",
"ERROR": "অনুগ্রহ করে একটি বৈধ URL দিন"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "গোপন কোড ক্লিপবোর্ডে কপি করুন",
"COPY_SUCCESS": "গোপন কোড ক্লিপবোর্ডে কপি হয়েছে",
"TOGGLE": "গোপন কোডের দৃশ্যমানতা টগল করুন",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "ওয়েবসাইট ডোমেইন",
"PLACEHOLDER": "আপনার ওয়েবসাইট ডোমেইন লিখুন (যেমন: acme.com)"
@@ -837,18 +837,6 @@
"SUCCESS_MESSAGE": "কাস্টম টুল সফলভাবে মুছে ফেলা হয়েছে",
"ERROR_MESSAGE": "কাস্টম টুল মুছে ফেলা যায়নি"
},
"PAYWALL": {
"TITLE": "Upgrade to use tools with Captain",
"AVAILABLE_ON": "Captain Tools are only available in Business and Enterprise plans. Please upgrade to Business plan to use the feature.",
"UPGRADE_PROMPT": "",
"UPGRADE_NOW": "Open billing",
"CANCEL_ANYTIME": ""
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain Tools are only available in the paid plans.",
"UPGRADE_PROMPT": "Please upgrade to a paid plan to use this feature.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"TEST": {
"BUTTON": "Test connection",
"SUCCESS": "Endpoint returned HTTP {status}",
@@ -93,7 +93,6 @@
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Mute Conversation",
@@ -68,8 +68,7 @@
"API_SUCCESS": "স্বাক্ষর সফলভাবে সংরক্ষণ হয়েছে",
"IMAGE_UPLOAD_ERROR": "ছবি আপলোড করা সম্ভব হয়নি! আবার চেষ্টা করুন",
"IMAGE_UPLOAD_SUCCESS": "ছবি সফলভাবে যোগ হয়েছে। স্বাক্ষর সংরক্ষণ করতে অনুগ্রহ করে সেভ-এ ক্লিক করুন।",
"IMAGE_UPLOAD_SIZE_ERROR": "ছবির আকার {size}MB-এর কম হতে হবে",
"INLINE_IMAGE_WARNING": "Pasted inline images were removed. Please use the image upload button to add images to your signature."
"IMAGE_UPLOAD_SIZE_ERROR": "ছবির আকার {size}MB-এর কম হতে হবে"
},
"MESSAGE_SIGNATURE": {
"LABEL": "বার্তার স্বাক্ষর",
@@ -45,13 +45,6 @@
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"SUBMIT": "Create account",
"HAVE_AN_ACCOUNT": "Already have an account?",
"VERIFY_EMAIL": {
"TITLE": "Check your inbox",
"DESCRIPTION": "We sent a verification link to {email}. Click the link to verify your email and get started.",
"RESEND": "যাচাইকরণ ইমেল পুনরায় পাঠান",
"RESEND_SUCCESS": "Verification email sent. Please check your inbox.",
"RESEND_ERROR": "Could not send verification email. Please try again."
}
"HAVE_AN_ACCOUNT": "Already have an account?"
}
}
@@ -63,16 +63,6 @@
"ERROR_MESSAGE": "No s'ha pogut actualitzar el bot. Torneu-ho a provar."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "Done",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "Token d'accés",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,8 +140,6 @@
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
@@ -171,7 +169,6 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"PRIVATE_NOTE": "Nota privada",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Correu electrònic",
"INBOX": "Safata d'entrada",
@@ -52,17 +52,5 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,7 +20,6 @@
"CALL": "Call",
"CALL_INITIATED": "Calling the contact…",
"CALL_FAILED": "Unable to start the call. Please try again.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -86,14 +86,6 @@
"PLACEHOLDER": "Introdueix l'URL del teu webhook",
"ERROR": "Introduïu una URL vàlid"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "Domini del lloc web",
"PLACEHOLDER": "Introduïu el vostre domini de lloc web (pe: acme.com)"
@@ -837,18 +837,6 @@
"SUCCESS_MESSAGE": "Custom tool deleted successfully",
"ERROR_MESSAGE": "Failed to delete custom tool"
},
"PAYWALL": {
"TITLE": "Upgrade to use tools with Captain",
"AVAILABLE_ON": "Captain Tools are only available in Business and Enterprise plans. Please upgrade to Business plan to use the feature.",
"UPGRADE_PROMPT": "",
"UPGRADE_NOW": "Obrir facturació",
"CANCEL_ANYTIME": ""
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain Tools are only available in the paid plans.",
"UPGRADE_PROMPT": "Please upgrade to a paid plan to use this feature.",
"ASK_ADMIN": "Posa't en contacte amb el vostre administrador per obtenir l'actualització."
},
"TEST": {
"BUTTON": "Test connection",
"SUCCESS": "Endpoint returned HTTP {status}",
@@ -93,7 +93,6 @@
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Silencia la conversa",
@@ -68,8 +68,7 @@
"API_SUCCESS": "La signatura s'ha desat correctament",
"IMAGE_UPLOAD_ERROR": "No s'ha pogut carregar la imatge! Torna-ho a provar",
"IMAGE_UPLOAD_SUCCESS": "La imatge s'ha afegit correctament. Fes clic a desa per desar la signatura",
"IMAGE_UPLOAD_SIZE_ERROR": "La mida de la imatge ha de ser inferior a {size}MB",
"INLINE_IMAGE_WARNING": "Pasted inline images were removed. Please use the image upload button to add images to your signature."
"IMAGE_UPLOAD_SIZE_ERROR": "La mida de la imatge ha de ser inferior a {size}MB"
},
"MESSAGE_SIGNATURE": {
"LABEL": "Signatura del missatge",
@@ -45,13 +45,6 @@
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
},
"SUBMIT": "Crear un compte",
"HAVE_AN_ACCOUNT": "Ja tens un compte?",
"VERIFY_EMAIL": {
"TITLE": "Check your inbox",
"DESCRIPTION": "We sent a verification link to {email}. Click the link to verify your email and get started.",
"RESEND": "Reenviar correu electrònic de verificació",
"RESEND_SUCCESS": "Verification email sent. Please check your inbox.",
"RESEND_ERROR": "Could not send verification email. Please try again."
}
"HAVE_AN_ACCOUNT": "Ja tens un compte?"
}
}
@@ -63,16 +63,6 @@
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "Done",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "Přístupový token",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,8 +140,6 @@
"ACTIONS": {
"ASSIGN_AGENT": "Přiřadit agentovi",
"ASSIGN_TEAM": "Přiřadit tým",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Přidat štítek",
"REMOVE_LABEL": "Odebrat štítek",
"SEND_EMAIL_TO_TEAM": "Poslat e-mail týmu",
@@ -171,7 +169,6 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Typ zprávy",
"PRIVATE_NOTE": "Soukromá poznámka",
"MESSAGE_CONTAINS": "Zpráva obsahuje",
"EMAIL": "E-mailová adresa",
"INBOX": "Inbox",
@@ -52,17 +52,5 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,7 +20,6 @@
"CALL": "Call",
"CALL_INITIATED": "Calling the contact…",
"CALL_FAILED": "Unable to start the call. Please try again.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -86,14 +86,6 @@
"PLACEHOLDER": "Please enter your Webhook URL",
"ERROR": "Zadejte prosím platnou URL"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "Doména webových stránek",
"PLACEHOLDER": "Zadejte doménu webu (např. acme.com)"
@@ -837,18 +837,6 @@
"SUCCESS_MESSAGE": "Custom tool deleted successfully",
"ERROR_MESSAGE": "Failed to delete custom tool"
},
"PAYWALL": {
"TITLE": "Upgrade to use tools with Captain",
"AVAILABLE_ON": "Captain Tools are only available in Business and Enterprise plans. Please upgrade to Business plan to use the feature.",
"UPGRADE_PROMPT": "",
"UPGRADE_NOW": "Open billing",
"CANCEL_ANYTIME": ""
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain Tools are only available in the paid plans.",
"UPGRADE_PROMPT": "Please upgrade to a paid plan to use this feature.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"TEST": {
"BUTTON": "Test connection",
"SUCCESS": "Endpoint returned HTTP {status}",
@@ -93,7 +93,6 @@
"ASSIGN_AGENT": "Přiřadit agenta",
"ADD_LABEL": "Přidat štítek",
"REMOVE_LABEL": "Odebrat štítek",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Ztlumit konverzaci",
@@ -68,8 +68,7 @@
"API_SUCCESS": "Signature saved successfully",
"IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again",
"IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature",
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB",
"INLINE_IMAGE_WARNING": "Pasted inline images were removed. Please use the image upload button to add images to your signature."
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB"
},
"MESSAGE_SIGNATURE": {
"LABEL": "Message Signature",
@@ -45,13 +45,6 @@
"ERROR_MESSAGE": "Nelze se připojit k Woot serveru, opakujte akci později"
},
"SUBMIT": "Create account",
"HAVE_AN_ACCOUNT": "Máte již účet?",
"VERIFY_EMAIL": {
"TITLE": "Check your inbox",
"DESCRIPTION": "We sent a verification link to {email}. Click the link to verify your email and get started.",
"RESEND": "Resend verification email",
"RESEND_SUCCESS": "Verification email sent. Please check your inbox.",
"RESEND_ERROR": "Could not send verification email. Please try again."
}
"HAVE_AN_ACCOUNT": "Máte již účet?"
}
}
@@ -63,16 +63,6 @@
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "Done",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "Adgangs Token",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,8 +140,6 @@
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
@@ -171,7 +169,6 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"PRIVATE_NOTE": "Privat Note",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "E-mail",
"INBOX": "Indbakke",
@@ -52,17 +52,5 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,7 +20,6 @@
"CALL": "Call",
"CALL_INITIATED": "Calling the contact…",
"CALL_FAILED": "Unable to start the call. Please try again.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -86,14 +86,6 @@
"PLACEHOLDER": "Please enter your Webhook URL",
"ERROR": "Angiv en gyldig URL"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "Hjemmeside Domæne",
"PLACEHOLDER": "Indtast dit website domæne (fx: ditfirma.dk)"

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