Skip to content
This repository was archived by the owner on Mar 30, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions components/CodePanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<div class="bg-zinc-900 p-4">
<pre>{{ code }}</pre>
</div>
</template>

<script setup lang="ts">
defineProps<{
code: string;
}>();
</script>
31 changes: 31 additions & 0 deletions components/FileTree.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<template>
<FileTreeFolder :root="root" :load-directory="loadDirectory" @click-item="$emit('clickItem', $event)" />
</template>

<script setup lang="ts">
import { Directory, setDirectoryRecursive } from './file-tree-utils';

const props = defineProps<{
loadDirectory: (path: string) => Promise<Directory>;
}>();

defineEmits<{
(event: 'clickItem', o: { path: string; type: 'file' | 'directory' }): void;
}>();

const root = ref<Directory>({
path: '/',
name: '/',
type: 'directory',
children: [],
});

async function loadDirectory(path: string) {
const directory = await props.loadDirectory(path);
root.value = setDirectoryRecursive(root.value, directory);
}

onMounted(async () => {
await loadDirectory('/');
});
</script>
57 changes: 57 additions & 0 deletions components/FileTreeFolder.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<template>
<div class="flex flex-col overflow-y">
<div v-for="c in root.children" :key="c.name">
<div
v-if="c.type === 'file'"
class="flex items-center gap-1 w-full cursor-pointer hover:bg-zinc-200 hover:dark:bg-zinc-700 rounded-md"
@click="$emit('clickItem', { path: c.path, type: 'file' })"
>
<UIcon name="i-heroicons-document" />
<span>{{ c.name }}</span>
</div>
<div v-else>
<div
class="flex items-center cursor-pointer hover:bg-zinc-200 hover:dark:bg-zinc-700 rounded-md gap-1 w-full"
@click="_loadDirectory(c)"
>
<UIcon v-if="isOpened[c.path]" name="i-heroicons-folder-open" />
<UIcon v-else name="i-heroicons-folder-20-solid" />
<span>{{ c.name }}</span>
</div>
<FileTreeFolder
v-if="isOpened[c.path]"
:root="c"
class="ml-2"
:load-directory="loadDirectory"
@click-item="$emit('clickItem', $event)"
/>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import type { Directory } from './file-tree-utils';

const props = defineProps<{
root: Directory;
loadDirectory: (path: string) => Promise<Directory>;
}>();

const emit = defineEmits<{
(event: 'clickItem', o: { path: string; type: 'file' | 'directory' }): void;
}>();

const isOpened = ref({} as Record<string, boolean>);

async function _loadDirectory(d: Directory) {
if (d.children && d.children.length > 0) {
isOpened.value[d.path] = !isOpened.value[d.path];
emit('clickItem', { path: d.path, type: 'directory' });
return;
}

await props.loadDirectory(d.path);
isOpened.value[d.path] = true;
}
</script>
43 changes: 43 additions & 0 deletions components/RepoExplorer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<template>
<div class="flex gap-4">
<div class="w-48 overflow flex-shrink-0">
<FileTree :load-directory="loadDirectory" @click-item="clickTreeItem" />
</div>
<div v-if="code" class="flex-grow">
<CodePanel :code="code" />
</div>
</div>
</template>

<script setup lang="ts">
const props = defineProps<{
repoId: string;
}>();

const repoId = toRef(props, 'repoId');
const code = ref<string>();

async function loadDirectory(path: string) {
return await $fetch(`/api/repos/${repoId.value}/files/tree`, {
query: {
path,
},
});
}

async function clickTreeItem({ path, type }: { path: string; type: 'file' | 'directory' }) {
if (type === 'directory') {
return;
}

code.value = undefined;

const c = await $fetch(`/api/repos/${repoId.value}/files/file`, {
query: {
path,
},
});

code.value = c;
}
</script>
94 changes: 94 additions & 0 deletions components/__snapshots__/file-tree.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`file-tree > should add a sub-sub-folder while not having the sub-folder yet 1`] = `
{
"children": [
{
"name": "file1.txt",
"path": "/file1.txt",
"type": "file",
},
{
"children": [
{
"children": [
{
"children": [
{
"name": "file4.txt",
"path": "/folder1/folder2/folder3/file4.txt",
"type": "file",
},
],
"name": "folder3",
"path": "/folder1/folder2/folder3",
"type": "directory",
},
],
"name": "folder2",
"path": "/folder1/folder2",
"type": "directory",
},
],
"name": "folder1",
"path": "/folder1",
"type": "directory",
},
],
"name": "root",
"path": "/",
"type": "directory",
}
`;

exports[`file-tree > should replace a sub-folder 1`] = `
{
"children": [
{
"name": "file1.txt",
"path": "/file1.txt",
"type": "file",
},
{
"children": [
{
"name": "file1.txt",
"path": "/folder1/subfile1.txt",
"type": "file",
},
{
"name": "folder2",
"path": "/folder1/folder2",
"type": "directory",
},
],
"name": "folder1",
"path": "/folder1",
"type": "directory",
},
],
"name": "root",
"path": "/",
"type": "directory",
}
`;

exports[`file-tree > should replace root folder 1`] = `
{
"children": [
{
"name": "file1.txt",
"path": "/file1.txt",
"type": "file",
},
{
"name": "folder1",
"path": "/folder1",
"type": "directory",
},
],
"name": "root",
"path": "/",
"type": "directory",
}
`;
77 changes: 77 additions & 0 deletions components/file-tree-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { expect, describe, it } from 'vitest';
import { Directory, setDirectoryRecursive } from './file-tree';

describe('file-tree', () => {
const fileTree: Directory = {
name: '/',
path: '/',
type: 'directory',
};

const dir: Directory = {
name: 'root',
path: '/',
type: 'directory',
children: [
{
type: 'file',
name: 'file1.txt',
path: '/file1.txt',
},
{
type: 'directory',
name: 'folder1',
path: '/folder1',
},
],
};

const subDir: Directory = {
type: 'directory',
name: 'folder1',
path: '/folder1',
children: [
{
type: 'file',
name: 'file1.txt',
path: '/folder1/subfile1.txt',
},
{
type: 'directory',
name: 'folder2',
path: '/folder1/folder2',
},
],
};

const subSubSubDir: Directory = {
type: 'directory',
name: 'folder3',
path: '/folder1/folder2/folder3',
children: [
{
type: 'file',
name: 'file4.txt',
path: '/folder1/folder2/folder3/file4.txt',
},
],
};

it('should replace root folder', () => {
const result = setDirectoryRecursive(fileTree, dir);

expect(result).toMatchSnapshot();
});

it('should replace a sub-folder', () => {
const result = setDirectoryRecursive(dir, subDir);

expect(result).toMatchSnapshot();
});

it('should add a sub-sub-folder while not having the sub-folder yet', () => {
const result = setDirectoryRecursive(dir, subSubSubDir);

expect(result).toMatchSnapshot();
});
});
49 changes: 49 additions & 0 deletions components/file-tree-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export type File = {
path: string;
name: string;
type: 'file';
};

export type Directory = {
path: string;
name: string;
type: 'directory';
children?: (Directory | File)[];
};

export function setDirectoryRecursive(parent: Directory, directory: Directory): Directory {
if (parent.path === directory.path) {
return directory;
}

let children = parent.children || [];

if (!children.find((c) => directory.path.startsWith(c.path))) {
const a = directory.path.replace(new RegExp(`^${parent.path}\/`), '').split('/');
const name = a[0];
if (name !== '') {
children.push({
path: `${parent.path}/${name}`,
name,
type: 'directory',
children: [],
});

console.log('should add', directory.path, name);
}
}

children = children.map((c) => {
if (c.type === 'file') {
return c;
}

if (parent.path === directory.path) {
return directory;
}

return setDirectoryRecursive(c, directory);
});

return { ...parent, children };
}
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"clean": "rm -rf dist/ node_modules/ .eslintcache",
"format:check": "prettier --check .",
"format:fix": "prettier --write .",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"test": "vitest",
"test:watch": "vitest --watch --ui"
},
"devDependencies": {
"@iconify/json": "^2.2.106",
Expand All @@ -25,6 +27,7 @@
"@types/better-sqlite3": "^7.6.4",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^18",
"@vitest/ui": "^0.34.3",
"better-sqlite3": "^8.5.0",
"dotenv": "^16.3.1",
"drizzle-kit": "^0.19.12",
Expand All @@ -33,7 +36,8 @@
"nuxt": "^3.6.5",
"prettier": "^3.0.1",
"tsx": "^3.12.7",
"typescript": "^5.1.6"
"typescript": "^5.1.6",
"vitest": "^0.34.3"
},
"dependencies": {
"@gitbeaker/core": "^39.10.3",
Expand Down
Loading