Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0c557f9b2 | ||
|
|
66a6400cda | ||
|
|
b2444a8118 |
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
name: Gitea Actions Demo
|
name: Gitea Actions
|
||||||
run-name: Deploy 🚀
|
run-name: Deploy 🚀
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ISSUE_LIST_FIELDS, useIssueListOptions } from '@/composables/useIssueListOptions.ts'
|
||||||
|
|
||||||
|
const visible = useIssueListOptions()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-menu width="15rem" :close-on-content-click="false">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn density="compact" icon="mdi-dots-horizontal" v-bind="props" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list border density="compact">
|
||||||
|
<v-list-item rounded="0" class="py-0" slim v-for="el in ISSUE_LIST_FIELDS" :key="el.alias">
|
||||||
|
<v-list-item-title>
|
||||||
|
<div class="menu-grid px-1">
|
||||||
|
<div>{{ el.title }}</div>
|
||||||
|
<v-switch
|
||||||
|
v-model="visible[el.alias]"
|
||||||
|
inset="material"
|
||||||
|
size="x-small"
|
||||||
|
color="success"
|
||||||
|
true-icon="mdi-check"
|
||||||
|
false-icon="mdi-close"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.menu-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,6 +4,7 @@ import StatusMenu from '@/components/StatusMenu.vue'
|
|||||||
import { useIssueStore } from '@/stores/issue.ts'
|
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'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
issue: Issue
|
issue: Issue
|
||||||
@@ -19,6 +20,7 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const issueStore = useIssueStore()
|
const issueStore = useIssueStore()
|
||||||
|
const visible = useIssueListOptions()
|
||||||
|
|
||||||
const onStatusChanged = async (status: Issue['status']) => {
|
const onStatusChanged = async (status: Issue['status']) => {
|
||||||
const updated = await issueStore.update(issue.id, { status })
|
const updated = await issueStore.update(issue.id, { status })
|
||||||
@@ -39,15 +41,25 @@ const onAssigneeChanged = async (assignee: string) => {
|
|||||||
<template>
|
<template>
|
||||||
<v-list-item class="bg-on-surface-variant" :value="issue.key">
|
<v-list-item class="bg-on-surface-variant" :value="issue.key">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="d-flex ga-4">
|
<div class="d-flex ga-4 min-w-0 mr-4">
|
||||||
<div>{{ issue.key }}</div>
|
<div class="flex-shrink-0">{{ issue.key }}</div>
|
||||||
<div class="d-flex ga-1 align-center">
|
<div class="d-flex ga-4 align-center min-w-0">
|
||||||
<v-icon
|
<v-icon
|
||||||
v-if="[3, 4].includes(issue.priority) && ['OPEN', 'IN_PROGRESS'].includes(issue.status)"
|
v-if="[3, 4].includes(issue.priority) && ['OPEN', 'IN_PROGRESS'].includes(issue.status)"
|
||||||
icon="mdi-alert-box"
|
icon="mdi-alert-box"
|
||||||
color="error"
|
color="error"
|
||||||
/>
|
/>
|
||||||
<div :class="{ 'text-decoration-line-through': issue.status === 'DONE' }">{{ issue.title }}</div>
|
<div v-if="visible.title" class="flex-shrink-0" :class="{ 'text-decoration-line-through': issue.status === 'DONE' }">
|
||||||
|
{{ issue.title }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="visible.content"
|
||||||
|
:class="{ 'text-decoration-line-through': issue.status === 'DONE' }"
|
||||||
|
class="text-body-medium text-truncate min-w-0"
|
||||||
|
:title="issue.content"
|
||||||
|
>
|
||||||
|
{{ issue.content }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -65,12 +77,16 @@ const onAssigneeChanged = async (assignee: string) => {
|
|||||||
|
|
||||||
<template #append>
|
<template #append>
|
||||||
<div class="d-flex ga-4 align-center">
|
<div class="d-flex ga-4 align-center">
|
||||||
<status-menu :value="issue.status" @changed="onStatusChanged" />
|
<status-menu v-if="visible.status" :value="issue.status" @changed="onStatusChanged" />
|
||||||
<priority-menu :value="issue.priority" @changed="onPriorityChanged" />
|
<priority-menu v-if="visible.priority" :value="issue.priority" @changed="onPriorityChanged" />
|
||||||
<assignee-menu :value="issue.assignee" @changed="onAssigneeChanged" />
|
<assignee-menu v-if="visible.assignee" :value="issue.assignee" @changed="onAssigneeChanged" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss">
|
||||||
|
.min-w-0 {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -28,7 +28,7 @@ const select = (value: unknown) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="">
|
<div>
|
||||||
<v-menu>
|
<v-menu>
|
||||||
<template v-slot:activator="{ props: activatorProps }">
|
<template v-slot:activator="{ props: activatorProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { createGlobalState, useLocalStorage } from '@vueuse/core'
|
||||||
|
|
||||||
|
export const ISSUE_LIST_FIELDS = [
|
||||||
|
{ title: 'Title', alias: 'title' },
|
||||||
|
{ title: 'Content', alias: 'content' },
|
||||||
|
{ title: 'Status', alias: 'status' },
|
||||||
|
{ title: 'Priority', alias: 'priority' },
|
||||||
|
{ title: 'Assignee', alias: 'assignee' },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type IssueListField = (typeof ISSUE_LIST_FIELDS)[number]['alias']
|
||||||
|
|
||||||
|
/** Persisted visibility of the issue list element fields, shared by every list element. */
|
||||||
|
export const useIssueListOptions = createGlobalState(() =>
|
||||||
|
useLocalStorage<Record<IssueListField, boolean>>(
|
||||||
|
'issue-list:visible-fields',
|
||||||
|
{ title: true, content: true, status: true, priority: true, assignee: true },
|
||||||
|
// Fields added later stay visible for users who already have saved options
|
||||||
|
{ mergeDefaults: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -12,6 +12,7 @@ import IssueStats from '@/components/IssueStats.vue'
|
|||||||
import IssueFilter from '@/components/IssueFilter.vue'
|
import IssueFilter from '@/components/IssueFilter.vue'
|
||||||
import SplitHandle from '@/components/SplitHandle.vue'
|
import SplitHandle from '@/components/SplitHandle.vue'
|
||||||
import { useSplitPane } from '@/composables/useSplitPane.ts'
|
import { useSplitPane } from '@/composables/useSplitPane.ts'
|
||||||
|
import IssueListOptions from '@/components/IssueListOptions.vue'
|
||||||
|
|
||||||
const issueStore = useIssueStore()
|
const issueStore = useIssueStore()
|
||||||
const issueDetailsRef = ref<InstanceType<typeof IssueDetails>>()
|
const issueDetailsRef = ref<InstanceType<typeof IssueDetails>>()
|
||||||
@@ -220,7 +221,7 @@ async function onDeleteBatch() {
|
|||||||
<template #append>
|
<template #append>
|
||||||
<div class="d-flex ga-4 align-center">
|
<div class="d-flex ga-4 align-center">
|
||||||
<issue-stats />
|
<issue-stats />
|
||||||
<v-btn density="compact" icon="mdi-dots-horizontal"></v-btn>
|
<issue-list-options />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ interface CalendarRange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const UNSCHEDULED_LIMIT = 20
|
const UNSCHEDULED_LIMIT = 20
|
||||||
|
/** Closed issues are not worth scheduling, so the side list shows only these */
|
||||||
|
const UNSCHEDULED_STATUSES: Issue['status'][] = ['OPEN', 'IN_PROGRESS']
|
||||||
const DRAG_TYPE = 'application/x-ulitka-issue'
|
const DRAG_TYPE = 'application/x-ulitka-issue'
|
||||||
|
|
||||||
const issueStore = useIssueStore()
|
const issueStore = useIssueStore()
|
||||||
@@ -118,6 +120,7 @@ async function loadUnscheduled(page = 1) {
|
|||||||
limit: UNSCHEDULED_LIMIT,
|
limit: UNSCHEDULED_LIMIT,
|
||||||
text: search.value,
|
text: search.value,
|
||||||
projectId: currentProjectId.value,
|
projectId: currentProjectId.value,
|
||||||
|
status: UNSCHEDULED_STATUSES,
|
||||||
hasDueDate: false,
|
hasDueDate: false,
|
||||||
})
|
})
|
||||||
if (requestId !== unscheduledRequestId) return
|
if (requestId !== unscheduledRequestId) return
|
||||||
@@ -158,7 +161,7 @@ async function setDueDate(issue: Issue, date: string | null) {
|
|||||||
if (updated.dueDate) {
|
if (updated.dueDate) {
|
||||||
scheduled.value.push(updated)
|
scheduled.value.push(updated)
|
||||||
if (wasUnscheduled) unscheduledTotal.value--
|
if (wasUnscheduled) unscheduledTotal.value--
|
||||||
} else {
|
} else if (UNSCHEDULED_STATUSES.includes(updated.status)) {
|
||||||
unscheduled.value.unshift(updated)
|
unscheduled.value.unshift(updated)
|
||||||
unscheduledTotal.value++
|
unscheduledTotal.value++
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user