feat: tag filtering functionality for test cases (#324)
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user