Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17ad52c532 | ||
|
|
9a3c3bb189 | ||
|
|
e62747d62d |
No files matched your search
@@ -31,6 +31,7 @@ export interface CreateIssueInput {
|
|||||||
status?: Issue['status']
|
status?: Issue['status']
|
||||||
priority?: Issue['priority']
|
priority?: Issue['priority']
|
||||||
assignee?: string
|
assignee?: string
|
||||||
|
dueDate?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateIssueInput = Partial<CreateIssueInput>
|
export type UpdateIssueInput = Partial<CreateIssueInput>
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value?: string
|
text?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const emit = defineEmits<{ changed: [value: string] }>()
|
const emit = defineEmits<{ changed: [text: string] }>()
|
||||||
|
|
||||||
const editing = ref(false)
|
const editing = ref(false)
|
||||||
const draft = ref(props.value ?? '')
|
const draft = ref(props.text ?? '')
|
||||||
|
|
||||||
const startEdit = () => {
|
const startEdit = () => {
|
||||||
draft.value = props.value ?? ''
|
draft.value = props.text ?? ''
|
||||||
editing.value = true
|
editing.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ const save = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
draft.value = props.value ?? ''
|
draft.value = props.text ?? ''
|
||||||
editing.value = false
|
editing.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -30,17 +30,24 @@ const cancel = () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="issue-content">
|
<div class="issue-content">
|
||||||
<template v-if="!editing">
|
<template v-if="!editing">
|
||||||
<v-sheet class="pa-2 cursor-pointer content-sheet" @click="startEdit">
|
<v-hover #default="{ isHovering, props }">
|
||||||
{{ value ?? 'click to add content' }}
|
<v-sheet
|
||||||
</v-sheet>
|
v-bind="props"
|
||||||
|
:class="isHovering ? 'px-4' : 'px-0'"
|
||||||
|
class="py-1 cursor-pointer content-sheet"
|
||||||
|
@click="startEdit"
|
||||||
|
>
|
||||||
|
{{ text ?? 'click to add content' }}
|
||||||
|
</v-sheet>
|
||||||
|
</v-hover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<v-textarea autofocus class="mb-1" hide-details v-model="draft" variant="outlined" auto-grow rows="4" />
|
<v-textarea autofocus class="mb-1" hide-details v-model="draft" variant="outlined" auto-grow rows="4" />
|
||||||
|
|
||||||
<div class="d-flex justify-end ga-1">
|
<div class="d-flex justify-end ga-1">
|
||||||
<v-btn density="compact" variant="text" text="Cancel" @click="cancel" />
|
<v-btn density="compact" prepend-icon="mdi-cancel" variant="text" text="Cancel" @click="cancel" />
|
||||||
<v-btn density="compact" color="primary" variant="flat" text="Save" @click="save" />
|
<v-btn density="compact" prepend-icon="mdi-check" color="primary" variant="flat" text="Save" @click="save" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,4 +57,7 @@ const cancel = () => {
|
|||||||
.content-sheet:hover {
|
.content-sheet:hover {
|
||||||
background-color: rgba(var(--v-theme-on-surface), 0.06);
|
background-color: rgba(var(--v-theme-on-surface), 0.06);
|
||||||
}
|
}
|
||||||
|
.v-sheet {
|
||||||
|
transition: padding 0.2s ease-in-out;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
date?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<{ changed: [date: string] }>()
|
||||||
|
|
||||||
|
const editing = ref(false)
|
||||||
|
const draft = ref<Date | null>(props.date ? new Date(props.date) : null)
|
||||||
|
|
||||||
|
const formatted = () => {
|
||||||
|
if (!props.date) return 'click to add due date'
|
||||||
|
return new Date(props.date).toLocaleDateString('ru-RU')
|
||||||
|
}
|
||||||
|
|
||||||
|
const startEdit = () => {
|
||||||
|
draft.value = props.date ? new Date(props.date) : null
|
||||||
|
editing.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
if (!draft.value) return
|
||||||
|
emit('changed', draft.value.toISOString())
|
||||||
|
editing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
draft.value = props.date ? new Date(props.date) : null
|
||||||
|
editing.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="issue-due-date">
|
||||||
|
<template v-if="!editing">
|
||||||
|
<v-hover #default="{ isHovering, props }">
|
||||||
|
<v-sheet v-bind="props" class="cursor-pointer d-flex align-center ga-1" @click="startEdit">
|
||||||
|
<div>{{ formatted() }}</div>
|
||||||
|
<v-icon v-if="isHovering" icon="mdi-pencil" size="small" />
|
||||||
|
</v-sheet>
|
||||||
|
</v-hover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="d-flex ga-1 align-center">
|
||||||
|
<v-date-input
|
||||||
|
input-format="dd-mm-yyyy"
|
||||||
|
autofocus
|
||||||
|
class="mb-1"
|
||||||
|
hide-details
|
||||||
|
prepend-icon=""
|
||||||
|
v-model="draft"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
persistent-placeholder
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-btn density="compact" prepend-icon="mdi-cancel" variant="text" text="Cancel" @click="cancel" />
|
||||||
|
<v-btn density="compact" prepend-icon="mdi-check" color="primary" variant="flat" text="Save" @click="save" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -7,6 +7,8 @@ import type { Issue } from '@/api/issue.api.ts'
|
|||||||
import StatusMenu from '@/components/StatusMenu.vue'
|
import StatusMenu from '@/components/StatusMenu.vue'
|
||||||
import AssigneeMenu from '@/components/AssigneeMenu.vue'
|
import AssigneeMenu from '@/components/AssigneeMenu.vue'
|
||||||
import ReporterInfo from '@/components/ReporterInfo.vue'
|
import ReporterInfo from '@/components/ReporterInfo.vue'
|
||||||
|
import IssueTitle from '@/components/IssueTitle.vue'
|
||||||
|
import IssueDate from '@/components/IssueDate.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ckey: string
|
ckey: string
|
||||||
@@ -15,6 +17,8 @@ interface Props {
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
statusChanged: [issue: Issue]
|
statusChanged: [issue: Issue]
|
||||||
assigneeChanged: [issue: Issue]
|
assigneeChanged: [issue: Issue]
|
||||||
|
titleChanged: [issue: Issue]
|
||||||
|
dueDateChanged: [issue: Issue]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const issueStore = useIssueStore()
|
const issueStore = useIssueStore()
|
||||||
@@ -40,6 +44,24 @@ async function fetch() {
|
|||||||
|
|
||||||
watchEffect(async () => fetch())
|
watchEffect(async () => fetch())
|
||||||
|
|
||||||
|
const onTitleChanged = async (title: string) => {
|
||||||
|
if (!issue.value) return
|
||||||
|
const updated = await issueStore.update(issue.value.id, { title })
|
||||||
|
if (updated) {
|
||||||
|
issue.value = updated
|
||||||
|
emit('titleChanged', updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDueDateChanged = async (date: string) => {
|
||||||
|
if (!issue.value) return
|
||||||
|
const updated = await issueStore.update(issue.value.id, { dueDate: date })
|
||||||
|
if (updated) {
|
||||||
|
issue.value = updated
|
||||||
|
emit('dueDateChanged', updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onContentChanged = async (content: string) => {
|
const onContentChanged = async (content: string) => {
|
||||||
if (!issue.value) return
|
if (!issue.value) return
|
||||||
const updated = await issueStore.update(issue.value.id, { content })
|
const updated = await issueStore.update(issue.value.id, { content })
|
||||||
@@ -78,7 +100,8 @@ defineExpose({
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="issue" class="d-flex flex-column ga-4 pa-4">
|
<div v-if="issue" class="d-flex flex-column ga-4 pa-4">
|
||||||
<div>{{ ckey }}</div>
|
<div>{{ ckey }}</div>
|
||||||
<div class="text-uppercase text-title-large">{{ issue.title }}</div>
|
|
||||||
|
<issue-title :text="issue.title" @changed="onTitleChanged" />
|
||||||
|
|
||||||
<v-card elevation="0" border>
|
<v-card elevation="0" border>
|
||||||
<v-card-text class="u-grid">
|
<v-card-text class="u-grid">
|
||||||
@@ -93,11 +116,14 @@ defineExpose({
|
|||||||
|
|
||||||
<div class="text-medium-emphasis">Created At:</div>
|
<div class="text-medium-emphasis">Created At:</div>
|
||||||
<div>{{ createdAtFormatted }}</div>
|
<div>{{ createdAtFormatted }}</div>
|
||||||
|
|
||||||
|
<div class="text-medium-emphasis">Due Date:</div>
|
||||||
|
<issue-date :date="issue.dueDate" @changed="onDueDateChanged" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<expansion-panel title="Details">
|
<expansion-panel title="Details">
|
||||||
<issue-content :value="issue.content" @changed="onContentChanged" />
|
<issue-content :text="issue.content" @changed="onContentChanged" />
|
||||||
</expansion-panel>
|
</expansion-panel>
|
||||||
|
|
||||||
<expansion-panel title="Activity">
|
<expansion-panel title="Activity">
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<{ changed: [text: string] }>()
|
||||||
|
|
||||||
|
const editing = ref(false)
|
||||||
|
const draft = ref(props.text ?? '')
|
||||||
|
|
||||||
|
const startEdit = () => {
|
||||||
|
draft.value = props.text ?? ''
|
||||||
|
editing.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
emit('changed', draft.value)
|
||||||
|
editing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
draft.value = props.text ?? ''
|
||||||
|
editing.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="issue-title">
|
||||||
|
<template v-if="!editing">
|
||||||
|
<v-hover #default="{ isHovering, props }">
|
||||||
|
<v-sheet
|
||||||
|
v-bind="props"
|
||||||
|
:class="isHovering ? 'px-3' : 'px-0'"
|
||||||
|
class="py-1 cursor-pointer title-sheet"
|
||||||
|
@click="startEdit"
|
||||||
|
>
|
||||||
|
<div class="text-uppercase text-title-large">
|
||||||
|
{{ text ?? 'click to write text' }}
|
||||||
|
</div>
|
||||||
|
</v-sheet>
|
||||||
|
</v-hover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<v-text-field autofocus class="mb-1" hide-details v-model="draft" variant="outlined" />
|
||||||
|
|
||||||
|
<div class="d-flex justify-end ga-1">
|
||||||
|
<v-btn density="compact" prepend-icon="mdi-cancel" variant="text" text="Cancel" @click="cancel" />
|
||||||
|
<v-btn density="compact" prepend-icon="mdi-check" color="primary" variant="flat" text="Save" @click="save" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.title-sheet:hover {
|
||||||
|
background-color: rgba(var(--v-theme-on-surface), 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-sheet {
|
||||||
|
transition: padding 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
text?: string
|
||||||
|
label?: string
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
text: 'text',
|
||||||
|
color: 'primary',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="d-flex align-center ga-1">
|
||||||
|
<v-chip v-if="props.label" size="small" :color="props.color" variant="flat" density="compact" :text="props.label" />
|
||||||
|
<div>{{ props.text }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useProjectStore } from '@/stores/project.ts'
|
import { useProjectStore } from '@/stores/project.ts'
|
||||||
|
import LabelText from '@/components/LabelText.vue'
|
||||||
|
|
||||||
const projectStore = useProjectStore()
|
const projectStore = useProjectStore()
|
||||||
const { projects, current } = storeToRefs(projectStore)
|
const { projects, current } = storeToRefs(projectStore)
|
||||||
@@ -35,12 +36,7 @@ onMounted(async () => {
|
|||||||
<v-menu>
|
<v-menu>
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<v-btn density="comfortable" v-bind="props">
|
<v-btn density="comfortable" v-bind="props">
|
||||||
<div class="d-flex ga-1 align-center">
|
<label-text :label="current?.key" :text="currentProjectName" />
|
||||||
<v-chip v-if="current" size="small" variant="flat" color="primary" density="compact">
|
|
||||||
{{ current.key }}
|
|
||||||
</v-chip>
|
|
||||||
<div>{{ currentProjectName }}</div>
|
|
||||||
</div>
|
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -55,9 +51,8 @@ onMounted(async () => {
|
|||||||
@update:selected="select"
|
@update:selected="select"
|
||||||
>
|
>
|
||||||
<v-list-item slim v-for="project in projects" :key="project.id" :value="project.id">
|
<v-list-item slim v-for="project in projects" :key="project.id" :value="project.id">
|
||||||
<v-list-item-title class="d-flex ga-1 align-center">
|
<v-list-item-title>
|
||||||
<v-chip size="small" color="primary" variant="flat" density="compact">{{ project.key }}</v-chip>
|
<label-text :label="project.key" :text="project.name" />
|
||||||
<div>{{ project.name }}</div>
|
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
|
|||||||
+3
-12
@@ -9,6 +9,7 @@ import { authClient } from '@/lib/auth-client.ts'
|
|||||||
import type { Issue } from '@/api/issue.api.ts'
|
import type { Issue } from '@/api/issue.api.ts'
|
||||||
import type { Project } from '@/api/project.api.ts'
|
import type { Project } from '@/api/project.api.ts'
|
||||||
import DashboardCountCard from '@/components/DashboardCountCard.vue'
|
import DashboardCountCard from '@/components/DashboardCountCard.vue'
|
||||||
|
import LabelText from '@/components/LabelText.vue'
|
||||||
|
|
||||||
const ACTIVE_STATUSES: Issue['status'][] = ['OPEN', 'IN_PROGRESS']
|
const ACTIVE_STATUSES: Issue['status'][] = ['OPEN', 'IN_PROGRESS']
|
||||||
|
|
||||||
@@ -177,18 +178,8 @@ function onClickCard(status?: string) {
|
|||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-divider />
|
<v-divider />
|
||||||
<v-list v-if="projects.length" density="compact">
|
<v-list v-if="projects.length" density="compact">
|
||||||
<v-list-item
|
<v-list-item v-for="project in projects.slice(0, 6)" :key="project.id" @click="openProject(project)">
|
||||||
v-for="project in projects.slice(0, 6)"
|
<label-text :label="project.key" :text="project.name" />
|
||||||
:key="project.id"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="openProject(project)"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<v-chip size="small" color="primary" variant="flat" density="compact">{{ project.key }}</v-chip>
|
|
||||||
</template>
|
|
||||||
<template #title>
|
|
||||||
<span class="ml-2">{{ project.name }}</span>
|
|
||||||
</template>
|
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
<v-card-text v-else class="text-medium-emphasis">Нет проектов</v-card-text>
|
<v-card-text v-else class="text-medium-emphasis">Нет проектов</v-card-text>
|
||||||
|
|||||||
@@ -21,13 +21,6 @@ const totalPages = ref(0)
|
|||||||
const serverItems = ref<Project[]>([])
|
const serverItems = ref<Project[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const headers = [
|
|
||||||
{ key: 'key', title: 'Key', sortable: false },
|
|
||||||
{ key: 'name', title: 'Name', sortable: false },
|
|
||||||
{ key: 'createdAt', title: 'Created', sortable: false },
|
|
||||||
{ key: 'actions', title: '', sortable: false, align: 'end' as const },
|
|
||||||
]
|
|
||||||
|
|
||||||
async function fetch() {
|
async function fetch() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -124,7 +117,6 @@ async function onDelete(project: Project) {
|
|||||||
deletingId.value = null
|
deletingId.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -138,7 +130,12 @@ async function onDelete(project: Project) {
|
|||||||
hover
|
hover
|
||||||
hide-default-footer
|
hide-default-footer
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:headers="headers"
|
:headers="[
|
||||||
|
{ key: 'key', title: 'Key', sortable: false, width: '8rem' },
|
||||||
|
{ key: 'name', title: 'Name', sortable: false },
|
||||||
|
{ key: 'createdAt', title: 'Created', sortable: false },
|
||||||
|
{ key: 'actions', title: '', sortable: false, align: 'end' as const },
|
||||||
|
]"
|
||||||
:items-per-page="itemsPerPage"
|
:items-per-page="itemsPerPage"
|
||||||
:page="page"
|
:page="page"
|
||||||
:items="serverItems"
|
:items="serverItems"
|
||||||
@@ -147,6 +144,10 @@ async function onDelete(project: Project) {
|
|||||||
@update:options="loadItems"
|
@update:options="loadItems"
|
||||||
@click:row="(_e: unknown, { item }: { item: Project }) => openIssues(item)"
|
@click:row="(_e: unknown, { item }: { item: Project }) => openIssues(item)"
|
||||||
>
|
>
|
||||||
|
<template #[`item.key`]="{ item }">
|
||||||
|
<v-chip size="small" color="primary" variant="flat" density="compact" :text="item.key" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #[`item.createdAt`]="{ item }">
|
<template #[`item.createdAt`]="{ item }">
|
||||||
{{ new Date(item.createdAt).toLocaleDateString('ru-RU') }}
|
{{ new Date(item.createdAt).toLocaleDateString('ru-RU') }}
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in new issue
Block a user