mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-24 00:09:32 +00:00
Compare commits
2 Commits
rizumu/fix
...
fix/worksp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1174aff0ae | ||
|
|
a5e323cdaa |
@@ -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 = []
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])"
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user