mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
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:
committed by
GitHub
parent
b091f3aa08
commit
8f7ee4e9a3
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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']]
|
||||
]
|
||||
|
||||
@@ -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": "جارٍ تطبيق الخامة...",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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": "テクスチャを適用中...",
|
||||
|
||||
@@ -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": "텍스처 적용 중...",
|
||||
|
||||
@@ -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": "Применение текстуры...",
|
||||
|
||||
@@ -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": "正在套用材質貼圖...",
|
||||
|
||||
@@ -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": "应用纹理中...",
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user