11import React , { useState } from "react" ;
2- import { Button , Input , Typography , message } from "antd" ;
2+ import { Button , Input , Modal , Tooltip , Typography , message } from "antd" ;
33import Editor from "@monaco-editor/react" ;
4- import { SaveOutlined , CloseOutlined , CopyOutlined } from "@ant-design/icons" ;
4+ import type * as monaco from "monaco-editor" ;
5+ import {
6+ SaveOutlined ,
7+ CloseOutlined ,
8+ CompressOutlined ,
9+ ExpandOutlined ,
10+ FormatPainterOutlined ,
11+ } from "@ant-design/icons" ;
512import "./EntryAttachmentsCard.css" ;
613
714const readFileAsText = ( file : File ) : Promise < string > =>
@@ -29,6 +36,10 @@ interface AttachmentEditorProps {
2936 error ?: string | null ;
3037 /** Callback to clear external error */
3138 onClearError ?: ( ) => void ;
39+ /** Whether the editor can be expanded into a modal */
40+ expandable ?: boolean ;
41+ /** Modal title when expanded */
42+ expandedTitle ?: string ;
3243}
3344
3445const AttachmentEditor : React . FC < AttachmentEditorProps > = ( {
@@ -40,11 +51,16 @@ const AttachmentEditor: React.FC<AttachmentEditorProps> = ({
4051 isSaving = false ,
4152 error = null ,
4253 onClearError,
54+ expandable = false ,
55+ expandedTitle = "Attachment Editor" ,
4356} ) => {
4457 const [ name , setName ] = useState ( initialKey ) ;
4558 const [ content , setContent ] = useState ( initialValue ) ;
4659 const [ localError , setLocalError ] = useState < string | null > ( null ) ;
4760 const [ isDragOver , setIsDragOver ] = useState ( false ) ;
61+ const [ isExpanded , setIsExpanded ] = useState ( false ) ;
62+ const [ editorInstance , setEditorInstance ] =
63+ useState < monaco . editor . IStandaloneCodeEditor | null > ( null ) ;
4864
4965 const displayError = error ?? localError ;
5066
@@ -62,29 +78,33 @@ const AttachmentEditor: React.FC<AttachmentEditorProps> = ({
6278 }
6379 } ;
6480
65- const copyToClipboard = async ( ) => {
66- try {
67- await navigator . clipboard . writeText ( content ) ;
68- message . success ( "Copied to clipboard" ) ;
69- } catch {
70- message . error ( "Failed to copy" ) ;
71- }
81+ const handleFormat = ( ) => {
82+ editorInstance ?. getAction ( "editor.action.formatDocument" ) ?. run ( ) ;
7283 } ;
7384
7485 const handleSave = async ( ) => {
86+ if ( ! readOnly && editorInstance ) {
87+ await editorInstance . getAction ( "editor.action.formatDocument" ) ?. run ( ) ;
88+ }
89+
90+ const currentContent = editorInstance ?. getValue ( ) ?? content ;
91+ if ( currentContent !== content ) {
92+ setContent ( currentContent ) ;
93+ }
94+
7595 const trimmedKey = name . trim ( ) ;
7696 if ( ! trimmedKey ) {
7797 setLocalError ( "Name cannot be empty." ) ;
7898 return ;
7999 }
80100
81- const jsonError = validateJson ( content ) ;
101+ const jsonError = validateJson ( currentContent ) ;
82102 if ( jsonError ) {
83103 setLocalError ( `Invalid JSON: ${ jsonError } ` ) ;
84104 return ;
85105 }
86106
87- const success = await onSave ( trimmedKey , content ) ;
107+ const success = await onSave ( trimmedKey , currentContent ) ;
88108 if ( success ) {
89109 setLocalError ( null ) ;
90110 }
@@ -109,6 +129,7 @@ const AttachmentEditor: React.FC<AttachmentEditorProps> = ({
109129 setLocalError ( null ) ;
110130 onClearError ?.( ) ;
111131 } else {
132+ setIsExpanded ( false ) ;
112133 onClose ( ) ;
113134 }
114135 } ;
@@ -167,7 +188,7 @@ const AttachmentEditor: React.FC<AttachmentEditorProps> = ({
167188 Math . max ( 100 , ( content + "\n" ) . split ( "\n" ) . length * 18 ) ,
168189 ) ;
169190
170- return (
191+ const renderEditor = ( expanded = false ) => (
171192 < div className = "expandedEditRow" >
172193 < div className = "expandedEditFields" >
173194 < Input
@@ -177,7 +198,7 @@ const AttachmentEditor: React.FC<AttachmentEditorProps> = ({
177198 disabled = { readOnly }
178199 />
179200 < div
180- className = { `monacoEditorWrapper${ isDragOver ? " dragOver" : "" } ` }
201+ className = { `monacoEditorWrapper${ isDragOver ? " dragOver" : "" } ${ expanded ? " expanded" : "" } ` }
181202 onDrop = { handleDrop }
182203 onDragOver = { handleDragOver }
183204 onDragLeave = { handleDragLeave }
@@ -189,76 +210,133 @@ const AttachmentEditor: React.FC<AttachmentEditorProps> = ({
189210 </ Typography . Text >
190211 </ div >
191212 ) }
192- < Editor
193- height = { `${ editorHeight } px` }
194- language = "json"
195- value = { content }
196- onChange = { handleContentChange }
197- options = { {
198- minimap : { enabled : false } ,
199- lineNumbers : "on" ,
200- scrollBeyondLastLine : false ,
201- wordWrap : "on" ,
202- automaticLayout : true ,
203- folding : false ,
204- glyphMargin : false ,
205- lineDecorationsWidth : 10 ,
206- lineNumbersMinChars : 3 ,
207- renderLineHighlight : "none" ,
208- scrollbar : {
209- vertical : "auto" ,
210- horizontal : "hidden" ,
211- verticalScrollbarSize : 8 ,
212- } ,
213- readOnly : readOnly ,
214- quickSuggestions : false ,
215- suggestOnTriggerCharacters : false ,
216- parameterHints : { enabled : false } ,
217- } }
218- />
213+ < div className = { `attachmentEditorBody${ expanded ? " expanded" : "" } ` } >
214+ < Editor
215+ height = { expanded ? "100%" : `${ editorHeight } px` }
216+ language = "json"
217+ value = { content }
218+ onChange = { handleContentChange }
219+ onMount = { ( editor ) => setEditorInstance ( editor ) }
220+ options = { {
221+ minimap : { enabled : false } ,
222+ lineNumbers : "on" ,
223+ scrollBeyondLastLine : false ,
224+ wordWrap : "on" ,
225+ automaticLayout : true ,
226+ folding : false ,
227+ glyphMargin : false ,
228+ lineDecorationsWidth : 10 ,
229+ lineNumbersMinChars : 3 ,
230+ renderLineHighlight : "none" ,
231+ scrollbar : {
232+ vertical : "auto" ,
233+ horizontal : "hidden" ,
234+ verticalScrollbarSize : 8 ,
235+ } ,
236+ readOnly : readOnly ,
237+ quickSuggestions : false ,
238+ suggestOnTriggerCharacters : false ,
239+ parameterHints : { enabled : false } ,
240+ } }
241+ />
242+ </ div >
219243 </ div >
220244 </ div >
221- < div className = "expandedEditActions" >
222- { ! readOnly && ! displayError && (
223- < span className = "dropHintText" >
224- < Typography . Text type = "secondary" >
225- Tip: drag & drop JSON file
226- </ Typography . Text >
227- </ span >
228- ) }
229- { displayError && (
230- < span className = "expandedRowError" >
231- < span className = "expandedRowErrorX" > ✗</ span >
232- < span > { displayError } </ span >
233- </ span >
234- ) }
235- < Button size = "small" icon = { < CopyOutlined /> } onClick = { copyToClipboard } >
236- Copy
237- </ Button >
238- { ! readOnly && (
245+ < div className = "attachmentEditorToolbar" >
246+ < div className = "attachmentEditorToolbarStatus" >
247+ { ! readOnly && ! displayError && (
248+ < span className = "dropHintText" >
249+ < Typography . Text type = "secondary" >
250+ Tip: drag & drop JSON file
251+ </ Typography . Text >
252+ </ span >
253+ ) }
254+ { displayError && (
255+ < span className = "expandedRowError" >
256+ < span className = "expandedRowErrorX" > ✗</ span >
257+ < span > { displayError } </ span >
258+ </ span >
259+ ) }
260+ </ div >
261+ < div className = "attachmentEditorToolbarActions" >
239262 < Button
240263 size = "small"
241- type = "primary"
242- icon = { < SaveOutlined /> }
243- onClick = { handleSave }
244- loading = { isSaving }
245- disabled = { ! hasChanges && initialKey !== "" }
264+ icon = { < CloseOutlined /> }
265+ onClick = { handleClose }
266+ className = { `attachmentEditorCancelButton${ hasChanges ? "" : " hidden" } ` }
267+ disabled = { ! hasChanges }
268+ aria-hidden = { ! hasChanges }
269+ tabIndex = { hasChanges ? 0 : - 1 }
246270 >
247- Save
248- </ Button >
249- ) }
250- { hasChanges ? (
251- < Button size = "small" icon = { < CloseOutlined /> } onClick = { handleClose } >
252271 Cancel
253272 </ Button >
254- ) : (
255- < Button size = "small" icon = { < CloseOutlined /> } onClick = { onClose } >
256- Close
257- </ Button >
258- ) }
273+ { ! readOnly && (
274+ < Button
275+ size = "small"
276+ type = "primary"
277+ icon = { < SaveOutlined /> }
278+ onClick = { handleSave }
279+ loading = { isSaving }
280+ disabled = { ! hasChanges && initialKey !== "" }
281+ >
282+ Save
283+ </ Button >
284+ ) }
285+ < Tooltip
286+ title = { readOnly ? "Cannot format in read-only mode" : "Format JSON" }
287+ >
288+ < Button
289+ size = "small"
290+ aria-label = "Format JSON"
291+ icon = { < FormatPainterOutlined /> }
292+ onClick = { handleFormat }
293+ disabled = { readOnly }
294+ />
295+ </ Tooltip >
296+ { expandable && (
297+ < Tooltip title = { isExpanded ? "Collapse editor" : "Expand editor" } >
298+ < Button
299+ size = "small"
300+ aria-label = { isExpanded ? "Collapse editor" : "Expand editor" }
301+ icon = { isExpanded ? < CompressOutlined /> : < ExpandOutlined /> }
302+ onClick = { ( ) => setIsExpanded ( ( prev ) => ! prev ) }
303+ />
304+ </ Tooltip >
305+ ) }
306+ </ div >
259307 </ div >
260308 </ div >
261309 ) ;
310+
311+ return (
312+ < >
313+ { isExpanded ? (
314+ < div className = "attachmentEditorPlaceholder" >
315+ Editing in expanded attachment editor
316+ </ div >
317+ ) : (
318+ renderEditor ( false )
319+ ) }
320+ { expandable && isExpanded && (
321+ < Modal
322+ open = { isExpanded }
323+ onCancel = { ( ) => setIsExpanded ( false ) }
324+ footer = { null }
325+ closable
326+ title = { expandedTitle }
327+ maskClosable = { false }
328+ keyboard = { false }
329+ className = "attachmentEditorModal"
330+ width = "90vw"
331+ centered
332+ >
333+ < div className = "attachmentEditorModalContent" >
334+ { renderEditor ( true ) }
335+ </ div >
336+ </ Modal >
337+ ) }
338+ </ >
339+ ) ;
262340} ;
263341
264342export default AttachmentEditor ;
0 commit comments