@terra-symposium/docs 0.0.0-stage → 0.1.17
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/LICENSE +201 -0
- package/README.md +72 -2
- package/lib-dist/Docs.d.ts +22 -0
- package/lib-dist/Docs.d.ts.map +1 -0
- package/lib-dist/Docs.js +60 -0
- package/lib-dist/Docs.js.map +1 -0
- package/lib-dist/LocalDocsWorkspace.d.ts +30 -0
- package/lib-dist/LocalDocsWorkspace.d.ts.map +1 -0
- package/lib-dist/LocalDocsWorkspace.js +148 -0
- package/lib-dist/LocalDocsWorkspace.js.map +1 -0
- package/lib-dist/SiteLayout.d.ts +30 -0
- package/lib-dist/SiteLayout.d.ts.map +1 -0
- package/lib-dist/SiteLayout.js +34 -0
- package/lib-dist/SiteLayout.js.map +1 -0
- package/lib-dist/StaticWorkspace.d.ts +21 -0
- package/lib-dist/StaticWorkspace.d.ts.map +1 -0
- package/lib-dist/StaticWorkspace.js +179 -0
- package/lib-dist/StaticWorkspace.js.map +1 -0
- package/lib-dist/_Toolbar.d.ts +7 -0
- package/lib-dist/_Toolbar.d.ts.map +1 -0
- package/lib-dist/_Toolbar.js +85 -0
- package/lib-dist/_Toolbar.js.map +1 -0
- package/lib-dist/blocks.d.ts +23 -0
- package/lib-dist/blocks.d.ts.map +1 -0
- package/lib-dist/blocks.js +321 -0
- package/lib-dist/blocks.js.map +1 -0
- package/lib-dist/docs-catalog.d.ts +5 -0
- package/lib-dist/docs-catalog.d.ts.map +1 -0
- package/lib-dist/docs-catalog.js +38 -0
- package/lib-dist/docs-catalog.js.map +1 -0
- package/lib-dist/docs-style.css +31 -0
- package/lib-dist/fonts/Avara-Bold.woff2 +0 -0
- package/lib-dist/fonts/Avara-BoldItalic.woff2 +0 -0
- package/lib-dist/fonts/OFL-1.1.txt +93 -0
- package/lib-dist/fonts/README.md +5 -0
- package/lib-dist/host-runtime.d.ts +5 -0
- package/lib-dist/host-runtime.d.ts.map +1 -0
- package/lib-dist/host-runtime.js +21 -0
- package/lib-dist/host-runtime.js.map +1 -0
- package/lib-dist/host.d.ts +170 -0
- package/lib-dist/host.d.ts.map +1 -0
- package/lib-dist/host.js +2 -0
- package/lib-dist/host.js.map +1 -0
- package/lib-dist/index.d.ts +35 -0
- package/lib-dist/index.d.ts.map +1 -0
- package/lib-dist/index.js +28 -0
- package/lib-dist/index.js.map +1 -0
- package/lib-dist/markdown-model.d.ts +7 -0
- package/lib-dist/markdown-model.d.ts.map +1 -0
- package/lib-dist/markdown-model.js +181 -0
- package/lib-dist/markdown-model.js.map +1 -0
- package/lib-dist/markdown.d.ts +3 -0
- package/lib-dist/markdown.d.ts.map +1 -0
- package/lib-dist/markdown.js +83 -0
- package/lib-dist/markdown.js.map +1 -0
- package/lib-dist/navigation.d.ts +17 -0
- package/lib-dist/navigation.d.ts.map +1 -0
- package/lib-dist/navigation.js +9 -0
- package/lib-dist/navigation.js.map +1 -0
- package/lib-dist/schema.d.ts +135 -0
- package/lib-dist/schema.d.ts.map +1 -0
- package/lib-dist/schema.js +55 -0
- package/lib-dist/schema.js.map +1 -0
- package/lib-dist/style.css +1 -0
- package/lib-dist/terra/Workspace.d.ts +5 -0
- package/lib-dist/terra/Workspace.d.ts.map +1 -0
- package/lib-dist/terra/Workspace.js +9 -0
- package/lib-dist/terra/Workspace.js.map +1 -0
- package/lib-dist/terra/editor/TerraContentEditor.d.ts +10 -0
- package/lib-dist/terra/editor/TerraContentEditor.d.ts.map +1 -0
- package/lib-dist/terra/editor/TerraContentEditor.js +47 -0
- package/lib-dist/terra/editor/TerraContentEditor.js.map +1 -0
- package/lib-dist/terra/editor/_blocks.d.ts +71 -0
- package/lib-dist/terra/editor/_blocks.d.ts.map +1 -0
- package/lib-dist/terra/editor/_blocks.js +110 -0
- package/lib-dist/terra/editor/_blocks.js.map +1 -0
- package/lib-dist/terra/editor/docs/_Callout.d.ts +14 -0
- package/lib-dist/terra/editor/docs/_Callout.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_Callout.js +41 -0
- package/lib-dist/terra/editor/docs/_Callout.js.map +1 -0
- package/lib-dist/terra/editor/docs/_CodeBlock.d.ts +39 -0
- package/lib-dist/terra/editor/docs/_CodeBlock.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_CodeBlock.js +494 -0
- package/lib-dist/terra/editor/docs/_CodeBlock.js.map +1 -0
- package/lib-dist/terra/editor/docs/_DocsCode.d.ts +14 -0
- package/lib-dist/terra/editor/docs/_DocsCode.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_DocsCode.js +95 -0
- package/lib-dist/terra/editor/docs/_DocsCode.js.map +1 -0
- package/lib-dist/terra/editor/docs/_DocsImage.d.ts +18 -0
- package/lib-dist/terra/editor/docs/_DocsImage.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_DocsImage.js +62 -0
- package/lib-dist/terra/editor/docs/_DocsImage.js.map +1 -0
- package/lib-dist/terra/editor/docs/_Embed.d.ts +15 -0
- package/lib-dist/terra/editor/docs/_Embed.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_Embed.js +27 -0
- package/lib-dist/terra/editor/docs/_Embed.js.map +1 -0
- package/lib-dist/terra/editor/docs/_Math.d.ts +31 -0
- package/lib-dist/terra/editor/docs/_Math.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_Math.js +126 -0
- package/lib-dist/terra/editor/docs/_Math.js.map +1 -0
- package/lib-dist/terra/editor/docs/_Nodes.d.ts +15 -0
- package/lib-dist/terra/editor/docs/_Nodes.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_Nodes.js +52 -0
- package/lib-dist/terra/editor/docs/_Nodes.js.map +1 -0
- package/lib-dist/terra/editor/docs/_Raw.d.ts +15 -0
- package/lib-dist/terra/editor/docs/_Raw.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_Raw.js +26 -0
- package/lib-dist/terra/editor/docs/_Raw.js.map +1 -0
- package/lib-dist/terra/editor/docs/_Table.d.ts +12 -0
- package/lib-dist/terra/editor/docs/_Table.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_Table.js +71 -0
- package/lib-dist/terra/editor/docs/_Table.js.map +1 -0
- package/lib-dist/terra/editor/docs/_Tabs.d.ts +14 -0
- package/lib-dist/terra/editor/docs/_Tabs.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_Tabs.js +104 -0
- package/lib-dist/terra/editor/docs/_Tabs.js.map +1 -0
- package/lib-dist/terra/editor/docs/_TsDocs.d.ts +15 -0
- package/lib-dist/terra/editor/docs/_TsDocs.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_TsDocs.js +81 -0
- package/lib-dist/terra/editor/docs/_TsDocs.js.map +1 -0
- package/lib-dist/terra/editor/docs/_math_inline.d.ts +14 -0
- package/lib-dist/terra/editor/docs/_math_inline.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/_math_inline.js +30 -0
- package/lib-dist/terra/editor/docs/_math_inline.js.map +1 -0
- package/lib-dist/terra/editor/docs/heading.d.ts +23 -0
- package/lib-dist/terra/editor/docs/heading.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/heading.js +60 -0
- package/lib-dist/terra/editor/docs/heading.js.map +1 -0
- package/lib-dist/terra/editor/docs/index.d.ts +13 -0
- package/lib-dist/terra/editor/docs/index.d.ts.map +1 -0
- package/lib-dist/terra/editor/docs/index.js +14 -0
- package/lib-dist/terra/editor/docs/index.js.map +1 -0
- package/lib-dist/terra/editor/types.d.ts +40 -0
- package/lib-dist/terra/editor/types.d.ts.map +1 -0
- package/lib-dist/terra/editor/types.js +11 -0
- package/lib-dist/terra/editor/types.js.map +1 -0
- package/lib-dist/terra/flow/Nodes.d.ts +78 -0
- package/lib-dist/terra/flow/Nodes.d.ts.map +1 -0
- package/lib-dist/terra/flow/Nodes.js +912 -0
- package/lib-dist/terra/flow/Nodes.js.map +1 -0
- package/lib-dist/terra/flow/_chrome.d.ts +267 -0
- package/lib-dist/terra/flow/_chrome.d.ts.map +1 -0
- package/lib-dist/terra/flow/_chrome.js +798 -0
- package/lib-dist/terra/flow/_chrome.js.map +1 -0
- package/lib-dist/terra/flow/_layout.d.ts +30 -0
- package/lib-dist/terra/flow/_layout.d.ts.map +1 -0
- package/lib-dist/terra/flow/_layout.js +53 -0
- package/lib-dist/terra/flow/_layout.js.map +1 -0
- package/lib-dist/terra/flow/menuContext.d.ts +13 -0
- package/lib-dist/terra/flow/menuContext.d.ts.map +1 -0
- package/lib-dist/terra/flow/menuContext.js +10 -0
- package/lib-dist/terra/flow/menuContext.js.map +1 -0
- package/lib-dist/terra/flow/powerSource.d.ts +16 -0
- package/lib-dist/terra/flow/powerSource.d.ts.map +1 -0
- package/lib-dist/terra/flow/powerSource.js +59 -0
- package/lib-dist/terra/flow/powerSource.js.map +1 -0
- package/lib-dist/terra/pages/docs/_attachments.d.ts +20 -0
- package/lib-dist/terra/pages/docs/_attachments.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_attachments.js +904 -0
- package/lib-dist/terra/pages/docs/_attachments.js.map +1 -0
- package/lib-dist/terra/pages/docs/_blocks.d.ts +48 -0
- package/lib-dist/terra/pages/docs/_blocks.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_blocks.js +192 -0
- package/lib-dist/terra/pages/docs/_blocks.js.map +1 -0
- package/lib-dist/terra/pages/docs/_custom.d.ts +5 -0
- package/lib-dist/terra/pages/docs/_custom.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_custom.js +6 -0
- package/lib-dist/terra/pages/docs/_custom.js.map +1 -0
- package/lib-dist/terra/pages/docs/_editable.d.ts +61 -0
- package/lib-dist/terra/pages/docs/_editable.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_editable.js +404 -0
- package/lib-dist/terra/pages/docs/_editable.js.map +1 -0
- package/lib-dist/terra/pages/docs/_export-dialog.d.ts +19 -0
- package/lib-dist/terra/pages/docs/_export-dialog.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_export-dialog.js +78 -0
- package/lib-dist/terra/pages/docs/_export-dialog.js.map +1 -0
- package/lib-dist/terra/pages/docs/_helpers.d.ts +395 -0
- package/lib-dist/terra/pages/docs/_helpers.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_helpers.js +911 -0
- package/lib-dist/terra/pages/docs/_helpers.js.map +1 -0
- package/lib-dist/terra/pages/docs/_icons.d.ts +20 -0
- package/lib-dist/terra/pages/docs/_icons.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_icons.js +29 -0
- package/lib-dist/terra/pages/docs/_icons.js.map +1 -0
- package/lib-dist/terra/pages/docs/_main.d.ts +51 -0
- package/lib-dist/terra/pages/docs/_main.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_main.js +1959 -0
- package/lib-dist/terra/pages/docs/_main.js.map +1 -0
- package/lib-dist/terra/pages/docs/_sidebar.d.ts +42 -0
- package/lib-dist/terra/pages/docs/_sidebar.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_sidebar.js +2450 -0
- package/lib-dist/terra/pages/docs/_sidebar.js.map +1 -0
- package/lib-dist/terra/pages/docs/_toolbar.d.ts +23 -0
- package/lib-dist/terra/pages/docs/_toolbar.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_toolbar.js +1539 -0
- package/lib-dist/terra/pages/docs/_toolbar.js.map +1 -0
- package/lib-dist/terra/pages/docs/_tree.d.ts +178 -0
- package/lib-dist/terra/pages/docs/_tree.d.ts.map +1 -0
- package/lib-dist/terra/pages/docs/_tree.js +1168 -0
- package/lib-dist/terra/pages/docs/_tree.js.map +1 -0
- package/lib-dist/terra/regex.mjs +398 -0
- package/lib-dist/terra/runtime/action-proxy.d.ts +5 -0
- package/lib-dist/terra/runtime/action-proxy.d.ts.map +1 -0
- package/lib-dist/terra/runtime/action-proxy.js +19 -0
- package/lib-dist/terra/runtime/action-proxy.js.map +1 -0
- package/lib-dist/terra/runtime/content.d.ts +150 -0
- package/lib-dist/terra/runtime/content.d.ts.map +1 -0
- package/lib-dist/terra/runtime/content.js +1171 -0
- package/lib-dist/terra/runtime/content.js.map +1 -0
- package/lib-dist/terra/runtime/leaf.d.ts +5 -0
- package/lib-dist/terra/runtime/leaf.d.ts.map +1 -0
- package/lib-dist/terra/runtime/leaf.js +13 -0
- package/lib-dist/terra/runtime/leaf.js.map +1 -0
- package/lib-dist/terra/runtime/nodes.d.ts +112 -0
- package/lib-dist/terra/runtime/nodes.d.ts.map +1 -0
- package/lib-dist/terra/runtime/nodes.js +534 -0
- package/lib-dist/terra/runtime/nodes.js.map +1 -0
- package/lib-dist/terra/runtime/runtime.d.ts +25 -0
- package/lib-dist/terra/runtime/runtime.d.ts.map +1 -0
- package/lib-dist/terra/runtime/runtime.js +50 -0
- package/lib-dist/terra/runtime/runtime.js.map +1 -0
- package/lib-dist/terra/runtime/shared.d.ts +35 -0
- package/lib-dist/terra/runtime/shared.d.ts.map +1 -0
- package/lib-dist/terra/runtime/shared.js +79 -0
- package/lib-dist/terra/runtime/shared.js.map +1 -0
- package/lib-dist/terra/runtime/types.d.ts +266 -0
- package/lib-dist/terra/runtime/types.d.ts.map +1 -0
- package/lib-dist/terra/runtime/types.js +18 -0
- package/lib-dist/terra/runtime/types.js.map +1 -0
- package/lib-dist/terra/runtime/ui.d.ts +31 -0
- package/lib-dist/terra/runtime/ui.d.ts.map +1 -0
- package/lib-dist/terra/runtime/ui.js +16 -0
- package/lib-dist/terra/runtime/ui.js.map +1 -0
- package/lib-dist/workspace.d.ts +78 -0
- package/lib-dist/workspace.d.ts.map +1 -0
- package/lib-dist/workspace.js +151 -0
- package/lib-dist/workspace.js.map +1 -0
- package/package.json +90 -3
- package/src/SiteLayout.astro +38 -0
- package/src/navigation.tsx +47 -0
- package/src/style.css +733 -0
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
// @ts-nocheck -- source-preserving extraction; public host contracts are typed in src/host.ts.
|
|
3
|
+
/**
|
|
4
|
+
* astro/src/pages/docs/_attachments.tsx
|
|
5
|
+
* Docs attachment collection, uploads, and attachment-specific context menus.
|
|
6
|
+
*/
|
|
7
|
+
/*//////////////////////////////////////////////////
|
|
8
|
+
IMPORTS
|
|
9
|
+
//////////////////////////////////////////////////*/
|
|
10
|
+
// API
|
|
11
|
+
import { $attachmentFilter, $surfaceReady, $transition, clearTransition, sanitizeName, uploadAssetChunked, useAtom } from './_helpers.js';
|
|
12
|
+
import { actions } from '../../runtime/action-proxy.js';
|
|
13
|
+
import { cn, error as logError, errorLog, isRec, notify, warn } from '../../runtime/runtime.js';
|
|
14
|
+
import { createElement, createElement as __terraQualityCreateElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
15
|
+
import { currLeafSrc } from '../../runtime/leaf.js';
|
|
16
|
+
import { dirname, docsPath, isDocsPath } from '../../runtime/shared.js';
|
|
17
|
+
import { toast } from 'sonner';
|
|
18
|
+
// COMPONENTS
|
|
19
|
+
import * as TerraQualityRegex from '../../regex.mjs';
|
|
20
|
+
import { ArrowUpRight as PopOutIcon, Box as BoxIcon, Catalog as CatalogIcon, Copy as CopyIcon, Doc as DocumentIcon, Download as DownloadIcon, Edit as EditIcon, Image as ImageIcon, Link as LinkIcon, LogoYoutube as YouTubeIcon, MagicWand as MagicIcon, TrashCan as TrashIcon, Upload as UploadIcon, } from '@carbon/icons-react';
|
|
21
|
+
import { Button } from '@terra-symposium/styling/ui/terra-original';
|
|
22
|
+
import { Card, CardDescription, CardHeader, CardTitle } from '@terra-symposium/styling/ui/terra-original';
|
|
23
|
+
import { CodeBlock } from '../../editor/docs/_CodeBlock.js';
|
|
24
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@terra-symposium/styling/ui/terra-original';
|
|
25
|
+
import { ErrorBoundary } from '../../runtime/ui.js';
|
|
26
|
+
import { Empty } from '../../runtime/ui.js';
|
|
27
|
+
import { Input } from '@terra-symposium/styling/ui/terra-original';
|
|
28
|
+
import { Label } from '@terra-symposium/styling/ui/terra-original';
|
|
29
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@terra-symposium/styling/ui/terra-original';
|
|
30
|
+
import { Textarea } from '@terra-symposium/styling/ui/terra-original';
|
|
31
|
+
import { TsDocsSelect } from './_blocks.js';
|
|
32
|
+
// DATA
|
|
33
|
+
import { ATTCH_TYPES } from './_helpers.js';
|
|
34
|
+
/*//////////////////////////////////////////////////
|
|
35
|
+
HELPERS
|
|
36
|
+
//////////////////////////////////////////////////*/
|
|
37
|
+
/**
|
|
38
|
+
* Handles latest content.
|
|
39
|
+
*
|
|
40
|
+
* @param content - Docs content whose attachment references should be collected.
|
|
41
|
+
*/
|
|
42
|
+
const latestContent = (content) => {
|
|
43
|
+
try {
|
|
44
|
+
return currLeafSrc(content, '_attachments.latestContent');
|
|
45
|
+
}
|
|
46
|
+
catch (_err) {
|
|
47
|
+
return typeof content === 'string' ? content : '';
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Handles text asset.
|
|
52
|
+
*
|
|
53
|
+
* @param sis - Attachment records to normalize for the Docs editor.
|
|
54
|
+
*/
|
|
55
|
+
const textAsset = (sis) => /\.(?:csv|json|log|md|mdx|text|txt|xml|ya?ml)$/i.test(sis.name || sis.path.split('?')[0] || '');
|
|
56
|
+
/*//////////////////////////////////////////////////
|
|
57
|
+
COMPONENTS
|
|
58
|
+
//////////////////////////////////////////////////*/
|
|
59
|
+
/**
|
|
60
|
+
* Renders the document attachment manager for filtering, selecting, uploading, and organizing assets.
|
|
61
|
+
*
|
|
62
|
+
* @param props - The document path and current Docs tree used to scope and display its attachments.
|
|
63
|
+
*
|
|
64
|
+
* @return The attachment manager and its upload, selection, and organization controls.
|
|
65
|
+
*/
|
|
66
|
+
export function Attachments({ dPath, tree }) {
|
|
67
|
+
// STATES
|
|
68
|
+
const docId = dPath.split('/').filter(Boolean).at(-1) || dPath;
|
|
69
|
+
const attchFilter = useAtom($attachmentFilter);
|
|
70
|
+
const [sisters, setSisters] = useState([]);
|
|
71
|
+
const [sel, setSel] = useState([]);
|
|
72
|
+
const [loading, setLoading] = useState(true);
|
|
73
|
+
const [uploading, setUploading] = useState(false);
|
|
74
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
75
|
+
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
|
76
|
+
const [dialogMode, setDialogMode] = useState('select');
|
|
77
|
+
const [urlInput, setUrlInput] = useState('');
|
|
78
|
+
const [renameDlg, setRenameDlg] = useState(null);
|
|
79
|
+
const [renameInput, setRenameInput] = useState('');
|
|
80
|
+
const [viewDlg, setViewDlg] = useState(null);
|
|
81
|
+
const fileRef = useRef(null);
|
|
82
|
+
const [transferDlg, setTransferDlg] = useState(null);
|
|
83
|
+
const [transferTgt, setTransferTgt] = useState('');
|
|
84
|
+
const [transferring, setTransferring] = useState(false);
|
|
85
|
+
const [mergePdfState, setMergePdfState] = useState(null);
|
|
86
|
+
const [ctxAttachment, setCtxAttachment] = useState(null);
|
|
87
|
+
const sistersMap = useMemo(() => {
|
|
88
|
+
const map = new Map();
|
|
89
|
+
sisters.forEach(s => {
|
|
90
|
+
if (s.path)
|
|
91
|
+
map.set(s.path, s);
|
|
92
|
+
if (s.id)
|
|
93
|
+
map.set(s.id, s);
|
|
94
|
+
});
|
|
95
|
+
return map;
|
|
96
|
+
}, [sisters]);
|
|
97
|
+
const selItems = useMemo(() => sel.map(p => sistersMap.get(p)).filter((s) => Boolean(s)), [sel, sistersMap]);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!viewDlg)
|
|
100
|
+
return;
|
|
101
|
+
}, [viewDlg]);
|
|
102
|
+
// PROCESS
|
|
103
|
+
const localLeaves = useMemo(() => {
|
|
104
|
+
let tgts = [];
|
|
105
|
+
const groups = [tree];
|
|
106
|
+
while (groups.length > 0) {
|
|
107
|
+
const nodes = groups.pop();
|
|
108
|
+
if (nodes.some(n => n.type === 'leaf' && n.path === dPath)) {
|
|
109
|
+
tgts = nodes.filter((n) => n.type === 'leaf' && n.path !== dPath);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
113
|
+
const node = nodes[i];
|
|
114
|
+
if (node?.type === 'branch')
|
|
115
|
+
groups.push(node.children || []);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return tgts;
|
|
119
|
+
}, [tree, dPath]);
|
|
120
|
+
const gSisters = useMemo(() => {
|
|
121
|
+
const groups = {};
|
|
122
|
+
for (const s of sisters) {
|
|
123
|
+
let cat = 'other';
|
|
124
|
+
if (s.type === 'youtube')
|
|
125
|
+
cat = 'youtube';
|
|
126
|
+
else if (s.name.toLowerCase().endsWith('.pdf') || s.path.toLowerCase().split('?')[0].endsWith('.pdf') || s.isMd || s.type === 'document')
|
|
127
|
+
cat = 'document';
|
|
128
|
+
else if (s.type === 'website')
|
|
129
|
+
cat = 'website';
|
|
130
|
+
else if (s.type === 'tsdocs')
|
|
131
|
+
cat = 'tsdocs';
|
|
132
|
+
else if (TerraQualityRegex.REGEX_PNG_JPE_G_GIF_WEBP_SVG_AVIF_I_REGEX_2().test(s.name))
|
|
133
|
+
cat = 'image';
|
|
134
|
+
else if (s.type && ((val) => ATTCH_TYPES.includes(val))(s.type))
|
|
135
|
+
cat = s.type;
|
|
136
|
+
if (!attchFilter.includes(cat))
|
|
137
|
+
continue;
|
|
138
|
+
(groups[cat] ??= []).push(s);
|
|
139
|
+
}
|
|
140
|
+
for (const arr of Object.values(groups)) {
|
|
141
|
+
arr?.sort((a, b) => (a.title || a.name).localeCompare(b.title || b.name));
|
|
142
|
+
}
|
|
143
|
+
return groups;
|
|
144
|
+
}, [sisters, attchFilter]);
|
|
145
|
+
// SURFACE MOUNT LIFECYCLE
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
$surfaceReady.set(null);
|
|
148
|
+
if (loading)
|
|
149
|
+
return;
|
|
150
|
+
let a = true;
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
if (a)
|
|
153
|
+
$surfaceReady.set('attachments');
|
|
154
|
+
}, 100);
|
|
155
|
+
return () => {
|
|
156
|
+
a = false;
|
|
157
|
+
if ($surfaceReady.get() === 'attachments')
|
|
158
|
+
$surfaceReady.set(null);
|
|
159
|
+
};
|
|
160
|
+
}, [loading, dPath]);
|
|
161
|
+
// RESOLVE DIRECTORY SISTER FILE ASSETS
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
let a = true;
|
|
164
|
+
setLoading(true);
|
|
165
|
+
if (!dPath)
|
|
166
|
+
warn('Attachments', 'dPath is empty — no fetch will succeed.');
|
|
167
|
+
actions.read.doc
|
|
168
|
+
.attachments({ path: docId })
|
|
169
|
+
.then(async ({ data: d, error: err }) => {
|
|
170
|
+
if (err) {
|
|
171
|
+
errorLog('Attachments', 'Action returned an error', logError(err));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (!a)
|
|
175
|
+
return;
|
|
176
|
+
if (!d) {
|
|
177
|
+
warn('Attachments', 'data is null/undefined — setSisters not called.');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
setSisters(d);
|
|
181
|
+
})
|
|
182
|
+
.catch(_err => {
|
|
183
|
+
errorLog('Attachments', 'Fetch threw an exception', logError(_err));
|
|
184
|
+
})
|
|
185
|
+
.finally(() => {
|
|
186
|
+
if (a)
|
|
187
|
+
setLoading(false);
|
|
188
|
+
});
|
|
189
|
+
return () => {
|
|
190
|
+
a = false;
|
|
191
|
+
};
|
|
192
|
+
}, [docId]);
|
|
193
|
+
// ATTACHMENT ACTION HANDLERS
|
|
194
|
+
/**
|
|
195
|
+
* Handles view asset.
|
|
196
|
+
*
|
|
197
|
+
* @param sis - Attachment records being filtered for the active document.
|
|
198
|
+
*/
|
|
199
|
+
const viewAsset = async (sis) => {
|
|
200
|
+
if (!textAsset(sis) || !sis.path.startsWith('http')) {
|
|
201
|
+
setViewDlg(sis);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const res = await fetch(sis.path);
|
|
206
|
+
if (!res.ok)
|
|
207
|
+
throw new Error(`Attachment preview failed (${res.status}).`);
|
|
208
|
+
setViewDlg({ ...sis, content: await res.text() });
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
notify({ error: err instanceof Error ? err : new Error('Attachment preview failed.'), fallback: 'Failed to load attachment preview.' });
|
|
212
|
+
setViewDlg(sis);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Handles process upload.
|
|
217
|
+
*
|
|
218
|
+
* @param file - File selected for upload or attachment.
|
|
219
|
+
* @param tempId - Temporary identifier assigned before persistence.
|
|
220
|
+
* @param isMdFile - Whether the file should be treated as Markdown content.
|
|
221
|
+
*/
|
|
222
|
+
const processUpload = async (file, tempId, isMdFile) => {
|
|
223
|
+
try {
|
|
224
|
+
const finalAsset = await uploadAssetChunked(file, docId, {
|
|
225
|
+
onProgress: ({ currentChunk: currChunk, totalChunks, message: msg }) => {
|
|
226
|
+
if (msg)
|
|
227
|
+
setSisters(p => p.map(s => (s.id === tempId ? { ...s, name: msg } : s)));
|
|
228
|
+
else if (totalChunks > 1)
|
|
229
|
+
setSisters(p => p.map(s => (s.id === tempId ? { ...s, name: `Uploading (${currChunk}/${totalChunks})...` } : s)));
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
setSisters(p => p.map(s => (s.id === tempId ? { ...s, path: finalAsset.path || finalAsset.src || tempId, name: finalAsset.name || file.name, isMd: isMdFile, title: file.name } : s)));
|
|
233
|
+
}
|
|
234
|
+
catch (_err) {
|
|
235
|
+
errorLog('Attachments:Upload', `Failed '${file.name}'`, logError(_err));
|
|
236
|
+
setSisters(p => p.filter(s => s.id !== tempId));
|
|
237
|
+
toast.error(_err instanceof Error ? _err.message : `Failed to upload ${file.name}.`);
|
|
238
|
+
notify({ error: _err instanceof Error ? _err : new Error('Failed to attach.'), fallback: `Failed to upload ${file.name}.` });
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const add = useCallback(async (v) => {
|
|
242
|
+
if ($transition.get() || !v || v.length === 0 || !dPath)
|
|
243
|
+
return;
|
|
244
|
+
const validFiles = [];
|
|
245
|
+
const pdfFiles = [];
|
|
246
|
+
Array.from(v).forEach(f => {
|
|
247
|
+
validFiles.push(f);
|
|
248
|
+
if (f.name.toLowerCase().endsWith('.pdf') || f.type === 'application/pdf')
|
|
249
|
+
pdfFiles.push(f);
|
|
250
|
+
});
|
|
251
|
+
if (pdfFiles.length > 1) {
|
|
252
|
+
setMergePdfState({ pdfs: pdfFiles, others: validFiles.filter(f => !pdfFiles.includes(f)) });
|
|
253
|
+
if (fileRef.current)
|
|
254
|
+
fileRef.current.value = '';
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
validFiles.forEach((f, i) => {
|
|
258
|
+
const isMdFile = TerraQualityRegex.REGEX_MD_MDX_I_REGEX().test(f.name);
|
|
259
|
+
const tmpId = `temp-${Date.now()}-${i}`;
|
|
260
|
+
setSisters(p => [...p, { id: tmpId, path: tmpId, name: f.name, isMd: isMdFile }]);
|
|
261
|
+
void processUpload(f, tmpId, isMdFile);
|
|
262
|
+
});
|
|
263
|
+
if (fileRef.current)
|
|
264
|
+
fileRef.current.value = '';
|
|
265
|
+
}, [docId]);
|
|
266
|
+
const addRemote = useCallback(async (v, t) => {
|
|
267
|
+
setAddDialogOpen(false);
|
|
268
|
+
if ($transition.get() || !dPath || !v.trim())
|
|
269
|
+
return;
|
|
270
|
+
const urls = v.split(TerraQualityRegex.REGEX_N_S_REGEX()).filter(u => (t === 'tsdocs' ? u.trim().length > 0 : u.startsWith('http') || isDocsPath(u) || u.startsWith('/tsdocs')));
|
|
271
|
+
if (!urls.length)
|
|
272
|
+
return;
|
|
273
|
+
setUploading(true);
|
|
274
|
+
await Promise.allSettled(urls.map(async (u, i) => {
|
|
275
|
+
let aType = t;
|
|
276
|
+
if (t === 'other') {
|
|
277
|
+
if (u.toLowerCase().split('?')[0].endsWith('.pdf'))
|
|
278
|
+
aType = 'document';
|
|
279
|
+
else if (TerraQualityRegex.REGEX_PNG_JPE_G_GIF_WEBP_SVG_AVIF_I_REGEX_2().test(u.toLowerCase().split('?')[0] ?? ''))
|
|
280
|
+
aType = 'image';
|
|
281
|
+
}
|
|
282
|
+
const tmpId = `temp-${aType}-${Date.now()}-${i}`;
|
|
283
|
+
// prettier-ignore
|
|
284
|
+
setSisters(p => [...p, { id: tmpId, path: tmpId, name: 'Loading...', isMd: aType === 'website', title: aType === 'youtube' ? 'Extracting Transcript...' : aType === 'document' ? 'Downloading Document...' : aType === 'tsdocs' ? 'Linking Reference...' : 'Downloading...', type: aType, sourceUrl: u },]);
|
|
285
|
+
try {
|
|
286
|
+
let item = null;
|
|
287
|
+
if (aType === 'youtube') {
|
|
288
|
+
const { data, error } = await actions.write.attachment.youtube({ path: docId, url: u });
|
|
289
|
+
if (error || !data)
|
|
290
|
+
throw error;
|
|
291
|
+
const { title } = data;
|
|
292
|
+
item = { id: tmpId, path: u, name: `${title} Video`, isMd: false, title, type: 'youtube', sourceUrl: u };
|
|
293
|
+
}
|
|
294
|
+
else if (aType === 'website') {
|
|
295
|
+
const { data: webData, error: webErr } = await actions.read.external.website({ url: u });
|
|
296
|
+
if (webErr || !webData)
|
|
297
|
+
throw webErr;
|
|
298
|
+
const { title, text } = webData;
|
|
299
|
+
const dir = dirname(dPath);
|
|
300
|
+
const nPath = `${dir === '.' ? '' : `${dir}/`}${sanitizeName(title || 'website')}.mdx`;
|
|
301
|
+
const { data: docData, error: wErr } = await actions.write.doc.editor({ path: nPath, source: text, title, type: 'website', sourceUrl: u });
|
|
302
|
+
if (wErr || !docData)
|
|
303
|
+
throw wErr;
|
|
304
|
+
item = { id: tmpId, path: nPath, name: nPath.split('/').pop() || '', isMd: true, title, type: 'website', sourceUrl: u, source: text };
|
|
305
|
+
}
|
|
306
|
+
else if (aType === 'tsdocs') {
|
|
307
|
+
const fmtUrl = u.startsWith('http') ? u : u.startsWith('/') ? `${window.location.origin}${u}` : `${window.location.origin}${docsPath(`tsdocs/${u}`)}`;
|
|
308
|
+
const { data: linkData, error: linkErr } = await actions.write.attachment.link({
|
|
309
|
+
path: docId,
|
|
310
|
+
url: fmtUrl,
|
|
311
|
+
type: 'tsdocs',
|
|
312
|
+
title: (u.split('/').filter(Boolean).pop()?.replace(TerraQualityRegex.REGEX_G_REGEX_7(), ' ') || 'TsDocs Reference').replace(TerraQualityRegex.REGEX_B_W_G_REGEX(), c => c.toUpperCase()),
|
|
313
|
+
});
|
|
314
|
+
if (linkErr || !linkData)
|
|
315
|
+
throw linkErr;
|
|
316
|
+
const { title } = linkData;
|
|
317
|
+
item = { id: tmpId, path: fmtUrl, name: title, isMd: false, title, type: 'tsdocs', sourceUrl: fmtUrl };
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
const { data, error } = await actions.write.attachment.remoteFile({ path: docId, url: u, type: aType });
|
|
321
|
+
if (error || !data)
|
|
322
|
+
throw error;
|
|
323
|
+
const { title, id, path: assetPath, name } = data;
|
|
324
|
+
item = { id, path: assetPath, name, isMd: false, title, type: aType, sourceUrl: u };
|
|
325
|
+
}
|
|
326
|
+
setSisters(p => p.map(s => (s.id === tmpId ? item : s)));
|
|
327
|
+
}
|
|
328
|
+
catch (_err) {
|
|
329
|
+
setSisters(p => p.filter(s => s.id !== tmpId));
|
|
330
|
+
toast.error(`Failed to process ${u}`);
|
|
331
|
+
}
|
|
332
|
+
}));
|
|
333
|
+
setUploading(false);
|
|
334
|
+
clearTransition();
|
|
335
|
+
}, [docId]);
|
|
336
|
+
useEffect(() => {
|
|
337
|
+
/**
|
|
338
|
+
* Handles handle paste.
|
|
339
|
+
*
|
|
340
|
+
* @param e - Input event carrying the selected attachment files.
|
|
341
|
+
*/
|
|
342
|
+
const handlePaste = (e) => {
|
|
343
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
|
|
344
|
+
return;
|
|
345
|
+
const text = e.clipboardData?.getData('text');
|
|
346
|
+
if (text) {
|
|
347
|
+
const urls = text.split(TerraQualityRegex.REGEX_N_S_REGEX()).filter(u => u.startsWith('http') || isDocsPath(u) || u.startsWith('/tsdocs'));
|
|
348
|
+
if (urls.length > 0) {
|
|
349
|
+
e.preventDefault();
|
|
350
|
+
const ytUrls = urls.filter(u => u.includes('youtube.com') || u.includes('youtu.be'));
|
|
351
|
+
const refUrls = urls.filter(u => u.includes('/tsdocs/'));
|
|
352
|
+
const imgUrls = urls.filter(u => TerraQualityRegex.REGEX_PNG_JPE_G_GIF_WEBP_SVG_AVIF_I_REGEX_2().test(u.toLowerCase().split('?')[0] ?? ''));
|
|
353
|
+
const docUrls = [...urls.filter(u => u.toLowerCase().split('?')[0].endsWith('.pdf')), ...urls.filter(u => u.toLowerCase().split('?')[0].endsWith('.json'))];
|
|
354
|
+
const webUrls = urls.filter(u => !ytUrls.includes(u) && !refUrls.includes(u) && !docUrls.includes(u) && !imgUrls.includes(u));
|
|
355
|
+
if (ytUrls.length > 0)
|
|
356
|
+
void addRemote(ytUrls.join('\n'), 'youtube');
|
|
357
|
+
if (refUrls.length > 0)
|
|
358
|
+
void addRemote(refUrls.join('\n'), 'tsdocs');
|
|
359
|
+
if (docUrls.length > 0)
|
|
360
|
+
void addRemote(docUrls.join('\n'), 'document');
|
|
361
|
+
if (imgUrls.length > 0)
|
|
362
|
+
void addRemote(imgUrls.join('\n'), 'image');
|
|
363
|
+
if (webUrls.length > 0)
|
|
364
|
+
void addRemote(webUrls.join('\n'), 'website');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (e.clipboardData?.files?.length) {
|
|
368
|
+
e.preventDefault();
|
|
369
|
+
void add(e.clipboardData.files);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
document.addEventListener('paste', handlePaste);
|
|
373
|
+
return () => {
|
|
374
|
+
document.removeEventListener('paste', handlePaste);
|
|
375
|
+
};
|
|
376
|
+
}, [addRemote, add]);
|
|
377
|
+
/**
|
|
378
|
+
* Handles rename attch.
|
|
379
|
+
*
|
|
380
|
+
* @param nName - Proposed attachment name displayed in the editor.
|
|
381
|
+
*/
|
|
382
|
+
const renameAttch = async (nName) => {
|
|
383
|
+
if (!renameDlg)
|
|
384
|
+
return;
|
|
385
|
+
try {
|
|
386
|
+
const { path, name, isMd, id } = renameDlg;
|
|
387
|
+
const nFilename = `${sanitizeName(nName)}${name.match(TerraQualityRegex.REGEX_I_REGEX())?.[0] ?? ''}`;
|
|
388
|
+
const nPath = `${dirname(path) === '.' ? '' : `${dirname(path)}/`}${nFilename}`;
|
|
389
|
+
setSisters(p => p.map(s => (s.path === path ? { ...s, path: nPath, name: nFilename } : s)));
|
|
390
|
+
if (isMd) {
|
|
391
|
+
const { error: err } = await actions.write.doc.commit({ cur: path, next: nPath });
|
|
392
|
+
if (err)
|
|
393
|
+
throw err;
|
|
394
|
+
}
|
|
395
|
+
else if (id)
|
|
396
|
+
await actions.write.attachment.rename({ docPath: docId, attachmentKey: id, newName: nFilename });
|
|
397
|
+
toast.success('Renamed successfully');
|
|
398
|
+
}
|
|
399
|
+
catch (_err) {
|
|
400
|
+
errorLog('Attachments:Rename', 'Rename failed', logError(_err));
|
|
401
|
+
setSisters(p => p.map(s => (s.path === renameDlg.path ? { ...s, path: renameDlg.path, name: renameDlg.name } : s)));
|
|
402
|
+
toast.error('Failed to rename attachment');
|
|
403
|
+
}
|
|
404
|
+
finally {
|
|
405
|
+
setRenameDlg(null);
|
|
406
|
+
setRenameInput('');
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
/**
|
|
410
|
+
* Handles remove.
|
|
411
|
+
*
|
|
412
|
+
* @param items - Attachment items rendered by the list.
|
|
413
|
+
*/
|
|
414
|
+
const remove = async (items) => {
|
|
415
|
+
if (!window.confirm(`Are you sure you want to delete ${items.length} items?`))
|
|
416
|
+
return;
|
|
417
|
+
const itemKeys = new Set(items.flatMap(i => [i.id, i.path].filter(Boolean)));
|
|
418
|
+
setSisters(p => p.filter(s => !(s.id && itemKeys.has(s.id)) && !(s.path && itemKeys.has(s.path))));
|
|
419
|
+
try {
|
|
420
|
+
await Promise.allSettled(items.map(async (i) => {
|
|
421
|
+
if (i.isMd)
|
|
422
|
+
await actions.write.doc.rm({ assets: false, path: i.path, type: 'doc' });
|
|
423
|
+
else if (i.id) {
|
|
424
|
+
await actions.write.attachment.rm({ docPath: docId, attachmentKey: i.id });
|
|
425
|
+
if (i.path.startsWith('http'))
|
|
426
|
+
await actions.write.asset.delete({ url: i.path });
|
|
427
|
+
}
|
|
428
|
+
}));
|
|
429
|
+
setSel([]);
|
|
430
|
+
}
|
|
431
|
+
catch (_err) {
|
|
432
|
+
setSisters([...sisters]);
|
|
433
|
+
notify({ error: _err instanceof Error ? _err : new Error('Deletion failed'), fallback: 'Failed to delete some attachments.' });
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
const isDialogOpen = !!mergePdfState || !!viewDlg || (renameDlg?.open ?? false) || (transferDlg?.open ?? false) || addDialogOpen;
|
|
437
|
+
// GLOBAL KEYBOARD SHORTCUTS
|
|
438
|
+
useEffect(() => {
|
|
439
|
+
/**
|
|
440
|
+
* Handles handle key down.
|
|
441
|
+
*
|
|
442
|
+
* @param e - Keyboard event used to operate the attachment panel.
|
|
443
|
+
*/
|
|
444
|
+
const handleKeyDown = (e) => {
|
|
445
|
+
if (loading || uploading || isDialogOpen || selItems.length === 0)
|
|
446
|
+
return;
|
|
447
|
+
const tgt = e.target;
|
|
448
|
+
if (tgt.tagName === 'INPUT' || tgt.tagName === 'TEXTAREA' || tgt.isContentEditable)
|
|
449
|
+
return;
|
|
450
|
+
if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
451
|
+
e.preventDefault();
|
|
452
|
+
void remove(selItems);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
456
|
+
return () => {
|
|
457
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
458
|
+
};
|
|
459
|
+
}, [loading, uploading, isDialogOpen, selItems]);
|
|
460
|
+
useEffect(() => {
|
|
461
|
+
if (!ctxAttachment)
|
|
462
|
+
return;
|
|
463
|
+
/**
|
|
464
|
+
* Closes the attachment context menu when the user presses Escape.
|
|
465
|
+
*
|
|
466
|
+
* @param e - Keyboard event checked for the Escape key.
|
|
467
|
+
*/
|
|
468
|
+
const close = (e) => {
|
|
469
|
+
if (e.key === 'Escape')
|
|
470
|
+
setCtxAttachment(null);
|
|
471
|
+
};
|
|
472
|
+
window.addEventListener('keydown', close);
|
|
473
|
+
return () => window.removeEventListener('keydown', close);
|
|
474
|
+
}, [ctxAttachment]);
|
|
475
|
+
useEffect(() => {
|
|
476
|
+
/**
|
|
477
|
+
* Opens the attachment context menu for the item under the pointer.
|
|
478
|
+
*
|
|
479
|
+
* @param e - Mouse event used to identify the attachment item and menu position.
|
|
480
|
+
*/
|
|
481
|
+
const open = (e) => {
|
|
482
|
+
const id = (e.target instanceof Element ? e.target.closest('[data-docs-attachment-id]') : null)?.dataset.docsAttachmentId;
|
|
483
|
+
const item = id ? sistersMap.get(id) : undefined;
|
|
484
|
+
if (!(e.target instanceof Element ? e.target.closest('[data-docs-attachments]') : null))
|
|
485
|
+
return;
|
|
486
|
+
e.preventDefault();
|
|
487
|
+
if (item)
|
|
488
|
+
setCtxAttachment({ item, x: e.clientX, y: e.clientY });
|
|
489
|
+
else
|
|
490
|
+
setAddDialogOpen(true);
|
|
491
|
+
};
|
|
492
|
+
document.addEventListener('contextmenu', open, true);
|
|
493
|
+
return () => document.removeEventListener('contextmenu', open, true);
|
|
494
|
+
}, [sistersMap]);
|
|
495
|
+
// RENDER
|
|
496
|
+
return (_jsxs(ErrorBoundary, { name: 'Attachments', children: [_jsxs("div", { "data-docs-attachments": true, "data-docs-path": dPath, "data-loading": loading, className: cn('-mx-6 -my-40 flex min-h-[calc(100vh+8rem)] flex-col rounded-xl border-2 px-6 pb-40 transition-colors md:-mx-20 md:px-20', isDragging ? 'border-primary-8 bg-primary-4/20' : 'border-transparent'), onDragOver: e => {
|
|
497
|
+
e.preventDefault();
|
|
498
|
+
e.stopPropagation();
|
|
499
|
+
setIsDragging(true);
|
|
500
|
+
}, onDragEnter: e => {
|
|
501
|
+
e.preventDefault();
|
|
502
|
+
e.stopPropagation();
|
|
503
|
+
setIsDragging(true);
|
|
504
|
+
}, onDragLeave: e => {
|
|
505
|
+
e.preventDefault();
|
|
506
|
+
e.stopPropagation();
|
|
507
|
+
setIsDragging(false);
|
|
508
|
+
}, onDrop: e => {
|
|
509
|
+
e.preventDefault();
|
|
510
|
+
e.stopPropagation();
|
|
511
|
+
setIsDragging(false);
|
|
512
|
+
if (e.dataTransfer.files?.length)
|
|
513
|
+
void add(e.dataTransfer.files);
|
|
514
|
+
}, onContextMenu: e => {
|
|
515
|
+
if (e.target === e.currentTarget) {
|
|
516
|
+
e.preventDefault();
|
|
517
|
+
setAddDialogOpen(true);
|
|
518
|
+
}
|
|
519
|
+
}, onClick: e => {
|
|
520
|
+
if (e.target === e.currentTarget)
|
|
521
|
+
setSel([]);
|
|
522
|
+
}, children: [_jsx("div", { className: 'flex flex-col gap-8 py-8', children: loading ? null : Object.keys(gSisters).length === 0 ? (_jsx(Empty, { children: "No attachments found." })) : (ATTCH_TYPES.filter(c => gSisters[c]).map(c => (_jsxs("div", { className: 'space-y-4', children: [_jsx("h3", { className: 'h3 capitalize', children: c }), _jsx("div", { className: 'grid grid-cols-1 content-start gap-4 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-3', children: gSisters[c].map(v => {
|
|
523
|
+
const isTemp = v.path.startsWith('temp-');
|
|
524
|
+
return (_jsx(Card, { "data-docs-attachment-id": v.id || v.path, role: 'button', tabIndex: isTemp ? -1 : 0, "aria-disabled": isTemp ? true : undefined, "aria-selected": sel.includes(v.path), "aria-label": `Attachment ${v.title || v.name}`, className: cn('cursor-pointer transition-colors hover:border-primary-8 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-8', sel.includes(v.path) && 'bg-primary-4/50 ring-2 ring-primary-8', isTemp && 'pointer-events-none opacity-50'), onKeyDown: e => {
|
|
525
|
+
if (isTemp)
|
|
526
|
+
return;
|
|
527
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
528
|
+
e.preventDefault();
|
|
529
|
+
if (v.type === 'youtube')
|
|
530
|
+
setViewDlg({ ...v, isMd: true, content: latestContent(v.content) || 'No transcript text found.' });
|
|
531
|
+
else
|
|
532
|
+
void viewAsset(v);
|
|
533
|
+
}
|
|
534
|
+
}, onClick: e => {
|
|
535
|
+
if (isTemp)
|
|
536
|
+
return;
|
|
537
|
+
e.stopPropagation();
|
|
538
|
+
if (e.shiftKey && sel.length > 0) {
|
|
539
|
+
const i1 = sisters.findIndex(s => s.path === sel[sel.length - 1]);
|
|
540
|
+
const i2 = sisters.findIndex(s => s.path === v.path);
|
|
541
|
+
setSel(p => Array.from(new Set([...(e.metaKey || e.ctrlKey ? p : []), ...sisters.slice(Math.min(i1, i2), Math.max(i1, i2) + 1).map(s => s.path)])));
|
|
542
|
+
}
|
|
543
|
+
else if (e.metaKey || e.ctrlKey) {
|
|
544
|
+
setSel(p => (p.includes(v.path) ? p.filter(x => x !== v.path) : [...p, v.path]));
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
if (v.type === 'youtube')
|
|
548
|
+
setViewDlg({ ...v, isMd: true, content: latestContent(v.content) || 'No transcript text found.' });
|
|
549
|
+
else
|
|
550
|
+
void viewAsset(v);
|
|
551
|
+
}
|
|
552
|
+
}, onContextMenu: e => {
|
|
553
|
+
e.preventDefault();
|
|
554
|
+
setCtxAttachment({ item: v, x: e.clientX, y: e.clientY });
|
|
555
|
+
}, children: _jsxs(CardHeader, { children: [__terraQualityCreateElement(
|
|
556
|
+
/* prettier-ignore */ v.type === 'youtube' ? YouTubeIcon : v.type === 'website' ? LinkIcon : v.type === 'image' ? ImageIcon : v.type === 'tsdocs' ? CatalogIcon : v.type === 'other' ? BoxIcon : DocumentIcon, { className: 'size-6 text-primary-10', 'aria-hidden': 'true' }), _jsx(CardTitle, { className: 'truncate text-base', children: v.title || v.name }), _jsx(CardDescription, { className: 'truncate', children: v.sourceUrl || v.path })] }) }, v.id || v.path));
|
|
557
|
+
}) })] }, c)))) }), _jsx("input", { "data-testid": 'docs-attachment-upload', ref: fileRef, type: 'file', className: 'hidden', multiple: true, onChange: e => void add(e.currentTarget.files), disabled: uploading || loading })] }), ctxAttachment && (_jsx("div", { role: 'menu', "data-docs-attachment-menu": true, className: 'fixed z-[120] min-w-32 rounded-md border border-primary-6 bg-white p-1 text-primary-12 shadow-lg dark:bg-black', style: { left: ctxAttachment.x, top: ctxAttachment.y }, onContextMenu: e => e.preventDefault(), children: (sel.includes(ctxAttachment.item.path) && sel.length > 1
|
|
558
|
+
? [
|
|
559
|
+
{ icon: _jsx(CopyIcon, {}), label: `Copy ${sel.length} items`, onSelect: () => setTransferDlg({ open: true, action: 'copy', items: selItems }) },
|
|
560
|
+
{ icon: _jsx(PopOutIcon, {}), label: `Move ${sel.length} items`, onSelect: () => setTransferDlg({ open: true, action: 'cut', items: selItems }) },
|
|
561
|
+
...(selItems.every(s => s.path.toLowerCase().split('?')[0].endsWith('.pdf') || s.name.toLowerCase().endsWith('.pdf'))
|
|
562
|
+
? [
|
|
563
|
+
{
|
|
564
|
+
icon: _jsx(DocumentIcon, {}),
|
|
565
|
+
label: `Merge ${sel.length} PDFs`,
|
|
566
|
+
onSelect: () => {
|
|
567
|
+
void (async () => {
|
|
568
|
+
if (!window.confirm(`Merge ${selItems.length} PDFs into a single file?`))
|
|
569
|
+
return;
|
|
570
|
+
setUploading(true);
|
|
571
|
+
const tmpId = `temp-merge-${Date.now()}`;
|
|
572
|
+
const itemKeys = new Set(selItems.flatMap(i => [i.id, i.path].filter(Boolean)));
|
|
573
|
+
setSisters(p => [
|
|
574
|
+
...p.filter(s => !(s.id && itemKeys.has(s.id)) && !(s.path && itemKeys.has(s.path))),
|
|
575
|
+
{ id: tmpId, path: tmpId, name: 'Merging PDFs...', isMd: false, type: 'document' },
|
|
576
|
+
]);
|
|
577
|
+
setSel([]);
|
|
578
|
+
try {
|
|
579
|
+
const { PDFDocument } = await import('pdf-lib');
|
|
580
|
+
const mergedPdf = await PDFDocument.create();
|
|
581
|
+
for (let i = 0; i < selItems.length; i++) {
|
|
582
|
+
const item = selItems[i];
|
|
583
|
+
let buf;
|
|
584
|
+
if (item?.path.startsWith('http')) {
|
|
585
|
+
buf = await (await fetch(item.path)).arrayBuffer();
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
const { data, error: err } = await actions.read.asset.get({ path: item?.path || '' });
|
|
589
|
+
if (err || !data)
|
|
590
|
+
throw err || new Error('Failed to read local asset');
|
|
591
|
+
buf = data;
|
|
592
|
+
}
|
|
593
|
+
const pdfDoc = await PDFDocument.load(buf);
|
|
594
|
+
(await mergedPdf.copyPages(pdfDoc, pdfDoc.getPageIndices())).forEach(p => mergedPdf.addPage(p));
|
|
595
|
+
}
|
|
596
|
+
const mergedFile = new File([(await mergedPdf.save()).buffer], 'Merged-Document.pdf', { type: 'application/pdf' });
|
|
597
|
+
setSisters(p => p.map(s => (s.id === tmpId ? { ...s, name: 'Uploading Merged PDF...' } : s)));
|
|
598
|
+
const finalAsset = await uploadAssetChunked(mergedFile, docId, {
|
|
599
|
+
onProgress: ({ currentChunk: currChunk, totalChunks, message: msg }) => {
|
|
600
|
+
if (msg)
|
|
601
|
+
setSisters(p => p.map(s => (s.id === tmpId ? { ...s, name: msg } : s)));
|
|
602
|
+
else if (totalChunks > 1)
|
|
603
|
+
setSisters(p => p.map(s => (s.id === tmpId ? { ...s, name: `Uploading (${currChunk}/${totalChunks})...` } : s)));
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
const fPath = finalAsset.path || finalAsset.src || tmpId;
|
|
607
|
+
setSisters(p => p.map(s => (s.id === tmpId ? { ...s, path: fPath, name: fPath.split('/').pop() || mergedFile.name } : s)));
|
|
608
|
+
await Promise.allSettled(selItems.map(async (i) => {
|
|
609
|
+
if (i.isMd)
|
|
610
|
+
await actions.write.doc.rm({ assets: false, path: i.path, type: 'doc' });
|
|
611
|
+
else if (i.id) {
|
|
612
|
+
await actions.write.attachment.rm({ docPath: docId, attachmentKey: i.id });
|
|
613
|
+
if (i.path.startsWith('http'))
|
|
614
|
+
await actions.write.asset.delete({ url: i.path });
|
|
615
|
+
}
|
|
616
|
+
}));
|
|
617
|
+
toast.success('PDFs merged successfully.');
|
|
618
|
+
}
|
|
619
|
+
catch (_err) {
|
|
620
|
+
errorLog('Attachments:Merge', 'Fatal exception caught during PDF merge', logError(_err));
|
|
621
|
+
toast.error('Failed to merge PDFs.');
|
|
622
|
+
setSisters([...sisters]);
|
|
623
|
+
}
|
|
624
|
+
finally {
|
|
625
|
+
setUploading(false);
|
|
626
|
+
}
|
|
627
|
+
})();
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
]
|
|
631
|
+
: []),
|
|
632
|
+
{ separator: true },
|
|
633
|
+
{ destructive: true, icon: _jsx(TrashIcon, {}), label: `Delete ${sel.length} items`, onSelect: () => void remove(selItems) },
|
|
634
|
+
]
|
|
635
|
+
: [
|
|
636
|
+
...(!ctxAttachment.item.type || ctxAttachment.item.type !== 'youtube'
|
|
637
|
+
? [
|
|
638
|
+
{
|
|
639
|
+
onSelect: () => {
|
|
640
|
+
setRenameInput(ctxAttachment.item.name.replace(TerraQualityRegex.REGEX_I_REGEX(), ''));
|
|
641
|
+
setRenameDlg({ open: true, path: ctxAttachment.item.path, name: ctxAttachment.item.name, isMd: ctxAttachment.item.isMd || false, id: ctxAttachment.item.id });
|
|
642
|
+
},
|
|
643
|
+
icon: _jsx(EditIcon, {}),
|
|
644
|
+
label: 'Rename',
|
|
645
|
+
},
|
|
646
|
+
]
|
|
647
|
+
: []),
|
|
648
|
+
{ icon: _jsx(CopyIcon, {}), label: 'Copy to...', onSelect: () => setTransferDlg({ open: true, action: 'copy', items: [ctxAttachment.item] }) },
|
|
649
|
+
{ icon: _jsx(PopOutIcon, {}), label: 'Move to...', onSelect: () => setTransferDlg({ open: true, action: 'cut', items: [ctxAttachment.item] }) },
|
|
650
|
+
...(ctxAttachment.item.isMd
|
|
651
|
+
? [
|
|
652
|
+
{
|
|
653
|
+
onSelect: () => document.dispatchEvent(new CustomEvent('docs:synthesize', { detail: { path: ctxAttachment.item.path, name: ctxAttachment.item.name } })),
|
|
654
|
+
icon: _jsx(MagicIcon, {}),
|
|
655
|
+
label: 'Synthesize Content',
|
|
656
|
+
},
|
|
657
|
+
...(!ctxAttachment.item.type || ctxAttachment.item.type !== 'youtube'
|
|
658
|
+
? [
|
|
659
|
+
{
|
|
660
|
+
icon: _jsx(CopyIcon, {}),
|
|
661
|
+
label: 'Duplicate',
|
|
662
|
+
onSelect: () => {
|
|
663
|
+
void (async () => {
|
|
664
|
+
const path = ctxAttachment.item.path;
|
|
665
|
+
if (!path)
|
|
666
|
+
return;
|
|
667
|
+
const name = window.prompt('Duplicate file name', `${path.split('/').at(-1)?.replace(TerraQualityRegex.REGEX_MD_MDX_I_REGEX(), '') ?? ''}-copy`);
|
|
668
|
+
if (!name)
|
|
669
|
+
return;
|
|
670
|
+
try {
|
|
671
|
+
const { data: docData, error: readErr } = await actions.read.doc.get({ path });
|
|
672
|
+
if (readErr || !docData)
|
|
673
|
+
throw readErr || new Error('Failed to read doc.');
|
|
674
|
+
const nPath = `${dirname(path)}/${sanitizeName(name)}.mdx`;
|
|
675
|
+
const { data, error: writeErr } = await actions.write.doc.save({ path: nPath, source: docData.source });
|
|
676
|
+
if (writeErr || !data)
|
|
677
|
+
throw writeErr || new Error('Failed to duplicate.');
|
|
678
|
+
const duplicateData = data;
|
|
679
|
+
setSisters(p => [
|
|
680
|
+
...p,
|
|
681
|
+
{
|
|
682
|
+
path: nPath,
|
|
683
|
+
name: nPath.split('/').pop() || '',
|
|
684
|
+
isMd: true,
|
|
685
|
+
title: duplicateData.title,
|
|
686
|
+
source: duplicateData.source,
|
|
687
|
+
type: duplicateData.type,
|
|
688
|
+
sourceUrl: duplicateData.sourceUrl,
|
|
689
|
+
},
|
|
690
|
+
]);
|
|
691
|
+
}
|
|
692
|
+
catch (_err) {
|
|
693
|
+
errorLog('Attachments:Duplicate', 'Duplicate failed', logError(_err));
|
|
694
|
+
notify({ error: _err instanceof Error ? _err : new Error('Failed to duplicate.'), fallback: 'Failed to duplicate.' });
|
|
695
|
+
}
|
|
696
|
+
})();
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
]
|
|
700
|
+
: []),
|
|
701
|
+
]
|
|
702
|
+
: []),
|
|
703
|
+
{ separator: true },
|
|
704
|
+
{ destructive: true, onSelect: () => void remove([ctxAttachment.item]), icon: _jsx(TrashIcon, {}), label: 'Delete' },
|
|
705
|
+
]).map((a, i) => a.separator ? (_jsx("div", { role: 'separator', className: 'mx-1.5 my-1 h-px bg-primary-6/50' }, `separator-${i}`)) : (_jsx("button", { type: 'button', role: 'menuitem', className: 'relative flex w-full cursor-default select-none items-center rounded-sm px-3 py-1 text-left text-sm hover:bg-primary-4', disabled: a.disabled, onClick: () => {
|
|
706
|
+
setCtxAttachment(null);
|
|
707
|
+
a.onSelect?.();
|
|
708
|
+
}, children: a.label }, `${a.label}-${i}`))) })), _jsxs(Dialog, { open: isDialogOpen, onOpenChange: open => {
|
|
709
|
+
if (open)
|
|
710
|
+
return;
|
|
711
|
+
if (viewDlg)
|
|
712
|
+
setViewDlg(null);
|
|
713
|
+
if (renameDlg?.open) {
|
|
714
|
+
setRenameDlg(null);
|
|
715
|
+
setRenameInput('');
|
|
716
|
+
}
|
|
717
|
+
if (transferDlg?.open)
|
|
718
|
+
setTransferDlg(null);
|
|
719
|
+
if (mergePdfState)
|
|
720
|
+
setMergePdfState(null);
|
|
721
|
+
if (addDialogOpen) {
|
|
722
|
+
setAddDialogOpen(false);
|
|
723
|
+
setTimeout(() => {
|
|
724
|
+
setDialogMode('select');
|
|
725
|
+
setUrlInput('');
|
|
726
|
+
}, 200);
|
|
727
|
+
}
|
|
728
|
+
}, children: [!!mergePdfState && (_jsxs(DialogContent, { className: 'sm:max-w-md', children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Multiple PDFs Selected" }), _jsxs(DialogDescription, { children: ["Would you like to merge these ", mergePdfState.pdfs.length, " PDFs into a single file, or upload them separately?"] })] }), _jsxs(DialogFooter, { className: 'flex gap-2 sm:justify-between', children: [_jsx(Button, { variant: 'ghost', onClick: () => {
|
|
729
|
+
[...mergePdfState.pdfs, ...mergePdfState.others].forEach((f, i) => {
|
|
730
|
+
const isMdFile = TerraQualityRegex.REGEX_MD_MDX_I_REGEX().test(f.name);
|
|
731
|
+
const tmpId = `temp-${Date.now()}-${i}`;
|
|
732
|
+
setSisters(p => [...p, { id: tmpId, path: tmpId, name: f.name, isMd: isMdFile, type: 'document' }]);
|
|
733
|
+
void processUpload(f, tmpId, isMdFile);
|
|
734
|
+
});
|
|
735
|
+
setMergePdfState(null);
|
|
736
|
+
}, children: "Upload Separately" }), _jsx(Button, { disabled: uploading, loading: uploading, onClick: async () => {
|
|
737
|
+
setUploading(true);
|
|
738
|
+
setMergePdfState(null);
|
|
739
|
+
try {
|
|
740
|
+
const { PDFDocument } = await import('pdf-lib');
|
|
741
|
+
const mergedPdf = await PDFDocument.create();
|
|
742
|
+
for (let i = 0; i < mergePdfState.pdfs.length; i++) {
|
|
743
|
+
const pdfDoc = await PDFDocument.load(await mergePdfState.pdfs[i].arrayBuffer());
|
|
744
|
+
(await mergedPdf.copyPages(pdfDoc, pdfDoc.getPageIndices())).forEach(p => mergedPdf.addPage(p));
|
|
745
|
+
}
|
|
746
|
+
[new File([(await mergedPdf.save()).buffer], 'Merged-Document.pdf', { type: 'application/pdf' }), ...mergePdfState.others].forEach((f, i) => {
|
|
747
|
+
const isMdFile = TerraQualityRegex.REGEX_MD_MDX_I_REGEX().test(f.name);
|
|
748
|
+
const tmpId = `temp-${Date.now()}-${i}`;
|
|
749
|
+
setSisters(p => [...p, { id: tmpId, path: tmpId, name: f.name, isMd: isMdFile, type: 'document' }]);
|
|
750
|
+
void processUpload(f, tmpId, isMdFile);
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
catch (_err) {
|
|
754
|
+
errorLog('Attachments:UploadMerge', 'Fatal exception caught during PDF merge', logError(_err));
|
|
755
|
+
toast.error('Failed to merge PDFs.');
|
|
756
|
+
}
|
|
757
|
+
finally {
|
|
758
|
+
setUploading(false);
|
|
759
|
+
}
|
|
760
|
+
}, children: "Merge to Single PDF" })] })] })), !!viewDlg && (_jsxs(DialogContent, { className: 'flex max-h-[85vh] flex-col sm:max-w-4xl', children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: viewDlg.title || viewDlg.name }), _jsx(DialogDescription, { className: 'truncate', children: viewDlg.sourceUrl || viewDlg.path })] }), _jsx("div", { className: 'min-h-0 flex-1 overflow-y-auto py-4', children: viewDlg.isMd ? (_jsx(CodeBlock, { docsSource: true, code: typeof viewDlg.content === 'string' ? viewDlg.content : latestContent(viewDlg.content) || '', language: 'mdx', tone: 'view', className: 'my-0 text-xs' })) : textAsset(viewDlg) ? (_jsx(CodeBlock, { code: typeof viewDlg.content === 'string' ? viewDlg.content : '', language: 'text', tone: 'view', className: 'my-0 text-xs' })) : (_jsxs("div", { className: 'flex flex-col items-center justify-center gap-4 py-12 text-primary-11', children: [_jsx(DocumentIcon, { className: 'size-16 opacity-50', "aria-hidden": 'true' }), _jsx("p", { children: "Binary storage entry." }), viewDlg.unavailable ? (_jsx("p", { children: "Source asset is unavailable." })) : (_jsxs(Button, { onClick: () => {
|
|
761
|
+
void (async () => {
|
|
762
|
+
if (!viewDlg)
|
|
763
|
+
return;
|
|
764
|
+
try {
|
|
765
|
+
if (viewDlg.type === 'tsdocs') {
|
|
766
|
+
const scopeMatch = viewDlg.path.match(TerraQualityRegex.REGEX_DOCS_TSDOCS_REGEX());
|
|
767
|
+
const scope = scopeMatch ? scopeMatch[1] : viewDlg.path;
|
|
768
|
+
const { data, error } = await actions.read.tsdocs({ scope });
|
|
769
|
+
if (!error && data?.docs) {
|
|
770
|
+
const url = URL.createObjectURL(new Blob([
|
|
771
|
+
JSON.stringify((Array.isArray(data.docs) ? data.docs : []).filter(isRec).map(d => ({ title: d.title, slug: d.slug, source: d.body })), null, 2),
|
|
772
|
+
], { type: 'application/json' }));
|
|
773
|
+
const a = document.createElement('a');
|
|
774
|
+
a.href = url;
|
|
775
|
+
a.download = `${sanitizeName(scope.replace(TerraQualityRegex.REGEX_G_REGEX_10(), '-'))}.json`;
|
|
776
|
+
document.body.appendChild(a);
|
|
777
|
+
a.click();
|
|
778
|
+
document.body.removeChild(a);
|
|
779
|
+
URL.revokeObjectURL(url);
|
|
780
|
+
}
|
|
781
|
+
else
|
|
782
|
+
toast.error('Failed to download TSDocs reference.');
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (viewDlg.type === 'youtube' || viewDlg.type === 'website') {
|
|
786
|
+
window.open(viewDlg.sourceUrl || viewDlg.path, '_blank');
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
let dlName = viewDlg.title || viewDlg.name;
|
|
790
|
+
const extMatch = viewDlg.path.split('?')[0]?.match(TerraQualityRegex.REGEX_A_Z0_9_I_REGEX());
|
|
791
|
+
if (extMatch && !dlName.toLowerCase().endsWith(extMatch[0].toLowerCase()))
|
|
792
|
+
dlName = `${dlName}${extMatch[0]}`;
|
|
793
|
+
else if (viewDlg.type === 'document' && !dlName.toLowerCase().endsWith('.pdf'))
|
|
794
|
+
dlName = `${dlName}.pdf`;
|
|
795
|
+
const { data, error } = await actions.read.asset.get({ path: viewDlg.path });
|
|
796
|
+
if (error || !data)
|
|
797
|
+
throw error;
|
|
798
|
+
const url = URL.createObjectURL(new Blob([data], { type: viewDlg.type === 'document' ? 'application/pdf' : 'application/octet-stream' }));
|
|
799
|
+
const a = document.createElement('a');
|
|
800
|
+
a.href = url;
|
|
801
|
+
a.download = dlName;
|
|
802
|
+
document.body.appendChild(a);
|
|
803
|
+
a.click();
|
|
804
|
+
document.body.removeChild(a);
|
|
805
|
+
URL.revokeObjectURL(url);
|
|
806
|
+
}
|
|
807
|
+
catch (_err) {
|
|
808
|
+
toast.error('Failed to download asset.');
|
|
809
|
+
}
|
|
810
|
+
})();
|
|
811
|
+
}, children: [_jsx(DownloadIcon, { className: 'mr-2 size-4' }), " Download Asset"] }))] })) }), _jsx(DialogFooter, { children: _jsx(Button, { variant: 'ghost', onClick: () => {
|
|
812
|
+
setViewDlg(null);
|
|
813
|
+
}, children: "Close" }) })] })), renameDlg?.open && (_jsxs(DialogContent, { className: 'sm:max-w-md', children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Rename Attachment" }), _jsx(DialogDescription, { children: "Assign folder-compliant file identity token string parameters." })] }), _jsx("div", { className: 'py-4', children: _jsx(Input, { autoFocus: true, value: renameInput, onChange: e => {
|
|
814
|
+
setRenameInput(e.target.value);
|
|
815
|
+
}, onKeyDown: e => e.key === 'Enter' && void renameAttch(renameInput) }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: 'ghost', onClick: () => { setRenameDlg(null); }, children: "Cancel" }), _jsx(Button, { disabled: !renameInput.trim(), onClick: () => void renameAttch(renameInput), children: "Rename" })] })] })), transferDlg?.open &&
|
|
816
|
+
// prettier-ignore
|
|
817
|
+
_jsxs(DialogContent, { className: 'sm:max-w-md', children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: [transferDlg.action === 'copy' ? 'Copy' : 'Move', " Asset", transferDlg.items.length > 1 ? 's' : ''] }), _jsx(DialogDescription, { children: "Shift structural components between system documents." })] }), _jsx("div", { className: 'py-4', children: _jsxs(Select, { value: transferTgt, onValueChange: setTransferTgt, children: [_jsx(SelectTrigger, { "aria-label": 'Select destination page', children: _jsx(SelectValue, { placeholder: 'Select destination page' }) }), _jsxs(SelectContent, { className: 'max-h-60', children: [_jsx(SelectItem, { value: '__new__', className: 'font-semibold text-primary-11', children: "\u2728 Move to new page..." }), localLeaves.length ? (localLeaves.map(l => (_jsx(SelectItem, { value: l.path, children: l.title || l.origName }, l.path)))) : (_jsx(SelectItem, { value: 'none', disabled: true, children: "No target layers found" }))] })] }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: 'ghost', onClick: () => { setTransferDlg(null); }, children: "Cancel" }), _jsx(Button, { disabled: !transferTgt || transferTgt === 'none' || transferring, loading: transferring, onClick: () => {
|
|
818
|
+
void (async () => {
|
|
819
|
+
if (!transferDlg || !transferTgt || transferTgt === 'none' || transferDlg.items.length === 0)
|
|
820
|
+
return;
|
|
821
|
+
setTransferring(true);
|
|
822
|
+
try {
|
|
823
|
+
let destPath = transferTgt;
|
|
824
|
+
if (transferTgt === '__new__') {
|
|
825
|
+
const title = window.prompt('Enter a title for the new page:');
|
|
826
|
+
if (!title) {
|
|
827
|
+
setTransferring(false);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const dir = dirname(dPath);
|
|
831
|
+
const nPath = `${dir === '.' ? '' : `${dir}/`}${sanitizeName(title)}.mdx`;
|
|
832
|
+
const { data: nDoc, error: createErr } = await actions.write.doc.editor({ path: nPath, title, source: '' });
|
|
833
|
+
if (createErr || !nDoc)
|
|
834
|
+
throw createErr || new Error('Failed to create new page.');
|
|
835
|
+
destPath = nPath;
|
|
836
|
+
}
|
|
837
|
+
if (transferDlg.action === 'copy') {
|
|
838
|
+
await Promise.allSettled(transferDlg.items.map(async (i) => {
|
|
839
|
+
if (i.isMd) {
|
|
840
|
+
const { data: docData } = await actions.read.doc.get({ path: i.path });
|
|
841
|
+
await actions.write.doc.save({
|
|
842
|
+
path: `${dirname(destPath)}/${`${i.name.replace(TerraQualityRegex.REGEX_I_REGEX(), '')}-copy-${Math.floor(Math.random() * 10000)}${i.name.match(TerraQualityRegex.REGEX_I_REGEX())?.[0] ?? ''}`}`,
|
|
843
|
+
source: docData.source,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
else if (i.id) {
|
|
847
|
+
await actions.write.attachment.move({ sourceDocPath: docId, targetDocPath: destPath, attachmentKey: i.id });
|
|
848
|
+
await actions.write.attachment.add({ files: [{ name: i.name }], path: destPath });
|
|
849
|
+
}
|
|
850
|
+
}));
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
await Promise.allSettled(transferDlg.items.map(async (i) => {
|
|
854
|
+
if (i.isMd) {
|
|
855
|
+
const { data: docData } = await actions.read.doc.get({ path: i.path });
|
|
856
|
+
await actions.write.doc.commit({ cur: i.path, next: `${dirname(destPath)}/${i.name}`, source: docData.source });
|
|
857
|
+
}
|
|
858
|
+
else if (i.id)
|
|
859
|
+
await actions.write.attachment.move({ sourceDocPath: docId, targetDocPath: destPath, attachmentKey: i.id });
|
|
860
|
+
}));
|
|
861
|
+
const itemKeys = new Set(transferDlg.items.flatMap(i => [i.id, i.path].filter(Boolean)));
|
|
862
|
+
setSisters(p => p.filter(s => !(s.id && itemKeys.has(s.id)) && !(s.path && itemKeys.has(s.path))));
|
|
863
|
+
}
|
|
864
|
+
setSel([]);
|
|
865
|
+
toast.success(`Successfully ${transferDlg.action === 'copy' ? 'copied' : 'moved'} attachments.`);
|
|
866
|
+
}
|
|
867
|
+
catch (_err) {
|
|
868
|
+
errorLog('Attachments:Transfer', 'Transfer failed', logError(_err));
|
|
869
|
+
toast.error(`Failed to ${transferDlg.action} attachments.`);
|
|
870
|
+
}
|
|
871
|
+
finally {
|
|
872
|
+
setTransferring(false);
|
|
873
|
+
setTransferDlg(null);
|
|
874
|
+
}
|
|
875
|
+
})();
|
|
876
|
+
}, children: transferDlg.action === 'copy' ? 'Copy' : 'Move' })] })] }), addDialogOpen && (_jsxs(DialogContent, { className: 'flex max-h-[85vh] flex-col overflow-hidden sm:max-w-xl', children: [_jsx(DialogHeader, { children: dialogMode === 'select' ? (_jsxs(_Fragment, { children: [_jsx(DialogTitle, { children: "Add Attachment" }), _jsx(DialogDescription, { children: "Select attachment category." })] })) : (_jsxs(_Fragment, { children: [_jsxs(DialogTitle, { className: 'capitalize', children: ["Add ", dialogMode] }), _jsx(DialogDescription, { children: dialogMode === 'tsdocs' ? 'Select an API reference page or folder to link.' : ['youtube', 'website'].includes(dialogMode) ? `Provide URLs to attach ${dialogMode} content.` : `Upload files or provide URLs to attach remote ${dialogMode}s.` })] })) }), _jsx("div", { className: 'flex-1 overflow-y-auto py-4', children: dialogMode === 'select' ? (_jsx("div", { className: 'grid grid-cols-2 gap-4 sm:grid-cols-3', children: [
|
|
877
|
+
{ Icon: DocumentIcon, text: 'Document', type: 'document' },
|
|
878
|
+
{ Icon: ImageIcon, text: 'Image', type: 'image' },
|
|
879
|
+
{ Icon: YouTubeIcon, text: 'YouTube', type: 'youtube' },
|
|
880
|
+
{ Icon: LinkIcon, text: 'Website', type: 'website' },
|
|
881
|
+
{ Icon: CatalogIcon, text: 'TsDocs', type: 'tsdocs' },
|
|
882
|
+
{ Icon: BoxIcon, text: 'Other', type: 'other' },
|
|
883
|
+
].map(({ Icon: i, text: t, type: v }) => (_jsxs("button", { type: 'button', "aria-label": `Add ${t} attachment`, className: 'flex cursor-pointer flex-col items-center justify-center rounded-xl border border-primary-6 bg-transparent p-6 text-center transition-colors hover:border-primary-8 hover:bg-primary-3/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-8', onClick: () => {
|
|
884
|
+
setDialogMode(v);
|
|
885
|
+
}, children: [createElement(i, { className: 'size-6 text-primary-11', 'aria-hidden': 'true' }), _jsx("span", { className: 'mt-2 text-sm font-medium text-primary-11', children: t })] }, t))) })) : (_jsxs("div", { className: 'flex flex-col gap-4', children: [!['youtube', 'website', 'tsdocs'].includes(dialogMode) && (_jsxs(Button, { variant: 'outline', onClick: () => {
|
|
886
|
+
setAddDialogOpen(false);
|
|
887
|
+
fileRef.current?.click();
|
|
888
|
+
}, children: [_jsx(UploadIcon, { className: 'mr-2 size-5' }), "Select Files to Upload"] })), _jsxs("div", { className: 'space-y-2', children: [_jsx(Label, { children: dialogMode === 'tsdocs' ? 'Select Reference' : !['youtube', 'website'].includes(dialogMode) ? 'Or Import via URL' : 'Import via URL' }), dialogMode === 'tsdocs' ? (_jsx(TsDocsSelect, { variant: 'inline', multiple: true, value: urlInput ? urlInput.split('\n').filter(Boolean) : [], onChange: (v) => {
|
|
889
|
+
setUrlInput(Array.isArray(v) ? v.join('\n') : v);
|
|
890
|
+
} })) : (_jsx(Textarea, { autoFocus: true, placeholder: 'https://...\nPaste multiple URLs on new lines', value: urlInput, onChange: e => {
|
|
891
|
+
setUrlInput(e.target.value);
|
|
892
|
+
}, rows: 4, onKeyDown: e => {
|
|
893
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
894
|
+
e.preventDefault();
|
|
895
|
+
void addRemote(urlInput, dialogMode);
|
|
896
|
+
}
|
|
897
|
+
} }))] })] })) }), _jsx(DialogFooter, { className: 'shrink-0 border-t border-primary-6 pt-4', children: dialogMode === 'select' ? (_jsx(Button, { variant: 'ghost', onClick: () => {
|
|
898
|
+
setAddDialogOpen(false);
|
|
899
|
+
}, children: "Cancel" })) : (_jsxs(_Fragment, { children: [_jsx(Button, { variant: 'ghost', onClick: () => {
|
|
900
|
+
setDialogMode('select');
|
|
901
|
+
}, children: "Back" }), _jsx(Button, { disabled: !urlInput.trim() || uploading, loading: uploading, onClick: () => void addRemote(urlInput, dialogMode), children: dialogMode === 'tsdocs' ? 'Attach Reference' : 'Attach URL' })] })) })] }))] })] }));
|
|
902
|
+
}
|
|
903
|
+
Attachments.displayName = 'DocsAttachments';
|
|
904
|
+
//# sourceMappingURL=_attachments.js.map
|