diff --git a/src/lib/server/logging.ts b/src/lib/server/logging.ts index e6d1aba..99b53c5 100644 --- a/src/lib/server/logging.ts +++ b/src/lib/server/logging.ts @@ -2,7 +2,7 @@ import winston from "winston" const { combine, timestamp, json } = winston.format export const logger = winston.createLogger({ - level: "info", + level: "debug", format: combine( timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" diff --git a/src/lib/server/sql.ts b/src/lib/server/sql.ts index bab967f..78add2e 100644 --- a/src/lib/server/sql.ts +++ b/src/lib/server/sql.ts @@ -17,6 +17,7 @@ export type SqlTemplateSchema = { id: number text: string parent_id: number | null + priority: number } export type SqlDsmCodeSchema = { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index f355157..33547aa 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,6 @@ export function shortenText(str: string, maxLength = 200) { if (str.length > maxLength) { - return str.substring(0, maxLength) + "..." + return str.substring(0, maxLength).trim() + "..." } return str } diff --git a/src/routes/api/templates/[id]/+server.ts b/src/routes/api/templates/[id]/+server.ts index d4fb496..e29d923 100644 --- a/src/routes/api/templates/[id]/+server.ts +++ b/src/routes/api/templates/[id]/+server.ts @@ -1,5 +1,5 @@ import { logger } from "$lib/server/logging" -import { pool, type SqlTemplateSchema } from "$lib/server/sql" +import { pool } from "$lib/server/sql" export async function POST({ request, params }) { const body = await request.json() @@ -26,45 +26,29 @@ export async function POST({ request, params }) { }) } -export async function PATCH({ params, request }) { - const id = params.id - let { text, parent_id } = await request.json() - logger.info(`Patching template with id ${id}`) - - const existingtemplate = await pool.connect().then(async client => { - const result = await client.query({ - text: "SELECT * FROM templates WHERE id = $1", - values: [id] - }) - client.release() - return result.rows[0] as SqlTemplateSchema - }) +type PutRequest = { + text: string | null + parentId: number | null + priority: number | null +} - if (!existingtemplate) { - throw new Error(`template with id ${id} not found`) - } - if (existingtemplate) { - text = text ?? existingtemplate.text - parent_id = parent_id ?? existingtemplate.parent_id - } +export async function PUT({ params, request }) { + const id = params.id + const { text, parentId, priority } = (await request.json()) as PutRequest - const query = { - text: "UPDATE templates SET", - values: [] as string[] + if (text === null || parentId === null || priority === null) { + logger.error("Missing parameter in request.") + return new Response(null, { status: 400 }) } - if (text !== undefined) { - query.text += ` text = $${query.values.length + 1},` - query.values.push(text) - } + logger.info(`Patching template with id ${id}`) - if (parent_id !== undefined) { - query.text += ` parent_id = $${query.values.length + 1},` - query.values.push(String(parent_id)) + const query = { + text: "UPDATE templates SET text = $1, parent_id = $2, priority = $3 WHERE id = $4", + values: [text, parentId, priority, id] } - query.text = query.text.slice(0, -1) + ` WHERE id = $${query.values.length + 1}` - query.values.push(String(id)) + logger.debug(query) return await pool .connect() diff --git a/src/routes/templates/DecisionTree.test.ts b/src/routes/templates/DecisionTree.test.ts new file mode 100644 index 0000000..0d4815a --- /dev/null +++ b/src/routes/templates/DecisionTree.test.ts @@ -0,0 +1,126 @@ +import { describe, it, expect } from "vitest" +import { DecisionTree } from "./DecisionTree" +import type { SqlTemplateSchema } from "$lib/server/sql" + +describe("DecisionTree", () => { + const mockData: SqlTemplateSchema[] = [ + { id: 1, text: "Root", parent_id: null, priority: 0 }, + { id: 2, text: "Child 1", parent_id: 1, priority: 0 }, + { id: 3, text: "Child 2", parent_id: 1, priority: 1 }, + { id: 4, text: "Grandchild 1", parent_id: 2, priority: 0 }, + { id: 5, text: "Grandchild 2", parent_id: 2, priority: 1 } + ] + + it("should construct a tree correctly", () => { + const tree = new DecisionTree(mockData) + + expect(tree.id).toBe(1) + expect(tree.text).toBe("Root") + expect(tree.children.length).toBe(2) + expect(tree.children[0].id).toBe(2) + expect(tree.children[1].id).toBe(3) + expect(tree.children[0].children.length).toBe(2) + }) + + it("should get parents correctly", () => { + const tree = new DecisionTree(mockData) + const grandchild = tree.getNodeById(4) + + expect(grandchild).not.toBeNull() + if (grandchild) { + const parents = grandchild.getAncestors() + expect(parents.length).toBe(2) + expect(parents[0].id).toBe(1) + expect(parents[1].id).toBe(2) + } + }) + + it("should get children recursively", () => { + const tree = new DecisionTree(mockData) + const children = tree.getChildrenRecursive() + + expect(children.length).toBe(4) + expect(children.map(c => c.id).sort()).toEqual([2, 3, 4, 5]) + }) + + it("should filter children by ids", () => { + const tree = new DecisionTree(mockData) + const filtered = tree.filterChildrenByIds([3, 4]) + + expect(filtered.length).toBe(2) + expect(filtered.map(c => c.id).sort()).toEqual([3, 4]) + }) + + it("should get path correctly", () => { + const tree = new DecisionTree(mockData) + const grandchild = tree.getNodeById(4) + + expect(grandchild).not.toBeNull() + + if (grandchild) { + const path = grandchild.getPath() + expect(path).toEqual(["Root", "Child 1"]) + } + }) + + it("should get node by id", () => { + const tree = new DecisionTree(mockData) + const node = tree.getNodeById(3) + expect(node).not.toBeNull() + expect(node?.id).toBe(3) + expect(node?.text).toBe("Child 2") + }) + + it("should add child correctly", () => { + const tree = new DecisionTree(mockData) + const newChild = new DecisionTree([{ id: 6, text: "New Child", parent_id: 1, priority: 0 }], 6) + + tree.addChild(newChild, 1) + + expect(tree.children.length).toBe(3) + expect(tree.children[1].id).toBe(6) + expect(tree.children[1].priority).toBe(1) + expect(tree.children[2].priority).toBe(2) + }) + + it("should add child at end for index too large", () => { + const tree = new DecisionTree(mockData) + const newChild = new DecisionTree([{ id: 6, text: "New Child", parent_id: 1, priority: 0 }], 6) + + tree.addChild(newChild, 99) + + expect(tree.children.length).toBe(3) + expect(tree.children[2].id).toBe(6) + }) + + it("should delete child correctly", () => { + const tree = new DecisionTree(mockData) + + tree.deleteChild(2) + + expect(tree.children.length).toBe(1) + expect(tree.children[0].id).toBe(3) + expect(tree.children[0].priority).toBe(0) + }) + + it("should move child correctly", () => { + const tree = new DecisionTree(mockData) + + tree.moveChild(3, 0) + + expect(tree.children[0].id).toBe(3) + expect(tree.children[0].priority).toBe(0) + expect(tree.children[1].id).toBe(2) + }) + + it("should sort children recursively", () => { + const unsortedData: SqlTemplateSchema[] = [ + { id: 1, text: "Root", parent_id: null, priority: 0 }, + { id: 2, text: "Child 1", parent_id: 1, priority: 1 }, + { id: 3, text: "Child 2", parent_id: 1, priority: 0 } + ] + const tree = new DecisionTree(unsortedData) + expect(tree.children[0].id).toBe(3) + expect(tree.children[1].id).toBe(2) + }) +}) diff --git a/src/routes/templates/DecisionTree.ts b/src/routes/templates/DecisionTree.ts index 9bb754b..65c7e60 100644 --- a/src/routes/templates/DecisionTree.ts +++ b/src/routes/templates/DecisionTree.ts @@ -22,10 +22,16 @@ export class DecisionTree { this.parent = parent this.children = table .filter(node => node.parent_id === this.id) + .sort((a, b) => a.priority - b.priority) .map(child => new DecisionTree(table, child.id, this)) + this.recursiveSortChildren() } - getParents(): DecisionTree[] { + get priority(): number { + return this.parent?.children.findIndex(child => child.id === this.id) ?? 0 + } + + getAncestors(): DecisionTree[] { const parents: DecisionTree[] = [] let current: DecisionTree | undefined = this.parent while (current) { @@ -71,14 +77,40 @@ export class DecisionTree { return null } - deleteNodeById(id: number): DecisionTree { - this.children = this.children.filter(child => child.id !== id) - this.children.forEach(child => child.deleteNodeById(id)) + addChild(child: DecisionTree, index: number | undefined = undefined) { + if (index === undefined) { + index = this.children.length + 1 + } + child.parent = this + this.children = [...this.children.slice(0, index), child, ...this.children.slice(index)] + return this + } + + deleteChild(id: number) { + const childIndex = this.children.findIndex(child => child.id === id) + if (childIndex === -1) { + return + } + this.children.splice(childIndex, 1) + return this + } + + moveChild(id: number, newIndex: number) { + if (newIndex >= this.children.length) return + + const currentIndex = this.children.findIndex(child => child.id === id) + if (currentIndex === -1 || currentIndex === newIndex) return + + const child = this.children[currentIndex] + + this.deleteChild(id) + this.addChild(child, newIndex) + return this } recursiveSortChildren() { - this.children = this.children?.sort((a, b) => a.text.localeCompare(b.text)) + this.children = this.children?.sort((a, b) => a.priority - b.priority) this.children?.forEach(child => child.recursiveSortChildren()) } } diff --git a/src/routes/templates/TemplatesDirectory/AdminButtons.svelte b/src/routes/templates/TemplatesDirectory/AdminButtons.svelte deleted file mode 100644 index 9c73084..0000000 --- a/src/routes/templates/TemplatesDirectory/AdminButtons.svelte +++ /dev/null @@ -1,145 +0,0 @@ - - - - {#each adminButtons as adminButton} - - {/each} - diff --git a/src/routes/templates/TemplatesDirectory/CreateButton.svelte b/src/routes/templates/TemplatesDirectory/CreateButton.svelte new file mode 100644 index 0000000..a91b19e --- /dev/null +++ b/src/routes/templates/TemplatesDirectory/CreateButton.svelte @@ -0,0 +1,56 @@ + + + diff --git a/src/routes/templates/TemplatesDirectory/DeleteButton.svelte b/src/routes/templates/TemplatesDirectory/DeleteButton.svelte new file mode 100644 index 0000000..1a56d53 --- /dev/null +++ b/src/routes/templates/TemplatesDirectory/DeleteButton.svelte @@ -0,0 +1,52 @@ + + + diff --git a/src/routes/templates/TemplatesDirectory/EditButton.svelte b/src/routes/templates/TemplatesDirectory/EditButton.svelte new file mode 100644 index 0000000..c554cfe --- /dev/null +++ b/src/routes/templates/TemplatesDirectory/EditButton.svelte @@ -0,0 +1,51 @@ + + + diff --git a/src/routes/templates/TemplatesDirectory/ModalSearchDecisionTree.svelte b/src/routes/templates/TemplatesDirectory/ModalSearchDecisionTree.svelte index 1e4828d..8e40166 100644 --- a/src/routes/templates/TemplatesDirectory/ModalSearchDecisionTree.svelte +++ b/src/routes/templates/TemplatesDirectory/ModalSearchDecisionTree.svelte @@ -2,8 +2,10 @@ import { getModalStore } from "@skeletonlabs/skeleton" import type { DecisionTree } from "../DecisionTree" import Fuse from "fuse.js" - import AdminButtons from "./AdminButtons.svelte" import { openNodeIds } from "./store" + import CreateButton from "./CreateButton.svelte" + import EditButton from "./EditButton.svelte" + import DeleteButton from "./DeleteButton.svelte" const modalStore = getModalStore() @@ -45,7 +47,7 @@ const searchedPaths = searcher.search(searchTerm).map(result => result.item) const searchedNodes = allChildNodes.filter(node => searchedPaths.some(path => path.id === node.id)) const searchedIds = [ - ...searchedNodes.map(node => [...node.getParents(), node]).flat(), + ...searchedNodes.map(node => [...node.getAncestors(), node]).flat(), ...searchedNodes.map(node => node.getChildrenRecursive()).flat() ] .filter((value, index, self) => self.indexOf(value) === index) @@ -62,7 +64,7 @@ } function onSearchClick(node: DecisionTree) { - const parents = node.getParents() + const parents = node.getAncestors() const ids = [...parents.map(parent => parent.id), node.id] openNodeIds.set(new Set(ids)) modalStore.close() @@ -99,7 +101,11 @@
modalStore.close()}> - + + + + {}} /> +
{/if} -
- -
- {#if editable} -
- -
+
+
+ +
+
- {#if !isFolded} -
- {#each node.children as child} - {#key child.id} - - {/key} - {/each} + {#if editable} +
+ + { + updateChildren() + dispatch("update") + }} + /> + {#if !isRoot} + dispatch("update")} /> + dispatch("update")} /> + {/if} +
{/if}
+ +
+ {#if !isFolded} + {#each node.children as child} + + {/each} + {/if} +