+
{content}
-
+
)
}
diff --git a/src/components/Dashboard/common/MarkdownInput.jsx b/src/components/Dashboard/common/MarkdownInput.jsx
index 3ca12c5..27ad49e 100644
--- a/src/components/Dashboard/common/MarkdownInput.jsx
+++ b/src/components/Dashboard/common/MarkdownInput.jsx
@@ -1,32 +1,213 @@
-import { Card, Splitter } from 'antd'
+import { EditorContent, useEditor } from '@tiptap/react'
+import Link from '@tiptap/extension-link'
+import Placeholder from '@tiptap/extension-placeholder'
+import Underline from '@tiptap/extension-underline'
+import { Markdown } from '@tiptap/markdown'
+import StarterKit from '@tiptap/starter-kit'
+import { Card, Splitter, theme } from 'antd'
import PropTypes from 'prop-types'
+import { useEffect, useRef, useState } from 'react'
+import { useThemeContext } from '../context/ThemeContext'
+import MarkdownToolbar from './MarkdownToolbar'
import CodeBlockEditor from './CodeBlockEditor'
-import MarkdownDisplay from './MarkdownDisplay'
-const MarkdownInput = ({ value, onChange }) => {
+const MarkdownInput = ({
+ value,
+ onChange,
+ minHeight = '120px',
+ size = 'small',
+ showCard = true
+}) => {
+ const markdownValue = typeof value === 'string' ? value : ''
+ const lastMarkdownRef = useRef(markdownValue)
+ const { isDarkMode } = useThemeContext()
+ const { token } = theme.useToken()
+ const [viewState, setViewState] = useState({ editor: true, code: false })
+ const [focusedPanel, setFocusedPanel] = useState(null)
+ const editorPanelRef = useRef(null)
+ const codePanelRef = useRef(null)
+
+ const handlePanelBlur = () => {
+ requestAnimationFrame(() => {
+ const active = document.activeElement
+ if (editorPanelRef.current?.contains(active)) {
+ setFocusedPanel('editor')
+ } else if (codePanelRef.current?.contains(active)) {
+ setFocusedPanel('code')
+ } else {
+ setFocusedPanel(null)
+ }
+ })
+ }
+ const editor = useEditor({
+ extensions: [
+ StarterKit,
+ Link.configure({
+ autolink: true,
+ defaultProtocol: 'https',
+ openOnClick: false
+ }),
+ Placeholder.configure({
+ placeholder: 'Enter text here...'
+ }),
+ Underline,
+ Markdown
+ ],
+ content: markdownValue,
+ contentType: 'markdown',
+ editorProps: {
+ attributes: {
+ class: 'md-editor__content'
+ }
+ },
+ immediatelyRender: false,
+ onUpdate: ({ editor }) => {
+ const nextMarkdown = editor.getMarkdown()
+ lastMarkdownRef.current = nextMarkdown
+ onChange?.(nextMarkdown)
+ }
+ })
+
+ useEffect(() => {
+ if (!editor) {
+ return
+ }
+
+ if (markdownValue !== lastMarkdownRef.current) {
+ editor.commands.setContent(markdownValue, {
+ contentType: 'markdown',
+ emitUpdate: false
+ })
+ lastMarkdownRef.current = markdownValue
+ }
+ }, [editor, markdownValue])
+
+ const editorPanel = (
+ setFocusedPanel('editor')}
+ onBlurCapture={handlePanelBlur}
+ >
+
+
+ )
+
+ const codePanel = (
+ setFocusedPanel('code')}
+ onBlurCapture={handlePanelBlur}
+ >
+ {
+ onChange?.(val)
+ }}
+ />
+
+ )
+
+ const renderContent = () => {
+ if (viewState.editor && viewState.code) {
+ return (
+
+ {viewState.editor && (
+
+ {editorPanel}
+
+ )}
+ {viewState.code && (
+
+ {codePanel}
+
+ )}
+
+ )
+ }
+ if (viewState.code) {
+ return codePanel
+ }
+ return editorPanel
+ }
+
+ const cardContent = (
+
+
+ {renderContent()}
+
+ )
+
return (
-
-
-
-
+ <>
+ {showCard ? (
+
+ {cardContent}
-
-
-
-
-
-
-
+ ) : (
+ cardContent
+ )}
+ >
)
}
MarkdownInput.propTypes = {
- value: PropTypes.string.isRequired,
- onChange: PropTypes.func
+ value: PropTypes.string,
+ onChange: PropTypes.func,
+ minHeight: PropTypes.string,
+ size: PropTypes.string,
+ showCard: PropTypes.bool
}
export default MarkdownInput
diff --git a/src/components/Dashboard/common/MarkdownToolbar.jsx b/src/components/Dashboard/common/MarkdownToolbar.jsx
new file mode 100644
index 0000000..dd0b93b
--- /dev/null
+++ b/src/components/Dashboard/common/MarkdownToolbar.jsx
@@ -0,0 +1,347 @@
+import { MinusOutlined } from '@ant-design/icons'
+import { useEditorState } from '@tiptap/react'
+import { useState } from 'react'
+import {
+ Button,
+ Card,
+ Checkbox,
+ Flex,
+ Input,
+ Popover,
+ Select,
+ Space,
+ Tooltip
+} from 'antd'
+import PropTypes from 'prop-types'
+import BlockquoteIcon from '../../Icons/BlockquoteIcon'
+import BoldIcon from '../../Icons/BoldIcon'
+import BulletListIcon from '../../Icons/BulletListIcon'
+import CheckIcon from '../../Icons/CheckIcon'
+import ItalicIcon from '../../Icons/ItalicIcon'
+import NumberedListIcon from '../../Icons/NumberedListIcon'
+import JsonObjectIcon from '../../Icons/JsonObjectIcon'
+import LinkIcon from '../../Icons/LinkIcon'
+import TextFormatIcon from '../../Icons/TextFormatIcon'
+import UnderlineIcon from '../../Icons/UnderlineIcon'
+
+const BLOCK_TYPE_OPTIONS = [
+ { label: 'Paragraph', value: 'paragraph' },
+ { label: 'Heading 1', value: 'h1' },
+ { label: 'Heading 2', value: 'h2' },
+ { label: 'Heading 3', value: 'h3' },
+ { label: 'Heading 4', value: 'h4' },
+ { label: 'Quote', value: 'quote' }
+]
+
+const getBlockTypeValue = (editor) => {
+ if (!editor) {
+ return 'paragraph'
+ }
+
+ if (editor.isActive('blockquote')) {
+ return 'quote'
+ }
+
+ for (const level of [1, 2, 3, 4]) {
+ if (editor.isActive('heading', { level })) {
+ return `h${level}`
+ }
+ }
+
+ return 'paragraph'
+}
+
+const VIEW_ITEMS = [
+ { key: 'editor', label: 'Editor' },
+ { key: 'code', label: 'Code' }
+]
+
+const MarkdownToolbar = ({
+ editor,
+ size = 'small',
+ showCard = true,
+ viewState = { editor: true, code: false },
+ onViewStateChange = () => {},
+ editingToolsDisabled = false
+}) => {
+ const [linkPopoverOpen, setLinkPopoverOpen] = useState(false)
+ const [linkUrl, setLinkUrl] = useState('')
+
+ const editorState = useEditorState({
+ editor,
+ selector: ({ editor }) => ({
+ blockTypeValue: getBlockTypeValue(editor),
+ isBold: editor?.isActive('bold') ?? false,
+ isItalic: editor?.isActive('italic') ?? false,
+ isUnderline: editor?.isActive('underline') ?? false,
+ isBulletList: editor?.isActive('bulletList') ?? false,
+ isOrderedList: editor?.isActive('orderedList') ?? false
+ })
+ })
+
+ const handleViewStateChange = (key, checked) => {
+ const nextState = { ...viewState, [key]: checked }
+ if (!nextState.editor && !nextState.code) {
+ nextState[key] = true
+ }
+ onViewStateChange(nextState)
+ }
+
+ const handleLinkPopoverOpenChange = (open) => {
+ setLinkPopoverOpen(open)
+ if (open) {
+ setLinkUrl(editor?.getAttributes('link').href ?? '')
+ }
+ }
+
+ const handleLinkSubmit = () => {
+ if (!editor) {
+ return
+ }
+
+ const trimmedUrl = linkUrl.trim()
+
+ if (!trimmedUrl) {
+ editor.chain().focus().extendMarkRange('link').unsetLink().run()
+ } else {
+ editor
+ .chain()
+ .focus()
+ .extendMarkRange('link')
+ .setLink({ href: trimmedUrl })
+ .run()
+ }
+
+ setLinkPopoverOpen(false)
+ }
+
+ const handleBlockTypeChange = (nextBlockType) => {
+ if (!editor) {
+ return
+ }
+
+ const chain = editor.chain().focus()
+
+ if (editor.isActive('blockquote') && nextBlockType !== 'quote') {
+ chain.toggleBlockquote()
+ }
+
+ if (nextBlockType === 'paragraph') {
+ chain.setParagraph().run()
+ return
+ }
+
+ if (nextBlockType === 'quote') {
+ if (editor.isActive('heading')) {
+ chain.setParagraph()
+ }
+
+ chain.toggleBlockquote().run()
+ return
+ }
+
+ const headingLevel = Number(nextBlockType.replace('h', ''))
+ chain.setHeading({ level: headingLevel }).run()
+ }
+
+ const linkPopoverContent = (
+
+ setLinkUrl(e.target.value)}
+ onPressEnter={handleLinkSubmit}
+ style={{ flex: 1 }}
+ />
+ }
+ style={{ minWidth: 32 }}
+ />
+
+ )
+
+ const cardContent = (
+
+
+ {VIEW_ITEMS.map((item) => (
+
+ handleViewStateChange(item.key, e.target.checked)
+ }
+ >
+ {item.label}
+
+ ))}
+
+ }
+ placement='bottomLeft'
+ trigger='hover'
+ arrow={false}
+ >
+
+
+ }
+ style={{ width: 160 }}
+ status={'defult'}
+ disabled={!editor || editingToolsDisabled}
+ />
+
+
+ }
+ onClick={() => editor?.chain().focus().toggleBold().run()}
+ disabled={!editor || editingToolsDisabled}
+ style={{ minWidth: 32 }}
+ />
+
+
+ }
+ onClick={() => editor?.chain().focus().toggleItalic().run()}
+ disabled={!editor || editingToolsDisabled}
+ style={{ minWidth: 32 }}
+ />
+
+
+ }
+ onClick={() => editor?.chain().focus().toggleUnderline().run()}
+ disabled={!editor || editingToolsDisabled}
+ style={{ minWidth: 32 }}
+ />
+
+
+
+
+ }
+ onClick={() => editor?.chain().focus().toggleBulletList().run()}
+ disabled={!editor || editingToolsDisabled}
+ style={{ minWidth: 32 }}
+ />
+
+
+ }
+ onClick={() => editor?.chain().focus().toggleOrderedList().run()}
+ disabled={!editor || editingToolsDisabled}
+ style={{ minWidth: 32 }}
+ />
+
+
+
+ }
+ onClick={() =>
+ handleBlockTypeChange(
+ editorState.blockTypeValue === 'quote' ? 'paragraph' : 'quote'
+ )
+ }
+ disabled={!editor || editingToolsDisabled}
+ style={{ minWidth: 32 }}
+ />
+
+
+
+ }
+ disabled={!editor || editingToolsDisabled}
+ style={{ minWidth: 32 }}
+ />
+
+
+
+ }
+ onClick={() => editor?.chain().focus().toggleCodeBlock().run()}
+ disabled={!editor || editingToolsDisabled}
+ style={{ minWidth: 32 }}
+ />
+
+
+ }
+ onClick={() => editor?.chain().focus().setHorizontalRule().run()}
+ disabled={!editor || editingToolsDisabled}
+ style={{ minWidth: 32 }}
+ />
+
+
+ )
+
+ return (
+ <>
+ {showCard ? (
+
+ {cardContent}
+
+ ) : (
+ cardContent
+ )}
+ >
+ )
+}
+
+MarkdownToolbar.propTypes = {
+ editor: PropTypes.shape({
+ chain: PropTypes.func,
+ getAttributes: PropTypes.func,
+ isActive: PropTypes.func
+ }),
+ size: PropTypes.string,
+ showCard: PropTypes.bool,
+ viewState: PropTypes.object,
+ onViewStateChange: PropTypes.func,
+ editingToolsDisabled: PropTypes.bool
+}
+
+export default MarkdownToolbar
diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx
index 3ed2d2b..041a197 100644
--- a/src/components/Dashboard/common/ObjectProperty.jsx
+++ b/src/components/Dashboard/common/ObjectProperty.jsx
@@ -91,6 +91,7 @@ const ObjectProperty = ({
maxWidth = '100%',
loading = false,
rollups = [],
+ showCard = true,
...rest
}) => {
if (value && typeof value == 'function' && objectData) {
@@ -733,7 +734,9 @@ const ObjectProperty = ({
/>
)
case 'markdown':
- return
+ return (
+
+ )
case 'id':
// id is not editable, just show view mode
if (value) {
@@ -868,7 +871,8 @@ ObjectProperty.propTypes = {
showSince: PropTypes.bool,
loading: PropTypes.bool,
rollups: PropTypes.arrayOf(PropTypes.object),
- canAddRemove: PropTypes.bool
+ canAddRemove: PropTypes.bool,
+ showCard: PropTypes.bool
}
export default ObjectProperty
diff --git a/src/components/Icons/BlockquoteIcon.jsx b/src/components/Icons/BlockquoteIcon.jsx
new file mode 100644
index 0000000..e7861ee
--- /dev/null
+++ b/src/components/Icons/BlockquoteIcon.jsx
@@ -0,0 +1,6 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/blockquoteicon.svg?react'
+
+const BlockquoteIcon = (props) =>
+
+export default BlockquoteIcon
diff --git a/src/components/Icons/BoldIcon.jsx b/src/components/Icons/BoldIcon.jsx
new file mode 100644
index 0000000..c32e587
--- /dev/null
+++ b/src/components/Icons/BoldIcon.jsx
@@ -0,0 +1,6 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/boldicon.svg?react'
+
+const BoldIcon = (props) =>
+
+export default BoldIcon
diff --git a/src/components/Icons/BulletListIcon.jsx b/src/components/Icons/BulletListIcon.jsx
new file mode 100644
index 0000000..00a8ca3
--- /dev/null
+++ b/src/components/Icons/BulletListIcon.jsx
@@ -0,0 +1,6 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/bulletlisticon.svg?react'
+
+const BulletListIcon = (props) =>
+
+export default BulletListIcon
diff --git a/src/components/Icons/ItalicIcon.jsx b/src/components/Icons/ItalicIcon.jsx
new file mode 100644
index 0000000..7fdf600
--- /dev/null
+++ b/src/components/Icons/ItalicIcon.jsx
@@ -0,0 +1,6 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/italicicon.svg?react'
+
+const ItalicIcon = (props) =>
+
+export default ItalicIcon
diff --git a/src/components/Icons/NumberedListIcon.jsx b/src/components/Icons/NumberedListIcon.jsx
new file mode 100644
index 0000000..7355219
--- /dev/null
+++ b/src/components/Icons/NumberedListIcon.jsx
@@ -0,0 +1,6 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/numberedlisticon.svg?react'
+
+const NumberedListIcon = (props) =>
+
+export default NumberedListIcon
diff --git a/src/components/Icons/TextFormatIcon.jsx b/src/components/Icons/TextFormatIcon.jsx
new file mode 100644
index 0000000..9abbe77
--- /dev/null
+++ b/src/components/Icons/TextFormatIcon.jsx
@@ -0,0 +1,6 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/textformaticon.svg?react'
+
+const TextFormatIcon = (props) =>
+
+export default TextFormatIcon
diff --git a/src/components/Icons/TextIcon.jsx b/src/components/Icons/TextIcon.jsx
new file mode 100644
index 0000000..62f5318
--- /dev/null
+++ b/src/components/Icons/TextIcon.jsx
@@ -0,0 +1,6 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/texticon.svg?react'
+
+const TextIcon = (props) =>
+
+export default TextIcon
diff --git a/src/components/Icons/UnderlineIcon.jsx b/src/components/Icons/UnderlineIcon.jsx
new file mode 100644
index 0000000..c1de193
--- /dev/null
+++ b/src/components/Icons/UnderlineIcon.jsx
@@ -0,0 +1,6 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/underlineicon.svg?react'
+
+const UnderlineIcon = (props) =>
+
+export default UnderlineIcon
diff --git a/vite.config.js b/vite.config.js
index 78b0113..3fedfbb 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -106,6 +106,11 @@ export default defineConfig({
) {
return 'tsparticles'
}
+
+ // --- Tiptap
+ if (id.includes('node_modules/@tiptap')) {
+ return 'tiptap'
+ }
}
}
}