feat: improve inbox management and conversation list
This commit is contained in:
@@ -2294,7 +2294,7 @@ func dashboardRuntimeConfig(c *gin.Context) {
|
||||
"helpCenterURL": strings.TrimRight(os.Getenv("HELPCENTER_URL"), "/"),
|
||||
"allowedLoginMethods": []string{"email"},
|
||||
"signupEnabled": "false",
|
||||
"selectedLocale": "en",
|
||||
"selectedLocale": "zh_CN",
|
||||
"globalConfig": map[string]any{
|
||||
"INSTALLATION_NAME": installationName,
|
||||
"BRAND_NAME": installationName,
|
||||
|
||||
@@ -1119,7 +1119,8 @@ func TestDashboardIndexServesChatwootShell(t *testing.T) {
|
||||
if !strings.Contains(runtimeConfig.Body.String(), `"hostURL":"https://app.example.test"`) ||
|
||||
!strings.Contains(runtimeConfig.Body.String(), `"INSTALLATION_NAME":"GoChat Test"`) ||
|
||||
!strings.Contains(runtimeConfig.Body.String(), `"BRAND_NAME":"GoChat Test"`) ||
|
||||
!strings.Contains(runtimeConfig.Body.String(), `"LOGO":"/brand-assets/logo.svg"`) {
|
||||
!strings.Contains(runtimeConfig.Body.String(), `"LOGO":"/brand-assets/logo.svg"`) ||
|
||||
!strings.Contains(runtimeConfig.Body.String(), `"selectedLocale":"zh_CN"`) {
|
||||
t.Fatalf("expected environment-backed runtime config: %s", runtimeConfig.Body.String())
|
||||
}
|
||||
if strings.Contains(runtimeConfig.Body.String(), "<script") {
|
||||
|
||||
+140
-151
@@ -14,36 +14,36 @@ import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
chat: { type: Object, required: true },
|
||||
currentContact: { type: Object, required: true },
|
||||
assignee: { type: Object, default: () => ({}) },
|
||||
inbox: { type: Object, default: () => ({}) },
|
||||
selected: { type: Boolean, default: false },
|
||||
isActiveChat: { type: Boolean, default: false },
|
||||
showAssignee: { type: Boolean, default: false },
|
||||
showInboxName: { type: Boolean, default: false },
|
||||
isInboxView: { type: Boolean, default: false },
|
||||
chat: { type: Object, required: true },
|
||||
currentContact: { type: Object, required: true },
|
||||
assignee: { type: Object, default: () => ({}) },
|
||||
inbox: { type: Object, default: () => ({}) },
|
||||
selected: { type: Boolean, default: false },
|
||||
isActiveChat: { type: Boolean, default: false },
|
||||
showAssignee: { type: Boolean, default: false },
|
||||
showInboxName: { type: Boolean, default: false },
|
||||
isInboxView: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'selectConversation',
|
||||
'deSelectConversation',
|
||||
'click',
|
||||
'contextmenu',
|
||||
'selectConversation',
|
||||
'deSelectConversation',
|
||||
'click',
|
||||
'contextmenu',
|
||||
]);
|
||||
|
||||
const lastMessageInChat = computed(() => getLastMessage(props.chat));
|
||||
const showLabelsSection = computed(() => props.chat.labels?.length > 0);
|
||||
|
||||
const voiceCallData = computed(() => {
|
||||
const last = lastMessageInChat.value;
|
||||
if (last?.content_type !== 'voice_call' || !last.call) {
|
||||
return { status: null, direction: null };
|
||||
}
|
||||
return {
|
||||
status: last.call.status,
|
||||
direction: last.call.direction === 'outgoing' ? 'outbound' : 'inbound',
|
||||
};
|
||||
const last = lastMessageInChat.value;
|
||||
if (last?.content_type !== 'voice_call' || !last.call) {
|
||||
return { status: null, direction: null };
|
||||
}
|
||||
return {
|
||||
status: last.call.status,
|
||||
direction: last.call.direction === 'outgoing' ? 'outbound' : 'inbound',
|
||||
};
|
||||
});
|
||||
|
||||
const unreadCount = computed(() => props.chat.unread_count);
|
||||
@@ -51,154 +51,143 @@ const unreadCount = computed(() => props.chat.unread_count);
|
||||
const slaCardLabel = useTemplateRef('slaCardLabel');
|
||||
|
||||
const hasSlaPolicyId = computed(
|
||||
() => props.chat?.sla_policy_id || slaCardLabel.value?.hasSlaThreshold
|
||||
() => props.chat?.sla_policy_id || slaCardLabel.value?.hasSlaThreshold
|
||||
);
|
||||
|
||||
const selectedModel = computed({
|
||||
get: () => props.selected,
|
||||
set: value => {
|
||||
if (value) {
|
||||
emit('selectConversation', value);
|
||||
} else {
|
||||
emit('deSelectConversation', value);
|
||||
}
|
||||
},
|
||||
get: () => props.selected,
|
||||
set: value => {
|
||||
if (value) {
|
||||
emit('selectConversation', value);
|
||||
} else {
|
||||
emit('deSelectConversation', value);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="conversation relative cursor-pointer group grid gap-4 items-center px-3 h-12 border-b border-n-slate-3 hover:border-n-surface-1 hover:z-[1] before:content-[none] before:absolute before:-top-px before:inset-x-0 before:h-px before:bg-n-surface-1 before:pointer-events-none hover:before:content-['']"
|
||||
:class="{
|
||||
'active animate-card-select bg-n-alpha-1 dark:bg-n-alpha-3 !border-n-surface-1':
|
||||
isActiveChat,
|
||||
'selected bg-n-slate-2 dark:bg-n-slate-3 !border-n-surface-1':
|
||||
selected,
|
||||
'hover:bg-n-alpha-1': !isActiveChat && !selected,
|
||||
'grid-cols-[minmax(0,2fr)_minmax(0,1fr)]': showLabelsSection,
|
||||
'grid-cols-[minmax(0,2fr)_max-content]': !showLabelsSection,
|
||||
}"
|
||||
@click="$emit('click', $event)"
|
||||
@contextmenu="$emit('contextmenu', $event)"
|
||||
>
|
||||
<!-- LEFT SECTION -->
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<div
|
||||
class="flex items-center justify-center flex-shrink-0"
|
||||
@click.stop
|
||||
>
|
||||
<Checkbox v-model="selectedModel" />
|
||||
</div>
|
||||
<div
|
||||
class="conversation relative cursor-pointer group grid gap-4 items-center px-3 h-11 border-b border-n-slate-3 hover:border-n-surface-1 hover:z-[1] before:content-[none] before:absolute before:-top-px before:inset-x-0 before:h-px before:bg-n-surface-1 before:pointer-events-none hover:before:content-['']"
|
||||
:class="{
|
||||
'active animate-card-select bg-n-alpha-1 dark:bg-n-alpha-3 !border-n-surface-1':
|
||||
isActiveChat,
|
||||
'selected bg-n-slate-2 dark:bg-n-slate-3 !border-n-surface-1': selected,
|
||||
'hover:bg-n-alpha-1': !isActiveChat && !selected,
|
||||
'grid-cols-[minmax(0,2fr)_minmax(0,1fr)]': showLabelsSection,
|
||||
'grid-cols-[minmax(0,2fr)_max-content]': !showLabelsSection,
|
||||
}"
|
||||
@click="$emit('click', $event)"
|
||||
@contextmenu="$emit('contextmenu', $event)"
|
||||
>
|
||||
<!-- LEFT SECTION -->
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<div class="flex items-center justify-center flex-shrink-0" @click.stop>
|
||||
<Checkbox v-model="selectedModel" />
|
||||
</div>
|
||||
|
||||
<div class="w-px h-3 bg-n-slate-6 flex-shrink-0" />
|
||||
<div class="w-px h-3 bg-n-slate-6 flex-shrink-0" />
|
||||
|
||||
<div class="w-4 flex items-center justify-center flex-shrink-0">
|
||||
<CardPriorityIcon :priority="chat.priority" show-empty />
|
||||
</div>
|
||||
<div class="w-4 flex items-center justify-center flex-shrink-0">
|
||||
<CardPriorityIcon :priority="chat.priority" show-empty />
|
||||
</div>
|
||||
|
||||
<div class="w-4 flex items-center justify-center flex-shrink-0">
|
||||
<Avatar
|
||||
v-if="showAssignee && assignee.name"
|
||||
v-tooltip.top="{
|
||||
content: assignee.name,
|
||||
delay: { show: 500, hide: 0 },
|
||||
}"
|
||||
:name="assignee.name"
|
||||
:src="assignee.thumbnail"
|
||||
:size="14"
|
||||
:status="assignee.availability_status"
|
||||
hide-offline-status
|
||||
/>
|
||||
<Icon
|
||||
v-else
|
||||
icon="i-woot-empty-assignee"
|
||||
class="size-4 text-n-slate-7"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-4 flex items-center justify-center flex-shrink-0">
|
||||
<Avatar
|
||||
v-if="showAssignee && assignee.name"
|
||||
v-tooltip.top="{
|
||||
content: assignee.name,
|
||||
delay: { show: 500, hide: 0 },
|
||||
}"
|
||||
:name="assignee.name"
|
||||
:src="assignee.thumbnail"
|
||||
:size="14"
|
||||
:status="assignee.availability_status"
|
||||
hide-offline-status
|
||||
/>
|
||||
<Icon
|
||||
v-else
|
||||
icon="i-woot-empty-assignee"
|
||||
class="size-4 text-n-slate-7"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-4 flex items-center justify-center flex-shrink-0">
|
||||
<CardStatusIcon :status="chat.status" show-empty />
|
||||
</div>
|
||||
<div class="w-4 flex items-center justify-center flex-shrink-0">
|
||||
<CardStatusIcon :status="chat.status" show-empty />
|
||||
</div>
|
||||
|
||||
<div class="w-px h-3 bg-n-slate-6 flex-shrink-0" />
|
||||
<div class="w-px h-3 bg-n-slate-6 flex-shrink-0" />
|
||||
|
||||
<div
|
||||
v-if="!isInboxView && showInboxName"
|
||||
class="w-20 flex-shrink-0"
|
||||
>
|
||||
<InboxName
|
||||
v-if="showInboxName"
|
||||
:inbox="inbox"
|
||||
class="min-w-0"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!isInboxView && showInboxName" class="w-20 flex-shrink-0">
|
||||
<InboxName v-if="showInboxName" :inbox="inbox" class="min-w-0" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isInboxView && showInboxName"
|
||||
class="w-px h-3 bg-n-slate-6 flex-shrink-0"
|
||||
/>
|
||||
<div
|
||||
v-if="!isInboxView && showInboxName"
|
||||
class="w-px h-3 bg-n-slate-6 flex-shrink-0"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-tooltip.top="{
|
||||
content: chat.id,
|
||||
delay: { show: 500, hide: 0 },
|
||||
}"
|
||||
class="h-6 flex items-center gap-1 max-w-20 w-full min-w-0 flex-shrink-0"
|
||||
>
|
||||
<Icon
|
||||
icon="i-woot-hash"
|
||||
class="size-3.5 text-n-slate-10 flex-shrink-0"
|
||||
/>
|
||||
<span class="text-body-main text-n-slate-11 truncate">
|
||||
{{ chat.id }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-tooltip.top="{
|
||||
content: chat.id,
|
||||
delay: { show: 500, hide: 0 },
|
||||
}"
|
||||
class="h-6 flex items-center gap-1 max-w-20 w-full min-w-0 flex-shrink-0"
|
||||
>
|
||||
<Icon
|
||||
icon="i-woot-hash"
|
||||
class="size-3.5 text-n-slate-10 flex-shrink-0"
|
||||
/>
|
||||
<span class="text-body-main text-n-slate-11 truncate">
|
||||
{{ chat.id }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<CardAvatar
|
||||
:contact="currentContact"
|
||||
:selected="false"
|
||||
:enable-selection="false"
|
||||
:hide-thumbnail="false"
|
||||
/>
|
||||
<CardAvatar
|
||||
:contact="currentContact"
|
||||
:selected="false"
|
||||
:enable-selection="false"
|
||||
:hide-thumbnail="false"
|
||||
/>
|
||||
|
||||
<div class="flex min-w-0 flex-1 flex-col justify-center">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<h4
|
||||
class="text-heading-3 my-0 min-w-0 flex-1 capitalize truncate text-n-slate-12 font-medium"
|
||||
>
|
||||
{{ currentContact.name }}
|
||||
</h4>
|
||||
<TimeAgo
|
||||
:conversation-id="chat.id"
|
||||
:last-activity-timestamp="chat.timestamp"
|
||||
:created-at-timestamp="chat.created_at"
|
||||
class="w-[4.375rem] flex-shrink-0 text-end font-440 !text-xs text-n-slate-11"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-1 flex-col justify-center">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<h4
|
||||
class="text-heading-3 my-0 min-w-0 flex-1 capitalize truncate text-n-slate-12 font-medium"
|
||||
>
|
||||
{{ currentContact.name }}
|
||||
</h4>
|
||||
<TimeAgo
|
||||
:conversation-id="chat.id"
|
||||
:last-activity-timestamp="chat.timestamp"
|
||||
:created-at-timestamp="chat.created_at"
|
||||
class="w-[4.375rem] flex-shrink-0 text-end font-440 !text-xs text-n-slate-11"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CardContent
|
||||
:last-message="lastMessageInChat"
|
||||
:voice-call-status="voiceCallData.status"
|
||||
:voice-call-direction="voiceCallData.direction"
|
||||
:unread-count="unreadCount"
|
||||
:show-expanded-preview="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CardContent
|
||||
:last-message="lastMessageInChat"
|
||||
:voice-call-status="voiceCallData.status"
|
||||
:voice-call-direction="voiceCallData.direction"
|
||||
:unread-count="unreadCount"
|
||||
:show-expanded-preview="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT SECTION -->
|
||||
<div class="flex items-center justify-end gap-1.5 flex-shrink-0">
|
||||
<div v-if="showLabelsSection" class="min-w-0 w-full">
|
||||
<CardLabels
|
||||
:labels="chat.labels"
|
||||
disable-toggle
|
||||
class="my-0 [&>div]:justify-end justify-end"
|
||||
/>
|
||||
</div>
|
||||
<!-- RIGHT SECTION -->
|
||||
<div class="flex items-center justify-end gap-1.5 flex-shrink-0">
|
||||
<div v-if="showLabelsSection" class="min-w-0 w-full">
|
||||
<CardLabels
|
||||
:labels="chat.labels"
|
||||
disable-toggle
|
||||
class="my-0 [&>div]:justify-end justify-end"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="hasSlaPolicyId" class="flex-shrink-0">
|
||||
<SLACardLabel ref="slaCardLabel" :chat="chat" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasSlaPolicyId" class="flex-shrink-0">
|
||||
<SLACardLabel ref="slaCardLabel" :chat="chat" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -237,7 +237,11 @@ const conversationListPagination = computed(() => {
|
||||
activeAssigneeTabCount.value < conversationsPerPage &&
|
||||
activeAssigneeTabCount.value > chatsOnView.value.length;
|
||||
|
||||
if (isNoFiltersOrFoldersAndChatListNotEmpty && isUnderPerPage && !hasCurrentPageEndReached.value) {
|
||||
if (
|
||||
isNoFiltersOrFoldersAndChatListNotEmpty &&
|
||||
isUnderPerPage &&
|
||||
!hasCurrentPageEndReached.value
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -359,6 +363,10 @@ const uniqueInboxes = computed(() => {
|
||||
return [...new Set(selectedInboxes.value)];
|
||||
});
|
||||
|
||||
const showInboxNameInConversationCard = computed(() => {
|
||||
return !props.conversationInbox;
|
||||
});
|
||||
|
||||
// ---------------------- Methods -----------------------
|
||||
function setFiltersFromUISettings() {
|
||||
const { conversations_filter_by: filterBy = {} } = uiSettings.value;
|
||||
@@ -791,6 +799,9 @@ onMounted(() => {
|
||||
setFiltersFromUISettings();
|
||||
store.dispatch('setChatStatusFilter', activeStatus.value);
|
||||
store.dispatch('setChatSortFilter', activeSortBy.value);
|
||||
if (!inboxesList.value.length) {
|
||||
store.dispatch('inboxes/get');
|
||||
}
|
||||
resetAndFetchData();
|
||||
if (hasActiveFolders.value) {
|
||||
store.dispatch('campaigns/get');
|
||||
@@ -800,15 +811,26 @@ onMounted(() => {
|
||||
// Fallback: if fetchAllConversations wasn't triggered during onMounted
|
||||
// (e.g., race condition with route transition), retry after store settles.
|
||||
// Uses a one-shot watch on conversationStats to detect empty state.
|
||||
const _fallbackWatch = watch(conversationStats, (stats) => {
|
||||
if (!stats) return;
|
||||
const totalCount = (stats.mineCount || 0) + (stats.unAssignedCount || 0) + (stats.allCount || 0);
|
||||
// If stats show conversations exist but chatList is empty, re-fetch
|
||||
if (totalCount > 0 && chatLists.value.length === 0 && !chatListLoading.value) {
|
||||
_fallbackWatch(); // unwatch immediately — one-shot
|
||||
fetchConversations();
|
||||
}
|
||||
}, { immediate: false });
|
||||
const stopFallbackWatch = watch(
|
||||
conversationStats,
|
||||
stats => {
|
||||
if (!stats) return;
|
||||
const totalCount =
|
||||
(stats.mineCount || 0) +
|
||||
(stats.unAssignedCount || 0) +
|
||||
(stats.allCount || 0);
|
||||
// If stats show conversations exist but chatList is empty, re-fetch
|
||||
if (
|
||||
totalCount > 0 &&
|
||||
chatLists.value.length === 0 &&
|
||||
!chatListLoading.value
|
||||
) {
|
||||
stopFallbackWatch(); // unwatch immediately — one-shot
|
||||
fetchConversations();
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
const deleteConversationDialogRef = ref(null);
|
||||
const selectedConversationId = ref(null);
|
||||
@@ -877,7 +899,7 @@ watch(chatLists, () => {
|
||||
class="flex flex-col flex-shrink-0 conversations-list-wrap bg-n-surface-1 relative"
|
||||
:class="[
|
||||
{ hidden: !showConversationList },
|
||||
isOnExpandedLayout ? 'basis-full' : 'w-[340px] 2xl:w-[412px]',
|
||||
isOnExpandedLayout ? 'basis-full' : 'w-[320px] 2xl:w-[360px]',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
@@ -957,6 +979,7 @@ watch(chatLists, () => {
|
||||
:folders-id="foldersId"
|
||||
:conversation-type="conversationType"
|
||||
:show-assignee="showAssigneeInConversationCard"
|
||||
:show-inbox-name="showInboxNameInConversationCard"
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@load-more="loadMoreConversations"
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,7 @@ const props = defineProps({
|
||||
conversationType: { type: String, default: '' },
|
||||
foldersId: { type: [String, Number], default: 0 },
|
||||
showAssignee: { type: Boolean, default: false },
|
||||
showInboxName: { type: Boolean, default: false },
|
||||
showExpanded: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
@@ -52,7 +53,6 @@ watch(
|
||||
);
|
||||
|
||||
const currentChat = useMapGetter('getSelectedChat');
|
||||
const inboxesList = useMapGetter('inboxes/getInboxes');
|
||||
const activeInbox = useMapGetter('getSelectedInbox');
|
||||
const accountId = useMapGetter('getCurrentAccountId');
|
||||
|
||||
@@ -68,13 +68,22 @@ const isActiveChat = computed(() => currentChat.value.id === props.source.id);
|
||||
|
||||
const inbox = computed(() => {
|
||||
const inboxId = props.source.inbox_id;
|
||||
return inboxId ? store.getters['inboxes/getInbox'](inboxId) : {};
|
||||
if (!inboxId) return {};
|
||||
|
||||
const inboxRecord = store.getters['inboxes/getInbox'](inboxId);
|
||||
return {
|
||||
...inboxRecord,
|
||||
id: inboxRecord.id || inboxId,
|
||||
name: inboxRecord.name || `#${inboxId}`,
|
||||
channel_type: inboxRecord.channel_type || props.source.meta?.channel,
|
||||
};
|
||||
});
|
||||
|
||||
const showInboxName = computed(
|
||||
() => !activeInbox.value && inboxesList.value.length > 1
|
||||
const showInboxName = computed(() => props.showInboxName);
|
||||
const activeInboxForRoute = computed(() =>
|
||||
props.showInboxName ? null : activeInbox.value
|
||||
);
|
||||
const isInboxView = computed(() => !!activeInbox.value);
|
||||
const isInboxView = computed(() => !!activeInboxForRoute.value);
|
||||
const showAssigneeForExpandedCard = computed(
|
||||
() => props.showExpanded || props.showAssignee
|
||||
);
|
||||
@@ -83,7 +92,7 @@ const conversationPath = computed(() =>
|
||||
frontendURL(
|
||||
conversationUrl({
|
||||
accountId: accountId.value,
|
||||
activeInbox: activeInbox.value,
|
||||
activeInbox: activeInboxForRoute.value,
|
||||
id: props.source.id,
|
||||
label: props.label,
|
||||
teamId: props.teamId,
|
||||
|
||||
@@ -19,6 +19,7 @@ const props = defineProps({
|
||||
foldersId: { type: [String, Number], default: 0 },
|
||||
conversationType: { type: String, default: '' },
|
||||
showAssignee: { type: Boolean, default: false },
|
||||
showInboxName: { type: Boolean, default: false },
|
||||
isOnExpandedLayout: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
@@ -77,6 +78,7 @@ defineExpose({ conversationListRef });
|
||||
:folders-id="foldersId"
|
||||
:conversation-type="conversationType"
|
||||
:show-assignee="showAssignee"
|
||||
:show-inbox-name="showInboxName"
|
||||
:show-expanded="showExpandedCards"
|
||||
/>
|
||||
</Virtualizer>
|
||||
|
||||
@@ -316,7 +316,7 @@ const resetDatePicker = () => {
|
||||
|
||||
const emitDateRange = () => {
|
||||
if (!isValid(selectedStartDate.value) || !isValid(selectedEndDate.value)) {
|
||||
useAlert('Please select a valid time range');
|
||||
useAlert(t('DATE_PICKER.INVALID_TIME_RANGE'));
|
||||
} else {
|
||||
showDatePicker.value = false;
|
||||
emit('dateRangeChanged', [
|
||||
|
||||
+178
-185
@@ -13,23 +13,23 @@ import VoiceCallStatus from './VoiceCallStatus.vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
|
||||
const props = defineProps({
|
||||
chat: { type: Object, required: true },
|
||||
currentContact: { type: Object, required: true },
|
||||
assignee: { type: Object, default: () => ({}) },
|
||||
inbox: { type: Object, default: () => ({}) },
|
||||
selected: { type: Boolean, default: false },
|
||||
isActiveChat: { type: Boolean, default: false },
|
||||
showAssignee: { type: Boolean, default: false },
|
||||
showInboxName: { type: Boolean, default: false },
|
||||
hideThumbnail: { type: Boolean, default: false },
|
||||
compact: { type: Boolean, default: false },
|
||||
chat: { type: Object, required: true },
|
||||
currentContact: { type: Object, required: true },
|
||||
assignee: { type: Object, default: () => ({}) },
|
||||
inbox: { type: Object, default: () => ({}) },
|
||||
selected: { type: Boolean, default: false },
|
||||
isActiveChat: { type: Boolean, default: false },
|
||||
showAssignee: { type: Boolean, default: false },
|
||||
showInboxName: { type: Boolean, default: false },
|
||||
hideThumbnail: { type: Boolean, default: false },
|
||||
compact: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'click',
|
||||
'contextmenu',
|
||||
'selectConversation',
|
||||
'deSelectConversation',
|
||||
'click',
|
||||
'contextmenu',
|
||||
'selectConversation',
|
||||
'deSelectConversation',
|
||||
]);
|
||||
|
||||
const hovered = ref(false);
|
||||
@@ -39,205 +39,198 @@ const hasUnread = computed(() => unreadCount.value > 0);
|
||||
const lastMessageInChat = computed(() => getLastMessage(props.chat));
|
||||
|
||||
const voiceCallData = computed(() => {
|
||||
const last = lastMessageInChat.value;
|
||||
if (last?.content_type !== 'voice_call' || !last.call) {
|
||||
return { status: null, direction: null };
|
||||
}
|
||||
return {
|
||||
status: last.call.status,
|
||||
direction: last.call.direction === 'outgoing' ? 'outbound' : 'inbound',
|
||||
};
|
||||
const last = lastMessageInChat.value;
|
||||
if (last?.content_type !== 'voice_call' || !last.call) {
|
||||
return { status: null, direction: null };
|
||||
}
|
||||
return {
|
||||
status: last.call.status,
|
||||
direction: last.call.direction === 'outgoing' ? 'outbound' : 'inbound',
|
||||
};
|
||||
});
|
||||
|
||||
const showMetaSection = computed(() => {
|
||||
return (
|
||||
props.showInboxName ||
|
||||
(props.showAssignee && props.assignee.name) ||
|
||||
props.chat.priority
|
||||
);
|
||||
return (
|
||||
props.showInboxName ||
|
||||
(props.showAssignee && props.assignee.name) ||
|
||||
props.chat.priority
|
||||
);
|
||||
});
|
||||
|
||||
const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
|
||||
|
||||
const showLabelsSection = computed(() => {
|
||||
return props.chat.labels?.length > 0 || hasSlaPolicyId.value;
|
||||
return props.chat.labels?.length > 0 || hasSlaPolicyId.value;
|
||||
});
|
||||
|
||||
const messagePreviewClass = computed(() => {
|
||||
return [
|
||||
hasUnread.value ? 'font-medium text-n-slate-12' : 'text-n-slate-11',
|
||||
!props.compact && hasUnread.value ? 'ltr:pr-4 rtl:pl-4' : '',
|
||||
props.compact && hasUnread.value ? 'ltr:pr-6 rtl:pl-6' : '',
|
||||
];
|
||||
return [
|
||||
'!h-5 !leading-5',
|
||||
hasUnread.value ? 'font-medium text-n-slate-12' : 'text-n-slate-11',
|
||||
!props.compact && hasUnread.value ? 'ltr:pr-4 rtl:pl-4' : '',
|
||||
props.compact && hasUnread.value ? 'ltr:pr-6 rtl:pl-6' : '',
|
||||
];
|
||||
});
|
||||
|
||||
const onThumbnailHover = () => {
|
||||
hovered.value = !props.hideThumbnail;
|
||||
hovered.value = !props.hideThumbnail;
|
||||
};
|
||||
|
||||
const onThumbnailLeave = () => {
|
||||
hovered.value = false;
|
||||
hovered.value = false;
|
||||
};
|
||||
|
||||
const onSelectConversation = checked => {
|
||||
if (checked) {
|
||||
emit('selectConversation', props.chat.id, props.inbox.id);
|
||||
} else {
|
||||
emit('deSelectConversation', props.chat.id, props.inbox.id);
|
||||
}
|
||||
if (checked) {
|
||||
emit('selectConversation', props.chat.id, props.inbox.id);
|
||||
} else {
|
||||
emit('deSelectConversation', props.chat.id, props.inbox.id);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedModel = computed({
|
||||
get: () => props.selected,
|
||||
set: value => onSelectConversation(value),
|
||||
get: () => props.selected,
|
||||
set: value => onSelectConversation(value),
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.chat.id,
|
||||
() => {
|
||||
hovered.value = false;
|
||||
}
|
||||
() => props.chat.id,
|
||||
() => {
|
||||
hovered.value = false;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full py-0 cursor-pointer conversation border-b border-n-slate-3 hover:border-n-surface-1 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3 group hover:z-[1] before:content-[none] before:absolute before:-top-px before:inset-x-0 before:h-px before:bg-n-surface-1 before:pointer-events-none hover:before:content-['']"
|
||||
:class="{
|
||||
'active animate-card-select bg-n-background !border-n-surface-1':
|
||||
isActiveChat,
|
||||
'selected bg-n-slate-2 !border-n-surface-1': selected,
|
||||
'px-0': compact,
|
||||
'px-3': !compact,
|
||||
}"
|
||||
@click="$emit('click', $event)"
|
||||
@contextmenu="$emit('contextmenu', $event)"
|
||||
>
|
||||
<div
|
||||
class="relative"
|
||||
@mouseenter="onThumbnailHover"
|
||||
@mouseleave="onThumbnailLeave"
|
||||
>
|
||||
<Avatar
|
||||
v-if="!hideThumbnail"
|
||||
:name="currentContact.name"
|
||||
:src="currentContact.thumbnail"
|
||||
:size="32"
|
||||
:status="currentContact.availability_status"
|
||||
:class="!showInboxName ? 'mt-4' : 'mt-8'"
|
||||
hide-offline-status
|
||||
>
|
||||
<template #overlay="{ size }">
|
||||
<label
|
||||
v-if="hovered || selected"
|
||||
class="flex items-center justify-center rounded-full cursor-pointer absolute inset-0 z-10 backdrop-blur-[2px]"
|
||||
:style="{ width: `${size}px`, height: `${size}px` }"
|
||||
@click.stop
|
||||
>
|
||||
<Checkbox v-model="selectedModel" />
|
||||
</label>
|
||||
</template>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div class="px-0 py-3 flex-1 min-w-0 border-line">
|
||||
<div
|
||||
v-if="showMetaSection"
|
||||
class="flex items-center min-w-0 gap-1"
|
||||
:class="{
|
||||
'ltr:ml-2 rtl:mr-2': !compact,
|
||||
'mx-2': compact,
|
||||
}"
|
||||
>
|
||||
<InboxName
|
||||
v-if="showInboxName"
|
||||
:inbox="inbox"
|
||||
class="flex-1 min-w-0"
|
||||
/>
|
||||
<div
|
||||
class="flex items-baseline gap-2 flex-shrink-0"
|
||||
:class="{
|
||||
'flex-1 justify-between': !showInboxName,
|
||||
}"
|
||||
>
|
||||
<span
|
||||
v-if="showAssignee && assignee.name"
|
||||
class="text-n-slate-11 text-xs font-medium leading-3 py-0.5 px-0 inline-flex items-center truncate"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="person"
|
||||
size="12"
|
||||
class="text-n-slate-11"
|
||||
/>
|
||||
{{ assignee.name }}
|
||||
</span>
|
||||
<CardPriorityIcon
|
||||
:priority="chat.priority"
|
||||
class="flex-shrink-0 !size-3.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex min-w-0 items-center gap-2 mx-2 pt-0.5">
|
||||
<h4
|
||||
class="conversation--user text-sm my-0 min-w-0 flex-1 capitalize text-ellipsis overflow-hidden whitespace-nowrap text-n-slate-12"
|
||||
:class="hasUnread ? 'font-semibold' : 'font-medium'"
|
||||
>
|
||||
{{ currentContact.name }}
|
||||
</h4>
|
||||
<TimeAgo
|
||||
:last-activity-timestamp="chat.timestamp"
|
||||
:created-at-timestamp="chat.created_at"
|
||||
:conversation-id="chat.id"
|
||||
class="w-[4.375rem] flex-shrink-0 text-end font-normal"
|
||||
/>
|
||||
</div>
|
||||
<VoiceCallStatus
|
||||
v-if="voiceCallData.status"
|
||||
key="voice-status-row"
|
||||
:status="voiceCallData.status"
|
||||
:direction="voiceCallData.direction"
|
||||
:message-preview-class="messagePreviewClass"
|
||||
/>
|
||||
<MessagePreview
|
||||
v-else-if="lastMessageInChat"
|
||||
key="message-preview"
|
||||
:message="lastMessageInChat"
|
||||
class="my-0 mx-2 leading-6 h-6 flex-1 min-w-0 text-sm"
|
||||
:class="messagePreviewClass"
|
||||
/>
|
||||
<p
|
||||
v-else
|
||||
key="no-messages"
|
||||
class="text-n-slate-11 text-sm my-0 mx-2 leading-6 h-6 flex-1 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
:class="messagePreviewClass"
|
||||
>
|
||||
<fluent-icon
|
||||
size="16"
|
||||
class="-mt-0.5 align-middle inline-block text-n-slate-10"
|
||||
icon="info"
|
||||
/>
|
||||
<span class="mx-0.5">
|
||||
{{ $t(`CHAT_LIST.NO_MESSAGES`) }}
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
v-if="hasUnread"
|
||||
class="absolute flex flex-col ltr:right-3 rtl:left-3"
|
||||
:class="showMetaSection ? 'top-8' : 'top-4'"
|
||||
>
|
||||
<UnreadBadge
|
||||
:count="unreadCount"
|
||||
class="ltr:ml-auto rtl:mr-auto mt-1"
|
||||
/>
|
||||
</div>
|
||||
<CardLabels
|
||||
v-if="showLabelsSection"
|
||||
:conversation-labels="chat.labels"
|
||||
class="mt-0.5 mx-2 mb-0"
|
||||
>
|
||||
<template v-if="hasSlaPolicyId" #before>
|
||||
<SLACardLabel :chat="chat" class="ltr:mr-1 rtl:ml-1" />
|
||||
</template>
|
||||
</CardLabels>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full py-0 cursor-pointer conversation border-b border-n-slate-3 hover:border-n-surface-1 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3 group hover:z-[1] before:content-[none] before:absolute before:-top-px before:inset-x-0 before:h-px before:bg-n-surface-1 before:pointer-events-none hover:before:content-['']"
|
||||
:class="{
|
||||
'active animate-card-select bg-n-background !border-n-surface-1':
|
||||
isActiveChat,
|
||||
'selected bg-n-slate-2 !border-n-surface-1': selected,
|
||||
'px-0': compact,
|
||||
'px-3': !compact,
|
||||
}"
|
||||
@click="$emit('click', $event)"
|
||||
@contextmenu="$emit('contextmenu', $event)"
|
||||
>
|
||||
<div
|
||||
class="relative"
|
||||
@mouseenter="onThumbnailHover"
|
||||
@mouseleave="onThumbnailLeave"
|
||||
>
|
||||
<Avatar
|
||||
v-if="!hideThumbnail"
|
||||
:name="currentContact.name"
|
||||
:src="currentContact.thumbnail"
|
||||
:size="32"
|
||||
:status="currentContact.availability_status"
|
||||
:class="!showInboxName ? 'mt-2' : 'mt-6'"
|
||||
hide-offline-status
|
||||
>
|
||||
<template #overlay="{ size }">
|
||||
<label
|
||||
v-if="hovered || selected"
|
||||
class="flex items-center justify-center rounded-full cursor-pointer absolute inset-0 z-10 backdrop-blur-[2px]"
|
||||
:style="{ width: `${size}px`, height: `${size}px` }"
|
||||
@click.stop
|
||||
>
|
||||
<Checkbox v-model="selectedModel" />
|
||||
</label>
|
||||
</template>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div class="px-0 py-2 flex-1 min-w-0 border-line">
|
||||
<div
|
||||
v-if="showMetaSection"
|
||||
class="flex items-center min-w-0 gap-1"
|
||||
:class="{
|
||||
'ltr:ml-2 rtl:mr-2': !compact,
|
||||
'mx-2': compact,
|
||||
}"
|
||||
>
|
||||
<InboxName v-if="showInboxName" :inbox="inbox" class="flex-1 min-w-0" />
|
||||
<div
|
||||
class="flex items-baseline gap-2 flex-shrink-0"
|
||||
:class="{
|
||||
'flex-1 justify-between': !showInboxName,
|
||||
}"
|
||||
>
|
||||
<span
|
||||
v-if="showAssignee && assignee.name"
|
||||
class="text-n-slate-11 text-xs font-medium leading-3 py-0.5 px-0 inline-flex items-center truncate"
|
||||
>
|
||||
<fluent-icon icon="person" size="12" class="text-n-slate-11" />
|
||||
{{ assignee.name }}
|
||||
</span>
|
||||
<CardPriorityIcon
|
||||
:priority="chat.priority"
|
||||
class="flex-shrink-0 !size-3.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex min-w-0 items-center gap-2 mx-2 pt-0.5">
|
||||
<h4
|
||||
class="conversation--user text-sm my-0 min-w-0 flex-1 capitalize text-ellipsis overflow-hidden whitespace-nowrap text-n-slate-12"
|
||||
:class="hasUnread ? 'font-semibold' : 'font-medium'"
|
||||
>
|
||||
{{ currentContact.name }}
|
||||
</h4>
|
||||
<TimeAgo
|
||||
:last-activity-timestamp="chat.timestamp"
|
||||
:created-at-timestamp="chat.created_at"
|
||||
:conversation-id="chat.id"
|
||||
class="w-[4.375rem] flex-shrink-0 text-end font-normal"
|
||||
/>
|
||||
</div>
|
||||
<VoiceCallStatus
|
||||
v-if="voiceCallData.status"
|
||||
key="voice-status-row"
|
||||
:status="voiceCallData.status"
|
||||
:direction="voiceCallData.direction"
|
||||
:message-preview-class="messagePreviewClass"
|
||||
/>
|
||||
<MessagePreview
|
||||
v-else-if="lastMessageInChat"
|
||||
key="message-preview"
|
||||
:message="lastMessageInChat"
|
||||
class="my-0 mx-2 leading-5 h-5 flex-1 min-w-0 text-sm"
|
||||
:class="messagePreviewClass"
|
||||
/>
|
||||
<p
|
||||
v-else
|
||||
key="no-messages"
|
||||
class="text-n-slate-11 text-sm my-0 mx-2 leading-5 h-5 flex-1 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
:class="messagePreviewClass"
|
||||
>
|
||||
<fluent-icon
|
||||
size="16"
|
||||
class="-mt-0.5 align-middle inline-block text-n-slate-10"
|
||||
icon="info"
|
||||
/>
|
||||
<span class="mx-0.5">
|
||||
{{ $t(`CHAT_LIST.NO_MESSAGES`) }}
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
v-if="hasUnread"
|
||||
class="absolute flex flex-col ltr:right-3 rtl:left-3"
|
||||
:class="showMetaSection ? 'top-8' : 'top-4'"
|
||||
>
|
||||
<UnreadBadge
|
||||
:count="unreadCount"
|
||||
class="ltr:ml-auto rtl:mr-auto mt-1"
|
||||
/>
|
||||
</div>
|
||||
<CardLabels
|
||||
v-if="showLabelsSection"
|
||||
:conversation-labels="chat.labels"
|
||||
class="mt-0.5 mx-2 mb-0"
|
||||
>
|
||||
<template v-if="hasSlaPolicyId" #before>
|
||||
<SLACardLabel :chat="chat" class="ltr:mr-1 rtl:ml-1" />
|
||||
</template>
|
||||
</CardLabels>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -71,7 +71,7 @@ export const registerSubscription = (onSuccess = () => {}) => {
|
||||
.catch(error => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Push subscription registration failed:', error);
|
||||
useAlert('This browser does not support desktop notification');
|
||||
useAlert('DESKTOP_NOTIFICATION_UNSUPPORTED', { usei18n: true });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -79,7 +79,7 @@ export const requestPushPermissions = ({ onSuccess }) => {
|
||||
if (!('Notification' in window)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Notification is not supported');
|
||||
useAlert('This browser does not support desktop notification');
|
||||
useAlert('DESKTOP_NOTIFICATION_UNSUPPORTED', { usei18n: true });
|
||||
} else if (Notification.permission === 'granted') {
|
||||
registerSubscription(onSuccess);
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"CHANNEL_SELECTOR": {
|
||||
"COMING_SOON": "Coming Soon!"
|
||||
},
|
||||
"DESKTOP_NOTIFICATION_UNSUPPORTED": "This browser does not support desktop notifications",
|
||||
"SLASH_COMMANDS": {
|
||||
"HEADING_1": "Heading 1",
|
||||
"HEADING_2": "Heading 2",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"WEEK_NUMBER": "Week #{weekNumber}",
|
||||
"APPLY_BUTTON": "Apply",
|
||||
"CLEAR_BUTTON": "Clear",
|
||||
"INVALID_TIME_RANGE": "Please select a valid time range",
|
||||
"DATE_RANGE_INPUT": {
|
||||
"START": "Start Date",
|
||||
"END": "End Date"
|
||||
|
||||
@@ -400,6 +400,7 @@
|
||||
"OFFLINE": "Offline"
|
||||
},
|
||||
"WEBHOOK_URL": "Connector webhook URL",
|
||||
"WEBHOOK_URL_PLACEHOLDER": "http://shangwutong-connector:9100/webhooks/gochat/v1",
|
||||
"SUBMIT_BUTTON": "Create Shangwutong channel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the Shangwutong channel"
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"CHANNEL_SELECTOR": {
|
||||
"COMING_SOON": "即将到来!"
|
||||
},
|
||||
"DESKTOP_NOTIFICATION_UNSUPPORTED": "此浏览器不支持桌面通知",
|
||||
"SLASH_COMMANDS": {
|
||||
"HEADING_1": "标题 1",
|
||||
"HEADING_2": "标题 2",
|
||||
@@ -63,6 +64,7 @@
|
||||
"CODE": "代码",
|
||||
"BULLET_LIST": "无序列表",
|
||||
"ORDERED_LIST": "有序列表",
|
||||
"TABLE": "表格"
|
||||
"TABLE": "表格",
|
||||
"IMAGE": "图片"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"WEEK_NUMBER": "周#{weekNumber}",
|
||||
"APPLY_BUTTON": "应用",
|
||||
"CLEAR_BUTTON": "清除",
|
||||
"INVALID_TIME_RANGE": "请选择有效的时间范围",
|
||||
"DATE_RANGE_INPUT": {
|
||||
"START": "开始日期",
|
||||
"END": "结束时间"
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"CONNECT_YOUR_TIKTOK_PROFILE": "连接您的 TikTok 主页",
|
||||
"HELP": "要将您的 TikTok 主页添加为渠道,您需要点击\"使用 TikTok 继续\"来认证您的 TikTok 主页",
|
||||
"ERROR_MESSAGE": "连接 TikTok 时出错,请重试",
|
||||
"ERROR_AUTH": "There was an error connecting to TikTok, please try again"
|
||||
"ERROR_AUTH": "连接 TikTok 时出错,请重试"
|
||||
},
|
||||
"TWITTER": {
|
||||
"HELP": "若要将您的Twitter个人资料添加为频道,您需要通过点击“使用Twitter登录”来验证您的Twitter个人资料。 ",
|
||||
@@ -400,6 +400,7 @@
|
||||
"OFFLINE": "离线"
|
||||
},
|
||||
"WEBHOOK_URL": "Connector Webhook 地址",
|
||||
"WEBHOOK_URL_PLACEHOLDER": "http://shangwutong-connector:9100/webhooks/gochat/v1",
|
||||
"SUBMIT_BUTTON": "创建商务通渠道",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "无法保存商务通渠道"
|
||||
@@ -950,8 +951,8 @@
|
||||
"NOT_FOUND": "The template does not exist in the Meta platform."
|
||||
},
|
||||
"TEMPLATE_CREATION": {
|
||||
"SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval",
|
||||
"ERROR_MESSAGE": "Failed to create WhatsApp template"
|
||||
"SUCCESS_MESSAGE": "WhatsApp 模板创建成功,已提交审核",
|
||||
"ERROR_MESSAGE": "WhatsApp 模板创建失败"
|
||||
},
|
||||
"TEMPLATE_UPDATE_DIALOG": {
|
||||
"TITLE": "Edit survey details",
|
||||
@@ -966,7 +967,7 @@
|
||||
"GUIDANCE_NOTE": "This is a guidance check, not a guarantee of Meta approval.",
|
||||
"SUGGESTION_LABEL": "Suggested utility-safe rewrite",
|
||||
"APPLY": "Use this rewrite",
|
||||
"ERROR_MESSAGE": "Couldn't analyze the message. Please try again.",
|
||||
"ERROR_MESSAGE": "无法分析消息,请重试。",
|
||||
"CLASSIFICATION": {
|
||||
"LIKELY_UTILITY": "Likely Utility",
|
||||
"LIKELY_MARKETING": "Likely Marketing",
|
||||
|
||||
@@ -10,6 +10,7 @@ import cannedMgmt from './cannedMgmt.json';
|
||||
import chatlist from './chatlist.json';
|
||||
import companies from './companies.json';
|
||||
import components from './components.json';
|
||||
import contentTemplates from './contentTemplates.json';
|
||||
import contact from './contact.json';
|
||||
import contactFilters from './contactFilters.json';
|
||||
import conversation from './conversation.json';
|
||||
@@ -35,9 +36,11 @@ import setNewPassword from './setNewPassword.json';
|
||||
import settings from './settings.json';
|
||||
import signup from './signup.json';
|
||||
import sla from './sla.json';
|
||||
import snooze from './snooze.json';
|
||||
import superAdmin from './superAdmin.json';
|
||||
import teamsSettings from './teamsSettings.json';
|
||||
import whatsappTemplates from './whatsappTemplates.json';
|
||||
import yearInReview from './yearInReview.json';
|
||||
|
||||
export default {
|
||||
...advancedFilters,
|
||||
@@ -52,6 +55,7 @@ export default {
|
||||
...chatlist,
|
||||
...companies,
|
||||
...components,
|
||||
...contentTemplates,
|
||||
...contact,
|
||||
...contactFilters,
|
||||
...conversation,
|
||||
@@ -77,7 +81,9 @@ export default {
|
||||
...settings,
|
||||
...signup,
|
||||
...sla,
|
||||
...snooze,
|
||||
...superAdmin,
|
||||
...teamsSettings,
|
||||
...whatsappTemplates,
|
||||
...yearInReview,
|
||||
};
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
"LABEL": "Work Email",
|
||||
"PLACEHOLDER": "Enter your work email"
|
||||
},
|
||||
"SUBMIT": "Continue with SSO",
|
||||
"SUBMIT": "使用单点登录继续",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
|
||||
"ERROR_MESSAGE": "单点登录认证失败,请检查凭据后重试。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-6
@@ -71,10 +71,7 @@ const fetchClassifications = async () => {
|
||||
catalog.value = response.data;
|
||||
resetSelections();
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error.message ||
|
||||
t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.LOAD_ERROR')
|
||||
);
|
||||
useAlert(t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.LOAD_ERROR'));
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
@@ -140,8 +137,7 @@ const updateClassification = async (type, value) => {
|
||||
} catch (error) {
|
||||
resetSelections();
|
||||
useAlert(
|
||||
error.message ||
|
||||
t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.UPDATE_ERROR')
|
||||
t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.UPDATE_ERROR')
|
||||
);
|
||||
} finally {
|
||||
saving.value = '';
|
||||
|
||||
+148
-158
@@ -9,176 +9,166 @@ import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const validSessionId = value => /^[A-Za-z0-9]{11}$/.test(value || '');
|
||||
const validWebhookUrl = value => {
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
return (
|
||||
['http:', 'https:'].includes(parsed.protocol) &&
|
||||
!parsed.username &&
|
||||
!parsed.password &&
|
||||
!parsed.hash
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
return (
|
||||
['http:', 'https:'].includes(parsed.protocol) &&
|
||||
!parsed.username &&
|
||||
!parsed.password &&
|
||||
!parsed.hash
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
components: { PageHeader, NextButton },
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
channelName: '',
|
||||
sessionId: '',
|
||||
username: '',
|
||||
password: '',
|
||||
desiredPresence: 'online',
|
||||
webhookUrl: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'inboxes/getUIFlags' }),
|
||||
},
|
||||
validations: {
|
||||
channelName: { required },
|
||||
sessionId: { required, validSessionId },
|
||||
username: { required },
|
||||
password: { required },
|
||||
webhookUrl: { required, validWebhookUrl },
|
||||
},
|
||||
methods: {
|
||||
async createChannel() {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) return;
|
||||
components: { PageHeader, NextButton },
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
channelName: '',
|
||||
sessionId: '',
|
||||
username: '',
|
||||
password: '',
|
||||
desiredPresence: 'online',
|
||||
webhookUrl: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'inboxes/getUIFlags' }),
|
||||
},
|
||||
validations: {
|
||||
channelName: { required },
|
||||
sessionId: { required, validSessionId },
|
||||
username: { required },
|
||||
password: { required },
|
||||
webhookUrl: { required, validWebhookUrl },
|
||||
},
|
||||
methods: {
|
||||
async createChannel() {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) return;
|
||||
|
||||
try {
|
||||
const inbox = await this.$store.dispatch(
|
||||
'inboxes/createChannel',
|
||||
{
|
||||
name: this.channelName.trim(),
|
||||
channel: {
|
||||
type: 'shangwutong',
|
||||
session_id: this.sessionId.trim(),
|
||||
username: this.username.trim(),
|
||||
password: this.password,
|
||||
desired_presence: this.desiredPresence,
|
||||
webhook_url: this.webhookUrl.trim(),
|
||||
},
|
||||
}
|
||||
);
|
||||
router.replace({
|
||||
name: 'settings_inboxes_add_agents',
|
||||
params: { page: 'new', inbox_id: inbox.id },
|
||||
});
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error.message ||
|
||||
this.$t(
|
||||
'INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.API.ERROR_MESSAGE'
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
try {
|
||||
const inbox = await this.$store.dispatch('inboxes/createChannel', {
|
||||
name: this.channelName.trim(),
|
||||
channel: {
|
||||
type: 'shangwutong',
|
||||
session_id: this.sessionId.trim(),
|
||||
username: this.username.trim(),
|
||||
password: this.password,
|
||||
desired_presence: this.desiredPresence,
|
||||
webhook_url: this.webhookUrl.trim(),
|
||||
},
|
||||
});
|
||||
router.replace({
|
||||
name: 'settings_inboxes_add_agents',
|
||||
params: { page: 'new', inbox_id: inbox.id },
|
||||
});
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
this.$t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full p-6 col-span-6">
|
||||
<PageHeader
|
||||
:header-title="$t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.TITLE')"
|
||||
:header-content="$t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.DESC')"
|
||||
/>
|
||||
<form class="flex flex-col" @submit.prevent="createChannel">
|
||||
<label :class="{ error: v$.channelName.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.CHANNEL_NAME') }}
|
||||
<input
|
||||
v-model="channelName"
|
||||
type="text"
|
||||
@blur="v$.channelName.$touch"
|
||||
/>
|
||||
</label>
|
||||
<div class="h-full w-full p-6 col-span-6">
|
||||
<PageHeader
|
||||
:header-title="$t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.TITLE')"
|
||||
:header-content="$t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.DESC')"
|
||||
/>
|
||||
<form class="flex flex-col" @submit.prevent="createChannel">
|
||||
<label :class="{ error: v$.channelName.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.CHANNEL_NAME') }}
|
||||
<input
|
||||
v-model="channelName"
|
||||
type="text"
|
||||
@blur="v$.channelName.$touch"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label :class="{ error: v$.sessionId.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.SESSION_ID') }}
|
||||
<input
|
||||
v-model="sessionId"
|
||||
type="text"
|
||||
maxlength="11"
|
||||
autocomplete="off"
|
||||
@blur="v$.sessionId.$touch"
|
||||
/>
|
||||
<span v-if="v$.sessionId.$error" class="message">
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.SESSION_ID_ERROR'
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ error: v$.sessionId.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.SESSION_ID') }}
|
||||
<input
|
||||
v-model="sessionId"
|
||||
type="text"
|
||||
maxlength="11"
|
||||
autocomplete="off"
|
||||
@blur="v$.sessionId.$touch"
|
||||
/>
|
||||
<span v-if="v$.sessionId.$error" class="message">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.SESSION_ID_ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label :class="{ error: v$.username.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.USERNAME') }}
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
maxlength="255"
|
||||
autocomplete="username"
|
||||
@blur="v$.username.$touch"
|
||||
/>
|
||||
</label>
|
||||
<label :class="{ error: v$.username.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.USERNAME') }}
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
maxlength="255"
|
||||
autocomplete="username"
|
||||
@blur="v$.username.$touch"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label :class="{ error: v$.password.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PASSWORD') }}
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
maxlength="4096"
|
||||
autocomplete="new-password"
|
||||
@blur="v$.password.$touch"
|
||||
/>
|
||||
</label>
|
||||
<label :class="{ error: v$.password.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PASSWORD') }}
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
maxlength="4096"
|
||||
autocomplete="new-password"
|
||||
@blur="v$.password.$touch"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.DESIRED_PRESENCE') }}
|
||||
<select v-model="desiredPresence">
|
||||
<option value="online">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PRESENCE.ONLINE') }}
|
||||
</option>
|
||||
<option value="busy">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PRESENCE.BUSY') }}
|
||||
</option>
|
||||
<option value="away">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PRESENCE.AWAY') }}
|
||||
</option>
|
||||
<option value="offline">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PRESENCE.OFFLINE') }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.DESIRED_PRESENCE') }}
|
||||
<select v-model="desiredPresence">
|
||||
<option value="online">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PRESENCE.ONLINE') }}
|
||||
</option>
|
||||
<option value="busy">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PRESENCE.BUSY') }}
|
||||
</option>
|
||||
<option value="away">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PRESENCE.AWAY') }}
|
||||
</option>
|
||||
<option value="offline">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.PRESENCE.OFFLINE') }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label :class="{ error: v$.webhookUrl.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.WEBHOOK_URL') }}
|
||||
<input
|
||||
v-model="webhookUrl"
|
||||
type="url"
|
||||
placeholder="http://shangwutong-connector:9100/webhooks/gochat/v1"
|
||||
@blur="v$.webhookUrl.$touch"
|
||||
/>
|
||||
</label>
|
||||
<label :class="{ error: v$.webhookUrl.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.WEBHOOK_URL') }}
|
||||
<input
|
||||
v-model="webhookUrl"
|
||||
type="url"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.WEBHOOK_URL_PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.webhookUrl.$touch"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div class="w-full mt-4">
|
||||
<NextButton
|
||||
type="submit"
|
||||
solid
|
||||
blue
|
||||
:is-loading="uiFlags.isCreating"
|
||||
:label="
|
||||
$t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.SUBMIT_BUTTON')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="w-full mt-4">
|
||||
<NextButton
|
||||
type="submit"
|
||||
solid
|
||||
blue
|
||||
:is-loading="uiFlags.isCreating"
|
||||
:label="$t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.SUBMIT_BUTTON')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+4
-6
@@ -49,7 +49,7 @@ export default {
|
||||
const response = await InboxHealthAPI.getHealthStatus(this.inbox.id);
|
||||
this.health = response.data;
|
||||
} catch (error) {
|
||||
useAlert(error.message || this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||
useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||
} finally {
|
||||
this.isLoadingHealth = false;
|
||||
}
|
||||
@@ -60,8 +60,7 @@ export default {
|
||||
this.classifications = response.data;
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error.message ||
|
||||
this.$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATION_LOAD_ERROR')
|
||||
this.$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATION_LOAD_ERROR')
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -96,8 +95,7 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error.message ||
|
||||
this.$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATION_SYNC_ERROR')
|
||||
this.$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATION_SYNC_ERROR')
|
||||
);
|
||||
} finally {
|
||||
this.isSyncingClassifications = false;
|
||||
@@ -120,7 +118,7 @@ export default {
|
||||
useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||
await this.fetchHealth();
|
||||
} catch (error) {
|
||||
useAlert(error.message || this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||
useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ import 'floating-vue/dist/style.css';
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // https://github.com/intlify/vue-i18n/issues/1902
|
||||
locale: 'en',
|
||||
locale: window.chatwootConfig?.selectedLocale || 'zh_CN',
|
||||
messages: i18nMessages,
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // https://github.com/intlify/vue-i18n/issues/1902
|
||||
locale: 'en',
|
||||
locale: window.chatwootConfig?.selectedLocale || 'zh_CN',
|
||||
messages: i18nMessages,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user