From adfcc821250cdd4de7aa4c82034da551858e634c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Mog=C3=A8re?= Date: Mon, 1 Jul 2024 20:41:48 +0200 Subject: [PATCH] feat(node): enable node swapping --- README.md | 20 +++--- app/components/TreeNode.vue | 21 ++++++ app/composables/store.ts | 137 ++++++++++++++++++++++-------------- app/schema.json | 3 +- app/validator.js | 2 +- public/configs/blank.json | 2 +- 6 files changed, 122 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index acd82d6..11a9724 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,17 @@ ## ✨ Features -- **Migration Tracking**: Monitor the migration progress of your Information Systems or applications. -- **Task Management**: Track tasks, subtasks, and their statuses. -- **Bug Tracking**: Manage bug reports, assign them to developers, and monitor their resolution. -- **Production Management**: Visualize production stages, quality control statuses, and inventory levels. -- **Sales and Marketing**: Follow marketing campaigns, sales opportunities, and contracts. -- **HR Management**: Track recruitment processes, employee training programs, and more. -- **Customizable Statuses**: Define and customize statuses to fit your specific workflow. -- **Import/Export**: Import project data from JSON files and export current project data for backup or sharing. -- **And more...** +- **Migration Tracking**: Effortlessly monitor the progress of migrations and transitions to ensure smooth operations. +- **Task Management**: Efficiently track tasks, subtasks, and their statuses to keep your projects organized and on schedule. +- **Customizable Statuses**: Define and customize statuses to fit your specific workflow, providing flexibility for various project needs. +- **Import/Export**: Easily import project data from JSON files and export current project data for backup or sharing. +- **Responsive Design**: Enjoy a seamless experience across desktops, tablets, and mobile devices with a fully responsive interface. +- **Editing and Customization**: Edit node labels, add new nodes, and delete or move existing nodes to tailor your project structure. +- **Drag and Drop**: Intuitively drag and drop nodes within the same level to reorder tasks and sections. +- **Visual Indicators**: Use visual cues to represent statuses, progress, and other key project metrics. +- **Real-Time Updates**: Receive instant updates as changes are made, ensuring all team members are on the same page. +- **User-Friendly Interface**: Navigate through a clean, intuitive interface designed for ease of use and productivity. +- **And More**: Discover additional features designed to optimize your project management experience. ## 🛠️ Installation diff --git a/app/components/TreeNode.vue b/app/components/TreeNode.vue index e555d3d..5d8b171 100644 --- a/app/components/TreeNode.vue +++ b/app/components/TreeNode.vue @@ -152,6 +152,23 @@ const deleteNode = (event: MouseEvent) => { store.deleteSection(props.node.key) } +const handleDragStart = (event: DragEvent) => { + event.stopPropagation() + event.dataTransfer?.setData('text/plain', props.node.key) +} + +const handleDrop = (event: DragEvent) => { + event.stopPropagation() + const draggedKey = event.dataTransfer?.getData('text/plain') + if (draggedKey && draggedKey !== props.node.key) { + store.swapSections(props.node.key, draggedKey) + } +} + +const handleDragOver = (event: DragEvent) => { + event.preventDefault() +} + watchEffect(() => { nodeStatus.value = props.node.status || store.statuses[0]?.name || '' checkIfSuccessNode(props.node) @@ -225,7 +242,11 @@ const applySuccessAnimation = (node: Section) => { :class="{ 'success-animation': isSuccessNode }" :style="nodeStyle" :data-node-key="node.key" + draggable="true" @click="handleClick" + @dragstart="handleDragStart" + @drop="handleDrop" + @dragover="handleDragOver" >
{ - const existingProject = this.findProjectByKey(node.key, existingProjects) + const existingProject = this.findSectionByKey(node.key, existingProjects) if (!node.status) { if (existingProject) { @@ -135,7 +135,7 @@ export const useStore = defineStore('store', { })) }, updateSectionStatus(key: string, status: string) { - const section = this.findProjectByKey(key, this.sections) + const section = this.findSectionByKey(key, this.sections) if (section) { section.status = status @@ -143,52 +143,77 @@ export const useStore = defineStore('store', { } }, updateSectionCollapse(key: string) { - const section = this.findProjectByKey(key, this.sections) + const section = this.findSectionByKey(key, this.sections) if (section) { section.isCollapsed = !section.isCollapsed } }, - addSection(parentKey: string, newSection: Section) { - const parent = this.findProjectByKey(parentKey, this.sections) - if (parent) { - if (!parent.children) { - parent.children = [] - } - parent.children.push(newSection) + updateSectionKey(key: string, newKey: string) { + const section = this.findSectionByKey(key, this.sections) + if (section) { + section.key = newKey } }, - addSiblingSection(parentKey: string, newSection: Section) { - const parent = this.findParentByKey(parentKey, this.sections) - - if (parent) { - parent.children.push(newSection) - - return + updateSectionName(key: string, newName: string) { + const section = this.findSectionByKey(key, this.sections) + if (section) { + section.name = newName + } + }, + addSection(key: string, newSection: Section) { + const parentSection = this.findSectionByKey(key, this.sections) + if (parentSection) { + parentSection.children.push(newSection) + } + }, + addSiblingSection(key: string, newSection: Section) { + const parentSection = this.findParentSectionByKey(key, this.sections) + if (parentSection) { + parentSection.children.push(newSection) + } + else { + this.sections.push(newSection) } - - this.sections.push(newSection) }, deleteSection(key: string) { - const deleteRecursively = (sections: Section[], key: string): Section[] => { - return sections - .filter(section => section.key !== key) - .map(section => ({ - ...section, - children: section.children ? deleteRecursively(section.children, key) : [], - })) + const parentSection = this.findParentSectionByKey(key, this.sections) + if (parentSection) { + parentSection.children = parentSection.children.filter(child => child.key !== key) + } + else { + this.sections = this.sections.filter(section => section.key !== key) } + }, + swapSections(key1: string, key2: string) { + const parentSection1 = this.findParentSectionByKey(key1, this.sections) + const parentSection2 = this.findParentSectionByKey(key2, this.sections) + + if (parentSection1 && parentSection2) { + const index1 = parentSection1.children.findIndex(child => child.key === key1) + const index2 = parentSection2.children.findIndex(child => child.key === key2) + + if (index1 !== -1 && index2 !== -1) { + [parentSection1.children[index1], parentSection2.children[index2]] = [parentSection2.children[index2], parentSection1.children[index1]] + } + } + else { + const index1 = this.sections.findIndex(section => section.key === key1) + const index2 = this.sections.findIndex(section => section.key === key2) - this.sections = deleteRecursively(this.sections, key) + if (index1 !== -1 && index2 !== -1) { + [this.sections[index1], this.sections[index2]] = [this.sections[index2], this.sections[index1]] + } + } }, - findProjectByKey(key: string, sections: Section[]): Section | undefined { + findSectionByKey(key: string, sections: Section[]): Section | undefined { for (const node of sections) { if (node.key === key) { return node } if (node.children) { - const found = this.findProjectByKey(key, node.children) + const found = this.findSectionByKey(key, node.children) if (found) { return found @@ -198,18 +223,16 @@ export const useStore = defineStore('store', { return undefined }, - findParentByKey(key: string, sections: Section[]): Section | undefined { + findParentSectionByKey(key: string, sections: Section[]): Section | undefined { for (const node of sections) { - if (node.children) { - for (const child of node.children) { - if (child.key === key) { - return node - } - } - const parent = this.findParentByKey(key, node.children) + if (node.children && node.children.some(child => child.key === key)) { + return node + } - if (parent) { - return parent + if (node.children) { + const found = this.findParentSectionByKey(key, node.children) + if (found) { + return found } } } @@ -254,18 +277,6 @@ export const useStore = defineStore('store', { removeStatus(index: number) { this.statuses.splice(index, 1) }, - updateSectionLabel(key: string, newLabel: string) { - const section = this.findProjectByKey(key, this.sections) - if (section) { - if (this.displayLabel === 'key') { - section.key = newLabel - - return - } - - section.name = newLabel - } - }, toggleEditingMode() { this.isEditingMode = !this.isEditingMode }, @@ -275,4 +286,28 @@ export const useStore = defineStore('store', { this.configLoaded = false }, }, + getters: { + duplicateProjects(state): { sections: string[], keys: string[] } { + const sections: string[] = [] + const keys: string[] = [] + + const checkDuplicates = (nodes: Section[]) => { + nodes.forEach((node) => { + sections.push(node.name) + keys.push(node.key ?? '') + + if (node.children) { + checkDuplicates(node.children) + } + }) + } + + checkDuplicates(state.sections) + + return { + sections: sections.filter((item, index) => sections.indexOf(item) !== index), + keys: keys.filter((item, index) => keys.indexOf(item) !== index), + } + }, + }, }) diff --git a/app/schema.json b/app/schema.json index 4c4cf99..fed80b3 100644 --- a/app/schema.json +++ b/app/schema.json @@ -14,7 +14,8 @@ "key": { "type": "string" }, "name": { "type": "string" }, "status": { "type": "string", "nullable": true }, - "children": { "$ref": "#" } + "children": { "$ref": "#" }, + "isCollapsed": { "type": "boolean", "nullable": true } }, "required": ["key", "name"], "additionalProperties": false diff --git a/app/validator.js b/app/validator.js index ad92813..bf3b6b0 100644 --- a/app/validator.js +++ b/app/validator.js @@ -1 +1 @@ -"use strict";export const validate = validate10;export default validate10;const schema11 = {"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","nullable":true},"children":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","nullable":true},"children":{"$ref":"#"}},"required":["key","name"],"additionalProperties":false},"nullable":true},"isCollapsed":{"type":"boolean","nullable":true}},"required":["key","name"],"additionalProperties":false}};const wrapper0 = {validate: validate10};function validate10(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;if(errors === 0){if(Array.isArray(data)){var valid0 = true;const len0 = data.length;for(let i0=0; i0