Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions src/components/builder/AppBuilder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import DraggableList from '@/components/common/DraggableList.vue'
import IoItem from '@/components/builder/IoItem.vue'
import PropertiesAccordionItem from '@/components/rightSidePanel/layout/PropertiesAccordionItem.vue'
import WidgetItem from '@/components/rightSidePanel/parameters/WidgetItem.vue'
import Button from '@/components/ui/button/Button.vue'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
Expand Down Expand Up @@ -211,9 +210,6 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
{{
isArrangeMode ? t('nodeHelpPage.inputs') : t('linearMode.builder.title')
}}
<Button class="ml-auto" @click="appModeStore.exitBuilder">
{{ t('linearMode.builder.exit') }}
</Button>
</div>
<DraggableList
v-if="isArrangeMode"
Expand Down
43 changes: 43 additions & 0 deletions src/components/builder/BuilderExitButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<template>
<div
class="fixed bottom-4 left-1/2 z-1000 flex -translate-x-1/2 items-center rounded-2xl border border-border-default bg-base-background p-2 shadow-interface"
>
<Button size="lg" @click="onExitBuilder">
{{ t('builderMenu.exitAppBuilder') }}
</Button>
</div>
</template>

<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
import { useI18n } from 'vue-i18n'

import Button from '@/components/ui/button/Button.vue'
import { useAppMode } from '@/composables/useAppMode'
import { useAppModeStore } from '@/stores/appModeStore'
import { useDialogStore } from '@/stores/dialogStore'

const { t } = useI18n()
const appModeStore = useAppModeStore()
const dialogStore = useDialogStore()
const { isBuilderMode } = useAppMode()

useEventListener(window, 'keydown', (e: KeyboardEvent) => {
if (
e.key === 'Escape' &&
!e.ctrlKey &&
!e.altKey &&
!e.metaKey &&
dialogStore.dialogStack.length === 0 &&
isBuilderMode.value
) {
e.preventDefault()
e.stopPropagation()
onExitBuilder()
}
Comment on lines +25 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard Escape handling while editing text inputs

The global Escape handler exits builder mode even when focus is in editable fields. With direct exit behavior, this can cause accidental mode exits during text entry.

Suggested fix
 useEventListener(window, 'keydown', (e: KeyboardEvent) => {
+  const target = e.target as HTMLElement | null
+  const isEditable =
+    target instanceof HTMLInputElement ||
+    target instanceof HTMLTextAreaElement ||
+    target?.isContentEditable === true
+  if (isEditable) return
+
   if (
     e.key === 'Escape' &&
     !e.ctrlKey &&
     !e.altKey &&
     !e.metaKey &&
+    !e.shiftKey &&
     dialogStore.dialogStack.length === 0 &&
     isBuilderMode.value
   ) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEventListener(window, 'keydown', (e: KeyboardEvent) => {
if (
e.key === 'Escape' &&
!e.ctrlKey &&
!e.altKey &&
!e.metaKey &&
dialogStore.dialogStack.length === 0 &&
isBuilderMode.value
) {
e.preventDefault()
e.stopPropagation()
onExitBuilder()
}
useEventListener(window, 'keydown', (e: KeyboardEvent) => {
const target = e.target as HTMLElement | null
const isEditable =
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target?.isContentEditable === true
if (isEditable) return
if (
e.key === 'Escape' &&
!e.ctrlKey &&
!e.altKey &&
!e.metaKey &&
!e.shiftKey &&
dialogStore.dialogStack.length === 0 &&
isBuilderMode.value
) {
e.preventDefault()
e.stopPropagation()
onExitBuilder()
}
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/builder/BuilderExitButton.vue` around lines 25 - 37, The
Escape handler registered via useEventListener currently calls onExitBuilder
whenever isBuilderMode.value is true and dialogStack is empty; change it to
first check document.activeElement and skip handling if focus is inside an
editable element (e.g., HTMLInputElement, HTMLTextAreaElement, or any element
with contentEditable === "true" or true) or if the active element is inside a
rich-text/editor component (check element.closest for common editor selectors if
needed). Update the listener logic in the same block where useEventListener is
used so the conditions include this focus guard before calling
e.preventDefault(), e.stopPropagation(), and onExitBuilder; keep the existing
checks for isBuilderMode.value and dialogStore.dialogStack.length.

})

function onExitBuilder() {
void appModeStore.exitBuilder()
}
</script>
73 changes: 73 additions & 0 deletions src/components/builder/BuilderMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<template>
<Popover :show-arrow="false" class="min-w-56 p-3">
<template #button>
<button
:class="
cn(
'absolute left-4 top-[calc(var(--workflow-tabs-height)+16px)] z-1000 inline-flex h-10 cursor-pointer items-center gap-2.5 rounded-lg py-2 pr-2 pl-3 shadow-interface transition-colors border-none',
'bg-secondary-background hover:bg-secondary-background-hover',
'data-[state=open]:bg-secondary-background-hover'
)
"
:aria-label="t('linearMode.appModeToolbar.appBuilder')"
>
<i class="icon-[lucide--hammer] size-4" />
<span class="text-sm font-medium">
{{ t('linearMode.appModeToolbar.appBuilder') }}
</span>
<i class="icon-[lucide--chevron-down] size-4 text-muted-foreground" />
</button>
</template>
<template #default="{ close }">
<button
:class="
cn(
'flex w-full items-center gap-3 rounded-md bg-transparent px-3 py-2 text-sm border-none',
hasOutputs
? 'cursor-pointer hover:bg-secondary-background-hover'
: 'opacity-50 pointer-events-none'
)
"
:disabled="!hasOutputs"
@click="onSave(close)"
>
<i class="icon-[lucide--save] size-4" />
{{ t('builderMenu.saveApp') }}
</button>
<div class="my-1 border-t border-border-default" />
<button
class="flex w-full cursor-pointer items-center gap-3 rounded-md bg-transparent px-3 py-2 text-sm border-none hover:bg-secondary-background-hover"
@click="onExitBuilder(close)"
>
<i class="icon-[lucide--square-pen] size-4" />
{{ t('builderMenu.exitAppBuilder') }}
</button>
</template>
</Popover>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'

import Popover from '@/components/ui/Popover.vue'
import { useAppModeStore } from '@/stores/appModeStore'
import { cn } from '@/utils/tailwindUtil'

import { useBuilderSave } from './useBuilderSave'

const { t } = useI18n()
const appModeStore = useAppModeStore()
const { hasOutputs } = storeToRefs(appModeStore)
const { setSaving } = useBuilderSave()

function onSave(close: () => void) {
setSaving(true)
close()
}

function onExitBuilder(close: () => void) {
void appModeStore.exitBuilder()
close()
}
</script>
2 changes: 1 addition & 1 deletion src/components/builder/BuilderToolbar.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<nav
class="fixed top-[calc(var(--workflow-tabs-height)+var(--spacing)*1.5)] left-1/2 z-[1000] -translate-x-1/2"
class="fixed top-[calc(var(--workflow-tabs-height)+var(--spacing)*1.5)] left-1/2 z-1000 -translate-x-1/2"
:aria-label="t('builderToolbar.label')"
>
<div
Expand Down
4 changes: 4 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -3349,5 +3349,9 @@
"saveSuccessAppPrompt": "Would you like to view it now?",
"saveSuccessGraphMessage": "'{name}' has been saved. It will open as a node graph by default.",
"viewApp": "View app"
},
"builderMenu": {
"saveApp": "Save app",
"exitAppBuilder": "Exit app builder"
}
}
10 changes: 0 additions & 10 deletions src/stores/appModeStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ import { defineStore } from 'pinia'
import { reactive, computed, watch } from 'vue'

import { useAppMode } from '@/composables/useAppMode'
import { t } from '@/i18n'
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LinearData } from '@/platform/workflow/management/stores/comfyWorkflow'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useDialogService } from '@/services/dialogService'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'

export const useAppModeStore = defineStore('appMode', () => {
Expand Down Expand Up @@ -69,14 +67,6 @@ export const useAppModeStore = defineStore('appMode', () => {
)

async function exitBuilder() {
if (
!(await useDialogService().confirm({
title: t('linearMode.builder.exitConfirmTitle'),
message: t('linearMode.builder.exitConfirmMessage')
}))
)
return

const workflow = workflowStore.activeWorkflow
if (workflow) workflow.dirtyLinearData = null
resetSelectedToWorkflow()
Expand Down
8 changes: 7 additions & 1 deletion src/views/GraphView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
<GraphCanvas @ready="onGraphReady" />
</div>
<LinearView v-if="linearMode" />
<BuilderToolbar v-if="isBuilderMode" />
<template v-if="isBuilderMode">
<BuilderToolbar />
<BuilderMenu />
<BuilderExitButton />
</template>
</div>

<GlobalToast />
Expand Down Expand Up @@ -87,6 +91,8 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
import { electronAPI } from '@/utils/envUtil'
import BuilderExitButton from '@/components/builder/BuilderExitButton.vue'
import BuilderMenu from '@/components/builder/BuilderMenu.vue'
import BuilderToolbar from '@/components/builder/BuilderToolbar.vue'
import LinearView from '@/views/LinearView.vue'
import ManagerProgressToast from '@/workbench/extensions/manager/components/ManagerProgressToast.vue'
Expand Down
Loading