mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 11:08:04 +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', | ||||
|     Roboto, | ||||
|     'Helvetica Neue', | ||||
|     Tahoma, | ||||
|     Arial, | ||||
|     sans-serif !important; | ||||
|   -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; | ||||
|  | ||||
|     &.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 { | ||||
|   | ||||
| @@ -2,23 +2,27 @@ | ||||
|   <div class="py-3 px-4"> | ||||
|     <div class="flex items-center mb-1"> | ||||
|       <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 | ||||
|             v-model="editedValue" | ||||
|             class="checkbox" | ||||
|             class="!my-0 mr-2 ml-0" | ||||
|             type="checkbox" | ||||
|             @change="onUpdate" | ||||
|           /> | ||||
|         </div> | ||||
|         <div class="flex items-center justify-between w-full"> | ||||
|           <span | ||||
|             class="attribute-name w-full text-slate-800 dark:text-slate-100 font-medium text-sm mb-0" | ||||
|             :class="{ error: $v.editedValue.$error }" | ||||
|             class="w-full font-medium text-sm mb-0" | ||||
|             :class=" | ||||
|               $v.editedValue.$error | ||||
|                 ? 'text-red-400 dark:text-red-500' | ||||
|                 : 'text-slate-800 dark:text-slate-100' | ||||
|             " | ||||
|           > | ||||
|             {{ label }} | ||||
|           </span> | ||||
|           <woot-button | ||||
|             v-if="showActions" | ||||
|             v-if="showCopyAndDeleteButton" | ||||
|             v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')" | ||||
|             variant="link" | ||||
|             size="medium" | ||||
| @@ -31,7 +35,7 @@ | ||||
|       </h4> | ||||
|     </div> | ||||
|     <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"> | ||||
|           <input | ||||
|             ref="inputfield" | ||||
| @@ -61,7 +65,7 @@ | ||||
|       </div> | ||||
|       <div | ||||
|         v-show="!isEditing" | ||||
|         class="value--view" | ||||
|         class="flex group" | ||||
|         :class="{ 'is-editable': showActions }" | ||||
|       > | ||||
|         <a | ||||
| @@ -69,35 +73,35 @@ | ||||
|           :href="hrefURL" | ||||
|           target="_blank" | ||||
|           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 }} | ||||
|         </a> | ||||
|         <p | ||||
|           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 || '---' }} | ||||
|         </p> | ||||
|         <div class="flex max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0"> | ||||
|           <woot-button | ||||
|             v-if="showActions" | ||||
|             v-if="showCopyAndDeleteButton" | ||||
|             v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')" | ||||
|             variant="link" | ||||
|             size="small" | ||||
|             color-scheme="secondary" | ||||
|             icon="clipboard" | ||||
|             class-names="edit-button" | ||||
|             class-names="hidden group-hover:flex !w-6 flex-shrink-0" | ||||
|             @click="onCopy" | ||||
|           /> | ||||
|           <woot-button | ||||
|             v-if="showActions" | ||||
|             v-if="showEditButton" | ||||
|             v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')" | ||||
|             variant="link" | ||||
|             size="small" | ||||
|             color-scheme="secondary" | ||||
|             icon="edit" | ||||
|             class-names="edit-button" | ||||
|             class-names="hidden group-hover:flex !w-6 flex-shrink-0" | ||||
|             @click="onEdit" | ||||
|           /> | ||||
|         </div> | ||||
| @@ -126,6 +130,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mixin as clickaway } from 'vue-clickaway'; | ||||
| import { format, parseISO } from 'date-fns'; | ||||
| import { required, url } from 'vuelidate/lib/validators'; | ||||
| import { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||
| @@ -138,7 +143,7 @@ export default { | ||||
|   components: { | ||||
|     MultiselectDropdown, | ||||
|   }, | ||||
|   mixins: [customAttributeMixin], | ||||
|   mixins: [customAttributeMixin, clickaway], | ||||
|   props: { | ||||
|     label: { type: String, required: true }, | ||||
|     values: { type: Array, default: () => [] }, | ||||
| @@ -160,11 +165,18 @@ export default { | ||||
|       editedValue: null, | ||||
|     }; | ||||
|   }, | ||||
|  | ||||
|   computed: { | ||||
|     showCopyAndDeleteButton() { | ||||
|       return this.value && this.showActions; | ||||
|     }, | ||||
|     showEditButton() { | ||||
|       return !this.value && this.showActions; | ||||
|     }, | ||||
|     displayValue() { | ||||
|       if (this.isAttributeTypeDate) { | ||||
|         return new Date(this.value || new Date()).toLocaleDateString(); | ||||
|         return this.value | ||||
|           ? new Date(this.value || new Date()).toLocaleDateString() | ||||
|           : ''; | ||||
|       } | ||||
|       if (this.isAttributeTypeCheckbox) { | ||||
|         return this.value === 'false' ? false : this.value; | ||||
| @@ -230,6 +242,10 @@ export default { | ||||
|       this.isEditing = false; | ||||
|       this.editedValue = this.formattedValue; | ||||
|     }, | ||||
|     contactId() { | ||||
|       // Fix to solve validation not resetting when contactId changes in contact page | ||||
|       this.$v.$reset(); | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   validations() { | ||||
| @@ -268,6 +284,10 @@ export default { | ||||
|         this.$refs.inputfield.focus(); | ||||
|       } | ||||
|     }, | ||||
|     onClickAway() { | ||||
|       this.$v.$reset(); | ||||
|       this.isEditing = false; | ||||
|     }, | ||||
|     onEdit() { | ||||
|       this.isEditing = true; | ||||
|       this.$nextTick(() => { | ||||
| @@ -294,6 +314,7 @@ export default { | ||||
|     }, | ||||
|     onDelete() { | ||||
|       this.isEditing = false; | ||||
|       this.$v.$reset(); | ||||
|       this.$emit('delete', this.attributeKey); | ||||
|     }, | ||||
|     onCopy() { | ||||
| @@ -304,35 +325,6 @@ export default { | ||||
| </script> | ||||
|  | ||||
| <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 { | ||||
|   .selector-wrap { | ||||
|     @apply m-0 top-1; | ||||
|   | ||||
| @@ -296,6 +296,8 @@ | ||||
|     "BUTTON": "Add custom attribute", | ||||
|     "NOT_AVAILABLE": "There are no custom attributes available for this contact.", | ||||
|     "COPY_SUCCESSFUL": "Copied to clipboard successfully", | ||||
|     "SHOW_MORE": "Show all attributes", | ||||
|     "SHOW_LESS": "Show less attributes", | ||||
|     "ACTIONS": { | ||||
|       "COPY": "Copy attribute", | ||||
|       "DELETE": "Delete attribute", | ||||
|   | ||||
| @@ -29,33 +29,6 @@ export default { | ||||
|     conversationId() { | ||||
|       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: { | ||||
|     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 attributeMixin from '../attributeMixin'; | ||||
| import Vuex from 'vuex'; | ||||
| import attributeFixtures from './attributeFixtures'; | ||||
|  | ||||
| const localVue = createLocalVue(); | ||||
| localVue.use(Vuex); | ||||
| @@ -41,43 +40,6 @@ describe('attributeMixin', () => { | ||||
|     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', () => { | ||||
|     const Component = { | ||||
|       render() {}, | ||||
|   | ||||
| @@ -38,13 +38,10 @@ | ||||
|                 :contact-id="contact.id" | ||||
|                 attribute-type="contact_attribute" | ||||
|                 attribute-class="conversation--attribute" | ||||
|                 attribute-from="contact_panel" | ||||
|                 :custom-attributes="contact.custom_attributes" | ||||
|                 class="even" | ||||
|               /> | ||||
|               <custom-attribute-selector | ||||
|                 attribute-type="contact_attribute" | ||||
|                 :contact-id="contact.id" | ||||
|               /> | ||||
|             </accordion-item> | ||||
|           </div> | ||||
|           <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 ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.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 uiSettingsMixin from 'dashboard/mixins/uiSettings'; | ||||
|  | ||||
| @@ -96,7 +92,6 @@ export default { | ||||
|     ContactInfo, | ||||
|     ContactLabel, | ||||
|     CustomAttributes, | ||||
|     CustomAttributeSelector, | ||||
|     draggable, | ||||
|   }, | ||||
|   mixins: [uiSettingsMixin], | ||||
|   | ||||
| @@ -87,10 +87,7 @@ | ||||
|                 attribute-type="contact_attribute" | ||||
|                 attribute-class="conversation--attribute" | ||||
|                 class="even" | ||||
|                 :contact-id="contact.id" | ||||
|               /> | ||||
|               <custom-attribute-selector | ||||
|                 attribute-type="contact_attribute" | ||||
|                 attribute-from="conversation_contact_panel" | ||||
|                 :contact-id="contact.id" | ||||
|               /> | ||||
|             </accordion-item> | ||||
| @@ -142,7 +139,6 @@ import ConversationParticipant from './ConversationParticipant.vue'; | ||||
| import ContactInfo from './contact/ContactInfo.vue'; | ||||
| import ConversationInfo from './ConversationInfo.vue'; | ||||
| import CustomAttributes from './customAttributes/CustomAttributes.vue'; | ||||
| import CustomAttributeSelector from './customAttributes/CustomAttributeSelector.vue'; | ||||
| import draggable from 'vuedraggable'; | ||||
| import uiSettingsMixin from 'dashboard/mixins/uiSettings'; | ||||
| import MacrosList from './Macros/List.vue'; | ||||
| @@ -154,7 +150,6 @@ export default { | ||||
|     ContactInfo, | ||||
|     ConversationInfo, | ||||
|     CustomAttributes, | ||||
|     CustomAttributeSelector, | ||||
|     ConversationAction, | ||||
|     ConversationParticipant, | ||||
|     draggable, | ||||
|   | ||||
| @@ -1,152 +1,109 @@ | ||||
| <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="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'; | ||||
| <script setup> | ||||
| import { computed } from 'vue'; | ||||
| import { getLanguageName } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages'; | ||||
| import ContactDetailsItem from './ContactDetailsItem.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 { | ||||
|   components: { | ||||
|     ContactDetailsItem, | ||||
|     CustomAttributes, | ||||
|     CustomAttributeSelector, | ||||
|   }, | ||||
|   props: { | ||||
|     conversationAttributes: { | ||||
|       type: Object, | ||||
|       default: () => ({}), | ||||
|     }, | ||||
|     contactAttributes: { | ||||
|       type: Object, | ||||
|       default: () => ({}), | ||||
|     }, | ||||
|   }, | ||||
|   STATIC_ATTRIBUTES: [ | ||||
| const referer = computed(() => props.conversationAttributes.referer); | ||||
| const initiatedAt = computed( | ||||
|   () => props.conversationAttributes.initiated_at?.timestamp | ||||
| ); | ||||
|  | ||||
| const browserInfo = props.conversationAttributes.browser; | ||||
|  | ||||
| const browserName = computed(() => { | ||||
|   if (!browserInfo) return ''; | ||||
|   const { browser_name: name = '', browser_version: version = '' } = | ||||
|     browserInfo; | ||||
|   return `${name} ${version}`; | ||||
| }); | ||||
|  | ||||
| const browserLanguage = computed(() => | ||||
|   getLanguageName(props.conversationAttributes.browser_language) | ||||
| ); | ||||
|  | ||||
| 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', | ||||
|       label: 'CONTACT_PANEL.INITIATED_AT', | ||||
|       content: initiatedAt, | ||||
|       title: 'CONTACT_PANEL.INITIATED_AT', | ||||
|     }, | ||||
|     { | ||||
|       name: 'referer', | ||||
|       label: 'CONTACT_PANEL.BROWSER', | ||||
|       content: browserLanguage, | ||||
|       title: 'CONTACT_PANEL.BROWSER_LANGUAGE', | ||||
|     }, | ||||
|     { | ||||
|       name: 'browserName', | ||||
|       label: 'CONTACT_PANEL.BROWSER', | ||||
|       content: referer, | ||||
|       title: 'CONTACT_PANEL.INITIATED_FROM', | ||||
|       type: 'link', | ||||
|     }, | ||||
|     { | ||||
|       name: 'platformName', | ||||
|       label: 'CONTACT_PANEL.OS', | ||||
|       content: browserName, | ||||
|       title: 'CONTACT_PANEL.BROWSER', | ||||
|     }, | ||||
|     { | ||||
|       name: 'ipAddress', | ||||
|       label: 'CONTACT_PANEL.IP_ADDRESS', | ||||
|       content: platformName, | ||||
|       title: 'CONTACT_PANEL.OS', | ||||
|     }, | ||||
|   ], | ||||
|   computed: { | ||||
|     referer() { | ||||
|       return this.conversationAttributes.referer; | ||||
|     { | ||||
|       content: createdAtIp, | ||||
|       title: 'CONTACT_PANEL.IP_ADDRESS', | ||||
|     }, | ||||
|     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}`; | ||||
|     }, | ||||
|     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'; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|   ].filter(attribute => !!attribute.content.value) | ||||
| ); | ||||
| </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"> | ||||
| .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) { | ||||
|     @apply bg-slate-25 dark:bg-slate-800; | ||||
|     @apply bg-slate-25 dark:bg-slate-800/50; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -125,8 +125,9 @@ | ||||
|       > | ||||
|         <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" | ||||
|           >{{ socialProfile.prefixURL }}</span | ||||
|         > | ||||
|           {{ socialProfile.prefixURL }} | ||||
|         </span> | ||||
|         <input | ||||
|           v-model="socialProfileUserNames[socialProfile.key]" | ||||
|           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> | ||||
|   <div class="custom-attributes--panel"> | ||||
|     <custom-attribute | ||||
|       v-for="attribute in filteredAttributes" | ||||
|       v-for="attribute in displayedAttributes" | ||||
|       :key="attribute.id" | ||||
|       :attribute-key="attribute.attribute_key" | ||||
|       :attribute-type="attribute.attribute_display_type" | ||||
|       :values="attribute.attribute_values" | ||||
|       :label="attribute.attribute_display_name" | ||||
|       :icon="attribute.icon" | ||||
|       emoji="" | ||||
|       :value="attribute.value" | ||||
|       :show-actions="true" | ||||
|       :attribute-regex="attribute.regex_pattern" | ||||
|       :regex-cue="attribute.regex_cue" | ||||
|       :class="attributeClass" | ||||
|       :contact-id="contactId" | ||||
|       @update="onUpdate" | ||||
|       @delete="onDelete" | ||||
|       @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> | ||||
| </template> | ||||
|  | ||||
| @@ -25,13 +37,14 @@ | ||||
| import CustomAttribute from 'dashboard/components/CustomAttribute.vue'; | ||||
| import alertMixin from 'shared/mixins/alertMixin'; | ||||
| import attributeMixin from 'dashboard/mixins/attributeMixin'; | ||||
| import uiSettingsMixin from 'dashboard/mixins/uiSettings'; | ||||
| import { copyTextToClipboard } from 'shared/helpers/clipboard'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     CustomAttribute, | ||||
|   }, | ||||
|   mixins: [alertMixin, attributeMixin], | ||||
|   mixins: [alertMixin, attributeMixin, uiSettingsMixin], | ||||
|   props: { | ||||
|     attributeType: { | ||||
|       type: String, | ||||
| @@ -42,8 +55,67 @@ export default { | ||||
|       default: '', | ||||
|     }, | ||||
|     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: { | ||||
|     initializeSettings() { | ||||
|       this.showAllAttributes = | ||||
|         this.uiSettings[this.showMoreUISettingsKey] || false; | ||||
|     }, | ||||
|     onClickToggle() { | ||||
|       this.showAllAttributes = !this.showAllAttributes; | ||||
|       this.updateUISettings({ | ||||
|         [this.showMoreUISettingsKey]: this.showAllAttributes, | ||||
|       }); | ||||
|     }, | ||||
|     async onUpdate(key, value) { | ||||
|       const updatedAttributes = { ...this.customAttributes, [key]: value }; | ||||
|       try { | ||||
| @@ -96,16 +168,17 @@ export default { | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
| .custom-attributes--panel { | ||||
|   .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 { | ||||
|     .conversation--attribute { | ||||
|       &: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 { | ||||
|     .conversation--attribute { | ||||
|       &: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 | ||||
|       variant="hollow" | ||||
|       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" | ||||
|     > | ||||
|       <div class="flex"> | ||||
|       <div class="flex gap-1"> | ||||
|         <Thumbnail | ||||
|           v-if="hasValue && hasThumbnail" | ||||
|           :src="selectedItem.thumbnail" | ||||
| @@ -21,19 +21,22 @@ | ||||
|         <div class="flex justify-between w-full min-w-0 items-center"> | ||||
|           <h4 | ||||
|             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 }} | ||||
|           </h4> | ||||
|           <h4 | ||||
|             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" | ||||
|           > | ||||
|             {{ selectedItem.name }} | ||||
|           </h4> | ||||
|           <i v-if="showSearchDropdown" class="icon ion-chevron-up" /> | ||||
|           <i v-else class="icon ion-chevron-down" /> | ||||
|           <i | ||||
|             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> | ||||
|     </woot-button> | ||||
| @@ -137,6 +140,7 @@ export default { | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .dropdown-pane { | ||||
|   @apply box-border top-[2.625rem] w-full; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Sivin Varghese
					Sivin Varghese