diff --git a/e2e/projects.spec.ts b/e2e/projects.spec.ts index d1288138..a66b1b1e 100644 --- a/e2e/projects.spec.ts +++ b/e2e/projects.spec.ts @@ -1302,17 +1302,22 @@ test('test that searching projects by name works', async ({ page, ctx }) => { await expect(page.getByText(matchingProjectName)).toBeVisible({ timeout: 10000 }); await expect(page.getByText(otherProjectName)).toBeVisible(); + const searchInput = page.getByRole('searchbox', { name: 'Search projects' }); + // Searching is case insensitive and matches part of the name - await page.getByTestId('project_search').fill('searchable project ' + suffix); + await searchInput.fill('SEARCHABLE'); 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 searchInput.fill('no project has this name'); await expect(page.getByText(matchingProjectName)).not.toBeVisible(); + await expect(page.getByText(otherProjectName)).not.toBeVisible(); + await expect(page.getByText('No matching projects')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Create your First Project' })).not.toBeVisible(); // Clearing the search restores both projects - await page.getByTestId('project_search').fill(''); + await searchInput.fill(''); await expect(page.getByText(matchingProjectName)).toBeVisible(); await expect(page.getByText(otherProjectName)).toBeVisible(); }); diff --git a/resources/js/Components/Common/Project/ProjectTable.vue b/resources/js/Components/Common/Project/ProjectTable.vue index a3486aa1..e5190f22 100644 --- a/resources/js/Components/Common/Project/ProjectTable.vue +++ b/resources/js/Components/Common/Project/ProjectTable.vue @@ -7,6 +7,7 @@ import ProjectCreateModal from '@/packages/ui/src/Project/ProjectCreateModal.vue import ProjectTableHeading from '@/Components/Common/Project/ProjectTableHeading.vue'; import ProjectTableRow from '@/Components/Common/Project/ProjectTableRow.vue'; import Pagination from '@/Components/Common/Pagination.vue'; +import LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue'; export type SortColumn = | 'name' @@ -34,12 +35,17 @@ import { const { organization } = useOrganizationQuery(getCurrentOrganizationId()!); -const props = defineProps<{ - projects: Project[]; - showBillableRate: boolean; - sortColumn: SortColumn; - sortDirection: SortDirection; -}>(); +const props = withDefaults( + defineProps<{ + projects: Project[]; + showBillableRate: boolean; + sortColumn: SortColumn; + sortDirection: SortDirection; + isFiltered?: boolean; + isLoading?: boolean; + }>(), + { isFiltered: false, isLoading: false } +); const emit = defineEmits<{ sort: [column: SortColumn, direction: SortDirection]; @@ -128,6 +134,28 @@ const paginatedProjects = computed(() => { return sortedProjects.value.slice(start, start + PAGE_SIZE); }); +const emptyState = computed(() => { + if (props.isFiltered) { + return { + title: 'No matching projects', + description: 'Try a different search term or adjust your filters.', + showCreateButton: false, + }; + } + if (!canCreateProjects()) { + return { + title: 'You are not a member of any projects', + description: 'Ask your manager to add you to a project as a team member.', + showCreateButton: false, + }; + } + return { + title: 'No projects found', + description: 'Create your first project now!', + showCreateButton: true, + }; +}); + const showCreateProjectModal = ref(false); async function createProject(project: CreateProjectBody): Promise { @@ -161,24 +189,21 @@ const gridTemplate = computed(() => { :sort-direction="props.sortDirection" :desc-first-columns="descFirstColumns" @sort="handleSort"> -
+
+ +
+
-

- {{ - canCreateProjects() - ? 'No projects found' - : 'You are not a member of any projects' - }} -

+

{{ emptyState.title }}

- {{ - canCreateProjects() - ? 'Create your first project now!' - : 'Ask your manager to add you to a project as a team member.' - }} + {{ emptyState.description }}

Create your First Project diff --git a/resources/js/Components/Common/Project/ProjectTableHeading.vue b/resources/js/Components/Common/Project/ProjectTableHeading.vue index 236f5d7b..9d4c295d 100644 --- a/resources/js/Components/Common/Project/ProjectTableHeading.vue +++ b/resources/js/Components/Common/Project/ProjectTableHeading.vue @@ -30,7 +30,7 @@ function handleSort(column: SortColumn) {
+ class="relative whitespace-nowrap flex items-center pl-3 text-right text-sm font-medium pr-2 sm:pr-4 lg:pr-6"> -import { computed, ref } from 'vue'; +import { ref } from 'vue'; import { UserGroupIcon, CheckCircleIcon, GlobeAltIcon } from '@heroicons/vue/16/solid'; import ListFilterIcon from '@/packages/ui/src/Icons/ListFilterIcon.vue'; import { @@ -81,23 +81,18 @@ function toggleNoClient() { clientIds, }); } - -const hasActiveFilters = computed(() => { - return ( - props.filters.status !== 'all' || - props.filters.visibility !== 'all' || - props.filters.clientIds.length > 0 - ); -});