Merge pull request #804 from PicPeak/fix/gallery-feedback-filter-chips
fix(gallery): show feedback filter chips on desktop for galleries without categories
This commit is contained in:
@@ -154,39 +154,45 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
|
||||
|
||||
{/* Category and Feedback Filters */}
|
||||
<div className="space-y-3">
|
||||
{/* Categories Row */}
|
||||
{categories && categories.length > 0 && (
|
||||
{/* Categories + desktop feedback row. Rendered whenever EITHER part
|
||||
has content: the desktop feedback chips must not depend on the
|
||||
(optional) categories existing, or category-less galleries show
|
||||
no feedback filter at all on desktop (#802 — the lg:hidden
|
||||
fallback block below only covers mobile/tablet). */}
|
||||
{((categories && categories.length > 0) || (feedbackEnabled && !!onFilterChange)) && (
|
||||
<div className="flex items-start lg:items-center justify-between flex-col lg:flex-row gap-3">
|
||||
{/* Categories: keep in a horizontal scroll container */}
|
||||
<div className="w-full overflow-x-auto pb-2 lg:pb-0">
|
||||
<div className="flex items-center gap-2 min-w-max">
|
||||
<Button
|
||||
variant={selectedCategoryId === null ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onCategoryChange(null)}
|
||||
leftIcon={<Grid className="w-3 h-3 md:w-4 md:h-4" />}
|
||||
className="text-xs md:text-sm whitespace-nowrap flex-shrink-0"
|
||||
>
|
||||
{showMediaFilter ? t('gallery.allMedia', 'All media') : t('gallery.allPhotos')} ({photos.length})
|
||||
</Button>
|
||||
{categories.map((category) => {
|
||||
const categoryPhotoCount = photos.filter(p => p.category_id === category.id).length;
|
||||
if (categoryPhotoCount === 0) return null;
|
||||
{categories && categories.length > 0 && (
|
||||
<div className="w-full overflow-x-auto pb-2 lg:pb-0">
|
||||
<div className="flex items-center gap-2 min-w-max">
|
||||
<Button
|
||||
variant={selectedCategoryId === null ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onCategoryChange(null)}
|
||||
leftIcon={<Grid className="w-3 h-3 md:w-4 md:h-4" />}
|
||||
className="text-xs md:text-sm whitespace-nowrap flex-shrink-0"
|
||||
>
|
||||
{showMediaFilter ? t('gallery.allMedia', 'All media') : t('gallery.allPhotos')} ({photos.length})
|
||||
</Button>
|
||||
{categories.map((category) => {
|
||||
const categoryPhotoCount = photos.filter(p => p.category_id === category.id).length;
|
||||
if (categoryPhotoCount === 0) return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={category.id}
|
||||
variant={selectedCategoryId === category.id ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onCategoryChange(category.id)}
|
||||
className="text-xs md:text-sm whitespace-nowrap flex-shrink-0"
|
||||
>
|
||||
{category.name} ({categoryPhotoCount})
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<Button
|
||||
key={category.id}
|
||||
variant={selectedCategoryId === category.id ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onCategoryChange(category.id)}
|
||||
className="text-xs md:text-sm whitespace-nowrap flex-shrink-0"
|
||||
>
|
||||
{category.name} ({categoryPhotoCount})
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Desktop: compact horizontal feedback filter with headline (icons only) */}
|
||||
{feedbackEnabled && onFilterChange && (
|
||||
@@ -244,7 +250,10 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs md:text-sm text-muted-theme flex-shrink-0 ml-auto">
|
||||
{/* Without categories this row only carries desktop content (the
|
||||
chips are lg-only; mobile has its own block below), so hide
|
||||
the count below lg to keep the mobile layout unchanged. */}
|
||||
<p className={`text-xs md:text-sm text-muted-theme flex-shrink-0 ml-auto ${categories && categories.length > 0 ? '' : 'hidden lg:block'}`}>
|
||||
{photoCount} {t('common.media', 'media')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Regression coverage for #802: the desktop feedback-filter chips
|
||||
* (All / Likes / Saved / Rated / Commented) were nested inside the
|
||||
* categories row, so a gallery WITHOUT photo categories (the default)
|
||||
* rendered no feedback filter at all on desktop — the standalone
|
||||
* fallback block is lg:hidden (mobile/tablet only). These tests pin
|
||||
* that both chip groups exist in the DOM regardless of categories.
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { PhotoFilterBar } from '../PhotoFilterBar';
|
||||
|
||||
vi.mock('react-i18next', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next');
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (_key: string, fallback?: unknown) =>
|
||||
typeof fallback === 'string' ? fallback : _key,
|
||||
i18n: { language: 'en' }
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
const baseProps = {
|
||||
categories: [] as Array<{ id: number; name: string; slug: string }>,
|
||||
photos: [] as never[],
|
||||
selectedCategoryId: null,
|
||||
onCategoryChange: vi.fn(),
|
||||
searchTerm: '',
|
||||
onSearchChange: vi.fn(),
|
||||
sortBy: 'date' as const,
|
||||
onSortChange: vi.fn(),
|
||||
photoCount: 0,
|
||||
};
|
||||
|
||||
describe('PhotoFilterBar feedback chips (#802)', () => {
|
||||
it('renders both chip groups (desktop lg:flex + mobile lg:hidden) with NO categories', () => {
|
||||
render(
|
||||
<PhotoFilterBar
|
||||
{...baseProps}
|
||||
feedbackEnabled
|
||||
currentFilter="all"
|
||||
onFilterChange={vi.fn()}
|
||||
/>
|
||||
);
|
||||
// Two groups: the desktop row variant and the mobile fallback. Before
|
||||
// the fix, only the mobile one rendered when categories were empty,
|
||||
// leaving desktop with no feedback filter at all.
|
||||
expect(screen.getAllByText('Feedback Filter')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('still renders both chip groups when categories exist', () => {
|
||||
render(
|
||||
<PhotoFilterBar
|
||||
{...baseProps}
|
||||
categories={[{ id: 1, name: 'Ceremony', slug: 'ceremony' }]}
|
||||
photos={[{ id: 1, category_id: 1 } as never]}
|
||||
feedbackEnabled
|
||||
currentFilter="all"
|
||||
onFilterChange={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getAllByText('Feedback Filter')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('renders no chips when feedback is disabled', () => {
|
||||
render(<PhotoFilterBar {...baseProps} />);
|
||||
expect(screen.queryByText('Feedback Filter')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user