fix: list project ui
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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="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" />
|
<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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user