Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ export function UserQuerying({ onFilterStateChange, onFiltersUpdated }: UserQuer
search: (prev) => ({
...prev,
...params,
pageOffset: undefined,
userId: undefined
pageOffset: undefined
})
});
// Only call onFiltersUpdated for actual filter changes, not search updates
Expand All @@ -89,7 +88,7 @@ export function UserQuerying({ onFilterStateChange, onFiltersUpdated }: UserQuer
);

useEffect(() => {
updateFilter({ search: debouncedSearch }, true);
updateFilter({ search: debouncedSearch || undefined }, true);
}, [debouncedSearch, updateFilter]);

// Count active filters for badge
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ export function UserTable({
const userInfo = useUserInfo();

const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>(() => ({
column: orderBy ?? "email",
direction: sortOrder === "Ascending" ? "ascending" : "descending"
column: orderBy ?? SortableUserProperties.Name,
direction: sortOrder === SortOrder.Descending ? "descending" : "ascending"
}));
const isKeyboardNavigation = useKeyboardNavigation();
const isMobile = useViewportResize();
Expand All @@ -66,9 +66,9 @@ export function UserTable({
UserStatus: userStatus,
StartDate: startDate,
EndDate: endDate,
OrderBy: orderBy,
SortOrder: sortOrder,
PageOffset: pageOffset
OrderBy: orderBy ?? SortableUserProperties.Name,
SortOrder: sortOrder ?? SortOrder.Ascending,
PageOffset: pageOffset ?? 0
}
},
enabled: !isMobile
Expand Down Expand Up @@ -112,12 +112,16 @@ export function UserTable({
const handleSortChange = useCallback(
(newSortDescriptor: SortDescriptor) => {
setSortDescriptor(newSortDescriptor);
const newOrderBy = (newSortDescriptor.column?.toString() ??
SortableUserProperties.Name) as SortableUserProperties;
const newSortOrder = newSortDescriptor.direction === "ascending" ? SortOrder.Ascending : SortOrder.Descending;

navigate({
to: "/admin/users",
search: (prev) => ({
...prev,
orderBy: (newSortDescriptor.column?.toString() ?? "Name") as SortableUserProperties,
sortOrder: newSortDescriptor.direction === "ascending" ? SortOrder.Ascending : SortOrder.Descending,
orderBy: newOrderBy === SortableUserProperties.Name ? undefined : newOrderBy,
sortOrder: newSortOrder === SortOrder.Ascending ? undefined : newSortOrder,
pageOffset: undefined
})
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { useCallback, useEffect, useState } from "react";
import {
api,
type components,
type SortableUserProperties,
type SortOrder,
SortableUserProperties,
SortOrder,
type UserRole,
type UserStatus
} from "@/shared/lib/api/client";
Expand Down Expand Up @@ -45,8 +45,8 @@ export function useInfiniteUsers({
UserStatus: userStatus,
StartDate: startDate,
EndDate: endDate,
OrderBy: orderBy,
SortOrder: sortOrder,
OrderBy: orderBy ?? SortableUserProperties.Name,
SortOrder: sortOrder ?? SortOrder.Ascending,
PageOffset: 0
}
},
Expand All @@ -65,8 +65,8 @@ export function useInfiniteUsers({
UserStatus: userStatus,
StartDate: startDate,
EndDate: endDate,
OrderBy: orderBy,
SortOrder: sortOrder,
OrderBy: orderBy ?? SortableUserProperties.Name,
SortOrder: sortOrder ?? SortOrder.Ascending,
PageOffset: nextPageToLoad
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ const userPageSearchSchema = z.object({
userStatus: z.nativeEnum(UserStatus).nullable().optional(),
startDate: z.string().optional(),
endDate: z.string().optional(),
orderBy: z.nativeEnum(SortableUserProperties).default(SortableUserProperties.Name).optional(),
sortOrder: z.nativeEnum(SortOrder).default(SortOrder.Ascending).optional(),
pageOffset: z.number().default(0).optional(),
orderBy: z.nativeEnum(SortableUserProperties).optional(),
sortOrder: z.nativeEnum(SortOrder).optional(),
pageOffset: z.number().optional(),
userId: z.string().optional()
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ test.describe("@comprehensive", () => {
await step("Open mobile menu & verify all navigation and settings are accessible")(async () => {
await page.getByRole("button", { name: "Open navigation menu" }).click();

const mobileDialog = page.getByRole("dialog");
const mobileDialog = page.getByRole("dialog", { name: "Mobile navigation menu" });
await expect(mobileDialog).toBeVisible();

// Verify user profile section is visible
Expand Down Expand Up @@ -120,7 +120,7 @@ test.describe("@comprehensive", () => {
await expect(page.getByText(newTitle)).toBeVisible();

// Close mobile menu by clicking the X button
const mobileDialog = page.getByRole("dialog");
const mobileDialog = page.getByRole("dialog", { name: "Mobile navigation menu" });
await mobileDialog.getByRole("button", { name: "Close menu" }).click();
await expect(mobileDialog).not.toBeVisible();
})();
Expand Down Expand Up @@ -148,7 +148,7 @@ test.describe("@comprehensive", () => {
await step("Change language back to English & verify language updates")(async () => {
await page.getByRole("button", { name: "Åbn navigationsmenu" }).click();

const mobileDialog = page.getByRole("dialog");
const mobileDialog = page.getByRole("dialog", { name: "Mobile navigation menu" });
await mobileDialog.getByRole("button", { name: "Sprog" }).click();

// Wait for language menu to open
Expand All @@ -168,7 +168,7 @@ test.describe("@comprehensive", () => {
await step("Change theme through mobile menu & verify theme applies")(async () => {
await page.getByRole("button", { name: "Open navigation menu" }).click();

const mobileDialog = page.getByRole("dialog");
const mobileDialog = page.getByRole("dialog", { name: "Mobile navigation menu" });
await mobileDialog.getByRole("button", { name: "Theme" }).click();

// Wait for theme menu to open
Expand All @@ -188,7 +188,7 @@ test.describe("@comprehensive", () => {
await step("Navigate to users page through mobile menu & verify navigation works")(async () => {
await page.getByRole("button", { name: "Open navigation menu" }).click();

const mobileDialog = page.getByRole("dialog");
const mobileDialog = page.getByRole("dialog", { name: "Mobile navigation menu" });
await mobileDialog.getByRole("link", { name: "Users" }).click();

// Mobile menu should close
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ test.describe("@comprehensive", () => {
await page.getByRole("button", { name: "User profile menu" }).click();

// Wait for user menu to open
const userMenu = page.getByRole("menu");
const userMenu = page.getByRole("menu", { name: "User profile menu" });
await expect(userMenu).toBeVisible();

await page.getByRole("menuitem", { name: "Log out" }).click();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from "@playwright/test";
import { test } from "@shared/e2e/fixtures/page-auth";
import { createTestContext, expectToastMessage } from "@shared/e2e/utils/test-assertions";
import { blurActiveElement, createTestContext, expectToastMessage } from "@shared/e2e/utils/test-assertions";
import { completeSignupFlow, getVerificationCode, testUser } from "@shared/e2e/utils/test-data";
import { step } from "@shared/e2e/utils/test-step-wrapper";

Expand Down Expand Up @@ -329,13 +329,26 @@ test.describe("@comprehensive", () => {
})();

await step("Filter by Owner role & verify only owner shown")(async () => {
// Clear the existing status filter (currently set to "Pending")
await page.getByLabel("User status").first().click();
await page.getByRole("option", { name: "Any status" }).click();

// Now filter by Owner role
await page.getByLabel("User role").first().click();
await page.getByRole("option", { name: "Owner" }).click();
// Filters are in the dialog that was opened in the previous step
const filterDialog = page.getByRole("dialog", { name: "Filters" });

// Clear status filter
await filterDialog.getByLabel("User status").click();
const statusListbox = page.getByRole("listbox", { name: "User status" });
await expect(statusListbox).toBeVisible();
await statusListbox.getByRole("option", { name: "Any status" }).click();
await blurActiveElement(page);

// Set role filter to Owner
await filterDialog.getByLabel("User role").click();
const roleListbox = page.getByRole("listbox", { name: "User role" });
await expect(roleListbox).toBeVisible();
await roleListbox.getByRole("option", { name: "Owner" }).click();
await blurActiveElement(page);

// Click OK to apply and close dialog
await filterDialog.getByRole("button", { name: "OK" }).click();
await expect(filterDialog).not.toBeVisible();

// Verify only owner is shown
await expect(page.locator("tbody").first().first().locator("tr")).toHaveCount(1);
Expand All @@ -345,9 +358,21 @@ test.describe("@comprehensive", () => {
})();

await step("Filter by Member role & verify only members shown")(async () => {
// Change filter to Member role
await page.getByLabel("User role").first().click();
await page.getByRole("option", { name: "Member" }).click();
// Reopen dialog
await page.getByRole("button", { name: "Show filters" }).click();
const filterDialog = page.getByRole("dialog", { name: "Filters" });
await expect(filterDialog).toBeVisible();

// Set role filter to Member
await filterDialog.getByLabel("User role").click();
const roleListbox = page.getByRole("listbox", { name: "User role" });
await expect(roleListbox).toBeVisible();
await roleListbox.getByRole("option", { name: "Member" }).click();
await blurActiveElement(page);

// Click OK to apply and close dialog
await filterDialog.getByRole("button", { name: "OK" }).click();
await expect(filterDialog).not.toBeVisible();

// Verify only member users are shown
await expect(page.locator("tbody").first().first().locator("tr")).toHaveCount(3);
Expand All @@ -357,13 +382,39 @@ test.describe("@comprehensive", () => {
await expect(page.locator("tbody").first()).not.toContainText(owner.email);
})();

await step("Clear all filters & verify all users are shown")(async () => {
// Reopen filter dialog
await page.getByRole("button", { name: "Show filters" }).click();
const filterDialog = page.getByRole("dialog", { name: "Filters" });
await expect(filterDialog).toBeVisible();

// Click Clear button to reset all filters
await filterDialog.getByRole("button", { name: "Clear" }).click();

// Close dialog with Escape (OK button gets detached after Clear click due to re-render)
await page.keyboard.press("Escape");
await expect(filterDialog).not.toBeVisible();

// Verify all 4 users are shown (no filters applied)
await expect(page.locator("tbody").first().first().locator("tr")).toHaveCount(4);
})();

await step("Filter by Pending status & verify only pending users shown")(async () => {
// Reset role filter and set status filter
await page.getByLabel("User role").first().click();
await page.getByRole("option", { name: "Any role" }).click();
// Reopen filter dialog
await page.getByRole("button", { name: "Show filters" }).click();
const filterDialog = page.getByRole("dialog", { name: "Filters" });
await expect(filterDialog).toBeVisible();

await page.getByLabel("User status").first().click();
await page.getByRole("option", { name: "Pending" }).click();
// Set status filter to Pending
await filterDialog.getByLabel("User status").click();
const statusListbox = page.getByRole("listbox", { name: "User status" });
await expect(statusListbox).toBeVisible();
await statusListbox.getByRole("option", { name: "Pending" }).click();
await blurActiveElement(page);

// Close dialog
await filterDialog.getByRole("button", { name: "OK" }).click();
await expect(filterDialog).not.toBeVisible();

// Verify only pending users are shown (invited users who haven't confirmed)
await expect(page.locator("tbody").first().first().locator("tr")).toHaveCount(3);
Expand All @@ -374,9 +425,21 @@ test.describe("@comprehensive", () => {
})();

await step("Filter by Active status & verify only active users shown")(async () => {
// Change filter to Active status
await page.getByLabel("User status").first().click();
await page.getByRole("option", { name: "Active" }).click();
// Reopen dialog
await page.getByRole("button", { name: "Show filters" }).click();
const filterDialog = page.getByRole("dialog", { name: "Filters" });
await expect(filterDialog).toBeVisible();

// Set status filter to Active
await filterDialog.getByLabel("User status").click();
const statusListbox = page.getByRole("listbox", { name: "User status" });
await expect(statusListbox).toBeVisible();
await statusListbox.getByRole("option", { name: "Active" }).click();
await blurActiveElement(page);

// Click OK to apply and close dialog
await filterDialog.getByRole("button", { name: "OK" }).click();
await expect(filterDialog).not.toBeVisible();

// Verify only active users are shown (owner who has confirmed email)
await expect(page.locator("tbody").first().first().locator("tr")).toHaveCount(1);
Expand All @@ -385,32 +448,35 @@ test.describe("@comprehensive", () => {
})();

await step("Filter by past date range & verify no users shown")(async () => {
// Reset status filter first
await page.getByLabel("User status").first().click();
await page.getByRole("option", { name: "Any status" }).click();
// Reopen dialog
await page.getByRole("button", { name: "Show filters" }).click();
const filterDialog = page.getByRole("dialog", { name: "Filters" });
await expect(filterDialog).toBeVisible();

// Clear status filter
await filterDialog.getByLabel("User status").click();
const statusListbox = page.getByRole("listbox", { name: "User status" });
await expect(statusListbox).toBeVisible();
await statusListbox.getByRole("option", { name: "Any status" }).click();
await blurActiveElement(page);

// Open date picker
await page.getByLabel("Modified date").first().click();

// Set start date to January 1, 2024
await page.locator('[role="spinbutton"][aria-label="month, Start Date, "]').clear();
await page.locator('[role="spinbutton"][aria-label="month, Start Date, "]').type("01");
await page.locator('[role="spinbutton"][aria-label="day, Start Date, "]').clear();
await page.locator('[role="spinbutton"][aria-label="day, Start Date, "]').type("01");
await page.locator('[role="spinbutton"][aria-label="year, Start Date, "]').clear();
await page.locator('[role="spinbutton"][aria-label="year, Start Date, "]').type("2024");

// Set end date to December 31, 2024
await page.locator('[role="spinbutton"][aria-label="month, End Date, "]').clear();
await page.locator('[role="spinbutton"][aria-label="month, End Date, "]').type("12");
await page.locator('[role="spinbutton"][aria-label="day, End Date, "]').clear();
await page.locator('[role="spinbutton"][aria-label="day, End Date, "]').type("31");
await page.locator('[role="spinbutton"][aria-label="year, End Date, "]').clear();
await page.locator('[role="spinbutton"][aria-label="year, End Date, "]').type("2024");

// Close the calendar
await filterDialog.getByLabel("Modified date").click();

// Close the calendar picker to enter keyboard mode
await page.keyboard.press("Escape");

// Click on year field to focus it for keyboard entry
await page.locator('[role="spinbutton"][aria-label="year, Start Date, "]').click({ force: true });

// Set dates via keyboard (auto-advances through fields)
await page.keyboard.type("20240101");
await page.keyboard.type("20241231");

// Close the filter dialog
await filterDialog.getByRole("button", { name: "OK" }).click();
await expect(filterDialog).not.toBeVisible();

// Verify no users are shown for the past date range (users were created in 2025)
await expect(page.locator("tbody").first().first().locator("tr")).toHaveCount(0);
})();
Expand Down Expand Up @@ -443,7 +509,7 @@ test.describe("@comprehensive", () => {

// Verify user is removed from table
await expectToastMessage(context, `User deleted successfully: ${user1.email}`);
await expect(page.getByRole("alertdialog")).not.toBeVisible();
await expect(page.getByRole("alertdialog", { name: "Delete user" })).not.toBeVisible();
await expect(page.locator("tbody").first().first().locator("tr")).toHaveCount(3); // owner + user2 + user3
await expect(page.getByText(user1.email)).not.toBeVisible();
await expect(page.locator("tbody").first()).toContainText(owner.email);
Expand Down Expand Up @@ -520,11 +586,12 @@ test.describe("@comprehensive", () => {
await step("Confirm bulk delete selected users & verify removal")(async () => {
await page.getByRole("button", { name: "Delete 2 users" }).click();

await expect(page.getByRole("alertdialog", { name: "Delete users" })).toBeVisible();
await page.getByRole("button", { name: "Delete" }).click();
const deleteDialog = page.getByRole("alertdialog", { name: "Delete users" });
await expect(deleteDialog).toBeVisible();
await deleteDialog.getByRole("button", { name: "Delete", exact: true }).click();

await expectToastMessage(context, "2 users deleted successfully");
await expect(page.getByRole("alertdialog")).not.toBeVisible();
await expect(deleteDialog).not.toBeVisible();
await expect(page.locator("tbody").first().first().locator("tr")).toHaveCount(1); // Only owner left
await expect(page.getByText(user2.email)).not.toBeVisible();
await expect(page.getByText(user3.email)).not.toBeVisible();
Expand Down
Loading
Loading