chore: Update display property design

This commit is contained in:
iamsivin
2026-01-22 18:18:11 +05:30
parent 077ce63c8f
commit 48ae6e5381
6 changed files with 191 additions and 181 deletions
@@ -3,7 +3,7 @@ import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'dashboard/composables/store';
import ContactCardForm from 'dashboard/components-next/Contacts/ContactsCard/ContactCardForm.vue';
import ContactCardOverview from 'dashboard/components-next/Contacts/ContactsCard/ContactCardOverview.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import Flag from 'dashboard/components-next/flag/Flag.vue';
@@ -365,7 +365,7 @@ const handleExpandHover = isHovered => {
class="relative flex flex-col pt-3 pb-4 lg:pt-[1.125rem] lg:pb-5 overflow-visible transition-opacity duration-[600ms] ease-out"
:class="isExpanded ? 'opacity-100 delay-200' : 'opacity-0'"
>
<ContactCardForm :contact-data="contactData" />
<ContactCardOverview :contact-data="contactData" />
</div>
</div>
</div>
@@ -1,12 +1,13 @@
<script setup>
import { ref } from 'vue';
import { useToggle } from '@vueuse/core';
import { vOnClickOutside } from '@vueuse/components';
import Button from 'dashboard/components-next/button/Button.vue';
import Input from 'dashboard/components-next/input/Input.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import ContactSortMenu from './components/ContactSortMenu.vue';
import ContactMoreActions from './components/ContactMoreActions.vue';
import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue';
import DisplayPropertiesModal from './components/DisplayPropertiesModal.vue';
import DisplayPropertiesMenu from './components/DisplayPropertiesMenu.vue';
defineProps({
showSearch: { type: Boolean, default: true },
@@ -32,10 +33,12 @@ const emit = defineEmits([
'deleteSegment',
]);
const displayPropertiesModalRef = ref(null);
const [showDisplayProperties, toggleDisplayProperties] = useToggle(false);
const openDisplayPropertiesModal = () => {
displayPropertiesModalRef.value?.dialogRef?.open();
const closeDisplayProperties = () => {
if (showDisplayProperties.value) {
toggleDisplayProperties(false);
}
};
</script>
@@ -47,7 +50,7 @@ const openDisplayPropertiesModal = () => {
<span class="text-heading-1 truncate text-n-slate-12">
{{ headerTitle }}
</span>
<div class="flex items-center flex-col sm:flex-row flex-shrink-0 gap-4">
<div class="flex items-center flex-col sm:flex-row flex-shrink-0 gap-3">
<div v-if="showSearch" class="flex items-center gap-2 w-full">
<Input
:model-value="searchValue"
@@ -67,15 +70,24 @@ const openDisplayPropertiesModal = () => {
</template>
</Input>
</div>
<div class="flex items-center flex-shrink-0 gap-4">
<div class="flex items-center gap-2">
<div class="flex items-center flex-shrink-0 gap-2">
<div class="w-px h-3 rounded-lg bg-n-strong" />
<div v-on-click-outside="closeDisplayProperties" class="relative">
<Button
icon="i-lucide-settings-2"
color="slate"
:label="$t('CONTACTS_LAYOUT.HEADER.DISPLAY_PROPERTIES')"
size="sm"
variant="ghost"
@click="openDisplayPropertiesModal"
:variant="!showDisplayProperties ? 'ghost' : 'faded'"
@click="toggleDisplayProperties()"
/>
<DisplayPropertiesMenu
v-if="showDisplayProperties"
class="absolute top-full mt-1 max-md:ltr:left-0 max-md:rtl:right-0 md:ltr:right-0 md:rtl:left-0"
/>
</div>
<div class="w-px h-3 rounded-lg bg-n-strong mx-1" />
<div class="flex items-center gap-2">
<div v-if="!isLabelView && !isActiveView" class="relative">
<Button
id="toggleContactsFilterButton"
@@ -127,7 +139,9 @@ const openDisplayPropertiesModal = () => {
@export="emit('export')"
/>
</div>
<div class="w-px h-4 bg-n-strong" />
<div
class="w-px h-3 rounded-lg bg-n-strong ltr:ml-1 rtl:mr-2 ltr:mr-1 rtl:ml-2"
/>
<ComposeConversation>
<template #trigger="{ toggle }">
<Button :label="buttonLabel" size="sm" @click="toggle" />
@@ -136,6 +150,5 @@ const openDisplayPropertiesModal = () => {
</div>
</div>
</div>
<DisplayPropertiesModal ref="displayPropertiesModalRef" />
</header>
</template>
@@ -0,0 +1,162 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useMapGetter } from 'dashboard/composables/store';
import { useUISettings } from 'dashboard/composables/useUISettings';
import Button from 'dashboard/components-next/button/Button.vue';
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
const { t } = useI18n();
const { uiSettings, updateUISettings } = useUISettings();
const contactAttributes = useMapGetter('attributes/getContactAttributes');
const MAX_SELECTED = 3;
const selectedAttributeKeys = computed({
get: () => uiSettings.value?.contact_list_display_properties || [],
set: value => {
updateUISettings({
contact_list_display_properties: value,
});
},
});
const selectedAttributes = computed(() => {
return (
contactAttributes.value?.filter(attr =>
selectedAttributeKeys.value.includes(attr.attributeKey)
) || []
);
});
const otherAttributes = computed(() => {
return (
contactAttributes.value?.filter(
attr => !selectedAttributeKeys.value.includes(attr.attributeKey)
) || []
);
});
const canSelectMore = computed(() => {
return selectedAttributeKeys.value.length < MAX_SELECTED;
});
const toggleAttribute = attributeKey => {
const currentKeys = [...selectedAttributeKeys.value];
const index = currentKeys.indexOf(attributeKey);
if (index > -1) {
currentKeys.splice(index, 1);
} else if (currentKeys.length < MAX_SELECTED) {
currentKeys.push(attributeKey);
}
selectedAttributeKeys.value = currentKeys;
};
const removeAttribute = attributeKey => {
const currentKeys = [...selectedAttributeKeys.value];
const index = currentKeys.indexOf(attributeKey);
if (index > -1) {
currentKeys.splice(index, 1);
selectedAttributeKeys.value = currentKeys;
}
};
</script>
<template>
<div
class="flex flex-col gap-2 px-3 pt-3 pb-4 bg-n-alpha-3 rounded-xl outline outline-1 outline-n-weak -outline-offset-1 w-80 z-20 backdrop-blur-md"
>
<div class="flex items-start gap-1 flex-col">
<h3 class="text-heading-3 text-n-slate-12 m-0">
{{ t('CONTACTS_LAYOUT.DISPLAY_PROPERTIES.TITLE') }}
</h3>
<p class="text-body-main text-n-slate-11 m-0">
{{ t('CONTACTS_LAYOUT.DISPLAY_PROPERTIES.DESCRIPTION') }}
</p>
</div>
<div v-if="selectedAttributes.length > 0" class="flex flex-col gap-2">
<span class="pt-2 text-label-small text-n-slate-11">
{{ t('CONTACTS_LAYOUT.DISPLAY_PROPERTIES.SELECTED_ATTRIBUTES') }}
</span>
<div class="flex flex-wrap gap-2">
<div
v-for="attribute in selectedAttributes"
:key="attribute.attributeKey"
class="inline-flex items-center gap-1 ltr:pl-1 rtl:pr-1 ltr:pr-2 rtl:pl-2 rounded-lg bg-n-button-color shadow-[0_1px_2px_0_rgba(27,28,29,0.04)] outline outline-1 -outline-offset-1 outline-n-container h-8"
>
<div class="size-5 flex items-center justify-center">
<Checkbox
model-value
@change="removeAttribute(attribute.attributeKey)"
/>
</div>
<span class="text-body-main text-n-slate-12">
{{ attribute.attributeDisplayName }}
</span>
<Button
icon="i-lucide-x"
slate
xs
ghost
class="!size-4 !rounded-md"
@click="removeAttribute(attribute.attributeKey)"
/>
</div>
</div>
</div>
<div v-if="otherAttributes.length > 0" class="flex flex-col gap-2">
<span class="pt-2 text-label-small text-n-slate-11">
{{ t('CONTACTS_LAYOUT.DISPLAY_PROPERTIES.OTHER_ATTRIBUTES') }}
</span>
<div class="flex flex-wrap gap-2">
<div
v-for="attribute in otherAttributes"
:key="attribute.attributeKey"
class="inline-flex items-center gap-1 ltr:pl-1 rtl:pr-1 ltr:pr-2 rtl:pl-2 rounded-lg shadow-[0_1px_2px_0_rgba(27,28,29,0.04)] outline outline-1 -outline-offset-1 outline-n-container h-8 bg-n-button-color"
:class="[
canSelectMore ? 'cursor-pointer' : 'opacity-50 cursor-not-allowed',
]"
@click="canSelectMore && toggleAttribute(attribute.attributeKey)"
>
<div class="size-5 flex items-center justify-center">
<Checkbox
:model-value="false"
:disabled="!canSelectMore"
@change="toggleAttribute(attribute.attributeKey)"
/>
</div>
<span class="text-body-main text-n-slate-12">
{{ attribute.attributeDisplayName }}
</span>
<Button
v-if="attribute.attributeDescription"
v-tooltip.top="{
content: attribute.attributeDescription,
delay: { show: 500, hide: 0 },
}"
icon="i-lucide-info"
slate
xs
ghost
:disabled="!canSelectMore"
class="!size-4 !rounded-md"
/>
</div>
</div>
</div>
<div
v-if="contactAttributes.length === 0"
class="flex flex-col items-center justify-center gap-3 py-8"
>
<p class="text-body-main text-center text-n-slate-11 m-0">
{{ t('CONTACTS_LAYOUT.DISPLAY_PROPERTIES.NO_ATTRIBUTES') }}
</p>
</div>
</div>
</template>
@@ -1,166 +0,0 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useMapGetter } from 'dashboard/composables/store';
import { useUISettings } from 'dashboard/composables/useUISettings';
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
const { t } = useI18n();
const { uiSettings, updateUISettings } = useUISettings();
const contactAttributes = useMapGetter('attributes/getContactAttributes');
const MAX_SELECTED = 3;
const selectedAttributeKeys = computed({
get: () => uiSettings.value?.contact_list_display_properties || [],
set: value => {
updateUISettings({
contact_list_display_properties: value,
});
},
});
const selectedAttributes = computed(() => {
return contactAttributes.value.filter(attr =>
selectedAttributeKeys.value.includes(attr.attributeKey)
);
});
const otherAttributes = computed(() => {
return contactAttributes.value.filter(
attr => !selectedAttributeKeys.value.includes(attr.attributeKey)
);
});
const canSelectMore = computed(() => {
return selectedAttributeKeys.value.length < MAX_SELECTED;
});
const toggleAttribute = attributeKey => {
const currentKeys = [...selectedAttributeKeys.value];
const index = currentKeys.indexOf(attributeKey);
if (index > -1) {
currentKeys.splice(index, 1);
} else if (currentKeys.length < MAX_SELECTED) {
currentKeys.push(attributeKey);
}
selectedAttributeKeys.value = currentKeys;
};
const removeAttribute = attributeKey => {
const currentKeys = [...selectedAttributeKeys.value];
const index = currentKeys.indexOf(attributeKey);
if (index > -1) {
currentKeys.splice(index, 1);
selectedAttributeKeys.value = currentKeys;
}
};
const dialogRef = ref(null);
const handleClose = () => {
// Close the dialog after saving
dialogRef.value?.close();
};
defineExpose({ dialogRef });
</script>
<template>
<Dialog
ref="dialogRef"
:title="t('CONTACTS_LAYOUT.DISPLAY_PROPERTIES.TITLE')"
:confirm-button-label="t('CONTACTS_LAYOUT.DISPLAY_PROPERTIES.SAVE')"
:cancel-button-label="t('CONTACTS_LAYOUT.DISPLAY_PROPERTIES.CANCEL')"
width="2xl"
@confirm="handleClose"
>
<div class="flex flex-col gap-6">
<p class="text-body-main text-n-slate-11 m-0">
{{ t('CONTACTS_LAYOUT.DISPLAY_PROPERTIES.DESCRIPTION') }}
</p>
<div v-if="selectedAttributes.length > 0" class="flex flex-col gap-3">
<h3 class="text-heading-3 text-n-slate-12 m-0">
{{ t('CONTACTS_LAYOUT.DISPLAY_PROPERTIES.SELECTED_ATTRIBUTES') }}
</h3>
<div class="flex flex-wrap gap-2">
<div
v-for="attribute in selectedAttributes"
:key="attribute.attributeKey"
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-n-brand-alpha text-n-brand border border-n-brand"
>
<Checkbox
model-value
@change="removeAttribute(attribute.attributeKey)"
/>
<span class="text-label-main">
{{ attribute.attributeDisplayName }}
</span>
<button
class="flex items-center justify-center size-4 rounded hover:bg-n-brand-alpha-hover transition-colors"
@click="removeAttribute(attribute.attributeKey)"
>
<Icon icon="i-lucide-x" class="size-3" />
</button>
</div>
</div>
</div>
<div v-if="otherAttributes.length > 0" class="flex flex-col gap-3">
<h3 class="text-heading-3 text-n-slate-12 m-0">
{{ t('CONTACTS_LAYOUT.DISPLAY_PROPERTIES.OTHER_ATTRIBUTES') }}
</h3>
<div class="flex flex-wrap gap-2">
<div
v-for="attribute in otherAttributes"
:key="attribute.attributeKey"
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md transition-colors"
:class="[
canSelectMore
? 'bg-n-alpha-2 hover:bg-n-alpha-3 cursor-pointer border border-n-weak'
: 'bg-n-alpha-1 border border-n-weak opacity-50 cursor-not-allowed',
]"
@click="canSelectMore && toggleAttribute(attribute.attributeKey)"
>
<Checkbox
:model-value="false"
:disabled="!canSelectMore"
@change="toggleAttribute(attribute.attributeKey)"
/>
<span class="text-label-main text-n-slate-12">
{{ attribute.attributeDisplayName }}
</span>
<Icon
v-if="attribute.attributeDescription"
v-tooltip.top="{
content: attribute.attributeDescription,
delay: { show: 500, hide: 0 },
}"
icon="i-lucide-info"
class="size-3.5 text-n-slate-11"
/>
</div>
</div>
</div>
<div
v-if="contactAttributes.length === 0"
class="flex flex-col items-center justify-center gap-3 py-8"
>
<Icon
icon="i-lucide-inbox"
class="size-12 text-n-slate-11 opacity-50"
/>
<p class="text-body-main text-n-slate-11 m-0">
{{ t('CONTACTS_LAYOUT.DISPLAY_PROPERTIES.NO_ATTRIBUTES') }}
</p>
</div>
</div>
</Dialog>
</template>
@@ -307,6 +307,7 @@
"SEND_MESSAGE": "Send message",
"BLOCK_CONTACT": "Block contact",
"UNBLOCK_CONTACT": "Unblock contact",
"DISPLAY_PROPERTIES": "Display",
"BREADCRUMB": {
"CONTACTS": "Contacts"
},
@@ -393,7 +394,7 @@
"DESCRIPTION": "Select up to three contact attributes to display.",
"SELECTED_ATTRIBUTES": "Selected attributes",
"OTHER_ATTRIBUTES": "Other attributes",
"NO_ATTRIBUTES": "No contact attributes available",
"NO_ATTRIBUTES": "No contact attributes added, you can add some to display.",
"CANCEL": "Cancel",
"SAVE": "Save",
"YES": "Yes",