Add comment and reply functionality with preview and notification templates

This commit is contained in:
pacnpal
2025-02-07 13:13:49 -05:00
parent 2c4d2daf34
commit 0e0ed01cee
30 changed files with 5153 additions and 383 deletions

View File

@@ -0,0 +1,234 @@
// Approval Panel Component
class ApprovalPanel {
constructor(options = {}) {
this.container = null;
this.changeset = null;
this.currentUser = options.currentUser;
this.onApprove = options.onApprove || (() => {});
this.onReject = options.onReject || (() => {});
this.onSubmit = options.onSubmit || (() => {});
}
initialize(containerId) {
this.container = document.getElementById(containerId);
if (!this.container) {
throw new Error(`Container element with id "${containerId}" not found`);
}
}
setChangeset(changeset) {
this.changeset = changeset;
this.render();
}
render() {
if (!this.container || !this.changeset) return;
const approvalState = this.changeset.approval_state || [];
const currentStage = approvalState.find(s => s.status === 'pending') || approvalState[0];
this.container.innerHTML = `
<div class="approval-panel">
<div class="approval-header">
<h3 class="approval-title">Change Approval</h3>
${this._renderStatus()}
</div>
<div class="approval-stages">
${this._renderStages(approvalState)}
</div>
${currentStage && this.changeset.status === 'pending_approval' ?
this._renderApprovalActions(currentStage) : ''
}
<div class="approval-history">
${this._renderHistory()}
</div>
</div>
`;
this.attachEventListeners();
}
_renderStatus() {
const statusMap = {
'draft': { class: 'status-draft', text: 'Draft' },
'pending_approval': { class: 'status-pending', text: 'Pending Approval' },
'approved': { class: 'status-approved', text: 'Approved' },
'rejected': { class: 'status-rejected', text: 'Rejected' },
'applied': { class: 'status-applied', text: 'Applied' },
'failed': { class: 'status-failed', text: 'Failed' },
'reverted': { class: 'status-reverted', text: 'Reverted' }
};
const status = statusMap[this.changeset.status] || statusMap.draft;
return `
<div class="approval-status ${status.class}">
${status.text}
</div>
`;
}
_renderStages(stages) {
return stages.map((stage, index) => `
<div class="approval-stage ${stage.status}-stage">
<div class="stage-header">
<span class="stage-name">${stage.name}</span>
<span class="stage-status ${stage.status}">${
this._formatStageStatus(stage.status)
}</span>
</div>
<div class="stage-details">
<div class="required-roles">
${stage.required_roles.map(role => `
<span class="role-badge">${role}</span>
`).join('')}
</div>
${stage.approvers.length > 0 ? `
<div class="approvers-list">
${stage.approvers.map(approver => this._renderApprover(approver)).join('')}
</div>
` : ''}
</div>
</div>
`).join('');
}
_renderApprover(approver) {
const decisionClass = approver.decision === 'approve' ? 'approved' : 'rejected';
return `
<div class="approver">
<div class="approver-info">
<span class="approver-name">${approver.username}</span>
<span class="approval-date">
${new Date(approver.timestamp).toLocaleString()}
</span>
</div>
<div class="approver-decision ${decisionClass}">
${approver.decision === 'approve' ? 'Approved' : 'Rejected'}
</div>
${approver.comment ? `
<div class="approver-comment">${approver.comment}</div>
` : ''}
</div>
`;
}
_renderApprovalActions(currentStage) {
if (!this.canUserApprove(currentStage)) {
return '';
}
return `
<div class="approval-actions">
<textarea
class="approval-comment"
placeholder="Add your comments (optional)"
></textarea>
<div class="action-buttons">
<button class="reject-button" onclick="this.handleReject()">
Reject Changes
</button>
<button class="approve-button" onclick="this.handleApprove()">
Approve Changes
</button>
</div>
</div>
`;
}
_renderHistory() {
if (!this.changeset.approval_history?.length) {
return '';
}
return `
<div class="approval-history-list">
${this.changeset.approval_history.map(entry => `
<div class="history-entry">
<div class="entry-header">
<span class="entry-user">${entry.username}</span>
<span class="entry-date">
${new Date(entry.timestamp).toLocaleString()}
</span>
</div>
<div class="entry-action ${entry.action}">
${this._formatHistoryAction(entry.action)}
</div>
${entry.comment ? `
<div class="entry-comment">${entry.comment}</div>
` : ''}
</div>
`).join('')}
</div>
`;
}
_formatStageStatus(status) {
const statusMap = {
'pending': 'Pending',
'approved': 'Approved',
'rejected': 'Rejected'
};
return statusMap[status] || status;
}
_formatHistoryAction(action) {
const actionMap = {
'submit': 'Submitted for approval',
'approve': 'Approved changes',
'reject': 'Rejected changes',
'revert': 'Reverted approval'
};
return actionMap[action] || action;
}
canUserApprove(stage) {
if (!this.currentUser) return false;
// Check if user already approved
const alreadyApproved = stage.approvers.some(
a => a.user_id === this.currentUser.id
);
if (alreadyApproved) return false;
// Check if user has required role
return stage.required_roles.some(
role => this.currentUser.roles.includes(role)
);
}
async handleApprove() {
const commentEl = this.container.querySelector('.approval-comment');
const comment = commentEl ? commentEl.value.trim() : '';
try {
await this.onApprove(comment);
this.render();
} catch (error) {
console.error('Failed to approve:', error);
// Show error message
}
}
async handleReject() {
const commentEl = this.container.querySelector('.approval-comment');
const comment = commentEl ? commentEl.value.trim() : '';
try {
await this.onReject(comment);
this.render();
} catch (error) {
console.error('Failed to reject:', error);
// Show error message
}
}
attachEventListeners() {
// Add any additional event listeners if needed
}
}
export default ApprovalPanel;