fix(shangwutong): 完善访客资料同步
This commit is contained in:
+463
-415
@@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import {
|
||||
DuplicateContactException,
|
||||
ExceptionWithMessage,
|
||||
DuplicateContactException,
|
||||
ExceptionWithMessage,
|
||||
} from 'shared/helpers/CustomErrors';
|
||||
import { required, email } from '@vuelidate/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
@@ -14,424 +14,472 @@ import Avatar from 'next/avatar/Avatar.vue';
|
||||
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
Avatar,
|
||||
ComboBox,
|
||||
},
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
inProgress: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onSubmit: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
emits: ['cancel', 'success'],
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
countries: countries,
|
||||
companyName: '',
|
||||
description: '',
|
||||
email: '',
|
||||
name: '',
|
||||
phoneNumber: '',
|
||||
activeDialCode: '',
|
||||
avatarFile: null,
|
||||
avatarUrl: '',
|
||||
country: {
|
||||
id: '',
|
||||
name: '',
|
||||
},
|
||||
city: '',
|
||||
socialProfileUserNames: {
|
||||
facebook: '',
|
||||
twitter: '',
|
||||
linkedin: '',
|
||||
github: '',
|
||||
telegram: '',
|
||||
},
|
||||
socialProfileKeys: [
|
||||
{ key: 'facebook', prefixURL: 'https://facebook.com/' },
|
||||
{ key: 'twitter', prefixURL: 'https://twitter.com/' },
|
||||
{ key: 'linkedin', prefixURL: 'https://linkedin.com/' },
|
||||
{ key: 'github', prefixURL: 'https://github.com/' },
|
||||
{ key: 'telegram', prefixURL: 'https://t.me/' },
|
||||
{ key: 'tiktok', prefixURL: 'https://tiktok.com/@' },
|
||||
],
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
name: {
|
||||
required,
|
||||
},
|
||||
description: {},
|
||||
email: {
|
||||
email,
|
||||
},
|
||||
companyName: {},
|
||||
phoneNumber: {},
|
||||
bio: {},
|
||||
},
|
||||
computed: {
|
||||
parsePhoneNumber() {
|
||||
return parsePhoneNumber(this.phoneNumber);
|
||||
},
|
||||
isPhoneNumberNotValid() {
|
||||
if (this.phoneNumber !== '') {
|
||||
return (
|
||||
!isPhoneNumberValid(this.phoneNumber, this.activeDialCode) ||
|
||||
(this.phoneNumber !== '' ? this.activeDialCode === '' : false)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
phoneNumberError() {
|
||||
if (this.activeDialCode === '') {
|
||||
return this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DIAL_CODE_ERROR');
|
||||
}
|
||||
if (!isPhoneNumberValid(this.phoneNumber, this.activeDialCode)) {
|
||||
return this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.ERROR');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
setPhoneNumber() {
|
||||
if (this.parsePhoneNumber && this.parsePhoneNumber.countryCallingCode) {
|
||||
return this.phoneNumber;
|
||||
}
|
||||
if (this.phoneNumber === '' && this.activeDialCode !== '') {
|
||||
return '';
|
||||
}
|
||||
return this.activeDialCode
|
||||
? `${this.activeDialCode}${this.phoneNumber}`
|
||||
: '';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
contact() {
|
||||
this.setContactObject();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setContactObject();
|
||||
this.setDialCode();
|
||||
},
|
||||
methods: {
|
||||
onCancel() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
onSuccess() {
|
||||
this.$emit('success');
|
||||
},
|
||||
countryNameWithCode({ name, id }) {
|
||||
if (!id) return name;
|
||||
if (!name && !id) return '';
|
||||
return `${name} (${id})`;
|
||||
},
|
||||
onCountryChange(value) {
|
||||
const selected = this.countries.find(c => c.id === value);
|
||||
this.country = selected
|
||||
? { id: selected.id, name: selected.name }
|
||||
: { id: '', name: '' };
|
||||
},
|
||||
setDialCode() {
|
||||
if (
|
||||
this.phoneNumber !== '' &&
|
||||
this.parsePhoneNumber &&
|
||||
this.parsePhoneNumber.countryCallingCode
|
||||
) {
|
||||
const dialCode = this.parsePhoneNumber.countryCallingCode;
|
||||
this.activeDialCode = `+${dialCode}`;
|
||||
}
|
||||
},
|
||||
setContactObject() {
|
||||
const {
|
||||
email: emailAddress,
|
||||
phone_number: phoneNumber,
|
||||
name,
|
||||
} = this.contact;
|
||||
const additionalAttributes = this.contact.additional_attributes || {};
|
||||
components: {
|
||||
NextButton,
|
||||
Avatar,
|
||||
ComboBox,
|
||||
},
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
inProgress: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onSubmit: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
emits: ['cancel', 'success'],
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
countries: countries,
|
||||
companyName: '',
|
||||
description: '',
|
||||
email: '',
|
||||
name: '',
|
||||
phoneNumber: '',
|
||||
activeDialCode: '',
|
||||
avatarFile: null,
|
||||
avatarUrl: '',
|
||||
country: {
|
||||
id: '',
|
||||
name: '',
|
||||
},
|
||||
city: '',
|
||||
socialProfileUserNames: {
|
||||
facebook: '',
|
||||
twitter: '',
|
||||
linkedin: '',
|
||||
github: '',
|
||||
telegram: '',
|
||||
},
|
||||
socialProfileKeys: [
|
||||
{ key: 'facebook', prefixURL: 'https://facebook.com/' },
|
||||
{ key: 'twitter', prefixURL: 'https://twitter.com/' },
|
||||
{ key: 'linkedin', prefixURL: 'https://linkedin.com/' },
|
||||
{ key: 'github', prefixURL: 'https://github.com/' },
|
||||
{ key: 'telegram', prefixURL: 'https://t.me/' },
|
||||
{ key: 'tiktok', prefixURL: 'https://tiktok.com/@' },
|
||||
],
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
name: {
|
||||
required,
|
||||
},
|
||||
description: {},
|
||||
email: {
|
||||
email,
|
||||
},
|
||||
companyName: {},
|
||||
phoneNumber: {},
|
||||
bio: {},
|
||||
},
|
||||
computed: {
|
||||
parsePhoneNumber() {
|
||||
return parsePhoneNumber(this.phoneNumber);
|
||||
},
|
||||
isPhoneNumberNotValid() {
|
||||
if (this.phoneNumber !== '') {
|
||||
return (
|
||||
!isPhoneNumberValid(
|
||||
this.phoneNumber,
|
||||
this.activeDialCode
|
||||
) ||
|
||||
(this.phoneNumber !== ''
|
||||
? this.activeDialCode === ''
|
||||
: false)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
phoneNumberError() {
|
||||
if (this.activeDialCode === '') {
|
||||
return this.$t(
|
||||
'CONTACT_FORM.FORM.PHONE_NUMBER.DIAL_CODE_ERROR'
|
||||
);
|
||||
}
|
||||
if (!isPhoneNumberValid(this.phoneNumber, this.activeDialCode)) {
|
||||
return this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.ERROR');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
setPhoneNumber() {
|
||||
if (
|
||||
this.parsePhoneNumber &&
|
||||
this.parsePhoneNumber.countryCallingCode
|
||||
) {
|
||||
return this.phoneNumber;
|
||||
}
|
||||
if (this.phoneNumber === '' && this.activeDialCode !== '') {
|
||||
return '';
|
||||
}
|
||||
return this.activeDialCode
|
||||
? `${this.activeDialCode}${this.phoneNumber}`
|
||||
: '';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
contact() {
|
||||
this.setContactObject();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setContactObject();
|
||||
this.setDialCode();
|
||||
},
|
||||
methods: {
|
||||
onCancel() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
onSuccess() {
|
||||
this.$emit('success');
|
||||
},
|
||||
countryNameWithCode({ name, id }) {
|
||||
if (!id) return name;
|
||||
if (!name && !id) return '';
|
||||
return `${name} (${id})`;
|
||||
},
|
||||
onCountryChange(value) {
|
||||
const selected = this.countries.find(c => c.id === value);
|
||||
this.country = selected
|
||||
? { id: selected.id, name: selected.name }
|
||||
: { id: '', name: '' };
|
||||
this.setPhoneCode(selected?.dial_code || '');
|
||||
},
|
||||
setDialCode() {
|
||||
if (
|
||||
this.phoneNumber !== '' &&
|
||||
this.parsePhoneNumber &&
|
||||
this.parsePhoneNumber.countryCallingCode
|
||||
) {
|
||||
const dialCode = this.parsePhoneNumber.countryCallingCode;
|
||||
this.activeDialCode = `+${dialCode}`;
|
||||
}
|
||||
},
|
||||
setContactObject() {
|
||||
const {
|
||||
email: emailAddress,
|
||||
phone_number: phoneNumber,
|
||||
name,
|
||||
} = this.contact;
|
||||
const additionalAttributes =
|
||||
this.contact.additional_attributes || {};
|
||||
|
||||
this.name = name || '';
|
||||
this.email = emailAddress || '';
|
||||
this.phoneNumber = phoneNumber || '';
|
||||
this.companyName = additionalAttributes.company_name || '';
|
||||
this.country = {
|
||||
id: additionalAttributes.country_code || '',
|
||||
name:
|
||||
additionalAttributes.country ||
|
||||
this.$t('CONTACT_FORM.FORM.COUNTRY.SELECT_COUNTRY'),
|
||||
};
|
||||
this.city = additionalAttributes.city || '';
|
||||
this.description = additionalAttributes.description || '';
|
||||
this.avatarUrl = this.contact.thumbnail || '';
|
||||
const {
|
||||
social_profiles: socialProfiles = {},
|
||||
screen_name: twitterScreenName,
|
||||
social_telegram_user_name: telegramUserName,
|
||||
} = additionalAttributes;
|
||||
this.socialProfileUserNames = {
|
||||
twitter: socialProfiles.twitter || twitterScreenName || '',
|
||||
facebook: socialProfiles.facebook || '',
|
||||
linkedin: socialProfiles.linkedin || '',
|
||||
github: socialProfiles.github || '',
|
||||
telegram: socialProfiles.telegram || telegramUserName || '',
|
||||
instagram: socialProfiles.instagram || '',
|
||||
tiktok: socialProfiles.tiktok || '',
|
||||
};
|
||||
},
|
||||
getContactObject() {
|
||||
if (this.country === null) {
|
||||
this.country = {
|
||||
id: '',
|
||||
name: '',
|
||||
};
|
||||
}
|
||||
const contactObject = {
|
||||
id: this.contact.id,
|
||||
name: this.name,
|
||||
email: this.email,
|
||||
phone_number: this.setPhoneNumber,
|
||||
additional_attributes: {
|
||||
...this.contact.additional_attributes,
|
||||
description: this.description,
|
||||
company_name: this.companyName,
|
||||
country_code: this.country.id,
|
||||
country:
|
||||
this.country.name ===
|
||||
this.$t('CONTACT_FORM.FORM.COUNTRY.SELECT_COUNTRY')
|
||||
? ''
|
||||
: this.country.name,
|
||||
city: this.city,
|
||||
social_profiles: this.socialProfileUserNames,
|
||||
},
|
||||
};
|
||||
if (this.avatarFile) {
|
||||
contactObject.avatar = this.avatarFile;
|
||||
contactObject.isFormData = true;
|
||||
}
|
||||
return contactObject;
|
||||
},
|
||||
setPhoneCode(code) {
|
||||
if (this.phoneNumber !== '' && this.parsePhoneNumber) {
|
||||
const dialCode = this.parsePhoneNumber.countryCallingCode;
|
||||
if (dialCode === code) {
|
||||
return;
|
||||
}
|
||||
this.activeDialCode = `+${dialCode}`;
|
||||
const newPhoneNumber = this.phoneNumber.replace(
|
||||
`+${dialCode}`,
|
||||
`${code}`
|
||||
);
|
||||
this.phoneNumber = newPhoneNumber;
|
||||
} else {
|
||||
this.activeDialCode = code;
|
||||
}
|
||||
},
|
||||
async handleSubmit() {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid || this.isPhoneNumberNotValid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.onSubmit(this.getContactObject());
|
||||
this.onSuccess();
|
||||
useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
if (error instanceof DuplicateContactException) {
|
||||
if (error.data.includes('email')) {
|
||||
useAlert(this.$t('CONTACT_FORM.FORM.EMAIL_ADDRESS.DUPLICATE'));
|
||||
} else if (error.data.includes('phone_number')) {
|
||||
useAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
|
||||
}
|
||||
} else if (error instanceof ExceptionWithMessage) {
|
||||
useAlert(error.data);
|
||||
} else {
|
||||
useAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
},
|
||||
handleImageUpload({ file, url }) {
|
||||
this.avatarFile = file;
|
||||
this.avatarUrl = url;
|
||||
},
|
||||
async handleAvatarDelete() {
|
||||
try {
|
||||
if (this.contact && this.contact.id) {
|
||||
await this.$store.dispatch('contacts/deleteAvatar', this.contact.id);
|
||||
useAlert(this.$t('CONTACT_FORM.DELETE_AVATAR.API.SUCCESS_MESSAGE'));
|
||||
}
|
||||
this.avatarFile = null;
|
||||
this.avatarUrl = '';
|
||||
this.activeDialCode = '';
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error.message
|
||||
? error.message
|
||||
: this.$t('CONTACT_FORM.DELETE_AVATAR.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
this.name = name || '';
|
||||
this.email = emailAddress || '';
|
||||
this.phoneNumber = phoneNumber || '';
|
||||
this.companyName = additionalAttributes.company_name || '';
|
||||
this.country = {
|
||||
id:
|
||||
additionalAttributes.country_code ||
|
||||
this.contact.country_code ||
|
||||
'',
|
||||
name:
|
||||
additionalAttributes.country ||
|
||||
this.countries.find(
|
||||
country =>
|
||||
country.id ===
|
||||
(additionalAttributes.country_code ||
|
||||
this.contact.country_code)
|
||||
)?.name ||
|
||||
this.$t('CONTACT_FORM.FORM.COUNTRY.SELECT_COUNTRY'),
|
||||
};
|
||||
if (!this.phoneNumber && this.country.id) {
|
||||
this.activeDialCode =
|
||||
this.countries.find(
|
||||
country => country.id === this.country.id
|
||||
)?.dial_code || '';
|
||||
}
|
||||
this.city = additionalAttributes.city || '';
|
||||
this.description = additionalAttributes.description || '';
|
||||
this.avatarUrl = this.contact.thumbnail || '';
|
||||
const {
|
||||
social_profiles: socialProfiles = {},
|
||||
screen_name: twitterScreenName,
|
||||
social_telegram_user_name: telegramUserName,
|
||||
} = additionalAttributes;
|
||||
this.socialProfileUserNames = {
|
||||
twitter: socialProfiles.twitter || twitterScreenName || '',
|
||||
facebook: socialProfiles.facebook || '',
|
||||
linkedin: socialProfiles.linkedin || '',
|
||||
github: socialProfiles.github || '',
|
||||
telegram: socialProfiles.telegram || telegramUserName || '',
|
||||
instagram: socialProfiles.instagram || '',
|
||||
tiktok: socialProfiles.tiktok || '',
|
||||
};
|
||||
},
|
||||
getContactObject() {
|
||||
if (this.country === null) {
|
||||
this.country = {
|
||||
id: '',
|
||||
name: '',
|
||||
};
|
||||
}
|
||||
const contactObject = {
|
||||
id: this.contact.id,
|
||||
name: this.name,
|
||||
email: this.email,
|
||||
phone_number: this.setPhoneNumber,
|
||||
additional_attributes: {
|
||||
...this.contact.additional_attributes,
|
||||
description: this.description,
|
||||
company_name: this.companyName,
|
||||
country_code: this.country.id,
|
||||
country:
|
||||
this.country.name ===
|
||||
this.$t('CONTACT_FORM.FORM.COUNTRY.SELECT_COUNTRY')
|
||||
? ''
|
||||
: this.country.name,
|
||||
city: this.city,
|
||||
social_profiles: this.socialProfileUserNames,
|
||||
},
|
||||
};
|
||||
if (this.avatarFile) {
|
||||
contactObject.avatar = this.avatarFile;
|
||||
contactObject.isFormData = true;
|
||||
}
|
||||
return contactObject;
|
||||
},
|
||||
setPhoneCode(code) {
|
||||
if (this.phoneNumber !== '' && this.parsePhoneNumber) {
|
||||
const dialCode = this.parsePhoneNumber.countryCallingCode;
|
||||
if (dialCode === code) {
|
||||
return;
|
||||
}
|
||||
this.activeDialCode = `+${dialCode}`;
|
||||
const newPhoneNumber = this.phoneNumber.replace(
|
||||
`+${dialCode}`,
|
||||
`${code}`
|
||||
);
|
||||
this.phoneNumber = newPhoneNumber;
|
||||
} else {
|
||||
this.activeDialCode = code;
|
||||
}
|
||||
},
|
||||
async handleSubmit() {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid || this.isPhoneNumberNotValid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.onSubmit(this.getContactObject());
|
||||
this.onSuccess();
|
||||
useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
if (error instanceof DuplicateContactException) {
|
||||
if (error.data.includes('email')) {
|
||||
useAlert(
|
||||
this.$t('CONTACT_FORM.FORM.EMAIL_ADDRESS.DUPLICATE')
|
||||
);
|
||||
} else if (error.data.includes('phone_number')) {
|
||||
useAlert(
|
||||
this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE')
|
||||
);
|
||||
}
|
||||
} else if (error instanceof ExceptionWithMessage) {
|
||||
useAlert(error.data);
|
||||
} else {
|
||||
useAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
},
|
||||
handleImageUpload({ file, url }) {
|
||||
this.avatarFile = file;
|
||||
this.avatarUrl = url;
|
||||
},
|
||||
async handleAvatarDelete() {
|
||||
try {
|
||||
if (this.contact && this.contact.id) {
|
||||
await this.$store.dispatch(
|
||||
'contacts/deleteAvatar',
|
||||
this.contact.id
|
||||
);
|
||||
useAlert(
|
||||
this.$t(
|
||||
'CONTACT_FORM.DELETE_AVATAR.API.SUCCESS_MESSAGE'
|
||||
)
|
||||
);
|
||||
}
|
||||
this.avatarFile = null;
|
||||
this.avatarUrl = '';
|
||||
this.activeDialCode = '';
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error.message
|
||||
? error.message
|
||||
: this.$t(
|
||||
'CONTACT_FORM.DELETE_AVATAR.API.ERROR_MESSAGE'
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
class="w-full px-8 pt-6 pb-8 contact--form"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<div class="flex flex-col mb-4 items-start gap-1 w-full">
|
||||
<label class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||
{{ $t('CONTACT_FORM.FORM.AVATAR.LABEL') }}
|
||||
</label>
|
||||
<Avatar
|
||||
:src="avatarUrl"
|
||||
:size="72"
|
||||
:name="contact.name"
|
||||
allow-upload
|
||||
rounded-full
|
||||
@upload="handleImageUpload"
|
||||
@delete="handleAvatarDelete"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.name.$error }">
|
||||
{{ $t('CONTACT_FORM.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model="name"
|
||||
type="text"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.NAME.PLACEHOLDER')"
|
||||
@input="v$.name.$touch"
|
||||
/>
|
||||
</label>
|
||||
<form
|
||||
class="w-full px-8 pt-6 pb-8 contact--form"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<div class="flex flex-col mb-4 items-start gap-1 w-full">
|
||||
<label class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||
{{ $t('CONTACT_FORM.FORM.AVATAR.LABEL') }}
|
||||
</label>
|
||||
<Avatar
|
||||
:src="avatarUrl"
|
||||
:size="72"
|
||||
:name="contact.name"
|
||||
allow-upload
|
||||
rounded-full
|
||||
@upload="handleImageUpload"
|
||||
@delete="handleAvatarDelete"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.name.$error }">
|
||||
{{ $t('CONTACT_FORM.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model="name"
|
||||
type="text"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.NAME.PLACEHOLDER')"
|
||||
@input="v$.name.$touch"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label :class="{ error: v$.email.$error }">
|
||||
{{ $t('CONTACT_FORM.FORM.EMAIL_ADDRESS.LABEL') }}
|
||||
<input
|
||||
v-model="email"
|
||||
type="text"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.EMAIL_ADDRESS.PLACEHOLDER')"
|
||||
@input="v$.email.$touch"
|
||||
/>
|
||||
<span v-if="v$.email.$error" class="message">
|
||||
{{ $t('CONTACT_FORM.FORM.EMAIL_ADDRESS.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.description.$error }">
|
||||
{{ $t('CONTACT_FORM.FORM.BIO.LABEL') }}
|
||||
<textarea
|
||||
v-model="description"
|
||||
type="text"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.BIO.PLACEHOLDER')"
|
||||
@input="v$.description.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<label
|
||||
:class="{
|
||||
error: isPhoneNumberNotValid,
|
||||
}"
|
||||
>
|
||||
{{ $t('CONTACT_FORM.FORM.PHONE_NUMBER.LABEL') }}
|
||||
<woot-phone-input
|
||||
v-model="phoneNumber"
|
||||
:value="phoneNumber"
|
||||
:error="isPhoneNumberNotValid"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.PHONE_NUMBER.PLACEHOLDER')"
|
||||
@blur="v$.phoneNumber.$touch"
|
||||
@set-code="setPhoneCode"
|
||||
/>
|
||||
<span v-if="isPhoneNumberNotValid" class="message">
|
||||
{{ phoneNumberError }}
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
v-if="isPhoneNumberNotValid || !phoneNumber"
|
||||
class="relative mx-0 mt-0 mb-2.5 p-2 rounded-md text-sm border border-solid border-n-amber-5 text-n-amber-12 bg-n-amber-3"
|
||||
>
|
||||
{{ $t('CONTACT_FORM.FORM.PHONE_NUMBER.HELP') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<woot-input
|
||||
v-model="companyName"
|
||||
class="w-full"
|
||||
:label="$t('CONTACT_FORM.FORM.COMPANY_NAME.LABEL')"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.COMPANY_NAME.PLACEHOLDER')"
|
||||
/>
|
||||
<div class="w-full mb-4">
|
||||
<label>
|
||||
{{ $t('CONTACT_FORM.FORM.COUNTRY.LABEL') }}
|
||||
</label>
|
||||
<ComboBox
|
||||
:model-value="country.id"
|
||||
:options="
|
||||
countries.map(c => ({
|
||||
value: c.id,
|
||||
label: countryNameWithCode(c),
|
||||
}))
|
||||
"
|
||||
class="[&>div>button]:!bg-n-alpha-black2"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.COUNTRY.PLACEHOLDER')"
|
||||
:search-placeholder="$t('CONTACT_FORM.FORM.COUNTRY.SELECT_PLACEHOLDER')"
|
||||
@update:model-value="onCountryChange"
|
||||
/>
|
||||
</div>
|
||||
<woot-input
|
||||
v-model="city"
|
||||
class="w-full"
|
||||
:label="$t('CONTACT_FORM.FORM.CITY.LABEL')"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.CITY.PLACEHOLDER')"
|
||||
/>
|
||||
<label :class="{ error: v$.email.$error }">
|
||||
{{ $t('CONTACT_FORM.FORM.EMAIL_ADDRESS.LABEL') }}
|
||||
<input
|
||||
v-model="email"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('CONTACT_FORM.FORM.EMAIL_ADDRESS.PLACEHOLDER')
|
||||
"
|
||||
@input="v$.email.$touch"
|
||||
/>
|
||||
<span v-if="v$.email.$error" class="message">
|
||||
{{ $t('CONTACT_FORM.FORM.EMAIL_ADDRESS.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.description.$error }">
|
||||
{{ $t('CONTACT_FORM.FORM.BIO.LABEL') }}
|
||||
<textarea
|
||||
v-model="description"
|
||||
type="text"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.BIO.PLACEHOLDER')"
|
||||
@input="v$.description.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<label
|
||||
:class="{
|
||||
error: isPhoneNumberNotValid,
|
||||
}"
|
||||
>
|
||||
{{ $t('CONTACT_FORM.FORM.PHONE_NUMBER.LABEL') }}
|
||||
<woot-phone-input
|
||||
v-model="phoneNumber"
|
||||
:value="phoneNumber"
|
||||
:error="isPhoneNumberNotValid"
|
||||
:placeholder="
|
||||
$t('CONTACT_FORM.FORM.PHONE_NUMBER.PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.phoneNumber.$touch"
|
||||
@set-code="setPhoneCode"
|
||||
/>
|
||||
<span v-if="isPhoneNumberNotValid" class="message">
|
||||
{{ phoneNumberError }}
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
v-if="isPhoneNumberNotValid || !phoneNumber"
|
||||
class="relative mx-0 mt-0 mb-2.5 p-2 rounded-md text-sm border border-solid border-n-amber-5 text-n-amber-12 bg-n-amber-3"
|
||||
>
|
||||
{{ $t('CONTACT_FORM.FORM.PHONE_NUMBER.HELP') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<woot-input
|
||||
v-model="companyName"
|
||||
class="w-full"
|
||||
:label="$t('CONTACT_FORM.FORM.COMPANY_NAME.LABEL')"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.COMPANY_NAME.PLACEHOLDER')"
|
||||
/>
|
||||
<div class="w-full mb-4">
|
||||
<label>
|
||||
{{ $t('CONTACT_FORM.FORM.COUNTRY.LABEL') }}
|
||||
</label>
|
||||
<ComboBox
|
||||
:model-value="country.id"
|
||||
:options="
|
||||
countries.map(c => ({
|
||||
value: c.id,
|
||||
label: countryNameWithCode(c),
|
||||
}))
|
||||
"
|
||||
class="[&>div>button]:!bg-n-alpha-black2"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.COUNTRY.PLACEHOLDER')"
|
||||
:search-placeholder="
|
||||
$t('CONTACT_FORM.FORM.COUNTRY.SELECT_PLACEHOLDER')
|
||||
"
|
||||
@update:model-value="onCountryChange"
|
||||
/>
|
||||
</div>
|
||||
<woot-input
|
||||
v-model="city"
|
||||
class="w-full"
|
||||
:label="$t('CONTACT_FORM.FORM.CITY.LABEL')"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.CITY.PLACEHOLDER')"
|
||||
/>
|
||||
|
||||
<div class="w-full">
|
||||
<label>{{ $t('CONTACTS_PAGE.LIST.TABLE_HEADER.SOCIAL_PROFILES') }}</label>
|
||||
<div
|
||||
v-for="socialProfile in socialProfileKeys"
|
||||
:key="socialProfile.key"
|
||||
class="flex items-stretch w-full mb-4"
|
||||
>
|
||||
<span
|
||||
class="flex items-center h-10 px-2 text-sm border-solid border-y ltr:border-l rtl:border-r ltr:rounded-l-md rtl:rounded-r-md bg-n-solid-3 text-n-slate-11 border-n-weak"
|
||||
>
|
||||
{{ socialProfile.prefixURL }}
|
||||
</span>
|
||||
<input
|
||||
v-model="socialProfileUserNames[socialProfile.key]"
|
||||
class="input-group-field ltr:!rounded-l-none rtl:!rounded-r-none !mb-0"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-start w-full gap-2 px-0 py-2">
|
||||
<NextButton
|
||||
type="submit"
|
||||
:label="$t('CONTACT_FORM.FORM.SUBMIT')"
|
||||
:is-loading="inProgress"
|
||||
/>
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('CONTACT_FORM.FORM.CANCEL')"
|
||||
@click.prevent="onCancel"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div class="w-full">
|
||||
<label>{{
|
||||
$t('CONTACTS_PAGE.LIST.TABLE_HEADER.SOCIAL_PROFILES')
|
||||
}}</label>
|
||||
<div
|
||||
v-for="socialProfile in socialProfileKeys"
|
||||
:key="socialProfile.key"
|
||||
class="flex items-stretch w-full mb-4"
|
||||
>
|
||||
<span
|
||||
class="flex items-center h-10 px-2 text-sm border-solid border-y ltr:border-l rtl:border-r ltr:rounded-l-md rtl:rounded-r-md bg-n-solid-3 text-n-slate-11 border-n-weak"
|
||||
>
|
||||
{{ socialProfile.prefixURL }}
|
||||
</span>
|
||||
<input
|
||||
v-model="socialProfileUserNames[socialProfile.key]"
|
||||
class="input-group-field ltr:!rounded-l-none rtl:!rounded-r-none !mb-0"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-start w-full gap-2 px-0 py-2">
|
||||
<NextButton
|
||||
type="submit"
|
||||
:label="$t('CONTACT_FORM.FORM.SUBMIT')"
|
||||
:is-loading="inProgress"
|
||||
/>
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('CONTACT_FORM.FORM.CANCEL')"
|
||||
@click.prevent="onCancel"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
+391
-329
@@ -2,8 +2,8 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import {
|
||||
DuplicateContactException,
|
||||
ExceptionWithMessage,
|
||||
DuplicateContactException,
|
||||
ExceptionWithMessage,
|
||||
} from 'shared/helpers/CustomErrors';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
@@ -19,341 +19,403 @@ import VoiceCallButton from 'dashboard/components-next/Contacts/VoiceCallButton.
|
||||
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
ContactInfoRow,
|
||||
EditContact,
|
||||
Avatar,
|
||||
ComposeConversation,
|
||||
SocialIcons,
|
||||
ContactMergeModal,
|
||||
ContactDeleteModal,
|
||||
VoiceCallButton,
|
||||
InlineInput,
|
||||
},
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
showAvatar: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['panelClose'],
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
return {
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showEditModal: false,
|
||||
isEditingName: false,
|
||||
editName: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'contacts/getUIFlags' }),
|
||||
contactProfileLink() {
|
||||
return `/app/accounts/${this.$route.params.accountId}/contacts/${this.contact.id}`;
|
||||
},
|
||||
additionalAttributes() {
|
||||
return this.contact.additional_attributes || {};
|
||||
},
|
||||
location() {
|
||||
const {
|
||||
country = '',
|
||||
city = '',
|
||||
country_code: countryCode,
|
||||
} = this.additionalAttributes;
|
||||
const cityAndCountry = [city, country].filter(item => !!item).join(', ');
|
||||
components: {
|
||||
NextButton,
|
||||
ContactInfoRow,
|
||||
EditContact,
|
||||
Avatar,
|
||||
ComposeConversation,
|
||||
SocialIcons,
|
||||
ContactMergeModal,
|
||||
ContactDeleteModal,
|
||||
VoiceCallButton,
|
||||
InlineInput,
|
||||
},
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
showAvatar: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['panelClose'],
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
return {
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showEditModal: false,
|
||||
isEditingName: false,
|
||||
editName: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'contacts/getUIFlags' }),
|
||||
contactProfileLink() {
|
||||
return `/app/accounts/${this.$route.params.accountId}/contacts/${this.contact.id}`;
|
||||
},
|
||||
additionalAttributes() {
|
||||
return this.contact.additional_attributes || {};
|
||||
},
|
||||
quickPhonePrefix() {
|
||||
const countryCode = (
|
||||
this.additionalAttributes.country_code ||
|
||||
this.contact.country_code ||
|
||||
''
|
||||
).toUpperCase();
|
||||
const country = this.additionalAttributes.country || '';
|
||||
return countryCode === 'CN' || country === 'China' ? '+86' : '';
|
||||
},
|
||||
location() {
|
||||
const {
|
||||
country = '',
|
||||
city = '',
|
||||
country_code: countryCode,
|
||||
} = this.additionalAttributes;
|
||||
const cityAndCountry = [city, country]
|
||||
.filter(item => !!item)
|
||||
.join(', ');
|
||||
|
||||
if (!cityAndCountry) {
|
||||
return '';
|
||||
}
|
||||
return this.findCountryFlag(countryCode, cityAndCountry);
|
||||
},
|
||||
socialProfiles() {
|
||||
const {
|
||||
social_profiles: socialProfiles,
|
||||
screen_name: twitterScreenName,
|
||||
social_telegram_user_name: telegramUsername,
|
||||
} = this.additionalAttributes;
|
||||
if (!cityAndCountry) {
|
||||
return '';
|
||||
}
|
||||
return this.findCountryFlag(countryCode, cityAndCountry);
|
||||
},
|
||||
socialProfiles() {
|
||||
const {
|
||||
social_profiles: socialProfiles,
|
||||
screen_name: twitterScreenName,
|
||||
social_telegram_user_name: telegramUsername,
|
||||
} = this.additionalAttributes;
|
||||
|
||||
const telegram = socialProfiles?.telegram || telegramUsername || '';
|
||||
const twitter = socialProfiles?.twitter || twitterScreenName || '';
|
||||
const telegram = socialProfiles?.telegram || telegramUsername || '';
|
||||
const twitter = socialProfiles?.twitter || twitterScreenName || '';
|
||||
|
||||
return {
|
||||
...(socialProfiles || {}),
|
||||
twitter,
|
||||
telegram,
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'contact.id': {
|
||||
handler(id) {
|
||||
this.$store.dispatch('contacts/fetchContactableInbox', id);
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dynamicTime,
|
||||
toggleEditModal() {
|
||||
this.showEditModal = !this.showEditModal;
|
||||
},
|
||||
findCountryFlag(countryCode, cityAndCountry) {
|
||||
try {
|
||||
if (!countryCode) {
|
||||
return `${cityAndCountry} 🌎`;
|
||||
}
|
||||
return {
|
||||
...(socialProfiles || {}),
|
||||
twitter,
|
||||
telegram,
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'contact.id': {
|
||||
handler(id) {
|
||||
this.$store.dispatch('contacts/fetchContactableInbox', id);
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dynamicTime,
|
||||
toggleEditModal() {
|
||||
this.showEditModal = !this.showEditModal;
|
||||
},
|
||||
findCountryFlag(countryCode, cityAndCountry) {
|
||||
try {
|
||||
if (!countryCode) {
|
||||
return `${cityAndCountry} 🌎`;
|
||||
}
|
||||
|
||||
const code = countryCode?.toLowerCase();
|
||||
return `${cityAndCountry} <span class="fi fi-${code} size-3.5"></span>`;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
startEditingName() {
|
||||
this.editName = this.contact.name || '';
|
||||
this.isEditingName = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.nameInput?.focus();
|
||||
});
|
||||
},
|
||||
saveNameEdit() {
|
||||
if (!this.isEditingName) return;
|
||||
this.isEditingName = false;
|
||||
const trimmed = this.editName.trim();
|
||||
if (trimmed && trimmed !== this.contact.name) {
|
||||
this.updateContactField({ name: trimmed });
|
||||
}
|
||||
},
|
||||
cancelNameEdit() {
|
||||
this.isEditingName = false;
|
||||
},
|
||||
onFieldUpdate(field, value) {
|
||||
this.updateContactField({ [field]: value });
|
||||
},
|
||||
async updateContactField(attrs) {
|
||||
const contactId = this.contact.id;
|
||||
try {
|
||||
await this.$store.dispatch('contacts/update', {
|
||||
id: contactId,
|
||||
...attrs,
|
||||
});
|
||||
useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
|
||||
await this.$store.dispatch('contacts/fetchContactableInbox', contactId);
|
||||
} catch (error) {
|
||||
if (error instanceof DuplicateContactException) {
|
||||
const detail = error.contactErrorDetail;
|
||||
if (detail) {
|
||||
useAlert(detail);
|
||||
} else {
|
||||
const invalidAttrs = Array.isArray(error.data) ? error.data : [];
|
||||
if (invalidAttrs.includes('email')) {
|
||||
useAlert(this.$t('CONTACT_FORM.FORM.EMAIL_ADDRESS.DUPLICATE'));
|
||||
} else if (invalidAttrs.includes('phone_number')) {
|
||||
useAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
|
||||
} else {
|
||||
useAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
} else if (error instanceof ExceptionWithMessage) {
|
||||
useAlert(error.data);
|
||||
} else {
|
||||
useAlert(error.message || this.$t('CONTACT_FORM.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
const code = countryCode?.toLowerCase();
|
||||
return `${cityAndCountry} <span class="fi fi-${code} size-3.5"></span>`;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
startEditingName() {
|
||||
this.editName = this.contact.name || '';
|
||||
this.isEditingName = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.nameInput?.focus();
|
||||
});
|
||||
},
|
||||
saveNameEdit() {
|
||||
if (!this.isEditingName) return;
|
||||
this.isEditingName = false;
|
||||
const trimmed = this.editName.trim();
|
||||
if (trimmed && trimmed !== this.contact.name) {
|
||||
this.updateContactField({ name: trimmed });
|
||||
}
|
||||
},
|
||||
cancelNameEdit() {
|
||||
this.isEditingName = false;
|
||||
},
|
||||
onFieldUpdate(field, value) {
|
||||
const normalizedValue =
|
||||
field === 'phone_number'
|
||||
? this.normalizeQuickPhone(value)
|
||||
: value;
|
||||
this.updateContactField({ [field]: normalizedValue });
|
||||
},
|
||||
normalizeQuickPhone(value) {
|
||||
const phone = value.trim().replace(/[\s()-]/g, '');
|
||||
if (!phone || phone.startsWith('+')) return phone;
|
||||
|
||||
const countryCode = (
|
||||
this.additionalAttributes.country_code ||
|
||||
this.contact.country_code ||
|
||||
''
|
||||
).toUpperCase();
|
||||
const country = this.additionalAttributes.country || '';
|
||||
if (countryCode === 'CN' || country === 'China') {
|
||||
return `+86${phone.replace(/^0+/, '')}`;
|
||||
}
|
||||
return phone;
|
||||
},
|
||||
async updateContactField(attrs) {
|
||||
const contactId = this.contact.id;
|
||||
try {
|
||||
await this.$store.dispatch('contacts/update', {
|
||||
id: contactId,
|
||||
...attrs,
|
||||
});
|
||||
useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
|
||||
await this.$store.dispatch(
|
||||
'contacts/fetchContactableInbox',
|
||||
contactId
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof DuplicateContactException) {
|
||||
const detail = error.contactErrorDetail;
|
||||
if (detail) {
|
||||
useAlert(detail);
|
||||
} else {
|
||||
const invalidAttrs = Array.isArray(error.data)
|
||||
? error.data
|
||||
: [];
|
||||
if (invalidAttrs.includes('email')) {
|
||||
useAlert(
|
||||
this.$t(
|
||||
'CONTACT_FORM.FORM.EMAIL_ADDRESS.DUPLICATE'
|
||||
)
|
||||
);
|
||||
} else if (invalidAttrs.includes('phone_number')) {
|
||||
useAlert(
|
||||
this.$t(
|
||||
'CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
useAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
} else if (error instanceof ExceptionWithMessage) {
|
||||
useAlert(error.data);
|
||||
} else {
|
||||
useAlert(
|
||||
error.message || this.$t('CONTACT_FORM.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative items-center w-full p-4">
|
||||
<div class="flex flex-col w-full gap-2 text-left rtl:text-right">
|
||||
<div class="flex flex-row justify-between">
|
||||
<Avatar
|
||||
v-if="showAvatar"
|
||||
:src="contact.thumbnail"
|
||||
:name="contact.name"
|
||||
:status="contact.availability_status"
|
||||
:size="48"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
/>
|
||||
</div>
|
||||
<div class="relative items-center w-full p-4">
|
||||
<div class="flex flex-col w-full gap-2 text-left rtl:text-right">
|
||||
<div class="flex flex-row justify-between">
|
||||
<Avatar
|
||||
v-if="showAvatar"
|
||||
:src="contact.thumbnail"
|
||||
:name="contact.name"
|
||||
:status="contact.availability_status"
|
||||
:size="48"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-start gap-1.5 min-w-0 w-full">
|
||||
<div v-if="showAvatar" class="flex items-center w-full min-w-0 gap-3">
|
||||
<InlineInput
|
||||
v-if="isEditingName"
|
||||
ref="nameInput"
|
||||
v-model="editName"
|
||||
custom-input-class="!text-base !font-medium"
|
||||
class="!w-fit"
|
||||
@enter-press="saveNameEdit"
|
||||
@escape-press="cancelNameEdit"
|
||||
@blur="saveNameEdit"
|
||||
/>
|
||||
<h3
|
||||
v-else
|
||||
class="group/name flex-shrink max-w-full min-w-0 my-0 text-base capitalize break-words text-n-slate-12 cursor-pointer hover:text-n-slate-12/80"
|
||||
:title="$t('CONTACT_PANEL.CLICK_TO_EDIT')"
|
||||
@click="startEditingName"
|
||||
>
|
||||
{{ contact.name }}
|
||||
<span
|
||||
class="i-lucide-pencil text-xs text-n-slate-10 opacity-0 group-hover/name:opacity-100 transition-opacity ml-1 align-middle"
|
||||
/>
|
||||
</h3>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<span
|
||||
v-if="contact.created_at"
|
||||
v-tooltip.left="
|
||||
`${$t('CONTACT_PANEL.CREATED_AT_LABEL')} ${dynamicTime(
|
||||
contact.created_at
|
||||
)}`
|
||||
"
|
||||
class="i-lucide-info text-sm text-n-slate-10"
|
||||
/>
|
||||
<a
|
||||
:href="contactProfileLink"
|
||||
target="_blank"
|
||||
rel="noopener nofollow noreferrer"
|
||||
class="leading-3"
|
||||
>
|
||||
<span class="i-lucide-external-link text-sm text-n-slate-10" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-start gap-1.5 min-w-0 w-full">
|
||||
<div
|
||||
v-if="showAvatar"
|
||||
class="flex items-center w-full min-w-0 gap-3"
|
||||
>
|
||||
<InlineInput
|
||||
v-if="isEditingName"
|
||||
ref="nameInput"
|
||||
v-model="editName"
|
||||
custom-input-class="!text-base !font-medium"
|
||||
class="!w-fit"
|
||||
@enter-press="saveNameEdit"
|
||||
@escape-press="cancelNameEdit"
|
||||
@blur="saveNameEdit"
|
||||
/>
|
||||
<h3
|
||||
v-else
|
||||
class="group/name flex-shrink max-w-full min-w-0 my-0 text-base capitalize break-words text-n-slate-12 cursor-pointer hover:text-n-slate-12/80"
|
||||
:title="$t('CONTACT_PANEL.CLICK_TO_EDIT')"
|
||||
@click="startEditingName"
|
||||
>
|
||||
{{ contact.name }}
|
||||
<span
|
||||
class="i-lucide-pencil text-xs text-n-slate-10 opacity-0 group-hover/name:opacity-100 transition-opacity ml-1 align-middle"
|
||||
/>
|
||||
</h3>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<span
|
||||
v-if="contact.created_at"
|
||||
v-tooltip.left="
|
||||
`${$t('CONTACT_PANEL.CREATED_AT_LABEL')} ${dynamicTime(
|
||||
contact.created_at
|
||||
)}`
|
||||
"
|
||||
class="i-lucide-info text-sm text-n-slate-10"
|
||||
/>
|
||||
<a
|
||||
:href="contactProfileLink"
|
||||
target="_blank"
|
||||
rel="noopener nofollow noreferrer"
|
||||
class="leading-3"
|
||||
>
|
||||
<span
|
||||
class="i-lucide-external-link text-sm text-n-slate-10"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="additionalAttributes.description" class="break-words mb-0.5">
|
||||
{{ additionalAttributes.description }}
|
||||
</p>
|
||||
<div class="flex flex-col items-start w-full gap-2">
|
||||
<ContactInfoRow
|
||||
:href="contact.email ? `mailto:${contact.email}` : ''"
|
||||
:value="contact.email"
|
||||
icon="mail"
|
||||
emoji="✉️"
|
||||
:title="$t('CONTACT_PANEL.EMAIL_ADDRESS')"
|
||||
show-copy
|
||||
editable
|
||||
@update="value => onFieldUpdate('email', value)"
|
||||
/>
|
||||
<ContactInfoRow
|
||||
:href="contact.phone_number ? `tel:${contact.phone_number}` : ''"
|
||||
:value="contact.phone_number"
|
||||
icon="call"
|
||||
emoji="📞"
|
||||
:title="$t('CONTACT_PANEL.PHONE_NUMBER')"
|
||||
show-copy
|
||||
editable
|
||||
@update="value => onFieldUpdate('phone_number', value)"
|
||||
/>
|
||||
<ContactInfoRow
|
||||
v-if="contact.identifier"
|
||||
:value="contact.identifier"
|
||||
icon="contact-identify"
|
||||
emoji="🪪"
|
||||
:title="$t('CONTACT_PANEL.IDENTIFIER')"
|
||||
/>
|
||||
<ContactInfoRow
|
||||
:value="additionalAttributes.company_name"
|
||||
icon="building-bank"
|
||||
emoji="🏢"
|
||||
:title="$t('CONTACT_PANEL.COMPANY')"
|
||||
editable
|
||||
@update="
|
||||
value =>
|
||||
updateContactField({
|
||||
additional_attributes: {
|
||||
...additionalAttributes,
|
||||
company_name: value,
|
||||
},
|
||||
})
|
||||
"
|
||||
/>
|
||||
<ContactInfoRow
|
||||
v-if="location || additionalAttributes.location"
|
||||
:value="location || additionalAttributes.location"
|
||||
icon="map"
|
||||
emoji="🌍"
|
||||
:title="$t('CONTACT_PANEL.LOCATION')"
|
||||
/>
|
||||
<SocialIcons :social-profiles="socialProfiles" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center w-full mt-0.5 gap-2">
|
||||
<ComposeConversation :contact-id="String(contact.id)">
|
||||
<template #trigger>
|
||||
<NextButton
|
||||
v-tooltip.top-end="$t('CONTACT_PANEL.NEW_MESSAGE')"
|
||||
icon="i-ph-chat-circle-dots"
|
||||
slate
|
||||
faded
|
||||
sm
|
||||
/>
|
||||
</template>
|
||||
</ComposeConversation>
|
||||
<VoiceCallButton
|
||||
:phone="contact.phone_number"
|
||||
:contact-id="contact.id"
|
||||
icon="i-ri-phone-fill"
|
||||
size="sm"
|
||||
:tooltip-label="$t('CONTACT_PANEL.CALL')"
|
||||
slate
|
||||
faded
|
||||
/>
|
||||
<NextButton
|
||||
v-tooltip.top-end="$t('EDIT_CONTACT.BUTTON_LABEL')"
|
||||
icon="i-ph-pencil-simple"
|
||||
slate
|
||||
faded
|
||||
sm
|
||||
@click="toggleEditModal"
|
||||
/>
|
||||
<ContactMergeModal :primary-contact="contact">
|
||||
<template #trigger>
|
||||
<NextButton
|
||||
v-tooltip.top-end="$t('CONTACT_PANEL.MERGE_CONTACT')"
|
||||
icon="i-ph-arrows-merge"
|
||||
slate
|
||||
faded
|
||||
sm
|
||||
:disabled="uiFlags.isMerging"
|
||||
/>
|
||||
</template>
|
||||
</ContactMergeModal>
|
||||
<ContactDeleteModal
|
||||
v-if="isAdmin"
|
||||
:contact="contact"
|
||||
@deleted="$emit('panelClose')"
|
||||
>
|
||||
<template #trigger>
|
||||
<NextButton
|
||||
v-tooltip.top-end="$t('DELETE_CONTACT.BUTTON_LABEL')"
|
||||
icon="i-ph-trash"
|
||||
slate
|
||||
faded
|
||||
sm
|
||||
ruby
|
||||
:disabled="uiFlags.isDeleting"
|
||||
/>
|
||||
</template>
|
||||
</ContactDeleteModal>
|
||||
</div>
|
||||
<EditContact
|
||||
:show="showEditModal"
|
||||
:contact="contact"
|
||||
@cancel="toggleEditModal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-if="additionalAttributes.description"
|
||||
class="break-words mb-0.5"
|
||||
>
|
||||
{{ additionalAttributes.description }}
|
||||
</p>
|
||||
<div class="flex flex-col items-start w-full gap-2">
|
||||
<ContactInfoRow
|
||||
:href="contact.email ? `mailto:${contact.email}` : ''"
|
||||
:value="contact.email"
|
||||
icon="mail"
|
||||
emoji="✉️"
|
||||
:title="$t('CONTACT_PANEL.EMAIL_ADDRESS')"
|
||||
show-copy
|
||||
editable
|
||||
@update="value => onFieldUpdate('email', value)"
|
||||
/>
|
||||
<ContactInfoRow
|
||||
:href="
|
||||
contact.phone_number
|
||||
? `tel:${contact.phone_number}`
|
||||
: ''
|
||||
"
|
||||
:value="contact.phone_number"
|
||||
:edit-prefix="quickPhonePrefix"
|
||||
icon="call"
|
||||
emoji="📞"
|
||||
:title="$t('CONTACT_PANEL.PHONE_NUMBER')"
|
||||
show-copy
|
||||
editable
|
||||
@update="value => onFieldUpdate('phone_number', value)"
|
||||
/>
|
||||
<ContactInfoRow
|
||||
v-if="contact.identifier"
|
||||
:value="contact.identifier"
|
||||
icon="contact-identify"
|
||||
emoji="🪪"
|
||||
:title="$t('CONTACT_PANEL.IDENTIFIER')"
|
||||
/>
|
||||
<ContactInfoRow
|
||||
:value="additionalAttributes.company_name"
|
||||
icon="building-bank"
|
||||
emoji="🏢"
|
||||
:title="$t('CONTACT_PANEL.COMPANY')"
|
||||
editable
|
||||
@update="
|
||||
value =>
|
||||
updateContactField({
|
||||
additional_attributes: {
|
||||
...additionalAttributes,
|
||||
company_name: value,
|
||||
},
|
||||
})
|
||||
"
|
||||
/>
|
||||
<ContactInfoRow
|
||||
v-if="location || additionalAttributes.location"
|
||||
:value="location || additionalAttributes.location"
|
||||
icon="map"
|
||||
emoji="🌍"
|
||||
:title="$t('CONTACT_PANEL.LOCATION')"
|
||||
/>
|
||||
<SocialIcons :social-profiles="socialProfiles" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center w-full mt-0.5 gap-2">
|
||||
<ComposeConversation :contact-id="String(contact.id)">
|
||||
<template #trigger>
|
||||
<NextButton
|
||||
v-tooltip.top-end="$t('CONTACT_PANEL.NEW_MESSAGE')"
|
||||
icon="i-ph-chat-circle-dots"
|
||||
slate
|
||||
faded
|
||||
sm
|
||||
/>
|
||||
</template>
|
||||
</ComposeConversation>
|
||||
<VoiceCallButton
|
||||
:phone="contact.phone_number"
|
||||
:contact-id="contact.id"
|
||||
icon="i-ri-phone-fill"
|
||||
size="sm"
|
||||
:tooltip-label="$t('CONTACT_PANEL.CALL')"
|
||||
slate
|
||||
faded
|
||||
/>
|
||||
<NextButton
|
||||
v-tooltip.top-end="$t('EDIT_CONTACT.BUTTON_LABEL')"
|
||||
icon="i-ph-pencil-simple"
|
||||
slate
|
||||
faded
|
||||
sm
|
||||
@click="toggleEditModal"
|
||||
/>
|
||||
<ContactMergeModal :primary-contact="contact">
|
||||
<template #trigger>
|
||||
<NextButton
|
||||
v-tooltip.top-end="
|
||||
$t('CONTACT_PANEL.MERGE_CONTACT')
|
||||
"
|
||||
icon="i-ph-arrows-merge"
|
||||
slate
|
||||
faded
|
||||
sm
|
||||
:disabled="uiFlags.isMerging"
|
||||
/>
|
||||
</template>
|
||||
</ContactMergeModal>
|
||||
<ContactDeleteModal
|
||||
v-if="isAdmin"
|
||||
:contact="contact"
|
||||
@deleted="$emit('panelClose')"
|
||||
>
|
||||
<template #trigger>
|
||||
<NextButton
|
||||
v-tooltip.top-end="
|
||||
$t('DELETE_CONTACT.BUTTON_LABEL')
|
||||
"
|
||||
icon="i-ph-trash"
|
||||
slate
|
||||
faded
|
||||
sm
|
||||
ruby
|
||||
:disabled="uiFlags.isDeleting"
|
||||
/>
|
||||
</template>
|
||||
</ContactDeleteModal>
|
||||
</div>
|
||||
<EditContact
|
||||
:show="showEditModal"
|
||||
:contact="contact"
|
||||
@cancel="toggleEditModal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+159
-155
@@ -6,165 +6,169 @@ import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmojiOrIcon,
|
||||
NextButton,
|
||||
InlineInput,
|
||||
},
|
||||
props: {
|
||||
href: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emoji: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showCopy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['update'],
|
||||
data() {
|
||||
return {
|
||||
isEditing: false,
|
||||
editValue: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async onCopy(e) {
|
||||
e.preventDefault();
|
||||
await copyTextToClipboard(this.value);
|
||||
useAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
||||
},
|
||||
startEditing() {
|
||||
if (!this.editable) return;
|
||||
this.editValue = this.value || '';
|
||||
this.isEditing = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.editInput?.focus();
|
||||
});
|
||||
},
|
||||
saveEdit() {
|
||||
if (!this.isEditing) return;
|
||||
this.isEditing = false;
|
||||
const trimmed = this.editValue.trim();
|
||||
if (trimmed !== (this.value || '')) {
|
||||
this.$emit('update', trimmed);
|
||||
}
|
||||
},
|
||||
cancelEdit() {
|
||||
this.isEditing = false;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
EmojiOrIcon,
|
||||
NextButton,
|
||||
InlineInput,
|
||||
},
|
||||
props: {
|
||||
href: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emoji: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showCopy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
editPrefix: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['update'],
|
||||
data() {
|
||||
return {
|
||||
isEditing: false,
|
||||
editValue: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async onCopy(e) {
|
||||
e.preventDefault();
|
||||
await copyTextToClipboard(this.value);
|
||||
useAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
||||
},
|
||||
startEditing() {
|
||||
if (!this.editable) return;
|
||||
this.editValue = this.value || this.editPrefix;
|
||||
this.isEditing = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.editInput?.focus();
|
||||
});
|
||||
},
|
||||
saveEdit() {
|
||||
if (!this.isEditing) return;
|
||||
this.isEditing = false;
|
||||
const trimmed = this.editValue.trim();
|
||||
if (trimmed !== (this.value || '')) {
|
||||
this.$emit('update', trimmed);
|
||||
}
|
||||
},
|
||||
cancelEdit() {
|
||||
this.isEditing = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="group/row w-full h-5 ltr:-ml-1 rtl:-mr-1">
|
||||
<!-- Inline edit mode -->
|
||||
<div v-if="isEditing" class="flex items-center gap-2">
|
||||
<EmojiOrIcon
|
||||
:icon="icon"
|
||||
:emoji="emoji"
|
||||
icon-size="14"
|
||||
class="flex-shrink-0 ltr:ml-1 rtl:mr-1"
|
||||
/>
|
||||
<InlineInput
|
||||
ref="editInput"
|
||||
v-model="editValue"
|
||||
:placeholder="title"
|
||||
class="!w-fit"
|
||||
@enter-press="saveEdit"
|
||||
@escape-press="cancelEdit"
|
||||
@blur="saveEdit"
|
||||
/>
|
||||
</div>
|
||||
<div class="group/row w-full h-5 ltr:-ml-1 rtl:-mr-1">
|
||||
<!-- Inline edit mode -->
|
||||
<div v-if="isEditing" class="flex items-center gap-2">
|
||||
<EmojiOrIcon
|
||||
:icon="icon"
|
||||
:emoji="emoji"
|
||||
icon-size="14"
|
||||
class="flex-shrink-0 ltr:ml-1 rtl:mr-1"
|
||||
/>
|
||||
<InlineInput
|
||||
ref="editInput"
|
||||
v-model="editValue"
|
||||
:placeholder="title"
|
||||
class="!w-fit"
|
||||
@enter-press="saveEdit"
|
||||
@escape-press="cancelEdit"
|
||||
@blur="saveEdit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Read mode with link -->
|
||||
<a
|
||||
v-else-if="href"
|
||||
:href="href"
|
||||
class="flex items-center gap-2 text-n-slate-11 hover:underline"
|
||||
>
|
||||
<EmojiOrIcon
|
||||
:icon="icon"
|
||||
:emoji="emoji"
|
||||
icon-size="14"
|
||||
class="flex-shrink-0 ltr:ml-1 rtl:mr-1"
|
||||
/>
|
||||
<span
|
||||
v-if="value"
|
||||
class="overflow-hidden text-sm whitespace-nowrap text-ellipsis"
|
||||
:title="value"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
<span v-else class="text-sm text-n-slate-11">
|
||||
{{ $t('CONTACT_PANEL.NOT_AVAILABLE') }}
|
||||
</span>
|
||||
<NextButton
|
||||
v-if="showCopy"
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
class="ltr:-ml-1 rtl:-mr-1"
|
||||
icon="i-lucide-clipboard"
|
||||
@click="onCopy"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="editable"
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
class="ltr:-ml-1 rtl:-mr-1 opacity-0 group-hover/row:opacity-100 transition-opacity"
|
||||
icon="i-lucide-pencil"
|
||||
@click.prevent="startEditing"
|
||||
/>
|
||||
</a>
|
||||
<!-- Read mode with link -->
|
||||
<a
|
||||
v-else-if="href"
|
||||
:href="href"
|
||||
class="flex items-center gap-2 text-n-slate-11 hover:underline"
|
||||
>
|
||||
<EmojiOrIcon
|
||||
:icon="icon"
|
||||
:emoji="emoji"
|
||||
icon-size="14"
|
||||
class="flex-shrink-0 ltr:ml-1 rtl:mr-1"
|
||||
/>
|
||||
<span
|
||||
v-if="value"
|
||||
class="overflow-hidden text-sm whitespace-nowrap text-ellipsis"
|
||||
:title="value"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
<span v-else class="text-sm text-n-slate-11">
|
||||
{{ $t('CONTACT_PANEL.NOT_AVAILABLE') }}
|
||||
</span>
|
||||
<NextButton
|
||||
v-if="showCopy"
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
class="ltr:-ml-1 rtl:-mr-1"
|
||||
icon="i-lucide-clipboard"
|
||||
@click="onCopy"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="editable"
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
class="ltr:-ml-1 rtl:-mr-1 opacity-0 group-hover/row:opacity-100 transition-opacity"
|
||||
icon="i-lucide-pencil"
|
||||
@click.prevent="startEditing"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<!-- Read mode without link -->
|
||||
<div v-else class="flex items-center gap-2 text-n-slate-11">
|
||||
<EmojiOrIcon
|
||||
:icon="icon"
|
||||
:emoji="emoji"
|
||||
icon-size="14"
|
||||
class="flex-shrink-0 ltr:ml-1 rtl:mr-1"
|
||||
/>
|
||||
<span
|
||||
v-if="value"
|
||||
v-dompurify-html="value"
|
||||
class="overflow-hidden text-sm whitespace-nowrap text-ellipsis"
|
||||
/>
|
||||
<span v-else class="text-sm text-n-slate-11">
|
||||
{{ $t('CONTACT_PANEL.NOT_AVAILABLE') }}
|
||||
</span>
|
||||
<NextButton
|
||||
v-if="editable"
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
class="ltr:-ml-1 rtl:-mr-1 opacity-0 group-hover/row:opacity-100 transition-opacity"
|
||||
icon="i-lucide-pencil"
|
||||
@click="startEditing"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Read mode without link -->
|
||||
<div v-else class="flex items-center gap-2 text-n-slate-11">
|
||||
<EmojiOrIcon
|
||||
:icon="icon"
|
||||
:emoji="emoji"
|
||||
icon-size="14"
|
||||
class="flex-shrink-0 ltr:ml-1 rtl:mr-1"
|
||||
/>
|
||||
<span
|
||||
v-if="value"
|
||||
v-dompurify-html="value"
|
||||
class="overflow-hidden text-sm whitespace-nowrap text-ellipsis"
|
||||
/>
|
||||
<span v-else class="text-sm text-n-slate-11">
|
||||
{{ $t('CONTACT_PANEL.NOT_AVAILABLE') }}
|
||||
</span>
|
||||
<NextButton
|
||||
v-if="editable"
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
class="ltr:-ml-1 rtl:-mr-1 opacity-0 group-hover/row:opacity-100 transition-opacity"
|
||||
icon="i-lucide-pencil"
|
||||
@click="startEditing"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user