Migrate edito components

This commit is contained in:
2025-05-18 15:19:58 +02:00
parent db75bdcc89
commit bfc5cc2d29
5 changed files with 195 additions and 121 deletions

View File

@@ -2,10 +2,21 @@ import React from 'react';
import { Text, Center } from '@mantine/core';
import Editor from './Editor';
import MarkdownPreview from './MarkdownPreview';
import { getFileUrl } from '../../api/git';
import { isImageFile } from '../../utils/fileHelpers';
import { getFileUrl, isImageFile } from '../../utils/fileHelpers';
import { useWorkspace } from '@/contexts/WorkspaceContext';
const ContentView = ({
type ViewTab = 'source' | 'preview';
interface ContentViewProps {
activeTab: ViewTab;
selectedFile: string | null;
content: string;
handleContentChange: (content: string) => void;
handleSave: (filePath: string, content: string) => Promise<boolean>;
handleFileSelect: (filePath: string | null) => Promise<void>;
}
const ContentView: React.FC<ContentViewProps> = ({
activeTab,
selectedFile,
content,
@@ -13,10 +24,21 @@ const ContentView = ({
handleSave,
handleFileSelect,
}) => {
const { currentWorkspace } = useWorkspace();
if (!currentWorkspace) {
return (
<Center style={{ height: '100%' }}>
<Text size="xl" fw={500}>
No workspace selected.
</Text>
</Center>
);
}
if (!selectedFile) {
return (
<Center style={{ height: '100%' }}>
<Text size="xl" weight={500}>
<Text size="xl" fw={500}>
No file selected.
</Text>
</Center>
@@ -27,7 +49,7 @@ const ContentView = ({
return (
<Center className="image-preview">
<img
src={getFileUrl(selectedFile)}
src={getFileUrl(currentWorkspace.name, selectedFile)}
alt={selectedFile}
style={{
maxWidth: '100%',

View File

@@ -7,13 +7,25 @@ import { defaultKeymap } from '@codemirror/commands';
import { oneDark } from '@codemirror/theme-one-dark';
import { useWorkspace } from '../../contexts/WorkspaceContext';
const Editor = ({ content, handleContentChange, handleSave, selectedFile }) => {
interface EditorProps {
content: string;
handleContentChange: (content: string) => void;
handleSave: (filePath: string, content: string) => Promise<boolean>;
selectedFile: string;
}
const Editor: React.FC<EditorProps> = ({
content,
handleContentChange,
handleSave,
selectedFile,
}) => {
const { colorScheme } = useWorkspace();
const editorRef = useRef();
const viewRef = useRef();
const editorRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null);
useEffect(() => {
const handleEditorSave = (view) => {
const handleEditorSave = (view: EditorView): boolean => {
handleSave(selectedFile, view.state.doc.toString());
return true;
};
@@ -36,6 +48,8 @@ const Editor = ({ content, handleContentChange, handleSave, selectedFile }) => {
},
});
if (!editorRef.current) return;
const state = EditorState.create({
doc: content,
extensions: [
@@ -69,8 +83,9 @@ const Editor = ({ content, handleContentChange, handleSave, selectedFile }) => {
return () => {
view.destroy();
viewRef.current = null;
};
}, [colorScheme, handleContentChange]);
}, [colorScheme, handleContentChange, handleSave, selectedFile]);
useEffect(() => {
if (viewRef.current && content !== viewRef.current.state.doc.toString()) {

View File

@@ -1,111 +0,0 @@
import React, { useState, useEffect, useMemo } from 'react';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkMath from 'remark-math';
import remarkRehype from 'remark-rehype';
import rehypeMathjax from 'rehype-mathjax';
import rehypeReact from 'rehype-react';
import rehypePrism from 'rehype-prism';
import * as prod from 'react/jsx-runtime';
import { notifications } from '@mantine/notifications';
import { remarkWikiLinks } from '../../utils/remarkWikiLinks';
import { useWorkspace } from '../../contexts/WorkspaceContext';
const MarkdownPreview = ({ content, handleFileSelect }) => {
const [processedContent, setProcessedContent] = useState(null);
const baseUrl = window.API_BASE_URL;
const { currentWorkspace } = useWorkspace();
const handleLinkClick = (e, href) => {
e.preventDefault();
if (href.startsWith(`${baseUrl}/internal/`)) {
// For existing files, extract the path and directly select it
const [filePath] = decodeURIComponent(
href.replace(`${baseUrl}/internal/`, '')
).split('#');
handleFileSelect(filePath);
} else if (href.startsWith(`${baseUrl}/notfound/`)) {
// For non-existent files, show a notification
const fileName = decodeURIComponent(
href.replace(`${baseUrl}/notfound/`, '')
);
notifications.show({
title: 'File Not Found',
message: `The file "${fileName}" does not exist.`,
color: 'red',
});
}
};
const processor = useMemo(
() =>
unified()
.use(remarkParse)
.use(remarkWikiLinks, currentWorkspace?.name)
.use(remarkMath)
.use(remarkRehype)
.use(rehypeMathjax)
.use(rehypePrism)
.use(rehypeReact, {
production: true,
jsx: prod.jsx,
jsxs: prod.jsxs,
Fragment: prod.Fragment,
components: {
img: ({ src, alt, ...props }) => (
<img
src={src}
alt={alt}
onError={(event) => {
console.error('Failed to load image:', event.target.src);
event.target.alt = 'Failed to load image';
}}
{...props}
/>
),
a: ({ href, children, ...props }) => (
<a
href={href}
onClick={(e) => handleLinkClick(e, href)}
{...props}
>
{children}
</a>
),
code: ({ children, className, ...props }) => {
const language = className
? className.replace('language-', '')
: null;
return (
<pre className={className}>
<code {...props}>{children}</code>
</pre>
);
},
},
}),
[baseUrl, handleFileSelect, currentWorkspace?.name]
);
useEffect(() => {
const processContent = async () => {
if (!currentWorkspace) {
return;
}
try {
const result = await processor.process(content);
setProcessedContent(result.result);
} catch (error) {
console.error('Error processing markdown:', error);
}
};
processContent();
}, [content, processor, currentWorkspace]);
return <div className="markdown-preview">{processedContent}</div>;
};
export default MarkdownPreview;

View File

@@ -0,0 +1,141 @@
import React, { useState, useEffect, useMemo, ReactNode } from 'react';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkMath from 'remark-math';
import remarkRehype from 'remark-rehype';
import rehypeMathjax from 'rehype-mathjax';
import rehypeReact from 'rehype-react';
import rehypePrism from 'rehype-prism';
import * as prod from 'react/jsx-runtime';
import { notifications } from '@mantine/notifications';
import { remarkWikiLinks } from '../../utils/remarkWikiLinks';
import { useWorkspace } from '../../contexts/WorkspaceContext';
interface MarkdownPreviewProps {
content: string;
handleFileSelect: (filePath: string | null) => Promise<void>;
}
interface MarkdownImageProps {
src: string;
alt?: string;
[key: string]: any;
}
interface MarkdownLinkProps {
href: string;
children: ReactNode;
[key: string]: any;
}
interface MarkdownCodeProps {
children: ReactNode;
className?: string;
[key: string]: any;
}
const MarkdownPreview: React.FC<MarkdownPreviewProps> = ({
content,
handleFileSelect,
}) => {
const [processedContent, setProcessedContent] = useState<ReactNode | null>(
null
);
const baseUrl = window.API_BASE_URL;
const { currentWorkspace } = useWorkspace();
const handleLinkClick = (
e: React.MouseEvent<HTMLAnchorElement>,
href: string
): void => {
e.preventDefault();
if (href.startsWith(`${baseUrl}/internal/`)) {
// For existing files, extract the path and directly select it
const [filePath] = decodeURIComponent(
href.replace(`${baseUrl}/internal/`, '')
).split('#');
handleFileSelect(filePath);
} else if (href.startsWith(`${baseUrl}/notfound/`)) {
// For non-existent files, show a notification
const fileName = decodeURIComponent(
href.replace(`${baseUrl}/notfound/`, '')
);
notifications.show({
title: 'File Not Found',
message: `The file "${fileName}" does not exist.`,
color: 'red',
});
}
};
const processor = useMemo(() => {
// Only create the processor if we have a workspace name
if (!currentWorkspace?.name) {
return unified();
}
return unified()
.use(remarkParse)
.use(remarkWikiLinks, currentWorkspace.name) // Now we know this is defined
.use(remarkMath)
.use(remarkRehype)
.use(rehypeMathjax)
.use(rehypePrism)
.use(rehypeReact as any, {
production: true,
jsx: prod.jsx,
jsxs: prod.jsxs,
Fragment: prod.Fragment,
components: {
img: ({ src, alt, ...props }: MarkdownImageProps) => (
<img
src={src}
alt={alt || ''}
onError={(event) => {
console.error('Failed to load image:', event.currentTarget.src);
event.currentTarget.alt = 'Failed to load image';
}}
{...props}
/>
),
a: ({ href, children, ...props }: MarkdownLinkProps) => (
<a href={href} onClick={(e) => handleLinkClick(e, href)} {...props}>
{children}
</a>
),
code: ({ children, className, ...props }: MarkdownCodeProps) => {
const language = className
? className.replace('language-', '')
: null;
return (
<pre className={className}>
<code {...props}>{children}</code>
</pre>
);
},
},
});
}, [baseUrl, handleFileSelect, currentWorkspace?.name]);
useEffect(() => {
const processContent = async (): Promise<void> => {
if (!currentWorkspace) {
return;
}
try {
const result = await processor.process(content);
setProcessedContent(result.result as ReactNode);
} catch (error) {
console.error('Error processing markdown:', error);
}
};
processContent();
}, [content, processor, currentWorkspace]);
return <div className="markdown-preview">{processedContent}</div>;
};
export default MarkdownPreview;

View File

@@ -1,3 +1,4 @@
import { API_BASE_URL } from '@/types/authApi';
import { IMAGE_EXTENSIONS } from '../types/file';
/**
@@ -8,3 +9,9 @@ import { IMAGE_EXTENSIONS } from '../types/file';
export const isImageFile = (filePath: string): boolean => {
return IMAGE_EXTENSIONS.some((ext) => filePath.toLowerCase().endsWith(ext));
};
export const getFileUrl = (workspaceName: string, filePath: string) => {
return `${API_BASE_URL}/workspaces/${encodeURIComponent(
workspaceName
)}/files/${encodeURIComponent(filePath)}`;
};