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

@@ -59,18 +59,6 @@ test.describe('Execution error', () => {
const executionError = comfyPage.page.locator('.comfy-error-report')
await expect(executionError).toBeVisible()
})
test('Can display Issue Report form', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
await comfyPage.page.getByLabel('Help Fix This').click()
const issueReportForm = comfyPage.page.getByText(
'Submit Error Report (Optional)'
)
await expect(issueReportForm).toBeVisible()
})
})
test.describe('Missing models warning', () => {
@@ -303,37 +291,16 @@ test.describe('Settings', () => {
})
})
test.describe('Feedback dialog', () => {
test('Should open from topmenu help command', async ({ comfyPage }) => {
// Open feedback dialog from top menu
test.describe('Support', () => {
test('Should open external zendesk link', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
const pagePromise = comfyPage.page.context().waitForEvent('page')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
const newPage = await pagePromise
// Verify feedback dialog content is visible
const feedbackHeader = comfyPage.page.getByRole('heading', {
name: 'Feedback'
})
await expect(feedbackHeader).toBeVisible()
})
test('Should close when close button clicked', async ({ comfyPage }) => {
// Open feedback dialog
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
const feedbackHeader = comfyPage.page.getByRole('heading', {
name: 'Feedback'
})
// Close feedback dialog
await comfyPage.page
.getByLabel('', { exact: true })
.getByLabel('Close')
.click()
await feedbackHeader.waitFor({ state: 'hidden' })
// Verify dialog is closed
await expect(feedbackHeader).not.toBeVisible()
await newPage.waitForLoadState('networkidle')
await expect(newPage).toHaveURL(/.*support\.comfy\.org.*/)
await newPage.close()
})
})

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')
}
},

View File

@@ -679,36 +679,13 @@ export function useCoreCommands(): ComfyCommand[] {
await workflowService.closeWorkflow(workflowStore.activeWorkflow)
}
},
{
id: 'Comfy.Feedback',
icon: 'pi pi-megaphone',
label: 'Give Feedback',
versionAdded: '1.8.2',
function: () => {
dialogService.showIssueReportDialog({
title: t('g.feedback'),
subtitle: t('issueReport.feedbackTitle'),
panelProps: {
errorType: 'Feedback',
defaultFields: ['SystemStats', 'Settings']
}
})
}
},
{
id: 'Comfy.ContactSupport',
icon: 'pi pi-question',
label: 'Contact Support',
versionAdded: '1.17.8',
function: () => {
dialogService.showIssueReportDialog({
title: t('issueReport.contactSupportTitle'),
subtitle: t('issueReport.contactSupportDescription'),
panelProps: {
errorType: 'ContactSupport',
defaultFields: ['Workflow', 'Logs', 'SystemStats', 'Settings']
}
})
window.open('https://support.comfy.org/', '_blank')
}
},
{

View File

@@ -23,8 +23,5 @@ export const CORE_MENU_COMMANDS = [
'Comfy.Help.OpenComfyUIForum'
]
],
[
['Help'],
['Comfy.Help.AboutComfyUI', 'Comfy.Feedback', 'Comfy.ContactSupport']
]
[['Help'], ['Comfy.Help.AboutComfyUI', 'Comfy.ContactSupport']]
]

View File

@@ -551,37 +551,7 @@
"updateConsent": "لقد وافقت سابقًا على الإبلاغ عن الأعطال. نحن الآن نتتبع إحصائيات مبنية على الأحداث للمساعدة في تحديد الأخطاء وتحسين التطبيق. لا يتم جمع معلومات شخصية قابلة للتعريف."
},
"issueReport": {
"contactFollowUp": "اتصل بي للمتابعة",
"contactSupportDescription": "يرجى ملء النموذج أدناه مع تقريرك",
"contactSupportTitle": "الاتصال بالدعم",
"describeTheProblem": "صف المشكلة",
"email": "البريد الإلكتروني",
"feedbackTitle": "ساعدنا في تحسين ComfyUI من خلال تقديم الملاحظات",
"helpFix": "المساعدة في الإصلاح",
"helpTypes": {
"billingPayments": "الفوترة / المدفوعات",
"bugReport": "تقرير خطأ",
"giveFeedback": "إرسال ملاحظات",
"loginAccessIssues": "مشكلة في تسجيل الدخول / الوصول",
"somethingElse": "أمر آخر"
},
"notifyResolve": "أعلمني عند الحل",
"provideAdditionalDetails": "أضف تفاصيل إضافية",
"provideEmail": "زودنا ببريدك الإلكتروني (اختياري)",
"rating": "التقييم",
"selectIssue": "اختر المشكلة",
"stackTrace": "أثر التكديس",
"submitErrorReport": "إرسال تقرير الخطأ (اختياري)",
"systemStats": "إحصائيات النظام",
"validation": {
"descriptionRequired": "الوصف مطلوب",
"helpTypeRequired": "نوع المساعدة مطلوب",
"invalidEmail": "يرجى إدخال بريد إلكتروني صالح",
"maxLength": "الرسالة طويلة جداً",
"selectIssueType": "يرجى اختيار نوع المشكلة"
},
"whatCanWeInclude": "حدد ما يجب تضمينه في التقرير",
"whatDoYouNeedHelpWith": "بماذا تحتاج المساعدة؟"
"helpFix": "المساعدة في الإصلاح"
},
"load3d": {
"applyingTexture": "جارٍ تطبيق الخامة...",

View File

@@ -210,37 +210,7 @@
}
},
"issueReport": {
"submitErrorReport": "Submit Error Report (Optional)",
"provideEmail": "Give us your email (optional)",
"provideAdditionalDetails": "Provide additional details",
"stackTrace": "Stack Trace",
"systemStats": "System Stats",
"contactFollowUp": "Contact me for follow up",
"notifyResolve": "Notify me when resolved",
"helpFix": "Help Fix This",
"rating": "Rating",
"feedbackTitle": "Help us improve ComfyUI by providing feedback",
"contactSupportTitle": "Contact Support",
"contactSupportDescription": "Please fill in the form below with your report",
"selectIssue": "Select the issue",
"whatDoYouNeedHelpWith": "What do you need help with?",
"whatCanWeInclude": "Specify what to include in the report",
"describeTheProblem": "Describe the problem",
"email": "Email",
"helpTypes": {
"billingPayments": "Billing / Payments",
"loginAccessIssues": "Login / Access Issues",
"giveFeedback": "Give Feedback",
"bugReport": "Bug Report",
"somethingElse": "Something Else"
},
"validation": {
"maxLength": "Message too long",
"invalidEmail": "Please enter a valid email address",
"selectIssueType": "Please select an issue type",
"descriptionRequired": "Description is required",
"helpTypeRequired": "Help type is required"
}
"helpFix": "Help Fix This"
},
"color": {
"noColor": "No Color",

View File

@@ -551,37 +551,7 @@
"updateConsent": "Anteriormente optaste por reportar fallos. Ahora estamos rastreando métricas basadas en eventos para ayudar a identificar errores y mejorar la aplicación. No se recoge ninguna información personal identificable."
},
"issueReport": {
"contactFollowUp": "Contáctame para seguimiento",
"contactSupportDescription": "Por favor, complete el siguiente formulario con su reporte",
"contactSupportTitle": "Contactar Soporte",
"describeTheProblem": "Describa el problema",
"email": "Correo electrónico",
"feedbackTitle": "Ayúdanos a mejorar ComfyUI proporcionando comentarios",
"helpFix": "Ayuda a Solucionar Esto",
"helpTypes": {
"billingPayments": "Facturación / Pagos",
"bugReport": "Reporte de error",
"giveFeedback": "Enviar comentarios",
"loginAccessIssues": "Problemas de inicio de sesión / acceso",
"somethingElse": "Otro"
},
"notifyResolve": "Notifícame cuando se resuelva",
"provideAdditionalDetails": "Proporciona detalles adicionales (opcional)",
"provideEmail": "Danos tu correo electrónico (opcional)",
"rating": "Calificación",
"selectIssue": "Seleccione el problema",
"stackTrace": "Rastreo de Pila",
"submitErrorReport": "Enviar Reporte de Error (Opcional)",
"systemStats": "Estadísticas del Sistema",
"validation": {
"descriptionRequired": "Se requiere una descripción",
"helpTypeRequired": "Se requiere el tipo de ayuda",
"invalidEmail": "Por favor ingresa una dirección de correo electrónico válida",
"maxLength": "Mensaje demasiado largo",
"selectIssueType": "Por favor, seleccione un tipo de problema"
},
"whatCanWeInclude": "Especifique qué incluir en el reporte",
"whatDoYouNeedHelpWith": "¿Con qué necesita ayuda?"
"helpFix": "Ayuda a Solucionar Esto"
},
"load3d": {
"applyingTexture": "Aplicando textura...",

View File

@@ -551,37 +551,7 @@
"updateConsent": "Vous avez précédemment accepté de signaler les plantages. Nous suivons maintenant des métriques basées sur les événements pour aider à identifier les bugs et améliorer l'application. Aucune information personnelle identifiable n'est collectée."
},
"issueReport": {
"contactFollowUp": "Contactez-moi pour un suivi",
"contactSupportDescription": "Veuillez remplir le formulaire ci-dessous avec votre signalement",
"contactSupportTitle": "Contacter le support",
"describeTheProblem": "Décrivez le problème",
"email": "E-mail",
"feedbackTitle": "Aidez-nous à améliorer ComfyUI en fournissant des commentaires",
"helpFix": "Aidez à résoudre cela",
"helpTypes": {
"billingPayments": "Facturation / Paiements",
"bugReport": "Signaler un bug",
"giveFeedback": "Donner un avis",
"loginAccessIssues": "Problèmes de connexion / d'accès",
"somethingElse": "Autre chose"
},
"notifyResolve": "Prévenez-moi lorsque résolu",
"provideAdditionalDetails": "Fournir des détails supplémentaires (facultatif)",
"provideEmail": "Donnez-nous votre email (Facultatif)",
"rating": "Évaluation",
"selectIssue": "Sélectionnez le problème",
"stackTrace": "Trace de la pile",
"submitErrorReport": "Soumettre un rapport d'erreur (Facultatif)",
"systemStats": "Statistiques du système",
"validation": {
"descriptionRequired": "La description est requise",
"helpTypeRequired": "Le type d'aide est requis",
"invalidEmail": "Veuillez entrer une adresse e-mail valide",
"maxLength": "Message trop long",
"selectIssueType": "Veuillez sélectionner un type de problème"
},
"whatCanWeInclude": "Précisez ce qu'il faut inclure dans le rapport",
"whatDoYouNeedHelpWith": "Avec quoi avez-vous besoin d'aide ?"
"helpFix": "Aidez à résoudre cela"
},
"load3d": {
"applyingTexture": "Application de la texture...",

View File

@@ -551,37 +551,7 @@
"updateConsent": "以前、クラッシュレポートを報告することに同意していました。現在、バグの特定とアプリの改善を助けるためにイベントベースのメトリクスを追跡しています。個人を特定できる情報は収集されません。"
},
"issueReport": {
"contactFollowUp": "フォローアップのために私に連絡する",
"contactSupportDescription": "下記のフォームにご報告内容をご記入ください",
"contactSupportTitle": "サポートに連絡",
"describeTheProblem": "問題の内容を記述してください",
"email": "メールアドレス",
"feedbackTitle": "フィードバックを提供してComfyUIの改善にご協力ください",
"helpFix": "これを修正するのを助ける",
"helpTypes": {
"billingPayments": "請求/支払い",
"bugReport": "バグ報告",
"giveFeedback": "フィードバックを送る",
"loginAccessIssues": "ログイン/アクセスの問題",
"somethingElse": "その他"
},
"notifyResolve": "解決したときに通知する",
"provideAdditionalDetails": "追加の詳細を提供する(オプション)",
"provideEmail": "あなたのメールアドレスを教えてください(オプション)",
"rating": "評価",
"selectIssue": "問題を選択してください",
"stackTrace": "スタックトレース",
"submitErrorReport": "エラーレポートを提出する(オプション)",
"systemStats": "システム統計",
"validation": {
"descriptionRequired": "説明は必須です",
"helpTypeRequired": "ヘルプの種類は必須です",
"invalidEmail": "有効なメールアドレスを入力してください",
"maxLength": "メッセージが長すぎます",
"selectIssueType": "問題の種類を選択してください"
},
"whatCanWeInclude": "レポートに含める内容を指定してください",
"whatDoYouNeedHelpWith": "どのようなサポートが必要ですか?"
"helpFix": "これを修正するのを助ける"
},
"load3d": {
"applyingTexture": "テクスチャを適用中...",

View File

@@ -551,37 +551,7 @@
"updateConsent": "이전에 충돌 보고에 동의하셨습니다. 이제 버그를 식별하고 앱을 개선하기 위해 이벤트 기반 통계 정보의 추적을 시작합니다. 개인을 식별할 수 있는 정보는 수집되지 않습니다."
},
"issueReport": {
"contactFollowUp": "추적 조사를 위해 연락해 주세요",
"contactSupportDescription": "아래 양식에 보고 내용을 작성해 주세요",
"contactSupportTitle": "지원팀에 문의하기",
"describeTheProblem": "문제를 설명해 주세요",
"email": "이메일",
"feedbackTitle": "피드백을 제공함으로써 ComfyUI를 개선하는 데 도움을 주십시오",
"helpFix": "이 문제 해결에 도움을 주세요",
"helpTypes": {
"billingPayments": "결제 / 지불",
"bugReport": "버그 신고",
"giveFeedback": "피드백 제공",
"loginAccessIssues": "로그인 / 접근 문제",
"somethingElse": "기타"
},
"notifyResolve": "해결되었을 때 알려주세요",
"provideAdditionalDetails": "추가 세부 사항 제공 (선택 사항)",
"provideEmail": "이메일을 알려주세요 (선택 사항)",
"rating": "평가",
"selectIssue": "문제를 선택하세요",
"stackTrace": "스택 추적",
"submitErrorReport": "오류 보고서 제출 (선택 사항)",
"systemStats": "시스템 통계",
"validation": {
"descriptionRequired": "설명은 필수입니다",
"helpTypeRequired": "도움 유형은 필수입니다",
"invalidEmail": "유효한 이메일 주소를 입력해 주세요",
"maxLength": "메시지가 너무 깁니다",
"selectIssueType": "문제 유형을 선택해 주세요"
},
"whatCanWeInclude": "보고서에 포함할 내용을 지정하세요",
"whatDoYouNeedHelpWith": "어떤 도움이 필요하신가요?"
"helpFix": "이 문제 해결에 도움을 주세요"
},
"load3d": {
"applyingTexture": "텍스처 적용 중...",

View File

@@ -551,37 +551,7 @@
"updateConsent": "Вы ранее согласились на отчётность об ошибках. Теперь мы отслеживаем метрики событий, чтобы помочь выявить ошибки и улучшить приложение. Личная идентифицируемая информация не собирается."
},
"issueReport": {
"contactFollowUp": "Свяжитесь со мной для уточнения",
"contactSupportDescription": "Пожалуйста, заполните форму ниже для отправки вашего отчёта",
"contactSupportTitle": "Связаться с поддержкой",
"describeTheProblem": "Опишите проблему",
"email": "Электронная почта",
"feedbackTitle": "Помогите нам улучшить ComfyUI, оставив отзыв",
"helpFix": "Помочь исправить это",
"helpTypes": {
"billingPayments": "Оплата / Платежи",
"bugReport": "Сообщить об ошибке",
"giveFeedback": "Оставить отзыв",
"loginAccessIssues": "Проблемы со входом / доступом",
"somethingElse": "Другое"
},
"notifyResolve": "Уведомить меня, когда проблема будет решена",
"provideAdditionalDetails": "Предоставьте дополнительные сведения (необязательно)",
"provideEmail": "Укажите вашу электронную почту (необязательно)",
"rating": "Рейтинг",
"selectIssue": "Выберите проблему",
"stackTrace": "Трассировка стека",
"submitErrorReport": "Отправить отчёт об ошибке (необязательно)",
"systemStats": "Статистика системы",
"validation": {
"descriptionRequired": "Описание обязательно",
"helpTypeRequired": "Тип помощи обязателен",
"invalidEmail": "Пожалуйста, введите действительный адрес электронной почты",
"maxLength": "Сообщение слишком длинное",
"selectIssueType": "Пожалуйста, выберите тип проблемы"
},
"whatCanWeInclude": "Уточните, что включить в отчёт",
"whatDoYouNeedHelpWith": "С чем вам нужна помощь?"
"helpFix": "Помочь исправить это"
},
"load3d": {
"applyingTexture": "Применение текстуры...",

View File

@@ -551,37 +551,7 @@
"updateConsent": "您先前已選擇回報當機。現在我們會追蹤事件型統計資料,以協助找出錯誤並改進應用程式。不會收集任何可識別個人身分的資訊。"
},
"issueReport": {
"contactFollowUp": "需要聯絡我以便後續追蹤",
"contactSupportDescription": "請填寫下列表單並提交您的報告",
"contactSupportTitle": "聯絡客服支援",
"describeTheProblem": "請描述問題",
"email": "電子郵件",
"feedbackTitle": "協助我們改進 ComfyUI請提供您的回饋",
"helpFix": "協助修復此問題",
"helpTypes": {
"billingPayments": "帳單/付款問題",
"bugReport": "錯誤回報",
"giveFeedback": "提供回饋",
"loginAccessIssues": "登入/存取問題",
"somethingElse": "其他"
},
"notifyResolve": "問題解決時通知我",
"provideAdditionalDetails": "提供更多細節",
"provideEmail": "請提供您的電子郵件(選填)",
"rating": "評分",
"selectIssue": "請選擇問題",
"stackTrace": "堆疊追蹤",
"submitErrorReport": "提交錯誤報告(選填)",
"systemStats": "系統狀態",
"validation": {
"descriptionRequired": "請填寫問題描述",
"helpTypeRequired": "請選擇協助類型",
"invalidEmail": "請輸入有效的電子郵件地址",
"maxLength": "訊息過長",
"selectIssueType": "請選擇問題類型"
},
"whatCanWeInclude": "請說明報告中要包含哪些內容",
"whatDoYouNeedHelpWith": "您需要什麼協助?"
"helpFix": "協助修復此問題"
},
"load3d": {
"applyingTexture": "正在套用材質貼圖...",

View File

@@ -551,37 +551,7 @@
"updateConsent": "您之前选择了报告崩溃。我们现在正在跟踪基于事件的度量,以帮助识别错误并改进应用程序。我们不收集任何个人可识别信息。"
},
"issueReport": {
"contactFollowUp": "跟进联系我",
"contactSupportDescription": "请填写下方表格提交您的报告",
"contactSupportTitle": "联系支持",
"describeTheProblem": "描述问题",
"email": "电子邮箱",
"feedbackTitle": "通过提供反馈帮助我们改进ComfyUI",
"helpFix": "帮助修复这个",
"helpTypes": {
"billingPayments": "账单 / 支付",
"bugReport": "错误报告",
"giveFeedback": "提交反馈",
"loginAccessIssues": "登录 / 访问问题",
"somethingElse": "其他"
},
"notifyResolve": "解决时通知我",
"provideAdditionalDetails": "提供额外的详细信息(可选)",
"provideEmail": "提供您的电子邮件(可选)",
"rating": "评分",
"selectIssue": "选择问题",
"stackTrace": "堆栈跟踪",
"submitErrorReport": "提交错误报告(可选)",
"systemStats": "系统状态",
"validation": {
"descriptionRequired": "描述为必填项",
"helpTypeRequired": "帮助类型为必选项",
"invalidEmail": "请输入有效的电子邮件地址",
"maxLength": "消息过长",
"selectIssueType": "请选择一个问题类型"
},
"whatCanWeInclude": "请说明报告中需要包含的内容",
"whatDoYouNeedHelpWith": "您需要什么帮助?"
"helpFix": "帮助修复这个"
},
"load3d": {
"applyingTexture": "应用纹理中...",

View File

@@ -1,28 +0,0 @@
import { z } from 'zod'
import { t } from '@/i18n'
const checkboxField = z.boolean().optional()
export const issueReportSchema = z
.object({
contactInfo: z.string().email().max(320).optional().or(z.literal('')),
details: z
.string()
.min(1, { message: t('validation.descriptionRequired') })
.max(5_000, { message: t('validation.maxLength', { length: 5_000 }) })
.optional(),
helpType: z.string().optional()
})
.catchall(checkboxField)
.refine((data) => Object.values(data).some((value) => value), {
path: ['details', 'helpType']
})
.refine((data) => data.helpType !== undefined && data.helpType !== '', {
message: t('issueReport.validation.helpTypeRequired'),
path: ['helpType']
})
.refine((data) => data.details !== undefined && data.details !== '', {
message: t('issueReport.validation.descriptionRequired'),
path: ['details']
})
export type IssueReportFormData = z.infer<typeof issueReportSchema>

View File

@@ -4,7 +4,6 @@ import { Component } from 'vue'
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
import ErrorDialogContent from '@/components/dialog/content/ErrorDialogContent.vue'
import IssueReportDialogContent from '@/components/dialog/content/IssueReportDialogContent.vue'
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
import ManagerProgressDialogContent from '@/components/dialog/content/ManagerProgressDialogContent.vue'
import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
@@ -124,16 +123,6 @@ export const useDialogService = () => {
})
}
function showIssueReportDialog(
props: InstanceType<typeof IssueReportDialogContent>['$props']
) {
dialogStore.showDialog({
key: 'global-issue-report',
component: IssueReportDialogContent,
props
})
}
function showManagerDialog(
props: InstanceType<typeof ManagerDialogContent>['$props'] = {}
) {
@@ -470,7 +459,6 @@ export const useDialogService = () => {
showAboutDialog,
showExecutionErrorDialog,
showTemplateWorkflowsDialog,
showIssueReportDialog,
showManagerDialog,
showManagerProgressDialog,
showErrorDialog,

View File

@@ -1,51 +0,0 @@
export type DefaultField = 'Workflow' | 'Logs' | 'SystemStats' | 'Settings'
export interface ReportField {
/**
* The label of the field, shown next to the checkbox if the field is opt-in.
*/
label: string
/**
* A unique identifier for the field, used internally as the key for this field's value.
*/
value: string
/**
* The data associated with this field, sent as part of the report.
*/
getData: () => unknown
/**
* Indicates whether the field requires explicit opt-in from the user
* before its data is included in the report.
*/
optIn: boolean
}
export interface IssueReportPanelProps {
/**
* The type of error being reported. This is used to categorize the error.
*/
errorType: string
/**
* Which of the default fields to include in the report.
*/
defaultFields?: DefaultField[]
/**
* Additional fields to include in the report.
*/
extraFields?: ReportField[]
/**
* Tags that will be added to the report. Tags are used to further categorize the error.
*/
tags?: Record<string, string>
/**
* The title displayed in the dialog.
*/
title?: string
}