Enhance subuser management functionality and improve UI responsiveness

This commit is contained in:
Jeppe Bundgaard
2026-07-13 10:25:20 +02:00
parent 6fb2c9e7df
commit fa9b05958d
14 changed files with 277 additions and 197 deletions
@@ -134,7 +134,7 @@ onBeforeUnmount(() => {
padding: 0 0.3rem;
pointer-events: none;
position: absolute;
right: -0.35rem;
right: -0.55rem;
top: -0.25rem;
z-index: 1;
}
@@ -27,6 +27,7 @@ const props = defineProps({
default: null,
},
});
const emit = defineEmits(["mutated"]);
const paginatedList = usePaginatedList();
provide(PaginatedListKey, paginatedList);
@@ -52,6 +53,10 @@ if (props.hideSearch) {
if (props.autoLoad) {
loadList();
}
const onTableMutated = () => {
emit("mutated");
};
</script>
<template>
@@ -64,6 +69,7 @@ if (props.autoLoad) {
:objects="list"
:show-customer="showCustomer"
:user-scoped-user-id="userScopedUserId"
@mutated="onTableMutated"
/>
</TableLabeledPagination>
</template>
@@ -1,5 +1,6 @@
<script setup>
import Swal from "sweetalert2";
import { computed } from "vue";
import { usePaginatedListInstance } from "@/components/pagination/paginatedList.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
@@ -24,10 +25,12 @@ const props = defineProps({
default: null,
},
});
const emit = defineEmits(["mutated"]);
const { loadList } = usePaginatedListInstance();
const isUserScoped = () => Boolean(props.userScopedUserId);
const shouldRenderGrantRows = () => props.showCustomer || isUserScoped();
const canEditPermissions = () =>
props.showCustomer ? SessionUser.canAccessSuperUser() : SessionUser.canManageSubusers("edit");
const canDisableAccess = () =>
@@ -74,10 +77,54 @@ const patchGrant = async (subuser, payload) => {
const refreshList = async () => {
await loadList();
emit("mutated");
};
const rowTestKey = (subuser) =>
props.showCustomer && subuser?.grant_id ? `${subuser.id}-${subuser.grant_id}` : `${subuser?.id}`;
shouldRenderGrantRows() && subuser?.grant_id ? `${subuser.id}-${subuser.grant_id}` : `${subuser?.id}`;
const normalizeGrantRow = (subuser, grant) => {
const customerNumber = grant?.customer_number ?? grant?.billing_customer_number ?? subuser?.customer_number ?? null;
const assignedVehicle = grant?.assigned_vehicle ?? subuser?.assigned_vehicle ?? null;
return {
...subuser,
grants: [],
grant_id: grant?.grant_id ?? grant?.id ?? subuser?.grant_id ?? null,
grant_enabled: grant?.grant_enabled ?? grant?.enabled ?? subuser?.grant_enabled ?? true,
grant_permissions: grant?.grant_permissions ?? grant?.permissions ?? subuser?.grant_permissions ?? [],
permission_template_key: grant?.permission_template_key ?? subuser?.permission_template_key ?? null,
permission_groups: grant?.permission_groups ?? subuser?.permission_groups ?? [],
grant_note: grant?.grant_note ?? grant?.note ?? subuser?.grant_note ?? "",
customer_number: customerNumber,
billing_customer_number: customerNumber,
customer_name: grant?.customer_name ?? subuser?.customer_name ?? null,
assigned_vehicle_id: grant?.assigned_vehicle_id ?? assignedVehicle?.id ?? subuser?.assigned_vehicle_id ?? null,
assigned_vehicle_reg:
grant?.assigned_vehicle_reg ??
grant?.vehicle_reg ??
assignedVehicle?.reg ??
subuser?.assigned_vehicle_reg ??
null,
assigned_vehicle: assignedVehicle,
dognvask_enabled:
typeof grant?.dognvask_enabled === "boolean" ? grant.dognvask_enabled : subuser?.dognvask_enabled,
access_state: grant?.access_state ?? subuser?.access_state,
can_resend_invite: grant?.can_resend_invite ?? subuser?.can_resend_invite,
};
};
const tableRows = computed(() =>
props.objects.flatMap((subuser) => {
const grants = Array.isArray(subuser?.grants) ? subuser.grants.filter(Boolean) : [];
if (!shouldRenderGrantRows() || grants.length === 0) {
return [subuser];
}
return grants.map((grant) => normalizeGrantRow(subuser, grant));
})
);
const formatDateTime = (dateString) => {
if (!dateString) {
@@ -481,48 +528,21 @@ const onResendInvite = async (subuser) => {
</tr>
</thead>
<tbody>
<tr v-if="props.objects.length === 0">
<tr v-if="tableRows.length === 0">
<td colspan="6" class="has-text-centered has-text-grey py-6">Ingen chauffører fundet.</td>
</tr>
<tr v-for="subuser in props.objects" :key="`${subuser.id}-${subuser.grant_id || 'profile'}`">
<tr v-for="subuser in tableRows" :key="`${subuser.id}-${subuser.grant_id || 'profile'}`">
<td :data-testid="`subuser-name-${rowTestKey(subuser)}`">
<button
v-if="canEditDriverAccount()"
type="button"
class="subusers-table__cell-button has-text-weight-semibold"
:data-testid="`subuser-name-edit-${rowTestKey(subuser)}`"
@click="onEditName(subuser)"
>
{{ subuser.name || "-" }}
</button>
<div v-else class="has-text-weight-semibold">{{ subuser.name || "-" }}</div>
<div class="has-text-weight-semibold">{{ subuser.name || "-" }}</div>
</td>
<td :data-testid="`subuser-phone-${rowTestKey(subuser)}`">
<button
v-if="canEditDriverAccount()"
type="button"
class="subusers-table__cell-button"
:data-testid="`subuser-phone-edit-${rowTestKey(subuser)}`"
@click="onEditContact(subuser)"
>
{{ formatPhone(subuser) }}
</button>
<span v-else>{{ formatPhone(subuser) }}</span>
<span>{{ formatPhone(subuser) }}</span>
</td>
<td class="license-plate" :data-testid="`subuser-plate-${rowTestKey(subuser)}`">
<button
v-if="canEditVehicleAssignment(subuser)"
type="button"
class="subusers-table__cell-button license-plate"
:data-testid="`subuser-plate-edit-${rowTestKey(subuser)}`"
@click="onAssignVehicle(subuser)"
>
{{ formatPlate(subuser) }}
</button>
<span v-else>{{ formatPlate(subuser) }}</span>
<span>{{ formatPlate(subuser) }}</span>
</td>
<td>
@@ -713,25 +733,6 @@ const onResendInvite = async (subuser) => {
text-transform: uppercase;
}
.subusers-table__cell-button {
appearance: none;
background: transparent;
border: 0;
color: inherit;
cursor: pointer;
font: inherit;
line-height: inherit;
padding: 0;
text-align: left;
}
.subusers-table__cell-button:hover,
.subusers-table__cell-button:focus {
color: #0f5aa7;
text-decoration: underline;
text-underline-offset: 0.18rem;
}
.subusers-table__tooltip-trigger {
display: inline-flex;
}
@@ -1,7 +1,7 @@
<script setup>
import MenuDefault from "@/components/menus/MenuDefault.vue";
import {ref, watch} from "vue";
import {SessionUser} from "@/components/session/token/SessionUser.vue";
import { ref, watch } from "vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import i18n from "@/i18n";
// Define the isLoaded function
@@ -10,308 +10,292 @@ import i18n from "@/i18n";
const isLoaded = ref(true);
const doesRouteStartWith = (route) => {
return window.location.pathname.startsWith(route);
}
};
const t = (key, fallback = key) => {
const value = i18n.global.t(key);
return value === key ? fallback : value;
}
};
const menu_items = ref([
{
name: SessionUser.objects.departments.meta.title,
route: '/superuser' +SessionUser.objects.departments.meta.endpoint,
route: "/superuser" + SessionUser.objects.departments.meta.endpoint,
icon: SessionUser.objects.departments.meta.icon,
children: []
children: [],
},
{
name: 'Nummerplade scanning',
route: '/superuser/scanners',
icon: 'fas fa-cogs',
children: []
name: "Nummerplade scanning",
route: "/superuser/scanners",
icon: "fas fa-cogs",
children: [],
},
{
name: 'Kassesystem',
permissions: [
'canAccessSuperUser'
],
icon: 'fas fa-cogs',
name: "Kassesystem",
permissions: ["canAccessSuperUser"],
icon: "fas fa-cogs",
options: [
{
label: SessionUser.objects.collectedOrderInvoices.meta.title,
value: SessionUser.objects.collectedOrderInvoices.meta.endpoint,
icon: SessionUser.objects.collectedOrderInvoices.meta.icon,
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.objects.products.meta.title,
value: SessionUser.objects.products.meta.endpoint,
icon: SessionUser.objects.products.meta.icon,
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.objects.orders.meta.title,
value: SessionUser.objects.orders.meta.endpoint,
icon: SessionUser.objects.orders.meta.icon,
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.objects.categories.meta.title,
value: SessionUser.objects.categories.meta.endpoint,
icon: SessionUser.objects.categories.meta.icon,
children: [],
hidden: false
hidden: false,
},
],
onSelect: async (value) => {
SessionUser.functions.redirectTo.superUser(value);
},
hidden: false
hidden: false,
},
{
name: 'Brugere',
permissions: [
'canAccessSuperUser'
],
icon: 'fas fa-cogs',
name: "Brugere",
permissions: ["canAccessSuperUser"],
icon: "fas fa-cogs",
options: [
{
label: 'Kunder',
value: '/customers',
icon: 'fas fa-users',
label: "Kunder",
value: "/customers",
icon: "fas fa-users",
children: [],
hidden: false
hidden: false,
},
{
label: 'Medarbejdere',
value: '/users',
icon: 'fas fa-users-cog',
label: "Medarbejdere",
value: "/users",
icon: "fas fa-users-cog",
children: [],
hidden: false
hidden: false,
},
{
label: 'Chauffører',
value: '/subusers',
icon: 'fas fa-id-card',
label: "Chauffører",
value: "/subusers",
icon: "fas fa-id-card",
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.objects.roles.meta.title,
value: SessionUser.objects.roles.meta.endpoint,
icon: SessionUser.objects.roles.meta.icon,
children: [],
hidden: false
hidden: false,
},
],
onSelect: async (value) => {
SessionUser.functions.redirectTo.superUser(value);
},
hidden: false
hidden: false,
},
{
name: 'Konfiguration',
permissions: [
'canAccessSuperUser'
],
icon: 'fas fa-cogs',
name: "Konfiguration",
permissions: ["canAccessSuperUser"],
icon: "fas fa-cogs",
options: [
{
label: SessionUser.superUser.modules.economic.meta.title,
value: SessionUser.superUser.modules.economic.meta.config_endpoint,
icon: SessionUser.superUser.modules.economic.meta.icon,
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.superUser.modules.reCAPTCHA.meta.title,
value: SessionUser.superUser.modules.reCAPTCHA.meta.config_endpoint,
icon: SessionUser.superUser.modules.reCAPTCHA.meta.icon,
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.superUser.modules.email.meta.title,
value: SessionUser.superUser.modules.email.meta.config_endpoint,
icon: SessionUser.superUser.modules.email.meta.icon,
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.superUser.modules.backups.meta.title,
value: SessionUser.superUser.modules.backups.meta.config_endpoint,
icon: SessionUser.superUser.modules.backups.meta.icon,
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.superUser.modules.coolify.meta.title,
value: SessionUser.superUser.modules.coolify.meta.config_endpoint,
icon: SessionUser.superUser.modules.coolify.meta.icon,
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.superUser.modules.releasemanager.meta.title,
value: SessionUser.superUser.modules.releasemanager.meta.config_endpoint,
icon: SessionUser.superUser.modules.releasemanager.meta.icon,
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.superUser.modules.motorapi.meta.title,
value: SessionUser.superUser.modules.motorapi.meta.config_endpoint,
icon: SessionUser.superUser.modules.motorapi.meta.icon,
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.superUser.modules.stripe.meta.title,
value: SessionUser.superUser.modules.stripe.meta.config_endpoint,
icon: SessionUser.superUser.modules.stripe.meta.icon,
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.superUser.modules.weatherapi.meta.title,
value: SessionUser.superUser.modules.weatherapi.meta.config_endpoint,
icon: SessionUser.superUser.modules.weatherapi.meta.icon,
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.superUser.modules.workfeed.meta.title,
value: SessionUser.superUser.modules.workfeed.meta.config_endpoint,
icon: SessionUser.superUser.modules.workfeed.meta.icon,
children: [],
hidden: false
hidden: false,
},
],
onSelect: async (value) => {
SessionUser.functions.redirectTo.superUser(value);
},
hidden: false
hidden: false,
},
{
name: t("cron.system_nav", "System"),
permissions: [
'canAccessSuperUser'
],
icon: 'fas fa-shield-alt',
permissions: ["canAccessSuperUser"],
icon: "fas fa-shield-alt",
options: [
{
label: t("system_status.title", "System status"),
value: null,
icon: 'fas fa-heartbeat',
icon: "fas fa-heartbeat",
children: [],
hidden: false
hidden: false,
},
{
label: t("cron.nav", "Cron tasks"),
value: '/system/cron',
icon: 'fas fa-clock',
value: "/system/cron",
icon: "fas fa-clock",
children: [],
hidden: false
hidden: false,
},
{
label: t("system_status.cards.database", "Database"),
value: '/system/database',
icon: 'fas fa-database',
value: "/system/database",
icon: "fas fa-database",
children: [],
hidden: false
hidden: false,
},
{
label: t("system_status.cards.minio", "MinIO"),
value: '/system/minio',
icon: 'fas fa-archive',
value: "/system/minio",
icon: "fas fa-archive",
children: [],
hidden: false
hidden: false,
},
{
label: t("system_status.cards.redis", "Redis"),
value: '/system/redis',
icon: 'fas fa-memory',
value: "/system/redis",
icon: "fas fa-memory",
children: [],
hidden: false
hidden: false,
},
{
label: t("security.nav", "Security"),
value: '/system/security/overview',
icon: 'fas fa-shield-alt',
value: "/system/security/overview",
icon: "fas fa-shield-alt",
children: [],
hidden: false
hidden: false,
},
],
onSelect: async (value) => {
SessionUser.functions.redirectTo.superUser(value);
},
hidden: false
hidden: false,
},
{
name: 'XL Vask',
permissions: [
'canAccessSuperUser'
],
icon: 'fas fa-cogs',
name: "XL Vask",
permissions: ["canAccessSuperUser"],
icon: "fas fa-cogs",
options: [
{
label: 'Dashboard',
value: '/xlvask',
icon: 'fas fa-tachometer-alt',
label: "Dashboard",
value: "/xlvask",
icon: "fas fa-tachometer-alt",
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.objects.global.language.customers,
value: '/xlvask/customers',
icon: 'fas fa-users',
value: "/xlvask/customers",
icon: "fas fa-users",
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.objects.global.language.usage_log,
value: '/xlvask/usagelogs',
icon: 'fas fa-users',
value: "/xlvask/usagelogs",
icon: "fas fa-users",
children: [],
hidden: false
hidden: false,
},
{
label: SessionUser.objects.global.language.configuration,
value: SessionUser.superUser.modules.xlvask.meta.config_endpoint,
icon: SessionUser.superUser.modules.xlvask.meta.icon,
children: [],
hidden: false
hidden: false,
},
],
onSelect: async (value) => {
SessionUser.functions.redirectTo.superUser(value);
},
hidden: false
hidden: false,
},
]);
const selectDepartment = (department) => {
// Get the current path (After /admin/:department_id)
const currentPath = window.location.pathname.split('/').slice(3).join('/');
console.log('Selected department: ' + department);
const currentPath = window.location.pathname.split("/").slice(3).join("/");
console.log("Selected department: " + department);
menu_items.value[1].selected = department;
// Hide the menu
menu_items.value[1].isExpanded = false;
// Redirect to the department
SessionUser.functions.redirectTo.department(department, currentPath);
}
};
</script>
<template>
<MenuDefault v-if="isLoaded"
v-bind:menu_items="menu_items"
:show_icons="false"
/>
<MenuDefault v-if="isLoaded" v-bind:menu_items="menu_items" :show_icons="false" />
</template>
<style scoped>
</style>
<style scoped></style>
+1
View File
@@ -4679,6 +4679,7 @@
},
"pagination": {
"archive": "Arkiv",
"clear_all": "Ryd alle",
"ascending": "Stigende (Ældste @:{'words.generated.først'})",
"booking_status": "Bogføringsstatus",
"bookings_overview": "@:{'templates.generated.compat.objects.bookings.description'}",
+1
View File
@@ -4793,6 +4793,7 @@
},
"pagination": {
"archive": "Arkiv",
"clear_all": "Tøm alle",
"ascending": "Stigende (eldst @:{'words.generated.først'})",
"booking_status": "Bestillingsstatus",
"bookings_overview": "Bestillingsoversikt",
@@ -2,6 +2,7 @@
"compat": {
"pagination": {
"archive": "Arkiv",
"clear_all": "Ryd alle",
"ascending": "Stigende (Ældste @:{'terms.glossary.først'})",
"booking_status": "Bogføringsstatus",
"bookings_overview": "@:{'phrases.compat.objects.bookings.description'}",
@@ -2,6 +2,7 @@
"compat": {
"pagination": {
"archive": "Arkiv",
"clear_all": "Tøm alle",
"ascending": "Stigende (eldst @:{'terms.glossary.først'})",
"booking_status": "Bestillingsstatus",
"bookings_overview": "Bestillingsoversikt",
@@ -171,6 +171,7 @@ watch(
:endpoint="endpoint"
:user-scoped-user-id="routeUserId"
auto-load="true"
@mutated="loadSummary"
/>
</SuperuserOverviewPanel>
</template>
+19 -10
View File
@@ -886,8 +886,10 @@ test("customer user with own-subuser permissions can invite and manage grant acc
await page.getByTestId("subuser-dognvask-tooltip-3").hover();
await expect(page.locator(".tooltip-content:visible")).toContainText("Kundeadgangen er deaktiveret");
await page.mouse.move(0, 0);
await page.getByTestId("subuser-remove-2").hover();
await expect(page.locator(".tooltip-content", { hasText: "Deaktiverer chaufførens kundeadgang" })).toBeVisible();
await page.locator(".b-tooltip", { has: page.getByTestId("subuser-remove-2") }).hover();
await expect(
page.locator(".tooltip-content:visible", { hasText: "Deaktiverer chaufførens kundeadgang" }).first()
).toContainText("Deaktiverer chaufførens kundeadgang");
await page.getByRole("button", { name: /Invit.*chauff/i }).click();
await page.fill("#subuser-form-name", "Invited Driver");
@@ -901,16 +903,17 @@ test("customer user with own-subuser permissions can invite and manage grant acc
await expect(page.getByTestId("subuser-phone-100")).toHaveText("+45 44444444");
await expect(page.getByTestId("subuser-plate-100")).toHaveText("-");
await page.getByTestId("subuser-plate-edit-100").click();
await page.selectOption("#subuser-assigned-vehicle", "104");
await page.getByRole("button", { name: "Gem" }).click();
await expect(page.getByTestId("subuser-plate-100")).toHaveText("NY44444");
let actions = await openSubuserActions(page, 100);
await expect(actions.getByTestId("subuser-section-account-100")).toHaveCount(0);
await expect(actions.getByTestId("subuser-section-vehicle-100")).toBeVisible();
await expect(actions.getByTestId("subuser-section-grant-100")).toBeVisible();
await expect(actions.getByTestId("subuser-section-details-100")).toBeVisible();
await actions.getByTestId("subuser-assign-vehicle-100").click();
await page.selectOption("#subuser-assigned-vehicle", "104");
await page.getByRole("button", { name: "Gem" }).click();
await expect(page.getByTestId("subuser-plate-100")).toHaveText("NY44444");
actions = await openSubuserActions(page, 100);
await actions.getByTestId("subuser-permissions-100").click();
await expect(page.getByText("Adgangsprofil for Invited Driver")).toBeVisible();
await expect(page.getByTestId("permission-template-driver")).toBeVisible();
@@ -985,14 +988,19 @@ test("superusers can list and invite chauffeurs across customers", async ({ page
await expect(actions.getByTestId("subuser-section-details-51-510")).toBeVisible();
await expect(actions).toContainText("12345678 - Nordic Transport");
await expect(actions.getByTestId("subuser-resend-51-510")).toBeVisible();
await expect(page.getByTestId("subuser-name-edit-51-510")).toHaveCount(0);
await expect(page.getByTestId("subuser-phone-edit-51-510")).toHaveCount(0);
await expect(page.getByTestId("subuser-plate-edit-51-510")).toHaveCount(0);
await expect(page.getByTestId("subuser-dognvask-51-511")).toBeVisible();
await page.getByTestId("subuser-name-edit-51-510").click();
await actions.getByTestId("subuser-edit-name-51-510").click();
await page.fill(".swal2-input", "Renamed Super Driver");
await page.getByRole("button", { name: "Gem" }).click();
await expect(page.getByTestId("subuser-name-51-510")).toContainText("Renamed Super Driver");
expect(profilePayloads).toContainEqual({ id: 51, payload: { name: "Renamed Super Driver" } });
await page.getByTestId("subuser-phone-edit-51-510").click();
actions = await openSubuserActions(page, "51-510");
await actions.getByTestId("subuser-edit-contact-51-510").click();
await page.fill("#subuser-admin-email", "renamed@example.com");
await page.fill("#subuser-admin-phone-country-code", "46");
await page.fill("#subuser-admin-phone", "12312312");
@@ -1007,7 +1015,8 @@ test("superusers can list and invite chauffeurs across customers", async ({ page
},
});
await page.getByTestId("subuser-plate-edit-51-510").click();
actions = await openSubuserActions(page, "51-510");
await actions.getByTestId("subuser-assign-vehicle-51-510").click();
await page.selectOption("#subuser-assigned-vehicle", "5102");
await page.getByRole("button", { name: "Gem" }).click();
await expect(page.getByTestId("subuser-plate-51-510")).toHaveText("NT24680");
+97 -28
View File
@@ -88,6 +88,7 @@ test.describe("superuser order date filters", () => {
await expect(otherFiltersButton).toBeVisible();
await expect(otherFiltersButton).not.toHaveClass(/is-small/);
const otherFiltersCount = page.getByTestId("invoice-orders-other-filters-count");
const clearOtherFiltersButton = page.getByTestId("invoice-orders-other-filters-clear");
await expect(otherFiltersCount).toHaveText("3");
await expect(anytimeDateShortcut).toBeVisible();
await expect(todayDateShortcut).toBeVisible();
@@ -107,8 +108,8 @@ test.describe("superuser order date filters", () => {
const shortcutCenterY = shortcutBox.y + shortcutBox.height / 2;
const otherToAnytimeGap = shortcutBox.x - (buttonBox.x + buttonBox.width);
const dateShortcutGap = todayShortcutBox.x - (shortcutBox.x + shortcutBox.width);
const doesCountProtrudeFromTopRight =
countBox.y < buttonBox.y && countBox.x + countBox.width > buttonBox.x + buttonBox.width;
const countRightProtrusion = countBox.x + countBox.width - (buttonBox.x + buttonBox.width);
const doesCountProtrudeFromTopRight = countBox.y < buttonBox.y && countRightProtrusion >= 8;
const hasCountTopClearance = countBox.y >= shortcutActionsBox.y + 2;
return (
@@ -120,35 +121,29 @@ test.describe("superuser order date filters", () => {
);
})
.toBe(true);
await expect(otherFiltersMenu).toBeHidden();
await otherFiltersButton.click();
await expect(otherFiltersMenu).toBeVisible();
for (const label of [
"Rækkefølge",
"Bogføringsstatus",
"Betalingsprocessor",
"Fuldført",
"Faktura valg",
"Specialaftale",
"Vaskeabonnement",
]) {
await expect(otherFiltersLabel(label)).toBeVisible();
const verifiedButtonBox = await otherFiltersButton.boundingBox();
const verifiedCountBox = await otherFiltersCount.boundingBox();
const verifiedShortcutBox = await anytimeDateShortcut.boundingBox();
if (!verifiedButtonBox || !verifiedCountBox || !verifiedShortcutBox) {
throw new Error("Unable to capture verified other filters badge screenshot");
}
await page.getByTestId("invoice-orders-other-filter-processor").selectOption("2");
await expect(otherFiltersCount).toHaveText("4");
await expect.poll(() => orderRequests.some((request) => request.filters.includes("processor:2"))).toBeTruthy();
await page.keyboard.press("Escape");
const verifiedTop = Math.min(verifiedButtonBox.y, verifiedCountBox.y);
const verifiedBottom = Math.max(
verifiedButtonBox.y + verifiedButtonBox.height,
verifiedCountBox.y + verifiedCountBox.height,
verifiedShortcutBox.y + verifiedShortcutBox.height
);
await page.screenshot({
path: "output/playwright/manual/invoice-orders-other-filters-badge-right-protrusion.png",
clip: {
x: Math.max(0, verifiedButtonBox.x - 28),
y: Math.max(0, verifiedTop - 24),
width: Math.ceil(verifiedShortcutBox.x + verifiedShortcutBox.width - verifiedButtonBox.x + 56),
height: Math.ceil(verifiedBottom - verifiedTop + 48),
},
});
await expect(otherFiltersMenu).toBeHidden();
await expect
.poll(() =>
orderRequests.some((request) => request.limit === 1000 && !request.filters.includes("created_at-date_"))
)
.toBeTruthy();
const fromField = page.getByTestId("invoice-orders-date-from");
const fromInput = fromField.locator("input").first();
const toInput = page.getByTestId("invoice-orders-date-to").locator("input").first();
@@ -177,6 +172,80 @@ test.describe("superuser order date filters", () => {
.poll(() => orderRequests.some((request) => request.filters.includes("created_at-date_from:2026-07-02")))
.toBeTruthy();
await otherFiltersButton.click();
await expect(otherFiltersMenu).toBeVisible();
for (const label of [
"Rækkefølge",
"Bogføringsstatus",
"Betalingsprocessor",
"Fuldført",
"Faktura valg",
"Specialaftale",
"Vaskeabonnement",
]) {
await expect(otherFiltersLabel(label)).toBeVisible();
}
await expect(clearOtherFiltersButton).toBeVisible();
await expect(clearOtherFiltersButton).toHaveText("Ryd alle");
await expect
.poll(async () => {
const menuBox = await otherFiltersMenu.boundingBox();
const clearBox = await clearOtherFiltersButton.boundingBox();
const lastSelectBox = await page.getByTestId("invoice-orders-other-filter-wash-subscription").boundingBox();
if (!menuBox || !clearBox || !lastSelectBox) {
return false;
}
return (
clearBox.y > lastSelectBox.y + lastSelectBox.height &&
clearBox.y + clearBox.height <= menuBox.y + menuBox.height
);
})
.toBe(true);
await page.getByTestId("invoice-orders-other-filter-processor").selectOption("2");
await expect(otherFiltersCount).toHaveText("4");
await expect.poll(() => orderRequests.some((request) => request.filters.includes("processor:2"))).toBeTruthy();
const requestCountBeforeClear = orderRequests.length;
await clearOtherFiltersButton.click();
await expect.poll(() => orderRequests.length > requestCountBeforeClear).toBeTruthy();
await expect(page.getByTestId("invoice-orders-other-filters-count")).toHaveCount(0);
await expect(page.getByTestId("invoice-orders-other-filter-order-direction")).toHaveValue("desc");
for (const filterTestId of [
"invoice-orders-other-filter-booked-invoice-id",
"invoice-orders-other-filter-processor",
"invoice-orders-other-filter-completed-at",
"invoice-orders-other-filter-invoice-selection",
"invoice-orders-other-filter-special-agreement",
"invoice-orders-other-filter-wash-subscription",
]) {
await expect(page.getByTestId(filterTestId)).toHaveValue("*");
}
await expect(fromInput).toHaveValue("02.07.2026");
await expect(toInput).toHaveValue("");
await expect
.poll(() => {
const latestTableRequest = [...orderRequests].reverse().find((request) => request.limit !== 1000);
return Boolean(
latestTableRequest?.filters.includes("created_at-date_from:2026-07-02") &&
!latestTableRequest.filters.includes("processor:2")
);
})
.toBeTruthy();
await page.keyboard.press("Escape");
await expect(otherFiltersMenu).toBeHidden();
await expect
.poll(() =>
orderRequests.some((request) => request.limit === 1000 && !request.filters.includes("created_at-date_"))
)
.toBeTruthy();
await fromField.locator(".icon.is-right").click();
await expect(fromInput).toHaveValue("");
+3
View File
@@ -210,6 +210,9 @@ test.describe("Superuser system security", () => {
await page.goto("/superuser/system/security/overview");
await expect(page.getByTestId("superuser-security-page")).toBeVisible();
const desktopNavigation = page.getByTestId("desktop-buefy-navigation");
await expect(desktopNavigation.getByText("System", { exact: true })).toBeVisible();
await expect(desktopNavigation.getByText("Security", { exact: true })).toBeVisible();
await expect(page.getByText("Security overview")).toBeVisible();
await expect(page.getByRole("link", { name: "Open incidents" })).toBeVisible();
await expect(page.getByText("Block firewall rule matched: ip 203.0.113.9")).toBeVisible();
+3
View File
@@ -433,6 +433,7 @@ describe("DatePeriodSelector month warning", () => {
showUpdateButton: false,
showStartDate: false,
showEndDate: false,
showToLabel: false,
showSelectionValidity: false,
showMonthSelector: false,
showYearSelector: false,
@@ -450,6 +451,8 @@ describe("DatePeriodSelector month warning", () => {
expect(anytimeButton.classes()).toContain("is-info");
expect(todayButton.classes()).not.toContain("is-info");
expect(wrapper.find(".level").exists()).toBe(false);
expect(wrapper.get("[data-testid='date-period-shortcut-anytime']").exists()).toBe(true);
});
it("hides empty range controls unless the selector is optional", () => {
@@ -67,7 +67,7 @@ describe("NavigationMenuItemsAdmin contract", () => {
expect(superuserSource).not.toContain('to: "/superuser/system/replication"');
expect(superuserLeftMenuSource).toContain('t("security.nav", "Security")');
expect(superuserLeftMenuSource).toContain("value: '/system/security/overview'");
expect(superuserLeftMenuSource).toContain('value: "/system/security/overview"');
expect(superuserLeftMenuSource).toContain('t("cron.system_nav", "System")');
});
});