Files
graph-requirements-planner/frontend/src/app/features/detail/detail-panel.component.html
2026-03-18 06:40:53 -05:00

412 lines
17 KiB
HTML

<div class="detail-panel">
<div class="panel-header">
<div class="header-top">
<span class="node-id">{{ node.id }}</span>
<span class="node-type" [style.color]="node.node_type === 'pillar' ? '#58a6ff' : '#8b949e'">
{{ node.node_type }}
</span>
<button class="close-btn" (click)="close.emit()">x</button>
</div>
@if (!editing) {
<h2 class="node-title">{{ node.title }}</h2>
} @else {
<input class="edit-input title-input" [(ngModel)]="editData.title" placeholder="Title" />
}
<div class="badges-row">
<span class="badge priority-badge" [style.background]="getPriorityColor(node.priority)">
{{ node.priority }}
</span>
<span class="badge status-badge" [style.background]="getStatusColor(node.status)">
{{ node.status }}
</span>
@if (node.phase != null) {
<span class="badge phase-badge">Phase {{ node.phase }}</span>
}
@if (node.gitea_issue_number && node.gitea_repo) {
<a class="badge issue-badge" [href]="getGiteaIssueUrl()" target="_blank" title="Open in Gitea">
#{{ node.gitea_issue_number }}
</a>
}
</div>
</div>
<div class="panel-body">
<!-- Quality Gates -->
<div class="section quality-section">
<h3 class="section-title">Quality Gates</h3>
<div class="quality-badges">
<span class="quality-badge auto" [class.pass]="node.traceable" [class.fail]="!node.traceable" title="Traceable (auto-calculated)">T</span>
<span class="quality-badge toggle" [class.pass]="effectiveMeasurable" [class.fail]="!effectiveMeasurable" [title]="measurableForced ? 'Measurable (forced off missing test spec)' : 'Measurable (click to toggle)'" (click)="toggleGate('measurable')">M</span>
<span class="quality-badge toggle" [class.pass]="node.consistent" [class.fail]="!node.consistent" title="Consistent (click to toggle)" (click)="toggleGate('consistent')">C</span>
<span class="quality-badge toggle" [class.pass]="node.single_purpose" [class.fail]="!node.single_purpose" title="Single Purpose (click to toggle)" (click)="toggleGate('single_purpose')">S</span>
</div>
@if (validationReport) {
@if (validationReport.errors.length > 0) {
<div class="validation-list errors">
@for (err of validationReport.errors; track err) {
<div class="validation-item error-item">{{ err }}</div>
}
</div>
}
@if (validationReport.warnings.length > 0) {
<div class="validation-list warnings">
@for (warn of validationReport.warnings; track warn) {
<div class="validation-item warning-item">{{ warn }}</div>
}
</div>
}
@if (validationReport.valid && node.traceable && node.measurable && node.consistent && node.single_purpose) {
<div class="validation-pass">All quality gates passed</div>
} @else if (validationReport.valid && validationReport.warnings.length === 0) {
<div class="validation-warn">No errors, but not all gates satisfied</div>
}
}
</div>
<!-- Description -->
<div class="section">
<h3 class="section-title">Description</h3>
@if (!editing) {
<p class="description">{{ node.description || 'No description' }}</p>
} @else {
<textarea class="edit-textarea" [(ngModel)]="editData.description" placeholder="Description..." rows="4"></textarea>
}
</div>
<!-- Editing fields -->
@if (editing) {
<div class="section">
<h3 class="section-title">Properties</h3>
<div class="edit-row">
<label>Priority</label>
<select class="edit-select" [(ngModel)]="editData.priority">
@for (p of priorities; track p) {
<option [value]="p">{{ p }}</option>
}
</select>
</div>
<div class="edit-row">
<label>Status</label>
<select class="edit-select" [(ngModel)]="editData.status">
@for (s of statuses; track s) {
<option [value]="s">{{ s }}</option>
}
</select>
</div>
<div class="edit-row">
<label>Phase</label>
<input class="edit-input" type="number" [(ngModel)]="editData.phase" placeholder="Phase" />
</div>
<div class="edit-row">
<label>Gitea Repo</label>
<input class="edit-input" [(ngModel)]="editData.gitea_repo" placeholder="owner/repo" />
</div>
<div class="edit-row">
<label>Issue #</label>
<input class="edit-input" type="number" [(ngModel)]="editData.gitea_issue_number" placeholder="Issue number" />
</div>
</div>
}
<!-- Acceptance Criteria -->
<div class="section">
<h3 class="section-title">Acceptance Criteria</h3>
@if (!editing) {
<pre class="code-block">{{ node.acceptance_criteria || 'No acceptance criteria defined' }}</pre>
} @else {
<textarea class="edit-textarea" [(ngModel)]="editData.acceptance_criteria" placeholder="Define what must be true for this requirement to be satisfied..." rows="4"></textarea>
}
</div>
<!-- Test Specification -->
<div class="section">
<h3 class="section-title">Test Specification</h3>
@if (!editing) {
<pre class="code-block">{{ node.test_spec || 'No test specification defined' }}</pre>
} @else {
<textarea class="edit-textarea mono" [(ngModel)]="editData.test_spec" placeholder="GIVEN...\nWHEN...\nTHEN..." rows="6"></textarea>
}
</div>
<!-- Test Code -->
<div class="section">
<h3 class="section-title">Test Code</h3>
@if (!editing) {
<pre class="code-block mono">{{ node.test_code || 'No test code' }}</pre>
} @else {
<textarea class="edit-textarea mono" [(ngModel)]="editData.test_code" placeholder="Test implementation..." rows="8"></textarea>
}
</div>
<!-- Implementation Code -->
<div class="section">
<h3 class="section-title">Implementation Code</h3>
@if (!editing) {
<pre class="code-block mono">{{ node.impl_code || 'No implementation code' }}</pre>
} @else {
<textarea class="edit-textarea mono" [(ngModel)]="editData.impl_code" placeholder="Implementation code..." rows="8"></textarea>
}
</div>
<!-- Parent Requirement -->
@if (parentNode) {
<div class="section">
<h3 class="section-title">Parent</h3>
<div class="child-card clickable" (click)="goToNode(parentNode.id)">
<div class="child-header">
<span class="child-id">{{ parentNode.id }}</span>
<span class="child-priority" [style.background]="getPriorityColor(parentNode.priority)">
{{ parentNode.priority }}
</span>
<div class="child-quality">
<span [class.pass]="parentNode.traceable" [class.fail]="!parentNode.traceable">T</span>
<span [class.pass]="parentNode.measurable" [class.fail]="!parentNode.measurable">M</span>
<span [class.pass]="parentNode.consistent" [class.fail]="!parentNode.consistent">C</span>
<span [class.pass]="parentNode.single_purpose" [class.fail]="!parentNode.single_purpose">S</span>
</div>
</div>
<div class="child-title">{{ parentNode.title }}</div>
<span class="child-status" [style.color]="getStatusColor(parentNode.status)">
{{ parentNode.status }}
</span>
</div>
</div>
}
<!-- Child Requirements -->
<div class="section">
<h3 class="section-title">
Child Requirements
<span class="child-count">({{ children.length }})</span>
</h3>
<div class="children-list">
@for (child of children; track child.id) {
<div class="child-card clickable" (click)="goToNode(child.id)">
<div class="child-header">
<span class="child-id">{{ child.id }}</span>
<span class="child-priority" [style.background]="getPriorityColor(child.priority)">
{{ child.priority }}
</span>
<div class="child-quality">
<span [class.pass]="child.traceable" [class.fail]="!child.traceable">T</span>
<span [class.pass]="child.measurable" [class.fail]="!child.measurable">M</span>
<span [class.pass]="child.consistent" [class.fail]="!child.consistent">C</span>
<span [class.pass]="child.single_purpose" [class.fail]="!child.single_purpose">S</span>
</div>
</div>
<div class="child-title">{{ child.title }}</div>
@if (child.description) {
<div class="child-desc">{{ child.description }}</div>
}
<span class="child-status" [style.color]="getStatusColor(child.status)">
{{ child.status }}
</span>
</div>
}
</div>
</div>
<!-- Add Child Form -->
@if (showAddChild) {
<div class="section add-child-form">
<h3 class="section-title">New Child Requirement</h3>
<input class="edit-input" [(ngModel)]="$any(newChild).id" placeholder="ID (e.g. {{ node.id }}.1)" />
<input class="edit-input" [(ngModel)]="newChild.title" placeholder="Title" />
<textarea class="edit-textarea" [(ngModel)]="newChild.description" placeholder="Description..." rows="3"></textarea>
<div class="edit-row">
<label>Priority</label>
<select class="edit-select" [(ngModel)]="newChild.priority">
@for (p of priorities; track p) {
<option [value]="p">{{ p }}</option>
}
</select>
</div>
@if (createError) {
<div class="create-error">{{ createError }}</div>
}
<div class="form-actions">
<button class="action-btn primary" (click)="createChild()">Create</button>
<button class="action-btn" (click)="toggleAddChild()">Cancel</button>
</div>
</div>
}
<!-- Also Satisfies (outgoing links) -->
<div class="section">
<h3 class="section-title">
Also Satisfies
<span class="child-count">({{ outgoingLinks.length }})</span>
</h3>
<div class="links-list">
@for (link of outgoingLinks; track link.target_id) {
<div class="link-card">
<span class="link-id" (click)="goToNode(link.target_id)">{{ link.target_id }}</span>
<button class="link-remove" (click)="deleteLink(link.source_id, link.target_id)" title="Remove link">x</button>
</div>
}
@if (outgoingLinks.length === 0 && !showAddLink) {
<div class="no-children">No satisfaction links</div>
}
</div>
@if (showAddLink) {
<div class="add-link-form">
<div class="typeahead-container">
<input
class="edit-input"
[(ngModel)]="newLinkTargetId"
placeholder="Search by ID or title..."
(input)="onLinkSearchInput()"
(focus)="onLinkSearchFocus()"
(keydown)="onLinkSearchKeydown($event)"
(keydown.enter)="highlightedIndex < 0 && createLink()"
(blur)="showLinkDropdown = false"
autocomplete="off"
/>
@if (showLinkDropdown) {
<div class="typeahead-dropdown">
@for (req of filteredRequirements; track req.id; let i = $index) {
<div
class="typeahead-option"
[class.highlighted]="i === highlightedIndex"
(mousedown)="selectLinkTarget(req)"
>
<span class="typeahead-id">{{ req.id }}</span>
<span class="typeahead-title">{{ req.title }}</span>
</div>
}
</div>
}
</div>
@if (linkError) {
<div class="create-error">{{ linkError }}</div>
}
<div class="form-actions">
<button class="action-btn primary small" (click)="createLink()">Link</button>
<button class="action-btn small" (click)="toggleAddLink()">Cancel</button>
</div>
</div>
} @else {
<button class="action-btn small" (click)="toggleAddLink()">+ Add Link</button>
}
</div>
<!-- Satisfied By (incoming links) -->
@if (incomingLinks.length > 0) {
<div class="section">
<h3 class="section-title">
Satisfied By
<span class="child-count">({{ incomingLinks.length }})</span>
</h3>
<div class="links-list">
@for (link of incomingLinks; track link.source_id) {
<div class="link-card clickable" (click)="goToNode(link.source_id)">
<span class="link-id">{{ link.source_id }}</span>
</div>
}
</div>
</div>
}
<!-- Metadata -->
<div class="section meta-section">
<div class="meta-row">
<span class="meta-label">Created</span>
<span class="meta-value">{{ node.created_at | date:'short' }}</span>
</div>
<div class="meta-row">
<span class="meta-label">Updated</span>
<span class="meta-value">{{ node.updated_at | date:'short' }}</span>
</div>
</div>
<!-- History -->
<div class="section">
<h3 class="section-title clickable-title" (click)="toggleHistory()">
<span>History</span>
@if (historyList.length > 0) {
<span class="child-count">({{ historyList.length }})</span>
}
<span class="toggle-arrow">{{ showHistory ? '\u25BC' : '\u25B6' }}</span>
</h3>
@if (showHistory) {
<div class="history-list">
@for (entry of historyList; track entry.id) {
<div class="history-item" [class.active]="selectedVersion?.version === entry.version" (click)="viewVersion(entry.version)">
<div class="history-header">
<span class="history-version">v{{ entry.version }}</span>
<span class="history-time">{{ entry.created_at | date:'short' }}</span>
</div>
<div class="history-summary">{{ entry.change_summary || 'No summary' }}</div>
</div>
} @empty {
<div class="no-children">No history yet</div>
}
</div>
@if (selectedVersion) {
<div class="history-detail">
<h4 class="history-detail-title">Version {{ selectedVersion.version }} Details</h4>
<div class="history-fields">
<div class="history-field" [class.changed]="isFieldChanged('title')">
<span class="history-field-label">Title</span>
<span class="history-field-value">{{ selectedVersion.title }}</span>
</div>
<div class="history-field" [class.changed]="isFieldChanged('description')">
<span class="history-field-label">Description</span>
<span class="history-field-value">{{ selectedVersion.description || '(empty)' }}</span>
</div>
<div class="history-field" [class.changed]="isFieldChanged('priority')">
<span class="history-field-label">Priority</span>
<span class="history-field-value">{{ selectedVersion.priority }}</span>
</div>
<div class="history-field" [class.changed]="isFieldChanged('status')">
<span class="history-field-label">Status</span>
<span class="history-field-value">{{ selectedVersion.status }}</span>
</div>
@if (isFieldChanged('acceptance_criteria')) {
<div class="history-field changed">
<span class="history-field-label">Acceptance Criteria</span>
<pre class="history-field-pre">{{ selectedVersion.acceptance_criteria || '(empty)' }}</pre>
</div>
}
@if (isFieldChanged('test_spec')) {
<div class="history-field changed">
<span class="history-field-label">Test Spec</span>
<pre class="history-field-pre">{{ selectedVersion.test_spec || '(empty)' }}</pre>
</div>
}
@if (isFieldChanged('test_code')) {
<div class="history-field changed">
<span class="history-field-label">Test Code</span>
<pre class="history-field-pre">{{ selectedVersion.test_code || '(empty)' }}</pre>
</div>
}
@if (isFieldChanged('impl_code')) {
<div class="history-field changed">
<span class="history-field-label">Impl Code</span>
<pre class="history-field-pre">{{ selectedVersion.impl_code || '(empty)' }}</pre>
</div>
}
</div>
<button class="action-btn primary small" (click)="restoreVersion(selectedVersion.version)">Restore this version</button>
</div>
}
}
</div>
</div>
<div class="panel-actions">
@if (!editing) {
<button class="action-btn primary" (click)="startEdit()">Edit</button>
<button class="action-btn" (click)="toggleAddChild()">Add Child</button>
<button class="action-btn danger" (click)="deleteNode()">Delete</button>
} @else {
<button class="action-btn primary" (click)="saveEdit()">Save</button>
<button class="action-btn" (click)="cancelEdit()">Cancel</button>
}
</div>
</div>