Files
ComfyUI_frontend/src/components/dialog/content/ErrorDialogContent.vue
Dante d6f632477f feat(dialog): migrate Error / NodeSearchBox / SecretForm / VideoHelp / Customization to Reka-UI (Phase 2) (#12109)
## 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
2026-05-15 02:05:46 +00:00

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>