Skip to content

Commit a19cd9e

Browse files
feat(project): display files tree (#97)
Co-authored-by: Sylvain Marroufin <marroufin.sylvain@gmail.com>
1 parent a832b2f commit a19cd9e

File tree

9 files changed

+277
-15
lines changed

9 files changed

+277
-15
lines changed

components/molecules/FilesTree.vue

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<template>
2+
<ul>
3+
<li v-for="file of files" :key="file.path">
4+
<div
5+
class="group border-r-2 py-2 pr-6 flex items-center text-sm font-medium focus:u-bg-gray-50 focus:outline-none w-full cursor-pointer"
6+
:class="{
7+
[`pl-${6 + (level * 3)}`]: true,
8+
'u-bg-gray-50 u-border-gray-800 u-text-gray-900': isSelected(file),
9+
'border-transparent u-text-gray-500 hover:u-text-gray-900 hover:u-bg-gray-50': !isSelected(file)
10+
}"
11+
@click="selectFile(file)"
12+
>
13+
<div class="flex items-center justify-between">
14+
<div class="flex items-center truncate">
15+
<FilesTreeIcon :file="file" class="mr-1.5" />
16+
<div class="line-clamp-1" :class="{ 'line-through': file.isDeleted }">
17+
{{ file.name }}
18+
</div>
19+
</div>
20+
<FilesTreeIndicator :file="file" />
21+
</div>
22+
</div>
23+
24+
<FilesTree
25+
v-if="isDir(file)"
26+
v-show="isOpen(file)"
27+
:level="level + 1"
28+
:files="file.children"
29+
:selected-file="selectedFile"
30+
@selectFile="file => $emit('selectFile', file)"
31+
/>
32+
</li>
33+
</ul>
34+
</template>
35+
36+
<script setup lang="ts">
37+
import { PropType } from 'vue'
38+
import type { File } from '~/types'
39+
40+
const props = defineProps({
41+
level: {
42+
type: Number,
43+
default: 0
44+
},
45+
files: {
46+
type: Array as PropType<File[]>,
47+
default: () => []
48+
},
49+
selectedFile: {
50+
type: Object as PropType<File>,
51+
default: null
52+
}
53+
})
54+
55+
const emit = defineEmits(['selectFile'])
56+
57+
// Methods
58+
const isDir = (file: File) => file.type === 'directory'
59+
const isOpen = (file: File) => file.isOpen
60+
const isSelected = (file: File) => props.selectedFile && file.path === props.selectedFile.path
61+
const selectFile = (file: File) => {
62+
// Prevent click when clicking on already selected file
63+
if (props.selectedFile && file.path === props.selectedFile.path) {
64+
return
65+
}
66+
67+
if (isDir(file)) {
68+
file.isOpen = !file.isOpen
69+
return
70+
}
71+
72+
emit('selectFile', file)
73+
}
74+
</script>
75+
76+
<style scoped>
77+
.drag-over {
78+
background-color: var(--tw-ring-color);
79+
}
80+
81+
li.drag-below {
82+
border-bottom-color: var(--tw-ring-color) !important;
83+
}
84+
li.drag-below + li {
85+
border-top-color: var(--tw-ring-color) !important;
86+
}
87+
88+
li.drag-above {
89+
border-top-color: var(--tw-ring-color) !important;
90+
}
91+
92+
ul.divide-y-2 > li:first-child {
93+
@apply border-t-2 border-transparent;
94+
}
95+
ul.divide-y-2 > li:last-child {
96+
@apply border-b-2 border-transparent;
97+
}
98+
</style>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<template>
2+
<UIcon
3+
:key="iconName"
4+
:name="iconName"
5+
class="w-4 h-4 flex-shrink-0"
6+
/>
7+
</template>
8+
9+
<script setup lang="ts">
10+
import { PropType } from 'vue'
11+
import type { File } from '~/types'
12+
13+
const props = defineProps({
14+
file: {
15+
type: Object as PropType<File>,
16+
default: null
17+
}
18+
})
19+
20+
const iconName = computed(() => {
21+
if (props.file.type === 'directory') {
22+
return `heroicons-outline:${props.file.isOpen ? 'folder-open' : 'folder'}`
23+
}
24+
return 'heroicons-outline:document-text'
25+
})
26+
</script>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<template>
2+
<div
3+
v-if="indicator"
4+
class="flex items-center justify-center border border-transparent"
5+
>
6+
<span
7+
class="w-2 h-2 m-1 rounded-full"
8+
:class="indicator"
9+
/>
10+
</div>
11+
</template>
12+
13+
<script setup lang="ts">
14+
import { PropType } from 'vue'
15+
import type { File } from '~/types'
16+
17+
const props = defineProps({
18+
file: {
19+
type: Object as PropType<File>,
20+
default: null
21+
}
22+
})
23+
24+
const indicator = computed(() => {
25+
if (props.file.isDeleted) { return 'bg-red-500' }
26+
if (props.file.isDraft) { return 'bg-gray-700' }
27+
if (props.file.isRenamed) { return 'bg-blue-500' }
28+
if (props.file.isAdded) { return 'bg-green-500' }
29+
if (props.file.isModified) { return 'bg-yellow-500' }
30+
return null
31+
})
32+
</script>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<template>
2+
<UIcon
3+
v-if="isDir"
4+
name="heroicons-outline:chevron-right"
5+
class="transform transition opacity-50 w-3 h-3 flex-shrink-0"
6+
:class="file.isOpen ? 'rotate-90' : 'rotate-0'"
7+
/>
8+
</template>
9+
10+
<script setup lang="ts">
11+
import { PropType } from 'vue'
12+
import type { File } from '~/types'
13+
14+
const props = defineProps({
15+
file: {
16+
type: Object as PropType<File>,
17+
default: null
18+
}
19+
})
20+
21+
const isDir = props.file.type === 'directory'
22+
</script>

components/organisms/project/ProjectPage.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
<main class="flex-1 flex overflow-hidden">
33
<!-- Primary column -->
44
<section class="min-w-0 flex-1 h-full overflow-y-auto lg:order-last">
5-
<div class="h-full p-4 sm:p-6 lg:p-8 xl:p-10">
5+
<div class="h-full p-4 sm:p-6 lg:p-8 xl:p-10 overflow-y-auto">
66
<slot />
77
</div>
88
</section>
99

1010
<!-- Secondary column (hidden on smaller screens) -->
1111
<aside v-if="$slots.aside" class="hidden lg:block lg:flex-shrink-0 lg:order-first">
1212
<div class="h-full relative flex flex-col w-72 border-r u-border-gray-200 u-bg-white overflow-y-auto">
13-
<div class="flex-shrink-0 h-16 px-6 flex items-center">
13+
<div class="flex-shrink-0 h-16 px-6 flex items-center sticky top-0 u-bg-white">
1414
<p class="text-lg font-semibold text-blue-gray-900">
1515
{{ title }}
1616
</p>

nuxt.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ export default defineNuxtConfig({
6666
},
6767
tailwindcss: {
6868
config: {
69-
content: ['presets/*.ts']
69+
content: ['presets/*.ts'],
70+
safelist: ['pl-6', 'pl-9', 'pl-12', 'pl-15', 'pl-18']
7071
}
7172
}
7273
})

pages/@[team]/[project]/content.vue

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
<template>
22
<ProjectPage title="Content">
33
<template #aside>
4-
<div class="px-6">
5-
Files...
6-
</div>
4+
<FilesTree :files="files" :selected-file="selectedFile" @select-file="selectFile" />
75
</template>
86

9-
Editor
7+
<div class="whitespace-pre break-words text-sm font-mono focus:outline-none" contenteditable>
8+
{{ content }}
9+
</div>
1010
</ProjectPage>
1111
</template>
1212

1313
<script setup lang="ts">
14-
import type { PropType } from 'vue'
15-
import type { Team, Project } from '~/types'
14+
import type { PropType, Ref } from 'vue'
15+
import type { Team, Project, File } from '~/types'
1616
17-
defineProps({
17+
const props = defineProps({
1818
team: {
1919
type: Object as PropType<Team>,
2020
default: null
@@ -24,4 +24,30 @@ defineProps({
2424
required: true
2525
}
2626
})
27+
28+
const client = useStrapiClient()
29+
30+
const { data: files } = await useAsyncData('files', () => client<File[]>(`/projects/${props.project.id}/tree`))
31+
32+
const selectedFile: Ref<File> = ref(files.value.find(file => file.path.toLowerCase().endsWith('index.md')) || files.value.find(file => file.type === 'file'))
33+
34+
const content = ref('')
35+
36+
function selectFile (file) {
37+
selectedFile.value = file
38+
}
39+
40+
watch(selectedFile, async () => {
41+
if (!selectedFile.value) {
42+
return
43+
}
44+
45+
const { content: fetchedContent } = await client(`/projects/${props.project.id}/file`, {
46+
params: {
47+
path: selectedFile.value.path
48+
}
49+
})
50+
51+
content.value = fetchedContent
52+
}, { immediate: true })
2753
</script>

types/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,20 @@ export interface Project {
7979
user: User
8080
}
8181

82+
export interface File {
83+
type: 'file' | 'directory'
84+
path: string
85+
name: string
86+
content?: string
87+
children?: File[]
88+
isOpen?: boolean
89+
isDraft?: boolean
90+
isAdded?: boolean
91+
isRenamed?: boolean
92+
isModified?: boolean
93+
isDeleted?: boolean
94+
}
95+
8296
export interface GitHubAccount {
8397
login: string
8498
avatar_url: string

yarn.lock

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@
421421
resolved "https://registry.yarnpkg.com/@nuxt/devalue/-/devalue-2.0.0.tgz#c7bd7e9a516514e612d5d2e511ffc399e0eac322"
422422
integrity sha512-YBI/6o2EBz02tdEJRBK8xkt3zvOFOWlLBf7WKYGBsSYSRtjjgrqPe2skp6VLLmKx5WbHHDNcW+6oACaurxGzeA==
423423

424-
"@nuxt/kit@npm:@nuxt/kit-edge@3.0.0-27434755.39f7eb2", "@nuxt/kit@npm:@nuxt/kit-edge@latest":
424+
"@nuxt/kit@npm:@nuxt/kit-edge@3.0.0-27434755.39f7eb2":
425425
version "3.0.0-27434755.39f7eb2"
426426
resolved "https://registry.yarnpkg.com/@nuxt/kit-edge/-/kit-edge-3.0.0-27434755.39f7eb2.tgz#9c3409af5e06787e0c86947035a8cc041a26d4ce"
427427
integrity sha512-i7ubhi461PaEqmYf3ohwyLa2Y+dFr0xhLmAofn4TK1HuS11z+1Wy3VBKBYJiOd7fPe0y09E3jzRuKfRTOdd7Fg==
@@ -444,6 +444,29 @@
444444
unctx "^1.0.2"
445445
untyped "^0.3.0"
446446

447+
"@nuxt/kit@npm:@nuxt/kit-edge@latest":
448+
version "3.0.0-27436017.7b6252a"
449+
resolved "https://registry.yarnpkg.com/@nuxt/kit-edge/-/kit-edge-3.0.0-27436017.7b6252a.tgz#6d4c92d192f6a23b3be628aac10ced24230e8a54"
450+
integrity sha512-93MOq97ClDgoHd3W2PfkpSNvfE7a2OU03B9IXosao68FzNK6X6MzivgC8hYjc4FMBgW+dnT7507Hl31xrNcNuQ==
451+
dependencies:
452+
"@nuxt/schema" "npm:@nuxt/schema-edge@3.0.0-27436017.7b6252a"
453+
c12 "^0.1.3"
454+
consola "^2.15.3"
455+
defu "^5.0.1"
456+
globby "^13.1.1"
457+
hash-sum "^2.0.0"
458+
ignore "^5.2.0"
459+
jiti "^1.13.0"
460+
knitwork "^0.1.0"
461+
lodash.template "^4.5.0"
462+
mlly "^0.4.3"
463+
pathe "^0.2.0"
464+
pkg-types "^0.3.2"
465+
scule "^0.2.1"
466+
semver "^7.3.5"
467+
unctx "^1.0.2"
468+
untyped "^0.3.0"
469+
447470
"@nuxt/nitro@npm:@nuxt/nitro-edge@3.0.0-27434755.39f7eb2":
448471
version "3.0.0-27434755.39f7eb2"
449472
resolved "https://registry.yarnpkg.com/@nuxt/nitro-edge/-/nitro-edge-3.0.0-27434755.39f7eb2.tgz#c8f67bb5b1bb8d3377e0f7a10c5184addfbd0233"
@@ -537,6 +560,21 @@
537560
std-env "^3.0.1"
538561
ufo "^0.7.11"
539562

563+
"@nuxt/schema@npm:@nuxt/schema-edge@3.0.0-27436017.7b6252a":
564+
version "3.0.0-27436017.7b6252a"
565+
resolved "https://registry.yarnpkg.com/@nuxt/schema-edge/-/schema-edge-3.0.0-27436017.7b6252a.tgz#9da899b44546aef0644b8a19ebc976d9562042af"
566+
integrity sha512-wCUYcs7bxEm4J/SKauoB5a+V2vWwiqsJM/foM+absECxHAOtuKUQAmzuqhu7xaSdab9RGLmUuHVIfEzmiiTzcw==
567+
dependencies:
568+
c12 "^0.1.3"
569+
create-require "^1.1.1"
570+
defu "^5.0.1"
571+
jiti "^1.13.0"
572+
pathe "^0.2.0"
573+
postcss-import-resolver "^2.0.0"
574+
scule "^0.2.1"
575+
std-env "^3.0.1"
576+
ufo "^0.7.11"
577+
540578
"@nuxt/vite-builder@npm:@nuxt/vite-builder-edge@3.0.0-27434755.39f7eb2":
541579
version "3.0.0-27434755.39f7eb2"
542580
resolved "https://registry.yarnpkg.com/@nuxt/vite-builder-edge/-/vite-builder-edge-3.0.0-27434755.39f7eb2.tgz#80b4f54fa38dbd98da925f2d4ccbdf8a700ee167"
@@ -567,9 +605,9 @@
567605
vite "^2.8.5"
568606

569607
"@nuxthq/ui@npm:@nuxthq/ui-edge@latest":
570-
version "0.0.2-27435844.4d0709a"
571-
resolved "https://registry.yarnpkg.com/@nuxthq/ui-edge/-/ui-edge-0.0.2-27435844.4d0709a.tgz#6f481cf6394b20c63a947dd646af74a31d043cbe"
572-
integrity sha512-Zyf6KAW1B5yCYU4DTBpmZsdVb6tendBJMLntMX6kphDrClIhBStd1RNgTyxU0NMi1NT4PXgZFjPQvBi48HhZuQ==
608+
version "0.0.2-27435863.da3ed26"
609+
resolved "https://registry.yarnpkg.com/@nuxthq/ui-edge/-/ui-edge-0.0.2-27435863.da3ed26.tgz#bae1cbf8afa9b22bdb0fc401d5475f0b324baaa2"
610+
integrity sha512-aoTZsMEdQphGK6H6jS6FPWrVrA7shNYI5/iolZpn6A7aB1DcTOgIt3TPtX2rgZC0jZxVUu2GedCkTZErLIswYA==
573611
dependencies:
574612
"@headlessui/vue" "1.4.3"
575613
"@iconify/vue" "^3.1.3"
@@ -3458,11 +3496,16 @@ has-flag@^4.0.0:
34583496
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
34593497
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
34603498

3461-
has-symbols@^1.0.1, has-symbols@^1.0.2:
3499+
has-symbols@^1.0.1:
34623500
version "1.0.2"
34633501
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
34643502
integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
34653503

3504+
has-symbols@^1.0.2:
3505+
version "1.0.3"
3506+
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
3507+
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
3508+
34663509
has-tostringtag@^1.0.0:
34673510
version "1.0.0"
34683511
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"

0 commit comments

Comments
 (0)