mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-10-23 05:11:57 +00:00
feat: translate conversations, reply box and message components
This commit is contained in:
@@ -114,7 +114,7 @@
|
||||
<Editor
|
||||
v-model:htmlContent="action.value[0]"
|
||||
@update:htmlContent="(value) => handleEditorChange(value, index)"
|
||||
:placeholder="t('admin.macro.message_content.placeholder')"
|
||||
:placeholder="t('editor.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -75,7 +75,7 @@
|
||||
<!-- Plain text input -->
|
||||
<Input
|
||||
type="text"
|
||||
:placeholder="t('form.fields.setValue')"
|
||||
:placeholder="t('form.field.setValue')"
|
||||
v-if="inputType(index) === 'text'"
|
||||
v-model="rule.value"
|
||||
@update:modelValue="(value) => handleValueChange(value, index)"
|
||||
@@ -84,7 +84,7 @@
|
||||
<!-- Number input -->
|
||||
<Input
|
||||
type="number"
|
||||
:placeholder="t('form.fields.setValue')"
|
||||
:placeholder="t('form.field.setValue')"
|
||||
v-if="inputType(index) === 'number'"
|
||||
v-model="rule.value"
|
||||
@update:modelValue="(value) => handleValueChange(value, index)"
|
||||
|
@@ -19,7 +19,7 @@
|
||||
<Editor
|
||||
v-model:htmlContent="componentField.modelValue"
|
||||
@update:htmlContent="(value) => componentField.onChange(value)"
|
||||
:placeholder="t('admin.macro.message_content.placeholder')"
|
||||
:placeholder="t('editor.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="h-screen w-full flex items-center justify-center min-w-[400px]">
|
||||
<p>Select a conversation from the left panel.</p>
|
||||
<p>{{ $t('conversation.placeholder') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -2,17 +2,17 @@
|
||||
<Dialog :open="dialogOpen" @update:open="dialogOpen = false">
|
||||
<DialogContent class="max-w-5xl w-full h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>New Conversation</DialogTitle>
|
||||
<DialogTitle>{{ $t('conversation.newConversation') }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form @submit="createConversation" class="flex flex-col flex-1 overflow-hidden">
|
||||
<div class="flex-1 space-y-4 pr-1 overflow-y-auto pb-2">
|
||||
<FormField name="contact_email">
|
||||
<FormItem class="relative">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormLabel>{{ $t('form.field.email') }}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Search contact by email or type new email"
|
||||
:placeholder="t('conversation.searchContact')"
|
||||
v-model="emailQuery"
|
||||
@input="handleSearchContacts"
|
||||
autocomplete="off"
|
||||
@@ -38,9 +38,9 @@
|
||||
|
||||
<FormField v-slot="{ componentField }" name="first_name">
|
||||
<FormItem>
|
||||
<FormLabel>First Name</FormLabel>
|
||||
<FormLabel>{{ $t('form.field.firstName') }}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" placeholder="First Name" v-bind="componentField" required />
|
||||
<Input type="text" placeholder="" v-bind="componentField" required />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -48,9 +48,9 @@
|
||||
|
||||
<FormField v-slot="{ componentField }" name="last_name">
|
||||
<FormItem>
|
||||
<FormLabel>Last Name</FormLabel>
|
||||
<FormLabel>{{ $t('form.field.lastName') }}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" placeholder="Last Name" v-bind="componentField" required />
|
||||
<Input type="text" placeholder="" v-bind="componentField" required />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -58,9 +58,9 @@
|
||||
|
||||
<FormField v-slot="{ componentField }" name="subject">
|
||||
<FormItem>
|
||||
<FormLabel>Subject</FormLabel>
|
||||
<FormLabel>{{ $t('form.field.subject') }}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" placeholder="Subject" v-bind="componentField" required />
|
||||
<Input type="text" placeholder="" v-bind="componentField" required />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -68,11 +68,11 @@
|
||||
|
||||
<FormField v-slot="{ componentField }" name="inbox_id">
|
||||
<FormItem>
|
||||
<FormLabel>Inbox</FormLabel>
|
||||
<FormLabel>{{ $t('form.field.inbox') }}</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an inbox" />
|
||||
<SelectValue :placeholder="t('form.field.selectInbox')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -94,13 +94,12 @@
|
||||
<!-- Set assigned team -->
|
||||
<FormField v-slot="{ componentField }" name="team_id">
|
||||
<FormItem>
|
||||
<FormLabel>Assign team (optional)</FormLabel>
|
||||
<FormLabel>{{ $t('form.field.assignTeamOptional') }}</FormLabel>
|
||||
<FormControl>
|
||||
<ComboBox
|
||||
v-bind="componentField"
|
||||
:items="[{ value: 'none', label: 'None' }, ...teamStore.options]"
|
||||
placeholder="Search team"
|
||||
defaultLabel="Assign team"
|
||||
:placeholder="t('form.field.selectTeam')"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<div class="flex items-center gap-3 py-2">
|
||||
@@ -118,11 +117,13 @@
|
||||
</template>
|
||||
|
||||
<template #selected="{ selected }">
|
||||
<div class="flex items-center gap-3" v-if="selected">
|
||||
<div class="w-7 h-7 flex items-center justify-center">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-7 h-7 flex items-center justify-center" v-if="selected">
|
||||
{{ selected?.emoji }}
|
||||
</div>
|
||||
<span class="text-sm">{{ selected?.label || 'Select team' }}</span>
|
||||
<span class="text-sm">{{
|
||||
selected?.label || t('form.field.selectTeam')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ComboBox>
|
||||
@@ -134,13 +135,12 @@
|
||||
<!-- Set assigned agent -->
|
||||
<FormField v-slot="{ componentField }" name="agent_id">
|
||||
<FormItem>
|
||||
<FormLabel>Assign agent (optional)</FormLabel>
|
||||
<FormLabel>{{ $t('form.field.assignAgentOptional') }}</FormLabel>
|
||||
<FormControl>
|
||||
<ComboBox
|
||||
v-bind="componentField"
|
||||
:items="[{ value: 'none', label: 'None' }, ...uStore.options]"
|
||||
placeholder="Search agent"
|
||||
defaultLabel="Assign agent"
|
||||
:placeholder="t('form.field.selectAgent')"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<div class="flex items-center gap-3 py-2">
|
||||
@@ -176,7 +176,9 @@
|
||||
}}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span class="text-sm">{{ selected?.label || 'Assign agent' }}</span>
|
||||
<span class="text-sm">{{
|
||||
selected?.label || t('form.field.selectAgent')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ComboBox>
|
||||
@@ -191,13 +193,13 @@
|
||||
class="flex-1 min-h-0 flex flex-col"
|
||||
>
|
||||
<FormItem class="flex flex-col flex-1">
|
||||
<FormLabel>Message</FormLabel>
|
||||
<FormLabel>{{ $t('form.field.message') }}</FormLabel>
|
||||
<FormControl class="flex-1 min-h-0 flex flex-col">
|
||||
<div class="flex-1 min-h-0 flex flex-col">
|
||||
<Editor
|
||||
v-model:htmlContent="componentField.modelValue"
|
||||
@update:htmlContent="(value) => componentField.onChange(value)"
|
||||
:placeholder="'Shift + Enter to add new line'"
|
||||
:placeholder="t('editor.placeholder')"
|
||||
class="w-full flex-1 overflow-y-auto p-2 min-h-[200px] box"
|
||||
/>
|
||||
</div>
|
||||
@@ -208,7 +210,9 @@
|
||||
</div>
|
||||
|
||||
<DialogFooter class="mt-4 pt-2 border-t shrink-0">
|
||||
<Button type="submit" :disabled="loading" :isLoading="loading"> Submit </Button>
|
||||
<Button type="submit" :disabled="loading" :isLoading="loading">
|
||||
{{ $t('globals.buttons.submit') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
@@ -247,6 +251,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Editor from '@/features/conversation/ConversationTextEditor.vue'
|
||||
import api from '@/api'
|
||||
|
||||
@@ -256,6 +261,7 @@ const dialogOpen = defineModel({
|
||||
})
|
||||
|
||||
const inboxStore = useInboxStore()
|
||||
const { t } = useI18n()
|
||||
const uStore = useUsersStore()
|
||||
const teamStore = useTeamStore()
|
||||
const emitter = useEmitter()
|
||||
@@ -265,16 +271,26 @@ const emailQuery = ref('')
|
||||
let timeoutId = null
|
||||
|
||||
const formSchema = z.object({
|
||||
subject: z.string().min(3, 'Subject must be at least 3 characters'),
|
||||
content: z.string().min(1, 'Message cannot be empty'),
|
||||
subject: z.string().min(
|
||||
3,
|
||||
t('form.error.min', {
|
||||
min: 3
|
||||
})
|
||||
),
|
||||
content: z.string().min(
|
||||
1,
|
||||
t('globals.messages.cannotBeEmpty', {
|
||||
name: t('globals.entities.message')
|
||||
})
|
||||
),
|
||||
inbox_id: z.any().refine((val) => inboxStore.options.some((option) => option.value === val), {
|
||||
message: 'Inbox is required'
|
||||
message: t('globals.messages.required')
|
||||
}),
|
||||
team_id: z.any().optional(),
|
||||
agent_id: z.any().optional(),
|
||||
contact_email: z.string().email('Invalid email address'),
|
||||
first_name: z.string().min(1, 'First name is required'),
|
||||
last_name: z.string().min(1, 'Last name is required')
|
||||
contact_email: z.string().email(t('globals.messages.invalidEmailAddress')),
|
||||
first_name: z.string().min(1, t('globals.messages.required')),
|
||||
last_name: z.string().min(1, t('globals.messages.required'))
|
||||
})
|
||||
|
||||
const form = useForm({
|
||||
@@ -310,7 +326,6 @@ const handleSearchContacts = async () => {
|
||||
searchResults.value = [...resp.data.data]
|
||||
} catch (error) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
@@ -335,7 +350,6 @@ const createConversation = form.handleSubmit(async (values) => {
|
||||
emailQuery.value = ''
|
||||
} catch (error) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
|
@@ -40,6 +40,7 @@
|
||||
<script setup>
|
||||
import { X, Users, User, MessageSquare, Tags, Flag } from 'lucide-vue-next'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps({
|
||||
actions: {
|
||||
@@ -52,6 +53,7 @@ defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const getIcon = (type) =>
|
||||
({
|
||||
assign_team: Users,
|
||||
@@ -71,17 +73,17 @@ const getDisplayValue = (action) => {
|
||||
const getTooltip = (action) => {
|
||||
switch (action.type) {
|
||||
case 'assign_team':
|
||||
return `Assign to team: ${getDisplayValue(action)}`
|
||||
return `${t('globals.messages.assign_team')}: ${getDisplayValue(action)}`
|
||||
case 'assign_user':
|
||||
return `Assign to user: ${getDisplayValue(action)}`
|
||||
return `${t('globals.messages.assign_user')}: ${getDisplayValue(action)}`
|
||||
case 'set_status':
|
||||
return `Set status to: ${getDisplayValue(action)}`
|
||||
return `${t('globals.messages.set_status')}: ${getDisplayValue(action)}`
|
||||
case 'set_priority':
|
||||
return `Set priority to: ${getDisplayValue(action)}`
|
||||
return `${t('globals.messages.set_priority')}: ${getDisplayValue(action)}`
|
||||
case 'set_tags':
|
||||
return `Set tags: ${getDisplayValue(action)}`
|
||||
return `${t('globals.messages.set_tags')}: ${getDisplayValue(action)}`
|
||||
default:
|
||||
return `Action: ${action.type}, Value: ${getDisplayValue(action)}`
|
||||
return `${t('globals.entities.action')}: ${action.type}, ${t('globals.entities.value')}: ${getDisplayValue(action)}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@@ -2,18 +2,22 @@
|
||||
<Dialog :open="openAIKeyPrompt" @update:open="openAIKeyPrompt = false">
|
||||
<DialogContent class="sm:max-w-lg">
|
||||
<DialogHeader class="space-y-2">
|
||||
<DialogTitle>Enter OpenAI API Key</DialogTitle>
|
||||
<DialogTitle>{{ $t('ai.enterOpenAIAPIKey') }}</DialogTitle>
|
||||
<DialogDescription>
|
||||
OpenAI API key is not set or invalid. Please enter a valid API key to use AI features.
|
||||
{{
|
||||
$t('ai.apiKey.description', {
|
||||
provider: 'OpenAI'
|
||||
})
|
||||
}}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form v-slot="{ handleSubmit }" as="" keep-values :validation-schema="formSchema">
|
||||
<form id="apiKeyForm" @submit="handleSubmit($event, updateProvider)">
|
||||
<FormField v-slot="{ componentField }" name="apiKey">
|
||||
<FormItem>
|
||||
<FormLabel>API Key</FormLabel>
|
||||
<FormLabel>{{ $t('globals.entities.apiKey') }}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" placeholder="Enter your API key" v-bind="componentField" />
|
||||
<Input type="text" placeholder="sk-am1RLw7XUWGX.." v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -26,7 +30,7 @@
|
||||
:is-loading="isOpenAIKeyUpdating"
|
||||
:disabled="isOpenAIKeyUpdating"
|
||||
>
|
||||
Save
|
||||
{{ $t('globals.buttons.save') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
@@ -136,7 +140,7 @@ import { handleHTTPError } from '@/utils/http'
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import api from '@/api'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useConversationStore } from '@/stores/conversation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
@@ -167,6 +171,7 @@ const formSchema = toTypedSchema(
|
||||
})
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
const conversationStore = useConversationStore()
|
||||
const emitter = useEmitter()
|
||||
const userStore = useUserStore()
|
||||
@@ -205,7 +210,6 @@ const fetchAiPrompts = async () => {
|
||||
aiPrompts.value = resp.data.data
|
||||
} catch (error) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
@@ -234,7 +238,6 @@ const handleAiPromptSelected = async (key) => {
|
||||
openAIKeyPrompt.value = true
|
||||
}
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
@@ -251,12 +254,12 @@ const updateProvider = async (values) => {
|
||||
await api.updateAIProvider({ api_key: values.apiKey, provider: 'openai' })
|
||||
openAIKeyPrompt.value = false
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Success',
|
||||
description: 'API key saved successfully.'
|
||||
description: t('globals.messages.savedSuccessfully', {
|
||||
name: t('globals.entities.apiKey')
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
@@ -288,7 +291,6 @@ const handleFileUpload = (event) => {
|
||||
.catch((error) => {
|
||||
uploadingFiles.value = uploadingFiles.value.filter((f) => f.name !== file.name)
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
@@ -316,7 +318,6 @@ const handleInlineImageUpload = (event) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
@@ -398,7 +399,6 @@ const processSend = async () => {
|
||||
} catch (error) {
|
||||
hasAPIErrored = true
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
|
@@ -13,14 +13,14 @@
|
||||
class="px-3 py-1 rounded-lg transition-colors duration-200"
|
||||
:class="{ 'bg-background text-foreground': messageType === 'reply' }"
|
||||
>
|
||||
Reply
|
||||
{{ $t('replyBox.reply') }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="private_note"
|
||||
class="px-3 py-1 rounded-lg transition-colors duration-200"
|
||||
:class="{ 'bg-background text-foreground': messageType === 'private_note' }"
|
||||
>
|
||||
Private note
|
||||
{{ $t('replyBox.privateNote') }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
@@ -46,7 +46,7 @@
|
||||
<label class="w-12 text-sm font-medium text-muted-foreground">CC:</label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Email addresses separated by comma"
|
||||
:placeholder="t('replyBox.emailAddresess')"
|
||||
v-model="cc"
|
||||
class="flex-grow px-3 py-2 text-sm border rounded-md focus:ring-2 focus:ring-ring"
|
||||
@blur="validateEmails('cc')"
|
||||
@@ -63,7 +63,7 @@
|
||||
<label class="w-12 text-sm font-medium text-muted-foreground">BCC:</label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Email addresses separated by comma"
|
||||
:placeholder="t('replyBox.emailAddresess')"
|
||||
v-model="bcc"
|
||||
class="flex-grow px-3 py-2 text-sm border rounded-md focus:ring-2 focus:ring-ring"
|
||||
@blur="validateEmails('bcc')"
|
||||
@@ -146,6 +146,7 @@ import { useEmitter } from '@/composables/useEmitter'
|
||||
import AttachmentsPreview from '@/features/conversation/message/attachment/AttachmentsPreview.vue'
|
||||
import MacroActionsPreview from '@/features/conversation/MacroActionsPreview.vue'
|
||||
import ReplyBoxMenuBar from '@/features/conversation/ReplyBoxMenuBar.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// Define models for two-way binding
|
||||
const messageType = defineModel('messageType', { default: 'reply' })
|
||||
@@ -198,11 +199,11 @@ const emit = defineEmits([
|
||||
|
||||
const conversationStore = useConversationStore()
|
||||
const emitter = useEmitter()
|
||||
const { t } = useI18n()
|
||||
|
||||
const insertContent = ref(null)
|
||||
const setInlineImage = ref(null)
|
||||
const editorPlaceholder =
|
||||
'Shift + Enter to add a new line. Cmd + Enter to send. Cmd + K to open command bar.'
|
||||
const editorPlaceholder = t('replyBox.editor.placeholder')
|
||||
|
||||
const toggleBcc = async () => {
|
||||
showBcc.value = !showBcc.value
|
||||
@@ -255,13 +256,13 @@ const validateEmails = (field) => {
|
||||
|
||||
// Remove any existing errors for this field
|
||||
emailErrors.value = emailErrors.value.filter(
|
||||
(error) => !error.startsWith(`Invalid email(s) in ${field.toUpperCase()}`)
|
||||
(error) => !error.startsWith(`${t('replyBox.invalidEmailsIn')} ${field.toUpperCase()}`)
|
||||
)
|
||||
|
||||
// Add new error if there are invalid emails
|
||||
if (invalidEmails.length > 0) {
|
||||
emailErrors.value.push(
|
||||
`Invalid email(s) in ${field.toUpperCase()}: ${invalidEmails.join(', ')}`
|
||||
`${t('replyBox.invalidEmailsIn')} ${field.toUpperCase()}: ${invalidEmails.join(', ')}`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -274,9 +275,8 @@ const handleSend = async () => {
|
||||
validateEmails('bcc')
|
||||
if (emailErrors.value.length > 0) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: 'Please correct the email errors before sending.'
|
||||
description: t('replyBox.correctEmailErrors')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@@ -35,9 +35,9 @@
|
||||
<Smile class="h-4 w-4" />
|
||||
</Toggle>
|
||||
</div>
|
||||
<Button class="h-8 w-6 px-8" @click="handleSend" :disabled="!enableSend" :isLoading="isSending"
|
||||
>Send</Button
|
||||
>
|
||||
<Button class="h-8 w-6 px-8" @click="handleSend" :disabled="!enableSend" :isLoading="isSending">
|
||||
{{ $t('globals.buttons.send') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@@ -40,23 +40,27 @@
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem @click="handleSortChange('oldest')">Oldest activity</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleSortChange('newest')">Newest activity</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleSortChange('started_first')"
|
||||
>Started first</DropdownMenuItem
|
||||
>
|
||||
<DropdownMenuItem @click="handleSortChange('started_last')"
|
||||
>Started last</DropdownMenuItem
|
||||
>
|
||||
<DropdownMenuItem @click="handleSortChange('waiting_longest')"
|
||||
>Waiting longest</DropdownMenuItem
|
||||
>
|
||||
<DropdownMenuItem @click="handleSortChange('next_sla_target')"
|
||||
>Next SLA target</DropdownMenuItem
|
||||
>
|
||||
<DropdownMenuItem @click="handleSortChange('priority_first')"
|
||||
>Priority first</DropdownMenuItem
|
||||
>
|
||||
<DropdownMenuItem @click="handleSortChange('oldest')">
|
||||
{{ $t('conversation.sort.oldestActivity') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleSortChange('newest')">
|
||||
{{ $t('conversation.sort.newestActivity') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleSortChange('started_first')">
|
||||
{{ $t('conversation.sort.startedFirst') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleSortChange('started_last')">
|
||||
{{ $t('conversation.sort.startedLast') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleSortChange('waiting_longest')">
|
||||
{{ $t('conversation.sort.waitingLongest') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleSortChange('next_sla_target')">
|
||||
{{ $t('conversation.sort.nextSLATarget') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleSortChange('priority_first')">
|
||||
{{ $t('conversation.sort.priorityFirst') }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
@@ -67,8 +71,8 @@
|
||||
v-if="!hasConversations && !hasErrored && !isLoading"
|
||||
key="empty"
|
||||
class="px-4 py-8"
|
||||
title="No conversations found"
|
||||
message="Try adjusting filters"
|
||||
:title="t('conversation.noConversationsFound')"
|
||||
:message="t('conversation.tryAdjustingFilters')"
|
||||
:icon="MessageCircleQuestion"
|
||||
/>
|
||||
|
||||
@@ -77,7 +81,7 @@
|
||||
v-if="conversationStore.conversations.errorMessage"
|
||||
key="error"
|
||||
class="px-4 py-8"
|
||||
title="Could not fetch conversations"
|
||||
:title="t('conversation.couldNotFetch')"
|
||||
:message="conversationStore.conversations.errorMessage"
|
||||
:icon="MessageCircleWarning"
|
||||
/>
|
||||
@@ -126,13 +130,13 @@
|
||||
class="transition-all duration-200 ease-in-out transform hover:scale-105"
|
||||
>
|
||||
<Loader2 v-if="isLoading" class="mr-2 h-4 w-4 animate-spin" />
|
||||
{{ isLoading ? 'Loading...' : 'Load more' }}
|
||||
{{ isLoading ? t('globals.entities.loading') : t('globals.entities.loadMore') }}
|
||||
</Button>
|
||||
<p
|
||||
class="text-sm text-gray-500"
|
||||
v-else-if="conversationStore.conversationsList.length > 10"
|
||||
>
|
||||
All conversations loaded
|
||||
{{ $t('conversation.allLoaded') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,11 +158,13 @@ import { SidebarTrigger } from '@/components/ui/sidebar'
|
||||
import EmptyList from '@/features/conversation/list/ConversationEmptyList.vue'
|
||||
import ConversationListItem from '@/features/conversation/list/ConversationListItem.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ConversationListItemSkeleton from '@/features/conversation/list/ConversationListItemSkeleton.vue'
|
||||
|
||||
const conversationStore = useConversationStore()
|
||||
const route = useRoute()
|
||||
let reFetchInterval = ref(null)
|
||||
const { t } = useI18n()
|
||||
|
||||
const title = computed(() => {
|
||||
const typeValue = route.meta?.type?.(route)
|
||||
|
@@ -39,7 +39,7 @@
|
||||
@click="toggleQuote"
|
||||
class="text-xs cursor-pointer text-muted-foreground px-2 py-1 w-max hover:bg-muted hover:text-primary rounded-md transition-all"
|
||||
>
|
||||
{{ showQuotedText ? 'Hide quoted text' : 'Show quoted text' }}
|
||||
{{ showQuotedText ? t('conversation.hideQuotedText') : t('conversation.showQuotedText') }}
|
||||
</div>
|
||||
|
||||
<!-- Attachments -->
|
||||
@@ -73,6 +73,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Letter } from 'vue-letter'
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import MessageAttachmentPreview from '@/features/conversation/message/attachment/MessageAttachmentPreview.vue'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -82,6 +83,7 @@ const props = defineProps({
|
||||
const convStore = useConversationStore()
|
||||
const settingsStore = useAppSettingsStore()
|
||||
const showQuotedText = ref(false)
|
||||
const { t } = useI18n()
|
||||
|
||||
const getAvatar = computed(() => {
|
||||
return convStore.current?.contact?.avatar_url || ''
|
||||
|
@@ -16,7 +16,7 @@
|
||||
class="transition-all duration-200 hover:bg-gray-100 dark:hover:bg-gray-700 hover:scale-105 active:scale-95"
|
||||
>
|
||||
<RefreshCw size="17" class="mr-2" />
|
||||
Load more
|
||||
{{ $t('globals.entities.loadMore') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1 mb-5">
|
||||
<p class="font-medium">Subject</p>
|
||||
<p class="font-medium">{{ $t('form.field.subject') }}</p>
|
||||
<Skeleton v-if="conversationStore.conversation.loading" class="w-32 h-4" />
|
||||
<p v-else>
|
||||
{{ conversation.subject || '-' }}
|
||||
@@ -8,14 +8,14 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 mb-5">
|
||||
<p class="font-medium">Reference number</p>
|
||||
<p class="font-medium">{{ $t('form.field.referenceNumber') }}</p>
|
||||
<Skeleton v-if="conversationStore.conversation.loading" class="w-32 h-4" />
|
||||
<p v-else>
|
||||
{{ conversation.reference_number }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 mb-5">
|
||||
<p class="font-medium">Initiated at</p>
|
||||
<p class="font-medium">{{ $t('form.field.initiatedAt') }}</p>
|
||||
<Skeleton v-if="conversationStore.conversation.loading" class="w-32 h-4" />
|
||||
<p v-if="conversation.created_at">
|
||||
{{ format(conversation.created_at, 'PPpp') }}
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<div class="flex flex-col gap-1 mb-5">
|
||||
<div class="flex justify-start items-center space-x-2">
|
||||
<p class="font-medium">First reply at</p>
|
||||
<p class="font-medium">{{ $t('form.field.firstReplyAt') }}</p>
|
||||
<SlaBadge
|
||||
v-if="conversation.first_response_deadline_at"
|
||||
:dueAt="conversation.first_response_deadline_at"
|
||||
@@ -44,8 +44,8 @@
|
||||
|
||||
<div class="flex flex-col gap-1 mb-5">
|
||||
<div class="flex justify-start items-center space-x-2">
|
||||
<p class="font-medium">Resolved at</p>
|
||||
<SlaBadge
|
||||
<p class="font-medium">{{ $t('form.field.resolvedAt') }}</p>
|
||||
<SlaBadge
|
||||
v-if="conversation.resolution_deadline_at"
|
||||
:dueAt="conversation.resolution_deadline_at"
|
||||
:actualAt="conversation.resolved_at"
|
||||
@@ -62,7 +62,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 mb-5" v-if="conversation.closed_at">
|
||||
<p class="font-medium">Closed at</p>
|
||||
<p class="font-medium">{{ $t('form.field.closedAt') }}</p>
|
||||
<Skeleton v-if="conversationStore.conversation.loading" class="w-32 h-4" />
|
||||
<p v-else>
|
||||
{{ format(conversation.closed_at, 'PPpp') }}
|
||||
@@ -70,7 +70,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 mb-5">
|
||||
<p class="font-medium">SLA policy</p>
|
||||
<p class="font-medium">{{ $t('form.field.slaPolicy') }}</p>
|
||||
<Skeleton v-if="conversationStore.conversation.loading" class="w-32 h-4" />
|
||||
<div v-else>
|
||||
<p v-if="conversation.sla_policy_name">
|
||||
|
@@ -8,14 +8,13 @@
|
||||
>
|
||||
<AccordionItem value="Actions" class="border-0 mb-2">
|
||||
<AccordionTrigger class="bg-muted px-4 py-3 text-sm font-medium rounded-lg mx-2">
|
||||
Actions
|
||||
{{ $t('conversation.sidebar.action', 2) }}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent class="space-y-4 p-4">
|
||||
<ComboBox
|
||||
v-model="assignedUserID"
|
||||
:items="[{ value: 'none', label: 'None' }, ...usersStore.options]"
|
||||
placeholder="Search agent"
|
||||
defaultLabel="Assign agent"
|
||||
:placeholder="t('form.field.selectAgent')"
|
||||
@select="selectAgent"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
@@ -46,7 +45,7 @@
|
||||
}}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span class="text-sm">{{ selected?.label || 'Assign agent' }}</span>
|
||||
<span class="text-sm">{{ selected?.label || t('form.field.assignAgent') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ComboBox>
|
||||
@@ -54,8 +53,7 @@
|
||||
<ComboBox
|
||||
v-model="assignedTeamID"
|
||||
:items="[{ value: 'none', label: 'None' }, ...teamsStore.options]"
|
||||
placeholder="Search team"
|
||||
defaultLabel="Assign team"
|
||||
:placeholder="t('form.field.selectTeam')"
|
||||
@select="selectTeam"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
@@ -74,11 +72,11 @@
|
||||
</template>
|
||||
|
||||
<template #selected="{ selected }">
|
||||
<div class="flex items-center gap-3" v-if="selected">
|
||||
<div class="w-7 h-7 flex items-center justify-center">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-7 h-7 flex items-center justify-center" v-if="selected">
|
||||
{{ selected?.emoji }}
|
||||
</div>
|
||||
<span class="text-sm">{{ selected?.label || 'Select team' }}</span>
|
||||
<span class="text-sm">{{ selected?.label || t('form.field.assignTeam') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ComboBox>
|
||||
@@ -86,8 +84,7 @@
|
||||
<ComboBox
|
||||
v-model="priorityID"
|
||||
:items="priorityOptions"
|
||||
:defaultLabel="'Select priority'"
|
||||
placeholder="Select priority"
|
||||
:placeholder="t('form.field.selectPriority')"
|
||||
@select="selectPriority"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
@@ -109,7 +106,7 @@
|
||||
>
|
||||
<component :is="getPriorityIcon(selected?.value)" size="14" />
|
||||
</div>
|
||||
<span class="text-sm">{{ selected?.label || 'Select priority' }}</span>
|
||||
<span class="text-sm">{{ selected?.label || t('form.field.selectPriority') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ComboBox>
|
||||
@@ -118,14 +115,14 @@
|
||||
v-if="conversationStore.current"
|
||||
v-model="conversationStore.current.tags"
|
||||
:items="tags.map((tag) => ({ label: tag, value: tag }))"
|
||||
placeholder="Select tags"
|
||||
:placeholder="t('form.field.selectTag', 2)"
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="Information" class="border-0 mb-2">
|
||||
<AccordionTrigger class="bg-muted px-4 py-3 text-sm font-medium rounded-lg mx-2">
|
||||
Information
|
||||
{{ $t('conversation.sidebar.information') }}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent class="p-4">
|
||||
<ConversationInfo />
|
||||
@@ -134,7 +131,7 @@
|
||||
|
||||
<AccordionItem value="Previous conversations" class="border-0 mb-2">
|
||||
<AccordionTrigger class="bg-muted px-4 py-3 text-sm font-medium rounded-lg mx-2">
|
||||
Previous conversations
|
||||
{{ $t('conversation.sidebar.previousConvo') }}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent class="p-4">
|
||||
<div
|
||||
@@ -144,7 +141,7 @@
|
||||
"
|
||||
class="text-center text-sm text-muted-foreground py-4"
|
||||
>
|
||||
No previous conversations
|
||||
{{ $t('conversation.sidebar.noPreviousConvo') }}
|
||||
</div>
|
||||
<div v-else class="space-y-3">
|
||||
<router-link
|
||||
@@ -201,6 +198,7 @@ import { handleHTTPError } from '@/utils/http'
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
import { CircleAlert, SignalLow, SignalMedium, SignalHigh, Users } from 'lucide-vue-next'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import api from '@/api'
|
||||
|
||||
const emitter = useEmitter()
|
||||
@@ -208,6 +206,7 @@ const conversationStore = useConversationStore()
|
||||
const usersStore = useUsersStore()
|
||||
const teamsStore = useTeamStore()
|
||||
const tags = ref([])
|
||||
const { t } = useI18n()
|
||||
let isConversationChange = false
|
||||
|
||||
// Watch for changes in the current conversation and set the flag
|
||||
@@ -264,7 +263,6 @@ const fetchTags = async () => {
|
||||
tags.value = resp.data.data.map((item) => item.name)
|
||||
} catch (error) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Error',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
|
@@ -40,7 +40,7 @@
|
||||
<Skeleton class="w-32 h-4" />
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ conversation?.contact?.phone_number || 'Not available' }}
|
||||
{{ conversation?.contact?.phone_number || t('conversation.sidebar.notAvailable') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,8 +55,10 @@ import { useEmitter } from '@/composables/useEmitter'
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
||||
import { useConversationStore } from '@/stores/conversation'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const conversationStore = useConversationStore()
|
||||
const emitter = useEmitter()
|
||||
const conversation = computed(() => conversationStore.current)
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
@@ -91,7 +91,7 @@
|
||||
v-model="componentField.modelValue"
|
||||
@update:modelValue="handleChange"
|
||||
:items="conversationEventOptions"
|
||||
:placeholder="t('form.fields.selectEvents')"
|
||||
:placeholder="t('form.field.selectEvents')"
|
||||
>
|
||||
</SelectTag>
|
||||
</FormControl>
|
||||
|
77
i18n/en.json
77
i18n/en.json
@@ -39,6 +39,7 @@
|
||||
"globals.entities.macro": "Macro | Macros",
|
||||
"globals.entities.macroAction": "Macro Action | Macro Actions",
|
||||
"globals.entities.action": "Action | Actions",
|
||||
"globals.entities.value": "Value | Values",
|
||||
"globals.entities.event": "Event | Events",
|
||||
"globals.entities.automation": "Automation | Automations",
|
||||
"globals.entities.oidc": "OIDC | OIDCs",
|
||||
@@ -53,6 +54,9 @@
|
||||
"globals.entities.day": "Day | Days",
|
||||
"globals.entities.filter": "Filter | Filters",
|
||||
"globals.entities.profile": "Profile | Profiles",
|
||||
"globals.entities.apiKey": "API key | API keys",
|
||||
"globals.entities.loading": "Loading...",
|
||||
"globals.entities.loadMore": "Load more",
|
||||
"globals.messages.adjustFilters": "Try adjusting your filters",
|
||||
"globals.messages.errorUploadingFile": "Error uploading file",
|
||||
"globals.messages.errorUpdating": "Error updating {name}",
|
||||
@@ -112,6 +116,7 @@
|
||||
"globals.messages.somethingWentWrong": "Something went wrong",
|
||||
"globals.messages.done": "Done",
|
||||
"globals.messages.emptyState": "Nothing here",
|
||||
"globals.messages.cannotBeEmpty": "{name} cannot be empty",
|
||||
"globals.messages.pressEnterToSelectAValue": "Press enter to select a value",
|
||||
"globals.messages.caseSensitiveMatch": "Case sensitive match",
|
||||
"globals.messages.viewDetails": "View details",
|
||||
@@ -144,7 +149,6 @@
|
||||
"app.couldNotReload": "Could not reload {name}, Please restart the app",
|
||||
"app.badRequest": "Bad request",
|
||||
"email.invalidFromAddress": "Invalid from email address format, make sure it's a valid email address in the format `Name <mail@example.com>`",
|
||||
"ai.apiKeyNotSet": "{provider} API Key is not set, Please ask your administrator to set it up",
|
||||
"inbox.emptyIMAP": "Empty IMAP config",
|
||||
"inbox.emptySMTP": "Empty SMTP config",
|
||||
"template.defaultTemplateAlreadyExists": "Default template already exists",
|
||||
@@ -159,13 +163,6 @@
|
||||
"macro.partiallyApplied": "Macro partially applied",
|
||||
"macro.applied": "Macro applied",
|
||||
"sla.firstResponseTimeAfterResolution": "First response time cannot be after resolution time",
|
||||
"conversation.resolveWithoutAssignee": "Cannot resolve the conversation without an assigned user, Please assign a user before attempting to resolve",
|
||||
"conversation.notMemberOfTeam": "You're not a member of this team, Please refresh the page and try again",
|
||||
"conversation.viewPermissionDenied": "You do not have access to view this view",
|
||||
"conversation.errorGeneratingMessageID": "Error generating message ID",
|
||||
"conversation.invalidSnoozeDuration": "Invalid snooze duration",
|
||||
"conversation.errorUnassigningOpenConversations": "Error unassigning open conversations",
|
||||
"conversation.errorRemovingConversationAssignee": "Error removing conversation assignee",
|
||||
"conversationStatus.alreadyInUse": "Cannot delete status as it is in use, Please remove this status from all conversations before deleting",
|
||||
"conversationStatus.cannotUpdateDefault": "Cannot update default conversation status",
|
||||
"csat.alreadySubmitted": "CSAT already submitted",
|
||||
@@ -236,12 +233,19 @@
|
||||
"navigation.edit": "Edit",
|
||||
"navigation.delete": "Delete",
|
||||
"form.field.name": "Name",
|
||||
"form.field.inbox": "Inbox",
|
||||
"form.field.provider": "Provider",
|
||||
"form.field.providerURL": "Provider URL",
|
||||
"form.field.clientID": "Client ID",
|
||||
"form.field.clientSecret": "Client Secret",
|
||||
"form.field.callbackURL": "Callback URL",
|
||||
"form.field.subject": "Subject",
|
||||
"form.field.referenceNumber": "Reference Number",
|
||||
"form.field.initiatedAt": "Initiated at",
|
||||
"form.field.firstReplyAt": "First Reply At",
|
||||
"form.field.resolvedAt": "Resolved At",
|
||||
"form.field.closedAt": "Closed At",
|
||||
"form.field.slaPolicy": "SLA Policy",
|
||||
"form.field.isDefault": "Is default",
|
||||
"form.field.body": "Body",
|
||||
"form.field.default": "Default",
|
||||
@@ -276,15 +280,23 @@
|
||||
"form.field.selectRoles": "Select roles",
|
||||
"form.field.selectField": "Select field",
|
||||
"form.field.selectTeam": "Select team",
|
||||
"form.field.selectTag": "Select tag",
|
||||
"form.field.selectPriority": "Select priority",
|
||||
"form.field.selectTag": "Select tag | Select tags",
|
||||
"form.field.selectAction": "Select action",
|
||||
"form.field.selectProvider": "Select provider",
|
||||
"form.field.selectUser": "Select user",
|
||||
"form.field.selectAgent": "Select agent",
|
||||
"form.field.selectValue": "Select value",
|
||||
"form.field.selectTeams": "Select teams",
|
||||
"form.field.selectType": "Select type",
|
||||
"form.fields.setValue": "Set value",
|
||||
"form.fields.selectEvents": "Select events",
|
||||
"form.field.selectInbox": "Select an inbox",
|
||||
"form.field.assignTeamOptional": "Assign team (optional)",
|
||||
"form.field.assignAgentOptional": "Assign agent (optional)",
|
||||
"form.field.assignAgent": "Assign agent",
|
||||
"form.field.assignTeam": "Assign team",
|
||||
"form.field.message": "Message",
|
||||
"form.field.setValue": "Set value",
|
||||
"form.field.selectEvents": "Select events",
|
||||
"form.field.selectOperator": "Select operator",
|
||||
"form.field.value": "Value",
|
||||
"form.error.min": "Must be at least {min} characters",
|
||||
@@ -378,7 +390,6 @@
|
||||
"admin.conversation_tags.name.valid": "Tag name should at least 3 characters",
|
||||
"admin.conversation_tags.delete_confirmation": "This action cannot be undone. This will permanently delete this tag, and remove it from all conversations.",
|
||||
"admin.macro.message_content": "Response to be sent when macro is used (optional)",
|
||||
"admin.macro.message_content.placeholder": "Shift + Enter to add a new line",
|
||||
"admin.macro.actions": "Actions (optional)",
|
||||
"admin.macro.visibility": "Visibility",
|
||||
"admin.macro.visibility.all": "All users",
|
||||
@@ -531,6 +542,8 @@
|
||||
"globals.buttons.save": "Save",
|
||||
"globals.buttons.save_changes": "Save changes",
|
||||
"globals.buttons.cancel": "Cancel",
|
||||
"globals.buttons.submit": "Submit",
|
||||
"globals.buttons.send": "Send",
|
||||
"globals.buttons.update": "Update",
|
||||
"globals.buttons.delete": "Delete",
|
||||
"globals.buttons.create": "Create",
|
||||
@@ -578,5 +591,43 @@
|
||||
"account.chooseAFile": "Choose a file...",
|
||||
"account.removeAvatar": "Remove avatar",
|
||||
"account.cropAvatar": "Crop avatar",
|
||||
"account.avatarRemoved": "Avatar removed"
|
||||
"account.avatarRemoved": "Avatar removed",
|
||||
"conversation.resolveWithoutAssignee": "Cannot resolve the conversation without an assigned user, Please assign a user before attempting to resolve",
|
||||
"conversation.notMemberOfTeam": "You're not a member of this team, Please refresh the page and try again",
|
||||
"conversation.viewPermissionDenied": "You do not have access to view this view",
|
||||
"conversation.errorGeneratingMessageID": "Error generating message ID",
|
||||
"conversation.invalidSnoozeDuration": "Invalid snooze duration",
|
||||
"conversation.errorUnassigningOpenConversations": "Error unassigning open conversations",
|
||||
"conversation.errorRemovingConversationAssignee": "Error removing conversation assignee",
|
||||
"conversation.placeholder": "Select a conversation from the left panel.",
|
||||
"conversation.newConversation": "New conversation",
|
||||
"conversation.searchContact": "Search contact by email or type new email",
|
||||
"conversation.sort.oldestActivity": "Oldest activity",
|
||||
"conversation.sort.newestActivity": "Newest activity",
|
||||
"conversation.sort.startedFirst": "Started first",
|
||||
"conversation.sort.startedLast": "Started last",
|
||||
"conversation.sort.waitingLongest": "Waiting longest",
|
||||
"conversation.sort.nextSLATarget": "Next SLA target",
|
||||
"conversation.sort.priorityFirst": "Priority first",
|
||||
"conversation.noConversationsFound": "No conversations found",
|
||||
"conversation.tryAdjustingFilters": "Try adjusting filters",
|
||||
"conversation.couldNotFetch": "Could not fetch conversations",
|
||||
"conversation.allLoaded": "All conversations loaded",
|
||||
"conversation.showQuotedText": "Show quoted text",
|
||||
"conversation.hideQuotedText": "Hide quoted text",
|
||||
"conversation.sidebar.action": "Action | Actions",
|
||||
"conversation.sidebar.information": "Information",
|
||||
"conversation.sidebar.previousConvo": "Previous conversastions",
|
||||
"conversation.sidebar.noPreviousConvo": "No previous conversations",
|
||||
"conversation.sidebar.notAvailable": "Not available",
|
||||
"editor.placeholder": "Shift + Enter to add a new line",
|
||||
"ai.apiKeyNotSet": "{provider} API Key is not set. Please ask administrator to set it up",
|
||||
"ai.enterOpenAIAPIKey": "Enter OpenAI API Key",
|
||||
"ai.apiKey.description": "{provider} API Key is not set or invalid. Please enter a valid API key to use AI features.",
|
||||
"replyBox.reply": "Reply",
|
||||
"replyBox.privateNote": "Private note",
|
||||
"replyBox.emailAddresess": "Email addresses separated by comma",
|
||||
"replyBox.editor.placeholder": "Shift + Enter to add a new line. Cmd + Enter to send. Cmd + K to open command bar.",
|
||||
"replyBox.invalidEmailsIn": "Invalid email(s) in",
|
||||
"replyBox.correctEmailErrors": "Please correct the email errors before sending."
|
||||
}
|
Reference in New Issue
Block a user