mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
fix: Render articles in widget if it is available (#7654)
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3 class="text-base font-medium text-slate-900 dark:text-slate-50 mb-0">
|
||||
<h3 class="text-sm font-medium text-slate-800 dark:text-slate-50 mb-0">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<article-list :articles="articles" @click="onArticleClick" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<category-card
|
||||
:title="$t('PORTAL.POPULAR_ARTICLES')"
|
||||
:articles="articles.slice(0, 4)"
|
||||
:articles="articles.slice(0, 6)"
|
||||
@view-all="$emit('view-all')"
|
||||
@view="onArticleClick"
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<li
|
||||
class="py-1 flex items-center justify-between -mx-1 px-1 hover:bg-slate-75 dark:hover:bg-slate-600 rounded cursor-pointer text-slate-700 dark:text-slate-50 dark:hover:text-slate-25 hover:text-slate-900 "
|
||||
@click="onClick"
|
||||
class="py-1 flex items-center justify-between -mx-1 px-1 hover:bg-slate-25 dark:hover:bg-slate-600 rounded cursor-pointer text-slate-700 dark:text-slate-50 dark:hover:text-slate-25 hover:text-slate-900 "
|
||||
>
|
||||
<button
|
||||
class="underline-offset-2 text-sm leading-6 text-left"
|
||||
|
||||
@@ -16,7 +16,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
users() {
|
||||
return this.agents.slice(0, 5).map(agent => ({
|
||||
return this.agents.slice(0, 4).map(agent => ({
|
||||
id: agent.id,
|
||||
avatar: agent.avatar_url,
|
||||
name: agent.name,
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
:class="$dm('bg-white', 'dark:bg-slate-900')"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<button v-if="showBackButton" @click="onBackButtonClick">
|
||||
<button
|
||||
v-if="showBackButton"
|
||||
class="-ml-3 px-2"
|
||||
@click="onBackButtonClick"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="chevron-left"
|
||||
size="24"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<header
|
||||
class="header-expanded py-6 px-5 relative box-border w-full"
|
||||
:class="$dm('bg-white', 'dark:bg-slate-900')"
|
||||
class="header-expanded pt-6 pb-4 px-5 relative box-border w-full bg-transparent"
|
||||
>
|
||||
<div
|
||||
class="flex items-start"
|
||||
@@ -13,23 +12,25 @@
|
||||
:src="avatarUrl"
|
||||
alt="Avatar"
|
||||
/>
|
||||
<header-actions :show-popout-button="showPopoutButton" />
|
||||
<header-actions
|
||||
:show-popout-button="showPopoutButton"
|
||||
:show-end-conversation-button="false"
|
||||
/>
|
||||
</div>
|
||||
<h2
|
||||
v-dompurify-html="introHeading"
|
||||
class="mt-5 text-3xl mb-3 leading-8 font-normal"
|
||||
class="mt-4 text-2xl mb-1.5 font-medium"
|
||||
:class="$dm('text-slate-900', 'dark:text-slate-50')"
|
||||
/>
|
||||
<p
|
||||
v-dompurify-html="introBody"
|
||||
class="text-lg leading-normal"
|
||||
class="text-base leading-normal"
|
||||
:class="$dm('text-slate-700', 'dark:text-slate-200')"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import HeaderActions from './HeaderActions';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
|
||||
@@ -57,10 +58,5 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
widgetColor: 'appConfig/getWidgetColor',
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
61
app/javascript/widget/components/ContinueChatButton.vue
Normal file
61
app/javascript/widget/components/ContinueChatButton.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full justify-between items-center rounded-md ring-1 ring-inset ring-slate-50 px-2 py-2 text-sm text-slate-700 bg-slate-25 hover:bg-slate-50 dark:text-white dark:bg-slate-800 dark:ring-slate-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-woot-600 group"
|
||||
@click="$emit('continue')"
|
||||
>
|
||||
<div
|
||||
class="w-10 h-10 rounded-md bg-slate-75 dark:bg-slate-700 text-lg flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="chat"
|
||||
size="16"
|
||||
class="text-slate-600 dark:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="text-left flex flex-col justify-start flex-grow max-w-[calc(100%-80px)] mx-2 group-hover:opacity-75"
|
||||
>
|
||||
<h5 class="font-medium text-slate-900 dark:text-white">
|
||||
{{ title }}
|
||||
</h5>
|
||||
<p class="h-4 leading-4 flex items-center gap-1">
|
||||
<span
|
||||
v-if="unreadCount > 0"
|
||||
class="inline-flex items-center justify-center rounded-full bg-green-200 px-1 min-w-[16px] leading-4 text-xxs font-medium text-green-700 mr-0.5"
|
||||
>
|
||||
{{ unreadCount }}
|
||||
</span>
|
||||
<span
|
||||
v-dompurify-html="content"
|
||||
class="leading-4 h-4 text-ellipsis overflow-hidden whitespace-nowrap dark:text-slate-25"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-8 h-10 flex items-center justify-center">
|
||||
<fluent-icon icon="chevron-right" />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
|
||||
export default {
|
||||
components: { FluentIcon },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Continue your chat',
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: 'Chat with us',
|
||||
},
|
||||
unreadCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -10,7 +10,7 @@
|
||||
"
|
||||
>
|
||||
<thumbnail
|
||||
size="40px"
|
||||
size="36px"
|
||||
:username="user.name"
|
||||
:src="user.avatar"
|
||||
has-border
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div v-if="showHeaderActions" class="actions flex items-center">
|
||||
<button
|
||||
v-if="canLeaveConversation && hasEndConversationEnabled"
|
||||
v-if="
|
||||
canLeaveConversation &&
|
||||
hasEndConversationEnabled &&
|
||||
showEndConversationButton
|
||||
"
|
||||
class="button transparent compact"
|
||||
:title="$t('END_CONVERSATION')"
|
||||
@click="resolveConversation"
|
||||
@@ -56,6 +60,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showEndConversationButton: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
<template>
|
||||
<div class="px-5">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div
|
||||
class="max-w-xs"
|
||||
:class="$dm('text-black-700', 'dark:text-slate-50')"
|
||||
>
|
||||
<div class="text-base leading-5 font-medium mb-1">
|
||||
<div class="p-4 shadow-sm rounded-md bg-white dark:bg-slate-700">
|
||||
<div class="flex items-center justify-between ">
|
||||
<div class=" ">
|
||||
<div class="text-sm font-medium text-slate-700 dark:text-slate-50">
|
||||
{{
|
||||
isOnline
|
||||
? $t('TEAM_AVAILABILITY.ONLINE')
|
||||
: $t('TEAM_AVAILABILITY.OFFLINE')
|
||||
}}
|
||||
</div>
|
||||
<div class="text-xs leading-3 mt-1">
|
||||
<div class="text-sm mt-1 text-slate-500 dark:text-slate-100">
|
||||
{{ replyWaitMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<available-agents v-if="isOnline" :agents="availableAgents" />
|
||||
</div>
|
||||
<custom-button
|
||||
class="font-medium"
|
||||
block
|
||||
:bg-color="widgetColor"
|
||||
:text-color="textColor"
|
||||
<button
|
||||
class="inline-flex text-sm font-medium rounded-md py-1 mt-2 px-2 -ml-2 leading-6 text-slate-800 dark:text-slate-50 justify-between items-center hover:bg-slate-25 dark:hover:bg-slate-800"
|
||||
:style="{ color: widgetColor }"
|
||||
@click="startConversation"
|
||||
>
|
||||
{{
|
||||
hasConversation ? $t('CONTINUE_CONVERSATION') : $t('START_CONVERSATION')
|
||||
}}
|
||||
</custom-button>
|
||||
<span class="pr-2 text-sm">
|
||||
{{
|
||||
hasConversation
|
||||
? $t('CONTINUE_CONVERSATION')
|
||||
: $t('START_CONVERSATION')
|
||||
}}
|
||||
</span>
|
||||
<fluent-icon icon="arrow-right" size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,18 +37,16 @@ import { mapGetters } from 'vuex';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
import nextAvailabilityTime from 'widget/mixins/nextAvailabilityTime';
|
||||
import AvailableAgents from 'widget/components/AvailableAgents.vue';
|
||||
import CustomButton from 'shared/components/Button';
|
||||
import configMixin from 'widget/mixins/configMixin';
|
||||
import availabilityMixin from 'widget/mixins/availability';
|
||||
import darkMixin from 'widget/mixins/darkModeMixin.js';
|
||||
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
export default {
|
||||
name: 'TeamAvailability',
|
||||
components: {
|
||||
AvailableAgents,
|
||||
CustomButton,
|
||||
FluentIcon,
|
||||
},
|
||||
mixins: [configMixin, nextAvailabilityTime, availabilityMixin, darkMixin],
|
||||
mixins: [configMixin, nextAvailabilityTime, availabilityMixin],
|
||||
props: {
|
||||
availableAgents: {
|
||||
type: Array,
|
||||
@@ -58,6 +56,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
unreadCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-full h-full flex flex-col"
|
||||
:class="$dm('bg-slate-50', 'dark:bg-slate-800')"
|
||||
class="w-full h-full bg-slate-25 dark:bg-slate-800"
|
||||
:class="{ 'overflow-auto': isOnHomeView }"
|
||||
@keydown.esc="closeWindow"
|
||||
>
|
||||
<div
|
||||
class="header-wrap"
|
||||
:class="{
|
||||
expanded: !isHeaderCollapsed,
|
||||
collapsed: isHeaderCollapsed,
|
||||
}"
|
||||
>
|
||||
<transition
|
||||
enter-active-class="transition-all delay-200 duration-300 ease-in"
|
||||
leave-active-class="transition-all duration-200 ease-out"
|
||||
enter-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
<div class="flex flex-col h-full relative">
|
||||
<div
|
||||
class="header-wrap sticky top-0 z-40 transition-all"
|
||||
:class="{
|
||||
expanded: !isHeaderCollapsed,
|
||||
collapsed: isHeaderCollapsed,
|
||||
'custom-header-shadow': isHeaderCollapsed,
|
||||
...opacityClass,
|
||||
}"
|
||||
>
|
||||
<chat-header-expanded
|
||||
v-if="!isHeaderCollapsed"
|
||||
@@ -32,24 +27,14 @@
|
||||
:avatar-url="channelConfig.avatarUrl"
|
||||
:show-popout-button="appConfig.showPopoutButton"
|
||||
:available-agents="availableAgents"
|
||||
:show-back-button="showBackButton"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
<banner />
|
||||
<transition
|
||||
enter-active-class="transition-all delay-300 duration-300 ease-in"
|
||||
leave-active-class="transition-all duration-200 ease-out"
|
||||
enter-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
</div>
|
||||
<banner />
|
||||
<router-view />
|
||||
</transition>
|
||||
<branding
|
||||
class="bg-slate-25 dark:bg-slate-800"
|
||||
:disable-branding="disableBranding"
|
||||
/>
|
||||
|
||||
<branding v-if="!isOnArticleViewer" :disable-branding="disableBranding" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@@ -58,7 +43,6 @@ import Branding from 'shared/components/Branding.vue';
|
||||
import ChatHeader from '../ChatHeader.vue';
|
||||
import ChatHeaderExpanded from '../ChatHeaderExpanded.vue';
|
||||
import configMixin from '../../mixins/configMixin';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { IFrameHelper } from 'widget/helpers/utils';
|
||||
|
||||
@@ -69,34 +53,88 @@ export default {
|
||||
ChatHeader,
|
||||
ChatHeaderExpanded,
|
||||
},
|
||||
mixins: [configMixin, darkModeMixin],
|
||||
mixins: [configMixin],
|
||||
data() {
|
||||
return {
|
||||
showPopoutButton: false,
|
||||
scrollPosition: 0,
|
||||
ticking: true,
|
||||
disableBranding: window.chatwootWebChannel.disableBranding || false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
availableAgents: 'agent/availableAgents',
|
||||
appConfig: 'appConfig/getAppConfig',
|
||||
availableAgents: 'agent/availableAgents',
|
||||
widgetColor: 'appConfig/getWidgetColor',
|
||||
}),
|
||||
portal() {
|
||||
return window.chatwootWebChannel.portal;
|
||||
},
|
||||
isHeaderCollapsed() {
|
||||
if (!this.hasIntroText) {
|
||||
return true;
|
||||
}
|
||||
return this.$route.name !== 'home';
|
||||
return !this.isOnHomeView;
|
||||
},
|
||||
hasIntroText() {
|
||||
return (
|
||||
this.channelConfig.welcomeTitle || this.channelConfig.welcomeTagline
|
||||
);
|
||||
},
|
||||
showBackButton() {
|
||||
return ['article-viewer', 'messages', 'prechat-form'].includes(
|
||||
this.$route.name
|
||||
);
|
||||
},
|
||||
isOnArticleViewer() {
|
||||
return ['article-viewer'].includes(this.$route.name);
|
||||
},
|
||||
isOnHomeView() {
|
||||
return ['home'].includes(this.$route.name);
|
||||
},
|
||||
opacityClass() {
|
||||
if (this.isHeaderCollapsed) {
|
||||
return {};
|
||||
}
|
||||
if (this.scrollPosition > 30) {
|
||||
return { 'opacity-30': true };
|
||||
}
|
||||
if (this.scrollPosition > 25) {
|
||||
return { 'opacity-40': true };
|
||||
}
|
||||
if (this.scrollPosition > 20) {
|
||||
return { 'opacity-60': true };
|
||||
}
|
||||
if (this.scrollPosition > 15) {
|
||||
return { 'opacity-80': true };
|
||||
}
|
||||
if (this.scrollPosition > 10) {
|
||||
return { 'opacity-90': true };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$el.addEventListener('scroll', this.updateScrollPosition);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$el.removeEventListener('scroll', this.updateScrollPosition);
|
||||
},
|
||||
methods: {
|
||||
closeWindow() {
|
||||
IFrameHelper.sendMessage({ event: 'closeWindow' });
|
||||
},
|
||||
updateScrollPosition(event) {
|
||||
this.scrollPosition = event.target.scrollTop;
|
||||
if (!this.ticking) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.ticking = false;
|
||||
});
|
||||
|
||||
this.ticking = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -105,11 +143,13 @@ export default {
|
||||
@import '~widget/assets/scss/variables';
|
||||
@import '~widget/assets/scss/mixins';
|
||||
|
||||
.custom-header-shadow {
|
||||
@include shadow-large;
|
||||
}
|
||||
|
||||
.header-wrap {
|
||||
flex-shrink: 0;
|
||||
transition: max-height 300ms;
|
||||
z-index: 99;
|
||||
@include shadow-large;
|
||||
transition: max-height 100ms;
|
||||
|
||||
&.expanded {
|
||||
max-height: 16rem;
|
||||
|
||||
Reference in New Issue
Block a user