feat: edit and delete owned comments (Closes #575)
This commit is contained in:
1 parent
43f5833f7f
commit
8e357a097b
14 files changed
+634
-2
No files matched your search
@@ -0,0 +1,103 @@
|
||||
function createCommentActions({ fetchJson, getLogin, confirmDelete = () => false }) {
|
||||
const encodedRepository = repository => String(repository || '').split('/')
|
||||
.map(encodeURIComponent).join('/');
|
||||
|
||||
function pathFor(context, commentId) {
|
||||
const item = context?.item || {};
|
||||
if (context?.kind === 'update') {
|
||||
return 'api/v1/notifications/' + encodeURIComponent(item.notification_id) +
|
||||
'/comments/' + encodeURIComponent(commentId);
|
||||
}
|
||||
if (!['issue', 'pull'].includes(context?.kind)) throw new Error('Comment conversation is unavailable.');
|
||||
return 'api/v1/repos/' + encodedRepository(item.repository) + '/' +
|
||||
(context.kind === 'pull' ? 'pulls/' : 'issues/') + encodeURIComponent(item.number) +
|
||||
'/comments/' + encodeURIComponent(commentId);
|
||||
}
|
||||
|
||||
const controller = {
|
||||
isOwned(comment) {
|
||||
const login = String(getLogin() || '').trim();
|
||||
return Boolean(login && comment && comment.author === login);
|
||||
},
|
||||
async edit(context, pager, commentId, body) {
|
||||
const draft = String(body || '').trim();
|
||||
if (!draft) throw new Error('Comment must not be blank.');
|
||||
const comment = await fetchJson(pathFor(context, commentId), {
|
||||
method: 'PATCH',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ body: draft }),
|
||||
});
|
||||
pager.replace(comment);
|
||||
return pager.snapshot();
|
||||
},
|
||||
async remove(context, pager, commentId) {
|
||||
if (!confirmDelete('Delete this comment permanently?')) return null;
|
||||
const result = await fetchJson(pathFor(context, commentId), {
|
||||
method: 'DELETE', headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!result?.deleted || Number(result.id) !== Number(commentId)) {
|
||||
throw new Error('Comment deletion was not confirmed.');
|
||||
}
|
||||
pager.remove(commentId);
|
||||
return pager.snapshot();
|
||||
},
|
||||
actionHtml(comment) {
|
||||
return controller.isOwned(comment) ?
|
||||
'<div class="comment-owned-actions" aria-label="Your comment actions">' +
|
||||
'<button type="button" data-comment-action="edit">Edit</button>' +
|
||||
'<button type="button" data-comment-action="delete">Delete</button></div>' : '';
|
||||
},
|
||||
wire({ root, getSurface, isOffline, escapeHtml }) {
|
||||
root.addEventListener('click', async event => {
|
||||
const button = event.target.closest('[data-comment-action]');
|
||||
if (!button) return;
|
||||
const card = button.closest('.issue-comment');
|
||||
const commentId = Number(card?.dataset.commentId);
|
||||
const surface = getSurface();
|
||||
const comment = surface.pager?.snapshot().comments.find(item => item.id === commentId);
|
||||
if (!comment || !controller.isOwned(comment)) return;
|
||||
if (isOffline()) {
|
||||
surface.status.textContent = 'Reconnect to edit or delete this comment.';
|
||||
return;
|
||||
}
|
||||
if (button.dataset.commentAction === 'delete') {
|
||||
try {
|
||||
const state = await controller.remove(surface.context, surface.pager, commentId);
|
||||
if (state) {
|
||||
surface.render(state);
|
||||
surface.status.textContent = 'Comment deleted.';
|
||||
}
|
||||
} catch (error) {
|
||||
surface.status.textContent = error.message || 'Comment deletion failed. Retry or open it in Gitea.';
|
||||
}
|
||||
return;
|
||||
}
|
||||
card.innerHTML = '<div class="small">Editing your comment</div>' +
|
||||
'<textarea class="comment-edit-textarea" maxlength="10000" aria-label="Edit comment">' +
|
||||
escapeHtml(comment.body || '') + '</textarea>' +
|
||||
'<div class="comment-owned-actions"><button type="button" data-comment-edit-save>Save</button>' +
|
||||
'<button type="button" data-comment-edit-cancel>Cancel</button></div>';
|
||||
const textarea = card.querySelector('.comment-edit-textarea');
|
||||
textarea.focus();
|
||||
card.querySelector('[data-comment-edit-cancel]').addEventListener('click', () => surface.render(surface.pager.snapshot()));
|
||||
card.querySelector('[data-comment-edit-save]').addEventListener('click', async saveEvent => {
|
||||
const save = saveEvent.currentTarget;
|
||||
save.disabled = true;
|
||||
surface.status.textContent = 'Saving comment…';
|
||||
try {
|
||||
const state = await controller.edit(surface.context, surface.pager, commentId, textarea.value);
|
||||
surface.render(state);
|
||||
surface.status.textContent = 'Comment updated.';
|
||||
} catch (error) {
|
||||
save.disabled = false;
|
||||
surface.status.textContent = error.message || 'Comment update failed. Your edit is safe; retry or open it in Gitea.';
|
||||
textarea.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
return controller;
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = createCommentActions;
|
||||
@@ -63,6 +63,21 @@ function createConversationPager({ loadPage }) {
|
||||
}
|
||||
return snapshot();
|
||||
},
|
||||
replace(comment) {
|
||||
if (!comment || !Number.isInteger(comment.id)) return snapshot();
|
||||
state = {
|
||||
...state,
|
||||
comments: state.comments.map(existing => existing.id === comment.id ? { ...comment } : existing),
|
||||
};
|
||||
return snapshot();
|
||||
},
|
||||
remove(commentId) {
|
||||
const comments = state.comments.filter(comment => comment.id !== commentId);
|
||||
if (comments.length !== state.comments.length) {
|
||||
state = { ...state, comments, total: Math.max(0, state.total - 1) };
|
||||
}
|
||||
return snapshot();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@ button { background: linear-gradient(180deg,#1f3a5f,#15324d); border:1px solid #
|
||||
.security-event strong, .security-event span { display:block; overflow-wrap:anywhere; }
|
||||
#load-more-security-activity { width:100%; min-height:44px; }
|
||||
button:hover { filter: brightness(1.15); }
|
||||
.issue-comment { min-width:0; overflow-wrap:anywhere; }
|
||||
.comment-owned-actions { display:flex; gap:8px; flex-wrap:wrap; margin:8px 0; }
|
||||
.comment-owned-actions button { min-height:44px; min-width:72px; }
|
||||
.comment-edit-textarea { box-sizing:border-box; display:block; width:100%; max-width:100%; min-height:132px; resize:vertical; overflow-wrap:anywhere; }
|
||||
.panel { border: 1px solid #1b2d45; border-radius: 14px; padding: 12px; background: rgba(11,21,38,.92); }
|
||||
.panel > summary { cursor: pointer; list-style-position: inside; }
|
||||
.panel > summary h2 { display: inline-block; margin-left: 4px; }
|
||||
|
||||
+45
-1
@@ -236,6 +236,8 @@
|
||||
return payload;
|
||||
}
|
||||
|
||||
let commentActions = { isOwned: () => false, wire: () => {} };
|
||||
|
||||
function loadMentionCandidates(repository, query) {
|
||||
return fetchReviewJson(
|
||||
'api/v1/repos/' + repository + '/mention-candidates?q=' + encodeURIComponent(query),
|
||||
@@ -413,6 +415,21 @@
|
||||
text: shareParams.get('text') || '',
|
||||
url: shareParams.get('url') || '',
|
||||
};
|
||||
const commentActionFeatures = createFeatureLoader({
|
||||
document,
|
||||
urls: {
|
||||
'comment-actions': document.querySelector('meta[name="stackchain-feature-comment-actions"]')?.content || '',
|
||||
},
|
||||
});
|
||||
await commentActionFeatures.run('comment-actions', {
|
||||
status: qs('#my-work-action-status'), retryLabel:'Reload to retry comment actions.',
|
||||
}, () => {
|
||||
commentActions = createCommentActions({
|
||||
fetchJson: fetchReviewJson,
|
||||
getLogin: () => confirmedOwnerLogin,
|
||||
confirmDelete: message => window.confirm(message),
|
||||
});
|
||||
});
|
||||
const issueCaptureFeatures = createFeatureLoader({
|
||||
document,
|
||||
urls: {
|
||||
@@ -2346,10 +2363,11 @@
|
||||
}
|
||||
|
||||
function renderIssueComment(comment) {
|
||||
const actions = commentActions.actionHtml?.(comment) || '';
|
||||
return '<div class="issue-comment" data-comment-id="' + Number(comment.id || 0) + '"><div class="small">' +
|
||||
escapeHtml(comment.author || 'Unknown author') +
|
||||
(comment.created_at ? ' · ' + escapeHtml(fmt(comment.created_at)) : '') +
|
||||
'</div><div class="issue-sheet-content markdown-content">' +
|
||||
'</div>' + actions + '<div class="issue-sheet-content markdown-content">' +
|
||||
renderMarkdown(comment.body || 'No comment body provided.') + '</div></div>';
|
||||
}
|
||||
|
||||
@@ -2381,6 +2399,32 @@
|
||||
comments.length + ' of ' + Math.max(state.total || 0, comments.length) + ' messages loaded.' : 'No comments yet.';
|
||||
}
|
||||
|
||||
function commentSurface(selector) {
|
||||
if (selector === '#issue-comments') return {
|
||||
context:{kind:'issue',item:selectedIssue}, pager:issueConversation,
|
||||
render:renderIssueConversation, status:qs('#issue-sheet-status'),
|
||||
};
|
||||
if (selector === '#pull-comments') return {
|
||||
context:{kind:'pull',item:selectedPull}, pager:pullConversation,
|
||||
render:renderPullConversation, status:qs('#pull-sheet-status'),
|
||||
};
|
||||
return {
|
||||
context:{kind:'update',item:selectedUpdate}, pager:notificationReader.commentPager(),
|
||||
render:renderUpdateConversation, status:qs('#update-sheet-status'),
|
||||
};
|
||||
}
|
||||
|
||||
function wireCommentActions(selector) {
|
||||
commentActions.wire({
|
||||
root:qs(selector), getSurface:()=>commentSurface(selector),
|
||||
isOffline:()=>offlineWorkMode || navigator.onLine === false, escapeHtml,
|
||||
});
|
||||
}
|
||||
|
||||
wireCommentActions('#issue-comments');
|
||||
wireCommentActions('#pull-comments');
|
||||
wireCommentActions('#update-comments');
|
||||
|
||||
function renderIssueLabelEditor(item, confirmedNames, labels) {
|
||||
const list = qs('#issue-label-list');
|
||||
const status = qs('#issue-label-status');
|
||||
|
||||
@@ -811,6 +811,7 @@
|
||||
<script src="static/later-picker.js"></script>
|
||||
<script src="static/pick-work.js"></script>
|
||||
<script src="static/conversation.js"></script>
|
||||
<script src="static/comment-actions.js"></script>
|
||||
<script src="static/issue-attachment.js"></script>
|
||||
<script src="static/issue-sheet.js"></script>
|
||||
<script src="static/create-issue-sheet.js"></script>
|
||||
|
||||
@@ -395,6 +395,9 @@ function createNotificationReader({
|
||||
|
||||
return {
|
||||
open,
|
||||
commentPager() {
|
||||
return conversationPager;
|
||||
},
|
||||
appendReply(comment) {
|
||||
if (!conversationPager) return false;
|
||||
onConversation(conversationPager.append(comment));
|
||||
|
||||
@@ -46,6 +46,7 @@ const SHELL = [
|
||||
BASE + 'static/later-picker.js',
|
||||
BASE + 'static/pick-work.js',
|
||||
BASE + 'static/conversation.js',
|
||||
BASE + 'static/comment-actions.js',
|
||||
BASE + 'static/issue-attachment.js',
|
||||
BASE + 'static/issue-sheet.js',
|
||||
BASE + 'static/create-issue-sheet.js',
|
||||
|
||||
Reference in new issue
Block a user