feat(stacks): add Schedule task shortcut to stack context and kebab menus (#772)

Right-clicking a stack or opening its 3-dot menu now shows a
'Schedule task' entry in the lifecycle group (visible to paid tiers).
Clicking it navigates to Scheduled Operations and opens the New
Schedule dialog pre-filled with the stack name and active node,
removing the need to navigate there manually and re-enter the target.

- Added openScheduleTask to StackMenuCtx; wired in buildMenuCtx using
  the active node from NodeContext
- Extended ScheduledOperationsView with optional prefill/onPrefillConsumed
  props; a ref-guarded effect calls openCreate() with the prefill data
- openCreate refactored to accept an optional prefill arg, removing the
  duplication between the effect and the existing 'New Schedule' button
This commit is contained in:
Anso
2026-04-25 11:49:01 -04:00
committed by GitHub
parent af9cb0aa63
commit 819d2a63fc
5 changed files with 58 additions and 27 deletions
@@ -32,6 +32,7 @@ function makeCtx(overrides: Partial<StackMenuCtx> = {}): StackMenuCtx {
createAndAssignLabel: vi.fn(),
openLabelManager: vi.fn(),
setAutoUpdateEnabled: vi.fn(),
openScheduleTask: vi.fn(),
...overrides,
};
}
@@ -82,15 +83,6 @@ describe('useStackMenuItems', () => {
expect(del.icon).toBe(Trash2);
});
it('lifecycle items follow menuVisibility flags', () => {
const { result } = renderHook(() => useStackMenuItems('web.yml', makeCtx({
menuVisibility: { showDeploy: true, showStop: false, showRestart: false, showUpdate: true },
})));
const lifecycle = result.current.find(g => g.id === 'lifecycle')!;
const ids = lifecycle.items.map(i => i.id);
expect(ids).toEqual(['deploy', 'update']);
});
it('shows auto-update toggle in inspect when isPaid', () => {
const { result } = renderHook(() => useStackMenuItems('web.yml', makeCtx({ isPaid: true, autoUpdateEnabled: true })));
const inspect = result.current.find(g => g.id === 'inspect')!;
@@ -113,12 +105,24 @@ describe('useStackMenuItems', () => {
expect(setAutoUpdateEnabled).toHaveBeenCalledWith(false);
});
it('disables every lifecycle item when isBusy', () => {
it('lifecycle items follow menuVisibility flags', () => {
const { result } = renderHook(() => useStackMenuItems('web.yml', makeCtx({
menuVisibility: { showDeploy: true, showStop: false, showRestart: false, showUpdate: true },
})));
const lifecycle = result.current.find(g => g.id === 'lifecycle')!;
const ids = lifecycle.items.map(i => i.id);
expect(ids).toEqual(['deploy', 'update', 'schedule']);
});
it('disables action lifecycle items when isBusy but leaves schedule enabled', () => {
const { result } = renderHook(() => useStackMenuItems('web.yml', makeCtx({
isBusy: true,
menuVisibility: { showDeploy: true, showStop: true, showRestart: true, showUpdate: true },
})));
const lifecycle = result.current.find(g => g.id === 'lifecycle')!;
expect(lifecycle.items.every(i => i.disabled === true)).toBe(true);
const actionItems = lifecycle.items.filter(i => i.id !== 'schedule');
expect(actionItems.every(i => i.disabled === true)).toBe(true);
const scheduleItem = lifecycle.items.find(i => i.id === 'schedule')!;
expect(scheduleItem.disabled).toBeFalsy();
});
});