Compare commits

...
2 Commits
Author SHA1 Message Date
vadim 7639a54c50 issue types
Gitea Actions / Deploy (push) Successful in 27s
2026-10-18 20:52:26 +03:00
vadim cc490f8d4a small fixes 2026-10-18 20:07:21 +03:00
10 changed files with 150 additions and 6 deletions

No files matched your search

+3 -1
View File
@@ -4,6 +4,7 @@ import { storeToRefs } from 'pinia'
import { computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useStatusStore } from '@/stores/status.ts'
import { useIssueTypeStore } from '@/stores/issue-type.ts'
import { useIssueStore } from '@/stores/issue.ts'
import { useProjectStore } from '@/stores/project.ts'
import { authClient } from '@/lib/auth-client.ts'
@@ -13,6 +14,7 @@ import SnackbarQueue from '@/components/SnackbarQueue.vue'
const route = useRoute()
const issueStore = useIssueStore()
const statusStore = useStatusStore()
const issueTypeStore = useIssueTypeStore()
const projectStore = useProjectStore()
const session = authClient.useSession()
const push = usePushNotifications()
@@ -60,7 +62,7 @@ const layout = computed(() => {
})
onMounted(async () => {
await statusStore.list()
await Promise.all([statusStore.list(), issueTypeStore.list()])
await issueStore.getStat(currentProjectId.value)
})
</script>
+13
View File
@@ -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')
}
+3
View File
@@ -1,5 +1,6 @@
import { apiClient, toQuery } from '@/lib/api-client.ts'
import type { User } from '@/api/user.api.ts'
import type { IssueType } from '@/api/issue-type.api.ts'
export interface Issue {
id: number
@@ -7,6 +8,7 @@ export interface Issue {
title: string
content: string
status: 'OPEN' | 'IN_PROGRESS' | 'DONE' | 'CANCELLED'
type: IssueType['alias']
priority: 0 | 1 | 2 | 3 | 4
assigneeId: string
reporterId: string
@@ -29,6 +31,7 @@ export interface CreateIssueInput {
projectId: string
content?: string
status?: Issue['status']
type?: Issue['type']
priority?: Issue['priority']
assignee?: string
dueDate?: string | null
+7 -2
View File
@@ -28,6 +28,10 @@ const onCreate = async () => {
newIssue.title = ''
showForm.value = false
}
const onCancel = () => {
showForm.value = false
}
</script>
<template>
@@ -54,15 +58,16 @@ const onCreate = async () => {
placeholder="Describe your issue"
autofocus
@keyup.enter="onCreate"
@keyup.esc="onCancel"
/>
<v-btn density="compact" variant="tonal" text="Create" @click="onCreate" />
<v-btn density="compact" variant="text" icon="mdi-close" @click="showForm = false" />
<v-btn density="compact" variant="text" icon="mdi-close" @click="onCancel" />
</div>
</template>
</v-list-item>
<v-list-item v-else-if="showForm && !current">
<div>
<v-alert closable density="compact" @click:close="showForm = false">
<v-alert closable density="compact" @click:close="onCancel">
Select or create project
<router-link class="text-decoration-none text-link text-capitalize" to="/settings/projects">HERE</router-link>.
</v-alert>
+15
View File
@@ -15,6 +15,7 @@ import SplitHandle from '@/components/SplitHandle.vue'
import { useSplitPane } from '@/composables/useSplitPane.ts'
import FileUpload from '@/components/FileUpload.vue'
import PriorityMenu from '@/components/PriorityMenu.vue'
import IssueType from '@/components/IssueType.vue'
interface Props {
ckey: string
@@ -22,6 +23,7 @@ interface Props {
const emit = defineEmits<{
statusChanged: [issue: Issue]
typeChanged: [issue: Issue]
assigneeChanged: [issue: Issue]
priorityChanged: [issue: Issue]
titleChanged: [issue: Issue]
@@ -77,6 +79,16 @@ const onContentChanged = async (content: string) => {
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']) => {
if (!issue.value) return
@@ -128,6 +140,9 @@ defineExpose({
<div class="text-medium-emphasis">Status:</div>
<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>
<priority-menu :value="issue.priority" :compact="false" @changed="onPriorityChanged" />
+73
View File
@@ -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>
+13 -1
View File
@@ -5,6 +5,7 @@ import { useIssueStore } from '@/stores/issue.ts'
import type { Issue } from '@/api/issue.api.ts'
import AssigneeMenu from '@/components/AssigneeMenu.vue'
import { useIssueListOptions } from '@/composables/useIssueListOptions.ts'
import IssueType from '@/components/IssueType.vue'
interface Props {
issue: Issue
@@ -14,6 +15,7 @@ interface Props {
const { issue, selected } = defineProps<Props>()
const emit = defineEmits<{
statusChanged: [issue: Issue]
typeChanged: [issue: Issue]
priorityChanged: [issue: Issue]
assigneeChanged: [issue: Issue]
toggleSelect: [id: number]
@@ -27,6 +29,11 @@ const onStatusChanged = async (status: Issue['status']) => {
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 updated = await issueStore.update(issue.id, { priority })
if (updated) emit('priorityChanged', updated)
@@ -49,7 +56,11 @@ const onAssigneeChanged = async (assignee: string) => {
icon="mdi-alert-box"
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 }}
</div>
<div
@@ -72,6 +83,7 @@ const onAssigneeChanged = async (assignee: string) => {
:model-value="selected"
@update:model-value="emit('toggleSelect', issue.id)"
/>
<issue-type :value="issue.type" @changed="onTypeChanged" />
</div>
</template>
+13
View File
@@ -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 }
})
+8 -2
View File
@@ -46,7 +46,13 @@ async function loadMyIssues(userId: string, projectId: string | null) {
const requestId = ++myIssuesRequestId
loadingMyIssues.value = true
try {
const res = await issueStore.list({ page: 1, limit: 50, assigneeId: userId, projectId, status: ACTIVE_STATUSES })
const res = await issueStore.list({
page: 1,
limit: 7,
assigneeId: userId,
projectId: projectId,
status: ACTIVE_STATUSES,
})
if (requestId !== myIssuesRequestId) return
myIssues.value = res.list
} finally {
@@ -233,7 +239,7 @@ function onClickCard(status?: string) {
</v-card-title>
<v-divider />
<v-list v-if="projects.length" density="compact">
<v-list-item v-for="project in projects.slice(0, 6)" :key="project.id" @click="openProject(project)">
<v-list-item v-for="project in projects.slice(0, 7)" :key="project.id" @click="openProject(project)">
<label-text :label="project.key" :text="project.name" />
</v-list-item>
</v-list>
+2
View File
@@ -248,6 +248,7 @@ async function onDeleteBatch() {
:issue="el"
:selected="batch.includes(el.id)"
@status-changed="onIssueUpdated"
@type-changed="onIssueUpdated"
@priority-changed="onIssueUpdated"
@assignee-changed="onIssueUpdated"
@toggle-select="toggleBatchSelect"
@@ -306,6 +307,7 @@ async function onDeleteBatch() {
ref="issueDetailsRef"
:ckey="selected[0]"
@status-changed="onIssueUpdated"
@type-changed="onIssueUpdated"
@assignee-changed="onIssueUpdated"
@priority-changed="onIssueUpdated"
@title-changed="onIssueUpdated"