Compare commits

...
3 Commits
Author SHA1 Message Date
vadim a0c557f9b2 list options
Gitea Actions / Deploy (push) Successful in 39s
2026-10-14 11:19:21 +03:00
vadim 66a6400cda add content in list, add list options 2026-10-14 11:01:54 +03:00
vadim b2444a8118 typo 2026-10-14 09:18:04 +03:00
7 changed files with 95 additions and 12 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
name: Gitea Actions Demo name: Gitea Actions
run-name: Deploy 🚀 run-name: Deploy 🚀
on: on:
push: push:
+42
View File
@@ -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>
+24 -8
View File
@@ -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>
+1 -1
View File
@@ -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
+21
View File
@@ -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 },
),
)
+2 -1
View File
@@ -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>
+4 -1
View File
@@ -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++
} }