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
Binary file added .tmp/proof/issue-130-empty-filters.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .tmp/proof/issue-130-mobile-filters.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .tmp/proof/issue-130-restored-filters.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 69 additions & 3 deletions packages/app/src/components/showcase/ShowcasePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,66 @@ import { ShowcasePage } from './ShowcasePage';
import { showcaseItems, countByCategory } from './showcaseConfig';

describe('ShowcasePage', () => {
it('labels search and exposes the active category and result count', async () => {
await renderInTestApp(<ShowcasePage />);

expect(
screen.getByRole('textbox', { name: 'Search showcase' }),
).toBeInTheDocument();
const categories = within(
screen.getByRole('group', { name: 'Showcase categories' }),
);
expect(
categories.getByRole('button', { name: /^All \(/, pressed: true }),
).toBeInTheDocument();

fireEvent.click(categories.getByRole('button', { name: /^GitHub \(/ }));

expect(
categories.getByRole('button', { name: /^GitHub \(/, pressed: true }),
).toBeInTheDocument();
expect(
categories.getByRole('button', { name: /^All \(/, pressed: false }),
).toBeInTheDocument();
expect(screen.getByRole('status')).toHaveTextContent('3 items available');
});

it('combines search and category filters and restores all cards from an empty result', async () => {
await renderInTestApp(<ShowcasePage />);
const search = screen.getByRole('textbox', { name: 'Search showcase' });

fireEvent.change(search, { target: { value: 'mcp' } });
expect(screen.getByRole('status')).toHaveTextContent('1 item available');
expect(
screen.getByTestId('showcase-card-template-mcp-server'),
).toBeInTheDocument();

fireEvent.click(screen.getByRole('button', { name: /^GitHub \(/ }));

expect(screen.getByRole('status')).toHaveTextContent('0 items available');
expect(screen.getByText(/No showcase items found/)).toHaveTextContent(
'in GitHub',
);
expect(screen.queryAllByTestId(/^showcase-card-/)).toHaveLength(0);

fireEvent.click(screen.getByRole('button', { name: 'Clear filters' }));

expect(search).toHaveValue('');
expect(search).toHaveFocus();
expect(
screen.getByRole('button', { name: /^All \(/, pressed: true }),
).toBeInTheDocument();
expect(screen.getByRole('status')).toHaveTextContent(
`${showcaseItems.length} items available`,
);
expect(screen.getAllByTestId(/^showcase-card-/)).toHaveLength(
showcaseItems.length,
);
expect(
screen.queryByRole('button', { name: 'Clear filters' }),
).not.toBeInTheDocument();
});

it('distinguishes live keyword search from the answer-engine extension', async () => {
await renderInTestApp(<ShowcasePage />);

Expand All @@ -28,7 +88,9 @@ describe('ShowcasePage', () => {
expect(
extension.getByText(/your own document index and AI Hub/),
).toBeInTheDocument();
expect(extension.getByRole('link', { name: 'Use template' })).toHaveAttribute(
expect(
extension.getByRole('link', { name: 'Use template' }),
).toHaveAttribute(
'href',
'/self-service/templates/default/answer-engine-worker',
);
Expand Down Expand Up @@ -71,7 +133,9 @@ describe('ShowcasePage', () => {
).not.toBeNull();
});
expect(
document.querySelector('[data-testid="showcase-card-template-mcp-server"]'),
document.querySelector(
'[data-testid="showcase-card-template-mcp-server"]',
),
).toBeNull();
});

Expand All @@ -83,7 +147,9 @@ describe('ShowcasePage', () => {
});

expect(
document.querySelector('[data-testid="showcase-card-template-mcp-server"]'),
document.querySelector(
'[data-testid="showcase-card-template-mcp-server"]',
),
).not.toBeNull();
expect(
document.querySelector(
Expand Down
33 changes: 27 additions & 6 deletions packages/app/src/components/showcase/ShowcasePage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useMemo, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import TextField from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment';
import Chip from '@mui/material/Chip';
import Grid from '@mui/material/Grid';
import Alert from '@mui/material/Alert';
import Button from '@mui/material/Button';
import { Page, Header, Content } from '@backstage/core-components';
import SearchIcon from '@mui/icons-material/Search';
import {
Expand All @@ -25,6 +26,7 @@ const categories: ShowcaseCategory[] = [
];

export const ShowcasePage = () => {
const searchInputRef = useRef<HTMLInputElement>(null);
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState<
ShowcaseCategory | 'all'
Expand Down Expand Up @@ -57,8 +59,8 @@ export const ShowcasePage = () => {
<Alert severity="info" sx={{ mb: 3 }}>
Catalog and TechDocs keyword search runs in this portal. Generated
answers are an extension you can build with the Answer Engine Worker
template, your own document index and AI Hub. The demo does not generate
answers.
template, your own document index and AI Hub. The demo does not
generate answers.
</Alert>

<Box
Expand All @@ -71,11 +73,13 @@ export const ShowcasePage = () => {
}}
>
<TextField
label="Search showcase"
placeholder="Search showcase..."
inputRef={searchInputRef}
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
size="small"
sx={{ minWidth: 300, maxWidth: 400 }}
sx={{ width: '100%', maxWidth: 400 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
Expand All @@ -84,14 +88,19 @@ export const ShowcasePage = () => {
),
}}
/>
<Typography variant="body2" color="text.secondary">
<Typography role="status" variant="body2" color="text.secondary">
{filtered.length} item{filtered.length !== 1 ? 's' : ''} available
</Typography>
</Box>

<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', mb: 4 }}>
<Box
role="group"
aria-label="Showcase categories"
sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', mb: 4 }}
>
<Chip
label={`All (${counts.all})`}
aria-pressed={selectedCategory === 'all'}
onClick={() => setSelectedCategory('all')}
variant={selectedCategory === 'all' ? 'filled' : 'outlined'}
color={selectedCategory === 'all' ? 'primary' : 'default'}
Expand All @@ -101,6 +110,7 @@ export const ShowcasePage = () => {
<Chip
key={category}
label={`${showcaseCategoryLabels[category]} (${counts[category]})`}
aria-pressed={selectedCategory === category}
onClick={() => setSelectedCategory(category)}
variant={selectedCategory === category ? 'filled' : 'outlined'}
color={selectedCategory === category ? 'primary' : 'default'}
Expand All @@ -126,6 +136,17 @@ export const ShowcasePage = () => {
{selectedCategory !== 'all' &&
` in ${showcaseCategoryLabels[selectedCategory]}`}
</Typography>
<Button
sx={{ mt: 2 }}
variant="outlined"
onClick={() => {
setSearchTerm('');
setSelectedCategory('all');
searchInputRef.current?.focus();
}}
>
Clear filters
</Button>
</Box>
)}
</Content>
Expand Down
Loading