@vertesia/ui 1.1.1-dev.20260505.163000Z → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/esm/features/store/collections/BrowseCollectionView.js +1 -1
- package/lib/esm/features/store/collections/BrowseCollectionView.js.map +1 -1
- package/lib/esm/features/store/collections/EditCollectionView.js +23 -3
- package/lib/esm/features/store/collections/EditCollectionView.js.map +1 -1
- package/lib/esm/features/store/objects/components/ContentOverview.js +13 -6
- package/lib/esm/features/store/objects/components/ContentOverview.js.map +1 -1
- package/lib/esm/features/store/objects/components/TextEditorPanel.js +3 -1
- package/lib/esm/features/store/objects/components/TextEditorPanel.js.map +1 -1
- package/lib/esm/features/store/objects/components/useDownloadFile.js +2 -0
- package/lib/esm/features/store/objects/components/useDownloadFile.js.map +1 -1
- package/lib/esm/i18n/locales/en.json +3 -0
- package/lib/esm/widgets/json-view/JSONEditor.js +53 -0
- package/lib/esm/widgets/json-view/JSONEditor.js.map +1 -0
- package/lib/esm/widgets/json-view/index.js +1 -0
- package/lib/esm/widgets/json-view/index.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types/features/store/collections/BrowseCollectionView.d.ts.map +1 -1
- package/lib/types/features/store/collections/EditCollectionView.d.ts.map +1 -1
- package/lib/types/features/store/objects/components/TextEditorPanel.d.ts.map +1 -1
- package/lib/types/features/store/objects/components/useDownloadFile.d.ts +4 -0
- package/lib/types/features/store/objects/components/useDownloadFile.d.ts.map +1 -1
- package/lib/types/widgets/json-view/JSONEditor.d.ts +28 -0
- package/lib/types/widgets/json-view/JSONEditor.d.ts.map +1 -0
- package/lib/types/widgets/json-view/index.d.ts +1 -0
- package/lib/types/widgets/json-view/index.d.ts.map +1 -1
- package/lib/vertesia-ui-core.js +1 -1
- package/lib/vertesia-ui-core.js.map +1 -1
- package/lib/vertesia-ui-features.js +1 -1
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-i18n.js +1 -1
- package/lib/vertesia-ui-i18n.js.map +1 -1
- package/lib/vertesia-ui-layout.js +1 -1
- package/lib/vertesia-ui-layout.js.map +1 -1
- package/lib/vertesia-ui-session.js +1 -1
- package/lib/vertesia-ui-session.js.map +1 -1
- package/lib/vertesia-ui-shell.js +1 -1
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/lib/vertesia-ui-widgets.js +1 -1
- package/lib/vertesia-ui-widgets.js.map +1 -1
- package/package.json +5 -5
- package/src/features/store/collections/BrowseCollectionView.tsx +8 -4
- package/src/features/store/collections/EditCollectionView.tsx +53 -2
- package/src/features/store/objects/components/ContentOverview.tsx +58 -40
- package/src/features/store/objects/components/TextEditorPanel.tsx +3 -1
- package/src/features/store/objects/components/useDownloadFile.ts +6 -0
- package/src/i18n/locales/en.json +3 -0
- package/src/widgets/json-view/JSONEditor.tsx +89 -0
- package/src/widgets/json-view/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertesia/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Vertesia UI components and and hooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -86,10 +86,10 @@
|
|
|
86
86
|
"vega": "^6.2.0",
|
|
87
87
|
"vega-embed": "^7.1.0",
|
|
88
88
|
"vega-lite": "^6.4.1",
|
|
89
|
-
"@vertesia/client": "1.
|
|
90
|
-
"@vertesia/
|
|
91
|
-
"@vertesia/
|
|
92
|
-
"@vertesia/
|
|
89
|
+
"@vertesia/client": "1.3.0",
|
|
90
|
+
"@vertesia/fusion-ux": "1.3.0",
|
|
91
|
+
"@vertesia/json": "1.3.0",
|
|
92
|
+
"@vertesia/common": "1.3.0"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
95
|
"@eslint/compat": "^2.0.2",
|
|
@@ -36,10 +36,14 @@ export function BrowseCollectionView({ collection }: BrowseCollectionViewProps)
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
const tableLayout = getTableLayout(collection, typeRegistry);
|
|
39
|
-
return
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex flex-col h-full">
|
|
41
|
+
{collection.dynamic ? (
|
|
42
|
+
<DocumentSearchResults layout={tableLayout} />
|
|
43
|
+
) : (
|
|
44
|
+
<DocumentSearchResultsWithDropZone onUploadDone={onUploadDone} layout={tableLayout} />
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
43
47
|
)
|
|
44
48
|
}
|
|
45
49
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Collection, CreateCollectionPayload, getContentTypeRefId, JSONSchemaObject } from "@vertesia/common";
|
|
2
|
-
import { Button, ErrorBox, FormItem, Input, Panel, Styles, Textarea, useFetch, useToast, useTheme } from "@vertesia/ui/core";
|
|
1
|
+
import { Collection, CreateCollectionPayload, getContentTypeRefId, JSONSchemaObject, SecurityLevelLabels } from "@vertesia/common";
|
|
2
|
+
import { Badge, Button, ErrorBox, FormItem, Input, Panel, SelectBox, Styles, Textarea, useFetch, useToast, useTheme } from "@vertesia/ui/core";
|
|
3
3
|
import { SharedPropsEditor, SyncMemberHeadsToggle, UserInfo } from "@vertesia/ui/features";
|
|
4
4
|
import { useUserSession } from "@vertesia/ui/session";
|
|
5
5
|
import { MonacoEditor, EditorApi, GeneratedForm, ManagedObject, Node } from "@vertesia/ui/widgets";
|
|
@@ -16,6 +16,8 @@ interface UpdateData {
|
|
|
16
16
|
tags: string[];
|
|
17
17
|
type: string;
|
|
18
18
|
allowed_types: string[];
|
|
19
|
+
sensitivity?: number;
|
|
20
|
+
compartments: string[];
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
interface EditCollectionViewProps {
|
|
@@ -38,7 +40,10 @@ export function EditCollectionView({ refetch, collection }: EditCollectionViewPr
|
|
|
38
40
|
tags: collection.tags || [],
|
|
39
41
|
type: collection.type ? getContentTypeRefId(collection.type) : "",
|
|
40
42
|
allowed_types: collection.allowed_types || [],
|
|
43
|
+
sensitivity: collection.sensitivity,
|
|
44
|
+
compartments: collection.compartments || [],
|
|
41
45
|
});
|
|
46
|
+
const [compartmentInput, setCompartmentInput] = useState('');
|
|
42
47
|
|
|
43
48
|
const tableLayoutValue = useMemo(() => {
|
|
44
49
|
return stringifyTableLayout(collection.table_layout);
|
|
@@ -65,6 +70,8 @@ export function EditCollectionView({ refetch, collection }: EditCollectionViewPr
|
|
|
65
70
|
tags: metadata.tags,
|
|
66
71
|
type: metadata.type,
|
|
67
72
|
allowed_types: metadata.allowed_types,
|
|
73
|
+
sensitivity: metadata.sensitivity,
|
|
74
|
+
compartments: metadata.compartments,
|
|
68
75
|
};
|
|
69
76
|
let error: string | undefined;
|
|
70
77
|
if (!payload.name) {
|
|
@@ -221,6 +228,50 @@ export function EditCollectionView({ refetch, collection }: EditCollectionViewPr
|
|
|
221
228
|
isClearable
|
|
222
229
|
/>
|
|
223
230
|
</FormItem>
|
|
231
|
+
<FormItem label="Sensitivity" description="BLP sensitivity level — propagated to member documents (max across collections)">
|
|
232
|
+
<SelectBox
|
|
233
|
+
options={SecurityLevelLabels.map((label, index) => ({ label: `${index} — ${label}`, value: index }))}
|
|
234
|
+
value={metadata.sensitivity !== undefined ? { label: `${metadata.sensitivity} — ${SecurityLevelLabels[metadata.sensitivity] ?? 'Unknown'}`, value: metadata.sensitivity } : undefined}
|
|
235
|
+
onChange={(opt: { label: string; value: number }) => setField('sensitivity', opt.value)}
|
|
236
|
+
optionLabel={(opt: { label: string }) => opt.label}
|
|
237
|
+
by="value"
|
|
238
|
+
placeholder="Not set"
|
|
239
|
+
/>
|
|
240
|
+
</FormItem>
|
|
241
|
+
<FormItem label="Compartments" description="Security compartments — propagated to member documents (union across collections)">
|
|
242
|
+
<div className="flex gap-2">
|
|
243
|
+
<Input
|
|
244
|
+
value={compartmentInput}
|
|
245
|
+
onChange={setCompartmentInput}
|
|
246
|
+
placeholder="Add a compartment"
|
|
247
|
+
onKeyDown={(e) => {
|
|
248
|
+
if (e.key === 'Enter') {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
const val = compartmentInput.trim();
|
|
251
|
+
if (val && !metadata.compartments.includes(val)) {
|
|
252
|
+
setField('compartments', [...metadata.compartments, val]);
|
|
253
|
+
setCompartmentInput('');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}}
|
|
257
|
+
/>
|
|
258
|
+
<Button type="button" variant="outline" onClick={() => {
|
|
259
|
+
const val = compartmentInput.trim();
|
|
260
|
+
if (val && !metadata.compartments.includes(val)) {
|
|
261
|
+
setField('compartments', [...metadata.compartments, val]);
|
|
262
|
+
setCompartmentInput('');
|
|
263
|
+
}
|
|
264
|
+
}}>Add</Button>
|
|
265
|
+
</div>
|
|
266
|
+
<div className="flex gap-1 flex-wrap mt-2">
|
|
267
|
+
{metadata.compartments.map((c) => (
|
|
268
|
+
<Badge key={c} variant="secondary" className="cursor-pointer"
|
|
269
|
+
onClick={() => setField('compartments', metadata.compartments.filter(x => x !== c))}>
|
|
270
|
+
{c} ×
|
|
271
|
+
</Badge>
|
|
272
|
+
))}
|
|
273
|
+
</div>
|
|
274
|
+
</FormItem>
|
|
224
275
|
</Panel>
|
|
225
276
|
|
|
226
277
|
{typeId && <PropertiesEditor typeId={typeId} collection={collection} />}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { memo, useEffect, useRef, useState, type RefObject } from "react";
|
|
2
2
|
|
|
3
3
|
import { AUDIO_RENDITION_NAME, AudioMetadata, ContentNature, ContentObject, ContentObjectStatus, DocAnalyzerProgress, DocProcessorOutputFormat, DocumentMetadata, ImageRenditionFormat, MarkdownRenditionFormat, PDF_RENDITION_NAME, Permission, POSTER_RENDITION_NAME, VideoMetadata, WorkflowExecutionStatus } from "@vertesia/common";
|
|
4
|
-
import { Button, Portal, ResizableHandle, ResizablePanel, ResizablePanelGroup, Spinner, useToast } from "@vertesia/ui/core";
|
|
4
|
+
import { Button, Dropdown, MenuItem, Portal, ResizableHandle, ResizablePanel, ResizablePanelGroup, Spinner, useFetch, useToast } from "@vertesia/ui/core";
|
|
5
5
|
import { NavLink } from "@vertesia/ui/router";
|
|
6
6
|
import { useUserSession } from "@vertesia/ui/session";
|
|
7
7
|
import { JSONDisplay, MarkdownRenderer, Progress, XMLViewer } from "@vertesia/ui/widgets";
|
|
@@ -541,14 +541,14 @@ function DataPanel({ object, loadText, handleCopyContent, refetch }: { object: C
|
|
|
541
541
|
<PdfProcessingPanel progress={pdfProgress} status={pdfStatus} outputFormat={pdfOutputFormat} />
|
|
542
542
|
</div>
|
|
543
543
|
)}
|
|
544
|
-
{currentPanel === PanelView.Text && !showProcessingPanel && isLoadingText && (
|
|
544
|
+
{currentPanel === PanelView.Text && !showProcessingPanel && !isEditing && isLoadingText && (
|
|
545
545
|
<div className={getPanelVisibility(true)}>
|
|
546
546
|
<div className="flex justify-center items-center flex-1">
|
|
547
547
|
<Spinner size="lg" />
|
|
548
548
|
</div>
|
|
549
549
|
</div>
|
|
550
550
|
)}
|
|
551
|
-
{currentPanel === PanelView.Text && !showProcessingPanel && !isLoadingText && (
|
|
551
|
+
{currentPanel === PanelView.Text && !showProcessingPanel && !isEditing && !isLoadingText && (
|
|
552
552
|
<div className={getPanelVisibility(true)}>
|
|
553
553
|
<TextPanel
|
|
554
554
|
object={object}
|
|
@@ -582,11 +582,16 @@ function TextActions({
|
|
|
582
582
|
onToggleEdit,
|
|
583
583
|
canEdit,
|
|
584
584
|
}: TextActionsProps) {
|
|
585
|
-
const { client } = useUserSession();
|
|
585
|
+
const { client, project } = useUserSession();
|
|
586
586
|
const toast = useToast();
|
|
587
587
|
const { t } = useUITranslation();
|
|
588
588
|
const content = object.content;
|
|
589
589
|
const { renderDocument, isDownloading } = useDownloadFile({ client, toast });
|
|
590
|
+
const { data: fullProject } = useFetch(
|
|
591
|
+
() => project ? client.projects.retrieve(project.id) : Promise.resolve(undefined),
|
|
592
|
+
[project?.id]
|
|
593
|
+
);
|
|
594
|
+
const pdfTemplateObjectId = fullProject?.configuration?.pdf_template_object_id;
|
|
590
595
|
|
|
591
596
|
const isMarkdown =
|
|
592
597
|
content &&
|
|
@@ -596,7 +601,7 @@ function TextActions({
|
|
|
596
601
|
// Get content processor type for file extension detection
|
|
597
602
|
const contentProcessorType = getContentProcessorType(object);
|
|
598
603
|
|
|
599
|
-
const handleExportDocument = async (format: MarkdownRenditionFormat) => {
|
|
604
|
+
const handleExportDocument = async (format: MarkdownRenditionFormat, useDefaultTemplate?: boolean) => {
|
|
600
605
|
// Prevent multiple concurrent exports
|
|
601
606
|
if (isDownloading) return;
|
|
602
607
|
|
|
@@ -608,14 +613,20 @@ function TextActions({
|
|
|
608
613
|
duration: 2000,
|
|
609
614
|
});
|
|
610
615
|
|
|
616
|
+
// For branded exports, use the project-configured template if available
|
|
617
|
+
const templateObjectId = useDefaultTemplate !== false ? pdfTemplateObjectId : undefined;
|
|
618
|
+
|
|
611
619
|
await renderDocument(object.id, {
|
|
612
620
|
format,
|
|
613
621
|
title: object.name || "document",
|
|
622
|
+
useDefaultTemplate,
|
|
623
|
+
templateObjectId,
|
|
614
624
|
});
|
|
615
625
|
};
|
|
616
626
|
|
|
617
627
|
const handleExportDocx = () => handleExportDocument(MarkdownRenditionFormat.docx);
|
|
618
|
-
const handleExportPdf = () => handleExportDocument(MarkdownRenditionFormat.pdf);
|
|
628
|
+
const handleExportPdf = () => handleExportDocument(MarkdownRenditionFormat.pdf, false);
|
|
629
|
+
const handleExportBrandedPdf = () => handleExportDocument(MarkdownRenditionFormat.pdf);
|
|
619
630
|
|
|
620
631
|
const handleDownloadText = (e: React.MouseEvent) => {
|
|
621
632
|
e.preventDefault();
|
|
@@ -668,43 +679,50 @@ function TextActions({
|
|
|
668
679
|
<SquarePen className="size-4" />
|
|
669
680
|
</SecureButton>
|
|
670
681
|
)}
|
|
671
|
-
<Button variant="ghost" size="sm" title="Download text" onClick={handleDownloadText}>
|
|
672
|
-
<Download className="size-4" />
|
|
673
|
-
</Button>
|
|
674
682
|
</>
|
|
675
683
|
)}
|
|
676
|
-
{
|
|
677
|
-
|
|
678
|
-
<
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
className="
|
|
684
|
-
>
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
684
|
+
{isDownloading ? (
|
|
685
|
+
<Button variant="ghost" size="sm" disabled className="flex items-center gap-2" alt="download">
|
|
686
|
+
<Spinner size="sm" />
|
|
687
|
+
</Button>
|
|
688
|
+
) : (
|
|
689
|
+
<Dropdown trigger={
|
|
690
|
+
<Button variant="ghost" size="sm" disabled={!text} className="flex items-center gap-2" alt="download">
|
|
691
|
+
<Download className="size-4" />
|
|
692
|
+
</Button>}>
|
|
693
|
+
{fullText && (
|
|
694
|
+
<MenuItem onClick={handleDownloadText}>
|
|
695
|
+
<div className="flex items-center gap-2">
|
|
696
|
+
<Download className="size-4" />
|
|
697
|
+
Download Text
|
|
698
|
+
</div>
|
|
699
|
+
</MenuItem>
|
|
700
|
+
)}
|
|
701
|
+
{isMarkdown && text && (
|
|
702
|
+
<>
|
|
703
|
+
<MenuItem onClick={handleExportDocx}>
|
|
704
|
+
<div className="flex items-center gap-2">
|
|
705
|
+
<Download className="size-4" />
|
|
706
|
+
Export as DOCX
|
|
707
|
+
</div>
|
|
708
|
+
</MenuItem>
|
|
709
|
+
<MenuItem onClick={handleExportPdf}>
|
|
710
|
+
<div className="flex items-center gap-2">
|
|
711
|
+
<Download className="size-4" />
|
|
712
|
+
Export as PDF
|
|
713
|
+
</div>
|
|
714
|
+
</MenuItem>
|
|
715
|
+
<MenuItem onClick={handleExportBrandedPdf}>
|
|
716
|
+
<div className="flex items-center gap-2">
|
|
717
|
+
<Download className="size-4" />
|
|
718
|
+
Export as Branded PDF
|
|
719
|
+
</div>
|
|
720
|
+
</MenuItem>
|
|
721
|
+
</>
|
|
722
|
+
)}
|
|
723
|
+
</Dropdown>
|
|
707
724
|
)}
|
|
725
|
+
|
|
708
726
|
</div>
|
|
709
727
|
</div>
|
|
710
728
|
</>
|
|
@@ -40,6 +40,8 @@ export function TextEditorPanel({ object, text, onClose, onSaved }: TextEditorPa
|
|
|
40
40
|
const [showConfirmation, setShowConfirmation] = useState(false);
|
|
41
41
|
|
|
42
42
|
const language = getMonacoLanguage(object.content?.type);
|
|
43
|
+
console.log('Determined language for Monaco Editor:', language);
|
|
44
|
+
console.log('TextEditorPanel rendered with object:', object, text);
|
|
43
45
|
|
|
44
46
|
const handleEditorChange = useCallback(() => {
|
|
45
47
|
if (!isDirty) setIsDirty(true);
|
|
@@ -112,7 +114,7 @@ export function TextEditorPanel({ object, text, onClose, onSaved }: TextEditorPa
|
|
|
112
114
|
<Button variant="ghost" size="sm" onClick={onClose} disabled={isSaving}>
|
|
113
115
|
{t('store.cancelEdit')}
|
|
114
116
|
</Button>
|
|
115
|
-
<Button variant="
|
|
117
|
+
<Button variant="secondary" size="sm" onClick={handleSave} disabled={!isDirty} isLoading={isSaving}>
|
|
116
118
|
{t('store.saveText')}
|
|
117
119
|
</Button>
|
|
118
120
|
</div>
|
|
@@ -18,6 +18,10 @@ export interface RenderAndDownloadOptions {
|
|
|
18
18
|
artifactRunId?: string;
|
|
19
19
|
/** Additional Pandoc options */
|
|
20
20
|
pandocOptions?: string[];
|
|
21
|
+
/** Use Vertesia default branded template (default: true for PDF) */
|
|
22
|
+
useDefaultTemplate?: boolean;
|
|
23
|
+
/** Object ID of a content object containing a custom LaTeX template to use instead of the default */
|
|
24
|
+
templateObjectId?: string;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export interface UseDownloadFileResult {
|
|
@@ -118,6 +122,8 @@ export function useDownloadFile({ client, toast }: UseDownloadFileOptions): UseD
|
|
|
118
122
|
format: options.format,
|
|
119
123
|
title: options.title,
|
|
120
124
|
pandoc_options: options.pandocOptions,
|
|
125
|
+
use_default_template: options.useDefaultTemplate,
|
|
126
|
+
template_path: options.templateObjectId ? `store:${options.templateObjectId}` : undefined,
|
|
121
127
|
}, filename);
|
|
122
128
|
|
|
123
129
|
toast({
|
package/src/i18n/locales/en.json
CHANGED
|
@@ -339,6 +339,9 @@
|
|
|
339
339
|
"store.actions.noObjectsSelected": "No objects selected",
|
|
340
340
|
"store.actions.pleaseSelectObjectsToDelete": "Please select objects to delete",
|
|
341
341
|
"store.actions.pleaseSelectObjectsToRemove": "Please select objects to remove from collection",
|
|
342
|
+
"store.actions.removeFromCollection": "Remove from Collection",
|
|
343
|
+
"store.actions.removeFromCollectionDesc": "Remove the selected objects from this collection",
|
|
344
|
+
"store.actions.confirmRemoveFromCollection": "Are you sure you want to remove the selected objects from this collection?",
|
|
342
345
|
"store.actions.selectCollection": "Select Collection",
|
|
343
346
|
"store.actions.startWorkflow": "Start Workflow",
|
|
344
347
|
"store.actions.startWorkflowByRule": "Start a Workflow by Rule",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useTheme } from '@vertesia/ui/core';
|
|
3
|
+
import { MonacoEditor, IEditorApi } from '../monacoEditor/MonacoEditor';
|
|
4
|
+
|
|
5
|
+
export interface JSONEditorProps {
|
|
6
|
+
/** The JSON value to edit */
|
|
7
|
+
value: Record<string, any> | undefined | null;
|
|
8
|
+
/** Called when the user saves (value is the parsed JSON) */
|
|
9
|
+
onChange?: (value: Record<string, any>) => void;
|
|
10
|
+
/** Called on every valid edit (for controlled mode) */
|
|
11
|
+
onValidChange?: (value: Record<string, any>) => void;
|
|
12
|
+
/** If true, the editor is read-only */
|
|
13
|
+
readonly?: boolean;
|
|
14
|
+
/** Editor height (default: '200px') */
|
|
15
|
+
height?: string;
|
|
16
|
+
/** Placeholder text when value is empty */
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
/** Additional CSS class */
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Reusable JSON editor based on Monaco.
|
|
24
|
+
* Parses and validates JSON, reports errors.
|
|
25
|
+
*/
|
|
26
|
+
export function JSONEditor({
|
|
27
|
+
value,
|
|
28
|
+
onChange: _onChange,
|
|
29
|
+
onValidChange,
|
|
30
|
+
readonly = false,
|
|
31
|
+
height = '200px',
|
|
32
|
+
placeholder,
|
|
33
|
+
className,
|
|
34
|
+
}: JSONEditorProps) {
|
|
35
|
+
const { theme } = useTheme();
|
|
36
|
+
const editorRef = useRef<IEditorApi>(undefined);
|
|
37
|
+
const [error, setError] = useState<string | null>(null);
|
|
38
|
+
|
|
39
|
+
const jsonString = useMemo(() => {
|
|
40
|
+
if (value === undefined || value === null) return placeholder ?? '{}';
|
|
41
|
+
try {
|
|
42
|
+
return JSON.stringify(value, null, 2);
|
|
43
|
+
} catch {
|
|
44
|
+
return '{}';
|
|
45
|
+
}
|
|
46
|
+
}, [value, placeholder]);
|
|
47
|
+
|
|
48
|
+
// Validate on change
|
|
49
|
+
const handleChange = useCallback(() => {
|
|
50
|
+
if (!editorRef.current || readonly) return;
|
|
51
|
+
const text = editorRef.current.getValue();
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(text);
|
|
54
|
+
setError(null);
|
|
55
|
+
onValidChange?.(parsed);
|
|
56
|
+
} catch {
|
|
57
|
+
setError('Invalid JSON');
|
|
58
|
+
}
|
|
59
|
+
}, [readonly, onValidChange]);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={className}>
|
|
63
|
+
<div className="border rounded overflow-hidden" style={{ height }}>
|
|
64
|
+
<MonacoEditor
|
|
65
|
+
defaultValue={jsonString}
|
|
66
|
+
editorRef={editorRef}
|
|
67
|
+
language="json"
|
|
68
|
+
theme={theme === 'dark' ? 'vs-dark' : 'vs'}
|
|
69
|
+
onChange={handleChange}
|
|
70
|
+
options={readonly ? { readOnly: true, domReadOnly: true } : undefined}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
{error && <p className="text-xs text-destructive mt-1">{error}</p>}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get the current parsed value from a JSONEditor ref.
|
|
80
|
+
* Returns the parsed object or null if invalid.
|
|
81
|
+
*/
|
|
82
|
+
export function getJSONEditorValue(editorRef: React.RefObject<IEditorApi | undefined>): Record<string, any> | null {
|
|
83
|
+
if (!editorRef.current) return null;
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(editorRef.current.getValue());
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|