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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user