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
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:
@@ -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));
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user