feat: Render conversation custom attributes (#3065)
This commit is contained in:
@@ -74,8 +74,4 @@ export default {
|
||||
margin-bottom: var(--space-normal);
|
||||
color: var(--b-500);
|
||||
}
|
||||
|
||||
.conv-details--item {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -61,10 +61,6 @@ export default {
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
|
||||
.conv-details--item {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.custom-attribute--row__attribute {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<template>
|
||||
<div class="conv-details--item">
|
||||
<div class="conv-details--item" :class="{ compact: compact }">
|
||||
<h4 class="conv-details--item__label text-block-title">
|
||||
<span class="title--icon">
|
||||
<emoji-or-icon :icon="icon" :emoji="emoji" />
|
||||
<span>{{ title }}</span>
|
||||
<span class="item__title">
|
||||
{{ title }}
|
||||
</span>
|
||||
<slot name="button"></slot>
|
||||
</h4>
|
||||
@@ -16,17 +15,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmojiOrIcon,
|
||||
},
|
||||
props: {
|
||||
title: { type: String, required: true },
|
||||
icon: { type: String, default: '' },
|
||||
emoji: { type: String, default: '' },
|
||||
value: { type: [String, Number], default: '' },
|
||||
compact: { type: Boolean, default: false },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -34,6 +29,12 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
.conv-details--item {
|
||||
overflow: auto;
|
||||
padding: var(--space-slab) var(--space-normal);
|
||||
|
||||
&.compact {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.conv-details--item__label {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@@ -46,19 +47,6 @@ export default {
|
||||
|
||||
.conv-details--item__value {
|
||||
word-break: break-all;
|
||||
margin-left: var(--space-medium);
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
|
||||
.title--icon .icon--emoji,
|
||||
.title--icon .icon--font {
|
||||
display: inline-block;
|
||||
width: var(--space-medium);
|
||||
}
|
||||
|
||||
.title--icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<div>
|
||||
<div class="multiselect-wrap--small">
|
||||
<contact-details-item
|
||||
compact
|
||||
:title="$t('CONVERSATION_SIDEBAR.ASSIGNEE_LABEL')"
|
||||
>
|
||||
<template v-slot:button>
|
||||
@@ -45,6 +46,7 @@
|
||||
</div>
|
||||
<div class="multiselect-wrap--small">
|
||||
<contact-details-item
|
||||
compact
|
||||
:title="$t('CONVERSATION_SIDEBAR.TEAM_LABEL')"
|
||||
/>
|
||||
<multiselect-dropdown
|
||||
@@ -64,68 +66,26 @@
|
||||
/>
|
||||
</div>
|
||||
<contact-details-item
|
||||
compact
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_LABELS')"
|
||||
/>
|
||||
<conversation-labels :conversation-id="conversationId" />
|
||||
</div>
|
||||
</accordion-item>
|
||||
</div>
|
||||
|
||||
<accordion-item
|
||||
v-if="browser.browser_name"
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_INFO')"
|
||||
:is-open="isContactSidebarItemOpen('is_conv_details_open')"
|
||||
compact
|
||||
@click="value => toggleSidebarUIState('is_conv_details_open', value)"
|
||||
>
|
||||
<div class="conversation--details">
|
||||
<contact-details-item
|
||||
v-if="location"
|
||||
:title="$t('CONTACT_FORM.FORM.LOCATION.LABEL')"
|
||||
:value="location"
|
||||
icon="ion-map"
|
||||
emoji="📍"
|
||||
/>
|
||||
<contact-details-item
|
||||
v-if="ipAddress"
|
||||
:title="$t('CONTACT_PANEL.IP_ADDRESS')"
|
||||
:value="ipAddress"
|
||||
icon="ion-android-locate"
|
||||
emoji="🧭"
|
||||
/>
|
||||
<contact-details-item
|
||||
v-if="browser.browser_name"
|
||||
:title="$t('CONTACT_PANEL.BROWSER')"
|
||||
:value="browserName"
|
||||
icon="ion-ios-world-outline"
|
||||
emoji="🌐"
|
||||
/>
|
||||
<contact-details-item
|
||||
v-if="browser.platform_name"
|
||||
:title="$t('CONTACT_PANEL.OS')"
|
||||
:value="platformName"
|
||||
icon="ion-laptop"
|
||||
emoji="💻"
|
||||
/>
|
||||
<contact-details-item
|
||||
v-if="referer"
|
||||
:title="$t('CONTACT_PANEL.INITIATED_FROM')"
|
||||
:value="referer"
|
||||
icon="ion-link"
|
||||
emoji="🔗"
|
||||
>
|
||||
<a :href="referer" rel="noopener noreferrer nofollow" target="_blank">
|
||||
{{ referer }}
|
||||
</a>
|
||||
</contact-details-item>
|
||||
<contact-details-item
|
||||
v-if="initiatedAt"
|
||||
:title="$t('CONTACT_PANEL.INITIATED_AT')"
|
||||
:value="initiatedAt.timestamp"
|
||||
icon="ion-clock"
|
||||
emoji="🕰"
|
||||
/>
|
||||
</div>
|
||||
<conversation-info
|
||||
:conversation-attributes="conversationAdditionalAttributes"
|
||||
:contact-attributes="contactAdditionalAttributes"
|
||||
>
|
||||
</conversation-info>
|
||||
</accordion-item>
|
||||
|
||||
<accordion-item
|
||||
v-if="hasContactAttributes"
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_ATTRIBUTES')"
|
||||
@@ -162,21 +122,21 @@ import ContactConversations from './ContactConversations.vue';
|
||||
import ContactCustomAttributes from './ContactCustomAttributes';
|
||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||
import ContactInfo from './contact/ContactInfo';
|
||||
import ConversationInfo from './ConversationInfo';
|
||||
import ConversationLabels from './labels/LabelBox.vue';
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
|
||||
import flag from 'country-code-emoji';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactCustomAttributes,
|
||||
AccordionItem,
|
||||
ContactConversations,
|
||||
ContactCustomAttributes,
|
||||
ContactDetailsItem,
|
||||
ContactInfo,
|
||||
ConversationInfo,
|
||||
ConversationLabels,
|
||||
MultiselectDropdown,
|
||||
AccordionItem,
|
||||
},
|
||||
mixins: [alertMixin, agentMixin, uiSettingsMixin],
|
||||
props: {
|
||||
@@ -200,68 +160,30 @@ export default {
|
||||
currentUser: 'getCurrentUser',
|
||||
uiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||
}),
|
||||
conversationAdditionalAttributes() {
|
||||
return this.currentConversationMetaData.additional_attributes || {};
|
||||
},
|
||||
channelType() {
|
||||
return this.currentChat.meta?.channel;
|
||||
},
|
||||
contact() {
|
||||
return this.$store.getters['contacts/getContact'](this.contactId);
|
||||
},
|
||||
contactAdditionalAttributes() {
|
||||
return this.contact.additional_attributes || {};
|
||||
},
|
||||
contactId() {
|
||||
return this.currentChat.meta?.sender?.id;
|
||||
},
|
||||
currentConversationMetaData() {
|
||||
return this.$store.getters[
|
||||
'conversationMetadata/getConversationMetadata'
|
||||
](this.conversationId);
|
||||
},
|
||||
additionalAttributes() {
|
||||
return this.currentConversationMetaData.additional_attributes || {};
|
||||
},
|
||||
hasContactAttributes() {
|
||||
const { custom_attributes: customAttributes } = this.contact;
|
||||
return customAttributes && Object.keys(customAttributes).length;
|
||||
},
|
||||
browser() {
|
||||
return this.additionalAttributes.browser || {};
|
||||
},
|
||||
referer() {
|
||||
return this.additionalAttributes.referer;
|
||||
},
|
||||
initiatedAt() {
|
||||
return this.additionalAttributes.initiated_at;
|
||||
},
|
||||
browserName() {
|
||||
return `${this.browser.browser_name || ''} ${this.browser
|
||||
.browser_version || ''}`;
|
||||
},
|
||||
contactAdditionalAttributes() {
|
||||
return this.contact.additional_attributes || {};
|
||||
},
|
||||
ipAddress() {
|
||||
const { created_at_ip: createdAtIp } = this.contactAdditionalAttributes;
|
||||
return createdAtIp;
|
||||
},
|
||||
location() {
|
||||
const {
|
||||
country = '',
|
||||
city = '',
|
||||
country_code: countryCode,
|
||||
} = this.contactAdditionalAttributes;
|
||||
const cityAndCountry = [city, country].filter(item => !!item).join(', ');
|
||||
|
||||
if (!cityAndCountry) {
|
||||
return '';
|
||||
}
|
||||
const countryFlag = countryCode ? flag(countryCode) : '🌎';
|
||||
return `${cityAndCountry} ${countryFlag}`;
|
||||
},
|
||||
platformName() {
|
||||
const {
|
||||
platform_name: platformName,
|
||||
platform_version: platformVersion,
|
||||
} = this.browser;
|
||||
return `${platformName || ''} ${platformVersion || ''}`;
|
||||
},
|
||||
channelType() {
|
||||
return this.currentChat.meta?.channel;
|
||||
},
|
||||
contactId() {
|
||||
return this.currentChat.meta?.sender?.id;
|
||||
},
|
||||
contact() {
|
||||
return this.$store.getters['contacts/getContact'](this.contactId);
|
||||
},
|
||||
teamsList() {
|
||||
if (this.assignedTeam) {
|
||||
return [
|
||||
@@ -330,6 +252,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.getContactDetails();
|
||||
this.$store.dispatch('attributes/get', 0);
|
||||
},
|
||||
methods: {
|
||||
onPanelToggle() {
|
||||
@@ -340,6 +263,11 @@ export default {
|
||||
this.$store.dispatch('contacts/show', { id: this.contactId });
|
||||
}
|
||||
},
|
||||
getAttributesByModel() {
|
||||
if (this.contactId) {
|
||||
this.$store.dispatch('contacts/show', { id: this.contactId });
|
||||
}
|
||||
},
|
||||
openTranscriptModal() {
|
||||
this.showTranscriptModal = true;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="conversation--details">
|
||||
<contact-details-item
|
||||
v-if="initiatedAt"
|
||||
:title="$t('CONTACT_PANEL.INITIATED_AT')"
|
||||
:value="initiatedAt.timestamp"
|
||||
class="conversation--attribute"
|
||||
/>
|
||||
<contact-details-item
|
||||
v-if="referer"
|
||||
:title="$t('CONTACT_PANEL.INITIATED_FROM')"
|
||||
:value="referer"
|
||||
class="conversation--attribute"
|
||||
>
|
||||
<a :href="referer" rel="noopener noreferrer nofollow" target="_blank">
|
||||
{{ referer }}
|
||||
</a>
|
||||
</contact-details-item>
|
||||
<contact-details-item
|
||||
v-if="browserName"
|
||||
:title="$t('CONTACT_PANEL.BROWSER')"
|
||||
:value="browserName"
|
||||
class="conversation--attribute"
|
||||
/>
|
||||
<contact-details-item
|
||||
v-if="platformName"
|
||||
:title="$t('CONTACT_PANEL.OS')"
|
||||
:value="platformName"
|
||||
class="conversation--attribute"
|
||||
/>
|
||||
<contact-details-item
|
||||
v-if="ipAddress"
|
||||
:title="$t('CONTACT_PANEL.IP_ADDRESS')"
|
||||
:value="ipAddress"
|
||||
class="conversation--attribute"
|
||||
/>
|
||||
<custom-attributes
|
||||
attribute-type="conversation_attribute"
|
||||
attribute-class="conversation--attribute"
|
||||
:class="customAttributeRowClass"
|
||||
/>
|
||||
<custom-attribute-selector attribute-type="conversation_attribute" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||
import CustomAttributes from './customAttributes/CustomAttributes.vue';
|
||||
import CustomAttributeSelector from './customAttributes/CustomAttributeSelector.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactDetailsItem,
|
||||
CustomAttributes,
|
||||
CustomAttributeSelector,
|
||||
},
|
||||
props: {
|
||||
conversationAttributes: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
contactAttributes: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
STATIC_ATTRIBUTES: [
|
||||
{
|
||||
name: 'initiated_at',
|
||||
label: 'CONTACT_PANEL.INITIATED_AT',
|
||||
},
|
||||
{
|
||||
name: 'referer',
|
||||
label: 'CONTACT_PANEL.BROWSER',
|
||||
},
|
||||
{
|
||||
name: 'browserName',
|
||||
label: 'CONTACT_PANEL.BROWSER',
|
||||
},
|
||||
{
|
||||
name: 'platformName',
|
||||
label: 'CONTACT_PANEL.OS',
|
||||
},
|
||||
{
|
||||
name: 'ipAddress',
|
||||
label: 'CONTACT_PANEL.IP_ADDRESS',
|
||||
},
|
||||
],
|
||||
computed: {
|
||||
referer() {
|
||||
return this.conversationAttributes.referer;
|
||||
},
|
||||
initiatedAt() {
|
||||
return this.conversationAttributes.initiated_at;
|
||||
},
|
||||
browserName() {
|
||||
if (!this.conversationAttributes.browser) {
|
||||
return '';
|
||||
}
|
||||
const {
|
||||
browser_name: browserName = '',
|
||||
browser_version: browserVersion = '',
|
||||
} = this.conversationAttributes.browser;
|
||||
return `${browserName} ${browserVersion}`;
|
||||
},
|
||||
platformName() {
|
||||
if (!this.conversationAttributes.browser) {
|
||||
return '';
|
||||
}
|
||||
const {
|
||||
platform_name: platformName,
|
||||
platform_version: platformVersion,
|
||||
} = this.conversationAttributes.browser;
|
||||
return `${platformName || ''} ${platformVersion || ''}`;
|
||||
},
|
||||
ipAddress() {
|
||||
const { created_at_ip: createdAtIp } = this.contactAttributes;
|
||||
return createdAtIp;
|
||||
},
|
||||
customAttributeRowClass() {
|
||||
const attributes = [
|
||||
'initiatedAt',
|
||||
'referer',
|
||||
'browserName',
|
||||
'platformName',
|
||||
'ipAddress',
|
||||
];
|
||||
const availableAttributes = attributes.filter(
|
||||
attribute => !!this[attribute]
|
||||
);
|
||||
return availableAttributes.length % 2 === 0 ? 'even' : 'odd';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.conversation--attribute {
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
|
||||
&:nth-child(2n) {
|
||||
background: var(--b-50);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -41,19 +41,19 @@
|
||||
emoji="📞"
|
||||
:title="$t('CONTACT_PANEL.PHONE_NUMBER')"
|
||||
/>
|
||||
<contact-info-row
|
||||
v-if="additionalAttributes.location"
|
||||
:value="additionalAttributes.location"
|
||||
icon="ion-map"
|
||||
emoji="🌍"
|
||||
:title="$t('CONTACT_PANEL.LOCATION')"
|
||||
/>
|
||||
<contact-info-row
|
||||
:value="additionalAttributes.company_name"
|
||||
icon="ion-briefcase"
|
||||
emoji="🏢"
|
||||
:title="$t('CONTACT_PANEL.COMPANY')"
|
||||
/>
|
||||
<contact-info-row
|
||||
v-if="location || additionalAttributes.location"
|
||||
:value="location || additionalAttributes.location"
|
||||
icon="ion-map"
|
||||
emoji="🌍"
|
||||
:title="$t('CONTACT_PANEL.LOCATION')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-actions">
|
||||
@@ -145,6 +145,7 @@ import ContactMergeModal from 'dashboard/modules/contact/ContactMergeModal';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import adminMixin from '../../../../mixins/isAdmin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import flag from 'country-code-emoji';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -190,6 +191,20 @@ export default {
|
||||
additionalAttributes() {
|
||||
return this.contact.additional_attributes || {};
|
||||
},
|
||||
location() {
|
||||
const {
|
||||
country = '',
|
||||
city = '',
|
||||
country_code: countryCode,
|
||||
} = this.additionalAttributes;
|
||||
const cityAndCountry = [city, country].filter(item => !!item).join(', ');
|
||||
|
||||
if (!cityAndCountry) {
|
||||
return '';
|
||||
}
|
||||
const countryFlag = countryCode ? flag(countryCode) : '🌎';
|
||||
return `${cityAndCountry} ${countryFlag}`;
|
||||
},
|
||||
socialProfiles() {
|
||||
const {
|
||||
social_profiles: socialProfiles,
|
||||
@@ -294,7 +309,7 @@ export default {
|
||||
}
|
||||
|
||||
.contact--metadata {
|
||||
margin-bottom: var(--space-small);
|
||||
margin-bottom: var(--space-slab);
|
||||
}
|
||||
|
||||
.contact-actions {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="dropdown-search-wrap">
|
||||
<h4 class="text-block-title">
|
||||
{{ $t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_SELECT.TITLE') }}
|
||||
</h4>
|
||||
<div class="search-wrap">
|
||||
<input
|
||||
ref="searchbar"
|
||||
v-model="search"
|
||||
type="text"
|
||||
class="search-input"
|
||||
autofocus="true"
|
||||
:placeholder="$t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_SELECT.PLACEHOLDER')"
|
||||
/>
|
||||
</div>
|
||||
<div class="list-wrap">
|
||||
<div class="list">
|
||||
<woot-dropdown-menu>
|
||||
<custom-attribute-drop-down-item
|
||||
v-for="attribute in filteredAttributes"
|
||||
:key="attribute.attribute_display_name"
|
||||
:title="attribute.attribute_display_name"
|
||||
@click="onAddAttribute(attribute)"
|
||||
/>
|
||||
</woot-dropdown-menu>
|
||||
<div v-if="noResult" class="no-result">
|
||||
{{ $t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_SELECT.NO_RESULT') }}
|
||||
</div>
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
class="add"
|
||||
icon="ion-plus-round"
|
||||
size="tiny"
|
||||
@click="addNewAttribute"
|
||||
>
|
||||
{{ $t('CUSTOM_ATTRIBUTES.FORM.ADD.TITLE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomAttributeDropDownItem from './CustomAttributeDropDownItem.vue';
|
||||
import attributeMixin from 'dashboard/mixins/attributeMixin';
|
||||
export default {
|
||||
components: {
|
||||
CustomAttributeDropDownItem,
|
||||
},
|
||||
mixins: [attributeMixin],
|
||||
props: {
|
||||
attributeType: {
|
||||
type: String,
|
||||
default: 'conversation_attribute',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredAttributes() {
|
||||
return this.attributes
|
||||
.filter(
|
||||
item =>
|
||||
!Object.keys(this.customAttributes).includes(item.attribute_key)
|
||||
)
|
||||
.filter(attribute => {
|
||||
return attribute.attribute_display_name
|
||||
.toLowerCase()
|
||||
.includes(this.search.toLowerCase());
|
||||
});
|
||||
},
|
||||
|
||||
noResult() {
|
||||
return this.filteredAttributes.length === 0 && this.search !== '';
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.focusInput();
|
||||
},
|
||||
|
||||
methods: {
|
||||
focusInput() {
|
||||
this.$refs.searchbar.focus();
|
||||
},
|
||||
addNewAttribute() {
|
||||
this.$router.push(
|
||||
`/app/accounts/${this.accountId}/settings/attributes/list`
|
||||
);
|
||||
},
|
||||
async onAddAttribute(attribute) {
|
||||
this.$emit('add-attribute', attribute);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-search-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-height: 20rem;
|
||||
|
||||
.search-wrap {
|
||||
margin-bottom: var(--space-small);
|
||||
flex: 0 0 auto;
|
||||
max-height: var(--space-large);
|
||||
|
||||
.search-input {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
height: var(--space-large);
|
||||
font-size: var(--font-size-small);
|
||||
padding: var(--space-small);
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border: 1px solid var(--w-500);
|
||||
}
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
|
||||
.list {
|
||||
width: 100%;
|
||||
.add {
|
||||
float: right;
|
||||
margin-top: var(--space-one);
|
||||
}
|
||||
}
|
||||
|
||||
.no-result {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: var(--s-700);
|
||||
padding: var(--space-smaller) var(--space-one);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<woot-dropdown-item>
|
||||
<woot-button variant="clear" @click="onClick">
|
||||
<span class="label-text" :title="title">{{ title }}</span>
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AttributeDropDownItem',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('click', this.title);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.item-wrap {
|
||||
display: flex;
|
||||
|
||||
::v-deep .button__content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
&.active {
|
||||
display: flex;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--w-700);
|
||||
}
|
||||
|
||||
.name-label-wrap {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
|
||||
.label-color--display {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.label-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.1;
|
||||
padding-right: var(--space-small);
|
||||
padding-left: var(--space-small);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label-color--display {
|
||||
border-radius: var(--border-radius-normal);
|
||||
height: var(--space-slab);
|
||||
margin-right: var(--space-smaller);
|
||||
margin-top: var(--space-micro);
|
||||
min-width: var(--space-slab);
|
||||
width: var(--space-slab);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div class="custom-attribute--selector">
|
||||
<div
|
||||
v-on-clickaway="closeDropdown"
|
||||
class="label-wrap"
|
||||
@keyup.esc="closeDropdown"
|
||||
>
|
||||
<woot-button
|
||||
size="small"
|
||||
variant="link"
|
||||
icon="ion-plus"
|
||||
@click="toggleAttributeDropDown"
|
||||
>
|
||||
{{ $t('CUSTOM_ATTRIBUTES.ADD_BUTTON_TEXT') }}
|
||||
</woot-button>
|
||||
|
||||
<div class="dropdown-wrap">
|
||||
<div
|
||||
:class="{ 'dropdown-pane--open': showAttributeDropDown }"
|
||||
class="dropdown-pane"
|
||||
>
|
||||
<custom-attribute-drop-down
|
||||
v-if="showAttributeDropDown"
|
||||
:attribute-type="attributeType"
|
||||
@add-attribute="addAttribute"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomAttributeDropDown from './CustomAttributeDropDown.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import attributeMixin from 'dashboard/mixins/attributeMixin';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CustomAttributeDropDown,
|
||||
},
|
||||
mixins: [clickaway, alertMixin, attributeMixin],
|
||||
props: {
|
||||
attributeType: {
|
||||
type: String,
|
||||
default: 'conversation_attribute',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAttributeDropDown: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async addAttribute(attribute) {
|
||||
const { attribute_key } = attribute;
|
||||
try {
|
||||
await this.$store.dispatch('updateCustomAttributes', {
|
||||
conversationId: this.conversationId,
|
||||
customAttributes: {
|
||||
...this.customAttributes,
|
||||
[attribute_key]: null,
|
||||
},
|
||||
});
|
||||
bus.$emit(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, attribute_key);
|
||||
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.ADD.SUCCESS'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message ||
|
||||
this.$t('CUSTOM_ATTRIBUTES.FORM.ADD.ERROR');
|
||||
this.showAlert(errorMessage);
|
||||
} finally {
|
||||
this.closeDropdown();
|
||||
}
|
||||
},
|
||||
|
||||
toggleAttributeDropDown() {
|
||||
this.showAttributeDropDown = !this.showAttributeDropDown;
|
||||
},
|
||||
|
||||
closeDropdown() {
|
||||
this.showAttributeDropDown = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-attribute--selector {
|
||||
width: 100%;
|
||||
padding: var(--space-slab) var(--space-normal);
|
||||
|
||||
.label-wrap {
|
||||
line-height: var(--space-medium);
|
||||
position: relative;
|
||||
|
||||
.dropdown-wrap {
|
||||
display: flex;
|
||||
left: -1px;
|
||||
margin-right: var(--space-medium);
|
||||
position: absolute;
|
||||
top: var(--space-medium);
|
||||
width: 100%;
|
||||
|
||||
.dropdown-pane {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--r-500);
|
||||
font-size: var(--font-size-mini);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="custom-attributes--panel">
|
||||
<custom-attribute
|
||||
v-for="attribute in filteredAttributes"
|
||||
:key="attribute.id"
|
||||
:attribute-key="attribute.attribute_key"
|
||||
:attribute-type="attribute.attribute_display_type"
|
||||
:label="attribute.attribute_display_name"
|
||||
:icon="attribute.icon"
|
||||
emoji=""
|
||||
:value="attribute.value"
|
||||
:show-actions="true"
|
||||
:class="attributeClass"
|
||||
@update="onUpdate"
|
||||
@delete="onDelete"
|
||||
@copy="onCopy"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import copy from 'copy-text-to-clipboard';
|
||||
import CustomAttribute from 'dashboard/components/CustomAttribute.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import attributeMixin from 'dashboard/mixins/attributeMixin';
|
||||
export default {
|
||||
components: {
|
||||
CustomAttribute,
|
||||
},
|
||||
mixins: [alertMixin, attributeMixin],
|
||||
props: {
|
||||
attributeType: {
|
||||
type: String,
|
||||
default: 'conversation_attribute',
|
||||
},
|
||||
attributeClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onUpdate(key, value) {
|
||||
try {
|
||||
await this.$store.dispatch('updateCustomAttributes', {
|
||||
conversationId: this.conversationId,
|
||||
customAttributes: { ...this.customAttributes, [key]: value },
|
||||
});
|
||||
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.SUCCESS'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message ||
|
||||
this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.ERROR');
|
||||
this.showAlert(errorMessage);
|
||||
}
|
||||
},
|
||||
async onDelete(key) {
|
||||
const { [key]: remove, ...updatedAttributes } = this.customAttributes;
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('updateCustomAttributes', {
|
||||
conversationId: this.conversationId,
|
||||
customAttributes: updatedAttributes,
|
||||
});
|
||||
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.SUCCESS'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message ||
|
||||
this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.ERROR');
|
||||
this.showAlert(errorMessage);
|
||||
}
|
||||
},
|
||||
onCopy(attributeValue) {
|
||||
copy(attributeValue);
|
||||
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.COPY_SUCCESSFUL'));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.custom-attributes--panel {
|
||||
.conversation--attribute {
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
&.odd {
|
||||
.conversation--attribute {
|
||||
&:nth-child(2n + 1) {
|
||||
background: var(--b-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.even {
|
||||
.conversation--attribute {
|
||||
&:nth-child(2n) {
|
||||
background: var(--b-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,8 +3,19 @@
|
||||
<div class="column content-box">
|
||||
<woot-modal-header :header-title="$t('ATTRIBUTES_MGMT.ADD.TITLE')" />
|
||||
|
||||
<form class="row" @submit.prevent="addAttributes()">
|
||||
<form class="row" @submit.prevent="addAttributes">
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.attributeModel.$error }">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.MODEL.LABEL') }}
|
||||
<select v-model="attributeModel">
|
||||
<option v-for="model in models" :key="model.id" :value="model.id">
|
||||
{{ model.option }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="$v.attributeModel.$error" class="message">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.MODEL.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<woot-input
|
||||
v-model="displayName"
|
||||
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.NAME.LABEL')"
|
||||
@@ -22,7 +33,7 @@
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.LABEL') }}
|
||||
<textarea
|
||||
v-model="description"
|
||||
rows="5"
|
||||
rows="3"
|
||||
type="text"
|
||||
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.DESC.PLACEHOLDER')"
|
||||
@blur="$v.description.$touch"
|
||||
@@ -31,18 +42,6 @@
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ error: $v.attributeModel.$error }">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.MODEL.LABEL') }}
|
||||
<select v-model="attributeModel">
|
||||
<option v-for="model in models" :key="model.id" :value="model.id">
|
||||
{{ model.option }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="$v.attributeModel.$error" class="message">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.MODEL.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label :class="{ error: $v.attributeType.$error }">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LABEL') }}
|
||||
<select v-model="attributeType">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/>
|
||||
</woot-tabs>
|
||||
|
||||
<div class="columns with-right-space ">
|
||||
<div class="columns with-right-space">
|
||||
<p
|
||||
v-if="!uiFlags.isFetching && !attributes.length"
|
||||
class="no-items-error-message"
|
||||
@@ -135,10 +135,6 @@ export default {
|
||||
key: 0,
|
||||
name: this.$t('ATTRIBUTES_MGMT.TABS.CONVERSATION'),
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
name: this.$t('ATTRIBUTES_MGMT.TABS.CONTACT'),
|
||||
},
|
||||
];
|
||||
},
|
||||
deleteConfirmText() {
|
||||
|
||||
@@ -3,10 +3,6 @@ export const ATTRIBUTE_MODELS = [
|
||||
id: 0,
|
||||
option: 'Conversation',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
option: 'Contact',
|
||||
},
|
||||
];
|
||||
|
||||
export const ATTRIBUTE_TYPES = [
|
||||
@@ -18,14 +14,6 @@ export const ATTRIBUTE_TYPES = [
|
||||
id: 1,
|
||||
option: 'Number',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
option: 'Currency',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
option: 'Percent',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
option: 'Link',
|
||||
|
||||
@@ -10,6 +10,7 @@ import profile from './profile/profile.routes';
|
||||
import reports from './reports/reports.routes';
|
||||
import campaigns from './campaigns/campaigns.routes';
|
||||
import teams from './teams/teams.routes';
|
||||
import attributes from './attributes/attributes.routes';
|
||||
import store from '../../../store';
|
||||
|
||||
export default {
|
||||
@@ -36,5 +37,6 @@ export default {
|
||||
...teams.routes,
|
||||
...campaigns.routes,
|
||||
...integrationapps.routes,
|
||||
...attributes.routes,
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user