Add title to every pages + new pull/push jobs + DropdownMenu
This commit is contained in:
parent
b54402fc19
commit
ac17134b7e
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<template v-for="(item, idx) of options">
|
||||
<template v-if="item.type === 'item'">
|
||||
<DropdownMenuItem :disabled="item.disabled" :textValue="item.label" @select="item.select">
|
||||
<Icon v-if="item.icon" :icon="item.icon" />
|
||||
<span>{{ item.label }}</span>
|
||||
<span v-if="item.kbd"> {{ item.kbd }} </span>
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
|
||||
<template v-else-if="item.type === 'checkbox'">
|
||||
<DropdownMenuCheckboxItem :disabled="item.disabled" :textValue="item.label" @update:checked="item.select">
|
||||
<DropdownMenuItemIndicator>
|
||||
<Icon icon="radix-icons:check" />
|
||||
</DropdownMenuItemIndicator>
|
||||
<span>{{ item.label }}</span>
|
||||
<span v-if="item.kbd"> {{ item.kbd }} </span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'radio'">
|
||||
<DropdownMenuLabel>{{ item.label }}</DropdownMenuLabel>
|
||||
<DropdownMenuRadioGroup @update:model-value="item.change">
|
||||
<DropdownMenuRadioItem v-for="option in item.items" :disabled="(option as any)?.disabled ?? false" :value="(option as any)?.value ?? option">
|
||||
<DropdownMenuItemIndicator>
|
||||
<Icon icon="radix-icons:dot-filled" />
|
||||
</DropdownMenuItemIndicator>
|
||||
<span>{{ (option as any)?.label || option }}</span>
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
|
||||
<DropdownMenuSeparator v-if="idx !== options.length - 1" />
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'submenu'">
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Icon v-if="item.icon" :icon="item.icon" />
|
||||
<span>{{ item.label }}</span>
|
||||
<Icon icon="radix-icons:chevron-right" />
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownContentRender :options="item.items" />
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'group'">
|
||||
<DropdownMenuLabel>{{ item.label }}</DropdownMenuLabel>
|
||||
<DropdownContetnRender :options="item.items" />
|
||||
|
||||
<DropdownMenuSeparator v-if="idx !== options.length - 1" />
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DropdownOption } from './DropdownMenu.vue';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
|
||||
const { options } = defineProps<{
|
||||
options: DropdownOption[]
|
||||
}>();
|
||||
</script>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<DropdownMenuRoot>
|
||||
<DropdownMenuTrigger :disabled="disabled" ><slot /></DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent :align="align" :side="side">
|
||||
<DropdownContentRender :options="options" />
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuArrow />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
export interface DropdownItem {
|
||||
type: 'item';
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
select?: () => void;
|
||||
icon?: string;
|
||||
kbd?: string;
|
||||
}
|
||||
export interface DropdownCheckbox {
|
||||
type: 'checkbox';
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
checked?: boolean | Ref<boolean>
|
||||
select?: (state: boolean) => void;
|
||||
kbd?: string;
|
||||
}
|
||||
export interface DropdownRadioGroup {
|
||||
type: 'radio';
|
||||
label: string;
|
||||
value?: string | Ref<string>
|
||||
items: (string | {label: string, value?: string, disabled?: boolean})[];
|
||||
change?: (value: string) => void;
|
||||
}
|
||||
export interface DropdownSubmenu {
|
||||
type: 'submenu';
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
items: DropdownOption[];
|
||||
icon?: string;
|
||||
}
|
||||
export interface DropdownGroup {
|
||||
type: 'group';
|
||||
label?: string;
|
||||
items: DropdownOption[];
|
||||
}
|
||||
export type DropdownOption = DropdownItem | DropdownCheckbox | DropdownRadioGroup | DropdownSubmenu | DropdownGroup;
|
||||
const { options, disabled = false, side, align } = defineProps<{
|
||||
options: DropdownOption[]
|
||||
disabled?: boolean
|
||||
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||
align?: 'start' | 'center' | 'end'
|
||||
}>();
|
||||
</script>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<TreeRoot v-slot="{ flattenItems }" class="list-none select-none text-light-100 dark:text-dark-100 p-2 xl:text-base text-sm" :items="model" :get-key="(item) => item.link ?? item.label">
|
||||
<TreeRoot v-slot="{ flattenItems }" class="list-none select-none text-light-100 dark:text-dark-100 p-2 xl:text-base text-sm" :items="model" :get-key="(item) => item.link ?? item.label" :defaultExpanded="flatten(model)">
|
||||
<TreeItem v-for="item in flattenItems" v-slot="{ isExpanded }" :key="item._id" :style="{ 'padding-left': `${item.level - 0.5}em` }" v-bind="item.bind" class="flex items-center px-2 outline-none relative cursor-pointer">
|
||||
<NuxtLink :href="item.value.link && !item.hasChildren ? { name: 'explore-path', params: { path: item.value.link } } : undefined" no-prefetch class="flex flex-1 items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" :class="{ 'border-s': !item.hasChildren, 'font-medium': item.hasChildren }" active-class="text-accent-blue border-s-2 !border-accent-blue">
|
||||
<Icon v-if="item.hasChildren" icon="radix-icons:chevron-right" :class="{ 'rotate-90': isExpanded }" class="h-4 w-4 transition-transform absolute" :style="{ 'left': `${item.level - 1}em` }" />
|
||||
|
|
@ -19,9 +19,15 @@ interface TreeItem
|
|||
label: string
|
||||
link?: string
|
||||
tag?: string
|
||||
open?: boolean
|
||||
children?: TreeItem[]
|
||||
}
|
||||
const model = defineModel<TreeItem[]>();
|
||||
|
||||
function flatten(arr: TreeItem[]): string[]
|
||||
{
|
||||
return arr.filter(e => e.open).flatMap(e => [e?.link ?? e.label, ...flatten(e.children ?? [])]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -12,12 +12,28 @@
|
|||
</div>
|
||||
<div class="flex items-center px-2">
|
||||
<Tooltip message="Changer de theme" side="left"><ThemeSwitch /></Tooltip>
|
||||
<Tooltip :message="loggedIn ? 'Mon profil' : 'Se connecter'" side="right">
|
||||
<NuxtLink class="" :to="{ name: 'user-profile' }">
|
||||
<Tooltip v-if="!loggedIn" :message="'Se connecter'" side="right">
|
||||
<div class="hover:border-opacity-70 flex">
|
||||
<Icon :icon="'radix-icons:person'" class="w-7 h-7 p-1" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip v-else :message="'Mon profil'" side="right">
|
||||
<DropdownMenu :options="[{
|
||||
type: 'group',
|
||||
items: [{
|
||||
type: 'item',
|
||||
label: 'Mon profil',
|
||||
click: () => useRouter().push({ name: 'user-profile' }),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Deconnexion',
|
||||
click: () => clear(),
|
||||
}]
|
||||
}]">
|
||||
<div class="hover:border-opacity-70 flex">
|
||||
<Icon :icon="loggedIn ? 'radix-icons:avatar' : 'radix-icons:person'" class="w-7 h-7 p-1" />
|
||||
<Icon :icon="'radix-icons:avatar'" class="w-7 h-7 p-1" />
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -32,13 +48,31 @@
|
|||
</NuxtLink>
|
||||
<div class="flex gap-4 items-center">
|
||||
<Tooltip message="Changer de theme" side="left"><ThemeSwitch /></Tooltip>
|
||||
<Tooltip :message="loggedIn ? 'Mon profil' : 'Se connecter'" side="right">
|
||||
<NuxtLink class="" :to="{ name: 'user-profile' }">
|
||||
<Tooltip v-if="!loggedIn" :message="'Se connecter'" side="right">
|
||||
<NuxtLink :to="{ name: 'user-login' }">
|
||||
<div class="bg-light-20 dark:bg-dark-20 hover:border-opacity-70 flex border p-px border-light-50 dark:border-dark-50">
|
||||
<Icon :icon="loggedIn ? 'radix-icons:avatar' : 'radix-icons:person'" class="w-7 h-7 p-1" />
|
||||
<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="[{
|
||||
type: 'group',
|
||||
items: [{
|
||||
type: 'item',
|
||||
label: 'Mon profil',
|
||||
click: () => useRouter().push({ name: 'user-profile' }),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Deconnexion',
|
||||
click: () => clear(),
|
||||
}]
|
||||
}]">
|
||||
<div class="bg-light-20 dark:bg-dark-20 hover:border-opacity-70 flex border p-px border-light-50 dark:border-dark-50">
|
||||
<Icon :icon="'radix-icons:avatar'" class="w-7 h-7 p-1" />
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -58,18 +92,21 @@
|
|||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
|
||||
const open = ref(false);
|
||||
const { loggedIn } = useUserSession();
|
||||
const { loggedIn, clear } = useUserSession();
|
||||
|
||||
const route = useRouter().currentRoute;
|
||||
const path = computed(() => route.value.params.path ? Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path : undefined);
|
||||
|
||||
watch(route, () => {
|
||||
open.value = false;
|
||||
});
|
||||
|
||||
const { data: pages } = await useLazyFetch('/api/navigation', {
|
||||
transform: transform,
|
||||
});
|
||||
|
||||
watch(useRouter().currentRoute, () => {
|
||||
open.value = false;
|
||||
});
|
||||
|
||||
function transform(list: any[]): any[]
|
||||
{
|
||||
return list?.map(e => ({ label: e.title, children: transform(e.children), link: e.path, tag: e.private ? 'private' : e.type }))
|
||||
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)}))
|
||||
}
|
||||
</script>
|
||||
|
|
@ -40,14 +40,13 @@ async function fetch()
|
|||
|
||||
<template>
|
||||
<Head>
|
||||
<Title>Administration</Title>
|
||||
<Title>d[any] - Administration</Title>
|
||||
</Head>
|
||||
<div class="flex flex-col justify-start">
|
||||
<ProseH2>Administration</ProseH2>
|
||||
<Select label="Job" v-model="job">
|
||||
<SelectItem label="Synchroniser" value="sync" />
|
||||
<SelectItem label="Nettoyer la base" value="clear" disabled />
|
||||
<SelectItem label="Reconstruire" value="rebuild" disabled />
|
||||
<SelectItem label="Récupérer les données d'Obsidian" value="pull" />
|
||||
<SelectItem label="Envoyer les données dans Obsidian" value="push" disabled />
|
||||
</Select>
|
||||
<Button class="self-center" @click="() => !!job && fetch()" :loading="status === 'pending'">
|
||||
<span>Executer</span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<Head>
|
||||
<Title>Editeur</Title>
|
||||
<Title>d[any] - Editeur</Title>
|
||||
</Head>
|
||||
<Editor v-model="model" />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<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>Modification de {{ page.title }}</Title>
|
||||
<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" />
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
<div v-else-if="status === 'pending'" class="flex">
|
||||
<Head>
|
||||
<Title>Chargement</Title>
|
||||
<Title>d[any] - Chargement</Title>
|
||||
</Head>
|
||||
<Loading />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
<template>
|
||||
<Head>
|
||||
<Title>d[any] - Mentions légales</Title>
|
||||
</Head>
|
||||
<div class="flex flex-col max-w-[1200px] p-16">
|
||||
<ProseH3>Mentions Légales</ProseH3>
|
||||
<ProseH4>Collecte et Traitement des Données Personnelles</ProseH4>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<Head>
|
||||
<Title>Connexion</Title>
|
||||
<Title>d[any] - Connexion</Title>
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col justify-center items-center">
|
||||
<div class="flex gap-8 items-center">
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ async function deleteUser()
|
|||
<template>
|
||||
|
||||
<Head>
|
||||
<Title>Mon profil</Title>
|
||||
<Title>d[any] - Mon profil</Title>
|
||||
</Head>
|
||||
<div class="grid lg:grid-cols-4 grid-col-2 w-full items-start py-8 gap-6 content-start" v-if="user">
|
||||
<div class="flex flex-col gap-4 col-span-4 lg:col-span-3 border border-light-35 dark:border-dark-35 p-4">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<Head>
|
||||
<Title>Inscription</Title>
|
||||
<Title>d[any] - Inscription</Title>
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col justify-center items-center">
|
||||
<div class="flex gap-8 items-center">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import useDatabase from "~/composables/useDatabase";
|
||||
import type { FileType } from '~/types/api';
|
||||
import { explorerContentTable } from "~/db/schema";
|
||||
import { eq, ne } from "drizzle-orm";
|
||||
|
||||
const typeMapping: Record<string, FileType> = {
|
||||
".md": "markdown",
|
||||
".canvas": "canvas"
|
||||
};
|
||||
|
||||
export default defineTask({
|
||||
meta: {
|
||||
name: 'pull',
|
||||
description: 'Pull the data from Git',
|
||||
},
|
||||
async run(event) {
|
||||
try {
|
||||
const tree = await $fetch('https://git.peaceultime.com/api/v1/repos/peaceultime/system-aspect/git/trees/master', {
|
||||
method: 'get',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
},
|
||||
params: {
|
||||
recursive: true,
|
||||
per_page: 1000,
|
||||
}
|
||||
}) as any;
|
||||
|
||||
const db = useDatabase();
|
||||
const files = db.select().from(explorerContentTable).where(ne(explorerContentTable.type, 'folder')).all();
|
||||
|
||||
useStorage('cache').clear();
|
||||
|
||||
return { result: true };
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
return { result: false, error: e };
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -11,8 +11,8 @@ const typeMapping: Record<string, FileType> = {
|
|||
|
||||
export default defineTask({
|
||||
meta: {
|
||||
name: 'sync',
|
||||
description: 'Synchronise the project with Obsidian',
|
||||
name: 'push',
|
||||
description: 'Push the data to Git',
|
||||
},
|
||||
async run(event) {
|
||||
try {
|
||||
|
|
@ -27,7 +27,6 @@ export default defineTask({
|
|||
}
|
||||
}) as any;
|
||||
|
||||
|
||||
const files: typeof explorerContentTable.$inferInsert = await Promise.all(tree.tree.filter((e: any) => !e.path.startsWith(".")).map(async (e: any) => {
|
||||
if(e.type === 'tree')
|
||||
{
|
||||
|
|
@ -64,71 +63,10 @@ export default defineTask({
|
|||
}
|
||||
}));
|
||||
|
||||
/*let tags: Tag[] = [];
|
||||
const tagFile = files.find(e => e.path === "tags");
|
||||
|
||||
if(tagFile)
|
||||
{
|
||||
const parsed = useMarkdown()(tagFile.content);
|
||||
const titles = parsed.children.filter(e => e.type === 'element' && e.tagName.match(/h\d/));
|
||||
for(let i = 0; i < titles.length; i++)
|
||||
{
|
||||
const start = titles[i].position?.start.offset ?? 0;
|
||||
const end = titles.length === i + 1 ? tagFile.content.length : titles[i + 1].position.start.offset - 1;
|
||||
tags.push({ tag: titles[i].properties.id, description: tagFile.content.substring(titles[i].position?.end.offset + 1, end) });
|
||||
}
|
||||
}*/
|
||||
|
||||
const db = useDatabase();
|
||||
db.delete(explorerContentTable).run();
|
||||
db.insert(explorerContentTable).values(files).run();
|
||||
|
||||
/*const oldFiles = db.prepare(`SELECT * FROM explorer_files WHERE project = ?1`).all('1') as File[];
|
||||
const removeFiles = db.prepare(`DELETE FROM explorer_files WHERE project = ?1 AND path = ?2`);
|
||||
db.transaction((data: File[]) => data.forEach(e => removeFiles.run('1', e.path)))(oldFiles.filter(e => !files.find(f => f.path = e.path)));
|
||||
removeFiles.finalize();
|
||||
|
||||
const oldTags = db.prepare(`SELECT * FROM explorer_tags WHERE project = ?1`).all('1') as Tag[];
|
||||
const removeTags = db.prepare(`DELETE FROM explorer_tags WHERE project = ?1 AND tag = ?2`);
|
||||
db.transaction((data: Tag[]) => data.forEach(e => removeTags.run('1', e.tag)))(oldTags.filter(e => !tags.find(f => f.tag = e.tag)));
|
||||
removeTags.finalize();
|
||||
|
||||
const insertFiles = db.prepare(`INSERT INTO explorer_files("project", "path", "owner", "title", "order", "type", "content") VALUES (1, $path, 1, $title, $order, $type, $content)`);
|
||||
const updateFiles = db.prepare(`UPDATE explorer_files SET content = $content WHERE project = 1 AND path = $path`);
|
||||
db.transaction((content) => {
|
||||
for (const item of content) {
|
||||
let order = item.order;
|
||||
|
||||
if (typeof order === 'string')
|
||||
order = parseInt(item.order, 10);
|
||||
|
||||
if (isNaN(order))
|
||||
order = 999;
|
||||
|
||||
if(oldFiles.find(e => item.path === e.path))
|
||||
updateFiles.run({ $path: item.path, $content: item.content });
|
||||
else
|
||||
insertFiles.run({ $path: item.path, $title: item.title, $type: item.type, $content: item.content, $order: order });
|
||||
}
|
||||
})(files);
|
||||
|
||||
insertFiles.finalize();
|
||||
updateFiles.finalize();
|
||||
|
||||
const insertTags = db.prepare(`INSERT INTO explorer_tags("project", "tag", "description") VALUES (1, $tag, $description)`);
|
||||
const updateTags = db.prepare(`UPDATE explorer_tags SET description = $description WHERE project = 1 AND tag = $tag`);
|
||||
db.transaction((content) => {
|
||||
for (const item of content) {
|
||||
if (oldTags.find(e => item.tag === e.tag))
|
||||
updateTags.run({ $tag: item.tag, $description: item.description });
|
||||
else
|
||||
insertTags.run({ $tag: item.tag, $description: item.description });
|
||||
}
|
||||
})(tags);
|
||||
|
||||
insertTags.finalize();
|
||||
updateTags.finalize();*/
|
||||
|
||||
useStorage('cache').clear();
|
||||
|
||||
return { result: true };
|
||||
Loading…
Reference in New Issue