Compare commits

...

4 Commits

Author SHA1 Message Date
Philipinho 949972cf41 fix 2026-03-30 13:15:15 +01:00
Philipinho 3e5a1f73ea fix media links 2026-03-30 12:38:27 +01:00
Philipinho 990273fb02 min resize dimensions 2026-03-30 12:23:24 +01:00
Philipinho 02334f4106 fix placeholder 2026-03-30 12:16:14 +01:00
8 changed files with 66 additions and 36 deletions
@@ -10,7 +10,7 @@ import { useCallback } from "react";
export default function AttachmentView(props: NodeViewProps) { export default function AttachmentView(props: NodeViewProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { editor, node, getPos, selected } = props; const { editor, node, getPos, selected } = props;
const { url, name, size, mime, attachmentId } = node.attrs; const { url, name, size, mime, attachmentId, placeholder } = node.attrs;
const { hovered, ref } = useHover(); const { hovered, ref } = useHover();
const isPdf = mime === "application/pdf" || name?.toLowerCase().endsWith(".pdf"); const isPdf = mime === "application/pdf" || name?.toLowerCase().endsWith(".pdf");
@@ -49,14 +49,14 @@ export default function AttachmentView(props: NodeViewProps) {
h={25} h={25}
> >
<Group wrap="nowrap" gap="sm" style={{ minWidth: 0, flex: 1 }}> <Group wrap="nowrap" gap="sm" style={{ minWidth: 0, flex: 1 }}>
{url ? ( {!url && placeholder ? (
<IconPaperclip size={20} style={{ flexShrink: 0 }} />
) : (
<Loader size={20} style={{ flexShrink: 0 }} /> <Loader size={20} style={{ flexShrink: 0 }} />
) : (
<IconPaperclip size={20} style={{ flexShrink: 0 }} />
)} )}
<Text component="span" size="md" truncate="end" style={{ minWidth: 0 }}> <Text component="span" size="md" truncate="end" style={{ minWidth: 0 }}>
{url ? name : t("Uploading {{name}}", { name })} {!url && placeholder ? t("Uploading {{name}}", { name }) : name}
</Text> </Text>
<Text component="span" size="sm" c="dimmed" style={{ flexShrink: 0 }}> <Text component="span" size="sm" c="dimmed" style={{ flexShrink: 0 }}>
@@ -29,7 +29,7 @@ export default function AudioView(props: NodeViewProps) {
return ( return (
<NodeViewWrapper data-drag-handle> <NodeViewWrapper data-drag-handle>
<div className={`${classes.audioWrapper} ${!safeSrc ? classes.skeleton : ''}`}> <div className={`${classes.audioWrapper} ${!safeSrc && placeholder ? classes.skeleton : ''}`}>
{safeSrc && ( {safeSrc && (
<audio <audio
className={classes.audio} className={classes.audio}
@@ -49,7 +49,7 @@ export default function AudioView(props: NodeViewProps) {
<Loader size={20} pos="absolute" top={6} right={6} /> <Loader size={20} pos="absolute" top={6} right={6} />
</Group> </Group>
)} )}
{!safeSrc && !previewSrc && ( {!safeSrc && !previewSrc && placeholder && (
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md" h={54}> <Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md" h={54}>
<Loader size={20} style={{ flexShrink: 0 }} /> <Loader size={20} style={{ flexShrink: 0 }} />
<Text component="span" size="sm" truncate="end"> <Text component="span" size="sm" truncate="end">
@@ -59,6 +59,9 @@ export default function AudioView(props: NodeViewProps) {
</Text> </Text>
</Group> </Group>
)} )}
{!safeSrc && !previewSrc && !placeholder && (
<audio className={classes.audio} controls />
)}
</div> </div>
</NodeViewWrapper> </NodeViewWrapper>
); );
@@ -33,7 +33,7 @@ export default function ImageView(props: NodeViewProps) {
className={clsx( className={clsx(
selected && "ProseMirror-selectednode", selected && "ProseMirror-selectednode",
classes.imageWrapper, classes.imageWrapper,
!src && classes.skeleton, !src && placeholder && classes.skeleton,
alignClass, alignClass,
)} )}
style={{ style={{
@@ -55,7 +55,7 @@ export default function ImageView(props: NodeViewProps) {
<Loader size={20} pos="absolute" bottom={6} right={6} /> <Loader size={20} pos="absolute" bottom={6} right={6} />
</Group> </Group>
)} )}
{!src && !previewSrc && ( {!src && !previewSrc && placeholder && (
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md"> <Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md">
<Loader size={20} style={{ flexShrink: 0 }} /> <Loader size={20} style={{ flexShrink: 0 }} />
<Text component="span" size="sm" truncate="end"> <Text component="span" size="sm" truncate="end">
@@ -73,7 +73,8 @@ export default function PdfView(props: NodeViewProps) {
if (!src || !safeSrc) { if (!src || !safeSrc) {
return ( return (
<NodeViewWrapper data-drag-handle> <NodeViewWrapper data-drag-handle>
<div className={`${classes.pdfWrapper} ${classes.skeleton}`} style={{ height: 600 }}> <div className={`${classes.pdfWrapper} ${placeholder ? classes.skeleton : ''}`} style={{ height: placeholder ? 600 : undefined }}>
{placeholder && (
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md"> <Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md">
<Loader size={20} style={{ flexShrink: 0 }} /> <Loader size={20} style={{ flexShrink: 0 }} />
<Text component="span" size="sm" truncate="end"> <Text component="span" size="sm" truncate="end">
@@ -82,6 +83,7 @@ export default function PdfView(props: NodeViewProps) {
: t("Uploading file")} : t("Uploading file")}
</Text> </Text>
</Group> </Group>
)}
</div> </div>
</NodeViewWrapper> </NodeViewWrapper>
); );
@@ -33,7 +33,7 @@ export default function VideoView(props: NodeViewProps) {
className={clsx( className={clsx(
selected && "ProseMirror-selectednode", selected && "ProseMirror-selectednode",
classes.videoWrapper, classes.videoWrapper,
!src && classes.skeleton, !src && placeholder && classes.skeleton,
alignClass, alignClass,
)} )}
style={{ style={{
@@ -60,7 +60,7 @@ export default function VideoView(props: NodeViewProps) {
<Loader size={20} pos="absolute" top={6} right={6} /> <Loader size={20} pos="absolute" top={6} right={6} />
</Group> </Group>
)} )}
{!src && !previewSrc && ( {!src && !previewSrc && placeholder && (
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md"> <Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md">
<Loader size={20} style={{ flexShrink: 0 }} /> <Loader size={20} style={{ flexShrink: 0 }} />
<Text component="span" size="sm" truncate="end"> <Text component="span" size="sm" truncate="end">
@@ -70,6 +70,9 @@ export default function VideoView(props: NodeViewProps) {
</Text> </Text>
</Group> </Group>
)} )}
{!src && !previewSrc && !placeholder && (
<video className={classes.video} controls />
)}
</div> </div>
</NodeViewWrapper> </NodeViewWrapper>
); );
@@ -253,8 +253,8 @@ export const mainExtensions = [
resize: { resize: {
enabled: true, enabled: true,
directions: ["left", "right"], directions: ["left", "right"],
minWidth: 80, minWidth: 24,
minHeight: 40, minHeight: 16,
alwaysPreserveAspectRatio: true, alwaysPreserveAspectRatio: true,
//@ts-ignore //@ts-ignore
createCustomHandle: createImageHandle, createCustomHandle: createImageHandle,
@@ -266,8 +266,8 @@ export const mainExtensions = [
resize: { resize: {
enabled: true, enabled: true,
directions: ["left", "right"], directions: ["left", "right"],
minWidth: 80, minWidth: 24,
minHeight: 40, minHeight: 16,
alwaysPreserveAspectRatio: true, alwaysPreserveAspectRatio: true,
//@ts-ignore //@ts-ignore
createCustomHandle: createResizeHandle, createCustomHandle: createResizeHandle,
@@ -297,8 +297,8 @@ export const mainExtensions = [
resize: { resize: {
enabled: true, enabled: true,
directions: ["left", "right"], directions: ["left", "right"],
minWidth: 80, minWidth: 24,
minHeight: 40, minHeight: 16,
alwaysPreserveAspectRatio: true, alwaysPreserveAspectRatio: true,
//@ts-ignore //@ts-ignore
createCustomHandle: createResizeHandle, createCustomHandle: createResizeHandle,
@@ -310,8 +310,8 @@ export const mainExtensions = [
resize: { resize: {
enabled: true, enabled: true,
directions: ["left", "right"], directions: ["left", "right"],
minWidth: 80, minWidth: 24,
minHeight: 40, minHeight: 16,
alwaysPreserveAspectRatio: true, alwaysPreserveAspectRatio: true,
//@ts-ignore //@ts-ignore
createCustomHandle: createResizeHandle, createCustomHandle: createResizeHandle,
@@ -193,6 +193,8 @@ export class ImportAttachmentService {
// Build a map from resolved archive path → real filename from Confluence // Build a map from resolved archive path → real filename from Confluence
// metadata. Confluence Server archives often store files under numeric IDs // metadata. Confluence Server archives often store files under numeric IDs
// (e.g. "attachments/65601/65602") instead of the original filename. // (e.g. "attachments/65601/65602") instead of the original filename.
// Also register aliases so HTML references using the original filename
// (e.g. "attachments/pageId/original.mp3") resolve to the numeric path.
const pageDir = path.dirname(pageRelativePath); const pageDir = path.dirname(pageRelativePath);
const attachmentNameByRelPath = new Map<string, string>(); const attachmentNameByRelPath = new Map<string, string>();
for (const attachment of pageAttachments) { for (const attachment of pageAttachments) {
@@ -203,6 +205,13 @@ export class ImportAttachmentService {
); );
if (relPath && attachment.fileName) { if (relPath && attachment.fileName) {
attachmentNameByRelPath.set(relPath, attachment.fileName); attachmentNameByRelPath.set(relPath, attachment.fileName);
const dir = path.posix.dirname(relPath);
const aliasKey = `${dir}/${attachment.fileName}`;
if (!attachmentCandidates.has(aliasKey)) {
attachmentCandidates.set(aliasKey, attachmentCandidates.get(relPath)!);
attachmentNameByRelPath.set(aliasKey, attachment.fileName);
}
} }
} }
@@ -562,18 +571,31 @@ export class ImportAttachmentService {
continue; continue;
} }
// Check if already processed (was referenced in HTML) // Resolve the metadata href to the actual archive path
if (processed.has(href)) { const resolvedHref = resolveRelativeAttachmentPath(
continue; href,
} pageDir,
attachmentCandidates,
);
if (!resolvedHref) continue;
// Skip if the file doesn't exist // Check if already processed (was referenced in HTML).
if (!attachmentCandidates.has(href)) { // Inline elements may have been processed under an alias key (original
// filename) rather than the numeric archive path, so also check whether
// the underlying absolute file path has already been uploaded.
const absPath = attachmentCandidates.get(resolvedHref);
const alreadyProcessed =
processed.has(resolvedHref) ||
(absPath &&
Array.from(processed.values()).some(
(entry) => entry.abs === absPath,
));
if (alreadyProcessed) {
continue; continue;
} }
// This attachment was in the list but not referenced in HTML - add it // This attachment was in the list but not referenced in HTML - add it
const { attachmentId, apiFilePath, abs } = processFile(href); const { attachmentId, apiFilePath, abs } = processFile(resolvedHref);
const mime = mimeType || getMimeType(abs); const mime = mimeType || getMimeType(abs);
// Add as attachment node at the end // Add as attachment node at the end