chore: Assignment policy improvements (#12429)

# Pull Request Template

## Description

This PR includes:

1. Ensuring dropdowns always stay within the screen, with auto
top/bottom/left/right positioning.
2. Fixing the dropdown truncation issue.
3. Removing the display of IDs from dropdowns.
4. Increasing the width of the inbox limit input field.
5. Updating the position of the **Update** button in the agent
assignment edit page.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Loom video

https://www.loom.com/share/f20afc854edc42fab69a003f347d089b?sid=5e5f45f3-5950-46ff-b25c-0d1894daaede


## 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:
Sivin Varghese
2025-09-16 14:10:10 +05:30
committed by GitHub
parent cc21016e6d
commit 4cc2f6d6ad
6 changed files with 146 additions and 81 deletions

View File

@@ -1,6 +1,6 @@
<script setup>
import { computed, ref } from 'vue';
import { useToggle } from '@vueuse/core';
import { useToggle, useWindowSize, useElementBounding } from '@vueuse/core';
import { vOnClickOutside } from '@vueuse/components';
import { picoSearch } from '@scmmishra/pico-search';
@@ -26,10 +26,24 @@ const props = defineProps({
const emit = defineEmits(['add']);
const BUFFER_SPACE = 20;
const [showPopover, togglePopover] = useToggle();
const buttonRef = ref();
const dropdownRef = ref();
const searchValue = ref('');
const { width: windowWidth, height: windowHeight } = useWindowSize();
const {
top: buttonTop,
left: buttonLeft,
width: buttonWidth,
height: buttonHeight,
} = useElementBounding(buttonRef);
const { width: dropdownWidth, height: dropdownHeight } =
useElementBounding(dropdownRef);
const filteredItems = computed(() => {
if (!searchValue.value) return props.items;
const query = searchValue.value.toLowerCase();
@@ -42,6 +56,26 @@ const handleAdd = item => {
togglePopover(false);
};
const shouldShowAbove = computed(() => {
if (!buttonRef.value || !dropdownRef.value) return false;
const spaceBelow =
windowHeight.value - (buttonTop.value + buttonHeight.value);
const spaceAbove = buttonTop.value;
return (
spaceBelow < dropdownHeight.value + BUFFER_SPACE && spaceAbove > spaceBelow
);
});
const shouldAlignRight = computed(() => {
if (!buttonRef.value || !dropdownRef.value) return false;
const spaceRight = windowWidth.value - buttonLeft.value;
const spaceLeft = buttonLeft.value + buttonWidth.value;
return (
spaceRight < dropdownWidth.value + BUFFER_SPACE && spaceLeft > spaceRight
);
});
const handleClickOutside = () => {
if (showPopover.value) {
togglePopover(false);
@@ -55,6 +89,7 @@ const handleClickOutside = () => {
class="relative flex items-center group"
>
<Button
ref="buttonRef"
slate
type="button"
icon="i-lucide-plus"
@@ -64,7 +99,12 @@ const handleClickOutside = () => {
/>
<div
v-if="showPopover"
class="top-full mt-2 ltr:right-0 rtl:left-0 xl:ltr:left-0 xl:rtl:right-0 z-50 flex flex-col items-start absolute bg-n-alpha-3 backdrop-blur-[50px] border-0 gap-4 outline outline-1 outline-n-weak rounded-xl max-w-96 min-w-80 max-h-[20rem] overflow-y-auto py-2"
ref="dropdownRef"
class="z-50 flex flex-col items-start absolute bg-n-alpha-3 backdrop-blur-[50px] border-0 gap-4 outline outline-1 outline-n-weak rounded-xl max-w-96 min-w-80 max-h-[20rem] overflow-y-auto py-2"
:class="[
shouldShowAbove ? 'bottom-full mb-2' : 'top-full mt-2',
shouldAlignRight ? 'right-0' : 'left-0',
]"
>
<div class="flex flex-col divide-y divide-n-slate-4 w-full">
<Input
@@ -90,7 +130,7 @@ const handleClickOutside = () => {
<Icon
v-if="item.icon"
:icon="item.icon"
class="size-2 text-n-slate-12 flex-shrink-0 mt-0.5"
class="size-4 text-n-slate-12 flex-shrink-0 mt-0.5"
/>
<span
v-else-if="item.color"
@@ -105,24 +145,19 @@ const handleClickOutside = () => {
:size="20"
rounded-full
/>
<div class="flex flex-col items-start gap-2 min-w-0">
<div class="flex items-center gap-1 min-w-0">
<div class="flex flex-col items-start gap-2 min-w-0 flex-1">
<div class="flex items-center gap-1 min-w-0 w-full">
<span
:title="item.name || item.title"
class="text-sm text-n-slate-12 truncate min-w-0"
class="text-sm text-n-slate-12 truncate min-w-0 flex-1"
>
{{ item.name || item.title }}
</span>
<span
v-if="item.id"
class="text-xs text-n-slate-11 flex-shrink-0"
>
{{ `#${item.id}` }}
</span>
</div>
<span
v-if="item.email || item.phoneNumber"
class="text-sm text-n-slate-11 truncate min-w-0"
:title="item.email || item.phoneNumber"
class="text-sm text-n-slate-11 truncate min-w-0 w-full block"
>
{{ item.email || item.phoneNumber }}
</span>

View File

@@ -119,7 +119,7 @@ onMounted(() => {
)
"
:items="filteredTags"
class="[&>button]:!text-n-blue-text"
class="[&>button]:!text-n-blue-text [&>div]:min-w-64"
@add="onClickAddTag"
/>
</div>

View File

@@ -110,53 +110,67 @@ const getInboxName = inboxId => {
<div
v-for="(limit, index) in inboxCapacityLimits"
:key="limit.id || `temp-${index}`"
class="flex items-center gap-3"
class="flex flex-col xs:flex-row items-stretch gap-3"
>
<div
class="flex items-start rounded-lg outline-1 outline cursor-not-allowed text-n-slate-11 outline-n-weak py-2.5 px-3 text-sm w-full"
class="flex items-center rounded-lg outline-1 outline cursor-not-allowed text-n-slate-11 outline-n-weak py-2.5 px-3 text-sm w-full min-w-0"
:title="getInboxName(limit.inboxId)"
>
{{ getInboxName(limit.inboxId) }}
<span class="truncate min-w-0">
{{ getInboxName(limit.inboxId) }}
</span>
</div>
<div
class="py-2.5 px-3 rounded-lg gap-2 outline outline-1 flex-shrink-0 flex items-center"
:class="[
!isLimitValid(limit) ? 'outline-n-ruby-8' : 'outline-n-weak',
]"
>
<label class="text-sm text-n-slate-12 ltr:pr-2 rtl:pl-2">
{{
t(`${BASE_KEY}.FORM.INBOX_CAPACITY_LIMIT.FIELD.MAX_CONVERSATIONS`)
}}
</label>
<div class="h-5 w-px bg-n-weak" />
<input
v-model.number="limit.conversationLimit"
type="number"
:min="MIN_CONVERSATION_LIMIT"
:max="MAX_CONVERSATION_LIMIT"
class="reset-base bg-transparent focus:outline-none max-w-20 text-sm"
<div class="flex items-center gap-3 w-full xs:w-auto">
<div
class="py-2.5 px-3 rounded-lg gap-2 outline outline-1 flex-1 xs:flex-shrink-0 flex items-center min-w-0"
:class="[
!isLimitValid(limit)
? 'placeholder:text-n-ruby-9 !text-n-ruby-9'
: 'placeholder:text-n-slate-10 text-n-slate-12',
!isLimitValid(limit) ? 'outline-n-ruby-8' : 'outline-n-weak',
]"
:placeholder="
t(`${BASE_KEY}.FORM.INBOX_CAPACITY_LIMIT.FIELD.SET_LIMIT`)
"
@blur="handleLimitChange(limit)"
>
<label
class="text-sm text-n-slate-12 ltr:pr-2 rtl:pl-2 truncate min-w-0 flex-shrink"
:title="
t(
`${BASE_KEY}.FORM.INBOX_CAPACITY_LIMIT.FIELD.MAX_CONVERSATIONS`
)
"
>
{{
t(
`${BASE_KEY}.FORM.INBOX_CAPACITY_LIMIT.FIELD.MAX_CONVERSATIONS`
)
}}
</label>
<div class="h-5 w-px bg-n-weak" />
<input
v-model.number="limit.conversationLimit"
type="number"
:min="MIN_CONVERSATION_LIMIT"
:max="MAX_CONVERSATION_LIMIT"
class="reset-base bg-transparent focus:outline-none min-w-16 w-24 text-sm flex-shrink-0"
:class="[
!isLimitValid(limit)
? 'placeholder:text-n-ruby-9 !text-n-ruby-9'
: 'placeholder:text-n-slate-10 text-n-slate-12',
]"
:placeholder="
t(`${BASE_KEY}.FORM.INBOX_CAPACITY_LIMIT.FIELD.SET_LIMIT`)
"
@blur="handleLimitChange(limit)"
/>
</div>
<Button
type="button"
slate
icon="i-lucide-trash"
class="flex-shrink-0"
@click="handleRemoveLimit(limit.id)"
/>
</div>
<Button
type="button"
slate
icon="i-lucide-trash"
class="flex-shrink-0"
@click="handleRemoveLimit(limit.id)"
/>
</div>
</div>
</div>

View File

@@ -5,6 +5,7 @@ import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useRoute, useRouter } from 'vue-router';
import { useAlert } from 'dashboard/composables';
import camelcaseKeys from 'camelcase-keys';
import { getInboxIconByType } from 'dashboard/helper/inbox';
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
import SettingsLayout from 'dashboard/routes/dashboard/settings/SettingsLayout.vue';
@@ -58,7 +59,19 @@ const allAgents = computed(() =>
const allLabels = computed(() => buildList(labelsList.value));
const allInboxes = computed(() => buildList(inboxes.value));
const allInboxes = computed(
() =>
inboxes.value
?.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map(({ name, id, email, phoneNumber, channelType, medium }) => ({
name,
id,
email,
phoneNumber,
icon: getInboxIconByType(channelType, medium, 'line'),
})) || []
);
const formData = computed(() => ({
name: selectedPolicy.value?.name || '',

View File

@@ -215,33 +215,6 @@ defineExpose({
v-model:window-unit="state.windowUnit"
/>
</div>
<div v-if="showInboxSection" class="py-4 flex-col flex gap-4">
<div class="flex items-end gap-4 w-full justify-between">
<div class="flex flex-col items-start gap-1 py-1">
<label class="text-sm font-medium text-n-slate-12 py-1">
{{ t(`${BASE_KEY}.FORM.INBOXES.LABEL`) }}
</label>
<p class="mb-0 text-n-slate-11 text-sm">
{{ t(`${BASE_KEY}.FORM.INBOXES.DESCRIPTION`) }}
</p>
</div>
<AddDataDropdown
:label="t(`${BASE_KEY}.FORM.INBOXES.ADD_BUTTON`)"
:search-placeholder="
t(`${BASE_KEY}.FORM.INBOXES.DROPDOWN.SEARCH_PLACEHOLDER`)
"
:items="inboxList"
@add="$emit('addInbox', $event)"
/>
</div>
<DataTable
:items="policyInboxes"
:is-fetching="isInboxLoading"
:empty-state-message="t(`${BASE_KEY}.FORM.INBOXES.EMPTY_STATE`)"
@delete="$emit('deleteInbox', $event)"
/>
</div>
</div>
<Button
@@ -250,5 +223,35 @@ defineExpose({
:disabled="!validationState.isValid || isLoading"
:is-loading="isLoading"
/>
<div
v-if="showInboxSection"
class="py-4 flex-col flex gap-4 border-t border-n-weak mt-6"
>
<div class="flex items-end gap-4 w-full justify-between">
<div class="flex flex-col items-start gap-1 py-1">
<label class="text-sm font-medium text-n-slate-12 py-1">
{{ t(`${BASE_KEY}.FORM.INBOXES.LABEL`) }}
</label>
<p class="mb-0 text-n-slate-11 text-sm">
{{ t(`${BASE_KEY}.FORM.INBOXES.DESCRIPTION`) }}
</p>
</div>
<AddDataDropdown
:label="t(`${BASE_KEY}.FORM.INBOXES.ADD_BUTTON`)"
:search-placeholder="
t(`${BASE_KEY}.FORM.INBOXES.DROPDOWN.SEARCH_PLACEHOLDER`)
"
:items="inboxList"
@add="$emit('addInbox', $event)"
/>
</div>
<DataTable
:items="policyInboxes"
:is-fetching="isInboxLoading"
:empty-state-message="t(`${BASE_KEY}.FORM.INBOXES.EMPTY_STATE`)"
@delete="$emit('deleteInbox', $event)"
/>
</div>
</form>
</template>

View File

@@ -145,7 +145,7 @@ defineExpose({
<template>
<form @submit.prevent="handleSubmit">
<div class="flex flex-col gap-4 divide-y divide-n-weak">
<div class="flex flex-col gap-4 mb-2 divide-y divide-n-weak">
<BaseInfo
v-model:policy-name="state.name"
v-model:description="state.description"