feat: integrate tags in Timer and Entries views
Timer shows tag selector below description, saves tags on stop. Entries table displays tag chips per row with color coding. Tags loaded from store on mount.
This commit is contained in:
@@ -94,6 +94,16 @@
|
||||
<td class="px-4 py-3 text-[0.75rem] text-text-secondary">
|
||||
<span v-if="entry.description" v-html="renderMarkdown(entry.description)" class="markdown-inline" />
|
||||
<span v-else>-</span>
|
||||
<div v-if="entryTags[entry.id!]?.length" class="flex flex-wrap gap-1 mt-1">
|
||||
<span
|
||||
v-for="tagId in entryTags[entry.id!]"
|
||||
:key="tagId"
|
||||
class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[0.5625rem]"
|
||||
:style="{ backgroundColor: getTagColor(tagId) + '22', color: getTagColor(tagId) }"
|
||||
>
|
||||
{{ getTagName(tagId) }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-[0.75rem] font-mono text-accent-text">
|
||||
{{ formatDuration(entry.duration) }}
|
||||
@@ -174,6 +184,12 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div>
|
||||
<label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Tags</label>
|
||||
<AppTagInput v-model="editEntryTags" />
|
||||
</div>
|
||||
|
||||
<!-- Duration -->
|
||||
<div>
|
||||
<label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Duration (minutes)</label>
|
||||
@@ -257,14 +273,19 @@ import AppNumberInput from '../components/AppNumberInput.vue'
|
||||
import AppSelect from '../components/AppSelect.vue'
|
||||
import AppDatePicker from '../components/AppDatePicker.vue'
|
||||
import AppDiscardDialog from '../components/AppDiscardDialog.vue'
|
||||
import AppTagInput from '../components/AppTagInput.vue'
|
||||
import { useEntriesStore, type TimeEntry } from '../stores/entries'
|
||||
import { useProjectsStore } from '../stores/projects'
|
||||
import { useTagsStore } from '../stores/tags'
|
||||
import { formatDate } from '../utils/locale'
|
||||
import { useFormGuard } from '../utils/formGuard'
|
||||
import { renderMarkdown } from '../utils/markdown'
|
||||
|
||||
const entriesStore = useEntriesStore()
|
||||
const projectsStore = useProjectsStore()
|
||||
const tagsStore = useTagsStore()
|
||||
const entryTags = ref<Record<number, number[]>>({})
|
||||
const editEntryTags = ref<number[]>([])
|
||||
|
||||
const { showDiscardDialog, snapshot: snapshotForm, tryClose: tryCloseForm, confirmDiscard, cancelDiscard } = useFormGuard()
|
||||
|
||||
@@ -362,6 +383,28 @@ function formatDuration(seconds: number): string {
|
||||
return `${minutes}m`
|
||||
}
|
||||
|
||||
// Tag helper functions
|
||||
function getTagName(tagId: number): string {
|
||||
const tag = tagsStore.tags.find(t => t.id === tagId)
|
||||
return tag?.name || ''
|
||||
}
|
||||
|
||||
function getTagColor(tagId: number): string {
|
||||
const tag = tagsStore.tags.find(t => t.id === tagId)
|
||||
return tag?.color || '#6B7280'
|
||||
}
|
||||
|
||||
async function loadEntryTags() {
|
||||
const tags: Record<number, number[]> = {}
|
||||
for (const entry of entriesStore.entries) {
|
||||
if (entry.id) {
|
||||
const entryTagList = await tagsStore.getEntryTags(entry.id)
|
||||
tags[entry.id] = entryTagList.map(t => t.id!).filter(id => id != null)
|
||||
}
|
||||
}
|
||||
entryTags.value = tags
|
||||
}
|
||||
|
||||
// Duplicate an entry with current timestamp
|
||||
async function duplicateEntry(entry: TimeEntry) {
|
||||
const now = new Date()
|
||||
@@ -375,6 +418,7 @@ async function duplicateEntry(entry: TimeEntry) {
|
||||
}
|
||||
await entriesStore.createEntry(newEntry)
|
||||
await entriesStore.fetchEntries()
|
||||
await loadEntryTags()
|
||||
}
|
||||
|
||||
// Copy yesterday's entries to today
|
||||
@@ -401,6 +445,7 @@ async function copyPreviousDay() {
|
||||
})
|
||||
}
|
||||
await entriesStore.fetchEntries()
|
||||
await loadEntryTags()
|
||||
}
|
||||
|
||||
// Copy last week's entries shifted forward 7 days
|
||||
@@ -428,23 +473,26 @@ async function copyPreviousWeek() {
|
||||
})
|
||||
}
|
||||
await entriesStore.fetchEntries()
|
||||
await loadEntryTags()
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
function applyFilters() {
|
||||
entriesStore.fetchEntries(startDate.value || undefined, endDate.value || undefined)
|
||||
async function applyFilters() {
|
||||
await entriesStore.fetchEntries(startDate.value || undefined, endDate.value || undefined)
|
||||
await loadEntryTags()
|
||||
}
|
||||
|
||||
// Clear filters
|
||||
function clearFilters() {
|
||||
async function clearFilters() {
|
||||
startDate.value = ''
|
||||
endDate.value = ''
|
||||
filterProject.value = null
|
||||
entriesStore.fetchEntries()
|
||||
await entriesStore.fetchEntries()
|
||||
await loadEntryTags()
|
||||
}
|
||||
|
||||
// Open edit dialog
|
||||
function openEditDialog(entry: TimeEntry) {
|
||||
async function openEditDialog(entry: TimeEntry) {
|
||||
editingEntry.value = entry
|
||||
editForm.id = entry.id || 0
|
||||
editForm.project_id = entry.project_id
|
||||
@@ -460,6 +508,14 @@ function openEditDialog(entry: TimeEntry) {
|
||||
editHour.value = dt.getHours()
|
||||
editMinute.value = dt.getMinutes()
|
||||
|
||||
// Load tags for this entry
|
||||
if (entry.id) {
|
||||
const tags = await tagsStore.getEntryTags(entry.id)
|
||||
editEntryTags.value = tags.map(t => t.id!).filter(id => id != null)
|
||||
} else {
|
||||
editEntryTags.value = []
|
||||
}
|
||||
|
||||
snapshotForm(getEditFormData())
|
||||
showEditDialog.value = true
|
||||
}
|
||||
@@ -488,6 +544,13 @@ async function handleEdit() {
|
||||
}
|
||||
|
||||
await entriesStore.updateEntry(updatedEntry)
|
||||
|
||||
// Save tags for the edited entry
|
||||
if (editForm.id) {
|
||||
await tagsStore.setEntryTags(editForm.id, editEntryTags.value)
|
||||
await loadEntryTags()
|
||||
}
|
||||
|
||||
closeEditDialog()
|
||||
}
|
||||
}
|
||||
@@ -516,9 +579,12 @@ async function handleDelete() {
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
entriesStore.fetchEntries(),
|
||||
projectsStore.fetchProjects()
|
||||
projectsStore.fetchProjects(),
|
||||
tagsStore.fetchTags()
|
||||
])
|
||||
|
||||
await loadEntryTags()
|
||||
|
||||
// Set default date range to this week
|
||||
const now = new Date()
|
||||
const weekStart = new Date(now)
|
||||
|
||||
@@ -104,6 +104,10 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Tags</label>
|
||||
<AppTagInput v-model="selectedTags" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent entries -->
|
||||
@@ -183,10 +187,12 @@ import { useToastStore } from '../stores/toast'
|
||||
import { Timer as TimerIcon, RotateCcw, ExternalLink, Star } from 'lucide-vue-next'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import AppSelect from '../components/AppSelect.vue'
|
||||
import AppTagInput from '../components/AppTagInput.vue'
|
||||
import IdlePromptDialog from '../components/IdlePromptDialog.vue'
|
||||
import AppTrackingPromptDialog from '../components/AppTrackingPromptDialog.vue'
|
||||
import { formatDateTime } from '../utils/locale'
|
||||
import { useFavoritesStore, type Favorite } from '../stores/favorites'
|
||||
import { useTagsStore } from '../stores/tags'
|
||||
|
||||
const timerStore = useTimerStore()
|
||||
const projectsStore = useProjectsStore()
|
||||
@@ -194,12 +200,14 @@ const entriesStore = useEntriesStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const toastStore = useToastStore()
|
||||
const favoritesStore = useFavoritesStore()
|
||||
const tagsStore = useTagsStore()
|
||||
const favorites = computed(() => favoritesStore.favorites)
|
||||
|
||||
// Local state for inputs
|
||||
const selectedProject = ref<number | null>(timerStore.selectedProjectId)
|
||||
const selectedTask = ref<number | null>(timerStore.selectedTaskId)
|
||||
const description = ref(timerStore.description)
|
||||
const selectedTags = ref<number[]>([])
|
||||
const projectTasks = ref<Task[]>([])
|
||||
|
||||
// Split timer into parts for colon animation
|
||||
@@ -300,7 +308,7 @@ async function openMiniTimer() {
|
||||
}
|
||||
|
||||
// Toggle timer
|
||||
function toggleTimer() {
|
||||
async function toggleTimer() {
|
||||
if (timerStore.isStopped) {
|
||||
if (!selectedProject.value) {
|
||||
toastStore.info('Please select a project before starting the timer')
|
||||
@@ -309,7 +317,16 @@ function toggleTimer() {
|
||||
timerStore.start()
|
||||
} else {
|
||||
timerStore.stop()
|
||||
entriesStore.fetchEntries()
|
||||
await entriesStore.fetchEntries()
|
||||
|
||||
// Save tags for the new entry if any were selected
|
||||
if (selectedTags.value.length > 0) {
|
||||
const entries = entriesStore.entries
|
||||
if (entries.length > 0 && entries[0].id) {
|
||||
await tagsStore.setEntryTags(entries[0].id, selectedTags.value)
|
||||
}
|
||||
}
|
||||
selectedTags.value = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,7 +407,8 @@ onMounted(async () => {
|
||||
projectsStore.fetchProjects(),
|
||||
entriesStore.fetchEntries(),
|
||||
settingsStore.fetchSettings(),
|
||||
favoritesStore.fetchFavorites()
|
||||
favoritesStore.fetchFavorites(),
|
||||
tagsStore.fetchTags()
|
||||
])
|
||||
|
||||
// Restore timer state
|
||||
|
||||
Reference in New Issue
Block a user