elspais 0.11.2__py3-none-any.whl → 0.43.5__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/__init__.py +1 -10
- elspais/{sponsors/__init__.py → associates.py} +102 -56
- elspais/cli.py +366 -69
- elspais/commands/__init__.py +9 -3
- elspais/commands/analyze.py +118 -169
- elspais/commands/changed.py +12 -23
- elspais/commands/config_cmd.py +10 -13
- elspais/commands/edit.py +33 -13
- elspais/commands/example_cmd.py +319 -0
- elspais/commands/hash_cmd.py +161 -183
- elspais/commands/health.py +1177 -0
- elspais/commands/index.py +98 -115
- elspais/commands/init.py +99 -22
- elspais/commands/reformat_cmd.py +41 -433
- elspais/commands/rules_cmd.py +2 -2
- elspais/commands/trace.py +443 -324
- elspais/commands/validate.py +193 -411
- elspais/config/__init__.py +799 -5
- elspais/{core/content_rules.py → content_rules.py} +20 -2
- elspais/docs/cli/assertions.md +67 -0
- elspais/docs/cli/commands.md +304 -0
- elspais/docs/cli/config.md +262 -0
- elspais/docs/cli/format.md +66 -0
- elspais/docs/cli/git.md +45 -0
- elspais/docs/cli/health.md +190 -0
- elspais/docs/cli/hierarchy.md +60 -0
- elspais/docs/cli/ignore.md +72 -0
- elspais/docs/cli/mcp.md +245 -0
- elspais/docs/cli/quickstart.md +58 -0
- elspais/docs/cli/traceability.md +89 -0
- elspais/docs/cli/validation.md +96 -0
- elspais/graph/GraphNode.py +383 -0
- elspais/graph/__init__.py +40 -0
- elspais/graph/annotators.py +927 -0
- elspais/graph/builder.py +1886 -0
- elspais/graph/deserializer.py +248 -0
- elspais/graph/factory.py +284 -0
- elspais/graph/metrics.py +127 -0
- elspais/graph/mutations.py +161 -0
- elspais/graph/parsers/__init__.py +156 -0
- elspais/graph/parsers/code.py +213 -0
- elspais/graph/parsers/comments.py +112 -0
- elspais/graph/parsers/config_helpers.py +29 -0
- elspais/graph/parsers/heredocs.py +225 -0
- elspais/graph/parsers/journey.py +131 -0
- elspais/graph/parsers/remainder.py +79 -0
- elspais/graph/parsers/requirement.py +347 -0
- elspais/graph/parsers/results/__init__.py +6 -0
- elspais/graph/parsers/results/junit_xml.py +229 -0
- elspais/graph/parsers/results/pytest_json.py +313 -0
- elspais/graph/parsers/test.py +305 -0
- elspais/graph/relations.py +78 -0
- elspais/graph/serialize.py +216 -0
- elspais/html/__init__.py +8 -0
- elspais/html/generator.py +731 -0
- elspais/html/templates/trace_view.html.j2 +2151 -0
- elspais/mcp/__init__.py +45 -29
- elspais/mcp/__main__.py +5 -1
- elspais/mcp/file_mutations.py +138 -0
- elspais/mcp/server.py +1998 -244
- elspais/testing/__init__.py +3 -3
- elspais/testing/config.py +3 -0
- elspais/testing/mapper.py +1 -1
- elspais/testing/scanner.py +301 -12
- elspais/utilities/__init__.py +1 -0
- elspais/utilities/docs_loader.py +115 -0
- elspais/utilities/git.py +607 -0
- elspais/{core → utilities}/hasher.py +8 -22
- elspais/utilities/md_renderer.py +189 -0
- elspais/{core → utilities}/patterns.py +56 -51
- elspais/utilities/reference_config.py +626 -0
- elspais/validation/__init__.py +19 -0
- elspais/validation/format.py +264 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/METADATA +7 -4
- elspais-0.43.5.dist-info/RECORD +80 -0
- elspais/config/defaults.py +0 -179
- elspais/config/loader.py +0 -494
- elspais/core/__init__.py +0 -21
- elspais/core/git.py +0 -346
- elspais/core/models.py +0 -320
- elspais/core/parser.py +0 -639
- elspais/core/rules.py +0 -509
- elspais/mcp/context.py +0 -172
- elspais/mcp/serializers.py +0 -112
- elspais/reformat/__init__.py +0 -50
- elspais/reformat/detector.py +0 -112
- elspais/reformat/hierarchy.py +0 -247
- elspais/reformat/line_breaks.py +0 -218
- elspais/reformat/prompts.py +0 -133
- elspais/reformat/transformer.py +0 -266
- elspais/trace_view/__init__.py +0 -55
- elspais/trace_view/coverage.py +0 -183
- elspais/trace_view/generators/__init__.py +0 -12
- elspais/trace_view/generators/base.py +0 -334
- elspais/trace_view/generators/csv.py +0 -118
- elspais/trace_view/generators/markdown.py +0 -170
- elspais/trace_view/html/__init__.py +0 -33
- elspais/trace_view/html/generator.py +0 -1140
- elspais/trace_view/html/templates/base.html +0 -283
- elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -14
- elspais/trace_view/html/templates/components/file_picker_modal.html +0 -20
- elspais/trace_view/html/templates/components/legend_modal.html +0 -69
- elspais/trace_view/html/templates/components/review_panel.html +0 -118
- elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -244
- elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -77
- elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -237
- elspais/trace_view/html/templates/partials/review/review-comments.js +0 -928
- elspais/trace_view/html/templates/partials/review/review-data.js +0 -961
- elspais/trace_view/html/templates/partials/review/review-help.js +0 -679
- elspais/trace_view/html/templates/partials/review/review-init.js +0 -177
- elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -429
- elspais/trace_view/html/templates/partials/review/review-packages.js +0 -1029
- elspais/trace_view/html/templates/partials/review/review-position.js +0 -540
- elspais/trace_view/html/templates/partials/review/review-resize.js +0 -115
- elspais/trace_view/html/templates/partials/review/review-status.js +0 -659
- elspais/trace_view/html/templates/partials/review/review-sync.js +0 -992
- elspais/trace_view/html/templates/partials/review-styles.css +0 -2238
- elspais/trace_view/html/templates/partials/scripts.js +0 -1741
- elspais/trace_view/html/templates/partials/styles.css +0 -1756
- elspais/trace_view/models.py +0 -378
- elspais/trace_view/review/__init__.py +0 -63
- elspais/trace_view/review/branches.py +0 -1142
- elspais/trace_view/review/models.py +0 -1200
- elspais/trace_view/review/position.py +0 -591
- elspais/trace_view/review/server.py +0 -1032
- elspais/trace_view/review/status.py +0 -455
- elspais/trace_view/review/storage.py +0 -1343
- elspais/trace_view/scanning.py +0 -213
- elspais/trace_view/specs/README.md +0 -84
- elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -36
- elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -37
- elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -43
- elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -40
- elspais/trace_view/specs/tv-d00005-test-format.md +0 -78
- elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -33
- elspais/trace_view/specs/tv-d00011-review-storage.md +0 -33
- elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -33
- elspais/trace_view/specs/tv-d00013-git-branches.md +0 -31
- elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -31
- elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -27
- elspais/trace_view/specs/tv-d00016-js-integration.md +0 -33
- elspais/trace_view/specs/tv-p00001-html-generator.md +0 -33
- elspais/trace_view/specs/tv-p00002-review-system.md +0 -29
- elspais-0.11.2.dist-info/RECORD +0 -101
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,928 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TraceView Review Comment UI Module
|
|
3
|
-
*
|
|
4
|
-
* User interface for comment threads:
|
|
5
|
-
* - Thread rendering (collapsible)
|
|
6
|
-
* - Comment form (new thread, reply)
|
|
7
|
-
* - Resolve/unresolve actions
|
|
8
|
-
* - Position selection UI
|
|
9
|
-
* - Click-to-highlight positions
|
|
10
|
-
*
|
|
11
|
-
* IMPLEMENTS REQUIREMENTS:
|
|
12
|
-
* REQ-tv-d00016: Review JavaScript Integration
|
|
13
|
-
* REQ-d00092: Click-to-Highlight Positions
|
|
14
|
-
* REQ-d00087: Position Resolution with Fallback
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
// Ensure TraceView.review namespace exists
|
|
18
|
-
window.TraceView = window.TraceView || {};
|
|
19
|
-
TraceView.review = TraceView.review || {};
|
|
20
|
-
|
|
21
|
-
(function(review) {
|
|
22
|
-
'use strict';
|
|
23
|
-
|
|
24
|
-
// ==========================================================================
|
|
25
|
-
// Templates
|
|
26
|
-
// ==========================================================================
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Create thread list container HTML
|
|
30
|
-
* REQ-d00099: Support read-only mode for archived packages
|
|
31
|
-
* @param {string} reqId - Requirement ID
|
|
32
|
-
* @returns {string} HTML
|
|
33
|
-
*/
|
|
34
|
-
function threadListTemplate(reqId) {
|
|
35
|
-
const isArchiveMode = review.packages && review.packages.isArchiveMode;
|
|
36
|
-
const addButtonHtml = isArchiveMode
|
|
37
|
-
? `<span class="rs-readonly-notice" title="Archived packages are read-only">Read Only</span>`
|
|
38
|
-
: `<button class="rs-btn rs-btn-primary rs-add-comment-btn" title="Add comment">+ Add Comment</button>`;
|
|
39
|
-
|
|
40
|
-
return `
|
|
41
|
-
<div class="rs-thread-list${isArchiveMode ? ' rs-archive-mode' : ''}" data-req-id="${reqId}">
|
|
42
|
-
<div class="rs-thread-list-header">
|
|
43
|
-
<h4>Comments</h4>
|
|
44
|
-
${addButtonHtml}
|
|
45
|
-
</div>
|
|
46
|
-
<div class="rs-thread-list-content">
|
|
47
|
-
<div class="rs-threads"></div>
|
|
48
|
-
<div class="rs-no-threads" style="display: none;">
|
|
49
|
-
No comments yet.
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Create thread HTML
|
|
58
|
-
* REQ-d00099-C: Hide action buttons in archive mode
|
|
59
|
-
* @param {Thread} thread - Thread object
|
|
60
|
-
* @returns {string} HTML
|
|
61
|
-
*/
|
|
62
|
-
function threadTemplate(thread) {
|
|
63
|
-
const resolvedClass = thread.resolved ? 'rs-thread-resolved' : '';
|
|
64
|
-
const resolvedBadge = thread.resolved ?
|
|
65
|
-
`<span class="rs-badge rs-badge-resolved">Resolved</span>` : '';
|
|
66
|
-
const confidenceClass = getConfidenceClass(thread);
|
|
67
|
-
const isArchiveMode = review.packages && review.packages.isArchiveMode;
|
|
68
|
-
|
|
69
|
-
// REQ-d00099-C: No action buttons in archive mode
|
|
70
|
-
const actionButtonsHtml = isArchiveMode ? '' : `
|
|
71
|
-
${thread.resolved ?
|
|
72
|
-
`<button class="rs-btn rs-btn-sm rs-unresolve-btn">Reopen</button>` :
|
|
73
|
-
`<button class="rs-btn rs-btn-sm rs-resolve-btn">Resolve</button>`
|
|
74
|
-
}
|
|
75
|
-
`;
|
|
76
|
-
|
|
77
|
-
// REQ-d00099-C: No reply form in archive mode
|
|
78
|
-
const replyHtml = isArchiveMode ? '' : `
|
|
79
|
-
<div class="rs-reply-form" style="display: none;">
|
|
80
|
-
<textarea class="rs-reply-input" placeholder="Write a reply..."></textarea>
|
|
81
|
-
<div class="rs-reply-actions">
|
|
82
|
-
<button class="rs-btn rs-btn-primary rs-submit-reply">Reply</button>
|
|
83
|
-
<button class="rs-btn rs-cancel-reply">Cancel</button>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
<button class="rs-btn rs-btn-link rs-show-reply-btn">Reply</button>
|
|
87
|
-
`;
|
|
88
|
-
|
|
89
|
-
return `
|
|
90
|
-
<div class="rs-thread ${resolvedClass}${isArchiveMode ? ' rs-archive-mode' : ''}" data-thread-id="${thread.threadId}">
|
|
91
|
-
<div class="rs-thread-header">
|
|
92
|
-
<div class="rs-thread-meta">
|
|
93
|
-
<span class="rs-position-label ${confidenceClass}"
|
|
94
|
-
data-thread-id="${thread.threadId}"
|
|
95
|
-
data-position-type="${thread.position?.type || 'general'}"
|
|
96
|
-
title="Click to highlight in REQ... click again to clear">
|
|
97
|
-
${getPositionIcon(thread)} ${getPositionLabel(thread)}
|
|
98
|
-
</span>
|
|
99
|
-
${resolvedBadge}
|
|
100
|
-
</div>
|
|
101
|
-
<div class="rs-thread-actions">
|
|
102
|
-
${actionButtonsHtml}
|
|
103
|
-
<button class="rs-btn rs-btn-sm rs-collapse-btn" title="Collapse">V</button>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
<div class="rs-thread-body">
|
|
107
|
-
<div class="rs-comments">
|
|
108
|
-
${thread.comments.map(c => commentTemplate(c)).join('')}
|
|
109
|
-
</div>
|
|
110
|
-
${replyHtml}
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Create comment HTML
|
|
118
|
-
* @param {Comment} comment - Comment object
|
|
119
|
-
* @returns {string} HTML
|
|
120
|
-
*/
|
|
121
|
-
function commentTemplate(comment) {
|
|
122
|
-
return `
|
|
123
|
-
<div class="rs-comment" data-comment-id="${comment.id}">
|
|
124
|
-
<div class="rs-comment-header">
|
|
125
|
-
<span class="rs-author">${escapeHtml(comment.author)}</span>
|
|
126
|
-
<span class="rs-time">${formatTime(comment.timestamp)}</span>
|
|
127
|
-
</div>
|
|
128
|
-
<div class="rs-comment-body">
|
|
129
|
-
${formatCommentBody(comment.body)}
|
|
130
|
-
</div>
|
|
131
|
-
</div>
|
|
132
|
-
`;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Create new comment form HTML
|
|
137
|
-
* @param {string} reqId - Requirement ID
|
|
138
|
-
* @returns {string} HTML
|
|
139
|
-
*/
|
|
140
|
-
function newCommentFormTemplate(reqId) {
|
|
141
|
-
return `
|
|
142
|
-
<div class="rs-new-comment-form" data-req-id="${reqId}">
|
|
143
|
-
<h4>New Comment</h4>
|
|
144
|
-
<div class="rs-form-group">
|
|
145
|
-
<label>Position</label>
|
|
146
|
-
<select class="rs-position-type">
|
|
147
|
-
<option value="general">General (whole requirement)</option>
|
|
148
|
-
<option value="line">Specific line</option>
|
|
149
|
-
<option value="block">Line range</option>
|
|
150
|
-
<option value="word">Word/phrase</option>
|
|
151
|
-
</select>
|
|
152
|
-
</div>
|
|
153
|
-
<div class="rs-position-options" style="display: none;">
|
|
154
|
-
<div class="rs-line-options" style="display: none;">
|
|
155
|
-
<label>Line number</label>
|
|
156
|
-
<input type="number" class="rs-line-input" min="1" value="1">
|
|
157
|
-
</div>
|
|
158
|
-
<div class="rs-block-options" style="display: none;">
|
|
159
|
-
<label>Line range</label>
|
|
160
|
-
<input type="number" class="rs-block-start" min="1" value="1">
|
|
161
|
-
<span>to</span>
|
|
162
|
-
<input type="number" class="rs-block-end" min="1" value="1">
|
|
163
|
-
</div>
|
|
164
|
-
<div class="rs-word-options" style="display: none;">
|
|
165
|
-
<label>Word/phrase</label>
|
|
166
|
-
<input type="text" class="rs-keyword" placeholder="Enter word or phrase">
|
|
167
|
-
<label>Occurrence</label>
|
|
168
|
-
<input type="number" class="rs-keyword-occurrence" min="1" value="1">
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
<div class="rs-form-group">
|
|
172
|
-
<label>Comment</label>
|
|
173
|
-
<textarea class="rs-comment-body-input"
|
|
174
|
-
placeholder="Write your comment..." rows="4"></textarea>
|
|
175
|
-
</div>
|
|
176
|
-
<div class="rs-form-actions">
|
|
177
|
-
<button class="rs-btn rs-btn-primary rs-submit-comment">Add Comment</button>
|
|
178
|
-
<button class="rs-btn rs-cancel-comment">Cancel</button>
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
`;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ==========================================================================
|
|
185
|
-
// Helper Functions
|
|
186
|
-
// ==========================================================================
|
|
187
|
-
|
|
188
|
-
function escapeHtml(text) {
|
|
189
|
-
const div = document.createElement('div');
|
|
190
|
-
div.textContent = text;
|
|
191
|
-
return div.innerHTML;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function formatTime(isoString) {
|
|
195
|
-
try {
|
|
196
|
-
const date = new Date(isoString);
|
|
197
|
-
const now = new Date();
|
|
198
|
-
const diff = now - date;
|
|
199
|
-
|
|
200
|
-
if (diff < 60000) return 'just now';
|
|
201
|
-
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
|
|
202
|
-
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
|
|
203
|
-
if (diff < 604800000) return Math.floor(diff / 86400000) + 'd ago';
|
|
204
|
-
|
|
205
|
-
return date.toLocaleDateString();
|
|
206
|
-
} catch (e) {
|
|
207
|
-
return isoString;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function formatCommentBody(body) {
|
|
212
|
-
// Simple markdown-like formatting
|
|
213
|
-
let html = escapeHtml(body);
|
|
214
|
-
// Bold
|
|
215
|
-
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
216
|
-
// Italic
|
|
217
|
-
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
218
|
-
// Code
|
|
219
|
-
html = html.replace(/`(.+?)`/g, '<code>$1</code>');
|
|
220
|
-
// Line breaks
|
|
221
|
-
html = html.replace(/\n/g, '<br>');
|
|
222
|
-
return html;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Get confidence class for position label styling
|
|
227
|
-
* REQ-d00092: Click-to-Highlight Positions
|
|
228
|
-
* REQ-d00087: Position Resolution with Fallback
|
|
229
|
-
* @param {Thread} thread - Thread object
|
|
230
|
-
* @returns {string} CSS class name
|
|
231
|
-
*/
|
|
232
|
-
function getConfidenceClass(thread) {
|
|
233
|
-
const resolvedPosition = thread.resolvedPosition;
|
|
234
|
-
if (!resolvedPosition || thread.position?.type === 'general') {
|
|
235
|
-
return 'rs-confidence-unanchored';
|
|
236
|
-
}
|
|
237
|
-
const confidence = resolvedPosition.confidence;
|
|
238
|
-
if (confidence === 'EXACT') return 'rs-confidence-exact';
|
|
239
|
-
if (confidence === 'APPROXIMATE') return 'rs-confidence-approximate';
|
|
240
|
-
return 'rs-confidence-unanchored';
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Get highlight class for line highlighting based on confidence
|
|
245
|
-
* REQ-d00092: Click-to-Highlight Positions
|
|
246
|
-
* REQ-d00087: Position Resolution with Fallback
|
|
247
|
-
* @param {Thread} thread - Thread object
|
|
248
|
-
* @returns {string} CSS class name for highlighting
|
|
249
|
-
*/
|
|
250
|
-
function getHighlightClassForThread(thread) {
|
|
251
|
-
const resolvedPosition = thread.resolvedPosition;
|
|
252
|
-
if (!resolvedPosition || thread.position?.type === 'general') {
|
|
253
|
-
return 'rs-highlight-unanchored';
|
|
254
|
-
}
|
|
255
|
-
const confidence = resolvedPosition.confidence;
|
|
256
|
-
if (confidence === 'EXACT') return 'rs-highlight-exact';
|
|
257
|
-
if (confidence === 'APPROXIMATE') return 'rs-highlight-approximate';
|
|
258
|
-
return 'rs-highlight-unanchored';
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function getPositionIcon(thread) {
|
|
262
|
-
switch (thread.position.type) {
|
|
263
|
-
case review.PositionType.LINE: return '[L]';
|
|
264
|
-
case review.PositionType.BLOCK: return '[B]';
|
|
265
|
-
case review.PositionType.WORD: return '[W]';
|
|
266
|
-
default: return '[G]';
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function getPositionTooltip(thread) {
|
|
271
|
-
const pos = thread.position;
|
|
272
|
-
switch (pos.type) {
|
|
273
|
-
case review.PositionType.LINE:
|
|
274
|
-
return `Line ${pos.lineNumber}`;
|
|
275
|
-
case review.PositionType.BLOCK:
|
|
276
|
-
return `Lines ${pos.lineRange[0]}-${pos.lineRange[1]}`;
|
|
277
|
-
case review.PositionType.WORD:
|
|
278
|
-
return `"${pos.keyword}" (occurrence ${pos.keywordOccurrence || 1})`;
|
|
279
|
-
default:
|
|
280
|
-
return 'General comment';
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function getPositionLabel(thread) {
|
|
285
|
-
const pos = thread.position;
|
|
286
|
-
switch (pos.type) {
|
|
287
|
-
case review.PositionType.LINE:
|
|
288
|
-
return `Line ${pos.lineNumber}`;
|
|
289
|
-
case review.PositionType.BLOCK:
|
|
290
|
-
return `Lines ${pos.lineRange[0]}-${pos.lineRange[1]}`;
|
|
291
|
-
case review.PositionType.WORD:
|
|
292
|
-
return `"${escapeHtml(pos.keyword)}"`;
|
|
293
|
-
default:
|
|
294
|
-
return 'General';
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// ==========================================================================
|
|
299
|
-
// UI Components
|
|
300
|
-
// ==========================================================================
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Render thread list for a requirement
|
|
304
|
-
* @param {Element} container - Container element
|
|
305
|
-
* @param {string} reqId - Requirement ID
|
|
306
|
-
*/
|
|
307
|
-
function renderThreadList(container, reqId) {
|
|
308
|
-
container.innerHTML = threadListTemplate(reqId);
|
|
309
|
-
|
|
310
|
-
const threads = review.state.getThreads(reqId);
|
|
311
|
-
const threadsContainer = container.querySelector('.rs-threads');
|
|
312
|
-
const noThreads = container.querySelector('.rs-no-threads');
|
|
313
|
-
|
|
314
|
-
if (threads.length === 0) {
|
|
315
|
-
noThreads.style.display = 'block';
|
|
316
|
-
} else {
|
|
317
|
-
threads.forEach(thread => {
|
|
318
|
-
threadsContainer.insertAdjacentHTML('beforeend', threadTemplate(thread));
|
|
319
|
-
});
|
|
320
|
-
bindThreadEvents(container);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Bind add comment button
|
|
324
|
-
const addBtn = container.querySelector('.rs-add-comment-btn');
|
|
325
|
-
if (addBtn) {
|
|
326
|
-
addBtn.addEventListener('click', (event) => {
|
|
327
|
-
// Stop propagation to prevent any parent handlers from clearing selection
|
|
328
|
-
event.stopPropagation();
|
|
329
|
-
showNewCommentForm(container, reqId);
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
review.renderThreadList = renderThreadList;
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Show new comment form
|
|
337
|
-
* REQ-d00099: Block in archive mode
|
|
338
|
-
* @param {Element} container - Container element (optional - uses current selection if not provided)
|
|
339
|
-
* @param {string} reqId - Requirement ID (optional - uses current selection if not provided)
|
|
340
|
-
*/
|
|
341
|
-
function showNewCommentForm(container, reqId) {
|
|
342
|
-
// REQ-d00099-C: Block comment creation in archive mode
|
|
343
|
-
if (review.packages && review.packages.isArchiveMode) {
|
|
344
|
-
alert('This package is archived and read-only.\n\nComments cannot be added to archived packages.');
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// IMPORTANT: Capture line selection state FIRST, before any DOM manipulation
|
|
349
|
-
// This prevents loss of selection due to focus changes or event handling
|
|
350
|
-
const lineSelection = review.getLineSelection ? review.getLineSelection() : {
|
|
351
|
-
type: window.selectedLineRange ? 'block' : (window.selectedLineNumber ? 'line' : null),
|
|
352
|
-
lineNumber: window.selectedLineNumber,
|
|
353
|
-
lineRange: window.selectedLineRange
|
|
354
|
-
};
|
|
355
|
-
// Make a copy of the values in case they get cleared
|
|
356
|
-
const capturedSelection = {
|
|
357
|
-
type: lineSelection.type,
|
|
358
|
-
lineNumber: lineSelection.lineNumber,
|
|
359
|
-
lineRange: lineSelection.lineRange ? { ...lineSelection.lineRange } : null
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
// If called without arguments, get from current review state
|
|
363
|
-
if (!reqId) {
|
|
364
|
-
reqId = review.selectedReqId;
|
|
365
|
-
if (!reqId) {
|
|
366
|
-
console.warn('showNewCommentForm: No REQ selected');
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
if (!container) {
|
|
371
|
-
// Find the comments section for the current REQ
|
|
372
|
-
container = document.querySelector('.rs-comments-section[data-req-id="' + reqId + '"]') ||
|
|
373
|
-
document.querySelector('.rs-thread-list[data-req-id="' + reqId + '"]') ||
|
|
374
|
-
document.getElementById('rs-comments-content');
|
|
375
|
-
if (!container) {
|
|
376
|
-
console.warn('showNewCommentForm: No container found');
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Check if form already exists
|
|
382
|
-
let form = container.querySelector('.rs-new-comment-form');
|
|
383
|
-
if (form) {
|
|
384
|
-
form.remove();
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
container.insertAdjacentHTML('afterbegin', newCommentFormTemplate(reqId));
|
|
388
|
-
form = container.querySelector('.rs-new-comment-form');
|
|
389
|
-
|
|
390
|
-
// Position type change handler
|
|
391
|
-
const posType = form.querySelector('.rs-position-type');
|
|
392
|
-
const posOptions = form.querySelector('.rs-position-options');
|
|
393
|
-
const lineOpts = form.querySelector('.rs-line-options');
|
|
394
|
-
const blockOpts = form.querySelector('.rs-block-options');
|
|
395
|
-
const wordOpts = form.querySelector('.rs-word-options');
|
|
396
|
-
|
|
397
|
-
posType.addEventListener('change', () => {
|
|
398
|
-
const val = posType.value;
|
|
399
|
-
posOptions.style.display = val === 'general' ? 'none' : 'block';
|
|
400
|
-
lineOpts.style.display = val === 'line' ? 'block' : 'none';
|
|
401
|
-
blockOpts.style.display = val === 'block' ? 'block' : 'none';
|
|
402
|
-
wordOpts.style.display = val === 'word' ? 'block' : 'none';
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
// Use the captured selection (captured at function start before any DOM changes)
|
|
406
|
-
if (capturedSelection.lineRange && capturedSelection.lineRange.start && capturedSelection.lineRange.end) {
|
|
407
|
-
// Range selection - lineRange is {start, end} object
|
|
408
|
-
posType.value = 'block';
|
|
409
|
-
posType.dispatchEvent(new Event('change'));
|
|
410
|
-
const startInput = form.querySelector('.rs-block-start');
|
|
411
|
-
const endInput = form.querySelector('.rs-block-end');
|
|
412
|
-
if (startInput) startInput.value = capturedSelection.lineRange.start;
|
|
413
|
-
if (endInput) endInput.value = capturedSelection.lineRange.end;
|
|
414
|
-
} else if (capturedSelection.lineNumber) {
|
|
415
|
-
// Single line selection
|
|
416
|
-
posType.value = 'line';
|
|
417
|
-
posType.dispatchEvent(new Event('change'));
|
|
418
|
-
const lineInput = form.querySelector('.rs-line-input');
|
|
419
|
-
if (lineInput) lineInput.value = capturedSelection.lineNumber;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Submit handler
|
|
423
|
-
form.querySelector('.rs-submit-comment').addEventListener('click', () => {
|
|
424
|
-
submitNewComment(form, reqId);
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
// Cancel handler
|
|
428
|
-
form.querySelector('.rs-cancel-comment').addEventListener('click', () => {
|
|
429
|
-
form.remove();
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
// Focus textarea
|
|
433
|
-
form.querySelector('.rs-comment-body-input').focus();
|
|
434
|
-
}
|
|
435
|
-
review.showNewCommentForm = showNewCommentForm;
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Submit new comment
|
|
439
|
-
* REQ-d00094: Threads must be owned by a package
|
|
440
|
-
* @param {Element} form - Form element
|
|
441
|
-
* @param {string} reqId - Requirement ID
|
|
442
|
-
*/
|
|
443
|
-
function submitNewComment(form, reqId) {
|
|
444
|
-
// REQ-d00095-B: Require explicit package selection
|
|
445
|
-
const activePackageId = review.packages && review.packages.activeId;
|
|
446
|
-
if (!activePackageId) {
|
|
447
|
-
alert('Please select a package first.\n\nThreads must be owned by a package.');
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// REQ-d00095-B: Verify the active package actually exists
|
|
452
|
-
const activePackage = review.packages.items.find(p => p.packageId === activePackageId);
|
|
453
|
-
if (!activePackage) {
|
|
454
|
-
console.error('Active package not found in packages list:', activePackageId);
|
|
455
|
-
alert('The selected package no longer exists.\n\nPlease select a different package.');
|
|
456
|
-
// Clear the stale activePackageId
|
|
457
|
-
review.packages.activeId = null;
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const body = form.querySelector('.rs-comment-body-input').value.trim();
|
|
462
|
-
if (!body) {
|
|
463
|
-
alert('Please enter a comment');
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const user = review.state.currentUser || 'anonymous';
|
|
468
|
-
const posType = form.querySelector('.rs-position-type').value;
|
|
469
|
-
|
|
470
|
-
// Get current REQ hash (would come from embedded data)
|
|
471
|
-
const hash = window.REQ_CONTENT_DATA?.[reqId]?.hash || '00000000';
|
|
472
|
-
|
|
473
|
-
// Create position based on type
|
|
474
|
-
let position;
|
|
475
|
-
switch (posType) {
|
|
476
|
-
case 'line': {
|
|
477
|
-
const lineNum = parseInt(form.querySelector('.rs-line-input').value, 10);
|
|
478
|
-
position = review.CommentPosition.createLine(hash, lineNum);
|
|
479
|
-
break;
|
|
480
|
-
}
|
|
481
|
-
case 'block': {
|
|
482
|
-
const start = parseInt(form.querySelector('.rs-block-start').value, 10);
|
|
483
|
-
const end = parseInt(form.querySelector('.rs-block-end').value, 10);
|
|
484
|
-
position = review.CommentPosition.createBlock(hash, start, end);
|
|
485
|
-
break;
|
|
486
|
-
}
|
|
487
|
-
case 'word': {
|
|
488
|
-
const keyword = form.querySelector('.rs-keyword').value.trim();
|
|
489
|
-
const occurrence = parseInt(form.querySelector('.rs-keyword-occurrence').value, 10);
|
|
490
|
-
if (!keyword) {
|
|
491
|
-
alert('Please enter a word or phrase');
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
position = review.CommentPosition.createWord(hash, keyword, occurrence);
|
|
495
|
-
break;
|
|
496
|
-
}
|
|
497
|
-
default:
|
|
498
|
-
position = review.CommentPosition.createGeneral(hash);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// REQ-d00094-A: Create thread with packageId
|
|
502
|
-
const thread = review.Thread.create(reqId, user, position, body, activePackageId);
|
|
503
|
-
review.state.addThread(thread);
|
|
504
|
-
|
|
505
|
-
// Auto-change status to Review if currently Draft
|
|
506
|
-
const reqData = window.REQ_CONTENT_DATA && window.REQ_CONTENT_DATA[reqId];
|
|
507
|
-
if (reqData && reqData.status === 'Draft' && typeof review.toggleToReview === 'function') {
|
|
508
|
-
review.toggleToReview(reqId).then(result => {
|
|
509
|
-
if (result.success) {
|
|
510
|
-
console.log(`Auto-changed REQ-${reqId} status to Review`);
|
|
511
|
-
}
|
|
512
|
-
}).catch(err => {
|
|
513
|
-
console.warn('Failed to auto-change status:', err);
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Trigger change event
|
|
518
|
-
document.dispatchEvent(new CustomEvent('traceview:thread-created', {
|
|
519
|
-
detail: { thread, reqId }
|
|
520
|
-
}));
|
|
521
|
-
|
|
522
|
-
// Re-render the thread list
|
|
523
|
-
// The form is inside #review-panel-content, find the thread list's parent container
|
|
524
|
-
const threadList = form.closest('.rs-thread-list') ||
|
|
525
|
-
form.parentElement?.querySelector('.rs-thread-list');
|
|
526
|
-
const reviewPanelContent = document.getElementById('review-panel-content');
|
|
527
|
-
|
|
528
|
-
if (threadList && threadList.parentElement) {
|
|
529
|
-
renderThreadList(threadList.parentElement, reqId);
|
|
530
|
-
} else if (reviewPanelContent) {
|
|
531
|
-
// Form is directly in review-panel-content, re-render there
|
|
532
|
-
renderThreadList(reviewPanelContent, reqId);
|
|
533
|
-
} else {
|
|
534
|
-
form.remove();
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Bind event handlers to thread elements
|
|
540
|
-
* @param {Element} container - Container element
|
|
541
|
-
*/
|
|
542
|
-
function bindThreadEvents(container) {
|
|
543
|
-
// Collapse/expand buttons
|
|
544
|
-
container.querySelectorAll('.rs-collapse-btn').forEach(btn => {
|
|
545
|
-
btn.addEventListener('click', () => {
|
|
546
|
-
const thread = btn.closest('.rs-thread');
|
|
547
|
-
const body = thread.querySelector('.rs-thread-body');
|
|
548
|
-
const isCollapsed = body.style.display === 'none';
|
|
549
|
-
body.style.display = isCollapsed ? 'block' : 'none';
|
|
550
|
-
btn.textContent = isCollapsed ? 'V' : '>';
|
|
551
|
-
});
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
// Resolve buttons
|
|
555
|
-
container.querySelectorAll('.rs-resolve-btn').forEach(btn => {
|
|
556
|
-
btn.addEventListener('click', () => {
|
|
557
|
-
const threadEl = btn.closest('.rs-thread');
|
|
558
|
-
const threadId = threadEl.getAttribute('data-thread-id');
|
|
559
|
-
resolveThread(threadId, container);
|
|
560
|
-
});
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
// Unresolve buttons
|
|
564
|
-
container.querySelectorAll('.rs-unresolve-btn').forEach(btn => {
|
|
565
|
-
btn.addEventListener('click', () => {
|
|
566
|
-
const threadEl = btn.closest('.rs-thread');
|
|
567
|
-
const threadId = threadEl.getAttribute('data-thread-id');
|
|
568
|
-
unresolveThread(threadId, container);
|
|
569
|
-
});
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
// Reply buttons
|
|
573
|
-
container.querySelectorAll('.rs-show-reply-btn').forEach(btn => {
|
|
574
|
-
btn.addEventListener('click', () => {
|
|
575
|
-
const thread = btn.closest('.rs-thread');
|
|
576
|
-
const replyForm = thread.querySelector('.rs-reply-form');
|
|
577
|
-
replyForm.style.display = 'block';
|
|
578
|
-
btn.style.display = 'none';
|
|
579
|
-
replyForm.querySelector('.rs-reply-input').focus();
|
|
580
|
-
});
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
// Cancel reply
|
|
584
|
-
container.querySelectorAll('.rs-cancel-reply').forEach(btn => {
|
|
585
|
-
btn.addEventListener('click', () => {
|
|
586
|
-
const thread = btn.closest('.rs-thread');
|
|
587
|
-
const replyForm = thread.querySelector('.rs-reply-form');
|
|
588
|
-
const showBtn = thread.querySelector('.rs-show-reply-btn');
|
|
589
|
-
replyForm.style.display = 'none';
|
|
590
|
-
replyForm.querySelector('.rs-reply-input').value = '';
|
|
591
|
-
showBtn.style.display = 'inline-block';
|
|
592
|
-
});
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
// Submit reply
|
|
596
|
-
container.querySelectorAll('.rs-submit-reply').forEach(btn => {
|
|
597
|
-
btn.addEventListener('click', () => {
|
|
598
|
-
const threadEl = btn.closest('.rs-thread');
|
|
599
|
-
submitReply(threadEl, container);
|
|
600
|
-
});
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
// REQ-d00092: Position label click handler with toggle behavior
|
|
604
|
-
container.querySelectorAll('.rs-position-label').forEach(positionLabel => {
|
|
605
|
-
positionLabel.addEventListener('click', (e) => {
|
|
606
|
-
e.stopPropagation();
|
|
607
|
-
|
|
608
|
-
const threadId = positionLabel.getAttribute('data-thread-id');
|
|
609
|
-
const isActive = positionLabel.classList.contains('rs-position-active');
|
|
610
|
-
|
|
611
|
-
// Clear all other active position labels
|
|
612
|
-
document.querySelectorAll('.rs-position-label.rs-position-active').forEach(el => {
|
|
613
|
-
el.classList.remove('rs-position-active');
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
if (isActive) {
|
|
617
|
-
// Toggle off - clear highlights
|
|
618
|
-
clearAllPositionHighlights();
|
|
619
|
-
const reqCard = document.querySelector(`[data-req-id]`);
|
|
620
|
-
if (reqCard) {
|
|
621
|
-
const lineContainer = reqCard.querySelector('.rs-lines-table');
|
|
622
|
-
clearCommentHighlights(lineContainer);
|
|
623
|
-
}
|
|
624
|
-
} else {
|
|
625
|
-
// Toggle on - highlight and mark active
|
|
626
|
-
positionLabel.classList.add('rs-position-active');
|
|
627
|
-
highlightThreadPositionInCard(threadId, container);
|
|
628
|
-
}
|
|
629
|
-
});
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
// Hover to highlight position
|
|
633
|
-
container.querySelectorAll('.rs-thread').forEach(threadEl => {
|
|
634
|
-
threadEl.addEventListener('mouseenter', () => {
|
|
635
|
-
const threadId = threadEl.getAttribute('data-thread-id');
|
|
636
|
-
review.activateHighlight(threadId);
|
|
637
|
-
});
|
|
638
|
-
threadEl.addEventListener('mouseleave', () => {
|
|
639
|
-
review.activateHighlight(null);
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
// Click to highlight position in REQ card
|
|
643
|
-
threadEl.addEventListener('click', (e) => {
|
|
644
|
-
// Don't trigger if clicking on buttons or reply form
|
|
645
|
-
if (e.target.closest('button') || e.target.closest('.rs-reply-form') ||
|
|
646
|
-
e.target.closest('textarea') || e.target.closest('input')) {
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
const threadId = threadEl.getAttribute('data-thread-id');
|
|
650
|
-
highlightThreadPositionInCard(threadId, container);
|
|
651
|
-
});
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* Highlight the position referenced by a thread in the REQ card
|
|
657
|
-
* REQ-d00092: Click-to-Highlight Positions
|
|
658
|
-
* @param {string} threadId - Thread ID
|
|
659
|
-
* @param {Element} container - Container element
|
|
660
|
-
*/
|
|
661
|
-
function highlightThreadPositionInCard(threadId, container) {
|
|
662
|
-
// Get the reqId and find the thread
|
|
663
|
-
const reqId = container.querySelector('[data-req-id]')?.getAttribute('data-req-id') ||
|
|
664
|
-
container.closest('[data-req-id]')?.getAttribute('data-req-id') ||
|
|
665
|
-
container.getAttribute('data-req-id') ||
|
|
666
|
-
(typeof currentReviewReqId !== 'undefined' ? currentReviewReqId : null);
|
|
667
|
-
|
|
668
|
-
if (!reqId) return;
|
|
669
|
-
|
|
670
|
-
const threads = review.state.getThreads(reqId);
|
|
671
|
-
const thread = threads.find(t => t.threadId === threadId);
|
|
672
|
-
if (!thread || !thread.position) return;
|
|
673
|
-
|
|
674
|
-
const position = thread.position;
|
|
675
|
-
|
|
676
|
-
// REQ-d00092: Get confidence-based highlight class
|
|
677
|
-
const highlightClass = getHighlightClassForThread(thread);
|
|
678
|
-
|
|
679
|
-
// Find the REQ card's line-numbered view
|
|
680
|
-
const reqCard = document.getElementById(`req-card-${reqId}`);
|
|
681
|
-
if (!reqCard) return;
|
|
682
|
-
|
|
683
|
-
const lineContainer = reqCard.querySelector('.rs-lines-table');
|
|
684
|
-
|
|
685
|
-
// Clear any existing highlights
|
|
686
|
-
clearAllPositionHighlights();
|
|
687
|
-
if (lineContainer) {
|
|
688
|
-
clearCommentHighlights(lineContainer);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// REQ-d00092: For GENERAL position type, highlight the whole card
|
|
692
|
-
if (position.type === 'general' || position.type === review.PositionType?.GENERAL) {
|
|
693
|
-
reqCard.classList.add('rs-card-highlight');
|
|
694
|
-
reqCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
if (!lineContainer) return;
|
|
699
|
-
|
|
700
|
-
// Highlight based on position type
|
|
701
|
-
let linesToHighlight = [];
|
|
702
|
-
|
|
703
|
-
if (position.type === review.PositionType.LINE && position.lineNumber) {
|
|
704
|
-
linesToHighlight = [position.lineNumber];
|
|
705
|
-
} else if (position.type === review.PositionType.BLOCK && position.lineRange) {
|
|
706
|
-
const [start, end] = position.lineRange;
|
|
707
|
-
for (let i = start; i <= end; i++) {
|
|
708
|
-
linesToHighlight.push(i);
|
|
709
|
-
}
|
|
710
|
-
} else if (position.type === review.PositionType.WORD && position.keyword) {
|
|
711
|
-
// For word positions, try to find the line containing the keyword
|
|
712
|
-
const reqData = window.REQ_CONTENT_DATA && window.REQ_CONTENT_DATA[reqId];
|
|
713
|
-
if (reqData && reqData.body) {
|
|
714
|
-
const foundLine = review.findKeywordOccurrence(
|
|
715
|
-
reqData.body,
|
|
716
|
-
position.keyword,
|
|
717
|
-
position.keywordOccurrence || 1
|
|
718
|
-
);
|
|
719
|
-
if (foundLine) {
|
|
720
|
-
linesToHighlight = [foundLine.line];
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Apply highlights and scroll to first highlighted line
|
|
726
|
-
if (linesToHighlight.length > 0) {
|
|
727
|
-
let firstRow = null;
|
|
728
|
-
linesToHighlight.forEach(lineNum => {
|
|
729
|
-
const lineRow = lineContainer.querySelector(`.rs-line-row[data-line="${lineNum}"]`);
|
|
730
|
-
if (lineRow) {
|
|
731
|
-
lineRow.classList.add('rs-comment-highlight');
|
|
732
|
-
lineRow.classList.add(highlightClass); // REQ-d00092: Add confidence class
|
|
733
|
-
lineRow.setAttribute('data-highlight-thread', threadId); // REQ-d00092: Track thread
|
|
734
|
-
if (!firstRow) firstRow = lineRow;
|
|
735
|
-
}
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
// Scroll the first highlighted line into view
|
|
739
|
-
if (firstRow) {
|
|
740
|
-
firstRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
review.highlightThreadPositionInCard = highlightThreadPositionInCard;
|
|
745
|
-
|
|
746
|
-
/**
|
|
747
|
-
* Clear all position highlights (card-level highlights for GENERAL position)
|
|
748
|
-
* REQ-d00092: Click-to-Highlight Positions
|
|
749
|
-
* REQ-d00087: Position Resolution with Fallback
|
|
750
|
-
*/
|
|
751
|
-
function clearAllPositionHighlights() {
|
|
752
|
-
// Clear card-level highlights (for GENERAL position)
|
|
753
|
-
document.querySelectorAll('.rs-card-highlight').forEach(el => {
|
|
754
|
-
el.classList.remove('rs-card-highlight');
|
|
755
|
-
});
|
|
756
|
-
// REQ-d00092: Clear highlight classes from req cards
|
|
757
|
-
document.querySelectorAll('[data-req-id]').forEach(reqCard => {
|
|
758
|
-
reqCard.classList.remove('rs-highlight-unanchored', 'rs-card-highlight');
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
review.clearAllPositionHighlights = clearAllPositionHighlights;
|
|
762
|
-
|
|
763
|
-
/**
|
|
764
|
-
* Clear comment highlights from line container
|
|
765
|
-
* REQ-d00092: Enhanced to remove all highlight-related classes and attributes
|
|
766
|
-
* @param {Element} lineContainer - The lines table element
|
|
767
|
-
*/
|
|
768
|
-
function clearCommentHighlights(lineContainer) {
|
|
769
|
-
if (!lineContainer) return;
|
|
770
|
-
// Remove all highlight-related classes and data attributes
|
|
771
|
-
lineContainer.querySelectorAll('.rs-comment-highlight, .rs-highlight-exact, .rs-highlight-approximate, .rs-highlight-unanchored, .rs-highlight-active').forEach(el => {
|
|
772
|
-
el.classList.remove('rs-comment-highlight', 'rs-highlight-exact', 'rs-highlight-approximate', 'rs-highlight-unanchored', 'rs-highlight-active');
|
|
773
|
-
el.removeAttribute('data-highlight-thread');
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
review.clearCommentHighlights = clearCommentHighlights;
|
|
777
|
-
|
|
778
|
-
/**
|
|
779
|
-
* Submit reply to a thread
|
|
780
|
-
* @param {Element} threadEl - Thread element
|
|
781
|
-
* @param {Element} container - Container element
|
|
782
|
-
*/
|
|
783
|
-
function submitReply(threadEl, container) {
|
|
784
|
-
const threadId = threadEl.getAttribute('data-thread-id');
|
|
785
|
-
const replyInput = threadEl.querySelector('.rs-reply-input');
|
|
786
|
-
const body = replyInput.value.trim();
|
|
787
|
-
|
|
788
|
-
if (!body) {
|
|
789
|
-
alert('Please enter a reply');
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
const user = review.state.currentUser || 'anonymous';
|
|
794
|
-
// Look for data-req-id in the container or its children (thread-list element)
|
|
795
|
-
const reqId = container.querySelector('[data-req-id]')?.getAttribute('data-req-id') ||
|
|
796
|
-
container.closest('[data-req-id]')?.getAttribute('data-req-id') ||
|
|
797
|
-
container.getAttribute('data-req-id');
|
|
798
|
-
|
|
799
|
-
// Find thread in state
|
|
800
|
-
if (reqId) {
|
|
801
|
-
const threads = review.state.getThreads(reqId);
|
|
802
|
-
const thread = threads.find(t => t.threadId === threadId);
|
|
803
|
-
if (thread) {
|
|
804
|
-
thread.addComment(user, body);
|
|
805
|
-
|
|
806
|
-
// Trigger change event
|
|
807
|
-
document.dispatchEvent(new CustomEvent('traceview:comment-added', {
|
|
808
|
-
detail: { thread, reqId, body }
|
|
809
|
-
}));
|
|
810
|
-
|
|
811
|
-
// Re-render - find the proper container
|
|
812
|
-
const threadListEl = container.querySelector('.rs-thread-list') || container;
|
|
813
|
-
const renderTarget = threadListEl.parentElement || container;
|
|
814
|
-
renderThreadList(renderTarget, reqId);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
/**
|
|
820
|
-
* Resolve a thread
|
|
821
|
-
* @param {string} threadId - Thread ID
|
|
822
|
-
* @param {Element} container - Container element
|
|
823
|
-
*/
|
|
824
|
-
function resolveThread(threadId, container) {
|
|
825
|
-
const reqId = container.querySelector('[data-req-id]')?.getAttribute('data-req-id') ||
|
|
826
|
-
container.closest('[data-req-id]')?.getAttribute('data-req-id') ||
|
|
827
|
-
container.getAttribute('data-req-id');
|
|
828
|
-
const user = review.state.currentUser || 'anonymous';
|
|
829
|
-
|
|
830
|
-
if (reqId) {
|
|
831
|
-
const threads = review.state.getThreads(reqId);
|
|
832
|
-
const thread = threads.find(t => t.threadId === threadId);
|
|
833
|
-
if (thread) {
|
|
834
|
-
thread.resolve(user);
|
|
835
|
-
|
|
836
|
-
// Trigger event
|
|
837
|
-
document.dispatchEvent(new CustomEvent('traceview:thread-resolved', {
|
|
838
|
-
detail: { thread, reqId, user }
|
|
839
|
-
}));
|
|
840
|
-
|
|
841
|
-
// Re-render - find the proper container
|
|
842
|
-
const threadListEl = container.querySelector('.rs-thread-list') || container;
|
|
843
|
-
const renderTarget = threadListEl.parentElement || container;
|
|
844
|
-
renderThreadList(renderTarget, reqId);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
/**
|
|
850
|
-
* Unresolve a thread
|
|
851
|
-
* @param {string} threadId - Thread ID
|
|
852
|
-
* @param {Element} container - Container element
|
|
853
|
-
*/
|
|
854
|
-
function unresolveThread(threadId, container) {
|
|
855
|
-
const reqId = container.querySelector('[data-req-id]')?.getAttribute('data-req-id') ||
|
|
856
|
-
container.closest('[data-req-id]')?.getAttribute('data-req-id') ||
|
|
857
|
-
container.getAttribute('data-req-id');
|
|
858
|
-
|
|
859
|
-
if (reqId) {
|
|
860
|
-
const threads = review.state.getThreads(reqId);
|
|
861
|
-
const thread = threads.find(t => t.threadId === threadId);
|
|
862
|
-
if (thread) {
|
|
863
|
-
thread.unresolve();
|
|
864
|
-
|
|
865
|
-
// Trigger event
|
|
866
|
-
document.dispatchEvent(new CustomEvent('traceview:thread-unresolved', {
|
|
867
|
-
detail: { thread, reqId }
|
|
868
|
-
}));
|
|
869
|
-
|
|
870
|
-
// Re-render - find the proper container
|
|
871
|
-
const threadListEl = container.querySelector('.rs-thread-list') || container;
|
|
872
|
-
const renderTarget = threadListEl.parentElement || container;
|
|
873
|
-
renderThreadList(renderTarget, reqId);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
/**
|
|
879
|
-
* Get comment count for a requirement
|
|
880
|
-
* @param {string} reqId - Requirement ID
|
|
881
|
-
* @returns {Object} {total, unresolved}
|
|
882
|
-
*/
|
|
883
|
-
function getCommentCount(reqId) {
|
|
884
|
-
const threads = review.state.getThreads(reqId);
|
|
885
|
-
return {
|
|
886
|
-
total: threads.length,
|
|
887
|
-
unresolved: threads.filter(t => !t.resolved).length
|
|
888
|
-
};
|
|
889
|
-
}
|
|
890
|
-
review.getCommentCount = getCommentCount;
|
|
891
|
-
|
|
892
|
-
// ==========================================================================
|
|
893
|
-
// Review Panel Integration
|
|
894
|
-
// ==========================================================================
|
|
895
|
-
|
|
896
|
-
/**
|
|
897
|
-
* Handle review panel ready event - add comments section
|
|
898
|
-
* @param {CustomEvent} event - Event with reqId and sectionsContainer
|
|
899
|
-
*/
|
|
900
|
-
function handleReviewPanelReady(event) {
|
|
901
|
-
const { reqId, sectionsContainer } = event.detail;
|
|
902
|
-
if (!sectionsContainer) return;
|
|
903
|
-
|
|
904
|
-
// Create comments section
|
|
905
|
-
const commentsSection = document.createElement('div');
|
|
906
|
-
commentsSection.className = 'rs-comments-section';
|
|
907
|
-
commentsSection.setAttribute('data-req-id', reqId);
|
|
908
|
-
sectionsContainer.appendChild(commentsSection);
|
|
909
|
-
|
|
910
|
-
// Render thread list
|
|
911
|
-
renderThreadList(commentsSection, reqId);
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
// Register event listener
|
|
915
|
-
document.addEventListener('traceview:review-panel-ready', handleReviewPanelReady);
|
|
916
|
-
|
|
917
|
-
// ==========================================================================
|
|
918
|
-
// RS Namespace Exports (REQ-d00092: Test Accessibility)
|
|
919
|
-
// ==========================================================================
|
|
920
|
-
// Export functions to RS namespace for test access
|
|
921
|
-
if (typeof window.RS === 'undefined') {
|
|
922
|
-
window.RS = {};
|
|
923
|
-
}
|
|
924
|
-
RS.highlightThreadPositionInCard = highlightThreadPositionInCard;
|
|
925
|
-
RS.clearAllPositionHighlights = clearAllPositionHighlights;
|
|
926
|
-
RS.clearCommentHighlights = clearCommentHighlights;
|
|
927
|
-
|
|
928
|
-
})(TraceView.review);
|