feat: Add apps sidebar tab (#9342)
## Summary <!-- One sentence describing what changed and why. --> ## Changes - **What**: <!-- Core functionality added/modified --> - **Breaking**: <!-- Any breaking changes (if none, remove this line) --> - **Dependencies**: <!-- New dependencies (if none, remove this line) --> ## Review Focus <!-- Critical design decisions or edge cases that need attention --> <!-- If this PR fixes an issue, uncomment and update the line below --> <!-- Fixes #ISSUE_NUMBER --> ## Screenshots (if applicable) <img width="383" height="359" alt="image" src="https://github.com/user-attachments/assets/47905196-9db6-4a57-8cf7-384d4d37d000" /> <img width="335" height="281" alt="image" src="https://github.com/user-attachments/assets/843068f3-e895-4781-bf5f-e0eb86d3387c" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9342-feat-Add-apps-sidebar-tab-3176d73d3650812b822fc9cc3f17322e) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 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 |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
@@ -21,8 +21,8 @@ const tooltipOptions = { showDelay: 300, hideDelay: 300 }
|
|||||||
const isAssetsActive = computed(
|
const isAssetsActive = computed(
|
||||||
() => workspaceStore.sidebarTab.activeSidebarTab?.id === 'assets'
|
() => workspaceStore.sidebarTab.activeSidebarTab?.id === 'assets'
|
||||||
)
|
)
|
||||||
const isWorkflowsActive = computed(
|
const isAppsActive = computed(
|
||||||
() => workspaceStore.sidebarTab.activeSidebarTab?.id === 'workflows'
|
() => workspaceStore.sidebarTab.activeSidebarTab?.id === 'apps'
|
||||||
)
|
)
|
||||||
|
|
||||||
function openAssets() {
|
function openAssets() {
|
||||||
@@ -30,7 +30,7 @@ function openAssets() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showApps() {
|
function showApps() {
|
||||||
void commandStore.execute('Workspace.ToggleSidebarTab.workflows')
|
void commandStore.execute('Workspace.ToggleSidebarTab.apps')
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTemplates() {
|
function openTemplates() {
|
||||||
@@ -104,9 +104,7 @@ function openTemplates() {
|
|||||||
variant="textonly"
|
variant="textonly"
|
||||||
size="unset"
|
size="unset"
|
||||||
:aria-label="t('linearMode.appModeToolbar.apps')"
|
:aria-label="t('linearMode.appModeToolbar.apps')"
|
||||||
:class="
|
:class="cn('size-10', isAppsActive && 'bg-secondary-background-hover')"
|
||||||
cn('size-10', isWorkflowsActive && 'bg-secondary-background-hover')
|
|
||||||
"
|
|
||||||
@click="showApps"
|
@click="showApps"
|
||||||
>
|
>
|
||||||
<i class="icon-[lucide--panels-top-left] size-4" />
|
<i class="icon-[lucide--panels-top-left] size-4" />
|
||||||
|
|||||||
@@ -3,14 +3,18 @@
|
|||||||
<Card>
|
<Card>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<i :class="icon" style="font-size: 3rem; margin-bottom: 1rem" />
|
<i
|
||||||
<h3>{{ title }}</h3>
|
v-if="icon"
|
||||||
|
:class="icon"
|
||||||
|
style="font-size: 3rem; margin-bottom: 1rem"
|
||||||
|
/>
|
||||||
|
<h3 v-if="title">{{ title }}</h3>
|
||||||
<p :class="textClass" class="text-center whitespace-pre-line">
|
<p :class="textClass" class="text-center whitespace-pre-line">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
v-if="buttonLabel"
|
v-if="buttonLabel"
|
||||||
variant="textonly"
|
:variant="buttonVariant ?? 'textonly'"
|
||||||
@click="$emit('action')"
|
@click="$emit('action')"
|
||||||
>
|
>
|
||||||
{{ buttonLabel }}
|
{{ buttonLabel }}
|
||||||
@@ -25,14 +29,16 @@
|
|||||||
import Card from 'primevue/card'
|
import Card from 'primevue/card'
|
||||||
|
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
|
import type { ButtonVariants } from '../ui/button/button.variants'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
class?: string
|
class?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
title: string
|
title?: string
|
||||||
message: string
|
message: string
|
||||||
textClass?: string
|
textClass?: string
|
||||||
buttonLabel?: string
|
buttonLabel?: string
|
||||||
|
buttonVariant?: ButtonVariants['variant']
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits(['action'])
|
defineEmits(['action'])
|
||||||
@@ -51,7 +57,6 @@ defineEmits(['action'])
|
|||||||
}
|
}
|
||||||
|
|
||||||
.no-results-placeholder p {
|
.no-results-placeholder p {
|
||||||
color: var(--text-color-secondary);
|
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
48
src/components/sidebar/tabs/AppsSidebarTab.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<BaseWorkflowsSidebarTab
|
||||||
|
:title="$t('linearMode.appModeToolbar.apps')"
|
||||||
|
:filter="isAppWorkflow"
|
||||||
|
:label-transform="stripAppJsonSuffix"
|
||||||
|
hide-leaf-icon
|
||||||
|
:search-subject="$t('linearMode.appModeToolbar.apps')"
|
||||||
|
data-testid="apps-sidebar"
|
||||||
|
>
|
||||||
|
<template #alt-title>
|
||||||
|
<span
|
||||||
|
class="ml-2 flex items-center rounded-full bg-primary-background px-1.5 py-0.5 text-xxs uppercase text-base-foreground"
|
||||||
|
>
|
||||||
|
{{ $t('g.beta') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #empty-state>
|
||||||
|
<NoResultsPlaceholder
|
||||||
|
button-variant="secondary"
|
||||||
|
text-class="text-muted-foreground text-sm"
|
||||||
|
:message="$t('linearMode.appModeToolbar.appsEmptyMessage')"
|
||||||
|
:button-label="$t('linearMode.appModeToolbar.enterAppMode')"
|
||||||
|
@action="enterAppMode"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</BaseWorkflowsSidebarTab>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||||
|
import BaseWorkflowsSidebarTab from '@/components/sidebar/tabs/BaseWorkflowsSidebarTab.vue'
|
||||||
|
import { useAppMode } from '@/composables/useAppMode'
|
||||||
|
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
|
|
||||||
|
const { setMode } = useAppMode()
|
||||||
|
|
||||||
|
function isAppWorkflow(workflow: ComfyWorkflow): boolean {
|
||||||
|
return workflow.suffix === 'app.json'
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripAppJsonSuffix(label: string): string {
|
||||||
|
return label.replace(/\.app\.json$/i, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterAppMode() {
|
||||||
|
setMode('app')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
352
src/components/sidebar/tabs/BaseWorkflowsSidebarTab.vue
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
<template>
|
||||||
|
<SidebarTabTemplate
|
||||||
|
:title="title"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:data-testid="dataTestid"
|
||||||
|
class="workflows-sidebar-tab"
|
||||||
|
>
|
||||||
|
<template #alt-title>
|
||||||
|
<slot name="alt-title" />
|
||||||
|
</template>
|
||||||
|
<template #tool-buttons>
|
||||||
|
<Button
|
||||||
|
v-tooltip.bottom="$t('g.refresh')"
|
||||||
|
variant="muted-textonly"
|
||||||
|
size="icon"
|
||||||
|
:aria-label="$t('g.refresh')"
|
||||||
|
@click="workflowStore.syncWorkflows()"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--refresh-cw] size-4" />
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<template #header>
|
||||||
|
<div class="px-2 2xl:px-4">
|
||||||
|
<SearchBox
|
||||||
|
ref="searchBoxRef"
|
||||||
|
v-model:model-value="searchQuery"
|
||||||
|
class="workflows-search-box"
|
||||||
|
:placeholder="$t('g.searchPlaceholder', { subject: searchSubject })"
|
||||||
|
@search="handleSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div v-if="!isSearching" class="comfyui-workflows-panel">
|
||||||
|
<div
|
||||||
|
v-if="workflowTabsPosition === 'Sidebar'"
|
||||||
|
class="comfyui-workflows-open"
|
||||||
|
>
|
||||||
|
<TextDivider
|
||||||
|
:text="t('sideToolbar.workflowTab.workflowTreeType.open')"
|
||||||
|
type="dashed"
|
||||||
|
class="ml-2"
|
||||||
|
/>
|
||||||
|
<TreeExplorer
|
||||||
|
v-model:expanded-keys="dummyExpandedKeys"
|
||||||
|
:root="renderTreeNode(openWorkflowsTree, WorkflowTreeType.Open)"
|
||||||
|
:selection-keys="selectionKeys"
|
||||||
|
>
|
||||||
|
<template #node="{ node }">
|
||||||
|
<TreeExplorerTreeNode :node="node">
|
||||||
|
<template #before-label="{ node: treeNode }">
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
(treeNode.data as ComfyWorkflow)?.isModified ||
|
||||||
|
!(treeNode.data as ComfyWorkflow)?.isPersisted
|
||||||
|
"
|
||||||
|
>*</span
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ node: treeNode }">
|
||||||
|
<Button
|
||||||
|
class="close-workflow-button"
|
||||||
|
:variant="
|
||||||
|
workspaceStore.shiftDown ? 'destructive' : 'textonly'
|
||||||
|
"
|
||||||
|
size="icon-sm"
|
||||||
|
:aria-label="$t('g.close')"
|
||||||
|
@click.stop="
|
||||||
|
handleCloseWorkflow(treeNode.data as ComfyWorkflow)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--x] size-3" />
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</TreeExplorerTreeNode>
|
||||||
|
</template>
|
||||||
|
</TreeExplorer>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-show="filteredBookmarkedWorkflows.length > 0"
|
||||||
|
class="comfyui-workflows-bookmarks"
|
||||||
|
>
|
||||||
|
<TextDivider
|
||||||
|
:text="t('sideToolbar.workflowTab.workflowTreeType.bookmarks')"
|
||||||
|
type="dashed"
|
||||||
|
class="ml-2"
|
||||||
|
/>
|
||||||
|
<TreeExplorer
|
||||||
|
v-model:expanded-keys="dummyExpandedKeys"
|
||||||
|
:root="
|
||||||
|
renderTreeNode(
|
||||||
|
bookmarkedWorkflowsTree,
|
||||||
|
WorkflowTreeType.Bookmarks
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:selection-keys="selectionKeys"
|
||||||
|
>
|
||||||
|
<template #node="{ node }">
|
||||||
|
<WorkflowTreeLeaf :node="node" />
|
||||||
|
</template>
|
||||||
|
</TreeExplorer>
|
||||||
|
</div>
|
||||||
|
<div class="comfyui-workflows-browse">
|
||||||
|
<TextDivider
|
||||||
|
:text="t('sideToolbar.workflowTab.workflowTreeType.browse')"
|
||||||
|
type="dashed"
|
||||||
|
class="ml-2"
|
||||||
|
/>
|
||||||
|
<TreeExplorer
|
||||||
|
v-if="filteredPersistedWorkflows.length > 0"
|
||||||
|
v-model:expanded-keys="expandedKeys"
|
||||||
|
:root="renderTreeNode(workflowsTree, WorkflowTreeType.Browse)"
|
||||||
|
:selection-keys="selectionKeys"
|
||||||
|
>
|
||||||
|
<template #node="{ node }">
|
||||||
|
<WorkflowTreeLeaf :node="node" />
|
||||||
|
</template>
|
||||||
|
</TreeExplorer>
|
||||||
|
<slot v-else name="empty-state">
|
||||||
|
<NoResultsPlaceholder
|
||||||
|
icon="pi pi-folder"
|
||||||
|
:title="$t('g.empty')"
|
||||||
|
:message="$t('g.noWorkflowsFound')"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="comfyui-workflows-search-panel">
|
||||||
|
<TreeExplorer
|
||||||
|
v-model:expanded-keys="expandedKeys"
|
||||||
|
:root="renderTreeNode(filteredRoot, WorkflowTreeType.Browse)"
|
||||||
|
>
|
||||||
|
<template #node="{ node }">
|
||||||
|
<WorkflowTreeLeaf :node="node" />
|
||||||
|
</template>
|
||||||
|
</TreeExplorer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SidebarTabTemplate>
|
||||||
|
<ConfirmDialog />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ConfirmDialog from 'primevue/confirmdialog'
|
||||||
|
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||||
|
import SearchBox from '@/components/common/SearchBox.vue'
|
||||||
|
import TextDivider from '@/components/common/TextDivider.vue'
|
||||||
|
import TreeExplorer from '@/components/common/TreeExplorer.vue'
|
||||||
|
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||||
|
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
|
||||||
|
import WorkflowTreeLeaf from '@/components/sidebar/tabs/workflows/WorkflowTreeLeaf.vue'
|
||||||
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
|
import { useTreeExpansion } from '@/composables/useTreeExpansion'
|
||||||
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
|
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||||
|
import {
|
||||||
|
ComfyWorkflow,
|
||||||
|
useWorkflowBookmarkStore,
|
||||||
|
useWorkflowStore
|
||||||
|
} from '@/platform/workflow/management/stores/workflowStore'
|
||||||
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
|
import type { TreeExplorerNode, TreeNode } from '@/types/treeExplorerTypes'
|
||||||
|
import { ensureWorkflowSuffix, getWorkflowSuffix } from '@/utils/formatUtil'
|
||||||
|
import { buildTree, sortedTree } from '@/utils/treeUtil'
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
filter,
|
||||||
|
searchSubject,
|
||||||
|
dataTestid,
|
||||||
|
labelTransform,
|
||||||
|
hideLeafIcon
|
||||||
|
} = defineProps<{
|
||||||
|
title: string
|
||||||
|
filter?: (workflow: ComfyWorkflow) => boolean
|
||||||
|
searchSubject: string
|
||||||
|
dataTestid: string
|
||||||
|
labelTransform?: (label: string) => string
|
||||||
|
hideLeafIcon?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const applyFilter = (workflows: ComfyWorkflow[]) =>
|
||||||
|
filter ? workflows.filter(filter) : workflows
|
||||||
|
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
const workflowTabsPosition = computed(() =>
|
||||||
|
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
|
||||||
|
)
|
||||||
|
|
||||||
|
const searchBoxRef = ref()
|
||||||
|
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const isSearching = computed(() => searchQuery.value.length > 0)
|
||||||
|
const filteredWorkflows = ref<ComfyWorkflow[]>([])
|
||||||
|
const filteredRoot = computed<TreeNode>(() => {
|
||||||
|
return buildWorkflowTree(filteredWorkflows.value as ComfyWorkflow[])
|
||||||
|
})
|
||||||
|
const handleSearch = async (query: string) => {
|
||||||
|
if (query.length === 0) {
|
||||||
|
filteredWorkflows.value = []
|
||||||
|
expandedKeys.value = {}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const lowerQuery = query.toLocaleLowerCase()
|
||||||
|
filteredWorkflows.value = applyFilter(workflowStore.workflows).filter(
|
||||||
|
(workflow) => {
|
||||||
|
return workflow.path.toLocaleLowerCase().includes(lowerQuery)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await nextTick()
|
||||||
|
expandNode(filteredRoot.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
|
const workflowService = useWorkflowService()
|
||||||
|
const workspaceStore = useWorkspaceStore()
|
||||||
|
const expandedKeys = ref<Record<string, boolean>>({})
|
||||||
|
const { expandNode, toggleNodeOnEvent } = useTreeExpansion(expandedKeys)
|
||||||
|
const dummyExpandedKeys = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
const handleCloseWorkflow = async (workflow?: ComfyWorkflow) => {
|
||||||
|
if (workflow) {
|
||||||
|
await workflowService.closeWorkflow(workflow, {
|
||||||
|
warnIfUnsaved: !workspaceStore.shiftDown
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WorkflowTreeType {
|
||||||
|
Open = 'Open',
|
||||||
|
Bookmarks = 'Bookmarks',
|
||||||
|
Browse = 'Browse'
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildWorkflowTree = (workflows: ComfyWorkflow[]) => {
|
||||||
|
return buildTree(workflows, (workflow: ComfyWorkflow) =>
|
||||||
|
workflow.key.split('/')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredPersistedWorkflows = computed(() =>
|
||||||
|
applyFilter(workflowStore.persistedWorkflows)
|
||||||
|
)
|
||||||
|
const filteredBookmarkedWorkflows = computed(() =>
|
||||||
|
applyFilter(workflowStore.bookmarkedWorkflows)
|
||||||
|
)
|
||||||
|
|
||||||
|
const workflowsTree = computed(() =>
|
||||||
|
sortedTree(buildWorkflowTree(filteredPersistedWorkflows.value), {
|
||||||
|
groupLeaf: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// Bookmarked workflows tree is flat.
|
||||||
|
const bookmarkedWorkflowsTree = computed(() =>
|
||||||
|
buildTree(filteredBookmarkedWorkflows.value, (workflow) => [workflow.key])
|
||||||
|
)
|
||||||
|
// Open workflows tree is flat.
|
||||||
|
const openWorkflowsTree = computed(() =>
|
||||||
|
buildTree(applyFilter(workflowStore.openWorkflows), (workflow) => [
|
||||||
|
workflow.key
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderTreeNode = (
|
||||||
|
node: TreeNode,
|
||||||
|
type: WorkflowTreeType
|
||||||
|
): TreeExplorerNode<ComfyWorkflow> => {
|
||||||
|
const children = node.children?.map((child) => renderTreeNode(child, type))
|
||||||
|
|
||||||
|
const workflow: ComfyWorkflow = node.data
|
||||||
|
|
||||||
|
async function handleClick(
|
||||||
|
this: TreeExplorerNode<ComfyWorkflow>,
|
||||||
|
e: MouseEvent
|
||||||
|
) {
|
||||||
|
if (this.leaf) {
|
||||||
|
await workflowService.openWorkflow(workflow)
|
||||||
|
} else {
|
||||||
|
toggleNodeOnEvent(e, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = node.leaf
|
||||||
|
? {
|
||||||
|
handleClick,
|
||||||
|
async handleRename(newName: string) {
|
||||||
|
const suffix = getWorkflowSuffix(workflow.suffix)
|
||||||
|
const newPath =
|
||||||
|
type === WorkflowTreeType.Browse
|
||||||
|
? workflow.directory + '/' + ensureWorkflowSuffix(newName, suffix)
|
||||||
|
: ComfyWorkflow.basePath + ensureWorkflowSuffix(newName, suffix)
|
||||||
|
|
||||||
|
await workflowService.renameWorkflow(workflow, newPath)
|
||||||
|
},
|
||||||
|
handleDelete: workflow.isTemporary
|
||||||
|
? undefined
|
||||||
|
: async function () {
|
||||||
|
await workflowService.deleteWorkflow(workflow)
|
||||||
|
},
|
||||||
|
contextMenuItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t('g.insert'),
|
||||||
|
icon: 'pi pi-file-export',
|
||||||
|
command: async () => {
|
||||||
|
const workflow = node.data
|
||||||
|
await workflowService.insertWorkflow(workflow)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('g.duplicate'),
|
||||||
|
icon: 'pi pi-file-export',
|
||||||
|
command: async () => {
|
||||||
|
const workflow = node.data
|
||||||
|
await workflowService.duplicateWorkflow(workflow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
draggable: true
|
||||||
|
}
|
||||||
|
: { handleClick }
|
||||||
|
|
||||||
|
const label =
|
||||||
|
node.leaf && labelTransform ? labelTransform(node.label) : node.label
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: node.key,
|
||||||
|
label,
|
||||||
|
leaf: node.leaf,
|
||||||
|
icon: node.leaf && hideLeafIcon ? 'hidden' : undefined,
|
||||||
|
data: node.data,
|
||||||
|
children,
|
||||||
|
...actions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectionKeys = computed(() => ({
|
||||||
|
[`root/${workflowStore.activeWorkflow?.key}`]: true
|
||||||
|
}))
|
||||||
|
|
||||||
|
const workflowBookmarkStore = useWorkflowBookmarkStore()
|
||||||
|
onMounted(async () => {
|
||||||
|
searchBoxRef.value?.focus()
|
||||||
|
await workflowBookmarkStore.loadBookmarks()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,315 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<SidebarTabTemplate
|
<BaseWorkflowsSidebarTab
|
||||||
:title="$t('sideToolbar.workflows')"
|
:title="$t('sideToolbar.workflows')"
|
||||||
v-bind="$attrs"
|
:search-subject="$t('g.workflow')"
|
||||||
data-testid="workflows-sidebar"
|
data-testid="workflows-sidebar"
|
||||||
class="workflows-sidebar-tab"
|
/>
|
||||||
>
|
|
||||||
<template #tool-buttons>
|
|
||||||
<Button
|
|
||||||
v-tooltip.bottom="$t('g.refresh')"
|
|
||||||
variant="muted-textonly"
|
|
||||||
size="icon"
|
|
||||||
:aria-label="$t('g.refresh')"
|
|
||||||
@click="workflowStore.syncWorkflows()"
|
|
||||||
>
|
|
||||||
<i class="icon-[lucide--refresh-cw] size-4" />
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
<template #header>
|
|
||||||
<div class="px-2 2xl:px-4">
|
|
||||||
<SearchBox
|
|
||||||
ref="searchBoxRef"
|
|
||||||
v-model:model-value="searchQuery"
|
|
||||||
class="workflows-search-box"
|
|
||||||
:placeholder="
|
|
||||||
$t('g.searchPlaceholder', { subject: $t('g.workflow') })
|
|
||||||
"
|
|
||||||
@search="handleSearch"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<div v-if="!isSearching" class="comfyui-workflows-panel">
|
|
||||||
<div
|
|
||||||
v-if="workflowTabsPosition === 'Sidebar'"
|
|
||||||
class="comfyui-workflows-open"
|
|
||||||
>
|
|
||||||
<TextDivider
|
|
||||||
:text="t('sideToolbar.workflowTab.workflowTreeType.open')"
|
|
||||||
type="dashed"
|
|
||||||
class="ml-2"
|
|
||||||
/>
|
|
||||||
<TreeExplorer
|
|
||||||
v-model:expanded-keys="dummyExpandedKeys"
|
|
||||||
:root="renderTreeNode(openWorkflowsTree, WorkflowTreeType.Open)"
|
|
||||||
:selection-keys="selectionKeys"
|
|
||||||
>
|
|
||||||
<template #node="{ node }">
|
|
||||||
<TreeExplorerTreeNode :node="node">
|
|
||||||
<template #before-label="{ node: treeNode }">
|
|
||||||
<span
|
|
||||||
v-if="
|
|
||||||
(treeNode.data as ComfyWorkflow)?.isModified ||
|
|
||||||
!(treeNode.data as ComfyWorkflow)?.isPersisted
|
|
||||||
"
|
|
||||||
>*</span
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<template #actions="{ node: treeNode }">
|
|
||||||
<Button
|
|
||||||
class="close-workflow-button"
|
|
||||||
:variant="
|
|
||||||
workspaceStore.shiftDown ? 'destructive' : 'textonly'
|
|
||||||
"
|
|
||||||
size="icon-sm"
|
|
||||||
:aria-label="$t('g.close')"
|
|
||||||
@click.stop="
|
|
||||||
handleCloseWorkflow(treeNode.data as ComfyWorkflow)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<i class="icon-[lucide--x] size-3" />
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
</TreeExplorerTreeNode>
|
|
||||||
</template>
|
|
||||||
</TreeExplorer>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-show="workflowStore.bookmarkedWorkflows.length > 0"
|
|
||||||
class="comfyui-workflows-bookmarks"
|
|
||||||
>
|
|
||||||
<TextDivider
|
|
||||||
:text="t('sideToolbar.workflowTab.workflowTreeType.bookmarks')"
|
|
||||||
type="dashed"
|
|
||||||
class="ml-2"
|
|
||||||
/>
|
|
||||||
<TreeExplorer
|
|
||||||
v-model:expanded-keys="dummyExpandedKeys"
|
|
||||||
:root="
|
|
||||||
renderTreeNode(
|
|
||||||
bookmarkedWorkflowsTree,
|
|
||||||
WorkflowTreeType.Bookmarks
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:selection-keys="selectionKeys"
|
|
||||||
>
|
|
||||||
<template #node="{ node }">
|
|
||||||
<WorkflowTreeLeaf :node="node" />
|
|
||||||
</template>
|
|
||||||
</TreeExplorer>
|
|
||||||
</div>
|
|
||||||
<div class="comfyui-workflows-browse">
|
|
||||||
<TextDivider
|
|
||||||
:text="t('sideToolbar.workflowTab.workflowTreeType.browse')"
|
|
||||||
type="dashed"
|
|
||||||
class="ml-2"
|
|
||||||
/>
|
|
||||||
<TreeExplorer
|
|
||||||
v-if="workflowStore.persistedWorkflows.length > 0"
|
|
||||||
v-model:expanded-keys="expandedKeys"
|
|
||||||
:root="renderTreeNode(workflowsTree, WorkflowTreeType.Browse)"
|
|
||||||
:selection-keys="selectionKeys"
|
|
||||||
>
|
|
||||||
<template #node="{ node }">
|
|
||||||
<WorkflowTreeLeaf :node="node" />
|
|
||||||
</template>
|
|
||||||
</TreeExplorer>
|
|
||||||
<NoResultsPlaceholder
|
|
||||||
v-else
|
|
||||||
icon="pi pi-folder"
|
|
||||||
:title="$t('g.empty')"
|
|
||||||
:message="$t('g.noWorkflowsFound')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="comfyui-workflows-search-panel">
|
|
||||||
<TreeExplorer
|
|
||||||
v-model:expanded-keys="expandedKeys"
|
|
||||||
:root="renderTreeNode(filteredRoot, WorkflowTreeType.Browse)"
|
|
||||||
>
|
|
||||||
<template #node="{ node }">
|
|
||||||
<WorkflowTreeLeaf :node="node" />
|
|
||||||
</template>
|
|
||||||
</TreeExplorer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</SidebarTabTemplate>
|
|
||||||
<ConfirmDialog />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ConfirmDialog from 'primevue/confirmdialog'
|
import BaseWorkflowsSidebarTab from '@/components/sidebar/tabs/BaseWorkflowsSidebarTab.vue'
|
||||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
|
||||||
import SearchBox from '@/components/common/SearchBox.vue'
|
|
||||||
import TextDivider from '@/components/common/TextDivider.vue'
|
|
||||||
import TreeExplorer from '@/components/common/TreeExplorer.vue'
|
|
||||||
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
|
||||||
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
|
|
||||||
import WorkflowTreeLeaf from '@/components/sidebar/tabs/workflows/WorkflowTreeLeaf.vue'
|
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
|
||||||
import { useTreeExpansion } from '@/composables/useTreeExpansion'
|
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
||||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
|
||||||
import {
|
|
||||||
ComfyWorkflow,
|
|
||||||
useWorkflowBookmarkStore,
|
|
||||||
useWorkflowStore
|
|
||||||
} from '@/platform/workflow/management/stores/workflowStore'
|
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
|
||||||
import type { TreeExplorerNode, TreeNode } from '@/types/treeExplorerTypes'
|
|
||||||
import { ensureWorkflowSuffix, getWorkflowSuffix } from '@/utils/formatUtil'
|
|
||||||
import { buildTree, sortedTree } from '@/utils/treeUtil'
|
|
||||||
|
|
||||||
const settingStore = useSettingStore()
|
|
||||||
const workflowTabsPosition = computed(() =>
|
|
||||||
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
|
|
||||||
)
|
|
||||||
|
|
||||||
const searchBoxRef = ref()
|
|
||||||
|
|
||||||
const searchQuery = ref('')
|
|
||||||
const isSearching = computed(() => searchQuery.value.length > 0)
|
|
||||||
const filteredWorkflows = ref<ComfyWorkflow[]>([])
|
|
||||||
const filteredRoot = computed<TreeNode>(() => {
|
|
||||||
return buildWorkflowTree(filteredWorkflows.value as ComfyWorkflow[])
|
|
||||||
})
|
|
||||||
const handleSearch = async (query: string) => {
|
|
||||||
if (query.length === 0) {
|
|
||||||
filteredWorkflows.value = []
|
|
||||||
expandedKeys.value = {}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const lowerQuery = query.toLocaleLowerCase()
|
|
||||||
filteredWorkflows.value = workflowStore.workflows.filter((workflow) => {
|
|
||||||
return workflow.path.toLocaleLowerCase().includes(lowerQuery)
|
|
||||||
})
|
|
||||||
await nextTick()
|
|
||||||
expandNode(filteredRoot.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowStore = useWorkflowStore()
|
|
||||||
const workflowService = useWorkflowService()
|
|
||||||
const workspaceStore = useWorkspaceStore()
|
|
||||||
const { t } = useI18n()
|
|
||||||
const expandedKeys = ref<Record<string, boolean>>({})
|
|
||||||
const { expandNode, toggleNodeOnEvent } = useTreeExpansion(expandedKeys)
|
|
||||||
const dummyExpandedKeys = ref<Record<string, boolean>>({})
|
|
||||||
|
|
||||||
const handleCloseWorkflow = async (workflow?: ComfyWorkflow) => {
|
|
||||||
if (workflow) {
|
|
||||||
await workflowService.closeWorkflow(workflow, {
|
|
||||||
warnIfUnsaved: !workspaceStore.shiftDown
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum WorkflowTreeType {
|
|
||||||
Open = 'Open',
|
|
||||||
Bookmarks = 'Bookmarks',
|
|
||||||
Browse = 'Browse'
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildWorkflowTree = (workflows: ComfyWorkflow[]) => {
|
|
||||||
return buildTree(workflows, (workflow: ComfyWorkflow) =>
|
|
||||||
workflow.key.split('/')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowsTree = computed(() =>
|
|
||||||
sortedTree(buildWorkflowTree(workflowStore.persistedWorkflows), {
|
|
||||||
groupLeaf: true
|
|
||||||
})
|
|
||||||
)
|
|
||||||
// Bookmarked workflows tree is flat.
|
|
||||||
const bookmarkedWorkflowsTree = computed(() =>
|
|
||||||
buildTree(workflowStore.bookmarkedWorkflows, (workflow) => [workflow.key])
|
|
||||||
)
|
|
||||||
// Open workflows tree is flat.
|
|
||||||
const openWorkflowsTree = computed(() =>
|
|
||||||
buildTree(workflowStore.openWorkflows, (workflow) => [workflow.key])
|
|
||||||
)
|
|
||||||
|
|
||||||
const renderTreeNode = (
|
|
||||||
node: TreeNode,
|
|
||||||
type: WorkflowTreeType
|
|
||||||
): TreeExplorerNode<ComfyWorkflow> => {
|
|
||||||
const children = node.children?.map((child) => renderTreeNode(child, type))
|
|
||||||
|
|
||||||
const workflow: ComfyWorkflow = node.data
|
|
||||||
|
|
||||||
async function handleClick(
|
|
||||||
this: TreeExplorerNode<ComfyWorkflow>,
|
|
||||||
e: MouseEvent
|
|
||||||
) {
|
|
||||||
if (this.leaf) {
|
|
||||||
await workflowService.openWorkflow(workflow)
|
|
||||||
} else {
|
|
||||||
toggleNodeOnEvent(e, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions = node.leaf
|
|
||||||
? {
|
|
||||||
handleClick,
|
|
||||||
async handleRename(newName: string) {
|
|
||||||
const suffix = getWorkflowSuffix(workflow.suffix)
|
|
||||||
const newPath =
|
|
||||||
type === WorkflowTreeType.Browse
|
|
||||||
? workflow.directory + '/' + ensureWorkflowSuffix(newName, suffix)
|
|
||||||
: ComfyWorkflow.basePath + ensureWorkflowSuffix(newName, suffix)
|
|
||||||
|
|
||||||
await workflowService.renameWorkflow(workflow, newPath)
|
|
||||||
},
|
|
||||||
handleDelete: workflow.isTemporary
|
|
||||||
? undefined
|
|
||||||
: async function () {
|
|
||||||
await workflowService.deleteWorkflow(workflow)
|
|
||||||
},
|
|
||||||
contextMenuItems() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: t('g.insert'),
|
|
||||||
icon: 'pi pi-file-export',
|
|
||||||
command: async () => {
|
|
||||||
const workflow = node.data
|
|
||||||
await workflowService.insertWorkflow(workflow)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('g.duplicate'),
|
|
||||||
icon: 'pi pi-file-export',
|
|
||||||
command: async () => {
|
|
||||||
const workflow = node.data
|
|
||||||
await workflowService.duplicateWorkflow(workflow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
draggable: true
|
|
||||||
}
|
|
||||||
: { handleClick }
|
|
||||||
|
|
||||||
return {
|
|
||||||
key: node.key,
|
|
||||||
label: node.label,
|
|
||||||
leaf: node.leaf,
|
|
||||||
data: node.data,
|
|
||||||
children,
|
|
||||||
...actions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectionKeys = computed(() => ({
|
|
||||||
[`root/${workflowStore.activeWorkflow?.key}`]: true
|
|
||||||
}))
|
|
||||||
|
|
||||||
const workflowBookmarkStore = useWorkflowBookmarkStore()
|
|
||||||
onMounted(async () => {
|
|
||||||
searchBoxRef.value?.focus()
|
|
||||||
await workflowBookmarkStore.loadBookmarks()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3043,7 +3043,9 @@
|
|||||||
},
|
},
|
||||||
"appModeToolbar": {
|
"appModeToolbar": {
|
||||||
"appBuilder": "App builder",
|
"appBuilder": "App builder",
|
||||||
"apps": "Apps"
|
"apps": "Apps",
|
||||||
|
"appsEmptyMessage": "Saved apps will show up here.\nClick below to build your first app.",
|
||||||
|
"enterAppMode": "Enter app mode"
|
||||||
},
|
},
|
||||||
"arrange": {
|
"arrange": {
|
||||||
"noOutputs": "No outputs added yet",
|
"noOutputs": "No outputs added yet",
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { markRaw } from 'vue'
|
||||||
|
|
||||||
|
import AppsSidebarTab from '@/components/sidebar/tabs/AppsSidebarTab.vue'
|
||||||
|
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||||
|
|
||||||
|
export const useAppsSidebarTab = (): SidebarTabExtension => {
|
||||||
|
return {
|
||||||
|
id: 'apps',
|
||||||
|
icon: 'icon-[lucide--panels-top-left]',
|
||||||
|
title: 'linearMode.appModeToolbar.apps',
|
||||||
|
tooltip: 'linearMode.appModeToolbar.apps',
|
||||||
|
component: markRaw(AppsSidebarTab),
|
||||||
|
type: 'vue'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,6 +84,15 @@ vi.mock(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
vi.mock('@/platform/workflow/management/composables/useAppsSidebarTab', () => ({
|
||||||
|
useAppsSidebarTab: () => ({
|
||||||
|
id: 'apps',
|
||||||
|
title: 'apps',
|
||||||
|
type: 'vue',
|
||||||
|
component: {}
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
describe('useSidebarTabStore', () => {
|
describe('useSidebarTabStore', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||||
@@ -105,9 +114,10 @@ describe('useSidebarTabStore', () => {
|
|||||||
'assets',
|
'assets',
|
||||||
'node-library',
|
'node-library',
|
||||||
'model-library',
|
'model-library',
|
||||||
'workflows'
|
'workflows',
|
||||||
|
'apps'
|
||||||
])
|
])
|
||||||
expect(mockRegisterCommand).toHaveBeenCalledTimes(5)
|
expect(mockRegisterCommand).toHaveBeenCalledTimes(6)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not register the job history tab when QPO V2 is disabled', () => {
|
it('does not register the job history tab when QPO V2 is disabled', () => {
|
||||||
@@ -122,9 +132,10 @@ describe('useSidebarTabStore', () => {
|
|||||||
'assets',
|
'assets',
|
||||||
'node-library',
|
'node-library',
|
||||||
'model-library',
|
'model-library',
|
||||||
'workflows'
|
'workflows',
|
||||||
|
'apps'
|
||||||
])
|
])
|
||||||
expect(mockRegisterCommand).toHaveBeenCalledTimes(4)
|
expect(mockRegisterCommand).toHaveBeenCalledTimes(5)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('prepends the job history tab when QPO V2 is toggled on', async () => {
|
it('prepends the job history tab when QPO V2 is toggled on', async () => {
|
||||||
@@ -144,8 +155,9 @@ describe('useSidebarTabStore', () => {
|
|||||||
'assets',
|
'assets',
|
||||||
'node-library',
|
'node-library',
|
||||||
'model-library',
|
'model-library',
|
||||||
'workflows'
|
'workflows',
|
||||||
|
'apps'
|
||||||
])
|
])
|
||||||
expect(mockRegisterCommand).toHaveBeenCalledTimes(5)
|
expect(mockRegisterCommand).toHaveBeenCalledTimes(6)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useModelLibrarySidebarTab } from '@/composables/sidebarTabs/useModelLib
|
|||||||
import { useNodeLibrarySidebarTab } from '@/composables/sidebarTabs/useNodeLibrarySidebarTab'
|
import { useNodeLibrarySidebarTab } from '@/composables/sidebarTabs/useNodeLibrarySidebarTab'
|
||||||
import { t, te } from '@/i18n'
|
import { t, te } from '@/i18n'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
|
import { useAppsSidebarTab } from '@/platform/workflow/management/composables/useAppsSidebarTab'
|
||||||
import { useWorkflowsSidebarTab } from '@/platform/workflow/management/composables/useWorkflowsSidebarTab'
|
import { useWorkflowsSidebarTab } from '@/platform/workflow/management/composables/useWorkflowsSidebarTab'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||||
@@ -135,6 +136,7 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
|||||||
registerSidebarTab(useNodeLibrarySidebarTab())
|
registerSidebarTab(useNodeLibrarySidebarTab())
|
||||||
registerSidebarTab(useModelLibrarySidebarTab())
|
registerSidebarTab(useModelLibrarySidebarTab())
|
||||||
registerSidebarTab(useWorkflowsSidebarTab())
|
registerSidebarTab(useWorkflowsSidebarTab())
|
||||||
|
registerSidebarTab(useAppsSidebarTab())
|
||||||
|
|
||||||
const menuStore = useMenuItemStore()
|
const menuStore = useMenuItemStore()
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,12 @@ const graphCanvasContainerRef = ref<HTMLDivElement | null>(null)
|
|||||||
const { isBuilderMode } = useAppMode()
|
const { isBuilderMode } = useAppMode()
|
||||||
const { linearMode } = storeToRefs(useCanvasStore())
|
const { linearMode } = storeToRefs(useCanvasStore())
|
||||||
|
|
||||||
|
watch(linearMode, (isLinear) => {
|
||||||
|
if (isLinear) {
|
||||||
|
useSidebarTabStore().activeSidebarTabId = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const telemetry = useTelemetry()
|
const telemetry = useTelemetry()
|
||||||
const firebaseAuthStore = useFirebaseAuthStore()
|
const firebaseAuthStore = useFirebaseAuthStore()
|
||||||
let hasTrackedLogin = false
|
let hasTrackedLogin = false
|
||||||
|
|||||||