diff --git a/.gitignore b/.gitignore index 02c3910a..4c317921 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ bun.lock AGENTS.md AI_GUIDE.md CLAUDE.md +.coverage/ \ No newline at end of file diff --git a/src/components/NavMenu.vue b/src/components/NavMenu.vue deleted file mode 100644 index b0d98926..00000000 --- a/src/components/NavMenu.vue +++ /dev/null @@ -1,833 +0,0 @@ - - - - - diff --git a/src/components/dialogs/CustomNavDialog.vue b/src/components/dialogs/CustomNavDialog.vue index 66b6e947..ea88232f 100644 --- a/src/components/dialogs/CustomNavDialog.vue +++ b/src/components/dialogs/CustomNavDialog.vue @@ -30,6 +30,8 @@ :drag-state="dragState" @edit-folder="openFolderEditor" @delete-folder="handleDeleteFolder" + @edit-dashboard="openDashboardEditor" + @delete-dashboard="handleDeleteDashboard" @hide="handleHideItem" @toggle="handleTreeToggle(item)" /> @@ -73,6 +75,9 @@ + @@ -93,7 +98,13 @@ - {{ t('nav_menu.custom_nav.edit_folder') }} + + {{ + folderEditor.editorType === 'dashboard' + ? t('nav_menu.custom_nav.edit_dashboard') + : t('nav_menu.custom_nav.edit_folder') + }} +
[] + }, + definitions: { + type: Array, + default: () => [] } }); - const emit = defineEmits(['update:visible', 'save']); + const emit = defineEmits(['update:visible', 'save', 'dashboard-created']); const { t } = useI18n(); + const dashboardStore = useDashboardStore(); + const modalStore = useModalStore(); const cloneLayout = (source) => { if (!Array.isArray(source)) return []; @@ -210,6 +229,7 @@ visible: false, isEditing: false, editingId: null, + editorType: 'folder', data: { id: '', name: '', icon: '' } }); @@ -243,49 +263,53 @@ const definitionsMap = computed(() => { const map = new Map(); - navDefinitions.forEach((def) => { + const source = props.definitions?.length ? props.definitions : navDefinitions; + source.forEach((def) => { if (def?.key) map.set(def.key, def); }); return map; }); const treeItems = computed(() => { - return localLayout.value.map((entry) => { - if (entry.type === 'folder') { - const children = (entry.items || []) - .map((key) => { - const def = definitionsMap.value.get(key); - if (!def) return null; - return { id: key, type: 'item', key, level: 1, parentId: entry.id }; - }) - .filter(Boolean); + return localLayout.value + .map((entry) => { + if (entry.type === 'folder') { + const children = (entry.items || []) + .map((key) => { + const def = definitionsMap.value.get(key); + if (!def) return null; + return { id: key, type: 'item', key, level: 1, parentId: entry.id }; + }) + .filter(Boolean); - const folderChildren = children.length - ? children - : [{ id: `${entry.id}__placeholder`, _placeholder: true, level: 1 }]; + const folderChildren = children.length + ? children + : [{ id: `${entry.id}__placeholder`, _placeholder: true, level: 1 }]; - return { - id: entry.id, - type: 'folder', - name: entry.name, - icon: entry.icon, - level: 0, - children: folderChildren - }; - } - return { id: entry.key, type: 'item', key: entry.key, level: 0 }; - }); + return { + id: entry.id, + type: 'folder', + name: entry.name, + icon: entry.icon, + level: 0, + children: folderChildren + }; + } + if (!definitionsMap.value.has(entry.key)) return null; + return { id: entry.key, type: 'item', key: entry.key, level: 0 }; + }) + .filter(Boolean); }); const expandedKeys = ref([]); const hiddenItems = computed(() => - navDefinitions + (props.definitions?.length ? props.definitions : navDefinitions) .filter((def) => hiddenKeySet.value.has(def.key)) .map((def) => ({ key: def.key, icon: def.icon, - label: t(def.labelKey) + label: def.isDashboard ? def.labelKey : t(def.labelKey) })) ); @@ -646,13 +670,31 @@ if (!entry) return; folderEditor.isEditing = true; folderEditor.editingId = folderId; + folderEditor.editorType = 'folder'; folderEditor.data = { id: entry.id, name: entry.name, icon: entry.icon }; folderEditor.visible = true; }; + const openDashboardEditor = (dashboardKey) => { + const dashboardId = String(dashboardKey || '').replace(DASHBOARD_NAV_KEY_PREFIX, ''); + const dashboard = dashboardStore.getDashboard(dashboardId); + if (!dashboard) return; + + folderEditor.isEditing = true; + folderEditor.editingId = dashboardKey; + folderEditor.editorType = 'dashboard'; + folderEditor.data = { + id: dashboardKey, + name: dashboard.name, + icon: dashboard.icon || '' + }; + folderEditor.visible = true; + }; + const handleAddFolder = () => { folderEditor.isEditing = false; folderEditor.editingId = null; + folderEditor.editorType = 'folder'; folderEditor.data = { id: createFolderId(), name: '', @@ -661,10 +703,27 @@ folderEditor.visible = true; }; - const handleFolderEditorSave = () => { + const handleAddDashboard = async () => { + const dashboard = await dashboardStore.createDashboard(t('dashboard.default_name')); + dashboardStore.setEditingDashboardId(dashboard.id); + localLayout.value.push({ + type: 'item', + key: `${DASHBOARD_NAV_KEY_PREFIX}${dashboard.id}` + }); + localLayout.value = [...localLayout.value]; + emit('dashboard-created', dashboard.id, cloneLayout(localLayout.value), [...hiddenKeySet.value]); + }; + + const handleFolderEditorSave = async () => { if (!folderEditor.data.name?.trim()) return; - if (folderEditor.isEditing) { + if (folderEditor.editorType === 'dashboard') { + const dashboardId = String(folderEditor.editingId || '').replace(DASHBOARD_NAV_KEY_PREFIX, ''); + await dashboardStore.updateDashboard(dashboardId, { + name: folderEditor.data.name.trim(), + icon: folderEditor.data.icon?.trim() || DEFAULT_DASHBOARD_ICON + }); + } else if (folderEditor.isEditing) { const entry = localLayout.value.find((e) => e.type === 'folder' && e.id === folderEditor.editingId); if (entry) { entry.name = folderEditor.data.name.trim(); @@ -689,6 +748,43 @@ folderEditor.visible = false; }; + const removeFromLayout = (key) => { + let removed = false; + for (let i = 0; i < localLayout.value.length; i++) { + const entry = localLayout.value[i]; + if (entry.type === 'item' && entry.key === key) { + localLayout.value.splice(i, 1); + removed = true; + break; + } + if (entry.type === 'folder') { + const idx = entry.items?.indexOf(key); + if (idx !== undefined && idx >= 0) { + entry.items.splice(idx, 1); + removed = true; + } + } + } + if (removed) { + hiddenKeySet.value.delete(key); + hiddenPlacement.value.delete(key); + localLayout.value = [...localLayout.value]; + } + }; + + const handleDeleteDashboard = async (dashboardKey) => { + const dashboardId = String(dashboardKey || '').replace(DASHBOARD_NAV_KEY_PREFIX, ''); + const { ok } = await modalStore.confirm({ + title: t('dashboard.confirmations.delete_title'), + description: t('dashboard.confirmations.delete_description') + }); + if (!ok) { + return; + } + await dashboardStore.deleteDashboard(dashboardId); + removeFromLayout(dashboardKey); + }; + const handleSave = () => { const cleanedLayout = localLayout.value.filter( (entry) => !(entry.type === 'folder' && (!entry.items || entry.items.length === 0)) diff --git a/src/components/dialogs/SortableTreeNode.vue b/src/components/dialogs/SortableTreeNode.vue index 7f22ba4d..1aae50af 100644 --- a/src/components/dialogs/SortableTreeNode.vue +++ b/src/components/dialogs/SortableTreeNode.vue @@ -19,7 +19,7 @@ dragState: { type: Object, default: () => ({}) } }); - const emit = defineEmits(['editFolder', 'deleteFolder', 'hide', 'toggle']); + const emit = defineEmits(['editFolder', 'deleteFolder', 'editDashboard', 'deleteDashboard', 'hide', 'toggle']); const { t } = useI18n(); @@ -27,6 +27,9 @@ const nodeValue = computed(() => props.item.value); const isFolder = computed(() => nodeValue.value?.type === 'folder'); + const isDashboard = computed(() => { + return !isFolder.value && nodeValue.value?.key?.startsWith('dashboard-'); + }); const hasChildren = computed(() => props.item.hasChildren); const level = computed(() => nodeValue.value?.level ?? 0); const nodeId = computed(() => (isFolder.value ? nodeValue.value?.id : nodeValue.value?.key)); @@ -43,7 +46,10 @@ return nodeValue.value.name?.trim() || t('nav_menu.custom_nav.folder_name_placeholder'); } const def = props.definitionsMap.get(nodeValue.value?.key); - return def ? t(def.labelKey) : nodeValue.value?.key || ''; + if (!def) { + return nodeValue.value?.key || ''; + } + return def.isDashboard ? def.labelKey : t(def.labelKey); }); const displayIcon = computed(() => { @@ -111,6 +117,14 @@ {{ t('nav_menu.custom_nav.delete_folder') }} +