Shivam Mishra
2024-10-02 13:06:30 +05:30
committed by GitHub
parent e0bf2bd9d4
commit 42f6621afb
661 changed files with 15939 additions and 31194 deletions

View File

@@ -86,11 +86,9 @@ export default {
return this.$t('UNREAD_VIEW.BOT');
},
avatarUrl() {
// eslint-disable-next-line
const BotImage = require('dashboard/assets/images/chatwoot_bot.png');
const displayImage = this.useInboxAvatarForBot
? this.inboxAvatarUrl
: BotImage;
: '/assets/images/chatwoot_bot.png';
if (this.message.message_type === MESSAGE_TYPE.TEMPLATE) {
return displayImage;

View File

@@ -122,7 +122,7 @@ export default {
:title="message"
:options="messageContentAttributes.items"
:hide-fields="!!messageContentAttributes.submitted_values"
@click="onOptionSelect"
@option-select="onOptionSelect"
/>
</div>
<ChatForm

View File

@@ -18,10 +18,7 @@ export default {
class="typing-bubble chat-bubble agent"
:class="getThemeClass('bg-white', 'dark:bg-slate-700')"
>
<img
src="~widget/assets/images/typing.gif"
alt="Agent is typing a message"
/>
<img src="assets/images/typing.gif" alt="Agent is typing a message" />
</div>
</div>
</div>
@@ -30,7 +27,8 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
@import '~widget/assets/scss/variables.scss';
@import 'widget/assets/scss/variables.scss';
.agent-message-wrap {
position: sticky;
bottom: $space-smaller;

View File

@@ -31,7 +31,7 @@ export default {
<h3 class="mb-0 text-sm font-medium text-slate-800 dark:text-slate-50">
{{ title }}
</h3>
<ArticleList :articles="articles" @click="onArticleClick" />
<ArticleList :articles="articles" @select-article="onArticleClick" />
<button
class="inline-flex items-center justify-between px-2 py-1 -ml-2 text-sm font-medium leading-6 rounded-md text-slate-800 dark:text-slate-50 hover:bg-slate-25 dark:hover:bg-slate-800 see-articles"
:style="{ color: widgetColor }"

View File

@@ -16,7 +16,7 @@ export default {
},
methods: {
onClick(link) {
this.$emit('click', link);
this.$emit('selectArticle', link);
},
},
};
@@ -29,7 +29,7 @@ export default {
:key="article.slug"
:link="article.link"
:title="article.title"
@click="onClick"
@select-article="onClick"
/>
</ul>
</template>

View File

@@ -18,7 +18,7 @@ export default {
},
methods: {
onClick() {
this.$emit('click', this.link);
this.$emit('selectArticle', this.link);
},
},
};

View File

@@ -32,16 +32,19 @@ export default {
</template>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';
@import 'widget/assets/scss/variables.scss';
.banner {
color: $color-white;
font-size: $font-size-default;
font-weight: $font-weight-bold;
padding: $space-slab;
text-align: center;
&.success {
background: $color-success;
}
&.error {
background: $color-error;
}

View File

@@ -10,6 +10,7 @@ import { BUS_EVENTS } from 'shared/constants/busEvents';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import { DirectUpload } from 'activestorage';
import { mapGetters } from 'vuex';
import { emitter } from 'shared/helpers/mitt';
export default {
components: { FluentIcon, FileUpload, Spinner },
@@ -78,7 +79,7 @@ export default {
upload.create((error, blob) => {
if (error) {
this.$emitter.emit(BUS_EVENTS.SHOW_ALERT, {
emitter.emit(BUS_EVENTS.SHOW_ALERT, {
message: error,
});
} else {
@@ -89,7 +90,7 @@ export default {
}
});
} else {
this.$emitter.emit(BUS_EVENTS.SHOW_ALERT, {
emitter.emit(BUS_EVENTS.SHOW_ALERT, {
message: this.$t('FILE_SIZE_LIMIT', {
MAXIMUM_FILE_UPLOAD_SIZE: this.fileUploadSizeLimit,
}),
@@ -112,7 +113,7 @@ export default {
...this.getLocalFileAttributes(file),
});
} else {
this.$emitter.emit(BUS_EVENTS.SHOW_ALERT, {
emitter.emit(BUS_EVENTS.SHOW_ALERT, {
message: this.$t('FILE_SIZE_LIMIT', {
MAXIMUM_FILE_UPLOAD_SIZE: this.fileUploadSizeLimit,
}),

View File

@@ -9,6 +9,7 @@ import { sendEmailTranscript } from 'widget/api/conversation';
import routerMixin from 'widget/mixins/routerMixin';
import { IFrameHelper } from '../helpers/utils';
import { CHATWOOT_ON_START_CONVERSATION } from '../constants/sdkEvents';
import { emitter } from 'shared/helpers/mitt';
export default {
components: {
@@ -51,7 +52,7 @@ export default {
},
},
mounted() {
this.$emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.toggleReplyTo);
emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.toggleReplyTo);
},
methods: {
...mapActions('conversation', [
@@ -99,12 +100,12 @@ export default {
if (this.hasEmail) {
try {
await sendEmailTranscript();
this.$emitter.emit(BUS_EVENTS.SHOW_ALERT, {
emitter.emit(BUS_EVENTS.SHOW_ALERT, {
message: this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'),
type: 'success',
});
} catch (error) {
this.$emitter.$emit(BUS_EVENTS.SHOW_ALERT, {
emitter.$emit(BUS_EVENTS.SHOW_ALERT, {
message: this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'),
});
}
@@ -157,7 +158,7 @@ export default {
</template>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';
@import 'widget/assets/scss/variables.scss';
.branding {
align-items: center;

View File

@@ -1,4 +1,5 @@
<script>
import { defineAsyncComponent } from 'vue';
import { mapGetters } from 'vuex';
import ChatAttachmentButton from 'widget/components/ChatAttachment.vue';
@@ -8,7 +9,9 @@ import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
import { useDarkMode } from 'widget/composables/useDarkMode';
const EmojiInput = () => import('shared/components/emoji/EmojiInput.vue');
const EmojiInput = defineAsyncComponent(
() => import('shared/components/emoji/EmojiInput.vue')
);
export default {
name: 'ChatInputWrap',
@@ -173,16 +176,16 @@ export default {
/>
<ChatSendButton
v-if="showSendButton"
:on-click="handleButtonClick"
:color="widgetColor"
@click="handleButtonClick"
/>
</div>
</div>
</template>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';
@import '~widget/assets/scss/mixins.scss';
@import 'widget/assets/scss/variables.scss';
@import 'widget/assets/scss/mixins.scss';
.chat-message--input {
align-items: center;

View File

@@ -55,7 +55,7 @@ export default {
</style>
<style lang="scss">
@import '~widget/assets/scss/variables.scss';
@import 'widget/assets/scss/variables.scss';
.chat-bubble .message-content,
.chat-bubble.user {

View File

@@ -16,10 +16,6 @@ export default {
type: Boolean,
default: false,
},
onClick: {
type: Function,
default: () => {},
},
color: {
type: String,
default: '#6e6f73',
@@ -33,7 +29,6 @@ export default {
type="submit"
:disabled="disabled"
class="icon-button flex items-center justify-center ml-1"
@click="onClick"
>
<FluentIcon v-if="!loading" icon="send" :style="`color: ${color}`" />
<Spinner v-else size="small" />

View File

@@ -122,8 +122,8 @@ export default {
</template>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';
@import '~widget/assets/scss/mixins.scss';
@import 'widget/assets/scss/variables.scss';
@import 'widget/assets/scss/mixins.scss';
.conversation--container {
display: flex;
@@ -135,6 +135,7 @@ export default {
&.light-scheme {
color-scheme: light;
}
&.dark-scheme {
color-scheme: dark;
}

View File

@@ -86,7 +86,7 @@ export default {
</template>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables.scss';
@import 'widget/assets/scss/variables.scss';
.file {
.icon-wrap {
@@ -115,6 +115,7 @@ export default {
.link-wrap {
line-height: 1;
}
.meta {
padding-right: $space-smaller;
}

View File

@@ -1,50 +0,0 @@
import { action } from '@storybook/addon-actions';
import wootInput from './Input';
export default {
title: 'Components/Form/Input',
component: wootInput,
argTypes: {
label: {
defaultValue: 'Email Address',
control: {
type: 'text',
},
},
type: {
defaultValue: 'email',
control: {
type: 'text',
},
},
placeholder: {
defaultValue: 'Please enter your email address',
control: {
type: 'text',
},
},
value: {
defaultValue: 'John12@ync.in',
control: {
type: 'text ,number',
},
},
error: {
defaultValue: '',
control: {
type: 'text',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { wootInput },
template: '<woot-input v-bind="$props" @input="onClick"></woot-input>',
});
export const Input = Template.bind({});
Input.args = {
onClick: action('Added'),
};

View File

@@ -1,238 +1,227 @@
<script>
import countries from 'shared/constants/countries.js';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import FormulateInputMixin from '@braid/vue-formulate/src/FormulateInputMixin';
<script setup>
import { ref, computed, watch, useTemplateRef, nextTick, unref } from 'vue';
import countriesList from 'shared/constants/countries.js';
import { useDarkMode } from 'widget/composables/useDarkMode';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import {
getActiveCountryCode,
getActiveDialCode,
} from 'shared/components/PhoneInput/helper';
export default {
components: {
FluentIcon,
const { context } = defineProps({
context: {
type: Object,
default: () => ({}),
},
mixins: [FormulateInputMixin],
props: {
placeholder: {
type: String,
default: '',
},
hasErrorInPhoneInput: {
type: Boolean,
default: false,
},
},
setup() {
const { getThemeClass } = useDarkMode();
return { getThemeClass };
},
data() {
return {
selectedIndex: -1,
showDropdown: false,
searchCountry: '',
activeCountryCode: getActiveCountryCode(),
activeDialCode: getActiveDialCode(),
phoneNumber: '',
};
},
computed: {
countries() {
return [
{
name: this.dropdownFirstItemName,
dial_code: '',
emoji: '',
id: '',
},
...countries,
];
},
dropdownFirstItemName() {
return this.activeCountryCode ? 'Clear selection' : 'Select Country';
},
dropdownClass() {
return `${this.getThemeClass(
'bg-slate-100',
'dark:bg-slate-700'
)} ${this.getThemeClass('text-slate-700', 'dark:text-slate-50')}`;
},
dropdownBackgroundClass() {
return `${this.getThemeClass(
'bg-white',
'dark:bg-slate-700'
)} ${this.getThemeClass('text-slate-700', 'dark:text-slate-50')}`;
},
dropdownItemClass() {
return `${this.getThemeClass(
'text-slate-700',
'dark:text-slate-50'
)} ${this.getThemeClass('hover:bg-slate-50', 'dark:hover:bg-slate-600')}`;
},
activeDropdownItemClass() {
return `active ${this.getThemeClass(
'bg-slate-100',
'dark:bg-slate-800'
)}`;
},
focusedDropdownItemClass() {
return `focus ${this.getThemeClass('bg-slate-50', 'dark:bg-slate-600')}`;
},
inputHasError() {
return this.hasErrorInPhoneInput
? `border-red-200 hover:border-red-300 focus:border-red-300 ${this.inputLightAndDarkModeColor}`
: `hover:border-black-300 focus:border-black-300 ${this.inputLightAndDarkModeColor} ${this.inputBorderColor}`;
},
inputBorderColor() {
return `${this.getThemeClass(
'border-black-200',
'dark:border-black-500'
)}`;
},
inputLightAndDarkModeColor() {
return `${this.getThemeClass(
'bg-white',
'dark:bg-slate-600'
)} ${this.getThemeClass('text-slate-700', 'dark:text-slate-50')}`;
},
items() {
return this.countries.filter(country => {
const { name, dial_code, id } = country;
const search = this.searchCountry.toLowerCase();
return (
name.toLowerCase().includes(search) ||
dial_code.toLowerCase().includes(search) ||
id.toLowerCase().includes(search)
);
});
},
activeCountry() {
return (
this.countries.find(country => country.id === this.activeCountryCode) ||
''
);
},
},
watch: {
items(newItems) {
if (newItems.length < this.selectedIndex + 1) {
// Reset the selected index to 0 if the new items length is less than the selected index.
this.selectedIndex = 0;
}
},
},
methods: {
setContextValue(code) {
// This function is used to set the context value.
// The context value is used to set the value of the phone number field in the pre-chat form.
this.context.model = `${code}${this.phoneNumber}`;
},
dynamicallySetCountryCode(value) {
// This function is used to set the country code dynamically.
// The country and dial code is used to set from the value of the phone number field in the pre-chat form.
if (!value) return;
});
// check the number first four digit and check weather it is available in the countries array or not.
const country = countries.find(code => value.startsWith(code.dial_code));
if (country) {
// if it is available then set the country code and dial code.
this.activeCountryCode = country.id;
this.activeDialCode = country.dial_code;
// set the phone number without dial code.
this.phoneNumber = value.replace(country.dial_code, '');
}
},
onChange(e) {
this.phoneNumber = e.target.value;
this.dynamicallySetCountryCode(this.phoneNumber);
// This function is used to set the context value when the user types in the phone number field.
this.setContextValue(this.activeDialCode);
},
dropdownItem() {
// This function is used to get all the items in the dropdown.
if (!this.showDropdown) return [];
return Array.from(
this.$refs.dropdown?.querySelectorAll(
'div.country-dropdown div.country-dropdown--item'
)
);
},
focusedOrActiveItem(className) {
// This function is used to get the focused or active item in the dropdown.
if (!this.showDropdown) return [];
return Array.from(
this.$refs.dropdown?.querySelectorAll(
`div.country-dropdown div.country-dropdown--item.${className}`
)
);
},
adjustScroll() {
this.$nextTick(() => {
this.scrollToFocusedOrActiveItem(this.focusedOrActiveItem('focus'));
});
},
adjustSelection(direction) {
if (!this.showDropdown) return;
const maxIndex = this.items.length - 1;
if (direction === 'up') {
this.selectedIndex =
this.selectedIndex <= 0 ? maxIndex : this.selectedIndex - 1;
} else if (direction === 'down') {
this.selectedIndex =
this.selectedIndex >= maxIndex ? 0 : this.selectedIndex + 1;
}
this.adjustScroll();
},
moveSelectionUp() {
this.adjustSelection('up');
},
moveSelectionDown() {
this.adjustSelection('down');
},
onSelect() {
if (!this.showDropdown || this.selectedIndex === -1) return;
this.onSelectCountry(this.items[this.selectedIndex]);
},
scrollToFocusedOrActiveItem(item) {
// This function is used to scroll the dropdown to the focused or active item.
const focusedOrActiveItem = item;
if (focusedOrActiveItem.length > 0) {
const dropdown = this.$refs.dropdown;
const dropdownHeight = dropdown.clientHeight;
const itemTop = focusedOrActiveItem[0].offsetTop;
const itemHeight = focusedOrActiveItem[0].offsetHeight;
const scrollPosition = itemTop - dropdownHeight / 2 + itemHeight / 2;
dropdown.scrollTo({
top: scrollPosition,
behavior: 'auto',
});
}
},
onSelectCountry(country) {
this.activeCountryCode = country.id;
this.searchCountry = '';
this.activeDialCode = country.dial_code ? country.dial_code : '';
this.setContextValue(country.dial_code);
this.closeDropdown();
},
toggleCountryDropdown() {
this.showDropdown = !this.showDropdown;
this.selectedIndex = -1;
if (this.showDropdown) {
this.$nextTick(() => {
this.$refs.searchbar.focus();
// This is used to scroll the dropdown to the active item.
this.scrollToFocusedOrActiveItem(this.focusedOrActiveItem('active'));
});
}
},
closeDropdown() {
this.selectedIndex = -1;
this.showDropdown = false;
},
const localValue = ref(context.value || '');
const { getThemeClass: $dm } = useDarkMode();
const selectedIndex = ref(-1);
const showDropdown = ref(false);
const searchCountry = ref('');
const activeCountryCode = ref(getActiveCountryCode());
const activeDialCode = ref(getActiveDialCode());
const phoneNumber = ref('');
const dropdownRef = useTemplateRef('dropdown');
const searchbarRef = useTemplateRef('searchbar');
const placeholder = computed(() => context?.attrs?.placeholder || '');
const hasErrorInPhoneInput = computed(() => context.hasErrorInPhoneInput);
const dropdownFirstItemName = computed(() =>
activeCountryCode.value ? 'Clear selection' : 'Select Country'
);
const countries = computed(() => [
{
name: dropdownFirstItemName.value,
dial_code: '',
emoji: '',
id: '',
},
};
...countriesList,
]);
const dropdownClass = computed(() =>
$dm('bg-slate-100 text-slate-700', 'dark:bg-slate-700 dark:text-slate-50')
);
const dropdownBackgroundClass = computed(() =>
$dm('bg-white text-slate-700', 'dark:bg-slate-700 dark:text-slate-50')
);
const dropdownItemClass = computed(() =>
$dm(
'text-slate-700 hover:bg-slate-50',
'dark:text-slate-50 dark:hover:bg-slate-600'
)
);
const activeDropdownItemClass = computed(
() => `active ${$dm('bg-slate-100', 'dark:bg-slate-800')}`
);
const focusedDropdownItemClass = computed(
() => `focus ${$dm('bg-slate-50', 'dark:bg-slate-600')}`
);
const inputLightAndDarkModeColor = computed(() =>
$dm('bg-white text-slate-700', 'dark:bg-slate-600 dark:text-slate-50')
);
const inputBorderColor = computed(
() => `${$dm('border-black-200', 'dark:border-black-500')}`
);
const inputHasError = computed(() =>
hasErrorInPhoneInput.value
? `border-red-200 hover:border-red-300 focus:border-red-300 ${inputLightAndDarkModeColor.value}`
: `hover:border-black-300 focus:border-black-300 ${inputLightAndDarkModeColor.value} ${inputBorderColor.value}`
);
const items = computed(() => {
return countries.value.filter(country => {
const { name, dial_code, id } = country;
const search = searchCountry.value.toLowerCase();
return (
name.toLowerCase().includes(search) ||
dial_code.toLowerCase().includes(search) ||
id.toLowerCase().includes(search)
);
});
});
const activeCountry = computed(() => {
return countries.value.find(
country => country.id === activeCountryCode.value
);
});
watch(items, newItems => {
if (newItems.length < selectedIndex.value + 1) {
// Reset the selected index to 0 if the new items length is less than the selected index.
selectedIndex.value = 0;
}
});
function setContextValue(code) {
const safeCode = unref(code);
// This function is used to set the context value.
// The context value is used to set the value of the phone number field in the pre-chat form.
localValue.value = `${safeCode}${phoneNumber.value}`;
context.node.input(localValue.value);
}
function dynamicallySetCountryCode(value) {
const safeValue = unref(value);
// This function is used to set the country code dynamically.
// The country and dial code is used to set from the value of the phone number field in the pre-chat form.
if (!safeValue) return;
// check the number first four digit and check weather it is available in the countries array or not.
const country = countries.value.find(code =>
safeValue.startsWith(code.dial_code)
);
if (country) {
// if it is available then set the country code and dial code.
activeCountryCode.value = country.id;
activeDialCode.value = country.dial_code;
// set the phone number without dial code.
phoneNumber.value = safeValue.replace(country.dial_code, '');
}
}
function onChange(e) {
phoneNumber.value = e.target.value;
dynamicallySetCountryCode(phoneNumber);
// This function is used to set the context value when the user types in the phone number field.
setContextValue(activeDialCode);
}
function focusedOrActiveItem(className) {
// This function is used to get the focused or active item in the dropdown.
if (!showDropdown.value) return [];
return Array.from(
dropdownRef.value?.querySelectorAll(
`div.country-dropdown div.country-dropdown--item.${className}`
)
);
}
function scrollToFocusedOrActiveItem(item) {
// This function is used to scroll the dropdown to the focused or active item.
const focusedOrActiveItemLocal = item;
if (focusedOrActiveItemLocal.length > 0) {
const dropdown = dropdownRef.value;
const dropdownHeight = dropdown.clientHeight;
const itemTop = focusedOrActiveItem[0].offsetTop;
const itemHeight = focusedOrActiveItem[0].offsetHeight;
const scrollPosition = itemTop - dropdownHeight / 2 + itemHeight / 2;
dropdown.scrollTo({
top: scrollPosition,
behavior: 'auto',
});
}
}
function adjustScroll() {
nextTick(() => {
scrollToFocusedOrActiveItem(focusedOrActiveItem('focus'));
});
}
function adjustSelection(direction) {
if (!showDropdown.value) return;
const maxIndex = items.value.length - 1;
if (direction === 'up') {
selectedIndex.value =
selectedIndex.value <= 0 ? maxIndex : selectedIndex.value - 1;
} else if (direction === 'down') {
selectedIndex.value =
selectedIndex.value >= maxIndex ? 0 : selectedIndex.value + 1;
}
adjustScroll();
}
function moveSelectionUp() {
adjustSelection('up');
}
function moveSelectionDown() {
adjustSelection('down');
}
function closeDropdown() {
selectedIndex.value = -1;
showDropdown.value = false;
}
function onSelectCountry(country) {
activeCountryCode.value = country.id;
searchCountry.value = '';
activeDialCode.value = country.dial_code ? country.dial_code : '';
setContextValue(country.dial_code);
closeDropdown();
}
function toggleCountryDropdown() {
showDropdown.value = !showDropdown.value;
selectedIndex.value = -1;
if (showDropdown.value) {
nextTick(() => {
searchbarRef.value.focus();
// This is used to scroll the dropdown to the active item.
scrollToFocusedOrActiveItem(focusedOrActiveItem('active'));
});
}
}
function onSelect() {
if (!showDropdown.value || selectedIndex.value === -1) return;
onSelectCountry(items.value[selectedIndex.value]);
}
</script>
<template>
@@ -255,7 +244,7 @@ export default {
<span
v-if="activeDialCode"
class="py-2 pl-2 pr-0 text-base"
:class="getThemeClass('text-slate-700', 'dark:text-slate-50')"
:class="$dm('text-slate-700', 'dark:text-slate-50')"
>
{{ activeDialCode }}
</span>
@@ -282,15 +271,12 @@ export default {
>
<div class="sticky top-0" :class="dropdownBackgroundClass">
<input
ref="searchbar"
v-model="searchCountry"
ref="searchbar"
type="text"
:placeholder="$t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.DROPDOWN_SEARCH')"
class="w-full h-8 px-3 py-2 mt-1 mb-1 text-sm border border-solid rounded outline-none dropdown-search"
:class="[
getThemeClass('bg-slate-50', 'dark:bg-slate-600'),
inputBorderColor,
]"
:class="[$dm('bg-slate-50', 'dark:bg-slate-600'), inputBorderColor]"
/>
</div>
<div
@@ -315,7 +301,7 @@ export default {
<div v-if="items.length === 0">
<span
class="flex justify-center mt-4 text-sm text-center"
:class="getThemeClass('text-slate-700', 'dark:text-slate-50')"
:class="$dm('text-slate-700', 'dark:text-slate-50')"
>
{{ $t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.DROPDOWN_EMPTY') }}
</span>
@@ -325,7 +311,7 @@ export default {
</template>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables.scss';
@import 'widget/assets/scss/variables.scss';
.phone-input--wrap {
.phone-input {

View File

@@ -1,51 +0,0 @@
import { action } from '@storybook/addon-actions';
import wootTextArea from './TextArea';
export default {
title: 'Components/Form/Text Area',
component: wootTextArea,
argTypes: {
label: {
defaultValue: 'Message',
control: {
type: 'text',
},
},
type: {
defaultValue: '',
control: {
type: 'text',
},
},
placeholder: {
defaultValue: 'Please enter your message',
control: {
type: 'text',
},
},
value: {
defaultValue: 'Lorem ipsum is a placeholder text commonly used',
control: {
type: 'text ,number',
},
},
error: {
defaultValue: '',
control: {
type: 'text',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { wootTextArea },
template:
'<woot-text-area v-bind="$props" @input="onClick"></woot-text-area>',
});
export const TextArea = Template.bind({});
TextArea.args = {
onClick: action('Added'),
};

View File

@@ -127,7 +127,7 @@ export default {
</template>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';
@import 'widget/assets/scss/variables.scss';
.actions {
button {
@@ -142,6 +142,7 @@ export default {
.close-button {
display: none;
}
.rn-close-button {
display: block !important;
}

View File

@@ -9,9 +9,6 @@ export default {
onImgError() {
this.$emit('error');
},
onClick() {
this.$emit('click');
},
},
};
</script>
@@ -24,19 +21,14 @@ export default {
class="image"
>
<div class="wrap">
<img
:src="thumb"
alt="Picture message"
@click="onClick"
@error="onImgError"
/>
<img :src="thumb" alt="Picture message" @error="onImgError" />
<span class="time">{{ readableTime }}</span>
</div>
</a>
</template>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables.scss';
@import 'widget/assets/scss/variables.scss';
.image {
display: block;

View File

@@ -10,7 +10,6 @@ export default {
<template>
<button
class="p-1 mb-1 rounded-full dark:text-slate-500 dark:bg-slate-900 text-slate-600 bg-slate-100 hover:text-slate-800"
@click="$emit('click')"
>
<FluentIcon icon="arrow-reply" size="11" class="flex-shrink-0" />
</button>

View File

@@ -9,11 +9,14 @@ import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import routerMixin from 'widget/mixins/routerMixin';
import { useDarkMode } from 'widget/composables/useDarkMode';
import configMixin from 'widget/mixins/configMixin';
import { FormKit, createInput } from '@formkit/vue';
import PhoneInput from 'widget/components/Form/PhoneInput.vue';
export default {
components: {
CustomButton,
Spinner,
FormKit,
},
mixins: [routerMixin, configMixin],
props: {
@@ -23,9 +26,13 @@ export default {
},
},
setup() {
const phoneInput = createInput(PhoneInput, {
props: ['hasErrorInPhoneInput'],
});
const { formatMessage } = useMessageFormatter();
const { getThemeClass } = useDarkMode();
return { formatMessage, getThemeClass };
return { formatMessage, phoneInput, getThemeClass };
},
data() {
return {
@@ -98,7 +105,7 @@ export default {
...field,
type:
field.name === 'phoneNumber'
? 'phoneInput'
? this.phoneInput
: this.findFieldType(field.type),
}));
},
@@ -156,8 +163,9 @@ export default {
'dark:text-red-400'
)}`;
},
inputClass(context) {
const { hasErrors, classification, type } = context;
inputClass(input) {
const { state, family: classification, type } = input.context;
const hasErrors = state.invalid;
if (classification === 'box' && type === 'checkbox') {
return '';
}
@@ -201,9 +209,7 @@ export default {
};
const validationKeys = Object.keys(validations);
const isRequired = this.isContactFieldRequired(name);
const validation = isRequired
? ['bail', 'required']
: ['bail', 'optional'];
const validation = isRequired ? ['required'] : ['optional'];
if (
validationKeys.includes(name) ||
@@ -212,10 +218,13 @@ export default {
) {
const validationType =
validations[type] || validations[name] || validations[field_type];
return validationType ? validation.concat(validationType) : validation;
const allValidations = validationType
? validation.concat(validationType)
: validation;
return allValidations.join('|');
}
return [];
return '';
},
findFieldType(type) {
if (type === 'link') {
@@ -238,7 +247,7 @@ export default {
});
return values;
}
return null;
return {};
},
onSubmit() {
const { emailAddress, fullName, phoneNumber, message } = this.formValues;
@@ -257,10 +266,16 @@ export default {
</script>
<template>
<FormulateForm
<!-- hide the default submit button for now -->
<FormKit
v-model="formValues"
class="flex flex-col flex-1 p-6 overflow-y-auto"
@submit="onSubmit"
type="form"
form-class="flex flex-col flex-1 w-full p-6 overflow-y-auto"
:incomplete-message="false"
:submit-attrs="{
inputClass: 'hidden',
wrapperClass: 'hidden',
}"
>
<div
v-if="shouldShowHeaderMessage"
@@ -268,7 +283,10 @@ export default {
class="mb-4 text-sm leading-5 pre-chat-header-message"
:class="getThemeClass('text-black-800', 'dark:text-slate-50')"
/>
<FormulateInput
<!-- Why do the v-bind shenanigan? Because Formkit API is really bad.
If we just pass the options as is even with null or undefined or false,
it assumes we are trying to make a multicheckbox. This is the best we have for now -->
<FormKit
v-for="item in enabledPreChatFields"
:key="item.name"
:name="item.name"
@@ -276,7 +294,13 @@ export default {
:label="getLabel(item)"
:placeholder="getPlaceHolder(item)"
:validation="getValidation(item)"
:options="getOptions(item)"
v-bind="
item.type === 'select'
? {
options: getOptions(item),
}
: undefined
"
:label-class="context => labelClass(context)"
:input-class="context => inputClass(context)"
:validation-messages="{
@@ -292,7 +316,7 @@ export default {
}"
:has-error-in-phone-input="hasErrorInPhoneInput"
/>
<FormulateInput
<FormKit
v-if="!hasActiveCampaign"
name="message"
type="textarea"
@@ -316,44 +340,30 @@ export default {
<Spinner v-if="isCreating" class="p-0" />
{{ $t('START_CONVERSATION') }}
</CustomButton>
</FormulateForm>
</FormKit>
</template>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables.scss';
::v-deep {
.wrapper[data-type='checkbox'] {
.formulate-input-wrapper {
display: flex;
align-items: center;
line-height: $space-normal;
<style lang="scss">
.formkit-outer {
@apply mt-2;
}
label {
margin-left: 0.2rem;
}
}
}
@media (prefers-color-scheme: dark) {
.wrapper {
.formulate-input-element--date,
.formulate-input-element--checkbox {
input {
color-scheme: dark;
}
}
}
}
.wrapper[data-type='textarea'] {
.formulate-input-element--textarea {
textarea {
min-height: 8rem;
}
}
}
[data-invalid] .formkit-message {
@apply text-red-500 block text-xs font-normal mb-1 w-full;
}
.formkit-outer[data-type='checkbox'] .formkit-wrapper {
@apply flex items-center gap-2 px-0.5;
}
.formkit-messages {
@apply list-none m-0 p-0;
}
@media (prefers-color-scheme: dark) {
.pre-chat-header-message {
.link {
color: $color-woot;
text-decoration: underline;
@apply text-woot-500 underline;
}
}
}

View File

@@ -28,7 +28,7 @@ export default {
},
},
beforeDestroy() {
unmounted() {
clearTimeout(this.timeOutID);
},
methods: {

View File

@@ -7,6 +7,8 @@ import {
ON_CAMPAIGN_MESSAGE_CLICK,
ON_UNREAD_MESSAGE_CLICK,
} from '../constants/widgetBusEvents';
import { emitter } from 'shared/helpers/mitt';
import { useDarkMode } from 'widget/composables/useDarkMode';
export default {
name: 'UnreadMessage',
@@ -50,10 +52,9 @@ export default {
},
avatarUrl() {
// eslint-disable-next-line
const BotImage = require('dashboard/assets/images/chatwoot_bot.png');
const displayImage = this.useInboxAvatarForBot
? this.inboxAvatarUrl
: BotImage;
: '/assets/images/chatwoot_bot.png';
if (this.isSenderExist(this.sender)) {
const { avatar_url: avatarUrl } = this.sender;
return avatarUrl;
@@ -84,9 +85,9 @@ export default {
},
onClickMessage() {
if (this.campaignId) {
this.$emitter.emit(ON_CAMPAIGN_MESSAGE_CLICK, this.campaignId);
emitter.emit(ON_CAMPAIGN_MESSAGE_CLICK, this.campaignId);
} else {
this.$emitter.emit(ON_UNREAD_MESSAGE_CLICK);
emitter.emit(ON_UNREAD_MESSAGE_CLICK);
}
},
},
@@ -119,7 +120,8 @@ export default {
</template>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables.scss';
@import 'widget/assets/scss/variables.scss';
.chat-bubble {
max-width: 85%;
padding: $space-normal;
@@ -132,10 +134,12 @@ export default {
text-align: left;
padding-bottom: $space-small;
font-size: $font-size-small;
.agent--name {
font-weight: $font-weight-medium;
margin-left: $space-smaller;
}
.company--name {
color: $color-light-gray;
margin-left: $space-smaller;

View File

@@ -5,6 +5,7 @@ import { ON_UNREAD_MESSAGE_CLICK } from '../constants/widgetBusEvents';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import UnreadMessage from 'widget/components/UnreadMessage.vue';
import { isWidgetColorLighter } from 'shared/helpers/colorHelper';
import { emitter } from 'shared/helpers/mitt';
export default {
name: 'Unread',
@@ -34,7 +35,7 @@ export default {
},
methods: {
openConversationView() {
this.$emitter.emit(ON_UNREAD_MESSAGE_CLICK);
emitter.emit(ON_UNREAD_MESSAGE_CLICK);
},
closeFullView() {
this.$emit('close');
@@ -100,7 +101,7 @@ export default {
</template>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables';
@import 'widget/assets/scss/variables';
.unread-wrap {
width: 100%;
@@ -147,6 +148,7 @@ export default {
color: $color-body;
}
}
.is-background-light {
color: $color-body !important;
}

View File

@@ -29,12 +29,12 @@ export default {
</template>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';
@import '~widget/assets/scss/mixins.scss';
@import 'widget/assets/scss/variables.scss';
@import 'widget/assets/scss/mixins.scss';
.user-avatar {
@include light-shadow;
background: url('~widget/assets/images/defaultUser.png') center center
background: url('widget/assets/images/defaultUser.png') center center
no-repeat;
background-size: cover;
border-radius: 50%;

View File

@@ -10,7 +10,7 @@ import messageMixin from '../mixins/messageMixin';
import ReplyToChip from 'widget/components/ReplyToChip.vue';
import DragWrapper from 'widget/components/DragWrapper.vue';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { emitter } from 'shared/helpers/mitt';
import { mapGetters } from 'vuex';
export default {
@@ -97,7 +97,7 @@ export default {
this.hasVideoError = true;
},
toggleReply() {
this.$emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.message);
emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.message);
},
},
};

View File

@@ -37,7 +37,7 @@ export default {
</template>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables.scss';
@import 'widget/assets/scss/variables.scss';
.chat-bubble.user::v-deep {
p code {

View File

@@ -28,7 +28,7 @@ export default {
mounted() {
document.addEventListener('keydown', this.onEscape);
},
beforeDestroy() {
unmounted() {
document.removeEventListener('keydown', this.onEscape);
},
methods: {
@@ -76,7 +76,7 @@ export default {
</template>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';
@import 'widget/assets/scss/variables.scss';
.menu-content {
width: max-content;

View File

@@ -54,14 +54,16 @@ export default {
</template>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';
@import 'widget/assets/scss/variables.scss';
.menu-item {
margin-left: $zero !important;
outline: none;
&:last-child {
border-bottom: none;
}
&:disabled {
cursor: not-allowed;
}

View File

@@ -79,7 +79,7 @@ export default {
mounted() {
this.$el.addEventListener('scroll', this.updateScrollPosition);
},
beforeDestroy() {
unmounted() {
this.$el.removeEventListener('scroll', this.updateScrollPosition);
cancelAnimationFrame(this.requestID);
},
@@ -142,8 +142,8 @@ export default {
</template>
<style scoped lang="scss">
@import '~widget/assets/scss/variables';
@import '~widget/assets/scss/mixins';
@import 'widget/assets/scss/variables';
@import 'widget/assets/scss/mixins';
.custom-header-shadow {
@include shadow-large;

View File

@@ -1,30 +0,0 @@
import { action } from '@storybook/addon-actions';
import ArticleHero from '../ArticleHero.vue'; // adjust this path to match your file's location
export default {
title: 'Components/Widgets/ArticleHero',
component: ArticleHero,
argTypes: {
articles: { control: 'array', description: 'Array of articles' },
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { ArticleHero },
template:
'<article-hero v-bind="$props" @view-all-articles="viewAllArticles" />',
methods: {
viewAllArticles: action('view-all-articles'),
},
});
export const Default = Template.bind({});
Default.args = {
articles: [
{ title: 'Article 1', content: 'This is article 1.' },
{ title: 'Article 2', content: 'This is article 2.' },
{ title: 'Article 3', content: 'This is article 3.' },
{ title: 'Article 4', content: 'This is article 4.' },
],
};

View File

@@ -52,7 +52,7 @@ export default {
</template>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables.scss';
@import 'widget/assets/scss/variables.scss';
.article-item {
border-bottom: 1px solid $color-border;

View File

@@ -92,7 +92,7 @@ export default {
@submit.prevent="onSubmit"
>
<input
v-model.trim="email"
v-model="email"
class="form-input"
:placeholder="$t('EMAIL_PLACEHOLDER')"
:class="inputHasError"
@@ -116,7 +116,7 @@ export default {
</template>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables.scss';
@import 'widget/assets/scss/variables.scss';
.email-input-group {
display: flex;

View File

@@ -85,7 +85,7 @@ export default {
</template>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables.scss';
@import 'widget/assets/scss/variables.scss';
.video-call--container {
position: fixed;