mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-04 13:07:55 +00:00 
			
		
		
		
	# 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>
 |