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
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,28 @@ import userEvent from '@testing-library/user-event';
import { vi } from 'vitest';
import { PropertyEditorTab } from './PropertyEditorTab';
import { usePropertyEditor } from '~/features/property-editor/hooks/usePropertyEditor';
import { useSchedulerStore } from '~/stores/schedulerStore';
import type { QueueInfo, PropertyDescriptor } from '~/types';

// Mock the hooks
vi.mock('~/features/property-editor/hooks/usePropertyEditor');

// Mock the scheduler store
vi.mock('~/stores/schedulerStore', () => ({
useSchedulerStore: vi.fn((selector: any) => {
const state = {
getGlobalPropertyValue: vi.fn().mockReturnValue({ value: '' }),
getQueuePropertyValue: vi.fn().mockReturnValue({ value: '' }),
stagedChanges: [],
configData: new Map(),
schedulerData: null,
hasPendingDeletion: vi.fn().mockReturnValue(false),
revertQueueDeletion: vi.fn(),
};
return selector ? selector(state) : state;
}),
}));

// Mock toast
vi.mock('sonner', () => ({
toast: {
Expand Down Expand Up @@ -161,6 +178,28 @@ describe('PropertyEditorTab', () => {
expect(within(capacityTrigger).getByText('1')).toBeInTheDocument();
});

describe('pending deletion state', () => {
it('should not show deletion banner in PropertyEditorTab (banner is in PropertyPanel)', () => {
vi.mocked(useSchedulerStore).mockImplementation((selector: any) => {
const state = {
getGlobalPropertyValue: vi.fn().mockReturnValue({ value: '' }),
getQueuePropertyValue: vi.fn().mockReturnValue({ value: '' }),
stagedChanges: [],
configData: new Map(),
schedulerData: null,
hasPendingDeletion: vi.fn().mockReturnValue(true),
revertQueueDeletion: vi.fn(),
};
return selector ? selector(state) : state;
});

render(<PropertyEditorTab queue={mockQueue} />);

expect(screen.queryByText(/marked for deletion/i)).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /undo delete/i })).not.toBeInTheDocument();
});
});

it('renders template configuration button when controls allow management', async () => {
const templateProperty = {
name: 'auto-queue-creation-v2.enabled',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import {
AccordionItem,
AccordionTrigger,
} from '~/components/ui/accordion';
import { Alert, AlertDescription } from '~/components/ui/alert';
import { AlertTriangle } from 'lucide-react';
import { usePropertyEditor } from '~/features/property-editor/hooks/usePropertyEditor';
import { PropertyFormField } from './PropertyFormField';
import type { QueueInfo } from '~/types';
Expand Down Expand Up @@ -364,18 +362,6 @@ export const PropertyEditorTab = ({
return (
<Form {...form}>
<div className="flex flex-col h-full">
{/* Warning banner for queues pending deletion */}
{isPendingDeletion && (
<div className="p-4 pb-0">
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
This queue is pending deletion and will be removed when changes are applied.
</AlertDescription>
</Alert>
</div>
)}

{/* Loading State */}
{isFormInitializing && (
<div className="flex justify-center items-center min-h-[200px] p-4">
Expand Down Expand Up @@ -436,7 +422,7 @@ export const PropertyEditorTab = ({
property={prop}
control={control}
stagedStatus={getStagedStatus(prop.originalName || prop.name)}
isEnabled={propertyState?.enabled ?? true}
isEnabled={(propertyState?.enabled ?? true) && !isPendingDeletion}
onBlur={handleFieldBlur}
errors={getFieldErrors(prop.formFieldName || prop.name)}
warnings={getFieldWarnings(prop.formFieldName || prop.name)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import userEvent from '@testing-library/user-event';
import { useSchedulerStore } from '~/stores/schedulerStore';
import type { SchedulerStore } from '~/stores/schedulerStore';
import type { QueueInfo } from '~/types';
import { SPECIAL_VALUES } from '~/types/constants/special-values';
import { toast } from 'sonner';

// Test helper
Expand Down Expand Up @@ -147,6 +148,8 @@ function getBaseStoreState(): Partial<SchedulerStore> {
stagedChanges: [],
configData: new Map<string, string>(),
schedulerData: null,
hasPendingDeletion: vi.fn().mockReturnValue(false),
revertQueueDeletion: vi.fn(),
shouldOpenTemplateConfig: false,
requestTemplateConfigOpen: () => {
storeState = { ...storeState, shouldOpenTemplateConfig: true };
Expand Down Expand Up @@ -542,4 +545,59 @@ describe('PropertyPanel', () => {
// Would need to set isSubmitting state through the PropertyEditorTab callbacks
// This is tested through integration tests
});

describe('pending deletion state', () => {
it('should hide Stage Changes and Reset buttons when queue is pending deletion', async () => {
const user = userEvent.setup();
setStoreState({
selectedQueuePath: 'root.default',
isPropertyPanelOpen: true,
stagedChanges: [
{
id: 'removal-1',
queuePath: 'root.default',
property: SPECIAL_VALUES.QUEUE_MARKER,
type: 'remove',
oldValue: 'exists',
newValue: undefined,
},
] as any,
revertQueueDeletion: vi.fn(),
});
mockGetQueueByPath.mockReturnValue(mockQueue);

render(<PropertyPanel />);

// Switch to settings tab to verify buttons are hidden
const settingsTab = screen.getByRole('tab', { name: /settings/i });
await user.click(settingsTab);

expect(screen.queryByText('Stage Changes')).not.toBeInTheDocument();
expect(screen.queryByText('Reset')).not.toBeInTheDocument();
});

it('should show deletion badge and undo button when queue is pending deletion', () => {
setStoreState({
selectedQueuePath: 'root.default',
isPropertyPanelOpen: true,
stagedChanges: [
{
id: 'removal-1',
queuePath: 'root.default',
property: SPECIAL_VALUES.QUEUE_MARKER,
type: 'remove',
oldValue: 'exists',
newValue: undefined,
},
] as any,
revertQueueDeletion: vi.fn(),
});
mockGetQueueByPath.mockReturnValue(mockQueue);

render(<PropertyPanel />);

expect(screen.getByText(/marked for deletion/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /undo/i })).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@


import React, { useReducer, useState, useEffect, useRef } from 'react';
import { Save, RotateCcw, GitBranch, Info, Settings, Edit, AlertTriangle } from 'lucide-react';
import { Save, RotateCcw, GitBranch, Info, Settings, Edit, AlertTriangle, Undo2, Trash2 } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
import { useSchedulerStore } from '~/stores/schedulerStore';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '~/components/ui/sheet';
Expand Down Expand Up @@ -110,6 +110,7 @@ export const PropertyPanel: React.FC = () => {
const selectQueue = useSchedulerStore((s) => s.selectQueue);
const clearTemplateConfigRequest = useSchedulerStore((s) => s.clearTemplateConfigRequest);
const getQueuePropertyValue = useSchedulerStore((s) => s.getQueuePropertyValue);
const revertQueueDeletion = useSchedulerStore((s) => s.revertQueueDeletion);

const [formState, dispatch] = useReducer(formReducer, INITIAL_FORM_STATE);
const { hasChanges, isFormDirty } = formState;
Expand Down Expand Up @@ -282,6 +283,10 @@ export const PropertyPanel: React.FC = () => {
}
}, [isPropertyPanelOpen]);

const isPendingDeletion = selectedQueuePath
? stagedChanges.some((c) => c.queuePath === selectedQueuePath && c.type === 'remove')
: false;

const queuePath = selectedQueue?.queuePath;

const queueIssues = !queuePath ? {} : (validationState[queuePath] ?? {});
Expand All @@ -308,7 +313,7 @@ export const PropertyPanel: React.FC = () => {

// Keyboard shortcuts - only active when panel is open and on settings tab
useKeyboardShortcuts(
isPanelVisible && tabValue === 'settings'
isPanelVisible && tabValue === 'settings' && !isPendingDeletion
? [
{
key: 's',
Expand Down Expand Up @@ -366,13 +371,34 @@ export const PropertyPanel: React.FC = () => {
issues={issueList}
onIssueSelect={handleIssueSelect}
/>
{isFormDirty && (
{isPendingDeletion && (
<>
<Badge
variant="outline"
className="text-xs border-amber-500/50 bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-400 dark:border-amber-500/30"
>
<Trash2 className="h-3 w-3 mr-1" />
Marked for deletion
</Badge>
<Button
type="button"
variant="ghost"
size="sm"
className="h-5 px-1.5 text-xs text-muted-foreground hover:text-foreground"
onClick={() => revertQueueDeletion(selectedQueue.queuePath)}
>
<Undo2 className="h-3 w-3 mr-0.5" />
Undo
</Button>
</>
)}
{!isPendingDeletion && isFormDirty && (
<Badge variant="outline" className="text-xs">
<Edit className="h-3 w-3 mr-1" />
Unsaved
</Badge>
)}
{!isFormDirty && hasChanges && (
{!isPendingDeletion && !isFormDirty && hasChanges && (
<Badge variant="default" className="text-xs">
<Edit className="h-3 w-3 mr-1" />
Staged
Expand Down Expand Up @@ -444,7 +470,7 @@ export const PropertyPanel: React.FC = () => {
</Tabs>

{/* Fixed Apply/Reset buttons - show on Settings tab */}
{tabValue === 'settings' && (
{tabValue === 'settings' && !isPendingDeletion && (
<div className="sticky bottom-0 left-0 right-0 mt-auto p-4 bg-background border-t flex gap-2 justify-end">
<Button
variant="outline"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
ContextMenuTrigger,
} from '~/components/ui/context-menu';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '~/components/ui/tooltip';
import { Plus, Trash2, Edit, Play, Pause, SlidersHorizontal, FileCog } from 'lucide-react';
import { Plus, Trash2, Edit, Play, Pause, SlidersHorizontal, FileCog, Undo2 } from 'lucide-react';
import { QUEUE_STATES, SPECIAL_VALUES } from '~/types';

type StagedStatus = 'new' | 'modified' | 'deleted';
Expand All @@ -55,6 +55,7 @@ interface QueueCardContextMenuProps {
onToggleState: () => void;
onAddChild: (event: React.MouseEvent) => void;
onDelete: (event: React.MouseEvent) => void;
onUndoDelete: () => void;
onRemoveStaged: (event: React.MouseEvent) => void;
onOpenChange: (open: boolean) => void;
}
Expand All @@ -77,6 +78,7 @@ export const QueueCardContextMenu: React.FC<QueueCardContextMenuProps> = ({
onToggleState,
onAddChild,
onDelete,
onUndoDelete,
onRemoveStaged,
onOpenChange,
}) => {
Expand All @@ -99,7 +101,7 @@ export const QueueCardContextMenu: React.FC<QueueCardContextMenuProps> = ({
e.stopPropagation();
onEditProperties(e);
}}
disabled={stagedStatus === 'new' || isAutoCreatedQueue}
disabled={stagedStatus === 'new' || isAutoCreatedQueue || hasPendingDeletion}
>
<Edit className="mr-2 h-4 w-4" />
Edit Properties
Expand All @@ -111,15 +113,15 @@ export const QueueCardContextMenu: React.FC<QueueCardContextMenuProps> = ({
e.stopPropagation();
onManageTemplate(e);
}}
disabled={isTemplateActionDisabled}
disabled={isTemplateActionDisabled || hasPendingDeletion}
>
<FileCog className="mr-2 h-4 w-4" />
Manage Template Properties
</ContextMenuItem>
)}

{!isRoot && (
<ContextMenuItem onClick={(e) => onEditCapacity(e)}>
<ContextMenuItem onClick={(e) => onEditCapacity(e)} disabled={hasPendingDeletion}>
<SlidersHorizontal className="mr-2 h-4 w-4" />
Edit Capacity
</ContextMenuItem>
Expand All @@ -137,7 +139,7 @@ export const QueueCardContextMenu: React.FC<QueueCardContextMenuProps> = ({
e.stopPropagation();
onToggleState();
}}
disabled={!canToggleState}
disabled={!canToggleState || hasPendingDeletion}
>
{isRunning ? (
<>
Expand Down Expand Up @@ -176,7 +178,7 @@ export const QueueCardContextMenu: React.FC<QueueCardContextMenuProps> = ({
</TooltipProvider>
)}

{canDelete && stagedStatus !== 'new' && (
{canDelete && stagedStatus !== 'new' && !hasPendingDeletion && (
<>
<ContextMenuSeparator />
<ContextMenuItem
Expand All @@ -187,7 +189,22 @@ export const QueueCardContextMenu: React.FC<QueueCardContextMenuProps> = ({
className="text-red-600 focus:text-red-600"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete Queue
Mark for Deletion
</ContextMenuItem>
</>
)}

{hasPendingDeletion && (
<>
<ContextMenuSeparator />
<ContextMenuItem
onClick={(e) => {
e.stopPropagation();
onUndoDelete();
}}
>
<Undo2 className="mr-2 h-4 w-4" />
Undo Delete
</ContextMenuItem>
</>
)}
Expand Down
Loading
Loading