diff --git a/browser_tests/tests/dialog.spec.ts b/browser_tests/tests/dialog.spec.ts
index bdfcd392f..8ac7449f4 100644
--- a/browser_tests/tests/dialog.spec.ts
+++ b/browser_tests/tests/dialog.spec.ts
@@ -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()
})
})
diff --git a/src/components/dialog/content/ErrorDialogContent.vue b/src/components/dialog/content/ErrorDialogContent.vue
index bdf10482a..4f35511cf 100644
--- a/src/components/dialog/content/ErrorDialogContent.vue
+++ b/src/components/dialog/content/ErrorDialogContent.vue
@@ -21,16 +21,9 @@
@click="showReport"
/>
-
@@ -41,16 +34,6 @@
-
& {
/**
@@ -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(
() => error.nodeType ?? error.exceptionType ?? t('errorDialog.defaultTitle')
)
-const stackTraceField = computed(() => {
- return {
- label: t('issueReport.stackTrace'),
- value: 'StackTrace',
- optIn: true,
- getData: () => error.traceback
- }
-})
-
const showContactSupport = async () => {
await useCommandStore().execute('Comfy.ContactSupport')
}
diff --git a/src/components/dialog/content/IssueReportDialogContent.vue b/src/components/dialog/content/IssueReportDialogContent.vue
deleted file mode 100644
index 871807c02..000000000
--- a/src/components/dialog/content/IssueReportDialogContent.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
- {{ title }}
-
- {{ subtitle }}
-
-
-
-
-
-
-
-
diff --git a/src/components/dialog/content/error/ReportIssuePanel.spec.ts b/src/components/dialog/content/error/ReportIssuePanel.spec.ts
deleted file mode 100644
index 46f8626d6..000000000
--- a/src/components/dialog/content/error/ReportIssuePanel.spec.ts
+++ /dev/null
@@ -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:
- '',
- 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:
- '
',
- 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)
- })
-})
diff --git a/src/components/dialog/content/error/ReportIssuePanel.vue b/src/components/dialog/content/error/ReportIssuePanel.vue
deleted file mode 100644
index 13f50030f..000000000
--- a/src/components/dialog/content/error/ReportIssuePanel.vue
+++ /dev/null
@@ -1,348 +0,0 @@
-
-
-
-
-
diff --git a/src/components/dialog/content/setting/CreditsPanel.vue b/src/components/dialog/content/setting/CreditsPanel.vue
index 4d6486a98..0931f61e6 100644
--- a/src/components/dialog/content/setting/CreditsPanel.vue
+++ b/src/components/dialog/content/setting/CreditsPanel.vue
@@ -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 = () => {
diff --git a/src/components/helpcenter/HelpCenterMenuContent.vue b/src/components/helpcenter/HelpCenterMenuContent.vue
index 307cd4574..5bc5b0d42 100644
--- a/src/components/helpcenter/HelpCenterMenuContent.vue
+++ b/src/components/helpcenter/HelpCenterMenuContent.vue
@@ -277,7 +277,7 @@ const menuItems = computed