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