Merge branch 'develop' into feat/email-forwarding

This commit is contained in:
Sivin Varghese
2025-05-29 13:04:35 +05:30
committed by GitHub
42 changed files with 1896 additions and 294 deletions
@@ -3,7 +3,7 @@
}
.tabs--container--with-border {
@apply border-b border-n-weak;
@apply border-b border-b-n-weak;
}
.tabs--container--compact.tab--chat-type {
@@ -0,0 +1,87 @@
<script setup>
import Button from 'dashboard/components-next/button/Button.vue';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { computed } from 'vue';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { useMapGetter } from 'dashboard/composables/store';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
const { updateUISettings } = useUISettings();
const currentAccountId = useMapGetter('getCurrentAccountId');
const isFeatureEnabledonAccount = useMapGetter(
'accounts/isFeatureEnabledonAccount'
);
const showCopilotTab = computed(() =>
isFeatureEnabledonAccount.value(currentAccountId.value, FEATURE_FLAGS.CAPTAIN)
);
const { uiSettings } = useUISettings();
const isContactSidebarOpen = computed(
() => uiSettings.value.is_contact_sidebar_open
);
const isCopilotPanelOpen = computed(
() => uiSettings.value.is_copilot_panel_open
);
const toggleConversationSidebarToggle = () => {
updateUISettings({
is_contact_sidebar_open: !isContactSidebarOpen.value,
is_copilot_panel_open: false,
});
};
const handleConversationSidebarToggle = () => {
updateUISettings({
is_contact_sidebar_open: true,
is_copilot_panel_open: false,
});
};
const handleCopilotSidebarToggle = () => {
updateUISettings({
is_contact_sidebar_open: false,
is_copilot_panel_open: true,
});
};
const keyboardEvents = {
'Alt+KeyO': {
action: toggleConversationSidebarToggle,
},
};
useKeyboardEvents(keyboardEvents);
</script>
<template>
<div
class="flex flex-col justify-center items-center absolute top-24 ltr:right-2 rtl:left-2 bg-n-solid-2 border border-n-weak rounded-full gap-2 p-1"
>
<Button
v-tooltip.top="$t('CONVERSATION.SIDEBAR.CONTACT')"
ghost
slate
sm
class="!rounded-full"
:class="{
'bg-n-alpha-2': isContactSidebarOpen,
}"
icon="i-ph-user-bold"
@click="handleConversationSidebarToggle"
/>
<Button
v-if="showCopilotTab"
v-tooltip.bottom="$t('CONVERSATION.SIDEBAR.COPILOT')"
ghost
slate
class="!rounded-full"
:class="{
'bg-n-alpha-2 !text-n-iris-9': isCopilotPanelOpen,
}"
sm
icon="i-woot-captain"
@click="handleCopilotSidebarToggle"
/>
</div>
</template>
@@ -1,21 +1,29 @@
<script setup>
import CopilotHeader from './CopilotHeader.vue';
import SidebarActionsHeader from './SidebarActionsHeader.vue';
</script>
<template>
<Story
title="Captain/Copilot/CopilotHeader"
title="Components/SidebarActionsHeader"
:layout="{ type: 'grid', width: '800px' }"
>
<!-- Default State -->
<Variant title="Default State">
<CopilotHeader />
<SidebarActionsHeader title="Default State" />
</Variant>
<!-- With New Conversation Button -->
<Variant title="With New Conversation Button">
<!-- eslint-disable-next-line vue/prefer-true-attribute-shorthand -->
<CopilotHeader :has-messages="true" />
<SidebarActionsHeader
title="With New Conversation Button"
:buttons="[
{
key: 'new_conversation',
icon: 'i-lucide-plus',
},
]"
/>
</Variant>
</Story>
</template>
@@ -0,0 +1,47 @@
<script setup>
import Button from './button/Button.vue';
defineProps({
title: {
type: String,
required: true,
},
buttons: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['click', 'close']);
const handleButtonClick = button => {
emit('click', button.key);
};
</script>
<template>
<div
class="flex items-center justify-between px-4 py-2 border-b border-n-weak h-12"
>
<div class="flex items-center justify-between gap-2 flex-1">
<span class="font-medium text-sm text-n-slate-12">{{ title }}</span>
<div class="flex items-center">
<Button
v-for="button in buttons"
:key="button.key"
v-tooltip="button.tooltip"
:icon="button.icon"
ghost
sm
@click="handleButtonClick(button)"
/>
<Button
v-tooltip="$t('GENERAL.CLOSE')"
icon="i-lucide-x"
ghost
sm
@click="$emit('close')"
/>
</div>
</div>
</div>
</template>
@@ -3,13 +3,15 @@ import { nextTick, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useTrack } from 'dashboard/composables';
import { COPILOT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { useUISettings } from 'dashboard/composables/useUISettings';
import CopilotInput from './CopilotInput.vue';
import CopilotLoader from './CopilotLoader.vue';
import CopilotAgentMessage from './CopilotAgentMessage.vue';
import CopilotAssistantMessage from './CopilotAssistantMessage.vue';
import ToggleCopilotAssistant from './ToggleCopilotAssistant.vue';
import Icon from '../icon/Icon.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import SidebarActionsHeader from 'dashboard/components-next/SidebarActionsHeader.vue';
const props = defineProps({
supportAgent: {
@@ -54,10 +56,6 @@ const useSuggestion = opt => {
useTrack(COPILOT_EVENTS.SEND_SUGGESTED);
};
const handleReset = () => {
emit('reset');
};
const chatContainer = ref(null);
const scrollToBottom = async () => {
@@ -82,6 +80,21 @@ const promptOptions = [
},
];
const { updateUISettings } = useUISettings();
const closeCopilotPanel = () => {
updateUISettings({
is_copilot_panel_open: false,
is_contact_sidebar_open: false,
});
};
const handleSidebarAction = action => {
if (action === 'reset') {
emit('reset');
}
};
watch(
[() => props.messages, () => props.isCaptainTyping],
() => {
@@ -93,6 +106,18 @@ watch(
<template>
<div class="flex flex-col h-full text-sm leading-6 tracking-tight w-full">
<SidebarActionsHeader
:title="$t('CAPTAIN.COPILOT.TITLE')"
:buttons="[
{
key: 'reset',
icon: 'i-lucide-refresh-ccw',
tooltip: $t('CAPTAIN.COPILOT.RESET'),
},
]"
@click="handleSidebarAction"
@close="closeCopilotPanel"
/>
<div ref="chatContainer" class="flex-1 px-4 py-4 space-y-6 overflow-y-auto">
<template v-for="message in messages" :key="message.id">
<CopilotAgentMessage
@@ -139,14 +164,6 @@ watch(
@set-assistant="$event => emit('setAssistant', $event)"
/>
<div v-else />
<button
v-if="messages.length"
class="text-xs flex items-center gap-1 hover:underline"
@click="handleReset"
>
<i class="i-lucide-refresh-ccw" />
<span>{{ $t('CAPTAIN.COPILOT.RESET') }}</span>
</button>
</div>
<CopilotInput class="mb-1 w-full" @send="sendMessage" />
</div>
@@ -1,32 +0,0 @@
<script setup>
import Button from '../button/Button.vue';
defineProps({
hasMessages: {
type: Boolean,
default: false,
},
});
defineEmits(['reset', 'close']);
</script>
<template>
<div
class="flex items-center justify-between px-5 py-2 border-b border-n-weak h-12"
>
<div class="flex items-center justify-between gap-2 flex-1">
<span class="font-medium text-sm text-n-slate-12">
{{ $t('CAPTAIN.COPILOT.TITLE') }}
</span>
<div class="flex items-center">
<Button
v-if="hasMessages"
icon="i-lucide-plus"
ghost
sm
@click="$emit('reset')"
/>
<Button icon="i-lucide-x" ghost sm @click="$emit('close')" />
</div>
</div>
</div>
</template>
@@ -519,7 +519,7 @@ const menuItems = computed(() => {
</div>
</section>
<nav class="grid flex-grow gap-2 px-2 pb-5 overflow-y-scroll no-scrollbar">
<ul class="flex flex-col gap-2 m-0 list-none">
<ul class="flex flex-col gap-1.5 m-0 list-none">
<SidebarGroup
v-for="item in menuItems"
:key="item.name"
@@ -814,7 +814,7 @@ watch(conversationFilters, (newVal, oldVal) => {
class="flex flex-col flex-shrink-0 bg-n-solid-1 conversations-list-wrap"
:class="[
{ hidden: !showConversationList },
isOnExpandedLayout ? 'basis-full' : 'w-[360px] 2xl:w-[420px]',
isOnExpandedLayout ? 'basis-full' : 'w-[340px] 2xl:w-[412px]',
]"
>
<slot />
@@ -80,16 +80,14 @@ const toggleConversationLayout = () => {
<template>
<div
class="flex items-center justify-between gap-2 px-4"
class="flex items-center justify-between gap-2 px-3 h-12"
:class="{
'pb-3 border-b border-n-strong': hasAppliedFiltersOrActiveFolders,
'pt-3 pb-2': showV4View,
'mb-2 pb-0': !showV4View,
'border-b border-n-strong': hasAppliedFiltersOrActiveFolders,
}"
>
<div class="flex items-center justify-center min-w-0">
<h1
class="text-lg font-medium truncate text-n-slate-12"
class="text-base font-medium truncate text-n-slate-12"
:title="pageTitle"
>
{{ pageTitle }}
@@ -20,7 +20,7 @@ export default {
<template>
<div>
<div
class="shadow-sm bg-slate-800 dark:bg-slate-700 rounded-[4px] items-center gap-3 inline-flex mb-2 max-w-[25rem] min-h-[1.875rem] min-w-[15rem] px-6 py-3 text-left"
class="shadow-sm bg-n-slate-12 dark:bg-n-slate-7 rounded-lg items-center gap-3 inline-flex mb-2 max-w-[25rem] min-h-[1.875rem] min-w-[15rem] px-6 py-3 text-left"
>
<div class="text-sm font-medium text-white dark:text-white">
{{ message }}
@@ -29,7 +29,7 @@ export default {
<router-link
v-if="action.type == 'link'"
:to="action.to"
class="font-medium cursor-pointer select-none text-woot-500 dark:text-woot-500 hover:text-woot-600 dark:hover:text-woot-600"
class="font-medium cursor-pointer select-none text-n-blue-10 hover:text-n-brand"
>
{{ action.message }}
</router-link>
@@ -1,62 +1,72 @@
<script>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import WootSnackbar from './Snackbar.vue';
import { emitter } from 'shared/helpers/mitt';
import { useI18n } from 'vue-i18n';
export default {
components: {
WootSnackbar,
},
props: {
duration: {
type: Number,
default: 2500,
},
const props = defineProps({
duration: {
type: Number,
default: 2500,
},
});
data() {
return {
snackMessages: [],
};
},
const { t } = useI18n();
mounted() {
emitter.on('newToastMessage', this.onNewToastMessage);
},
unmounted() {
emitter.off('newToastMessage', this.onNewToastMessage);
},
methods: {
onNewToastMessage({ message: originalMessage, action }) {
// FIX ME: This is a temporary workaround to pass string from functions
// that doesn't have the context of the VueApp.
const usei18n = action?.usei18n;
const duration = action?.duration || this.duration;
const message = usei18n ? this.$t(originalMessage) : originalMessage;
const snackMessages = ref([]);
const snackbarContainer = ref(null);
this.snackMessages.push({
key: new Date().getTime(),
message,
action,
});
window.setTimeout(() => {
this.snackMessages.splice(0, 1);
}, duration);
},
},
const showPopover = () => {
try {
const el = snackbarContainer.value;
if (el?.matches(':popover-open')) {
el.hidePopover();
}
el?.showPopover();
} catch (e) {
// ignore
}
};
const onNewToastMessage = ({ message: originalMessage, action }) => {
const message = action?.usei18n ? t(originalMessage) : originalMessage;
const duration = action?.duration || props.duration;
snackMessages.value.push({
key: Date.now(),
message,
action,
});
nextTick(showPopover);
setTimeout(() => {
snackMessages.value.shift();
}, duration);
};
onMounted(() => {
emitter.on('newToastMessage', onNewToastMessage);
});
onUnmounted(() => {
emitter.off('newToastMessage', onNewToastMessage);
});
</script>
<template>
<transition-group
name="toast-fade"
tag="div"
class="left-0 my-0 mx-auto max-w-[25rem] overflow-hidden absolute right-0 text-center top-4 z-[9999]"
<div
ref="snackbarContainer"
popover="manual"
class="fixed top-4 left-1/2 -translate-x-1/2 max-w-[25rem] w-[calc(100%-2rem)] text-center bg-transparent border-0 p-0 m-0 outline-none overflow-visible"
>
<WootSnackbar
v-for="snackMessage in snackMessages"
:key="snackMessage.key"
:message="snackMessage.message"
:action="snackMessage.action"
/>
</transition-group>
<transition-group name="toast-fade" tag="div">
<WootSnackbar
v-for="snackMessage in snackMessages"
:key="snackMessage.key"
:message="snackMessage.message"
:action="snackMessage.action"
/>
</transition-group>
</div>
</template>
@@ -48,12 +48,13 @@ useKeyboardEvents(keyboardEvents);
<template>
<woot-tabs
:index="activeTabIndex"
class="w-full px-4 py-0 tab--chat-type"
class="w-full px-3 -mt-1 py-0 tab--chat-type"
@change="onTabChange"
>
<woot-tabs-item
v-for="(item, index) in items"
:key="item.key"
class="text-sm"
:index="index"
:name="item.name"
:count="item.count"
@@ -4,17 +4,14 @@ import ConversationHeader from './ConversationHeader.vue';
import DashboardAppFrame from '../DashboardApp/Frame.vue';
import EmptyState from './EmptyState/EmptyState.vue';
import MessagesView from './MessagesView.vue';
import ConversationSidebar from './ConversationSidebar.vue';
export default {
components: {
ConversationSidebar,
ConversationHeader,
DashboardAppFrame,
EmptyState,
MessagesView,
},
props: {
inboxId: {
type: [Number, String],
@@ -34,7 +31,6 @@ export default {
default: true,
},
},
emits: ['contactPanelToggle'],
data() {
return { activeIndex: 0 };
},
@@ -86,9 +82,6 @@ export default {
}
this.$store.dispatch('conversationLabels/get', this.currentChat.id);
},
onToggleContactPanel() {
this.$emit('contactPanelToggle');
},
onDashboardAppTabChange(index) {
this.activeIndex = index;
},
@@ -98,7 +91,7 @@ export default {
<template>
<div
class="conversation-details-wrap bg-n-background"
class="conversation-details-wrap bg-n-background relative"
:class="{
'border-l rtl:border-l-0 rtl:border-r border-n-weak': !isOnExpandedLayout,
}"
@@ -107,14 +100,12 @@ export default {
v-if="currentChat.id"
:chat="currentChat"
:is-inbox-view="isInboxView"
:is-contact-panel-open="isContactPanelOpen"
:show-back-button="isOnExpandedLayout && !isInboxView"
@contact-panel-toggle="onToggleContactPanel"
/>
<woot-tabs
v-if="dashboardApps.length && currentChat.id"
:index="activeIndex"
class="-mt-px bg-white dashboard-app--tabs dark:bg-slate-900"
class="-mt-px dashboard-app--tabs border-t border-t-n-background"
@change="onDashboardAppTabChange"
>
<woot-tabs-item
@@ -130,18 +121,12 @@ export default {
v-if="currentChat.id"
:inbox-id="inboxId"
:is-inbox-view="isInboxView"
:is-contact-panel-open="isContactPanelOpen"
@contact-panel-toggle="onToggleContactPanel"
/>
<EmptyState
v-if="!currentChat.id && !isInboxView"
:is-on-expanded-layout="isOnExpandedLayout"
/>
<ConversationSidebar
v-if="showContactPanel"
:current-chat="currentChat"
@toggle-contact-panel="onToggleContactPanel"
/>
<slot />
</div>
<DashboardAppFrame
v-for="(dashboardApp, index) in dashboardApps"
@@ -243,7 +243,7 @@ export default {
<template>
<div
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full px-4 py-0 border-t-0 border-b-0 border-l-2 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3 group"
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full px-3 py-0 border-t-0 border-b-0 border-l-2 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3 group"
:class="{
'active animate-card-select bg-n-alpha-1 dark:bg-n-alpha-3 border-n-weak':
isActiveChat,
@@ -278,7 +278,7 @@ export default {
:badge="inboxBadge"
:username="currentContact.name"
:status="currentContact.availability_status"
size="40px"
size="32px"
/>
</div>
<div
@@ -1,6 +1,5 @@
<script>
import { mapGetters } from 'vuex';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import BackButton from '../BackButton.vue';
import inboxMixin from 'shared/mixins/inboxMixin';
import InboxName from '../InboxName.vue';
@@ -29,11 +28,7 @@ export default {
props: {
chat: {
type: Object,
default: () => {},
},
isContactPanelOpen: {
type: Boolean,
default: false,
default: () => ({}),
},
showBackButton: {
type: Boolean,
@@ -44,15 +39,6 @@ export default {
default: false,
},
},
emits: ['contactPanelToggle'],
setup(props, { emit }) {
const keyboardEvents = {
'Alt+KeyO': {
action: () => emit('contactPanelToggle'),
},
};
useKeyboardEvents(keyboardEvents);
},
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
@@ -99,13 +85,6 @@ export default {
}
return this.$t('CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_REPLY');
},
contactPanelToggleText() {
return `${
this.isContactPanelOpen
? this.$t('CONVERSATION.HEADER.CLOSE')
: this.$t('CONVERSATION.HEADER.OPEN')
} ${this.$t('CONVERSATION.HEADER.DETAILS')}`;
},
inbox() {
const { inbox_id: inboxId } = this.chat;
return this.$store.getters['inboxes/getInbox'](inboxId);
@@ -133,7 +112,7 @@ export default {
<template>
<div
class="flex flex-col items-center justify-between px-4 py-2 border-b bg-n-background border-n-weak md:flex-row"
class="flex flex-col items-center justify-between px-3 py-2 border-b bg-n-background border-n-weak md:flex-row h-12"
>
<div
class="flex flex-col items-center justify-center flex-1 w-full min-w-0"
@@ -150,6 +129,7 @@ export default {
:badge="inboxBadge"
:username="currentContact.name"
:status="currentContact.availability_status"
size="32px"
/>
<div
class="flex flex-col items-start min-w-0 ml-2 overflow-hidden rtl:ml-0 rtl:mr-2 w-fit"
@@ -157,9 +137,9 @@ export default {
<div
class="flex flex-row items-center max-w-full gap-1 p-0 m-0 w-fit"
>
<NextButton link slate @click.prevent="$emit('contactPanelToggle')">
<NextButton link slate>
<span
class="text-base font-medium truncate leading-tight text-n-slate-12"
class="text-sm font-medium truncate leading-tight text-n-slate-12"
>
{{ currentContact.name }}
</span>
@@ -180,19 +160,11 @@ export default {
<span v-if="isSnoozed" class="font-medium text-n-amber-10">
{{ snoozedDisplayText }}
</span>
<NextButton
link
xs
blue
:label="contactPanelToggleText"
@click="$emit('contactPanelToggle')"
/>
</div>
</div>
</div>
<div
class="flex flex-row items-center justify-end flex-grow gap-2 mt-3 header-actions-wrap lg:mt-0"
:class="{ 'justify-end': isContactPanelOpen }"
>
<SLACardLabel v-if="hasSlaPolicyId" :chat="chat" show-extended-info />
<Linear
@@ -1,11 +1,10 @@
<script setup>
import { computed, ref } from 'vue';
import { computed } from 'vue';
import CopilotContainer from '../../copilot/CopilotContainer.vue';
import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel.vue';
import TabBar from 'dashboard/components-next/tabbar/TabBar.vue';
import { useI18n } from 'vue-i18n';
import { useMapGetter } from 'dashboard/composables/store';
import { FEATURE_FLAGS } from '../../../featureFlags';
import { useUISettings } from 'dashboard/composables/useUISettings';
const props = defineProps({
currentChat: {
@@ -14,33 +13,8 @@ const props = defineProps({
},
});
const emit = defineEmits(['toggleContactPanel']);
const { t } = useI18n();
const channelType = computed(() => props.currentChat?.meta?.channel || '');
const CONTACT_TABS_OPTIONS = [
{ key: 'CONTACT', value: 'contact' },
{ key: 'COPILOT', value: 'copilot' },
];
const tabs = computed(() => {
return CONTACT_TABS_OPTIONS.map(tab => ({
label: t(`CONVERSATION.SIDEBAR.${tab.key}`),
value: tab.value,
}));
});
const activeTab = ref(0);
const toggleContactPanel = () => {
emit('toggleContactPanel');
};
const handleTabChange = selectedTab => {
activeTab.value = tabs.value.findIndex(
tabItem => tabItem.value === selectedTab.value
);
};
const currentAccountId = useMapGetter('getCurrentAccountId');
const isFeatureEnabledonAccount = useMapGetter(
'accounts/isFeatureEnabledonAccount'
@@ -49,29 +23,37 @@ const isFeatureEnabledonAccount = useMapGetter(
const showCopilotTab = computed(() =>
isFeatureEnabledonAccount.value(currentAccountId.value, FEATURE_FLAGS.CAPTAIN)
);
const { uiSettings } = useUISettings();
const activeTab = computed(() => {
const {
is_contact_sidebar_open: isContactSidebarOpen,
is_copilot_panel_open: isCopilotPanelOpen,
} = uiSettings.value;
if (isContactSidebarOpen) {
return 0;
}
if (isCopilotPanelOpen) {
return 1;
}
return null;
});
</script>
<template>
<div
class="ltr:border-l rtl:border-r border-n-weak h-full overflow-hidden z-10 w-80 min-w-80 2xl:min-w-96 2xl:w-96 flex flex-col bg-n-background"
>
<div v-if="showCopilotTab" class="p-2">
<TabBar
:tabs="tabs"
:initial-active-tab="activeTab"
class="w-full [&>button]:w-full"
@tab-changed="handleTabChange"
/>
</div>
<div class="flex flex-1 overflow-auto">
<ContactPanel
v-if="!activeTab"
v-show="activeTab === 0"
:conversation-id="currentChat.id"
:inbox-id="currentChat.inbox_id"
:on-toggle="toggleContactPanel"
/>
<CopilotContainer
v-else-if="activeTab === 1 && showCopilotTab"
v-show="activeTab === 1 && showCopilotTab"
:key="currentChat.id"
:conversation-inbox-type="channelType"
:conversation-id="currentChat.id"
@@ -39,8 +39,6 @@ import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { FEATURE_FLAGS } from '../../../featureFlags';
import { INBOX_TYPES } from 'dashboard/helper/inbox';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
Message,
@@ -49,20 +47,8 @@ export default {
ReplyBox,
Banner,
ConversationLabelSuggestion,
NextButton,
},
mixins: [inboxMixin],
props: {
isContactPanelOpen: {
type: Boolean,
default: false,
},
isInboxView: {
type: Boolean,
default: false,
},
},
emits: ['contactPanelToggle'],
setup() {
const isPopOutReplyBox = ref(false);
const conversationPanelRef = ref(null);
@@ -205,12 +191,6 @@ export default {
isATweet() {
return this.conversationType === 'tweet';
},
isRightOrLeftIcon() {
if (this.isContactPanelOpen) {
return 'arrow-chevron-right';
}
return 'arrow-chevron-left';
},
getLastSeenAt() {
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
return contactLastSeenAt;
@@ -446,9 +426,6 @@ export default {
relevantMessages
);
},
onToggleContactPanel() {
this.$emit('contactPanelToggle');
},
setScrollParams() {
this.heightBeforeLoad = this.conversationPanel.scrollHeight;
this.scrollTopBeforeLoad = this.conversationPanel.scrollTop;
@@ -532,19 +509,6 @@ export default {
class="mx-2 mt-2 overflow-hidden rounded-lg"
:banner-message="$t('CONVERSATION.OLD_INSTAGRAM_INBOX_REPLY_BANNER')"
/>
<div class="flex justify-end">
<NextButton
faded
xs
slate
class="!rounded-r-none rtl:rotate-180 !rounded-2xl !fixed z-10"
:icon="
isContactPanelOpen ? 'i-ph-caret-right-fill' : 'i-ph-caret-left-fill'
"
:class="isInboxView ? 'top-52 md:top-40' : 'top-32'"
@click="onToggleContactPanel"
/>
</div>
<NextMessageList
v-if="showNextBubbles"
ref="conversationPanelRef"
@@ -4,6 +4,7 @@
"PHONE_INPUT": {
"PLACEHOLDER": "Search",
"EMPTY_STATE": "No results found"
}
},
"CLOSE": "Close"
}
}
@@ -11,14 +11,14 @@ import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue';
import ContactConversations from './ContactConversations.vue';
import ConversationAction from './ConversationAction.vue';
import ConversationParticipant from './ConversationParticipant.vue';
import ContactInfo from './contact/ContactInfo.vue';
import ContactNotes from './contact/ContactNotes.vue';
import ConversationInfo from './ConversationInfo.vue';
import CustomAttributes from './customAttributes/CustomAttributes.vue';
import Draggable from 'vuedraggable';
import MacrosList from './Macros/List.vue';
import ShopifyOrdersList from '../../../components/widgets/conversation/ShopifyOrdersList.vue';
import ShopifyOrdersList from 'dashboard/components/widgets/conversation/ShopifyOrdersList.vue';
import SidebarActionsHeader from 'dashboard/components-next/SidebarActionsHeader.vue';
const props = defineProps({
conversationId: {
@@ -29,10 +29,6 @@ const props = defineProps({
type: Number,
default: undefined,
},
onToggle: {
type: Function,
default: () => {},
},
});
const {
@@ -89,8 +85,6 @@ watch(conversationId, (newConversationId, prevConversationId) => {
watch(contactId, getContactDetails);
const onPanelToggle = props.onToggle;
const onDragEnd = () => {
dragging.value = false;
updateUISettings({
@@ -98,6 +92,13 @@ const onDragEnd = () => {
});
};
const closeContactPanel = () => {
updateUISettings({
is_contact_sidebar_open: false,
is_copilot_panel_open: false,
});
};
onMounted(() => {
conversationSidebarItems.value = conversationSidebarItemsOrder.value;
getContactDetails();
@@ -107,11 +108,11 @@ onMounted(() => {
<template>
<div class="w-full">
<ContactInfo
:contact="contact"
:channel-type="channelType"
@toggle-panel="onPanelToggle"
<SidebarActionsHeader
:title="$t('CONVERSATION.SIDEBAR.CONTACT')"
@close="closeContactPanel"
/>
<ContactInfo :contact="contact" :channel-type="channelType" />
<div class="list-group pb-8">
<Draggable
:list="conversationSidebarItems"
@@ -10,6 +10,8 @@ import { BUS_EVENTS } from 'shared/constants/busEvents';
import CmdBarConversationSnooze from 'dashboard/routes/dashboard/commands/CmdBarConversationSnooze.vue';
import { emitter } from 'shared/helpers/mitt';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import SidepanelSwitch from 'dashboard/components-next/Conversation/SidepanelSwitch.vue';
import ConversationSidebar from 'dashboard/components/widgets/conversation/ConversationSidebar.vue';
export default {
components: {
@@ -17,6 +19,8 @@ export default {
ConversationBox,
PopOverSearch,
CmdBarConversationSnooze,
SidepanelSwitch,
ConversationSidebar,
},
beforeRouteLeave(to, from, next) {
// Clear selected state if navigating away from a conversation to a route without a conversationId to prevent stale data issues
@@ -87,13 +91,17 @@ export default {
this.uiSettings;
return conversationDisplayType !== CONDENSED;
},
isContactPanelOpen() {
if (this.currentChat.id) {
const { is_contact_sidebar_open: isContactSidebarOpen } =
this.uiSettings;
return isContactSidebarOpen;
shouldShowSidebar() {
if (!this.currentChat.id) {
return false;
}
return false;
const {
is_contact_sidebar_open: isContactSidebarOpen,
is_copilot_panel_open: isCopilotPanelOpen,
} = this.uiSettings;
return isContactSidebarOpen || isCopilotPanelOpen;
},
showPopOverSearch() {
return !this.isFeatureEnabledonAccount(
@@ -189,11 +197,6 @@ export default {
this.$store.dispatch('clearSelectedState');
}
},
onToggleContactPanel() {
this.updateUISettings({
is_contact_sidebar_open: !this.isContactPanelOpen,
});
},
onSearch() {
this.showSearchModal = true;
},
@@ -225,10 +228,11 @@ export default {
<ConversationBox
v-if="showMessageView"
:inbox-id="inboxId"
:is-contact-panel-open="isContactPanelOpen"
:is-on-expanded-layout="isOnExpandedLayout"
@contact-panel-toggle="onToggleContactPanel"
/>
>
<SidepanelSwitch v-if="currentChat.id" />
</ConversationBox>
<ConversationSidebar v-if="shouldShowSidebar" :current-chat="currentChat" />
<CmdBarConversationSnooze />
</section>
</template>
+10 -17
View File
@@ -2,6 +2,7 @@ import { createApp } from 'vue';
import VueDOMPurifyHTML from 'vue-dompurify-html';
import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer';
import { directive as onClickaway } from 'vue3-click-away';
import { isSameHost } from '@chatwoot/utils';
import slugifyWithCounter from '@sindresorhus/slugify';
import PublicArticleSearch from './components/PublicArticleSearch.vue';
@@ -27,31 +28,23 @@ export const getHeadingsfromTheArticle = () => {
export const openExternalLinksInNewTab = () => {
const { customDomain, hostURL } = window.portalConfig;
const isSameHost =
window.location.href.includes(customDomain) ||
window.location.href.includes(hostURL);
// Modify external links only on articles page
const isOnArticlePage =
isSameHost && document.querySelector('#cw-article-content') !== null;
document.querySelector('#cw-article-content') !== null;
document.addEventListener('click', event => {
if (!isOnArticlePage) return;
// Some of the links come wrapped in strong tag through prosemirror
const link = event.target.closest('a');
const isTagAnchor = event.target.tagName === 'A';
const isParentTagAnchor =
event.target.tagName === 'STRONG' &&
event.target.parentNode.tagName === 'A';
if (isTagAnchor || isParentTagAnchor) {
const link = isTagAnchor ? event.target : event.target.parentNode;
if (link) {
const currentLocation = window.location.href;
const linkHref = link.href;
// Check against current location and custom domains
const isInternalLink =
link.hostname === window.location.hostname ||
link.href.includes(customDomain) ||
link.href.includes(hostURL);
isSameHost(linkHref, currentLocation) ||
(customDomain && isSameHost(linkHref, customDomain)) ||
(hostURL && isSameHost(linkHref, hostURL));
if (!isInternalLink) {
link.target = '_blank';
+140 -1
View File
@@ -1,6 +1,9 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { JSDOM } from 'jsdom';
import { InitializationHelpers } from '../portalHelpers';
import {
InitializationHelpers,
openExternalLinksInNewTab,
} from '../portalHelpers';
describe('InitializationHelpers.navigateToLocalePage', () => {
let dom;
@@ -44,3 +47,139 @@ describe('InitializationHelpers.navigateToLocalePage', () => {
);
});
});
describe('openExternalLinksInNewTab', () => {
let dom;
let document;
let window;
beforeEach(() => {
dom = new JSDOM(
`<!DOCTYPE html>
<html>
<body>
<div id="cw-article-content">
<a href="https://external.com" id="external">External</a>
<a href="https://app.chatwoot.com/page" id="internal">Internal</a>
<a href="https://custom.domain.com/page" id="custom">Custom</a>
<a href="https://example.com" id="nested"><code>Code</code><strong>Bold</strong></a>
<ul>
<li>Visit the preferences centre here &gt; <a href="https://external.com" id="list-link"><strong>https://external.com</strong></a></li>
</ul>
</div>
</body>
</html>`,
{ url: 'https://app.chatwoot.com/hc/article' }
);
document = dom.window.document;
window = dom.window;
window.portalConfig = {
customDomain: 'custom.domain.com',
hostURL: 'app.chatwoot.com',
};
global.document = document;
global.window = window;
});
afterEach(() => {
dom = null;
document = null;
window = null;
delete global.document;
delete global.window;
});
const simulateClick = selector => {
const element = document.querySelector(selector);
const event = new window.MouseEvent('click', { bubbles: true });
element.dispatchEvent(event);
return element.closest('a') || element;
};
it('opens external links in new tab', () => {
openExternalLinksInNewTab();
const link = simulateClick('#external');
expect(link.target).toBe('_blank');
expect(link.rel).toBe('noopener noreferrer');
});
it('preserves internal links', () => {
openExternalLinksInNewTab();
const internal = simulateClick('#internal');
const custom = simulateClick('#custom');
expect(internal.target).not.toBe('_blank');
expect(custom.target).not.toBe('_blank');
});
it('handles clicks on nested elements', () => {
openExternalLinksInNewTab();
simulateClick('#nested code');
simulateClick('#nested strong');
const link = document.getElementById('nested');
expect(link.target).toBe('_blank');
expect(link.rel).toBe('noopener noreferrer');
});
it('handles links inside list items with strong tags', () => {
openExternalLinksInNewTab();
// Click on the strong element inside the link in the list
simulateClick('#list-link strong');
const link = document.getElementById('list-link');
expect(link.target).toBe('_blank');
expect(link.rel).toBe('noopener noreferrer');
});
it('opens external links in a new tab even if customDomain is empty', () => {
window = dom.window;
window.portalConfig = {
hostURL: 'app.chatwoot.com',
};
global.window = window;
openExternalLinksInNewTab();
const link = simulateClick('#external');
const internal = simulateClick('#internal');
const custom = simulateClick('#custom');
expect(link.target).toBe('_blank');
expect(link.rel).toBe('noopener noreferrer');
expect(internal.target).not.toBe('_blank');
// this will be blank since the configs customDomain is empty
// which is a fair expectation
expect(custom.target).toBe('_blank');
});
it('opens external links in a new tab even if hostURL is empty', () => {
window = dom.window;
window.portalConfig = {
customDomain: 'custom.domain.com',
};
global.window = window;
openExternalLinksInNewTab();
const link = simulateClick('#external');
const internal = simulateClick('#internal');
const custom = simulateClick('#custom');
expect(link.target).toBe('_blank');
expect(link.rel).toBe('noopener noreferrer');
expect(internal.target).not.toBe('_blank');
expect(custom.target).not.toBe('_blank');
});
});
@@ -5,6 +5,7 @@ module ActivityMessageHandler
include LabelActivityMessageHandler
include SlaActivityMessageHandler
include TeamActivityMessageHandler
include CsatActivityMessageHandler
private
@@ -0,0 +1,8 @@
module CsatActivityMessageHandler
extend ActiveSupport::Concern
def create_csat_not_sent_activity_message
content = I18n.t('conversations.activity.csat.not_sent_due_to_messaging_window')
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
end
end
@@ -17,7 +17,7 @@ class MessageTemplates::HookExecutionService
::MessageTemplates::Template::OutOfOffice.new(conversation: conversation).perform if should_send_out_of_office_message?
::MessageTemplates::Template::Greeting.new(conversation: conversation).perform if should_send_greeting?
::MessageTemplates::Template::EmailCollect.new(conversation: conversation).perform if inbox.enable_email_collect && should_send_email_collect?
::MessageTemplates::Template::CsatSurvey.new(conversation: conversation).perform if should_send_csat_survey?
handle_csat_survey
end
def should_send_out_of_office_message?
@@ -65,13 +65,26 @@ class MessageTemplates::HookExecutionService
true
end
def should_send_csat_survey?
def handle_csat_survey
return unless csat_enabled_conversation?
# only send CSAT once in a conversation
return if conversation.messages.where(content_type: :input_csat).present?
return if csat_already_sent?
true
# Only send CSAT if agent can still reply by checking the messaging window restriction
# https://www.chatwoot.com/docs/self-hosted/supported-features#outgoing-message-restriction
if within_messaging_window?
::MessageTemplates::Template::CsatSurvey.new(conversation: conversation).perform
else
conversation.create_csat_not_sent_activity_message
end
end
def csat_already_sent?
conversation.messages.where(content_type: :input_csat).present?
end
def within_messaging_window?
conversation.can_reply?
end
end
MessageTemplates::HookExecutionService.prepend_mod_with('MessageTemplates::HookExecutionService')
+2
View File
@@ -185,6 +185,8 @@ en:
removed: '%{user_name} removed %{labels}'
sla:
added: '%{user_name} added SLA policy %{sla_name}'
csat:
not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
removed: '%{user_name} removed SLA policy %{sla_name}'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
+1 -1
View File
@@ -34,7 +34,7 @@
"@breezystack/lamejs": "^1.2.7",
"@chatwoot/ninja-keys": "1.2.3",
"@chatwoot/prosemirror-schema": "1.1.1-next",
"@chatwoot/utils": "^0.0.43",
"@chatwoot/utils": "^0.0.45",
"@formkit/core": "^1.6.7",
"@formkit/vue": "^1.6.7",
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
+5 -5
View File
@@ -23,8 +23,8 @@ importers:
specifier: 1.1.1-next
version: 1.1.1-next
'@chatwoot/utils':
specifier: ^0.0.43
version: 0.0.43
specifier: ^0.0.45
version: 0.0.45
'@formkit/core':
specifier: ^1.6.7
version: 1.6.7
@@ -406,8 +406,8 @@ packages:
'@chatwoot/prosemirror-schema@1.1.1-next':
resolution: {integrity: sha512-/M2qZ+ZF7GlQNt1riwVP499fvp3hxSqd5iy8hxyF9pkj9qQ+OKYn5JK+v3qwwqQY3IxhmNOn1Lp6tm7vstrd9Q==}
'@chatwoot/utils@0.0.43':
resolution: {integrity: sha512-kMIXAGebCak9qOi68QnGer+rQLLo/z2N9cR+7tvGdZCW0ThDiVCF7JbHYHVDlYsdDFIx0FLlyIdCfEbooVT2Dw==}
'@chatwoot/utils@0.0.45':
resolution: {integrity: sha512-zqmuri6MrEFAY1tLv7Z3HBy4Ig60LhSrLkEiHegVsOVSxPv4Bedq+xmAW7LphvcLNgbkkvu17MU91gvMVlpEHw==}
engines: {node: '>=10'}
'@codemirror/commands@6.7.0':
@@ -5255,7 +5255,7 @@ snapshots:
prosemirror-utils: 1.2.2(prosemirror-model@1.22.3)(prosemirror-state@1.4.3)
prosemirror-view: 1.34.1
'@chatwoot/utils@0.0.43':
'@chatwoot/utils@0.0.45':
dependencies:
date-fns: 2.30.0
+14
View File
@@ -435,6 +435,20 @@ RSpec.describe Conversation do
end
end
describe '#create_csat_not_sent_activity_message' do
subject(:create_csat_not_sent_activity_message) { conversation.create_csat_not_sent_activity_message }
let(:conversation) { create(:conversation) }
it 'creates CSAT not sent activity message' do
create_csat_not_sent_activity_message
expect(Conversations::ActivityMessageJob)
.to(have_been_enqueued.at_least(:once).with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id,
message_type: :activity,
content: 'CSAT survey not sent due to outgoing message restrictions' }))
end
end
describe 'unread_messages' do
subject(:unread_messages) { conversation.unread_messages }
@@ -121,8 +121,9 @@ describe MessageTemplates::HookExecutionService do
create(:message, conversation: conversation, message_type: 'incoming')
end
it 'calls ::MessageTemplates::Template::CsatSurvey when a conversation is resolved in an inbox with survey enabled' do
it 'calls ::MessageTemplates::Template::CsatSurvey when a conversation is resolved in an inbox with survey enabled and can reply' do
conversation.inbox.update(csat_survey_enabled: true)
allow(conversation).to receive(:can_reply?).and_return(true)
conversation.resolved!
Conversations::ActivityMessageJob.perform_now(conversation,
@@ -172,6 +173,32 @@ describe MessageTemplates::HookExecutionService do
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new).with(conversation: conversation)
expect(csat_survey).not_to have_received(:perform)
end
it 'will not call ::MessageTemplates::Template::CsatSurvey if cannot reply' do
conversation.inbox.update(csat_survey_enabled: true)
allow(conversation).to receive(:can_reply?).and_return(false)
conversation.resolved!
Conversations::ActivityMessageJob.perform_now(conversation,
{ account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
content: 'Conversation marked resolved!!' })
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new).with(conversation: conversation)
expect(csat_survey).not_to have_received(:perform)
end
it 'creates activity message when CSAT not sent due to messaging window restriction' do
conversation.inbox.update(csat_survey_enabled: true)
allow(conversation).to receive(:can_reply?).and_return(false)
allow(conversation).to receive(:create_csat_not_sent_activity_message)
conversation.resolved!
Conversations::ActivityMessageJob.perform_now(conversation,
{ account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
content: 'Conversation marked resolved!!' })
expect(conversation).to have_received(:create_csat_not_sent_activity_message)
end
end
context 'when it is after working hours' do
+7
View File
@@ -60,6 +60,10 @@ webhook:
$ref: ./resource/webhook.yml
account:
$ref: ./resource/account.yml
account_detail:
$ref: ./resource/account_detail.yml
account_show_response:
$ref: ./resource/account_show_response.yml
account_user:
$ref: ./resource/account_user.yml
platform_account:
@@ -87,6 +91,9 @@ public_inbox:
account_create_update_payload:
$ref: ./request/account/create_update_payload.yml
account_update_payload:
$ref: ./request/account/update_payload.yml
account_user_create_update_payload:
$ref: ./request/account_user/create_update_payload.yml
@@ -0,0 +1,49 @@
type: object
properties:
name:
type: string
description: Name of the account
example: 'My Account'
locale:
type: string
description: The locale of the account
example: 'en'
domain:
type: string
description: The domain of the account
example: 'example.com'
support_email:
type: string
description: The support email of the account
example: 'support@example.com'
# Settings parameters (stored in settings JSONB column)
auto_resolve_after:
type: integer
minimum: 10
maximum: 1439856
nullable: true
description: Auto resolve conversations after specified minutes
example: 1440
auto_resolve_message:
type: string
nullable: true
description: Message to send when auto resolving
example: "This conversation has been automatically resolved due to inactivity"
auto_resolve_ignore_waiting:
type: boolean
nullable: true
description: Whether to ignore waiting conversations for auto resolve
example: false
# Custom attributes parameters (stored in custom_attributes JSONB column)
industry:
type: string
description: Industry type
example: "Technology"
company_size:
type: string
description: Company size
example: "50-100"
timezone:
type: string
description: Account timezone
example: "UTC"
@@ -0,0 +1,84 @@
type: object
properties:
id:
type: number
description: Account ID
name:
type: string
description: Name of the account
locale:
type: string
description: The locale of the account
domain:
type: string
description: The domain of the account
support_email:
type: string
description: The support email of the account
status:
type: string
description: The status of the account
created_at:
type: string
format: date-time
description: The creation date of the account
cache_keys:
type: object
description: Cache keys for the account
features:
type: array
items:
type: string
description: Enabled features for the account
settings:
type: object
description: Account settings
properties:
auto_resolve_after:
type: number
description: Auto resolve conversations after specified minutes
auto_resolve_message:
type: string
description: Message to send when auto resolving
auto_resolve_ignore_waiting:
type: boolean
description: Whether to ignore waiting conversations for auto resolve
custom_attributes:
type: object
description: Custom attributes of the account
properties:
plan_name:
type: string
description: Subscription plan name
subscribed_quantity:
type: number
description: Subscribed quantity
subscription_status:
type: string
description: Subscription status
subscription_ends_on:
type: string
format: date
description: Subscription end date
industry:
type: string
description: Industry type
company_size:
type: string
description: Company size
timezone:
type: string
description: Account timezone
logo:
type: string
description: Account logo URL
onboarding_step:
type: string
description: Current onboarding step
marked_for_deletion_at:
type: string
format: date-time
description: When account was marked for deletion
marked_for_deletion_reason:
type: string
description: Reason for account deletion
@@ -0,0 +1,13 @@
allOf:
- $ref: '#/components/schemas/account_detail'
- type: object
properties:
latest_chatwoot_version:
type: string
description: Latest version of Chatwoot available
example: "3.0.0"
subscribed_features:
type: array
items:
type: string
description: List of subscribed enterprise features (if enterprise edition is enabled)
@@ -0,0 +1,28 @@
tags:
- Account
operationId: get-account-details
summary: Get account details
description: Get the details of the current account
security:
- userApiKey: []
parameters:
- $ref: '#/components/parameters/account_id'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/account_show_response'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
'404':
description: Account not found
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
@@ -0,0 +1,43 @@
tags:
- Account
operationId: update-account
summary: Update account
description: Update account details, settings, and custom attributes
security:
- userApiKey: []
parameters:
- $ref: '#/components/parameters/account_id'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/account_update_payload'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/account_update_payload'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/account_detail'
'401':
description: Unauthorized (requires administrator role)
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
'404':
description: Account not found
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
'422':
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
+9
View File
@@ -166,6 +166,15 @@
# ------------ Application API routes ------------#
# Accounts
/api/v1/accounts/{id}:
parameters:
- $ref: '#/components/parameters/account_id'
get:
$ref: ./application/accounts/show.yml
patch:
$ref: ./application/accounts/update.yml
# AgentBots
/api/v1/accounts/{account_id}/agent_bots:
parameters:
+331
View File
@@ -1476,6 +1476,138 @@
}
}
},
"/api/v1/accounts/{id}": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
}
],
"get": {
"tags": [
"Account"
],
"operationId": "get-account-details",
"summary": "Get account details",
"description": "Get the details of the current account",
"security": [
{
"userApiKey": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/account_id"
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/account_show_response"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
},
"404": {
"description": "Account not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
},
"patch": {
"tags": [
"Account"
],
"operationId": "update-account",
"summary": "Update account",
"description": "Update account details, settings, and custom attributes",
"security": [
{
"userApiKey": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/account_id"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/account_update_payload"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/account_update_payload"
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/account_detail"
}
}
}
},
"401": {
"description": "Unauthorized (requires administrator role)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
},
"404": {
"description": "Account not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
},
"422": {
"description": "Validation error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/api/v1/accounts/{account_id}/agent_bots": {
"parameters": [
{
@@ -8774,6 +8906,145 @@
}
}
},
"account_detail": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "Account ID"
},
"name": {
"type": "string",
"description": "Name of the account"
},
"locale": {
"type": "string",
"description": "The locale of the account"
},
"domain": {
"type": "string",
"description": "The domain of the account"
},
"support_email": {
"type": "string",
"description": "The support email of the account"
},
"status": {
"type": "string",
"description": "The status of the account"
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "The creation date of the account"
},
"cache_keys": {
"type": "object",
"description": "Cache keys for the account"
},
"features": {
"type": "array",
"items": {
"type": "string"
},
"description": "Enabled features for the account"
},
"settings": {
"type": "object",
"description": "Account settings",
"properties": {
"auto_resolve_after": {
"type": "number",
"description": "Auto resolve conversations after specified minutes"
},
"auto_resolve_message": {
"type": "string",
"description": "Message to send when auto resolving"
},
"auto_resolve_ignore_waiting": {
"type": "boolean",
"description": "Whether to ignore waiting conversations for auto resolve"
}
}
},
"custom_attributes": {
"type": "object",
"description": "Custom attributes of the account",
"properties": {
"plan_name": {
"type": "string",
"description": "Subscription plan name"
},
"subscribed_quantity": {
"type": "number",
"description": "Subscribed quantity"
},
"subscription_status": {
"type": "string",
"description": "Subscription status"
},
"subscription_ends_on": {
"type": "string",
"format": "date",
"description": "Subscription end date"
},
"industry": {
"type": "string",
"description": "Industry type"
},
"company_size": {
"type": "string",
"description": "Company size"
},
"timezone": {
"type": "string",
"description": "Account timezone"
},
"logo": {
"type": "string",
"description": "Account logo URL"
},
"onboarding_step": {
"type": "string",
"description": "Current onboarding step"
},
"marked_for_deletion_at": {
"type": "string",
"format": "date-time",
"description": "When account was marked for deletion"
},
"marked_for_deletion_reason": {
"type": "string",
"description": "Reason for account deletion"
}
}
}
}
},
"account_show_response": {
"allOf": [
{
"$ref": "#/components/schemas/account_detail"
},
{
"type": "object",
"properties": {
"latest_chatwoot_version": {
"type": "string",
"description": "Latest version of Chatwoot available",
"example": "3.0.0"
},
"subscribed_features": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of subscribed enterprise features (if enterprise edition is enabled)"
}
}
}
]
},
"account_user": {
"type": "array",
"description": "Array of account users",
@@ -9113,6 +9384,66 @@
}
}
},
"account_update_payload": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the account",
"example": "My Account"
},
"locale": {
"type": "string",
"description": "The locale of the account",
"example": "en"
},
"domain": {
"type": "string",
"description": "The domain of the account",
"example": "example.com"
},
"support_email": {
"type": "string",
"description": "The support email of the account",
"example": "support@example.com"
},
"auto_resolve_after": {
"type": "integer",
"minimum": 10,
"maximum": 1439856,
"nullable": true,
"description": "Auto resolve conversations after specified minutes",
"example": 1440
},
"auto_resolve_message": {
"type": "string",
"nullable": true,
"description": "Message to send when auto resolving",
"example": "This conversation has been automatically resolved due to inactivity"
},
"auto_resolve_ignore_waiting": {
"type": "boolean",
"nullable": true,
"description": "Whether to ignore waiting conversations for auto resolve",
"example": false
},
"industry": {
"type": "string",
"description": "Industry type",
"example": "Technology"
},
"company_size": {
"type": "string",
"description": "Company size",
"example": "50-100"
},
"timezone": {
"type": "string",
"description": "Account timezone",
"example": "UTC"
}
}
},
"account_user_create_update_payload": {
"type": "object",
"required": [
+199
View File
@@ -7135,6 +7135,145 @@
}
}
},
"account_detail": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "Account ID"
},
"name": {
"type": "string",
"description": "Name of the account"
},
"locale": {
"type": "string",
"description": "The locale of the account"
},
"domain": {
"type": "string",
"description": "The domain of the account"
},
"support_email": {
"type": "string",
"description": "The support email of the account"
},
"status": {
"type": "string",
"description": "The status of the account"
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "The creation date of the account"
},
"cache_keys": {
"type": "object",
"description": "Cache keys for the account"
},
"features": {
"type": "array",
"items": {
"type": "string"
},
"description": "Enabled features for the account"
},
"settings": {
"type": "object",
"description": "Account settings",
"properties": {
"auto_resolve_after": {
"type": "number",
"description": "Auto resolve conversations after specified minutes"
},
"auto_resolve_message": {
"type": "string",
"description": "Message to send when auto resolving"
},
"auto_resolve_ignore_waiting": {
"type": "boolean",
"description": "Whether to ignore waiting conversations for auto resolve"
}
}
},
"custom_attributes": {
"type": "object",
"description": "Custom attributes of the account",
"properties": {
"plan_name": {
"type": "string",
"description": "Subscription plan name"
},
"subscribed_quantity": {
"type": "number",
"description": "Subscribed quantity"
},
"subscription_status": {
"type": "string",
"description": "Subscription status"
},
"subscription_ends_on": {
"type": "string",
"format": "date",
"description": "Subscription end date"
},
"industry": {
"type": "string",
"description": "Industry type"
},
"company_size": {
"type": "string",
"description": "Company size"
},
"timezone": {
"type": "string",
"description": "Account timezone"
},
"logo": {
"type": "string",
"description": "Account logo URL"
},
"onboarding_step": {
"type": "string",
"description": "Current onboarding step"
},
"marked_for_deletion_at": {
"type": "string",
"format": "date-time",
"description": "When account was marked for deletion"
},
"marked_for_deletion_reason": {
"type": "string",
"description": "Reason for account deletion"
}
}
}
}
},
"account_show_response": {
"allOf": [
{
"$ref": "#/components/schemas/account_detail"
},
{
"type": "object",
"properties": {
"latest_chatwoot_version": {
"type": "string",
"description": "Latest version of Chatwoot available",
"example": "3.0.0"
},
"subscribed_features": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of subscribed enterprise features (if enterprise edition is enabled)"
}
}
}
]
},
"account_user": {
"type": "array",
"description": "Array of account users",
@@ -7474,6 +7613,66 @@
}
}
},
"account_update_payload": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the account",
"example": "My Account"
},
"locale": {
"type": "string",
"description": "The locale of the account",
"example": "en"
},
"domain": {
"type": "string",
"description": "The domain of the account",
"example": "example.com"
},
"support_email": {
"type": "string",
"description": "The support email of the account",
"example": "support@example.com"
},
"auto_resolve_after": {
"type": "integer",
"minimum": 10,
"maximum": 1439856,
"nullable": true,
"description": "Auto resolve conversations after specified minutes",
"example": 1440
},
"auto_resolve_message": {
"type": "string",
"nullable": true,
"description": "Message to send when auto resolving",
"example": "This conversation has been automatically resolved due to inactivity"
},
"auto_resolve_ignore_waiting": {
"type": "boolean",
"nullable": true,
"description": "Whether to ignore waiting conversations for auto resolve",
"example": false
},
"industry": {
"type": "string",
"description": "Industry type",
"example": "Technology"
},
"company_size": {
"type": "string",
"description": "Company size",
"example": "50-100"
},
"timezone": {
"type": "string",
"description": "Account timezone",
"example": "UTC"
}
}
},
"account_user_create_update_payload": {
"type": "object",
"required": [
+199
View File
@@ -1978,6 +1978,145 @@
}
}
},
"account_detail": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "Account ID"
},
"name": {
"type": "string",
"description": "Name of the account"
},
"locale": {
"type": "string",
"description": "The locale of the account"
},
"domain": {
"type": "string",
"description": "The domain of the account"
},
"support_email": {
"type": "string",
"description": "The support email of the account"
},
"status": {
"type": "string",
"description": "The status of the account"
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "The creation date of the account"
},
"cache_keys": {
"type": "object",
"description": "Cache keys for the account"
},
"features": {
"type": "array",
"items": {
"type": "string"
},
"description": "Enabled features for the account"
},
"settings": {
"type": "object",
"description": "Account settings",
"properties": {
"auto_resolve_after": {
"type": "number",
"description": "Auto resolve conversations after specified minutes"
},
"auto_resolve_message": {
"type": "string",
"description": "Message to send when auto resolving"
},
"auto_resolve_ignore_waiting": {
"type": "boolean",
"description": "Whether to ignore waiting conversations for auto resolve"
}
}
},
"custom_attributes": {
"type": "object",
"description": "Custom attributes of the account",
"properties": {
"plan_name": {
"type": "string",
"description": "Subscription plan name"
},
"subscribed_quantity": {
"type": "number",
"description": "Subscribed quantity"
},
"subscription_status": {
"type": "string",
"description": "Subscription status"
},
"subscription_ends_on": {
"type": "string",
"format": "date",
"description": "Subscription end date"
},
"industry": {
"type": "string",
"description": "Industry type"
},
"company_size": {
"type": "string",
"description": "Company size"
},
"timezone": {
"type": "string",
"description": "Account timezone"
},
"logo": {
"type": "string",
"description": "Account logo URL"
},
"onboarding_step": {
"type": "string",
"description": "Current onboarding step"
},
"marked_for_deletion_at": {
"type": "string",
"format": "date-time",
"description": "When account was marked for deletion"
},
"marked_for_deletion_reason": {
"type": "string",
"description": "Reason for account deletion"
}
}
}
}
},
"account_show_response": {
"allOf": [
{
"$ref": "#/components/schemas/account_detail"
},
{
"type": "object",
"properties": {
"latest_chatwoot_version": {
"type": "string",
"description": "Latest version of Chatwoot available",
"example": "3.0.0"
},
"subscribed_features": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of subscribed enterprise features (if enterprise edition is enabled)"
}
}
}
]
},
"account_user": {
"type": "array",
"description": "Array of account users",
@@ -2317,6 +2456,66 @@
}
}
},
"account_update_payload": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the account",
"example": "My Account"
},
"locale": {
"type": "string",
"description": "The locale of the account",
"example": "en"
},
"domain": {
"type": "string",
"description": "The domain of the account",
"example": "example.com"
},
"support_email": {
"type": "string",
"description": "The support email of the account",
"example": "support@example.com"
},
"auto_resolve_after": {
"type": "integer",
"minimum": 10,
"maximum": 1439856,
"nullable": true,
"description": "Auto resolve conversations after specified minutes",
"example": 1440
},
"auto_resolve_message": {
"type": "string",
"nullable": true,
"description": "Message to send when auto resolving",
"example": "This conversation has been automatically resolved due to inactivity"
},
"auto_resolve_ignore_waiting": {
"type": "boolean",
"nullable": true,
"description": "Whether to ignore waiting conversations for auto resolve",
"example": false
},
"industry": {
"type": "string",
"description": "Industry type",
"example": "Technology"
},
"company_size": {
"type": "string",
"description": "Company size",
"example": "50-100"
},
"timezone": {
"type": "string",
"description": "Account timezone",
"example": "UTC"
}
}
},
"account_user_create_update_payload": {
"type": "object",
"required": [
+199
View File
@@ -1393,6 +1393,145 @@
}
}
},
"account_detail": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "Account ID"
},
"name": {
"type": "string",
"description": "Name of the account"
},
"locale": {
"type": "string",
"description": "The locale of the account"
},
"domain": {
"type": "string",
"description": "The domain of the account"
},
"support_email": {
"type": "string",
"description": "The support email of the account"
},
"status": {
"type": "string",
"description": "The status of the account"
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "The creation date of the account"
},
"cache_keys": {
"type": "object",
"description": "Cache keys for the account"
},
"features": {
"type": "array",
"items": {
"type": "string"
},
"description": "Enabled features for the account"
},
"settings": {
"type": "object",
"description": "Account settings",
"properties": {
"auto_resolve_after": {
"type": "number",
"description": "Auto resolve conversations after specified minutes"
},
"auto_resolve_message": {
"type": "string",
"description": "Message to send when auto resolving"
},
"auto_resolve_ignore_waiting": {
"type": "boolean",
"description": "Whether to ignore waiting conversations for auto resolve"
}
}
},
"custom_attributes": {
"type": "object",
"description": "Custom attributes of the account",
"properties": {
"plan_name": {
"type": "string",
"description": "Subscription plan name"
},
"subscribed_quantity": {
"type": "number",
"description": "Subscribed quantity"
},
"subscription_status": {
"type": "string",
"description": "Subscription status"
},
"subscription_ends_on": {
"type": "string",
"format": "date",
"description": "Subscription end date"
},
"industry": {
"type": "string",
"description": "Industry type"
},
"company_size": {
"type": "string",
"description": "Company size"
},
"timezone": {
"type": "string",
"description": "Account timezone"
},
"logo": {
"type": "string",
"description": "Account logo URL"
},
"onboarding_step": {
"type": "string",
"description": "Current onboarding step"
},
"marked_for_deletion_at": {
"type": "string",
"format": "date-time",
"description": "When account was marked for deletion"
},
"marked_for_deletion_reason": {
"type": "string",
"description": "Reason for account deletion"
}
}
}
}
},
"account_show_response": {
"allOf": [
{
"$ref": "#/components/schemas/account_detail"
},
{
"type": "object",
"properties": {
"latest_chatwoot_version": {
"type": "string",
"description": "Latest version of Chatwoot available",
"example": "3.0.0"
},
"subscribed_features": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of subscribed enterprise features (if enterprise edition is enabled)"
}
}
}
]
},
"account_user": {
"type": "array",
"description": "Array of account users",
@@ -1732,6 +1871,66 @@
}
}
},
"account_update_payload": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the account",
"example": "My Account"
},
"locale": {
"type": "string",
"description": "The locale of the account",
"example": "en"
},
"domain": {
"type": "string",
"description": "The domain of the account",
"example": "example.com"
},
"support_email": {
"type": "string",
"description": "The support email of the account",
"example": "support@example.com"
},
"auto_resolve_after": {
"type": "integer",
"minimum": 10,
"maximum": 1439856,
"nullable": true,
"description": "Auto resolve conversations after specified minutes",
"example": 1440
},
"auto_resolve_message": {
"type": "string",
"nullable": true,
"description": "Message to send when auto resolving",
"example": "This conversation has been automatically resolved due to inactivity"
},
"auto_resolve_ignore_waiting": {
"type": "boolean",
"nullable": true,
"description": "Whether to ignore waiting conversations for auto resolve",
"example": false
},
"industry": {
"type": "string",
"description": "Industry type",
"example": "Technology"
},
"company_size": {
"type": "string",
"description": "Company size",
"example": "50-100"
},
"timezone": {
"type": "string",
"description": "Account timezone",
"example": "UTC"
}
}
},
"account_user_create_update_payload": {
"type": "object",
"required": [
+199
View File
@@ -2154,6 +2154,145 @@
}
}
},
"account_detail": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "Account ID"
},
"name": {
"type": "string",
"description": "Name of the account"
},
"locale": {
"type": "string",
"description": "The locale of the account"
},
"domain": {
"type": "string",
"description": "The domain of the account"
},
"support_email": {
"type": "string",
"description": "The support email of the account"
},
"status": {
"type": "string",
"description": "The status of the account"
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "The creation date of the account"
},
"cache_keys": {
"type": "object",
"description": "Cache keys for the account"
},
"features": {
"type": "array",
"items": {
"type": "string"
},
"description": "Enabled features for the account"
},
"settings": {
"type": "object",
"description": "Account settings",
"properties": {
"auto_resolve_after": {
"type": "number",
"description": "Auto resolve conversations after specified minutes"
},
"auto_resolve_message": {
"type": "string",
"description": "Message to send when auto resolving"
},
"auto_resolve_ignore_waiting": {
"type": "boolean",
"description": "Whether to ignore waiting conversations for auto resolve"
}
}
},
"custom_attributes": {
"type": "object",
"description": "Custom attributes of the account",
"properties": {
"plan_name": {
"type": "string",
"description": "Subscription plan name"
},
"subscribed_quantity": {
"type": "number",
"description": "Subscribed quantity"
},
"subscription_status": {
"type": "string",
"description": "Subscription status"
},
"subscription_ends_on": {
"type": "string",
"format": "date",
"description": "Subscription end date"
},
"industry": {
"type": "string",
"description": "Industry type"
},
"company_size": {
"type": "string",
"description": "Company size"
},
"timezone": {
"type": "string",
"description": "Account timezone"
},
"logo": {
"type": "string",
"description": "Account logo URL"
},
"onboarding_step": {
"type": "string",
"description": "Current onboarding step"
},
"marked_for_deletion_at": {
"type": "string",
"format": "date-time",
"description": "When account was marked for deletion"
},
"marked_for_deletion_reason": {
"type": "string",
"description": "Reason for account deletion"
}
}
}
}
},
"account_show_response": {
"allOf": [
{
"$ref": "#/components/schemas/account_detail"
},
{
"type": "object",
"properties": {
"latest_chatwoot_version": {
"type": "string",
"description": "Latest version of Chatwoot available",
"example": "3.0.0"
},
"subscribed_features": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of subscribed enterprise features (if enterprise edition is enabled)"
}
}
}
]
},
"account_user": {
"type": "array",
"description": "Array of account users",
@@ -2493,6 +2632,66 @@
}
}
},
"account_update_payload": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the account",
"example": "My Account"
},
"locale": {
"type": "string",
"description": "The locale of the account",
"example": "en"
},
"domain": {
"type": "string",
"description": "The domain of the account",
"example": "example.com"
},
"support_email": {
"type": "string",
"description": "The support email of the account",
"example": "support@example.com"
},
"auto_resolve_after": {
"type": "integer",
"minimum": 10,
"maximum": 1439856,
"nullable": true,
"description": "Auto resolve conversations after specified minutes",
"example": 1440
},
"auto_resolve_message": {
"type": "string",
"nullable": true,
"description": "Message to send when auto resolving",
"example": "This conversation has been automatically resolved due to inactivity"
},
"auto_resolve_ignore_waiting": {
"type": "boolean",
"nullable": true,
"description": "Whether to ignore waiting conversations for auto resolve",
"example": false
},
"industry": {
"type": "string",
"description": "Industry type",
"example": "Technology"
},
"company_size": {
"type": "string",
"description": "Company size",
"example": "50-100"
},
"timezone": {
"type": "string",
"description": "Account timezone",
"example": "UTC"
}
}
},
"account_user_create_update_payload": {
"type": "object",
"required": [