Re-Route Support to Zendesk (#5259)

* refactor: replace feedback command with contact support in Help Center menu

* refactor: replace feedback dialog with external support link in Help menu

* refactor: simplify error reporting UI by removing send report functionality

* refactor: remove issue report dialog and update support contact method

* refactor: remove IssueReportDialog and associated components

* refactor: remove unused issue report schema

* refactor: remove unused issue report types

* refactor: remove unused issue report fields from localization files
This commit is contained in:
Johnpaul Chiwetelu
2025-09-02 19:02:15 +01:00
committed by GitHub
parent b091f3aa08
commit 8f7ee4e9a3
21 changed files with 25 additions and 1207 deletions

View File

@@ -21,16 +21,9 @@
@click="showReport"
/>
<Button
v-show="!sendReportOpen"
text
:label="$t('issueReport.helpFix')"
@click="showSendReport"
/>
<Button
v-if="authStore.currentUser"
v-show="!reportOpen"
text
:label="$t('issueReport.contactSupportTitle')"
:label="$t('issueReport.helpFix')"
@click="showContactSupport"
/>
</div>
@@ -41,16 +34,6 @@
</ScrollPanel>
<Divider />
</template>
<ReportIssuePanel
v-if="sendReportOpen"
:title="$t('issueReport.submitErrorReport')"
:error-type="error.reportType ?? 'unknownError'"
:extra-fields="[stackTraceField]"
:tags="{
exceptionMessage: error.exceptionMessage,
nodeType: error.nodeType ?? 'UNKNOWN'
}"
/>
<div class="flex gap-4 justify-end">
<FindIssueButton
:error-message="error.exceptionMessage"
@@ -81,18 +64,12 @@ import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import type { ReportField } from '@/types/issueReportTypes'
import {
type ErrorReportData,
generateErrorReport
} from '@/utils/errorReportUtil'
import ReportIssuePanel from './error/ReportIssuePanel.vue'
const authStore = useFirebaseAuthStore()
const { error } = defineProps<{
error: Omit<ErrorReportData, 'workflow' | 'systemStats' | 'serverLogs'> & {
/**
@@ -114,10 +91,6 @@ const reportOpen = ref(false)
const showReport = () => {
reportOpen.value = true
}
const sendReportOpen = ref(false)
const showSendReport = () => {
sendReportOpen.value = true
}
const toast = useToast()
const { t } = useI18n()
const systemStatsStore = useSystemStatsStore()
@@ -126,15 +99,6 @@ const title = computed<string>(
() => error.nodeType ?? error.exceptionType ?? t('errorDialog.defaultTitle')
)
const stackTraceField = computed<ReportField>(() => {
return {
label: t('issueReport.stackTrace'),
value: 'StackTrace',
optIn: true,
getData: () => error.traceback
}
})
const showContactSupport = async () => {
await useCommandStore().execute('Comfy.ContactSupport')
}

View File

@@ -1,33 +0,0 @@
<template>
<div class="p-2 h-full" aria-labelledby="issue-report-title">
<Panel
:pt="{
root: 'border-none',
content: 'p-0'
}"
>
<template #header>
<header class="flex flex-col items-center w-full">
<h2 id="issue-report-title" class="text-4xl">
{{ title }}
</h2>
<span v-if="subtitle" class="text-muted mt-0">{{ subtitle }}</span>
</header>
</template>
<ReportIssuePanel v-bind="panelProps" :pt="{ root: 'border-none' }" />
</Panel>
</div>
</template>
<script setup lang="ts">
import Panel from 'primevue/panel'
import ReportIssuePanel from '@/components/dialog/content/error/ReportIssuePanel.vue'
import type { IssueReportPanelProps } from '@/types/issueReportTypes'
defineProps<{
title: string
subtitle?: string
panelProps: IssueReportPanelProps
}>()
</script>

View File

@@ -1,338 +0,0 @@
import { Form } from '@primevue/forms'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import Checkbox from 'primevue/checkbox'
import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext'
import Textarea from 'primevue/textarea'
import Tooltip from 'primevue/tooltip'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import enMesages from '@/locales/en/main.json'
import { IssueReportPanelProps } from '@/types/issueReportTypes'
import ReportIssuePanel from './ReportIssuePanel.vue'
const DEFAULT_FIELDS = ['Workflow', 'Logs', 'Settings', 'SystemStats']
const CUSTOM_FIELDS = [
{
label: 'Custom Field',
value: 'CustomField',
optIn: true,
getData: () => 'mock data'
}
]
async function getSubmittedContext() {
const { captureMessage } = (await import('@sentry/core')) as any
return captureMessage.mock.calls[0][1]
}
async function submitForm(wrapper: any) {
await wrapper.findComponent(Form).trigger('submit')
return getSubmittedContext()
}
async function findAndUpdateCheckbox(
wrapper: any,
value: string,
checked = true
) {
const checkbox = wrapper
.findAllComponents(Checkbox)
.find((c: any) => c.props('value') === value)
if (!checkbox) throw new Error(`Checkbox with value "${value}" not found`)
await checkbox.vm.$emit('update:modelValue', checked)
return checkbox
}
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: enMesages
}
})
vi.mock('primevue/usetoast', () => ({
useToast: vi.fn(() => ({
add: vi.fn()
}))
}))
vi.mock('@/scripts/api', () => ({
api: {
getLogs: vi.fn().mockResolvedValue('mock logs'),
getSystemStats: vi.fn().mockResolvedValue('mock stats'),
getSettings: vi.fn().mockResolvedValue('mock settings'),
fetchApi: vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue({}),
text: vi.fn().mockResolvedValue('')
}),
apiURL: vi.fn().mockReturnValue('https://test.com')
}
}))
vi.mock('@/scripts/app', () => ({
app: {
graph: {
asSerialisable: vi.fn().mockReturnValue({})
}
}
}))
vi.mock('@sentry/core', () => ({
captureMessage: vi.fn()
}))
vi.mock('@primevue/forms', () => ({
Form: {
name: 'Form',
template:
'<form @submit.prevent="onSubmit"><slot :values="formValues" /></form>',
props: ['resolver'],
data() {
return {
formValues: {}
}
},
methods: {
onSubmit() {
// @ts-expect-error fixme ts strict error
this.$emit('submit', {
valid: true,
// @ts-expect-error fixme ts strict error
values: this.formValues
})
},
updateFieldValue(name: string, value: any) {
// @ts-expect-error fixme ts strict error
this.formValues[name] = value
}
}
},
FormField: {
name: 'FormField',
template:
'<div><slot :modelValue="modelValue" @update:modelValue="updateValue" /></div>',
props: ['name'],
data() {
return {
modelValue: ''
}
},
methods: {
// @ts-expect-error fixme ts strict error
updateValue(value) {
// @ts-expect-error fixme ts strict error
this.modelValue = value
// @ts-expect-error fixme ts strict error
let parent = this.$parent
while (parent && parent.$options.name !== 'Form') {
parent = parent.$parent
}
if (parent) {
// @ts-expect-error fixme ts strict error
parent.updateFieldValue(this.name, value)
}
}
}
}
}))
vi.mock('@/stores/firebaseAuthStore', () => ({
useFirebaseAuthStore: () => ({
currentUser: {
email: 'test@example.com'
}
})
}))
describe('ReportIssuePanel', () => {
beforeEach(() => {
vi.clearAllMocks()
const pinia = createPinia()
setActivePinia(pinia)
})
const mountComponent = (props: IssueReportPanelProps, options = {}): any => {
return mount(ReportIssuePanel, {
global: {
plugins: [PrimeVue, i18n, createPinia()],
directives: { tooltip: Tooltip }
},
props,
...options
})
}
it('renders the panel with all required components', () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
expect(wrapper.find('.p-panel').exists()).toBe(true)
expect(wrapper.findAllComponents(Checkbox).length).toBe(6)
expect(wrapper.findComponent(InputText).exists()).toBe(true)
expect(wrapper.findComponent(Textarea).exists()).toBe(true)
})
it('updates selection when checkboxes are selected', async () => {
const wrapper = mountComponent({
errorType: 'Test Error'
})
const checkboxes = wrapper.findAllComponents(Checkbox)
for (const field of DEFAULT_FIELDS) {
const checkbox = checkboxes.find(
// @ts-expect-error fixme ts strict error
(checkbox) => checkbox.props('value') === field
)
expect(checkbox).toBeDefined()
await checkbox?.vm.$emit('update:modelValue', [field])
expect(wrapper.vm.selection).toContain(field)
}
})
it('updates contactInfo when input is changed', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const input = wrapper.findComponent(InputText)
await input.vm.$emit('update:modelValue', 'test@example.com')
const context = await submitForm(wrapper)
expect(context.user.email).toBe('test@example.com')
})
it('updates additional details when textarea is changed', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const textarea = wrapper.findComponent(Textarea)
await textarea.vm.$emit('update:modelValue', 'This is a test detail.')
const context = await submitForm(wrapper)
expect(context.extra.details).toBe('This is a test detail.')
})
it('set contact preferences back to false if email is removed', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const input = wrapper.findComponent(InputText)
// Set a valid email, enabling the contact preferences to be changed
await input.vm.$emit('update:modelValue', 'name@example.com')
// Enable both contact preferences
for (const pref of ['followUp', 'notifyOnResolution']) {
await findAndUpdateCheckbox(wrapper, pref)
}
// Change the email back to empty
await input.vm.$emit('update:modelValue', '')
const context = await submitForm(wrapper)
// Check that the contact preferences are back to false automatically
expect(context.tags.followUp).toBe(false)
expect(context.tags.notifyOnResolution).toBe(false)
})
it('renders with overridden default fields', () => {
const wrapper = mountComponent({
errorType: 'Test Error',
defaultFields: ['Settings']
})
// Filter out the contact preferences checkboxes
const fieldCheckboxes = wrapper.findAllComponents(Checkbox).filter(
// @ts-expect-error fixme ts strict error
(checkbox) =>
!['followUp', 'notifyOnResolution'].includes(checkbox.props('value'))
)
expect(fieldCheckboxes.length).toBe(1)
expect(fieldCheckboxes.at(0)?.props('value')).toBe('Settings')
})
it('renders additional fields when extraFields prop is provided', () => {
const wrapper = mountComponent({
errorType: 'Test Error',
extraFields: CUSTOM_FIELDS
})
const customCheckbox = wrapper
.findAllComponents(Checkbox)
// @ts-expect-error fixme ts strict error
.find((checkbox) => checkbox.props('value') === 'CustomField')
expect(customCheckbox).toBeDefined()
})
it('allows custom fields to be selected', async () => {
const wrapper = mountComponent({
errorType: 'Test Error',
extraFields: CUSTOM_FIELDS
})
await findAndUpdateCheckbox(wrapper, 'CustomField')
const context = await submitForm(wrapper)
expect(context.extra.CustomField).toBe('mock data')
})
it('does not submit unchecked fields', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const textarea = wrapper.findComponent(Textarea)
// Set details but don't check any field checkboxes
await textarea.vm.$emit(
'update:modelValue',
'Report with only text but no fields selected'
)
const context = await submitForm(wrapper)
// Verify none of the optional fields were included
for (const field of DEFAULT_FIELDS) {
expect(context.extra[field]).toBeUndefined()
}
})
it.each([
{
checkbox: 'Logs',
apiMethod: 'getLogs',
expectedKey: 'Logs',
mockValue: 'mock logs'
},
{
checkbox: 'SystemStats',
apiMethod: 'getSystemStats',
expectedKey: 'SystemStats',
mockValue: 'mock stats'
},
{
checkbox: 'Settings',
apiMethod: 'getSettings',
expectedKey: 'Settings',
mockValue: 'mock settings'
}
])(
'submits $checkbox data when checkbox is selected',
async ({ checkbox, apiMethod, expectedKey, mockValue }) => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const { api } = (await import('@/scripts/api')) as any
vi.spyOn(api, apiMethod).mockResolvedValue(mockValue)
await findAndUpdateCheckbox(wrapper, checkbox)
const context = await submitForm(wrapper)
expect(context.extra[expectedKey]).toBe(mockValue)
}
)
it('submits workflow when the Workflow checkbox is selected', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const { app } = (await import('@/scripts/app')) as any
const mockWorkflow = { nodes: [], edges: [] }
vi.spyOn(app.graph, 'asSerialisable').mockReturnValue(mockWorkflow)
await findAndUpdateCheckbox(wrapper, 'Workflow')
const context = await submitForm(wrapper)
expect(context.extra.Workflow).toEqual(mockWorkflow)
})
})

View File

@@ -1,348 +0,0 @@
<template>
<Form
v-slot="$form"
:resolver="zodResolver(issueReportSchema)"
@submit="submit"
>
<Panel :pt="$attrs.pt as any">
<template #header>
<div class="flex items-center gap-2">
<span class="font-bold">{{ title }}</span>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-4">
<Button
v-tooltip="!submitted ? $t('g.reportIssueTooltip') : undefined"
:label="submitted ? $t('g.reportSent') : $t('g.reportIssue')"
:severity="submitted ? 'secondary' : 'primary'"
:icon="submitted ? 'pi pi-check' : 'pi pi-send'"
:disabled="submitted"
type="submit"
/>
</div>
</template>
<div class="p-4 mt-2 border border-round surface-border shadow-1">
<div class="flex flex-col gap-6">
<FormField
v-slot="$field"
name="contactInfo"
:initial-value="authStore.currentUser?.email"
>
<div class="self-stretch inline-flex justify-start items-center">
<label for="contactInfo" class="pb-2 pt-0 opacity-80">{{
$t('issueReport.email')
}}</label>
</div>
<InputText
id="contactInfo"
v-bind="$field"
class="w-full"
:placeholder="$t('issueReport.provideEmail')"
/>
<Message
v-if="$field?.error && $field.touched && $field.value !== ''"
severity="error"
size="small"
variant="simple"
>
{{ t('issueReport.validation.invalidEmail') }}
</Message>
</FormField>
<FormField v-slot="$field" name="helpType">
<div class="flex flex-col gap-2">
<div
class="self-stretch inline-flex justify-start items-center gap-2.5"
>
<label for="helpType" class="pb-2 pt-0 opacity-80">{{
$t('issueReport.whatDoYouNeedHelpWith')
}}</label>
</div>
<Dropdown
v-bind="$field"
v-model="$field.value"
:options="helpTypes"
option-label="label"
option-value="value"
:placeholder="$t('issueReport.selectIssue')"
class="w-full"
/>
<Message
v-if="$field?.error"
severity="error"
size="small"
variant="simple"
>
{{ t('issueReport.validation.selectIssueType') }}
</Message>
</div>
</FormField>
<div class="flex flex-col gap-2">
<div
class="self-stretch inline-flex justify-start items-center gap-2.5"
>
<span class="pb-2 pt-0 opacity-80">{{
$t('issueReport.whatCanWeInclude')
}}</span>
</div>
<div class="flex flex-row gap-3">
<div v-for="field in fields" :key="field.value">
<FormField
v-if="field.optIn"
v-slot="$field"
:name="field.value"
class="flex space-x-1"
>
<Checkbox
v-bind="$field"
v-model="selection"
:input-id="field.value"
:value="field.value"
/>
<label :for="field.value">{{ field.label }}</label>
</FormField>
</div>
</div>
</div>
<div class="flex flex-col gap-2">
<FormField v-slot="$field" name="details">
<div
class="self-stretch inline-flex justify-start items-center gap-2.5"
>
<label for="details" class="pb-2 pt-0 opacity-80">{{
$t('issueReport.describeTheProblem')
}}</label>
</div>
<Textarea
v-bind="$field"
id="details"
class="w-full"
rows="5"
:placeholder="$t('issueReport.provideAdditionalDetails')"
:aria-label="$t('issueReport.provideAdditionalDetails')"
/>
<Message
v-if="$field?.error && $field.touched"
severity="error"
size="small"
variant="simple"
>
{{
$field.value
? t('issueReport.validation.maxLength')
: t('issueReport.validation.descriptionRequired')
}}
</Message>
</FormField>
</div>
<div class="flex flex-col gap-3 mt-2">
<div v-for="checkbox in contactCheckboxes" :key="checkbox.value">
<FormField
v-slot="$field"
:name="checkbox.value"
class="flex space-x-1"
>
<Checkbox
v-bind="$field"
v-model="contactPrefs"
:input-id="checkbox.value"
:value="checkbox.value"
:disabled="
$form.contactInfo?.error || !$form.contactInfo?.value
"
/>
<label :for="checkbox.value">{{ checkbox.label }}</label>
</FormField>
</div>
</div>
</div>
</div>
</Panel>
</Form>
</template>
<script setup lang="ts">
import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import type { CaptureContext, User } from '@sentry/core'
import { captureMessage } from '@sentry/core'
import _ from 'es-toolkit/compat'
import { cloneDeep } from 'es-toolkit/compat'
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
import Dropdown from 'primevue/dropdown'
import InputText from 'primevue/inputtext'
import Message from 'primevue/message'
import Panel from 'primevue/panel'
import Textarea from 'primevue/textarea'
import { useToast } from 'primevue/usetoast'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import {
type IssueReportFormData,
issueReportSchema
} from '@/schemas/issueReportSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import type {
DefaultField,
IssueReportPanelProps,
ReportField
} from '@/types/issueReportTypes'
import { isElectron } from '@/utils/envUtil'
import { generateUUID } from '@/utils/formatUtil'
const DEFAULT_ISSUE_NAME = 'User reported issue'
const props = defineProps<IssueReportPanelProps>()
const { defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'] } =
props
const { t } = useI18n()
const toast = useToast()
const authStore = useFirebaseAuthStore()
const selection = ref<string[]>([])
const contactPrefs = ref<string[]>([])
const submitted = ref(false)
const contactCheckboxes = [
{ label: t('issueReport.contactFollowUp'), value: 'followUp' },
{ label: t('issueReport.notifyResolve'), value: 'notifyOnResolution' }
]
const helpTypes = [
{
label: t('issueReport.helpTypes.billingPayments'),
value: 'billingPayments'
},
{
label: t('issueReport.helpTypes.loginAccessIssues'),
value: 'loginAccessIssues'
},
{ label: t('issueReport.helpTypes.giveFeedback'), value: 'giveFeedback' },
{ label: t('issueReport.helpTypes.bugReport'), value: 'bugReport' },
{ label: t('issueReport.helpTypes.somethingElse'), value: 'somethingElse' }
]
const defaultFieldsConfig: ReportField[] = [
{
label: t('issueReport.systemStats'),
value: 'SystemStats',
getData: () => api.getSystemStats(),
optIn: true
},
{
label: t('g.workflow'),
value: 'Workflow',
getData: () => cloneDeep(app.graph.asSerialisable()),
optIn: true
},
{
label: t('g.logs'),
value: 'Logs',
getData: () => api.getLogs(),
optIn: true
},
{
label: t('g.settings'),
value: 'Settings',
getData: () => api.getSettings(),
optIn: true
}
]
const fields = computed(() => [
...defaultFieldsConfig.filter(({ value }) =>
defaultFields.includes(value as DefaultField)
),
...(props.extraFields ?? [])
])
const createUser = (formData: IssueReportFormData): User => ({
email: formData.contactInfo || undefined
})
const createExtraData = async (formData: IssueReportFormData) => {
const result: Record<string, unknown> = {}
const isChecked = (fieldValue: string) => formData[fieldValue]
await Promise.all(
fields.value
.filter((field) => !field.optIn || isChecked(field.value))
.map(async (field) => {
try {
result[field.value] = await field.getData()
} catch (error) {
console.error(`Failed to collect ${field.value}:`, error)
result[field.value] = { error: String(error) }
}
})
)
return result
}
const createCaptureContext = async (
formData: IssueReportFormData
): Promise<CaptureContext> => {
return {
user: createUser(formData),
level: 'error',
tags: {
errorType: props.errorType,
helpType: formData.helpType,
followUp: formData.contactInfo ? formData.followUp : false,
notifyOnResolution: formData.contactInfo
? formData.notifyOnResolution
: false,
isElectron: isElectron(),
..._.mapValues(props.tags, (tag) => _.trim(tag).replace(/[\n\r\t]/g, ' '))
},
extra: {
details: formData.details,
...(await createExtraData(formData))
}
}
}
const generateUniqueTicketId = (type: string) => `${type}-${generateUUID()}`
const submit = async (event: FormSubmitEvent) => {
if (event.valid) {
try {
const captureContext = await createCaptureContext(event.values)
// If it's billing or access issue, generate unique id to be used by customer service ticketing
const isValidContactInfo = event.values.contactInfo?.length
const isCustomerServiceIssue =
isValidContactInfo &&
['billingPayments', 'loginAccessIssues'].includes(
event.values.helpType || ''
)
const issueName = isCustomerServiceIssue
? `ticket-${generateUniqueTicketId(event.values.helpType || '')}`
: DEFAULT_ISSUE_NAME
captureMessage(issueName, captureContext)
submitted.value = true
toast.add({
severity: 'success',
summary: t('g.reportSent'),
life: 3000
})
} catch (error) {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: error instanceof Error ? error.message : String(error),
life: 3000
})
}
}
}
</script>

View File

@@ -112,12 +112,12 @@ import Divider from 'primevue/divider'
import Skeleton from 'primevue/skeleton'
import TabPanel from 'primevue/tabpanel'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import UserCredit from '@/components/common/UserCredit.vue'
import UsageLogsTable from '@/components/dialog/content/setting/UsageLogsTable.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { formatMetronomeCurrency } from '@/utils/formatUtil'
@@ -128,10 +128,10 @@ interface CreditHistoryItemData {
isPositive: boolean
}
const { t } = useI18n()
const dialogService = useDialogService()
const authStore = useFirebaseAuthStore()
const authActions = useFirebaseAuthActions()
const commandStore = useCommandStore()
const loading = computed(() => authStore.loading)
const balanceLoading = computed(() => authStore.isFetchingBalance)
@@ -160,15 +160,8 @@ const handleCreditsHistoryClick = async () => {
await authActions.accessBillingPortal()
}
const handleMessageSupport = () => {
dialogService.showIssueReportDialog({
title: t('issueReport.contactSupportTitle'),
subtitle: t('issueReport.contactSupportDescription'),
panelProps: {
errorType: 'BillingSupport',
defaultFields: ['Workflow', 'Logs', 'SystemStats', 'Settings']
}
})
const handleMessageSupport = async () => {
await commandStore.execute('Comfy.ContactSupport')
}
const handleFaqClick = () => {

View File

@@ -277,7 +277,7 @@ const menuItems = computed<MenuItem[]>(() => {
icon: 'pi pi-question-circle',
label: t('helpCenter.helpFeedback'),
action: () => {
void commandStore.execute('Comfy.Feedback')
void commandStore.execute('Comfy.ContactSupport')
emit('close')
}
},