Add CodeDiffViewer and DiffLabel components for enhanced code comparison
- Introduced CodeDiffViewer component utilizing @codemirror/merge for visual code diffs. - Added DiffLabel component to display code change statistics and trigger the CodeDiffViewer modal. - Updated PropertyChanges component to integrate DiffLabel for code property changes. - Enhanced styling for code diff display with new CSS classes. - Updated package.json and lock files to include @codemirror/merge dependency.
This commit is contained in:
parent
d8c13426d1
commit
ee038d08c3
@ -1291,3 +1291,20 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
||||
flex: 0 0 auto;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.code-diff-viewer {
|
||||
height: 70vh;
|
||||
border: 1px solid #85858541;
|
||||
}
|
||||
|
||||
.code-diff-viewer .cm-mergeView {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.diff-label-square {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background-color: #85858541;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
3
bun.lock
3
bun.lock
@ -23,6 +23,7 @@
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "6.12.1",
|
||||
"@codemirror/merge": "^6.12.2",
|
||||
"@codemirror/state": "6.7.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "6.39.12",
|
||||
@ -238,6 +239,8 @@
|
||||
|
||||
"@codemirror/lint": ["@codemirror/lint@6.9.3", "", { "dependencies": { "@codemirror/state": "6.7.1", "@codemirror/view": "6.39.12", "crelt": "1.0.6" } }, "sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw=="],
|
||||
|
||||
"@codemirror/merge": ["@codemirror/merge@6.12.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/highlight": "^1.0.0", "style-mod": "^4.1.0" } }, "sha512-V8JvyAPjHbPupqP7BeMcsdsYCbyPij74jxIbaIJDORI+VZzW44zFmon8bF+oxGWvOKhcRmkiUMXd8MxHr3YA2w=="],
|
||||
|
||||
"@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "6.7.1", "@codemirror/view": "6.39.12", "crelt": "1.0.7" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="],
|
||||
|
||||
"@codemirror/state": ["@codemirror/state@6.7.1", "", { "dependencies": { "@marijn/find-cluster-break": "1.0.3" } }, "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A=="],
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "6.12.1",
|
||||
"@codemirror/merge": "^6.12.2",
|
||||
"@codemirror/state": "6.7.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "6.39.12",
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@ -70,6 +70,9 @@ importers:
|
||||
'@codemirror/language':
|
||||
specifier: 6.12.1
|
||||
version: 6.12.1
|
||||
'@codemirror/merge':
|
||||
specifier: ^6.12.2
|
||||
version: 6.12.2
|
||||
'@codemirror/state':
|
||||
specifier: 6.7.1
|
||||
version: 6.7.1
|
||||
@ -611,6 +614,9 @@ packages:
|
||||
'@codemirror/lint@6.9.3':
|
||||
resolution: {integrity: sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==}
|
||||
|
||||
'@codemirror/merge@6.12.2':
|
||||
resolution: {integrity: sha512-V8JvyAPjHbPupqP7BeMcsdsYCbyPij74jxIbaIJDORI+VZzW44zFmon8bF+oxGWvOKhcRmkiUMXd8MxHr3YA2w==}
|
||||
|
||||
'@codemirror/search@6.6.0':
|
||||
resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==}
|
||||
|
||||
@ -7047,6 +7053,14 @@ snapshots:
|
||||
'@codemirror/view': 6.39.12
|
||||
crelt: 1.0.6
|
||||
|
||||
'@codemirror/merge@6.12.2':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.12.1
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.39.12
|
||||
'@lezer/highlight': 1.2.3
|
||||
style-mod: 4.1.3
|
||||
|
||||
'@codemirror/search@6.6.0':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.7.1
|
||||
|
||||
@ -22,6 +22,64 @@ import { Button, Modal, Typography } from 'antd'
|
||||
|
||||
const { Link } = Typography
|
||||
|
||||
export function toEditorCode(code, language = 'javascript') {
|
||||
if (code == null) return ''
|
||||
if (typeof code === 'object' && language === 'json') {
|
||||
return JSON.stringify(code, null, 2)
|
||||
}
|
||||
return String(code)
|
||||
}
|
||||
|
||||
export function getCodeLanguageExtension(
|
||||
language = 'javascript',
|
||||
autoCompleteObject = null
|
||||
) {
|
||||
const lang = Array.isArray(language) ? language[0] : language
|
||||
|
||||
switch (String(lang || 'javascript').toLowerCase()) {
|
||||
case 'javascript':
|
||||
case 'js':
|
||||
return javascript()
|
||||
case 'python':
|
||||
case 'py':
|
||||
return python()
|
||||
case 'json':
|
||||
return json()
|
||||
case 'xml':
|
||||
return xml()
|
||||
case 'fctemplatelang':
|
||||
case 'ejs':
|
||||
return fcTemplateLang({ autoCompleteObject })
|
||||
case 'html':
|
||||
return html()
|
||||
case 'css':
|
||||
return css()
|
||||
case 'markdown':
|
||||
case 'md':
|
||||
return markdown()
|
||||
case 'sql':
|
||||
return sql()
|
||||
case 'java':
|
||||
return java()
|
||||
case 'cpp':
|
||||
case 'c++':
|
||||
case 'c':
|
||||
return cpp()
|
||||
case 'rust':
|
||||
case 'rs':
|
||||
return rust()
|
||||
case 'go':
|
||||
return go()
|
||||
case 'php':
|
||||
return php()
|
||||
case 'yaml':
|
||||
case 'yml':
|
||||
return yaml()
|
||||
default:
|
||||
return javascript()
|
||||
}
|
||||
}
|
||||
|
||||
export default function CodeBlockEditor({
|
||||
code = '',
|
||||
language = 'javascript',
|
||||
@ -37,57 +95,12 @@ export default function CodeBlockEditor({
|
||||
}) {
|
||||
const { isDarkMode } = useThemeContext()
|
||||
const [codeMirrorOpen, setCodeMirrorOpen] = useState(false)
|
||||
var editorCode = code
|
||||
const editorCode = toEditorCode(code, language)
|
||||
|
||||
if (typeof code == 'object' && language == 'json') {
|
||||
editorCode = JSON.stringify(code, null, 2)
|
||||
}
|
||||
|
||||
// Map language to CodeMirror extension
|
||||
const languageExtension = useMemo(() => {
|
||||
switch (language.toLowerCase()) {
|
||||
case 'javascript':
|
||||
case 'js':
|
||||
return javascript()
|
||||
case 'python':
|
||||
case 'py':
|
||||
return python()
|
||||
case 'json':
|
||||
return json()
|
||||
case 'xml':
|
||||
return xml()
|
||||
case 'fctemplatelang':
|
||||
case 'ejs':
|
||||
return fcTemplateLang({ autoCompleteObject })
|
||||
case 'html':
|
||||
return html()
|
||||
case 'css':
|
||||
return css()
|
||||
case 'markdown':
|
||||
case 'md':
|
||||
return markdown()
|
||||
case 'sql':
|
||||
return sql()
|
||||
case 'java':
|
||||
return java()
|
||||
case 'cpp':
|
||||
case 'c++':
|
||||
case 'c':
|
||||
return cpp()
|
||||
case 'rust':
|
||||
case 'rs':
|
||||
return rust()
|
||||
case 'go':
|
||||
return go()
|
||||
case 'php':
|
||||
return php()
|
||||
case 'yaml':
|
||||
case 'yml':
|
||||
return yaml()
|
||||
default:
|
||||
return javascript() // Default fallback
|
||||
}
|
||||
}, [language, autoCompleteObject])
|
||||
const languageExtension = useMemo(
|
||||
() => getCodeLanguageExtension(language, autoCompleteObject),
|
||||
[language, autoCompleteObject]
|
||||
)
|
||||
|
||||
const handleOnChange = (value) => {
|
||||
if (typeof code == 'object' && language == 'json') {
|
||||
|
||||
85
src/components/Dashboard/common/CodeDiffViewer.jsx
Normal file
85
src/components/Dashboard/common/CodeDiffViewer.jsx
Normal file
@ -0,0 +1,85 @@
|
||||
import { useLayoutEffect, useMemo, useRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { MergeView } from '@codemirror/merge'
|
||||
import { EditorState } from '@codemirror/state'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { oneDark } from '@codemirror/theme-one-dark'
|
||||
import { basicSetup } from 'codemirror'
|
||||
import { useThemeContext } from '../context/ThemeContext'
|
||||
import {
|
||||
getCodeLanguageExtension,
|
||||
toEditorCode
|
||||
} from './CodeBlockEditor'
|
||||
|
||||
export default function CodeDiffViewer({
|
||||
oldCode = '',
|
||||
newCode = '',
|
||||
language = 'javascript'
|
||||
}) {
|
||||
const { isDarkMode } = useThemeContext()
|
||||
const parentRef = useRef(null)
|
||||
const oldDoc = toEditorCode(oldCode, language)
|
||||
const newDoc = toEditorCode(newCode, language)
|
||||
|
||||
const languageExtension = useMemo(
|
||||
() => getCodeLanguageExtension(language),
|
||||
[language]
|
||||
)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!parentRef.current) return
|
||||
|
||||
const editorTheme = EditorView.theme({
|
||||
'&': {
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace'
|
||||
}
|
||||
})
|
||||
|
||||
const sharedExtensions = [
|
||||
basicSetup,
|
||||
languageExtension,
|
||||
editorTheme,
|
||||
EditorView.editable.of(false),
|
||||
EditorState.readOnly.of(true),
|
||||
...(isDarkMode ? [oneDark] : [])
|
||||
]
|
||||
|
||||
const view = new MergeView({
|
||||
a: {
|
||||
doc: oldDoc,
|
||||
extensions: sharedExtensions
|
||||
},
|
||||
b: {
|
||||
doc: newDoc,
|
||||
extensions: sharedExtensions
|
||||
},
|
||||
parent: parentRef.current,
|
||||
highlightChanges: true,
|
||||
gutter: true,
|
||||
collapseUnchanged: { margin: 3, minSize: 6 }
|
||||
})
|
||||
|
||||
return () => {
|
||||
view.destroy()
|
||||
}
|
||||
}, [oldDoc, newDoc, languageExtension, isDarkMode])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={parentRef}
|
||||
className={
|
||||
isDarkMode ? 'code-diff-viewer is-dark' : 'code-diff-viewer'
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
CodeDiffViewer.propTypes = {
|
||||
oldCode: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
newCode: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
language: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string)
|
||||
])
|
||||
}
|
||||
116
src/components/Dashboard/common/DiffLabel.jsx
Normal file
116
src/components/Dashboard/common/DiffLabel.jsx
Normal file
@ -0,0 +1,116 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Button, Modal, Typography, theme, Flex } from 'antd'
|
||||
import { presentableDiff } from '@codemirror/merge'
|
||||
import CodeDiffViewer from './CodeDiffViewer'
|
||||
import { toEditorCode } from './CodeBlockEditor'
|
||||
|
||||
const { Link, Text } = Typography
|
||||
|
||||
const SQUARE_COUNT = 10
|
||||
|
||||
const countLines = (str) => {
|
||||
if (!str) return 0
|
||||
const parts = str.split('\n')
|
||||
if (parts[parts.length - 1] === '') parts.pop()
|
||||
return Math.max(parts.length, 1)
|
||||
}
|
||||
|
||||
export function getCodeDiffStats(oldCode, newCode, language = 'javascript') {
|
||||
const oldText = toEditorCode(oldCode, language)
|
||||
const newText = toEditorCode(newCode, language)
|
||||
const changes = presentableDiff(oldText, newText)
|
||||
let additions = 0
|
||||
let deletions = 0
|
||||
|
||||
for (const change of changes) {
|
||||
const deleted = oldText.slice(change.fromA, change.toA)
|
||||
const added = newText.slice(change.fromB, change.toB)
|
||||
if (deleted) deletions += countLines(deleted)
|
||||
if (added) additions += countLines(added)
|
||||
}
|
||||
|
||||
return { additions, deletions }
|
||||
}
|
||||
|
||||
export default function DiffLabel({
|
||||
oldCode = '',
|
||||
newCode = '',
|
||||
language = 'javascript'
|
||||
}) {
|
||||
const { token } = theme.useToken()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [viewerReady, setViewerReady] = useState(false)
|
||||
const { additions, deletions } = useMemo(
|
||||
() => getCodeDiffStats(oldCode, newCode, language),
|
||||
[oldCode, newCode, language]
|
||||
)
|
||||
|
||||
const greenCount = Math.round(
|
||||
(additions / (additions + deletions)) * SQUARE_COUNT
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
className='diff-label'
|
||||
onClick={() => {
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
<Flex gap='small' align='center' style={{ marginTop: 0.5 }}>
|
||||
<Text style={{ color: token.colorSuccess }}>+{additions}</Text>
|
||||
<Flex gap='4px' align='center' style={{ marginBottom: 2 }}>
|
||||
{Array.from({ length: SQUARE_COUNT }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className='diff-label-square'
|
||||
style={{
|
||||
backgroundColor:
|
||||
i < greenCount ? token.colorSuccess : token.colorError
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
<Text style={{ color: token.colorError }}>-{deletions}</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
<Modal
|
||||
closeIcon={false}
|
||||
open={open}
|
||||
width='90%'
|
||||
destroyOnClose
|
||||
afterOpenChange={setViewerReady}
|
||||
onCancel={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
footer={
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{viewerReady ? (
|
||||
<CodeDiffViewer
|
||||
oldCode={oldCode}
|
||||
newCode={newCode}
|
||||
language={language}
|
||||
/>
|
||||
) : null}
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
DiffLabel.propTypes = {
|
||||
oldCode: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
newCode: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
language: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string)
|
||||
])
|
||||
}
|
||||
@ -1,7 +1,11 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { Descriptions, Typography, Flex } from 'antd'
|
||||
import { getModelProperty } from '../../../database/ObjectModels'
|
||||
import {
|
||||
getModelProperty,
|
||||
getPropertyValue
|
||||
} from '../../../database/ObjectModels'
|
||||
import ObjectProperty from './ObjectProperty'
|
||||
import DiffLabel from './DiffLabel'
|
||||
import ArrowRightIcon from '../../Icons/ArrowRightIcon'
|
||||
|
||||
const { Text } = Typography
|
||||
@ -51,6 +55,17 @@ const PropertyChanges = ({ type, value }) => {
|
||||
if (!changeProperty) {
|
||||
return null
|
||||
}
|
||||
|
||||
const oldValue = getPropertyValue(value?.old, key)
|
||||
const newValue = getPropertyValue(value?.new, key)
|
||||
const isCodeKey = key === 'code' || changeProperty.type === 'codeBlock'
|
||||
const isCodeEdit =
|
||||
isCodeKey &&
|
||||
oldValue != null &&
|
||||
oldValue !== '' &&
|
||||
newValue != null &&
|
||||
newValue !== ''
|
||||
|
||||
return (
|
||||
<Descriptions.Item key={key} label={changeProperty.label}>
|
||||
<Flex
|
||||
@ -58,31 +73,41 @@ const PropertyChanges = ({ type, value }) => {
|
||||
style={{ minWidth: 0, width: '100%' }}
|
||||
className='property-changes'
|
||||
>
|
||||
{value?.old ? (
|
||||
<ObjectProperty
|
||||
{...changeProperty}
|
||||
longId={false}
|
||||
minimal={true}
|
||||
showPreview={false}
|
||||
objectData={value?.old}
|
||||
maxWidth='200px'
|
||||
{isCodeEdit ? (
|
||||
<DiffLabel
|
||||
oldCode={oldValue}
|
||||
newCode={newValue}
|
||||
language={changeProperty.language}
|
||||
/>
|
||||
) : null}
|
||||
{value?.old && value?.new ? (
|
||||
<Text type='secondary'>
|
||||
<ArrowRightIcon />
|
||||
</Text>
|
||||
) : null}
|
||||
{value?.new ? (
|
||||
<ObjectProperty
|
||||
{...changeProperty}
|
||||
longId={false}
|
||||
minimal={true}
|
||||
showPreview={false}
|
||||
objectData={value?.new}
|
||||
maxWidth='200px'
|
||||
/>
|
||||
) : null}
|
||||
) : (
|
||||
<>
|
||||
{value?.old ? (
|
||||
<ObjectProperty
|
||||
{...changeProperty}
|
||||
longId={false}
|
||||
minimal={true}
|
||||
showPreview={false}
|
||||
objectData={value?.old}
|
||||
maxWidth='200px'
|
||||
/>
|
||||
) : null}
|
||||
{value?.old && value?.new ? (
|
||||
<Text type='secondary'>
|
||||
<ArrowRightIcon />
|
||||
</Text>
|
||||
) : null}
|
||||
{value?.new ? (
|
||||
<ObjectProperty
|
||||
{...changeProperty}
|
||||
longId={false}
|
||||
minimal={true}
|
||||
showPreview={false}
|
||||
objectData={value?.new}
|
||||
maxWidth='200px'
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Descriptions.Item>
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user