All checks were successful
Continuous Integration / Update Development (push) Successful in 2m2s
144 lines
5.3 KiB
Vue
144 lines
5.3 KiB
Vue
<script setup lang="ts">
|
|
import { addMemberToRole, getRoleDetails, getRoleMembers, removeMemberFromRole } from '@/api/roles'
|
|
import type { MemberLight } from '@shared/types/member'
|
|
import type { Role } from '@shared/types/roles'
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import Separator from '@/components/ui/separator/Separator.vue'
|
|
import { Plus, SearchIcon, X } from 'lucide-vue-next'
|
|
import MemberCard from '../members/MemberCard.vue'
|
|
import InputGroup from '../ui/input-group/InputGroup.vue'
|
|
import InputGroupAddon from '../ui/input-group/InputGroupAddon.vue'
|
|
|
|
import AddMember from './addMember.vue'
|
|
import Spinner from '../ui/spinner/Spinner.vue'
|
|
|
|
const route = useRoute()
|
|
|
|
const roleData = ref<Role | null>(null)
|
|
const roleMembers = ref<MemberLight[]>([])
|
|
const loading = ref(true)
|
|
|
|
async function loadRole() {
|
|
const id = Number(route.params.id)
|
|
roleData.value = await getRoleDetails(id)
|
|
roleMembers.value = await getRoleMembers(id)
|
|
|
|
loading.value = false
|
|
}
|
|
|
|
const searchQuery = ref('')
|
|
const roleMembersFiltered = computed(() => {
|
|
if (!searchQuery.value) return roleMembers.value
|
|
const query = searchQuery.value.toLowerCase()
|
|
return roleMembers.value.filter(member =>
|
|
member.displayName?.toLowerCase().includes(query) ||
|
|
member.username?.toLowerCase().includes(query)
|
|
)
|
|
})
|
|
|
|
const props = defineProps<{
|
|
allMembers: MemberLight[]
|
|
}>()
|
|
|
|
|
|
const availableMembers = computed(() =>
|
|
props.allMembers.filter(
|
|
m => !roleMembers.value.some(rm => rm.id === m.id)
|
|
)
|
|
)
|
|
|
|
async function handleRemoveMember(memberId: number) {
|
|
await removeMemberFromRole(memberId, Number(route.params.id));
|
|
await loadRole();
|
|
}
|
|
|
|
const addMemberRef = ref<InstanceType<typeof AddMember> | null>(null)
|
|
|
|
onMounted(loadRole)
|
|
watch(() => route.params.id, loadRole)
|
|
</script>
|
|
|
|
<template>
|
|
<AddMember ref="addMemberRef" :all-members="availableMembers" :role="roleData" @submit="loadRole"></AddMember>
|
|
<div class="h-full px-6 py-2">
|
|
<!-- Loading -->
|
|
<div v-if="loading" class="h-full flex items-center justify-center text-muted-foreground">
|
|
<Spinner class="size-8" />
|
|
</div>
|
|
|
|
<!-- No role selected -->
|
|
<div v-else-if="!roleData" class="text-muted-foreground">
|
|
Select a group to view details
|
|
</div>
|
|
|
|
<!-- Role details -->
|
|
<div v-else class="space-y-6">
|
|
<!-- Header -->
|
|
<div class="flex items-start justify-between">
|
|
<div class="space-y-1">
|
|
<div class="flex items-center gap-3">
|
|
<span class="h-3 w-3 rounded-full" :style="{ backgroundColor: roleData.color }" />
|
|
<h2 class="text-2xl font-semibold tracking-tight">
|
|
{{ roleData.name }}
|
|
</h2>
|
|
</div>
|
|
|
|
<p class="text-sm text-muted-foreground">
|
|
{{ roleData.description || 'No description provided.' }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- <Button variant="ghost" size="sm" class="text-destructive">
|
|
Delete
|
|
</Button> -->
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<!-- Members -->
|
|
<div class="space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-medium">
|
|
Members ({{ roleMembers.length }})
|
|
</h3>
|
|
<div class="flex items-center gap-5">
|
|
<InputGroup class="w-64">
|
|
<InputGroupAddon>
|
|
<SearchIcon class="h-4 w-4 text-muted-foreground" />
|
|
</InputGroupAddon>
|
|
|
|
<input v-model="searchQuery" type="text" placeholder="Search members…"
|
|
class="flex-1 bg-transparent outline-none text-sm" />
|
|
</InputGroup>
|
|
<Button variant="secondary" @click="addMemberRef.openDialog()">
|
|
<Plus class="mr-2 h-4 w-4" />
|
|
Add Member
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div v-if="roleMembers.length === 0" class="text-sm text-muted-foreground py-6 text-center">
|
|
No members in this group yet.
|
|
</div>
|
|
|
|
<div class="overflow-y-auto max-h-[55dvh] pr-1 scrollbar-themed">
|
|
<ul class="space-y-1">
|
|
<li v-for="member in roleMembersFiltered" :key="member.id"
|
|
class="flex items-center justify-between rounded-md px-3 py-2 hover:bg-muted/50">
|
|
<MemberCard :member-id="member.id" />
|
|
|
|
<Button variant="ghost" size="icon" class="text-muted-foreground"
|
|
@click="handleRemoveMember(member.id)">
|
|
<X class="h-4 w-4" />
|
|
</Button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|