feat: Update contact details page UI (#13041)
This commit is contained in:
@@ -76,12 +76,12 @@ const closeMobileSidebar = () => {
|
||||
class="flex w-full h-full overflow-hidden justify-evenly bg-n-surface-1"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col w-full h-full transition-all duration-300 ltr:2xl:ml-56 rtl:2xl:mr-56"
|
||||
class="flex flex-col w-full h-full transition-all duration-300 ltr:3xl:ml-56 rtl:3xl:mr-56"
|
||||
>
|
||||
<header class="sticky top-0 z-10 px-6 3xl:px-0">
|
||||
<div class="w-full mx-auto max-w-[40.625rem]">
|
||||
<div class="w-full mx-auto max-w-4xl">
|
||||
<div
|
||||
class="flex flex-col xs:flex-row items-start xs:items-center justify-between w-full py-7 gap-2"
|
||||
class="flex flex-col xs:flex-row items-start xs:items-center justify-between w-full py-4 gap-2"
|
||||
>
|
||||
<Breadcrumb
|
||||
:items="breadcrumbItems"
|
||||
@@ -94,23 +94,20 @@ const closeMobileSidebar = () => {
|
||||
? $t('CONTACTS_LAYOUT.HEADER.BLOCK_CONTACT')
|
||||
: $t('CONTACTS_LAYOUT.HEADER.UNBLOCK_CONTACT')
|
||||
"
|
||||
size="sm"
|
||||
slate
|
||||
:is-loading="isUpdating"
|
||||
:disabled="isUpdating"
|
||||
@click="toggleBlock"
|
||||
/>
|
||||
<VoiceCallButton
|
||||
:phone="selectedContact?.phoneNumber"
|
||||
:contact-id="contactId"
|
||||
size="md"
|
||||
:label="$t('CONTACT_PANEL.CALL')"
|
||||
size="sm"
|
||||
/>
|
||||
<ComposeConversation :contact-id="contactId">
|
||||
<template #trigger="{ toggle }">
|
||||
<Button
|
||||
:label="$t('CONTACTS_LAYOUT.HEADER.SEND_MESSAGE')"
|
||||
size="sm"
|
||||
@click="toggle"
|
||||
/>
|
||||
</template>
|
||||
@@ -120,7 +117,7 @@ const closeMobileSidebar = () => {
|
||||
</div>
|
||||
</header>
|
||||
<main class="flex-1 px-6 overflow-y-auto 3xl:px-px">
|
||||
<div class="w-full py-4 mx-auto max-w-[40.625rem]">
|
||||
<div class="w-full pt-2.5 pb-4 mx-auto max-w-4xl">
|
||||
<slot name="default" />
|
||||
</div>
|
||||
</main>
|
||||
@@ -129,7 +126,7 @@ const closeMobileSidebar = () => {
|
||||
<!-- Desktop sidebar -->
|
||||
<div
|
||||
v-if="slots.sidebar"
|
||||
class="hidden lg:block overflow-y-auto justify-end min-w-52 w-full py-6 max-w-md border-l border-n-weak bg-n-solid-2"
|
||||
class="hidden lg:block overflow-y-auto justify-end min-w-52 w-full py-5 max-w-md border-l border-n-weak bg-n-surface-2"
|
||||
>
|
||||
<slot name="sidebar" />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineProps({
|
||||
selectedContact: {
|
||||
@@ -37,13 +38,13 @@ const { t } = useI18n();
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between h-5 gap-2">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between h-6 gap-2">
|
||||
<label class="text-sm text-n-slate-12">
|
||||
{{ t('CONTACTS_LAYOUT.SIDEBAR.MERGE.PRIMARY') }}
|
||||
</label>
|
||||
<span
|
||||
class="flex items-center justify-center w-24 h-5 text-xs rounded-md text-n-teal-11 bg-n-alpha-2"
|
||||
class="flex items-center justify-center px-1.5 h-6 font-420 text-xs rounded-md text-n-teal-11 outline outline-1 outline-n-weak"
|
||||
>
|
||||
{{ t('CONTACTS_LAYOUT.SIDEBAR.MERGE.PRIMARY_HELP_LABEL') }}
|
||||
</span>
|
||||
@@ -69,7 +70,7 @@ const { t } = useI18n();
|
||||
@search="query => emit('search', query)"
|
||||
/>
|
||||
</div>
|
||||
<div class="relative flex justify-center gap-2 top-4">
|
||||
<div class="relative flex justify-center gap-2 mt-4 mb-3">
|
||||
<div v-for="i in 3" :key="i" class="relative w-4 h-8">
|
||||
<div
|
||||
class="absolute w-0 h-0 border-l-[4px] border-r-[4px] border-b-[6px] border-l-transparent border-r-transparent border-n-strong ltr:translate-x-[4px] rtl:-translate-x-[4px] -translate-y-[4px]"
|
||||
@@ -79,13 +80,13 @@ const { t } = useI18n();
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between h-5 gap-2">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between h-6 gap-2">
|
||||
<label class="text-sm text-n-slate-12">
|
||||
{{ t('CONTACTS_LAYOUT.SIDEBAR.MERGE.PARENT') }}
|
||||
</label>
|
||||
<span
|
||||
class="flex items-center justify-center w-24 h-5 text-xs rounded-md text-n-ruby-11 bg-n-alpha-2"
|
||||
class="flex items-center justify-center px-1.5 h-6 font-420 text-xs rounded-md text-n-ruby-11 outline outline-1 outline-n-weak"
|
||||
>
|
||||
{{ t('CONTACTS_LAYOUT.SIDEBAR.MERGE.PARENT_HELP_LABEL') }}
|
||||
</span>
|
||||
|
||||
@@ -4,11 +4,11 @@ import { useI18n } from 'vue-i18n';
|
||||
import { required, email } from '@vuelidate/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { splitName } from '@chatwoot/utils';
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||
import CountryDropdown from 'dashboard/components-next/Countries/CountryDropdown.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import PhoneNumberInput from 'dashboard/components-next/phonenumberinput/PhoneNumberInput.vue';
|
||||
import CompaniesDropdown from 'dashboard/components-next/Companies/CompaniesDropdown.vue';
|
||||
|
||||
const props = defineProps({
|
||||
contactData: {
|
||||
@@ -122,10 +122,6 @@ const prepareStateBasedOnProps = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const countryOptions = computed(() =>
|
||||
countries.map(({ name, id }) => ({ label: name, value: id }))
|
||||
);
|
||||
|
||||
const editDetailsForm = computed(() =>
|
||||
Object.keys(FORM_CONFIG).map(key => ({
|
||||
key,
|
||||
@@ -204,9 +200,14 @@ const getMessageType = key => {
|
||||
: 'info';
|
||||
};
|
||||
|
||||
const handleCountrySelection = value => {
|
||||
const selectedCountry = countries.find(option => option.id === value);
|
||||
state.additionalAttributes.country = selectedCountry?.name || '';
|
||||
const handleCountryChange = country => {
|
||||
state.additionalAttributes.countryCode = country?.id || '';
|
||||
state.additionalAttributes.country = country?.name || '';
|
||||
emit('update', state);
|
||||
};
|
||||
|
||||
const handleCompanyChange = company => {
|
||||
state.additionalAttributes.companyName = company?.name || '';
|
||||
emit('update', state);
|
||||
};
|
||||
|
||||
@@ -236,41 +237,49 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col items-start gap-2">
|
||||
<span class="py-1 text-sm font-medium text-n-slate-12">
|
||||
<div class="flex flex-col gap-6 w-full">
|
||||
<div class="flex flex-col items-start gap-5 w-full">
|
||||
<span class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.TITLE') }}
|
||||
</span>
|
||||
<div class="grid w-full grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<template v-for="item in editDetailsForm" :key="item.key">
|
||||
<ComboBox
|
||||
<CountryDropdown
|
||||
v-if="item.key === 'COUNTRY'"
|
||||
v-model="state.additionalAttributes.countryCode"
|
||||
:options="countryOptions"
|
||||
:placeholder="item.placeholder"
|
||||
class="[&>div>button]:h-8"
|
||||
:class="{
|
||||
'[&>div>button]:bg-n-alpha-black2 [&>div>button:not(.focused)]:!outline-transparent':
|
||||
!isDetailsView,
|
||||
'[&>div>button]:!bg-n-alpha-black2': isDetailsView,
|
||||
'[&>div>button]:h-8': !isDetailsView,
|
||||
'[&_button]:!h-10 [&_button]:!rounded-[10px]': isDetailsView,
|
||||
}"
|
||||
@update:model-value="handleCountrySelection"
|
||||
@change="handleCountryChange"
|
||||
/>
|
||||
<PhoneNumberInput
|
||||
v-else-if="item.key === 'PHONE_NUMBER'"
|
||||
v-model="getFormBinding(item.key).value"
|
||||
:placeholder="item.placeholder"
|
||||
:show-border="isDetailsView"
|
||||
:compact="!isDetailsView"
|
||||
/>
|
||||
<CompaniesDropdown
|
||||
v-else-if="item.key === 'COMPANY_NAME'"
|
||||
:placeholder="item.placeholder"
|
||||
:selected-company-name="state.additionalAttributes.companyName"
|
||||
:class="{
|
||||
'[&>div>button]:h-8': !isDetailsView,
|
||||
'[&_button]:!h-10 [&_button]:!rounded-[10px]': isDetailsView,
|
||||
}"
|
||||
@change="handleCompanyChange"
|
||||
/>
|
||||
<Input
|
||||
v-else
|
||||
v-model="getFormBinding(item.key).value"
|
||||
:placeholder="item.placeholder"
|
||||
:message-type="getMessageType(item.key)"
|
||||
:custom-input-class="`h-8 !pt-1 !pb-1 ${
|
||||
:custom-input-class="`!pt-1 !pb-1 ${
|
||||
!isDetailsView
|
||||
? '[&:not(.error,.focus)]:!outline-transparent'
|
||||
: ''
|
||||
? '[&:not(.error,.focus)]:!outline-transparent h-8'
|
||||
: 'h-10 !rounded-[10px]'
|
||||
}`"
|
||||
class="w-full"
|
||||
@input="
|
||||
@@ -285,18 +294,19 @@ defineExpose({
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-start gap-2">
|
||||
<span class="py-1 text-sm font-medium text-n-slate-12">
|
||||
<div class="flex flex-col items-start gap-5">
|
||||
<span class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('CONTACTS_LAYOUT.CARD.SOCIAL_MEDIA.TITLE') }}
|
||||
</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="item in socialProfilesForm"
|
||||
:key="item.key"
|
||||
class="flex items-center h-8 gap-2 px-2 rounded-lg"
|
||||
class="flex items-center h-8 gap-2 px-2 bg-n-alpha-2 dark:bg-n-solid-2"
|
||||
:class="{
|
||||
'bg-n-alpha-2 dark:bg-n-solid-2': isDetailsView,
|
||||
'bg-n-alpha-2 dark:bg-n-solid-3': !isDetailsView,
|
||||
'rounded-[10px] outline-1 outline outline-n-weak -outline-offset-1 hover:outline-n-slate-6':
|
||||
isDetailsView,
|
||||
'rounded-lg': !isDetailsView,
|
||||
}"
|
||||
>
|
||||
<Icon
|
||||
|
||||
+8
-3
@@ -9,6 +9,7 @@ import ListAttribute from 'dashboard/components-next/CustomAttributes/ListAttrib
|
||||
import CheckboxAttribute from 'dashboard/components-next/CustomAttributes/CheckboxAttribute.vue';
|
||||
import DateAttribute from 'dashboard/components-next/CustomAttributes/DateAttribute.vue';
|
||||
import OtherAttribute from 'dashboard/components-next/CustomAttributes/OtherAttribute.vue';
|
||||
import HelperTextPopup from 'dashboard/components/ui/HelperTextPopup.vue';
|
||||
|
||||
const props = defineProps({
|
||||
attribute: {
|
||||
@@ -75,11 +76,15 @@ const CurrentAttributeComponent = computed(() => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="grid grid-cols-[140px,1fr] group/attribute items-center w-full gap-2"
|
||||
class="grid grid-cols-[auto,auto] group/attribute items-center w-full gap-4"
|
||||
:class="isEditingView ? 'min-h-10' : 'min-h-11'"
|
||||
>
|
||||
<div class="flex items-center justify-between truncate">
|
||||
<span class="text-sm font-medium truncate text-n-slate-12">
|
||||
<div class="flex items-center gap-1.5 min-w-0">
|
||||
<HelperTextPopup
|
||||
v-if="attribute.attributeDescription"
|
||||
:message="attribute.attributeDescription"
|
||||
/>
|
||||
<span class="text-sm font-420 truncate text-n-slate-12">
|
||||
{{ attribute.attributeDisplayName }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
+5
-5
@@ -108,7 +108,7 @@ const hasNoUsedAttributes = computed(() => usedAttributes.value.length === 0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasContactAttributes" class="flex flex-col gap-6 px-6 py-6">
|
||||
<div v-if="hasContactAttributes" class="flex flex-col gap-4 px-5 pt-4 pb-6">
|
||||
<div v-if="!hasNoUsedAttributes" class="flex flex-col gap-2">
|
||||
<ContactCustomAttributeItem
|
||||
v-for="attribute in usedAttributes"
|
||||
@@ -117,16 +117,16 @@ const hasNoUsedAttributes = computed(() => usedAttributes.value.length === 0);
|
||||
:attribute="attribute"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!hasNoUnusedAttributes" class="flex items-center gap-3">
|
||||
<div v-if="!hasNoUnusedAttributes" class="flex items-center gap-2">
|
||||
<div class="flex-1 h-[1px] bg-n-slate-5" />
|
||||
<span class="text-sm font-medium text-n-slate-10">{{
|
||||
<span class="text-xs font-420 text-n-slate-10">{{
|
||||
t('CONTACTS_LAYOUT.SIDEBAR.ATTRIBUTES.UNUSED_ATTRIBUTES', {
|
||||
count: unusedAttributesCount,
|
||||
})
|
||||
}}</span>
|
||||
<div class="flex-1 h-[1px] bg-n-slate-5" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div v-if="!hasNoUnusedAttributes" class="relative">
|
||||
<span class="absolute i-lucide-search size-3.5 top-2 left-3" />
|
||||
<input
|
||||
@@ -135,7 +135,7 @@ const hasNoUsedAttributes = computed(() => usedAttributes.value.length === 0);
|
||||
:placeholder="
|
||||
t('CONTACTS_LAYOUT.SIDEBAR.ATTRIBUTES.SEARCH_PLACEHOLDER')
|
||||
"
|
||||
class="w-full h-8 py-2 pl-10 pr-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
|
||||
class="w-full h-8 py-2 ltr:pl-10 ltr:pr-2 rtl:pr-10 rtl:pl-2 mb-0 reset-base outline-n-weak outline-1 outline hover:outline-n-slate-6 text-sm border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12 outline-offset-0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ const contactConversations = computed(() =>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="contactConversations.length > 0"
|
||||
class="px-6 py-4 divide-y divide-n-weak"
|
||||
class="px-6 pt-4 pb-6 divide-y divide-n-weak"
|
||||
>
|
||||
<ConversationCard
|
||||
v-for="conversation in contactConversations"
|
||||
|
||||
+10
-11
@@ -103,12 +103,12 @@ const onMergeContacts = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-8 px-6 py-6">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h4 class="text-base text-n-slate-12">
|
||||
<div class="flex flex-col gap-6 px-5 pb-6 pt-4">
|
||||
<div class="flex flex-col gap-2 mt-4">
|
||||
<h4 class="text-base font-medium text-n-slate-12">
|
||||
{{ t('CONTACTS_LAYOUT.SIDEBAR.MERGE.TITLE') }}
|
||||
</h4>
|
||||
<p class="text-sm text-n-slate-11">
|
||||
<p class="text-sm font-420 text-n-slate-11">
|
||||
{{ t('CONTACTS_LAYOUT.SIDEBAR.MERGE.DESCRIPTION') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -126,13 +126,6 @@ const onMergeContacts = async () => {
|
||||
@search="onContactSearch"
|
||||
/>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<Button
|
||||
variant="faded"
|
||||
color="slate"
|
||||
:label="t('CONTACTS_LAYOUT.SIDEBAR.MERGE.BUTTONS.CANCEL')"
|
||||
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3"
|
||||
@click="resetState"
|
||||
/>
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.SIDEBAR.MERGE.BUTTONS.CONFIRM')"
|
||||
class="w-full"
|
||||
@@ -140,6 +133,12 @@ const onMergeContacts = async () => {
|
||||
:disabled="isMergingContact"
|
||||
@click="onMergeContacts"
|
||||
/>
|
||||
<Button
|
||||
slate
|
||||
:label="t('CONTACTS_LAYOUT.SIDEBAR.MERGE.BUTTONS.CANCEL')"
|
||||
class="w-full"
|
||||
@click="resetState"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -55,12 +55,12 @@ useKeyboardEvents(keyboardEvents);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-6 py-6">
|
||||
<div class="flex flex-col gap-4 pt-4 pb-6">
|
||||
<Editor
|
||||
v-model="state.message"
|
||||
:placeholder="t('CONTACTS_LAYOUT.SIDEBAR.NOTES.PLACEHOLDER')"
|
||||
focus-on-mount
|
||||
class="[&>div]:!border-transparent [&>div]:px-4 [&>div]:py-4 px-6"
|
||||
class="[&>div]:px-4 [&>div]:py-4 px-5"
|
||||
>
|
||||
<template #actions>
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -87,7 +87,7 @@ useKeyboardEvents(keyboardEvents);
|
||||
<ContactNoteItem
|
||||
v-for="note in notes"
|
||||
:key="note.id"
|
||||
class="mx-6 py-4"
|
||||
class="mx-5 py-4"
|
||||
:note="note"
|
||||
:written-by="getWrittenBy(note)"
|
||||
allow-delete
|
||||
|
||||
@@ -11,6 +11,7 @@ import ContactLabels from 'dashboard/components-next/Contacts/ContactLabels/Cont
|
||||
import ContactsForm from 'dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue';
|
||||
import ConfirmContactDeleteDialog from 'dashboard/components-next/Contacts/ContactsForm/ConfirmContactDeleteDialog.vue';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
selectedContact: {
|
||||
@@ -121,17 +122,17 @@ const handleAvatarDelete = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-start gap-8 pb-6">
|
||||
<div class="flex flex-col items-start gap-3">
|
||||
<div class="flex flex-col items-start gap-6 pb-6 w-full">
|
||||
<div class="flex flex-col items-start gap-3 w-full">
|
||||
<Avatar
|
||||
:src="avatarSrc || ''"
|
||||
:name="selectedContact?.name || ''"
|
||||
:size="72"
|
||||
:size="64"
|
||||
allow-upload
|
||||
@upload="handleAvatarUpload"
|
||||
@delete="handleAvatarDelete"
|
||||
/>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-base font-medium text-n-slate-12">
|
||||
{{ selectedContact?.name }}
|
||||
</h3>
|
||||
@@ -140,27 +141,41 @@ const handleAvatarDelete = async () => {
|
||||
v-if="selectedContact?.identifier"
|
||||
class="inline-flex items-center gap-1 text-sm text-n-slate-11"
|
||||
>
|
||||
<span class="i-ph-user-gear text-n-slate-10 size-4" />
|
||||
<Icon
|
||||
icon="i-ph-user-gear"
|
||||
class="text-n-slate-10 size-4 flex-shrink-0"
|
||||
/>
|
||||
{{ selectedContact?.identifier }}
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1 text-sm text-n-slate-11">
|
||||
<span
|
||||
<span class="inline-flex items-start gap-1 text-sm text-n-slate-11">
|
||||
<Icon
|
||||
v-if="selectedContact?.identifier"
|
||||
class="i-ph-activity text-n-slate-10 size-4"
|
||||
icon="i-ph-activity"
|
||||
class="text-n-slate-10 size-4 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
{{ $t('CONTACTS_LAYOUT.DETAILS.CREATED_AT', { date: createdAt }) }}
|
||||
•
|
||||
{{
|
||||
$t('CONTACTS_LAYOUT.DETAILS.LAST_ACTIVITY', {
|
||||
date: lastActivityAt,
|
||||
})
|
||||
}}
|
||||
<div
|
||||
class="flex flex-col sm:flex-row sm:inline-flex items-start sm:items-center gap-1 sm:gap-3"
|
||||
>
|
||||
<span>
|
||||
{{
|
||||
$t('CONTACTS_LAYOUT.DETAILS.CREATED_AT', { date: createdAt })
|
||||
}}
|
||||
</span>
|
||||
<div class="w-px h-3 rounded-xl bg-n-weak hidden sm:block" />
|
||||
<span>
|
||||
{{
|
||||
$t('CONTACTS_LAYOUT.DETAILS.LAST_ACTIVITY', {
|
||||
date: lastActivityAt,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ContactLabels :contact-id="selectedContact?.id" />
|
||||
</div>
|
||||
<div class="flex flex-col items-start gap-6">
|
||||
<div class="flex flex-col items-start gap-6 w-full">
|
||||
<ContactsForm
|
||||
ref="contactsFormRef"
|
||||
:contact-data="contactData"
|
||||
@@ -175,21 +190,23 @@ const handleAvatarDelete = async () => {
|
||||
@click="updateContact"
|
||||
/>
|
||||
</div>
|
||||
<Policy :permissions="['administrator']">
|
||||
<Policy :permissions="['administrator']" class="w-full">
|
||||
<div class="w-full h-px bg-n-slate-3 mt-1 mb-7" />
|
||||
<div
|
||||
class="flex flex-col items-start w-full gap-4 pt-6 border-t border-n-strong"
|
||||
class="flex items-end justify-between w-full gap-4 outline outline-1 outline-n-weak rounded-2xl p-4"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<h6 class="text-base font-medium text-n-slate-12">
|
||||
{{ t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT') }}
|
||||
</h6>
|
||||
<span class="text-sm text-n-slate-11">
|
||||
<span class="text-sm font-420 text-n-slate-11">
|
||||
{{ t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT_DESCRIPTION') }}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT')"
|
||||
color="ruby"
|
||||
ruby
|
||||
class="flex-shrink-0"
|
||||
@click="openConfirmDeleteContactDialog"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -101,7 +101,6 @@ const onPickInbox = async inbox => {
|
||||
:label="label"
|
||||
:icon="icon"
|
||||
:size="size"
|
||||
class="!px-2"
|
||||
@click="onClick"
|
||||
/>
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ const handleAttributeAction = async action => {
|
||||
:menu-items="attributeListMenuItems"
|
||||
show-search
|
||||
class="w-48 mt-2 top-full ltr:right-0 rtl:left-0"
|
||||
@click.stop
|
||||
@action="handleAttributeAction($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Flag from 'dashboard/components-next/flag/Flag.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -27,6 +29,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const modelValue = defineModel({
|
||||
@@ -67,7 +73,6 @@ const countryList = computed(() => {
|
||||
value: country.id,
|
||||
label: country.name,
|
||||
dialCode: country.dial_code,
|
||||
emoji: country.emoji,
|
||||
isSelected: String(activeCountryCode.value) === String(country.id),
|
||||
action: 'phoneNumberInput',
|
||||
}));
|
||||
@@ -163,39 +168,56 @@ watch(
|
||||
<div>
|
||||
<div
|
||||
v-on-clickaway="() => closeCountryDropdown()"
|
||||
class="relative flex items-center h-8 transition-all duration-500 ease-in-out outline outline-1 outline-offset-[-1px] rounded-lg bg-n-alpha-black2"
|
||||
:class="[inputBorderClass, { 'cursor-not-allowed opacity-50': disabled }]"
|
||||
class="relative flex items-center transition-all duration-500 ease-in-out outline outline-1 outline-offset-[-1px] rounded-lg bg-n-alpha-black2"
|
||||
:class="[
|
||||
inputBorderClass,
|
||||
{
|
||||
'cursor-not-allowed opacity-50': disabled,
|
||||
'h-8': compact,
|
||||
'h-10': !compact,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Input
|
||||
v-model="phoneNumber"
|
||||
type="tel"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
custom-input-class="!border-0 !outline-none h-8 !py-0.5 !bg-transparent ltr:!pl-1 rtl:!pr-1"
|
||||
custom-input-class="!border-0 !outline-none !py-0.5 !bg-transparent ltr:!pl-1 rtl:!pr-1"
|
||||
class="w-full !flex-row"
|
||||
:class="{
|
||||
'[&>input]:!h-8': compact,
|
||||
'[&>input]:h-10': !compact,
|
||||
}"
|
||||
>
|
||||
<template #prefix>
|
||||
<div class="flex items-center flex-shrink-0">
|
||||
<Button
|
||||
:label="activeCountry?.emoji || ''"
|
||||
color="slate"
|
||||
size="sm"
|
||||
:icon="
|
||||
!activeCountry ? 'i-lucide-globe' : 'i-lucide-chevron-down'
|
||||
"
|
||||
trailing-icon
|
||||
slate
|
||||
sm
|
||||
:icon="!activeCountry ? 'i-lucide-globe' : ''"
|
||||
:disabled="disabled"
|
||||
no-animation
|
||||
type="button"
|
||||
class="!h-[1.875rem] top-1 ltr:ml-px rtl:mr-px !px-2 outline-0 !outline-none !rounded-lg border-0 ltr:!rounded-r-none rtl:!rounded-l-none"
|
||||
class="top-1 ltr:ml-px rtl:mr-px !px-2 outline-0 !outline-none !rounded-lg border-0 ltr:!rounded-r-none rtl:!rounded-l-none flex-shrink-0"
|
||||
:class="{
|
||||
'!h-[1.875rem]': compact,
|
||||
'!h-[2.375rem]': !compact,
|
||||
}"
|
||||
@click="toggleCountryDropdown"
|
||||
>
|
||||
<span
|
||||
v-if="activeCountry"
|
||||
class="inline-flex justify-center text-sm whitespace-nowrap"
|
||||
>
|
||||
{{ activeCountry?.emoji }}
|
||||
</span>
|
||||
<template v-if="activeCountry" #icon>
|
||||
<Flag :country="activeCountry.value" class="size-3" />
|
||||
</template>
|
||||
|
||||
<Icon
|
||||
:icon="
|
||||
!showDropdown
|
||||
? 'i-lucide-chevron-down'
|
||||
: 'i-lucide-chevron-up'
|
||||
"
|
||||
class="flex-shrink-0 text-n-slate-11"
|
||||
/>
|
||||
</Button>
|
||||
<span
|
||||
v-if="activeCountry"
|
||||
@@ -212,7 +234,11 @@ watch(
|
||||
show-search
|
||||
class="z-[100] w-48 mt-2 overflow-y-auto ltr:left-0 rtl:right-0 top-full max-h-52"
|
||||
@action="onSelectCountry"
|
||||
/>
|
||||
>
|
||||
<template #icon="{ item }">
|
||||
<Flag :country="item.value" class="size-3" />
|
||||
</template>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<template v-if="phoneNumberError">
|
||||
<p
|
||||
|
||||
@@ -560,7 +560,7 @@
|
||||
"PRIMARY": "Primary contact",
|
||||
"PRIMARY_HELP_LABEL": "To be saved",
|
||||
"PRIMARY_REQUIRED_ERROR": "Please select a contact to merge with before proceeding",
|
||||
"PARENT": "To be merged",
|
||||
"PARENT": "Contact to be merged",
|
||||
"PARENT_HELP_LABEL": "To be deleted",
|
||||
"EMPTY_STATE": "No contacts found",
|
||||
"PLACEHOLDER": "Search for primary contact",
|
||||
|
||||
@@ -150,7 +150,7 @@ onMounted(() => {
|
||||
@go-to-contacts-list="goToContactsList"
|
||||
/>
|
||||
<template #sidebar>
|
||||
<div class="px-6">
|
||||
<div class="px-5">
|
||||
<TabBar
|
||||
:tabs="tabs"
|
||||
:initial-active-tab="activeTabIndex"
|
||||
|
||||
Reference in New Issue
Block a user