Compare commits

..

4 Commits

Author SHA1 Message Date
CodeRabbit Fixer
9e8a8ee041 fix: Optimize monotone interpolator by precomputing segment constants (#9111)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 19:00:45 +01:00
AustinMroz
55b8236c8d Fix localization on share and hide entry (#9395)
A placeholder share entry was added in #9368, but the localization for
this share label was then removed in #9361.

This localization is re-added in a location that is less likely to be
overwritten and the menu item is set to hidden. I'll manually connect it
to the workflow sharing feature flag in a followup PR after that has
been merged.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9395-Fix-localization-on-share-and-hide-entry-3196d73d36508146a343f625a5327bdd)
by [Unito](https://www.unito.io)
2026-03-06 09:35:18 -08:00
Johnpaul Chiwetelu
5e17bbbf85 feat: expose litegraph internal keybindings (#9459)
## Summary

Migrate hardcoded litegraph canvas keybindings (Ctrl+A/C/V, Delete,
Backspace) into the customizable keybinding system so users can remap
them via Settings > Keybindings.

## Changes

- **What**: Register Ctrl+A (SelectAll), Ctrl+C (CopySelected), Ctrl+V
(PasteFromClipboard), Ctrl+Shift+V (PasteFromClipboardWithConnect),
Delete/Backspace (DeleteSelectedItems) as core keybindings in
`defaults.ts`. Add new `PasteFromClipboardWithConnect` command. Remove
hardcoded handling from litegraph `processKey()`, the `app.ts` Ctrl+C/V
monkey-patch, and the `keybindingService` canvas forwarding logic.

Fixes #1082
Fixes #2015

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9459-feat-expose-litegraph-internal-keybindings-31b6d73d3650819a8499fd96c8a6678f)
by [Unito](https://www.unito.io)
2026-03-06 18:30:35 +01:00
Johnpaul Chiwetelu
7cb07f9b2d fix: standardize i18n pluralization to two-part English format (#9384)
## Summary

Standardize 5 English pluralization strings from incorrect 3-part format
to proper 2-part `"singular | plural"` format.

## Changes

- **What**: Convert `nodesCount`, `asset`, `errorCount`,
`downloadsFailed`, and `exportFailed` i18n keys from redundant 3-part
pluralization (zero/one/many) to standard 2-part English format
(singular/plural)

## Review Focus

The 3-part format (`a | b | a`) was redundant for English since the
first and third parts were identical. vue-i18n only needs 2 parts for
English pluralization.

Fixes #9277

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9384-fix-standardize-i18n-pluralization-to-two-part-English-format-3196d73d365081cf97c4e7cfa310ce8e)
by [Unito](https://www.unito.io)
2026-03-06 14:53:13 +01:00
29 changed files with 480 additions and 405 deletions

View File

@@ -51,6 +51,17 @@ export function createMonotoneInterpolator(
}
}
const segCount = n - 1
const segDx = new Float64Array(segCount)
const segM0dx = new Float64Array(segCount)
const segM1dx = new Float64Array(segCount)
for (let i = 0; i < segCount; i++) {
const dx = xs[i + 1] - xs[i]
segDx[i] = dx
segM0dx[i] = slopes[i] * dx
segM1dx[i] = slopes[i + 1] * dx
}
return (x: number): number => {
if (x <= xs[0]) return ys[0]
if (x >= xs[n - 1]) return ys[n - 1]
@@ -63,7 +74,7 @@ export function createMonotoneInterpolator(
else hi = mid
}
const dx = xs[hi] - xs[lo]
const dx = segDx[lo]
if (dx === 0) return ys[lo]
const t = (x - xs[lo]) / dx
@@ -75,12 +86,7 @@ export function createMonotoneInterpolator(
const h01 = -2 * t3 + 3 * t2
const h11 = t3 - t2
return (
h00 * ys[lo] +
h10 * dx * slopes[lo] +
h01 * ys[hi] +
h11 * dx * slopes[hi]
)
return h00 * ys[lo] + h10 * segM0dx[lo] + h01 * ys[hi] + h11 * segM1dx[lo]
}
}

View File

@@ -905,6 +905,14 @@ export function useCoreCommands(): ComfyCommand[] {
app.canvas.pasteFromClipboard()
}
},
{
id: 'Comfy.Canvas.PasteFromClipboardWithConnect',
icon: 'icon-[lucide--clipboard-paste]',
label: () => t('Paste with Connect'),
function: () => {
app.canvas.pasteFromClipboard({ connectInputs: true })
}
},
{
id: 'Comfy.Canvas.SelectAll',
icon: 'icon-[lucide--lasso-select]',
@@ -919,6 +927,12 @@ export function useCoreCommands(): ComfyCommand[] {
label: 'Delete Selected Items',
versionAdded: '1.10.5',
function: () => {
if (app.canvas.selectedItems.size === 0) {
app.canvas.canvas.dispatchEvent(
new CustomEvent('litegraph:no-items-selected', { bubbles: true })
)
return
}
app.canvas.deleteSelected()
app.canvas.setDirty(true, true)
}

View File

@@ -189,11 +189,10 @@ export function useWorkflowActionsMenu(
addItem({
id: 'share',
label: t('menuLabels.Share'),
label: t('breadcrumbsMenu.share'),
icon: 'icon-[comfy--send]',
command: async () => {},
disabled: true,
visible: isRoot
visible: false
})
addItem({

View File

@@ -3791,13 +3791,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
return
}
private _noItemsSelected(): void {
const event = new CustomEvent('litegraph:no-items-selected', {
bubbles: true
})
this.canvas.dispatchEvent(event)
}
/**
* process a key event
*/
@@ -3842,31 +3835,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.node_panel?.close()
this.options_panel?.close()
if (this.node_panel || this.options_panel) block_default = true
} else if (e.keyCode === 65 && e.ctrlKey) {
// select all Control A
this.selectItems()
block_default = true
} else if (e.keyCode === 67 && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
// copy
if (this.selected_nodes) {
this.copyToClipboard()
block_default = true
}
} else if (e.keyCode === 86 && (e.metaKey || e.ctrlKey)) {
// paste
this.pasteFromClipboard({ connectInputs: e.shiftKey })
} else if (e.key === 'Delete' || e.key === 'Backspace') {
// delete or backspace
// @ts-expect-error EventTarget.localName is not in standard types
if (e.target.localName != 'input' && e.target.localName != 'textarea') {
if (this.selectedItems.size === 0) {
this._noItemsSelected()
return
}
this.deleteSelected()
block_default = true
}
}
// TODO

View File

@@ -178,7 +178,7 @@
"uploadAlreadyInProgress": "Upload already in progress",
"capture": "capture",
"nodes": "Nodes",
"nodesCount": "{count} nodes | {count} node | {count} nodes",
"nodesCount": "{count} node | {count} nodes",
"addNode": "Add a node...",
"filterBy": "Filter by:",
"filterByType": "Filter by {type}...",
@@ -222,7 +222,7 @@
"failed": "Failed",
"cancelled": "Cancelled",
"job": "Job",
"asset": "{count} assets | {count} asset | {count} assets",
"asset": "{count} asset | {count} assets",
"untitled": "Untitled",
"emDash": "—",
"enabling": "Enabling {id}",
@@ -1262,6 +1262,7 @@
"Move Selected Nodes Right": "Move Selected Nodes Right",
"Move Selected Nodes Up": "Move Selected Nodes Up",
"Paste": "Paste",
"Paste with Connect": "Paste with Connect",
"Reset View": "Reset View",
"Resize Selected Nodes": "Resize Selected Nodes",
"Select All": "Select All",
@@ -2603,7 +2604,8 @@
"deleteWorkflow": "Delete Workflow",
"deleteBlueprint": "Delete Blueprint",
"enterNewName": "Enter new name",
"missingNodesWarning": "Workflow contains unsupported nodes (highlighted red)."
"missingNodesWarning": "Workflow contains unsupported nodes (highlighted red).",
"share": "Share"
},
"shortcuts": {
"shortcuts": "Shortcuts",
@@ -3002,101 +3004,127 @@
"share": "Share",
"shareTooltip": "Share workflow"
},
"workflowSharing": {
"workflowName": "Workflow name",
"shareWorkflow": {
"shareLinkTab": "Share",
"publishToHubTab": "Publish",
"loadingTitle": "Share workflow",
"unsavedTitle": "Save workflow first",
"unsavedDescription": "You must save your workflow before sharing. Save it now to continue.",
"saveButton": "Save workflow",
"saving": "Saving...",
"workflowNameLabel": "Workflow name",
"createLinkTitle": "Share workflow",
"createLinkDescription": "When you create a link for your workflow, you will share these media items along with your workflow",
"privateAssetsDescription": "Your workflow contains private models and/or media files",
"createLinkButton": "Create a link",
"creatingLink": "Creating a link...",
"successTitle": "Workflow successfully published!",
"successDescription": "Anyone with this link can view and use this workflow. If you make changes to this workflow, you can republish to update the shared version.",
"hasChangesTitle": "Share workflow",
"hasChangesDescription": "You have made changes since this workflow was last published.",
"updateLinkButton": "Update link",
"updatingLink": "Updating link...",
"publishedOn": "Published on {date}",
"copyLink": "Copy",
"linkCopied": "Copied!",
"shareUrlLabel": "Share URL",
"loadFailed": "Failed to load shared workflow",
"saveFailedTitle": "Save failed",
"saveFailedDescription": "Failed to save workflow. Please try again.",
"mediaLabel": "{count} Media File | {count} Media Files",
"modelsLabel": "{count} Model | {count} Models",
"share": {
"shareLinkTab": "Share",
"publishToHubTab": "Publish",
"unsavedDescription": "You must save your workflow before sharing. Save it now to continue.",
"saveButton": "Save workflow",
"saving": "Saving...",
"privateAssetsDescription": "Your workflow contains private models and/or media files",
"createLinkButton": "Create a link",
"creatingLink": "Creating a link...",
"successDescription": "Anyone with this link can view and use this workflow. If you make changes to this workflow, you can republish to update the shared version.",
"hasChangesDescription": "You have made changes since this workflow was last published.",
"updateLinkButton": "Update link",
"updatingLink": "Updating link...",
"publishedOn": "Published on {date}",
"copyLink": "Copy",
"linkCopied": "Copied!",
"shareUrlLabel": "Share URL",
"loadFailed": "Failed to load shared workflow",
"saveFailedTitle": "Save failed",
"saveFailedDescription": "Failed to save workflow. Please try again.",
"checkingAssets": "Checking media visibility…",
"acknowledgeCheckbox": "I understand these media items will be published and made public",
"inLibrary": "In library"
},
"open": {
"dialogTitle": "Open shared workflow",
"copyDescription": "Opening the workflow will create a new copy in your workspace",
"nonPublicAssetsWarningLine1": "This workflow comes with non-public assets.",
"copyAssetsAndOpen": "Import assets & open workflow",
"openWorkflow": "Open workflow",
"openWithoutImporting": "Open without importing",
"importFailed": "Failed to import workflow assets",
"loadError": "Could not load this shared workflow. Please try again later."
},
"publish": {
"title": "Publish to ComfyHub",
"stepDescribe": "Describe your workflow",
"stepExamples": "Add output examples",
"stepFinish": "Finish publishing",
"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",
"tagsDescription": "Select tags so people can find your workflow faster",
"selectAThumbnail": "Select a thumbnail",
"showMoreTags": "Show more...",
"showLessTags": "Show less...",
"thumbnailImage": "Image",
"thumbnailVideo": "Video",
"thumbnailImageComparison": "Image comparison",
"uploadThumbnail": "Upload an image",
"uploadVideo": "Upload a video",
"uploadComparison": "Upload before and after",
"thumbnailPreview": "Thumbnail preview",
"uploadPromptClickToBrowse": "Click to browse or",
"uploadPromptDropImage": "drop an image here",
"uploadPromptDropVideo": "drop a video here",
"uploadComparisonBeforePrompt": "Before",
"uploadComparisonAfterPrompt": "After",
"uploadThumbnailHint": "1:1 preferred, 1080p max",
"back": "Back",
"next": "Next",
"publishButton": "Publish to ComfyHub",
"examplesDescription": "Add up to {total} additional sample images",
"uploadExampleImage": "Upload example image",
"exampleImage": "Example image {index}",
"videoPreview": "Video thumbnail preview",
"createProfileToPublish": "Create a profile to publish to ComfyHub",
"createProfileCta": "Create a profile"
},
"profile": {
"profileCreationNav": "Profile creation",
"introTitle": "Publish to the ComfyHub",
"introDescription": "Publish your workflows, build your portfolio and get discovered by millions of users",
"startPublishingButton": "Start publishing",
"createProfileTitle": "Create your ComfyHub profile",
"chooseProfilePicture": "Choose a profile picture",
"nameLabel": "Your name",
"namePlaceholder": "Enter your name here",
"usernameLabel": "Your username (required)",
"usernamePlaceholder": "@",
"descriptionLabel": "Your description",
"descriptionPlaceholder": "Tell the community about yourself...",
"createProfile": "Create profile",
"creatingProfile": "Creating profile..."
}
"checkingAssets": "Checking media visibility…",
"acknowledgeCheckbox": "I understand these media items will be published and made public",
"inLibrary": "In library",
"comfyHubTitle": "Upload to ComfyHub",
"comfyHubDescription": "ComfyHub is ComfyUI's official community hub.\nYour workflow will have a public page viewable by all.",
"comfyHubButton": "Upload to ComfyHub"
},
"openSharedWorkflow": {
"dialogTitle": "Open shared workflow",
"author": "Author:",
"copyDescription": "Opening the workflow will create a new copy in your workspace",
"nonPublicAssetsWarningLine1": "This workflow comes with non-public assets.",
"nonPublicAssetsWarningLine2": "These will be imported to your library when you open the workflow",
"copyAssetsAndOpen": "Import assets & open workflow",
"openWorkflow": "Open workflow",
"openWithoutImporting": "Open without importing",
"importFailed": "Failed to import workflow assets",
"loadError": "Could not load this shared workflow. Please try again later."
},
"comfyHubPublish": {
"title": "Publish to ComfyHub",
"stepDescribe": "Describe your workflow",
"stepExamples": "Add output examples",
"stepFinish": "Finish publishing",
"workflowName": "Workflow name",
"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",
"selectAThumbnail": "Select a thumbnail",
"showMoreTags": "Show more...",
"showLessTags": "Show less...",
"suggestedTags": "Suggested tags",
"thumbnailImage": "Image",
"thumbnailVideo": "Video",
"thumbnailImageComparison": "Image comparison",
"uploadThumbnail": "Upload an image",
"uploadVideo": "Upload a video",
"uploadComparison": "Upload before and after",
"thumbnailPreview": "Thumbnail preview",
"uploadPromptClickToBrowse": "Click to browse or",
"uploadPromptDropImage": "drop an image here",
"uploadPromptDropVideo": "drop a video here",
"uploadComparisonBeforePrompt": "Before",
"uploadComparisonAfterPrompt": "After",
"uploadThumbnailHint": "1:1 preferred, 1080p max",
"back": "Back",
"next": "Next",
"publishButton": "Publish to ComfyHub",
"examplesDescription": "Add up to {total} additional sample images",
"uploadAnImage": "Click to browse or drag an image",
"uploadExampleImage": "Upload example image",
"exampleImage": "Example image {index}",
"videoPreview": "Video thumbnail preview",
"maxExamples": "You can select up to {max} examples",
"createProfileToPublish": "Create a profile to publish to ComfyHub",
"createProfileCta": "Create a profile"
},
"comfyHubProfile": {
"checkingAccess": "Checking your publishing access...",
"profileCreationNav": "Profile creation",
"introTitle": "Publish to the ComfyHub",
"introDescription": "Publish your workflows, build your portfolio and get discovered by millions of users",
"introSubtitle": "To share your workflow on ComfyHub, let's first create your profile.",
"createProfileButton": "Create my profile",
"startPublishingButton": "Start publishing",
"modalTitle": "Create your profile on ComfyHub",
"createProfileTitle": "Create your Comfy Hub profile",
"uploadCover": "+ Upload a cover",
"uploadProfilePicture": "+ Upload a profile picture",
"chooseProfilePicture": "Choose a profile picture",
"nameLabel": "Your name",
"namePlaceholder": "Enter your name here",
"usernameLabel": "Your username (required)",
"usernamePlaceholder": "@",
"descriptionLabel": "Your description",
"descriptionPlaceholder": "Tell the community about yourself...",
"createProfile": "Create profile",
"creatingProfile": "Creating profile...",
"successTitle": "Looking good, {'@'}{username}!",
"successProfileUrl": "Your profile page is live at",
"successProfileLink": "comfy.com/p/{username}",
"successDescription": "You can now upload your workflow to your creator page",
"uploadWorkflowButton": "Upload my workflow"
},
"desktopDialogs": {
"": {
@@ -3321,7 +3349,7 @@
}
},
"errorOverlay": {
"errorCount": "{count} ERRORS | {count} ERROR | {count} ERRORS",
"errorCount": "{count} ERROR | {count} ERRORS",
"seeErrors": "See Errors"
},
"help": {
@@ -3331,7 +3359,7 @@
"progressToast": {
"importingModels": "Importing Models",
"downloadingModel": "Downloading model...",
"downloadsFailed": "{count} downloads failed | {count} download failed | {count} downloads failed",
"downloadsFailed": "{count} download failed | {count} downloads failed",
"allDownloadsCompleted": "All downloads completed",
"noImportsInQueue": "No {filter} in queue",
"failed": "Failed",
@@ -3348,7 +3376,7 @@
"exportingAssets": "Exporting Assets",
"preparingExport": "Preparing export...",
"exportError": "Export failed",
"exportFailed": "{count} export failed | {count} export failed | {count} exports failed",
"exportFailed": "{count} export failed | {count} exports failed",
"allExportsCompleted": "All exports completed",
"noExportsInQueue": "No {filter} exports in queue",
"exportStarted": "Preparing ZIP download...",

View File

@@ -208,5 +208,52 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
key: 'Escape'
},
commandId: 'Comfy.Graph.ExitSubgraph'
},
{
combo: {
ctrl: true,
key: 'a'
},
commandId: 'Comfy.Canvas.SelectAll',
targetElementId: 'graph-canvas-container'
},
{
combo: {
ctrl: true,
key: 'c'
},
commandId: 'Comfy.Canvas.CopySelected',
targetElementId: 'graph-canvas-container'
},
{
combo: {
ctrl: true,
key: 'v'
},
commandId: 'Comfy.Canvas.PasteFromClipboard',
targetElementId: 'graph-canvas-container'
},
{
combo: {
ctrl: true,
shift: true,
key: 'v'
},
commandId: 'Comfy.Canvas.PasteFromClipboardWithConnect',
targetElementId: 'graph-canvas-container'
},
{
combo: {
key: 'Delete'
},
commandId: 'Comfy.Canvas.DeleteSelectedItems',
targetElementId: 'graph-canvas-container'
},
{
combo: {
key: 'Backspace'
},
commandId: 'Comfy.Canvas.DeleteSelectedItems',
targetElementId: 'graph-canvas-container'
}
]

View File

@@ -1,22 +1,11 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
vi.mock('@/scripts/app', () => {
return {
app: {
canvas: {
processKey: vi.fn()
}
}
}
})
vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: vi.fn(() => ({
get: vi.fn(() => [])
@@ -36,13 +25,15 @@ function createTestKeyboardEvent(
ctrlKey?: boolean
altKey?: boolean
metaKey?: boolean
shiftKey?: boolean
} = {}
): KeyboardEvent {
const {
target = document.body,
ctrlKey = false,
altKey = false,
metaKey = false
metaKey = false,
shiftKey = false
} = options
const event = new KeyboardEvent('keydown', {
@@ -50,6 +41,7 @@ function createTestKeyboardEvent(
ctrlKey,
altKey,
metaKey,
shiftKey,
bubbles: true,
cancelable: true
})
@@ -60,8 +52,10 @@ function createTestKeyboardEvent(
return event
}
describe('keybindingService - Event Forwarding', () => {
describe('keybindingService - Canvas Keybindings', () => {
let keybindingService: ReturnType<typeof useKeybindingService>
let canvasContainer: HTMLDivElement
let canvasChild: HTMLCanvasElement
beforeEach(() => {
vi.clearAllMocks()
@@ -76,94 +70,156 @@ describe('keybindingService - Event Forwarding', () => {
typeof useDialogStore
>)
canvasContainer = document.createElement('div')
canvasContainer.id = 'graph-canvas-container'
canvasChild = document.createElement('canvas')
canvasContainer.appendChild(canvasChild)
document.body.appendChild(canvasContainer)
keybindingService = useKeybindingService()
keybindingService.registerCoreKeybindings()
})
it('should forward Delete key to canvas when no keybinding exists', async () => {
const event = createTestKeyboardEvent('Delete')
afterEach(() => {
canvasContainer.remove()
})
it('should execute DeleteSelectedItems for Delete key on canvas', async () => {
const event = createTestKeyboardEvent('Delete', {
target: canvasChild
})
await keybindingService.keybindHandler(event)
expect(vi.mocked(app.canvas.processKey)).toHaveBeenCalledWith(event)
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
'Comfy.Canvas.DeleteSelectedItems'
)
})
it('should forward Backspace key to canvas when no keybinding exists', async () => {
const event = createTestKeyboardEvent('Backspace')
it('should execute DeleteSelectedItems for Backspace key on canvas', async () => {
const event = createTestKeyboardEvent('Backspace', {
target: canvasChild
})
await keybindingService.keybindHandler(event)
expect(vi.mocked(app.canvas.processKey)).toHaveBeenCalledWith(event)
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
'Comfy.Canvas.DeleteSelectedItems'
)
})
it('should not forward Delete key when typing in input field', async () => {
it('should not execute DeleteSelectedItems when typing in input field', async () => {
const inputElement = document.createElement('input')
const event = createTestKeyboardEvent('Delete', { target: inputElement })
await keybindingService.keybindHandler(event)
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
})
it('should not forward Delete key when typing in textarea', async () => {
it('should not execute DeleteSelectedItems when typing in textarea', async () => {
const textareaElement = document.createElement('textarea')
const event = createTestKeyboardEvent('Delete', { target: textareaElement })
const event = createTestKeyboardEvent('Delete', {
target: textareaElement
})
await keybindingService.keybindHandler(event)
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
})
it('should not forward Delete key when canvas processKey is not available', async () => {
// Temporarily replace processKey with undefined - testing edge case
const originalProcessKey = vi.mocked(app.canvas).processKey
vi.mocked(app.canvas).processKey = undefined!
const event = createTestKeyboardEvent('Delete')
try {
await keybindingService.keybindHandler(event)
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
} finally {
// Restore processKey for other tests
vi.mocked(app.canvas).processKey = originalProcessKey
}
})
it('should not forward Delete key when canvas is not available', async () => {
const originalCanvas = vi.mocked(app).canvas
vi.mocked(app).canvas = null!
const event = createTestKeyboardEvent('Delete')
try {
await keybindingService.keybindHandler(event)
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
} finally {
// Restore canvas for other tests
vi.mocked(app).canvas = originalCanvas
}
})
it('should not forward non-canvas keys', async () => {
const event = createTestKeyboardEvent('Enter')
it('should execute SelectAll for Ctrl+A on canvas', async () => {
const event = createTestKeyboardEvent('a', {
ctrlKey: true,
target: canvasChild
})
await keybindingService.keybindHandler(event)
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
'Comfy.Canvas.SelectAll'
)
})
it('should not forward when modifier keys are pressed', async () => {
const event = createTestKeyboardEvent('Delete', { ctrlKey: true })
it('should execute CopySelected for Ctrl+C on canvas', async () => {
const event = createTestKeyboardEvent('c', {
ctrlKey: true,
target: canvasChild
})
await keybindingService.keybindHandler(event)
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
'Comfy.Canvas.CopySelected'
)
})
it('should execute PasteFromClipboard for Ctrl+V on canvas', async () => {
const event = createTestKeyboardEvent('v', {
ctrlKey: true,
target: canvasChild
})
await keybindingService.keybindHandler(event)
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
'Comfy.Canvas.PasteFromClipboard'
)
})
it('should execute PasteFromClipboardWithConnect for Ctrl+Shift+V on canvas', async () => {
const event = createTestKeyboardEvent('v', {
ctrlKey: true,
shiftKey: true,
target: canvasChild
})
await keybindingService.keybindHandler(event)
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
'Comfy.Canvas.PasteFromClipboardWithConnect'
)
})
it('should execute graph-canvas bindings by normalizing to graph-canvas-container', async () => {
const event = createTestKeyboardEvent('=', {
altKey: true,
target: canvasChild
})
await keybindingService.keybindHandler(event)
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
'Comfy.Canvas.ZoomIn'
)
})
it('should not execute graph-canvas bindings when target is outside canvas', async () => {
const outsideDiv = document.createElement('div')
document.body.appendChild(outsideDiv)
const event = createTestKeyboardEvent('=', {
altKey: true,
target: outsideDiv
})
await keybindingService.keybindHandler(event)
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
outsideDiv.remove()
})
it('should not execute canvas commands when target is outside canvas container', async () => {
const outsideDiv = document.createElement('div')
document.body.appendChild(outsideDiv)
const event = createTestKeyboardEvent('Delete', {
target: outsideDiv
})
await keybindingService.keybindHandler(event)
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
outsideDiv.remove()
})
})

View File

@@ -1,6 +1,5 @@
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
@@ -15,16 +14,6 @@ export function useKeybindingService() {
const settingStore = useSettingStore()
const dialogStore = useDialogStore()
function shouldForwardToCanvas(event: KeyboardEvent): boolean {
if (event.ctrlKey || event.altKey || event.metaKey) {
return false
}
const canvasKeys = ['Delete', 'Backspace']
return canvasKeys.includes(event.key)
}
async function keybindHandler(event: KeyboardEvent) {
const keyCombo = KeyComboImpl.fromEvent(event)
if (keyCombo.isModifier) {
@@ -44,7 +33,17 @@ export function useKeybindingService() {
}
const keybinding = keybindingStore.getKeybinding(keyCombo)
if (keybinding && keybinding.targetElementId !== 'graph-canvas') {
if (keybinding) {
const targetElementId =
keybinding.targetElementId === 'graph-canvas'
? 'graph-canvas-container'
: keybinding.targetElementId
if (targetElementId) {
const container = document.getElementById(targetElementId)
if (!container?.contains(target)) {
return
}
}
if (
event.key === 'Escape' &&
!event.ctrlKey &&
@@ -74,18 +73,6 @@ export function useKeybindingService() {
return
}
if (!keybinding && shouldForwardToCanvas(event)) {
const canvas = app.canvas
if (
canvas &&
canvas.processKey &&
typeof canvas.processKey === 'function'
) {
canvas.processKey(event)
return
}
}
if (event.ctrlKey || event.altKey || event.metaKey) {
return
}

View File

@@ -53,7 +53,7 @@
v-if="item.in_library"
class="ml-auto shrink-0 text-xs text-muted-foreground"
>
{{ $t('workflowSharing.share.inLibrary') }}
{{ $t('shareWorkflow.inLibrary') }}
</span>
</li>
</ul>

View File

@@ -20,23 +20,23 @@ const i18n = createI18n({
messages: {
en: {
g: { close: 'Close', cancel: 'Cancel' },
workflowSharing: {
openSharedWorkflow: {
dialogTitle: 'Open shared workflow',
copyDescription:
'Opening the workflow will create a new copy in your workspace',
nonPublicAssetsWarningLine1:
'This workflow comes with non-public assets.',
nonPublicAssetsWarningLine2:
'These will be added to your library when you open the workflow',
copyAssetsAndOpen: 'Copy assets & open workflow',
openWorkflow: 'Open workflow',
openWithoutImporting: 'Open without importing',
loadError:
'Could not load this shared workflow. Please try again later.'
},
shareWorkflow: {
mediaLabel: '{count} Media File | {count} Media Files',
modelsLabel: '{count} Model | {count} Models',
open: {
dialogTitle: 'Open shared workflow',
copyDescription:
'Opening the workflow will create a new copy in your workspace',
nonPublicAssetsWarningLine1:
'This workflow comes with non-public assets.',
nonPublicAssetsWarningLine2:
'These will be added to your library when you open the workflow',
copyAssetsAndOpen: 'Copy assets & open workflow',
openWorkflow: 'Open workflow',
openWithoutImporting: 'Open without importing',
loadError:
'Could not load this shared workflow. Please try again later.'
}
modelsLabel: '{count} Model | {count} Models'
}
}
}

View File

@@ -4,7 +4,7 @@
class="flex h-12 items-center justify-between gap-2 border-b border-border-default px-4"
>
<h2 class="text-sm text-base-foreground">
{{ $t('workflowSharing.open.dialogTitle') }}
{{ $t('openSharedWorkflow.dialogTitle') }}
</h2>
<Button size="icon" :aria-label="$t('g.close')" @click="onCancel">
<i class="icon-[lucide--x] size-4" />
@@ -37,7 +37,7 @@
aria-hidden="true"
/>
<p class="m-0 text-center text-sm text-muted-foreground">
{{ $t('workflowSharing.open.loadError') }}
{{ $t('openSharedWorkflow.loadError') }}
</p>
</main>
<footer
@@ -56,7 +56,7 @@
{{ workflowName }}
</h2>
<p class="m-0 text-sm text-muted-foreground">
{{ $t('workflowSharing.open.copyDescription') }}
{{ $t('openSharedWorkflow.copyDescription') }}
</p>
</div>
@@ -77,7 +77,7 @@
<span
class="m-0 flex-1 text-left text-sm text-muted-foreground"
>
{{ $t('workflowSharing.open.nonPublicAssetsWarningLine1') }}
{{ $t('openSharedWorkflow.nonPublicAssetsWarningLine1') }}
</span>
<i
:class="
@@ -111,13 +111,13 @@
size="lg"
@click="onOpenWithoutImporting(sharedWorkflow)"
>
{{ $t('workflowSharing.open.openWithoutImporting') }}
{{ $t('openSharedWorkflow.openWithoutImporting') }}
</Button>
<Button variant="primary" size="lg" @click="onConfirm(sharedWorkflow)">
{{
hasAssets
? $t('workflowSharing.open.copyAssetsAndOpen')
: $t('workflowSharing.open.openWorkflow')
? $t('openSharedWorkflow.copyAssetsAndOpen')
: $t('openSharedWorkflow.openWorkflow')
}}
</Button>
</footer>

View File

@@ -12,14 +12,12 @@ const i18n = createI18n({
locale: 'en',
messages: {
en: {
workflowSharing: {
shareWorkflow: {
privateAssetsDescription:
'Your workflow contains private models and/or media files',
mediaLabel: '{count} Media File | {count} Media Files',
modelsLabel: '{count} Model | {count} Models',
share: {
privateAssetsDescription:
'Your workflow contains private models and/or media files',
acknowledgeCheckbox: 'I understand these assets...'
}
acknowledgeCheckbox: 'I understand these assets...'
}
}
}

View File

@@ -14,7 +14,7 @@
aria-hidden="true"
/>
<span class="m-0 flex-1 text-left text-sm text-muted-foreground">
{{ $t('workflowSharing.share.privateAssetsDescription') }}
{{ $t('shareWorkflow.privateAssetsDescription') }}
</span>
<i
@@ -42,7 +42,7 @@
class="size-3.5 shrink-0 cursor-pointer accent-primary-background"
/>
<span class="text-sm text-muted-foreground">
{{ $t('workflowSharing.share.acknowledgeCheckbox') }}
{{ $t('shareWorkflow.acknowledgeCheckbox') }}
</span>
</label>
</div>

View File

@@ -3,7 +3,7 @@
<Input
readonly
:model-value="url"
:aria-label="$t('workflowSharing.share.shareUrlLabel')"
:aria-label="$t('shareWorkflow.shareUrlLabel')"
class="flex-1"
@focus="($event.target as HTMLInputElement).select()"
/>
@@ -14,9 +14,7 @@
@click="handleCopy"
>
{{
copied
? $t('workflowSharing.share.linkCopied')
: $t('workflowSharing.share.copyLink')
copied ? $t('shareWorkflow.linkCopied') : $t('shareWorkflow.copyLink')
}}
<i class="icon-[lucide--link] size-3.5" aria-hidden="true" />
</Button>

View File

@@ -102,32 +102,30 @@ const i18n = createI18n({
messages: {
en: {
g: { close: 'Close', error: 'Error' },
workflowSharing: {
workflowName: 'Workflow name',
shareWorkflow: {
unsavedDescription: 'You must save your workflow before sharing.',
shareLinkTab: 'Share',
publishToHubTab: 'Publish',
workflowNameLabel: 'Workflow name',
saving: 'Saving...',
saveButton: 'Save workflow',
createLinkButton: 'Create link',
creatingLink: 'Creating link...',
checkingAssets: 'Checking assets...',
successDescription: 'Anyone with this link...',
hasChangesDescription: 'You have made changes...',
updateLinkButton: 'Update link',
updatingLink: 'Updating link...',
publishedOn: 'Published on {date}',
mediaLabel: '{count} Media File | {count} Media Files',
modelsLabel: '{count} Model | {count} Models',
share: {
unsavedDescription: 'You must save your workflow before sharing.',
shareLinkTab: 'Share',
publishToHubTab: 'Publish',
saving: 'Saving...',
saveButton: 'Save workflow',
createLinkButton: 'Create link',
creatingLink: 'Creating link...',
checkingAssets: 'Checking assets...',
successDescription: 'Anyone with this link...',
hasChangesDescription: 'You have made changes...',
updateLinkButton: 'Update link',
updatingLink: 'Updating link...',
publishedOn: 'Published on {date}',
acknowledgeCheckbox: 'I understand these assets...',
loadFailed: 'Failed to load publish status'
},
profile: {
introTitle: 'Introducing ComfyHub',
createProfileButton: 'Create my profile',
startPublishingButton: 'Start publishing'
}
acknowledgeCheckbox: 'I understand these assets...',
loadFailed: 'Failed to load publish status'
},
comfyHubProfile: {
introTitle: 'Introducing ComfyHub',
createProfileButton: 'Create my profile',
startPublishingButton: 'Start publishing'
}
}
}

View File

@@ -15,7 +15,7 @@
:class="tabButtonClass('shareLink')"
@click="handleDialogModeChange('shareLink')"
>
{{ $t('workflowSharing.share.shareLinkTab') }}
{{ $t('shareWorkflow.shareLinkTab') }}
</Button>
<Button
id="tab-publish"
@@ -25,11 +25,11 @@
@click="handleDialogModeChange('publishToHub')"
>
<i class="icon-[lucide--globe] size-4" aria-hidden="true" />
{{ $t('workflowSharing.share.publishToHubTab') }}
{{ $t('shareWorkflow.publishToHubTab') }}
</Button>
</div>
<div v-else class="select-none">
{{ $t('workflowSharing.share.shareLinkTab') }}
{{ $t('shareWorkflow.shareLinkTab') }}
</div>
<Button size="icon" :aria-label="$t('g.close')" @click="onClose">
<i class="icon-[lucide--x] size-4" />
@@ -52,11 +52,11 @@
<template v-if="dialogState === 'unsaved'">
<p class="m-0 text-sm text-muted-foreground">
{{ $t('workflowSharing.share.unsavedDescription') }}
{{ $t('shareWorkflow.unsavedDescription') }}
</p>
<label v-if="isTemporary" class="flex flex-col gap-1">
<span class="text-sm font-medium text-muted-foreground">
{{ $t('workflowSharing.workflowName') }}
{{ $t('shareWorkflow.workflowNameLabel') }}
</span>
<Input
ref="nameInputRef"
@@ -73,8 +73,8 @@
>
{{
isSaving
? $t('workflowSharing.share.saving')
: $t('workflowSharing.share.saveButton')
? $t('shareWorkflow.saving')
: $t('shareWorkflow.saveButton')
}}
</Button>
</template>
@@ -84,13 +84,13 @@
v-if="dialogState === 'stale'"
class="m-0 text-xs text-muted-foreground"
>
{{ $t('workflowSharing.share.hasChangesDescription') }}
{{ $t('shareWorkflow.hasChangesDescription') }}
</p>
<p
v-if="isLoadingAssets"
class="m-0 text-sm text-muted-foreground italic"
>
{{ $t('workflowSharing.share.checkingAssets') }}
{{ $t('shareWorkflow.checkingAssets') }}
</p>
<ShareAssetWarningBox
v-else-if="requiresAcknowledgment"
@@ -118,12 +118,10 @@
v-if="publishResult.publishedAt"
class="m-0 text-xs text-muted-foreground"
>
{{
$t('workflowSharing.share.publishedOn', { date: formattedDate })
}}
{{ $t('shareWorkflow.publishedOn', { date: formattedDate }) }}
</p>
<p class="m-0 text-xs text-muted-foreground">
{{ $t('workflowSharing.share.successDescription') }}
{{ $t('shareWorkflow.successDescription') }}
</p>
</div>
</template>
@@ -274,21 +272,21 @@ const formattedDate = computed(() => {
const publishButtonLabel = computed(() => {
if (dialogState.value === 'stale') {
return isPublishing.value
? t('workflowSharing.share.updatingLink')
: t('workflowSharing.share.updateLinkButton')
? t('shareWorkflow.updatingLink')
: t('shareWorkflow.updateLinkButton')
}
return isPublishing.value
? t('workflowSharing.share.creatingLink')
: t('workflowSharing.share.createLinkButton')
? t('shareWorkflow.creatingLink')
: t('shareWorkflow.createLinkButton')
})
function stripWorkflowExtension(filename: string): string {
return filename.replace(/\.app\.json$/i, '').replace(/\.json$/i, '')
function stripJsonExtension(filename: string): string {
return filename.replace(/\.json$/i, '')
}
function buildWorkflowPath(directory: string, filename: string): string {
const normalizedDirectory = directory.replace(/\/+$/, '')
const normalizedFilename = appendJsonExt(stripWorkflowExtension(filename))
const normalizedFilename = appendJsonExt(stripJsonExtension(filename))
return normalizedDirectory
? `${normalizedDirectory}/${normalizedFilename}`
@@ -301,7 +299,7 @@ async function refreshDialogState() {
if (!workflow || workflow.isTemporary || workflow.isModified) {
dialogState.value = 'unsaved'
if (workflow) {
workflowName.value = stripWorkflowExtension(workflow.filename)
workflowName.value = stripJsonExtension(workflow.filename)
}
return
}
@@ -317,7 +315,7 @@ async function refreshDialogState() {
dialogState.value = 'ready'
toast.add({
severity: 'error',
summary: t('workflowSharing.share.loadFailed')
summary: t('shareWorkflow.loadFailed')
})
}
}
@@ -353,8 +351,8 @@ const { isLoading: isSaving, execute: handleSave } = useAsyncState(
console.error('Failed to save workflow:', error)
toast.add({
severity: 'error',
summary: t('workflowSharing.share.saveFailedTitle'),
detail: t('workflowSharing.share.saveFailedDescription'),
summary: t('shareWorkflow.saveFailedTitle'),
detail: t('shareWorkflow.saveFailedDescription'),
life: 5000
})
}

View File

@@ -8,20 +8,20 @@
class="flex h-16 items-center justify-between px-6"
>
<h2 class="text-base font-normal text-base-foreground">
{{ $t('workflowSharing.profile.createProfileTitle') }}
{{ $t('comfyHubProfile.createProfileTitle') }}
</h2>
<Button size="icon" :aria-label="$t('g.close')" @click="onClose">
<i class="icon-[lucide--x] size-4" />
</Button>
</header>
<h2 v-else class="px-6 pt-6 text-base font-normal text-base-foreground">
{{ $t('workflowSharing.profile.createProfileTitle') }}
{{ $t('comfyHubProfile.createProfileTitle') }}
</h2>
<div class="flex min-h-0 flex-1 flex-col gap-6 overflow-y-auto px-6 py-4">
<div class="flex flex-col gap-4">
<label for="profile-picture" class="text-sm text-muted-foreground">
{{ $t('workflowSharing.profile.chooseProfilePicture') }}
{{ $t('comfyHubProfile.chooseProfilePicture') }}
</label>
<label
class="flex size-13 cursor-pointer items-center justify-center overflow-hidden rounded-full bg-linear-to-b from-green-600/50 to-green-900"
@@ -36,7 +36,7 @@
<template v-if="profilePreviewUrl">
<img
:src="profilePreviewUrl"
:alt="$t('workflowSharing.profile.chooseProfilePicture')"
:alt="$t('comfyHubProfile.chooseProfilePicture')"
class="size-full rounded-full object-cover"
/>
</template>
@@ -51,18 +51,18 @@
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-4">
<label for="profile-name" class="text-sm text-muted-foreground">
{{ $t('workflowSharing.profile.nameLabel') }}
{{ $t('comfyHubProfile.nameLabel') }}
</label>
<Input
id="profile-name"
v-model="name"
:placeholder="$t('workflowSharing.profile.namePlaceholder')"
:placeholder="$t('comfyHubProfile.namePlaceholder')"
/>
</div>
<div class="flex flex-col gap-2">
<label for="profile-username" class="text-sm text-muted-foreground">
{{ $t('workflowSharing.profile.usernameLabel') }}
{{ $t('comfyHubProfile.usernameLabel') }}
</label>
<div class="relative">
<span
@@ -84,12 +84,12 @@
for="profile-description"
class="text-sm text-muted-foreground"
>
{{ $t('workflowSharing.profile.descriptionLabel') }}
{{ $t('comfyHubProfile.descriptionLabel') }}
</label>
<Textarea
id="profile-description"
v-model="description"
:placeholder="$t('workflowSharing.profile.descriptionPlaceholder')"
:placeholder="$t('comfyHubProfile.descriptionPlaceholder')"
class="h-24 resize-none rounded-lg border-none bg-secondary-background p-4 text-sm shadow-none"
/>
</div>
@@ -110,8 +110,8 @@
>
{{
isCreating
? $t('workflowSharing.profile.creatingProfile')
: $t('workflowSharing.profile.createProfile')
? $t('comfyHubProfile.creatingProfile')
: $t('comfyHubProfile.createProfile')
}}
</Button>
</footer>

View File

@@ -15,10 +15,10 @@
<!-- Content -->
<section class="flex flex-col items-center gap-4 px-4 pt-4 pb-6">
<h2 class="m-0 text-base font-semibold text-base-foreground">
{{ $t('workflowSharing.profile.introTitle') }}
{{ $t('comfyHubProfile.introTitle') }}
</h2>
<p class="m-0 text-center text-sm text-muted-foreground">
{{ $t('workflowSharing.profile.introDescription') }}
{{ $t('comfyHubProfile.introDescription') }}
</p>
<Button
variant="primary"
@@ -26,7 +26,7 @@
class="mt-2 w-full"
@click="onCreateProfile"
>
{{ $t('workflowSharing.profile.startPublishingButton') }}
{{ $t('comfyHubProfile.startPublishingButton') }}
</Button>
</section>
</div>

View File

@@ -2,24 +2,22 @@
<div class="flex min-h-0 flex-1 flex-col gap-6 px-6 py-4">
<label class="flex flex-col gap-2">
<span class="text-sm text-base-foreground">
{{ $t('workflowSharing.workflowName') }}
{{ $t('comfyHubPublish.workflowName') }}
</span>
<Input
:model-value="name"
:placeholder="$t('workflowSharing.publish.workflowNamePlaceholder')"
:placeholder="$t('comfyHubPublish.workflowNamePlaceholder')"
@update:model-value="$emit('update:name', String($event))"
/>
</label>
<label class="flex flex-col gap-2">
<span class="text-sm text-base-foreground">
{{ $t('workflowSharing.publish.workflowDescription') }}
{{ $t('comfyHubPublish.workflowDescription') }}
</span>
<Textarea
:model-value="description"
:placeholder="
$t('workflowSharing.publish.workflowDescriptionPlaceholder')
"
:placeholder="$t('comfyHubPublish.workflowDescriptionPlaceholder')"
rows="5"
@update:model-value="$emit('update:description', String($event))"
/>
@@ -27,7 +25,7 @@
<label class="flex flex-col gap-2">
<span class="text-sm text-base-foreground">
{{ $t('workflowSharing.publish.workflowType') }}
{{ $t('comfyHubPublish.workflowType') }}
</span>
<Select
:model-value="workflowType"
@@ -37,7 +35,7 @@
>
<SelectTrigger>
<SelectValue
:placeholder="$t('workflowSharing.publish.workflowTypePlaceholder')"
:placeholder="$t('comfyHubPublish.workflowTypePlaceholder')"
/>
</SelectTrigger>
<SelectContent>
@@ -54,7 +52,7 @@
<fieldset class="flex flex-col gap-2">
<legend class="text-sm text-base-foreground">
{{ $t('workflowSharing.publish.tagsDescription') }}
{{ $t('comfyHubPublish.tagsDescription') }}
</legend>
<TagsInput
v-slot="{ isEmpty }"
@@ -99,8 +97,8 @@
{{
$t(
showAllSuggestions
? 'workflowSharing.publish.showLessTags'
: 'workflowSharing.publish.showMoreTags'
? 'comfyHubPublish.showLessTags'
: 'comfyHubPublish.showMoreTags'
)
}}
</Button>
@@ -148,19 +146,19 @@ const { t } = useI18n()
const workflowTypeOptions = computed(() => [
{
value: 'imageGeneration',
label: t('workflowSharing.publish.workflowTypeImageGeneration')
label: t('comfyHubPublish.workflowTypeImageGeneration')
},
{
value: 'videoGeneration',
label: t('workflowSharing.publish.workflowTypeVideoGeneration')
label: t('comfyHubPublish.workflowTypeVideoGeneration')
},
{
value: 'upscaling',
label: t('workflowSharing.publish.workflowTypeUpscaling')
label: t('comfyHubPublish.workflowTypeUpscaling')
},
{
value: 'editing',
label: t('workflowSharing.publish.workflowTypeEditing')
label: t('comfyHubPublish.workflowTypeEditing')
}
])

View File

@@ -2,7 +2,7 @@
<div class="flex min-h-0 flex-1 flex-col gap-6">
<p class="text-sm">
{{
$t('workflowSharing.publish.examplesDescription', {
$t('comfyHubPublish.examplesDescription', {
selected: selectedExampleIds.length,
total: MAX_EXAMPLES
})
@@ -14,7 +14,7 @@
<label
tabindex="0"
role="button"
:aria-label="$t('workflowSharing.publish.uploadExampleImage')"
: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"
@dragenter.stop
@dragleave.stop
@@ -36,7 +36,7 @@
aria-hidden="true"
/>
<span class="sr-only">{{
$t('workflowSharing.publish.uploadExampleImage')
$t('comfyHubPublish.uploadExampleImage')
}}</span>
</label>
@@ -56,9 +56,7 @@
>
<img
:src="image.url"
:alt="
$t('workflowSharing.publish.exampleImage', { index: index + 1 })
"
:alt="$t('comfyHubPublish.exampleImage', { index: index + 1 })"
class="size-full object-cover"
/>
<div

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex min-h-0 flex-1 flex-col gap-4 px-6 py-4">
<p class="text-sm text-base-foreground">
{{ $t('workflowSharing.publish.createProfileToPublish') }}
{{ $t('comfyHubPublish.createProfileToPublish') }}
</p>
<Button
@@ -17,7 +17,7 @@
</div>
<span class="inline-flex items-center gap-1 text-sm text-base-foreground">
<i class="icon-[lucide--plus] size-4" />
{{ $t('workflowSharing.publish.createProfileCta') }}
{{ $t('comfyHubPublish.createProfileCta') }}
</span>
</Button>
</div>

View File

@@ -1,13 +1,13 @@
<template>
<BaseModalLayout
:content-title="$t('workflowSharing.publish.title')"
:content-title="$t('comfyHubPublish.title')"
content-padding="none"
left-panel-width="16.5rem"
size="md"
>
<template #leftPanelHeaderTitle>
<h2 class="flex-1 text-base font-semibold select-none">
{{ $t('workflowSharing.publish.title') }}
{{ $t('comfyHubPublish.title') }}
</h2>
</template>

View File

@@ -2,12 +2,12 @@
<footer class="flex shrink items-center justify-between py-2">
<div>
<Button v-if="!isFirstStep" size="lg" @click="$emit('back')">
{{ $t('workflowSharing.publish.back') }}
{{ $t('comfyHubPublish.back') }}
</Button>
</div>
<div class="flex gap-4">
<Button v-if="!isLastStep" size="lg" @click="$emit('next')">
{{ $t('workflowSharing.publish.next') }}
{{ $t('comfyHubPublish.next') }}
<i class="icon-[lucide--chevron-right] size-4" />
</Button>
<Button
@@ -18,7 +18,7 @@
@click="$emit('publish')"
>
<i class="icon-[lucide--upload] size-4" />
{{ $t('workflowSharing.publish.publishButton') }}
{{ $t('comfyHubPublish.publishButton') }}
</Button>
</div>
</footer>

View File

@@ -54,7 +54,7 @@
class="flex h-10 w-full items-center rounded-lg bg-secondary-background-selected pl-11 select-none"
>
<span class="truncate text-sm text-base-foreground">
{{ $t('workflowSharing.profile.profileCreationNav') }}
{{ $t('comfyHubProfile.profileCreationNav') }}
</span>
</div>
</li>
@@ -89,18 +89,14 @@ const steps = [
{
name: 'describe' as const,
number: 1,
label: t('workflowSharing.publish.stepDescribe')
label: t('comfyHubPublish.stepDescribe')
},
{
name: 'examples' as const,
number: 2,
label: t('workflowSharing.publish.stepExamples')
label: t('comfyHubPublish.stepExamples')
},
{
name: 'finish' as const,
number: 3,
label: t('workflowSharing.publish.stepFinish')
}
{ name: 'finish' as const, number: 3, label: t('comfyHubPublish.stepFinish') }
]
const isProfileCreationFlow = computed(() => currentStep === 'profileCreation')

View File

@@ -2,7 +2,7 @@
<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">
{{ $t('workflowSharing.publish.selectAThumbnail') }}
{{ $t('comfyHubPublish.selectAThumbnail') }}
</legend>
<ToggleGroup
type="single"
@@ -49,12 +49,12 @@
>
<img
:src="comparisonPreviewUrls.after!"
:alt="$t('workflowSharing.publish.uploadComparisonAfterPrompt')"
:alt="$t('comfyHubPublish.uploadComparisonAfterPrompt')"
class="size-full object-contain"
/>
<img
:src="comparisonPreviewUrls.before!"
:alt="$t('workflowSharing.publish.uploadComparisonBeforePrompt')"
:alt="$t('comfyHubPublish.uploadComparisonBeforePrompt')"
class="absolute inset-0 size-full object-contain"
:style="{
clipPath: `inset(0 ${100 - previewSliderPosition}% 0 0)`
@@ -110,7 +110,7 @@
{{ slot.label }}
</span>
<span class="text-xs text-muted-foreground">
{{ $t('workflowSharing.publish.uploadThumbnailHint') }}
{{ $t('comfyHubPublish.uploadThumbnailHint') }}
</span>
</template>
</label>
@@ -149,7 +149,7 @@
<video
v-if="isVideoFile"
:src="thumbnailPreviewUrl"
:aria-label="$t('workflowSharing.publish.videoPreview')"
:aria-label="$t('comfyHubPublish.videoPreview')"
class="max-h-full max-w-full object-contain"
muted
loop
@@ -158,19 +158,19 @@
<img
v-else
:src="thumbnailPreviewUrl"
:alt="$t('workflowSharing.publish.thumbnailPreview')"
:alt="$t('comfyHubPublish.thumbnailPreview')"
class="max-h-full max-w-full object-contain"
/>
</template>
<template v-else>
<span class="text-sm text-muted-foreground">
{{ $t('workflowSharing.publish.uploadPromptClickToBrowse') }}
{{ $t('comfyHubPublish.uploadPromptClickToBrowse') }}
</span>
<span class="text-sm text-muted-foreground">
{{ uploadDropText }}
</span>
<span class="text-xs text-muted-foreground">
{{ $t('workflowSharing.publish.uploadThumbnailHint') }}
{{ $t('comfyHubPublish.uploadThumbnailHint') }}
</span>
</template>
</label>
@@ -223,31 +223,31 @@ function handleThumbnailTypeChange(value: unknown) {
}
const uploadSectionLabel = computed(() => {
if (thumbnailType === 'video') return t('workflowSharing.publish.uploadVideo')
if (thumbnailType === 'video') return t('comfyHubPublish.uploadVideo')
if (thumbnailType === 'imageComparison') {
return t('workflowSharing.publish.uploadComparison')
return t('comfyHubPublish.uploadComparison')
}
return t('workflowSharing.publish.uploadThumbnail')
return t('comfyHubPublish.uploadThumbnail')
})
const uploadDropText = computed(() =>
thumbnailType === 'video'
? t('workflowSharing.publish.uploadPromptDropVideo')
: t('workflowSharing.publish.uploadPromptDropImage')
? t('comfyHubPublish.uploadPromptDropVideo')
: t('comfyHubPublish.uploadPromptDropImage')
)
const thumbnailOptions = [
{
value: 'image' as const,
label: t('workflowSharing.publish.thumbnailImage')
label: t('comfyHubPublish.thumbnailImage')
},
{
value: 'video' as const,
label: t('workflowSharing.publish.thumbnailVideo')
label: t('comfyHubPublish.thumbnailVideo')
},
{
value: 'imageComparison' as const,
label: t('workflowSharing.publish.thumbnailImageComparison')
label: t('comfyHubPublish.thumbnailImageComparison')
}
]
@@ -335,11 +335,11 @@ type ComparisonSlot = 'before' | 'after'
const comparisonSlots = [
{
key: 'before' as const,
label: t('workflowSharing.publish.uploadComparisonBeforePrompt')
label: t('comfyHubPublish.uploadComparisonBeforePrompt')
},
{
key: 'after' as const,
label: t('workflowSharing.publish.uploadComparisonAfterPrompt')
label: t('comfyHubPublish.uploadComparisonAfterPrompt')
}
]

View File

@@ -17,12 +17,12 @@ export function useAssetSections(items: () => AssetInfo[]) {
const allSections: AssetSection[] = [
{
id: 'media',
labelKey: 'workflowSharing.mediaLabel',
labelKey: 'shareWorkflow.mediaLabel',
items: media
},
{
id: 'models',
labelKey: 'workflowSharing.modelsLabel',
labelKey: 'shareWorkflow.modelsLabel',
items: models
}
]

View File

@@ -62,13 +62,13 @@ vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: vi.fn((key: string) => {
if (key === 'g.error') return 'Error'
if (key === 'workflowSharing.share.loadFailed') {
if (key === 'shareWorkflow.loadFailed') {
return 'Failed to load shared workflow'
}
if (key === 'workflowSharing.open.dialogTitle') {
if (key === 'openSharedWorkflow.dialogTitle') {
return 'Open shared workflow'
}
if (key === 'workflowSharing.open.importFailed') {
if (key === 'openSharedWorkflow.importFailed') {
return 'Failed to import workflow assets'
}
return key

View File

@@ -118,7 +118,7 @@ export function useSharedWorkflowUrlLoader() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('workflowSharing.share.loadFailed'),
detail: t('shareWorkflow.loadFailed'),
life: 3000
})
cleanupUrlParams()
@@ -135,7 +135,7 @@ export function useSharedWorkflowUrlLoader() {
}
const { payload } = result
const workflowName = payload.name || t('workflowSharing.open.dialogTitle')
const workflowName = payload.name || t('openSharedWorkflow.dialogTitle')
const nonOwnedAssets = payload.assets.filter((a) => !a.in_library)
try {
@@ -148,7 +148,7 @@ export function useSharedWorkflowUrlLoader() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('workflowSharing.share.loadFailed'),
detail: t('shareWorkflow.loadFailed'),
life: 5000
})
return 'failed'
@@ -167,7 +167,7 @@ export function useSharedWorkflowUrlLoader() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('workflowSharing.open.importFailed')
detail: t('openSharedWorkflow.importFailed')
})
cleanupUrlParams()
clearPreservedQuery(SHARE_NAMESPACE)

View File

@@ -676,20 +676,6 @@ export class ComfyApp {
e.stopImmediatePropagation()
return
}
// Ctrl+C Copy
if (e.key === 'c' && (e.metaKey || e.ctrlKey)) {
return
}
// Ctrl+V Paste
if (
(e.key === 'v' || e.key == 'V') &&
(e.metaKey || e.ctrlKey) &&
!e.shiftKey
) {
return
}
}
// Fall through to Litegraph defaults