feat(V5): Update settings pages UI (#13396)
# Pull Request Template ## Description This PR updates settings page UI ## Type of change - [x] New feature (non-breaking change which adds functionality) ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
This commit is contained in:
committed by
aakashb95
parent
ac88ebf1d7
commit
a75e0f73ad
+2
-2
@@ -49,7 +49,7 @@ const handleFetchUsers = () => {
|
||||
<div class="flex flex-col gap-2 relative justify-between w-full">
|
||||
<div class="flex items-center gap-3 justify-between w-full">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-base font-medium text-n-slate-12 line-clamp-1">
|
||||
<h3 class="text-heading-2 text-n-slate-12 line-clamp-1">
|
||||
{{ name }}
|
||||
</h3>
|
||||
<CardPopover
|
||||
@@ -78,7 +78,7 @@ const handleFetchUsers = () => {
|
||||
<Button icon="i-lucide-trash" sm slate ghost @click="handleDelete" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-n-slate-11 text-sm line-clamp-1 mb-0 py-1">
|
||||
<p class="text-n-slate-11 text-body-para line-clamp-1 mb-0 py-1">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+3
-3
@@ -20,7 +20,7 @@ const handleClick = () => {
|
||||
<CardLayout class="[&>div]:px-5 cursor-pointer" @click="handleClick">
|
||||
<div class="flex flex-col items-start gap-2">
|
||||
<div class="flex justify-between w-full items-center">
|
||||
<h3 class="text-n-slate-12 text-base font-medium">{{ title }}</h3>
|
||||
<h3 class="text-n-slate-12 text-heading-2">{{ title }}</h3>
|
||||
<Button
|
||||
xs
|
||||
slate
|
||||
@@ -29,14 +29,14 @@ const handleClick = () => {
|
||||
@click.stop="handleClick"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-n-slate-11 text-sm mb-0">{{ description }}</p>
|
||||
<p class="text-n-slate-11 text-body-para mb-0">{{ description }}</p>
|
||||
</div>
|
||||
|
||||
<ul class="flex flex-col items-start gap-3 mt-3">
|
||||
<li
|
||||
v-for="feature in features"
|
||||
:key="feature.id"
|
||||
class="flex items-center gap-3 text-sm"
|
||||
class="flex items-center gap-3 text-body-para"
|
||||
>
|
||||
<Icon
|
||||
:icon="feature.icon"
|
||||
|
||||
+14
-18
@@ -60,23 +60,19 @@ const handleFetchInboxes = () => {
|
||||
<div class="flex flex-col gap-2 relative justify-between w-full">
|
||||
<div class="flex items-center gap-3 justify-between w-full">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-base font-medium text-n-slate-12 line-clamp-1">
|
||||
<h3 class="text-heading-2 text-n-slate-12 line-clamp-1">
|
||||
{{ name }}
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<CardPopover
|
||||
:title="
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.POPOVER'
|
||||
)
|
||||
"
|
||||
icon="i-lucide-inbox"
|
||||
:count="assignedInboxCount"
|
||||
:items="inboxes"
|
||||
:is-fetching="isFetchingInboxes"
|
||||
@fetch="handleFetchInboxes"
|
||||
/>
|
||||
</div>
|
||||
<CardPopover
|
||||
:title="
|
||||
t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.POPOVER')
|
||||
"
|
||||
icon="i-lucide-inbox"
|
||||
:count="assignedInboxCount"
|
||||
:items="inboxes"
|
||||
:is-fetching="isFetchingInboxes"
|
||||
@fetch="handleFetchInboxes"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
@@ -93,18 +89,18 @@ const handleFetchInboxes = () => {
|
||||
<Button icon="i-lucide-trash" sm slate ghost @click="handleDelete" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-n-slate-11 text-sm line-clamp-1 mb-0 py-1">
|
||||
<p class="text-n-slate-11 text-body-para line-clamp-1 mb-0 py-1">
|
||||
{{ description }}
|
||||
</p>
|
||||
<div class="flex items-center gap-3 py-1.5">
|
||||
<span v-if="order" class="text-n-slate-11 text-sm">
|
||||
<span v-if="order" class="text-n-slate-11 text-body-para">
|
||||
{{
|
||||
`${t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.ORDER')}:`
|
||||
}}
|
||||
<span class="text-n-slate-12">{{ order }}</span>
|
||||
</span>
|
||||
<div v-if="order" class="w-px h-3 bg-n-strong" />
|
||||
<span v-if="priority" class="text-n-slate-11 text-sm">
|
||||
<span v-if="priority" class="text-n-slate-11 text-body-para">
|
||||
{{
|
||||
`${t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.PRIORITY')}:`
|
||||
}}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import AddDataDropdown from 'dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue';
|
||||
import LabelItem from 'dashboard/components-next/Label/LabelItem.vue';
|
||||
import LabelItem from 'dashboard/components-next/label/LabelItem.vue';
|
||||
import DurationInput from 'dashboard/components-next/input/DurationInput.vue';
|
||||
import { DURATION_UNITS } from 'dashboard/components-next/input/constants';
|
||||
|
||||
|
||||
+26
-34
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Label from 'dashboard/components-next/label/Label.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
@@ -22,6 +22,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabledLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
disabledMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
@@ -30,8 +34,6 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['select']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const handleChange = () => {
|
||||
if (!props.isActive && !props.disabled) {
|
||||
emit('select', props.id);
|
||||
@@ -41,7 +43,7 @@ const handleChange = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative rounded-xl outline outline-1 p-4 transition-all duration-200 bg-n-solid-1 py-4 ltr:pl-4 rtl:pr-4 ltr:pr-6 rtl:pl-6"
|
||||
class="cursor-pointer rounded-xl outline outline-1 p-4 transition-all duration-200 bg-n-solid-1 py-4 ltr:pl-4 rtl:pr-4 ltr:pr-6 rtl:pl-6"
|
||||
:class="[
|
||||
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
|
||||
isActive ? 'outline-n-blue-9' : 'outline-n-weak',
|
||||
@@ -49,39 +51,29 @@ const handleChange = () => {
|
||||
]"
|
||||
@click="handleChange"
|
||||
>
|
||||
<div class="absolute top-4 right-4">
|
||||
<input
|
||||
:id="`${id}`"
|
||||
:checked="isActive"
|
||||
:value="id"
|
||||
:name="id"
|
||||
:disabled="disabled"
|
||||
type="radio"
|
||||
class="h-4 w-4 border-n-slate-6 text-n-brand focus:ring-n-brand focus:ring-offset-0"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex flex-col gap-3 items-start">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="text-sm font-medium text-n-slate-12">
|
||||
{{ label }}
|
||||
</h3>
|
||||
<span
|
||||
v-if="disabled"
|
||||
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-n-yellow-3 text-n-yellow-11"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.FORM.ASSIGNMENT_ORDER.BALANCED.PREMIUM_BADGE'
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<div class="flex flex-col gap-2 items-start">
|
||||
<div class="flex items-center justify-between w-full gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="text-heading-3 text-n-slate-12">
|
||||
{{ label }}
|
||||
</h3>
|
||||
<Label v-if="disabled" :label="disabledLabel" color="amber" compact />
|
||||
</div>
|
||||
<input
|
||||
:id="`${id}`"
|
||||
:checked="isActive"
|
||||
:value="id"
|
||||
:name="id"
|
||||
:disabled="disabled"
|
||||
type="radio"
|
||||
class="h-4 w-4 border-n-slate-6 text-n-brand focus:ring-n-brand focus:ring-offset-0 flex-shrink-0"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-sm text-n-slate-11">
|
||||
<p class="text-body-main text-n-slate-11">
|
||||
{{ disabled && disabledMessage ? disabledMessage : description }}
|
||||
</p>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -21,10 +21,10 @@ const handleButtonClick = () => {
|
||||
|
||||
<template>
|
||||
<section class="flex flex-col w-full h-full overflow-hidden bg-n-surface-1">
|
||||
<header class="sticky top-0 z-10 px-6 lg:px-0">
|
||||
<div class="w-full max-w-[60rem] mx-auto">
|
||||
<header class="sticky top-0 z-10 px-6">
|
||||
<div class="w-full max-w-5xl mx-auto">
|
||||
<div class="flex items-center justify-between w-full h-20 gap-2">
|
||||
<span class="text-xl font-medium text-n-slate-12">
|
||||
<span class="text-heading-1 text-n-slate-12">
|
||||
{{ headerTitle }}
|
||||
</span>
|
||||
<div
|
||||
@@ -43,8 +43,8 @@ const handleButtonClick = () => {
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="flex-1 px-6 overflow-y-auto lg:px-0">
|
||||
<div class="w-full max-w-[60rem] mx-auto py-4">
|
||||
<main class="flex-1 px-6 overflow-y-auto">
|
||||
<div class="w-full max-w-5xl mx-auto py-4">
|
||||
<slot name="default" />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -19,7 +19,7 @@ const handleClick = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col w-full outline-1 outline outline-n-container group/cardLayout rounded-xl bg-n-solid-2"
|
||||
class="flex flex-col w-full outline-1 outline outline-n-container -outline-offset-1 group/cardLayout rounded-xl bg-n-solid-2"
|
||||
>
|
||||
<div
|
||||
class="flex w-full gap-3 py-5"
|
||||
|
||||
+3
-3
@@ -15,11 +15,11 @@ const emit = defineEmits(['search', 'update:sort']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="sticky top-0 z-10">
|
||||
<header class="sticky top-0 z-10 px-6">
|
||||
<div
|
||||
class="flex items-start sm:items-center justify-between w-full py-6 px-6 gap-2 mx-auto max-w-[60rem]"
|
||||
class="flex items-start sm:items-center justify-between w-full py-6 gap-2 mx-auto max-w-5xl"
|
||||
>
|
||||
<span class="text-xl font-medium truncate text-n-slate-12">
|
||||
<span class="text-heading-1 truncate text-n-slate-12">
|
||||
{{ headerTitle }}
|
||||
</span>
|
||||
<div class="flex items-center flex-row flex-shrink-0 gap-2">
|
||||
|
||||
@@ -32,17 +32,18 @@ const updateCurrentPage = page => {
|
||||
@search="emit('search', $event)"
|
||||
@update:sort="emit('update:sort', $event)"
|
||||
/>
|
||||
<main class="flex-1 overflow-y-auto">
|
||||
<div class="w-full mx-auto max-w-[60rem]">
|
||||
<main class="flex-1 px-6 overflow-y-auto">
|
||||
<div class="w-full mx-auto max-w-5xl py-4">
|
||||
<slot name="default" />
|
||||
</div>
|
||||
</main>
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-0 px-4 pb-4">
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-0">
|
||||
<PaginationFooter
|
||||
current-page-info="COMPANIES_LAYOUT.PAGINATION_FOOTER.SHOWING"
|
||||
:current-page="currentPage"
|
||||
:total-items="totalItems"
|
||||
:items-per-page="25"
|
||||
class="max-w-[67rem]"
|
||||
@update:current-page="updateCurrentPage"
|
||||
/>
|
||||
</footer>
|
||||
|
||||
@@ -3,8 +3,8 @@ import { computed, watch, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
|
||||
import LabelItem from 'dashboard/components-next/Label/LabelItem.vue';
|
||||
import AddLabel from 'dashboard/components-next/Label/AddLabel.vue';
|
||||
import LabelItem from 'dashboard/components-next/label/LabelItem.vue';
|
||||
import AddLabel from 'dashboard/components-next/label/AddLabel.vue';
|
||||
|
||||
const props = defineProps({
|
||||
contactId: {
|
||||
|
||||
@@ -32,9 +32,9 @@ const emit = defineEmits([
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="sticky top-0 z-10">
|
||||
<header class="sticky top-0 z-10 px-6">
|
||||
<div
|
||||
class="flex items-start sm:items-center justify-between w-full py-6 px-6 gap-2 mx-auto max-w-[60rem]"
|
||||
class="flex items-start sm:items-center justify-between w-full py-6 gap-2 mx-auto max-w-5xl"
|
||||
>
|
||||
<span class="text-xl font-medium truncate text-n-slate-12">
|
||||
{{ headerTitle }}
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ const activeFilterQueryData = computed(() => {
|
||||
t('CONTACTS_LAYOUT.FILTER.ACTIVE_FILTERS.CLEAR_FILTERS')
|
||||
"
|
||||
:show-clear-button="!hasActiveSegments"
|
||||
class="max-w-[60rem] px-6"
|
||||
class="max-w-5xl"
|
||||
@open-filter="emit('openFilter')"
|
||||
@clear-filters="emit('clearFilters')"
|
||||
/>
|
||||
|
||||
@@ -98,8 +98,8 @@ const showPagination = computed(() => {
|
||||
@apply-filter="emit('applyFilter', $event)"
|
||||
@clear-filters="emit('clearFilters')"
|
||||
/>
|
||||
<main class="flex-1 overflow-y-auto">
|
||||
<div class="w-full mx-auto max-w-[60rem]">
|
||||
<main class="flex-1 overflow-y-auto px-6">
|
||||
<div class="w-full mx-auto max-w-5xl">
|
||||
<ContactsActiveFiltersPreview
|
||||
v-if="showActiveFiltersPreview"
|
||||
:active-segment="activeSegment"
|
||||
@@ -114,11 +114,12 @@ const showPagination = computed(() => {
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<footer v-if="showPagination" class="sticky bottom-0 z-0 px-4 pb-4">
|
||||
<footer v-if="showPagination" class="sticky bottom-0 z-0">
|
||||
<PaginationFooter
|
||||
current-page-info="CONTACTS_LAYOUT.PAGINATION_FOOTER.SHOWING"
|
||||
:current-page="currentPage"
|
||||
:total-items="totalItems"
|
||||
class="max-w-[67rem]"
|
||||
:items-per-page="itemsPerPage"
|
||||
@update:current-page="updateCurrentPage"
|
||||
/>
|
||||
|
||||
+46
-45
@@ -1,8 +1,10 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import Label from 'dashboard/components-next/label/Label.vue';
|
||||
import AttributeBadge from 'dashboard/components-next/CustomAttributes/AttributeBadge.vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
attribute: {
|
||||
@@ -18,7 +20,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(['edit', 'delete']);
|
||||
|
||||
const iconByType = {
|
||||
text: 'i-lucide-align-justify',
|
||||
text: 'i-lucide-menu',
|
||||
checkbox: 'i-lucide-circle-check-big',
|
||||
list: 'i-lucide-list',
|
||||
date: 'i-lucide-calendar',
|
||||
@@ -28,61 +30,60 @@ const iconByType = {
|
||||
|
||||
const attributeIcon = computed(() => {
|
||||
const typeKey = props.attribute.type?.toLowerCase();
|
||||
return iconByType[typeKey] || 'i-lucide-align-justify';
|
||||
return iconByType[typeKey] || 'i-lucide-menu';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col gap-2 p-4 bg-n-solid-1 rounded-2xl outline outline-1 outline-n-container"
|
||||
>
|
||||
<div class="flex flex-wrap gap-2 justify-between items-center">
|
||||
<div class="flex flex-wrap gap-2 items-center min-w-0">
|
||||
<h4 class="text-sm font-medium truncate text-n-slate-12">
|
||||
{{ attribute.label }}
|
||||
</h4>
|
||||
<div class="w-px h-3 bg-n-strong" />
|
||||
<div class="flex gap-2 items-center text-sm text-n-slate-11">
|
||||
<div class="flex items-center gap-1.5 text-n-slate-11">
|
||||
<Icon :icon="attributeIcon" class="size-4" />
|
||||
<span class="text-sm">{{ attribute.type }}</span>
|
||||
<div class="flex flex-col py-4 min-w-0">
|
||||
<div class="flex justify-between flex-row items-center gap-4 min-w-0">
|
||||
<div class="flex items-center gap-4 min-w-0">
|
||||
<div
|
||||
class="flex items-center flex-shrink-0 size-10 justify-center rounded-xl outline outline-1 outline-n-weak -outline-offset-1"
|
||||
>
|
||||
<Icon :icon="attributeIcon" class="size-4 text-n-slate-11" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5 items-start min-w-0 overflow-hidden">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<h4 class="text-heading-3 truncate text-n-slate-12 min-w-0">
|
||||
{{ attribute.label }}
|
||||
</h4>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Label :label="attribute.type" compact />
|
||||
<AttributeBadge
|
||||
v-for="badge in badges"
|
||||
:key="badge.type"
|
||||
:type="badge.type"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-px h-3 bg-n-weak" />
|
||||
<div class="flex items-center gap-1.5 text-n-slate-11">
|
||||
<Icon icon="i-lucide-key-round" class="size-4" />
|
||||
<span class="line-clamp-1 text-sm">{{ attribute.value }}</span>
|
||||
<div class="grid grid-cols-[auto_1fr] items-center gap-1.5">
|
||||
<Icon icon="i-lucide-key-round" class="size-3.5 text-n-slate-11" />
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-body-main text-n-slate-11 truncate">
|
||||
{{ attribute.value }}
|
||||
</span>
|
||||
<template
|
||||
v-if="attribute.attribute_description || attribute.description"
|
||||
>
|
||||
<div class="w-px h-3 rounded-lg bg-n-weak flex-shrink-0" />
|
||||
<span class="text-body-main text-n-slate-11 truncate">
|
||||
{{ attribute.attribute_description || attribute.description }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<AttributeBadge
|
||||
v-for="badge in badges"
|
||||
:key="badge.type"
|
||||
:type="badge.type"
|
||||
/>
|
||||
<div
|
||||
v-if="badges.length > 0"
|
||||
class="w-px h-3 bg-n-strong ltr:ml-1.5 rtl:mr-1.5"
|
||||
/>
|
||||
<div class="flex gap-3 justify-end flex-shrink-0">
|
||||
<Button
|
||||
icon="i-lucide-pencil-line"
|
||||
size="sm"
|
||||
color="slate"
|
||||
ghost
|
||||
icon="i-woot-edit-pen"
|
||||
slate
|
||||
sm
|
||||
@click="emit('edit', attribute)"
|
||||
/>
|
||||
<div class="w-px h-3 bg-n-strong" />
|
||||
<Button
|
||||
icon="i-lucide-trash"
|
||||
size="sm"
|
||||
color="slate"
|
||||
ghost
|
||||
@click="emit('delete', attribute)"
|
||||
/>
|
||||
<Button icon="i-woot-bin" slate sm @click="emit('delete', attribute)" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0 text-sm text-n-slate-11">
|
||||
{{ attribute.attribute_description || attribute.description || '' }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+5
-3
@@ -35,18 +35,20 @@ const handleDelete = () => {
|
||||
<template>
|
||||
<div class="flex justify-between items-center px-4 py-3 w-full">
|
||||
<div class="flex gap-3 items-center">
|
||||
<h5 class="text-sm font-medium text-n-slate-12 line-clamp-1">
|
||||
<h5 class="text-heading-3 text-n-slate-12 line-clamp-1">
|
||||
{{ attribute.label }}
|
||||
</h5>
|
||||
<div class="w-px h-2.5 bg-n-slate-5" />
|
||||
<div class="flex gap-1.5 items-center">
|
||||
<Icon :icon="attributeIcon" class="size-4 text-n-slate-11" />
|
||||
<span class="text-sm text-n-slate-11">{{ attribute.type }}</span>
|
||||
<span class="text-body-para text-n-slate-11">{{ attribute.type }}</span>
|
||||
</div>
|
||||
<div class="w-px h-2.5 bg-n-slate-5" />
|
||||
<div class="flex gap-1.5 items-center">
|
||||
<Icon icon="i-lucide-key-round" class="size-4 text-n-slate-11" />
|
||||
<span class="text-sm text-n-slate-11">{{ attribute.value }}</span>
|
||||
<span class="text-body-para text-n-slate-11">{{
|
||||
attribute.value
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
|
||||
+2
-2
@@ -129,10 +129,10 @@ const handleDelete = attribute => {
|
||||
<div class="flex flex-col gap-2 items-start px-5 py-4">
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-base font-medium text-n-slate-12">
|
||||
<h3 class="text-heading-2 text-n-slate-12">
|
||||
{{ $t('CONVERSATION_WORKFLOW.REQUIRED_ATTRIBUTES.TITLE') }}
|
||||
</h3>
|
||||
<p class="mb-0 text-sm text-n-slate-11">
|
||||
<p class="mb-0 text-body-para text-n-slate-11">
|
||||
{{ $t('CONVERSATION_WORKFLOW.REQUIRED_ATTRIBUTES.DESCRIPTION') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import Label from 'dashboard/components-next/label/Label.vue';
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
@@ -18,11 +19,13 @@ const attributeConfig = {
|
||||
colorClass: 'text-n-blue-11',
|
||||
icon: 'i-lucide-message-circle',
|
||||
labelKey: 'ATTRIBUTES_MGMT.BADGES.PRE_CHAT',
|
||||
color: 'slate',
|
||||
},
|
||||
resolution: {
|
||||
colorClass: 'text-n-teal-11',
|
||||
icon: 'i-lucide-circle-check-big',
|
||||
labelKey: 'ATTRIBUTES_MGMT.BADGES.RESOLUTION',
|
||||
color: 'slate',
|
||||
},
|
||||
};
|
||||
const config = computed(
|
||||
@@ -31,12 +34,9 @@ const config = computed(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex gap-1 justify-center items-center px-1.5 py-1 rounded-md shadow outline-1 outline outline-n-container bg-n-solid-2"
|
||||
>
|
||||
<Icon :icon="config.icon" class="size-4" :class="config.colorClass" />
|
||||
<span class="text-xs" :class="config.colorClass">{{
|
||||
t(config.labelKey)
|
||||
}}</span>
|
||||
</div>
|
||||
<Label :label="t(config.labelKey)" :color="config.color" compact>
|
||||
<template #icon>
|
||||
<Icon :icon="config.icon" class="size-3.5 text-n-slate-12" />
|
||||
</template>
|
||||
</Label>
|
||||
</template>
|
||||
|
||||
@@ -26,7 +26,7 @@ defineProps({
|
||||
class="relative flex flex-col items-center justify-center w-full h-full overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="relative w-full max-w-[60rem] mx-auto overflow-hidden h-full max-h-[28rem]"
|
||||
class="relative w-full max-w-5xl mx-auto overflow-hidden h-full max-h-[28rem]"
|
||||
>
|
||||
<div
|
||||
v-if="showBackdrop"
|
||||
|
||||
@@ -59,8 +59,8 @@ const togglePortalSwitcher = () => {
|
||||
|
||||
<template>
|
||||
<section class="flex flex-col w-full h-full overflow-hidden bg-n-surface-1">
|
||||
<header class="sticky top-0 z-10 px-6 pb-3 lg:px-0">
|
||||
<div class="w-full max-w-[60rem] mx-auto lg:px-6">
|
||||
<header class="sticky top-0 z-10 px-6 pb-3">
|
||||
<div class="w-full max-w-5xl mx-auto">
|
||||
<div
|
||||
v-if="showHeaderTitle"
|
||||
class="flex items-center justify-start h-20 gap-2"
|
||||
@@ -95,16 +95,17 @@ const togglePortalSwitcher = () => {
|
||||
<slot name="header-actions" />
|
||||
</div>
|
||||
</header>
|
||||
<main class="flex-1 px-6 overflow-y-auto lg:px-0">
|
||||
<div class="w-full max-w-[60rem] mx-auto py-3 lg:px-6">
|
||||
<main class="flex-1 px-6 overflow-y-auto">
|
||||
<div class="w-full max-w-5xl mx-auto py-3">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</main>
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10 px-4 pb-4">
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10">
|
||||
<PaginationFooter
|
||||
:current-page="currentPage"
|
||||
:total-items="totalItems"
|
||||
:items-per-page="itemsPerPage"
|
||||
class="max-w-[67rem]"
|
||||
@update:current-page="updateCurrentPage"
|
||||
/>
|
||||
</footer>
|
||||
|
||||
+1
@@ -62,6 +62,7 @@ const onCreate = async () => {
|
||||
from: route.name,
|
||||
});
|
||||
|
||||
selectedLocale.value = '';
|
||||
dialogRef.value?.close();
|
||||
useAlert(
|
||||
t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.API.SUCCESS_MESSAGE')
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ describe('composeConversationHelper', () => {
|
||||
const result = helpers.buildContactableInboxesList(inboxes);
|
||||
expect(result[0]).toMatchObject({
|
||||
id: 1,
|
||||
icon: 'i-ri-mail-line',
|
||||
icon: 'i-woot-mail',
|
||||
label: 'Email Inbox (support@example.com)',
|
||||
action: 'inbox',
|
||||
value: 1,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
import { useToggle } from '@vueuse/core';
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
defaultOpen: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [isOpen, toggle] = useToggle(props.defaultOpen);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-bind="$attrs"
|
||||
class="flex items-center justify-between w-full cursor-pointer pb-2 pt-4 border-t border-n-weak"
|
||||
@click="toggle()"
|
||||
>
|
||||
<span class="text-heading-2 text-n-slate-12 w-full">
|
||||
{{ title }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="isOpen" class="w-full space-y-4 pt-4 mb-4">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
helpText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full py-2 mb-2 [interpolate-size:allow-keywords]">
|
||||
<div
|
||||
class="grid grid-cols-1 lg:grid-cols-8 gap-1.5 lg:gap-4 items-start lg:items-center"
|
||||
>
|
||||
<label class="text-heading-3 text-n-slate-12 col-span-1 lg:col-span-2">
|
||||
{{ label }}
|
||||
</label>
|
||||
<div class="col-span-1 lg:col-span-6">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="helpText" class="grid grid-cols-1 lg:grid-cols-8">
|
||||
<div class="col-span-1 lg:col-span-2 invisible" />
|
||||
<p
|
||||
v-if="helpText"
|
||||
class="mt-1.5 col-span-1 lg:col-span-6 text-label-small text-n-slate-11 ltr:ml-1 rtl:mr-1"
|
||||
>
|
||||
{{ helpText }}
|
||||
</p>
|
||||
</div>
|
||||
<slot name="extra" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup>
|
||||
import ToggleSwitch from 'dashboard/components-next/switch/Switch.vue';
|
||||
|
||||
defineProps({
|
||||
header: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const modelValue = defineModel({ type: Boolean, default: false });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col items-start outline outline-1 -outline-offset-1 outline-n-weak rounded-xl [interpolate-size:allow-keywords]"
|
||||
>
|
||||
<div class="flex flex-col gap-1 items-start w-full px-4 py-3">
|
||||
<div class="flex items-center gap-3 w-full justify-between">
|
||||
<span class="text-heading-3 text-n-slate-12">
|
||||
{{ header }}
|
||||
</span>
|
||||
<ToggleSwitch v-model="modelValue" />
|
||||
</div>
|
||||
<span v-if="description" class="text-body-main text-n-slate-11">
|
||||
{{ description }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="$slots.editor"
|
||||
class="w-full border-t border-n-weak"
|
||||
:class="{ 'p-0': compact, 'px-4 pb-4 pt-2': !compact }"
|
||||
>
|
||||
<slot name="editor" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -112,6 +112,19 @@ const containerStyles = computed(() => ({
|
||||
height: `${props.size}px`,
|
||||
}));
|
||||
|
||||
const borderRadiusClass = computed(() => {
|
||||
if (props.roundedFull) {
|
||||
return 'rounded-full';
|
||||
}
|
||||
|
||||
// Approximates 25% of size
|
||||
if (props.size <= 16) return 'rounded'; // 4px
|
||||
if (props.size <= 24) return 'rounded-md'; // 6px
|
||||
if (props.size <= 32) return 'rounded-lg'; // 8px
|
||||
if (props.size <= 48) return 'rounded-xl'; // 12px
|
||||
return 'rounded-2xl'; // 16px
|
||||
});
|
||||
|
||||
const avatarStyles = computed(() => ({
|
||||
...containerStyles.value,
|
||||
backgroundColor:
|
||||
@@ -184,7 +197,7 @@ watch(
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="relative inline-flex group/avatar z-0 flex-shrink-0"
|
||||
class="relative inline-flex group/avatar z-0 flex-shrink-0 align-middle"
|
||||
:style="containerStyles"
|
||||
>
|
||||
<!-- Status Badge -->
|
||||
@@ -216,9 +229,9 @@ watch(
|
||||
<!-- Avatar Container -->
|
||||
<span
|
||||
role="img"
|
||||
class="relative inline-flex items-center justify-center object-cover overflow-hidden font-medium"
|
||||
class="relative inline-flex items-center justify-center object-cover overflow-hidden font-medium outline outline-1 -outline-offset-1 outline-[rgb(0_0_0_/_0.03)] dark:outline-[rgb(255_255_255_/_0.04)]"
|
||||
:class="[
|
||||
roundedFull ? 'rounded-full' : 'rounded-xl',
|
||||
borderRadiusClass,
|
||||
{
|
||||
'dark:!bg-[var(--dark-bg)] dark:!text-[var(--dark-text)]':
|
||||
!showDefaultAvatar && (!src || !isImageValid),
|
||||
@@ -267,7 +280,8 @@ watch(
|
||||
:handle-image-upload="handleImageUpload"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 z-10 flex items-center justify-center invisible w-full h-full transition-all duration-300 ease-in-out opacity-0 rounded-xl bg-n-alpha-black1 group-hover/avatar:visible group-hover/avatar:opacity-100"
|
||||
class="absolute inset-0 z-10 flex items-center justify-center invisible w-full h-full transition-all duration-300 ease-in-out opacity-0 bg-n-alpha-black1 group-hover/avatar:visible group-hover/avatar:opacity-100"
|
||||
:class="borderRadiusClass"
|
||||
@click="handleUploadAvatar"
|
||||
>
|
||||
<Icon
|
||||
|
||||
@@ -117,7 +117,7 @@ const handleCreateAssistant = () => {
|
||||
<template>
|
||||
<section class="flex flex-col w-full h-full overflow-hidden bg-n-surface-1">
|
||||
<header class="sticky top-0 z-10 px-6">
|
||||
<div class="w-full max-w-[60rem] mx-auto">
|
||||
<div class="w-full max-w-5xl mx-auto">
|
||||
<div
|
||||
class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row"
|
||||
>
|
||||
@@ -140,7 +140,9 @@ const handleCreateAssistant = () => {
|
||||
>
|
||||
<Button
|
||||
icon="i-lucide-chevron-down"
|
||||
variant="ghost"
|
||||
:variant="
|
||||
showAssistantSwitcherDropdown ? 'faded' : 'ghost'
|
||||
"
|
||||
color="slate"
|
||||
size="xs"
|
||||
:disabled="isFetchingAssistants"
|
||||
@@ -204,7 +206,7 @@ const handleCreateAssistant = () => {
|
||||
</div>
|
||||
</header>
|
||||
<main class="flex-1 px-6 overflow-y-auto">
|
||||
<div class="w-full max-w-[60rem] h-full mx-auto py-4">
|
||||
<div class="w-full max-w-5xl h-full mx-auto py-4">
|
||||
<slot v-if="!showPaywall" name="controls" />
|
||||
<div
|
||||
v-if="isFetching"
|
||||
@@ -222,11 +224,12 @@ const handleCreateAssistant = () => {
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10 px-4 pb-4">
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10">
|
||||
<PaginationFooter
|
||||
:current-page="currentPage"
|
||||
:total-items="totalCount"
|
||||
:items-per-page="itemsPerPage"
|
||||
class="max-w-[67rem]"
|
||||
@update:current-page="handlePageChange"
|
||||
/>
|
||||
</footer>
|
||||
|
||||
@@ -27,7 +27,7 @@ const openBilling = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full max-w-[60rem] mx-auto h-full max-h-[448px] grid place-content-center"
|
||||
class="w-full max-w-5xl mx-auto h-full max-h-[448px] grid place-content-center"
|
||||
>
|
||||
<BasePaywallModal
|
||||
class="mx-auto"
|
||||
|
||||
@@ -79,8 +79,8 @@ defineExpose({
|
||||
:aria-multiselectable="multiple"
|
||||
>
|
||||
<li
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
v-for="(option, index) in options"
|
||||
:key="`${option.value}-${index}`"
|
||||
class="flex items-center justify-between w-full gap-2 px-3 py-2 text-sm transition-colors duration-150 cursor-pointer hover:bg-n-alpha-2"
|
||||
:class="{
|
||||
'bg-n-alpha-2': isSelected(option),
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ const openLink = link => {
|
||||
]"
|
||||
>
|
||||
<section
|
||||
class="absolute top-full mt-6 ltr:left-0 rtl:right-0 outline outline-1 outline-n-weak bg-n-alpha-3 backdrop-blur-[100px] rounded-xl p-4 w-80"
|
||||
class="absolute top-full mt-6 ltr:left-0 rtl:right-0 outline outline-1 outline-n-weak bg-n-alpha-3 backdrop-blur-[100px] rounded-xl p-4 w-80 z-20"
|
||||
>
|
||||
<div
|
||||
class="absolute -top-[0.77rem] ltr:left-12 rtl:right-12 w-6 h-6 ltr:rotate-45 rtl:-rotate-45 rtl:rounded-tr ltr:rounded-tl rtl:border-r ltr:border-l border-t border-n-weak bg-n-alpha-3 z-10"
|
||||
|
||||
@@ -9,12 +9,12 @@ export function useChannelIcon(inbox) {
|
||||
'Channel::Sms': 'i-woot-sms',
|
||||
'Channel::Telegram': 'i-woot-telegram',
|
||||
'Channel::TwilioSms': 'i-woot-sms',
|
||||
'Channel::TwitterProfile': 'i-ri-twitter-x-fill',
|
||||
'Channel::TwitterProfile': 'i-woot-x',
|
||||
'Channel::WebWidget': 'i-woot-website',
|
||||
'Channel::Whatsapp': 'i-woot-whatsapp',
|
||||
'Channel::Instagram': 'i-woot-instagram',
|
||||
'Channel::Tiktok': 'i-woot-tiktok',
|
||||
'Channel::Voice': 'i-ri-phone-fill',
|
||||
'Channel::Voice': 'i-woot-voice',
|
||||
};
|
||||
|
||||
const providerIconMap = {
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('useChannelIcon', () => {
|
||||
it('returns correct icon for Voice channel', () => {
|
||||
const inbox = { channel_type: 'Channel::Voice' };
|
||||
const { value: icon } = useChannelIcon(inbox);
|
||||
expect(icon).toBe('i-ri-phone-fill');
|
||||
expect(icon).toBe('i-woot-voice');
|
||||
});
|
||||
|
||||
it('returns correct icon for Line channel', () => {
|
||||
@@ -46,7 +46,7 @@ describe('useChannelIcon', () => {
|
||||
it('returns correct icon for Twitter channel', () => {
|
||||
const inbox = { channel_type: 'Channel::TwitterProfile' };
|
||||
const { value: icon } = useChannelIcon(inbox);
|
||||
expect(icon).toBe('i-ri-twitter-x-fill');
|
||||
expect(icon).toBe('i-woot-x');
|
||||
});
|
||||
|
||||
it('returns correct icon for WebWidget channel', () => {
|
||||
|
||||
@@ -108,7 +108,7 @@ onMounted(() => {
|
||||
<label
|
||||
v-if="label"
|
||||
:for="uniqueId"
|
||||
class="mb-0.5 text-sm font-medium text-n-slate-12"
|
||||
class="mb-0.5 text-heading-3 text-n-slate-12"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
@@ -145,7 +145,7 @@ onMounted(() => {
|
||||
/>
|
||||
<p
|
||||
v-if="message"
|
||||
class="min-w-0 mt-1 mb-0 text-xs truncate transition-all duration-500 ease-in-out"
|
||||
class="min-w-0 mt-1 mb-0 text-label-small truncate transition-all duration-500 ease-in-out"
|
||||
:class="messageClass"
|
||||
>
|
||||
{{ message }}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: [Object, String],
|
||||
required: true,
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'slate',
|
||||
validator: value =>
|
||||
['slate', 'amber', 'teal', 'ruby', 'blue', 'iris'].includes(value),
|
||||
},
|
||||
});
|
||||
|
||||
const COLOR_CLASSES = {
|
||||
slate: 'bg-n-label-color outline-n-label-border text-n-slate-12',
|
||||
amber: 'bg-n-amber-2 outline-n-amber-4 text-n-amber-11',
|
||||
teal: 'bg-n-teal-2 outline-n-teal-4 text-n-teal-11',
|
||||
ruby: 'bg-n-ruby-2 outline-n-ruby-4 text-n-ruby-11',
|
||||
blue: 'bg-n-blue-2 outline-n-blue-4 text-n-blue-11',
|
||||
iris: 'bg-n-iris-2 outline-n-iris-4 text-n-iris-11',
|
||||
};
|
||||
|
||||
const isStringLabel = computed(() => typeof props.label === 'string');
|
||||
|
||||
const labelTitle = computed(() => {
|
||||
return isStringLabel.value ? props.label : props.label?.title;
|
||||
});
|
||||
|
||||
const labelDescription = computed(() => {
|
||||
return (!isStringLabel.value && props.label?.description) || '';
|
||||
});
|
||||
|
||||
const labelColor = computed(() => {
|
||||
return isStringLabel.value ? null : props.label.color;
|
||||
});
|
||||
|
||||
const colorClasses = computed(() => COLOR_CLASSES[props.color]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:title="labelDescription"
|
||||
class="rounded-lg -outline-offset-1 outline outline-1 inline-flex items-center flex-shrink-0"
|
||||
:class="[
|
||||
colorClasses,
|
||||
compact ? 'px-1.5 h-6 gap-1 rounded-md' : 'px-2.5 h-8 gap-1.5 rounded-lg',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
v-if="labelColor"
|
||||
class="rounded-sm flex-shrink-0"
|
||||
:class="compact ? 'size-1.5' : 'size-2'"
|
||||
:style="{ background: labelColor }"
|
||||
/>
|
||||
<slot v-else name="icon" />
|
||||
<span
|
||||
class="whitespace-nowrap"
|
||||
:class="compact ? 'text-label-small' : 'text-label !font-420'"
|
||||
>
|
||||
{{ labelTitle }}
|
||||
</span>
|
||||
<slot name="action" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -71,10 +71,10 @@ const pageInfo = computed(() => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex justify-between h-12 w-full max-w-[calc(60rem-3px)] outline outline-n-container outline-1 -outline-offset-1 mx-auto bg-n-solid-2 rounded-xl py-2 ltr:pl-4 rtl:pr-4 ltr:pr-3 rtl:pl-3 items-center before:absolute before:inset-x-0 before:-top-4 before:bg-gradient-to-t before:from-n-surface-1 before:from-10% before:dark:from-0% before:to-transparent before:h-4 before:pointer-events-none"
|
||||
class="flex justify-between h-[3.375rem] w-full border-t border-n-weak mx-auto bg-n-surface-1 py-3 px-6 items-center before:absolute before:inset-x-0 before:-top-4 before:bg-gradient-to-t before:from-n-surface-1 before:from-0% before:to-transparent before:h-4 before:pointer-events-none"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="min-w-0 text-sm font-normal line-clamp-1 text-n-slate-11">
|
||||
<span class="min-w-0 text-body-main line-clamp-1 text-n-slate-11">
|
||||
{{ currentPageInformation }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -97,11 +97,13 @@ const pageInfo = computed(() => {
|
||||
:disabled="isFirstPage"
|
||||
@click="changePage(currentPage - 1)"
|
||||
/>
|
||||
<div class="inline-flex items-center gap-2 text-sm text-n-slate-11">
|
||||
<span class="px-3 tabular-nums py-0.5 bg-n-alpha-black2 rounded-md">
|
||||
<div class="inline-flex items-center gap-2 text-sm">
|
||||
<span
|
||||
class="px-3 tabular-nums py-0.5 font-420 bg-n-input-background text-body-main text-n-slate-12 rounded-md"
|
||||
>
|
||||
{{ formatFullNumber(currentPage) }}
|
||||
</span>
|
||||
<span class="truncate">
|
||||
<span class="truncate text-body-main text-n-slate-11">
|
||||
{{ pageInfo }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup>
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
defineProps({
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
validator: options =>
|
||||
options.every(
|
||||
opt => typeof opt === 'object' && 'value' in opt && 'label' in opt
|
||||
),
|
||||
},
|
||||
groups: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
validator: groups =>
|
||||
groups.every(
|
||||
group =>
|
||||
'label' in group &&
|
||||
Array.isArray(group.options) &&
|
||||
group.options.every(opt => 'value' in opt && 'label' in opt)
|
||||
),
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
error: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const modelValue = defineModel({
|
||||
type: [String, Number, Boolean],
|
||||
default: '',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-fit relative">
|
||||
<select
|
||||
v-model="modelValue"
|
||||
:disabled="disabled"
|
||||
class="appearance-none bg-none rounded-lg border-0 outline-1 outline -outline-offset-1 transition-all duration-200 bg-n-surface-1 !mb-0 py-2 px-3 pr-10 text-sm"
|
||||
:class="{
|
||||
'outline-n-weak hover:outline-n-slate-6 focus:outline-n-blue-9':
|
||||
!error && !disabled,
|
||||
'outline-n-red-9 focus:outline-n-red-9': error && !disabled,
|
||||
'outline-n-weak bg-n-slate-2 cursor-not-allowed opacity-60': disabled,
|
||||
}"
|
||||
>
|
||||
<option v-if="placeholder" value="" disabled>
|
||||
{{ placeholder }}
|
||||
</option>
|
||||
<template v-if="groups.length">
|
||||
<optgroup
|
||||
v-for="group in groups"
|
||||
:key="group.label"
|
||||
:label="group.label"
|
||||
>
|
||||
<option
|
||||
v-for="option in group.options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
:disabled="option.disabled"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</template>
|
||||
<template v-else>
|
||||
<option
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
:disabled="option.disabled"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</template>
|
||||
</select>
|
||||
<div
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none"
|
||||
>
|
||||
<Icon
|
||||
icon="i-lucide-chevron-down"
|
||||
class="size-4 text-n-slate-11"
|
||||
:class="{ 'opacity-50': disabled }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,8 +25,8 @@ const reauthorizationRequired = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="size-5 grid place-content-center rounded-full bg-n-alpha-2">
|
||||
<ChannelIcon :inbox="inbox" class="size-3" />
|
||||
<span class="size-4 grid place-content-center rounded-full">
|
||||
<ChannelIcon :inbox="inbox" class="size-4" />
|
||||
</span>
|
||||
<div class="flex-1 truncate min-w-0">{{ label }}</div>
|
||||
<div
|
||||
|
||||
@@ -286,7 +286,7 @@ const menuItems = computed(() => {
|
||||
children: sortedInboxes.value.map(inbox => ({
|
||||
name: `${inbox.name}-${inbox.id}`,
|
||||
label: inbox.name,
|
||||
icon: h(ChannelIcon, { inbox, class: 'size-[12px]' }),
|
||||
icon: h(ChannelIcon, { inbox, class: 'size-[16px]' }),
|
||||
to: accountScopedRoute('inbox_dashboard', { inbox_id: inbox.id }),
|
||||
component: leafProps =>
|
||||
h(ChannelLeaf, {
|
||||
@@ -595,6 +595,16 @@ const menuItems = computed(() => {
|
||||
name: 'Settings Teams',
|
||||
label: t('SIDEBAR.TEAMS'),
|
||||
icon: 'i-lucide-users',
|
||||
activeOn: [
|
||||
'settings_teams_list',
|
||||
'settings_teams_new',
|
||||
'settings_teams_finish',
|
||||
'settings_teams_add_agents',
|
||||
'settings_teams_show',
|
||||
'settings_teams_edit',
|
||||
'settings_teams_edit_members',
|
||||
'settings_teams_edit_finish',
|
||||
],
|
||||
to: accountScopedRoute('settings_teams_list'),
|
||||
},
|
||||
...(hasAdvancedAssignment.value
|
||||
@@ -603,6 +613,15 @@ const menuItems = computed(() => {
|
||||
name: 'Settings Agent Assignment',
|
||||
label: t('SIDEBAR.AGENT_ASSIGNMENT'),
|
||||
icon: 'i-lucide-user-cog',
|
||||
activeOn: [
|
||||
'assignment_policy_index',
|
||||
'agent_assignment_policy_index',
|
||||
'agent_assignment_policy_create',
|
||||
'agent_assignment_policy_edit',
|
||||
'agent_capacity_policy_index',
|
||||
'agent_capacity_policy_create',
|
||||
'agent_capacity_policy_edit',
|
||||
],
|
||||
to: accountScopedRoute('assignment_policy_index'),
|
||||
},
|
||||
]
|
||||
@@ -611,6 +630,14 @@ const menuItems = computed(() => {
|
||||
name: 'Settings Inboxes',
|
||||
label: t('SIDEBAR.INBOXES'),
|
||||
icon: 'i-lucide-inbox',
|
||||
activeOn: [
|
||||
'settings_inbox_list',
|
||||
'settings_inbox_show',
|
||||
'settings_inbox_new',
|
||||
'settings_inbox_finish',
|
||||
'settings_inboxes_page_channel',
|
||||
'settings_inboxes_add_agents',
|
||||
],
|
||||
to: accountScopedRoute('settings_inbox_list'),
|
||||
},
|
||||
{
|
||||
@@ -703,7 +730,7 @@ const menuItems = computed(() => {
|
||||
closeMobileSidebar,
|
||||
{ ignore: ['#mobile-sidebar-launcher'] },
|
||||
]"
|
||||
class="bg-n-background flex flex-col text-sm pb-0.5 fixed top-0 ltr:left-0 rtl:right-0 h-full z-40 w-[200px] md:w-auto md:relative md:flex-shrink-0 md:ltr:translate-x-0 md:rtl:translate-x-0 ltr:border-r rtl:border-l border-n-weak"
|
||||
class="bg-n-background flex flex-col text-sm pb-px fixed top-0 ltr:left-0 rtl:right-0 h-full z-40 w-[200px] md:w-auto md:relative md:flex-shrink-0 md:ltr:translate-x-0 md:rtl:translate-x-0 ltr:border-r rtl:border-l border-n-weak"
|
||||
:class="[
|
||||
{
|
||||
'shadow-lg md:shadow-none': isMobileSidebarOpen,
|
||||
@@ -824,7 +851,7 @@ const menuItems = computed(() => {
|
||||
"
|
||||
/>
|
||||
<div
|
||||
class="p-1 flex-shrink-0 flex w-full z-50 gap-2 items-center border-t border-n-weak shadow-[0px_-2px_4px_0px_rgba(27,28,29,0.02)]"
|
||||
class="px-1 py-1.5 flex-shrink-0 flex w-full z-50 gap-2 items-center border-t border-n-weak shadow-[0px_-2px_4px_0px_rgba(27,28,29,0.02)]"
|
||||
:class="isEffectivelyCollapsed ? 'justify-center' : 'justify-between'"
|
||||
>
|
||||
<SidebarProfileMenu
|
||||
|
||||
@@ -44,8 +44,10 @@ const shouldRenderComponent = computed(() => {
|
||||
:active
|
||||
/>
|
||||
<template v-else>
|
||||
<Icon v-if="icon" :icon="icon" class="size-4 inline-block" />
|
||||
<div class="flex-1 truncate min-w-0">{{ label }}</div>
|
||||
<span v-if="icon" class="size-4 grid place-content-center rounded-full">
|
||||
<Icon :icon="icon" class="size-4 inline-block" />
|
||||
</span>
|
||||
<div class="flex-1 truncate min-w-0 text-sm">{{ label }}</div>
|
||||
</template>
|
||||
</component>
|
||||
</Policy>
|
||||
|
||||
@@ -19,20 +19,24 @@ const updateValue = () => {
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="relative h-4 transition-colors duration-200 ease-in-out rounded-full w-7 focus:outline-none focus:ring-1 focus:ring-n-brand focus:ring-offset-n-slate-2 focus:ring-offset-2 flex-shrink-0"
|
||||
:class="modelValue ? 'bg-n-brand' : 'bg-n-slate-6 disabled:bg-n-slate-6/60'"
|
||||
class="group relative h-4 rounded-full w-7 flex-shrink-0 select-none focus:outline-none focus:ring-1 focus:ring-n-brand focus:ring-offset-n-slate-2 focus:ring-offset-2 transition-colors duration-200 ease-in-out"
|
||||
:class="modelValue ? 'bg-n-brand' : 'bg-n-slate-6'"
|
||||
role="switch"
|
||||
:aria-checked="modelValue"
|
||||
@click="updateValue"
|
||||
>
|
||||
<span class="sr-only">{{ t('SWITCH.TOGGLE') }}</span>
|
||||
<span
|
||||
class="absolute top-0.5 left-0.5 h-3 w-3 transform rounded-full shadow-sm transition-transform duration-200 ease-out"
|
||||
class="absolute top-1/2 ltr:left-0.5 rtl:right-0.5 -translate-y-1/2 transition-transform duration-[350ms] ease-[cubic-bezier(0.34,1.56,0.64,1)]"
|
||||
:class="
|
||||
modelValue
|
||||
? 'translate-x-3 bg-n-background'
|
||||
: 'translate-x-0 bg-n-background'
|
||||
? 'ltr:translate-x-3 rtl:-translate-x-3 group-active:ltr:translate-x-[6px] rtl:group-active:-translate-x-[6px]'
|
||||
: 'ltr:translate-x-0 rtl:translate-x-0'
|
||||
"
|
||||
/>
|
||||
>
|
||||
<span
|
||||
class="block h-3 w-3 rounded-full bg-n-background shadow-md transition-[width] duration-[180ms] ease-in-out group-active:w-[18px]"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { BaseTable, BaseTableRow, BaseTableCell } from './index';
|
||||
import Button from '../button/Button.vue';
|
||||
import Avatar from '../avatar/Avatar.vue';
|
||||
import ToggleSwitch from '../switch/Switch.vue';
|
||||
|
||||
const automationData = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Welcome Message',
|
||||
description: 'Send welcome message to new contacts',
|
||||
active: true,
|
||||
createdOn: 'Apr 21, 2022',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Auto-assign to Sales',
|
||||
description: 'Automatically assign sales conversations to sales team',
|
||||
active: false,
|
||||
createdOn: 'May 15, 2022',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Tag Premium Users',
|
||||
description: 'Add premium tag to conversations from premium users',
|
||||
active: true,
|
||||
createdOn: 'Jun 10, 2022',
|
||||
},
|
||||
]);
|
||||
|
||||
const agentData = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
role: 'Administrator',
|
||||
verified: true,
|
||||
avatarUrl: '',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Jane Smith',
|
||||
email: 'jane@example.com',
|
||||
role: 'Agent',
|
||||
verified: true,
|
||||
avatarUrl: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const emptyData = ref([]);
|
||||
|
||||
const headers = ['Name', 'Active', 'Created on', 'Actions'];
|
||||
const agentHeaders = ['Agent', 'Role', 'Verification', 'Actions'];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/Table" :layout="{ type: 'grid', width: '100%' }">
|
||||
<!-- Basic Table -->
|
||||
<Variant title="Basic Table">
|
||||
<div class="p-4 bg-n-surface-1">
|
||||
<BaseTable :headers="headers" :items="automationData">
|
||||
<template #row="{ items }">
|
||||
<BaseTableRow
|
||||
v-for="automation in items"
|
||||
:key="automation.id"
|
||||
:item="automation"
|
||||
>
|
||||
<template #default>
|
||||
<BaseTableCell>
|
||||
<div class="flex items-center gap-2 min-w-0 max-w-full">
|
||||
<span
|
||||
class="text-body-main text-n-slate-12 truncate min-w-0 flex-1"
|
||||
>
|
||||
{{ automation.name }}
|
||||
</span>
|
||||
<div class="w-px h-3 rounded-lg bg-n-weak flex-shrink-0" />
|
||||
<span
|
||||
class="text-body-main text-n-slate-11 truncate min-w-0 flex-1"
|
||||
>
|
||||
{{ automation.description }}
|
||||
</span>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell>
|
||||
<div class="flex justify-center">
|
||||
<ToggleSwitch v-model="automation.active" />
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell>
|
||||
<span
|
||||
class="text-body-main text-n-slate-12 whitespace-nowrap"
|
||||
>
|
||||
{{ automation.createdOn }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell align="end" class="w-24">
|
||||
<div class="flex gap-3 justify-end flex-shrink-0">
|
||||
<Button icon="i-woot-edit-pen" slate sm />
|
||||
<Button icon="i-woot-bin" slate sm />
|
||||
<Button icon="i-woot-clone" slate sm />
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<!-- Table with Avatars -->
|
||||
<Variant title="Table with Avatars">
|
||||
<div class="p-4 bg-n-surface-1">
|
||||
<BaseTable :headers="agentHeaders" :items="agentData">
|
||||
<template #row="{ items }">
|
||||
<BaseTableRow v-for="agent in items" :key="agent.id" :item="agent">
|
||||
<template #default>
|
||||
<BaseTableCell>
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<Avatar :user="agent" :size="40" class="flex-shrink-0" />
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="text-body-main text-n-slate-12 truncate">
|
||||
{{ agent.name }}
|
||||
</span>
|
||||
<span class="text-body-main text-n-slate-11 truncate">
|
||||
{{ agent.email }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell>
|
||||
<span
|
||||
class="text-body-main text-n-slate-12 whitespace-nowrap"
|
||||
>
|
||||
{{ agent.role }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell>
|
||||
<span
|
||||
class="text-body-main text-n-slate-12 whitespace-nowrap"
|
||||
>
|
||||
{{ agent.verified ? 'Verified' : 'Pending' }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell align="end" class="w-24">
|
||||
<div class="flex gap-3 justify-end flex-shrink-0">
|
||||
<Button icon="i-woot-edit-pen" slate sm />
|
||||
<Button icon="i-woot-bin" slate sm />
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<!-- Empty State -->
|
||||
<Variant title="Empty State">
|
||||
<div class="p-4 bg-n-surface-1">
|
||||
<BaseTable
|
||||
:headers="headers"
|
||||
:items="emptyData"
|
||||
no-data-message="No automation rules found"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
headers: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
noDataMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const hasHeaderSlot = computed(() => !!props.headers.length);
|
||||
const showHeaders = computed(
|
||||
() => hasHeaderSlot.value && props.items.length > 0
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<table class="min-w-full table-auto divide-y divide-n-weak">
|
||||
<thead v-if="showHeaders" class="border-t border-n-weak">
|
||||
<tr>
|
||||
<th
|
||||
v-for="(header, index) in headers"
|
||||
:key="index"
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 text-start text-heading-3 text-n-slate-12 capitalize"
|
||||
>
|
||||
<slot :name="`header-${index}`" :header="header">
|
||||
{{ header }}
|
||||
</slot>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-n-weak text-n-slate-11">
|
||||
<template v-if="items.length">
|
||||
<slot name="row" :items="items" />
|
||||
</template>
|
||||
<tr v-else-if="noDataMessage && !loading">
|
||||
<td
|
||||
:colspan="headers.length || 1"
|
||||
class="py-20 text-center text-body-main !text-base text-n-slate-11"
|
||||
>
|
||||
{{ noDataMessage }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
align: {
|
||||
type: String,
|
||||
default: 'start',
|
||||
validator: value => ['start', 'center', 'end'].includes(value),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<td
|
||||
class="py-3 ltr:pr-4 rtl:pl-4 text-body-main"
|
||||
:class="{
|
||||
'text-start': align === 'start',
|
||||
'text-center': align === 'center',
|
||||
'text-end': align === 'end',
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</td>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr>
|
||||
<slot :item="item" />
|
||||
</tr>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as BaseTable } from './BaseTable.vue';
|
||||
export { default as BaseTableRow } from './BaseTableRow.vue';
|
||||
export { default as BaseTableCell } from './BaseTableCell.vue';
|
||||
@@ -18,7 +18,7 @@ const props = defineProps({
|
||||
},
|
||||
year: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user