1
- import React , { useMemo } from "react" ;
1
+ import React , { Key , useCallback , useMemo , useState } from "react" ;
2
2
import ReactMarkdown , { Components } from "react-markdown" ;
3
3
import remarkBreaks from "remark-breaks" ;
4
4
import classNames from "classnames" ;
@@ -18,10 +18,22 @@ import {
18
18
Link ,
19
19
Quote ,
20
20
Strong ,
21
+ Button ,
22
+ Flex ,
23
+ Box ,
21
24
} from "@radix-ui/themes" ;
22
25
import rehypeKatex from "rehype-katex" ;
23
26
import remarkMath from "remark-math" ;
24
27
import "katex/dist/katex.min.css" ;
28
+ import { diffApi } from "../../services/refact" ;
29
+ import {
30
+ useConfig ,
31
+ useDiffApplyMutation ,
32
+ useEventsBusForIDE ,
33
+ } from "../../hooks" ;
34
+ import { selectOpenFiles } from "../../features/OpenFiles/openFilesSlice" ;
35
+ import { useSelector } from "react-redux" ;
36
+ import { ErrorCallout , DiffWarningCallout } from "../Callout" ;
25
37
26
38
export type MarkdownProps = Pick <
27
39
React . ComponentProps < typeof ReactMarkdown > ,
@@ -31,15 +43,184 @@ export type MarkdownProps = Pick<
31
43
Pick <
32
44
MarkdownCodeBlockProps ,
33
45
"startingLineNumber" | "showLineNumbers" | "useInlineStyles" | "style"
34
- > ;
46
+ > & { canHavePins ?: boolean } ;
47
+
48
+ const MaybePinButton : React . FC < {
49
+ key ?: Key | null ;
50
+ children ?: React . ReactNode ;
51
+ getMarkdown : ( pin : string ) => string | undefined ;
52
+ } > = ( { children, getMarkdown } ) => {
53
+ const { host } = useConfig ( ) ;
54
+
55
+ const { diffPreview } = useEventsBusForIDE ( ) ;
56
+ const { onSubmit, result : _result } = useDiffApplyMutation ( ) ;
57
+ const openFiles = useSelector ( selectOpenFiles ) ;
58
+ const isPin = typeof children === "string" && children . startsWith ( "📍" ) ;
59
+ const markdown = getMarkdown ( String ( children ) ) ;
60
+
61
+ const [ errorMessage , setErrorMessage ] = useState < {
62
+ type : "warning" | "error" ;
63
+ text : string ;
64
+ } | null > ( null ) ;
65
+
66
+ const [ getPatch , _patchResult ] =
67
+ diffApi . useLazyPatchSingleFileFromTicketQuery ( ) ;
68
+
69
+ const handleShow = useCallback ( ( ) => {
70
+ if ( typeof children !== "string" ) return ;
71
+ if ( ! markdown ) return ;
72
+
73
+ getPatch ( { pin : children , markdown } )
74
+ . unwrap ( )
75
+ . then ( ( patch ) => {
76
+ if ( patch . chunks . length === 0 ) {
77
+ throw new Error ( "No Chunks to show" ) ;
78
+ }
79
+ diffPreview ( patch ) ;
80
+ } )
81
+ . catch ( ( ) => {
82
+ setErrorMessage ( { type : "warning" , text : "No patch to show" } ) ;
83
+ } ) ;
84
+ } , [ children , diffPreview , getPatch , markdown ] ) ;
85
+
86
+ const handleApply = useCallback ( ( ) => {
87
+ if ( typeof children !== "string" ) return ;
88
+ if ( ! markdown ) return ;
89
+ getPatch ( { pin : children , markdown } )
90
+ . unwrap ( )
91
+ . then ( ( patch ) => {
92
+ const files = patch . results . reduce < string [ ] > ( ( acc , cur ) => {
93
+ const { file_name_add, file_name_delete, file_name_edit } = cur ;
94
+ if ( file_name_add ) acc . push ( file_name_add ) ;
95
+ if ( file_name_delete ) acc . push ( file_name_delete ) ;
96
+ if ( file_name_edit ) acc . push ( file_name_edit ) ;
97
+ return acc ;
98
+ } , [ ] ) ;
99
+
100
+ if ( files . length === 0 ) {
101
+ setErrorMessage ( { type : "warning" , text : "No chunks to apply" } ) ;
102
+ return ;
103
+ }
104
+
105
+ const fileIsOpen = files . some ( ( file ) => openFiles . includes ( file ) ) ;
106
+
107
+ if ( fileIsOpen ) {
108
+ diffPreview ( patch ) ;
109
+ } else {
110
+ const chunks = patch . chunks ;
111
+ const toApply = chunks . map ( ( ) => true ) ;
112
+ void onSubmit ( { chunks, toApply } ) ;
113
+ }
114
+ } )
115
+ . catch ( ( error : Error ) => {
116
+ setErrorMessage ( {
117
+ type : "error" ,
118
+ text : error . message
119
+ ? "Failed to apply patch: " + error . message
120
+ : "Failed to apply patch." ,
121
+ } ) ;
122
+ } ) ;
123
+ } , [ children , diffPreview , getPatch , markdown , onSubmit , openFiles ] ) ;
124
+
125
+ const handleCalloutClick = useCallback ( ( ) => {
126
+ setErrorMessage ( null ) ;
127
+ } , [ ] ) ;
128
+
129
+ if ( isPin ) {
130
+ return (
131
+ < Box >
132
+ < Flex my = "2" gap = "2" wrap = "wrap-reverse" >
133
+ < Text
134
+ as = "p"
135
+ wrap = "wrap"
136
+ style = { { lineBreak : "anywhere" , wordBreak : "break-all" } }
137
+ >
138
+ { children }
139
+ </ Text >
140
+ < Flex gap = "2" justify = "end" ml = "auto" >
141
+ { host !== "web" && (
142
+ < Button
143
+ size = "1"
144
+ // loading={patchResult.isFetching}
145
+ onClick = { handleShow }
146
+ title = "Show Patch"
147
+ disabled = { ! ! errorMessage }
148
+ >
149
+ Open
150
+ </ Button >
151
+ ) }
152
+ < Button
153
+ size = "1"
154
+ // loading={patchResult.isFetching}
155
+ onClick = { handleApply }
156
+ disabled = { ! ! errorMessage }
157
+ title = "Apply patch"
158
+ >
159
+ Apply
160
+ </ Button >
161
+ </ Flex >
162
+ </ Flex >
163
+ { errorMessage && errorMessage . type === "error" && (
164
+ < ErrorCallout onClick = { handleCalloutClick } timeout = { 3000 } >
165
+ { errorMessage . text }
166
+ </ ErrorCallout >
167
+ ) }
168
+ { errorMessage && errorMessage . type === "warning" && (
169
+ < DiffWarningCallout
170
+ timeout = { 3000 }
171
+ onClick = { handleCalloutClick }
172
+ message = { errorMessage . text }
173
+ />
174
+ ) }
175
+ </ Box >
176
+ ) ;
177
+ }
178
+
179
+ return (
180
+ < Text my = "2" as = "p" >
181
+ { children }
182
+ </ Text >
183
+ ) ;
184
+ } ;
185
+
186
+ function processPinAndMarkdown ( message ?: string | null ) : Map < string , string > {
187
+ if ( ! message ) return new Map < string , string > ( ) ;
188
+
189
+ const regexp = / 📍 [ \s \S ] * ?\n ` ` ` \n / g;
190
+
191
+ const results = message . match ( regexp ) ?? [ ] ;
192
+
193
+ const pinsAndMarkdown = results . map < [ string , string ] > ( ( result ) => {
194
+ const firstNewLine = result . indexOf ( "\n" ) ;
195
+ const pin = result . slice ( 0 , firstNewLine ) ;
196
+ const markdown = result . slice ( firstNewLine + 1 ) ;
197
+ return [ pin , markdown ] ;
198
+ } ) ;
199
+
200
+ const hashMap = new Map ( pinsAndMarkdown ) ;
201
+
202
+ return hashMap ;
203
+ }
35
204
36
205
const _Markdown : React . FC < MarkdownProps > = ( {
37
206
children,
38
207
allowedElements,
39
208
unwrapDisallowed,
40
-
209
+ canHavePins ,
41
210
...rest
42
211
} ) => {
212
+ const pinsAndMarkdown = useMemo < Map < string , string > > (
213
+ ( ) => processPinAndMarkdown ( children ) ,
214
+ [ children ] ,
215
+ ) ;
216
+
217
+ const getMarkDownForPin = useCallback (
218
+ ( pin : string ) => {
219
+ return pinsAndMarkdown . get ( pin ) ;
220
+ } ,
221
+ [ pinsAndMarkdown ] ,
222
+ ) ;
223
+
43
224
const components : Partial < Components > = useMemo ( ( ) => {
44
225
return {
45
226
ol ( props ) {
@@ -56,6 +237,9 @@ const _Markdown: React.FC<MarkdownProps> = ({
56
237
return < MarkdownCodeBlock { ...props } { ...rest } /> ;
57
238
} ,
58
239
p ( { color : _color , ref : _ref , node : _node , ...props } ) {
240
+ if ( canHavePins ) {
241
+ return < MaybePinButton { ...props } getMarkdown = { getMarkDownForPin } /> ;
242
+ }
59
243
return < Text my = "2" as = "p" { ...props } /> ;
60
244
} ,
61
245
h1 ( { color : _color , ref : _ref , node : _node , ...props } ) {
@@ -101,7 +285,7 @@ const _Markdown: React.FC<MarkdownProps> = ({
101
285
return < Em { ...props } /> ;
102
286
} ,
103
287
} ;
104
- } , [ rest ] ) ;
288
+ } , [ getMarkDownForPin , rest , canHavePins ] ) ;
105
289
return (
106
290
< ReactMarkdown
107
291
className = { styles . markdown }
0 commit comments