@bendyline/docblocks-react 1.1.1 → 2.0.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/README.md +50 -15
- package/dist/AppMenu/AppMenu.d.ts +29 -2
- package/dist/AppMenu/AppMenu.d.ts.map +1 -1
- package/dist/AppMenu/AppMenu.js +41 -2
- package/dist/AppMenu/AppMenu.js.map +1 -1
- package/dist/AppMenu/index.d.ts +1 -1
- package/dist/AppMenu/index.d.ts.map +1 -1
- package/dist/DocBlocksShell/DocBlocksShell.d.ts +17 -4
- package/dist/DocBlocksShell/DocBlocksShell.d.ts.map +1 -1
- package/dist/DocBlocksShell/DocBlocksShell.js +1710 -299
- package/dist/DocBlocksShell/DocBlocksShell.js.map +1 -1
- package/dist/DocBlocksShell/LazyEditorShell.d.ts +3 -0
- package/dist/DocBlocksShell/LazyEditorShell.d.ts.map +1 -0
- package/dist/DocBlocksShell/LazyEditorShell.js +3 -0
- package/dist/DocBlocksShell/LazyEditorShell.js.map +1 -0
- package/dist/DocBlocksShell/UpdateAvailableNotice.d.ts +13 -0
- package/dist/DocBlocksShell/UpdateAvailableNotice.d.ts.map +1 -0
- package/dist/DocBlocksShell/UpdateAvailableNotice.js +16 -0
- package/dist/DocBlocksShell/UpdateAvailableNotice.js.map +1 -0
- package/dist/DocBlocksShell/document-title.d.ts +3 -0
- package/dist/DocBlocksShell/document-title.d.ts.map +1 -0
- package/dist/DocBlocksShell/document-title.js +17 -0
- package/dist/DocBlocksShell/document-title.js.map +1 -0
- package/dist/DocBlocksShell/workspace-authority-barrier.d.ts +14 -0
- package/dist/DocBlocksShell/workspace-authority-barrier.d.ts.map +1 -0
- package/dist/DocBlocksShell/workspace-authority-barrier.js +23 -0
- package/dist/DocBlocksShell/workspace-authority-barrier.js.map +1 -0
- package/dist/Export/DeferredExportToolbarControls.d.ts +5 -0
- package/dist/Export/DeferredExportToolbarControls.d.ts.map +1 -0
- package/dist/Export/DeferredExportToolbarControls.js +10 -0
- package/dist/Export/DeferredExportToolbarControls.js.map +1 -0
- package/dist/Export/ExportDialog.d.ts +10 -1
- package/dist/Export/ExportDialog.d.ts.map +1 -1
- package/dist/Export/ExportDialog.js +39 -20
- package/dist/Export/ExportDialog.js.map +1 -1
- package/dist/Export/ExportToolbarControls.d.ts +21 -1
- package/dist/Export/ExportToolbarControls.d.ts.map +1 -1
- package/dist/Export/ExportToolbarControls.js +161 -26
- package/dist/Export/ExportToolbarControls.js.map +1 -1
- package/dist/Export/export-destination.d.ts +3 -0
- package/dist/Export/export-destination.d.ts.map +1 -0
- package/dist/Export/export-destination.js +32 -0
- package/dist/Export/export-destination.js.map +1 -0
- package/dist/Export/index.d.ts +8 -4
- package/dist/Export/index.d.ts.map +1 -1
- package/dist/Export/index.js +4 -1
- package/dist/Export/index.js.map +1 -1
- package/dist/Export/public-api.d.ts +12 -0
- package/dist/Export/public-api.d.ts.map +1 -0
- package/dist/Export/public-api.js +9 -0
- package/dist/Export/public-api.js.map +1 -0
- package/dist/Export/run-export.d.ts +3 -1
- package/dist/Export/run-export.d.ts.map +1 -1
- package/dist/Export/run-export.js +58 -25
- package/dist/Export/run-export.js.map +1 -1
- package/dist/Export/transform-summaries.d.ts +7 -0
- package/dist/Export/transform-summaries.d.ts.map +1 -0
- package/dist/Export/transform-summaries.js +6 -0
- package/dist/Export/transform-summaries.js.map +1 -0
- package/dist/ExportDialog-MEUISWZX.js +9 -0
- package/dist/ExportDialog-MEUISWZX.js.map +1 -0
- package/dist/ExportToolbarControls-D2YDWOXW.js +373 -0
- package/dist/ExportToolbarControls-D2YDWOXW.js.map +1 -0
- package/dist/FileExplorer/FileExplorer.d.ts +26 -4
- package/dist/FileExplorer/FileExplorer.d.ts.map +1 -1
- package/dist/FileExplorer/FileExplorer.js +204 -29
- package/dist/FileExplorer/FileExplorer.js.map +1 -1
- package/dist/FileExplorer/FileTreeNode.d.ts +24 -3
- package/dist/FileExplorer/FileTreeNode.d.ts.map +1 -1
- package/dist/FileExplorer/FileTreeNode.js +27 -8
- package/dist/FileExplorer/FileTreeNode.js.map +1 -1
- package/dist/FileExplorer/index.d.ts +1 -1
- package/dist/FileExplorer/index.d.ts.map +1 -1
- package/dist/FileExplorer/useFileTree.d.ts +5 -3
- package/dist/FileExplorer/useFileTree.d.ts.map +1 -1
- package/dist/FileExplorer/useFileTree.js +173 -13
- package/dist/FileExplorer/useFileTree.js.map +1 -1
- package/dist/Git/GitAuthGuidanceDialog.d.ts +18 -0
- package/dist/Git/GitAuthGuidanceDialog.d.ts.map +1 -0
- package/dist/Git/GitAuthGuidanceDialog.js +27 -0
- package/dist/Git/GitAuthGuidanceDialog.js.map +1 -0
- package/dist/Git/GitBranchesDialog.d.ts +13 -0
- package/dist/Git/GitBranchesDialog.d.ts.map +1 -0
- package/dist/Git/GitBranchesDialog.js +59 -0
- package/dist/Git/GitBranchesDialog.js.map +1 -0
- package/dist/Git/GitCloneDialog.d.ts +14 -0
- package/dist/Git/GitCloneDialog.d.ts.map +1 -0
- package/dist/Git/GitCloneDialog.js +79 -0
- package/dist/Git/GitCloneDialog.js.map +1 -0
- package/dist/Git/GitCommitDialog.d.ts +12 -0
- package/dist/Git/GitCommitDialog.d.ts.map +1 -0
- package/dist/Git/GitCommitDialog.js +38 -0
- package/dist/Git/GitCommitDialog.js.map +1 -0
- package/dist/Git/GitConflictsDialog.d.ts +12 -0
- package/dist/Git/GitConflictsDialog.d.ts.map +1 -0
- package/dist/Git/GitConflictsDialog.js +17 -0
- package/dist/Git/GitConflictsDialog.js.map +1 -0
- package/dist/Git/GitContext.d.ts +89 -0
- package/dist/Git/GitContext.d.ts.map +1 -0
- package/dist/Git/GitContext.js +15 -0
- package/dist/Git/GitContext.js.map +1 -0
- package/dist/Git/GitDiffDialog.d.ts +20 -0
- package/dist/Git/GitDiffDialog.d.ts.map +1 -0
- package/dist/Git/GitDiffDialog.js +100 -0
- package/dist/Git/GitDiffDialog.js.map +1 -0
- package/dist/Git/GitDiffView.d.ts +23 -0
- package/dist/Git/GitDiffView.d.ts.map +1 -0
- package/dist/Git/GitDiffView.js +77 -0
- package/dist/Git/GitDiffView.js.map +1 -0
- package/dist/Git/GitHistoryDialog.d.ts +15 -0
- package/dist/Git/GitHistoryDialog.d.ts.map +1 -0
- package/dist/Git/GitHistoryDialog.js +107 -0
- package/dist/Git/GitHistoryDialog.js.map +1 -0
- package/dist/Git/GitStatusBar.d.ts +8 -0
- package/dist/Git/GitStatusBar.d.ts.map +1 -0
- package/dist/Git/GitStatusBar.js +49 -0
- package/dist/Git/GitStatusBar.js.map +1 -0
- package/dist/Git/GitToolbarControl.d.ts +15 -0
- package/dist/Git/GitToolbarControl.d.ts.map +1 -0
- package/dist/Git/GitToolbarControl.js +51 -0
- package/dist/Git/GitToolbarControl.js.map +1 -0
- package/dist/Git/GitUI.d.ts +14 -0
- package/dist/Git/GitUI.d.ts.map +1 -0
- package/dist/Git/GitUI.js +21 -0
- package/dist/Git/GitUI.js.map +1 -0
- package/dist/Git/commit-selection.d.ts +22 -0
- package/dist/Git/commit-selection.d.ts.map +1 -0
- package/dist/Git/commit-selection.js +47 -0
- package/dist/Git/commit-selection.js.map +1 -0
- package/dist/Git/git-clone-utils.d.ts +8 -0
- package/dist/Git/git-clone-utils.d.ts.map +1 -0
- package/dist/Git/git-clone-utils.js +26 -0
- package/dist/Git/git-clone-utils.js.map +1 -0
- package/dist/Git/git-status.d.ts +24 -0
- package/dist/Git/git-status.d.ts.map +1 -0
- package/dist/Git/git-status.js +105 -0
- package/dist/Git/git-status.js.map +1 -0
- package/dist/Git/useGit.d.ts +15 -0
- package/dist/Git/useGit.d.ts.map +1 -0
- package/dist/Git/useGit.js +206 -0
- package/dist/Git/useGit.js.map +1 -0
- package/dist/Git/useGitStatus.d.ts +17 -0
- package/dist/Git/useGitStatus.d.ts.map +1 -0
- package/dist/Git/useGitStatus.js +63 -0
- package/dist/Git/useGitStatus.js.map +1 -0
- package/dist/GitToolbarControl-LMIR4R7I.js +117 -0
- package/dist/GitToolbarControl-LMIR4R7I.js.map +1 -0
- package/dist/GitUI-KGF5MSNV.js +1048 -0
- package/dist/GitUI-KGF5MSNV.js.map +1 -0
- package/dist/LazyEditorShell-EF2BWRXU.js +6 -0
- package/dist/LazyEditorShell-EF2BWRXU.js.map +1 -0
- package/dist/Settings/Settings.d.ts +23 -0
- package/dist/Settings/Settings.d.ts.map +1 -0
- package/dist/Settings/Settings.js +22 -0
- package/dist/Settings/Settings.js.map +1 -0
- package/dist/Settings/public-api.d.ts +3 -0
- package/dist/Settings/public-api.d.ts.map +1 -0
- package/dist/Settings/public-api.js +2 -0
- package/dist/Settings/public-api.js.map +1 -0
- package/dist/WorkspacePicker/WorkspacePicker.d.ts +6 -1
- package/dist/WorkspacePicker/WorkspacePicker.d.ts.map +1 -1
- package/dist/WorkspacePicker/WorkspacePicker.js +16 -5
- package/dist/WorkspacePicker/WorkspacePicker.js.map +1 -1
- package/dist/chunk-3SAXDU2U.js +157 -0
- package/dist/chunk-3SAXDU2U.js.map +1 -0
- package/dist/chunk-4XXSWN3T.js +13 -0
- package/dist/chunk-4XXSWN3T.js.map +1 -0
- package/dist/chunk-5A4KEWKT.js +97 -0
- package/dist/chunk-5A4KEWKT.js.map +1 -0
- package/dist/chunk-AOVTHZU7.js +321 -0
- package/dist/chunk-AOVTHZU7.js.map +1 -0
- package/dist/chunk-DD4VFJQI.js +51 -0
- package/dist/chunk-DD4VFJQI.js.map +1 -0
- package/dist/chunk-GSNZSCIK.js +55 -0
- package/dist/chunk-GSNZSCIK.js.map +1 -0
- package/dist/chunk-VHDP4IPL.js +70 -0
- package/dist/chunk-VHDP4IPL.js.map +1 -0
- package/dist/chunk-ZI56BNB7.js +270 -0
- package/dist/chunk-ZI56BNB7.js.map +1 -0
- package/dist/components/Dialog.d.ts +22 -0
- package/dist/components/Dialog.d.ts.map +1 -0
- package/dist/components/Dialog.js +33 -0
- package/dist/components/Dialog.js.map +1 -0
- package/dist/editor.d.ts +10 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +70 -0
- package/dist/editor.js.map +1 -0
- package/dist/export/index.d.ts +87 -0
- package/dist/export/index.js +26 -0
- package/dist/export/index.js.map +1 -0
- package/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useDocumentSession.d.ts +16 -0
- package/dist/hooks/useDocumentSession.d.ts.map +1 -0
- package/dist/hooks/useDocumentSession.js +18 -0
- package/dist/hooks/useDocumentSession.js.map +1 -0
- package/dist/icons.d.ts +11 -6
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +21 -20
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/preferences/theme.d.ts +18 -0
- package/dist/preferences/theme.d.ts.map +1 -0
- package/dist/preferences/theme.js +63 -0
- package/dist/preferences/theme.js.map +1 -0
- package/dist/provider-lease.d.ts +10 -0
- package/dist/provider-lease.d.ts.map +1 -0
- package/dist/provider-lease.js +42 -0
- package/dist/provider-lease.js.map +1 -0
- package/dist/run-export-ICTZITXC.js +10 -0
- package/dist/run-export-ICTZITXC.js.map +1 -0
- package/dist/settings/index.d.ts +28 -0
- package/dist/settings/index.js +12 -0
- package/dist/settings/index.js.map +1 -0
- package/package.json +27 -10
- package/src/AppMenu/AppMenu.tsx +169 -99
- package/src/AppMenu/index.ts +1 -1
- package/src/DocBlocksShell/DocBlocksShell.tsx +2183 -516
- package/src/DocBlocksShell/LazyEditorShell.ts +2 -0
- package/src/DocBlocksShell/UpdateAvailableNotice.tsx +60 -0
- package/src/DocBlocksShell/document-title.ts +19 -0
- package/src/DocBlocksShell/workspace-authority-barrier.ts +26 -0
- package/src/Export/DeferredExportToolbarControls.tsx +23 -0
- package/src/Export/ExportDialog.tsx +102 -27
- package/src/Export/ExportToolbarControls.tsx +343 -57
- package/src/Export/export-destination.ts +30 -0
- package/src/Export/index.ts +18 -4
- package/src/Export/public-api.ts +17 -0
- package/src/Export/run-export.ts +66 -29
- package/src/Export/transform-summaries.ts +14 -0
- package/src/FileExplorer/FileExplorer.tsx +284 -38
- package/src/FileExplorer/FileTreeNode.tsx +100 -8
- package/src/FileExplorer/index.ts +1 -1
- package/src/FileExplorer/useFileTree.ts +190 -16
- package/src/Git/GitAuthGuidanceDialog.tsx +84 -0
- package/src/Git/GitBranchesDialog.tsx +144 -0
- package/src/Git/GitCloneDialog.tsx +200 -0
- package/src/Git/GitCommitDialog.tsx +119 -0
- package/src/Git/GitConflictsDialog.tsx +62 -0
- package/src/Git/GitContext.tsx +88 -0
- package/src/Git/GitDiffDialog.tsx +147 -0
- package/src/Git/GitDiffView.tsx +105 -0
- package/src/Git/GitHistoryDialog.tsx +195 -0
- package/src/Git/GitStatusBar.tsx +157 -0
- package/src/Git/GitToolbarControl.tsx +123 -0
- package/src/Git/GitUI.tsx +68 -0
- package/src/Git/commit-selection.ts +63 -0
- package/src/Git/git-clone-utils.ts +23 -0
- package/src/Git/git-status.ts +113 -0
- package/src/Git/useGit.ts +279 -0
- package/src/Git/useGitStatus.ts +78 -0
- package/src/Settings/Settings.tsx +118 -0
- package/src/Settings/public-api.ts +8 -0
- package/src/WorkspacePicker/WorkspacePicker.tsx +62 -8
- package/src/components/Dialog.tsx +81 -0
- package/src/editor.ts +72 -0
- package/src/hooks/index.ts +2 -1
- package/src/hooks/useDocumentSession.ts +37 -0
- package/src/icons.tsx +33 -55
- package/src/index.ts +35 -5
- package/src/preferences/theme.ts +67 -0
- package/src/provider-lease.ts +52 -0
- package/src/styles/docblocks.css +2247 -77
- package/src/types/pwa-dom.d.ts +31 -0
- package/dist/hooks/useAutoSave.d.ts +0 -15
- package/dist/hooks/useAutoSave.d.ts.map +0 -1
- package/dist/hooks/useAutoSave.js +0 -83
- package/dist/hooks/useAutoSave.js.map +0 -1
- package/src/fonts/fonts.css +0 -362
- package/src/fonts/licenses/LICENSE-cormorant-garamond.txt +0 -93
- package/src/fonts/licenses/LICENSE-crimson-text.txt +0 -93
- package/src/fonts/licenses/LICENSE-dm-sans.txt +0 -93
- package/src/fonts/licenses/LICENSE-dm-serif-display.txt +0 -93
- package/src/fonts/licenses/LICENSE-hanken-grotesk.txt +0 -94
- package/src/fonts/licenses/LICENSE-ibm-plex-sans.txt +0 -93
- package/src/fonts/licenses/LICENSE-inter.txt +0 -93
- package/src/fonts/licenses/LICENSE-jetbrains-mono.txt +0 -93
- package/src/fonts/licenses/LICENSE-lora.txt +0 -93
- package/src/fonts/licenses/LICENSE-merriweather.txt +0 -93
- package/src/fonts/licenses/LICENSE-oswald.txt +0 -93
- package/src/fonts/licenses/LICENSE-playfair-display.txt +0 -93
- package/src/fonts/licenses/LICENSE-pt-serif.txt +0 -93
- package/src/fonts/licenses/LICENSE-roboto.txt +0 -93
- package/src/fonts/licenses/LICENSE-source-serif-4.txt +0 -93
- package/src/fonts/woff2/cormorant-garamond-400.woff2 +0 -0
- package/src/fonts/woff2/cormorant-garamond-700.woff2 +0 -0
- package/src/fonts/woff2/crimson-text-400.woff2 +0 -0
- package/src/fonts/woff2/crimson-text-400i.woff2 +0 -0
- package/src/fonts/woff2/crimson-text-700.woff2 +0 -0
- package/src/fonts/woff2/dm-sans-400.woff2 +0 -0
- package/src/fonts/woff2/dm-sans-400i.woff2 +0 -0
- package/src/fonts/woff2/dm-sans-500.woff2 +0 -0
- package/src/fonts/woff2/dm-sans-700.woff2 +0 -0
- package/src/fonts/woff2/dm-serif-display-400.woff2 +0 -0
- package/src/fonts/woff2/hanken-grotesk-400.woff2 +0 -0
- package/src/fonts/woff2/hanken-grotesk-400i.woff2 +0 -0
- package/src/fonts/woff2/hanken-grotesk-500.woff2 +0 -0
- package/src/fonts/woff2/hanken-grotesk-600.woff2 +0 -0
- package/src/fonts/woff2/hanken-grotesk-700.woff2 +0 -0
- package/src/fonts/woff2/ibm-plex-sans-400.woff2 +0 -0
- package/src/fonts/woff2/ibm-plex-sans-400i.woff2 +0 -0
- package/src/fonts/woff2/ibm-plex-sans-500.woff2 +0 -0
- package/src/fonts/woff2/ibm-plex-sans-600.woff2 +0 -0
- package/src/fonts/woff2/ibm-plex-sans-700.woff2 +0 -0
- package/src/fonts/woff2/inter-400.woff2 +0 -0
- package/src/fonts/woff2/inter-400i.woff2 +0 -0
- package/src/fonts/woff2/inter-500.woff2 +0 -0
- package/src/fonts/woff2/inter-600.woff2 +0 -0
- package/src/fonts/woff2/inter-700.woff2 +0 -0
- package/src/fonts/woff2/jetbrains-mono-400.woff2 +0 -0
- package/src/fonts/woff2/jetbrains-mono-400i.woff2 +0 -0
- package/src/fonts/woff2/jetbrains-mono-700.woff2 +0 -0
- package/src/fonts/woff2/lora-400.woff2 +0 -0
- package/src/fonts/woff2/lora-400i.woff2 +0 -0
- package/src/fonts/woff2/lora-700.woff2 +0 -0
- package/src/fonts/woff2/merriweather-400.woff2 +0 -0
- package/src/fonts/woff2/merriweather-700.woff2 +0 -0
- package/src/fonts/woff2/oswald-400.woff2 +0 -0
- package/src/fonts/woff2/oswald-700.woff2 +0 -0
- package/src/fonts/woff2/playfair-display-400.woff2 +0 -0
- package/src/fonts/woff2/playfair-display-700.woff2 +0 -0
- package/src/fonts/woff2/pt-serif-400.woff2 +0 -0
- package/src/fonts/woff2/pt-serif-700.woff2 +0 -0
- package/src/fonts/woff2/roboto-400.woff2 +0 -0
- package/src/fonts/woff2/roboto-400i.woff2 +0 -0
- package/src/fonts/woff2/roboto-700.woff2 +0 -0
- package/src/fonts/woff2/source-serif-4-400.woff2 +0 -0
- package/src/fonts/woff2/source-serif-4-400i.woff2 +0 -0
- package/src/fonts/woff2/source-serif-4-700.woff2 +0 -0
- package/src/hooks/useAutoSave.ts +0 -116
|
@@ -5,24 +5,94 @@
|
|
|
5
5
|
* for creating new files/folders.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useCallback, useState } from 'react';
|
|
9
|
-
import
|
|
8
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
9
|
+
import {
|
|
10
|
+
isFileSystemMoveStateError,
|
|
11
|
+
parseWorkspacePath,
|
|
12
|
+
type FileSystemProvider,
|
|
13
|
+
type FileSystemEntry,
|
|
14
|
+
} from '@bendyline/docblocks/filesystem';
|
|
10
15
|
import { useFileTree } from './useFileTree.js';
|
|
11
|
-
import {
|
|
12
|
-
|
|
16
|
+
import {
|
|
17
|
+
FileTreeNode,
|
|
18
|
+
type FileTreeNodeBadge,
|
|
19
|
+
type FileTreeNodeGitActions,
|
|
20
|
+
} from './FileTreeNode.js';
|
|
21
|
+
import { NewFileIcon, NewFolderIcon } from '../icons.js';
|
|
22
|
+
import { useGitContext } from '../Git/GitContext.js';
|
|
23
|
+
import { BADGE_GLYPHS, BADGE_LABELS, isFileDirty } from '../Git/git-status.js';
|
|
13
24
|
|
|
14
25
|
const SUPPORTED_EXTENSIONS = new Set(['.txt', '.md', '.docx', '.pdf', '.dbk', '.zip']);
|
|
26
|
+
const INTERNAL_DRAG_TYPE = 'application/x-docblocks-entry';
|
|
27
|
+
|
|
28
|
+
export type FileTreeChange =
|
|
29
|
+
| { type: 'create'; path: string; kind?: 'file' | 'directory' }
|
|
30
|
+
| { type: 'delete'; path: string; kind: 'file' | 'directory' }
|
|
31
|
+
| {
|
|
32
|
+
type: 'move';
|
|
33
|
+
oldPath: string;
|
|
34
|
+
newPath: string;
|
|
35
|
+
kind: 'file' | 'directory';
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type FileTreeMutationHandler = (
|
|
39
|
+
change: FileTreeChange,
|
|
40
|
+
mutate: () => Promise<void>,
|
|
41
|
+
) => Promise<void>;
|
|
42
|
+
|
|
43
|
+
function normalisePath(path: string): string {
|
|
44
|
+
return parseWorkspacePath(path);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasEquivalentPath(paths: ReadonlySet<string>, path: string): boolean {
|
|
48
|
+
const canonical = normalisePath(path);
|
|
49
|
+
return (
|
|
50
|
+
paths.has(path) || paths.has(canonical) || (canonical !== '' && paths.has(`/${canonical}`))
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getEquivalentPathValue<T>(values: ReadonlyMap<string, T>, path: string): T | undefined {
|
|
55
|
+
const canonical = normalisePath(path);
|
|
56
|
+
return values.get(path) ?? values.get(canonical) ?? values.get(`/${canonical}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parentPath(path: string): string {
|
|
60
|
+
const clean = normalisePath(path);
|
|
61
|
+
const slash = clean.lastIndexOf('/');
|
|
62
|
+
return slash < 0 ? '' : clean.slice(0, slash);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function pathInDirectory(entry: FileSystemEntry, directoryPath: string): string {
|
|
66
|
+
const prefix = directoryPath.replace(/\/+$/, '');
|
|
67
|
+
if (prefix) return `${prefix}/${entry.name}`;
|
|
68
|
+
return entry.path.startsWith('/') ? `/${entry.name}` : entry.name;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function canMoveTo(entry: FileSystemEntry, directoryPath: string): boolean {
|
|
72
|
+
const source = normalisePath(entry.path);
|
|
73
|
+
const target = normalisePath(directoryPath);
|
|
74
|
+
if (parentPath(source) === target) return false;
|
|
75
|
+
if (entry.kind === 'directory' && (target === source || target.startsWith(`${source}/`))) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isInternalDrag(dataTransfer: DataTransfer): boolean {
|
|
82
|
+
return Array.from(dataTransfer.types).includes(INTERNAL_DRAG_TYPE);
|
|
83
|
+
}
|
|
15
84
|
|
|
16
85
|
/**
|
|
17
|
-
* Hide
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
86
|
+
* Hide dot-entries (`.git`, `.gitignore`, `.DS_Store`, …) — never user
|
|
87
|
+
* documents — and auto-generated `<basename>_files/` companion folders,
|
|
88
|
+
* which hold per-document images and version snapshots (useful to the
|
|
89
|
+
* editor, noisy in the user-facing file list). Everything still exists on
|
|
90
|
+
* disk; we just don't surface it in the explorer.
|
|
21
91
|
*/
|
|
22
92
|
function isHiddenEntry(entry: FileSystemEntry): boolean {
|
|
23
|
-
if (entry.kind !== 'directory') return false;
|
|
24
93
|
const name = entry.path.replace(/^\/+/, '').split('/').pop() ?? '';
|
|
25
|
-
|
|
94
|
+
if (name.startsWith('.')) return true;
|
|
95
|
+
return entry.kind === 'directory' && name.endsWith('_files');
|
|
26
96
|
}
|
|
27
97
|
|
|
28
98
|
function filterVisible(entries: FileSystemEntry[]): FileSystemEntry[] {
|
|
@@ -32,10 +102,17 @@ function filterVisible(entries: FileSystemEntry[]): FileSystemEntry[] {
|
|
|
32
102
|
export interface FileExplorerProps {
|
|
33
103
|
/** The filesystem to display. */
|
|
34
104
|
provider: FileSystemProvider | null;
|
|
105
|
+
/** Active document to select and reveal by expanding all ancestor folders. */
|
|
106
|
+
activeFilePath?: string | null;
|
|
35
107
|
/** Called when any entry is selected (file or directory). */
|
|
36
108
|
onSelect?: (path: string, kind: 'file' | 'directory') => void;
|
|
37
|
-
/**
|
|
38
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Wraps destructive tree mutations so the active document session can
|
|
111
|
+
* flush, cancel, or retarget itself before storage changes.
|
|
112
|
+
*/
|
|
113
|
+
onTreeMutation?: FileTreeMutationHandler;
|
|
114
|
+
/** Called after any mutation, with move details when paths change. */
|
|
115
|
+
onTreeChange?: (change?: FileTreeChange) => void;
|
|
39
116
|
/** Called when supported files are dropped onto the explorer. */
|
|
40
117
|
onImportFiles?: (files: File[]) => void;
|
|
41
118
|
/** Optional className for the root element. */
|
|
@@ -44,7 +121,9 @@ export interface FileExplorerProps {
|
|
|
44
121
|
|
|
45
122
|
export function FileExplorer({
|
|
46
123
|
provider,
|
|
124
|
+
activeFilePath,
|
|
47
125
|
onSelect,
|
|
126
|
+
onTreeMutation,
|
|
48
127
|
onTreeChange,
|
|
49
128
|
onImportFiles,
|
|
50
129
|
className,
|
|
@@ -53,11 +132,21 @@ export function FileExplorer({
|
|
|
53
132
|
const { childEntries } = tree as typeof tree & {
|
|
54
133
|
childEntries: Map<string, FileSystemEntry[]>;
|
|
55
134
|
};
|
|
135
|
+
const { reveal } = tree;
|
|
136
|
+
// Null on surfaces without git (site, tests) — everything degrades.
|
|
137
|
+
const git = useGitContext();
|
|
56
138
|
|
|
57
139
|
const [newItemName, setNewItemName] = useState('');
|
|
58
140
|
const [newItemType, setNewItemType] = useState<'file' | 'directory' | null>(null);
|
|
59
141
|
const [dragOver, setDragOver] = useState(false);
|
|
60
|
-
const
|
|
142
|
+
const [draggedEntry, setDraggedEntry] = useState<FileSystemEntry | null>(null);
|
|
143
|
+
const [dropTarget, setDropTarget] = useState<string | null>(null);
|
|
144
|
+
const [moveError, setMoveError] = useState<string | null>(null);
|
|
145
|
+
const dragCounter = useRef(0);
|
|
146
|
+
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (activeFilePath) reveal(activeFilePath, 'file');
|
|
149
|
+
}, [activeFilePath, provider, reveal]);
|
|
61
150
|
|
|
62
151
|
const hasSupported = useCallback((dt: DataTransfer): boolean => {
|
|
63
152
|
for (const item of Array.from(dt.items)) {
|
|
@@ -73,6 +162,7 @@ export function FileExplorer({
|
|
|
73
162
|
|
|
74
163
|
const handleDragEnter = useCallback(
|
|
75
164
|
(e: React.DragEvent) => {
|
|
165
|
+
if (isInternalDrag(e.dataTransfer)) return;
|
|
76
166
|
e.preventDefault();
|
|
77
167
|
dragCounter.current += 1;
|
|
78
168
|
if (hasSupported(e.dataTransfer)) setDragOver(true);
|
|
@@ -81,12 +171,14 @@ export function FileExplorer({
|
|
|
81
171
|
);
|
|
82
172
|
|
|
83
173
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
174
|
+
if (isInternalDrag(e.dataTransfer)) return;
|
|
84
175
|
e.preventDefault();
|
|
85
176
|
e.dataTransfer.dropEffect = 'copy';
|
|
86
177
|
}, []);
|
|
87
178
|
|
|
88
179
|
const handleDragLeave = useCallback(
|
|
89
180
|
(e: React.DragEvent) => {
|
|
181
|
+
if (isInternalDrag(e.dataTransfer)) return;
|
|
90
182
|
e.preventDefault();
|
|
91
183
|
dragCounter.current -= 1;
|
|
92
184
|
if (dragCounter.current <= 0) {
|
|
@@ -99,6 +191,7 @@ export function FileExplorer({
|
|
|
99
191
|
|
|
100
192
|
const handleDrop = useCallback(
|
|
101
193
|
(e: React.DragEvent) => {
|
|
194
|
+
if (isInternalDrag(e.dataTransfer)) return;
|
|
102
195
|
e.preventDefault();
|
|
103
196
|
dragCounter.current = 0;
|
|
104
197
|
setDragOver(false);
|
|
@@ -108,7 +201,7 @@ export function FileExplorer({
|
|
|
108
201
|
});
|
|
109
202
|
if (supported.length > 0) onImportFiles?.(supported);
|
|
110
203
|
},
|
|
111
|
-
[onImportFiles
|
|
204
|
+
[onImportFiles],
|
|
112
205
|
);
|
|
113
206
|
|
|
114
207
|
const handleSelect = useCallback(
|
|
@@ -141,31 +234,155 @@ export function FileExplorer({
|
|
|
141
234
|
// Scope to currently selected folder (or root)
|
|
142
235
|
const prefix =
|
|
143
236
|
tree.selectedKind === 'directory' && tree.selectedPath ? `${tree.selectedPath}/` : '';
|
|
237
|
+
let createdPath = `${prefix}${name}`;
|
|
144
238
|
if (newItemType === 'file') {
|
|
145
239
|
const filename = name.endsWith('.md') ? name : `${name}.md`;
|
|
146
|
-
|
|
240
|
+
createdPath = `${prefix}${filename}`;
|
|
241
|
+
await tree.createFile(createdPath, '');
|
|
147
242
|
} else if (newItemType === 'directory') {
|
|
148
|
-
await tree.createDirectory(
|
|
243
|
+
await tree.createDirectory(createdPath);
|
|
149
244
|
}
|
|
150
245
|
setNewItemName('');
|
|
151
246
|
setNewItemType(null);
|
|
152
|
-
onTreeChange?.();
|
|
247
|
+
onTreeChange?.({ type: 'create', path: createdPath });
|
|
153
248
|
}, [newItemName, newItemType, tree, onTreeChange]);
|
|
154
249
|
|
|
250
|
+
const runTreeMutation = useCallback(
|
|
251
|
+
async (change: FileTreeChange, mutate: () => Promise<void>) => {
|
|
252
|
+
if (onTreeMutation) await onTreeMutation(change, mutate);
|
|
253
|
+
else await mutate();
|
|
254
|
+
onTreeChange?.(change);
|
|
255
|
+
},
|
|
256
|
+
[onTreeMutation, onTreeChange],
|
|
257
|
+
);
|
|
258
|
+
|
|
155
259
|
const handleDelete = useCallback(
|
|
156
|
-
async (path: string) => {
|
|
157
|
-
|
|
158
|
-
|
|
260
|
+
async (path: string, kind: 'file' | 'directory') => {
|
|
261
|
+
const change: FileTreeChange = { type: 'delete', path, kind };
|
|
262
|
+
await runTreeMutation(change, () => tree.deleteEntry(path));
|
|
159
263
|
},
|
|
160
|
-
[tree,
|
|
264
|
+
[tree, runTreeMutation],
|
|
161
265
|
);
|
|
162
266
|
|
|
163
267
|
const handleRename = useCallback(
|
|
164
|
-
async (oldPath: string, newPath: string) => {
|
|
165
|
-
|
|
166
|
-
|
|
268
|
+
async (oldPath: string, newPath: string, kind: 'file' | 'directory') => {
|
|
269
|
+
setMoveError(null);
|
|
270
|
+
const change: FileTreeChange = { type: 'move', oldPath, newPath, kind };
|
|
271
|
+
try {
|
|
272
|
+
await runTreeMutation(change, () => tree.renameEntry(oldPath, newPath, kind));
|
|
273
|
+
} catch (error: unknown) {
|
|
274
|
+
await tree.refresh();
|
|
275
|
+
if (isFileSystemMoveStateError(error) && error.documentLocation === 'destination') {
|
|
276
|
+
onTreeChange?.(change);
|
|
277
|
+
}
|
|
278
|
+
setMoveError(error instanceof Error ? error.message : 'Unable to move this entry.');
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
[tree, runTreeMutation, onTreeChange],
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const handleMove = useCallback(
|
|
285
|
+
async (entry: FileSystemEntry, directoryPath: string) => {
|
|
286
|
+
if (!canMoveTo(entry, directoryPath)) return;
|
|
287
|
+
const newPath = pathInDirectory(entry, directoryPath);
|
|
288
|
+
await handleRename(entry.path, newPath, entry.kind);
|
|
289
|
+
if (directoryPath && !tree.expanded.has(directoryPath)) {
|
|
290
|
+
tree.toggleExpand(directoryPath);
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
[handleRename, tree],
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const handleInternalDragStart = useCallback((e: React.DragEvent, entry: FileSystemEntry) => {
|
|
297
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
298
|
+
e.dataTransfer.setData(INTERNAL_DRAG_TYPE, entry.path);
|
|
299
|
+
e.dataTransfer.setData('text/plain', entry.path);
|
|
300
|
+
setDraggedEntry(entry);
|
|
301
|
+
setMoveError(null);
|
|
302
|
+
}, []);
|
|
303
|
+
|
|
304
|
+
const handleInternalDragEnd = useCallback(() => {
|
|
305
|
+
setDraggedEntry(null);
|
|
306
|
+
setDropTarget(null);
|
|
307
|
+
}, []);
|
|
308
|
+
|
|
309
|
+
const handleEntryDragOver = useCallback(
|
|
310
|
+
(e: React.DragEvent, entry: FileSystemEntry) => {
|
|
311
|
+
if (!isInternalDrag(e.dataTransfer)) return;
|
|
312
|
+
e.stopPropagation();
|
|
313
|
+
const canDrop =
|
|
314
|
+
entry.kind === 'directory' && draggedEntry && canMoveTo(draggedEntry, entry.path);
|
|
315
|
+
e.dataTransfer.dropEffect = canDrop ? 'move' : 'none';
|
|
316
|
+
if (canDrop) {
|
|
317
|
+
e.preventDefault();
|
|
318
|
+
setDropTarget(entry.path);
|
|
319
|
+
} else {
|
|
320
|
+
setDropTarget(null);
|
|
321
|
+
}
|
|
167
322
|
},
|
|
168
|
-
[
|
|
323
|
+
[draggedEntry],
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const handleEntryDrop = useCallback(
|
|
327
|
+
(e: React.DragEvent, entry: FileSystemEntry) => {
|
|
328
|
+
if (!isInternalDrag(e.dataTransfer)) return;
|
|
329
|
+
e.preventDefault();
|
|
330
|
+
e.stopPropagation();
|
|
331
|
+
setDropTarget(null);
|
|
332
|
+
if (entry.kind === 'directory' && draggedEntry) {
|
|
333
|
+
void handleMove(draggedEntry, entry.path);
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
[draggedEntry, handleMove],
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const handleRootDragOver = useCallback(
|
|
340
|
+
(e: React.DragEvent) => {
|
|
341
|
+
if (!isInternalDrag(e.dataTransfer) || !draggedEntry) return;
|
|
342
|
+
e.preventDefault();
|
|
343
|
+
e.stopPropagation();
|
|
344
|
+
const canDrop = canMoveTo(draggedEntry, '');
|
|
345
|
+
e.dataTransfer.dropEffect = canDrop ? 'move' : 'none';
|
|
346
|
+
setDropTarget(canDrop ? '' : null);
|
|
347
|
+
},
|
|
348
|
+
[draggedEntry],
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const handleRootDrop = useCallback(
|
|
352
|
+
(e: React.DragEvent) => {
|
|
353
|
+
if (!isInternalDrag(e.dataTransfer)) return;
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
e.stopPropagation();
|
|
356
|
+
setDropTarget(null);
|
|
357
|
+
if (draggedEntry) void handleMove(draggedEntry, '');
|
|
358
|
+
},
|
|
359
|
+
[draggedEntry, handleMove],
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const badgeFor = useCallback(
|
|
363
|
+
(entry: FileSystemEntry): FileTreeNodeBadge | undefined => {
|
|
364
|
+
if (!git?.repo) return undefined;
|
|
365
|
+
const key = entry.path.startsWith('/') ? entry.path : `/${entry.path}`;
|
|
366
|
+
const kind = git.badges.get(key);
|
|
367
|
+
if (!kind) return undefined;
|
|
368
|
+
return { kind, glyph: BADGE_GLYPHS[kind], label: BADGE_LABELS[kind] };
|
|
369
|
+
},
|
|
370
|
+
[git],
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const gitActionsFor = useCallback(
|
|
374
|
+
(entry: FileSystemEntry): FileTreeNodeGitActions | undefined => {
|
|
375
|
+
if (!git?.repo || entry.kind !== 'file') return undefined;
|
|
376
|
+
const path = entry.path.startsWith('/') ? entry.path : `/${entry.path}`;
|
|
377
|
+
return {
|
|
378
|
+
viewChanges: isFileDirty(git.status, path)
|
|
379
|
+
? () => git.openDialog({ kind: 'diff', path })
|
|
380
|
+
: undefined,
|
|
381
|
+
fileHistory: () => git.openDialog({ kind: 'history', path }),
|
|
382
|
+
openOnRemote: git.remoteWeb ? () => git.openOnRemote(path) : undefined,
|
|
383
|
+
};
|
|
384
|
+
},
|
|
385
|
+
[git],
|
|
169
386
|
);
|
|
170
387
|
|
|
171
388
|
const renderEntries = useCallback(
|
|
@@ -175,20 +392,46 @@ export function FileExplorer({
|
|
|
175
392
|
key={entry.path}
|
|
176
393
|
entry={entry}
|
|
177
394
|
depth={depth}
|
|
178
|
-
expanded={tree.expanded
|
|
179
|
-
selected={
|
|
395
|
+
expanded={hasEquivalentPath(tree.expanded, entry.path)}
|
|
396
|
+
selected={
|
|
397
|
+
tree.selectedPath !== null &&
|
|
398
|
+
normalisePath(tree.selectedPath) === normalisePath(entry.path)
|
|
399
|
+
}
|
|
400
|
+
badge={badgeFor(entry)}
|
|
401
|
+
gitActions={gitActionsFor(entry)}
|
|
180
402
|
onToggle={tree.toggleExpand}
|
|
181
403
|
onSelect={handleSelect}
|
|
182
404
|
onDelete={handleDelete}
|
|
183
405
|
onRename={handleRename}
|
|
406
|
+
draggable
|
|
407
|
+
dragging={draggedEntry?.path === entry.path}
|
|
408
|
+
dropTarget={dropTarget === entry.path}
|
|
409
|
+
onDragStart={handleInternalDragStart}
|
|
410
|
+
onDragEnd={handleInternalDragEnd}
|
|
411
|
+
onDragOverEntry={handleEntryDragOver}
|
|
412
|
+
onDropEntry={handleEntryDrop}
|
|
184
413
|
renderChildren={(dirPath: string) => {
|
|
185
|
-
const children = childEntries
|
|
414
|
+
const children = getEquivalentPathValue(childEntries, dirPath) ?? [];
|
|
186
415
|
return renderEntries(children, depth + 1);
|
|
187
416
|
}}
|
|
188
417
|
/>
|
|
189
418
|
));
|
|
190
419
|
},
|
|
191
|
-
[
|
|
420
|
+
[
|
|
421
|
+
tree,
|
|
422
|
+
handleSelect,
|
|
423
|
+
handleDelete,
|
|
424
|
+
handleRename,
|
|
425
|
+
childEntries,
|
|
426
|
+
badgeFor,
|
|
427
|
+
gitActionsFor,
|
|
428
|
+
draggedEntry,
|
|
429
|
+
dropTarget,
|
|
430
|
+
handleInternalDragStart,
|
|
431
|
+
handleInternalDragEnd,
|
|
432
|
+
handleEntryDragOver,
|
|
433
|
+
handleEntryDrop,
|
|
434
|
+
],
|
|
192
435
|
);
|
|
193
436
|
|
|
194
437
|
return (
|
|
@@ -219,14 +462,6 @@ export function FileExplorer({
|
|
|
219
462
|
>
|
|
220
463
|
<NewFolderIcon />
|
|
221
464
|
</button>
|
|
222
|
-
<button
|
|
223
|
-
className="db-explorer-btn"
|
|
224
|
-
onClick={() => tree.refresh()}
|
|
225
|
-
title="Refresh file list"
|
|
226
|
-
aria-label="Refresh file list"
|
|
227
|
-
>
|
|
228
|
-
<RefreshIcon />
|
|
229
|
-
</button>
|
|
230
465
|
</div>
|
|
231
466
|
</div>
|
|
232
467
|
|
|
@@ -261,6 +496,12 @@ export function FileExplorer({
|
|
|
261
496
|
</div>
|
|
262
497
|
)}
|
|
263
498
|
|
|
499
|
+
{moveError && (
|
|
500
|
+
<div className="db-tree-error" role="alert">
|
|
501
|
+
{moveError}
|
|
502
|
+
</div>
|
|
503
|
+
)}
|
|
504
|
+
|
|
264
505
|
{/* Tree — role="tree" only when there are real treeitem children; the
|
|
265
506
|
loading/empty states get a status role so axe doesn't flag the
|
|
266
507
|
tree as missing required children. */}
|
|
@@ -273,7 +514,12 @@ export function FileExplorer({
|
|
|
273
514
|
<div className="db-tree-empty">No files yet</div>
|
|
274
515
|
</div>
|
|
275
516
|
) : (
|
|
276
|
-
<div
|
|
517
|
+
<div
|
|
518
|
+
className={`db-tree ${dropTarget === '' ? 'db-tree--drop-target' : ''}`}
|
|
519
|
+
role="tree"
|
|
520
|
+
onDragOver={handleRootDragOver}
|
|
521
|
+
onDrop={handleRootDrop}
|
|
522
|
+
>
|
|
277
523
|
{renderEntries(tree.entries, 0)}
|
|
278
524
|
</div>
|
|
279
525
|
)}
|
|
@@ -7,16 +7,39 @@ import { createPortal } from 'react-dom';
|
|
|
7
7
|
import type { FileSystemEntry } from '@bendyline/docblocks/filesystem';
|
|
8
8
|
import { MoreIcon } from '../icons.js';
|
|
9
9
|
|
|
10
|
+
/** Git decoration for a row — precomputed by FileExplorer so this node stays context-free. */
|
|
11
|
+
export interface FileTreeNodeBadge {
|
|
12
|
+
kind: string;
|
|
13
|
+
glyph: string;
|
|
14
|
+
label: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Git context-menu actions, prebound to this entry's path. */
|
|
18
|
+
export interface FileTreeNodeGitActions {
|
|
19
|
+
viewChanges?: () => void;
|
|
20
|
+
fileHistory?: () => void;
|
|
21
|
+
openOnRemote?: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
export interface FileTreeNodeProps {
|
|
11
25
|
entry: FileSystemEntry;
|
|
12
26
|
depth: number;
|
|
13
27
|
expanded: boolean;
|
|
14
28
|
selected: boolean;
|
|
29
|
+
badge?: FileTreeNodeBadge;
|
|
30
|
+
gitActions?: FileTreeNodeGitActions;
|
|
15
31
|
children?: FileSystemEntry[];
|
|
16
32
|
onToggle: (path: string) => void;
|
|
17
33
|
onSelect: (path: string) => void;
|
|
18
|
-
onDelete: (path: string) => Promise<void>;
|
|
19
|
-
onRename: (oldPath: string, newPath: string) => Promise<void>;
|
|
34
|
+
onDelete: (path: string, kind: 'file' | 'directory') => Promise<void>;
|
|
35
|
+
onRename: (oldPath: string, newPath: string, kind: 'file' | 'directory') => Promise<void>;
|
|
36
|
+
draggable?: boolean;
|
|
37
|
+
dragging?: boolean;
|
|
38
|
+
dropTarget?: boolean;
|
|
39
|
+
onDragStart?: (event: React.DragEvent, entry: FileSystemEntry) => void;
|
|
40
|
+
onDragEnd?: () => void;
|
|
41
|
+
onDragOverEntry?: (event: React.DragEvent, entry: FileSystemEntry) => void;
|
|
42
|
+
onDropEntry?: (event: React.DragEvent, entry: FileSystemEntry) => void;
|
|
20
43
|
renderChildren?: (dirPath: string) => React.ReactNode;
|
|
21
44
|
}
|
|
22
45
|
|
|
@@ -25,10 +48,19 @@ export function FileTreeNode({
|
|
|
25
48
|
depth,
|
|
26
49
|
expanded,
|
|
27
50
|
selected,
|
|
51
|
+
badge,
|
|
52
|
+
gitActions,
|
|
28
53
|
onToggle,
|
|
29
54
|
onSelect,
|
|
30
55
|
onDelete,
|
|
31
56
|
onRename,
|
|
57
|
+
draggable = false,
|
|
58
|
+
dragging = false,
|
|
59
|
+
dropTarget = false,
|
|
60
|
+
onDragStart,
|
|
61
|
+
onDragEnd,
|
|
62
|
+
onDragOverEntry,
|
|
63
|
+
onDropEntry,
|
|
32
64
|
renderChildren,
|
|
33
65
|
}: FileTreeNodeProps) {
|
|
34
66
|
const [renaming, setRenaming] = useState(false);
|
|
@@ -76,15 +108,19 @@ export function FileTreeNode({
|
|
|
76
108
|
? entry.path.slice(0, entry.path.lastIndexOf('/'))
|
|
77
109
|
: '';
|
|
78
110
|
const newPath = parentPath ? `${parentPath}/${renameValue}` : renameValue;
|
|
79
|
-
await onRename(entry.path, newPath);
|
|
111
|
+
await onRename(entry.path, newPath, entry.kind);
|
|
80
112
|
}
|
|
81
113
|
setRenaming(false);
|
|
82
|
-
}, [renameValue, entry.name, entry.path, onRename]);
|
|
114
|
+
}, [renameValue, entry.name, entry.path, entry.kind, onRename]);
|
|
83
115
|
|
|
84
116
|
const handleDeleteClick = useCallback(async () => {
|
|
85
117
|
setShowContext(false);
|
|
86
|
-
|
|
87
|
-
|
|
118
|
+
const description = entry.kind === 'directory' ? 'folder and everything inside it' : 'document';
|
|
119
|
+
if (!window.confirm(`Delete the ${description} "${entry.name}"? This cannot be undone.`)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
await onDelete(entry.path, entry.kind);
|
|
123
|
+
}, [entry.name, entry.path, entry.kind, onDelete]);
|
|
88
124
|
|
|
89
125
|
// Close context menu on outside click or scroll
|
|
90
126
|
useEffect(() => {
|
|
@@ -112,6 +148,12 @@ export function FileTreeNode({
|
|
|
112
148
|
}
|
|
113
149
|
}, [renaming]);
|
|
114
150
|
|
|
151
|
+
// Active documents can be revealed after asynchronous ancestor loads. Keep
|
|
152
|
+
// the selected row inside the explorer's scroll viewport once it mounts.
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (selected) nodeRef.current?.scrollIntoView?.({ block: 'nearest' });
|
|
155
|
+
}, [selected]);
|
|
156
|
+
|
|
115
157
|
// Clamp menu inside viewport after it renders
|
|
116
158
|
useLayoutEffect(() => {
|
|
117
159
|
if (!showContext || !contextRef.current) return;
|
|
@@ -135,13 +177,19 @@ export function FileTreeNode({
|
|
|
135
177
|
return (
|
|
136
178
|
<div className="db-tree-node" ref={nodeRef}>
|
|
137
179
|
<div
|
|
138
|
-
className={`db-tree-row ${selected ? 'db-tree-row--selected' : ''}`}
|
|
180
|
+
className={`db-tree-row ${selected ? 'db-tree-row--selected' : ''} ${dragging ? 'db-tree-row--dragging' : ''} ${dropTarget ? 'db-tree-row--drop-target' : ''}`}
|
|
139
181
|
style={{ paddingLeft: depth * 16 + 4 }}
|
|
140
182
|
onClick={handleClick}
|
|
141
183
|
onContextMenu={handleContextMenu}
|
|
184
|
+
draggable={draggable && !renaming}
|
|
185
|
+
onDragStart={(event) => onDragStart?.(event, entry)}
|
|
186
|
+
onDragEnd={onDragEnd}
|
|
187
|
+
onDragOver={(event) => onDragOverEntry?.(event, entry)}
|
|
188
|
+
onDrop={(event) => onDropEntry?.(event, entry)}
|
|
142
189
|
role="treeitem"
|
|
143
190
|
aria-expanded={isDir ? expanded : undefined}
|
|
144
191
|
aria-selected={selected}
|
|
192
|
+
aria-label={badge ? `${entry.name}, ${badge.label}` : undefined}
|
|
145
193
|
tabIndex={0}
|
|
146
194
|
onKeyDown={(e) => {
|
|
147
195
|
if (e.key === 'Enter') handleClick();
|
|
@@ -166,6 +214,11 @@ export function FileTreeNode({
|
|
|
166
214
|
<span className="db-tree-label">
|
|
167
215
|
{entry.name.endsWith('.md') ? entry.name.slice(0, -3) : entry.name}
|
|
168
216
|
</span>
|
|
217
|
+
{badge && (
|
|
218
|
+
<span className={`db-git-badge db-git-badge--${badge.kind}`} aria-hidden="true">
|
|
219
|
+
{badge.glyph}
|
|
220
|
+
</span>
|
|
221
|
+
)}
|
|
169
222
|
<button
|
|
170
223
|
type="button"
|
|
171
224
|
className={`db-tree-more${showContext ? ' db-tree-more--active' : ''}`}
|
|
@@ -176,7 +229,7 @@ export function FileTreeNode({
|
|
|
176
229
|
aria-expanded={showContext}
|
|
177
230
|
tabIndex={-1}
|
|
178
231
|
>
|
|
179
|
-
<MoreIcon
|
|
232
|
+
<MoreIcon />
|
|
180
233
|
</button>
|
|
181
234
|
</>
|
|
182
235
|
)}
|
|
@@ -193,6 +246,45 @@ export function FileTreeNode({
|
|
|
193
246
|
<button className="db-tree-context-item" onClick={handleRenameStart}>
|
|
194
247
|
Rename
|
|
195
248
|
</button>
|
|
249
|
+
{gitActions && (gitActions.viewChanges || gitActions.fileHistory) && (
|
|
250
|
+
<>
|
|
251
|
+
<div className="db-tree-context-divider" role="separator" />
|
|
252
|
+
{gitActions.viewChanges && (
|
|
253
|
+
<button
|
|
254
|
+
className="db-tree-context-item"
|
|
255
|
+
onClick={() => {
|
|
256
|
+
setShowContext(false);
|
|
257
|
+
gitActions.viewChanges?.();
|
|
258
|
+
}}
|
|
259
|
+
>
|
|
260
|
+
View changes
|
|
261
|
+
</button>
|
|
262
|
+
)}
|
|
263
|
+
{gitActions.fileHistory && (
|
|
264
|
+
<button
|
|
265
|
+
className="db-tree-context-item"
|
|
266
|
+
onClick={() => {
|
|
267
|
+
setShowContext(false);
|
|
268
|
+
gitActions.fileHistory?.();
|
|
269
|
+
}}
|
|
270
|
+
>
|
|
271
|
+
File history…
|
|
272
|
+
</button>
|
|
273
|
+
)}
|
|
274
|
+
{gitActions.openOnRemote && (
|
|
275
|
+
<button
|
|
276
|
+
className="db-tree-context-item"
|
|
277
|
+
onClick={() => {
|
|
278
|
+
setShowContext(false);
|
|
279
|
+
gitActions.openOnRemote?.();
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
Open on remote
|
|
283
|
+
</button>
|
|
284
|
+
)}
|
|
285
|
+
<div className="db-tree-context-divider" role="separator" />
|
|
286
|
+
</>
|
|
287
|
+
)}
|
|
196
288
|
<button
|
|
197
289
|
className="db-tree-context-item db-tree-context-item--danger"
|
|
198
290
|
onClick={handleDeleteClick}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { FileExplorer } from './FileExplorer.js';
|
|
2
|
-
export type { FileExplorerProps } from './FileExplorer.js';
|
|
2
|
+
export type { FileExplorerProps, FileTreeChange, FileTreeMutationHandler } from './FileExplorer.js';
|
|
3
3
|
export { FileTreeNode } from './FileTreeNode.js';
|
|
4
4
|
export type { FileTreeNodeProps } from './FileTreeNode.js';
|
|
5
5
|
export { useFileTree } from './useFileTree.js';
|