feat(dashboard): verbatim modal + defect feed on /defects (T8)
This commit is contained in:
47
app/components/DefectFeed.vue
Normal file
47
app/components/DefectFeed.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
// The defect feed (C6, F5): every filed defect beside the board, newest-first.
|
||||
// Pure presentation — the parent passes the already-ordered, bounded slice from
|
||||
// `/api/defects` (T6); we don't refetch or re-sort. Each entry carries the date,
|
||||
// section, project, reporter and the verbatim itself (the point of the feed).
|
||||
import { sections } from '~/board/definition'
|
||||
|
||||
interface FeedDefect {
|
||||
id: string
|
||||
sectionId: string
|
||||
projectName: string
|
||||
reporterEmail: string
|
||||
verbatim: string
|
||||
createdAt: string | Date
|
||||
}
|
||||
|
||||
defineProps<{ defects: FeedDefect[] }>()
|
||||
|
||||
const labelFor = (id: string) => sections.find((s) => s.id === id)?.label ?? id
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="flex w-full flex-col gap-3" data-test="defect-feed">
|
||||
<h2 class="text-lg font-bold">Feed</h2>
|
||||
|
||||
<p v-if="defects.length === 0" class="opacity-60" data-test="feed-empty">
|
||||
No defects filed yet.
|
||||
</p>
|
||||
|
||||
<ul v-else class="flex flex-col gap-2">
|
||||
<li
|
||||
v-for="defect in defects"
|
||||
:key="defect.id"
|
||||
class="border-base-300 flex flex-col gap-1 border-l-2 pl-3"
|
||||
data-test="feed-item"
|
||||
>
|
||||
<p class="text-sm">{{ defect.verbatim }}</p>
|
||||
<p class="flex flex-wrap gap-x-2 text-xs opacity-60">
|
||||
<span>{{ datestamp(defect.createdAt) }}</span>
|
||||
<span>· {{ labelFor(defect.sectionId) }}</span>
|
||||
<span>· {{ defect.projectName }}</span>
|
||||
<span>· {{ defect.reporterEmail }}</span>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
@@ -19,6 +19,10 @@ const props = defineProps<{
|
||||
cap?: number
|
||||
}>()
|
||||
|
||||
// Forward a Section click up so the page can open that section's verbatim modal
|
||||
// (T8); BoardView owns the click, DotMap just relays the id.
|
||||
defineEmits<{ select: [sectionId: string] }>()
|
||||
|
||||
const cap = computed(() => props.cap ?? 10)
|
||||
|
||||
// Group the feed by section, preserving its newest-first order.
|
||||
@@ -56,7 +60,7 @@ function dotStyle(index: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BoardView>
|
||||
<BoardView @select="$emit('select', $event)">
|
||||
<template #overlay="{ sectionId }">
|
||||
<span
|
||||
v-for="(defect, index) in dotsFor(sectionId)"
|
||||
|
||||
103
app/components/VerbatimModal.vue
Normal file
103
app/components/VerbatimModal.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
// Per-section verbatim list (C6, F5): clicking a Section/dot on the board opens
|
||||
// this modal over that section's defects without leaving the board. The list is
|
||||
// loaded lazily — `/api/defects?section=ID` (T6) — only when a section opens, so
|
||||
// a hot section's full history isn't paid for up front.
|
||||
//
|
||||
// Same native <dialog> (DaisyUI `modal`) as DefectForm: Escape, focus-trap and
|
||||
// backdrop come for free, and the parent-controlled `sectionId` drives open/close.
|
||||
import { sections } from '~/board/definition'
|
||||
|
||||
interface SectionDefect {
|
||||
id: string
|
||||
sectionId: string
|
||||
projectName: string
|
||||
reporterEmail: string
|
||||
verbatim: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
const props = defineProps<{ sectionId: string | null }>()
|
||||
const emit = defineEmits<{ close: [] }>()
|
||||
|
||||
const dialog = ref<HTMLDialogElement>()
|
||||
const defects = ref<SectionDefect[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const sectionLabel = computed(
|
||||
() => sections.find((s) => s.id === props.sectionId)?.label ?? '',
|
||||
)
|
||||
|
||||
// Lazily load the clicked section's history, bounded generously (well past the
|
||||
// dot cap so the modal shows more than the map does).
|
||||
async function load(sectionId: string) {
|
||||
loading.value = true
|
||||
defects.value = []
|
||||
try {
|
||||
defects.value = await $fetch<SectionDefect[]>('/api/defects', {
|
||||
query: { section: sectionId, limit: 200 },
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Mirror the parent-controlled section into the dialog's open state.
|
||||
function sync() {
|
||||
const el = dialog.value
|
||||
if (!el) return
|
||||
if (props.sectionId !== null) {
|
||||
if (!el.open) el.showModal()
|
||||
load(props.sectionId)
|
||||
} else if (el.open) {
|
||||
el.close()
|
||||
}
|
||||
}
|
||||
onMounted(sync)
|
||||
watch(() => props.sectionId, sync)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dialog ref="dialog" class="modal" @close="emit('close')">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">{{ sectionLabel }} — defects</h3>
|
||||
|
||||
<p v-if="loading" class="mt-4 opacity-60">Loading…</p>
|
||||
|
||||
<p
|
||||
v-else-if="defects.length === 0"
|
||||
class="mt-4 opacity-60"
|
||||
data-test="modal-empty"
|
||||
>
|
||||
No defects filed against this section yet.
|
||||
</p>
|
||||
|
||||
<ul v-else class="mt-4 flex flex-col gap-3">
|
||||
<li
|
||||
v-for="defect in defects"
|
||||
:key="defect.id"
|
||||
class="border-base-300 flex flex-col gap-1 border-l-2 pl-3"
|
||||
data-test="modal-item"
|
||||
>
|
||||
<p class="text-sm">{{ defect.verbatim }}</p>
|
||||
<p class="flex flex-wrap gap-x-2 text-xs opacity-60">
|
||||
<span>{{ datestamp(defect.createdAt) }}</span>
|
||||
<span>· {{ defect.projectName }}</span>
|
||||
<span>· {{ defect.reporterEmail }}</span>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn btn-ghost" @click="dialog?.close()">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Native backdrop: clicking it closes the dialog. -->
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button aria-label="Close">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user