@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.
Files changed (48) hide show
  1. package/lib/esm/features/store/collections/BrowseCollectionView.js +1 -1
  2. package/lib/esm/features/store/collections/BrowseCollectionView.js.map +1 -1
  3. package/lib/esm/features/store/collections/EditCollectionView.js +23 -3
  4. package/lib/esm/features/store/collections/EditCollectionView.js.map +1 -1
  5. package/lib/esm/features/store/objects/components/ContentOverview.js +13 -6
  6. package/lib/esm/features/store/objects/components/ContentOverview.js.map +1 -1
  7. package/lib/esm/features/store/objects/components/TextEditorPanel.js +3 -1
  8. package/lib/esm/features/store/objects/components/TextEditorPanel.js.map +1 -1
  9. package/lib/esm/features/store/objects/components/useDownloadFile.js +2 -0
  10. package/lib/esm/features/store/objects/components/useDownloadFile.js.map +1 -1
  11. package/lib/esm/i18n/locales/en.json +3 -0
  12. package/lib/esm/widgets/json-view/JSONEditor.js +53 -0
  13. package/lib/esm/widgets/json-view/JSONEditor.js.map +1 -0
  14. package/lib/esm/widgets/json-view/index.js +1 -0
  15. package/lib/esm/widgets/json-view/index.js.map +1 -1
  16. package/lib/tsconfig.tsbuildinfo +1 -1
  17. package/lib/types/features/store/collections/BrowseCollectionView.d.ts.map +1 -1
  18. package/lib/types/features/store/collections/EditCollectionView.d.ts.map +1 -1
  19. package/lib/types/features/store/objects/components/TextEditorPanel.d.ts.map +1 -1
  20. package/lib/types/features/store/objects/components/useDownloadFile.d.ts +4 -0
  21. package/lib/types/features/store/objects/components/useDownloadFile.d.ts.map +1 -1
  22. package/lib/types/widgets/json-view/JSONEditor.d.ts +28 -0
  23. package/lib/types/widgets/json-view/JSONEditor.d.ts.map +1 -0
  24. package/lib/types/widgets/json-view/index.d.ts +1 -0
  25. package/lib/types/widgets/json-view/index.d.ts.map +1 -1
  26. package/lib/vertesia-ui-core.js +1 -1
  27. package/lib/vertesia-ui-core.js.map +1 -1
  28. package/lib/vertesia-ui-features.js +1 -1
  29. package/lib/vertesia-ui-features.js.map +1 -1
  30. package/lib/vertesia-ui-i18n.js +1 -1
  31. package/lib/vertesia-ui-i18n.js.map +1 -1
  32. package/lib/vertesia-ui-layout.js +1 -1
  33. package/lib/vertesia-ui-layout.js.map +1 -1
  34. package/lib/vertesia-ui-session.js +1 -1
  35. package/lib/vertesia-ui-session.js.map +1 -1
  36. package/lib/vertesia-ui-shell.js +1 -1
  37. package/lib/vertesia-ui-shell.js.map +1 -1
  38. package/lib/vertesia-ui-widgets.js +1 -1
  39. package/lib/vertesia-ui-widgets.js.map +1 -1
  40. package/package.json +5 -5
  41. package/src/features/store/collections/BrowseCollectionView.tsx +8 -4
  42. package/src/features/store/collections/EditCollectionView.tsx +53 -2
  43. package/src/features/store/objects/components/ContentOverview.tsx +58 -40
  44. package/src/features/store/objects/components/TextEditorPanel.tsx +3 -1
  45. package/src/features/store/objects/components/useDownloadFile.ts +6 -0
  46. package/src/i18n/locales/en.json +3 -0
  47. package/src/widgets/json-view/JSONEditor.tsx +89 -0
  48. 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.1.1-dev.20260505.163000Z",
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.1.1-dev.20260505.163000Z",
90
- "@vertesia/common": "1.1.1-dev.20260505.163000Z",
91
- "@vertesia/fusion-ux": "1.1.1-dev.20260505.163000Z",
92
- "@vertesia/json": "1.1.1-dev.20260505.163000Z"
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 collection.dynamic ? (
40
- <DocumentSearchResults layout={tableLayout} />
41
- ) : (
42
- <DocumentSearchResultsWithDropZone onUploadDone={onUploadDone} layout={tableLayout} />
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
- {isMarkdown && text && (
677
- <>
678
- <Button
679
- variant="ghost"
680
- size="sm"
681
- onClick={handleExportDocx}
682
- disabled={isDownloading}
683
- className="flex items-center gap-2"
684
- >
685
- {isDownloading ? (
686
- <Spinner size="sm" />
687
- ) : (
688
- <Download className="size-4" />
689
- )}
690
- DOCX
691
- </Button>
692
- <Button
693
- variant="ghost"
694
- size="sm"
695
- onClick={handleExportPdf}
696
- disabled={isDownloading}
697
- className="flex items-center gap-2"
698
- >
699
- {isDownloading ? (
700
- <Spinner size="sm" />
701
- ) : (
702
- <Download className="size-4" />
703
- )}
704
- PDF
705
- </Button>
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="outline" size="sm" onClick={handleSave} disabled={!isDirty} isLoading={isSaving}>
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({
@@ -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
+ }
@@ -1,5 +1,6 @@
1
1
  export type * from "./types.js";
2
2
  export * from "./JSONCode.js";
3
3
  export * from "./JSONDisplay.js";
4
+ export * from "./JSONEditor.js";
4
5
  export * from "./JSONView.js";
5
6
  export * from "./JSONSwitcher.js";