diff --git a/e2e/projects.spec.ts b/e2e/projects.spec.ts index d4cab6c2..d1288138 100644 --- a/e2e/projects.spec.ts +++ b/e2e/projects.spec.ts @@ -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(); +}); diff --git a/resources/js/Pages/Projects.vue b/resources/js/Pages/Projects.vue index ae94d420..3346e50f 100644 --- a/resources/js/Pages/Projects.vue +++ b/resources/js/Pages/Projects.vue @@ -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 { + 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" /> + + +