Rework file access and link archiving
This commit is contained in:
parent
f7094f7ce1
commit
602b0af212
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<HoverCardRoot :open-delay="delay">
|
<HoverCardRoot :open-delay="delay" @update:open="(...args) => emits('open', ...args)">
|
||||||
<HoverCardTrigger class="inline-block cursor-help outline-none">
|
<HoverCardTrigger class="inline-block cursor-help outline-none">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
|
|
@ -18,4 +18,6 @@ const { delay = 500, disabled = false, side = 'bottom' } = defineProps<{
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
side?: 'top' | 'right' | 'bottom' | 'left'
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const emits = defineEmits(['open'])
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1,29 +1,31 @@
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink class="text-accent-blue inline-flex items-center" v-if="data && data[0]"
|
<NuxtLink class="text-accent-blue inline-flex items-center" v-if="overview"
|
||||||
:to="{ name: 'explore-path', params: { path: data[0].path }, hash: hash }" :class="class">
|
:to="{ name: 'explore-path', params: { path: overview.path }, hash: hash }" :class="class">
|
||||||
<HoverCard class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': data[0].type === 'canvas'}">
|
<HoverCard class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview.type === 'canvas'}" @open="load">
|
||||||
<template #content>
|
<template #content>
|
||||||
<template v-if="data[0].type === 'markdown'">
|
<template v-if="loading">
|
||||||
|
<Loading />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="overview.type === 'markdown'">
|
||||||
<div class="px-10">
|
<div class="px-10">
|
||||||
<Markdown :content="data[0].content" />
|
<Markdown :content="content!.content" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="data[0].type === 'canvas'">
|
<template v-else-if="overview.type === 'canvas'">
|
||||||
<div class="w-[600px] h-[600px] relative">
|
<div class="w-[600px] h-[600px] relative">
|
||||||
<Canvas :canvas="JSON.parse(data[0].content)" />
|
<Canvas :canvas="JSON.parse(content!.content)" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<slot v-bind="$attrs"></slot>
|
<slot v-bind="$attrs"></slot>
|
||||||
<Icon class="w-4 h-4 inline-block" v-if="data && data[0] && data[0].type !== 'markdown'" :icon="iconByType[data[0].type]" />
|
<Icon class="w-4 h-4 inline-block" v-if="overview && overview.type !== 'markdown'" :icon="iconByType[overview.type]" />
|
||||||
</template>
|
</template>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink v-else-if="href" :to="href" :class="class" class="text-accent-blue inline-flex items-center">
|
<NuxtLink v-else-if="href" :to="href" :class="class" class="text-accent-blue inline-flex items-center">
|
||||||
<slot v-bind="$attrs"></slot>
|
<slot v-bind="$attrs"></slot>
|
||||||
<Icon class="w-4 h-4 inline-block" v-if="data && data[0] && data[0].type !== 'markdown'" :height="20" :width="20"
|
<Icon class="w-4 h-4 inline-block" v-if="overview && overview.type !== 'markdown'" :height="20" :width="20" :icon="iconByType[overview.type]" />
|
||||||
:icon="`icons/link-${data[0].type.toLowerCase()}`" />
|
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<slot :class="class" v-else v-bind="$attrs"></slot>
|
<slot :class="class" v-else v-bind="$attrs"></slot>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -39,17 +41,35 @@ const { href } = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { hash, pathname, protocol } = parseURL(href);
|
const { hash, pathname, protocol } = parseURL(href);
|
||||||
const data = ref(), loading = ref(false);
|
const overview = ref<{
|
||||||
|
path: string;
|
||||||
|
owner: number;
|
||||||
|
title: string;
|
||||||
|
type: "file" | "folder" | "markdown" | "canvas";
|
||||||
|
navigable: boolean;
|
||||||
|
private: boolean;
|
||||||
|
order: number;
|
||||||
|
visit: number;
|
||||||
|
}>(), content = ref<{
|
||||||
|
content: string;
|
||||||
|
}>(), loading = ref(false), fetched = ref(false);
|
||||||
|
|
||||||
if(!!pathname && !protocol)
|
if(!!pathname && !protocol)
|
||||||
{
|
{
|
||||||
|
try {
|
||||||
|
overview.value = await $fetch(`/api/file/overview/${encodeURIComponent(pathname)}`);
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load()
|
||||||
|
{
|
||||||
|
if(fetched.value === true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
fetched.value = true;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
data.value = await $fetch(`/api/file`, {
|
content.value = await $fetch(`/api/file/content/${encodeURIComponent(pathname)}`);
|
||||||
query: {
|
|
||||||
search: `%${pathname}`
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch(e) { }
|
} catch(e) { }
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -55,7 +55,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 xl:px-6 px-3 max-w-full max-h-full overflow-y-auto overflow-x-hidden">
|
<div class="flex-1 xl:px-6 px-3 max-w-full max-h-full overflow-y-auto overflow-x-hidden">
|
||||||
<NuxtLink :href="{ name: 'explore' }" class="flex flex-1 font-bold text-lg items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" active-class="text-accent-blue border-s-2 !border-accent-blue">
|
<NuxtLink :href="{ name: 'explore-path', params: { path: 'index' } }" class="flex flex-1 font-bold text-lg items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" active-class="text-accent-blue border-s-2 !border-accent-blue">
|
||||||
<div class="pl-3 py-1 flex-1 truncate">Projet</div>
|
<div class="pl-3 py-1 flex-1 truncate">Projet</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<Tree v-if="pages" v-model="pages" :getKey="(item) => item.path">
|
<Tree v-if="pages" v-model="pages" :getKey="(item) => item.path">
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,46 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="status === 'pending'" class="flex">
|
<div v-if="overviewStatus === 'pending'" class="flex">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>d[any] - Chargement</Title>
|
<Title>d[any] - Chargement</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<Loading />
|
<Loading />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 justify-start items-start" v-else-if="page">
|
<div class="flex flex-1 justify-start items-start" v-else-if="overview">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>d[any] - {{ page.title }}</Title>
|
<Title>d[any] - {{ overview.title }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<template v-if="page.type === 'markdown'">
|
<template v-if="overview.type === 'markdown'">
|
||||||
<div class="flex flex-1 justify-start items-start flex-col xl:px-24 md:px-8 px-4 py-6">
|
<div class="flex flex-1 justify-start items-start flex-col xl:px-24 md:px-8 px-4 py-6">
|
||||||
<div class="flex flex-1 flex-row justify-between items-center">
|
<div class="flex flex-1 flex-row justify-between items-center">
|
||||||
<ProseH1>{{ page.title }}</ProseH1>
|
<ProseH1>{{ overview.title }}</ProseH1>
|
||||||
<NuxtLink :href="{ name: 'explore-edit-path', params: { path: path } }"><Button v-if="isOwner">Modifier</Button></NuxtLink>
|
<div class="flex gap-4">
|
||||||
|
<NuxtLink :href="{ name: 'explore-edit-path', params: { path: path } }" v-if="isOwner"><Button>Modifier</Button></NuxtLink>
|
||||||
|
<NuxtLink :href="{ name: 'explore-edit' }" v-if="isOwner && path === 'index'"><Button>Configurer le projet</Button></NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<Markdown :content="page.content" />
|
</div>
|
||||||
|
<Markdown v-if="content" :content="content.content" />
|
||||||
|
<Loading v-else />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="page.type === 'canvas'">
|
<template v-else-if="overview.type === 'canvas'">
|
||||||
<Canvas :canvas="JSON.parse(page.content)" />
|
<Canvas v-if="content" :canvas="JSON.parse(content.content)" />
|
||||||
|
<Loading v-else />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ProseH2 class="flex-1 text-center">Impossible d'afficher le contenu demandé</ProseH2>
|
<ProseH2 class="flex-1 text-center">Impossible d'afficher le contenu demandé</ProseH2>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="status === 'error'">
|
<div v-else-if="overviewStatus === 'error'">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>d[any] - Erreur</Title>
|
<Title>d[any] - Erreur</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<span>{{ error?.message }}</span>
|
<span>{{ overviewError?.message }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="contentStatus === 'error'">
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Erreur</Title>
|
||||||
|
</Head>
|
||||||
|
<span>{{ contentError?.message }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<Head>
|
<Head>
|
||||||
|
|
@ -43,15 +54,9 @@
|
||||||
const route = useRouter().currentRoute;
|
const route = useRouter().currentRoute;
|
||||||
const path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path);
|
const path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path);
|
||||||
|
|
||||||
watch(path, () => {
|
const { user } = useUserSession();
|
||||||
if(path.value === 'index')
|
|
||||||
{
|
|
||||||
useRouter().replace({ name: 'explore' });
|
|
||||||
}
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
const { loggedIn, user } = useUserSession();
|
const { data: overview, status: overviewStatus, error: overviewError } = await useFetch(`/api/file/overview/${encodeURIComponent(path.value)}`, { watch: [route, path], });
|
||||||
|
const { data: content, status: contentStatus, error: contentError } = await useFetch(`/api/file/content/${encodeURIComponent(path.value)}`, { watch: [route, path], });
|
||||||
const { data: page, status, error } = await useFetch(`/api/file/${encodeURIComponent(path.value)}`, { watch: [route, path], });
|
const isOwner = computed(() => user.value?.id === overview.value?.owner);
|
||||||
const isOwner = computed(() => user.value?.id === page.value?.owner);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="page" class="xl:p-12 lg:p-8 py-4 flex flex-1 flex-col items-start justify-start max-h-full">
|
<div v-if="overview" class="xl:p-12 lg:p-8 py-4 flex flex-1 flex-col items-start justify-start max-h-full">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>d[any] - Modification de {{ page.title }}</Title>
|
<Title>d[any] - Modification de {{ overview.title }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<div class="flex flex-col xl:flex-row xl:justify-between justify-center items-center w-full px-4 pb-4 border-b border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0">
|
<div class="flex flex-col xl:flex-row xl:justify-between justify-center items-center w-full px-4 pb-4 border-b border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0">
|
||||||
<input type="text" v-model="page.title" placeholder="Titre" class="flex-1 mx-4 h-16 w-full caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 appearance-none outline-none px-3 py-1 text-5xl font-thin bg-transparent" />
|
<input type="text" v-model="overview.title" placeholder="Titre" class="flex-1 mx-4 h-16 w-full caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 appearance-none outline-none px-3 py-1 text-5xl font-thin bg-transparent" />
|
||||||
<div class="flex gap-4 self-end xl:self-auto flex-wrap">
|
<div class="flex gap-4 self-end xl:self-auto flex-wrap">
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<Tooltip message="Consultable uniquement par le propriétaire" side="bottom"><Switch label="Privé" v-model="page.private" /></Tooltip>
|
<Tooltip message="Consultable uniquement par le propriétaire" side="bottom"><Switch label="Privé" v-model="overview.private" /></Tooltip>
|
||||||
<Tooltip message="Afficher dans le menu de navigation" side="bottom"><Switch label="Navigable" v-model="page.navigable" /></Tooltip>
|
<Tooltip message="Afficher dans le menu de navigation" side="bottom"><Switch label="Navigable" v-model="overview.navigable" /></Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<Tooltip message="Ctrl+S" side="bottom"><Button @click="() => save(true)" :loading="saveStatus === 'pending'" class="border-light-blue dark:border-dark-blue hover:border-light-blue dark:hover:border-dark-blue focus:shadow-light-blue dark:focus:shadow-dark-blue">Enregistrer</Button></Tooltip>
|
<Tooltip message="Ctrl+S" side="bottom"><Button @click="() => save(true)" :loading="saveStatus === 'pending'" class="border-light-blue dark:border-dark-blue hover:border-light-blue dark:hover:border-dark-blue focus:shadow-light-blue dark:focus:shadow-dark-blue">Enregistrer</Button></Tooltip>
|
||||||
|
|
@ -17,10 +17,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-4 flex-1 w-full max-h-full flex">
|
<div class="my-4 flex-1 w-full max-h-full flex">
|
||||||
<template v-if="page.type === 'markdown'">
|
<template v-if="overview.type === 'markdown'">
|
||||||
<SplitterGroup direction="horizontal" class="flex-1 w-full flex" >
|
<Loading v-if="contentStatus === 'pending'" />
|
||||||
|
<span v-else-if="contentError">{{ contentError.message }}</span>
|
||||||
|
<SplitterGroup direction="horizontal" class="flex-1 w-full flex" v-else >
|
||||||
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }" :defaultSize="50">
|
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }" :defaultSize="50">
|
||||||
<Editor v-model="page.content" class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none !overflow-y-auto" :class="{ 'hidden': isCollapsed }" />
|
<Editor v-model="page!.content" class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none !overflow-y-auto" :class="{ 'hidden': isCollapsed }" />
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
<SplitterResizeHandle class="bg-light-35 dark:bg-dark-35 w-px xl!mx-4 mx-2" />
|
<SplitterResizeHandle class="bg-light-35 dark:bg-dark-35 w-px xl!mx-4 mx-2" />
|
||||||
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }">
|
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }">
|
||||||
|
|
@ -28,21 +30,21 @@
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
</SplitterGroup>
|
</SplitterGroup>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="page.type === 'canvas'">
|
<template v-else-if="overview.type === 'canvas'">
|
||||||
<span class="flex-1 items-center"><ProseH1>Editeur de graphe en cours de développement</ProseH1></span>
|
<span class="flex-1 items-center"><ProseH1>Editeur de graphe en cours de développement</ProseH1></span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="page.type === 'file'">
|
<template v-else-if="overview.type === 'file'">
|
||||||
<span>Modifier le contenu :</span><input type="file" @change="(e) => console.log(e)" />
|
<span>Modifier le contenu :</span><input type="file" @change="(e) => console.log(e)" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="status === 'pending'" class="flex">
|
<div v-else-if="overviewStatus === 'pending'" class="flex">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>d[any] - Chargement</Title>
|
<Title>d[any] - Chargement</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<Loading />
|
<Loading />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="status === 'error'">{{ error?.message }}</div>
|
<div v-else-if="overviewStatus === 'error'">{{ overviewError?.message }}</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
@ -59,7 +61,8 @@ const toaster = useToast();
|
||||||
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
||||||
|
|
||||||
const sessionContent = useSessionStorage<string | undefined>(path.value, undefined);
|
const sessionContent = useSessionStorage<string | undefined>(path.value, undefined);
|
||||||
const { data: page, status, error } = await useFetch(`/api/file/${encodeURIComponent(path.value)}`, { watch: [ route, path ], transform: (value) => {
|
const { data: overview, status: overviewStatus, error: overviewError } = await useFetch(`/api/file/overview/${encodeURIComponent(path.value)}`, { watch: [ route, path ] });
|
||||||
|
const { data: page, status: contentStatus, error: contentError } = await useFetch(`/api/file/content/${encodeURIComponent(path.value)}`, { watch: [ route, path ], transform: (value) => {
|
||||||
if(value && sessionContent.value !== undefined)
|
if(value && sessionContent.value !== undefined)
|
||||||
{
|
{
|
||||||
value.content = sessionContent.value;
|
value.content = sessionContent.value;
|
||||||
|
|
@ -79,7 +82,7 @@ const { data: page, status, error } = await useFetch(`/api/file/${encodeURICompo
|
||||||
const content = computed(() => page.value?.content);
|
const content = computed(() => page.value?.content);
|
||||||
const debounced = useDebounce(content, 250);
|
const debounced = useDebounce(content, 250);
|
||||||
|
|
||||||
if(!loggedIn || (page.value && page.value.owner !== user.value?.id))
|
if(!loggedIn || (overview.value && overview.value.owner !== user.value?.id))
|
||||||
{
|
{
|
||||||
router.replace({ name: 'explore-path', params: { path: path.value } });
|
router.replace({ name: 'explore-path', params: { path: path.value } });
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +102,7 @@ async function save(redirect: boolean): Promise<void>
|
||||||
try {
|
try {
|
||||||
await $fetch(`/api/file`, {
|
await $fetch(`/api/file`, {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
body: page.value,
|
body: { ...page.value, ...overview.value },
|
||||||
});
|
});
|
||||||
saveStatus.value = 'success';
|
saveStatus.value = 'success';
|
||||||
sessionContent.value = undefined;
|
sessionContent.value = undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-if="status === 'pending'" class="flex">
|
|
||||||
<Head>
|
|
||||||
<Title>d[any] - Chargement</Title>
|
|
||||||
</Head>
|
|
||||||
<Loading />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-1 justify-start items-start" v-else-if="page">
|
|
||||||
<Head>
|
|
||||||
<Title>d[any] - Accueil</Title>
|
|
||||||
</Head>
|
|
||||||
<div class="flex flex-1 justify-start items-start flex-col xl:px-24 md:px-8 px-4 py-6">
|
|
||||||
<div class="flex flex-1 flex-row justify-between items-center">
|
|
||||||
<ProseH1>{{ page.title }}</ProseH1>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<NuxtLink :href="{ name: 'explore-edit-path', params: { path: 'index' } }"><Button v-if="isOwner">Modifier la page</Button></NuxtLink>
|
|
||||||
<NuxtLink :href="{ name: 'explore-edit' }"><Button v-if="isOwner">Configurer le projet</Button></NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Markdown :content="page.content" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="status === 'error'">
|
|
||||||
<Head>
|
|
||||||
<Title>d[any] - Erreur</Title>
|
|
||||||
</Head>
|
|
||||||
<span>{{ error?.message }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<Head>
|
|
||||||
<Title>d[any] - Erreur</Title>
|
|
||||||
</Head>
|
|
||||||
<div><ProseH2>Impossible d'afficher le contenu demandé</ProseH2></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const { loggedIn, user } = useUserSession();
|
|
||||||
|
|
||||||
const { data: page, status, error } = await useFetch(`/api/file/index`);
|
|
||||||
const isOwner = computed(() => user.value?.id === page.value?.owner);
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import { and, eq, like, sql } from 'drizzle-orm';
|
|
||||||
import useDatabase from '~/composables/useDatabase';
|
|
||||||
import { explorerContentTable } from '~/db/schema';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
|
||||||
const query = getQuery(e);
|
|
||||||
const where = [];
|
|
||||||
|
|
||||||
if(query && query.path !== undefined)
|
|
||||||
{
|
|
||||||
where.push(eq(explorerContentTable.path, sql.placeholder('path')));
|
|
||||||
}
|
|
||||||
if(query && query.title !== undefined)
|
|
||||||
{
|
|
||||||
where.push(eq(explorerContentTable.title, sql.placeholder('title')));
|
|
||||||
}
|
|
||||||
if(query && query.type !== undefined)
|
|
||||||
{
|
|
||||||
where.push(eq(explorerContentTable.type, sql.placeholder('type')));
|
|
||||||
}
|
|
||||||
if (query && query.search !== undefined)
|
|
||||||
{
|
|
||||||
where.push(like(explorerContentTable.path, sql.placeholder('search')));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(where.length > 0)
|
|
||||||
{
|
|
||||||
const db = useDatabase();
|
|
||||||
|
|
||||||
const content = db.select({
|
|
||||||
'path': explorerContentTable.path,
|
|
||||||
'owner': explorerContentTable.owner,
|
|
||||||
'title': explorerContentTable.title,
|
|
||||||
'type': explorerContentTable.type,
|
|
||||||
'content': sql<string>`cast(${explorerContentTable.content} as TEXT)`.as('content'),
|
|
||||||
'navigable': explorerContentTable.navigable,
|
|
||||||
'private': explorerContentTable.private,
|
|
||||||
}).from(explorerContentTable).where(and(...where)).prepare().all(query);
|
|
||||||
|
|
||||||
if(content.length > 0)
|
|
||||||
{
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setResponseStatus(e, 404);
|
|
||||||
});
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { explorerContentTable } from '~/db/schema';
|
import { explorerContentTable } from '~/db/schema';
|
||||||
import { schema } from '~/schemas/file';
|
import { schema } from '~/schemas/file';
|
||||||
|
import { parsePath } from '~/shared/general.utils';
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
const body = await readValidatedBody(e, schema.safeParse);
|
const body = await readValidatedBody(e, schema.safeParse);
|
||||||
|
|
@ -10,9 +11,10 @@ export default defineEventHandler(async (e) => {
|
||||||
throw body.error;
|
throw body.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buffer = Buffer.from(body.data.content, 'utf-8');
|
|
||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
|
|
||||||
|
const buffer = Buffer.from(convertToStorableLinks(body.data.content, db.select({ path: explorerContentTable.path }).from(explorerContentTable).all().map(e => e.path)), 'utf-8');
|
||||||
|
|
||||||
const content = db.insert(explorerContentTable).values({ ...body.data, content: buffer }).onConflictDoUpdate({ target: explorerContentTable.path, set: { ...body.data, content: buffer, timestamp: new Date() } });
|
const content = db.insert(explorerContentTable).values({ ...body.data, content: buffer }).onConflictDoUpdate({ target: explorerContentTable.path, set: { ...body.data, content: buffer, timestamp: new Date() } });
|
||||||
|
|
||||||
if(content !== undefined)
|
if(content !== undefined)
|
||||||
|
|
@ -23,3 +25,13 @@ export default defineEventHandler(async (e) => {
|
||||||
setResponseStatus(e, 404);
|
setResponseStatus(e, 404);
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function convertToStorableLinks(content: string, path: string[]): string
|
||||||
|
{
|
||||||
|
return content.replaceAll(/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/g, (e: string, a1?: string, a2?: string , a3?: string) => {
|
||||||
|
const parsed = parsePath(a1 ?? '%%%%----%%%%----%%%%');
|
||||||
|
const replacer = path.find(e => e.endsWith(parsed));
|
||||||
|
const value = `[[${replacer ?? ''}${a2 ?? ''}${(!a3 && replacer !== parsed ? '|' + a1 : a3) ?? ''}]]`;
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { eq, sql } from 'drizzle-orm';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { explorerContentTable } from '~/db/schema';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const path = decodeURIComponent(getRouterParam(e, "path") ?? '');
|
||||||
|
const query = getQuery(e);
|
||||||
|
|
||||||
|
if(!path)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const content = db.select({
|
||||||
|
'content': sql<string>`cast(${explorerContentTable.content} as TEXT)`.as('content'),
|
||||||
|
'private': explorerContentTable.private,
|
||||||
|
'owner': explorerContentTable.owner,
|
||||||
|
'visit': explorerContentTable.visit,
|
||||||
|
}).from(explorerContentTable).where(eq(explorerContentTable.path, sql.placeholder('path'))).prepare().get({ path });
|
||||||
|
|
||||||
|
if(content !== undefined)
|
||||||
|
{
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
|
||||||
|
if(content.private && (!session || !session.user))
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(session && session.user && content.private && session.user.id !== content.owner)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(query.type === 'view')
|
||||||
|
{
|
||||||
|
db.update(explorerContentTable).set({ visit: content.visit + 1 }).where(eq(explorerContentTable.path, path)).run();
|
||||||
|
}
|
||||||
|
if(query.type === 'editing')
|
||||||
|
{
|
||||||
|
content.content = convertFromStorableLinks(content.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { content: content.content };
|
||||||
|
}
|
||||||
|
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
export function convertFromStorableLinks(content: string): string
|
||||||
|
{
|
||||||
|
/*return content.replaceAll(/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/g, (e: string, a1?: string, a2?: string , a3?: string) => {
|
||||||
|
const parsed = parsePath(a1 ?? '%%%%----%%%%----%%%%');
|
||||||
|
const replacer = path.find(e => e.endsWith(parsed)) ?? parsed;
|
||||||
|
const value = `[[${replacer}${a2 ?? ''}${(!a3 && replacer !== parsed ? '|' + a1 : a3) ?? ''}]]`;
|
||||||
|
return value;
|
||||||
|
});*/
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,6 @@ export default defineEventHandler(async (e) => {
|
||||||
'owner': explorerContentTable.owner,
|
'owner': explorerContentTable.owner,
|
||||||
'title': explorerContentTable.title,
|
'title': explorerContentTable.title,
|
||||||
'type': explorerContentTable.type,
|
'type': explorerContentTable.type,
|
||||||
'content': sql<string>`cast(${explorerContentTable.content} as TEXT)`.as('content'),
|
|
||||||
'navigable': explorerContentTable.navigable,
|
'navigable': explorerContentTable.navigable,
|
||||||
'private': explorerContentTable.private,
|
'private': explorerContentTable.private,
|
||||||
'order': explorerContentTable.order,
|
'order': explorerContentTable.order,
|
||||||
|
|
@ -27,7 +26,18 @@ export default defineEventHandler(async (e) => {
|
||||||
|
|
||||||
if(content !== undefined)
|
if(content !== undefined)
|
||||||
{
|
{
|
||||||
db.update(explorerContentTable).set({ visit: content.visit + 1 }).where(eq(explorerContentTable.path, content.path)).run();
|
const session = await getUserSession(e);
|
||||||
|
|
||||||
|
if(content.private && (!session || !session.user))
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(session && session.user && content.private && session.user.id !== content.owner)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import { extname, basename } from 'node:path';
|
||||||
import type { FileType } from '~/types/api';
|
import type { FileType } from '~/types/api';
|
||||||
import type { CanvasColor, CanvasContent } from "~/types/canvas";
|
import type { CanvasColor, CanvasContent } from "~/types/canvas";
|
||||||
import { explorerContentTable } from "~/db/schema";
|
import { explorerContentTable } from "~/db/schema";
|
||||||
|
import { convertToStorableLinks } from "../api/file.post";
|
||||||
|
|
||||||
const typeMapping: Record<string, FileType> = {
|
const typeMapping: Record<string, FileType> = {
|
||||||
".md": "markdown",
|
".md": "markdown",
|
||||||
|
|
@ -27,7 +28,7 @@ export default defineTask({
|
||||||
}
|
}
|
||||||
}) as { tree: any[] } & Record<string, any>;
|
}) as { tree: any[] } & Record<string, any>;
|
||||||
|
|
||||||
const files: typeof explorerContentTable.$inferInsert = await Promise.all(tree.tree.filter((e: any) => !e.path.startsWith(".")).map(async (e, i) => {
|
const files: typeof explorerContentTable.$inferInsert[] = await Promise.all(tree.tree.filter((e: any) => !e.path.startsWith(".")).map(async (e, i) => {
|
||||||
if(e.type === 'tree')
|
if(e.type === 'tree')
|
||||||
{
|
{
|
||||||
const title = basename(e.path);
|
const title = basename(e.path);
|
||||||
|
|
@ -63,12 +64,18 @@ export default defineTask({
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const pathList = files.map(e => e.path);
|
||||||
|
files.forEach(e => {
|
||||||
|
if(e.type !== 'folder' && e.content)
|
||||||
|
{
|
||||||
|
e.content = Buffer.from(convertToStorableLinks(e.content.toString('utf-8'), files.map(e => e.path)), 'utf-8');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
db.delete(explorerContentTable).run();
|
db.delete(explorerContentTable).run();
|
||||||
db.insert(explorerContentTable).values(files).run();
|
db.insert(explorerContentTable).values(files).run();
|
||||||
|
|
||||||
useStorage('cache').clear();
|
|
||||||
|
|
||||||
return { result: true };
|
return { result: true };
|
||||||
}
|
}
|
||||||
catch(e)
|
catch(e)
|
||||||
|
|
@ -85,6 +92,9 @@ function reshapeContent(content: string, type: FileType): string | null
|
||||||
switch(type)
|
switch(type)
|
||||||
{
|
{
|
||||||
case "markdown":
|
case "markdown":
|
||||||
|
return content.replaceAll(/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/g, (e: string, a1?: string, a2?: string , a3?: string) => {
|
||||||
|
return `[[${a1?.split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/') ?? ''}${a2 ?? ''}${a3 ?? ''}]]`;
|
||||||
|
});
|
||||||
case "file":
|
case "file":
|
||||||
return content;
|
return content;
|
||||||
case "canvas":
|
case "canvas":
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue