Merge remote-tracking branch 'origin/main' into feat/top-menu-active-jobs-label
@@ -96,15 +96,15 @@ const config: StorybookConfig = {
|
||||
}
|
||||
]
|
||||
},
|
||||
esbuild: {
|
||||
// Prevent minification of identifiers to preserve _sfc_main
|
||||
minifyIdentifiers: false,
|
||||
keepNames: true
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
// Disable tree-shaking for Storybook to prevent Vue SFC exports from being removed
|
||||
rolldownOptions: {
|
||||
experimental: {
|
||||
strictExecutionOrder: true
|
||||
},
|
||||
treeshake: false,
|
||||
output: {
|
||||
keepNames: true
|
||||
},
|
||||
onwarn: (warning, warn) => {
|
||||
// Suppress specific warnings
|
||||
if (
|
||||
|
||||
@@ -64,7 +64,7 @@ export default defineConfig(() => {
|
||||
})
|
||||
],
|
||||
build: {
|
||||
minify: SHOULD_MINIFY ? ('esbuild' as const) : false,
|
||||
minify: SHOULD_MINIFY,
|
||||
target: 'es2022',
|
||||
sourcemap: true
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.38.6",
|
||||
"version": "1.38.7",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -12,6 +12,7 @@ declare global {
|
||||
const __ALGOLIA_API_KEY__: string
|
||||
const __USE_PROD_CONFIG__: boolean
|
||||
const __DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
|
||||
const __IS_NIGHTLY__: boolean
|
||||
}
|
||||
|
||||
type GlobalWithDefines = typeof globalThis & {
|
||||
@@ -22,6 +23,7 @@ type GlobalWithDefines = typeof globalThis & {
|
||||
__ALGOLIA_API_KEY__: string
|
||||
__USE_PROD_CONFIG__: boolean
|
||||
__DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
|
||||
__IS_NIGHTLY__: boolean
|
||||
window?: Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -36,6 +38,7 @@ globalWithDefines.__ALGOLIA_APP_ID__ = ''
|
||||
globalWithDefines.__ALGOLIA_API_KEY__ = ''
|
||||
globalWithDefines.__USE_PROD_CONFIG__ = false
|
||||
globalWithDefines.__DISTRIBUTION__ = 'localhost'
|
||||
globalWithDefines.__IS_NIGHTLY__ = false
|
||||
|
||||
// Provide a minimal window shim for Node environment
|
||||
// This is needed for code that checks window existence during imports
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="node-actions touch:opacity-100 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
|
||||
class="node-actions flex gap-1 touch:opacity-100 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
|
||||
>
|
||||
<slot name="actions" :node="props.node" />
|
||||
</div>
|
||||
|
||||
@@ -25,13 +25,31 @@ const widgetsSectionDataList = computed((): NodeWidgetsListList => {
|
||||
return nodes.map((node) => {
|
||||
const { widgets = [] } = node
|
||||
const shownWidgets = widgets
|
||||
.filter((w) => !(w.options?.canvasOnly || w.options?.hidden))
|
||||
.filter(
|
||||
(w) =>
|
||||
!(w.options?.canvasOnly || w.options?.hidden || w.options?.advanced)
|
||||
)
|
||||
.map((widget) => ({ node, widget }))
|
||||
|
||||
return { widgets: shownWidgets, node }
|
||||
})
|
||||
})
|
||||
|
||||
const advancedWidgetsSectionDataList = computed((): NodeWidgetsListList => {
|
||||
return nodes
|
||||
.map((node) => {
|
||||
const { widgets = [] } = node
|
||||
const advancedWidgets = widgets
|
||||
.filter(
|
||||
(w) =>
|
||||
!(w.options?.canvasOnly || w.options?.hidden) && w.options?.advanced
|
||||
)
|
||||
.map((widget) => ({ node, widget }))
|
||||
return { widgets: advancedWidgets, node }
|
||||
})
|
||||
.filter(({ widgets }) => widgets.length > 0)
|
||||
})
|
||||
|
||||
const isMultipleNodesSelected = computed(
|
||||
() => widgetsSectionDataList.value.length > 1
|
||||
)
|
||||
@@ -56,6 +74,12 @@ const label = computed(() => {
|
||||
: t('rightSidePanel.inputsNone')
|
||||
: undefined // SectionWidgets display node titles by default
|
||||
})
|
||||
|
||||
const advancedLabel = computed(() => {
|
||||
return !mustShowNodeTitle && !isMultipleNodesSelected.value
|
||||
? t('rightSidePanel.advancedInputs')
|
||||
: undefined // SectionWidgets display node titles by default
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -93,4 +117,16 @@ const label = computed(() => {
|
||||
class="border-b border-interface-stroke"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
<template v-if="advancedWidgetsSectionDataList.length > 0 && !isSearching">
|
||||
<SectionWidgets
|
||||
v-for="{ widgets, node } in advancedWidgetsSectionDataList"
|
||||
:key="`advanced-${node.id}`"
|
||||
:collapse="true"
|
||||
:node
|
||||
:label="advancedLabel"
|
||||
:widgets
|
||||
:show-locate-button="isMultipleNodesSelected"
|
||||
class="border-b border-interface-stroke"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -368,7 +368,7 @@ export class GroupNodeConfig {
|
||||
}
|
||||
|
||||
getNodeDef(
|
||||
node: GroupNodeData
|
||||
node: GroupNodeData | GroupNodeWorkflowData['nodes'][number]
|
||||
): GroupNodeDef | ComfyNodeDef | null | undefined {
|
||||
if (node.type) {
|
||||
const def = globalDefs[node.type]
|
||||
@@ -386,7 +386,8 @@ export class GroupNodeConfig {
|
||||
let type: string | number | null = linksFrom[0]?.[0]?.[5] ?? null
|
||||
if (type === 'COMBO') {
|
||||
// Use the array items
|
||||
const source = node.outputs?.[0]?.widget?.name
|
||||
const output = node.outputs?.[0] as GroupNodeOutput | undefined
|
||||
const source = output?.widget?.name
|
||||
const nodeIdx = linksFrom[0]?.[0]?.[2]
|
||||
if (source && nodeIdx != null) {
|
||||
const fromTypeName = this.nodeData.nodes[Number(nodeIdx)]?.type
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
|
||||
import {
|
||||
type LGraphNode,
|
||||
type LGraphNodeConstructor,
|
||||
LiteGraph
|
||||
import type {
|
||||
GroupNodeConfigEntry,
|
||||
GroupNodeWorkflowData,
|
||||
LGraphNode,
|
||||
LGraphNodeConstructor
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
|
||||
import { type ComfyApp, app } from '../../scripts/app'
|
||||
@@ -15,18 +17,20 @@ import './groupNodeManage.css'
|
||||
|
||||
const ORDER: symbol = Symbol()
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
function merge(target, source) {
|
||||
if (typeof target === 'object' && typeof source === 'object') {
|
||||
for (const key in source) {
|
||||
const sv = source[key]
|
||||
if (typeof sv === 'object') {
|
||||
let tv = target[key]
|
||||
if (!tv) tv = target[key] = {}
|
||||
merge(tv, source[key])
|
||||
} else {
|
||||
target[key] = sv
|
||||
function merge(
|
||||
target: Record<string, unknown>,
|
||||
source: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
for (const key in source) {
|
||||
const sv = source[key]
|
||||
if (typeof sv === 'object' && sv !== null) {
|
||||
let tv = target[key] as Record<string, unknown> | undefined
|
||||
if (!tv) {
|
||||
tv = target[key] = {}
|
||||
}
|
||||
merge(tv, sv as Record<string, unknown>)
|
||||
} else {
|
||||
target[key] = sv
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +38,7 @@ function merge(target, source) {
|
||||
}
|
||||
|
||||
export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
tabs: Record<
|
||||
tabs!: Record<
|
||||
'Inputs' | 'Outputs' | 'Widgets',
|
||||
{ tab: HTMLAnchorElement; page: HTMLElement }
|
||||
>
|
||||
@@ -52,31 +55,26 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
>
|
||||
>
|
||||
> = {}
|
||||
// @ts-expect-error fixme ts strict error
|
||||
nodeItems: any[]
|
||||
nodeItems!: HTMLLIElement[]
|
||||
app: ComfyApp
|
||||
// @ts-expect-error fixme ts strict error
|
||||
groupNodeType: LGraphNodeConstructor<LGraphNode>
|
||||
groupNodeDef: any
|
||||
groupData: any
|
||||
groupNodeType!: LGraphNodeConstructor<LGraphNode>
|
||||
groupData!: GroupNodeConfig
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
innerNodesList: HTMLUListElement
|
||||
// @ts-expect-error fixme ts strict error
|
||||
widgetsPage: HTMLElement
|
||||
// @ts-expect-error fixme ts strict error
|
||||
inputsPage: HTMLElement
|
||||
// @ts-expect-error fixme ts strict error
|
||||
outputsPage: HTMLElement
|
||||
draggable: any
|
||||
innerNodesList!: HTMLUListElement
|
||||
widgetsPage!: HTMLElement
|
||||
inputsPage!: HTMLElement
|
||||
outputsPage!: HTMLElement
|
||||
draggable: DraggableList | undefined
|
||||
|
||||
get selectedNodeInnerIndex() {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex
|
||||
get selectedNodeInnerIndex(): number {
|
||||
const index = this.selectedNodeIndex
|
||||
if (index == null) throw new Error('No node selected')
|
||||
const item = this.nodeItems[index]
|
||||
if (!item?.dataset.nodeindex) throw new Error('Invalid node item')
|
||||
return +item.dataset.nodeindex
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
constructor(app) {
|
||||
constructor(app: ComfyApp) {
|
||||
super()
|
||||
this.app = app
|
||||
this.element = $el('dialog.comfy-group-manage', {
|
||||
@@ -84,19 +82,15 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
}) as HTMLDialogElement
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
changeTab(tab) {
|
||||
changeTab(tab: keyof ManageGroupDialog['tabs']): void {
|
||||
this.tabs[this.selectedTab].tab.classList.remove('active')
|
||||
this.tabs[this.selectedTab].page.classList.remove('active')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.tabs[tab].tab.classList.add('active')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.tabs[tab].page.classList.add('active')
|
||||
this.selectedTab = tab
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
changeNode(index, force?) {
|
||||
changeNode(index: number, force?: boolean): void {
|
||||
if (!force && this.selectedNodeIndex === index) return
|
||||
|
||||
if (this.selectedNodeIndex != null) {
|
||||
@@ -122,43 +116,41 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
this.groupNodeType = LiteGraph.registered_node_types[
|
||||
`${PREFIX}${SEPARATOR}` + this.selectedGroup
|
||||
] as unknown as LGraphNodeConstructor<LGraphNode>
|
||||
this.groupNodeDef = this.groupNodeType.nodeData
|
||||
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType)
|
||||
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType)!
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
changeGroup(group, reset = true) {
|
||||
changeGroup(group: string, reset = true): void {
|
||||
this.selectedGroup = group
|
||||
this.getGroupData()
|
||||
|
||||
const nodes = this.groupData.nodeData.nodes
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.nodeItems = nodes.map((n, i) =>
|
||||
$el(
|
||||
'li.draggable-item',
|
||||
{
|
||||
dataset: {
|
||||
nodeindex: n.index + ''
|
||||
},
|
||||
onclick: () => {
|
||||
this.changeNode(i)
|
||||
}
|
||||
},
|
||||
[
|
||||
$el('span.drag-handle'),
|
||||
$el(
|
||||
'div',
|
||||
{
|
||||
textContent: n.title ?? n.type
|
||||
this.nodeItems = nodes.map(
|
||||
(n, i) =>
|
||||
$el(
|
||||
'li.draggable-item',
|
||||
{
|
||||
dataset: {
|
||||
nodeindex: n.index + ''
|
||||
},
|
||||
n.title
|
||||
? $el('span', {
|
||||
textContent: n.type
|
||||
})
|
||||
: []
|
||||
)
|
||||
]
|
||||
)
|
||||
onclick: () => {
|
||||
this.changeNode(i)
|
||||
}
|
||||
},
|
||||
[
|
||||
$el('span.drag-handle'),
|
||||
$el(
|
||||
'div',
|
||||
{
|
||||
textContent: n.title ?? n.type
|
||||
},
|
||||
n.title
|
||||
? $el('span', {
|
||||
textContent: n.type
|
||||
})
|
||||
: []
|
||||
)
|
||||
]
|
||||
) as HTMLLIElement
|
||||
)
|
||||
|
||||
this.innerNodesList.replaceChildren(...this.nodeItems)
|
||||
@@ -167,47 +159,46 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
this.selectedNodeIndex = null
|
||||
this.changeNode(0)
|
||||
} else {
|
||||
const items = this.draggable.getAllItems()
|
||||
// @ts-expect-error fixme ts strict error
|
||||
let index = items.findIndex((item) => item.classList.contains('selected'))
|
||||
if (index === -1) index = this.selectedNodeIndex
|
||||
const items = this.draggable!.getAllItems()
|
||||
let index = items.findIndex((item: Element) =>
|
||||
item.classList.contains('selected')
|
||||
)
|
||||
if (index === -1) index = this.selectedNodeIndex!
|
||||
this.changeNode(index, true)
|
||||
}
|
||||
|
||||
const ordered = [...nodes]
|
||||
this.draggable?.dispose()
|
||||
this.draggable = new DraggableList(this.innerNodesList, 'li')
|
||||
this.draggable.addEventListener(
|
||||
'dragend',
|
||||
// @ts-expect-error fixme ts strict error
|
||||
({ detail: { oldPosition, newPosition } }) => {
|
||||
if (oldPosition === newPosition) return
|
||||
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0])
|
||||
for (let i = 0; i < ordered.length; i++) {
|
||||
this.storeModification({
|
||||
nodeIndex: ordered[i].index,
|
||||
section: ORDER,
|
||||
prop: 'order',
|
||||
value: i
|
||||
})
|
||||
}
|
||||
this.draggable.addEventListener('dragend', (e: Event) => {
|
||||
const { oldPosition, newPosition } = (e as CustomEvent).detail
|
||||
if (oldPosition === newPosition) return
|
||||
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0])
|
||||
for (let i = 0; i < ordered.length; i++) {
|
||||
this.storeModification({
|
||||
nodeIndex: ordered[i].index,
|
||||
section: ORDER,
|
||||
prop: 'order',
|
||||
value: i
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
storeModification(props: {
|
||||
nodeIndex?: number
|
||||
section: symbol
|
||||
section: string | symbol
|
||||
prop: string
|
||||
value: any
|
||||
value: unknown
|
||||
}) {
|
||||
const { nodeIndex, section, prop, value } = props
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const groupMod = (this.modifications[this.selectedGroup] ??= {})
|
||||
const nodesMod = (groupMod.nodes ??= {})
|
||||
const groupKey = this.selectedGroup!
|
||||
const groupMod = (this.modifications[groupKey] ??= {})
|
||||
const nodesMod = ((groupMod as Record<string, unknown>).nodes ??=
|
||||
{}) as Record<string, Record<symbol | string, Record<string, unknown>>>
|
||||
const nodeMod = (nodesMod[nodeIndex ?? this.selectedNodeInnerIndex] ??= {})
|
||||
const typeMod = (nodeMod[section] ??= {})
|
||||
if (typeof value === 'object') {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const objMod = (typeMod[prop] ??= {})
|
||||
Object.assign(objMod, value)
|
||||
} else {
|
||||
@@ -215,35 +206,45 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
getEditElement(section, prop, value, placeholder, checked, checkable = true) {
|
||||
if (value === placeholder) value = ''
|
||||
getEditElement(
|
||||
section: string,
|
||||
prop: string | number,
|
||||
value: unknown,
|
||||
placeholder: string,
|
||||
checked: boolean,
|
||||
checkable = true
|
||||
): HTMLDivElement {
|
||||
let displayValue = value === placeholder ? '' : value
|
||||
|
||||
const mods =
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.modifications[this.selectedGroup]?.nodes?.[
|
||||
this.selectedNodeInnerIndex
|
||||
]?.[section]?.[prop]
|
||||
if (mods) {
|
||||
if (mods.name != null) {
|
||||
value = mods.name
|
||||
const groupKey = this.selectedGroup!
|
||||
const mods = (
|
||||
this.modifications[groupKey] as Record<string, unknown> | undefined
|
||||
)?.nodes as
|
||||
| Record<
|
||||
number,
|
||||
Record<string, Record<string, { name?: string; visible?: boolean }>>
|
||||
>
|
||||
| undefined
|
||||
const modEntry = mods?.[this.selectedNodeInnerIndex]?.[section]?.[prop]
|
||||
if (modEntry) {
|
||||
if (modEntry.name != null) {
|
||||
displayValue = modEntry.name
|
||||
}
|
||||
if (mods.visible != null) {
|
||||
checked = mods.visible
|
||||
if (modEntry.visible != null) {
|
||||
checked = modEntry.visible
|
||||
}
|
||||
}
|
||||
|
||||
return $el('div', [
|
||||
$el('input', {
|
||||
value,
|
||||
value: displayValue as string,
|
||||
placeholder,
|
||||
type: 'text',
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onchange: (e) => {
|
||||
onchange: (e: Event) => {
|
||||
this.storeModification({
|
||||
section,
|
||||
prop,
|
||||
value: { name: e.target.value }
|
||||
prop: String(prop),
|
||||
value: { name: (e.target as HTMLInputElement).value }
|
||||
})
|
||||
}
|
||||
}),
|
||||
@@ -252,25 +253,23 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
type: 'checkbox',
|
||||
checked,
|
||||
disabled: !checkable,
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onchange: (e) => {
|
||||
onchange: (e: Event) => {
|
||||
this.storeModification({
|
||||
section,
|
||||
prop,
|
||||
value: { visible: !!e.target.checked }
|
||||
prop: String(prop),
|
||||
value: { visible: !!(e.target as HTMLInputElement).checked }
|
||||
})
|
||||
}
|
||||
})
|
||||
])
|
||||
])
|
||||
]) as HTMLDivElement
|
||||
}
|
||||
|
||||
buildWidgetsPage() {
|
||||
const widgets =
|
||||
this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex]
|
||||
const items = Object.keys(widgets ?? {})
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const type = app.rootGraph.extra.groupNodes[this.selectedGroup]
|
||||
const type = app.rootGraph.extra.groupNodes![this.selectedGroup!]!
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.input
|
||||
this.widgetsPage.replaceChildren(
|
||||
...items.map((oldName) => {
|
||||
@@ -289,28 +288,25 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
buildInputsPage() {
|
||||
const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex]
|
||||
const items = Object.keys(inputs ?? {})
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const type = app.rootGraph.extra.groupNodes[this.selectedGroup]
|
||||
const type = app.rootGraph.extra.groupNodes![this.selectedGroup!]!
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.input
|
||||
this.inputsPage.replaceChildren(
|
||||
// @ts-expect-error fixme ts strict error
|
||||
...items
|
||||
.map((oldName) => {
|
||||
let value = inputs[oldName]
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
const elements = items
|
||||
.map((oldName) => {
|
||||
const value = inputs[oldName]
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.getEditElement(
|
||||
'input',
|
||||
oldName,
|
||||
value,
|
||||
oldName,
|
||||
config?.[oldName]?.visible !== false
|
||||
)
|
||||
})
|
||||
.filter(Boolean)
|
||||
)
|
||||
return this.getEditElement(
|
||||
'input',
|
||||
oldName,
|
||||
value,
|
||||
oldName,
|
||||
config?.[oldName]?.visible !== false
|
||||
)
|
||||
})
|
||||
.filter((el): el is HTMLDivElement => el !== null)
|
||||
this.inputsPage.replaceChildren(...elements)
|
||||
return !!items.length
|
||||
}
|
||||
|
||||
@@ -323,38 +319,35 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
const groupOutputs =
|
||||
this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex]
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const type = app.rootGraph.extra.groupNodes[this.selectedGroup]
|
||||
const type = app.rootGraph.extra.groupNodes![this.selectedGroup!]!
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.output
|
||||
const node = this.groupData.nodeData.nodes[this.selectedNodeInnerIndex]
|
||||
const checkable = node.type !== 'PrimitiveNode'
|
||||
this.outputsPage.replaceChildren(
|
||||
...outputs
|
||||
// @ts-expect-error fixme ts strict error
|
||||
.map((type, slot) => {
|
||||
const groupOutputIndex = groupOutputs?.[slot]
|
||||
const oldName = innerNodeDef.output_name?.[slot] ?? type
|
||||
let value = config?.[slot]?.name
|
||||
const visible = config?.[slot]?.visible || groupOutputIndex != null
|
||||
if (!value || value === oldName) {
|
||||
value = ''
|
||||
}
|
||||
return this.getEditElement(
|
||||
'output',
|
||||
slot,
|
||||
value,
|
||||
oldName,
|
||||
visible,
|
||||
checkable
|
||||
)
|
||||
})
|
||||
.filter(Boolean)
|
||||
)
|
||||
const elements = outputs.map((outputType: unknown, slot: number) => {
|
||||
const groupOutputIndex = groupOutputs?.[slot]
|
||||
const oldName = innerNodeDef?.output_name?.[slot] ?? String(outputType)
|
||||
let value = config?.[slot]?.name
|
||||
const visible = config?.[slot]?.visible || groupOutputIndex != null
|
||||
if (!value || value === oldName) {
|
||||
value = ''
|
||||
}
|
||||
return this.getEditElement(
|
||||
'output',
|
||||
slot,
|
||||
value,
|
||||
oldName,
|
||||
visible,
|
||||
checkable
|
||||
)
|
||||
})
|
||||
this.outputsPage.replaceChildren(...elements)
|
||||
return !!outputs.length
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
show(type?) {
|
||||
override show(groupNodeType?: string | HTMLElement | HTMLElement[]): void {
|
||||
// Extract string type - this method repurposes the show signature
|
||||
const nodeType =
|
||||
typeof groupNodeType === 'string' ? groupNodeType : undefined
|
||||
const groupNodes = Object.keys(app.rootGraph.extra?.groupNodes ?? {}).sort(
|
||||
(a, b) => a.localeCompare(b)
|
||||
)
|
||||
@@ -371,24 +364,27 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
this.outputsPage
|
||||
])
|
||||
|
||||
this.tabs = [
|
||||
type TabName = 'Inputs' | 'Widgets' | 'Outputs'
|
||||
const tabEntries: [TabName, HTMLElement][] = [
|
||||
['Inputs', this.inputsPage],
|
||||
['Widgets', this.widgetsPage],
|
||||
['Outputs', this.outputsPage]
|
||||
// @ts-expect-error fixme ts strict error
|
||||
].reduce((p, [name, page]: [string, HTMLElement]) => {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
p[name] = {
|
||||
tab: $el('a', {
|
||||
onclick: () => {
|
||||
this.changeTab(name)
|
||||
},
|
||||
textContent: name
|
||||
}),
|
||||
page
|
||||
}
|
||||
return p
|
||||
}, {}) as any
|
||||
]
|
||||
this.tabs = tabEntries.reduce(
|
||||
(p, [name, page]) => {
|
||||
p[name] = {
|
||||
tab: $el('a', {
|
||||
onclick: () => {
|
||||
this.changeTab(name)
|
||||
},
|
||||
textContent: name
|
||||
}) as HTMLAnchorElement,
|
||||
page
|
||||
}
|
||||
return p
|
||||
},
|
||||
{} as ManageGroupDialog['tabs']
|
||||
)
|
||||
|
||||
const outer = $el('div.comfy-group-manage-outer', [
|
||||
$el('header', [
|
||||
@@ -396,15 +392,14 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
$el(
|
||||
'select',
|
||||
{
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onchange: (e) => {
|
||||
this.changeGroup(e.target.value)
|
||||
onchange: (e: Event) => {
|
||||
this.changeGroup((e.target as HTMLSelectElement).value)
|
||||
}
|
||||
},
|
||||
groupNodes.map((g) =>
|
||||
$el('option', {
|
||||
textContent: g,
|
||||
selected: `${PREFIX}${SEPARATOR}${g}` === type,
|
||||
selected: `${PREFIX}${SEPARATOR}${g}` === nodeType,
|
||||
value: g
|
||||
})
|
||||
)
|
||||
@@ -439,8 +434,7 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
`Are you sure you want to remove the node: "${this.selectedGroup}"`
|
||||
)
|
||||
) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
delete app.rootGraph.extra.groupNodes[this.selectedGroup]
|
||||
delete app.rootGraph.extra.groupNodes![this.selectedGroup!]
|
||||
LiteGraph.unregisterNodeType(
|
||||
`${PREFIX}${SEPARATOR}` + this.selectedGroup
|
||||
)
|
||||
@@ -454,97 +448,106 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
'button.comfy-btn',
|
||||
{
|
||||
onclick: async () => {
|
||||
let nodesByType
|
||||
let recreateNodes = []
|
||||
const types = {}
|
||||
type NodesByType = Record<string, LGraphNode[]>
|
||||
let nodesByType: NodesByType | undefined
|
||||
const recreateNodes: LGraphNode[] = []
|
||||
const types: Record<string, GroupNodeWorkflowData> = {}
|
||||
for (const g in this.modifications) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const type = app.rootGraph.extra.groupNodes[g]
|
||||
let config = (type.config ??= {})
|
||||
const groupNodeData = app.rootGraph.extra.groupNodes![g]!
|
||||
let config = (groupNodeData.config ??= {})
|
||||
|
||||
let nodeMods = this.modifications[g]?.nodes
|
||||
type NodeMods = Record<
|
||||
string,
|
||||
Record<symbol | string, Record<string, unknown>>
|
||||
>
|
||||
let nodeMods = this.modifications[g]?.nodes as
|
||||
| NodeMods
|
||||
| undefined
|
||||
if (nodeMods) {
|
||||
const keys = Object.keys(nodeMods)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (nodeMods[keys[0]][ORDER]) {
|
||||
if (nodeMods[keys[0]]?.[ORDER]) {
|
||||
// If any node is reordered, they will all need sequencing
|
||||
const orderedNodes = []
|
||||
const orderedMods = {}
|
||||
const orderedConfig = {}
|
||||
const orderedNodes: GroupNodeWorkflowData['nodes'] = []
|
||||
const orderedMods: NodeMods = {}
|
||||
const orderedConfig: Record<number, GroupNodeConfigEntry> =
|
||||
{}
|
||||
|
||||
for (const n of keys) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const order = nodeMods[n][ORDER].order
|
||||
orderedNodes[order] = type.nodes[+n]
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const order = (nodeMods[n][ORDER] as { order: number })
|
||||
.order
|
||||
orderedNodes[order] = groupNodeData.nodes[+n]
|
||||
orderedMods[order] = nodeMods[n]
|
||||
orderedNodes[order].index = order
|
||||
}
|
||||
|
||||
// Rewrite links
|
||||
for (const l of type.links) {
|
||||
// @ts-expect-error l[0]/l[2] used as node index
|
||||
if (l[0] != null) l[0] = type.nodes[l[0]].index
|
||||
// @ts-expect-error l[0]/l[2] used as node index
|
||||
if (l[2] != null) l[2] = type.nodes[l[2]].index
|
||||
const nodesLen = groupNodeData.nodes.length
|
||||
for (const l of groupNodeData.links) {
|
||||
const srcIdx = l[0] as number
|
||||
const dstIdx = l[2] as number
|
||||
if (srcIdx != null && srcIdx < nodesLen)
|
||||
l[0] = groupNodeData.nodes[srcIdx].index!
|
||||
if (dstIdx != null && dstIdx < nodesLen)
|
||||
l[2] = groupNodeData.nodes[dstIdx].index!
|
||||
}
|
||||
|
||||
// Rewrite externals
|
||||
if (type.external) {
|
||||
for (const ext of type.external) {
|
||||
if (ext[0] != null) {
|
||||
// @ts-expect-error ext[0] used as node index
|
||||
ext[0] = type.nodes[ext[0]].index
|
||||
if (groupNodeData.external) {
|
||||
for (const ext of groupNodeData.external) {
|
||||
const extIdx = ext[0] as number
|
||||
if (extIdx != null && extIdx < nodesLen) {
|
||||
ext[0] = groupNodeData.nodes[extIdx].index!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite modifications
|
||||
for (const id of keys) {
|
||||
// @ts-expect-error id used as node index
|
||||
if (config[id]) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
orderedConfig[type.nodes[id].index] = config[id]
|
||||
if (config[+id]) {
|
||||
orderedConfig[groupNodeData.nodes[+id].index!] =
|
||||
config[+id]
|
||||
}
|
||||
// @ts-expect-error id used as config key
|
||||
delete config[id]
|
||||
delete config[+id]
|
||||
}
|
||||
|
||||
type.nodes = orderedNodes
|
||||
groupNodeData.nodes = orderedNodes
|
||||
nodeMods = orderedMods
|
||||
type.config = config = orderedConfig
|
||||
groupNodeData.config = config = orderedConfig
|
||||
}
|
||||
|
||||
merge(config, nodeMods)
|
||||
merge(
|
||||
config as Record<string, unknown>,
|
||||
nodeMods as Record<string, unknown>
|
||||
)
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
types[g] = type
|
||||
types[g] = groupNodeData
|
||||
|
||||
if (!nodesByType) {
|
||||
nodesByType = app.rootGraph.nodes.reduce((p, n) => {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
p[n.type] ??= []
|
||||
// @ts-expect-error fixme ts strict error
|
||||
p[n.type].push(n)
|
||||
return p
|
||||
}, {})
|
||||
nodesByType = app.rootGraph.nodes.reduce<NodesByType>(
|
||||
(p, n) => {
|
||||
const nodeType = n.type ?? ''
|
||||
p[nodeType] ??= []
|
||||
p[nodeType].push(n)
|
||||
return p
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const nodes = nodesByType[`${PREFIX}${SEPARATOR}` + g]
|
||||
if (nodes) recreateNodes.push(...nodes)
|
||||
const groupTypeNodes = nodesByType[`${PREFIX}${SEPARATOR}` + g]
|
||||
if (groupTypeNodes) recreateNodes.push(...groupTypeNodes)
|
||||
}
|
||||
|
||||
await GroupNodeConfig.registerFromWorkflow(types, [])
|
||||
|
||||
for (const node of recreateNodes) {
|
||||
node.recreate()
|
||||
node.recreate?.()
|
||||
}
|
||||
|
||||
this.modifications = {}
|
||||
this.app.canvas.setDirty(true, true)
|
||||
this.changeGroup(this.selectedGroup, false)
|
||||
this.changeGroup(this.selectedGroup!, false)
|
||||
}
|
||||
},
|
||||
'Save'
|
||||
@@ -559,8 +562,8 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
|
||||
this.element.replaceChildren(outer)
|
||||
this.changeGroup(
|
||||
type
|
||||
? (groupNodes.find((g) => `${PREFIX}${SEPARATOR}${g}` === type) ??
|
||||
nodeType
|
||||
? (groupNodes.find((g) => `${PREFIX}${SEPARATOR}${g}` === nodeType) ??
|
||||
groupNodes[0])
|
||||
: groupNodes[0]
|
||||
)
|
||||
|
||||
@@ -102,16 +102,23 @@ export interface LGraphConfig {
|
||||
links_ontop?: boolean
|
||||
}
|
||||
|
||||
export interface GroupNodeConfigEntry {
|
||||
input?: Record<string, { name?: string; visible?: boolean }>
|
||||
output?: Record<number, { name?: string; visible?: boolean }>
|
||||
}
|
||||
|
||||
export interface GroupNodeWorkflowData {
|
||||
external: (number | string)[][]
|
||||
links: SerialisedLLinkArray[]
|
||||
nodes: {
|
||||
index?: number
|
||||
type?: string
|
||||
title?: string
|
||||
inputs?: unknown[]
|
||||
outputs?: unknown[]
|
||||
widgets_values?: unknown[]
|
||||
}[]
|
||||
config?: Record<number, unknown>
|
||||
config?: Record<number, GroupNodeConfigEntry>
|
||||
}
|
||||
|
||||
export interface LGraphExtra extends Dictionary<unknown> {
|
||||
|
||||
@@ -1350,12 +1350,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
})
|
||||
|
||||
function inner_clicked(
|
||||
this: ContextMenu<string>,
|
||||
this: ContextMenuDivElement,
|
||||
v?: string | IContextMenuValue<string>
|
||||
) {
|
||||
if (!node || typeof v === 'string' || !v?.value) return
|
||||
|
||||
const rect = this.root.getBoundingClientRect()
|
||||
const rect = this.getBoundingClientRect()
|
||||
canvas.showEditPropertyValue(node, v.value, {
|
||||
position: [rect.left, rect.top]
|
||||
})
|
||||
|
||||
@@ -104,6 +104,8 @@ export type {
|
||||
} from './interfaces'
|
||||
export {
|
||||
LGraph,
|
||||
type GroupNodeConfigEntry,
|
||||
type GroupNodeWorkflowData,
|
||||
type LGraphTriggerAction,
|
||||
type LGraphTriggerParam
|
||||
} from './LGraph'
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
<template>
|
||||
<div class="flex gap-3">
|
||||
<div class="flex gap-3 items-center">
|
||||
<SearchBox
|
||||
:model-value="searchQuery"
|
||||
:placeholder="$t('sideToolbar.searchAssets') + '...'"
|
||||
@update:model-value="handleSearchChange"
|
||||
/>
|
||||
<MediaAssetFilterButton
|
||||
v-if="isCloud"
|
||||
v-tooltip.top="{ value: $t('assetBrowser.filterBy') }"
|
||||
size="md"
|
||||
>
|
||||
<template #default="{ close }">
|
||||
<MediaAssetFilterMenu
|
||||
:media-type-filters="mediaTypeFilters"
|
||||
:close="close"
|
||||
@update:media-type-filters="handleMediaTypeFiltersChange"
|
||||
/>
|
||||
</template>
|
||||
</MediaAssetFilterButton>
|
||||
<AssetSortButton
|
||||
v-if="isCloud"
|
||||
v-tooltip.top="{ value: $t('assetBrowser.sortBy') }"
|
||||
size="md"
|
||||
>
|
||||
<template #default="{ close }">
|
||||
<MediaAssetSortMenu
|
||||
v-model:sort-by="sortBy"
|
||||
:show-generation-time-sort
|
||||
:close="close"
|
||||
/>
|
||||
</template>
|
||||
</AssetSortButton>
|
||||
<MediaAssetViewModeToggle
|
||||
v-if="isQueuePanelV2Enabled"
|
||||
v-model:view-mode="viewMode"
|
||||
/>
|
||||
<div class="flex gap-1.5 items-center">
|
||||
<MediaAssetFilterButton
|
||||
v-if="isCloud"
|
||||
v-tooltip.top="{ value: $t('assetBrowser.filterBy') }"
|
||||
size="md"
|
||||
>
|
||||
<template #default="{ close }">
|
||||
<MediaAssetFilterMenu
|
||||
:media-type-filters
|
||||
:close
|
||||
@update:media-type-filters="handleMediaTypeFiltersChange"
|
||||
/>
|
||||
</template>
|
||||
</MediaAssetFilterButton>
|
||||
<AssetSortButton
|
||||
v-if="isCloud"
|
||||
v-tooltip.top="{ value: $t('assetBrowser.sortBy') }"
|
||||
size="md"
|
||||
>
|
||||
<template #default="{ close }">
|
||||
<MediaAssetSortMenu
|
||||
v-model:sort-by="sortBy"
|
||||
:show-generation-time-sort
|
||||
:close
|
||||
/>
|
||||
</template>
|
||||
</AssetSortButton>
|
||||
<MediaAssetViewModeToggle
|
||||
v-if="isQueuePanelV2Enabled"
|
||||
v-model:view-mode="viewMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ type Distribution = 'desktop' | 'localhost' | 'cloud'
|
||||
|
||||
declare global {
|
||||
const __DISTRIBUTION__: Distribution
|
||||
const __IS_NIGHTLY__: boolean
|
||||
}
|
||||
|
||||
/** Current distribution - replaced at compile time */
|
||||
@@ -18,3 +19,10 @@ const DISTRIBUTION: Distribution = __DISTRIBUTION__
|
||||
export const isDesktop = DISTRIBUTION === 'desktop' || isElectron() // TODO: replace with build var
|
||||
export const isCloud = DISTRIBUTION === 'cloud'
|
||||
// export const isLocalhost = DISTRIBUTION === 'localhost' || (!isDesktop && !isCloud)
|
||||
|
||||
/**
|
||||
* Whether this is a nightly build (from main branch).
|
||||
* Nightly builds may show experimental features and surveys.
|
||||
* @public
|
||||
*/
|
||||
export const isNightly = __IS_NIGHTLY__
|
||||
|
||||
131
src/platform/surveys/useFeatureUsageTracker.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const STORAGE_KEY = 'Comfy.FeatureUsage'
|
||||
|
||||
describe('useFeatureUsageTracker', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('initializes with zero count for new feature', async () => {
|
||||
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
|
||||
const { useCount } = useFeatureUsageTracker('test-feature')
|
||||
|
||||
expect(useCount.value).toBe(0)
|
||||
})
|
||||
|
||||
it('increments count on trackUsage', async () => {
|
||||
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
|
||||
const { useCount, trackUsage } = useFeatureUsageTracker('test-feature')
|
||||
|
||||
expect(useCount.value).toBe(0)
|
||||
|
||||
trackUsage()
|
||||
expect(useCount.value).toBe(1)
|
||||
|
||||
trackUsage()
|
||||
expect(useCount.value).toBe(2)
|
||||
})
|
||||
|
||||
it('sets firstUsed only on first use', async () => {
|
||||
vi.useFakeTimers()
|
||||
const firstTs = 1000000
|
||||
vi.setSystemTime(firstTs)
|
||||
try {
|
||||
const { useFeatureUsageTracker } =
|
||||
await import('./useFeatureUsageTracker')
|
||||
const { usage, trackUsage } = useFeatureUsageTracker('test-feature')
|
||||
|
||||
trackUsage()
|
||||
expect(usage.value?.firstUsed).toBe(firstTs)
|
||||
|
||||
vi.setSystemTime(firstTs + 5000)
|
||||
trackUsage()
|
||||
expect(usage.value?.firstUsed).toBe(firstTs)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('updates lastUsed on each use', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const { useFeatureUsageTracker } =
|
||||
await import('./useFeatureUsageTracker')
|
||||
const { usage, trackUsage } = useFeatureUsageTracker('test-feature')
|
||||
|
||||
trackUsage()
|
||||
const firstLastUsed = usage.value?.lastUsed ?? 0
|
||||
|
||||
vi.advanceTimersByTime(10)
|
||||
trackUsage()
|
||||
|
||||
expect(usage.value?.lastUsed).toBeGreaterThan(firstLastUsed)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('reset clears feature data', async () => {
|
||||
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
|
||||
const { useCount, trackUsage, reset } =
|
||||
useFeatureUsageTracker('test-feature')
|
||||
|
||||
trackUsage()
|
||||
trackUsage()
|
||||
expect(useCount.value).toBe(2)
|
||||
|
||||
reset()
|
||||
expect(useCount.value).toBe(0)
|
||||
})
|
||||
|
||||
it('tracks multiple features independently', async () => {
|
||||
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
|
||||
const featureA = useFeatureUsageTracker('feature-a')
|
||||
const featureB = useFeatureUsageTracker('feature-b')
|
||||
|
||||
featureA.trackUsage()
|
||||
featureA.trackUsage()
|
||||
featureB.trackUsage()
|
||||
|
||||
expect(featureA.useCount.value).toBe(2)
|
||||
expect(featureB.useCount.value).toBe(1)
|
||||
})
|
||||
|
||||
it('persists to localStorage', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const { useFeatureUsageTracker } =
|
||||
await import('./useFeatureUsageTracker')
|
||||
const { trackUsage } = useFeatureUsageTracker('persisted-feature')
|
||||
|
||||
trackUsage()
|
||||
await vi.runAllTimersAsync()
|
||||
|
||||
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}')
|
||||
expect(stored['persisted-feature']?.useCount).toBe(1)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('loads existing data from localStorage', async () => {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
'existing-feature': { useCount: 5, firstUsed: 1000, lastUsed: 2000 }
|
||||
})
|
||||
)
|
||||
|
||||
vi.resetModules()
|
||||
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
|
||||
const { useCount } = useFeatureUsageTracker('existing-feature')
|
||||
|
||||
expect(useCount.value).toBe(5)
|
||||
})
|
||||
})
|
||||
46
src/platform/surveys/useFeatureUsageTracker.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface FeatureUsage {
|
||||
useCount: number
|
||||
firstUsed: number
|
||||
lastUsed: number
|
||||
}
|
||||
|
||||
type FeatureUsageRecord = Record<string, FeatureUsage>
|
||||
|
||||
const STORAGE_KEY = 'Comfy.FeatureUsage'
|
||||
|
||||
/**
|
||||
* Tracks feature usage for survey eligibility.
|
||||
* Persists to localStorage.
|
||||
*/
|
||||
export function useFeatureUsageTracker(featureId: string) {
|
||||
const usageData = useStorage<FeatureUsageRecord>(STORAGE_KEY, {})
|
||||
|
||||
const usage = computed(() => usageData.value[featureId])
|
||||
|
||||
const useCount = computed(() => usage.value?.useCount ?? 0)
|
||||
|
||||
function trackUsage() {
|
||||
const now = Date.now()
|
||||
const existing = usageData.value[featureId]
|
||||
|
||||
usageData.value[featureId] = {
|
||||
useCount: (existing?.useCount ?? 0) + 1,
|
||||
firstUsed: existing?.firstUsed ?? now,
|
||||
lastUsed: now
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
delete usageData.value[featureId]
|
||||
}
|
||||
|
||||
return {
|
||||
usage,
|
||||
useCount,
|
||||
trackUsage,
|
||||
reset
|
||||
}
|
||||
}
|
||||
@@ -482,18 +482,30 @@ const lgraphNode = computed(() => {
|
||||
|
||||
const showAdvancedInputsButton = computed(() => {
|
||||
const node = lgraphNode.value
|
||||
if (!node || !(node instanceof SubgraphNode)) return false
|
||||
if (!node) return false
|
||||
|
||||
// Check if there are hidden inputs (widgets not promoted)
|
||||
const interiorNodes = node.subgraph.nodes
|
||||
const allInteriorWidgets = interiorNodes.flatMap((n) => n.widgets ?? [])
|
||||
// For subgraph nodes: check for unpromoted widgets
|
||||
if (node instanceof SubgraphNode) {
|
||||
const interiorNodes = node.subgraph.nodes
|
||||
const allInteriorWidgets = interiorNodes.flatMap((n) => n.widgets ?? [])
|
||||
return allInteriorWidgets.some((w) => !w.computedDisabled && !w.promoted)
|
||||
}
|
||||
|
||||
return allInteriorWidgets.some((w) => !w.computedDisabled && !w.promoted)
|
||||
// For regular nodes: show button if there are advanced widgets and they're currently hidden
|
||||
const hasAdvancedWidgets = nodeData.widgets?.some((w) => w.options?.advanced)
|
||||
return hasAdvancedWidgets && !node.showAdvanced
|
||||
})
|
||||
|
||||
function handleShowAdvancedInputs() {
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
rightSidePanelStore.focusSection('advanced-inputs')
|
||||
const node = lgraphNode.value
|
||||
if (!node) return
|
||||
|
||||
if (node instanceof SubgraphNode) {
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
rightSidePanelStore.focusSection('advanced-inputs')
|
||||
} else {
|
||||
node.showAdvanced = true
|
||||
}
|
||||
}
|
||||
|
||||
const nodeMedia = computed(() => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import Popover from 'primevue/popover'
|
||||
import RadioButton from 'primevue/radiobutton'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { ControlOptions } from '@/types/simplifiedWidget'
|
||||
@@ -14,14 +13,8 @@ type ControlOption = {
|
||||
title: string
|
||||
}
|
||||
|
||||
const popover = ref()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const toggle = (event: Event) => {
|
||||
popover.value.toggle(event)
|
||||
}
|
||||
defineExpose({ toggle })
|
||||
|
||||
const controlOptions: ControlOption[] = [
|
||||
{
|
||||
mode: 'fixed',
|
||||
@@ -57,70 +50,63 @@ const controlMode = defineModel<ControlOptions>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover
|
||||
ref="popover"
|
||||
class="bg-interface-panel-surface border border-interface-stroke rounded-lg"
|
||||
>
|
||||
<div class="w-113 max-w-md p-4 space-y-4">
|
||||
<div class="text-sm text-muted-foreground leading-tight">
|
||||
{{ $t('widgets.valueControl.header.prefix') }}
|
||||
<span class="text-base-foreground font-medium">
|
||||
{{
|
||||
widgetControlMode === 'before'
|
||||
? $t('widgets.valueControl.header.before')
|
||||
: $t('widgets.valueControl.header.after')
|
||||
}}
|
||||
</span>
|
||||
{{ $t('widgets.valueControl.header.postfix') }}
|
||||
</div>
|
||||
<div class="w-113 max-w-md p-4 space-y-4">
|
||||
<div class="text-sm text-muted-foreground leading-tight">
|
||||
{{ $t('widgets.valueControl.header.prefix') }}
|
||||
<span class="text-base-foreground font-medium">
|
||||
{{
|
||||
widgetControlMode === 'before'
|
||||
? $t('widgets.valueControl.header.before')
|
||||
: $t('widgets.valueControl.header.after')
|
||||
}}
|
||||
</span>
|
||||
{{ $t('widgets.valueControl.header.postfix') }}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="option in controlOptions"
|
||||
:key="option.mode"
|
||||
class="flex items-center justify-between py-2 gap-7"
|
||||
>
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div
|
||||
class="flex items-center justify-center w-8 h-8 rounded-lg flex-shrink-0 bg-secondary-background border border-border-subtle"
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="option in controlOptions"
|
||||
:key="option.mode"
|
||||
class="flex items-center justify-between py-2 gap-7"
|
||||
>
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div
|
||||
class="flex items-center justify-center w-8 h-8 rounded-lg flex-shrink-0 bg-secondary-background border border-border-subtle"
|
||||
>
|
||||
<i
|
||||
v-if="option.icon"
|
||||
:class="option.icon"
|
||||
class="text-base text-base-foreground"
|
||||
/>
|
||||
<span
|
||||
v-if="option.text"
|
||||
class="text-xs font-normal text-base-foreground"
|
||||
>
|
||||
<i
|
||||
v-if="option.icon"
|
||||
:class="option.icon"
|
||||
class="text-base text-base-foreground"
|
||||
/>
|
||||
<span
|
||||
v-if="option.text"
|
||||
class="text-xs font-normal text-base-foreground"
|
||||
>
|
||||
{{ option.text }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-0.5 min-w-0 flex-1">
|
||||
<div
|
||||
class="text-sm font-normal text-base-foreground leading-tight"
|
||||
>
|
||||
<span>
|
||||
{{ $t(`widgets.valueControl.${option.title}`) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="text-sm font-normal text-muted-foreground leading-tight"
|
||||
>
|
||||
{{ $t(`widgets.valueControl.${option.description}`) }}
|
||||
</div>
|
||||
</div>
|
||||
{{ option.text }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<RadioButton
|
||||
v-model="controlMode"
|
||||
class="flex-shrink-0"
|
||||
:input-id="option.mode"
|
||||
:value="option.mode"
|
||||
/>
|
||||
<div class="flex flex-col gap-0.5 min-w-0 flex-1">
|
||||
<div class="text-sm font-normal text-base-foreground leading-tight">
|
||||
<span>
|
||||
{{ $t(`widgets.valueControl.${option.title}`) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="text-sm font-normal text-muted-foreground leading-tight"
|
||||
>
|
||||
{{ $t(`widgets.valueControl.${option.description}`) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RadioButton
|
||||
v-model="controlMode"
|
||||
class="flex-shrink-0"
|
||||
:input-id="option.mode"
|
||||
:value="option.mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -24,13 +24,5 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
ref="domEl"
|
||||
@pointerdown.stop
|
||||
@pointermove.stop
|
||||
@pointerup.stop
|
||||
@mousedown.stop
|
||||
@mousemove.stop
|
||||
@mouseup.stop
|
||||
/>
|
||||
<div ref="domEl" @pointerdown.stop @pointermove.stop @pointerup.stop />
|
||||
</template>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { onClickOutside } from '@vueuse/core'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { evaluateInput } from '@/lib/litegraph/src/utils/widget'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
@@ -65,7 +66,6 @@ function updateValue(e: UIEvent) {
|
||||
textEdit.value = false
|
||||
}
|
||||
|
||||
const sharedButtonClass = 'w-8 bg-transparent border-0 text-sm text-smoke-700'
|
||||
const canDecrement = computed(
|
||||
() =>
|
||||
modelValue.value > filteredProps.value.min &&
|
||||
@@ -205,16 +205,17 @@ const sliderWidth = computed(() => {
|
||||
class="bg-primary-background/15 absolute left-0 bottom-0 h-full rounded-lg pointer-events-none"
|
||||
:style="{ width: `${sliderWidth}%` }"
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
v-if="!buttonsDisabled"
|
||||
data-testid="decrement"
|
||||
:class="
|
||||
cn(sharedButtonClass, 'pi pi-minus', !canDecrement && 'opacity-60')
|
||||
"
|
||||
class="h-full w-8 rounded-r-none hover:bg-base-foreground/20 disabled:opacity-30"
|
||||
variant="muted-textonly"
|
||||
:disabled="!canDecrement"
|
||||
tabindex="-1"
|
||||
@click="modelValue -= stepValue"
|
||||
/>
|
||||
>
|
||||
<i class="pi pi-minus" />
|
||||
</Button>
|
||||
<div class="relative min-w-[4ch] flex-1 py-1.5 my-0.25">
|
||||
<input
|
||||
ref="inputField"
|
||||
@@ -262,16 +263,17 @@ const sliderWidth = computed(() => {
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
<button
|
||||
<Button
|
||||
v-if="!buttonsDisabled"
|
||||
data-testid="increment"
|
||||
:class="
|
||||
cn(sharedButtonClass, 'pi pi-plus', !canIncrement && 'opacity-60')
|
||||
"
|
||||
class="h-full w-8 rounded-l-none hover:bg-base-foreground/20 disabled:opacity-30"
|
||||
variant="muted-textonly"
|
||||
:disabled="!canIncrement"
|
||||
tabindex="-1"
|
||||
@click="modelValue += stepValue"
|
||||
/>
|
||||
>
|
||||
<i class="pi pi-plus" />
|
||||
</Button>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type {
|
||||
SimplifiedControlWidget,
|
||||
@@ -19,8 +20,6 @@ const props = defineProps<{
|
||||
|
||||
const modelValue = defineModel<T>()
|
||||
|
||||
const popover = ref()
|
||||
|
||||
const controlModel = ref(props.widget.controlWidget.value)
|
||||
|
||||
const controlButtonIcon = computed(() => {
|
||||
@@ -37,24 +36,24 @@ const controlButtonIcon = computed(() => {
|
||||
})
|
||||
|
||||
watch(controlModel, props.widget.controlWidget.update)
|
||||
|
||||
const togglePopover = (event: Event) => {
|
||||
popover.value.toggle(event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative grid grid-cols-subgrid">
|
||||
<component :is="component" v-bind="$attrs" v-model="modelValue" :widget>
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
class="h-4 w-7 self-center rounded-xl bg-blue-100/30 p-0"
|
||||
@click.stop.prevent="togglePopover"
|
||||
>
|
||||
<i :class="`${controlButtonIcon} text-blue-100 text-xs size-3.5`" />
|
||||
</Button>
|
||||
<Popover>
|
||||
<template #button>
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
class="h-4 w-7 p-0 self-center rounded-xl bg-primary-background/30 hover:bg-primary-background-hover/30"
|
||||
>
|
||||
<i
|
||||
:class="`${controlButtonIcon} text-primary-background text-xs w-full`"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
<ValueControlPopover v-model="controlModel" />
|
||||
</Popover>
|
||||
</component>
|
||||
<ValueControlPopover ref="popover" v-model="controlModel" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -13,6 +13,9 @@ export type PromptId = z.infer<typeof zPromptId>
|
||||
export const resultItemType = z.enum(['input', 'output', 'temp'])
|
||||
export type ResultItemType = z.infer<typeof resultItemType>
|
||||
|
||||
const zCustomNodesI18n = z.record(z.string(), z.unknown())
|
||||
export type CustomNodesI18n = z.infer<typeof zCustomNodesI18n>
|
||||
|
||||
const zResultItem = z.object({
|
||||
filename: z.string().optional(),
|
||||
subfolder: z.string().optional(),
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type {
|
||||
AssetDownloadWsMessage,
|
||||
CustomNodesI18n,
|
||||
EmbeddingsResponse,
|
||||
ExecutedWsMessage,
|
||||
ExecutingWsMessage,
|
||||
@@ -35,6 +36,7 @@ import type {
|
||||
LogsRawResponse,
|
||||
LogsWsMessage,
|
||||
NotificationWsMessage,
|
||||
PreviewMethod,
|
||||
ProgressStateWsMessage,
|
||||
ProgressTextWsMessage,
|
||||
ProgressWsMessage,
|
||||
@@ -44,8 +46,7 @@ import type {
|
||||
StatusWsMessageStatus,
|
||||
SystemStats,
|
||||
User,
|
||||
UserDataFullInfo,
|
||||
PreviewMethod
|
||||
UserDataFullInfo
|
||||
} from '@/schemas/apiSchema'
|
||||
import type {
|
||||
JobDetail,
|
||||
@@ -951,7 +952,7 @@ export class ComfyApi extends EventTarget {
|
||||
* @param {*} type The endpoint to post to
|
||||
* @param {*} body Optional POST data
|
||||
*/
|
||||
async #postItem(type: string, body: any) {
|
||||
async #postItem(type: string, body?: Record<string, unknown>) {
|
||||
try {
|
||||
await this.fetchApi('/' + type, {
|
||||
method: 'POST',
|
||||
@@ -1074,7 +1075,7 @@ export class ComfyApi extends EventTarget {
|
||||
*/
|
||||
async storeUserData(
|
||||
file: string,
|
||||
data: any,
|
||||
data: unknown,
|
||||
options: RequestInit & {
|
||||
overwrite?: boolean
|
||||
stringify?: boolean
|
||||
@@ -1091,7 +1092,7 @@ export class ComfyApi extends EventTarget {
|
||||
`/userdata/${encodeURIComponent(file)}?overwrite=${options.overwrite}&full_info=${options.full_info}`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: options?.stringify ? JSON.stringify(data) : data,
|
||||
body: options?.stringify ? JSON.stringify(data) : (data as BodyInit),
|
||||
...options
|
||||
}
|
||||
)
|
||||
@@ -1251,7 +1252,7 @@ export class ComfyApi extends EventTarget {
|
||||
*
|
||||
* @returns The custom nodes i18n data
|
||||
*/
|
||||
async getCustomNodesI18n(): Promise<Record<string, any>> {
|
||||
async getCustomNodesI18n(): Promise<CustomNodesI18n> {
|
||||
return (await axios.get(this.apiURL('/i18n'))).data
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import _ from 'es-toolkit/compat'
|
||||
import * as jsondiffpatch from 'jsondiffpatch'
|
||||
import log from 'loglevel'
|
||||
|
||||
import type { CanvasPointerEvent } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
ComfyWorkflow,
|
||||
@@ -40,7 +41,7 @@ export class ChangeTracker {
|
||||
_restoringState: boolean = false
|
||||
|
||||
ds?: { scale: number; offset: [number, number] }
|
||||
nodeOutputs?: Record<string, any>
|
||||
nodeOutputs?: Record<string, ExecutedWsMessage['output']>
|
||||
|
||||
private subgraphState?: {
|
||||
navigation: string[]
|
||||
@@ -303,11 +304,11 @@ export class ChangeTracker {
|
||||
const prompt = LGraphCanvas.prototype.prompt
|
||||
LGraphCanvas.prototype.prompt = function (
|
||||
title: string,
|
||||
value: any,
|
||||
callback: (v: any) => void,
|
||||
event: any
|
||||
value: string | number,
|
||||
callback: (v: string) => void,
|
||||
event: CanvasPointerEvent
|
||||
) {
|
||||
const extendedCallback = (v: any) => {
|
||||
const extendedCallback = (v: string) => {
|
||||
callback(v)
|
||||
checkState()
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { $el } from '../../ui'
|
||||
import { ComfyDialog } from '../dialog'
|
||||
|
||||
export class ComfyAsyncDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
#resolve: (value: any) => void
|
||||
type DialogAction<T> = string | { value?: T; text: string }
|
||||
|
||||
constructor(actions?: Array<string | { value?: any; text: string }>) {
|
||||
export class ComfyAsyncDialog<
|
||||
T = string | null
|
||||
> extends ComfyDialog<HTMLDialogElement> {
|
||||
#resolve: (value: T | null) => void = () => {}
|
||||
|
||||
constructor(actions?: Array<DialogAction<T>>) {
|
||||
super(
|
||||
'dialog.comfy-dialog.comfyui-dialog',
|
||||
// @ts-expect-error fixme ts strict error
|
||||
actions?.map((opt) => {
|
||||
if (typeof opt === 'string') {
|
||||
opt = { text: opt }
|
||||
}
|
||||
const action = typeof opt === 'string' ? { text: opt } : opt
|
||||
return $el('button.comfyui-button', {
|
||||
type: 'button',
|
||||
textContent: opt.text,
|
||||
onclick: () => this.close(opt.value ?? opt.text)
|
||||
})
|
||||
textContent: action.text,
|
||||
onclick: () => this.close((action.value ?? action.text) as T)
|
||||
}) as HTMLButtonElement
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
override show(html: string | HTMLElement | HTMLElement[]) {
|
||||
override show(html: string | HTMLElement | HTMLElement[]): Promise<T | null> {
|
||||
this.element.addEventListener('close', () => {
|
||||
this.close()
|
||||
})
|
||||
@@ -34,7 +34,7 @@ export class ComfyAsyncDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
})
|
||||
}
|
||||
|
||||
showModal(html: string | HTMLElement | HTMLElement[]) {
|
||||
showModal(html: string | HTMLElement | HTMLElement[]): Promise<T | null> {
|
||||
this.element.addEventListener('close', () => {
|
||||
this.close()
|
||||
})
|
||||
@@ -47,22 +47,22 @@ export class ComfyAsyncDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
})
|
||||
}
|
||||
|
||||
override close(result = null) {
|
||||
override close(result: T | null = null) {
|
||||
this.#resolve(result)
|
||||
this.element.close()
|
||||
super.close()
|
||||
}
|
||||
|
||||
static async prompt({
|
||||
static async prompt<U = string>({
|
||||
title = null,
|
||||
message,
|
||||
actions
|
||||
}: {
|
||||
title: string | null
|
||||
message: string
|
||||
actions: Array<string | { value?: any; text: string }>
|
||||
}) {
|
||||
const dialog = new ComfyAsyncDialog(actions)
|
||||
actions: Array<DialogAction<U>>
|
||||
}): Promise<U | null> {
|
||||
const dialog = new ComfyAsyncDialog<U>(actions)
|
||||
const content = [$el('span', message)]
|
||||
if (title) {
|
||||
content.unshift($el('h3', title))
|
||||
|
||||
@@ -4,11 +4,10 @@ export class ComfyDialog<
|
||||
T extends HTMLElement = HTMLElement
|
||||
> extends EventTarget {
|
||||
element: T
|
||||
// @ts-expect-error fixme ts strict error
|
||||
textElement: HTMLElement
|
||||
textElement!: HTMLElement
|
||||
#buttons: HTMLButtonElement[] | null
|
||||
|
||||
constructor(type = 'div', buttons = null) {
|
||||
constructor(type = 'div', buttons: HTMLButtonElement[] | null = null) {
|
||||
super()
|
||||
this.#buttons = buttons
|
||||
this.element = $el(type + '.comfy-modal', { parent: document.body }, [
|
||||
@@ -35,8 +34,7 @@ export class ComfyDialog<
|
||||
this.element.style.display = 'none'
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
show(html) {
|
||||
show(html: string | HTMLElement | HTMLElement[]): void {
|
||||
if (typeof html === 'string') {
|
||||
this.textElement.innerHTML = html
|
||||
} else {
|
||||
|
||||
133
vite.config.mts
@@ -51,6 +51,14 @@ const DISTRIBUTION: 'desktop' | 'localhost' | 'cloud' =
|
||||
? 'cloud'
|
||||
: 'localhost'
|
||||
|
||||
// Nightly builds are from main branch; RC/stable builds are from core/* branches
|
||||
// Can be overridden via IS_NIGHTLY env var for testing
|
||||
const IS_NIGHTLY =
|
||||
process.env.IS_NIGHTLY === 'true' ||
|
||||
(process.env.IS_NIGHTLY !== 'false' &&
|
||||
process.env.CI === 'true' &&
|
||||
process.env.GITHUB_REF_NAME === 'main')
|
||||
|
||||
// Disable Vue DevTools for production cloud distribution
|
||||
const DISABLE_VUE_PLUGINS =
|
||||
process.env.DISABLE_VUE_PLUGINS === 'true' ||
|
||||
@@ -412,77 +420,83 @@ export default defineConfig({
|
||||
],
|
||||
|
||||
build: {
|
||||
minify: SHOULD_MINIFY ? 'esbuild' : false,
|
||||
minify: SHOULD_MINIFY,
|
||||
target: 'es2022',
|
||||
sourcemap: GENERATE_SOURCEMAP,
|
||||
rollupOptions: {
|
||||
treeshake: true,
|
||||
output: {
|
||||
manualChunks: (id) => {
|
||||
if (!id.includes('node_modules')) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (id.includes('primevue') || id.includes('@primeuix')) {
|
||||
return 'vendor-primevue'
|
||||
}
|
||||
|
||||
if (id.includes('@tiptap')) {
|
||||
return 'vendor-tiptap'
|
||||
}
|
||||
|
||||
if (id.includes('chart.js')) {
|
||||
return 'vendor-chart'
|
||||
}
|
||||
|
||||
if (id.includes('three') || id.includes('@sparkjsdev')) {
|
||||
return 'vendor-three'
|
||||
}
|
||||
|
||||
if (id.includes('@xterm')) {
|
||||
return 'vendor-xterm'
|
||||
}
|
||||
|
||||
if (id.includes('/vue') || id.includes('pinia')) {
|
||||
return 'vendor-vue'
|
||||
}
|
||||
if (id.includes('reka-ui')) {
|
||||
return 'vendor-reka-ui'
|
||||
}
|
||||
|
||||
return 'vendor-other'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
esbuild: {
|
||||
minifyIdentifiers: SHOULD_MINIFY,
|
||||
keepNames: true,
|
||||
minifySyntax: SHOULD_MINIFY,
|
||||
minifyWhitespace: SHOULD_MINIFY,
|
||||
pure: SHOULD_MINIFY
|
||||
? [
|
||||
'console.log',
|
||||
rolldownOptions: {
|
||||
treeshake: {
|
||||
manualPureFunctions: [
|
||||
'console.clear',
|
||||
'console.count',
|
||||
'console.countReset',
|
||||
'console.debug',
|
||||
'console.info',
|
||||
'console.trace',
|
||||
'console.dir',
|
||||
'console.dirxml',
|
||||
'console.group',
|
||||
'console.groupCollapsed',
|
||||
'console.groupEnd',
|
||||
'console.info',
|
||||
'console.log',
|
||||
'console.profile',
|
||||
'console.profileEnd',
|
||||
'console.table',
|
||||
'console.time',
|
||||
'console.timeEnd',
|
||||
'console.timeLog',
|
||||
'console.count',
|
||||
'console.countReset',
|
||||
'console.profile',
|
||||
'console.profileEnd',
|
||||
'console.clear'
|
||||
'console.trace'
|
||||
]
|
||||
: []
|
||||
},
|
||||
experimental: {
|
||||
strictExecutionOrder: true
|
||||
},
|
||||
output: {
|
||||
keepNames: true,
|
||||
codeSplitting: {
|
||||
groups: [
|
||||
{
|
||||
name: 'vendor-primevue',
|
||||
test: /[\\/]node_modules[\\/](@?primevue|@primeuix)[\\/]/,
|
||||
priority: 10
|
||||
},
|
||||
{
|
||||
name: 'vendor-tiptap',
|
||||
test: /[\\/]node_modules[\\/]@tiptap[\\/]/,
|
||||
priority: 10
|
||||
},
|
||||
{
|
||||
name: 'vendor-chart',
|
||||
test: /[\\/]node_modules[\\/]chart\.js[\\/]/,
|
||||
priority: 10
|
||||
},
|
||||
{
|
||||
name: 'vendor-three',
|
||||
test: /[\\/]node_modules[\\/](three|@sparkjsdev)[\\/]/,
|
||||
priority: 10
|
||||
},
|
||||
{
|
||||
name: 'vendor-xterm',
|
||||
test: /[\\/]node_modules[\\/]@xterm[\\/]/,
|
||||
priority: 10
|
||||
},
|
||||
{
|
||||
name: 'vendor-vue',
|
||||
test: /[\\/]node_modules[\\/](vue|pinia)[\\/]/,
|
||||
priority: 10
|
||||
},
|
||||
{
|
||||
name: 'vendor-reka-ui',
|
||||
test: /[\\/]node_modules[\\/]reka-ui[\\/]/,
|
||||
priority: 10
|
||||
},
|
||||
{
|
||||
name: 'vendor-other',
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
priority: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
define: {
|
||||
@@ -496,7 +510,8 @@ export default defineConfig({
|
||||
__ALGOLIA_APP_ID__: JSON.stringify(process.env.ALGOLIA_APP_ID || ''),
|
||||
__ALGOLIA_API_KEY__: JSON.stringify(process.env.ALGOLIA_API_KEY || ''),
|
||||
__USE_PROD_CONFIG__: process.env.USE_PROD_CONFIG === 'true',
|
||||
__DISTRIBUTION__: JSON.stringify(DISTRIBUTION)
|
||||
__DISTRIBUTION__: JSON.stringify(DISTRIBUTION),
|
||||
__IS_NIGHTLY__: JSON.stringify(IS_NIGHTLY)
|
||||
},
|
||||
|
||||
resolve: {
|
||||
|
||||
@@ -46,6 +46,7 @@ globalThis.__ALGOLIA_APP_ID__ = ''
|
||||
globalThis.__ALGOLIA_API_KEY__ = ''
|
||||
globalThis.__USE_PROD_CONFIG__ = false
|
||||
globalThis.__DISTRIBUTION__ = 'localhost'
|
||||
globalThis.__IS_NIGHTLY__ = false
|
||||
|
||||
// Define runtime config for tests
|
||||
window.__CONFIG__ = {
|
||||
|
||||