1 parent
cc490f8d4a
commit
7639a54c50
8 files changed
+135
-2
No files matched your search
+3
-1
@@ -4,6 +4,7 @@ import { storeToRefs } from 'pinia'
|
|||||||
import { computed, onMounted, watch } from 'vue'
|
import { computed, onMounted, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useStatusStore } from '@/stores/status.ts'
|
import { useStatusStore } from '@/stores/status.ts'
|
||||||
|
import { useIssueTypeStore } from '@/stores/issue-type.ts'
|
||||||
import { useIssueStore } from '@/stores/issue.ts'
|
import { useIssueStore } from '@/stores/issue.ts'
|
||||||
import { useProjectStore } from '@/stores/project.ts'
|
import { useProjectStore } from '@/stores/project.ts'
|
||||||
import { authClient } from '@/lib/auth-client.ts'
|
import { authClient } from '@/lib/auth-client.ts'
|
||||||
@@ -13,6 +14,7 @@ import SnackbarQueue from '@/components/SnackbarQueue.vue'
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const issueStore = useIssueStore()
|
const issueStore = useIssueStore()
|
||||||
const statusStore = useStatusStore()
|
const statusStore = useStatusStore()
|
||||||
|
const issueTypeStore = useIssueTypeStore()
|
||||||
const projectStore = useProjectStore()
|
const projectStore = useProjectStore()
|
||||||
const session = authClient.useSession()
|
const session = authClient.useSession()
|
||||||
const push = usePushNotifications()
|
const push = usePushNotifications()
|
||||||
@@ -60,7 +62,7 @@ const layout = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await statusStore.list()
|
await Promise.all([statusStore.list(), issueTypeStore.list()])
|
||||||
await issueStore.getStat(currentProjectId.value)
|
await issueStore.getStat(currentProjectId.value)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { apiClient } from '@/lib/api-client.ts'
|
||||||
|
|
||||||
|
export interface IssueType {
|
||||||
|
name: string
|
||||||
|
alias: 'TASK' | 'BUG' | 'FEATURE'
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLIENT REQUEST
|
||||||
|
|
||||||
|
export async function issueTypesList() {
|
||||||
|
return apiClient.get<IssueType[]>('/api/issue-types')
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { apiClient, toQuery } from '@/lib/api-client.ts'
|
import { apiClient, toQuery } from '@/lib/api-client.ts'
|
||||||
import type { User } from '@/api/user.api.ts'
|
import type { User } from '@/api/user.api.ts'
|
||||||
|
import type { IssueType } from '@/api/issue-type.api.ts'
|
||||||
|
|
||||||
export interface Issue {
|
export interface Issue {
|
||||||
id: number
|
id: number
|
||||||
@@ -7,6 +8,7 @@ export interface Issue {
|
|||||||
title: string
|
title: string
|
||||||
content: string
|
content: string
|
||||||
status: 'OPEN' | 'IN_PROGRESS' | 'DONE' | 'CANCELLED'
|
status: 'OPEN' | 'IN_PROGRESS' | 'DONE' | 'CANCELLED'
|
||||||
|
type: IssueType['alias']
|
||||||
priority: 0 | 1 | 2 | 3 | 4
|
priority: 0 | 1 | 2 | 3 | 4
|
||||||
assigneeId: string
|
assigneeId: string
|
||||||
reporterId: string
|
reporterId: string
|
||||||
@@ -29,6 +31,7 @@ export interface CreateIssueInput {
|
|||||||
projectId: string
|
projectId: string
|
||||||
content?: string
|
content?: string
|
||||||
status?: Issue['status']
|
status?: Issue['status']
|
||||||
|
type?: Issue['type']
|
||||||
priority?: Issue['priority']
|
priority?: Issue['priority']
|
||||||
assignee?: string
|
assignee?: string
|
||||||
dueDate?: string | null
|
dueDate?: string | null
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import SplitHandle from '@/components/SplitHandle.vue'
|
|||||||
import { useSplitPane } from '@/composables/useSplitPane.ts'
|
import { useSplitPane } from '@/composables/useSplitPane.ts'
|
||||||
import FileUpload from '@/components/FileUpload.vue'
|
import FileUpload from '@/components/FileUpload.vue'
|
||||||
import PriorityMenu from '@/components/PriorityMenu.vue'
|
import PriorityMenu from '@/components/PriorityMenu.vue'
|
||||||
|
import IssueType from '@/components/IssueType.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ckey: string
|
ckey: string
|
||||||
@@ -22,6 +23,7 @@ interface Props {
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
statusChanged: [issue: Issue]
|
statusChanged: [issue: Issue]
|
||||||
|
typeChanged: [issue: Issue]
|
||||||
assigneeChanged: [issue: Issue]
|
assigneeChanged: [issue: Issue]
|
||||||
priorityChanged: [issue: Issue]
|
priorityChanged: [issue: Issue]
|
||||||
titleChanged: [issue: Issue]
|
titleChanged: [issue: Issue]
|
||||||
@@ -77,6 +79,16 @@ const onContentChanged = async (content: string) => {
|
|||||||
if (updated) issue.value = updated
|
if (updated) issue.value = updated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onTypeChanged = async (type: Issue['type']) => {
|
||||||
|
if (!issue.value) return
|
||||||
|
|
||||||
|
const updated = await issueStore.update(issue.value.id, { type })
|
||||||
|
if (updated) {
|
||||||
|
issue.value = updated
|
||||||
|
emit('typeChanged', updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onStatusChanged = async (status: Issue['status']) => {
|
const onStatusChanged = async (status: Issue['status']) => {
|
||||||
if (!issue.value) return
|
if (!issue.value) return
|
||||||
|
|
||||||
@@ -128,6 +140,9 @@ defineExpose({
|
|||||||
<div class="text-medium-emphasis">Status:</div>
|
<div class="text-medium-emphasis">Status:</div>
|
||||||
<status-menu :value="issue.status" @changed="onStatusChanged" />
|
<status-menu :value="issue.status" @changed="onStatusChanged" />
|
||||||
|
|
||||||
|
<div class="text-medium-emphasis">Type:</div>
|
||||||
|
<issue-type :value="issue.type" :compact="false" @changed="onTypeChanged" />
|
||||||
|
|
||||||
<div class="text-medium-emphasis">Priority:</div>
|
<div class="text-medium-emphasis">Priority:</div>
|
||||||
<priority-menu :value="issue.priority" :compact="false" @changed="onPriorityChanged" />
|
<priority-menu :value="issue.priority" :compact="false" @changed="onPriorityChanged" />
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useIssueTypeStore } from '@/stores/issue-type.ts'
|
||||||
|
import type { Issue } from '@/api/issue.api.ts'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value?: string
|
||||||
|
compact?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), { compact: true })
|
||||||
|
const emit = defineEmits<{ changed: [value: Issue['type']] }>()
|
||||||
|
|
||||||
|
const issueTypeStore = useIssueTypeStore()
|
||||||
|
|
||||||
|
const icons: Record<string, string> = {
|
||||||
|
BUG: 'mdi-bug-outline',
|
||||||
|
TASK: 'mdi-checkbox-outline',
|
||||||
|
FEATURE: 'mdi-bookmark-outline',
|
||||||
|
}
|
||||||
|
const defaultIcon = 'mdi-star-outline'
|
||||||
|
|
||||||
|
const iconOf = (alias?: string) => (alias && icons[alias]) || defaultIcon
|
||||||
|
|
||||||
|
const current = computed(() => issueTypeStore.issueTypes?.find((el) => el.alias === props.value))
|
||||||
|
const currentIcon = computed(() => iconOf(props.value))
|
||||||
|
const currentColor = computed(() => current.value?.color ?? 'primary')
|
||||||
|
const currentName = computed(() => current.value?.name ?? props.value ?? '')
|
||||||
|
|
||||||
|
const availableTypes = computed(() => issueTypeStore.issueTypes?.filter((el) => el.alias !== props.value))
|
||||||
|
|
||||||
|
const select = (value: unknown) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
emit('changed', value[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-menu>
|
||||||
|
<template v-slot:activator="{ props: activatorProps }">
|
||||||
|
<v-btn
|
||||||
|
v-if="props.compact"
|
||||||
|
density="compact"
|
||||||
|
:icon="currentIcon"
|
||||||
|
:color="currentColor"
|
||||||
|
:title="currentName"
|
||||||
|
variant="text"
|
||||||
|
v-bind="activatorProps"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-btn v-else density="compact" class="px-1" variant="text" v-bind="activatorProps">
|
||||||
|
<div class="d-flex align-center ga-1">
|
||||||
|
<v-icon :color="currentColor" :icon="currentIcon" />
|
||||||
|
<span>{{ currentName }}</span>
|
||||||
|
</div>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list border class="mt-1" select-strategy="single-leaf" density="compact" @update:selected="select">
|
||||||
|
<v-list-item v-for="el in availableTypes" :key="el.alias" :value="el.alias">
|
||||||
|
<div class="d-flex justify-start align-center ga-1">
|
||||||
|
<v-icon size="small" :icon="iconOf(el.alias)" :color="el.color" />
|
||||||
|
<div>{{ el.name }}</div>
|
||||||
|
</div>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -5,6 +5,7 @@ import { useIssueStore } from '@/stores/issue.ts'
|
|||||||
import type { Issue } from '@/api/issue.api.ts'
|
import type { Issue } from '@/api/issue.api.ts'
|
||||||
import AssigneeMenu from '@/components/AssigneeMenu.vue'
|
import AssigneeMenu from '@/components/AssigneeMenu.vue'
|
||||||
import { useIssueListOptions } from '@/composables/useIssueListOptions.ts'
|
import { useIssueListOptions } from '@/composables/useIssueListOptions.ts'
|
||||||
|
import IssueType from '@/components/IssueType.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
issue: Issue
|
issue: Issue
|
||||||
@@ -14,6 +15,7 @@ interface Props {
|
|||||||
const { issue, selected } = defineProps<Props>()
|
const { issue, selected } = defineProps<Props>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
statusChanged: [issue: Issue]
|
statusChanged: [issue: Issue]
|
||||||
|
typeChanged: [issue: Issue]
|
||||||
priorityChanged: [issue: Issue]
|
priorityChanged: [issue: Issue]
|
||||||
assigneeChanged: [issue: Issue]
|
assigneeChanged: [issue: Issue]
|
||||||
toggleSelect: [id: number]
|
toggleSelect: [id: number]
|
||||||
@@ -27,6 +29,11 @@ const onStatusChanged = async (status: Issue['status']) => {
|
|||||||
if (updated) emit('statusChanged', updated)
|
if (updated) emit('statusChanged', updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onTypeChanged = async (type: Issue['type']) => {
|
||||||
|
const updated = await issueStore.update(issue.id, { type })
|
||||||
|
if (updated) emit('typeChanged', updated)
|
||||||
|
}
|
||||||
|
|
||||||
const onPriorityChanged = async (priority: Issue['priority']) => {
|
const onPriorityChanged = async (priority: Issue['priority']) => {
|
||||||
const updated = await issueStore.update(issue.id, { priority })
|
const updated = await issueStore.update(issue.id, { priority })
|
||||||
if (updated) emit('priorityChanged', updated)
|
if (updated) emit('priorityChanged', updated)
|
||||||
@@ -49,7 +56,11 @@ const onAssigneeChanged = async (assignee: string) => {
|
|||||||
icon="mdi-alert-box"
|
icon="mdi-alert-box"
|
||||||
color="error"
|
color="error"
|
||||||
/>
|
/>
|
||||||
<div v-if="visible.title" class="flex-shrink-0" :class="{ 'text-decoration-line-through': issue.status === 'DONE' }">
|
<div
|
||||||
|
v-if="visible.title"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
:class="{ 'text-decoration-line-through': issue.status === 'DONE' }"
|
||||||
|
>
|
||||||
{{ issue.title }}
|
{{ issue.title }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -72,6 +83,7 @@ const onAssigneeChanged = async (assignee: string) => {
|
|||||||
:model-value="selected"
|
:model-value="selected"
|
||||||
@update:model-value="emit('toggleSelect', issue.id)"
|
@update:model-value="emit('toggleSelect', issue.id)"
|
||||||
/>
|
/>
|
||||||
|
<issue-type :value="issue.type" @changed="onTypeChanged" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { type IssueType, issueTypesList } from '@/api/issue-type.api.ts'
|
||||||
|
|
||||||
|
export const useIssueTypeStore = defineStore('issueType', () => {
|
||||||
|
const issueTypes = ref<IssueType[]>()
|
||||||
|
|
||||||
|
async function list() {
|
||||||
|
issueTypes.value = await issueTypesList()
|
||||||
|
}
|
||||||
|
|
||||||
|
return { issueTypes, list }
|
||||||
|
})
|
||||||
@@ -248,6 +248,7 @@ async function onDeleteBatch() {
|
|||||||
:issue="el"
|
:issue="el"
|
||||||
:selected="batch.includes(el.id)"
|
:selected="batch.includes(el.id)"
|
||||||
@status-changed="onIssueUpdated"
|
@status-changed="onIssueUpdated"
|
||||||
|
@type-changed="onIssueUpdated"
|
||||||
@priority-changed="onIssueUpdated"
|
@priority-changed="onIssueUpdated"
|
||||||
@assignee-changed="onIssueUpdated"
|
@assignee-changed="onIssueUpdated"
|
||||||
@toggle-select="toggleBatchSelect"
|
@toggle-select="toggleBatchSelect"
|
||||||
@@ -306,6 +307,7 @@ async function onDeleteBatch() {
|
|||||||
ref="issueDetailsRef"
|
ref="issueDetailsRef"
|
||||||
:ckey="selected[0]"
|
:ckey="selected[0]"
|
||||||
@status-changed="onIssueUpdated"
|
@status-changed="onIssueUpdated"
|
||||||
|
@type-changed="onIssueUpdated"
|
||||||
@assignee-changed="onIssueUpdated"
|
@assignee-changed="onIssueUpdated"
|
||||||
@priority-changed="onIssueUpdated"
|
@priority-changed="onIssueUpdated"
|
||||||
@title-changed="onIssueUpdated"
|
@title-changed="onIssueUpdated"
|
||||||
|
|||||||
Reference in new issue
Block a user