appends requirement edition mode
This commit is contained in:
@@ -1,39 +1,181 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import Button from 'primevue/button'
|
||||
import Chip from 'primevue/chip'
|
||||
import Divider from 'primevue/divider'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import TabView from 'primevue/tabview'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import Tag from 'primevue/tag'
|
||||
|
||||
export interface Requirement {
|
||||
id: number
|
||||
reference: string
|
||||
title: string
|
||||
status: 'Draft' | 'In Review' | 'Approved' | 'Blocked' | 'Delivered'
|
||||
priority: 'Low' | 'Medium' | 'High' | 'Critical'
|
||||
owner: string
|
||||
progress: number
|
||||
dueDate: string
|
||||
description: string
|
||||
rationale: string
|
||||
acceptanceCriteria: string[]
|
||||
impactedModules: string[]
|
||||
blockers: string[]
|
||||
notes: string
|
||||
stakeholders: string[]
|
||||
flexibility: string
|
||||
tolerances: string
|
||||
}
|
||||
import Textarea from 'primevue/textarea'
|
||||
import type { Requirement, RequirementPriority, RequirementStatus } from '../stores/requirements'
|
||||
|
||||
interface Props {
|
||||
requirement: Requirement
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
interface FormState {
|
||||
title: string
|
||||
owner: string
|
||||
status: RequirementStatus
|
||||
priority: RequirementPriority
|
||||
description: string
|
||||
flexibility: string
|
||||
tolerances: string
|
||||
notes: string
|
||||
stakeholders: string
|
||||
acceptanceCriteria: string
|
||||
impactedModules: string
|
||||
blockers: string
|
||||
}
|
||||
|
||||
const statusSeverity = (status: Requirement['status']) => {
|
||||
const map: Record<Requirement['status'], 'secondary' | 'info' | 'success' | 'danger' | 'contrast'> = {
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'save', requirement: Requirement): void
|
||||
(e: 'dirty-change', value: boolean): void
|
||||
}>()
|
||||
|
||||
const isEditing = ref(false)
|
||||
|
||||
const form = reactive<FormState>({
|
||||
title: '',
|
||||
owner: '',
|
||||
status: 'Draft',
|
||||
priority: 'Low',
|
||||
description: '',
|
||||
flexibility: '',
|
||||
tolerances: '',
|
||||
notes: '',
|
||||
stakeholders: '',
|
||||
acceptanceCriteria: '',
|
||||
impactedModules: '',
|
||||
blockers: '',
|
||||
})
|
||||
|
||||
const errors = reactive<Record<string, string>>({})
|
||||
|
||||
const normalizeCsv = (value: string) => csvToList(value).join(',')
|
||||
const normalizeMultiline = (value: string) => multilineToList(value).join('|')
|
||||
|
||||
const listToCsv = (value: string[]) => value.join(', ')
|
||||
const listToMultiline = (value: string[]) => value.join('\n')
|
||||
|
||||
const csvToList = (value: string) =>
|
||||
value
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const multilineToList = (value: string) =>
|
||||
value
|
||||
.split('\n')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const resetForm = (requirement: Requirement) => {
|
||||
form.title = requirement.title
|
||||
form.owner = requirement.owner
|
||||
form.status = requirement.status
|
||||
form.priority = requirement.priority
|
||||
form.description = requirement.description
|
||||
form.flexibility = requirement.flexibility
|
||||
form.tolerances = requirement.tolerances
|
||||
form.notes = requirement.notes
|
||||
form.stakeholders = listToCsv(requirement.stakeholders)
|
||||
form.acceptanceCriteria = listToMultiline(requirement.acceptanceCriteria)
|
||||
form.impactedModules = listToCsv(requirement.impactedModules)
|
||||
form.blockers = listToMultiline(requirement.blockers)
|
||||
}
|
||||
|
||||
const clearErrors = () => {
|
||||
Object.keys(errors).forEach((key) => delete errors[key])
|
||||
}
|
||||
|
||||
const startEdit = () => {
|
||||
resetForm(props.requirement)
|
||||
clearErrors()
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
isEditing.value = false
|
||||
clearErrors()
|
||||
resetForm(props.requirement)
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
clearErrors()
|
||||
|
||||
if (!form.title.trim()) {
|
||||
errors.title = 'Title is required.'
|
||||
}
|
||||
|
||||
if (!form.owner.trim()) {
|
||||
errors.owner = 'Author is required.'
|
||||
}
|
||||
|
||||
if (!form.description.trim()) {
|
||||
errors.description = 'Description is required.'
|
||||
}
|
||||
|
||||
if (csvToList(form.stakeholders).length === 0) {
|
||||
errors.stakeholders = 'At least one stakeholder is required.'
|
||||
}
|
||||
|
||||
return Object.keys(errors).length === 0
|
||||
}
|
||||
|
||||
const saveEdit = () => {
|
||||
if (!validate()) {
|
||||
return
|
||||
}
|
||||
|
||||
isEditing.value = false
|
||||
|
||||
emit('save', {
|
||||
...props.requirement,
|
||||
title: form.title.trim(),
|
||||
owner: form.owner.trim(),
|
||||
status: form.status,
|
||||
priority: form.priority,
|
||||
description: form.description.trim(),
|
||||
flexibility: form.flexibility.trim(),
|
||||
tolerances: form.tolerances.trim(),
|
||||
notes: form.notes.trim(),
|
||||
stakeholders: csvToList(form.stakeholders),
|
||||
acceptanceCriteria: multilineToList(form.acceptanceCriteria),
|
||||
impactedModules: csvToList(form.impactedModules),
|
||||
blockers: multilineToList(form.blockers),
|
||||
})
|
||||
|
||||
clearErrors()
|
||||
}
|
||||
|
||||
const isDirty = computed(() => {
|
||||
if (!isEditing.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
form.title.trim() !== props.requirement.title ||
|
||||
form.owner.trim() !== props.requirement.owner ||
|
||||
form.status !== props.requirement.status ||
|
||||
form.priority !== props.requirement.priority ||
|
||||
form.description.trim() !== props.requirement.description ||
|
||||
form.flexibility.trim() !== props.requirement.flexibility ||
|
||||
form.tolerances.trim() !== props.requirement.tolerances ||
|
||||
form.notes.trim() !== props.requirement.notes ||
|
||||
normalizeCsv(form.stakeholders) !== props.requirement.stakeholders.join(',') ||
|
||||
normalizeCsv(form.impactedModules) !== props.requirement.impactedModules.join(',') ||
|
||||
normalizeMultiline(form.acceptanceCriteria) !==
|
||||
props.requirement.acceptanceCriteria.join('|') ||
|
||||
normalizeMultiline(form.blockers) !== props.requirement.blockers.join('|')
|
||||
)
|
||||
})
|
||||
|
||||
const statusSeverity = (status: RequirementStatus) => {
|
||||
const map: Record<RequirementStatus, 'secondary' | 'info' | 'success' | 'danger' | 'contrast'> = {
|
||||
Draft: 'secondary',
|
||||
'In Review': 'info',
|
||||
Approved: 'success',
|
||||
@@ -43,8 +185,8 @@ const statusSeverity = (status: Requirement['status']) => {
|
||||
return map[status]
|
||||
}
|
||||
|
||||
const prioritySeverity = (priority: Requirement['priority']) => {
|
||||
const map: Record<Requirement['priority'], 'success' | 'info' | 'warn' | 'danger'> = {
|
||||
const prioritySeverity = (priority: RequirementPriority) => {
|
||||
const map: Record<RequirementPriority, 'success' | 'info' | 'warn' | 'danger'> = {
|
||||
Low: 'success',
|
||||
Medium: 'info',
|
||||
High: 'warn',
|
||||
@@ -52,42 +194,113 @@ const prioritySeverity = (priority: Requirement['priority']) => {
|
||||
}
|
||||
return map[priority]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.requirement,
|
||||
(requirement) => {
|
||||
resetForm(requirement)
|
||||
clearErrors()
|
||||
isEditing.value = false
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
watch(isDirty, (value) => {
|
||||
emit('dirty-change', value)
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="detail-zone">
|
||||
<section class="detail-surface">
|
||||
<header class="detail-card__header">
|
||||
<div>
|
||||
<header
|
||||
class="detail-card__header"
|
||||
:class="{ 'detail-card__header--editing': isEditing }"
|
||||
>
|
||||
<div class="detail-card__main">
|
||||
<p class="eyebrow">Selected requirement</p>
|
||||
<h2>{{ requirement.title }}</h2>
|
||||
<h2 v-if="!isEditing">{{ requirement.title }}</h2>
|
||||
<InputText
|
||||
v-else
|
||||
v-model="form.title"
|
||||
class="edit-input"
|
||||
placeholder="Requirement title"
|
||||
/>
|
||||
<small v-if="errors.title" class="error-text">{{ errors.title }}</small>
|
||||
<p class="detail-card__subtitle">
|
||||
{{ requirement.reference }} - Owner: {{ requirement.owner }}
|
||||
{{ requirement.reference }} - Owner: {{ isEditing ? form.owner : requirement.owner }}
|
||||
</p>
|
||||
<p class="detail-card__uuid">UUID: {{ requirement.uuid }}</p>
|
||||
</div>
|
||||
|
||||
<div class="detail-actions">
|
||||
<Button label="Edit" icon="pi pi-pencil" severity="secondary" outlined />
|
||||
<Button
|
||||
v-if="!isEditing"
|
||||
label="Edit"
|
||||
icon="pi pi-pencil"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="startEdit"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<TabView class="detail-tabs">
|
||||
<TabPanel value="details" header="Details">
|
||||
<div class="detail-badges">
|
||||
<Tag :value="requirement.status" :severity="statusSeverity(requirement.status)" />
|
||||
<Tag :value="requirement.priority" :severity="prioritySeverity(requirement.priority)" rounded />
|
||||
<Tag
|
||||
v-if="!isEditing"
|
||||
:value="requirement.status"
|
||||
:severity="statusSeverity(requirement.status)"
|
||||
/>
|
||||
<select v-else v-model="form.status" class="edit-select">
|
||||
<option value="Draft">Draft</option>
|
||||
<option value="In Review">In Review</option>
|
||||
<option value="Approved">Approved</option>
|
||||
<option value="Blocked">Blocked</option>
|
||||
<option value="Delivered">Delivered</option>
|
||||
</select>
|
||||
|
||||
<Tag
|
||||
v-if="!isEditing"
|
||||
:value="requirement.priority"
|
||||
:severity="prioritySeverity(requirement.priority)"
|
||||
rounded
|
||||
/>
|
||||
<select v-else v-model="form.priority" class="edit-select">
|
||||
<option value="Low">Low</option>
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="High">High</option>
|
||||
<option value="Critical">Critical</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<p class="detail-description">{{ requirement.description }}</p>
|
||||
<p v-if="!isEditing" class="detail-description">{{ requirement.description }}</p>
|
||||
<div v-else>
|
||||
<Textarea
|
||||
v-model="form.description"
|
||||
class="edit-textarea"
|
||||
rows="3"
|
||||
placeholder="Requirement description"
|
||||
/>
|
||||
<small v-if="errors.description" class="error-text">{{ errors.description }}</small>
|
||||
</div>
|
||||
|
||||
<article v-if="requirement.blockers.length" class="info-panel info-panel--warning">
|
||||
<article v-if="isEditing || requirement.blockers.length" class="info-panel info-panel--warning">
|
||||
<h3>Identified blockers</h3>
|
||||
<ul class="blockers-list">
|
||||
<ul v-if="!isEditing" class="blockers-list">
|
||||
<li v-for="blocker in requirement.blockers" :key="blocker">
|
||||
<i class="pi pi-exclamation-triangle" />
|
||||
<span>{{ blocker }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Textarea
|
||||
v-else
|
||||
v-model="form.blockers"
|
||||
class="edit-textarea"
|
||||
rows="3"
|
||||
placeholder="One blocker per line"
|
||||
/>
|
||||
</article>
|
||||
|
||||
<Divider />
|
||||
@@ -95,24 +308,50 @@ const prioritySeverity = (priority: Requirement['priority']) => {
|
||||
<div class="detail-grid">
|
||||
<article class="info-panel">
|
||||
<h3>Author</h3>
|
||||
<p>{{ requirement.owner }}</p>
|
||||
<p v-if="!isEditing">{{ requirement.owner }}</p>
|
||||
<div v-else>
|
||||
<InputText v-model="form.owner" class="edit-input" placeholder="Author" />
|
||||
<small v-if="errors.owner" class="error-text">{{ errors.owner }}</small>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="info-panel">
|
||||
<h3>Stakeholders</h3>
|
||||
<div class="chip-row">
|
||||
<div v-if="!isEditing" class="chip-row">
|
||||
<Chip v-for="stakeholder in requirement.stakeholders" :key="stakeholder" :label="stakeholder" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<InputText
|
||||
v-model="form.stakeholders"
|
||||
class="edit-input"
|
||||
placeholder="Comma separated stakeholders"
|
||||
/>
|
||||
<small v-if="errors.stakeholders" class="error-text">{{ errors.stakeholders }}</small>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="info-panel">
|
||||
<h3>Flexibility</h3>
|
||||
<p>{{ requirement.flexibility }}</p>
|
||||
<p v-if="!isEditing">{{ requirement.flexibility }}</p>
|
||||
<Textarea
|
||||
v-else
|
||||
v-model="form.flexibility"
|
||||
class="edit-textarea"
|
||||
rows="2"
|
||||
placeholder="Flexibility constraints"
|
||||
/>
|
||||
</article>
|
||||
|
||||
<article class="info-panel">
|
||||
<h3>Tolerances</h3>
|
||||
<p>{{ requirement.tolerances }}</p>
|
||||
<p v-if="!isEditing">{{ requirement.tolerances }}</p>
|
||||
<Textarea
|
||||
v-else
|
||||
v-model="form.tolerances"
|
||||
class="edit-textarea"
|
||||
rows="2"
|
||||
placeholder="Tolerance details"
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -121,24 +360,44 @@ const prioritySeverity = (priority: Requirement['priority']) => {
|
||||
<div class="detail-columns">
|
||||
<article class="info-panel">
|
||||
<h3>Acceptance criteria</h3>
|
||||
<ul class="checklist">
|
||||
<ul v-if="!isEditing" class="checklist">
|
||||
<li v-for="item in requirement.acceptanceCriteria" :key="item">
|
||||
<i class="pi pi-check-circle" />
|
||||
<span>{{ item }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Textarea
|
||||
v-else
|
||||
v-model="form.acceptanceCriteria"
|
||||
class="edit-textarea"
|
||||
rows="6"
|
||||
placeholder="One acceptance criterion per line"
|
||||
/>
|
||||
</article>
|
||||
|
||||
<article class="info-panel">
|
||||
<h3>Impacted modules</h3>
|
||||
<div class="chip-row">
|
||||
<div v-if="!isEditing" class="chip-row">
|
||||
<Chip v-for="module in requirement.impactedModules" :key="module" :label="module" />
|
||||
</div>
|
||||
<InputText
|
||||
v-else
|
||||
v-model="form.impactedModules"
|
||||
class="edit-input"
|
||||
placeholder="Comma separated modules"
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<h3>Remarks</h3>
|
||||
<p>{{ requirement.notes }}</p>
|
||||
<p v-if="!isEditing">{{ requirement.notes }}</p>
|
||||
<Textarea
|
||||
v-else
|
||||
v-model="form.notes"
|
||||
class="edit-textarea"
|
||||
rows="4"
|
||||
placeholder="Remarks"
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
</TabPanel>
|
||||
@@ -150,6 +409,21 @@ const prioritySeverity = (priority: Requirement['priority']) => {
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabView>
|
||||
|
||||
<footer v-if="isEditing" class="edit-footer">
|
||||
<Button
|
||||
label="Cancel"
|
||||
icon="pi pi-times"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="cancelEdit"
|
||||
/>
|
||||
<Button
|
||||
label="Save"
|
||||
icon="pi pi-check"
|
||||
@click="saveEdit"
|
||||
/>
|
||||
</footer>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
@@ -180,6 +454,24 @@ const prioritySeverity = (priority: Requirement['priority']) => {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.detail-card__header--editing {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.detail-card__header--editing .detail-card__main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detail-card__header--editing .detail-actions {
|
||||
margin-top: 0.5rem;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.detail-card__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
@@ -200,6 +492,13 @@ const prioritySeverity = (priority: Requirement['priority']) => {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.detail-card__uuid {
|
||||
margin: 0.35rem 0 0;
|
||||
color: #7c8aa5;
|
||||
font-size: 0.75rem;
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -207,6 +506,14 @@ const prioritySeverity = (priority: Requirement['priority']) => {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.edit-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid rgba(96, 117, 156, 0.12);
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
@@ -218,6 +525,38 @@ const prioritySeverity = (priority: Requirement['priority']) => {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.detail-tabs .p-tabview-nav-container),
|
||||
:deep(.detail-tabs .p-tabview-nav-content),
|
||||
:deep(.detail-tabs .p-tabview-nav) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:deep(.detail-tabs .p-tabview-nav) {
|
||||
position: relative;
|
||||
border-bottom: 1px solid rgba(96, 117, 156, 0.2) !important;
|
||||
}
|
||||
|
||||
:deep(.detail-tabs .p-tabview-nav::before),
|
||||
:deep(.detail-tabs .p-tabview-nav::after) {
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
:deep(.detail-tabs .p-tabview-ink-bar) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:deep(.detail-tabs [role='tab']) {
|
||||
border-bottom: 2px solid transparent !important;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
:deep(.detail-tabs [role='tab'][aria-selected='true']) {
|
||||
border-bottom-color: #2563eb !important;
|
||||
color: #1d4ed8 !important;
|
||||
}
|
||||
|
||||
:deep(.detail-tabs .p-tabview-panels) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -326,6 +665,27 @@ const prioritySeverity = (priority: Requirement['priority']) => {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.edit-input,
|
||||
.edit-textarea,
|
||||
.edit-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.edit-select {
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.45rem 0.6rem;
|
||||
background: #fff;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
display: block;
|
||||
margin-top: 0.35rem;
|
||||
color: #b91c1c;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.detail-grid,
|
||||
.detail-columns {
|
||||
@@ -349,6 +709,3 @@ const prioritySeverity = (priority: Requirement['priority']) => {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3,23 +3,7 @@ import { computed, ref } from 'vue'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Tree from 'primevue/tree'
|
||||
import Tag from 'primevue/tag'
|
||||
|
||||
export interface Requirement {
|
||||
id: number
|
||||
reference: string
|
||||
title: string
|
||||
status: 'Draft' | 'In Review' | 'Approved' | 'Blocked' | 'Delivered'
|
||||
priority: 'Low' | 'Medium' | 'High' | 'Critical'
|
||||
owner: string
|
||||
progress: number
|
||||
dueDate: string
|
||||
description: string
|
||||
rationale: string
|
||||
acceptanceCriteria: string[]
|
||||
impactedModules: string[]
|
||||
blockers: string[]
|
||||
notes: string
|
||||
}
|
||||
import type { Requirement, RequirementPriority, RequirementStatus } from '../stores/requirements'
|
||||
|
||||
export interface TreeNode {
|
||||
key: string
|
||||
@@ -91,7 +75,7 @@ const filteredTreeData = computed(() => {
|
||||
|
||||
const statusSeverity = (status: Requirement['status']) => {
|
||||
const map: Record<
|
||||
Requirement['status'],
|
||||
RequirementStatus,
|
||||
'secondary' | 'info' | 'success' | 'danger' | 'contrast'
|
||||
> = {
|
||||
Draft: 'secondary',
|
||||
@@ -104,7 +88,7 @@ const statusSeverity = (status: Requirement['status']) => {
|
||||
}
|
||||
|
||||
const prioritySeverity = (priority: Requirement['priority']) => {
|
||||
const map: Record<Requirement['priority'], 'success' | 'info' | 'warn' | 'danger'> = {
|
||||
const map: Record<RequirementPriority, 'success' | 'info' | 'warn' | 'danger'> = {
|
||||
Low: 'success',
|
||||
Medium: 'info',
|
||||
High: 'warn',
|
||||
|
||||
@@ -7,6 +7,7 @@ export type RequirementPriority = 'Low' | 'Medium' | 'High' | 'Critical'
|
||||
export interface Requirement {
|
||||
id: number
|
||||
reference: string
|
||||
uuid: string
|
||||
title: string
|
||||
status: RequirementStatus
|
||||
priority: RequirementPriority
|
||||
@@ -29,6 +30,7 @@ export const useRequirementsStore = defineStore('requirements', () => {
|
||||
{
|
||||
id: 101,
|
||||
reference: 'REQ-101',
|
||||
uuid: '9f6c4f7a-9d2f-4dc0-8fa2-7ac2d79b0c11',
|
||||
title: 'Full requirement traceability',
|
||||
status: 'Approved',
|
||||
priority: 'Critical',
|
||||
@@ -54,6 +56,7 @@ export const useRequirementsStore = defineStore('requirements', () => {
|
||||
{
|
||||
id: 102,
|
||||
reference: 'REQ-102',
|
||||
uuid: '8aa1b6f5-2378-4c47-b490-2b92c7ee8b5f',
|
||||
title: 'Business status management',
|
||||
status: 'In Review',
|
||||
priority: 'High',
|
||||
@@ -79,6 +82,7 @@ export const useRequirementsStore = defineStore('requirements', () => {
|
||||
{
|
||||
id: 103,
|
||||
reference: 'REQ-103',
|
||||
uuid: '6f99d740-67bf-4a1f-88f2-4fd947f9cb8a',
|
||||
title: 'Fast search and filtering',
|
||||
status: 'Draft',
|
||||
priority: 'Medium',
|
||||
@@ -104,6 +108,7 @@ export const useRequirementsStore = defineStore('requirements', () => {
|
||||
{
|
||||
id: 104,
|
||||
reference: 'REQ-104',
|
||||
uuid: 'f47e5f4a-c54d-46c9-8fef-f5f56e5e4d7a',
|
||||
title: 'Delivery scope by release',
|
||||
status: 'Blocked',
|
||||
priority: 'Critical',
|
||||
@@ -152,11 +157,30 @@ export const useRequirementsStore = defineStore('requirements', () => {
|
||||
return { total, approvedCount, blockedCount, criticalCount }
|
||||
})
|
||||
|
||||
const updateRequirement = (updatedRequirement: Requirement) => {
|
||||
const index = requirements.value.findIndex(
|
||||
(requirement) => requirement.id === updatedRequirement.id
|
||||
)
|
||||
|
||||
if (index === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
requirements.value[index] = {
|
||||
...updatedRequirement,
|
||||
acceptanceCriteria: [...updatedRequirement.acceptanceCriteria],
|
||||
impactedModules: [...updatedRequirement.impactedModules],
|
||||
blockers: [...updatedRequirement.blockers],
|
||||
stakeholders: [...updatedRequirement.stakeholders],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
requirements,
|
||||
selectedId,
|
||||
searchQuery,
|
||||
selectedRequirement,
|
||||
stats,
|
||||
updateRequirement,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount } from 'vue'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Splitter from 'primevue/splitter'
|
||||
import SplitterPanel from 'primevue/splitterpanel'
|
||||
@@ -15,6 +15,8 @@ const requirementsStore = useRequirementsStore()
|
||||
const { requirements, selectedId, searchQuery, selectedRequirement, stats } =
|
||||
storeToRefs(requirementsStore)
|
||||
|
||||
const hasUnsavedChanges = ref(false)
|
||||
|
||||
const splitterDraggingClass = 'is-splitter-dragging'
|
||||
|
||||
const onSplitterResizeStart = () => {
|
||||
@@ -81,6 +83,34 @@ const buildTreeData = (): TreeNode[] => {
|
||||
}
|
||||
|
||||
const treeData = computed(() => buildTreeData())
|
||||
|
||||
const onSaveRequirement = (updatedRequirement: Requirement) => {
|
||||
requirementsStore.updateRequirement(updatedRequirement)
|
||||
hasUnsavedChanges.value = false
|
||||
}
|
||||
|
||||
const onDetailDirtyChange = (value: boolean) => {
|
||||
hasUnsavedChanges.value = value
|
||||
}
|
||||
|
||||
const onSelectRequirement = (nextId: number) => {
|
||||
if (nextId === selectedId.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (hasUnsavedChanges.value) {
|
||||
const confirmed = window.confirm(
|
||||
'You have unsaved changes. Switching requirement will discard them. Continue?'
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hasUnsavedChanges.value = false
|
||||
selectedId.value = nextId
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -98,12 +128,16 @@ const treeData = computed(() => buildTreeData())
|
||||
:selectedId="selectedId"
|
||||
:stats="stats"
|
||||
@update:searchQuery="searchQuery = $event"
|
||||
@update:selectedId="selectedId = $event"
|
||||
@update:selectedId="onSelectRequirement"
|
||||
/>
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel :size="72" :min-size="35">
|
||||
<RequirementDetail :requirement="selectedRequirement" />
|
||||
<RequirementDetail
|
||||
:requirement="selectedRequirement"
|
||||
@save="onSaveRequirement"
|
||||
@dirty-change="onDetailDirtyChange"
|
||||
/>
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user