fix: list project ui

This commit is contained in:
2026-09-14 21:41:56 +07:00
parent c9245c131c
commit 4a25557cfe
2 changed files with 67 additions and 81 deletions
+1
View File
@@ -55,4 +55,5 @@ deploy:
- ssh -p "$DEPLOY_SSH_PORT" "$DEPLOYER_USER@$DEPLOYER_HOST" "docker stop '$CONTAINER_NAME' 2>/dev/null || true" - ssh -p "$DEPLOY_SSH_PORT" "$DEPLOYER_USER@$DEPLOYER_HOST" "docker stop '$CONTAINER_NAME' 2>/dev/null || true"
- ssh -p "$DEPLOY_SSH_PORT" "$DEPLOYER_USER@$DEPLOYER_HOST" "docker rm '$CONTAINER_NAME' 2>/dev/null || true" - ssh -p "$DEPLOY_SSH_PORT" "$DEPLOYER_USER@$DEPLOYER_HOST" "docker rm '$CONTAINER_NAME' 2>/dev/null || true"
- ssh -p "$DEPLOY_SSH_PORT" "$DEPLOYER_USER@$DEPLOYER_HOST" "docker run -d --name '$CONTAINER_NAME' --restart unless-stopped -p '$HOST_PORT:$CONTAINER_PORT' '$IMAGE_BASE:$CI_COMMIT_SHORT_SHA'" - ssh -p "$DEPLOY_SSH_PORT" "$DEPLOYER_USER@$DEPLOYER_HOST" "docker run -d --name '$CONTAINER_NAME' --restart unless-stopped -p '$HOST_PORT:$CONTAINER_PORT' '$IMAGE_BASE:$CI_COMMIT_SHORT_SHA'"
- ssh -p "$DEPLOY_SSH_PORT" "$DEPLOYER_USER@$DEPLOYER_HOST" "docker images '$IMAGE_BASE' --format '{{.Repository}}:{{.Tag}}' | grep -v -e ':latest$' -e ':$CI_COMMIT_SHORT_SHA$' | xargs -r docker rmi || true"
- ssh -p "$DEPLOY_SSH_PORT" "$DEPLOYER_USER@$DEPLOYER_HOST" "docker logout" - ssh -p "$DEPLOY_SSH_PORT" "$DEPLOYER_USER@$DEPLOYER_HOST" "docker logout"
+66 -81
View File
@@ -71,11 +71,42 @@
<span class="muted-note">{{ formatDate(data.updatedAt) }}</span> <span class="muted-note">{{ formatDate(data.updatedAt) }}</span>
</template> </template>
</Column> </Column>
<Column header="Updated by" style="width: 12%"> <Column header="Updated by" style="width: 10%">
<template #body="{ data }"> <template #body="{ data }">
<span class="muted-note">{{ data.updatedByName || '—' }}</span> <span class="muted-note">{{ data.updatedByName || '—' }}</span>
</template> </template>
</Column> </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> </AppDataTable>
</AppTableShell> </AppTableShell>
@@ -94,57 +125,20 @@
</template> </template>
</Dialog> </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="flex flex-col gap-4">
<div class="field"> <div class="field">
<label for="detail-name">Name</label> <label for="edit-name">Name</label>
<InputText id="detail-name" v-model.trim="editName" class="w-full" /> <InputText id="edit-name" v-model.trim="editName" class="w-full" autofocus />
</div> </div>
<div class="field"> <div class="field">
<label for="detail-desc">Description</label> <label for="edit-desc">Description</label>
<Textarea id="detail-desc" v-model="editDescription" rows="3" class="w-full" /> <Textarea id="edit-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>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<div class="flex w-full items-center justify-between gap-2"> <Button label="Cancel" severity="secondary" text @click="editDialog = false" />
<div class="flex gap-1"> <Button label="Save" :loading="saving" @click="onSave" />
<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>
</template> </template>
</Dialog> </Dialog>
</div> </div>
@@ -176,8 +170,6 @@ const statusOptions = [
{ label: 'Active', value: 'Active' }, { label: 'Active', value: 'Active' },
{ label: 'Archived', value: 'Archived' }, { label: 'Archived', value: 'Archived' },
] ]
const selectedProject = ref<Project | null>(null)
const page = ref(1) const page = ref(1)
const pageSize = ref(20) const pageSize = ref(20)
const totalCount = ref(0) const totalCount = ref(0)
@@ -188,12 +180,13 @@ const newName = ref('')
const newDescription = ref('') const newDescription = ref('')
const creating = ref(false) const creating = ref(false)
const detailDialog = ref(false) const editDialog = ref(false)
const editingProject = ref<Project | null>(null)
const editName = ref('') const editName = ref('')
const editDescription = ref('') const editDescription = ref('')
const saving = ref(false) const saving = ref(false)
const archiving = ref(false) const archivingId = ref<string | null>(null)
const deleting = ref(false) const deletingId = ref<string | null>(null)
let textTimer: ReturnType<typeof setTimeout> | undefined let textTimer: ReturnType<typeof setTimeout> | undefined
@@ -241,43 +234,40 @@ function notImplemented(feature: string) {
} }
function onRowClick(event: DataTableRowClickEvent) { function onRowClick(event: DataTableRowClickEvent) {
if (event.data) openDetail(event.data as Project) if (event.data) openProject(event.data as Project)
} }
function openDetail(project: Project) { function openProject(project: Project) {
selectedProject.value = 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 editName.value = project.name
editDescription.value = project.description ?? '' editDescription.value = project.description ?? ''
detailDialog.value = true editDialog.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 } })
} }
async function onSave() { async function onSave() {
if (!selectedProject.value) return if (!editingProject.value) return
if (!editName.value) { if (!editName.value) {
toast.add({ severity: 'warn', summary: 'Name is required', life: 3000 }) toast.add({ severity: 'warn', summary: 'Name is required', life: 3000 })
return return
} }
saving.value = true saving.value = true
try { try {
const updated = await updateProject(selectedProject.value.id, { const updated = await updateProject(editingProject.value.id, {
name: editName.value, name: editName.value,
description: editDescription.value || null, 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) const idx = projects.value.findIndex((p) => p.id === updated.id)
if (idx !== -1) { if (idx !== -1) {
projects.value[idx].name = updated.name projects.value[idx].name = updated.name
projects.value[idx].description = updated.description projects.value[idx].description = updated.description
} }
detailDialog.value = false editDialog.value = false
toast.add({ severity: 'success', summary: 'Project updated', life: 3000 }) toast.add({ severity: 'success', summary: 'Project updated', life: 3000 })
} catch (e) { } catch (e) {
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 }) toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
@@ -286,17 +276,15 @@ async function onSave() {
} }
} }
async function onArchive() { async function onArchive(project: Project) {
if (!selectedProject.value) return const next = project.status === 'Archived' ? 'Active' : 'Archived'
const next = selectedProject.value.status === 'Archived' ? 'Active' : 'Archived' archivingId.value = project.id
archiving.value = true
try { try {
const updated = await updateProject(selectedProject.value.id, { const updated = await updateProject(project.id, {
name: selectedProject.value.name, name: project.name,
description: selectedProject.value.description ?? '', description: project.description ?? '',
status: next, status: next,
}) })
selectedProject.value.status = updated.status
const idx = projects.value.findIndex((p) => p.id === updated.id) const idx = projects.value.findIndex((p) => p.id === updated.id)
if (idx !== -1) projects.value[idx].status = updated.status if (idx !== -1) projects.value[idx].status = updated.status
toast.add({ toast.add({
@@ -307,13 +295,11 @@ async function onArchive() {
} catch (e) { } catch (e) {
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 }) toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
} finally { } finally {
archiving.value = false archivingId.value = null
} }
} }
function onDelete() { function onDelete(target: Project) {
if (!selectedProject.value) return
const target = selectedProject.value
confirm.require({ confirm.require({
message: `Delete "${target.name}"? This cannot be undone.`, message: `Delete "${target.name}"? This cannot be undone.`,
header: 'Delete Project', header: 'Delete Project',
@@ -321,7 +307,7 @@ function onDelete() {
acceptProps: { severity: 'danger' }, acceptProps: { severity: 'danger' },
rejectProps: { severity: 'secondary', outlined: true }, rejectProps: { severity: 'secondary', outlined: true },
accept: async () => { accept: async () => {
deleting.value = true deletingId.value = target.id
try { try {
await deleteProject(target.id) await deleteProject(target.id)
projects.value = projects.value.filter((p) => p.id !== 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) { if (localStorage.getItem('mws_last_project') === target.id) {
localStorage.removeItem('mws_last_project') localStorage.removeItem('mws_last_project')
} }
detailDialog.value = false
toast.add({ severity: 'success', summary: 'Project deleted', life: 3000 }) toast.add({ severity: 'success', summary: 'Project deleted', life: 3000 })
} catch (e) { } catch (e) {
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 }) toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
} finally { } finally {
deleting.value = false deletingId.value = null
} }
}, },
}) })