Even further app fixes (#9617)

- Allow dragging zoom pane with middle click
- Prevent selection of canvasOnly widgets
- These widgets would not display in app mode, so allow selection would
only cause confusion.
- Support displaying the error dialogue in app mode
- Add a somewhat involved mobile app mode error indication system
<img width="300" alt="image"
src="https://github.com/user-attachments/assets/d8793bbd-fff5-4b2a-a316-6ff154bae2c4"
/> <img width="300" alt="image"
src="https://github.com/user-attachments/assets/cb88b0f6-f7e5-409e-ae43-f1348f946b19"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9617-Even-further-app-fixes-31d6d73d365081c891dfdfe3477cfd61)
by [Unito](https://www.unito.io)
This commit is contained in:
AustinMroz
2026-03-09 09:35:31 -07:00
committed by GitHub
parent 76fd80aa98
commit 592f992d1d
9 changed files with 267 additions and 10 deletions

View File

@@ -160,7 +160,7 @@ function handleClick(e: MouseEvent) {
else appModeStore.selectedOutputs.splice(index, 1)
return
}
if (!isSelectInputsMode.value) return
if (!isSelectInputsMode.value || widget.options.canvasOnly) return
const index = appModeStore.selectedInputs.findIndex(
([nodeId, widgetName]) => node.id == nodeId && widget.name === widgetName

View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
import {
DialogClose,
DialogContent,
DialogOverlay,
DialogPortal,
DialogRoot,
DialogTitle,
DialogTrigger
} from 'reka-ui'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
defineProps<{ title?: string; to?: string | HTMLElement }>()
const { t } = useI18n()
</script>
<template>
<DialogRoot v-slot="{ close }">
<DialogTrigger as-child>
<slot name="button" />
</DialogTrigger>
<DialogPortal :to>
<DialogOverlay
class="data-[state=open]:animate-overlayShow fixed inset-0 z-30 bg-black/70"
/>
<DialogContent
v-bind="$attrs"
class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] z-1700 max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] rounded-2xl border border-border-subtle bg-base-background p-2 shadow-sm"
>
<div
v-if="title"
class="flex w-full items-center justify-between border-b border-border-subtle px-4"
>
<DialogTitle class="text-sm">{{ title }}</DialogTitle>
<DialogClose as-child>
<Button
:aria-label="t('g.close')"
size="icon"
variant="muted-textonly"
>
<i class="icon-[lucide--x]" />
</Button>
</DialogClose>
</div>
<slot :close />
</DialogContent>
</DialogPortal>
</DialogRoot>
</template>

View File

@@ -50,7 +50,9 @@
{{ t('g.dismiss') }}
</Button>
<Button variant="secondary" size="lg" @click="seeErrors">
{{ t('errorOverlay.seeErrors') }}
{{
appMode ? t('linearMode.error.goto') : t('errorOverlay.seeErrors')
}}
</Button>
</div>
</div>
@@ -69,6 +71,8 @@ import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useErrorGroups } from '@/components/rightSidePanel/errors/useErrorGroups'
defineProps<{ appMode?: boolean }>()
const { t } = useI18n()
const executionErrorStore = useExecutionErrorStore()
const rightSidePanelStore = useRightSidePanelStore()
@@ -94,6 +98,7 @@ function dismiss() {
}
function seeErrors() {
canvasStore.linearMode = false
if (canvasStore.canvas) {
canvasStore.canvas.deselectAll()
canvasStore.updateSelectedItems()

View File

@@ -1,5 +1,5 @@
import { computed, reactive, ref, watch } from 'vue'
import type { Ref } from 'vue'
import { computed, reactive, ref, toValue, watch } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
import Fuse from 'fuse.js'
import type { IFuseOptions } from 'fuse.js'
@@ -227,7 +227,7 @@ function searchErrorGroups(groups: ErrorGroup[], query: string) {
}
export function useErrorGroups(
searchQuery: Ref<string>,
searchQuery: MaybeRefOrGetter<string>,
t: (key: string) => string
) {
const executionErrorStore = useExecutionErrorStore()
@@ -584,7 +584,7 @@ export function useErrorGroups(
})
const filteredGroups = computed<ErrorGroup[]>(() => {
const query = searchQuery.value.trim()
const query = toValue(searchQuery).trim()
return searchErrorGroups(tabErrorGroups.value, query)
})

View File

@@ -24,7 +24,7 @@ function handleWheel(e: WheelEvent) {
let dragging = false
function handleDown(e: PointerEvent) {
if (e.button !== 0) return
if (e.button !== 0 && e.button !== 1) return
const zoomPaneEl = zoomPane.value
if (!zoomPaneEl) return

View File

@@ -3180,6 +3180,7 @@
"cancelThisRun": "Cancel this run",
"deleteAllAssets": "Delete all assets from this run",
"hasCreditCost": "Requires additional credits",
"viewGraph": "View node graph",
"welcome": {
"title": "App Mode",
"message": "A simplified view that hides the node graph so you can focus on creating.",
@@ -3224,6 +3225,19 @@
"outputPlaceholder": "Output nodes will show up here",
"outputRequiredPlaceholder": "At least one node is required"
},
"error": {
"header": "This app encountered an error",
"log": "Error Logs",
"mobileFixable": "Check {0} for errors",
"requiresGraph": "Something went wrong during generation. This could be due to invalid hidden inputs, missing resources, or workflow configuration issues.",
"promptVisitGraph": "View the node graph to see the full error.",
"getHelp": "For help, view our {0}, {1}, or {2} with the copied error.",
"goto": "Show errors in graph",
"github": "submit a GitHub issue",
"guide": "troubleshooting guide",
"support": "contact our support",
"promptShow": "Show error report"
},
"queue": {
"clickToClear": "Click to clear queue",
"clear": "Clear queue"

View File

@@ -15,7 +15,9 @@ import { useWorkflowStore } from '@/platform/workflow/management/stores/workflow
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import LinearControls from '@/renderer/extensions/linearMode/LinearControls.vue'
import LinearPreview from '@/renderer/extensions/linearMode/LinearPreview.vue'
import MobileError from '@/renderer/extensions/linearMode/MobileError.vue'
import { useColorPaletteService } from '@/services/colorPaletteService'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { useQueueStore } from '@/stores/queueStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
@@ -31,6 +33,7 @@ const canvasStore = useCanvasStore()
const colorPaletteService = useColorPaletteService()
const colorPaletteStore = useColorPaletteStore()
const { isLoggedIn } = useCurrentUser()
const executionErrorStore = useExecutionErrorStore()
const { t } = useI18n()
const { commandIdToMenuItem } = useMenuItemStore()
const queueStore = useQueueStore()
@@ -40,7 +43,7 @@ const { toggle: toggleFullscreen } = useFullscreen(undefined, {
autoExit: true
})
const activeIndex = ref(2)
const activeIndex = ref(1)
const sliderPaneRef = useTemplateRef('sliderPaneRef')
const sliderWidth = computed(() => sliderPaneRef.value?.offsetWidth)
@@ -192,7 +195,11 @@ const menuEntries = computed<MenuItem[]>(() => [
<div
class="absolute top-0 left-[100vw] flex h-full w-screen flex-col bg-base-background"
>
<LinearPreview mobile />
<MobileError
v-if="executionErrorStore.isErrorOverlayOpen"
@navigate-controls="activeIndex = 0"
/>
<LinearPreview v-else mobile @navigate-controls="activeIndex = 0" />
</div>
<AssetsSidebarTab
class="absolute top-0 left-[200vw] h-full w-screen bg-base-background"
@@ -213,7 +220,11 @@ const menuEntries = computed<MenuItem[]>(() => [
<div class="relative size-4">
<i :class="cn('size-4', icon)" />
<div
v-if="
v-if="index === 1 && executionErrorStore.isErrorOverlayOpen"
class="absolute -top-1 -right-1 size-2 rounded-full bg-error"
/>
<div
v-else-if="
index === 1 &&
(queueStore.runningTasks.length > 0 ||
queueStore.pendingTasks.length > 0)

View File

@@ -0,0 +1,174 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import Dialogue from '@/components/common/Dialogue.vue'
import { useErrorGroups } from '@/components/rightSidePanel/errors/useErrorGroups'
import Button from '@/components/ui/button/Button.vue'
import { useAppMode } from '@/composables/useAppMode'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { useExternalLink } from '@/composables/useExternalLink'
import { buildSupportUrl } from '@/platform/support/config'
import { useAppModeStore } from '@/stores/appModeStore'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
defineEmits<{ navigateControls: [] }>()
const { t } = useI18n()
const appModeStore = useAppModeStore()
const { setMode } = useAppMode()
const executionErrorStore = useExecutionErrorStore()
const { buildDocsUrl, staticUrls } = useExternalLink()
const { allErrorGroups } = useErrorGroups('', t)
const { copyToClipboard } = useCopyToClipboard()
const guideUrl = buildDocsUrl('troubleshooting/overview', {
includeLocale: true
})
const supportUrl = buildSupportUrl()
const inputNodeIds = computed(() => {
const ids = new Set()
for (const [id] of appModeStore.selectedInputs) ids.add(String(id))
return ids
})
const accessibleNodeErrors = computed(() =>
Object.keys(executionErrorStore.lastNodeErrors ?? {}).filter((k) =>
inputNodeIds.value.has(k)
)
)
const accessibleErrors = computed(() =>
accessibleNodeErrors.value.flatMap((k) =>
executionErrorStore.lastNodeErrors![k].errors.flatMap((error) => {
const { extra_info } = error
if (!extra_info) return []
const selectedInput = appModeStore.selectedInputs.find(
([id, name]) => id == k && extra_info.input_name === name
)
if (!selectedInput) return []
return [`${selectedInput[1]}: ${error.message}`]
})
)
)
const allErrors = computed(() =>
allErrorGroups.value.flatMap((group) => {
if (group.type !== 'execution') return [group.title]
return group.cards.flatMap((c) =>
c.errors.map((e) =>
e.details
? `${c.title} (${e.details}): ${e.message}`
: `${c.title}: ${e.message}`
)
)
})
)
function copy(obj: unknown) {
copyToClipboard(JSON.stringify(obj))
}
</script>
<template>
<section class="flex h-full flex-col items-center justify-center gap-2 px-4">
<i class="icon-[lucide--circle-alert] size-6 bg-error" />
{{ t('linearMode.error.header') }}
<div class="p-1 text-muted-foreground">
<i18n-t
v-if="accessibleErrors.length"
keypath="linearMode.error.mobileFixable"
>
<Button @click="$emit('navigateControls')">
{{ t('linearMode.mobileControls') }}
</Button>
</i18n-t>
<div v-else class="text-center">
<p v-text="t('linearMode.error.requiresGraph')" />
<p v-text="t('linearMode.error.promptVisitGraph')" />
<p class="*:text-muted-foreground">
<i18n-t keypath="linearMode.error.getHelp">
<a
:href="guideUrl"
target="_blank"
v-text="t('linearMode.error.guide')"
/>
<a
:href="staticUrls.githubIssues"
target="_blank"
v-text="t('linearMode.error.github')"
/>
<a
:href="supportUrl"
target="_blank"
v-text="t('linearMode.error.support')"
/>
</i18n-t>
</p>
<Dialogue :title="t('linearMode.error.log')">
<template #button>
<Button variant="textonly">
{{ t('linearMode.error.promptShow') }}
<i class="icon-[lucide--chevron-right] size-5" />
</Button>
</template>
<template #default="{ close }">
<article class="flex flex-col gap-2 p-4">
<section class="flex max-h-[60vh] flex-col gap-2 overflow-y-auto">
<div
v-for="error in allErrors"
:key="error"
class="w-full rounded-lg bg-secondary-background p-2 text-muted-foreground"
v-text="error"
/>
</section>
<div class="flex items-center justify-end gap-4">
<Button variant="muted-textonly" size="lg" @click="close">
{{ t('g.close') }}
</Button>
<Button size="lg" @click="copy(allErrors)">
{{ t('importFailed.copyError') }}
<i class="icon-[lucide--copy]" />
</Button>
</div>
</article>
</template>
</Dialogue>
</div>
</div>
<div
v-if="accessibleErrors.length"
class="my-8 w-full rounded-lg bg-secondary-background text-muted-foreground"
>
<ul>
<li
v-for="error in accessibleErrors"
:key="error"
class="before:content"
v-text="error"
/>
</ul>
</div>
<div class="flex gap-2">
<Button
variant="textonly"
size="lg"
@click="executionErrorStore.dismissErrorOverlay()"
>
{{ t('g.dismiss') }}
</Button>
<Button variant="textonly" size="lg" @click="setMode('graph')">
{{ t('linearMode.viewGraph') }}
</Button>
<Button
v-if="accessibleErrors.length"
size="lg"
@click="copy(accessibleErrors)"
>
{{ t('importFailed.copyError') }}
<i class="icon-[lucide--copy]" />
</Button>
</div>
</section>
</template>

View File

@@ -9,6 +9,7 @@ import { computed, useTemplateRef } from 'vue'
import AppBuilder from '@/components/builder/AppBuilder.vue'
import AppModeToolbar from '@/components/appMode/AppModeToolbar.vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import ErrorOverlay from '@/components/error/ErrorOverlay.vue'
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
import TopbarSubscribeButton from '@/components/topbar/TopbarSubscribeButton.vue'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
@@ -156,6 +157,7 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
</div>
<div ref="bottomLeftRef" class="absolute bottom-7 left-4 z-20" />
<div ref="bottomRightRef" class="absolute right-4 bottom-7 z-20" />
<div class="absolute top-4 right-4 z-20"><ErrorOverlay app-mode /></div>
</SplitterPanel>
<SplitterPanel
v-if="hasRightPanel"