+
Anexos
+
{
+ void addFiles(Array.from(event.target.files ?? []));
+ event.target.value = "";
+ }}
+ />
+
+ {attachments.length > 0 && (
+
+ {attachments.map((attachment) => (
+
+ {attachment.mimeType === "application/pdf" ? (
+
+ ) : (
+
+ )}
+
+
+ {attachment.fileName}
+
+
+ {formatBytes(attachment.fileSize)}
+
+
+
+ {!readonly && (
+
+ )}
+
+ ))}
+
+ )}
+
+ {pendingFiles.map((file, index) => (
+
+
+
+
{file.name}
+
+ Será enviado ao salvar
+
+
+
+
+ ))}
+
+ {!readonly && (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/features/notes/components/note-card.tsx b/src/features/notes/components/note-card.tsx
index a39ade2..117a234 100644
--- a/src/features/notes/components/note-card.tsx
+++ b/src/features/notes/components/note-card.tsx
@@ -2,6 +2,7 @@
import {
RiArchiveLine,
+ RiAttachment2,
RiCheckLine,
RiDeleteBin5Line,
RiFileList2Line,
@@ -87,11 +88,19 @@ export function NoteCard({
)}
- {isTask && (
-
- {note.description}
+
+
+ {note.description}
+
+ {note.attachments.length > 0 && (
+
undefined}
+ onPendingFilesChange={() => undefined}
+ readonly
+ />
+ )}
)}
diff --git a/src/features/notes/components/note-dialog.tsx b/src/features/notes/components/note-dialog.tsx
index a6b0a7e..e16aaff 100644
--- a/src/features/notes/components/note-dialog.tsx
+++ b/src/features/notes/components/note-dialog.tsx
@@ -15,6 +15,10 @@ import {
} from "react";
import { toast } from "sonner";
import { createNoteAction, updateNoteAction } from "@/features/notes/actions";
+import {
+ NoteAttachmentsField,
+ uploadNoteAttachment,
+} from "@/features/notes/components/note-attachments-field";
import { Button } from "@/shared/components/ui/button";
import { Checkbox } from "@/shared/components/ui/checkbox";
import {
@@ -34,6 +38,7 @@ import { useFormState } from "@/shared/hooks/use-form-state";
import { cn } from "@/shared/utils/ui";
import {
type Note,
+ type NoteAttachment,
type NoteFormValues,
sortTasksByStatus,
type Task,
@@ -46,6 +51,7 @@ interface NoteDialogProps {
note?: Note;
open?: boolean;
onOpenChange?: (open: boolean) => void;
+ attachmentMaxSizeMb?: number;
}
const MAX_TITLE = 30;
@@ -69,12 +75,16 @@ export function NoteDialog({
note,
open,
onOpenChange,
+ attachmentMaxSizeMb,
}: NoteDialogProps) {
const [isPending, startTransition] = useTransition();
const [errorMessage, setErrorMessage] = useState
(null);
const [newTaskText, setNewTaskText] = useState("");
const [editingTaskId, setEditingTaskId] = useState(null);
const [editingTaskText, setEditingTaskText] = useState("");
+ const [noteAttachments, setNoteAttachments] = useState([]);
+ const [pendingFiles, setPendingFiles] = useState([]);
+ const [isAttachmentPending, setIsAttachmentPending] = useState(false);
const titleRef = useRef(null);
const descRef = useRef(null);
@@ -99,6 +109,9 @@ export function NoteDialog({
setNewTaskText("");
setEditingTaskId(null);
setEditingTaskText("");
+ setNoteAttachments(note?.attachments ?? []);
+ setPendingFiles([]);
+ setIsAttachmentPending(false);
requestAnimationFrame(() => titleRef.current?.focus());
}
}, [dialogOpen, note, resetForm]);
@@ -137,12 +150,14 @@ export function NoteDialog({
const disableSubmit =
isPending ||
+ isAttachmentPending ||
onlySpaces ||
unchanged ||
invalidLen ||
Boolean(editingTaskId);
const handleOpenChange = (v: boolean) => {
+ if (!v && (isPending || isAttachmentPending)) return;
setDialogOpen(v);
if (!v) setErrorMessage(null);
};
@@ -252,7 +267,9 @@ export function NoteDialog({
}
startTransition(async () => {
- let result: { success: boolean; message?: string; error?: string };
+ let result:
+ | Awaited>
+ | Awaited>;
if (mode === "create") {
result = await createNoteAction(payload);
} else {
@@ -266,7 +283,31 @@ export function NoteDialog({
}
if (result.success) {
- toast.success(result.message);
+ if (mode === "create" && pendingFiles.length > 0) {
+ const noteId = "data" in result ? result.data?.noteId : undefined;
+ if (noteId) {
+ let failedUploads = 0;
+ for (const file of pendingFiles) {
+ const upload = await uploadNoteAttachment(noteId, file);
+ if (!upload.success) failedUploads += 1;
+ }
+ if (failedUploads > 0) {
+ toast.warning(
+ failedUploads === 1
+ ? "A nota foi salva, mas um anexo não pôde ser enviado."
+ : `A nota foi salva, mas ${failedUploads} anexos não puderam ser enviados.`,
+ );
+ } else {
+ toast.success(
+ pendingFiles.length === 1
+ ? "Anotação e anexo salvos."
+ : "Anotação e anexos salvos.",
+ );
+ }
+ }
+ } else {
+ toast.success(result.message);
+ }
setDialogOpen(false);
return;
}
@@ -355,35 +396,48 @@ export function NoteDialog({
{isNote && (
-
-
-
-
MAX_DESC
- ? "text-destructive"
- : "text-muted-foreground",
- )}
- >
- {descCount}/{MAX_DESC}
-
+
+
+
+
+ MAX_DESC
+ ? "text-destructive"
+ : "text-muted-foreground",
+ )}
+ >
+ {descCount}/{MAX_DESC}
+
+
+
-
)}
@@ -517,7 +571,7 @@ export function NoteDialog({
type="button"
variant="outline"
onClick={() => handleOpenChange(false)}
- disabled={isPending}
+ disabled={isPending || isAttachmentPending}
>
Cancelar
diff --git a/src/features/notes/components/notes-page.tsx b/src/features/notes/components/notes-page.tsx
index dbf1183..03d1b04 100644
--- a/src/features/notes/components/notes-page.tsx
+++ b/src/features/notes/components/notes-page.tsx
@@ -22,9 +22,14 @@ import type { Note } from "./types";
interface NotesPageProps {
notes: Note[];
archivedNotes: Note[];
+ attachmentMaxSizeMb?: number;
}
-export function NotesPage({ notes, archivedNotes }: NotesPageProps) {
+export function NotesPage({
+ notes,
+ archivedNotes,
+ attachmentMaxSizeMb,
+}: NotesPageProps) {
const [activeTab, setActiveTab] = useState("ativas");
const [createOpen, setCreateOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false);
@@ -192,6 +197,7 @@ export function NotesPage({ notes, archivedNotes }: NotesPageProps) {
mode="create"
open={createOpen}
onOpenChange={handleCreateOpenChange}
+ attachmentMaxSizeMb={attachmentMaxSizeMb}
trigger={