mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-16 17:53: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();
|
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 { FolderIcon, PlusIcon } from '@heroicons/vue/20/solid';
|
||||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||||
import ProjectTable from '@/Components/Common/Project/ProjectTable.vue';
|
import ProjectTable from '@/Components/Common/Project/ProjectTable.vue';
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsStore } from '@/utils/useProjects';
|
||||||
import ProjectCreateModal from '@/packages/ui/src/Project/ProjectCreateModal.vue';
|
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
|
// Filter projects based on current filters
|
||||||
const filteredProjects = computed(() => {
|
const filteredProjects = computed(() => {
|
||||||
|
const searchTerm = search.value.trim().toLowerCase();
|
||||||
|
|
||||||
return projects.value.filter((project) => {
|
return projects.value.filter((project) => {
|
||||||
|
// Name search
|
||||||
|
if (searchTerm && !project.name.toLowerCase().includes(searchTerm)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Status filter
|
// Status filter
|
||||||
if (tableState.value.filters.status === 'active' && project.is_archived) {
|
if (tableState.value.filters.status === 'active' && project.is_archived) {
|
||||||
return false;
|
return false;
|
||||||
@@ -158,6 +169,14 @@ const showBillableRate = computed(() => {
|
|||||||
:clients="clients"
|
:clients="clients"
|
||||||
@update:filters="tableState.filters = $event" />
|
@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 -->
|
<!-- Active Filters -->
|
||||||
<ProjectStatusFilterBadge
|
<ProjectStatusFilterBadge
|
||||||
v-if="tableState.filters.status !== 'all'"
|
v-if="tableState.filters.status !== 'all'"
|
||||||
|
|||||||
Reference in New Issue
Block a user