diff --git a/cypress/component/GenericHeader.cy.tsx b/cypress/component/GenericHeader.cy.tsx new file mode 100644 index 0000000..9e9312b --- /dev/null +++ b/cypress/component/GenericHeader.cy.tsx @@ -0,0 +1,218 @@ +import React from 'react'; +import GenericHeader from '../../src/Components/GenericHeader/GenericHeader'; +import { MemoryRouter } from 'react-router-dom'; +import { DashboardTemplate } from '../../src/api/dashboard-templates'; + +const mockDashboard: DashboardTemplate = { + id: 1, + createdAt: '2024-01-01', + updatedAt: '2024-01-01', + deletedAt: null, + userIdentityID: 1, + default: false, + templateBase: { name: 'test', displayName: 'Test' }, + templateConfig: { sm: [], md: [], lg: [], xl: [] }, + dashboardName: 'My Dashboard', +}; + +describe('GenericHeader', () => { + describe('without dashboard', () => { + it('renders "Add widgets" button', () => { + cy.mount( + + + + ); + cy.get('[data-ouia-component-id="add-widget-button"]').should('be.visible').and('contain.text', 'Add widgets'); + }); + + it('does not render edit name button when no dashboard', () => { + cy.mount( + + + + ); + cy.get('button[aria-label="Edit dashboard name"]').should('not.exist'); + }); + + it('does not render kebab dropdown when no dashboard', () => { + cy.mount( + + + + ); + cy.get('button[aria-label="kebab dropdown toggle"]').should('not.exist'); + }); + }); + + describe('with dashboard', () => { + const mountHeader = (onRenameDashboard = cy.stub().resolves()) => { + cy.mount( + + + + ); + }; + + it('renders the dashboard name', () => { + mountHeader(); + cy.contains('h1', 'My Dashboard').should('be.visible'); + }); + + it('renders "Add widgets" button', () => { + mountHeader(); + cy.get('[data-ouia-component-id="add-widget-button"]').should('be.visible'); + }); + + it('renders the edit name button', () => { + mountHeader(); + cy.get('button[aria-label="Edit dashboard name"]').should('be.visible'); + }); + + it('renders the kebab dropdown', () => { + mountHeader(); + cy.get('button[aria-label="kebab dropdown toggle"]').should('be.visible'); + }); + + describe('inline editing', () => { + it('enters edit mode when pencil icon is clicked', () => { + mountHeader(); + cy.get('button[aria-label="Edit dashboard name"]').click(); + cy.get('input[aria-label="Dashboard name"]').should('be.visible').and('have.value', 'My Dashboard'); + }); + + it('shows confirm and cancel buttons in edit mode', () => { + mountHeader(); + cy.get('button[aria-label="Edit dashboard name"]').click(); + cy.get('button[aria-label="Confirm name"]').should('be.visible'); + cy.get('button[aria-label="Cancel editing"]').should('be.visible'); + }); + + it('hides the dashboard name heading in edit mode', () => { + mountHeader(); + cy.get('button[aria-label="Edit dashboard name"]').click(); + cy.contains('h1', 'My Dashboard').should('not.exist'); + }); + + it('calls onRenameDashboard with new name on confirm click', () => { + const onRenameDashboard = cy.stub().as('onRenameDashboard').resolves(); + mountHeader(onRenameDashboard); + + cy.get('button[aria-label="Edit dashboard name"]').click(); + cy.get('input[aria-label="Dashboard name"]').clear().type('Renamed Dashboard'); + cy.get('button[aria-label="Confirm name"]').click(); + + cy.get('@onRenameDashboard').should('have.been.calledOnceWith', 'Renamed Dashboard'); + }); + + it('calls onRenameDashboard on Enter key', () => { + const onRenameDashboard = cy.stub().as('onRenameDashboard').resolves(); + mountHeader(onRenameDashboard); + + cy.get('button[aria-label="Edit dashboard name"]').click(); + cy.get('input[aria-label="Dashboard name"]').clear().type('Enter Dashboard{enter}'); + + cy.get('@onRenameDashboard').should('have.been.calledOnceWith', 'Enter Dashboard'); + }); + + it('cancels editing and restores original name on cancel click', () => { + mountHeader(); + + cy.get('button[aria-label="Edit dashboard name"]').click(); + cy.get('input[aria-label="Dashboard name"]').clear().type('Something else'); + cy.get('button[aria-label="Cancel editing"]').click(); + + cy.contains('h1', 'My Dashboard').should('be.visible'); + cy.get('input[aria-label="Dashboard name"]').should('not.exist'); + }); + + it('cancels editing on Escape key', () => { + mountHeader(); + + cy.get('button[aria-label="Edit dashboard name"]').click(); + cy.get('input[aria-label="Dashboard name"]').clear().type('Something else{esc}'); + + cy.contains('h1', 'My Dashboard').should('be.visible'); + cy.get('input[aria-label="Dashboard name"]').should('not.exist'); + }); + + it('does not call onRenameDashboard when name is empty', () => { + const onRenameDashboard = cy.stub().as('onRenameDashboard').resolves(); + mountHeader(onRenameDashboard); + + cy.get('button[aria-label="Edit dashboard name"]').click(); + cy.get('input[aria-label="Dashboard name"]').clear(); + cy.get('button[aria-label="Confirm name"]').click(); + + cy.get('@onRenameDashboard').should('not.have.been.called'); + }); + + it('trims whitespace from the new name', () => { + const onRenameDashboard = cy.stub().as('onRenameDashboard').resolves(); + mountHeader(onRenameDashboard); + + cy.get('button[aria-label="Edit dashboard name"]').click(); + cy.get('input[aria-label="Dashboard name"]').clear().type(' Trimmed Name '); + cy.get('button[aria-label="Confirm name"]').click(); + + cy.get('@onRenameDashboard').should('have.been.calledOnceWith', 'Trimmed Name'); + }); + + it('exits edit mode after successful rename', () => { + const onRenameDashboard = cy.stub().as('onRenameDashboard').resolves(); + mountHeader(onRenameDashboard); + + cy.get('button[aria-label="Edit dashboard name"]').click(); + cy.get('input[aria-label="Dashboard name"]').clear().type('New Name'); + cy.get('button[aria-label="Confirm name"]').click(); + + cy.get('input[aria-label="Dashboard name"]').should('not.exist'); + cy.get('button[aria-label="Edit dashboard name"]').should('be.visible'); + }); + }); + + describe('kebab dropdown', () => { + it('opens the dropdown menu on toggle click', () => { + mountHeader(); + cy.get('button[aria-label="kebab dropdown toggle"]').click(); + cy.get('[role="menu"]').should('be.visible'); + }); + + it('displays all menu items', () => { + mountHeader(); + cy.get('button[aria-label="kebab dropdown toggle"]').click(); + + cy.contains('[role="menuitem"]', 'Set as homepage').should('be.visible'); + cy.contains('[role="menuitem"]', 'Duplicate').should('be.visible'); + cy.contains('[role="menuitem"]', 'Copy configuration string').should('be.visible'); + cy.contains('[role="menuitem"]', 'Delete dashboard').should('be.visible'); + }); + + it('opens delete modal when Delete dashboard is clicked', () => { + mountHeader(); + cy.get('button[aria-label="kebab dropdown toggle"]').click(); + cy.contains('[role="menuitem"]', 'Delete dashboard').click(); + + cy.get('.pf-v6-c-modal-box').should('be.visible'); + }); + }); + }); + + describe('with default dashboard', () => { + const defaultDashboard: DashboardTemplate = { + ...mockDashboard, + default: true, + }; + + it('has Set as homepage item disabled when dashboard is default', () => { + cy.mount( + + + + ); + + cy.get('button[aria-label="kebab dropdown toggle"]').click(); + cy.contains('[role="menuitem"]', 'Set as homepage').should('have.attr', 'aria-disabled', 'true'); + }); + }); +}); diff --git a/playwright/set-as-homepage.spec.ts b/playwright/editing-dashboard.spec.ts similarity index 66% rename from playwright/set-as-homepage.spec.ts rename to playwright/editing-dashboard.spec.ts index 8919810..1b04171 100644 --- a/playwright/set-as-homepage.spec.ts +++ b/playwright/editing-dashboard.spec.ts @@ -128,3 +128,67 @@ test.describe('Set Dashboard as Homepage from Generic Page', () => { expect(await hasHomeIcon(page, defaultName)).toBe(false); }); }); + +test.describe('Inline Editing Dashboard Name', () => { + test.beforeEach(async ({ page }) => { + await disableCookiePrompt(page); + }); + + test('should rename a dashboard and see the new name on the generic page and in Dashboard Hub', async ({ page }) => { + await navigateToDashboardHub(page); + + const { nonDefaultName } = await findDashboardNames(page); + if (!nonDefaultName) { + test.skip(true, 'No non-default dashboard found to test with'); + return; + } + + const newName = `Renamed ${Date.now()}`; + + await navigateToGenericDashboard(page, nonDefaultName); + + await page.getByRole('button', { name: 'Edit dashboard name' }).click(); + const input = page.getByRole('textbox', { name: 'Dashboard name' }); + await expect(input).toBeVisible(); + await input.clear(); + await input.fill(newName); + await page.getByRole('button', { name: 'Confirm name' }).click(); + + await expect(input).not.toBeVisible({ timeout: 5000 }); + await expect(page.locator('h1').filter({ hasText: newName })).toBeVisible({ timeout: 5000 }); + + await navigateToDashboardHub(page); + await expect(page.getByRole('link', { name: newName })).toBeVisible({ timeout: 10000 }); + + // Restore the original name + await navigateToGenericDashboard(page, newName); + await page.getByRole('button', { name: 'Edit dashboard name' }).click(); + const restoreInput = page.getByRole('textbox', { name: 'Dashboard name' }); + await restoreInput.clear(); + await restoreInput.fill(nonDefaultName); + await page.getByRole('button', { name: 'Confirm name' }).click(); + await expect(restoreInput).not.toBeVisible({ timeout: 5000 }); + }); + + test('should cancel editing and keep the original name', async ({ page }) => { + await navigateToDashboardHub(page); + + const { nonDefaultName } = await findDashboardNames(page); + if (!nonDefaultName) { + test.skip(true, 'No non-default dashboard found to test with'); + return; + } + + await navigateToGenericDashboard(page, nonDefaultName); + + await page.getByRole('button', { name: 'Edit dashboard name' }).click(); + const input = page.getByRole('textbox', { name: 'Dashboard name' }); + await expect(input).toBeVisible(); + await input.clear(); + await input.fill('Should Not Be Saved'); + await page.getByRole('button', { name: 'Cancel editing' }).click(); + + await expect(input).not.toBeVisible({ timeout: 5000 }); + await expect(page.locator('h1').filter({ hasText: nonDefaultName })).toBeVisible({ timeout: 5000 }); + }); +}); diff --git a/src/Components/GenericHeader/GenericHeader.tsx b/src/Components/GenericHeader/GenericHeader.tsx index e26c9be..6faf3c6 100644 --- a/src/Components/GenericHeader/GenericHeader.tsx +++ b/src/Components/GenericHeader/GenericHeader.tsx @@ -1,9 +1,9 @@ import '../Header/Header.scss'; -import { ActionList, ActionListItem, Button } from '@patternfly/react-core'; +import { ActionList, ActionListItem, Button, Flex, FlexItem, TextInput } from '@patternfly/react-core'; import PageHeader from '@patternfly/react-component-groups/dist/dynamic/PageHeader'; -import React from 'react'; -import { PlusCircleIcon } from '@patternfly/react-icons'; +import React, { useState } from 'react'; +import { CheckIcon, PencilAltIcon, PlusCircleIcon, TimesIcon } from '@patternfly/react-icons'; import { useSetAtom } from 'jotai'; import { drawerExpandedAtom } from '../../state/drawerExpandedAtom'; import { DashboardTemplate } from '../../api/dashboard-templates'; @@ -11,14 +11,70 @@ import GenericHeaderDropdown from './GenericHeaderDropdown'; interface GenericHeaderProps { dashboard?: DashboardTemplate; + onRenameDashboard: (dashboardName: string) => Promise; } -const GenericHeader = ({ dashboard }: GenericHeaderProps) => { +const GenericHeader = ({ dashboard, onRenameDashboard }: GenericHeaderProps) => { const toggleOpen = useSetAtom(drawerExpandedAtom); + const [isEditing, setIsEditing] = useState(false); + const [editedName, setEditedName] = useState(dashboard?.dashboardName ?? ''); + + const handleConfirm = async () => { + if (editedName.trim() && dashboard) { + await onRenameDashboard(editedName.trim()); + } + setIsEditing(false); + }; + + const handleCancel = () => { + setEditedName(dashboard?.dashboardName ?? ''); + setIsEditing(false); + }; + + const titleContent = isEditing ? ( + + + setEditedName(value)} + onKeyDown={(event) => { + if (event.key === 'Enter') handleConfirm(); + if (event.key === 'Escape') handleCancel(); + }} + aria-label="Dashboard name" + autoFocus + /> + + +