mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 18:11:08 -05:00
Add comment and reply functionality with preview and notification templates
This commit is contained in:
314
static/js/components/version-comparison.js
Normal file
314
static/js/components/version-comparison.js
Normal file
@@ -0,0 +1,314 @@
|
||||
// Version Comparison Component
|
||||
|
||||
class VersionComparison {
|
||||
constructor(options = {}) {
|
||||
this.container = null;
|
||||
this.versions = new Map();
|
||||
this.selectedVersions = new Set();
|
||||
this.maxSelections = options.maxSelections || 3;
|
||||
this.onCompare = options.onCompare || (() => {});
|
||||
this.onRollback = options.onRollback || (() => {});
|
||||
this.timeline = null;
|
||||
}
|
||||
|
||||
initialize(containerId) {
|
||||
this.container = document.getElementById(containerId);
|
||||
if (!this.container) {
|
||||
throw new Error(`Container element with id "${containerId}" not found`);
|
||||
}
|
||||
this._initializeTimeline();
|
||||
}
|
||||
|
||||
setVersions(versions) {
|
||||
this.versions = new Map(versions.map(v => [v.name, v]));
|
||||
this._updateTimeline();
|
||||
this.render();
|
||||
}
|
||||
|
||||
_initializeTimeline() {
|
||||
this.timeline = document.createElement('div');
|
||||
this.timeline.className = 'version-timeline';
|
||||
this.container.appendChild(this.timeline);
|
||||
}
|
||||
|
||||
_updateTimeline() {
|
||||
const sortedVersions = Array.from(this.versions.values())
|
||||
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||
|
||||
this.timeline.innerHTML = `
|
||||
<div class="timeline-track">
|
||||
${this._renderTimelineDots(sortedVersions)}
|
||||
</div>
|
||||
<div class="timeline-labels">
|
||||
${this._renderTimelineLabels(sortedVersions)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
_renderTimelineDots(versions) {
|
||||
return versions.map(version => `
|
||||
<div class="timeline-point ${this.selectedVersions.has(version.name) ? 'selected' : ''}"
|
||||
data-version="${version.name}"
|
||||
style="--impact-score: ${version.comparison_metadata.impact_score || 0}"
|
||||
onclick="this._toggleVersionSelection('${version.name}')">
|
||||
<div class="point-indicator"></div>
|
||||
${this._renderImpactIndicator(version)}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
_renderImpactIndicator(version) {
|
||||
const impact = version.comparison_metadata.impact_score || 0;
|
||||
const size = Math.max(8, Math.min(24, impact * 24)); // 8-24px based on impact
|
||||
|
||||
return `
|
||||
<div class="impact-indicator"
|
||||
style="width: ${size}px; height: ${size}px"
|
||||
title="Impact Score: ${Math.round(impact * 100)}%">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
_renderTimelineLabels(versions) {
|
||||
return versions.map(version => `
|
||||
<div class="timeline-label" data-version="${version.name}">
|
||||
<div class="version-name">${version.name}</div>
|
||||
<div class="version-date">
|
||||
${new Date(version.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.container) return;
|
||||
|
||||
const selectedVersionsArray = Array.from(this.selectedVersions);
|
||||
|
||||
this.container.innerHTML = `
|
||||
<div class="version-comparison-tool">
|
||||
<div class="comparison-header">
|
||||
<h3>Version Comparison</h3>
|
||||
<div class="selected-versions">
|
||||
${this._renderSelectedVersions(selectedVersionsArray)}
|
||||
</div>
|
||||
<div class="comparison-actions">
|
||||
${this._renderActionButtons(selectedVersionsArray)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.timeline.outerHTML}
|
||||
|
||||
<div class="comparison-content">
|
||||
${this._renderComparisonContent(selectedVersionsArray)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.attachEventListeners();
|
||||
}
|
||||
|
||||
_renderSelectedVersions(selectedVersions) {
|
||||
return selectedVersions.map((version, index) => `
|
||||
<div class="selected-version">
|
||||
<span class="version-label">Version ${index + 1}:</span>
|
||||
<span class="version-value">${version}</span>
|
||||
<button class="remove-version" onclick="this._removeVersion('${version}')">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
_renderActionButtons(selectedVersions) {
|
||||
const canCompare = selectedVersions.length >= 2;
|
||||
const canRollback = selectedVersions.length === 1;
|
||||
|
||||
return `
|
||||
${canCompare ? `
|
||||
<button class="compare-button" onclick="this._handleCompare()">
|
||||
Compare Versions
|
||||
</button>
|
||||
` : ''}
|
||||
${canRollback ? `
|
||||
<button class="rollback-button" onclick="this._handleRollback()">
|
||||
Rollback to Version
|
||||
</button>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
_renderComparisonContent(selectedVersions) {
|
||||
if (selectedVersions.length < 2) {
|
||||
return `
|
||||
<div class="comparison-placeholder">
|
||||
Select at least two versions to compare
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="comparison-results">
|
||||
<div class="results-loading">
|
||||
Computing differences...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
_toggleVersionSelection(versionName) {
|
||||
if (this.selectedVersions.has(versionName)) {
|
||||
this.selectedVersions.delete(versionName);
|
||||
} else if (this.selectedVersions.size < this.maxSelections) {
|
||||
this.selectedVersions.add(versionName);
|
||||
} else {
|
||||
// Show max selections warning
|
||||
this._showWarning(`Maximum ${this.maxSelections} versions can be compared`);
|
||||
return;
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
_removeVersion(versionName) {
|
||||
this.selectedVersions.delete(versionName);
|
||||
this.render();
|
||||
}
|
||||
|
||||
async _handleCompare() {
|
||||
const selectedVersions = Array.from(this.selectedVersions);
|
||||
if (selectedVersions.length < 2) return;
|
||||
|
||||
try {
|
||||
const results = await this.onCompare(selectedVersions);
|
||||
this._renderComparisonResults(results);
|
||||
} catch (error) {
|
||||
console.error('Comparison failed:', error);
|
||||
this._showError('Failed to compare versions');
|
||||
}
|
||||
}
|
||||
|
||||
async _handleRollback() {
|
||||
const selectedVersion = Array.from(this.selectedVersions)[0];
|
||||
if (!selectedVersion) return;
|
||||
|
||||
try {
|
||||
await this.onRollback(selectedVersion);
|
||||
// Handle successful rollback
|
||||
} catch (error) {
|
||||
console.error('Rollback failed:', error);
|
||||
this._showError('Failed to rollback version');
|
||||
}
|
||||
}
|
||||
|
||||
_renderComparisonResults(results) {
|
||||
const resultsContainer = this.container.querySelector('.comparison-results');
|
||||
if (!resultsContainer) return;
|
||||
|
||||
resultsContainer.innerHTML = `
|
||||
<div class="results-content">
|
||||
${results.map(diff => this._renderDiffSection(diff)).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
_renderDiffSection(diff) {
|
||||
return `
|
||||
<div class="diff-section">
|
||||
<div class="diff-header">
|
||||
<h4>Changes: ${diff.version1} → ${diff.version2}</h4>
|
||||
<div class="diff-stats">
|
||||
<span class="computation-time">
|
||||
Computed in ${diff.computation_time.toFixed(2)}s
|
||||
</span>
|
||||
<span class="impact-score">
|
||||
Impact Score: ${Math.round(diff.impact_score * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="changes-list">
|
||||
${this._renderChanges(diff.changes)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
_renderChanges(changes) {
|
||||
return changes.map(change => `
|
||||
<div class="change-item">
|
||||
<div class="change-header">
|
||||
<span class="change-type">${change.type}</span>
|
||||
<span class="change-file">${change.file}</span>
|
||||
</div>
|
||||
<div class="change-content">
|
||||
<div class="old-value">
|
||||
<div class="value-header">Previous</div>
|
||||
<pre>${this._escapeHtml(change.old_value)}</pre>
|
||||
</div>
|
||||
<div class="new-value">
|
||||
<div class="value-header">New</div>
|
||||
<pre>${this._escapeHtml(change.new_value)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
_showWarning(message) {
|
||||
const warning = document.createElement('div');
|
||||
warning.className = 'comparison-warning';
|
||||
warning.textContent = message;
|
||||
this.container.appendChild(warning);
|
||||
setTimeout(() => warning.remove(), 3000);
|
||||
}
|
||||
|
||||
_showError(message) {
|
||||
const error = document.createElement('div');
|
||||
error.className = 'comparison-error';
|
||||
error.textContent = message;
|
||||
this.container.appendChild(error);
|
||||
setTimeout(() => error.remove(), 3000);
|
||||
}
|
||||
|
||||
_escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
// Timeline scroll handling
|
||||
const timeline = this.container.querySelector('.version-timeline');
|
||||
if (timeline) {
|
||||
let isDown = false;
|
||||
let startX;
|
||||
let scrollLeft;
|
||||
|
||||
timeline.addEventListener('mousedown', (e) => {
|
||||
isDown = true;
|
||||
timeline.classList.add('active');
|
||||
startX = e.pageX - timeline.offsetLeft;
|
||||
scrollLeft = timeline.scrollLeft;
|
||||
});
|
||||
|
||||
timeline.addEventListener('mouseleave', () => {
|
||||
isDown = false;
|
||||
timeline.classList.remove('active');
|
||||
});
|
||||
|
||||
timeline.addEventListener('mouseup', () => {
|
||||
isDown = false;
|
||||
timeline.classList.remove('active');
|
||||
});
|
||||
|
||||
timeline.addEventListener('mousemove', (e) => {
|
||||
if (!isDown) return;
|
||||
e.preventDefault();
|
||||
const x = e.pageX - timeline.offsetLeft;
|
||||
const walk = (x - startX) * 2;
|
||||
timeline.scrollLeft = scrollLeft - walk;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default VersionComparison;
|
||||
Reference in New Issue
Block a user