Improve superuser role permissions page (#149)

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
This commit is contained in:
Jeppe B
2026-07-06 12:43:03 +02:00
committed by GitHub
co-authored by Jeppe Bundgaard
parent 987acc5b0f
commit 22f8b5f203
19 changed files with 2345 additions and 246 deletions
+10 -1
View File
@@ -7,7 +7,7 @@ export const fallbackChangePatterns = [
/^vite\.config\.js$/u,
/^playwright(?:\..+)?\.config\.(?:js|ts)$/u,
/^playwright\.global-(?:setup|teardown)\.mjs$/u,
/^scripts\/run-playwright/u,
/^scripts\/run-playwright-(?:pr|ci-parallel|batched-chromium)\.mjs$/u,
/^tests\/e2e\/(?:support|fixtures)\//u,
];
@@ -24,6 +24,15 @@ export const sourceMappings = [
specs: ["tests/e2e/navigation.smoke.spec.js"],
projects: chromiumProjects,
},
{
name: "superuser-roles-permissions",
patterns: [
/^src\/views\/dashboards\/superUserDashboard\/roles\/SuperUserRolesPermissions\.vue$/u,
/^src\/views\/dashboards\/superUserDashboard\/roles\/rolePermissionCatalog\.js$/u,
],
specs: ["tests/e2e/superuser-roles-permissions.spec.ts"],
projects: chromiumProjects,
},
{
name: "booking",
patterns: [/bookings?/iu, /time-bookings/iu, /^src\/views\/guest\/book\//u],
+1
View File
@@ -106,6 +106,7 @@ export const ownedFilesByRole = {
"superuser-departments-archive.spec.ts",
"superuser-drafts.spec.ts",
"superuser-products-layout.spec.ts",
"superuser-roles-permissions.spec.ts",
"superuser-system-status.smoke.spec.js",
"superuser-users.spec.ts",
"superuser-vehicles.smoke.spec.js",
+91
View File
@@ -4846,6 +4846,97 @@
"subtitle": "@.capitalize:{'words.generated.administreret'} @:{'words.replication.services.database'}-, @:{'words.replication.services.redis'}- @:{'words.generated.og'} @:{'words.replication.services.minio'}-@:{'words.generated.replikering'}"
},
"roles": {
"permissions": {
"access_scope": "Adgangsomrade",
"actions": {
"clear_all": "Ryd gruppe",
"reload": "Genindlaes",
"retry": "Prov igen",
"select_all": "Vaelg gruppe"
},
"columns": {
"enabled": "Aktiveret",
"permission": "Tilladelse",
"route": "Rute",
"status": "Status"
},
"department_access_description": "Giver adgang til {name}.",
"empty": "Der er ingen tilladelser tilgaengelige.",
"empty_group": "Denne gruppe har ingen tilladelser.",
"errors": {
"load": "Rolletilladelser kunne ikke indlaeses.",
"update": "En eller flere tilladelser kunne ikke opdateres. Rollens aktuelle tilladelser blev genindlaest."
},
"filtered_summary": "{count} matcher",
"groups": {
"access": {
"description": "Sidetype- og afdelingsadgang.",
"label": "Adgang"
},
"bookings": {
"description": "Tilladelser til bookinger, tidsbookinger og planlaegning.",
"label": "Bookinger"
},
"customers_users": {
"description": "Tilladelser til kunder, brugere, medarbejdere, roller og login.",
"label": "Kunder og brugere"
},
"departments_operations": {
"description": "Afdelingsdrift, baner, relaer, notifikationer, rapporter og mal.",
"label": "Afdelinger og drift"
},
"invoicing_economic": {
"description": "Tilladelser til fakturaer, faktureringsperioder, opkraevning og e-conomic.",
"label": "Fakturering og e-conomic"
},
"orders_pos": {
"description": "Tilladelser til ordrer, POS, betalinger og ordrelinjer.",
"label": "Ordrer og POS"
},
"other": {
"description": "Tilladelser, der ikke matcher en anden gruppe.",
"label": "Andet"
},
"products_pricing": {
"description": "Tilladelser til produkter, priser, rabatter og valuta.",
"label": "Produkter og priser"
},
"selfserve_edge": {
"description": "Tilladelser til selvbetjening, vask, scannere, gateways og edge-udstyr.",
"label": "Selvbetjening og edge"
},
"system_config": {
"description": "System, konfiguration, release, replikering og integrationer.",
"label": "System og konfiguration"
}
},
"no_description": "Der er ingen beskrivelse.",
"no_search_results": "Ingen tilladelser matcher sogningen.",
"page_access": {
"admin": {
"description": "Giver adgang til administrationssider.",
"label": "Admin"
},
"superuser": {
"description": "Giver adgang til superbrugersider.",
"label": "Superbruger"
},
"user": {
"description": "Giver adgang til brugersider.",
"label": "Bruger"
}
},
"search_label": "Sog i tilladelser",
"search_placeholder": "Sog pa label, nogle, beskrivelse, rute eller metode",
"selected_summary": "{selected} af {total} valgt",
"status": {
"disabled": "Deaktiveret",
"enabled": "Aktiveret"
},
"subtitle": "Rolletilladelser",
"title": "Tilladelser for rolle {id}",
"title_with_name": "Tilladelser for {name}"
},
"subtitle": "@.capitalize:{'words.generated.brugerroller'}"
},
"self_wash": {
+91
View File
@@ -4957,6 +4957,97 @@
"subtitle": "@.capitalize:{'words.generated.managed'} @:{'words.generated.database'} @:{'words.generated.and'} @:{'words.replication.services.redis'} @:{'words.generated.replication'}"
},
"roles": {
"permissions": {
"access_scope": "Zugriffsbereich",
"actions": {
"clear_all": "Gruppe leeren",
"reload": "Neu laden",
"retry": "Erneut versuchen",
"select_all": "Gruppe auswaehlen"
},
"columns": {
"enabled": "Aktiviert",
"permission": "Berechtigung",
"route": "Route",
"status": "Status"
},
"department_access_description": "Erlaubt Zugriff auf {name}.",
"empty": "Es sind keine Berechtigungen verfuegbar.",
"empty_group": "Diese Gruppe hat keine Berechtigungen.",
"errors": {
"load": "Rollenberechtigungen konnten nicht geladen werden.",
"update": "Eine oder mehrere Berechtigungen konnten nicht aktualisiert werden. Die aktuellen Rollenberechtigungen wurden neu geladen."
},
"filtered_summary": "{count} Treffer",
"groups": {
"access": {
"description": "Seitentyp- und Abteilungszugriff.",
"label": "Zugriff"
},
"bookings": {
"description": "Berechtigungen fuer Buchungen, Zeitbuchungen und Planung.",
"label": "Buchungen"
},
"customers_users": {
"description": "Berechtigungen fuer Kunden, Benutzer, Mitarbeiter, Rollen und Anmeldung.",
"label": "Kunden und Benutzer"
},
"departments_operations": {
"description": "Abteilungsbetrieb, Spuren, Relais, Benachrichtigungen, Berichte und Ziele.",
"label": "Abteilungen und Betrieb"
},
"invoicing_economic": {
"description": "Berechtigungen fuer Rechnungen, Abrechnungsperioden, Inkasso und e-conomic.",
"label": "Rechnungen und e-conomic"
},
"orders_pos": {
"description": "Berechtigungen fuer Auftraege, POS, Zahlungen und Auftragszeilen.",
"label": "Auftraege und POS"
},
"other": {
"description": "Berechtigungen, die keiner anderen Gruppe entsprechen.",
"label": "Sonstige"
},
"products_pricing": {
"description": "Berechtigungen fuer Produkte, Preise, Rabatte und Waehrungen.",
"label": "Produkte und Preise"
},
"selfserve_edge": {
"description": "Berechtigungen fuer Self-Service, Waschen, Scanner, Gateways und Edge-Hardware.",
"label": "Self-Service und Edge"
},
"system_config": {
"description": "System, Konfiguration, Release, Replikation und Integrationen.",
"label": "System und Konfiguration"
}
},
"no_description": "Keine Beschreibung verfuegbar.",
"no_search_results": "Keine Berechtigungen entsprechen der Suche.",
"page_access": {
"admin": {
"description": "Erlaubt Zugriff auf Admin-Dashboardseiten.",
"label": "Admin"
},
"superuser": {
"description": "Erlaubt Zugriff auf Superuser-Dashboardseiten.",
"label": "Superuser"
},
"user": {
"description": "Erlaubt Zugriff auf Benutzer-Dashboardseiten.",
"label": "Benutzer"
}
},
"search_label": "Berechtigungen suchen",
"search_placeholder": "Nach Label, Schluessel, Beschreibung, Route oder Methode suchen",
"selected_summary": "{selected} von {total} ausgewaehlt",
"status": {
"disabled": "Deaktiviert",
"enabled": "Aktiviert"
},
"subtitle": "Rollenberechtigungen",
"title": "Berechtigungen fuer Rolle {id}",
"title_with_name": "Berechtigungen fuer {name}"
},
"subtitle": "Benutzerrollen"
},
"self_wash": {
+91
View File
@@ -4678,6 +4678,97 @@
"subtitle": "@.capitalize:{'words.generated.managed'} @:{'words.replication.services.database'}, @:{'words.replication.services.redis'}, @:{'words.generated.and'} @:{'words.replication.services.minio'} @:{'words.generated.replication'}"
},
"roles": {
"permissions": {
"access_scope": "Access scope",
"actions": {
"clear_all": "Clear group",
"reload": "Reload",
"retry": "Try again",
"select_all": "Select group"
},
"columns": {
"enabled": "Enabled",
"permission": "Permission",
"route": "Route",
"status": "Status"
},
"department_access_description": "Allows access to {name}.",
"empty": "No permissions are available.",
"empty_group": "This group has no permissions.",
"errors": {
"load": "Could not load role permissions.",
"update": "Could not update one or more permissions. The current role permissions were reloaded."
},
"filtered_summary": "{count} matching",
"groups": {
"access": {
"description": "Page type and department access permissions.",
"label": "Access"
},
"bookings": {
"description": "Booking, time booking, and planning permissions.",
"label": "Bookings"
},
"customers_users": {
"description": "Customer, user, employee, role, and authentication permissions.",
"label": "Customers and users"
},
"departments_operations": {
"description": "Department operations, lanes, relays, notifications, reports, and goals.",
"label": "Departments and operations"
},
"invoicing_economic": {
"description": "Invoice, billing period, collection, and e-conomic permissions.",
"label": "Invoicing and e-conomic"
},
"orders_pos": {
"description": "Order, POS, payment, and order line permissions.",
"label": "Orders and POS"
},
"other": {
"description": "Permissions that do not match another group.",
"label": "Other"
},
"products_pricing": {
"description": "Product, price, discount, and currency permissions.",
"label": "Products and pricing"
},
"selfserve_edge": {
"description": "Self-serve, wash, scanner, gateway, and edge hardware permissions.",
"label": "Self-serve and edge"
},
"system_config": {
"description": "System, configuration, release, replication, and integration permissions.",
"label": "System and configuration"
}
},
"no_description": "No description is available.",
"no_search_results": "No permissions match the current search.",
"page_access": {
"admin": {
"description": "Allows access to admin dashboard pages.",
"label": "Admin"
},
"superuser": {
"description": "Allows access to superuser dashboard pages.",
"label": "Superuser"
},
"user": {
"description": "Allows access to user dashboard pages.",
"label": "User"
}
},
"search_label": "Search permissions",
"search_placeholder": "Search by label, key, description, route, or method",
"selected_summary": "{selected} of {total} selected",
"status": {
"disabled": "Disabled",
"enabled": "Enabled"
},
"subtitle": "Role permissions",
"title": "Role {id} permissions",
"title_with_name": "{name} permissions"
},
"subtitle": "@.capitalize:{'words.generated.user'} @:{'words.generated.roles'}"
},
"self_wash": {
+91
View File
@@ -4255,6 +4255,97 @@
"title": "@:replication.labels.replication"
},
"roles": {
"permissions": {
"access_scope": "@:{'templates.generated.compat.roles.permissions.access_scope'}",
"actions": {
"clear_all": "@:{'templates.generated.compat.roles.permissions.actions.clear_all'}",
"reload": "@:{'templates.generated.compat.roles.permissions.actions.reload'}",
"retry": "@:{'templates.generated.compat.roles.permissions.actions.retry'}",
"select_all": "@:{'templates.generated.compat.roles.permissions.actions.select_all'}"
},
"columns": {
"enabled": "@:{'templates.generated.compat.roles.permissions.columns.enabled'}",
"permission": "@:{'templates.generated.compat.roles.permissions.columns.permission'}",
"route": "@:{'templates.generated.compat.roles.permissions.columns.route'}",
"status": "@:{'templates.generated.compat.roles.permissions.columns.status'}"
},
"department_access_description": "@:{'templates.generated.compat.roles.permissions.department_access_description'}",
"empty": "@:{'templates.generated.compat.roles.permissions.empty'}",
"empty_group": "@:{'templates.generated.compat.roles.permissions.empty_group'}",
"errors": {
"load": "@:{'templates.generated.compat.roles.permissions.errors.load'}",
"update": "@:{'templates.generated.compat.roles.permissions.errors.update'}"
},
"filtered_summary": "@:{'templates.generated.compat.roles.permissions.filtered_summary'}",
"groups": {
"access": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.access.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.access.label'}"
},
"bookings": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.bookings.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.bookings.label'}"
},
"customers_users": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.customers_users.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.customers_users.label'}"
},
"departments_operations": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.departments_operations.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.departments_operations.label'}"
},
"invoicing_economic": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.invoicing_economic.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.invoicing_economic.label'}"
},
"orders_pos": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.orders_pos.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.orders_pos.label'}"
},
"other": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.other.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.other.label'}"
},
"products_pricing": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.products_pricing.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.products_pricing.label'}"
},
"selfserve_edge": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.selfserve_edge.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.selfserve_edge.label'}"
},
"system_config": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.system_config.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.system_config.label'}"
}
},
"no_description": "@:{'templates.generated.compat.roles.permissions.no_description'}",
"no_search_results": "@:{'templates.generated.compat.roles.permissions.no_search_results'}",
"page_access": {
"admin": {
"description": "@:{'templates.generated.compat.roles.permissions.page_access.admin.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.page_access.admin.label'}"
},
"superuser": {
"description": "@:{'templates.generated.compat.roles.permissions.page_access.superuser.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.page_access.superuser.label'}"
},
"user": {
"description": "@:{'templates.generated.compat.roles.permissions.page_access.user.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.page_access.user.label'}"
}
},
"search_label": "@:{'templates.generated.compat.roles.permissions.search_label'}",
"search_placeholder": "@:{'templates.generated.compat.roles.permissions.search_placeholder'}",
"selected_summary": "@:{'templates.generated.compat.roles.permissions.selected_summary'}",
"status": {
"disabled": "@:{'templates.generated.compat.roles.permissions.status.disabled'}",
"enabled": "@:{'templates.generated.compat.roles.permissions.status.enabled'}"
},
"subtitle": "@:{'templates.generated.compat.roles.permissions.subtitle'}",
"title": "@:{'templates.generated.compat.roles.permissions.title'}",
"title_with_name": "@:{'templates.generated.compat.roles.permissions.title_with_name'}"
},
"subtitle": "@:{'templates.generated.compat.roles.subtitle'}",
"title": "@:{'templates.generated.compat.common.roles'}"
},
+91
View File
@@ -4960,6 +4960,97 @@
"subtitle": "@.capitalize:{'words.generated.managed'} @:{'words.replication.services.database'} @:{'words.generated.and'} @:{'words.replication.services.redis'} @:{'words.generated.replication'}"
},
"roles": {
"permissions": {
"access_scope": "Tilgangsomrade",
"actions": {
"clear_all": "Tomm gruppe",
"reload": "Last inn pa nytt",
"retry": "Prov igjen",
"select_all": "Velg gruppe"
},
"columns": {
"enabled": "Aktivert",
"permission": "Tillatelse",
"route": "Rute",
"status": "Status"
},
"department_access_description": "Gir tilgang til {name}.",
"empty": "Ingen tillatelser er tilgjengelige.",
"empty_group": "Denne gruppen har ingen tillatelser.",
"errors": {
"load": "Kunne ikke laste rolletillatelser.",
"update": "En eller flere tillatelser kunne ikke oppdateres. De gjeldende rolletillatelsene ble lastet inn pa nytt."
},
"filtered_summary": "{count} treff",
"groups": {
"access": {
"description": "Sidetype- og avdelingstilgang.",
"label": "Tilgang"
},
"bookings": {
"description": "Tillatelser for bookinger, tidsbookinger og planlegging.",
"label": "Bookinger"
},
"customers_users": {
"description": "Tillatelser for kunder, brukere, ansatte, roller og innlogging.",
"label": "Kunder og brukere"
},
"departments_operations": {
"description": "Avdelingsdrift, baner, releer, varsler, rapporter og mal.",
"label": "Avdelinger og drift"
},
"invoicing_economic": {
"description": "Tillatelser for fakturaer, faktureringsperioder, innkreving og e-conomic.",
"label": "Fakturering og e-conomic"
},
"orders_pos": {
"description": "Tillatelser for ordrer, POS, betalinger og ordrelinjer.",
"label": "Ordrer og POS"
},
"other": {
"description": "Tillatelser som ikke matcher en annen gruppe.",
"label": "Annet"
},
"products_pricing": {
"description": "Tillatelser for produkter, priser, rabatter og valuta.",
"label": "Produkter og priser"
},
"selfserve_edge": {
"description": "Tillatelser for selvbetjening, vask, skannere, gatewayer og edge-utstyr.",
"label": "Selvbetjening og edge"
},
"system_config": {
"description": "System, konfigurasjon, release, replikering og integrasjoner.",
"label": "System og konfigurasjon"
}
},
"no_description": "Ingen beskrivelse er tilgjengelig.",
"no_search_results": "Ingen tillatelser matcher soket.",
"page_access": {
"admin": {
"description": "Gir tilgang til administrasjonssider.",
"label": "Admin"
},
"superuser": {
"description": "Gir tilgang til superbrukersider.",
"label": "Superbruker"
},
"user": {
"description": "Gir tilgang til brukersider.",
"label": "Bruker"
}
},
"search_label": "Sok i tillatelser",
"search_placeholder": "Sok pa label, nokkel, beskrivelse, rute eller metode",
"selected_summary": "{selected} av {total} valgt",
"status": {
"disabled": "Deaktivert",
"enabled": "Aktivert"
},
"subtitle": "Rolletillatelser",
"title": "Tillatelser for rolle {id}",
"title_with_name": "Tillatelser for {name}"
},
"subtitle": "@.capitalize:{'words.generated.user'} roles"
},
"self_wash": {
+91
View File
@@ -5010,6 +5010,97 @@
"subtitle": "@.capitalize:{'words.generated.managed'} @:{'words.generated.database'} @:{'words.generated.and'} @:{'words.replication.services.redis'} @:{'words.generated.replication'}"
},
"roles": {
"permissions": {
"access_scope": "Atkomstomrade",
"actions": {
"clear_all": "Rensa grupp",
"reload": "Ladda om",
"retry": "Forsok igen",
"select_all": "Valj grupp"
},
"columns": {
"enabled": "Aktiverad",
"permission": "Behorighet",
"route": "Rutt",
"status": "Status"
},
"department_access_description": "Ger atkomst till {name}.",
"empty": "Inga behorigheter ar tillgangliga.",
"empty_group": "Den har gruppen har inga behorigheter.",
"errors": {
"load": "Kunde inte lasa in rollbehorigheter.",
"update": "En eller flera behorigheter kunde inte uppdateras. Rollens aktuella behorigheter laddades om."
},
"filtered_summary": "{count} traffar",
"groups": {
"access": {
"description": "Sidtyp- och avdelningsatkomst.",
"label": "Atkomst"
},
"bookings": {
"description": "Behorigheter for bokningar, tidsbokningar och planering.",
"label": "Bokningar"
},
"customers_users": {
"description": "Behorigheter for kunder, anvandare, medarbetare, roller och inloggning.",
"label": "Kunder och anvandare"
},
"departments_operations": {
"description": "Avdelningsdrift, banor, relan, aviseringar, rapporter och mal.",
"label": "Avdelningar och drift"
},
"invoicing_economic": {
"description": "Behorigheter for fakturor, faktureringsperioder, inkasso och e-conomic.",
"label": "Fakturering och e-conomic"
},
"orders_pos": {
"description": "Behorigheter for ordrar, POS, betalningar och orderrader.",
"label": "Ordrar och POS"
},
"other": {
"description": "Behorigheter som inte matchar nagon annan grupp.",
"label": "Ovrigt"
},
"products_pricing": {
"description": "Behorigheter for produkter, priser, rabatter och valuta.",
"label": "Produkter och priser"
},
"selfserve_edge": {
"description": "Behorigheter for sjalvbetjaning, tvatt, skannrar, gateways och edge-utrustning.",
"label": "Sjalvbetjaning och edge"
},
"system_config": {
"description": "System, konfiguration, release, replikering och integrationer.",
"label": "System och konfiguration"
}
},
"no_description": "Ingen beskrivning ar tillganglig.",
"no_search_results": "Inga behorigheter matchar aktuell sokning.",
"page_access": {
"admin": {
"description": "Ger atkomst till administrationssidor.",
"label": "Admin"
},
"superuser": {
"description": "Ger atkomst till superuser-sidor.",
"label": "Superuser"
},
"user": {
"description": "Ger atkomst till anvandarsidor.",
"label": "Anvandare"
}
},
"search_label": "Sok behorigheter",
"search_placeholder": "Sok pa label, nyckel, beskrivning, rutt eller metod",
"selected_summary": "{selected} av {total} valda",
"status": {
"disabled": "Inaktiverad",
"enabled": "Aktiverad"
},
"subtitle": "Rollbehorigheter",
"title": "Behorigheter for roll {id}",
"title_with_name": "Behorigheter for {name}"
},
"subtitle": "@.capitalize:{'words.generated.user'} @:{'words.generated.roles'}"
},
"self_wash": {
@@ -0,0 +1,97 @@
{
"compat": {
"roles": {
"permissions": {
"access_scope": "Adgangsomrade",
"actions": {
"clear_all": "Ryd gruppe",
"reload": "Genindlaes",
"retry": "Prov igen",
"select_all": "Vaelg gruppe"
},
"columns": {
"enabled": "Aktiveret",
"permission": "Tilladelse",
"route": "Rute",
"status": "Status"
},
"department_access_description": "Giver adgang til {name}.",
"empty": "Der er ingen tilladelser tilgaengelige.",
"empty_group": "Denne gruppe har ingen tilladelser.",
"errors": {
"load": "Rolletilladelser kunne ikke indlaeses.",
"update": "En eller flere tilladelser kunne ikke opdateres. Rollens aktuelle tilladelser blev genindlaest."
},
"filtered_summary": "{count} matcher",
"groups": {
"access": {
"description": "Sidetype- og afdelingsadgang.",
"label": "Adgang"
},
"bookings": {
"description": "Tilladelser til bookinger, tidsbookinger og planlaegning.",
"label": "Bookinger"
},
"customers_users": {
"description": "Tilladelser til kunder, brugere, medarbejdere, roller og login.",
"label": "Kunder og brugere"
},
"departments_operations": {
"description": "Afdelingsdrift, baner, relaer, notifikationer, rapporter og mal.",
"label": "Afdelinger og drift"
},
"invoicing_economic": {
"description": "Tilladelser til fakturaer, faktureringsperioder, opkraevning og e-conomic.",
"label": "Fakturering og e-conomic"
},
"orders_pos": {
"description": "Tilladelser til ordrer, POS, betalinger og ordrelinjer.",
"label": "Ordrer og POS"
},
"other": {
"description": "Tilladelser, der ikke matcher en anden gruppe.",
"label": "Andet"
},
"products_pricing": {
"description": "Tilladelser til produkter, priser, rabatter og valuta.",
"label": "Produkter og priser"
},
"selfserve_edge": {
"description": "Tilladelser til selvbetjening, vask, scannere, gateways og edge-udstyr.",
"label": "Selvbetjening og edge"
},
"system_config": {
"description": "System, konfiguration, release, replikering og integrationer.",
"label": "System og konfiguration"
}
},
"no_description": "Der er ingen beskrivelse.",
"no_search_results": "Ingen tilladelser matcher sogningen.",
"page_access": {
"admin": {
"description": "Giver adgang til administrationssider.",
"label": "Admin"
},
"superuser": {
"description": "Giver adgang til superbrugersider.",
"label": "Superbruger"
},
"user": {
"description": "Giver adgang til brugersider.",
"label": "Bruger"
}
},
"search_label": "Sog i tilladelser",
"search_placeholder": "Sog pa label, nogle, beskrivelse, rute eller metode",
"selected_summary": "{selected} af {total} valgt",
"status": {
"disabled": "Deaktiveret",
"enabled": "Aktiveret"
},
"subtitle": "Rolletilladelser",
"title": "Tilladelser for rolle {id}",
"title_with_name": "Tilladelser for {name}"
}
}
}
}
@@ -0,0 +1,97 @@
{
"compat": {
"roles": {
"permissions": {
"access_scope": "Zugriffsbereich",
"actions": {
"clear_all": "Gruppe leeren",
"reload": "Neu laden",
"retry": "Erneut versuchen",
"select_all": "Gruppe auswaehlen"
},
"columns": {
"enabled": "Aktiviert",
"permission": "Berechtigung",
"route": "Route",
"status": "Status"
},
"department_access_description": "Erlaubt Zugriff auf {name}.",
"empty": "Es sind keine Berechtigungen verfuegbar.",
"empty_group": "Diese Gruppe hat keine Berechtigungen.",
"errors": {
"load": "Rollenberechtigungen konnten nicht geladen werden.",
"update": "Eine oder mehrere Berechtigungen konnten nicht aktualisiert werden. Die aktuellen Rollenberechtigungen wurden neu geladen."
},
"filtered_summary": "{count} Treffer",
"groups": {
"access": {
"description": "Seitentyp- und Abteilungszugriff.",
"label": "Zugriff"
},
"bookings": {
"description": "Berechtigungen fuer Buchungen, Zeitbuchungen und Planung.",
"label": "Buchungen"
},
"customers_users": {
"description": "Berechtigungen fuer Kunden, Benutzer, Mitarbeiter, Rollen und Anmeldung.",
"label": "Kunden und Benutzer"
},
"departments_operations": {
"description": "Abteilungsbetrieb, Spuren, Relais, Benachrichtigungen, Berichte und Ziele.",
"label": "Abteilungen und Betrieb"
},
"invoicing_economic": {
"description": "Berechtigungen fuer Rechnungen, Abrechnungsperioden, Inkasso und e-conomic.",
"label": "Rechnungen und e-conomic"
},
"orders_pos": {
"description": "Berechtigungen fuer Auftraege, POS, Zahlungen und Auftragszeilen.",
"label": "Auftraege und POS"
},
"other": {
"description": "Berechtigungen, die keiner anderen Gruppe entsprechen.",
"label": "Sonstige"
},
"products_pricing": {
"description": "Berechtigungen fuer Produkte, Preise, Rabatte und Waehrungen.",
"label": "Produkte und Preise"
},
"selfserve_edge": {
"description": "Berechtigungen fuer Self-Service, Waschen, Scanner, Gateways und Edge-Hardware.",
"label": "Self-Service und Edge"
},
"system_config": {
"description": "System, Konfiguration, Release, Replikation und Integrationen.",
"label": "System und Konfiguration"
}
},
"no_description": "Keine Beschreibung verfuegbar.",
"no_search_results": "Keine Berechtigungen entsprechen der Suche.",
"page_access": {
"admin": {
"description": "Erlaubt Zugriff auf Admin-Dashboardseiten.",
"label": "Admin"
},
"superuser": {
"description": "Erlaubt Zugriff auf Superuser-Dashboardseiten.",
"label": "Superuser"
},
"user": {
"description": "Erlaubt Zugriff auf Benutzer-Dashboardseiten.",
"label": "Benutzer"
}
},
"search_label": "Berechtigungen suchen",
"search_placeholder": "Nach Label, Schluessel, Beschreibung, Route oder Methode suchen",
"selected_summary": "{selected} von {total} ausgewaehlt",
"status": {
"disabled": "Deaktiviert",
"enabled": "Aktiviert"
},
"subtitle": "Rollenberechtigungen",
"title": "Berechtigungen fuer Rolle {id}",
"title_with_name": "Berechtigungen fuer {name}"
}
}
}
}
@@ -0,0 +1,97 @@
{
"compat": {
"roles": {
"permissions": {
"access_scope": "Access scope",
"actions": {
"clear_all": "Clear group",
"reload": "Reload",
"retry": "Try again",
"select_all": "Select group"
},
"columns": {
"enabled": "Enabled",
"permission": "Permission",
"route": "Route",
"status": "Status"
},
"department_access_description": "Allows access to {name}.",
"empty": "No permissions are available.",
"empty_group": "This group has no permissions.",
"errors": {
"load": "Could not load role permissions.",
"update": "Could not update one or more permissions. The current role permissions were reloaded."
},
"filtered_summary": "{count} matching",
"groups": {
"access": {
"description": "Page type and department access permissions.",
"label": "Access"
},
"bookings": {
"description": "Booking, time booking, and planning permissions.",
"label": "Bookings"
},
"customers_users": {
"description": "Customer, user, employee, role, and authentication permissions.",
"label": "Customers and users"
},
"departments_operations": {
"description": "Department operations, lanes, relays, notifications, reports, and goals.",
"label": "Departments and operations"
},
"invoicing_economic": {
"description": "Invoice, billing period, collection, and e-conomic permissions.",
"label": "Invoicing and e-conomic"
},
"orders_pos": {
"description": "Order, POS, payment, and order line permissions.",
"label": "Orders and POS"
},
"other": {
"description": "Permissions that do not match another group.",
"label": "Other"
},
"products_pricing": {
"description": "Product, price, discount, and currency permissions.",
"label": "Products and pricing"
},
"selfserve_edge": {
"description": "Self-serve, wash, scanner, gateway, and edge hardware permissions.",
"label": "Self-serve and edge"
},
"system_config": {
"description": "System, configuration, release, replication, and integration permissions.",
"label": "System and configuration"
}
},
"no_description": "No description is available.",
"no_search_results": "No permissions match the current search.",
"page_access": {
"admin": {
"description": "Allows access to admin dashboard pages.",
"label": "Admin"
},
"superuser": {
"description": "Allows access to superuser dashboard pages.",
"label": "Superuser"
},
"user": {
"description": "Allows access to user dashboard pages.",
"label": "User"
}
},
"search_label": "Search permissions",
"search_placeholder": "Search by label, key, description, route, or method",
"selected_summary": "{selected} of {total} selected",
"status": {
"disabled": "Disabled",
"enabled": "Enabled"
},
"subtitle": "Role permissions",
"title": "Role {id} permissions",
"title_with_name": "{name} permissions"
}
}
}
}
@@ -1,5 +1,96 @@
{
"roles": {
"permissions": {
"access_scope": "@:{'phrases.compat.roles.permissions.access_scope'}",
"actions": {
"clear_all": "@:{'phrases.compat.roles.permissions.actions.clear_all'}",
"reload": "@:{'phrases.compat.roles.permissions.actions.reload'}",
"retry": "@:{'phrases.compat.roles.permissions.actions.retry'}",
"select_all": "@:{'phrases.compat.roles.permissions.actions.select_all'}"
},
"columns": {
"enabled": "@:{'phrases.compat.roles.permissions.columns.enabled'}",
"permission": "@:{'phrases.compat.roles.permissions.columns.permission'}",
"route": "@:{'phrases.compat.roles.permissions.columns.route'}",
"status": "@:{'phrases.compat.roles.permissions.columns.status'}"
},
"department_access_description": "@:{'phrases.compat.roles.permissions.department_access_description'}",
"empty": "@:{'phrases.compat.roles.permissions.empty'}",
"empty_group": "@:{'phrases.compat.roles.permissions.empty_group'}",
"errors": {
"load": "@:{'phrases.compat.roles.permissions.errors.load'}",
"update": "@:{'phrases.compat.roles.permissions.errors.update'}"
},
"filtered_summary": "@:{'phrases.compat.roles.permissions.filtered_summary'}",
"groups": {
"access": {
"description": "@:{'phrases.compat.roles.permissions.groups.access.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.access.label'}"
},
"bookings": {
"description": "@:{'phrases.compat.roles.permissions.groups.bookings.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.bookings.label'}"
},
"customers_users": {
"description": "@:{'phrases.compat.roles.permissions.groups.customers_users.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.customers_users.label'}"
},
"departments_operations": {
"description": "@:{'phrases.compat.roles.permissions.groups.departments_operations.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.departments_operations.label'}"
},
"invoicing_economic": {
"description": "@:{'phrases.compat.roles.permissions.groups.invoicing_economic.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.invoicing_economic.label'}"
},
"orders_pos": {
"description": "@:{'phrases.compat.roles.permissions.groups.orders_pos.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.orders_pos.label'}"
},
"other": {
"description": "@:{'phrases.compat.roles.permissions.groups.other.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.other.label'}"
},
"products_pricing": {
"description": "@:{'phrases.compat.roles.permissions.groups.products_pricing.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.products_pricing.label'}"
},
"selfserve_edge": {
"description": "@:{'phrases.compat.roles.permissions.groups.selfserve_edge.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.selfserve_edge.label'}"
},
"system_config": {
"description": "@:{'phrases.compat.roles.permissions.groups.system_config.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.system_config.label'}"
}
},
"no_description": "@:{'phrases.compat.roles.permissions.no_description'}",
"no_search_results": "@:{'phrases.compat.roles.permissions.no_search_results'}",
"page_access": {
"admin": {
"description": "@:{'phrases.compat.roles.permissions.page_access.admin.description'}",
"label": "@:{'phrases.compat.roles.permissions.page_access.admin.label'}"
},
"superuser": {
"description": "@:{'phrases.compat.roles.permissions.page_access.superuser.description'}",
"label": "@:{'phrases.compat.roles.permissions.page_access.superuser.label'}"
},
"user": {
"description": "@:{'phrases.compat.roles.permissions.page_access.user.description'}",
"label": "@:{'phrases.compat.roles.permissions.page_access.user.label'}"
}
},
"search_label": "@:{'phrases.compat.roles.permissions.search_label'}",
"search_placeholder": "@:{'phrases.compat.roles.permissions.search_placeholder'}",
"selected_summary": "@:{'phrases.compat.roles.permissions.selected_summary'}",
"status": {
"disabled": "@:{'phrases.compat.roles.permissions.status.disabled'}",
"enabled": "@:{'phrases.compat.roles.permissions.status.enabled'}"
},
"subtitle": "@:{'phrases.compat.roles.permissions.subtitle'}",
"title": "@:{'phrases.compat.roles.permissions.title'}",
"title_with_name": "@:{'phrases.compat.roles.permissions.title_with_name'}"
},
"subtitle": "@:{'phrases.compat.roles.subtitle'}",
"title": "@:{'phrases.compat.common.roles'}"
}
@@ -0,0 +1,97 @@
{
"compat": {
"roles": {
"permissions": {
"access_scope": "Tilgangsomrade",
"actions": {
"clear_all": "Tomm gruppe",
"reload": "Last inn pa nytt",
"retry": "Prov igjen",
"select_all": "Velg gruppe"
},
"columns": {
"enabled": "Aktivert",
"permission": "Tillatelse",
"route": "Rute",
"status": "Status"
},
"department_access_description": "Gir tilgang til {name}.",
"empty": "Ingen tillatelser er tilgjengelige.",
"empty_group": "Denne gruppen har ingen tillatelser.",
"errors": {
"load": "Kunne ikke laste rolletillatelser.",
"update": "En eller flere tillatelser kunne ikke oppdateres. De gjeldende rolletillatelsene ble lastet inn pa nytt."
},
"filtered_summary": "{count} treff",
"groups": {
"access": {
"description": "Sidetype- og avdelingstilgang.",
"label": "Tilgang"
},
"bookings": {
"description": "Tillatelser for bookinger, tidsbookinger og planlegging.",
"label": "Bookinger"
},
"customers_users": {
"description": "Tillatelser for kunder, brukere, ansatte, roller og innlogging.",
"label": "Kunder og brukere"
},
"departments_operations": {
"description": "Avdelingsdrift, baner, releer, varsler, rapporter og mal.",
"label": "Avdelinger og drift"
},
"invoicing_economic": {
"description": "Tillatelser for fakturaer, faktureringsperioder, innkreving og e-conomic.",
"label": "Fakturering og e-conomic"
},
"orders_pos": {
"description": "Tillatelser for ordrer, POS, betalinger og ordrelinjer.",
"label": "Ordrer og POS"
},
"other": {
"description": "Tillatelser som ikke matcher en annen gruppe.",
"label": "Annet"
},
"products_pricing": {
"description": "Tillatelser for produkter, priser, rabatter og valuta.",
"label": "Produkter og priser"
},
"selfserve_edge": {
"description": "Tillatelser for selvbetjening, vask, skannere, gatewayer og edge-utstyr.",
"label": "Selvbetjening og edge"
},
"system_config": {
"description": "System, konfigurasjon, release, replikering og integrasjoner.",
"label": "System og konfigurasjon"
}
},
"no_description": "Ingen beskrivelse er tilgjengelig.",
"no_search_results": "Ingen tillatelser matcher soket.",
"page_access": {
"admin": {
"description": "Gir tilgang til administrasjonssider.",
"label": "Admin"
},
"superuser": {
"description": "Gir tilgang til superbrukersider.",
"label": "Superbruker"
},
"user": {
"description": "Gir tilgang til brukersider.",
"label": "Bruker"
}
},
"search_label": "Sok i tillatelser",
"search_placeholder": "Sok pa label, nokkel, beskrivelse, rute eller metode",
"selected_summary": "{selected} av {total} valgt",
"status": {
"disabled": "Deaktivert",
"enabled": "Aktivert"
},
"subtitle": "Rolletillatelser",
"title": "Tillatelser for rolle {id}",
"title_with_name": "Tillatelser for {name}"
}
}
}
}
@@ -0,0 +1,97 @@
{
"compat": {
"roles": {
"permissions": {
"access_scope": "Atkomstomrade",
"actions": {
"clear_all": "Rensa grupp",
"reload": "Ladda om",
"retry": "Forsok igen",
"select_all": "Valj grupp"
},
"columns": {
"enabled": "Aktiverad",
"permission": "Behorighet",
"route": "Rutt",
"status": "Status"
},
"department_access_description": "Ger atkomst till {name}.",
"empty": "Inga behorigheter ar tillgangliga.",
"empty_group": "Den har gruppen har inga behorigheter.",
"errors": {
"load": "Kunde inte lasa in rollbehorigheter.",
"update": "En eller flera behorigheter kunde inte uppdateras. Rollens aktuella behorigheter laddades om."
},
"filtered_summary": "{count} traffar",
"groups": {
"access": {
"description": "Sidtyp- och avdelningsatkomst.",
"label": "Atkomst"
},
"bookings": {
"description": "Behorigheter for bokningar, tidsbokningar och planering.",
"label": "Bokningar"
},
"customers_users": {
"description": "Behorigheter for kunder, anvandare, medarbetare, roller och inloggning.",
"label": "Kunder och anvandare"
},
"departments_operations": {
"description": "Avdelningsdrift, banor, relan, aviseringar, rapporter och mal.",
"label": "Avdelningar och drift"
},
"invoicing_economic": {
"description": "Behorigheter for fakturor, faktureringsperioder, inkasso och e-conomic.",
"label": "Fakturering och e-conomic"
},
"orders_pos": {
"description": "Behorigheter for ordrar, POS, betalningar och orderrader.",
"label": "Ordrar och POS"
},
"other": {
"description": "Behorigheter som inte matchar nagon annan grupp.",
"label": "Ovrigt"
},
"products_pricing": {
"description": "Behorigheter for produkter, priser, rabatter och valuta.",
"label": "Produkter och priser"
},
"selfserve_edge": {
"description": "Behorigheter for sjalvbetjaning, tvatt, skannrar, gateways och edge-utrustning.",
"label": "Sjalvbetjaning och edge"
},
"system_config": {
"description": "System, konfiguration, release, replikering och integrationer.",
"label": "System och konfiguration"
}
},
"no_description": "Ingen beskrivning ar tillganglig.",
"no_search_results": "Inga behorigheter matchar aktuell sokning.",
"page_access": {
"admin": {
"description": "Ger atkomst till administrationssidor.",
"label": "Admin"
},
"superuser": {
"description": "Ger atkomst till superuser-sidor.",
"label": "Superuser"
},
"user": {
"description": "Ger atkomst till anvandarsidor.",
"label": "Anvandare"
}
},
"search_label": "Sok behorigheter",
"search_placeholder": "Sok pa label, nyckel, beskrivning, rutt eller metod",
"selected_summary": "{selected} av {total} valda",
"status": {
"disabled": "Inaktiverad",
"enabled": "Aktiverad"
},
"subtitle": "Rollbehorigheter",
"title": "Behorigheter for roll {id}",
"title_with_name": "Behorigheter for {name}"
}
}
}
}
@@ -1,286 +1,572 @@
<script setup>
import { computed, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import PageTitle from "@/components/global/PageTitle.vue";
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
import {SessionUser} from "@/components/session/token/SessionUser.vue";
import {useRouter} from 'vue-router'
import {ref, watch} from "vue";
import {loadList, setEndpoint, setOrder} from "@/components/pagination/paginatedList.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
import RolesSubPageWrapper from "@/views/dashboards/superUserDashboard/roles/RolesSubPageWrapper.vue";
import {
filterPermissionRecords,
getPermissionDelta,
groupPermissionRecords,
normalizePermissionCatalog,
pageAccessPermissionKeys,
permissionGroupDefinitions,
sanitizePermissionDomId,
} from "@/views/dashboards/superUserDashboard/roles/rolePermissionCatalog.js";
// Lists
const permissions = ref([]);
const permissionsList = ref([]);
const rolePermissions = ref([]);
const { t } = useI18n();
const route = useRoute();
const role = ref(null);
const departments = ref([]);
const permissionRecords = ref([]);
const selectedPermissions = ref(new Set());
const busyPermissions = ref(new Set());
const busyGroups = ref(new Set());
const isLoading = ref(true);
const loadError = ref("");
const mutationError = ref("");
const search = ref("");
const activeGroup = ref("access");
// Loaded state
const isLoaded = ref(false);
// Individually loaded states
const isDepartmentsLoaded = ref(false);
const isPermissionsLoaded = ref(false);
const isRolePermissionsLoaded = ref(false);
const isPermissionsListLoaded = ref(false);
const roleId = computed(() => Number.parseInt(String(route.params.roleId || ""), 10));
const roleTitle = computed(() =>
role.value?.name
? t("roles.permissions.title_with_name", { name: role.value.name })
: t("roles.permissions.title", { id: Number.isFinite(roleId.value) ? roleId.value : "" })
);
watch(() => departments.value, () => {
console.log('Departments loaded');
isDepartmentsLoaded.value = true;
});
const buildPermissionOverrides = (departmentList) => {
const overrides = {};
watch(() => permissionsList.value, () => {
console.log('Permissions list loaded');
isPermissionsListLoaded.value = true;
});
for (const permission of pageAccessPermissionKeys) {
overrides[permission] = {
label: t(`roles.permissions.page_access.${permission}.label`),
description: t(`roles.permissions.page_access.${permission}.description`),
groupKey: "access",
isVirtual: true,
};
}
watch(() => permissions.value, () => {
console.log('Permissions loaded');
isPermissionsLoaded.value = true;
});
watch(() => rolePermissions.value, () => {
console.log('Role permissions loaded');
isRolePermissionsLoaded.value = true;
});
// Get the department from the route
const router = useRouter()
const roleId = ref(parseInt(router.currentRoute.value.params.roleId));
// Set the endpoint
setEndpoint(SessionUser.objects.department_categories.meta.endpoint + `?id=${roleId.value}`);
// Set the order
setOrder("category_id", "asc");
// Load the list
loadList();
const loadPermissionsList = async () => {
const response = await SessionUser.objects.permissions.get.all().then((response) => {
const tmpPermissionsList = [];
const tmpPermissions = [];
for (const i in response) {
const endpoint = i;
const methods = response[i];
for (const method in methods) {
const permissions = methods[method];
for (const permission in permissions) {
tmpPermissionsList.push(permission);
tmpPermissions[endpoint] = tmpPermissions[endpoint] || {}; // Create the endpoint if it doesn't exist
tmpPermissions[endpoint][method] = tmpPermissions[endpoint][method] || {}; // Create the method if it doesn't exist
tmpPermissions[endpoint][method][permission] = permissions[permission];
}
}
for (const department of departmentList) {
const departmentId = Number.parseInt(String(department?.id ?? ""), 10);
if (!Number.isFinite(departmentId)) {
continue;
}
permissionsList.value = tmpPermissionsList;
permissions.value = tmpPermissions;
});
}
// Get all available permissions
loadPermissionsList();
const loadRolePermissions = async () => {
await SessionUser.objects.roles.get.single(roleId.value).then((response) => {
rolePermissions.value = response.permissions;
});
}
// Get the permissions for the role
loadRolePermissions();
const convertRouteToHumanReadable = (route) => {
let arrow = " -> ";
// Replace / with space
let humanReadable = route.replace(/\//g, arrow);
// Replace _ with space
humanReadable = humanReadable.replace(/_/g, " ");
// Capitalize the first letter
humanReadable = humanReadable.charAt(0).toUpperCase() + humanReadable.slice(1);
// If the route ends with an arrow, remove it
if (humanReadable.endsWith(arrow)) {
humanReadable = humanReadable.slice(0, -arrow.length);
const name = String(department?.name || departmentId);
overrides[`department_access_${departmentId}`] = {
label: name,
description: t("roles.permissions.department_access_description", { name }),
groupKey: "access",
isVirtual: true,
departmentId,
};
}
// If the route starts with an arrow, remove it
if (humanReadable.startsWith(arrow)) {
humanReadable = humanReadable.slice(arrow.length);
}
return humanReadable;
}
const endpointMethodColors = {
GET: "is-success",
POST: "is-info",
PUT: "is-warning",
PATCH: "is-warning",
DELETE: "is-danger",
OPTIONS: "is-light",
}
return overrides;
};
const hasPermission = (permission) => {
return rolePermissions.value.includes(permission);
}
const togglePermission = (permission) => {
if (hasPermission(permission)) {
SessionUser.objects.roles.functions.permissions.remove(roleId.value, permission).then((response) => {
// Remove the permission
rolePermissions.value = rolePermissions.value.filter((value) => value !== permission);
});
const replaceSetValue = (targetRef, value, shouldAdd) => {
const next = new Set(targetRef.value);
if (shouldAdd) {
next.add(value);
} else {
SessionUser.objects.roles.functions.permissions.add(roleId.value, permission).then((response) => {
// Add the permission
rolePermissions.value.push(permission);
});
next.delete(value);
}
}
targetRef.value = next;
};
const setPermissionBusy = (permission, isBusy) => {
replaceSetValue(busyPermissions, permission, isBusy);
};
const loadDepartments = async () => {
const response = await SessionUser.objects.departments.get.all().then((response) => {
departments.value = response;
const setGroupBusy = (groupKey, isBusy) => {
replaceSetValue(busyGroups, groupKey, isBusy);
};
const hasPermission = (permission) => selectedPermissions.value.has(permission);
const isPermissionBusy = (permission) => busyPermissions.value.has(permission);
const isGroupBusy = (groupKey) => busyGroups.value.has(groupKey);
const permissionDomId = (permission) => `role-permission-${sanitizePermissionDomId(permission)}`;
const permissionTestId = (permission) => `role-permission-row-${sanitizePermissionDomId(permission)}`;
const allRecordsByGroup = computed(() => groupPermissionRecords(permissionRecords.value));
const filteredPermissionRecords = computed(() => filterPermissionRecords(permissionRecords.value, search.value));
const filteredRecordsByGroup = computed(() => groupPermissionRecords(filteredPermissionRecords.value));
const groupTabs = computed(() =>
permissionGroupDefinitions
.map((group) => {
const allRecords = allRecordsByGroup.value.get(group.key) || [];
const filteredRecords = filteredRecordsByGroup.value.get(group.key) || [];
return {
...group,
label: t(`roles.permissions.groups.${group.key}.label`),
description: t(`roles.permissions.groups.${group.key}.description`),
totalCount: allRecords.length,
filteredCount: filteredRecords.length,
};
})
.filter((group) => group.totalCount > 0)
);
const totalCount = computed(() => permissionRecords.value.length);
const filteredCount = computed(() => filteredPermissionRecords.value.length);
const selectedCount = computed(() => selectedPermissions.value.size);
const getAllGroupRecords = (groupKey) => allRecordsByGroup.value.get(groupKey) || [];
const getFilteredGroupRecords = (groupKey) => filteredRecordsByGroup.value.get(groupKey) || [];
const isGroupFullySelected = (groupKey) => {
const records = getAllGroupRecords(groupKey);
return records.length > 0 && records.every((record) => selectedPermissions.value.has(record.permission));
};
const isGroupFullyCleared = (groupKey) => {
const records = getAllGroupRecords(groupKey);
return records.length > 0 && records.every((record) => !selectedPermissions.value.has(record.permission));
};
watch(
groupTabs,
(tabs) => {
if (tabs.length === 0) {
return;
}
if (!tabs.some((tab) => tab.key === activeGroup.value)) {
activeGroup.value = tabs[0].key;
}
},
{ immediate: true }
);
const loadRolePermissions = async () => {
ObjectsGlobal.clearCache();
const selectedRole = await SessionUser.objects.roles.get.single(roleId.value);
role.value = selectedRole || null;
selectedPermissions.value = new Set(Array.isArray(selectedRole?.permissions) ? selectedRole.permissions : []);
};
const loadPageData = async () => {
isLoading.value = true;
loadError.value = "";
mutationError.value = "";
try {
if (!Number.isFinite(roleId.value)) {
throw new Error("Invalid role id");
}
ObjectsGlobal.clearCache();
const [permissionCatalog, selectedRole, departmentList] = await Promise.all([
SessionUser.objects.permissions.get.all(),
SessionUser.objects.roles.get.single(roleId.value),
SessionUser.objects.departments.get.all(),
]);
const safeDepartments = Array.isArray(departmentList) ? departmentList : [];
role.value = selectedRole || null;
departments.value = safeDepartments;
selectedPermissions.value = new Set(Array.isArray(selectedRole?.permissions) ? selectedRole.permissions : []);
permissionRecords.value = normalizePermissionCatalog(permissionCatalog || {}, {
departments: safeDepartments,
permissionOverrides: buildPermissionOverrides(safeDepartments),
});
} catch (error) {
console.error("Failed to load role permissions page", error);
loadError.value = t("roles.permissions.errors.load");
} finally {
isLoading.value = false;
}
};
const mutatePermissions = async ({ add = [], remove = [], groupKey = null }) => {
const additions = add.filter((permission) => !selectedPermissions.value.has(permission) && !isPermissionBusy(permission));
const removals = remove.filter((permission) => selectedPermissions.value.has(permission) && !isPermissionBusy(permission));
const affectedPermissions = [...additions, ...removals];
if (affectedPermissions.length === 0) {
return;
}
mutationError.value = "";
if (groupKey) {
setGroupBusy(groupKey, true);
}
for (const permission of affectedPermissions) {
setPermissionBusy(permission, true);
}
const optimisticPermissions = new Set(selectedPermissions.value);
for (const permission of additions) {
optimisticPermissions.add(permission);
}
for (const permission of removals) {
optimisticPermissions.delete(permission);
}
selectedPermissions.value = optimisticPermissions;
try {
const operations = [
...additions.map((permission) =>
SessionUser.objects.roles.functions.permissions.add(roleId.value, permission).then(() => ({
permission,
action: "add",
}))
),
...removals.map((permission) =>
SessionUser.objects.roles.functions.permissions.remove(roleId.value, permission).then(() => ({
permission,
action: "remove",
}))
),
];
const results = await Promise.allSettled(operations);
const failedResult = results.find((result) => result.status === "rejected");
if (failedResult) {
throw failedResult.reason;
}
ObjectsGlobal.clearCache();
const nextPermissions = new Set(selectedPermissions.value);
for (const permission of additions) {
nextPermissions.add(permission);
}
for (const permission of removals) {
nextPermissions.delete(permission);
}
selectedPermissions.value = nextPermissions;
} catch (error) {
console.error("Failed to update role permissions", error);
mutationError.value = t("roles.permissions.errors.update");
await loadRolePermissions().catch((reloadError) => {
console.error("Failed to reload role permissions after mutation error", reloadError);
});
} finally {
for (const permission of affectedPermissions) {
setPermissionBusy(permission, false);
}
if (groupKey) {
setGroupBusy(groupKey, false);
}
}
};
const togglePermission = async (record) => {
if (!record?.permission || isPermissionBusy(record.permission)) {
return;
}
const shouldAdd = !selectedPermissions.value.has(record.permission);
await mutatePermissions({
add: shouldAdd ? [record.permission] : [],
remove: shouldAdd ? [] : [record.permission],
});
}
};
loadDepartments();
const applyGroupSelection = async (groupKey, shouldSelect) => {
if (isGroupBusy(groupKey)) {
return;
}
watch(() => isDepartmentsLoaded.value && isPermissionsListLoaded.value && isPermissionsLoaded.value && isRolePermissionsLoaded.value, () => {
console.log('Loaded:', 'Departments:', departments.value.length, 'Permissions:', permissionsList.value.length, 'Role permissions:', rolePermissions.value.length);
isLoaded.value = true;
});
const delta = getPermissionDelta(getAllGroupRecords(groupKey), selectedPermissions.value, shouldSelect);
await mutatePermissions({
...delta,
groupKey,
});
};
onMounted(loadPageData);
</script>
<template>
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessSuperUser()">
<RolesSubPageWrapper>
<template #title>
<PageTitle :title="`Role ${roleId}`" subtitle="Role permissions">
<PageTitle :title="roleTitle" :subtitle="t('roles.permissions.subtitle')">
<template #buttons>
<button class="button is-dark" type="button" :disabled="isLoading" @click="loadPageData">
<span class="icon">
<i class="fas fa-sync-alt"></i>
</span>
<span>{{ t("roles.permissions.actions.reload") }}</span>
</button>
</template>
</PageTitle>
</template>
<!-- Department access permissions -->
<div class="box" v-if="isLoaded">
<p class="is-pulled-right">
<span class="tag is-dark is-large">
Department access
</span>
</p>
<p class="mt-2"> Department dependent permissions applies to:</p>
<div class="columns is-multiline">
<div v-for="department in departments" :key="department.id" class="column is-3">
<span class="tag is-dark">
{{ department.name }}
</span>
<!-- Enable switch -->
<div class="field">
<input
:id="'enable_department_' + department.id + '_access'"
type="checkbox"
class="switch is-rounded"
@change="togglePermission('department_access_' + department.id)"
:checked="hasPermission('department_access_' + department.id)"
/>
<label :for="'enable_department_' + department.id + '_access'"></label>
</div>
</div>
<section class="role-permissions-page" data-testid="role-permissions-page">
<div v-if="loadError" class="notification is-danger is-light" data-testid="role-permissions-load-error">
<p>{{ loadError }}</p>
<button class="button is-danger is-light mt-3" type="button" @click="loadPageData">
{{ t("roles.permissions.actions.retry") }}
</button>
</div>
</div>
<!-- Page type permissions -->
<div class="box">
<p class="is-pulled-right">
<span class="tag is-dark is-large">
Page type access
</span>
</p>
<p class="mt-2"> Page type dependent permissions applies to:</p>
<div class="columns is-multiline">
<!-- User -->
<div class="column is-3">
<span class="tag is-dark">
User
</span>
<!-- Enable switch -->
<div class="field">
<input
id="enable_user"
type="checkbox"
class="switch is-rounded"
@change="togglePermission('user')"
:checked="hasPermission('user')"
/>
<label for="enable_user"></label>
</div>
</div>
<!-- Admin -->
<div class="column is-3">
<span class="tag is-dark">
Admin
</span>
<!-- Enable switch -->
<div class="field">
<input
id="enable_admin"
type="checkbox"
class="switch is-rounded"
@change="togglePermission('admin')"
:checked="hasPermission('admin')"
/>
<label for="enable_admin"></label>
</div>
</div>
<!-- Super user -->
<div class="column is-3">
<span class="tag is-dark">
Super user
</span>
<!-- Enable switch -->
<div class="field">
<input
id="enable_superuser"
type="checkbox"
class="switch is-rounded"
@change="togglePermission('superuser')"
:checked="hasPermission('superuser')"
/>
<label for="enable_superuser"></label>
</div>
<div v-else-if="isLoading" class="role-permissions-loading" data-testid="role-permissions-loading">
<progress class="progress is-small is-dark" max="100"></progress>
</div>
</div>
</div>
<!-- Permissions -->
<div v-for="([endpoint, permission]) of Object.entries(permissions).sort((a, b) => a[0].localeCompare(b[0]))" class="box" v-if="isLoaded">
<p class="is-pulled-right">
<span class="tag is-dark is-large">
{{ endpoint }}
</span>
</p>
<!-- Permissions -->
<div class="mt-2" v-for="(permission, method) in permission" :key="method">
<span class="tag mt-3" :class="endpointMethodColors[method]">
<strong class="has-text-black">{{ method }}</strong>
</span>
<strong>&nbsp;{{ endpoint}}</strong> {{ convertRouteToHumanReadable(endpoint) }}
<!--{{ permission }} -->
<p class="mt-1"> Permissions </p>
<div v-for="(permission, permission_node) in permission" :key="permission_node">
<span class="tag is-dark">
{{ permission_node }}
</span>
{{ permission }}
<!-- Enable switch -->
<div class="field">
<input
:id="'enable_' + permission_node"
type="checkbox"
class="switch is-rounded"
:checked="hasPermission(permission_node)"
@change="togglePermission(permission_node)"
/>
<label :for="'enable_' + permission_node"></label>
<template v-else>
<div v-if="mutationError" class="notification is-danger is-light" data-testid="role-permission-error">
{{ mutationError }}
</div>
<div class="role-permissions-toolbar">
<div class="field role-permissions-search">
<label class="label" for="role-permissions-search">
{{ t("roles.permissions.search_label") }}
</label>
<div class="control has-icons-left">
<input
id="role-permissions-search"
v-model="search"
class="input"
type="search"
:placeholder="t('roles.permissions.search_placeholder')"
data-testid="role-permissions-search"
/>
<span class="icon is-left">
<i class="fas fa-search"></i>
</span>
</div>
</div>
<div class="role-permissions-summary" aria-live="polite">
<span class="tag is-dark is-light" data-testid="role-permissions-selected-count">
{{ t("roles.permissions.selected_summary", { selected: selectedCount, total: totalCount }) }}
</span>
<span v-if="search" class="tag is-info is-light" data-testid="role-permissions-filtered-count">
{{ t("roles.permissions.filtered_summary", { count: filteredCount }) }}
</span>
</div>
</div>
</div>
<!--<pre>{{ permission }}</pre> -->
</div>
<b-tabs
v-if="groupTabs.length"
v-model="activeGroup"
expanded
type="is-boxed"
data-testid="role-permissions-tabs"
>
<b-tab-item
v-for="group in groupTabs"
:key="group.key"
:value="group.key"
:label="`${group.label} (${group.filteredCount})`"
:icon="group.icon"
icon-pack="fas"
>
<section class="role-permission-group" :data-testid="`role-permissions-group-${group.key}`">
<div class="role-permission-group-header">
<div>
<h2 class="title is-5">{{ group.label }}</h2>
<p class="role-permission-group-description">{{ group.description }}</p>
</div>
<div class="buttons role-permission-group-actions">
<button
class="button is-dark is-small"
type="button"
:disabled="isGroupBusy(group.key) || isGroupFullySelected(group.key)"
:data-testid="`role-permissions-select-all-${group.key}`"
@click="applyGroupSelection(group.key, true)"
>
<span class="icon">
<i class="fas fa-check-square"></i>
</span>
<span>{{ t("roles.permissions.actions.select_all") }}</span>
</button>
<button
class="button is-light is-small"
type="button"
:disabled="isGroupBusy(group.key) || isGroupFullyCleared(group.key)"
:data-testid="`role-permissions-clear-all-${group.key}`"
@click="applyGroupSelection(group.key, false)"
>
<span class="icon">
<i class="fas fa-minus-square"></i>
</span>
<span>{{ t("roles.permissions.actions.clear_all") }}</span>
</button>
</div>
</div>
<div
v-if="getFilteredGroupRecords(group.key).length === 0"
class="notification is-light"
:data-testid="`role-permissions-empty-${group.key}`"
>
{{ search ? t("roles.permissions.no_search_results") : t("roles.permissions.empty_group") }}
</div>
<div v-else class="table-container">
<table class="table is-fullwidth is-hoverable role-permissions-table">
<thead>
<tr>
<th>{{ t("roles.permissions.columns.enabled") }}</th>
<th>{{ t("roles.permissions.columns.permission") }}</th>
<th>{{ t("roles.permissions.columns.route") }}</th>
<th>{{ t("roles.permissions.columns.status") }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="record in getFilteredGroupRecords(group.key)"
:key="record.permission"
:class="{ 'role-permission-selected': hasPermission(record.permission) }"
:data-testid="permissionTestId(record.permission)"
>
<td class="role-permission-toggle-cell">
<div class="field">
<input
:id="permissionDomId(record.permission)"
class="switch is-rounded"
type="checkbox"
:checked="hasPermission(record.permission)"
:disabled="isPermissionBusy(record.permission)"
:data-testid="`role-permission-toggle-${sanitizePermissionDomId(record.permission)}`"
@change="togglePermission(record)"
/>
<label :for="permissionDomId(record.permission)"></label>
</div>
</td>
<td>
<strong>{{ record.label }}</strong>
<p class="role-permission-description">
{{ record.description || t("roles.permissions.no_description") }}
</p>
<code>{{ record.permission }}</code>
</td>
<td>
<div class="role-permission-route">
<span>{{ record.endpointLabel || t("roles.permissions.access_scope") }}</span>
<span v-if="record.method" class="tag is-small" :class="record.methodColor">
{{ record.method }}
</span>
</div>
<code v-if="record.endpoint">{{ record.endpoint }}</code>
</td>
<td>
<span
class="tag"
:class="hasPermission(record.permission) ? 'is-success is-light' : 'is-light'"
>
{{
hasPermission(record.permission)
? t("roles.permissions.status.enabled")
: t("roles.permissions.status.disabled")
}}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</section>
</b-tab-item>
</b-tabs>
<div v-else class="notification is-light" data-testid="role-permissions-empty">
{{ t("roles.permissions.empty") }}
</div>
</template>
</section>
</RolesSubPageWrapper>
</RestrictedPageWrapper>
</template>
<style scoped>
.role-permissions-page {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>
.role-permissions-loading {
padding: 1rem 0;
}
.role-permissions-toolbar {
align-items: flex-end;
display: flex;
gap: 1rem;
justify-content: space-between;
}
.role-permissions-search {
flex: 1 1 28rem;
max-width: 42rem;
}
.role-permissions-summary {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: flex-end;
padding-bottom: 0.25rem;
}
.role-permission-group {
padding-top: 1rem;
}
.role-permission-group-header {
align-items: flex-start;
display: flex;
gap: 1rem;
justify-content: space-between;
margin-bottom: 1rem;
}
.role-permission-group-description,
.role-permission-description {
color: #5f6672;
}
.role-permission-group-actions {
flex: 0 0 auto;
justify-content: flex-end;
}
.role-permissions-table th:first-child,
.role-permission-toggle-cell {
width: 6rem;
}
.role-permission-route {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.role-permission-selected {
background: #f2fbf5;
}
@media screen and (max-width: 768px) {
.role-permissions-toolbar,
.role-permission-group-header {
align-items: stretch;
flex-direction: column;
}
.role-permissions-search {
max-width: none;
}
.role-permissions-summary,
.role-permission-group-actions {
justify-content: flex-start;
}
}
</style>
@@ -0,0 +1,297 @@
export const endpointMethodColors = {
GET: "is-success",
POST: "is-info",
PUT: "is-warning",
PATCH: "is-warning",
DELETE: "is-danger",
OPTIONS: "is-light",
};
export const pageAccessPermissionKeys = ["user", "admin", "superuser"];
export const permissionGroupDefinitions = [
{
key: "access",
icon: "unlock",
patterns: [/^(user|admin|superuser)$/u, /^department_access_\d+$/u],
},
{
key: "orders_pos",
icon: "cash-register",
patterns: [/order/iu, /pos/iu, /stripe/iu, /payment/iu, /xlvask/iu],
},
{
key: "bookings",
icon: "calendar-alt",
patterns: [/booking/iu, /time[-_]?booking/iu],
},
{
key: "customers_users",
icon: "users",
patterns: [/customer/iu, /user/iu, /subuser/iu, /employee/iu, /auth/iu, /role/iu, /permission/iu],
},
{
key: "departments_operations",
icon: "building",
patterns: [/department/iu, /lane/iu, /relay/iu, /daily[-_]?report/iu, /complaint/iu, /goal/iu, /notification/iu],
},
{
key: "products_pricing",
icon: "tags",
patterns: [/product/iu, /price/iu, /pricing/iu, /discount/iu, /currency/iu],
},
{
key: "invoicing_economic",
icon: "file-invoice",
patterns: [/invoice/iu, /economic/iu, /billing/iu, /collection/iu],
},
{
key: "selfserve_edge",
icon: "network-wired",
patterns: [/self[-_]?serve/iu, /selfserve/iu, /edge/iu, /gateway/iu, /scanner/iu, /wash/iu, /shelly/iu],
},
{
key: "system_config",
icon: "cogs",
patterns: [/system/iu, /config/iu, /configuration/iu, /replication/iu, /release/iu, /coolify/iu, /module/iu, /backup/iu, /workfeed/iu, /error[-_]?report/iu],
},
{
key: "other",
icon: "key",
patterns: [],
},
];
const permissionGroupByKey = new Map(permissionGroupDefinitions.map((group) => [group.key, group]));
const permissionGroupMatchPriority = [
"access",
"orders_pos",
"bookings",
"departments_operations",
"products_pricing",
"invoicing_economic",
"selfserve_edge",
"system_config",
"customers_users",
];
const permissionGroupsForMatching = permissionGroupDefinitions
.filter((group) => group.key !== "other")
.sort(
(left, right) =>
permissionGroupMatchPriority.indexOf(left.key) - permissionGroupMatchPriority.indexOf(right.key)
);
const normalizeText = (value) =>
String(value ?? "")
.toLowerCase()
.normalize("NFKD")
.replace(/[\u0300-\u036f]/gu, "")
.trim();
export const sanitizePermissionDomId = (value) =>
String(value ?? "")
.replace(/[^a-zA-Z0-9_-]+/g, "-")
.replace(/^-+|-+$/g, "")
.toLowerCase() || "permission";
const splitHumanWords = (value) =>
String(value ?? "")
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/[_./:-]+/g, " ")
.replace(/\s+/g, " ")
.trim();
export const humanizePermissionKey = (permission) => {
const words = splitHumanWords(permission);
if (!words) {
return "";
}
return words.charAt(0).toUpperCase() + words.slice(1);
};
export const humanizeEndpoint = (endpoint) => {
const words = splitHumanWords(String(endpoint ?? "").replace(/^\/+|\/+$/g, ""));
if (!words) {
return "";
}
return words.charAt(0).toUpperCase() + words.slice(1);
};
export const getPermissionGroupKey = ({ permission = "", endpoint = "", description = "" } = {}) => {
const haystack = `${permission} ${endpoint} ${description}`;
const match = permissionGroupsForMatching.find((group) =>
group.patterns.some((pattern) => pattern.test(haystack))
);
return match?.key || "other";
};
const buildSearchText = (record) =>
normalizeText(
[
record.permission,
record.label,
record.description,
record.endpoint,
record.endpointLabel,
record.method,
record.groupKey,
].join(" ")
);
const createPermissionRecord = ({
permission,
description = "",
endpoint = "",
method = "",
permissionOverrides = {},
}) => {
const key = String(permission || "").trim();
const override = permissionOverrides[key] || {};
const record = {
id: key,
permission: key,
label: override.label || humanizePermissionKey(key),
description: override.description || String(description || ""),
endpoint: String(endpoint || ""),
endpointLabel: humanizeEndpoint(endpoint),
method: String(method || ""),
methodColor: endpointMethodColors[String(method || "").toUpperCase()] || "is-light",
groupKey: override.groupKey || getPermissionGroupKey({ permission: key, endpoint, description }),
isVirtual: Boolean(override.isVirtual),
departmentId: override.departmentId || null,
};
record.searchText = buildSearchText(record);
return record;
};
const createAccessPermissionRecords = ({ departments = [], permissionOverrides = {} }) => {
const pageRecords = pageAccessPermissionKeys.map((permission) =>
createPermissionRecord({
permission,
permissionOverrides: {
...permissionOverrides,
[permission]: {
...(permissionOverrides[permission] || {}),
groupKey: "access",
isVirtual: true,
},
},
})
);
const departmentRecords = departments.map((department) => {
const departmentId = Number.parseInt(String(department?.id ?? ""), 10);
const permission = `department_access_${departmentId}`;
const override = permissionOverrides[permission] || {};
return createPermissionRecord({
permission,
permissionOverrides: {
...permissionOverrides,
[permission]: {
label: override.label || String(department?.name || permission),
description: override.description || "",
groupKey: "access",
isVirtual: true,
departmentId,
},
},
});
});
return [...pageRecords, ...departmentRecords].filter((record) => record.permission && record.permission !== "department_access_NaN");
};
export const normalizePermissionCatalog = (catalog = {}, options = {}) => {
const permissionOverrides = options.permissionOverrides || {};
const recordsByPermission = new Map();
for (const record of createAccessPermissionRecords({
departments: options.departments || [],
permissionOverrides,
})) {
recordsByPermission.set(record.permission, record);
}
for (const [endpoint, methods] of Object.entries(catalog || {})) {
if (!methods || typeof methods !== "object") {
continue;
}
for (const [method, permissions] of Object.entries(methods)) {
if (!permissions || typeof permissions !== "object") {
continue;
}
for (const [permission, description] of Object.entries(permissions)) {
const key = String(permission || "").trim();
if (!key || recordsByPermission.has(key)) {
continue;
}
recordsByPermission.set(
key,
createPermissionRecord({
permission: key,
description,
endpoint,
method,
permissionOverrides,
})
);
}
}
}
return [...recordsByPermission.values()].sort((left, right) => {
const groupCompare =
permissionGroupDefinitions.findIndex((group) => group.key === left.groupKey) -
permissionGroupDefinitions.findIndex((group) => group.key === right.groupKey);
if (groupCompare !== 0) {
return groupCompare;
}
return left.label.localeCompare(right.label, undefined, { sensitivity: "base" });
});
};
export const filterPermissionRecords = (records = [], search = "") => {
const normalizedSearch = normalizeText(search);
if (!normalizedSearch) {
return [...records];
}
return records.filter((record) => record.searchText.includes(normalizedSearch));
};
export const groupPermissionRecords = (records = []) => {
const groups = new Map(permissionGroupDefinitions.map((group) => [group.key, []]));
for (const record of records) {
const groupKey = permissionGroupByKey.has(record.groupKey) ? record.groupKey : "other";
groups.get(groupKey).push(record);
}
return groups;
};
export const getPermissionDelta = (records = [], selectedPermissions = [], shouldSelect = true) => {
const selectedSet = selectedPermissions instanceof Set ? selectedPermissions : new Set(selectedPermissions);
const uniquePermissions = [...new Set(records.map((record) => record.permission).filter(Boolean))];
if (shouldSelect) {
return {
add: uniquePermissions.filter((permission) => !selectedSet.has(permission)),
remove: [],
};
}
return {
add: [],
remove: uniquePermissions.filter((permission) => selectedSet.has(permission)),
};
};
@@ -0,0 +1,207 @@
import { expect, test, type Page, type Route } from "@playwright/test";
import { apiPathPattern, mockApi, seedAuthenticatedState } from "./support/network.js";
// This route is sensitive to stale cached Vue modules when the PWA service worker is active in mobile runs.
test.use({ serviceWorkers: "block" });
const json = (body: unknown, status = 200) => ({
status,
contentType: "application/json",
body: JSON.stringify(body),
});
const permissionCatalog = {
"/orders": {
GET: {
list_orders: "List orders",
},
POST: {
add_order: "Create orders",
},
},
"/admin/bookings": {
POST: {
create_bookings: "Create bookings",
},
},
"/superuser/system/replication": {
GET: {
superuser_replication_view: "View replication status",
},
},
};
const departments = [
{
id: 1,
name: "Copenhagen",
},
{
id: 2,
name: "Odense",
},
];
type PermissionCall = {
action: "add" | "remove";
permission: string;
};
const openGroupTab = async (page: Page, label: RegExp) => {
await page.locator(".tabs").getByText(label).click();
};
const clickPermissionSwitch = async (page: Page, permission: string) => {
await page.locator(`label[for="role-permission-${permission}"]`).click();
};
const installRolePermissionRoutes = async (
page: Page,
options: {
failAction?: "add" | "remove";
failPermission?: string;
} = {}
) => {
let rolePermissions = ["user", "superuser", "department_access_1", "list_orders"];
const calls: PermissionCall[] = [];
const rolePayload = () => ({
id: 2,
name: "Operations manager",
description: "Can manage operations.",
created_at: "2026-07-06T08:00:00.000Z",
permissions: [...rolePermissions],
});
await page.route(apiPathPattern("/permissions"), async (route: Route) => {
await route.fulfill(json({ data: permissionCatalog }));
});
await page.route(apiPathPattern("/departments"), async (route: Route) => {
await route.fulfill(json({ data: departments }));
});
await page.route(apiPathPattern("/roles"), async (route: Route) => {
await route.fulfill(json({ data: rolePayload() }));
});
await page.route(apiPathPattern("/roles/permissions"), async (route: Route) => {
const request = route.request();
const method = request.method();
const url = new URL(request.url());
const body = method === "POST" ? request.postDataJSON() : {};
const permission =
method === "POST" ? String(body.permission_id || "") : String(url.searchParams.get("permission_id") || "");
const action = method === "POST" ? "add" : "remove";
calls.push({ action, permission });
if (options.failAction === action && options.failPermission === permission) {
await route.fulfill(json({ data: { message: "Mutation failed" } }, 500));
return;
}
if (action === "add" && !rolePermissions.includes(permission)) {
rolePermissions = [...rolePermissions, permission];
}
if (action === "remove") {
rolePermissions = rolePermissions.filter((value) => value !== permission);
}
await route.fulfill(json({ data: rolePayload() }));
});
return {
calls,
getRolePermissions: () => [...rolePermissions],
};
};
const bootRolePermissionsPage = async (page: Page, routeOptions = {}) => {
await page.addInitScript(() => {
window.localStorage.setItem("locale", "en");
});
await seedAuthenticatedState(page, "superuser-role-permissions-token");
await mockApi(page, {
authenticated: true,
permissions: [
"superuser",
"user",
"permissions_list",
"list_roles",
"add_role_permission",
"delete_role_permission",
],
sessionData: {
group_id: 1,
},
});
const harness = await installRolePermissionRoutes(page, routeOptions);
await page.goto("/superuser/roles/2", { waitUntil: "domcontentloaded" });
await expect(page.getByTestId("role-permissions-page")).toBeVisible();
await expect(page.getByTestId("role-permissions-loading")).toHaveCount(0);
return harness;
};
test.describe("Superuser role permissions", () => {
test("@smoke @pr loads grouped permissions, searches, and toggles without a blank page", async ({ page }) => {
const { calls } = await bootRolePermissionsPage(page);
await expect(page.getByText("Operations manager permissions")).toBeVisible();
await expect(page.getByTestId("role-permissions-selected-count")).toContainText("4 of 9 selected");
await expect(page.getByRole("heading", { name: "Access" })).toBeVisible();
await openGroupTab(page, /Orders and POS/);
await expect(page.getByTestId("role-permission-row-list_orders")).toContainText("List orders");
await clickPermissionSwitch(page, "list_orders");
await expect.poll(() => calls).toContainEqual({ action: "remove", permission: "list_orders" });
await expect(page.getByTestId("role-permissions-page")).toBeVisible();
await expect(page.locator("body")).not.toContainText(/white screen|TypeError|Unhandled/i);
await page.getByTestId("role-permissions-search").fill("booking");
await openGroupTab(page, /Bookings/);
await expect(page.getByTestId("role-permission-row-create_bookings")).toContainText("Create bookings");
await openGroupTab(page, /Orders and POS/);
await expect(page.getByTestId("role-permissions-empty-orders_pos")).toBeVisible();
});
test("selects and clears all permissions in a group without no-op requests", async ({ page }) => {
const { calls, getRolePermissions } = await bootRolePermissionsPage(page);
await openGroupTab(page, /Orders and POS/);
await page.getByTestId("role-permissions-clear-all-orders_pos").click();
await expect.poll(() => calls).toEqual([{ action: "remove", permission: "list_orders" }]);
expect(getRolePermissions()).not.toContain("list_orders");
expect(getRolePermissions()).not.toContain("add_order");
await page.getByTestId("role-permissions-select-all-orders_pos").click();
await expect
.poll(() => calls)
.toEqual([
{ action: "remove", permission: "list_orders" },
{ action: "add", permission: "add_order" },
{ action: "add", permission: "list_orders" },
]);
expect(getRolePermissions()).toEqual(expect.arrayContaining(["list_orders", "add_order"]));
});
test("keeps the page rendered and reloads role permissions when a mutation fails", async ({ page }) => {
const { calls, getRolePermissions } = await bootRolePermissionsPage(page, {
failAction: "add",
failPermission: "add_order",
});
await openGroupTab(page, /Orders and POS/);
await clickPermissionSwitch(page, "add_order");
await expect.poll(() => calls).toContainEqual({ action: "add", permission: "add_order" });
await expect(page.getByTestId("role-permission-error")).toBeVisible();
await expect(page.getByTestId("role-permissions-page")).toBeVisible();
await expect(page.getByTestId("role-permission-toggle-add_order")).not.toBeChecked();
expect(getRolePermissions()).not.toContain("add_order");
});
});
+18 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { sourceMappings } from "../../scripts/playwright-pr-mapping.mjs";
import { fallbackChangePatterns, sourceMappings } from "../../scripts/playwright-pr-mapping.mjs";
const specsFor = (file) =>
sourceMappings
@@ -7,6 +7,8 @@ const specsFor = (file) =>
.flatMap((mapping) => mapping.specs);
describe("Playwright PR mapping", () => {
const triggersFallback = (file) => fallbackChangePatterns.some((pattern) => pattern.test(file));
it("maps user vehicle table changes to the user vehicles E2E coverage", () => {
expect(specsFor("src/components/displays/user/vehicles/vehiclesTable.vue")).toContain(
"tests/e2e/userVehicles.spec.ts"
@@ -21,4 +23,19 @@ describe("Playwright PR mapping", () => {
specsFor("src/components/displays/pagination/models/DepartmentPos/NotificationsPhonePagination.vue")
).toContain("tests/e2e/admin-department-notifications.spec.ts");
});
it("maps superuser role permission page changes to role permissions E2E coverage", () => {
expect(specsFor("src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue")).toContain(
"tests/e2e/superuser-roles-permissions.spec.ts"
);
expect(specsFor("src/views/dashboards/superUserDashboard/roles/rolePermissionCatalog.js")).toContain(
"tests/e2e/superuser-roles-permissions.spec.ts"
);
});
it("keeps full-slice ownership metadata out of broad PR smoke fallback", () => {
expect(triggersFallback("scripts/run-playwright-pr.mjs")).toBe(true);
expect(triggersFallback("scripts/run-playwright-ci-parallel.mjs")).toBe(true);
expect(triggersFallback("scripts/run-playwright-full-slice.mjs")).toBe(false);
});
});
+160
View File
@@ -0,0 +1,160 @@
import { describe, expect, it } from "vitest";
import {
filterPermissionRecords,
getPermissionDelta,
groupPermissionRecords,
humanizeEndpoint,
humanizePermissionKey,
normalizePermissionCatalog,
sanitizePermissionDomId,
} from "../../src/views/dashboards/superUserDashboard/roles/rolePermissionCatalog.js";
const catalog = {
"/orders": {
GET: {
list_orders: "List orders",
},
POST: {
add_order: "Add an order",
},
},
"/admin/bookings": {
POST: {
create_bookings: "Create bookings",
},
},
"/superuser/system/replication": {
GET: {
superuser_replication_view: "View replication status",
},
},
};
const departments = [
{
id: 7,
name: "Copenhagen",
},
];
const permissionOverrides = {
user: {
label: "User",
description: "Allows access to user dashboard pages.",
groupKey: "access",
},
admin: {
label: "Admin",
description: "Allows access to admin dashboard pages.",
groupKey: "access",
},
superuser: {
label: "Superuser",
description: "Allows access to superuser dashboard pages.",
groupKey: "access",
},
department_access_7: {
label: "Copenhagen",
description: "Allows access to Copenhagen.",
groupKey: "access",
departmentId: 7,
},
};
describe("role permission catalog helpers", () => {
it("normalizes route permissions and virtual access permissions", () => {
const records = normalizePermissionCatalog(catalog, {
departments,
permissionOverrides,
});
expect(records.map((record) => record.permission)).toEqual(
expect.arrayContaining([
"user",
"admin",
"superuser",
"department_access_7",
"list_orders",
"add_order",
"create_bookings",
"superuser_replication_view",
])
);
expect(records.find((record) => record.permission === "department_access_7")).toMatchObject({
groupKey: "access",
label: "Copenhagen",
description: "Allows access to Copenhagen.",
isVirtual: true,
});
expect(records.find((record) => record.permission === "list_orders")).toMatchObject({
groupKey: "orders_pos",
label: "List orders",
description: "List orders",
endpoint: "/orders",
endpointLabel: "Orders",
method: "GET",
});
expect(records.find((record) => record.permission === "create_bookings")).toMatchObject({
groupKey: "bookings",
});
expect(records.find((record) => record.permission === "superuser_replication_view")).toMatchObject({
groupKey: "system_config",
});
});
it("searches labels, descriptions, keys, endpoints, and methods", () => {
const records = normalizePermissionCatalog(catalog, {
departments,
permissionOverrides,
});
expect(filterPermissionRecords(records, "copenhagen").map((record) => record.permission)).toEqual([
"department_access_7",
]);
expect(filterPermissionRecords(records, "POST").map((record) => record.permission)).toEqual(
expect.arrayContaining(["add_order", "create_bookings"])
);
expect(filterPermissionRecords(records, "replication status").map((record) => record.permission)).toEqual([
"superuser_replication_view",
]);
});
it("groups records by stable group key", () => {
const grouped = groupPermissionRecords(
normalizePermissionCatalog(catalog, {
departments,
permissionOverrides,
})
);
expect(grouped.get("access").map((record) => record.permission)).toEqual(
expect.arrayContaining(["user", "admin", "superuser", "department_access_7"])
);
expect(grouped.get("orders_pos").map((record) => record.permission)).toEqual(
expect.arrayContaining(["list_orders", "add_order"])
);
expect(grouped.get("bookings").map((record) => record.permission)).toEqual(["create_bookings"]);
});
it("calculates bulk add and remove deltas without no-op requests", () => {
const records = normalizePermissionCatalog(catalog, {
departments,
permissionOverrides,
}).filter((record) => ["list_orders", "add_order"].includes(record.permission));
expect(getPermissionDelta(records, new Set(["list_orders"]), true)).toEqual({
add: ["add_order"],
remove: [],
});
expect(getPermissionDelta(records, new Set(["list_orders"]), false)).toEqual({
add: [],
remove: ["list_orders"],
});
});
it("humanizes and sanitizes permission display values", () => {
expect(humanizePermissionKey("superuser_replication_view")).toBe("Superuser replication view");
expect(humanizeEndpoint("/superuser/system/replication")).toBe("Superuser system replication");
expect(sanitizePermissionDomId("department/access 7")).toBe("department-access-7");
});
});