Skip to content

Commit 09d226d

Browse files
authored
176 expandable attachment editor (#184)
* expand attachement in a modal * test modal for attachements * update changelog
1 parent 472e484 commit 09d226d

5 files changed

Lines changed: 324 additions & 398 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
### Changed
2121

2222
- Move time controls into the conditional query toolbox header, [PR-183](https://github.com/reductstore/web-console/pull/183)
23+
- Make existing attachment JSON editors expandable with conditional-query-style toolbar actions, [PR-184](https://github.com/reductstore/web-console/pull/184)
2324

2425
## 1.13.0 - 2026-01-24
2526

src/Components/Entry/AttachmentEditor.tsx

Lines changed: 151 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import React, { useState } from "react";
2-
import { Button, Input, Typography, message } from "antd";
2+
import { Button, Input, Modal, Tooltip, Typography, message } from "antd";
33
import 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";
512
import "./EntryAttachmentsCard.css";
613

714
const 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

3445
const 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

264342
export default AttachmentEditor;

0 commit comments

Comments
 (0)