fix: list project ui
This commit is contained in:
@@ -71,11 +71,42 @@
|
||||
<span class="muted-note">{{ formatDate(data.updatedAt) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="Updated by" style="width: 12%">
|
||||
<Column header="Updated by" style="width: 10%">
|
||||
<template #body="{ data }">
|
||||
<span class="muted-note">{{ data.updatedByName || '—' }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="" style="width: 10%">
|
||||
<template #body="{ data }">
|
||||
<div class="flex justify-end gap-1">
|
||||
<Button
|
||||
v-if="auth.can('projects', 'edit')"
|
||||
icon="pi pi-pencil"
|
||||
text
|
||||
aria-label="Edit project"
|
||||
@click.stop="openEdit(data)"
|
||||
/>
|
||||
<Button
|
||||
v-if="auth.can('projects', 'edit')"
|
||||
:icon="data.status === 'Archived' ? 'pi pi-undo' : 'pi pi-archive'"
|
||||
text
|
||||
severity="warn"
|
||||
:aria-label="data.status === 'Archived' ? 'Unarchive project' : 'Archive project'"
|
||||
:loading="archivingId === data.id"
|
||||
@click.stop="onArchive(data)"
|
||||
/>
|
||||
<Button
|
||||
v-if="auth.can('projects', 'delete')"
|
||||
icon="pi pi-trash"
|
||||
text
|
||||
severity="danger"
|
||||
aria-label="Delete project"
|
||||
:loading="deletingId === data.id"
|
||||
@click.stop="onDelete(data)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</AppDataTable>
|
||||
</AppTableShell>
|
||||
|
||||
@@ -94,57 +125,20 @@
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="detailDialog" :header="selectedProject?.name" :modal="true" style="width: min(620px, 94vw)">
|
||||
<Dialog v-model:visible="editDialog" header="Edit Project" :modal="true" style="width: min(520px, 94vw)">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="field">
|
||||
<label for="detail-name">Name</label>
|
||||
<InputText id="detail-name" v-model.trim="editName" class="w-full" />
|
||||
<label for="edit-name">Name</label>
|
||||
<InputText id="edit-name" v-model.trim="editName" class="w-full" autofocus />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="detail-desc">Description</label>
|
||||
<Textarea id="detail-desc" v-model="editDescription" rows="3" class="w-full" />
|
||||
</div>
|
||||
<div v-if="selectedProject" class="flex gap-6 text-[0.85rem]">
|
||||
<div>
|
||||
<p class="muted-note mb-1 text-[0.75rem]">Status</p>
|
||||
<Tag :value="selectedProject.status" :severity="selectedProject.status === 'Archived' ? 'warn' : 'success'" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="muted-note mb-1 text-[0.75rem]">Created</p>
|
||||
<p style="color: var(--ink)">{{ formatDate(selectedProject.createdAt) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="muted-note mb-1 text-[0.75rem]">Updated</p>
|
||||
<p style="color: var(--ink)">{{ formatDate(selectedProject.updatedAt) }}</p>
|
||||
</div>
|
||||
<label for="edit-desc">Description</label>
|
||||
<Textarea id="edit-desc" v-model="editDescription" rows="3" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
<div class="flex gap-1">
|
||||
<Button
|
||||
:label="selectedProject?.status === 'Archived' ? 'Unarchive' : 'Archive'"
|
||||
icon="pi pi-archive"
|
||||
severity="warn"
|
||||
text
|
||||
:loading="archiving"
|
||||
@click="onArchive"
|
||||
/>
|
||||
<Button
|
||||
label="Delete"
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
text
|
||||
:loading="deleting"
|
||||
@click="onDelete"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button label="Open project" icon="pi pi-arrow-right" iconPos="right" text @click="openProject" />
|
||||
<Button label="Cancel" severity="secondary" text @click="detailDialog = false" />
|
||||
<Button label="Save" :loading="saving" @click="onSave" />
|
||||
</div>
|
||||
</div>
|
||||
<Button label="Cancel" severity="secondary" text @click="editDialog = false" />
|
||||
<Button label="Save" :loading="saving" @click="onSave" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
@@ -176,8 +170,6 @@ const statusOptions = [
|
||||
{ label: 'Active', value: 'Active' },
|
||||
{ label: 'Archived', value: 'Archived' },
|
||||
]
|
||||
const selectedProject = ref<Project | null>(null)
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const totalCount = ref(0)
|
||||
@@ -188,12 +180,13 @@ const newName = ref('')
|
||||
const newDescription = ref('')
|
||||
const creating = ref(false)
|
||||
|
||||
const detailDialog = ref(false)
|
||||
const editDialog = ref(false)
|
||||
const editingProject = ref<Project | null>(null)
|
||||
const editName = ref('')
|
||||
const editDescription = ref('')
|
||||
const saving = ref(false)
|
||||
const archiving = ref(false)
|
||||
const deleting = ref(false)
|
||||
const archivingId = ref<string | null>(null)
|
||||
const deletingId = ref<string | null>(null)
|
||||
|
||||
let textTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
@@ -241,43 +234,40 @@ function notImplemented(feature: string) {
|
||||
}
|
||||
|
||||
function onRowClick(event: DataTableRowClickEvent) {
|
||||
if (event.data) openDetail(event.data as Project)
|
||||
if (event.data) openProject(event.data as Project)
|
||||
}
|
||||
|
||||
function openDetail(project: Project) {
|
||||
selectedProject.value = project
|
||||
function openProject(project: Project) {
|
||||
localStorage.setItem('mws_last_project', project.id)
|
||||
router.push({ name: 'tasks-board', params: { id: project.id } })
|
||||
}
|
||||
|
||||
function openEdit(project: Project) {
|
||||
editingProject.value = project
|
||||
editName.value = project.name
|
||||
editDescription.value = project.description ?? ''
|
||||
detailDialog.value = true
|
||||
}
|
||||
|
||||
function openProject() {
|
||||
if (!selectedProject.value) return
|
||||
localStorage.setItem('mws_last_project', selectedProject.value.id)
|
||||
router.push({ name: 'tasks-board', params: { id: selectedProject.value.id } })
|
||||
editDialog.value = true
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (!selectedProject.value) return
|
||||
if (!editingProject.value) return
|
||||
if (!editName.value) {
|
||||
toast.add({ severity: 'warn', summary: 'Name is required', life: 3000 })
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const updated = await updateProject(selectedProject.value.id, {
|
||||
const updated = await updateProject(editingProject.value.id, {
|
||||
name: editName.value,
|
||||
description: editDescription.value || null,
|
||||
status: selectedProject.value.status,
|
||||
status: editingProject.value.status,
|
||||
})
|
||||
selectedProject.value.name = updated.name
|
||||
selectedProject.value.description = updated.description
|
||||
const idx = projects.value.findIndex((p) => p.id === updated.id)
|
||||
if (idx !== -1) {
|
||||
projects.value[idx].name = updated.name
|
||||
projects.value[idx].description = updated.description
|
||||
}
|
||||
detailDialog.value = false
|
||||
editDialog.value = false
|
||||
toast.add({ severity: 'success', summary: 'Project updated', life: 3000 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
@@ -286,17 +276,15 @@ async function onSave() {
|
||||
}
|
||||
}
|
||||
|
||||
async function onArchive() {
|
||||
if (!selectedProject.value) return
|
||||
const next = selectedProject.value.status === 'Archived' ? 'Active' : 'Archived'
|
||||
archiving.value = true
|
||||
async function onArchive(project: Project) {
|
||||
const next = project.status === 'Archived' ? 'Active' : 'Archived'
|
||||
archivingId.value = project.id
|
||||
try {
|
||||
const updated = await updateProject(selectedProject.value.id, {
|
||||
name: selectedProject.value.name,
|
||||
description: selectedProject.value.description ?? '',
|
||||
const updated = await updateProject(project.id, {
|
||||
name: project.name,
|
||||
description: project.description ?? '',
|
||||
status: next,
|
||||
})
|
||||
selectedProject.value.status = updated.status
|
||||
const idx = projects.value.findIndex((p) => p.id === updated.id)
|
||||
if (idx !== -1) projects.value[idx].status = updated.status
|
||||
toast.add({
|
||||
@@ -307,13 +295,11 @@ async function onArchive() {
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
} finally {
|
||||
archiving.value = false
|
||||
archivingId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function onDelete() {
|
||||
if (!selectedProject.value) return
|
||||
const target = selectedProject.value
|
||||
function onDelete(target: Project) {
|
||||
confirm.require({
|
||||
message: `Delete "${target.name}"? This cannot be undone.`,
|
||||
header: 'Delete Project',
|
||||
@@ -321,7 +307,7 @@ function onDelete() {
|
||||
acceptProps: { severity: 'danger' },
|
||||
rejectProps: { severity: 'secondary', outlined: true },
|
||||
accept: async () => {
|
||||
deleting.value = true
|
||||
deletingId.value = target.id
|
||||
try {
|
||||
await deleteProject(target.id)
|
||||
projects.value = projects.value.filter((p) => p.id !== target.id)
|
||||
@@ -329,12 +315,11 @@ function onDelete() {
|
||||
if (localStorage.getItem('mws_last_project') === target.id) {
|
||||
localStorage.removeItem('mws_last_project')
|
||||
}
|
||||
detailDialog.value = false
|
||||
toast.add({ severity: 'success', summary: 'Project deleted', life: 3000 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
} finally {
|
||||
deleting.value = false
|
||||
deletingId.value = null
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user