mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-03 20:48:07 +00:00 
			
		
		
		
	# Pull Request Template ### Changes includes * Update send message and add note button <img width="151" alt="image" src="https://github.com/user-attachments/assets/646d1d22-07be-4f2f-9090-6642556aa761" /> <img width="151" alt="image" src="https://github.com/user-attachments/assets/b5651420-c48b-4932-aff9-911788b9eabc" /> <img width="165" alt="image" src="https://github.com/user-attachments/assets/f1ace171-2115-4eba-9055-e568d1b73c5e" /> <img width="165" alt="image" src="https://github.com/user-attachments/assets/0c818461-ccdb-46ab-b7d3-3917e4ee4e74" /> <img width="165" alt="image" src="https://github.com/user-attachments/assets/eef1545b-1bed-47a1-8b83-ed5d5da3f24f" /> <img width="165" alt="image" src="https://github.com/user-attachments/assets/429de6e6-1263-4216-9222-4eaece95da81" /> <img width="165" alt="image" src="https://github.com/user-attachments/assets/59a69867-4da1-4695-b88c-329142a693a8" /> <img width="165" alt="image" src="https://github.com/user-attachments/assets/7114745c-836f-4c5a-b5d6-e200e1343a73" /> * Remove Unused component `AnnouncementPopup.vue` * Updated button for custom attributes in conversation sidebar. <img width="225" alt="image" src="https://github.com/user-attachments/assets/a71f6c31-aca9-4e1b-bf63-6b9d5ed183c8" /> <img width="310" alt="image" src="https://github.com/user-attachments/assets/8d847e1b-4a13-4108-a487-ce3d36257afa" /> * Update button in custom snooze modal buttons <img width="207" alt="image" src="https://github.com/user-attachments/assets/78315ce6-9734-467b-a4d3-e753d3eca384" /> * Update modal component close button <img width="80" alt="image" src="https://github.com/user-attachments/assets/643e9ef0-b781-47ce-a66b-a9ee4760c952" /> * Update AI assistant modal and AICTA modal <img width="319" alt="image" src="https://github.com/user-attachments/assets/8d0986ec-ec7a-4abb-9327-f73df8b4d942" /> <img width="565" alt="image" src="https://github.com/user-attachments/assets/1e02ddd1-7f51-4d8a-bb57-558b9a50c938" /> * Update remove attachment button <img width="301" alt="image" src="https://github.com/user-attachments/assets/90c93eee-0b4d-4839-9db5-edc4b023df4b" /> * Update the conversation header buttons <img width="256" alt="image" src="https://github.com/user-attachments/assets/abac5d7e-dd83-40ae-b548-76bbafaa2231" /> * Update the retry button in old message bubbles. --------- Co-authored-by: Pranav <pranav@chatwoot.com>
		
			
				
	
	
		
			349 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			349 lines
		
	
	
		
			9.4 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.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';
 | 
						|
    },
 | 
						|
    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 && value"
 | 
						|
            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 && value"
 | 
						|
            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>
 |