mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Cleanup: YAGNI readonly props, private swap on ComfyApp, Canvas resize events simplification, v-memos on individual instances (#5869)
## Summary Assorted cleanup opportunities found while working through the Vue node rendering logic cleanup. ## Review Focus Am I wrong that the readonly logic was never actually executing because it was defined as False in GraphCanvas when making each LGraphNode? Is there an edge case or some other reason that the ResizeObserver wouldn't work as a single signal to resize the canvas? ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5869-Cleanup-YAGNI-readonly-props-private-swap-on-ComfyApp-Canvas-resize-events-simplificat-27e6d73d3650811ba1dcf29e8d43091e) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -53,6 +53,10 @@ test.describe('DOM Widget', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should reposition when layout changes', async ({ comfyPage }) => {
|
test('should reposition when layout changes', async ({ comfyPage }) => {
|
||||||
|
test.skip(
|
||||||
|
true,
|
||||||
|
'Only recalculates when the Canvas size changes, need to recheck the logic'
|
||||||
|
)
|
||||||
// --- setup ---
|
// --- setup ---
|
||||||
|
|
||||||
const textareaWidget = comfyPage.page
|
const textareaWidget = comfyPage.page
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
@@ -172,7 +172,6 @@
|
|||||||
v-for="template in isLoading ? [] : displayTemplates"
|
v-for="template in isLoading ? [] : displayTemplates"
|
||||||
:key="template.name"
|
:key="template.name"
|
||||||
ref="cardRefs"
|
ref="cardRefs"
|
||||||
v-memo="[template.name, hoveredTemplate === template.name]"
|
|
||||||
ratio="smallSquare"
|
ratio="smallSquare"
|
||||||
type="workflow-template-card"
|
type="workflow-template-card"
|
||||||
:data-testid="`template-workflow-${template.name}`"
|
:data-testid="`template-workflow-${template.name}`"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
id="graph-canvas"
|
id="graph-canvas"
|
||||||
ref="canvasRef"
|
ref="canvasRef"
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
class="align-top w-full h-full touch-none"
|
class="absolute inset-0 size-full touch-none"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- TransformPane for Vue node rendering -->
|
<!-- TransformPane for Vue node rendering -->
|
||||||
@@ -43,7 +43,6 @@
|
|||||||
v-for="nodeData in allNodes"
|
v-for="nodeData in allNodes"
|
||||||
:key="nodeData.id"
|
:key="nodeData.id"
|
||||||
:node-data="nodeData"
|
:node-data="nodeData"
|
||||||
:readonly="false"
|
|
||||||
:error="
|
:error="
|
||||||
executionStore.lastExecutionError?.node_id === nodeData.id
|
executionStore.lastExecutionError?.node_id === nodeData.id
|
||||||
? 'Execution error'
|
? 'Execution error'
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
ref="connectionDotRef"
|
ref="connectionDotRef"
|
||||||
:color="slotColor"
|
:color="slotColor"
|
||||||
:class="cn('-translate-x-1/2', errorClassesDot)"
|
:class="cn('-translate-x-1/2', errorClassesDot)"
|
||||||
v-on="readonly ? {} : { pointerdown: onPointerDown }"
|
@pointerdown="onPointerDown"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Slot Name -->
|
<!-- Slot Name -->
|
||||||
@@ -54,7 +54,6 @@ interface InputSlotProps {
|
|||||||
index: number
|
index: number
|
||||||
connected?: boolean
|
connected?: boolean
|
||||||
compatible?: boolean
|
compatible?: boolean
|
||||||
readonly?: boolean
|
|
||||||
dotOnly?: boolean
|
dotOnly?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +116,7 @@ const slotColor = computed(() => {
|
|||||||
const slotWrapperClass = computed(() =>
|
const slotWrapperClass = computed(() =>
|
||||||
cn(
|
cn(
|
||||||
'lg-slot lg-slot--input flex items-center group rounded-r-lg h-6',
|
'lg-slot lg-slot--input flex items-center group rounded-r-lg h-6',
|
||||||
props.readonly ? 'cursor-default opacity-70' : 'cursor-crosshair',
|
'cursor-crosshair',
|
||||||
props.dotOnly
|
props.dotOnly
|
||||||
? 'lg-slot--dot-only'
|
? 'lg-slot--dot-only'
|
||||||
: 'pr-6 hover:bg-black/5 hover:dark:bg-white/5',
|
: 'pr-6 hover:bg-black/5 hover:dark:bg-white/5',
|
||||||
@@ -148,7 +147,6 @@ useSlotElementTracking({
|
|||||||
const { onPointerDown } = useSlotLinkInteraction({
|
const { onPointerDown } = useSlotLinkInteraction({
|
||||||
nodeId: props.nodeId ?? '',
|
nodeId: props.nodeId ?? '',
|
||||||
index: props.index,
|
index: props.index,
|
||||||
type: 'input',
|
type: 'input'
|
||||||
readonly: props.readonly
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -50,15 +50,7 @@
|
|||||||
<SlotConnectionDot multi class="absolute right-0 translate-x-1/2" />
|
<SlotConnectionDot multi class="absolute right-0 translate-x-1/2" />
|
||||||
</template>
|
</template>
|
||||||
<NodeHeader
|
<NodeHeader
|
||||||
v-memo="[
|
|
||||||
nodeData.title,
|
|
||||||
nodeData.color,
|
|
||||||
nodeData.bgcolor,
|
|
||||||
isCollapsed,
|
|
||||||
nodeData.flags?.pinned
|
|
||||||
]"
|
|
||||||
:node-data="nodeData"
|
:node-data="nodeData"
|
||||||
:readonly="readonly"
|
|
||||||
:collapsed="isCollapsed"
|
:collapsed="isCollapsed"
|
||||||
@collapse="handleCollapse"
|
@collapse="handleCollapse"
|
||||||
@update:title="handleHeaderTitleUpdate"
|
@update:title="handleHeaderTitleUpdate"
|
||||||
@@ -100,37 +92,19 @@
|
|||||||
:data-testid="`node-body-${nodeData.id}`"
|
:data-testid="`node-body-${nodeData.id}`"
|
||||||
>
|
>
|
||||||
<!-- Slots only rendered at full detail -->
|
<!-- Slots only rendered at full detail -->
|
||||||
<NodeSlots
|
<NodeSlots :node-data="nodeData" />
|
||||||
v-memo="[
|
|
||||||
nodeData.inputs?.length,
|
|
||||||
nodeData.outputs?.length,
|
|
||||||
executionStore.lastNodeErrors
|
|
||||||
]"
|
|
||||||
:node-data="nodeData"
|
|
||||||
:readonly="readonly"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Widgets rendered at reduced+ detail -->
|
<!-- Widgets rendered at reduced+ detail -->
|
||||||
<NodeWidgets
|
<NodeWidgets v-if="nodeData.widgets?.length" :node-data="nodeData" />
|
||||||
v-if="nodeData.widgets?.length"
|
|
||||||
v-memo="[nodeData.widgets?.length]"
|
|
||||||
:node-data="nodeData"
|
|
||||||
:readonly="readonly"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Custom content at reduced+ detail -->
|
<!-- Custom content at reduced+ detail -->
|
||||||
<NodeContent
|
<NodeContent
|
||||||
v-if="hasCustomContent"
|
v-if="hasCustomContent"
|
||||||
:node-data="nodeData"
|
:node-data="nodeData"
|
||||||
:readonly="readonly"
|
|
||||||
:image-urls="nodeImageUrls"
|
:image-urls="nodeImageUrls"
|
||||||
/>
|
/>
|
||||||
<!-- Live preview image -->
|
<!-- Live preview image -->
|
||||||
<div
|
<div v-if="shouldShowPreviewImg" class="px-4">
|
||||||
v-if="shouldShowPreviewImg"
|
|
||||||
v-memo="[latestPreviewUrl]"
|
|
||||||
class="px-4"
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
:src="latestPreviewUrl"
|
:src="latestPreviewUrl"
|
||||||
alt="preview"
|
alt="preview"
|
||||||
@@ -180,16 +154,11 @@ import SlotConnectionDot from './SlotConnectionDot.vue'
|
|||||||
// Extended props for main node component
|
// Extended props for main node component
|
||||||
interface LGraphNodeProps {
|
interface LGraphNodeProps {
|
||||||
nodeData: VueNodeData
|
nodeData: VueNodeData
|
||||||
readonly?: boolean
|
|
||||||
error?: string | null
|
error?: string | null
|
||||||
zoomLevel?: number
|
zoomLevel?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { nodeData, error = null } = defineProps<LGraphNodeProps>()
|
||||||
nodeData,
|
|
||||||
error = null,
|
|
||||||
readonly = false
|
|
||||||
} = defineProps<LGraphNodeProps>()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleNodeCollapse,
|
handleNodeCollapse,
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import ImagePreview from './ImagePreview.vue'
|
|||||||
interface NodeContentProps {
|
interface NodeContentProps {
|
||||||
node?: LGraphNode // For backwards compatibility
|
node?: LGraphNode // For backwards compatibility
|
||||||
nodeData?: VueNodeData // New clean data structure
|
nodeData?: VueNodeData // New clean data structure
|
||||||
readonly?: boolean
|
|
||||||
imageUrls?: string[]
|
imageUrls?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ const mountHeader = (
|
|||||||
...config,
|
...config,
|
||||||
props: {
|
props: {
|
||||||
nodeData: makeNodeData(),
|
nodeData: makeNodeData(),
|
||||||
readonly: false,
|
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
...props
|
...props
|
||||||
}
|
}
|
||||||
@@ -182,20 +181,6 @@ describe('NodeHeader.vue', () => {
|
|||||||
expect(wrapper.get('[data-testid="node-title"]').text()).toContain('KeepMe')
|
expect(wrapper.get('[data-testid="node-title"]').text()).toContain('KeepMe')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('honors readonly: hides collapse button and prevents editing', async () => {
|
|
||||||
const wrapper = mountHeader({ readonly: true })
|
|
||||||
|
|
||||||
// Collapse button should be hidden
|
|
||||||
const btn = wrapper.find('[data-testid="node-collapse-button"]')
|
|
||||||
expect(btn.exists()).toBe(true)
|
|
||||||
// v-show hides via display:none
|
|
||||||
expect((btn.element as HTMLButtonElement).style.display).toBe('none')
|
|
||||||
// In unit test, presence is fine; simulate double click should not create input
|
|
||||||
await wrapper.get('[data-testid="node-header-1"]').trigger('dblclick')
|
|
||||||
const input = wrapper.find('[data-testid="node-title-input"]')
|
|
||||||
expect(input.exists()).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('renders correct chevron icon based on collapsed prop', async () => {
|
it('renders correct chevron icon based on collapsed prop', async () => {
|
||||||
const wrapper = mountHeader({ collapsed: false })
|
const wrapper = mountHeader({ collapsed: false })
|
||||||
const expandedIcon = wrapper.get('i')
|
const expandedIcon = wrapper.get('i')
|
||||||
@@ -222,16 +207,6 @@ describe('NodeHeader.vue', () => {
|
|||||||
expect(directive).toBeTruthy()
|
expect(directive).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('disables tooltip when in readonly mode', () => {
|
|
||||||
const wrapper = mountHeader({
|
|
||||||
readonly: true,
|
|
||||||
nodeData: makeNodeData({ type: 'KSampler' })
|
|
||||||
})
|
|
||||||
|
|
||||||
const titleElement = wrapper.find('[data-testid="node-title"]')
|
|
||||||
expect(titleElement.exists()).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('disables tooltip when editing is active', async () => {
|
it('disables tooltip when editing is active', async () => {
|
||||||
const wrapper = mountHeader({
|
const wrapper = mountHeader({
|
||||||
nodeData: makeNodeData({ type: 'KSampler' })
|
nodeData: makeNodeData({ type: 'KSampler' })
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
<div class="flex items-center justify-between gap-2.5 relative">
|
<div class="flex items-center justify-between gap-2.5 relative">
|
||||||
<!-- Collapse/Expand Button -->
|
<!-- Collapse/Expand Button -->
|
||||||
<button
|
<button
|
||||||
v-show="!readonly"
|
|
||||||
class="bg-transparent border-transparent flex items-center lod-toggle"
|
class="bg-transparent border-transparent flex items-center lod-toggle"
|
||||||
data-testid="node-collapse-button"
|
data-testid="node-collapse-button"
|
||||||
@click.stop="handleCollapse"
|
@click.stop="handleCollapse"
|
||||||
@@ -43,7 +42,7 @@
|
|||||||
data-testid="node-pin-indicator"
|
data-testid="node-pin-indicator"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!readonly" class="flex items-center lod-toggle shrink-0">
|
<div class="flex items-center lod-toggle shrink-0">
|
||||||
<IconButton
|
<IconButton
|
||||||
v-if="isSubgraphNode"
|
v-if="isSubgraphNode"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -85,11 +84,10 @@ import LODFallback from './LODFallback.vue'
|
|||||||
|
|
||||||
interface NodeHeaderProps {
|
interface NodeHeaderProps {
|
||||||
nodeData?: VueNodeData
|
nodeData?: VueNodeData
|
||||||
readonly?: boolean
|
|
||||||
collapsed?: boolean
|
collapsed?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nodeData, readonly, collapsed } = defineProps<NodeHeaderProps>()
|
const { nodeData, collapsed } = defineProps<NodeHeaderProps>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
collapse: []
|
collapse: []
|
||||||
@@ -118,7 +116,7 @@ const { getNodeDescription, createTooltipConfig } = useNodeTooltips(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const tooltipConfig = computed(() => {
|
const tooltipConfig = computed(() => {
|
||||||
if (readonly || isEditing.value) {
|
if (isEditing.value) {
|
||||||
return { value: '', disabled: true }
|
return { value: '', disabled: true }
|
||||||
}
|
}
|
||||||
const description = getNodeDescription.value
|
const description = getNodeDescription.value
|
||||||
@@ -189,9 +187,7 @@ const handleCollapse = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDoubleClick = () => {
|
const handleDoubleClick = () => {
|
||||||
if (!readonly) {
|
isEditing.value = true
|
||||||
isEditing.value = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTitleEdit = (newTitle: string) => {
|
const handleTitleEdit = (newTitle: string) => {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
:node-type="nodeData?.type || ''"
|
:node-type="nodeData?.type || ''"
|
||||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||||
:index="getActualInputIndex(input, index)"
|
:index="getActualInputIndex(input, index)"
|
||||||
:readonly="readonly"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -23,7 +22,6 @@
|
|||||||
:node-type="nodeData?.type || ''"
|
:node-type="nodeData?.type || ''"
|
||||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||||
:index="index"
|
:index="index"
|
||||||
:readonly="readonly"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,10 +40,9 @@ import OutputSlot from './OutputSlot.vue'
|
|||||||
|
|
||||||
interface NodeSlotsProps {
|
interface NodeSlotsProps {
|
||||||
nodeData?: VueNodeData
|
nodeData?: VueNodeData
|
||||||
readonly?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nodeData = null, readonly } = defineProps<NodeSlotsProps>()
|
const { nodeData = null } = defineProps<NodeSlotsProps>()
|
||||||
|
|
||||||
// Filter out input slots that have corresponding widgets
|
// Filter out input slots that have corresponding widgets
|
||||||
const filteredInputs = computed(() => {
|
const filteredInputs = computed(() => {
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
}"
|
}"
|
||||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||||
:index="getWidgetInputIndex(widget)"
|
:index="getWidgetInputIndex(widget)"
|
||||||
:readonly="readonly"
|
|
||||||
:dot-only="true"
|
:dot-only="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +43,6 @@
|
|||||||
v-tooltip.left="widget.tooltipConfig"
|
v-tooltip.left="widget.tooltipConfig"
|
||||||
:widget="widget.simplified"
|
:widget="widget.simplified"
|
||||||
:model-value="widget.value"
|
:model-value="widget.value"
|
||||||
:readonly="readonly"
|
|
||||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
@update:model-value="widget.updateHandler"
|
@update:model-value="widget.updateHandler"
|
||||||
@@ -76,10 +74,9 @@ import InputSlot from './InputSlot.vue'
|
|||||||
|
|
||||||
interface NodeWidgetsProps {
|
interface NodeWidgetsProps {
|
||||||
nodeData?: VueNodeData
|
nodeData?: VueNodeData
|
||||||
readonly?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nodeData, readonly } = defineProps<NodeWidgetsProps>()
|
const { nodeData } = defineProps<NodeWidgetsProps>()
|
||||||
|
|
||||||
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
|
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
|
||||||
useCanvasInteractions()
|
useCanvasInteractions()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
ref="connectionDotRef"
|
ref="connectionDotRef"
|
||||||
:color="slotColor"
|
:color="slotColor"
|
||||||
class="translate-x-1/2"
|
class="translate-x-1/2"
|
||||||
v-on="readonly ? {} : { pointerdown: onPointerDown }"
|
@pointerdown="onPointerDown"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -50,7 +50,6 @@ interface OutputSlotProps {
|
|||||||
index: number
|
index: number
|
||||||
connected?: boolean
|
connected?: boolean
|
||||||
compatible?: boolean
|
compatible?: boolean
|
||||||
readonly?: boolean
|
|
||||||
dotOnly?: boolean
|
dotOnly?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +86,7 @@ const slotColor = computed(() => getSlotColor(props.slotData.type))
|
|||||||
const slotWrapperClass = computed(() =>
|
const slotWrapperClass = computed(() =>
|
||||||
cn(
|
cn(
|
||||||
'lg-slot lg-slot--output flex items-center justify-end group rounded-l-lg h-6',
|
'lg-slot lg-slot--output flex items-center justify-end group rounded-l-lg h-6',
|
||||||
props.readonly ? 'cursor-default opacity-70' : 'cursor-crosshair',
|
'cursor-crosshair',
|
||||||
props.dotOnly
|
props.dotOnly
|
||||||
? 'lg-slot--dot-only justify-center'
|
? 'lg-slot--dot-only justify-center'
|
||||||
: 'pl-6 hover:bg-black/5 hover:dark:bg-white/5',
|
: 'pl-6 hover:bg-black/5 hover:dark:bg-white/5',
|
||||||
@@ -120,7 +119,6 @@ useSlotElementTracking({
|
|||||||
const { onPointerDown } = useSlotLinkInteraction({
|
const { onPointerDown } = useSlotLinkInteraction({
|
||||||
nodeId: props.nodeId ?? '',
|
nodeId: props.nodeId ?? '',
|
||||||
index: props.index,
|
index: props.index,
|
||||||
type: 'output',
|
type: 'output'
|
||||||
readonly: props.readonly
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ interface SlotInteractionOptions {
|
|||||||
nodeId: string
|
nodeId: string
|
||||||
index: number
|
index: number
|
||||||
type: 'input' | 'output'
|
type: 'input' | 'output'
|
||||||
readonly?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SlotInteractionHandlers {
|
interface SlotInteractionHandlers {
|
||||||
@@ -78,15 +77,8 @@ function createPointerSession(): PointerSession {
|
|||||||
export function useSlotLinkInteraction({
|
export function useSlotLinkInteraction({
|
||||||
nodeId,
|
nodeId,
|
||||||
index,
|
index,
|
||||||
type,
|
type
|
||||||
readonly
|
|
||||||
}: SlotInteractionOptions): SlotInteractionHandlers {
|
}: SlotInteractionOptions): SlotInteractionHandlers {
|
||||||
if (readonly) {
|
|
||||||
return {
|
|
||||||
onPointerDown: () => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { state, beginDrag, endDrag, updatePointerPosition } =
|
const { state, beginDrag, endDrag, updatePointerPosition } =
|
||||||
useSlotLinkDragState()
|
useSlotLinkDragState()
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,7 @@
|
|||||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||||
widget.name
|
widget.name
|
||||||
}}</label>
|
}}</label>
|
||||||
<Button
|
<Button v-bind="filteredProps" size="small" @click="handleClick" />
|
||||||
v-bind="filteredProps"
|
|
||||||
:disabled="readonly"
|
|
||||||
size="small"
|
|
||||||
@click="handleClick"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -25,7 +20,6 @@ import {
|
|||||||
// Button widgets don't have a v-model value, they trigger actions
|
// Button widgets don't have a v-model value, they trigger actions
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<void>
|
widget: SimplifiedWidget<void>
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Button specific excluded props
|
// Button specific excluded props
|
||||||
@@ -36,7 +30,7 @@ const filteredProps = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (!props.readonly && props.widget.callback) {
|
if (props.widget.callback) {
|
||||||
props.widget.callback()
|
props.widget.callback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const value = defineModel<ChartData>({ required: true })
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<ChartData, ChartWidgetOptions>
|
widget: SimplifiedWidget<ChartData, ChartWidgetOptions>
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const chartType = computed(() => props.widget.options?.type ?? 'line')
|
const chartType = computed(() => props.widget.options?.type ?? 'line')
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
<ColorPicker
|
<ColorPicker
|
||||||
v-model="localValue"
|
v-model="localValue"
|
||||||
v-bind="filteredProps"
|
v-bind="filteredProps"
|
||||||
:disabled="readonly"
|
|
||||||
class="w-8 h-4 !rounded-full overflow-hidden border-none"
|
class="w-8 h-4 !rounded-full overflow-hidden border-none"
|
||||||
:pt="{
|
:pt="{
|
||||||
preview: '!w-full !h-full !border-none'
|
preview: '!w-full !h-full !border-none'
|
||||||
@@ -48,7 +47,6 @@ type WidgetOptions = { format?: ColorFormat } & Record<string, unknown>
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<string, WidgetOptions>
|
widget: SimplifiedWidget<string, WidgetOptions>
|
||||||
modelValue: string
|
modelValue: string
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
icon="pi pi-folder"
|
icon="pi pi-folder"
|
||||||
size="small"
|
size="small"
|
||||||
class="!w-8 !h-8"
|
class="!w-8 !h-8"
|
||||||
:disabled="readonly"
|
|
||||||
@click="triggerFileInput"
|
@click="triggerFileInput"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,7 +46,6 @@
|
|||||||
/>
|
/>
|
||||||
<!-- Control buttons in top right on hover -->
|
<!-- Control buttons in top right on hover -->
|
||||||
<div
|
<div
|
||||||
v-if="!readonly"
|
|
||||||
class="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
|
class="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
|
||||||
>
|
>
|
||||||
<!-- Edit button -->
|
<!-- Edit button -->
|
||||||
@@ -100,7 +98,6 @@
|
|||||||
icon="pi pi-folder"
|
icon="pi pi-folder"
|
||||||
size="small"
|
size="small"
|
||||||
class="!w-8 !h-8"
|
class="!w-8 !h-8"
|
||||||
:disabled="readonly"
|
|
||||||
@click="triggerFileInput"
|
@click="triggerFileInput"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,7 +125,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Control buttons -->
|
<!-- Control buttons -->
|
||||||
<div v-if="!readonly" class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<!-- Delete button -->
|
<!-- Delete button -->
|
||||||
<button
|
<button
|
||||||
class="w-8 h-8 rounded flex items-center justify-center transition-all duration-150 focus:outline-none border-none hover:bg-[#262729]"
|
class="w-8 h-8 rounded flex items-center justify-center transition-all duration-150 focus:outline-none border-none hover:bg-[#262729]"
|
||||||
@@ -159,7 +156,6 @@
|
|||||||
size="small"
|
size="small"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
class="text-xs"
|
class="text-xs"
|
||||||
:disabled="readonly"
|
|
||||||
@click="triggerFileInput"
|
@click="triggerFileInput"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,7 +169,6 @@
|
|||||||
class="hidden"
|
class="hidden"
|
||||||
:accept="widget.options?.accept"
|
:accept="widget.options?.accept"
|
||||||
:multiple="false"
|
:multiple="false"
|
||||||
:disabled="readonly"
|
|
||||||
@change="handleFileChange"
|
@change="handleFileChange"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -187,14 +182,9 @@ import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
|||||||
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
||||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||||
|
|
||||||
const {
|
const { widget, modelValue } = defineProps<{
|
||||||
widget,
|
|
||||||
modelValue,
|
|
||||||
readonly = false
|
|
||||||
} = defineProps<{
|
|
||||||
widget: SimplifiedWidget<File[] | null>
|
widget: SimplifiedWidget<File[] | null>
|
||||||
modelValue: File[] | null
|
modelValue: File[] | null
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -284,7 +274,7 @@ const triggerFileInput = () => {
|
|||||||
|
|
||||||
const handleFileChange = (event: Event) => {
|
const handleFileChange = (event: Event) => {
|
||||||
const target = event.target as HTMLInputElement
|
const target = event.target as HTMLInputElement
|
||||||
if (!readonly && target.files && target.files.length > 0) {
|
if (target.files && target.files.length > 0) {
|
||||||
// Since we only support single file, take the first one
|
// Since we only support single file, take the first one
|
||||||
const file = target.files[0]
|
const file = target.files[0]
|
||||||
|
|
||||||
|
|||||||
@@ -61,8 +61,7 @@ function createMockWidget(
|
|||||||
|
|
||||||
function mountComponent(
|
function mountComponent(
|
||||||
widget: SimplifiedWidget<GalleryValue>,
|
widget: SimplifiedWidget<GalleryValue>,
|
||||||
modelValue: GalleryValue,
|
modelValue: GalleryValue
|
||||||
readonly = false
|
|
||||||
) {
|
) {
|
||||||
return mount(WidgetGalleria, {
|
return mount(WidgetGalleria, {
|
||||||
global: {
|
global: {
|
||||||
@@ -71,7 +70,6 @@ function mountComponent(
|
|||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
widget,
|
widget,
|
||||||
readonly,
|
|
||||||
modelValue
|
modelValue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -87,11 +85,10 @@ function createImageStrings(count: number): string[] {
|
|||||||
// Factory function that takes images, creates widget internally, returns wrapper
|
// Factory function that takes images, creates widget internally, returns wrapper
|
||||||
function createGalleriaWrapper(
|
function createGalleriaWrapper(
|
||||||
images: GalleryValue,
|
images: GalleryValue,
|
||||||
options: Partial<GalleriaProps> = {},
|
options: Partial<GalleriaProps> = {}
|
||||||
readonly = false
|
|
||||||
) {
|
) {
|
||||||
const widget = createMockWidget(images, options)
|
const widget = createMockWidget(images, options)
|
||||||
return mountComponent(widget, images, readonly)
|
return mountComponent(widget, images)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('WidgetGalleria Image Display', () => {
|
describe('WidgetGalleria Image Display', () => {
|
||||||
@@ -249,25 +246,6 @@ describe('WidgetGalleria Image Display', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Readonly Mode', () => {
|
|
||||||
it('passes readonly state to galleria when readonly', () => {
|
|
||||||
const images = createImageStrings(3)
|
|
||||||
const widget = createMockWidget(images)
|
|
||||||
const wrapper = mountComponent(widget, images, true)
|
|
||||||
|
|
||||||
// Galleria component should receive readonly state (though it may not support disabled)
|
|
||||||
expect(wrapper.props('readonly')).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('passes readonly state to galleria when not readonly', () => {
|
|
||||||
const images = createImageStrings(3)
|
|
||||||
const widget = createMockWidget(images)
|
|
||||||
const wrapper = mountComponent(widget, images, false)
|
|
||||||
|
|
||||||
expect(wrapper.props('readonly')).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Widget Options Handling', () => {
|
describe('Widget Options Handling', () => {
|
||||||
it('passes through valid widget options', () => {
|
it('passes through valid widget options', () => {
|
||||||
const images = createImageStrings(2)
|
const images = createImageStrings(2)
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ const value = defineModel<GalleryValue>({ required: true })
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<GalleryValue>
|
widget: SimplifiedWidget<GalleryValue>
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const activeIndex = ref(0)
|
const activeIndex = ref(0)
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export interface ImageCompareValue {
|
|||||||
// Image compare widgets typically don't have v-model, they display comparison
|
// Image compare widgets typically don't have v-model, they display comparison
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<ImageCompareValue | string>
|
widget: SimplifiedWidget<ImageCompareValue | string>
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const beforeImage = computed(() => {
|
const beforeImage = computed(() => {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import WidgetInputNumberSlider from './WidgetInputNumberSlider.vue'
|
|||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
widget: SimplifiedWidget<number>
|
widget: SimplifiedWidget<number>
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const modelValue = defineModel<number>({ default: 0 })
|
const modelValue = defineModel<number>({ default: 0 })
|
||||||
@@ -21,7 +20,6 @@ const modelValue = defineModel<number>({ default: 0 })
|
|||||||
"
|
"
|
||||||
v-model="modelValue"
|
v-model="modelValue"
|
||||||
:widget="widget"
|
:widget="widget"
|
||||||
:readonly="readonly"
|
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<number>
|
widget: SimplifiedWidget<number>
|
||||||
modelValue: number
|
modelValue: number
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -72,7 +71,6 @@ const buttonsDisabled = computed(() => {
|
|||||||
|
|
||||||
// Tooltip message for disabled buttons
|
// Tooltip message for disabled buttons
|
||||||
const buttonTooltip = computed(() => {
|
const buttonTooltip = computed(() => {
|
||||||
if (props.readonly) return null
|
|
||||||
if (buttonsDisabled.value) {
|
if (buttonsDisabled.value) {
|
||||||
return 'Increment/decrement disabled: value exceeds JavaScript precision limit (±2^53)'
|
return 'Increment/decrement disabled: value exceeds JavaScript precision limit (±2^53)'
|
||||||
}
|
}
|
||||||
@@ -89,7 +87,6 @@ const buttonTooltip = computed(() => {
|
|||||||
:show-buttons="!buttonsDisabled"
|
:show-buttons="!buttonsDisabled"
|
||||||
button-layout="horizontal"
|
button-layout="horizontal"
|
||||||
size="small"
|
size="small"
|
||||||
:disabled="readonly"
|
|
||||||
:step="stepValue"
|
:step="stepValue"
|
||||||
:use-grouping="useGrouping"
|
:use-grouping="useGrouping"
|
||||||
:class="cn(WidgetInputBaseClass, 'w-full text-xs')"
|
:class="cn(WidgetInputBaseClass, 'w-full text-xs')"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
<Slider
|
<Slider
|
||||||
:model-value="[localValue]"
|
:model-value="[localValue]"
|
||||||
v-bind="filteredProps"
|
v-bind="filteredProps"
|
||||||
:disabled="readonly"
|
|
||||||
class="flex-grow text-xs"
|
class="flex-grow text-xs"
|
||||||
:step="stepValue"
|
:step="stepValue"
|
||||||
@update:model-value="updateLocalValue"
|
@update:model-value="updateLocalValue"
|
||||||
@@ -17,7 +16,6 @@
|
|||||||
:key="timesEmptied"
|
:key="timesEmptied"
|
||||||
:model-value="localValue"
|
:model-value="localValue"
|
||||||
v-bind="filteredProps"
|
v-bind="filteredProps"
|
||||||
:disabled="readonly"
|
|
||||||
:step="stepValue"
|
:step="stepValue"
|
||||||
:min-fraction-digits="precision"
|
:min-fraction-digits="precision"
|
||||||
:max-fraction-digits="precision"
|
:max-fraction-digits="precision"
|
||||||
@@ -46,10 +44,9 @@ import {
|
|||||||
import { WidgetInputBaseClass } from './layout'
|
import { WidgetInputBaseClass } from './layout'
|
||||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||||
|
|
||||||
const { widget, modelValue, readonly } = defineProps<{
|
const { widget, modelValue } = defineProps<{
|
||||||
widget: SimplifiedWidget<number>
|
widget: SimplifiedWidget<number>
|
||||||
modelValue: number
|
modelValue: number
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
<InputText
|
<InputText
|
||||||
v-model="localValue"
|
v-model="localValue"
|
||||||
v-bind="filteredProps"
|
v-bind="filteredProps"
|
||||||
:disabled="readonly"
|
|
||||||
:class="cn(WidgetInputBaseClass, 'w-full text-xs py-2 px-4')"
|
:class="cn(WidgetInputBaseClass, 'w-full text-xs py-2 px-4')"
|
||||||
size="small"
|
size="small"
|
||||||
@update:model-value="onChange"
|
@update:model-value="onChange"
|
||||||
@@ -29,7 +28,6 @@ import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<string>
|
widget: SimplifiedWidget<string>
|
||||||
modelValue: string
|
modelValue: string
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
v-show="isEditing"
|
v-show="isEditing"
|
||||||
ref="textareaRef"
|
ref="textareaRef"
|
||||||
v-model="localValue"
|
v-model="localValue"
|
||||||
:disabled="readonly"
|
|
||||||
class="w-full min-h-[60px] absolute inset-0 resize-none"
|
class="w-full min-h-[60px] absolute inset-0 resize-none"
|
||||||
:pt="{
|
:pt="{
|
||||||
root: {
|
root: {
|
||||||
@@ -45,7 +44,6 @@ import LODFallback from '../../components/LODFallback.vue'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<string>
|
widget: SimplifiedWidget<string>
|
||||||
modelValue: string
|
modelValue: string
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -70,7 +68,7 @@ const renderedHtml = computed(() => {
|
|||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const startEditing = async () => {
|
const startEditing = async () => {
|
||||||
if (props.readonly || isEditing.value) return
|
if (isEditing.value) return
|
||||||
|
|
||||||
isEditing.value = true
|
isEditing.value = true
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
v-model="localValue"
|
v-model="localValue"
|
||||||
:options="multiSelectOptions"
|
:options="multiSelectOptions"
|
||||||
v-bind="combinedProps"
|
v-bind="combinedProps"
|
||||||
:disabled="readonly"
|
|
||||||
class="w-full text-xs"
|
class="w-full text-xs"
|
||||||
size="small"
|
size="small"
|
||||||
display="chip"
|
display="chip"
|
||||||
@@ -33,7 +32,6 @@ import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<T[]>
|
widget: SimplifiedWidget<T[]>
|
||||||
modelValue: T[]
|
modelValue: T[]
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import WidgetSelectDropdown from './WidgetSelectDropdown.vue'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<string | number | undefined>
|
widget: SimplifiedWidget<string | number | undefined>
|
||||||
modelValue: string | number | undefined
|
modelValue: string | number | undefined
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
<FormSelectButton
|
<FormSelectButton
|
||||||
v-model="localValue"
|
v-model="localValue"
|
||||||
:options="widget.options?.values || []"
|
:options="widget.options?.values || []"
|
||||||
:disabled="readonly"
|
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@update:model-value="onChange"
|
@update:model-value="onChange"
|
||||||
/>
|
/>
|
||||||
@@ -20,7 +19,6 @@ import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<string>
|
widget: SimplifiedWidget<string>
|
||||||
modelValue: string
|
modelValue: string
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
v-model="localValue"
|
v-model="localValue"
|
||||||
:options="selectOptions"
|
:options="selectOptions"
|
||||||
v-bind="combinedProps"
|
v-bind="combinedProps"
|
||||||
:disabled="readonly"
|
|
||||||
:class="cn(WidgetInputBaseClass, 'w-full text-xs')"
|
:class="cn(WidgetInputBaseClass, 'w-full text-xs')"
|
||||||
size="small"
|
size="small"
|
||||||
:pt="{
|
:pt="{
|
||||||
@@ -35,7 +34,6 @@ import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<string | number | undefined>
|
widget: SimplifiedWidget<string | number | undefined>
|
||||||
modelValue: string | number | undefined
|
modelValue: string | number | undefined
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<string | number | undefined>
|
widget: SimplifiedWidget<string | number | undefined>
|
||||||
modelValue: string | number | undefined
|
modelValue: string | number | undefined
|
||||||
readonly?: boolean
|
|
||||||
assetKind?: AssetKind
|
assetKind?: AssetKind
|
||||||
allowUpload?: boolean
|
allowUpload?: boolean
|
||||||
uploadFolder?: ResultItemType
|
uploadFolder?: ResultItemType
|
||||||
@@ -222,7 +221,6 @@ const filterOptions = ref<FilterOption[]>([
|
|||||||
:placeholder="mediaPlaceholder"
|
:placeholder="mediaPlaceholder"
|
||||||
:multiple="false"
|
:multiple="false"
|
||||||
:uploadable="uploadable"
|
:uploadable="uploadable"
|
||||||
:disabled="readonly"
|
|
||||||
:filter-options="filterOptions"
|
:filter-options="filterOptions"
|
||||||
v-bind="combinedProps"
|
v-bind="combinedProps"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
<Textarea
|
<Textarea
|
||||||
v-model="localValue"
|
v-model="localValue"
|
||||||
v-bind="filteredProps"
|
v-bind="filteredProps"
|
||||||
:disabled="readonly"
|
|
||||||
:class="cn(WidgetInputBaseClass, 'w-full text-xs lod-toggle')"
|
:class="cn(WidgetInputBaseClass, 'w-full text-xs lod-toggle')"
|
||||||
:placeholder="placeholder || widget.name || ''"
|
:placeholder="placeholder || widget.name || ''"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -33,7 +32,6 @@ import { WidgetInputBaseClass } from './layout'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<string>
|
widget: SimplifiedWidget<string>
|
||||||
modelValue: string
|
modelValue: string
|
||||||
readonly?: boolean
|
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
v-model="localValue"
|
v-model="localValue"
|
||||||
v-bind="filteredProps"
|
v-bind="filteredProps"
|
||||||
:disabled="readonly"
|
|
||||||
@update:model-value="onChange"
|
@update:model-value="onChange"
|
||||||
/>
|
/>
|
||||||
</WidgetLayoutField>
|
</WidgetLayoutField>
|
||||||
@@ -25,7 +24,6 @@ import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<boolean>
|
widget: SimplifiedWidget<boolean>
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
<TreeSelect
|
<TreeSelect
|
||||||
v-model="localValue"
|
v-model="localValue"
|
||||||
v-bind="combinedProps"
|
v-bind="combinedProps"
|
||||||
:disabled="readonly"
|
|
||||||
class="w-full text-xs"
|
class="w-full text-xs"
|
||||||
size="small"
|
size="small"
|
||||||
@update:model-value="onChange"
|
@update:model-value="onChange"
|
||||||
@@ -37,7 +36,6 @@ export type TreeNode = {
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: SimplifiedWidget<any>
|
widget: SimplifiedWidget<any>
|
||||||
modelValue: any
|
modelValue: any
|
||||||
readonly?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { useResizeObserver } from '@vueuse/core'
|
||||||
import _ from 'es-toolkit/compat'
|
import _ from 'es-toolkit/compat'
|
||||||
import type { ToastMessageOptions } from 'primevue/toast'
|
import type { ToastMessageOptions } from 'primevue/toast'
|
||||||
import { reactive } from 'vue'
|
import { reactive, unref } from 'vue'
|
||||||
|
import { shallowRef } from 'vue'
|
||||||
|
|
||||||
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
||||||
import { st, t } from '@/i18n'
|
import { st, t } from '@/i18n'
|
||||||
@@ -129,7 +131,7 @@ export class ComfyApp {
|
|||||||
/**
|
/**
|
||||||
* List of entries to queue
|
* List of entries to queue
|
||||||
*/
|
*/
|
||||||
#queueItems: {
|
private queueItems: {
|
||||||
number: number
|
number: number
|
||||||
batchCount: number
|
batchCount: number
|
||||||
queueNodeIds?: NodeExecutionId[]
|
queueNodeIds?: NodeExecutionId[]
|
||||||
@@ -137,7 +139,7 @@ export class ComfyApp {
|
|||||||
/**
|
/**
|
||||||
* If the queue is currently being processed
|
* If the queue is currently being processed
|
||||||
*/
|
*/
|
||||||
#processingQueue: boolean = false
|
private processingQueue: boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Content Clipboard
|
* Content Clipboard
|
||||||
@@ -176,12 +178,15 @@ export class ComfyApp {
|
|||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
canvas: LGraphCanvas
|
canvas: LGraphCanvas
|
||||||
dragOverNode: LGraphNode | null = null
|
dragOverNode: LGraphNode | null = null
|
||||||
// @ts-expect-error fixme ts strict error
|
readonly canvasElRef = shallowRef<HTMLCanvasElement>()
|
||||||
canvasEl: HTMLCanvasElement
|
get canvasEl() {
|
||||||
|
// TODO: Fix possibly undefined reference
|
||||||
|
return unref(this.canvasElRef)!
|
||||||
|
}
|
||||||
|
|
||||||
#configuringGraphLevel: number = 0
|
private configuringGraphLevel: number = 0
|
||||||
get configuringGraph() {
|
get configuringGraph() {
|
||||||
return this.#configuringGraphLevel > 0
|
return this.configuringGraphLevel > 0
|
||||||
}
|
}
|
||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
ctx: CanvasRenderingContext2D
|
ctx: CanvasRenderingContext2D
|
||||||
@@ -195,7 +200,7 @@ export class ComfyApp {
|
|||||||
// Set by Comfy.Clipspace extension
|
// Set by Comfy.Clipspace extension
|
||||||
openClipspace: () => void = () => {}
|
openClipspace: () => void = () => {}
|
||||||
|
|
||||||
#positionConversion?: {
|
private positionConversion?: {
|
||||||
clientPosToCanvasPos: (pos: Vector2) => Vector2
|
clientPosToCanvasPos: (pos: Vector2) => Vector2
|
||||||
canvasPosToClientPos: (pos: Vector2) => Vector2
|
canvasPosToClientPos: (pos: Vector2) => Vector2
|
||||||
}
|
}
|
||||||
@@ -517,7 +522,7 @@ export class ComfyApp {
|
|||||||
/**
|
/**
|
||||||
* Adds a handler allowing drag+drop of files onto the window to load workflows
|
* Adds a handler allowing drag+drop of files onto the window to load workflows
|
||||||
*/
|
*/
|
||||||
#addDropHandler() {
|
private addDropHandler() {
|
||||||
// Get prompt from dropped PNG or json
|
// Get prompt from dropped PNG or json
|
||||||
document.addEventListener('drop', async (event) => {
|
document.addEventListener('drop', async (event) => {
|
||||||
try {
|
try {
|
||||||
@@ -593,7 +598,7 @@ export class ComfyApp {
|
|||||||
/**
|
/**
|
||||||
* Handle keypress
|
* Handle keypress
|
||||||
*/
|
*/
|
||||||
#addProcessKeyHandler() {
|
private addProcessKeyHandler() {
|
||||||
const origProcessKey = LGraphCanvas.prototype.processKey
|
const origProcessKey = LGraphCanvas.prototype.processKey
|
||||||
LGraphCanvas.prototype.processKey = function (e: KeyboardEvent) {
|
LGraphCanvas.prototype.processKey = function (e: KeyboardEvent) {
|
||||||
if (!this.graph) return
|
if (!this.graph) return
|
||||||
@@ -640,7 +645,7 @@ export class ComfyApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#addDrawNodeHandler() {
|
private addDrawNodeHandler() {
|
||||||
const origDrawNode = LGraphCanvas.prototype.drawNode
|
const origDrawNode = LGraphCanvas.prototype.drawNode
|
||||||
LGraphCanvas.prototype.drawNode = function (node) {
|
LGraphCanvas.prototype.drawNode = function (node) {
|
||||||
const editor_alpha = this.editor_alpha
|
const editor_alpha = this.editor_alpha
|
||||||
@@ -689,7 +694,7 @@ export class ComfyApp {
|
|||||||
/**
|
/**
|
||||||
* Handles updates from the API socket
|
* Handles updates from the API socket
|
||||||
*/
|
*/
|
||||||
#addApiUpdateHandlers() {
|
private addApiUpdateHandlers() {
|
||||||
api.addEventListener('status', ({ detail }) => {
|
api.addEventListener('status', ({ detail }) => {
|
||||||
this.ui.setStatus(detail)
|
this.ui.setStatus(detail)
|
||||||
})
|
})
|
||||||
@@ -763,15 +768,15 @@ export class ComfyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Flag that the graph is configuring to prevent nodes from running checks while its still loading */
|
/** Flag that the graph is configuring to prevent nodes from running checks while its still loading */
|
||||||
#addConfigureHandler() {
|
private addConfigureHandler() {
|
||||||
const app = this
|
const app = this
|
||||||
const configure = LGraph.prototype.configure
|
const configure = LGraph.prototype.configure
|
||||||
LGraph.prototype.configure = function (...args) {
|
LGraph.prototype.configure = function (...args) {
|
||||||
app.#configuringGraphLevel++
|
app.configuringGraphLevel++
|
||||||
try {
|
try {
|
||||||
return configure.apply(this, args)
|
return configure.apply(this, args)
|
||||||
} finally {
|
} finally {
|
||||||
app.#configuringGraphLevel--
|
app.configuringGraphLevel--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -808,16 +813,15 @@ export class ComfyApp {
|
|||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
this.canvasContainer = document.getElementById('graph-canvas-container')
|
this.canvasContainer = document.getElementById('graph-canvas-container')
|
||||||
|
|
||||||
this.canvasEl = canvasEl
|
this.canvasElRef.value = canvasEl
|
||||||
this.resizeCanvas()
|
|
||||||
|
|
||||||
await useWorkspaceStore().workflow.syncWorkflows()
|
await useWorkspaceStore().workflow.syncWorkflows()
|
||||||
await useSubgraphStore().fetchSubgraphs()
|
await useSubgraphStore().fetchSubgraphs()
|
||||||
await useExtensionService().loadExtensions()
|
await useExtensionService().loadExtensions()
|
||||||
|
|
||||||
this.#addProcessKeyHandler()
|
this.addProcessKeyHandler()
|
||||||
this.#addConfigureHandler()
|
this.addConfigureHandler()
|
||||||
this.#addApiUpdateHandlers()
|
this.addApiUpdateHandlers()
|
||||||
|
|
||||||
const graph = new LGraph()
|
const graph = new LGraph()
|
||||||
|
|
||||||
@@ -883,39 +887,37 @@ export class ComfyApp {
|
|||||||
this.graph.start()
|
this.graph.start()
|
||||||
|
|
||||||
// Ensure the canvas fills the window
|
// Ensure the canvas fills the window
|
||||||
this.resizeCanvas()
|
useResizeObserver(this.canvasElRef, ([canvasEl]) => {
|
||||||
window.addEventListener('resize', () => this.resizeCanvas())
|
if (canvasEl.target instanceof HTMLCanvasElement) {
|
||||||
const ro = new ResizeObserver(() => this.resizeCanvas())
|
this.resizeCanvas(canvasEl.target)
|
||||||
ro.observe(this.bodyTop)
|
}
|
||||||
ro.observe(this.bodyLeft)
|
})
|
||||||
ro.observe(this.bodyRight)
|
|
||||||
ro.observe(this.bodyBottom)
|
|
||||||
|
|
||||||
await useExtensionService().invokeExtensionsAsync('init')
|
await useExtensionService().invokeExtensionsAsync('init')
|
||||||
await this.registerNodes()
|
await this.registerNodes()
|
||||||
|
|
||||||
this.#addDrawNodeHandler()
|
this.addDrawNodeHandler()
|
||||||
this.#addDropHandler()
|
this.addDropHandler()
|
||||||
|
|
||||||
await useExtensionService().invokeExtensionsAsync('setup')
|
await useExtensionService().invokeExtensionsAsync('setup')
|
||||||
|
|
||||||
this.#positionConversion = useCanvasPositionConversion(
|
this.positionConversion = useCanvasPositionConversion(
|
||||||
this.canvasContainer,
|
this.canvasContainer,
|
||||||
this.canvas
|
this.canvas
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
resizeCanvas() {
|
private resizeCanvas(canvas: HTMLCanvasElement) {
|
||||||
// Limit minimal scale to 1, see https://github.com/comfyanonymous/ComfyUI/pull/845
|
// Limit minimal scale to 1, see https://github.com/comfyanonymous/ComfyUI/pull/845
|
||||||
const scale = Math.max(window.devicePixelRatio, 1)
|
const scale = Math.max(window.devicePixelRatio, 1)
|
||||||
|
|
||||||
// Clear fixed width and height while calculating rect so it uses 100% instead
|
// Clear fixed width and height while calculating rect so it uses 100% instead
|
||||||
this.canvasEl.height = this.canvasEl.width = NaN
|
canvas.height = canvas.width = NaN
|
||||||
const { width, height } = this.canvasEl.getBoundingClientRect()
|
const { width, height } = canvas.getBoundingClientRect()
|
||||||
this.canvasEl.width = Math.round(width * scale)
|
canvas.width = Math.round(width * scale)
|
||||||
this.canvasEl.height = Math.round(height * scale)
|
canvas.height = Math.round(height * scale)
|
||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
this.canvasEl.getContext('2d').scale(scale, scale)
|
canvas.getContext('2d').scale(scale, scale)
|
||||||
this.canvas?.draw(true, true)
|
this.canvas?.draw(true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -963,7 +965,7 @@ export class ComfyApp {
|
|||||||
nodeDefStore.updateNodeDefs(nodeDefArray)
|
nodeDefStore.updateNodeDefs(nodeDefArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
async #getNodeDefs(): Promise<Record<string, ComfyNodeDefV1>> {
|
async getNodeDefs(): Promise<Record<string, ComfyNodeDefV1>> {
|
||||||
const translateNodeDef = (def: ComfyNodeDefV1): ComfyNodeDefV1 => ({
|
const translateNodeDef = (def: ComfyNodeDefV1): ComfyNodeDefV1 => ({
|
||||||
...def,
|
...def,
|
||||||
display_name: st(
|
display_name: st(
|
||||||
@@ -987,7 +989,7 @@ export class ComfyApp {
|
|||||||
*/
|
*/
|
||||||
async registerNodes() {
|
async registerNodes() {
|
||||||
// Load node definitions from the backend
|
// Load node definitions from the backend
|
||||||
const defs = await this.#getNodeDefs()
|
const defs = await this.getNodeDefs()
|
||||||
await this.registerNodesFromDefs(defs)
|
await this.registerNodesFromDefs(defs)
|
||||||
await useExtensionService().invokeExtensionsAsync('registerCustomNodes')
|
await useExtensionService().invokeExtensionsAsync('registerCustomNodes')
|
||||||
if (this.vueAppReady) {
|
if (this.vueAppReady) {
|
||||||
@@ -1055,14 +1057,14 @@ export class ComfyApp {
|
|||||||
localStorage.setItem('litegrapheditor_clipboard', old)
|
localStorage.setItem('litegrapheditor_clipboard', old)
|
||||||
}
|
}
|
||||||
|
|
||||||
#showMissingNodesError(missingNodeTypes: MissingNodeType[]) {
|
private showMissingNodesError(missingNodeTypes: MissingNodeType[]) {
|
||||||
if (useSettingStore().get('Comfy.Workflow.ShowMissingNodesWarning')) {
|
if (useSettingStore().get('Comfy.Workflow.ShowMissingNodesWarning')) {
|
||||||
useDialogService().showLoadWorkflowWarning({ missingNodeTypes })
|
useDialogService().showLoadWorkflowWarning({ missingNodeTypes })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
#showMissingModelsError(missingModels, paths) {
|
private showMissingModelsError(missingModels, paths) {
|
||||||
if (useSettingStore().get('Comfy.Workflow.ShowMissingModelsWarning')) {
|
if (useSettingStore().get('Comfy.Workflow.ShowMissingModelsWarning')) {
|
||||||
useDialogService().showMissingModelsWarning({
|
useDialogService().showMissingModelsWarning({
|
||||||
missingModels,
|
missingModels,
|
||||||
@@ -1295,11 +1297,11 @@ export class ComfyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (missingNodeTypes.length && showMissingNodesDialog) {
|
if (missingNodeTypes.length && showMissingNodesDialog) {
|
||||||
this.#showMissingNodesError(missingNodeTypes)
|
this.showMissingNodesError(missingNodeTypes)
|
||||||
}
|
}
|
||||||
if (missingModels.length && showMissingModelsDialog) {
|
if (missingModels.length && showMissingModelsDialog) {
|
||||||
const paths = await api.getFolderPaths()
|
const paths = await api.getFolderPaths()
|
||||||
this.#showMissingModelsError(missingModels, paths)
|
this.showMissingModelsError(missingModels, paths)
|
||||||
}
|
}
|
||||||
await useExtensionService().invokeExtensionsAsync(
|
await useExtensionService().invokeExtensionsAsync(
|
||||||
'afterConfigureGraph',
|
'afterConfigureGraph',
|
||||||
@@ -1325,14 +1327,14 @@ export class ComfyApp {
|
|||||||
batchCount: number = 1,
|
batchCount: number = 1,
|
||||||
queueNodeIds?: NodeExecutionId[]
|
queueNodeIds?: NodeExecutionId[]
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
this.#queueItems.push({ number, batchCount, queueNodeIds })
|
this.queueItems.push({ number, batchCount, queueNodeIds })
|
||||||
|
|
||||||
// Only have one action process the items so each one gets a unique seed correctly
|
// Only have one action process the items so each one gets a unique seed correctly
|
||||||
if (this.#processingQueue) {
|
if (this.processingQueue) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#processingQueue = true
|
this.processingQueue = true
|
||||||
const executionStore = useExecutionStore()
|
const executionStore = useExecutionStore()
|
||||||
executionStore.lastNodeErrors = null
|
executionStore.lastNodeErrors = null
|
||||||
|
|
||||||
@@ -1340,8 +1342,8 @@ export class ComfyApp {
|
|||||||
let comfyOrgApiKey = useApiKeyAuthStore().getApiKey()
|
let comfyOrgApiKey = useApiKeyAuthStore().getApiKey()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (this.#queueItems.length) {
|
while (this.queueItems.length) {
|
||||||
const { number, batchCount, queueNodeIds } = this.#queueItems.pop()!
|
const { number, batchCount, queueNodeIds } = this.queueItems.pop()!
|
||||||
|
|
||||||
for (let i = 0; i < batchCount; i++) {
|
for (let i = 0; i < batchCount; i++) {
|
||||||
// Allow widgets to run callbacks before a prompt has been queued
|
// Allow widgets to run callbacks before a prompt has been queued
|
||||||
@@ -1406,7 +1408,7 @@ export class ComfyApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.#processingQueue = false
|
this.processingQueue = false
|
||||||
}
|
}
|
||||||
api.dispatchCustomEvent('promptQueued', { number, batchCount })
|
api.dispatchCustomEvent('promptQueued', { number, batchCount })
|
||||||
return !executionStore.lastNodeErrors
|
return !executionStore.lastNodeErrors
|
||||||
@@ -1610,7 +1612,7 @@ export class ComfyApp {
|
|||||||
(n) => !LiteGraph.registered_node_types[n.class_type]
|
(n) => !LiteGraph.registered_node_types[n.class_type]
|
||||||
)
|
)
|
||||||
if (missingNodeTypes.length) {
|
if (missingNodeTypes.length) {
|
||||||
this.#showMissingNodesError(missingNodeTypes.map((t) => t.class_type))
|
this.showMissingNodesError(missingNodeTypes.map((t) => t.class_type))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1729,7 +1731,7 @@ export class ComfyApp {
|
|||||||
useToastStore().add(requestToastMessage)
|
useToastStore().add(requestToastMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
const defs = await this.#getNodeDefs()
|
const defs = await this.getNodeDefs()
|
||||||
for (const nodeId in defs) {
|
for (const nodeId in defs) {
|
||||||
this.registerNodeDef(nodeId, defs[nodeId])
|
this.registerNodeDef(nodeId, defs[nodeId])
|
||||||
}
|
}
|
||||||
@@ -1805,17 +1807,17 @@ export class ComfyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clientPosToCanvasPos(pos: Vector2): Vector2 {
|
clientPosToCanvasPos(pos: Vector2): Vector2 {
|
||||||
if (!this.#positionConversion) {
|
if (!this.positionConversion) {
|
||||||
throw new Error('clientPosToCanvasPos called before setup')
|
throw new Error('clientPosToCanvasPos called before setup')
|
||||||
}
|
}
|
||||||
return this.#positionConversion.clientPosToCanvasPos(pos)
|
return this.positionConversion.clientPosToCanvasPos(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
canvasPosToClientPos(pos: Vector2): Vector2 {
|
canvasPosToClientPos(pos: Vector2): Vector2 {
|
||||||
if (!this.#positionConversion) {
|
if (!this.positionConversion) {
|
||||||
throw new Error('canvasPosToClientPos called before setup')
|
throw new Error('canvasPosToClientPos called before setup')
|
||||||
}
|
}
|
||||||
return this.#positionConversion.canvasPosToClientPos(pos)
|
return this.positionConversion.canvasPosToClientPos(pos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,20 +131,6 @@ describe('NodeHeader - Subgraph Functionality', () => {
|
|||||||
expect(subgraphButton.exists()).toBe(false)
|
expect(subgraphButton.exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not show subgraph button in readonly mode', async () => {
|
|
||||||
await setupMocks(true) // isSubgraph = true
|
|
||||||
|
|
||||||
const wrapper = createWrapper({
|
|
||||||
nodeData: createMockNodeData('test-node-1'),
|
|
||||||
readonly: true
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
|
||||||
expect(subgraphButton.exists()).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should emit enter-subgraph event when button is clicked', async () => {
|
it('should emit enter-subgraph event when button is clicked', async () => {
|
||||||
await setupMocks(true) // isSubgraph = true
|
await setupMocks(true) // isSubgraph = true
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user