permissions refactoring

This commit is contained in:
2026-04-12 19:21:04 +02:00
parent 51014395a2
commit 9dcdfb981a
64 changed files with 1842 additions and 656 deletions
+193 -8
View File
@@ -16,10 +16,10 @@ import {
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import AppLayout from '@/layouts/AppLayout.vue';
import { index as teamsIndex } from '@/routes/admin/teams';
import { index as teamsIndex } from '@/routes/teams';
import type { BreadcrumbItem, TeamsIndexPageProps } from '@/types';
defineProps<TeamsIndexPageProps>();
const props = defineProps<TeamsIndexPageProps>();
const breadcrumbs: BreadcrumbItem[] = [
{
@@ -27,6 +27,15 @@ const breadcrumbs: BreadcrumbItem[] = [
href: teamsIndex(),
},
];
const hasPermission = (
permissions: Record<string, string[]>,
actorId: number,
permission: string,
) => (permissions[actorId] ?? []).includes(permission);
const permissionsForTeam = (teamId: number): Record<string, string[]> =>
props.teamPermissions[teamId] ?? {};
</script>
<template>
@@ -39,12 +48,16 @@ const breadcrumbs: BreadcrumbItem[] = [
>
<Heading
title="Teams"
description="Create reusable teams and manage member assignments for inherited project permissions."
description="Create reusable teams, manage memberships, and assign team-scoped permissions to users."
/>
<Badge variant="outline" class="gap-1.5 px-3 py-1">
<Shield class="size-3.5" />
Admin-only area
{{
canCreateTeams
? 'Global admin'
: 'Team-scoped access'
}}
</Badge>
</div>
@@ -55,8 +68,15 @@ const breadcrumbs: BreadcrumbItem[] = [
{{ status }}
</div>
<div class="grid gap-6 xl:grid-cols-[22rem_minmax(0,1fr)]">
<Card>
<div
class="grid gap-6"
:class="
canCreateTeams
? 'xl:grid-cols-[22rem_minmax(0,1fr)]'
: 'xl:grid-cols-1'
"
>
<Card v-if="canCreateTeams">
<CardHeader>
<CardTitle>Create team</CardTitle>
<CardDescription>
@@ -121,7 +141,7 @@ const breadcrumbs: BreadcrumbItem[] = [
</div>
</CardHeader>
<CardContent class="pt-6">
<CardContent class="space-y-6 pt-6">
<Form
v-bind="
TeamController.updateMembers.form(team.id)
@@ -164,12 +184,177 @@ const breadcrumbs: BreadcrumbItem[] = [
Save members
</Button>
</Form>
<div class="space-y-4 border-t pt-6">
<div class="space-y-1">
<h3 class="text-foreground font-medium">
Team permissions
</h3>
<p class="text-muted-foreground text-sm">
Direct user permissions on this team are
stored separately from team membership.
</p>
</div>
<div class="overflow-x-auto">
<table
class="divide-border min-w-full divide-y text-sm"
>
<thead
class="bg-muted/40 text-muted-foreground text-left"
>
<tr>
<th
class="px-4 py-3 font-medium"
>
User
</th>
<th
class="px-4 py-3 font-medium"
>
Current permissions
</th>
<th
class="px-4 py-3 text-right font-medium"
>
Action
</th>
</tr>
</thead>
<tbody class="divide-border divide-y">
<tr
v-for="user in users"
:key="`${team.id}-${user.id}`"
class="bg-background align-top"
>
<td class="px-4 py-4">
<div
class="text-foreground font-medium"
>
{{ user.name }}
</div>
<div
class="text-muted-foreground text-xs"
>
{{ user.email }}
</div>
</td>
<td class="px-4 py-4">
<div
class="flex flex-wrap gap-2"
>
<Badge
v-for="option in permissionOptions"
v-show="
hasPermission(
permissionsForTeam(
team.id,
),
user.id,
option.value,
)
"
:key="
`${team.id}-${user.id}-${option.value}-badge`
"
variant="secondary"
>
{{ option.label }}
</Badge>
<span
v-if="
(
permissionsForTeam(
team.id,
)[user.id] ??
[]
).length === 0
"
class="text-muted-foreground"
>
No direct team
permissions
</span>
</div>
</td>
<td class="px-4 py-4">
<div
class="flex justify-end"
>
<Form
v-bind="
TeamController.updateUserPermissions.form(
[
team.id,
user.id,
],
)
"
class="flex flex-wrap justify-end gap-3"
v-slot="{
processing,
}"
>
<label
v-for="option in permissionOptions"
:key="
`${team.id}-${user.id}-${option.value}`
"
class="border-border inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm"
>
<input
type="checkbox"
name="permissions[]"
:value="
option.value
"
:checked="
hasPermission(
permissionsForTeam(
team.id,
),
user.id,
option.value,
)
"
class="border-input text-primary focus:ring-ring size-4 rounded"
/>
<span>
{{
option.label
}}
</span>
</label>
<div
class="w-full sm:hidden"
/>
<Button
type="submit"
size="sm"
variant="outline"
:disabled="
processing
"
>
Save
</Button>
</Form>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</CardContent>
</Card>
<Card v-if="teams.length === 0">
<CardContent class="text-muted-foreground p-6 text-sm">
No teams exist yet.
No teams are available for your current
permissions.
</CardContent>
</Card>
</div>