@xpert-ai/plugin-lucidchart 0.2.0 → 0.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/dist/lib/lucidchart-view.provider.d.ts.map +1 -1
- package/dist/lib/lucidchart-view.provider.js +15 -0
- package/dist/lib/lucidchart-view.provider.js.map +1 -1
- package/dist/lib/lucidchart.service.d.ts +33 -1
- package/dist/lib/lucidchart.service.d.ts.map +1 -1
- package/dist/lib/lucidchart.service.js +29 -0
- package/dist/lib/lucidchart.service.js.map +1 -1
- package/dist/lib/remote-components/lucidchart-workbench/app.js +230 -41
- package/dist/lib/remote-components/lucidchart-workbench/src/i18n.d.ts +1 -1
- package/dist/lib/remote-components/lucidchart-workbench/src/i18n.d.ts.map +1 -1
- package/dist/lib/remote-components/lucidchart-workbench/src/i18n.js +60 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/i18n.js.map +1 -1
- package/dist/lib/remote-components/lucidchart-workbench/src/i18n.ts +90 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/main.tsx +436 -137
- package/dist/lib/remote-components/lucidchart-workbench/src/styles.d.ts.map +1 -1
- package/dist/lib/remote-components/lucidchart-workbench/src/styles.js +213 -24
- package/dist/lib/remote-components/lucidchart-workbench/src/styles.js.map +1 -1
- package/dist/lib/remote-components/lucidchart-workbench/src/styles.ts +213 -24
- package/dist/lib/types.d.ts +8 -1
- package/dist/lib/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -3,8 +3,14 @@ import {
|
|
|
3
3
|
Badge,
|
|
4
4
|
Button,
|
|
5
5
|
Check,
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
6
11
|
Download,
|
|
7
12
|
FileJson,
|
|
13
|
+
Image,
|
|
8
14
|
Input,
|
|
9
15
|
PanelLeftClose,
|
|
10
16
|
PanelLeftOpen,
|
|
@@ -29,6 +35,10 @@ import {
|
|
|
29
35
|
SidebarRail,
|
|
30
36
|
SidebarTitle,
|
|
31
37
|
SidebarTrigger,
|
|
38
|
+
Tabs,
|
|
39
|
+
TabsContent,
|
|
40
|
+
TabsList,
|
|
41
|
+
TabsTrigger,
|
|
32
42
|
Textarea,
|
|
33
43
|
Upload,
|
|
34
44
|
installShadcnThemeVars
|
|
@@ -52,6 +62,7 @@ import {
|
|
|
52
62
|
} from './runtime'
|
|
53
63
|
|
|
54
64
|
type StatusFilter = '' | 'draft' | 'reviewed' | 'archived'
|
|
65
|
+
type DocumentKind = 'diagram' | 'flowchart' | 'architecture' | 'process' | 'wireframe' | 'orgchart' | 'network' | 'other'
|
|
55
66
|
type DocumentRecord = Record<string, any>
|
|
56
67
|
type DocumentVersion = Record<string, any>
|
|
57
68
|
type DetailPayload = {
|
|
@@ -88,6 +99,10 @@ type StandardImportPreviewModel = {
|
|
|
88
99
|
lines: PreviewLine[]
|
|
89
100
|
viewBox: string
|
|
90
101
|
}
|
|
102
|
+
type JsonParseResult = {
|
|
103
|
+
value: Record<string, unknown> | null
|
|
104
|
+
error: string | null
|
|
105
|
+
}
|
|
91
106
|
|
|
92
107
|
const DEFAULT_MERMAID = `flowchart TD
|
|
93
108
|
A[User Request] --> B[Agent Plans Lucidchart Draft]
|
|
@@ -118,6 +133,8 @@ const LUCIDCHART_MUTATION_TOOL_NAMES = new Set([
|
|
|
118
133
|
'lucidchart_report_failure'
|
|
119
134
|
])
|
|
120
135
|
|
|
136
|
+
const DOCUMENT_KINDS: DocumentKind[] = ['diagram', 'flowchart', 'architecture', 'process', 'wireframe', 'orgchart', 'network', 'other']
|
|
137
|
+
|
|
121
138
|
installShadcnThemeVars({ styleId: 'lucidchart-workbench-shadcn-ui-vars' })
|
|
122
139
|
injectStyles()
|
|
123
140
|
|
|
@@ -130,8 +147,14 @@ function App() {
|
|
|
130
147
|
const [status, setStatus] = React.useState<StatusFilter>('')
|
|
131
148
|
const [busy, setBusy] = React.useState(false)
|
|
132
149
|
const [dirty, setDirty] = React.useState(false)
|
|
150
|
+
const [newDialogOpen, setNewDialogOpen] = React.useState(false)
|
|
133
151
|
const [newTitle, setNewTitle] = React.useState('')
|
|
134
152
|
const [newDescription, setNewDescription] = React.useState('')
|
|
153
|
+
const [newKind, setNewKind] = React.useState<DocumentKind>('diagram')
|
|
154
|
+
const [metadataTitle, setMetadataTitle] = React.useState('')
|
|
155
|
+
const [metadataDescription, setMetadataDescription] = React.useState('')
|
|
156
|
+
const [metadataKind, setMetadataKind] = React.useState<DocumentKind>('diagram')
|
|
157
|
+
const [metadataDirty, setMetadataDirty] = React.useState(false)
|
|
135
158
|
const [changeSummary, setChangeSummary] = React.useState('')
|
|
136
159
|
const [assistantPrompt, setAssistantPrompt] = React.useState('')
|
|
137
160
|
const [standardImportText, setStandardImportText] = React.useState(() => stringifyJson(createDefaultStandardImport('Untitled')))
|
|
@@ -140,8 +163,10 @@ function App() {
|
|
|
140
163
|
const [lucidDocumentUrl, setLucidDocumentUrl] = React.useState('')
|
|
141
164
|
const [embedUrl, setEmbedUrl] = React.useState('')
|
|
142
165
|
const [previewUrl, setPreviewUrl] = React.useState('')
|
|
143
|
-
const [
|
|
144
|
-
const [
|
|
166
|
+
const [mainTab, setMainTab] = React.useState('preview')
|
|
167
|
+
const [inspectorTab, setInspectorTab] = React.useState('info')
|
|
168
|
+
const [leftPanelCollapsed, setLeftPanelCollapsed] = React.useState(() => isCompactViewport())
|
|
169
|
+
const [rightPanelCollapsed, setRightPanelCollapsed] = React.useState(() => isCompactViewport())
|
|
145
170
|
const fileInputRef = React.useRef<HTMLInputElement | null>(null)
|
|
146
171
|
const contextRef = React.useRef<any>(null)
|
|
147
172
|
const selectedIdRef = React.useRef('')
|
|
@@ -186,7 +211,7 @@ function App() {
|
|
|
186
211
|
post('ready')
|
|
187
212
|
}, [])
|
|
188
213
|
|
|
189
|
-
React.useEffect(reportResize, [documents, detail, busy, dirty, leftPanelCollapsed, rightPanelCollapsed])
|
|
214
|
+
React.useEffect(reportResize, [documents, detail, busy, dirty, metadataDirty, mainTab, inspectorTab, leftPanelCollapsed, rightPanelCollapsed])
|
|
190
215
|
|
|
191
216
|
function hydratePayload(payload: any) {
|
|
192
217
|
if (!payload) {
|
|
@@ -212,6 +237,11 @@ function App() {
|
|
|
212
237
|
setChangeSummary('')
|
|
213
238
|
const version = payload.currentVersion || null
|
|
214
239
|
const title = payload.item?.title || t('untitled')
|
|
240
|
+
const nextKind = normalizeDocumentKind(payload.item?.kind)
|
|
241
|
+
setMetadataTitle(title)
|
|
242
|
+
setMetadataDescription(typeof payload.item?.description === 'string' ? payload.item.description : '')
|
|
243
|
+
setMetadataKind(nextKind)
|
|
244
|
+
setMetadataDirty(false)
|
|
215
245
|
const standardImport = isObject(version?.standardImport) ? version?.standardImport : createDefaultStandardImport(title)
|
|
216
246
|
const nextText = stringifyJson(standardImport)
|
|
217
247
|
setStandardImportText(nextText)
|
|
@@ -308,19 +338,40 @@ function App() {
|
|
|
308
338
|
}
|
|
309
339
|
}
|
|
310
340
|
|
|
341
|
+
function hasUnsavedChanges() {
|
|
342
|
+
return dirty || metadataDirty
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function confirmDiscardUnsavedChanges() {
|
|
346
|
+
return !hasUnsavedChanges() || window.confirm(t('discardUnsavedChanges'))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function selectDocumentWithGuard(documentId: string) {
|
|
350
|
+
if (documentId === selectedIdRef.current) {
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
if (!confirmDiscardUnsavedChanges()) {
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
await selectDocument(documentId)
|
|
357
|
+
}
|
|
358
|
+
|
|
311
359
|
async function createDocument() {
|
|
312
360
|
const title = newTitle.trim() || t('untitled')
|
|
313
361
|
setBusy(true)
|
|
314
362
|
try {
|
|
315
363
|
const response = await executeAction('create_document', null, {
|
|
316
364
|
title,
|
|
317
|
-
description: newDescription
|
|
365
|
+
description: newDescription,
|
|
366
|
+
kind: newKind
|
|
318
367
|
})
|
|
319
368
|
const result = getResponsePayload(response)
|
|
320
369
|
notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('documentCreated'))
|
|
321
370
|
const documentId = result?.item?.id || result?.data?.item?.id
|
|
322
371
|
setNewTitle('')
|
|
323
372
|
setNewDescription('')
|
|
373
|
+
setNewKind('diagram')
|
|
374
|
+
setNewDialogOpen(false)
|
|
324
375
|
setChangeSummary('')
|
|
325
376
|
if (documentId) {
|
|
326
377
|
await reloadList()
|
|
@@ -335,26 +386,53 @@ function App() {
|
|
|
335
386
|
}
|
|
336
387
|
}
|
|
337
388
|
|
|
338
|
-
async function
|
|
389
|
+
async function updateDocumentMetadata() {
|
|
339
390
|
if (!selectedId) {
|
|
340
391
|
notify('warning', t('noDocument'))
|
|
341
392
|
return
|
|
342
393
|
}
|
|
343
|
-
|
|
394
|
+
const title = metadataTitle.trim()
|
|
395
|
+
if (!title) {
|
|
396
|
+
notify('warning', t('titleRequired'))
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
setBusy(true)
|
|
344
400
|
try {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
401
|
+
const response = await executeAction('update_document_metadata', selectedId, {
|
|
402
|
+
documentId: selectedId,
|
|
403
|
+
title,
|
|
404
|
+
description: metadataDescription,
|
|
405
|
+
kind: metadataKind,
|
|
406
|
+
changeSummary: changeSummary.trim() || undefined
|
|
407
|
+
})
|
|
408
|
+
const result = getResponsePayload(response)
|
|
409
|
+
notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('metadataSaved'))
|
|
410
|
+
setMetadataDirty(false)
|
|
411
|
+
setChangeSummary('')
|
|
412
|
+
await selectDocument(selectedId)
|
|
413
|
+
await reloadList()
|
|
349
414
|
} catch (error) {
|
|
350
|
-
notify('error',
|
|
415
|
+
notify('error', getErrorMessage(error))
|
|
416
|
+
} finally {
|
|
417
|
+
setBusy(false)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function saveStandardImport() {
|
|
422
|
+
if (!selectedId) {
|
|
423
|
+
notify('warning', t('noDocument'))
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
const parsed = parseStandardImportDocument(standardImportText)
|
|
427
|
+
if (parsed.error || !parsed.value) {
|
|
428
|
+
notify('error', `${t('invalidJson')}: ${parsed.error || t('unknownError')}`)
|
|
351
429
|
return
|
|
352
430
|
}
|
|
353
431
|
setBusy(true)
|
|
354
432
|
try {
|
|
355
433
|
const response = await executeAction('save_standard_import_version', selectedId, {
|
|
356
434
|
documentId: selectedId,
|
|
357
|
-
standardImport,
|
|
435
|
+
standardImport: parsed.value,
|
|
358
436
|
mermaidSource: mermaidSource.trim() || undefined,
|
|
359
437
|
lucidDocumentId: lucidDocumentId.trim() || undefined,
|
|
360
438
|
lucidDocumentUrl: lucidDocumentUrl.trim() || undefined,
|
|
@@ -385,8 +463,9 @@ function App() {
|
|
|
385
463
|
try {
|
|
386
464
|
const response = await executeAction('save_mermaid_draft', selectedId || null, {
|
|
387
465
|
documentId: selectedId || undefined,
|
|
388
|
-
title:
|
|
389
|
-
description:
|
|
466
|
+
title: metadataTitle.trim() || detail?.item?.title || t('untitled'),
|
|
467
|
+
description: metadataDescription,
|
|
468
|
+
kind: metadataKind,
|
|
390
469
|
mermaidSource: source,
|
|
391
470
|
changeSummary: changeSummary.trim() || undefined
|
|
392
471
|
})
|
|
@@ -406,7 +485,7 @@ function App() {
|
|
|
406
485
|
}
|
|
407
486
|
|
|
408
487
|
async function registerExternalDocument() {
|
|
409
|
-
if (!selectedId && !
|
|
488
|
+
if (!selectedId && !metadataTitle.trim()) {
|
|
410
489
|
notify('warning', t('noDocument'))
|
|
411
490
|
return
|
|
412
491
|
}
|
|
@@ -414,8 +493,9 @@ function App() {
|
|
|
414
493
|
try {
|
|
415
494
|
const response = await executeAction('register_external_document', selectedId || null, {
|
|
416
495
|
documentId: selectedId || undefined,
|
|
417
|
-
title:
|
|
418
|
-
description:
|
|
496
|
+
title: metadataTitle.trim() || detail?.item?.title || t('untitled'),
|
|
497
|
+
description: metadataDescription,
|
|
498
|
+
kind: metadataKind,
|
|
419
499
|
lucidDocumentId: lucidDocumentId.trim() || undefined,
|
|
420
500
|
lucidDocumentUrl: lucidDocumentUrl.trim() || undefined,
|
|
421
501
|
embedUrl: embedUrl.trim() || undefined,
|
|
@@ -442,6 +522,9 @@ function App() {
|
|
|
442
522
|
if (!selectedId || !versionId) {
|
|
443
523
|
return
|
|
444
524
|
}
|
|
525
|
+
if (!confirmDiscardUnsavedChanges()) {
|
|
526
|
+
return
|
|
527
|
+
}
|
|
445
528
|
setBusy(true)
|
|
446
529
|
try {
|
|
447
530
|
const response = await executeAction('restore_version', selectedId, {
|
|
@@ -464,6 +547,9 @@ function App() {
|
|
|
464
547
|
if (!selectedId) {
|
|
465
548
|
return
|
|
466
549
|
}
|
|
550
|
+
if (!confirmDiscardUnsavedChanges() || !window.confirm(t('confirmArchive'))) {
|
|
551
|
+
return
|
|
552
|
+
}
|
|
467
553
|
setBusy(true)
|
|
468
554
|
try {
|
|
469
555
|
await executeAction('archive_document', selectedId, { documentId: selectedId })
|
|
@@ -534,6 +620,12 @@ function App() {
|
|
|
534
620
|
if (!file) {
|
|
535
621
|
return
|
|
536
622
|
}
|
|
623
|
+
if (hasUnsavedChanges() && !window.confirm(t('discardUnsavedChanges'))) {
|
|
624
|
+
if (fileInputRef.current) {
|
|
625
|
+
fileInputRef.current.value = ''
|
|
626
|
+
}
|
|
627
|
+
return
|
|
628
|
+
}
|
|
537
629
|
setBusy(true)
|
|
538
630
|
try {
|
|
539
631
|
const response = await executeFileAction(
|
|
@@ -617,15 +709,52 @@ function App() {
|
|
|
617
709
|
}
|
|
618
710
|
|
|
619
711
|
function exportJson() {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
`${detail?.item?.title || 'document'}.json`
|
|
625
|
-
)
|
|
626
|
-
} catch (error) {
|
|
627
|
-
notify('error', `${t('invalidJson')}: ${getErrorMessage(error)}`)
|
|
712
|
+
const parsed = parseStandardImportDocument(standardImportText)
|
|
713
|
+
if (parsed.error || !parsed.value) {
|
|
714
|
+
notify('error', `${t('invalidJson')}: ${parsed.error || t('unknownError')}`)
|
|
715
|
+
return
|
|
628
716
|
}
|
|
717
|
+
downloadBlob(
|
|
718
|
+
new Blob([JSON.stringify(parsed.value, null, 2)], { type: 'application/json' }),
|
|
719
|
+
`${detail?.item?.title || 'document'}.json`
|
|
720
|
+
)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function formatStandardImportJson() {
|
|
724
|
+
const parsed = parseStandardImportDocument(standardImportText)
|
|
725
|
+
if (parsed.error || !parsed.value) {
|
|
726
|
+
notify('error', `${t('invalidJson')}: ${parsed.error || t('unknownError')}`)
|
|
727
|
+
return
|
|
728
|
+
}
|
|
729
|
+
updateStandardImportText(stringifyJson(parsed.value))
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function revertStandardImportJson() {
|
|
733
|
+
const title = detail?.item?.title || t('untitled')
|
|
734
|
+
const standardImport = isObject(currentVersion?.standardImport) ? currentVersion.standardImport : createDefaultStandardImport(title)
|
|
735
|
+
updateStandardImportText(stringifyJson(standardImport))
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function updateMetadataTitle(nextTitle: string) {
|
|
739
|
+
setMetadataTitle(nextTitle)
|
|
740
|
+
setMetadataDirty(true)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function updateMetadataDescription(nextDescription: string) {
|
|
744
|
+
setMetadataDescription(nextDescription)
|
|
745
|
+
setMetadataDirty(true)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function updateMetadataKind(nextKind: DocumentKind) {
|
|
749
|
+
setMetadataKind(nextKind)
|
|
750
|
+
setMetadataDirty(true)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function openNewDocumentDialog() {
|
|
754
|
+
if (!confirmDiscardUnsavedChanges()) {
|
|
755
|
+
return
|
|
756
|
+
}
|
|
757
|
+
setNewDialogOpen(true)
|
|
629
758
|
}
|
|
630
759
|
|
|
631
760
|
const currentVersion = detail?.currentVersion || null
|
|
@@ -633,8 +762,11 @@ function App() {
|
|
|
633
762
|
const embeddableUrl = embedUrl.trim()
|
|
634
763
|
const imagePreviewUrl = previewUrl.trim()
|
|
635
764
|
const lucidOpenUrl = embeddableUrl || lucidDocumentUrl.trim()
|
|
765
|
+
const standardImportParse = React.useMemo(() => parseStandardImportDocument(standardImportText), [standardImportText])
|
|
636
766
|
const standardImportPreview = React.useMemo(() => createStandardImportPreview(standardImportText), [standardImportText])
|
|
637
|
-
const
|
|
767
|
+
const isJsonValid = Boolean(standardImportParse.value && !standardImportParse.error)
|
|
768
|
+
const canSave = Boolean(selectedId && dirty && !busy && isJsonValid)
|
|
769
|
+
const currentTitle = detail?.item?.title || t('untitled')
|
|
638
770
|
const shellClassName = `lw-shell ${leftPanelCollapsed ? 'left-collapsed' : ''} ${rightPanelCollapsed ? 'right-collapsed' : ''}`
|
|
639
771
|
const previewBadge = embeddableUrl
|
|
640
772
|
? t('embedPreview')
|
|
@@ -646,6 +778,39 @@ function App() {
|
|
|
646
778
|
|
|
647
779
|
return (
|
|
648
780
|
<div className={shellClassName}>
|
|
781
|
+
<Dialog open={newDialogOpen} onOpenChange={setNewDialogOpen}>
|
|
782
|
+
<DialogContent className="lw-dialog">
|
|
783
|
+
<DialogHeader>
|
|
784
|
+
<DialogTitle>{t('newDocument')}</DialogTitle>
|
|
785
|
+
</DialogHeader>
|
|
786
|
+
<div className="lw-dialog-stack">
|
|
787
|
+
<Input value={newTitle} placeholder={t('title')} onChange={(event: any) => setNewTitle(event.target.value)} />
|
|
788
|
+
<Textarea value={newDescription} placeholder={t('description')} onChange={(event: any) => setNewDescription(event.target.value)} />
|
|
789
|
+
<Select value={newKind} onValueChange={(value: string) => setNewKind(normalizeDocumentKind(value))}>
|
|
790
|
+
<SelectTrigger aria-label={t('kind')}>
|
|
791
|
+
<SelectValue placeholder={t('kind')} />
|
|
792
|
+
</SelectTrigger>
|
|
793
|
+
<SelectContent>
|
|
794
|
+
{DOCUMENT_KINDS.map((kind) => (
|
|
795
|
+
<SelectItem value={kind} key={kind}>
|
|
796
|
+
{t(kind)}
|
|
797
|
+
</SelectItem>
|
|
798
|
+
))}
|
|
799
|
+
</SelectContent>
|
|
800
|
+
</Select>
|
|
801
|
+
</div>
|
|
802
|
+
<DialogFooter>
|
|
803
|
+
<Button type="button" variant="outline" disabled={busy} onClick={() => setNewDialogOpen(false)}>
|
|
804
|
+
{t('cancel')}
|
|
805
|
+
</Button>
|
|
806
|
+
<Button type="button" disabled={busy} onClick={createDocument}>
|
|
807
|
+
<Plus className="lw-button-icon" aria-hidden="true" />
|
|
808
|
+
{t('create')}
|
|
809
|
+
</Button>
|
|
810
|
+
</DialogFooter>
|
|
811
|
+
</DialogContent>
|
|
812
|
+
</Dialog>
|
|
813
|
+
|
|
649
814
|
<Sidebar className="lw-sidebar" side="left" collapsed={leftPanelCollapsed}>
|
|
650
815
|
<SidebarHeader>
|
|
651
816
|
<SidebarTrigger
|
|
@@ -703,10 +868,10 @@ function App() {
|
|
|
703
868
|
<SidebarMenu>
|
|
704
869
|
{documents.map((document) => (
|
|
705
870
|
<SidebarMenuItem key={document.id}>
|
|
706
|
-
<SidebarMenuButton type="button" active={document.id === selectedId} onClick={() =>
|
|
871
|
+
<SidebarMenuButton type="button" active={document.id === selectedId} onClick={() => selectDocumentWithGuard(document.id)}>
|
|
707
872
|
<span className="lw-item-title">{document.title || t('untitled')}</span>
|
|
708
873
|
<span className="lw-item-meta">
|
|
709
|
-
v{document.currentVersionNumber || 0} · {t((document.status || 'draft') as TranslationKey)}
|
|
874
|
+
v{document.currentVersionNumber || 0} · {t((document.status || 'draft') as TranslationKey)} · {t(normalizeDocumentKind(document.kind))}
|
|
710
875
|
</span>
|
|
711
876
|
</SidebarMenuButton>
|
|
712
877
|
</SidebarMenuItem>
|
|
@@ -720,10 +885,22 @@ function App() {
|
|
|
720
885
|
<main className="lw-main">
|
|
721
886
|
<div className="lw-toolbar">
|
|
722
887
|
<div className="lw-toolbar-title">
|
|
723
|
-
<
|
|
888
|
+
<div className="lw-title-text">{selectedId ? currentTitle : t('workbenchTitle')}</div>
|
|
889
|
+
<div className="lw-title-meta">
|
|
890
|
+
{selectedId ? (
|
|
891
|
+
<>
|
|
892
|
+
<Badge variant="secondary">{t(documentStatus as TranslationKey)}</Badge>
|
|
893
|
+
<Badge variant="secondary">v{detail?.item?.currentVersionNumber || 0}</Badge>
|
|
894
|
+
<Badge variant="secondary">{t(normalizeDocumentKind(detail?.item?.kind))}</Badge>
|
|
895
|
+
{currentVersion?.sourceType ? <Badge variant="secondary">{currentVersion.sourceType}</Badge> : null}
|
|
896
|
+
</>
|
|
897
|
+
) : (
|
|
898
|
+
<span>{t('noDocument')}</span>
|
|
899
|
+
)}
|
|
900
|
+
</div>
|
|
724
901
|
</div>
|
|
725
902
|
<div className="lw-toolbar-actions">
|
|
726
|
-
<Button type="button" variant="outline" size="sm" disabled={busy} onClick={
|
|
903
|
+
<Button type="button" variant="outline" size="sm" disabled={busy} onClick={openNewDocumentDialog}>
|
|
727
904
|
<Plus className="lw-button-icon" aria-hidden="true" />
|
|
728
905
|
{t('newDocument')}
|
|
729
906
|
</Button>
|
|
@@ -735,7 +912,7 @@ function App() {
|
|
|
735
912
|
<Upload className="lw-button-icon" aria-hidden="true" />
|
|
736
913
|
{t('import')}
|
|
737
914
|
</Button>
|
|
738
|
-
<Button type="button" variant="outline" size="sm" disabled={!selectedId} onClick={exportJson}>
|
|
915
|
+
<Button type="button" variant="outline" size="sm" disabled={!selectedId || !isJsonValid} onClick={exportJson}>
|
|
739
916
|
<Download className="lw-button-icon" aria-hidden="true" />
|
|
740
917
|
{t('exportJson')}
|
|
741
918
|
</Button>
|
|
@@ -744,7 +921,7 @@ function App() {
|
|
|
744
921
|
{t('openLucid')}
|
|
745
922
|
</Button>
|
|
746
923
|
<Badge className="lw-status" variant={dirty ? 'warning' : 'secondary'}>
|
|
747
|
-
{
|
|
924
|
+
{hasUnsavedChanges() ? t('dirty') : t('saved')}
|
|
748
925
|
</Badge>
|
|
749
926
|
</div>
|
|
750
927
|
<input
|
|
@@ -758,27 +935,85 @@ function App() {
|
|
|
758
935
|
<div className="lw-stage">
|
|
759
936
|
{selectedId || detail?.item ? (
|
|
760
937
|
<div className="lw-editor-pane">
|
|
761
|
-
<
|
|
762
|
-
<
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
className="lw-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
938
|
+
<Tabs className="lw-tabs" value={mainTab} onValueChange={(value: string) => setMainTab(value)}>
|
|
939
|
+
<div className="lw-editor-header">
|
|
940
|
+
<TabsList>
|
|
941
|
+
<TabsTrigger value="preview">
|
|
942
|
+
<Image className="lw-button-icon" aria-hidden="true" />
|
|
943
|
+
{t('preview')}
|
|
944
|
+
</TabsTrigger>
|
|
945
|
+
<TabsTrigger value="json">
|
|
946
|
+
<FileJson className="lw-button-icon" aria-hidden="true" />
|
|
947
|
+
{t('json')}
|
|
948
|
+
</TabsTrigger>
|
|
949
|
+
<TabsTrigger value="mermaid">{t('mermaid')}</TabsTrigger>
|
|
950
|
+
<TabsTrigger value="links">{t('links')}</TabsTrigger>
|
|
951
|
+
</TabsList>
|
|
952
|
+
<Badge variant={embeddableUrl || imagePreviewUrl || standardImportPreview ? 'success' : 'secondary'}>{previewBadge}</Badge>
|
|
953
|
+
</div>
|
|
954
|
+
|
|
955
|
+
<TabsContent className="lw-tab-content" value="preview">
|
|
956
|
+
<div className="lw-visual-frame">
|
|
957
|
+
{embeddableUrl ? (
|
|
958
|
+
<iframe title="Lucidchart embed" src={embeddableUrl} />
|
|
959
|
+
) : imagePreviewUrl ? (
|
|
960
|
+
<img src={imagePreviewUrl} alt={t('imagePreview')} />
|
|
961
|
+
) : standardImportPreview ? (
|
|
962
|
+
<StandardImportPreview model={standardImportPreview} />
|
|
963
|
+
) : (
|
|
964
|
+
<div className="lw-embed-empty">{t('previewUnavailable')}</div>
|
|
965
|
+
)}
|
|
966
|
+
</div>
|
|
967
|
+
</TabsContent>
|
|
968
|
+
|
|
969
|
+
<TabsContent className="lw-tab-content lw-json-tab" value="json">
|
|
970
|
+
<div className="lw-tab-toolbar">
|
|
971
|
+
<div className="lw-inline-badges">
|
|
972
|
+
<Badge variant={isJsonValid ? 'success' : 'warning'}>{isJsonValid ? t('jsonValid') : t('jsonInvalid')}</Badge>
|
|
973
|
+
{standardImportParse.error ? <span className="lw-muted">{standardImportParse.error}</span> : null}
|
|
974
|
+
</div>
|
|
975
|
+
<div className="lw-inline-actions">
|
|
976
|
+
<Button type="button" variant="outline" size="sm" disabled={!isJsonValid} onClick={formatStandardImportJson}>
|
|
977
|
+
{t('formatJson')}
|
|
978
|
+
</Button>
|
|
979
|
+
<Button type="button" variant="outline" size="sm" disabled={busy} onClick={revertStandardImportJson}>
|
|
980
|
+
<RotateCcw className="lw-button-icon" aria-hidden="true" />
|
|
981
|
+
{t('revertJson')}
|
|
982
|
+
</Button>
|
|
983
|
+
</div>
|
|
984
|
+
</div>
|
|
985
|
+
<Textarea className="lw-json-editor" value={standardImportText} onChange={(event: any) => updateStandardImportText(event.target.value)} />
|
|
986
|
+
</TabsContent>
|
|
987
|
+
|
|
988
|
+
<TabsContent className="lw-tab-content lw-form-tab" value="mermaid">
|
|
989
|
+
<section className="lw-section">
|
|
990
|
+
<div className="lw-section-title">{t('mermaid')}</div>
|
|
991
|
+
<Textarea className="lw-tall-textarea" value={mermaidSource} onChange={(event: any) => updateMermaidSource(event.target.value)} />
|
|
992
|
+
<div className="lw-muted">{t('standardImportNotice')}</div>
|
|
993
|
+
<div className="lw-inline-actions">
|
|
994
|
+
<Button type="button" disabled={busy || !mermaidSource.trim()} onClick={saveMermaidDraft}>
|
|
995
|
+
<Save className="lw-button-icon" aria-hidden="true" />
|
|
996
|
+
{t('saveMermaid')}
|
|
997
|
+
</Button>
|
|
998
|
+
</div>
|
|
999
|
+
</section>
|
|
1000
|
+
</TabsContent>
|
|
1001
|
+
|
|
1002
|
+
<TabsContent className="lw-tab-content lw-form-tab" value="links">
|
|
1003
|
+
<section className="lw-section">
|
|
1004
|
+
<div className="lw-section-title">{t('externalDocument')}</div>
|
|
1005
|
+
<Input value={lucidDocumentUrl} placeholder={t('lucidDocumentUrl')} onChange={(event: any) => updateLucidDocumentUrl(event.target.value)} />
|
|
1006
|
+
<Input value={embedUrl} placeholder={t('embedUrl')} onChange={(event: any) => updateEmbedUrl(event.target.value)} />
|
|
1007
|
+
<Input value={lucidDocumentId} placeholder={t('lucidDocumentId')} onChange={(event: any) => updateLucidDocumentId(event.target.value)} />
|
|
1008
|
+
<Input value={previewUrl} placeholder={t('previewUrl')} onChange={(event: any) => updatePreviewUrl(event.target.value)} />
|
|
1009
|
+
<div className="lw-inline-actions">
|
|
1010
|
+
<Button type="button" disabled={busy || (!lucidDocumentId.trim() && !lucidDocumentUrl.trim() && !embedUrl.trim())} onClick={registerExternalDocument}>
|
|
1011
|
+
{t('registerExternal')}
|
|
1012
|
+
</Button>
|
|
1013
|
+
</div>
|
|
1014
|
+
</section>
|
|
1015
|
+
</TabsContent>
|
|
1016
|
+
</Tabs>
|
|
782
1017
|
</div>
|
|
783
1018
|
) : (
|
|
784
1019
|
<div className="lw-empty">{t('noDocument')}</div>
|
|
@@ -789,28 +1024,7 @@ function App() {
|
|
|
789
1024
|
<Sidebar className="lw-inspector" side="right" collapsed={rightPanelCollapsed}>
|
|
790
1025
|
<SidebarHeader>
|
|
791
1026
|
{!rightPanelCollapsed ? (
|
|
792
|
-
|
|
793
|
-
<div className="lw-inspector-actions">
|
|
794
|
-
{documentStatus === 'archived' ? (
|
|
795
|
-
<Badge variant="secondary">{t('archived')}</Badge>
|
|
796
|
-
) : documentStatus === 'reviewed' ? (
|
|
797
|
-
<Button type="button" variant="outline" size="sm" disabled={busy || !selectedId} onClick={() => setDocumentReviewStatus('draft')}>
|
|
798
|
-
<RotateCcw className="lw-button-icon" aria-hidden="true" />
|
|
799
|
-
{t('backToDraft')}
|
|
800
|
-
</Button>
|
|
801
|
-
) : (
|
|
802
|
-
<Button type="button" variant="outline" size="sm" disabled={busy || !selectedId} onClick={() => setDocumentReviewStatus('reviewed')}>
|
|
803
|
-
<Check className="lw-button-icon" aria-hidden="true" />
|
|
804
|
-
{t('markReviewed')}
|
|
805
|
-
</Button>
|
|
806
|
-
)}
|
|
807
|
-
<Button type="button" variant="destructiveOutline" size="sm" disabled={busy || !selectedId || documentStatus === 'archived'} onClick={archiveDocument}>
|
|
808
|
-
<Archive className="lw-button-icon" aria-hidden="true" />
|
|
809
|
-
{t('archive')}
|
|
810
|
-
</Button>
|
|
811
|
-
</div>
|
|
812
|
-
<SidebarTitle className="lw-sidebar-title-truncate">{detail?.item?.title || t('inspector')}</SidebarTitle>
|
|
813
|
-
</>
|
|
1027
|
+
<SidebarTitle className="lw-sidebar-title-truncate">{detail?.item?.title || t('inspector')}</SidebarTitle>
|
|
814
1028
|
) : null}
|
|
815
1029
|
<SidebarTrigger
|
|
816
1030
|
className="lw-sidebar-trigger-right"
|
|
@@ -828,72 +1042,117 @@ function App() {
|
|
|
828
1042
|
) : (
|
|
829
1043
|
<SidebarContent>
|
|
830
1044
|
<ScrollArea className="lw-inspector-scroll">
|
|
831
|
-
<
|
|
832
|
-
<
|
|
833
|
-
<
|
|
834
|
-
<
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
<
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
>
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1045
|
+
<Tabs className="lw-inspector-tabs" value={inspectorTab} onValueChange={(value: string) => setInspectorTab(value)}>
|
|
1046
|
+
<TabsList className="lw-inspector-tabs-list">
|
|
1047
|
+
<TabsTrigger value="info">{t('info')}</TabsTrigger>
|
|
1048
|
+
<TabsTrigger value="versions">{t('versions')}</TabsTrigger>
|
|
1049
|
+
<TabsTrigger value="activity">{t('activity')}</TabsTrigger>
|
|
1050
|
+
<TabsTrigger value="assistant">{t('assistant')}</TabsTrigger>
|
|
1051
|
+
</TabsList>
|
|
1052
|
+
|
|
1053
|
+
<TabsContent className="lw-inspector-stack" value="info">
|
|
1054
|
+
<section className="lw-section">
|
|
1055
|
+
<div className="lw-section-title">{t('documentInfo')}</div>
|
|
1056
|
+
<Input value={metadataTitle} placeholder={t('title')} onChange={(event: any) => updateMetadataTitle(event.target.value)} />
|
|
1057
|
+
<Textarea value={metadataDescription} placeholder={t('description')} onChange={(event: any) => updateMetadataDescription(event.target.value)} />
|
|
1058
|
+
<Select value={metadataKind} onValueChange={(value: string) => updateMetadataKind(normalizeDocumentKind(value))}>
|
|
1059
|
+
<SelectTrigger aria-label={t('kind')}>
|
|
1060
|
+
<SelectValue placeholder={t('kind')} />
|
|
1061
|
+
</SelectTrigger>
|
|
1062
|
+
<SelectContent>
|
|
1063
|
+
{DOCUMENT_KINDS.map((kind) => (
|
|
1064
|
+
<SelectItem value={kind} key={kind}>
|
|
1065
|
+
{t(kind)}
|
|
1066
|
+
</SelectItem>
|
|
1067
|
+
))}
|
|
1068
|
+
</SelectContent>
|
|
1069
|
+
</Select>
|
|
1070
|
+
</section>
|
|
1071
|
+
|
|
1072
|
+
<section className="lw-section">
|
|
1073
|
+
<div className="lw-section-title">{t('changeSummary')}</div>
|
|
1074
|
+
<Input value={changeSummary} placeholder={t('changeSummary')} onChange={(event: any) => setChangeSummary(event.target.value)} />
|
|
1075
|
+
</section>
|
|
860
1076
|
|
|
861
|
-
<section className="lw-section">
|
|
862
|
-
<div className="lw-section-title">{t('mermaid')}</div>
|
|
863
|
-
<Textarea value={mermaidSource} onChange={(event: any) => updateMermaidSource(event.target.value)} />
|
|
864
|
-
<div className="lw-muted">{t('standardImportNotice')}</div>
|
|
865
1077
|
<div className="lw-inline-actions">
|
|
866
|
-
<Button type="button"
|
|
867
|
-
|
|
1078
|
+
<Button type="button" disabled={busy || !selectedId || !metadataDirty} onClick={updateDocumentMetadata}>
|
|
1079
|
+
<Save className="lw-button-icon" aria-hidden="true" />
|
|
1080
|
+
{t('saveMetadata')}
|
|
1081
|
+
</Button>
|
|
1082
|
+
{documentStatus === 'archived' ? (
|
|
1083
|
+
<Badge variant="secondary">{t('archived')}</Badge>
|
|
1084
|
+
) : documentStatus === 'reviewed' ? (
|
|
1085
|
+
<Button type="button" variant="outline" disabled={busy || !selectedId} onClick={() => setDocumentReviewStatus('draft')}>
|
|
1086
|
+
<RotateCcw className="lw-button-icon" aria-hidden="true" />
|
|
1087
|
+
{t('backToDraft')}
|
|
1088
|
+
</Button>
|
|
1089
|
+
) : (
|
|
1090
|
+
<Button type="button" variant="outline" disabled={busy || !selectedId} onClick={() => setDocumentReviewStatus('reviewed')}>
|
|
1091
|
+
<Check className="lw-button-icon" aria-hidden="true" />
|
|
1092
|
+
{t('markReviewed')}
|
|
1093
|
+
</Button>
|
|
1094
|
+
)}
|
|
1095
|
+
<Button type="button" variant="destructiveOutline" disabled={busy || !selectedId || documentStatus === 'archived'} onClick={archiveDocument}>
|
|
1096
|
+
<Archive className="lw-button-icon" aria-hidden="true" />
|
|
1097
|
+
{t('archive')}
|
|
868
1098
|
</Button>
|
|
869
1099
|
</div>
|
|
870
|
-
</
|
|
871
|
-
|
|
872
|
-
<
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1100
|
+
</TabsContent>
|
|
1101
|
+
|
|
1102
|
+
<TabsContent className="lw-inspector-stack" value="versions">
|
|
1103
|
+
{(detail?.versions || []).length ? (
|
|
1104
|
+
(detail?.versions || []).map((version) => (
|
|
1105
|
+
<div className="lw-version" key={version.id}>
|
|
1106
|
+
<div>
|
|
1107
|
+
<div>v{version.versionNumber}</div>
|
|
1108
|
+
<div className="lw-muted">{version.sourceType || 'workbench'}</div>
|
|
1109
|
+
{version.changeSummary ? <div className="lw-muted">{version.changeSummary}</div> : null}
|
|
1110
|
+
</div>
|
|
1111
|
+
<Button
|
|
1112
|
+
className="lw-version-action"
|
|
1113
|
+
type="button"
|
|
1114
|
+
variant="outline"
|
|
1115
|
+
size="icon"
|
|
1116
|
+
title={t('restore')}
|
|
1117
|
+
aria-label={`${t('restore')} v${version.versionNumber}`}
|
|
1118
|
+
disabled={busy}
|
|
1119
|
+
onClick={() => restoreVersion(version.id)}
|
|
1120
|
+
>
|
|
1121
|
+
<RotateCcw className="lw-button-icon" aria-hidden="true" />
|
|
1122
|
+
</Button>
|
|
1123
|
+
</div>
|
|
1124
|
+
))
|
|
1125
|
+
) : (
|
|
1126
|
+
<div className="lw-empty-state">{t('noVersions')}</div>
|
|
1127
|
+
)}
|
|
1128
|
+
</TabsContent>
|
|
1129
|
+
|
|
1130
|
+
<TabsContent className="lw-inspector-stack" value="activity">
|
|
1131
|
+
{(detail?.logs || []).length ? (
|
|
1132
|
+
(detail?.logs || []).map((log) => (
|
|
1133
|
+
<div className="lw-log" key={log.id || `${log.action}-${log.createdAt}`}>
|
|
1134
|
+
<div className="lw-log-title">{formatLogTitle(log)}</div>
|
|
1135
|
+
<div className="lw-muted">{formatDateTime(log.createdAt)}</div>
|
|
1136
|
+
{log.message ? <div className="lw-log-message">{log.message}</div> : null}
|
|
1137
|
+
{log.errorMessage ? <div className="lw-log-error">{log.errorMessage}</div> : null}
|
|
1138
|
+
</div>
|
|
1139
|
+
))
|
|
1140
|
+
) : (
|
|
1141
|
+
<div className="lw-empty-state">{t('noActivity')}</div>
|
|
1142
|
+
)}
|
|
1143
|
+
</TabsContent>
|
|
1144
|
+
|
|
1145
|
+
<TabsContent className="lw-inspector-stack" value="assistant">
|
|
1146
|
+
<section className="lw-section">
|
|
1147
|
+
<div className="lw-section-title">{t('drawingRequest')}</div>
|
|
1148
|
+
<Textarea className="lw-tall-textarea" value={assistantPrompt} placeholder={t('drawingRequest')} onChange={(event: any) => setAssistantPrompt(event.target.value)} />
|
|
1149
|
+
<Button type="button" disabled={busy || !assistantPrompt.trim()} onClick={sendAssistantPrompt}>
|
|
1150
|
+
<Send className="lw-button-icon" aria-hidden="true" />
|
|
1151
|
+
{t('askAssistant')}
|
|
1152
|
+
</Button>
|
|
1153
|
+
</section>
|
|
1154
|
+
</TabsContent>
|
|
1155
|
+
</Tabs>
|
|
897
1156
|
</ScrollArea>
|
|
898
1157
|
</SidebarContent>
|
|
899
1158
|
)}
|
|
@@ -960,6 +1219,46 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
960
1219
|
return Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
|
961
1220
|
}
|
|
962
1221
|
|
|
1222
|
+
function parseStandardImportDocument(source: string): JsonParseResult {
|
|
1223
|
+
try {
|
|
1224
|
+
const parsed = JSON.parse(source)
|
|
1225
|
+
if (!isObject(parsed)) {
|
|
1226
|
+
return { value: null, error: 'Expected a JSON object.' }
|
|
1227
|
+
}
|
|
1228
|
+
return { value: parsed, error: null }
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
return {
|
|
1231
|
+
value: null,
|
|
1232
|
+
error: error instanceof Error && error.message ? error.message : 'Invalid JSON.'
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function normalizeDocumentKind(value: unknown): DocumentKind {
|
|
1238
|
+
return DOCUMENT_KINDS.includes(value as DocumentKind) ? (value as DocumentKind) : 'diagram'
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function formatDateTime(value: unknown) {
|
|
1242
|
+
if (!value) {
|
|
1243
|
+
return ''
|
|
1244
|
+
}
|
|
1245
|
+
const date = new Date(String(value))
|
|
1246
|
+
if (Number.isNaN(date.getTime())) {
|
|
1247
|
+
return String(value)
|
|
1248
|
+
}
|
|
1249
|
+
return date.toLocaleString()
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function formatLogTitle(log: Record<string, unknown>) {
|
|
1253
|
+
const action = typeof log.action === 'string' && log.action.trim() ? log.action.trim() : 'activity'
|
|
1254
|
+
const actor = typeof log.actorType === 'string' && log.actorType.trim() ? log.actorType.trim() : ''
|
|
1255
|
+
return actor ? `${action} · ${actor}` : action
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function isCompactViewport() {
|
|
1259
|
+
return typeof window !== 'undefined' && window.innerWidth < 1040
|
|
1260
|
+
}
|
|
1261
|
+
|
|
963
1262
|
function StandardImportPreview({ model }: { model: StandardImportPreviewModel }) {
|
|
964
1263
|
return (
|
|
965
1264
|
<div className="lw-standard-preview">
|