feat: Add Contact card and form component (#10466)
Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||||
|
import ContactsForm from 'dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: { type: Number, required: true },
|
||||||
|
name: { type: String, default: '' },
|
||||||
|
email: { type: String, default: '' },
|
||||||
|
additionalAttributes: { type: Object, default: () => ({}) },
|
||||||
|
phoneNumber: { type: String, default: '' },
|
||||||
|
thumbnail: { type: String, default: '' },
|
||||||
|
isExpanded: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['toggle', 'updateContact', 'showContact']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const handleFormUpdate = updatedData => {
|
||||||
|
emit('updateContact', { id: props.id, updatedData });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickViewDetails = async () => {
|
||||||
|
emit('showContact', props.id);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CardLayout :key="id" layout="row">
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<Avatar :name="name" :src="thumbnail" :size="48" rounded-full />
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm font-medium truncate text-n-slate-12">
|
||||||
|
{{ name }}
|
||||||
|
</span>
|
||||||
|
<template v-if="additionalAttributes?.companyName">
|
||||||
|
<span class="text-sm text-n-slate-11">
|
||||||
|
{{ t('CONTACTS_LAYOUT.CARD.OF') }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-medium truncate text-n-slate-12">
|
||||||
|
{{ additionalAttributes.companyName }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span v-if="email" class="text-sm text-n-slate-11">{{ email }}</span>
|
||||||
|
<div v-if="email" class="w-px h-3 bg-n-slate-6" />
|
||||||
|
<span v-if="phoneNumber" class="text-sm text-n-slate-11">
|
||||||
|
{{ phoneNumber }}
|
||||||
|
</span>
|
||||||
|
<div v-if="phoneNumber" class="w-px h-3 bg-n-slate-6" />
|
||||||
|
<Button
|
||||||
|
:label="t('CONTACTS_LAYOUT.CARD.VIEW_DETAILS')"
|
||||||
|
variant="link"
|
||||||
|
size="xs"
|
||||||
|
@click="onClickViewDetails"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
icon="i-lucide-chevron-down"
|
||||||
|
variant="ghost"
|
||||||
|
color="slate"
|
||||||
|
size="xs"
|
||||||
|
:class="{ 'rotate-180': isExpanded }"
|
||||||
|
@click="emit('toggle')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template #after>
|
||||||
|
<transition
|
||||||
|
enter-active-class="overflow-hidden transition-all duration-300 ease-out"
|
||||||
|
leave-active-class="overflow-hidden transition-all duration-300 ease-in"
|
||||||
|
enter-from-class="overflow-hidden opacity-0 max-h-0"
|
||||||
|
enter-to-class="opacity-100 max-h-[360px]"
|
||||||
|
leave-from-class="opacity-100 max-h-[360px]"
|
||||||
|
leave-to-class="overflow-hidden opacity-0 max-h-0"
|
||||||
|
>
|
||||||
|
<div v-show="isExpanded" class="w-full">
|
||||||
|
<div class="p-6 border-t border-n-strong">
|
||||||
|
<ContactsForm
|
||||||
|
:contact-data="{
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
phoneNumber,
|
||||||
|
additionalAttributes,
|
||||||
|
}"
|
||||||
|
@update="handleFormUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
</CardLayout>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import ContactsCard from '../ContactsCard.vue';
|
||||||
|
import contacts from './fixtures';
|
||||||
|
|
||||||
|
const expandedCardId = ref(null);
|
||||||
|
|
||||||
|
const toggleExpanded = id => {
|
||||||
|
expandedCardId.value = expandedCardId.value === id ? null : id;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Components/Contacts/ContactsCard"
|
||||||
|
:layout="{ type: 'grid', width: '800px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Default with expandable function">
|
||||||
|
<div class="flex flex-col p-4">
|
||||||
|
<ContactsCard
|
||||||
|
v-bind="contacts[0]"
|
||||||
|
:is-expanded="expandedCardId === contacts[0].id"
|
||||||
|
@toggle="toggleExpanded(contacts[0].id)"
|
||||||
|
@update-contact="() => {}"
|
||||||
|
@show-contact="() => {}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="With Company Name and without phone number">
|
||||||
|
<div class="flex flex-col p-4">
|
||||||
|
<ContactsCard
|
||||||
|
v-bind="{ ...contacts[1], phoneNumber: '' }"
|
||||||
|
:is-expanded="false"
|
||||||
|
@toggle="() => {}"
|
||||||
|
@update-contact="() => {}"
|
||||||
|
@show-contact="() => {}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Expanded State">
|
||||||
|
<div class="flex flex-col p-4">
|
||||||
|
<ContactsCard
|
||||||
|
v-bind="contacts[2]"
|
||||||
|
is-expanded
|
||||||
|
@toggle="() => {}"
|
||||||
|
@update-contact="() => {}"
|
||||||
|
@show-contact="() => {}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Without Email and Phone">
|
||||||
|
<div class="flex flex-col p-4">
|
||||||
|
<ContactsCard
|
||||||
|
v-bind="{ ...contacts[3], email: '', phoneNumber: '' }"
|
||||||
|
:is-expanded="false"
|
||||||
|
@toggle="() => {}"
|
||||||
|
@update-contact="() => {}"
|
||||||
|
@show-contact="() => {}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
additionalAttributes: {
|
||||||
|
socialProfiles: {},
|
||||||
|
},
|
||||||
|
availabilityStatus: null,
|
||||||
|
email: 'johndoe@chatwoot.com',
|
||||||
|
id: 370,
|
||||||
|
name: 'John Doe',
|
||||||
|
phoneNumber: '+918634322418',
|
||||||
|
identifier: null,
|
||||||
|
thumbnail: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Felix',
|
||||||
|
customAttributes: {},
|
||||||
|
lastActivityAt: 1731608270,
|
||||||
|
createdAt: 1731586271,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
additionalAttributes: {
|
||||||
|
city: 'kerala',
|
||||||
|
country: 'India',
|
||||||
|
description: 'Curious about the web. ',
|
||||||
|
companyName: 'Chatwoot',
|
||||||
|
countryCode: '',
|
||||||
|
socialProfiles: {
|
||||||
|
github: 'abozler',
|
||||||
|
twitter: 'ozler',
|
||||||
|
facebook: 'abozler',
|
||||||
|
linkedin: 'abozler',
|
||||||
|
instagram: 'ozler',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
availabilityStatus: null,
|
||||||
|
email: 'ozler@chatwoot.com',
|
||||||
|
id: 29,
|
||||||
|
name: 'Abraham Ozlers',
|
||||||
|
phoneNumber: '+246232222222',
|
||||||
|
identifier: null,
|
||||||
|
thumbnail: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Upload',
|
||||||
|
customAttributes: {
|
||||||
|
dateContact: '2024-02-01T00:00:00.000Z',
|
||||||
|
linkContact: 'https://staging.chatwoot.com/app/accounts/3/contacts-new',
|
||||||
|
listContact: 'Not spam',
|
||||||
|
numberContact: '12',
|
||||||
|
},
|
||||||
|
lastActivityAt: 1712127410,
|
||||||
|
createdAt: 1712127389,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
additionalAttributes: {
|
||||||
|
city: 'Kerala',
|
||||||
|
country: 'India',
|
||||||
|
description:
|
||||||
|
"I'm Candice developer focusing on building things for the web 🌍. Currently, I’m working as a Product Developer here at @chatwootapp ⚡️🔥",
|
||||||
|
companyName: 'Chatwoot',
|
||||||
|
countryCode: 'IN',
|
||||||
|
socialProfiles: {
|
||||||
|
github: 'cmathersonj',
|
||||||
|
twitter: 'cmather',
|
||||||
|
facebook: 'cmathersonj',
|
||||||
|
linkedin: 'cmathersonj',
|
||||||
|
instagram: 'cmathersonjs',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
availabilityStatus: null,
|
||||||
|
email: 'cmathersonj@va.test',
|
||||||
|
id: 22,
|
||||||
|
name: 'Candice Matherson',
|
||||||
|
phoneNumber: '+917474774742',
|
||||||
|
identifier: null,
|
||||||
|
thumbnail: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Emery',
|
||||||
|
customAttributes: {
|
||||||
|
dateContact: '2024-11-12T03:23:06.963Z',
|
||||||
|
linkContact: 'https://sd.sd',
|
||||||
|
textContact: 'hey',
|
||||||
|
numberContact: '12',
|
||||||
|
checkboxContact: true,
|
||||||
|
},
|
||||||
|
lastActivityAt: 1712123233,
|
||||||
|
createdAt: 1712123233,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
additionalAttributes: {
|
||||||
|
city: '',
|
||||||
|
country: '',
|
||||||
|
description: '',
|
||||||
|
companyName: '',
|
||||||
|
countryCode: '',
|
||||||
|
socialProfiles: {
|
||||||
|
github: '',
|
||||||
|
twitter: '',
|
||||||
|
facebook: '',
|
||||||
|
linkedin: '',
|
||||||
|
instagram: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
availabilityStatus: null,
|
||||||
|
email: 'ofolkardi@taobao.test',
|
||||||
|
id: 21,
|
||||||
|
name: 'Ophelia Folkard',
|
||||||
|
phoneNumber: '',
|
||||||
|
identifier: null,
|
||||||
|
thumbnail:
|
||||||
|
'https://sivin-tunnel.chatwoot.dev/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBPZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--08dcac8eb72ef12b2cad92d58dddd04cd8a5f513/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--df796c2af3c0153e55236c2f3cf3a199ac2cb6f7/32.jpg',
|
||||||
|
customAttributes: {},
|
||||||
|
lastActivityAt: 1712123233,
|
||||||
|
createdAt: 1712123233,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
additionalAttributes: {
|
||||||
|
socialProfiles: {},
|
||||||
|
},
|
||||||
|
availabilityStatus: null,
|
||||||
|
email: 'wcasteloth@exblog.jp',
|
||||||
|
id: 20,
|
||||||
|
name: 'Willy Castelot',
|
||||||
|
phoneNumber: '+919384',
|
||||||
|
identifier: null,
|
||||||
|
thumbnail: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Jade',
|
||||||
|
customAttributes: {},
|
||||||
|
lastActivityAt: 1712123233,
|
||||||
|
createdAt: 1712123233,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
additionalAttributes: {
|
||||||
|
city: '',
|
||||||
|
country: '',
|
||||||
|
description: '',
|
||||||
|
companyName: '',
|
||||||
|
countryCode: '',
|
||||||
|
socialProfiles: {
|
||||||
|
github: '',
|
||||||
|
twitter: '',
|
||||||
|
facebook: '',
|
||||||
|
linkedin: '',
|
||||||
|
instagram: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
availabilityStatus: null,
|
||||||
|
email: 'ederingtong@printfriendly.test',
|
||||||
|
id: 19,
|
||||||
|
name: 'Elisabeth Derington',
|
||||||
|
phoneNumber: '',
|
||||||
|
identifier: null,
|
||||||
|
thumbnail: 'https://api.dicebear.com/9.x/avataaars/svg?seed=Jade',
|
||||||
|
customAttributes: {},
|
||||||
|
lastActivityAt: 1712123232,
|
||||||
|
createdAt: 1712123232,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, reactive, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { required, email, minLength } from '@vuelidate/validators';
|
||||||
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
|
||||||
|
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 Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
|
import PhoneNumberInput from 'dashboard/components-next/phonenumberinput/PhoneNumberInput.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
contactData: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
isDetailsView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isNewContact: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const FORM_CONFIG = {
|
||||||
|
FIRST_NAME: { field: 'firstName' },
|
||||||
|
LAST_NAME: { field: 'lastName' },
|
||||||
|
EMAIL_ADDRESS: { field: 'email' },
|
||||||
|
PHONE_NUMBER: { field: 'phoneNumber' },
|
||||||
|
CITY: { field: 'additionalAttributes.city' },
|
||||||
|
COUNTRY: { field: 'additionalAttributes.country' },
|
||||||
|
BIO: { field: 'additionalAttributes.description' },
|
||||||
|
COMPANY_NAME: { field: 'additionalAttributes.companyName' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const SOCIAL_CONFIG = {
|
||||||
|
FACEBOOK: 'i-ri-facebook-circle-fill',
|
||||||
|
GITHUB: 'i-ri-github-fill',
|
||||||
|
INSTAGRAM: 'i-ri-instagram-line',
|
||||||
|
LINKEDIN: 'i-ri-linkedin-box-fill',
|
||||||
|
TWITTER: 'i-ri-twitter-x-fill',
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
phoneNumber: '',
|
||||||
|
additionalAttributes: {
|
||||||
|
description: '',
|
||||||
|
companyName: '',
|
||||||
|
countryCode: '',
|
||||||
|
country: '',
|
||||||
|
city: '',
|
||||||
|
socialProfiles: {
|
||||||
|
facebook: '',
|
||||||
|
github: '',
|
||||||
|
instagram: '',
|
||||||
|
linkedin: '',
|
||||||
|
twitter: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive({ ...defaultState });
|
||||||
|
|
||||||
|
const validationRules = {
|
||||||
|
firstName: { required, minLength: minLength(2) },
|
||||||
|
email: { email },
|
||||||
|
};
|
||||||
|
|
||||||
|
const v$ = useVuelidate(validationRules, state);
|
||||||
|
|
||||||
|
const prepareStateBasedOnProps = () => {
|
||||||
|
if (props.isNewContact) {
|
||||||
|
return; // Added to prevent state update for new contact form
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name = '',
|
||||||
|
email: emailAddress,
|
||||||
|
phoneNumber,
|
||||||
|
additionalAttributes = {},
|
||||||
|
} = props.contactData || {};
|
||||||
|
|
||||||
|
const [firstName = '', lastName = ''] = name.split(' ');
|
||||||
|
const {
|
||||||
|
description,
|
||||||
|
companyName,
|
||||||
|
countryCode,
|
||||||
|
country,
|
||||||
|
city,
|
||||||
|
socialProfiles = {},
|
||||||
|
} = additionalAttributes || {};
|
||||||
|
|
||||||
|
Object.assign(state, {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email: emailAddress,
|
||||||
|
phoneNumber,
|
||||||
|
additionalAttributes: {
|
||||||
|
description,
|
||||||
|
companyName,
|
||||||
|
countryCode,
|
||||||
|
country,
|
||||||
|
city,
|
||||||
|
socialProfiles,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const countryOptions = computed(() =>
|
||||||
|
countries.map(({ name }) => ({ label: name, value: name }))
|
||||||
|
);
|
||||||
|
|
||||||
|
const editDetailsForm = computed(() =>
|
||||||
|
Object.keys(FORM_CONFIG).map(key => ({
|
||||||
|
key,
|
||||||
|
placeholder: t(
|
||||||
|
`CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.FORM.${key}.PLACEHOLDER`
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const socialProfilesForm = computed(() =>
|
||||||
|
Object.entries(SOCIAL_CONFIG).map(([key, icon]) => ({
|
||||||
|
key,
|
||||||
|
placeholder: t(`CONTACTS_LAYOUT.CARD.SOCIAL_MEDIA.FORM.${key}.PLACEHOLDER`),
|
||||||
|
icon,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const isValidationField = key => {
|
||||||
|
const field = FORM_CONFIG[key]?.field;
|
||||||
|
return ['firstName', 'email'].includes(field);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getValidationKey = key => {
|
||||||
|
return FORM_CONFIG[key]?.field;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Creates a computed property for two-way form field binding
|
||||||
|
const getFormBinding = key => {
|
||||||
|
const field = FORM_CONFIG[key]?.field;
|
||||||
|
if (!field) return null;
|
||||||
|
|
||||||
|
return computed({
|
||||||
|
get: () => {
|
||||||
|
// Handle firstName/lastName fields
|
||||||
|
if (field === 'firstName' || field === 'lastName') {
|
||||||
|
return state[field]?.toString() || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle nested vs non-nested fields
|
||||||
|
const [base, nested] = field.split('.');
|
||||||
|
// Example: 'email' → state.email
|
||||||
|
// Example: 'additionalAttributes.city' → state.additionalAttributes.city
|
||||||
|
return (nested ? state[base][nested] : state[base])?.toString() || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
set: async value => {
|
||||||
|
// Handle name fields specially to maintain the combined 'name' field
|
||||||
|
if (field === 'firstName' || field === 'lastName') {
|
||||||
|
state[field] = value;
|
||||||
|
// Example: firstName="John", lastName="Doe" → name="John Doe"
|
||||||
|
state.name = `${state.firstName} ${state.lastName}`.trim();
|
||||||
|
} else {
|
||||||
|
// Handle nested vs non-nested fields
|
||||||
|
const [base, nested] = field.split('.');
|
||||||
|
if (nested) {
|
||||||
|
// Example: additionalAttributes.city = "New York"
|
||||||
|
state[base][nested] = value;
|
||||||
|
} else {
|
||||||
|
// Example: email = "test@example.com"
|
||||||
|
state[base] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFormValid = await v$.value.$validate();
|
||||||
|
if (isFormValid) {
|
||||||
|
const { firstName, lastName, ...stateWithoutNames } = state;
|
||||||
|
emit('update', stateWithoutNames);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMessageType = key => {
|
||||||
|
return isValidationField(key) && v$.value[getValidationKey(key)]?.$error
|
||||||
|
? 'error'
|
||||||
|
: 'info';
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => props.contactData, prepareStateBasedOnProps, {
|
||||||
|
immediate: true,
|
||||||
|
deep: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expose state to parent component for avatar upload
|
||||||
|
defineExpose({
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
</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">
|
||||||
|
{{ t('CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.TITLE') }}
|
||||||
|
</span>
|
||||||
|
<div class="grid w-full grid-cols-2 gap-4">
|
||||||
|
<template v-for="item in editDetailsForm" :key="item.key">
|
||||||
|
<ComboBox
|
||||||
|
v-if="item.key === 'COUNTRY'"
|
||||||
|
v-model="state.additionalAttributes.country"
|
||||||
|
:options="countryOptions"
|
||||||
|
:placeholder="item.placeholder"
|
||||||
|
class="[&>div>button]:h-8"
|
||||||
|
:class="{
|
||||||
|
'[&>div>button]:bg-n-alpha-black2 [&>div>button]:!outline-transparent':
|
||||||
|
!isDetailsView,
|
||||||
|
'[&>div>button]:!outline-n-weak [&>div>button]:hover:!outline-n-strong [&>div>button]:!bg-n-alpha-black2':
|
||||||
|
isDetailsView,
|
||||||
|
}"
|
||||||
|
@update:model-value="emit('update', state)"
|
||||||
|
/>
|
||||||
|
<PhoneNumberInput
|
||||||
|
v-else-if="item.key === 'PHONE_NUMBER'"
|
||||||
|
v-model="getFormBinding(item.key).value"
|
||||||
|
:placeholder="item.placeholder"
|
||||||
|
:show-border="isDetailsView"
|
||||||
|
/>
|
||||||
|
<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 ${
|
||||||
|
!isDetailsView ? '[&:not(.error)]:!border-transparent' : ''
|
||||||
|
}`"
|
||||||
|
class="w-full"
|
||||||
|
@input="
|
||||||
|
isValidationField(item.key) &&
|
||||||
|
v$[getValidationKey(item.key)].$touch()
|
||||||
|
"
|
||||||
|
@blur="
|
||||||
|
isValidationField(item.key) &&
|
||||||
|
v$[getValidationKey(item.key)].$touch()
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-start gap-2">
|
||||||
|
<span class="py-1 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="{
|
||||||
|
'bg-n-alpha-2 dark:bg-n-solid-2': isDetailsView,
|
||||||
|
'bg-n-alpha-2 dark:bg-n-solid-3': !isDetailsView,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
:icon="item.icon"
|
||||||
|
class="flex-shrink-0 text-n-slate-11 size-4"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="
|
||||||
|
state.additionalAttributes.socialProfiles[item.key.toLowerCase()]
|
||||||
|
"
|
||||||
|
class="w-auto min-w-[100px] text-sm bg-transparent reset-base text-n-slate-12 dark:text-n-slate-12 placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10"
|
||||||
|
:placeholder="item.placeholder"
|
||||||
|
:size="item.placeholder.length"
|
||||||
|
@input="emit('update', state)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<script setup>
|
||||||
|
import ContactsForm from '../ContactsForm.vue';
|
||||||
|
import contactData from './fixtures';
|
||||||
|
|
||||||
|
const handleUpdate = updatedData => {
|
||||||
|
console.log('Form updated:', updatedData);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Components/Contacts/ContactsForm"
|
||||||
|
:layout="{ type: 'grid', width: '600px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Default without border">
|
||||||
|
<div class="p-6 border rounded-lg border-n-strong">
|
||||||
|
<ContactsForm :contact-data="contactData" @update="handleUpdate" />
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Details View with border">
|
||||||
|
<div class="p-6 border rounded-lg border-n-strong">
|
||||||
|
<ContactsForm
|
||||||
|
:contact-data="contactData"
|
||||||
|
is-details-view
|
||||||
|
@update="handleUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Minimal Data">
|
||||||
|
<div class="p-6 border rounded-lg border-n-strong">
|
||||||
|
<ContactsForm
|
||||||
|
:contact-data="{
|
||||||
|
id: 21,
|
||||||
|
name: 'Ophelia Folkard',
|
||||||
|
email: 'ofolkardi@taobao.test',
|
||||||
|
phoneNumber: '',
|
||||||
|
additionalAttributes: {
|
||||||
|
city: '',
|
||||||
|
country: '',
|
||||||
|
description: '',
|
||||||
|
companyName: '',
|
||||||
|
countryCode: '',
|
||||||
|
socialProfiles: {
|
||||||
|
github: '',
|
||||||
|
twitter: '',
|
||||||
|
facebook: '',
|
||||||
|
linkedin: '',
|
||||||
|
instagram: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
@update="handleUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="With All Social Profiles">
|
||||||
|
<div class="p-6 border rounded-lg border-n-strong">
|
||||||
|
<ContactsForm
|
||||||
|
:contact-data="{
|
||||||
|
...contactData,
|
||||||
|
additionalAttributes: {
|
||||||
|
...contactData.additionalAttributes,
|
||||||
|
socialProfiles: {
|
||||||
|
github: 'cmathersonj',
|
||||||
|
twitter: 'cmather',
|
||||||
|
facebook: 'cmathersonj',
|
||||||
|
linkedin: 'cmathersonj',
|
||||||
|
instagram: 'cmathersonjs',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
@update="handleUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export default {
|
||||||
|
id: 370,
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'johndoe@chatwoot.com',
|
||||||
|
phoneNumber: '+918634322418',
|
||||||
|
additionalAttributes: {
|
||||||
|
city: 'Kerala',
|
||||||
|
country: 'India',
|
||||||
|
description: 'Curious about the web.',
|
||||||
|
companyName: 'Chatwoot',
|
||||||
|
countryCode: 'IN',
|
||||||
|
socialProfiles: {
|
||||||
|
github: 'johndoe',
|
||||||
|
twitter: 'johndoe',
|
||||||
|
facebook: 'johndoe',
|
||||||
|
linkedin: 'johndoe',
|
||||||
|
instagram: 'johndoe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -385,5 +385,61 @@
|
|||||||
"DROPDOWN_ITEM": {
|
"DROPDOWN_ITEM": {
|
||||||
"ID": "(ID: {identifier})"
|
"ID": "(ID: {identifier})"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"CONTACTS_LAYOUT": {
|
||||||
|
"CARD": {
|
||||||
|
"OF": "of",
|
||||||
|
"VIEW_DETAILS": "View details",
|
||||||
|
"EDIT_DETAILS_FORM": {
|
||||||
|
"TITLE": "Edit contact details",
|
||||||
|
"FORM": {
|
||||||
|
"FIRST_NAME": {
|
||||||
|
"PLACEHOLDER": "Enter the first name"
|
||||||
|
},
|
||||||
|
"LAST_NAME": {
|
||||||
|
"PLACEHOLDER": "Enter the last name"
|
||||||
|
},
|
||||||
|
"EMAIL_ADDRESS": {
|
||||||
|
"PLACEHOLDER": "Enter the email address"
|
||||||
|
},
|
||||||
|
"PHONE_NUMBER": {
|
||||||
|
"PLACEHOLDER": "Enter the phone number"
|
||||||
|
},
|
||||||
|
"CITY": {
|
||||||
|
"PLACEHOLDER": "Enter the city name"
|
||||||
|
},
|
||||||
|
"COUNTRY": {
|
||||||
|
"PLACEHOLDER": "Select country"
|
||||||
|
},
|
||||||
|
"BIO": {
|
||||||
|
"PLACEHOLDER": "Enter the bio"
|
||||||
|
},
|
||||||
|
"COMPANY_NAME": {
|
||||||
|
"PLACEHOLDER": "Enter the company name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SOCIAL_MEDIA": {
|
||||||
|
"TITLE": "Edit social links",
|
||||||
|
"FORM": {
|
||||||
|
"FACEBOOK": {
|
||||||
|
"PLACEHOLDER": "Add Facebook"
|
||||||
|
},
|
||||||
|
"GITHUB": {
|
||||||
|
"PLACEHOLDER": "Add Github"
|
||||||
|
},
|
||||||
|
"INSTAGRAM": {
|
||||||
|
"PLACEHOLDER": "Add Instagram"
|
||||||
|
},
|
||||||
|
"LINKEDIN": {
|
||||||
|
"PLACEHOLDER": "Add LinkedIn"
|
||||||
|
},
|
||||||
|
"TWITTER": {
|
||||||
|
"PLACEHOLDER": "Add Twitter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user