feat: attachments on tickets and comments
Files are uploaded over REST, stored on disk (a volume in Docker) and recorded on the ticket as events; the bytes are written first, so no attachment ever points at a missing file. Only images, video, audio, PDF and plain text are shown inline, everything with nosniff and a sandbox CSP. The web shows thumbnails, uploads by drag and drop or by pasting a screenshot, and attaches files to new comments. Also adds `nx run api:contracts`, which writes schema.gql and openapi.json without starting the API or any database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import { inject, Injectable, Provider } from '@angular/core';
|
||||
import { injectDispatch } from '@ngrx/signals/events';
|
||||
import {
|
||||
Attachment,
|
||||
AttachmentsGateway,
|
||||
IssueLink,
|
||||
IssueLinkType,
|
||||
UpdateIssueInput,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
import { Observable } from 'rxjs';
|
||||
import { issuePageEvents } from '../events/issue.events';
|
||||
import { IssueStore } from '../stores/issue.store';
|
||||
|
||||
@@ -16,6 +19,7 @@ import { IssueStore } from '../stores/issue.store';
|
||||
export class IssueFacade {
|
||||
private readonly store = inject(IssueStore);
|
||||
private readonly dispatch = injectDispatch(issuePageEvents);
|
||||
private readonly files = inject(AttachmentsGateway);
|
||||
|
||||
readonly issue = this.store.issue;
|
||||
readonly comments = this.store.comments;
|
||||
@@ -74,12 +78,33 @@ export class IssueFacade {
|
||||
);
|
||||
}
|
||||
|
||||
addComment(body: string): void {
|
||||
/** `files` are attached under the new comment. */
|
||||
addComment(body: string, files: File[] = []): void {
|
||||
this.withIssue((issueId) =>
|
||||
this.dispatch.commentAddRequested({ issueId, body }),
|
||||
this.dispatch.commentAddRequested({ issueId, body, files }),
|
||||
);
|
||||
}
|
||||
|
||||
/** Attaches files to the ticket (or under one of its comments). */
|
||||
attach(files: File[], commentId: string | null = null): void {
|
||||
if (!files.length) return;
|
||||
this.withIssue((issueId) =>
|
||||
this.dispatch.attachRequested({ issueId, files, commentId }),
|
||||
);
|
||||
}
|
||||
|
||||
removeAttachment(attachmentId: string): void {
|
||||
this.withIssue((issueId) =>
|
||||
this.dispatch.attachmentRemoveRequested({ issueId, attachmentId }),
|
||||
);
|
||||
}
|
||||
|
||||
/** The file's bytes, to show an image or save the file. */
|
||||
fileOf(attachment: Attachment): Observable<Blob> {
|
||||
const issueId = this.store.issue()?.id ?? '';
|
||||
return this.files.download(issueId, attachment);
|
||||
}
|
||||
|
||||
editComment(commentId: string, body: string): void {
|
||||
this.dispatch.commentEditRequested({ commentId, body });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user