mirror of https://github.com/nocodb/nocodb
Muhammed Mustafa
2 years ago
8 changed files with 693 additions and 1 deletions
@ -0,0 +1,140 @@
|
||||
<script setup lang="ts"> |
||||
import { useI18n } from 'vue-i18n' |
||||
import type { Edge, Node } from '@braks/vue-flow' |
||||
import { Background, MarkerType, VueFlow, isNode, useVueFlow } from '@braks/vue-flow' |
||||
import { ref } from 'vue' |
||||
import { ColumnType, UITypes } from 'nocodb-sdk' |
||||
import dagre from 'dagre' |
||||
import TableNode from './erd/TableNode.vue' |
||||
import RelationEdge from './erd/RelationEdge.vue' |
||||
import { useNuxtApp, useProject } from '#imports' |
||||
|
||||
const dagreGraph = new dagre.graphlib.Graph() |
||||
dagreGraph.setDefaultEdgeLabel(() => ({})) |
||||
|
||||
const { updateNodeInternals } = useVueFlow() |
||||
|
||||
const { $api } = useNuxtApp() |
||||
const { project, tables } = useProject() |
||||
const { t } = useI18n() |
||||
|
||||
const { metas, getMeta, metasWithId } = useMetas() |
||||
|
||||
const nodes = ref<Node[]>([]) |
||||
const edges = ref<Edge[]>([]) |
||||
|
||||
let isLoading = $ref(true) |
||||
|
||||
const loadMetasOfTablesNotInMetas = async () => { |
||||
await Promise.all( |
||||
tables.value |
||||
.filter((table) => !metas.value[table.id!]) |
||||
.map(async (table) => { |
||||
await getMeta(table.id!) |
||||
}), |
||||
) |
||||
} |
||||
|
||||
const populateTables = () => { |
||||
Object.keys(metasWithId.value).forEach((tableId) => { |
||||
nodes.value.push({ |
||||
id: tableId, |
||||
data: metasWithId.value[tableId], |
||||
type: 'custom', |
||||
}) |
||||
dagreGraph.setNode(tableId, { width: 250, height: 30 * metasWithId.value[tableId].columns.length }) |
||||
}) |
||||
|
||||
dagreGraph.setGraph({ rankdir: 'LR' }) |
||||
} |
||||
|
||||
const populateRelations = () => { |
||||
const ltarColumns = Object.keys(metasWithId.value).reduce((acc, tableId) => { |
||||
const table = metasWithId.value[tableId] |
||||
const ltarColumns = table.columns.filter((column) => column.uidt === UITypes.LinkToAnotherRecord) |
||||
|
||||
ltarColumns.forEach((column) => { |
||||
if (column.colOptions.type === 'hm') { |
||||
acc.push(column) |
||||
} |
||||
|
||||
if (column.colOptions.type === 'mm') { |
||||
// Remove duplicate relations |
||||
const relatedColumnId = column.colOptions.fk_child_column_id |
||||
if (!acc.find((col) => col.id === relatedColumnId)) { |
||||
acc.push(column) |
||||
} |
||||
} |
||||
}) |
||||
|
||||
return acc |
||||
}, []) |
||||
|
||||
edges.value = ltarColumns.map((column: any) => { |
||||
const source = column.fk_model_id |
||||
const target = column.colOptions.fk_related_model_id |
||||
|
||||
dagreGraph.setEdge(source, target) |
||||
|
||||
return { |
||||
id: `e${source}-${target}`, |
||||
source: `${source}`, |
||||
target: `${target}`, |
||||
sourceHandle: `s-${column.id}-${source}`, |
||||
targetHandle: `d-${column.id}-${target}`, |
||||
type: 'custom', |
||||
data: { column, table: metasWithId.value[source], relatedTable: metasWithId.value[target] }, |
||||
markerEnd: MarkerType.ArrowClosed, |
||||
} |
||||
}) |
||||
|
||||
// console.log('json:elements', JSON.parse(JSON.stringify(elements))) |
||||
// console.log('elements', elements) |
||||
} |
||||
|
||||
const layoutNodes = () => { |
||||
dagre.layout(dagreGraph) |
||||
|
||||
nodes.value = nodes.value.map((node) => { |
||||
const nodeWithPosition = dagreGraph.node(node.id) |
||||
if (!nodeWithPosition) return node |
||||
|
||||
return { ...node, position: { x: nodeWithPosition.x, y: nodeWithPosition.y } } |
||||
}) |
||||
} |
||||
|
||||
const populateElements = () => { |
||||
populateTables() |
||||
} |
||||
|
||||
onMounted(async () => { |
||||
if (isLoading) { |
||||
await loadMetasOfTablesNotInMetas() |
||||
|
||||
populateElements() |
||||
populateRelations() |
||||
layoutNodes() |
||||
|
||||
console.log('nodes', nodes.value) |
||||
console.log('edges', edges.value) |
||||
|
||||
updateNodeInternals(nodes.value as any) |
||||
isLoading = false |
||||
} |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div style="height: 650px"> |
||||
<div v-if="isLoading"></div> |
||||
<VueFlow v-else :nodes="nodes" :edges="edges" :fit-view-on-init="true" :default-edge-options="{ type: 'step' }"> |
||||
<template #node-custom="props"> |
||||
<TableNode :data="props.data" /> |
||||
</template> |
||||
<template #edge-custom="props"> |
||||
<RelationEdge v-bind="props" /> |
||||
</template> |
||||
<Background /> |
||||
</VueFlow> |
||||
</div> |
||||
</template> |
@ -0,0 +1,120 @@
|
||||
<script setup> |
||||
import { EdgeText, getBezierCenter, getBezierPath, getEdgeCenter } from '@braks/vue-flow' |
||||
import { computed } from 'vue' |
||||
|
||||
const props = defineProps({ |
||||
id: { |
||||
type: String, |
||||
required: true, |
||||
}, |
||||
sourceX: { |
||||
type: Number, |
||||
required: true, |
||||
}, |
||||
sourceY: { |
||||
type: Number, |
||||
required: true, |
||||
}, |
||||
targetX: { |
||||
type: Number, |
||||
required: true, |
||||
}, |
||||
targetY: { |
||||
type: Number, |
||||
required: true, |
||||
}, |
||||
sourcePosition: { |
||||
type: String, |
||||
required: true, |
||||
}, |
||||
targetPosition: { |
||||
type: String, |
||||
required: true, |
||||
}, |
||||
data: { |
||||
type: Object, |
||||
required: false, |
||||
}, |
||||
markerEnd: { |
||||
type: String, |
||||
required: false, |
||||
}, |
||||
style: { |
||||
type: Object, |
||||
required: false, |
||||
}, |
||||
sourceHandleId: { |
||||
type: String, |
||||
required: false, |
||||
}, |
||||
targetHandleId: { |
||||
type: String, |
||||
required: false, |
||||
}, |
||||
}) |
||||
|
||||
const isHovered = ref(false) |
||||
const { column, relatedTable, table } = props.data |
||||
|
||||
const edgePath = computed(() => |
||||
getBezierPath({ |
||||
sourceX: props.sourceX, |
||||
sourceY: props.sourceY, |
||||
sourcePosition: props.sourcePosition, |
||||
targetX: props.targetX, |
||||
targetY: props.targetY, |
||||
targetPosition: props.targetPosition, |
||||
}), |
||||
) |
||||
|
||||
const center = computed(() => |
||||
getEdgeCenter({ |
||||
sourceX: props.sourceX, |
||||
sourceY: props.sourceY, |
||||
sourcePosition: props.sourcePosition, |
||||
targetX: props.targetX, |
||||
targetY: props.targetY, |
||||
targetPosition: props.targetPosition, |
||||
}), |
||||
) |
||||
|
||||
watch( |
||||
() => isHovered.value, |
||||
(val) => { |
||||
console.log(val) |
||||
}, |
||||
) |
||||
</script> |
||||
|
||||
<script> |
||||
export default { |
||||
inheritAttrs: false, |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<circle :cx="sourceX" :cy="sourceY" fill="#fff" :r="5" stroke="#6F3381" :stroke-width="1.5" /> |
||||
<path |
||||
:id="id" |
||||
:style="style" |
||||
class="stroke-gray-500 p-4 hover:stroke-green-500 hover:cursor-pointer" |
||||
:class="{ 'stroke-green-500': isHovered }" |
||||
:stroke-width="2.5" |
||||
fill="none" |
||||
:d="edgePath" |
||||
:marker-end="markerEnd" |
||||
onmouseover="isHovered = true" |
||||
onmouseout="isHovered = false" |
||||
/> |
||||
<EdgeText |
||||
:x="center[0]" |
||||
:y="center[1]" |
||||
label="Text" |
||||
:label-style="{ fill: 'white' }" |
||||
:label-show-bg="true" |
||||
:label-bg-style="{ fill: '#10b981' }" |
||||
:label-bg-padding="[2, 4]" |
||||
:label-bg-border-radius="2" |
||||
/> |
||||
<circle :cx="targetX" :cy="targetY" fill="#fff" :r="5" stroke="#6F3381" :stroke-width="1.5" /> |
||||
</template> |
@ -0,0 +1,54 @@
|
||||
<script setup> |
||||
import { Handle, Position } from '@braks/vue-flow' |
||||
import { UITypes, isVirtualCol } from 'nocodb-sdk' |
||||
|
||||
const props = defineProps({ |
||||
data: { |
||||
type: Object, |
||||
required: true, |
||||
}, |
||||
}) |
||||
|
||||
const { data: table } = props |
||||
const columns = table.columns |
||||
// console.log(table) |
||||
|
||||
const pkColumn = computed(() => { |
||||
return columns.find((col) => col.pk) |
||||
}) |
||||
|
||||
const nonPkColumns = computed(() => { |
||||
return columns.filter((col) => !col.pk && col.uidt !== UITypes.ForeignKey) |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="h-full flex flex-col min-w-16 bg-gray-50 rounded-lg border-1"> |
||||
<div class="text-gray-600 text-md py-2 border-b-2 border-gray-100 w-full pl-3 bg-gray-100 font-semibold"> |
||||
{{ table.title }} |
||||
</div> |
||||
<div class="mx-1"> |
||||
<div class="w-full border-b-1 py-2 border-gray-100"> |
||||
<SmartsheetHeaderCell v-if="pkColumn" :column="pkColumn" :hide-menu="true" /> |
||||
</div> |
||||
<div v-for="col in nonPkColumns" :key="col.title"> |
||||
<div class="w-full h-full flex items-center min-w-32 border-b-1 border-gray-100 py-2"> |
||||
<div v-if="col.uidt === UITypes.LinkToAnotherRecord" class="flex relative w-full"> |
||||
<Handle :id="`s-${col.id}-${table.id}`" class="-right-4" type="source" :position="Position.Right" :hidden="false" /> |
||||
<Handle |
||||
:id="`d-${col.id}-${table.id}`" |
||||
class="-left-1" |
||||
type="destination" |
||||
:position="Position.Left" |
||||
:hidden="false" |
||||
/> |
||||
<SmartsheetHeaderVirtualCell :column="col" :hide-menu="true" /> |
||||
</div> |
||||
<SmartsheetHeaderVirtualCell v-else-if="isVirtualCol(col)" :column="col" :hide-menu="true" /> |
||||
|
||||
<SmartsheetHeaderCell v-else :column="col" :hide-menu="true" /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
Loading…
Reference in new issue