feat: Eslint rules (#9839)

# Pull Request Template

## Description

This PR adds new eslint rules to the code base.

**Error rules**

|    Rule name     | Type | Files updated |
| ----------------- | --- | - |
| `vue/block-order`  | error  |    |
| `vue/component-name-in-template-casing`  | error  |    |
| `vue/component-options-name-casing`  | error  |    |
| `vue/custom-event-name-casing`  | error  |    |
| `vue/define-emits-declaration`  | error  |    |
| `vue/no-unused-properties`  | error  |    |
| `vue/define-macros-order`  | error  |    |
| `vue/define-props-declaration`  | error  |    |
| `vue/match-component-import-name`  | error  |    |
| `vue/next-tick-style`  | error  |    |
| `vue/no-bare-strings-in-template`  | error  |    |
| `vue/no-empty-component-block`  | error  |    |
| `vue/no-multiple-objects-in-class`  | error  |    |
| `vue/no-required-prop-with-default`  | error  |    |
| `vue/no-static-inline-styles`  | error  |    |
| `vue/no-template-target-blank`  | error  |    |
| `vue/no-this-in-before-route-enter`  | error  |    |
| `vue/no-undef-components`  | error  |    |
| `vue/no-unused-emit-declarations`  | error  |    |
| `vue/no-unused-refs`  | error  |    |
| `vue/no-use-v-else-with-v-for`  | error  |    |
| `vue/no-useless-v-bind`  | error  |    |
| `vue/no-v-text`  | error  |    |
| `vue/padding-line-between-blocks`  | error  |    |
| ~`vue/prefer-prop-type-boolean-first`~ | ~error~ |  (removed this
rule, cause a bug in displaying custom attributes) |
| `vue/prefer-separate-static-class`  | error  |    |
| `vue/prefer-true-attribute-shorthand`  | error  |    |
| `vue/require-explicit-slots`  | error  |    |
| `vue/require-macro-variable-name`  | error  |    |


**Warn rules**

|    Rule name     | Type | Files updated |
| ---- | ------------- | ------------- |
| `vue/no-root-v-if`  | warn  |    |


Fixes https://linear.app/chatwoot/issue/CW-3492/vue-eslint-rules

## Type of change

- [x] New feature (non-breaking change which adds functionality)


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Fayaz Ahmed <fayazara@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
Sivin Varghese
2024-08-05 14:02:16 +05:30
committed by GitHub
parent 6166ccb014
commit b4b308336f
625 changed files with 23071 additions and 22980 deletions

View File

@@ -1,23 +1,3 @@
<!-- eslint-disable vue/no-mutating-props -->
<template>
<woot-modal :show.sync="show" :on-close="onClose">
<woot-modal-header
:header-title="$t('MERGE_CONTACTS.TITLE')"
:header-content="$t('MERGE_CONTACTS.DESCRIPTION')"
/>
<merge-contact
:primary-contact="primaryContact"
:is-searching="isSearching"
:is-merging="uiFlags.isMerging"
:search-results="searchResults"
@search="onContactSearch"
@cancel="onClose"
@submit="onMergeContacts"
/>
</woot-modal>
</template>
<script>
import { useAlert } from 'dashboard/composables';
import MergeContact from 'dashboard/modules/contact/components/MergeContact.vue';
@@ -88,4 +68,23 @@ export default {
},
};
</script>
<style lang="scss" scoped></style>
<!-- eslint-disable vue/no-mutating-props -->
<template>
<woot-modal :show.sync="show" :on-close="onClose">
<woot-modal-header
:header-title="$t('MERGE_CONTACTS.TITLE')"
:header-content="$t('MERGE_CONTACTS.DESCRIPTION')"
/>
<MergeContact
:primary-contact="primaryContact"
:is-searching="isSearching"
:is-merging="uiFlags.isMerging"
:search-results="searchResults"
@search="onContactSearch"
@cancel="onClose"
@submit="onMergeContacts"
/>
</woot-modal>
</template>

View File

@@ -1,41 +1,3 @@
<!-- eslint-disable vue/no-mutating-props -->
<template>
<modal :show.sync="show" :on-close="onClose">
<woot-modal-header
:header-title="$t('CUSTOM_ATTRIBUTES.ADD.TITLE')"
:header-content="$t('CUSTOM_ATTRIBUTES.ADD.DESC')"
/>
<form class="w-full" @submit.prevent="addCustomAttribute">
<woot-input
v-model.trim="attributeName"
:class="{ error: v$.attributeName.$error }"
class="w-full"
:error="attributeNameError"
:label="$t('CUSTOM_ATTRIBUTES.FORM.NAME.LABEL')"
:placeholder="$t('CUSTOM_ATTRIBUTES.FORM.NAME.PLACEHOLDER')"
@input="v$.attributeName.$touch"
/>
<woot-input
v-model.trim="attributeValue"
class="w-full"
:label="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.LABEL')"
:placeholder="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.PLACEHOLDER')"
/>
<div class="flex items-center justify-end gap-2 px-0 py-2">
<woot-button
:is-disabled="v$.attributeName.$invalid || isCreating"
:is-loading="isCreating"
>
{{ $t('CUSTOM_ATTRIBUTES.FORM.CREATE') }}
</woot-button>
<woot-button variant="clear" @click.prevent="onClose">
{{ $t('CUSTOM_ATTRIBUTES.FORM.CANCEL') }}
</woot-button>
</div>
</form>
</modal>
</template>
<script>
import Modal from 'dashboard/components/Modal.vue';
import { useVuelidate } from '@vuelidate/core';
@@ -98,3 +60,41 @@ export default {
},
};
</script>
<!-- eslint-disable vue/no-mutating-props -->
<template>
<Modal :show.sync="show" :on-close="onClose">
<woot-modal-header
:header-title="$t('CUSTOM_ATTRIBUTES.ADD.TITLE')"
:header-content="$t('CUSTOM_ATTRIBUTES.ADD.DESC')"
/>
<form class="w-full" @submit.prevent="addCustomAttribute">
<woot-input
v-model.trim="attributeName"
:class="{ error: v$.attributeName.$error }"
class="w-full"
:error="attributeNameError"
:label="$t('CUSTOM_ATTRIBUTES.FORM.NAME.LABEL')"
:placeholder="$t('CUSTOM_ATTRIBUTES.FORM.NAME.PLACEHOLDER')"
@input="v$.attributeName.$touch"
/>
<woot-input
v-model.trim="attributeValue"
class="w-full"
:label="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.LABEL')"
:placeholder="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.PLACEHOLDER')"
/>
<div class="flex items-center justify-end gap-2 px-0 py-2">
<woot-button
:is-disabled="v$.attributeName.$invalid || isCreating"
:is-loading="isCreating"
>
{{ $t('CUSTOM_ATTRIBUTES.FORM.CREATE') }}
</woot-button>
<woot-button variant="clear" @click.prevent="onClose">
{{ $t('CUSTOM_ATTRIBUTES.FORM.CANCEL') }}
</woot-button>
</div>
</form>
</Modal>
</template>

View File

@@ -1,9 +1,47 @@
<script>
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
export default {
components: {
EmojiOrIcon,
},
props: {
label: { type: String, required: true },
icon: { type: String, default: '' },
emoji: { type: String, default: '' },
value: { type: [String, Number], default: '' },
showEdit: { type: Boolean, default: false },
},
data() {
return {
isEditing: false,
editedValue: this.value,
};
},
methods: {
focusInput() {
this.$refs.inputfield.focus();
},
onEdit() {
this.isEditing = true;
this.$nextTick(() => {
this.focusInput();
});
},
onUpdate() {
this.isEditing = false;
this.$emit('update', this.editedValue);
},
},
};
</script>
<template>
<div class="contact-attribute">
<div class="title-wrap">
<h4 class="text-sm title">
<div class="title--icon">
<emoji-or-icon :icon="icon" :emoji="emoji" />
<EmojiOrIcon :icon="icon" :emoji="emoji" />
</div>
{{ label }}
</h4>
@@ -49,44 +87,6 @@
</div>
</template>
<script>
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
export default {
components: {
EmojiOrIcon,
},
props: {
label: { type: String, required: true },
icon: { type: String, default: '' },
emoji: { type: String, default: '' },
value: { type: [String, Number], default: '' },
showEdit: { type: Boolean, default: false },
},
data() {
return {
isEditing: false,
editedValue: this.value,
};
},
methods: {
focusInput() {
this.$refs.inputfield.focus();
},
onEdit() {
this.isEditing = true;
this.$nextTick(() => {
this.focusInput();
});
},
onUpdate() {
this.isEditing = false;
this.$emit('update', this.editedValue);
},
},
};
</script>
<style lang="scss" scoped>
.contact-attribute {
margin-bottom: var(--space-small);

View File

@@ -1,28 +1,3 @@
<template>
<div class="option-item--user">
<thumbnail :src="thumbnail" size="28px" :username="name" />
<div class="option__user-data">
<h5 class="option__title">
{{ name }}
<span v-if="identifier" class="user-identifier">
(ID: {{ identifier }})
</span>
</h5>
<p class="option__body">
<span v-if="email" class="email-icon-wrap">
<fluent-icon class="merge-contact--icon" icon="mail" size="12" />
{{ email }}
</span>
<span v-if="phoneNumber" class="phone-icon-wrap">
<fluent-icon class="merge-contact--icon" icon="call" size="12" />
{{ phoneNumber }}
</span>
<span v-if="!phoneNumber && !email">{{ '---' }}</span>
</p>
</div>
</div>
</template>
<script>
import Thumbnail from '../../../components/widgets/Thumbnail.vue';
@@ -55,6 +30,31 @@ export default {
};
</script>
<template>
<div class="option-item--user">
<Thumbnail :src="thumbnail" size="28px" :username="name" />
<div class="option__user-data">
<h5 class="option__title">
{{ name }}
<span v-if="identifier" class="user-identifier">
{{ $t('MERGE_CONTACTS.DROPDOWN_ITEM.ID', { identifier }) }}
</span>
</h5>
<p class="option__body">
<span v-if="email" class="email-icon-wrap">
<fluent-icon class="merge-contact--icon" icon="mail" size="12" />
{{ email }}
</span>
<span v-if="phoneNumber" class="phone-icon-wrap">
<fluent-icon class="merge-contact--icon" icon="call" size="12" />
{{ phoneNumber }}
</span>
<span v-if="!phoneNumber && !email">{{ '---' }}</span>
</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.option-item--user {
@apply flex items-center;

View File

@@ -1,56 +1,3 @@
<template>
<div class="contact-fields">
<h3 class="text-lg title">
{{ $t('CONTACTS_PAGE.FIELDS') }}
</h3>
<attribute
:label="$t('CONTACT_PANEL.EMAIL_ADDRESS')"
icon="mail"
emoji=""
:value="contact.email"
:show-edit="true"
@update="onEmailUpdate"
/>
<attribute
:label="$t('CONTACT_PANEL.PHONE_NUMBER')"
icon="call"
emoji=""
:value="contact.phone_number"
:show-edit="true"
@update="onPhoneUpdate"
/>
<attribute
v-if="additionalAttributes.location"
:label="$t('CONTACT_PANEL.LOCATION')"
icon="map"
emoji="🌍"
:value="additionalAttributes.location"
:show-edit="true"
@update="onLocationUpdate"
/>
<div
v-for="attribute in customAttributekeys"
:key="attribute"
class="custom-attribute--row"
>
<attribute
:label="attribute"
icon="chevron-right"
:value="customAttributes[attribute]"
:show-edit="true"
@update="value => onCustomAttributeUpdate(attribute, value)"
/>
</div>
<woot-button
size="small"
variant="link"
icon="add"
@click="handleCustomCreate"
>
{{ $t('CUSTOM_ATTRIBUTES.ADD.TITLE') }}
</woot-button>
</div>
</template>
<script>
import Attribute from './ContactAttribute.vue';
@@ -93,7 +40,7 @@ export default {
this.$emit('update', { location: value });
},
handleCustomCreate() {
this.$emit('create-attribute');
this.$emit('createAttribute');
},
onCustomAttributeUpdate(key, value) {
this.$emit('update', { custom_attributes: { [key]: value } });
@@ -102,6 +49,60 @@ export default {
};
</script>
<template>
<div class="contact-fields">
<h3 class="text-lg title">
{{ $t('CONTACTS_PAGE.FIELDS') }}
</h3>
<Attribute
:label="$t('CONTACT_PANEL.EMAIL_ADDRESS')"
icon="mail"
emoji=""
:value="contact.email"
show-edit
@update="onEmailUpdate"
/>
<Attribute
:label="$t('CONTACT_PANEL.PHONE_NUMBER')"
icon="call"
emoji=""
:value="contact.phone_number"
show-edit
@update="onPhoneUpdate"
/>
<Attribute
v-if="additionalAttributes.location"
:label="$t('CONTACT_PANEL.LOCATION')"
icon="map"
emoji="🌍"
:value="additionalAttributes.location"
show-edit
@update="onLocationUpdate"
/>
<div
v-for="attribute in customAttributekeys"
:key="attribute"
class="custom-attribute--row"
>
<Attribute
:label="attribute"
icon="chevron-right"
:value="customAttributes[attribute]"
show-edit
@update="value => onCustomAttributeUpdate(attribute, value)"
/>
</div>
<woot-button
size="small"
variant="link"
icon="add"
@click="handleCustomCreate"
>
{{ $t('CUSTOM_ATTRIBUTES.ADD.TITLE') }}
</woot-button>
</div>
</template>
<style scoped lang="scss">
.contact-fields {
margin-top: var(--space-medium);

View File

@@ -1,46 +1,3 @@
<template>
<div class="contact--intro">
<thumbnail
:src="contact.thumbnail"
size="64px"
:username="contact.name"
:status="contact.availability_status"
/>
<div class="contact--details">
<h2 class="text-lg contact--name">
{{ contact.name }}
</h2>
<h3 class="text-base contact--work">
{{ contact.title }}
<i v-if="company.name" class="icon ion-minus-round" />
<span class="company-name">{{ company.name }}</span>
</h3>
<p v-if="additionalAttributes.description" class="contact--bio">
{{ additionalAttributes.description }}
</p>
<social-icons :social-profiles="socialProfiles" />
</div>
<div class="contact-actions">
<woot-button
class="new-message"
size="small expanded"
icon="ion-paper-airplane"
@click="onNewMessageClick"
>
{{ $t('CONTACT_PANEL.NEW_MESSAGE') }}
</woot-button>
<woot-button
variant="hollow"
size="small expanded"
icon="edit"
@click="onEditClick"
>
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
</woot-button>
</div>
</div>
</template>
<script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import SocialIcons from 'dashboard/routes/dashboard/conversation/contact/SocialIcons.vue';
@@ -85,6 +42,50 @@ export default {
};
</script>
<template>
<div class="contact--intro">
<Thumbnail
:src="contact.thumbnail"
size="64px"
:username="contact.name"
:status="contact.availability_status"
/>
<div class="contact--details">
<h2 class="text-lg contact--name">
{{ contact.name }}
</h2>
<h3 class="text-base contact--work">
{{ contact.title }}
<i v-if="company.name" class="icon ion-minus-round" />
<span class="company-name">{{ company.name }}</span>
</h3>
<p v-if="additionalAttributes.description" class="contact--bio">
{{ additionalAttributes.description }}
</p>
<SocialIcons :social-profiles="socialProfiles" />
</div>
<div class="contact-actions">
<woot-button
class="new-message"
size="small expanded"
icon="ion-paper-airplane"
@click="onNewMessageClick"
>
{{ $t('CONTACT_PANEL.NEW_MESSAGE') }}
</woot-button>
<woot-button
variant="hollow"
size="small expanded"
icon="edit"
@click="onEditClick"
>
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
</woot-button>
</div>
</div>
</template>
<style scoped lang="scss">
.contact--details {
margin-top: var(--space-small);

View File

@@ -1,34 +1,3 @@
<template>
<div class="panel">
<contact-intro
:contact="contact"
@message="toggleConversationModal"
@edit="toggleEditModal"
/>
<contact-fields
:contact="contact"
@update="updateField"
@create-attribute="toggleCustomAttributeModal"
/>
<edit-contact
v-if="showEditModal"
:show="showEditModal"
:contact="contact"
@cancel="toggleEditModal"
/>
<new-conversation
v-if="enableNewConversation"
:show="showConversationModal"
:contact="contact"
@cancel="toggleConversationModal"
/>
<add-custom-attribute
:show="showCustomAttributeModal"
@cancel="toggleCustomAttributeModal"
@create="createCustomAttribute"
/>
</div>
</template>
<script>
import EditContact from 'dashboard/routes/dashboard/conversation/contact/EditContact.vue';
import NewConversation from 'dashboard/routes/dashboard/conversation/contact/NewConversation.vue';
@@ -98,6 +67,38 @@ export default {
};
</script>
<template>
<div class="panel">
<ContactIntro
:contact="contact"
@message="toggleConversationModal"
@edit="toggleEditModal"
/>
<ContactFields
:contact="contact"
@update="updateField"
@createAttribute="toggleCustomAttributeModal"
/>
<EditContact
v-if="showEditModal"
:show="showEditModal"
:contact="contact"
@cancel="toggleEditModal"
/>
<NewConversation
v-if="enableNewConversation"
:show="showConversationModal"
:contact="contact"
@cancel="toggleConversationModal"
/>
<AddCustomAttribute
:show="showCustomAttributeModal"
@cancel="toggleCustomAttributeModal"
@create="createCustomAttribute"
/>
</div>
</template>
<style scoped lang="scss">
.panel {
padding: var(--space-normal) var(--space-normal);

View File

@@ -1,118 +1,3 @@
<template>
<form @submit.prevent="onSubmit">
<div>
<div>
<div
class="mt-1 multiselect-wrap--medium"
:class="{ error: v$.parentContact.$error }"
>
<label class="multiselect__label">
{{ $t('MERGE_CONTACTS.PARENT.TITLE') }}
<woot-label
:title="$t('MERGE_CONTACTS.PARENT.HELP_LABEL')"
color-scheme="success"
small
class="ml-2"
/>
</label>
<multiselect
v-model="parentContact"
:options="searchResults"
label="name"
track-by="id"
:internal-search="false"
:clear-on-select="false"
:show-labels="false"
:placeholder="$t('MERGE_CONTACTS.PARENT.PLACEHOLDER')"
:allow-empty="true"
:loading="isSearching"
:max-height="150"
open-direction="top"
@search-change="searchChange"
>
<template slot="singleLabel" slot-scope="props">
<contact-dropdown-item
:thumbnail="props.option.thumbnail"
:identifier="props.option.id"
:name="props.option.name"
:email="props.option.email"
:phone-number="props.option.phone_number"
/>
</template>
<template slot="option" slot-scope="props">
<contact-dropdown-item
:thumbnail="props.option.thumbnail"
:identifier="props.option.id"
:name="props.option.name"
:email="props.option.email"
:phone-number="props.option.phone_number"
/>
</template>
<span slot="noResult">
{{ $t('AGENT_MGMT.SEARCH.NO_RESULTS') }}
</span>
</multiselect>
<span v-if="v$.parentContact.$error" class="message">
{{ $t('MERGE_CONTACTS.FORM.CHILD_CONTACT.ERROR') }}
</span>
</div>
</div>
<div class="flex multiselect-wrap--medium">
<div
class="w-8 relative text-base text-slate-100 dark:text-slate-600 after:content-[''] after:h-12 after:w-0 after:left-4 after:absolute after:border-l after:border-solid after:border-slate-100 after:dark:border-slate-600 before:content-[''] before:h-0 before:w-4 before:left-4 before:top-12 before:absolute before:border-b before:border-solid before:border-slate-100 before:dark:border-slate-600"
>
<fluent-icon
icon="arrow-up"
class="absolute -top-1 left-2"
size="17"
/>
</div>
<div class="flex flex-col w-full">
<label class="multiselect__label">
{{ $t('MERGE_CONTACTS.PRIMARY.TITLE') }}
<woot-label
:title="$t('MERGE_CONTACTS.PRIMARY.HELP_LABEL')"
color-scheme="alert"
small
class="ml-2"
/>
</label>
<multiselect
:value="primaryContact"
disabled
:options="[]"
:show-labels="false"
label="name"
track-by="id"
>
<template slot="singleLabel" slot-scope="props">
<contact-dropdown-item
:thumbnail="props.option.thumbnail"
:name="props.option.name"
:identifier="props.option.id"
:email="props.option.email"
:phone-number="props.option.phoneNumber"
/>
</template>
</multiselect>
</div>
</div>
</div>
<merge-contact-summary
:primary-contact-name="primaryContact.name"
:parent-contact-name="parentContactName"
/>
<div class="flex justify-end gap-2 mt-6">
<woot-button variant="clear" @click.prevent="onCancel">
{{ $t('MERGE_CONTACTS.FORM.CANCEL') }}
</woot-button>
<woot-button type="submit" :is-loading="isMerging">
{{ $t('MERGE_CONTACTS.FORM.SUBMIT') }}
</woot-button>
</div>
</form>
</template>
<script>
import { useVuelidate } from '@vuelidate/core';
import { required } from '@vuelidate/validators';
@@ -180,6 +65,121 @@ export default {
};
</script>
<template>
<form @submit.prevent="onSubmit">
<div>
<div>
<div
class="mt-1 multiselect-wrap--medium"
:class="{ error: v$.parentContact.$error }"
>
<label class="multiselect__label">
{{ $t('MERGE_CONTACTS.PARENT.TITLE') }}
<woot-label
:title="$t('MERGE_CONTACTS.PARENT.HELP_LABEL')"
color-scheme="success"
small
class="ml-2"
/>
</label>
<multiselect
v-model="parentContact"
:options="searchResults"
label="name"
track-by="id"
:internal-search="false"
:clear-on-select="false"
:show-labels="false"
:placeholder="$t('MERGE_CONTACTS.PARENT.PLACEHOLDER')"
allow-empty
:loading="isSearching"
:max-height="150"
open-direction="top"
@search-change="searchChange"
>
<template slot="singleLabel" slot-scope="props">
<ContactDropdownItem
:thumbnail="props.option.thumbnail"
:identifier="props.option.id"
:name="props.option.name"
:email="props.option.email"
:phone-number="props.option.phone_number"
/>
</template>
<template slot="option" slot-scope="props">
<ContactDropdownItem
:thumbnail="props.option.thumbnail"
:identifier="props.option.id"
:name="props.option.name"
:email="props.option.email"
:phone-number="props.option.phone_number"
/>
</template>
<span slot="noResult">
{{ $t('AGENT_MGMT.SEARCH.NO_RESULTS') }}
</span>
</multiselect>
<span v-if="v$.parentContact.$error" class="message">
{{ $t('MERGE_CONTACTS.FORM.CHILD_CONTACT.ERROR') }}
</span>
</div>
</div>
<div class="flex multiselect-wrap--medium">
<div
class="w-8 relative text-base text-slate-100 dark:text-slate-600 after:content-[''] after:h-12 after:w-0 after:left-4 after:absolute after:border-l after:border-solid after:border-slate-100 after:dark:border-slate-600 before:content-[''] before:h-0 before:w-4 before:left-4 before:top-12 before:absolute before:border-b before:border-solid before:border-slate-100 before:dark:border-slate-600"
>
<fluent-icon
icon="arrow-up"
class="absolute -top-1 left-2"
size="17"
/>
</div>
<div class="flex flex-col w-full">
<label class="multiselect__label">
{{ $t('MERGE_CONTACTS.PRIMARY.TITLE') }}
<woot-label
:title="$t('MERGE_CONTACTS.PRIMARY.HELP_LABEL')"
color-scheme="alert"
small
class="ml-2"
/>
</label>
<multiselect
:value="primaryContact"
disabled
:options="[]"
:show-labels="false"
label="name"
track-by="id"
>
<template slot="singleLabel" slot-scope="props">
<ContactDropdownItem
:thumbnail="props.option.thumbnail"
:name="props.option.name"
:identifier="props.option.id"
:email="props.option.email"
:phone-number="props.option.phoneNumber"
/>
</template>
</multiselect>
</div>
</div>
</div>
<MergeContactSummary
:primary-contact-name="primaryContact.name"
:parent-contact-name="parentContactName"
/>
<div class="flex justify-end gap-2 mt-6">
<woot-button variant="clear" @click.prevent="onCancel">
{{ $t('MERGE_CONTACTS.FORM.CANCEL') }}
</woot-button>
<woot-button type="submit" :is-loading="isMerging">
{{ $t('MERGE_CONTACTS.FORM.SUBMIT') }}
</woot-button>
</div>
</form>
</template>
<style lang="scss" scoped>
/* TDOD: Clean errors in forms style */
.error .message {

View File

@@ -1,3 +1,18 @@
<script>
export default {
props: {
primaryContactName: {
type: String,
default: '',
},
parentContactName: {
type: String,
default: '',
},
},
};
</script>
<template>
<div
v-if="parentContactName"
@@ -31,18 +46,3 @@
</ul>
</div>
</template>
<script>
export default {
props: {
primaryContactName: {
type: String,
default: '',
},
parentContactName: {
type: String,
default: '',
},
},
};
</script>

View File

@@ -1,108 +1,3 @@
<template>
<div class="context-menu">
<!-- Add To Canned Responses -->
<woot-modal
v-if="isCannedResponseModalOpen && enabledOptions['cannedResponse']"
:show.sync="isCannedResponseModalOpen"
:on-close="hideCannedResponseModal"
>
<add-canned-modal
:response-content="plainTextContent"
:on-close="hideCannedResponseModal"
/>
</woot-modal>
<!-- Translate Content -->
<translate-modal
v-if="showTranslateModal"
:content="messageContent"
:content-attributes="contentAttributes"
@close="onCloseTranslateModal"
/>
<!-- Confirm Deletion -->
<woot-delete-modal
v-if="showDeleteModal"
class="context-menu--delete-modal"
:show.sync="showDeleteModal"
:on-close="closeDeleteModal"
:on-confirm="confirmDeletion"
:title="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.TITLE')"
:message="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.MESSAGE')"
:confirm-text="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.DELETE')"
:reject-text="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.CANCEL')"
/>
<woot-button
icon="more-vertical"
color-scheme="secondary"
variant="clear"
size="small"
@click="handleOpen"
/>
<woot-context-menu
v-if="isOpen && !isCannedResponseModalOpen"
:x="contextMenuPosition.x"
:y="contextMenuPosition.y"
@close="handleClose"
>
<div class="menu-container">
<menu-item
v-if="enabledOptions['replyTo']"
:option="{
icon: 'arrow-reply',
label: $t('CONVERSATION.CONTEXT_MENU.REPLY_TO'),
}"
variant="icon"
@click="handleReplyTo"
/>
<menu-item
v-if="enabledOptions['copy']"
:option="{
icon: 'clipboard',
label: $t('CONVERSATION.CONTEXT_MENU.COPY'),
}"
variant="icon"
@click="handleCopy"
/>
<menu-item
v-if="enabledOptions['copy']"
:option="{
icon: 'translate',
label: $t('CONVERSATION.CONTEXT_MENU.TRANSLATE'),
}"
variant="icon"
@click="handleTranslate"
/>
<hr />
<menu-item
:option="{
icon: 'link',
label: $t('CONVERSATION.CONTEXT_MENU.COPY_PERMALINK'),
}"
variant="icon"
@click="copyLinkToMessage"
/>
<menu-item
v-if="enabledOptions['cannedResponse']"
:option="{
icon: 'comment-add',
label: $t('CONVERSATION.CONTEXT_MENU.CREATE_A_CANNED_RESPONSE'),
}"
variant="icon"
@click="showCannedResponseModal"
/>
<hr v-if="enabledOptions['delete']" />
<menu-item
v-if="enabledOptions['delete']"
:option="{
icon: 'delete',
label: $t('CONVERSATION.CONTEXT_MENU.DELETE'),
}"
variant="icon"
@click="openDeleteModal"
/>
</div>
</woot-context-menu>
</div>
</template>
<script>
import { useAlert } from 'dashboard/composables';
import { mapGetters } from 'vuex';
@@ -245,6 +140,113 @@ export default {
},
};
</script>
<template>
<div class="context-menu">
<!-- Add To Canned Responses -->
<woot-modal
v-if="isCannedResponseModalOpen && enabledOptions['cannedResponse']"
:show.sync="isCannedResponseModalOpen"
:on-close="hideCannedResponseModal"
>
<AddCannedModal
:response-content="plainTextContent"
:on-close="hideCannedResponseModal"
/>
</woot-modal>
<!-- Translate Content -->
<TranslateModal
v-if="showTranslateModal"
:content="messageContent"
:content-attributes="contentAttributes"
@close="onCloseTranslateModal"
/>
<!-- Confirm Deletion -->
<woot-delete-modal
v-if="showDeleteModal"
class="context-menu--delete-modal"
:show.sync="showDeleteModal"
:on-close="closeDeleteModal"
:on-confirm="confirmDeletion"
:title="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.TITLE')"
:message="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.MESSAGE')"
:confirm-text="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.DELETE')"
:reject-text="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.CANCEL')"
/>
<woot-button
icon="more-vertical"
color-scheme="secondary"
variant="clear"
size="small"
@click="handleOpen"
/>
<woot-context-menu
v-if="isOpen && !isCannedResponseModalOpen"
:x="contextMenuPosition.x"
:y="contextMenuPosition.y"
@close="handleClose"
>
<div class="menu-container">
<MenuItem
v-if="enabledOptions['replyTo']"
:option="{
icon: 'arrow-reply',
label: $t('CONVERSATION.CONTEXT_MENU.REPLY_TO'),
}"
variant="icon"
@click="handleReplyTo"
/>
<MenuItem
v-if="enabledOptions['copy']"
:option="{
icon: 'clipboard',
label: $t('CONVERSATION.CONTEXT_MENU.COPY'),
}"
variant="icon"
@click="handleCopy"
/>
<MenuItem
v-if="enabledOptions['copy']"
:option="{
icon: 'translate',
label: $t('CONVERSATION.CONTEXT_MENU.TRANSLATE'),
}"
variant="icon"
@click="handleTranslate"
/>
<hr />
<MenuItem
:option="{
icon: 'link',
label: $t('CONVERSATION.CONTEXT_MENU.COPY_PERMALINK'),
}"
variant="icon"
@click="copyLinkToMessage"
/>
<MenuItem
v-if="enabledOptions['cannedResponse']"
:option="{
icon: 'comment-add',
label: $t('CONVERSATION.CONTEXT_MENU.CREATE_A_CANNED_RESPONSE'),
}"
variant="icon"
@click="showCannedResponseModal"
/>
<hr v-if="enabledOptions['delete']" />
<MenuItem
v-if="enabledOptions['delete']"
:option="{
icon: 'delete',
label: $t('CONVERSATION.CONTEXT_MENU.DELETE'),
}"
variant="icon"
@click="openDeleteModal"
/>
</div>
</woot-context-menu>
</div>
</template>
<style lang="scss" scoped>
.menu-container {
@apply p-1 bg-white dark:bg-slate-900 shadow-xl rounded-md;

View File

@@ -1,12 +1,3 @@
<template>
<note-list
:is-fetching="uiFlags.isFetching"
:notes="notes"
@add="onAdd"
@delete="onDelete"
/>
</template>
<script>
import { mapGetters } from 'vuex';
import NoteList from './components/NoteList.vue';
@@ -48,3 +39,12 @@ export default {
},
};
</script>
<template>
<NoteList
:is-fetching="uiFlags.isFetching"
:notes="notes"
@add="onAdd"
@delete="onDelete"
/>
</template>

View File

@@ -1,26 +1,3 @@
<template>
<div
class="flex flex-col mb-2 p-4 border border-solid border-slate-75 dark:border-slate-700 overflow-hidden rounded-md flex-grow shadow-sm bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-100"
>
<woot-message-editor
v-model="noteContent"
class="input--note"
:placeholder="$t('NOTES.ADD.PLACEHOLDER')"
:enable-suggestions="false"
/>
<div class="flex justify-end w-full">
<woot-button
color-scheme="warning"
:title="$t('NOTES.ADD.TITLE')"
:is-disabled="buttonDisabled"
@click="onAdd"
>
{{ $t('NOTES.ADD.BUTTON') }} ()
</woot-button>
</div>
</div>
</template>
<script>
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
@@ -59,6 +36,29 @@ export default {
};
</script>
<template>
<div
class="flex flex-col flex-grow p-4 mb-2 overflow-hidden bg-white border border-solid rounded-md shadow-sm border-slate-75 dark:border-slate-700 dark:bg-slate-900 text-slate-700 dark:text-slate-100"
>
<WootMessageEditor
v-model="noteContent"
class="input--note"
:placeholder="$t('NOTES.ADD.PLACEHOLDER')"
:enable-suggestions="false"
/>
<div class="flex justify-end w-full">
<woot-button
color-scheme="warning"
:title="$t('NOTES.ADD.TITLE')"
:is-disabled="buttonDisabled"
@click="onAdd"
>
{{ $t('NOTES.ADD.BUTTON') }} {{ '(⌘⏎)' }}
</woot-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.input--note {
&::v-deep .ProseMirror-menubar {

View File

@@ -1,55 +1,3 @@
<template>
<div
class="flex flex-col flex-grow p-4 mb-2 overflow-hidden bg-white border border-solid rounded-md shadow-sm border-slate-75 dark:border-slate-700 dark:bg-slate-900 text-slate-700 dark:text-slate-100 note-wrap"
>
<div class="flex items-end justify-between gap-1 text-xs">
<div class="flex items-center">
<thumbnail
:title="noteAuthorName"
:src="noteAuthor.thumbnail"
:username="noteAuthorName"
size="20px"
/>
<div class="my-0 mx-1 p-0.5 flex flex-row gap-1">
<span class="font-medium text-slate-800 dark:text-slate-100">
{{ noteAuthorName }}
</span>
<span class="text-slate-700 dark:text-slate-100">
{{ $t('NOTES.LIST.LABEL') }}
</span>
<span class="font-medium text-slate-700 dark:text-slate-100">
{{ readableTime }}
</span>
</div>
</div>
<div class="flex invisible actions">
<woot-button
v-tooltip="$t('NOTES.CONTENT_HEADER.DELETE')"
variant="smooth"
size="tiny"
icon="delete"
color-scheme="secondary"
@click="toggleDeleteModal"
/>
</div>
<woot-delete-modal
v-if="showDeleteModal"
:show.sync="showDeleteModal"
:on-close="closeDelete"
:on-confirm="confirmDeletion"
:title="$t('DELETE_NOTE.CONFIRM.TITLE')"
:message="$t('DELETE_NOTE.CONFIRM.MESSAGE')"
:confirm-text="$t('DELETE_NOTE.CONFIRM.YES')"
:reject-text="$t('DELETE_NOTE.CONFIRM.NO')"
/>
</div>
<p
v-dompurify-html="formatMessage(note || '')"
class="mt-4 note__content"
/>
</div>
</template>
<script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
@@ -115,6 +63,58 @@ export default {
};
</script>
<template>
<div
class="flex flex-col flex-grow p-4 mb-2 overflow-hidden bg-white border border-solid rounded-md shadow-sm border-slate-75 dark:border-slate-700 dark:bg-slate-900 text-slate-700 dark:text-slate-100 note-wrap"
>
<div class="flex items-end justify-between gap-1 text-xs">
<div class="flex items-center">
<Thumbnail
:title="noteAuthorName"
:src="noteAuthor.thumbnail"
:username="noteAuthorName"
size="20px"
/>
<div class="my-0 mx-1 p-0.5 flex flex-row gap-1">
<span class="font-medium text-slate-800 dark:text-slate-100">
{{ noteAuthorName }}
</span>
<span class="text-slate-700 dark:text-slate-100">
{{ $t('NOTES.LIST.LABEL') }}
</span>
<span class="font-medium text-slate-700 dark:text-slate-100">
{{ readableTime }}
</span>
</div>
</div>
<div class="flex invisible actions">
<woot-button
v-tooltip="$t('NOTES.CONTENT_HEADER.DELETE')"
variant="smooth"
size="tiny"
icon="delete"
color-scheme="secondary"
@click="toggleDeleteModal"
/>
</div>
<woot-delete-modal
v-if="showDeleteModal"
:show.sync="showDeleteModal"
:on-close="closeDelete"
:on-confirm="confirmDeletion"
:title="$t('DELETE_NOTE.CONFIRM.TITLE')"
:message="$t('DELETE_NOTE.CONFIRM.MESSAGE')"
:confirm-text="$t('DELETE_NOTE.CONFIRM.YES')"
:reject-text="$t('DELETE_NOTE.CONFIRM.NO')"
/>
</div>
<p
v-dompurify-html="formatMessage(note || '')"
class="mt-4 note__content"
/>
</div>
</template>
<style lang="scss" scoped>
// For RTL direction view
.app-rtl--wrapper {

View File

@@ -1,27 +1,3 @@
<template>
<div>
<add-note @add="onAddNote" />
<contact-note
v-for="note in notes"
:id="note.id"
:key="note.id"
:note="note.content"
:user="note.user"
:created-at="note.created_at"
@edit="onEditNote"
@delete="onDeleteNote"
/>
<div v-if="isFetching" class="text-center p-4 text-base">
<spinner size="" />
<span>{{ $t('NOTES.FETCHING_NOTES') }}</span>
</div>
<div v-else-if="!notes.length" class="text-center p-4 text-base">
<span>{{ $t('NOTES.NOT_AVAILABLE') }}</span>
</div>
</div>
</template>
<script>
import AddNote from './AddNote.vue';
import ContactNote from './ContactNote.vue';
@@ -58,3 +34,27 @@ export default {
},
};
</script>
<template>
<div>
<AddNote @add="onAddNote" />
<ContactNote
v-for="note in notes"
:id="note.id"
:key="note.id"
:note="note.content"
:user="note.user"
:created-at="note.created_at"
@edit="onEditNote"
@delete="onDeleteNote"
/>
<div v-if="isFetching" class="text-center p-4 text-base">
<Spinner size="" />
<span>{{ $t('NOTES.FETCHING_NOTES') }}</span>
</div>
<div v-else-if="!notes.length" class="text-center p-4 text-base">
<span>{{ $t('NOTES.NOT_AVAILABLE') }}</span>
</div>
</div>
</template>

View File

@@ -1,20 +1,3 @@
<template>
<blockquote
ref="messageContainer"
class="message border-l-2 border-slate-100 dark:border-slate-700"
>
<p class="header">
<strong class="text-slate-700 dark:text-slate-100">
{{ author }}
</strong>
{{ $t('SEARCH.WROTE') }}
</p>
<read-more :shrink="isOverflowing" @expand="isOverflowing = false">
<div v-dompurify-html="prepareContent(content)" class="message-content" />
</read-more>
</blockquote>
</template>
<script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import ReadMore from './ReadMore.vue';
@@ -81,6 +64,23 @@ export default {
};
</script>
<template>
<blockquote
ref="messageContainer"
class="message border-l-2 border-slate-100 dark:border-slate-700"
>
<p class="header">
<strong class="text-slate-700 dark:text-slate-100">
{{ author }}
</strong>
{{ $t('SEARCH.WROTE') }}
</p>
<ReadMore :shrink="isOverflowing" @expand="isOverflowing = false">
<div v-dompurify-html="prepareContent(content)" class="message-content" />
</ReadMore>
</blockquote>
</template>
<style scoped lang="scss">
.message {
@apply py-0 px-2 mt-2;

View File

@@ -1,7 +1,17 @@
<script>
export default {
props: {
shrink: {
type: Boolean,
default: false,
},
},
};
</script>
<template>
<div class="read-more">
<div
ref="content"
:class="{
'shrink-container after:shrink-gradient-light dark:after:shrink-gradient-dark':
shrink,
@@ -22,17 +32,6 @@
</div>
</template>
<script>
export default {
props: {
shrink: {
type: Boolean,
default: false,
},
},
};
</script>
<style scoped>
@tailwind components;
@layer components {
@@ -56,7 +55,6 @@ export default {
@apply max-h-[100px] overflow-hidden relative;
}
.shrink-container::after {
@apply content-[''] absolute bottom-0 left-0 right-0 h-[50px] z-10;
}
.read-more-button {
@apply max-w-max absolute bottom-2 left-0 right-0 mx-auto mt-0 z-20 shadow-sm;

View File

@@ -1,27 +1,3 @@
<template>
<div class="input-container" :class="{ 'is-focused': isInputFocused }">
<div class="icon-container">
<fluent-icon icon="search" class="icon" aria-hidden="true" />
</div>
<input
ref="searchInput"
type="search"
class="dark:bg-slate-900"
:placeholder="$t('SEARCH.INPUT_PLACEHOLDER')"
:value="searchQuery"
@focus="onFocus"
@blur="onBlur"
@input="debounceSearch"
/>
<woot-label
:title="$t('SEARCH.PLACEHOLDER_KEYBINDING')"
:show-close="false"
small
class="helper-label"
/>
</div>
</template>
<script>
export default {
data() {
@@ -71,6 +47,30 @@ export default {
};
</script>
<template>
<div class="input-container" :class="{ 'is-focused': isInputFocused }">
<div class="icon-container">
<fluent-icon icon="search" class="icon" aria-hidden="true" />
</div>
<input
ref="searchInput"
type="search"
class="dark:bg-slate-900"
:placeholder="$t('SEARCH.INPUT_PLACEHOLDER')"
:value="searchQuery"
@focus="onFocus"
@blur="onBlur"
@input="debounceSearch"
/>
<woot-label
:title="$t('SEARCH.PLACEHOLDER_KEYBINDING')"
:show-close="false"
small
class="helper-label"
/>
</div>
</template>
<style lang="scss" scoped>
.input-container {
transition: border-bottom 0.2s ease-in-out;

View File

@@ -9,7 +9,7 @@
<div class="search-input">
<fluent-icon icon="search" size="14px" class="search--icon" />
<span
class="text-ellipsis overflow-hidden whitespace-nowrap search-placeholder"
class="overflow-hidden text-ellipsis whitespace-nowrap search-placeholder"
>
{{ $t('CONVERSATION.SEARCH_MESSAGES') }}
</span>
@@ -18,20 +18,6 @@
</div>
</template>
<script>
export default {
props: {
tabs: {
type: Array,
default: () => [],
},
},
data() {
return {};
},
methods: {},
};
</script>
<style lang="scss" scoped>
.search-input-box {
@apply p-2;

View File

@@ -1,27 +1,3 @@
<template>
<router-link :to="navigateTo" class="contact-item">
<woot-thumbnail :src="thumbnail" :username="name" size="24px" />
<div class="ml-2 rtl:mr-2 rtl:ml-0">
<h5 class="text-sm name text-slate-800 dark:text-slate-200">
{{ name }}
</h5>
<p
class="m-0 text-slate-600 dark:text-slate-200 gap-1 text-sm flex items-center"
>
<span v-if="email" class="email text-slate-800 dark:text-slate-200">{{
email
}}</span>
<span v-if="phone" class="separator text-slate-700 dark:text-slate-200">
</span>
<span v-if="phone" class="phone text-slate-800 dark:text-slate-200">
{{ phone }}
</span>
</p>
</div>
</router-link>
</template>
<script>
import { frontendURL } from 'dashboard/helper/URLHelper';
export default {
@@ -59,6 +35,30 @@ export default {
};
</script>
<template>
<router-link :to="navigateTo" class="contact-item">
<woot-thumbnail :src="thumbnail" :username="name" size="24px" />
<div class="ml-2 rtl:mr-2 rtl:ml-0">
<h5 class="text-sm name text-slate-800 dark:text-slate-200">
{{ name }}
</h5>
<p
class="m-0 text-slate-600 dark:text-slate-200 gap-1 text-sm flex items-center"
>
<span v-if="email" class="email text-slate-800 dark:text-slate-200">{{
email
}}</span>
<span v-if="phone" class="separator text-slate-700 dark:text-slate-200">
</span>
<span v-if="phone" class="phone text-slate-800 dark:text-slate-200">
{{ phone }}
</span>
</p>
</div>
</router-link>
</template>
<style scoped lang="scss">
.contact-item {
@apply cursor-pointer flex items-center p-2 rounded-sm hover:bg-slate-25 dark:hover:bg-slate-800;

View File

@@ -1,26 +1,3 @@
<template>
<search-result-section
:title="$t('SEARCH.SECTION.CONTACTS')"
:empty="!contacts.length"
:query="query"
:show-title="showTitle"
:is-fetching="isFetching"
>
<ul v-if="contacts.length" class="search-list">
<li v-for="contact in contacts" :key="contact.id">
<search-result-contact-item
:id="contact.id"
:name="contact.name"
:email="contact.email"
:phone="contact.phone_number"
:account-id="accountId"
:thumbnail="contact.thumbnail"
/>
</li>
</ul>
</search-result-section>
</template>
<script>
import { mapGetters } from 'vuex';
@@ -57,3 +34,26 @@ export default {
},
};
</script>
<template>
<SearchResultSection
:title="$t('SEARCH.SECTION.CONTACTS')"
:empty="!contacts.length"
:query="query"
:show-title="showTitle"
:is-fetching="isFetching"
>
<ul v-if="contacts.length" class="search-list">
<li v-for="contact in contacts" :key="contact.id">
<SearchResultContactItem
:id="contact.id"
:name="contact.name"
:email="contact.email"
:phone="contact.phone_number"
:account-id="accountId"
:thumbnail="contact.thumbnail"
/>
</li>
</ul>
</SearchResultSection>
</template>

View File

@@ -1,43 +1,3 @@
<template>
<router-link :to="navigateTo" class="conversation-item">
<div class="icon-wrap">
<fluent-icon icon="chat-multiple" :size="14" />
</div>
<div class="conversation-details">
<div class="meta-wrap">
<div class="flex">
<woot-label
class="conversation-id"
:title="`#${id}`"
:show-close="false"
small
/>
<div class="inbox-name-wrap">
<inbox-name :inbox="inbox" class="mr-2 rtl:mr-0 rtl:ml-2" />
</div>
</div>
<div>
<span class="created-at">{{ createdAtTime }}</span>
</div>
</div>
<div class="user-details">
<h5 v-if="name" class="text-sm name text-slate-800 dark:text-slate-100">
<span class="pre-text"> {{ $t('SEARCH.FROM') }}: </span>
{{ name }}
</h5>
<h5
v-if="email"
class="overflow-hidden text-sm email text-slate-700 dark:text-slate-200 whitespace-nowrap text-ellipsis"
>
<span class="pre-text">{{ $t('SEARCH.EMAIL') }}:</span>
{{ email }}
</h5>
</div>
<slot />
</div>
</router-link>
</template>
<script>
import { frontendURL } from 'dashboard/helper/URLHelper.js';
import { dynamicTime } from 'shared/helpers/timeHelper';
@@ -95,6 +55,46 @@ export default {
};
</script>
<template>
<router-link :to="navigateTo" class="conversation-item">
<div class="icon-wrap">
<fluent-icon icon="chat-multiple" :size="14" />
</div>
<div class="conversation-details">
<div class="meta-wrap">
<div class="flex">
<woot-label
class="conversation-id"
:title="`#${id}`"
:show-close="false"
small
/>
<div class="inbox-name-wrap">
<InboxName :inbox="inbox" class="mr-2 rtl:mr-0 rtl:ml-2" />
</div>
</div>
<div>
<span class="created-at">{{ createdAtTime }}</span>
</div>
</div>
<div class="user-details">
<h5 v-if="name" class="text-sm name text-slate-800 dark:text-slate-100">
<span class="pre-text"> {{ $t('SEARCH.FROM') }}: </span>
{{ name }}
</h5>
<h5
v-if="email"
class="overflow-hidden text-sm email text-slate-700 dark:text-slate-200 whitespace-nowrap text-ellipsis"
>
<span class="pre-text">{{ $t('SEARCH.EMAIL') }}:</span>
{{ email }}
</h5>
</div>
<slot />
</div>
</router-link>
</template>
<style scoped lang="scss">
.conversation-item {
@apply cursor-pointer flex p-2 rounded hover:bg-slate-25 dark:hover:bg-slate-800;

View File

@@ -1,26 +1,3 @@
<template>
<search-result-section
:title="$t('SEARCH.SECTION.CONVERSATIONS')"
:empty="!conversations.length"
:query="query"
:show-title="showTitle || isFetching"
:is-fetching="isFetching"
>
<ul v-if="conversations.length" class="search-list">
<li v-for="conversation in conversations" :key="conversation.id">
<search-result-conversation-item
:id="conversation.id"
:name="conversation.contact.name"
:email="conversation.contact.email"
:account-id="accountId"
:inbox="conversation.inbox"
:created-at="conversation.created_at"
/>
</li>
</ul>
</search-result-section>
</template>
<script>
import { mapGetters } from 'vuex';
import SearchResultSection from './SearchResultSection.vue';
@@ -56,3 +33,26 @@ export default {
},
};
</script>
<template>
<SearchResultSection
:title="$t('SEARCH.SECTION.CONVERSATIONS')"
:empty="!conversations.length"
:query="query"
:show-title="showTitle || isFetching"
:is-fetching="isFetching"
>
<ul v-if="conversations.length" class="search-list">
<li v-for="conversation in conversations" :key="conversation.id">
<SearchResultConversationItem
:id="conversation.id"
:name="conversation.contact.name"
:email="conversation.contact.email"
:account-id="accountId"
:inbox="conversation.inbox"
:created-at="conversation.created_at"
/>
</li>
</ul>
</SearchResultSection>
</template>

View File

@@ -1,31 +1,3 @@
<template>
<search-result-section
:title="$t('SEARCH.SECTION.MESSAGES')"
:empty="!messages.length"
:query="query"
:show-title="showTitle"
:is-fetching="isFetching"
>
<ul v-if="messages.length" class="search-list">
<li v-for="message in messages" :key="message.id">
<search-result-conversation-item
:id="message.conversation_id"
:account-id="accountId"
:inbox="message.inbox"
:created-at="message.created_at"
:message-id="message.id"
>
<message-content
:author="getName(message)"
:content="message.content"
:search-term="query"
/>
</search-result-conversation-item>
</li>
</ul>
</search-result-section>
</template>
<script>
import { mapGetters } from 'vuex';
import SearchResultConversationItem from './SearchResultConversationItem.vue';
@@ -70,3 +42,31 @@ export default {
},
};
</script>
<template>
<SearchResultSection
:title="$t('SEARCH.SECTION.MESSAGES')"
:empty="!messages.length"
:query="query"
:show-title="showTitle"
:is-fetching="isFetching"
>
<ul v-if="messages.length" class="search-list">
<li v-for="message in messages" :key="message.id">
<SearchResultConversationItem
:id="message.conversation_id"
:account-id="accountId"
:inbox="message.inbox"
:created-at="message.created_at"
:message-id="message.id"
>
<MessageContent
:author="getName(message)"
:content="message.content"
:search-term="query"
/>
</SearchResultConversationItem>
</li>
</ul>
</SearchResultSection>
</template>

View File

@@ -1,19 +1,3 @@
<template>
<section class="result-section">
<div v-if="showTitle" class="header">
<h3 class="text-sm text-slate-800 dark:text-slate-100">{{ title }}</h3>
</div>
<woot-loading-state v-if="isFetching" :message="'Searching'" />
<slot v-else />
<div v-if="empty && !isFetching" class="empty">
<fluent-icon icon="info" size="16px" class="icon" />
<p class="empty-state__text">
{{ $t('SEARCH.EMPTY_STATE', { item: titleCase, query }) }}
</p>
</div>
</section>
</template>
<script>
export default {
props: {
@@ -46,6 +30,25 @@ export default {
};
</script>
<template>
<section class="result-section">
<div v-if="showTitle" class="header">
<h3 class="text-sm text-slate-800 dark:text-slate-100">{{ title }}</h3>
</div>
<woot-loading-state
v-if="isFetching"
:message="$t('SEARCH.SEARCHING_DATA')"
/>
<slot v-else />
<div v-if="empty && !isFetching" class="empty">
<fluent-icon icon="info" size="16px" class="icon" />
<p class="empty-state__text">
{{ $t('SEARCH.EMPTY_STATE', { item: titleCase, query }) }}
</p>
</div>
</section>
</template>
<style scoped lang="scss">
.result-section {
@apply my-2 mx-0;

View File

@@ -1,16 +1,3 @@
<template>
<div class="tab-container">
<woot-tabs :index="activeTab" :border="false" @change="onTabChange">
<woot-tabs-item
v-for="item in tabs"
:key="item.key"
:name="item.name"
:count="item.count"
/>
</woot-tabs>
</div>
</template>
<script>
export default {
props: {
@@ -38,11 +25,25 @@ export default {
methods: {
onTabChange(index) {
this.activeTab = index;
this.$emit('tab-change', this.tabs[index].key);
this.$emit('tabChange', this.tabs[index].key);
},
},
};
</script>
<template>
<div class="tab-container">
<woot-tabs :index="activeTab" :border="false" @change="onTabChange">
<woot-tabs-item
v-for="item in tabs"
:key="item.key"
:name="item.name"
:count="item.count"
/>
</woot-tabs>
</div>
</template>
<style lang="scss" scoped>
.tab-container {
@apply mt-1 border-b border-solid border-slate-100 dark:border-slate-800/50;

View File

@@ -1,71 +1,3 @@
<template>
<div class="search-page">
<div class="page-header">
<woot-button
icon="chevron-left"
variant="smooth"
size="small "
class="back-button"
@click="onBack"
>
{{ $t('GENERAL_SETTINGS.BACK') }}
</woot-button>
</div>
<section class="search-root">
<header>
<search-header @search="onSearch" />
<search-tabs
v-if="query"
:tabs="tabs"
:selected-tab="activeTabIndex"
@tab-change="tab => (selectedTab = tab)"
/>
</header>
<div class="search-results">
<div v-if="showResultsSection">
<search-result-contacts-list
v-if="filterContacts"
:is-fetching="uiFlags.contact.isFetching"
:contacts="contacts"
:query="query"
:show-title="isSelectedTabAll"
/>
<search-result-messages-list
v-if="filterMessages"
:is-fetching="uiFlags.message.isFetching"
:messages="messages"
:query="query"
:show-title="isSelectedTabAll"
/>
<search-result-conversations-list
v-if="filterConversations"
:is-fetching="uiFlags.conversation.isFetching"
:conversations="conversations"
:query="query"
:show-title="isSelectedTabAll"
/>
</div>
<div v-else-if="showEmptySearchResults" class="empty">
<fluent-icon icon="info" size="16px" class="icon" />
<p class="empty-state__text">
{{ $t('SEARCH.EMPTY_STATE_FULL', { query }) }}
</p>
</div>
<div v-else class="empty text-center">
<p class="text-center margin-bottom-0">
<fluent-icon icon="search" size="24px" class="icon" />
</p>
<p class="empty-state__text">
{{ $t('SEARCH.EMPTY_STATE_DEFAULT') }}
</p>
</div>
</div>
</section>
</div>
</template>
<script>
import SearchHeader from './SearchHeader.vue';
import SearchTabs from './SearchTabs.vue';
@@ -208,6 +140,74 @@ export default {
};
</script>
<template>
<div class="search-page">
<div class="page-header">
<woot-button
icon="chevron-left"
variant="smooth"
size="small "
class="back-button"
@click="onBack"
>
{{ $t('GENERAL_SETTINGS.BACK') }}
</woot-button>
</div>
<section class="search-root">
<header>
<SearchHeader @search="onSearch" />
<SearchTabs
v-if="query"
:tabs="tabs"
:selected-tab="activeTabIndex"
@tabChange="tab => (selectedTab = tab)"
/>
</header>
<div class="search-results">
<div v-if="showResultsSection">
<SearchResultContactsList
v-if="filterContacts"
:is-fetching="uiFlags.contact.isFetching"
:contacts="contacts"
:query="query"
:show-title="isSelectedTabAll"
/>
<SearchResultMessagesList
v-if="filterMessages"
:is-fetching="uiFlags.message.isFetching"
:messages="messages"
:query="query"
:show-title="isSelectedTabAll"
/>
<SearchResultConversationsList
v-if="filterConversations"
:is-fetching="uiFlags.conversation.isFetching"
:conversations="conversations"
:query="query"
:show-title="isSelectedTabAll"
/>
</div>
<div v-else-if="showEmptySearchResults" class="empty">
<fluent-icon icon="info" size="16px" class="icon" />
<p class="empty-state__text">
{{ $t('SEARCH.EMPTY_STATE_FULL', { query }) }}
</p>
</div>
<div v-else class="text-center empty">
<p class="text-center margin-bottom-0">
<fluent-icon icon="search" size="24px" class="icon" />
</p>
<p class="empty-state__text">
{{ $t('SEARCH.EMPTY_STATE_DEFAULT') }}
</p>
</div>
</div>
</section>
</div>
</template>
<style lang="scss" scoped>
.search-page {
@apply flex flex-col w-full bg-white dark:bg-slate-900;

View File

@@ -1,52 +1,3 @@
<template>
<div class="widget-preview-container">
<div v-if="isWidgetVisible" class="screen-selector">
<input-radio-group
name="widget-screen"
:items="widgetScreens"
:action="handleScreenChange"
/>
</div>
<div v-if="isWidgetVisible" class="widget-wrapper">
<WidgetHead :config="getWidgetHeadConfig" />
<div>
<WidgetBody :config="getWidgetBodyConfig" />
<WidgetFooter :config="getWidgetFooterConfig" />
<div class="branding">
<a class="branding-link">
<img class="branding-image" :src="globalConfig.logoThumbnail" />
<span>
{{
useInstallationName(
$t('INBOX_MGMT.WIDGET_BUILDER.BRANDING_TEXT'),
globalConfig.installationName
)
}}
</span>
</a>
</div>
</div>
</div>
<div class="widget-bubble" :style="getBubblePositionStyle">
<button
class="bubble"
:class="getBubbleTypeClass"
:style="{ background: color }"
@click="toggleWidget"
>
<img
v-if="!isWidgetVisible"
src="~dashboard/assets/images/bubble-logo.svg"
alt=""
/>
<div>
{{ getWidgetBubbleLauncherTitle }}
</div>
</button>
</div>
</div>
</template>
<script>
import WidgetHead from './WidgetHead.vue';
import WidgetBody from './WidgetBody.vue';
@@ -75,7 +26,6 @@ export default {
},
websiteName: {
type: String,
default: '',
required: true,
},
logo: {
@@ -198,6 +148,55 @@ export default {
};
</script>
<template>
<div class="widget-preview-container">
<div v-if="isWidgetVisible" class="screen-selector">
<InputRadioGroup
name="widget-screen"
:items="widgetScreens"
:action="handleScreenChange"
/>
</div>
<div v-if="isWidgetVisible" class="widget-wrapper">
<WidgetHead :config="getWidgetHeadConfig" />
<div>
<WidgetBody :config="getWidgetBodyConfig" />
<WidgetFooter :config="getWidgetFooterConfig" />
<div class="branding">
<a class="branding-link">
<img class="branding-image" :src="globalConfig.logoThumbnail" />
<span>
{{
useInstallationName(
$t('INBOX_MGMT.WIDGET_BUILDER.BRANDING_TEXT'),
globalConfig.installationName
)
}}
</span>
</a>
</div>
</div>
</div>
<div class="widget-bubble" :style="getBubblePositionStyle">
<button
class="bubble"
:class="getBubbleTypeClass"
:style="{ background: color }"
@click="toggleWidget"
>
<img
v-if="!isWidgetVisible"
src="~dashboard/assets/images/bubble-logo.svg"
alt=""
/>
<div>
{{ getWidgetBubbleLauncherTitle }}
</div>
</button>
</div>
</div>
</template>
<style lang="scss" scoped>
.screen-selector {
display: flex;

View File

@@ -1,51 +1,3 @@
<template>
<div class="widget-body-container">
<div v-if="config.isDefaultScreen" class="availability-content">
<div class="availability-info">
<div class="team-status">
{{ getStatusText }}
</div>
<div class="reply-wait-message">
{{ config.replyTime }}
</div>
</div>
<thumbnail username="J" size="40px" />
</div>
<div v-else class="conversation-content">
<div class="conversation-wrap">
<div class="message-wrap">
<div class="user-message-wrap">
<div class="user-message">
<div class="message-wrap">
<div
class="chat-bubble user"
:style="{ background: config.color }"
>
<p>{{ $t('INBOX_MGMT.WIDGET_BUILDER.BODY.USER_MESSAGE') }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="agent-message-wrap">
<div class="agent-message">
<div class="avatar-wrap" />
<div class="message-wrap">
<div class="chat-bubble agent">
<div class="message-content">
<p>
{{ $t('INBOX_MGMT.WIDGET_BUILDER.BODY.AGENT_MESSAGE') }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
export default {
@@ -85,6 +37,54 @@ export default {
};
</script>
<template>
<div class="widget-body-container">
<div v-if="config.isDefaultScreen" class="availability-content">
<div class="availability-info">
<div class="team-status">
{{ getStatusText }}
</div>
<div class="reply-wait-message">
{{ config.replyTime }}
</div>
</div>
<Thumbnail username="J" size="40px" />
</div>
<div v-else class="conversation-content">
<div class="conversation-wrap">
<div class="message-wrap">
<div class="user-message-wrap">
<div class="user-message">
<div class="message-wrap">
<div
class="chat-bubble user"
:style="{ background: config.color }"
>
<p>{{ $t('INBOX_MGMT.WIDGET_BUILDER.BODY.USER_MESSAGE') }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="agent-message-wrap">
<div class="agent-message">
<div class="avatar-wrap" />
<div class="message-wrap">
<div class="chat-bubble agent">
<div class="message-content">
<p>
{{ $t('INBOX_MGMT.WIDGET_BUILDER.BODY.AGENT_MESSAGE') }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.widget-body-container {
.availability-content {

View File

@@ -1,31 +1,3 @@
<template>
<div class="footer-wrap">
<custom-button
v-if="config.isDefaultScreen"
class="start-conversation"
:style="{ background: config.color }"
>
{{
$t('INBOX_MGMT.WIDGET_BUILDER.FOOTER.START_CONVERSATION_BUTTON_TEXT')
}}
</custom-button>
<div v-else class="chat-message-input is-focused">
<resizable-text-area
id="chat-input"
:rows="1"
:placeholder="
$t('INBOX_MGMT.WIDGET_BUILDER.FOOTER.CHAT_INPUT_PLACEHOLDER')
"
class="user-message-input is-focused"
/>
<div class="button-wrap">
<fluent-icon icon="emoji" />
<fluent-icon class="icon-send" icon="send" />
</div>
</div>
</div>
</template>
<script>
import CustomButton from 'dashboard/components/buttons/Button.vue';
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
@@ -44,6 +16,34 @@ export default {
};
</script>
<template>
<div class="footer-wrap">
<CustomButton
v-if="config.isDefaultScreen"
class="start-conversation"
:style="{ background: config.color }"
>
{{
$t('INBOX_MGMT.WIDGET_BUILDER.FOOTER.START_CONVERSATION_BUTTON_TEXT')
}}
</CustomButton>
<div v-else class="chat-message-input is-focused">
<ResizableTextArea
id="chat-input"
:rows="1"
:placeholder="
$t('INBOX_MGMT.WIDGET_BUILDER.FOOTER.CHAT_INPUT_PLACEHOLDER')
"
class="user-message-input is-focused"
/>
<div class="button-wrap">
<fluent-icon icon="emoji" />
<fluent-icon class="icon-send" icon="send" />
</div>
</div>
</div>
</template>
<style scoped lang="scss">
@import '~dashboard/assets/scss/variables.scss';
.footer-wrap {

View File

@@ -1,3 +1,25 @@
<script>
export default {
props: {
config: {
type: Object,
default: () => {},
},
},
computed: {
isDefaultScreen() {
return (
this.config.isDefaultScreen &&
((this.config.welcomeHeading &&
this.config.welcomeHeading.length !== 0) ||
(this.config.welcomeTagLine &&
this.config.welcomeTagline.length !== 0))
);
},
},
};
</script>
<template>
<div class="header-wrapper">
<div class="header-branding">
@@ -24,28 +46,6 @@
</div>
</template>
<script>
export default {
props: {
config: {
type: Object,
default: () => {},
},
},
computed: {
isDefaultScreen() {
return (
this.config.isDefaultScreen &&
((this.config.welcomeHeading &&
this.config.welcomeHeading.length !== 0) ||
(this.config.welcomeTagLine &&
this.config.welcomeTagline.length !== 0))
);
},
},
};
</script>
<style lang="scss" scoped>
.header-wrapper {
background-color: var(--white);