Merge branch 'develop' into feature/enhanced-template-support
This commit is contained in:
@@ -20,6 +20,7 @@ const props = defineProps({
|
||||
enableVariables: { type: Boolean, default: false },
|
||||
enableCannedResponses: { type: Boolean, default: true },
|
||||
enabledMenuOptions: { type: Array, default: () => [] },
|
||||
enableCaptainTools: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
@@ -98,6 +99,7 @@ watch(
|
||||
:enable-variables="enableVariables"
|
||||
:enable-canned-responses="enableCannedResponses"
|
||||
:enabled-menu-options="enabledMenuOptions"
|
||||
:enable-captain-tools="enableCaptainTools"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
|
||||
+26
-1
@@ -1,6 +1,9 @@
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { helpers } from '@vuelidate/validators';
|
||||
import { isValidDomain } from '@chatwoot/utils';
|
||||
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
@@ -26,6 +29,20 @@ const formState = reactive({
|
||||
customDomain: props.customDomain,
|
||||
});
|
||||
|
||||
const rules = {
|
||||
customDomain: {
|
||||
isValidDomain: helpers.withMessage(
|
||||
() =>
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DIALOG.FORMAT_ERROR'
|
||||
),
|
||||
isValidDomain
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(rules, formState);
|
||||
|
||||
watch(
|
||||
() => props.customDomain,
|
||||
newVal => {
|
||||
@@ -33,7 +50,10 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const handleDialogConfirm = () => {
|
||||
const handleDialogConfirm = async () => {
|
||||
const isFormCorrect = await v$.value.$validate();
|
||||
if (!isFormCorrect) return;
|
||||
|
||||
emit('addCustomDomain', formState.customDomain);
|
||||
};
|
||||
|
||||
@@ -67,6 +87,11 @@ defineExpose({ dialogRef });
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DIALOG.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:message="
|
||||
v$.customDomain.$error ? v$.customDomain.$errors[0].$message : ''
|
||||
"
|
||||
:message-type="v$.customDomain.$error ? 'error' : 'info'"
|
||||
@blur="v$.customDomain.$touch()"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
+138
-37
@@ -1,9 +1,15 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { reactive, computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import { getHostNameFromURL } from 'dashboard/helper/URLHelper';
|
||||
import { email, required } from '@vuelidate/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
customDomain: {
|
||||
@@ -12,10 +18,20 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['confirm']);
|
||||
const emit = defineEmits(['send', 'close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const state = reactive({
|
||||
email: '',
|
||||
});
|
||||
|
||||
const validationRules = {
|
||||
email: { email, required },
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(validationRules, state);
|
||||
|
||||
const domain = computed(() => {
|
||||
const { hostURL, helpCenterURL } = window?.chatwootConfig || {};
|
||||
return getHostNameFromURL(helpCenterURL) || getHostNameFromURL(hostURL) || '';
|
||||
@@ -25,10 +41,34 @@ const subdomainCNAME = computed(
|
||||
() => `${props.customDomain} CNAME ${domain.value}`
|
||||
);
|
||||
|
||||
const handleCopy = async e => {
|
||||
e.stopPropagation();
|
||||
await copyTextToClipboard(subdomainCNAME.value);
|
||||
useAlert(
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.COPY'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const dialogRef = ref(null);
|
||||
|
||||
const handleDialogConfirm = () => {
|
||||
emit('confirm');
|
||||
const resetForm = () => {
|
||||
v$.value.$reset();
|
||||
state.email = '';
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
resetForm();
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
const isFormCorrect = await v$.value.$validate();
|
||||
if (!isFormCorrect) return;
|
||||
|
||||
emit('send', state.email);
|
||||
onClose();
|
||||
};
|
||||
|
||||
defineExpose({ dialogRef });
|
||||
@@ -37,42 +77,103 @@ defineExpose({ dialogRef });
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
:title="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.HEADER'
|
||||
)
|
||||
"
|
||||
:confirm-button-label="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.CONFIRM_BUTTON_LABEL'
|
||||
)
|
||||
"
|
||||
:show-cancel-button="false"
|
||||
@confirm="handleDialogConfirm"
|
||||
:show-confirm-button="false"
|
||||
@close="resetForm"
|
||||
>
|
||||
<template #description>
|
||||
<p class="mb-0 text-sm text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.DESCRIPTION'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</template>
|
||||
<NextButton
|
||||
icon="i-lucide-x"
|
||||
sm
|
||||
ghost
|
||||
slate
|
||||
class="flex-shrink-0 absolute top-2 ltr:right-2 rtl:left-2"
|
||||
@click="onClose"
|
||||
/>
|
||||
<div class="flex flex-col gap-6 divide-y divide-n-strong">
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2 ltr:pr-10 rtl:pl-10">
|
||||
<h3 class="text-base font-medium leading-6 text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.HEADER'
|
||||
)
|
||||
}}
|
||||
</h3>
|
||||
<p class="mb-0 text-sm text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.DESCRIPTION'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 w-full">
|
||||
<span
|
||||
class="min-h-10 px-3 py-2.5 inline-flex items-center w-full text-sm bg-transparent border rounded-lg text-n-slate-11 border-n-strong"
|
||||
>
|
||||
{{ subdomainCNAME }}
|
||||
</span>
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="button"
|
||||
icon="i-lucide-copy"
|
||||
class="flex-shrink-0"
|
||||
@click="handleCopy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<span
|
||||
class="h-10 px-3 py-2.5 text-sm select-none bg-transparent border rounded-lg text-n-slate-11 border-n-strong"
|
||||
>
|
||||
{{ subdomainCNAME }}
|
||||
</span>
|
||||
<p class="text-sm text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.HELP_TEXT'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<div class="flex flex-col gap-6 pt-6">
|
||||
<div class="flex flex-col gap-2 ltr:pr-10 rtl:pl-10">
|
||||
<h3 class="text-base font-medium leading-6 text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.SEND_INSTRUCTIONS.HEADER'
|
||||
)
|
||||
}}
|
||||
</h3>
|
||||
<p class="mb-0 text-sm text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.SEND_INSTRUCTIONS.DESCRIPTION'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
class="flex items-start gap-3 w-full"
|
||||
@submit.prevent="handleSend"
|
||||
>
|
||||
<Input
|
||||
v-model="state.email"
|
||||
:placeholder="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.SEND_INSTRUCTIONS.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:message="
|
||||
v$.email.$error
|
||||
? t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.SEND_INSTRUCTIONS.ERROR'
|
||||
)
|
||||
: ''
|
||||
"
|
||||
:message-type="v$.email.$error ? 'error' : 'info'"
|
||||
class="w-full"
|
||||
@blur="v$.email.$touch()"
|
||||
/>
|
||||
<NextButton
|
||||
:label="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.SEND_INSTRUCTIONS.SEND_BUTTON'
|
||||
)
|
||||
"
|
||||
type="submit"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
+159
-23
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
|
||||
import AddCustomDomainDialog from 'dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/AddCustomDomainDialog.vue';
|
||||
import DNSConfigurationDialog from 'dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/DNSConfigurationDialog.vue';
|
||||
@@ -11,11 +12,52 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isFetchingStatus: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['updatePortalConfiguration']);
|
||||
const emit = defineEmits([
|
||||
'updatePortalConfiguration',
|
||||
'refreshStatus',
|
||||
'sendCnameInstructions',
|
||||
]);
|
||||
|
||||
const SSL_STATUS = {
|
||||
LIVE: ['active', 'staging_active'],
|
||||
PENDING: [
|
||||
'provisioned',
|
||||
'pending',
|
||||
'initializing',
|
||||
'pending_validation',
|
||||
'pending_deployment',
|
||||
'pending_issuance',
|
||||
'holding_deployment',
|
||||
'holding_validation',
|
||||
'pending_expiration',
|
||||
'pending_cleanup',
|
||||
'pending_deletion',
|
||||
'staging_deployment',
|
||||
'backup_issued',
|
||||
],
|
||||
ERROR: [
|
||||
'blocked',
|
||||
'inactive',
|
||||
'moved',
|
||||
'expired',
|
||||
'deleted',
|
||||
'timed_out_initializing',
|
||||
'timed_out_validation',
|
||||
'timed_out_issuance',
|
||||
'timed_out_deployment',
|
||||
'timed_out_deletion',
|
||||
'deactivating',
|
||||
],
|
||||
};
|
||||
|
||||
const { t } = useI18n();
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
|
||||
const addCustomDomainDialogRef = ref(null);
|
||||
const dnsConfigurationDialogRef = ref(null);
|
||||
@@ -25,6 +67,45 @@ const customDomainAddress = computed(
|
||||
() => props.activePortal?.custom_domain || ''
|
||||
);
|
||||
|
||||
const sslSettings = computed(() => props.activePortal?.ssl_settings || {});
|
||||
const verificationErrors = computed(
|
||||
() => sslSettings.value.verification_errors || ''
|
||||
);
|
||||
|
||||
const isLive = computed(() =>
|
||||
SSL_STATUS.LIVE.includes(sslSettings.value.status)
|
||||
);
|
||||
const isPending = computed(() =>
|
||||
SSL_STATUS.PENDING.includes(sslSettings.value.status)
|
||||
);
|
||||
const isError = computed(() =>
|
||||
SSL_STATUS.ERROR.includes(sslSettings.value.status)
|
||||
);
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (isLive.value)
|
||||
return t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.STATUS.LIVE'
|
||||
);
|
||||
if (isPending.value)
|
||||
return t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.STATUS.PENDING'
|
||||
);
|
||||
if (isError.value)
|
||||
return t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.STATUS.ERROR'
|
||||
);
|
||||
return '';
|
||||
});
|
||||
|
||||
const statusColors = computed(() => {
|
||||
if (isLive.value)
|
||||
return { text: 'text-n-teal-11', bubble: 'outline-n-teal-6 bg-n-teal-9' };
|
||||
if (isError.value)
|
||||
return { text: 'text-n-ruby-11', bubble: 'outline-n-ruby-6 bg-n-ruby-9' };
|
||||
return { text: 'text-n-amber-11', bubble: 'outline-n-amber-6 bg-n-amber-9' };
|
||||
});
|
||||
|
||||
const updatePortalConfiguration = customDomain => {
|
||||
const portal = {
|
||||
id: props.activePortal?.id,
|
||||
@@ -42,6 +123,17 @@ const closeDNSConfigurationDialog = () => {
|
||||
updatedDomainAddress.value = '';
|
||||
dnsConfigurationDialogRef.value.dialogRef.close();
|
||||
};
|
||||
|
||||
const onClickRefreshSSLStatus = () => {
|
||||
emit('refreshStatus');
|
||||
};
|
||||
|
||||
const onClickSend = email => {
|
||||
emit('sendCnameInstructions', {
|
||||
portalSlug: props.activePortal?.slug,
|
||||
email,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -63,33 +155,76 @@ const closeDNSConfigurationDialog = () => {
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<div class="flex justify-between w-full gap-2">
|
||||
<div
|
||||
v-if="customDomainAddress"
|
||||
class="flex items-center w-full h-8 gap-4"
|
||||
>
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
<div class="flex items-center justify-between w-full gap-2">
|
||||
<div v-if="customDomainAddress" class="flex flex-col gap-1">
|
||||
<div class="flex items-center w-full h-8 gap-4">
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.LABEL'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
<span class="text-sm text-n-slate-12">
|
||||
{{ customDomainAddress }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="!isLive && isOnChatwootCloud"
|
||||
class="text-sm text-n-slate-11"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.LABEL'
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.STATUS_DESCRIPTION'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
<span class="text-sm text-n-slate-12">
|
||||
{{ customDomainAddress }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-end w-full">
|
||||
<Button
|
||||
v-if="customDomainAddress"
|
||||
color="slate"
|
||||
:label="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.EDIT_BUTTON'
|
||||
)
|
||||
"
|
||||
@click="addCustomDomainDialogRef.dialogRef.open()"
|
||||
/>
|
||||
<div class="flex items-center">
|
||||
<div v-if="customDomainAddress" class="flex items-center gap-3">
|
||||
<div
|
||||
v-if="statusText && isOnChatwootCloud"
|
||||
v-tooltip="verificationErrors"
|
||||
class="flex items-center gap-3 flex-shrink-0"
|
||||
>
|
||||
<span
|
||||
class="size-1.5 rounded-full outline outline-2 block flex-shrink-0"
|
||||
:class="statusColors.bubble"
|
||||
/>
|
||||
<span
|
||||
:class="statusColors.text"
|
||||
class="text-sm leading-[16px] font-medium"
|
||||
>
|
||||
{{ statusText }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="statusText && isOnChatwootCloud"
|
||||
class="w-px h-3 bg-n-weak"
|
||||
/>
|
||||
<Button
|
||||
slate
|
||||
sm
|
||||
link
|
||||
:label="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.EDIT_BUTTON'
|
||||
)
|
||||
"
|
||||
class="hover:!no-underline flex-shrink-0"
|
||||
@click="addCustomDomainDialogRef.dialogRef.open()"
|
||||
/>
|
||||
<div v-if="isOnChatwootCloud" class="w-px h-3 bg-n-weak" />
|
||||
<Button
|
||||
v-if="isOnChatwootCloud"
|
||||
slate
|
||||
sm
|
||||
link
|
||||
icon="i-lucide-refresh-ccw"
|
||||
:class="isFetchingStatus && 'animate-spin'"
|
||||
@click="onClickRefreshSSLStatus"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
v-else
|
||||
:label="
|
||||
@@ -112,7 +247,8 @@ const closeDNSConfigurationDialog = () => {
|
||||
<DNSConfigurationDialog
|
||||
ref="dnsConfigurationDialogRef"
|
||||
:custom-domain="updatedDomainAddress || customDomainAddress"
|
||||
@confirm="closeDNSConfigurationDialog"
|
||||
@close="closeDNSConfigurationDialog"
|
||||
@send="onClickSend"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+14
@@ -26,6 +26,8 @@ const emit = defineEmits([
|
||||
'updatePortal',
|
||||
'updatePortalConfiguration',
|
||||
'deletePortal',
|
||||
'refreshStatus',
|
||||
'sendCnameInstructions',
|
||||
]);
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -36,6 +38,7 @@ const confirmDeletePortalDialogRef = ref(null);
|
||||
const currentPortalSlug = computed(() => route.params.portalSlug);
|
||||
|
||||
const isSwitchingPortal = useMapGetter('portals/isSwitchingPortal');
|
||||
const isFetchingSSLStatus = useMapGetter('portals/isFetchingSSLStatus');
|
||||
|
||||
const activePortal = computed(() => {
|
||||
return props.portals?.find(portal => portal.slug === currentPortalSlug.value);
|
||||
@@ -53,6 +56,14 @@ const handleUpdatePortalConfiguration = portal => {
|
||||
emit('updatePortalConfiguration', portal);
|
||||
};
|
||||
|
||||
const fetchSSLStatus = () => {
|
||||
emit('refreshStatus');
|
||||
};
|
||||
|
||||
const handleSendCnameInstructions = payload => {
|
||||
emit('sendCnameInstructions', payload);
|
||||
};
|
||||
|
||||
const openConfirmDeletePortalDialog = () => {
|
||||
confirmDeletePortalDialogRef.value.dialogRef.open();
|
||||
};
|
||||
@@ -85,7 +96,10 @@ const handleDeletePortal = () => {
|
||||
<PortalConfigurationSettings
|
||||
:active-portal="activePortal"
|
||||
:is-fetching="isFetching"
|
||||
:is-fetching-status="isFetchingSSLStatus"
|
||||
@update-portal-configuration="handleUpdatePortalConfiguration"
|
||||
@refresh-status="fetchSSLStatus"
|
||||
@send-cname-instructions="handleSendCnameInstructions"
|
||||
/>
|
||||
<div class="w-full h-px bg-n-weak" />
|
||||
<div class="flex items-end justify-between w-full gap-4">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||
@@ -44,7 +45,10 @@ const onClickCancel = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex relative">
|
||||
<div
|
||||
v-on-click-outside="() => togglePopover(false)"
|
||||
class="inline-flex relative"
|
||||
>
|
||||
<Button
|
||||
:label="buttonLabel"
|
||||
sm
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<script setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||
|
||||
const emit = defineEmits(['add']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const [showPopover, togglePopover] = useToggle();
|
||||
|
||||
const state = reactive({
|
||||
id: '',
|
||||
title: '',
|
||||
description: '',
|
||||
instruction: '',
|
||||
});
|
||||
|
||||
const rules = {
|
||||
title: { required, minLength: minLength(1) },
|
||||
description: { required },
|
||||
instruction: { required },
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(rules, state);
|
||||
|
||||
const titleError = computed(() =>
|
||||
v$.value.title.$error
|
||||
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.ERROR')
|
||||
: ''
|
||||
);
|
||||
|
||||
const descriptionError = computed(() =>
|
||||
v$.value.description.$error
|
||||
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.ERROR')
|
||||
: ''
|
||||
);
|
||||
|
||||
const instructionError = computed(() =>
|
||||
v$.value.instruction.$error
|
||||
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.ERROR')
|
||||
: ''
|
||||
);
|
||||
|
||||
const resetState = () => {
|
||||
Object.assign(state, {
|
||||
id: '',
|
||||
title: '',
|
||||
description: '',
|
||||
instruction: '',
|
||||
});
|
||||
};
|
||||
|
||||
const onClickAdd = async () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
|
||||
await emit('add', state);
|
||||
resetState();
|
||||
togglePopover(false);
|
||||
};
|
||||
|
||||
const onClickCancel = () => {
|
||||
togglePopover(false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-on-click-outside="() => togglePopover(false)"
|
||||
class="inline-flex relative"
|
||||
>
|
||||
<Button
|
||||
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.CREATE')"
|
||||
sm
|
||||
slate
|
||||
class="flex-shrink-0"
|
||||
@click="togglePopover(!showPopover)"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="showPopover"
|
||||
class="w-[31.25rem] absolute top-10 ltr:left-0 rtl:right-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-n-weak shadow-md flex flex-col gap-6 z-50"
|
||||
>
|
||||
<h3 class="text-base font-medium text-n-slate-12">
|
||||
{{ t(`CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.TITLE`) }}
|
||||
</h3>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<Input
|
||||
v-model="state.title"
|
||||
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.LABEL')"
|
||||
:placeholder="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.PLACEHOLDER')
|
||||
"
|
||||
:message="titleError"
|
||||
:message-type="titleError ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
v-model="state.description"
|
||||
:label="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.LABEL')
|
||||
"
|
||||
:placeholder="
|
||||
t(
|
||||
'CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:message="descriptionError"
|
||||
:message-type="descriptionError ? 'error' : 'info'"
|
||||
show-character-count
|
||||
/>
|
||||
<Editor
|
||||
v-model="state.instruction"
|
||||
:label="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.LABEL')
|
||||
"
|
||||
:placeholder="
|
||||
t(
|
||||
'CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:message="instructionError"
|
||||
:message-type="instructionError ? 'error' : 'info'"
|
||||
:show-character-count="false"
|
||||
enable-captain-tools
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between w-full gap-3">
|
||||
<Button
|
||||
variant="faded"
|
||||
color="slate"
|
||||
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.CANCEL')"
|
||||
class="w-full bg-n-alpha-2 !text-n-blue-text hover:bg-n-alpha-3"
|
||||
@click="onClickCancel"
|
||||
/>
|
||||
<Button
|
||||
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.CREATE')"
|
||||
class="w-full"
|
||||
@click="onClickAdd"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -73,7 +73,6 @@ const saveEdit = () => {
|
||||
v-if="isEditing"
|
||||
v-model="editedContent"
|
||||
focus-on-mount
|
||||
custom-input-class="flex items-center gap-2 text-sm text-n-slate-12"
|
||||
@keyup.enter="saveEdit"
|
||||
/>
|
||||
<span v-else class="flex items-center gap-2 text-sm text-n-slate-12">
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup>
|
||||
import ScenariosCard from './ScenariosCard.vue';
|
||||
|
||||
const sampleScenarios = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Refund Order',
|
||||
description: 'User requests a refund for a recent purchase.',
|
||||
instruction:
|
||||
'Gather order details and reason for refund. Use [Order Search](tool://order_search) then submit with [Refund Payment](tool://refund_payment).',
|
||||
tools: ['order_search', 'refund_payment'],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Bug Report',
|
||||
description: 'Customer reports a bug in the mobile app.',
|
||||
instruction:
|
||||
'Ask for reproduction steps and environment. Check [Known Issues](tool://known_issues) then create ticket with [Create Bug Report](tool://bug_report_create).',
|
||||
tools: ['known_issues', 'bug_report_create'],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/Assistant/ScenariosCard"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Default">
|
||||
<div
|
||||
v-for="scenario in sampleScenarios"
|
||||
:key="scenario.id"
|
||||
class="px-4 py-4 bg-n-background"
|
||||
>
|
||||
<ScenariosCard
|
||||
:id="scenario.id"
|
||||
:title="scenario.title"
|
||||
:description="scenario.description"
|
||||
:instruction="scenario.instruction"
|
||||
:tools="scenario.tools"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,218 @@
|
||||
<script setup>
|
||||
import { computed, h, reactive } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
instruction: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tools: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select', 'hover', 'delete', 'update']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
|
||||
const modelValue = computed({
|
||||
get: () => props.isSelected,
|
||||
set: () => emit('select', props.id),
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
id: '',
|
||||
title: '',
|
||||
description: '',
|
||||
instruction: '',
|
||||
});
|
||||
|
||||
const [isEditing, toggleEditing] = useToggle();
|
||||
|
||||
const startEdit = () => {
|
||||
Object.assign(state, {
|
||||
id: props.id,
|
||||
title: props.title,
|
||||
description: props.description,
|
||||
instruction: props.instruction,
|
||||
tools: props.tools,
|
||||
});
|
||||
toggleEditing(true);
|
||||
};
|
||||
|
||||
const rules = {
|
||||
title: { required, minLength: minLength(1) },
|
||||
description: { required },
|
||||
instruction: { required },
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(rules, state);
|
||||
|
||||
const titleError = computed(() =>
|
||||
v$.value.title.$error
|
||||
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.ERROR')
|
||||
: ''
|
||||
);
|
||||
|
||||
const descriptionError = computed(() =>
|
||||
v$.value.description.$error
|
||||
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.ERROR')
|
||||
: ''
|
||||
);
|
||||
|
||||
const onClickUpdate = () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
emit('update', { ...state });
|
||||
toggleEditing(false);
|
||||
};
|
||||
|
||||
const instructionError = computed(() =>
|
||||
v$.value.instruction.$error
|
||||
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.ERROR')
|
||||
: ''
|
||||
);
|
||||
|
||||
const LINK_INSTRUCTION_CLASS =
|
||||
'[&_a[href^="tool://"]]:text-n-iris-11 [&_a:not([href^="tool://"])]:text-n-slate-12 [&_a]:pointer-events-none [&_a]:cursor-default';
|
||||
|
||||
const renderInstruction = instruction => () =>
|
||||
h('p', {
|
||||
class: `text-sm text-n-slate-12 py-4 mb-0 [&_ol]:list-decimal ${LINK_INSTRUCTION_CLASS}`,
|
||||
innerHTML: instruction,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardLayout
|
||||
selectable
|
||||
class="relative [&>div]:!py-4"
|
||||
:class="{
|
||||
'[&>div]:ltr:!pr-4 [&>div]:rtl:!pl-4': !isEditing,
|
||||
'[&>div]:ltr:!pr-10 [&>div]:rtl:!pl-10': isEditing,
|
||||
}"
|
||||
layout="row"
|
||||
@mouseenter="emit('hover', true)"
|
||||
@mouseleave="emit('hover', false)"
|
||||
>
|
||||
<div
|
||||
v-show="selectable && !isEditing"
|
||||
class="absolute top-[1.125rem] ltr:left-3 rtl:right-3"
|
||||
>
|
||||
<Checkbox v-model="modelValue" />
|
||||
</div>
|
||||
|
||||
<div v-if="!isEditing" class="flex flex-col w-full">
|
||||
<div class="flex items-start justify-between w-full gap-2">
|
||||
<div class="flex flex-col items-start">
|
||||
<span class="text-sm text-n-slate-12 font-medium">{{ title }}</span>
|
||||
<span class="text-sm text-n-slate-11 mt-2">
|
||||
{{ description }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- <Button label="Test" slate xs ghost class="!text-sm" />
|
||||
<span class="w-px h-4 bg-n-weak" /> -->
|
||||
<Button icon="i-lucide-pen" slate xs ghost @click="startEdit" />
|
||||
<span class="w-px h-4 bg-n-weak" />
|
||||
<Button
|
||||
icon="i-lucide-trash"
|
||||
slate
|
||||
xs
|
||||
ghost
|
||||
@click="emit('delete', id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<component :is="renderInstruction(formatMessage(instruction, false))" />
|
||||
<span class="text-sm text-n-slate-11 font-medium mb-1">
|
||||
{{ t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.SUGGESTED.TOOLS_USED') }}
|
||||
{{ tools?.map(tool => `@${tool}`).join(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="overflow-hidden flex flex-col gap-4 w-full">
|
||||
<Input
|
||||
v-model="state.title"
|
||||
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.LABEL')"
|
||||
:placeholder="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.PLACEHOLDER')
|
||||
"
|
||||
:message="titleError"
|
||||
:message-type="titleError ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
v-model="state.description"
|
||||
:label="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.LABEL')
|
||||
"
|
||||
:placeholder="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.PLACEHOLDER')
|
||||
"
|
||||
:message="descriptionError"
|
||||
:message-type="descriptionError ? 'error' : 'info'"
|
||||
show-character-count
|
||||
/>
|
||||
<Editor
|
||||
v-model="state.instruction"
|
||||
:label="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.LABEL')
|
||||
"
|
||||
:placeholder="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.PLACEHOLDER')
|
||||
"
|
||||
:message="instructionError"
|
||||
:message-type="instructionError ? 'error' : 'info'"
|
||||
:show-character-count="false"
|
||||
enable-captain-tools
|
||||
/>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button
|
||||
faded
|
||||
slate
|
||||
sm
|
||||
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.UPDATE.CANCEL')"
|
||||
@click="toggleEditing(false)"
|
||||
/>
|
||||
<Button
|
||||
sm
|
||||
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.UPDATE.UPDATE')"
|
||||
@click="onClickUpdate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import ToolsDropdown from './ToolsDropdown.vue';
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: 'order_search',
|
||||
title: 'Order Search',
|
||||
description: 'Lookup orders by customer ID, email, or order number',
|
||||
},
|
||||
{
|
||||
id: 'refund_payment',
|
||||
title: 'Refund Payment',
|
||||
description: 'Initiates a refund on a specific payment',
|
||||
},
|
||||
{
|
||||
id: 'fetch_customer',
|
||||
title: 'Fetch Customer',
|
||||
description: 'Pulls customer details (email, tags, last seen, etc.)',
|
||||
},
|
||||
];
|
||||
|
||||
const selectedIndex = ref(0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/Assistant/ToolsDropdown"
|
||||
:layout="{ type: 'grid', width: '600px' }"
|
||||
>
|
||||
<Variant title="Default">
|
||||
<div class="relative h-80 bg-n-background p-4">
|
||||
<ToolsDropdown :items="items" :selected-index="selectedIndex" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup>
|
||||
import { ref, watch, nextTick } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
selectedIndex: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select']);
|
||||
|
||||
const toolsDropdownRef = ref(null);
|
||||
|
||||
const onItemClick = idx => emit('select', idx);
|
||||
|
||||
watch(
|
||||
() => props.selectedIndex,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
const el = toolsDropdownRef.value?.querySelector(
|
||||
`#tool-item-${props.selectedIndex}`
|
||||
);
|
||||
if (el) {
|
||||
el.scrollIntoView({ block: 'nearest', behavior: 'auto' });
|
||||
}
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="toolsDropdownRef"
|
||||
class="w-[22.5rem] p-2 flex flex-col gap-1 z-50 absolute rounded-xl bg-n-alpha-3 shadow outline outline-1 outline-n-weak backdrop-blur-[50px] max-h-[20rem] overflow-y-auto"
|
||||
>
|
||||
<div
|
||||
v-for="(tool, idx) in items"
|
||||
:id="`tool-item-${idx}`"
|
||||
:key="tool.id || idx"
|
||||
:class="{ 'bg-n-alpha-black2': idx === selectedIndex }"
|
||||
class="flex flex-col gap-1 rounded-md py-2 px-2 cursor-pointer hover:bg-n-alpha-black2"
|
||||
@click="onItemClick(idx)"
|
||||
>
|
||||
<span class="text-n-slate-12 font-medium text-sm">{{ tool.title }}</span>
|
||||
<span class="text-n-slate-11 text-sm">{{ tool.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
isMobileSidebarOpen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['toggle']);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const isConversationRoute = computed(() => {
|
||||
const CONVERSATION_ROUTES = [
|
||||
'inbox_conversation',
|
||||
'conversation_through_inbox',
|
||||
'conversations_through_label',
|
||||
'team_conversations_through_label',
|
||||
'conversations_through_folders',
|
||||
'conversation_through_mentions',
|
||||
'conversation_through_unattended',
|
||||
'conversation_through_participating',
|
||||
'inbox_view_conversation',
|
||||
];
|
||||
return CONVERSATION_ROUTES.includes(route.name);
|
||||
});
|
||||
|
||||
const toggleSidebar = () => {
|
||||
emit('toggle');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="!isConversationRoute"
|
||||
id="mobile-sidebar-launcher"
|
||||
class="fixed bottom-4 ltr:left-4 rtl:right-4 z-40 transition-transform duration-200 ease-in-out block md:hidden"
|
||||
:class="[
|
||||
{
|
||||
'ltr:translate-x-48 rtl:-translate-x-48': isMobileSidebarOpen,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="rounded-full bg-n-alpha-2 p-1">
|
||||
<Button
|
||||
icon="i-lucide-menu"
|
||||
class="!rounded-full !bg-n-solid-3 dark:!bg-n-alpha-2 !text-n-slate-12 text-xl"
|
||||
lg
|
||||
@click="toggleSidebar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else />
|
||||
</template>
|
||||
@@ -8,6 +8,7 @@ import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import SidebarGroup from './SidebarGroup.vue';
|
||||
@@ -17,10 +18,18 @@ import SidebarAccountSwitcher from './SidebarAccountSwitcher.vue';
|
||||
import Logo from 'next/icon/Logo.vue';
|
||||
import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue';
|
||||
|
||||
const props = defineProps({
|
||||
isMobileSidebarOpen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'closeKeyShortcutModal',
|
||||
'openKeyShortcutModal',
|
||||
'showCreateAccountModal',
|
||||
'closeMobileSidebar',
|
||||
]);
|
||||
|
||||
const { accountScopedRoute } = useAccount();
|
||||
@@ -77,6 +86,11 @@ const sortedInboxes = computed(() =>
|
||||
inboxes.value.slice().sort((a, b) => a.name.localeCompare(b.name))
|
||||
);
|
||||
|
||||
const closeMobileSidebar = () => {
|
||||
if (!props.isMobileSidebarOpen) return;
|
||||
emit('closeMobileSidebar');
|
||||
};
|
||||
|
||||
const newReportRoutes = () => [
|
||||
{
|
||||
name: 'Reports Agent',
|
||||
@@ -488,7 +502,19 @@ const menuItems = computed(() => {
|
||||
|
||||
<template>
|
||||
<aside
|
||||
class="w-[200px] bg-n-solid-2 rtl:border-l ltr:border-r border-n-weak h-screen flex flex-col text-sm pb-1"
|
||||
v-on-click-outside="[
|
||||
closeMobileSidebar,
|
||||
{ ignore: ['#mobile-sidebar-launcher'] },
|
||||
]"
|
||||
class="bg-n-solid-2 rtl:border-l ltr:border-r border-n-weak flex flex-col text-sm pb-1 fixed top-0 ltr:left-0 rtl:right-0 h-full z-40 transition-transform duration-200 ease-in-out md:static w-[200px] basis-[200px] md:flex-shrink-0 md:ltr:translate-x-0 md:rtl:-translate-x-0"
|
||||
:class="[
|
||||
{
|
||||
'ltr:translate-x-0 rtl:-translate-x-0 shadow-lg md:shadow-none':
|
||||
isMobileSidebarOpen,
|
||||
'ltr:-translate-x-full rtl:translate-x-full md:translate-x-0':
|
||||
!isMobileSidebarOpen,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<section class="grid gap-2 mt-2 mb-4">
|
||||
<div class="flex items-center min-w-0 gap-2 px-2">
|
||||
|
||||
Reference in New Issue
Block a user