Compare commits

..

5 Commits

Author SHA1 Message Date
Rizumu Ayaka
b255fc033e fix: guard against invalid oldPosition in TabGlobalParameters reorder
Add early return when draggableItem is not found in getAllItems(),
consistent with TabSubgraphInputs and preventing splice(-1, 1).
2026-04-10 19:08:55 +08:00
Rizumu Ayaka
8ce5616c98 fix: reset isDragging on guaranteed dragEnd path instead of inside applyNewItemsOrder
Wrap the base dragEnd method to reset isDragging after cleanup,
ensuring the flag is always cleared even if applyNewItemsOrder
returns early (e.g. when draggableItem is not found).
2026-04-10 19:02:51 +08:00
Rizumu Ayaka
665b703ae7 Merge branch 'main' into rizumu/fix/properties-panel-drag-reorder-flicker 2026-04-09 17:23:30 +08:00
Rizumu Ayaka
d9fdfdb6c2 fix: restrict dragAxis to 'y' or 'both' in DraggableList component 2026-04-09 16:55:14 +08:00
Rizumu Ayaka
70aa476060 fix: properties panel drag reorder visual flicker 2026-04-09 16:39:02 +08:00
11 changed files with 245 additions and 679 deletions

View File

@@ -134,9 +134,6 @@ export type {
DeleteHubWorkflowErrors,
DeleteHubWorkflowResponse,
DeleteHubWorkflowResponses,
DeleteMonitoringTasksSubpathData,
DeleteMonitoringTasksSubpathErrors,
DeleteMonitoringTasksSubpathResponses,
DeleteSecretData,
DeleteSecretError,
DeleteSecretErrors,
@@ -199,9 +196,6 @@ export type {
GetAllSettingsErrors,
GetAllSettingsResponse,
GetAllSettingsResponses,
GetApiViewVideoAliasData,
GetApiViewVideoAliasErrors,
GetApiViewVideoAliasResponses,
GetAssetByIdData,
GetAssetByIdError,
GetAssetByIdErrors,
@@ -242,8 +236,6 @@ export type {
GetDeletionRequestErrors,
GetDeletionRequestResponse,
GetDeletionRequestResponses,
GetExtensionsData,
GetExtensionsResponses,
GetFeaturesData,
GetFeaturesResponse,
GetFeaturesResponses,
@@ -262,9 +254,6 @@ export type {
GetGlobalSubgraphsErrors,
GetGlobalSubgraphsResponse,
GetGlobalSubgraphsResponses,
GetHealthData,
GetHealthErrors,
GetHealthResponses,
GetHistoryData,
GetHistoryError,
GetHistoryErrors,
@@ -328,12 +317,6 @@ export type {
GetModelsInFolderErrors,
GetModelsInFolderResponse,
GetModelsInFolderResponses,
GetMonitoringTasksData,
GetMonitoringTasksErrors,
GetMonitoringTasksResponses,
GetMonitoringTasksSubpathData,
GetMonitoringTasksSubpathErrors,
GetMonitoringTasksSubpathResponses,
GetMyHubProfileData,
GetMyHubProfileError,
GetMyHubProfileErrors,
@@ -342,19 +325,11 @@ export type {
GetNodeInfoData,
GetNodeInfoResponse,
GetNodeInfoResponses,
GetOpenapiSpecData,
GetOpenapiSpecResponses,
GetPaymentPortalData,
GetPaymentPortalError,
GetPaymentPortalErrors,
GetPaymentPortalResponse,
GetPaymentPortalResponses,
GetPprofData,
GetPprofProfileData,
GetPprofProfileResponses,
GetPprofResponses,
GetPprofTraceData,
GetPprofTraceResponses,
GetPromptInfoData,
GetPromptInfoError,
GetPromptInfoErrors,
@@ -370,6 +345,11 @@ export type {
GetQueueInfoErrors,
GetQueueInfoResponse,
GetQueueInfoResponses,
GetRawLogsData,
GetRawLogsError,
GetRawLogsErrors,
GetRawLogsResponse,
GetRawLogsResponses,
GetRemoteAssetMetadataData,
GetRemoteAssetMetadataError,
GetRemoteAssetMetadataErrors,
@@ -385,9 +365,6 @@ export type {
GetSettingByKeyErrors,
GetSettingByKeyResponse,
GetSettingByKeyResponses,
GetStaticExtensionsData,
GetStaticExtensionsErrors,
GetStaticExtensionsResponses,
GetSystemStatsData,
GetSystemStatsError,
GetSystemStatsErrors,
@@ -398,8 +375,6 @@ export type {
GetTaskErrors,
GetTaskResponse,
GetTaskResponses,
GetTemplateProxyData,
GetTemplateProxyErrors,
GetUserData,
GetUserdataData,
GetUserdataError,
@@ -422,23 +397,6 @@ export type {
GetUserErrors,
GetUserResponse,
GetUserResponses,
GetUsersRawData,
GetUsersRawErrors,
GetUsersRawResponses,
GetVhsQueryVideoData,
GetVhsQueryVideoErrors,
GetVhsQueryVideoResponses,
GetVhsViewAudioData,
GetVhsViewAudioErrors,
GetVhsViewAudioResponses,
GetVhsViewVideoData,
GetVhsViewVideoErrors,
GetVhsViewVideoResponses,
GetViewCompatAliasData,
GetViewCompatAliasErrors,
GetViewCompatAliasResponses,
GetWebsocketData,
GetWebsocketErrors,
GetWorkflowContentData,
GetWorkflowContentError,
GetWorkflowContentErrors,
@@ -568,6 +526,7 @@ export type {
ListWorkspacesResponse2,
ListWorkspacesResponses,
LogsResponse,
LogsSubscribeRequest,
ManageHistoryData,
ManageHistoryError,
ManageHistoryErrors,
@@ -601,11 +560,6 @@ export type {
PostAssetsFromWorkflowErrors,
PostAssetsFromWorkflowResponse,
PostAssetsFromWorkflowResponses,
PostMonitoringTasksSubpathData,
PostMonitoringTasksSubpathErrors,
PostMonitoringTasksSubpathResponses,
PostPprofSymbolData,
PostPprofSymbolResponses,
PostUserdataFileData,
PostUserdataFileError,
PostUserdataFileErrors,
@@ -639,6 +593,7 @@ export type {
QueueInfo,
QueueManageRequest,
QueueManageResponse,
RawLogsResponse,
RemoveAssetTagsData,
RemoveAssetTagsError,
RemoveAssetTagsErrors,
@@ -694,6 +649,11 @@ export type {
SubscribeResponse,
SubscribeResponse2,
SubscribeResponses,
SubscribeToLogsData,
SubscribeToLogsError,
SubscribeToLogsErrors,
SubscribeToLogsResponse,
SubscribeToLogsResponses,
SubscriptionDuration,
SubscriptionTier,
SystemStatsResponse,

View File

@@ -1961,6 +1961,35 @@ export type SystemStatsResponse = {
}>
}
export type LogsSubscribeRequest = {
/**
* Whether to enable or disable log subscription
*/
enabled: boolean
}
/**
* Raw logs response with entries and size
*/
export type RawLogsResponse = {
entries?: Array<{
/**
* Log message
*/
m?: string
}>
size?: {
/**
* Terminal column size
*/
cols?: number
/**
* Terminal row size
*/
rows?: number
}
}
/**
* System logs response
*/
@@ -5247,7 +5276,7 @@ export type GetLogsData = {
body?: never
path?: never
query?: never
url: '/api/logs'
url: '/internal/logs'
}
export type GetLogsErrors = {
@@ -5268,6 +5297,67 @@ export type GetLogsResponses = {
export type GetLogsResponse = GetLogsResponses[keyof GetLogsResponses]
export type GetRawLogsData = {
body?: never
path?: never
query?: never
url: '/internal/logs/raw'
}
export type GetRawLogsErrors = {
/**
* Unauthorized
*/
401: ErrorResponse
}
export type GetRawLogsError = GetRawLogsErrors[keyof GetRawLogsErrors]
export type GetRawLogsResponses = {
/**
* Success
*/
200: RawLogsResponse
}
export type GetRawLogsResponse = GetRawLogsResponses[keyof GetRawLogsResponses]
export type SubscribeToLogsData = {
body: LogsSubscribeRequest
path?: never
query?: never
url: '/internal/logs/subscribe'
}
export type SubscribeToLogsErrors = {
/**
* Bad request
*/
400: ErrorResponse
/**
* Unauthorized
*/
401: ErrorResponse
}
export type SubscribeToLogsError =
SubscribeToLogsErrors[keyof SubscribeToLogsErrors]
export type SubscribeToLogsResponses = {
/**
* Success
*/
200: {
/**
* Whether logs subscription is enabled
*/
enabled?: boolean
}
}
export type SubscribeToLogsResponse =
SubscribeToLogsResponses[keyof SubscribeToLogsResponses]
export type GetSystemStatsData = {
body?: never
path?: never
@@ -7602,444 +7692,3 @@ export type GetPublishedWorkflowResponses = {
export type GetPublishedWorkflowResponse =
GetPublishedWorkflowResponses[keyof GetPublishedWorkflowResponses]
export type GetExtensionsData = {
body?: never
path?: never
query?: never
url: '/api/extensions'
}
export type GetExtensionsResponses = {
/**
* JSON array of extension file paths
*/
200: unknown
}
export type GetVhsViewVideoData = {
body?: never
path?: never
query: {
/**
* Name of the video file to view
*/
filename: string
/**
* Type of file (e.g., output, input, temp)
*/
type?: string
/**
* Subfolder path where the file is located
*/
subfolder?: string
}
url: '/api/vhs/viewvideo'
}
export type GetVhsViewVideoErrors = {
/**
* Unauthorized
*/
401: unknown
}
export type GetVhsViewVideoResponses = {
/**
* Video stream
*/
200: unknown
}
export type GetVhsViewAudioData = {
body?: never
path?: never
query: {
/**
* Name of the audio file to view
*/
filename: string
/**
* Type of file (e.g., output, input, temp)
*/
type?: string
/**
* Subfolder path where the file is located
*/
subfolder?: string
}
url: '/api/vhs/viewaudio'
}
export type GetVhsViewAudioErrors = {
/**
* Unauthorized
*/
401: unknown
}
export type GetVhsViewAudioResponses = {
/**
* Audio stream
*/
200: unknown
}
export type GetVhsQueryVideoData = {
body?: never
path?: never
query: {
/**
* Name of the video file to query
*/
filename: string
}
url: '/api/vhs/queryvideo'
}
export type GetVhsQueryVideoErrors = {
/**
* Unauthorized
*/
401: unknown
}
export type GetVhsQueryVideoResponses = {
/**
* Video metadata
*/
200: unknown
}
export type GetUsersRawData = {
body?: never
path?: never
query?: never
url: '/api/users'
}
export type GetUsersRawErrors = {
/**
* Unauthorized
*/
401: unknown
}
export type GetUsersRawResponses = {
/**
* User list
*/
200: unknown
}
export type GetApiViewVideoAliasData = {
body?: never
path?: never
query: {
/**
* Name of the file to view (see `/api/view` for the full handler contract)
*/
filename: string
}
url: '/api/viewvideo'
}
export type GetApiViewVideoAliasErrors = {
/**
* Unauthorized
*/
401: unknown
}
export type GetApiViewVideoAliasResponses = {
/**
* File stream
*/
200: unknown
}
export type GetViewCompatAliasData = {
body?: never
path?: never
query: {
/**
* Name of the file to view (see `/api/view` for the full handler contract)
*/
filename: string
}
url: '/view'
}
export type GetViewCompatAliasErrors = {
/**
* Unauthorized
*/
401: unknown
}
export type GetViewCompatAliasResponses = {
/**
* File stream
*/
200: unknown
}
export type GetWebsocketData = {
body?: never
path?: never
query?: {
/**
* Stable client identifier used to associate the WebSocket
* connection with the frontend session. If omitted, the server
* generates one.
*
*/
clientId?: string
}
url: '/ws'
}
export type GetWebsocketErrors = {
/**
* Unauthorized
*/
401: unknown
}
export type GetTemplateProxyData = {
body?: never
path: {
path: string
}
query?: never
url: '/templates/{path}'
}
export type GetTemplateProxyErrors = {
/**
* Template not found
*/
404: unknown
}
export type GetHealthData = {
body?: never
path?: never
query?: never
url: '/health'
}
export type GetHealthErrors = {
/**
* Service is unhealthy
*/
503: unknown
}
export type GetHealthResponses = {
/**
* Service is healthy
*/
200: unknown
}
export type GetOpenapiSpecData = {
body?: never
path?: never
query?: never
url: '/openapi'
}
export type GetOpenapiSpecResponses = {
/**
* OpenAPI specification document
*/
200: unknown
}
export type GetMonitoringTasksData = {
body?: never
path?: never
query?: never
url: '/monitoring/tasks'
}
export type GetMonitoringTasksErrors = {
/**
* Unauthorized
*/
401: unknown
/**
* Forbidden
*/
403: unknown
}
export type GetMonitoringTasksResponses = {
/**
* HTML dashboard
*/
200: unknown
}
export type DeleteMonitoringTasksSubpathData = {
body?: never
path: {
path: string
}
query?: never
url: '/monitoring/tasks/{path}'
}
export type DeleteMonitoringTasksSubpathErrors = {
/**
* Unauthorized
*/
401: unknown
/**
* Forbidden
*/
403: unknown
}
export type DeleteMonitoringTasksSubpathResponses = {
/**
* Deletion result
*/
200: unknown
}
export type GetMonitoringTasksSubpathData = {
body?: never
path: {
path: string
}
query?: never
url: '/monitoring/tasks/{path}'
}
export type GetMonitoringTasksSubpathErrors = {
/**
* Unauthorized
*/
401: unknown
/**
* Forbidden
*/
403: unknown
}
export type GetMonitoringTasksSubpathResponses = {
/**
* Subpath response (asynqmon-determined content type)
*/
200: unknown
}
export type PostMonitoringTasksSubpathData = {
body?: never
path: {
path: string
}
query?: never
url: '/monitoring/tasks/{path}'
}
export type PostMonitoringTasksSubpathErrors = {
/**
* Unauthorized
*/
401: unknown
/**
* Forbidden
*/
403: unknown
}
export type PostMonitoringTasksSubpathResponses = {
/**
* Action result
*/
200: unknown
}
export type GetPprofData = {
body?: never
path: {
path: string
}
query?: never
url: '/debug/pprof/{path}'
}
export type GetPprofResponses = {
/**
* Profile data
*/
200: unknown
}
export type GetPprofProfileData = {
body?: never
path?: never
query?: never
url: '/debug/pprof/profile'
}
export type GetPprofProfileResponses = {
/**
* CPU profile data
*/
200: unknown
}
export type GetPprofTraceData = {
body?: never
path?: never
query?: never
url: '/debug/pprof/trace'
}
export type GetPprofTraceResponses = {
/**
* Execution trace data
*/
200: unknown
}
export type PostPprofSymbolData = {
body?: never
path?: never
query?: never
url: '/debug/pprof/symbol'
}
export type PostPprofSymbolResponses = {
/**
* Resolved symbols
*/
200: unknown
}
export type GetStaticExtensionsData = {
body?: never
path: {
path: string
}
query?: never
url: '/extensions/{path}'
}
export type GetStaticExtensionsErrors = {
/**
* File not found
*/
404: unknown
}
export type GetStaticExtensionsResponses = {
/**
* Static file
*/
200: unknown
}

View File

@@ -1070,6 +1070,29 @@ export const zSystemStatsResponse = z.object({
)
})
export const zLogsSubscribeRequest = z.object({
enabled: z.boolean()
})
/**
* Raw logs response with entries and size
*/
export const zRawLogsResponse = z.object({
entries: z
.array(
z.object({
m: z.string().optional()
})
)
.optional(),
size: z
.object({
cols: z.number().int().optional(),
rows: z.number().int().optional()
})
.optional()
})
/**
* System logs response
*/
@@ -2225,6 +2248,30 @@ export const zGetLogsData = z.object({
*/
export const zGetLogsResponse = zLogsResponse
export const zGetRawLogsData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.never().optional()
})
/**
* Success
*/
export const zGetRawLogsResponse = zRawLogsResponse
export const zSubscribeToLogsData = z.object({
body: zLogsSubscribeRequest,
path: z.never().optional(),
query: z.never().optional()
})
/**
* Success
*/
export const zSubscribeToLogsResponse = z.object({
enabled: z.boolean().optional()
})
export const zGetSystemStatsData = z.object({
body: z.never().optional(),
path: z.never().optional(),
@@ -2984,153 +3031,3 @@ export const zGetPublishedWorkflowData = z.object({
* Published workflow details with asset statuses
*/
export const zGetPublishedWorkflowResponse = zPublishedWorkflowDetail
export const zGetExtensionsData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.never().optional()
})
export const zGetVhsViewVideoData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.object({
filename: z.string(),
type: z.string().optional(),
subfolder: z.string().optional()
})
})
export const zGetVhsViewAudioData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.object({
filename: z.string(),
type: z.string().optional(),
subfolder: z.string().optional()
})
})
export const zGetVhsQueryVideoData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.object({
filename: z.string()
})
})
export const zGetUsersRawData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.never().optional()
})
export const zGetApiViewVideoAliasData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.object({
filename: z.string()
})
})
export const zGetViewCompatAliasData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.object({
filename: z.string()
})
})
export const zGetWebsocketData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z
.object({
clientId: z.string().optional()
})
.optional()
})
export const zGetTemplateProxyData = z.object({
body: z.never().optional(),
path: z.object({
path: z.string()
}),
query: z.never().optional()
})
export const zGetHealthData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.never().optional()
})
export const zGetOpenapiSpecData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.never().optional()
})
export const zGetMonitoringTasksData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.never().optional()
})
export const zDeleteMonitoringTasksSubpathData = z.object({
body: z.never().optional(),
path: z.object({
path: z.string()
}),
query: z.never().optional()
})
export const zGetMonitoringTasksSubpathData = z.object({
body: z.never().optional(),
path: z.object({
path: z.string()
}),
query: z.never().optional()
})
export const zPostMonitoringTasksSubpathData = z.object({
body: z.never().optional(),
path: z.object({
path: z.string()
}),
query: z.never().optional()
})
export const zGetPprofData = z.object({
body: z.never().optional(),
path: z.object({
path: z.string()
}),
query: z.never().optional()
})
export const zGetPprofProfileData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.never().optional()
})
export const zGetPprofTraceData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.never().optional()
})
export const zPostPprofSymbolData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.never().optional()
})
export const zGetStaticExtensionsData = z.object({
body: z.never().optional(),
path: z.object({
path: z.string()
}),
query: z.never().optional()
})

View File

@@ -3,6 +3,10 @@ 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')
@@ -13,7 +17,8 @@ watchPostEffect(() => {
if (!draggableItems.value?.children?.length) return
draggableList.value = new DraggableList(
draggableItems.value,
'.draggable-item'
'.draggable-item',
{ dragAxis }
)
draggableList.value.applyNewItemsOrder = function () {
const reorderedItems = []

View File

@@ -159,7 +159,7 @@ import { useI18n } from 'vue-i18n'
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
import { useExternalLink } from '@/composables/useExternalLink'
import { isCloud, isDesktop, isNightly } from '@/platform/distribution/types'
import { isCloud, isDesktop } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import type { ReleaseNote } from '@/platform/updates/common/releaseService'
@@ -299,18 +299,9 @@ const menuItems = computed<MenuItem[]>(() => {
type: 'item',
icon: 'icon-[lucide--clipboard-pen]',
label: t('helpCenter.feedback'),
showExternalIcon: isCloud || isNightly,
action: () => {
trackResourceClick('help_feedback', isCloud || isNightly)
if (isCloud || isNightly) {
window.open(
'https://form.typeform.com/to/q7azbWPi',
'_blank',
'noopener,noreferrer'
)
} else {
void commandStore.execute('Comfy.ContactSupport')
}
trackResourceClick('help_feedback', false)
void commandStore.execute('Comfy.ContactSupport')
emit('close')
}
},

View File

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

View File

@@ -28,6 +28,7 @@ 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
@@ -56,8 +57,29 @@ function setDraggableState() {
const container = sectionWidgetsRef.value?.widgetsContainer
if (isSearching.value || !container?.children?.length) return
draggableList.value = new DraggableList(container, '.draggable-item')
draggableList.value = new DraggableList(container, '.draggable-item', {
dragAxis: 'y'
})
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[] = []
@@ -75,6 +97,8 @@ 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') {
@@ -85,11 +109,13 @@ function setDraggableState() {
const newPosition = reorderedItems.indexOf(
this.draggableItem as HTMLElement
)
const widgets = [...searchedFavoritedWidgets.value]
const [widget] = widgets.splice(oldPosition, 1)
widgets.splice(newPosition, 0, widget)
searchedFavoritedWidgets.value = widgets
favoritedWidgetsStore.reorderFavorites(widgets)
if (oldPosition !== newPosition) {
const widgets = [...searchedFavoritedWidgets.value]
const [widget] = widgets.splice(oldPosition, 1)
widgets.splice(newPosition, 0, widget)
searchedFavoritedWidgets.value = widgets
favoritedWidgetsStore.reorderFavorites(widgets)
}
}
}
@@ -131,6 +157,7 @@ function onCollapseUpdate() {
:label
:widgets="searchedFavoritedWidgets"
:is-draggable="!isSearching"
:is-dragging="isDragging"
hidden-favorite-indicator
show-node-name
enable-empty-state

View File

@@ -52,6 +52,7 @@ const isAllCollapsed = computed({
}
})
const draggableList = ref<DraggableList | undefined>(undefined)
const isDragging = ref(false)
const sectionWidgetsRef = useTemplateRef('sectionWidgetsRef')
const advancedInputsSectionRef = useTemplateRef('advancedInputsSectionRef')
@@ -155,8 +156,29 @@ function setDraggableState() {
const container = sectionWidgetsRef.value?.widgetsContainer
if (isSearching.value || !container?.children?.length) return
draggableList.value = new DraggableList(container, '.draggable-item')
draggableList.value = new DraggableList(container, '.draggable-item', {
dragAxis: 'y'
})
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[] = []
@@ -189,14 +211,15 @@ function setDraggableState() {
const newPosition = reorderedItems.indexOf(
this.draggableItem as HTMLElement
)
promotionStore.movePromotion(
node.rootGraph.id,
node.id,
oldPosition,
newPosition
)
canvasStore.canvas?.setDirty(true, true)
if (oldPosition !== newPosition) {
promotionStore.movePromotion(
node.rootGraph.id,
node.id,
oldPosition,
newPosition
)
canvasStore.canvas?.setDirty(true, true)
}
}
}
@@ -236,6 +259,7 @@ const label = computed(() => {
:parents
:widgets="searchedWidgetsList"
:is-draggable="!isSearching"
:is-dragging="isDragging"
:enable-empty-state="isSearching"
:tooltip="
isSearching || searchedWidgetsList.length

View File

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

View File

@@ -1,9 +1,10 @@
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { buildFeedbackUrl } from '@/platform/support/config'
import { useExtensionService } from '@/services/extensionService'
import type { ActionBarButton } from '@/types/comfy'
const TYPEFORM_SURVEY_URL = 'https://form.typeform.com/to/q7azbWPi'
const feedbackUrl = buildFeedbackUrl()
const buttons: ActionBarButton[] = [
{
@@ -11,7 +12,7 @@ const buttons: ActionBarButton[] = [
label: t('actionbar.feedback'),
tooltip: t('actionbar.feedbackTooltip'),
onClick: () => {
window.open(TYPEFORM_SURVEY_URL, '_blank', 'noopener,noreferrer')
window.open(feedbackUrl, '_blank', 'noopener,noreferrer')
}
}
]

View File

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