mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +00:00 
			
		
		
		
	 42548dc24c
			
		
	
	42548dc24c
	
	
	
		
			
			# Pull Request Template ## Description This PR fixes, 1. Fix display issue where the attribute value `0` was not shown for attributes of type "number" and instead displayed as `"---"` 2. Fix issue where the copy and delete buttons were not visible when the attribute value was `0` 3. Fix an issue with updating contact custom attributes in the conversation sidebar (caused by the camelCase param key mismatch in `contacts/update`; only reproducible for attributes type "date") 4. Ensure the delete button is shown for checkbox-type attributes, even when the value is `true` or `false` (previously hidden when the value was `false`, despite the key being present) Fixes [CW-4326](https://linear.app/chatwoot/issue/CW-4326/custom-attribute-with-value-0-not-displayed-correctly-in-chatwoot) https://github.com/chatwoot/chatwoot/issues/11459 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ### Loom video https://www.loom.com/share/77257dc09a37452bab7876d1554b8a03?sid=dc5635eb-4fe0-4f39-8da2-036d71649ffc ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
		
			
				
	
	
		
			352 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			352 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <script>
 | |
| import { format, parseISO } from 'date-fns';
 | |
| import { required, url } from '@vuelidate/validators';
 | |
| import { BUS_EVENTS } from 'shared/constants/busEvents';
 | |
| import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
 | |
| import HelperTextPopup from 'dashboard/components/ui/HelperTextPopup.vue';
 | |
| import { isValidURL } from '../helper/URLHelper';
 | |
| import { getRegexp } from 'shared/helpers/Validators';
 | |
| import { useVuelidate } from '@vuelidate/core';
 | |
| import { emitter } from 'shared/helpers/mitt';
 | |
| 
 | |
| import NextButton from 'dashboard/components-next/button/Button.vue';
 | |
| 
 | |
| const DATE_FORMAT = 'yyyy-MM-dd';
 | |
| 
 | |
| export default {
 | |
|   components: {
 | |
|     MultiselectDropdown,
 | |
|     HelperTextPopup,
 | |
|     NextButton,
 | |
|   },
 | |
|   props: {
 | |
|     label: { type: String, required: true },
 | |
|     description: { type: String, default: '' },
 | |
|     values: { type: Array, default: () => [] },
 | |
|     value: { type: [String, Number, Boolean], default: '' },
 | |
|     showActions: { type: Boolean, default: false },
 | |
|     attributeType: { type: String, default: 'text' },
 | |
|     attributeRegex: {
 | |
|       type: String,
 | |
|       default: null,
 | |
|     },
 | |
|     regexCue: { type: String, default: null },
 | |
|     attributeKey: { type: String, required: true },
 | |
|     contactId: { type: Number, default: null },
 | |
|   },
 | |
|   emits: ['update', 'delete', 'copy'],
 | |
|   setup() {
 | |
|     return { v$: useVuelidate() };
 | |
|   },
 | |
|   data() {
 | |
|     return {
 | |
|       isEditing: false,
 | |
|       editedValue: null,
 | |
|     };
 | |
|   },
 | |
|   computed: {
 | |
|     displayValue() {
 | |
|       if (this.isAttributeTypeDate) {
 | |
|         return this.value
 | |
|           ? new Date(this.value || new Date()).toLocaleDateString()
 | |
|           : '---';
 | |
|       }
 | |
|       if (this.isAttributeTypeCheckbox) {
 | |
|         return this.value === 'false' ? false : this.value;
 | |
|       }
 | |
|       return this.hasValue ? this.value : '---';
 | |
|     },
 | |
|     formattedValue() {
 | |
|       return this.isAttributeTypeDate
 | |
|         ? format(this.value ? new Date(this.value) : new Date(), DATE_FORMAT)
 | |
|         : this.value;
 | |
|     },
 | |
|     listOptions() {
 | |
|       return this.values.map((value, index) => ({
 | |
|         id: index + 1,
 | |
|         name: value,
 | |
|       }));
 | |
|     },
 | |
|     selectedItem() {
 | |
|       const id = this.values.indexOf(this.editedValue) + 1;
 | |
|       return { id, name: this.editedValue };
 | |
|     },
 | |
|     isAttributeTypeCheckbox() {
 | |
|       return this.attributeType === 'checkbox';
 | |
|     },
 | |
|     isAttributeTypeList() {
 | |
|       return this.attributeType === 'list';
 | |
|     },
 | |
|     isAttributeTypeLink() {
 | |
|       return this.attributeType === 'link';
 | |
|     },
 | |
|     isAttributeTypeDate() {
 | |
|       return this.attributeType === 'date';
 | |
|     },
 | |
|     hasValue() {
 | |
|       return this.value !== null && this.value !== '';
 | |
|     },
 | |
|     urlValue() {
 | |
|       return isValidURL(this.value) ? this.value : '---';
 | |
|     },
 | |
|     hrefURL() {
 | |
|       return isValidURL(this.value) ? this.value : '';
 | |
|     },
 | |
|     notAttributeTypeCheckboxAndList() {
 | |
|       return !this.isAttributeTypeCheckbox && !this.isAttributeTypeList;
 | |
|     },
 | |
|     inputType() {
 | |
|       return this.isAttributeTypeLink ? 'url' : this.attributeType;
 | |
|     },
 | |
|     shouldShowErrorMessage() {
 | |
|       return this.v$.editedValue.$error;
 | |
|     },
 | |
|     errorMessage() {
 | |
|       if (this.v$.editedValue.url) {
 | |
|         return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_URL');
 | |
|       }
 | |
|       if (!this.v$.editedValue.regexValidation) {
 | |
|         return this.regexCue
 | |
|           ? this.regexCue
 | |
|           : this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_INPUT');
 | |
|       }
 | |
|       return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
 | |
|     },
 | |
|   },
 | |
|   watch: {
 | |
|     value() {
 | |
|       this.isEditing = false;
 | |
|       this.editedValue = this.formattedValue;
 | |
|     },
 | |
|     contactId() {
 | |
|       // Fix to solve validation not resetting when contactId changes in contact page
 | |
|       this.v$.$reset();
 | |
|     },
 | |
|   },
 | |
| 
 | |
|   validations() {
 | |
|     if (this.isAttributeTypeLink) {
 | |
|       return {
 | |
|         editedValue: { required, url },
 | |
|       };
 | |
|     }
 | |
|     return {
 | |
|       editedValue: {
 | |
|         required,
 | |
|         regexValidation: value => {
 | |
|           return !(
 | |
|             this.attributeRegex && !getRegexp(this.attributeRegex).test(value)
 | |
|           );
 | |
|         },
 | |
|       },
 | |
|     };
 | |
|   },
 | |
|   mounted() {
 | |
|     this.editedValue = this.formattedValue;
 | |
|     emitter.on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
 | |
|   },
 | |
|   unmounted() {
 | |
|     emitter.off(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
 | |
|   },
 | |
|   methods: {
 | |
|     onFocusAttribute(focusAttributeKey) {
 | |
|       if (this.attributeKey === focusAttributeKey) {
 | |
|         this.onEdit();
 | |
|       }
 | |
|     },
 | |
|     focusInput() {
 | |
|       if (this.$refs.inputfield) {
 | |
|         this.$refs.inputfield.focus();
 | |
|       }
 | |
|     },
 | |
|     onClickAway() {
 | |
|       this.v$.$reset();
 | |
|       this.isEditing = false;
 | |
|     },
 | |
|     onEdit() {
 | |
|       this.isEditing = true;
 | |
|       this.$nextTick(() => {
 | |
|         this.focusInput();
 | |
|       });
 | |
|     },
 | |
|     onUpdateListValue(value) {
 | |
|       if (value) {
 | |
|         this.editedValue = value.name;
 | |
|         this.onUpdate();
 | |
|       }
 | |
|     },
 | |
|     onUpdate() {
 | |
|       const updatedValue =
 | |
|         this.attributeType === 'date'
 | |
|           ? parseISO(this.editedValue)
 | |
|           : this.editedValue;
 | |
|       this.v$.$touch();
 | |
|       if (this.v$.$invalid) {
 | |
|         return;
 | |
|       }
 | |
|       this.isEditing = false;
 | |
|       this.$emit('update', this.attributeKey, updatedValue);
 | |
|     },
 | |
|     onDelete() {
 | |
|       this.isEditing = false;
 | |
|       this.v$.$reset();
 | |
|       this.$emit('delete', this.attributeKey);
 | |
|     },
 | |
|     onCopy() {
 | |
|       this.$emit('copy', this.value);
 | |
|     },
 | |
|   },
 | |
| };
 | |
| </script>
 | |
| 
 | |
| <template>
 | |
|   <div class="px-4 py-3">
 | |
|     <div class="flex items-center mb-1">
 | |
|       <h4 class="flex items-center w-full m-0 text-sm error">
 | |
|         <div v-if="isAttributeTypeCheckbox" class="flex items-center">
 | |
|           <input
 | |
|             v-model="editedValue"
 | |
|             class="!my-0 ltr:mr-2 ltr:ml-0 rtl:mr-0 rtl:ml-2"
 | |
|             type="checkbox"
 | |
|             @change="onUpdate"
 | |
|           />
 | |
|         </div>
 | |
|         <div class="flex items-center justify-between w-full">
 | |
|           <span
 | |
|             class="w-full inline-flex gap-1.5 items-start font-medium whitespace-nowrap text-sm mb-0"
 | |
|             :class="
 | |
|               v$.editedValue.$error ? 'text-n-ruby-11' : 'text-n-slate-12'
 | |
|             "
 | |
|           >
 | |
|             {{ label }}
 | |
|             <HelperTextPopup
 | |
|               v-if="description"
 | |
|               :message="description"
 | |
|               class="mt-0.5"
 | |
|             />
 | |
|           </span>
 | |
|           <NextButton
 | |
|             v-if="showActions && hasValue"
 | |
|             v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
 | |
|             slate
 | |
|             sm
 | |
|             link
 | |
|             icon="i-lucide-trash-2"
 | |
|             @click="onDelete"
 | |
|           />
 | |
|         </div>
 | |
|       </h4>
 | |
|     </div>
 | |
|     <div v-if="notAttributeTypeCheckboxAndList">
 | |
|       <div v-if="isEditing" v-on-clickaway="onClickAway">
 | |
|         <div class="flex items-center w-full mb-2">
 | |
|           <input
 | |
|             ref="inputfield"
 | |
|             v-model="editedValue"
 | |
|             :type="inputType"
 | |
|             class="!h-8 ltr:!rounded-r-none rtl:!rounded-l-none !mb-0 !text-sm"
 | |
|             autofocus="true"
 | |
|             :class="{ error: v$.editedValue.$error }"
 | |
|             @blur="v$.editedValue.$touch"
 | |
|             @keyup.enter="onUpdate"
 | |
|           />
 | |
|           <div>
 | |
|             <NextButton
 | |
|               sm
 | |
|               icon="i-lucide-check"
 | |
|               class="ltr:rounded-l-none rtl:rounded-r-none h-[34px]"
 | |
|               @click="onUpdate"
 | |
|             />
 | |
|           </div>
 | |
|         </div>
 | |
|         <span
 | |
|           v-if="shouldShowErrorMessage"
 | |
|           class="block w-full -mt-px text-sm font-normal text-n-ruby-11"
 | |
|         >
 | |
|           {{ errorMessage }}
 | |
|         </span>
 | |
|       </div>
 | |
|       <div
 | |
|         v-show="!isEditing"
 | |
|         class="flex group"
 | |
|         :class="{ 'is-editable': showActions }"
 | |
|       >
 | |
|         <a
 | |
|           v-if="isAttributeTypeLink"
 | |
|           :href="hrefURL"
 | |
|           target="_blank"
 | |
|           rel="noopener noreferrer"
 | |
|           class="group-hover:bg-n-slate-3 group-hover:dark:bg-n-solid-3 inline-block rounded-sm mb-0 break-all py-0.5 px-1"
 | |
|         >
 | |
|           {{ urlValue }}
 | |
|         </a>
 | |
|         <p
 | |
|           v-else
 | |
|           class="group-hover:bg-n-slate-3 group-hover:dark:bg-n-solid-3 inline-block rounded-sm mb-0 break-all py-0.5 px-1"
 | |
|         >
 | |
|           {{ displayValue }}
 | |
|         </p>
 | |
|         <div
 | |
|           class="flex items-center max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0"
 | |
|         >
 | |
|           <NextButton
 | |
|             v-if="showActions && hasValue"
 | |
|             v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
 | |
|             xs
 | |
|             slate
 | |
|             ghost
 | |
|             icon="i-lucide-clipboard"
 | |
|             class="hidden group-hover:flex flex-shrink-0"
 | |
|             @click="onCopy"
 | |
|           />
 | |
|           <NextButton
 | |
|             v-if="showActions"
 | |
|             v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
 | |
|             xs
 | |
|             slate
 | |
|             ghost
 | |
|             icon="i-lucide-pen"
 | |
|             class="hidden group-hover:flex flex-shrink-0"
 | |
|             @click="onEdit"
 | |
|           />
 | |
|         </div>
 | |
|       </div>
 | |
|     </div>
 | |
|     <div v-if="isAttributeTypeList">
 | |
|       <MultiselectDropdown
 | |
|         :options="listOptions"
 | |
|         :selected-item="selectedItem"
 | |
|         :has-thumbnail="false"
 | |
|         :multiselector-placeholder="
 | |
|           $t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.PLACEHOLDER')
 | |
|         "
 | |
|         :no-search-result="
 | |
|           $t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.NO_RESULT')
 | |
|         "
 | |
|         :input-placeholder="
 | |
|           $t(
 | |
|             'CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.SEARCH_INPUT_PLACEHOLDER'
 | |
|           )
 | |
|         "
 | |
|         @select="onUpdateListValue"
 | |
|       />
 | |
|     </div>
 | |
|   </div>
 | |
| </template>
 | |
| 
 | |
| <style lang="scss" scoped>
 | |
| ::v-deep {
 | |
|   .selector-wrap {
 | |
|     @apply m-0 top-1;
 | |
| 
 | |
|     .selector-name {
 | |
|       @apply ml-0;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   .name {
 | |
|     @apply ml-0;
 | |
|   }
 | |
| }
 | |
| </style>
 |