Skip to content
Open
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
86 changes: 86 additions & 0 deletions e2e/path-delete.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { test, expect } from '@playwright/test';

const EDITOR_LOCATOR = '.pf-v6-c-page';

test.describe('Path deletion', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await expect(page.locator(EDITOR_LOCATOR)).toBeVisible({ timeout: 15000 });
});

test('trash icon appears next to path items', async ({ page }) => {
// The default Pet Store API has paths - verify trash icons are present
const trashButton = page.getByRole('button', { name: /^Delete path / });
await expect(trashButton.first()).toBeVisible();
});

test('clicking trash icon opens confirmation dialog', async ({ page }) => {
// Click the trash icon on the first path
const trashButton = page.getByRole('button', { name: /^Delete path / }).first();
await trashButton.click();

// Verify the confirmation modal appears
await expect(page.getByText('Delete Path')).toBeVisible();
await expect(page.getByText('Are you sure you want to delete the path')).toBeVisible();
});

test('cancel button closes confirmation dialog without deleting', async ({ page }) => {
// Count initial paths
const initialTrashButtons = page.getByRole('button', { name: /^Delete path / });
const initialCount = await initialTrashButtons.count();

// Click trash on the first path
await initialTrashButtons.first().click();

// Verify dialog is open
await expect(page.getByText('Are you sure you want to delete the path')).toBeVisible();

// Click Cancel
await page.getByRole('button', { name: 'Cancel' }).click();

// Verify dialog is closed
await expect(page.getByText('Are you sure you want to delete the path')).not.toBeVisible();

// Verify path count is unchanged
await expect(page.getByRole('button', { name: /^Delete path / })).toHaveCount(initialCount);
});

test('create a path then delete it via trash icon', async ({ page }) => {
const testPath = '/test-delete-path';

// Click the add path button
await page.getByRole('button', { name: 'Add path' }).click();

// Fill in the path name and create it
await page.getByPlaceholder('/pets').fill(testPath);
await page.getByRole('button', { name: 'Create' }).click();

// Verify the new path appears in the navigation
await expect(page.getByText(testPath)).toBeVisible();

// Find and click the trash icon for our test path
const trashButton = page.getByRole('button', { name: `Delete path ${testPath}` });
await expect(trashButton).toBeVisible();
await trashButton.click();

// Verify the confirmation dialog shows the correct path name
await expect(page.getByText('Are you sure you want to delete the path')).toBeVisible();
const modalBody = page.locator('.pf-v6-c-modal-box__body');
await expect(modalBody).toContainText(testPath);

// Click Delete to confirm
await page.getByRole('button', { name: 'Delete' }).click();

// Verify the dialog is closed
await expect(page.getByText('Are you sure you want to delete the path')).not.toBeVisible();

// Verify the path is gone from the navigation
await expect(page.getByRole('button', { name: `Delete path ${testPath}` })).not.toBeVisible();
});

test('schemas section does not have trash icons', async ({ page }) => {
// Schemas should not have delete buttons (only paths do)
const schemaTrashButton = page.getByRole('button', { name: /^Delete schema / });
await expect(schemaTrashButton).toHaveCount(0);
});
});
29 changes: 29 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
],
webServer: {
command: 'npm run test-app:dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
});
30 changes: 28 additions & 2 deletions src/components/editor/NavigationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
* Navigation panel for the master section
*/

import React, { useState, useMemo } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import {
Nav,
NavList,
NavItem,
Divider,
SearchInput
SearchInput,
Modal,
ModalVariant,
ModalHeader,
ModalBody,
ModalFooter,
Button,
} from '@patternfly/react-core';
import { useDocument } from '@hooks/useDocument';
import { useSelection } from '@hooks/useSelection';
Expand Down Expand Up @@ -41,6 +47,8 @@ export const NavigationPanel: React.FC = () => {
const [isCreatePathModalOpen, setIsCreatePathModalOpen] = useState(false);
const [isCreateSchemaModalOpen, setIsCreateSchemaModalOpen] = useState(false);
const [filterText, setFilterText] = useState('');
const [deleteConfirmPath, setDeleteConfirmPath] = useState<string | null>(null);
const handleDeleteRequest = useCallback((path: string) => setDeleteConfirmPath(path), []);

/**
* Get list of paths from the document
Expand Down Expand Up @@ -245,6 +253,7 @@ export const NavigationPanel: React.FC = () => {
isItemActive={(path) => navigationObjectType === "pathItem" && navigationObject?.mapPropertyName() === path}
nodePath="/paths"
isTooltipEnabled={true}
onDeleteItem={handleDeleteRequest}
/>

<Divider />
Expand Down Expand Up @@ -277,6 +286,23 @@ export const NavigationPanel: React.FC = () => {
onClose={() => setIsCreateSchemaModalOpen(false)}
onConfirm={handleCreateSchema}
/>

{/* Delete Path Confirmation Modal */}
<Modal
variant={ModalVariant.small}
isOpen={deleteConfirmPath !== null}
onClose={() => setDeleteConfirmPath(null)}
aria-labelledby="delete-path-modal-title"
>
<ModalHeader title="Delete Path" labelId="delete-path-modal-title" />
<ModalBody>
Are you sure you want to delete the path <strong>{deleteConfirmPath}</strong>?
</ModalBody>
<ModalFooter>
<Button variant="link" onClick={() => setDeleteConfirmPath(null)}>Cancel</Button>
<Button variant="danger" onClick={() => { if (deleteConfirmPath) handleDeletePath(deleteConfirmPath); setDeleteConfirmPath(null); }}>Delete</Button>
</ModalFooter>
</Modal>
</>
);
};
42 changes: 33 additions & 9 deletions src/components/editor/NavigationPanelSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
MenuList,
MenuItem, Tooltip,
} from '@patternfly/react-core';
import { PlusCircleIcon } from '@patternfly/react-icons';
import { PlusCircleIcon, TrashIcon } from '@patternfly/react-icons';
import { ExpandablePanel } from '@components/common/ExpandablePanel';
import {PathLabel} from "@components/common/PathLabel.tsx";

Expand Down Expand Up @@ -78,6 +78,11 @@ export interface NavigationPanelSectionProps {
* Optional node path for the section (for selection support)
*/
nodePath?: string;

/**
* Optional handler for deleting an item (renders a trash icon when provided)
*/
onDeleteItem?: (itemName: string) => void;
}

/**
Expand All @@ -95,6 +100,7 @@ export const NavigationPanelSection: React.FC<NavigationPanelSectionProps> = ({
isTooltipEnabled,
isItemActive,
nodePath,
onDeleteItem,
}) => {
// Manage expanded/collapsed state internally
const [isExpanded, setIsExpanded] = useState(true);
Expand Down Expand Up @@ -197,15 +203,33 @@ export const NavigationPanelSection: React.FC<NavigationPanelSectionProps> = ({
onClick={() => onItemClick(itemName)}
style={hasContextMenu ? { backgroundColor: 'var(--pf-v6-global--BackgroundColor--200)' } : undefined}
>
{isTooltipEnabled ? (
<a className={hasContextMenu ? "pf-contextMenu" : undefined} onContextMenu={(e) => handleContextMenu(e, itemName)} style={{width: "100%", overflowX: "hidden", textWrap: "nowrap"}}>
<Tooltip content={<div>{itemName}</div>}>
<PathLabel path={itemName} />
<div style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<div style={{ flex: 1, overflow: 'hidden', textWrap: 'nowrap' }}>
{isTooltipEnabled ? (
<a className={hasContextMenu ? "pf-contextMenu" : undefined} onContextMenu={(e) => handleContextMenu(e, itemName)} style={{width: "100%", overflowX: "hidden", textWrap: "nowrap"}}>
<Tooltip content={<div>{itemName}</div>}>
<PathLabel path={itemName} />
</Tooltip>
</a>
) : (
<a className={hasContextMenu ? "pf-contextMenu" : undefined} onContextMenu={(e) => handleContextMenu(e, itemName)}>{itemName}</a>
)}
</div>
{onDeleteItem && (
<Tooltip content="Delete">
<Button
variant="plain"
aria-label={`Delete ${itemType} ${itemName}`}
onClick={(e) => {
e.stopPropagation();
onDeleteItem(itemName);
}}
icon={<TrashIcon />}
style={{ minWidth: 'auto', padding: '2px' }}
/>
</Tooltip>
</a>
) : (
<a className={hasContextMenu ? "pf-contextMenu" : undefined} onContextMenu={(e) => handleContextMenu(e, itemName)}>{itemName}</a>
)}
)}
</div>
</NavItem>
);
})
Expand Down