Compare commits
10 Commits
pysssss/no
...
refactor/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3831868e11 | ||
|
|
ed7ceab571 | ||
|
|
e40995fb6c | ||
|
|
908a3ea418 | ||
|
|
0ae4b78cbc | ||
|
|
5f14276159 | ||
|
|
df81c66725 | ||
|
|
c5ad6cf1aa | ||
|
|
fe1fae4de1 | ||
|
|
38aad02fb9 |
@@ -91,6 +91,12 @@ export class CanvasHelper {
|
||||
await this.page.mouse.move(10, 10)
|
||||
}
|
||||
|
||||
async isReadOnly(): Promise<boolean> {
|
||||
return this.page.evaluate(() => {
|
||||
return window.app!.canvas.state.readOnly
|
||||
})
|
||||
}
|
||||
|
||||
async getScale(): Promise<number> {
|
||||
return this.page.evaluate(() => {
|
||||
return window.app!.canvas.ds.scale
|
||||
|
||||
@@ -38,16 +38,13 @@ const customColorPalettes = {
|
||||
CLEAR_BACKGROUND_COLOR: '#222222',
|
||||
NODE_TITLE_COLOR: 'rgba(255,255,255,.75)',
|
||||
NODE_SELECTED_TITLE_COLOR: '#FFF',
|
||||
NODE_TEXT_SIZE: 14,
|
||||
NODE_TEXT_COLOR: '#b8b8b8',
|
||||
NODE_SUBTEXT_SIZE: 12,
|
||||
NODE_DEFAULT_COLOR: 'rgba(0,0,0,.8)',
|
||||
NODE_DEFAULT_BGCOLOR: 'rgba(22,22,22,.8)',
|
||||
NODE_DEFAULT_BOXCOLOR: 'rgba(255,255,255,.75)',
|
||||
NODE_DEFAULT_SHAPE: 'box',
|
||||
NODE_BOX_OUTLINE_COLOR: '#236692',
|
||||
DEFAULT_SHADOW_COLOR: 'rgba(0,0,0,0)',
|
||||
DEFAULT_GROUP_FONT: 24,
|
||||
WIDGET_BGCOLOR: '#242424',
|
||||
WIDGET_OUTLINE_COLOR: '#333',
|
||||
WIDGET_TEXT_COLOR: '#a3a3a8',
|
||||
@@ -102,16 +99,13 @@ const customColorPalettes = {
|
||||
CLEAR_BACKGROUND_COLOR: '#000',
|
||||
NODE_TITLE_COLOR: 'rgba(255,255,255,.75)',
|
||||
NODE_SELECTED_TITLE_COLOR: '#FFF',
|
||||
NODE_TEXT_SIZE: 14,
|
||||
NODE_TEXT_COLOR: '#b8b8b8',
|
||||
NODE_SUBTEXT_SIZE: 12,
|
||||
NODE_DEFAULT_COLOR: 'rgba(0,0,0,.8)',
|
||||
NODE_DEFAULT_BGCOLOR: 'rgba(22,22,22,.8)',
|
||||
NODE_DEFAULT_BOXCOLOR: 'rgba(255,255,255,.75)',
|
||||
NODE_DEFAULT_SHAPE: 'box',
|
||||
NODE_BOX_OUTLINE_COLOR: '#236692',
|
||||
DEFAULT_SHADOW_COLOR: 'rgba(0,0,0,0)',
|
||||
DEFAULT_GROUP_FONT: 24,
|
||||
WIDGET_BGCOLOR: '#242424',
|
||||
WIDGET_OUTLINE_COLOR: '#333',
|
||||
WIDGET_TEXT_COLOR: '#a3a3a8',
|
||||
|
||||
318
browser_tests/tests/defaultKeybindings.spec.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
async function pressKeyAndExpectRequest(
|
||||
comfyPage: ComfyPage,
|
||||
key: string,
|
||||
urlPattern: string,
|
||||
method: string = 'POST'
|
||||
) {
|
||||
const requestPromise = comfyPage.page.waitForRequest(
|
||||
(req) => req.url().includes(urlPattern) && req.method() === method,
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
await comfyPage.page.keyboard.press(key)
|
||||
return requestPromise
|
||||
}
|
||||
|
||||
test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
|
||||
test.describe('Sidebar Toggle Shortcuts', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.canvas.click({ position: { x: 400, y: 400 } })
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
const sidebarTabs = [
|
||||
{ key: 'KeyW', tabId: 'workflows', label: 'workflows' },
|
||||
{ key: 'KeyN', tabId: 'node-library', label: 'node library' },
|
||||
{ key: 'KeyM', tabId: 'model-library', label: 'model library' },
|
||||
{ key: 'KeyA', tabId: 'assets', label: 'assets' }
|
||||
] as const
|
||||
|
||||
for (const { key, tabId, label } of sidebarTabs) {
|
||||
test(`'${key}' toggles ${label} sidebar`, async ({ comfyPage }) => {
|
||||
const selectedButton = comfyPage.page.locator(
|
||||
`.${tabId}-tab-button.side-bar-button-selected`
|
||||
)
|
||||
|
||||
await expect(selectedButton).not.toBeVisible()
|
||||
|
||||
await comfyPage.canvas.press(key)
|
||||
await comfyPage.nextFrame()
|
||||
await expect(selectedButton).toBeVisible()
|
||||
|
||||
await comfyPage.canvas.press(key)
|
||||
await comfyPage.nextFrame()
|
||||
await expect(selectedButton).not.toBeVisible()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('Canvas View Controls', () => {
|
||||
test("'Alt+=' zooms in", async ({ comfyPage }) => {
|
||||
const initialScale = await comfyPage.canvasOps.getScale()
|
||||
|
||||
await comfyPage.canvas.press('Alt+Equal')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const newScale = await comfyPage.canvasOps.getScale()
|
||||
expect(newScale).toBeGreaterThan(initialScale)
|
||||
})
|
||||
|
||||
test("'Alt+-' zooms out", async ({ comfyPage }) => {
|
||||
const initialScale = await comfyPage.canvasOps.getScale()
|
||||
|
||||
await comfyPage.canvas.press('Alt+Minus')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const newScale = await comfyPage.canvasOps.getScale()
|
||||
expect(newScale).toBeLessThan(initialScale)
|
||||
})
|
||||
|
||||
test("'.' fits view to nodes", async ({ comfyPage }) => {
|
||||
// Set scale very small so fit-view will zoom back to fit nodes
|
||||
await comfyPage.canvasOps.setScale(0.1)
|
||||
const scaleBefore = await comfyPage.canvasOps.getScale()
|
||||
expect(scaleBefore).toBeCloseTo(0.1, 1)
|
||||
|
||||
// Click canvas to ensure focus is within graph-canvas-container
|
||||
await comfyPage.canvas.click({ position: { x: 400, y: 400 } })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.canvas.press('Period')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const scaleAfter = await comfyPage.canvasOps.getScale()
|
||||
expect(scaleAfter).toBeGreaterThan(scaleBefore)
|
||||
})
|
||||
|
||||
test("'h' locks canvas", async ({ comfyPage }) => {
|
||||
expect(await comfyPage.canvasOps.isReadOnly()).toBe(false)
|
||||
|
||||
await comfyPage.canvas.press('KeyH')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await comfyPage.canvasOps.isReadOnly()).toBe(true)
|
||||
})
|
||||
|
||||
test("'v' unlocks canvas", async ({ comfyPage }) => {
|
||||
// Lock first
|
||||
await comfyPage.command.executeCommand('Comfy.Canvas.Lock')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await comfyPage.canvasOps.isReadOnly()).toBe(true)
|
||||
|
||||
await comfyPage.canvas.press('KeyV')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await comfyPage.canvasOps.isReadOnly()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Node State Toggles', () => {
|
||||
test("'Alt+c' collapses and expands selected nodes", async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodes = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
|
||||
expect(nodes.length).toBeGreaterThan(0)
|
||||
const node = nodes[0]
|
||||
|
||||
await node.click('title')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await node.isCollapsed()).toBe(false)
|
||||
|
||||
await comfyPage.canvas.press('Alt+KeyC')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await node.isCollapsed()).toBe(true)
|
||||
|
||||
await comfyPage.canvas.press('Alt+KeyC')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await node.isCollapsed()).toBe(false)
|
||||
})
|
||||
|
||||
test("'Ctrl+m' mutes and unmutes selected nodes", async ({ comfyPage }) => {
|
||||
const nodes = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
|
||||
expect(nodes.length).toBeGreaterThan(0)
|
||||
const node = nodes[0]
|
||||
|
||||
await node.click('title')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Normal mode is ALWAYS (0)
|
||||
const getMode = () =>
|
||||
comfyPage.page.evaluate((nodeId) => {
|
||||
return window.app!.canvas.graph!.getNodeById(nodeId)!.mode
|
||||
}, node.id)
|
||||
|
||||
expect(await getMode()).toBe(0)
|
||||
|
||||
await comfyPage.canvas.press('Control+KeyM')
|
||||
await comfyPage.nextFrame()
|
||||
// NEVER (2) = muted
|
||||
expect(await getMode()).toBe(2)
|
||||
|
||||
await comfyPage.canvas.press('Control+KeyM')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await getMode()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Mode and Panel Toggles', () => {
|
||||
test("'Alt+m' toggles app mode", async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
|
||||
// Set up linearData so app mode has something to show
|
||||
await comfyPage.appMode.enterAppModeWithInputs([])
|
||||
await expect(comfyPage.appMode.linearWidgets).toBeVisible()
|
||||
|
||||
// Toggle off with Alt+m
|
||||
await comfyPage.page.keyboard.press('Alt+KeyM')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.appMode.linearWidgets).not.toBeVisible()
|
||||
|
||||
// Toggle on again
|
||||
await comfyPage.page.keyboard.press('Alt+KeyM')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.appMode.linearWidgets).toBeVisible()
|
||||
})
|
||||
|
||||
test("'Alt+Shift+m' toggles minimap", async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.settings.setSetting('Comfy.Minimap.Visible', true)
|
||||
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
|
||||
await comfyPage.workflow.loadWorkflow('default')
|
||||
|
||||
const minimap = comfyPage.page.locator('.litegraph-minimap')
|
||||
await expect(minimap).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Alt+Shift+KeyM')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(minimap).not.toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Alt+Shift+KeyM')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(minimap).toBeVisible()
|
||||
})
|
||||
|
||||
test("'Ctrl+`' toggles terminal/logs panel", async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
|
||||
await expect(comfyPage.bottomPanel.root).not.toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Control+Backquote')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.bottomPanel.root).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Control+Backquote')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.bottomPanel.root).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Queue and Execution', () => {
|
||||
test("'Ctrl+Enter' queues prompt", async ({ comfyPage }) => {
|
||||
const request = await pressKeyAndExpectRequest(
|
||||
comfyPage,
|
||||
'Control+Enter',
|
||||
'/prompt',
|
||||
'POST'
|
||||
)
|
||||
expect(request.url()).toContain('/prompt')
|
||||
})
|
||||
|
||||
test("'Ctrl+Shift+Enter' queues prompt to front", async ({ comfyPage }) => {
|
||||
const request = await pressKeyAndExpectRequest(
|
||||
comfyPage,
|
||||
'Control+Shift+Enter',
|
||||
'/prompt',
|
||||
'POST'
|
||||
)
|
||||
const body = request.postDataJSON()
|
||||
expect(body.front).toBe(true)
|
||||
})
|
||||
|
||||
test("'Ctrl+Alt+Enter' interrupts execution", async ({ comfyPage }) => {
|
||||
const request = await pressKeyAndExpectRequest(
|
||||
comfyPage,
|
||||
'Control+Alt+Enter',
|
||||
'/interrupt',
|
||||
'POST'
|
||||
)
|
||||
expect(request.url()).toContain('/interrupt')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('File Operations', () => {
|
||||
test("'Ctrl+s' triggers save workflow", async ({ comfyPage }) => {
|
||||
// On a new unsaved workflow, Ctrl+s triggers Save As dialog.
|
||||
// The dialog appearing proves the keybinding was intercepted by the app.
|
||||
await comfyPage.page.keyboard.press('Control+s')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// The Save As dialog should appear (p-dialog overlay)
|
||||
const dialogOverlay = comfyPage.page.locator('.p-dialog-mask')
|
||||
await expect(dialogOverlay).toBeVisible({ timeout: 3000 })
|
||||
|
||||
// Dismiss the dialog
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
test("'Ctrl+o' triggers open workflow", async ({ comfyPage }) => {
|
||||
// Ctrl+o calls app.ui.loadFile() which clicks a hidden file input.
|
||||
// Detect the file input click via an event listener.
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window.TestCommand = false
|
||||
const fileInputs =
|
||||
document.querySelectorAll<HTMLInputElement>('input[type="file"]')
|
||||
for (const input of fileInputs) {
|
||||
input.addEventListener('click', () => {
|
||||
window.TestCommand = true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
await comfyPage.page.keyboard.press('Control+o')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Graph Operations', () => {
|
||||
test("'Ctrl+Shift+e' converts selection to subgraph", async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
expect(initialCount).toBeGreaterThan(1)
|
||||
|
||||
// Select all nodes
|
||||
await comfyPage.canvas.press('Control+a')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.page.keyboard.press('Control+Shift+KeyE')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// After conversion, node count should decrease
|
||||
// (multiple nodes replaced by single subgraph node)
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getGraphNodesCount(), {
|
||||
timeout: 5000
|
||||
})
|
||||
.toBeLessThan(initialCount)
|
||||
})
|
||||
|
||||
test("'r' refreshes node definitions", async ({ comfyPage }) => {
|
||||
const request = await pressKeyAndExpectRequest(
|
||||
comfyPage,
|
||||
'KeyR',
|
||||
'/object_info',
|
||||
'GET'
|
||||
)
|
||||
expect(request.url()).toContain('/object_info')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
132
browser_tests/tests/subgraphSlotAlignment.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { readFileSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
interface SlotMeasurement {
|
||||
key: string
|
||||
offsetX: number
|
||||
offsetY: number
|
||||
}
|
||||
|
||||
interface NodeSlotData {
|
||||
nodeId: string
|
||||
isSubgraph: boolean
|
||||
nodeW: number
|
||||
nodeH: number
|
||||
slots: SlotMeasurement[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression test for link misalignment on SubgraphNodes when loading
|
||||
* workflows with workflowRendererVersion: "LG".
|
||||
*
|
||||
* Root cause: ensureCorrectLayoutScale scales nodes by 1.2x for LG workflows,
|
||||
* and fitView() updates lgCanvas.ds immediately. The Vue TransformPane's CSS
|
||||
* transform lags by a frame, causing clientPosToCanvasPos to produce wrong
|
||||
* slot offsets. The fix uses DOM-relative measurement instead.
|
||||
*/
|
||||
test.describe(
|
||||
'Subgraph slot alignment after LG layout scale',
|
||||
{ tag: ['@subgraph', '@canvas'] },
|
||||
() => {
|
||||
test('slot positions stay within node bounds after loading LG workflow', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const SLOT_BOUNDS_MARGIN = 20
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
|
||||
const workflowPath = resolve(
|
||||
import.meta.dirname,
|
||||
'../assets/subgraphs/basic-subgraph.json'
|
||||
)
|
||||
const workflow = JSON.parse(
|
||||
readFileSync(workflowPath, 'utf-8')
|
||||
) as ComfyWorkflowJSON
|
||||
workflow.extra = {
|
||||
...workflow.extra,
|
||||
workflowRendererVersion: 'LG'
|
||||
}
|
||||
|
||||
await comfyPage.page.evaluate(
|
||||
(wf) =>
|
||||
window.app!.loadGraphData(wf as ComfyWorkflowJSON, true, true, null, {
|
||||
openSource: 'template'
|
||||
}),
|
||||
workflow
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Wait for slot elements to appear in DOM
|
||||
await comfyPage.page.locator('[data-slot-key]').first().waitFor()
|
||||
|
||||
const result: NodeSlotData[] = await comfyPage.page.evaluate(() => {
|
||||
const nodes = window.app!.graph._nodes
|
||||
const slotData: NodeSlotData[] = []
|
||||
|
||||
for (const node of nodes) {
|
||||
const nodeId = String(node.id)
|
||||
const nodeEl = document.querySelector(
|
||||
`[data-node-id="${nodeId}"]`
|
||||
) as HTMLElement | null
|
||||
if (!nodeEl) continue
|
||||
|
||||
const slotEls = nodeEl.querySelectorAll('[data-slot-key]')
|
||||
if (slotEls.length === 0) continue
|
||||
|
||||
const slots: SlotMeasurement[] = []
|
||||
|
||||
const nodeRect = nodeEl.getBoundingClientRect()
|
||||
for (const slotEl of slotEls) {
|
||||
const slotRect = slotEl.getBoundingClientRect()
|
||||
const slotKey = (slotEl as HTMLElement).dataset.slotKey ?? 'unknown'
|
||||
slots.push({
|
||||
key: slotKey,
|
||||
offsetX: slotRect.left + slotRect.width / 2 - nodeRect.left,
|
||||
offsetY: slotRect.top + slotRect.height / 2 - nodeRect.top
|
||||
})
|
||||
}
|
||||
|
||||
slotData.push({
|
||||
nodeId,
|
||||
isSubgraph: !!node.isSubgraphNode?.(),
|
||||
nodeW: nodeRect.width,
|
||||
nodeH: nodeRect.height,
|
||||
slots
|
||||
})
|
||||
}
|
||||
|
||||
return slotData
|
||||
})
|
||||
|
||||
const subgraphNodes = result.filter((n) => n.isSubgraph)
|
||||
expect(subgraphNodes.length).toBeGreaterThan(0)
|
||||
|
||||
for (const node of subgraphNodes) {
|
||||
for (const slot of node.slots) {
|
||||
expect(
|
||||
slot.offsetX,
|
||||
`Slot ${slot.key} on node ${node.nodeId}: X offset ${slot.offsetX} outside node width ${node.nodeW}`
|
||||
).toBeGreaterThanOrEqual(-SLOT_BOUNDS_MARGIN)
|
||||
expect(
|
||||
slot.offsetX,
|
||||
`Slot ${slot.key} on node ${node.nodeId}: X offset ${slot.offsetX} outside node width ${node.nodeW}`
|
||||
).toBeLessThanOrEqual(node.nodeW + SLOT_BOUNDS_MARGIN)
|
||||
|
||||
expect(
|
||||
slot.offsetY,
|
||||
`Slot ${slot.key} on node ${node.nodeId}: Y offset ${slot.offsetY} outside node height ${node.nodeH}`
|
||||
).toBeGreaterThanOrEqual(-SLOT_BOUNDS_MARGIN)
|
||||
expect(
|
||||
slot.offsetY,
|
||||
`Slot ${slot.key} on node ${node.nodeId}: Y offset ${slot.offsetY} outside node height ${node.nodeH}`
|
||||
).toBeLessThanOrEqual(node.nodeH + SLOT_BOUNDS_MARGIN)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
@@ -46,4 +46,16 @@ test.describe('Vue Multiline String Widget', () => {
|
||||
|
||||
await expect(textarea).toHaveValue('Keep me around')
|
||||
})
|
||||
test('should use native context menu when focused', async ({ comfyPage }) => {
|
||||
const textarea = getFirstMultilineStringWidget(comfyPage)
|
||||
const vueContextMenu = comfyPage.page.locator('.p-contextmenu')
|
||||
|
||||
await textarea.focus()
|
||||
await textarea.click({ button: 'right' })
|
||||
await expect(vueContextMenu).not.toBeVisible()
|
||||
await textarea.blur()
|
||||
|
||||
await textarea.click({ button: 'right' })
|
||||
await expect(vueContextMenu).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
21
src/App.vue
@@ -9,13 +9,10 @@ import { captureException } from '@sentry/vue'
|
||||
import BlockUI from 'primevue/blockui'
|
||||
import { computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import config from '@/config'
|
||||
import { isDesktop } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
@@ -23,7 +20,6 @@ import { parsePreloadError } from '@/utils/preloadErrorUtil'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
||||
|
||||
const { t } = useI18n()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
app.extensionManager = useWorkspaceStore()
|
||||
|
||||
@@ -98,12 +94,17 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
}
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.preloadErrorTitle'),
|
||||
detail: t('g.preloadError'),
|
||||
life: 10000
|
||||
})
|
||||
// Disabled: Third-party custom node extensions frequently trigger this toast
|
||||
// (e.g., bare "vue" imports, wrong relative paths to scripts/app.js, missing
|
||||
// core dependencies). These are plugin bugs, not ComfyUI core failures, but
|
||||
// the generic error message alarms users and offers no actionable guidance.
|
||||
// The console.error above still logs the details for developers to debug.
|
||||
// useToastStore().add({
|
||||
// severity: 'error',
|
||||
// summary: t('g.preloadErrorTitle'),
|
||||
// detail: t('g.preloadError'),
|
||||
// life: 10000
|
||||
// })
|
||||
})
|
||||
|
||||
// Capture resource load failures (CSS, scripts) in non-localhost distributions
|
||||
|
||||
@@ -34,9 +34,7 @@
|
||||
"CLEAR_BACKGROUND_COLOR": "#2b2f38",
|
||||
"NODE_TITLE_COLOR": "#b2b7bd",
|
||||
"NODE_SELECTED_TITLE_COLOR": "#FFF",
|
||||
"NODE_TEXT_SIZE": 14,
|
||||
"NODE_TEXT_COLOR": "#AAA",
|
||||
"NODE_SUBTEXT_SIZE": 12,
|
||||
"NODE_DEFAULT_COLOR": "#2b2f38",
|
||||
"NODE_DEFAULT_BGCOLOR": "#242730",
|
||||
"NODE_DEFAULT_BOXCOLOR": "#6e7581",
|
||||
@@ -45,7 +43,6 @@
|
||||
"NODE_BYPASS_BGCOLOR": "#FF00FF",
|
||||
"NODE_ERROR_COLOUR": "#E00",
|
||||
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
|
||||
"DEFAULT_GROUP_FONT": 22,
|
||||
"WIDGET_BGCOLOR": "#2b2f38",
|
||||
"WIDGET_OUTLINE_COLOR": "#6e7581",
|
||||
"WIDGET_TEXT_COLOR": "#DDD",
|
||||
|
||||
@@ -25,10 +25,8 @@
|
||||
"CLEAR_BACKGROUND_COLOR": "#141414",
|
||||
"NODE_TITLE_COLOR": "#999",
|
||||
"NODE_SELECTED_TITLE_COLOR": "#FFF",
|
||||
"NODE_TEXT_SIZE": 14,
|
||||
"NODE_TEXT_COLOR": "#AAA",
|
||||
"NODE_TEXT_HIGHLIGHT_COLOR": "#FFF",
|
||||
"NODE_SUBTEXT_SIZE": 12,
|
||||
"NODE_DEFAULT_COLOR": "#333",
|
||||
"NODE_DEFAULT_BGCOLOR": "#353535",
|
||||
"NODE_DEFAULT_BOXCOLOR": "#666",
|
||||
@@ -37,7 +35,6 @@
|
||||
"NODE_BYPASS_BGCOLOR": "#FF00FF",
|
||||
"NODE_ERROR_COLOUR": "#E00",
|
||||
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
|
||||
"DEFAULT_GROUP_FONT": 24,
|
||||
"WIDGET_BGCOLOR": "#222",
|
||||
"WIDGET_OUTLINE_COLOR": "#666",
|
||||
"WIDGET_TEXT_COLOR": "#DDD",
|
||||
|
||||
@@ -34,9 +34,7 @@
|
||||
"CLEAR_BACKGROUND_COLOR": "#040506",
|
||||
"NODE_TITLE_COLOR": "#999",
|
||||
"NODE_SELECTED_TITLE_COLOR": "#e5eaf0",
|
||||
"NODE_TEXT_SIZE": 14,
|
||||
"NODE_TEXT_COLOR": "#bcc2c8",
|
||||
"NODE_SUBTEXT_SIZE": 12,
|
||||
"NODE_DEFAULT_COLOR": "#161b22",
|
||||
"NODE_DEFAULT_BGCOLOR": "#13171d",
|
||||
"NODE_DEFAULT_BOXCOLOR": "#30363d",
|
||||
@@ -45,7 +43,6 @@
|
||||
"NODE_BYPASS_BGCOLOR": "#FF00FF",
|
||||
"NODE_ERROR_COLOUR": "#E00",
|
||||
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
|
||||
"DEFAULT_GROUP_FONT": 24,
|
||||
"WIDGET_BGCOLOR": "#161b22",
|
||||
"WIDGET_OUTLINE_COLOR": "#30363d",
|
||||
"WIDGET_TEXT_COLOR": "#bcc2c8",
|
||||
|
||||
@@ -26,10 +26,8 @@
|
||||
"CLEAR_BACKGROUND_COLOR": "lightgray",
|
||||
"NODE_TITLE_COLOR": "#222",
|
||||
"NODE_SELECTED_TITLE_COLOR": "#000",
|
||||
"NODE_TEXT_SIZE": 14,
|
||||
"NODE_TEXT_COLOR": "#444",
|
||||
"NODE_TEXT_HIGHLIGHT_COLOR": "#1e293b",
|
||||
"NODE_SUBTEXT_SIZE": 12,
|
||||
"NODE_DEFAULT_COLOR": "#F7F7F7",
|
||||
"NODE_DEFAULT_BGCOLOR": "#F5F5F5",
|
||||
"NODE_DEFAULT_BOXCOLOR": "#CCC",
|
||||
@@ -38,7 +36,6 @@
|
||||
"NODE_BYPASS_BGCOLOR": "#FF00FF",
|
||||
"NODE_ERROR_COLOUR": "#E00",
|
||||
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.1)",
|
||||
"DEFAULT_GROUP_FONT": 24,
|
||||
"WIDGET_BGCOLOR": "#D4D4D4",
|
||||
"WIDGET_OUTLINE_COLOR": "#999",
|
||||
"WIDGET_TEXT_COLOR": "#222",
|
||||
|
||||
@@ -34,9 +34,7 @@
|
||||
"CLEAR_BACKGROUND_COLOR": "#212732",
|
||||
"NODE_TITLE_COLOR": "#999",
|
||||
"NODE_SELECTED_TITLE_COLOR": "#e5eaf0",
|
||||
"NODE_TEXT_SIZE": 14,
|
||||
"NODE_TEXT_COLOR": "#bcc2c8",
|
||||
"NODE_SUBTEXT_SIZE": 12,
|
||||
"NODE_DEFAULT_COLOR": "#2e3440",
|
||||
"NODE_DEFAULT_BGCOLOR": "#161b22",
|
||||
"NODE_DEFAULT_BOXCOLOR": "#545d70",
|
||||
@@ -45,7 +43,6 @@
|
||||
"NODE_BYPASS_BGCOLOR": "#FF00FF",
|
||||
"NODE_ERROR_COLOUR": "#E00",
|
||||
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
|
||||
"DEFAULT_GROUP_FONT": 24,
|
||||
"WIDGET_BGCOLOR": "#2e3440",
|
||||
"WIDGET_OUTLINE_COLOR": "#545d70",
|
||||
"WIDGET_TEXT_COLOR": "#bcc2c8",
|
||||
|
||||
@@ -19,9 +19,7 @@
|
||||
"litegraph_base": {
|
||||
"NODE_TITLE_COLOR": "#fdf6e3",
|
||||
"NODE_SELECTED_TITLE_COLOR": "#A9D400",
|
||||
"NODE_TEXT_SIZE": 14,
|
||||
"NODE_TEXT_COLOR": "#657b83",
|
||||
"NODE_SUBTEXT_SIZE": 12,
|
||||
"NODE_DEFAULT_COLOR": "#094656",
|
||||
"NODE_DEFAULT_BGCOLOR": "#073642",
|
||||
"NODE_DEFAULT_BOXCOLOR": "#839496",
|
||||
@@ -30,7 +28,6 @@
|
||||
"NODE_BYPASS_BGCOLOR": "#FF00FF",
|
||||
"NODE_ERROR_COLOUR": "#E00",
|
||||
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
|
||||
"DEFAULT_GROUP_FONT": 24,
|
||||
"WIDGET_BGCOLOR": "#002b36",
|
||||
"WIDGET_OUTLINE_COLOR": "#839496",
|
||||
"WIDGET_TEXT_COLOR": "#fdf6e3",
|
||||
|
||||
@@ -71,8 +71,8 @@ vi.mock('@/workbench/extensions/manager/composables/useManagerState', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
currentUser: null,
|
||||
loading: false
|
||||
}))
|
||||
|
||||
@@ -32,8 +32,8 @@ const mockBalance = vi.hoisted(() => ({
|
||||
|
||||
const mockIsFetchingBalance = vi.hoisted(() => ({ value: false }))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
balance: mockBalance.value,
|
||||
isFetchingBalance: mockIsFetchingBalance.value
|
||||
}))
|
||||
|
||||
@@ -30,14 +30,14 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { formatCreditsFromCents } from '@/base/credits/comfyCredits'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const { textClass, showCreditsOnly } = defineProps<{
|
||||
textClass?: string
|
||||
showCreditsOnly?: boolean
|
||||
}>()
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const balanceLoading = computed(() => authStore.isFetchingBalance)
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { getComfyPlatformBaseUrl } from '@/config/comfyApi'
|
||||
import {
|
||||
configValueOrDefault,
|
||||
@@ -167,7 +167,7 @@ const { onSuccess } = defineProps<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const isSecureContext = window.isSecureContext
|
||||
const isSignIn = ref(true)
|
||||
const showApiKeyForm = ref(false)
|
||||
|
||||
@@ -156,7 +156,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { creditsToUsd, usdToCredits } from '@/base/credits/comfyCredits'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import FormattedNumberStepper from '@/components/ui/stepper/FormattedNumberStepper.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
@@ -171,7 +171,7 @@ const { isInsufficientCredits = false } = defineProps<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const dialogStore = useDialogStore()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const telemetry = useTelemetry()
|
||||
|
||||
@@ -21,10 +21,10 @@ import { ref } from 'vue'
|
||||
|
||||
import PasswordFields from '@/components/dialog/content/signin/PasswordFields.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { updatePasswordSchema } from '@/schemas/signInSchema'
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const loading = ref(false)
|
||||
|
||||
const { onSuccess } = defineProps<{
|
||||
|
||||
@@ -116,12 +116,12 @@ import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import UsageLogsTable from '@/components/dialog/content/setting/UsageLogsTable.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
interface CreditHistoryItemData {
|
||||
@@ -133,8 +133,8 @@ interface CreditHistoryItemData {
|
||||
|
||||
const { buildDocsUrl, docsPaths } = useExternalLink()
|
||||
const dialogService = useDialogService()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authStore = useAuthStore()
|
||||
const authActions = useAuthActions()
|
||||
const commandStore = useCommandStore()
|
||||
const telemetry = useTelemetry()
|
||||
const { isActiveSubscription } = useBillingContext()
|
||||
|
||||
@@ -18,8 +18,8 @@ import ApiKeyForm from './ApiKeyForm.vue'
|
||||
const mockStoreApiKey = vi.fn()
|
||||
const mockLoading = vi.fn(() => false)
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
loading: mockLoading()
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -100,9 +100,9 @@ import {
|
||||
} from '@/platform/remoteConfig/remoteConfig'
|
||||
import { apiKeySchema } from '@/schemas/signInSchema'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const apiKeyStore = useApiKeyAuthStore()
|
||||
const loading = computed(() => authStore.loading)
|
||||
const comfyPlatformBaseUrl = computed(() =>
|
||||
|
||||
@@ -35,15 +35,15 @@ vi.mock('firebase/auth', () => ({
|
||||
|
||||
// Mock the auth composables and stores
|
||||
const mockSendPasswordReset = vi.fn()
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: vi.fn(() => ({
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: vi.fn(() => ({
|
||||
sendPasswordReset: mockSendPasswordReset
|
||||
}))
|
||||
}))
|
||||
|
||||
let mockLoading = false
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
get loading() {
|
||||
return mockLoading
|
||||
}
|
||||
|
||||
@@ -88,14 +88,14 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { signInSchema } from '@/schemas/signInSchema'
|
||||
import type { SignInData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const firebaseAuthActions = useFirebaseAuthActions()
|
||||
const authStore = useAuthStore()
|
||||
const authActions = useAuthActions()
|
||||
const loading = computed(() => authStore.loading)
|
||||
const toast = useToast()
|
||||
|
||||
@@ -127,6 +127,6 @@ const handleForgotPassword = async (
|
||||
document.getElementById(emailInputId)?.focus?.()
|
||||
return
|
||||
}
|
||||
await firebaseAuthActions.sendPasswordReset(email)
|
||||
await authActions.sendPasswordReset(email)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -54,12 +54,12 @@ import { useI18n } from 'vue-i18n'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { signUpSchema } from '@/schemas/signInSchema'
|
||||
import type { SignUpData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
import PasswordFields from './PasswordFields.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -84,7 +84,9 @@ watch(
|
||||
pos: group.pos,
|
||||
size: [group.size[0], group.titleHeight]
|
||||
})
|
||||
inputFontStyle.value = { fontSize: `${group.font_size * scale}px` }
|
||||
inputFontStyle.value = {
|
||||
fontSize: `${LiteGraph.GROUP_TEXT_SIZE * scale}px`
|
||||
}
|
||||
} else if (target instanceof LGraphNode) {
|
||||
const node = target
|
||||
const [x, y] = node.getBounding()
|
||||
|
||||
@@ -61,10 +61,10 @@ vi.mock('@/composables/auth/useCurrentUser', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock the useFirebaseAuthActions composable
|
||||
// Mock the useAuthActions composable
|
||||
const mockLogout = vi.fn()
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: vi.fn(() => ({
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: vi.fn(() => ({
|
||||
fetchBalance: vi.fn().mockResolvedValue(undefined),
|
||||
logout: mockLogout
|
||||
}))
|
||||
@@ -77,7 +77,7 @@ vi.mock('@/services/dialogService', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock the firebaseAuthStore with hoisted state for per-test manipulation
|
||||
// Mock the authStore with hoisted state for per-test manipulation
|
||||
const mockAuthStoreState = vi.hoisted(() => ({
|
||||
balance: {
|
||||
amount_micros: 100_000,
|
||||
@@ -91,8 +91,8 @@ const mockAuthStoreState = vi.hoisted(() => ({
|
||||
isFetchingBalance: false
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
getAuthHeader: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ Authorization: 'Bearer mock-token' }),
|
||||
|
||||
@@ -159,7 +159,7 @@ import { formatCreditsFromCents } from '@/base/credits/comfyCredits'
|
||||
import UserAvatar from '@/components/common/UserAvatar.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
@@ -168,7 +168,7 @@ import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
@@ -178,8 +178,8 @@ const { buildDocsUrl, docsPaths } = useExternalLink()
|
||||
|
||||
const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
|
||||
useCurrentUser()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authActions = useAuthActions()
|
||||
const authStore = useAuthStore()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const dialogService = useDialogService()
|
||||
const {
|
||||
|
||||
@@ -11,8 +11,8 @@ import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import type { BillingPortalTargetTier } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { BillingPortalTargetTier } from '@/stores/authStore'
|
||||
import { usdToMicros } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
@@ -20,8 +20,8 @@ import { usdToMicros } from '@/utils/formatUtil'
|
||||
* All actions are wrapped with error handling.
|
||||
* @returns {Object} - Object containing all Firebase Auth actions
|
||||
*/
|
||||
export const useFirebaseAuthActions = () => {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
export const useAuthActions = () => {
|
||||
const authStore = useAuthStore()
|
||||
const toastStore = useToastStore()
|
||||
const { wrapWithErrorHandlingAsync, toastErrorHandler } = useErrorHandling()
|
||||
|
||||
@@ -3,11 +3,11 @@ import { computed, watch } from 'vue'
|
||||
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { AuthUserInfo } from '@/types/authTypes'
|
||||
|
||||
export const useCurrentUser = () => {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const commandStore = useCommandStore()
|
||||
const apiKeyStore = useApiKeyAuthStore()
|
||||
|
||||
|
||||
@@ -70,8 +70,8 @@ vi.mock(
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: () => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: () => ({
|
||||
balance: { amount_micros: 5000000 },
|
||||
fetchBalance: vi.fn().mockResolvedValue({ amount_micros: 5000000 })
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
PreviewSubscribeResponse,
|
||||
SubscribeResponse
|
||||
} from '@/platform/workspace/api/workspaceApi'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
import type {
|
||||
BalanceInfo,
|
||||
@@ -33,7 +33,7 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
showSubscriptionDialog: legacyShowSubscriptionDialog
|
||||
} = useSubscription()
|
||||
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const isInitialized = ref(false)
|
||||
const isLoading = ref(false)
|
||||
@@ -55,12 +55,12 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
renewalDate: formattedRenewalDate.value || null,
|
||||
endDate: formattedEndDate.value || null,
|
||||
isCancelled: isCancelled.value,
|
||||
hasFunds: (firebaseAuthStore.balance?.amount_micros ?? 0) > 0
|
||||
hasFunds: (authStore.balance?.amount_micros ?? 0) > 0
|
||||
}
|
||||
})
|
||||
|
||||
const balance = computed<BalanceInfo | null>(() => {
|
||||
const legacyBalance = firebaseAuthStore.balance
|
||||
const legacyBalance = authStore.balance
|
||||
if (!legacyBalance) return null
|
||||
|
||||
return {
|
||||
@@ -118,7 +118,7 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await firebaseAuthStore.fetchBalance()
|
||||
await authStore.fetchBalance()
|
||||
} catch (err) {
|
||||
error.value =
|
||||
err instanceof Error ? err.message : 'Failed to fetch balance'
|
||||
|
||||
@@ -56,8 +56,8 @@ vi.mock('@/scripts/api', () => ({
|
||||
|
||||
vi.mock('@/platform/settings/settingStore')
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({}))
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuth', () => ({
|
||||
@@ -123,8 +123,8 @@ vi.mock('@/stores/workspace/colorPaletteStore', () => ({
|
||||
useColorPaletteStore: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: vi.fn(() => ({}))
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { useSubgraphOperations } from '@/composables/graph/useSubgraphOperations'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
@@ -78,7 +78,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const dialogService = useDialogService()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const firebaseAuthActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const toastStore = useToastStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const executionStore = useExecutionStore()
|
||||
@@ -996,7 +996,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Sign Out',
|
||||
versionAdded: '1.18.1',
|
||||
function: async () => {
|
||||
await firebaseAuthActions.logout()
|
||||
await authActions.logout()
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2616,8 +2616,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
pointer.finally = () => (this.resizingGroup = null)
|
||||
} else {
|
||||
const f = group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE
|
||||
const headerHeight = f * 1.4
|
||||
const headerHeight = LiteGraph.NODE_TITLE_HEIGHT
|
||||
if (
|
||||
isInRectangle(
|
||||
x,
|
||||
|
||||
@@ -40,7 +40,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
||||
color?: string
|
||||
title: string
|
||||
font?: string
|
||||
font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24
|
||||
font_size: number = LiteGraph.GROUP_TEXT_SIZE
|
||||
_bounding = new Rectangle(10, 10, LGraphGroup.minWidth, LGraphGroup.minHeight)
|
||||
|
||||
_pos: Point = this._bounding.pos
|
||||
@@ -116,7 +116,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
||||
}
|
||||
|
||||
get titleHeight() {
|
||||
return this.font_size * 1.4
|
||||
return LiteGraph.NODE_TITLE_HEIGHT
|
||||
}
|
||||
|
||||
get children(): ReadonlySet<Positionable> {
|
||||
@@ -148,7 +148,6 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
||||
this._bounding.set(o.bounding)
|
||||
this.color = o.color
|
||||
this.flags = o.flags || this.flags
|
||||
if (o.font_size) this.font_size = o.font_size
|
||||
}
|
||||
|
||||
serialize(): ISerialisedGroup {
|
||||
@@ -158,7 +157,6 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
||||
title: this.title,
|
||||
bounding: [...b],
|
||||
color: this.color,
|
||||
font_size: this.font_size,
|
||||
flags: this.flags
|
||||
}
|
||||
}
|
||||
@@ -170,7 +168,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
||||
*/
|
||||
draw(graphCanvas: LGraphCanvas, ctx: CanvasRenderingContext2D): void {
|
||||
const { padding, resizeLength, defaultColour } = LGraphGroup
|
||||
const font_size = this.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE
|
||||
const font_size = LiteGraph.GROUP_TEXT_SIZE
|
||||
|
||||
const [x, y] = this._pos
|
||||
const [width, height] = this._size
|
||||
@@ -181,7 +179,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
||||
ctx.fillStyle = color
|
||||
ctx.strokeStyle = color
|
||||
ctx.beginPath()
|
||||
ctx.rect(x + 0.5, y + 0.5, width, font_size * 1.4)
|
||||
ctx.rect(x + 0.5, y + 0.5, width, LiteGraph.NODE_TITLE_HEIGHT)
|
||||
ctx.fill()
|
||||
|
||||
// Group background, border
|
||||
@@ -203,11 +201,13 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
||||
// Title
|
||||
ctx.font = `${font_size}px ${LiteGraph.GROUP_FONT}`
|
||||
ctx.textAlign = 'left'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(
|
||||
this.title + (this.pinned ? '📌' : ''),
|
||||
x + padding,
|
||||
y + font_size
|
||||
x + font_size / 2,
|
||||
y + LiteGraph.NODE_TITLE_HEIGHT / 2 + 1
|
||||
)
|
||||
ctx.textBaseline = 'alphabetic'
|
||||
|
||||
if (LiteGraph.highlight_selected_group && this.selected) {
|
||||
strokeShape(ctx, this._bounding, {
|
||||
|
||||
@@ -72,8 +72,7 @@ export class LiteGraphGlobal {
|
||||
DEFAULT_FONT = 'Inter'
|
||||
DEFAULT_SHADOW_COLOR = 'rgba(0,0,0,0.5)'
|
||||
|
||||
DEFAULT_GROUP_FONT = 24
|
||||
DEFAULT_GROUP_FONT_SIZE = 24
|
||||
GROUP_TEXT_SIZE = 20
|
||||
GROUP_FONT = 'Inter'
|
||||
|
||||
WIDGET_BGCOLOR = '#222'
|
||||
|
||||
@@ -18,7 +18,6 @@ exports[`LGraph > supports schema v0.4 graphs > oldSchemaGraph 1`] = `
|
||||
],
|
||||
"color": "#6029aa",
|
||||
"flags": {},
|
||||
"font_size": 14,
|
||||
"id": 123,
|
||||
"title": "A group to test with",
|
||||
},
|
||||
|
||||
@@ -10,7 +10,6 @@ exports[`LGraphGroup > serializes to the existing format > Basic 1`] = `
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"flags": {},
|
||||
"font_size": 24,
|
||||
"id": 929,
|
||||
"title": "title",
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ LiteGraphGlobal {
|
||||
"ContextMenu": [Function],
|
||||
"CurveEditor": [Function],
|
||||
"DEFAULT_FONT": "Inter",
|
||||
"DEFAULT_GROUP_FONT": 24,
|
||||
"DEFAULT_GROUP_FONT_SIZE": 24,
|
||||
"DEFAULT_POSITION": [
|
||||
100,
|
||||
100,
|
||||
@@ -34,6 +32,7 @@ LiteGraphGlobal {
|
||||
"EVENT_LINK_COLOR": "#A86",
|
||||
"GRID_SHAPE": 6,
|
||||
"GROUP_FONT": "Inter",
|
||||
"GROUP_TEXT_SIZE": 20,
|
||||
"Globals": {},
|
||||
"HIDDEN_LINK": -1,
|
||||
"INPUT": 1,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
/**
|
||||
* Session cookie management for cloud authentication.
|
||||
@@ -21,7 +21,7 @@ export const useSessionCookie = () => {
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
try {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
let authHeader: Record<string, string>
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
|
||||
interface Props {
|
||||
errorMessage?: string
|
||||
@@ -83,7 +83,7 @@ interface Props {
|
||||
defineProps<Props>()
|
||||
|
||||
const router = useRouter()
|
||||
const { logout } = useFirebaseAuthActions()
|
||||
const { logout } = useAuthActions()
|
||||
const showTechnicalDetails = ref(false)
|
||||
|
||||
const handleRestart = async () => {
|
||||
|
||||
@@ -76,11 +76,11 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
|
||||
const email = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -110,7 +110,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import CloudSignInForm from '@/platform/cloud/onboarding/components/CloudSignInForm.vue'
|
||||
import { useFreeTierOnboarding } from '@/platform/cloud/onboarding/composables/useFreeTierOnboarding'
|
||||
import { getSafePreviousFullPath } from '@/platform/cloud/onboarding/utils/previousFullPath'
|
||||
@@ -120,7 +120,7 @@ import type { SignInData } from '@/schemas/signInSchema'
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const isSecureContext = globalThis.isSecureContext
|
||||
const authError = ref('')
|
||||
const toastStore = useToastStore()
|
||||
|
||||
@@ -133,7 +133,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import SignUpForm from '@/components/dialog/content/signin/SignUpForm.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useFreeTierOnboarding } from '@/platform/cloud/onboarding/composables/useFreeTierOnboarding'
|
||||
import { getSafePreviousFullPath } from '@/platform/cloud/onboarding/utils/previousFullPath'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
@@ -145,7 +145,7 @@ import { isInChina } from '@/utils/networkUtil'
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const isSecureContext = globalThis.isSecureContext
|
||||
const authError = ref('')
|
||||
const userIsInChina = ref(false)
|
||||
|
||||
@@ -25,8 +25,8 @@ const authActionMocks = vi.hoisted(() => ({
|
||||
accessBillingPortal: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: () => authActionMocks
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: () => authActionMocks
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useErrorHandling', () => ({
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import { performSubscriptionCheckout } from '@/platform/cloud/subscription/utils/subscriptionCheckoutUtil'
|
||||
@@ -16,7 +16,7 @@ import type { BillingCycle } from '../subscription/utils/subscriptionTierRank'
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { reportError, accessBillingPortal } = useFirebaseAuthActions()
|
||||
const { reportError, accessBillingPortal } = useAuthActions()
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const { isActiveSubscription, isInitialized, initialize } = useBillingContext()
|
||||
|
||||
@@ -89,9 +89,9 @@ import { useI18n } from 'vue-i18n'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { signInSchema } from '@/schemas/signInSchema'
|
||||
import type { SignInData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -31,8 +31,8 @@ vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: () => ({
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: () => ({
|
||||
accessBillingPortal: mockAccessBillingPortal,
|
||||
reportError: mockReportError
|
||||
})
|
||||
@@ -56,13 +56,13 @@ vi.mock('@/composables/useErrorHandling', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: () =>
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: () =>
|
||||
reactive({
|
||||
getAuthHeader: mockGetAuthHeader,
|
||||
userId: computed(() => mockUserId.value)
|
||||
}),
|
||||
FirebaseAuthStoreError: class extends Error {}
|
||||
AuthStoreError: class extends Error {}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
|
||||
@@ -262,7 +262,7 @@ import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import {
|
||||
@@ -279,7 +279,7 @@ import type { BillingCycle } from '@/platform/cloud/subscription/utils/subscript
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { CheckoutAttributionMetadata } from '@/platform/telemetry/types'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type SubscriptionTier = components['schemas']['SubscriptionTier']
|
||||
@@ -365,8 +365,8 @@ const {
|
||||
isYearlySubscription
|
||||
} = useSubscription()
|
||||
const telemetry = useTelemetry()
|
||||
const { userId } = storeToRefs(useFirebaseAuthStore())
|
||||
const { accessBillingPortal, reportError } = useFirebaseAuthActions()
|
||||
const { userId } = storeToRefs(useAuthStore())
|
||||
const { accessBillingPortal, reportError } = useAuthActions()
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
@@ -82,8 +82,8 @@ vi.mock(
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: vi.fn(() => ({
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: vi.fn(() => ({
|
||||
authActions: vi.fn(() => ({
|
||||
accessBillingPortal: vi.fn()
|
||||
}))
|
||||
|
||||
@@ -211,7 +211,7 @@ import { computed, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useSubscriptionActions } from '@/platform/cloud/subscription/composables/useSubscriptionActions'
|
||||
@@ -227,7 +227,7 @@ import type { TierBenefit } from '@/platform/cloud/subscription/utils/tierBenefi
|
||||
import { getCommonTierBenefits } from '@/platform/cloud/subscription/utils/tierBenefits'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const { t, n } = useI18n()
|
||||
|
||||
const {
|
||||
|
||||
@@ -65,8 +65,8 @@ vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: vi.fn(() => mockTelemetry)
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: vi.fn(() => ({
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: vi.fn(() => ({
|
||||
reportError: mockReportError,
|
||||
accessBillingPortal: mockAccessBillingPortal
|
||||
}))
|
||||
@@ -106,14 +106,14 @@ vi.mock('@/services/dialogService', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
getAuthHeader: mockGetAuthHeader,
|
||||
get userId() {
|
||||
return mockUserId.value
|
||||
}
|
||||
})),
|
||||
FirebaseAuthStoreError: class extends Error {}
|
||||
AuthStoreError: class extends Error {}
|
||||
}))
|
||||
|
||||
// Mock fetch
|
||||
|
||||
@@ -2,7 +2,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { getComfyApiBaseUrl, getComfyPlatformBaseUrl } from '@/config/comfyApi'
|
||||
import { t } from '@/i18n'
|
||||
@@ -10,10 +10,7 @@ import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { SubscriptionDialogReason } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
|
||||
import type { CheckoutAttributionMetadata } from '@/platform/telemetry/types'
|
||||
import {
|
||||
FirebaseAuthStoreError,
|
||||
useFirebaseAuthStore
|
||||
} from '@/stores/firebaseAuthStore'
|
||||
import { AuthStoreError, useAuthStore } from '@/stores/authStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { TIER_TO_KEY } from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import type { operations } from '@/types/comfyRegistryTypes'
|
||||
@@ -37,11 +34,11 @@ function useSubscriptionInternal() {
|
||||
|
||||
return subscriptionStatus.value?.is_active ?? false
|
||||
})
|
||||
const { reportError, accessBillingPortal } = useFirebaseAuthActions()
|
||||
const { reportError, accessBillingPortal } = useAuthActions()
|
||||
const { showSubscriptionRequiredDialog } = useDialogService()
|
||||
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const { getAuthHeader } = firebaseAuthStore
|
||||
const authStore = useAuthStore()
|
||||
const { getAuthHeader } = authStore
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
@@ -194,7 +191,7 @@ function useSubscriptionInternal() {
|
||||
async function fetchSubscriptionStatus(): Promise<CloudSubscriptionStatusResponse | null> {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
@@ -209,7 +206,7 @@ function useSubscriptionInternal() {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToFetchSubscription', {
|
||||
error: errorData.message
|
||||
})
|
||||
@@ -248,9 +245,7 @@ function useSubscriptionInternal() {
|
||||
async (): Promise<CloudSubscriptionCheckoutResponse> => {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.userNotAuthenticated')
|
||||
)
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
const checkoutAttribution = await getCheckoutAttributionForCloud()
|
||||
|
||||
@@ -268,7 +263,7 @@ function useSubscriptionInternal() {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToInitiateSubscription', {
|
||||
error: errorData.message
|
||||
})
|
||||
|
||||
@@ -8,8 +8,8 @@ const mockFetchStatus = vi.fn()
|
||||
const mockShowTopUpCreditsDialog = vi.fn()
|
||||
const mockExecute = vi.fn()
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: () => ({
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: () => ({
|
||||
fetchBalance: mockFetchBalance
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
@@ -12,7 +12,7 @@ import { useCommandStore } from '@/stores/commandStore'
|
||||
*/
|
||||
export function useSubscriptionActions() {
|
||||
const dialogService = useDialogService()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
const commandStore = useCommandStore()
|
||||
const telemetry = useTelemetry()
|
||||
const { fetchStatus } = useBillingContext()
|
||||
|
||||
@@ -36,14 +36,14 @@ vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: vi.fn(() => mockTelemetry)
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() =>
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() =>
|
||||
reactive({
|
||||
getAuthHeader: mockGetAuthHeader,
|
||||
userId: computed(() => mockUserId.value)
|
||||
})
|
||||
),
|
||||
FirebaseAuthStoreError: class extends Error {}
|
||||
AuthStoreError: class extends Error {}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
|
||||
@@ -4,10 +4,7 @@ import { getComfyApiBaseUrl } from '@/config/comfyApi'
|
||||
import { t } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import {
|
||||
FirebaseAuthStoreError,
|
||||
useFirebaseAuthStore
|
||||
} from '@/stores/firebaseAuthStore'
|
||||
import { AuthStoreError, useAuthStore } from '@/stores/authStore'
|
||||
import type { CheckoutAttributionMetadata } from '@/platform/telemetry/types'
|
||||
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import type { BillingCycle } from './subscriptionTierRank'
|
||||
@@ -51,13 +48,13 @@ export async function performSubscriptionCheckout(
|
||||
): Promise<void> {
|
||||
if (!isCloud) return
|
||||
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const { userId } = storeToRefs(firebaseAuthStore)
|
||||
const authStore = useAuthStore()
|
||||
const { userId } = storeToRefs(authStore)
|
||||
const telemetry = useTelemetry()
|
||||
const authHeader = await firebaseAuthStore.getAuthHeader()
|
||||
const authHeader = await authStore.getAuthHeader()
|
||||
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const checkoutTier = getCheckoutTier(tierKey, currentBillingCycle)
|
||||
@@ -97,7 +94,7 @@ export async function performSubscriptionCheckout(
|
||||
}
|
||||
}
|
||||
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToInitiateSubscription', {
|
||||
error: errorMessage
|
||||
})
|
||||
|
||||
@@ -90,7 +90,7 @@ import CurrentUserMessage from '@/components/dialog/content/setting/CurrentUserM
|
||||
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
import NavItem from '@/components/widget/nav/NavItem.vue'
|
||||
import NavTitle from '@/components/widget/nav/NavTitle.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import ColorPaletteMessage from '@/platform/settings/components/ColorPaletteMessage.vue'
|
||||
import SettingsPanel from '@/platform/settings/components/SettingsPanel.vue'
|
||||
import { useSettingSearch } from '@/platform/settings/composables/useSettingSearch'
|
||||
@@ -129,7 +129,7 @@ const {
|
||||
getSearchResults
|
||||
} = useSettingSearch()
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authActions = useAuthActions()
|
||||
|
||||
const navRef = ref<HTMLElement | null>(null)
|
||||
const activeCategoryKey = ref<string | null>(defaultCategory.value?.key ?? null)
|
||||
|
||||
@@ -6,7 +6,7 @@ type MockApiKeyUser = {
|
||||
email?: string
|
||||
} | null
|
||||
|
||||
type MockFirebaseUser = {
|
||||
type MockAuthUser = {
|
||||
uid: string
|
||||
email?: string | null
|
||||
} | null
|
||||
@@ -14,19 +14,19 @@ type MockFirebaseUser = {
|
||||
const {
|
||||
mockCaptureCheckoutAttributionFromSearch,
|
||||
mockUseApiKeyAuthStore,
|
||||
mockUseFirebaseAuthStore,
|
||||
mockUseAuthStore,
|
||||
mockApiKeyAuthStore,
|
||||
mockFirebaseAuthStore
|
||||
mockAuthStore
|
||||
} = vi.hoisted(() => ({
|
||||
mockCaptureCheckoutAttributionFromSearch: vi.fn(),
|
||||
mockUseApiKeyAuthStore: vi.fn(),
|
||||
mockUseFirebaseAuthStore: vi.fn(),
|
||||
mockUseAuthStore: vi.fn(),
|
||||
mockApiKeyAuthStore: {
|
||||
isAuthenticated: false,
|
||||
currentUser: null as MockApiKeyUser
|
||||
},
|
||||
mockFirebaseAuthStore: {
|
||||
currentUser: null as MockFirebaseUser
|
||||
mockAuthStore: {
|
||||
currentUser: null as MockAuthUser
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -38,8 +38,8 @@ vi.mock('@/stores/apiKeyAuthStore', () => ({
|
||||
useApiKeyAuthStore: mockUseApiKeyAuthStore
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: mockUseFirebaseAuthStore
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: mockUseAuthStore
|
||||
}))
|
||||
|
||||
import { ImpactTelemetryProvider } from './ImpactTelemetryProvider'
|
||||
@@ -64,14 +64,14 @@ describe('ImpactTelemetryProvider', () => {
|
||||
beforeEach(() => {
|
||||
mockCaptureCheckoutAttributionFromSearch.mockReset()
|
||||
mockUseApiKeyAuthStore.mockReset()
|
||||
mockUseFirebaseAuthStore.mockReset()
|
||||
mockUseAuthStore.mockReset()
|
||||
mockApiKeyAuthStore.isAuthenticated = false
|
||||
mockApiKeyAuthStore.currentUser = null
|
||||
mockFirebaseAuthStore.currentUser = null
|
||||
mockAuthStore.currentUser = null
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
mockUseApiKeyAuthStore.mockReturnValue(mockApiKeyAuthStore)
|
||||
mockUseFirebaseAuthStore.mockReturnValue(mockFirebaseAuthStore)
|
||||
mockUseAuthStore.mockReturnValue(mockAuthStore)
|
||||
|
||||
const queueFn: NonNullable<Window['ire']> = (...args: unknown[]) => {
|
||||
;(queueFn.a ??= []).push(args)
|
||||
@@ -93,7 +93,7 @@ describe('ImpactTelemetryProvider', () => {
|
||||
})
|
||||
|
||||
it('captures attribution and invokes identify with hashed email', async () => {
|
||||
mockFirebaseAuthStore.currentUser = {
|
||||
mockAuthStore.currentUser = {
|
||||
uid: 'user-123',
|
||||
email: ' User@Example.com '
|
||||
}
|
||||
@@ -153,7 +153,7 @@ describe('ImpactTelemetryProvider', () => {
|
||||
})
|
||||
|
||||
it('invokes identify on each page view even with identical identity payloads', async () => {
|
||||
mockFirebaseAuthStore.currentUser = {
|
||||
mockAuthStore.currentUser = {
|
||||
uid: 'user-123',
|
||||
email: 'user@example.com'
|
||||
}
|
||||
@@ -189,7 +189,7 @@ describe('ImpactTelemetryProvider', () => {
|
||||
id: 'api-key-user-123',
|
||||
email: 'apikey@example.com'
|
||||
}
|
||||
mockFirebaseAuthStore.currentUser = {
|
||||
mockAuthStore.currentUser = {
|
||||
uid: 'firebase-user-123',
|
||||
email: 'firebase@example.com'
|
||||
}
|
||||
@@ -228,7 +228,7 @@ describe('ImpactTelemetryProvider', () => {
|
||||
id: 'api-key-user-123',
|
||||
email: 'apikey@example.com'
|
||||
}
|
||||
mockFirebaseAuthStore.currentUser = null
|
||||
mockAuthStore.currentUser = null
|
||||
vi.stubGlobal('crypto', {
|
||||
subtle: {
|
||||
digest: vi.fn(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { captureCheckoutAttributionFromSearch } from '@/platform/telemetry/utils/checkoutAttribution'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
import type { PageViewMetadata, TelemetryProvider } from '../../types'
|
||||
|
||||
@@ -17,7 +17,7 @@ export class ImpactTelemetryProvider implements TelemetryProvider {
|
||||
private initialized = false
|
||||
private stores: {
|
||||
apiKeyAuthStore: ReturnType<typeof useApiKeyAuthStore>
|
||||
firebaseAuthStore: ReturnType<typeof useFirebaseAuthStore>
|
||||
authStore: ReturnType<typeof useAuthStore>
|
||||
} | null = null
|
||||
|
||||
constructor() {
|
||||
@@ -109,12 +109,11 @@ export class ImpactTelemetryProvider implements TelemetryProvider {
|
||||
}
|
||||
}
|
||||
|
||||
if (stores.firebaseAuthStore.currentUser) {
|
||||
if (stores.authStore.currentUser) {
|
||||
return {
|
||||
customerId:
|
||||
stores.firebaseAuthStore.currentUser.uid ?? EMPTY_CUSTOMER_VALUE,
|
||||
customerId: stores.authStore.currentUser.uid ?? EMPTY_CUSTOMER_VALUE,
|
||||
customerEmail:
|
||||
stores.firebaseAuthStore.currentUser.email ?? EMPTY_CUSTOMER_VALUE
|
||||
stores.authStore.currentUser.email ?? EMPTY_CUSTOMER_VALUE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +134,7 @@ export class ImpactTelemetryProvider implements TelemetryProvider {
|
||||
|
||||
private resolveAuthStores(): {
|
||||
apiKeyAuthStore: ReturnType<typeof useApiKeyAuthStore>
|
||||
firebaseAuthStore: ReturnType<typeof useFirebaseAuthStore>
|
||||
authStore: ReturnType<typeof useAuthStore>
|
||||
} | null {
|
||||
if (this.stores) {
|
||||
return this.stores
|
||||
@@ -144,7 +143,7 @@ export class ImpactTelemetryProvider implements TelemetryProvider {
|
||||
try {
|
||||
const stores = {
|
||||
apiKeyAuthStore: useApiKeyAuthStore(),
|
||||
firebaseAuthStore: useFirebaseAuthStore()
|
||||
authStore: useAuthStore()
|
||||
}
|
||||
this.stores = stores
|
||||
return stores
|
||||
|
||||
@@ -53,8 +53,8 @@ export function useWorkflowPersistenceV2() {
|
||||
const toast = useToast()
|
||||
const { onUserLogout } = useCurrentUser()
|
||||
|
||||
// Run migration on module load
|
||||
migrateV1toV2()
|
||||
// Run migration on module load, passing clientId for tab state migration
|
||||
migrateV1toV2(undefined, api.clientId ?? api.initialClientId ?? undefined)
|
||||
|
||||
// Clear workflow persistence storage when user signs out (cloud only)
|
||||
onUserLogout(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { hashPath } from '../base/hashUtil'
|
||||
import { readOpenPaths } from '../base/storageIO'
|
||||
import {
|
||||
cleanupV1Data,
|
||||
getMigrationStatus,
|
||||
@@ -212,6 +213,85 @@ describe('migrateV1toV2', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('V1 tab state migration', () => {
|
||||
it('migrates V1 tab state pointers to V2 format', () => {
|
||||
// Simulate V1 state: user had 3 workflows open, 2nd was active
|
||||
const v1Drafts = {
|
||||
'workflows/a.json': {
|
||||
data: '{"nodes":[1]}',
|
||||
updatedAt: 1000,
|
||||
name: 'a',
|
||||
isTemporary: true
|
||||
},
|
||||
'workflows/b.json': {
|
||||
data: '{"nodes":[2]}',
|
||||
updatedAt: 2000,
|
||||
name: 'b',
|
||||
isTemporary: true
|
||||
},
|
||||
'workflows/c.json': {
|
||||
data: '{"nodes":[3]}',
|
||||
updatedAt: 3000,
|
||||
name: 'c',
|
||||
isTemporary: false
|
||||
}
|
||||
}
|
||||
setV1Data(v1Drafts, [
|
||||
'workflows/a.json',
|
||||
'workflows/b.json',
|
||||
'workflows/c.json'
|
||||
])
|
||||
|
||||
// V1 tab state stored by setStorageValue (localStorage fallback keys)
|
||||
localStorage.setItem(
|
||||
'Comfy.OpenWorkflowsPaths',
|
||||
JSON.stringify([
|
||||
'workflows/a.json',
|
||||
'workflows/b.json',
|
||||
'workflows/c.json'
|
||||
])
|
||||
)
|
||||
localStorage.setItem('Comfy.ActiveWorkflowIndex', JSON.stringify(1))
|
||||
|
||||
// Run migration (simulating upgrade from pre-V2 to V2)
|
||||
const clientId = 'client-123'
|
||||
const result = migrateV1toV2(workspaceId, clientId)
|
||||
expect(result).toBe(3)
|
||||
|
||||
// V2 tab state should be readable via the V2 API
|
||||
const openPaths = readOpenPaths(clientId, workspaceId)
|
||||
|
||||
// This is the bug: V1 tab state is NOT migrated, so openPaths is null
|
||||
expect(openPaths).not.toBeNull()
|
||||
expect(openPaths!.paths).toEqual([
|
||||
'workflows/a.json',
|
||||
'workflows/b.json',
|
||||
'workflows/c.json'
|
||||
])
|
||||
expect(openPaths!.activeIndex).toBe(1)
|
||||
})
|
||||
|
||||
it('does not migrate tab state when V1 tab state keys are absent', () => {
|
||||
const v1Drafts = {
|
||||
'workflows/a.json': {
|
||||
data: '{}',
|
||||
updatedAt: 1000,
|
||||
name: 'a',
|
||||
isTemporary: true
|
||||
}
|
||||
}
|
||||
setV1Data(v1Drafts, ['workflows/a.json'])
|
||||
|
||||
// No V1 tab state keys in localStorage
|
||||
migrateV1toV2(workspaceId)
|
||||
|
||||
const openPaths = readOpenPaths('any-client-id', workspaceId)
|
||||
|
||||
// No tab state to migrate — should remain null
|
||||
expect(openPaths).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMigrationStatus', () => {
|
||||
it('reports correct status', () => {
|
||||
setV1Data(
|
||||
|
||||
@@ -10,7 +10,12 @@ import type { DraftIndexV2 } from '../base/draftTypes'
|
||||
import { upsertEntry, createEmptyIndex } from '../base/draftCacheV2'
|
||||
import { hashPath } from '../base/hashUtil'
|
||||
import { getWorkspaceId } from '../base/storageKeys'
|
||||
import { readIndex, writeIndex, writePayload } from '../base/storageIO'
|
||||
import {
|
||||
readIndex,
|
||||
writeIndex,
|
||||
writeOpenPaths,
|
||||
writePayload
|
||||
} from '../base/storageIO'
|
||||
|
||||
/**
|
||||
* V1 draft snapshot structure (from draftCache.ts)
|
||||
@@ -27,7 +32,9 @@ interface V1DraftSnapshot {
|
||||
*/
|
||||
const V1_KEYS = {
|
||||
drafts: (workspaceId: string) => `Comfy.Workflow.Drafts:${workspaceId}`,
|
||||
order: (workspaceId: string) => `Comfy.Workflow.DraftOrder:${workspaceId}`
|
||||
order: (workspaceId: string) => `Comfy.Workflow.DraftOrder:${workspaceId}`,
|
||||
openPaths: 'Comfy.OpenWorkflowsPaths',
|
||||
activeIndex: 'Comfy.ActiveWorkflowIndex'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,7 +71,10 @@ function readV1Drafts(
|
||||
*
|
||||
* @returns Number of drafts migrated, or -1 if migration not needed/failed
|
||||
*/
|
||||
export function migrateV1toV2(workspaceId: string = getWorkspaceId()): number {
|
||||
export function migrateV1toV2(
|
||||
workspaceId: string = getWorkspaceId(),
|
||||
clientId?: string
|
||||
): number {
|
||||
// Check if V2 already exists
|
||||
if (isV2MigrationComplete(workspaceId)) {
|
||||
return -1
|
||||
@@ -116,12 +126,48 @@ export function migrateV1toV2(workspaceId: string = getWorkspaceId()): number {
|
||||
return -1
|
||||
}
|
||||
|
||||
// Migrate V1 tab state pointers to V2 sessionStorage format.
|
||||
// V1 used setStorageValue which stored tab state in localStorage as fallback.
|
||||
// V2 uses sessionStorage keyed by clientId. Without this migration,
|
||||
// users upgrading from V1 lose their open tab list.
|
||||
migrateV1TabState(workspaceId, clientId)
|
||||
|
||||
if (migrated > 0) {
|
||||
console.warn(`[V2 Migration] Migrated ${migrated} drafts from V1 to V2`)
|
||||
}
|
||||
return migrated
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates V1 tab state (open paths + active index) to V2 format.
|
||||
* V1 stored these in localStorage via setStorageValue fallback.
|
||||
* V2 uses sessionStorage keyed by clientId.
|
||||
*/
|
||||
function migrateV1TabState(workspaceId: string, clientId?: string): void {
|
||||
if (!clientId) return
|
||||
|
||||
try {
|
||||
const pathsJson = localStorage.getItem(V1_KEYS.openPaths)
|
||||
if (!pathsJson) return
|
||||
|
||||
const paths = JSON.parse(pathsJson)
|
||||
if (!Array.isArray(paths) || paths.length === 0) return
|
||||
|
||||
const indexJson = localStorage.getItem(V1_KEYS.activeIndex)
|
||||
let activeIndex = 0
|
||||
if (indexJson !== null) {
|
||||
const parsed = JSON.parse(indexJson)
|
||||
if (typeof parsed === 'number' && Number.isFinite(parsed)) {
|
||||
activeIndex = Math.min(Math.max(0, parsed), paths.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
writeOpenPaths(clientId, { workspaceId, paths, activeIndex })
|
||||
} catch {
|
||||
// Best effort - don't block draft migration on tab state errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up V1 data after successful migration.
|
||||
* Should NOT be called until 2026-07-15 to allow rollback.
|
||||
|
||||
@@ -2,7 +2,7 @@ import axios from 'axios'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
export type WorkspaceType = 'personal' | 'team'
|
||||
export type WorkspaceRole = 'owner' | 'member'
|
||||
@@ -288,7 +288,7 @@ const workspaceApiClient = axios.create({
|
||||
})
|
||||
|
||||
async function getAuthHeaderOrThrow() {
|
||||
const authHeader = await useFirebaseAuthStore().getAuthHeader()
|
||||
const authHeader = await useAuthStore().getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new WorkspaceApiError(
|
||||
t('toastMessages.userNotAuthenticated'),
|
||||
@@ -300,7 +300,7 @@ async function getAuthHeaderOrThrow() {
|
||||
}
|
||||
|
||||
async function getFirebaseHeaderOrThrow() {
|
||||
const authHeader = await useFirebaseAuthStore().getFirebaseAuthHeader()
|
||||
const authHeader = await useAuthStore().getFirebaseAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new WorkspaceApiError(
|
||||
t('toastMessages.userNotAuthenticated'),
|
||||
|
||||
@@ -8,8 +8,8 @@ import WorkspaceAuthGate from './WorkspaceAuthGate.vue'
|
||||
const mockIsInitialized = ref(false)
|
||||
const mockCurrentUser = ref<object | null>(null)
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: () => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: () => ({
|
||||
isInitialized: mockIsInitialized,
|
||||
currentUser: mockCurrentUser
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
|
||||
import { refreshRemoteConfig } from '@/platform/remoteConfig/refreshRemoteConfig'
|
||||
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const FIREBASE_INIT_TIMEOUT_MS = 16_000
|
||||
const CONFIG_REFRESH_TIMEOUT_MS = 10_000
|
||||
@@ -38,7 +38,7 @@ const subscriptionDialog = useSubscriptionDialog()
|
||||
async function initialize(): Promise<void> {
|
||||
if (!isCloud) return
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const { isInitialized, currentUser } = storeToRefs(authStore)
|
||||
|
||||
try {
|
||||
|
||||
@@ -10,8 +10,8 @@ import { WORKSPACE_STORAGE_KEYS } from '@/platform/workspace/workspaceConstants'
|
||||
|
||||
const mockGetIdToken = vi.fn()
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: () => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: () => ({
|
||||
getIdToken: mockGetIdToken
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
WORKSPACE_STORAGE_KEYS
|
||||
} from '@/platform/workspace/workspaceConstants'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { AuthHeader } from '@/types/authTypes'
|
||||
import type { WorkspaceWithRole } from '@/platform/workspace/workspaceTypes'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
@@ -181,8 +181,8 @@ export const useWorkspaceAuthStore = defineStore('workspaceAuth', () => {
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const firebaseToken = await firebaseAuthStore.getIdToken()
|
||||
const authStore = useAuthStore()
|
||||
const firebaseToken = await authStore.getIdToken()
|
||||
if (!firebaseToken) {
|
||||
throw new WorkspaceAuthError(
|
||||
t('workspaceAuth.errors.notAuthenticated'),
|
||||
|
||||
@@ -24,12 +24,6 @@ vi.mock('@/scripts/app', () => ({
|
||||
app: { canvas: { graph: mockGraph, setDirty: vi.fn() } }
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/element/useCanvasPositionConversion', () => ({
|
||||
useSharedCanvasPositionConversion: () => ({
|
||||
clientPosToCanvasPos: ([x, y]: [number, number]) => [x, y]
|
||||
})
|
||||
}))
|
||||
|
||||
const NODE_ID = 'test-node'
|
||||
const SLOT_INDEX = 0
|
||||
|
||||
@@ -51,15 +45,47 @@ function createWrapperComponent(type: 'input' | 'output') {
|
||||
})
|
||||
}
|
||||
|
||||
function createSlotElement(): HTMLElement {
|
||||
const container = document.createElement('div')
|
||||
container.dataset.nodeId = NODE_ID
|
||||
container.getBoundingClientRect = () =>
|
||||
({
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 200,
|
||||
bottom: 100,
|
||||
width: 200,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({})
|
||||
}) as DOMRect
|
||||
document.body.appendChild(container)
|
||||
|
||||
const el = document.createElement('div')
|
||||
el.getBoundingClientRect = () =>
|
||||
({
|
||||
left: 10,
|
||||
top: 30,
|
||||
right: 20,
|
||||
bottom: 40,
|
||||
width: 10,
|
||||
height: 10,
|
||||
x: 10,
|
||||
y: 30,
|
||||
toJSON: () => ({})
|
||||
}) as DOMRect
|
||||
container.appendChild(el)
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount the wrapper, set the element ref, and wait for slot registration.
|
||||
*/
|
||||
async function mountAndRegisterSlot(type: 'input' | 'output') {
|
||||
const wrapper = mount(createWrapperComponent(type))
|
||||
const slotEl = document.createElement('div')
|
||||
slotEl.getBoundingClientRect = vi.fn(() => new DOMRect(100, 200, 16, 16))
|
||||
document.body.append(slotEl)
|
||||
wrapper.vm.el = slotEl
|
||||
wrapper.vm.el = createSlotElement()
|
||||
await nextTick()
|
||||
flushScheduledSlotLayoutSync()
|
||||
return wrapper
|
||||
@@ -186,17 +212,19 @@ describe('useSlotElementTracking', () => {
|
||||
|
||||
it('skips slot layout writeback when measured slot geometry is unchanged', () => {
|
||||
const slotKey = getSlotKey(NODE_ID, SLOT_INDEX, true)
|
||||
const slotEl = document.createElement('div')
|
||||
document.body.append(slotEl)
|
||||
slotEl.getBoundingClientRect = vi.fn(() => new DOMRect(100, 200, 16, 16))
|
||||
const slotEl = createSlotElement()
|
||||
|
||||
const registryStore = useNodeSlotRegistryStore()
|
||||
const node = registryStore.ensureNode(NODE_ID)
|
||||
|
||||
const expectedX = 15
|
||||
const expectedY = 35 - LiteGraph.NODE_TITLE_HEIGHT
|
||||
|
||||
node.slots.set(slotKey, {
|
||||
el: slotEl,
|
||||
index: SLOT_INDEX,
|
||||
type: 'input',
|
||||
cachedOffset: { x: 108, y: 208 }
|
||||
cachedOffset: { x: expectedX, y: expectedY }
|
||||
})
|
||||
|
||||
const slotSize = LiteGraph.NODE_SLOT_HEIGHT
|
||||
@@ -205,10 +233,10 @@ describe('useSlotElementTracking', () => {
|
||||
nodeId: NODE_ID,
|
||||
index: SLOT_INDEX,
|
||||
type: 'input',
|
||||
position: { x: 108, y: 208 },
|
||||
position: { x: expectedX, y: expectedY },
|
||||
bounds: {
|
||||
x: 108 - halfSlotSize,
|
||||
y: 208 - halfSlotSize,
|
||||
x: expectedX - halfSlotSize,
|
||||
y: expectedY - halfSlotSize,
|
||||
width: slotSize,
|
||||
height: slotSize
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
@@ -102,10 +101,9 @@ export function flushScheduledSlotLayoutSync() {
|
||||
completePendingSlotSync()
|
||||
return
|
||||
}
|
||||
const conv = useSharedCanvasPositionConversion()
|
||||
for (const nodeId of Array.from(pendingNodes)) {
|
||||
pendingNodes.delete(nodeId)
|
||||
syncNodeSlotLayoutsFromDOM(nodeId, conv)
|
||||
syncNodeSlotLayoutsFromDOM(nodeId)
|
||||
}
|
||||
|
||||
// Keep pending sync active until at least one measurable slot layout has
|
||||
@@ -115,19 +113,33 @@ export function flushScheduledSlotLayoutSync() {
|
||||
completePendingSlotSync()
|
||||
}
|
||||
|
||||
export function syncNodeSlotLayoutsFromDOM(
|
||||
nodeId: string,
|
||||
conv?: ReturnType<typeof useSharedCanvasPositionConversion>
|
||||
) {
|
||||
export function syncNodeSlotLayoutsFromDOM(nodeId: string) {
|
||||
const nodeSlotRegistryStore = useNodeSlotRegistryStore()
|
||||
const node = nodeSlotRegistryStore.getNode(nodeId)
|
||||
if (!node) return
|
||||
const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value
|
||||
if (!nodeLayout) return
|
||||
|
||||
const batch: Array<{ key: string; layout: SlotLayout }> = []
|
||||
// Find the node's DOM element for relative offset measurement.
|
||||
// Using DOM-relative measurement avoids the transform desync issue where
|
||||
// lgCanvas.ds (used by clientPosToCanvasPos) can diverge from the
|
||||
// TransformPane's CSS transform during workflow loading (e.g., after
|
||||
// fitView or ensureCorrectLayoutScale). Both the slot and node elements
|
||||
// share the same DOM transform, so their pixel difference divided by the
|
||||
// effective scale yields a correct canvas-space offset regardless of
|
||||
// whether the TransformPane has flushed its latest transform to the DOM.
|
||||
const closestNode = node.slots
|
||||
.values()
|
||||
.next()
|
||||
.value?.el.closest('[data-node-id]')
|
||||
const nodeEl = closestNode instanceof HTMLElement ? closestNode : null
|
||||
const nodeRect = nodeEl?.getBoundingClientRect()
|
||||
const effectiveScale =
|
||||
nodeRect && nodeLayout.size.width > 0
|
||||
? nodeRect.width / nodeLayout.size.width
|
||||
: 0
|
||||
|
||||
const positionConv = conv ?? useSharedCanvasPositionConversion()
|
||||
const batch: Array<{ key: string; layout: SlotLayout }> = []
|
||||
|
||||
for (const [slotKey, entry] of node.slots) {
|
||||
const rect = getSlotElementRect(entry.el)
|
||||
@@ -142,13 +154,23 @@ export function syncNodeSlotLayoutsFromDOM(
|
||||
rect.left + rect.width / 2,
|
||||
rect.top + rect.height / 2
|
||||
]
|
||||
const [x, y] = positionConv.clientPosToCanvasPos(screenCenter)
|
||||
const centerCanvas = { x, y }
|
||||
|
||||
// Cache offset relative to node position for fast updates later
|
||||
if (!nodeRect || effectiveScale <= 0) continue
|
||||
|
||||
// DOM-relative measurement: compute offset from the node element's
|
||||
// top-left corner in canvas units. The node element is rendered at
|
||||
// (position.x, position.y - NODE_TITLE_HEIGHT), so the Y offset
|
||||
// must subtract NODE_TITLE_HEIGHT to be relative to position.y.
|
||||
entry.cachedOffset = {
|
||||
x: centerCanvas.x - nodeLayout.position.x,
|
||||
y: centerCanvas.y - nodeLayout.position.y
|
||||
x: (screenCenter[0] - nodeRect.left) / effectiveScale,
|
||||
y:
|
||||
(screenCenter[1] - nodeRect.top) / effectiveScale -
|
||||
LiteGraph.NODE_TITLE_HEIGHT
|
||||
}
|
||||
|
||||
const centerCanvas = {
|
||||
x: nodeLayout.position.x + entry.cachedOffset.x,
|
||||
y: nodeLayout.position.y + entry.cachedOffset.y
|
||||
}
|
||||
|
||||
const nextLayout = createSlotLayout({
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<Textarea
|
||||
v-bind="filteredProps"
|
||||
:id
|
||||
ref="textAreaRef"
|
||||
v-model="modelValue"
|
||||
:class="
|
||||
cn(
|
||||
@@ -28,7 +29,7 @@
|
||||
:placeholder
|
||||
:readonly="isReadOnly"
|
||||
data-capture-wheel="true"
|
||||
@pointerdown.capture.stop
|
||||
@pointerdown.capture.stop="trackFocus"
|
||||
@pointermove.capture.stop
|
||||
@pointerup.capture.stop
|
||||
@contextmenu.capture="handleContextMenu"
|
||||
@@ -49,7 +50,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useId } from 'vue'
|
||||
import { computed, ref, useId, useTemplateRef } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import Textarea from '@/components/ui/textarea/Textarea.vue'
|
||||
@@ -70,8 +71,15 @@ const { widget, placeholder = '' } = defineProps<{
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
const textAreaRef = useTemplateRef('textAreaRef')
|
||||
|
||||
const modelValue = defineModel<string>({ default: '' })
|
||||
|
||||
const isFocused = ref(false)
|
||||
function trackFocus() {
|
||||
isFocused.value = document.activeElement === textAreaRef.value?.$el
|
||||
}
|
||||
|
||||
const hideLayoutField = useHideLayoutField()
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
|
||||
@@ -87,7 +95,7 @@ const isReadOnly = computed(() =>
|
||||
)
|
||||
|
||||
function handleContextMenu(e: MouseEvent) {
|
||||
if (isNodeOptionsOpen()) {
|
||||
if (isNodeOptionsOpen() || isFocused.value) {
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -38,9 +38,9 @@ vi.mock('@/platform/distribution/types', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', async () => {
|
||||
vi.mock('@/stores/authStore', async () => {
|
||||
return {
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
getAuthHeader: vi.fn(() => Promise.resolve(mockCloudAuth.authHeader))
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { IWidget, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const MAX_RETRIES = 5
|
||||
const TIMEOUT = 4096
|
||||
@@ -23,7 +23,7 @@ interface CacheEntry<T> {
|
||||
|
||||
async function getAuthHeaders() {
|
||||
if (isCloud) {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const authHeader = await authStore.getAuthHeader()
|
||||
return {
|
||||
...(authHeader && { headers: authHeader })
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { isCloud, isDesktop } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import LayoutDefault from '@/views/layouts/LayoutDefault.vue'
|
||||
|
||||
@@ -140,7 +140,7 @@ if (isCloud) {
|
||||
}
|
||||
// Global authentication guard
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Wait for Firebase auth to initialize
|
||||
// Timeout after 16 seconds
|
||||
|
||||
@@ -26,10 +26,8 @@ const litegraphBaseSchema = z.object({
|
||||
CLEAR_BACKGROUND_COLOR: z.string(),
|
||||
NODE_TITLE_COLOR: z.string(),
|
||||
NODE_SELECTED_TITLE_COLOR: z.string(),
|
||||
NODE_TEXT_SIZE: z.number(),
|
||||
NODE_TEXT_COLOR: z.string(),
|
||||
NODE_TEXT_HIGHLIGHT_COLOR: z.string(),
|
||||
NODE_SUBTEXT_SIZE: z.number(),
|
||||
NODE_DEFAULT_COLOR: z.string(),
|
||||
NODE_DEFAULT_BGCOLOR: z.string(),
|
||||
NODE_DEFAULT_BOXCOLOR: z.string(),
|
||||
@@ -44,7 +42,6 @@ const litegraphBaseSchema = z.object({
|
||||
NODE_BYPASS_BGCOLOR: z.string(),
|
||||
NODE_ERROR_COLOUR: z.string(),
|
||||
DEFAULT_SHADOW_COLOR: z.string(),
|
||||
DEFAULT_GROUP_FONT: z.number(),
|
||||
WIDGET_BGCOLOR: z.string(),
|
||||
WIDGET_OUTLINE_COLOR: z.string(),
|
||||
WIDGET_TEXT_COLOR: z.string(),
|
||||
|
||||
@@ -60,7 +60,7 @@ import type {
|
||||
JobListItem
|
||||
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import type { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import type { useAuthStore } from '@/stores/authStore'
|
||||
import type { AuthHeader } from '@/types/authTypes'
|
||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||
import {
|
||||
@@ -332,7 +332,7 @@ export class ComfyApi extends EventTarget {
|
||||
/**
|
||||
* Cache Firebase auth store composable function.
|
||||
*/
|
||||
private authStoreComposable?: typeof useFirebaseAuthStore
|
||||
private authStoreComposable?: typeof useAuthStore
|
||||
|
||||
reportedUnknownMessageTypes = new Set<string>()
|
||||
|
||||
@@ -399,8 +399,8 @@ export class ComfyApi extends EventTarget {
|
||||
private async getAuthStore() {
|
||||
if (isCloud) {
|
||||
if (!this.authStoreComposable) {
|
||||
const module = await import('@/stores/firebaseAuthStore')
|
||||
this.authStoreComposable = module.useFirebaseAuthStore
|
||||
const module = await import('@/stores/authStore')
|
||||
this.authStoreComposable = module.useAuthStore
|
||||
}
|
||||
|
||||
return this.authStoreComposable()
|
||||
|
||||
@@ -67,7 +67,7 @@ import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useMissingNodesErrorStore } from '@/platform/nodeReplacement/missingNodesErrorStore'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { useJobPreviewStore } from '@/stores/jobPreviewStore'
|
||||
import { KeyComboImpl } from '@/platform/keybindings/keyCombo'
|
||||
@@ -1591,7 +1591,7 @@ export class ComfyApp {
|
||||
executionErrorStore.clearAllErrors()
|
||||
|
||||
// Get auth token for backend nodes - uses workspace token if enabled, otherwise Firebase token
|
||||
const comfyOrgAuthToken = await useFirebaseAuthStore().getAuthToken()
|
||||
const comfyOrgAuthToken = await useAuthStore().getAuthToken()
|
||||
const comfyOrgApiKey = useApiKeyAuthStore().getApiKey()
|
||||
|
||||
try {
|
||||
|
||||
@@ -11,7 +11,7 @@ const mockAxiosInstance = vi.hoisted(() => ({
|
||||
get: vi.fn()
|
||||
}))
|
||||
|
||||
const mockFirebaseAuthStore = vi.hoisted(() => ({
|
||||
const mockAuthStore = vi.hoisted(() => ({
|
||||
getAuthHeader: vi.fn()
|
||||
}))
|
||||
|
||||
@@ -27,8 +27,8 @@ vi.mock('axios', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => mockFirebaseAuthStore)
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => mockAuthStore)
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
@@ -81,7 +81,7 @@ describe('useCustomerEventsService', () => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Setup default mocks
|
||||
mockFirebaseAuthStore.getAuthHeader.mockResolvedValue(mockAuthHeaders)
|
||||
mockAuthStore.getAuthHeader.mockResolvedValue(mockAuthHeaders)
|
||||
mockI18n.d.mockImplementation((date, options) => {
|
||||
// Mock i18n date formatting
|
||||
if (options?.month === 'short') {
|
||||
@@ -118,7 +118,7 @@ describe('useCustomerEventsService', () => {
|
||||
limit: 10
|
||||
})
|
||||
|
||||
expect(mockFirebaseAuthStore.getAuthHeader).toHaveBeenCalled()
|
||||
expect(mockAuthStore.getAuthHeader).toHaveBeenCalled()
|
||||
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/customers/events', {
|
||||
params: { page: 1, limit: 10 },
|
||||
headers: mockAuthHeaders
|
||||
@@ -141,7 +141,7 @@ describe('useCustomerEventsService', () => {
|
||||
})
|
||||
|
||||
it('should return null when auth headers are missing', async () => {
|
||||
mockFirebaseAuthStore.getAuthHeader.mockResolvedValue(null)
|
||||
mockAuthStore.getAuthHeader.mockResolvedValue(null)
|
||||
|
||||
const result = await service.getMyEvents()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ref, watch } from 'vue'
|
||||
|
||||
import { getComfyApiBaseUrl } from '@/config/comfyApi'
|
||||
import { d } from '@/i18n'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { components, operations } from '@/types/comfyRegistryTypes'
|
||||
import { isAbortError } from '@/utils/typeGuardUtil'
|
||||
|
||||
@@ -179,7 +179,7 @@ export const useCustomerEventsService = () => {
|
||||
}
|
||||
|
||||
// Get auth headers
|
||||
const authHeaders = await useFirebaseAuthStore().getAuthHeader()
|
||||
const authHeaders = await useAuthStore().getAuthHeader()
|
||||
if (!authHeaders) {
|
||||
error.value = 'Authentication header is missing'
|
||||
return null
|
||||
|
||||
42
src/stores/__tests__/authStoreMock.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { createAuthStoreMock, mockAuthStoreModule } from './authStoreMock'
|
||||
|
||||
describe('authStoreMock', () => {
|
||||
it('creates a mock with reactive computed properties', () => {
|
||||
const { mock, controls } = createAuthStoreMock()
|
||||
|
||||
expect(mock.isAuthenticated).toBe(false)
|
||||
expect(mock.userEmail).toBeNull()
|
||||
expect(mock.userId).toBeNull()
|
||||
|
||||
controls.currentUser.value = { uid: 'u1', email: 'a@b.com' }
|
||||
|
||||
expect(mock.isAuthenticated).toBe(true)
|
||||
expect(mock.userEmail).toBe('a@b.com')
|
||||
expect(mock.userId).toBe('u1')
|
||||
})
|
||||
|
||||
it('starts with clean defaults', () => {
|
||||
const { controls } = createAuthStoreMock()
|
||||
expect(controls.currentUser.value).toBeNull()
|
||||
expect(controls.isInitialized.value).toBe(false)
|
||||
expect(controls.loading.value).toBe(false)
|
||||
expect(controls.balance.value).toBeNull()
|
||||
expect(controls.isFetchingBalance.value).toBe(false)
|
||||
})
|
||||
|
||||
it('creates independent instances per call', () => {
|
||||
const a = createAuthStoreMock()
|
||||
const b = createAuthStoreMock()
|
||||
a.controls.currentUser.value = { uid: 'a' }
|
||||
expect(b.mock.isAuthenticated).toBe(false)
|
||||
})
|
||||
|
||||
it('mockAuthStoreModule wraps mock correctly', () => {
|
||||
const { mock } = createAuthStoreMock()
|
||||
const module = mockAuthStoreModule(mock)
|
||||
expect(module.useAuthStore()).toBe(mock)
|
||||
expect(new module.AuthStoreError('test').name).toBe('AuthStoreError')
|
||||
})
|
||||
})
|
||||
103
src/stores/__tests__/authStoreMock.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { vi } from 'vitest'
|
||||
import type { Mock } from 'vitest'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
/**
|
||||
* Shared mock factory for useAuthStore.
|
||||
*
|
||||
* Usage in test files:
|
||||
* import { createAuthStoreMock, mockAuthStoreModule } from '@/stores/__tests__/authStoreMock'
|
||||
*
|
||||
* const { mock, controls } = createAuthStoreMock()
|
||||
* vi.mock('@/stores/authStore', () => mockAuthStoreModule(mock))
|
||||
*
|
||||
* // Per-test customization:
|
||||
* controls.currentUser.value = { uid: 'test-123', email: 'a@b.com' }
|
||||
* controls.getAuthHeader.mockResolvedValue({ Authorization: 'Bearer tok' })
|
||||
*/
|
||||
|
||||
export interface AuthStoreMockControls {
|
||||
currentUser: ReturnType<typeof ref<Record<string, unknown> | null>>
|
||||
isInitialized: ReturnType<typeof ref<boolean>>
|
||||
loading: ReturnType<typeof ref<boolean>>
|
||||
balance: ReturnType<typeof ref<Record<string, unknown> | null>>
|
||||
isFetchingBalance: ReturnType<typeof ref<boolean>>
|
||||
tokenRefreshTrigger: ReturnType<typeof ref<number>>
|
||||
|
||||
login: Mock
|
||||
register: Mock
|
||||
logout: Mock
|
||||
getIdToken: Mock
|
||||
getAuthHeader: Mock
|
||||
getFirebaseAuthHeader: Mock
|
||||
getAuthToken: Mock
|
||||
createCustomer: Mock
|
||||
fetchBalance: Mock
|
||||
accessBillingPortal: Mock
|
||||
loginWithGoogle: Mock
|
||||
loginWithGithub: Mock
|
||||
sendPasswordReset: Mock
|
||||
updatePassword: Mock
|
||||
initiateCreditPurchase: Mock
|
||||
}
|
||||
|
||||
export function createAuthStoreMock(): {
|
||||
mock: Record<string, unknown>
|
||||
controls: AuthStoreMockControls
|
||||
} {
|
||||
const currentUser = ref<Record<string, unknown> | null>(null)
|
||||
const isInitialized = ref(false)
|
||||
const loading = ref(false)
|
||||
const balance = ref<Record<string, unknown> | null>(null)
|
||||
const isFetchingBalance = ref(false)
|
||||
const tokenRefreshTrigger = ref(0)
|
||||
|
||||
const controls: AuthStoreMockControls = {
|
||||
currentUser,
|
||||
isInitialized,
|
||||
loading,
|
||||
balance,
|
||||
isFetchingBalance,
|
||||
tokenRefreshTrigger,
|
||||
login: vi.fn(),
|
||||
register: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
getIdToken: vi.fn().mockResolvedValue('mock-id-token'),
|
||||
getAuthHeader: vi.fn().mockResolvedValue(null),
|
||||
getFirebaseAuthHeader: vi.fn().mockResolvedValue(null),
|
||||
getAuthToken: vi.fn().mockResolvedValue(undefined),
|
||||
createCustomer: vi.fn(),
|
||||
fetchBalance: vi.fn(),
|
||||
accessBillingPortal: vi.fn(),
|
||||
loginWithGoogle: vi.fn(),
|
||||
loginWithGithub: vi.fn(),
|
||||
sendPasswordReset: vi.fn(),
|
||||
updatePassword: vi.fn(),
|
||||
initiateCreditPurchase: vi.fn()
|
||||
}
|
||||
|
||||
const mock = reactive({
|
||||
...controls,
|
||||
isAuthenticated: computed(() => !!currentUser.value),
|
||||
userEmail: computed(
|
||||
() => (currentUser.value as Record<string, unknown> | null)?.email ?? null
|
||||
),
|
||||
userId: computed(
|
||||
() => (currentUser.value as Record<string, unknown> | null)?.uid ?? null
|
||||
)
|
||||
})
|
||||
|
||||
return { mock, controls }
|
||||
}
|
||||
|
||||
export function mockAuthStoreModule(mock: Record<string, unknown>) {
|
||||
return {
|
||||
useAuthStore: () => mock,
|
||||
AuthStoreError: class extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'AuthStoreError'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { ApiKeyAuthHeader } from '@/types/authTypes'
|
||||
import type { operations } from '@/types/comfyRegistryTypes'
|
||||
|
||||
@@ -15,7 +15,7 @@ type ComfyApiUser =
|
||||
const STORAGE_KEY = 'comfy_api_key'
|
||||
|
||||
export const useApiKeyAuthStore = defineStore('apiKeyAuth', () => {
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const apiKey = useLocalStorage<string | null>(STORAGE_KEY, null)
|
||||
const toastStore = useToastStore()
|
||||
const { wrapWithErrorHandlingAsync, toastErrorHandler } = useErrorHandling()
|
||||
@@ -24,7 +24,7 @@ export const useApiKeyAuthStore = defineStore('apiKeyAuth', () => {
|
||||
const isAuthenticated = computed(() => !!currentUser.value)
|
||||
|
||||
const initializeUserFromApiKey = async () => {
|
||||
const createCustomerResponse = await firebaseAuthStore
|
||||
const createCustomerResponse = await authStore
|
||||
.createCustomer()
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
|
||||
@@ -7,7 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as vuefire from 'vuefire'
|
||||
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
|
||||
// Hoisted mocks for dynamic imports
|
||||
@@ -85,10 +85,19 @@ vi.mock('firebase/auth', async (importOriginal) => {
|
||||
addScope = vi.fn()
|
||||
setCustomParameters = vi.fn()
|
||||
},
|
||||
getAdditionalUserInfo: vi.fn(),
|
||||
setPersistence: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
// Mock telemetry
|
||||
const mockTrackAuth = vi.fn()
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: () => ({
|
||||
trackAuth: mockTrackAuth
|
||||
})
|
||||
}))
|
||||
|
||||
// Mock useToastStore
|
||||
vi.mock('@/stores/toastStore', () => ({
|
||||
useToastStore: () => ({
|
||||
@@ -113,8 +122,8 @@ vi.mock('@/stores/apiKeyAuthStore', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
describe('useFirebaseAuthStore', () => {
|
||||
let store: ReturnType<typeof useFirebaseAuthStore>
|
||||
describe('useAuthStore', () => {
|
||||
let store: ReturnType<typeof useAuthStore>
|
||||
let authStateCallback: (user: User | null) => void
|
||||
let idTokenCallback: (user: User | null) => void
|
||||
|
||||
@@ -173,7 +182,7 @@ describe('useFirebaseAuthStore', () => {
|
||||
|
||||
// Initialize Pinia
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
store = useFirebaseAuthStore()
|
||||
store = useAuthStore()
|
||||
|
||||
// Reset and set up getIdToken mock
|
||||
mockUser.getIdToken.mockReset()
|
||||
@@ -201,8 +210,8 @@ describe('useFirebaseAuthStore', () => {
|
||||
)
|
||||
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
const storeModule = await import('@/stores/firebaseAuthStore')
|
||||
store = storeModule.useFirebaseAuthStore()
|
||||
const storeModule = await import('@/stores/authStore')
|
||||
store = storeModule.useAuthStore()
|
||||
})
|
||||
|
||||
it("should not increment tokenRefreshTrigger on the user's first ID token event", () => {
|
||||
@@ -572,6 +581,82 @@ describe('useFirebaseAuthStore', () => {
|
||||
|
||||
expect(store.loading).toBe(false)
|
||||
})
|
||||
|
||||
describe('sign-up telemetry OR logic', () => {
|
||||
const mockUserCredential = {
|
||||
user: mockUser
|
||||
} as Partial<UserCredential> as UserCredential
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(firebaseAuth.signInWithPopup).mockResolvedValue(
|
||||
mockUserCredential
|
||||
)
|
||||
})
|
||||
|
||||
it.each(['loginWithGoogle', 'loginWithGithub'] as const)(
|
||||
'%s should track is_new_user=true when Firebase says new user',
|
||||
async (method) => {
|
||||
vi.mocked(firebaseAuth.getAdditionalUserInfo).mockReturnValue({
|
||||
isNewUser: true,
|
||||
providerId: 'google.com',
|
||||
profile: null
|
||||
})
|
||||
|
||||
await store[method]()
|
||||
|
||||
expect(mockTrackAuth).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ is_new_user: true })
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['loginWithGoogle', 'loginWithGithub'] as const)(
|
||||
'%s should track is_new_user=true when UI options say new user',
|
||||
async (method) => {
|
||||
vi.mocked(firebaseAuth.getAdditionalUserInfo).mockReturnValue({
|
||||
isNewUser: false,
|
||||
providerId: 'google.com',
|
||||
profile: null
|
||||
})
|
||||
|
||||
await store[method]({ isNewUser: true })
|
||||
|
||||
expect(mockTrackAuth).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ is_new_user: true })
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['loginWithGoogle', 'loginWithGithub'] as const)(
|
||||
'%s should track is_new_user=false when neither source says new user',
|
||||
async (method) => {
|
||||
vi.mocked(firebaseAuth.getAdditionalUserInfo).mockReturnValue({
|
||||
isNewUser: false,
|
||||
providerId: 'google.com',
|
||||
profile: null
|
||||
})
|
||||
|
||||
await store[method]()
|
||||
|
||||
expect(mockTrackAuth).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ is_new_user: false })
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['loginWithGoogle', 'loginWithGithub'] as const)(
|
||||
'%s should track is_new_user=false when getAdditionalUserInfo returns null',
|
||||
async (method) => {
|
||||
vi.mocked(firebaseAuth.getAdditionalUserInfo).mockReturnValue(null)
|
||||
|
||||
await store[method]()
|
||||
|
||||
expect(mockTrackAuth).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ is_new_user: false })
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('accessBillingPortal', () => {
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
GoogleAuthProvider,
|
||||
browserLocalPersistence,
|
||||
createUserWithEmailAndPassword,
|
||||
getAdditionalUserInfo,
|
||||
onAuthStateChanged,
|
||||
onIdTokenChanged,
|
||||
sendPasswordResetEmail,
|
||||
@@ -48,14 +49,14 @@ export type BillingPortalTargetTier = NonNullable<
|
||||
>['application/json']
|
||||
>['target_tier']
|
||||
|
||||
export class FirebaseAuthStoreError extends Error {
|
||||
export class AuthStoreError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'FirebaseAuthStoreError'
|
||||
this.name = 'AuthStoreError'
|
||||
}
|
||||
}
|
||||
|
||||
export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const { flags } = useFeatureFlags()
|
||||
|
||||
// State
|
||||
@@ -240,9 +241,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
try {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.userNotAuthenticated')
|
||||
)
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const response = await fetch(buildApiUrl('/customers/balance'), {
|
||||
@@ -258,7 +257,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
return null
|
||||
}
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToFetchBalance', {
|
||||
error: errorData.message
|
||||
})
|
||||
@@ -278,7 +277,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
const createCustomer = async (): Promise<CreateCustomerResponse> => {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const createCustomerRes = await fetch(buildApiUrl('/customers'), {
|
||||
@@ -289,7 +288,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
}
|
||||
})
|
||||
if (!createCustomerRes.ok) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToCreateCustomer', {
|
||||
error: createCustomerRes.statusText
|
||||
})
|
||||
@@ -299,7 +298,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
const createCustomerResJson: CreateCustomerResponse =
|
||||
await createCustomerRes.json()
|
||||
if (!createCustomerResJson?.id) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToCreateCustomer', {
|
||||
error: 'No customer ID returned'
|
||||
})
|
||||
@@ -386,9 +385,11 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
)
|
||||
|
||||
if (isCloud) {
|
||||
const additionalUserInfo = getAdditionalUserInfo(result)
|
||||
useTelemetry()?.trackAuth({
|
||||
method: 'google',
|
||||
is_new_user: options?.isNewUser ?? false,
|
||||
is_new_user:
|
||||
options?.isNewUser || additionalUserInfo?.isNewUser || false,
|
||||
user_id: result.user.uid
|
||||
})
|
||||
}
|
||||
@@ -405,9 +406,11 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
)
|
||||
|
||||
if (isCloud) {
|
||||
const additionalUserInfo = getAdditionalUserInfo(result)
|
||||
useTelemetry()?.trackAuth({
|
||||
method: 'github',
|
||||
is_new_user: options?.isNewUser ?? false,
|
||||
is_new_user:
|
||||
options?.isNewUser || additionalUserInfo?.isNewUser || false,
|
||||
user_id: result.user.uid
|
||||
})
|
||||
}
|
||||
@@ -426,7 +429,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
/** Update password for current user */
|
||||
const _updatePassword = async (newPassword: string): Promise<void> => {
|
||||
if (!currentUser.value) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
await updatePassword(currentUser.value, newPassword)
|
||||
}
|
||||
@@ -436,7 +439,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
): Promise<CreditPurchaseResponse> => {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
// Ensure customer was created during login/registration
|
||||
@@ -456,7 +459,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToInitiateCreditPurchase', {
|
||||
error: errorData.message
|
||||
})
|
||||
@@ -476,7 +479,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
): Promise<AccessBillingPortalResponse> => {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const response = await fetch(buildApiUrl('/customers/billing'), {
|
||||
@@ -492,7 +495,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
throw new AuthStoreError(
|
||||
t('toastMessages.failedToAccessBillingPortal', {
|
||||
error: errorData.message
|
||||
})
|
||||
@@ -50,12 +50,12 @@ vi.mock('@/stores/userStore', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
const mockIsFirebaseInitialized = ref(false)
|
||||
const mockIsFirebaseAuthenticated = ref(false)
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
isInitialized: mockIsFirebaseInitialized,
|
||||
isAuthenticated: mockIsFirebaseAuthenticated
|
||||
const mockIsAuthInitialized = ref(false)
|
||||
const mockIsAuthAuthenticated = ref(false)
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
isInitialized: mockIsAuthInitialized,
|
||||
isAuthenticated: mockIsAuthAuthenticated
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -67,8 +67,8 @@ vi.mock('@/platform/distribution/types', () => mockDistributionTypes)
|
||||
describe('bootstrapStore', () => {
|
||||
beforeEach(() => {
|
||||
mockIsSettingsReady.value = false
|
||||
mockIsFirebaseInitialized.value = false
|
||||
mockIsFirebaseAuthenticated.value = false
|
||||
mockIsAuthInitialized.value = false
|
||||
mockIsAuthAuthenticated.value = false
|
||||
mockNeedsLogin.value = false
|
||||
mockDistributionTypes.isCloud = false
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
@@ -107,14 +107,14 @@ describe('bootstrapStore', () => {
|
||||
expect(settingStore.isReady).toBe(false)
|
||||
|
||||
// Firebase initialized but user not yet authenticated
|
||||
mockIsFirebaseInitialized.value = true
|
||||
mockIsAuthInitialized.value = true
|
||||
await nextTick()
|
||||
|
||||
expect(store.isI18nReady).toBe(false)
|
||||
expect(settingStore.isReady).toBe(false)
|
||||
|
||||
// User authenticates (e.g. signs in on login page)
|
||||
mockIsFirebaseAuthenticated.value = true
|
||||
mockIsAuthAuthenticated.value = true
|
||||
await bootstrapPromise
|
||||
|
||||
await vi.waitFor(() => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
|
||||
export const useBootstrapStore = defineStore('bootstrap', () => {
|
||||
@@ -37,9 +37,7 @@ export const useBootstrapStore = defineStore('bootstrap', () => {
|
||||
|
||||
async function startStoreBootstrap() {
|
||||
if (isCloud) {
|
||||
const { isInitialized, isAuthenticated } = storeToRefs(
|
||||
useFirebaseAuthStore()
|
||||
)
|
||||
const { isInitialized, isAuthenticated } = storeToRefs(useAuthStore())
|
||||
await until(isInitialized).toBe(true)
|
||||
await until(isAuthenticated).toBe(true)
|
||||
}
|
||||
|
||||
@@ -156,6 +156,16 @@ describe('useSubgraphStore', () => {
|
||||
} as ComfyNodeDefV1)
|
||||
expect(res).toBeTruthy()
|
||||
})
|
||||
it('should return a deep copy from getBlueprint so mutations do not corrupt the cache', async () => {
|
||||
await mockFetch({ 'test.json': mockGraph })
|
||||
const first = store.getBlueprint(store.typePrefix + 'test')
|
||||
first.nodes[0].id = -1
|
||||
first.definitions!.subgraphs![0].id = 'corrupted'
|
||||
|
||||
const second = store.getBlueprint(store.typePrefix + 'test')
|
||||
expect(second.nodes[0].id).not.toBe(-1)
|
||||
expect(second.definitions!.subgraphs![0].id).toBe('123')
|
||||
})
|
||||
it('should identify user blueprints as non-global', async () => {
|
||||
await mockFetch({ 'test.json': mockGraph })
|
||||
expect(store.isGlobalBlueprint('test')).toBe(false)
|
||||
|
||||
@@ -392,7 +392,7 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
if (!(name in subgraphCache))
|
||||
//As loading is blocked on in startup, this can likely be changed to invalid type
|
||||
throw new Error('not yet loaded')
|
||||
return subgraphCache[name].changeTracker.initialState
|
||||
return structuredClone(subgraphCache[name].changeTracker.initialState)
|
||||
}
|
||||
async function deleteBlueprint(nodeType: string) {
|
||||
const name = nodeType.slice(typePrefix.length)
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { SidebarTabExtension, ToastManager } from '@/types/extensionTypes'
|
||||
import { useApiKeyAuthStore } from './apiKeyAuthStore'
|
||||
import { useCommandStore } from './commandStore'
|
||||
import { useExecutionErrorStore } from './executionErrorStore'
|
||||
import { useFirebaseAuthStore } from './firebaseAuthStore'
|
||||
import { useAuthStore } from './authStore'
|
||||
import { useQueueSettingsStore } from './queueStore'
|
||||
import { useBottomPanelStore } from './workspace/bottomPanelStore'
|
||||
import { useSidebarTabStore } from './workspace/sidebarTabStore'
|
||||
@@ -48,7 +48,7 @@ function workspaceStoreSetup() {
|
||||
const dialog = useDialogService()
|
||||
const bottomPanel = useBottomPanelStore()
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const apiKeyStore = useApiKeyAuthStore()
|
||||
|
||||
const firebaseUser = computed(() => authStore.currentUser)
|
||||
|
||||
@@ -78,7 +78,7 @@ import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useAssetsStore } from '@/stores/assetsStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { useModelStore } from '@/stores/modelStore'
|
||||
import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'
|
||||
@@ -120,7 +120,7 @@ watch(linearMode, (isLinear) => {
|
||||
})
|
||||
|
||||
const telemetry = useTelemetry()
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
let hasTrackedLogin = false
|
||||
|
||||
watch(
|
||||
@@ -308,7 +308,7 @@ void nextTick(() => {
|
||||
const onGraphReady = () => {
|
||||
runWhenGlobalIdle(() => {
|
||||
// Track user login when app is ready in graph view (cloud only)
|
||||
if (isCloud && firebaseAuthStore.isAuthenticated && !hasTrackedLogin) {
|
||||
if (isCloud && authStore.isAuthenticated && !hasTrackedLogin) {
|
||||
telemetry?.trackUserLoggedIn()
|
||||
hasTrackedLogin = true
|
||||
}
|
||||
|
||||