{{ t(`CAMPAIGN.LIVE_CHAT.CREATE.TITLE`) }}
diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/SMSCampaign/SMSCampaignDialog.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/SMSCampaign/SMSCampaignDialog.vue
index b190b031b..d959499ee 100644
--- a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/SMSCampaign/SMSCampaignDialog.vue
+++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/SMSCampaign/SMSCampaignDialog.vue
@@ -39,7 +39,7 @@ const handleClose = () => emit('close');
{{ t(`CAMPAIGN.SMS.CREATE.TITLE`) }}
diff --git a/app/javascript/dashboard/components-next/CardLayout.vue b/app/javascript/dashboard/components-next/CardLayout.vue
index 0fd3f5986..462402167 100644
--- a/app/javascript/dashboard/components-next/CardLayout.vue
+++ b/app/javascript/dashboard/components-next/CardLayout.vue
@@ -4,6 +4,10 @@ defineProps({
type: String,
default: 'col',
},
+ selectable: {
+ type: Boolean,
+ default: false,
+ },
});
const emit = defineEmits(['click']);
@@ -18,10 +22,11 @@ const handleClick = () => {
class="flex flex-col w-full shadow outline-1 outline outline-n-container group/cardLayout rounded-2xl bg-n-solid-2"
>
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue b/app/javascript/dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue
index 255c1b113..af04de9a7 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue
@@ -18,6 +18,11 @@ const route = useRoute();
const showDropdown = ref(false);
+// Store the currently hovered label's ID
+// Using JS state management instead of CSS :hover / group hover
+// This will solve the flickering issue when hovering over the last label item
+const hoveredLabel = ref(null);
+
const allLabels = useMapGetter('labels/getLabels');
const contactLabels = useMapGetter('contactLabels/getContactLabels');
@@ -37,7 +42,7 @@ const labelMenuItems = computed(() => {
isSelected: savedLabels.value.some(
savedLabel => savedLabel.id === label.id
),
- action: 'addLabel',
+ action: 'contactLabel',
}))
.toSorted((a, b) => Number(a.isSelected) - Number(b.isSelected));
});
@@ -49,7 +54,7 @@ const fetchLabels = async contactId => {
store.dispatch('contactLabels/get', contactId);
};
-const handleLabelAction = async ({ action, value }) => {
+const handleLabelAction = async ({ value }) => {
try {
// Get current label titles
const currentLabels = savedLabels.value.map(label => label.title);
@@ -59,16 +64,15 @@ const handleLabelAction = async ({ action, value }) => {
if (!selectedLabel) return;
let updatedLabels;
- if (action === 'addLabel') {
- // If label is already selected, remove it (toggle behavior)
- if (currentLabels.includes(selectedLabel.title)) {
- updatedLabels = currentLabels.filter(
- labelTitle => labelTitle !== selectedLabel.title
- );
- } else {
- // Add the new label
- updatedLabels = [...currentLabels, selectedLabel.title];
- }
+
+ // If label is already selected, remove it (toggle behavior)
+ if (currentLabels.includes(selectedLabel.title)) {
+ updatedLabels = currentLabels.filter(
+ labelTitle => labelTitle !== selectedLabel.title
+ );
+ } else {
+ // Add the new label
+ updatedLabels = [...currentLabels, selectedLabel.title];
}
await store.dispatch('contactLabels/update', {
@@ -82,6 +86,10 @@ const handleLabelAction = async ({ action, value }) => {
}
};
+const handleRemoveLabel = labelId => {
+ return handleLabelAction({ value: labelId });
+};
+
watch(
() => props.contactId,
(newVal, oldVal) => {
@@ -95,11 +103,31 @@ onMounted(() => {
fetchLabels(route.params.contactId);
}
});
+
+const handleMouseLeave = () => {
+ // Reset hover state when mouse leaves the container
+ // This ensures all labels return to their default state
+ hoveredLabel.value = null;
+};
+
+const handleLabelHover = labelId => {
+ // Added this to prevent flickering on when showing remove button on hover
+ // If the label item is at end of the line, it will show the remove button
+ // when hovering over the last label item
+ hoveredLabel.value = labelId;
+};
-
-
+
+
({}),
},
+ isUpdating: {
+ type: Boolean,
+ default: false,
+ },
});
-const emit = defineEmits(['goToContactsList']);
+const emit = defineEmits(['goToContactsList', 'toggleBlock']);
const { t } = useI18n();
const slots = useSlots();
@@ -45,9 +45,17 @@ const breadcrumbItems = computed(() => {
return items;
});
+const isContactBlocked = computed(() => {
+ return props.selectedContact?.blocked;
+});
+
const handleBreadcrumbClick = () => {
emit('goToContactsList');
};
+
+const toggleBlock = () => {
+ emit('toggleBlock', isContactBlocked.value);
+};
@@ -57,23 +65,41 @@ const handleBreadcrumbClick = () => {
-
-
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+
@@ -81,7 +107,7 @@ const handleBreadcrumbClick = () => {
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactMergeForm.vue b/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactMergeForm.vue
index 8adcce6b2..2cee66d12 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactMergeForm.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactMergeForm.vue
@@ -50,6 +50,7 @@ const { t } = useI18n();
@@ -266,7 +265,9 @@ defineExpose({
:placeholder="item.placeholder"
:message-type="getMessageType(item.key)"
:custom-input-class="`h-8 !pt-1 !pb-1 ${
- !isDetailsView ? '[&:not(.error,.focus)]:!border-transparent' : ''
+ !isDetailsView
+ ? '[&:not(.error,.focus)]:!outline-transparent'
+ : ''
}`"
class="w-full"
@input="
@@ -303,7 +304,7 @@ defineExpose({
v-model="
state.additionalAttributes.socialProfiles[item.key.toLowerCase()]
"
- class="w-auto min-w-[100px] text-sm bg-transparent reset-base text-n-slate-12 dark:text-n-slate-12 placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10"
+ class="w-auto min-w-[100px] text-sm bg-transparent outline-none reset-base text-n-slate-12 dark:text-n-slate-12 placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10"
:placeholder="item.placeholder"
:size="item.placeholder.length"
@input="emit('update', state)"
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsForm/CreateNewContactDialog.vue b/app/javascript/dashboard/components-next/Contacts/ContactsForm/CreateNewContactDialog.vue
index 806fcf4ac..75400692e 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsForm/CreateNewContactDialog.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/CreateNewContactDialog.vue
@@ -51,16 +51,18 @@ defineExpose({ dialogRef, contactsFormRef, onSuccess });
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactHeader.vue b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactHeader.vue
index 44350bba1..42f526f95 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactHeader.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactHeader.vue
@@ -60,7 +60,7 @@ const emit = defineEmits([
{{ headerTitle }}
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue
index 8b2a53b27..627addff8 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue
@@ -106,10 +106,15 @@ const onImport = async file => {
try {
await store.dispatch('contacts/import', file);
contactImportDialogRef.value?.dialogRef.close();
- useAlert(t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
+ useAlert(
+ t('CONTACTS_LAYOUT.HEADER.ACTIONS.IMPORT_CONTACT.SUCCESS_MESSAGE')
+ );
useTrack(CONTACTS_EVENTS.IMPORT_SUCCESS);
} catch (error) {
- useAlert(error.message ?? t('IMPORT_CONTACTS.ERROR_MESSAGE'));
+ useAlert(
+ error.message ??
+ t('CONTACTS_LAYOUT.HEADER.ACTIONS.IMPORT_CONTACT.ERROR_MESSAGE')
+ );
useTrack(CONTACTS_EVENTS.IMPORT_FAILURE);
}
};
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactSortMenu.vue b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactSortMenu.vue
index b263b882d..7125d8385 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactSortMenu.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactSortMenu.vue
@@ -31,10 +31,6 @@ const sortMenus = [
label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.EMAIL'),
value: 'email',
},
- {
- label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.PHONE_NUMBER'),
- value: 'phone_number',
- },
{
label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.COMPANY'),
value: 'company_name',
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactsActiveFiltersPreview.vue b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactsActiveFiltersPreview.vue
index 36162520d..88ee94604 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactsActiveFiltersPreview.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactsActiveFiltersPreview.vue
@@ -62,7 +62,7 @@ const activeFilterQueryData = computed(() => {
t('CONTACTS_LAYOUT.FILTER.ACTIVE_FILTERS.CLEAR_FILTERS')
"
:show-clear-button="!hasActiveSegments"
- class="max-w-[960px] px-6"
+ class="max-w-[60rem] px-6"
@open-filter="emit('openFilter')"
@clear-filters="emit('clearFilters')"
/>
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsListLayout.vue b/app/javascript/dashboard/components-next/Contacts/ContactsListLayout.vue
index 22b32d7b8..25aac5df8 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsListLayout.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsListLayout.vue
@@ -72,7 +72,7 @@ const openFilter = () => {
@clear-filters="emit('clearFilters')"
/>
-
+
{
const isCurrentUser = note?.user?.id === currentUser.value.id;
return isCurrentUser
? t('CONTACTS_LAYOUT.SIDEBAR.NOTES.YOU')
- : note.user.name;
+ : note?.user?.name || 'Bot';
};
const onAdd = content => {
@@ -87,8 +87,10 @@ useKeyboardEvents(keyboardEvents);
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue
index 504be1547..c11fa2d9d 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue
@@ -1,6 +1,8 @@
-
-
-
+
+
+
- {{ writtenBy }}
+ {{ writtenBy }}
{{ t('CONTACTS_LAYOUT.SIDEBAR.NOTES.WROTE') }}
- {{ dynamicTime(note.createdAt) }}
+
+ {{ dynamicTime(note.createdAt) }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue
index a81c80b87..a2b3ad7fb 100644
--- a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue
+++ b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue
@@ -21,7 +21,7 @@ const lastNonActivityMessageContent = computed(() => {
props.conversation;
const { email: { subject } = {} } = customAttributes;
return getPlainText(
- subject || lastNonActivityMessage.content || t('CHAT_LIST.NO_CONTENT')
+ subject || lastNonActivityMessage?.content || t('CHAT_LIST.NO_CONTENT')
);
});
diff --git a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue
index 19dc7008a..f1ad9d09b 100644
--- a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue
+++ b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue
@@ -1,5 +1,5 @@
@@ -73,7 +83,11 @@ const hasSlaThreshold = computed(() => props.conversation?.slaPolicyId);
: 'grid-cols-[1fr_20px]'
"
>
-
+
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
import { getInboxIconByType } from 'dashboard/helper/inbox';
import { useRouter, useRoute } from 'vue-router';
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper.js';
@@ -33,6 +33,8 @@ const props = defineProps({
const router = useRouter();
const route = useRoute();
+const cardMessagePreviewWithMetaRef = ref(null);
+
const currentContact = computed(() => props.contact);
const currentContactName = computed(() => currentContact.value?.name);
@@ -56,8 +58,10 @@ const lastActivityAt = computed(() => {
});
const showMessagePreviewWithoutMeta = computed(() => {
- const { slaPolicyId, labels = [] } = props.conversation;
- return !slaPolicyId && labels.length === 0;
+ const { labels = [] } = props.conversation;
+ return (
+ !cardMessagePreviewWithMetaRef.value?.hasSlaThreshold && labels.length === 0
+ );
});
const onCardClick = e => {
@@ -82,6 +86,7 @@ const onCardClick = e => {
@@ -92,7 +97,7 @@ const onCardClick = e => {
:status="currentContactStatus"
rounded-full
/>
-
+
{{ currentContactName }}
@@ -114,11 +119,12 @@ const onCardClick = e => {
diff --git a/app/javascript/dashboard/components-next/Conversation/ConversationCard/SLACardLabel.vue b/app/javascript/dashboard/components-next/Conversation/ConversationCard/SLACardLabel.vue
index a9db5d99b..ff57d6c93 100644
--- a/app/javascript/dashboard/components-next/Conversation/ConversationCard/SLACardLabel.vue
+++ b/app/javascript/dashboard/components-next/Conversation/ConversationCard/SLACardLabel.vue
@@ -31,13 +31,17 @@ const convertObjectCamelCaseToSnakeCase = object => {
const appliedSLA = computed(() => props.conversation?.appliedSla);
const isSlaMissed = computed(() => slaStatus.value?.isSlaMissed);
+const hasSlaThreshold = computed(() => {
+ return slaStatus.value?.threshold && appliedSLA.value?.id;
+});
+
const slaStatusText = computed(() => {
return slaStatus.value?.type?.toUpperCase();
});
const updateSlaStatus = () => {
slaStatus.value = evaluateSLAStatus({
- appliedSla: convertObjectCamelCaseToSnakeCase(appliedSLA.value),
+ appliedSla: convertObjectCamelCaseToSnakeCase(appliedSLA.value || {}),
chat: props.conversation,
});
};
@@ -61,6 +65,21 @@ onUnmounted(() => {
});
watch(() => props.conversation, updateSlaStatus);
+
+// This expose is to provide context to the parent component, so that it can decided weather
+// a new row has to be added to the conversation card or not
+// SLACardLabel > CardMessagePreviewWithMeta > ConversationCard
+//
+// We need to do this becuase each SLA card has it's own SLA timer
+// and it's just convenient to have this logic in the SLACardLabel component
+// However this is a bit hacky, and we should change this in the future
+//
+// TODO: A better implementation would be to have the timer as a shared composable, just like the provider pattern
+// we use across the next components. Have the calculation be done on the top ConversationCard component
+// and then the value be injected to the SLACardLabel component
+defineExpose({
+ hasSlaThreshold,
+});
diff --git a/app/javascript/dashboard/components-next/Editor/Editor.vue b/app/javascript/dashboard/components-next/Editor/Editor.vue
index 671a628dc..6b5fefab9 100644
--- a/app/javascript/dashboard/components-next/Editor/Editor.vue
+++ b/app/javascript/dashboard/components-next/Editor/Editor.vue
@@ -41,6 +41,8 @@ const props = defineProps({
default: 'info',
validator: value => ['info', 'error', 'success'].includes(value),
},
+ enableVariables: { type: Boolean, default: false },
+ enableCannedResponses: { type: Boolean, default: true },
});
const emit = defineEmits(['update:modelValue']);
@@ -116,6 +118,8 @@ watch(
:placeholder="placeholder"
:focus-on-mount="focusOnMount"
:disabled="disabled"
+ :enable-variables="enableVariables"
+ :enable-canned-responses="enableCannedResponses"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
diff --git a/app/javascript/dashboard/components-next/EmptyStateLayout.vue b/app/javascript/dashboard/components-next/EmptyStateLayout.vue
index 04c478ba6..09b8afee5 100644
--- a/app/javascript/dashboard/components-next/EmptyStateLayout.vue
+++ b/app/javascript/dashboard/components-next/EmptyStateLayout.vue
@@ -1,4 +1,6 @@
@@ -16,7 +22,7 @@ defineProps({
class="relative flex flex-col items-center justify-center w-full h-full overflow-hidden"
>
-
+
+
+
diff --git a/app/javascript/dashboard/components-next/HelpCenter/HelpCenterLayout.vue b/app/javascript/dashboard/components-next/HelpCenter/HelpCenterLayout.vue
index 9e3c9ee1e..c430a3d15 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/HelpCenterLayout.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/HelpCenterLayout.vue
@@ -60,7 +60,7 @@ const togglePortalSwitcher = () => {
-
+
{
-
+
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue
index f5b925fbe..1709d0f5f 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue
@@ -36,6 +36,8 @@ const emit = defineEmits([
const { t } = useI18n();
+const isNewArticle = computed(() => !props.article?.id);
+
const saveAndSync = value => {
emit('saveArticle', value);
};
@@ -52,21 +54,32 @@ const quickSave = debounce(
// 2.5 seconds is enough to know that the user has stopped typing and is taking a pause
// so we can save the data to the backend and retrieve the updated data
// this will update the local state with response data
+// Only use to save for existing articles
const saveAndSyncDebounced = debounce(saveAndSync, 2500, false);
+// Debounced save for new articles
+const quickSaveNewArticle = debounce(saveAndSync, 400, false);
+
+const handleSave = value => {
+ if (isNewArticle.value) {
+ quickSaveNewArticle(value);
+ } else {
+ quickSave(value);
+ saveAndSyncDebounced(value);
+ }
+};
+
const articleTitle = computed({
get: () => props.article.title,
set: value => {
- quickSave({ title: value });
- saveAndSyncDebounced({ title: value });
+ handleSave({ title: value });
},
});
const articleContent = computed({
get: () => props.article.content,
set: content => {
- quickSave({ content });
- saveAndSyncDebounced({ content });
+ handleSave({ content });
},
});
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorControls.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorControls.vue
index d46fb5264..a910f4d8d 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorControls.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorControls.vue
@@ -200,7 +200,8 @@ onMounted(() => {
@@ -231,7 +232,8 @@ onMounted(() => {
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorProperties.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorProperties.vue
index 4de31a6ba..aa04ba56a 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorProperties.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorProperties.vue
@@ -52,7 +52,7 @@ onMounted(() => {
@@ -75,7 +75,7 @@ onMounted(() => {
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleHeaderControls.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleHeaderControls.vue
index 5e7feb9fa..6565fab82 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleHeaderControls.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleHeaderControls.vue
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { OnClickOutside } from '@vueuse/components';
+import { useUISettings } from 'dashboard/composables/useUISettings';
import {
ARTICLE_TABS,
CATEGORY_ALL,
@@ -37,6 +38,7 @@ const emit = defineEmits([
const route = useRoute();
const { t } = useI18n();
+const { updateUISettings } = useUISettings();
const isCategoryMenuOpen = ref(false);
const isLocaleMenuOpen = ref(false);
@@ -111,13 +113,12 @@ const localeMenuItems = computed(() => {
}));
});
-const hasMoreThanOneLocaleMenuItems = computed(() => {
- return localeMenuItems.value?.length > 1;
-});
-
const handleLocaleAction = ({ value }) => {
emit('localeChange', value);
isLocaleMenuOpen.value = false;
+ updateUISettings({
+ last_active_locale_code: value,
+ });
};
const handleCategoryAction = ({ value }) => {
@@ -143,7 +144,7 @@ const handleTabChange = value => {
/>
-
+
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalSettings.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalSettings.vue
index ccf1ad997..a49493818 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalSettings.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalSettings.vue
@@ -74,7 +74,7 @@ const handleDeletePortal = () => {
t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.ERROR'),
+ required
+ ),
+ isValidSlug: helpers.withMessage(
+ () => t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.FORMAT_ERROR'),
+ isValidSlug
+ ),
+ },
};
const v$ = useVuelidate(rules, state);
@@ -40,9 +50,9 @@ const nameError = computed(() =>
v$.value.name.$error ? t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.ERROR') : ''
);
-const slugError = computed(() =>
- v$.value.slug.$error ? t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.ERROR') : ''
-);
+const slugError = computed(() => {
+ return v$.value.slug.$errors[0]?.$message || '';
+});
const isSubmitDisabled = computed(() => v$.value.$invalid);
@@ -131,6 +141,7 @@ defineExpose({ dialogRef });
:message="
nameError || t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.MESSAGE')
"
+ @blur="v$.name.$touch()"
/>
diff --git a/app/javascript/dashboard/components-next/HelpCenter/PortalSwitcher/PortalSwitcher.vue b/app/javascript/dashboard/components-next/HelpCenter/PortalSwitcher/PortalSwitcher.vue
index 1b4668b91..5a835f8cb 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/PortalSwitcher/PortalSwitcher.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/PortalSwitcher/PortalSwitcher.vue
@@ -92,7 +92,7 @@ const redirectToPortalHomePage = () => {
+import { computed, ref, onBeforeMount } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { getInboxIconByType } from 'dashboard/helper/inbox';
+import { dynamicTime, shortTimestamp } from 'shared/helpers/timeHelper';
+import {
+ snoozedReopenTimeToTimestamp,
+ shortenSnoozeTime,
+} from 'dashboard/helper/snoozeHelpers';
+import { NOTIFICATION_TYPES_MAPPING } from 'dashboard/routes/dashboard/inbox/helpers/InboxViewHelpers';
+
+import Icon from 'dashboard/components-next/icon/Icon.vue';
+import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
+import CardPriorityIcon from 'dashboard/components-next/Conversation/ConversationCard/CardPriorityIcon.vue';
+import SLACardLabel from 'dashboard/components-next/Conversation/ConversationCard/SLACardLabel.vue';
+import InboxContextMenu from 'dashboard/routes/dashboard/inbox/components/InboxContextMenu.vue';
+
+const props = defineProps({
+ inboxItem: { type: Object, default: () => ({}) },
+ stateInbox: { type: Object, default: () => ({}) },
+});
+
+const emit = defineEmits([
+ 'click',
+ 'contextMenuOpen',
+ 'contextMenuClose',
+ 'markNotificationAsRead',
+ 'markNotificationAsUnRead',
+ 'deleteNotification',
+]);
+
+const { t } = useI18n();
+
+const isContextMenuOpen = ref(false);
+const contextMenuPosition = ref({ x: null, y: null });
+const slaCardLabel = ref(null);
+
+const getMessageClasses = {
+ emphasis: 'text-sm font-medium text-n-slate-11',
+ emphasisUnread: 'text-sm font-medium text-n-slate-12',
+ normal: 'text-sm font-normal text-n-slate-11',
+ normalUnread: 'text-sm text-n-slate-12',
+};
+
+const primaryActor = computed(() => props.inboxItem?.primaryActor);
+const meta = computed(() => primaryActor.value?.meta);
+const assigneeMeta = computed(() => meta.value?.sender);
+const isUnread = computed(() => !props.inboxItem?.readAt);
+const inbox = computed(() => props.stateInbox);
+
+const inboxIcon = computed(() => {
+ const { phoneNumber, channelType } = inbox.value;
+ return getInboxIconByType(channelType, phoneNumber);
+});
+
+const hasSlaThreshold = computed(() => {
+ return slaCardLabel.value?.hasSlaThreshold && primaryActor.value?.slaPolicyId;
+});
+
+const lastActivityAt = computed(() => {
+ const timestamp = props.inboxItem?.lastActivityAt;
+ return timestamp ? shortTimestamp(dynamicTime(timestamp)) : '';
+});
+
+const menuItems = computed(() => [
+ { key: 'delete', label: t('INBOX.MENU_ITEM.DELETE') },
+ {
+ key: isUnread.value ? 'mark_as_read' : 'mark_as_unread',
+ label: t(`INBOX.MENU_ITEM.MARK_AS_${isUnread.value ? 'READ' : 'UNREAD'}`),
+ },
+]);
+
+const messageClasses = computed(() => ({
+ emphasis: isUnread.value
+ ? getMessageClasses.emphasisUnread
+ : getMessageClasses.emphasis,
+ normal: isUnread.value
+ ? getMessageClasses.normalUnread
+ : getMessageClasses.normal,
+}));
+
+const formatPushMessage = message => {
+ if (message.startsWith(': ')) {
+ return message.slice(2);
+ }
+
+ return message.replace(/^([^:]+):/g, (match, name) => {
+ return ``;
+ });
+};
+
+const formattedMessage = computed(() => {
+ const messageContent = ``;
+
+ return isUnread.value
+ ? ` ${messageContent}`
+ : messageContent;
+});
+
+const notificationDetails = computed(() => {
+ const type = props.inboxItem?.notificationType?.toUpperCase() || '';
+ const [icon = '', color = 'text-n-blue-text'] =
+ NOTIFICATION_TYPES_MAPPING[type] || [];
+ return { text: type ? t(`INBOX.TYPES_NEXT.${type}`) : '', icon, color };
+});
+
+const snoozedUntilTime = computed(() => {
+ const { snoozedUntil } = props.inboxItem;
+ if (!snoozedUntil) return null;
+ return shortenSnoozeTime(
+ dynamicTime(snoozedReopenTimeToTimestamp(snoozedUntil))
+ );
+});
+
+const hasLastSnoozed = computed(() => props.inboxItem?.meta?.lastSnoozedAt);
+
+const snoozedText = computed(() => {
+ return !hasLastSnoozed.value
+ ? t('INBOX.TYPES_NEXT.SNOOZED_UNTIL', {
+ time: shortTimestamp(snoozedUntilTime.value),
+ })
+ : t('INBOX.TYPES_NEXT.SNOOZED_ENDS');
+});
+
+const contextMenuActions = {
+ close: () => {
+ isContextMenuOpen.value = false;
+ contextMenuPosition.value = { x: null, y: null };
+ emit('contextMenuClose');
+ },
+ open: e => {
+ e.preventDefault();
+ contextMenuPosition.value = {
+ x: e.pageX || e.clientX,
+ y: e.pageY || e.clientY,
+ };
+ isContextMenuOpen.value = true;
+ emit('contextMenuOpen');
+ },
+ handle: key => {
+ const actions = {
+ mark_as_read: () => emit('markNotificationAsRead', props.inboxItem),
+ mark_as_unread: () => emit('markNotificationAsUnRead', props.inboxItem),
+ delete: () => emit('deleteNotification', props.inboxItem),
+ };
+ actions[key]?.();
+ },
+};
+
+onBeforeMount(contextMenuActions.close);
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ snoozedText }}
+
+
+
+
+
+ {{ notificationDetails.text }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ lastActivityAt }}
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/Label/AddLabel.vue b/app/javascript/dashboard/components-next/Label/AddLabel.vue
index c9571e771..568ed6879 100644
--- a/app/javascript/dashboard/components-next/Label/AddLabel.vue
+++ b/app/javascript/dashboard/components-next/Label/AddLabel.vue
@@ -21,7 +21,7 @@ const showDropdown = ref(false);
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue
index 89784037d..90cb67c4f 100644
--- a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue
@@ -1,6 +1,5 @@
@@ -99,7 +113,8 @@ const errorClass = computed(() => {
{{
@@ -109,6 +124,7 @@ const errorClass = computed(() => {
}}
{
:is-loading="isLoading"
:disabled="contactableInboxesList?.length > 0 && showInboxesDropdown"
allow-create
- type="email"
+ :type="inputType"
class="flex-1 min-h-7"
:class="errorClass"
focus-on-mount
- @input="emit('searchContacts', $event)"
+ @input="handleInput"
@on-click-outside="emit('updateDropdown', 'contacts', false)"
@add="emit('setSelectedContact', $event)"
@remove="emit('clearSelectedContact')"
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue b/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue
index 4e1b0f510..7794d5bdd 100644
--- a/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue
@@ -52,7 +52,7 @@ const targetInboxLabel = computed(() => {
{{ targetInboxLabel }}
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/MessageEditor.vue b/app/javascript/dashboard/components-next/NewConversation/components/MessageEditor.vue
index ca54f7eca..01b2e4a8d 100644
--- a/app/javascript/dashboard/components-next/NewConversation/components/MessageEditor.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/components/MessageEditor.vue
@@ -1,22 +1,22 @@
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue b/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue
index 8d32509b8..3980c6b30 100644
--- a/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue
@@ -84,7 +84,7 @@ const handleSendMessage = template => {
/>
@@ -96,7 +96,7 @@ const handleSendMessage = template => {
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.SEARCH_PLACEHOLDER'
)
"
- class="w-full h-8 py-2 pl-10 pr-2 text-sm border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
+ class="w-full h-8 py-2 pl-10 pr-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
/>
{
{{
diff --git a/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js b/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js
index 12a5e76b3..57e819245 100644
--- a/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js
+++ b/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js
@@ -87,6 +87,21 @@ export const processContactableInboxes = inboxes => {
}));
};
+export const mergeInboxDetails = (inboxesData, inboxesList = []) => {
+ if (!inboxesData || !inboxesData.length) {
+ return [];
+ }
+
+ return inboxesData.map(inboxData => {
+ const matchingInbox =
+ inboxesList.find(inbox => inbox.id === inboxData.id) || {};
+ return {
+ ...camelcaseKeys(matchingInbox, { deep: true }),
+ ...inboxData,
+ };
+ });
+};
+
export const prepareAttachmentPayload = (
attachedFiles,
directUploadsEnabled
@@ -193,10 +208,12 @@ export const searchContacts = async ({ keys, query }) => {
return filteredPayload || [];
};
-export const createNewContact = async email => {
+export const createNewContact = async input => {
const payload = {
- name: getCapitalizedNameFromEmail(email),
- email,
+ name: input.startsWith('+')
+ ? input.slice(1) // Remove the '+' prefix if it exists
+ : getCapitalizedNameFromEmail(input),
+ ...(input.startsWith('+') ? { phone_number: input } : { email: input }),
};
const {
diff --git a/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js b/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js
index 5a2d0092d..3de1fad0d 100644
--- a/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js
+++ b/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js
@@ -110,6 +110,153 @@ describe('composeConversationHelper', () => {
});
});
+ describe('mergeInboxDetails', () => {
+ it('returns empty array if inboxesData is empty or null', () => {
+ expect(helpers.mergeInboxDetails(null)).toEqual([]);
+ expect(helpers.mergeInboxDetails([])).toEqual([]);
+ expect(helpers.mergeInboxDetails(undefined)).toEqual([]);
+ });
+
+ it('merges inbox data with matching inboxes from the list', () => {
+ const inboxesData = [
+ { id: 1, sourceId: 'source1' },
+ { id: 2, sourceId: 'source2' },
+ ];
+
+ const inboxesList = [
+ {
+ id: 1,
+ name: 'Inbox 1',
+ channel_type: 'Channel::Email',
+ channel_id: 10,
+ phone_number: null,
+ },
+ {
+ id: 2,
+ name: 'Inbox 2',
+ channel_type: 'Channel::Whatsapp',
+ channel_id: 20,
+ phone_number: '+1234567890',
+ },
+ {
+ id: 3,
+ name: 'Inbox 3',
+ channel_type: 'Channel::Api',
+ channel_id: 30,
+ phone_number: null,
+ },
+ ];
+
+ const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
+
+ expect(result.length).toBe(2);
+ expect(result[0]).toMatchObject({
+ id: 1,
+ sourceId: 'source1',
+ name: 'Inbox 1',
+ channelType: 'Channel::Email',
+ channelId: 10,
+ phoneNumber: null,
+ });
+
+ expect(result[1]).toMatchObject({
+ id: 2,
+ sourceId: 'source2',
+ name: 'Inbox 2',
+ channelType: 'Channel::Whatsapp',
+ channelId: 20,
+ phoneNumber: '+1234567890',
+ });
+ });
+
+ it('handles inboxes not found in the list', () => {
+ const inboxesData = [
+ { id: 1, sourceId: 'source1' },
+ { id: 99, sourceId: 'source99' }, // This doesn't exist in inboxesList
+ ];
+
+ const inboxesList = [
+ {
+ id: 1,
+ name: 'Inbox 1',
+ channel_type: 'Channel::Email',
+ },
+ ];
+
+ const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
+
+ expect(result.length).toBe(2);
+
+ expect(result[0]).toMatchObject({
+ id: 1,
+ sourceId: 'source1',
+ name: 'Inbox 1',
+ channelType: 'Channel::Email',
+ });
+
+ expect(result[1]).toMatchObject({
+ id: 99,
+ sourceId: 'source99',
+ });
+
+ expect(result[1].name).toBeUndefined();
+ expect(result[1].channelType).toBeUndefined();
+ });
+
+ it('camelcases properties from inboxesList', () => {
+ const inboxesData = [{ id: 1, sourceId: 'source1' }];
+
+ const inboxesList = [
+ {
+ id: 1,
+ name: 'Inbox 1',
+ channel_type: 'Channel::Email',
+ avatar_url: 'https://example.com/avatar.png',
+ working_hours: [
+ {
+ day_of_week: 1,
+ closed_all_day: false,
+ },
+ ],
+ },
+ ];
+
+ const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
+
+ expect(result[0]).toMatchObject({
+ id: 1,
+ sourceId: 'source1',
+ name: 'Inbox 1',
+ channelType: 'Channel::Email',
+ avatarUrl: 'https://example.com/avatar.png',
+ });
+
+ expect(result[0].workingHours[0]).toMatchObject({
+ dayOfWeek: 1,
+ closedAllDay: false,
+ });
+ });
+
+ it('preserves original properties when they conflict with inboxesList', () => {
+ const inboxesData = [
+ { id: 1, sourceId: 'source1', name: 'Original Name' },
+ ];
+
+ const inboxesList = [
+ {
+ id: 1,
+ name: 'List Name',
+ channel_type: 'Channel::Email',
+ },
+ ];
+
+ const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
+
+ expect(result[0].name).toBe('Original Name');
+ expect(result[0].channelType).toBe('Channel::Email');
+ });
+ });
+
describe('prepareAttachmentPayload', () => {
it('prepares direct upload files', () => {
const files = [{ blobSignedId: 'signed1' }];
@@ -429,6 +576,28 @@ describe('composeConversationHelper', () => {
email: 'john@example.com',
});
});
+
+ it('creates new contact with phone number', async () => {
+ const mockContact = {
+ id: 1,
+ name: '919999999999',
+ phone_number: '+919999999999',
+ };
+ ContactAPI.create.mockResolvedValue({
+ data: { payload: { contact: mockContact } },
+ });
+
+ const result = await helpers.createNewContact('+919999999999');
+ expect(result).toEqual({
+ id: 1,
+ name: '919999999999',
+ phoneNumber: '+919999999999',
+ });
+ expect(ContactAPI.create).toHaveBeenCalledWith({
+ name: '919999999999',
+ phone_number: '+919999999999',
+ });
+ });
});
describe('fetchContactableInboxes', () => {
diff --git a/app/javascript/dashboard/components-next/avatar/Avatar.vue b/app/javascript/dashboard/components-next/avatar/Avatar.vue
index fb23b3cff..65859b1c7 100644
--- a/app/javascript/dashboard/components-next/avatar/Avatar.vue
+++ b/app/javascript/dashboard/components-next/avatar/Avatar.vue
@@ -174,7 +174,7 @@ watch(
-
+
+
+
+
+
+
+
+
+
+
+
+ {{ actionLabel }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/button/Button.vue b/app/javascript/dashboard/components-next/button/Button.vue
index fecdbaf9b..c54bd395a 100644
--- a/app/javascript/dashboard/components-next/button/Button.vue
+++ b/app/javascript/dashboard/components-next/button/Button.vue
@@ -6,6 +6,7 @@ import Icon from 'dashboard/components-next/icon/Icon.vue';
import {
VARIANT_OPTIONS,
COLOR_OPTIONS,
+ JUSTIFY_OPTIONS,
SIZE_OPTIONS,
EXCLUDED_ATTRS,
} from './constants.js';
@@ -27,6 +28,11 @@ const props = defineProps({
default: null,
validator: value => SIZE_OPTIONS.includes(value) || value === null,
},
+ justify: {
+ type: String,
+ default: null,
+ validator: value => JUSTIFY_OPTIONS.includes(value) || value === null,
+ },
icon: { type: [String, Object, Function], default: '' },
trailingIcon: { type: Boolean, default: false },
isLoading: { type: Boolean, default: false },
@@ -81,48 +87,70 @@ const computedSize = computed(() => {
return 'md';
});
+const computedJustify = computed(() => {
+ if (props.justify) return props.justify;
+ if (attrs.start || attrs.start === '') return 'start';
+ if (attrs.center || attrs.center === '') return 'center';
+ if (attrs.end || attrs.end === '') return 'end';
+
+ return 'center';
+});
+
const STYLE_CONFIG = {
colors: {
blue: {
- solid: 'bg-n-brand text-white hover:brightness-110 outline-transparent',
+ solid:
+ 'bg-n-brand text-white hover:enabled:brightness-110 focus-visible:brightness-110 outline-transparent',
faded:
- 'bg-n-brand/10 text-n-blue-text hover:bg-n-brand/20 outline-transparent',
- outline: 'text-n-blue-text outline-n-blue-border',
- ghost: 'text-n-blue-text hover:bg-n-alpha-2 outline-transparent',
- link: 'text-n-blue-text hover:underline outline-transparent',
+ 'bg-n-brand/10 text-n-blue-text hover:enabled:bg-n-brand/20 focus-visible:bg-n-brand/20 outline-transparent',
+ outline: 'text-n-blue-text outline-n-brand',
+ ghost:
+ 'text-n-blue-text hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
+ link: 'text-n-blue-text hover:enabled:underline focus-visible:underline outline-transparent',
},
ruby: {
- solid: 'bg-n-ruby-9 text-white hover:bg-n-ruby-10 outline-transparent',
+ solid:
+ 'bg-n-ruby-9 text-white hover:enabled:bg-n-ruby-10 focus-visible:bg-n-ruby-10 outline-transparent',
faded:
- 'bg-n-ruby-9/10 text-n-ruby-11 hover:bg-n-ruby-9/20 outline-transparent',
- outline: 'text-n-ruby-11 hover:bg-n-ruby-9/10 outline-n-ruby-8',
- ghost: 'text-n-ruby-11 hover:bg-n-alpha-2 outline-transparent',
- link: 'text-n-ruby-9 hover:underline outline-transparent',
+ 'bg-n-ruby-9/10 text-n-ruby-11 hover:enabled:bg-n-ruby-9/20 focus-visible:bg-n-ruby-9/20 outline-transparent',
+ outline:
+ 'text-n-ruby-11 hover:enabled:bg-n-ruby-9/10 focus-visible:bg-n-ruby-9/10 outline-n-ruby-8',
+ ghost:
+ 'text-n-ruby-11 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
+ link: 'text-n-ruby-9 hover:enabled:underline focus-visible:underline outline-transparent',
},
amber: {
- solid: 'bg-n-amber-9 text-white hover:bg-n-amber-10 outline-transparent',
+ solid:
+ 'bg-n-amber-9 text-white hover:enabled:bg-n-amber-10 focus-visible:bg-n-amber-10 outline-transparent',
faded:
- 'bg-n-amber-9/10 text-n-slate-12 hover:bg-n-amber-9/20 outline-transparent',
- outline: 'text-n-amber-11 hover:bg-n-amber-9/10 outline-n-amber-9',
- link: 'text-n-amber-9 hover:underline outline-transparent',
- ghost: 'text-n-amber-9 hover:bg-n-alpha-2 outline-transparent',
+ 'bg-n-amber-9/10 text-n-slate-12 hover:enabled:bg-n-amber-9/20 focus-visible:bg-n-amber-9/20 outline-transparent',
+ outline:
+ 'text-n-amber-11 hover:enabled:bg-n-amber-9/10 focus-visible:bg-n-amber-9/10 outline-n-amber-9',
+ link: 'text-n-amber-9 hover:enabled:underline focus-visible:underline outline-transparent',
+ ghost:
+ 'text-n-amber-9 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
},
slate: {
solid:
- 'bg-n-solid-3 dark:hover:bg-n-solid-2 hover:bg-n-alpha-2 text-n-slate-12 outline-n-container',
+ 'bg-n-solid-3 dark:hover:enabled:bg-n-solid-2 dark:focus-visible:bg-n-solid-2 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 text-n-slate-12 outline-n-container',
faded:
- 'bg-n-slate-9/10 text-n-slate-12 hover:bg-n-slate-9/20 outline-transparent',
- outline: 'text-n-slate-11 outline-n-strong hover:bg-n-slate-9/10',
- link: 'text-n-slate-11 hover:text-n-slate-12 hover:underline outline-transparent',
- ghost: 'text-n-slate-12 hover:bg-n-alpha-2 outline-transparent',
+ 'bg-n-slate-9/10 text-n-slate-12 hover:enabled:bg-n-slate-9/20 focus-visible:bg-n-slate-9/20 outline-transparent',
+ outline:
+ 'text-n-slate-11 outline-n-strong hover:enabled:bg-n-slate-9/10 focus-visible:bg-n-slate-9/10',
+ link: 'text-n-slate-11 hover:enabled:text-n-slate-12 focus-visible:text-n-slate-12 hover:enabled:underline focus-visible:underline outline-transparent',
+ ghost:
+ 'text-n-slate-12 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
},
teal: {
- solid: 'bg-n-teal-9 text-white hover:bg-n-teal-10 outline-transparent',
+ solid:
+ 'bg-n-teal-9 text-white hover:enabled:bg-n-teal-10 focus-visible:bg-n-teal-10 outline-transparent',
faded:
- 'bg-n-teal-9/10 text-n-slate-12 hover:bg-n-teal-9/20 outline-transparent',
- outline: 'text-n-teal-11 hover:bg-n-teal-9/10 outline-n-teal-9',
- link: 'text-n-teal-9 hover:underline outline-transparent',
- ghost: 'text-n-teal-9 hover:bg-n-alpha-2 outline-transparent',
+ 'bg-n-teal-9/10 text-n-slate-12 hover:enabled:bg-n-teal-9/20 focus-visible:bg-n-teal-9/20 outline-transparent',
+ outline:
+ 'text-n-teal-11 hover:enabled:bg-n-teal-9/10 focus-visible:bg-n-teal-9/10 outline-n-teal-9',
+ link: 'text-n-teal-9 hover:enabled:underline focus-visible:underline outline-transparent',
+ ghost:
+ 'text-n-teal-9 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
},
},
sizes: {
@@ -151,13 +179,18 @@ const STYLE_CONFIG = {
md: 'text-sm font-medium',
lg: 'text-base',
},
- base: 'inline-flex items-center justify-center min-w-0 gap-2 transition-all duration-200 ease-in-out border-0 rounded-lg outline-1 outline disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50',
+ justify: {
+ start: 'justify-start',
+ center: 'justify-center',
+ end: 'justify-end',
+ },
+ base: 'inline-flex items-center min-w-0 gap-2 transition-all duration-200 ease-in-out border-0 rounded-lg outline-1 outline disabled:opacity-50',
};
const variantClasses = computed(() => {
const variantMap = {
ghost: `${STYLE_CONFIG.colors[computedColor.value].ghost}`,
- link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-4`,
+ link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-2`,
outline: STYLE_CONFIG.colors[computedColor.value].outline,
faded: STYLE_CONFIG.colors[computedColor.value].faded,
solid: STYLE_CONFIG.colors[computedColor.value].solid,
@@ -197,6 +230,7 @@ const linkButtonClasses = computed(() => {
[STYLE_CONFIG.base]: true,
[isLink ? linkButtonClasses : buttonClasses]: true,
[STYLE_CONFIG.fontSize[computedSize]]: true,
+ [STYLE_CONFIG.justify[computedJustify]]: true,
'flex-row-reverse': trailingIcon && !isIconOnly,
}"
>
diff --git a/app/javascript/dashboard/components-next/button/constants.js b/app/javascript/dashboard/components-next/button/constants.js
index e65299420..d463abdd3 100644
--- a/app/javascript/dashboard/components-next/button/constants.js
+++ b/app/javascript/dashboard/components-next/button/constants.js
@@ -1,6 +1,7 @@
export const VARIANT_OPTIONS = ['solid', 'outline', 'faded', 'link', 'ghost'];
export const COLOR_OPTIONS = ['blue', 'ruby', 'amber', 'slate', 'teal'];
export const SIZE_OPTIONS = ['xs', 'sm', 'md', 'lg'];
+export const JUSTIFY_OPTIONS = ['start', 'center', 'end'];
export const EXCLUDED_ATTRS = [
'variant',
@@ -12,4 +13,5 @@ export const EXCLUDED_ATTRS = [
...VARIANT_OPTIONS,
...COLOR_OPTIONS,
...SIZE_OPTIONS,
+ ...JUSTIFY_OPTIONS,
];
diff --git a/app/javascript/dashboard/components-next/captain/PageLayout.vue b/app/javascript/dashboard/components-next/captain/PageLayout.vue
new file mode 100644
index 000000000..7355ac616
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/PageLayout.vue
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ headerTitle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.story.vue
new file mode 100644
index 000000000..b388d067e
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.story.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.vue b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.vue
new file mode 100644
index 000000000..222baaecd
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+ {{ name }}
+
+
+
+
+
+
+
+
+
+
+ {{ description || 'Description not available' }}
+
+
+ {{ lastUpdatedAt }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/AssistantPlayground.vue b/app/javascript/dashboard/components-next/captain/assistant/AssistantPlayground.vue
new file mode 100644
index 000000000..28587745a
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/AssistantPlayground.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+ {{ t('CAPTAIN.PLAYGROUND.HEADER') }}
+
+
+
+
+ {{ t('CAPTAIN.PLAYGROUND.DESCRIPTION') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('CAPTAIN.PLAYGROUND.CREDIT_NOTE') }}
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.story.vue
new file mode 100644
index 000000000..df1ada6cc
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.story.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue
new file mode 100644
index 000000000..d6e546581
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+ {{ name }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ assistant?.name || '' }}
+
+
+
+ {{ externalLink }}
+
+
+ {{ createdAt }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/InboxCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.story.vue
new file mode 100644
index 000000000..2fd0e9756
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.story.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue
new file mode 100644
index 000000000..30fe85b0a
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+ {{ inboxName }}
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/MessageList.vue b/app/javascript/dashboard/components-next/captain/assistant/MessageList.vue
new file mode 100644
index 000000000..1d6529a45
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/MessageList.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.story.vue
new file mode 100644
index 000000000..1a281b799
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.story.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue
new file mode 100644
index 000000000..f00354105
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+
+
+
+ {{ question }}
+
+
+
+
+
+
+
+
+
+ {{ answer }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/AssistantSelector.vue b/app/javascript/dashboard/components-next/captain/pageComponents/AssistantSelector.vue
new file mode 100644
index 000000000..4998e4044
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/AssistantSelector.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue
new file mode 100644
index 000000000..82f8fa308
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue
new file mode 100644
index 000000000..31e18394f
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/Paywall.vue b/app/javascript/dashboard/components-next/captain/pageComponents/Paywall.vue
new file mode 100644
index 000000000..b9deb0ff0
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/Paywall.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue
new file mode 100644
index 000000000..3c8afec07
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue
@@ -0,0 +1,174 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue
new file mode 100644
index 000000000..ac67063ec
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue
new file mode 100644
index 000000000..3a668a757
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue
@@ -0,0 +1,306 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/document/CreateDocumentDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/document/CreateDocumentDialog.vue
new file mode 100644
index 000000000..7db74f523
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/document/CreateDocumentDialog.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/document/DocumentForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/document/DocumentForm.vue
new file mode 100644
index 000000000..2a2730e46
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/document/DocumentForm.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/document/LimitBanner.vue b/app/javascript/dashboard/components-next/captain/pageComponents/document/LimitBanner.vue
new file mode 100644
index 000000000..d4f512f39
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/document/LimitBanner.vue
@@ -0,0 +1,40 @@
+
+
+
+
+ {{ $t('CAPTAIN.BANNER.DOCUMENTS') }}
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/document/RelatedResponses.vue b/app/javascript/dashboard/components-next/captain/pageComponents/document/RelatedResponses.vue
new file mode 100644
index 000000000..6e00eda9b
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/document/RelatedResponses.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue
new file mode 100644
index 000000000..365b78222
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/DocumentPageEmptyState.vue b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/DocumentPageEmptyState.vue
new file mode 100644
index 000000000..39182587e
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/DocumentPageEmptyState.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/InboxPageEmptyState.vue b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/InboxPageEmptyState.vue
new file mode 100644
index 000000000..2a0735930
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/InboxPageEmptyState.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue
new file mode 100644
index 000000000..7e95a4a51
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js
new file mode 100644
index 000000000..af0331430
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js
@@ -0,0 +1,286 @@
+import { INBOX_TYPES } from 'dashboard/helper/inbox';
+
+export const assistantsList = [
+ {
+ account_id: 2,
+ config: { product_name: 'HelpDesk Pro' },
+ created_at: 1736033561,
+ description:
+ 'An advanced AI assistant designed to enhance customer support solutions by automating workflows and providing instant responses.',
+ id: 4,
+ name: 'Support Genie',
+ },
+ {
+ account_id: 3,
+ config: { product_name: 'CRM Tools' },
+ created_at: 1736033562,
+ description:
+ 'Helps streamline customer relationship management by organizing contacts, automating follow-ups, and providing insights.',
+ id: 5,
+ name: 'CRM Assistant',
+ },
+ {
+ account_id: 4,
+ config: { product_name: 'SalesFlow' },
+ created_at: 1736033563,
+ description:
+ 'Optimizes your sales pipeline by tracking prospects, forecasting sales, and automating administrative tasks.',
+ id: 6,
+ name: 'SalesBot',
+ },
+ {
+ account_id: 5,
+ config: { product_name: 'TicketMaster AI' },
+ created_at: 1736033564,
+ description:
+ 'Automates ticket assignment, categorization, and customer query responses to enhance support efficiency.',
+ id: 7,
+ name: 'TicketBot',
+ },
+ {
+ account_id: 6,
+ config: { product_name: 'FinanceAssist' },
+ created_at: 1736033565,
+ description:
+ 'Provides financial analytics, reporting, and insights, helping teams make data-driven financial decisions.',
+ id: 8,
+ name: 'Finance Wizard',
+ },
+ {
+ account_id: 8,
+ config: { product_name: 'HR Assistant' },
+ created_at: 1736033567,
+ description:
+ 'Streamlines HR operations including employee management, payroll, and recruitment processes.',
+ id: 10,
+ name: 'HR Helper',
+ },
+];
+
+export const documentsList = [
+ {
+ account_id: 1,
+ assistant: { id: 1, name: 'Helper Pro' },
+ content:
+ 'Comprehensive guide on using conversation filters to manage chats effectively.',
+ created_at: 1736143272,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688192-how-to-use-conversation-filters',
+ id: 3059,
+ name: 'How to use Conversation Filters? | User Guide | Chatwoot',
+ status: 'available',
+ },
+ {
+ account_id: 2,
+ assistant: { id: 2, name: 'Support Genie' },
+ content:
+ 'Step-by-step guide for automating ticket assignments and improving support workflow in Chatwoot.',
+ created_at: 1736143273,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688200-automating-ticket-assignments',
+ id: 3060,
+ name: 'Automating Ticket Assignments | User Guide | Chatwoot',
+ status: 'available',
+ },
+ {
+ account_id: 3,
+ assistant: { id: 3, name: 'CRM Assistant' },
+ content:
+ 'A detailed guide on managing and organizing customer profiles for better relationship management.',
+ created_at: 1736143274,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688210-managing-customer-profiles',
+ id: 3061,
+ name: 'Managing Customer Profiles | User Guide | Chatwoot',
+ status: 'available',
+ },
+ {
+ account_id: 4,
+ assistant: { id: 4, name: 'SalesBot' },
+ content:
+ 'Learn how to optimize sales tracking and improve your sales forecasting using advanced features.',
+ created_at: 1736143275,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688220-sales-tracking-guide',
+ id: 3062,
+ name: 'Sales Tracking Guide | User Guide | Chatwoot',
+ status: 'available',
+ },
+ {
+ account_id: 5,
+ assistant: { id: 5, name: 'TicketBot' },
+ content:
+ 'How to efficiently create, manage, and resolve tickets in Chatwoot.',
+ created_at: 1736143276,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688230-managing-tickets',
+ id: 3063,
+ name: 'Managing Tickets | User Guide | Chatwoot',
+ status: 'available',
+ },
+ {
+ account_id: 6,
+ assistant: { id: 6, name: 'Finance Wizard' },
+ content:
+ 'Detailed guide on how to use financial reporting tools and generate insightful analytics.',
+ created_at: 1736143277,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688240-financial-reporting',
+ id: 3064,
+ name: 'Financial Reporting | User Guide | Chatwoot',
+ status: 'available',
+ },
+];
+
+export const responsesList = [
+ {
+ account_id: 1,
+ answer:
+ 'Messenger may be deactivated because you are on a free plan or the limit for inboxes might have been reached.',
+ created_at: 1736283330,
+ id: 87,
+ question: 'Why is my Messenger in Chatwoot deactivated?',
+ status: 'pending',
+ assistant: {
+ account_id: 1,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033280,
+ description: 'Assists with general queries and system-wide issues.',
+ id: 1,
+ name: 'Assistant 2',
+ },
+ },
+ {
+ account_id: 2,
+ answer:
+ 'You can integrate your WhatsApp account by navigating to the Integrations section and selecting the WhatsApp integration option.',
+ created_at: 1736283340,
+ id: 88,
+ question: 'How do I integrate WhatsApp with Chatwoot?',
+ assistant: {
+ account_id: 2,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033281,
+ description: 'Helps with integration and setup-related inquiries.',
+ id: 2,
+ name: 'Assistant 3',
+ },
+ },
+ {
+ account_id: 3,
+ answer:
+ "To reset your password, go to the login page and click on 'Forgot Password', then follow the instructions sent to your email.",
+ created_at: 1736283350,
+ id: 89,
+ question: 'How can I reset my password in Chatwoot?',
+ assistant: {
+ account_id: 3,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033282,
+ description: 'Handles account management and recovery support.',
+ id: 3,
+ name: 'Assistant 4',
+ },
+ },
+ {
+ account_id: 4,
+ answer:
+ "You can enable the dark mode in settings by navigating to 'Appearance' and selecting 'Dark Mode'.",
+ created_at: 1736283360,
+ id: 90,
+ question: 'How do I enable dark mode in Chatwoot?',
+ assistant: {
+ account_id: 4,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033283,
+ description: 'Helps with UI and theme-related inquiries.',
+ id: 4,
+ name: 'Assistant 5',
+ },
+ },
+ {
+ account_id: 5,
+ answer:
+ "To add a new team member, navigate to 'Settings', then 'Team', and click on 'Add Team Member'.",
+ created_at: 1736283370,
+ id: 91,
+ question: 'How do I add a new team member in Chatwoot?',
+ assistant: {
+ account_id: 5,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033284,
+ description: 'Supports team management and user access-related queries.',
+ id: 5,
+ name: 'Assistant 6',
+ },
+ },
+ {
+ account_id: 6,
+ answer:
+ "Campaigns in Chatwoot allow you to send targeted messages to specific user segments. You can create them in the 'Campaigns' section.",
+ created_at: 1736283380,
+ id: 92,
+ question: 'What are campaigns in Chatwoot?',
+ assistant: {
+ account_id: 6,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033285,
+ description:
+ 'Specialized in marketing, campaign management, and messaging strategies.',
+ id: 6,
+ name: 'Assistant 7',
+ },
+ },
+];
+
+export const inboxes = [
+ {
+ id: 7,
+ name: 'Email Support',
+ channel_type: INBOX_TYPES.EMAIL,
+ email: 'support@company.com',
+ },
+ {
+ id: 1,
+ name: 'Website Chat',
+ channel_type: INBOX_TYPES.WEB,
+ },
+ {
+ id: 2,
+ name: 'Facebook Support',
+ channel_type: INBOX_TYPES.FB,
+ },
+ {
+ id: 5,
+ name: 'SMS Service',
+ channel_type: INBOX_TYPES.TWILIO,
+ messaging_service_sid: 'MGxxxxxx',
+ },
+ {
+ id: 6,
+ name: 'WhatsApp Support',
+ channel_type: INBOX_TYPES.WHATSAPP,
+ phone_number: '+1987654321',
+ },
+ {
+ id: 8,
+ name: 'Telegram Support',
+ channel_type: INBOX_TYPES.TELEGRAM,
+ },
+ {
+ id: 9,
+ name: 'LINE Support',
+ channel_type: INBOX_TYPES.LINE,
+ },
+ {
+ id: 10,
+ name: 'API Channel',
+ channel_type: INBOX_TYPES.API,
+ },
+ {
+ id: 11,
+ name: 'SMS Basic',
+ channel_type: INBOX_TYPES.SMS,
+ phone_number: '+1555555555',
+ },
+];
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxDialog.vue
new file mode 100644
index 000000000..06c6e8db3
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxDialog.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxForm.vue
new file mode 100644
index 000000000..23aae478f
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxForm.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue
new file mode 100644
index 000000000..0bae4cf17
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/response/LimitBanner.vue b/app/javascript/dashboard/components-next/captain/pageComponents/response/LimitBanner.vue
new file mode 100644
index 000000000..0b5462f0b
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/response/LimitBanner.vue
@@ -0,0 +1,42 @@
+
+
+
+
+ {{ $t('CAPTAIN.BANNER.RESPONSES') }}
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/response/ResponseForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/response/ResponseForm.vue
new file mode 100644
index 000000000..5582199a7
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/response/ResponseForm.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/checkbox/Checkbox.story.vue b/app/javascript/dashboard/components-next/checkbox/Checkbox.story.vue
new file mode 100644
index 000000000..553700bf1
--- /dev/null
+++ b/app/javascript/dashboard/components-next/checkbox/Checkbox.story.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+ Default:
+
+
+
+
+ Checked:
+
+
+
+
+ Indeterminate:
+
+
+
+
+ Indeterminate disabled:
+
+
+
+
+ Disabled:
+
+
+
+
+ Disabled Checked:
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/checkbox/Checkbox.vue b/app/javascript/dashboard/components-next/checkbox/Checkbox.vue
new file mode 100644
index 000000000..986867c37
--- /dev/null
+++ b/app/javascript/dashboard/components-next/checkbox/Checkbox.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/combobox/ComboBox.vue b/app/javascript/dashboard/components-next/combobox/ComboBox.vue
index de84974e6..026a63a70 100644
--- a/app/javascript/dashboard/components-next/combobox/ComboBox.vue
+++ b/app/javascript/dashboard/components-next/combobox/ComboBox.vue
@@ -13,34 +13,14 @@ const props = defineProps({
validator: value =>
value.every(option => 'value' in option && 'label' in option),
},
- placeholder: {
- type: String,
- default: '',
- },
- modelValue: {
- type: [String, Number],
- default: '',
- },
- disabled: {
- type: Boolean,
- default: false,
- },
- searchPlaceholder: {
- type: String,
- default: '',
- },
- emptyState: {
- type: String,
- default: '',
- },
- message: {
- type: String,
- default: '',
- },
- hasError: {
- type: Boolean,
- default: false,
- },
+ placeholder: { type: String, default: '' },
+ modelValue: { type: [String, Number], default: '' },
+ disabled: { type: Boolean, default: false },
+ searchPlaceholder: { type: String, default: '' },
+ emptyState: { type: String, default: '' },
+ message: { type: String, default: '' },
+ hasError: { type: Boolean, default: false },
+ useApiResults: { type: Boolean, default: false }, // useApiResults prop to determine if search is handled by API
});
const emit = defineEmits(['update:modelValue', 'search']);
@@ -54,6 +34,12 @@ const dropdownRef = ref(null);
const comboboxRef = ref(null);
const filteredOptions = computed(() => {
+ // For API search, don't filter options locally
+ if (props.useApiResults && search.value) {
+ return props.options;
+ }
+
+ // For local search, filter options based on search term
const searchTerm = search.value.toLowerCase();
return props.options.filter(option =>
option.label.toLowerCase().includes(searchTerm)
@@ -110,7 +96,7 @@ watch(
:label="selectedLabel"
trailing-icon
:disabled="disabled"
- class="justify-between w-full !px-3 !py-2.5 text-n-slate-12 font-normal group-hover/combobox:border-n-slate-6"
+ class="justify-between w-full !px-3 !py-2.5 text-n-slate-12 font-normal group-hover/combobox:border-n-slate-6 [&:not(.focused)]:hover:enabled:outline-n-slate-6 [&:not(.focused)]:dark:hover:enabled:outline-n-slate-6 [&:not(.focused)]:dark:outline-n-weak focus:outline-n-brand"
:class="{ focused: open }"
:icon="open ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
@click="toggleDropdown"
diff --git a/app/javascript/dashboard/components-next/combobox/ComboBoxDropdown.vue b/app/javascript/dashboard/components-next/combobox/ComboBoxDropdown.vue
index e1cd8a086..e24a366e2 100644
--- a/app/javascript/dashboard/components-next/combobox/ComboBoxDropdown.vue
+++ b/app/javascript/dashboard/components-next/combobox/ComboBoxDropdown.vue
@@ -69,7 +69,7 @@ defineExpose({
:value="searchValue"
type="search"
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
- class="w-full py-2 pl-10 pr-2 text-sm border-none rounded-t-md bg-n-solid-1 text-slate-900 dark:text-slate-50"
+ class="reset-base w-full py-2 pl-10 pr-2 text-sm focus:outline-none border-none rounded-t-md bg-n-solid-1 text-slate-900 dark:text-slate-50"
@input="onInputSearch"
/>
diff --git a/app/javascript/dashboard/components-next/copilot/Copilot.vue b/app/javascript/dashboard/components-next/copilot/Copilot.vue
index 3839532f2..e284bb983 100644
--- a/app/javascript/dashboard/components-next/copilot/Copilot.vue
+++ b/app/javascript/dashboard/components-next/copilot/Copilot.vue
@@ -1,9 +1,14 @@
-
-
+
+
-
+
+
+
+ {{ $t('COPILOT.TRY_THESE_PROMPTS') }}
+
+ useSuggestion(prompt)"
+ >
+ {{ prompt.label }}
+
+
+
+
+
+
+
+ emit('setAssistant', $event)"
+ />
+
+
+
+ {{ $t('CAPTAIN.COPILOT.RESET') }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue b/app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue
index a14508150..1877e1630 100644
--- a/app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue
+++ b/app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue
@@ -1,12 +1,48 @@
@@ -19,8 +55,23 @@ defineProps({
/>
{{ $t('CAPTAIN.NAME') }}
-
- {{ message.content }}
+
+ {{ $t('CAPTAIN.COPILOT.EMPTY_MESSAGE') }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue b/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue
new file mode 100644
index 000000000..ec21cec9f
--- /dev/null
+++ b/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+ emit('setAssistant', assistant)"
+ >
+
+
+
+
+ {{ assistant.name }}
+
+
+ {{ assistant.description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/dialog/Dialog.vue b/app/javascript/dashboard/components-next/dialog/Dialog.vue
index c485549ae..6800b1254 100644
--- a/app/javascript/dashboard/components-next/dialog/Dialog.vue
+++ b/app/javascript/dashboard/components-next/dialog/Dialog.vue
@@ -80,10 +80,12 @@ const maxWidthClass = computed(() => {
const open = () => {
dialogRef.value?.showModal();
};
+
const close = () => {
emit('close');
dialogRef.value?.close();
};
+
const confirm = () => {
emit('confirm');
};
@@ -104,9 +106,10 @@ defineExpose({ open, close });
@close="close"
>
-
@@ -122,13 +125,17 @@ defineExpose({ open, close });
-
+
-
+
@@ -150,6 +157,6 @@ defineExpose({ open, close });
diff --git a/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue b/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue
index 95b368dae..c8e69b049 100644
--- a/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue
+++ b/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue
@@ -29,6 +29,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
+ labelClass: {
+ type: String,
+ default: '',
+ },
});
const emit = defineEmits(['action']);
@@ -71,7 +75,7 @@ onMounted(() => {
:placeholder="
searchPlaceholder || t('DROPDOWN_MENU.SEARCH_PLACEHOLDER')
"
- class="w-full h-8 py-2 pl-10 pr-2 text-sm border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
+ class="reset-base w-full h-8 py-2 pl-10 pr-2 text-sm focus:outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
/>
{
{{ item.emoji }}
- {{
- item.label
- }}
+
+ {{ item.label }}
+
{
// Add extra blur layer only when strong prop is true, as a hack for Chrome's stacked backdrop-blur limitation
// https://issues.chromium.org/issues/40835530
- return "before:content-['\x00A0'] before:absolute before:bottom-0 before:left-0 before:w-full before:h-full before:backdrop-contrast-70 before:backdrop-blur-sm before:z-0 [&>*]:relative";
+ return "before:content-['\x00A0'] before:absolute before:bottom-0 before:left-0 before:w-full before:h-full before:rounded-xl before:backdrop-contrast-70 before:backdrop-blur-sm before:z-0 [&>*]:relative";
});
diff --git a/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownItem.vue b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownItem.vue
index 821b4218b..9db3a09d4 100644
--- a/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownItem.vue
+++ b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownItem.vue
@@ -6,7 +6,8 @@ import { useDropdownContext } from './provider.js';
const props = defineProps({
label: { type: String, default: '' },
icon: { type: [String, Object, Function], default: '' },
- link: { type: String, default: '' },
+ link: { type: [String, Object], default: '' },
+ nativeLink: { type: Boolean, default: false },
click: { type: Function, default: null },
preserveOpen: { type: Boolean, default: false },
});
@@ -18,7 +19,13 @@ defineOptions({
const { closeMenu } = useDropdownContext();
const componentIs = computed(() => {
- if (props.link) return 'router-link';
+ if (props.link) {
+ if (props.nativeLink && typeof props.link === 'string') {
+ return 'a';
+ }
+
+ return 'router-link';
+ }
if (props.click) return 'button';
return 'div';
@@ -41,7 +48,8 @@ const triggerClick = () => {
:class="{
'hover:bg-n-alpha-2 rounded-lg w-full gap-3': !$slots.default,
}"
- :href="props.link || null"
+ :href="componentIs === 'a' ? props.link : null"
+ :to="componentIs === 'router-link' ? props.link : null"
@click="triggerClick"
>
diff --git a/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.story.vue b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.story.vue
new file mode 100644
index 000000000..d42af2a11
--- /dev/null
+++ b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.story.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.vue b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.vue
new file mode 100644
index 000000000..d8e145817
--- /dev/null
+++ b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ note }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.story.vue b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.story.vue
new file mode 100644
index 000000000..48bbc3c93
--- /dev/null
+++ b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.story.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue
new file mode 100644
index 000000000..6d3c48cdc
--- /dev/null
+++ b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ note }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/filter/SaveCustomView.vue b/app/javascript/dashboard/components-next/filter/SaveCustomView.vue
index 95b2e0388..fb7018f1b 100644
--- a/app/javascript/dashboard/components-next/filter/SaveCustomView.vue
+++ b/app/javascript/dashboard/components-next/filter/SaveCustomView.vue
@@ -103,7 +103,7 @@ export default {
{{ $t('FILTER.CUSTOM_VIEWS.ADD.TITLE') }}
diff --git a/app/javascript/dashboard/components-next/filter/contactProvider.js b/app/javascript/dashboard/components-next/filter/contactProvider.js
index 4aceac359..4b2709e62 100644
--- a/app/javascript/dashboard/components-next/filter/contactProvider.js
+++ b/app/javascript/dashboard/components-next/filter/contactProvider.js
@@ -2,7 +2,10 @@ import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useOperators } from './operators';
import { useMapGetter } from 'dashboard/composables/store.js';
-import { buildAttributesFilterTypes } from './helper/filterHelper.js';
+import {
+ buildAttributesFilterTypes,
+ CONTACT_ATTRIBUTES,
+} from './helper/filterHelper.js';
import countries from 'shared/constants/countries.js';
/**
@@ -59,7 +62,11 @@ export function useContactFilterContext() {
* @type {import('vue').ComputedRef}
*/
const customFilterTypes = computed(() =>
- buildAttributesFilterTypes(contactAttributes.value, getOperatorTypes)
+ buildAttributesFilterTypes(
+ contactAttributes.value,
+ getOperatorTypes,
+ 'contact'
+ )
);
/**
@@ -67,8 +74,8 @@ export function useContactFilterContext() {
*/
const filterTypes = computed(() => [
{
- attributeKey: 'name',
- value: 'name',
+ attributeKey: CONTACT_ATTRIBUTES.NAME,
+ value: CONTACT_ATTRIBUTES.NAME,
attributeName: t('CONTACTS_LAYOUT.FILTER.NAME'),
label: t('CONTACTS_LAYOUT.FILTER.NAME'),
inputType: 'plainText',
@@ -77,8 +84,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'email',
- value: 'email',
+ attributeKey: CONTACT_ATTRIBUTES.EMAIL,
+ value: CONTACT_ATTRIBUTES.EMAIL,
attributeName: t('CONTACTS_LAYOUT.FILTER.EMAIL'),
label: t('CONTACTS_LAYOUT.FILTER.EMAIL'),
inputType: 'plainText',
@@ -87,8 +94,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'phone_number',
- value: 'phone_number',
+ attributeKey: CONTACT_ATTRIBUTES.PHONE_NUMBER,
+ value: CONTACT_ATTRIBUTES.PHONE_NUMBER,
attributeName: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'),
label: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'),
inputType: 'plainText',
@@ -97,8 +104,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'identifier',
- value: 'identifier',
+ attributeKey: CONTACT_ATTRIBUTES.IDENTIFIER,
+ value: CONTACT_ATTRIBUTES.IDENTIFIER,
attributeName: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'),
label: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'),
inputType: 'plainText',
@@ -107,8 +114,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'country_code',
- value: 'country_code',
+ attributeKey: CONTACT_ATTRIBUTES.COUNTRY_CODE,
+ value: CONTACT_ATTRIBUTES.COUNTRY_CODE,
attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
inputType: 'searchSelect',
@@ -118,8 +125,8 @@ export function useContactFilterContext() {
attributeModel: 'additional',
},
{
- attributeKey: 'city',
- value: 'city',
+ attributeKey: CONTACT_ATTRIBUTES.CITY,
+ value: CONTACT_ATTRIBUTES.CITY,
attributeName: t('CONTACTS_LAYOUT.FILTER.CITY'),
label: t('CONTACTS_LAYOUT.FILTER.CITY'),
inputType: 'plainText',
@@ -128,8 +135,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'created_at',
- value: 'created_at',
+ attributeKey: CONTACT_ATTRIBUTES.CREATED_AT,
+ value: CONTACT_ATTRIBUTES.CREATED_AT,
attributeName: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'),
label: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'),
inputType: 'date',
@@ -138,8 +145,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'last_activity_at',
- value: 'last_activity_at',
+ attributeKey: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT,
+ value: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT,
attributeName: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'),
label: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'),
inputType: 'date',
@@ -148,8 +155,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'referer',
- value: 'referer',
+ attributeKey: CONTACT_ATTRIBUTES.REFERER,
+ value: CONTACT_ATTRIBUTES.REFERER,
attributeName: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'),
label: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'),
inputType: 'plainText',
@@ -158,8 +165,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'blocked',
- value: 'blocked',
+ attributeKey: CONTACT_ATTRIBUTES.BLOCKED,
+ value: CONTACT_ATTRIBUTES.BLOCKED,
attributeName: t('CONTACTS_LAYOUT.FILTER.BLOCKED'),
label: t('CONTACTS_LAYOUT.FILTER.BLOCKED'),
inputType: 'searchSelect',
diff --git a/app/javascript/dashboard/components-next/filter/helper/filterHelper.js b/app/javascript/dashboard/components-next/filter/helper/filterHelper.js
index 2160365f6..cb5acfe40 100644
--- a/app/javascript/dashboard/components-next/filter/helper/filterHelper.js
+++ b/app/javascript/dashboard/components-next/filter/helper/filterHelper.js
@@ -1,3 +1,35 @@
+/**
+ * Standard attributes of the conversation model
+ */
+export const CONVERSATION_ATTRIBUTES = {
+ STATUS: 'status',
+ PRIORITY: 'priority',
+ ASSIGNEE_ID: 'assignee_id',
+ INBOX_ID: 'inbox_id',
+ TEAM_ID: 'team_id',
+ DISPLAY_ID: 'display_id',
+ CAMPAIGN_ID: 'campaign_id',
+ LABELS: 'labels',
+ BROWSER_LANGUAGE: 'browser_language',
+ COUNTRY_CODE: 'country_code',
+ REFERER: 'referer',
+ CREATED_AT: 'created_at',
+ LAST_ACTIVITY_AT: 'last_activity_at',
+};
+
+export const CONTACT_ATTRIBUTES = {
+ NAME: 'name',
+ EMAIL: 'email',
+ PHONE_NUMBER: 'phone_number',
+ IDENTIFIER: 'identifier',
+ COUNTRY_CODE: 'country_code',
+ CITY: 'city',
+ CREATED_AT: 'created_at',
+ LAST_ACTIVITY_AT: 'last_activity_at',
+ REFERER: 'referer',
+ BLOCKED: 'blocked',
+};
+
/**
* Determines the input type for a custom attribute based on its key
* @param {string} key - The attribute display type key
@@ -20,24 +52,37 @@ export const getCustomAttributeInputType = key => {
/**
* Builds filter types for custom attributes
+ * This also removes any conflicting attributes
* @param {Array} attributes - The attributes array
* @param {Function} getOperatorTypes - Function to get operator types
* @returns {Array} Array of filter types
*/
-export const buildAttributesFilterTypes = (attributes, getOperatorTypes) => {
- return attributes.map(attr => ({
- attributeKey: attr.attributeKey,
- value: attr.attributeKey,
- attributeName: attr.attributeDisplayName,
- label: attr.attributeDisplayName,
- inputType: getCustomAttributeInputType(attr.attributeDisplayType),
- filterOperators: getOperatorTypes(attr.attributeDisplayType),
- options:
- attr.attributeDisplayType === 'list'
- ? attr.attributeValues.map(item => ({ id: item, name: item }))
- : [],
- attributeModel: 'customAttributes',
- }));
+export const buildAttributesFilterTypes = (
+ attributes,
+ getOperatorTypes,
+ filterModel = 'conversation'
+) => {
+ const standardAttributes = Object.values(
+ filterModel === 'conversation'
+ ? CONVERSATION_ATTRIBUTES
+ : CONTACT_ATTRIBUTES
+ );
+
+ return attributes
+ .filter(attr => !standardAttributes.includes(attr.attributeKey))
+ .map(attr => ({
+ attributeKey: attr.attributeKey,
+ value: attr.attributeKey,
+ attributeName: attr.attributeDisplayName,
+ label: attr.attributeDisplayName,
+ inputType: getCustomAttributeInputType(attr.attributeDisplayType),
+ filterOperators: getOperatorTypes(attr.attributeDisplayType),
+ options:
+ attr.attributeDisplayType === 'list'
+ ? attr.attributeValues.map(item => ({ id: item, name: item }))
+ : [],
+ attributeModel: 'customAttributes',
+ }));
};
/**
diff --git a/app/javascript/dashboard/components-next/filter/provider.js b/app/javascript/dashboard/components-next/filter/provider.js
index bb775a663..f6d078d76 100644
--- a/app/javascript/dashboard/components-next/filter/provider.js
+++ b/app/javascript/dashboard/components-next/filter/provider.js
@@ -3,7 +3,10 @@ import { useI18n } from 'vue-i18n';
import { useOperators } from './operators';
import { useMapGetter } from 'dashboard/composables/store.js';
import { useChannelIcon } from 'next/icon/provider';
-import { buildAttributesFilterTypes } from './helper/filterHelper';
+import {
+ buildAttributesFilterTypes,
+ CONVERSATION_ATTRIBUTES,
+} from './helper/filterHelper';
import countries from 'shared/constants/countries.js';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
@@ -70,7 +73,11 @@ export function useConversationFilterContext() {
* @type {import('vue').ComputedRef}
*/
const customFilterTypes = computed(() =>
- buildAttributesFilterTypes(conversationAttributes.value, getOperatorTypes)
+ buildAttributesFilterTypes(
+ conversationAttributes.value,
+ getOperatorTypes,
+ 'conversation'
+ )
);
/**
@@ -78,8 +85,8 @@ export function useConversationFilterContext() {
*/
const filterTypes = computed(() => [
{
- attributeKey: 'status',
- value: 'status',
+ attributeKey: CONVERSATION_ATTRIBUTES.STATUS,
+ value: CONVERSATION_ATTRIBUTES.STATUS,
attributeName: t('FILTER.ATTRIBUTES.STATUS'),
label: t('FILTER.ATTRIBUTES.STATUS'),
inputType: 'multiSelect',
@@ -94,8 +101,24 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'assignee_id',
- value: 'assignee_id',
+ attributeKey: CONVERSATION_ATTRIBUTES.PRIORITY,
+ value: CONVERSATION_ATTRIBUTES.PRIORITY,
+ attributeName: t('FILTER.ATTRIBUTES.PRIORITY'),
+ label: t('FILTER.ATTRIBUTES.PRIORITY'),
+ inputType: 'multiSelect',
+ options: ['low', 'medium', 'high', 'urgent'].map(id => {
+ return {
+ id,
+ name: t(`CONVERSATION.PRIORITY.OPTIONS.${id.toUpperCase()}`),
+ };
+ }),
+ dataType: 'text',
+ filterOperators: equalityOperators.value,
+ attributeModel: 'standard',
+ },
+ {
+ attributeKey: CONVERSATION_ATTRIBUTES.ASSIGNEE_ID,
+ value: CONVERSATION_ATTRIBUTES.ASSIGNEE_ID,
attributeName: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'),
label: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'),
inputType: 'searchSelect',
@@ -110,8 +133,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'inbox_id',
- value: 'inbox_id',
+ attributeKey: CONVERSATION_ATTRIBUTES.INBOX_ID,
+ value: CONVERSATION_ATTRIBUTES.INBOX_ID,
attributeName: t('FILTER.ATTRIBUTES.INBOX_NAME'),
label: t('FILTER.ATTRIBUTES.INBOX_NAME'),
inputType: 'searchSelect',
@@ -126,8 +149,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'team_id',
- value: 'team_id',
+ attributeKey: CONVERSATION_ATTRIBUTES.TEAM_ID,
+ value: CONVERSATION_ATTRIBUTES.TEAM_ID,
attributeName: t('FILTER.ATTRIBUTES.TEAM_NAME'),
label: t('FILTER.ATTRIBUTES.TEAM_NAME'),
inputType: 'searchSelect',
@@ -137,8 +160,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'display_id',
- value: 'display_id',
+ attributeKey: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
+ value: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
attributeName: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'),
label: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'),
inputType: 'plainText',
@@ -147,8 +170,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'campaign_id',
- value: 'campaign_id',
+ attributeKey: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID,
+ value: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID,
attributeName: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'),
label: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'),
inputType: 'searchSelect',
@@ -161,8 +184,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'labels',
- value: 'labels',
+ attributeKey: CONVERSATION_ATTRIBUTES.LABELS,
+ value: CONVERSATION_ATTRIBUTES.LABELS,
attributeName: t('FILTER.ATTRIBUTES.LABELS'),
label: t('FILTER.ATTRIBUTES.LABELS'),
inputType: 'multiSelect',
@@ -185,8 +208,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'browser_language',
- value: 'browser_language',
+ attributeKey: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE,
+ value: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE,
attributeName: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'),
label: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'),
inputType: 'searchSelect',
@@ -196,8 +219,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional',
},
{
- attributeKey: 'country_code',
- value: 'country_code',
+ attributeKey: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
+ value: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
inputType: 'searchSelect',
@@ -207,8 +230,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional',
},
{
- attributeKey: 'referer',
- value: 'referer',
+ attributeKey: CONVERSATION_ATTRIBUTES.REFERER,
+ value: CONVERSATION_ATTRIBUTES.REFERER,
attributeName: t('FILTER.ATTRIBUTES.REFERER_LINK'),
label: t('FILTER.ATTRIBUTES.REFERER_LINK'),
inputType: 'plainText',
@@ -217,8 +240,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional',
},
{
- attributeKey: 'created_at',
- value: 'created_at',
+ attributeKey: CONVERSATION_ATTRIBUTES.CREATED_AT,
+ value: CONVERSATION_ATTRIBUTES.CREATED_AT,
attributeName: t('FILTER.ATTRIBUTES.CREATED_AT'),
label: t('FILTER.ATTRIBUTES.CREATED_AT'),
inputType: 'date',
@@ -227,8 +250,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'last_activity_at',
- value: 'last_activity_at',
+ attributeKey: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT,
+ value: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT,
attributeName: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'),
label: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'),
inputType: 'date',
diff --git a/app/javascript/dashboard/components-next/icon/FileIcon.story.vue b/app/javascript/dashboard/components-next/icon/FileIcon.story.vue
new file mode 100644
index 000000000..3a831b96f
--- /dev/null
+++ b/app/javascript/dashboard/components-next/icon/FileIcon.story.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+ {{ file.name }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/icon/FileIcon.vue b/app/javascript/dashboard/components-next/icon/FileIcon.vue
new file mode 100644
index 000000000..8dd9e7ce1
--- /dev/null
+++ b/app/javascript/dashboard/components-next/icon/FileIcon.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/icon/Logo.vue b/app/javascript/dashboard/components-next/icon/Logo.vue
index 2330021c3..6dd0bbd8f 100644
--- a/app/javascript/dashboard/components-next/icon/Logo.vue
+++ b/app/javascript/dashboard/components-next/icon/Logo.vue
@@ -1,6 +1,21 @@
+
+
+
{{ t(`CAMPAIGN.SMS.CREATE.TITLE`) }}
diff --git a/app/javascript/dashboard/components-next/CardLayout.vue b/app/javascript/dashboard/components-next/CardLayout.vue
index 0fd3f5986..462402167 100644
--- a/app/javascript/dashboard/components-next/CardLayout.vue
+++ b/app/javascript/dashboard/components-next/CardLayout.vue
@@ -4,6 +4,10 @@ defineProps({
type: String,
default: 'col',
},
+ selectable: {
+ type: Boolean,
+ default: false,
+ },
});
const emit = defineEmits(['click']);
@@ -18,10 +22,11 @@ const handleClick = () => {
class="flex flex-col w-full shadow outline-1 outline outline-n-container group/cardLayout rounded-2xl bg-n-solid-2"
>
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue b/app/javascript/dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue
index 255c1b113..af04de9a7 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue
@@ -18,6 +18,11 @@ const route = useRoute();
const showDropdown = ref(false);
+// Store the currently hovered label's ID
+// Using JS state management instead of CSS :hover / group hover
+// This will solve the flickering issue when hovering over the last label item
+const hoveredLabel = ref(null);
+
const allLabels = useMapGetter('labels/getLabels');
const contactLabels = useMapGetter('contactLabels/getContactLabels');
@@ -37,7 +42,7 @@ const labelMenuItems = computed(() => {
isSelected: savedLabels.value.some(
savedLabel => savedLabel.id === label.id
),
- action: 'addLabel',
+ action: 'contactLabel',
}))
.toSorted((a, b) => Number(a.isSelected) - Number(b.isSelected));
});
@@ -49,7 +54,7 @@ const fetchLabels = async contactId => {
store.dispatch('contactLabels/get', contactId);
};
-const handleLabelAction = async ({ action, value }) => {
+const handleLabelAction = async ({ value }) => {
try {
// Get current label titles
const currentLabels = savedLabels.value.map(label => label.title);
@@ -59,16 +64,15 @@ const handleLabelAction = async ({ action, value }) => {
if (!selectedLabel) return;
let updatedLabels;
- if (action === 'addLabel') {
- // If label is already selected, remove it (toggle behavior)
- if (currentLabels.includes(selectedLabel.title)) {
- updatedLabels = currentLabels.filter(
- labelTitle => labelTitle !== selectedLabel.title
- );
- } else {
- // Add the new label
- updatedLabels = [...currentLabels, selectedLabel.title];
- }
+
+ // If label is already selected, remove it (toggle behavior)
+ if (currentLabels.includes(selectedLabel.title)) {
+ updatedLabels = currentLabels.filter(
+ labelTitle => labelTitle !== selectedLabel.title
+ );
+ } else {
+ // Add the new label
+ updatedLabels = [...currentLabels, selectedLabel.title];
}
await store.dispatch('contactLabels/update', {
@@ -82,6 +86,10 @@ const handleLabelAction = async ({ action, value }) => {
}
};
+const handleRemoveLabel = labelId => {
+ return handleLabelAction({ value: labelId });
+};
+
watch(
() => props.contactId,
(newVal, oldVal) => {
@@ -95,11 +103,31 @@ onMounted(() => {
fetchLabels(route.params.contactId);
}
});
+
+const handleMouseLeave = () => {
+ // Reset hover state when mouse leaves the container
+ // This ensures all labels return to their default state
+ hoveredLabel.value = null;
+};
+
+const handleLabelHover = labelId => {
+ // Added this to prevent flickering on when showing remove button on hover
+ // If the label item is at end of the line, it will show the remove button
+ // when hovering over the last label item
+ hoveredLabel.value = labelId;
+};
-
-
+
+
({}),
},
+ isUpdating: {
+ type: Boolean,
+ default: false,
+ },
});
-const emit = defineEmits(['goToContactsList']);
+const emit = defineEmits(['goToContactsList', 'toggleBlock']);
const { t } = useI18n();
const slots = useSlots();
@@ -45,9 +45,17 @@ const breadcrumbItems = computed(() => {
return items;
});
+const isContactBlocked = computed(() => {
+ return props.selectedContact?.blocked;
+});
+
const handleBreadcrumbClick = () => {
emit('goToContactsList');
};
+
+const toggleBlock = () => {
+ emit('toggleBlock', isContactBlocked.value);
+};
@@ -57,23 +65,41 @@ const handleBreadcrumbClick = () => {
-
-
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+
@@ -81,7 +107,7 @@ const handleBreadcrumbClick = () => {
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactMergeForm.vue b/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactMergeForm.vue
index 8adcce6b2..2cee66d12 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactMergeForm.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactMergeForm.vue
@@ -50,6 +50,7 @@ const { t } = useI18n();
@@ -266,7 +265,9 @@ defineExpose({
:placeholder="item.placeholder"
:message-type="getMessageType(item.key)"
:custom-input-class="`h-8 !pt-1 !pb-1 ${
- !isDetailsView ? '[&:not(.error,.focus)]:!border-transparent' : ''
+ !isDetailsView
+ ? '[&:not(.error,.focus)]:!outline-transparent'
+ : ''
}`"
class="w-full"
@input="
@@ -303,7 +304,7 @@ defineExpose({
v-model="
state.additionalAttributes.socialProfiles[item.key.toLowerCase()]
"
- class="w-auto min-w-[100px] text-sm bg-transparent reset-base text-n-slate-12 dark:text-n-slate-12 placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10"
+ class="w-auto min-w-[100px] text-sm bg-transparent outline-none reset-base text-n-slate-12 dark:text-n-slate-12 placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10"
:placeholder="item.placeholder"
:size="item.placeholder.length"
@input="emit('update', state)"
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsForm/CreateNewContactDialog.vue b/app/javascript/dashboard/components-next/Contacts/ContactsForm/CreateNewContactDialog.vue
index 806fcf4ac..75400692e 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsForm/CreateNewContactDialog.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/CreateNewContactDialog.vue
@@ -51,16 +51,18 @@ defineExpose({ dialogRef, contactsFormRef, onSuccess });
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactHeader.vue b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactHeader.vue
index 44350bba1..42f526f95 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactHeader.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactHeader.vue
@@ -60,7 +60,7 @@ const emit = defineEmits([
{{ headerTitle }}
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue
index 8b2a53b27..627addff8 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue
@@ -106,10 +106,15 @@ const onImport = async file => {
try {
await store.dispatch('contacts/import', file);
contactImportDialogRef.value?.dialogRef.close();
- useAlert(t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
+ useAlert(
+ t('CONTACTS_LAYOUT.HEADER.ACTIONS.IMPORT_CONTACT.SUCCESS_MESSAGE')
+ );
useTrack(CONTACTS_EVENTS.IMPORT_SUCCESS);
} catch (error) {
- useAlert(error.message ?? t('IMPORT_CONTACTS.ERROR_MESSAGE'));
+ useAlert(
+ error.message ??
+ t('CONTACTS_LAYOUT.HEADER.ACTIONS.IMPORT_CONTACT.ERROR_MESSAGE')
+ );
useTrack(CONTACTS_EVENTS.IMPORT_FAILURE);
}
};
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactSortMenu.vue b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactSortMenu.vue
index b263b882d..7125d8385 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactSortMenu.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactSortMenu.vue
@@ -31,10 +31,6 @@ const sortMenus = [
label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.EMAIL'),
value: 'email',
},
- {
- label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.PHONE_NUMBER'),
- value: 'phone_number',
- },
{
label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.COMPANY'),
value: 'company_name',
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactsActiveFiltersPreview.vue b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactsActiveFiltersPreview.vue
index 36162520d..88ee94604 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactsActiveFiltersPreview.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactsActiveFiltersPreview.vue
@@ -62,7 +62,7 @@ const activeFilterQueryData = computed(() => {
t('CONTACTS_LAYOUT.FILTER.ACTIVE_FILTERS.CLEAR_FILTERS')
"
:show-clear-button="!hasActiveSegments"
- class="max-w-[960px] px-6"
+ class="max-w-[60rem] px-6"
@open-filter="emit('openFilter')"
@clear-filters="emit('clearFilters')"
/>
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsListLayout.vue b/app/javascript/dashboard/components-next/Contacts/ContactsListLayout.vue
index 22b32d7b8..25aac5df8 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsListLayout.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsListLayout.vue
@@ -72,7 +72,7 @@ const openFilter = () => {
@clear-filters="emit('clearFilters')"
/>
-
+
{
const isCurrentUser = note?.user?.id === currentUser.value.id;
return isCurrentUser
? t('CONTACTS_LAYOUT.SIDEBAR.NOTES.YOU')
- : note.user.name;
+ : note?.user?.name || 'Bot';
};
const onAdd = content => {
@@ -87,8 +87,10 @@ useKeyboardEvents(keyboardEvents);
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue
index 504be1547..c11fa2d9d 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue
@@ -1,6 +1,8 @@
-
-
-
+
+
+
- {{ writtenBy }}
+ {{ writtenBy }}
{{ t('CONTACTS_LAYOUT.SIDEBAR.NOTES.WROTE') }}
- {{ dynamicTime(note.createdAt) }}
+
+ {{ dynamicTime(note.createdAt) }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue
index a81c80b87..a2b3ad7fb 100644
--- a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue
+++ b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreview.vue
@@ -21,7 +21,7 @@ const lastNonActivityMessageContent = computed(() => {
props.conversation;
const { email: { subject } = {} } = customAttributes;
return getPlainText(
- subject || lastNonActivityMessage.content || t('CHAT_LIST.NO_CONTENT')
+ subject || lastNonActivityMessage?.content || t('CHAT_LIST.NO_CONTENT')
);
});
diff --git a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue
index 19dc7008a..f1ad9d09b 100644
--- a/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue
+++ b/app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue
@@ -1,5 +1,5 @@
@@ -73,7 +83,11 @@ const hasSlaThreshold = computed(() => props.conversation?.slaPolicyId);
: 'grid-cols-[1fr_20px]'
"
>
-
+
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
import { getInboxIconByType } from 'dashboard/helper/inbox';
import { useRouter, useRoute } from 'vue-router';
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper.js';
@@ -33,6 +33,8 @@ const props = defineProps({
const router = useRouter();
const route = useRoute();
+const cardMessagePreviewWithMetaRef = ref(null);
+
const currentContact = computed(() => props.contact);
const currentContactName = computed(() => currentContact.value?.name);
@@ -56,8 +58,10 @@ const lastActivityAt = computed(() => {
});
const showMessagePreviewWithoutMeta = computed(() => {
- const { slaPolicyId, labels = [] } = props.conversation;
- return !slaPolicyId && labels.length === 0;
+ const { labels = [] } = props.conversation;
+ return (
+ !cardMessagePreviewWithMetaRef.value?.hasSlaThreshold && labels.length === 0
+ );
});
const onCardClick = e => {
@@ -82,6 +86,7 @@ const onCardClick = e => {
@@ -92,7 +97,7 @@ const onCardClick = e => {
:status="currentContactStatus"
rounded-full
/>
-
+
{{ currentContactName }}
@@ -114,11 +119,12 @@ const onCardClick = e => {
diff --git a/app/javascript/dashboard/components-next/Conversation/ConversationCard/SLACardLabel.vue b/app/javascript/dashboard/components-next/Conversation/ConversationCard/SLACardLabel.vue
index a9db5d99b..ff57d6c93 100644
--- a/app/javascript/dashboard/components-next/Conversation/ConversationCard/SLACardLabel.vue
+++ b/app/javascript/dashboard/components-next/Conversation/ConversationCard/SLACardLabel.vue
@@ -31,13 +31,17 @@ const convertObjectCamelCaseToSnakeCase = object => {
const appliedSLA = computed(() => props.conversation?.appliedSla);
const isSlaMissed = computed(() => slaStatus.value?.isSlaMissed);
+const hasSlaThreshold = computed(() => {
+ return slaStatus.value?.threshold && appliedSLA.value?.id;
+});
+
const slaStatusText = computed(() => {
return slaStatus.value?.type?.toUpperCase();
});
const updateSlaStatus = () => {
slaStatus.value = evaluateSLAStatus({
- appliedSla: convertObjectCamelCaseToSnakeCase(appliedSLA.value),
+ appliedSla: convertObjectCamelCaseToSnakeCase(appliedSLA.value || {}),
chat: props.conversation,
});
};
@@ -61,6 +65,21 @@ onUnmounted(() => {
});
watch(() => props.conversation, updateSlaStatus);
+
+// This expose is to provide context to the parent component, so that it can decided weather
+// a new row has to be added to the conversation card or not
+// SLACardLabel > CardMessagePreviewWithMeta > ConversationCard
+//
+// We need to do this becuase each SLA card has it's own SLA timer
+// and it's just convenient to have this logic in the SLACardLabel component
+// However this is a bit hacky, and we should change this in the future
+//
+// TODO: A better implementation would be to have the timer as a shared composable, just like the provider pattern
+// we use across the next components. Have the calculation be done on the top ConversationCard component
+// and then the value be injected to the SLACardLabel component
+defineExpose({
+ hasSlaThreshold,
+});
diff --git a/app/javascript/dashboard/components-next/Editor/Editor.vue b/app/javascript/dashboard/components-next/Editor/Editor.vue
index 671a628dc..6b5fefab9 100644
--- a/app/javascript/dashboard/components-next/Editor/Editor.vue
+++ b/app/javascript/dashboard/components-next/Editor/Editor.vue
@@ -41,6 +41,8 @@ const props = defineProps({
default: 'info',
validator: value => ['info', 'error', 'success'].includes(value),
},
+ enableVariables: { type: Boolean, default: false },
+ enableCannedResponses: { type: Boolean, default: true },
});
const emit = defineEmits(['update:modelValue']);
@@ -116,6 +118,8 @@ watch(
:placeholder="placeholder"
:focus-on-mount="focusOnMount"
:disabled="disabled"
+ :enable-variables="enableVariables"
+ :enable-canned-responses="enableCannedResponses"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
diff --git a/app/javascript/dashboard/components-next/EmptyStateLayout.vue b/app/javascript/dashboard/components-next/EmptyStateLayout.vue
index 04c478ba6..09b8afee5 100644
--- a/app/javascript/dashboard/components-next/EmptyStateLayout.vue
+++ b/app/javascript/dashboard/components-next/EmptyStateLayout.vue
@@ -1,4 +1,6 @@
@@ -16,7 +22,7 @@ defineProps({
class="relative flex flex-col items-center justify-center w-full h-full overflow-hidden"
>
-
+
+
+
diff --git a/app/javascript/dashboard/components-next/HelpCenter/HelpCenterLayout.vue b/app/javascript/dashboard/components-next/HelpCenter/HelpCenterLayout.vue
index 9e3c9ee1e..c430a3d15 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/HelpCenterLayout.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/HelpCenterLayout.vue
@@ -60,7 +60,7 @@ const togglePortalSwitcher = () => {
-
+
{
-
+
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue
index f5b925fbe..1709d0f5f 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue
@@ -36,6 +36,8 @@ const emit = defineEmits([
const { t } = useI18n();
+const isNewArticle = computed(() => !props.article?.id);
+
const saveAndSync = value => {
emit('saveArticle', value);
};
@@ -52,21 +54,32 @@ const quickSave = debounce(
// 2.5 seconds is enough to know that the user has stopped typing and is taking a pause
// so we can save the data to the backend and retrieve the updated data
// this will update the local state with response data
+// Only use to save for existing articles
const saveAndSyncDebounced = debounce(saveAndSync, 2500, false);
+// Debounced save for new articles
+const quickSaveNewArticle = debounce(saveAndSync, 400, false);
+
+const handleSave = value => {
+ if (isNewArticle.value) {
+ quickSaveNewArticle(value);
+ } else {
+ quickSave(value);
+ saveAndSyncDebounced(value);
+ }
+};
+
const articleTitle = computed({
get: () => props.article.title,
set: value => {
- quickSave({ title: value });
- saveAndSyncDebounced({ title: value });
+ handleSave({ title: value });
},
});
const articleContent = computed({
get: () => props.article.content,
set: content => {
- quickSave({ content });
- saveAndSyncDebounced({ content });
+ handleSave({ content });
},
});
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorControls.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorControls.vue
index d46fb5264..a910f4d8d 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorControls.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorControls.vue
@@ -200,7 +200,8 @@ onMounted(() => {
@@ -231,7 +232,8 @@ onMounted(() => {
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorProperties.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorProperties.vue
index 4de31a6ba..aa04ba56a 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorProperties.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorProperties.vue
@@ -52,7 +52,7 @@ onMounted(() => {
@@ -75,7 +75,7 @@ onMounted(() => {
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleHeaderControls.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleHeaderControls.vue
index 5e7feb9fa..6565fab82 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleHeaderControls.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleHeaderControls.vue
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { OnClickOutside } from '@vueuse/components';
+import { useUISettings } from 'dashboard/composables/useUISettings';
import {
ARTICLE_TABS,
CATEGORY_ALL,
@@ -37,6 +38,7 @@ const emit = defineEmits([
const route = useRoute();
const { t } = useI18n();
+const { updateUISettings } = useUISettings();
const isCategoryMenuOpen = ref(false);
const isLocaleMenuOpen = ref(false);
@@ -111,13 +113,12 @@ const localeMenuItems = computed(() => {
}));
});
-const hasMoreThanOneLocaleMenuItems = computed(() => {
- return localeMenuItems.value?.length > 1;
-});
-
const handleLocaleAction = ({ value }) => {
emit('localeChange', value);
isLocaleMenuOpen.value = false;
+ updateUISettings({
+ last_active_locale_code: value,
+ });
};
const handleCategoryAction = ({ value }) => {
@@ -143,7 +144,7 @@ const handleTabChange = value => {
/>
-
+
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalSettings.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalSettings.vue
index ccf1ad997..a49493818 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalSettings.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalSettings.vue
@@ -74,7 +74,7 @@ const handleDeletePortal = () => {
t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.ERROR'),
+ required
+ ),
+ isValidSlug: helpers.withMessage(
+ () => t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.FORMAT_ERROR'),
+ isValidSlug
+ ),
+ },
};
const v$ = useVuelidate(rules, state);
@@ -40,9 +50,9 @@ const nameError = computed(() =>
v$.value.name.$error ? t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.ERROR') : ''
);
-const slugError = computed(() =>
- v$.value.slug.$error ? t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.ERROR') : ''
-);
+const slugError = computed(() => {
+ return v$.value.slug.$errors[0]?.$message || '';
+});
const isSubmitDisabled = computed(() => v$.value.$invalid);
@@ -131,6 +141,7 @@ defineExpose({ dialogRef });
:message="
nameError || t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.MESSAGE')
"
+ @blur="v$.name.$touch()"
/>
diff --git a/app/javascript/dashboard/components-next/HelpCenter/PortalSwitcher/PortalSwitcher.vue b/app/javascript/dashboard/components-next/HelpCenter/PortalSwitcher/PortalSwitcher.vue
index 1b4668b91..5a835f8cb 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/PortalSwitcher/PortalSwitcher.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/PortalSwitcher/PortalSwitcher.vue
@@ -92,7 +92,7 @@ const redirectToPortalHomePage = () => {
+import { computed, ref, onBeforeMount } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { getInboxIconByType } from 'dashboard/helper/inbox';
+import { dynamicTime, shortTimestamp } from 'shared/helpers/timeHelper';
+import {
+ snoozedReopenTimeToTimestamp,
+ shortenSnoozeTime,
+} from 'dashboard/helper/snoozeHelpers';
+import { NOTIFICATION_TYPES_MAPPING } from 'dashboard/routes/dashboard/inbox/helpers/InboxViewHelpers';
+
+import Icon from 'dashboard/components-next/icon/Icon.vue';
+import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
+import CardPriorityIcon from 'dashboard/components-next/Conversation/ConversationCard/CardPriorityIcon.vue';
+import SLACardLabel from 'dashboard/components-next/Conversation/ConversationCard/SLACardLabel.vue';
+import InboxContextMenu from 'dashboard/routes/dashboard/inbox/components/InboxContextMenu.vue';
+
+const props = defineProps({
+ inboxItem: { type: Object, default: () => ({}) },
+ stateInbox: { type: Object, default: () => ({}) },
+});
+
+const emit = defineEmits([
+ 'click',
+ 'contextMenuOpen',
+ 'contextMenuClose',
+ 'markNotificationAsRead',
+ 'markNotificationAsUnRead',
+ 'deleteNotification',
+]);
+
+const { t } = useI18n();
+
+const isContextMenuOpen = ref(false);
+const contextMenuPosition = ref({ x: null, y: null });
+const slaCardLabel = ref(null);
+
+const getMessageClasses = {
+ emphasis: 'text-sm font-medium text-n-slate-11',
+ emphasisUnread: 'text-sm font-medium text-n-slate-12',
+ normal: 'text-sm font-normal text-n-slate-11',
+ normalUnread: 'text-sm text-n-slate-12',
+};
+
+const primaryActor = computed(() => props.inboxItem?.primaryActor);
+const meta = computed(() => primaryActor.value?.meta);
+const assigneeMeta = computed(() => meta.value?.sender);
+const isUnread = computed(() => !props.inboxItem?.readAt);
+const inbox = computed(() => props.stateInbox);
+
+const inboxIcon = computed(() => {
+ const { phoneNumber, channelType } = inbox.value;
+ return getInboxIconByType(channelType, phoneNumber);
+});
+
+const hasSlaThreshold = computed(() => {
+ return slaCardLabel.value?.hasSlaThreshold && primaryActor.value?.slaPolicyId;
+});
+
+const lastActivityAt = computed(() => {
+ const timestamp = props.inboxItem?.lastActivityAt;
+ return timestamp ? shortTimestamp(dynamicTime(timestamp)) : '';
+});
+
+const menuItems = computed(() => [
+ { key: 'delete', label: t('INBOX.MENU_ITEM.DELETE') },
+ {
+ key: isUnread.value ? 'mark_as_read' : 'mark_as_unread',
+ label: t(`INBOX.MENU_ITEM.MARK_AS_${isUnread.value ? 'READ' : 'UNREAD'}`),
+ },
+]);
+
+const messageClasses = computed(() => ({
+ emphasis: isUnread.value
+ ? getMessageClasses.emphasisUnread
+ : getMessageClasses.emphasis,
+ normal: isUnread.value
+ ? getMessageClasses.normalUnread
+ : getMessageClasses.normal,
+}));
+
+const formatPushMessage = message => {
+ if (message.startsWith(': ')) {
+ return message.slice(2);
+ }
+
+ return message.replace(/^([^:]+):/g, (match, name) => {
+ return ``;
+ });
+};
+
+const formattedMessage = computed(() => {
+ const messageContent = ``;
+
+ return isUnread.value
+ ? ` ${messageContent}`
+ : messageContent;
+});
+
+const notificationDetails = computed(() => {
+ const type = props.inboxItem?.notificationType?.toUpperCase() || '';
+ const [icon = '', color = 'text-n-blue-text'] =
+ NOTIFICATION_TYPES_MAPPING[type] || [];
+ return { text: type ? t(`INBOX.TYPES_NEXT.${type}`) : '', icon, color };
+});
+
+const snoozedUntilTime = computed(() => {
+ const { snoozedUntil } = props.inboxItem;
+ if (!snoozedUntil) return null;
+ return shortenSnoozeTime(
+ dynamicTime(snoozedReopenTimeToTimestamp(snoozedUntil))
+ );
+});
+
+const hasLastSnoozed = computed(() => props.inboxItem?.meta?.lastSnoozedAt);
+
+const snoozedText = computed(() => {
+ return !hasLastSnoozed.value
+ ? t('INBOX.TYPES_NEXT.SNOOZED_UNTIL', {
+ time: shortTimestamp(snoozedUntilTime.value),
+ })
+ : t('INBOX.TYPES_NEXT.SNOOZED_ENDS');
+});
+
+const contextMenuActions = {
+ close: () => {
+ isContextMenuOpen.value = false;
+ contextMenuPosition.value = { x: null, y: null };
+ emit('contextMenuClose');
+ },
+ open: e => {
+ e.preventDefault();
+ contextMenuPosition.value = {
+ x: e.pageX || e.clientX,
+ y: e.pageY || e.clientY,
+ };
+ isContextMenuOpen.value = true;
+ emit('contextMenuOpen');
+ },
+ handle: key => {
+ const actions = {
+ mark_as_read: () => emit('markNotificationAsRead', props.inboxItem),
+ mark_as_unread: () => emit('markNotificationAsUnRead', props.inboxItem),
+ delete: () => emit('deleteNotification', props.inboxItem),
+ };
+ actions[key]?.();
+ },
+};
+
+onBeforeMount(contextMenuActions.close);
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ snoozedText }}
+
+
+
+
+
+ {{ notificationDetails.text }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ lastActivityAt }}
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/Label/AddLabel.vue b/app/javascript/dashboard/components-next/Label/AddLabel.vue
index c9571e771..568ed6879 100644
--- a/app/javascript/dashboard/components-next/Label/AddLabel.vue
+++ b/app/javascript/dashboard/components-next/Label/AddLabel.vue
@@ -21,7 +21,7 @@ const showDropdown = ref(false);
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue
index 89784037d..90cb67c4f 100644
--- a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue
@@ -1,6 +1,5 @@
@@ -99,7 +113,8 @@ const errorClass = computed(() => {
{{
@@ -109,6 +124,7 @@ const errorClass = computed(() => {
}}
{
:is-loading="isLoading"
:disabled="contactableInboxesList?.length > 0 && showInboxesDropdown"
allow-create
- type="email"
+ :type="inputType"
class="flex-1 min-h-7"
:class="errorClass"
focus-on-mount
- @input="emit('searchContacts', $event)"
+ @input="handleInput"
@on-click-outside="emit('updateDropdown', 'contacts', false)"
@add="emit('setSelectedContact', $event)"
@remove="emit('clearSelectedContact')"
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue b/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue
index 4e1b0f510..7794d5bdd 100644
--- a/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue
@@ -52,7 +52,7 @@ const targetInboxLabel = computed(() => {
{{ targetInboxLabel }}
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/MessageEditor.vue b/app/javascript/dashboard/components-next/NewConversation/components/MessageEditor.vue
index ca54f7eca..01b2e4a8d 100644
--- a/app/javascript/dashboard/components-next/NewConversation/components/MessageEditor.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/components/MessageEditor.vue
@@ -1,22 +1,22 @@
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue b/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue
index 8d32509b8..3980c6b30 100644
--- a/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue
@@ -84,7 +84,7 @@ const handleSendMessage = template => {
/>
@@ -96,7 +96,7 @@ const handleSendMessage = template => {
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.SEARCH_PLACEHOLDER'
)
"
- class="w-full h-8 py-2 pl-10 pr-2 text-sm border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
+ class="w-full h-8 py-2 pl-10 pr-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
/>
{
{{
diff --git a/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js b/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js
index 12a5e76b3..57e819245 100644
--- a/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js
+++ b/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js
@@ -87,6 +87,21 @@ export const processContactableInboxes = inboxes => {
}));
};
+export const mergeInboxDetails = (inboxesData, inboxesList = []) => {
+ if (!inboxesData || !inboxesData.length) {
+ return [];
+ }
+
+ return inboxesData.map(inboxData => {
+ const matchingInbox =
+ inboxesList.find(inbox => inbox.id === inboxData.id) || {};
+ return {
+ ...camelcaseKeys(matchingInbox, { deep: true }),
+ ...inboxData,
+ };
+ });
+};
+
export const prepareAttachmentPayload = (
attachedFiles,
directUploadsEnabled
@@ -193,10 +208,12 @@ export const searchContacts = async ({ keys, query }) => {
return filteredPayload || [];
};
-export const createNewContact = async email => {
+export const createNewContact = async input => {
const payload = {
- name: getCapitalizedNameFromEmail(email),
- email,
+ name: input.startsWith('+')
+ ? input.slice(1) // Remove the '+' prefix if it exists
+ : getCapitalizedNameFromEmail(input),
+ ...(input.startsWith('+') ? { phone_number: input } : { email: input }),
};
const {
diff --git a/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js b/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js
index 5a2d0092d..3de1fad0d 100644
--- a/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js
+++ b/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js
@@ -110,6 +110,153 @@ describe('composeConversationHelper', () => {
});
});
+ describe('mergeInboxDetails', () => {
+ it('returns empty array if inboxesData is empty or null', () => {
+ expect(helpers.mergeInboxDetails(null)).toEqual([]);
+ expect(helpers.mergeInboxDetails([])).toEqual([]);
+ expect(helpers.mergeInboxDetails(undefined)).toEqual([]);
+ });
+
+ it('merges inbox data with matching inboxes from the list', () => {
+ const inboxesData = [
+ { id: 1, sourceId: 'source1' },
+ { id: 2, sourceId: 'source2' },
+ ];
+
+ const inboxesList = [
+ {
+ id: 1,
+ name: 'Inbox 1',
+ channel_type: 'Channel::Email',
+ channel_id: 10,
+ phone_number: null,
+ },
+ {
+ id: 2,
+ name: 'Inbox 2',
+ channel_type: 'Channel::Whatsapp',
+ channel_id: 20,
+ phone_number: '+1234567890',
+ },
+ {
+ id: 3,
+ name: 'Inbox 3',
+ channel_type: 'Channel::Api',
+ channel_id: 30,
+ phone_number: null,
+ },
+ ];
+
+ const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
+
+ expect(result.length).toBe(2);
+ expect(result[0]).toMatchObject({
+ id: 1,
+ sourceId: 'source1',
+ name: 'Inbox 1',
+ channelType: 'Channel::Email',
+ channelId: 10,
+ phoneNumber: null,
+ });
+
+ expect(result[1]).toMatchObject({
+ id: 2,
+ sourceId: 'source2',
+ name: 'Inbox 2',
+ channelType: 'Channel::Whatsapp',
+ channelId: 20,
+ phoneNumber: '+1234567890',
+ });
+ });
+
+ it('handles inboxes not found in the list', () => {
+ const inboxesData = [
+ { id: 1, sourceId: 'source1' },
+ { id: 99, sourceId: 'source99' }, // This doesn't exist in inboxesList
+ ];
+
+ const inboxesList = [
+ {
+ id: 1,
+ name: 'Inbox 1',
+ channel_type: 'Channel::Email',
+ },
+ ];
+
+ const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
+
+ expect(result.length).toBe(2);
+
+ expect(result[0]).toMatchObject({
+ id: 1,
+ sourceId: 'source1',
+ name: 'Inbox 1',
+ channelType: 'Channel::Email',
+ });
+
+ expect(result[1]).toMatchObject({
+ id: 99,
+ sourceId: 'source99',
+ });
+
+ expect(result[1].name).toBeUndefined();
+ expect(result[1].channelType).toBeUndefined();
+ });
+
+ it('camelcases properties from inboxesList', () => {
+ const inboxesData = [{ id: 1, sourceId: 'source1' }];
+
+ const inboxesList = [
+ {
+ id: 1,
+ name: 'Inbox 1',
+ channel_type: 'Channel::Email',
+ avatar_url: 'https://example.com/avatar.png',
+ working_hours: [
+ {
+ day_of_week: 1,
+ closed_all_day: false,
+ },
+ ],
+ },
+ ];
+
+ const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
+
+ expect(result[0]).toMatchObject({
+ id: 1,
+ sourceId: 'source1',
+ name: 'Inbox 1',
+ channelType: 'Channel::Email',
+ avatarUrl: 'https://example.com/avatar.png',
+ });
+
+ expect(result[0].workingHours[0]).toMatchObject({
+ dayOfWeek: 1,
+ closedAllDay: false,
+ });
+ });
+
+ it('preserves original properties when they conflict with inboxesList', () => {
+ const inboxesData = [
+ { id: 1, sourceId: 'source1', name: 'Original Name' },
+ ];
+
+ const inboxesList = [
+ {
+ id: 1,
+ name: 'List Name',
+ channel_type: 'Channel::Email',
+ },
+ ];
+
+ const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
+
+ expect(result[0].name).toBe('Original Name');
+ expect(result[0].channelType).toBe('Channel::Email');
+ });
+ });
+
describe('prepareAttachmentPayload', () => {
it('prepares direct upload files', () => {
const files = [{ blobSignedId: 'signed1' }];
@@ -429,6 +576,28 @@ describe('composeConversationHelper', () => {
email: 'john@example.com',
});
});
+
+ it('creates new contact with phone number', async () => {
+ const mockContact = {
+ id: 1,
+ name: '919999999999',
+ phone_number: '+919999999999',
+ };
+ ContactAPI.create.mockResolvedValue({
+ data: { payload: { contact: mockContact } },
+ });
+
+ const result = await helpers.createNewContact('+919999999999');
+ expect(result).toEqual({
+ id: 1,
+ name: '919999999999',
+ phoneNumber: '+919999999999',
+ });
+ expect(ContactAPI.create).toHaveBeenCalledWith({
+ name: '919999999999',
+ phone_number: '+919999999999',
+ });
+ });
});
describe('fetchContactableInboxes', () => {
diff --git a/app/javascript/dashboard/components-next/avatar/Avatar.vue b/app/javascript/dashboard/components-next/avatar/Avatar.vue
index fb23b3cff..65859b1c7 100644
--- a/app/javascript/dashboard/components-next/avatar/Avatar.vue
+++ b/app/javascript/dashboard/components-next/avatar/Avatar.vue
@@ -174,7 +174,7 @@ watch(
-
+
+
+
+
+
+
+
+
+
+
+
+ {{ actionLabel }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/button/Button.vue b/app/javascript/dashboard/components-next/button/Button.vue
index fecdbaf9b..c54bd395a 100644
--- a/app/javascript/dashboard/components-next/button/Button.vue
+++ b/app/javascript/dashboard/components-next/button/Button.vue
@@ -6,6 +6,7 @@ import Icon from 'dashboard/components-next/icon/Icon.vue';
import {
VARIANT_OPTIONS,
COLOR_OPTIONS,
+ JUSTIFY_OPTIONS,
SIZE_OPTIONS,
EXCLUDED_ATTRS,
} from './constants.js';
@@ -27,6 +28,11 @@ const props = defineProps({
default: null,
validator: value => SIZE_OPTIONS.includes(value) || value === null,
},
+ justify: {
+ type: String,
+ default: null,
+ validator: value => JUSTIFY_OPTIONS.includes(value) || value === null,
+ },
icon: { type: [String, Object, Function], default: '' },
trailingIcon: { type: Boolean, default: false },
isLoading: { type: Boolean, default: false },
@@ -81,48 +87,70 @@ const computedSize = computed(() => {
return 'md';
});
+const computedJustify = computed(() => {
+ if (props.justify) return props.justify;
+ if (attrs.start || attrs.start === '') return 'start';
+ if (attrs.center || attrs.center === '') return 'center';
+ if (attrs.end || attrs.end === '') return 'end';
+
+ return 'center';
+});
+
const STYLE_CONFIG = {
colors: {
blue: {
- solid: 'bg-n-brand text-white hover:brightness-110 outline-transparent',
+ solid:
+ 'bg-n-brand text-white hover:enabled:brightness-110 focus-visible:brightness-110 outline-transparent',
faded:
- 'bg-n-brand/10 text-n-blue-text hover:bg-n-brand/20 outline-transparent',
- outline: 'text-n-blue-text outline-n-blue-border',
- ghost: 'text-n-blue-text hover:bg-n-alpha-2 outline-transparent',
- link: 'text-n-blue-text hover:underline outline-transparent',
+ 'bg-n-brand/10 text-n-blue-text hover:enabled:bg-n-brand/20 focus-visible:bg-n-brand/20 outline-transparent',
+ outline: 'text-n-blue-text outline-n-brand',
+ ghost:
+ 'text-n-blue-text hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
+ link: 'text-n-blue-text hover:enabled:underline focus-visible:underline outline-transparent',
},
ruby: {
- solid: 'bg-n-ruby-9 text-white hover:bg-n-ruby-10 outline-transparent',
+ solid:
+ 'bg-n-ruby-9 text-white hover:enabled:bg-n-ruby-10 focus-visible:bg-n-ruby-10 outline-transparent',
faded:
- 'bg-n-ruby-9/10 text-n-ruby-11 hover:bg-n-ruby-9/20 outline-transparent',
- outline: 'text-n-ruby-11 hover:bg-n-ruby-9/10 outline-n-ruby-8',
- ghost: 'text-n-ruby-11 hover:bg-n-alpha-2 outline-transparent',
- link: 'text-n-ruby-9 hover:underline outline-transparent',
+ 'bg-n-ruby-9/10 text-n-ruby-11 hover:enabled:bg-n-ruby-9/20 focus-visible:bg-n-ruby-9/20 outline-transparent',
+ outline:
+ 'text-n-ruby-11 hover:enabled:bg-n-ruby-9/10 focus-visible:bg-n-ruby-9/10 outline-n-ruby-8',
+ ghost:
+ 'text-n-ruby-11 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
+ link: 'text-n-ruby-9 hover:enabled:underline focus-visible:underline outline-transparent',
},
amber: {
- solid: 'bg-n-amber-9 text-white hover:bg-n-amber-10 outline-transparent',
+ solid:
+ 'bg-n-amber-9 text-white hover:enabled:bg-n-amber-10 focus-visible:bg-n-amber-10 outline-transparent',
faded:
- 'bg-n-amber-9/10 text-n-slate-12 hover:bg-n-amber-9/20 outline-transparent',
- outline: 'text-n-amber-11 hover:bg-n-amber-9/10 outline-n-amber-9',
- link: 'text-n-amber-9 hover:underline outline-transparent',
- ghost: 'text-n-amber-9 hover:bg-n-alpha-2 outline-transparent',
+ 'bg-n-amber-9/10 text-n-slate-12 hover:enabled:bg-n-amber-9/20 focus-visible:bg-n-amber-9/20 outline-transparent',
+ outline:
+ 'text-n-amber-11 hover:enabled:bg-n-amber-9/10 focus-visible:bg-n-amber-9/10 outline-n-amber-9',
+ link: 'text-n-amber-9 hover:enabled:underline focus-visible:underline outline-transparent',
+ ghost:
+ 'text-n-amber-9 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
},
slate: {
solid:
- 'bg-n-solid-3 dark:hover:bg-n-solid-2 hover:bg-n-alpha-2 text-n-slate-12 outline-n-container',
+ 'bg-n-solid-3 dark:hover:enabled:bg-n-solid-2 dark:focus-visible:bg-n-solid-2 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 text-n-slate-12 outline-n-container',
faded:
- 'bg-n-slate-9/10 text-n-slate-12 hover:bg-n-slate-9/20 outline-transparent',
- outline: 'text-n-slate-11 outline-n-strong hover:bg-n-slate-9/10',
- link: 'text-n-slate-11 hover:text-n-slate-12 hover:underline outline-transparent',
- ghost: 'text-n-slate-12 hover:bg-n-alpha-2 outline-transparent',
+ 'bg-n-slate-9/10 text-n-slate-12 hover:enabled:bg-n-slate-9/20 focus-visible:bg-n-slate-9/20 outline-transparent',
+ outline:
+ 'text-n-slate-11 outline-n-strong hover:enabled:bg-n-slate-9/10 focus-visible:bg-n-slate-9/10',
+ link: 'text-n-slate-11 hover:enabled:text-n-slate-12 focus-visible:text-n-slate-12 hover:enabled:underline focus-visible:underline outline-transparent',
+ ghost:
+ 'text-n-slate-12 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
},
teal: {
- solid: 'bg-n-teal-9 text-white hover:bg-n-teal-10 outline-transparent',
+ solid:
+ 'bg-n-teal-9 text-white hover:enabled:bg-n-teal-10 focus-visible:bg-n-teal-10 outline-transparent',
faded:
- 'bg-n-teal-9/10 text-n-slate-12 hover:bg-n-teal-9/20 outline-transparent',
- outline: 'text-n-teal-11 hover:bg-n-teal-9/10 outline-n-teal-9',
- link: 'text-n-teal-9 hover:underline outline-transparent',
- ghost: 'text-n-teal-9 hover:bg-n-alpha-2 outline-transparent',
+ 'bg-n-teal-9/10 text-n-slate-12 hover:enabled:bg-n-teal-9/20 focus-visible:bg-n-teal-9/20 outline-transparent',
+ outline:
+ 'text-n-teal-11 hover:enabled:bg-n-teal-9/10 focus-visible:bg-n-teal-9/10 outline-n-teal-9',
+ link: 'text-n-teal-9 hover:enabled:underline focus-visible:underline outline-transparent',
+ ghost:
+ 'text-n-teal-9 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
},
},
sizes: {
@@ -151,13 +179,18 @@ const STYLE_CONFIG = {
md: 'text-sm font-medium',
lg: 'text-base',
},
- base: 'inline-flex items-center justify-center min-w-0 gap-2 transition-all duration-200 ease-in-out border-0 rounded-lg outline-1 outline disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50',
+ justify: {
+ start: 'justify-start',
+ center: 'justify-center',
+ end: 'justify-end',
+ },
+ base: 'inline-flex items-center min-w-0 gap-2 transition-all duration-200 ease-in-out border-0 rounded-lg outline-1 outline disabled:opacity-50',
};
const variantClasses = computed(() => {
const variantMap = {
ghost: `${STYLE_CONFIG.colors[computedColor.value].ghost}`,
- link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-4`,
+ link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-2`,
outline: STYLE_CONFIG.colors[computedColor.value].outline,
faded: STYLE_CONFIG.colors[computedColor.value].faded,
solid: STYLE_CONFIG.colors[computedColor.value].solid,
@@ -197,6 +230,7 @@ const linkButtonClasses = computed(() => {
[STYLE_CONFIG.base]: true,
[isLink ? linkButtonClasses : buttonClasses]: true,
[STYLE_CONFIG.fontSize[computedSize]]: true,
+ [STYLE_CONFIG.justify[computedJustify]]: true,
'flex-row-reverse': trailingIcon && !isIconOnly,
}"
>
diff --git a/app/javascript/dashboard/components-next/button/constants.js b/app/javascript/dashboard/components-next/button/constants.js
index e65299420..d463abdd3 100644
--- a/app/javascript/dashboard/components-next/button/constants.js
+++ b/app/javascript/dashboard/components-next/button/constants.js
@@ -1,6 +1,7 @@
export const VARIANT_OPTIONS = ['solid', 'outline', 'faded', 'link', 'ghost'];
export const COLOR_OPTIONS = ['blue', 'ruby', 'amber', 'slate', 'teal'];
export const SIZE_OPTIONS = ['xs', 'sm', 'md', 'lg'];
+export const JUSTIFY_OPTIONS = ['start', 'center', 'end'];
export const EXCLUDED_ATTRS = [
'variant',
@@ -12,4 +13,5 @@ export const EXCLUDED_ATTRS = [
...VARIANT_OPTIONS,
...COLOR_OPTIONS,
...SIZE_OPTIONS,
+ ...JUSTIFY_OPTIONS,
];
diff --git a/app/javascript/dashboard/components-next/captain/PageLayout.vue b/app/javascript/dashboard/components-next/captain/PageLayout.vue
new file mode 100644
index 000000000..7355ac616
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/PageLayout.vue
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ headerTitle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.story.vue
new file mode 100644
index 000000000..b388d067e
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.story.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.vue b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.vue
new file mode 100644
index 000000000..222baaecd
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+ {{ name }}
+
+
+
+
+
+
+
+
+
+
+ {{ description || 'Description not available' }}
+
+
+ {{ lastUpdatedAt }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/AssistantPlayground.vue b/app/javascript/dashboard/components-next/captain/assistant/AssistantPlayground.vue
new file mode 100644
index 000000000..28587745a
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/AssistantPlayground.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+ {{ t('CAPTAIN.PLAYGROUND.HEADER') }}
+
+
+
+
+ {{ t('CAPTAIN.PLAYGROUND.DESCRIPTION') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('CAPTAIN.PLAYGROUND.CREDIT_NOTE') }}
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.story.vue
new file mode 100644
index 000000000..df1ada6cc
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.story.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue
new file mode 100644
index 000000000..d6e546581
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+ {{ name }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ assistant?.name || '' }}
+
+
+
+ {{ externalLink }}
+
+
+ {{ createdAt }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/InboxCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.story.vue
new file mode 100644
index 000000000..2fd0e9756
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.story.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue
new file mode 100644
index 000000000..30fe85b0a
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+ {{ inboxName }}
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/MessageList.vue b/app/javascript/dashboard/components-next/captain/assistant/MessageList.vue
new file mode 100644
index 000000000..1d6529a45
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/MessageList.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.story.vue
new file mode 100644
index 000000000..1a281b799
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.story.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue
new file mode 100644
index 000000000..f00354105
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+
+
+
+ {{ question }}
+
+
+
+
+
+
+
+
+
+ {{ answer }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/AssistantSelector.vue b/app/javascript/dashboard/components-next/captain/pageComponents/AssistantSelector.vue
new file mode 100644
index 000000000..4998e4044
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/AssistantSelector.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue
new file mode 100644
index 000000000..82f8fa308
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue
new file mode 100644
index 000000000..31e18394f
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/Paywall.vue b/app/javascript/dashboard/components-next/captain/pageComponents/Paywall.vue
new file mode 100644
index 000000000..b9deb0ff0
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/Paywall.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue
new file mode 100644
index 000000000..3c8afec07
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue
@@ -0,0 +1,174 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue
new file mode 100644
index 000000000..ac67063ec
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue
new file mode 100644
index 000000000..3a668a757
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue
@@ -0,0 +1,306 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/document/CreateDocumentDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/document/CreateDocumentDialog.vue
new file mode 100644
index 000000000..7db74f523
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/document/CreateDocumentDialog.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/document/DocumentForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/document/DocumentForm.vue
new file mode 100644
index 000000000..2a2730e46
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/document/DocumentForm.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/document/LimitBanner.vue b/app/javascript/dashboard/components-next/captain/pageComponents/document/LimitBanner.vue
new file mode 100644
index 000000000..d4f512f39
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/document/LimitBanner.vue
@@ -0,0 +1,40 @@
+
+
+
+
+ {{ $t('CAPTAIN.BANNER.DOCUMENTS') }}
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/document/RelatedResponses.vue b/app/javascript/dashboard/components-next/captain/pageComponents/document/RelatedResponses.vue
new file mode 100644
index 000000000..6e00eda9b
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/document/RelatedResponses.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue
new file mode 100644
index 000000000..365b78222
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/DocumentPageEmptyState.vue b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/DocumentPageEmptyState.vue
new file mode 100644
index 000000000..39182587e
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/DocumentPageEmptyState.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/InboxPageEmptyState.vue b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/InboxPageEmptyState.vue
new file mode 100644
index 000000000..2a0735930
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/InboxPageEmptyState.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue
new file mode 100644
index 000000000..7e95a4a51
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js
new file mode 100644
index 000000000..af0331430
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js
@@ -0,0 +1,286 @@
+import { INBOX_TYPES } from 'dashboard/helper/inbox';
+
+export const assistantsList = [
+ {
+ account_id: 2,
+ config: { product_name: 'HelpDesk Pro' },
+ created_at: 1736033561,
+ description:
+ 'An advanced AI assistant designed to enhance customer support solutions by automating workflows and providing instant responses.',
+ id: 4,
+ name: 'Support Genie',
+ },
+ {
+ account_id: 3,
+ config: { product_name: 'CRM Tools' },
+ created_at: 1736033562,
+ description:
+ 'Helps streamline customer relationship management by organizing contacts, automating follow-ups, and providing insights.',
+ id: 5,
+ name: 'CRM Assistant',
+ },
+ {
+ account_id: 4,
+ config: { product_name: 'SalesFlow' },
+ created_at: 1736033563,
+ description:
+ 'Optimizes your sales pipeline by tracking prospects, forecasting sales, and automating administrative tasks.',
+ id: 6,
+ name: 'SalesBot',
+ },
+ {
+ account_id: 5,
+ config: { product_name: 'TicketMaster AI' },
+ created_at: 1736033564,
+ description:
+ 'Automates ticket assignment, categorization, and customer query responses to enhance support efficiency.',
+ id: 7,
+ name: 'TicketBot',
+ },
+ {
+ account_id: 6,
+ config: { product_name: 'FinanceAssist' },
+ created_at: 1736033565,
+ description:
+ 'Provides financial analytics, reporting, and insights, helping teams make data-driven financial decisions.',
+ id: 8,
+ name: 'Finance Wizard',
+ },
+ {
+ account_id: 8,
+ config: { product_name: 'HR Assistant' },
+ created_at: 1736033567,
+ description:
+ 'Streamlines HR operations including employee management, payroll, and recruitment processes.',
+ id: 10,
+ name: 'HR Helper',
+ },
+];
+
+export const documentsList = [
+ {
+ account_id: 1,
+ assistant: { id: 1, name: 'Helper Pro' },
+ content:
+ 'Comprehensive guide on using conversation filters to manage chats effectively.',
+ created_at: 1736143272,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688192-how-to-use-conversation-filters',
+ id: 3059,
+ name: 'How to use Conversation Filters? | User Guide | Chatwoot',
+ status: 'available',
+ },
+ {
+ account_id: 2,
+ assistant: { id: 2, name: 'Support Genie' },
+ content:
+ 'Step-by-step guide for automating ticket assignments and improving support workflow in Chatwoot.',
+ created_at: 1736143273,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688200-automating-ticket-assignments',
+ id: 3060,
+ name: 'Automating Ticket Assignments | User Guide | Chatwoot',
+ status: 'available',
+ },
+ {
+ account_id: 3,
+ assistant: { id: 3, name: 'CRM Assistant' },
+ content:
+ 'A detailed guide on managing and organizing customer profiles for better relationship management.',
+ created_at: 1736143274,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688210-managing-customer-profiles',
+ id: 3061,
+ name: 'Managing Customer Profiles | User Guide | Chatwoot',
+ status: 'available',
+ },
+ {
+ account_id: 4,
+ assistant: { id: 4, name: 'SalesBot' },
+ content:
+ 'Learn how to optimize sales tracking and improve your sales forecasting using advanced features.',
+ created_at: 1736143275,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688220-sales-tracking-guide',
+ id: 3062,
+ name: 'Sales Tracking Guide | User Guide | Chatwoot',
+ status: 'available',
+ },
+ {
+ account_id: 5,
+ assistant: { id: 5, name: 'TicketBot' },
+ content:
+ 'How to efficiently create, manage, and resolve tickets in Chatwoot.',
+ created_at: 1736143276,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688230-managing-tickets',
+ id: 3063,
+ name: 'Managing Tickets | User Guide | Chatwoot',
+ status: 'available',
+ },
+ {
+ account_id: 6,
+ assistant: { id: 6, name: 'Finance Wizard' },
+ content:
+ 'Detailed guide on how to use financial reporting tools and generate insightful analytics.',
+ created_at: 1736143277,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688240-financial-reporting',
+ id: 3064,
+ name: 'Financial Reporting | User Guide | Chatwoot',
+ status: 'available',
+ },
+];
+
+export const responsesList = [
+ {
+ account_id: 1,
+ answer:
+ 'Messenger may be deactivated because you are on a free plan or the limit for inboxes might have been reached.',
+ created_at: 1736283330,
+ id: 87,
+ question: 'Why is my Messenger in Chatwoot deactivated?',
+ status: 'pending',
+ assistant: {
+ account_id: 1,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033280,
+ description: 'Assists with general queries and system-wide issues.',
+ id: 1,
+ name: 'Assistant 2',
+ },
+ },
+ {
+ account_id: 2,
+ answer:
+ 'You can integrate your WhatsApp account by navigating to the Integrations section and selecting the WhatsApp integration option.',
+ created_at: 1736283340,
+ id: 88,
+ question: 'How do I integrate WhatsApp with Chatwoot?',
+ assistant: {
+ account_id: 2,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033281,
+ description: 'Helps with integration and setup-related inquiries.',
+ id: 2,
+ name: 'Assistant 3',
+ },
+ },
+ {
+ account_id: 3,
+ answer:
+ "To reset your password, go to the login page and click on 'Forgot Password', then follow the instructions sent to your email.",
+ created_at: 1736283350,
+ id: 89,
+ question: 'How can I reset my password in Chatwoot?',
+ assistant: {
+ account_id: 3,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033282,
+ description: 'Handles account management and recovery support.',
+ id: 3,
+ name: 'Assistant 4',
+ },
+ },
+ {
+ account_id: 4,
+ answer:
+ "You can enable the dark mode in settings by navigating to 'Appearance' and selecting 'Dark Mode'.",
+ created_at: 1736283360,
+ id: 90,
+ question: 'How do I enable dark mode in Chatwoot?',
+ assistant: {
+ account_id: 4,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033283,
+ description: 'Helps with UI and theme-related inquiries.',
+ id: 4,
+ name: 'Assistant 5',
+ },
+ },
+ {
+ account_id: 5,
+ answer:
+ "To add a new team member, navigate to 'Settings', then 'Team', and click on 'Add Team Member'.",
+ created_at: 1736283370,
+ id: 91,
+ question: 'How do I add a new team member in Chatwoot?',
+ assistant: {
+ account_id: 5,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033284,
+ description: 'Supports team management and user access-related queries.',
+ id: 5,
+ name: 'Assistant 6',
+ },
+ },
+ {
+ account_id: 6,
+ answer:
+ "Campaigns in Chatwoot allow you to send targeted messages to specific user segments. You can create them in the 'Campaigns' section.",
+ created_at: 1736283380,
+ id: 92,
+ question: 'What are campaigns in Chatwoot?',
+ assistant: {
+ account_id: 6,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033285,
+ description:
+ 'Specialized in marketing, campaign management, and messaging strategies.',
+ id: 6,
+ name: 'Assistant 7',
+ },
+ },
+];
+
+export const inboxes = [
+ {
+ id: 7,
+ name: 'Email Support',
+ channel_type: INBOX_TYPES.EMAIL,
+ email: 'support@company.com',
+ },
+ {
+ id: 1,
+ name: 'Website Chat',
+ channel_type: INBOX_TYPES.WEB,
+ },
+ {
+ id: 2,
+ name: 'Facebook Support',
+ channel_type: INBOX_TYPES.FB,
+ },
+ {
+ id: 5,
+ name: 'SMS Service',
+ channel_type: INBOX_TYPES.TWILIO,
+ messaging_service_sid: 'MGxxxxxx',
+ },
+ {
+ id: 6,
+ name: 'WhatsApp Support',
+ channel_type: INBOX_TYPES.WHATSAPP,
+ phone_number: '+1987654321',
+ },
+ {
+ id: 8,
+ name: 'Telegram Support',
+ channel_type: INBOX_TYPES.TELEGRAM,
+ },
+ {
+ id: 9,
+ name: 'LINE Support',
+ channel_type: INBOX_TYPES.LINE,
+ },
+ {
+ id: 10,
+ name: 'API Channel',
+ channel_type: INBOX_TYPES.API,
+ },
+ {
+ id: 11,
+ name: 'SMS Basic',
+ channel_type: INBOX_TYPES.SMS,
+ phone_number: '+1555555555',
+ },
+];
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxDialog.vue
new file mode 100644
index 000000000..06c6e8db3
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxDialog.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxForm.vue
new file mode 100644
index 000000000..23aae478f
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxForm.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue
new file mode 100644
index 000000000..0bae4cf17
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/response/LimitBanner.vue b/app/javascript/dashboard/components-next/captain/pageComponents/response/LimitBanner.vue
new file mode 100644
index 000000000..0b5462f0b
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/response/LimitBanner.vue
@@ -0,0 +1,42 @@
+
+
+
+
+ {{ $t('CAPTAIN.BANNER.RESPONSES') }}
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/response/ResponseForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/response/ResponseForm.vue
new file mode 100644
index 000000000..5582199a7
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/response/ResponseForm.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/checkbox/Checkbox.story.vue b/app/javascript/dashboard/components-next/checkbox/Checkbox.story.vue
new file mode 100644
index 000000000..553700bf1
--- /dev/null
+++ b/app/javascript/dashboard/components-next/checkbox/Checkbox.story.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+ Default:
+
+
+
+
+ Checked:
+
+
+
+
+ Indeterminate:
+
+
+
+
+ Indeterminate disabled:
+
+
+
+
+ Disabled:
+
+
+
+
+ Disabled Checked:
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/checkbox/Checkbox.vue b/app/javascript/dashboard/components-next/checkbox/Checkbox.vue
new file mode 100644
index 000000000..986867c37
--- /dev/null
+++ b/app/javascript/dashboard/components-next/checkbox/Checkbox.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/combobox/ComboBox.vue b/app/javascript/dashboard/components-next/combobox/ComboBox.vue
index de84974e6..026a63a70 100644
--- a/app/javascript/dashboard/components-next/combobox/ComboBox.vue
+++ b/app/javascript/dashboard/components-next/combobox/ComboBox.vue
@@ -13,34 +13,14 @@ const props = defineProps({
validator: value =>
value.every(option => 'value' in option && 'label' in option),
},
- placeholder: {
- type: String,
- default: '',
- },
- modelValue: {
- type: [String, Number],
- default: '',
- },
- disabled: {
- type: Boolean,
- default: false,
- },
- searchPlaceholder: {
- type: String,
- default: '',
- },
- emptyState: {
- type: String,
- default: '',
- },
- message: {
- type: String,
- default: '',
- },
- hasError: {
- type: Boolean,
- default: false,
- },
+ placeholder: { type: String, default: '' },
+ modelValue: { type: [String, Number], default: '' },
+ disabled: { type: Boolean, default: false },
+ searchPlaceholder: { type: String, default: '' },
+ emptyState: { type: String, default: '' },
+ message: { type: String, default: '' },
+ hasError: { type: Boolean, default: false },
+ useApiResults: { type: Boolean, default: false }, // useApiResults prop to determine if search is handled by API
});
const emit = defineEmits(['update:modelValue', 'search']);
@@ -54,6 +34,12 @@ const dropdownRef = ref(null);
const comboboxRef = ref(null);
const filteredOptions = computed(() => {
+ // For API search, don't filter options locally
+ if (props.useApiResults && search.value) {
+ return props.options;
+ }
+
+ // For local search, filter options based on search term
const searchTerm = search.value.toLowerCase();
return props.options.filter(option =>
option.label.toLowerCase().includes(searchTerm)
@@ -110,7 +96,7 @@ watch(
:label="selectedLabel"
trailing-icon
:disabled="disabled"
- class="justify-between w-full !px-3 !py-2.5 text-n-slate-12 font-normal group-hover/combobox:border-n-slate-6"
+ class="justify-between w-full !px-3 !py-2.5 text-n-slate-12 font-normal group-hover/combobox:border-n-slate-6 [&:not(.focused)]:hover:enabled:outline-n-slate-6 [&:not(.focused)]:dark:hover:enabled:outline-n-slate-6 [&:not(.focused)]:dark:outline-n-weak focus:outline-n-brand"
:class="{ focused: open }"
:icon="open ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
@click="toggleDropdown"
diff --git a/app/javascript/dashboard/components-next/combobox/ComboBoxDropdown.vue b/app/javascript/dashboard/components-next/combobox/ComboBoxDropdown.vue
index e1cd8a086..e24a366e2 100644
--- a/app/javascript/dashboard/components-next/combobox/ComboBoxDropdown.vue
+++ b/app/javascript/dashboard/components-next/combobox/ComboBoxDropdown.vue
@@ -69,7 +69,7 @@ defineExpose({
:value="searchValue"
type="search"
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
- class="w-full py-2 pl-10 pr-2 text-sm border-none rounded-t-md bg-n-solid-1 text-slate-900 dark:text-slate-50"
+ class="reset-base w-full py-2 pl-10 pr-2 text-sm focus:outline-none border-none rounded-t-md bg-n-solid-1 text-slate-900 dark:text-slate-50"
@input="onInputSearch"
/>
diff --git a/app/javascript/dashboard/components-next/copilot/Copilot.vue b/app/javascript/dashboard/components-next/copilot/Copilot.vue
index 3839532f2..e284bb983 100644
--- a/app/javascript/dashboard/components-next/copilot/Copilot.vue
+++ b/app/javascript/dashboard/components-next/copilot/Copilot.vue
@@ -1,9 +1,14 @@
-
-
+
+
-
+
+
+
+ {{ $t('COPILOT.TRY_THESE_PROMPTS') }}
+
+ useSuggestion(prompt)"
+ >
+ {{ prompt.label }}
+
+
+
+
+
+
+
+ emit('setAssistant', $event)"
+ />
+
+
+
+ {{ $t('CAPTAIN.COPILOT.RESET') }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue b/app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue
index a14508150..1877e1630 100644
--- a/app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue
+++ b/app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue
@@ -1,12 +1,48 @@
@@ -19,8 +55,23 @@ defineProps({
/>
{{ $t('CAPTAIN.NAME') }}
-
- {{ message.content }}
+
+ {{ $t('CAPTAIN.COPILOT.EMPTY_MESSAGE') }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue b/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue
new file mode 100644
index 000000000..ec21cec9f
--- /dev/null
+++ b/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+ emit('setAssistant', assistant)"
+ >
+
+
+
+
+ {{ assistant.name }}
+
+
+ {{ assistant.description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/dialog/Dialog.vue b/app/javascript/dashboard/components-next/dialog/Dialog.vue
index c485549ae..6800b1254 100644
--- a/app/javascript/dashboard/components-next/dialog/Dialog.vue
+++ b/app/javascript/dashboard/components-next/dialog/Dialog.vue
@@ -80,10 +80,12 @@ const maxWidthClass = computed(() => {
const open = () => {
dialogRef.value?.showModal();
};
+
const close = () => {
emit('close');
dialogRef.value?.close();
};
+
const confirm = () => {
emit('confirm');
};
@@ -104,9 +106,10 @@ defineExpose({ open, close });
@close="close"
>
-
@@ -122,13 +125,17 @@ defineExpose({ open, close });
-
+
-
+
@@ -150,6 +157,6 @@ defineExpose({ open, close });
diff --git a/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue b/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue
index 95b368dae..c8e69b049 100644
--- a/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue
+++ b/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue
@@ -29,6 +29,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
+ labelClass: {
+ type: String,
+ default: '',
+ },
});
const emit = defineEmits(['action']);
@@ -71,7 +75,7 @@ onMounted(() => {
:placeholder="
searchPlaceholder || t('DROPDOWN_MENU.SEARCH_PLACEHOLDER')
"
- class="w-full h-8 py-2 pl-10 pr-2 text-sm border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
+ class="reset-base w-full h-8 py-2 pl-10 pr-2 text-sm focus:outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
/>
{
{{ item.emoji }}
- {{
- item.label
- }}
+
+ {{ item.label }}
+
{
// Add extra blur layer only when strong prop is true, as a hack for Chrome's stacked backdrop-blur limitation
// https://issues.chromium.org/issues/40835530
- return "before:content-['\x00A0'] before:absolute before:bottom-0 before:left-0 before:w-full before:h-full before:backdrop-contrast-70 before:backdrop-blur-sm before:z-0 [&>*]:relative";
+ return "before:content-['\x00A0'] before:absolute before:bottom-0 before:left-0 before:w-full before:h-full before:rounded-xl before:backdrop-contrast-70 before:backdrop-blur-sm before:z-0 [&>*]:relative";
});
diff --git a/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownItem.vue b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownItem.vue
index 821b4218b..9db3a09d4 100644
--- a/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownItem.vue
+++ b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownItem.vue
@@ -6,7 +6,8 @@ import { useDropdownContext } from './provider.js';
const props = defineProps({
label: { type: String, default: '' },
icon: { type: [String, Object, Function], default: '' },
- link: { type: String, default: '' },
+ link: { type: [String, Object], default: '' },
+ nativeLink: { type: Boolean, default: false },
click: { type: Function, default: null },
preserveOpen: { type: Boolean, default: false },
});
@@ -18,7 +19,13 @@ defineOptions({
const { closeMenu } = useDropdownContext();
const componentIs = computed(() => {
- if (props.link) return 'router-link';
+ if (props.link) {
+ if (props.nativeLink && typeof props.link === 'string') {
+ return 'a';
+ }
+
+ return 'router-link';
+ }
if (props.click) return 'button';
return 'div';
@@ -41,7 +48,8 @@ const triggerClick = () => {
:class="{
'hover:bg-n-alpha-2 rounded-lg w-full gap-3': !$slots.default,
}"
- :href="props.link || null"
+ :href="componentIs === 'a' ? props.link : null"
+ :to="componentIs === 'router-link' ? props.link : null"
@click="triggerClick"
>
diff --git a/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.story.vue b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.story.vue
new file mode 100644
index 000000000..d42af2a11
--- /dev/null
+++ b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.story.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.vue b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.vue
new file mode 100644
index 000000000..d8e145817
--- /dev/null
+++ b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ note }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.story.vue b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.story.vue
new file mode 100644
index 000000000..48bbc3c93
--- /dev/null
+++ b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.story.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue
new file mode 100644
index 000000000..6d3c48cdc
--- /dev/null
+++ b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ note }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/filter/SaveCustomView.vue b/app/javascript/dashboard/components-next/filter/SaveCustomView.vue
index 95b2e0388..fb7018f1b 100644
--- a/app/javascript/dashboard/components-next/filter/SaveCustomView.vue
+++ b/app/javascript/dashboard/components-next/filter/SaveCustomView.vue
@@ -103,7 +103,7 @@ export default {
{{ $t('FILTER.CUSTOM_VIEWS.ADD.TITLE') }}
diff --git a/app/javascript/dashboard/components-next/filter/contactProvider.js b/app/javascript/dashboard/components-next/filter/contactProvider.js
index 4aceac359..4b2709e62 100644
--- a/app/javascript/dashboard/components-next/filter/contactProvider.js
+++ b/app/javascript/dashboard/components-next/filter/contactProvider.js
@@ -2,7 +2,10 @@ import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useOperators } from './operators';
import { useMapGetter } from 'dashboard/composables/store.js';
-import { buildAttributesFilterTypes } from './helper/filterHelper.js';
+import {
+ buildAttributesFilterTypes,
+ CONTACT_ATTRIBUTES,
+} from './helper/filterHelper.js';
import countries from 'shared/constants/countries.js';
/**
@@ -59,7 +62,11 @@ export function useContactFilterContext() {
* @type {import('vue').ComputedRef}
*/
const customFilterTypes = computed(() =>
- buildAttributesFilterTypes(contactAttributes.value, getOperatorTypes)
+ buildAttributesFilterTypes(
+ contactAttributes.value,
+ getOperatorTypes,
+ 'contact'
+ )
);
/**
@@ -67,8 +74,8 @@ export function useContactFilterContext() {
*/
const filterTypes = computed(() => [
{
- attributeKey: 'name',
- value: 'name',
+ attributeKey: CONTACT_ATTRIBUTES.NAME,
+ value: CONTACT_ATTRIBUTES.NAME,
attributeName: t('CONTACTS_LAYOUT.FILTER.NAME'),
label: t('CONTACTS_LAYOUT.FILTER.NAME'),
inputType: 'plainText',
@@ -77,8 +84,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'email',
- value: 'email',
+ attributeKey: CONTACT_ATTRIBUTES.EMAIL,
+ value: CONTACT_ATTRIBUTES.EMAIL,
attributeName: t('CONTACTS_LAYOUT.FILTER.EMAIL'),
label: t('CONTACTS_LAYOUT.FILTER.EMAIL'),
inputType: 'plainText',
@@ -87,8 +94,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'phone_number',
- value: 'phone_number',
+ attributeKey: CONTACT_ATTRIBUTES.PHONE_NUMBER,
+ value: CONTACT_ATTRIBUTES.PHONE_NUMBER,
attributeName: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'),
label: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'),
inputType: 'plainText',
@@ -97,8 +104,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'identifier',
- value: 'identifier',
+ attributeKey: CONTACT_ATTRIBUTES.IDENTIFIER,
+ value: CONTACT_ATTRIBUTES.IDENTIFIER,
attributeName: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'),
label: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'),
inputType: 'plainText',
@@ -107,8 +114,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'country_code',
- value: 'country_code',
+ attributeKey: CONTACT_ATTRIBUTES.COUNTRY_CODE,
+ value: CONTACT_ATTRIBUTES.COUNTRY_CODE,
attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
inputType: 'searchSelect',
@@ -118,8 +125,8 @@ export function useContactFilterContext() {
attributeModel: 'additional',
},
{
- attributeKey: 'city',
- value: 'city',
+ attributeKey: CONTACT_ATTRIBUTES.CITY,
+ value: CONTACT_ATTRIBUTES.CITY,
attributeName: t('CONTACTS_LAYOUT.FILTER.CITY'),
label: t('CONTACTS_LAYOUT.FILTER.CITY'),
inputType: 'plainText',
@@ -128,8 +135,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'created_at',
- value: 'created_at',
+ attributeKey: CONTACT_ATTRIBUTES.CREATED_AT,
+ value: CONTACT_ATTRIBUTES.CREATED_AT,
attributeName: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'),
label: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'),
inputType: 'date',
@@ -138,8 +145,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'last_activity_at',
- value: 'last_activity_at',
+ attributeKey: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT,
+ value: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT,
attributeName: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'),
label: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'),
inputType: 'date',
@@ -148,8 +155,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'referer',
- value: 'referer',
+ attributeKey: CONTACT_ATTRIBUTES.REFERER,
+ value: CONTACT_ATTRIBUTES.REFERER,
attributeName: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'),
label: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'),
inputType: 'plainText',
@@ -158,8 +165,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'blocked',
- value: 'blocked',
+ attributeKey: CONTACT_ATTRIBUTES.BLOCKED,
+ value: CONTACT_ATTRIBUTES.BLOCKED,
attributeName: t('CONTACTS_LAYOUT.FILTER.BLOCKED'),
label: t('CONTACTS_LAYOUT.FILTER.BLOCKED'),
inputType: 'searchSelect',
diff --git a/app/javascript/dashboard/components-next/filter/helper/filterHelper.js b/app/javascript/dashboard/components-next/filter/helper/filterHelper.js
index 2160365f6..cb5acfe40 100644
--- a/app/javascript/dashboard/components-next/filter/helper/filterHelper.js
+++ b/app/javascript/dashboard/components-next/filter/helper/filterHelper.js
@@ -1,3 +1,35 @@
+/**
+ * Standard attributes of the conversation model
+ */
+export const CONVERSATION_ATTRIBUTES = {
+ STATUS: 'status',
+ PRIORITY: 'priority',
+ ASSIGNEE_ID: 'assignee_id',
+ INBOX_ID: 'inbox_id',
+ TEAM_ID: 'team_id',
+ DISPLAY_ID: 'display_id',
+ CAMPAIGN_ID: 'campaign_id',
+ LABELS: 'labels',
+ BROWSER_LANGUAGE: 'browser_language',
+ COUNTRY_CODE: 'country_code',
+ REFERER: 'referer',
+ CREATED_AT: 'created_at',
+ LAST_ACTIVITY_AT: 'last_activity_at',
+};
+
+export const CONTACT_ATTRIBUTES = {
+ NAME: 'name',
+ EMAIL: 'email',
+ PHONE_NUMBER: 'phone_number',
+ IDENTIFIER: 'identifier',
+ COUNTRY_CODE: 'country_code',
+ CITY: 'city',
+ CREATED_AT: 'created_at',
+ LAST_ACTIVITY_AT: 'last_activity_at',
+ REFERER: 'referer',
+ BLOCKED: 'blocked',
+};
+
/**
* Determines the input type for a custom attribute based on its key
* @param {string} key - The attribute display type key
@@ -20,24 +52,37 @@ export const getCustomAttributeInputType = key => {
/**
* Builds filter types for custom attributes
+ * This also removes any conflicting attributes
* @param {Array} attributes - The attributes array
* @param {Function} getOperatorTypes - Function to get operator types
* @returns {Array} Array of filter types
*/
-export const buildAttributesFilterTypes = (attributes, getOperatorTypes) => {
- return attributes.map(attr => ({
- attributeKey: attr.attributeKey,
- value: attr.attributeKey,
- attributeName: attr.attributeDisplayName,
- label: attr.attributeDisplayName,
- inputType: getCustomAttributeInputType(attr.attributeDisplayType),
- filterOperators: getOperatorTypes(attr.attributeDisplayType),
- options:
- attr.attributeDisplayType === 'list'
- ? attr.attributeValues.map(item => ({ id: item, name: item }))
- : [],
- attributeModel: 'customAttributes',
- }));
+export const buildAttributesFilterTypes = (
+ attributes,
+ getOperatorTypes,
+ filterModel = 'conversation'
+) => {
+ const standardAttributes = Object.values(
+ filterModel === 'conversation'
+ ? CONVERSATION_ATTRIBUTES
+ : CONTACT_ATTRIBUTES
+ );
+
+ return attributes
+ .filter(attr => !standardAttributes.includes(attr.attributeKey))
+ .map(attr => ({
+ attributeKey: attr.attributeKey,
+ value: attr.attributeKey,
+ attributeName: attr.attributeDisplayName,
+ label: attr.attributeDisplayName,
+ inputType: getCustomAttributeInputType(attr.attributeDisplayType),
+ filterOperators: getOperatorTypes(attr.attributeDisplayType),
+ options:
+ attr.attributeDisplayType === 'list'
+ ? attr.attributeValues.map(item => ({ id: item, name: item }))
+ : [],
+ attributeModel: 'customAttributes',
+ }));
};
/**
diff --git a/app/javascript/dashboard/components-next/filter/provider.js b/app/javascript/dashboard/components-next/filter/provider.js
index bb775a663..f6d078d76 100644
--- a/app/javascript/dashboard/components-next/filter/provider.js
+++ b/app/javascript/dashboard/components-next/filter/provider.js
@@ -3,7 +3,10 @@ import { useI18n } from 'vue-i18n';
import { useOperators } from './operators';
import { useMapGetter } from 'dashboard/composables/store.js';
import { useChannelIcon } from 'next/icon/provider';
-import { buildAttributesFilterTypes } from './helper/filterHelper';
+import {
+ buildAttributesFilterTypes,
+ CONVERSATION_ATTRIBUTES,
+} from './helper/filterHelper';
import countries from 'shared/constants/countries.js';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
@@ -70,7 +73,11 @@ export function useConversationFilterContext() {
* @type {import('vue').ComputedRef}
*/
const customFilterTypes = computed(() =>
- buildAttributesFilterTypes(conversationAttributes.value, getOperatorTypes)
+ buildAttributesFilterTypes(
+ conversationAttributes.value,
+ getOperatorTypes,
+ 'conversation'
+ )
);
/**
@@ -78,8 +85,8 @@ export function useConversationFilterContext() {
*/
const filterTypes = computed(() => [
{
- attributeKey: 'status',
- value: 'status',
+ attributeKey: CONVERSATION_ATTRIBUTES.STATUS,
+ value: CONVERSATION_ATTRIBUTES.STATUS,
attributeName: t('FILTER.ATTRIBUTES.STATUS'),
label: t('FILTER.ATTRIBUTES.STATUS'),
inputType: 'multiSelect',
@@ -94,8 +101,24 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'assignee_id',
- value: 'assignee_id',
+ attributeKey: CONVERSATION_ATTRIBUTES.PRIORITY,
+ value: CONVERSATION_ATTRIBUTES.PRIORITY,
+ attributeName: t('FILTER.ATTRIBUTES.PRIORITY'),
+ label: t('FILTER.ATTRIBUTES.PRIORITY'),
+ inputType: 'multiSelect',
+ options: ['low', 'medium', 'high', 'urgent'].map(id => {
+ return {
+ id,
+ name: t(`CONVERSATION.PRIORITY.OPTIONS.${id.toUpperCase()}`),
+ };
+ }),
+ dataType: 'text',
+ filterOperators: equalityOperators.value,
+ attributeModel: 'standard',
+ },
+ {
+ attributeKey: CONVERSATION_ATTRIBUTES.ASSIGNEE_ID,
+ value: CONVERSATION_ATTRIBUTES.ASSIGNEE_ID,
attributeName: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'),
label: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'),
inputType: 'searchSelect',
@@ -110,8 +133,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'inbox_id',
- value: 'inbox_id',
+ attributeKey: CONVERSATION_ATTRIBUTES.INBOX_ID,
+ value: CONVERSATION_ATTRIBUTES.INBOX_ID,
attributeName: t('FILTER.ATTRIBUTES.INBOX_NAME'),
label: t('FILTER.ATTRIBUTES.INBOX_NAME'),
inputType: 'searchSelect',
@@ -126,8 +149,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'team_id',
- value: 'team_id',
+ attributeKey: CONVERSATION_ATTRIBUTES.TEAM_ID,
+ value: CONVERSATION_ATTRIBUTES.TEAM_ID,
attributeName: t('FILTER.ATTRIBUTES.TEAM_NAME'),
label: t('FILTER.ATTRIBUTES.TEAM_NAME'),
inputType: 'searchSelect',
@@ -137,8 +160,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'display_id',
- value: 'display_id',
+ attributeKey: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
+ value: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
attributeName: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'),
label: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'),
inputType: 'plainText',
@@ -147,8 +170,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'campaign_id',
- value: 'campaign_id',
+ attributeKey: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID,
+ value: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID,
attributeName: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'),
label: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'),
inputType: 'searchSelect',
@@ -161,8 +184,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'labels',
- value: 'labels',
+ attributeKey: CONVERSATION_ATTRIBUTES.LABELS,
+ value: CONVERSATION_ATTRIBUTES.LABELS,
attributeName: t('FILTER.ATTRIBUTES.LABELS'),
label: t('FILTER.ATTRIBUTES.LABELS'),
inputType: 'multiSelect',
@@ -185,8 +208,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'browser_language',
- value: 'browser_language',
+ attributeKey: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE,
+ value: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE,
attributeName: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'),
label: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'),
inputType: 'searchSelect',
@@ -196,8 +219,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional',
},
{
- attributeKey: 'country_code',
- value: 'country_code',
+ attributeKey: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
+ value: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
inputType: 'searchSelect',
@@ -207,8 +230,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional',
},
{
- attributeKey: 'referer',
- value: 'referer',
+ attributeKey: CONVERSATION_ATTRIBUTES.REFERER,
+ value: CONVERSATION_ATTRIBUTES.REFERER,
attributeName: t('FILTER.ATTRIBUTES.REFERER_LINK'),
label: t('FILTER.ATTRIBUTES.REFERER_LINK'),
inputType: 'plainText',
@@ -217,8 +240,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional',
},
{
- attributeKey: 'created_at',
- value: 'created_at',
+ attributeKey: CONVERSATION_ATTRIBUTES.CREATED_AT,
+ value: CONVERSATION_ATTRIBUTES.CREATED_AT,
attributeName: t('FILTER.ATTRIBUTES.CREATED_AT'),
label: t('FILTER.ATTRIBUTES.CREATED_AT'),
inputType: 'date',
@@ -227,8 +250,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'last_activity_at',
- value: 'last_activity_at',
+ attributeKey: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT,
+ value: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT,
attributeName: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'),
label: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'),
inputType: 'date',
diff --git a/app/javascript/dashboard/components-next/icon/FileIcon.story.vue b/app/javascript/dashboard/components-next/icon/FileIcon.story.vue
new file mode 100644
index 000000000..3a831b96f
--- /dev/null
+++ b/app/javascript/dashboard/components-next/icon/FileIcon.story.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+ {{ file.name }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/icon/FileIcon.vue b/app/javascript/dashboard/components-next/icon/FileIcon.vue
new file mode 100644
index 000000000..8dd9e7ce1
--- /dev/null
+++ b/app/javascript/dashboard/components-next/icon/FileIcon.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/icon/Logo.vue b/app/javascript/dashboard/components-next/icon/Logo.vue
index 2330021c3..6dd0bbd8f 100644
--- a/app/javascript/dashboard/components-next/icon/Logo.vue
+++ b/app/javascript/dashboard/components-next/icon/Logo.vue
@@ -1,6 +1,21 @@
+
+
+
+ +
{{ currentContactName }} @@ -114,11 +119,12 @@ const onCardClick = e => {
@@ -75,7 +75,7 @@ onMounted(() => {
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleHeaderControls.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleHeaderControls.vue
index 5e7feb9fa..6565fab82 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleHeaderControls.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleHeaderControls.vue
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { OnClickOutside } from '@vueuse/components';
+import { useUISettings } from 'dashboard/composables/useUISettings';
import {
ARTICLE_TABS,
CATEGORY_ALL,
@@ -37,6 +38,7 @@ const emit = defineEmits([
const route = useRoute();
const { t } = useI18n();
+const { updateUISettings } = useUISettings();
const isCategoryMenuOpen = ref(false);
const isLocaleMenuOpen = ref(false);
@@ -111,13 +113,12 @@ const localeMenuItems = computed(() => {
}));
});
-const hasMoreThanOneLocaleMenuItems = computed(() => {
- return localeMenuItems.value?.length > 1;
-});
-
const handleLocaleAction = ({ value }) => {
emit('localeChange', value);
isLocaleMenuOpen.value = false;
+ updateUISettings({
+ last_active_locale_code: value,
+ });
};
const handleCategoryAction = ({ value }) => {
@@ -143,7 +144,7 @@ const handleTabChange = value => {
/>
-
+
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalSettings.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalSettings.vue
index ccf1ad997..a49493818 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalSettings.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalSettings.vue
@@ -74,7 +74,7 @@ const handleDeletePortal = () => {
t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.ERROR'),
+ required
+ ),
+ isValidSlug: helpers.withMessage(
+ () => t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.FORMAT_ERROR'),
+ isValidSlug
+ ),
+ },
};
const v$ = useVuelidate(rules, state);
@@ -40,9 +50,9 @@ const nameError = computed(() =>
v$.value.name.$error ? t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.ERROR') : ''
);
-const slugError = computed(() =>
- v$.value.slug.$error ? t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.ERROR') : ''
-);
+const slugError = computed(() => {
+ return v$.value.slug.$errors[0]?.$message || '';
+});
const isSubmitDisabled = computed(() => v$.value.$invalid);
@@ -131,6 +141,7 @@ defineExpose({ dialogRef });
:message="
nameError || t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.MESSAGE')
"
+ @blur="v$.name.$touch()"
/>
diff --git a/app/javascript/dashboard/components-next/HelpCenter/PortalSwitcher/PortalSwitcher.vue b/app/javascript/dashboard/components-next/HelpCenter/PortalSwitcher/PortalSwitcher.vue
index 1b4668b91..5a835f8cb 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/PortalSwitcher/PortalSwitcher.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/PortalSwitcher/PortalSwitcher.vue
@@ -92,7 +92,7 @@ const redirectToPortalHomePage = () => {
+import { computed, ref, onBeforeMount } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { getInboxIconByType } from 'dashboard/helper/inbox';
+import { dynamicTime, shortTimestamp } from 'shared/helpers/timeHelper';
+import {
+ snoozedReopenTimeToTimestamp,
+ shortenSnoozeTime,
+} from 'dashboard/helper/snoozeHelpers';
+import { NOTIFICATION_TYPES_MAPPING } from 'dashboard/routes/dashboard/inbox/helpers/InboxViewHelpers';
+
+import Icon from 'dashboard/components-next/icon/Icon.vue';
+import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
+import CardPriorityIcon from 'dashboard/components-next/Conversation/ConversationCard/CardPriorityIcon.vue';
+import SLACardLabel from 'dashboard/components-next/Conversation/ConversationCard/SLACardLabel.vue';
+import InboxContextMenu from 'dashboard/routes/dashboard/inbox/components/InboxContextMenu.vue';
+
+const props = defineProps({
+ inboxItem: { type: Object, default: () => ({}) },
+ stateInbox: { type: Object, default: () => ({}) },
+});
+
+const emit = defineEmits([
+ 'click',
+ 'contextMenuOpen',
+ 'contextMenuClose',
+ 'markNotificationAsRead',
+ 'markNotificationAsUnRead',
+ 'deleteNotification',
+]);
+
+const { t } = useI18n();
+
+const isContextMenuOpen = ref(false);
+const contextMenuPosition = ref({ x: null, y: null });
+const slaCardLabel = ref(null);
+
+const getMessageClasses = {
+ emphasis: 'text-sm font-medium text-n-slate-11',
+ emphasisUnread: 'text-sm font-medium text-n-slate-12',
+ normal: 'text-sm font-normal text-n-slate-11',
+ normalUnread: 'text-sm text-n-slate-12',
+};
+
+const primaryActor = computed(() => props.inboxItem?.primaryActor);
+const meta = computed(() => primaryActor.value?.meta);
+const assigneeMeta = computed(() => meta.value?.sender);
+const isUnread = computed(() => !props.inboxItem?.readAt);
+const inbox = computed(() => props.stateInbox);
+
+const inboxIcon = computed(() => {
+ const { phoneNumber, channelType } = inbox.value;
+ return getInboxIconByType(channelType, phoneNumber);
+});
+
+const hasSlaThreshold = computed(() => {
+ return slaCardLabel.value?.hasSlaThreshold && primaryActor.value?.slaPolicyId;
+});
+
+const lastActivityAt = computed(() => {
+ const timestamp = props.inboxItem?.lastActivityAt;
+ return timestamp ? shortTimestamp(dynamicTime(timestamp)) : '';
+});
+
+const menuItems = computed(() => [
+ { key: 'delete', label: t('INBOX.MENU_ITEM.DELETE') },
+ {
+ key: isUnread.value ? 'mark_as_read' : 'mark_as_unread',
+ label: t(`INBOX.MENU_ITEM.MARK_AS_${isUnread.value ? 'READ' : 'UNREAD'}`),
+ },
+]);
+
+const messageClasses = computed(() => ({
+ emphasis: isUnread.value
+ ? getMessageClasses.emphasisUnread
+ : getMessageClasses.emphasis,
+ normal: isUnread.value
+ ? getMessageClasses.normalUnread
+ : getMessageClasses.normal,
+}));
+
+const formatPushMessage = message => {
+ if (message.startsWith(': ')) {
+ return message.slice(2);
+ }
+
+ return message.replace(/^([^:]+):/g, (match, name) => {
+ return ``;
+ });
+};
+
+const formattedMessage = computed(() => {
+ const messageContent = ``;
+
+ return isUnread.value
+ ? ` ${messageContent}`
+ : messageContent;
+});
+
+const notificationDetails = computed(() => {
+ const type = props.inboxItem?.notificationType?.toUpperCase() || '';
+ const [icon = '', color = 'text-n-blue-text'] =
+ NOTIFICATION_TYPES_MAPPING[type] || [];
+ return { text: type ? t(`INBOX.TYPES_NEXT.${type}`) : '', icon, color };
+});
+
+const snoozedUntilTime = computed(() => {
+ const { snoozedUntil } = props.inboxItem;
+ if (!snoozedUntil) return null;
+ return shortenSnoozeTime(
+ dynamicTime(snoozedReopenTimeToTimestamp(snoozedUntil))
+ );
+});
+
+const hasLastSnoozed = computed(() => props.inboxItem?.meta?.lastSnoozedAt);
+
+const snoozedText = computed(() => {
+ return !hasLastSnoozed.value
+ ? t('INBOX.TYPES_NEXT.SNOOZED_UNTIL', {
+ time: shortTimestamp(snoozedUntilTime.value),
+ })
+ : t('INBOX.TYPES_NEXT.SNOOZED_ENDS');
+});
+
+const contextMenuActions = {
+ close: () => {
+ isContextMenuOpen.value = false;
+ contextMenuPosition.value = { x: null, y: null };
+ emit('contextMenuClose');
+ },
+ open: e => {
+ e.preventDefault();
+ contextMenuPosition.value = {
+ x: e.pageX || e.clientX,
+ y: e.pageY || e.clientY,
+ };
+ isContextMenuOpen.value = true;
+ emit('contextMenuOpen');
+ },
+ handle: key => {
+ const actions = {
+ mark_as_read: () => emit('markNotificationAsRead', props.inboxItem),
+ mark_as_unread: () => emit('markNotificationAsUnRead', props.inboxItem),
+ delete: () => emit('deleteNotification', props.inboxItem),
+ };
+ actions[key]?.();
+ },
+};
+
+onBeforeMount(contextMenuActions.close);
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ snoozedText }}
+
+
+
+
+
+ {{ notificationDetails.text }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ lastActivityAt }}
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/Label/AddLabel.vue b/app/javascript/dashboard/components-next/Label/AddLabel.vue
index c9571e771..568ed6879 100644
--- a/app/javascript/dashboard/components-next/Label/AddLabel.vue
+++ b/app/javascript/dashboard/components-next/Label/AddLabel.vue
@@ -21,7 +21,7 @@ const showDropdown = ref(false);
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue
index 89784037d..90cb67c4f 100644
--- a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue
@@ -1,6 +1,5 @@
@@ -99,7 +113,8 @@ const errorClass = computed(() => {
{{
@@ -109,6 +124,7 @@ const errorClass = computed(() => {
}}
{
:is-loading="isLoading"
:disabled="contactableInboxesList?.length > 0 && showInboxesDropdown"
allow-create
- type="email"
+ :type="inputType"
class="flex-1 min-h-7"
:class="errorClass"
focus-on-mount
- @input="emit('searchContacts', $event)"
+ @input="handleInput"
@on-click-outside="emit('updateDropdown', 'contacts', false)"
@add="emit('setSelectedContact', $event)"
@remove="emit('clearSelectedContact')"
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue b/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue
index 4e1b0f510..7794d5bdd 100644
--- a/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue
@@ -52,7 +52,7 @@ const targetInboxLabel = computed(() => {
{{ targetInboxLabel }}
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/MessageEditor.vue b/app/javascript/dashboard/components-next/NewConversation/components/MessageEditor.vue
index ca54f7eca..01b2e4a8d 100644
--- a/app/javascript/dashboard/components-next/NewConversation/components/MessageEditor.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/components/MessageEditor.vue
@@ -1,22 +1,22 @@
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue b/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue
index 8d32509b8..3980c6b30 100644
--- a/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue
@@ -84,7 +84,7 @@ const handleSendMessage = template => {
/>
@@ -96,7 +96,7 @@ const handleSendMessage = template => {
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.SEARCH_PLACEHOLDER'
)
"
- class="w-full h-8 py-2 pl-10 pr-2 text-sm border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
+ class="w-full h-8 py-2 pl-10 pr-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
/>
{
{{
diff --git a/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js b/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js
index 12a5e76b3..57e819245 100644
--- a/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js
+++ b/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js
@@ -87,6 +87,21 @@ export const processContactableInboxes = inboxes => {
}));
};
+export const mergeInboxDetails = (inboxesData, inboxesList = []) => {
+ if (!inboxesData || !inboxesData.length) {
+ return [];
+ }
+
+ return inboxesData.map(inboxData => {
+ const matchingInbox =
+ inboxesList.find(inbox => inbox.id === inboxData.id) || {};
+ return {
+ ...camelcaseKeys(matchingInbox, { deep: true }),
+ ...inboxData,
+ };
+ });
+};
+
export const prepareAttachmentPayload = (
attachedFiles,
directUploadsEnabled
@@ -193,10 +208,12 @@ export const searchContacts = async ({ keys, query }) => {
return filteredPayload || [];
};
-export const createNewContact = async email => {
+export const createNewContact = async input => {
const payload = {
- name: getCapitalizedNameFromEmail(email),
- email,
+ name: input.startsWith('+')
+ ? input.slice(1) // Remove the '+' prefix if it exists
+ : getCapitalizedNameFromEmail(input),
+ ...(input.startsWith('+') ? { phone_number: input } : { email: input }),
};
const {
diff --git a/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js b/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js
index 5a2d0092d..3de1fad0d 100644
--- a/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js
+++ b/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js
@@ -110,6 +110,153 @@ describe('composeConversationHelper', () => {
});
});
+ describe('mergeInboxDetails', () => {
+ it('returns empty array if inboxesData is empty or null', () => {
+ expect(helpers.mergeInboxDetails(null)).toEqual([]);
+ expect(helpers.mergeInboxDetails([])).toEqual([]);
+ expect(helpers.mergeInboxDetails(undefined)).toEqual([]);
+ });
+
+ it('merges inbox data with matching inboxes from the list', () => {
+ const inboxesData = [
+ { id: 1, sourceId: 'source1' },
+ { id: 2, sourceId: 'source2' },
+ ];
+
+ const inboxesList = [
+ {
+ id: 1,
+ name: 'Inbox 1',
+ channel_type: 'Channel::Email',
+ channel_id: 10,
+ phone_number: null,
+ },
+ {
+ id: 2,
+ name: 'Inbox 2',
+ channel_type: 'Channel::Whatsapp',
+ channel_id: 20,
+ phone_number: '+1234567890',
+ },
+ {
+ id: 3,
+ name: 'Inbox 3',
+ channel_type: 'Channel::Api',
+ channel_id: 30,
+ phone_number: null,
+ },
+ ];
+
+ const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
+
+ expect(result.length).toBe(2);
+ expect(result[0]).toMatchObject({
+ id: 1,
+ sourceId: 'source1',
+ name: 'Inbox 1',
+ channelType: 'Channel::Email',
+ channelId: 10,
+ phoneNumber: null,
+ });
+
+ expect(result[1]).toMatchObject({
+ id: 2,
+ sourceId: 'source2',
+ name: 'Inbox 2',
+ channelType: 'Channel::Whatsapp',
+ channelId: 20,
+ phoneNumber: '+1234567890',
+ });
+ });
+
+ it('handles inboxes not found in the list', () => {
+ const inboxesData = [
+ { id: 1, sourceId: 'source1' },
+ { id: 99, sourceId: 'source99' }, // This doesn't exist in inboxesList
+ ];
+
+ const inboxesList = [
+ {
+ id: 1,
+ name: 'Inbox 1',
+ channel_type: 'Channel::Email',
+ },
+ ];
+
+ const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
+
+ expect(result.length).toBe(2);
+
+ expect(result[0]).toMatchObject({
+ id: 1,
+ sourceId: 'source1',
+ name: 'Inbox 1',
+ channelType: 'Channel::Email',
+ });
+
+ expect(result[1]).toMatchObject({
+ id: 99,
+ sourceId: 'source99',
+ });
+
+ expect(result[1].name).toBeUndefined();
+ expect(result[1].channelType).toBeUndefined();
+ });
+
+ it('camelcases properties from inboxesList', () => {
+ const inboxesData = [{ id: 1, sourceId: 'source1' }];
+
+ const inboxesList = [
+ {
+ id: 1,
+ name: 'Inbox 1',
+ channel_type: 'Channel::Email',
+ avatar_url: 'https://example.com/avatar.png',
+ working_hours: [
+ {
+ day_of_week: 1,
+ closed_all_day: false,
+ },
+ ],
+ },
+ ];
+
+ const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
+
+ expect(result[0]).toMatchObject({
+ id: 1,
+ sourceId: 'source1',
+ name: 'Inbox 1',
+ channelType: 'Channel::Email',
+ avatarUrl: 'https://example.com/avatar.png',
+ });
+
+ expect(result[0].workingHours[0]).toMatchObject({
+ dayOfWeek: 1,
+ closedAllDay: false,
+ });
+ });
+
+ it('preserves original properties when they conflict with inboxesList', () => {
+ const inboxesData = [
+ { id: 1, sourceId: 'source1', name: 'Original Name' },
+ ];
+
+ const inboxesList = [
+ {
+ id: 1,
+ name: 'List Name',
+ channel_type: 'Channel::Email',
+ },
+ ];
+
+ const result = helpers.mergeInboxDetails(inboxesData, inboxesList);
+
+ expect(result[0].name).toBe('Original Name');
+ expect(result[0].channelType).toBe('Channel::Email');
+ });
+ });
+
describe('prepareAttachmentPayload', () => {
it('prepares direct upload files', () => {
const files = [{ blobSignedId: 'signed1' }];
@@ -429,6 +576,28 @@ describe('composeConversationHelper', () => {
email: 'john@example.com',
});
});
+
+ it('creates new contact with phone number', async () => {
+ const mockContact = {
+ id: 1,
+ name: '919999999999',
+ phone_number: '+919999999999',
+ };
+ ContactAPI.create.mockResolvedValue({
+ data: { payload: { contact: mockContact } },
+ });
+
+ const result = await helpers.createNewContact('+919999999999');
+ expect(result).toEqual({
+ id: 1,
+ name: '919999999999',
+ phoneNumber: '+919999999999',
+ });
+ expect(ContactAPI.create).toHaveBeenCalledWith({
+ name: '919999999999',
+ phone_number: '+919999999999',
+ });
+ });
});
describe('fetchContactableInboxes', () => {
diff --git a/app/javascript/dashboard/components-next/avatar/Avatar.vue b/app/javascript/dashboard/components-next/avatar/Avatar.vue
index fb23b3cff..65859b1c7 100644
--- a/app/javascript/dashboard/components-next/avatar/Avatar.vue
+++ b/app/javascript/dashboard/components-next/avatar/Avatar.vue
@@ -174,7 +174,7 @@ watch(
-
+
+
+
+
+
+
+
+
+
+
+
+ {{ actionLabel }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/button/Button.vue b/app/javascript/dashboard/components-next/button/Button.vue
index fecdbaf9b..c54bd395a 100644
--- a/app/javascript/dashboard/components-next/button/Button.vue
+++ b/app/javascript/dashboard/components-next/button/Button.vue
@@ -6,6 +6,7 @@ import Icon from 'dashboard/components-next/icon/Icon.vue';
import {
VARIANT_OPTIONS,
COLOR_OPTIONS,
+ JUSTIFY_OPTIONS,
SIZE_OPTIONS,
EXCLUDED_ATTRS,
} from './constants.js';
@@ -27,6 +28,11 @@ const props = defineProps({
default: null,
validator: value => SIZE_OPTIONS.includes(value) || value === null,
},
+ justify: {
+ type: String,
+ default: null,
+ validator: value => JUSTIFY_OPTIONS.includes(value) || value === null,
+ },
icon: { type: [String, Object, Function], default: '' },
trailingIcon: { type: Boolean, default: false },
isLoading: { type: Boolean, default: false },
@@ -81,48 +87,70 @@ const computedSize = computed(() => {
return 'md';
});
+const computedJustify = computed(() => {
+ if (props.justify) return props.justify;
+ if (attrs.start || attrs.start === '') return 'start';
+ if (attrs.center || attrs.center === '') return 'center';
+ if (attrs.end || attrs.end === '') return 'end';
+
+ return 'center';
+});
+
const STYLE_CONFIG = {
colors: {
blue: {
- solid: 'bg-n-brand text-white hover:brightness-110 outline-transparent',
+ solid:
+ 'bg-n-brand text-white hover:enabled:brightness-110 focus-visible:brightness-110 outline-transparent',
faded:
- 'bg-n-brand/10 text-n-blue-text hover:bg-n-brand/20 outline-transparent',
- outline: 'text-n-blue-text outline-n-blue-border',
- ghost: 'text-n-blue-text hover:bg-n-alpha-2 outline-transparent',
- link: 'text-n-blue-text hover:underline outline-transparent',
+ 'bg-n-brand/10 text-n-blue-text hover:enabled:bg-n-brand/20 focus-visible:bg-n-brand/20 outline-transparent',
+ outline: 'text-n-blue-text outline-n-brand',
+ ghost:
+ 'text-n-blue-text hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
+ link: 'text-n-blue-text hover:enabled:underline focus-visible:underline outline-transparent',
},
ruby: {
- solid: 'bg-n-ruby-9 text-white hover:bg-n-ruby-10 outline-transparent',
+ solid:
+ 'bg-n-ruby-9 text-white hover:enabled:bg-n-ruby-10 focus-visible:bg-n-ruby-10 outline-transparent',
faded:
- 'bg-n-ruby-9/10 text-n-ruby-11 hover:bg-n-ruby-9/20 outline-transparent',
- outline: 'text-n-ruby-11 hover:bg-n-ruby-9/10 outline-n-ruby-8',
- ghost: 'text-n-ruby-11 hover:bg-n-alpha-2 outline-transparent',
- link: 'text-n-ruby-9 hover:underline outline-transparent',
+ 'bg-n-ruby-9/10 text-n-ruby-11 hover:enabled:bg-n-ruby-9/20 focus-visible:bg-n-ruby-9/20 outline-transparent',
+ outline:
+ 'text-n-ruby-11 hover:enabled:bg-n-ruby-9/10 focus-visible:bg-n-ruby-9/10 outline-n-ruby-8',
+ ghost:
+ 'text-n-ruby-11 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
+ link: 'text-n-ruby-9 hover:enabled:underline focus-visible:underline outline-transparent',
},
amber: {
- solid: 'bg-n-amber-9 text-white hover:bg-n-amber-10 outline-transparent',
+ solid:
+ 'bg-n-amber-9 text-white hover:enabled:bg-n-amber-10 focus-visible:bg-n-amber-10 outline-transparent',
faded:
- 'bg-n-amber-9/10 text-n-slate-12 hover:bg-n-amber-9/20 outline-transparent',
- outline: 'text-n-amber-11 hover:bg-n-amber-9/10 outline-n-amber-9',
- link: 'text-n-amber-9 hover:underline outline-transparent',
- ghost: 'text-n-amber-9 hover:bg-n-alpha-2 outline-transparent',
+ 'bg-n-amber-9/10 text-n-slate-12 hover:enabled:bg-n-amber-9/20 focus-visible:bg-n-amber-9/20 outline-transparent',
+ outline:
+ 'text-n-amber-11 hover:enabled:bg-n-amber-9/10 focus-visible:bg-n-amber-9/10 outline-n-amber-9',
+ link: 'text-n-amber-9 hover:enabled:underline focus-visible:underline outline-transparent',
+ ghost:
+ 'text-n-amber-9 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
},
slate: {
solid:
- 'bg-n-solid-3 dark:hover:bg-n-solid-2 hover:bg-n-alpha-2 text-n-slate-12 outline-n-container',
+ 'bg-n-solid-3 dark:hover:enabled:bg-n-solid-2 dark:focus-visible:bg-n-solid-2 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 text-n-slate-12 outline-n-container',
faded:
- 'bg-n-slate-9/10 text-n-slate-12 hover:bg-n-slate-9/20 outline-transparent',
- outline: 'text-n-slate-11 outline-n-strong hover:bg-n-slate-9/10',
- link: 'text-n-slate-11 hover:text-n-slate-12 hover:underline outline-transparent',
- ghost: 'text-n-slate-12 hover:bg-n-alpha-2 outline-transparent',
+ 'bg-n-slate-9/10 text-n-slate-12 hover:enabled:bg-n-slate-9/20 focus-visible:bg-n-slate-9/20 outline-transparent',
+ outline:
+ 'text-n-slate-11 outline-n-strong hover:enabled:bg-n-slate-9/10 focus-visible:bg-n-slate-9/10',
+ link: 'text-n-slate-11 hover:enabled:text-n-slate-12 focus-visible:text-n-slate-12 hover:enabled:underline focus-visible:underline outline-transparent',
+ ghost:
+ 'text-n-slate-12 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
},
teal: {
- solid: 'bg-n-teal-9 text-white hover:bg-n-teal-10 outline-transparent',
+ solid:
+ 'bg-n-teal-9 text-white hover:enabled:bg-n-teal-10 focus-visible:bg-n-teal-10 outline-transparent',
faded:
- 'bg-n-teal-9/10 text-n-slate-12 hover:bg-n-teal-9/20 outline-transparent',
- outline: 'text-n-teal-11 hover:bg-n-teal-9/10 outline-n-teal-9',
- link: 'text-n-teal-9 hover:underline outline-transparent',
- ghost: 'text-n-teal-9 hover:bg-n-alpha-2 outline-transparent',
+ 'bg-n-teal-9/10 text-n-slate-12 hover:enabled:bg-n-teal-9/20 focus-visible:bg-n-teal-9/20 outline-transparent',
+ outline:
+ 'text-n-teal-11 hover:enabled:bg-n-teal-9/10 focus-visible:bg-n-teal-9/10 outline-n-teal-9',
+ link: 'text-n-teal-9 hover:enabled:underline focus-visible:underline outline-transparent',
+ ghost:
+ 'text-n-teal-9 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
},
},
sizes: {
@@ -151,13 +179,18 @@ const STYLE_CONFIG = {
md: 'text-sm font-medium',
lg: 'text-base',
},
- base: 'inline-flex items-center justify-center min-w-0 gap-2 transition-all duration-200 ease-in-out border-0 rounded-lg outline-1 outline disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50',
+ justify: {
+ start: 'justify-start',
+ center: 'justify-center',
+ end: 'justify-end',
+ },
+ base: 'inline-flex items-center min-w-0 gap-2 transition-all duration-200 ease-in-out border-0 rounded-lg outline-1 outline disabled:opacity-50',
};
const variantClasses = computed(() => {
const variantMap = {
ghost: `${STYLE_CONFIG.colors[computedColor.value].ghost}`,
- link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-4`,
+ link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-2`,
outline: STYLE_CONFIG.colors[computedColor.value].outline,
faded: STYLE_CONFIG.colors[computedColor.value].faded,
solid: STYLE_CONFIG.colors[computedColor.value].solid,
@@ -197,6 +230,7 @@ const linkButtonClasses = computed(() => {
[STYLE_CONFIG.base]: true,
[isLink ? linkButtonClasses : buttonClasses]: true,
[STYLE_CONFIG.fontSize[computedSize]]: true,
+ [STYLE_CONFIG.justify[computedJustify]]: true,
'flex-row-reverse': trailingIcon && !isIconOnly,
}"
>
diff --git a/app/javascript/dashboard/components-next/button/constants.js b/app/javascript/dashboard/components-next/button/constants.js
index e65299420..d463abdd3 100644
--- a/app/javascript/dashboard/components-next/button/constants.js
+++ b/app/javascript/dashboard/components-next/button/constants.js
@@ -1,6 +1,7 @@
export const VARIANT_OPTIONS = ['solid', 'outline', 'faded', 'link', 'ghost'];
export const COLOR_OPTIONS = ['blue', 'ruby', 'amber', 'slate', 'teal'];
export const SIZE_OPTIONS = ['xs', 'sm', 'md', 'lg'];
+export const JUSTIFY_OPTIONS = ['start', 'center', 'end'];
export const EXCLUDED_ATTRS = [
'variant',
@@ -12,4 +13,5 @@ export const EXCLUDED_ATTRS = [
...VARIANT_OPTIONS,
...COLOR_OPTIONS,
...SIZE_OPTIONS,
+ ...JUSTIFY_OPTIONS,
];
diff --git a/app/javascript/dashboard/components-next/captain/PageLayout.vue b/app/javascript/dashboard/components-next/captain/PageLayout.vue
new file mode 100644
index 000000000..7355ac616
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/PageLayout.vue
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ headerTitle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.story.vue
new file mode 100644
index 000000000..b388d067e
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.story.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.vue b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.vue
new file mode 100644
index 000000000..222baaecd
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+ {{ name }}
+
+
+
+
+
+
+
+
+
+
+ {{ description || 'Description not available' }}
+
+
+ {{ lastUpdatedAt }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/AssistantPlayground.vue b/app/javascript/dashboard/components-next/captain/assistant/AssistantPlayground.vue
new file mode 100644
index 000000000..28587745a
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/AssistantPlayground.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+ {{ t('CAPTAIN.PLAYGROUND.HEADER') }}
+
+
+
+
+ {{ t('CAPTAIN.PLAYGROUND.DESCRIPTION') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('CAPTAIN.PLAYGROUND.CREDIT_NOTE') }}
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.story.vue
new file mode 100644
index 000000000..df1ada6cc
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.story.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue
new file mode 100644
index 000000000..d6e546581
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+ {{ name }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ assistant?.name || '' }}
+
+
+
+ {{ externalLink }}
+
+
+ {{ createdAt }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/InboxCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.story.vue
new file mode 100644
index 000000000..2fd0e9756
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.story.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue
new file mode 100644
index 000000000..30fe85b0a
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+ {{ inboxName }}
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/MessageList.vue b/app/javascript/dashboard/components-next/captain/assistant/MessageList.vue
new file mode 100644
index 000000000..1d6529a45
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/MessageList.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.story.vue
new file mode 100644
index 000000000..1a281b799
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.story.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue
new file mode 100644
index 000000000..f00354105
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+
+
+
+ {{ question }}
+
+
+
+
+
+
+
+
+
+ {{ answer }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/AssistantSelector.vue b/app/javascript/dashboard/components-next/captain/pageComponents/AssistantSelector.vue
new file mode 100644
index 000000000..4998e4044
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/AssistantSelector.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue
new file mode 100644
index 000000000..82f8fa308
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue
new file mode 100644
index 000000000..31e18394f
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/Paywall.vue b/app/javascript/dashboard/components-next/captain/pageComponents/Paywall.vue
new file mode 100644
index 000000000..b9deb0ff0
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/Paywall.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue
new file mode 100644
index 000000000..3c8afec07
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue
@@ -0,0 +1,174 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue
new file mode 100644
index 000000000..ac67063ec
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue
new file mode 100644
index 000000000..3a668a757
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue
@@ -0,0 +1,306 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/document/CreateDocumentDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/document/CreateDocumentDialog.vue
new file mode 100644
index 000000000..7db74f523
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/document/CreateDocumentDialog.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/document/DocumentForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/document/DocumentForm.vue
new file mode 100644
index 000000000..2a2730e46
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/document/DocumentForm.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/document/LimitBanner.vue b/app/javascript/dashboard/components-next/captain/pageComponents/document/LimitBanner.vue
new file mode 100644
index 000000000..d4f512f39
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/document/LimitBanner.vue
@@ -0,0 +1,40 @@
+
+
+
+
+ {{ $t('CAPTAIN.BANNER.DOCUMENTS') }}
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/document/RelatedResponses.vue b/app/javascript/dashboard/components-next/captain/pageComponents/document/RelatedResponses.vue
new file mode 100644
index 000000000..6e00eda9b
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/document/RelatedResponses.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue
new file mode 100644
index 000000000..365b78222
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/DocumentPageEmptyState.vue b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/DocumentPageEmptyState.vue
new file mode 100644
index 000000000..39182587e
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/DocumentPageEmptyState.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/InboxPageEmptyState.vue b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/InboxPageEmptyState.vue
new file mode 100644
index 000000000..2a0735930
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/InboxPageEmptyState.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue
new file mode 100644
index 000000000..7e95a4a51
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js
new file mode 100644
index 000000000..af0331430
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js
@@ -0,0 +1,286 @@
+import { INBOX_TYPES } from 'dashboard/helper/inbox';
+
+export const assistantsList = [
+ {
+ account_id: 2,
+ config: { product_name: 'HelpDesk Pro' },
+ created_at: 1736033561,
+ description:
+ 'An advanced AI assistant designed to enhance customer support solutions by automating workflows and providing instant responses.',
+ id: 4,
+ name: 'Support Genie',
+ },
+ {
+ account_id: 3,
+ config: { product_name: 'CRM Tools' },
+ created_at: 1736033562,
+ description:
+ 'Helps streamline customer relationship management by organizing contacts, automating follow-ups, and providing insights.',
+ id: 5,
+ name: 'CRM Assistant',
+ },
+ {
+ account_id: 4,
+ config: { product_name: 'SalesFlow' },
+ created_at: 1736033563,
+ description:
+ 'Optimizes your sales pipeline by tracking prospects, forecasting sales, and automating administrative tasks.',
+ id: 6,
+ name: 'SalesBot',
+ },
+ {
+ account_id: 5,
+ config: { product_name: 'TicketMaster AI' },
+ created_at: 1736033564,
+ description:
+ 'Automates ticket assignment, categorization, and customer query responses to enhance support efficiency.',
+ id: 7,
+ name: 'TicketBot',
+ },
+ {
+ account_id: 6,
+ config: { product_name: 'FinanceAssist' },
+ created_at: 1736033565,
+ description:
+ 'Provides financial analytics, reporting, and insights, helping teams make data-driven financial decisions.',
+ id: 8,
+ name: 'Finance Wizard',
+ },
+ {
+ account_id: 8,
+ config: { product_name: 'HR Assistant' },
+ created_at: 1736033567,
+ description:
+ 'Streamlines HR operations including employee management, payroll, and recruitment processes.',
+ id: 10,
+ name: 'HR Helper',
+ },
+];
+
+export const documentsList = [
+ {
+ account_id: 1,
+ assistant: { id: 1, name: 'Helper Pro' },
+ content:
+ 'Comprehensive guide on using conversation filters to manage chats effectively.',
+ created_at: 1736143272,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688192-how-to-use-conversation-filters',
+ id: 3059,
+ name: 'How to use Conversation Filters? | User Guide | Chatwoot',
+ status: 'available',
+ },
+ {
+ account_id: 2,
+ assistant: { id: 2, name: 'Support Genie' },
+ content:
+ 'Step-by-step guide for automating ticket assignments and improving support workflow in Chatwoot.',
+ created_at: 1736143273,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688200-automating-ticket-assignments',
+ id: 3060,
+ name: 'Automating Ticket Assignments | User Guide | Chatwoot',
+ status: 'available',
+ },
+ {
+ account_id: 3,
+ assistant: { id: 3, name: 'CRM Assistant' },
+ content:
+ 'A detailed guide on managing and organizing customer profiles for better relationship management.',
+ created_at: 1736143274,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688210-managing-customer-profiles',
+ id: 3061,
+ name: 'Managing Customer Profiles | User Guide | Chatwoot',
+ status: 'available',
+ },
+ {
+ account_id: 4,
+ assistant: { id: 4, name: 'SalesBot' },
+ content:
+ 'Learn how to optimize sales tracking and improve your sales forecasting using advanced features.',
+ created_at: 1736143275,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688220-sales-tracking-guide',
+ id: 3062,
+ name: 'Sales Tracking Guide | User Guide | Chatwoot',
+ status: 'available',
+ },
+ {
+ account_id: 5,
+ assistant: { id: 5, name: 'TicketBot' },
+ content:
+ 'How to efficiently create, manage, and resolve tickets in Chatwoot.',
+ created_at: 1736143276,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688230-managing-tickets',
+ id: 3063,
+ name: 'Managing Tickets | User Guide | Chatwoot',
+ status: 'available',
+ },
+ {
+ account_id: 6,
+ assistant: { id: 6, name: 'Finance Wizard' },
+ content:
+ 'Detailed guide on how to use financial reporting tools and generate insightful analytics.',
+ created_at: 1736143277,
+ external_link:
+ 'https://www.chatwoot.com/hc/user-guide/articles/1677688240-financial-reporting',
+ id: 3064,
+ name: 'Financial Reporting | User Guide | Chatwoot',
+ status: 'available',
+ },
+];
+
+export const responsesList = [
+ {
+ account_id: 1,
+ answer:
+ 'Messenger may be deactivated because you are on a free plan or the limit for inboxes might have been reached.',
+ created_at: 1736283330,
+ id: 87,
+ question: 'Why is my Messenger in Chatwoot deactivated?',
+ status: 'pending',
+ assistant: {
+ account_id: 1,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033280,
+ description: 'Assists with general queries and system-wide issues.',
+ id: 1,
+ name: 'Assistant 2',
+ },
+ },
+ {
+ account_id: 2,
+ answer:
+ 'You can integrate your WhatsApp account by navigating to the Integrations section and selecting the WhatsApp integration option.',
+ created_at: 1736283340,
+ id: 88,
+ question: 'How do I integrate WhatsApp with Chatwoot?',
+ assistant: {
+ account_id: 2,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033281,
+ description: 'Helps with integration and setup-related inquiries.',
+ id: 2,
+ name: 'Assistant 3',
+ },
+ },
+ {
+ account_id: 3,
+ answer:
+ "To reset your password, go to the login page and click on 'Forgot Password', then follow the instructions sent to your email.",
+ created_at: 1736283350,
+ id: 89,
+ question: 'How can I reset my password in Chatwoot?',
+ assistant: {
+ account_id: 3,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033282,
+ description: 'Handles account management and recovery support.',
+ id: 3,
+ name: 'Assistant 4',
+ },
+ },
+ {
+ account_id: 4,
+ answer:
+ "You can enable the dark mode in settings by navigating to 'Appearance' and selecting 'Dark Mode'.",
+ created_at: 1736283360,
+ id: 90,
+ question: 'How do I enable dark mode in Chatwoot?',
+ assistant: {
+ account_id: 4,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033283,
+ description: 'Helps with UI and theme-related inquiries.',
+ id: 4,
+ name: 'Assistant 5',
+ },
+ },
+ {
+ account_id: 5,
+ answer:
+ "To add a new team member, navigate to 'Settings', then 'Team', and click on 'Add Team Member'.",
+ created_at: 1736283370,
+ id: 91,
+ question: 'How do I add a new team member in Chatwoot?',
+ assistant: {
+ account_id: 5,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033284,
+ description: 'Supports team management and user access-related queries.',
+ id: 5,
+ name: 'Assistant 6',
+ },
+ },
+ {
+ account_id: 6,
+ answer:
+ "Campaigns in Chatwoot allow you to send targeted messages to specific user segments. You can create them in the 'Campaigns' section.",
+ created_at: 1736283380,
+ id: 92,
+ question: 'What are campaigns in Chatwoot?',
+ assistant: {
+ account_id: 6,
+ config: { product_name: 'Chatwoot' },
+ created_at: 1736033285,
+ description:
+ 'Specialized in marketing, campaign management, and messaging strategies.',
+ id: 6,
+ name: 'Assistant 7',
+ },
+ },
+];
+
+export const inboxes = [
+ {
+ id: 7,
+ name: 'Email Support',
+ channel_type: INBOX_TYPES.EMAIL,
+ email: 'support@company.com',
+ },
+ {
+ id: 1,
+ name: 'Website Chat',
+ channel_type: INBOX_TYPES.WEB,
+ },
+ {
+ id: 2,
+ name: 'Facebook Support',
+ channel_type: INBOX_TYPES.FB,
+ },
+ {
+ id: 5,
+ name: 'SMS Service',
+ channel_type: INBOX_TYPES.TWILIO,
+ messaging_service_sid: 'MGxxxxxx',
+ },
+ {
+ id: 6,
+ name: 'WhatsApp Support',
+ channel_type: INBOX_TYPES.WHATSAPP,
+ phone_number: '+1987654321',
+ },
+ {
+ id: 8,
+ name: 'Telegram Support',
+ channel_type: INBOX_TYPES.TELEGRAM,
+ },
+ {
+ id: 9,
+ name: 'LINE Support',
+ channel_type: INBOX_TYPES.LINE,
+ },
+ {
+ id: 10,
+ name: 'API Channel',
+ channel_type: INBOX_TYPES.API,
+ },
+ {
+ id: 11,
+ name: 'SMS Basic',
+ channel_type: INBOX_TYPES.SMS,
+ phone_number: '+1555555555',
+ },
+];
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxDialog.vue
new file mode 100644
index 000000000..06c6e8db3
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxDialog.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxForm.vue
new file mode 100644
index 000000000..23aae478f
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/inbox/ConnectInboxForm.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue
new file mode 100644
index 000000000..0bae4cf17
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/response/LimitBanner.vue b/app/javascript/dashboard/components-next/captain/pageComponents/response/LimitBanner.vue
new file mode 100644
index 000000000..0b5462f0b
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/response/LimitBanner.vue
@@ -0,0 +1,42 @@
+
+
+
+
+ {{ $t('CAPTAIN.BANNER.RESPONSES') }}
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/response/ResponseForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/response/ResponseForm.vue
new file mode 100644
index 000000000..5582199a7
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/response/ResponseForm.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/checkbox/Checkbox.story.vue b/app/javascript/dashboard/components-next/checkbox/Checkbox.story.vue
new file mode 100644
index 000000000..553700bf1
--- /dev/null
+++ b/app/javascript/dashboard/components-next/checkbox/Checkbox.story.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+ Default:
+
+
+
+
+ Checked:
+
+
+
+
+ Indeterminate:
+
+
+
+
+ Indeterminate disabled:
+
+
+
+
+ Disabled:
+
+
+
+
+ Disabled Checked:
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/checkbox/Checkbox.vue b/app/javascript/dashboard/components-next/checkbox/Checkbox.vue
new file mode 100644
index 000000000..986867c37
--- /dev/null
+++ b/app/javascript/dashboard/components-next/checkbox/Checkbox.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/combobox/ComboBox.vue b/app/javascript/dashboard/components-next/combobox/ComboBox.vue
index de84974e6..026a63a70 100644
--- a/app/javascript/dashboard/components-next/combobox/ComboBox.vue
+++ b/app/javascript/dashboard/components-next/combobox/ComboBox.vue
@@ -13,34 +13,14 @@ const props = defineProps({
validator: value =>
value.every(option => 'value' in option && 'label' in option),
},
- placeholder: {
- type: String,
- default: '',
- },
- modelValue: {
- type: [String, Number],
- default: '',
- },
- disabled: {
- type: Boolean,
- default: false,
- },
- searchPlaceholder: {
- type: String,
- default: '',
- },
- emptyState: {
- type: String,
- default: '',
- },
- message: {
- type: String,
- default: '',
- },
- hasError: {
- type: Boolean,
- default: false,
- },
+ placeholder: { type: String, default: '' },
+ modelValue: { type: [String, Number], default: '' },
+ disabled: { type: Boolean, default: false },
+ searchPlaceholder: { type: String, default: '' },
+ emptyState: { type: String, default: '' },
+ message: { type: String, default: '' },
+ hasError: { type: Boolean, default: false },
+ useApiResults: { type: Boolean, default: false }, // useApiResults prop to determine if search is handled by API
});
const emit = defineEmits(['update:modelValue', 'search']);
@@ -54,6 +34,12 @@ const dropdownRef = ref(null);
const comboboxRef = ref(null);
const filteredOptions = computed(() => {
+ // For API search, don't filter options locally
+ if (props.useApiResults && search.value) {
+ return props.options;
+ }
+
+ // For local search, filter options based on search term
const searchTerm = search.value.toLowerCase();
return props.options.filter(option =>
option.label.toLowerCase().includes(searchTerm)
@@ -110,7 +96,7 @@ watch(
:label="selectedLabel"
trailing-icon
:disabled="disabled"
- class="justify-between w-full !px-3 !py-2.5 text-n-slate-12 font-normal group-hover/combobox:border-n-slate-6"
+ class="justify-between w-full !px-3 !py-2.5 text-n-slate-12 font-normal group-hover/combobox:border-n-slate-6 [&:not(.focused)]:hover:enabled:outline-n-slate-6 [&:not(.focused)]:dark:hover:enabled:outline-n-slate-6 [&:not(.focused)]:dark:outline-n-weak focus:outline-n-brand"
:class="{ focused: open }"
:icon="open ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
@click="toggleDropdown"
diff --git a/app/javascript/dashboard/components-next/combobox/ComboBoxDropdown.vue b/app/javascript/dashboard/components-next/combobox/ComboBoxDropdown.vue
index e1cd8a086..e24a366e2 100644
--- a/app/javascript/dashboard/components-next/combobox/ComboBoxDropdown.vue
+++ b/app/javascript/dashboard/components-next/combobox/ComboBoxDropdown.vue
@@ -69,7 +69,7 @@ defineExpose({
:value="searchValue"
type="search"
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
- class="w-full py-2 pl-10 pr-2 text-sm border-none rounded-t-md bg-n-solid-1 text-slate-900 dark:text-slate-50"
+ class="reset-base w-full py-2 pl-10 pr-2 text-sm focus:outline-none border-none rounded-t-md bg-n-solid-1 text-slate-900 dark:text-slate-50"
@input="onInputSearch"
/>
diff --git a/app/javascript/dashboard/components-next/copilot/Copilot.vue b/app/javascript/dashboard/components-next/copilot/Copilot.vue
index 3839532f2..e284bb983 100644
--- a/app/javascript/dashboard/components-next/copilot/Copilot.vue
+++ b/app/javascript/dashboard/components-next/copilot/Copilot.vue
@@ -1,9 +1,14 @@
-
-
+
+
-
+
+
+
+ {{ $t('COPILOT.TRY_THESE_PROMPTS') }}
+
+ useSuggestion(prompt)"
+ >
+ {{ prompt.label }}
+
+
+
+
+
+
+
+ emit('setAssistant', $event)"
+ />
+
+
+
+ {{ $t('CAPTAIN.COPILOT.RESET') }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue b/app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue
index a14508150..1877e1630 100644
--- a/app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue
+++ b/app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue
@@ -1,12 +1,48 @@
@@ -19,8 +55,23 @@ defineProps({
/>
{{ $t('CAPTAIN.NAME') }}
-
- {{ message.content }}
+
+ {{ $t('CAPTAIN.COPILOT.EMPTY_MESSAGE') }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue b/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue
new file mode 100644
index 000000000..ec21cec9f
--- /dev/null
+++ b/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+ emit('setAssistant', assistant)"
+ >
+
+
+
+
+ {{ assistant.name }}
+
+
+ {{ assistant.description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/dialog/Dialog.vue b/app/javascript/dashboard/components-next/dialog/Dialog.vue
index c485549ae..6800b1254 100644
--- a/app/javascript/dashboard/components-next/dialog/Dialog.vue
+++ b/app/javascript/dashboard/components-next/dialog/Dialog.vue
@@ -80,10 +80,12 @@ const maxWidthClass = computed(() => {
const open = () => {
dialogRef.value?.showModal();
};
+
const close = () => {
emit('close');
dialogRef.value?.close();
};
+
const confirm = () => {
emit('confirm');
};
@@ -104,9 +106,10 @@ defineExpose({ open, close });
@close="close"
>
-
@@ -122,13 +125,17 @@ defineExpose({ open, close });
-
+
-
+
@@ -150,6 +157,6 @@ defineExpose({ open, close });
diff --git a/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue b/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue
index 95b368dae..c8e69b049 100644
--- a/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue
+++ b/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue
@@ -29,6 +29,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
+ labelClass: {
+ type: String,
+ default: '',
+ },
});
const emit = defineEmits(['action']);
@@ -71,7 +75,7 @@ onMounted(() => {
:placeholder="
searchPlaceholder || t('DROPDOWN_MENU.SEARCH_PLACEHOLDER')
"
- class="w-full h-8 py-2 pl-10 pr-2 text-sm border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
+ class="reset-base w-full h-8 py-2 pl-10 pr-2 text-sm focus:outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
/>
{
{{ item.emoji }}
- {{
- item.label
- }}
+
+ {{ item.label }}
+
{
// Add extra blur layer only when strong prop is true, as a hack for Chrome's stacked backdrop-blur limitation
// https://issues.chromium.org/issues/40835530
- return "before:content-['\x00A0'] before:absolute before:bottom-0 before:left-0 before:w-full before:h-full before:backdrop-contrast-70 before:backdrop-blur-sm before:z-0 [&>*]:relative";
+ return "before:content-['\x00A0'] before:absolute before:bottom-0 before:left-0 before:w-full before:h-full before:rounded-xl before:backdrop-contrast-70 before:backdrop-blur-sm before:z-0 [&>*]:relative";
});
diff --git a/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownItem.vue b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownItem.vue
index 821b4218b..9db3a09d4 100644
--- a/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownItem.vue
+++ b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownItem.vue
@@ -6,7 +6,8 @@ import { useDropdownContext } from './provider.js';
const props = defineProps({
label: { type: String, default: '' },
icon: { type: [String, Object, Function], default: '' },
- link: { type: String, default: '' },
+ link: { type: [String, Object], default: '' },
+ nativeLink: { type: Boolean, default: false },
click: { type: Function, default: null },
preserveOpen: { type: Boolean, default: false },
});
@@ -18,7 +19,13 @@ defineOptions({
const { closeMenu } = useDropdownContext();
const componentIs = computed(() => {
- if (props.link) return 'router-link';
+ if (props.link) {
+ if (props.nativeLink && typeof props.link === 'string') {
+ return 'a';
+ }
+
+ return 'router-link';
+ }
if (props.click) return 'button';
return 'div';
@@ -41,7 +48,8 @@ const triggerClick = () => {
:class="{
'hover:bg-n-alpha-2 rounded-lg w-full gap-3': !$slots.default,
}"
- :href="props.link || null"
+ :href="componentIs === 'a' ? props.link : null"
+ :to="componentIs === 'router-link' ? props.link : null"
@click="triggerClick"
>
diff --git a/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.story.vue b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.story.vue
new file mode 100644
index 000000000..d42af2a11
--- /dev/null
+++ b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.story.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.vue b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.vue
new file mode 100644
index 000000000..d8e145817
--- /dev/null
+++ b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlight.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ note }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.story.vue b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.story.vue
new file mode 100644
index 000000000..48bbc3c93
--- /dev/null
+++ b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.story.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue
new file mode 100644
index 000000000..6d3c48cdc
--- /dev/null
+++ b/app/javascript/dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ note }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/filter/SaveCustomView.vue b/app/javascript/dashboard/components-next/filter/SaveCustomView.vue
index 95b2e0388..fb7018f1b 100644
--- a/app/javascript/dashboard/components-next/filter/SaveCustomView.vue
+++ b/app/javascript/dashboard/components-next/filter/SaveCustomView.vue
@@ -103,7 +103,7 @@ export default {
{{ $t('FILTER.CUSTOM_VIEWS.ADD.TITLE') }}
diff --git a/app/javascript/dashboard/components-next/filter/contactProvider.js b/app/javascript/dashboard/components-next/filter/contactProvider.js
index 4aceac359..4b2709e62 100644
--- a/app/javascript/dashboard/components-next/filter/contactProvider.js
+++ b/app/javascript/dashboard/components-next/filter/contactProvider.js
@@ -2,7 +2,10 @@ import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useOperators } from './operators';
import { useMapGetter } from 'dashboard/composables/store.js';
-import { buildAttributesFilterTypes } from './helper/filterHelper.js';
+import {
+ buildAttributesFilterTypes,
+ CONTACT_ATTRIBUTES,
+} from './helper/filterHelper.js';
import countries from 'shared/constants/countries.js';
/**
@@ -59,7 +62,11 @@ export function useContactFilterContext() {
* @type {import('vue').ComputedRef}
*/
const customFilterTypes = computed(() =>
- buildAttributesFilterTypes(contactAttributes.value, getOperatorTypes)
+ buildAttributesFilterTypes(
+ contactAttributes.value,
+ getOperatorTypes,
+ 'contact'
+ )
);
/**
@@ -67,8 +74,8 @@ export function useContactFilterContext() {
*/
const filterTypes = computed(() => [
{
- attributeKey: 'name',
- value: 'name',
+ attributeKey: CONTACT_ATTRIBUTES.NAME,
+ value: CONTACT_ATTRIBUTES.NAME,
attributeName: t('CONTACTS_LAYOUT.FILTER.NAME'),
label: t('CONTACTS_LAYOUT.FILTER.NAME'),
inputType: 'plainText',
@@ -77,8 +84,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'email',
- value: 'email',
+ attributeKey: CONTACT_ATTRIBUTES.EMAIL,
+ value: CONTACT_ATTRIBUTES.EMAIL,
attributeName: t('CONTACTS_LAYOUT.FILTER.EMAIL'),
label: t('CONTACTS_LAYOUT.FILTER.EMAIL'),
inputType: 'plainText',
@@ -87,8 +94,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'phone_number',
- value: 'phone_number',
+ attributeKey: CONTACT_ATTRIBUTES.PHONE_NUMBER,
+ value: CONTACT_ATTRIBUTES.PHONE_NUMBER,
attributeName: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'),
label: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'),
inputType: 'plainText',
@@ -97,8 +104,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'identifier',
- value: 'identifier',
+ attributeKey: CONTACT_ATTRIBUTES.IDENTIFIER,
+ value: CONTACT_ATTRIBUTES.IDENTIFIER,
attributeName: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'),
label: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'),
inputType: 'plainText',
@@ -107,8 +114,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'country_code',
- value: 'country_code',
+ attributeKey: CONTACT_ATTRIBUTES.COUNTRY_CODE,
+ value: CONTACT_ATTRIBUTES.COUNTRY_CODE,
attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
inputType: 'searchSelect',
@@ -118,8 +125,8 @@ export function useContactFilterContext() {
attributeModel: 'additional',
},
{
- attributeKey: 'city',
- value: 'city',
+ attributeKey: CONTACT_ATTRIBUTES.CITY,
+ value: CONTACT_ATTRIBUTES.CITY,
attributeName: t('CONTACTS_LAYOUT.FILTER.CITY'),
label: t('CONTACTS_LAYOUT.FILTER.CITY'),
inputType: 'plainText',
@@ -128,8 +135,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'created_at',
- value: 'created_at',
+ attributeKey: CONTACT_ATTRIBUTES.CREATED_AT,
+ value: CONTACT_ATTRIBUTES.CREATED_AT,
attributeName: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'),
label: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'),
inputType: 'date',
@@ -138,8 +145,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'last_activity_at',
- value: 'last_activity_at',
+ attributeKey: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT,
+ value: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT,
attributeName: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'),
label: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'),
inputType: 'date',
@@ -148,8 +155,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'referer',
- value: 'referer',
+ attributeKey: CONTACT_ATTRIBUTES.REFERER,
+ value: CONTACT_ATTRIBUTES.REFERER,
attributeName: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'),
label: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'),
inputType: 'plainText',
@@ -158,8 +165,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'blocked',
- value: 'blocked',
+ attributeKey: CONTACT_ATTRIBUTES.BLOCKED,
+ value: CONTACT_ATTRIBUTES.BLOCKED,
attributeName: t('CONTACTS_LAYOUT.FILTER.BLOCKED'),
label: t('CONTACTS_LAYOUT.FILTER.BLOCKED'),
inputType: 'searchSelect',
diff --git a/app/javascript/dashboard/components-next/filter/helper/filterHelper.js b/app/javascript/dashboard/components-next/filter/helper/filterHelper.js
index 2160365f6..cb5acfe40 100644
--- a/app/javascript/dashboard/components-next/filter/helper/filterHelper.js
+++ b/app/javascript/dashboard/components-next/filter/helper/filterHelper.js
@@ -1,3 +1,35 @@
+/**
+ * Standard attributes of the conversation model
+ */
+export const CONVERSATION_ATTRIBUTES = {
+ STATUS: 'status',
+ PRIORITY: 'priority',
+ ASSIGNEE_ID: 'assignee_id',
+ INBOX_ID: 'inbox_id',
+ TEAM_ID: 'team_id',
+ DISPLAY_ID: 'display_id',
+ CAMPAIGN_ID: 'campaign_id',
+ LABELS: 'labels',
+ BROWSER_LANGUAGE: 'browser_language',
+ COUNTRY_CODE: 'country_code',
+ REFERER: 'referer',
+ CREATED_AT: 'created_at',
+ LAST_ACTIVITY_AT: 'last_activity_at',
+};
+
+export const CONTACT_ATTRIBUTES = {
+ NAME: 'name',
+ EMAIL: 'email',
+ PHONE_NUMBER: 'phone_number',
+ IDENTIFIER: 'identifier',
+ COUNTRY_CODE: 'country_code',
+ CITY: 'city',
+ CREATED_AT: 'created_at',
+ LAST_ACTIVITY_AT: 'last_activity_at',
+ REFERER: 'referer',
+ BLOCKED: 'blocked',
+};
+
/**
* Determines the input type for a custom attribute based on its key
* @param {string} key - The attribute display type key
@@ -20,24 +52,37 @@ export const getCustomAttributeInputType = key => {
/**
* Builds filter types for custom attributes
+ * This also removes any conflicting attributes
* @param {Array} attributes - The attributes array
* @param {Function} getOperatorTypes - Function to get operator types
* @returns {Array} Array of filter types
*/
-export const buildAttributesFilterTypes = (attributes, getOperatorTypes) => {
- return attributes.map(attr => ({
- attributeKey: attr.attributeKey,
- value: attr.attributeKey,
- attributeName: attr.attributeDisplayName,
- label: attr.attributeDisplayName,
- inputType: getCustomAttributeInputType(attr.attributeDisplayType),
- filterOperators: getOperatorTypes(attr.attributeDisplayType),
- options:
- attr.attributeDisplayType === 'list'
- ? attr.attributeValues.map(item => ({ id: item, name: item }))
- : [],
- attributeModel: 'customAttributes',
- }));
+export const buildAttributesFilterTypes = (
+ attributes,
+ getOperatorTypes,
+ filterModel = 'conversation'
+) => {
+ const standardAttributes = Object.values(
+ filterModel === 'conversation'
+ ? CONVERSATION_ATTRIBUTES
+ : CONTACT_ATTRIBUTES
+ );
+
+ return attributes
+ .filter(attr => !standardAttributes.includes(attr.attributeKey))
+ .map(attr => ({
+ attributeKey: attr.attributeKey,
+ value: attr.attributeKey,
+ attributeName: attr.attributeDisplayName,
+ label: attr.attributeDisplayName,
+ inputType: getCustomAttributeInputType(attr.attributeDisplayType),
+ filterOperators: getOperatorTypes(attr.attributeDisplayType),
+ options:
+ attr.attributeDisplayType === 'list'
+ ? attr.attributeValues.map(item => ({ id: item, name: item }))
+ : [],
+ attributeModel: 'customAttributes',
+ }));
};
/**
diff --git a/app/javascript/dashboard/components-next/filter/provider.js b/app/javascript/dashboard/components-next/filter/provider.js
index bb775a663..f6d078d76 100644
--- a/app/javascript/dashboard/components-next/filter/provider.js
+++ b/app/javascript/dashboard/components-next/filter/provider.js
@@ -3,7 +3,10 @@ import { useI18n } from 'vue-i18n';
import { useOperators } from './operators';
import { useMapGetter } from 'dashboard/composables/store.js';
import { useChannelIcon } from 'next/icon/provider';
-import { buildAttributesFilterTypes } from './helper/filterHelper';
+import {
+ buildAttributesFilterTypes,
+ CONVERSATION_ATTRIBUTES,
+} from './helper/filterHelper';
import countries from 'shared/constants/countries.js';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
@@ -70,7 +73,11 @@ export function useConversationFilterContext() {
* @type {import('vue').ComputedRef}
*/
const customFilterTypes = computed(() =>
- buildAttributesFilterTypes(conversationAttributes.value, getOperatorTypes)
+ buildAttributesFilterTypes(
+ conversationAttributes.value,
+ getOperatorTypes,
+ 'conversation'
+ )
);
/**
@@ -78,8 +85,8 @@ export function useConversationFilterContext() {
*/
const filterTypes = computed(() => [
{
- attributeKey: 'status',
- value: 'status',
+ attributeKey: CONVERSATION_ATTRIBUTES.STATUS,
+ value: CONVERSATION_ATTRIBUTES.STATUS,
attributeName: t('FILTER.ATTRIBUTES.STATUS'),
label: t('FILTER.ATTRIBUTES.STATUS'),
inputType: 'multiSelect',
@@ -94,8 +101,24 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'assignee_id',
- value: 'assignee_id',
+ attributeKey: CONVERSATION_ATTRIBUTES.PRIORITY,
+ value: CONVERSATION_ATTRIBUTES.PRIORITY,
+ attributeName: t('FILTER.ATTRIBUTES.PRIORITY'),
+ label: t('FILTER.ATTRIBUTES.PRIORITY'),
+ inputType: 'multiSelect',
+ options: ['low', 'medium', 'high', 'urgent'].map(id => {
+ return {
+ id,
+ name: t(`CONVERSATION.PRIORITY.OPTIONS.${id.toUpperCase()}`),
+ };
+ }),
+ dataType: 'text',
+ filterOperators: equalityOperators.value,
+ attributeModel: 'standard',
+ },
+ {
+ attributeKey: CONVERSATION_ATTRIBUTES.ASSIGNEE_ID,
+ value: CONVERSATION_ATTRIBUTES.ASSIGNEE_ID,
attributeName: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'),
label: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'),
inputType: 'searchSelect',
@@ -110,8 +133,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'inbox_id',
- value: 'inbox_id',
+ attributeKey: CONVERSATION_ATTRIBUTES.INBOX_ID,
+ value: CONVERSATION_ATTRIBUTES.INBOX_ID,
attributeName: t('FILTER.ATTRIBUTES.INBOX_NAME'),
label: t('FILTER.ATTRIBUTES.INBOX_NAME'),
inputType: 'searchSelect',
@@ -126,8 +149,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'team_id',
- value: 'team_id',
+ attributeKey: CONVERSATION_ATTRIBUTES.TEAM_ID,
+ value: CONVERSATION_ATTRIBUTES.TEAM_ID,
attributeName: t('FILTER.ATTRIBUTES.TEAM_NAME'),
label: t('FILTER.ATTRIBUTES.TEAM_NAME'),
inputType: 'searchSelect',
@@ -137,8 +160,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'display_id',
- value: 'display_id',
+ attributeKey: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
+ value: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
attributeName: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'),
label: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'),
inputType: 'plainText',
@@ -147,8 +170,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'campaign_id',
- value: 'campaign_id',
+ attributeKey: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID,
+ value: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID,
attributeName: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'),
label: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'),
inputType: 'searchSelect',
@@ -161,8 +184,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'labels',
- value: 'labels',
+ attributeKey: CONVERSATION_ATTRIBUTES.LABELS,
+ value: CONVERSATION_ATTRIBUTES.LABELS,
attributeName: t('FILTER.ATTRIBUTES.LABELS'),
label: t('FILTER.ATTRIBUTES.LABELS'),
inputType: 'multiSelect',
@@ -185,8 +208,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'browser_language',
- value: 'browser_language',
+ attributeKey: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE,
+ value: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE,
attributeName: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'),
label: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'),
inputType: 'searchSelect',
@@ -196,8 +219,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional',
},
{
- attributeKey: 'country_code',
- value: 'country_code',
+ attributeKey: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
+ value: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
inputType: 'searchSelect',
@@ -207,8 +230,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional',
},
{
- attributeKey: 'referer',
- value: 'referer',
+ attributeKey: CONVERSATION_ATTRIBUTES.REFERER,
+ value: CONVERSATION_ATTRIBUTES.REFERER,
attributeName: t('FILTER.ATTRIBUTES.REFERER_LINK'),
label: t('FILTER.ATTRIBUTES.REFERER_LINK'),
inputType: 'plainText',
@@ -217,8 +240,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional',
},
{
- attributeKey: 'created_at',
- value: 'created_at',
+ attributeKey: CONVERSATION_ATTRIBUTES.CREATED_AT,
+ value: CONVERSATION_ATTRIBUTES.CREATED_AT,
attributeName: t('FILTER.ATTRIBUTES.CREATED_AT'),
label: t('FILTER.ATTRIBUTES.CREATED_AT'),
inputType: 'date',
@@ -227,8 +250,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
- attributeKey: 'last_activity_at',
- value: 'last_activity_at',
+ attributeKey: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT,
+ value: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT,
attributeName: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'),
label: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'),
inputType: 'date',
diff --git a/app/javascript/dashboard/components-next/icon/FileIcon.story.vue b/app/javascript/dashboard/components-next/icon/FileIcon.story.vue
new file mode 100644
index 000000000..3a831b96f
--- /dev/null
+++ b/app/javascript/dashboard/components-next/icon/FileIcon.story.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+ {{ file.name }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/icon/FileIcon.vue b/app/javascript/dashboard/components-next/icon/FileIcon.vue
new file mode 100644
index 000000000..8dd9e7ce1
--- /dev/null
+++ b/app/javascript/dashboard/components-next/icon/FileIcon.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/icon/Logo.vue b/app/javascript/dashboard/components-next/icon/Logo.vue
index 2330021c3..6dd0bbd8f 100644
--- a/app/javascript/dashboard/components-next/icon/Logo.vue
+++ b/app/javascript/dashboard/components-next/icon/Logo.vue
@@ -1,6 +1,21 @@
+
+
+
+ {{ t('CAPTAIN.PLAYGROUND.HEADER') }} +
++ {{ t('CAPTAIN.PLAYGROUND.DESCRIPTION') }} +
++ {{ t('CAPTAIN.PLAYGROUND.CREDIT_NOTE') }} +
+{{ note }}
+ ++ {{ note }} +
+ +{{ file.name }}
+
-
+
+
___
# Chatwoot
-Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
-
-
-Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
+
+
-Chatwoot supports the following conversation channels:
+---
- - **Website**: Talk to your customers using our live chat widget and make use of our SDK to identify a user and provide contextual support.
- - **Facebook**: Connect your Facebook pages and start replying to the direct messages to your page.
- - **Instagram**: Connect your Instagram profile and start replying to the direct messages.
- - **Twitter**: Connect your Twitter profiles and reply to direct messages or the tweets where you are mentioned.
- - **Telegram**: Connect your Telegram bot and reply to your customers right from a single dashboard.
- - **WhatsApp**: Connect your WhatsApp business account and manage the conversation in Chatwoot.
- - **Line**: Connect your Line account and manage the conversations in Chatwoot.
- - **SMS**: Connect your Twilio SMS account and reply to the SMS queries in Chatwoot.
- - **API Channel**: Build custom communication channels using our API channel.
- - **Email**: Forward all your email queries to Chatwoot and view it in our integrated dashboard.
+Chatwoot is the modern, open-source, and self-hosted customer support platform designed to help businesses deliver exceptional customer support experience. Built for scale and flexibility, Chatwoot gives you full control over your customer data while providing powerful tools to manage conversations across channels.
-And more.
+### ✨ Captain – AI Agent for Support
-Other features include:
+Supercharge your support with Captain, Chatwoot’s AI agent. Captain helps automate responses, handle common queries, and reduce agent workload—ensuring customers get instant, accurate answers. With Captain, your team can focus on complex conversations while routine questions are resolved automatically. Read more about Captain [here](https://chwt.app/captain-docs).
+
+### 💬 Omnichannel Support Desk
+
+Chatwoot centralizes all customer conversations into one powerful inbox, no matter where your customers reach out from. It supports live chat on your website, email, Facebook, Instagram, Twitter, WhatsApp, Telegram, Line, SMS etc.
+
+### 📚 Help center portal
+
+Publish help articles, FAQs, and guides through the built-in Help Center Portal. Enable customers to find answers on their own, reduce repetitive queries, and keep your support team focused on more complex issues.
+
+### 🗂️ Other features
+
+#### Collaboration & Productivity
+
+- Private Notes and @mentions for internal team discussions.
+- Labels to organize and categorize conversations.
+- Keyboard Shortcuts and a Command Bar for quick navigation.
+- Canned Responses to reply faster to frequently asked questions.
+- Auto-Assignment to route conversations based on agent availability.
+- Multi-lingual Support to serve customers in multiple languages.
+- Custom Views and Filters for better inbox organization.
+- Business Hours and Auto-Responders to manage response expectations.
+- Teams and Automation tools for scaling support workflows.
+- Agent Capacity Management to balance workload across the team.
+
+#### Customer Data & Segmentation
+- Contact Management with profiles and interaction history.
+- Contact Segments and Notes for targeted communication.
+- Campaigns to proactively engage customers.
+- Custom Attributes for storing additional customer data.
+- Pre-Chat Forms to collect user information before starting conversations.
+
+#### Integrations
+- Slack Integration to manage conversations directly from Slack.
+- Dialogflow Integration for chatbot automation.
+- Dashboard Apps to embed internal tools within Chatwoot.
+- Shopify Integration to view and manage customer orders right within Chatwoot.
+- Use Google Translate to translate messages from your customers in realtime.
+- Create and manage Linear tickets within Chatwoot.
+
+#### Reports & Insights
+- Live View of ongoing conversations for real-time monitoring.
+- Conversation, Agent, Inbox, Label, and Team Reports for operational visibility.
+- CSAT Reports to measure customer satisfaction.
+- Downloadable Reports for offline analysis and reporting.
-- **CRM**: Save all your customer information right inside Chatwoot, use contact notes to log emails, phone calls, or meeting notes.
-- **Custom Attributes**: Define custom attribute attributes to store information about a contact or a conversation and extend the product to match your workflow.
-- **Shared multi-brand inboxes**: Manage multiple brands or pages using a shared inbox.
-- **Private notes**: Use @mentions and private notes to communicate internally about a conversation.
-- **Canned responses (Saved replies)**: Improve the response rate by adding saved replies for frequently asked questions.
-- **Conversation Labels**: Use conversation labels to create custom workflows.
-- **Auto assignment**: Chatwoot intelligently assigns a ticket to the agents who have access to the inbox depending on their availability and load.
-- **Conversation continuity**: If the user has provided an email address through the chat widget, Chatwoot will send an email to the customer under the agent name so that the user can continue the conversation over the email.
-- **Multi-lingual support**: Chatwoot supports 10+ languages.
-- **Powerful API & Webhooks**: Extend the capability of the software using Chatwoot’s webhooks and APIs.
-- **Integrations**: Chatwoot natively integrates with Slack right now. Manage your conversations in Slack without logging into the dashboard.
## Documentation
@@ -107,17 +126,15 @@ For other supported options, checkout our [deployment page](https://chatwoot.com
Looking to report a vulnerability? Please refer our [SECURITY.md](./SECURITY.md) file.
-
-## Community? Questions? Support ?
+## Community
If you need help or just want to hang out, come, say hi on our [Discord](https://discord.gg/cJXdrwS) server.
-
-## Contributors ✨
+## Contributors
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):