mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 06:35:10 +00:00
feat(dialog): migrate Settings dialog to Reka-UI (Phase 3)
Closes the parity gap in the Reka renderer (maximize affordance, headless layout mode, overlay class plumbing), then flips useSettingsDialog onto the Reka path. - dialog.variants: new `maximized` variant (centered+sized vs. inset-2 fullscreen) plus a DialogMaximize button wired through GlobalDialog when `maximizable` is set - GlobalDialog: when `headless: true`, skip the inner padding wrapper so layout dialogs control their own chrome; forward `overlayClass` to DialogOverlay - useSettingsDialog: renderer 'reka', size 'full', explicit contentClass matching BaseModalLayout sm (960px x 80vh), overlayClass 'p-8' when isCloud + teamWorkspacesEnabled - Drop the now-dead workspace mask special-case + orphan .settings-dialog-workspace CSS from GlobalDialog
This commit is contained in:
@@ -8,9 +8,10 @@
|
||||
@update:open="(open) => onRekaOpenChange(item.key, open)"
|
||||
>
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogOverlay :class="item.dialogComponentProps.overlayClass" />
|
||||
<DialogContent
|
||||
:size="item.dialogComponentProps.size ?? 'md'"
|
||||
:maximized="!!item.dialogComponentProps.maximized"
|
||||
:class="item.dialogComponentProps.contentClass"
|
||||
:aria-labelledby="item.key"
|
||||
@escape-key-down="
|
||||
@@ -25,28 +26,46 @@
|
||||
"
|
||||
@mousedown="() => dialogStore.riseDialog({ key: item.key })"
|
||||
>
|
||||
<DialogHeader v-if="!item.dialogComponentProps.headless">
|
||||
<component
|
||||
:is="item.headerComponent"
|
||||
v-if="item.headerComponent"
|
||||
v-bind="item.headerProps"
|
||||
:id="item.key"
|
||||
/>
|
||||
<DialogTitle v-else :id="item.key">
|
||||
{{ item.title || ' ' }}
|
||||
</DialogTitle>
|
||||
<DialogClose v-if="item.dialogComponentProps.closable !== false" />
|
||||
</DialogHeader>
|
||||
<div class="flex-1 overflow-auto px-4 py-2">
|
||||
<template v-if="item.dialogComponentProps.headless">
|
||||
<component
|
||||
:is="item.component"
|
||||
v-bind="item.contentProps"
|
||||
:maximized="item.dialogComponentProps.maximized"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter v-if="item.footerComponent">
|
||||
<component :is="item.footerComponent" v-bind="item.footerProps" />
|
||||
</DialogFooter>
|
||||
</template>
|
||||
<template v-else>
|
||||
<DialogHeader>
|
||||
<component
|
||||
:is="item.headerComponent"
|
||||
v-if="item.headerComponent"
|
||||
v-bind="item.headerProps"
|
||||
:id="item.key"
|
||||
/>
|
||||
<DialogTitle v-else :id="item.key">
|
||||
{{ item.title || ' ' }}
|
||||
</DialogTitle>
|
||||
<div class="flex items-center gap-1">
|
||||
<DialogMaximize
|
||||
v-if="item.dialogComponentProps.maximizable"
|
||||
:maximized="!!item.dialogComponentProps.maximized"
|
||||
@toggle="toggleMaximize(item)"
|
||||
/>
|
||||
<DialogClose
|
||||
v-if="item.dialogComponentProps.closable !== false"
|
||||
/>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div class="flex-1 overflow-auto px-4 py-2">
|
||||
<component
|
||||
:is="item.component"
|
||||
v-bind="item.contentProps"
|
||||
:maximized="item.dialogComponentProps.maximized"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter v-if="item.footerComponent">
|
||||
<component :is="item.footerComponent" v-bind="item.footerProps" />
|
||||
</DialogFooter>
|
||||
</template>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</Dialog>
|
||||
@@ -55,7 +74,6 @@
|
||||
v-model:visible="item.visible"
|
||||
class="global-dialog"
|
||||
v-bind="item.dialogComponentProps"
|
||||
:pt="getDialogPt(item)"
|
||||
:aria-labelledby="item.key"
|
||||
>
|
||||
<template #header>
|
||||
@@ -86,29 +104,20 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { merge } from 'es-toolkit/compat'
|
||||
import PrimeDialog from 'primevue/dialog'
|
||||
import type { DialogPassThroughOptions } from 'primevue/dialog'
|
||||
import { computed } from '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 DialogMaximize from '@/components/ui/dialog/DialogMaximize.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 { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import type { DialogComponentProps, DialogInstance } from '@/stores/dialogStore'
|
||||
import type { DialogInstance } from '@/stores/dialogStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
const teamWorkspacesEnabled = computed(
|
||||
() => isCloud && flags.teamWorkspacesEnabled
|
||||
)
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
function isRekaItem(item: DialogInstance) {
|
||||
@@ -119,20 +128,8 @@ function onRekaOpenChange(key: string, open: boolean) {
|
||||
if (!open) dialogStore.closeDialog({ key })
|
||||
}
|
||||
|
||||
function getDialogPt(item: {
|
||||
key: string
|
||||
dialogComponentProps: DialogComponentProps
|
||||
}): DialogPassThroughOptions {
|
||||
const isWorkspaceSettingsDialog =
|
||||
item.key === 'global-settings' && teamWorkspacesEnabled.value
|
||||
const basePt = item.dialogComponentProps.pt || {}
|
||||
|
||||
if (isWorkspaceSettingsDialog) {
|
||||
return merge(basePt, {
|
||||
mask: { class: 'p-8' }
|
||||
})
|
||||
}
|
||||
return basePt
|
||||
function toggleMaximize(item: DialogInstance) {
|
||||
item.dialogComponentProps.maximized = !item.dialogComponentProps.maximized
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -163,19 +160,6 @@ function getDialogPt(item: {
|
||||
}
|
||||
}
|
||||
|
||||
/* Workspace mode: wider settings dialog */
|
||||
.settings-dialog-workspace {
|
||||
width: 100%;
|
||||
max-width: 1440px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.settings-dialog-workspace .p-dialog-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.manager-dialog {
|
||||
height: 80vh;
|
||||
max-width: 1724px;
|
||||
|
||||
@@ -10,11 +10,13 @@ import { dialogContentVariants } from './dialog.variants'
|
||||
|
||||
const {
|
||||
size,
|
||||
maximized = false,
|
||||
class: customClass = '',
|
||||
...restProps
|
||||
} = defineProps<
|
||||
DialogContentProps & {
|
||||
size?: DialogContentSize
|
||||
maximized?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
>()
|
||||
@@ -26,7 +28,7 @@ const forwarded = useForwardPropsEmits(restProps, emits)
|
||||
<template>
|
||||
<DialogContent
|
||||
v-bind="forwarded"
|
||||
:class="cn(dialogContentVariants({ size }), customClass)"
|
||||
:class="cn(dialogContentVariants({ size, maximized }), customClass)"
|
||||
>
|
||||
<slot />
|
||||
</DialogContent>
|
||||
|
||||
25
src/components/ui/dialog/DialogMaximize.vue
Normal file
25
src/components/ui/dialog/DialogMaximize.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
const { maximized = false } = defineProps<{ maximized?: boolean }>()
|
||||
const emit = defineEmits<{ toggle: [] }>()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
:aria-label="maximized ? t('g.restoreDialog') : t('g.maximizeDialog')"
|
||||
size="icon"
|
||||
variant="muted-textonly"
|
||||
@click="emit('toggle')"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
maximized ? 'icon-[lucide--minimize-2]' : 'icon-[lucide--maximize-2]'
|
||||
"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
@@ -2,7 +2,7 @@ import type { VariantProps } from 'cva'
|
||||
import { cva } from 'cva'
|
||||
|
||||
export const dialogContentVariants = cva({
|
||||
base: 'fixed top-1/2 left-1/2 z-1700 flex max-h-[85vh] w-[calc(100vw-1rem)] -translate-x-1/2 -translate-y-1/2 flex-col rounded-lg border border-border-subtle bg-base-background shadow-lg outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
base: 'fixed z-1700 flex flex-col rounded-lg border border-border-subtle bg-base-background shadow-lg outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'sm:max-w-sm',
|
||||
@@ -10,10 +10,15 @@ export const dialogContentVariants = cva({
|
||||
lg: 'sm:max-w-3xl',
|
||||
xl: 'sm:max-w-5xl',
|
||||
full: 'sm:max-w-[calc(100vw-1rem)]'
|
||||
},
|
||||
maximized: {
|
||||
true: 'inset-2 top-2 left-2 size-auto max-h-none max-w-none sm:max-w-none',
|
||||
false: 'top-1/2 left-1/2 max-h-[85vh] w-[calc(100vw-1rem)] -translate-1/2'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md'
|
||||
size: 'md',
|
||||
maximized: false
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -138,6 +138,8 @@
|
||||
"hideLeftPanel": "Hide left panel",
|
||||
"showRightPanel": "Show right panel",
|
||||
"hideRightPanel": "Hide right panel",
|
||||
"maximizeDialog": "Maximize dialog",
|
||||
"restoreDialog": "Restore dialog",
|
||||
"or": "or",
|
||||
"defaultBanner": "default banner",
|
||||
"enableOrDisablePack": "Enable or disable pack",
|
||||
|
||||
92
src/platform/settings/composables/useSettingsDialog.test.ts
Normal file
92
src/platform/settings/composables/useSettingsDialog.test.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Settings dialog migration regression net: `useSettingsDialog().show()` must
|
||||
* open the Reka-renderer path with sizing that matches the previous
|
||||
* `BaseModalLayout size="sm"` (960px × 80vh). Catches accidental reverts of
|
||||
* the Phase 3 renderer flip.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const showDialog = vi.hoisted(() => vi.fn())
|
||||
const teamWorkspacesFlag = vi.hoisted(() => ({ value: false }))
|
||||
const isCloudRef = vi.hoisted(() => ({ value: false }))
|
||||
|
||||
vi.mock('@/stores/dialogStore', () => ({
|
||||
useDialogStore: () => ({ showDialog, closeDialog: vi.fn() })
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useFeatureFlags', () => ({
|
||||
useFeatureFlags: () => ({
|
||||
flags: {
|
||||
get teamWorkspacesEnabled() {
|
||||
return teamWorkspacesFlag.value
|
||||
}
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
get isCloud() {
|
||||
return isCloudRef.value
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({ t: (k: string) => k }))
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: () => ({ trackEvent: vi.fn() })
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({
|
||||
isActiveSubscription: { value: true },
|
||||
isFreeTier: { value: false },
|
||||
type: { value: 'legacy' }
|
||||
})
|
||||
}))
|
||||
|
||||
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
||||
|
||||
describe('useSettingsDialog', () => {
|
||||
beforeEach(() => {
|
||||
showDialog.mockReset()
|
||||
teamWorkspacesFlag.value = false
|
||||
isCloudRef.value = false
|
||||
})
|
||||
|
||||
it("show() opens the Reka renderer with size 'full' and 960px content sizing", () => {
|
||||
useSettingsDialog().show()
|
||||
const [args] = showDialog.mock.calls[0]
|
||||
expect(args.key).toBe('global-settings')
|
||||
expect(args.dialogComponentProps.renderer).toBe('reka')
|
||||
expect(args.dialogComponentProps.size).toBe('full')
|
||||
expect(args.dialogComponentProps.contentClass).toContain('max-w-[960px]')
|
||||
expect(args.dialogComponentProps.contentClass).toContain('h-[80vh]')
|
||||
})
|
||||
|
||||
it('show() omits overlayClass when not in workspace mode', () => {
|
||||
useSettingsDialog().show()
|
||||
const [args] = showDialog.mock.calls[0]
|
||||
expect(args.dialogComponentProps.overlayClass).toBeUndefined()
|
||||
})
|
||||
|
||||
it("show() sets overlayClass 'p-8' when isCloud && teamWorkspacesEnabled", () => {
|
||||
isCloudRef.value = true
|
||||
teamWorkspacesFlag.value = true
|
||||
|
||||
useSettingsDialog().show()
|
||||
const [args] = showDialog.mock.calls[0]
|
||||
expect(args.dialogComponentProps.overlayClass).toBe('p-8')
|
||||
})
|
||||
|
||||
it('show(panel) forwards defaultPanel to the dialog props', () => {
|
||||
useSettingsDialog().show('about')
|
||||
const [args] = showDialog.mock.calls[0]
|
||||
expect(args.props.defaultPanel).toBe('about')
|
||||
})
|
||||
|
||||
it('showAbout() opens the about panel', () => {
|
||||
useSettingsDialog().showAbout()
|
||||
const [args] = showDialog.mock.calls[0]
|
||||
expect(args.props.defaultPanel).toBe('about')
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
@@ -6,15 +8,20 @@ import type { SettingPanelType } from '@/platform/settings/types'
|
||||
|
||||
const DIALOG_KEY = 'global-settings'
|
||||
|
||||
const SETTINGS_CONTENT_CLASS =
|
||||
'w-[90vw] max-w-[960px] sm:max-w-[960px] h-[80vh] max-h-none rounded-2xl overflow-hidden'
|
||||
|
||||
export function useSettingsDialog() {
|
||||
const dialogService = useDialogService()
|
||||
const dialogStore = useDialogStore()
|
||||
const { flags } = useFeatureFlags()
|
||||
|
||||
function hide() {
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
}
|
||||
|
||||
function show(panel?: SettingPanelType, settingId?: string) {
|
||||
const isWorkspaceMode = isCloud && flags.teamWorkspacesEnabled
|
||||
dialogService.showLayoutDialog({
|
||||
key: DIALOG_KEY,
|
||||
component: SettingDialog,
|
||||
@@ -22,6 +29,12 @@ export function useSettingsDialog() {
|
||||
onClose: hide,
|
||||
...(panel ? { defaultPanel: panel } : {}),
|
||||
...(settingId ? { scrollToSettingId: settingId } : {})
|
||||
},
|
||||
dialogComponentProps: {
|
||||
renderer: 'reka',
|
||||
size: 'full',
|
||||
contentClass: SETTINGS_CONTENT_CLASS,
|
||||
overlayClass: isWorkspaceMode ? 'p-8' : undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,6 +48,11 @@ interface CustomDialogComponentProps {
|
||||
* PrimeVue path — use `pt` for that renderer.
|
||||
*/
|
||||
contentClass?: HTMLAttributes['class']
|
||||
/**
|
||||
* Class applied to the Reka-UI `DialogOverlay` element. Ignored on the
|
||||
* PrimeVue path — use `pt.mask` for that renderer.
|
||||
*/
|
||||
overlayClass?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
export type DialogComponentProps = ComponentAttrs<typeof GlobalDialog> &
|
||||
|
||||
Reference in New Issue
Block a user