mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-07 21:33:18 +01:00
add search input to projects overview page
Filters the project table by name client-side, reusing the existing filteredProjects computed and the dropdown search input styling. Search is deliberately not persisted in the table state so a reload never leaves the table silently filtered.
This commit is contained in:
@@ -1288,3 +1288,31 @@ test.describe('Projects Pagination', () => {
|
||||
await expect(page.getByText(prefix + '00')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('test that searching projects by name works', async ({ page, ctx }) => {
|
||||
const suffix = Math.floor(1 + Math.random() * 10000);
|
||||
const matchingProjectName = 'Searchable Project ' + suffix;
|
||||
const otherProjectName = 'Unrelated Work ' + suffix;
|
||||
await createProjectViaApi(ctx, { name: matchingProjectName });
|
||||
await createProjectViaApi(ctx, { name: otherProjectName });
|
||||
|
||||
await goToProjectsOverview(page);
|
||||
await clearProjectTableState(page);
|
||||
await page.reload();
|
||||
await expect(page.getByText(matchingProjectName)).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByText(otherProjectName)).toBeVisible();
|
||||
|
||||
// Searching is case insensitive and matches part of the name
|
||||
await page.getByTestId('project_search').fill('searchable project ' + suffix);
|
||||
await expect(page.getByText(matchingProjectName)).toBeVisible();
|
||||
await expect(page.getByText(otherProjectName)).not.toBeVisible();
|
||||
|
||||
// A term that matches nothing empties the table
|
||||
await page.getByTestId('project_search').fill('no project has this name');
|
||||
await expect(page.getByText(matchingProjectName)).not.toBeVisible();
|
||||
|
||||
// Clearing the search restores both projects
|
||||
await page.getByTestId('project_search').fill('');
|
||||
await expect(page.getByText(matchingProjectName)).toBeVisible();
|
||||
await expect(page.getByText(otherProjectName)).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import AppLayout from '@/Layouts/AppLayout.vue';
|
||||
import { FolderIcon, PlusIcon } from '@heroicons/vue/20/solid';
|
||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||
import ProjectTable from '@/Components/Common/Project/ProjectTable.vue';
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
||||
import { useProjectsStore } from '@/utils/useProjects';
|
||||
import ProjectCreateModal from '@/packages/ui/src/Project/ProjectCreateModal.vue';
|
||||
@@ -62,9 +62,20 @@ const { tableState, handleSort } = useTableSortState<SortColumn, ProjectTableSta
|
||||
})
|
||||
);
|
||||
|
||||
// Search is intentionally not persisted in tableState, so a reload never leaves
|
||||
// the table silently filtered by a term the user has forgotten about.
|
||||
const search = ref('');
|
||||
|
||||
// Filter projects based on current filters
|
||||
const filteredProjects = computed(() => {
|
||||
const searchTerm = search.value.trim().toLowerCase();
|
||||
|
||||
return projects.value.filter((project) => {
|
||||
// Name search
|
||||
if (searchTerm && !project.name.toLowerCase().includes(searchTerm)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (tableState.value.filters.status === 'active' && project.is_archived) {
|
||||
return false;
|
||||
@@ -158,6 +169,14 @@ const showBillableRate = computed(() => {
|
||||
:clients="clients"
|
||||
@update:filters="tableState.filters = $event" />
|
||||
|
||||
<!-- Same style as the dropdown search inputs, see MultiselectDropdown.vue -->
|
||||
<input
|
||||
v-model="search"
|
||||
type="search"
|
||||
data-testid="project_search"
|
||||
placeholder="Search for a Project..."
|
||||
class="w-60 h-8 rounded-md border border-input-border bg-input-background px-3 text-sm text-text-primary placeholder:text-text-tertiary focus:outline-none" />
|
||||
|
||||
<!-- Active Filters -->
|
||||
<ProjectStatusFilterBadge
|
||||
v-if="tableState.filters.status !== 'all'"
|
||||
|
||||
Reference in New Issue
Block a user