feat: Use vue-router on widget route management (#3415)

* feat: Add vue-router to widget

Co-authored-by: Pranav <pranav@chatwoot.com>

* Move to dynamic imports

* Move to routerMixin

* Fix popup button display

* Remove unnecessary import

* router -> route

* Fix open state

* Fix issues

* Remove used CSS

* Fix specs

* Fix specs

* Fix widgetColor specs

* Fix mutation specs

* Fixes broken lint errors

* Fixes issues with widget flow

Co-authored-by: Nithin <nithin@chatwoot.com>
Co-authored-by: Nithin David <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Pranav Raj S
2022-01-12 02:55:27 -08:00
committed by GitHub
parent 991a42c417
commit 9c31d7c672
38 changed files with 617 additions and 725 deletions

View File

@@ -0,0 +1,28 @@
<template>
<unread-message-list :messages="messages" />
</template>
<script>
import { mapGetters } from 'vuex';
import UnreadMessageList from '../components/UnreadMessageList.vue';
export default {
name: 'Campaigns',
components: {
UnreadMessageList,
},
computed: {
...mapGetters({ campaign: 'campaign/getActiveCampaign' }),
messages() {
const { sender, id: campaignId, message: content } = this.campaign;
return [
{
content,
sender,
campaignId,
},
];
},
},
};
</script>

View File

@@ -1,113 +1,33 @@
<template>
<div
v-if="!conversationSize && isFetchingList"
class="flex flex-1 items-center h-full bg-black-25 justify-center"
>
<spinner size="" />
</div>
<div v-else class="home" @keydown.esc="closeChat">
<div
class="header-wrap bg-white"
:class="{ expanded: !isHeaderCollapsed, collapsed: isHeaderCollapsed }"
>
<transition
enter-active-class="transition-all delay-200 duration-300 ease"
leave-active-class="transition-all duration-200 ease-in"
enter-class="opacity-0 transform"
enter-to-class="opacity-100 transform"
leave-class="opacity-100 transform"
leave-to-class="opacity-0 transform"
>
<chat-header-expanded
v-if="!isHeaderCollapsed"
:intro-heading="channelConfig.welcomeTitle"
:intro-body="channelConfig.welcomeTagline"
:avatar-url="channelConfig.avatarUrl"
:show-popout-button="showPopoutButton"
/>
<chat-header
v-if="isHeaderCollapsed"
:title="channelConfig.websiteName"
:avatar-url="channelConfig.avatarUrl"
:show-popout-button="showPopoutButton"
:available-agents="availableAgents"
/>
</transition>
</div>
<banner />
<div class="flex flex-1 flex-col justify-end">
<div class="flex flex-1 overflow-auto">
<conversation-wrap
v-if="currentView === 'messageView'"
:grouped-messages="groupedMessages"
/>
<pre-chat-form
v-if="currentView === 'preChatFormView'"
:options="preChatFormOptions"
/>
</div>
<div class="footer-wrap">
<transition
enter-active-class="transition-all delay-300 duration-300 ease"
leave-active-class="transition-all duration-200 ease-in"
enter-class="opacity-0 transform"
enter-to-class="opacity-100 transform translate-y-0"
leave-class="opacity-100 transform translate-y-0"
leave-to-class="opacity-0 transform "
>
<div v-if="currentView === 'messageView'" class="input-wrap">
<chat-footer />
</div>
<team-availability
v-if="currentView === 'cardView'"
:available-agents="availableAgents"
@start-conversation="startConversation"
/>
</transition>
<branding></branding>
<!-- Load Converstion List Components Here -->
</div>
<team-availability
:available-agents="availableAgents"
:has-conversation="!!conversationSize"
@start-conversation="startConversation"
/>
</div>
</template>
<script>
import Branding from 'shared/components/Branding.vue';
import ChatFooter from 'widget/components/ChatFooter.vue';
import ChatHeaderExpanded from 'widget/components/ChatHeaderExpanded.vue';
import ChatHeader from 'widget/components/ChatHeader.vue';
import ConversationWrap from 'widget/components/ConversationWrap.vue';
import { IFrameHelper } from 'widget/helpers/utils';
import configMixin from '../mixins/configMixin';
import TeamAvailability from 'widget/components/TeamAvailability';
import Spinner from 'shared/components/Spinner.vue';
import Banner from 'widget/components/Banner.vue';
import { mapGetters } from 'vuex';
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import PreChatForm from '../components/PreChat/Form';
import { isEmptyObject } from 'widget/helpers/utils';
import routerMixin from 'widget/mixins/routerMixin';
export default {
name: 'Home',
components: {
Branding,
ChatFooter,
ChatHeader,
ChatHeaderExpanded,
ConversationWrap,
PreChatForm,
Spinner,
TeamAvailability,
Banner,
},
mixins: [configMixin],
mixins: [configMixin, routerMixin],
props: {
hasFetched: {
type: Boolean,
default: false,
},
showPopoutButton: {
type: Boolean,
default: false,
},
isCampaignViewClicked: {
type: Boolean,
default: false,
@@ -122,57 +42,9 @@ export default {
computed: {
...mapGetters({
availableAgents: 'agent/availableAgents',
conversationAttributes: 'conversationAttributes/getConversationParams',
conversationSize: 'conversation/getConversationSize',
groupedMessages: 'conversation/getGroupedConversation',
isFetchingList: 'conversation/getIsFetchingList',
currentUser: 'contacts/getCurrentUser',
activeCampaign: 'campaign/getActiveCampaign',
getCampaignHasExecuted: 'campaign/getCampaignHasExecuted',
conversationSize: 'conversation/getConversationSize',
}),
currentView() {
const { email: currentUserEmail = '' } = this.currentUser;
if (this.isHeaderCollapsed) {
if (this.conversationSize) {
return 'messageView';
}
if (
!this.getCampaignHasExecuted &&
((this.preChatFormEnabled &&
!isEmptyObject(this.activeCampaign) &&
this.preChatFormOptions.requireEmail) ||
this.isOnNewConversation ||
(this.preChatFormEnabled && !currentUserEmail))
) {
return 'preChatFormView';
}
return 'messageView';
}
return 'cardView';
},
isOpen() {
return this.conversationAttributes.status === 'open';
},
fileUploadSizeLimit() {
return MAXIMUM_FILE_UPLOAD_SIZE;
},
isHeaderCollapsed() {
if (
!this.hasIntroText ||
this.conversationSize ||
this.isCampaignViewClicked
) {
return true;
}
return this.isOnCollapsedView;
},
hasIntroText() {
return (
this.channelConfig.welcomeTitle || this.channelConfig.welcomeTagline
);
},
},
mounted() {
bus.$on(BUS_EVENTS.START_NEW_CONVERSATION, () => {
@@ -182,73 +54,11 @@ export default {
},
methods: {
startConversation() {
this.isOnCollapsedView = !this.isOnCollapsedView;
},
closeChat() {
IFrameHelper.sendMessage({ event: 'closeChat' });
if (this.preChatFormEnabled && !this.conversationSize) {
return this.replaceRoute('prechat-form');
}
return this.replaceRoute('messages');
},
},
};
</script>
<style scoped lang="scss">
@import '~widget/assets/scss/variables';
@import '~widget/assets/scss/mixins';
.home {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
overflow: hidden;
background: $color-background;
.header-wrap {
border-radius: $space-normal $space-normal 0 0;
flex-shrink: 0;
transition: max-height 300ms;
z-index: 99;
@include shadow-large;
&.expanded {
max-height: 16rem;
}
&.collapsed {
max-height: 4.5rem;
}
@media only screen and (min-device-width: 320px) and (max-device-width: 667px) {
border-radius: 0;
}
}
.footer-wrap {
flex-shrink: 0;
width: 100%;
display: flex;
flex-direction: column;
position: relative;
&:before {
content: '';
position: absolute;
top: -$space-normal;
left: 0;
width: 100%;
height: $space-normal;
opacity: 0.1;
background: linear-gradient(
to top,
$color-background,
rgba($color-background, 0)
);
}
}
.input-wrap {
padding: 0 $space-two;
}
}
</style>

View File

@@ -0,0 +1,27 @@
<template>
<div class="flex flex-col flex-1 overflow-hidden">
<div class="flex flex-1 overflow-auto">
<conversation-wrap :grouped-messages="groupedMessages" />
</div>
<div class="px-5">
<chat-footer />
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import ChatFooter from '../components/ChatFooter.vue';
import ConversationWrap from '../components/ConversationWrap.vue';
export default {
components: { ChatFooter, ConversationWrap },
computed: {
...mapGetters({
groupedMessages: 'conversation/getGroupedConversation',
}),
},
mounted() {
this.$store.dispatch('conversation/setUserLastSeen');
},
};
</script>

View File

@@ -0,0 +1,49 @@
<template>
<div class="flex flex-1 overflow-auto">
<pre-chat-form :options="preChatFormOptions" @submit="onSubmit" />
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import PreChatForm from '../components/PreChat/Form';
import configMixin from '../mixins/configMixin';
import routerMixin from '../mixins/routerMixin';
export default {
components: {
PreChatForm,
},
mixins: [configMixin, routerMixin],
computed: {
...mapGetters({
conversationSize: 'conversation/getConversationSize',
}),
},
watch: {
conversationSize(newSize, oldSize) {
if (!oldSize && newSize > oldSize) {
this.replaceRoute('messages');
}
},
},
methods: {
onSubmit({ fullName, emailAddress, message, activeCampaignId }) {
if (activeCampaignId) {
bus.$emit('execute-campaign', activeCampaignId);
this.$store.dispatch('contacts/update', {
user: {
email: emailAddress,
name: fullName,
},
});
} else {
this.$store.dispatch('conversation/createConversation', {
fullName: fullName,
emailAddress: emailAddress,
message: message,
});
}
},
},
};
</script>

View File

@@ -1,82 +0,0 @@
<template>
<div
id="app"
class="woot-widget-wrap"
:class="{
'is-mobile': isMobile,
'is-widget-right': !isLeftAligned,
'is-bubble-hidden': hideMessageBubble,
}"
>
<home
v-if="showHomePage"
:has-fetched="hasFetched"
:unread-message-count="unreadMessageCount"
:show-popout-button="showPopoutButton"
:is-campaign-view-clicked="isCampaignViewClicked"
/>
<unread
v-else
:show-unread-view="showUnreadView"
:has-fetched="hasFetched"
:unread-message-count="unreadMessageCount"
:hide-message-bubble="hideMessageBubble"
/>
</div>
</template>
<script>
import Home from './Home';
import Unread from './Unread';
export default {
name: 'Router',
components: {
Home,
Unread,
},
props: {
hasFetched: {
type: Boolean,
default: false,
},
isMobile: {
type: Boolean,
default: false,
},
isLeftAligned: {
type: Boolean,
default: false,
},
showUnreadView: {
type: Boolean,
default: false,
},
showCampaignView: {
type: Boolean,
default: false,
},
hideMessageBubble: {
type: Boolean,
default: false,
},
unreadMessageCount: {
type: Number,
default: 0,
},
showPopoutButton: {
type: Boolean,
default: false,
},
isCampaignViewClicked: {
type: Boolean,
default: false,
},
},
computed: {
showHomePage() {
return !this.showUnreadView && !this.showCampaignView;
},
},
};
</script>

View File

@@ -1,180 +0,0 @@
<template>
<div class="unread-wrap">
<div class="close-unread-wrap">
<button
v-if="showCloseButton"
class="button small close-unread-button"
@click="closeFullView"
>
<div class="flex items-center">
<fluent-icon class="mr-1" icon="dismiss" size="12" />
{{ $t('UNREAD_VIEW.CLOSE_MESSAGES_BUTTON') }}
</div>
</button>
</div>
<div class="unread-messages">
<unread-message
v-for="(message, index) in allMessages"
:key="message.id"
:message-type="message.messageType"
:message-id="message.id"
:show-sender="!index"
:sender="message.sender"
:message="getMessageContent(message)"
:campaign-id="message.campaignId"
/>
</div>
<div class="open-read-view-wrap">
<button
v-if="unreadMessageCount"
class="button clear-button"
@click="openFullView"
>
<div class="flex items-center">
<fluent-icon class="mr-2" size="16" icon="arrow-right" />
{{ $t('UNREAD_VIEW.VIEW_MESSAGES_BUTTON') }}
</div>
</button>
</div>
</div>
</template>
<script>
import { IFrameHelper } from 'widget/helpers/utils';
import { mapGetters } from 'vuex';
import configMixin from '../mixins/configMixin';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import UnreadMessage from 'widget/components/UnreadMessage.vue';
export default {
name: 'Unread',
components: {
FluentIcon,
UnreadMessage,
},
mixins: [configMixin],
props: {
hasFetched: {
type: Boolean,
default: false,
},
unreadMessageCount: {
type: Number,
default: 0,
},
hideMessageBubble: {
type: Boolean,
default: false,
},
showUnreadView: {
type: Boolean,
default: false,
},
},
computed: {
...mapGetters({
unreadMessages: 'conversation/getUnreadTextMessages',
campaign: 'campaign/getActiveCampaign',
}),
showCloseButton() {
return this.unreadMessageCount;
},
sender() {
const [firstMessage] = this.unreadMessages;
return firstMessage.sender || {};
},
allMessages() {
if (this.showUnreadView) {
return this.unreadMessages;
}
const { sender, id: campaignId, message: content } = this.campaign;
return [
{
content,
sender,
campaignId,
},
];
},
},
methods: {
openFullView() {
bus.$emit('on-unread-view-clicked');
},
closeFullView() {
if (IFrameHelper.isIFrame()) {
IFrameHelper.sendMessage({
event: 'toggleBubble',
});
}
},
getMessageContent(message) {
const { attachments, content } = message;
const hasAttachments = attachments && attachments.length;
if (content) return content;
if (hasAttachments) return `📑`;
return '';
},
},
};
</script>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables';
.unread-wrap {
width: 100%;
height: auto;
max-height: 100vh;
background: transparent;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: flex-end;
overflow: hidden;
.unread-messages {
padding-bottom: $space-small;
}
.clear-button {
background: transparent;
color: $color-woot;
padding: 0;
border: 0;
font-weight: $font-weight-bold;
font-size: $font-size-medium;
transition: all 0.3s $ease-in-cubic;
margin-left: $space-smaller;
padding-right: $space-one;
&:hover {
transform: translateX($space-smaller);
color: $color-primary-dark;
}
}
.close-unread-button {
background: $color-background;
color: $color-light-gray;
border: 0;
font-weight: $font-weight-medium;
font-size: $font-size-mini;
transition: all 0.3s $ease-in-cubic;
margin-bottom: $space-slab;
border-radius: $space-normal;
&:hover {
color: $color-body;
}
}
.close-unread-wrap {
text-align: left;
}
}
</style>

View File

@@ -0,0 +1,20 @@
<template>
<unread-message-list :messages="messages" />
</template>
<script>
import { mapGetters } from 'vuex';
import UnreadMessageList from '../components/UnreadMessageList.vue';
export default {
name: 'UnreadMessages',
components: {
UnreadMessageList,
},
computed: {
...mapGetters({
messages: 'conversation/getUnreadTextMessages',
}),
},
};
</script>