feat(reader): note length counter and save guards in the reader UI
Oversized notes previously failed only at save time, surfacing the raw backend validator error to the reader. The reader now handles the limit inline and keeps oversized input editable: - Live character counter under both note textareas (annotations drawer "Add a note" and the selection popover note editor), formatted "12,345/100,000" — muted normally, red once over - Pasting is never truncated; the full text stays in the textarea so the reader can shrink it however they see fit - The three save actions (Add Note, Highlight with note, Save note) disable while over the limit, with a toast fallback in addNote / createHighlight / saveHighlightChanges so the guard holds even outside the disabled-button path - noteMaxLength mirrors the server-side cap (100,000, see internal/handlers/media.go) so client and server stay linked Also truncate note rows in the drawer list — highlight rows already truncated, but a long note previously stretched the drawer body. The resize handle needed no work: .reader-note-input already ships resize: vertical in the compiled CSS; style.css is rebuilt only for the new disabled:* utility classes.
This commit is contained in:
@@ -446,6 +446,10 @@ document.addEventListener("alpine:init", () => {
|
||||
noteItems: [] as { id: string; content: string; positionLabel: string }[],
|
||||
annotationsTab: "highlights" as string,
|
||||
newNoteText: "",
|
||||
// Mirrors the server-side validator cap on note content
|
||||
// (internal/handlers/media.go). Enforced client-side so oversized
|
||||
// input is caught before a doomed POST.
|
||||
noteMaxLength: 100000 as number,
|
||||
highlightColors: HIGHLIGHT_COLORS,
|
||||
searchOpen: false,
|
||||
searchQuery: "",
|
||||
@@ -1652,6 +1656,13 @@ document.addEventListener("alpine:init", () => {
|
||||
// The note editor writes into p.note live; "Highlight with note"
|
||||
// creates the highlight AND its note in one POST.
|
||||
const note = p.note.trim();
|
||||
if (this.noteOverLimit(note)) {
|
||||
showToast(
|
||||
`Note exceeds the ${this.noteMaxLength.toLocaleString()} character limit (${note.length.toLocaleString()}). Shrink it and try again.`,
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const pdfAnchor =
|
||||
p.pdfPage >= 0
|
||||
? JSON.stringify({ v: 1, page: p.pdfPage, rects: p.pdfRects })
|
||||
@@ -1721,6 +1732,13 @@ document.addEventListener("alpine:init", () => {
|
||||
const p = this.selectionPopover;
|
||||
const token = getToken();
|
||||
if (!token || !this.mediaItemId || !p.id) return;
|
||||
if (this.noteOverLimit(p.note)) {
|
||||
showToast(
|
||||
`Note exceeds the ${this.noteMaxLength.toLocaleString()} character limit (${p.note.length.toLocaleString()}). Shrink it and try again.`,
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const anchor =
|
||||
p.pdfPage >= 0
|
||||
? JSON.stringify({ v: 1, page: p.pdfPage, rects: p.pdfRects })
|
||||
@@ -1997,9 +2015,22 @@ document.addEventListener("alpine:init", () => {
|
||||
this.closeDrawers();
|
||||
}
|
||||
},
|
||||
noteOverLimit(text: string): boolean {
|
||||
return text.length > this.noteMaxLength;
|
||||
},
|
||||
noteCountLabel(text: string): string {
|
||||
return `${text.length.toLocaleString()}/${this.noteMaxLength.toLocaleString()}`;
|
||||
},
|
||||
async addNote(content: string) {
|
||||
const token = getToken();
|
||||
if (!token || !this.mediaItemId || !content.trim()) return;
|
||||
if (this.noteOverLimit(content)) {
|
||||
showToast(
|
||||
`Note exceeds the ${this.noteMaxLength.toLocaleString()} character limit (${content.length.toLocaleString()}). Shrink it and try again.`,
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`/api/media-items/${this.mediaItemId}/notes`,
|
||||
|
||||
Reference in New Issue
Block a user