feat: Update conversation side panel layout and styling (#12983)

This commit is contained in:
Sivin Varghese
2025-12-04 13:55:07 +05:30
committed by GitHub
parent 832d61c004
commit 604d4a9c30
68 changed files with 2336 additions and 2553 deletions
@@ -1,28 +1,19 @@
<script setup>
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
import { defineEmits } from 'vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
defineProps({
title: {
type: String,
required: true,
},
compact: {
type: Boolean,
default: false,
},
icon: {
type: String,
default: '',
},
emoji: {
type: String,
default: '',
},
isOpen: {
type: Boolean,
default: true,
},
compact: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['toggle']);
@@ -33,31 +24,30 @@ const onToggle = () => {
</script>
<template>
<div class="text-sm">
<div class="text-sm py-4 grid gap-2">
<button
class="flex items-center select-none w-full rounded-lg bg-n-slate-2 outline outline-1 outline-n-weak m-0 cursor-grab justify-between py-2 px-4 drag-handle"
:class="{ 'rounded-bl-none rounded-br-none': isOpen }"
class="flex items-center select-none w-full m-0 cursor-grab justify-between drag-handle px-4 py-0 focus-visible:outline-none outline-none !transition-none"
@click.stop="onToggle"
>
<div class="flex justify-between">
<EmojiOrIcon class="inline-block w-5" :icon="icon" :emoji="emoji" />
<h5 class="text-n-slate-12 text-sm mb-0 py-0 pr-2 pl-0">
{{ title }}
</h5>
</div>
<div class="flex flex-row">
<slot name="button" />
<div class="flex justify-end w-3 text-n-blue-text cursor-pointer">
<fluent-icon v-if="isOpen" size="24" icon="subtract" type="solid" />
<fluent-icon v-else size="24" icon="add" type="solid" />
</div>
<h5 class="text-n-slate-12 text-sm font-medium mb-0 py-0 pr-2 pl-0">
{{ title }}
</h5>
<div
class="flex justify-end w-3 text-n-blue-text flex-shrink-0 cursor-pointer size-4"
>
<Icon
v-if="isOpen"
icon="i-lucide-chevron-up"
class="size-4 text-n-slate-11"
/>
<Icon
v-else
icon="i-lucide-chevron-down"
class="size-4 text-n-slate-11"
/>
</div>
</button>
<div
v-if="isOpen"
class="outline outline-1 outline-n-weak -mt-[-1px] border-t-0 rounded-br-lg rounded-bl-lg"
:class="compact ? 'p-0' : 'px-2 py-4'"
>
<div v-if="isOpen" :class="compact ? 'px-1' : 'px-4'">
<slot />
</div>
</div>
@@ -82,7 +82,7 @@ useKeyboardEvents({
>
<Virtualizer
:data="items"
class="[&>div]:after:content-[''] [&>div]:after:absolute [&>div]:after:bottom-0 [&>div]:after:left-0 [&>div]:after:right-0 [&>div]:after:h-px [&>div]:after:bg-n-weak [&>div]:after:pointer-events-none [&>div]:after:transition-colors [&>div]:after:duration-150 [&>div:has(*:hover)]:after:!bg-n-surface-1 [&>div:has(+_*:hover)]:after:!bg-n-surface-1 [&>div:has(.active)]:after:!bg-n-surface-1 [&>div:has(+_*_.active)]:after:!bg-n-surface-1 [&>div:has(.selected)]:after:!bg-n-surface-1 [&>div:has(+_*_.selected)]:after:!bg-n-surface-1"
class="[&>div]:after:content-[''] [&>div]:after:absolute [&>div]:after:bottom-0 [&>div]:after:left-0 [&>div]:after:right-0 [&>div]:after:h-px [&>div]:after:bg-n-weak [&>div]:after:pointer-events-none [&>div:has(*:hover)]:after:!bg-n-surface-1 [&>div:has(+_*:hover)]:after:!bg-n-surface-1 [&>div:has(.active)]:after:!bg-n-surface-1 [&>div:has(+_*_.active)]:after:!bg-n-surface-1 [&>div:has(.selected)]:after:!bg-n-surface-1 [&>div:has(+_*_.selected)]:after:!bg-n-surface-1"
>
<template #default="{ item }">
<div :data-id="item.id">
@@ -1,351 +0,0 @@
<script>
import { format, parseISO } from 'date-fns';
import { required, url } from '@vuelidate/validators';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
import HelperTextPopup from 'dashboard/components/ui/HelperTextPopup.vue';
import { isValidURL } from '../helper/URLHelper';
import { getRegexp } from 'shared/helpers/Validators';
import { useVuelidate } from '@vuelidate/core';
import { emitter } from 'shared/helpers/mitt';
import NextButton from 'dashboard/components-next/button/Button.vue';
const DATE_FORMAT = 'yyyy-MM-dd';
export default {
components: {
MultiselectDropdown,
HelperTextPopup,
NextButton,
},
props: {
label: { type: String, required: true },
description: { type: String, default: '' },
values: { type: Array, default: () => [] },
value: { type: [String, Number, Boolean], default: '' },
showActions: { type: Boolean, default: false },
attributeType: { type: String, default: 'text' },
attributeRegex: {
type: String,
default: null,
},
regexCue: { type: String, default: null },
attributeKey: { type: String, required: true },
contactId: { type: Number, default: null },
},
emits: ['update', 'delete', 'copy'],
setup() {
return { v$: useVuelidate() };
},
data() {
return {
isEditing: false,
editedValue: null,
};
},
computed: {
displayValue() {
if (this.isAttributeTypeDate) {
return this.value
? new Date(this.value || new Date()).toLocaleDateString()
: '---';
}
if (this.isAttributeTypeCheckbox) {
return this.value === 'false' ? false : this.value;
}
return this.hasValue ? this.value : '---';
},
formattedValue() {
return this.isAttributeTypeDate
? format(this.value ? new Date(this.value) : new Date(), DATE_FORMAT)
: this.value;
},
listOptions() {
return this.values.map((value, index) => ({
id: index + 1,
name: value,
}));
},
selectedItem() {
const id = this.values.indexOf(this.editedValue) + 1;
return { id, name: this.editedValue };
},
isAttributeTypeCheckbox() {
return this.attributeType === 'checkbox';
},
isAttributeTypeList() {
return this.attributeType === 'list';
},
isAttributeTypeLink() {
return this.attributeType === 'link';
},
isAttributeTypeDate() {
return this.attributeType === 'date';
},
hasValue() {
return this.value !== null && this.value !== '';
},
urlValue() {
return isValidURL(this.value) ? this.value : '---';
},
hrefURL() {
return isValidURL(this.value) ? this.value : '';
},
notAttributeTypeCheckboxAndList() {
return !this.isAttributeTypeCheckbox && !this.isAttributeTypeList;
},
inputType() {
return this.isAttributeTypeLink ? 'url' : this.attributeType;
},
shouldShowErrorMessage() {
return this.v$.editedValue.$error;
},
errorMessage() {
if (this.v$.editedValue.url) {
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_URL');
}
if (!this.v$.editedValue.regexValidation) {
return this.regexCue
? this.regexCue
: this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_INPUT');
}
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
},
},
watch: {
value() {
this.isEditing = false;
this.editedValue = this.formattedValue;
},
contactId() {
// Fix to solve validation not resetting when contactId changes in contact page
this.v$.$reset();
},
},
validations() {
if (this.isAttributeTypeLink) {
return {
editedValue: { required, url },
};
}
return {
editedValue: {
required,
regexValidation: value => {
return !(
this.attributeRegex && !getRegexp(this.attributeRegex).test(value)
);
},
},
};
},
mounted() {
this.editedValue = this.formattedValue;
emitter.on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
},
unmounted() {
emitter.off(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
},
methods: {
onFocusAttribute(focusAttributeKey) {
if (this.attributeKey === focusAttributeKey) {
this.onEdit();
}
},
focusInput() {
if (this.$refs.inputfield) {
this.$refs.inputfield.focus();
}
},
onClickAway() {
this.v$.$reset();
this.isEditing = false;
},
onEdit() {
this.isEditing = true;
this.$nextTick(() => {
this.focusInput();
});
},
onUpdateListValue(value) {
if (value) {
this.editedValue = value.name;
this.onUpdate();
}
},
onUpdate() {
const updatedValue =
this.attributeType === 'date'
? parseISO(this.editedValue)
: this.editedValue;
this.v$.$touch();
if (this.v$.$invalid) {
return;
}
this.isEditing = false;
this.$emit('update', this.attributeKey, updatedValue);
},
onDelete() {
this.isEditing = false;
this.v$.$reset();
this.$emit('delete', this.attributeKey);
},
onCopy() {
this.$emit('copy', this.value);
},
},
};
</script>
<template>
<div class="px-4 py-3">
<div class="flex items-center mb-1">
<h4 class="flex items-center w-full m-0 text-sm error">
<div v-if="isAttributeTypeCheckbox" class="flex items-center">
<input
v-model="editedValue"
class="!my-0 ltr:mr-2 ltr:ml-0 rtl:mr-0 rtl:ml-2"
type="checkbox"
@change="onUpdate"
/>
</div>
<div class="flex items-center justify-between w-full">
<span
class="w-full inline-flex gap-1.5 items-start font-medium whitespace-nowrap text-sm mb-0"
:class="
v$.editedValue.$error ? 'text-n-ruby-11' : 'text-n-slate-12'
"
>
{{ label }}
<HelperTextPopup
v-if="description"
:message="description"
class="mt-0.5"
/>
</span>
<NextButton
v-if="showActions && hasValue"
v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
slate
sm
link
icon="i-lucide-trash-2"
@click="onDelete"
/>
</div>
</h4>
</div>
<div v-if="notAttributeTypeCheckboxAndList">
<div v-if="isEditing" v-on-clickaway="onClickAway">
<div class="flex items-center w-full mb-2">
<input
ref="inputfield"
v-model="editedValue"
:type="inputType"
class="!h-8 ltr:!rounded-r-none rtl:!rounded-l-none !mb-0 !text-sm"
autofocus="true"
:class="{ error: v$.editedValue.$error }"
@blur="v$.editedValue.$touch"
@keyup.enter="onUpdate"
/>
<div>
<NextButton
sm
icon="i-lucide-check"
class="ltr:rounded-l-none rtl:rounded-r-none h-[34px]"
@click="onUpdate"
/>
</div>
</div>
<span
v-if="shouldShowErrorMessage"
class="block w-full -mt-px text-sm font-normal text-n-ruby-11"
>
{{ errorMessage }}
</span>
</div>
<div
v-show="!isEditing"
class="flex group"
:class="{ 'is-editable': showActions }"
>
<a
v-if="isAttributeTypeLink"
:href="hrefURL"
target="_blank"
rel="noopener noreferrer"
class="group-hover:bg-n-slate-3 group-hover:dark:bg-n-solid-3 inline-block rounded-sm mb-0 break-all py-0.5 px-1"
>
{{ urlValue }}
</a>
<p
v-else
class="group-hover:bg-n-slate-3 group-hover:dark:bg-n-solid-3 inline-block rounded-sm mb-0 break-all py-0.5 px-1"
>
{{ displayValue }}
</p>
<div
class="flex items-center max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0"
>
<NextButton
v-if="showActions && hasValue"
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
xs
slate
ghost
icon="i-lucide-clipboard"
class="hidden group-hover:flex flex-shrink-0"
@click="onCopy"
/>
<NextButton
v-if="showActions"
v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
xs
slate
ghost
icon="i-lucide-pen"
class="hidden group-hover:flex flex-shrink-0"
@click="onEdit"
/>
</div>
</div>
</div>
<div v-if="isAttributeTypeList">
<MultiselectDropdown
:options="listOptions"
:selected-item="selectedItem"
:has-thumbnail="false"
:multiselector-placeholder="
$t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.PLACEHOLDER')
"
:no-search-result="
$t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.NO_RESULT')
"
:input-placeholder="
$t(
'CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.SEARCH_INPUT_PLACEHOLDER'
)
"
@select="onUpdateListValue"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
::v-deep {
.selector-wrap {
@apply m-0 top-1;
.selector-name {
@apply ml-0;
}
}
.name {
@apply ml-0;
}
}
</style>
@@ -7,8 +7,6 @@ import { useStore, useStoreGetters } from 'dashboard/composables/store';
import { useEmitter } from 'dashboard/composables/emitter';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import wootConstants from 'dashboard/constants/globals';
import {
CMD_REOPEN_CONVERSATION,
@@ -186,34 +184,30 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
<div
v-if="showActionsDropdown"
v-on-clickaway="closeDropdown"
class="border rounded-lg shadow-lg border-n-strong dark:border-n-strong box-content p-2 w-fit z-10 bg-n-alpha-3 backdrop-blur-[100px] absolute block left-auto top-full mt-0.5 start-0 xl:start-auto xl:end-0 max-w-[12.5rem] min-w-[9.75rem] [&_ul>li]:mb-0"
class="border rounded-xl shadow-lg border-n-strong dark:border-n-strong box-content p-2 w-fit z-10 bg-n-alpha-3 backdrop-blur-[100px] absolute block left-auto top-full mt-0.5 start-0 lg:start-auto lg:end-0 max-w-[12.5rem] min-w-[9.75rem] [&_ul>li]:mb-0"
>
<WootDropdownMenu class="mb-0">
<WootDropdownItem v-if="!isPending">
<Button
:label="t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL')"
ghost
slate
sm
start
icon="i-lucide-alarm-clock-minus"
class="w-full"
@click="() => openSnoozeModal()"
/>
</WootDropdownItem>
<WootDropdownItem v-if="!isPending">
<Button
:label="t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING')"
ghost
slate
sm
start
icon="i-lucide-circle-dot-dashed"
class="w-full"
@click="() => toggleStatus(wootConstants.STATUS_TYPE.PENDING)"
/>
</WootDropdownItem>
</WootDropdownMenu>
<Button
v-if="!isPending"
:label="t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL')"
ghost
slate
sm
start
icon="i-lucide-alarm-clock-minus"
class="w-full"
@click="() => openSnoozeModal()"
/>
<Button
v-if="!isPending"
:label="t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING')"
ghost
slate
sm
start
icon="i-lucide-circle-dot-dashed"
class="w-full"
@click="() => toggleStatus(wootConstants.STATUS_TYPE.PENDING)"
/>
</div>
</div>
</template>
@@ -133,7 +133,7 @@ onMounted(() => {
<div
v-if="shouldShowCopilotPanel"
v-on-click-outside="() => closeCopilotPanel()"
class="bg-n-surface-2 h-full overflow-hidden flex-col fixed top-0 ltr:right-0 rtl:left-0 z-40 w-full max-w-sm transition-transform duration-300 ease-in-out md:static md:w-[320px] md:min-w-[320px] ltr:border-l rtl:border-r border-n-weak 2xl:min-w-[360px] 2xl:w-[360px] shadow-lg md:shadow-none"
class="bg-n-surface-2 h-full overflow-hidden flex-col fixed top-0 ltr:right-0 rtl:left-0 z-40 w-full max-w-sm transition-transform duration-300 ease-in-out md:static md:w-[360px] md:min-w-[360px] ltr:border-l rtl:border-r border-n-weak shadow-lg md:shadow-none"
:class="[
{
'md:flex': shouldShowCopilotPanel,
@@ -5,8 +5,6 @@ import ColorPicker from './widgets/ColorPicker.vue';
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
import DeleteModal from './widgets/modal/DeleteModal.vue';
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import FeatureToggle from './widgets/FeatureToggle.vue';
import Input from './widgets/forms/Input.vue';
import PhoneInput from './widgets/forms/PhoneInput.vue';
@@ -25,8 +23,6 @@ const WootUIKit = {
ConfirmDeleteModal,
ConfirmModal,
DeleteModal,
DropdownItem,
DropdownMenu,
FeatureToggle,
Input,
PhoneInput,
@@ -1,4 +1,6 @@
<script setup>
import Icon from 'dashboard/components-next/icon/Icon.vue';
defineProps({
message: {
type: String,
@@ -8,16 +10,12 @@ defineProps({
</script>
<template>
<div class="relative group w-[inherit] whitespace-normal z-20">
<fluent-icon
icon="info"
size="14"
class="mt-0.5 text-n-slate-11 absolute"
/>
<div class="relative group flex-shrink-0 flex items-center">
<Icon icon="i-lucide-info" class="text-n-slate-11 size-3.5 cursor-help" />
<div
class="bg-n-background w-fit ltr:left-4 rtl:right-4 top-0 border p-2.5 group-hover:flex items-center hidden absolute border-n-weak rounded-lg shadow-md"
class="hidden group-hover:block absolute z-50 ltr:left-5 rtl:right-5 top-0 w-max max-w-64 bg-n-alpha-3 backdrop-blur-[50px] border border-n-weak rounded-xl shadow-lg px-3 py-2"
>
<p class="text-n-slate-12 mb-0 text-xs">
<p class="text-n-slate-12 text-xs leading-relaxed mb-0 whitespace-normal">
{{ message }}
</p>
</div>
@@ -1,69 +0,0 @@
<script setup>
import { computed } from 'vue';
import Avatar from 'next/avatar/Avatar.vue';
const props = defineProps({
usersList: {
type: Array,
default: () => [],
},
size: {
type: Number,
default: 24,
},
showMoreThumbnailsCount: {
type: Boolean,
default: false,
},
moreThumbnailsText: {
type: String,
default: '',
},
gap: {
type: String,
default: 'normal',
validator(value) {
// The value must match one of these strings
return ['normal', 'tight'].includes(value);
},
},
});
const gapClass = computed(() => {
if (props.gap === 'tight') {
return 'ltr:[&:not(:first-child)]:-ml-2 rtl:[&:not(:first-child)]:-mr-2';
}
return 'ltr:[&:not(:first-child)]:-ml-1 rtl:[&:not(:first-child)]:-mr-1';
});
const moreThumbnailsClass = computed(() => {
if (props.gap === 'tight') {
return 'ltr:-ml-2 rtl:-mr-2';
}
return 'ltr:-ml-1 rtl:-mr-1';
});
</script>
<template>
<div class="flex">
<Avatar
v-for="user in usersList"
:key="user.id"
v-tooltip="user.name"
:title="user.name"
:src="user.thumbnail"
:name="user.name"
:size="size"
class="[&>span]:outline [&>span]:outline-1 [&>span]:outline-n-background [&>span]:shadow"
:class="gapClass"
rounded-full
/>
<span
v-if="showMoreThumbnailsCount"
class="text-n-slate-11 bg-n-slate-4 outline outline-1 outline-n-background text-xs font-medium rounded-full px-2 inline-flex items-center shadow relative"
:class="moreThumbnailsClass"
>
{{ moreThumbnailsText }}
</span>
</div>
</template>
@@ -92,6 +92,7 @@ onMounted(() => {
:chat="currentChat"
:is-on-expanded-view="isOnExpandedLayout"
:show-back-button="isOnExpandedLayout && !isInboxView"
:class="{ 'border-b border-n-weak !pb-3': !dashboardApps.length }"
/>
<woot-tabs
v-if="dashboardApps.length && currentChat.id"
@@ -12,6 +12,7 @@ import { conversationListPageURL } from 'dashboard/helper/URLHelper';
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
import { useInbox } from 'dashboard/composables/useInbox';
import { useI18n } from 'vue-i18n';
import { useUISettings } from 'dashboard/composables/useUISettings';
const props = defineProps({
chat: {
@@ -35,6 +36,15 @@ const conversationHeader = ref(null);
const { width } = useElementSize(conversationHeader);
const { isAWebWidgetInbox } = useInbox();
const { uiSettings } = useUISettings();
const isSidepanelOpen = computed(
() =>
uiSettings.value.is_contact_sidebar_open ||
uiSettings.value.is_copilot_panel_open ||
false
);
const currentChat = computed(() => store.getters.getSelectedChat);
const accountId = computed(() => store.getters.getCurrentAccountId);
@@ -91,20 +101,24 @@ const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
ref="conversationHeader"
class="flex flex-col items-start gap-x-6 gap-y-2 w-full min-w-0 pt-3 pb-2 ltr:pl-4 rtl:pr-4 ltr:pr-1 rtl:pl-1"
:class="{
'md:flex-row md:items-center': isOnExpandedView,
'lg:flex-row lg:items-center': !isOnExpandedView,
'md:flex-row md:items-center': isOnExpandedView && !isSidepanelOpen,
'lg:flex-row lg:items-center': isOnExpandedView && isSidepanelOpen,
'lg:flex-row lg:items-center': !isOnExpandedView && !isSidepanelOpen,
'xl:flex-row xl:items-center': !isOnExpandedView && isSidepanelOpen,
}"
>
<div
class="flex items-center gap-2 min-w-0 w-full"
:class="{
'md:flex-1 md:basis-0': isOnExpandedView,
'lg:flex-1 lg:basis-0': !isOnExpandedView,
'md:flex-1 md:basis-0': isOnExpandedView && !isSidepanelOpen,
'lg:flex-1 lg:basis-0': isOnExpandedView && isSidepanelOpen,
'lg:flex-1 lg:basis-0': !isOnExpandedView && !isSidepanelOpen,
'xl:flex-1 xl:basis-0': !isOnExpandedView && isSidepanelOpen,
}"
>
<BackButton v-if="showBackButton" :back-url="backButtonUrl" />
<div class="flex items-center gap-1 min-w-0 flex-1">
<div class="flex items-center gap-1 min-w-0 ltr:mr-2 rtl:ml-2">
<span class="text-base font-medium text-n-slate-12 truncate min-w-0">
{{ currentContact.name }}
</span>
@@ -120,8 +134,14 @@ const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
<div
class="flex items-center gap-2 min-w-0 w-full"
:class="{
'md:flex-1 md:basis-0 md:justify-end': isOnExpandedView,
'lg:flex-1 lg:basis-0 lg:justify-end': !isOnExpandedView,
'md:flex-1 md:basis-0 md:justify-end':
isOnExpandedView && !isSidepanelOpen,
'lg:flex-1 lg:basis-0 lg:justify-end':
isOnExpandedView && isSidepanelOpen,
'lg:flex-1 lg:basis-0 lg:justify-end':
!isOnExpandedView && !isSidepanelOpen,
'xl:flex-1 xl:basis-0 xl:justify-end':
!isOnExpandedView && isSidepanelOpen,
}"
>
<div v-if="isSnoozed" class="flex items-center gap-1 min-w-0 shrink">
@@ -42,7 +42,7 @@ const closeContactPanel = () => {
<template>
<div
v-on-click-outside="() => closeContactPanel()"
class="bg-n-surface-2 h-full overflow-hidden flex flex-col fixed top-0 z-40 w-full max-w-sm transition-transform duration-300 ease-in-out ltr:right-0 rtl:left-0 md:static md:w-[320px] md:min-w-[320px] ltr:border-l rtl:border-r border-n-weak 2xl:min-w-[360px] 2xl:w-[360px] shadow-lg md:shadow-none"
class="bg-n-surface-2 h-full overflow-hidden flex flex-col fixed top-0 z-40 w-full max-w-sm transition-transform duration-300 ease-in-out ltr:right-0 rtl:left-0 md:static ltr:border-l rtl:border-r border-n-weak md:min-w-[360px] md:w-[360px] shadow-lg md:shadow-none"
:class="[
{
'md:flex': activeTab === 0,
@@ -4,6 +4,7 @@ import { useFunctionGetter } from 'dashboard/composables/store';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import ShopifyAPI from '../../../api/integrations/shopify';
import ShopifyOrderItem from './ShopifyOrderItem.vue';
import SidePanelEmptyState from 'dashboard/routes/dashboard/conversation/SidePanelEmptyState.vue';
const props = defineProps({
contactId: {
@@ -47,7 +48,7 @@ watch(
</script>
<template>
<div class="px-4 py-2 text-n-slate-12">
<div class="py-2 px-3 text-n-slate-12">
<div v-if="!hasSearchableInfo" class="text-center text-n-slate-12">
{{ $t('CONVERSATION_SIDEBAR.SHOPIFY.NO_SHOPIFY_ORDERS') }}
</div>
@@ -57,8 +58,10 @@ watch(
<div v-else-if="error" class="text-center text-n-ruby-12">
{{ error }}
</div>
<div v-else-if="!orders.length" class="text-center text-n-slate-12">
{{ $t('CONVERSATION_SIDEBAR.SHOPIFY.NO_SHOPIFY_ORDERS') }}
<div v-else-if="!orders.length" class="mt-2">
<SidePanelEmptyState
:message="$t('CONVERSATION_SIDEBAR.SHOPIFY.NO_SHOPIFY_ORDERS')"
/>
</div>
<div v-else>
<ShopifyOrderItem
@@ -67,7 +67,7 @@ const handleCancel = () => {
};
onMounted(() => {
store.dispatch('teams/fetch');
store.dispatch('teams/get');
});
</script>
@@ -8,6 +8,7 @@ import CreateOrLinkIssue from './CreateOrLinkIssue.vue';
import LinearIssueItem from './LinearIssueItem.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import SidePanelEmptyState from 'dashboard/routes/dashboard/conversation/SidePanelEmptyState.vue';
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
@@ -86,10 +87,10 @@ onMounted(() => {
<template>
<div>
<div class="px-4 pt-3 pb-2">
<div class="pt-3 px-3 pb-2">
<NextButton
ghost
xs
sm
slate
icon="i-lucide-plus"
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK_BUTTON')"
@click="openCreateModal"
@@ -100,17 +101,17 @@ onMounted(() => {
<Spinner />
</div>
<div v-else-if="!hasIssues" class="flex justify-center p-4">
<p class="text-sm text-n-slate-11">
{{ $t('INTEGRATION_SETTINGS.LINEAR.NO_LINKED_ISSUES') }}
</p>
<div v-else-if="!hasIssues" class="mt-2">
<SidePanelEmptyState
:message="$t('INTEGRATION_SETTINGS.LINEAR.NO_LINKED_ISSUES')"
/>
</div>
<div v-else class="max-h-[300px] overflow-y-auto">
<div v-else class="max-h-[300px] overflow-y-auto px-2">
<LinearIssueItem
v-for="linkedIssue in linkedIssues"
:key="linkedIssue.id"
class="px-4 pt-3 pb-4 border-b border-n-weak last:border-b-0"
class="px-1 pt-3 pb-4 border-b border-n-weak last:border-b-0"
:linked-issue="linkedIssue"
@unlink-issue="unlinkIssue"
/>
@@ -73,11 +73,11 @@ const variableKey = (item = {}) => {
class="bg-n-solid-1 p-1 rounded-xl overflow-auto absolute w-full z-20 shadow-md left-0 bottom-full max-h-[9.75rem] border border-solid border-n-strong mention--box"
>
<ul class="mb-0 vertical dropdown menu">
<woot-dropdown-item
<li
v-for="(item, index) in items"
:id="`mention-item-${index}`"
:key="item.key"
class="!mb-1"
class="mb-1 list-none"
@mouseover="onHover(index)"
>
<button
@@ -106,7 +106,7 @@ const variableKey = (item = {}) => {
</p>
</slot>
</button>
</woot-dropdown-item>
</li>
</ul>
</div>
</template>