elspais 0.9.3__py3-none-any.whl → 0.11.1__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 +141 -10
- 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.1.dist-info}/METADATA +36 -18
- elspais-0.11.1.dist-info/RECORD +101 -0
- elspais-0.9.3.dist-info/RECORD +0 -40
- {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/WHEEL +0 -0
- {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/entry_points.txt +0 -0
- {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TraceView Review Status Request UI Module
|
|
3
|
+
*
|
|
4
|
+
* User interface for status change requests:
|
|
5
|
+
* - Status change request form
|
|
6
|
+
* - Approval workflow display
|
|
7
|
+
* - Pending request badges
|
|
8
|
+
*
|
|
9
|
+
* IMPLEMENTS REQUIREMENTS:
|
|
10
|
+
* REQ-tv-d00016: Review JavaScript Integration
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Ensure TraceView.review namespace exists
|
|
14
|
+
window.TraceView = window.TraceView || {};
|
|
15
|
+
TraceView.review = TraceView.review || {};
|
|
16
|
+
|
|
17
|
+
(function(review) {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
// ==========================================================================
|
|
21
|
+
// Templates
|
|
22
|
+
// ==========================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if a REQ is in the active package
|
|
26
|
+
* @param {string} reqId - Requirement ID
|
|
27
|
+
* @returns {boolean} True if REQ is in active package
|
|
28
|
+
*/
|
|
29
|
+
function isReqInActivePackage(reqId) {
|
|
30
|
+
const packages = review.packages;
|
|
31
|
+
if (!packages || !packages.activeId) return false;
|
|
32
|
+
const activePkg = packages.items.find(p => p.packageId === packages.activeId);
|
|
33
|
+
return activePkg && activePkg.reqIds && activePkg.reqIds.includes(reqId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create status request panel HTML
|
|
38
|
+
* @param {string} reqId - Requirement ID
|
|
39
|
+
* @param {string} currentStatus - Current status of the requirement
|
|
40
|
+
* @returns {string} HTML
|
|
41
|
+
*/
|
|
42
|
+
function statusPanelTemplate(reqId, currentStatus) {
|
|
43
|
+
// Show quick add button for Draft REQs - adds to review package
|
|
44
|
+
const quickToggle = currentStatus === 'Draft' ? `
|
|
45
|
+
<button class="rs-btn rs-btn-primary rs-quick-toggle" data-req-id="${reqId}">
|
|
46
|
+
Add to Review
|
|
47
|
+
</button>
|
|
48
|
+
` : '';
|
|
49
|
+
|
|
50
|
+
// Package membership button
|
|
51
|
+
const inPackage = isReqInActivePackage(reqId);
|
|
52
|
+
const packageBtn = `
|
|
53
|
+
<button class="rs-btn ${inPackage ? 'rs-btn-danger' : 'rs-btn-secondary'} rs-package-toggle" data-req-id="${reqId}">
|
|
54
|
+
${inPackage ? '− Remove from Package' : '+ Add to Package'}
|
|
55
|
+
</button>
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
return `
|
|
59
|
+
<div class="rs-status-panel" data-req-id="${reqId}">
|
|
60
|
+
<div class="rs-status-header">
|
|
61
|
+
<h4>Status</h4>
|
|
62
|
+
<span class="rs-current-status status-badge status-${currentStatus.toLowerCase()}">
|
|
63
|
+
${escapeHtml(currentStatus)}
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="rs-quick-actions">
|
|
67
|
+
${quickToggle}
|
|
68
|
+
${packageBtn}
|
|
69
|
+
</div>
|
|
70
|
+
<div class="rs-status-content">
|
|
71
|
+
<div class="rs-requests"></div>
|
|
72
|
+
<div class="rs-no-requests" style="display: none;">
|
|
73
|
+
No pending status change requests.
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="rs-status-actions">
|
|
77
|
+
<button class="rs-btn rs-btn-secondary rs-request-change-btn">
|
|
78
|
+
Request Status Change
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create status request card HTML
|
|
87
|
+
* @param {StatusRequest} request - Request object
|
|
88
|
+
* @returns {string} HTML
|
|
89
|
+
*/
|
|
90
|
+
function requestCardTemplate(request) {
|
|
91
|
+
const stateClass = `rs-state-${request.state}`;
|
|
92
|
+
const stateLabel = getStateLabel(request.state);
|
|
93
|
+
const progressPercent = getApprovalProgress(request);
|
|
94
|
+
|
|
95
|
+
return `
|
|
96
|
+
<div class="rs-request-card ${stateClass}" data-request-id="${request.requestId}">
|
|
97
|
+
<div class="rs-request-header">
|
|
98
|
+
<span class="rs-request-transition">
|
|
99
|
+
${escapeHtml(request.fromStatus)} -> ${escapeHtml(request.toStatus)}
|
|
100
|
+
</span>
|
|
101
|
+
<span class="rs-request-state rs-badge rs-badge-${request.state}">
|
|
102
|
+
${stateLabel}
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="rs-request-meta">
|
|
106
|
+
<span>Requested by <strong>${escapeHtml(request.requestedBy)}</strong></span>
|
|
107
|
+
<span>${formatTime(request.requestedAt)}</span>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="rs-request-justification">
|
|
110
|
+
${formatCommentBody(request.justification)}
|
|
111
|
+
</div>
|
|
112
|
+
<div class="rs-approval-progress">
|
|
113
|
+
<div class="rs-progress-bar">
|
|
114
|
+
<div class="rs-progress-fill" style="width: ${progressPercent}%"></div>
|
|
115
|
+
</div>
|
|
116
|
+
<span class="rs-progress-label">
|
|
117
|
+
${request.approvals.length}/${request.requiredApprovers.length} approvals
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="rs-approvers-list">
|
|
121
|
+
${renderApproversList(request)}
|
|
122
|
+
</div>
|
|
123
|
+
${request.state === review.RequestState.PENDING ? renderApprovalActions(request) : ''}
|
|
124
|
+
</div>
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Render approvers list with status
|
|
130
|
+
* @param {StatusRequest} request - Request object
|
|
131
|
+
* @returns {string} HTML
|
|
132
|
+
*/
|
|
133
|
+
function renderApproversList(request) {
|
|
134
|
+
const approvalMap = {};
|
|
135
|
+
request.approvals.forEach(a => {
|
|
136
|
+
approvalMap[a.user] = a;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return `
|
|
140
|
+
<div class="rs-approvers">
|
|
141
|
+
${request.requiredApprovers.map(approver => {
|
|
142
|
+
const approval = approvalMap[approver];
|
|
143
|
+
if (approval) {
|
|
144
|
+
const icon = approval.decision === 'approve' ? '[+]' : '[-]';
|
|
145
|
+
const cls = approval.decision === 'approve' ? 'approved' : 'rejected';
|
|
146
|
+
return `
|
|
147
|
+
<span class="rs-approver rs-approver-${cls}" title="${approval.comment || ''}">
|
|
148
|
+
${icon} ${escapeHtml(approver)}
|
|
149
|
+
</span>
|
|
150
|
+
`;
|
|
151
|
+
} else {
|
|
152
|
+
return `
|
|
153
|
+
<span class="rs-approver rs-approver-pending">
|
|
154
|
+
[ ] ${escapeHtml(approver)}
|
|
155
|
+
</span>
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
}).join('')}
|
|
159
|
+
</div>
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Render approval action buttons
|
|
165
|
+
* @param {StatusRequest} request - Request object
|
|
166
|
+
* @returns {string} HTML
|
|
167
|
+
*/
|
|
168
|
+
function renderApprovalActions(request) {
|
|
169
|
+
const user = review.state.currentUser;
|
|
170
|
+
if (!user || !request.requiredApprovers.includes(user)) {
|
|
171
|
+
return '';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check if user already approved
|
|
175
|
+
const existing = request.approvals.find(a => a.user === user);
|
|
176
|
+
if (existing) {
|
|
177
|
+
return `<div class="rs-already-voted">You have already ${existing.decision}d this request.</div>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return `
|
|
181
|
+
<div class="rs-approval-actions">
|
|
182
|
+
<button class="rs-btn rs-btn-success rs-approve-btn">Approve</button>
|
|
183
|
+
<button class="rs-btn rs-btn-danger rs-reject-btn">Reject</button>
|
|
184
|
+
<input type="text" class="rs-approval-comment" placeholder="Comment (optional)">
|
|
185
|
+
</div>
|
|
186
|
+
`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Create new request form HTML
|
|
191
|
+
* @param {string} reqId - Requirement ID
|
|
192
|
+
* @param {string} currentStatus - Current status
|
|
193
|
+
* @returns {string} HTML
|
|
194
|
+
*/
|
|
195
|
+
function requestFormTemplate(reqId, currentStatus) {
|
|
196
|
+
const transitions = getValidTransitions(currentStatus);
|
|
197
|
+
|
|
198
|
+
return `
|
|
199
|
+
<div class="rs-request-form" data-req-id="${reqId}">
|
|
200
|
+
<h4>Request Status Change</h4>
|
|
201
|
+
<div class="rs-form-group">
|
|
202
|
+
<label>Current Status</label>
|
|
203
|
+
<span class="rs-current-status-display">${escapeHtml(currentStatus)}</span>
|
|
204
|
+
</div>
|
|
205
|
+
<div class="rs-form-group">
|
|
206
|
+
<label>New Status</label>
|
|
207
|
+
<select class="rs-new-status">
|
|
208
|
+
${transitions.map(status =>
|
|
209
|
+
`<option value="${status}">${status}</option>`
|
|
210
|
+
).join('')}
|
|
211
|
+
</select>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="rs-form-group">
|
|
214
|
+
<label>Justification</label>
|
|
215
|
+
<textarea class="rs-justification" rows="3"
|
|
216
|
+
placeholder="Explain why this status change is needed..."></textarea>
|
|
217
|
+
</div>
|
|
218
|
+
<div class="rs-required-approvers">
|
|
219
|
+
<label>Required Approvers</label>
|
|
220
|
+
<span class="rs-approvers-display"></span>
|
|
221
|
+
</div>
|
|
222
|
+
<div class="rs-form-actions">
|
|
223
|
+
<button class="rs-btn rs-btn-primary rs-submit-request">Submit Request</button>
|
|
224
|
+
<button class="rs-btn rs-cancel-request">Cancel</button>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ==========================================================================
|
|
231
|
+
// Helper Functions
|
|
232
|
+
// ==========================================================================
|
|
233
|
+
|
|
234
|
+
function escapeHtml(text) {
|
|
235
|
+
const div = document.createElement('div');
|
|
236
|
+
div.textContent = text;
|
|
237
|
+
return div.innerHTML;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function formatTime(isoString) {
|
|
241
|
+
try {
|
|
242
|
+
const date = new Date(isoString);
|
|
243
|
+
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
|
244
|
+
} catch (e) {
|
|
245
|
+
return isoString;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function formatCommentBody(body) {
|
|
250
|
+
let html = escapeHtml(body);
|
|
251
|
+
html = html.replace(/\n/g, '<br>');
|
|
252
|
+
return html;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getStateLabel(state) {
|
|
256
|
+
switch (state) {
|
|
257
|
+
case review.RequestState.PENDING: return 'Pending';
|
|
258
|
+
case review.RequestState.APPROVED: return 'Approved';
|
|
259
|
+
case review.RequestState.REJECTED: return 'Rejected';
|
|
260
|
+
case review.RequestState.APPLIED: return 'Applied';
|
|
261
|
+
default: return state;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function getApprovalProgress(request) {
|
|
266
|
+
if (request.requiredApprovers.length === 0) return 100;
|
|
267
|
+
const approved = request.approvals.filter(a => a.decision === 'approve').length;
|
|
268
|
+
return Math.round((approved / request.requiredApprovers.length) * 100);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function getValidTransitions(currentStatus) {
|
|
272
|
+
const transitions = {
|
|
273
|
+
'Draft': ['Review', 'Active', 'Deprecated'],
|
|
274
|
+
'Review': ['Active', 'Draft', 'Deprecated'],
|
|
275
|
+
'Active': ['Deprecated'],
|
|
276
|
+
'Deprecated': [] // No transitions from Deprecated
|
|
277
|
+
};
|
|
278
|
+
return transitions[currentStatus] || [];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Change status directly via API (no approval workflow)
|
|
283
|
+
* @param {string} reqId - Requirement ID
|
|
284
|
+
* @param {string} newStatus - New status to set
|
|
285
|
+
* @returns {Promise<object>} API response
|
|
286
|
+
*/
|
|
287
|
+
async function changeStatusDirect(reqId, newStatus) {
|
|
288
|
+
const user = review.state.currentUser || 'anonymous';
|
|
289
|
+
try {
|
|
290
|
+
const response = await fetch(`/api/reviews/reqs/${reqId}/status`, {
|
|
291
|
+
method: 'POST',
|
|
292
|
+
headers: { 'Content-Type': 'application/json' },
|
|
293
|
+
body: JSON.stringify({ newStatus, user })
|
|
294
|
+
});
|
|
295
|
+
const result = await response.json();
|
|
296
|
+
if (result.success) {
|
|
297
|
+
// Update local state
|
|
298
|
+
const reqData = window.REQ_CONTENT_DATA && window.REQ_CONTENT_DATA[reqId];
|
|
299
|
+
if (reqData) {
|
|
300
|
+
reqData.status = newStatus;
|
|
301
|
+
}
|
|
302
|
+
// Refresh status display
|
|
303
|
+
updateStatusBadge(reqId, newStatus);
|
|
304
|
+
|
|
305
|
+
// Handle auto-add to package when status changes to Review
|
|
306
|
+
if (result.addedToPackage && typeof review.renderPackagesPanel === 'function') {
|
|
307
|
+
// Update local package state
|
|
308
|
+
const pkg = review.packages && review.packages.items &&
|
|
309
|
+
review.packages.items.find(p => p.packageId === result.addedToPackage.packageId);
|
|
310
|
+
if (pkg && !pkg.reqIds.includes(reqId)) {
|
|
311
|
+
pkg.reqIds.push(reqId);
|
|
312
|
+
}
|
|
313
|
+
// Re-render packages panel to update counts
|
|
314
|
+
review.renderPackagesPanel();
|
|
315
|
+
console.log(`REQ-${reqId} added to package: ${result.addedToPackage.packageName}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return result;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.error('Error changing status:', error);
|
|
321
|
+
return { success: false, error: error.message };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
review.changeStatusDirect = changeStatusDirect;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Update status badge in the UI
|
|
328
|
+
* @param {string} reqId - Requirement ID
|
|
329
|
+
* @param {string} newStatus - New status
|
|
330
|
+
*/
|
|
331
|
+
function updateStatusBadge(reqId, newStatus) {
|
|
332
|
+
// Update in grid/tree
|
|
333
|
+
const statusBadge = document.querySelector(`[data-req-id="${reqId}"] .status-badge`);
|
|
334
|
+
if (statusBadge) {
|
|
335
|
+
statusBadge.className = `status-badge status-${newStatus.toLowerCase()}`;
|
|
336
|
+
statusBadge.textContent = newStatus;
|
|
337
|
+
}
|
|
338
|
+
// Update in middle column if visible
|
|
339
|
+
const middleStatusBadge = document.querySelector(`#req-card-${reqId} .status-badge`);
|
|
340
|
+
if (middleStatusBadge) {
|
|
341
|
+
middleStatusBadge.className = `status-badge status-${newStatus.toLowerCase()}`;
|
|
342
|
+
middleStatusBadge.textContent = newStatus;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
review.updateStatusBadge = updateStatusBadge;
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Add Draft REQ to review package (shortcut for review mode)
|
|
349
|
+
* Note: Does NOT change status - valid statuses are Draft, Active, Deprecated only.
|
|
350
|
+
* This just adds the REQ to the active review package for tracking.
|
|
351
|
+
* @param {string} reqId - Requirement ID
|
|
352
|
+
* @returns {Promise<object>} API response
|
|
353
|
+
*/
|
|
354
|
+
async function toggleToReview(reqId) {
|
|
355
|
+
const reqData = window.REQ_CONTENT_DATA && window.REQ_CONTENT_DATA[reqId];
|
|
356
|
+
if (!reqData) return { success: false, error: 'REQ not found' };
|
|
357
|
+
|
|
358
|
+
if (reqData.status === 'Draft') {
|
|
359
|
+
// Add to active package for review tracking (don't change status)
|
|
360
|
+
if (review.addReqToActivePackage) {
|
|
361
|
+
const packageResult = await review.addReqToActivePackage(reqId);
|
|
362
|
+
if (packageResult && packageResult.success) {
|
|
363
|
+
console.log(`REQ-${reqId} added to review package`);
|
|
364
|
+
return { success: true, addedToPackage: packageResult };
|
|
365
|
+
} else {
|
|
366
|
+
return { success: false, error: packageResult?.error || 'Failed to add to package' };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return { success: false, error: 'Package system not available' };
|
|
370
|
+
}
|
|
371
|
+
return { success: false, error: 'REQ is not in Draft status' };
|
|
372
|
+
}
|
|
373
|
+
review.toggleToReview = toggleToReview;
|
|
374
|
+
|
|
375
|
+
// ==========================================================================
|
|
376
|
+
// UI Components
|
|
377
|
+
// ==========================================================================
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Render status panel for a requirement
|
|
381
|
+
* @param {Element} container - Container element
|
|
382
|
+
* @param {string} reqId - Requirement ID
|
|
383
|
+
* @param {string} currentStatus - Current requirement status
|
|
384
|
+
*/
|
|
385
|
+
function renderStatusPanel(container, reqId, currentStatus) {
|
|
386
|
+
container.innerHTML = statusPanelTemplate(reqId, currentStatus);
|
|
387
|
+
|
|
388
|
+
const requests = review.state.getRequests(reqId);
|
|
389
|
+
const requestsContainer = container.querySelector('.rs-requests');
|
|
390
|
+
const noRequests = container.querySelector('.rs-no-requests');
|
|
391
|
+
|
|
392
|
+
if (requests.length === 0) {
|
|
393
|
+
noRequests.style.display = 'block';
|
|
394
|
+
} else {
|
|
395
|
+
requests.forEach(request => {
|
|
396
|
+
requestsContainer.insertAdjacentHTML('beforeend', requestCardTemplate(request));
|
|
397
|
+
});
|
|
398
|
+
bindRequestEvents(container);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Bind quick toggle button
|
|
402
|
+
const quickToggleBtn = container.querySelector('.rs-quick-toggle');
|
|
403
|
+
if (quickToggleBtn) {
|
|
404
|
+
quickToggleBtn.addEventListener('click', async () => {
|
|
405
|
+
quickToggleBtn.disabled = true;
|
|
406
|
+
quickToggleBtn.textContent = 'Updating...';
|
|
407
|
+
const result = await toggleToReview(reqId);
|
|
408
|
+
if (result.success) {
|
|
409
|
+
// Re-render the panel with new status
|
|
410
|
+
renderStatusPanel(container, reqId, 'Review');
|
|
411
|
+
} else {
|
|
412
|
+
quickToggleBtn.disabled = false;
|
|
413
|
+
quickToggleBtn.textContent = 'Set to Review';
|
|
414
|
+
alert('Failed to change status: ' + (result.error || 'Unknown error'));
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Bind request change button
|
|
420
|
+
const requestBtn = container.querySelector('.rs-request-change-btn');
|
|
421
|
+
if (requestBtn) {
|
|
422
|
+
const transitions = getValidTransitions(currentStatus);
|
|
423
|
+
if (transitions.length === 0) {
|
|
424
|
+
requestBtn.disabled = true;
|
|
425
|
+
requestBtn.title = 'No valid transitions from current status';
|
|
426
|
+
} else {
|
|
427
|
+
requestBtn.addEventListener('click', () => {
|
|
428
|
+
showRequestForm(container, reqId, currentStatus);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Bind package toggle button
|
|
434
|
+
const packageToggleBtn = container.querySelector('.rs-package-toggle');
|
|
435
|
+
if (packageToggleBtn) {
|
|
436
|
+
packageToggleBtn.addEventListener('click', async () => {
|
|
437
|
+
packageToggleBtn.disabled = true;
|
|
438
|
+
const originalText = packageToggleBtn.textContent;
|
|
439
|
+
packageToggleBtn.textContent = 'Updating...';
|
|
440
|
+
|
|
441
|
+
const inPackage = isReqInActivePackage(reqId);
|
|
442
|
+
let result;
|
|
443
|
+
|
|
444
|
+
if (inPackage) {
|
|
445
|
+
// Remove from active package
|
|
446
|
+
const packageId = review.packages.activeId;
|
|
447
|
+
result = await review.removeReqFromPackage(packageId, reqId);
|
|
448
|
+
} else {
|
|
449
|
+
// Add to active package (or default if none active)
|
|
450
|
+
result = await review.addReqToActivePackage(reqId);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (result && result.success) {
|
|
454
|
+
// Re-render the panel to update button state
|
|
455
|
+
renderStatusPanel(container, reqId, currentStatus);
|
|
456
|
+
} else {
|
|
457
|
+
packageToggleBtn.disabled = false;
|
|
458
|
+
packageToggleBtn.textContent = originalText;
|
|
459
|
+
alert('Failed to update package: ' + (result?.error || 'Unknown error'));
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
review.renderStatusPanel = renderStatusPanel;
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Show request form
|
|
468
|
+
* @param {Element} container - Container element
|
|
469
|
+
* @param {string} reqId - Requirement ID
|
|
470
|
+
* @param {string} currentStatus - Current status
|
|
471
|
+
*/
|
|
472
|
+
function showRequestForm(container, reqId, currentStatus) {
|
|
473
|
+
// Remove existing form
|
|
474
|
+
let form = container.querySelector('.rs-request-form');
|
|
475
|
+
if (form) form.remove();
|
|
476
|
+
|
|
477
|
+
container.insertAdjacentHTML('afterbegin', requestFormTemplate(reqId, currentStatus));
|
|
478
|
+
form = container.querySelector('.rs-request-form');
|
|
479
|
+
|
|
480
|
+
// Update approvers display on status change
|
|
481
|
+
const newStatus = form.querySelector('.rs-new-status');
|
|
482
|
+
const approversDisplay = form.querySelector('.rs-approvers-display');
|
|
483
|
+
|
|
484
|
+
function updateApprovers() {
|
|
485
|
+
const toStatus = newStatus.value;
|
|
486
|
+
const approvers = review.state.config.getRequiredApprovers(currentStatus, toStatus);
|
|
487
|
+
approversDisplay.textContent = approvers.join(', ');
|
|
488
|
+
}
|
|
489
|
+
updateApprovers();
|
|
490
|
+
newStatus.addEventListener('change', updateApprovers);
|
|
491
|
+
|
|
492
|
+
// Submit handler
|
|
493
|
+
form.querySelector('.rs-submit-request').addEventListener('click', () => {
|
|
494
|
+
submitStatusRequest(form, reqId, currentStatus);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Cancel handler
|
|
498
|
+
form.querySelector('.rs-cancel-request').addEventListener('click', () => {
|
|
499
|
+
form.remove();
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Focus justification
|
|
503
|
+
form.querySelector('.rs-justification').focus();
|
|
504
|
+
}
|
|
505
|
+
review.showRequestForm = showRequestForm;
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Submit status change request
|
|
509
|
+
* @param {Element} form - Form element
|
|
510
|
+
* @param {string} reqId - Requirement ID
|
|
511
|
+
* @param {string} currentStatus - Current status
|
|
512
|
+
*/
|
|
513
|
+
function submitStatusRequest(form, reqId, currentStatus) {
|
|
514
|
+
const newStatus = form.querySelector('.rs-new-status').value;
|
|
515
|
+
const justification = form.querySelector('.rs-justification').value.trim();
|
|
516
|
+
|
|
517
|
+
if (!justification) {
|
|
518
|
+
alert('Please provide a justification');
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const user = review.state.currentUser || 'anonymous';
|
|
523
|
+
const approvers = review.state.config.getRequiredApprovers(currentStatus, newStatus);
|
|
524
|
+
|
|
525
|
+
// Create request
|
|
526
|
+
const request = review.StatusRequest.create(
|
|
527
|
+
reqId, currentStatus, newStatus, user, justification, approvers
|
|
528
|
+
);
|
|
529
|
+
review.state.addRequest(request);
|
|
530
|
+
|
|
531
|
+
// Trigger event
|
|
532
|
+
document.dispatchEvent(new CustomEvent('traceview:request-created', {
|
|
533
|
+
detail: { request, reqId }
|
|
534
|
+
}));
|
|
535
|
+
|
|
536
|
+
// Re-render
|
|
537
|
+
const panel = form.closest('.rs-status-panel');
|
|
538
|
+
if (panel) {
|
|
539
|
+
renderStatusPanel(panel.parentElement, reqId, currentStatus);
|
|
540
|
+
} else {
|
|
541
|
+
form.remove();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Bind event handlers to request elements
|
|
547
|
+
* @param {Element} container - Container element
|
|
548
|
+
*/
|
|
549
|
+
function bindRequestEvents(container) {
|
|
550
|
+
// Approve buttons
|
|
551
|
+
container.querySelectorAll('.rs-approve-btn').forEach(btn => {
|
|
552
|
+
btn.addEventListener('click', () => {
|
|
553
|
+
const card = btn.closest('.rs-request-card');
|
|
554
|
+
submitApproval(card, container, 'approve');
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// Reject buttons
|
|
559
|
+
container.querySelectorAll('.rs-reject-btn').forEach(btn => {
|
|
560
|
+
btn.addEventListener('click', () => {
|
|
561
|
+
const card = btn.closest('.rs-request-card');
|
|
562
|
+
submitApproval(card, container, 'reject');
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Submit approval/rejection
|
|
569
|
+
* @param {Element} card - Request card element
|
|
570
|
+
* @param {Element} container - Container element
|
|
571
|
+
* @param {string} decision - 'approve' or 'reject'
|
|
572
|
+
*/
|
|
573
|
+
function submitApproval(card, container, decision) {
|
|
574
|
+
const requestId = card.getAttribute('data-request-id');
|
|
575
|
+
const comment = card.querySelector('.rs-approval-comment')?.value || '';
|
|
576
|
+
const user = review.state.currentUser;
|
|
577
|
+
const reqId = container.querySelector('[data-req-id]')?.getAttribute('data-req-id') ||
|
|
578
|
+
container.closest('[data-req-id]')?.getAttribute('data-req-id');
|
|
579
|
+
|
|
580
|
+
if (!user) {
|
|
581
|
+
alert('Please set your username first');
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (reqId) {
|
|
586
|
+
const requests = review.state.getRequests(reqId);
|
|
587
|
+
const request = requests.find(r => r.requestId === requestId);
|
|
588
|
+
if (request) {
|
|
589
|
+
request.addApproval(user, decision, comment);
|
|
590
|
+
|
|
591
|
+
// Trigger event
|
|
592
|
+
document.dispatchEvent(new CustomEvent('traceview:approval-added', {
|
|
593
|
+
detail: { request, reqId, user, decision }
|
|
594
|
+
}));
|
|
595
|
+
|
|
596
|
+
// Get current status from display
|
|
597
|
+
const currentStatus = container.querySelector('.rs-current-status strong')?.textContent || 'Draft';
|
|
598
|
+
|
|
599
|
+
// Re-render
|
|
600
|
+
renderStatusPanel(container.parentElement || container, reqId, currentStatus);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Get pending request count for a requirement
|
|
607
|
+
* @param {string} reqId - Requirement ID
|
|
608
|
+
* @returns {number} Count of pending requests
|
|
609
|
+
*/
|
|
610
|
+
function getPendingRequestCount(reqId) {
|
|
611
|
+
const requests = review.state.getRequests(reqId);
|
|
612
|
+
return requests.filter(r => r.state === review.RequestState.PENDING).length;
|
|
613
|
+
}
|
|
614
|
+
review.getPendingRequestCount = getPendingRequestCount;
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Create status badge for display in REQ list
|
|
618
|
+
* @param {string} reqId - Requirement ID
|
|
619
|
+
* @returns {string} HTML for badge or empty string
|
|
620
|
+
*/
|
|
621
|
+
function createStatusBadge(reqId) {
|
|
622
|
+
const pending = getPendingRequestCount(reqId);
|
|
623
|
+
if (pending === 0) return '';
|
|
624
|
+
|
|
625
|
+
return `<span class="rs-badge rs-badge-pending" title="${pending} pending status request(s)">
|
|
626
|
+
[P] ${pending}
|
|
627
|
+
</span>`;
|
|
628
|
+
}
|
|
629
|
+
review.createStatusBadge = createStatusBadge;
|
|
630
|
+
|
|
631
|
+
// ==========================================================================
|
|
632
|
+
// Review Panel Integration
|
|
633
|
+
// ==========================================================================
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Handle review panel ready event - add status section
|
|
637
|
+
* @param {CustomEvent} event - Event with reqId, req, and sectionsContainer
|
|
638
|
+
*/
|
|
639
|
+
function handleReviewPanelReady(event) {
|
|
640
|
+
const { reqId, req, sectionsContainer } = event.detail;
|
|
641
|
+
if (!sectionsContainer) return;
|
|
642
|
+
|
|
643
|
+
// Get current status from requirement data
|
|
644
|
+
const currentStatus = req ? req.status : 'Draft';
|
|
645
|
+
|
|
646
|
+
// Create status section
|
|
647
|
+
const statusSection = document.createElement('div');
|
|
648
|
+
statusSection.className = 'rs-status-section';
|
|
649
|
+
statusSection.setAttribute('data-req-id', reqId);
|
|
650
|
+
sectionsContainer.appendChild(statusSection);
|
|
651
|
+
|
|
652
|
+
// Render status panel
|
|
653
|
+
renderStatusPanel(statusSection, reqId, currentStatus);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Register event listener
|
|
657
|
+
document.addEventListener('traceview:review-panel-ready', handleReviewPanelReady);
|
|
658
|
+
|
|
659
|
+
})(TraceView.review);
|