appends requirement edition mode

This commit is contained in:
Sylvain Schneider
2026-05-12 00:44:18 +02:00
parent 8aa3128edf
commit da85df11f7
4 changed files with 466 additions and 67 deletions

View File

@@ -1,39 +1,181 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import Button from 'primevue/button' import Button from 'primevue/button'
import Chip from 'primevue/chip' import Chip from 'primevue/chip'
import Divider from 'primevue/divider' import Divider from 'primevue/divider'
import InputText from 'primevue/inputtext'
import TabView from 'primevue/tabview' import TabView from 'primevue/tabview'
import TabPanel from 'primevue/tabpanel' import TabPanel from 'primevue/tabpanel'
import Tag from 'primevue/tag' import Tag from 'primevue/tag'
import Textarea from 'primevue/textarea'
export interface Requirement { import type { Requirement, RequirementPriority, RequirementStatus } from '../stores/requirements'
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
}
interface Props { interface Props {
requirement: Requirement 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 props = defineProps<Props>()
const map: Record<Requirement['status'], 'secondary' | 'info' | 'success' | 'danger' | 'contrast'> = {
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', Draft: 'secondary',
'In Review': 'info', 'In Review': 'info',
Approved: 'success', Approved: 'success',
@@ -43,8 +185,8 @@ const statusSeverity = (status: Requirement['status']) => {
return map[status] return map[status]
} }
const prioritySeverity = (priority: Requirement['priority']) => { const prioritySeverity = (priority: RequirementPriority) => {
const map: Record<Requirement['priority'], 'success' | 'info' | 'warn' | 'danger'> = { const map: Record<RequirementPriority, 'success' | 'info' | 'warn' | 'danger'> = {
Low: 'success', Low: 'success',
Medium: 'info', Medium: 'info',
High: 'warn', High: 'warn',
@@ -52,42 +194,113 @@ const prioritySeverity = (priority: Requirement['priority']) => {
} }
return map[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> </script>
<template> <template>
<section class="detail-zone"> <section class="detail-zone">
<section class="detail-surface"> <section class="detail-surface">
<header class="detail-card__header"> <header
<div> class="detail-card__header"
:class="{ 'detail-card__header--editing': isEditing }"
>
<div class="detail-card__main">
<p class="eyebrow">Selected requirement</p> <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"> <p class="detail-card__subtitle">
{{ requirement.reference }} - Owner: {{ requirement.owner }} {{ requirement.reference }} - Owner: {{ isEditing ? form.owner : requirement.owner }}
</p> </p>
<p class="detail-card__uuid">UUID: {{ requirement.uuid }}</p>
</div> </div>
<div class="detail-actions"> <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> </div>
</header> </header>
<TabView class="detail-tabs"> <TabView class="detail-tabs">
<TabPanel value="details" header="Details"> <TabPanel value="details" header="Details">
<div class="detail-badges"> <div class="detail-badges">
<Tag :value="requirement.status" :severity="statusSeverity(requirement.status)" /> <Tag
<Tag :value="requirement.priority" :severity="prioritySeverity(requirement.priority)" rounded /> 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> </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> <h3>Identified blockers</h3>
<ul class="blockers-list"> <ul v-if="!isEditing" class="blockers-list">
<li v-for="blocker in requirement.blockers" :key="blocker"> <li v-for="blocker in requirement.blockers" :key="blocker">
<i class="pi pi-exclamation-triangle" /> <i class="pi pi-exclamation-triangle" />
<span>{{ blocker }}</span> <span>{{ blocker }}</span>
</li> </li>
</ul> </ul>
<Textarea
v-else
v-model="form.blockers"
class="edit-textarea"
rows="3"
placeholder="One blocker per line"
/>
</article> </article>
<Divider /> <Divider />
@@ -95,24 +308,50 @@ const prioritySeverity = (priority: Requirement['priority']) => {
<div class="detail-grid"> <div class="detail-grid">
<article class="info-panel"> <article class="info-panel">
<h3>Author</h3> <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>
<article class="info-panel"> <article class="info-panel">
<h3>Stakeholders</h3> <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" /> <Chip v-for="stakeholder in requirement.stakeholders" :key="stakeholder" :label="stakeholder" />
</div> </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>
<article class="info-panel"> <article class="info-panel">
<h3>Flexibility</h3> <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>
<article class="info-panel"> <article class="info-panel">
<h3>Tolerances</h3> <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> </article>
</div> </div>
@@ -121,24 +360,44 @@ const prioritySeverity = (priority: Requirement['priority']) => {
<div class="detail-columns"> <div class="detail-columns">
<article class="info-panel"> <article class="info-panel">
<h3>Acceptance criteria</h3> <h3>Acceptance criteria</h3>
<ul class="checklist"> <ul v-if="!isEditing" class="checklist">
<li v-for="item in requirement.acceptanceCriteria" :key="item"> <li v-for="item in requirement.acceptanceCriteria" :key="item">
<i class="pi pi-check-circle" /> <i class="pi pi-check-circle" />
<span>{{ item }}</span> <span>{{ item }}</span>
</li> </li>
</ul> </ul>
<Textarea
v-else
v-model="form.acceptanceCriteria"
class="edit-textarea"
rows="6"
placeholder="One acceptance criterion per line"
/>
</article> </article>
<article class="info-panel"> <article class="info-panel">
<h3>Impacted modules</h3> <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" /> <Chip v-for="module in requirement.impactedModules" :key="module" :label="module" />
</div> </div>
<InputText
v-else
v-model="form.impactedModules"
class="edit-input"
placeholder="Comma separated modules"
/>
<Divider /> <Divider />
<h3>Remarks</h3> <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> </article>
</div> </div>
</TabPanel> </TabPanel>
@@ -150,6 +409,21 @@ const prioritySeverity = (priority: Requirement['priority']) => {
</div> </div>
</TabPanel> </TabPanel>
</TabView> </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>
</section> </section>
</template> </template>
@@ -180,6 +454,24 @@ const prioritySeverity = (priority: Requirement['priority']) => {
gap: 1rem; 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 { .eyebrow {
margin: 0; margin: 0;
text-transform: uppercase; text-transform: uppercase;
@@ -200,6 +492,13 @@ const prioritySeverity = (priority: Requirement['priority']) => {
font-size: 0.82rem; 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 { .detail-actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -207,6 +506,14 @@ const prioritySeverity = (priority: Requirement['priority']) => {
gap: 0.5rem; 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 { .detail-tabs {
min-height: 0; min-height: 0;
flex: 1; flex: 1;
@@ -218,6 +525,38 @@ const prioritySeverity = (priority: Requirement['priority']) => {
min-height: 0; 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) { :deep(.detail-tabs .p-tabview-panels) {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -326,6 +665,27 @@ const prioritySeverity = (priority: Requirement['priority']) => {
font-size: 2.5rem; 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) { @media (max-width: 1120px) {
.detail-grid, .detail-grid,
.detail-columns { .detail-columns {
@@ -349,6 +709,3 @@ const prioritySeverity = (priority: Requirement['priority']) => {
} }
} }
</style> </style>

View File

@@ -3,23 +3,7 @@ import { computed, ref } from 'vue'
import InputText from 'primevue/inputtext' import InputText from 'primevue/inputtext'
import Tree from 'primevue/tree' import Tree from 'primevue/tree'
import Tag from 'primevue/tag' import Tag from 'primevue/tag'
import type { Requirement, RequirementPriority, RequirementStatus } from '../stores/requirements'
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
}
export interface TreeNode { export interface TreeNode {
key: string key: string
@@ -91,7 +75,7 @@ const filteredTreeData = computed(() => {
const statusSeverity = (status: Requirement['status']) => { const statusSeverity = (status: Requirement['status']) => {
const map: Record< const map: Record<
Requirement['status'], RequirementStatus,
'secondary' | 'info' | 'success' | 'danger' | 'contrast' 'secondary' | 'info' | 'success' | 'danger' | 'contrast'
> = { > = {
Draft: 'secondary', Draft: 'secondary',
@@ -104,7 +88,7 @@ const statusSeverity = (status: Requirement['status']) => {
} }
const prioritySeverity = (priority: Requirement['priority']) => { const prioritySeverity = (priority: Requirement['priority']) => {
const map: Record<Requirement['priority'], 'success' | 'info' | 'warn' | 'danger'> = { const map: Record<RequirementPriority, 'success' | 'info' | 'warn' | 'danger'> = {
Low: 'success', Low: 'success',
Medium: 'info', Medium: 'info',
High: 'warn', High: 'warn',

View File

@@ -7,6 +7,7 @@ export type RequirementPriority = 'Low' | 'Medium' | 'High' | 'Critical'
export interface Requirement { export interface Requirement {
id: number id: number
reference: string reference: string
uuid: string
title: string title: string
status: RequirementStatus status: RequirementStatus
priority: RequirementPriority priority: RequirementPriority
@@ -29,6 +30,7 @@ export const useRequirementsStore = defineStore('requirements', () => {
{ {
id: 101, id: 101,
reference: 'REQ-101', reference: 'REQ-101',
uuid: '9f6c4f7a-9d2f-4dc0-8fa2-7ac2d79b0c11',
title: 'Full requirement traceability', title: 'Full requirement traceability',
status: 'Approved', status: 'Approved',
priority: 'Critical', priority: 'Critical',
@@ -54,6 +56,7 @@ export const useRequirementsStore = defineStore('requirements', () => {
{ {
id: 102, id: 102,
reference: 'REQ-102', reference: 'REQ-102',
uuid: '8aa1b6f5-2378-4c47-b490-2b92c7ee8b5f',
title: 'Business status management', title: 'Business status management',
status: 'In Review', status: 'In Review',
priority: 'High', priority: 'High',
@@ -79,6 +82,7 @@ export const useRequirementsStore = defineStore('requirements', () => {
{ {
id: 103, id: 103,
reference: 'REQ-103', reference: 'REQ-103',
uuid: '6f99d740-67bf-4a1f-88f2-4fd947f9cb8a',
title: 'Fast search and filtering', title: 'Fast search and filtering',
status: 'Draft', status: 'Draft',
priority: 'Medium', priority: 'Medium',
@@ -104,6 +108,7 @@ export const useRequirementsStore = defineStore('requirements', () => {
{ {
id: 104, id: 104,
reference: 'REQ-104', reference: 'REQ-104',
uuid: 'f47e5f4a-c54d-46c9-8fef-f5f56e5e4d7a',
title: 'Delivery scope by release', title: 'Delivery scope by release',
status: 'Blocked', status: 'Blocked',
priority: 'Critical', priority: 'Critical',
@@ -152,11 +157,30 @@ export const useRequirementsStore = defineStore('requirements', () => {
return { total, approvedCount, blockedCount, criticalCount } 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 { return {
requirements, requirements,
selectedId, selectedId,
searchQuery, searchQuery,
selectedRequirement, selectedRequirement,
stats, stats,
updateRequirement,
} }
}) })

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount } from 'vue' import { computed, onBeforeUnmount, ref } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import Splitter from 'primevue/splitter' import Splitter from 'primevue/splitter'
import SplitterPanel from 'primevue/splitterpanel' import SplitterPanel from 'primevue/splitterpanel'
@@ -15,6 +15,8 @@ const requirementsStore = useRequirementsStore()
const { requirements, selectedId, searchQuery, selectedRequirement, stats } = const { requirements, selectedId, searchQuery, selectedRequirement, stats } =
storeToRefs(requirementsStore) storeToRefs(requirementsStore)
const hasUnsavedChanges = ref(false)
const splitterDraggingClass = 'is-splitter-dragging' const splitterDraggingClass = 'is-splitter-dragging'
const onSplitterResizeStart = () => { const onSplitterResizeStart = () => {
@@ -81,6 +83,34 @@ const buildTreeData = (): TreeNode[] => {
} }
const treeData = computed(() => buildTreeData()) 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> </script>
<template> <template>
@@ -98,12 +128,16 @@ const treeData = computed(() => buildTreeData())
:selectedId="selectedId" :selectedId="selectedId"
:stats="stats" :stats="stats"
@update:searchQuery="searchQuery = $event" @update:searchQuery="searchQuery = $event"
@update:selectedId="selectedId = $event" @update:selectedId="onSelectRequirement"
/> />
</SplitterPanel> </SplitterPanel>
<SplitterPanel :size="72" :min-size="35"> <SplitterPanel :size="72" :min-size="35">
<RequirementDetail :requirement="selectedRequirement" /> <RequirementDetail
:requirement="selectedRequirement"
@save="onSaveRequirement"
@dirty-change="onDetailDirtyChange"
/>
</SplitterPanel> </SplitterPanel>
</Splitter> </Splitter>
</section> </section>