Inline edit for phone, email, company
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
"CALL": "Call",
|
||||
"CALL_INITIATED": "Calling the contact…",
|
||||
"CALL_FAILED": "Unable to start the call. Please try again.",
|
||||
"CLICK_TO_EDIT": "Click to edit",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "Choose a voice inbox"
|
||||
},
|
||||
|
||||
@@ -52,6 +52,8 @@ export default {
|
||||
return {
|
||||
showEditModal: false,
|
||||
showDeleteModal: false,
|
||||
isEditingName: false,
|
||||
editName: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -173,6 +175,38 @@ export default {
|
||||
openMergeModal() {
|
||||
this.$refs.mergeModal?.open();
|
||||
},
|
||||
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) {
|
||||
try {
|
||||
await this.$store.dispatch('contacts/update', {
|
||||
id: this.contact.id,
|
||||
...attrs,
|
||||
});
|
||||
useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(error.message || this.$t('CONTACT_FORM.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -194,10 +228,26 @@ export default {
|
||||
|
||||
<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">
|
||||
<input
|
||||
v-if="isEditingName"
|
||||
ref="nameInput"
|
||||
v-model="editName"
|
||||
type="text"
|
||||
class="!mb-0 !h-7 !text-base !py-0 !px-1.5 !rounded !min-w-0 flex-shrink max-w-full w-full"
|
||||
@keydown.enter="saveNameEdit"
|
||||
@keydown.escape="cancelNameEdit"
|
||||
@blur="saveNameEdit"
|
||||
/>
|
||||
<h3
|
||||
class="flex-shrink max-w-full min-w-0 my-0 text-base capitalize break-words text-n-slate-12"
|
||||
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
|
||||
@@ -231,6 +281,8 @@ export default {
|
||||
emoji="✉️"
|
||||
:title="$t('CONTACT_PANEL.EMAIL_ADDRESS')"
|
||||
show-copy
|
||||
editable
|
||||
@update="value => onFieldUpdate('email', value)"
|
||||
/>
|
||||
<ContactInfoRow
|
||||
:href="contact.phone_number ? `tel:${contact.phone_number}` : ''"
|
||||
@@ -239,6 +291,8 @@ export default {
|
||||
emoji="📞"
|
||||
:title="$t('CONTACT_PANEL.PHONE_NUMBER')"
|
||||
show-copy
|
||||
editable
|
||||
@update="value => onFieldUpdate('phone_number', value)"
|
||||
/>
|
||||
<ContactInfoRow
|
||||
v-if="contact.identifier"
|
||||
@@ -252,6 +306,16 @@ export default {
|
||||
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"
|
||||
|
||||
@@ -30,6 +30,21 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['update'],
|
||||
data() {
|
||||
return {
|
||||
isEditing: false,
|
||||
editValue: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async onCopy(e) {
|
||||
@@ -37,14 +52,54 @@ export default {
|
||||
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;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-5 ltr:-ml-1 rtl:-mr-1">
|
||||
<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"
|
||||
/>
|
||||
<input
|
||||
ref="editInput"
|
||||
v-model="editValue"
|
||||
type="text"
|
||||
:placeholder="title"
|
||||
class="!mb-0 !h-6 !text-sm !py-0 !px-1.5 !rounded !min-w-0 w-full"
|
||||
@keydown.enter="saveEdit"
|
||||
@keydown.escape="cancelEdit"
|
||||
@blur="saveEdit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Read mode with link -->
|
||||
<a
|
||||
v-if="href"
|
||||
v-else-if="href"
|
||||
:href="href"
|
||||
class="flex items-center gap-2 text-n-slate-11 hover:underline"
|
||||
>
|
||||
@@ -73,8 +128,18 @@ export default {
|
||||
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"
|
||||
@@ -90,6 +155,15 @@ export default {
|
||||
<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>
|
||||
|
||||
@@ -36,7 +36,11 @@ const buildContactFormData = contactParams => {
|
||||
|
||||
export const handleContactOperationErrors = error => {
|
||||
if (error.response?.status === 422) {
|
||||
throw new DuplicateContactException(error.response.data.attributes);
|
||||
const exception = new DuplicateContactException(
|
||||
error.response.data.attributes
|
||||
);
|
||||
exception.message = error.response.data.message || exception.message;
|
||||
throw exception;
|
||||
} else if (error.response?.data?.message) {
|
||||
throw new ExceptionWithMessage(error.response.data.message);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user