feat(attachments): update TypeScript configuration and enhance attachment handling
- Upgraded TypeScript target and library to ES2023 for improved language features. - Enhanced TeamProvisioningService to better handle plain text attachments with UTF-8 validation. - Improved TaskCommentInput to differentiate between image and non-image file previews, including a new FileIcon for non-image files. - Refactored attachment handling in useAttachments and useComposerDraft hooks to simplify file processing. - Added validation for empty files in attachmentUtils to improve user feedback on unsupported uploads.
This commit is contained in:
parent
b20b69066e
commit
5cf9751b41
6 changed files with 80 additions and 48 deletions
|
|
@ -3624,18 +3624,33 @@ export class TeamProvisioningService {
|
|||
media_type: 'application/pdf',
|
||||
data: att.data,
|
||||
},
|
||||
title: att.filename,
|
||||
});
|
||||
} else if (att.mimeType === 'text/plain') {
|
||||
// Text file → document block with text source (decode base64 → UTF-8)
|
||||
contentBlocks.push({
|
||||
type: 'document',
|
||||
source: {
|
||||
type: 'text',
|
||||
media_type: 'text/plain',
|
||||
data: Buffer.from(att.data, 'base64').toString('utf-8'),
|
||||
},
|
||||
title: att.filename,
|
||||
});
|
||||
const decoded = Buffer.from(att.data, 'base64').toString('utf-8');
|
||||
if (decoded.includes('\uFFFD')) {
|
||||
// Non-UTF-8 file: fallback to base64 document to avoid garbled content
|
||||
contentBlocks.push({
|
||||
type: 'document',
|
||||
source: {
|
||||
type: 'base64',
|
||||
media_type: 'text/plain',
|
||||
data: att.data,
|
||||
},
|
||||
title: att.filename,
|
||||
});
|
||||
} else {
|
||||
contentBlocks.push({
|
||||
type: 'document',
|
||||
source: {
|
||||
type: 'text',
|
||||
media_type: 'text/plain',
|
||||
data: decoded,
|
||||
},
|
||||
title: att.filename,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Image (default) → image block
|
||||
contentBlocks.push({
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useCallback, useMemo, useRef, useState } from 'react';
|
|||
|
||||
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
||||
import { ImageLightbox } from '@renderer/components/team/attachments/ImageLightbox';
|
||||
import { FileIcon } from '@renderer/components/team/editor/FileIcon';
|
||||
import { MemberBadge } from '@renderer/components/team/MemberBadge';
|
||||
import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
||||
|
|
@ -19,7 +20,7 @@ import {
|
|||
stripEncodedTaskReferenceMetadata,
|
||||
} from '@renderer/utils/taskReferenceUtils';
|
||||
import { MAX_TEXT_LENGTH } from '@shared/constants';
|
||||
import { categorizeFile, getEffectiveMimeType } from '@shared/constants/attachments';
|
||||
import { categorizeFile, getEffectiveMimeType, isImageMime } from '@shared/constants/attachments';
|
||||
import { Mic, Paperclip, Send, Trash2, X } from 'lucide-react';
|
||||
|
||||
import type { MentionSuggestion } from '@renderer/types/mention';
|
||||
|
|
@ -254,25 +255,40 @@ export const TaskCommentInput = ({
|
|||
{/* Pending attachment previews */}
|
||||
{pendingAttachments.length > 0 ? (
|
||||
<div className="mb-2 flex flex-wrap gap-1.5">
|
||||
{pendingAttachments.map((att, idx) => (
|
||||
<div
|
||||
key={att.id}
|
||||
className="group relative size-14 cursor-pointer overflow-hidden rounded border border-[var(--color-border)] bg-[var(--color-surface)] transition-colors hover:border-[var(--color-border-emphasis)]"
|
||||
onClick={() => setLightboxIndex(idx)}
|
||||
>
|
||||
<img src={att.previewUrl} alt={att.filename} className="size-full object-cover" />
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-0.5 top-0.5 rounded bg-black/60 p-0.5 text-white opacity-0 transition-opacity hover:bg-red-600 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeAttachment(att.id);
|
||||
}}
|
||||
{pendingAttachments.map((att, idx) => {
|
||||
const isImage = isImageMime(att.mimeType);
|
||||
const lightboxIdx = isImage
|
||||
? pendingAttachments.slice(0, idx).filter((a) => isImageMime(a.mimeType)).length
|
||||
: -1;
|
||||
return (
|
||||
<div
|
||||
key={att.id}
|
||||
className="group relative size-14 cursor-pointer overflow-hidden rounded border border-[var(--color-border)] bg-[var(--color-surface)] transition-colors hover:border-[var(--color-border-emphasis)]"
|
||||
onClick={isImage ? () => setLightboxIndex(lightboxIdx) : undefined}
|
||||
>
|
||||
<Trash2 size={8} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{isImage ? (
|
||||
<img src={att.previewUrl} alt={att.filename} className="size-full object-cover" />
|
||||
) : (
|
||||
<div className="flex size-full flex-col items-center justify-center gap-0.5">
|
||||
<FileIcon fileName={att.filename} className="size-5" />
|
||||
<span className="max-w-[48px] truncate text-[7px] text-[var(--color-text-muted)]">
|
||||
{att.filename}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-0.5 top-0.5 rounded bg-black/60 p-0.5 text-white opacity-0 transition-opacity hover:bg-red-600 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeAttachment(att.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={8} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
|
@ -280,13 +296,15 @@ export const TaskCommentInput = ({
|
|||
<ImageLightbox
|
||||
open
|
||||
onClose={() => setLightboxIndex(null)}
|
||||
slides={pendingAttachments.map((att) => ({
|
||||
src: att.previewUrl,
|
||||
alt: att.filename,
|
||||
title: att.filename,
|
||||
}))}
|
||||
slides={pendingAttachments
|
||||
.filter((att) => isImageMime(att.mimeType))
|
||||
.map((att) => ({
|
||||
src: att.previewUrl,
|
||||
alt: att.filename,
|
||||
title: att.filename,
|
||||
}))}
|
||||
index={lightboxIndex}
|
||||
showCounter={pendingAttachments.length > 1}
|
||||
showCounter={pendingAttachments.filter((a) => isImageMime(a.mimeType)).length > 1}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
|
|
|||
|
|
@ -270,19 +270,17 @@ export function useAttachments(options?: UseAttachmentsOptions): UseAttachmentsR
|
|||
const items = event.clipboardData?.items;
|
||||
if (!items) return;
|
||||
|
||||
const supportedFiles: File[] = [];
|
||||
const pastedFiles: File[] = [];
|
||||
for (const item of Array.from(items)) {
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file && categorizeFile(file) !== 'unsupported') {
|
||||
supportedFiles.push(file);
|
||||
}
|
||||
if (file) pastedFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (supportedFiles.length > 0) {
|
||||
if (pastedFiles.length > 0) {
|
||||
event.preventDefault();
|
||||
void addFiles(supportedFiles);
|
||||
void addFiles(pastedFiles);
|
||||
}
|
||||
},
|
||||
[addFiles]
|
||||
|
|
|
|||
|
|
@ -420,19 +420,17 @@ export function useComposerDraft(teamName: string): UseComposerDraftResult {
|
|||
const items = event.clipboardData?.items;
|
||||
if (!items) return;
|
||||
|
||||
const supportedFiles: File[] = [];
|
||||
const pastedFiles: File[] = [];
|
||||
for (const item of Array.from(items)) {
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file && categorizeFile(file) !== 'unsupported') {
|
||||
supportedFiles.push(file);
|
||||
}
|
||||
if (file) pastedFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (supportedFiles.length > 0) {
|
||||
if (pastedFiles.length > 0) {
|
||||
event.preventDefault();
|
||||
void addFiles(supportedFiles);
|
||||
void addFiles(pastedFiles);
|
||||
}
|
||||
},
|
||||
[addFiles]
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ export function validateAttachment(file: File): { valid: true } | { valid: false
|
|||
if (cat === 'unsupported') {
|
||||
return { valid: false, error: `Unsupported file type: ${file.name}` };
|
||||
}
|
||||
if (file.size === 0) {
|
||||
return { valid: false, error: `File "${file.name}" is empty` };
|
||||
}
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
return { valid: false, error: `File "${file.name}" exceeds 10MB limit` };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"target": "ES2023",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
|
|||
Loading…
Reference in a new issue