mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 17:10:06 +00:00
## Summary Enable `better-tailwindcss/enforce-consistent-class-order` lint rule and auto-fix all 1027 violations across 263 files. Stacked on #9427. ## Changes - **What**: Sort Tailwind classes into consistent order via `eslint --fix` - Enable `enforce-consistent-class-order` as `'error'` in eslint config - Purely cosmetic reordering — no behavioral or visual changes ## Review Focus Mechanical auto-fix PR — all changes are class reordering only. This is the largest diff but lowest risk since it changes no class names, only their order. **Stack:** #9417 → #9427 → **this PR** Fixes #9300 (partial — 3 of 3 rules) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9428-fix-enable-enforce-consistent-class-order-tailwind-lint-rule-31a6d73d3650811c9065f5178ba3e724) by [Unito](https://www.unito.io)
349 lines
11 KiB
Vue
349 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import { useEventListener, useTimeout } from '@vueuse/core'
|
|
import { partition, remove, takeWhile } from 'es-toolkit'
|
|
import { storeToRefs } from 'pinia'
|
|
import { computed, ref, shallowRef } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import Loader from '@/components/common/Loader.vue'
|
|
import ScrubableNumberInput from '@/components/common/ScrubableNumberInput.vue'
|
|
import Popover from '@/components/ui/Popover.vue'
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import { extractVueNodeData } from '@/composables/graph/useGraphNodeManager'
|
|
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
|
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
|
import { appendCloudResParam } from '@/platform/distribution/cloudPreviewUtil'
|
|
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
|
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
import { useTelemetry } from '@/platform/telemetry'
|
|
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
|
import DropZone from '@/renderer/extensions/linearMode/DropZone.vue'
|
|
import NodeWidgets from '@/renderer/extensions/vueNodes/components/NodeWidgets.vue'
|
|
import { api } from '@/scripts/api'
|
|
import { app } from '@/scripts/app'
|
|
import { useCommandStore } from '@/stores/commandStore'
|
|
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
|
import { useQueueSettingsStore } from '@/stores/queueStore'
|
|
import { cn } from '@/utils/tailwindUtil'
|
|
import { useAppMode } from '@/composables/useAppMode'
|
|
import { useAppModeStore } from '@/stores/appModeStore'
|
|
import { resolveNode } from '@/utils/litegraphUtil'
|
|
const { t } = useI18n()
|
|
const commandStore = useCommandStore()
|
|
const executionErrorStore = useExecutionErrorStore()
|
|
const { batchCount } = storeToRefs(useQueueSettingsStore())
|
|
const settingStore = useSettingStore()
|
|
const { isActiveSubscription } = useBillingContext()
|
|
const workflowStore = useWorkflowStore()
|
|
const { isBuilderMode } = useAppMode()
|
|
const appModeStore = useAppModeStore()
|
|
const { hasOutputs } = storeToRefs(appModeStore)
|
|
|
|
const props = defineProps<{
|
|
toastTo?: string | HTMLElement
|
|
mobile?: boolean
|
|
}>()
|
|
|
|
defineEmits<{ navigateAssets: [] }>()
|
|
|
|
//NOTE: due to batching, will never be greater than 2
|
|
const pendingJobQueues = ref(0)
|
|
const { ready: jobToastTimeout, start: resetJobToastTimeout } = useTimeout(
|
|
8000,
|
|
{ controls: true, immediate: false }
|
|
)
|
|
|
|
const graphNodes = shallowRef<LGraphNode[]>(app.rootGraph.nodes)
|
|
useEventListener(
|
|
app.rootGraph.events,
|
|
'configured',
|
|
() => (graphNodes.value = app.rootGraph.nodes)
|
|
)
|
|
|
|
const mappedSelections = computed(() => {
|
|
let unprocessedInputs = [...appModeStore.selectedInputs]
|
|
//FIXME strict typing here
|
|
const processedInputs: ReturnType<typeof nodeToNodeData>[] = []
|
|
while (unprocessedInputs.length) {
|
|
const nodeId = unprocessedInputs[0][0]
|
|
const inputGroup = takeWhile(
|
|
unprocessedInputs,
|
|
([id]) => id === nodeId
|
|
).map(([, widgetName]) => widgetName)
|
|
unprocessedInputs = unprocessedInputs.slice(inputGroup.length)
|
|
const node = resolveNode(nodeId)
|
|
if (!node) continue
|
|
|
|
const nodeData = nodeToNodeData(node)
|
|
remove(nodeData.widgets ?? [], (w) => !inputGroup.includes(w.name))
|
|
processedInputs.push(nodeData)
|
|
}
|
|
return processedInputs
|
|
})
|
|
|
|
function getDropIndicator(node: LGraphNode) {
|
|
if (node.type !== 'LoadImage') return undefined
|
|
|
|
const filename = node.widgets?.[0]?.value
|
|
const resultItem = { type: 'input', filename: `${filename}` }
|
|
|
|
const buildImageUrl = () => {
|
|
if (!filename) return undefined
|
|
const params = new URLSearchParams(resultItem)
|
|
appendCloudResParam(params, resultItem.filename)
|
|
return api.apiURL(`/view?${params}${app.getPreviewFormatParam()}`)
|
|
}
|
|
|
|
return {
|
|
iconClass: 'icon-[lucide--image]',
|
|
imageUrl: buildImageUrl(),
|
|
label: t('linearMode.dragAndDropImage'),
|
|
onClick: () => node.widgets?.[1]?.callback?.(undefined)
|
|
}
|
|
}
|
|
|
|
function nodeToNodeData(node: LGraphNode) {
|
|
const dropIndicator = getDropIndicator(node)
|
|
const nodeData = extractVueNodeData(node)
|
|
for (const widget of nodeData.widgets ?? []) widget.slotMetadata = undefined
|
|
|
|
return {
|
|
...nodeData,
|
|
//note lastNodeErrors uses exeuctionid, node.id is execution for root
|
|
hasErrors: !!executionErrorStore.lastNodeErrors?.[node.id],
|
|
|
|
dropIndicator,
|
|
onDragDrop: node.onDragDrop,
|
|
onDragOver: node.onDragOver
|
|
}
|
|
}
|
|
const partitionedNodes = computed(() => {
|
|
const parts = partition(
|
|
graphNodes.value
|
|
.filter((node) => node.mode === 0 && node.widgets?.length)
|
|
.map(nodeToNodeData)
|
|
.reverse(),
|
|
(node) => ['MarkdownNote', 'Note'].includes(node.type)
|
|
)
|
|
for (const noteNode of parts[0]) {
|
|
for (const widget of noteNode.widgets ?? [])
|
|
widget.options = { ...widget.options, read_only: true }
|
|
}
|
|
return parts
|
|
})
|
|
|
|
//TODO: refactor out of this file.
|
|
//code length is small, but changes should propagate
|
|
async function runButtonClick(e: Event) {
|
|
try {
|
|
pendingJobQueues.value += 1
|
|
resetJobToastTimeout()
|
|
const isShiftPressed = 'shiftKey' in e && e.shiftKey
|
|
const commandId = isShiftPressed
|
|
? 'Comfy.QueuePromptFront'
|
|
: 'Comfy.QueuePrompt'
|
|
|
|
if (batchCount.value > 1) {
|
|
useTelemetry()?.trackUiButtonClicked({
|
|
button_id: 'queue_run_multiple_batches_submitted'
|
|
})
|
|
}
|
|
await commandStore.execute(commandId, {
|
|
metadata: {
|
|
subscribe_to_run: false,
|
|
trigger_source: 'linear'
|
|
}
|
|
})
|
|
} finally {
|
|
//TODO: Error state indicator for failed queue?
|
|
pendingJobQueues.value -= 1
|
|
}
|
|
}
|
|
|
|
defineExpose({ runButtonClick })
|
|
</script>
|
|
<template>
|
|
<div
|
|
v-if="!isBuilderMode && hasOutputs"
|
|
class="flex h-full min-w-80 flex-col"
|
|
v-bind="$attrs"
|
|
>
|
|
<section
|
|
v-if="!mobile"
|
|
data-testid="linear-workflow-info"
|
|
class="flex h-12 items-center gap-2 border-x border-border-subtle bg-comfy-menu-bg px-4 py-2 contain-size"
|
|
>
|
|
<span
|
|
class="truncate font-bold"
|
|
v-text="workflowStore.activeWorkflow?.filename"
|
|
/>
|
|
<div class="flex-1" />
|
|
<Popover
|
|
v-if="partitionedNodes[0].length"
|
|
align="end"
|
|
class="z-100 max-h-(--reka-popover-content-available-height) overflow-x-clip overflow-y-auto"
|
|
side="bottom"
|
|
:side-offset="-8"
|
|
>
|
|
<template #button>
|
|
<Button variant="muted-textonly">
|
|
<i class="icon-[lucide--info]" />
|
|
</Button>
|
|
</template>
|
|
<div>
|
|
<template
|
|
v-for="(nodeData, index) in partitionedNodes[0]"
|
|
:key="nodeData.id"
|
|
>
|
|
<div
|
|
v-if="index !== 0"
|
|
class="w-full border-t border-border-subtle"
|
|
/>
|
|
<NodeWidgets
|
|
:node-data
|
|
class="max-w-100 gap-y-3 rounded-lg py-3 *:has-[textarea]:h-50 **:[.col-span-2]:grid-cols-1"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</Popover>
|
|
<Button v-if="false"> {{ t('menuLabels.publish') }} </Button>
|
|
</section>
|
|
<div
|
|
class="flex h-full flex-col gap-2 border-x border-(--interface-stroke) bg-comfy-menu-bg px-2 md:border-y"
|
|
>
|
|
<section
|
|
data-testid="linear-widgets"
|
|
class="grow overflow-y-auto contain-size"
|
|
>
|
|
<template
|
|
v-for="(nodeData, index) of appModeStore.selectedInputs.length
|
|
? mappedSelections
|
|
: partitionedNodes[0]"
|
|
:key="nodeData.id"
|
|
>
|
|
<div
|
|
v-if="index !== 0 && !appModeStore.selectedInputs.length"
|
|
class="w-full border-t border-node-component-border"
|
|
/>
|
|
<DropZone
|
|
:on-drag-over="nodeData.onDragOver"
|
|
:on-drag-drop="nodeData.onDragDrop"
|
|
:drop-indicator="mobile ? undefined : nodeData.dropIndicator"
|
|
class="text-muted-foreground"
|
|
>
|
|
<NodeWidgets
|
|
:node-data
|
|
:class="
|
|
cn(
|
|
'gap-y-3 rounded-lg py-3 *:has-[textarea]:h-50 **:[.col-span-2]:grid-cols-1 **:[.h-7]:h-10',
|
|
nodeData.hasErrors &&
|
|
'ring-2 ring-node-stroke-error ring-inset'
|
|
)
|
|
"
|
|
/>
|
|
</DropZone>
|
|
</template>
|
|
</section>
|
|
<Teleport
|
|
v-if="!jobToastTimeout || pendingJobQueues > 0"
|
|
defer
|
|
:disabled="mobile"
|
|
:to="toastTo"
|
|
>
|
|
<div
|
|
class="flex h-10 items-center gap-2 rounded-lg bg-base-foreground p-1 pr-2 text-base-background md:h-8 md:bg-secondary-background md:text-base-foreground"
|
|
>
|
|
<template v-if="pendingJobQueues === 0">
|
|
<i
|
|
class="icon-[lucide--check] size-5 not-md:bg-success-background"
|
|
/>
|
|
<span class="mr-auto" v-text="t('queue.jobAddedToQueue')" />
|
|
<Button
|
|
v-if="mobile"
|
|
variant="inverted"
|
|
@click="$emit('navigateAssets')"
|
|
>
|
|
{{ t('linearMode.viewJob') }}
|
|
</Button>
|
|
</template>
|
|
<template v-else>
|
|
<Loader size="sm" />
|
|
<span v-text="t('queue.jobQueueing')" />
|
|
</template>
|
|
</div>
|
|
</Teleport>
|
|
<section
|
|
v-if="mobile"
|
|
data-testid="linear-run-button"
|
|
class="border-t border-node-component-border p-4 pb-6"
|
|
>
|
|
<SubscribeToRunButton
|
|
v-if="!isActiveSubscription"
|
|
class="mt-4 w-full"
|
|
/>
|
|
<div v-else class="mt-4 flex">
|
|
<Popover side="top" @open-auto-focus.prevent>
|
|
<template #button>
|
|
<Button size="lg" class="-mr-3 pr-7">
|
|
<i v-if="batchCount == 1" class="icon-[lucide--chevron-down]" />
|
|
<div v-else class="tabular-nums" v-text="`${batchCount}x`" />
|
|
</Button>
|
|
</template>
|
|
<div
|
|
class="m-1 mb-2 text-node-component-slot-text"
|
|
v-text="t('linearMode.runCount')"
|
|
/>
|
|
<ScrubableNumberInput
|
|
v-model="batchCount"
|
|
:aria-label="t('linearMode.runCount')"
|
|
:min="1"
|
|
:max="settingStore.get('Comfy.QueueButton.BatchCountLimit')"
|
|
class="h-10 min-w-40"
|
|
/>
|
|
</Popover>
|
|
<Button
|
|
variant="primary"
|
|
class="grow"
|
|
size="lg"
|
|
@click="runButtonClick"
|
|
>
|
|
<i class="icon-[lucide--play]" />
|
|
{{ t('menu.run') }}
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
<section
|
|
v-else
|
|
data-testid="linear-run-button"
|
|
class="border-t border-node-component-border p-4 pb-6"
|
|
>
|
|
<div
|
|
class="m-1 mb-2 text-node-component-slot-text"
|
|
v-text="t('linearMode.runCount')"
|
|
/>
|
|
<ScrubableNumberInput
|
|
v-model="batchCount"
|
|
:aria-label="t('linearMode.runCount')"
|
|
:min="1"
|
|
:max="settingStore.get('Comfy.QueueButton.BatchCountLimit')"
|
|
class="h-7 min-w-40"
|
|
/>
|
|
<SubscribeToRunButton
|
|
v-if="!isActiveSubscription"
|
|
class="mt-4 w-full"
|
|
/>
|
|
<Button
|
|
v-else
|
|
variant="primary"
|
|
class="mt-4 w-full text-sm"
|
|
size="lg"
|
|
@click="runButtonClick"
|
|
>
|
|
<i class="icon-[lucide--play]" />
|
|
{{ t('menu.run') }}
|
|
</Button>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</template>
|