Files
thrillwiki_django_no_react/static/js/version-control.js

536 lines
18 KiB
JavaScript

// Version Control System Functionality
class VersionControl {
constructor() {
this.setupEventListeners();
}
setupEventListeners() {
// Branch switching
document.addEventListener('htmx:afterRequest', (event) => {
if (event.detail.target.id === 'branch-form-container') {
this.handleBranchFormResponse(event);
}
});
// Listen for branch switches
document.addEventListener('branch-switched', () => {
this.refreshContent();
});
// Handle merge operations
document.addEventListener('htmx:afterRequest', (event) => {
if (event.detail.target.id === 'merge-panel') {
this.handleMergeResponse(event);
}
});
}
handleBranchFormResponse(event) {
if (event.detail.successful) {
// Clear the branch form container
document.getElementById('branch-form-container').innerHTML = '';
// Trigger branch list refresh
document.body.dispatchEvent(new CustomEvent('branch-updated'));
}
}
handleMergeResponse(event) {
if (event.detail.successful) {
const mergePanel = document.getElementById('merge-panel');
if (mergePanel.innerHTML.includes('Merge Successful')) {
// Trigger content refresh after successful merge
setTimeout(() => {
this.refreshContent();
}, 1500);
}
}
}
refreshContent() {
// Reload the page to show content from new branch
window.location.reload();
}
// Branch operations
createBranch(name, parentBranch = null) {
const formData = new FormData();
formData.append('name', name);
if (parentBranch) {
formData.append('parent', parentBranch);
}
return fetch('/vcs/branches/create/', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': this.getCsrfToken()
}
}).then(response => response.json());
}
switchBranch(branchName) {
const formData = new FormData();
formData.append('branch', branchName);
return fetch('/vcs/branches/switch/', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': this.getCsrfToken()
}
}).then(response => {
if (response.ok) {
document.body.dispatchEvent(new CustomEvent('branch-switched'));
}
return response.json();
});
}
// Merge operations
initiateMerge(sourceBranch, targetBranch) {
const formData = new FormData();
formData.append('source', sourceBranch);
formData.append('target', targetBranch);
return fetch('/vcs/merge/', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': this.getCsrfToken()
}
}).then(response => response.json());
}
resolveConflicts(resolutions) {
return fetch('/vcs/resolve-conflicts/', {
method: 'POST',
body: JSON.stringify(resolutions),
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken()
}
}).then(response => response.json());
}
// History operations
getHistory(branch = null) {
let url = '/vcs/history/';
if (branch) {
url += `?branch=${encodeURIComponent(branch)}`;
}
return fetch(url)
.then(response => response.json());
}
// Comment operations
async createComment(threadId, content, parentId = null) {
try {
const response = await fetch('/vcs/comments/create/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken()
},
body: JSON.stringify({
thread_id: threadId,
content: content,
parent_id: parentId
})
});
if (!response.ok) {
throw new Error('Failed to create comment');
}
const comment = await response.json();
return comment;
} catch (error) {
this.showError('Error creating comment: ' + error.message);
throw error;
}
}
async createCommentThread(changeId, anchor, initialComment) {
try {
const response = await fetch('/vcs/comments/threads/create/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken()
},
body: JSON.stringify({
change_id: changeId,
anchor: anchor,
initial_comment: initialComment
})
});
if (!response.ok) {
throw new Error('Failed to create comment thread');
}
const thread = await response.json();
return thread;
} catch (error) {
this.showError('Error creating comment thread: ' + error.message);
throw error;
}
}
async resolveThread(threadId) {
try {
const response = await fetch(`/vcs/comments/threads/${threadId}/resolve/`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCsrfToken()
}
});
if (!response.ok) {
throw new Error('Failed to resolve thread');
}
return await response.json();
} catch (error) {
this.showError('Error resolving thread: ' + error.message);
throw error;
}
}
async editComment(commentId, content) {
try {
const response = await fetch(`/vcs/comments/${commentId}/`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken()
},
body: JSON.stringify({
content: content
})
});
if (!response.ok) {
throw new Error('Failed to edit comment');
}
return await response.json();
} catch (error) {
this.showError('Error editing comment: ' + error.message);
throw error;
}
}
async getThreadComments(threadId) {
try {
const response = await fetch(`/vcs/comments/threads/${threadId}/`);
if (!response.ok) {
throw new Error('Failed to fetch thread comments');
}
return await response.json();
} catch (error) {
this.showError('Error fetching comments: ' + error.message);
throw error;
}
}
initializeCommentPanel(containerId, options = {}) {
const panel = new InlineCommentPanel({
...options,
onReply: async (content, parentId) => {
const comment = await this.createComment(
options.threadId,
content,
parentId
);
const thread = await this.getThreadComments(options.threadId);
panel.setThread(thread);
},
onResolve: async () => {
await this.resolveThread(options.threadId);
const thread = await this.getThreadComments(options.threadId);
panel.setThread(thread);
}
});
panel.initialize(containerId);
return panel;
}
// Branch locking operations
async acquireLock(branchName, duration = 48, reason = "") {
try {
const response = await fetch('/vcs/branches/lock/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken()
},
body: JSON.stringify({
branch: branchName,
duration: duration,
reason: reason
})
});
if (!response.ok) {
throw new Error('Failed to acquire lock');
}
const result = await response.json();
if (result.success) {
this.refreshLockStatus(branchName);
return true;
}
return false;
} catch (error) {
this.showError('Error acquiring lock: ' + error.message);
return false;
}
}
async releaseLock(branchName, force = false) {
try {
const response = await fetch('/vcs/branches/unlock/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken()
},
body: JSON.stringify({
branch: branchName,
force: force
})
});
if (!response.ok) {
throw new Error('Failed to release lock');
}
const result = await response.json();
if (result.success) {
this.refreshLockStatus(branchName);
return true;
}
return false;
} catch (error) {
this.showError('Error releasing lock: ' + error.message);
return false;
}
}
async getLockStatus(branchName) {
try {
const response = await fetch(`/vcs/branches/${encodeURIComponent(branchName)}/lock-status/`);
if (!response.ok) {
throw new Error('Failed to get lock status');
}
return await response.json();
} catch (error) {
this.showError('Error getting lock status: ' + error.message);
return null;
}
}
async getLockHistory(branchName, limit = 10) {
try {
const response = await fetch(
`/vcs/branches/${encodeURIComponent(branchName)}/lock-history/?limit=${limit}`
);
if (!response.ok) {
throw new Error('Failed to get lock history');
}
return await response.json();
} catch (error) {
this.showError('Error getting lock history: ' + error.message);
return [];
}
}
async refreshLockStatus(branchName) {
const lockStatus = await this.getLockStatus(branchName);
if (!lockStatus) return;
const statusElement = document.querySelector(`[data-branch="${branchName}"] .lock-status`);
if (!statusElement) return;
if (lockStatus.locked) {
const expiryDate = new Date(lockStatus.expires);
statusElement.className = 'lock-status locked';
statusElement.innerHTML = `
<svg class="lock-icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
</svg>
<div class="lock-info">
<span class="user">${lockStatus.user}</span>
<span class="expiry">Expires: ${expiryDate.toLocaleString()}</span>
${lockStatus.reason ? `<span class="reason">${lockStatus.reason}</span>` : ''}
</div>
`;
} else {
statusElement.className = 'lock-status unlocked';
statusElement.innerHTML = `
<svg class="lock-icon" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 2a5 5 0 00-5 5v2a2 2 0 00-2 2v5a2 2 0 002 2h10a2 2 0 002-2v-5a2 2 0 00-2-2H7V7a3 3 0 015.905-.75 1 1 0 001.937-.5A5.002 5.002 0 0010 2z"/>
</svg>
<span>Unlocked</span>
`;
}
// Update lock controls
this.updateLockControls(branchName, lockStatus);
}
async updateLockControls(branchName, lockStatus) {
const controlsElement = document.querySelector(`[data-branch="${branchName}"] .lock-controls`);
if (!controlsElement) return;
if (lockStatus.locked) {
controlsElement.innerHTML = `
<button class="lock-button unlock" onclick="window.versionControl.releaseLock('${branchName}')">
<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 2a5 5 0 00-5 5v2a2 2 0 00-2 2v5a2 2 0 002 2h10a2 2 0 002-2v-5a2 2 0 00-2-2H7V7a3 3 0 015.905-.75 1 1 0 001.937-.5A5.002 5.002 0 0010 2z"/>
</svg>
Unlock Branch
</button>
`;
} else {
controlsElement.innerHTML = `
<button class="lock-button lock" onclick="window.versionControl.acquireLock('${branchName}')">
<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
</svg>
Lock Branch
</button>
`;
}
}
// Utility functions
getCsrfToken() {
return document.querySelector('[name=csrfmiddlewaretoken]').value;
}
showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4';
errorDiv.innerHTML = `
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
</div>
<div class="ml-3">
<p>${message}</p>
</div>
</div>
`;
document.querySelector('.version-control-ui').prepend(errorDiv);
setTimeout(() => errorDiv.remove(), 5000);
}
}
// Import DiffViewer component
import DiffViewer from './components/diff-viewer.js';
// Initialize version control
document.addEventListener('DOMContentLoaded', () => {
window.versionControl = new VersionControl();
// Initialize DiffViewer if diff container exists
const diffContainer = document.getElementById('diff-container');
if (diffContainer) {
window.diffViewer = new DiffViewer({
renderStrategy: 'side-by-side'
});
diffViewer.initialize('diff-container');
}
});
// Add to VersionControl class constructor
class VersionControl {
constructor() {
this.setupEventListeners();
this.diffViewer = null;
if (document.getElementById('diff-container')) {
this.diffViewer = new DiffViewer({
renderStrategy: 'side-by-side'
});
this.diffViewer.initialize('diff-container');
}
}
// Add getDiff method to VersionControl class
async getDiff(version1, version2) {
const url = `/vcs/diff/?v1=${encodeURIComponent(version1)}&v2=${encodeURIComponent(version2)}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Failed to fetch diff');
}
const diffData = await response.json();
if (this.diffViewer) {
await this.diffViewer.render(diffData);
}
return diffData;
} catch (error) {
this.showError('Error loading diff: ' + error.message);
throw error;
}
}
// Add viewChanges method to VersionControl class
async viewChanges(changeId) {
try {
const response = await fetch(`/vcs/changes/${changeId}/`);
if (!response.ok) {
throw new Error('Failed to fetch changes');
}
const changeData = await response.json();
if (this.diffViewer) {
await this.diffViewer.render(changeData);
} else {
this.showError('Diff viewer not initialized');
}
} catch (error) {
this.showError('Error loading changes: ' + error.message);
throw error;
}
}
// Add addComment method to VersionControl class
async addComment(changeId, anchor, content) {
try {
const response = await fetch('/vcs/comments/add/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken()
},
body: JSON.stringify({
change_id: changeId,
anchor: anchor,
content: content
})
});
if (!response.ok) {
throw new Error('Failed to add comment');
}
const comment = await response.json();
if (this.diffViewer) {
this.diffViewer.addCommentThread(anchor, [comment]);
}
return comment;
} catch (error) {
this.showError('Error adding comment: ' + error.message);
throw error;
}
}