initial
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<InputText v-model.trim="userSearch" placeholder="Search users..." class="w-full sm:w-[320px]" @input="debouncedUsers" />
|
||||
<Button v-if="isOwner" label="Add Member" icon="pi pi-plus" @click="addDialog = true" />
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<DataTable :value="members" :loading="loading" emptyMessage="No members" class="min-w-[480px]">
|
||||
<Column header="User" style="width: 50%">
|
||||
<template #body="{ data }">
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar :label="(data.displayName || data.username).slice(0, 2).toUpperCase()"
|
||||
style="background: #3b82f6; color: #fff" />
|
||||
<span>{{ data.displayName }}</span>
|
||||
<span class="text-slate-500 dark:text-slate-400">@{{ data.username }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="role" header="Role" style="width: 20%">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="data.role" :severity="data.role === 'Owner' ? 'warn' : 'secondary'" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="" style="width: 10%">
|
||||
<template #body="{ data }">
|
||||
<Button
|
||||
v-if="isOwner && data.role !== 'Owner'"
|
||||
icon="pi pi-trash"
|
||||
text
|
||||
severity="danger"
|
||||
@click="onRemove(data.userId)"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:visible="addDialog" header="Add Member" :modal="true" style="width: min(460px, 92vw)">
|
||||
<Select
|
||||
v-model="selectedUserId"
|
||||
:options="userOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Select user"
|
||||
filter
|
||||
class="w-full"
|
||||
/>
|
||||
<Select v-model="newRole" :options="roleOptions" optionLabel="label" optionValue="value"
|
||||
placeholder="Role" class="mt-2 w-full" />
|
||||
<template #footer>
|
||||
<Button label="Cancel" severity="secondary" text @click="addDialog = false" />
|
||||
<Button label="Add" :loading="adding" @click="onAdd" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getMembers, addMember, removeMember, getUsers } from '../../services/backend'
|
||||
import { errorMessage } from '../../services/api'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import type { ProjectMember } from '../../types'
|
||||
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const projectId = String(route.params.id)
|
||||
const members = ref<ProjectMember[]>([])
|
||||
const loading = ref(false)
|
||||
const isOwner = ref(false)
|
||||
|
||||
const addDialog = ref(false)
|
||||
const userSearch = ref('')
|
||||
const users = ref<{ label: string; value: string }[]>([])
|
||||
const selectedUserId = ref<string | null>(null)
|
||||
const newRole = ref<'Owner' | 'Member'>('Member')
|
||||
const roleOptions = [
|
||||
{ label: 'Member', value: 'Member' },
|
||||
{ label: 'Owner', value: 'Owner' },
|
||||
]
|
||||
const adding = ref(false)
|
||||
|
||||
const userOptions = computed(() => users.value.filter((u) => !members.value.some((m) => m.userId === u.value)))
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
async function loadMembers() {
|
||||
loading.value = true
|
||||
try {
|
||||
members.value = await getMembers(projectId)
|
||||
isOwner.value = members.value.some((m) => m.userId === auth.user?.id && m.role === 'Owner')
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
users.value = (await getUsers(userSearch.value || undefined)).map((u: { id: string; username: string; displayName: string }) => ({
|
||||
label: `${u.displayName} (@${u.username})`,
|
||||
value: u.id,
|
||||
}))
|
||||
}
|
||||
|
||||
function debouncedUsers() {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(loadUsers, 300)
|
||||
}
|
||||
|
||||
async function onAdd() {
|
||||
if (!selectedUserId.value) {
|
||||
toast.add({ severity: 'warn', summary: 'Select a user', life: 3000 })
|
||||
return
|
||||
}
|
||||
adding.value = true
|
||||
try {
|
||||
await addMember(projectId, selectedUserId.value, newRole.value)
|
||||
addDialog.value = false
|
||||
selectedUserId.value = null
|
||||
toast.add({ severity: 'success', summary: 'Member added', life: 3000 })
|
||||
await loadMembers()
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
} finally {
|
||||
adding.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onRemove(userId: string) {
|
||||
try {
|
||||
await removeMember(projectId, userId)
|
||||
toast.add({ severity: 'success', summary: 'Member removed', life: 3000 })
|
||||
await loadMembers()
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadMembers()
|
||||
await loadUsers()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user