Update DB schema to include an ID and split overview and content. Progressing on ContentEditor with the ID fixing many issues. Adding modal and sync features.
This commit is contained in:
parent
227d7224e5
commit
1d41514b26
3
bun.lock
3
bun.lock
|
|
@ -5,6 +5,7 @@
|
|||
"name": "d-any",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.5.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||
"@codemirror/lang-markdown": "^6.3.2",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
|
|
@ -67,6 +68,8 @@
|
|||
|
||||
"@atlaskit/pragmatic-drag-and-drop": ["@atlaskit/pragmatic-drag-and-drop@1.5.0", "", { "dependencies": { "@babel/runtime": "^7.0.0", "bind-event-listener": "^3.0.0", "raf-schd": "^4.0.3" } }, "sha512-VnHcgOBALm+mbL9CoJPI6wBNQeB0is+CkejdfAlaP8RfBoELe+0sQtE8j4Z4fPRqDzo11OEqUYKHkmx4Ttzozg=="],
|
||||
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": ["@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.0", "", { "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.4.0", "@babel/runtime": "^7.0.0" } }, "sha512-E52y8/0BTTf4ai6BJyFYgdVHFgQ1AES33KvAVQpZ41jMkoukLIq6UoCudOXku7xs3qoPygQdpC+vitVUuEFJXw=="],
|
||||
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": ["@atlaskit/pragmatic-drag-and-drop-hitbox@1.0.3", "", { "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.1.0", "@babel/runtime": "^7.0.0" } }, "sha512-/Sbu/HqN2VGLYBhnsG7SbRNg98XKkbF6L7XDdBi+izRybfaK1FeMfodPpm/xnBHPJzwYMdkE0qtLyv6afhgMUA=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
|
||||
|
|
|
|||
|
|
@ -29,12 +29,6 @@ type EdgeEditor = InstanceType<typeof CanvasEdgeEditor>;
|
|||
|
||||
const cancelEvent = (e: Event) => e.preventDefault();
|
||||
const stopPropagation = (e: Event) => e.stopImmediatePropagation();
|
||||
function getID(length: number)
|
||||
{
|
||||
for (var id = [], i = 0; i < length; i++)
|
||||
id.push((16 * Math.random() | 0).toString(16));
|
||||
return id.join("");
|
||||
}
|
||||
function center(touches: TouchList): Position
|
||||
{
|
||||
const pos = { x: 0, y: 0 };
|
||||
|
|
@ -58,7 +52,7 @@ function distance(touches: TouchList): number
|
|||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { clamp } from '#shared/general.util';
|
||||
import { clamp, getID, ID_SIZE } from '#shared/general.util';
|
||||
import type { CanvasContent, CanvasEdge, CanvasNode } from '~/types/canvas';
|
||||
|
||||
const canvas = defineModel<CanvasContent>({ required: true });
|
||||
|
|
@ -342,11 +336,10 @@ function edit(element: Element)
|
|||
}
|
||||
function createNode(e: MouseEvent)
|
||||
{
|
||||
const centerX = (viewportSize.right.value - viewportSize.left.value) / 2 + viewportSize.left.value, centerY = (viewportSize.bottom.value - viewportSize.top.value) / 2 + viewportSize.top.value;
|
||||
const width = 250, height = 100;
|
||||
const x = e.layerX / zoom.value - dispX.value - width / 2;
|
||||
const y = e.layerY / zoom.value - dispY.value - height / 2;
|
||||
const node: CanvasNode = { id: getID(16), x, y, width, height, type: 'text' };
|
||||
const node: CanvasNode = { id: getID(ID_SIZE), x, y, width, height, type: 'text' };
|
||||
|
||||
if(!canvas.value.nodes)
|
||||
canvas.value.nodes = [node];
|
||||
|
|
@ -412,7 +405,7 @@ function dragEndEdgeTo(e: MouseEvent): void
|
|||
if(fakeEdge.value.snapped)
|
||||
{
|
||||
const node = canvas.value.nodes!.find(e => e.id === fakeEdge.value.drag!.id)!;
|
||||
const edge: CanvasEdge = { fromNode: fakeEdge.value.drag!.id, fromSide: fakeEdge.value.fromSide!, toNode: fakeEdge.value.snapped.node, toSide: fakeEdge.value.snapped.side, id: getID(16), color: node.color };
|
||||
const edge: CanvasEdge = { fromNode: fakeEdge.value.drag!.id, fromSide: fakeEdge.value.fromSide!, toNode: fakeEdge.value.snapped.node, toSide: fakeEdge.value.snapped.side, id: getID(ID_SIZE), color: node.color };
|
||||
canvas.value.edges?.push(edge);
|
||||
|
||||
addAction('create', [{ from: undefined, to: edge, element: { id: edge.id, type: 'edge' } }]);
|
||||
|
|
|
|||
|
|
@ -1,276 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, WidgetType, type DecorationSet } from '@codemirror/view';
|
||||
import { Annotation, EditorState, RangeValue, SelectionRange, type Range } from '@codemirror/state';
|
||||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||
import { bracketMatching, defaultHighlightStyle, foldKeymap, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
|
||||
import { search, searchKeymap } from '@codemirror/search';
|
||||
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
|
||||
import { lintKeymap } from '@codemirror/lint';
|
||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||
import { IterMode, Tree } from '@lezer/common';
|
||||
import { tags } from '@lezer/highlight';
|
||||
const External = Annotation.define<boolean>();
|
||||
const Hidden = Decoration.mark({ class: 'hidden' });
|
||||
const Bullet = Decoration.mark({ class: '*:hidden before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4' });
|
||||
const Blockquote = Decoration.line({ class: '*:hidden before:block !ps-4 relative before:absolute before:top-0 before:bottom-0 before:left-0 before:w-1 before:bg-none before:bg-light-30 dark:before:bg-dark-30' });
|
||||
|
||||
const TagTag = tags.special(tags.content);
|
||||
|
||||
const intersects = (a: {
|
||||
from: number;
|
||||
to: number;
|
||||
}, b: {
|
||||
from: number;
|
||||
to: number;
|
||||
}) => !(a.to < b.from || b.to < a.from);
|
||||
|
||||
const highlight = HighlightStyle.define([
|
||||
{ tag: tags.heading1, class: 'text-5xl pt-4 pb-2 after:hidden' },
|
||||
{ tag: tags.heading2, class: 'text-4xl pt-4 pb-2 ps-1 leading-loose after:hidden' },
|
||||
{ tag: tags.heading3, class: 'text-2xl font-bold pt-1 after:hidden' },
|
||||
{ tag: tags.heading4, class: 'text-xl font-semibold pt-1 after:hidden variant-cap' },
|
||||
{ tag: tags.meta, color: "#404740" },
|
||||
{ tag: tags.link, textDecoration: "underline" },
|
||||
{ tag: tags.heading, textDecoration: "underline", fontWeight: "bold" },
|
||||
{ tag: tags.emphasis, fontStyle: "italic" },
|
||||
{ tag: tags.strong, fontWeight: "bold" },
|
||||
{ tag: tags.strikethrough, textDecoration: "line-through" },
|
||||
{ tag: tags.keyword, color: "#708" },
|
||||
{ tag: TagTag, class: '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' }
|
||||
]);
|
||||
|
||||
class Decorator
|
||||
{
|
||||
static hiddenNodes: string[] = [
|
||||
'HardBreak',
|
||||
'LinkMark',
|
||||
'EmphasisMark',
|
||||
'CodeMark',
|
||||
'CodeInfo',
|
||||
'URL',
|
||||
]
|
||||
decorations: DecorationSet;
|
||||
constructor(view: EditorView)
|
||||
{
|
||||
this.decorations = Decoration.set(this.iterate(syntaxTree(view.state), view.visibleRanges, []), true);
|
||||
}
|
||||
update(update: ViewUpdate)
|
||||
{
|
||||
if(!update.docChanged && !update.viewportChanged && !update.selectionSet)
|
||||
return;
|
||||
|
||||
this.decorations = this.decorations.update({
|
||||
filter: (f, t, v) => false,
|
||||
add: this.iterate(syntaxTree(update.state), update.view.visibleRanges, update.state.selection.ranges),
|
||||
sort: true,
|
||||
});
|
||||
}
|
||||
iterate(tree: Tree, visible: readonly {
|
||||
from: number;
|
||||
to: number;
|
||||
}[], selection: readonly SelectionRange[]): Range<Decoration>[]
|
||||
{
|
||||
const decorations: Range<Decoration>[] = [];
|
||||
|
||||
for (let { from, to } of visible) {
|
||||
tree.iterate({
|
||||
from, to, mode: IterMode.IgnoreMounts,
|
||||
enter: node => {
|
||||
if(node.node.parent && selection.some(e => intersects(e, node.node.parent!)))
|
||||
return true;
|
||||
|
||||
else if(node.name === 'HeaderMark')
|
||||
decorations.push(Hidden.range(node.from, node.to + 1));
|
||||
|
||||
else if(Decorator.hiddenNodes.includes(node.name))
|
||||
decorations.push(Hidden.range(node.from, node.to));
|
||||
|
||||
else if(node.matchContext(['BulletList', 'ListItem']) && node.name === 'ListMark')
|
||||
decorations.push(Bullet.range(node.from, node.to + 1));
|
||||
|
||||
else if(node.matchContext(['Blockquote']))
|
||||
decorations.push(Blockquote.range(node.from, node.from));
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return decorations;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { autofocus = false } = defineProps<{
|
||||
placeholder?: string
|
||||
autofocus?: boolean
|
||||
}>();
|
||||
const model = defineModel<string>();
|
||||
|
||||
const editor = useTemplateRef('editor');
|
||||
const view = ref<EditorView>();
|
||||
|
||||
onMounted(() => {
|
||||
if(editor.value)
|
||||
{
|
||||
view.value = new EditorView({
|
||||
doc: model.value,
|
||||
parent: editor.value,
|
||||
extensions: [
|
||||
markdown({
|
||||
base: markdownLanguage,
|
||||
extensions: {
|
||||
defineNodes: [
|
||||
{ name: "Tag", style: TagTag },
|
||||
{ name: "TagMark", style: tags.processingInstruction }
|
||||
],
|
||||
parseInline: [{
|
||||
name: "Tag",
|
||||
parse(cx, next, pos) {
|
||||
if (next != 35 || cx.char(pos + 1) == 35) return -1;
|
||||
let elts = [cx.elt("TagMark", pos, pos + 1)];
|
||||
for (let i = pos + 1; i < cx.end; i++) {
|
||||
let next = cx.char(i);
|
||||
if (next == 35)
|
||||
return cx.addElement(cx.elt("Tag", pos, i + 1, elts.concat(cx.elt("TagMark", i, i + 1))));
|
||||
if (next == 92)
|
||||
elts.push(cx.elt("Escape", i, i++ + 2));
|
||||
if (next == 32 || next == 9 || next == 10 || next == 13) break;
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}],
|
||||
}
|
||||
}),
|
||||
history(),
|
||||
search(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
syntaxHighlighting(highlight),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
crosshairCursor(),
|
||||
EditorView.lineWrapping,
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
...lintKeymap
|
||||
]),
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))
|
||||
{
|
||||
model.value = viewUpdate.state.doc.toString();
|
||||
}
|
||||
}),
|
||||
EditorView.contentAttributes.of({spellcheck: "true"}),
|
||||
ViewPlugin.fromClass(Decorator, {
|
||||
decorations: e => e.decorations,
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
if(autofocus)
|
||||
{
|
||||
view.value.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (view.value)
|
||||
{
|
||||
view.value?.destroy();
|
||||
view.value = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (model.value === void 0) {
|
||||
return;
|
||||
}
|
||||
const currentValue = view.value ? view.value.state.doc.toString() : "";
|
||||
if (view.value && model.value !== currentValue) {
|
||||
view.value.dispatch({
|
||||
changes: { from: 0, to: currentValue.length, insert: model.value || "" },
|
||||
annotations: [External.of(true)],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({ focus: () => editor.value?.focus() });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="editor" class="flex flex-1 w-full justify-stretch items-stretch py-2 px-1.5 font-sans text-base"></div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.CodeMirror
|
||||
{
|
||||
@apply bg-transparent;
|
||||
@apply flex-1 h-full;
|
||||
@apply font-sans;
|
||||
|
||||
@apply text-light-100 dark:text-dark-100;
|
||||
}
|
||||
.cancel-gutters .CodeMirror-gutters
|
||||
{
|
||||
@apply hidden;
|
||||
}
|
||||
.CodeMirror-sizer
|
||||
{
|
||||
@apply !px-3;
|
||||
}
|
||||
.cancel-gutters .CodeMirror-sizer
|
||||
{
|
||||
@apply ms-2;
|
||||
}
|
||||
.CodeMirror-gutters
|
||||
{
|
||||
@apply bg-transparent;
|
||||
@apply border-transparent;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper
|
||||
{
|
||||
@apply absolute top-0 bottom-0;
|
||||
@apply flex justify-center items-center;
|
||||
}
|
||||
.CodeMirror-foldmarker
|
||||
{
|
||||
@apply text-light-100;
|
||||
@apply dark:text-dark-100;
|
||||
@apply ps-3;
|
||||
text-shadow: none;
|
||||
}
|
||||
.hmd-inactive-line .cm-formatting-header, .hmd-inactive-line .cm-formatting-link, .hmd-inactive-line .cm-link-has-alias, .hmd-inactive-line .cm-link-alias-pipe
|
||||
{
|
||||
@apply hidden;
|
||||
}
|
||||
.CodeMirror-line
|
||||
{
|
||||
@apply text-base;
|
||||
}
|
||||
.CodeMirror-cursor
|
||||
{
|
||||
@apply border-light-100 dark:border-dark-100;
|
||||
}
|
||||
.CodeMirror-selected
|
||||
{
|
||||
@apply bg-light-35 dark:bg-dark-35;
|
||||
}
|
||||
.HyperMD-list-line-1 {
|
||||
@apply !ps-0;
|
||||
}
|
||||
.HyperMD-list-line-2 {
|
||||
@apply !ps-6;
|
||||
}
|
||||
.HyperMD-list-line-3 {
|
||||
@apply !ps-12;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
<template>
|
||||
<Editor ref="editor" v-model="model" autofocus :gutters="false" />
|
||||
<iframe ref="iframe" class="w-full h-full border-0" sandbox="allow-same-origin allow-scripts"></iframe>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const model = defineModel<string>();
|
||||
const editor = useTemplateRef('editor'), iframe = useTemplateRef('iframe');
|
||||
|
||||
onMounted(() => {
|
||||
if(iframe.value && iframe.value.contentDocument && editor.value)
|
||||
{
|
||||
editor.value.$el.remove();
|
||||
|
||||
iframe.value.contentDocument.documentElement.setAttribute('class', document.documentElement.getAttribute('class') ?? '');
|
||||
iframe.value.contentDocument.documentElement.setAttribute('style', document.documentElement.getAttribute('style') ?? '');
|
||||
|
||||
const base = iframe.value.contentDocument.head.appendChild(iframe.value.contentDocument.createElement('base'));
|
||||
base.setAttribute('href', window.location.href);
|
||||
|
||||
for(let element of document.getElementsByTagName('link'))
|
||||
{
|
||||
if(element.getAttribute('rel') === 'stylesheet')
|
||||
iframe.value.contentDocument.head.appendChild(element.cloneNode(true));
|
||||
}
|
||||
|
||||
for(let element of document.getElementsByTagName('style'))
|
||||
{
|
||||
iframe.value.contentDocument.head.appendChild(element.cloneNode(true));
|
||||
}
|
||||
|
||||
iframe.value.contentDocument.body.setAttribute('class', document.body.getAttribute('class') ?? '');
|
||||
iframe.value.contentDocument.body.setAttribute('style', document.body.getAttribute('style') ?? '');
|
||||
|
||||
iframe.value.contentDocument.body.appendChild(editor.value.$el);
|
||||
|
||||
editor.value.focus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
<template>
|
||||
<div ref="element"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { heading } from 'hast-util-heading';
|
||||
import { headingRank } from 'hast-util-heading-rank';
|
||||
import { parseId } from '~/shared/general.util';
|
||||
import type { Root } from 'hast';
|
||||
import { renderMarkdown } from '~/shared/markdown.util';
|
||||
import { tag, a, blockquote, h1, h2, h3, h4, h5, hr, li, small, table, td, th, type Prose } from '~/shared/proses';
|
||||
import { callout } from '../shared/proses';
|
||||
|
||||
const { content, proses, filter } = defineProps<{
|
||||
content: string
|
||||
proses?: Record<string, Prose>
|
||||
filter?: string
|
||||
}>();
|
||||
const element = useTemplateRef('element');
|
||||
|
||||
const parser = useMarkdown(), data = ref<Root>();
|
||||
const node = computed(() => content ? parser.parseSync(content) : undefined);
|
||||
watch([node], () => {
|
||||
if(!node.value)
|
||||
{
|
||||
data.value = undefined;
|
||||
return;
|
||||
}
|
||||
else if(!filter)
|
||||
{
|
||||
data.value = node.value;
|
||||
}
|
||||
else
|
||||
{
|
||||
const start = node.value?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1;
|
||||
|
||||
if(start === -1)
|
||||
data.value = node.value;
|
||||
else
|
||||
{
|
||||
let end = start;
|
||||
const rank = headingRank(node.value.children[start])!;
|
||||
while(end < node.value.children.length)
|
||||
{
|
||||
end++;
|
||||
if(heading(node.value.children[end]) && headingRank(node.value.children[end])! <= rank)
|
||||
break;
|
||||
}
|
||||
data.value = { ...node.value, children: node.value.children.slice(start, end) };
|
||||
}
|
||||
}
|
||||
|
||||
mount();
|
||||
}, { immediate: true, });
|
||||
|
||||
onMounted(() => {
|
||||
mount();
|
||||
})
|
||||
|
||||
function mount()
|
||||
{
|
||||
if(!element.value || !data.value)
|
||||
return;
|
||||
|
||||
const el = element.value!;
|
||||
|
||||
for(let i = el.childElementCount - 1; i >= 0; i--)
|
||||
{
|
||||
el.removeChild(el.children[i]);
|
||||
}
|
||||
|
||||
el.appendChild(renderMarkdown(data.value, { tag, a, blockquote, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th, ...proses }));
|
||||
|
||||
const hash = useRouter().currentRoute.value.hash;
|
||||
if(hash.length > 0)
|
||||
{
|
||||
document.getElementById(parseId(hash.substring(1))!)?.scrollIntoView({ behavior: 'instant' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { RootContent, Root } from 'hast';
|
||||
import { Text, Comment } from 'vue';
|
||||
|
||||
import ProseP from '~/components/prose/ProseP.vue';
|
||||
import ProseA from '~/components/prose/ProseA.vue';
|
||||
import ProseBlockquote from '~/components/prose/ProseBlockquote.vue';
|
||||
import ProseCallout from './prose/ProseCallout.vue';
|
||||
import ProseCode from '~/components/prose/ProseCode.vue';
|
||||
import ProsePre from '~/components/prose/ProsePre.vue';
|
||||
import ProseEm from '~/components/prose/ProseEm.vue';
|
||||
import ProseH1 from '~/components/prose/ProseH1.vue';
|
||||
import ProseH2 from '~/components/prose/ProseH2.vue';
|
||||
import ProseH3 from '~/components/prose/ProseH3.vue';
|
||||
import ProseH4 from '~/components/prose/ProseH4.vue';
|
||||
import ProseH5 from '~/components/prose/ProseH5.vue';
|
||||
import ProseH6 from '~/components/prose/ProseH6.vue';
|
||||
import ProseHr from '~/components/prose/ProseHr.vue';
|
||||
import ProseImg from '~/components/prose/ProseImg.vue';
|
||||
import ProseUl from '~/components/prose/ProseUl.vue';
|
||||
import ProseOl from '~/components/prose/ProseOl.vue';
|
||||
import ProseLi from '~/components/prose/ProseLi.vue';
|
||||
import ProseSmall from './prose/ProseSmall.vue';
|
||||
import ProseStrong from '~/components/prose/ProseStrong.vue';
|
||||
import ProseTable from '~/components/prose/ProseTable.vue';
|
||||
import ProseTag from '~/components/prose/ProseTag.vue';
|
||||
import ProseThead from '~/components/prose/ProseThead.vue';
|
||||
import ProseTbody from '~/components/prose/ProseTbody.vue';
|
||||
import ProseTd from '~/components/prose/ProseTd.vue';
|
||||
import ProseTh from '~/components/prose/ProseTh.vue';
|
||||
import ProseTr from '~/components/prose/ProseTr.vue';
|
||||
import ProseScript from '~/components/prose/ProseScript.vue';
|
||||
|
||||
const proseList = {
|
||||
"p": ProseP,
|
||||
"a": ProseA,
|
||||
"blockquote": ProseBlockquote,
|
||||
"callout": ProseCallout,
|
||||
"code": ProseCode,
|
||||
"pre": ProsePre,
|
||||
"em": ProseEm,
|
||||
"h1": ProseH1,
|
||||
"h2": ProseH2,
|
||||
"h3": ProseH3,
|
||||
"h4": ProseH4,
|
||||
"h5": ProseH5,
|
||||
"h6": ProseH6,
|
||||
"hr": ProseHr,
|
||||
"img": ProseImg,
|
||||
"ul": ProseUl,
|
||||
"ol": ProseOl,
|
||||
"li": ProseLi,
|
||||
"small": ProseSmall,
|
||||
"strong": ProseStrong,
|
||||
"table": ProseTable,
|
||||
"tag": ProseTag,
|
||||
"thead": ProseThead,
|
||||
"tbody": ProseTbody,
|
||||
"td": ProseTd,
|
||||
"th": ProseTh,
|
||||
"tr": ProseTr,
|
||||
"script": ProseScript
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MarkdownRenderer',
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
proses: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
async setup(props) {
|
||||
if(props.proses)
|
||||
{
|
||||
for(const prose of Object.keys(props.proses))
|
||||
{
|
||||
if(typeof props.proses[prose] === 'string')
|
||||
props.proses[prose] = await resolveComponent(props.proses[prose]);
|
||||
}
|
||||
}
|
||||
return { tags: Object.assign({}, proseList, props.proses) };
|
||||
},
|
||||
render(ctx: any) {
|
||||
const { node, tags } = ctx;
|
||||
|
||||
if(!node)
|
||||
return null;
|
||||
|
||||
return h('div', null, {default: () => (node as Root).children.map(e => renderNode(e, tags)).filter(e => !!e)});
|
||||
}
|
||||
});
|
||||
|
||||
function renderNode(node: RootContent, tags: Record<string, any>): VNode | undefined
|
||||
{
|
||||
if(node.type === 'text' && node.value.length > 0 && node.value !== '\n')
|
||||
{
|
||||
return h(Text, node.value);
|
||||
}
|
||||
else if(node.type === 'comment' && node.value.length > 0 && node.value !== '\n')
|
||||
{
|
||||
return h(Comment, node.value);
|
||||
}
|
||||
else if(node.type === 'element')
|
||||
{
|
||||
return h(tags[node.tagName] ?? node.tagName, { ...node.properties, class: node.properties.className }, { default: () => node.children.map(e => renderNode(e, tags)).filter(e => !!e) });
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
</script>
|
||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
46
db/schema.ts
46
db/schema.ts
|
|
@ -1,7 +1,7 @@
|
|||
import { relations } from 'drizzle-orm';
|
||||
import { int, text, sqliteTable, type SQLiteTableExtraConfig, primaryKey, blob } from 'drizzle-orm/sqlite-core';
|
||||
import { int, text, sqliteTable as table, primaryKey, blob } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const usersTable = sqliteTable("users", {
|
||||
export const usersTable = table("users", {
|
||||
id: int().primaryKey({ autoIncrement: true }),
|
||||
username: text().notNull().unique(),
|
||||
email: text().notNull().unique(),
|
||||
|
|
@ -9,46 +9,41 @@ export const usersTable = sqliteTable("users", {
|
|||
state: int().notNull().default(0),
|
||||
});
|
||||
|
||||
export const usersDataTable = sqliteTable("users_data", {
|
||||
export const usersDataTable = table("users_data", {
|
||||
id: int().primaryKey().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
signin: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
lastTimestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
logCount: int().notNull().default(0),
|
||||
});
|
||||
|
||||
export const userSessionsTable = sqliteTable("user_sessions", {
|
||||
export const userSessionsTable = table("user_sessions", {
|
||||
id: int().notNull(),
|
||||
user_id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
}, (table): SQLiteTableExtraConfig => {
|
||||
return {
|
||||
pk: primaryKey({ columns: [table.id, table.user_id] }),
|
||||
}
|
||||
});
|
||||
}, (table) => [ primaryKey({ columns: [table.id, table.user_id] }) ]);
|
||||
|
||||
export const userPermissionsTable = sqliteTable("user_permissions", {
|
||||
export const userPermissionsTable = table("user_permissions", {
|
||||
id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
permission: text().notNull(),
|
||||
}, (table): SQLiteTableExtraConfig => {
|
||||
return {
|
||||
pk: primaryKey({ columns: [table.id, table.permission] }),
|
||||
}
|
||||
});
|
||||
}, (table) => [ primaryKey({ columns: [table.id, table.permission] }) ]);
|
||||
|
||||
export const explorerContentTable = sqliteTable("explorer_content", {
|
||||
path: text().primaryKey(),
|
||||
export const projectFilesTable = table("project_files", {
|
||||
id: text().primaryKey(),
|
||||
path: text().notNull().unique(),
|
||||
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
title: text().notNull(),
|
||||
type: text({ enum: ['file', 'folder', 'markdown', 'canvas', 'map'] }).notNull(),
|
||||
content: blob({ mode: 'buffer' }),
|
||||
navigable: int({ mode: 'boolean' }).notNull().default(true),
|
||||
private: int({ mode: 'boolean' }).notNull().default(false),
|
||||
order: int().notNull(),
|
||||
visit: int().notNull().default(0),
|
||||
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
export const emailValidationTable = sqliteTable("email_validation", {
|
||||
export const projectContentTable = table("project_content", {
|
||||
id: text().primaryKey(),
|
||||
content: blob({ mode: 'buffer' }),
|
||||
});
|
||||
|
||||
export const emailValidationTable = table("email_validation", {
|
||||
id: text().primaryKey(),
|
||||
timestamp: int({ mode: 'timestamp' }).notNull(),
|
||||
})
|
||||
|
|
@ -57,7 +52,7 @@ export const usersRelation = relations(usersTable, ({ one, many }) => ({
|
|||
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
|
||||
session: many(userSessionsTable),
|
||||
permission: many(userPermissionsTable),
|
||||
content: many(explorerContentTable),
|
||||
files: many(projectFilesTable),
|
||||
}));
|
||||
export const usersDataRelation = relations(usersDataTable, ({ one }) => ({
|
||||
users: one(usersTable, { fields: [usersDataTable.id], references: [usersTable.id], }),
|
||||
|
|
@ -68,6 +63,9 @@ export const userSessionsRelation = relations(userSessionsTable, ({ one }) => ({
|
|||
export const userPermissionsRelation = relations(userPermissionsTable, ({ one }) => ({
|
||||
users: one(usersTable, { fields: [userPermissionsTable.id], references: [usersTable.id], }),
|
||||
}));
|
||||
export const explorerContentRelation = relations(explorerContentTable, ({ one }) => ({
|
||||
users: one(usersTable, { fields: [explorerContentTable.owner], references: [usersTable.id], }),
|
||||
export const projectFilesRelation = relations(projectFilesTable, ({ one }) => ({
|
||||
users: one(usersTable, { fields: [projectFilesTable.owner], references: [usersTable.id], }),
|
||||
}));
|
||||
export const projectContentRelation = relations(projectContentTable, ({ one }) => ({
|
||||
files: one(projectFilesTable, { fields: [projectContentTable.id], references: [projectFilesTable.id], }),
|
||||
}));
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
CREATE TABLE `project_content` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`content` blob
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `project_files` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`path` text NOT NULL,
|
||||
`owner` integer NOT NULL,
|
||||
`title` text NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`navigable` integer DEFAULT true NOT NULL,
|
||||
`private` integer DEFAULT false NOT NULL,
|
||||
`order` integer NOT NULL,
|
||||
`timestamp` integer NOT NULL,
|
||||
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `project_files_path_unique` ON `project_files` (`path`);--> statement-breakpoint
|
||||
DROP TABLE `explorer_content`;--> statement-breakpoint
|
||||
ALTER TABLE `users_data` DROP COLUMN `logCount`;
|
||||
|
|
@ -0,0 +1,375 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "b24a44ad-b43a-4aba-be9c-350fd41bed04",
|
||||
"prevId": "a2731c1f-4150-4423-946e-670d794f8961",
|
||||
"tables": {
|
||||
"email_validation": {
|
||||
"name": "email_validation",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"project_content": {
|
||||
"name": "project_content",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "blob",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"project_files": {
|
||||
"name": "project_files",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"owner": {
|
||||
"name": "owner",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"navigable": {
|
||||
"name": "navigable",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"private": {
|
||||
"name": "private",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"order": {
|
||||
"name": "order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"project_files_path_unique": {
|
||||
"name": "project_files_path_unique",
|
||||
"columns": [
|
||||
"path"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"project_files_owner_users_id_fk": {
|
||||
"name": "project_files_owner_users_id_fk",
|
||||
"tableFrom": "project_files",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"owner"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user_permissions": {
|
||||
"name": "user_permissions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"permission": {
|
||||
"name": "permission",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_permissions_id_users_id_fk": {
|
||||
"name": "user_permissions_id_users_id_fk",
|
||||
"tableFrom": "user_permissions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_permissions_id_permission_pk": {
|
||||
"columns": [
|
||||
"id",
|
||||
"permission"
|
||||
],
|
||||
"name": "user_permissions_id_permission_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user_sessions": {
|
||||
"name": "user_sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_sessions_user_id_users_id_fk": {
|
||||
"name": "user_sessions_user_id_users_id_fk",
|
||||
"tableFrom": "user_sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_sessions_id_user_id_pk": {
|
||||
"columns": [
|
||||
"id",
|
||||
"user_id"
|
||||
],
|
||||
"name": "user_sessions_id_user_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users_data": {
|
||||
"name": "users_data",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"signin": {
|
||||
"name": "signin",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lastTimestamp": {
|
||||
"name": "lastTimestamp",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"users_data_id_users_id_fk": {
|
||||
"name": "users_data_id_users_id_fk",
|
||||
"tableFrom": "users_data",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"hash": {
|
||||
"name": "hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_username_unique": {
|
||||
"name": "users_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"users_hash_unique": {
|
||||
"name": "users_hash_unique",
|
||||
"columns": [
|
||||
"hash"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,13 @@
|
|||
"when": 1734426608563,
|
||||
"tag": "0005_panoramic_slayback",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "6",
|
||||
"when": 1743344302223,
|
||||
"tag": "0006_luxuriant_blade",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<CollapsibleRoot class="flex flex-1 flex-col" v-model:open="open">
|
||||
<div class="z-40 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
||||
<div class="z-30 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
||||
<div class="flex items-center px-2 gap-4">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button icon class="!bg-transparent group md:hidden">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.5.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||
"@codemirror/lang-markdown": "^6.3.2",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<Title>d[any] - Modification</Title>
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col xl:-mx-12 xl:-my-8 lg:-mx-8 lg:-my-6 -mx-6 -my-3 overflow-hidden">
|
||||
<div class="z-50 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
||||
<div class="z-30 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
||||
<div class="flex items-center px-2 gap-4">
|
||||
<!-- <CollapsibleTrigger asChild>
|
||||
<Button icon class="!bg-transparent group md:hidden">
|
||||
|
|
@ -36,22 +36,54 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { Content, Editor } from '#shared/content.util';
|
||||
import { loading } from '#shared/proses';
|
||||
import { button, loading } from '#shared/proses';
|
||||
import { dom, icon, text } from '~/shared/dom.util';
|
||||
import { modal, popper } from '~/shared/floating.util';
|
||||
|
||||
definePageMeta({
|
||||
rights: ['admin', 'editor'],
|
||||
layout: 'null',
|
||||
});
|
||||
|
||||
const toaster = useToast();
|
||||
const { user } = useUserSession();
|
||||
const tree = useTemplateRef('tree'), container = useTemplateRef('container');
|
||||
let editor: Editor;
|
||||
|
||||
function pull()
|
||||
{
|
||||
Content.pull().then(e => {
|
||||
toaster.add({ type: 'success', content: 'Données mises à jour avec succès.', timer: true, duration: 7500 });
|
||||
}).catch(e => {
|
||||
toaster.add({ type: 'success', content: 'Une erreur est survenue durant la récupération des données.', timer: true, duration: 7500 });
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
function push()
|
||||
{
|
||||
const { close } = modal([dom('div', { class: 'flex flex-col gap-4 justify-center items-center' }, [ dom('div', { class: 'text-xl', text: 'Mise à jour des données' }), loading('large') ])], { priority: false, closeWhenOutside: true, });
|
||||
Content.push().then(e => {
|
||||
close();
|
||||
toaster.add({ type: 'success', content: 'Données mises à jour avec succès.', timer: true, duration: 7500 });
|
||||
}).catch(e => {
|
||||
close();
|
||||
toaster.add({ type: 'success', content: 'Une erreur est survenue durant l\'enregistrement des données.', timer: true, duration: 7500 });
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if(tree.value && container.value && await Content.ready)
|
||||
{
|
||||
const load = loading('normal');
|
||||
tree.value.appendChild(load);
|
||||
|
||||
const content = dom('div', { class: 'flex flex-row justify-start items-center gap-4 p-2' }, [
|
||||
popper(button(icon('ph:cloud-arrow-down', { height: 20, width: 20 }), pull, 'p-1'), { placement: 'top', offset: 4, delay: 120, arrow: true, content: [text('Actualiser')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50' }),
|
||||
popper(button(icon('ph:cloud-arrow-up', { height: 20, width: 20 }), push, 'p-1'), { placement: 'top', offset: 4, delay: 120, arrow: true, content: [text('Enregistrer')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50' }),
|
||||
])
|
||||
|
||||
tree.value.insertBefore(content, load);
|
||||
|
||||
editor = new Editor();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const fileType = z.enum(['folder', 'file', 'markdown', 'canvas', 'map']);
|
||||
export const schema = z.object({
|
||||
path: z.string(),
|
||||
owner: z.number().finite(),
|
||||
title: z.string(),
|
||||
type: fileType,
|
||||
content: z.string(),
|
||||
navigable: z.boolean(),
|
||||
private: z.boolean(),
|
||||
order: z.number().finite(),
|
||||
});
|
||||
|
||||
export type FileType = z.infer<typeof fileType>;
|
||||
export type File = z.infer<typeof schema>;
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import { z } from "zod";
|
||||
import { fileType } from "./file";
|
||||
|
||||
export const single = z.object({
|
||||
path: z.string(),
|
||||
owner: z.number().finite(),
|
||||
title: z.string(),
|
||||
type: fileType,
|
||||
navigable: z.boolean(),
|
||||
private: z.boolean(),
|
||||
order: z.number().finite(),
|
||||
});
|
||||
export const table = z.array(single);
|
||||
|
||||
export type Navigation = z.infer<typeof table>;
|
||||
export type NavigationItem = z.infer<typeof single>;
|
||||
|
|
@ -1,22 +1,16 @@
|
|||
import { z } from "zod";
|
||||
import { fileType } from "./file";
|
||||
import { projectFilesTable } from "~/db/schema";
|
||||
|
||||
const baseItem = z.object({
|
||||
export const Project = z.array(z.object({
|
||||
id: z.string(),
|
||||
path: z.string(),
|
||||
parent: z.string(),
|
||||
name: z.string(),
|
||||
title: z.string(),
|
||||
type: fileType,
|
||||
type: z.enum(projectFilesTable.type.enumValues),
|
||||
navigable: z.boolean(),
|
||||
private: z.boolean(),
|
||||
order: z.number().finite(),
|
||||
content: z.string().optional().or(z.null()),
|
||||
});
|
||||
export const item: z.ZodType<ProjectItem> = baseItem.extend({
|
||||
children: z.lazy(() => item.array().optional()),
|
||||
});
|
||||
export const project = z.array(item);
|
||||
timestamp: z.string(),
|
||||
}));
|
||||
|
||||
export type ProjectItem = z.infer<typeof baseItem> & {
|
||||
children?: ProjectItem[]
|
||||
};
|
||||
export type ProjectType = z.infer<typeof Project>;
|
||||
export type ProjectItemType = ProjectType[number];
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import type { SitemapUrlInput } from '#sitemap/types'
|
||||
import { explorerContentTable } from '~/db/schema';
|
||||
import { projectFilesTable as files } from '~/db/schema';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
|
||||
export default defineSitemapEventHandler(() => {
|
||||
const db = useDatabase();
|
||||
const pages = db.select({ path: explorerContentTable.path, lastMod: explorerContentTable.timestamp, navigable: explorerContentTable.navigable, private: explorerContentTable.private, type: explorerContentTable.type }).from(explorerContentTable).all();
|
||||
const pages = db.select({ path: files.path, lastMod: files.timestamp, navigable: files.navigable, private: files.private, type: files.type }).from(files).all();
|
||||
|
||||
return pages.filter(e => e.type !== 'folder' && e.navigable && !e.private && e.path.split('/').map((_, i, a) => a.slice(0, i).join('/')).every(p => !pages.find(_p => _p.path === p)?.private)).map(e => ({
|
||||
loc: `/explore/${encodeURIComponent(e.path)}`,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { ne, sql } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { explorerContentTable } from '~/db/schema';
|
||||
import { hasPermissions } from '~/shared/auth.util';
|
||||
import { projectFilesTable } from '~/db/schema';
|
||||
import { hasPermissions } from '#shared/auth.util';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const session = await getUserSession(e);
|
||||
|
|
@ -16,17 +15,15 @@ export default defineEventHandler(async (e) => {
|
|||
|
||||
const db = useDatabase();
|
||||
const content = db.select({
|
||||
path: explorerContentTable.path,
|
||||
owner: explorerContentTable.owner,
|
||||
title: explorerContentTable.title,
|
||||
type: explorerContentTable.type,
|
||||
size: sql<number>`CASE WHEN ${explorerContentTable.content} IS NULL THEN 0 ELSE length(${explorerContentTable.content}) END`.as('size'),
|
||||
navigable: explorerContentTable.navigable,
|
||||
private: explorerContentTable.private,
|
||||
order: explorerContentTable.order,
|
||||
visit: explorerContentTable.visit,
|
||||
timestamp: explorerContentTable.timestamp,
|
||||
}).from(explorerContentTable).all();
|
||||
path: projectFilesTable.path,
|
||||
owner: projectFilesTable.owner,
|
||||
title: projectFilesTable.title,
|
||||
type: projectFilesTable.type,
|
||||
navigable: projectFilesTable.navigable,
|
||||
private: projectFilesTable.private,
|
||||
order: projectFilesTable.order,
|
||||
timestamp: projectFilesTable.timestamp,
|
||||
}).from(projectFilesTable).all();
|
||||
|
||||
content.sort((a, b) => {
|
||||
return a.path.split('/').length - b.path.split('/').length;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import { sql } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { userSessionsTable } from '~/db/schema';
|
||||
import { hasPermissions } from '~/shared/auth.util';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
|
|
|
|||
|
|
@ -93,8 +93,6 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
|||
}
|
||||
}) as UserSessionRequired);
|
||||
|
||||
db.update(usersDataTable).set({ logCount: user.data.logCount + 1 }).where(eq(usersDataTable.id, user.id)).run();
|
||||
|
||||
setResponseStatus(e, 201);
|
||||
return { success: true, session: data };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
|||
|
||||
db.insert(usersDataTable).values({ id: sql.placeholder('id') }).prepare().run({ id: id.id });
|
||||
|
||||
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date(), logCount: 1 } }) as UserSessionRequired);
|
||||
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date() } }) as UserSessionRequired);
|
||||
|
||||
const emailId = Bun.hash('register' + id.id + hash, Date.now());
|
||||
const timestamp = Date.now() + 1000 * 60 * 60;
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
|||
|
||||
db.insert(usersDataTable).values({ id: sql.placeholder('id') }).prepare().run({ id: id.id });
|
||||
|
||||
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date(), logCount: 1 } }) as UserSessionRequired);
|
||||
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date() } }) as UserSessionRequired);
|
||||
|
||||
await runTask('mail', {
|
||||
payload: {
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
import { explorerContentTable } from '~/db/schema';
|
||||
import { schema } from '~/schemas/file';
|
||||
import { parsePath } from '~/shared/general.util';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const body = await readValidatedBody(e, schema.safeParse);
|
||||
if(!body.success)
|
||||
{
|
||||
setResponseStatus(e, 403);
|
||||
throw body.error;
|
||||
}
|
||||
|
||||
const db = useDatabase();
|
||||
|
||||
const buffer = Buffer.from(convertToStorableLinks(body.data.content, db.select({ path: explorerContentTable.path }).from(explorerContentTable).all().map(e => e.path)), 'utf-8');
|
||||
|
||||
const content = db.insert(explorerContentTable).values({ ...body.data, content: buffer }).onConflictDoUpdate({ target: explorerContentTable.path, set: { ...body.data, content: buffer, timestamp: new Date() } });
|
||||
|
||||
if(content !== undefined)
|
||||
{
|
||||
return content;
|
||||
}
|
||||
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
});
|
||||
|
||||
export function convertToStorableLinks(content: string, path: string[]): string
|
||||
{
|
||||
return content.replaceAll(/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/g, (e: string, a1?: string, a2?: string , a3?: string) => {
|
||||
const parsed = parsePath(a1 ?? '%%%%----%%%%----%%%%');
|
||||
const replacer = path.find(e => e.endsWith(parsed));
|
||||
const value = `[[${a1 ? (replacer ?? '') : ''}${a2 ?? ''}${(!a3 && a1 && replacer !== parsed ? '|' + a1 : a3) ?? ''}]]`;
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { eq, sql } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { projectFilesTable as files, projectContentTable as content } from '~/db/schema';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
try
|
||||
{
|
||||
const id = getRouterParam(e, "id") ?? '';
|
||||
if(!id)
|
||||
{
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const db = useDatabase();
|
||||
const data = db.select({ content: sql<string>`cast(${content.content} as TEXT)`.as('content'), private: files.private, owner: files.owner, }).from(content).leftJoin(files, eq(content.id, files.id)).where(eq(content.id, id)).get();
|
||||
|
||||
if(data && data.content)
|
||||
{
|
||||
const session = await getUserSession(e);
|
||||
|
||||
if(!session || !session.user || session.user.id !== data.owner)
|
||||
{
|
||||
if(data.private)
|
||||
{
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return data.content.replace(/%%(.+?)%%/g, "");
|
||||
}
|
||||
}
|
||||
|
||||
return data.content;
|
||||
}
|
||||
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
}
|
||||
catch(_e)
|
||||
{
|
||||
console.error(_e);
|
||||
setResponseStatus(e, 500);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
import { hasPermissions } from '#shared/auth.util';
|
||||
import { projectContentTable, projectFilesTable } from '~/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const { user } = await getUserSession(e);
|
||||
|
||||
if(!user || !hasPermissions(user.permissions, ['admin', 'editor']))
|
||||
{
|
||||
throw createError({ statusCode: 401, statusText: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const id = getRouterParam(e, "id") ?? '';
|
||||
const body = await readRawBody(e);
|
||||
|
||||
if(!id)
|
||||
{
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const db = useDatabase();
|
||||
const item = db.select({ id: projectFilesTable.id }).from(projectFilesTable).where(eq(projectFilesTable.id, id)).get();
|
||||
|
||||
if(!item)
|
||||
{
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
}
|
||||
|
||||
db.insert(projectContentTable).values({ id: id, content: body }).onConflictDoUpdate({ set: { content: body }, target: projectContentTable.id }).run();
|
||||
db.update(projectFilesTable).set({ timestamp: new Date() }).where(eq(projectFilesTable.id, id)).run()
|
||||
}
|
||||
catch(_e)
|
||||
{
|
||||
console.error(_e);
|
||||
setResponseStatus(e, 500);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import { eq, sql } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { explorerContentTable } from '~/db/schema';
|
||||
import { Content } from '~/shared/content.util';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
try
|
||||
{
|
||||
const path = decodeURIComponent(getRouterParam(e, "path") ?? '');
|
||||
const query = getQuery(e);
|
||||
|
||||
if(!path)
|
||||
{
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const db = useDatabase();
|
||||
const content = db.select({
|
||||
'content': sql<string>`cast(${explorerContentTable.content} as TEXT)`.as('content'),
|
||||
'private': explorerContentTable.private,
|
||||
'type': explorerContentTable.type,
|
||||
'owner': explorerContentTable.owner,
|
||||
'visit': explorerContentTable.visit,
|
||||
}).from(explorerContentTable).where(eq(explorerContentTable.path, sql.placeholder('path'))).prepare().get({ path });
|
||||
|
||||
if(content != undefined && content.content != undefined)
|
||||
{
|
||||
const session = await getUserSession(e);
|
||||
|
||||
if(!session || !session.user || session.user.id !== content.owner)
|
||||
{
|
||||
if(content.private)
|
||||
{
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
content.content = content.content.replace(/%%(.+)%%/g, "");
|
||||
}
|
||||
}
|
||||
if(query?.type === 'view')
|
||||
{
|
||||
db.update(explorerContentTable).set({ visit: content.visit + 1 }).where(eq(explorerContentTable.path, path)).run();
|
||||
}
|
||||
if(query?.type === 'editing')
|
||||
{
|
||||
content.content = convertFromStorableLinks(content.content);
|
||||
}
|
||||
|
||||
return Content.fromString(content as any, content.content);
|
||||
}
|
||||
|
||||
console.log(content);
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
}
|
||||
catch(_e)
|
||||
{
|
||||
console.error(_e);
|
||||
setResponseStatus(e, 500);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
export function convertFromStorableLinks(content: string): string
|
||||
{
|
||||
/*return content.replaceAll(/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/g, (e: string, a1?: string, a2?: string , a3?: string) => {
|
||||
const parsed = parsePath(a1 ?? '%%%%----%%%%----%%%%');
|
||||
const replacer = path.find(e => e.endsWith(parsed)) ?? parsed;
|
||||
const value = `[[${replacer}${a2 ?? ''}${(!a3 && replacer !== parsed ? '|' + a1 : a3) ?? ''}]]`;
|
||||
return value;
|
||||
});*/
|
||||
return content;
|
||||
}
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
import { getTableColumns } from 'drizzle-orm';
|
||||
import { explorerContentTable } from '~/db/schema';
|
||||
import { projectFilesTable } from '~/db/schema';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const { user } = await getUserSession(e);
|
||||
|
||||
const db = useDatabase();
|
||||
const { content: _, ...columns } = getTableColumns(explorerContentTable);
|
||||
const content = db.select(columns).from(explorerContentTable).all();
|
||||
const content = db.select().from(projectFilesTable).all();
|
||||
|
||||
content.sort((a, b) => {
|
||||
return a.path.split('/').length - b.path.split('/').length;
|
||||
|
|
@ -36,6 +34,5 @@ export default defineEventHandler(async (e) => {
|
|||
return content.filter(e => !!e);
|
||||
}
|
||||
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
return [];
|
||||
});
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
import { hasPermissions } from "#shared/auth.util";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { projectFilesTable } from "~/db/schema";
|
||||
import { Project } from "~/schemas/project";
|
||||
|
||||
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 db = useDatabase(), items = body.data, blocked: string[] = [];
|
||||
|
||||
db.transaction((tx) => {
|
||||
const data = tx.select({ id: projectFilesTable.id, timestamp: projectFilesTable.timestamp }).from(projectFilesTable).all();
|
||||
const deletion = tx.delete(projectFilesTable).where(eq(projectFilesTable.id, sql.placeholder('id'))).prepare();
|
||||
|
||||
for(let i = 0; i < items.length; i++)
|
||||
{
|
||||
const item = items[i];
|
||||
|
||||
const index = data.findIndex(e => e.id === item.id);
|
||||
|
||||
if(index !== -1)
|
||||
{
|
||||
if(data[index].timestamp > new Date(item.timestamp))
|
||||
{
|
||||
blocked.push(item.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
data.splice(index, 1);
|
||||
}
|
||||
|
||||
tx.insert(projectFilesTable).values({
|
||||
id: item.id,
|
||||
path: item.path,
|
||||
owner: user.id,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
navigable: item.navigable,
|
||||
private: item.private,
|
||||
order: item.order,
|
||||
}).onConflictDoUpdate({
|
||||
set: {
|
||||
id: item.id,
|
||||
path: item.path,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
navigable: item.navigable,
|
||||
private: item.private,
|
||||
order: item.order,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
target: projectFilesTable.id,
|
||||
}).run();
|
||||
}
|
||||
|
||||
for(let i = 0; i < data.length; i++)
|
||||
{
|
||||
deletion.run({ id: data[i].id });
|
||||
}
|
||||
});
|
||||
|
||||
return blocked;
|
||||
});
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { eq, sql } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { explorerContentTable } from '~/db/schema';
|
||||
import { projectFilesTable } from '~/db/schema';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const path = decodeURIComponent(getRouterParam(e, "path") ?? '');
|
||||
export default defineCachedEventHandler(async (e) => {
|
||||
const id = getRouterParam(e, "id") ?? '';
|
||||
|
||||
if(!path)
|
||||
if(!id)
|
||||
{
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
|
|
@ -14,13 +14,14 @@ export default defineEventHandler(async (e) => {
|
|||
const db = useDatabase();
|
||||
|
||||
const content = db.select({
|
||||
'path': explorerContentTable.path,
|
||||
'owner': explorerContentTable.owner,
|
||||
'title': explorerContentTable.title,
|
||||
'type': explorerContentTable.type,
|
||||
'navigable': explorerContentTable.navigable,
|
||||
'private': explorerContentTable.private,
|
||||
}).from(explorerContentTable).where(eq(explorerContentTable.path, sql.placeholder('path'))).prepare().get({ path });
|
||||
'id': projectFilesTable.id,
|
||||
'path': projectFilesTable.path,
|
||||
'owner': projectFilesTable.owner,
|
||||
'title': projectFilesTable.title,
|
||||
'type': projectFilesTable.type,
|
||||
'navigable': projectFilesTable.navigable,
|
||||
'private': projectFilesTable.private,
|
||||
}).from(projectFilesTable).where(eq(projectFilesTable.id, sql.placeholder('id'))).prepare().get({ id });
|
||||
|
||||
if(content !== undefined)
|
||||
{
|
||||
|
|
@ -47,4 +48,4 @@ export default defineEventHandler(async (e) => {
|
|||
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
});
|
||||
}, { getKey: (e) => getRouterParam(e, "id") ?? '', });
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
import { explorerContentTable } from '~/db/schema';
|
||||
import type { NavigationItem } from '~/schemas/navigation';
|
||||
|
||||
export type NavigationTreeItem = NavigationItem & { children?: NavigationTreeItem[] };
|
||||
export default defineEventHandler(async (e) => {
|
||||
const { user } = await getUserSession(e);
|
||||
|
||||
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).all();
|
||||
|
||||
content.sort((a, b) => {
|
||||
return a.path.split('/').length - b.path.split('/').length;
|
||||
});
|
||||
|
||||
if(content.length > 0)
|
||||
{
|
||||
const navigation: NavigationTreeItem[] = [];
|
||||
|
||||
for(const idx in content)
|
||||
{
|
||||
const item = content[idx];
|
||||
if(!!item.private && (user?.id ?? -1) !== item.owner || !item.navigable)
|
||||
{
|
||||
delete content[idx];
|
||||
continue;
|
||||
}
|
||||
|
||||
const parent = item.path.includes('/') ? item.path.substring(0, item.path.lastIndexOf('/')) : undefined;
|
||||
|
||||
if(parent && !content.find(e => e && e.path === parent))
|
||||
{
|
||||
delete content[idx];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for(const item of content.filter(e => !!e))
|
||||
{
|
||||
addChild(navigation, item);
|
||||
}
|
||||
|
||||
return navigation;
|
||||
}
|
||||
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
});
|
||||
|
||||
function addChild(arr: NavigationTreeItem[], 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({ ...e });
|
||||
arr.sort((a, b) => {
|
||||
if(a.order !== b.order)
|
||||
return a.order - b.order;
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
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();
|
||||
|
||||
content.sort((a, b) => {
|
||||
return a.path.split('/').length - b.path.split('/').length;
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
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.util";
|
||||
import { eq, getTableColumns, sql } from "drizzle-orm";
|
||||
import type { ExploreContent, TreeItem } from "~/types/content";
|
||||
import type { TreeItemEditable } from "~/pages/explore/edit/index.vue";
|
||||
|
||||
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) as Array<TreeItemEditable & { match?: number }>;
|
||||
|
||||
const db = useDatabase();
|
||||
const { ...cols } = getTableColumns(explorerContentTable);
|
||||
const full = db.select(cols).from(explorerContentTable).all() as Record<string, any>[];
|
||||
|
||||
for(let i = full.length - 1; i >= 0; i--)
|
||||
{
|
||||
const item = items.find(e => (e.path === '' ? [e.parent, parsePath(e.name === '' ? e.title : e.name)].filter(e => !!e).join('/') : e.path) === full[i].path);
|
||||
if(item)
|
||||
{
|
||||
item.match = i;
|
||||
full[i].include = true;
|
||||
}
|
||||
}
|
||||
|
||||
db.transaction((tx) => {
|
||||
for(let i = 0; i < items.length; i++)
|
||||
{
|
||||
const item = items[i];
|
||||
const old = full[item.match!];
|
||||
|
||||
const path = [item.parent, parsePath(item.name === '' ? item.title : item.name)].filter(e => !!e).join('/');
|
||||
|
||||
tx.insert(explorerContentTable).values({
|
||||
path: item.path || path,
|
||||
owner: user.id,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
navigable: item.navigable,
|
||||
private: item.private,
|
||||
order: item.order,
|
||||
content: item.content ?? old?.content ?? null,
|
||||
}).onConflictDoUpdate({
|
||||
set: {
|
||||
path: path,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
navigable: item.navigable,
|
||||
private: item.private,
|
||||
order: item.order,
|
||||
timestamp: new Date(),
|
||||
content: item.content ?? old?.content ?? null,
|
||||
},
|
||||
target: explorerContentTable.path,
|
||||
}).run();
|
||||
|
||||
if(item.path !== path && !old)
|
||||
{
|
||||
tx.update(explorerContentTable).set({ content: sql`replace(${explorerContentTable.content}, ${sql.placeholder('old')}, ${sql.placeholder('new')})` }).prepare().run({ 'old': item.path, 'new': path });
|
||||
}
|
||||
}
|
||||
for(let i = 0; i < full.length; i++)
|
||||
{
|
||||
if(full[i].include !== true)
|
||||
tx.delete(explorerContentTable).where(eq(explorerContentTable.path, full[i].path)).run();
|
||||
}
|
||||
});
|
||||
|
||||
return items.map(e => ({
|
||||
path: e.path,
|
||||
owner: e.owner,
|
||||
title: e.title,
|
||||
type: e.type,
|
||||
content: e.content,
|
||||
navigable: e.navigable,
|
||||
private: e.private,
|
||||
order: e.order,
|
||||
visit: e.visit,
|
||||
timestamp: e.timestamp,
|
||||
})) as ExploreContent[];
|
||||
});
|
||||
|
||||
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 ?? [])]);
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import useDatabase from "~/composables/useDatabase";
|
||||
import { extname, basename } from 'node:path';
|
||||
import type { FileType } from '~/types/content';
|
||||
import type { CanvasColor, CanvasContent } from "~/types/canvas";
|
||||
import { explorerContentTable } from "~/db/schema";
|
||||
import { convertToStorableLinks } from "../api/file.post";
|
||||
import type { FileType, ProjectContent } from "#shared/content.util";
|
||||
import { getID, ID_SIZE } from "#shared/general.util";
|
||||
import { projectContentTable, projectFilesTable } from "~/db/schema";
|
||||
|
||||
const typeMapping: Record<string, FileType> = {
|
||||
".md": "markdown",
|
||||
|
|
@ -17,6 +17,7 @@ export default defineTask({
|
|||
},
|
||||
async run(event) {
|
||||
try {
|
||||
//@ts-ignore
|
||||
const tree = await $fetch('https://git.peaceultime.com/api/v1/repos/peaceultime/system-aspect/git/trees/master', {
|
||||
method: 'get',
|
||||
headers: {
|
||||
|
|
@ -28,21 +29,23 @@ export default defineTask({
|
|||
}
|
||||
}) as { tree: any[] } & Record<string, any>;
|
||||
|
||||
const files: typeof explorerContentTable.$inferInsert[] = await Promise.all(tree.tree.filter((e: any) => !e.path.startsWith(".")).map(async (e, i) => {
|
||||
const files: ProjectContent[] = await Promise.all(tree.tree.filter((e: any) => !e.path.startsWith(".")).map(async (e, i) => {
|
||||
if(e.type === 'tree')
|
||||
{
|
||||
const title = basename(e.path);
|
||||
const order = /(\d+)\. ?(.+)/gsmi.exec(title);
|
||||
const path = (e.path as string).split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/');
|
||||
return {
|
||||
id: getID(ID_SIZE),
|
||||
path: path.toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, ""),
|
||||
order: i,
|
||||
title: order && order[2] ? order[2] : title,
|
||||
type: 'folder',
|
||||
content: null,
|
||||
owner: '1',
|
||||
owner: 1,
|
||||
navigable: true,
|
||||
private: e.path.startsWith('98.Privé'),
|
||||
timestamp: new Date(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,28 +56,26 @@ export default defineTask({
|
|||
const content = (await $fetch(`https://git.peaceultime.com/api/v1/repos/peaceultime/system-aspect/raw/${encodeURIComponent(e.path)}`));
|
||||
|
||||
return {
|
||||
id: getID(ID_SIZE),
|
||||
path: (extension === '.md' ? path.replace(extension, '') : path).toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, ""),
|
||||
order: i,
|
||||
title: order && order[2] ? order[2] : title,
|
||||
type: (typeMapping[extension] ?? 'file'),
|
||||
content: reshapeContent(content as string, typeMapping[extension] ?? 'File'),
|
||||
owner: '1',
|
||||
owner: 1,
|
||||
navigable: true,
|
||||
private: e.path.startsWith('98.Privé')
|
||||
private: e.path.startsWith('98.Privé'),
|
||||
timestamp: new Date(),
|
||||
}
|
||||
}));
|
||||
|
||||
const pathList = files.map(e => e.path);
|
||||
files.forEach(e => {
|
||||
if(e.type !== 'folder' && e.content)
|
||||
{
|
||||
e.content = Buffer.from(convertToStorableLinks(e.content.toString('utf-8'), files.map(e => e.path)), 'utf-8');
|
||||
}
|
||||
})
|
||||
|
||||
const db = useDatabase();
|
||||
db.delete(explorerContentTable).run();
|
||||
db.insert(explorerContentTable).values(files).run();
|
||||
db.transaction(tx => {
|
||||
db.delete(projectFilesTable).run();
|
||||
db.insert(projectFilesTable).values(files.map(e => {const { content, ...rest } = e; return rest; })).run();
|
||||
db.delete(projectContentTable).run();
|
||||
db.insert(projectContentTable).values(files.map(e => ({ content: e.content ? Buffer.from(e.content as string) : null, id: e.id }))).run();
|
||||
});
|
||||
|
||||
return { result: true };
|
||||
}
|
||||
|
|
@ -99,8 +100,8 @@ function reshapeContent(content: string, type: FileType): string | null
|
|||
return content;
|
||||
case "canvas":
|
||||
const data = JSON.parse(content) as CanvasContent;
|
||||
data.edges.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined);
|
||||
data.nodes.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined);
|
||||
data.edges?.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined);
|
||||
data.nodes?.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined);
|
||||
return JSON.stringify(data);
|
||||
default:
|
||||
case 'folder':
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import useDatabase from "~/composables/useDatabase";
|
||||
/*import useDatabase from "~/composables/useDatabase";
|
||||
import type { FileType } from '~/types/content';
|
||||
import { explorerContentTable } from "~/db/schema";
|
||||
import { eq, ne } from "drizzle-orm";
|
||||
|
|
@ -27,4 +27,4 @@ export default defineTask({
|
|||
return { result: false, error: e };
|
||||
}
|
||||
},
|
||||
})
|
||||
})*/
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { safeDestr as parse } from 'destr';
|
||||
import { Canvas } from "#shared/canvas.util";
|
||||
import render from "#shared/markdown.util";
|
||||
import { contextmenu, popper } from "#shared/floating.util";
|
||||
import { confirm, contextmenu, popper } from "#shared/floating.util";
|
||||
import { cancelPropagation, dom, icon, text, type Node } from "#shared/dom.util";
|
||||
import prose, { h1, h2, loading } from "#shared/proses";
|
||||
import { parsePath } from '#shared/general.util';
|
||||
import { Tree, TreeDOM, type Recursive } from '#shared/tree';
|
||||
import { getID, ID_SIZE, parsePath } from '#shared/general.util';
|
||||
import { TreeDOM, type Recursive } from '#shared/tree';
|
||||
import { History } from '#shared/history.util';
|
||||
import { MarkdownEditor } from '#shared/editor.util';
|
||||
import type { CanvasContent } from '~/types/canvas';
|
||||
|
|
@ -13,6 +13,7 @@ import type { CanvasContent } from '~/types/canvas';
|
|||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable, dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { type Instruction, attachInstruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
||||
import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
|
||||
import type { CleanupFn } from '@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types';
|
||||
|
||||
export type FileType = keyof ContentMap;
|
||||
|
|
@ -26,6 +27,7 @@ export interface ContentMap
|
|||
}
|
||||
export interface Overview<T extends FileType>
|
||||
{
|
||||
id: string;
|
||||
path: string;
|
||||
owner: number;
|
||||
title: string;
|
||||
|
|
@ -33,10 +35,9 @@ export interface Overview<T extends FileType>
|
|||
navigable: boolean;
|
||||
private: boolean;
|
||||
order: number;
|
||||
visit: number;
|
||||
type: T;
|
||||
}
|
||||
export type ExploreContent<T extends FileType = FileType> = Overview<T> & { content: ContentMap[T] };
|
||||
export type ProjectContent<T extends FileType = FileType> = Overview<T> & { content: ContentMap[T] };
|
||||
|
||||
class AsyncQueue
|
||||
{
|
||||
|
|
@ -48,6 +49,7 @@ class AsyncQueue
|
|||
finished: boolean = true;
|
||||
|
||||
private res: (value: void | PromiseLike<void>) => void = () => {};
|
||||
private rej: (value: void | PromiseLike<void>) => void = () => {};
|
||||
|
||||
constructor(size: number = 8)
|
||||
{
|
||||
|
|
@ -63,6 +65,7 @@ class AsyncQueue
|
|||
|
||||
this.promise = new Promise((res, rej) => {
|
||||
this.res = res;
|
||||
this.rej = rej;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +82,7 @@ class AsyncQueue
|
|||
this.count++;
|
||||
|
||||
const fn = this._queue.shift()!;
|
||||
fn().finally(() => {
|
||||
fn().catch(e => this.rej(e)).then(() => {
|
||||
this.count--;
|
||||
this.refresh();
|
||||
});
|
||||
|
|
@ -100,7 +103,7 @@ export const DEFAULT_CONTENT: Record<FileType, ContentMap[FileType]> = {
|
|||
file: '',
|
||||
folder: null,
|
||||
};
|
||||
export type LocalContent<T extends FileType = FileType> = ExploreContent<T> & {
|
||||
export type LocalContent<T extends FileType = FileType> = ProjectContent<T> & {
|
||||
localEdit?: boolean;
|
||||
error?: boolean;
|
||||
};
|
||||
|
|
@ -111,16 +114,15 @@ export class Content
|
|||
|
||||
private static root: FileSystemDirectoryHandle;
|
||||
|
||||
private static _overview: Omit<LocalContent, 'content'>[];
|
||||
private static dlQueue = new AsyncQueue();
|
||||
private static writeQueue = new AsyncQueue(1);
|
||||
private static _overview: Record<string, Omit<LocalContent, 'content'>>;
|
||||
private static queue = new AsyncQueue();
|
||||
|
||||
static init(): Promise<boolean>
|
||||
{
|
||||
if(Content._ready)
|
||||
return Promise.resolve(true);
|
||||
|
||||
Content.initPromise = new Promise(async (res, rej) => {
|
||||
Content.initPromise = new Promise(async (res) => {
|
||||
try
|
||||
{
|
||||
if(!('storage' in navigator))
|
||||
|
|
@ -131,11 +133,11 @@ export class Content
|
|||
const overview = await Content.read('overview', { create: true });
|
||||
try
|
||||
{
|
||||
Content._overview = parse<Omit<LocalContent, 'content'>[]>(overview);
|
||||
Content._overview = parse<Record<string, Omit<LocalContent, 'content'>>>(overview);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
Content._overview = [];
|
||||
Content._overview = {};
|
||||
await Content.pull();
|
||||
}
|
||||
|
||||
|
|
@ -151,51 +153,61 @@ export class Content
|
|||
|
||||
return Content.initPromise;
|
||||
}
|
||||
static overview(path: string): Omit<LocalContent, 'content'> | undefined
|
||||
static async get(id: string, content: boolean): Promise<LocalContent | undefined>
|
||||
{
|
||||
return Content._overview.find(e => getPath(e) === path);
|
||||
}
|
||||
static async content(path: string): Promise<LocalContent | undefined>
|
||||
{
|
||||
const overview = Content._overview.find(e => getPath(e) === path);
|
||||
const overview = Content._overview[id];
|
||||
|
||||
return overview ? { ...overview, content: Content.fromString(overview, await Content.read(encodeURIComponent(path)) ?? '') } as LocalContent : undefined;
|
||||
return overview ? { ...overview, content: content ? Content.fromString(overview, await Content.read(id) ?? '') : undefined } as LocalContent : undefined;
|
||||
}
|
||||
static update(item: Recursive<LocalContent>)
|
||||
static async set(id: string, overview?: Omit<LocalContent, 'content'> | Recursive<Omit<LocalContent, 'content'>>)
|
||||
{
|
||||
const index = Content._overview.findIndex(e => e.path === getPath(item));
|
||||
if(index !== -1)
|
||||
Content._overview[index] = item;
|
||||
if(overview === undefined)
|
||||
{
|
||||
delete Content._overview[id];
|
||||
}
|
||||
else
|
||||
{
|
||||
const {
|
||||
id: _id,
|
||||
path: _path,
|
||||
owner: _owner,
|
||||
title: _title,
|
||||
timestamp: _timestamp,
|
||||
navigable: _navigable,
|
||||
private: _private,
|
||||
order: _order,
|
||||
type: _type,
|
||||
...rest
|
||||
} = overview as Recursive<LocalContent>;
|
||||
Content._overview[id] = {
|
||||
id: _id,
|
||||
path: _path,
|
||||
owner: _owner,
|
||||
title: _title,
|
||||
timestamp: _timestamp,
|
||||
navigable: _navigable,
|
||||
private: _private,
|
||||
order: _order,
|
||||
type: _type,
|
||||
};
|
||||
}
|
||||
}
|
||||
static async save(content?: ProjectContent)
|
||||
{
|
||||
const overviewAsString = JSON.stringify(Content._overview)
|
||||
Content.queue.queue(() => Content.write("overview", overviewAsString));
|
||||
|
||||
const overview = JSON.stringify(Content._overview, (k, v) => ['parent', 'children', 'content'].includes(k) ? undefined : v);
|
||||
return Content.writeQueue.queue(() => Content.write('overview', overview));
|
||||
}
|
||||
static rename(from: string, to: string)
|
||||
{
|
||||
const index = Content._overview.findIndex(e => getPath(e) === from);
|
||||
if(index !== -1)
|
||||
Content._overview[index].path = to;
|
||||
if(content && content.content)
|
||||
{
|
||||
const contentAsString = Content.toString(content);
|
||||
Content.queue.queue(() => Content.write(content.id, contentAsString));
|
||||
}
|
||||
|
||||
return Content.writeQueue.queue(async () => {
|
||||
const content = await Content.read(encodeURIComponent(from));
|
||||
if(content !== undefined)
|
||||
{
|
||||
await Content.write(encodeURIComponent(to), content, { create: true });
|
||||
await Content.remove(encodeURIComponent(from));
|
||||
}
|
||||
});
|
||||
return Content.queue.promise;
|
||||
}
|
||||
static save(content?: Recursive<LocalContent>)
|
||||
static async pull()
|
||||
{
|
||||
if(!content)
|
||||
return;
|
||||
|
||||
const string = Content.toString(content), path = getPath(content);
|
||||
return Content.writeQueue.queue(() => Content.write(encodeURIComponent(path), string, { create: true }));
|
||||
}
|
||||
private static async pull()
|
||||
{
|
||||
const overview = (await useRequestFetch()('/api/file/overview', { cache: 'no-cache' })) as ExploreContent<FileType>[] | undefined;
|
||||
const overview = (await useRequestFetch()('/api/file/overview', { cache: 'no-cache' })) as ProjectContent<FileType>[] | undefined;
|
||||
|
||||
if(!overview)
|
||||
{
|
||||
|
|
@ -206,44 +218,50 @@ export class Content
|
|||
|
||||
for(const file of overview)
|
||||
{
|
||||
let index = Content._overview.findIndex(e => getPath(e) === file.path), _overview = (index === -1 ? undefined : Content._overview[index]);
|
||||
if(!_overview || _overview.localEdit)
|
||||
const _overview = Content._overview[file.id];
|
||||
if(_overview && _overview.localEdit)
|
||||
{
|
||||
const encoded = encodeURIComponent(file.path);
|
||||
if(!_overview)
|
||||
{
|
||||
index = Content._overview.push(file) - 1;
|
||||
}
|
||||
else
|
||||
Content._overview[index] = file;
|
||||
|
||||
_overview = file;
|
||||
//TODO: Ask what to do about this file.
|
||||
}
|
||||
else
|
||||
{
|
||||
Content._overview[file.id] = file;
|
||||
|
||||
if(file.type === 'folder')
|
||||
continue;
|
||||
|
||||
Content.dlQueue.queue(() => {
|
||||
return useRequestFetch()(`/api/file/content/${encoded}`, { cache: 'no-cache' }).then(async (content: ContentMap[FileType] | undefined) => {
|
||||
Content.queue.queue(() => {
|
||||
return useRequestFetch()(`/api/file/content/${file.id}`, { cache: 'no-cache' }).then(async (content: ContentMap[FileType] | undefined) => {
|
||||
if(content)
|
||||
await Content.write(encoded, Content.toString({ ...file, content }), { create: true });
|
||||
Content.queue.queue(() => Content.write(file.id, Content.toString({ ...file, content }), { create: true }));
|
||||
else
|
||||
Content._overview[index].error = true;
|
||||
Content._overview[file.id].error = true;
|
||||
}).catch(e => {
|
||||
Content._overview[index].error = true;
|
||||
})
|
||||
Content._overview[file.id].error = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Content.dlQueue.queue(() => {
|
||||
return Content.queue.queue(() => {
|
||||
return Content.write('overview', JSON.stringify(Content._overview), { create: true });
|
||||
});
|
||||
|
||||
await Content.dlQueue.promise;
|
||||
}
|
||||
private static async push()
|
||||
static async push()
|
||||
{
|
||||
const blocked = (await useRequestFetch()('/api/file/overview', { method: 'POST', body: Object.values(Content._overview), cache: 'no-cache' }));
|
||||
|
||||
for(const [id, value] of Object.entries(Content._overview).filter(e => !blocked.includes(e[0])))
|
||||
{
|
||||
if(value.type === 'folder')
|
||||
continue;
|
||||
|
||||
Content.queue.queue(() => Content.read(id).then(e => {
|
||||
if(e) Content.queue.queue(() => useRequestFetch()(`/api/file/content/${id}`, { method: 'POST', body: e, cache: 'no-cache' }));
|
||||
}));
|
||||
}
|
||||
|
||||
return Content.queue.promise;
|
||||
}
|
||||
//Maybe store the file handles ? Is it safe to keep them ?
|
||||
private static async read(path: string, options?: FileSystemGetFileOptions): Promise<string | undefined>
|
||||
|
|
@ -283,24 +301,18 @@ export class Content
|
|||
}
|
||||
console.timeEnd(`Writing ${size} bytes to '${path}'`);
|
||||
}
|
||||
private static async remove(path: string): Promise<void>
|
||||
{
|
||||
console.time(`Removing '${path}'`);
|
||||
await Content.root.removeEntry(path)
|
||||
console.timeEnd(`Removing '${path}'`);
|
||||
}
|
||||
|
||||
static get estimate(): Promise<StorageEstimate>
|
||||
{
|
||||
return Content._ready ? navigator.storage.estimate() : Promise.reject();
|
||||
}
|
||||
|
||||
static toString<T extends FileType>(content: ExploreContent<T>): string
|
||||
static toString<T extends FileType>(content: ProjectContent<T>): string
|
||||
{
|
||||
return handlers[content.type].toString(content.content);
|
||||
}
|
||||
|
||||
static fromString<T extends FileType>(overview: Omit<ExploreContent<T>, 'content'>, content: string): ContentMap[T]
|
||||
static fromString<T extends FileType>(overview: Omit<ProjectContent<T>, 'content'>, content: string): ContentMap[T]
|
||||
{
|
||||
return handlers[overview.type].fromString(content);
|
||||
}
|
||||
|
|
@ -362,7 +374,7 @@ export class Content
|
|||
|
||||
for(const element of Object.values(Content._overview))
|
||||
{
|
||||
addChild(arr, element);
|
||||
addChild(arr, {...element});
|
||||
}
|
||||
|
||||
return arr;
|
||||
|
|
@ -411,9 +423,9 @@ const handlers: { [K in FileType]: ContentTypeHandler<K> } = {
|
|||
else
|
||||
{
|
||||
element = loading("large");
|
||||
Content.content(content.path).then(e => {
|
||||
Content.get(content.id, true).then(e => {
|
||||
if(!e)
|
||||
return element.parentElement?.replaceChild(dom('div', { class: '', text: '' }), element);
|
||||
return element.parentElement?.replaceChild(dom('div', { class: '', text: 'Une erreur est survenue.' }), element);
|
||||
|
||||
MarkdownEditor.singleton.content = content.content = (e as typeof content).content;
|
||||
element.parentElement?.replaceChild(MarkdownEditor.singleton.dom, element);
|
||||
|
|
@ -452,7 +464,7 @@ export const iconByType: Record<FileType, string> = {
|
|||
'file': 'radix-icons:file',
|
||||
'markdown': 'radix-icons:file-text',
|
||||
'map': 'lucide:map',
|
||||
}
|
||||
};
|
||||
|
||||
export class Editor
|
||||
{
|
||||
|
|
@ -472,7 +484,7 @@ export class Editor
|
|||
this.history.register('overview', {
|
||||
move: {
|
||||
undo: (action) => {
|
||||
this.tree.tree.remove(getPath(action.element));
|
||||
this.tree.tree.remove(action.element.id);
|
||||
action.element.parent = action.from.parent;
|
||||
action.element.order = action.from.order;
|
||||
const path = getPath(action.element), depth = path.split("/").filter(e => !!e).length;
|
||||
|
|
@ -483,11 +495,10 @@ export class Editor
|
|||
this.dragndrop(action.element, depth, action.element.parent);
|
||||
this.tree.update();
|
||||
|
||||
Content.rename(action.element.path, path);
|
||||
action.element.path = path;
|
||||
},
|
||||
redo: (action) => {
|
||||
this.tree.tree.remove(getPath(action.element));
|
||||
this.tree.tree.remove(action.element.id);
|
||||
action.element.parent = action.to.parent;
|
||||
action.element.order = action.to.order;
|
||||
const path = getPath(action.element), depth = path.split("/").filter(e => !!e).length;
|
||||
|
|
@ -499,16 +510,16 @@ export class Editor
|
|||
this.dragndrop(action.element, depth, action.element.parent);
|
||||
this.tree.update();
|
||||
|
||||
Content.rename(action.element.path, path);
|
||||
action.element.path = path;
|
||||
},
|
||||
},
|
||||
add: {
|
||||
undo: (action) => {
|
||||
this.tree.tree.remove(getPath(action.element));
|
||||
this.tree.tree.remove(action.element.id);
|
||||
if(this.selected === action.element) this.select();
|
||||
action.element.cleanup();
|
||||
action.element.remove();
|
||||
action.element.element?.remove();
|
||||
|
||||
},
|
||||
redo: (action) => {
|
||||
if(!action.element)
|
||||
|
|
@ -527,10 +538,10 @@ export class Editor
|
|||
this.tree.update();
|
||||
},
|
||||
redo: (action) => {
|
||||
this.tree.tree.remove(getPath(action.element));
|
||||
this.tree.tree.remove(action.element.id);
|
||||
if(this.selected === action.element) this.select();
|
||||
action.element.cleanup();
|
||||
action.element.remove();
|
||||
action.element.element?.remove();
|
||||
},
|
||||
},
|
||||
rename: {
|
||||
|
|
@ -542,7 +553,6 @@ export class Editor
|
|||
const path = getPath(action.element), depth = path.split("/").filter(e => !!e).length;
|
||||
action.element?.cleanup();
|
||||
this.dragndrop(action.element, depth, action.element.parent);
|
||||
Content.rename(action.element.path, path);
|
||||
action.element.path = path;
|
||||
},
|
||||
redo: (action) => {
|
||||
|
|
@ -553,7 +563,6 @@ export class Editor
|
|||
const path = getPath(action.element), depth = path.split("/").filter(e => !!e).length;
|
||||
action.element?.cleanup();
|
||||
this.dragndrop(action.element, depth, action.element.parent);
|
||||
Content.rename(action.element.path, path);
|
||||
action.element.path = path;
|
||||
},
|
||||
},
|
||||
|
|
@ -577,7 +586,7 @@ export class Editor
|
|||
action.element.element!.children[0].children[3].children[0].replaceWith(icon(action.element.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !action.element.private }] }));
|
||||
},
|
||||
},
|
||||
}, action => Content.update(action.element));
|
||||
}, () => { this.tree.tree.each(e => Content.set(e.id, e)); Content.save(); });
|
||||
|
||||
this.tree = new TreeDOM((item, depth) => {
|
||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent)} }, [
|
||||
|
|
@ -601,20 +610,20 @@ export class Editor
|
|||
|
||||
this.container = dom('div', { class: 'flex flex-1 flex-col items-start justify-start max-h-full relative' }, [dom('div', { class: 'py-4 flex-1 w-full max-h-full flex overflow-auto xl:px-12 lg:px-8 px-6 relative' })]);
|
||||
}
|
||||
private contextmenu(e: MouseEvent, item: LocalContent)
|
||||
private contextmenu(e: MouseEvent, item: Recursive<LocalContent>)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
const close = contextmenu(e.clientX, e.clientY, { placement: 'right-start', offset: 8, content: [
|
||||
const close = contextmenu(e.clientX, e.clientY, [
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-100 dark:text-dark-100', listeners: { click: (e) => { this.add("markdown", item); close() }} }, [icon('radix-icons:plus'), text('Ajouter')]),
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-100 dark:text-dark-100', listeners: { click: (e) => { this.rename(item); close() }} }, [icon('radix-icons:input'), text('Renommer')]),
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-red dark:text-dark-red', listeners: { click: (e) => { this.remove(item); close() }} }, [icon('radix-icons:trash'), text('Supprimer')]),
|
||||
] });
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-red dark:text-dark-red', listeners: { click: (e) => { close(); confirm(`Confirmer la suppression de ${item.title}${item.children ? ' et de ses enfants' : ''} ?`).then(e => { if(e) this.remove(item)}) }} }, [icon('radix-icons:trash'), text('Supprimer')]),
|
||||
], { placement: 'right-start', offset: 8 });
|
||||
}
|
||||
private add(type: FileType, nextTo: Recursive<LocalContent>)
|
||||
{
|
||||
const count = Object.values(Content.files).filter(e => e.title.match(/^Nouveau( \(\d+\))?$/)).length;
|
||||
const item: Recursive<Omit<LocalContent, 'path' | 'content'> & { element?: HTMLElement }> = { navigable: true, private: false, owner: 0, order: nextTo.order + 1, timestamp: new Date(), title: count === 0 ? 'Nouveau' : `Nouveau (${count})`, type: type, visit: 0, parent: nextTo.parent };
|
||||
const item: Recursive<Omit<LocalContent, 'path' | 'content'> & { element?: HTMLElement }> = { id: getID(ID_SIZE), navigable: true, private: false, owner: 0, order: nextTo.order + 1, timestamp: new Date(), title: count === 0 ? 'Nouveau' : `Nouveau (${count})`, type: type, parent: nextTo.parent };
|
||||
this.history.add('overview', 'add', [{ element: item, from: undefined, to: nextTo.order + 1 }]);
|
||||
}
|
||||
private remove(item: LocalContent & { element?: HTMLElement })
|
||||
|
|
@ -670,6 +679,8 @@ export class Editor
|
|||
if (instruction !== null)
|
||||
this.updateTree(instruction, location.initial.dropTargets[0].data.id as string, target.data.id as string);
|
||||
},
|
||||
}), autoScrollForElements({
|
||||
element: this.tree.container,
|
||||
}));
|
||||
}
|
||||
private dragndrop(item: Omit<LocalContent & { element?: HTMLElement, cleanup?: () => void }, "content">, depth: number, parent?: Omit<LocalContent & { element?: HTMLElement }, "content">): CleanupFn
|
||||
|
|
@ -697,7 +708,7 @@ export class Editor
|
|||
dropTargetForElements({
|
||||
element,
|
||||
getData: ({ input }) => {
|
||||
const data = { id: getPath(item) };
|
||||
const data = { id: item.id };
|
||||
|
||||
return attachInstruction(data, {
|
||||
input,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export type Class = string | Array<Class> | Record<string, boolean> | undefined;
|
|||
type Listener<K extends keyof HTMLElementEventMap> = | ((ev: HTMLElementEventMap[K]) => any) | {
|
||||
options?: boolean | AddEventListenerOptions;
|
||||
listener: (ev: HTMLElementEventMap[K]) => any;
|
||||
}
|
||||
} | undefined;
|
||||
|
||||
export interface NodeProperties
|
||||
{
|
||||
|
|
@ -43,7 +43,7 @@ export function dom<K extends keyof HTMLElementTagNameMap>(tag: K, properties?:
|
|||
const key = k as keyof HTMLElementEventMap, value = v as Listener<typeof key>;
|
||||
if(typeof value === 'function')
|
||||
element.addEventListener(key, value);
|
||||
else
|
||||
else if(value)
|
||||
element.addEventListener(key, value.listener, value.options);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { lintKeymap } from '@codemirror/lint';
|
|||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||
import { IterMode, Tree } from '@lezer/common';
|
||||
import { tags } from '@lezer/highlight';
|
||||
import { dom } from './dom.util';
|
||||
const External = Annotation.define<boolean>();
|
||||
const Hidden = Decoration.mark({ class: 'hidden' });
|
||||
const Bullet = Decoration.mark({ class: '*:hidden before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4' });
|
||||
|
|
@ -199,4 +200,52 @@ export class MarkdownEditor
|
|||
MarkdownEditor._singleton = new MarkdownEditor();
|
||||
return MarkdownEditor._singleton;
|
||||
}
|
||||
}
|
||||
|
||||
export class FramedEditor
|
||||
{
|
||||
editor: MarkdownEditor;
|
||||
dom: HTMLIFrameElement;
|
||||
|
||||
private static _singleton: FramedEditor;
|
||||
static get singleton()
|
||||
{
|
||||
if(!FramedEditor._singleton)
|
||||
FramedEditor._singleton = new FramedEditor();
|
||||
|
||||
return FramedEditor._singleton;
|
||||
}
|
||||
private constructor()
|
||||
{
|
||||
this.editor = MarkdownEditor.singleton;
|
||||
this.dom = dom('iframe');
|
||||
this.dom.addEventListener('load', () => {
|
||||
if(!this.dom.contentDocument)
|
||||
return;
|
||||
|
||||
this.dom.contentDocument.documentElement.setAttribute('class', document.documentElement.getAttribute('class') ?? '');
|
||||
this.dom.contentDocument.documentElement.setAttribute('style', document.documentElement.getAttribute('style') ?? '');
|
||||
|
||||
const base = this.dom.contentDocument.head.appendChild(this.dom.contentDocument.createElement('base'));
|
||||
base.setAttribute('href', window.location.href);
|
||||
|
||||
for(let element of document.getElementsByTagName('link'))
|
||||
{
|
||||
if(element.getAttribute('rel') === 'stylesheet')
|
||||
this.dom.contentDocument.head.appendChild(element.cloneNode(true));
|
||||
}
|
||||
|
||||
for(let element of document.getElementsByTagName('style'))
|
||||
{
|
||||
this.dom.contentDocument.head.appendChild(element.cloneNode(true));
|
||||
}
|
||||
|
||||
this.dom.contentDocument.body.setAttribute('class', document.body.getAttribute('class') ?? '');
|
||||
this.dom.contentDocument.body.setAttribute('style', document.body.getAttribute('style') ?? '');
|
||||
|
||||
this.dom.contentDocument.body.appendChild(this.editor.dom);
|
||||
|
||||
this.editor.focus();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,33 @@
|
|||
import * as FloatingUI from "@floating-ui/dom";
|
||||
import { cancelPropagation, dom, svg, type Class, type NodeChildren } from "./dom.util";
|
||||
import { cancelPropagation, dom, svg, text, type Class, type NodeChildren } from "./dom.util";
|
||||
import { button } from "./proses";
|
||||
|
||||
export interface CommonProperties
|
||||
export interface ContextProperties
|
||||
{
|
||||
placement?: FloatingUI.Placement;
|
||||
offset?: number;
|
||||
arrow?: boolean;
|
||||
class?: Class;
|
||||
}
|
||||
export interface PopperProperties
|
||||
{
|
||||
placement?: FloatingUI.Placement;
|
||||
offset?: number;
|
||||
arrow?: boolean;
|
||||
class?: Class;
|
||||
content?: NodeChildren;
|
||||
}
|
||||
export interface PopperProperties extends CommonProperties
|
||||
{
|
||||
delay?: number;
|
||||
|
||||
onShow?: (element: HTMLDivElement) => boolean | void;
|
||||
onHide?: (element: HTMLDivElement) => boolean | void;
|
||||
}
|
||||
|
||||
export interface ModalProperties
|
||||
{
|
||||
priority?: boolean;
|
||||
closeWhenOutside?: boolean;
|
||||
}
|
||||
|
||||
let teleport: HTMLDivElement;
|
||||
export function init()
|
||||
{
|
||||
|
|
@ -141,7 +152,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
|
|||
|
||||
return container;
|
||||
}
|
||||
export function contextmenu(x: number, y: number, properties?: CommonProperties): () => void
|
||||
export function contextmenu(x: number, y: number, content: NodeChildren, properties?: ContextProperties): () => void
|
||||
{
|
||||
const virtual = {
|
||||
getBoundingClientRect() {
|
||||
|
|
@ -159,7 +170,7 @@ export function contextmenu(x: number, y: number, properties?: CommonProperties)
|
|||
};
|
||||
|
||||
const arrow = svg('svg', { class: 'absolute fill-light-35 dark:fill-dark-35', attributes: { width: "10", height: "7", viewBox: "0 0 30 10" } }, [svg('polygon', { attributes: { points: "0,0 30,0 15,10" } })]);
|
||||
const container = dom('div', { class: ['fixed bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 z-50', properties?.class] }, [...(properties?.content ?? [])]);
|
||||
const container = dom('div', { class: ['fixed bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 z-50', properties?.class] }, content);
|
||||
|
||||
function update()
|
||||
{
|
||||
|
|
@ -236,7 +247,26 @@ export function contextmenu(x: number, y: number, properties?: CommonProperties)
|
|||
|
||||
return close;
|
||||
}
|
||||
//TODO
|
||||
export function modal()
|
||||
|
||||
export function modal(content: NodeChildren, properties?: ModalProperties)
|
||||
{
|
||||
const _modalBlocker = dom('div', { class: [' absolute top-0 left-0 bottom-0 right-0 z-0', { 'bg-light-0 dark:bg-dark-0 opacity-70': properties?.priority ?? false }], listeners: { click: properties?.closeWhenOutside ? (() => _modal.remove()) : undefined } });
|
||||
const _closer = properties?.priority ? undefined : dom('span', { class: 'absolute top-4 right-4', text: '×', listeners: { click: () => _modal.remove() } });
|
||||
const _modal = dom('div', { class: 'fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40' }, [ _modalBlocker, dom('div', { class: 'max-h-[85vh] max-w-[450px] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 text-light-100 dark:text-dark-100 z-10 relative' }, content)])
|
||||
|
||||
teleport.appendChild(_modal);
|
||||
|
||||
return {
|
||||
close: () => _modal.remove(),
|
||||
}
|
||||
}
|
||||
|
||||
export function confirm(title: string): Promise<boolean>
|
||||
{
|
||||
return new Promise(res => {
|
||||
const mod = modal([ dom('div', { class: 'flex flex-col justify-start gap-4' }, [ dom('div', { class: 'text-xl' }, [ text(title) ]), dom('div', { class: 'flex flex-row gap-2' }, [ button(text("Non"), () => (mod.close(), res(false)), 'h-[35px] px-[15px]'), button(text("Oui"), () => (mod.close(), res(true)), 'h-[35px] px-[15px] !border-light-red dark:!border-dark-red text-light:red dark:text-dark-red') ]) ]) ], {
|
||||
priority: true,
|
||||
closeWhenOutside: false,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
|
@ -1,7 +1,15 @@
|
|||
export const ID_SIZE = 32;
|
||||
|
||||
export function unifySlug(slug: string | string[]): string
|
||||
{
|
||||
return (Array.isArray(slug) ? slug.join('/') : slug);
|
||||
}
|
||||
export function getID(length: number)
|
||||
{
|
||||
for (var id = [], i = 0; i < length; i++)
|
||||
id.push((16 * Math.random() | 0).toString(16));
|
||||
return id.join("");
|
||||
}
|
||||
export function parsePath(path: string): string
|
||||
{
|
||||
return path.toLowerCase().replaceAll(" ", "-").normalize("NFD").replaceAll(/[\u0300-\u036f]/g, "").replaceAll('(', '').replaceAll(')', '');
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ interface HistoryAction
|
|||
|
||||
export class History
|
||||
{
|
||||
private handlers: Record<string, { handlers: Record<string, HistoryHandler>, any?: (action: HistoryAction) => void }>;
|
||||
private handlers: Record<string, { handlers: Record<string, HistoryHandler>, any?: (action: HistoryAction) => any }>;
|
||||
private history: HistoryEvent[];
|
||||
private position: number;
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ export class History
|
|||
this.handlers[source] && this.handlers[source].any && this.handlers[source].any(e);
|
||||
});
|
||||
}
|
||||
register(source: string, handlers: Record<string, HistoryHandler>, any?: (action: HistoryAction) => void)
|
||||
register(source: string, handlers: Record<string, HistoryHandler>, any?: (action: HistoryAction) => any)
|
||||
{
|
||||
this.handlers[source] = { handlers, any };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,4 +252,10 @@ export function link(properties?: NodeProperties & { active?: Class }, link?: Ro
|
|||
export function loading(size: 'small' | 'normal' | 'large' = 'normal'): HTMLElement
|
||||
{
|
||||
return dom('span', { class: ["after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin", {'w-6 h-6 border-4 border-transparent after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}] })
|
||||
}
|
||||
export function button(content: Node, onClick?: () => void, cls?: Class)
|
||||
{
|
||||
return dom('button', { class: [`text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
|
||||
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50`, cls], listeners: { click: onClick } }, [ content ]);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Content, getPath, type LocalContent } from "./content.util";
|
||||
import { Content, type LocalContent } from "./content.util";
|
||||
import { dom } from "./dom.util";
|
||||
import { clamp } from "./general.util";
|
||||
|
||||
|
|
@ -17,9 +17,9 @@ export class Tree<T extends Omit<LocalContent, 'content'>>
|
|||
this._flatten = this.accumulate(e => e);
|
||||
}
|
||||
|
||||
remove(path: string)
|
||||
remove(id: string)
|
||||
{
|
||||
const recursive = (data?: Recursive<T>[], parent?: T) => data?.filter(e => getPath(e) !== path)?.map((e, i) => {
|
||||
const recursive = (data?: Recursive<T>[], parent?: T) => data?.filter(e => e.id !== id)?.map((e, i) => {
|
||||
e.order = i;
|
||||
e.children = recursive(e.children as T[], e);
|
||||
return e;
|
||||
|
|
@ -30,9 +30,9 @@ export class Tree<T extends Omit<LocalContent, 'content'>>
|
|||
}
|
||||
insertAt(item: Recursive<T>, pos: number)
|
||||
{
|
||||
const parent = item.parent ? getPath(item.parent) : undefined;
|
||||
const parent = item.parent ? item.parent.id : undefined;
|
||||
const recursive = (data?: Recursive<T>[]) => data?.flatMap(e => {
|
||||
if(getPath(e) === parent)
|
||||
if(e.id === parent)
|
||||
{
|
||||
e.children = e.children ?? [];
|
||||
e.children.splice(clamp(pos, 0, e.children.length), 0, item);
|
||||
|
|
@ -53,7 +53,7 @@ export class Tree<T extends Omit<LocalContent, 'content'>>
|
|||
|
||||
this._flatten = this.accumulate(e => e);
|
||||
}
|
||||
find(path: string): T | undefined
|
||||
find(id: string): T | undefined
|
||||
{
|
||||
const recursive = (data?: Recursive<T>[]): T | undefined => {
|
||||
if(!data)
|
||||
|
|
@ -61,7 +61,7 @@ export class Tree<T extends Omit<LocalContent, 'content'>>
|
|||
|
||||
for(const e of data)
|
||||
{
|
||||
if(getPath(e) === path)
|
||||
if(e.id === id)
|
||||
return e;
|
||||
|
||||
const result = recursive(e.children as T[]);
|
||||
|
|
@ -94,9 +94,9 @@ export class Tree<T extends Omit<LocalContent, 'content'>>
|
|||
|
||||
return recursive(this._data);
|
||||
}
|
||||
subset(path: string): Tree<T> | undefined
|
||||
subset(id: string): Tree<T> | undefined
|
||||
{
|
||||
const subset = this.find(path);
|
||||
const subset = this.find(id);
|
||||
return subset ? new Tree([subset]) : undefined;
|
||||
}
|
||||
each(callback: (item: T, depth: number, parent?: T) => void)
|
||||
|
|
|
|||
|
|
@ -31,9 +31,8 @@ export interface UserRawData {
|
|||
}
|
||||
|
||||
export interface UserExtendedData {
|
||||
signin: string;
|
||||
lastTimestamp: string;
|
||||
logCount: number;
|
||||
signin: Date;
|
||||
lastTimestamp: Date;
|
||||
}
|
||||
|
||||
export type Permissions = { permissions: string[] };
|
||||
|
|
|
|||
Loading…
Reference in New Issue