mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 21:38:52 +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
170 lines
5.2 KiB
Vue
170 lines
5.2 KiB
Vue
<template>
|
|
<Dialog v-model:open="visible" :modal="false">
|
|
<DialogPortal>
|
|
<DialogOverlay />
|
|
<DialogContent
|
|
size="md"
|
|
:aria-labelledby="titleId"
|
|
@pointer-down-outside="onPointerDownOutside"
|
|
>
|
|
<DialogHeader>
|
|
<DialogTitle :id="titleId">
|
|
{{ $t('g.customizeFolder') }}
|
|
</DialogTitle>
|
|
<DialogClose />
|
|
</DialogHeader>
|
|
<div class="flex flex-col gap-4 px-4 py-2">
|
|
<div class="flex flex-col gap-2">
|
|
<label for="customization-icon" class="text-sm font-medium">
|
|
{{ $t('g.icon') }}
|
|
</label>
|
|
<SelectButton
|
|
id="customization-icon"
|
|
v-model="selectedIcon"
|
|
:options="iconOptions"
|
|
option-label="name"
|
|
data-key="value"
|
|
>
|
|
<template #option="slotProps">
|
|
<i
|
|
:class="['pi', slotProps.option.value, 'mr-2']"
|
|
:style="{ color: finalColor }"
|
|
/>
|
|
</template>
|
|
</SelectButton>
|
|
</div>
|
|
<hr class="border-t border-border-subtle" />
|
|
<div class="flex flex-col gap-2">
|
|
<label for="customization-color" class="text-sm font-medium">
|
|
{{ $t('g.color') }}
|
|
</label>
|
|
<ColorCustomizationSelector
|
|
id="customization-color"
|
|
v-model="finalColor"
|
|
:color-options="colorOptions"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="textonly" @click="resetCustomization">
|
|
<i class="pi pi-refresh" />
|
|
{{ $t('g.reset') }}
|
|
</Button>
|
|
<Button autofocus @click="confirmCustomization">
|
|
<i class="pi pi-check" />
|
|
{{ $t('g.confirm') }}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</DialogPortal>
|
|
</Dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import SelectButton from 'primevue/selectbutton'
|
|
import { ref, useId, watch } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import ColorCustomizationSelector from '@/components/common/ColorCustomizationSelector.vue'
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import Dialog from '@/components/ui/dialog/Dialog.vue'
|
|
import DialogClose from '@/components/ui/dialog/DialogClose.vue'
|
|
import DialogContent from '@/components/ui/dialog/DialogContent.vue'
|
|
import DialogFooter from '@/components/ui/dialog/DialogFooter.vue'
|
|
import DialogHeader from '@/components/ui/dialog/DialogHeader.vue'
|
|
import DialogOverlay from '@/components/ui/dialog/DialogOverlay.vue'
|
|
import DialogPortal from '@/components/ui/dialog/DialogPortal.vue'
|
|
import DialogTitle from '@/components/ui/dialog/DialogTitle.vue'
|
|
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
|
|
|
const { t } = useI18n()
|
|
|
|
const { initialIcon, initialColor } = defineProps<{
|
|
initialIcon?: string
|
|
initialColor?: string
|
|
}>()
|
|
|
|
const visible = defineModel<boolean>({ default: false })
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'confirm', icon: string, color: string): void
|
|
}>()
|
|
|
|
const titleId = useId()
|
|
|
|
// PrimeVue ColorPicker overlay teleports to body. Reka treats clicks on it as
|
|
// outside and would dismiss the dialog mid-color-pick. Treat any PrimeVue
|
|
// overlay click as inside.
|
|
const PRIMEVUE_OVERLAY_SELECTORS =
|
|
'.p-colorpicker-panel, .p-overlay, .p-overlay-mask'
|
|
|
|
function onPointerDownOutside(
|
|
event: CustomEvent<{ originalEvent: PointerEvent }>
|
|
) {
|
|
const target = event.detail.originalEvent.target
|
|
if (target instanceof Element && target.closest(PRIMEVUE_OVERLAY_SELECTORS)) {
|
|
event.preventDefault()
|
|
}
|
|
}
|
|
|
|
const nodeBookmarkStore = useNodeBookmarkStore()
|
|
|
|
const iconOptions = [
|
|
{ name: t('icon.bookmark'), value: nodeBookmarkStore.defaultBookmarkIcon },
|
|
{ name: t('icon.folder'), value: 'pi-folder' },
|
|
{ name: t('icon.star'), value: 'pi-star' },
|
|
{ name: t('icon.heart'), value: 'pi-heart' },
|
|
{ name: t('icon.file'), value: 'pi-file' },
|
|
{ name: t('icon.inbox'), value: 'pi-inbox' },
|
|
{ name: t('icon.box'), value: 'pi-box' },
|
|
{ name: t('icon.briefcase'), value: 'pi-briefcase' }
|
|
]
|
|
|
|
const colorOptions = [
|
|
{ name: t('color.default'), value: nodeBookmarkStore.defaultBookmarkColor },
|
|
{ name: t('color.blue'), value: '#007bff' },
|
|
{ name: t('color.green'), value: '#28a745' },
|
|
{ name: t('color.red'), value: '#dc3545' },
|
|
{ name: t('color.pink'), value: '#e83e8c' },
|
|
{ name: t('color.yellow'), value: '#ffc107' }
|
|
]
|
|
|
|
const defaultIcon = iconOptions.find(
|
|
(option) => option.value === nodeBookmarkStore.defaultBookmarkIcon
|
|
)
|
|
|
|
const selectedIcon = ref(defaultIcon ?? iconOptions[0])
|
|
const finalColor = ref(initialColor || nodeBookmarkStore.defaultBookmarkColor)
|
|
|
|
const resetCustomization = () => {
|
|
selectedIcon.value =
|
|
iconOptions.find((option) => option.value === initialIcon) ?? iconOptions[0]
|
|
finalColor.value = initialColor || nodeBookmarkStore.defaultBookmarkColor
|
|
}
|
|
|
|
const confirmCustomization = () => {
|
|
emit('confirm', selectedIcon.value.value, finalColor.value)
|
|
visible.value = false
|
|
}
|
|
|
|
watch(
|
|
visible,
|
|
(newValue) => {
|
|
if (newValue) {
|
|
resetCustomization()
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.p-selectbutton .p-button {
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.p-selectbutton .p-button .pi {
|
|
font-size: 1.5rem;
|
|
}
|
|
</style>
|