mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 06:35:10 +00:00
## Summary Phase 2 of the dialog migration kicked off in #11719 and continued in #12041. Migrates four medium-complexity dialogs to the Reka-UI primitives. Public API of `useDialogService` / `dialogStore` is unchanged. Parent: [FE-571](https://linear.app/comfyorg/issue/FE-571/dialog-system-migration-primevue-reka-ui-parent) This phase: [FE-574](https://linear.app/comfyorg/issue/FE-574/phase-2-migrate-error-nodesearchbox-secretform-videohelp-customization) Predecessors: #11719 (Phase 0, merged), #12041 (Phase 1, merged) > **NodeSearchBoxPopover deferred** — host of an inner PrimeVue Dialog (filter panel) that teleports to body and conflicts with Reka's DismissableLayer outside-pointer detection (CI dismissed the outer dialog mid-interaction). Tracking as a follow-up PR; FE-574 stays open for it. ## Changes ### `src/services/dialogService.ts` | Call site | Renderer | Size | | --- | --- | --- | | `showExecutionErrorDialog()` | `'reka'` | `lg` | | `showErrorDialog()` | `'reka'` | `lg` | ### `src/components/dialog/content/ErrorDialogContent.vue` - Drops `import Divider from 'primevue/divider'` and `import ScrollPanel from 'primevue/scrollpanel'` - Replaces with `<hr class="border-t border-border-subtle">` + `<div class="h-[400px] w-full max-w-[80vw] overflow-auto">` ### Direct PrimeVue → Reka swaps (no `dialogStore` involvement) | File | Notes | | --- | --- | | `src/components/common/CustomizationDialog.vue` | Reka primitives + DialogTitle/Header/Footer; drops PrimeVue Divider; `:modal="false"` and `pointer-down-outside` overlay guard so the PrimeVue ColorPicker overlay (teleported to body) does not auto-dismiss the dialog | | `src/platform/assets/components/VideoHelpDialog.vue` | Headless Reka content; preserves capture-phase ESC by stopping propagation on `escape-key-down`; `VisuallyHidden` title for a11y | | `src/platform/secrets/components/SecretFormDialog.vue` | Reka primitives, retains `v-model:visible`, autofocus on the provider trigger, form submit/validation | ### Tests - `src/services/dialogService.renderer.test.ts`: extends the regression net to cover both error-dialog call sites (renderer `'reka'`, size `'lg'`) - `src/components/common/CustomizationDialog.test.ts`: swaps PrimeVue Dialog stub for Reka primitive stubs ### screenshot <img width="1236" height="761" alt="Screenshot 2026-05-11 at 10 26 51 PM" src="https://github.com/user-attachments/assets/086cb73f-a98d-41f8-96ee-21922da8dd73" /> <img width="1161" height="786" alt="Screenshot 2026-05-12 at 1 26 39 PM" src="https://github.com/user-attachments/assets/db7383d8-f737-4472-91c0-dab5aa41547b" /> ## Quality gates - [x] `pnpm typecheck` — clean - [x] `pnpm lint` — 0 errors (3 pre-existing warnings unrelated to this PR) - [x] `pnpm format` — applied - [x] `pnpm test:unit` (touched + adjacent areas): - `dialogService.renderer.test.ts` — 5/5 - `CustomizationDialog.test.ts` — 4/4 - All `src/components/dialog` tests — 73/73 - `src/platform/secrets` tests — 39/39 - `NodeBookmarkTreeExplorer.test.ts` — 7/7 - [ ] CI Playwright matrix ## Public API impact None. `useDialogService` / `dialogStore` signatures unchanged. Custom-node extensions calling `app.extensionManager.dialog.*` continue to work. ## Out of scope (later phases) - NodeSearchBoxPopover — follow-up PR under FE-574 - Settings dialog — Phase 3 (FE-575) - Manager dialog — Phase 4 (FE-576) - `ConfirmDialog` callers (`SecretsPanel`, `BaseWorkflowsSidebarTab`) — Phase 5 (FE-577) - Removing PrimeVue `Dialog`/`<style>` overrides in `GlobalDialog.vue` — Phase 6 (FE-578) ## Test plan - [x] Unit: 73/73 dialog-area, 39/39 secrets - [ ] CI: full Vitest + Playwright matrix - [ ] Manual on a backend: - Trigger an execution error → error dialog opens through Reka, scroll body, copy-to-clipboard, close - Add/edit a secret → form submits, validation errors render, ESC and cancel close - Open VideoHelpDialog from `UploadModelFooter` while inside the asset modal → ESC closes only the help dialog - Customize a node bookmark color/icon → apply/reset, color picker overlay works
162 lines
4.6 KiB
Vue
162 lines
4.6 KiB
Vue
<template>
|
|
<div
|
|
data-testid="error-dialog"
|
|
class="comfy-error-report flex flex-col gap-4"
|
|
>
|
|
<NoResultsPlaceholder
|
|
class="pb-0"
|
|
icon="pi pi-exclamation-circle"
|
|
:title="title"
|
|
:message="error.exceptionMessage"
|
|
text-class="break-words max-w-[60vw]"
|
|
/>
|
|
<template v-if="error.extensionFile">
|
|
<span>{{ t('errorDialog.extensionFileHint') }}:</span>
|
|
<br />
|
|
<span class="font-bold">{{ error.extensionFile }}</span>
|
|
</template>
|
|
|
|
<div class="flex justify-center gap-2">
|
|
<Button
|
|
v-show="!reportOpen"
|
|
data-testid="error-dialog-show-report"
|
|
variant="textonly"
|
|
@click="showReport"
|
|
>
|
|
{{ $t('g.showReport') }}
|
|
</Button>
|
|
<Button
|
|
v-show="!reportOpen"
|
|
data-testid="error-dialog-contact-support"
|
|
variant="textonly"
|
|
@click="showContactSupport"
|
|
>
|
|
{{ $t('issueReport.helpFix') }}
|
|
</Button>
|
|
</div>
|
|
<template v-if="reportOpen">
|
|
<hr class="border-t border-border-subtle" />
|
|
<div class="h-[400px] w-full max-w-[80vw] overflow-auto">
|
|
<pre class="wrap-break-word whitespace-pre-wrap">{{
|
|
reportContent
|
|
}}</pre>
|
|
</div>
|
|
<hr class="border-t border-border-subtle" />
|
|
</template>
|
|
<div class="flex justify-end gap-4">
|
|
<FindIssueButton
|
|
:error-message="error.exceptionMessage"
|
|
:repo-owner="repoOwner"
|
|
:repo-name="repoName"
|
|
/>
|
|
<Button
|
|
v-if="reportOpen"
|
|
data-testid="error-dialog-copy-report"
|
|
@click="copyReportToClipboard"
|
|
>
|
|
<i class="pi pi-copy" />
|
|
{{ $t('g.copyToClipboard') }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useToast } from 'primevue/usetoast'
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
|
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
|
import { useTelemetry } from '@/platform/telemetry'
|
|
import { api } from '@/scripts/api'
|
|
import { app } from '@/scripts/app'
|
|
import { useCommandStore } from '@/stores/commandStore'
|
|
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
|
import { generateErrorReport } from '@/utils/errorReportUtil'
|
|
import type { ErrorReportData } from '@/utils/errorReportUtil'
|
|
|
|
const { error } = defineProps<{
|
|
error: Omit<ErrorReportData, 'workflow' | 'systemStats' | 'serverLogs'> & {
|
|
/**
|
|
* The type of error report to submit.
|
|
* @default 'unknownError'
|
|
*/
|
|
reportType?: string
|
|
/**
|
|
* The file name of the extension that caused the error.
|
|
*/
|
|
extensionFile?: string
|
|
}
|
|
}>()
|
|
|
|
const repoOwner = 'comfyanonymous'
|
|
const repoName = 'ComfyUI'
|
|
const reportContent = ref('')
|
|
const reportOpen = ref(false)
|
|
/**
|
|
* Open the error report content and track telemetry.
|
|
*/
|
|
const showReport = () => {
|
|
useTelemetry()?.trackUiButtonClicked({
|
|
button_id: 'error_dialog_show_report_clicked'
|
|
})
|
|
reportOpen.value = true
|
|
}
|
|
const toast = useToast()
|
|
const { t } = useI18n()
|
|
const systemStatsStore = useSystemStatsStore()
|
|
const telemetry = useTelemetry()
|
|
|
|
const title = computed<string>(
|
|
() => error.nodeType ?? error.exceptionType ?? t('errorDialog.defaultTitle')
|
|
)
|
|
|
|
/**
|
|
* Open contact support flow from error dialog and track telemetry.
|
|
*/
|
|
const showContactSupport = async () => {
|
|
telemetry?.trackHelpResourceClicked({
|
|
resource_type: 'help_feedback',
|
|
is_external: true,
|
|
source: 'error_dialog'
|
|
})
|
|
await useCommandStore().execute('Comfy.ContactSupport')
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (!systemStatsStore.systemStats) {
|
|
await systemStatsStore.refetchSystemStats()
|
|
}
|
|
|
|
try {
|
|
const [logs] = await Promise.all([api.getLogs()])
|
|
|
|
reportContent.value = generateErrorReport({
|
|
systemStats: systemStatsStore.systemStats!,
|
|
serverLogs: logs,
|
|
workflow: app.rootGraph.serialize(),
|
|
exceptionType: error.exceptionType,
|
|
exceptionMessage: error.exceptionMessage,
|
|
traceback: error.traceback,
|
|
nodeId: error.nodeId,
|
|
nodeType: error.nodeType
|
|
})
|
|
} catch (error) {
|
|
console.error('Error fetching logs:', error)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: t('g.error'),
|
|
detail: t('toastMessages.failedToFetchLogs')
|
|
})
|
|
}
|
|
})
|
|
|
|
const { copyToClipboard } = useCopyToClipboard()
|
|
const copyReportToClipboard = async () => {
|
|
await copyToClipboard(reportContent.value)
|
|
}
|
|
</script>
|