feat: align publish dialog with Figma design

- Remove workflowType dropdown, type, and 6 unused i18n keys
- Add Cancel button to footer on all steps wired through onCancel→onClose
- Add border-t separator on footer
- Change Next button to variant="primary"
- Example images: balanced grid layout with computed column count
- Hide upload tile when max 8 images reached

Amp-Thread-ID: https://ampcode.com/threads/T-019ce932-92b1-779e-8c17-ab4e431edea8
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-03-13 18:22:24 -07:00
parent e8b264d2ac
commit 9f546e22e4
14 changed files with 105 additions and 156 deletions

View File

@@ -3108,12 +3108,6 @@
"workflowNamePlaceholder": "Tip: enter a descriptive name that's easy to search",
"workflowDescription": "Workflow description",
"workflowDescriptionPlaceholder": "What makes your workflow exciting and special? Be specific so people know what to expect.",
"workflowType": "Workflow type",
"workflowTypePlaceholder": "Select the type",
"workflowTypeImageGeneration": "Image generation",
"workflowTypeVideoGeneration": "Video generation",
"workflowTypeUpscaling": "Upscaling",
"workflowTypeEditing": "Editing",
"tags": "Tags",
"tagsDescription": "Select tags so people can find your workflow faster",
"tagsPlaceholder": "Enter tags that match your workflow to help people find it e.g #nanobanana, #anime or #faceswap",

View File

@@ -25,35 +25,8 @@
<label class="flex flex-col gap-2">
<span class="text-sm text-base-foreground">
{{ $t('comfyHubPublish.workflowType') }}
</span>
<Select
:model-value="workflowType"
@update:model-value="
emit('update:workflowType', $event as ComfyHubWorkflowType)
"
>
<SelectTrigger>
<SelectValue
:placeholder="$t('comfyHubPublish.workflowTypePlaceholder')"
/>
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="option in workflowTypeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</SelectItem>
</SelectContent>
</Select>
</label>
<fieldset class="flex flex-col gap-2">
<legend class="text-sm text-base-foreground">
{{ $t('comfyHubPublish.tagsDescription') }}
</legend>
</span>
<TagsInput
v-slot="{ isEmpty }"
always-editing
@@ -67,54 +40,48 @@
</TagsInputItem>
<TagsInputInput :is-empty />
</TagsInput>
<TagsInput
disabled
class="hover-within:bg-transparent bg-transparent p-0 hover:bg-transparent"
</label>
<TagsInput
disabled
class="hover-within:bg-transparent bg-transparent p-0 hover:bg-transparent"
>
<div
v-if="displayedSuggestions.length > 0"
class="flex basis-full flex-wrap gap-2"
>
<div
v-if="displayedSuggestions.length > 0"
class="flex basis-full flex-wrap gap-2"
<TagsInputItem
v-for="tag in displayedSuggestions"
:key="tag"
v-auto-animate
:value="tag"
class="cursor-pointer bg-secondary-background px-2 text-muted-foreground transition-colors select-none hover:bg-secondary-background-selected"
@click="addTag(tag)"
>
<TagsInputItem
v-for="tag in displayedSuggestions"
:key="tag"
v-auto-animate
:value="tag"
class="cursor-pointer bg-secondary-background px-2 text-muted-foreground transition-colors select-none hover:bg-secondary-background-selected"
@click="addTag(tag)"
>
<TagsInputItemText />
</TagsInputItem>
</div>
<Button
v-if="shouldShowSuggestionToggle"
variant="muted-textonly"
size="unset"
class="hover:bg-unset px-0 text-xs"
@click="showAllSuggestions = !showAllSuggestions"
>
{{
$t(
showAllSuggestions
? 'comfyHubPublish.showLessTags'
: 'comfyHubPublish.showMoreTags'
)
}}
</Button>
</TagsInput>
</fieldset>
<TagsInputItemText />
</TagsInputItem>
</div>
<Button
v-if="shouldShowSuggestionToggle"
variant="muted-textonly"
size="unset"
class="hover:bg-unset px-0 text-xs"
@click="showAllSuggestions = !showAllSuggestions"
>
{{
$t(
showAllSuggestions
? 'comfyHubPublish.showLessTags'
: 'comfyHubPublish.showMoreTags'
)
}}
</Button>
</TagsInput>
</div>
</template>
<script setup lang="ts">
import Button from '@/components/ui/button/Button.vue'
import Input from '@/components/ui/input/Input.vue'
import Select from '@/components/ui/select/Select.vue'
import SelectContent from '@/components/ui/select/SelectContent.vue'
import SelectItem from '@/components/ui/select/SelectItem.vue'
import SelectTrigger from '@/components/ui/select/SelectTrigger.vue'
import SelectValue from '@/components/ui/select/SelectValue.vue'
import TagsInput from '@/components/ui/tags-input/TagsInput.vue'
import TagsInputInput from '@/components/ui/tags-input/TagsInputInput.vue'
import TagsInputItem from '@/components/ui/tags-input/TagsInputItem.vue'
@@ -122,46 +89,21 @@ import TagsInputItemDelete from '@/components/ui/tags-input/TagsInputItemDelete.
import TagsInputItemText from '@/components/ui/tags-input/TagsInputItemText.vue'
import Textarea from '@/components/ui/textarea/Textarea.vue'
import { COMFY_HUB_TAG_OPTIONS } from '@/platform/workflow/sharing/constants/comfyHubTags'
import type { ComfyHubWorkflowType } from '@/platform/workflow/sharing/types/comfyHubTypes'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { vAutoAnimate } from '@formkit/auto-animate/vue'
const { tags, workflowType } = defineProps<{
const { tags } = defineProps<{
name: string
description: string
workflowType: ComfyHubWorkflowType | ''
tags: string[]
}>()
const emit = defineEmits<{
'update:name': [value: string]
'update:description': [value: string]
'update:workflowType': [value: ComfyHubWorkflowType | '']
'update:tags': [value: string[]]
}>()
const { t } = useI18n()
const workflowTypeOptions = computed(() => [
{
value: 'imageGeneration',
label: t('comfyHubPublish.workflowTypeImageGeneration')
},
{
value: 'videoGeneration',
label: t('comfyHubPublish.workflowTypeVideoGeneration')
},
{
value: 'upscaling',
label: t('comfyHubPublish.workflowTypeUpscaling')
},
{
value: 'editing',
label: t('comfyHubPublish.workflowTypeEditing')
}
])
const INITIAL_TAG_SUGGESTION_COUNT = 10
const showAllSuggestions = ref(false)

View File

@@ -1,6 +1,6 @@
<template>
<div class="flex min-h-0 flex-1 flex-col gap-6">
<p class="text-sm">
<div class="flex min-h-0 flex-1 flex-col">
<p class="text-sm select-none">
{{
$t('comfyHubPublish.examplesDescription', {
selected: selectedExampleIds.length,
@@ -9,13 +9,17 @@
}}
</p>
<div class="grid grid-cols-4 gap-2.5 overflow-y-auto">
<!-- Upload tile -->
<div
class="grid gap-2"
:style="{ gridTemplateColumns: `repeat(${gridColumns}, 1fr)` }"
>
<!-- Upload tile (hidden when max images reached) -->
<label
v-if="showUploadTile"
tabindex="0"
role="button"
:aria-label="$t('comfyHubPublish.uploadExampleImage')"
class="focus-visible:outline-ring flex aspect-square h-25 cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border-default text-center transition-colors hover:border-muted-foreground focus-visible:outline-2 focus-visible:outline-offset-2"
class="focus-visible:outline-ring flex aspect-square max-w-32 cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border-default text-center transition-colors hover:border-muted-foreground focus-visible:outline-2 focus-visible:outline-offset-2"
@dragenter.stop
@dragleave.stop
@dragover.prevent.stop
@@ -48,7 +52,7 @@
size="unset"
:class="
cn(
'relative h-25 cursor-pointer overflow-hidden rounded-sm p-0',
'relative aspect-square cursor-pointer overflow-hidden rounded-sm p-0',
isSelected(image.id) ? 'ring-ring ring-2' : 'ring-0'
)
"
@@ -72,7 +76,7 @@
<script setup lang="ts">
import { v4 as uuidv4 } from 'uuid'
import { ref } from 'vue'
import { computed, ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import type { ExampleImage } from '@/platform/workflow/sharing/types/comfyHubTypes'
@@ -91,6 +95,19 @@ const { exampleImages, selectedExampleIds } = defineProps<{
selectedExampleIds: string[]
}>()
const showUploadTile = computed(() => exampleImages.length < MAX_EXAMPLES)
const gridColumns = computed(() => {
const total = exampleImages.length + (showUploadTile.value ? 1 : 0)
if (total <= 5) return total
for (const cols of [5, 4, 3]) {
if (total % cols === 0) return cols
}
return [3, 4, 5].reduce((best, cols) =>
total % cols > total % best ? cols : best
)
})
const emit = defineEmits<{
'update:exampleImages': [value: ExampleImage[]]
'update:selectedExampleIds': [value: string[]]

View File

@@ -29,7 +29,6 @@ vi.mock(
formData: ref({
name: '',
description: '',
workflowType: '',
tags: [],
thumbnailType: 'image',
thumbnailFile: null,
@@ -89,7 +88,7 @@ describe('ComfyHubPublishDialog', () => {
},
ComfyHubPublishWizardContent: {
template:
'<div :data-is-publishing="$props.isPublishing"><button data-testid="require-profile" @click="$props.onRequireProfile()" /><button data-testid="gate-complete" @click="$props.onGateComplete()" /><button data-testid="gate-close" @click="$props.onGateClose()" /><button data-testid="publish" @click="$props.onPublish()" /></div>',
'<div :data-is-publishing="$props.isPublishing"><button data-testid="require-profile" @click="$props.onRequireProfile()" /><button data-testid="gate-complete" @click="$props.onGateComplete()" /><button data-testid="gate-close" @click="$props.onGateClose()" /><button data-testid="publish" @click="$props.onPublish()" /><button data-testid="cancel" @click="$props.onCancel()" /></div>',
props: [
'currentStep',
'formData',
@@ -98,6 +97,7 @@ describe('ComfyHubPublishDialog', () => {
'isPublishing',
'onGoNext',
'onGoBack',
'onCancel',
'onPublish',
'onRequireProfile',
'onGateComplete',

View File

@@ -26,6 +26,7 @@
:on-update-form-data="updateFormData"
:on-go-next="goNext"
:on-go-back="goBack"
:on-cancel="onClose"
:on-require-profile="handleRequireProfile"
:on-gate-complete="handlePublishGateComplete"
:on-gate-close="handlePublishGateClose"

View File

@@ -1,27 +1,30 @@
<template>
<footer class="flex shrink items-center justify-between py-2">
<div>
<Button v-if="!isFirstStep" size="lg" @click="$emit('back')">
{{ $t('comfyHubPublish.back') }}
</Button>
</div>
<div class="flex gap-4">
<Button v-if="!isLastStep" size="lg" @click="$emit('next')">
{{ $t('comfyHubPublish.next') }}
<i class="icon-[lucide--chevron-right] size-4" />
</Button>
<Button
v-else
variant="primary"
size="lg"
:disabled="isPublishDisabled || isPublishing"
:loading="isPublishing"
@click="$emit('publish')"
>
<i class="icon-[lucide--upload] size-4" />
{{ $t('comfyHubPublish.publishButton') }}
</Button>
</div>
<footer
class="flex shrink items-center justify-end gap-4 border-t border-border-default px-6 py-4"
>
<Button v-if="!isFirstStep" size="lg" @click="$emit('back')">
{{ $t('comfyHubPublish.back') }}
</Button>
<Button
v-if="!isLastStep"
variant="primary"
size="lg"
@click="$emit('next')"
>
{{ $t('comfyHubPublish.next') }}
<i class="icon-[lucide--chevron-right] size-4" />
</Button>
<Button
v-else
variant="primary"
size="lg"
:disabled="isPublishDisabled || isPublishing"
:loading="isPublishing"
@click="$emit('publish')"
>
<i class="icon-[lucide--upload] size-4" />
{{ $t('comfyHubPublish.publishButton') }}
</Button>
</footer>
</template>

View File

@@ -1,6 +1,6 @@
<template>
<nav class="flex flex-col gap-6 px-3 py-4">
<ol class="flex flex-col">
<ol class="flex list-none flex-col p-0">
<li
v-for="step in steps"
:key="step.name"

View File

@@ -39,7 +39,6 @@ function createDefaultFormData(): ComfyHubPublishFormData {
return {
name: 'Test Workflow',
description: '',
workflowType: '',
tags: [],
thumbnailType: 'image',
thumbnailFile: null,
@@ -54,6 +53,7 @@ describe('ComfyHubPublishWizardContent', () => {
const onPublish = vi.fn()
const onGoNext = vi.fn()
const onGoBack = vi.fn()
const onCancel = vi.fn()
const onUpdateFormData = vi.fn()
const onRequireProfile = vi.fn()
const onGateComplete = vi.fn()
@@ -80,6 +80,7 @@ describe('ComfyHubPublishWizardContent', () => {
isLastStep: true,
onGoNext,
onGoBack,
onCancel,
onUpdateFormData,
onPublish,
onRequireProfile,
@@ -116,14 +117,14 @@ describe('ComfyHubPublishWizardContent', () => {
},
ComfyHubPublishFooter: {
template:
'<div data-testid="publish-footer" :data-publish-disabled="isPublishDisabled" :data-is-publishing="isPublishing"><button data-testid="publish-btn" @click="$emit(\'publish\')" /><button data-testid="next-btn" @click="$emit(\'next\')" /><button data-testid="back-btn" @click="$emit(\'back\')" /></div>',
'<div data-testid="publish-footer" :data-publish-disabled="isPublishDisabled" :data-is-publishing="isPublishing"><button data-testid="publish-btn" @click="$emit(\'publish\')" /><button data-testid="next-btn" @click="$emit(\'next\')" /><button data-testid="back-btn" @click="$emit(\'back\')" /><button data-testid="cancel-btn" @click="$emit(\'cancel\')" /></div>',
props: [
'isFirstStep',
'isLastStep',
'isPublishDisabled',
'isPublishing'
],
emits: ['publish', 'next', 'back']
emits: ['publish', 'next', 'back', 'cancel']
}
}
}

View File

@@ -7,17 +7,15 @@
:on-close="onGateClose"
:show-close-button="false"
/>
<div v-else class="flex min-h-0 flex-1 flex-col px-6 pt-4 pb-2">
<div v-else class="flex min-h-0 flex-1 flex-col">
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
<ComfyHubDescribeStep
v-if="currentStep === 'describe'"
:name="formData.name"
:description="formData.description"
:workflow-type="formData.workflowType"
:tags="formData.tags"
@update:name="onUpdateFormData({ name: $event })"
@update:description="onUpdateFormData({ description: $event })"
@update:workflow-type="onUpdateFormData({ workflowType: $event })"
@update:tags="onUpdateFormData({ tags: $event })"
/>
<div
@@ -55,6 +53,7 @@
:is-publish-disabled
:is-publishing="isPublishInFlight"
@back="onGoBack"
@cancel="onCancel"
@next="onGoNext"
@publish="handlePublish"
/>
@@ -85,6 +84,7 @@ const {
isPublishing = false,
onGoNext,
onGoBack,
onCancel,
onUpdateFormData,
onPublish,
onRequireProfile,
@@ -98,6 +98,7 @@ const {
isPublishing?: boolean
onGoNext: () => void
onGoBack: () => void
onCancel: () => void
onUpdateFormData: (patch: Partial<ComfyHubPublishFormData>) => void
onPublish: () => Promise<void>
onRequireProfile: () => void

View File

@@ -1,9 +1,9 @@
<template>
<div class="flex min-h-0 flex-1 flex-col gap-6">
<fieldset class="flex flex-col gap-2">
<legend class="text-sm text-base-foreground">
<div class="flex flex-col gap-2">
<span class="text-sm text-base-foreground select-none">
{{ $t('comfyHubPublish.selectAThumbnail') }}
</legend>
</span>
<ToggleGroup
type="single"
:model-value="thumbnailType"
@@ -21,11 +21,11 @@
</span>
</ToggleGroupItem>
</ToggleGroup>
</fieldset>
</div>
<div class="flex min-h-0 flex-1 flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-sm text-base-foreground">
<span class="text-sm text-base-foreground select-none">
{{ uploadSectionLabel }}
</span>
<Button
@@ -80,7 +80,7 @@
:ref="(el) => (comparisonDropRefs[slot.key] = el as HTMLElement)"
:class="
cn(
'flex max-w-1/2 cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed transition-colors',
'flex aspect-square max-w-1/2 cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed transition-colors',
comparisonPreviewUrls[slot.key] ? 'self-start' : 'flex-1',
comparisonOverStates[slot.key]
? 'border-muted-foreground'
@@ -123,7 +123,7 @@
ref="singleDropRef"
:class="
cn(
'flex cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed transition-colors',
'm-auto flex aspect-square min-h-0 cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed transition-colors',
thumbnailPreviewUrl ? 'self-center p-1' : 'flex-1',
isOverSingleDrop
? 'border-muted-foreground'

View File

@@ -53,7 +53,6 @@ function createFormData(
return {
name: 'Demo workflow',
description: 'A demo workflow',
workflowType: 'imageGeneration',
tags: ['demo'],
thumbnailType: 'image',
thumbnailFile: null,

View File

@@ -35,7 +35,6 @@ describe('useComfyHubPublishWizard', () => {
it('initialises all other form fields to defaults', () => {
const { formData } = useComfyHubPublishWizard()
expect(formData.value.description).toBe('')
expect(formData.value.workflowType).toBe('')
expect(formData.value.tags).toEqual([])
expect(formData.value.thumbnailType).toBe('image')
expect(formData.value.thumbnailFile).toBeNull()

View File

@@ -18,7 +18,6 @@ function createDefaultFormData(): ComfyHubPublishFormData {
return {
name: activeWorkflow?.filename ?? '',
description: '',
workflowType: '',
tags: [],
thumbnailType: 'image',
thumbnailFile: null,

View File

@@ -2,12 +2,6 @@ export type ThumbnailType = 'image' | 'video' | 'imageComparison'
export type ComfyHubApiThumbnailType = 'image' | 'video' | 'image_comparison'
export type ComfyHubWorkflowType =
| 'imageGeneration'
| 'videoGeneration'
| 'upscaling'
| 'editing'
export interface ExampleImage {
id: string
url: string
@@ -17,7 +11,6 @@ export interface ExampleImage {
export interface ComfyHubPublishFormData {
name: string
description: string
workflowType: ComfyHubWorkflowType | ''
tags: string[]
thumbnailType: ThumbnailType
thumbnailFile: File | null