mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-03 20:48:07 +00:00 
			
		
		
		
	chore: Search improvements (#10801)
- Adds pagination support for search. - Use composition API on all search related component. - Minor UI improvements. - Adds missing specs Loom video https://www.loom.com/share/5b01afa5c9204e7d97ff81b215621dde?sid=82ca6d22-ca8c-4d5e-8740-ba06ca4051ba
This commit is contained in:
		@@ -14,26 +14,29 @@ class SearchAPI extends ApiClient {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  contacts({ q }) {
 | 
					  contacts({ q, page = 1 }) {
 | 
				
			||||||
    return axios.get(`${this.url}/contacts`, {
 | 
					    return axios.get(`${this.url}/contacts`, {
 | 
				
			||||||
      params: {
 | 
					      params: {
 | 
				
			||||||
        q,
 | 
					        q,
 | 
				
			||||||
 | 
					        page: page,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  conversations({ q }) {
 | 
					  conversations({ q, page = 1 }) {
 | 
				
			||||||
    return axios.get(`${this.url}/conversations`, {
 | 
					    return axios.get(`${this.url}/conversations`, {
 | 
				
			||||||
      params: {
 | 
					      params: {
 | 
				
			||||||
        q,
 | 
					        q,
 | 
				
			||||||
 | 
					        page: page,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  messages({ q }) {
 | 
					  messages({ q, page = 1 }) {
 | 
				
			||||||
    return axios.get(`${this.url}/messages`, {
 | 
					    return axios.get(`${this.url}/messages`, {
 | 
				
			||||||
      params: {
 | 
					      params: {
 | 
				
			||||||
        q,
 | 
					        q,
 | 
				
			||||||
 | 
					        page: page,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,10 @@
 | 
				
			|||||||
      "CONVERSATIONS": "Conversations",
 | 
					      "CONVERSATIONS": "Conversations",
 | 
				
			||||||
      "MESSAGES": "Messages"
 | 
					      "MESSAGES": "Messages"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "VIEW_MORE": "View more",
 | 
				
			||||||
 | 
					    "LOAD_MORE": "Load more",
 | 
				
			||||||
    "SEARCHING_DATA": "Searching",
 | 
					    "SEARCHING_DATA": "Searching",
 | 
				
			||||||
 | 
					    "LOADING_DATA": "Loading",
 | 
				
			||||||
    "EMPTY_STATE": "No {item} found for query '{query}'",
 | 
					    "EMPTY_STATE": "No {item} found for query '{query}'",
 | 
				
			||||||
    "EMPTY_STATE_FULL": "No results found for query '{query}'",
 | 
					    "EMPTY_STATE_FULL": "No results found for query '{query}'",
 | 
				
			||||||
    "PLACEHOLDER_KEYBINDING": "/ to focus",
 | 
					    "PLACEHOLDER_KEYBINDING": "/ to focus",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,73 +1,59 @@
 | 
				
			|||||||
<script>
 | 
					<script setup>
 | 
				
			||||||
 | 
					import { ref, useTemplateRef, onMounted, watch, nextTick } from 'vue';
 | 
				
			||||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
 | 
					import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
 | 
				
			||||||
import ReadMore from './ReadMore.vue';
 | 
					import ReadMore from './ReadMore.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					const props = defineProps({
 | 
				
			||||||
  components: {
 | 
					  author: {
 | 
				
			||||||
    ReadMore,
 | 
					    type: String,
 | 
				
			||||||
 | 
					    default: '',
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  content: {
 | 
				
			||||||
    author: {
 | 
					    type: String,
 | 
				
			||||||
      type: String,
 | 
					    default: '',
 | 
				
			||||||
      default: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    content: {
 | 
					 | 
				
			||||||
      type: String,
 | 
					 | 
				
			||||||
      default: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    searchTerm: {
 | 
					 | 
				
			||||||
      type: String,
 | 
					 | 
				
			||||||
      default: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  setup() {
 | 
					  searchTerm: {
 | 
				
			||||||
    const { formatMessage, highlightContent } = useMessageFormatter();
 | 
					    type: String,
 | 
				
			||||||
    return {
 | 
					    default: '',
 | 
				
			||||||
      formatMessage,
 | 
					 | 
				
			||||||
      highlightContent,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					});
 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      isOverflowing: false,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  computed: {
 | 
					 | 
				
			||||||
    messageContent() {
 | 
					 | 
				
			||||||
      return this.formatMessage(this.content);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  mounted() {
 | 
					 | 
				
			||||||
    this.$watch(() => {
 | 
					 | 
				
			||||||
      return this.$refs.messageContainer;
 | 
					 | 
				
			||||||
    }, this.setOverflow);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.$nextTick(this.setOverflow);
 | 
					const { highlightContent } = useMessageFormatter();
 | 
				
			||||||
  },
 | 
					
 | 
				
			||||||
  methods: {
 | 
					const messageContainer = useTemplateRef('messageContainer');
 | 
				
			||||||
    setOverflow() {
 | 
					const isOverflowing = ref(false);
 | 
				
			||||||
      const wrap = this.$refs.messageContainer;
 | 
					
 | 
				
			||||||
      if (wrap) {
 | 
					const setOverflow = () => {
 | 
				
			||||||
        const message = wrap.querySelector('.message-content');
 | 
					  const wrap = messageContainer.value;
 | 
				
			||||||
        this.isOverflowing = message.offsetHeight > 150;
 | 
					  if (wrap) {
 | 
				
			||||||
      }
 | 
					    const message = wrap.querySelector('.message-content');
 | 
				
			||||||
    },
 | 
					    isOverflowing.value = message.offsetHeight > 150;
 | 
				
			||||||
    escapeHtml(html) {
 | 
					  }
 | 
				
			||||||
      var text = document.createTextNode(html);
 | 
					 | 
				
			||||||
      var p = document.createElement('p');
 | 
					 | 
				
			||||||
      p.appendChild(text);
 | 
					 | 
				
			||||||
      return p.innerText;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    prepareContent(content = '') {
 | 
					 | 
				
			||||||
      const escapedText = this.escapeHtml(content);
 | 
					 | 
				
			||||||
      return this.highlightContent(
 | 
					 | 
				
			||||||
        escapedText,
 | 
					 | 
				
			||||||
        this.searchTerm,
 | 
					 | 
				
			||||||
        'searchkey--highlight'
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const escapeHtml = html => {
 | 
				
			||||||
 | 
					  var text = document.createTextNode(html);
 | 
				
			||||||
 | 
					  var p = document.createElement('p');
 | 
				
			||||||
 | 
					  p.appendChild(text);
 | 
				
			||||||
 | 
					  return p.innerText;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const prepareContent = (content = '') => {
 | 
				
			||||||
 | 
					  const escapedText = escapeHtml(content);
 | 
				
			||||||
 | 
					  return highlightContent(
 | 
				
			||||||
 | 
					    escapedText,
 | 
				
			||||||
 | 
					    props.searchTerm,
 | 
				
			||||||
 | 
					    'searchkey--highlight'
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					  watch(() => {
 | 
				
			||||||
 | 
					    return messageContainer.value;
 | 
				
			||||||
 | 
					  }, setOverflow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  nextTick(setOverflow);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,6 @@
 | 
				
			|||||||
<script setup>
 | 
					<script setup>
 | 
				
			||||||
 | 
					import NextButton from 'dashboard/components-next/button/Button.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
defineProps({
 | 
					defineProps({
 | 
				
			||||||
  shrink: {
 | 
					  shrink: {
 | 
				
			||||||
    type: Boolean,
 | 
					    type: Boolean,
 | 
				
			||||||
@@ -18,17 +20,18 @@ defineEmits(['expand']);
 | 
				
			|||||||
    >
 | 
					    >
 | 
				
			||||||
      <slot />
 | 
					      <slot />
 | 
				
			||||||
      <div
 | 
					      <div
 | 
				
			||||||
        class="bg-n-slate-3 rounded-md dark:bg-n-solid-3 absolute left-0 right-0 z-20 mx-auto mt-0 max-w-max bottom-2 backdrop-blur[100px]"
 | 
					        v-if="shrink"
 | 
				
			||||||
 | 
					        class="absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t to-transparent from-n-background flex items-end justify-center pb-2"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <woot-button
 | 
					        <NextButton
 | 
				
			||||||
          v-if="shrink"
 | 
					          :label="$t('SEARCH.READ_MORE')"
 | 
				
			||||||
          size="tiny"
 | 
					          icon="i-lucide-chevrons-down"
 | 
				
			||||||
          variant="smooth"
 | 
					          blue
 | 
				
			||||||
          color-scheme="primary"
 | 
					          xs
 | 
				
			||||||
 | 
					          faded
 | 
				
			||||||
 | 
					          class="backdrop-filter backdrop-blur-[2px]"
 | 
				
			||||||
          @click.prevent="$emit('expand')"
 | 
					          @click.prevent="$emit('expand')"
 | 
				
			||||||
        >
 | 
					        />
 | 
				
			||||||
          {{ $t('SEARCH.READ_MORE') }}
 | 
					 | 
				
			||||||
        </woot-button>
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,52 +1,51 @@
 | 
				
			|||||||
<script>
 | 
					<script setup>
 | 
				
			||||||
export default {
 | 
					import { ref, useTemplateRef, onMounted, onUnmounted } from 'vue';
 | 
				
			||||||
  emits: ['search'],
 | 
					import { debounce } from '@chatwoot/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  data() {
 | 
					const emit = defineEmits(['search']);
 | 
				
			||||||
    return {
 | 
					
 | 
				
			||||||
      searchQuery: '',
 | 
					const searchQuery = ref('');
 | 
				
			||||||
      isInputFocused: false,
 | 
					const isInputFocused = ref(false);
 | 
				
			||||||
    };
 | 
					
 | 
				
			||||||
  },
 | 
					const searchInput = useTemplateRef('searchInput');
 | 
				
			||||||
  mounted() {
 | 
					
 | 
				
			||||||
    this.$refs.searchInput.focus();
 | 
					const handler = e => {
 | 
				
			||||||
    document.addEventListener('keydown', this.handler);
 | 
					  if (e.key === '/' && document.activeElement.tagName !== 'INPUT') {
 | 
				
			||||||
  },
 | 
					    e.preventDefault();
 | 
				
			||||||
  unmounted() {
 | 
					    searchInput.value.focus();
 | 
				
			||||||
    document.removeEventListener('keydown', this.handler);
 | 
					  } else if (e.key === 'Escape' && document.activeElement.tagName === 'INPUT') {
 | 
				
			||||||
  },
 | 
					    e.preventDefault();
 | 
				
			||||||
  methods: {
 | 
					    searchInput.value.blur();
 | 
				
			||||||
    handler(e) {
 | 
					  }
 | 
				
			||||||
      if (e.key === '/' && document.activeElement.tagName !== 'INPUT') {
 | 
					 | 
				
			||||||
        e.preventDefault();
 | 
					 | 
				
			||||||
        this.$refs.searchInput.focus();
 | 
					 | 
				
			||||||
      } else if (
 | 
					 | 
				
			||||||
        e.key === 'Escape' &&
 | 
					 | 
				
			||||||
        document.activeElement.tagName === 'INPUT'
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        e.preventDefault();
 | 
					 | 
				
			||||||
        this.$refs.searchInput.blur();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    debounceSearch(e) {
 | 
					 | 
				
			||||||
      this.searchQuery = e.target.value;
 | 
					 | 
				
			||||||
      clearTimeout(this.debounce);
 | 
					 | 
				
			||||||
      this.debounce = setTimeout(async () => {
 | 
					 | 
				
			||||||
        if (this.searchQuery.length > 2 || this.searchQuery.match(/^[0-9]+$/)) {
 | 
					 | 
				
			||||||
          this.$emit('search', this.searchQuery);
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          this.$emit('search', '');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }, 500);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    onFocus() {
 | 
					 | 
				
			||||||
      this.isInputFocused = true;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    onBlur() {
 | 
					 | 
				
			||||||
      this.isInputFocused = false;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const debouncedEmit = debounce(
 | 
				
			||||||
 | 
					  value =>
 | 
				
			||||||
 | 
					    emit('search', value.length > 1 || value.match(/^[0-9]+$/) ? value : ''),
 | 
				
			||||||
 | 
					  500
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onInput = e => {
 | 
				
			||||||
 | 
					  searchQuery.value = e.target.value;
 | 
				
			||||||
 | 
					  debouncedEmit(searchQuery.value);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onFocus = () => {
 | 
				
			||||||
 | 
					  isInputFocused.value = true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onBlur = () => {
 | 
				
			||||||
 | 
					  isInputFocused.value = false;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					  searchInput.value.focus();
 | 
				
			||||||
 | 
					  document.addEventListener('keydown', handler);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onUnmounted(() => {
 | 
				
			||||||
 | 
					  document.removeEventListener('keydown', handler);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
@@ -76,7 +75,7 @@ export default {
 | 
				
			|||||||
      :value="searchQuery"
 | 
					      :value="searchQuery"
 | 
				
			||||||
      @focus="onFocus"
 | 
					      @focus="onFocus"
 | 
				
			||||||
      @blur="onBlur"
 | 
					      @blur="onBlur"
 | 
				
			||||||
      @input="debounceSearch"
 | 
					      @input="onInput"
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
    <woot-label
 | 
					    <woot-label
 | 
				
			||||||
      :title="$t('SEARCH.PLACEHOLDER_KEYBINDING')"
 | 
					      :title="$t('SEARCH.PLACEHOLDER_KEYBINDING')"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,35 +0,0 @@
 | 
				
			|||||||
<template>
 | 
					 | 
				
			||||||
  <div class="search-input-box">
 | 
					 | 
				
			||||||
    <woot-button
 | 
					 | 
				
			||||||
      class="hollow"
 | 
					 | 
				
			||||||
      size="small"
 | 
					 | 
				
			||||||
      color-scheme="secondary"
 | 
					 | 
				
			||||||
      is-expanded
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <div class="search-input">
 | 
					 | 
				
			||||||
        <fluent-icon icon="search" size="14px" class="search--icon" />
 | 
					 | 
				
			||||||
        <span
 | 
					 | 
				
			||||||
          class="overflow-hidden text-ellipsis whitespace-nowrap search-placeholder"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          {{ $t('CONVERSATION.SEARCH_MESSAGES') }}
 | 
					 | 
				
			||||||
        </span>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </woot-button>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="scss" scoped>
 | 
					 | 
				
			||||||
.search-input-box {
 | 
					 | 
				
			||||||
  @apply p-2;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.search--icon {
 | 
					 | 
				
			||||||
  @apply flex-shrink-0 text-slate-500 dark:text-slate-300;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.search-placeholder {
 | 
					 | 
				
			||||||
  @apply text-slate-500 dark:text-slate-300;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.search-input {
 | 
					 | 
				
			||||||
  @apply flex justify-center items-center gap-1;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
<script setup>
 | 
					<script setup>
 | 
				
			||||||
import { frontendURL } from 'dashboard/helper/URLHelper';
 | 
					 | 
				
			||||||
import { computed } from 'vue';
 | 
					import { computed } from 'vue';
 | 
				
			||||||
 | 
					import { frontendURL } from 'dashboard/helper/URLHelper';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps({
 | 
					const props = defineProps({
 | 
				
			||||||
  id: {
 | 
					  id: {
 | 
				
			||||||
@@ -37,28 +39,27 @@ const navigateTo = computed(() => {
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <router-link
 | 
					  <router-link
 | 
				
			||||||
    :to="navigateTo"
 | 
					    :to="navigateTo"
 | 
				
			||||||
    class="flex items-center p-2 rounded-md cursor-pointer hover:bg-n-slate-3 dark:hover:bg-n-solid-3"
 | 
					    class="flex items-start p-2 rounded-xl cursor-pointer hover:bg-n-slate-2"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <woot-thumbnail :src="thumbnail" :username="name" size="24px" />
 | 
					    <Avatar
 | 
				
			||||||
    <div class="ml-2 rtl:mr-2 rtl:ml-0">
 | 
					      :name="name"
 | 
				
			||||||
      <h5 class="text-sm name text-n-slate-12 dark:text-n-slate-12">
 | 
					      :src="thumbnail"
 | 
				
			||||||
 | 
					      :size="24"
 | 
				
			||||||
 | 
					      rounded-full
 | 
				
			||||||
 | 
					      class="mt-0.5"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <div class="ml-2 rtl:mr-2 min-w-0 rtl:ml-0">
 | 
				
			||||||
 | 
					      <h5 class="text-sm name truncate min-w-0 text-n-slate-12">
 | 
				
			||||||
        {{ name }}
 | 
					        {{ name }}
 | 
				
			||||||
      </h5>
 | 
					      </h5>
 | 
				
			||||||
      <p
 | 
					      <p
 | 
				
			||||||
        class="flex items-center gap-1 m-0 text-sm text-slate-600 dark:text-slate-200"
 | 
					        class="grid items-center m-0 gap-1 text-sm grid-cols-[minmax(0,1fr)_auto_auto]"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <span v-if="email" class="email text-n-slate-12 dark:text-n-slate-12">{{
 | 
					        <span v-if="email" class="truncate text-n-slate-12" :title="email">
 | 
				
			||||||
          email
 | 
					          {{ email }}
 | 
				
			||||||
        }}</span>
 | 
					 | 
				
			||||||
        <span
 | 
					 | 
				
			||||||
          v-if="phone"
 | 
					 | 
				
			||||||
          class="separator text-n-slate-10 dark:text-n-slate-10"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          •
 | 
					 | 
				
			||||||
        </span>
 | 
					 | 
				
			||||||
        <span v-if="phone" class="phone text-n-slate-12 dark:text-n-slate-12">
 | 
					 | 
				
			||||||
          {{ phone }}
 | 
					 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
 | 
					        <span v-if="phone" class="text-n-slate-10">•</span>
 | 
				
			||||||
 | 
					        <span v-if="phone" class="text-n-slate-12">{{ phone }}</span>
 | 
				
			||||||
      </p>
 | 
					      </p>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </router-link>
 | 
					  </router-link>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,38 +1,29 @@
 | 
				
			|||||||
<script>
 | 
					<script setup>
 | 
				
			||||||
import { mapGetters } from 'vuex';
 | 
					import { useMapGetter } from 'dashboard/composables/store.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import SearchResultSection from './SearchResultSection.vue';
 | 
					import SearchResultSection from './SearchResultSection.vue';
 | 
				
			||||||
import SearchResultContactItem from './SearchResultContactItem.vue';
 | 
					import SearchResultContactItem from './SearchResultContactItem.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					defineProps({
 | 
				
			||||||
  components: {
 | 
					  contacts: {
 | 
				
			||||||
    SearchResultSection,
 | 
					    type: Array,
 | 
				
			||||||
    SearchResultContactItem,
 | 
					    default: () => [],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  query: {
 | 
				
			||||||
    contacts: {
 | 
					    type: String,
 | 
				
			||||||
      type: Array,
 | 
					    default: '',
 | 
				
			||||||
      default: () => [],
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    query: {
 | 
					 | 
				
			||||||
      type: String,
 | 
					 | 
				
			||||||
      default: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    isFetching: {
 | 
					 | 
				
			||||||
      type: Boolean,
 | 
					 | 
				
			||||||
      default: false,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    showTitle: {
 | 
					 | 
				
			||||||
      type: Boolean,
 | 
					 | 
				
			||||||
      default: true,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  isFetching: {
 | 
				
			||||||
    ...mapGetters({
 | 
					    type: Boolean,
 | 
				
			||||||
      accountId: 'getCurrentAccountId',
 | 
					    default: false,
 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					  showTitle: {
 | 
				
			||||||
 | 
					    type: Boolean,
 | 
				
			||||||
 | 
					    default: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const accountId = useMapGetter('getCurrentAccountId');
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
@@ -43,7 +34,7 @@ export default {
 | 
				
			|||||||
    :show-title="showTitle"
 | 
					    :show-title="showTitle"
 | 
				
			||||||
    :is-fetching="isFetching"
 | 
					    :is-fetching="isFetching"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <ul v-if="contacts.length" class="space-y-1.5">
 | 
					    <ul v-if="contacts.length" class="space-y-1.5 list-none">
 | 
				
			||||||
      <li v-for="contact in contacts" :key="contact.id">
 | 
					      <li v-for="contact in contacts" :key="contact.id">
 | 
				
			||||||
        <SearchResultContactItem
 | 
					        <SearchResultContactItem
 | 
				
			||||||
          :id="contact.id"
 | 
					          :id="contact.id"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,72 +1,69 @@
 | 
				
			|||||||
<script>
 | 
					<script setup>
 | 
				
			||||||
 | 
					import { computed } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { frontendURL } from 'dashboard/helper/URLHelper.js';
 | 
					import { frontendURL } from 'dashboard/helper/URLHelper.js';
 | 
				
			||||||
import { dynamicTime } from 'shared/helpers/timeHelper';
 | 
					import { dynamicTime } from 'shared/helpers/timeHelper';
 | 
				
			||||||
import InboxName from 'dashboard/components/widgets/InboxName.vue';
 | 
					import InboxName from 'dashboard/components/widgets/InboxName.vue';
 | 
				
			||||||
 | 
					import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					const props = defineProps({
 | 
				
			||||||
  components: {
 | 
					  id: {
 | 
				
			||||||
    InboxName,
 | 
					    type: Number,
 | 
				
			||||||
 | 
					    default: 0,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  inbox: {
 | 
				
			||||||
    id: {
 | 
					    type: Object,
 | 
				
			||||||
      type: Number,
 | 
					    default: () => ({}),
 | 
				
			||||||
      default: 0,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    inbox: {
 | 
					 | 
				
			||||||
      type: Object,
 | 
					 | 
				
			||||||
      default: () => ({}),
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    name: {
 | 
					 | 
				
			||||||
      type: String,
 | 
					 | 
				
			||||||
      default: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    email: {
 | 
					 | 
				
			||||||
      type: String,
 | 
					 | 
				
			||||||
      default: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    accountId: {
 | 
					 | 
				
			||||||
      type: [String, Number],
 | 
					 | 
				
			||||||
      default: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    createdAt: {
 | 
					 | 
				
			||||||
      type: [String, Date, Number],
 | 
					 | 
				
			||||||
      default: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    messageId: {
 | 
					 | 
				
			||||||
      type: Number,
 | 
					 | 
				
			||||||
      default: 0,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  name: {
 | 
				
			||||||
    navigateTo() {
 | 
					    type: String,
 | 
				
			||||||
      const params = {};
 | 
					    default: '',
 | 
				
			||||||
      if (this.messageId) {
 | 
					 | 
				
			||||||
        params.messageId = this.messageId;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return frontendURL(
 | 
					 | 
				
			||||||
        `accounts/${this.accountId}/conversations/${this.id}`,
 | 
					 | 
				
			||||||
        params
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    createdAtTime() {
 | 
					 | 
				
			||||||
      return dynamicTime(this.createdAt);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					  email: {
 | 
				
			||||||
 | 
					    type: String,
 | 
				
			||||||
 | 
					    default: '',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  accountId: {
 | 
				
			||||||
 | 
					    type: [String, Number],
 | 
				
			||||||
 | 
					    default: '',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  createdAt: {
 | 
				
			||||||
 | 
					    type: [String, Date, Number],
 | 
				
			||||||
 | 
					    default: '',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  messageId: {
 | 
				
			||||||
 | 
					    type: Number,
 | 
				
			||||||
 | 
					    default: 0,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const navigateTo = computed(() => {
 | 
				
			||||||
 | 
					  const params = {};
 | 
				
			||||||
 | 
					  if (props.messageId) {
 | 
				
			||||||
 | 
					    params.messageId = props.messageId;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return frontendURL(
 | 
				
			||||||
 | 
					    `accounts/${props.accountId}/conversations/${props.id}`,
 | 
				
			||||||
 | 
					    params
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const createdAtTime = dynamicTime(props.createdAt);
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <router-link
 | 
					  <router-link
 | 
				
			||||||
    :to="navigateTo"
 | 
					    :to="navigateTo"
 | 
				
			||||||
    class="flex p-2 rounded-md cursor-pointer hover:bg-n-slate-3 dark:hover:bg-n-solid-3"
 | 
					    class="flex p-2 rounded-xl cursor-pointer hover:bg-n-slate-2"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <div
 | 
					    <Avatar
 | 
				
			||||||
      class="flex items-center justify-center flex-shrink-0 w-6 h-6 rounded bg-n-brand/10 dark:bg-n-brand/40 text-n-blue-text dark:text-n-blue-text"
 | 
					      name="chats"
 | 
				
			||||||
    >
 | 
					      :size="24"
 | 
				
			||||||
      <fluent-icon icon="chat-multiple" :size="14" />
 | 
					      icon-name="i-lucide-messages-square"
 | 
				
			||||||
    </div>
 | 
					      class="[&>span]:rounded"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
    <div class="flex-grow min-w-0 ml-2">
 | 
					    <div class="flex-grow min-w-0 ml-2">
 | 
				
			||||||
      <div class="flex items-center justify-between mb-1">
 | 
					      <div class="flex items-center min-w-0 justify-between gap-1 mb-1">
 | 
				
			||||||
        <div class="flex">
 | 
					        <div class="flex">
 | 
				
			||||||
          <woot-label
 | 
					          <woot-label
 | 
				
			||||||
            class="!bg-n-slate-3 dark:!bg-n-solid-3 !border-n-weak dark:!border-n-strong m-0"
 | 
					            class="!bg-n-slate-3 dark:!bg-n-solid-3 !border-n-weak dark:!border-n-strong m-0"
 | 
				
			||||||
@@ -83,29 +80,25 @@ export default {
 | 
				
			|||||||
            />
 | 
					            />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div>
 | 
					        <span
 | 
				
			||||||
          <span
 | 
					          class="text-xs font-normal min-w-0 truncate text-n-slate-11 dark:text-n-slate-11"
 | 
				
			||||||
            class="text-xs font-normal text-n-slate-11 dark:text-n-slate-11"
 | 
					        >
 | 
				
			||||||
          >
 | 
					          {{ createdAtTime }}
 | 
				
			||||||
            {{ createdAtTime }}
 | 
					        </span>
 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="flex gap-2">
 | 
					      <div class="flex gap-2">
 | 
				
			||||||
        <h5
 | 
					        <h5
 | 
				
			||||||
          v-if="name"
 | 
					          v-if="name"
 | 
				
			||||||
          class="m-0 text-sm text-n-slate-12 dark:text-n-slate-12"
 | 
					          class="m-0 text-sm min-w-0 truncate text-n-slate-12 dark:text-n-slate-12"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <span
 | 
					          <span class="text-xs font-norma text-n-slate-11 dark:text-n-slate-11">
 | 
				
			||||||
            class="text-xs font-normal text-n-slate-11 dark:text-n-slate-11"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            {{ $t('SEARCH.FROM') }}:
 | 
					            {{ $t('SEARCH.FROM') }}:
 | 
				
			||||||
          </span>
 | 
					          </span>
 | 
				
			||||||
          {{ name }}
 | 
					          {{ name }}
 | 
				
			||||||
        </h5>
 | 
					        </h5>
 | 
				
			||||||
        <h5
 | 
					        <h5
 | 
				
			||||||
          v-if="email"
 | 
					          v-if="email"
 | 
				
			||||||
          class="m-0 overflow-hidden text-sm text-n-slate-12 dark:text-n-slate-12 whitespace-nowrap text-ellipsis"
 | 
					          class="m-0 overflow-hidden text-sm text-n-slate-12 dark:text-n-slate-12 truncate"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <span
 | 
					          <span
 | 
				
			||||||
            class="text-xs font-normal text-n-slate-11 dark:text-n-slate-11"
 | 
					            class="text-xs font-normal text-n-slate-11 dark:text-n-slate-11"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,37 +1,28 @@
 | 
				
			|||||||
<script>
 | 
					<script setup>
 | 
				
			||||||
import { mapGetters } from 'vuex';
 | 
					import { useMapGetter } from 'dashboard/composables/store.js';
 | 
				
			||||||
import SearchResultSection from './SearchResultSection.vue';
 | 
					import SearchResultSection from './SearchResultSection.vue';
 | 
				
			||||||
import SearchResultConversationItem from './SearchResultConversationItem.vue';
 | 
					import SearchResultConversationItem from './SearchResultConversationItem.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					defineProps({
 | 
				
			||||||
  components: {
 | 
					  conversations: {
 | 
				
			||||||
    SearchResultSection,
 | 
					    type: Array,
 | 
				
			||||||
    SearchResultConversationItem,
 | 
					    default: () => [],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  query: {
 | 
				
			||||||
    conversations: {
 | 
					    type: String,
 | 
				
			||||||
      type: Array,
 | 
					    default: '',
 | 
				
			||||||
      default: () => [],
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    query: {
 | 
					 | 
				
			||||||
      type: String,
 | 
					 | 
				
			||||||
      default: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    isFetching: {
 | 
					 | 
				
			||||||
      type: Boolean,
 | 
					 | 
				
			||||||
      default: false,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    showTitle: {
 | 
					 | 
				
			||||||
      type: Boolean,
 | 
					 | 
				
			||||||
      default: true,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  isFetching: {
 | 
				
			||||||
    ...mapGetters({
 | 
					    type: Boolean,
 | 
				
			||||||
      accountId: 'getCurrentAccountId',
 | 
					    default: false,
 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					  showTitle: {
 | 
				
			||||||
 | 
					    type: Boolean,
 | 
				
			||||||
 | 
					    default: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const accountId = useMapGetter('getCurrentAccountId');
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
@@ -39,10 +30,10 @@ export default {
 | 
				
			|||||||
    :title="$t('SEARCH.SECTION.CONVERSATIONS')"
 | 
					    :title="$t('SEARCH.SECTION.CONVERSATIONS')"
 | 
				
			||||||
    :empty="!conversations.length"
 | 
					    :empty="!conversations.length"
 | 
				
			||||||
    :query="query"
 | 
					    :query="query"
 | 
				
			||||||
    :show-title="showTitle || isFetching"
 | 
					    :show-title="showTitle"
 | 
				
			||||||
    :is-fetching="isFetching"
 | 
					    :is-fetching="isFetching"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <ul v-if="conversations.length" class="space-y-1.5">
 | 
					    <ul v-if="conversations.length" class="space-y-1.5 list-none">
 | 
				
			||||||
      <li v-for="conversation in conversations" :key="conversation.id">
 | 
					      <li v-for="conversation in conversations" :key="conversation.id">
 | 
				
			||||||
        <SearchResultConversationItem
 | 
					        <SearchResultConversationItem
 | 
				
			||||||
          :id="conversation.id"
 | 
					          :id="conversation.id"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,45 +1,37 @@
 | 
				
			|||||||
<script>
 | 
					<script setup>
 | 
				
			||||||
import { mapGetters } from 'vuex';
 | 
					import { useI18n } from 'vue-i18n';
 | 
				
			||||||
 | 
					import { useMapGetter } from 'dashboard/composables/store.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import SearchResultConversationItem from './SearchResultConversationItem.vue';
 | 
					import SearchResultConversationItem from './SearchResultConversationItem.vue';
 | 
				
			||||||
import SearchResultSection from './SearchResultSection.vue';
 | 
					import SearchResultSection from './SearchResultSection.vue';
 | 
				
			||||||
import MessageContent from './MessageContent.vue';
 | 
					import MessageContent from './MessageContent.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					defineProps({
 | 
				
			||||||
  components: {
 | 
					  messages: {
 | 
				
			||||||
    SearchResultConversationItem,
 | 
					    type: Array,
 | 
				
			||||||
    SearchResultSection,
 | 
					    default: () => [],
 | 
				
			||||||
    MessageContent,
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  query: {
 | 
				
			||||||
    messages: {
 | 
					    type: String,
 | 
				
			||||||
      type: Array,
 | 
					    default: '',
 | 
				
			||||||
      default: () => [],
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    query: {
 | 
					 | 
				
			||||||
      type: String,
 | 
					 | 
				
			||||||
      default: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    isFetching: {
 | 
					 | 
				
			||||||
      type: Boolean,
 | 
					 | 
				
			||||||
      default: false,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    showTitle: {
 | 
					 | 
				
			||||||
      type: Boolean,
 | 
					 | 
				
			||||||
      default: true,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  isFetching: {
 | 
				
			||||||
    ...mapGetters({
 | 
					    type: Boolean,
 | 
				
			||||||
      accountId: 'getCurrentAccountId',
 | 
					    default: false,
 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  showTitle: {
 | 
				
			||||||
    getName(message) {
 | 
					    type: Boolean,
 | 
				
			||||||
      return message && message.sender && message.sender.name
 | 
					    default: true,
 | 
				
			||||||
        ? message.sender.name
 | 
					 | 
				
			||||||
        : this.$t('SEARCH.BOT_LABEL');
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					const { t } = useI18n();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const accountId = useMapGetter('getCurrentAccountId');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getName = message => {
 | 
				
			||||||
 | 
					  return message && message.sender && message.sender.name
 | 
				
			||||||
 | 
					    ? message.sender.name
 | 
				
			||||||
 | 
					    : t('SEARCH.BOT_LABEL');
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -51,7 +43,7 @@ export default {
 | 
				
			|||||||
    :show-title="showTitle"
 | 
					    :show-title="showTitle"
 | 
				
			||||||
    :is-fetching="isFetching"
 | 
					    :is-fetching="isFetching"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <ul v-if="messages.length" class="space-y-1.5">
 | 
					    <ul v-if="messages.length" class="space-y-1.5 list-none">
 | 
				
			||||||
      <li v-for="message in messages" :key="message.id">
 | 
					      <li v-for="message in messages" :key="message.id">
 | 
				
			||||||
        <SearchResultConversationItem
 | 
					        <SearchResultConversationItem
 | 
				
			||||||
          :id="message.conversation_id"
 | 
					          :id="message.conversation_id"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,14 +32,14 @@ const titleCase = computed(() => props.title.toLowerCase());
 | 
				
			|||||||
    <div v-if="showTitle" class="sticky top-0 p-2 z-50 mb-0.5 bg-n-background">
 | 
					    <div v-if="showTitle" class="sticky top-0 p-2 z-50 mb-0.5 bg-n-background">
 | 
				
			||||||
      <h3 class="text-sm text-n-slate-12">{{ title }}</h3>
 | 
					      <h3 class="text-sm text-n-slate-12">{{ title }}</h3>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					    <slot />
 | 
				
			||||||
    <woot-loading-state
 | 
					    <woot-loading-state
 | 
				
			||||||
      v-if="isFetching"
 | 
					      v-if="isFetching"
 | 
				
			||||||
      :message="$t('SEARCH.SEARCHING_DATA')"
 | 
					      :message="empty ? $t('SEARCH.SEARCHING_DATA') : $t('SEARCH.LOADING_DATA')"
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
    <slot v-else />
 | 
					 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      v-if="empty && !isFetching"
 | 
					      v-if="empty && !isFetching"
 | 
				
			||||||
      class="flex items-center justify-center px-4 py-6 m-2 rounded-md bg-n-slate-2 dark:bg-n-solid-3"
 | 
					      class="flex items-center justify-center px-4 py-6 m-2 rounded-xl bg-n-slate-2 dark:bg-n-solid-1"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <fluent-icon icon="info" size="16px" class="text-n-slate-11" />
 | 
					      <fluent-icon icon="info" size="16px" class="text-n-slate-11" />
 | 
				
			||||||
      <p class="mx-2 my-0 text-center text-n-slate-11">
 | 
					      <p class="mx-2 my-0 text-center text-n-slate-11">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,39 +1,38 @@
 | 
				
			|||||||
<script>
 | 
					<script setup>
 | 
				
			||||||
export default {
 | 
					import { ref, watch } from 'vue';
 | 
				
			||||||
  props: {
 | 
					
 | 
				
			||||||
    tabs: {
 | 
					const props = defineProps({
 | 
				
			||||||
      type: Array,
 | 
					  tabs: {
 | 
				
			||||||
      default: () => [],
 | 
					    type: Array,
 | 
				
			||||||
    },
 | 
					    default: () => [],
 | 
				
			||||||
    selectedTab: {
 | 
					 | 
				
			||||||
      type: Number,
 | 
					 | 
				
			||||||
      default: 0,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  emits: ['tabChange'],
 | 
					  selectedTab: {
 | 
				
			||||||
  data() {
 | 
					    type: Number,
 | 
				
			||||||
    return {
 | 
					    default: 0,
 | 
				
			||||||
      activeTab: 0,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  watch: {
 | 
					 | 
				
			||||||
    selectedTab(value, oldValue) {
 | 
					 | 
				
			||||||
      if (value !== oldValue) {
 | 
					 | 
				
			||||||
        this.activeTab = this.selectedTab;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  methods: {
 | 
					 | 
				
			||||||
    onTabChange(index) {
 | 
					 | 
				
			||||||
      this.activeTab = index;
 | 
					 | 
				
			||||||
      this.$emit('tabChange', this.tabs[index].key);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits(['tabChange']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const activeTab = ref(props.selectedTab);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
					  () => props.selectedTab,
 | 
				
			||||||
 | 
					  (value, oldValue) => {
 | 
				
			||||||
 | 
					    if (value !== oldValue) {
 | 
				
			||||||
 | 
					      activeTab.value = props.selectedTab;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onTabChange = index => {
 | 
				
			||||||
 | 
					  activeTab.value = index;
 | 
				
			||||||
 | 
					  emit('tabChange', props.tabs[index].key);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="tab-container">
 | 
					  <div class="mt-1 border-b border-n-weak">
 | 
				
			||||||
    <woot-tabs :index="activeTab" :border="false" @change="onTabChange">
 | 
					    <woot-tabs :index="activeTab" :border="false" @change="onTabChange">
 | 
				
			||||||
      <woot-tabs-item
 | 
					      <woot-tabs-item
 | 
				
			||||||
        v-for="(item, index) in tabs"
 | 
					        v-for="(item, index) in tabs"
 | 
				
			||||||
@@ -41,13 +40,8 @@ export default {
 | 
				
			|||||||
        :index="index"
 | 
					        :index="index"
 | 
				
			||||||
        :name="item.name"
 | 
					        :name="item.name"
 | 
				
			||||||
        :count="item.count"
 | 
					        :count="item.count"
 | 
				
			||||||
 | 
					        :show-badge="item.showBadge"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    </woot-tabs>
 | 
					    </woot-tabs>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="scss" scoped>
 | 
					 | 
				
			||||||
.tab-container {
 | 
					 | 
				
			||||||
  @apply mt-1 border-b border-solid border-slate-100 dark:border-slate-800/50;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,9 @@
 | 
				
			|||||||
<script>
 | 
					<script setup>
 | 
				
			||||||
import SearchHeader from './SearchHeader.vue';
 | 
					import { ref, computed, onMounted, onUnmounted } from 'vue';
 | 
				
			||||||
import SearchTabs from './SearchTabs.vue';
 | 
					import { useMapGetter, useStore } from 'dashboard/composables/store.js';
 | 
				
			||||||
import SearchResultConversationsList from './SearchResultConversationsList.vue';
 | 
					import { useRouter } from 'vue-router';
 | 
				
			||||||
import SearchResultMessagesList from './SearchResultMessagesList.vue';
 | 
					 | 
				
			||||||
import SearchResultContactsList from './SearchResultContactsList.vue';
 | 
					 | 
				
			||||||
import ButtonV4 from 'dashboard/components-next/button/Button.vue';
 | 
					 | 
				
			||||||
import { useTrack } from 'dashboard/composables';
 | 
					import { useTrack } from 'dashboard/composables';
 | 
				
			||||||
import Policy from 'dashboard/components/policy.vue';
 | 
					import { useI18n } from 'vue-i18n';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ROLES,
 | 
					  ROLES,
 | 
				
			||||||
  CONVERSATION_PERMISSIONS,
 | 
					  CONVERSATION_PERMISSIONS,
 | 
				
			||||||
@@ -16,197 +13,233 @@ import {
 | 
				
			|||||||
  getUserPermissions,
 | 
					  getUserPermissions,
 | 
				
			||||||
  filterItemsByPermission,
 | 
					  filterItemsByPermission,
 | 
				
			||||||
} from 'dashboard/helper/permissionsHelper.js';
 | 
					} from 'dashboard/helper/permissionsHelper.js';
 | 
				
			||||||
 | 
					 | 
				
			||||||
import { mapGetters } from 'vuex';
 | 
					 | 
				
			||||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
 | 
					import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					import Policy from 'dashboard/components/policy.vue';
 | 
				
			||||||
  components: {
 | 
					import NextButton from 'dashboard/components-next/button/Button.vue';
 | 
				
			||||||
    SearchHeader,
 | 
					import SearchHeader from './SearchHeader.vue';
 | 
				
			||||||
    SearchTabs,
 | 
					import SearchTabs from './SearchTabs.vue';
 | 
				
			||||||
    SearchResultContactsList,
 | 
					import SearchResultConversationsList from './SearchResultConversationsList.vue';
 | 
				
			||||||
    SearchResultConversationsList,
 | 
					import SearchResultMessagesList from './SearchResultMessagesList.vue';
 | 
				
			||||||
    SearchResultMessagesList,
 | 
					import SearchResultContactsList from './SearchResultContactsList.vue';
 | 
				
			||||||
    Policy,
 | 
					 | 
				
			||||||
    ButtonV4,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  data() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      selectedTab: 'all',
 | 
					 | 
				
			||||||
      query: '',
 | 
					 | 
				
			||||||
      contactPermissions: CONTACT_PERMISSIONS,
 | 
					 | 
				
			||||||
      conversationPermissions: CONVERSATION_PERMISSIONS,
 | 
					 | 
				
			||||||
      rolePermissions: ROLES,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  computed: {
 | 
					const router = useRouter();
 | 
				
			||||||
    ...mapGetters({
 | 
					const store = useStore();
 | 
				
			||||||
      currentUser: 'getCurrentUser',
 | 
					const { t } = useI18n();
 | 
				
			||||||
      currentAccountId: 'getCurrentAccountId',
 | 
					 | 
				
			||||||
      contactRecords: 'conversationSearch/getContactRecords',
 | 
					 | 
				
			||||||
      conversationRecords: 'conversationSearch/getConversationRecords',
 | 
					 | 
				
			||||||
      messageRecords: 'conversationSearch/getMessageRecords',
 | 
					 | 
				
			||||||
      uiFlags: 'conversationSearch/getUIFlags',
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
    contacts() {
 | 
					 | 
				
			||||||
      return this.contactRecords.map(contact => ({
 | 
					 | 
				
			||||||
        ...contact,
 | 
					 | 
				
			||||||
        type: 'contact',
 | 
					 | 
				
			||||||
      }));
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    conversations() {
 | 
					 | 
				
			||||||
      return this.conversationRecords.map(conversation => ({
 | 
					 | 
				
			||||||
        ...conversation,
 | 
					 | 
				
			||||||
        type: 'conversation',
 | 
					 | 
				
			||||||
      }));
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    messages() {
 | 
					 | 
				
			||||||
      return this.messageRecords.map(message => ({
 | 
					 | 
				
			||||||
        ...message,
 | 
					 | 
				
			||||||
        type: 'message',
 | 
					 | 
				
			||||||
      }));
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    all() {
 | 
					 | 
				
			||||||
      return [...this.contacts, ...this.conversations, ...this.messages];
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    filterContacts() {
 | 
					 | 
				
			||||||
      return this.selectedTab === 'contacts' || this.isSelectedTabAll;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    filterConversations() {
 | 
					 | 
				
			||||||
      return this.selectedTab === 'conversations' || this.isSelectedTabAll;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    filterMessages() {
 | 
					 | 
				
			||||||
      return this.selectedTab === 'messages' || this.isSelectedTabAll;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    userPermissions() {
 | 
					 | 
				
			||||||
      return getUserPermissions(this.currentUser, this.currentAccountId);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    totalSearchResultsCount() {
 | 
					 | 
				
			||||||
      const permissionCounts = {
 | 
					 | 
				
			||||||
        contacts: {
 | 
					 | 
				
			||||||
          permissions: [...this.rolePermissions, this.contactPermissions],
 | 
					 | 
				
			||||||
          count: () => this.contacts.length,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        conversations: {
 | 
					 | 
				
			||||||
          permissions: [
 | 
					 | 
				
			||||||
            ...this.rolePermissions,
 | 
					 | 
				
			||||||
            ...this.conversationPermissions,
 | 
					 | 
				
			||||||
          ],
 | 
					 | 
				
			||||||
          count: () => this.conversations.length + this.messages.length,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const filteredCounts = filterItemsByPermission(
 | 
					const PER_PAGE = 15; // Results per page
 | 
				
			||||||
        permissionCounts,
 | 
					const selectedTab = ref('all');
 | 
				
			||||||
        this.userPermissions,
 | 
					const query = ref('');
 | 
				
			||||||
        item => item.permissions,
 | 
					const pages = ref({
 | 
				
			||||||
        (_, item) => item.count
 | 
					  contacts: 1,
 | 
				
			||||||
      );
 | 
					  conversations: 1,
 | 
				
			||||||
 | 
					  messages: 1,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return filteredCounts.reduce((total, count) => total + count(), 0);
 | 
					const currentUser = useMapGetter('getCurrentUser');
 | 
				
			||||||
    },
 | 
					const currentAccountId = useMapGetter('getCurrentAccountId');
 | 
				
			||||||
    tabs() {
 | 
					const contactRecords = useMapGetter('conversationSearch/getContactRecords');
 | 
				
			||||||
      const allTabsConfig = {
 | 
					const conversationRecords = useMapGetter(
 | 
				
			||||||
        all: {
 | 
					  'conversationSearch/getConversationRecords'
 | 
				
			||||||
          key: 'all',
 | 
					);
 | 
				
			||||||
          name: this.$t('SEARCH.TABS.ALL'),
 | 
					const messageRecords = useMapGetter('conversationSearch/getMessageRecords');
 | 
				
			||||||
          count: this.totalSearchResultsCount,
 | 
					const uiFlags = useMapGetter('conversationSearch/getUIFlags');
 | 
				
			||||||
          permissions: [
 | 
					 | 
				
			||||||
            this.contactPermissions,
 | 
					 | 
				
			||||||
            ...this.rolePermissions,
 | 
					 | 
				
			||||||
            ...this.conversationPermissions,
 | 
					 | 
				
			||||||
          ],
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        contacts: {
 | 
					 | 
				
			||||||
          key: 'contacts',
 | 
					 | 
				
			||||||
          name: this.$t('SEARCH.TABS.CONTACTS'),
 | 
					 | 
				
			||||||
          count: this.contacts.length,
 | 
					 | 
				
			||||||
          permissions: [...this.rolePermissions, this.contactPermissions],
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        conversations: {
 | 
					 | 
				
			||||||
          key: 'conversations',
 | 
					 | 
				
			||||||
          name: this.$t('SEARCH.TABS.CONVERSATIONS'),
 | 
					 | 
				
			||||||
          count: this.conversations.length,
 | 
					 | 
				
			||||||
          permissions: [
 | 
					 | 
				
			||||||
            ...this.rolePermissions,
 | 
					 | 
				
			||||||
            ...this.conversationPermissions,
 | 
					 | 
				
			||||||
          ],
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        messages: {
 | 
					 | 
				
			||||||
          key: 'messages',
 | 
					 | 
				
			||||||
          name: this.$t('SEARCH.TABS.MESSAGES'),
 | 
					 | 
				
			||||||
          count: this.messages.length,
 | 
					 | 
				
			||||||
          permissions: [
 | 
					 | 
				
			||||||
            ...this.rolePermissions,
 | 
					 | 
				
			||||||
            ...this.conversationPermissions,
 | 
					 | 
				
			||||||
          ],
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return filterItemsByPermission(
 | 
					const addTypeToRecords = (records, type) =>
 | 
				
			||||||
        allTabsConfig,
 | 
					  records.value.map(item => ({ ...item, type }));
 | 
				
			||||||
        this.userPermissions,
 | 
					
 | 
				
			||||||
        item => item.permissions
 | 
					const mappedContacts = computed(() =>
 | 
				
			||||||
      );
 | 
					  addTypeToRecords(contactRecords, 'contact')
 | 
				
			||||||
    },
 | 
					);
 | 
				
			||||||
    activeTabIndex() {
 | 
					const mappedConversations = computed(() =>
 | 
				
			||||||
      const index = this.tabs.findIndex(tab => tab.key === this.selectedTab);
 | 
					  addTypeToRecords(conversationRecords, 'conversation')
 | 
				
			||||||
      return index >= 0 ? index : 0;
 | 
					);
 | 
				
			||||||
    },
 | 
					const mappedMessages = computed(() =>
 | 
				
			||||||
    showEmptySearchResults() {
 | 
					  addTypeToRecords(messageRecords, 'message')
 | 
				
			||||||
      return (
 | 
					);
 | 
				
			||||||
        this.totalSearchResultsCount === 0 &&
 | 
					
 | 
				
			||||||
        this.uiFlags.isSearchCompleted &&
 | 
					const isSelectedTabAll = computed(() => selectedTab.value === 'all');
 | 
				
			||||||
        !this.uiFlags.isFetching &&
 | 
					
 | 
				
			||||||
        this.query
 | 
					const sliceRecordsIfAllTab = items =>
 | 
				
			||||||
      );
 | 
					  isSelectedTabAll.value ? items.value.slice(0, 5) : items.value;
 | 
				
			||||||
    },
 | 
					
 | 
				
			||||||
    showResultsSection() {
 | 
					const contacts = computed(() => sliceRecordsIfAllTab(mappedContacts));
 | 
				
			||||||
      return (
 | 
					const conversations = computed(() => sliceRecordsIfAllTab(mappedConversations));
 | 
				
			||||||
        (this.uiFlags.isSearchCompleted &&
 | 
					const messages = computed(() => sliceRecordsIfAllTab(mappedMessages));
 | 
				
			||||||
          this.totalSearchResultsCount !== 0) ||
 | 
					
 | 
				
			||||||
        this.uiFlags.isFetching
 | 
					const filterByTab = tab =>
 | 
				
			||||||
      );
 | 
					  computed(() => selectedTab.value === tab || isSelectedTabAll.value);
 | 
				
			||||||
    },
 | 
					
 | 
				
			||||||
    isSelectedTabAll() {
 | 
					const filterContacts = filterByTab('contacts');
 | 
				
			||||||
      return this.selectedTab === 'all';
 | 
					const filterConversations = filterByTab('conversations');
 | 
				
			||||||
    },
 | 
					const filterMessages = filterByTab('messages');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const userPermissions = computed(() =>
 | 
				
			||||||
 | 
					  getUserPermissions(currentUser.value, currentAccountId.value)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TABS_CONFIG = {
 | 
				
			||||||
 | 
					  all: {
 | 
				
			||||||
 | 
					    permissions: [CONTACT_PERMISSIONS, ...ROLES, ...CONVERSATION_PERMISSIONS],
 | 
				
			||||||
 | 
					    count: () => null, // No count for all tab
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  unmounted() {
 | 
					  contacts: {
 | 
				
			||||||
    this.query = '';
 | 
					    permissions: [...ROLES, CONTACT_PERMISSIONS],
 | 
				
			||||||
    this.$store.dispatch('conversationSearch/clearSearchResults');
 | 
					    count: () => mappedContacts.value.length,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {
 | 
					  conversations: {
 | 
				
			||||||
    this.$store.dispatch('conversationSearch/clearSearchResults');
 | 
					    permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
 | 
				
			||||||
 | 
					    count: () => mappedConversations.value.length,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  messages: {
 | 
				
			||||||
    onSearch(q) {
 | 
					    permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
 | 
				
			||||||
      this.selectedTab = 'all';
 | 
					    count: () => mappedMessages.value.length,
 | 
				
			||||||
      this.query = q;
 | 
					 | 
				
			||||||
      if (!q) {
 | 
					 | 
				
			||||||
        this.$store.dispatch('conversationSearch/clearSearchResults');
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      useTrack(CONVERSATION_EVENTS.SEARCH_CONVERSATION);
 | 
					 | 
				
			||||||
      this.$store.dispatch('conversationSearch/fullSearch', { q });
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    onBack() {
 | 
					 | 
				
			||||||
      if (window.history.length > 2) {
 | 
					 | 
				
			||||||
        this.$router.go(-1);
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        this.$router.push({ name: 'home' });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const tabs = computed(() => {
 | 
				
			||||||
 | 
					  const configs = Object.entries(TABS_CONFIG).map(([key, config]) => ({
 | 
				
			||||||
 | 
					    key,
 | 
				
			||||||
 | 
					    name: t(`SEARCH.TABS.${key.toUpperCase()}`),
 | 
				
			||||||
 | 
					    count: config.count(),
 | 
				
			||||||
 | 
					    showBadge: key !== 'all',
 | 
				
			||||||
 | 
					    permissions: config.permissions,
 | 
				
			||||||
 | 
					  }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return filterItemsByPermission(
 | 
				
			||||||
 | 
					    configs,
 | 
				
			||||||
 | 
					    userPermissions.value,
 | 
				
			||||||
 | 
					    item => item.permissions
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const totalSearchResultsCount = computed(() => {
 | 
				
			||||||
 | 
					  const permissionCounts = {
 | 
				
			||||||
 | 
					    contacts: {
 | 
				
			||||||
 | 
					      permissions: [...ROLES, CONTACT_PERMISSIONS],
 | 
				
			||||||
 | 
					      count: () => contacts.value.length,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    conversations: {
 | 
				
			||||||
 | 
					      permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
 | 
				
			||||||
 | 
					      count: () => conversations.value.length + messages.value.length,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return filterItemsByPermission(
 | 
				
			||||||
 | 
					    permissionCounts,
 | 
				
			||||||
 | 
					    userPermissions.value,
 | 
				
			||||||
 | 
					    item => item.permissions,
 | 
				
			||||||
 | 
					    (_, item) => item.count
 | 
				
			||||||
 | 
					  ).reduce((total, count) => total + count(), 0);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const activeTabIndex = computed(() => {
 | 
				
			||||||
 | 
					  const index = tabs.value.findIndex(tab => tab.key === selectedTab.value);
 | 
				
			||||||
 | 
					  return index >= 0 ? index : 0;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isFetchingAny = computed(() => {
 | 
				
			||||||
 | 
					  const { contact, message, conversation, isFetching } = uiFlags.value;
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    isFetching ||
 | 
				
			||||||
 | 
					    contact.isFetching ||
 | 
				
			||||||
 | 
					    message.isFetching ||
 | 
				
			||||||
 | 
					    conversation.isFetching
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const showEmptySearchResults = computed(
 | 
				
			||||||
 | 
					  () =>
 | 
				
			||||||
 | 
					    totalSearchResultsCount.value === 0 &&
 | 
				
			||||||
 | 
					    uiFlags.value.isSearchCompleted &&
 | 
				
			||||||
 | 
					    isSelectedTabAll.value &&
 | 
				
			||||||
 | 
					    !isFetchingAny.value &&
 | 
				
			||||||
 | 
					    query.value
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const showResultsSection = computed(
 | 
				
			||||||
 | 
					  () =>
 | 
				
			||||||
 | 
					    (uiFlags.value.isSearchCompleted && totalSearchResultsCount.value !== 0) ||
 | 
				
			||||||
 | 
					    isFetchingAny.value ||
 | 
				
			||||||
 | 
					    (!isSelectedTabAll.value && query.value && !isFetchingAny.value)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const showLoadMore = computed(() => {
 | 
				
			||||||
 | 
					  if (!query.value || isFetchingAny.value || selectedTab.value === 'all')
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const records = {
 | 
				
			||||||
 | 
					    contacts: mappedContacts.value,
 | 
				
			||||||
 | 
					    conversations: mappedConversations.value,
 | 
				
			||||||
 | 
					    messages: mappedMessages.value,
 | 
				
			||||||
 | 
					  }[selectedTab.value];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    records?.length > 0 &&
 | 
				
			||||||
 | 
					    records.length === pages.value[selectedTab.value] * PER_PAGE
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const showViewMore = computed(() => ({
 | 
				
			||||||
 | 
					  // Hide view more button if the number of records is less than 5
 | 
				
			||||||
 | 
					  contacts: mappedContacts.value?.length > 5 && isSelectedTabAll.value,
 | 
				
			||||||
 | 
					  conversations:
 | 
				
			||||||
 | 
					    mappedConversations.value?.length > 5 && isSelectedTabAll.value,
 | 
				
			||||||
 | 
					  messages: mappedMessages.value?.length > 5 && isSelectedTabAll.value,
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const clearSearchResult = () => {
 | 
				
			||||||
 | 
					  pages.value = { contacts: 1, conversations: 1, messages: 1 };
 | 
				
			||||||
 | 
					  store.dispatch('conversationSearch/clearSearchResults');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onSearch = q => {
 | 
				
			||||||
 | 
					  query.value = q;
 | 
				
			||||||
 | 
					  clearSearchResult();
 | 
				
			||||||
 | 
					  if (!q) return;
 | 
				
			||||||
 | 
					  useTrack(CONVERSATION_EVENTS.SEARCH_CONVERSATION);
 | 
				
			||||||
 | 
					  store.dispatch('conversationSearch/fullSearch', { q, page: 1 });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onBack = () => {
 | 
				
			||||||
 | 
					  if (window.history.length > 2) {
 | 
				
			||||||
 | 
					    router.go(-1);
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    router.push({ name: 'home' });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  clearSearchResult();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const loadMore = () => {
 | 
				
			||||||
 | 
					  const SEARCH_ACTIONS = {
 | 
				
			||||||
 | 
					    contacts: 'conversationSearch/contactSearch',
 | 
				
			||||||
 | 
					    conversations: 'conversationSearch/conversationSearch',
 | 
				
			||||||
 | 
					    messages: 'conversationSearch/messageSearch',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (uiFlags.value.isFetching || selectedTab.value === 'all') return;
 | 
				
			||||||
 | 
					  const tab = selectedTab.value;
 | 
				
			||||||
 | 
					  pages.value[tab] += 1;
 | 
				
			||||||
 | 
					  store.dispatch(SEARCH_ACTIONS[tab], {
 | 
				
			||||||
 | 
					    q: query.value,
 | 
				
			||||||
 | 
					    page: pages.value[tab],
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					  store.dispatch('conversationSearch/clearSearchResults');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onUnmounted(() => {
 | 
				
			||||||
 | 
					  query.value = '';
 | 
				
			||||||
 | 
					  store.dispatch('conversationSearch/clearSearchResults');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="flex flex-col w-full bg-n-background">
 | 
					  <div class="flex flex-col w-full h-full bg-n-background">
 | 
				
			||||||
    <div class="flex p-4">
 | 
					    <div class="flex w-full p-4">
 | 
				
			||||||
      <ButtonV4
 | 
					      <NextButton
 | 
				
			||||||
        :label="$t('GENERAL_SETTINGS.BACK')"
 | 
					        :label="t('GENERAL_SETTINGS.BACK')"
 | 
				
			||||||
        icon="i-lucide-chevron-left"
 | 
					        icon="i-lucide-chevron-left"
 | 
				
			||||||
        faded
 | 
					        faded
 | 
				
			||||||
        primary
 | 
					        primary
 | 
				
			||||||
@@ -214,73 +247,119 @@ export default {
 | 
				
			|||||||
        @click="onBack"
 | 
					        @click="onBack"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <section
 | 
					    <section class="flex flex-col flex-grow w-full h-full overflow-hidden">
 | 
				
			||||||
      class="flex my-0 p-4 relative mx-auto max-w-[45rem] min-h-[20rem] flex-col w-full h-full bg-n-background"
 | 
					      <div class="w-full max-w-4xl mx-auto">
 | 
				
			||||||
    >
 | 
					        <div class="flex flex-col w-full px-4">
 | 
				
			||||||
      <header>
 | 
					          <SearchHeader @search="onSearch" />
 | 
				
			||||||
        <SearchHeader @search="onSearch" />
 | 
					          <SearchTabs
 | 
				
			||||||
        <SearchTabs
 | 
					            v-if="query"
 | 
				
			||||||
          v-if="query"
 | 
					            :tabs="tabs"
 | 
				
			||||||
          :tabs="tabs"
 | 
					            :selected-tab="activeTabIndex"
 | 
				
			||||||
          :selected-tab="activeTabIndex"
 | 
					            @tab-change="tab => (selectedTab = tab)"
 | 
				
			||||||
          @tab-change="tab => (selectedTab = tab)"
 | 
					          />
 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </header>
 | 
					 | 
				
			||||||
      <div class="flex-grow h-full px-2 py-0 overflow-y-auto">
 | 
					 | 
				
			||||||
        <div v-if="showResultsSection">
 | 
					 | 
				
			||||||
          <Policy :permissions="[...rolePermissions, contactPermissions]">
 | 
					 | 
				
			||||||
            <SearchResultContactsList
 | 
					 | 
				
			||||||
              v-if="filterContacts"
 | 
					 | 
				
			||||||
              :is-fetching="uiFlags.contact.isFetching"
 | 
					 | 
				
			||||||
              :contacts="contacts"
 | 
					 | 
				
			||||||
              :query="query"
 | 
					 | 
				
			||||||
              :show-title="isSelectedTabAll"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </Policy>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <Policy
 | 
					 | 
				
			||||||
            :permissions="[...rolePermissions, ...conversationPermissions]"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <SearchResultMessagesList
 | 
					 | 
				
			||||||
              v-if="filterMessages"
 | 
					 | 
				
			||||||
              :is-fetching="uiFlags.message.isFetching"
 | 
					 | 
				
			||||||
              :messages="messages"
 | 
					 | 
				
			||||||
              :query="query"
 | 
					 | 
				
			||||||
              :show-title="isSelectedTabAll"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </Policy>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <Policy
 | 
					 | 
				
			||||||
            :permissions="[...rolePermissions, ...conversationPermissions]"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <SearchResultConversationsList
 | 
					 | 
				
			||||||
              v-if="filterConversations"
 | 
					 | 
				
			||||||
              :is-fetching="uiFlags.conversation.isFetching"
 | 
					 | 
				
			||||||
              :conversations="conversations"
 | 
					 | 
				
			||||||
              :query="query"
 | 
					 | 
				
			||||||
              :show-title="isSelectedTabAll"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </Policy>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div
 | 
					      </div>
 | 
				
			||||||
          v-else-if="showEmptySearchResults"
 | 
					      <div class="flex-grow w-full h-full overflow-y-auto">
 | 
				
			||||||
          class="flex flex-col items-center justify-center px-4 py-6 mt-8 rounded-md"
 | 
					        <div class="w-full max-w-4xl mx-auto px-4 pb-6">
 | 
				
			||||||
        >
 | 
					          <div v-if="showResultsSection">
 | 
				
			||||||
          <fluent-icon icon="info" size="16px" class="text-n-slate-11" />
 | 
					            <Policy
 | 
				
			||||||
          <p class="m-2 text-center text-n-slate-11">
 | 
					              :permissions="[...ROLES, CONTACT_PERMISSIONS]"
 | 
				
			||||||
            {{ $t('SEARCH.EMPTY_STATE_FULL', { query }) }}
 | 
					              class="flex flex-col justify-center"
 | 
				
			||||||
          </p>
 | 
					            >
 | 
				
			||||||
        </div>
 | 
					              <SearchResultContactsList
 | 
				
			||||||
        <div
 | 
					                v-if="filterContacts"
 | 
				
			||||||
          v-else
 | 
					                :is-fetching="uiFlags.contact.isFetching"
 | 
				
			||||||
          class="flex flex-col items-center justify-center px-4 py-6 mt-8 text-center rounded-md"
 | 
					                :contacts="contacts"
 | 
				
			||||||
        >
 | 
					                :query="query"
 | 
				
			||||||
          <p class="text-center margin-bottom-0">
 | 
					                :show-title="isSelectedTabAll"
 | 
				
			||||||
            <fluent-icon icon="search" size="24px" class="text-n-slate-11" />
 | 
					              />
 | 
				
			||||||
          </p>
 | 
					              <NextButton
 | 
				
			||||||
          <p class="m-2 text-center text-n-slate-11">
 | 
					                v-if="showViewMore.contacts"
 | 
				
			||||||
            {{ $t('SEARCH.EMPTY_STATE_DEFAULT') }}
 | 
					                :label="t(`SEARCH.VIEW_MORE`)"
 | 
				
			||||||
          </p>
 | 
					                icon="i-lucide-eye"
 | 
				
			||||||
 | 
					                slate
 | 
				
			||||||
 | 
					                sm
 | 
				
			||||||
 | 
					                outline
 | 
				
			||||||
 | 
					                @click="selectedTab = 'contacts'"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </Policy>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <Policy
 | 
				
			||||||
 | 
					              :permissions="[...ROLES, ...CONVERSATION_PERMISSIONS]"
 | 
				
			||||||
 | 
					              class="flex flex-col justify-center"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <SearchResultMessagesList
 | 
				
			||||||
 | 
					                v-if="filterMessages"
 | 
				
			||||||
 | 
					                :is-fetching="uiFlags.message.isFetching"
 | 
				
			||||||
 | 
					                :messages="messages"
 | 
				
			||||||
 | 
					                :query="query"
 | 
				
			||||||
 | 
					                :show-title="isSelectedTabAll"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <NextButton
 | 
				
			||||||
 | 
					                v-if="showViewMore.messages"
 | 
				
			||||||
 | 
					                :label="t(`SEARCH.VIEW_MORE`)"
 | 
				
			||||||
 | 
					                icon="i-lucide-eye"
 | 
				
			||||||
 | 
					                slate
 | 
				
			||||||
 | 
					                sm
 | 
				
			||||||
 | 
					                outline
 | 
				
			||||||
 | 
					                @click="selectedTab = 'messages'"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </Policy>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <Policy
 | 
				
			||||||
 | 
					              :permissions="[...ROLES, ...CONVERSATION_PERMISSIONS]"
 | 
				
			||||||
 | 
					              class="flex flex-col justify-center"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <SearchResultConversationsList
 | 
				
			||||||
 | 
					                v-if="filterConversations"
 | 
				
			||||||
 | 
					                :is-fetching="uiFlags.conversation.isFetching"
 | 
				
			||||||
 | 
					                :conversations="conversations"
 | 
				
			||||||
 | 
					                :query="query"
 | 
				
			||||||
 | 
					                :show-title="isSelectedTabAll"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <NextButton
 | 
				
			||||||
 | 
					                v-if="showViewMore.conversations"
 | 
				
			||||||
 | 
					                :label="t(`SEARCH.VIEW_MORE`)"
 | 
				
			||||||
 | 
					                icon="i-lucide-eye"
 | 
				
			||||||
 | 
					                slate
 | 
				
			||||||
 | 
					                sm
 | 
				
			||||||
 | 
					                outline
 | 
				
			||||||
 | 
					                @click="selectedTab = 'conversations'"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </Policy>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div v-if="showLoadMore" class="flex justify-center mt-4 mb-6">
 | 
				
			||||||
 | 
					              <NextButton
 | 
				
			||||||
 | 
					                v-if="!isSelectedTabAll"
 | 
				
			||||||
 | 
					                :label="t(`SEARCH.LOAD_MORE`)"
 | 
				
			||||||
 | 
					                icon="i-lucide-cloud-download"
 | 
				
			||||||
 | 
					                slate
 | 
				
			||||||
 | 
					                sm
 | 
				
			||||||
 | 
					                faded
 | 
				
			||||||
 | 
					                @click="loadMore"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            v-else-if="showEmptySearchResults"
 | 
				
			||||||
 | 
					            class="flex flex-col items-center justify-center px-4 py-6 mt-8 rounded-md"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <fluent-icon icon="info" size="16px" class="text-n-slate-11" />
 | 
				
			||||||
 | 
					            <p class="m-2 text-center text-n-slate-11">
 | 
				
			||||||
 | 
					              {{ t('SEARCH.EMPTY_STATE_FULL', { query }) }}
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            v-else-if="!query"
 | 
				
			||||||
 | 
					            class="flex flex-col items-center justify-center px-4 py-6 mt-8 text-center rounded-md"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <p class="text-center margin-bottom-0">
 | 
				
			||||||
 | 
					              <fluent-icon icon="search" size="24px" class="text-n-slate-11" />
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					            <p class="m-2 text-center text-n-slate-11">
 | 
				
			||||||
 | 
					              {{ t('SEARCH.EMPTY_STATE_DEFAULT') }}
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </section>
 | 
					    </section>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -75,11 +75,10 @@ export const actions = {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  async contactSearch({ commit }, { q }) {
 | 
					  async contactSearch({ commit }, { q, page = 1 }) {
 | 
				
			||||||
    commit(types.CONTACT_SEARCH_SET, []);
 | 
					 | 
				
			||||||
    commit(types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: true });
 | 
					    commit(types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: true });
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const { data } = await SearchAPI.contacts({ q });
 | 
					      const { data } = await SearchAPI.contacts({ q, page });
 | 
				
			||||||
      commit(types.CONTACT_SEARCH_SET, data.payload.contacts);
 | 
					      commit(types.CONTACT_SEARCH_SET, data.payload.contacts);
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      // Ignore error
 | 
					      // Ignore error
 | 
				
			||||||
@@ -87,11 +86,10 @@ export const actions = {
 | 
				
			|||||||
      commit(types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: false });
 | 
					      commit(types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: false });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  async conversationSearch({ commit }, { q }) {
 | 
					  async conversationSearch({ commit }, { q, page = 1 }) {
 | 
				
			||||||
    commit(types.CONVERSATION_SEARCH_SET, []);
 | 
					 | 
				
			||||||
    commit(types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: true });
 | 
					    commit(types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: true });
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const { data } = await SearchAPI.conversations({ q });
 | 
					      const { data } = await SearchAPI.conversations({ q, page });
 | 
				
			||||||
      commit(types.CONVERSATION_SEARCH_SET, data.payload.conversations);
 | 
					      commit(types.CONVERSATION_SEARCH_SET, data.payload.conversations);
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      // Ignore error
 | 
					      // Ignore error
 | 
				
			||||||
@@ -99,11 +97,10 @@ export const actions = {
 | 
				
			|||||||
      commit(types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: false });
 | 
					      commit(types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: false });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  async messageSearch({ commit }, { q }) {
 | 
					  async messageSearch({ commit }, { q, page = 1 }) {
 | 
				
			||||||
    commit(types.MESSAGE_SEARCH_SET, []);
 | 
					 | 
				
			||||||
    commit(types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: true });
 | 
					    commit(types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: true });
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const { data } = await SearchAPI.messages({ q });
 | 
					      const { data } = await SearchAPI.messages({ q, page });
 | 
				
			||||||
      commit(types.MESSAGE_SEARCH_SET, data.payload.messages);
 | 
					      commit(types.MESSAGE_SEARCH_SET, data.payload.messages);
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      // Ignore error
 | 
					      // Ignore error
 | 
				
			||||||
@@ -112,9 +109,7 @@ export const actions = {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  async clearSearchResults({ commit }) {
 | 
					  async clearSearchResults({ commit }) {
 | 
				
			||||||
    commit(types.MESSAGE_SEARCH_SET, []);
 | 
					    commit(types.CLEAR_SEARCH_RESULTS);
 | 
				
			||||||
    commit(types.CONVERSATION_SEARCH_SET, []);
 | 
					 | 
				
			||||||
    commit(types.CONTACT_SEARCH_SET, []);
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -123,13 +118,13 @@ export const mutations = {
 | 
				
			|||||||
    state.records = records;
 | 
					    state.records = records;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  [types.CONTACT_SEARCH_SET](state, records) {
 | 
					  [types.CONTACT_SEARCH_SET](state, records) {
 | 
				
			||||||
    state.contactRecords = records;
 | 
					    state.contactRecords = [...state.contactRecords, ...records];
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  [types.CONVERSATION_SEARCH_SET](state, records) {
 | 
					  [types.CONVERSATION_SEARCH_SET](state, records) {
 | 
				
			||||||
    state.conversationRecords = records;
 | 
					    state.conversationRecords = [...state.conversationRecords, ...records];
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  [types.MESSAGE_SEARCH_SET](state, records) {
 | 
					  [types.MESSAGE_SEARCH_SET](state, records) {
 | 
				
			||||||
    state.messageRecords = records;
 | 
					    state.messageRecords = [...state.messageRecords, ...records];
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  [types.SEARCH_CONVERSATIONS_SET_UI_FLAG](state, uiFlags) {
 | 
					  [types.SEARCH_CONVERSATIONS_SET_UI_FLAG](state, uiFlags) {
 | 
				
			||||||
    state.uiFlags = { ...state.uiFlags, ...uiFlags };
 | 
					    state.uiFlags = { ...state.uiFlags, ...uiFlags };
 | 
				
			||||||
@@ -146,6 +141,11 @@ export const mutations = {
 | 
				
			|||||||
  [types.MESSAGE_SEARCH_SET_UI_FLAG](state, uiFlags) {
 | 
					  [types.MESSAGE_SEARCH_SET_UI_FLAG](state, uiFlags) {
 | 
				
			||||||
    state.uiFlags.message = { ...state.uiFlags.message, ...uiFlags };
 | 
					    state.uiFlags.message = { ...state.uiFlags.message, ...uiFlags };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  [types.CLEAR_SEARCH_RESULTS](state) {
 | 
				
			||||||
 | 
					    state.contactRecords = [];
 | 
				
			||||||
 | 
					    state.conversationRecords = [];
 | 
				
			||||||
 | 
					    state.messageRecords = [];
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,19 @@
 | 
				
			|||||||
import { actions } from '../../conversationSearch';
 | 
					import { actions } from '../../conversationSearch';
 | 
				
			||||||
import types from '../../../mutation-types';
 | 
					import types from '../../../mutation-types';
 | 
				
			||||||
import axios from 'axios';
 | 
					import axios from 'axios';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const commit = vi.fn();
 | 
					const commit = vi.fn();
 | 
				
			||||||
 | 
					const dispatch = vi.fn();
 | 
				
			||||||
global.axios = axios;
 | 
					global.axios = axios;
 | 
				
			||||||
vi.mock('axios');
 | 
					vi.mock('axios');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('#actions', () => {
 | 
					describe('#actions', () => {
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    commit.mockClear();
 | 
				
			||||||
 | 
					    dispatch.mockClear();
 | 
				
			||||||
 | 
					    axios.get.mockClear();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('#get', () => {
 | 
					  describe('#get', () => {
 | 
				
			||||||
    it('sends correct actions if no query param is provided', () => {
 | 
					    it('sends correct actions if no query param is provided', () => {
 | 
				
			||||||
      actions.get({ commit }, { q: '' });
 | 
					      actions.get({ commit }, { q: '' });
 | 
				
			||||||
@@ -41,4 +49,111 @@ describe('#actions', () => {
 | 
				
			|||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('#fullSearch', () => {
 | 
				
			||||||
 | 
					    it('should not dispatch any actions if no query provided', async () => {
 | 
				
			||||||
 | 
					      await actions.fullSearch({ commit, dispatch }, { q: '' });
 | 
				
			||||||
 | 
					      expect(dispatch).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should dispatch all search actions and set UI flags correctly', async () => {
 | 
				
			||||||
 | 
					      await actions.fullSearch({ commit, dispatch }, { q: 'test' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(commit.mock.calls).toEqual([
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					          types.FULL_SEARCH_SET_UI_FLAG,
 | 
				
			||||||
 | 
					          { isFetching: true, isSearchCompleted: false },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					          types.FULL_SEARCH_SET_UI_FLAG,
 | 
				
			||||||
 | 
					          { isFetching: false, isSearchCompleted: true },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(dispatch).toHaveBeenCalledWith('contactSearch', { q: 'test' });
 | 
				
			||||||
 | 
					      expect(dispatch).toHaveBeenCalledWith('conversationSearch', {
 | 
				
			||||||
 | 
					        q: 'test',
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      expect(dispatch).toHaveBeenCalledWith('messageSearch', { q: 'test' });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('#contactSearch', () => {
 | 
				
			||||||
 | 
					    it('should handle successful contact search', async () => {
 | 
				
			||||||
 | 
					      axios.get.mockResolvedValue({
 | 
				
			||||||
 | 
					        data: { payload: { contacts: [{ id: 1 }] } },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await actions.contactSearch({ commit }, { q: 'test', page: 1 });
 | 
				
			||||||
 | 
					      expect(commit.mock.calls).toEqual([
 | 
				
			||||||
 | 
					        [types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: true }],
 | 
				
			||||||
 | 
					        [types.CONTACT_SEARCH_SET, [{ id: 1 }]],
 | 
				
			||||||
 | 
					        [types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: false }],
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should handle failed contact search', async () => {
 | 
				
			||||||
 | 
					      axios.get.mockRejectedValue({});
 | 
				
			||||||
 | 
					      await actions.contactSearch({ commit }, { q: 'test' });
 | 
				
			||||||
 | 
					      expect(commit.mock.calls).toEqual([
 | 
				
			||||||
 | 
					        [types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: true }],
 | 
				
			||||||
 | 
					        [types.CONTACT_SEARCH_SET_UI_FLAG, { isFetching: false }],
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('#conversationSearch', () => {
 | 
				
			||||||
 | 
					    it('should handle successful conversation search', async () => {
 | 
				
			||||||
 | 
					      axios.get.mockResolvedValue({
 | 
				
			||||||
 | 
					        data: { payload: { conversations: [{ id: 1 }] } },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await actions.conversationSearch({ commit }, { q: 'test', page: 1 });
 | 
				
			||||||
 | 
					      expect(commit.mock.calls).toEqual([
 | 
				
			||||||
 | 
					        [types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: true }],
 | 
				
			||||||
 | 
					        [types.CONVERSATION_SEARCH_SET, [{ id: 1 }]],
 | 
				
			||||||
 | 
					        [types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: false }],
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should handle failed conversation search', async () => {
 | 
				
			||||||
 | 
					      axios.get.mockRejectedValue({});
 | 
				
			||||||
 | 
					      await actions.conversationSearch({ commit }, { q: 'test' });
 | 
				
			||||||
 | 
					      expect(commit.mock.calls).toEqual([
 | 
				
			||||||
 | 
					        [types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: true }],
 | 
				
			||||||
 | 
					        [types.CONVERSATION_SEARCH_SET_UI_FLAG, { isFetching: false }],
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('#messageSearch', () => {
 | 
				
			||||||
 | 
					    it('should handle successful message search', async () => {
 | 
				
			||||||
 | 
					      axios.get.mockResolvedValue({
 | 
				
			||||||
 | 
					        data: { payload: { messages: [{ id: 1 }] } },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await actions.messageSearch({ commit }, { q: 'test', page: 1 });
 | 
				
			||||||
 | 
					      expect(commit.mock.calls).toEqual([
 | 
				
			||||||
 | 
					        [types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: true }],
 | 
				
			||||||
 | 
					        [types.MESSAGE_SEARCH_SET, [{ id: 1 }]],
 | 
				
			||||||
 | 
					        [types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: false }],
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should handle failed message search', async () => {
 | 
				
			||||||
 | 
					      axios.get.mockRejectedValue({});
 | 
				
			||||||
 | 
					      await actions.messageSearch({ commit }, { q: 'test' });
 | 
				
			||||||
 | 
					      expect(commit.mock.calls).toEqual([
 | 
				
			||||||
 | 
					        [types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: true }],
 | 
				
			||||||
 | 
					        [types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: false }],
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('#clearSearchResults', () => {
 | 
				
			||||||
 | 
					    it('should commit clear search results mutation', () => {
 | 
				
			||||||
 | 
					      actions.clearSearchResults({ commit });
 | 
				
			||||||
 | 
					      expect(commit).toHaveBeenCalledWith(types.CLEAR_SEARCH_RESULTS);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,10 +10,49 @@ describe('#getters', () => {
 | 
				
			|||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('getContactRecords', () => {
 | 
				
			||||||
 | 
					    const state = {
 | 
				
			||||||
 | 
					      contactRecords: [{ id: 1, name: 'Contact 1' }],
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    expect(getters.getContactRecords(state)).toEqual([
 | 
				
			||||||
 | 
					      { id: 1, name: 'Contact 1' },
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('getConversationRecords', () => {
 | 
				
			||||||
 | 
					    const state = {
 | 
				
			||||||
 | 
					      conversationRecords: [{ id: 1, title: 'Conversation 1' }],
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    expect(getters.getConversationRecords(state)).toEqual([
 | 
				
			||||||
 | 
					      { id: 1, title: 'Conversation 1' },
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('getMessageRecords', () => {
 | 
				
			||||||
 | 
					    const state = {
 | 
				
			||||||
 | 
					      messageRecords: [{ id: 1, content: 'Message 1' }],
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    expect(getters.getMessageRecords(state)).toEqual([
 | 
				
			||||||
 | 
					      { id: 1, content: 'Message 1' },
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('getUIFlags', () => {
 | 
					  it('getUIFlags', () => {
 | 
				
			||||||
    const state = {
 | 
					    const state = {
 | 
				
			||||||
      uiFlags: { isFetching: false },
 | 
					      uiFlags: {
 | 
				
			||||||
 | 
					        isFetching: false,
 | 
				
			||||||
 | 
					        isSearchCompleted: true,
 | 
				
			||||||
 | 
					        contact: { isFetching: true },
 | 
				
			||||||
 | 
					        message: { isFetching: false },
 | 
				
			||||||
 | 
					        conversation: { isFetching: false },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    expect(getters.getUIFlags(state)).toEqual({ isFetching: false });
 | 
					    expect(getters.getUIFlags(state)).toEqual({
 | 
				
			||||||
 | 
					      isFetching: false,
 | 
				
			||||||
 | 
					      isSearchCompleted: true,
 | 
				
			||||||
 | 
					      contact: { isFetching: true },
 | 
				
			||||||
 | 
					      message: { isFetching: false },
 | 
				
			||||||
 | 
					      conversation: { isFetching: false },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ describe('#mutations', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('#SEARCH_CONVERSATIONS_SET', () => {
 | 
					  describe('#SEARCH_CONVERSATIONS_SET_UI_FLAG', () => {
 | 
				
			||||||
    it('set uiFlags correctly', () => {
 | 
					    it('set uiFlags correctly', () => {
 | 
				
			||||||
      const state = { uiFlags: { isFetching: true } };
 | 
					      const state = { uiFlags: { isFetching: true } };
 | 
				
			||||||
      mutations[types.SEARCH_CONVERSATIONS_SET_UI_FLAG](state, {
 | 
					      mutations[types.SEARCH_CONVERSATIONS_SET_UI_FLAG](state, {
 | 
				
			||||||
@@ -19,4 +19,99 @@ describe('#mutations', () => {
 | 
				
			|||||||
      expect(state.uiFlags).toEqual({ isFetching: false });
 | 
					      expect(state.uiFlags).toEqual({ isFetching: false });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('#CONTACT_SEARCH_SET', () => {
 | 
				
			||||||
 | 
					    it('should append new contact records to existing ones', () => {
 | 
				
			||||||
 | 
					      const state = { contactRecords: [{ id: 1 }] };
 | 
				
			||||||
 | 
					      mutations[types.CONTACT_SEARCH_SET](state, [{ id: 2 }]);
 | 
				
			||||||
 | 
					      expect(state.contactRecords).toEqual([{ id: 1 }, { id: 2 }]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('#CONVERSATION_SEARCH_SET', () => {
 | 
				
			||||||
 | 
					    it('should append new conversation records to existing ones', () => {
 | 
				
			||||||
 | 
					      const state = { conversationRecords: [{ id: 1 }] };
 | 
				
			||||||
 | 
					      mutations[types.CONVERSATION_SEARCH_SET](state, [{ id: 2 }]);
 | 
				
			||||||
 | 
					      expect(state.conversationRecords).toEqual([{ id: 1 }, { id: 2 }]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('#MESSAGE_SEARCH_SET', () => {
 | 
				
			||||||
 | 
					    it('should append new message records to existing ones', () => {
 | 
				
			||||||
 | 
					      const state = { messageRecords: [{ id: 1 }] };
 | 
				
			||||||
 | 
					      mutations[types.MESSAGE_SEARCH_SET](state, [{ id: 2 }]);
 | 
				
			||||||
 | 
					      expect(state.messageRecords).toEqual([{ id: 1 }, { id: 2 }]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('#FULL_SEARCH_SET_UI_FLAG', () => {
 | 
				
			||||||
 | 
					    it('set full search UI flags correctly', () => {
 | 
				
			||||||
 | 
					      const state = {
 | 
				
			||||||
 | 
					        uiFlags: {
 | 
				
			||||||
 | 
					          isFetching: true,
 | 
				
			||||||
 | 
					          isSearchCompleted: false,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      mutations[types.FULL_SEARCH_SET_UI_FLAG](state, {
 | 
				
			||||||
 | 
					        isFetching: false,
 | 
				
			||||||
 | 
					        isSearchCompleted: true,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      expect(state.uiFlags).toEqual({
 | 
				
			||||||
 | 
					        isFetching: false,
 | 
				
			||||||
 | 
					        isSearchCompleted: true,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('#CONTACT_SEARCH_SET_UI_FLAG', () => {
 | 
				
			||||||
 | 
					    it('set contact search UI flags correctly', () => {
 | 
				
			||||||
 | 
					      const state = {
 | 
				
			||||||
 | 
					        uiFlags: {
 | 
				
			||||||
 | 
					          contact: { isFetching: true },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      mutations[types.CONTACT_SEARCH_SET_UI_FLAG](state, { isFetching: false });
 | 
				
			||||||
 | 
					      expect(state.uiFlags.contact).toEqual({ isFetching: false });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('#CONVERSATION_SEARCH_SET_UI_FLAG', () => {
 | 
				
			||||||
 | 
					    it('set conversation search UI flags correctly', () => {
 | 
				
			||||||
 | 
					      const state = {
 | 
				
			||||||
 | 
					        uiFlags: {
 | 
				
			||||||
 | 
					          conversation: { isFetching: true },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      mutations[types.CONVERSATION_SEARCH_SET_UI_FLAG](state, {
 | 
				
			||||||
 | 
					        isFetching: false,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      expect(state.uiFlags.conversation).toEqual({ isFetching: false });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('#MESSAGE_SEARCH_SET_UI_FLAG', () => {
 | 
				
			||||||
 | 
					    it('set message search UI flags correctly', () => {
 | 
				
			||||||
 | 
					      const state = {
 | 
				
			||||||
 | 
					        uiFlags: {
 | 
				
			||||||
 | 
					          message: { isFetching: true },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      mutations[types.MESSAGE_SEARCH_SET_UI_FLAG](state, { isFetching: false });
 | 
				
			||||||
 | 
					      expect(state.uiFlags.message).toEqual({ isFetching: false });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('#CLEAR_SEARCH_RESULTS', () => {
 | 
				
			||||||
 | 
					    it('should clear all search records', () => {
 | 
				
			||||||
 | 
					      const state = {
 | 
				
			||||||
 | 
					        contactRecords: [{ id: 1 }],
 | 
				
			||||||
 | 
					        conversationRecords: [{ id: 1 }],
 | 
				
			||||||
 | 
					        messageRecords: [{ id: 1 }],
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      mutations[types.CLEAR_SEARCH_RESULTS](state);
 | 
				
			||||||
 | 
					      expect(state.contactRecords).toEqual([]);
 | 
				
			||||||
 | 
					      expect(state.conversationRecords).toEqual([]);
 | 
				
			||||||
 | 
					      expect(state.messageRecords).toEqual([]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -311,6 +311,7 @@ export default {
 | 
				
			|||||||
  CONVERSATION_SEARCH_SET: 'CONVERSATION_SEARCH_SET',
 | 
					  CONVERSATION_SEARCH_SET: 'CONVERSATION_SEARCH_SET',
 | 
				
			||||||
  CONVERSATION_SEARCH_SET_UI_FLAG: 'CONVERSATION_SEARCH_SET_UI_FLAG',
 | 
					  CONVERSATION_SEARCH_SET_UI_FLAG: 'CONVERSATION_SEARCH_SET_UI_FLAG',
 | 
				
			||||||
  MESSAGE_SEARCH_SET: 'MESSAGE_SEARCH_SET',
 | 
					  MESSAGE_SEARCH_SET: 'MESSAGE_SEARCH_SET',
 | 
				
			||||||
 | 
					  CLEAR_SEARCH_RESULTS: 'CLEAR_SEARCH_RESULTS',
 | 
				
			||||||
  MESSAGE_SEARCH_SET_UI_FLAG: 'MESSAGE_SEARCH_SET_UI_FLAG',
 | 
					  MESSAGE_SEARCH_SET_UI_FLAG: 'MESSAGE_SEARCH_SET_UI_FLAG',
 | 
				
			||||||
  FULL_SEARCH_SET_UI_FLAG: 'FULL_SEARCH_SET_UI_FLAG',
 | 
					  FULL_SEARCH_SET_UI_FLAG: 'FULL_SEARCH_SET_UI_FLAG',
 | 
				
			||||||
  SET_CONVERSATION_PARTICIPANTS_UI_FLAG:
 | 
					  SET_CONVERSATION_PARTICIPANTS_UI_FLAG:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,7 +30,8 @@ class SearchService
 | 
				
			|||||||
                                    .where("cast(conversations.display_id as text) ILIKE :search OR contacts.name ILIKE :search OR contacts.email
 | 
					                                    .where("cast(conversations.display_id as text) ILIKE :search OR contacts.name ILIKE :search OR contacts.email
 | 
				
			||||||
                            ILIKE :search OR contacts.phone_number ILIKE :search OR contacts.identifier ILIKE :search", search: "%#{search_query}%")
 | 
					                            ILIKE :search OR contacts.phone_number ILIKE :search OR contacts.identifier ILIKE :search", search: "%#{search_query}%")
 | 
				
			||||||
                                    .order('conversations.created_at DESC')
 | 
					                                    .order('conversations.created_at DESC')
 | 
				
			||||||
                                    .limit(10)
 | 
					                                    .page(params[:page])
 | 
				
			||||||
 | 
					                                    .per(15)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def filter_messages
 | 
					  def filter_messages
 | 
				
			||||||
@@ -38,13 +39,14 @@ class SearchService
 | 
				
			|||||||
                               .where('messages.content ILIKE :search', search: "%#{search_query}%")
 | 
					                               .where('messages.content ILIKE :search', search: "%#{search_query}%")
 | 
				
			||||||
                               .where('created_at >= ?', 3.months.ago)
 | 
					                               .where('created_at >= ?', 3.months.ago)
 | 
				
			||||||
                               .reorder('created_at DESC')
 | 
					                               .reorder('created_at DESC')
 | 
				
			||||||
                               .limit(10)
 | 
					                               .page(params[:page])
 | 
				
			||||||
 | 
					                               .per(15)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def filter_contacts
 | 
					  def filter_contacts
 | 
				
			||||||
    @contacts = current_account.contacts.where(
 | 
					    @contacts = current_account.contacts.where(
 | 
				
			||||||
      "name ILIKE :search OR email ILIKE :search OR phone_number
 | 
					      "name ILIKE :search OR email ILIKE :search OR phone_number
 | 
				
			||||||
      ILIKE :search OR identifier ILIKE :search", search: "%#{search_query}%"
 | 
					      ILIKE :search OR identifier ILIKE :search", search: "%#{search_query}%"
 | 
				
			||||||
    ).resolved_contacts.order_on_last_activity_at('desc').limit(10)
 | 
					    ).resolved_contacts.order_on_last_activity_at('desc').page(params[:page]).per(15)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user