mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +00:00 
			
		
		
		
	feat: Custom attribute sidebar list UX improvements (#9070)
--------- Co-authored-by: Pranav <pranav@chatwoot.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
		| @@ -11,6 +11,7 @@ body { | |||||||
|     'Segoe UI', |     'Segoe UI', | ||||||
|     Roboto, |     Roboto, | ||||||
|     'Helvetica Neue', |     'Helvetica Neue', | ||||||
|  |     Tahoma, | ||||||
|     Arial, |     Arial, | ||||||
|     sans-serif !important; |     sans-serif !important; | ||||||
|   -moz-osx-font-smoothing: grayscale; |   -moz-osx-font-smoothing: grayscale; | ||||||
|   | |||||||
| @@ -118,7 +118,7 @@ button { | |||||||
|     @apply border border-woot-500 bg-transparent dark:bg-transparent dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900; |     @apply border border-woot-500 bg-transparent dark:bg-transparent dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900; | ||||||
|  |  | ||||||
|     &.secondary { |     &.secondary { | ||||||
|       @apply text-slate-700 border-slate-200 dark:border-slate-600 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700; |       @apply text-slate-700 border-slate-100 dark:border-slate-800 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     &.success { |     &.success { | ||||||
|   | |||||||
| @@ -2,23 +2,27 @@ | |||||||
|   <div class="py-3 px-4"> |   <div class="py-3 px-4"> | ||||||
|     <div class="flex items-center mb-1"> |     <div class="flex items-center mb-1"> | ||||||
|       <h4 class="text-sm flex items-center m-0 w-full error"> |       <h4 class="text-sm flex items-center m-0 w-full error"> | ||||||
|         <div v-if="isAttributeTypeCheckbox" class="checkbox-wrap"> |         <div v-if="isAttributeTypeCheckbox" class="flex items-center"> | ||||||
|           <input |           <input | ||||||
|             v-model="editedValue" |             v-model="editedValue" | ||||||
|             class="checkbox" |             class="!my-0 mr-2 ml-0" | ||||||
|             type="checkbox" |             type="checkbox" | ||||||
|             @change="onUpdate" |             @change="onUpdate" | ||||||
|           /> |           /> | ||||||
|         </div> |         </div> | ||||||
|         <div class="flex items-center justify-between w-full"> |         <div class="flex items-center justify-between w-full"> | ||||||
|           <span |           <span | ||||||
|             class="attribute-name w-full text-slate-800 dark:text-slate-100 font-medium text-sm mb-0" |             class="w-full font-medium text-sm mb-0" | ||||||
|             :class="{ error: $v.editedValue.$error }" |             :class=" | ||||||
|  |               $v.editedValue.$error | ||||||
|  |                 ? 'text-red-400 dark:text-red-500' | ||||||
|  |                 : 'text-slate-800 dark:text-slate-100' | ||||||
|  |             " | ||||||
|           > |           > | ||||||
|             {{ label }} |             {{ label }} | ||||||
|           </span> |           </span> | ||||||
|           <woot-button |           <woot-button | ||||||
|             v-if="showActions" |             v-if="showCopyAndDeleteButton" | ||||||
|             v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')" |             v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')" | ||||||
|             variant="link" |             variant="link" | ||||||
|             size="medium" |             size="medium" | ||||||
| @@ -31,7 +35,7 @@ | |||||||
|       </h4> |       </h4> | ||||||
|     </div> |     </div> | ||||||
|     <div v-if="notAttributeTypeCheckboxAndList"> |     <div v-if="notAttributeTypeCheckboxAndList"> | ||||||
|       <div v-show="isEditing"> |       <div v-if="isEditing" v-on-clickaway="onClickAway"> | ||||||
|         <div class="mb-2 w-full flex items-center"> |         <div class="mb-2 w-full flex items-center"> | ||||||
|           <input |           <input | ||||||
|             ref="inputfield" |             ref="inputfield" | ||||||
| @@ -61,7 +65,7 @@ | |||||||
|       </div> |       </div> | ||||||
|       <div |       <div | ||||||
|         v-show="!isEditing" |         v-show="!isEditing" | ||||||
|         class="value--view" |         class="flex group" | ||||||
|         :class="{ 'is-editable': showActions }" |         :class="{ 'is-editable': showActions }" | ||||||
|       > |       > | ||||||
|         <a |         <a | ||||||
| @@ -69,35 +73,35 @@ | |||||||
|           :href="hrefURL" |           :href="hrefURL" | ||||||
|           target="_blank" |           target="_blank" | ||||||
|           rel="noopener noreferrer" |           rel="noopener noreferrer" | ||||||
|           class="value inline-block rounded-sm mb-0 break-all py-0.5 px-1" |           class="group-hover:bg-slate-50 group-hover:dark:bg-slate-700 inline-block rounded-sm mb-0 break-all py-0.5 px-1" | ||||||
|         > |         > | ||||||
|           {{ urlValue }} |           {{ urlValue }} | ||||||
|         </a> |         </a> | ||||||
|         <p |         <p | ||||||
|           v-else |           v-else | ||||||
|           class="value inline-block rounded-sm mb-0 break-all py-0.5 px-1" |           class="group-hover:bg-slate-50 group-hover:dark:bg-slate-700 inline-block rounded-sm mb-0 break-all py-0.5 px-1" | ||||||
|         > |         > | ||||||
|           {{ displayValue || '---' }} |           {{ displayValue || '---' }} | ||||||
|         </p> |         </p> | ||||||
|         <div class="flex max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0"> |         <div class="flex max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0"> | ||||||
|           <woot-button |           <woot-button | ||||||
|             v-if="showActions" |             v-if="showCopyAndDeleteButton" | ||||||
|             v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')" |             v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')" | ||||||
|             variant="link" |             variant="link" | ||||||
|             size="small" |             size="small" | ||||||
|             color-scheme="secondary" |             color-scheme="secondary" | ||||||
|             icon="clipboard" |             icon="clipboard" | ||||||
|             class-names="edit-button" |             class-names="hidden group-hover:flex !w-6 flex-shrink-0" | ||||||
|             @click="onCopy" |             @click="onCopy" | ||||||
|           /> |           /> | ||||||
|           <woot-button |           <woot-button | ||||||
|             v-if="showActions" |             v-if="showEditButton" | ||||||
|             v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')" |             v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')" | ||||||
|             variant="link" |             variant="link" | ||||||
|             size="small" |             size="small" | ||||||
|             color-scheme="secondary" |             color-scheme="secondary" | ||||||
|             icon="edit" |             icon="edit" | ||||||
|             class-names="edit-button" |             class-names="hidden group-hover:flex !w-6 flex-shrink-0" | ||||||
|             @click="onEdit" |             @click="onEdit" | ||||||
|           /> |           /> | ||||||
|         </div> |         </div> | ||||||
| @@ -126,6 +130,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|  | import { mixin as clickaway } from 'vue-clickaway'; | ||||||
| import { format, parseISO } from 'date-fns'; | import { format, parseISO } from 'date-fns'; | ||||||
| import { required, url } from 'vuelidate/lib/validators'; | import { required, url } from 'vuelidate/lib/validators'; | ||||||
| import { BUS_EVENTS } from 'shared/constants/busEvents'; | import { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||||
| @@ -138,7 +143,7 @@ export default { | |||||||
|   components: { |   components: { | ||||||
|     MultiselectDropdown, |     MultiselectDropdown, | ||||||
|   }, |   }, | ||||||
|   mixins: [customAttributeMixin], |   mixins: [customAttributeMixin, clickaway], | ||||||
|   props: { |   props: { | ||||||
|     label: { type: String, required: true }, |     label: { type: String, required: true }, | ||||||
|     values: { type: Array, default: () => [] }, |     values: { type: Array, default: () => [] }, | ||||||
| @@ -160,11 +165,18 @@ export default { | |||||||
|       editedValue: null, |       editedValue: null, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   computed: { |   computed: { | ||||||
|  |     showCopyAndDeleteButton() { | ||||||
|  |       return this.value && this.showActions; | ||||||
|  |     }, | ||||||
|  |     showEditButton() { | ||||||
|  |       return !this.value && this.showActions; | ||||||
|  |     }, | ||||||
|     displayValue() { |     displayValue() { | ||||||
|       if (this.isAttributeTypeDate) { |       if (this.isAttributeTypeDate) { | ||||||
|         return new Date(this.value || new Date()).toLocaleDateString(); |         return this.value | ||||||
|  |           ? new Date(this.value || new Date()).toLocaleDateString() | ||||||
|  |           : ''; | ||||||
|       } |       } | ||||||
|       if (this.isAttributeTypeCheckbox) { |       if (this.isAttributeTypeCheckbox) { | ||||||
|         return this.value === 'false' ? false : this.value; |         return this.value === 'false' ? false : this.value; | ||||||
| @@ -230,6 +242,10 @@ export default { | |||||||
|       this.isEditing = false; |       this.isEditing = false; | ||||||
|       this.editedValue = this.formattedValue; |       this.editedValue = this.formattedValue; | ||||||
|     }, |     }, | ||||||
|  |     contactId() { | ||||||
|  |       // Fix to solve validation not resetting when contactId changes in contact page | ||||||
|  |       this.$v.$reset(); | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   validations() { |   validations() { | ||||||
| @@ -268,6 +284,10 @@ export default { | |||||||
|         this.$refs.inputfield.focus(); |         this.$refs.inputfield.focus(); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     onClickAway() { | ||||||
|  |       this.$v.$reset(); | ||||||
|  |       this.isEditing = false; | ||||||
|  |     }, | ||||||
|     onEdit() { |     onEdit() { | ||||||
|       this.isEditing = true; |       this.isEditing = true; | ||||||
|       this.$nextTick(() => { |       this.$nextTick(() => { | ||||||
| @@ -294,6 +314,7 @@ export default { | |||||||
|     }, |     }, | ||||||
|     onDelete() { |     onDelete() { | ||||||
|       this.isEditing = false; |       this.isEditing = false; | ||||||
|  |       this.$v.$reset(); | ||||||
|       this.$emit('delete', this.attributeKey); |       this.$emit('delete', this.attributeKey); | ||||||
|     }, |     }, | ||||||
|     onCopy() { |     onCopy() { | ||||||
| @@ -304,35 +325,6 @@ export default { | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .checkbox-wrap { |  | ||||||
|   @apply flex items-center; |  | ||||||
| } |  | ||||||
| .checkbox { |  | ||||||
|   @apply my-0 mr-2 ml-0; |  | ||||||
| } |  | ||||||
| .attribute-name { |  | ||||||
|   &.error { |  | ||||||
|     @apply text-red-400 dark:text-red-500; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .edit-button { |  | ||||||
|   @apply hidden; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .value--view { |  | ||||||
|   @apply flex; |  | ||||||
|  |  | ||||||
|   &.is-editable:hover { |  | ||||||
|     .value { |  | ||||||
|       @apply bg-slate-50 dark:bg-slate-700 mb-0; |  | ||||||
|     } |  | ||||||
|     .edit-button { |  | ||||||
|       @apply block; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| ::v-deep { | ::v-deep { | ||||||
|   .selector-wrap { |   .selector-wrap { | ||||||
|     @apply m-0 top-1; |     @apply m-0 top-1; | ||||||
|   | |||||||
| @@ -296,6 +296,8 @@ | |||||||
|     "BUTTON": "Add custom attribute", |     "BUTTON": "Add custom attribute", | ||||||
|     "NOT_AVAILABLE": "There are no custom attributes available for this contact.", |     "NOT_AVAILABLE": "There are no custom attributes available for this contact.", | ||||||
|     "COPY_SUCCESSFUL": "Copied to clipboard successfully", |     "COPY_SUCCESSFUL": "Copied to clipboard successfully", | ||||||
|  |     "SHOW_MORE": "Show all attributes", | ||||||
|  |     "SHOW_LESS": "Show less attributes", | ||||||
|     "ACTIONS": { |     "ACTIONS": { | ||||||
|       "COPY": "Copy attribute", |       "COPY": "Copy attribute", | ||||||
|       "DELETE": "Delete attribute", |       "DELETE": "Delete attribute", | ||||||
|   | |||||||
| @@ -29,33 +29,6 @@ export default { | |||||||
|     conversationId() { |     conversationId() { | ||||||
|       return this.currentChat.id; |       return this.currentChat.id; | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     filteredAttributes() { |  | ||||||
|       return Object.keys(this.customAttributes).map(key => { |  | ||||||
|         const item = this.attributes.find( |  | ||||||
|           attribute => attribute.attribute_key === key |  | ||||||
|         ); |  | ||||||
|         if (item) { |  | ||||||
|           return { |  | ||||||
|             ...item, |  | ||||||
|             value: this.customAttributes[key], |  | ||||||
|           }; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return { |  | ||||||
|           ...item, |  | ||||||
|           value: this.customAttributes[key], |  | ||||||
|           attribute_description: key, |  | ||||||
|           attribute_display_name: key, |  | ||||||
|           attribute_display_type: this.attributeDisplayType( |  | ||||||
|             this.customAttributes[key] |  | ||||||
|           ), |  | ||||||
|           attribute_key: key, |  | ||||||
|           attribute_model: this.attributeType, |  | ||||||
|           id: Math.random(), |  | ||||||
|         }; |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     isAttributeNumber(attributeValue) { |     isAttributeNumber(attributeValue) { | ||||||
|   | |||||||
| @@ -1,26 +0,0 @@ | |||||||
| export default [ |  | ||||||
|   { |  | ||||||
|     attribute_description: 'Product name', |  | ||||||
|     attribute_display_name: 'Product name', |  | ||||||
|     attribute_display_type: 'text', |  | ||||||
|     attribute_key: 'product_name', |  | ||||||
|     attribute_model: 'conversation_attribute', |  | ||||||
|     created_at: '2021-09-03T10:45:09.587Z', |  | ||||||
|     default_value: null, |  | ||||||
|     id: 6, |  | ||||||
|     updated_at: '2021-09-22T10:40:42.511Z', |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     attribute_description: 'Product identifier', |  | ||||||
|     attribute_display_name: 'Product id', |  | ||||||
|     attribute_display_type: 'number', |  | ||||||
|     attribute_key: 'product_id', |  | ||||||
|     attribute_model: 'conversation_attribute', |  | ||||||
|     created_at: '2021-09-16T13:06:47.329Z', |  | ||||||
|     default_value: null, |  | ||||||
|     icon: 'fluent-calculator', |  | ||||||
|     id: 10, |  | ||||||
|     updated_at: '2021-09-22T10:42:25.873Z', |  | ||||||
|     value: 2021, |  | ||||||
|   }, |  | ||||||
| ]; |  | ||||||
| @@ -1,7 +1,6 @@ | |||||||
| import { shallowMount, createLocalVue } from '@vue/test-utils'; | import { shallowMount, createLocalVue } from '@vue/test-utils'; | ||||||
| import attributeMixin from '../attributeMixin'; | import attributeMixin from '../attributeMixin'; | ||||||
| import Vuex from 'vuex'; | import Vuex from 'vuex'; | ||||||
| import attributeFixtures from './attributeFixtures'; |  | ||||||
|  |  | ||||||
| const localVue = createLocalVue(); | const localVue = createLocalVue(); | ||||||
| localVue.use(Vuex); | localVue.use(Vuex); | ||||||
| @@ -41,43 +40,6 @@ describe('attributeMixin', () => { | |||||||
|     expect(wrapper.vm.conversationId).toEqual(7165); |     expect(wrapper.vm.conversationId).toEqual(7165); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   it('returns filtered attributes from conversation custom attributes', () => { |  | ||||||
|     const Component = { |  | ||||||
|       render() {}, |  | ||||||
|       title: 'TestComponent', |  | ||||||
|       mixins: [attributeMixin], |  | ||||||
|       computed: { |  | ||||||
|         attributes() { |  | ||||||
|           return attributeFixtures; |  | ||||||
|         }, |  | ||||||
|         contact() { |  | ||||||
|           return { |  | ||||||
|             id: 7165, |  | ||||||
|             custom_attributes: { |  | ||||||
|               product_id: 2021, |  | ||||||
|             }, |  | ||||||
|           }; |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
|     }; |  | ||||||
|     const wrapper = shallowMount(Component, { store, localVue }); |  | ||||||
|     expect(wrapper.vm.filteredAttributes).toEqual([ |  | ||||||
|       { |  | ||||||
|         attribute_description: 'Product identifier', |  | ||||||
|         attribute_display_name: 'Product id', |  | ||||||
|         attribute_display_type: 'number', |  | ||||||
|         attribute_key: 'product_id', |  | ||||||
|         attribute_model: 'conversation_attribute', |  | ||||||
|         created_at: '2021-09-16T13:06:47.329Z', |  | ||||||
|         default_value: null, |  | ||||||
|         icon: 'fluent-calculator', |  | ||||||
|         id: 10, |  | ||||||
|         updated_at: '2021-09-22T10:42:25.873Z', |  | ||||||
|         value: 2021, |  | ||||||
|       }, |  | ||||||
|     ]); |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   it('return display type if attribute passed', () => { |   it('return display type if attribute passed', () => { | ||||||
|     const Component = { |     const Component = { | ||||||
|       render() {}, |       render() {}, | ||||||
|   | |||||||
| @@ -38,13 +38,10 @@ | |||||||
|                 :contact-id="contact.id" |                 :contact-id="contact.id" | ||||||
|                 attribute-type="contact_attribute" |                 attribute-type="contact_attribute" | ||||||
|                 attribute-class="conversation--attribute" |                 attribute-class="conversation--attribute" | ||||||
|  |                 attribute-from="contact_panel" | ||||||
|                 :custom-attributes="contact.custom_attributes" |                 :custom-attributes="contact.custom_attributes" | ||||||
|                 class="even" |                 class="even" | ||||||
|               /> |               /> | ||||||
|               <custom-attribute-selector |  | ||||||
|                 attribute-type="contact_attribute" |  | ||||||
|                 :contact-id="contact.id" |  | ||||||
|               /> |  | ||||||
|             </accordion-item> |             </accordion-item> | ||||||
|           </div> |           </div> | ||||||
|           <div v-if="element.name === 'contact_labels'"> |           <div v-if="element.name === 'contact_labels'"> | ||||||
| @@ -85,7 +82,6 @@ import ContactConversations from 'dashboard/routes/dashboard/conversation/Contac | |||||||
| import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/ContactInfo.vue'; | import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/ContactInfo.vue'; | ||||||
| import ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.vue'; | import ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.vue'; | ||||||
| import CustomAttributes from 'dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue'; | import CustomAttributes from 'dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue'; | ||||||
| import CustomAttributeSelector from 'dashboard/routes/dashboard/conversation/customAttributes/CustomAttributeSelector.vue'; |  | ||||||
| import draggable from 'vuedraggable'; | import draggable from 'vuedraggable'; | ||||||
| import uiSettingsMixin from 'dashboard/mixins/uiSettings'; | import uiSettingsMixin from 'dashboard/mixins/uiSettings'; | ||||||
|  |  | ||||||
| @@ -96,7 +92,6 @@ export default { | |||||||
|     ContactInfo, |     ContactInfo, | ||||||
|     ContactLabel, |     ContactLabel, | ||||||
|     CustomAttributes, |     CustomAttributes, | ||||||
|     CustomAttributeSelector, |  | ||||||
|     draggable, |     draggable, | ||||||
|   }, |   }, | ||||||
|   mixins: [uiSettingsMixin], |   mixins: [uiSettingsMixin], | ||||||
|   | |||||||
| @@ -87,10 +87,7 @@ | |||||||
|                 attribute-type="contact_attribute" |                 attribute-type="contact_attribute" | ||||||
|                 attribute-class="conversation--attribute" |                 attribute-class="conversation--attribute" | ||||||
|                 class="even" |                 class="even" | ||||||
|                 :contact-id="contact.id" |                 attribute-from="conversation_contact_panel" | ||||||
|               /> |  | ||||||
|               <custom-attribute-selector |  | ||||||
|                 attribute-type="contact_attribute" |  | ||||||
|                 :contact-id="contact.id" |                 :contact-id="contact.id" | ||||||
|               /> |               /> | ||||||
|             </accordion-item> |             </accordion-item> | ||||||
| @@ -142,7 +139,6 @@ import ConversationParticipant from './ConversationParticipant.vue'; | |||||||
| import ContactInfo from './contact/ContactInfo.vue'; | import ContactInfo from './contact/ContactInfo.vue'; | ||||||
| import ConversationInfo from './ConversationInfo.vue'; | import ConversationInfo from './ConversationInfo.vue'; | ||||||
| import CustomAttributes from './customAttributes/CustomAttributes.vue'; | import CustomAttributes from './customAttributes/CustomAttributes.vue'; | ||||||
| import CustomAttributeSelector from './customAttributes/CustomAttributeSelector.vue'; |  | ||||||
| import draggable from 'vuedraggable'; | import draggable from 'vuedraggable'; | ||||||
| import uiSettingsMixin from 'dashboard/mixins/uiSettings'; | import uiSettingsMixin from 'dashboard/mixins/uiSettings'; | ||||||
| import MacrosList from './Macros/List.vue'; | import MacrosList from './Macros/List.vue'; | ||||||
| @@ -154,7 +150,6 @@ export default { | |||||||
|     ContactInfo, |     ContactInfo, | ||||||
|     ConversationInfo, |     ConversationInfo, | ||||||
|     CustomAttributes, |     CustomAttributes, | ||||||
|     CustomAttributeSelector, |  | ||||||
|     ConversationAction, |     ConversationAction, | ||||||
|     ConversationParticipant, |     ConversationParticipant, | ||||||
|     draggable, |     draggable, | ||||||
|   | |||||||
| @@ -1,152 +1,109 @@ | |||||||
| <template> | <script setup> | ||||||
|   <div class="conversation--details"> | import { computed } from 'vue'; | ||||||
|     <contact-details-item | import { getLanguageName } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages'; | ||||||
|       v-if="initiatedAt" |  | ||||||
|       :title="$t('CONTACT_PANEL.INITIATED_AT')" |  | ||||||
|       :value="initiatedAt.timestamp" |  | ||||||
|       class="conversation--attribute" |  | ||||||
|     /> |  | ||||||
|     <contact-details-item |  | ||||||
|       v-if="browserLanguage" |  | ||||||
|       :title="$t('CONTACT_PANEL.BROWSER_LANGUAGE')" |  | ||||||
|       :value="browserLanguage" |  | ||||||
|       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 { getLanguageName } from '../../../components/widgets/conversation/advancedFilterItems/languages'; |  | ||||||
| import ContactDetailsItem from './ContactDetailsItem.vue'; | import ContactDetailsItem from './ContactDetailsItem.vue'; | ||||||
| import CustomAttributes from './customAttributes/CustomAttributes.vue'; | import CustomAttributes from './customAttributes/CustomAttributes.vue'; | ||||||
| import CustomAttributeSelector from './customAttributes/CustomAttributeSelector.vue'; | const props = defineProps({ | ||||||
|  |   conversationAttributes: { | ||||||
|  |     type: Object, | ||||||
|  |     default: () => ({}), | ||||||
|  |   }, | ||||||
|  |   contactAttributes: { | ||||||
|  |     type: Object, | ||||||
|  |     default: () => ({}), | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  |  | ||||||
| export default { | const referer = computed(() => props.conversationAttributes.referer); | ||||||
|   components: { | const initiatedAt = computed( | ||||||
|     ContactDetailsItem, |   () => props.conversationAttributes.initiated_at?.timestamp | ||||||
|     CustomAttributes, | ); | ||||||
|     CustomAttributeSelector, |  | ||||||
|   }, | const browserInfo = props.conversationAttributes.browser; | ||||||
|   props: { |  | ||||||
|     conversationAttributes: { | const browserName = computed(() => { | ||||||
|       type: Object, |   if (!browserInfo) return ''; | ||||||
|       default: () => ({}), |   const { browser_name: name = '', browser_version: version = '' } = | ||||||
|     }, |     browserInfo; | ||||||
|     contactAttributes: { |   return `${name} ${version}`; | ||||||
|       type: Object, | }); | ||||||
|       default: () => ({}), |  | ||||||
|     }, | const browserLanguage = computed(() => | ||||||
|   }, |   getLanguageName(props.conversationAttributes.browser_language) | ||||||
|   STATIC_ATTRIBUTES: [ | ); | ||||||
|  |  | ||||||
|  | const platformName = computed(() => { | ||||||
|  |   if (!browserInfo) return ''; | ||||||
|  |   const { platform_name: name = '', platform_version: version = '' } = | ||||||
|  |     browserInfo; | ||||||
|  |   return `${name} ${version}`; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const createdAtIp = computed(() => props.contactAttributes.created_at_ip); | ||||||
|  |  | ||||||
|  | const staticElements = computed(() => | ||||||
|  |   [ | ||||||
|     { |     { | ||||||
|       name: 'initiated_at', |       content: initiatedAt, | ||||||
|       label: 'CONTACT_PANEL.INITIATED_AT', |       title: 'CONTACT_PANEL.INITIATED_AT', | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       name: 'referer', |       content: browserLanguage, | ||||||
|       label: 'CONTACT_PANEL.BROWSER', |       title: 'CONTACT_PANEL.BROWSER_LANGUAGE', | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       name: 'browserName', |       content: referer, | ||||||
|       label: 'CONTACT_PANEL.BROWSER', |       title: 'CONTACT_PANEL.INITIATED_FROM', | ||||||
|  |       type: 'link', | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       name: 'platformName', |       content: browserName, | ||||||
|       label: 'CONTACT_PANEL.OS', |       title: 'CONTACT_PANEL.BROWSER', | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       name: 'ipAddress', |       content: platformName, | ||||||
|       label: 'CONTACT_PANEL.IP_ADDRESS', |       title: 'CONTACT_PANEL.OS', | ||||||
|     }, |     }, | ||||||
|   ], |     { | ||||||
|   computed: { |       content: createdAtIp, | ||||||
|     referer() { |       title: 'CONTACT_PANEL.IP_ADDRESS', | ||||||
|       return this.conversationAttributes.referer; |  | ||||||
|     }, |     }, | ||||||
|     initiatedAt() { |   ].filter(attribute => !!attribute.content.value) | ||||||
|       return this.conversationAttributes.initiated_at; | ); | ||||||
|     }, |  | ||||||
|     browserName() { |  | ||||||
|       if (!this.conversationAttributes.browser) { |  | ||||||
|         return ''; |  | ||||||
|       } |  | ||||||
|       const { |  | ||||||
|         browser_name: browserName = '', |  | ||||||
|         browser_version: browserVersion = '', |  | ||||||
|       } = this.conversationAttributes.browser; |  | ||||||
|       return `${browserName} ${browserVersion}`; |  | ||||||
|     }, |  | ||||||
|     browserLanguage() { |  | ||||||
|       return getLanguageName(this.conversationAttributes.browser_language); |  | ||||||
|     }, |  | ||||||
|     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> | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <div class="conversation--details"> | ||||||
|  |     <ContactDetailsItem | ||||||
|  |       v-for="element in staticElements" | ||||||
|  |       :key="element.title" | ||||||
|  |       :title="$t(element.title)" | ||||||
|  |       :value="element.content.value" | ||||||
|  |       class="conversation--attribute" | ||||||
|  |     > | ||||||
|  |       <a | ||||||
|  |         v-if="element.type === 'link'" | ||||||
|  |         :href="referer" | ||||||
|  |         rel="noopener noreferrer nofollow" | ||||||
|  |         target="_blank" | ||||||
|  |         class="text-woot-400 dark:text-woot-600" | ||||||
|  |       > | ||||||
|  |         {{ referer }} | ||||||
|  |       </a> | ||||||
|  |     </ContactDetailsItem> | ||||||
|  |     <CustomAttributes | ||||||
|  |       :class="staticElements.length % 2 === 0 ? 'even' : 'odd'" | ||||||
|  |       attribute-class="conversation--attribute" | ||||||
|  |       attribute-from="conversation_panel" | ||||||
|  |       attribute-type="conversation_attribute" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
| <style scoped lang="scss"> | <style scoped lang="scss"> | ||||||
| .conversation--attribute { | .conversation--attribute { | ||||||
|   @apply border-slate-50 dark:border-slate-700 border-b border-solid; |   @apply border-slate-50 dark:border-slate-700/50 border-b border-solid; | ||||||
|  |  | ||||||
|   &:nth-child(2n) { |   &:nth-child(2n) { | ||||||
|     @apply bg-slate-25 dark:bg-slate-800; |     @apply bg-slate-25 dark:bg-slate-800/50; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -125,8 +125,9 @@ | |||||||
|       > |       > | ||||||
|         <span |         <span | ||||||
|           class="flex items-center h-10 px-2 text-sm border-solid bg-slate-50 border-y ltr:border-l rtl:border-r ltr:rounded-l-md rtl:rounded-r-md dark:bg-slate-700 text-slate-800 dark:text-slate-100 border-slate-200 dark:border-slate-600" |           class="flex items-center h-10 px-2 text-sm border-solid bg-slate-50 border-y ltr:border-l rtl:border-r ltr:rounded-l-md rtl:rounded-r-md dark:bg-slate-700 text-slate-800 dark:text-slate-100 border-slate-200 dark:border-slate-600" | ||||||
|           >{{ socialProfile.prefixURL }}</span |  | ||||||
|         > |         > | ||||||
|  |           {{ socialProfile.prefixURL }} | ||||||
|  |         </span> | ||||||
|         <input |         <input | ||||||
|           v-model="socialProfileUserNames[socialProfile.key]" |           v-model="socialProfileUserNames[socialProfile.key]" | ||||||
|           class="input-group-field ltr:rounded-l-none rtl:rounded-r-none !mb-0" |           class="input-group-field ltr:rounded-l-none rtl:rounded-r-none !mb-0" | ||||||
|   | |||||||
| @@ -1,114 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="flex flex-col w-full max-h-[12.5rem]"> |  | ||||||
|     <h4 |  | ||||||
|       class="text-sm text-slate-800 dark:text-slate-100 mb-1 overflow-hidden whitespace-nowrap text-ellipsis flex-grow" |  | ||||||
|     > |  | ||||||
|       {{ $t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_SELECT.TITLE') }} |  | ||||||
|     </h4> |  | ||||||
|     <div class="mb-2 flex-shrink-0 flex-grow-0 flex-auto max-h-8"> |  | ||||||
|       <input |  | ||||||
|         ref="searchbar" |  | ||||||
|         v-model="search" |  | ||||||
|         type="text" |  | ||||||
|         class="search-input" |  | ||||||
|         autofocus="true" |  | ||||||
|         :placeholder="$t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_SELECT.PLACEHOLDER')" |  | ||||||
|       /> |  | ||||||
|     </div> |  | ||||||
|     <div |  | ||||||
|       class="flex justify-start items-start flex-grow flex-shrink flex-auto overflow-auto h-32" |  | ||||||
|     > |  | ||||||
|       <div class="w-full h-full"> |  | ||||||
|         <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="w-full justify-center items-center flex mb-2 h-[70%] text-slate-500 dark:text-slate-300 py-2 px-2.5 overflow-hidden whitespace-nowrap text-ellipsis text-sm" |  | ||||||
|         > |  | ||||||
|           {{ $t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_SELECT.NO_RESULT') }} |  | ||||||
|         </div> |  | ||||||
|         <woot-button |  | ||||||
|           class="float-right" |  | ||||||
|           icon="add" |  | ||||||
|           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', |  | ||||||
|     }, |  | ||||||
|     contactId: { type: Number, default: null }, |  | ||||||
|   }, |  | ||||||
|  |  | ||||||
|   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; |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|  |  | ||||||
|   mounted() { |  | ||||||
|     this.focusInput(); |  | ||||||
|   }, |  | ||||||
|  |  | ||||||
|   methods: { |  | ||||||
|     focusInput() { |  | ||||||
|       this.$refs.searchbar.focus(); |  | ||||||
|     }, |  | ||||||
|     addNewAttribute() { |  | ||||||
|       this.$router.push( |  | ||||||
|         `/app/accounts/${this.accountId}/settings/custom-attributes/list` |  | ||||||
|       ); |  | ||||||
|     }, |  | ||||||
|     async onAddAttribute(attribute) { |  | ||||||
|       this.$emit('add-attribute', attribute); |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .search-input { |  | ||||||
|   @apply m-0 w-full border border-solid border-transparent h-8 text-sm text-slate-700 dark:text-slate-100 rounded-md focus:border-woot-500 bg-slate-50 dark:bg-slate-900; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,79 +0,0 @@ | |||||||
| <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> |  | ||||||
| @@ -1,138 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="custom-attribute--selector"> |  | ||||||
|     <div |  | ||||||
|       v-on-clickaway="closeDropdown" |  | ||||||
|       class="label-wrap" |  | ||||||
|       @keyup.esc="closeDropdown" |  | ||||||
|     > |  | ||||||
|       <woot-button |  | ||||||
|         size="small" |  | ||||||
|         variant="link" |  | ||||||
|         icon="add" |  | ||||||
|         @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" |  | ||||||
|             :contact-id="contactId" |  | ||||||
|             @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', |  | ||||||
|     }, |  | ||||||
|     contactId: { type: Number, default: null }, |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       showAttributeDropDown: false, |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     async addAttribute(attribute) { |  | ||||||
|       try { |  | ||||||
|         const { |  | ||||||
|           attribute_key: attributeKey, |  | ||||||
|           attribute_display_type: attributeDisplayType, |  | ||||||
|           default_value: attributeDefaultValue, |  | ||||||
|         } = attribute; |  | ||||||
|         const isCheckbox = attributeDisplayType === 'checkbox'; |  | ||||||
|         const defaultValue = isCheckbox ? false : attributeDefaultValue || null; |  | ||||||
|         if (this.attributeType === 'conversation_attribute') { |  | ||||||
|           await this.$store.dispatch('updateCustomAttributes', { |  | ||||||
|             conversationId: this.conversationId, |  | ||||||
|             customAttributes: { |  | ||||||
|               ...this.customAttributes, |  | ||||||
|               [attributeKey]: defaultValue, |  | ||||||
|             }, |  | ||||||
|           }); |  | ||||||
|         } else { |  | ||||||
|           await this.$store.dispatch('contacts/update', { |  | ||||||
|             id: this.contactId, |  | ||||||
|             custom_attributes: { |  | ||||||
|               ...this.customAttributes, |  | ||||||
|               [attributeKey]: defaultValue, |  | ||||||
|             }, |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|         bus.$emit(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, attributeKey); |  | ||||||
|         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> |  | ||||||
| @@ -1,23 +1,35 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="custom-attributes--panel"> |   <div class="custom-attributes--panel"> | ||||||
|     <custom-attribute |     <custom-attribute | ||||||
|       v-for="attribute in filteredAttributes" |       v-for="attribute in displayedAttributes" | ||||||
|       :key="attribute.id" |       :key="attribute.id" | ||||||
|       :attribute-key="attribute.attribute_key" |       :attribute-key="attribute.attribute_key" | ||||||
|       :attribute-type="attribute.attribute_display_type" |       :attribute-type="attribute.attribute_display_type" | ||||||
|       :values="attribute.attribute_values" |       :values="attribute.attribute_values" | ||||||
|       :label="attribute.attribute_display_name" |       :label="attribute.attribute_display_name" | ||||||
|       :icon="attribute.icon" |  | ||||||
|       emoji="" |  | ||||||
|       :value="attribute.value" |       :value="attribute.value" | ||||||
|       :show-actions="true" |       :show-actions="true" | ||||||
|       :attribute-regex="attribute.regex_pattern" |       :attribute-regex="attribute.regex_pattern" | ||||||
|       :regex-cue="attribute.regex_cue" |       :regex-cue="attribute.regex_cue" | ||||||
|       :class="attributeClass" |       :class="attributeClass" | ||||||
|  |       :contact-id="contactId" | ||||||
|       @update="onUpdate" |       @update="onUpdate" | ||||||
|       @delete="onDelete" |       @delete="onDelete" | ||||||
|       @copy="onCopy" |       @copy="onCopy" | ||||||
|     /> |     /> | ||||||
|  |     <!-- Show more and show less buttons show it if the filteredAttributes length is greater than 5 --> | ||||||
|  |     <div v-if="filteredAttributes.length > 5" class="flex px-2 py-2"> | ||||||
|  |       <woot-button | ||||||
|  |         size="small" | ||||||
|  |         :icon="showAllAttributes ? 'chevron-up' : 'chevron-down'" | ||||||
|  |         variant="clear" | ||||||
|  |         color-scheme="primary" | ||||||
|  |         class="!px-2 hover:!bg-transparent dark:hover:!bg-transparent" | ||||||
|  |         @click="onClickToggle" | ||||||
|  |       > | ||||||
|  |         {{ toggleButtonText }} | ||||||
|  |       </woot-button> | ||||||
|  |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -25,13 +37,14 @@ | |||||||
| import CustomAttribute from 'dashboard/components/CustomAttribute.vue'; | import CustomAttribute from 'dashboard/components/CustomAttribute.vue'; | ||||||
| import alertMixin from 'shared/mixins/alertMixin'; | import alertMixin from 'shared/mixins/alertMixin'; | ||||||
| import attributeMixin from 'dashboard/mixins/attributeMixin'; | import attributeMixin from 'dashboard/mixins/attributeMixin'; | ||||||
|  | import uiSettingsMixin from 'dashboard/mixins/uiSettings'; | ||||||
| import { copyTextToClipboard } from 'shared/helpers/clipboard'; | import { copyTextToClipboard } from 'shared/helpers/clipboard'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     CustomAttribute, |     CustomAttribute, | ||||||
|   }, |   }, | ||||||
|   mixins: [alertMixin, attributeMixin], |   mixins: [alertMixin, attributeMixin, uiSettingsMixin], | ||||||
|   props: { |   props: { | ||||||
|     attributeType: { |     attributeType: { | ||||||
|       type: String, |       type: String, | ||||||
| @@ -42,8 +55,67 @@ export default { | |||||||
|       default: '', |       default: '', | ||||||
|     }, |     }, | ||||||
|     contactId: { type: Number, default: null }, |     contactId: { type: Number, default: null }, | ||||||
|  |     attributeFrom: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       showAllAttributes: false, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     toggleButtonText() { | ||||||
|  |       return !this.showAllAttributes | ||||||
|  |         ? this.$t('CUSTOM_ATTRIBUTES.SHOW_MORE') | ||||||
|  |         : this.$t('CUSTOM_ATTRIBUTES.SHOW_LESS'); | ||||||
|  |     }, | ||||||
|  |     filteredAttributes() { | ||||||
|  |       return this.attributes.map(attribute => { | ||||||
|  |         // Check if the attribute key exists in customAttributes | ||||||
|  |         const hasValue = Object.hasOwnProperty.call( | ||||||
|  |           this.customAttributes, | ||||||
|  |           attribute.attribute_key | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         const isCheckbox = attribute.attribute_display_type === 'checkbox'; | ||||||
|  |         const defaultValue = isCheckbox ? false : ''; | ||||||
|  |  | ||||||
|  |         return { | ||||||
|  |           ...attribute, | ||||||
|  |           // Set value from customAttributes if it exists, otherwise use default value | ||||||
|  |           value: hasValue | ||||||
|  |             ? this.customAttributes[attribute.attribute_key] | ||||||
|  |             : defaultValue, | ||||||
|  |         }; | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     displayedAttributes() { | ||||||
|  |       // Show only the first 5 attributes or all depending on showAllAttributes | ||||||
|  |       if (this.showAllAttributes || this.filteredAttributes.length <= 5) { | ||||||
|  |         return this.filteredAttributes; | ||||||
|  |       } | ||||||
|  |       return this.filteredAttributes.slice(0, 5); | ||||||
|  |     }, | ||||||
|  |     showMoreUISettingsKey() { | ||||||
|  |       return `show_all_attributes_${this.attributeFrom}`; | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.initializeSettings(); | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |     initializeSettings() { | ||||||
|  |       this.showAllAttributes = | ||||||
|  |         this.uiSettings[this.showMoreUISettingsKey] || false; | ||||||
|  |     }, | ||||||
|  |     onClickToggle() { | ||||||
|  |       this.showAllAttributes = !this.showAllAttributes; | ||||||
|  |       this.updateUISettings({ | ||||||
|  |         [this.showMoreUISettingsKey]: this.showAllAttributes, | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|     async onUpdate(key, value) { |     async onUpdate(key, value) { | ||||||
|       const updatedAttributes = { ...this.customAttributes, [key]: value }; |       const updatedAttributes = { ...this.customAttributes, [key]: value }; | ||||||
|       try { |       try { | ||||||
| @@ -96,16 +168,17 @@ export default { | |||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style scoped lang="scss"> | <style scoped lang="scss"> | ||||||
| .custom-attributes--panel { | .custom-attributes--panel { | ||||||
|   .conversation--attribute { |   .conversation--attribute { | ||||||
|     @apply border-slate-50 dark:border-slate-700 border-b border-solid; |     @apply border-slate-50 dark:border-slate-700/50 border-b border-solid; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &.odd { |   &.odd { | ||||||
|     .conversation--attribute { |     .conversation--attribute { | ||||||
|       &:nth-child(2n + 1) { |       &:nth-child(2n + 1) { | ||||||
|         @apply bg-slate-25 dark:bg-slate-800; |         @apply bg-slate-25 dark:bg-slate-800/50; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -113,7 +186,7 @@ export default { | |||||||
|   &.even { |   &.even { | ||||||
|     .conversation--attribute { |     .conversation--attribute { | ||||||
|       &:nth-child(2n) { |       &:nth-child(2n) { | ||||||
|         @apply bg-slate-25 dark:bg-slate-800; |         @apply bg-slate-25 dark:bg-slate-800/50; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -7,10 +7,10 @@ | |||||||
|     <woot-button |     <woot-button | ||||||
|       variant="hollow" |       variant="hollow" | ||||||
|       color-scheme="secondary" |       color-scheme="secondary" | ||||||
|       class="w-full border border-solid border-slate-200 dark:border-slate-700 px-2.5 hover:border-slate-75 dark:hover:border-slate-600" |       class="w-full border border-solid border-slate-100 dark:border-slate-700 px-2 hover:border-slate-75 dark:hover:border-slate-600" | ||||||
|       @click="toggleDropdown" |       @click="toggleDropdown" | ||||||
|     > |     > | ||||||
|       <div class="flex"> |       <div class="flex gap-1"> | ||||||
|         <Thumbnail |         <Thumbnail | ||||||
|           v-if="hasValue && hasThumbnail" |           v-if="hasValue && hasThumbnail" | ||||||
|           :src="selectedItem.thumbnail" |           :src="selectedItem.thumbnail" | ||||||
| @@ -21,19 +21,22 @@ | |||||||
|         <div class="flex justify-between w-full min-w-0 items-center"> |         <div class="flex justify-between w-full min-w-0 items-center"> | ||||||
|           <h4 |           <h4 | ||||||
|             v-if="!hasValue" |             v-if="!hasValue" | ||||||
|             class="mt-0 mb-0 mr-2 ml-0 text-ellipsis text-sm text-slate-800 dark:text-slate-100" |             class="text-ellipsis text-sm text-slate-800 dark:text-slate-100" | ||||||
|           > |           > | ||||||
|             {{ multiselectorPlaceholder }} |             {{ multiselectorPlaceholder }} | ||||||
|           </h4> |           </h4> | ||||||
|           <h4 |           <h4 | ||||||
|             v-else |             v-else | ||||||
|             class="items-center leading-tight my-0 mx-2 overflow-hidden whitespace-nowrap text-ellipsis text-sm text-slate-800 dark:text-slate-100" |             class="items-center leading-tight overflow-hidden whitespace-nowrap text-ellipsis text-sm text-slate-800 dark:text-slate-100" | ||||||
|             :title="selectedItem.name" |             :title="selectedItem.name" | ||||||
|           > |           > | ||||||
|             {{ selectedItem.name }} |             {{ selectedItem.name }} | ||||||
|           </h4> |           </h4> | ||||||
|           <i v-if="showSearchDropdown" class="icon ion-chevron-up" /> |           <i | ||||||
|           <i v-else class="icon ion-chevron-down" /> |             v-if="showSearchDropdown" | ||||||
|  |             class="icon ion-chevron-up text-slate-600 mr-1" | ||||||
|  |           /> | ||||||
|  |           <i v-else class="icon ion-chevron-down text-slate-600 mr-1" /> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </woot-button> |     </woot-button> | ||||||
| @@ -137,6 +140,7 @@ export default { | |||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .dropdown-pane { | .dropdown-pane { | ||||||
|   @apply box-border top-[2.625rem] w-full; |   @apply box-border top-[2.625rem] w-full; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Sivin Varghese
					Sivin Varghese