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:
28
app/javascript/widget/views/Campaigns.vue
Normal file
28
app/javascript/widget/views/Campaigns.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
27
app/javascript/widget/views/Messages.vue
Normal file
27
app/javascript/widget/views/Messages.vue
Normal 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>
|
||||
49
app/javascript/widget/views/PreChatForm.vue
Normal file
49
app/javascript/widget/views/PreChatForm.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
20
app/javascript/widget/views/UnreadMessages.vue
Normal file
20
app/javascript/widget/views/UnreadMessages.vue
Normal 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>
|
||||
Reference in New Issue
Block a user