mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
publish storage for temporaries, some i18n keys, an actions menu stub, the beginnings of a component dialog with a pure vue stub, and the first core command
This commit is contained in:
@@ -65,6 +65,7 @@ import {
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
import { useWorkflowTemplateSelectorDialog } from './useWorkflowTemplateSelectorDialog'
|
||||
import { useTemplateMarketplaceDialog } from './useTemplateMarketplaceDialog'
|
||||
|
||||
import { useMaskEditorStore } from '@/stores/maskEditorStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
@@ -337,6 +338,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
}
|
||||
},
|
||||
{
|
||||
// comeback
|
||||
id: 'Comfy.BrowseTemplates',
|
||||
icon: 'pi pi-folder-open',
|
||||
label: 'Browse Templates',
|
||||
@@ -344,6 +346,15 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
useWorkflowTemplateSelectorDialog().show()
|
||||
}
|
||||
},
|
||||
{
|
||||
// comeback
|
||||
id: 'Comfy.ShowTemplateMarketplace',
|
||||
icon: 'pi pi-objects-column',
|
||||
label: 'Show Template Marketplace',
|
||||
function: () => {
|
||||
useTemplateMarketplaceDialog().show()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.ZoomIn',
|
||||
icon: 'pi pi-plus',
|
||||
|
||||
36
src/composables/useTemplateMarketplaceDialog.ts
Normal file
36
src/composables/useTemplateMarketplaceDialog.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { h } from 'vue'
|
||||
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const DIALOG_KEY = 'global-workflow-template-selector'
|
||||
// const GETTING_STARTED_CATEGORY_ID = 'basics-getting-started' // comeback when there are tabs to pick between, or remove if they're fundamentally ordered
|
||||
|
||||
export const useTemplateMarketplaceDialog = () => {
|
||||
const dialogService = useDialogService()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
function hide() {
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
}
|
||||
|
||||
function show(options?: { initialPage?: string }) {
|
||||
// comeback need a new telemetry for this
|
||||
// useTelemetry()?.trackTemplateLibraryOpened({ source })
|
||||
|
||||
dialogService.showLayoutDialog({
|
||||
key: DIALOG_KEY,
|
||||
component: () => h('div', 'Placeholder comeback'),
|
||||
// component: TemplateMarketplaceDialog,
|
||||
props: {
|
||||
onClose: hide,
|
||||
initialPage: options?.initialPage
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
hide
|
||||
}
|
||||
}
|
||||
@@ -197,6 +197,16 @@ export function useWorkflowActionsMenu(
|
||||
prependSeparator: true
|
||||
})
|
||||
|
||||
addItem({
|
||||
label: t('menuLabels.templateMarketplace'),
|
||||
icon: 'pi pi-objects-column',
|
||||
command: async () => {
|
||||
await commandStore.execute('Comfy.OpenTemplateMarketplace')
|
||||
},
|
||||
visible: isRoot && flags.templateMarketplaceEnabled,
|
||||
prependSeparator: true
|
||||
})
|
||||
|
||||
addItem({
|
||||
label: t('subgraphStore.publish'),
|
||||
icon: 'pi pi-upload',
|
||||
|
||||
@@ -1054,6 +1054,11 @@
|
||||
"enterFilenamePrompt": "Enter the filename:",
|
||||
"saveWorkflow": "Save workflow"
|
||||
},
|
||||
"templateMarketplace": {
|
||||
"publishToMarketplace": "Publish to Marketplace",
|
||||
"saveDraft": "Save Draft",
|
||||
"previewThenSave": "Preview then Save"
|
||||
},
|
||||
"subgraphStore": {
|
||||
"confirmDeleteTitle": "Delete blueprint?",
|
||||
"confirmDelete": "This action will permanently remove the blueprint from your library",
|
||||
@@ -1327,7 +1332,8 @@
|
||||
"Assets": "Assets",
|
||||
"Model Library": "Model Library",
|
||||
"Node Library": "Node Library",
|
||||
"Workflows": "Workflows"
|
||||
"Workflows": "Workflows",
|
||||
"templateMarketplace": "Template Marketplace"
|
||||
},
|
||||
"desktopMenu": {
|
||||
"reinstall": "Reinstall",
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
loadTemplateUnderway,
|
||||
saveTemplateUnderway
|
||||
} from '@/platform/workflow/templates/composables/useTemplatePublishStorage'
|
||||
import type { MarketplaceTemplate } from '@/types/templateMarketplace'
|
||||
|
||||
const STORAGE_KEY = 'Comfy.TemplateMarketplace.TemplateUnderway'
|
||||
|
||||
const storageMocks = vi.hoisted(() => ({
|
||||
getStorageValue: vi.fn(),
|
||||
setStorageValue: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/utils', () => storageMocks)
|
||||
|
||||
describe('useTemplatePublishStorage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const template: Partial<MarketplaceTemplate> = {
|
||||
title: 'My Template',
|
||||
description: 'A test template',
|
||||
difficulty: 'beginner',
|
||||
tags: ['test']
|
||||
}
|
||||
|
||||
describe('saveTemplateUnderway', () => {
|
||||
it('serialises the template and writes it to storage', () => {
|
||||
saveTemplateUnderway(template)
|
||||
|
||||
expect(storageMocks.setStorageValue).toHaveBeenCalledWith(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify(template)
|
||||
)
|
||||
})
|
||||
|
||||
it('throws TypeError when the template cannot be serialised', () => {
|
||||
const circular = { title: 'oops' } as Partial<MarketplaceTemplate>
|
||||
;(circular as Record<string, unknown>).self = circular
|
||||
|
||||
expect(() => saveTemplateUnderway(circular)).toThrow(TypeError)
|
||||
expect(storageMocks.setStorageValue).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('loadTemplateUnderway', () => {
|
||||
it('returns the parsed template when storage contains valid JSON', () => {
|
||||
storageMocks.getStorageValue.mockReturnValue(JSON.stringify(template))
|
||||
|
||||
expect(loadTemplateUnderway()).toEqual(template)
|
||||
expect(storageMocks.getStorageValue).toHaveBeenCalledWith(STORAGE_KEY)
|
||||
})
|
||||
|
||||
it('returns null when no value is stored', () => {
|
||||
storageMocks.getStorageValue.mockReturnValue(null)
|
||||
|
||||
expect(loadTemplateUnderway()).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when stored value is invalid JSON', () => {
|
||||
storageMocks.getStorageValue.mockReturnValue('not json{{{')
|
||||
|
||||
expect(loadTemplateUnderway()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('round-trip', () => {
|
||||
beforeEach(() => {
|
||||
let stored: string | null = null
|
||||
storageMocks.setStorageValue.mockImplementation(
|
||||
(_key: string, value: string) => {
|
||||
stored = value
|
||||
}
|
||||
)
|
||||
storageMocks.getStorageValue.mockImplementation(() => stored)
|
||||
})
|
||||
|
||||
it.each<{
|
||||
label: string
|
||||
input: Partial<MarketplaceTemplate>
|
||||
}>([
|
||||
{
|
||||
label: 'string values',
|
||||
input: { title: 'hello', description: '' }
|
||||
},
|
||||
{
|
||||
label: 'number values',
|
||||
input: { vramRequirement: 0 }
|
||||
},
|
||||
{
|
||||
label: 'negative and fractional numbers',
|
||||
input: { vramRequirement: -1.5 }
|
||||
},
|
||||
{
|
||||
label: 'boolean values',
|
||||
input: {
|
||||
author: { id: '1', name: 'a', isVerified: false, profileUrl: '' }
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'null values',
|
||||
input: { videoPreview: undefined, reviewFeedback: undefined }
|
||||
},
|
||||
{
|
||||
label: 'array values',
|
||||
input: { tags: ['a', 'b'], categories: [], requiredNodes: ['node1'] }
|
||||
},
|
||||
{
|
||||
label: 'nested objects',
|
||||
input: {
|
||||
author: {
|
||||
id: '1',
|
||||
name: 'Author',
|
||||
isVerified: true,
|
||||
profileUrl: '/u/1'
|
||||
},
|
||||
stats: {
|
||||
downloads: 42,
|
||||
favorites: 7,
|
||||
rating: 4.5,
|
||||
reviewCount: 3,
|
||||
weeklyTrend: -2.1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'mixed types in a single template',
|
||||
input: {
|
||||
id: '123',
|
||||
title: 'Full',
|
||||
description: 'A template with all JSON types',
|
||||
tags: ['mixed'],
|
||||
vramRequirement: 8_000_000_000,
|
||||
author: { id: '1', name: 'Test', isVerified: true, profileUrl: '' },
|
||||
gallery: [
|
||||
{
|
||||
type: 'image',
|
||||
url: 'https://example.com/img.png',
|
||||
isBefore: true
|
||||
}
|
||||
],
|
||||
requiredModels: [{ name: 'model', type: 'checkpoint', size: 0 }],
|
||||
stats: {
|
||||
downloads: 0,
|
||||
favorites: 0,
|
||||
rating: 0,
|
||||
reviewCount: 0,
|
||||
weeklyTrend: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
])('preserves $label through save/load', ({ input }) => {
|
||||
saveTemplateUnderway(input)
|
||||
const result = loadTemplateUnderway()
|
||||
|
||||
expect(result).toEqual(structuredClone(input))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Persists and retrieves the in-progress template being published to the
|
||||
* marketplace.
|
||||
*
|
||||
* Uses {@link setStorageValue} / {@link getStorageValue} so the draft
|
||||
* survives page reloads (localStorage) while also being per-tab aware
|
||||
* (sessionStorage scoped by clientId).
|
||||
*/
|
||||
import type { MarketplaceTemplate } from '@/types/templateMarketplace'
|
||||
|
||||
import { getStorageValue, setStorageValue } from '@/scripts/utils'
|
||||
|
||||
const STORAGE_KEY = 'Comfy.TemplateMarketplace.TemplateUnderway'
|
||||
|
||||
/**
|
||||
* Saves a template that is in the process of being published.
|
||||
*
|
||||
* The template is JSON-serialised and written to both localStorage and
|
||||
* the per-tab sessionStorage slot via {@link setStorageValue}.
|
||||
*
|
||||
* @param template - The current state of the template being published.
|
||||
* @throws {TypeError} If the template cannot be serialised to JSON
|
||||
* (e.g. circular references or BigInt values).
|
||||
*/
|
||||
export function saveTemplateUnderway(
|
||||
template: Partial<MarketplaceTemplate>
|
||||
): void {
|
||||
let json: string
|
||||
try {
|
||||
json = JSON.stringify(template)
|
||||
} catch (error) {
|
||||
throw new TypeError(
|
||||
`Template cannot be serialised to JSON: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
setStorageValue(STORAGE_KEY, json)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the locally stored in-progress template, if one exists.
|
||||
*
|
||||
* Reads from the per-tab sessionStorage first, falling back to
|
||||
* localStorage, via {@link getStorageValue}.
|
||||
*
|
||||
* @returns The partially completed template, or `null` when no draft
|
||||
* is stored or the stored value cannot be parsed.
|
||||
*/
|
||||
export function loadTemplateUnderway(): Partial<MarketplaceTemplate> | null {
|
||||
const raw = getStorageValue(STORAGE_KEY)
|
||||
if (!raw) return null
|
||||
|
||||
try {
|
||||
return JSON.parse(raw) as Partial<MarketplaceTemplate>
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user