feat: Update conversation side panel layout and styling (#12983)
This commit is contained in:
@@ -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
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user