Merge branch 'develop' into feat/message-bubble
This commit is contained in:
@@ -6,6 +6,7 @@ import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import ContactsForm from 'dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
import Flag from 'dashboard/components-next/flag/Flag.vue';
|
||||
import countries from 'shared/constants/countries';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -56,13 +57,19 @@ const countryDetails = computed(() => {
|
||||
|
||||
if (!activeCountry) return null;
|
||||
|
||||
const parts = [
|
||||
activeCountry.emoji,
|
||||
city ? `${city},` : null,
|
||||
activeCountry.name,
|
||||
].filter(Boolean);
|
||||
return {
|
||||
countryCode: activeCountry.id,
|
||||
city: city ? `${city},` : null,
|
||||
name: activeCountry.name,
|
||||
};
|
||||
});
|
||||
|
||||
return parts.length ? parts.join(' ') : null;
|
||||
const formattedLocation = computed(() => {
|
||||
if (!countryDetails.value) return '';
|
||||
|
||||
return [countryDetails.value.city, countryDetails.value.name]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
});
|
||||
|
||||
const handleFormUpdate = updatedData => {
|
||||
@@ -114,8 +121,12 @@ const onClickViewDetails = () => emit('showContact', props.id);
|
||||
{{ phoneNumber }}
|
||||
</span>
|
||||
<div v-if="phoneNumber" class="w-px h-3 truncate bg-n-slate-6" />
|
||||
<span v-if="countryDetails" class="text-sm truncate text-n-slate-11">
|
||||
{{ countryDetails }}
|
||||
<span
|
||||
v-if="countryDetails"
|
||||
class="inline-flex items-center gap-2 text-sm truncate text-n-slate-11"
|
||||
>
|
||||
<Flag :country="countryDetails.countryCode" class="size-3.5" />
|
||||
{{ formattedLocation }}
|
||||
</span>
|
||||
<div v-if="countryDetails" class="w-px h-3 truncate bg-n-slate-6" />
|
||||
<Button
|
||||
|
||||
+7
-2
@@ -106,10 +106,15 @@ const onImport = async file => {
|
||||
try {
|
||||
await store.dispatch('contacts/import', file);
|
||||
contactImportDialogRef.value?.dialogRef.close();
|
||||
useAlert(t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
|
||||
useAlert(
|
||||
t('CONTACTS_LAYOUT.HEADER.ACTIONS.IMPORT_CONTACT.SUCCESS_MESSAGE')
|
||||
);
|
||||
useTrack(CONTACTS_EVENTS.IMPORT_SUCCESS);
|
||||
} catch (error) {
|
||||
useAlert(error.message ?? t('IMPORT_CONTACTS.ERROR_MESSAGE'));
|
||||
useAlert(
|
||||
error.message ??
|
||||
t('CONTACTS_LAYOUT.HEADER.ACTIONS.IMPORT_CONTACT.ERROR_MESSAGE')
|
||||
);
|
||||
useTrack(CONTACTS_EVENTS.IMPORT_FAILURE);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,6 +70,10 @@ const updateContact = async () => {
|
||||
try {
|
||||
const { customAttributes, ...basicContactData } = contactData.value;
|
||||
await store.dispatch('contacts/update', basicContactData);
|
||||
await store.dispatch(
|
||||
'contacts/fetchContactableInbox',
|
||||
props.selectedContact.id
|
||||
);
|
||||
useAlert(t('CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(t('CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.ERROR_MESSAGE'));
|
||||
|
||||
@@ -153,7 +153,6 @@ watch(
|
||||
activeContact,
|
||||
() => {
|
||||
if (activeContact.value && props.contactId) {
|
||||
// Add null check for contactInboxes
|
||||
const contactInboxes = activeContact.value?.contactInboxes || [];
|
||||
selectedContact.value = {
|
||||
...activeContact.value,
|
||||
|
||||
+50
-19
@@ -3,6 +3,15 @@ import { getInboxIconByType } from 'dashboard/helper/inbox';
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
import ContactAPI from 'dashboard/api/contacts';
|
||||
|
||||
const CHANNEL_PRIORITY = {
|
||||
'Channel::Email': 1,
|
||||
'Channel::Whatsapp': 2,
|
||||
'Channel::Sms': 3,
|
||||
'Channel::TwilioSms': 4,
|
||||
'Channel::WebWidget': 5,
|
||||
'Channel::Api': 6,
|
||||
};
|
||||
|
||||
export const generateLabelForContactableInboxesList = ({
|
||||
name,
|
||||
email,
|
||||
@@ -21,27 +30,49 @@ export const generateLabelForContactableInboxesList = ({
|
||||
return name;
|
||||
};
|
||||
|
||||
const transformInbox = ({
|
||||
name,
|
||||
id,
|
||||
email,
|
||||
channelType,
|
||||
phoneNumber,
|
||||
...rest
|
||||
}) => ({
|
||||
id,
|
||||
icon: getInboxIconByType(channelType, phoneNumber, 'line'),
|
||||
label: generateLabelForContactableInboxesList({
|
||||
name,
|
||||
email,
|
||||
channelType,
|
||||
phoneNumber,
|
||||
}),
|
||||
action: 'inbox',
|
||||
value: id,
|
||||
name,
|
||||
email,
|
||||
phoneNumber,
|
||||
channelType,
|
||||
...rest,
|
||||
});
|
||||
|
||||
export const compareInboxes = (a, b) => {
|
||||
// Channels that have no priority defined should come at the end.
|
||||
const priorityA = CHANNEL_PRIORITY[a.channelType] || 999;
|
||||
const priorityB = CHANNEL_PRIORITY[b.channelType] || 999;
|
||||
|
||||
if (priorityA !== priorityB) {
|
||||
return priorityA - priorityB;
|
||||
}
|
||||
|
||||
const nameA = a.name || '';
|
||||
const nameB = b.name || '';
|
||||
return nameA.localeCompare(nameB);
|
||||
};
|
||||
|
||||
export const buildContactableInboxesList = contactInboxes => {
|
||||
if (!contactInboxes) return [];
|
||||
return contactInboxes.map(
|
||||
({ name, id, email, channelType, phoneNumber, ...rest }) => ({
|
||||
id,
|
||||
icon: getInboxIconByType(channelType, phoneNumber, 'line'),
|
||||
label: generateLabelForContactableInboxesList({
|
||||
name,
|
||||
email,
|
||||
channelType,
|
||||
phoneNumber,
|
||||
}),
|
||||
action: 'inbox',
|
||||
value: id,
|
||||
name,
|
||||
email,
|
||||
phoneNumber,
|
||||
channelType,
|
||||
...rest,
|
||||
})
|
||||
);
|
||||
|
||||
return contactInboxes.map(transformInbox).sort(compareInboxes);
|
||||
};
|
||||
|
||||
export const getCapitalizedNameFromEmail = email => {
|
||||
|
||||
+71
@@ -463,3 +463,74 @@ describe('composeConversationHelper', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareInboxes', () => {
|
||||
it('should sort inboxes by channel priority', () => {
|
||||
const inboxes = [
|
||||
{ channelType: 'Channel::Api', name: 'API Inbox' },
|
||||
{ channelType: 'Channel::Email', name: 'Email Inbox' },
|
||||
{ channelType: 'Channel::WebWidget', name: 'Widget' },
|
||||
{ channelType: 'Channel::Whatsapp', name: 'WhatsApp' },
|
||||
];
|
||||
|
||||
const sorted = [...inboxes].sort(helpers.compareInboxes);
|
||||
|
||||
expect(sorted[0].channelType).toBe('Channel::Email');
|
||||
expect(sorted[1].channelType).toBe('Channel::Whatsapp');
|
||||
expect(sorted[2].channelType).toBe('Channel::WebWidget');
|
||||
expect(sorted[3].channelType).toBe('Channel::Api');
|
||||
});
|
||||
|
||||
it('should sort SMS channels correctly', () => {
|
||||
const inboxes = [
|
||||
{ channelType: 'Channel::TwilioSms', name: 'Twilio' },
|
||||
{ channelType: 'Channel::Sms', name: 'Regular SMS' },
|
||||
];
|
||||
|
||||
const sorted = [...inboxes].sort(helpers.compareInboxes);
|
||||
|
||||
expect(sorted[0].channelType).toBe('Channel::Sms');
|
||||
expect(sorted[1].channelType).toBe('Channel::TwilioSms');
|
||||
});
|
||||
|
||||
it('should sort by name when channel types are same', () => {
|
||||
const inboxes = [
|
||||
{ channelType: 'Channel::Email', name: 'Support' },
|
||||
{ channelType: 'Channel::Email', name: 'Marketing' },
|
||||
{ channelType: 'Channel::Email', name: 'Billing' },
|
||||
];
|
||||
|
||||
const sorted = [...inboxes].sort(helpers.compareInboxes);
|
||||
|
||||
expect(sorted.map(inbox => inbox.name)).toEqual([
|
||||
'Billing',
|
||||
'Marketing',
|
||||
'Support',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should put channels without priority at the end', () => {
|
||||
const inboxes = [
|
||||
{ channelType: 'Channel::Unknown', name: 'Unknown' },
|
||||
{ channelType: 'Channel::Email', name: 'Email' },
|
||||
{ channelType: 'Channel::LineChannel', name: 'Line' },
|
||||
{ channelType: 'Channel::Whatsapp', name: 'WhatsApp' },
|
||||
];
|
||||
|
||||
const sorted = [...inboxes].sort(helpers.compareInboxes);
|
||||
|
||||
expect(sorted.map(i => i.channelType)).toEqual([
|
||||
'Channel::Email',
|
||||
'Channel::Whatsapp',
|
||||
|
||||
'Channel::LineChannel',
|
||||
'Channel::Unknown',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty array', () => {
|
||||
const inboxes = [];
|
||||
const sorted = [...inboxes].sort(helpers.compareInboxes);
|
||||
expect(sorted).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Copilot from './Copilot.vue';
|
||||
|
||||
const supportAgent = {
|
||||
available_name: 'Pranav Raj',
|
||||
avatar_url:
|
||||
'https://app.chatwoot.com/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBd3FodGc9PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--d218a325af0ef45061eefd352f8efb9ac84275e8/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lKYW5CbFp3WTZCa1ZVT2hOeVpYTnBlbVZmZEc5ZlptbHNiRnNIYVFINk1BPT0iLCJleHAiOm51bGwsInB1ciI6InZhcmlhdGlvbiJ9fQ==--533c3ad7218e24c4b0e8f8959dc1953ce1d279b9/1707423736896.jpeg',
|
||||
};
|
||||
|
||||
const messages = ref([
|
||||
{
|
||||
id: 1,
|
||||
role: 'user',
|
||||
content: 'Hi there! How can I help you today?',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
role: 'assistant',
|
||||
content:
|
||||
"Hello! I'm the AI assistant. I'll be helping the support team today.",
|
||||
},
|
||||
]);
|
||||
|
||||
const isCaptainTyping = ref(false);
|
||||
|
||||
const sendMessage = message => {
|
||||
// Add user message
|
||||
messages.value.push({
|
||||
id: messages.value.length + 1,
|
||||
role: 'user',
|
||||
content: message,
|
||||
});
|
||||
|
||||
// Simulate AI response
|
||||
isCaptainTyping.value = true;
|
||||
setTimeout(() => {
|
||||
isCaptainTyping.value = false;
|
||||
messages.value.push({
|
||||
id: messages.value.length + 1,
|
||||
role: 'assistant',
|
||||
content: 'This is a simulated AI response.',
|
||||
});
|
||||
}, 2000);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/Copilot"
|
||||
:layout="{ type: 'grid', width: '400px', height: '800px' }"
|
||||
>
|
||||
<Copilot
|
||||
:support-agent="supportAgent"
|
||||
:messages="messages"
|
||||
:is-captain-typing="isCaptainTyping"
|
||||
@send-message="sendMessage"
|
||||
/>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup>
|
||||
import CopilotInput from './CopilotInput.vue';
|
||||
import CopilotLoader from './CopilotLoader.vue';
|
||||
import CopilotAgentMessage from './CopilotAgentMessage.vue';
|
||||
import CopilotAssistantMessage from './CopilotAssistantMessage.vue';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
supportAgent: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
messages: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isCaptainTyping: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['sendMessage']);
|
||||
|
||||
const COPILOT_USER_ROLES = ['assistant', 'system'];
|
||||
|
||||
const sendMessage = message => {
|
||||
emit('sendMessage', message);
|
||||
};
|
||||
const chatContainer = ref(null);
|
||||
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick();
|
||||
if (chatContainer.value) {
|
||||
chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
[() => props.messages, () => props.isCaptainTyping],
|
||||
() => {
|
||||
scrollToBottom();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col ]mx-auto h-full text-sm leading-6 tracking-tight">
|
||||
<div ref="chatContainer" class="flex-1 overflow-y-auto py-4 space-y-6 px-4">
|
||||
<template v-for="message in messages" :key="message.id">
|
||||
<CopilotAgentMessage
|
||||
v-if="message.role === 'user'"
|
||||
:support-agent="supportAgent"
|
||||
:message="message"
|
||||
/>
|
||||
<CopilotAssistantMessage
|
||||
v-else-if="COPILOT_USER_ROLES.includes(message.role)"
|
||||
:message="message"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<CopilotLoader v-if="isCaptainTyping" />
|
||||
</div>
|
||||
|
||||
<CopilotInput class="mx-3 mb-4 mt-px" @send="sendMessage" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
import Avatar from '../avatar/Avatar.vue';
|
||||
|
||||
defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
supportAgent: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Avatar
|
||||
:name="supportAgent.available_name"
|
||||
:src="supportAgent.avatar_url"
|
||||
:size="24"
|
||||
rounded-full
|
||||
/>
|
||||
<div class="space-y-1 text-n-slate-12">
|
||||
<div class="font-medium">{{ $t('CAPTAIN.COPILOT.YOU') }}</div>
|
||||
<div class="break-words">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import Avatar from '../avatar/Avatar.vue';
|
||||
|
||||
defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Avatar
|
||||
name="Captain Copilot"
|
||||
icon-name="i-woot-captain"
|
||||
:size="24"
|
||||
rounded-full
|
||||
/>
|
||||
<div class="flex flex-col gap-1 text-n-slate-12">
|
||||
<div class="font-medium">{{ $t('CAPTAIN.NAME') }}</div>
|
||||
<div class="break-words">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const emit = defineEmits(['send']);
|
||||
const message = ref('');
|
||||
|
||||
const sendMessage = () => {
|
||||
if (message.value.trim()) {
|
||||
emit('send', message.value);
|
||||
message.value = '';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
class="border border-n-weak bg-n-alpha-3 rounded-lg h-12 flex"
|
||||
@submit.prevent="sendMessage"
|
||||
>
|
||||
<input
|
||||
v-model="message"
|
||||
type="text"
|
||||
:placeholder="$t('CAPTAIN.COPILOT.SEND_MESSAGE')"
|
||||
class="w-full reset-base bg-transparent px-4 py-3 text-n-slate-11 text-sm"
|
||||
@keyup.enter="sendMessage"
|
||||
/>
|
||||
<button
|
||||
class="h-auto w-12 flex items-center justify-center text-n-slate-11"
|
||||
type="submit"
|
||||
>
|
||||
<i class="i-ph-arrow-up" />
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup>
|
||||
import CopilotLoader from './CopilotLoader.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/CopilotLoader"
|
||||
:layout="{ type: 'grid', width: '400px', height: '800px' }"
|
||||
>
|
||||
<CopilotLoader />
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
// Copilot Loader Component
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-start">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-n-iris-11 font-medium">
|
||||
{{ $t('CAPTAIN.COPILOT.LOADER') }}
|
||||
</span>
|
||||
<div class="flex space-x-1">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full bg-n-iris-9 animate-bounce [animation-delay:-0.3s]"
|
||||
/>
|
||||
<div
|
||||
class="w-2 h-2 rounded-full bg-n-iris-9 animate-bounce [animation-delay:-0.15s]"
|
||||
/>
|
||||
<div class="w-2 h-2 rounded-full bg-n-iris-9 animate-bounce" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -6,7 +6,8 @@ import { useDropdownContext } from './provider.js';
|
||||
const props = defineProps({
|
||||
label: { type: String, default: '' },
|
||||
icon: { type: [String, Object, Function], default: '' },
|
||||
link: { type: String, default: '' },
|
||||
link: { type: [String, Object], default: '' },
|
||||
nativeLink: { type: Boolean, default: false },
|
||||
click: { type: Function, default: null },
|
||||
preserveOpen: { type: Boolean, default: false },
|
||||
});
|
||||
@@ -18,7 +19,13 @@ defineOptions({
|
||||
const { closeMenu } = useDropdownContext();
|
||||
|
||||
const componentIs = computed(() => {
|
||||
if (props.link) return 'router-link';
|
||||
if (props.link) {
|
||||
if (props.nativeLink && typeof props.link === 'string') {
|
||||
return 'a';
|
||||
}
|
||||
|
||||
return 'router-link';
|
||||
}
|
||||
if (props.click) return 'button';
|
||||
|
||||
return 'div';
|
||||
@@ -41,7 +48,8 @@ const triggerClick = () => {
|
||||
:class="{
|
||||
'hover:bg-n-alpha-2 rounded-lg w-full gap-3': !$slots.default,
|
||||
}"
|
||||
:href="props.link || null"
|
||||
:href="componentIs === 'a' ? props.link : null"
|
||||
:to="componentIs === 'router-link' ? props.link : null"
|
||||
@click="triggerClick"
|
||||
>
|
||||
<slot>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
import { h, defineProps } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
country: { type: String, required: true },
|
||||
squared: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const renderFlag = () => {
|
||||
const classes = ['fi', `fi-${props.country.toLowerCase()}`, 'flex-shrink-0'];
|
||||
if (props.squared) {
|
||||
classes.push('fis');
|
||||
}
|
||||
return h('span', { class: classes });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="renderFlag" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import 'flag-icons/css/flag-icons.min.css';
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Flag from '../Flag.vue';
|
||||
import countries from 'shared/constants/countries';
|
||||
|
||||
const BasicTemplate = {
|
||||
components: { Flag },
|
||||
props: {
|
||||
country: {
|
||||
type: String,
|
||||
default: 'us',
|
||||
},
|
||||
squared: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="flex items-center gap-4 p-4 border rounded border-n-weak">
|
||||
<Flag :country="country" :squared="squared" />
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
const SizeVariants = {
|
||||
components: { Flag },
|
||||
setup() {
|
||||
const isSquared = ref(false);
|
||||
return { isSquared };
|
||||
},
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" v-model="isSquared">
|
||||
Squared flags
|
||||
</label>
|
||||
<div class="flex items-center gap-4 p-4 border rounded border-n-weak">
|
||||
<Flag country="in" class="!size-4" :squared="isSquared" />
|
||||
<Flag country="in" class="!size-6" :squared="isSquared" />
|
||||
<Flag country="in" class="!size-8" :squared="isSquared" />
|
||||
<Flag country="in" class="!size-10" :squared="isSquared" />
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
const AllFlags = {
|
||||
components: { Flag },
|
||||
setup() {
|
||||
const isSquared = ref(false);
|
||||
return { countries, isSquared };
|
||||
},
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" v-model="isSquared">
|
||||
Squared flags
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 p-4 border rounded border-n-strong md:grid-cols-3 lg:grid-cols-4">
|
||||
<div
|
||||
v-for="country in countries"
|
||||
:key="country.id"
|
||||
class="flex items-center gap-2 px-4 py-2 border rounded border-n-strong"
|
||||
>
|
||||
<Flag
|
||||
:country="country.id"
|
||||
:squared="isSquared"
|
||||
class="size-6"
|
||||
/>
|
||||
<span class="text-sm">{{ country.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/Flag">
|
||||
<Variant title="Basic Usage">
|
||||
<BasicTemplate country="us" :squared="false" />
|
||||
</Variant>
|
||||
|
||||
<Variant title="Size Variants">
|
||||
<SizeVariants />
|
||||
</Variant>
|
||||
|
||||
<Variant title="All Flags">
|
||||
<AllFlags />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, onMounted, nextTick } from 'vue';
|
||||
import { useSidebarContext } from './provider';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
@@ -113,6 +113,13 @@ const toggleTrigger = () => {
|
||||
}
|
||||
setExpandedItem(props.name);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
if (hasActiveChild.value) {
|
||||
setExpandedItem(props.name);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Auth from 'dashboard/api/auth';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
@@ -21,7 +20,6 @@ defineOptions({
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const globalConfig = useMapGetter('globalConfig/get');
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
@@ -49,9 +47,7 @@ const menuItems = computed(() => {
|
||||
show: true,
|
||||
label: t('SIDEBAR_ITEMS.PROFILE_SETTINGS'),
|
||||
icon: 'i-lucide-user-pen',
|
||||
click: () => {
|
||||
router.push({ name: 'profile_settings_index' });
|
||||
},
|
||||
link: { name: 'profile_settings_index' },
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
@@ -66,15 +62,16 @@ const menuItems = computed(() => {
|
||||
show: true,
|
||||
label: t('SIDEBAR_ITEMS.DOCS'),
|
||||
icon: 'i-lucide-book',
|
||||
click: () => {
|
||||
window.open('https://www.chatwoot.com/hc/user-guide/en', '_blank');
|
||||
},
|
||||
link: 'https://www.chatwoot.com/hc/user-guide/en',
|
||||
nativeLink: true,
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
show: currentUser.value.type === 'SuperAdmin',
|
||||
label: t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE'),
|
||||
icon: 'i-lucide-castle',
|
||||
link: '/super_admin',
|
||||
nativeLink: true,
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user