From 598b23a7d0be1fdf343f0e2a54e7c1be031c1548 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sun, 9 Aug 2026 13:55:37 +0200 Subject: [PATCH 1/7] feature: use tree view --- package.json | 1 + src/components/HomepageFeatures/index.tsx | 23 ++++++++++++++++++++++ yarn.lock | 24 +++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/package.json b/package.json index 0f3dfe1b0..e351e7c72 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@mdi/react": "^1.6.1", "@mdx-js/react": "^3.1.1", "@mermaid-js/layout-elk": "^0.2.1", + "@pierre/trees": "^1.0.0-beta.6", "@sentry/react": "^10.64.0", "@sentry/webpack-plugin": "^3.3.1", "ace-builds": "^1.44.0", diff --git a/src/components/HomepageFeatures/index.tsx b/src/components/HomepageFeatures/index.tsx index 8d37750df..765d48a26 100644 --- a/src/components/HomepageFeatures/index.tsx +++ b/src/components/HomepageFeatures/index.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx'; +import React from 'react'; import styles from './styles.module.scss'; import { useStore } from '@tdev-hooks/useStore'; import { observer } from 'mobx-react-lite'; @@ -10,14 +11,36 @@ import { useIsLive } from '@tdev-hooks/useIsLive'; import Card from '@tdev-components/shared/Card'; import customFields from '@tdev-components/utils/customFields'; const { BACKEND_URL, NO_AUTH, OFFLINE_API } = customFields; +import { prepareFileTreeInput } from '@pierre/trees'; +import { FileTree, useFileTree } from '@pierre/trees/react'; const HomepageFeatures = observer(() => { const socketStore = useStore('socketStore'); const sessionStore = useStore('sessionStore'); const userStore = useStore('userStore'); const isLive = useIsLive(); + const fsTree = React.useMemo( + () => + prepareFileTreeInput( + ['src/components', 'src/hooks/bla.py', 'src/hooks/bla.tsx', 'src/pages', 'src/utils'], + { + flattenEmptyDirectories: true + } + ), + [] + ); + const { model } = useFileTree({ + preparedInput: fsTree, + search: true, + icons: { + set: 'complete', + colored: false + }, + initialExpandedPaths: ['src', 'src/hooks'] + }); return (
+ {fsTree && } {sessionStore.apiMode === 'api' ? (

Socket.IO

diff --git a/yarn.lock b/yarn.lock index 48ea566f2..0976c08e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4103,6 +4103,20 @@ tslib "^2.8.1" tsyringe "^4.10.0" +"@pierre/theming@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@pierre/theming/-/theming-1.0.0.tgz#520e0f857c1b762e975f5b7a15f74a3e8320b825" + integrity sha512-WsdrnhKfjeyXGDikZmN9pkpeZ5S/cl6EE72feiSc0tlynT1tMYqXqouhuv/foK+PY9OEnebOAVRQn3+rAstR8g== + +"@pierre/trees@^1.0.0-beta.6": + version "1.0.0-beta.6" + resolved "https://registry.yarnpkg.com/@pierre/trees/-/trees-1.0.0-beta.6.tgz#6ba438cf6088b37b3f32f2272733afeff7d5fe07" + integrity sha512-zxeuSFM9TveM7b5XofweJALCtm/tGYV9HZzdbf7Uf+kBxIlUyz24/EHaGRjB0dsmmfDQl2ETz7AWwJ15lhSnpw== + dependencies: + "@pierre/theming" "1.0.0" + preact "11.0.0-beta.0" + preact-render-to-string "6.6.5" + "@pnpm/config.env-replace@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" @@ -13312,6 +13326,16 @@ postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.33, postcss@^8.5.25, postcss@^8.5 picocolors "^1.1.1" source-map-js "^1.2.1" +preact-render-to-string@6.6.5: + version "6.6.5" + resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-6.6.5.tgz#1be84cc8565e3bcfcf2a33cb9c61185e6498ab31" + integrity sha512-O6MHzYNIKYaiSX3bOw0gGZfEbOmlIDtDfWwN1JJdc/T3ihzRT6tGGSEWE088dWrEDGa1u7101q+6fzQnO9XCPA== + +preact@11.0.0-beta.0: + version "11.0.0-beta.0" + resolved "https://registry.yarnpkg.com/preact/-/preact-11.0.0-beta.0.tgz#c6e5c6e8657abf18b4a637eae88b5b377d36973f" + integrity sha512-IcODoASASYwJ9kxz7+MJeiJhvLriwSb4y4mHIyxdgaRZp6kPUud7xytrk/6GZw8U3y6EFJaRb5wi9SrEK+8+lg== + prebuild-install@^7.1.1, prebuild-install@^7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.3.tgz#d630abad2b147443f20a212917beae68b8092eec" From 98fc91dbf72781b0e6e4a8dc937dadf5e3a5c5e4 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Fri, 18 Sep 2026 19:57:54 +0200 Subject: [PATCH 2/7] implement basic document file tree --- src/api/OfflineApi/index.ts | 4 +- src/components/HomepageFeatures/index.tsx | 171 +++++++++--------- .../FileSystem/DocumentFileTree/index.tsx | 134 ++++++++++++++ .../DocumentFileTree/styles.module.scss | 0 src/models/documents/FileSystem/Directory.ts | 36 +++- .../documents/FileSystem/iFileSystem.ts | 46 ++++- src/stores/DocumentStore.ts | 5 +- 7 files changed, 299 insertions(+), 97 deletions(-) create mode 100644 src/components/documents/FileSystem/DocumentFileTree/index.tsx create mode 100644 src/components/documents/FileSystem/DocumentFileTree/styles.module.scss diff --git a/src/api/OfflineApi/index.ts b/src/api/OfflineApi/index.ts index 480aca141..7499dae56 100644 --- a/src/api/OfflineApi/index.ts +++ b/src/api/OfflineApi/index.ts @@ -13,7 +13,7 @@ import _ from 'es-toolkit/compat'; import { DB_NAME } from '@tdev-api/config'; const TIME_NOW = new Date().toISOString(); -const LOG_REQUESTS = false; +const LOG_REQUESTS = true; export const DEFAULT_OFFLINE_USER: User = { id: 'c23c0238-4aeb-457f-9a2c-3d2d5d8931c0', @@ -406,7 +406,7 @@ export default class OfflineApi { if (parts[0] === 'linkTo') { document = { ...document, - documentRootId: parts[1], + parentId: parts[1], updatedAt: updatedAt }; } else { diff --git a/src/components/HomepageFeatures/index.tsx b/src/components/HomepageFeatures/index.tsx index 765d48a26..36c19aa0b 100644 --- a/src/components/HomepageFeatures/index.tsx +++ b/src/components/HomepageFeatures/index.tsx @@ -1,4 +1,3 @@ -import clsx from 'clsx'; import React from 'react'; import styles from './styles.module.scss'; import { useStore } from '@tdev-hooks/useStore'; @@ -11,98 +10,102 @@ import { useIsLive } from '@tdev-hooks/useIsLive'; import Card from '@tdev-components/shared/Card'; import customFields from '@tdev-components/utils/customFields'; const { BACKEND_URL, NO_AUTH, OFFLINE_API } = customFields; -import { prepareFileTreeInput } from '@pierre/trees'; -import { FileTree, useFileTree } from '@pierre/trees/react'; +import { useFirstMainDocument } from '@tdev-hooks/useFirstMainDocument'; +import { ModelMeta } from '@tdev-models/documents/FileSystem/Directory'; +import { MetaInit } from '@tdev-models/documents/FileSystem/iFileSystem'; +import DocumentFileTree from '@tdev-components/documents/FileSystem/DocumentFileTree'; const HomepageFeatures = observer(() => { const socketStore = useStore('socketStore'); const sessionStore = useStore('sessionStore'); const userStore = useStore('userStore'); const isLive = useIsLive(); - const fsTree = React.useMemo( - () => - prepareFileTreeInput( - ['src/components', 'src/hooks/bla.py', 'src/hooks/bla.tsx', 'src/pages', 'src/utils'], - { - flattenEmptyDirectories: true - } - ), - [] + const [meta] = React.useState( + new ModelMeta({ id: '2686fc4e-10e7-4288-bf41-e6175e489b8e', readonly: false } as MetaInit) ); - const { model } = useFileTree({ - preparedInput: fsTree, - search: true, - icons: { - set: 'complete', - colored: false - }, - initialExpandedPaths: ['src', 'src/hooks'] - }); + const root = useFirstMainDocument('2686fc4e-10e7-4288-bf41-e6175e489b8e', meta); + return ( -
- {fsTree && } - {sessionStore.apiMode === 'api' ? ( -
-

Socket.IO

- -
URL
-
{BACKEND_URL}
-
Connected?
-
- {isLive ? ( - - {' '} - Live - - ) : ( - - {' '} - Offline - + <> +
+ {root && } +
+                    {JSON.stringify(root?.fileTree, null, 2)}
+                
+
+
+ {sessionStore.apiMode === 'api' ? ( +
+

Socket.IO

+ +
URL
+
{BACKEND_URL}
+
Connected?
+
+ {isLive ? ( + + {' '} + Live + + ) : ( + + {' '} + Offline + + )} +
+ {isLive && ( + <> +
Clients
+
+ {socketStore.connectedClients.get(userStore.viewedUser?.id ?? '') ?? + 0} +
+ )} -
- {isLive && ( - <> -
Clients
-
- {socketStore.connectedClients.get(userStore.viewedUser?.id ?? '') ?? 0} -
- - )} -
Offline API
-
{OFFLINE_API || '-'}
-
No Auth
-
{NO_AUTH ? 'Ja' : 'Nein'}
-
Connection
-
-
-
-
-
-
- ) : ( - -

Willkommen 🥳

-
- )} -
+
Offline API
+
{OFFLINE_API || '-'}
+
No Auth
+
{NO_AUTH ? 'Ja' : 'Nein'}
+
Connection
+
+
+
+
+ +
+ ) : ( + +

Willkommen 🥳

+
+ )} +
+ ); }); diff --git a/src/components/documents/FileSystem/DocumentFileTree/index.tsx b/src/components/documents/FileSystem/DocumentFileTree/index.tsx new file mode 100644 index 000000000..2e0e33fae --- /dev/null +++ b/src/components/documents/FileSystem/DocumentFileTree/index.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { useStore } from '@tdev-hooks/useStore'; +import { observer } from 'mobx-react-lite'; +import customFields from '@tdev-components/utils/customFields'; +import { FileTree, useFileTree } from '@pierre/trees/react'; +import Directory from '@tdev-models/documents/FileSystem/Directory'; +import { preparePresortedFileTreeInput } from '@pierre/trees'; +import { isFileSystemType } from '@tdev-models/documents/FileSystem/iFileSystem'; + +interface Props { + dir: Directory; +} +const DocumentFileTree = observer((props: Props) => { + const { dir } = props; + const rootId = dir.documentRootId; + const documentStore = useStore('documentStore'); + const docRootStore = useStore('documentRootStore'); + const { model } = useFileTree({ + preparedInput: preparePresortedFileTreeInput(dir?.fileTree ?? []), + search: true, + id: dir.id, + initialVisibleRowCount: 11, + icons: { + set: 'complete', + colored: false + }, + renaming: { + canRename: (item) => item.path !== 'package.json', + onRename: ({ sourcePath, destinationPath }) => { + const file = dir.allFiles.find((d) => d.filePath === sourcePath); + const hasConflict = dir.allFiles.some((d) => d.filePath === destinationPath); + if (!file) { + console.error(`File not found for path: ${sourcePath}`); + return; + } + if (hasConflict) { + console.error(`Destination path already exists: ${destinationPath}`); + return; + } + const newName = destinationPath.replace(file.basePath, ''); + if (newName.includes('/')) { + console.error(`New name cannot contain slashes: ${newName}`); + return; + } + file.setName(newName); + }, + onError: (message) => { + console.error(message); + } + }, + dragAndDrop: { + canDrag: (draggedPaths) => true, + canDrop: ({ target }) => true, + onDropComplete: ({ draggedPaths, target }) => { + const targetDoc = dir.allFiles.find((d) => d.filePath === target.directoryPath); + const files = dir.allFiles.filter((d) => draggedPaths.includes(d.filePath)); + console.log( + files.map((f) => f.filePath), + 'dropped on', + targetDoc?.filePath + ); + if (targetDoc && files.length > 0) { + files.forEach((f) => { + documentStore.relinkParent(f, targetDoc); + }); + } + }, + onDropError: (message) => { + console.error(message); + } + }, + composition: { + contextMenu: { + enabled: true, + triggerMode: 'both', + buttonVisibility: 'when-needed' + } + }, + flattenEmptyDirectories: false, + onSelectionChange: (selectedPaths) => { + const root = docRootStore.find<'dir'>(rootId); + if (!root) { + return; + } + const selected = root.documents + .filter((d) => isFileSystemType(d)) + .filter((d) => selectedPaths.includes(d.filePath)); + if (selected.length === 1) { + selected[0].setIsOpen(true); + console.log('Selected document:', selected[0].name, selected[0].filePath); + } + } + }); + + /** + * useFileTree only builds the model once, from the initial props - it never + * reacts to prop changes. Paths often arrive asynchronously (e.g. after a + * page reload, before the document is fetched), so the tree must be + * re-synced whenever the paths change after mount. + */ + React.useEffect(() => { + const openPaths = dir?.allFiles + .filter((d) => d.type === 'dir' && d.filePath && d.isOpen) + .map((d) => d.filePath); + console.log('open', openPaths); + model.resetPaths({ + preparedInput: preparePresortedFileTreeInput(dir.fileTree ?? []), + initialExpandedPaths: openPaths + }); + }, [model, dir.fileTree]); + + return ( + ( +
+ +
+ )} + /> + ); +}); + +export default DocumentFileTree; diff --git a/src/components/documents/FileSystem/DocumentFileTree/styles.module.scss b/src/components/documents/FileSystem/DocumentFileTree/styles.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/models/documents/FileSystem/Directory.ts b/src/models/documents/FileSystem/Directory.ts index 50b1b9203..f2ee7b7cd 100644 --- a/src/models/documents/FileSystem/Directory.ts +++ b/src/models/documents/FileSystem/Directory.ts @@ -1,9 +1,10 @@ import { computed } from 'mobx'; -import { DocumentType, Document as DocumentProps } from '@tdev-api/document'; +import { Document as DocumentProps } from '@tdev-api/document'; import DocumentStore from '@tdev-stores/DocumentStore'; import _ from 'es-toolkit/compat'; +import { orderBy } from 'es-toolkit/array'; import File from './File'; -import iFileSystem, { DefaultName, iFSMeta, MetaInit } from './iFileSystem'; +import iFileSystem, { DefaultName, iFSMeta, isFileSystemType, MetaInit } from './iFileSystem'; import { formatDateTime } from '@tdev-models/helpers/date'; export class ModelMeta extends iFSMeta<'dir'> { @@ -27,14 +28,22 @@ class Directory extends iFileSystem<'dir'> { return new ModelMeta({}); } + @computed + get allFiles(): iFileSystem[] { + const files = this.root?.documents.filter( + (d) => isFileSystemType(d) && d.filePath.startsWith(this.filePath) + ) as iFileSystem[]; + return orderBy(files || [], ['filePath'], ['asc']); + } + @computed get files() { if (!this.root) { return []; } - return _.orderBy( + return orderBy( this.root.documents.filter((d) => d.parentId === this.id && d.type === 'file') as File[], - [(f) => `${f.name}`.replace(/\d+/g, (n) => n.padStart(10, '0'))], + [(f) => `${f.name}`], ['asc'] ); } @@ -44,12 +53,27 @@ class Directory extends iFileSystem<'dir'> { if (!this.root) { return []; } - return _.orderBy( + return orderBy( this.root.documents.filter((d) => d.parentId === this.id && d.type === 'dir') as Directory[], - [(d) => `${d.name}`.replace(/\d+/g, (n) => n.padStart(10, '0'))], + [(d) => `${d.name}`], ['asc'] ); } + + @computed + get fileTree(): string[] { + // returns relative to self path all files having the current directory as parent. + const basePath = this.parentId ? `${this.name || this.id}/` : ''; + const directoryTrees = this.directories.flatMap((d) => { + return d.fileTree.map((p) => `${basePath}${p}`); + }); + const filePaths = this.files.map((f) => `${basePath}${f.name || f.id}`); + const n = directoryTrees.length + filePaths.length; + if (n === 0) { + return [basePath]; + } + return [...directoryTrees, ...filePaths]; + } } export default Directory; diff --git a/src/models/documents/FileSystem/iFileSystem.ts b/src/models/documents/FileSystem/iFileSystem.ts index a9129dade..e246f5d7e 100644 --- a/src/models/documents/FileSystem/iFileSystem.ts +++ b/src/models/documents/FileSystem/iFileSystem.ts @@ -1,6 +1,11 @@ import { action, computed, observable } from 'mobx'; import iDocument, { Source } from '@tdev-models/iDocument'; -import { Document as DocumentProps, TypeDataMapping, Access, DocumentModelType } from '@tdev-api/document'; +import { + Document as DocumentProps, + TypeDataMapping, + DocumentType, + DocumentModelType +} from '@tdev-api/document'; import DocumentStore from '@tdev-stores/DocumentStore'; import { TypeMeta } from '@tdev-models/DocumentRoot'; import { formatDateTime } from '@tdev-models/helpers/date'; @@ -13,11 +18,13 @@ export interface MetaInit { type SystemType = 'file' | 'dir'; -export const DefaultName = { +export const DefaultName: Record = { ['file']: 'Dokument', ['dir']: 'Ordner' }; +export const SystemDocumentTypes: SystemType[] = Object.keys(DefaultName) as SystemType[]; + export class iFSMeta extends TypeMeta { readonly readonly?: boolean; readonly name: string; @@ -35,7 +42,7 @@ export class iFSMeta extends TypeMeta { } } -abstract class iFileSystem extends iDocument { +abstract class iFileSystem extends iDocument { @observable accessor name: string; @observable accessor isOpen: boolean = true; @observable accessor isEditing: boolean = false; @@ -76,7 +83,34 @@ abstract class iFileSystem extends iDocument { abstract get meta(): iFSMeta; @computed - get path() { + get filePath(): string { + if (!this.parentId) { + if (this.type === 'dir') { + return ''; + } + return this.name; + } + const name = this.type === 'dir' ? `${this.name}/` : this.name; + if (this.parent?.type !== 'dir') { + return name; + } + const basePath = this.parent.filePath ? this.parent.filePath : ''; + return `${basePath}${name}`; + } + + @computed + get basePath(): string { + if (!this.parentId) { + return ''; + } + if (this.parent?.type !== 'dir') { + return ''; + } + return this.parent.filePath; + } + + @computed + get path(): DocumentModelType[] { const path: DocumentModelType[] = []; let parent = this.parent; while (parent) { @@ -111,4 +145,8 @@ abstract class iFileSystem extends iDocument { } } +export const isFileSystemType = (doc: iDocument): doc is iFileSystem => { + return doc.type === 'file' || doc.type === 'dir'; +}; + export default iFileSystem; diff --git a/src/stores/DocumentStore.ts b/src/stores/DocumentStore.ts index 77f7260ed..d8b80dac1 100644 --- a/src/stores/DocumentStore.ts +++ b/src/stores/DocumentStore.ts @@ -42,6 +42,7 @@ import TrueFalseAnswer from '@tdev-models/documents/Assessable/TrueFalseAnswer'; import Quiz from '@tdev-models/documents/Assessable/Quiz'; import { isStalledUpdate } from '@tdev/helpers/isStalledUpdate'; import Unknown from '@tdev-models/documents/Unknown'; +import iFileSystem from '@tdev-models/documents/FileSystem/iFileSystem'; const IsNotUniqueError = (error: any) => { try { @@ -444,11 +445,13 @@ class DocumentStore extends iStore<`delete-${string}`> { } @action - relinkParent(document: DocumentModelType, newParent: DocumentModelType) { + relinkParent(document: DocumentModelType | iFileSystem, newParent: DocumentModelType | iFileSystem) { + console.log('Relinking', document.id, 'to new parent', newParent.id); return this.withAbortController(`save-${document.id}`, (sig) => { return apiLinkTo(document.id, newParent.id, sig.signal); }) .then((res) => { + console.log('Relinking successful', res.data); this.addToStore(res.data); }) .catch((err) => { From 25c47f493393a2fe6053a5415372d3c0f74117e0 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Fri, 18 Sep 2026 20:36:21 +0200 Subject: [PATCH 3/7] respect theme --- .../FileSystem/DocumentFileTree/index.tsx | 4 +++- .../DocumentFileTree/styles.module.scss | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/documents/FileSystem/DocumentFileTree/index.tsx b/src/components/documents/FileSystem/DocumentFileTree/index.tsx index 2e0e33fae..17641c502 100644 --- a/src/components/documents/FileSystem/DocumentFileTree/index.tsx +++ b/src/components/documents/FileSystem/DocumentFileTree/index.tsx @@ -6,6 +6,8 @@ import { FileTree, useFileTree } from '@pierre/trees/react'; import Directory from '@tdev-models/documents/FileSystem/Directory'; import { preparePresortedFileTreeInput } from '@pierre/trees'; import { isFileSystemType } from '@tdev-models/documents/FileSystem/iFileSystem'; +import styles from './styles.module.scss'; +import clsx from 'clsx'; interface Props { dir: Directory; @@ -112,7 +114,7 @@ const DocumentFileTree = observer((props: Props) => { return ( (
diff --git a/src/components/documents/FileSystem/DocumentFileTree/styles.module.scss b/src/components/documents/FileSystem/DocumentFileTree/styles.module.scss index e69de29bb..e2a7a18f1 100644 --- a/src/components/documents/FileSystem/DocumentFileTree/styles.module.scss +++ b/src/components/documents/FileSystem/DocumentFileTree/styles.module.scss @@ -0,0 +1,16 @@ +.tree { + --trees-search-bg: var(--ifm-background-surface-color); + --trees-bg: var(--ifm-background-color); + // --trees-search-fg + // --trees-border-color + // --trees-accent + // --trees-fg-muted + // --trees-bg-muted + color-scheme: light; +} + +[data-theme='dark'] { + .tree { + color-scheme: dark; + } +} From 4dc81aab3f2109f15c2065e31737078c728a3a7f Mon Sep 17 00:00:00 2001 From: bh0fer Date: Fri, 18 Sep 2026 20:47:18 +0200 Subject: [PATCH 4/7] simple file edit preview --- .../FileSystem/DocumentFileTree/index.tsx | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/components/documents/FileSystem/DocumentFileTree/index.tsx b/src/components/documents/FileSystem/DocumentFileTree/index.tsx index 17641c502..bbfef689f 100644 --- a/src/components/documents/FileSystem/DocumentFileTree/index.tsx +++ b/src/components/documents/FileSystem/DocumentFileTree/index.tsx @@ -8,6 +8,8 @@ import { preparePresortedFileTreeInput } from '@pierre/trees'; import { isFileSystemType } from '@tdev-models/documents/FileSystem/iFileSystem'; import styles from './styles.module.scss'; import clsx from 'clsx'; +import { default as FileModel } from '@tdev-models/documents/FileSystem/File'; +import File from '../File'; interface Props { dir: Directory; @@ -17,6 +19,7 @@ const DocumentFileTree = observer((props: Props) => { const rootId = dir.documentRootId; const documentStore = useStore('documentStore'); const docRootStore = useStore('documentRootStore'); + const [selected, setSelected] = React.useState(null); const { model } = useFileTree({ preparedInput: preparePresortedFileTreeInput(dir?.fileTree ?? []), search: true, @@ -88,6 +91,9 @@ const DocumentFileTree = observer((props: Props) => { .filter((d) => isFileSystemType(d)) .filter((d) => selectedPaths.includes(d.filePath)); if (selected.length === 1) { + if (selected[0].type === 'file') { + setSelected(selected[0]); + } selected[0].setIsOpen(true); console.log('Selected document:', selected[0].name, selected[0].filePath); } @@ -112,24 +118,27 @@ const DocumentFileTree = observer((props: Props) => { }, [model, dir.fileTree]); return ( - ( -
- -
- )} - /> +
+ ( +
+ +
+ )} + /> + {selected && } +
); }); From ff194fe794eaf343d66a52a9122e705aea699eac Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sat, 19 Sep 2026 14:05:13 +0200 Subject: [PATCH 5/7] add simple styling --- src/components/HomepageFeatures/index.tsx | 11 +++--- .../FileSystem/DocumentFileTree/index.tsx | 5 +-- .../DocumentFileTree/styles.module.scss | 37 +++++++++++++------ .../documents/FileSystem/File/index.tsx | 5 ++- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/components/HomepageFeatures/index.tsx b/src/components/HomepageFeatures/index.tsx index 36c19aa0b..17f4b56fa 100644 --- a/src/components/HomepageFeatures/index.tsx +++ b/src/components/HomepageFeatures/index.tsx @@ -27,12 +27,11 @@ const HomepageFeatures = observer(() => { return ( <> -
- {root && } -
-                    {JSON.stringify(root?.fileTree, null, 2)}
-                
-
+ {root && ( +
+ +
+ )}
{sessionStore.apiMode === 'api' ? (
diff --git a/src/components/documents/FileSystem/DocumentFileTree/index.tsx b/src/components/documents/FileSystem/DocumentFileTree/index.tsx index bbfef689f..4d9f8d4dc 100644 --- a/src/components/documents/FileSystem/DocumentFileTree/index.tsx +++ b/src/components/documents/FileSystem/DocumentFileTree/index.tsx @@ -95,7 +95,6 @@ const DocumentFileTree = observer((props: Props) => { setSelected(selected[0]); } selected[0].setIsOpen(true); - console.log('Selected document:', selected[0].name, selected[0].filePath); } } }); @@ -118,7 +117,7 @@ const DocumentFileTree = observer((props: Props) => { }, [model, dir.fileTree]); return ( -
+
{
)} /> - {selected && } +
{selected && }
); }); diff --git a/src/components/documents/FileSystem/DocumentFileTree/styles.module.scss b/src/components/documents/FileSystem/DocumentFileTree/styles.module.scss index e2a7a18f1..471f5bec9 100644 --- a/src/components/documents/FileSystem/DocumentFileTree/styles.module.scss +++ b/src/components/documents/FileSystem/DocumentFileTree/styles.module.scss @@ -1,16 +1,31 @@ -.tree { - --trees-search-bg: var(--ifm-background-surface-color); - --trees-bg: var(--ifm-background-color); - // --trees-search-fg - // --trees-border-color - // --trees-accent - // --trees-fg-muted - // --trees-bg-muted - color-scheme: light; +.container { + display: flex; + align-items: flex-start; + width: 100%; + gap: 0.25rem; + flex-wrap: wrap; + .tree { + --trees-search-bg: var(--ifm-background-surface-color); + --trees-bg: var(--ifm-background-color); + // --trees-search-fg + // --trees-border-color + // --trees-accent + // --trees-fg-muted + // --trees-bg-muted + color-scheme: light; + } + .selectedFile { + flex-grow: 1; + flex-shrink: 1; + flex-basis: 300px; + max-width: 80vw; + } } [data-theme='dark'] { - .tree { - color-scheme: dark; + .container { + .tree { + color-scheme: dark; + } } } diff --git a/src/components/documents/FileSystem/File/index.tsx b/src/components/documents/FileSystem/File/index.tsx index 108d836da..7e95b5441 100644 --- a/src/components/documents/FileSystem/File/index.tsx +++ b/src/components/documents/FileSystem/File/index.tsx @@ -30,6 +30,7 @@ import iCode from '@tdev-models/documents/iCode'; interface Props { file: FileModel; + className?: string; } const getColor = (type?: DocumentType) => { @@ -72,11 +73,11 @@ const getOpenIcon = (type?: DocumentType) => { }; const File = observer((props: Props) => { - const { file } = props; + const { file, className } = props; return ( Date: Sat, 19 Sep 2026 17:39:26 +0200 Subject: [PATCH 6/7] handle root drop --- .../FileSystem/DocumentFileTree/index.tsx | 16 +++++----- .../DocumentFileTree/styles.module.scss | 3 +- .../CodeEditorSelector.tsx | 0 .../FileSystem/DocumentView/index.tsx | 31 +++++++++++++++++++ .../DocumentView/styles.module.scss | 3 ++ .../documents/FileSystem/File/index.tsx | 29 ++--------------- .../FileSystem/File/styles.module.scss | 3 -- src/models/documents/FileSystem/File.ts | 2 +- 8 files changed, 47 insertions(+), 40 deletions(-) rename src/components/documents/FileSystem/{File => DocumentView}/CodeEditorSelector.tsx (100%) create mode 100644 src/components/documents/FileSystem/DocumentView/index.tsx create mode 100644 src/components/documents/FileSystem/DocumentView/styles.module.scss diff --git a/src/components/documents/FileSystem/DocumentFileTree/index.tsx b/src/components/documents/FileSystem/DocumentFileTree/index.tsx index 4d9f8d4dc..3a7494915 100644 --- a/src/components/documents/FileSystem/DocumentFileTree/index.tsx +++ b/src/components/documents/FileSystem/DocumentFileTree/index.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { useStore } from '@tdev-hooks/useStore'; import { observer } from 'mobx-react-lite'; -import customFields from '@tdev-components/utils/customFields'; import { FileTree, useFileTree } from '@pierre/trees/react'; import Directory from '@tdev-models/documents/FileSystem/Directory'; import { preparePresortedFileTreeInput } from '@pierre/trees'; @@ -10,12 +9,14 @@ import styles from './styles.module.scss'; import clsx from 'clsx'; import { default as FileModel } from '@tdev-models/documents/FileSystem/File'; import File from '../File'; +import DocumentView from '../DocumentView'; interface Props { dir: Directory; } const DocumentFileTree = observer((props: Props) => { const { dir } = props; + const dirId = dir.id; const rootId = dir.documentRootId; const documentStore = useStore('documentStore'); const docRootStore = useStore('documentRootStore'); @@ -32,6 +33,7 @@ const DocumentFileTree = observer((props: Props) => { renaming: { canRename: (item) => item.path !== 'package.json', onRename: ({ sourcePath, destinationPath }) => { + const dir = documentStore.find(dirId) as Directory; const file = dir.allFiles.find((d) => d.filePath === sourcePath); const hasConflict = dir.allFiles.some((d) => d.filePath === destinationPath); if (!file) { @@ -57,13 +59,11 @@ const DocumentFileTree = observer((props: Props) => { canDrag: (draggedPaths) => true, canDrop: ({ target }) => true, onDropComplete: ({ draggedPaths, target }) => { - const targetDoc = dir.allFiles.find((d) => d.filePath === target.directoryPath); + const targetDoc = + target.kind === 'root' + ? dir + : dir.allFiles.find((d) => d.filePath === target.directoryPath); const files = dir.allFiles.filter((d) => draggedPaths.includes(d.filePath)); - console.log( - files.map((f) => f.filePath), - 'dropped on', - targetDoc?.filePath - ); if (targetDoc && files.length > 0) { files.forEach((f) => { documentStore.relinkParent(f, targetDoc); @@ -136,7 +136,7 @@ const DocumentFileTree = observer((props: Props) => {
)} /> -
{selected && }
+
{}
); }); diff --git a/src/components/documents/FileSystem/DocumentFileTree/styles.module.scss b/src/components/documents/FileSystem/DocumentFileTree/styles.module.scss index 471f5bec9..50483d362 100644 --- a/src/components/documents/FileSystem/DocumentFileTree/styles.module.scss +++ b/src/components/documents/FileSystem/DocumentFileTree/styles.module.scss @@ -2,7 +2,6 @@ display: flex; align-items: flex-start; width: 100%; - gap: 0.25rem; flex-wrap: wrap; .tree { --trees-search-bg: var(--ifm-background-surface-color); @@ -18,7 +17,7 @@ flex-grow: 1; flex-shrink: 1; flex-basis: 300px; - max-width: 80vw; + max-width: 1200px; } } diff --git a/src/components/documents/FileSystem/File/CodeEditorSelector.tsx b/src/components/documents/FileSystem/DocumentView/CodeEditorSelector.tsx similarity index 100% rename from src/components/documents/FileSystem/File/CodeEditorSelector.tsx rename to src/components/documents/FileSystem/DocumentView/CodeEditorSelector.tsx diff --git a/src/components/documents/FileSystem/DocumentView/index.tsx b/src/components/documents/FileSystem/DocumentView/index.tsx new file mode 100644 index 000000000..58862b574 --- /dev/null +++ b/src/components/documents/FileSystem/DocumentView/index.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import styles from './styles.module.scss'; +import { observer } from 'mobx-react-lite'; +import { QuillV2Component } from '../../QuillV2'; +import { ExcalidocComponent } from '@tdev/excalidoc/Component'; +import CodeEditorSelector from './CodeEditorSelector'; +import iCode from '@tdev-models/documents/iCode'; +import { DocumentModelType } from '@tdev-api/document'; + +interface Props { + document?: DocumentModelType; +} + +const DocumentView = observer((props: Props) => { + const { document } = props; + if (!document) { + return null; + } + if (document.type === 'script' || document.type === 'pyodide_code' || document.type === 'code') { + return ; + } + if (document.type === 'quill_v2') { + return ; + } + if (document.type === 'excalidoc') { + return ; + } + return null; +}); + +export default DocumentView; diff --git a/src/components/documents/FileSystem/DocumentView/styles.module.scss b/src/components/documents/FileSystem/DocumentView/styles.module.scss new file mode 100644 index 000000000..a31cbd5fc --- /dev/null +++ b/src/components/documents/FileSystem/DocumentView/styles.module.scss @@ -0,0 +1,3 @@ +.quill { + margin-bottom: 0; +} diff --git a/src/components/documents/FileSystem/File/index.tsx b/src/components/documents/FileSystem/File/index.tsx index 7e95b5441..c8bb1eeed 100644 --- a/src/components/documents/FileSystem/File/index.tsx +++ b/src/components/documents/FileSystem/File/index.tsx @@ -15,18 +15,11 @@ import { } from '@mdi/js'; import SyncStatus from '../../../SyncStatus'; import { DocumentType } from '@tdev-api/document'; -import { QuillV2Component } from '../../QuillV2'; import Actions from '../Actions'; import Name from '../Name'; import FsDetails from '../FsDetails'; -import { - ExcalidocComponent, - ExcalidrawColor, - mdiExcalidraw, - mdiExcalidrawOutline -} from '@tdev/excalidoc/Component'; -import CodeEditorSelector from './CodeEditorSelector'; -import iCode from '@tdev-models/documents/iCode'; +import { ExcalidrawColor, mdiExcalidraw, mdiExcalidrawOutline } from '@tdev/excalidoc/Component'; +import DocumentView from '../DocumentView'; interface Props { file: FileModel; @@ -97,23 +90,7 @@ const File = observer((props: Props) => { } >
- {file.document && file.isOpen && ( - <> - {file.document.type === 'script' && ( - - )} - {file.document.type === 'quill_v2' && ( - - )} - {file.document.type === 'excalidoc' && ( - - )} - - )} + {file.isOpen && }
); diff --git a/src/components/documents/FileSystem/File/styles.module.scss b/src/components/documents/FileSystem/File/styles.module.scss index 395195743..ef64b76cf 100644 --- a/src/components/documents/FileSystem/File/styles.module.scss +++ b/src/components/documents/FileSystem/File/styles.module.scss @@ -7,8 +7,5 @@ } .content { padding: 1px; - .quill { - margin-bottom: 0; - } } } diff --git a/src/models/documents/FileSystem/File.ts b/src/models/documents/FileSystem/File.ts index ccb7928d5..e217774d5 100644 --- a/src/models/documents/FileSystem/File.ts +++ b/src/models/documents/FileSystem/File.ts @@ -1,5 +1,5 @@ import { computed } from 'mobx'; -import { DocumentType, Document as DocumentProps } from '@tdev-api/document'; +import { Document as DocumentProps } from '@tdev-api/document'; import DocumentStore from '@tdev-stores/DocumentStore'; import _ from 'es-toolkit/compat'; import iFileSystem, { DefaultName, iFSMeta, MetaInit } from './iFileSystem'; From 05992a7bddd1cf9f1e43cee9dd13e3c7badf9b4d Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sat, 19 Sep 2026 17:52:27 +0200 Subject: [PATCH 7/7] add cions --- .../documents/FileSystem/DocumentFileTree/index.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/documents/FileSystem/DocumentFileTree/index.tsx b/src/components/documents/FileSystem/DocumentFileTree/index.tsx index 3a7494915..3485c4c25 100644 --- a/src/components/documents/FileSystem/DocumentFileTree/index.tsx +++ b/src/components/documents/FileSystem/DocumentFileTree/index.tsx @@ -10,6 +10,7 @@ import clsx from 'clsx'; import { default as FileModel } from '@tdev-models/documents/FileSystem/File'; import File from '../File'; import DocumentView from '../DocumentView'; +import { mdiExcalidraw } from '@tdev/excalidoc/Component'; interface Props { dir: Directory; @@ -28,7 +29,17 @@ const DocumentFileTree = observer((props: Props) => { initialVisibleRowCount: 11, icons: { set: 'complete', - colored: false + colored: true, + spriteSheet: ` + + `, + byFileExtension: { + excalidraw: 'mdi-excalidraw' + } }, renaming: { canRename: (item) => item.path !== 'package.json',