v3: keep pasted description images out of the dump kind and the card teasers
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 47s

The dump modal's window-level paste listener looked at the clipboard files
before it looked at the event target, so an image pasted into the "Why?"
field was claimed twice: the editor uploaded it as an inline attachment,
and the modal switched itself to file mode with that same image as the
dump. Writing a URL dump with an illustrated description was impossible.
The target guard that already existed for text pastes now runs first and
covers files too — fields own their paste, and the URL input keeps its own
handler for the image-to-file-dump shortcut. Pasting onto the modal body,
where nothing else is listening, still starts a file dump.

Card teasers are line-clamped boxes, which an embedded image blows apart.
Inline markdown — the mode the dump and journal cards already ask for —
now drops img nodes, so a description image shows up on the detail page
and nowhere else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiAPtJZeCYYk8rKehtLUQU
This commit is contained in:
khannurien
2026-09-08 12:43:59 +00:00
parent 79b7adce8f
commit d76154d15d
2 changed files with 21 additions and 7 deletions

View File

@@ -186,6 +186,12 @@ export function DumpCreateModal(
// Paste handler // Paste handler
useEffect(() => { useEffect(() => {
const handler = (e: ClipboardEvent) => { const handler = (e: ClipboardEvent) => {
// Fields own their paste: the comment editor uploads pasted images as
// inline attachments, and the URL input has its own file handler below.
// Without this guard, pasting an image into the description would flip
// the whole modal over to a file dump.
const tag = (e.target as HTMLElement).tagName;
if (tag === "INPUT" || tag === "TEXTAREA") return;
const pastedFile = e.clipboardData?.files[0]; const pastedFile = e.clipboardData?.files[0];
if (pastedFile) { if (pastedFile) {
setMode("file"); setMode("file");
@@ -194,8 +200,6 @@ export function DumpCreateModal(
clearErrors("root"); clearErrors("root");
return; return;
} }
const tag = (e.target as HTMLElement).tagName;
if (tag === "INPUT" || tag === "TEXTAREA") return;
const text = e.clipboardData?.getData("text") ?? ""; const text = e.clipboardData?.getData("text") ?? "";
try { try {
const u = new URL(normalizeUrl(text)); const u = new URL(normalizeUrl(text));

View File

@@ -16,10 +16,10 @@ function preprocessMentions(text: string): string {
return text.replace(/(?<![[(])@([\w]+)/g, "[@$1](/users/$1)"); return text.replace(/(?<![[(])@([\w]+)/g, "[@$1](/users/$1)");
} }
// Static components object — defined once at module scope to avoid recreation on every render type Components = React.ComponentProps<typeof ReactMarkdown>["components"];
const MARKDOWN_COMPONENTS: React.ComponentProps<
typeof ReactMarkdown // Static components objects — defined once at module scope to avoid recreation on every render
>["components"] = { const MARKDOWN_COMPONENTS: Components = {
a: ({ href, children: linkChildren }) => { a: ({ href, children: linkChildren }) => {
if (href?.startsWith("/users/")) { if (href?.startsWith("/users/")) {
return <Link to={href}>{linkChildren}</Link>; return <Link to={href}>{linkChildren}</Link>;
@@ -32,6 +32,14 @@ const MARKDOWN_COMPONENTS: React.ComponentProps<
}, },
}; };
// Inline renderings are line-clamped teasers inside cards, where an embedded
// image blows the layout apart. Drop images there — the full description is
// one click away on the detail page.
const INLINE_MARKDOWN_COMPONENTS: Components = {
...MARKDOWN_COMPONENTS,
img: () => null,
};
export function Markdown( export function Markdown(
{ children, className, inline = false }: MarkdownProps, { children, className, inline = false }: MarkdownProps,
) { ) {
@@ -45,7 +53,9 @@ export function Markdown(
> >
<ReactMarkdown <ReactMarkdown
remarkPlugins={REMARK_PLUGINS} remarkPlugins={REMARK_PLUGINS}
components={MARKDOWN_COMPONENTS} components={inline
? INLINE_MARKDOWN_COMPONENTS
: MARKDOWN_COMPONENTS}
> >
{processed} {processed}
</ReactMarkdown> </ReactMarkdown>