Skip to content

Commit a31357e

Browse files
committed
[add] Git Diff View component for Lark JSON diff
1 parent 41707dd commit a31357e

8 files changed

Lines changed: 2881 additions & 3391 deletions

File tree

components/GitDiffView.tsx

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import '@git-diff-view/react/styles/diff-view.css';
2+
3+
import { generateDiffFile } from '@git-diff-view/file';
4+
import { DiffModeEnum, DiffView } from '@git-diff-view/react';
5+
import { computed, observable } from 'mobx';
6+
import { observer } from 'mobx-react';
7+
import { ObservedComponent } from 'mobx-react-helper';
8+
import { Alert, Button, ButtonGroup, Form } from 'react-bootstrap';
9+
10+
export interface TextFileData {
11+
fileName: string;
12+
content: string;
13+
language?: string;
14+
}
15+
16+
export type GitDiffViewProps = Record<'oldFile' | 'newFile', TextFileData>;
17+
18+
@observer
19+
export class GitDiffView extends ObservedComponent<GitDiffViewProps> {
20+
@observable
21+
accessor diffViewMode = DiffModeEnum.Split;
22+
23+
@observable
24+
accessor diffViewWrap = false;
25+
26+
@observable
27+
accessor diffViewHighlight = true;
28+
29+
@observable
30+
accessor expandAll = false;
31+
32+
@computed
33+
get hasNoDiff() {
34+
const { oldFile, newFile } = this.observedProps;
35+
36+
return oldFile.content === newFile.content;
37+
}
38+
39+
@computed
40+
get diffFile() {
41+
const { oldFile, newFile } = this.observedProps;
42+
const { diffViewMode, expandAll } = this;
43+
44+
const file = generateDiffFile(
45+
oldFile.fileName,
46+
oldFile.content,
47+
newFile.fileName,
48+
newFile.content,
49+
oldFile.language || 'text',
50+
newFile.language || 'text',
51+
);
52+
53+
file.init();
54+
55+
if (diffViewMode & DiffModeEnum.Split) {
56+
file.buildSplitDiffLines();
57+
} else {
58+
file.buildUnifiedDiffLines();
59+
}
60+
61+
if (expandAll)
62+
file.onAllExpand(diffViewMode & DiffModeEnum.Split ? 'split' : 'unified');
63+
64+
return file;
65+
}
66+
67+
renderDiff() {
68+
const {
69+
diffViewMode,
70+
diffViewWrap,
71+
diffViewHighlight,
72+
expandAll,
73+
diffFile,
74+
} = this;
75+
76+
return (
77+
<>
78+
<div className="d-flex flex-wrap align-items-center gap-2 mb-3">
79+
<ButtonGroup size="sm" aria-label="Diff mode">
80+
<Button
81+
variant={
82+
diffViewMode & DiffModeEnum.Split
83+
? 'primary'
84+
: 'outline-primary'
85+
}
86+
onClick={() => (this.diffViewMode = DiffModeEnum.Split)}
87+
>
88+
Split
89+
</Button>
90+
<Button
91+
variant={
92+
diffViewMode & DiffModeEnum.Split
93+
? 'outline-primary'
94+
: 'primary'
95+
}
96+
onClick={() => (this.diffViewMode = DiffModeEnum.Unified)}
97+
>
98+
Unified
99+
</Button>
100+
</ButtonGroup>
101+
102+
<Form.Check
103+
type="switch"
104+
id="text-file-diff-wrap"
105+
label="Wrap"
106+
checked={diffViewWrap}
107+
onChange={({ currentTarget }) =>
108+
(this.diffViewWrap = currentTarget.checked)
109+
}
110+
/>
111+
<Form.Check
112+
type="switch"
113+
id="text-file-diff-highlight"
114+
label="Highlight"
115+
checked={diffViewHighlight}
116+
onChange={({ currentTarget }) =>
117+
(this.diffViewHighlight = currentTarget.checked)
118+
}
119+
/>
120+
<Form.Check
121+
type="switch"
122+
id="text-file-diff-expand-all"
123+
label="Expand all"
124+
checked={expandAll}
125+
onChange={({ currentTarget }) =>
126+
(this.expandAll = currentTarget.checked)
127+
}
128+
/>
129+
</div>
130+
131+
<DiffView
132+
diffFile={diffFile}
133+
diffViewMode={diffViewMode}
134+
diffViewWrap={diffViewWrap}
135+
diffViewHighlight={diffViewHighlight}
136+
/>
137+
</>
138+
);
139+
}
140+
141+
render() {
142+
const { hasNoDiff } = this;
143+
144+
return hasNoDiff ? (
145+
<>
146+
<Alert variant="danger">No diff found between the two files.</Alert>
147+
148+
<pre>{this.props.oldFile.content}</pre>
149+
</>
150+
) : (
151+
this.renderDiff()
152+
);
153+
}
154+
}

eslint.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export default defineConfig(
2626
'simple-import-sort': simpleImportSortPlugin,
2727
'@typescript-eslint': tsEslint.plugin,
2828
react,
29-
// @ts-expect-error https://github.com/vercel/next.js/discussions/84792
3029
'@next/next': nextPlugin,
3130
},
3231
},

package.json

Lines changed: 37 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,101 +8,94 @@
88
},
99
"dependencies": {
1010
"@editorjs/code": "^2.9.4",
11-
"@editorjs/editorjs": "^2.31.5",
12-
"@editorjs/header": "^2.8.8",
11+
"@editorjs/editorjs": "^2.31.6",
12+
"@editorjs/header": "^2.8.9",
1313
"@editorjs/image": "^2.10.3",
1414
"@editorjs/link": "^2.6.2",
1515
"@editorjs/list": "^2.0.9",
1616
"@editorjs/paragraph": "^2.11.7",
1717
"@editorjs/quote": "^2.7.6",
18+
"@git-diff-view/file": "^0.1.5",
19+
"@git-diff-view/react": "^0.1.5",
1820
"@mdx-js/loader": "^3.1.1",
1921
"@mdx-js/react": "^3.1.1",
20-
"@next/mdx": "^16.2.2",
21-
"@sentry/nextjs": "^10.47.0",
22+
"@next/mdx": "^16.2.9",
23+
"@sentry/nextjs": "^10.57.0",
2224
"copy-webpack-plugin": "^14.0.0",
2325
"core-js": "^3.49.0",
2426
"editorjs-html": "^4.0.5",
25-
"file-type": "^22.0.0",
27+
"file-type": "^22.0.1",
2628
"formidable": "^3.5.4",
27-
"idea-react": "^2.0.0-rc.13",
29+
"idea-react": "^2.2.2",
2830
"jsonwebtoken": "^9.0.3",
29-
"koa": "^3.2.0",
31+
"koa": "^3.2.1",
3032
"koa-jwt": "^4.0.4",
3133
"koajax": "^3.3.0",
3234
"less": "^4.6.4",
33-
"less-loader": "^12.3.2",
35+
"less-loader": "^13.0.0",
3436
"lodash": "^4.18.1",
35-
"marked": "^17.0.6",
37+
"marked": "^18.0.5",
3638
"mime": "^4.1.0",
37-
"mobx": "^6.15.0",
39+
"mobx": "^6.16.1",
3840
"mobx-github": "^0.6.2",
3941
"mobx-i18n": "^0.7.2",
4042
"mobx-lark": "^2.8.1",
41-
"mobx-react": "^9.2.1",
43+
"mobx-react": "^9.2.2",
4244
"mobx-react-helper": "^0.5.1",
4345
"mobx-restful": "^2.1.4",
4446
"mobx-restful-table": "^2.6.3",
45-
"next": "^16.2.2",
47+
"next": "^16.2.9",
4648
"next-pwa": "~5.6.0",
4749
"next-ssr-middleware": "^1.1.0",
4850
"next-with-less": "^3.0.1",
4951
"prismjs": "^1.30.0",
50-
"react": "^19.2.4",
52+
"react": "^19.2.7",
5153
"react-bootstrap": "^2.10.10",
5254
"react-bootstrap-editor": "^2.1.1",
53-
"react-dom": "^19.2.4",
55+
"react-dom": "^19.2.7",
5456
"react-editor-js": "^2.1.0",
5557
"remark-frontmatter": "^5.0.0",
5658
"remark-gfm": "^4.0.1",
5759
"remark-mdx-frontmatter": "^5.2.0",
58-
"undici": "^8.0.2",
59-
"web-utility": "^4.6.5",
60-
"webpack": "^5.105.4",
61-
"yaml": "^2.8.3"
60+
"undici": "^8.4.1",
61+
"web-utility": "^4.6.6",
62+
"webpack": "^5.107.2",
63+
"yaml": "^2.9.0"
6264
},
6365
"devDependencies": {
64-
"@babel/plugin-proposal-decorators": "^7.29.0",
65-
"@babel/plugin-transform-typescript": "^7.28.6",
66-
"@babel/preset-react": "^7.28.5",
67-
"@cspell/eslint-plugin": "^10.0.0",
66+
"@babel/plugin-proposal-decorators": "^7.29.7",
67+
"@babel/plugin-transform-typescript": "^7.29.7",
68+
"@babel/preset-react": "^7.29.7",
69+
"@cspell/eslint-plugin": "^10.0.1",
6870
"@eslint/js": "^10.0.1",
69-
"@next/eslint-plugin-next": "^16.2.2",
71+
"@next/eslint-plugin-next": "^16.2.9",
7072
"@softonus/prettier-plugin-duplicate-remover": "^1.1.2",
7173
"@stylistic/eslint-plugin": "^5.10.0",
7274
"@types/eslint-config-prettier": "^6.11.3",
73-
"@types/formidable": "^3.5.0",
75+
"@types/formidable": "^3.5.1",
7476
"@types/jsonwebtoken": "^9.0.10",
75-
"@types/koa": "^3.0.2",
77+
"@types/koa": "^3.0.3",
7678
"@types/lodash": "^4.17.24",
7779
"@types/next-pwa": "^5.6.9",
78-
"@types/node": "^24.12.2",
79-
"@types/react": "^19.2.14",
80-
"eslint": "^10.2.0",
81-
"eslint-config-next": "^16.2.2",
80+
"@types/node": "^24.13.2",
81+
"@types/react": "^19.2.17",
82+
"eslint": "^10.5.0",
83+
"eslint-config-next": "^16.2.9",
8284
"eslint-config-prettier": "^10.1.8",
8385
"eslint-plugin-react": "^7.37.5",
84-
"eslint-plugin-simple-import-sort": "^12.1.1",
85-
"globals": "^17.4.0",
86+
"eslint-plugin-simple-import-sort": "^13.0.0",
87+
"globals": "^17.6.0",
8688
"husky": "^9.1.7",
87-
"jiti": "^2.6.1",
88-
"lint-staged": "^16.4.0",
89-
"prettier": "^3.8.1",
89+
"jiti": "^2.7.0",
90+
"lint-staged": "^17.0.7",
91+
"prettier": "^3.8.4",
9092
"prettier-plugin-css-order": "^2.2.0",
9193
"typescript": "~5.9.3",
92-
"typescript-eslint": "^8.58.0"
94+
"typescript-eslint": "^8.61.0"
9395
},
9496
"resolutions": {
9597
"next": "$next"
9698
},
97-
"pnpm": {
98-
"onlyBuiltDependencies": [
99-
"@sentry/cli",
100-
"core-js",
101-
"less",
102-
"sharp",
103-
"unrs-resolver"
104-
]
105-
},
10699
"prettier": {
107100
"singleQuote": true,
108101
"trailingComma": "all",

pages/api/Lark/file/[id]/[name].ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Middleware } from 'koa';
33
import MIME from 'mime';
44
import { createKoaRouter, withKoaRouter } from 'next-ssr-middleware';
55
import { Readable } from 'stream';
6+
import { parseJSON } from 'web-utility';
67

78
import { CACHE_HOST } from '../../../../../models/configuration';
89
import { safeAPI } from '../../../core';
@@ -31,11 +32,7 @@ const downloader: Middleware = async context => {
3132
if (!ok) {
3233
context.status = status;
3334

34-
try {
35-
return (context.body = await response.json());
36-
} catch {
37-
return (context.body = await response.text());
38-
}
35+
return (context.body = parseJSON(await response.text()));
3936
}
4037
const mime = headers.get('Content-Type'),
4138
[stream1, stream2] = body!.tee();

pages/wiki/[node_token]/debugger.tsx

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { CodeBlock } from 'idea-react';
21
import { Block, DocumentBlockModel, renderBlocks, WikiNode } from 'mobx-lark';
32
import { GetServerSideProps } from 'next';
43
import { FC } from 'react';
5-
import { Col, Container, Row } from 'react-bootstrap';
4+
import { Container } from 'react-bootstrap';
65

6+
import { GitDiffView } from '../../../components/GitDiffView';
77
import { PageHead } from '../../../components/Layout/PageHead';
88
import documentStore from '../../../models/Document';
99
import wikiStore from '../../../models/Wiki';
@@ -43,38 +43,33 @@ const WikiDocumentDebuggerPage: FC<WikiDocumentDebuggerPageProps> = ({
4343
rawBlocks,
4444
renderableBlocks,
4545
}) => {
46+
const title = `${node.title} - Debugger`;
4647
const rendered = renderBlocks(renderableBlocks);
4748

4849
return (
4950
<Container>
50-
<PageHead title={`${node.title} - Debugger`} />
51+
<PageHead title={title} />
52+
<h1>{title}</h1>
5153

52-
<Row>
53-
<Col>
54-
<h2>Raw Blocks</h2>
55-
56-
<CodeBlock language="json">
57-
{JSON.stringify(rawBlocks, null, 2)}
58-
</CodeBlock>
59-
</Col>
60-
61-
<Col>
62-
<h2>Renderable Blocks</h2>
63-
64-
<CodeBlock language="json">
65-
{JSON.stringify(renderableBlocks, null, 2)}
66-
</CodeBlock>
67-
</Col>
68-
69-
<Col>
70-
<h2>Component Tree</h2>
71-
72-
<CodeBlock language="tsx">{rendered}</CodeBlock>
73-
</Col>
74-
</Row>
54+
<section>
55+
<h2>🔍Block diff</h2>
56+
57+
<GitDiffView
58+
oldFile={{
59+
fileName: 'raw-blocks.json',
60+
content: JSON.stringify(rawBlocks, null, 2),
61+
language: 'json',
62+
}}
63+
newFile={{
64+
fileName: 'renderable-blocks.json',
65+
content: JSON.stringify(renderableBlocks, null, 2),
66+
language: 'json',
67+
}}
68+
/>
69+
</section>
7570

7671
<section>
77-
<h2>Document</h2>
72+
<h2>📄Document</h2>
7873

7974
{rendered}
8075
</section>

0 commit comments

Comments
 (0)