Add deletion of files, add slots to tree and a global icon by type. Project update needs rework.

This commit is contained in:
Peaceultime 2024-11-20 22:51:51 +01:00
parent 4f2fc31695
commit 2855d4ba2e
10 changed files with 62 additions and 72 deletions

View File

@ -1,56 +1,20 @@
<template> <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" :defaultExpanded="flatten(model)"> <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="getKey" :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"> <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"> <slot :isExpanded="isExpanded" :item="item" />
<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` }" />
<div class="pl-3 py-1 flex-1 truncate" :data-tag="item.value.tag">
{{ item.value.label }}
</div>
</NuxtLink>
</TreeItem> </TreeItem>
</TreeRoot> </TreeRoot>
</template> </template>
<script setup lang="ts"> <script setup lang="ts" generic="T extends Record<string, any>">
import { Icon } from '@iconify/vue/dist/iconify.js'; const { getKey } = defineProps<{
getKey: (val: T) => string
}>();
interface TreeItem const model = defineModel<T[]>();
{
label: string
link?: string
tag?: string
open?: boolean
children?: TreeItem[]
}
const model = defineModel<TreeItem[]>();
function flatten(arr: TreeItem[]): string[] function flatten(arr: T[]): string[]
{ {
return arr.filter(e => e.open).flatMap(e => [e?.link ?? e.label, ...flatten(e.children ?? [])]); return arr.filter(e => e.open).flatMap(e => [getKey(e), ...flatten(e.children ?? [])]);
} }
</script> </script>
<style>
[data-tag="canvas"]:after,
[data-tag="private"]:after
{
@apply text-sm;
@apply font-normal;
@apply float-end;
@apply border ;
@apply border-light-35 ;
@apply dark:border-dark-35;
@apply px-1;
@apply bg-light-20;
@apply dark:bg-dark-20;
font-variant: small-caps;
}
[data-tag="canvas"]:after
{
content: 'Canvas'
}
[data-tag="private"]:after
{
content: 'Privé'
}
</style>

View File

@ -31,12 +31,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { parseURL } from 'ufo'; import { parseURL } from 'ufo';
import { Icon } from '@iconify/vue/dist/iconify.js'; import { Icon } from '@iconify/vue/dist/iconify.js';
import { iconByType } from '#shared/general.utils';
const iconByType: Record<string, string> = {
'folder': 'lucide:folder',
'canvas': 'ph:graph-light',
'file': 'radix-icons:file',
}
const { href } = defineProps<{ const { href } = defineProps<{
href: string href: string
class?: string class?: string

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -76,7 +76,17 @@
<NuxtLink :href="{ name: 'explore' }" no-prefetch class="flex flex-1 font-bold text-lg items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" active-class="text-accent-blue border-s-2 !border-accent-blue"> <NuxtLink :href="{ name: 'explore' }" no-prefetch class="flex flex-1 font-bold text-lg items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" active-class="text-accent-blue border-s-2 !border-accent-blue">
<div class="pl-3 py-1 flex-1 truncate">Projet</div> <div class="pl-3 py-1 flex-1 truncate">Projet</div>
</NuxtLink> </NuxtLink>
<Tree v-if="pages" v-model="pages"/> <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" no-prefetch class="group flex flex-1 items-center hover:border-accent-blue" :class="{ 'font-medium': item.hasChildren }" active-class="text-accent-blue" :data-private="item.value.private">
<Icon v-if="item.hasChildren" icon="radix-icons:chevron-right" :class="{ 'rotate-90': isExpanded }" class="h-4 w-4 transition-transform absolute group-[:hover]:text-accent-purple" :style="{ 'left': `${item.level - 1}em` }" />
<Icon v-else-if="iconByType[item.value.type]" :icon="iconByType[item.value.type]" class="group-[:hover]:text-accent-purple" />
<div class="pl-3 py-1 flex-1 truncate">
{{ item.value.title }}
</div>
</NuxtLink>
</template>
</Tree>
</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: 'roadmap' }">Roadmap</NuxtLink> -
@ -93,6 +103,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js'; import { Icon } from '@iconify/vue/dist/iconify.js';
import type { NavigationTreeItem } from '~/server/api/navigation.get'; import type { NavigationTreeItem } from '~/server/api/navigation.get';
import { iconByType } from '#shared/general.utils';
const open = ref(false); const open = ref(false);
const { loggedIn, clear } = useUserSession(); const { loggedIn, clear } = useUserSession();
@ -109,8 +120,8 @@ const { data: pages } = await useLazyFetch('/api/navigation', {
watch: [useRouter().currentRoute] watch: [useRouter().currentRoute]
}); });
function transform(list: NavigationTreeItem[] | undefined): any[] | undefined function transform(list: NavigationTreeItem[] | undefined): NavigationTreeItem[] | undefined
{ {
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)})) return list?.map(e => ({ ...e, open: path.value?.startsWith(e.path), children: transform(e.children) }));
} }
</script> </script>

View File

@ -5,13 +5,14 @@
<div class="flex flex-1 flex-row gap-4 p-6 items-start" v-if="navigation"> <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"> <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" <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<ProjectItem>) => item.path ? getPath(item as ProjectItem) : ''" @updateTree="drop" @update:model-value="console.log"> :items="navigation ?? undefined" :get-key="(item: Partial<ProjectItem>) => item.path !== undefined ? getPath(item as ProjectItem) : ''" @updateTree="drop">
<template #default="{ handleToggle, handleSelect, isExpanded, isDragging, item }"> <template #default="{ handleToggle, handleSelect, isExpanded, isSelected, isDragging, 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 items-center 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" >
<Icon :icon="isExpanded ? 'lucide:folder-open' : 'lucide:folder'"/> <Icon :icon="isExpanded ? 'lucide:folder-open' : 'lucide:folder'"/>
</span> </span>
<div class="ps-2 py-1 flex-1 truncate" :class="{'!ps-4 border-s border-light-35 dark:border-dark-35': !item.hasChildren}" :title="item.value.title" @click="handleSelect"> <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; }">
{{ item.value.title }} {{ item.value.title }}
</div> </div>
</div> </div>
@ -54,14 +55,14 @@
}]"> }]">
<Button>Nouveau</Button> <Button>Nouveau</Button>
</DropdownMenu> </DropdownMenu>
<Button @click="" 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> <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+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> <Tooltip message="Ctrl+Shift+Z" side="bottom"><NuxtLink :href="{ name: 'explore' }"><Button>Annuler</Button></NuxtLink></Tooltip>
</div> </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 ? 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> <span><pre class="ps-6 inline">/{{ selected.path === '' ? getPath(selected) : selected.path }}</pre></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>
@ -79,6 +80,7 @@ import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/
import { parsePath } from '#shared/general.utils'; import { parsePath } from '#shared/general.utils';
import type { ProjectItem } from '~/schemas/project'; import type { ProjectItem } from '~/schemas/project';
import type { FileType } from '~/schemas/file'; import type { FileType } from '~/schemas/file';
import { iconByType } from '#shared/general.utils';
definePageMeta({ definePageMeta({
rights: ['admin', 'editor'], rights: ['admin', 'editor'],
@ -192,7 +194,7 @@ const tree = {
for (const item of data) for (const item of data)
{ {
if (item[prop]?.toString()?.includes(value)) if (item[prop]?.toString().toLowerCase()?.startsWith(value.toLowerCase()))
arr.push(item); arr.push(item);
if (tree.hasChildren(item)) { if (tree.hasChildren(item)) {
@ -236,10 +238,8 @@ function add(type: FileType): void
return; return;
} }
const news = [...tree.search(navigation.value, 'name', 'nouveau'), ...tree.search(navigation.value, 'title', 'Nouveau')].filter((e, i, a) => a.indexOf(e) === i); const news = [...tree.search(navigation.value, 'title', 'Nouveau')].filter((e, i, a) => a.indexOf(e) === i);
console.log(news); const item: ProjectItem = { navigable: true, private: false, parent: '', path: '', title: `Nouveau${news.length > 0 ? ' (' + news.length +')' : ''}`, type: type, order: 0, children: type === 'folder' ? [] : undefined };
const item: ProjectItem = { navigable: true, private: false, parent: '', path: '', title: `Nouveau${news.length > 0 ? ' (' + news.length +')' : ''}`, type: type, order: 0, name: `nouveau${news.length > 0 ? '-' + news.length : ''}`, children: type === 'folder' ? [] : undefined };
if(!selected.value) if(!selected.value)
{ {
@ -254,8 +254,6 @@ function add(type: FileType): void
{ {
navigation.value = tree.insertAfter(navigation.value, getPath(selected.value), item); navigation.value = tree.insertAfter(navigation.value, getPath(selected.value), item);
} }
selected.value = item;
} }
function updateTree(instruction: Instruction, itemId: string, targetId: string) : ProjectItem[] | undefined { function updateTree(instruction: Instruction, itemId: string, targetId: string) : ProjectItem[] | undefined {
if(!navigation.value) if(!navigation.value)
@ -318,7 +316,6 @@ function drop(instruction: Instruction, itemId: string, targetId: string)
} }
function rebuildPath(tree: ProjectItem[] | null | undefined, parentPath: string) function rebuildPath(tree: ProjectItem[] | null | undefined, parentPath: string)
{ {
debugger;
if(!tree) if(!tree)
return; return;
@ -352,6 +349,6 @@ async function save(redirect: boolean): Promise<void>
} }
function getPath(item: ProjectItem): string function getPath(item: ProjectItem): string
{ {
return [item.parent, parsePath(item?.name ?? item.title)].filter(e => !!e).join('/'); return [item.parent, parsePath(item.title)].filter(e => !!e).join('/');
} }
</script> </script>

View File

@ -4,7 +4,6 @@ import { fileType } from "./file";
const baseItem = z.object({ const baseItem = z.object({
path: z.string(), path: z.string(),
parent: z.string(), parent: z.string(),
name: z.string().optional(),
title: z.string(), title: z.string(),
type: fileType, type: fileType,
navigable: z.boolean(), navigable: z.boolean(),

View File

@ -3,6 +3,7 @@ import useDatabase from '~/composables/useDatabase';
import { explorerContentTable } from '~/db/schema'; import { explorerContentTable } from '~/db/schema';
import { project, type ProjectItem } from '~/schemas/project'; import { project, type ProjectItem } from '~/schemas/project';
import { parsePath } from "#shared/general.utils"; import { parsePath } from "#shared/general.utils";
import { eq, getTableColumns } from "drizzle-orm";
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
const { user } = await getUserSession(e); const { user } = await getUserSession(e);
@ -22,6 +23,15 @@ export default defineEventHandler(async (e) => {
const items = buildOrder(body.data.items); const items = buildOrder(body.data.items);
const db = useDatabase(); const db = useDatabase();
const { content, ...cols } = getTableColumns(explorerContentTable);
const full = db.select(cols).from(explorerContentTable).all();
for(let i = full.length - 1; i >= 0; i--)
{
if(items.find(e => (e.path === '' ? [e.parent, parsePath(e.title)].filter(e => !!e).join('/') : e.path) === full[i].path))
full.splice(i, 1);
}
db.transaction((tx) => { db.transaction((tx) => {
for(let i = 0; i < items.length; i++) for(let i = 0; i < items.length; i++)
{ {
@ -35,10 +45,10 @@ export default defineEventHandler(async (e) => {
navigable: item.navigable, navigable: item.navigable,
private: item.private, private: item.private,
order: item.order, order: item.order,
content: Buffer.from('', 'utf-8'), content: null,
}).onConflictDoUpdate({ }).onConflictDoUpdate({
set: { set: {
path: [item.parent, parsePath(item?.name ?? item.title)].filter(e => !!e).join('/'), path: [item.parent, parsePath(item.title)].filter(e => !!e).join('/'),
title: item.title, title: item.title,
type: item.type, type: item.type,
navigable: item.navigable, navigable: item.navigable,
@ -48,6 +58,10 @@ export default defineEventHandler(async (e) => {
target: explorerContentTable.path, target: explorerContentTable.path,
}).run(); }).run();
} }
for(let i = 0; i < full.length; i++)
{
tx.delete(explorerContentTable).where(eq(explorerContentTable.path, full[i].path)).run();
}
}); });
}); });

View File

@ -1,3 +1,5 @@
import type { FileType } from "~/schemas/file";
export function unifySlug(slug: string | string[]): string export function unifySlug(slug: string | string[]): string
{ {
return (Array.isArray(slug) ? slug.join('/') : slug); return (Array.isArray(slug) ? slug.join('/') : slug);
@ -44,3 +46,10 @@ export function clamp(x: number, min: number, max: number): number {
return min; return min;
return x; return x;
} }
export const iconByType: Record<FileType, string> = {
'folder': 'lucide:folder',
'canvas': 'ph:graph-light',
'file': 'radix-icons:file',
'markdown': 'radix-icons:file',
}