Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
<script setup>
|
||||
import { computed, defineEmits } from 'vue';
|
||||
import { OnClickOutside } from '@vueuse/components';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
import MultiselectDropdownItems from 'shared/components/ui/MultiselectDropdownItems.vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedItem: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
hasThumbnail: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
multiselectorTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
multiselectorPlaceholder: {
|
||||
type: String,
|
||||
default: 'None',
|
||||
},
|
||||
noSearchResult: {
|
||||
type: String,
|
||||
default: 'No results found',
|
||||
},
|
||||
inputPlaceholder: {
|
||||
type: String,
|
||||
default: 'Search',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select']);
|
||||
const [showSearchDropdown, toggleDropdown] = useToggle(false);
|
||||
|
||||
const onCloseDropdown = () => toggleDropdown(false);
|
||||
const onClickSelectItem = value => {
|
||||
emit('select', value);
|
||||
onCloseDropdown();
|
||||
};
|
||||
|
||||
const hasValue = computed(() => {
|
||||
if (props.selectedItem && props.selectedItem.id) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OnClickOutside @trigger="onCloseDropdown">
|
||||
<div class="relative w-full mb-2" @keyup.esc="onCloseDropdown">
|
||||
<Button
|
||||
slate
|
||||
outline
|
||||
trailing-icon
|
||||
:icon="
|
||||
showSearchDropdown ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'
|
||||
"
|
||||
class="w-full !px-2"
|
||||
@click="
|
||||
() => toggleDropdown() // ensure that the event is not passed to the button
|
||||
"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full min-w-0">
|
||||
<h4 v-if="!hasValue" class="text-sm text-ellipsis text-n-slate-12">
|
||||
{{ multiselectorPlaceholder }}
|
||||
</h4>
|
||||
<h4
|
||||
v-else
|
||||
class="items-center overflow-hidden text-sm leading-tight whitespace-nowrap text-ellipsis text-n-slate-12"
|
||||
:title="selectedItem.name"
|
||||
>
|
||||
{{ selectedItem.name }}
|
||||
</h4>
|
||||
</div>
|
||||
<Avatar
|
||||
v-if="hasValue && hasThumbnail"
|
||||
:src="selectedItem.thumbnail"
|
||||
:status="selectedItem.availability_status"
|
||||
:name="selectedItem.name"
|
||||
:size="24"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
/>
|
||||
</Button>
|
||||
<div
|
||||
:class="{
|
||||
'block visible': showSearchDropdown,
|
||||
'hidden invisible': !showSearchDropdown,
|
||||
}"
|
||||
class="box-border top-[2.625rem] w-full border rounded-lg bg-n-alpha-3 backdrop-blur-[100px] absolute shadow-lg border-n-strong dark:border-n-strong p-2 z-[9999]"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<h4
|
||||
class="m-0 overflow-hidden text-sm text-n-slate-11 whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ multiselectorTitle }}
|
||||
</h4>
|
||||
<Button ghost slate xs icon="i-lucide-x" @click="onCloseDropdown" />
|
||||
</div>
|
||||
<MultiselectDropdownItems
|
||||
v-if="showSearchDropdown"
|
||||
:options="options"
|
||||
:selected-items="[selectedItem]"
|
||||
:has-thumbnail="hasThumbnail"
|
||||
:input-placeholder="inputPlaceholder"
|
||||
:no-search-result="noSearchResult"
|
||||
@select="onClickSelectItem"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</OnClickOutside>
|
||||
</template>
|
||||
@@ -0,0 +1,151 @@
|
||||
<script>
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDropdownItem,
|
||||
WootDropdownMenu,
|
||||
Avatar,
|
||||
NextButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
hasThumbnail: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
inputPlaceholder: {
|
||||
type: String,
|
||||
default: 'Search',
|
||||
},
|
||||
noSearchResult: {
|
||||
type: String,
|
||||
default: 'No results found',
|
||||
},
|
||||
},
|
||||
emits: ['select'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredOptions() {
|
||||
return this.options.filter(option => {
|
||||
return option.name.toLowerCase().includes(this.search.toLowerCase());
|
||||
});
|
||||
},
|
||||
noResult() {
|
||||
return this.filteredOptions.length === 0 && this.search !== '';
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.focusInput();
|
||||
},
|
||||
|
||||
methods: {
|
||||
onclick(option) {
|
||||
this.$emit('select', option);
|
||||
},
|
||||
focusInput() {
|
||||
this.$refs.searchbar.focus();
|
||||
},
|
||||
isActive(option) {
|
||||
return this.selectedItems.some(item => item && option.id === item.id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown-wrap">
|
||||
<div class="flex-auto flex-grow-0 flex-shrink-0 mb-2 max-h-8">
|
||||
<input
|
||||
ref="searchbar"
|
||||
v-model="search"
|
||||
type="text"
|
||||
class="search-input"
|
||||
autofocus="true"
|
||||
:placeholder="inputPlaceholder"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-start justify-start flex-auto overflow-auto mt-2">
|
||||
<div class="w-full max-h-[10rem]">
|
||||
<WootDropdownMenu>
|
||||
<WootDropdownItem v-for="option in filteredOptions" :key="option.id">
|
||||
<NextButton
|
||||
slate
|
||||
:variant="isActive(option) ? 'faded' : 'ghost'"
|
||||
trailing-icon
|
||||
:icon="isActive(option) ? 'i-lucide-check' : ''"
|
||||
class="w-full !px-2.5"
|
||||
@click="() => onclick(option)"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between w-full min-w-0 gap-2"
|
||||
>
|
||||
<span
|
||||
class="my-0 overflow-hidden text-sm leading-4 whitespace-nowrap text-ellipsis"
|
||||
:title="option.name"
|
||||
>
|
||||
{{ option.name }}
|
||||
</span>
|
||||
</div>
|
||||
<Avatar
|
||||
v-if="hasThumbnail"
|
||||
:src="option.thumbnail"
|
||||
:name="option.name"
|
||||
:status="option.availability_status"
|
||||
:size="24"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
/>
|
||||
</NextButton>
|
||||
</WootDropdownItem>
|
||||
</WootDropdownMenu>
|
||||
<h4
|
||||
v-if="noResult"
|
||||
class="w-full justify-center items-center flex text-n-slate-10 py-2 px-2.5 overflow-hidden whitespace-nowrap text-ellipsis text-sm"
|
||||
>
|
||||
{{ noSearchResult }}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-wrap {
|
||||
@apply w-full flex flex-col max-h-[12.5rem];
|
||||
}
|
||||
|
||||
.search-input {
|
||||
@apply m-0 w-full border border-solid border-transparent h-8 text-sm text-n-slate-12 rounded-md focus:border-n-brand bg-n-background dark:bg-n-background;
|
||||
}
|
||||
|
||||
.multiselect-dropdown--item {
|
||||
@apply justify-between w-full;
|
||||
|
||||
&.active {
|
||||
@apply bg-n-slate-2 dark:bg-n-solid-3 border-n-weak/50 dark:border-n-weak font-medium;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-n-slate-2 dark:bg-n-solid-3 text-n-slate-12;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const emit = defineEmits(['add']);
|
||||
|
||||
const addLabel = () => {
|
||||
emit('add');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
faded
|
||||
xs
|
||||
icon="i-lucide-plus"
|
||||
class="mb-0.5 ltr:mr-0.5 rtl:ml-0.5 !rounded-[4px]"
|
||||
:label="$t('CONTACT_PANEL.LABELS.CONVERSATION.ADD_BUTTON')"
|
||||
@click="addLabel"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
class="list-none my-1 mx-0 border-b border-n-weak"
|
||||
:tabindex="null"
|
||||
:aria-disabled="true"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
export default {
|
||||
componentName: 'WootDropdownMenu',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="inline-flex list-none" :tabindex="null" :aria-disabled="true">
|
||||
<span
|
||||
class="text-xs text-n-slate-12 mt-1 font-medium w-full block text-left rtl:text-right whitespace-nowrap p-2"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
<slot />
|
||||
</li>
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'WootDropdownItem',
|
||||
componentName: 'WootDropdownMenu',
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
class="mb-1 list-none dropdown-menu__item"
|
||||
:class="{
|
||||
'is-disabled': disabled,
|
||||
}"
|
||||
:tabindex="disabled ? null : -1"
|
||||
:aria-disabled="disabled"
|
||||
>
|
||||
<slot />
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-menu__item {
|
||||
::v-deep {
|
||||
a,
|
||||
.button {
|
||||
@apply inline-flex whitespace-nowrap w-full text-left rtl:text-right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A hacky fix to remove the background that came from the foundation styles node module file
|
||||
// Can be removed once we remove the foundation styles node module
|
||||
.dropdown.menu {
|
||||
// Top-level item
|
||||
> li > a {
|
||||
background: transparent;
|
||||
padding: 4px 10.8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
|
||||
defineProps({
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'top',
|
||||
},
|
||||
});
|
||||
|
||||
const dropdownMenuRef = ref(null);
|
||||
|
||||
const dropdownMenuButtons = () => {
|
||||
return dropdownMenuRef.value.querySelectorAll(
|
||||
'ul.dropdown li.dropdown-menu__item .button'
|
||||
);
|
||||
};
|
||||
|
||||
const getActiveButtonIndex = menuButtons => {
|
||||
const focusedButton = dropdownMenuRef.value.querySelector(
|
||||
'ul.dropdown li.dropdown-menu__item .button:focus'
|
||||
);
|
||||
return Array.from(menuButtons).indexOf(focusedButton);
|
||||
};
|
||||
|
||||
const focusButton = (menuButtons, newIndex) => {
|
||||
if (menuButtons.length === 0) return;
|
||||
menuButtons[newIndex].focus();
|
||||
};
|
||||
|
||||
const focusPreviousButton = menuButtons => {
|
||||
const activeIndex = getActiveButtonIndex(menuButtons);
|
||||
const newIndex = activeIndex >= 1 ? activeIndex - 1 : menuButtons.length - 1;
|
||||
focusButton(menuButtons, newIndex);
|
||||
};
|
||||
|
||||
const focusNextButton = menuButtons => {
|
||||
const activeIndex = getActiveButtonIndex(menuButtons);
|
||||
const newIndex = activeIndex === menuButtons.length - 1 ? 0 : activeIndex + 1;
|
||||
focusButton(menuButtons, newIndex);
|
||||
};
|
||||
|
||||
const keyboardEvents = {
|
||||
ArrowUp: {
|
||||
action: () => focusPreviousButton(dropdownMenuButtons()),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
ArrowDown: {
|
||||
action: () => focusNextButton(dropdownMenuButtons()),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul
|
||||
ref="dropdownMenuRef"
|
||||
class="dropdown menu vertical"
|
||||
:class="[placement && `dropdown--${placement}`]"
|
||||
>
|
||||
<slot />
|
||||
</ul>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script>
|
||||
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
|
||||
|
||||
export default {
|
||||
name: 'WootDropdownMenu',
|
||||
componentName: 'WootDropdownMenu',
|
||||
|
||||
components: {
|
||||
WootDropdownHeader,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="!mt-0.5">
|
||||
<ul class="!m-0">
|
||||
<WootDropdownHeader v-if="title" :title="title" />
|
||||
<slot />
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
@@ -0,0 +1,184 @@
|
||||
<script>
|
||||
import LabelDropdownItem from './LabelDropdownItem.vue';
|
||||
import Hotkey from 'dashboard/components/base/Hotkey.vue';
|
||||
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import { sanitizeLabel } from 'shared/helpers/sanitizeData';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LabelDropdownItem,
|
||||
AddLabelModal,
|
||||
Hotkey,
|
||||
NextButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
accountLabels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedLabels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
allowCreation: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update', 'add', 'remove'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
createModalVisible: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
createLabelPlaceholder() {
|
||||
const label = this.$t('CONTACT_PANEL.LABELS.LABEL_SELECT.CREATE_LABEL');
|
||||
return this.search ? `${label}:` : label;
|
||||
},
|
||||
|
||||
filteredActiveLabels() {
|
||||
if (!this.search) return this.accountLabels;
|
||||
|
||||
return picoSearch(this.accountLabels, this.search, ['title'], {
|
||||
threshold: 0.9,
|
||||
});
|
||||
},
|
||||
|
||||
noResult() {
|
||||
return this.filteredActiveLabels.length === 0;
|
||||
},
|
||||
|
||||
hasExactMatchInResults() {
|
||||
return this.filteredActiveLabels.some(
|
||||
label => label.title === this.search
|
||||
);
|
||||
},
|
||||
|
||||
shouldShowCreate() {
|
||||
return this.allowCreation && this.filteredActiveLabels.length < 3;
|
||||
},
|
||||
|
||||
parsedSearch() {
|
||||
return sanitizeLabel(this.search);
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.focusInput();
|
||||
},
|
||||
|
||||
methods: {
|
||||
focusInput() {
|
||||
this.$refs.searchbar.focus();
|
||||
},
|
||||
|
||||
updateLabels(label) {
|
||||
this.$emit('update', label);
|
||||
},
|
||||
|
||||
onAdd(label) {
|
||||
this.$emit('add', label);
|
||||
},
|
||||
|
||||
onRemove(label) {
|
||||
this.$emit('remove', label);
|
||||
},
|
||||
|
||||
onAddRemove(label) {
|
||||
if (this.selectedLabels.includes(label.title)) {
|
||||
this.onRemove(label.title);
|
||||
} else {
|
||||
this.onAdd(label);
|
||||
}
|
||||
},
|
||||
|
||||
showCreateModal() {
|
||||
this.createModalVisible = true;
|
||||
},
|
||||
|
||||
hideCreateModal() {
|
||||
this.createModalVisible = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full max-h-[12.5rem]">
|
||||
<div class="flex items-center justify-center mb-1">
|
||||
<h4
|
||||
class="flex-grow m-0 overflow-hidden text-sm text-n-slate-12 whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ $t('CONTACT_PANEL.LABELS.LABEL_SELECT.TITLE') }}
|
||||
</h4>
|
||||
<Hotkey
|
||||
custom-class="border border-solid text-n-slate-12 bg-n-slate-2 text-xxs border-n-strong flex-shrink-0"
|
||||
>
|
||||
{{ 'L' }}
|
||||
</Hotkey>
|
||||
</div>
|
||||
<div class="flex-auto flex-grow-0 flex-shrink-0 mb-2 max-h-8">
|
||||
<input
|
||||
ref="searchbar"
|
||||
v-model="search"
|
||||
type="text"
|
||||
class="search-input"
|
||||
autofocus="true"
|
||||
:placeholder="$t('CONTACT_PANEL.LABELS.LABEL_SELECT.PLACEHOLDER')"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-start justify-start flex-auto flex-grow flex-shrink overflow-auto"
|
||||
>
|
||||
<div class="w-full my-1">
|
||||
<woot-dropdown-menu>
|
||||
<LabelDropdownItem
|
||||
v-for="label in filteredActiveLabels"
|
||||
:key="label.title"
|
||||
:title="label.title"
|
||||
:color="label.color"
|
||||
:selected="selectedLabels.includes(label.title)"
|
||||
@select-label="onAddRemove(label)"
|
||||
/>
|
||||
</woot-dropdown-menu>
|
||||
<div
|
||||
v-if="noResult"
|
||||
class="flex justify-center py-4 px-2.5 font-medium text-xs text-n-slate-11"
|
||||
>
|
||||
{{ $t('CONTACT_PANEL.LABELS.LABEL_SELECT.NO_RESULT') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="allowCreation && shouldShowCreate"
|
||||
class="flex pt-1 border-t border-solid border-n-weak"
|
||||
>
|
||||
<NextButton
|
||||
icon="i-lucide-plus"
|
||||
slate
|
||||
sm
|
||||
ghost
|
||||
:label="`${createLabelPlaceholder} ${parsedSearch}`"
|
||||
:disabled="hasExactMatchInResults"
|
||||
@click="showCreateModal"
|
||||
/>
|
||||
|
||||
<woot-modal
|
||||
v-model:show="createModalVisible"
|
||||
:on-close="hideCreateModal"
|
||||
>
|
||||
<AddLabelModal
|
||||
:prefill-title="parsedSearch"
|
||||
@close="hideCreateModal"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script>
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['selectLabel'],
|
||||
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('selectLabel', this.title);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-dropdown-item>
|
||||
<NextButton
|
||||
slate
|
||||
ghost
|
||||
blue
|
||||
trailing-icon
|
||||
:icon="selected ? 'i-lucide-circle-check' : ''"
|
||||
class="w-full !px-2.5 justify-between"
|
||||
:class="{ '!flex-row': !selected }"
|
||||
@click="onClick"
|
||||
>
|
||||
<div class="flex items-center min-w-0 gap-2">
|
||||
<div
|
||||
v-if="color"
|
||||
class="size-3 flex-shrink-0 rounded-full outline outline-1 outline-n-weak"
|
||||
:style="{ backgroundColor: color }"
|
||||
/>
|
||||
<span
|
||||
class="overflow-hidden text-ellipsis whitespace-nowrap leading-[1.1]"
|
||||
:title="title"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
</div>
|
||||
</NextButton>
|
||||
</woot-dropdown-item>
|
||||
</template>
|
||||
Reference in New Issue
Block a user