-
+
{
emit('setReplyMode', mode);
@@ -189,7 +189,7 @@ export default {
class="text-n-slate-11"
sm
icon="i-lucide-maximize-2"
- @click="$emit('togglePopout')"
+ @click="$emit('toggleEditorSize')"
/>
diff --git a/app/javascript/dashboard/components/widgets/conversation/CopilotEditorSection.vue b/app/javascript/dashboard/components/widgets/conversation/CopilotEditorSection.vue
index bdd537d92..17d6712d4 100644
--- a/app/javascript/dashboard/components/widgets/conversation/CopilotEditorSection.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/CopilotEditorSection.vue
@@ -16,10 +16,6 @@ defineProps({
type: String,
default: '',
},
- isPopout: {
- type: Boolean,
- default: false,
- },
});
const emit = defineEmits([
@@ -69,7 +65,6 @@ const onSend = () => {
:generated-content="generatedContent"
:min-height="4"
:enabled-menu-options="[]"
- :is-popout="isPopout"
@focus="onFocus"
@blur="onBlur"
@clear-selection="clearEditorSelection"
diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue
index bc0b6c946..6b6e3d2b2 100644
--- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue
@@ -1,7 +1,7 @@
-
-
-
diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
index a5122094e..d18be0caa 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
@@ -81,13 +81,7 @@ export default {
CopilotReplyBottomPanel,
},
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
- props: {
- popOutReplyBox: {
- type: Boolean,
- default: false,
- },
- },
- emits: ['update:popOutReplyBox'],
+ emits: ['toggleEditorSize'],
setup() {
const {
uiSettings,
@@ -797,7 +791,6 @@ export default {
this.clearMessage();
this.hideEmojiPicker();
- this.$emit('update:popOutReplyBox', false);
}
},
sendMessageAsMultipleMessages(message, copilotAcceptedMessage = '') {
@@ -1217,8 +1210,9 @@ export default {
file => !file?.isRecordedAudio
);
},
- togglePopout() {
- this.$emit('update:popOutReplyBox', !this.popOutReplyBox);
+ toggleEditorSize() {
+ this.$emit('toggleEditorSize');
+ this.$nextTick(() => this.messageEditor?.focusEditorInputField());
},
onSubmitCopilotReply() {
const acceptedMessage = this.copilot.accept();
@@ -1244,9 +1238,8 @@ export default {
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
:characters-remaining="charactersRemaining"
:editor-content="message"
- :popout-reply-box="popOutReplyBox"
@set-reply-mode="setReplyMode"
- @toggle-popout="togglePopout"
+ @toggle-editor-size="toggleEditorSize"
@toggle-copilot="copilot.toggleEditor"
@execute-copilot-action="executeCopilotAction"
/>
@@ -1275,7 +1268,7 @@ export default {
v-if="showEmojiPicker"
v-on-clickaway="hideEmojiPicker"
:class="{
- 'emoji-dialog--expanded': isOnExpandedLayout || popOutReplyBox,
+ 'emoji-dialog--expanded': isOnExpandedLayout,
}"
:on-click="addIntoEditor"
/>
@@ -1299,7 +1292,6 @@ export default {
:show-copilot-editor="copilot.showEditor.value"
:is-generating-content="copilot.isGenerating.value"
:generated-content="copilot.generatedContent.value"
- :is-popout="popOutReplyBox"
:placeholder="$t('CONVERSATION.FOOTER.COPILOT_MSG_INPUT')"
@focus="onFocus"
@blur="onBlur"
diff --git a/app/javascript/dashboard/components/widgets/conversation/ResizableEditorWrapper.vue b/app/javascript/dashboard/components/widgets/conversation/ResizableEditorWrapper.vue
new file mode 100644
index 000000000..4d5c041ac
--- /dev/null
+++ b/app/javascript/dashboard/components/widgets/conversation/ResizableEditorWrapper.vue
@@ -0,0 +1,154 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsBulkActionBar.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsBulkActionBar.vue
index e56a2dda7..d19f84d99 100644
--- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsBulkActionBar.vue
+++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsBulkActionBar.vue
@@ -66,8 +66,7 @@ const selectionModel = computed({
return;
}
- const shouldSelectAll =
- newSet.size === props.visibleContactIds.length && newSet.size > 0;
+ const shouldSelectAll = props.visibleContactIds.every(id => newSet.has(id));
emit('toggleAll', shouldSelectAll);
},
});
diff --git a/app/javascript/dashboard/routes/dashboard/contacts/pages/ContactsIndex.vue b/app/javascript/dashboard/routes/dashboard/contacts/pages/ContactsIndex.vue
index 357758e01..b072fa67d 100644
--- a/app/javascript/dashboard/routes/dashboard/contacts/pages/ContactsIndex.vue
+++ b/app/javascript/dashboard/routes/dashboard/contacts/pages/ContactsIndex.vue
@@ -151,7 +151,13 @@ const openBulkDeleteDialog = () => {
};
const toggleSelectAll = shouldSelect => {
- selectedContactIds.value = shouldSelect ? [...visibleContactIds.value] : [];
+ const currentSelection = new Set(selectedContactIds.value);
+ if (shouldSelect) {
+ visibleContactIds.value.forEach(id => currentSelection.add(id));
+ } else {
+ visibleContactIds.value.forEach(id => currentSelection.delete(id));
+ }
+ selectedContactIds.value = Array.from(currentSelection);
};
const toggleContactSelection = ({ id, value }) => {
@@ -190,16 +196,28 @@ const getCommonFetchParams = (page = 1) => ({
label: activeLabel.value,
});
-const fetchContacts = async (page = 1) => {
- clearSelection();
+const fetchContacts = async (page = 1, options = {}) => {
+ const { clearSelection: shouldClearSelection = true } = options;
+ if (shouldClearSelection) {
+ clearSelection();
+ }
await store.dispatch('contacts/clearContactFilters');
await store.dispatch('contacts/get', getCommonFetchParams(page));
updatePageParam(page);
};
-const fetchSavedOrAppliedFilteredContact = async (payload, page = 1) => {
+const fetchSavedOrAppliedFilteredContact = async (
+ payload,
+ page = 1,
+ options = {}
+) => {
if (!activeSegmentId.value && !hasAppliedFilters.value) return;
- clearSelection();
+
+ const { clearSelection: shouldClearSelection = true } = options;
+ if (shouldClearSelection) {
+ clearSelection();
+ }
+
await store.dispatch('contacts/filter', {
...getCommonFetchParams(page),
queryPayload: payload,
@@ -207,8 +225,12 @@ const fetchSavedOrAppliedFilteredContact = async (payload, page = 1) => {
updatePageParam(page);
};
-const fetchActiveContacts = async (page = 1) => {
- clearSelection();
+const fetchActiveContacts = async (page = 1, options = {}) => {
+ const { clearSelection: shouldClearSelection = true } = options;
+ if (shouldClearSelection) {
+ clearSelection();
+ }
+
await store.dispatch('contacts/clearContactFilters');
await store.dispatch('contacts/active', {
page,
@@ -217,28 +239,36 @@ const fetchActiveContacts = async (page = 1) => {
updatePageParam(page);
};
-const searchContacts = debounce(async (value, page = 1, append = false) => {
- if (!append) {
- clearSelection();
- searchPageNumber.value = 1;
- }
- await store.dispatch('contacts/clearContactFilters');
- searchValue.value = value;
+const searchContacts = debounce(
+ async (value, page = 1, append = false, options = {}) => {
+ const { clearSelection: shouldClearSelection = true } = options;
- if (!value) {
- updatePageParam(page);
- await fetchContacts(page);
- return;
- }
+ if (!append) {
+ searchPageNumber.value = 1;
- updatePageParam(page, value);
- await store.dispatch('contacts/search', {
- ...getCommonFetchParams(page),
- search: encodeURIComponent(value),
- append,
- });
- searchPageNumber.value = page;
-}, DEBOUNCE_DELAY);
+ if (shouldClearSelection) {
+ clearSelection();
+ }
+ }
+ await store.dispatch('contacts/clearContactFilters');
+ searchValue.value = value;
+
+ if (!value) {
+ updatePageParam(page);
+ await fetchContacts(page, { clearSelection: false });
+ return;
+ }
+
+ updatePageParam(page, value);
+ await store.dispatch('contacts/search', {
+ ...getCommonFetchParams(page),
+ search: encodeURIComponent(value),
+ append,
+ });
+ searchPageNumber.value = page;
+ },
+ DEBOUNCE_DELAY
+);
const loadMoreSearchResults = async () => {
if (!hasMore.value || isLoadingMore.value) return;
@@ -256,19 +286,26 @@ const loadMoreSearchResults = async () => {
isLoadingMore.value = false;
};
-const fetchContactsBasedOnContext = async page => {
- clearSelection();
+const fetchContactsBasedOnContext = async (page, options = {}) => {
+ const { clearSelection: shouldClearSelection = true } = options;
+ if (shouldClearSelection) {
+ clearSelection();
+ }
updatePageParam(page, searchValue.value);
if (isFetchingList.value) return;
if (searchQuery.value) {
- await searchContacts(searchQuery.value, page);
+ await searchContacts(searchQuery.value, page, false, {
+ clearSelection: shouldClearSelection,
+ });
return;
}
// Reset the search value when we change the view
searchValue.value = '';
// If we're on the active route, fetch active contacts
if (isActiveView.value) {
- await fetchActiveContacts(page);
+ await fetchActiveContacts(page, {
+ clearSelection: shouldClearSelection,
+ });
return;
}
// If there are applied filters or active segment with query
@@ -278,13 +315,20 @@ const fetchContactsBasedOnContext = async page => {
) {
const queryPayload =
activeSegment.value?.query || filterQueryGenerator(appliedFilters.value);
- await fetchSavedOrAppliedFilteredContact(queryPayload, page);
+ await fetchSavedOrAppliedFilteredContact(queryPayload, page, {
+ clearSelection: shouldClearSelection,
+ });
return;
}
// Default case: fetch regular contacts + label
- await fetchContacts(page);
+ await fetchContacts(page, {
+ clearSelection: shouldClearSelection,
+ });
};
+const onPageChange = page =>
+ fetchContactsBasedOnContext(page, { clearSelection: false });
+
const assignLabels = async labels => {
if (!labels.length || !selectedContactIds.value.length) {
return;
@@ -338,7 +382,9 @@ const handleSort = async ({ sort, order }) => {
});
if (searchQuery.value) {
- await searchContacts(searchValue.value);
+ await searchContacts(searchValue.value, pageNumber.value, false, {
+ clearSelection: false,
+ });
return;
}
@@ -360,17 +406,6 @@ const createContact = async contact => {
await store.dispatch('contacts/create', contact);
};
-watch(
- contacts,
- newContacts => {
- const idsOnPage = newContacts.map(contact => contact.id);
- selectedContactIds.value = selectedContactIds.value.filter(id =>
- idsOnPage.includes(id)
- );
- },
- { deep: true }
-);
-
watch(hasSelection, value => {
if (!value) {
bulkDeleteDialogRef.value?.close?.();
@@ -416,7 +451,9 @@ watch(searchQuery, value => {
onMounted(async () => {
if (!activeSegmentId.value) {
if (searchQuery.value) {
- await searchContacts(searchQuery.value, pageNumber.value);
+ await searchContacts(searchQuery.value, pageNumber.value, false, {
+ clearSelection: false,
+ });
return;
}
if (isActiveView.value) {
@@ -452,8 +489,10 @@ onMounted(async () => {
:use-infinite-scroll="isSearchView"
:has-more="hasMore"
:is-loading-more="isLoadingMore"
- @update:current-page="fetchContactsBasedOnContext"
- @search="searchContacts"
+ @update:current-page="onPageChange"
+ @search="
+ value => searchContacts(value, 1, false, { clearSelection: false })
+ "
@update:sort="handleSort"
@apply-filter="fetchSavedOrAppliedFilteredContact"
@clear-filters="fetchContacts"
@@ -485,6 +524,7 @@ onMounted(async () => {
:button-label="t('CONTACTS_LAYOUT.EMPTY_STATE.BUTTON_LABEL')"
@create="createContact"
/>
+
{
{{ emptyStateMessage }}
+