feat: added logic to search test cases by title and description (#337)

This commit is contained in:
Matheus Santana
2025-11-15 06:21:30 -03:00
committed by GitHub
parent 068e077805
commit 7db464e28b
10 changed files with 44 additions and 36 deletions

View File

@@ -17,7 +17,7 @@ export default function (sequelize) {
Tags.belongsToMany(Case, { through: 'caseTags', foreignKey: 'tagId', otherKey: 'caseId' }); Tags.belongsToMany(Case, { through: 'caseTags', foreignKey: 'tagId', otherKey: 'caseId' });
router.get('/', verifySignedIn, verifyProjectVisibleFromFolderId, async (req, res) => { router.get('/', verifySignedIn, verifyProjectVisibleFromFolderId, async (req, res) => {
const { folderId, title, priority, type, tag } = req.query; const { folderId, search, priority, type, tag } = req.query;
if (!folderId) { if (!folderId) {
return res.status(400).json({ error: 'folderId is required' }); return res.status(400).json({ error: 'folderId is required' });
@@ -28,15 +28,18 @@ export default function (sequelize) {
folderId: folderId, folderId: folderId,
}; };
if (title) { if (search) {
const searchTerm = title.trim(); const searchTerm = search.trim();
if (searchTerm.length > 100) { if (searchTerm.length > 100) {
return res.status(400).json({ error: 'too long title param' }); return res.status(400).json({ error: 'too long search param' });
} }
if (searchTerm.length >= 1) { if (searchTerm.length >= 1) {
whereClause[Op.or] = [{ title: { [Op.like]: `%${searchTerm}%` } }]; whereClause[Op.or] = [
{ title: { [Op.like]: `%${searchTerm}%` } },
{ description: { [Op.like]: `%${searchTerm}%` } },
];
} }
} }

View File

@@ -223,6 +223,7 @@
"no_cases_found": "No test cases found", "no_cases_found": "No test cases found",
"case_title": "Test Case Title", "case_title": "Test Case Title",
"case_description": "Test Case Description", "case_description": "Test Case Description",
"case_title_or_description": "Test case title or description",
"create": "Create", "create": "Create",
"please_enter": "Please enter test case title", "please_enter": "Please enter test case title",
"filter": "Filter", "filter": "Filter",

View File

@@ -223,6 +223,7 @@
"no_cases_found": "テストケースがありません", "no_cases_found": "テストケースがありません",
"case_title": "テストケースタイトル", "case_title": "テストケースタイトル",
"case_description": "テストケース詳細", "case_description": "テストケース詳細",
"case_title_or_description": "テストケースのタイトルまたは説明",
"create": "作成", "create": "作成",
"please_enter": "テストケースタイトルを入力してください", "please_enter": "テストケースタイトルを入力してください",
"filter": "フィルター", "filter": "フィルター",

View File

@@ -223,6 +223,7 @@
"no_cases_found": "Nenhum caso de teste encontrado", "no_cases_found": "Nenhum caso de teste encontrado",
"case_title": "Título do Caso de Teste", "case_title": "Título do Caso de Teste",
"case_description": "Descrição do Caso de Teste", "case_description": "Descrição do Caso de Teste",
"case_title_or_description": "Título ou descrição do caso de teste",
"create": "Criar", "create": "Criar",
"please_enter": "Por favor, insira o título do caso de teste", "please_enter": "Por favor, insira o título do caso de teste",
"filter": "Filtrar", "filter": "Filtrar",

View File

@@ -34,7 +34,7 @@ export default function CasesPane({
}: Props) { }: Props) {
const [cases, setCases] = useState<CaseType[]>([]); const [cases, setCases] = useState<CaseType[]>([]);
const [isCaseDialogOpen, setIsCaseDialogOpen] = useState(false); const [isCaseDialogOpen, setIsCaseDialogOpen] = useState(false);
const [titleFilter, setTitleFilter] = useState(''); const [searchFilter, setSearchFilter] = useState('');
const [priorityFilter, setPriorityFilter] = useState<number[]>([]); const [priorityFilter, setPriorityFilter] = useState<number[]>([]);
const [typeFilter, setTypeFilter] = useState<number[]>([]); const [typeFilter, setTypeFilter] = useState<number[]>([]);
const [tagFilter, setTagFilter] = useState<number[]>([]); const [tagFilter, setTagFilter] = useState<number[]>([]);
@@ -45,13 +45,13 @@ export default function CasesPane({
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const updateUrlParams = (updates: { title?: string; priority?: number[]; type?: number[]; tag?: number[] }) => { const updateUrlParams = (updates: { search?: string; priority?: number[]; type?: number[]; tag?: number[] }) => {
const currentParams = new URLSearchParams(searchParams.toString()); const currentParams = new URLSearchParams(searchParams.toString());
if (updates.title) { if (updates.search) {
currentParams.set('title', updates.title); currentParams.set('search', updates.search);
} else { } else {
currentParams.delete('title'); currentParams.delete('search');
} }
if (updates.priority && updates.priority.length > 0) { if (updates.priority && updates.priority.length > 0) {
@@ -80,12 +80,12 @@ export default function CasesPane({
async function fetchDataEffect() { async function fetchDataEffect() {
if (!context.isSignedIn()) return; if (!context.isSignedIn()) return;
const titleParam = searchParams.get('title') || ''; const searchParam = searchParams.get('search') || '';
const priorityParam = parseQueryParam(searchParams.get('priority')); const priorityParam = parseQueryParam(searchParams.get('priority'));
const typeParam = parseQueryParam(searchParams.get('type')); const typeParam = parseQueryParam(searchParams.get('type'));
const tagParam = parseQueryParam(searchParams.get('tag')); const tagParam = parseQueryParam(searchParams.get('tag'));
setTitleFilter(titleParam); setSearchFilter(searchParam);
setPriorityFilter(priorityParam); setPriorityFilter(priorityParam);
setTypeFilter(typeParam); setTypeFilter(typeParam);
setTagFilter(tagParam); setTagFilter(tagParam);
@@ -94,7 +94,7 @@ export default function CasesPane({
const data = await fetchCases( const data = await fetchCases(
context.token.access_token, context.token.access_token,
Number(folderId), Number(folderId),
titleParam || undefined, searchParam || undefined,
priorityParam.length > 0 ? priorityParam : undefined, priorityParam.length > 0 ? priorityParam : undefined,
typeParam.length > 0 ? typeParam : undefined, typeParam.length > 0 ? typeParam : undefined,
tagParam.length > 0 ? tagParam : undefined tagParam.length > 0 ? tagParam : undefined
@@ -143,12 +143,12 @@ export default function CasesPane({
await exportCases(context.token.access_token, Number(folderId), type); await exportCases(context.token.access_token, Number(folderId), type);
}; };
const handleFilterChange = (title: string, priorities: number[], types: number[], tag: number[]) => { const handleFilterChange = (search: string, priorities: number[], types: number[], tag: number[]) => {
setTitleFilter(title); setSearchFilter(search);
setPriorityFilter(priorities); setPriorityFilter(priorities);
setTypeFilter(types); setTypeFilter(types);
setTagFilter(tag); setTagFilter(tag);
updateUrlParams({ title: title, priority: priorities, type: types, tag: tag }); updateUrlParams({ search: search, priority: priorities, type: types, tag: tag });
}; };
// ************************************************************************** // **************************************************************************
@@ -186,7 +186,7 @@ export default function CasesPane({
onDeleteCases={onDeleteCases} onDeleteCases={onDeleteCases}
onExportCases={onExportCases} onExportCases={onExportCases}
onFilterChange={handleFilterChange} onFilterChange={handleFilterChange}
activeTitleFilter={titleFilter} activeSearchFilter={searchFilter}
activePriorityFilters={priorityFilter} activePriorityFilters={priorityFilter}
activeTypeFilters={typeFilter} activeTypeFilters={typeFilter}
activeTagFilters={tagFilter} activeTagFilters={tagFilter}

View File

@@ -23,12 +23,12 @@ type TestCaseFilterProps = {
messages: CasesMessages; messages: CasesMessages;
priorityMessages: PriorityMessages; priorityMessages: PriorityMessages;
testTypeMessages: TestTypeMessages; testTypeMessages: TestTypeMessages;
activeTitleFilter: string; activeSearchFilter: string;
activePriorityFilters: number[]; activePriorityFilters: number[];
activeTypeFilters: number[]; activeTypeFilters: number[];
activeTagFilters: number[]; activeTagFilters: number[];
projectId: string; projectId: string;
onFilterChange: (title: string, priorities: number[], types: number[], tags: number[]) => void; onFilterChange: (search: string, priorities: number[], types: number[], tags: number[]) => void;
}; };
type Tag = Pick<TagType, 'id' | 'name'>; type Tag = Pick<TagType, 'id' | 'name'>;
@@ -37,7 +37,7 @@ export default function TestCaseFilter({
messages, messages,
priorityMessages, priorityMessages,
testTypeMessages, testTypeMessages,
activeTitleFilter, activeSearchFilter,
activePriorityFilters, activePriorityFilters,
activeTypeFilters, activeTypeFilters,
activeTagFilters, activeTagFilters,
@@ -45,7 +45,7 @@ export default function TestCaseFilter({
projectId, projectId,
}: TestCaseFilterProps) { }: TestCaseFilterProps) {
const tokenContext = useContext(TokenContext); const tokenContext = useContext(TokenContext);
const [title, setTitle] = useState<string>(''); const [search, setSearch] = useState<string>('');
const [selectedPriorities, setSelectedPriorities] = useState<Selection>(new Set([])); const [selectedPriorities, setSelectedPriorities] = useState<Selection>(new Set([]));
const [selectedTypes, setSelectedTypes] = useState<Selection>(new Set([])); const [selectedTypes, setSelectedTypes] = useState<Selection>(new Set([]));
const [selectedTags, setSelectedTags] = useState<Selection>(new Set([])); const [selectedTags, setSelectedTags] = useState<Selection>(new Set([]));
@@ -74,8 +74,8 @@ export default function TestCaseFilter({
}, [activeTagFilters]); }, [activeTagFilters]);
useEffect(() => { useEffect(() => {
setTitle(activeTitleFilter); setSearch(activeSearchFilter);
}, [activeTitleFilter]); }, [activeSearchFilter]);
useEffect(() => { useEffect(() => {
if (activePriorityFilters.length > 0) { if (activePriorityFilters.length > 0) {
@@ -125,7 +125,7 @@ export default function TestCaseFilter({
.filter((id) => !isNaN(id)); .filter((id) => !isNaN(id));
} }
onFilterChange(title, priorityIndices, typeIndices, tagIds); onFilterChange(search, priorityIndices, typeIndices, tagIds);
}; };
const handleClearFilter = () => { const handleClearFilter = () => {
@@ -137,7 +137,7 @@ export default function TestCaseFilter({
return ( return (
<div className="p-3"> <div className="p-3">
<div className="mb-3 space-y-1"> <div className="mb-3 space-y-1">
<h3 className="text-default-500 text-small">{messages.caseTitle}</h3> <h3 className="text-default-500 text-small">{messages.caseTitleOrDescription}</h3>
<Input <Input
variant="bordered" variant="bordered"
classNames={{ classNames={{
@@ -148,8 +148,8 @@ export default function TestCaseFilter({
size="sm" size="sm"
startContent={<SearchIcon size={18} />} startContent={<SearchIcon size={18} />}
type="search" type="search"
value={title} value={search}
onValueChange={setTitle} onValueChange={setSearch}
maxLength={100} maxLength={100}
/> />
</div> </div>

View File

@@ -47,7 +47,7 @@ type Props = {
onDeleteCases: (caseIds: number[]) => void; onDeleteCases: (caseIds: number[]) => void;
onExportCases: (type: string) => void; onExportCases: (type: string) => void;
onFilterChange: (query: string, priorities: number[], types: number[], tag: number[]) => void; onFilterChange: (query: string, priorities: number[], types: number[], tag: number[]) => void;
activeTitleFilter: string; activeSearchFilter: string;
activePriorityFilters: number[]; activePriorityFilters: number[];
activeTypeFilters: number[]; activeTypeFilters: number[];
activeTagFilters: number[]; activeTagFilters: number[];
@@ -66,7 +66,7 @@ export default function TestCaseTable({
onDeleteCases, onDeleteCases,
onExportCases, onExportCases,
onFilterChange, onFilterChange,
activeTitleFilter, activeSearchFilter,
activePriorityFilters, activePriorityFilters,
activeTypeFilters, activeTypeFilters,
activeTagFilters, activeTagFilters,
@@ -105,7 +105,7 @@ export default function TestCaseTable({
> >
{highlightSearchTerm({ {highlightSearchTerm({
text: cellValue as string, text: cellValue as string,
searchTerm: activeTitleFilter, searchTerm: activeSearchFilter,
})} })}
</Link> </Link>
); );
@@ -147,7 +147,7 @@ export default function TestCaseTable({
} }
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[activeTitleFilter] [activeSearchFilter]
); );
// ************************************************************************** // **************************************************************************
@@ -155,7 +155,7 @@ export default function TestCaseTable({
// ************************************************************************** // **************************************************************************
const [showFilter, setShowFilter] = useState(false); const [showFilter, setShowFilter] = useState(false);
const activeFilterNum = const activeFilterNum =
(activeTitleFilter ? 1 : 0) + activePriorityFilters.length + activeTypeFilters.length + activeTagFilters.length; (activeSearchFilter ? 1 : 0) + activePriorityFilters.length + activeTypeFilters.length + activeTagFilters.length;
// ************************************************************************** // **************************************************************************
// select test case // select test case
@@ -318,7 +318,7 @@ export default function TestCaseTable({
messages={messages} messages={messages}
priorityMessages={priorityMessages} priorityMessages={priorityMessages}
testTypeMessages={testTypeMessages} testTypeMessages={testTypeMessages}
activeTitleFilter={activeTitleFilter} activeSearchFilter={activeSearchFilter}
activePriorityFilters={activePriorityFilters} activePriorityFilters={activePriorityFilters}
activeTypeFilters={activeTypeFilters} activeTypeFilters={activeTypeFilters}
activeTagFilters={activeTagFilters} activeTagFilters={activeTagFilters}

View File

@@ -31,6 +31,7 @@ export default function Page({ params }: { params: { projectId: string; folderId
noCasesFound: t('no_cases_found'), noCasesFound: t('no_cases_found'),
caseTitle: t('case_title'), caseTitle: t('case_title'),
caseDescription: t('case_description'), caseDescription: t('case_description'),
caseTitleOrDescription: t('case_title_or_description'),
create: t('create'), create: t('create'),
pleaseEnter: t('please_enter'), pleaseEnter: t('please_enter'),
apply: t('apply'), apply: t('apply'),

View File

@@ -79,6 +79,7 @@ type CasesMessages = {
noCasesFound: string; noCasesFound: string;
caseTitle: string; caseTitle: string;
caseDescription: string; caseDescription: string;
caseTitleOrDescription: string;
create: string; create: string;
pleaseEnter: string; pleaseEnter: string;
filter: string; filter: string;

View File

@@ -29,15 +29,15 @@ async function fetchCase(jwt: string, caseId: number) {
async function fetchCases( async function fetchCases(
jwt: string, jwt: string,
folderId: number, folderId: number,
title?: string, search?: string,
priority?: number[], priority?: number[],
type?: number[], type?: number[],
tag?: number[] tag?: number[]
) { ) {
const queryParams = [`folderId=${folderId}`]; const queryParams = [`folderId=${folderId}`];
if (title) { if (search) {
queryParams.push(`title=${title}`); queryParams.push(`search=${search}`);
} }
if (priority && priority.length > 0) { if (priority && priority.length > 0) {