allow to delete comment in a ticket
This commit is contained in:
parent
6f1d35016c
commit
53f0363da6
|
|
@ -2501,6 +2501,42 @@ public class Controller : ControllerBase
|
|||
return comment;
|
||||
}
|
||||
|
||||
[HttpDelete(nameof(DeleteTicketComment))]
|
||||
public async Task<ActionResult> DeleteTicketComment(Int64 id, Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
var comment = Db.TicketComments.FirstOrDefault(c => c.Id == id);
|
||||
if (comment is null) return NotFound();
|
||||
|
||||
// Author-only; AI comments cannot be deleted via this endpoint.
|
||||
if (comment.AuthorType != (Int32)CommentAuthorType.Human) return Unauthorized();
|
||||
if (comment.AuthorId != user.Id) return Unauthorized();
|
||||
|
||||
// Clean up S3 objects for documents attached to this comment before DB delete.
|
||||
var s3Keys = Db.Documents
|
||||
.Where(d => d.TicketCommentId == comment.Id)
|
||||
.Select(d => d.S3Key)
|
||||
.ToList();
|
||||
if (s3Keys.Count > 0)
|
||||
{
|
||||
try { await DocumentBucket.DeleteObjects(s3Keys); }
|
||||
catch (Exception ex) { Console.WriteLine($"[Documents] S3 cleanup on comment delete failed: {ex.Message}"); }
|
||||
}
|
||||
|
||||
if (!Db.Delete(comment)) return StatusCode(500, "Failed to delete comment.");
|
||||
|
||||
var ticket = Db.GetTicketById(comment.TicketId);
|
||||
if (ticket is not null)
|
||||
{
|
||||
ticket.UpdatedAt = DateTime.UtcNow;
|
||||
Db.Update(ticket);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetTicketDetail))]
|
||||
public ActionResult<Object> GetTicketDetail(Int64 id, Token authToken)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ public static partial class Db
|
|||
// Ticket system tables
|
||||
public static TableQuery<Ticket> Tickets => Connection.Table<Ticket>();
|
||||
public static TableQuery<TicketComment> TicketComments => Connection.Table<TicketComment>();
|
||||
public static TableQuery<TicketCommentMention> TicketCommentMentions => Connection.Table<TicketCommentMention>();
|
||||
public static TableQuery<TicketAiDiagnosis> TicketAiDiagnoses => Connection.Table<TicketAiDiagnosis>();
|
||||
public static TableQuery<TicketTimelineEvent> TicketTimelineEvents => Connection.Table<TicketTimelineEvent>();
|
||||
|
||||
|
|
|
|||
|
|
@ -129,10 +129,13 @@ public static partial class Db
|
|||
.Select(t => t.Id).ToList();
|
||||
foreach (var tid in ticketIds)
|
||||
{
|
||||
// Delete documents attached to ticket comments
|
||||
// Delete documents and mentions attached to ticket comments
|
||||
var tCommentIds = TicketComments.Where(c => c.TicketId == tid).Select(c => c.Id).ToList();
|
||||
foreach (var cid in tCommentIds)
|
||||
Documents.Delete(d => d.TicketCommentId == cid);
|
||||
{
|
||||
Documents .Delete(d => d.TicketCommentId == cid);
|
||||
TicketCommentMentions .Delete(m => m.CommentId == cid);
|
||||
}
|
||||
|
||||
// Delete documents attached directly to the ticket
|
||||
Documents .Delete(d => d.TicketId == tid);
|
||||
|
|
@ -231,13 +234,16 @@ public static partial class Db
|
|||
|
||||
Boolean DeleteTicketAndChildren()
|
||||
{
|
||||
// Delete documents attached to comments on this ticket
|
||||
// Delete documents and mentions attached to comments on this ticket
|
||||
var commentIds = TicketComments
|
||||
.Where(c => c.TicketId == ticket.Id)
|
||||
.Select(c => c.Id)
|
||||
.ToList();
|
||||
foreach (var cid in commentIds)
|
||||
Documents.Delete(d => d.TicketCommentId == cid);
|
||||
{
|
||||
Documents .Delete(d => d.TicketCommentId == cid);
|
||||
TicketCommentMentions .Delete(m => m.CommentId == cid);
|
||||
}
|
||||
|
||||
// Delete documents attached directly to the ticket
|
||||
Documents .Delete(d => d.TicketId == ticket.Id);
|
||||
|
|
@ -256,6 +262,21 @@ public static partial class Db
|
|||
return success;
|
||||
}
|
||||
|
||||
public static Boolean Delete(TicketComment comment)
|
||||
{
|
||||
var deleteSuccess = RunTransaction(DeleteCommentAndChildren);
|
||||
if (deleteSuccess) Backup();
|
||||
return deleteSuccess;
|
||||
|
||||
Boolean DeleteCommentAndChildren()
|
||||
{
|
||||
// Document rows attached to this comment (S3 cleanup happens in the controller)
|
||||
Documents .Delete(d => d.TicketCommentId == comment.Id);
|
||||
TicketCommentMentions .Delete(m => m.CommentId == comment.Id);
|
||||
return TicketComments .Delete(c => c.Id == comment.Id) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<String> GetS3KeysForTicketDocuments(Int64 ticketId)
|
||||
{
|
||||
// Get documents attached directly to the ticket
|
||||
|
|
|
|||
|
|
@ -9,18 +9,27 @@ import {
|
|||
CardHeader,
|
||||
Chip,
|
||||
ClickAwayListener,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
IconButton,
|
||||
LinearProgress,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Paper,
|
||||
Popper,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import PersonIcon from '@mui/icons-material/Person';
|
||||
import SmartToyIcon from '@mui/icons-material/SmartToy';
|
||||
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import axiosConfig from 'src/Resources/axiosConfig';
|
||||
import { AdminUser, CommentAuthorType, TicketComment } from 'src/interfaces/TicketTypes';
|
||||
|
|
@ -49,6 +58,8 @@ function CommentThread({
|
|||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [editBody, setEditBody] = useState('');
|
||||
const [savingEdit, setSavingEdit] = useState(false);
|
||||
const [deleteCandidateId, setDeleteCandidateId] = useState<number | null>(null);
|
||||
const [deletingId, setDeletingId] = useState<number | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
|
@ -147,6 +158,23 @@ function CommentThread({
|
|||
setEditBody('');
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (deleteCandidateId == null) return;
|
||||
const id = deleteCandidateId;
|
||||
setDeletingId(id);
|
||||
try {
|
||||
await axiosConfig.delete('/DeleteTicketComment', { params: { id } });
|
||||
setDeleteCandidateId(null);
|
||||
if (editingId === id) {
|
||||
setEditingId(null);
|
||||
setEditBody('');
|
||||
}
|
||||
onCommentAdded();
|
||||
} finally {
|
||||
setDeletingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const saveEdit = async (commentId: number) => {
|
||||
if (!editBody.trim()) return;
|
||||
setSavingEdit(true);
|
||||
|
|
@ -274,14 +302,28 @@ function CommentThread({
|
|||
</Typography>
|
||||
)}
|
||||
{canEdit && !isEditing && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
sx={{ minWidth: 0, p: 0, ml: 'auto', textTransform: 'none' }}
|
||||
onClick={() => startEdit(comment)}
|
||||
>
|
||||
<FormattedMessage id="edit" defaultMessage="Edit" />
|
||||
</Button>
|
||||
<Box sx={{ ml: 'auto', display: 'flex', gap: 0.5 }}>
|
||||
<Tooltip title={intl.formatMessage({ id: 'edit', defaultMessage: 'Edit' })}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => startEdit(comment)}
|
||||
aria-label={intl.formatMessage({ id: 'edit', defaultMessage: 'Edit' })}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={intl.formatMessage({ id: 'delete', defaultMessage: 'Delete' })}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => setDeleteCandidateId(comment.id)}
|
||||
disabled={deletingId === comment.id}
|
||||
aria-label={intl.formatMessage({ id: 'delete', defaultMessage: 'Delete' })}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{isEditing ? (
|
||||
|
|
@ -422,6 +464,38 @@ function CommentThread({
|
|||
{uploading && <LinearProgress />}
|
||||
</Box>
|
||||
</CardContent>
|
||||
<Dialog
|
||||
open={deleteCandidateId !== null}
|
||||
onClose={() => deletingId === null && setDeleteCandidateId(null)}
|
||||
>
|
||||
<DialogTitle>
|
||||
<FormattedMessage id="deleteComment" defaultMessage="Delete comment" />
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<FormattedMessage
|
||||
id="deleteCommentConfirm"
|
||||
defaultMessage="This will permanently delete the comment and any attachments. Continue?"
|
||||
/>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => setDeleteCandidateId(null)}
|
||||
disabled={deletingId !== null}
|
||||
>
|
||||
<FormattedMessage id="cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={confirmDelete}
|
||||
color="error"
|
||||
variant="contained"
|
||||
disabled={deletingId !== null}
|
||||
>
|
||||
<FormattedMessage id="delete" defaultMessage="Delete" />
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -573,6 +573,8 @@
|
|||
"comments": "Kommentare",
|
||||
"noComments": "Noch keine Kommentare.",
|
||||
"commentEdited": "(bearbeitet {time})",
|
||||
"deleteComment": "Kommentar löschen",
|
||||
"deleteCommentConfirm": "Der Kommentar und alle Anhänge werden dauerhaft gelöscht. Fortfahren?",
|
||||
"commentMarkdownHint": "Markdown: **fett**, #, ##",
|
||||
"commentFormatBold": "Fett",
|
||||
"commentFormatH1": "Überschrift 1",
|
||||
|
|
|
|||
|
|
@ -321,6 +321,8 @@
|
|||
"comments": "Comments",
|
||||
"noComments": "No comments yet.",
|
||||
"commentEdited": "(edited {time})",
|
||||
"deleteComment": "Delete comment",
|
||||
"deleteCommentConfirm": "This will permanently delete the comment and any attachments. Continue?",
|
||||
"commentMarkdownHint": "Markdown: **bold**, #, ##",
|
||||
"commentFormatBold": "Bold",
|
||||
"commentFormatH1": "Heading 1",
|
||||
|
|
|
|||
|
|
@ -573,6 +573,8 @@
|
|||
"comments": "Commentaires",
|
||||
"noComments": "Aucun commentaire pour le moment.",
|
||||
"commentEdited": "(modifié {time})",
|
||||
"deleteComment": "Supprimer le commentaire",
|
||||
"deleteCommentConfirm": "Le commentaire et toutes les pièces jointes seront définitivement supprimés. Continuer ?",
|
||||
"commentMarkdownHint": "Markdown : **gras**, #, ##",
|
||||
"commentFormatBold": "Gras",
|
||||
"commentFormatH1": "Titre 1",
|
||||
|
|
|
|||
|
|
@ -573,6 +573,8 @@
|
|||
"comments": "Commenti",
|
||||
"noComments": "Nessun commento ancora.",
|
||||
"commentEdited": "(modificato {time})",
|
||||
"deleteComment": "Elimina commento",
|
||||
"deleteCommentConfirm": "Il commento e tutti gli allegati saranno eliminati definitivamente. Continuare?",
|
||||
"commentMarkdownHint": "Markdown: **grassetto**, #, ##",
|
||||
"commentFormatBold": "Grassetto",
|
||||
"commentFormatH1": "Titolo 1",
|
||||
|
|
|
|||
Loading…
Reference in New Issue