Shivam Mishra
2024-10-02 00:36:30 -07:00
committed by GitHub
co-authored by Pranav Sivin Varghese
parent e0bf2bd9d4
commit 42f6621afb
661 changed files with 15939 additions and 31194 deletions
@@ -35,7 +35,7 @@ export default {
this.$store.dispatch('getCannedResponse', { searchKey: this.searchKey });
},
handleMentionClick(item = {}) {
this.$emit('click', item.description);
this.$emit('replace', item.description);
},
},
};
@@ -9,6 +9,7 @@ import { useFilter } from 'shared/composables/useFilter';
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { validateConversationOrContactFilters } from 'dashboard/helper/validations.js';
import { useTrack } from 'dashboard/composables';
export default {
components: {
@@ -141,8 +142,27 @@ export default {
const type = this.filterTypes.find(filter => filter.attributeKey === key);
return type?.filterOperators;
},
statusFilterItems() {
return {
open: {
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.open.TEXT'),
},
resolved: {
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.resolved.TEXT'),
},
pending: {
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.pending.TEXT'),
},
snoozed: {
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.snoozed.TEXT'),
},
all: {
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.all.TEXT'),
},
};
},
getDropdownValues(type) {
const statusFilters = this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
const statusFilters = this.statusFilterItems();
const allCustomAttributes = this.$store.getters[
'attributes/getAttributesByModel'
](this.attributeModel);
@@ -268,7 +288,7 @@ export default {
JSON.parse(JSON.stringify(this.appliedFilters))
);
this.$emit('applyFilter', this.appliedFilters);
this.$track(CONVERSATION_EVENTS.APPLY_FILTER, {
useTrack(CONVERSATION_EVENTS.APPLY_FILTER, {
applied_filters: this.appliedFilters.map(filter => ({
key: filter.attribute_key,
operator: filter.filter_operator,
@@ -4,6 +4,25 @@ import { mapGetters } from 'vuex';
import FilterItem from './FilterItem.vue';
import { useUISettings } from 'dashboard/composables/useUISettings';
const CHAT_STATUS_FILTER_ITEMS = Object.freeze([
'open',
'resolved',
'pending',
'snoozed',
'all',
]);
const SORT_ORDER_ITEMS = Object.freeze([
'last_activity_at_asc',
'last_activity_at_desc',
'created_at_desc',
'created_at_asc',
'priority_desc',
'priority_asc',
'waiting_since_asc',
'waiting_since_desc',
]);
export default {
components: {
FilterItem,
@@ -18,8 +37,8 @@ export default {
data() {
return {
showActionsDropdown: false,
chatStatusItems: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS'),
chatSortItems: this.$t('CHAT_LIST.SORT_ORDER_ITEMS'),
chatStatusItems: CHAT_STATUS_FILTER_ITEMS,
chatSortItems: SORT_ORDER_ITEMS,
};
},
computed: {
@@ -46,10 +46,12 @@ export default {
return [
{
key: 'messages',
index: 0,
name: this.$t('CONVERSATION.DASHBOARD_APP_TAB_MESSAGES'),
},
...this.dashboardApps.map(dashboardApp => ({
...this.dashboardApps.map((dashboardApp, index) => ({
key: `dashboard-${dashboardApp.id}`,
index: index + 1,
name: dashboardApp.title,
})),
];
@@ -112,6 +114,7 @@ export default {
<woot-tabs-item
v-for="tab in dashboardAppTabs"
:key="tab.key"
:index="tab.index"
:name="tab.name"
:show-badge="false"
/>
@@ -12,6 +12,7 @@ import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
import CardLabels from './conversationCardComponents/CardLabels.vue';
import PriorityMark from './PriorityMark.vue';
import SLACardLabel from './components/SLACardLabel.vue';
import ContextMenu from 'dashboard/components/ui/ContextMenu.vue';
export default {
components: {
@@ -23,6 +24,7 @@ export default {
MessagePreview,
PriorityMark,
SLACardLabel,
ContextMenu,
},
mixins: [inboxMixin],
props: {
@@ -316,17 +318,13 @@ export default {
{{ unreadCount > 9 ? '9+' : unreadCount }}
</span>
</div>
<CardLabels
:conversation-id="chat.id"
:conversation-labels="chat.labels"
class="mt-0.5 mx-2 mb-0"
>
<CardLabels :conversation-labels="chat.labels" class="mt-0.5 mx-2 mb-0">
<template v-if="hasSlaPolicyId" #before>
<SLACardLabel :chat="chat" class="ltr:mr-1 rtl:ml-1" />
</template>
</CardLabels>
</div>
<woot-context-menu
<ContextMenu
v-if="showContextMenu"
:x="contextMenu.x"
:y="contextMenu.y"
@@ -345,7 +343,7 @@ export default {
@markAsUnread="markAsUnread"
@assignPriority="assignPriority"
/>
</woot-context-menu>
</ContextMenu>
</div>
</template>
@@ -132,7 +132,7 @@ export default {
<div v-if="sentToOtherEmailAddress" class="w-[50%] mt-1">
<label :class="{ error: v$.email.$error }">
<input
v-model.trim="email"
v-model="email"
type="text"
:placeholder="$t('EMAIL_TRANSCRIPT.FORM.EMAIL.PLACEHOLDER')"
@input="v$.email.$touch"
@@ -56,7 +56,7 @@ export default {
!this.loadingChatList &&
this.isAdmin
) {
return 'h-full overflow-auto';
return 'h-full overflow-auto w-full';
}
return 'flex-1 min-w-0 px-0 flex flex-col items-center justify-center h-full';
},
@@ -73,7 +73,7 @@ export default {
<!-- No inboxes attached -->
<div
v-if="!inboxesList.length && !uiFlags.isFetching && !loadingChatList"
class="clearfix"
class="clearfix mx-auto"
>
<OnboardingView v-if="isAdmin" />
<EmptyStateMessage v-else :message="$t('CONVERSATION.NO_INBOX_AGENT')" />
@@ -15,12 +15,12 @@ export default {
<div class="flex flex-col items-center justify-center h-full">
<img
class="m-4 w-32 hidden dark:block"
src="~dashboard/assets/images/no-chat-dark.svg"
src="dashboard/assets/images/no-chat-dark.svg"
alt="No Chat dark"
/>
<img
class="m-4 w-32 block dark:hidden"
src="~dashboard/assets/images/no-chat.svg"
src="dashboard/assets/images/no-chat.svg"
alt="No Chat"
/>
<span
@@ -6,7 +6,7 @@ export default {
required: true,
},
items: {
type: Object,
type: Array,
required: true,
},
type: {
@@ -42,8 +42,8 @@ export default {
class="bg-slate-25 dark:bg-slate-700 text-xs h-6 my-0 mx-1 py-0 pr-6 pl-2 w-32 border border-solid border-slate-75 dark:border-slate-600 text-slate-800 dark:text-slate-100"
@change="onTabChange()"
>
<option v-for="(value, status) in items" :key="status" :value="status">
{{ $t(`${pathPrefix}.${status}.TEXT`) }}
<option v-for="value in items" :key="value" :value="value">
{{ $t(`${pathPrefix}.${value}.TEXT`) }}
</option>
</select>
</template>
@@ -21,7 +21,9 @@ import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
import { getDayDifferenceFromNow } from 'shared/helpers/DateHelper';
import * as Sentry from '@sentry/browser';
import * as Sentry from '@sentry/vue';
import { useTrack } from 'dashboard/composables';
import { emitter } from 'shared/helpers/mitt';
export default {
components: {
@@ -349,11 +351,11 @@ export default {
},
mounted() {
this.hasMediaLoadError = false;
this.$emitter.on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
emitter.on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
this.setupHighlightTimer();
},
beforeDestroy() {
this.$emitter.off(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
unmounted() {
emitter.off(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
clearTimeout(this.higlightTimeout);
},
methods: {
@@ -366,9 +368,6 @@ export default {
if (this.hasAttachments && this.data.attachments.length > 0) {
return this.compareMessageFileType(this.data, type);
}
if (this.storyReply) {
return true;
}
return false;
},
compareMessageFileType(messageData, type) {
@@ -406,7 +405,7 @@ export default {
e.preventDefault();
if (e.type === 'contextmenu') {
this.$track(ACCOUNT_EVENTS.OPEN_MESSAGE_CONTEXT_MENU);
useTrack(ACCOUNT_EVENTS.OPEN_MESSAGE_CONTEXT_MENU);
}
this.contextMenuPosition = {
x: e.pageX || e.clientX,
@@ -423,7 +422,7 @@ export default {
const { conversation_id: conversationId, id: replyTo } = this.data;
LocalStorage.updateJsonStore(replyStorageKey, conversationId, replyTo);
this.$emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.data);
emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.data);
},
setupHighlightTimer() {
if (Number(this.$route.query.messageId) !== Number(this.data.id)) {
@@ -18,6 +18,7 @@ import { mapGetters } from 'vuex';
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
// utils
import { emitter } from 'shared/helpers/mitt';
import { getTypingUsersText } from '../../../helper/commons';
import { calculateScrollTop } from './helpers/scrollTopCalculationHelper';
import { LocalStorage } from 'shared/helpers/localStorage';
@@ -251,12 +252,12 @@ export default {
},
created() {
this.$emitter.on(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
emitter.on(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
// when a new message comes in, we refetch the label suggestions
this.$emitter.on(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS, this.fetchSuggestions);
emitter.on(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS, this.fetchSuggestions);
// when a message is sent we set the flag to true this hides the label suggestions,
// until the chat is changed and the flag is reset in the watch for currentChat
this.$emitter.on(BUS_EVENTS.MESSAGE_SENT, () => {
emitter.on(BUS_EVENTS.MESSAGE_SENT, () => {
this.messageSentSinceOpened = true;
});
},
@@ -267,7 +268,7 @@ export default {
this.fetchSuggestions();
},
beforeDestroy() {
unmounted() {
this.removeBusListeners();
this.removeScrollListener();
},
@@ -323,7 +324,7 @@ export default {
this.$store.dispatch('fetchAllAttachments', this.currentChat.id);
},
removeBusListeners() {
this.$emitter.off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
emitter.off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
},
onScrollToMessage({ messageId = '' } = {}) {
this.$nextTick(() => {
@@ -428,7 +429,7 @@ export default {
} else {
this.hasUserScrolled = true;
}
this.$emitter.emit(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL);
emitter.emit(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL);
this.fetchPreviousMessages(e.target.scrollTop);
},
@@ -540,7 +541,7 @@ export default {
{{ typingUserNames }}
<img
class="w-6 ltr:ml-2 rtl:mr-2"
src="~dashboard/assets/images/typing.gif"
src="assets/images/typing.gif"
alt="Someone is typing"
/>
</div>
@@ -548,7 +549,7 @@ export default {
<ReplyBox
:conversation-id="currentChat.id"
:popout-reply-box.sync="isPopOutReplyBox"
@click="showPopOutReplyBox"
@togglePopout="showPopOutReplyBox"
/>
</div>
</div>
@@ -1,6 +1,7 @@
<script>
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { emitter } from 'shared/helpers/mitt';
import EmailTranscriptModal from './EmailTranscriptModal.vue';
import ResolveAction from '../../buttons/ResolveAction.vue';
import {
@@ -23,14 +24,14 @@ export default {
...mapGetters({ currentChat: 'getSelectedChat' }),
},
mounted() {
this.$emitter.on(CMD_MUTE_CONVERSATION, this.mute);
this.$emitter.on(CMD_UNMUTE_CONVERSATION, this.unmute);
this.$emitter.on(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
emitter.on(CMD_MUTE_CONVERSATION, this.mute);
emitter.on(CMD_UNMUTE_CONVERSATION, this.unmute);
emitter.on(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
},
destroyed() {
this.$emitter.off(CMD_MUTE_CONVERSATION, this.mute);
this.$emitter.off(CMD_UNMUTE_CONVERSATION, this.unmute);
this.$emitter.off(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
emitter.off(CMD_MUTE_CONVERSATION, this.mute);
emitter.off(CMD_UNMUTE_CONVERSATION, this.unmute);
emitter.off(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
},
methods: {
mute() {
@@ -31,7 +31,9 @@ defineProps({
<div
class="h-full w-full bg-white dark:bg-slate-900 border border-slate-100 dark:border-white/10 rounded-lg p-4 flex flex-col"
>
<img :src="imageSrc" :alt="imageAlt" class="h-36 w-auto mx-auto" />
<div class="flex-1 flex items-center justify-center">
<img :src="imageSrc" :alt="imageAlt" class="h-36 w-auto mx-auto" />
</div>
<div class="mt-auto">
<p
class="text-base text-slate-800 dark:text-slate-100 font-interDisplay font-semibold tracking-[0.3px]"
@@ -47,7 +49,7 @@ defineProps({
class="no-underline text-woot-500 text-sm font-medium"
>
<span>{{ linkText }}</span>
<span>{{ `` }}</span>
<span class="ml-2">{{ `` }}</span>
</router-link>
</div>
</div>
@@ -1,7 +1,7 @@
<script setup>
import OnboardingFeatureCard from './OnboardingFeatureCard.vue';
import { computed } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n';
import { useI18n } from 'vue-i18n';
import { useStoreGetters } from 'dashboard/composables/store';
const getters = useStoreGetters();
@@ -28,7 +28,7 @@ const greetingMessage = computed(() => {
<template>
<div
class="min-h-screen max-w-4xl mx-auto grid grid-cols-1 xl:grid-cols-2 grid-rows-[auto_1fr_1fr] gap-4 p-8 w-full font-inter overflow-auto"
class="min-h-screen lg:max-w-5xl max-w-4xl mx-auto grid grid-cols-2 grid-rows-[auto_1fr_1fr] auto-rows-min gap-4 p-8 w-full font-inter overflow-auto"
>
<div class="col-span-full self-start">
<p
@@ -1,7 +1,9 @@
<script>
import { defineAsyncComponent } from 'vue';
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useTrack } from 'dashboard/composables';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import CannedResponse from './CannedResponse.vue';
@@ -12,7 +14,7 @@ import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel
import ReplyEmailHead from './ReplyEmailHead.vue';
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert';
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
import Banner from 'dashboard/components/ui/Banner.vue';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
@@ -40,8 +42,10 @@ import {
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
const EmojiInput = () => import('shared/components/emoji/EmojiInput.vue');
import { emitter } from 'shared/helpers/mitt';
const EmojiInput = defineAsyncComponent(
() => import('shared/components/emoji/EmojiInput.vue')
);
export default {
components: {
@@ -449,15 +453,12 @@ export default {
);
this.fetchAndSetReplyTo();
this.$emitter.on(
BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE,
this.fetchAndSetReplyTo
);
emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
// A hacky fix to solve the drag and drop
// Is showing on top of new conversation modal drag and drop
// TODO need to find a better solution
this.$emitter.on(
emitter.on(
BUS_EVENTS.NEW_CONVERSATION_MODAL,
this.onNewConversationModalActive
);
@@ -465,13 +466,10 @@ export default {
destroyed() {
document.removeEventListener('paste', this.onPaste);
document.removeEventListener('keydown', this.handleKeyEvents);
this.$emitter.off(
BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE,
this.fetchAndSetReplyTo
);
emitter.off(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
},
beforeDestroy() {
this.$emitter.off(
unmounted() {
emitter.off(
BUS_EVENTS.NEW_CONVERSATION_MODAL,
this.onNewConversationModalActive
);
@@ -484,7 +482,7 @@ export default {
const lines = title.split('\n');
const nonEmptyLines = lines.filter(line => line.trim() !== '');
const filteredMarkdown = nonEmptyLines.join(' ');
this.$emitter.emit(
emitter.emit(
BUS_EVENTS.INSERT_INTO_RICH_EDITOR,
`[${filteredMarkdown}](${url})`
);
@@ -494,7 +492,7 @@ export default {
);
}
this.$track(CONVERSATION_EVENTS.INSERT_ARTICLE_LINK);
useTrack(CONVERSATION_EVENTS.INSERT_ARTICLE_LINK);
},
toggleRichContentEditor() {
this.updateUISettings({
@@ -689,8 +687,8 @@ export default {
sendMessageAnalyticsData(isPrivate) {
// Analytics data for message signature is enabled or not in channels
return isPrivate
? this.$track(CONVERSATION_EVENTS.SENT_PRIVATE_NOTE)
: this.$track(CONVERSATION_EVENTS.SENT_MESSAGE, {
? useTrack(CONVERSATION_EVENTS.SENT_PRIVATE_NOTE)
: useTrack(CONVERSATION_EVENTS.SENT_MESSAGE, {
channelType: this.channelType,
signatureEnabled: this.sendWithSignature,
hasReplyTo: !!this.inReplyTo?.id,
@@ -726,8 +724,8 @@ export default {
'createPendingMessageAndSend',
messagePayload
);
this.$emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
this.$emitter.emit(BUS_EVENTS.MESSAGE_SENT);
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
emitter.emit(BUS_EVENTS.MESSAGE_SENT);
this.removeFromDraft();
this.sendMessageAnalyticsData(messagePayload.private);
} catch (error) {
@@ -757,7 +755,7 @@ export default {
});
setTimeout(() => {
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
useTrack(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
this.message = updatedMessage;
}, 100);
},
@@ -832,7 +830,7 @@ export default {
if (this.isRecordingAudio) {
if (!this.isRecorderAudioStopped) {
this.isRecorderAudioStopped = true;
this.$refs.audioRecorderInput.stopAudioRecording();
this.$refs.audioRecorderInput.stopRecording();
} else if (this.isRecorderAudioStopped) {
this.$refs.audioRecorderInput.playPause();
}
@@ -859,7 +857,7 @@ export default {
onFocus() {
this.isFocused = true;
},
onStateProgressRecorderChanged(duration) {
onRecordProgressChanged(duration) {
this.recordingAudioDurationText = duration;
},
onStateRecorderChanged(state) {
@@ -1053,7 +1051,7 @@ export default {
resetReplyToMessage() {
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
LocalStorage.deleteFromJsonStore(replyStorageKey, this.conversationId);
this.$emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE);
emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE);
},
onNewConversationModalActive(isActive) {
// Issue is if the new conversation modal is open and we drag and drop the file
@@ -1083,7 +1081,7 @@ export default {
:banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')"
has-action-button
:action-button-label="$t('CONVERSATION.ASSIGN_TO_ME')"
@click="onClickSelfAssign"
@primaryAction="onClickSelfAssign"
/>
<ReplyTopPanel
:mode="replyType"
@@ -1091,7 +1089,7 @@ export default {
:characters-remaining="charactersRemaining"
:popout-reply-box="popoutReplyBox"
@setReplyMode="setReplyMode"
@click="$emit('click')"
@togglePopout="$emit('togglePopout')"
/>
<ArticleSearchPopover
v-if="showArticleSearchPopover && connectedPortalSlug"
@@ -1110,7 +1108,7 @@ export default {
v-on-clickaway="hideMentions"
class="normal-editor__canned-box"
:search-key="mentionSearchKey"
@click="replaceText"
@replace="replaceText"
/>
<EmojiInput
v-if="showEmojiPicker"
@@ -1120,15 +1118,15 @@ export default {
/>
<ReplyEmailHead
v-if="showReplyHead"
:cc-emails.sync="ccEmails"
:bcc-emails.sync="bccEmails"
:to-emails.sync="toEmails"
v-model:cc-emails="ccEmails"
v-model:bcc-emails="bccEmails"
v-model:to-emails="toEmails"
/>
<WootAudioRecorder
v-if="showAudioRecorderEditor"
ref="audioRecorderInput"
:audio-record-format="audioRecordFormat"
@stateRecorderProgressChanged="onStateProgressRecorderChanged"
@recorderProgressChanged="onRecordProgressChanged"
@stateRecorderChanged="onStateRecorderChanged"
@finishRecord="onFinishRecorder"
/>
@@ -90,7 +90,7 @@ export default {
</label>
<div class="rounded-none flex-1 min-w-0 m-0 whitespace-nowrap">
<woot-input
v-model.trim="v$.toEmailsVal.$model"
v-model="v$.toEmailsVal.$model"
type="text"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:h-8 [&>input]:text-sm [&>input]:!border-0 [&>input]:border-none"
:class="{ error: v$.toEmailsVal.$error }"
@@ -107,7 +107,7 @@ export default {
</label>
<div class="rounded-none flex-1 min-w-0 m-0 whitespace-nowrap">
<woot-input
v-model.trim="v$.ccEmailsVal.$model"
v-model="v$.ccEmailsVal.$model"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:h-8 [&>input]:text-sm [&>input]:!border-0 [&>input]:border-none"
type="text"
:class="{ error: v$.ccEmailsVal.$error }"
@@ -135,7 +135,7 @@ export default {
</label>
<div class="rounded-none flex-1 min-w-0 m-0 whitespace-nowrap">
<woot-input
v-model.trim="v$.bccEmailsVal.$model"
v-model="v$.bccEmailsVal.$model"
type="text"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:h-8 [&>input]:text-sm [&>input]:!border-0 [&>input]:border-none"
:class="{ error: v$.bccEmailsVal.$error }"
@@ -10,7 +10,7 @@ const props = defineProps({
},
});
const emit = defineEmits(['click']);
const emit = defineEmits(['selectAgent']);
const getters = useStoreGetters();
const agents = computed(() => getters['agents/getVerifiedAgents'].value);
@@ -36,7 +36,7 @@ const adjustScroll = () => {
};
const onSelect = () => {
emit('click', items.value[selectedIndex.value]);
emit('selectAgent', items.value[selectedIndex.value]);
};
useKeyboardNavigableList({
@@ -50,7 +50,7 @@ export default {
},
methods: {
handleVariableClick(item = {}) {
this.$emit('click', item.key);
this.$emit('selectVariable', item.key);
},
},
};
@@ -179,9 +179,11 @@ footer {
@apply ml-2.5;
}
}
.error {
@apply bg-red-100 dark:bg-red-100 rounded-md text-red-800 dark:text-red-800 p-2.5 text-center;
}
.template-input {
@apply bg-slate-25 dark:bg-slate-900 text-slate-700 dark:text-slate-100;
}
@@ -46,7 +46,7 @@ export default {
</template>
<style lang="scss" scoped>
@import '~dashboard/assets/scss/variables';
@import 'dashboard/assets/scss/variables';
.file {
display: flex;
@@ -2,6 +2,7 @@
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
import { MESSAGE_TYPE } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { emitter } from 'shared/helpers/mitt';
export default {
name: 'ReplyTo',
@@ -27,7 +28,7 @@ export default {
},
methods: {
scrollToMessage() {
this.$emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE, {
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE, {
messageId: this.message.id,
});
},
@@ -74,7 +74,7 @@ export default {
this.updateSlaStatus();
this.createTimer();
},
beforeDestroy() {
unmounted() {
if (this.timer) {
clearTimeout(this.timer);
}
@@ -8,6 +8,7 @@ import MenuItem from './menuItem.vue';
import MenuItemWithSubmenu from './menuItemWithSubmenu.vue';
import wootConstants from 'dashboard/constants/globals';
import AgentLoadingPlaceholder from './agentLoadingPlaceholder.vue';
export default {
components: {
MenuItem,
@@ -192,7 +193,7 @@ export default {
v-if="!hasUnreadMessages"
:option="unreadOption"
variant="icon"
@click="$emit('markAsUnread')"
@click.stop="$emit('markAsUnread')"
/>
<template v-for="option in statusMenuConfig">
<MenuItem
@@ -200,14 +201,14 @@ export default {
:key="option.key"
:option="option"
variant="icon"
@click="toggleStatus(option.key, null)"
@click.stop="toggleStatus(option.key, null)"
/>
</template>
<MenuItem
v-if="showSnooze"
:option="snoozeOption"
variant="icon"
@click="snoozeConversation()"
@click.stop="snoozeConversation()"
/>
<MenuItemWithSubmenu :option="priorityConfig">
@@ -215,7 +216,7 @@ export default {
v-for="(option, i) in priorityConfig.options"
:key="i"
:option="option"
@click="assignPriority(option.key)"
@click.stop="assignPriority(option.key)"
/>
</MenuItemWithSubmenu>
<MenuItemWithSubmenu
@@ -227,7 +228,7 @@ export default {
:key="label.id"
:option="generateMenuLabelConfig(label, 'label')"
variant="label"
@click="$emit('assignLabel', label)"
@click.stop="$emit('assignLabel', label)"
/>
</MenuItemWithSubmenu>
<MenuItemWithSubmenu
@@ -241,7 +242,7 @@ export default {
:key="agent.id"
:option="generateMenuLabelConfig(agent, 'agent')"
variant="agent"
@click="$emit('assignAgent', agent)"
@click.stop="$emit('assignAgent', agent)"
/>
</template>
</MenuItemWithSubmenu>
@@ -253,7 +254,7 @@ export default {
v-for="team in teams"
:key="team.id"
:option="generateMenuLabelConfig(team, 'team')"
@click="$emit('assignTeam', team)"
@click.stop="$emit('assignTeam', team)"
/>
</MenuItemWithSubmenu>
</div>
@@ -18,11 +18,7 @@ export default {
</script>
<template>
<div
class="menu text-slate-800 dark:text-slate-100"
role="button"
@click.stop="$emit('click')"
>
<div class="menu text-slate-800 dark:text-slate-100" role="button">
<fluent-icon
v-if="variant === 'icon' && option.icon"
:icon="option.icon"
@@ -5,6 +5,7 @@ import Avatar from '../../Avatar.vue';
// composables
import { useAI } from 'dashboard/composables/useAI';
import { useTrack } from 'dashboard/composables';
// store & api
import { mapGetters } from 'vuex';
@@ -143,7 +144,7 @@ export default {
: this.suggestedLabels,
};
this.$track(event, payload);
useTrack(event, payload);
},
},
};
@@ -1,6 +1,7 @@
<script>
import { getUnixTime } from 'date-fns';
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
import { emitter } from 'shared/helpers/mitt';
import wootConstants from 'dashboard/constants/globals';
import {
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
@@ -58,29 +59,29 @@ export default {
};
},
mounted() {
this.$emitter.on(
emitter.on(
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
this.onCmdSnoozeConversation
);
this.$emitter.on(
emitter.on(
CMD_BULK_ACTION_REOPEN_CONVERSATION,
this.onCmdReopenConversation
);
this.$emitter.on(
emitter.on(
CMD_BULK_ACTION_RESOLVE_CONVERSATION,
this.onCmdResolveConversation
);
},
destroyed() {
this.$emitter.off(
emitter.off(
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
this.onCmdSnoozeConversation
);
this.$emitter.off(
emitter.off(
CMD_BULK_ACTION_REOPEN_CONVERSATION,
this.onCmdReopenConversation
);
this.$emitter.off(
emitter.off(
CMD_BULK_ACTION_RESOLVE_CONVERSATION,
this.onCmdResolveConversation
);
@@ -1,5 +1,5 @@
<script setup>
import { useI18n } from 'dashboard/composables/useI18n';
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
@@ -1,82 +1,67 @@
<script>
import { computed } from 'vue';
<script setup>
import { ref, computed, watch, onMounted, nextTick, useSlots } from 'vue';
import { useMapGetter } from 'dashboard/composables/store';
export default {
props: {
conversationLabels: {
type: Array,
required: true,
},
const props = defineProps({
conversationLabels: {
type: Array,
required: true,
},
setup(props) {
const accountLabels = useMapGetter('labels/getLabels');
});
const activeLabels = computed(() => {
return accountLabels.value.filter(({ title }) =>
props.conversationLabels.includes(title)
);
});
const slots = useSlots();
const accountLabels = useMapGetter('labels/getLabels');
return {
activeLabels,
};
},
data() {
return {
showAllLabels: false,
showExpandLabelButton: false,
labelPosition: -1,
};
},
watch: {
activeLabels() {
this.$nextTick(() => this.computeVisibleLabelPosition());
},
},
mounted() {
// the problem here is that there is a certain amount of delay between the conversation
// card being mounted and the resize event eventually being triggered
// This means we need to run the function immediately after the component is mounted
// Happens especially when used in a virtual list.
// We can make the first trigger, a standard part of the directive, in case
// we face this issue again
this.computeVisibleLabelPosition();
},
methods: {
onShowLabels(e) {
e.stopPropagation();
this.showAllLabels = !this.showAllLabels;
this.$nextTick(() => this.computeVisibleLabelPosition());
},
computeVisibleLabelPosition() {
const beforeSlot = this.$slots.before ? 100 : 0;
const labelContainer = this.$refs.labelContainer;
if (!labelContainer) return;
const activeLabels = computed(() => {
return accountLabels.value.filter(({ title }) =>
props.conversationLabels.includes(title)
);
});
const labels = Array.from(labelContainer.querySelectorAll('.label'));
let labelOffset = 0;
this.showExpandLabelButton = false;
labels.forEach((label, index) => {
labelOffset += label.offsetWidth + 8;
if (labelOffset < labelContainer.clientWidth - 16 - beforeSlot) {
this.labelPosition = index;
} else {
this.showExpandLabelButton = labels.length > 1;
}
});
},
},
const showAllLabels = ref(false);
const showExpandLabelButton = ref(false);
const labelPosition = ref(-1);
const labelContainer = ref(null);
const computeVisibleLabelPosition = () => {
const beforeSlot = slots.before ? 100 : 0;
if (!labelContainer.value) {
return;
}
const labels = Array.from(labelContainer.value.querySelectorAll('.label'));
let labelOffset = 0;
showExpandLabelButton.value = false;
labels.forEach((label, index) => {
labelOffset += label.offsetWidth + 8;
if (labelOffset < labelContainer.value.clientWidth - beforeSlot) {
labelPosition.value = index;
} else {
showExpandLabelButton.value = labels.length > 1;
}
});
};
watch(activeLabels, () => {
nextTick(() => computeVisibleLabelPosition());
});
onMounted(() => {
computeVisibleLabelPosition();
});
const onShowLabels = e => {
e.stopPropagation();
showAllLabels.value = !showAllLabels.value;
nextTick(() => computeVisibleLabelPosition());
};
</script>
<template>
<div
v-if="activeLabels.length || $slots.before"
ref="labelContainer"
v-resize="computeVisibleLabelPosition"
>
<div ref="labelContainer" v-resize="computeVisibleLabelPosition">
<div
v-if="activeLabels.length || $slots.before"
class="flex items-end flex-shrink min-w-0 gap-y-1"
:class="{ 'h-auto overflow-visible flex-row flex-wrap': showAllLabels }"
>
@@ -90,7 +75,9 @@ export default {
variant="smooth"
class="!mb-0 max-w-[calc(100%-0.5rem)]"
small
:class="{ hidden: !showAllLabels && index > labelPosition }"
:class="{
'invisible absolute': !showAllLabels && index > labelPosition,
}"
/>
<woot-button
v-if="showExpandLabelButton"
@@ -123,8 +110,4 @@ export default {
@apply border border-solid border-slate-100 dark:border-slate-700;
}
}
.hidden {
@apply invisible absolute;
}
</style>
@@ -1,7 +1,7 @@
<script setup>
import { reactive, computed, onMounted, ref } from 'vue';
import { useVuelidate } from '@vuelidate/core';
import { useI18n } from 'dashboard/composables/useI18n';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import LinearAPI from 'dashboard/api/integrations/linear';
import validations from './validations';
@@ -1,5 +1,5 @@
<script setup>
import { useI18n } from 'dashboard/composables/useI18n';
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import LinkIssue from './LinkIssue.vue';
import CreateIssue from './CreateIssue.vue';
@@ -65,8 +65,9 @@ const onClickTabChange = index => {
@change="onClickTabChange"
>
<woot-tabs-item
v-for="tab in tabs"
v-for="(tab, index) in tabs"
:key="tab.key"
:index="index"
:name="tab.name"
:show-badge="false"
/>
@@ -1,6 +1,6 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import LinearAPI from 'dashboard/api/integrations/linear';
import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
@@ -121,7 +121,7 @@ const linkIssue = async () => {
enable-search
class="left-0 flex flex-col w-full overflow-y-auto h-fit !max-h-[160px] md:left-auto md:right-0 top-10"
@onSearch="onSearch"
@click="onSelectIssue"
@select="onSelectIssue"
/>
</template>
</FilterButton>
@@ -1,5 +1,5 @@
<script setup>
import { ref, computed, defineComponent } from 'vue';
import { ref, computed, defineOptions } from 'vue';
import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue';
@@ -14,7 +14,7 @@ const props = defineProps({
const emit = defineEmits(['change']);
defineComponent({
defineOptions({
name: 'SearchableDropdown',
});
@@ -64,7 +64,7 @@ const selectedItemId = computed(() => selectedItem.value?.id || null);
:input-placeholder="placeholder"
enable-search
class="left-0 flex flex-col w-full overflow-y-auto h-fit !max-h-[160px] md:left-auto md:right-0 top-10"
@click="onSelect"
@select="onSelect"
/>
</template>
</FilterButton>
@@ -1,8 +1,8 @@
<script setup>
import { computed, ref, onMounted, watch, defineComponent, provide } from 'vue';
import { computed, ref, onMounted, watch, defineOptions, provide } from 'vue';
import { useAlert } from 'dashboard/composables';
import { useStoreGetters } from 'dashboard/composables/store';
import { useI18n } from 'dashboard/composables/useI18n';
import { useI18n } from 'vue-i18n';
import LinearAPI from 'dashboard/api/integrations/linear';
import CreateOrLinkIssue from './CreateOrLinkIssue.vue';
import Issue from './Issue.vue';
@@ -15,7 +15,7 @@ const props = defineProps({
},
});
defineComponent({
defineOptions({
name: 'Linear',
});
@@ -1,7 +1,7 @@
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import VTooltip from 'v-tooltip';
import FloatingVue from 'floating-vue';
import Button from 'dashboard/components/buttons/Button.vue';
import i18n from 'dashboard/i18n';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
@@ -20,7 +20,7 @@ import { emitter } from 'shared/helpers/mitt';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.use(VTooltip);
localVue.use(FloatingVue);
localVue.component('fluent-icon', FluentIcon);
localVue.component('woot-button', Button);
@@ -1,28 +0,0 @@
import ContactBubble from '../bubble/Contact.vue';
export default {
title: 'Components/Messaging/ContactBubble',
component: ContactBubble,
argTypes: {
name: {
defaultValue: 'Eden Hazard',
control: {
type: 'string',
},
},
phoneNumber: {
defaultValue: '+517554433220',
control: {
type: 'string',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { ContactBubble },
template: '<contact-bubble v-bind="$props" />',
});
export const ContactBubbleView = Template.bind({});
@@ -1,34 +0,0 @@
import LocationBubble from '../bubble/Location.vue';
export default {
title: 'Components/Help Center',
component: LocationBubble,
argTypes: {
latitude: {
defaultValue: 1,
control: {
type: 'number',
},
},
longitude: {
defaultValue: 1,
control: {
type: 'number',
},
},
name: {
defaultValue: '420, Dope street',
control: {
type: 'string',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { LocationBubble },
template: '<location-bubble v-bind="$props" />',
});
export const LocationBubbleView = Template.bind({});
@@ -1,27 +0,0 @@
import ReplyEmailHead from '../ReplyEmailHead';
export default {
title: 'Components/ReplyBox/EmailHead',
component: ReplyEmailHead,
argTypes: {
ccEmails: {
control: {
type: 'string',
},
},
bccEmails: {
control: {
type: 'string',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { ReplyEmailHead },
template:
'<reply-email-head v-bind="$props" @add="onAdd" @click="onClick"></reply-email-head>',
});
export const Add = Template.bind({});