Finished project configuration page with reorder
This commit is contained in:
parent
a9363e8c06
commit
1c239f161b
|
|
@ -1,6 +1,10 @@
|
||||||
|
<script lang="ts">
|
||||||
|
const External = Annotation.define<boolean>();
|
||||||
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { dropCursor, crosshairCursor, keymap, EditorView } from '@codemirror/view';
|
import { dropCursor, crosshairCursor, keymap, EditorView, ViewUpdate } from '@codemirror/view';
|
||||||
import { EditorState } from '@codemirror/state';
|
import { Annotation, EditorState } from '@codemirror/state';
|
||||||
import { indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap } from '@codemirror/language';
|
import { indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap } from '@codemirror/language';
|
||||||
import { history, defaultKeymap, historyKeymap } from '@codemirror/commands';
|
import { history, defaultKeymap, historyKeymap } from '@codemirror/commands';
|
||||||
import { searchKeymap } from '@codemirror/search';
|
import { searchKeymap } from '@codemirror/search';
|
||||||
|
|
@ -36,7 +40,13 @@ onMounted(() => {
|
||||||
...foldKeymap,
|
...foldKeymap,
|
||||||
...completionKeymap,
|
...completionKeymap,
|
||||||
...lintKeymap
|
...lintKeymap
|
||||||
])
|
]),
|
||||||
|
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||||
|
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))
|
||||||
|
{
|
||||||
|
model.value = viewUpdate.state.doc.toString();
|
||||||
|
}
|
||||||
|
})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
view.value = new EditorView({
|
view.value = new EditorView({
|
||||||
|
|
@ -60,16 +70,15 @@ watchEffect(() => {
|
||||||
const currentValue = view.value ? view.value.state.doc.toString() : "";
|
const currentValue = view.value ? view.value.state.doc.toString() : "";
|
||||||
if (view.value && model.value !== currentValue) {
|
if (view.value && model.value !== currentValue) {
|
||||||
view.value.dispatch({
|
view.value.dispatch({
|
||||||
changes: { from: 0, to: currentValue.length, insert: model.value || "" }
|
changes: { from: 0, to: currentValue.length, insert: model.value || "" },
|
||||||
|
annotations: [External.of(true)],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-1 justify-center items-start p-12">
|
<div ref="editor" class="flex flex-1 w-full justify-stretch items-stretch border border-light-35 dark:border-dark-35 caret-light-100 dark:caret-dark-100" />
|
||||||
<div ref="editor" class="flex flex-1 justify-center items-stretch border border-light-35 dark:border-dark-35 caret-light-100 dark:caret-dark-100" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- <HoverPopup @before-show="fetch">
|
|
||||||
<template #content>
|
|
||||||
<Suspense suspensible>
|
|
||||||
<div class="mw-[400px]">
|
|
||||||
<div v-if="fetched === false" class="loading w-[400px] h-[150px]"></div>
|
|
||||||
<template v-else-if="!!data">
|
|
||||||
<div v-if="data.description" class="pb-4 pt-3 px-8">
|
|
||||||
<span class="text-2xl font-semibold">#{{ data.tag }}</span>
|
|
||||||
<Markdown :content="data.description"></Markdown>
|
|
||||||
</div>
|
|
||||||
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center" v-else>
|
|
||||||
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Fichier vide</div>
|
|
||||||
<div class="text-lg text-light-60 dark:text-dark-60">Cette page est vide</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center" v-else>
|
|
||||||
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Impossible d'afficher</div>
|
|
||||||
<div class="text-lg text-light-60 dark:text-dark-60">Cette page est impossible à traiter</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template #fallback><div class="loading w-[400px] h-[150px]"></div></template>
|
|
||||||
</Suspense>
|
|
||||||
</template>
|
|
||||||
<template #default>
|
|
||||||
<span class="before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30">
|
|
||||||
<slot></slot>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</HoverPopup> -->
|
|
||||||
<span class="before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30">
|
<span class="before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- <script setup lang="ts">
|
|
||||||
import type { Tag } from '~/types/api';
|
|
||||||
|
|
||||||
const { tag } = defineProps({
|
|
||||||
tag: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = ref<Tag>(), fetched = ref(false);
|
|
||||||
const route = useRoute();
|
|
||||||
const project = computed(() => parseInt(Array.isArray(route.params.projectId) ? '0' : route.params.projectId));
|
|
||||||
async function fetch()
|
|
||||||
{
|
|
||||||
if(fetched.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
data.value = await $fetch(`/api/project/${project.value}/tags/${encodeURIComponent(tag)}`);
|
|
||||||
fetched.value = true;
|
|
||||||
}
|
|
||||||
</script> -->
|
|
||||||
|
|
@ -9,7 +9,7 @@ export default function useDatabase()
|
||||||
{
|
{
|
||||||
const database = useRuntimeConfig().database;
|
const database = useRuntimeConfig().database;
|
||||||
const sqlite = new Database(database);
|
const sqlite = new Database(database);
|
||||||
instance = drizzle({ client: sqlite, schema });
|
instance = drizzle({ client: sqlite, schema, /* logger: true */ });
|
||||||
|
|
||||||
instance.run("PRAGMA journal_mode = WAL;");
|
instance.run("PRAGMA journal_mode = WAL;");
|
||||||
instance.run("PRAGMA foreign_keys = true;");
|
instance.run("PRAGMA foreign_keys = true;");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import type { ComputedRef, WatchSource } from 'vue'
|
||||||
|
import { logicAnd, logicNot } from '@vueuse/math'
|
||||||
|
import { useEventListener, useDebounceFn, createSharedComposable, useActiveElement } from '@vueuse/core'
|
||||||
|
|
||||||
|
export interface ShortcutConfig {
|
||||||
|
handler: Function
|
||||||
|
usingInput?: string | boolean
|
||||||
|
whenever?: WatchSource<boolean>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShortcutsConfig {
|
||||||
|
[key: string]: ShortcutConfig | Function
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShortcutsOptions {
|
||||||
|
chainDelay?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Shortcut {
|
||||||
|
handler: Function
|
||||||
|
condition: ComputedRef<boolean>
|
||||||
|
chained: boolean
|
||||||
|
// KeyboardEvent attributes
|
||||||
|
key: string
|
||||||
|
ctrlKey: boolean
|
||||||
|
metaKey: boolean
|
||||||
|
shiftKey: boolean
|
||||||
|
altKey: boolean
|
||||||
|
// code?: string
|
||||||
|
// keyCode?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const chainedShortcutRegex = /^[^-]+.*-.*[^-]+$/
|
||||||
|
const combinedShortcutRegex = /^[^_]+.*_.*[^_]+$/
|
||||||
|
|
||||||
|
export const useShortcuts = (config: ShortcutsConfig, options: ShortcutsOptions = {}) => {
|
||||||
|
const { macOS, usingInput } = _useShortcuts()
|
||||||
|
|
||||||
|
let shortcuts: Shortcut[] = []
|
||||||
|
|
||||||
|
const chainedInputs = ref<string[]>([])
|
||||||
|
const clearChainedInput = () => {
|
||||||
|
chainedInputs.value.splice(0, chainedInputs.value.length)
|
||||||
|
}
|
||||||
|
const debouncedClearChainedInput = useDebounceFn(clearChainedInput, options.chainDelay ?? 800)
|
||||||
|
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
// Input autocomplete triggers a keydown event
|
||||||
|
if (!e.key) { return }
|
||||||
|
|
||||||
|
const alphabeticalKey = /^[a-z]{1}$/i.test(e.key)
|
||||||
|
|
||||||
|
let chainedKey
|
||||||
|
chainedInputs.value.push(e.key)
|
||||||
|
// try matching a chained shortcut
|
||||||
|
if (chainedInputs.value.length >= 2) {
|
||||||
|
chainedKey = chainedInputs.value.slice(-2).join('-')
|
||||||
|
|
||||||
|
for (const shortcut of shortcuts.filter(s => s.chained)) {
|
||||||
|
if (shortcut.key !== chainedKey) { continue }
|
||||||
|
|
||||||
|
if (shortcut.condition.value) {
|
||||||
|
e.preventDefault()
|
||||||
|
shortcut.handler()
|
||||||
|
}
|
||||||
|
clearChainedInput()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try matching a standard shortcut
|
||||||
|
for (const shortcut of shortcuts.filter(s => !s.chained)) {
|
||||||
|
if (e.key.toLowerCase() !== shortcut.key) { continue }
|
||||||
|
if (e.metaKey !== shortcut.metaKey) { continue }
|
||||||
|
if (e.ctrlKey !== shortcut.ctrlKey) { continue }
|
||||||
|
// shift modifier is only checked in combination with alphabetical keys
|
||||||
|
// (shift with non-alphabetical keys would change the key)
|
||||||
|
if (alphabeticalKey && e.shiftKey !== shortcut.shiftKey) { continue }
|
||||||
|
// alt modifier changes the combined key anyways
|
||||||
|
// if (e.altKey !== shortcut.altKey) { continue }
|
||||||
|
|
||||||
|
if (shortcut.condition.value) {
|
||||||
|
e.preventDefault()
|
||||||
|
shortcut.handler()
|
||||||
|
}
|
||||||
|
clearChainedInput()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
debouncedClearChainedInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map config to full detailled shortcuts
|
||||||
|
shortcuts = Object.entries(config).map(([key, shortcutConfig]) => {
|
||||||
|
if (!shortcutConfig) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse key and modifiers
|
||||||
|
let shortcut: Partial<Shortcut>
|
||||||
|
|
||||||
|
if (key.includes('-') && key !== '-' && !key.match(chainedShortcutRegex)?.length) {
|
||||||
|
console.trace(`[Shortcut] Invalid key: "${key}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.includes('_') && key !== '_' && !key.match(combinedShortcutRegex)?.length) {
|
||||||
|
console.trace(`[Shortcut] Invalid key: "${key}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const chained = key.includes('-') && key !== '-'
|
||||||
|
if (chained) {
|
||||||
|
shortcut = {
|
||||||
|
key: key.toLowerCase(),
|
||||||
|
metaKey: false,
|
||||||
|
ctrlKey: false,
|
||||||
|
shiftKey: false,
|
||||||
|
altKey: false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const keySplit = key.toLowerCase().split('_').map(k => k)
|
||||||
|
shortcut = {
|
||||||
|
key: keySplit.filter(k => !['meta', 'ctrl', 'shift', 'alt'].includes(k)).join('_'),
|
||||||
|
metaKey: keySplit.includes('meta'),
|
||||||
|
ctrlKey: keySplit.includes('ctrl'),
|
||||||
|
shiftKey: keySplit.includes('shift'),
|
||||||
|
altKey: keySplit.includes('alt')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shortcut.chained = chained
|
||||||
|
|
||||||
|
// Convert Meta to Ctrl for non-MacOS
|
||||||
|
if (!macOS.value && shortcut.metaKey && !shortcut.ctrlKey) {
|
||||||
|
shortcut.metaKey = false
|
||||||
|
shortcut.ctrlKey = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve handler function
|
||||||
|
if (typeof shortcutConfig === 'function') {
|
||||||
|
shortcut.handler = shortcutConfig
|
||||||
|
} else if (typeof shortcutConfig === 'object') {
|
||||||
|
shortcut = { ...shortcut, handler: shortcutConfig.handler }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shortcut.handler) {
|
||||||
|
console.trace('[Shortcut] Invalid value')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create shortcut computed
|
||||||
|
const conditions: ComputedRef<boolean>[] = []
|
||||||
|
if (!(shortcutConfig as ShortcutConfig).usingInput) {
|
||||||
|
conditions.push(logicNot(usingInput))
|
||||||
|
} else if (typeof (shortcutConfig as ShortcutConfig).usingInput === 'string') {
|
||||||
|
conditions.push(computed(() => usingInput.value === (shortcutConfig as ShortcutConfig).usingInput))
|
||||||
|
}
|
||||||
|
shortcut.condition = logicAnd(...conditions, ...((shortcutConfig as ShortcutConfig).whenever || []))
|
||||||
|
|
||||||
|
return shortcut as Shortcut
|
||||||
|
}).filter(Boolean) as Shortcut[]
|
||||||
|
|
||||||
|
useEventListener('keydown', onKeyDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const _useShortcuts = () => {
|
||||||
|
const macOS = computed(() => process.client && navigator && navigator.userAgent && navigator.userAgent.match(/Macintosh;/))
|
||||||
|
|
||||||
|
const metaSymbol = ref(' ')
|
||||||
|
|
||||||
|
const activeElement = useActiveElement()
|
||||||
|
const usingInput = computed(() => {
|
||||||
|
const usingInput = !!(activeElement.value?.tagName === 'INPUT' || activeElement.value?.tagName === 'TEXTAREA' || activeElement.value?.contentEditable === 'true')
|
||||||
|
|
||||||
|
if (usingInput) {
|
||||||
|
return ((activeElement.value as any)?.name as string) || true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
metaSymbol.value = macOS.value ? '⌘' : 'Ctrl'
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
macOS,
|
||||||
|
metaSymbol,
|
||||||
|
activeElement,
|
||||||
|
usingInput
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -1,4 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Erreur {{ error?.statusCode }}</Title>
|
||||||
|
</Head>
|
||||||
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden justify-center items-center flex-col gap-4">
|
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden justify-center items-center flex-col gap-4">
|
||||||
<NuxtRouteAnnouncer/>
|
<NuxtRouteAnnouncer/>
|
||||||
<div class="flex gap-4 items-center">
|
<div class="flex gap-4 items-center">
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@
|
||||||
<Tree v-if="pages" v-model="pages"/>
|
<Tree v-if="pages" v-model="pages"/>
|
||||||
</div>
|
</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 text-start text-xs text-light-60 dark:text-dark-60 relative top-4">
|
||||||
|
<NuxtLink class="hover:underline italic" :to="{ name: 'roadmap' }">Roadmap</NuxtLink> -
|
||||||
<NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
<NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
||||||
<p>Copyright Peaceultime - 2024</p>
|
<p>Copyright Peaceultime - 2024</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -108,8 +109,8 @@ const { data: pages } = await useLazyFetch('/api/navigation', {
|
||||||
watch: [useRouter().currentRoute]
|
watch: [useRouter().currentRoute]
|
||||||
});
|
});
|
||||||
|
|
||||||
function transform(list: NavigationTreeItem[]): any[]
|
function transform(list: NavigationTreeItem[] | undefined): any[] | undefined
|
||||||
{
|
{
|
||||||
return list?.map(e => ({ label: e.title, children: transform(e?.children ?? []), link: e.path, tag: e.private ? 'private' : e.type, open: path.value?.startsWith(e.path)}))
|
return list?.map(e => ({ label: e.title, children: transform(e?.children ?? undefined), link: e.path, tag: e.private ? 'private' : e.type, open: path.value?.startsWith(e.path)}))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"@nuxtjs/color-mode": "^3.5.2",
|
"@nuxtjs/color-mode": "^3.5.2",
|
||||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||||
"@vueuse/gesture": "^2.0.0",
|
"@vueuse/gesture": "^2.0.0",
|
||||||
|
"@vueuse/math": "^11.2.0",
|
||||||
"@vueuse/nuxt": "^11.1.0",
|
"@vueuse/nuxt": "^11.1.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"drizzle-orm": "^0.35.3",
|
"drizzle-orm": "^0.35.3",
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@
|
||||||
<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="page.navigable" /></Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<Button @click="() => save()" :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 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>
|
||||||
<NuxtLink :href="{ name: 'explore-path', params: { path: path } }"><Button>Annuler</Button></NuxtLink>
|
<Tooltip message="Ctrl+Shift+Z" side="bottom"><NuxtLink :href="{ name: 'explore-path', params: { path: path } }"><Button>Annuler</Button></NuxtLink></Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
<template v-if="page.type === 'markdown'">
|
<template v-if="page.type === 'markdown'">
|
||||||
<SplitterGroup direction="horizontal" class="flex-1 w-full flex" >
|
<SplitterGroup direction="horizontal" class="flex-1 w-full flex" >
|
||||||
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }" :defaultSize="50">
|
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }" :defaultSize="50">
|
||||||
<textarea v-model="content" class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none !overflow-y-auto" :class="{ 'hidden': isCollapsed }"></textarea>
|
<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 }">
|
||||||
|
|
@ -48,28 +48,52 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import FakeA from '~/components/prose/FakeA.vue';
|
import FakeA from '~/components/prose/FakeA.vue';
|
||||||
|
|
||||||
const route = useRouter().currentRoute;
|
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 path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path);
|
||||||
const { user, loggedIn } = useUserSession();
|
const { user, loggedIn } = useUserSession();
|
||||||
const sessionContent = useSessionStorage<string | undefined>(path.value, undefined);
|
|
||||||
|
|
||||||
const toaster = useToast();
|
const toaster = useToast();
|
||||||
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
||||||
|
|
||||||
const { data: page, status, error } = await useLazyFetch(`/api/file/${encodeURIComponent(path.value)}`, { watch: [ route, path ]});
|
const sessionContent = useSessionStorage<string | undefined>(path.value, undefined);
|
||||||
const content = computed(() => sessionContent.value ?? page.value?.content);
|
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);
|
const debounced = useDebounce(content, 250);
|
||||||
|
|
||||||
if(!loggedIn || (page.value && page.value.owner !== user.value?.id))
|
if(!loggedIn || (page.value && page.value.owner !== user.value?.id))
|
||||||
{
|
{
|
||||||
useRouter().replace({ name: 'explore-path', params: { path: path.value } });
|
router.replace({ name: 'explore-path', params: { path: path.value } });
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(debounced, (value) => {
|
watch(debounced, (value) => {
|
||||||
sessionContent.value = value;
|
sessionContent.value = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function save(): Promise<void>
|
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';
|
saveStatus.value = 'pending';
|
||||||
try {
|
try {
|
||||||
|
|
@ -78,13 +102,15 @@ async function save(): Promise<void>
|
||||||
body: page.value,
|
body: page.value,
|
||||||
});
|
});
|
||||||
saveStatus.value = 'success';
|
saveStatus.value = 'success';
|
||||||
|
sessionContent.value = undefined;
|
||||||
|
|
||||||
toaster.clear('error');
|
toaster.clear('error');
|
||||||
toaster.add({
|
toaster.add({
|
||||||
type: 'success', content: 'Contenu enregistré', timer: true, duration: 10000
|
type: 'success', content: 'Contenu enregistré', timer: true, duration: 10000
|
||||||
});
|
});
|
||||||
|
|
||||||
useRouter().push({ name: 'explore-path', params: { path: path.value } });
|
if(redirect)
|
||||||
|
router.push({ name: 'explore-path', params: { path: path.value } });
|
||||||
} catch(e: any) {
|
} catch(e: any) {
|
||||||
toaster.add({
|
toaster.add({
|
||||||
type: 'error', content: e.message, timer: true, duration: 10000
|
type: 'error', content: e.message, timer: true, duration: 10000
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
<Head>
|
<Head>
|
||||||
<Title>d[any] - Configuration du projet</Title>
|
<Title>d[any] - Configuration du projet</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<div class="flex flex-1 flex-row gap-4 p-6 items-start">
|
<div class="flex flex-1 flex-row gap-4 p-6 items-start" v-if="navigation">
|
||||||
<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 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 w-[450px] max-h-full"
|
||||||
:items="navigation ?? undefined" :get-key="(item) => item.path" @updateTree="drop">
|
:items="navigation ?? undefined" :get-key="(item: Partial<ProjectItem>) => item.path ? getPath(item as ProjectItem) : ''" @updateTree="drop">
|
||||||
<template #default="{ handleToggle, handleSelect, isExpanded, isSelected, isDragging, isDraggedOver, item }">
|
<template #default="{ handleToggle, handleSelect, isExpanded, isSelected, isDragging, isDraggedOver, item }">
|
||||||
<div class="flex flex-1 px-2" :class="{ 'opacity-50': isDragging }" :style="{ 'padding-left': `${item.level - 0.5}em` }">
|
<div class="flex flex-1 px-2" :class="{ 'opacity-50': isDragging }" :style="{ 'padding-left': `${item.level - 0.5}em` }">
|
||||||
<span class="py-2 px-2" @click="handleToggle" v-if="item.hasChildren" >
|
<span class="py-2 px-2" @click="handleToggle" v-if="item.hasChildren" >
|
||||||
|
|
@ -16,16 +16,24 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #hint="{ instruction }">
|
<template #hint="{ instruction }">
|
||||||
<div v-if="instruction" class="absolute h-full w-full top-0 left-0 border-light-50 dark:border-dark-50" :class="{
|
<div v-if="instruction" class="absolute h-full w-full top-0 right-0 border-light-50 dark:border-dark-50" :style="{
|
||||||
'!border-b-2': instruction?.type === 'reorder-below',
|
width: `calc(100% - ${instruction.currentLevel - 1}em)`
|
||||||
'!border-t-2': instruction?.type === 'reorder-above',
|
}" :class="{
|
||||||
'!border-2 rounded': instruction?.type === 'make-child',
|
'!border-b-4': instruction?.type === 'reorder-below',
|
||||||
|
'!border-t-4': instruction?.type === 'reorder-above',
|
||||||
|
'!border-4': instruction?.type === 'make-child',
|
||||||
}"></div>
|
}"></div>
|
||||||
</template>
|
</template>
|
||||||
</DraggableTree>
|
</DraggableTree>
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<div class="flex self-end gap-4 px-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' }"><Button>Annuler</Button></NuxtLink></Tooltip>
|
||||||
|
</div>
|
||||||
<div v-if="selected" class="flex-1 flex justify-start items-start">
|
<div v-if="selected" class="flex-1 flex justify-start items-start">
|
||||||
<div class="flex flex-col flex-1 justify-start items-start">
|
<div class="flex flex-col flex-1 justify-start items-start">
|
||||||
<input type="text" v-model="selected.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="selected.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" />
|
||||||
|
<span><pre class="ps-2 inline">{{ selected.parent }}/</pre><input v-model="selected.name" placeholder="Titre" class="font-mono border-b border-light-35 dark:border-dark-35 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 bg-transparent" /></span>
|
||||||
<div class="flex ms-6 flex-col justify-start items-start">
|
<div class="flex ms-6 flex-col justify-start items-start">
|
||||||
<Tooltip message="Consultable uniquement par le propriétaire" side="right"><Switch label="Privé" v-model="selected.private" /></Tooltip>
|
<Tooltip message="Consultable uniquement par le propriétaire" side="right"><Switch label="Privé" v-model="selected.private" /></Tooltip>
|
||||||
<Tooltip message="Afficher dans le menu de navigation" side="right"><Switch label="Navigable" v-model="selected.navigable" /></Tooltip>
|
<Tooltip message="Afficher dans le menu de navigation" side="right"><Switch label="Navigable" v-model="selected.navigable" /></Tooltip>
|
||||||
|
|
@ -33,32 +41,50 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NavigationTreeItem } from '~/server/api/navigation.get';
|
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/tree-item';
|
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/tree-item';
|
||||||
import { parsePath } from '#shared/general.utils';
|
import { parsePath } from '#shared/general.utils';
|
||||||
|
import type { ProjectItem } from '~/schemas/project';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
rights: ['admin', 'editor'],
|
rights: ['admin', 'editor'],
|
||||||
})
|
});
|
||||||
|
|
||||||
const route = useRouter().currentRoute;
|
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 path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path);
|
||||||
|
|
||||||
const toaster = useToast();
|
const toaster = useToast();
|
||||||
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
||||||
|
|
||||||
const { data: navigation } = await useLazyFetch(`/api/navigation`);
|
const { data: project } = await useFetch(`/api/project`);
|
||||||
const selected = ref<NavigationTreeItem>();
|
const navigation = computed({
|
||||||
|
get: () => project.value?.items,
|
||||||
|
set: (value) => {
|
||||||
|
const proj = project.value;
|
||||||
|
|
||||||
|
if(proj && value)
|
||||||
|
proj.items = value;
|
||||||
|
|
||||||
|
project.value = proj;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const selected = ref<ProjectItem>();
|
||||||
|
|
||||||
|
useShortcuts({
|
||||||
|
meta_s: { usingInput: true, handler: () => save(false) },
|
||||||
|
meta_shift_z: { usingInput: true, handler: () => router.push({ name: 'explore-path', params: { path: path.value }}) }
|
||||||
|
})
|
||||||
|
|
||||||
const tree = {
|
const tree = {
|
||||||
remove(data: NavigationTreeItem[], id: string): NavigationTreeItem[] {
|
remove(data: ProjectItem[], id: string): ProjectItem[] {
|
||||||
return data
|
return data
|
||||||
.filter(item => parsePath(item.title) !== id)
|
.filter(item => getPath(item) !== id)
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
if (tree.hasChildren(item)) {
|
if (tree.hasChildren(item)) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -69,9 +95,9 @@ const tree = {
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
insertBefore(data: NavigationTreeItem[], targetId: string, newItem: NavigationTreeItem): NavigationTreeItem[] {
|
insertBefore(data: ProjectItem[], targetId: string, newItem: ProjectItem): ProjectItem[] {
|
||||||
return data.flatMap((item) => {
|
return data.flatMap((item) => {
|
||||||
if (parsePath(item.title) === targetId)
|
if (getPath(item) === targetId)
|
||||||
return [newItem, item];
|
return [newItem, item];
|
||||||
|
|
||||||
if (tree.hasChildren(item)) {
|
if (tree.hasChildren(item)) {
|
||||||
|
|
@ -83,9 +109,9 @@ const tree = {
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
insertAfter(data: NavigationTreeItem[], targetId: string, newItem: NavigationTreeItem): NavigationTreeItem[] {
|
insertAfter(data: ProjectItem[], targetId: string, newItem: ProjectItem): ProjectItem[] {
|
||||||
return data.flatMap((item) => {
|
return data.flatMap((item) => {
|
||||||
if (parsePath(item.title) === targetId)
|
if (getPath(item) === targetId)
|
||||||
return [item, newItem];
|
return [item, newItem];
|
||||||
|
|
||||||
if (tree.hasChildren(item)) {
|
if (tree.hasChildren(item)) {
|
||||||
|
|
@ -98,9 +124,9 @@ const tree = {
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
insertChild(data: NavigationTreeItem[], targetId: string, newItem: NavigationTreeItem): NavigationTreeItem[] {
|
insertChild(data: ProjectItem[], targetId: string, newItem: ProjectItem): ProjectItem[] {
|
||||||
return data.flatMap((item) => {
|
return data.flatMap((item) => {
|
||||||
if (parsePath(item.title) === targetId) {
|
if (getPath(item) === targetId) {
|
||||||
// already a parent: add as first child
|
// already a parent: add as first child
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
|
|
@ -119,9 +145,9 @@ const tree = {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
find(data: NavigationTreeItem[], itemId: string): NavigationTreeItem | undefined {
|
find(data: ProjectItem[], itemId: string): ProjectItem | undefined {
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
if (parsePath(item.title) === itemId)
|
if (getPath(item) === itemId)
|
||||||
return item;
|
return item;
|
||||||
|
|
||||||
if (tree.hasChildren(item)) {
|
if (tree.hasChildren(item)) {
|
||||||
|
|
@ -136,33 +162,34 @@ const tree = {
|
||||||
targetId,
|
targetId,
|
||||||
parentIds = [],
|
parentIds = [],
|
||||||
}: {
|
}: {
|
||||||
current: NavigationTreeItem[]
|
current: ProjectItem[]
|
||||||
targetId: string
|
targetId: string
|
||||||
parentIds?: string[]
|
parentIds?: string[]
|
||||||
}): string[] | undefined {
|
}): string[] | undefined {
|
||||||
for (const item of current) {
|
for (const item of current) {
|
||||||
if (parsePath(item.title) === targetId)
|
if (getPath(item) === targetId)
|
||||||
return parentIds;
|
return parentIds;
|
||||||
|
|
||||||
const nested = tree.getPathToItem({
|
const nested = tree.getPathToItem({
|
||||||
current: (item.children ?? []),
|
current: (item.children ?? []),
|
||||||
targetId,
|
targetId,
|
||||||
parentIds: [...parentIds, parsePath(item.title)],
|
parentIds: [...parentIds, getPath(item)],
|
||||||
});
|
});
|
||||||
if (nested)
|
if (nested)
|
||||||
return nested;
|
return nested;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hasChildren(item: NavigationTreeItem): boolean {
|
hasChildren(item: ProjectItem): boolean {
|
||||||
return (item.children ?? []).length > 0;
|
return (item.children ?? []).length > 0;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTree(instruction: Instruction, itemId: string, targetId: string) {
|
function updateTree(instruction: Instruction, itemId: string, targetId: string) : ProjectItem[] | undefined {
|
||||||
if(!navigation.value)
|
if(!navigation.value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const item = tree.find(navigation.value, itemId);
|
const item = tree.find(navigation.value, itemId);
|
||||||
|
const target = tree.find(navigation.value, targetId);
|
||||||
|
|
||||||
if(!item)
|
if(!item)
|
||||||
return;
|
return;
|
||||||
|
|
@ -200,8 +227,12 @@ function updateTree(instruction: Instruction, itemId: string, targetId: string)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instruction.type === 'make-child') {
|
if (instruction.type === 'make-child') {
|
||||||
|
if(!target || target.type !== 'folder')
|
||||||
|
return;
|
||||||
|
|
||||||
let result = tree.remove(navigation.value, itemId);
|
let result = tree.remove(navigation.value, itemId);
|
||||||
result = tree.insertChild(result, targetId, item);
|
result = tree.insertChild(result, targetId, item);
|
||||||
|
rebuildPath([item], targetId);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,15 +241,26 @@ function updateTree(instruction: Instruction, itemId: string, targetId: string)
|
||||||
|
|
||||||
function drop(instruction: Instruction, itemId: string, targetId: string)
|
function drop(instruction: Instruction, itemId: string, targetId: string)
|
||||||
{
|
{
|
||||||
navigation.value = updateTree(instruction, itemId, targetId) ?? navigation.value;
|
navigation.value = updateTree(instruction, itemId, targetId) ?? navigation.value ?? [];
|
||||||
}
|
}
|
||||||
async function save(): Promise<void>
|
function rebuildPath(tree: ProjectItem[] | null | undefined, parentPath: string)
|
||||||
|
{
|
||||||
|
debugger;
|
||||||
|
if(!tree)
|
||||||
|
return;
|
||||||
|
|
||||||
|
tree.forEach(e => {
|
||||||
|
e.parent = parentPath;
|
||||||
|
rebuildPath(e.children, getPath(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function save(redirect: boolean): Promise<void>
|
||||||
{
|
{
|
||||||
saveStatus.value = 'pending';
|
saveStatus.value = 'pending';
|
||||||
try {
|
try {
|
||||||
await $fetch(`/api/navigation`, {
|
await $fetch(`/api/project`, {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
body: navigation.value,
|
body: project.value,
|
||||||
});
|
});
|
||||||
saveStatus.value = 'success';
|
saveStatus.value = 'success';
|
||||||
|
|
||||||
|
|
@ -227,7 +269,7 @@ async function save(): Promise<void>
|
||||||
type: 'success', content: 'Contenu enregistré', timer: true, duration: 10000
|
type: 'success', content: 'Contenu enregistré', timer: true, duration: 10000
|
||||||
});
|
});
|
||||||
|
|
||||||
useRouter().push({ name: 'explore-path', params: { path: path.value } });
|
if(redirect) router.push({ name: 'explore-path', params: { path: path.value } });
|
||||||
} catch(e: any) {
|
} catch(e: any) {
|
||||||
toaster.add({
|
toaster.add({
|
||||||
type: 'error', content: e.message, timer: true, duration: 10000
|
type: 'error', content: e.message, timer: true, duration: 10000
|
||||||
|
|
@ -235,4 +277,8 @@ async function save(): Promise<void>
|
||||||
saveStatus.value = 'error';
|
saveStatus.value = 'error';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function getPath(item: ProjectItem): string
|
||||||
|
{
|
||||||
|
return [item.parent, parsePath(item?.name ?? item.title)].filter(e => !!e).join('/');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<template>
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Roadmap</Title>
|
||||||
|
</Head>
|
||||||
|
<div class="flex flex-col justify-start p-6">
|
||||||
|
<ProseH2>Roadmap</ProseH2>
|
||||||
|
<div class="grid grid-cols-4 gap-x-2 gap-y-4">
|
||||||
|
<div class="flex flex-col gap-2 justify-start">
|
||||||
|
<ProseH3>Administration</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=" ">Statistiques de consultation</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=" ">Statistiques de connexion</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=" ">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" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Gestion de droits</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>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 justify-start">
|
||||||
|
<ProseH3>Editeur</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">Edition de 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" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Edition riche de 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=" ">Edition live de 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" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Raccourcis d'edition</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=" ">Affichage alternatif par page</span></Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 justify-start">
|
||||||
|
<ProseH3>Projet</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">Edition du projet</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">Déplacement des fichiers</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=" ">Configuration de droit du projet</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=" ">Theme par projet</span></Label>
|
||||||
|
</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>
|
||||||
|
</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" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">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=" ">Image de profil</span></Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const baseItem = z.object({
|
||||||
|
path: z.string(),
|
||||||
|
parent: z.string(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
title: z.string(),
|
||||||
|
type: z.enum(['folder', 'file', 'markdown', 'canvas']),
|
||||||
|
navigable: z.boolean(),
|
||||||
|
private: z.boolean(),
|
||||||
|
order: z.number().finite(),
|
||||||
|
});
|
||||||
|
export const item: z.ZodType<ProjectItem> = baseItem.extend({
|
||||||
|
children: z.lazy(() => item.array().optional()),
|
||||||
|
});
|
||||||
|
export const project = z.object({
|
||||||
|
items: z.array(item),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Project = z.infer<typeof project>;
|
||||||
|
export type ProjectItem = z.infer<typeof baseItem> & {
|
||||||
|
children?: ProjectItem[]
|
||||||
|
};
|
||||||
|
|
@ -21,6 +21,7 @@ export default defineEventHandler(async (e) => {
|
||||||
'content': sql<string>`cast(${explorerContentTable.content} as TEXT)`.as('content'),
|
'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,
|
||||||
}).from(explorerContentTable).where(eq(explorerContentTable.path, sql.placeholder('path'))).prepare().get({ path });
|
}).from(explorerContentTable).where(eq(explorerContentTable.path, sql.placeholder('path'))).prepare().get({ path });
|
||||||
|
|
||||||
if(content !== undefined)
|
if(content !== undefined)
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,6 @@ export type NavigationTreeItem = NavigationItem & { children?: NavigationTreeIte
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
const { user } = await getUserSession(e);
|
const { user } = await getUserSession(e);
|
||||||
|
|
||||||
/*if(!user)
|
|
||||||
{
|
|
||||||
throw createError({ statusCode: 401, statusText: 'Unauthorized' });
|
|
||||||
}*/
|
|
||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
const content = db.select({
|
const content = db.select({
|
||||||
path: explorerContentTable.path,
|
path: explorerContentTable.path,
|
||||||
|
|
@ -52,6 +47,7 @@ export default defineEventHandler(async (e) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
setResponseStatus(e, 404);
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
|
|
||||||
function addChild(arr: NavigationTreeItem[], e: NavigationItem): void
|
function addChild(arr: NavigationTreeItem[], e: NavigationItem): void
|
||||||
|
|
@ -69,7 +65,7 @@ function addChild(arr: NavigationTreeItem[], e: NavigationItem): void
|
||||||
{
|
{
|
||||||
arr.push({ ...e });
|
arr.push({ ...e });
|
||||||
arr.sort((a, b) => {
|
arr.sort((a, b) => {
|
||||||
if(a.order && b.order)
|
if(a.order !== b.order)
|
||||||
return a.order - b.order;
|
return a.order - b.order;
|
||||||
return a.title.localeCompare(b.title);
|
return a.title.localeCompare(b.title);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import { hasPermissions } from "#shared/auth.util";
|
|
||||||
import useDatabase from '~/composables/useDatabase';
|
|
||||||
import { explorerContentTable } from '~/db/schema';
|
|
||||||
import { table } from '~/schemas/navigation';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
|
||||||
const { user } = await getUserSession(e);
|
|
||||||
|
|
||||||
if(!user || !hasPermissions(user.permissions, ['admin', 'editor']))
|
|
||||||
{
|
|
||||||
throw createError({ statusCode: 401, statusText: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await readValidatedBody(e, table.safeParse);
|
|
||||||
|
|
||||||
if(!body.success)
|
|
||||||
{
|
|
||||||
throw body.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = useDatabase();
|
|
||||||
db.transaction((tx) => {
|
|
||||||
for(let i = 0; i < body.data.length; i++)
|
|
||||||
{
|
|
||||||
tx.insert(explorerContentTable).values({
|
|
||||||
path: body.data[i].path,
|
|
||||||
owner: body.data[i].owner,
|
|
||||||
title: body.data[i].title,
|
|
||||||
type: body.data[i].type,
|
|
||||||
navigable: body.data[i].navigable,
|
|
||||||
private: body.data[i].private,
|
|
||||||
order: body.data[i].order,
|
|
||||||
content: Buffer.from('', 'utf-8'),
|
|
||||||
}).onConflictDoUpdate({
|
|
||||||
set: {
|
|
||||||
owner: body.data[i].owner,
|
|
||||||
title: body.data[i].title,
|
|
||||||
type: body.data[i].type,
|
|
||||||
navigable: body.data[i].navigable,
|
|
||||||
private: body.data[i].private,
|
|
||||||
order: body.data[i].order,
|
|
||||||
},
|
|
||||||
target: explorerContentTable.path,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setResponseStatus(e, 404);
|
|
||||||
});
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { explorerContentTable } from '~/db/schema';
|
||||||
|
import type { NavigationItem } from '~/schemas/navigation';
|
||||||
|
import type { ProjectItem, Project } from '~/schemas/project';
|
||||||
|
import { hasPermissions } from '~/shared/auth.util';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const { user } = await getUserSession(e);
|
||||||
|
|
||||||
|
if(!user || !hasPermissions(user.permissions, ['editor', 'admin']))
|
||||||
|
{
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
statusMessage: 'Unauthorized',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const content = db.select({
|
||||||
|
path: explorerContentTable.path,
|
||||||
|
type: explorerContentTable.type,
|
||||||
|
owner: explorerContentTable.owner,
|
||||||
|
title: explorerContentTable.title,
|
||||||
|
navigable: explorerContentTable.navigable,
|
||||||
|
private: explorerContentTable.private,
|
||||||
|
order: explorerContentTable.order,
|
||||||
|
}).from(explorerContentTable).prepare().all();
|
||||||
|
|
||||||
|
if(content.length > 0)
|
||||||
|
{
|
||||||
|
const project: Project = {
|
||||||
|
items: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const item of content.filter(e => !!e))
|
||||||
|
{
|
||||||
|
addChild(project.items, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
function addChild(arr: ProjectItem[], e: NavigationItem): void
|
||||||
|
{
|
||||||
|
const parent = arr.find(f => e.path.startsWith(f.path));
|
||||||
|
|
||||||
|
if(parent)
|
||||||
|
{
|
||||||
|
if(!parent.children)
|
||||||
|
parent.children = [];
|
||||||
|
|
||||||
|
addChild(parent.children, e);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
arr.push({
|
||||||
|
path: e.path,
|
||||||
|
parent: e.path.substring(0, e.path.lastIndexOf('/')),
|
||||||
|
name: e.path.substring(e.path.lastIndexOf('/') + 1),
|
||||||
|
title: e.title,
|
||||||
|
type: e.type,
|
||||||
|
navigable: e.navigable,
|
||||||
|
private: e.private,
|
||||||
|
order: e.order,
|
||||||
|
});
|
||||||
|
arr.sort((a, b) => {
|
||||||
|
if(a.order !== b.order)
|
||||||
|
return a.order - b.order;
|
||||||
|
return a.title.localeCompare(b.title);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { hasPermissions } from "#shared/auth.util";
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { explorerContentTable } from '~/db/schema';
|
||||||
|
import { project, type ProjectItem } from '~/schemas/project';
|
||||||
|
import { parsePath } from "#shared/general.utils";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const { user } = await getUserSession(e);
|
||||||
|
|
||||||
|
if(!user || !hasPermissions(user.permissions, ['admin', 'editor']))
|
||||||
|
{
|
||||||
|
throw createError({ statusCode: 401, statusText: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readValidatedBody(e, project.safeParse);
|
||||||
|
|
||||||
|
if(!body.success)
|
||||||
|
{
|
||||||
|
throw body.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = buildOrder(body.data.items);
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
db.transaction((tx) => {
|
||||||
|
for(let i = 0; i < items.length; i++)
|
||||||
|
{
|
||||||
|
const item = items[i];
|
||||||
|
|
||||||
|
tx.insert(explorerContentTable).values({
|
||||||
|
path: item.path,
|
||||||
|
owner: user.id,
|
||||||
|
title: item.title,
|
||||||
|
type: item.type,
|
||||||
|
navigable: item.navigable,
|
||||||
|
private: item.private,
|
||||||
|
order: item.order,
|
||||||
|
content: Buffer.from('', 'utf-8'),
|
||||||
|
}).onConflictDoUpdate({
|
||||||
|
set: {
|
||||||
|
path: [item.parent, parsePath(item?.name ?? item.title)].filter(e => !!e).join('/'),
|
||||||
|
title: item.title,
|
||||||
|
type: item.type,
|
||||||
|
navigable: item.navigable,
|
||||||
|
private: item.private,
|
||||||
|
order: item.order,
|
||||||
|
},
|
||||||
|
target: explorerContentTable.path,
|
||||||
|
}).run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildOrder(items: ProjectItem[]): ProjectItem[]
|
||||||
|
{
|
||||||
|
items.forEach((e, i) => {
|
||||||
|
e.order = i;
|
||||||
|
if(e.children) e.children = buildOrder(e.children);
|
||||||
|
});
|
||||||
|
|
||||||
|
return items.flatMap(e => [e, ...(e.children ?? [])]);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue