Add deletion of files, add slots to tree and a global icon by type. Project update needs rework.
This commit is contained in:
parent
4f2fc31695
commit
2855d4ba2e
|
|
@ -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>
|
||||
</script>
|
||||
|
|
@ -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-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { FileType } from "~/schemas/file";
|
||||
|
||||
export function unifySlug(slug: string | string[]): string
|
||||
{
|
||||
return (Array.isArray(slug) ? slug.join('/') : slug);
|
||||
|
|
@ -43,4 +45,11 @@ export function clamp(x: number, min: number, max: number): number {
|
|||
if (x < min)
|
||||
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',
|
||||
}
|
||||
Loading…
Reference in New Issue