criterios aceptacion: parsear Quill HTML a lista JSON + tooltip en dashboard
This commit is contained in:
@@ -22,3 +22,27 @@ export function extractFirstSentence(text: string, maxLen = 200): string {
|
|||||||
export function stripHtmlTags(html: string): string {
|
export function stripHtmlTags(html: string): string {
|
||||||
return stripHtml(html)
|
return stripHtml(html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseQuillList(html: string): string[] {
|
||||||
|
if (!html) return []
|
||||||
|
const items: string[] = []
|
||||||
|
const liRegex = /<li[^>]*>([\s\S]*?)<\/li>/gi
|
||||||
|
let match
|
||||||
|
while ((match = liRegex.exec(html)) !== null) {
|
||||||
|
const text = match[1].replace(/<[^>]*>/g, '').trim()
|
||||||
|
if (text) items.push(text)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
export function criteriaToJson(html: string): string {
|
||||||
|
return JSON.stringify(parseQuillList(html))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function criteriaFromJson(json: string): string[] {
|
||||||
|
try {
|
||||||
|
return JSON.parse(json)
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { kappa } from '@/services/kappa-api'
|
import { kappa } from '@/services/kappa-api'
|
||||||
import { tauriDb, type UserStoryRecord, type EpicRecord } from '@/services/tauri-db'
|
import { tauriDb, type UserStoryRecord, type EpicRecord } from '@/services/tauri-db'
|
||||||
import { stripHtml } from '@/services/clean-html'
|
import { stripHtml } from '@/services/clean-html'
|
||||||
|
import { criteriaToJson, parseQuillList } from '@/services/clean-html'
|
||||||
import { parseHierarchy, stripHierarchy, getItemType, type ItemType } from '@/services/hierarchy'
|
import { parseHierarchy, stripHierarchy, getItemType, type ItemType } from '@/services/hierarchy'
|
||||||
import type { KappaUserStory, KappaLogbookEntry, KappaPlanningEntry, KappaEpicDevelopment } from '@/types/kappa'
|
import type { KappaUserStory, KappaLogbookEntry, KappaPlanningEntry, KappaEpicDevelopment } from '@/types/kappa'
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ export interface EnrichedUserStory extends KappaUserStory {
|
|||||||
_itemType: ItemType
|
_itemType: ItemType
|
||||||
_hierarchyPath: string | null
|
_hierarchyPath: string | null
|
||||||
_cleanTitle: string
|
_cleanTitle: string
|
||||||
|
_criteriaList: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnrichedEpic extends KappaEpicDevelopment {
|
export interface EnrichedEpic extends KappaEpicDevelopment {
|
||||||
@@ -55,15 +57,20 @@ export const useWorkItemsStore = defineStore('workitems', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function enrichHU(hu: { title?: string; id?: number }, initiativeId: number): EnrichedUserStory {
|
function enrichHU(hu: { title?: string; id?: number; acceptance_criteria?: string | null; criterios_aceptacion?: string | null }, initiativeId: number): EnrichedUserStory {
|
||||||
const h = parseHierarchy(hu.title || '')
|
const h = parseHierarchy(hu.title || '')
|
||||||
|
const rawCriteria = hu.acceptance_criteria || hu.criterios_aceptacion || ''
|
||||||
|
const criteriaList = rawCriteria ? parseQuillList(rawCriteria) : []
|
||||||
return {
|
return {
|
||||||
id: hu.id ?? 0,
|
id: hu.id ?? 0,
|
||||||
title: hu.title || '',
|
title: hu.title || '',
|
||||||
|
acceptance_criteria: rawCriteria,
|
||||||
|
criterios_aceptacion: rawCriteria,
|
||||||
initiative: initiativeId,
|
initiative: initiativeId,
|
||||||
_itemType: h?.items[h.items.length - 1]?.type || 'U',
|
_itemType: h?.items[h.items.length - 1]?.type || 'U',
|
||||||
_hierarchyPath: h?.fullPath ?? null,
|
_hierarchyPath: h?.fullPath ?? null,
|
||||||
_cleanTitle: h ? stripHierarchy(hu.title || '') : (hu.title || ''),
|
_cleanTitle: h ? stripHierarchy(hu.title || '') : (hu.title || ''),
|
||||||
|
_criteriaList: criteriaList,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +173,7 @@ export const useWorkItemsStore = defineStore('workitems', () => {
|
|||||||
code: safeStr(hu.code),
|
code: safeStr(hu.code),
|
||||||
title: String(hu.title || ''),
|
title: String(hu.title || ''),
|
||||||
description: stripHtml(String(hu.description || '')),
|
description: stripHtml(String(hu.description || '')),
|
||||||
acceptance_criteria: safeStr(hu.acceptance_criteria),
|
acceptance_criteria: (hu.acceptance_criteria || hu.criterios_aceptacion || null) as string | null,
|
||||||
status: safeStr(hu.status),
|
status: safeStr(hu.status),
|
||||||
priority: safeStr(hu.priority),
|
priority: safeStr(hu.priority),
|
||||||
story_points: null,
|
story_points: null,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export interface KappaUserStory {
|
|||||||
title: string
|
title: string
|
||||||
description?: string
|
description?: string
|
||||||
acceptance_criteria?: string
|
acceptance_criteria?: string
|
||||||
|
criterios_aceptacion?: string
|
||||||
status?: string
|
status?: string
|
||||||
priority?: string
|
priority?: string
|
||||||
initiative: number | string
|
initiative: number | string
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { useProjectsStore } from '@/stores/projects'
|
import { useProjectsStore } from '@/stores/projects'
|
||||||
import { useWorkItemsStore } from '@/stores/workitems'
|
import { useWorkItemsStore } from '@/stores/workitems'
|
||||||
import { getTypeLabel, getTypeColor, getTypeIcon } from '@/services/hierarchy'
|
import { getTypeLabel, getTypeColor, getTypeIcon } from '@/services/hierarchy'
|
||||||
import { Activity, FileText, Layers, Clock } from 'lucide-vue-next'
|
import { Activity, FileText, Layers, Clock, Info } from 'lucide-vue-next'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
@@ -208,8 +208,20 @@ const statusLabel = (status: unknown) => {
|
|||||||
{{ getTypeLabel(hu._itemType) }}
|
{{ getTypeLabel(hu._itemType) }}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-sm max-w-[280px] truncate">
|
<TableCell class="text-sm max-w-[280px] truncate flex items-center gap-1">
|
||||||
{{ hu._cleanTitle || hu.title }}
|
<span class="truncate">{{ hu._cleanTitle || hu.title }}</span>
|
||||||
|
<span
|
||||||
|
v-if="hu._criteriaList?.length"
|
||||||
|
class="group relative inline-flex flex-shrink-0 cursor-help"
|
||||||
|
>
|
||||||
|
<Info class="size-3.5 text-muted-foreground hover:text-foreground transition-colors" />
|
||||||
|
<div class="absolute bottom-full left-0 mb-2 w-72 p-3 rounded-lg border bg-popover text-popover-foreground text-xs shadow-md opacity-0 group-hover:opacity-100 transition-opacity z-50 pointer-events-none">
|
||||||
|
<p class="font-semibold mb-1.5 text-[11px] uppercase tracking-wider text-muted-foreground">Criterios de aceptación</p>
|
||||||
|
<ol class="list-decimal list-inside space-y-1 text-[11px]">
|
||||||
|
<li v-for="(c, i) in hu._criteriaList" :key="i">{{ c }}</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge :variant="statusVariant(hu.status || '')" class="text-xs capitalize">
|
<Badge :variant="statusVariant(hu.status || '')" class="text-xs capitalize">
|
||||||
|
|||||||
Reference in New Issue
Block a user