Compare commits

..

2 Commits

Author SHA1 Message Date
dante01yoon
1174aff0ae fix: make workspace_id optional for profile creation
The workspace ID is only available when teamWorkspacesEnabled is true
and the backend has provisioned workspaces for the user. For users
without workspace context, send the profile creation request without
workspace_id and let the backend resolve it from the auth token.
2026-04-10 06:56:07 +09:00
dante01yoon
a5e323cdaa fix: read workspace ID from store instead of sessionStorage
The createProfile flow read workspace ID directly from sessionStorage,
which is only populated when the teamWorkspaces feature flag is enabled.
For users without the flag, sessionStorage never contains the workspace
entry, causing "Unable to determine current workspace" errors.

Read from workspaceAuthStore.currentWorkspace instead, which is the
canonical source of workspace state.

Fixes the creator profile creation error reported by multiple users.
2026-04-10 06:36:03 +09:00
9 changed files with 66 additions and 133 deletions

View File

@@ -3,10 +3,6 @@ import { onBeforeUnmount, ref, useTemplateRef, watchPostEffect } from 'vue'
import { DraggableList } from '@/scripts/ui/draggableList'
const { dragAxis } = defineProps<{
dragAxis?: 'y' | 'both'
}>()
const modelValue = defineModel<T[]>({ required: true })
const draggableList = ref<DraggableList>()
const draggableItems = useTemplateRef('draggableItems')
@@ -17,8 +13,7 @@ watchPostEffect(() => {
if (!draggableItems.value?.children?.length) return
draggableList.value = new DraggableList(
draggableItems.value,
'.draggable-item',
{ dragAxis }
'.draggable-item'
)
draggableList.value.applyNewItemsOrder = function () {
const reorderedItems = []

View File

@@ -31,7 +31,6 @@ const {
widgets: widgetsProp,
showLocateButton = false,
isDraggable = false,
isDragging = false,
hiddenFavoriteIndicator = false,
showNodeName = false,
parents = [],
@@ -44,7 +43,6 @@ const {
widgets: { widget: IBaseWidget; node: LGraphNode }[]
showLocateButton?: boolean
isDraggable?: boolean
isDragging?: boolean
hiddenFavoriteIndicator?: boolean
showNodeName?: boolean
/**
@@ -274,7 +272,7 @@ defineExpose({
ref="widgetsContainer"
class="relative space-y-2 rounded-lg px-4 pt-1"
>
<TransitionGroup :name="isDragging ? undefined : 'list-scale'">
<TransitionGroup name="list-scale">
<WidgetItem
v-for="{ widget, node } in widgets"
:key="getStableWidgetRenderKey(widget)"

View File

@@ -28,7 +28,6 @@ const { t } = useI18n()
const draggableList = ref<DraggableList | undefined>(undefined)
const sectionWidgetsRef = ref<{ widgetsContainer: HTMLElement }>()
const isSearching = ref(false)
const isDragging = ref(false)
const favoritedWidgets = computed(
() => favoritedWidgetsStore.validFavoritedWidgets
@@ -57,29 +56,8 @@ function setDraggableState() {
const container = sectionWidgetsRef.value?.widgetsContainer
if (isSearching.value || !container?.children?.length) return
draggableList.value = new DraggableList(container, '.draggable-item', {
dragAxis: 'y'
})
draggableList.value = new DraggableList(container, '.draggable-item')
draggableList.value.addEventListener('dragstart', () => {
isDragging.value = true
})
const baseDragEnd = draggableList.value.dragEnd
draggableList.value.dragEnd = function () {
baseDragEnd.call(this)
nextTick(() => {
isDragging.value = false
})
}
/**
* Override to skip the base class's DOM `appendChild` reorder, which breaks
* Vue's vdom tracking inside <TransitionGroup> fragments. Instead, only
* update reactive state and let Vue handle the DOM reconciliation.
* TransitionGroup's move animation is suppressed via the `isDragging` prop
* on SectionWidgets to prevent the FLIP "snap-back" effect.
*/
draggableList.value.applyNewItemsOrder = function () {
const reorderedItems: HTMLElement[] = []
@@ -97,8 +75,6 @@ function setDraggableState() {
reorderedItems[newIndex] = item
})
if (oldPosition === -1) return
for (let index = 0; index < this.getAllItems().length; index++) {
const item = reorderedItems[index]
if (typeof item === 'undefined') {
@@ -109,13 +85,11 @@ function setDraggableState() {
const newPosition = reorderedItems.indexOf(
this.draggableItem as HTMLElement
)
if (oldPosition !== newPosition) {
const widgets = [...searchedFavoritedWidgets.value]
const [widget] = widgets.splice(oldPosition, 1)
widgets.splice(newPosition, 0, widget)
searchedFavoritedWidgets.value = widgets
favoritedWidgetsStore.reorderFavorites(widgets)
}
const widgets = [...searchedFavoritedWidgets.value]
const [widget] = widgets.splice(oldPosition, 1)
widgets.splice(newPosition, 0, widget)
searchedFavoritedWidgets.value = widgets
favoritedWidgetsStore.reorderFavorites(widgets)
}
}
@@ -157,7 +131,6 @@ function onCollapseUpdate() {
:label
:widgets="searchedFavoritedWidgets"
:is-draggable="!isSearching"
:is-dragging="isDragging"
hidden-favorite-indicator
show-node-name
enable-empty-state

View File

@@ -52,7 +52,6 @@ const isAllCollapsed = computed({
}
})
const draggableList = ref<DraggableList | undefined>(undefined)
const isDragging = ref(false)
const sectionWidgetsRef = useTemplateRef('sectionWidgetsRef')
const advancedInputsSectionRef = useTemplateRef('advancedInputsSectionRef')
@@ -156,29 +155,8 @@ function setDraggableState() {
const container = sectionWidgetsRef.value?.widgetsContainer
if (isSearching.value || !container?.children?.length) return
draggableList.value = new DraggableList(container, '.draggable-item', {
dragAxis: 'y'
})
draggableList.value = new DraggableList(container, '.draggable-item')
draggableList.value.addEventListener('dragstart', () => {
isDragging.value = true
})
const baseDragEnd = draggableList.value.dragEnd
draggableList.value.dragEnd = function () {
baseDragEnd.call(this)
nextTick(() => {
isDragging.value = false
})
}
/**
* Override to skip the base class's DOM `appendChild` reorder, which breaks
* Vue's vdom tracking inside <TransitionGroup> fragments. Instead, only
* update reactive state and let Vue handle the DOM reconciliation.
* TransitionGroup's move animation is suppressed via the `isDragging` prop
* on SectionWidgets to prevent the FLIP "snap-back" effect.
*/
draggableList.value.applyNewItemsOrder = function () {
const reorderedItems: HTMLElement[] = []
@@ -211,15 +189,14 @@ function setDraggableState() {
const newPosition = reorderedItems.indexOf(
this.draggableItem as HTMLElement
)
if (oldPosition !== newPosition) {
promotionStore.movePromotion(
node.rootGraph.id,
node.id,
oldPosition,
newPosition
)
canvasStore.canvas?.setDirty(true, true)
}
promotionStore.movePromotion(
node.rootGraph.id,
node.id,
oldPosition,
newPosition
)
canvasStore.canvas?.setDirty(true, true)
}
}
@@ -259,7 +236,6 @@ const label = computed(() => {
:parents
:widgets="searchedWidgetsList"
:is-draggable="!isSearching"
:is-dragging="isDragging"
:enable-empty-state="isSearching"
:tooltip="
isSearching || searchedWidgetsList.length

View File

@@ -259,11 +259,7 @@ onMounted(() => {
{{ $t('subgraphStore.hideAll') }}</a
>
</div>
<DraggableList
v-slot="{ dragClass }"
v-model="activeWidgets"
drag-axis="y"
>
<DraggableList v-slot="{ dragClass }" v-model="activeWidgets">
<SubgraphNodeWidget
v-for="[node, widget] in filteredActive"
:key="toKey([node, widget])"

View File

@@ -10,6 +10,14 @@ const mockToastErrorHandler = vi.hoisted(() => vi.fn())
const mockResolvedUserInfo = vi.hoisted(() => ({
value: { id: 'user-a' }
}))
const mockCurrentWorkspace = vi.hoisted(() => ({
value: {
id: 'workspace-1',
type: 'team',
name: 'Test Workspace',
role: 'owner'
} as { id: string; type: string; name: string; role: string } | null
}))
vi.mock('@/platform/workflow/sharing/services/comfyHubService', () => ({
useComfyHubService: () => ({
@@ -32,6 +40,14 @@ vi.mock('@/composables/useErrorHandling', () => ({
})
}))
vi.mock('@/platform/workspace/stores/workspaceAuthStore', () => ({
useWorkspaceAuthStore: () => ({
get currentWorkspace() {
return mockCurrentWorkspace.value
}
})
}))
// Must import after vi.mock declarations
const { useComfyHubProfileGate } = await import('./useComfyHubProfileGate')
@@ -41,25 +57,18 @@ const mockProfile: ComfyHubProfile = {
description: 'A test profile'
}
function setCurrentWorkspace(workspaceId: string) {
sessionStorage.setItem(
'Comfy.Workspace.Current',
JSON.stringify({
id: workspaceId,
type: 'team',
name: 'Test Workspace',
role: 'owner'
})
)
}
describe('useComfyHubProfileGate', () => {
let gate: ReturnType<typeof useComfyHubProfileGate>
beforeEach(() => {
vi.clearAllMocks()
mockResolvedUserInfo.value = { id: 'user-a' }
setCurrentWorkspace('workspace-1')
mockCurrentWorkspace.value = {
id: 'workspace-1',
type: 'team',
name: 'Test Workspace',
role: 'owner'
}
mockGetMyProfile.mockResolvedValue(mockProfile)
mockRequestAssetUploadUrl.mockResolvedValue({
uploadUrl: 'https://upload.example.com/avatar.png',
@@ -193,5 +202,22 @@ describe('useComfyHubProfileGate', () => {
expect(requestCallOrder[0]).toBeLessThan(uploadCallOrder[0])
expect(uploadCallOrder[0]).toBeLessThan(createCallOrder[0])
})
it('creates profile without workspace_id when workspace is not available', async () => {
mockCurrentWorkspace.value = null
await gate.createProfile({
username: 'testuser',
name: 'Test User'
})
expect(mockCreateProfile).toHaveBeenCalledWith({
workspaceId: undefined,
username: 'testuser',
displayName: 'Test User',
description: undefined,
avatarToken: undefined
})
})
})
})

View File

@@ -3,7 +3,7 @@ import { ref } from 'vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useComfyHubService } from '@/platform/workflow/sharing/services/comfyHubService'
import { WORKSPACE_STORAGE_KEYS } from '@/platform/workspace/workspaceConstants'
import { useWorkspaceAuthStore } from '@/platform/workspace/stores/workspaceAuthStore'
import type { ComfyHubProfile } from '@/schemas/apiSchema'
// TODO: Migrate to a Pinia store for proper singleton state management
@@ -15,34 +15,6 @@ const profile = ref<ComfyHubProfile | null>(null)
const cachedUserId = ref<string | null>(null)
let inflightFetch: Promise<ComfyHubProfile | null> | null = null
function getCurrentWorkspaceId(): string {
const workspaceJson = sessionStorage.getItem(
WORKSPACE_STORAGE_KEYS.CURRENT_WORKSPACE
)
if (!workspaceJson) {
throw new Error('Unable to determine current workspace')
}
let workspace: unknown
try {
workspace = JSON.parse(workspaceJson)
} catch {
throw new Error('Unable to determine current workspace')
}
if (
!workspace ||
typeof workspace !== 'object' ||
!('id' in workspace) ||
typeof workspace.id !== 'string' ||
workspace.id.length === 0
) {
throw new Error('Unable to determine current workspace')
}
return workspace.id
}
export function useComfyHubProfileGate() {
const { resolvedUserInfo } = useCurrentUser()
const { toastErrorHandler } = useErrorHandling()
@@ -122,6 +94,9 @@ export function useComfyHubProfileGate() {
}): Promise<ComfyHubProfile> {
syncCachedProfileWithCurrentUser()
const workspaceAuthStore = useWorkspaceAuthStore()
const workspaceId = workspaceAuthStore.currentWorkspace?.id
let avatarToken: string | undefined
if (data.profilePicture) {
const contentType = data.profilePicture.type || 'application/octet-stream'
@@ -140,7 +115,7 @@ export function useComfyHubProfileGate() {
}
const createdProfile = await createComfyHubProfile({
workspaceId: getCurrentWorkspaceId(),
workspaceId,
username: data.username,
displayName: data.name,
description: data.description,

View File

@@ -12,7 +12,7 @@ type HubThumbnailType = 'image' | 'video' | 'image_comparison'
type ThumbnailTypeInput = HubThumbnailType | 'imageComparison'
interface CreateProfileInput {
workspaceId: string
workspaceId?: string
username: string
displayName?: string
description?: string

View File

@@ -53,19 +53,14 @@ export class DraggableList extends EventTarget {
items = []
itemSelector
handleClass = 'drag-handle'
dragAxis: 'y' | 'both' = 'both'
off = []
offDrag = []
constructor(
element: HTMLElement,
itemSelector: string,
options?: { dragAxis?: 'y' | 'both' }
) {
// @ts-expect-error fixme ts strict error
constructor(element, itemSelector) {
super()
this.listContainer = element
this.itemSelector = itemSelector
if (options?.dragAxis) this.dragAxis = options.dragAxis
if (!this.listContainer) return
@@ -208,8 +203,7 @@ export class DraggableList extends EventTarget {
this.listContainer.scrollBy(0, -10)
}
const pointerOffsetX =
this.dragAxis === 'y' ? 0 : clientX - this.pointerStartX
const pointerOffsetX = clientX - this.pointerStartX
const pointerOffsetY = clientY - this.pointerStartY
this.updateIdleItemsStateAndPosition()