diff --git a/app/components/FlowEditor.vue b/app/components/FlowEditor.vue index b76e2d1..325d223 100644 --- a/app/components/FlowEditor.vue +++ b/app/components/FlowEditor.vue @@ -29,6 +29,11 @@ +
@@ -78,7 +83,14 @@
- +
+ + +
@@ -91,7 +103,7 @@
-
+
-
+
-
+
-
+
@@ -126,6 +138,15 @@
+
+ + +
@@ -166,7 +187,7 @@ const auth = useAuth() const authHeaders = computed(() => ({ Authorization: `Bearer ${auth.token.value}` })) const elements = ref([]) const selectedNode = ref(null) -const canvasName = computed(() => props.canvasId) +const canvasName = ref(props.canvasId) const nodeTypes = [ { type: 'text', name: 'Текст', icon: '📝', description: 'Markdown заметка' }, @@ -181,6 +202,7 @@ const nodeTypes = [ const loadData = async () => { const data = await loadCanvas(props.canvasId) elements.value = transformToFlowElements(data) + canvasName.value = data.name || props.canvasId // Mark initial state as saved markAsSaved(data) } @@ -405,6 +427,23 @@ const onNodeDragStop = (event) => { const onNodeDoubleClick = (event) => { const node = event.node + + // 1. Cross-Canvas Navigation Logic + // Check if it's a file node or has a URL/file reference to another canvas + const targetId = node.data?.file || node.data?.url || '' + + if (targetId && (node.type === 'file' || targetId.includes('.canvas') || targetId.startsWith('/canvas/'))) { + // Clean up ID (e.g. "project.canvas" -> "project", "/canvas/project" -> "project") + const cleanId = targetId.replace('.canvas', '').replace('/canvas/', '') + console.log('[Navigation] Opening cross-linked canvas:', cleanId) + navigateTo(`/canvas/${cleanId}`) + return // Stop further processing + } else if (node.type === 'file' && !targetId) { + alert('У этого узла еще нет ссылки на другую карту. Выберите карту в панели свойств (справа).') + return + } + + // 2. Task Toggle Logic (fallback) const currentState = node.data?.state || 'todo' // Quick toggle workflow: todo -> in_progress -> review -> completed -> todo @@ -413,7 +452,7 @@ const onNodeDoubleClick = (event) => { 'in_progress': 'review', 'review': 'completed', 'completed': 'todo', - 'blocked': 'todo' // From blocked back to todo + 'blocked': 'todo' } const nextState = nextStateMap[currentState] || 'todo' @@ -446,6 +485,27 @@ const exportObsidian = () => { URL.revokeObjectURL(url) } +const deleteCanvas = async () => { + if (!confirm('Вы уверены, что хотите полностью удалить эту карту? Это действие необратимо.')) return + + try { + const res = await fetch(`/api/canvas/${props.canvasId}`, { + method: 'DELETE', + headers: authHeaders.value + }) + + if (res.ok) { + navigateTo('/canvases') + } else { + const err = await res.json() + alert('Ошибка при удалении: ' + (err.message || 'неизвестная ошибка')) + } + } catch (err) { + console.error('Delete error:', err) + alert('Не удалось удалить карту') + } +} + const fitView = () => { const padding = 40 const cols = 3 @@ -551,6 +611,7 @@ const saveCanvas = async () => { } const usersList = ref([]) +const canvasesList = ref([]) // Load data on mount onMounted(async () => { @@ -569,6 +630,19 @@ onMounted(async () => { } catch (e) { console.error('Failed to load users', e) } + + // Load canvases for cross-link dropdown + try { + const res = await fetch('/api/canvas', { + headers: authHeaders.value + }) + if (res.ok) { + const data = await res.json() + canvasesList.value = data.canvases || [] + } + } catch (e) { + console.error('Failed to load canvases', e) + } }) diff --git a/app/components/FlowNodeRenderer.vue b/app/components/FlowNodeRenderer.vue index c162500..1b50e3f 100644 --- a/app/components/FlowNodeRenderer.vue +++ b/app/components/FlowNodeRenderer.vue @@ -15,10 +15,10 @@
{{ data.label || getNodeTitle() }}
{{ data.type || type }}
-
+
{{ stateLabel }}
-
+
{{ priorityLabel }}
diff --git a/app/composables/useCanvas.ts b/app/composables/useCanvas.ts index 9f230a9..041501e 100644 --- a/app/composables/useCanvas.ts +++ b/app/composables/useCanvas.ts @@ -23,6 +23,8 @@ export interface CanvasEdge { } export interface CanvasData { + id?: string + name?: string nodes: CanvasNode[] edges: CanvasEdge[] } @@ -72,7 +74,12 @@ export const useCanvas = () => { toSide: e.toSide || 'left' })) - return { nodes, edges } + return { + id: json.id, + name: json.name, + nodes, + edges + } } const getAuthHeaders = (): Record => { diff --git a/app/pages/canvases.vue b/app/pages/canvases.vue index 852d901..659743a 100644 --- a/app/pages/canvases.vue +++ b/app/pages/canvases.vue @@ -60,8 +60,11 @@
@@ -118,7 +121,7 @@ definePageMeta({ }) const auth = useAuth() -const user = computed(() => auth.user) +const user = computed(() => auth.user.value) const authFetch = async (url, opts = {}) => { try { return await $fetch(url, { @@ -145,7 +148,7 @@ const activeTab = ref('personal') groupId: '' }) -const currentUserId = computed(() => auth.user.value?.id || '') +const currentUserId = computed(() => user.value?.id || '') // Load data const loadData = async () => { diff --git a/app/server/api/canvas/[id].delete.ts b/app/server/api/canvas/[id].delete.ts index 8ce4d86..e652db4 100644 --- a/app/server/api/canvas/[id].delete.ts +++ b/app/server/api/canvas/[id].delete.ts @@ -8,7 +8,15 @@ export default defineEventHandler(async (event) => { const canvas = await prisma.canvas.findUnique({ where: { id } }) if (!canvas) throw createError({ statusCode: 404, message: 'Canvas not found' }) - if (canvas.ownerId !== user.id) throw createError({ statusCode: 403, message: 'Access denied' }) + + // Allow deletion if owner OR if user is admin + const isOwner = canvas.ownerId === user.id + const isAdmin = user.role === 'admin' + + if (!isOwner && !isAdmin) { + console.warn(`[Delete Access Denied] User ${user.id} (${user.role}) tried to delete canvas ${id} owned by ${canvas.ownerId}`) + throw createError({ statusCode: 403, message: 'У вас нет прав на удаление этой карты. Она принадлежит другому пользователю.' }) + } await prisma.canvas.delete({ where: { id } })