306 lines
8.3 KiB
Vue
306 lines
8.3 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
import { storeToRefs } from 'pinia'
|
|
import { useIssueStore } from '@/stores/issue'
|
|
import { useProjectStore } from '@/stores/project.ts'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import IssueDetails from '@/components/IssueDetails.vue'
|
|
import IssueCreate from '@/components/IssueCreate.vue'
|
|
import type { Issue } from '@/api/issue.api.ts'
|
|
import IssuesListElement from '@/components/IssuesListElement.vue'
|
|
import IssueStats from '@/components/IssueStats.vue'
|
|
import IssueFilter from '@/components/IssueFilter.vue'
|
|
|
|
const issueStore = useIssueStore()
|
|
const issueDetailsRef = ref<InstanceType<typeof IssueDetails>>()
|
|
const projectStore = useProjectStore()
|
|
const { currentProjectId } = storeToRefs(projectStore)
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const itemsPerPage = ref(10)
|
|
const page = ref(1)
|
|
const totalItems = ref(0)
|
|
const totalPages = ref(1)
|
|
|
|
const serverItems = ref<Issue[]>([])
|
|
|
|
const text = ref<string | null>(null)
|
|
const assigneeId = ref<string | null>(null)
|
|
const status = ref<string | null>(null)
|
|
const selected = ref<[string] | []>([])
|
|
const batch = ref<number[]>([])
|
|
const deleting = ref(false)
|
|
|
|
async function fetch() {
|
|
const res = await issueStore.list({
|
|
page: page.value,
|
|
limit: itemsPerPage.value,
|
|
text: text.value,
|
|
status: status.value,
|
|
assigneeId: assigneeId.value,
|
|
projectId: currentProjectId.value,
|
|
})
|
|
|
|
serverItems.value = [...res.list]
|
|
totalPages.value = res.meta.totalPages
|
|
totalItems.value = res.meta.totalCount
|
|
}
|
|
|
|
async function load() {
|
|
syncQuery()
|
|
await fetch()
|
|
}
|
|
|
|
function syncQuery() {
|
|
const query: Record<string, string> = { page: String(page.value) }
|
|
|
|
if (text.value) query.text = text.value
|
|
if (assigneeId.value) query.assignee = assigneeId.value
|
|
if (status.value) query.status = status.value
|
|
if (selected.value.length) query.selected_issue = selected.value[0]
|
|
|
|
router.replace({ query })
|
|
}
|
|
|
|
watch(
|
|
() => route.query,
|
|
(query) => {
|
|
page.value = query.page ? Number(query.page) : 1
|
|
text.value = query.text ? String(query.text) : null
|
|
assigneeId.value = query.assignee ? String(query.assignee) : null
|
|
status.value = query.status ? String(query.status) : null
|
|
selected.value = query.selected_issue ? [String(query.selected_issue)] : []
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
watch(selected, () => syncQuery())
|
|
|
|
watch(currentProjectId, async () => {
|
|
page.value = 1
|
|
|
|
clearBatch()
|
|
close()
|
|
await load()
|
|
await issueStore.getStat(currentProjectId.value)
|
|
})
|
|
|
|
onMounted(async () => {
|
|
await load()
|
|
await issueStore.getStat(currentProjectId.value)
|
|
})
|
|
const gridTemplate = computed(() => (selected.value.length ? '1fr 1fr' : '1fr'))
|
|
const goToIssue = (key: string[]) => router.push(`/issues/${key[0]}`)
|
|
|
|
async function updatePage(val: number) {
|
|
page.value = val
|
|
await load()
|
|
}
|
|
|
|
async function onTextChanged(value: string | null) {
|
|
text.value = value
|
|
page.value = 1
|
|
await load()
|
|
}
|
|
|
|
async function onAssigneeChanged(value: string | null) {
|
|
assigneeId.value = value
|
|
page.value = 1
|
|
await load()
|
|
}
|
|
|
|
async function onStatusChanged(value: string | null) {
|
|
status.value = value
|
|
page.value = 1
|
|
await load()
|
|
}
|
|
|
|
async function onClear() {
|
|
text.value = null
|
|
assigneeId.value = null
|
|
status.value = null
|
|
page.value = 1
|
|
|
|
await load()
|
|
}
|
|
|
|
function close() {
|
|
selected.value = []
|
|
}
|
|
|
|
function onIssueUpdated(updated: Issue) {
|
|
const idx = serverItems.value.findIndex((el) => el.id === updated.id)
|
|
if (idx !== -1) {
|
|
serverItems.value.splice(idx, 1, updated)
|
|
}
|
|
issueDetailsRef.value?.applyUpdate(updated)
|
|
}
|
|
|
|
async function onIssueCreated(created: Issue) {
|
|
await fetch()
|
|
|
|
if (selected.value.length) {
|
|
selected.value = [created.key]
|
|
}
|
|
}
|
|
|
|
function toggleBatchSelect(id: number) {
|
|
const idx = batch.value.indexOf(id)
|
|
if (idx === -1) batch.value.push(id)
|
|
else batch.value.splice(idx, 1)
|
|
}
|
|
|
|
function clearBatch() {
|
|
batch.value = []
|
|
}
|
|
|
|
const allVisibleSelected = computed(
|
|
() => serverItems.value.length > 0 && serverItems.value.every((el) => batch.value.includes(el.id)),
|
|
)
|
|
const someVisibleSelected = computed(() => serverItems.value.some((el) => batch.value.includes(el.id)))
|
|
|
|
function toggleSelectAllVisible() {
|
|
if (allVisibleSelected.value) {
|
|
const visibleIds = new Set(serverItems.value.map((el) => el.id))
|
|
batch.value = batch.value.filter((id) => !visibleIds.has(id))
|
|
} else {
|
|
batch.value = [...new Set([...batch.value, ...serverItems.value.map((el) => el.id)])]
|
|
}
|
|
}
|
|
|
|
async function onDeleteBatch() {
|
|
deleting.value = true
|
|
try {
|
|
await Promise.all(batch.value.map((id) => issueStore.remove(id)))
|
|
batch.value = []
|
|
close()
|
|
await fetch()
|
|
} finally {
|
|
deleting.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="d-flex flex-column ga-2">
|
|
<div class="u-grid ga-1">
|
|
<div class="u-height">
|
|
<issue-filter
|
|
class="mb-2"
|
|
:text="text"
|
|
:assignee-id="assigneeId"
|
|
:status="status"
|
|
@update:text="onTextChanged"
|
|
@update:assignee-id="onAssigneeChanged"
|
|
@update:status="onStatusChanged"
|
|
@update:clear="onClear"
|
|
/>
|
|
<v-list class="pa-0" density="compact" v-model:selected="selected" mandatory>
|
|
<v-list-item>
|
|
<template #prepend>
|
|
<div class="mr-2" @click.stop>
|
|
<v-checkbox
|
|
:model-value="allVisibleSelected"
|
|
:indeterminate="someVisibleSelected && !allVisibleSelected"
|
|
@update:model-value="toggleSelectAllVisible"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<template #title>
|
|
<div>
|
|
<div>({{ serverItems.length }} of {{ totalItems }} elements visible)</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #append>
|
|
<div class="d-flex ga-4 align-center">
|
|
<issue-stats />
|
|
<v-btn density="compact" icon="mdi-dots-horizontal"></v-btn>
|
|
</div>
|
|
</template>
|
|
</v-list-item>
|
|
|
|
<issue-create @created="onIssueCreated" />
|
|
|
|
<issues-list-element
|
|
v-for="el in serverItems"
|
|
:key="el.id"
|
|
:issue="el"
|
|
:selected="batch.includes(el.id)"
|
|
@status-changed="onIssueUpdated"
|
|
@priority-changed="onIssueUpdated"
|
|
@assignee-changed="onIssueUpdated"
|
|
@toggle-select="toggleBatchSelect"
|
|
/>
|
|
</v-list>
|
|
|
|
<div class="d-flex justify-center pa-2">
|
|
<v-pagination
|
|
:model-value="page"
|
|
:length="totalPages"
|
|
:total-visible="7"
|
|
density="comfortable"
|
|
@update:model-value="updatePage"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="batch.length" class="d-flex justify-center ga-2">
|
|
<v-banner max-width="30rem" border lines="one">
|
|
<div class="d-flex align-center ga-2">
|
|
<div>{{ batch.length }} selected</div>
|
|
<v-divider vertical />
|
|
|
|
<v-btn
|
|
prepend-icon="mdi-trash-can"
|
|
density="compact"
|
|
variant="tonal"
|
|
:loading="deleting"
|
|
@click="onDeleteBatch"
|
|
>
|
|
Delete
|
|
</v-btn>
|
|
<v-btn prepend-icon="mdi-circle" density="compact" variant="tonal"> Done </v-btn>
|
|
</div>
|
|
|
|
<template #actions>
|
|
<div class="d-flex align-center ga-2">
|
|
<v-divider vertical />
|
|
<v-btn density="comfortable" icon="mdi-close" @click="clearBatch"></v-btn>
|
|
</div>
|
|
</template>
|
|
</v-banner>
|
|
</div>
|
|
</div>
|
|
|
|
<v-sheet border v-if="selected.length" class="overflow-y-auto">
|
|
<div class="d-flex align-center ga-2 px-4 py-2">
|
|
<div>Work item</div>
|
|
<v-spacer />
|
|
<v-icon-btn icon="mdi-open-in-new" @click="goToIssue(selected)" />
|
|
<v-icon-btn icon="mdi-close" @click="close" />
|
|
</div>
|
|
<v-divider />
|
|
<issue-details
|
|
ref="issueDetailsRef"
|
|
:ckey="selected[0]"
|
|
@status-changed="onIssueUpdated"
|
|
@assignee-changed="onIssueUpdated"
|
|
/>
|
|
</v-sheet>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.u-grid {
|
|
display: grid;
|
|
grid-template-columns: v-bind(gridTemplate);
|
|
}
|
|
|
|
.u-height {
|
|
height: calc(100vh - 10rem);
|
|
}
|
|
</style>
|