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>
<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">
<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` }" />
<div class="pl-3 py-1 flex-1 truncate" :data-tag="item.value.tag">
{{ item.value.label }}
</div>
</NuxtLink>
<slot :isExpanded="isExpanded" :item="item" />
</TreeItem>
</TreeRoot>
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
<script setup lang="ts" generic="T extends Record<string, any>">
const { getKey } = defineProps<{
getKey: (val: T) => string
}>();
interface TreeItem
{
label: string
link?: string
tag?: string
open?: boolean
children?: TreeItem[]
}
const model = defineModel<TreeItem[]>();
const model = defineModel<T[]>();
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>
<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">
import { parseURL } from 'ufo';
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<{
href: 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">
<div class="pl-3 py-1 flex-1 truncate">Projet</div>
</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 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> -
@ -93,6 +103,7 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
import type { NavigationTreeItem } from '~/server/api/navigation.get';
import { iconByType } from '#shared/general.utils';
const open = ref(false);
const { loggedIn, clear } = useUserSession();
@ -109,8 +120,8 @@ const { data: pages } = await useLazyFetch('/api/navigation', {
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>

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

View File

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

View File

@ -3,6 +3,7 @@ import useDatabase from '~/composables/useDatabase';
import { explorerContentTable } from '~/db/schema';
import { project, type ProjectItem } from '~/schemas/project';
import { parsePath } from "#shared/general.utils";
import { eq, getTableColumns } from "drizzle-orm";
export default defineEventHandler(async (e) => {
const { user } = await getUserSession(e);
@ -22,6 +23,15 @@ export default defineEventHandler(async (e) => {
const items = buildOrder(body.data.items);
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) => {
for(let i = 0; i < items.length; i++)
{
@ -35,10 +45,10 @@ export default defineEventHandler(async (e) => {
navigable: item.navigable,
private: item.private,
order: item.order,
content: Buffer.from('', 'utf-8'),
content: null,
}).onConflictDoUpdate({
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,
type: item.type,
navigable: item.navigable,
@ -48,6 +58,10 @@ export default defineEventHandler(async (e) => {
target: explorerContentTable.path,
}).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
{
return (Array.isArray(slug) ? slug.join('/') : slug);
@ -44,3 +46,10 @@ export function clamp(x: number, min: number, max: number): number {
return min;
return x;
}
export const iconByType: Record<FileType, string> = {
'folder': 'lucide:folder',
'canvas': 'ph:graph-light',
'file': 'radix-icons:file',
'markdown': 'radix-icons:file',
}