mirror of
https://github.com/appwrite/console.git
synced 2026-04-07 19:17:46 +00:00
Fix filesystem tree handling and add file sync with Synapse
Improves path normalization, handles empty path segments, properly identifies folders, adds two-way communication with Synapse server for file/folder operations, and enables real-time file system exploration.
This commit is contained in:
@@ -44,10 +44,16 @@ export function treeFromFilesystem(files: string[]): TreeItem[] {
|
||||
const tree: TreeItem[] = [];
|
||||
|
||||
for (const path of files) {
|
||||
const parts = path.split('/');
|
||||
const normalizedPath = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||
|
||||
if (!normalizedPath) continue;
|
||||
|
||||
const parts = normalizedPath.split('/');
|
||||
let currentLevel = tree;
|
||||
|
||||
parts.forEach((part, index) => {
|
||||
if (!part) return;
|
||||
|
||||
const existingItem = currentLevel.find((item) => item.title === part);
|
||||
|
||||
if (existingItem) {
|
||||
@@ -56,7 +62,7 @@ export function treeFromFilesystem(files: string[]): TreeItem[] {
|
||||
}
|
||||
currentLevel = existingItem.children;
|
||||
} else {
|
||||
const isFile = index === parts.length - 1;
|
||||
const isFile = index === parts.length - 1 && !path.endsWith('/');
|
||||
const currentPath = parts.slice(0, index + 1).join('/');
|
||||
const newItem: TreeItem = {
|
||||
title: part,
|
||||
|
||||
@@ -13,19 +13,19 @@
|
||||
} = getContext<TreeView>('tree');
|
||||
</script>
|
||||
|
||||
{#each items as { title, icon, children, path }, i}
|
||||
{@const hasChildren = !!children?.length}
|
||||
{#each items as { title, icon, children, path } (path)}
|
||||
{@const isFolder = children !== undefined}
|
||||
{@const isRoot = level === 1}
|
||||
|
||||
<li style:margin-inline-start={isRoot ? '' : '1rem'}>
|
||||
<button
|
||||
data-file={!hasChildren}
|
||||
data-file={!isFolder}
|
||||
class:selected={$isSelected(path)}
|
||||
use:melt={$item({
|
||||
id: path,
|
||||
hasChildren
|
||||
hasChildren: isFolder
|
||||
})}>
|
||||
{#if icon === 'folder' && hasChildren && $isExpanded(path)}
|
||||
{#if icon === 'folder' && isFolder && $isExpanded(path)}
|
||||
<Icon icon={icons['folderOpen']} />
|
||||
{:else}
|
||||
<Icon icon={icons[icon]} />
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
|
||||
type Props = {
|
||||
files: string[];
|
||||
onopenfile?: (path: string) => void;
|
||||
onopenfile: (path: string) => void;
|
||||
onopenfolder: (path: string) => void;
|
||||
};
|
||||
let { files, onopenfile = null }: Props = $props();
|
||||
let { files, onopenfile, onopenfolder = null }: Props = $props();
|
||||
|
||||
const ctx = createTreeView();
|
||||
setContext('tree', ctx);
|
||||
@@ -17,12 +18,14 @@
|
||||
elements: { tree },
|
||||
states: { selectedItem }
|
||||
} = ctx;
|
||||
|
||||
const items = $derived(treeFromFilesystem(files));
|
||||
|
||||
$effect(() => {
|
||||
if ($selectedItem && onopenfile) {
|
||||
if ($selectedItem) {
|
||||
if ($selectedItem.dataset['file'] === 'true') {
|
||||
onopenfile($selectedItem.dataset['id']);
|
||||
} else {
|
||||
onopenfolder($selectedItem.dataset['id']);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type FileSystem = Record<string, string>;
|
||||
export type FileSystem = string[];
|
||||
|
||||
export const filesystem = writable<FileSystem>({});
|
||||
export const filesystem = writable<FileSystem>([]);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
type WebSocketEvent = 'connect' | 'disconnect' | 'reconnect';
|
||||
type SynapseMessageType = 'terminal' | 'fs';
|
||||
type SynapseMessageType = 'terminal' | 'fs' | 'synapse';
|
||||
type SynapseMessageOperations = {
|
||||
operation: 'updateWorkDir';
|
||||
params: { workdir: string };
|
||||
};
|
||||
type SynapseMessageOperationFileSystem =
|
||||
| {
|
||||
operation: 'createFile' | 'updateFile';
|
||||
@@ -28,7 +32,7 @@ type SynapseResponseType = `${SynapseMessageType}Response`;
|
||||
type Events = WebSocketEvent | SynapseResponseType;
|
||||
type BaseMessage = {
|
||||
success: boolean;
|
||||
data: string;
|
||||
data: unknown;
|
||||
type: SynapseResponseType;
|
||||
requestId: string;
|
||||
};
|
||||
@@ -41,6 +45,7 @@ export class Synapse {
|
||||
> = {};
|
||||
private reconnectAttempts = 0;
|
||||
private maxReconnectAttempts = 10;
|
||||
private requestCounter = 0;
|
||||
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
public isReconnecting = $state(false);
|
||||
|
||||
@@ -112,23 +117,41 @@ export class Synapse {
|
||||
}
|
||||
}
|
||||
|
||||
public dispatch<T extends SynapseMessageType>(
|
||||
public async dispatch<T extends SynapseMessageType>(
|
||||
type: T,
|
||||
payload: {
|
||||
synapse: SynapseMessageOperations;
|
||||
fs: SynapseMessageOperationFileSystem;
|
||||
terminal: SynapseMessageOperationTerminal;
|
||||
}[T]
|
||||
) {
|
||||
): Promise<BaseMessage> {
|
||||
const requestId = String(Date.now().toString() + ++this.requestCounter);
|
||||
|
||||
const message = {
|
||||
type,
|
||||
operation: payload.operation,
|
||||
params: payload.params,
|
||||
requestId: Date.now().toString()
|
||||
requestId
|
||||
};
|
||||
|
||||
const response = new Promise<BaseMessage>((resolve, reject) => {
|
||||
if (type === 'terminal') resolve(null);
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Request timed out'));
|
||||
}, 5000);
|
||||
const callback = (message: MessageEvent<string>) => {
|
||||
const response = JSON.parse(message.data);
|
||||
if (!this.isMessage(response)) return;
|
||||
if (response.requestId === requestId) {
|
||||
resolve(response);
|
||||
clearTimeout(timeout);
|
||||
this.ws.removeEventListener('message', callback);
|
||||
}
|
||||
};
|
||||
this.ws.addEventListener('message', callback);
|
||||
});
|
||||
this.ws.send(JSON.stringify(message));
|
||||
|
||||
return message;
|
||||
return response;
|
||||
}
|
||||
public addEventListener(
|
||||
event: Events,
|
||||
|
||||
@@ -35,11 +35,12 @@
|
||||
});
|
||||
synapse.addEventListener('terminalResponse', ({ message }) => {
|
||||
const { data } = message;
|
||||
term.write(data);
|
||||
if (typeof data === 'string') term.write(data);
|
||||
});
|
||||
const observer = new ResizeObserver(() => {
|
||||
const { cols, rows } = fitAddon.proposeDimensions();
|
||||
if (term.cols === cols && term.rows === rows) return;
|
||||
if (!Number.isInteger(cols) || !Number.isInteger(rows)) return;
|
||||
term.resize(cols, rows);
|
||||
synapse.dispatch('terminal', {
|
||||
operation: 'updateSize',
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
import { filesystem } from '$lib/components/editor/filesystem';
|
||||
import { previewFrameRef } from '$routes/(console)/project-[project]/store';
|
||||
import { getChatWidthFromPrefs, saveChatWidthToPrefs } from '$lib/helpers/studioLayout';
|
||||
import { synapse } from '$lib/components/studio/synapse.svelte';
|
||||
|
||||
let hasProjectSidebar = $state(false);
|
||||
|
||||
@@ -214,6 +215,13 @@
|
||||
...$filesystem,
|
||||
[action.src]: action.content
|
||||
};
|
||||
synapse.dispatch('fs', {
|
||||
operation: 'createFile',
|
||||
params: {
|
||||
filepath: action.src,
|
||||
content: action.content
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { synapse } from '$lib/components/studio/synapse.svelte';
|
||||
import { showChat } from '$lib/stores/chat';
|
||||
import { default as IconChatLayout } from '../assets/chat-layout.svelte';
|
||||
import { filesystem } from '$lib/components/editor/filesystem';
|
||||
|
||||
const { children } = $props();
|
||||
|
||||
@@ -38,7 +39,31 @@
|
||||
hasChildren: true
|
||||
}
|
||||
];
|
||||
synapse.dispatch('synapse', {
|
||||
operation: 'updateWorkDir',
|
||||
params: {
|
||||
workdir: `/artifact/${artifactId}`
|
||||
}
|
||||
});
|
||||
synapse
|
||||
.dispatch('fs', {
|
||||
operation: 'getFolder',
|
||||
params: {
|
||||
folderpath: '.'
|
||||
}
|
||||
})
|
||||
.then((message) => {
|
||||
const data = message.data as Array<{ name: string; isDirectory: boolean }>;
|
||||
if (!Array.isArray(data)) return;
|
||||
for (const { name, isDirectory } of data) {
|
||||
const key = isDirectory ? name + '/' : name;
|
||||
filesystem.update((n) => {
|
||||
n.push(key);
|
||||
|
||||
return n;
|
||||
});
|
||||
}
|
||||
});
|
||||
let terminalOpen = $state(getTerminalOpenFromPrefs());
|
||||
let asideRef: HTMLElement;
|
||||
let isResizing = false;
|
||||
|
||||
+28
-5
@@ -2,8 +2,7 @@
|
||||
import Editor from '$lib/components/editor/editor.svelte';
|
||||
import { filesystem } from '$lib/components/editor/filesystem';
|
||||
import Filesystem from '$lib/components/editor/filesystem.svelte';
|
||||
|
||||
const files = $derived(Object.keys($filesystem));
|
||||
import { synapse } from '$lib/components/studio/synapse.svelte';
|
||||
|
||||
let instance: Editor;
|
||||
|
||||
@@ -15,13 +14,37 @@
|
||||
return 'file';
|
||||
}
|
||||
|
||||
function openFile(path: string) {
|
||||
instance?.loadCode($filesystem[path], getLanguageFromExtensions(path));
|
||||
async function openFile(path: string) {
|
||||
const message = await synapse.dispatch('fs', {
|
||||
operation: 'getFile',
|
||||
params: {
|
||||
filepath: path
|
||||
}
|
||||
});
|
||||
instance?.loadCode(message.data as string, getLanguageFromExtensions(path));
|
||||
}
|
||||
async function openFolder(path: string) {
|
||||
const message = await synapse.dispatch('fs', {
|
||||
operation: 'getFolder',
|
||||
params: {
|
||||
folderpath: path
|
||||
}
|
||||
});
|
||||
const data = message.data as Array<{ name: string; isDirectory: boolean }>;
|
||||
if (!Array.isArray(data)) return;
|
||||
for (const { name, isDirectory } of data) {
|
||||
const key = isDirectory ? name + '/' : name;
|
||||
filesystem.update((n) => {
|
||||
n.push(path + '/' + key);
|
||||
console.log(path + '/' + key);
|
||||
return n;
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<Filesystem {files} onopenfile={openFile} />
|
||||
<Filesystem files={$filesystem} onopenfile={openFile} onopenfolder={openFolder} />
|
||||
<Editor bind:this={instance} />
|
||||
</main>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user