feat: update UI
This commit is contained in:
@@ -1,39 +1,82 @@
|
||||
<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" />
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="sm:w-[320px]">
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText v-model.trim="userSearch" placeholder="Search users..." class="search-input w-full" @input="debouncedUsers" />
|
||||
</IconField>
|
||||
</div>
|
||||
<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]">
|
||||
<div class="panel flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="flex min-h-0 flex-1 flex-col overflow-x-auto">
|
||||
<AppDataTable
|
||||
:value="members"
|
||||
:loading="loading"
|
||||
:lazy="true"
|
||||
:paginator="true"
|
||||
:rows="pageSize"
|
||||
:totalRecords="totalCount"
|
||||
:first="first"
|
||||
@page="onPageChange"
|
||||
emptyMessage="No members yet"
|
||||
scrollable
|
||||
scrollHeight="flex"
|
||||
class="min-h-0 flex-1 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" />
|
||||
style="background: var(--primary); color: #fff" />
|
||||
<span>{{ data.displayName }}</span>
|
||||
<span class="text-slate-500 dark:text-slate-400">@{{ data.username }}</span>
|
||||
<span class="muted-note">@{{ data.username }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="role" header="Role" style="width: 20%">
|
||||
<Column field="role" header="Role" style="width: 16%">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="data.role" :severity="data.role === 'Owner' ? 'warn' : 'secondary'" />
|
||||
<Tag :value="data.role" :severity="data.role === 'Owner' ? 'primary' : 'secondary'" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="Document Access" style="width: 34%">
|
||||
<template #body="{ data }">
|
||||
<div v-if="data.role === 'Owner'" class="flex flex-wrap gap-1">
|
||||
<Tag value="Full Access" severity="primary" />
|
||||
</div>
|
||||
<div v-else class="flex flex-wrap gap-1">
|
||||
<Tag v-if="data.canViewDocuments" value="View" severity="secondary" />
|
||||
<Tag v-if="data.canCreateDocuments" value="Create" severity="secondary" />
|
||||
<Tag v-if="data.canEditDocuments" value="Edit" severity="secondary" />
|
||||
<Tag v-if="data.canDeleteDocuments" value="Delete" severity="secondary" />
|
||||
<Tag v-if="!data.canViewDocuments" value="No access" severity="danger" />
|
||||
</div>
|
||||
</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)"
|
||||
/>
|
||||
<div v-if="isOwner && data.role !== 'Owner'" class="flex justify-end gap-1">
|
||||
<Button
|
||||
icon="pi pi-sliders-h"
|
||||
text
|
||||
severity="secondary"
|
||||
:aria-label="`Set document permissions for ${data.displayName}`"
|
||||
@click="openPermissions(data)"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
text
|
||||
severity="danger"
|
||||
:aria-label="`Remove ${data.displayName}`"
|
||||
@click="onRemove(data.userId)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</AppDataTable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:visible="addDialog" header="Add Member" :modal="true" style="width: min(460px, 92vw)">
|
||||
@@ -53,14 +96,40 @@
|
||||
<Button label="Add" :loading="adding" @click="onAdd" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="permDialog" header="Document Access" :modal="true" style="width: min(440px, 92vw)">
|
||||
<div class="muted-note mb-3">
|
||||
Permissions for <span class="font-medium" style="color: var(--ink)">{{ permTarget?.displayName }}</span>
|
||||
@{{ permTarget?.username }}
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<label
|
||||
v-for="opt in permOptions"
|
||||
:key="opt.key"
|
||||
class="flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-2" style="border-color: var(--hairline)"
|
||||
>
|
||||
<Checkbox v-model="permForm[opt.key]" binary />
|
||||
<div>
|
||||
<div class="text-sm font-medium">{{ opt.label }}</div>
|
||||
<div class="muted-note">{{ opt.hint }}</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="Cancel" severity="secondary" text @click="permDialog = false" />
|
||||
<Button label="Save" :loading="savingPerms" @click="savePermissions" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getMembers, addMember, removeMember, getUsers } from '../../services/backend'
|
||||
import { getMembers, addMember, removeMember, getUsers, updateMemberDocumentPermissions, getMasterDataOptions } from '../../services/backend'
|
||||
import { errorMessage } from '../../services/api'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import AppDataTable from '../../components/AppDataTable.vue'
|
||||
import type { ProjectMember } from '../../types'
|
||||
import type { DataTablePageEvent } from 'primevue/datatable'
|
||||
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
@@ -71,17 +140,30 @@ const members = ref<ProjectMember[]>([])
|
||||
const loading = ref(false)
|
||||
const isOwner = ref(false)
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const totalCount = ref(0)
|
||||
const first = computed(() => (page.value - 1) * pageSize.value)
|
||||
|
||||
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 roleOptions = ref<{ label: string; value: string }[]>([])
|
||||
const adding = ref(false)
|
||||
|
||||
const permDialog = ref(false)
|
||||
const permTarget = ref<ProjectMember | null>(null)
|
||||
const permForm = reactive({ canViewDocuments: false, canCreateDocuments: false, canEditDocuments: false, canDeleteDocuments: false })
|
||||
const savingPerms = ref(false)
|
||||
const permOptions = [
|
||||
{ key: 'canViewDocuments' as const, label: 'View', hint: 'See documents in the project' },
|
||||
{ key: 'canCreateDocuments' as const, label: 'Create', hint: 'Create new documents and folders' },
|
||||
{ key: 'canEditDocuments' as const, label: 'Edit', hint: 'Edit content, rename and move' },
|
||||
{ key: 'canDeleteDocuments' as const, label: 'Delete', hint: 'Delete documents and folders' },
|
||||
]
|
||||
|
||||
const userOptions = computed(() => users.value.filter((u) => !members.value.some((m) => m.userId === u.value)))
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
@@ -89,7 +171,9 @@ let timer: ReturnType<typeof setTimeout> | undefined
|
||||
async function loadMembers() {
|
||||
loading.value = true
|
||||
try {
|
||||
members.value = await getMembers(projectId)
|
||||
const res = await getMembers(projectId, page.value, pageSize.value)
|
||||
members.value = res.items
|
||||
totalCount.value = res.totalCount
|
||||
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 })
|
||||
@@ -98,6 +182,12 @@ async function loadMembers() {
|
||||
}
|
||||
}
|
||||
|
||||
function onPageChange(event: DataTablePageEvent) {
|
||||
page.value = event.page + 1
|
||||
pageSize.value = event.rows
|
||||
void loadMembers()
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
users.value = (await getUsers(userSearch.value || undefined)).map((u: { id: string; username: string; displayName: string }) => ({
|
||||
label: `${u.displayName} (@${u.username})`,
|
||||
@@ -139,7 +229,32 @@ async function onRemove(userId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function openPermissions(member: ProjectMember) {
|
||||
permTarget.value = member
|
||||
permForm.canViewDocuments = member.canViewDocuments
|
||||
permForm.canCreateDocuments = member.canCreateDocuments
|
||||
permForm.canEditDocuments = member.canEditDocuments
|
||||
permForm.canDeleteDocuments = member.canDeleteDocuments
|
||||
permDialog.value = true
|
||||
}
|
||||
|
||||
async function savePermissions() {
|
||||
if (!permTarget.value) return
|
||||
savingPerms.value = true
|
||||
try {
|
||||
await updateMemberDocumentPermissions(projectId, permTarget.value.userId, { ...permForm })
|
||||
permDialog.value = false
|
||||
toast.add({ severity: 'success', summary: 'Permissions updated', life: 3000 })
|
||||
await loadMembers()
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
} finally {
|
||||
savingPerms.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
roleOptions.value = await getMasterDataOptions('member_role')
|
||||
await loadMembers()
|
||||
await loadUsers()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user