feat: tag filtering functionality for test cases (#324)

This commit is contained in:
Eliezer Castro
2025-11-08 10:30:50 -03:00
committed by GitHub
parent 0575c928e8
commit 12b5a1babe
10 changed files with 150 additions and 25 deletions

View File

@@ -37,6 +37,7 @@ export default function CasesPane({
const [titleFilter, setTitleFilter] = useState('');
const [priorityFilter, setPriorityFilter] = useState<number[]>([]);
const [typeFilter, setTypeFilter] = useState<number[]>([]);
const [tagFilter, setTagFilter] = useState<number[]>([]);
const [isDeleteConfirmDialogOpen, setIsDeleteConfirmDialogOpen] = useState(false);
const [deleteCaseIds, setDeleteCaseIds] = useState<number[]>([]);
@@ -44,7 +45,7 @@ export default function CasesPane({
const router = useRouter();
const searchParams = useSearchParams();
const updateUrlParams = (updates: { title?: string; priority?: number[]; type?: number[] }) => {
const updateUrlParams = (updates: { title?: string; priority?: number[]; type?: number[]; tag?: number[] }) => {
const currentParams = new URLSearchParams(searchParams.toString());
if (updates.title) {
@@ -65,6 +66,12 @@ export default function CasesPane({
currentParams.delete('type');
}
if (updates.tag && updates.tag.length > 0) {
currentParams.set('tag', updates.tag.join(','));
} else {
currentParams.delete('tag');
}
const newUrl = `${window.location.pathname}?${currentParams.toString()}`;
router.push(newUrl, { scroll: false });
};
@@ -76,10 +83,12 @@ export default function CasesPane({
const titleParam = searchParams.get('title') || '';
const priorityParam = parseQueryParam(searchParams.get('priority'));
const typeParam = parseQueryParam(searchParams.get('type'));
const tagParam = parseQueryParam(searchParams.get('tag'));
setTitleFilter(titleParam);
setPriorityFilter(priorityParam);
setTypeFilter(typeParam);
setTagFilter(tagParam);
try {
const data = await fetchCases(
@@ -87,7 +96,8 @@ export default function CasesPane({
Number(folderId),
titleParam || undefined,
priorityParam.length > 0 ? priorityParam : undefined,
typeParam.length > 0 ? typeParam : undefined
typeParam.length > 0 ? typeParam : undefined,
tagParam.length > 0 ? tagParam : undefined
);
setCases(data);
} catch (error: unknown) {
@@ -133,11 +143,12 @@ export default function CasesPane({
await exportCases(context.token.access_token, Number(folderId), type);
};
const handleFilterChange = (title: string, priorities: number[], types: number[]) => {
const handleFilterChange = (title: string, priorities: number[], types: number[], tag: number[]) => {
setTitleFilter(title);
setPriorityFilter(priorities);
setTypeFilter(types);
updateUrlParams({ title: title, priority: priorities, type: types });
setTagFilter(tag);
updateUrlParams({ title: title, priority: priorities, type: types, tag: tag });
};
// **************************************************************************
@@ -178,6 +189,7 @@ export default function CasesPane({
activeTitleFilter={titleFilter}
activePriorityFilters={priorityFilter}
activeTypeFilters={typeFilter}
activeTagFilters={tagFilter}
messages={messages}
priorityMessages={priorityMessages}
testTypeMessages={testTypeMessages}

View File

@@ -1,10 +1,23 @@
import { useState, useEffect } from 'react';
import { Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Selection, Input } from '@heroui/react';
import { useState, useEffect, useContext } from 'react';
import {
Button,
Dropdown,
DropdownTrigger,
DropdownMenu,
DropdownItem,
Selection,
Input,
addToast,
} from '@heroui/react';
import { SearchIcon, ChevronDown, Circle } from 'lucide-react';
import { PriorityMessages } from '@/types/priority';
import { TestTypeMessages } from '@/types/testType';
import { CasesMessages } from '@/types/case';
import { priorities, testTypes } from '@/config/selection';
import { TagType } from '@/types/tag';
import { fetchTags } from '@/utils/tagsControls';
import { TokenContext } from '@/utils/TokenProvider';
import { logError } from '@/utils/errorHandler';
type TestCaseFilterProps = {
messages: CasesMessages;
@@ -13,9 +26,13 @@ type TestCaseFilterProps = {
activeTitleFilter: string;
activePriorityFilters: number[];
activeTypeFilters: number[];
onFilterChange: (title: string, priorities: number[], types: number[]) => void;
activeTagFilters: number[];
projectId: string;
onFilterChange: (title: string, priorities: number[], types: number[], tags: number[]) => void;
};
type Tag = Pick<TagType, 'id' | 'name'>;
export default function TestCaseFilter({
messages,
priorityMessages,
@@ -23,11 +40,38 @@ export default function TestCaseFilter({
activeTitleFilter,
activePriorityFilters,
activeTypeFilters,
activeTagFilters,
onFilterChange,
projectId,
}: TestCaseFilterProps) {
const tokenContext = useContext(TokenContext);
const [title, setTitle] = useState<string>('');
const [selectedPriorities, setSelectedPriorities] = useState<Selection>(new Set([]));
const [selectedTypes, setSelectedTypes] = useState<Selection>(new Set([]));
const [selectedTags, setSelectedTags] = useState<Selection>(new Set([]));
const [tags, setTags] = useState<Tag[]>([]);
useEffect(() => {
const fetchDataEffect = async () => {
try {
const tagsResponse = (await fetchTags(tokenContext.token.access_token, projectId)) || [];
setTags(tagsResponse);
} catch (error) {
logError('Error fetching case tags', error);
addToast({ title: 'Error', description: 'Error fetching tags', color: 'danger' });
}
};
fetchDataEffect();
}, [projectId, tokenContext.token.access_token]);
useEffect(() => {
if (activeTagFilters.length > 0) {
const activeKeys = activeTagFilters.map((id) => id.toString());
setSelectedTags(new Set(activeKeys));
} else {
setSelectedTags(new Set([]));
}
}, [activeTagFilters]);
useEffect(() => {
setTitle(activeTitleFilter);
@@ -74,13 +118,20 @@ export default function TestCaseFilter({
.filter((index) => index !== -1);
}
onFilterChange(title, priorityIndices, typeIndices);
let tagIds: number[] = [];
if (selectedTags !== 'all' && selectedTags.size > 0) {
tagIds = Array.from(selectedTags)
.map((key) => parseInt(key as string))
.filter((id) => !isNaN(id));
}
onFilterChange(title, priorityIndices, typeIndices, tagIds);
};
const handleClearFilter = () => {
setSelectedPriorities(new Set([]));
setSelectedTypes(new Set([]));
onFilterChange('', [], []);
onFilterChange('', [], [], []);
};
return (
@@ -159,6 +210,35 @@ export default function TestCaseFilter({
</DropdownMenu>
</Dropdown>
</div>
<div className="flex-col space-y-1">
<h3 className="text-default-500 text-small"></h3>
</div>
</div>
<div className="mb-3 space-y-1">
<h3 className="text-default-500 text-small">{messages.tags}</h3>
<Dropdown>
<DropdownTrigger>
<Button size="sm" variant="bordered" className="w-32" endContent={<ChevronDown size={16} />}>
{selectedTags === 'all' || selectedTags.size === 0
? messages.selectTags
: `${selectedTags.size} ${messages.selected || 'selected'}`}
</Button>
</DropdownTrigger>
<DropdownMenu
className="max-h-[50vh] overflow-y-auto"
aria-label="Tag filter"
selectionMode="multiple"
selectedKeys={selectedTags}
onSelectionChange={setSelectedTags}
>
{tags.map((tag) => (
<DropdownItem key={tag.id.toString()} textValue={tag.name}>
<span className="text-sm">{tag.name}</span>
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
</div>
<div className="flex justify-end">
<Button className="me-2" size="sm" variant="light" onPress={handleClearFilter}>

View File

@@ -46,10 +46,11 @@ type Props = {
onDeleteCase: (caseId: number) => void;
onDeleteCases: (caseIds: number[]) => void;
onExportCases: (type: string) => void;
onFilterChange: (query: string, priorities: number[], types: number[]) => void;
onFilterChange: (query: string, priorities: number[], types: number[], tag: number[]) => void;
activeTitleFilter: string;
activePriorityFilters: number[];
activeTypeFilters: number[];
activeTagFilters: number[];
messages: CasesMessages;
priorityMessages: PriorityMessages;
testTypeMessages: TestTypeMessages;
@@ -68,6 +69,7 @@ export default function TestCaseTable({
activeTitleFilter,
activePriorityFilters,
activeTypeFilters,
activeTagFilters,
messages,
priorityMessages,
testTypeMessages,
@@ -152,7 +154,8 @@ export default function TestCaseTable({
// filter test case
// **************************************************************************
const [showFilter, setShowFilter] = useState(false);
const activeFilterNum = (activeTitleFilter ? 1 : 0) + activePriorityFilters.length + activeTypeFilters.length;
const activeFilterNum =
(activeTitleFilter ? 1 : 0) + activePriorityFilters.length + activeTypeFilters.length + activeTagFilters.length;
// **************************************************************************
// select test case
@@ -318,9 +321,11 @@ export default function TestCaseTable({
activeTitleFilter={activeTitleFilter}
activePriorityFilters={activePriorityFilters}
activeTypeFilters={activeTypeFilters}
onFilterChange={(newTitleFilter, newPriorityFilters, newTypeFilters) => {
activeTagFilters={activeTagFilters}
projectId={projectId}
onFilterChange={(newTitleFilter, newPriorityFilters, newTypeFilters, newTagFilters) => {
setShowFilter(false);
onFilterChange(newTitleFilter, newPriorityFilters, newTypeFilters);
onFilterChange(newTitleFilter, newPriorityFilters, newTypeFilters, newTagFilters);
}}
/>
</PopoverContent>

View File

@@ -47,6 +47,7 @@ export default function Page({ params }: { params: { projectId: string; folderId
casesMoved: t('cases_moved'),
casesCloned: t('cases_cloned'),
tags: t('tags'),
selectTags: t('select_tags'),
};
const priorityTranslation = useTranslations('Priority');