elspais 0.9.3__py3-none-any.whl → 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- elspais/cli.py +99 -1
- elspais/commands/hash_cmd.py +72 -26
- elspais/commands/reformat_cmd.py +458 -0
- elspais/commands/trace.py +157 -3
- elspais/commands/validate.py +44 -16
- elspais/core/models.py +2 -0
- elspais/core/parser.py +68 -24
- elspais/reformat/__init__.py +50 -0
- elspais/reformat/detector.py +119 -0
- elspais/reformat/hierarchy.py +246 -0
- elspais/reformat/line_breaks.py +220 -0
- elspais/reformat/prompts.py +123 -0
- elspais/reformat/transformer.py +264 -0
- elspais/sponsors/__init__.py +432 -0
- elspais/trace_view/__init__.py +54 -0
- elspais/trace_view/coverage.py +183 -0
- elspais/trace_view/generators/__init__.py +12 -0
- elspais/trace_view/generators/base.py +329 -0
- elspais/trace_view/generators/csv.py +122 -0
- elspais/trace_view/generators/markdown.py +175 -0
- elspais/trace_view/html/__init__.py +31 -0
- elspais/trace_view/html/generator.py +1006 -0
- elspais/trace_view/html/templates/base.html +283 -0
- elspais/trace_view/html/templates/components/code_viewer_modal.html +14 -0
- elspais/trace_view/html/templates/components/file_picker_modal.html +20 -0
- elspais/trace_view/html/templates/components/legend_modal.html +69 -0
- elspais/trace_view/html/templates/components/review_panel.html +118 -0
- elspais/trace_view/html/templates/partials/review/help/help-panel.json +244 -0
- elspais/trace_view/html/templates/partials/review/help/onboarding.json +77 -0
- elspais/trace_view/html/templates/partials/review/help/tooltips.json +237 -0
- elspais/trace_view/html/templates/partials/review/review-comments.js +928 -0
- elspais/trace_view/html/templates/partials/review/review-data.js +961 -0
- elspais/trace_view/html/templates/partials/review/review-help.js +679 -0
- elspais/trace_view/html/templates/partials/review/review-init.js +177 -0
- elspais/trace_view/html/templates/partials/review/review-line-numbers.js +429 -0
- elspais/trace_view/html/templates/partials/review/review-packages.js +1029 -0
- elspais/trace_view/html/templates/partials/review/review-position.js +540 -0
- elspais/trace_view/html/templates/partials/review/review-resize.js +115 -0
- elspais/trace_view/html/templates/partials/review/review-status.js +659 -0
- elspais/trace_view/html/templates/partials/review/review-sync.js +992 -0
- elspais/trace_view/html/templates/partials/review-styles.css +2238 -0
- elspais/trace_view/html/templates/partials/scripts.js +1741 -0
- elspais/trace_view/html/templates/partials/styles.css +1756 -0
- elspais/trace_view/models.py +353 -0
- elspais/trace_view/review/__init__.py +60 -0
- elspais/trace_view/review/branches.py +1149 -0
- elspais/trace_view/review/models.py +1205 -0
- elspais/trace_view/review/position.py +609 -0
- elspais/trace_view/review/server.py +1056 -0
- elspais/trace_view/review/status.py +470 -0
- elspais/trace_view/review/storage.py +1367 -0
- elspais/trace_view/scanning.py +213 -0
- elspais/trace_view/specs/README.md +84 -0
- elspais/trace_view/specs/tv-d00001-template-architecture.md +36 -0
- elspais/trace_view/specs/tv-d00002-css-extraction.md +37 -0
- elspais/trace_view/specs/tv-d00003-js-extraction.md +43 -0
- elspais/trace_view/specs/tv-d00004-build-embedding.md +40 -0
- elspais/trace_view/specs/tv-d00005-test-format.md +78 -0
- elspais/trace_view/specs/tv-d00010-review-data-models.md +33 -0
- elspais/trace_view/specs/tv-d00011-review-storage.md +33 -0
- elspais/trace_view/specs/tv-d00012-position-resolution.md +33 -0
- elspais/trace_view/specs/tv-d00013-git-branches.md +31 -0
- elspais/trace_view/specs/tv-d00014-review-api-server.md +31 -0
- elspais/trace_view/specs/tv-d00015-status-modifier.md +27 -0
- elspais/trace_view/specs/tv-d00016-js-integration.md +33 -0
- elspais/trace_view/specs/tv-p00001-html-generator.md +33 -0
- elspais/trace_view/specs/tv-p00002-review-system.md +29 -0
- {elspais-0.9.3.dist-info → elspais-0.11.0.dist-info}/METADATA +33 -18
- elspais-0.11.0.dist-info/RECORD +101 -0
- elspais-0.9.3.dist-info/RECORD +0 -40
- {elspais-0.9.3.dist-info → elspais-0.11.0.dist-info}/WHEEL +0 -0
- {elspais-0.9.3.dist-info → elspais-0.11.0.dist-info}/entry_points.txt +0 -0
- {elspais-0.9.3.dist-info → elspais-0.11.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1741 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TraceView Interactive Traceability Matrix JavaScript
|
|
3
|
+
*
|
|
4
|
+
* This module provides all interactive functionality for the trace-view HTML report.
|
|
5
|
+
* Organized using the module pattern with logical sub-objects for maintainability.
|
|
6
|
+
*
|
|
7
|
+
* IMPLEMENTS REQUIREMENTS:
|
|
8
|
+
* REQ-tv-d00003: JavaScript Extraction
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const TraceView = (function() {
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
// ==========================================================================
|
|
15
|
+
// State Management (REQ-tv-d00003-I: Global state encapsulated)
|
|
16
|
+
// ==========================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Internal state object - encapsulates all global state variables
|
|
20
|
+
*/
|
|
21
|
+
const state = {
|
|
22
|
+
reqCardStack: [],
|
|
23
|
+
pendingMoves: [],
|
|
24
|
+
movedRequirements: new Map(), // Track moved reqs: reqId -> {from, to}
|
|
25
|
+
editModeActive: false,
|
|
26
|
+
leafOnlyActive: false,
|
|
27
|
+
pendingMovesCollapsed: false,
|
|
28
|
+
filePickerState: { reqId: null, sourceFile: null },
|
|
29
|
+
allSpecFiles: [],
|
|
30
|
+
userAddedFiles: new Set(),
|
|
31
|
+
originalStatusSuffixes: new Map(),
|
|
32
|
+
// Navigation state
|
|
33
|
+
collapsedInstances: new Set(),
|
|
34
|
+
currentView: 'flat',
|
|
35
|
+
// Review mode state
|
|
36
|
+
reviewModeActive: false
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ==========================================================================
|
|
40
|
+
// Helper Functions
|
|
41
|
+
// ==========================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Escape HTML special characters
|
|
45
|
+
* @param {string} text - Text to escape
|
|
46
|
+
* @returns {string} Escaped text
|
|
47
|
+
*/
|
|
48
|
+
function escapeHtml(text) {
|
|
49
|
+
const div = document.createElement('div');
|
|
50
|
+
div.textContent = text;
|
|
51
|
+
return div.innerHTML;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Show a toast notification
|
|
56
|
+
* @param {string} message - Message to display
|
|
57
|
+
* @param {string} type - Type: 'success', 'error', 'warning', 'info'
|
|
58
|
+
*/
|
|
59
|
+
function showToast(message, type = 'info') {
|
|
60
|
+
// Create toast container if it doesn't exist
|
|
61
|
+
let container = document.getElementById('toast-container');
|
|
62
|
+
if (!container) {
|
|
63
|
+
container = document.createElement('div');
|
|
64
|
+
container.id = 'toast-container';
|
|
65
|
+
container.className = 'toast-container';
|
|
66
|
+
document.body.appendChild(container);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create toast element
|
|
70
|
+
const toast = document.createElement('div');
|
|
71
|
+
toast.className = `toast toast-${type}`;
|
|
72
|
+
toast.textContent = message;
|
|
73
|
+
|
|
74
|
+
// Add to container
|
|
75
|
+
container.appendChild(toast);
|
|
76
|
+
|
|
77
|
+
// Trigger animation
|
|
78
|
+
requestAnimationFrame(() => {
|
|
79
|
+
toast.classList.add('show');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Auto-remove after 4 seconds
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
toast.classList.remove('show');
|
|
85
|
+
setTimeout(() => toast.remove(), 300);
|
|
86
|
+
}, 4000);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Render markdown body with line numbers (table-based layout for alignment)
|
|
91
|
+
* Line numbers are file-relative (starting from req.line)
|
|
92
|
+
* @param {string} body - Markdown body text
|
|
93
|
+
* @param {number} startLine - Starting line number in source file
|
|
94
|
+
* @returns {string} HTML with line numbers
|
|
95
|
+
*/
|
|
96
|
+
function renderMarkdownWithLines(body, startLine) {
|
|
97
|
+
const lines = body.split('\n');
|
|
98
|
+
const tableRowsHtml = lines.map((line, i) => {
|
|
99
|
+
const lineNum = startLine + i;
|
|
100
|
+
// Render each line as markdown (basic inline formatting)
|
|
101
|
+
let content = escapeHtml(line);
|
|
102
|
+
// Apply basic markdown formatting
|
|
103
|
+
content = content
|
|
104
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') // Bold
|
|
105
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>') // Italic
|
|
106
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>') // Inline code
|
|
107
|
+
.replace(/^(#{1,6})\s+(.+)$/, (m, h, t) => // Headers
|
|
108
|
+
`<span class="md-heading md-h${h.length}">${t}</span>`)
|
|
109
|
+
.replace(/^[-*+]\s+(.+)$/, '<span class="md-list-item">• $1</span>') // Bullet list items
|
|
110
|
+
.replace(/^\d+\.\s+(.+)$/, '<span class="md-list-item">$1</span>') // Numbered list (1. 2. 3.)
|
|
111
|
+
.replace(/^([A-Za-z])\.\s+(.+)$/, '<span class="md-list-item">$1. $2</span>'); // Lettered list (A. B. C.)
|
|
112
|
+
|
|
113
|
+
return `<div class="rs-line-row" data-line="${lineNum}">
|
|
114
|
+
<span class="rs-line-number">${lineNum}</span>
|
|
115
|
+
<span class="rs-line-text">${content || ' '}</span>
|
|
116
|
+
</div>`;
|
|
117
|
+
}).join('');
|
|
118
|
+
|
|
119
|
+
return `<div class="rs-lines-table">${tableRowsHtml}</div>`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ==========================================================================
|
|
123
|
+
// Panel Management (REQ-tv-d00003-F: Logical sub-objects)
|
|
124
|
+
// ==========================================================================
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Side panel operations for requirement details
|
|
128
|
+
*/
|
|
129
|
+
const panel = {
|
|
130
|
+
/**
|
|
131
|
+
* Open a requirement in the side panel
|
|
132
|
+
* @param {string} reqId - The requirement ID to display
|
|
133
|
+
*/
|
|
134
|
+
open: function(reqId) {
|
|
135
|
+
const panelEl = document.getElementById('req-panel');
|
|
136
|
+
const cardStack = document.getElementById('req-card-stack');
|
|
137
|
+
const reqData = window.REQ_CONTENT_DATA;
|
|
138
|
+
|
|
139
|
+
if (!reqData || !reqData[reqId]) {
|
|
140
|
+
console.error('Requirement data not found:', reqId);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Show panel if hidden
|
|
145
|
+
panelEl.classList.remove('hidden');
|
|
146
|
+
|
|
147
|
+
// Check if card already exists - if so, move it to top
|
|
148
|
+
if (state.reqCardStack.includes(reqId)) {
|
|
149
|
+
// Remove existing card and re-add at top
|
|
150
|
+
const existingCard = document.getElementById(`req-card-${reqId}`);
|
|
151
|
+
if (existingCard) {
|
|
152
|
+
existingCard.remove();
|
|
153
|
+
}
|
|
154
|
+
const index = state.reqCardStack.indexOf(reqId);
|
|
155
|
+
if (index > -1) {
|
|
156
|
+
state.reqCardStack.splice(index, 1);
|
|
157
|
+
}
|
|
158
|
+
// Continue to create new card at top
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Add to stack
|
|
162
|
+
state.reqCardStack.unshift(reqId);
|
|
163
|
+
|
|
164
|
+
// Create card element
|
|
165
|
+
const req = reqData[reqId];
|
|
166
|
+
const card = document.createElement('div');
|
|
167
|
+
card.className = 'req-card';
|
|
168
|
+
card.id = `req-card-${reqId}`;
|
|
169
|
+
|
|
170
|
+
// Render markdown content with line numbers
|
|
171
|
+
// Line numbers are file-relative (starting from req.line)
|
|
172
|
+
const bodyHtml = renderMarkdownWithLines(req.body, req.line);
|
|
173
|
+
// Rationale starts after body - calculate line offset
|
|
174
|
+
const rationaleStartLine = req.line + req.body.split('\n').length + 2; // +2 for "Rationale:" header
|
|
175
|
+
const rationaleHtml = req.rationale
|
|
176
|
+
? renderMarkdownWithLines(req.rationale, rationaleStartLine)
|
|
177
|
+
: '';
|
|
178
|
+
|
|
179
|
+
// Build implements links
|
|
180
|
+
let implementsHtml = '';
|
|
181
|
+
if (req.implements && req.implements.length > 0) {
|
|
182
|
+
const implLinks = req.implements.sort().map(parentId =>
|
|
183
|
+
`<a href="#" onclick="TraceView.panel.open('${parentId}'); return false;" class="implements-link">${parentId}</a>`
|
|
184
|
+
).join(', ');
|
|
185
|
+
implementsHtml = `<div class="req-card-implements">Implements: ${implLinks}</div>`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Determine if in roadmap based on file path
|
|
189
|
+
const isInRoadmap = req.filePath.includes('roadmap/');
|
|
190
|
+
const moveButtons = isInRoadmap
|
|
191
|
+
? `<button class="edit-btn from-roadmap panel-edit-btn" onclick="TraceView.editMode.addMove('${reqId}', '${req.file}', 'from-roadmap')" title="Move out of roadmap">↩ From Roadmap</button>
|
|
192
|
+
<button class="edit-btn move-file panel-edit-btn" onclick="TraceView.filePicker.show('${reqId}', '${req.file}')" title="Move to different file">📁 Move</button>`
|
|
193
|
+
: `<button class="edit-btn to-roadmap panel-edit-btn" onclick="TraceView.editMode.addMove('${reqId}', '${req.file}', 'to-roadmap')" title="Move to roadmap">🗺️ To Roadmap</button>
|
|
194
|
+
<button class="edit-btn move-file panel-edit-btn" onclick="TraceView.filePicker.show('${reqId}', '${req.file}')" title="Move to different file">📁 Move</button>`;
|
|
195
|
+
|
|
196
|
+
// Generate VS Code link - use relative path when REPO_ROOT is empty (portable mode)
|
|
197
|
+
// Strip ALL leading ../ components to get path relative to repo root
|
|
198
|
+
const repoRelPath = req.filePath.replace(/^(\.\.\/)+/, '');
|
|
199
|
+
const vscodeHref = window.REPO_ROOT
|
|
200
|
+
? `vscode://file/${window.REPO_ROOT}/${repoRelPath}:${req.line}`
|
|
201
|
+
: `${req.filePath}`; // Relative link for portable mode
|
|
202
|
+
const vscodeTitle = window.REPO_ROOT
|
|
203
|
+
? 'Open in VS Code'
|
|
204
|
+
: `Open file (${repoRelPath}:${req.line})`;
|
|
205
|
+
|
|
206
|
+
card.innerHTML = `
|
|
207
|
+
<div class="req-card-header">
|
|
208
|
+
<span class="req-card-title">REQ-${reqId}: ${req.title}</span>
|
|
209
|
+
<button class="close-btn" onclick="TraceView.panel.close('${reqId}')">×</button>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="req-card-body">
|
|
212
|
+
<div class="req-card-meta">
|
|
213
|
+
<span class="badge">${req.level}</span>
|
|
214
|
+
<span class="badge">${req.status}</span>
|
|
215
|
+
<a href="#" onclick="TraceView.codeViewer.open('${req.filePath}', ${req.line}); return false;" class="file-ref-link">${req.file}:${req.line}</a>
|
|
216
|
+
<a href="${vscodeHref}" title="${vscodeTitle}" class="vscode-link">🔧</a>
|
|
217
|
+
</div>
|
|
218
|
+
<div class="req-card-actions edit-actions">
|
|
219
|
+
${moveButtons}
|
|
220
|
+
</div>
|
|
221
|
+
${implementsHtml}
|
|
222
|
+
<div class="req-card-content rs-lined-content">
|
|
223
|
+
<div class="req-body-section">
|
|
224
|
+
<h5 class="rs-section-label">Body</h5>
|
|
225
|
+
${bodyHtml}
|
|
226
|
+
</div>
|
|
227
|
+
${rationaleHtml ? `
|
|
228
|
+
<div class="req-rationale-section">
|
|
229
|
+
<h5 class="rs-section-label">Rationale</h5>
|
|
230
|
+
${rationaleHtml}
|
|
231
|
+
</div>` : ''}
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
`;
|
|
235
|
+
|
|
236
|
+
// Add to top of stack
|
|
237
|
+
cardStack.insertBefore(card, cardStack.firstChild);
|
|
238
|
+
|
|
239
|
+
// Add click handler to update Review Panel when card is clicked
|
|
240
|
+
card.addEventListener('click', function(e) {
|
|
241
|
+
// Don't trigger if clicking on buttons, links, or inputs
|
|
242
|
+
if (e.target.closest('button, a, input, textarea')) return;
|
|
243
|
+
|
|
244
|
+
const reviewActive = (window.ReviewSystem && window.ReviewSystem.isReviewModeActive && window.ReviewSystem.isReviewModeActive()) ||
|
|
245
|
+
state.reviewModeActive;
|
|
246
|
+
if (reviewActive) {
|
|
247
|
+
document.dispatchEvent(new CustomEvent('traceview:req-selected', {
|
|
248
|
+
detail: { reqId: reqId, req: req }
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Notify review system if in review mode (initial open)
|
|
254
|
+
// Check ReviewSystem.isReviewModeActive() directly since it's the source of truth
|
|
255
|
+
const reviewActive = (window.ReviewSystem && window.ReviewSystem.isReviewModeActive && window.ReviewSystem.isReviewModeActive()) ||
|
|
256
|
+
state.reviewModeActive;
|
|
257
|
+
if (reviewActive) {
|
|
258
|
+
// Apply interactive line numbers with click handlers (REQ-d00092)
|
|
259
|
+
if (window.ReviewSystem && window.ReviewSystem.applyLineNumbersToCard) {
|
|
260
|
+
window.ReviewSystem.applyLineNumbersToCard(card, reqId);
|
|
261
|
+
} else if (window.TraceView && window.TraceView.review && window.TraceView.review.applyLineNumbersToCard) {
|
|
262
|
+
window.TraceView.review.applyLineNumbersToCard(card, reqId);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
document.dispatchEvent(new CustomEvent('traceview:req-selected', {
|
|
266
|
+
detail: { reqId: reqId, req: req }
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Close a specific requirement card
|
|
273
|
+
* @param {string} reqId - The requirement ID to close
|
|
274
|
+
*/
|
|
275
|
+
close: function(reqId) {
|
|
276
|
+
const card = document.getElementById(`req-card-${reqId}`);
|
|
277
|
+
if (card) {
|
|
278
|
+
card.remove();
|
|
279
|
+
}
|
|
280
|
+
const index = state.reqCardStack.indexOf(reqId);
|
|
281
|
+
if (index > -1) {
|
|
282
|
+
state.reqCardStack.splice(index, 1);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Hide panel if empty
|
|
286
|
+
if (state.reqCardStack.length === 0) {
|
|
287
|
+
document.getElementById('req-panel').classList.add('hidden');
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Close all requirement cards
|
|
293
|
+
*/
|
|
294
|
+
closeAll: function() {
|
|
295
|
+
const cardStack = document.getElementById('req-card-stack');
|
|
296
|
+
cardStack.innerHTML = '';
|
|
297
|
+
state.reqCardStack.length = 0;
|
|
298
|
+
document.getElementById('req-panel').classList.add('hidden');
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Initialize panel resize functionality
|
|
303
|
+
*/
|
|
304
|
+
initResize: function() {
|
|
305
|
+
const panelEl = document.getElementById('req-panel');
|
|
306
|
+
const handle = document.getElementById('resizeHandle');
|
|
307
|
+
if (!panelEl || !handle) return;
|
|
308
|
+
|
|
309
|
+
let isResizing = false;
|
|
310
|
+
let startX, startWidth;
|
|
311
|
+
|
|
312
|
+
handle.addEventListener('mousedown', function(e) {
|
|
313
|
+
isResizing = true;
|
|
314
|
+
startX = e.clientX;
|
|
315
|
+
startWidth = panelEl.offsetWidth;
|
|
316
|
+
handle.classList.add('dragging');
|
|
317
|
+
document.body.style.cursor = 'col-resize';
|
|
318
|
+
document.body.style.userSelect = 'none';
|
|
319
|
+
e.preventDefault();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
document.addEventListener('mousemove', function(e) {
|
|
323
|
+
if (!isResizing) return;
|
|
324
|
+
const diff = startX - e.clientX;
|
|
325
|
+
const newWidth = Math.min(Math.max(startWidth + diff, 250), window.innerWidth * 0.7);
|
|
326
|
+
panelEl.style.width = newWidth + 'px';
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
document.addEventListener('mouseup', function() {
|
|
330
|
+
if (isResizing) {
|
|
331
|
+
isResizing = false;
|
|
332
|
+
handle.classList.remove('dragging');
|
|
333
|
+
document.body.style.cursor = '';
|
|
334
|
+
document.body.style.userSelect = '';
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// ==========================================================================
|
|
341
|
+
// Code Viewer (REQ-tv-d00003-F: Logical sub-objects)
|
|
342
|
+
// ==========================================================================
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Code viewer modal operations
|
|
346
|
+
*/
|
|
347
|
+
const codeViewer = {
|
|
348
|
+
/**
|
|
349
|
+
* Get language class for syntax highlighting
|
|
350
|
+
* @param {string} ext - File extension
|
|
351
|
+
* @returns {string} Language class for highlight.js
|
|
352
|
+
*/
|
|
353
|
+
getLangClass: function(ext) {
|
|
354
|
+
const langMap = {
|
|
355
|
+
'dart': 'language-dart',
|
|
356
|
+
'sql': 'language-sql',
|
|
357
|
+
'py': 'language-python',
|
|
358
|
+
'js': 'language-javascript',
|
|
359
|
+
'ts': 'language-typescript',
|
|
360
|
+
'json': 'language-json',
|
|
361
|
+
'md': 'language-markdown',
|
|
362
|
+
'yaml': 'language-yaml',
|
|
363
|
+
'yml': 'language-yaml',
|
|
364
|
+
'sh': 'language-bash',
|
|
365
|
+
'bash': 'language-bash'
|
|
366
|
+
};
|
|
367
|
+
return langMap[ext] || 'language-plaintext';
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Open the code viewer modal with file content
|
|
372
|
+
* @param {string} filePath - Path to the file
|
|
373
|
+
* @param {number} lineNum - Line number to highlight
|
|
374
|
+
*/
|
|
375
|
+
open: async function(filePath, lineNum) {
|
|
376
|
+
const modal = document.getElementById('code-viewer-modal');
|
|
377
|
+
const content = document.getElementById('code-viewer-content');
|
|
378
|
+
const title = document.getElementById('code-viewer-title');
|
|
379
|
+
const lineInfo = document.getElementById('code-viewer-line');
|
|
380
|
+
const vscodeLink = document.getElementById('code-viewer-vscode');
|
|
381
|
+
|
|
382
|
+
title.textContent = filePath;
|
|
383
|
+
lineInfo.textContent = `Line ${lineNum}`;
|
|
384
|
+
content.innerHTML = '<div class="loading">Loading...</div>';
|
|
385
|
+
modal.classList.remove('hidden');
|
|
386
|
+
|
|
387
|
+
// Set VS Code link
|
|
388
|
+
if (vscodeLink) {
|
|
389
|
+
// Strip ALL leading ../ components to get path relative to repo root
|
|
390
|
+
const repoRelPath = filePath.replace(/^(\.\.\/)+/, '');
|
|
391
|
+
if (window.REPO_ROOT) {
|
|
392
|
+
const absPath = window.REPO_ROOT + '/' + repoRelPath;
|
|
393
|
+
vscodeLink.href = `vscode://file/${absPath}:${lineNum}`;
|
|
394
|
+
vscodeLink.title = 'Open in VS Code';
|
|
395
|
+
} else {
|
|
396
|
+
vscodeLink.href = filePath;
|
|
397
|
+
vscodeLink.title = `Open file (${repoRelPath}:${lineNum})`;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
// Handle file:// URLs by using the server API (browsers block file:// fetches)
|
|
403
|
+
let fetchUrl = filePath;
|
|
404
|
+
if (filePath.startsWith('file://')) {
|
|
405
|
+
const absPath = filePath.replace('file://', '');
|
|
406
|
+
fetchUrl = `/api/files?path=${encodeURIComponent(absPath)}`;
|
|
407
|
+
}
|
|
408
|
+
const response = await fetch(fetchUrl);
|
|
409
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
410
|
+
const text = await response.text();
|
|
411
|
+
const ext = filePath.split('.').pop().toLowerCase();
|
|
412
|
+
|
|
413
|
+
// For markdown files, render as formatted markdown
|
|
414
|
+
if (ext === 'md' && window.marked) {
|
|
415
|
+
// Pre-process: convert lettered lists (A. B. C.) to proper markdown lists
|
|
416
|
+
// Standard markdown only recognizes numbered (1. 2.) and bullet (- *) lists
|
|
417
|
+
const processedText = this._preprocessMarkdown(text);
|
|
418
|
+
const renderedHtml = marked.parse(processedText);
|
|
419
|
+
content.innerHTML = `<div class="markdown-viewer markdown-body">${renderedHtml}</div>`;
|
|
420
|
+
content.classList.add('markdown-mode');
|
|
421
|
+
this._scrollToMarkdownLine(content, text, lineNum);
|
|
422
|
+
} else {
|
|
423
|
+
// For code files, show with line numbers
|
|
424
|
+
content.classList.remove('markdown-mode');
|
|
425
|
+
this._renderCodeWithLines(content, text, lineNum, ext);
|
|
426
|
+
}
|
|
427
|
+
} catch (err) {
|
|
428
|
+
content.innerHTML = `<div class="error">Failed to load file: ${err.message}</div>`;
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Render code with line numbers and highlighting
|
|
434
|
+
* @private
|
|
435
|
+
*/
|
|
436
|
+
_renderCodeWithLines: function(content, text, lineNum, ext) {
|
|
437
|
+
const lines = text.split('\n');
|
|
438
|
+
const langClass = this.getLangClass(ext);
|
|
439
|
+
|
|
440
|
+
let html = '<table class="code-table"><tbody>';
|
|
441
|
+
lines.forEach((line, idx) => {
|
|
442
|
+
const lineNumber = idx + 1;
|
|
443
|
+
const isHighlighted = lineNumber === lineNum;
|
|
444
|
+
const highlightClass = isHighlighted ? 'highlighted-line' : '';
|
|
445
|
+
const lineId = `L${lineNumber}`;
|
|
446
|
+
const escapedLine = line
|
|
447
|
+
.replace(/&/g, '&')
|
|
448
|
+
.replace(/</g, '<')
|
|
449
|
+
.replace(/>/g, '>');
|
|
450
|
+
html += `<tr id="${lineId}" class="${highlightClass}">`;
|
|
451
|
+
html += `<td class="line-num">${lineNumber}</td>`;
|
|
452
|
+
html += `<td class="line-code"><pre><code class="${langClass}">${escapedLine || ' '}</code></pre></td>`;
|
|
453
|
+
html += '</tr>';
|
|
454
|
+
});
|
|
455
|
+
html += '</tbody></table>';
|
|
456
|
+
|
|
457
|
+
content.innerHTML = html;
|
|
458
|
+
|
|
459
|
+
// Scroll to highlighted line
|
|
460
|
+
setTimeout(() => {
|
|
461
|
+
const highlightedRow = content.querySelector('.highlighted-line');
|
|
462
|
+
if (highlightedRow) {
|
|
463
|
+
highlightedRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
464
|
+
}
|
|
465
|
+
}, 100);
|
|
466
|
+
|
|
467
|
+
// Apply syntax highlighting if hljs is available
|
|
468
|
+
if (window.hljs) {
|
|
469
|
+
content.querySelectorAll('code').forEach(block => {
|
|
470
|
+
hljs.highlightElement(block);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Scroll to approximate line in markdown view
|
|
477
|
+
* @private
|
|
478
|
+
*/
|
|
479
|
+
_scrollToMarkdownLine: function(content, text, lineNum) {
|
|
480
|
+
const lines = text.split('\n');
|
|
481
|
+
setTimeout(() => {
|
|
482
|
+
let targetElement = null;
|
|
483
|
+
|
|
484
|
+
// Find the nearest heading at or before the target line
|
|
485
|
+
const headings = content.querySelectorAll('h1, h2, h3, h4');
|
|
486
|
+
for (const heading of headings) {
|
|
487
|
+
const headingText = heading.textContent.trim();
|
|
488
|
+
for (let i = 0; i < lines.length; i++) {
|
|
489
|
+
const line = lines[i].trim();
|
|
490
|
+
if (line.startsWith('#') && line.includes(headingText)) {
|
|
491
|
+
if (i + 1 <= lineNum) {
|
|
492
|
+
targetElement = heading;
|
|
493
|
+
}
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Fallback to first heading
|
|
500
|
+
if (!targetElement) {
|
|
501
|
+
targetElement = content.querySelector('h1, h2, h3');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (targetElement) {
|
|
505
|
+
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
506
|
+
targetElement.classList.add('highlight-target');
|
|
507
|
+
setTimeout(() => targetElement.classList.remove('highlight-target'), 2000);
|
|
508
|
+
}
|
|
509
|
+
}, 100);
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Pre-process markdown to handle non-standard list formats
|
|
514
|
+
* Converts lettered lists (A. B. C.) to standard bullet lists
|
|
515
|
+
* @private
|
|
516
|
+
*/
|
|
517
|
+
_preprocessMarkdown: function(text) {
|
|
518
|
+
const lines = text.split('\n');
|
|
519
|
+
const result = [];
|
|
520
|
+
|
|
521
|
+
for (let i = 0; i < lines.length; i++) {
|
|
522
|
+
let line = lines[i];
|
|
523
|
+
// Match lines starting with a single uppercase letter followed by period and space
|
|
524
|
+
// e.g., "A. The system SHALL..." -> "- **A.** The system SHALL..."
|
|
525
|
+
const letteredMatch = line.match(/^([A-Z])\.\s+(.+)$/);
|
|
526
|
+
if (letteredMatch) {
|
|
527
|
+
// Convert to bullet list with bold letter prefix
|
|
528
|
+
line = `- **${letteredMatch[1]}.** ${letteredMatch[2]}`;
|
|
529
|
+
}
|
|
530
|
+
result.push(line);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return result.join('\n');
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Close the code viewer modal
|
|
538
|
+
*/
|
|
539
|
+
close: function() {
|
|
540
|
+
document.getElementById('code-viewer-modal').classList.add('hidden');
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// ==========================================================================
|
|
545
|
+
// Edit Mode (REQ-tv-d00003-F: Logical sub-objects)
|
|
546
|
+
// ==========================================================================
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Edit mode operations for batch requirement moves
|
|
550
|
+
*/
|
|
551
|
+
const editMode = {
|
|
552
|
+
/**
|
|
553
|
+
* Toggle edit mode on/off
|
|
554
|
+
*/
|
|
555
|
+
toggle: function() {
|
|
556
|
+
state.editModeActive = !state.editModeActive;
|
|
557
|
+
const btn = document.getElementById('btnEditMode');
|
|
558
|
+
const panel = document.getElementById('editModePanel');
|
|
559
|
+
|
|
560
|
+
if (state.editModeActive) {
|
|
561
|
+
document.body.classList.add('edit-mode-active');
|
|
562
|
+
btn.classList.add('active');
|
|
563
|
+
panel.style.display = 'block';
|
|
564
|
+
document.getElementById('chkIncludeRoadmap').checked = true;
|
|
565
|
+
applyFilters();
|
|
566
|
+
} else {
|
|
567
|
+
document.body.classList.remove('edit-mode-active');
|
|
568
|
+
btn.classList.remove('active');
|
|
569
|
+
panel.style.display = 'none';
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Add a pending move operation
|
|
575
|
+
* @param {string} reqId - Requirement ID
|
|
576
|
+
* @param {string} sourceFile - Source file path
|
|
577
|
+
* @param {string} moveType - Type of move ('to-roadmap', 'from-roadmap', 'move-file')
|
|
578
|
+
*/
|
|
579
|
+
addMove: function(reqId, sourceFile, moveType) {
|
|
580
|
+
const existing = state.pendingMoves.find(m => m.reqId === reqId);
|
|
581
|
+
if (existing) {
|
|
582
|
+
alert('This requirement already has a pending move. Clear selection first.');
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const reqItem = document.querySelector(`.req-item[data-req-id="${reqId}"]`);
|
|
587
|
+
const title = reqItem ? reqItem.dataset.title : '';
|
|
588
|
+
|
|
589
|
+
const move = {
|
|
590
|
+
reqId: reqId,
|
|
591
|
+
sourceFile: sourceFile,
|
|
592
|
+
moveType: moveType,
|
|
593
|
+
title: title,
|
|
594
|
+
targetFile: moveType === 'to-roadmap' ? `roadmap/${sourceFile}` :
|
|
595
|
+
moveType === 'from-roadmap' ? sourceFile.replace('roadmap/', '') :
|
|
596
|
+
null
|
|
597
|
+
};
|
|
598
|
+
state.pendingMoves.push(move);
|
|
599
|
+
this._updateUI();
|
|
600
|
+
this._updateDestinationColumns();
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Remove a pending move by index
|
|
605
|
+
* @param {number} index - Index in pendingMoves array
|
|
606
|
+
*/
|
|
607
|
+
removeMove: function(index) {
|
|
608
|
+
state.pendingMoves.splice(index, 1);
|
|
609
|
+
this._updateUI();
|
|
610
|
+
this._updateDestinationColumns();
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Clear all pending moves
|
|
615
|
+
*/
|
|
616
|
+
clearMoves: function() {
|
|
617
|
+
state.pendingMoves.length = 0;
|
|
618
|
+
this._updateUI();
|
|
619
|
+
this._updateDestinationColumns();
|
|
620
|
+
},
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Apply moves via server API
|
|
624
|
+
*/
|
|
625
|
+
applyMoves: async function() {
|
|
626
|
+
if (state.pendingMoves.length === 0) {
|
|
627
|
+
showToast('No pending moves to apply.', 'warning');
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const moves = state.pendingMoves
|
|
632
|
+
.filter(m => m.targetFile)
|
|
633
|
+
.map(m => ({
|
|
634
|
+
reqId: m.reqId,
|
|
635
|
+
source: m.sourceFile,
|
|
636
|
+
target: m.targetFile
|
|
637
|
+
}));
|
|
638
|
+
|
|
639
|
+
if (moves.length === 0) {
|
|
640
|
+
showToast('No valid moves (all moves need target files).', 'warning');
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Disable button during operation
|
|
645
|
+
const btn = document.getElementById('btnApplyMoves');
|
|
646
|
+
if (btn) {
|
|
647
|
+
btn.disabled = true;
|
|
648
|
+
btn.textContent = 'Applying...';
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
const response = await fetch('/api/apply-moves', {
|
|
653
|
+
method: 'POST',
|
|
654
|
+
headers: { 'Content-Type': 'application/json' },
|
|
655
|
+
body: JSON.stringify(moves)
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
const result = await response.json();
|
|
659
|
+
|
|
660
|
+
if (result.success) {
|
|
661
|
+
// Track moved requirements for visual indicator
|
|
662
|
+
moves.forEach(move => {
|
|
663
|
+
state.movedRequirements.set(move.reqId, {
|
|
664
|
+
from: move.source,
|
|
665
|
+
to: move.target
|
|
666
|
+
});
|
|
667
|
+
// Update the file in REQ_CONTENT_DATA
|
|
668
|
+
if (window.REQ_CONTENT_DATA && window.REQ_CONTENT_DATA[move.reqId]) {
|
|
669
|
+
window.REQ_CONTENT_DATA[move.reqId].file = move.target;
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// Clear pending moves
|
|
674
|
+
this.clearMoves();
|
|
675
|
+
|
|
676
|
+
// Update tree to show moved indicators
|
|
677
|
+
this._updateMovedIndicators();
|
|
678
|
+
|
|
679
|
+
showToast(`Successfully moved ${moves.length} requirement(s)`, 'success');
|
|
680
|
+
} else {
|
|
681
|
+
showToast(`Move failed: ${result.error}`, 'error');
|
|
682
|
+
}
|
|
683
|
+
} catch (err) {
|
|
684
|
+
showToast(`Move failed: ${err.message}`, 'error');
|
|
685
|
+
} finally {
|
|
686
|
+
if (btn) {
|
|
687
|
+
btn.disabled = state.pendingMoves.length === 0;
|
|
688
|
+
btn.textContent = 'Apply Moves';
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Update tree nodes to show moved indicators
|
|
695
|
+
* @private
|
|
696
|
+
*/
|
|
697
|
+
_updateMovedIndicators: function() {
|
|
698
|
+
state.movedRequirements.forEach((info, reqId) => {
|
|
699
|
+
// Find tree nodes with this reqId
|
|
700
|
+
const nodes = document.querySelectorAll(`[data-req-id="${reqId}"]`);
|
|
701
|
+
nodes.forEach(node => {
|
|
702
|
+
// Add moved indicator if not already present
|
|
703
|
+
if (!node.querySelector('.moved-indicator')) {
|
|
704
|
+
const indicator = document.createElement('span');
|
|
705
|
+
indicator.className = 'moved-indicator';
|
|
706
|
+
indicator.textContent = ' 📦';
|
|
707
|
+
indicator.title = `Moved from: ${info.from} → ${info.to}`;
|
|
708
|
+
// Insert inline with REQ ID in hierarchy view
|
|
709
|
+
const reqIdEl = node.querySelector('.req-id');
|
|
710
|
+
if (reqIdEl) {
|
|
711
|
+
reqIdEl.appendChild(indicator);
|
|
712
|
+
} else {
|
|
713
|
+
// Fallback for other node types
|
|
714
|
+
node.appendChild(indicator);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
// Also update the Requirements panel if this REQ is open
|
|
720
|
+
const card = document.getElementById(`req-card-${reqId}`);
|
|
721
|
+
if (card) {
|
|
722
|
+
this._updateCardFileInfo(card, reqId, info);
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
},
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Update file info in a requirement card after move
|
|
729
|
+
* @private
|
|
730
|
+
*/
|
|
731
|
+
_updateCardFileInfo: function(card, reqId, moveInfo) {
|
|
732
|
+
// Find the file link in the card header
|
|
733
|
+
const fileLink = card.querySelector('.req-card-file');
|
|
734
|
+
if (fileLink) {
|
|
735
|
+
// Update the file path display
|
|
736
|
+
const oldText = fileLink.textContent;
|
|
737
|
+
fileLink.innerHTML = `📦 ${moveInfo.to} <span class="moved-from">(was: ${moveInfo.from})</span>`;
|
|
738
|
+
fileLink.classList.add('file-moved');
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Toggle pending moves list visibility
|
|
744
|
+
*/
|
|
745
|
+
togglePendingMoves: function() {
|
|
746
|
+
state.pendingMovesCollapsed = !state.pendingMovesCollapsed;
|
|
747
|
+
const list = document.getElementById('pendingMovesList');
|
|
748
|
+
const toggleBtn = document.getElementById('pendingMovesToggle');
|
|
749
|
+
if (state.pendingMovesCollapsed) {
|
|
750
|
+
list.style.display = 'none';
|
|
751
|
+
toggleBtn.textContent = '▶';
|
|
752
|
+
} else {
|
|
753
|
+
list.style.display = 'block';
|
|
754
|
+
toggleBtn.textContent = '▼';
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Update the pending moves UI
|
|
760
|
+
* @private
|
|
761
|
+
*/
|
|
762
|
+
_updateUI: function() {
|
|
763
|
+
const list = document.getElementById('pendingMovesList');
|
|
764
|
+
const count = document.getElementById('pendingChangesCount');
|
|
765
|
+
const btn = document.getElementById('btnApplyMoves');
|
|
766
|
+
|
|
767
|
+
count.textContent = state.pendingMoves.length + ' pending';
|
|
768
|
+
btn.disabled = state.pendingMoves.length === 0;
|
|
769
|
+
|
|
770
|
+
if (state.pendingMoves.length === 0) {
|
|
771
|
+
list.innerHTML = '<div style="color: #666; padding: 10px;">No pending moves. Click edit buttons on requirements to select them.</div>';
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
list.innerHTML = state.pendingMoves.map((m, i) => {
|
|
776
|
+
const displayTarget = m.targetFile ?
|
|
777
|
+
(m.moveType === 'to-roadmap' ? 'Roadmap' :
|
|
778
|
+
m.moveType === 'from-roadmap' ? m.targetFile :
|
|
779
|
+
m.targetFile) :
|
|
780
|
+
'(select target)';
|
|
781
|
+
const titleDisplay = m.title ? ` - ${m.title}` : '';
|
|
782
|
+
return `
|
|
783
|
+
<div class="pending-move-item">
|
|
784
|
+
<span><strong>REQ-${m.reqId}</strong>${titleDisplay}</span>
|
|
785
|
+
<span style="color: #666; margin-left: 8px;">→ ${displayTarget}</span>
|
|
786
|
+
<button onclick="TraceView.editMode.removeMove(${i})" style="background: none; border: none; cursor: pointer; margin-left: auto;">✕</button>
|
|
787
|
+
</div>
|
|
788
|
+
`}).join('');
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Update destination columns in the requirement tree
|
|
793
|
+
* @private
|
|
794
|
+
*/
|
|
795
|
+
_updateDestinationColumns: function() {
|
|
796
|
+
// Reset all destination columns
|
|
797
|
+
document.querySelectorAll('.req-destination').forEach(el => {
|
|
798
|
+
const editActions = el.querySelector('.edit-actions');
|
|
799
|
+
const destText = el.querySelector('.dest-text');
|
|
800
|
+
if (editActions) editActions.style.display = '';
|
|
801
|
+
if (destText) {
|
|
802
|
+
destText.textContent = '';
|
|
803
|
+
destText.style.display = 'none';
|
|
804
|
+
}
|
|
805
|
+
el.className = 'req-destination edit-mode-column';
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// Restore original status suffixes for items not in pending moves
|
|
809
|
+
document.querySelectorAll('.req-item[data-req-id]').forEach(item => {
|
|
810
|
+
const reqId = item.dataset.reqId;
|
|
811
|
+
const suffixEl = item.querySelector('.status-suffix');
|
|
812
|
+
if (suffixEl && state.originalStatusSuffixes.has(reqId)) {
|
|
813
|
+
const original = state.originalStatusSuffixes.get(reqId);
|
|
814
|
+
if (!state.pendingMoves.some(m => m.reqId === reqId)) {
|
|
815
|
+
suffixEl.textContent = original.text;
|
|
816
|
+
suffixEl.className = original.className;
|
|
817
|
+
suffixEl.title = original.title;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// Update destination columns and status suffixes for pending moves
|
|
823
|
+
state.pendingMoves.forEach(m => {
|
|
824
|
+
const reqItem = document.querySelector(`.req-item[data-req-id="${m.reqId}"]`);
|
|
825
|
+
if (!reqItem) return;
|
|
826
|
+
|
|
827
|
+
const destEl = reqItem.querySelector('.req-destination');
|
|
828
|
+
const suffixEl = reqItem.querySelector('.status-suffix');
|
|
829
|
+
|
|
830
|
+
// Save original status suffix if not already saved
|
|
831
|
+
if (suffixEl && !state.originalStatusSuffixes.has(m.reqId)) {
|
|
832
|
+
state.originalStatusSuffixes.set(m.reqId, {
|
|
833
|
+
text: suffixEl.textContent,
|
|
834
|
+
className: suffixEl.className,
|
|
835
|
+
title: suffixEl.title
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Update destination column
|
|
840
|
+
if (destEl) {
|
|
841
|
+
const editActions = destEl.querySelector('.edit-actions');
|
|
842
|
+
const destText = destEl.querySelector('.dest-text');
|
|
843
|
+
|
|
844
|
+
if (editActions) editActions.style.display = 'none';
|
|
845
|
+
if (destText) {
|
|
846
|
+
destText.style.display = '';
|
|
847
|
+
if (m.moveType === 'to-roadmap') {
|
|
848
|
+
destText.textContent = '→ Roadmap';
|
|
849
|
+
destEl.className = 'req-destination edit-mode-column to-roadmap';
|
|
850
|
+
} else if (m.moveType === 'from-roadmap') {
|
|
851
|
+
destText.textContent = '← From Roadmap';
|
|
852
|
+
destEl.className = 'req-destination edit-mode-column from-roadmap';
|
|
853
|
+
} else if (m.targetFile) {
|
|
854
|
+
const displayName = m.targetFile.replace('roadmap/', '').replace(/\.md$/, '');
|
|
855
|
+
destText.textContent = '→ ' + displayName;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Update status suffix
|
|
861
|
+
if (suffixEl) {
|
|
862
|
+
const originalText = state.originalStatusSuffixes.get(m.reqId)?.text || '';
|
|
863
|
+
if (originalText && originalText !== '↝' && originalText !== '⇢') {
|
|
864
|
+
suffixEl.textContent = '⇢' + originalText;
|
|
865
|
+
suffixEl.className = 'status-suffix status-pending-move';
|
|
866
|
+
suffixEl.title = 'PENDING MOVE + ' + (state.originalStatusSuffixes.get(m.reqId)?.title || '');
|
|
867
|
+
} else {
|
|
868
|
+
suffixEl.textContent = '⇢';
|
|
869
|
+
suffixEl.className = 'status-suffix status-pending-move';
|
|
870
|
+
suffixEl.title = 'PENDING MOVE (not yet executed)';
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
// ==========================================================================
|
|
878
|
+
// File Picker (REQ-tv-d00003-F: Logical sub-objects)
|
|
879
|
+
// ==========================================================================
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* File picker modal operations
|
|
883
|
+
*/
|
|
884
|
+
const filePicker = {
|
|
885
|
+
/**
|
|
886
|
+
* Show the file picker modal
|
|
887
|
+
* @param {string} reqId - Requirement ID
|
|
888
|
+
* @param {string} sourceFile - Source file path
|
|
889
|
+
*/
|
|
890
|
+
show: function(reqId, sourceFile) {
|
|
891
|
+
state.filePickerState = { reqId, sourceFile };
|
|
892
|
+
state.allSpecFiles = this._getAvailableFiles();
|
|
893
|
+
|
|
894
|
+
const modal = document.getElementById('file-picker-modal');
|
|
895
|
+
const input = document.getElementById('filePickerInput');
|
|
896
|
+
const error = document.getElementById('filePickerError');
|
|
897
|
+
|
|
898
|
+
input.value = '';
|
|
899
|
+
error.textContent = '';
|
|
900
|
+
error.style.display = 'none';
|
|
901
|
+
|
|
902
|
+
this._renderList('');
|
|
903
|
+
modal.classList.remove('hidden');
|
|
904
|
+
input.focus();
|
|
905
|
+
},
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Close the file picker modal
|
|
909
|
+
*/
|
|
910
|
+
close: function() {
|
|
911
|
+
document.getElementById('file-picker-modal').classList.add('hidden');
|
|
912
|
+
state.filePickerState = { reqId: null, sourceFile: null };
|
|
913
|
+
},
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Filter the file list based on input
|
|
917
|
+
* @param {string} value - Filter value
|
|
918
|
+
*/
|
|
919
|
+
filter: function(value) {
|
|
920
|
+
this._renderList(value);
|
|
921
|
+
this._validate(value);
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Select a file from the list
|
|
926
|
+
* @param {string} filename - Selected filename
|
|
927
|
+
*/
|
|
928
|
+
select: function(filename) {
|
|
929
|
+
document.getElementById('filePickerInput').value = filename;
|
|
930
|
+
this._validate(filename);
|
|
931
|
+
},
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Confirm the file picker selection
|
|
935
|
+
*/
|
|
936
|
+
confirm: function() {
|
|
937
|
+
const input = document.getElementById('filePickerInput');
|
|
938
|
+
const filename = input.value.trim();
|
|
939
|
+
|
|
940
|
+
if (!this._validate(filename)) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
state.userAddedFiles.add(filename);
|
|
945
|
+
editMode.addMove(state.filePickerState.reqId, state.filePickerState.sourceFile, 'move-file');
|
|
946
|
+
state.pendingMoves[state.pendingMoves.length - 1].targetFile = filename;
|
|
947
|
+
editMode._updateUI();
|
|
948
|
+
editMode._updateDestinationColumns();
|
|
949
|
+
|
|
950
|
+
this.close();
|
|
951
|
+
},
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Render the file list
|
|
955
|
+
* @private
|
|
956
|
+
*/
|
|
957
|
+
_renderList: function(filter) {
|
|
958
|
+
const list = document.getElementById('filePickerList');
|
|
959
|
+
const filterLower = filter.toLowerCase();
|
|
960
|
+
|
|
961
|
+
const filtered = state.allSpecFiles.filter(f =>
|
|
962
|
+
f.toLowerCase().includes(filterLower)
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
if (filtered.length === 0 && filter) {
|
|
966
|
+
list.innerHTML = '<div class="file-picker-empty">No matching files. You can enter a new filename.</div>';
|
|
967
|
+
} else {
|
|
968
|
+
list.innerHTML = filtered.map(f =>
|
|
969
|
+
`<div class="file-picker-item" onclick="TraceView.filePicker.select('${f}')">${f}</div>`
|
|
970
|
+
).join('');
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Validate the filename
|
|
976
|
+
* @private
|
|
977
|
+
*/
|
|
978
|
+
_validate: function(filename) {
|
|
979
|
+
const error = document.getElementById('filePickerError');
|
|
980
|
+
|
|
981
|
+
if (!filename || !filename.trim()) {
|
|
982
|
+
error.style.display = 'none';
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
filename = filename.trim();
|
|
987
|
+
|
|
988
|
+
if (!filename.endsWith('.md')) {
|
|
989
|
+
error.textContent = 'Filename must end with .md';
|
|
990
|
+
error.style.display = 'block';
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const illegalChars = /[<>:"|?*\x00-\x1f]/;
|
|
995
|
+
if (illegalChars.test(filename)) {
|
|
996
|
+
error.textContent = 'Filename contains illegal characters';
|
|
997
|
+
error.style.display = 'block';
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (filename.includes(' ')) {
|
|
1002
|
+
error.textContent = 'Use dashes instead of spaces';
|
|
1003
|
+
error.style.display = 'block';
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (/^[.\-\/]/.test(filename)) {
|
|
1008
|
+
error.textContent = 'Filename cannot start with . - or /';
|
|
1009
|
+
error.style.display = 'block';
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
error.style.display = 'none';
|
|
1014
|
+
return true;
|
|
1015
|
+
},
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Get available target files
|
|
1019
|
+
* @private
|
|
1020
|
+
*/
|
|
1021
|
+
_getAvailableFiles: function() {
|
|
1022
|
+
const files = new Set();
|
|
1023
|
+
document.querySelectorAll('.req-item[data-file]').forEach(item => {
|
|
1024
|
+
files.add(item.dataset.file);
|
|
1025
|
+
});
|
|
1026
|
+
state.userAddedFiles.forEach(f => files.add(f));
|
|
1027
|
+
return Array.from(files).sort();
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
// ==========================================================================
|
|
1032
|
+
// Legend Modal
|
|
1033
|
+
// ==========================================================================
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Legend modal operations
|
|
1037
|
+
*/
|
|
1038
|
+
const legend = {
|
|
1039
|
+
/**
|
|
1040
|
+
* Open the legend modal
|
|
1041
|
+
*/
|
|
1042
|
+
open: function() {
|
|
1043
|
+
document.getElementById('legend-modal').classList.remove('hidden');
|
|
1044
|
+
},
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Close the legend modal
|
|
1048
|
+
*/
|
|
1049
|
+
close: function() {
|
|
1050
|
+
document.getElementById('legend-modal').classList.add('hidden');
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
// ==========================================================================
|
|
1055
|
+
// Navigation & View Management
|
|
1056
|
+
// ==========================================================================
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Navigation operations for requirement tree
|
|
1060
|
+
*/
|
|
1061
|
+
const navigation = {
|
|
1062
|
+
/**
|
|
1063
|
+
* Toggle a single requirement instance's children
|
|
1064
|
+
* @param {HTMLElement} element - The clicked element
|
|
1065
|
+
*/
|
|
1066
|
+
toggleRequirement: function(element) {
|
|
1067
|
+
const item = element.closest('.req-item');
|
|
1068
|
+
const instanceId = item.dataset.instanceId;
|
|
1069
|
+
const icon = element.querySelector('.collapse-icon');
|
|
1070
|
+
|
|
1071
|
+
if (!icon || !icon.textContent) return; // No children to collapse
|
|
1072
|
+
|
|
1073
|
+
const isExpanding = state.collapsedInstances.has(instanceId);
|
|
1074
|
+
|
|
1075
|
+
if (isExpanding) {
|
|
1076
|
+
state.collapsedInstances.delete(instanceId);
|
|
1077
|
+
icon.classList.remove('collapsed');
|
|
1078
|
+
} else {
|
|
1079
|
+
state.collapsedInstances.add(instanceId);
|
|
1080
|
+
icon.classList.add('collapsed');
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (state.currentView === 'hierarchy') {
|
|
1084
|
+
this.toggleRequirementHierarchy(instanceId, isExpanding);
|
|
1085
|
+
} else {
|
|
1086
|
+
if (isExpanding) {
|
|
1087
|
+
this.showDescendants(instanceId);
|
|
1088
|
+
} else {
|
|
1089
|
+
this.hideDescendants(instanceId);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
this.updateExpandCollapseButtons();
|
|
1093
|
+
},
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* Hide all descendants of a requirement instance
|
|
1097
|
+
* @param {string} parentInstanceId - Parent instance ID
|
|
1098
|
+
*/
|
|
1099
|
+
hideDescendants: function(parentInstanceId) {
|
|
1100
|
+
document.querySelectorAll(`[data-parent-instance-id="${parentInstanceId}"]`).forEach(child => {
|
|
1101
|
+
child.classList.add('collapsed-by-parent');
|
|
1102
|
+
this.hideDescendants(child.dataset.instanceId);
|
|
1103
|
+
});
|
|
1104
|
+
},
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Show immediate children of a requirement instance
|
|
1108
|
+
* @param {string} parentInstanceId - Parent instance ID
|
|
1109
|
+
*/
|
|
1110
|
+
showDescendants: function(parentInstanceId) {
|
|
1111
|
+
document.querySelectorAll(`[data-parent-instance-id="${parentInstanceId}"]`).forEach(child => {
|
|
1112
|
+
child.classList.remove('collapsed-by-parent');
|
|
1113
|
+
});
|
|
1114
|
+
},
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Modified toggle for hierarchy view
|
|
1118
|
+
* @param {string} parentInstanceId - Parent instance ID
|
|
1119
|
+
* @param {boolean} isExpanding - Whether expanding or collapsing
|
|
1120
|
+
*/
|
|
1121
|
+
toggleRequirementHierarchy: function(parentInstanceId, isExpanding) {
|
|
1122
|
+
document.querySelectorAll(`[data-parent-instance-id="${parentInstanceId}"]`).forEach(child => {
|
|
1123
|
+
if (isExpanding) {
|
|
1124
|
+
child.classList.add('hierarchy-visible');
|
|
1125
|
+
child.classList.remove('collapsed-by-parent');
|
|
1126
|
+
} else {
|
|
1127
|
+
child.classList.remove('hierarchy-visible');
|
|
1128
|
+
child.classList.add('collapsed-by-parent');
|
|
1129
|
+
const childIcon = child.querySelector('.collapse-icon');
|
|
1130
|
+
if (childIcon && childIcon.textContent) {
|
|
1131
|
+
state.collapsedInstances.add(child.dataset.instanceId);
|
|
1132
|
+
childIcon.classList.add('collapsed');
|
|
1133
|
+
this.toggleRequirementHierarchy(child.dataset.instanceId, false);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
},
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Update expand/collapse button states
|
|
1141
|
+
*/
|
|
1142
|
+
updateExpandCollapseButtons: function() {
|
|
1143
|
+
const btnExpand = document.getElementById('btnExpandAll');
|
|
1144
|
+
const btnCollapse = document.getElementById('btnCollapseAll');
|
|
1145
|
+
|
|
1146
|
+
let expandableCount = 0;
|
|
1147
|
+
let expandedCount = 0;
|
|
1148
|
+
let collapsedCount = 0;
|
|
1149
|
+
|
|
1150
|
+
document.querySelectorAll('.req-item:not(.filtered-out)').forEach(item => {
|
|
1151
|
+
const icon = item.querySelector('.collapse-icon');
|
|
1152
|
+
if (icon && icon.textContent) {
|
|
1153
|
+
expandableCount++;
|
|
1154
|
+
if (icon.classList.contains('collapsed')) {
|
|
1155
|
+
collapsedCount++;
|
|
1156
|
+
} else {
|
|
1157
|
+
expandedCount++;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
if (expandableCount > 0 && expandedCount === expandableCount) {
|
|
1163
|
+
btnExpand.classList.add('active');
|
|
1164
|
+
btnExpand.textContent = '▼ All Expanded';
|
|
1165
|
+
} else {
|
|
1166
|
+
btnExpand.classList.remove('active');
|
|
1167
|
+
btnExpand.textContent = '▼ Expand All';
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (expandableCount > 0 && collapsedCount === expandableCount) {
|
|
1171
|
+
btnCollapse.classList.add('active');
|
|
1172
|
+
btnCollapse.textContent = '▶ All Collapsed';
|
|
1173
|
+
} else {
|
|
1174
|
+
btnCollapse.classList.remove('active');
|
|
1175
|
+
btnCollapse.textContent = '▶ Collapse All';
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Expand all requirements
|
|
1181
|
+
*/
|
|
1182
|
+
expandAll: function() {
|
|
1183
|
+
state.collapsedInstances.clear();
|
|
1184
|
+
const isHierarchyView = state.currentView === 'hierarchy';
|
|
1185
|
+
document.querySelectorAll('.req-item').forEach(item => {
|
|
1186
|
+
item.classList.remove('collapsed-by-parent');
|
|
1187
|
+
if (isHierarchyView && item.dataset.isRoot !== 'true') {
|
|
1188
|
+
item.classList.add('hierarchy-visible');
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
document.querySelectorAll('.collapse-icon').forEach(el => {
|
|
1192
|
+
el.classList.remove('collapsed');
|
|
1193
|
+
});
|
|
1194
|
+
this.updateExpandCollapseButtons();
|
|
1195
|
+
},
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Collapse all requirements
|
|
1199
|
+
*/
|
|
1200
|
+
collapseAll: function() {
|
|
1201
|
+
const isHierarchyView = state.currentView === 'hierarchy';
|
|
1202
|
+
document.querySelectorAll('.req-item').forEach(item => {
|
|
1203
|
+
const icon = item.querySelector('.collapse-icon');
|
|
1204
|
+
if (isHierarchyView && item.dataset.isRoot !== 'true') {
|
|
1205
|
+
item.classList.remove('hierarchy-visible');
|
|
1206
|
+
item.classList.add('collapsed-by-parent');
|
|
1207
|
+
}
|
|
1208
|
+
if (icon && icon.textContent) {
|
|
1209
|
+
state.collapsedInstances.add(item.dataset.instanceId);
|
|
1210
|
+
this.hideDescendants(item.dataset.instanceId);
|
|
1211
|
+
icon.classList.add('collapsed');
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
this.updateExpandCollapseButtons();
|
|
1215
|
+
},
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Switch between view modes
|
|
1219
|
+
* @param {string} viewMode - 'flat', 'hierarchy', 'uncommitted', or 'branch'
|
|
1220
|
+
*/
|
|
1221
|
+
switchView: function(viewMode) {
|
|
1222
|
+
state.currentView = viewMode;
|
|
1223
|
+
const reqTree = document.getElementById('reqTree');
|
|
1224
|
+
const btnFlat = document.getElementById('btnFlatView');
|
|
1225
|
+
const btnHierarchy = document.getElementById('btnHierarchyView');
|
|
1226
|
+
const btnUncommitted = document.getElementById('btnUncommittedView');
|
|
1227
|
+
const btnBranch = document.getElementById('btnBranchView');
|
|
1228
|
+
const treeTitle = document.getElementById('treeTitle');
|
|
1229
|
+
|
|
1230
|
+
btnFlat.classList.remove('active');
|
|
1231
|
+
btnHierarchy.classList.remove('active');
|
|
1232
|
+
btnUncommitted.classList.remove('active');
|
|
1233
|
+
btnBranch.classList.remove('active');
|
|
1234
|
+
reqTree.classList.remove('hierarchy-view');
|
|
1235
|
+
reqTree.classList.remove('flat-view');
|
|
1236
|
+
|
|
1237
|
+
if (viewMode === 'hierarchy') {
|
|
1238
|
+
reqTree.classList.add('hierarchy-view');
|
|
1239
|
+
btnHierarchy.classList.add('active');
|
|
1240
|
+
treeTitle.textContent = 'Traceability Tree - Hierarchical View';
|
|
1241
|
+
state.collapsedInstances.clear();
|
|
1242
|
+
document.querySelectorAll('.req-item').forEach(item => {
|
|
1243
|
+
item.classList.remove('collapsed-by-parent');
|
|
1244
|
+
item.classList.remove('hierarchy-visible');
|
|
1245
|
+
const icon = item.querySelector('.collapse-icon');
|
|
1246
|
+
if (icon) {
|
|
1247
|
+
// Reset all icons first
|
|
1248
|
+
icon.classList.remove('collapsed');
|
|
1249
|
+
// Then collapse root items only
|
|
1250
|
+
if (icon.textContent && item.dataset.isRoot === 'true') {
|
|
1251
|
+
state.collapsedInstances.add(item.dataset.instanceId);
|
|
1252
|
+
icon.classList.add('collapsed');
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
} else if (viewMode === 'uncommitted') {
|
|
1257
|
+
btnUncommitted.classList.add('active');
|
|
1258
|
+
treeTitle.textContent = 'Traceability Tree - Uncommitted Changes';
|
|
1259
|
+
document.querySelectorAll('.req-item').forEach(item => {
|
|
1260
|
+
item.classList.remove('hierarchy-visible');
|
|
1261
|
+
});
|
|
1262
|
+
this.collapseAll();
|
|
1263
|
+
} else if (viewMode === 'branch') {
|
|
1264
|
+
btnBranch.classList.add('active');
|
|
1265
|
+
treeTitle.textContent = 'Traceability Tree - Changed vs Main';
|
|
1266
|
+
document.querySelectorAll('.req-item').forEach(item => {
|
|
1267
|
+
item.classList.remove('hierarchy-visible');
|
|
1268
|
+
});
|
|
1269
|
+
this.collapseAll();
|
|
1270
|
+
} else {
|
|
1271
|
+
btnFlat.classList.add('active');
|
|
1272
|
+
treeTitle.textContent = 'Traceability Tree - Flat View';
|
|
1273
|
+
reqTree.classList.add('flat-view');
|
|
1274
|
+
// Reset all collapse state for flat view
|
|
1275
|
+
state.collapsedInstances.clear();
|
|
1276
|
+
document.querySelectorAll('.req-item').forEach(item => {
|
|
1277
|
+
item.classList.remove('hierarchy-visible');
|
|
1278
|
+
item.classList.remove('collapsed-by-parent');
|
|
1279
|
+
});
|
|
1280
|
+
// Reset all collapse icons to expanded state
|
|
1281
|
+
document.querySelectorAll('.collapse-icon').forEach(el => {
|
|
1282
|
+
el.classList.remove('collapsed');
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
applyFilters();
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
// ==========================================================================
|
|
1291
|
+
// Filtering
|
|
1292
|
+
// ==========================================================================
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Apply all filters to the requirement tree
|
|
1296
|
+
*/
|
|
1297
|
+
function applyFilters() {
|
|
1298
|
+
const filterReqId = document.getElementById('filterReqId');
|
|
1299
|
+
const filterTitle = document.getElementById('filterTitle');
|
|
1300
|
+
const filterLevel = document.getElementById('filterLevel');
|
|
1301
|
+
const filterStatus = document.getElementById('filterStatus');
|
|
1302
|
+
const filterTopic = document.getElementById('filterTopic');
|
|
1303
|
+
const filterTests = document.getElementById('filterTests');
|
|
1304
|
+
const filterCoverage = document.getElementById('filterCoverage');
|
|
1305
|
+
const chkIncludeDeprecated = document.getElementById('chkIncludeDeprecated');
|
|
1306
|
+
const chkIncludeRoadmap = document.getElementById('chkIncludeRoadmap');
|
|
1307
|
+
|
|
1308
|
+
const reqIdFilter = filterReqId ? filterReqId.value.toLowerCase().trim() : '';
|
|
1309
|
+
const titleFilter = filterTitle ? filterTitle.value.toLowerCase().trim() : '';
|
|
1310
|
+
const levelFilter = filterLevel ? filterLevel.value : '';
|
|
1311
|
+
const statusFilter = filterStatus ? filterStatus.value : '';
|
|
1312
|
+
const topicFilter = filterTopic ? filterTopic.value.toLowerCase().trim() : '';
|
|
1313
|
+
const testFilter = filterTests ? filterTests.value : '';
|
|
1314
|
+
const coverageFilter = filterCoverage ? filterCoverage.value : '';
|
|
1315
|
+
const isLeafOnly = state.leafOnlyActive;
|
|
1316
|
+
const includeDeprecated = chkIncludeDeprecated ? chkIncludeDeprecated.checked : false;
|
|
1317
|
+
const includeRoadmap = chkIncludeRoadmap ? chkIncludeRoadmap.checked : false;
|
|
1318
|
+
|
|
1319
|
+
const isUncommittedView = state.currentView === 'uncommitted';
|
|
1320
|
+
const isBranchView = state.currentView === 'branch';
|
|
1321
|
+
const isModifiedView = isUncommittedView || isBranchView;
|
|
1322
|
+
const anyFilterActive = reqIdFilter || titleFilter || levelFilter || statusFilter ||
|
|
1323
|
+
topicFilter || testFilter || coverageFilter || isLeafOnly || isModifiedView;
|
|
1324
|
+
|
|
1325
|
+
let visibleCount = 0;
|
|
1326
|
+
const seenReqIds = new Set();
|
|
1327
|
+
const seenVisibleReqIds = new Set();
|
|
1328
|
+
const allReqIds = new Set();
|
|
1329
|
+
|
|
1330
|
+
document.querySelectorAll('.req-item').forEach(item => {
|
|
1331
|
+
const reqId = item.dataset.reqId ? item.dataset.reqId.toLowerCase() : '';
|
|
1332
|
+
const isImplFile = item.classList.contains('impl-file');
|
|
1333
|
+
const status = item.dataset.status;
|
|
1334
|
+
|
|
1335
|
+
if (!isImplFile && reqId) {
|
|
1336
|
+
if (includeDeprecated || status !== 'Deprecated') {
|
|
1337
|
+
allReqIds.add(reqId);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const level = item.dataset.level;
|
|
1342
|
+
const topic = item.dataset.topic ? item.dataset.topic.toLowerCase() : '';
|
|
1343
|
+
const title = item.dataset.title ? item.dataset.title.toLowerCase() : '';
|
|
1344
|
+
const isUncommitted = item.dataset.uncommitted === 'true';
|
|
1345
|
+
const isBranchChanged = item.dataset.branchChanged === 'true';
|
|
1346
|
+
|
|
1347
|
+
let matches = true;
|
|
1348
|
+
|
|
1349
|
+
if (isUncommittedView) {
|
|
1350
|
+
if (isImplFile) {
|
|
1351
|
+
const parentId = item.dataset.parentInstanceId;
|
|
1352
|
+
const parent = document.querySelector(`[data-instance-id="${parentId}"]`);
|
|
1353
|
+
if (!parent || parent.dataset.uncommitted !== 'true') {
|
|
1354
|
+
matches = false;
|
|
1355
|
+
}
|
|
1356
|
+
} else if (!isUncommitted) {
|
|
1357
|
+
matches = false;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if (isBranchView) {
|
|
1362
|
+
if (isImplFile) {
|
|
1363
|
+
const parentId = item.dataset.parentInstanceId;
|
|
1364
|
+
const parent = document.querySelector(`[data-instance-id="${parentId}"]`);
|
|
1365
|
+
if (!parent || parent.dataset.branchChanged !== 'true') {
|
|
1366
|
+
matches = false;
|
|
1367
|
+
}
|
|
1368
|
+
} else if (!isBranchChanged) {
|
|
1369
|
+
matches = false;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
if (reqIdFilter && !reqId.includes(reqIdFilter)) matches = false;
|
|
1374
|
+
if (titleFilter && !title.includes(titleFilter)) matches = false;
|
|
1375
|
+
if (levelFilter && level !== levelFilter) matches = false;
|
|
1376
|
+
if (statusFilter && status !== statusFilter) matches = false;
|
|
1377
|
+
if (topicFilter && topic !== topicFilter && !topic.startsWith(topicFilter + '-')) {
|
|
1378
|
+
matches = false;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// Toggle filter: hidden levels (PRD/OPS/DEV buttons)
|
|
1382
|
+
if (matches && !isImplFile && TraceView.hiddenLevels && TraceView.hiddenLevels.has(level)) {
|
|
1383
|
+
matches = false;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// Toggle filter: hidden repos (CORE/CAL/TTN etc buttons)
|
|
1387
|
+
if (matches && !isImplFile && TraceView.hiddenRepos) {
|
|
1388
|
+
const repo = item.dataset.repo || 'CORE'; // Empty repo = CORE
|
|
1389
|
+
if (TraceView.hiddenRepos.has(repo)) {
|
|
1390
|
+
matches = false;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Toggle filter: hide implementation files
|
|
1395
|
+
if (matches && isImplFile) {
|
|
1396
|
+
// Hide all files if Files toggle is off
|
|
1397
|
+
if (TraceView.hideFiles) {
|
|
1398
|
+
matches = false;
|
|
1399
|
+
} else {
|
|
1400
|
+
// Check if parent requirement is hidden (due to level/repo filter)
|
|
1401
|
+
const parentInstanceId = item.dataset.parentInstanceId;
|
|
1402
|
+
if (parentInstanceId) {
|
|
1403
|
+
const parent = document.querySelector(`[data-instance-id="${parentInstanceId}"]`);
|
|
1404
|
+
if (parent) {
|
|
1405
|
+
const parentLevel = parent.dataset.level;
|
|
1406
|
+
const parentRepo = parent.dataset.repo || 'CORE';
|
|
1407
|
+
// Hide file if parent's level is hidden
|
|
1408
|
+
if (TraceView.hiddenLevels && TraceView.hiddenLevels.has(parentLevel)) {
|
|
1409
|
+
matches = false;
|
|
1410
|
+
}
|
|
1411
|
+
// Hide file if parent's repo is hidden
|
|
1412
|
+
if (matches && TraceView.hiddenRepos && TraceView.hiddenRepos.has(parentRepo)) {
|
|
1413
|
+
matches = false;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
if (testFilter && matches) {
|
|
1421
|
+
const testStatus = item.dataset.testStatus || 'not-tested';
|
|
1422
|
+
if (testFilter !== testStatus) matches = false;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
if (coverageFilter && matches) {
|
|
1426
|
+
const coverage = item.dataset.coverage || 'none';
|
|
1427
|
+
if (coverageFilter !== coverage) matches = false;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
if (isLeafOnly && matches && !isImplFile) {
|
|
1431
|
+
const hasChildren = item.dataset.hasChildren === 'true';
|
|
1432
|
+
if (hasChildren) matches = false;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
if (!includeDeprecated && matches && !isImplFile) {
|
|
1436
|
+
if (status === 'Deprecated') matches = false;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
if (!includeRoadmap && matches && !isImplFile) {
|
|
1440
|
+
const isRoadmap = item.dataset.roadmap === 'true';
|
|
1441
|
+
const isConflict = item.dataset.conflict === 'true';
|
|
1442
|
+
const isCycle = item.dataset.cycle === 'true';
|
|
1443
|
+
if (isRoadmap && !isConflict && !isCycle) matches = false;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
if (matches && anyFilterActive && !isImplFile && seenReqIds.has(reqId)) {
|
|
1447
|
+
matches = false;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if (matches) {
|
|
1451
|
+
item.classList.remove('filtered-out');
|
|
1452
|
+
if (anyFilterActive) {
|
|
1453
|
+
item.classList.remove('collapsed-by-parent');
|
|
1454
|
+
// In hierarchy view, non-root items need hierarchy-visible to be shown
|
|
1455
|
+
const isHierarchyView = state.currentView === 'hierarchy';
|
|
1456
|
+
if (isHierarchyView && item.dataset.isRoot !== 'true') {
|
|
1457
|
+
item.classList.add('hierarchy-visible');
|
|
1458
|
+
}
|
|
1459
|
+
if (!isImplFile) seenReqIds.add(reqId);
|
|
1460
|
+
}
|
|
1461
|
+
if (!isImplFile && reqId && !seenVisibleReqIds.has(reqId)) {
|
|
1462
|
+
seenVisibleReqIds.add(reqId);
|
|
1463
|
+
visibleCount++;
|
|
1464
|
+
}
|
|
1465
|
+
} else {
|
|
1466
|
+
item.classList.add('filtered-out');
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
const totalCount = allReqIds.size;
|
|
1471
|
+
let statsText;
|
|
1472
|
+
if (isUncommittedView) {
|
|
1473
|
+
statsText = `Showing ${visibleCount} uncommitted requirements`;
|
|
1474
|
+
} else if (isBranchView) {
|
|
1475
|
+
statsText = `Showing ${visibleCount} requirements changed vs main`;
|
|
1476
|
+
} else {
|
|
1477
|
+
statsText = `Showing ${visibleCount} of ${totalCount} requirements`;
|
|
1478
|
+
}
|
|
1479
|
+
document.getElementById('filterStats').textContent = statsText;
|
|
1480
|
+
navigation.updateExpandCollapseButtons();
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/**
|
|
1484
|
+
* Clear all filters
|
|
1485
|
+
*/
|
|
1486
|
+
function clearFilters() {
|
|
1487
|
+
const filterReqId = document.getElementById('filterReqId');
|
|
1488
|
+
const filterTitle = document.getElementById('filterTitle');
|
|
1489
|
+
const filterLevel = document.getElementById('filterLevel');
|
|
1490
|
+
const filterStatus = document.getElementById('filterStatus');
|
|
1491
|
+
const filterTopic = document.getElementById('filterTopic');
|
|
1492
|
+
const filterTests = document.getElementById('filterTests');
|
|
1493
|
+
const filterCoverage = document.getElementById('filterCoverage');
|
|
1494
|
+
const btnLeafOnly = document.getElementById('btnLeafOnly');
|
|
1495
|
+
const chkIncludeDeprecated = document.getElementById('chkIncludeDeprecated');
|
|
1496
|
+
const chkIncludeRoadmap = document.getElementById('chkIncludeRoadmap');
|
|
1497
|
+
|
|
1498
|
+
if (filterReqId) filterReqId.value = '';
|
|
1499
|
+
if (filterTitle) filterTitle.value = '';
|
|
1500
|
+
if (filterLevel) filterLevel.value = '';
|
|
1501
|
+
if (filterStatus) filterStatus.value = '';
|
|
1502
|
+
if (filterTopic) filterTopic.value = '';
|
|
1503
|
+
if (filterTests) filterTests.value = '';
|
|
1504
|
+
if (filterCoverage) filterCoverage.value = '';
|
|
1505
|
+
|
|
1506
|
+
state.leafOnlyActive = false;
|
|
1507
|
+
if (btnLeafOnly) btnLeafOnly.classList.remove('active');
|
|
1508
|
+
if (chkIncludeDeprecated) chkIncludeDeprecated.checked = false;
|
|
1509
|
+
if (chkIncludeRoadmap) chkIncludeRoadmap.checked = false;
|
|
1510
|
+
|
|
1511
|
+
toggleIncludeDeprecated();
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// ==========================================================================
|
|
1515
|
+
// Leaf Only Filter
|
|
1516
|
+
// ==========================================================================
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* Toggle leaf-only filter
|
|
1520
|
+
*/
|
|
1521
|
+
function toggleLeafOnly() {
|
|
1522
|
+
state.leafOnlyActive = !state.leafOnlyActive;
|
|
1523
|
+
const btn = document.getElementById('btnLeafOnly');
|
|
1524
|
+
if (state.leafOnlyActive) {
|
|
1525
|
+
btn.classList.add('active');
|
|
1526
|
+
} else {
|
|
1527
|
+
btn.classList.remove('active');
|
|
1528
|
+
}
|
|
1529
|
+
applyFilters();
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* Toggle include deprecated checkbox
|
|
1534
|
+
*/
|
|
1535
|
+
function toggleIncludeDeprecated() {
|
|
1536
|
+
const includeDeprecated = document.getElementById('chkIncludeDeprecated').checked;
|
|
1537
|
+
|
|
1538
|
+
['PRD', 'OPS', 'DEV'].forEach(level => {
|
|
1539
|
+
const badge = document.getElementById('badge' + level);
|
|
1540
|
+
if (badge) {
|
|
1541
|
+
const count = includeDeprecated ? badge.dataset.all : badge.dataset.active;
|
|
1542
|
+
badge.textContent = level + ': ' + count;
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
applyFilters();
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Toggle include roadmap checkbox
|
|
1551
|
+
*/
|
|
1552
|
+
function toggleIncludeRoadmap() {
|
|
1553
|
+
applyFilters();
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// ==========================================================================
|
|
1557
|
+
// Review Mode Toggle (REQ-tv-d00016)
|
|
1558
|
+
// ==========================================================================
|
|
1559
|
+
|
|
1560
|
+
/**
|
|
1561
|
+
* Toggle review mode on/off
|
|
1562
|
+
* Shows/hides the review panel and updates body class for 3-column layout
|
|
1563
|
+
*/
|
|
1564
|
+
function toggleReviewMode() {
|
|
1565
|
+
state.reviewModeActive = !state.reviewModeActive;
|
|
1566
|
+
const btn = document.getElementById('btnReviewMode');
|
|
1567
|
+
const panel = document.getElementById('review-panel');
|
|
1568
|
+
const packagesPanel = document.getElementById('rs-packages-panel');
|
|
1569
|
+
|
|
1570
|
+
if (state.reviewModeActive) {
|
|
1571
|
+
// Activate review mode
|
|
1572
|
+
document.body.classList.add('review-mode-active');
|
|
1573
|
+
if (btn) btn.classList.add('active');
|
|
1574
|
+
if (panel) panel.classList.remove('hidden');
|
|
1575
|
+
if (packagesPanel) packagesPanel.style.display = 'block';
|
|
1576
|
+
|
|
1577
|
+
// Initialize review system if available
|
|
1578
|
+
if (window.TraceView && window.TraceView.review) {
|
|
1579
|
+
window.TraceView.review.init();
|
|
1580
|
+
}
|
|
1581
|
+
} else {
|
|
1582
|
+
// Deactivate review mode
|
|
1583
|
+
document.body.classList.remove('review-mode-active');
|
|
1584
|
+
if (btn) btn.classList.remove('active');
|
|
1585
|
+
if (panel) panel.classList.add('hidden');
|
|
1586
|
+
if (packagesPanel) packagesPanel.style.display = 'none';
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// Dispatch custom event for review system to respond to
|
|
1590
|
+
document.dispatchEvent(new CustomEvent('traceview:review-mode-changed', {
|
|
1591
|
+
detail: { active: state.reviewModeActive }
|
|
1592
|
+
}));
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// ==========================================================================
|
|
1596
|
+
// Initialization (REQ-tv-d00003-J: addEventListener for dynamic elements)
|
|
1597
|
+
// ==========================================================================
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* Initialize TraceView
|
|
1601
|
+
*/
|
|
1602
|
+
function init() {
|
|
1603
|
+
panel.initResize();
|
|
1604
|
+
|
|
1605
|
+
// Close modals on escape key
|
|
1606
|
+
document.addEventListener('keydown', function(e) {
|
|
1607
|
+
if (e.key === 'Escape') {
|
|
1608
|
+
codeViewer.close();
|
|
1609
|
+
legend.close();
|
|
1610
|
+
filePicker.close();
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
// Sync review mode state with review system (REQ-d00092)
|
|
1615
|
+
// The review system dispatches this event when mode changes
|
|
1616
|
+
document.addEventListener('rs:review-mode-changed', function(e) {
|
|
1617
|
+
state.reviewModeActive = e.detail.active;
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// ==========================================================================
|
|
1622
|
+
// Public API
|
|
1623
|
+
// ==========================================================================
|
|
1624
|
+
|
|
1625
|
+
return {
|
|
1626
|
+
// Sub-objects
|
|
1627
|
+
panel: panel,
|
|
1628
|
+
codeViewer: codeViewer,
|
|
1629
|
+
editMode: editMode,
|
|
1630
|
+
filePicker: filePicker,
|
|
1631
|
+
legend: legend,
|
|
1632
|
+
navigation: navigation,
|
|
1633
|
+
state: state,
|
|
1634
|
+
|
|
1635
|
+
// Functions
|
|
1636
|
+
init: init,
|
|
1637
|
+
toggleLeafOnly: toggleLeafOnly,
|
|
1638
|
+
toggleIncludeDeprecated: toggleIncludeDeprecated,
|
|
1639
|
+
toggleIncludeRoadmap: toggleIncludeRoadmap,
|
|
1640
|
+
toggleReviewMode: toggleReviewMode,
|
|
1641
|
+
applyFilters: applyFilters,
|
|
1642
|
+
clearFilters: clearFilters
|
|
1643
|
+
};
|
|
1644
|
+
})();
|
|
1645
|
+
|
|
1646
|
+
// ==========================================================================
|
|
1647
|
+
// Global function aliases for backward compatibility with inline onclick handlers
|
|
1648
|
+
// ==========================================================================
|
|
1649
|
+
|
|
1650
|
+
function openReqPanel(reqId) { TraceView.panel.open(reqId); }
|
|
1651
|
+
function closeReqCard(reqId) { TraceView.panel.close(reqId); }
|
|
1652
|
+
function closeAllCards() { TraceView.panel.closeAll(); }
|
|
1653
|
+
function openCodeViewer(filePath, lineNum) { TraceView.codeViewer.open(filePath, lineNum); }
|
|
1654
|
+
function closeCodeViewer() { TraceView.codeViewer.close(); }
|
|
1655
|
+
function openLegendModal() { TraceView.legend.open(); }
|
|
1656
|
+
function closeLegendModal() { TraceView.legend.close(); }
|
|
1657
|
+
function toggleEditMode() { TraceView.editMode.toggle(); }
|
|
1658
|
+
function addPendingMove(reqId, sourceFile, moveType) { TraceView.editMode.addMove(reqId, sourceFile, moveType); }
|
|
1659
|
+
function removePendingMove(index) { TraceView.editMode.removeMove(index); }
|
|
1660
|
+
function clearPendingMoves() { TraceView.editMode.clearMoves(); }
|
|
1661
|
+
function togglePendingMoves() { TraceView.editMode.togglePendingMoves(); }
|
|
1662
|
+
function applyMoves() { TraceView.editMode.applyMoves(); }
|
|
1663
|
+
function showMoveToFile(reqId, sourceFile) { TraceView.filePicker.show(reqId, sourceFile); }
|
|
1664
|
+
function closeFilePicker() { TraceView.filePicker.close(); }
|
|
1665
|
+
function filterFiles(value) { TraceView.filePicker.filter(value); }
|
|
1666
|
+
function selectFile(filename) { TraceView.filePicker.select(filename); }
|
|
1667
|
+
function confirmFilePicker() { TraceView.filePicker.confirm(); }
|
|
1668
|
+
function toggleLeafOnly() { TraceView.toggleLeafOnly(); }
|
|
1669
|
+
function toggleIncludeDeprecated() { TraceView.toggleIncludeDeprecated(); }
|
|
1670
|
+
function toggleIncludeRoadmap() { TraceView.toggleIncludeRoadmap(); }
|
|
1671
|
+
function toggleReviewMode() { TraceView.toggleReviewMode(); }
|
|
1672
|
+
|
|
1673
|
+
// Navigation functions
|
|
1674
|
+
function toggleRequirement(element) { TraceView.navigation.toggleRequirement(element); }
|
|
1675
|
+
function expandAll() { TraceView.navigation.expandAll(); }
|
|
1676
|
+
function collapseAll() { TraceView.navigation.collapseAll(); }
|
|
1677
|
+
function switchView(viewMode) { TraceView.navigation.switchView(viewMode); }
|
|
1678
|
+
function applyFilters() { TraceView.applyFilters(); }
|
|
1679
|
+
function clearFilters() { TraceView.clearFilters(); }
|
|
1680
|
+
|
|
1681
|
+
// Toggle filters - independent on/off toggles for levels and repos
|
|
1682
|
+
// Track hidden items - everything starts visible, clicking hides them
|
|
1683
|
+
TraceView.hiddenLevels = TraceView.hiddenLevels || new Set();
|
|
1684
|
+
TraceView.hiddenRepos = TraceView.hiddenRepos || new Set();
|
|
1685
|
+
|
|
1686
|
+
// Toggle level filter (PRD/OPS/DEV) - independent on/off
|
|
1687
|
+
function filterByLevel(level) {
|
|
1688
|
+
const badge = document.getElementById('badge' + level);
|
|
1689
|
+
if (!badge) return;
|
|
1690
|
+
|
|
1691
|
+
if (TraceView.hiddenLevels.has(level)) {
|
|
1692
|
+
// Currently hidden, show it
|
|
1693
|
+
TraceView.hiddenLevels.delete(level);
|
|
1694
|
+
badge.classList.remove('filter-hidden');
|
|
1695
|
+
} else {
|
|
1696
|
+
// Currently visible, hide it
|
|
1697
|
+
TraceView.hiddenLevels.add(level);
|
|
1698
|
+
badge.classList.add('filter-hidden');
|
|
1699
|
+
}
|
|
1700
|
+
applyFilters();
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// Toggle repo filter (CORE, CAL, TTN, etc.) - independent on/off
|
|
1704
|
+
function toggleRepoFilter(repoPrefix) {
|
|
1705
|
+
const badge = document.getElementById('badgeRepo' + repoPrefix);
|
|
1706
|
+
if (!badge) return;
|
|
1707
|
+
|
|
1708
|
+
if (TraceView.hiddenRepos.has(repoPrefix)) {
|
|
1709
|
+
// Currently hidden, show it
|
|
1710
|
+
TraceView.hiddenRepos.delete(repoPrefix);
|
|
1711
|
+
badge.classList.remove('filter-hidden');
|
|
1712
|
+
} else {
|
|
1713
|
+
// Currently visible, hide it
|
|
1714
|
+
TraceView.hiddenRepos.add(repoPrefix);
|
|
1715
|
+
badge.classList.add('filter-hidden');
|
|
1716
|
+
}
|
|
1717
|
+
applyFilters();
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// Toggle files filter - show/hide implementation files
|
|
1721
|
+
TraceView.hideFiles = false; // Files visible by default
|
|
1722
|
+
|
|
1723
|
+
function toggleFilesFilter() {
|
|
1724
|
+
const badge = document.getElementById('badgeFiles');
|
|
1725
|
+
if (!badge) return;
|
|
1726
|
+
|
|
1727
|
+
TraceView.hideFiles = !TraceView.hideFiles;
|
|
1728
|
+
if (TraceView.hideFiles) {
|
|
1729
|
+
badge.classList.add('filter-hidden');
|
|
1730
|
+
} else {
|
|
1731
|
+
badge.classList.remove('filter-hidden');
|
|
1732
|
+
}
|
|
1733
|
+
applyFilters();
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// Initialize on DOM ready
|
|
1737
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1738
|
+
TraceView.init();
|
|
1739
|
+
// Start with hierarchical view - show tree structure with collapsible nodes
|
|
1740
|
+
TraceView.navigation.switchView('hierarchy');
|
|
1741
|
+
});
|