Skip to content

Commit b18a071

Browse files
feat: App mode enter builder menu item (#9341)
## Summary Adds enter builder menu item for easier access to app builder. Fixes issues with seen item tracking ## Changes - **What**: - add enter builder menu item - change non visible items to still be returned as part of the array, so they are not incorrectly removed from the seen-items tracking - split toggle-app-mode into two stable items ## Screenshots (if applicable) <img width="309" height="526" alt="image" src="https://github.com/user-attachments/assets/69affc2c-34ab-45eb-b47b-efacb8a20b99" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9341-feat-App-mode-enter-builder-menu-item-3176d73d365081a9a7e7cf1a1986354f) by [Unito](https://www.unito.io)
1 parent d360b22 commit b18a071

8 files changed

Lines changed: 172 additions & 26 deletions

src/components/common/WorkflowActionsList.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,24 @@ describe('WorkflowActionsList', () => {
7070
expect(wrapper.text()).toContain('NEW')
7171
})
7272

73+
it('does not render items with visible set to false', () => {
74+
const items: WorkflowMenuItem[] = [
75+
{
76+
id: 'hidden',
77+
label: 'Hidden Item',
78+
icon: 'pi pi-eye-slash',
79+
command: vi.fn(),
80+
visible: false
81+
},
82+
{ id: 'shown', label: 'Shown Item', icon: 'pi pi-eye', command: vi.fn() }
83+
]
84+
85+
const wrapper = createWrapper(items)
86+
87+
expect(wrapper.text()).not.toContain('Hidden Item')
88+
expect(wrapper.text()).toContain('Shown Item')
89+
})
90+
7391
it('does not render badge when absent', () => {
7492
const items: WorkflowMenuAction[] = [
7593
{ id: 'plain', label: 'Plain', icon: 'pi pi-check', command: vi.fn() }

src/components/common/WorkflowActionsList.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const {
2626
/>
2727
<component
2828
:is="itemComponent"
29-
v-else
29+
v-else-if="item.visible !== false"
3030
:disabled="item.disabled"
3131
:class="
3232
cn(

src/composables/useNewMenuItemIndicator.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,80 @@ describe('useNewMenuItemIndicator', () => {
9898
)
9999
})
100100

101+
it('does not count hidden items as unseen', () => {
102+
const items: WorkflowMenuItem[] = [
103+
{
104+
id: 'hidden-feature',
105+
label: 'Hidden',
106+
icon: 'pi pi-test',
107+
command: vi.fn(),
108+
isNew: true,
109+
badge: 'BETA',
110+
visible: false
111+
}
112+
]
113+
const { hasUnseenItems } = useNewMenuItemIndicator(() => items)
114+
115+
expect(hasUnseenItems.value).toBe(false)
116+
})
117+
118+
it('markAsSeen does not include never-seen hidden items', () => {
119+
const items: WorkflowMenuItem[] = [
120+
...createItems('feature-a'),
121+
{
122+
id: 'hidden-feature',
123+
label: 'Hidden',
124+
icon: 'pi pi-test',
125+
command: vi.fn(),
126+
isNew: true,
127+
badge: 'BETA',
128+
visible: false
129+
}
130+
]
131+
const { markAsSeen } = useNewMenuItemIndicator(() => items)
132+
133+
markAsSeen()
134+
135+
expect(mockSettingStore.set).toHaveBeenCalledWith(
136+
'Comfy.WorkflowActions.SeenItems',
137+
['feature-a']
138+
)
139+
})
140+
141+
it('markAsSeen retains previously-seen hidden items', () => {
142+
mockSettingStore.get.mockReturnValue(['hidden-feature'])
143+
const items: WorkflowMenuItem[] = [
144+
...createItems('feature-a'),
145+
{
146+
id: 'hidden-feature',
147+
label: 'Hidden',
148+
icon: 'pi pi-test',
149+
command: vi.fn(),
150+
isNew: true,
151+
badge: 'BETA',
152+
visible: false
153+
}
154+
]
155+
const { markAsSeen } = useNewMenuItemIndicator(() => items)
156+
157+
markAsSeen()
158+
159+
expect(mockSettingStore.set).toHaveBeenCalledWith(
160+
'Comfy.WorkflowActions.SeenItems',
161+
['feature-a', 'hidden-feature']
162+
)
163+
})
164+
165+
it('markAsSeen skips write when stored list already matches', () => {
166+
mockSettingStore.get.mockReturnValue(['feature-a', 'feature-b'])
167+
const items = createItems('feature-a', 'feature-b')
168+
const { markAsSeen } = useNewMenuItemIndicator(() => items)
169+
170+
markAsSeen()
171+
172+
expect(mockSettingStore.set).not.toHaveBeenCalled()
173+
})
174+
101175
it('markAsSeen does nothing when there are no new items', () => {
102176
const items: WorkflowMenuItem[] = [
103177
{ id: 'regular', label: 'Regular', icon: 'pi pi-test', command: vi.fn() }

src/composables/useNewMenuItemIndicator.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,47 @@ import type {
77
WorkflowMenuItem
88
} from '@/types/workflowMenuItem'
99

10-
function getNewItemIds(items: WorkflowMenuItem[]): string[] {
10+
function getNewActions(items: WorkflowMenuItem[]): WorkflowMenuAction[] {
1111
return items
1212
.filter((i): i is WorkflowMenuAction => !('separator' in i && i.separator))
1313
.filter((i) => i.isNew)
14-
.map((i) => i.id)
1514
}
1615

1716
export function useNewMenuItemIndicator(
1817
menuItems: MaybeRefOrGetter<WorkflowMenuItem[]>
1918
) {
2019
const settingStore = useSettingStore()
2120

22-
const newItemIds = computed(() => getNewItemIds(toValue(menuItems)))
21+
const newActions = computed(() => getNewActions(toValue(menuItems)))
2322

2423
const seenItems = computed<string[]>(
2524
() => settingStore.get('Comfy.WorkflowActions.SeenItems') ?? []
2625
)
2726

2827
const hasUnseenItems = computed(() => {
2928
const seen = new Set(seenItems.value)
30-
return newItemIds.value.some((id) => !seen.has(id))
29+
return newActions.value
30+
.filter((i) => i.visible !== false)
31+
.some((i) => !seen.has(i.id))
3132
})
3233

3334
function markAsSeen() {
34-
if (!newItemIds.value.length) return
35-
void settingStore.set('Comfy.WorkflowActions.SeenItems', [
36-
...newItemIds.value
37-
])
35+
const actions = newActions.value
36+
if (!actions.length) return
37+
38+
const seen = new Set(seenItems.value)
39+
const visibleIds = actions
40+
.filter((i) => i.visible !== false)
41+
.map((i) => i.id)
42+
const retainedIds = actions
43+
.filter((i) => i.visible === false && seen.has(i.id))
44+
.map((i) => i.id)
45+
46+
const nextSeen = [...visibleIds, ...retainedIds]
47+
if (nextSeen.length === seen.size && nextSeen.every((id) => seen.has(id)))
48+
return
49+
50+
void settingStore.set('Comfy.WorkflowActions.SeenItems', nextSeen)
3851
}
3952

4053
return { hasUnseenItems, markAsSeen }

src/composables/useWorkflowActionsMenu.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ const mockCanvasStore = vi.hoisted(() => ({
4444
linearMode: false
4545
}))
4646

47+
const mockAppModeStore = vi.hoisted(() => ({
48+
enterBuilder: vi.fn()
49+
}))
50+
4751
const mockFeatureFlags = vi.hoisted(() => ({
4852
flags: { linearToggleEnabled: false }
4953
}))
@@ -73,14 +77,20 @@ vi.mock('@/renderer/core/canvas/canvasStore', () => ({
7377
useCanvasStore: vi.fn(() => mockCanvasStore)
7478
}))
7579

80+
vi.mock('@/stores/appModeStore', () => ({
81+
useAppModeStore: vi.fn(() => mockAppModeStore)
82+
}))
83+
7684
vi.mock('@/composables/useFeatureFlags', () => ({
7785
useFeatureFlags: vi.fn(() => mockFeatureFlags)
7886
}))
7987

8088
type MenuItems = ReturnType<typeof useWorkflowActionsMenu>['menuItems']['value']
8189

8290
function actionItems(items: MenuItems): WorkflowMenuAction[] {
83-
return items.filter((i): i is WorkflowMenuAction => !i.separator)
91+
return items.filter(
92+
(i): i is WorkflowMenuAction => !i.separator && i.visible !== false
93+
)
8494
}
8595

8696
function menuLabels(items: MenuItems) {
@@ -288,6 +298,18 @@ describe('useWorkflowActionsMenu', () => {
288298
expect(mockBookmarkStore.toggleBookmarked).toHaveBeenCalledWith('test.json')
289299
})
290300

301+
it('enter builder mode calls enterBuilder', async () => {
302+
mockFeatureFlags.flags.linearToggleEnabled = true
303+
304+
const { menuItems } = useWorkflowActionsMenu(vi.fn(), { isRoot: true })
305+
await findItem(
306+
menuItems.value,
307+
'breadcrumbsMenu.enterBuilderMode'
308+
).command?.()
309+
310+
expect(mockAppModeStore.enterBuilder).toHaveBeenCalled()
311+
})
312+
291313
it('app mode toggle executes Comfy.ToggleLinear', async () => {
292314
mockFeatureFlags.flags.linearToggleEnabled = true
293315

src/composables/useWorkflowActionsMenu.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
1313
import { useCommandStore } from '@/stores/commandStore'
1414
import { useMenuItemStore } from '@/stores/menuItemStore'
1515
import { useSubgraphStore } from '@/stores/subgraphStore'
16+
import { useAppModeStore } from '@/stores/appModeStore'
1617
import type {
1718
WorkflowMenuAction,
1819
WorkflowMenuItem
@@ -52,6 +53,7 @@ export function useWorkflowActionsMenu(
5253
const menuItemStore = useMenuItemStore()
5354
const canvasStore = useCanvasStore()
5455
const { flags } = useFeatureFlags()
56+
const { enterBuilder } = useAppModeStore()
5557

5658
const targetWorkflow = computed(
5759
() => workflow?.value ?? workflowStore.activeWorkflow
@@ -81,9 +83,9 @@ export function useWorkflowActionsMenu(
8183
prependSeparator = false,
8284
isNew = false
8385
}: AddItemOptions) => {
84-
if (!visible) return
85-
if (prependSeparator) items.push({ separator: true })
86+
if (prependSeparator && visible) items.push({ separator: true })
8687
const item: WorkflowMenuAction = { id, label, icon, command, disabled }
88+
if (!visible) item.visible = false
8789
if (isNew) {
8890
item.badge = t('g.experimental')
8991
item.isNew = true
@@ -96,6 +98,11 @@ export function useWorkflowActionsMenu(
9698
isRoot && (menuItemStore.hasSeenLinear || flags.linearToggleEnabled)
9799
const isBookmarked = bookmarkStore.isBookmarked(workflow?.path ?? '')
98100

101+
const toggleLinear = async () => {
102+
await commandStore.execute('Comfy.ToggleLinear', {
103+
metadata: { source: 'breadcrumb_menu' }
104+
})
105+
}
99106
addItem({
100107
id: 'rename',
101108
label: t('g.rename'),
@@ -181,21 +188,31 @@ export function useWorkflowActionsMenu(
181188
})
182189

183190
addItem({
184-
id: 'toggle-app-mode',
185-
label: isLinearMode
186-
? t('breadcrumbsMenu.exitAppMode')
187-
: t('breadcrumbsMenu.enterAppMode'),
188-
icon: isLinearMode
189-
? 'icon-[comfy--workflow]'
190-
: 'icon-[lucide--panels-top-left]',
191-
command: async () => {
192-
await commandStore.execute('Comfy.ToggleLinear', {
193-
metadata: { source: 'breadcrumb_menu' }
194-
})
195-
},
196-
visible: showAppModeItems,
191+
id: 'enter-app-mode',
192+
label: t('breadcrumbsMenu.enterAppMode'),
193+
icon: 'icon-[lucide--panels-top-left]',
194+
command: toggleLinear,
195+
visible: showAppModeItems && !isLinearMode,
197196
prependSeparator: true,
198-
isNew: !isLinearMode
197+
isNew: true
198+
})
199+
200+
addItem({
201+
id: 'exit-app-mode',
202+
label: t('breadcrumbsMenu.exitAppMode'),
203+
icon: 'icon-[comfy--workflow]',
204+
command: toggleLinear,
205+
visible: isLinearMode,
206+
prependSeparator: true
207+
})
208+
209+
addItem({
210+
id: 'enter-builder-mode',
211+
label: t('breadcrumbsMenu.enterBuilderMode'),
212+
icon: 'icon-[lucide--hammer]',
213+
command: () => enterBuilder(),
214+
visible: showAppModeItems,
215+
isNew: true
199216
})
200217

201218
addItem({

src/locales/en/main.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2589,6 +2589,7 @@
25892589
"duplicate": "Duplicate",
25902590
"enterAppMode": "Enter app mode",
25912591
"exitAppMode": "Exit app mode",
2592+
"enterBuilderMode": "Enter app builder",
25922593
"workflowActions": "Workflow actions",
25932594
"clearWorkflow": "Clear Workflow",
25942595
"deleteWorkflow": "Delete Workflow",

src/types/workflowMenuItem.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface WorkflowMenuSeparator {
88

99
export interface WorkflowMenuAction {
1010
separator?: false
11+
visible?: boolean
1112
id: string
1213
label: string
1314
icon?: string

0 commit comments

Comments
 (0)