Compare commits
7 Commits
429f1d4b38
...
298f47a280
| Author | SHA1 | Date |
|---|---|---|
|
|
298f47a280 | |
|
|
161f0d856a | |
|
|
51a5d501be | |
|
|
ecdfa947ac | |
|
|
fd951c294f | |
|
|
602b0af212 | |
|
|
f7094f7ce1 |
|
|
@ -23,6 +23,6 @@ logs
|
|||
.env.*
|
||||
!.env.example
|
||||
|
||||
*.sqlite
|
||||
*.sqlite-wal
|
||||
*.sqlite-shm
|
||||
db.sqlite
|
||||
db.sqlite-wal
|
||||
db.sqlite-shm
|
||||
2
app.vue
2
app.vue
|
|
@ -4,7 +4,7 @@
|
|||
<NuxtLoadingIndicator />
|
||||
<TooltipProvider>
|
||||
<NuxtLayout>
|
||||
<div class="xl:ps-12 xl:pe-12 ps-6 pe-4 flex flex-1 justify-center overflow-auto max-h-full relative">
|
||||
<div class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 justify-center overflow-auto max-h-full relative">
|
||||
<NuxtPage></NuxtPage>
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ const External = Annotation.define<boolean>();
|
|||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dropCursor, crosshairCursor, keymap, EditorView, ViewUpdate } from '@codemirror/view';
|
||||
import { dropCursor, crosshairCursor, keymap, EditorView, ViewUpdate, placeholder as placeholderExtension } from '@codemirror/view';
|
||||
import { Annotation, EditorState } from '@codemirror/state';
|
||||
import { indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap } from '@codemirror/language';
|
||||
import { history, defaultKeymap, historyKeymap } from '@codemirror/commands';
|
||||
import { searchKeymap } from '@codemirror/search';
|
||||
import { search, searchKeymap } from '@codemirror/search';
|
||||
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
|
||||
import { lintKeymap } from '@codemirror/lint';
|
||||
|
||||
|
|
@ -15,6 +15,9 @@ const editor = useTemplateRef('editor');
|
|||
const view = ref<EditorView>();
|
||||
const state = ref<EditorState>();
|
||||
|
||||
const { placeholder } = defineProps<{
|
||||
placeholder?: string
|
||||
}>();
|
||||
const model = defineModel<string>();
|
||||
|
||||
onMounted(() => {
|
||||
|
|
@ -24,6 +27,7 @@ onMounted(() => {
|
|||
doc: model.value,
|
||||
extensions: [
|
||||
history(),
|
||||
search(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
|
|
@ -31,6 +35,7 @@ onMounted(() => {
|
|||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
crosshairCursor(),
|
||||
placeholderExtension(placeholder ?? ''),
|
||||
EditorView.lineWrapping,
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
|
|
@ -46,7 +51,8 @@ onMounted(() => {
|
|||
{
|
||||
model.value = viewUpdate.state.doc.toString();
|
||||
}
|
||||
})
|
||||
}),
|
||||
EditorView.contentAttributes.of({spellcheck: "true"}),
|
||||
]
|
||||
});
|
||||
view.value = new EditorView({
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Text, Comment } from 'vue';
|
|||
import ProseP from '~/components/prose/ProseP.vue';
|
||||
import ProseA from '~/components/prose/ProseA.vue';
|
||||
import ProseBlockquote from '~/components/prose/ProseBlockquote.vue';
|
||||
import ProseCallout from './prose/ProseCallout.vue';
|
||||
import ProseCode from '~/components/prose/ProseCode.vue';
|
||||
import ProsePre from '~/components/prose/ProsePre.vue';
|
||||
import ProseEm from '~/components/prose/ProseEm.vue';
|
||||
|
|
@ -33,6 +34,7 @@ const proseList = {
|
|||
"p": ProseP,
|
||||
"a": ProseA,
|
||||
"blockquote": ProseBlockquote,
|
||||
"callout": ProseCallout,
|
||||
"code": ProseCode,
|
||||
"pre": ProsePre,
|
||||
"em": ProseEm,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<TreeRoot v-bind="forward" v-slot="{ flattenItems }" class="list-none select-none border border-light-35 dark:border-dark-35 text-light-100 dark:text-dark-100 p-2 xl:text-base text-sm overflow-auto w-[450px] max-h-full">
|
||||
<TreeRoot v-bind="forward" v-slot="{ flattenItems }" class="list-none select-none text-light-100 dark:text-dark-100 overflow-auto max-h-full">
|
||||
<DraggableTreeItem v-for="item in flattenItems" :key="item._id" v-bind="item" class="flex items-center outline-none relative cursor-pointer hover:bg-light-20 dark:hover:bg-dark-20 data-[selected]:bg-light-35 dark:data-[selected]:bg-dark-35" @select.prevent @toggle.prevent>
|
||||
<template #default="{ handleToggle, handleSelect, isExpanded, isSelected, isDragging, isDraggedOver }">
|
||||
<slot :handleToggle="handleToggle"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<DropdownMenuRoot>
|
||||
<DropdownMenuTrigger :disabled="disabled" ><slot /></DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger :disabled="disabled"><slot /></DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent :align="align" :side="side" class="z-50 outline-none bg-light-20 dark:bg-dark-20 will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade border border-light-35 dark:border-dark-35">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<HoverCardRoot :open-delay="delay">
|
||||
<HoverCardRoot :open-delay="delay" @update:open="(...args) => emits('open', ...args)">
|
||||
<HoverCardTrigger class="inline-block cursor-help outline-none">
|
||||
<slot></slot>
|
||||
</HoverCardTrigger>
|
||||
|
|
@ -18,4 +18,6 @@ const { delay = 500, disabled = false, side = 'bottom' } = defineProps<{
|
|||
disabled?: boolean
|
||||
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['open'])
|
||||
</script>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<Label class="flex justify-center items-center my-2">{{ label }}
|
||||
<Label class="flex justify-center items-center my-2">
|
||||
<span class="md:text-base text-sm">{{ label }}</span>
|
||||
<SwitchRoot v-model:checked="model" :disabled="disabled"
|
||||
class="group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none
|
||||
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
|
|
|
|||
|
|
@ -1,29 +1,31 @@
|
|||
<template>
|
||||
<NuxtLink class="text-accent-blue inline-flex items-center" v-if="data && data[0]"
|
||||
:to="{ name: 'explore-path', params: { path: data[0].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'}">
|
||||
<NuxtLink class="text-accent-blue inline-flex items-center" v-if="overview"
|
||||
: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': overview.type === 'canvas'}" @open="load">
|
||||
<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">
|
||||
<Markdown :content="data[0].content" />
|
||||
<Markdown :content="content!.content" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="data[0].type === 'canvas'">
|
||||
<template v-else-if="overview.type === 'canvas'">
|
||||
<div class="w-[600px] h-[600px] relative">
|
||||
<Canvas :canvas="JSON.parse(data[0].content)" />
|
||||
<Canvas :canvas="JSON.parse(content!.content)" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template #default>
|
||||
<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>
|
||||
</HoverCard>
|
||||
</NuxtLink>
|
||||
<NuxtLink v-else-if="href" :to="href" :class="class" class="text-accent-blue inline-flex items-center">
|
||||
<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="`icons/link-${data[0].type.toLowerCase()}`" />
|
||||
<Icon class="w-4 h-4 inline-block" v-if="overview && overview.type !== 'markdown'" :height="20" :width="20" :icon="iconByType[overview.type]" />
|
||||
</NuxtLink>
|
||||
<slot :class="class" v-else v-bind="$attrs"></slot>
|
||||
</template>
|
||||
|
|
@ -39,17 +41,35 @@ const { href } = defineProps<{
|
|||
}>();
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
try {
|
||||
data.value = await $fetch(`/api/file`, {
|
||||
query: {
|
||||
search: `%${pathname}`
|
||||
},
|
||||
});
|
||||
content.value = await $fetch(`/api/file/content/${encodeURIComponent(pathname)}`);
|
||||
} catch(e) { }
|
||||
loading.value = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,179 +1,5 @@
|
|||
<template>
|
||||
<blockquote ref="el">
|
||||
<blockquote class="empty:before:hidden ps-4 my-4 relative before:absolute before:-top-1 before:-bottom-1 before:left-0 before:w-1 before:bg-light-30 dark:before:bg-dark-30" ref="el">
|
||||
<slot />
|
||||
</blockquote>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const attrs = useAttrs(), el = ref<HTMLQuoteElement>(), title = ref<Element | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if(el && el.value && attrs.hasOwnProperty("dataCalloutFold"))
|
||||
{
|
||||
title.value = el.value.querySelector('.callout-title');
|
||||
title.value?.addEventListener('click', toggle);
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
title.value?.removeEventListener('click', toggle);
|
||||
})
|
||||
function toggle() {
|
||||
el.value?.classList?.toggle('is-collapsed');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
blockquote:not(.callout)
|
||||
{
|
||||
@apply ps-4;
|
||||
@apply my-4;
|
||||
@apply relative;
|
||||
@apply before:absolute;
|
||||
@apply before:-top-1;
|
||||
@apply before:-bottom-1;
|
||||
@apply before:left-0;
|
||||
@apply before:w-1;
|
||||
@apply before:bg-light-30;
|
||||
@apply dark:before:bg-dark-30;
|
||||
}
|
||||
blockquote:empty
|
||||
{
|
||||
@apply before:hidden;
|
||||
}
|
||||
.callout {
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
}
|
||||
.callout.is-collapsible .callout-title
|
||||
{
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
.callout .fold
|
||||
{
|
||||
@apply transition-transform;
|
||||
}
|
||||
.callout.is-collapsed .fold
|
||||
{
|
||||
@apply -rotate-90;
|
||||
}
|
||||
.callout.is-collapsed > p
|
||||
{
|
||||
@apply hidden;
|
||||
}
|
||||
.callout[datacallout="abstract"],
|
||||
.callout[datacallout="summary"],
|
||||
.callout[datacallout="tldr"] {
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[datacallout="info"] {
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[datacallout="todo"] {
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[datacallout="important"] {
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[datacallout="tip"],
|
||||
.callout[datacallout="hint"] {
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[datacallout="success"],
|
||||
.callout[datacallout="check"],
|
||||
.callout[datacallout="done"] {
|
||||
@apply bg-light-green;
|
||||
@apply dark:bg-dark-green;
|
||||
@apply text-light-green;
|
||||
@apply dark:text-dark-green;
|
||||
}
|
||||
.callout[datacallout="question"],
|
||||
.callout[datacallout="help"],
|
||||
.callout[datacallout="faq"] {
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
.callout[datacallout="warning"],
|
||||
.callout[datacallout="caution"],
|
||||
.callout[datacallout="attention"] {
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
.callout[datacallout="failure"],
|
||||
.callout[datacallout="fail"],
|
||||
.callout[datacallout="missing"] {
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[datacallout="danger"],
|
||||
.callout[datacallout="error"] {
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[datacallout="bug"] {
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[datacallout="example"] {
|
||||
@apply bg-light-purple;
|
||||
@apply dark:bg-dark-purple;
|
||||
@apply text-light-purple;
|
||||
@apply dark:text-dark-purple;
|
||||
}
|
||||
|
||||
.callout
|
||||
{
|
||||
@apply overflow-hidden;
|
||||
@apply my-4;
|
||||
@apply p-3;
|
||||
@apply ps-6;
|
||||
@apply bg-blend-lighten;
|
||||
@apply !bg-opacity-25;
|
||||
@apply border-l-4;
|
||||
@apply inline-block;
|
||||
@apply pe-8;
|
||||
}
|
||||
.callout-icon
|
||||
{
|
||||
@apply w-6;
|
||||
@apply h-6;
|
||||
@apply stroke-2;
|
||||
@apply float-start;
|
||||
@apply me-2;
|
||||
}
|
||||
.callout-title-inner
|
||||
{
|
||||
@apply block;
|
||||
@apply font-bold;
|
||||
@apply ps-8;
|
||||
}
|
||||
.callout > p
|
||||
{
|
||||
@apply mt-2;
|
||||
@apply font-semibold;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
<template>
|
||||
<CollapsibleRoot :disabled="fold === undefined" :defaultOpen="fold === true || fold === undefined" class="callout group overflow-hidden my-4 p-3 ps-4 bg-blend-lighten !bg-opacity-25 border-l-4 inline-block pe-8 bg-light-blue dark:bg-dark-blue" :data-type="type">
|
||||
<CollapsibleTrigger asChild>
|
||||
<div :class="{ 'cursor-pointer': fold !== undefined }" class="flex flex-row items-center justify-start ps-2">
|
||||
<Icon :icon="calloutIconByType[type] ?? defaultCalloutIcon" class="w-6 h-6 stroke-2 float-start me-2" />
|
||||
<span v-if="title" class="block font-bold">{{ title }}</span>
|
||||
<Icon icon="radix-icons:caret-right" v-if="fold !== undefined" class="transition-transform group-data-[state=open]:rotate-90 w-6 h-6 mx-6" />
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent class="overflow-hidden data-[state=closed]:animate-[collapseClose_0.2s_ease-in-out] data-[state=open]:animate-[collapseOpen_0.2s_ease-in-out] data-[state=closed]:h-0">
|
||||
<div class="px-2">
|
||||
<slot />
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</CollapsibleRoot>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
const calloutIconByType: Record<string, string> = {
|
||||
note: 'radix-icons:pencil-1',
|
||||
abstract: 'radix-icons:file-text',
|
||||
info: 'radix-icons:info-circled',
|
||||
todo: 'radix-icons:check-circled',
|
||||
tip: 'radix-icons:star',
|
||||
success: 'radix-icons:check',
|
||||
question: 'radix-icons:question-mark-circled',
|
||||
warning: 'radix-icons:exclamation-triangle',
|
||||
failure: 'radix-icons:cross-circled',
|
||||
danger: 'radix-icons:circle-backslash',
|
||||
bug: 'solar:bug-linear',
|
||||
example: 'radix-icons:list-bullet',
|
||||
quote: 'radix-icons:quote',
|
||||
};
|
||||
const defaultCalloutIcon = 'radix-icons:info-circled';
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
const { type, title, fold } = defineProps<{
|
||||
type: string;
|
||||
title?: string;
|
||||
fold?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.callout[data-type="abstract"],
|
||||
.callout[data-type="summary"],
|
||||
.callout[data-type="tldr"] {
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[data-type="info"] {
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[data-type="todo"] {
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[data-type="important"] {
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[data-type="tip"],
|
||||
.callout[data-type="hint"] {
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[data-type="success"],
|
||||
.callout[data-type="check"],
|
||||
.callout[data-type="done"] {
|
||||
@apply bg-light-green;
|
||||
@apply dark:bg-dark-green;
|
||||
@apply text-light-green;
|
||||
@apply dark:text-dark-green;
|
||||
}
|
||||
.callout[data-type="question"],
|
||||
.callout[data-type="help"],
|
||||
.callout[data-type="faq"] {
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
.callout[data-type="warning"],
|
||||
.callout[data-type="caution"],
|
||||
.callout[data-type="attention"] {
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
.callout[data-type="failure"],
|
||||
.callout[data-type="fail"],
|
||||
.callout[data-type="missing"] {
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[data-type="danger"],
|
||||
.callout[data-type="error"] {
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[data-type="bug"] {
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[data-type="example"] {
|
||||
@apply bg-light-purple;
|
||||
@apply dark:bg-dark-purple;
|
||||
@apply text-light-purple;
|
||||
@apply dark:text-dark-purple;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
<template>
|
||||
<Separator class="border-light-35 dark:border-dark-35 m-4" />
|
||||
<Separator class="border-b border-light-35 dark:border-dark-35 m-4" />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@ import RemarkParse from "remark-parse";
|
|||
|
||||
import RemarkRehype from 'remark-rehype';
|
||||
import RemarkOfm from 'remark-ofm';
|
||||
import RemarkBreaks from 'remark-breaks'
|
||||
import RemarkGfm from 'remark-gfm';
|
||||
import RemarkFrontmatter from 'remark-frontmatter';
|
||||
import RehypeRaw from 'rehype-raw';
|
||||
|
||||
export default function useMarkdown(): (md: string) => Root
|
||||
{
|
||||
|
|
@ -16,9 +14,8 @@ export default function useMarkdown(): (md: string) => Root
|
|||
const parse = (markdown: string) => {
|
||||
if (!processor)
|
||||
{
|
||||
processor = unified().use([RemarkParse, RemarkGfm , RemarkOfm , RemarkBreaks, RemarkFrontmatter]);
|
||||
processor.use(RemarkRehype, { allowDangerousHtml: true });
|
||||
processor.use(RehypeRaw);
|
||||
processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkFrontmatter]);
|
||||
processor.use(RemarkRehype);
|
||||
}
|
||||
|
||||
const processed = processor.runSync(processor.parse(markdown)) as Root;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export interface ShortcutConfig {
|
|||
handler: Function
|
||||
usingInput?: string | boolean
|
||||
whenever?: WatchSource<boolean>[]
|
||||
prevent?: boolean
|
||||
}
|
||||
|
||||
export interface ShortcutsConfig {
|
||||
|
|
@ -29,6 +30,7 @@ interface Shortcut {
|
|||
altKey: boolean
|
||||
// code?: string
|
||||
// keyCode?: number
|
||||
prevent?: boolean
|
||||
}
|
||||
|
||||
const chainedShortcutRegex = /^[^-]+.*-.*[^-]+$/
|
||||
|
|
@ -61,7 +63,8 @@ export const useShortcuts = (config: ShortcutsConfig, options: ShortcutsOptions
|
|||
if (shortcut.key !== chainedKey) { continue }
|
||||
|
||||
if (shortcut.condition.value) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation
|
||||
shortcut.prevent && e.preventDefault()
|
||||
shortcut.handler()
|
||||
}
|
||||
clearChainedInput()
|
||||
|
|
@ -139,7 +142,7 @@ export const useShortcuts = (config: ShortcutsConfig, options: ShortcutsOptions
|
|||
if (typeof shortcutConfig === 'function') {
|
||||
shortcut.handler = shortcutConfig
|
||||
} else if (typeof shortcutConfig === 'object') {
|
||||
shortcut = { ...shortcut, handler: shortcutConfig.handler }
|
||||
shortcut = { ...shortcut, handler: shortcutConfig.handler, prevent: shortcutConfig.prevent }
|
||||
}
|
||||
|
||||
if (!shortcut.handler) {
|
||||
|
|
|
|||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -13,13 +13,15 @@
|
|||
<div class="flex items-center px-2">
|
||||
<Tooltip message="Changer de theme" side="left"><ThemeSwitch /></Tooltip>
|
||||
<Tooltip v-if="!loggedIn" :message="'Se connecter'" side="right">
|
||||
<div class="hover:border-opacity-70 flex">
|
||||
<NuxtLink :to="{ name: 'user-login' }">
|
||||
<div class="hover:border-opacity-70 flex items-center">
|
||||
<Icon :icon="'radix-icons:person'" class="w-7 h-7 p-1" />
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</Tooltip>
|
||||
<Tooltip v-else :message="'Mon profil'" side="right">
|
||||
<DropdownMenu :options="options" side="bottom" align="end">
|
||||
<div class="hover:border-opacity-70 flex">
|
||||
<div class="hover:border-opacity-70 flex items-center">
|
||||
<Icon :icon="'radix-icons:avatar'" class="w-7 h-7 p-1" />
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
|
|
@ -28,8 +30,8 @@
|
|||
</div>
|
||||
<div class="flex flex-1 flex-row relative h-screen overflow-hidden">
|
||||
<CollapsibleContent asChild forceMount>
|
||||
<div class="bg-light-0 md:py-11 dark:bg-dark-0 z-40 xl:w-96 md:w-[15em] w-full border-r border-light-30 dark:border-dark-30 flex flex-col justify-between max-md:absolute max-md:-top-0 max-md:-bottom-0 md:left-0 max-md:data-[state=closed]:-left-full max-md:transition-[left] py-8 max-md:z-40 max-md:data-[state=open]:left-0">
|
||||
<div class="relative bottom-6 flex flex-col gap-4 xl:px-6 px-3">
|
||||
<div class="bg-light-0 dark:bg-dark-0 z-40 xl:w-96 md:w-[15em] w-full border-r border-light-30 dark:border-dark-30 flex flex-col justify-between max-md:absolute max-md:-top-0 max-md:-bottom-0 md:left-0 max-md:data-[state=closed]:-left-full max-md:transition-[left] max-md:z-40 max-md:data-[state=open]:left-0">
|
||||
<div class="flex flex-col gap-4 xl:px-6 px-3 py-4">
|
||||
<div class="flex justify-between items-center max-md:hidden">
|
||||
<NuxtLink class=" text-light-100 dark:text-dark-100 hover:text-opacity-70 max-md:ps-6" aria-label="Accueil" :to="{ path: '/', force: true }">
|
||||
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
|
||||
|
|
@ -55,9 +57,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<div class="pl-3 py-1 flex-1 truncate">Projet</div>
|
||||
<div class="flex flex-row flex-1 justify-between items-center">
|
||||
<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">
|
||||
<span class="pl-3 py-1 flex-1 truncate">Projet</span>
|
||||
</NuxtLink>
|
||||
<NuxtLink v-if="user && hasPermissions(user.permissions, ['admin', 'editor'])" :to="{ name: 'explore-edit' }"><Button icon><Icon icon="radix-icons:pencil-2" /></Button></NuxtLink>
|
||||
</div>
|
||||
<Tree v-if="pages" v-model="pages" :getKey="(item) => item.path">
|
||||
<template #default="{ item, isExpanded }">
|
||||
<NuxtLink :href="item.value.path && !item.hasChildren ? { name: 'explore-path', params: { path: item.value.path } } : undefined" class="flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple" :class="{ 'font-medium': item.hasChildren }" active-class="text-accent-blue" :data-private="item.value.private">
|
||||
|
|
@ -71,7 +76,7 @@
|
|||
</template>
|
||||
</Tree>
|
||||
</div>
|
||||
<div class="xl:px-12 px-6 text-start text-xs text-light-60 dark:text-dark-60 relative top-4">
|
||||
<div class="xl:px-12 px-6 py-4 text-center text-xs text-light-60 dark:text-dark-60">
|
||||
<NuxtLink class="hover:underline italic" :to="{ name: 'roadmap' }">Roadmap</NuxtLink> - <NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
||||
<p>Copyright Peaceultime - 2024</p>
|
||||
</div>
|
||||
|
|
@ -87,6 +92,7 @@ import { Icon } from '@iconify/vue/dist/iconify.js';
|
|||
import type { NavigationTreeItem } from '~/server/api/navigation.get';
|
||||
import { iconByType } from '#shared/general.utils';
|
||||
import type { DropdownOption } from '~/components/base/DropdownMenu.vue';
|
||||
import { hasPermissions } from '~/shared/auth.util';
|
||||
|
||||
const options = ref<DropdownOption[]>([{
|
||||
type: 'item',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<slot></slot>
|
||||
</template>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
import vuePlugin from 'rollup-plugin-vue'
|
||||
import postcssPlugin from 'rollup-plugin-postcss'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2024-04-03',
|
||||
|
|
@ -118,21 +117,9 @@ export default defineNuxtConfig({
|
|||
path: '~/components',
|
||||
pathPrefix: false,
|
||||
},
|
||||
{
|
||||
path: '~/server/components',
|
||||
pathPrefix: true,
|
||||
global: true,
|
||||
},
|
||||
],
|
||||
nitro: {
|
||||
alias: {
|
||||
'public': '//public',
|
||||
},
|
||||
publicAssets: [{
|
||||
baseURL: 'public',
|
||||
dir: 'public',
|
||||
}],
|
||||
preset: 'bun',
|
||||
preset: 'node',
|
||||
experimental: {
|
||||
tasks: true,
|
||||
},
|
||||
|
|
@ -166,7 +153,7 @@ export default defineNuxtConfig({
|
|||
xssValidator: false,
|
||||
},
|
||||
sitemap: {
|
||||
exclude: ['/admin/**', '/explore/edit/**', '/user/mailvalidated'],
|
||||
exclude: ['/admin/**', '/explore/edit', '/user/mailvalidated'],
|
||||
sources: ['/api/__sitemap__/urls']
|
||||
},
|
||||
experimental: {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"predev": "bun i && bunx nuxi cleanup",
|
||||
"predev": "bun i",
|
||||
"dev": "bunx --bun nuxi dev"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -135,6 +135,27 @@ async function editPermissions(user: User)
|
|||
});
|
||||
}
|
||||
}
|
||||
async function logout(user: User)
|
||||
{
|
||||
try
|
||||
{
|
||||
await $fetch(`/api/admin/user/${user.id}/logout`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
user.session.length = 0;
|
||||
|
||||
toaster.add({
|
||||
duration: 10000, type: 'success', content: 'L\'utilisateur vient d\'être déconnecté.', timer: true,
|
||||
});
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
toaster.add({
|
||||
duration: 10000, type: 'error', content: (e as any).message, timer: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -178,7 +199,7 @@ async function editPermissions(user: User)
|
|||
</DialogTitle>
|
||||
<div class="flex flex-1 justify-end gap-4">
|
||||
<DialogClose asChild><Button>Non</Button></DialogClose>
|
||||
<DialogClose asChild><Button class="border-light-green dark:border-dark-green hover:border-light-green dark:hover:border-dark-green hover:bg-light-greenBack dark:hover:bg-dark-greenBack text-light-green dark:text-dark-green focus:shadow-light-green dark:focus:shadow-dark-green">Oui</Button></DialogClose>
|
||||
<DialogClose asChild><Button @click="() => logout(user)" class="border-light-green dark:border-dark-green hover:border-light-green dark:hover:border-dark-green hover:bg-light-greenBack dark:hover:bg-dark-greenBack text-light-green dark:text-dark-green focus:shadow-light-green dark:focus:shadow-dark-green">Oui</Button></DialogClose>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
|
|
@ -242,7 +263,7 @@ async function editPermissions(user: User)
|
|||
</td>
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-center">{{ textualFileSize(page.size) }}</td>
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-center">{{ page.visit }}</td>
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-center"><div class="flex justify-center items-center"><NuxtLink :to="{ name: 'explore-edit-path', params: { path: page.path } }"><Icon icon="radix-icons:pencil-1" /></NuxtLink></div></td>
|
||||
<td class="border border-light-35 dark:border-dark-35 px-2 py-1 text-center"><div class="flex justify-center items-center"><NuxtLink :to="{ name: 'explore-edit', hash: '#' + page.path }"><Icon icon="radix-icons:pencil-1" /></NuxtLink></div></td>
|
||||
</tr>
|
||||
</DialogRoot>
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
<template>
|
||||
<Head>
|
||||
<Title>d[any] - Editeur</Title>
|
||||
</Head>
|
||||
<Editor v-model="model" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
rights: ['admin', 'editor'],
|
||||
})
|
||||
const model = defineModel<string>({
|
||||
default: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam quis orci et est malesuada vulputate. Aenean sagittis congue eros, non feugiat metus bibendum consectetur. Duis volutpat leo nisi, in maximus nulla rhoncus ac. Sed scelerisque ipsum et volutpat dignissim. Integer massa nibh, imperdiet quis condimentum vitae, imperdiet quis quam. Cras pretium ex eget hendrerit porttitor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque rutrum scelerisque quam, sit amet malesuada mi convallis aliquam. Curabitur eget dolor in diam scelerisque tincidunt at et sapien. Nulla vel nisl finibus odio porttitor sagittis ac ut sem. Aenean orci enim, fringilla eu porta eget, egestas vel libero. Aenean ac efficitur nunc, id finibus nibh. Suspendisse potenti. Quisque vel vestibulum ante. Morbi mi nulla, gravida ac malesuada at, hendrerit nec nibh.
|
||||
|
||||
Fusce sodales convallis velit, ac tempor sem auctor sed.Aenean commodo sodales lorem eu mollis.Suspendisse lectus diam, bibendum quis maximus id, euismod placerat velit.Vestibulum hendrerit justo vel ultricies molestie.Donec rhoncus, ante at facilisis fermentum, diam diam hendrerit nunc, et dapibus lacus leo in massa.Duis iaculis sem sed molestie posuere.Morbi a erat hendrerit, volutpat libero non, elementum dui.
|
||||
|
||||
Cras imperdiet velit cursus, fringilla tellus eu, lacinia neque.Sed id est suscipit quam gravida vestibulum ut sed tortor.Aliquam erat volutpat.Praesent non orci ac quam consequat tempor.Nulla facilisi.Proin at vulputate lectus.Nunc at tellus at diam faucibus eleifend et et diam.Duis pellentesque lobortis lectus id egestas.Sed quis lacinia sapien.Quisque porta tincidunt pulvinar.Aliquam hendrerit hendrerit quam, sed pulvinar turpis dictum nec.
|
||||
|
||||
Donec bibendum, orci nec tempus fermentum, diam tellus pretium elit, vel porttitor ligula lectus a augue.Aliquam tristique, mi eu mollis sodales, enim lorem hendrerit est, id semper dui tellus id felis.Duis finibus lacus nunc, vitae tincidunt metus sagittis at.Curabitur euismod neque sed malesuada consectetur.Aliquam eget efficitur urna.Sed neque sem, interdum in turpis vitae, efficitur aliquam neque.Integer consectetur consequat diam, sed suscipit arcu maximus ac.Nunc imperdiet leo condimentum tellus luctus porta.Aenean et lorem sit amet eros rutrum fermentum.
|
||||
|
||||
Nam placerat leo sed nulla imperdiet dapibus.Etiam vitae tortor efficitur, interdum ipsum non, tincidunt ante.Quisque et placerat nisi, eu bibendum neque.Nulla facilisi.Pellentesque accumsan lacus arcu, vitae iaculis elit sollicitudin quis.Sed et iaculis neque.In quis nunc laoreet turpis fermentum sodales.Etiam eget sodales lorem.Nunc id risus ac purus mollis auctor.Integer imperdiet placerat massa eu efficitur.` });
|
||||
</script>
|
||||
|
|
@ -1,35 +1,45 @@
|
|||
<template>
|
||||
<div v-if="status === 'pending'" class="flex">
|
||||
<div v-if="overviewStatus === '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">
|
||||
<div class="flex flex-1 justify-start items-start" v-else-if="overview">
|
||||
<Head>
|
||||
<Title>d[any] - {{ page.title }}</Title>
|
||||
<Title>d[any] - {{ overview.title }}</Title>
|
||||
</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 flex-row justify-between items-center">
|
||||
<ProseH1>{{ page.title }}</ProseH1>
|
||||
<NuxtLink :href="{ name: 'explore-edit-path', params: { path: path } }"><Button v-if="isOwner">Modifier</Button></NuxtLink>
|
||||
<ProseH1>{{ overview.title }}</ProseH1>
|
||||
<div class="flex gap-4">
|
||||
<NuxtLink :href="{ name: 'explore-edit', hash: '#' + overview.path }" v-if="isOwner"><Button>Modifier</Button></NuxtLink>
|
||||
</div>
|
||||
<Markdown :content="page.content" />
|
||||
</div>
|
||||
<Markdown v-if="content" :content="content.content" />
|
||||
<Loading v-else />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page.type === 'canvas'">
|
||||
<Canvas :canvas="JSON.parse(page.content)" />
|
||||
<template v-else-if="overview.type === 'canvas'">
|
||||
<Canvas v-if="content" :canvas="JSON.parse(content.content)" />
|
||||
<Loading v-else />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ProseH2 class="flex-1 text-center">Impossible d'afficher le contenu demandé</ProseH2>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="status === 'error'">
|
||||
<div v-else-if="overviewStatus === 'error'">
|
||||
<Head>
|
||||
<Title>d[any] - Erreur</Title>
|
||||
</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 v-else>
|
||||
<Head>
|
||||
|
|
@ -43,15 +53,9 @@
|
|||
const route = useRouter().currentRoute;
|
||||
const path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path);
|
||||
|
||||
watch(path, () => {
|
||||
if(path.value === 'index')
|
||||
{
|
||||
useRouter().replace({ name: 'explore' });
|
||||
}
|
||||
}, { immediate: true });
|
||||
const { user } = useUserSession();
|
||||
|
||||
const { loggedIn, user } = useUserSession();
|
||||
|
||||
const { data: page, status, error } = await useFetch(`/api/file/${encodeURIComponent(path.value)}`, { watch: [route, path], });
|
||||
const isOwner = computed(() => user.value?.id === page.value?.owner);
|
||||
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 isOwner = computed(() => user.value?.id === overview.value?.owner);
|
||||
</script>
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
<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">
|
||||
<Head>
|
||||
<Title>d[any] - Modification de {{ page.title }}</Title>
|
||||
</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">
|
||||
<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" />
|
||||
<div class="flex gap-4 self-end xl:self-auto flex-wrap">
|
||||
<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="Afficher dans le menu de navigation" side="bottom"><Switch label="Navigable" v-model="page.navigable" /></Tooltip>
|
||||
</div>
|
||||
<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+Shift+Z" side="bottom"><NuxtLink :href="{ name: 'explore-path', params: { path: path } }"><Button>Annuler</Button></NuxtLink></Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-4 flex-1 w-full max-h-full flex">
|
||||
<template v-if="page.type === 'markdown'">
|
||||
<SplitterGroup direction="horizontal" class="flex-1 w-full flex" >
|
||||
<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 }" />
|
||||
</SplitterPanel>
|
||||
<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 }">
|
||||
<div class="flex-1 max-h-full !overflow-y-auto px-8" :class="{ 'hidden': isCollapsed }"><Markdown :content="debounced" :proses="{ 'a': FakeA }" /></div>
|
||||
</SplitterPanel>
|
||||
</SplitterGroup>
|
||||
</template>
|
||||
<template v-else-if="page.type === 'canvas'">
|
||||
<span class="flex-1 items-center"><ProseH1>Editeur de graphe en cours de développement</ProseH1></span>
|
||||
</template>
|
||||
<template v-else-if="page.type === 'file'">
|
||||
<span>Modifier le contenu :</span><input type="file" @change="(e) => console.log(e)" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="status === 'pending'" class="flex">
|
||||
<Head>
|
||||
<Title>d[any] - Chargement</Title>
|
||||
</Head>
|
||||
<Loading />
|
||||
</div>
|
||||
<div v-else-if="status === 'error'">{{ error?.message }}</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FakeA from '~/components/prose/FakeA.vue';
|
||||
|
||||
const nuxt = useNuxtApp();
|
||||
|
||||
const router = useRouter();
|
||||
const route = router.currentRoute;
|
||||
const path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path);
|
||||
const { user, loggedIn } = useUserSession();
|
||||
|
||||
const toaster = useToast();
|
||||
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
||||
|
||||
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) => {
|
||||
if(value && sessionContent.value !== undefined)
|
||||
{
|
||||
value.content = sessionContent.value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}, getCachedData: (key) => {
|
||||
const value = nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key];
|
||||
if(value && sessionContent.value !== undefined)
|
||||
{
|
||||
value.content = sessionContent.value;
|
||||
}
|
||||
|
||||
return value;
|
||||
} });
|
||||
|
||||
const content = computed(() => page.value?.content);
|
||||
const debounced = useDebounce(content, 250);
|
||||
|
||||
if(!loggedIn || (page.value && page.value.owner !== user.value?.id))
|
||||
{
|
||||
router.replace({ name: 'explore-path', params: { path: path.value } });
|
||||
}
|
||||
|
||||
watch(debounced, (value) => {
|
||||
sessionContent.value = value;
|
||||
});
|
||||
|
||||
useShortcuts({
|
||||
meta_s: { usingInput: true, handler: () => save(false) },
|
||||
meta_shift_z: { usingInput: true, handler: () => router.push({ name: 'explore-path', params: { path: path.value }}) }
|
||||
})
|
||||
|
||||
async function save(redirect: boolean): Promise<void>
|
||||
{
|
||||
saveStatus.value = 'pending';
|
||||
try {
|
||||
await $fetch(`/api/file`, {
|
||||
method: 'post',
|
||||
body: page.value,
|
||||
});
|
||||
saveStatus.value = 'success';
|
||||
sessionContent.value = undefined;
|
||||
|
||||
toaster.clear('error');
|
||||
toaster.add({
|
||||
type: 'success', content: 'Contenu enregistré', timer: true, duration: 10000
|
||||
});
|
||||
|
||||
if(redirect)
|
||||
router.push({ name: 'explore-path', params: { path: path.value } });
|
||||
} catch(e: any) {
|
||||
toaster.add({
|
||||
type: 'error', content: e.message, timer: true, duration: 10000
|
||||
})
|
||||
saveStatus.value = 'error';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,18 +1,97 @@
|
|||
<template>
|
||||
<Head>
|
||||
<Title>d[any] - Configuration du projet</Title>
|
||||
<Title>d[any] - Modification</Title>
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-row gap-4 p-6 items-start" v-if="navigation">
|
||||
<div class="flex flex-1 flex-col w-[450px] max-w-[450px] max-h-full">
|
||||
<DraggableTree class="list-none select-none border border-light-35 dark:border-dark-35 text-light-100 dark:text-dark-100 p-2 xl:text-base text-sm overflow-auto"
|
||||
:items="navigation ?? undefined" :get-key="(item: Partial<ProjectExtendedItem>) => item.path !== undefined ? getPath(item as ProjectExtendedItem) : ''" @updateTree="drop">
|
||||
<ClientOnly>
|
||||
<CollapsibleRoot asChild class="flex flex-1 flex-col xl:-mx-12 xl:-my-8 lg:-mx-8 lg:-my-6 -mx-6 -my-3 overflow-hidden" v-model="open">
|
||||
<div>
|
||||
<div class="z-50 md:hidden flex w-full items-center justify-between h-12 border-b border-light-35 dark:border-dark-35">
|
||||
<div class="flex items-center px-2">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button icon class="ms-2 !bg-transparent group">
|
||||
<Icon class="group-data-[state=open]:hidden" icon="radix-icons:hamburger-menu" />
|
||||
<Icon class="group-data-[state=closed]:hidden" icon="radix-icons:cross-1" />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
<div class="flex items-center px-2">
|
||||
<Tooltip message="Changer de theme" side="left"><ThemeSwitch /></Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-row relative overflow-hidden">
|
||||
<CollapsibleContent asChild forceMount>
|
||||
<div class=" overflow-hidden bg-light-0 dark:bg-dark-0 z-40 xl:w-96 md:w-[15em] max-h-full w-full border-r border-light-30 dark:border-dark-30 flex flex-col justify-between max-md:absolute max-md:-top-0 max-md:-bottom-0 md:left-0 max-md:data-[state=closed]:-left-full max-md:transition-[left] max-md:z-40 max-md:data-[state=open]:left-0">
|
||||
<div class="flex flex-col gap-4 xl:px-6 px-3 py-4">
|
||||
<div class="flex justify-between items-center max-md:hidden">
|
||||
<div class=" text-light-100 dark:text-dark-100 hover:text-opacity-70 max-md:ps-6" aria-label="Accueil">
|
||||
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
|
||||
<Avatar src="/logo.light.svg" class="block dark:hidden" />
|
||||
</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<Tooltip message="Changer de theme" side="left"><ThemeSwitch /></Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col max-w-full max-h-full overflow-hidden py-3" v-if="navigation">
|
||||
<div class="flex flex-row justify-between items-center mb-4 px-6">
|
||||
<div class="flex flex-1 flex-row justify-start items-center gap-4">
|
||||
<Tooltip side="top" message="Annuler (Ctrl+Shift+W)" ><Button icon @click="router.go(-1)"><Icon class="w-5 h-5" icon="radix-icons:arrow-left" /></Button></Tooltip>
|
||||
<Tooltip side="top" message="Enregistrer (Ctrl+S)" ><Button icon :loading="saveStatus === 'pending'" @click="save(true)"><Icon class="w-5 h-5" icon="radix-icons:check" /></Button></Tooltip>
|
||||
<span v-if="edited" class="text-sm text-light-60 dark:text-dark-60 italic">Modifications non enregistrées</span>
|
||||
</div>
|
||||
<div class="flex flex-row justify-end items-center gap-4">
|
||||
<AlertDialogRoot v-if="selected">
|
||||
<Tooltip side="top" message="Supprimer"><AlertDialogTrigger as="span"><Button icon class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red" ><Icon class="w-5 h-5" icon="radix-icons:trash" /></Button></AlertDialogTrigger></Tooltip>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||
<AlertDialogContent class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100 flex md:flex-row flex-col gap-4 items-center">
|
||||
<AlertDialogTitle class="text-xl font-semibold">Supprimer <span>{{ selected.title }}</span><span v-if="selected.children"> et tous ces enfants</span> ?</AlertDialogTitle>
|
||||
<div class="flex flex-1 flex-row gap-4 justify-end">
|
||||
<AlertDialogAction asChild @click="navigation = tree.remove(navigation, getPath(selected))"><Button class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui</Button></AlertDialogAction>
|
||||
<AlertDialogCancel asChild><Button>Non</Button></AlertDialogCancel>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
<Tooltip side="top" message="Nouveau">
|
||||
<DropdownMenu align="center" side="bottom" :options="[{
|
||||
type: 'item',
|
||||
label: 'Markdown',
|
||||
kbd: 'Ctrl+N',
|
||||
icon: 'radix-icons:file',
|
||||
select: () => add('markdown'),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Dossier',
|
||||
kbd: 'Ctrl+Shift+N',
|
||||
icon: 'lucide:folder',
|
||||
select: () => add('folder'),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Canvas',
|
||||
icon: 'ph:graph-light',
|
||||
select: () => add('canvas'),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Fichier',
|
||||
icon: 'radix-icons:file-text',
|
||||
select: () => add('file'),
|
||||
}]">
|
||||
<Button icon><Icon class="w-5 h-5" icon="radix-icons:plus" /></Button>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<DraggableTree class="ps-4 pe-2 xl:text-base text-sm"
|
||||
:items="navigation ?? undefined" :get-key="(item: Partial<ProjectExtendedItem>) => item.path !== undefined ? getPath(item as ProjectExtendedItem) : ''" @updateTree="drop"
|
||||
v-model="selected" :defaultExpanded="defaultExpanded" >
|
||||
<template #default="{ handleToggle, handleSelect, isExpanded, isSelected, isDragging, item }">
|
||||
<div class="flex flex-1 items-center px-2" :class="{ 'opacity-50': isDragging }" :style="{ 'padding-left': `${item.level - 0.5}em` }">
|
||||
<div class="flex flex-1 items-center px-2 max-w-full pe-4" :class="{ 'opacity-50': isDragging }" :style="{ 'padding-left': `${item.level - 0.5}em` }">
|
||||
<span class="py-2 px-2" @click="handleToggle" v-if="item.hasChildren" >
|
||||
<Icon :icon="isExpanded ? 'lucide:folder-open' : 'lucide:folder'"/>
|
||||
</span>
|
||||
<Icon v-else-if="iconByType[item.value.type]" :icon="iconByType[item.value.type]" class="group-[:hover]:text-accent-purple mx-2" @click="() => { handleSelect(); selected = isSelected ? undefined : item.value; }" />
|
||||
<div class="pl-3 py-1 flex-1 truncate" :title="item.value.title" @click="() => { handleSelect(); selected = isSelected ? undefined : item.value; }">
|
||||
<Icon v-else-if="iconByType[item.value.type]" :icon="iconByType[item.value.type]" class="group-[:hover]:text-accent-purple mx-2" @click="handleSelect" />
|
||||
<div class="pl-3 py-1 flex-1 truncate" :title="item.value.title" @click="handleSelect" :class="{ 'font-semibold': item.hasChildren }">
|
||||
{{ item.value.title }}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
|
|
@ -38,73 +117,58 @@
|
|||
</template>
|
||||
</DraggableTree>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex self-end gap-4 px-4">
|
||||
<DropdownMenu align="center" side="bottom" :options="[{
|
||||
type: 'item',
|
||||
label: 'Markdown',
|
||||
kbd: 'Ctrl+N',
|
||||
icon: 'radix-icons:file',
|
||||
select: () => add('markdown'),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Dossier',
|
||||
kbd: 'Ctrl+Shift+N',
|
||||
icon: 'lucide:folder',
|
||||
select: () => add('folder'),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Canvas',
|
||||
icon: 'ph:graph-light',
|
||||
select: () => add('canvas'),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Fichier',
|
||||
icon: 'radix-icons:file-text',
|
||||
select: () => add('file'),
|
||||
}]">
|
||||
<Button>Nouveau</Button>
|
||||
</DropdownMenu>
|
||||
<Button @click="navigation = tree.remove(navigation, getPath(selected))" v-if="selected" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Supprimer</Button>
|
||||
<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+Shift+Z" side="bottom"><NuxtLink :href="{ name: 'explore' }"><Button>Annuler</Button></NuxtLink></Tooltip>
|
||||
</div>
|
||||
<div v-if="selected" class="flex-1 flex justify-start items-start">
|
||||
<div class="flex flex-col flex-1 justify-start items-start">
|
||||
<input type="text" v-model="selected.title" @change="(e) => {
|
||||
if(selected && !selected.customPath)
|
||||
{
|
||||
selected.name = parsePath(selected.title);
|
||||
rebuildPath(selected.children, getPath(selected));
|
||||
}
|
||||
}" 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 ms-6 flex-col justify-start items-start gap-2">
|
||||
</CollapsibleContent>
|
||||
<div class="flex flex-1 flex-row max-h-full overflow-hidden">
|
||||
<div v-if="selected" class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 flex-col items-start justify-start max-h-full relative">
|
||||
<Head>
|
||||
<Title>d[any] - Modification de {{ selected.title }}</Title>
|
||||
</Head>
|
||||
<div>
|
||||
<input type="text" v-model="selected.title" placeholder="Titre" class="inline md:h-16 h-12 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 py-1 md:text-5xl text-4xl font-thin bg-transparent" />
|
||||
<div class="flex flex-col justify-start items-start">
|
||||
<Switch label="Chemin personnalisé" v-model="selected.customPath" />
|
||||
<span>
|
||||
<pre v-if="selected.customPath" class="flex items-center">/{{ selected.parent !== '' ? selected.parent + '/' : '' }}<TextInput v-model="selected.name" @input="(e) => {
|
||||
<pre v-if="selected.customPath" class="flex md:items-center md:text-base md:text-nowrap text-wrap md:flex-nowrap flex-wrap text-sm">/{{ selected.parent !== '' ? selected.parent + '/' : '' }}<TextInput v-model="selected.name" @input="(e) => {
|
||||
if(selected && selected.customPath)
|
||||
{
|
||||
selected.name = parsePath(selected.name);
|
||||
rebuildPath(selected.children, getPath(selected));
|
||||
}
|
||||
}" class="mx-0"/></pre>
|
||||
<pre v-else>/{{ getPath(selected) }}</pre>
|
||||
<pre v-else class="md:text-base text-smmd:text-nowrap text-wrap ">/{{ getPath(selected) }}</pre>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<HoverCard class="!py-2 !px-4"><Icon icon="radix-icons:question-mark-circled" /><template #content><span class="text-sm italic text-light-60 dark:text-dark-60">Un fichier privé n'est consultable que par le propriétaire du projet. Rendre un dossier privé cache automatiquement son contenu sans avoir à chaque fichier un par un.</span></template></HoverCard>
|
||||
<Switch label="Privé" v-model="selected.private" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<HoverCard class="!py-2 !px-4"><Icon icon="radix-icons:question-mark-circled" /><template #content><span class="text-sm italic text-light-60 dark:text-dark-60">Un fichier navigable est disponible dans le menu de navigation à gauche. Les fichiers non navigable peuvent toujours être utilisés dans des liens.</span></template></HoverCard>
|
||||
<Switch label="Navigable" v-model="selected.navigable" />
|
||||
</div>
|
||||
<div class="py-4 flex-1 w-full max-h-full flex overflow-hidden">
|
||||
<template v-if="selected.type === 'markdown'">
|
||||
<div v-if="contentStatus === 'pending'" class="flex flex-1 justify-center items-center">
|
||||
<Loading />
|
||||
</div>
|
||||
<span v-else-if="contentError">{{ contentError }}</span>
|
||||
<SplitterGroup direction="horizontal" class="flex-1 w-full flex" v-else-if="selected.content !== undefined">
|
||||
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }" :defaultSize="50">
|
||||
<Editor v-model="selected.content" placeholder="Commencer votre aventure ..." class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none !overflow-y-auto" :class="{ 'hidden': isCollapsed }" />
|
||||
</SplitterPanel>
|
||||
<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 }">
|
||||
<div class="flex-1 max-h-full !overflow-y-auto px-8" :class="{ 'hidden': isCollapsed }"><Markdown :content="debounced" :proses="{ 'a': FakeA }" /></div>
|
||||
</SplitterPanel>
|
||||
</SplitterGroup>
|
||||
</template>
|
||||
<template v-else-if="selected.type === 'canvas'">
|
||||
<span class="flex flex-1 justify-center items-center"><ProseH3>Editeur de graphe en cours de développement</ProseH3></span>
|
||||
</template>
|
||||
<template v-else-if="selected.type === 'file'">
|
||||
<span>Modifier le contenu :</span><input type="file" @change="(e) => console.log(e)" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleRoot>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
@ -114,10 +178,12 @@ import { parsePath } from '#shared/general.utils';
|
|||
import type { ProjectItem } from '~/schemas/project';
|
||||
import type { FileType } from '~/schemas/file';
|
||||
import { iconByType } from '#shared/general.utils';
|
||||
import FakeA from '~/components/prose/FakeA.vue';
|
||||
|
||||
interface ProjectExtendedItem extends ProjectItem
|
||||
{
|
||||
customPath: boolean
|
||||
content?: string
|
||||
children?: ProjectExtendedItem[]
|
||||
}
|
||||
interface ProjectExtended
|
||||
|
|
@ -127,15 +193,17 @@ interface ProjectExtended
|
|||
|
||||
definePageMeta({
|
||||
rights: ['admin', 'editor'],
|
||||
layout: 'null',
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const open = ref(true);
|
||||
|
||||
const toaster = useToast();
|
||||
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
||||
|
||||
const { data: project } = await useFetch(`/api/project`, {
|
||||
transform: (project) =>{
|
||||
transform: (project) => {
|
||||
if(project)
|
||||
(project as ProjectExtended).items = transform(project.items)!;
|
||||
|
||||
|
|
@ -146,6 +214,7 @@ const navigation = computed<ProjectExtendedItem[] | undefined>({
|
|||
get: () => project.value?.items,
|
||||
set: (value) => {
|
||||
const proj = project.value;
|
||||
edited.value = true;
|
||||
|
||||
if(proj && value)
|
||||
proj.items = value;
|
||||
|
|
@ -153,13 +222,63 @@ const navigation = computed<ProjectExtendedItem[] | undefined>({
|
|||
project.value = proj;
|
||||
}
|
||||
});
|
||||
const selected = ref<ProjectExtendedItem>();
|
||||
const selected = ref<ProjectExtendedItem>(), edited = ref(false);
|
||||
const contentStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), contentError = ref<string>();
|
||||
|
||||
watch(selected, async (value, old) => {
|
||||
if(selected.value)
|
||||
{
|
||||
if(!selected.value.content && selected.value.path)
|
||||
{
|
||||
contentStatus.value = 'pending';
|
||||
try
|
||||
{
|
||||
const storedEdit = sessionStorage.getItem(`editing:${encodeURIComponent(selected.value.path)}`);
|
||||
|
||||
if(storedEdit)
|
||||
{
|
||||
selected.value.content = storedEdit;
|
||||
contentStatus.value = 'success';
|
||||
}
|
||||
else
|
||||
{
|
||||
selected.value.content = (await $fetch(`/api/file/content/${encodeURIComponent(selected.value.path)}`, { query: { type: 'editing'} }))?.content;
|
||||
contentStatus.value = 'success';
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
debounced.value = selected.value.content ?? '';
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
contentError.value = (e as Error).message;
|
||||
contentStatus.value = 'error';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//@ts-ignore
|
||||
debounced.value = selected.value.content ?? '';
|
||||
}
|
||||
router.replace({ hash: '#' + encodeURIComponent(selected.value.path || getPath(selected.value)) });
|
||||
}
|
||||
else
|
||||
{
|
||||
router.replace({ hash: '' });
|
||||
}
|
||||
})
|
||||
const content = computed(() => selected.value?.content ?? '');
|
||||
const debounced = useDebounce(content, 250, { maxWait: 500 });
|
||||
|
||||
watch(debounced, () => {
|
||||
if(selected.value && debounced.value)
|
||||
sessionStorage.setItem(`editing:${encodeURIComponent(selected.value.path)}`, debounced.value);
|
||||
});
|
||||
useShortcuts({
|
||||
meta_s: { usingInput: true, handler: () => save(false) },
|
||||
meta_n: { usingInput: true, handler: () => add('markdown') },
|
||||
meta_shift_n: { usingInput: true, handler: () => add('folder') },
|
||||
meta_shift_z: { usingInput: true, handler: () => router.push({ name: 'explore' }) }
|
||||
meta_shift_z: { usingInput: true, handler: () => router.push({ name: 'explore-path', params: { path: 'index' } }) }
|
||||
})
|
||||
|
||||
const tree = {
|
||||
|
|
@ -289,7 +408,7 @@ function add(type: FileType): void
|
|||
|
||||
const news = [...tree.search(navigation.value, 'title', 'Nouveau')].filter((e, i, a) => a.indexOf(e) === i);
|
||||
const title = `Nouveau${news.length > 0 ? ' (' + news.length +')' : ''}`;
|
||||
const item: ProjectExtendedItem = { navigable: true, private: false, parent: '', path: '', title: title, name: parsePath(title), type: type, order: 0, children: type === 'folder' ? [] : undefined, customPath: false };
|
||||
const item: ProjectExtendedItem = { navigable: true, private: false, parent: '', path: '', title: title, name: parsePath(title), type: type, order: 0, children: type === 'folder' ? [] : undefined, customPath: false, content: type === 'markdown' ? '' : undefined };
|
||||
|
||||
if(!selected.value)
|
||||
{
|
||||
|
|
@ -386,13 +505,15 @@ async function save(redirect: boolean): Promise<void>
|
|||
body: project.value,
|
||||
});
|
||||
saveStatus.value = 'success';
|
||||
edited.value = false;
|
||||
sessionStorage.clear();
|
||||
|
||||
toaster.clear('error');
|
||||
toaster.add({
|
||||
type: 'success', content: 'Contenu enregistré', timer: true, duration: 10000
|
||||
});
|
||||
|
||||
if(redirect) router.push({ name: 'explore' });
|
||||
if(redirect) router.go(-1);
|
||||
} catch(e: any) {
|
||||
toaster.add({
|
||||
type: 'error', content: e.message, timer: true, duration: 10000
|
||||
|
|
@ -404,4 +525,19 @@ function getPath(item: ProjectExtendedItem): string
|
|||
{
|
||||
return [item.parent, parsePath(item.customPath ? item.name : item.title)].filter(e => !!e).join('/');
|
||||
}
|
||||
|
||||
const defaultExpanded = computed(() => {
|
||||
if(router.currentRoute.value.hash)
|
||||
{
|
||||
const split = router.currentRoute.value.hash.substring(1).split('/');
|
||||
split.forEach((e, i) => { if(i !== 0) split[i] = split[i - 1] + '/' + e });
|
||||
return split;
|
||||
}
|
||||
})
|
||||
watch(router.currentRoute, (value) => {
|
||||
if(value && value.hash && navigation.value)
|
||||
selected.value = tree.find(navigation.value, decodeURIComponent(value.hash.substring(1)));
|
||||
else
|
||||
selected.value = undefined;
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
|
@ -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>
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Dashboard de statistiques</span></Label>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Editeur de permissions</span><ProseTag>prioritaire</ProseTag></Label>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Synchro project <-> GIT</span><ProseTag>prioritaire</ProseTag></Label>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Versionning automatisé, releases et newsletter</span></Label>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 justify-start">
|
||||
<ProseH3>Editeur</ProseH3>
|
||||
|
|
@ -28,14 +29,17 @@
|
|||
</div>
|
||||
<div class="flex flex-col gap-2 justify-start">
|
||||
<ProseH3>Nouvelles features</ProseH3>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Historique des modifs</span><ProseTag>prioritaire</ProseTag></Label>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Commentaire par page</span></Label>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Historique des modifs</span><ProseTag>prioritaire</ProseTag></Label><!-- Objet release: key hash, timestamp, version, name, description?. Objet edit: key hash, key property, value, timestamp -->
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Commentaire par page</span></Label><!-- Object comment: key path, key comment_id, position, content, owner, following? -->
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Type de fichier: Timeline</span></Label><!-- Propriétés: array of (from, (to || ponctual), ((title, content) || dedicated page)) -->
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Type de fichier: Whiteboard</span></Label><!-- Tableau de données SVG -->
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 justify-start">
|
||||
<ProseH3>Utilisateur</ProseH3>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Validation du compte par mail<ProseTag>prioritaire</ProseTag></span></Label>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Modification de profil</span></Label>
|
||||
<!-- <Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Modification de profil</span></Label> -->
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Image de profil</span></Label>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Préférence d'email</span></Label><!-- New features, newsletter et surveys -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const baseItem = z.object({
|
|||
navigable: z.boolean(),
|
||||
private: z.boolean(),
|
||||
order: z.number().finite(),
|
||||
content: z.string().optional(),
|
||||
});
|
||||
export const item: z.ZodType<ProjectItem> = baseItem.extend({
|
||||
children: z.lazy(() => item.array().optional()),
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import type { SitemapUrlInput } from '#sitemap/types'
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { explorerContentTable } from '~/db/schema';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
|
||||
export default defineSitemapEventHandler(() => {
|
||||
const db = useDatabase();
|
||||
const pages = db.select({ path: explorerContentTable.path, lastMod: explorerContentTable.timestamp }).from(explorerContentTable).where(and(eq(explorerContentTable.private, false), eq(explorerContentTable.navigable, true))).all();
|
||||
const pages = db.select({ path: explorerContentTable.path, lastMod: explorerContentTable.timestamp, navigable: explorerContentTable.navigable, private: explorerContentTable.private }).from(explorerContentTable).all();
|
||||
|
||||
return pages.map(e => ({
|
||||
return pages.filter(e => e.navigable && !e.private && e.path.split('/').map((_, i, a) => a.slice(0, i).join('/')).every(p => !pages.find(_p => _p.path === p)?.private)).map(e => ({
|
||||
loc: `/explore/${e.path}`,
|
||||
lastmod: e.lastMod,
|
||||
})) satisfies SitemapUrlInput[];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import { hasPermissions } from "~/shared/auth.util";
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { and, eq, notInArray } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { userSessionsTable } from "~/db/schema";
|
||||
|
||||
const schema = z.array(z.string());
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const session = await getUserSession(e);
|
||||
|
||||
if(!session || !session.user || !hasPermissions(session.user.permissions, ['admin']))
|
||||
{
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
}
|
||||
|
||||
const param = getRouterParam(e, 'id');
|
||||
|
||||
if(!param)
|
||||
{
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Forbidden',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const id = parseInt(param, 10);
|
||||
|
||||
const db = useDatabase();
|
||||
db.delete(userSessionsTable).where(eq(userSessionsTable.user_id, id)).run();
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
|
@ -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 { explorerContentTable } from '~/db/schema';
|
||||
import { schema } from '~/schemas/file';
|
||||
import { parsePath } from '~/shared/general.utils';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const body = await readValidatedBody(e, schema.safeParse);
|
||||
|
|
@ -10,9 +11,10 @@ export default defineEventHandler(async (e) => {
|
|||
throw body.error;
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(body.data.content, 'utf-8');
|
||||
|
||||
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() } });
|
||||
|
||||
if(content !== undefined)
|
||||
|
|
@ -23,3 +25,13 @@ export default defineEventHandler(async (e) => {
|
|||
setResponseStatus(e, 404);
|
||||
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,
|
||||
'title': explorerContentTable.title,
|
||||
'type': explorerContentTable.type,
|
||||
'content': sql<string>`cast(${explorerContentTable.content} as TEXT)`.as('content'),
|
||||
'navigable': explorerContentTable.navigable,
|
||||
'private': explorerContentTable.private,
|
||||
'order': explorerContentTable.order,
|
||||
|
|
@ -27,7 +26,18 @@ export default defineEventHandler(async (e) => {
|
|||
|
||||
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;
|
||||
}
|
||||
|
|
@ -20,22 +20,27 @@ export default defineEventHandler(async (e) => {
|
|||
throw body.error;
|
||||
}
|
||||
|
||||
const items = buildOrder(body.data.items);
|
||||
const items = buildOrder(body.data.items) as Array<ProjectItem & { match?: number }>;
|
||||
|
||||
const db = useDatabase();
|
||||
const { content, ...cols } = getTableColumns(explorerContentTable);
|
||||
const full = db.select(cols).from(explorerContentTable).all();
|
||||
const { ...cols } = getTableColumns(explorerContentTable);
|
||||
const full = db.select(cols).from(explorerContentTable).all() as Record<string, any>[];
|
||||
|
||||
for(let i = full.length - 1; i >= 0; i--)
|
||||
{
|
||||
if(items.find(e => (e.path === '' ? [e.parent, parsePath(e.name === '' ? e.title : e.name)].filter(e => !!e).join('/') : e.path) === full[i].path))
|
||||
full.splice(i, 1);
|
||||
const item = items.find(e => (e.path === '' ? [e.parent, parsePath(e.name === '' ? e.title : e.name)].filter(e => !!e).join('/') : e.path) === full[i].path);
|
||||
if(item)
|
||||
{
|
||||
item.match = i;
|
||||
full[i].include = true;
|
||||
}
|
||||
}
|
||||
|
||||
db.transaction((tx) => {
|
||||
for(let i = 0; i < items.length; i++)
|
||||
{
|
||||
const item = items[i];
|
||||
const old = full[item.match!];
|
||||
|
||||
tx.insert(explorerContentTable).values({
|
||||
path: item.path,
|
||||
|
|
@ -45,7 +50,7 @@ export default defineEventHandler(async (e) => {
|
|||
navigable: item.navigable,
|
||||
private: item.private,
|
||||
order: item.order,
|
||||
content: null,
|
||||
content: item.content ?? old.content,
|
||||
}).onConflictDoUpdate({
|
||||
set: {
|
||||
path: [item.parent, parsePath(item.name === '' ? item.title : item.name)].filter(e => !!e).join('/'),
|
||||
|
|
@ -55,12 +60,14 @@ export default defineEventHandler(async (e) => {
|
|||
private: item.private,
|
||||
order: item.order,
|
||||
timestamp: new Date(),
|
||||
content: item.content ?? old.content,
|
||||
},
|
||||
target: explorerContentTable.path,
|
||||
}).run();
|
||||
}
|
||||
for(let i = 0; i < full.length; i++)
|
||||
{
|
||||
if(full[i].include !== true)
|
||||
tx.delete(explorerContentTable).where(eq(explorerContentTable.path, full[i].path)).run();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { extname, basename } from 'node:path';
|
|||
import type { FileType } from '~/types/api';
|
||||
import type { CanvasColor, CanvasContent } from "~/types/canvas";
|
||||
import { explorerContentTable } from "~/db/schema";
|
||||
import { convertToStorableLinks } from "../api/file.post";
|
||||
|
||||
const typeMapping: Record<string, FileType> = {
|
||||
".md": "markdown",
|
||||
|
|
@ -27,7 +28,7 @@ export default defineTask({
|
|||
}
|
||||
}) 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')
|
||||
{
|
||||
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();
|
||||
db.delete(explorerContentTable).run();
|
||||
db.insert(explorerContentTable).values(files).run();
|
||||
|
||||
useStorage('cache').clear();
|
||||
|
||||
return { result: true };
|
||||
}
|
||||
catch(e)
|
||||
|
|
@ -85,6 +92,9 @@ function reshapeContent(content: string, type: FileType): string | null
|
|||
switch(type)
|
||||
{
|
||||
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":
|
||||
return content;
|
||||
case "canvas":
|
||||
|
|
|
|||
Loading…
Reference in New Issue