Browse Source

Merge branch 'develop' into feat/import-optimization

pull/4135/head
Wing-Kam Wong 2 years ago
parent
commit
012224c71b
  1. 6
      packages/nc-gui/assets/style.scss
  2. 20
      packages/nc-gui/components/cell/Checkbox.vue
  3. 20
      packages/nc-gui/components/cell/Currency.vue
  4. 2
      packages/nc-gui/components/dashboard/TreeView.vue
  5. 1
      packages/nc-gui/components/dashboard/settings/AppStore.vue
  6. 2
      packages/nc-gui/components/dashboard/settings/Erd.vue
  7. 59
      packages/nc-gui/components/erd/ConfigPanel.vue
  8. 106
      packages/nc-gui/components/erd/Flow.vue
  9. 19
      packages/nc-gui/components/erd/HistogramPanel.vue
  10. 199
      packages/nc-gui/components/erd/RelationEdge.vue
  11. 189
      packages/nc-gui/components/erd/TableNode.vue
  12. 124
      packages/nc-gui/components/erd/View.vue
  13. 225
      packages/nc-gui/components/erd/utils.ts
  14. 71
      packages/nc-gui/components/general/Tooltip.vue
  15. 57
      packages/nc-gui/components/smartsheet/Cell.vue
  16. 2
      packages/nc-gui/components/smartsheet/Form.vue
  17. 138
      packages/nc-gui/components/smartsheet/Grid.vue
  18. 36
      packages/nc-gui/components/smartsheet/toolbar/Erd.vue
  19. 7
      packages/nc-gui/components/webhook/Editor.vue
  20. 5
      packages/nc-gui/composables/useColumn.ts
  21. 2
      packages/nc-gui/composables/useMultiSelect/index.ts
  22. 23
      packages/nc-gui/composables/useViewData.ts
  23. 1
      packages/nc-gui/lib/types.ts
  24. 644
      packages/nc-gui/package-lock.json
  25. 4
      packages/nc-gui/package.json
  26. 9
      packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue
  27. 52
      packages/nc-gui/pages/[projectType]/form/[viewId]/index.vue
  28. 8
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue
  29. 41
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue
  30. 5
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  31. 12
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  32. 123
      packages/nocodb/src/lib/meta/api/columnApis.ts
  33. 16
      packages/nocodb/src/lib/meta/api/sync/helpers/job.ts

6
packages/nc-gui/assets/style.scss

@ -280,4 +280,8 @@ a {
.nc-toolbar-btn {
@apply !shadow-none rounded hover:(ring-1 ring-primary ring-opacity-100) focus:(ring-1 ring-accent ring-opacity-100);
}
}
.ant-modal {
@apply !top-[50px];
}

20
packages/nc-gui/components/cell/Checkbox.vue

@ -4,7 +4,7 @@ import { ColumnInj, IsFormInj, ReadonlyInj, getMdiIcon, inject } from '#imports'
interface Props {
// If the previous cell value was a text, the initial checkbox value is a string type
// otherwise it can be either a boolean, or a string representing a boolean, i.e '0' or '1'
modelValue?: boolean | string | '0' | '1'
modelValue?: boolean | string | number | '0' | '1'
}
interface Emits {
@ -16,7 +16,7 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>()
let vModel = $computed({
get: () => !!props.modelValue && props.modelValue !== '0',
get: () => !!props.modelValue && props.modelValue !== '0' && props.modelValue !== 0,
set: (val) => emits('update:modelValue', val),
})
@ -46,7 +46,7 @@ function onClick() {
<template>
<div
class="flex"
class="flex cursor-pointer"
:class="{
'justify-center': !isForm && !readOnly,
'w-full': isForm,
@ -56,12 +56,14 @@ function onClick() {
@click="onClick"
>
<div class="px-1 pt-1 rounded-full items-center" :class="{ 'bg-gray-100': !vModel, '!ml-[-8px]': readOnly }">
<component
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"
:style="{
color: checkboxMeta.color,
}"
/>
<Transition name="layout" mode="out-in" :duration="100">
<component
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"
:style="{
color: checkboxMeta.color,
}"
/>
</Transition>
</div>
</div>
</template>

20
packages/nc-gui/components/cell/Currency.vue

@ -8,14 +8,16 @@ interface Props {
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'save'])
const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj)
const editEnabled = inject(EditModeInj)!
const vModel = useVModel(props, 'modelValue', emit)
const lastSaved = ref()
const currencyMeta = computed(() => {
return {
currency_locale: 'en-US',
@ -38,6 +40,18 @@ const currency = computed(() => {
})
const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
const submitCurrency = () => {
if (lastSaved.value !== vModel.value) {
lastSaved.value = vModel.value
emit('save')
}
editEnabled.value = false
}
onMounted(() => {
lastSaved.value = vModel.value
})
</script>
<template>
@ -46,7 +60,7 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
:ref="focus"
v-model="vModel"
class="w-full h-full border-none outline-none px-2"
@blur="editEnabled = false"
@blur="submitCurrency"
/>
<span v-else-if="vModel">{{ currency }}</span>

2
packages/nc-gui/components/dashboard/TreeView.vue

@ -323,7 +323,7 @@ function openTableCreateDialog() {
:data-id="table.id"
@click="addTableTab(table)"
>
<GeneralTooltip wrapper-class="pl-5 pr-3 py-2" modifier-key="Alt">
<GeneralTooltip class="pl-5 pr-3 py-2" modifier-key="Alt">
<template #title>{{ table.table_name }}</template>
<div class="flex items-center gap-2 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto">

1
packages/nc-gui/components/dashboard/settings/AppStore.vue

@ -77,6 +77,7 @@ onMounted(async () => {
min-height="300"
:footer="null"
wrap-class-name="nc-modal-plugin-install"
v-bind="$attrs"
>
<LazyDashboardSettingsAppInstall
v-if="pluginApp && showPluginInstallModal"

2
packages/nc-gui/components/dashboard/settings/Erd.vue

@ -1,5 +1,5 @@
<template>
<div class="w-full h-full !py-0 !px-2" style="height: 70vh">
<div class="w-full h-full !p-0 h-70vh">
<ErdView />
</div>
</template>

59
packages/nc-gui/components/erd/ConfigPanel.vue

@ -0,0 +1,59 @@
<script lang="ts" setup>
import { Panel } from '@vue-flow/additional-components'
import type { ERDConfig } from './utils'
import { ref, useGlobal, useVModel } from '#imports'
const props = defineProps<{
config: ERDConfig
}>()
const { includeM2M } = useGlobal()
const config = useVModel(props, 'config')
const showAdvancedOptions = ref(false)
</script>
<template>
<Panel class="flex flex-col bg-white border-1 rounded border-gray-200 z-50 px-3 py-1 nc-erd-context-menu" position="top-right">
<div class="flex items-center gap-2">
<a-checkbox v-model:checked="config.showAllColumns" v-e="['c:erd:showAllColumns']" class="nc-erd-showColumns-checkbox" />
<span class="select-none nc-erd-showColumns-label" style="font-size: 0.65rem" @dblclick="showAdvancedOptions = true">
{{ $t('activity.erd.showColumns') }}
</span>
</div>
<div class="flex items-center gap-2">
<a-checkbox
v-model:checked="config.showPkAndFk"
v-e="['c:erd:showPkAndFk']"
class="nc-erd-showPkAndFk-checkbox"
:class="[
`nc-erd-showPkAndFk-checkbox-${config.showAllColumns ? 'enabled' : 'disabled'}`,
`nc-erd-showPkAndFk-checkbox-${config.showPkAndFk ? 'checked' : 'unchecked'}`,
]"
:disabled="!config.showAllColumns"
/>
<span class="select-none text-[0.65rem]">{{ $t('activity.erd.showPkAndFk') }}</span>
</div>
<div v-if="!config.singleTableMode" class="flex items-center gap-2">
<a-checkbox v-model:checked="config.showViews" v-e="['c:erd:showViews']" class="nc-erd-showViews-checkbox" />
<span class="select-none text-[0.65rem]">{{ $t('activity.erd.showSqlViews') }}</span>
</div>
<div v-if="!config.singleTableMode && showAdvancedOptions && includeM2M" class="flex flex-row items-center">
<a-checkbox v-model:checked="config.showMMTables" v-e="['c:erd:showMMTables']" class="nc-erd-showMMTables-checkbox" />
<span class="ml-2 select-none text-[0.65rem]">{{ $t('activity.erd.showMMTables') }}</span>
</div>
<div v-if="showAdvancedOptions && includeM2M" class="flex items-center gap-2">
<a-checkbox
v-model:checked="config.showJunctionTableNames"
v-e="['c:erd:showJunctionTableNames']"
class="nc-erd-showJunctionTableNames-checkbox"
/>
<span class="select-none text-[0.65rem]">{{ $t('activity.erd.showJunctionTableNames') }}</span>
</div>
</Panel>
</template>

106
packages/nc-gui/components/erd/Flow.vue

@ -1,66 +1,108 @@
<script setup lang="ts">
import { Background, Controls } from '@vue-flow/additional-components'
import { Background, Controls, Panel } from '@vue-flow/additional-components'
import { VueFlow, useVueFlow } from '@vue-flow/core'
import type { TableType } from 'nocodb-sdk'
import type { ErdFlowConfig } from './utils'
import type { ERDConfig } from './utils'
import { useErdElements } from './utils'
import { onScopeDispose, toRefs, watch } from '#imports'
import { computed, onScopeDispose, toRefs, watch } from '#imports'
interface Props {
tables: TableType[]
config: ErdFlowConfig
config: ERDConfig
}
const props = defineProps<Props>()
const { tables, config } = toRefs(props)
const { $destroy, fitView, onPaneReady } = useVueFlow({ minZoom: 0.1, maxZoom: 2 })
const { $destroy, fitView, onPaneReady, viewport, onNodeDoubleClick } = useVueFlow({ minZoom: 0.05, maxZoom: 2 })
const { layout, elements } = useErdElements(tables, config)
const showSkeleton = computed(() => viewport.value.zoom < 0.15)
function init() {
layout()
layout(showSkeleton.value)
if (!showSkeleton.value) {
setTimeout(zoomIn, 100)
}
}
function zoomIn(nodeId?: string) {
fitView({ nodes: nodeId ? [nodeId] : undefined, duration: 300, minZoom: 0.2 })
}
onPaneReady(() => {
layout(showSkeleton.value)
setTimeout(() => {
fitView({ duration: 500 })
fitView({ duration: 250, minZoom: 0.16 })
}, 100)
}
})
onPaneReady(init)
onNodeDoubleClick(({ node }) => {
if (showSkeleton.value) zoomIn()
watch([() => tables, () => config], init, { deep: true, flush: 'post' })
setTimeout(() => {
zoomIn(node.id)
}, 250)
})
watch(tables, init)
watch(showSkeleton, (isSkeleton) => {
layout(isSkeleton)
setTimeout(() => {
fitView({
duration: 300,
minZoom: isSkeleton ? undefined : viewport.value.zoom,
maxZoom: isSkeleton ? viewport.value.zoom : undefined,
})
}, 100)
})
onScopeDispose($destroy)
</script>
<template>
<VueFlow v-model="elements" elevate-edges-on-select>
<Controls position="top-right" :show-fit-view="false" :show-interactive="false" />
<VueFlow v-model="elements">
<Controls class="rounded" position="bottom-left" :show-fit-view="false" :show-interactive="false" />
<template #node-custom="{ data }">
<ErdTableNode :data="data" />
<template #node-custom="{ data, dragging }">
<ErdTableNode :data="data" :dragging="dragging" :show-skeleton="showSkeleton" />
</template>
<template #edge-custom="edgeProps">
<ErdRelationEdge v-bind="edgeProps" />
<ErdRelationEdge v-bind="edgeProps" :show-skeleton="showSkeleton" />
</template>
<Background />
<div
v-if="!config.singleTableMode"
class="absolute bottom-0 right-0 flex flex-col text-xs bg-white px-2 py-1 border-1 rounded-md border-gray-200 z-50 nc-erd-histogram"
style="font-size: 0.6rem"
>
<div class="flex flex-row items-center space-x-1 border-b-1 pb-1 border-gray-100">
<MdiTableLarge class="text-primary" />
<div>{{ $t('objects.table') }}</div>
</div>
<div class="flex flex-row items-center space-x-1 pt-1">
<MdiEyeCircleOutline class="text-primary" />
<div>{{ $t('objects.sqlVIew') }}</div>
</div>
</div>
<Background :size="showSkeleton ? 2 : undefined" :gap="showSkeleton ? 50 : undefined" />
<Transition name="layout">
<Panel
v-if="showSkeleton && config.showAllColumns"
position="bottom-center"
class="color-transition z-5 cursor-pointer rounded shadow-sm text-slate-400 font-semibold px-4 py-2 bg-slate-100/50 hover:(text-slate-900 ring ring-accent ring-opacity-100 bg-slate-100/90)"
@click="zoomIn"
>
<!-- todo: i18n -->
Zoom in to view columns
</Panel>
</Transition>
<slot />
</VueFlow>
</template>
<style>
.vue-flow__edges {
z-index: 1000 !important;
}
.vue-flow__controls-zoomin {
@apply rounded-t;
}
.vue-flow__controls-zoomout {
@apply rounded-b;
}
</style>

19
packages/nc-gui/components/erd/HistogramPanel.vue

@ -0,0 +1,19 @@
<script lang="ts" setup>
import { Panel } from '@vue-flow/additional-components'
</script>
<template>
<Panel class="text-xs bg-white border-1 rounded border-gray-200 z-50 nc-erd-histogram" position="bottom-right">
<div class="flex flex-col divide-y-1">
<div class="flex items-center gap-1 p-2">
<MdiTableLarge class="text-primary" />
<div>{{ $t('objects.table') }}</div>
</div>
<div class="flex items-center gap-1 p-2">
<MdiEyeCircleOutline class="text-primary" />
<div>{{ $t('objects.sqlVIew') }}</div>
</div>
</div>
</Panel>
</template>

199
packages/nc-gui/components/erd/RelationEdge.vue

@ -2,9 +2,10 @@
import type { EdgeProps, Position } from '@vue-flow/core'
import { EdgeText, getBezierPath } from '@vue-flow/core'
import type { CSSProperties } from '@vue/runtime-dom'
import type { EdgeData } from './utils'
import { computed, toRef } from '#imports'
interface RelationEdgeProps extends EdgeProps {
interface RelationEdgeProps extends EdgeProps<EdgeData> {
id: string
sourceX: number
sourceY: number
@ -12,36 +13,41 @@ interface RelationEdgeProps extends EdgeProps {
targetY: number
sourcePosition: Position
targetPosition: Position
data: {
isManyToMany: boolean
isSelfRelation: boolean
label: string
}
markerEnd: string
data: EdgeData
style: CSSProperties
targetHandleId: string
selected?: boolean
showSkeleton: boolean
markerEnd: string
events: EdgeProps['events']
sourceNode: EdgeProps['sourceNode']
targetNode: EdgeProps['targetNode']
}
const props = defineProps<RelationEdgeProps>()
const baseStroke = 2
const data = toRef(props, 'data')
const isHovering = ref(false)
const edgePath = computed(() => {
if (data.value.isSelfRelation) {
const { sourceX, sourceY, targetX, targetY } = props
const radiusX = (sourceX - targetX) * 0.6
const radiusY = 50
return `M ${sourceX} ${sourceY} A ${radiusX} ${radiusY} 0 1 0 ${targetX} ${targetY}`
return [`M ${sourceX} ${sourceY} A ${radiusX} ${radiusY} 0 1 0 ${targetX} ${targetY}`]
}
return getBezierPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
sourcePosition: props.sourcePosition,
targetX: props.targetX,
targetY: props.targetY,
targetPosition: props.targetPosition,
})
return getBezierPath({ ...props })
})
props.events?.mouseEnter?.(() => {
isHovering.value = true
})
props.events?.mouseLeave?.(() => {
isHovering.value = false
})
</script>
@ -52,72 +58,95 @@ export default {
</script>
<template>
<path
:id="id"
:style="style"
class="path-wrapper p-4 hover:cursor-pointer"
:stroke-width="8"
fill="none"
:d="edgePath[0]"
:marker-end="markerEnd"
/>
<path
:id="id"
:style="style"
class="path stroke-gray-500 hover:stroke-green-500 hover:cursor-pointer"
:stroke-width="1.5"
fill="none"
:d="edgePath[0]"
:marker-end="markerEnd"
/>
<EdgeText
v-if="data.label?.length > 0"
:class="`nc-erd-table-label-${data.label.toLowerCase().replace(' ', '-').replace('\(', '').replace(')', '')}`"
:x="edgePath[1]"
:y="edgePath[2]"
:label="data.label"
:label-style="{ fill: 'white' }"
:label-show-bg="true"
:label-bg-style="{ fill: '#10b981' }"
:label-bg-padding="[2, 4]"
:label-bg-border-radius="2"
/>
<rect
class="nc-erd-edge-rect"
:x="sourceX"
:y="sourceY - 4"
width="8"
height="8"
fill="#fff"
stroke="#6F3381"
:stroke-width="1.5"
:transform="`rotate(45,${sourceX + 2},${sourceY - 4})`"
/>
<rect
v-if="data.isManyToMany"
class="nc-erd-edge-rect"
:x="targetX"
:y="targetY - 4"
width="8"
height="8"
fill="#fff"
stroke="#6F3381"
:stroke-width="1.5"
:transform="`rotate(45,${targetX + 2},${targetY - 4})`"
/>
<circle v-else class="nc-erd-edge-circle" :cx="targetX" :cy="targetY" fill="#fff" :r="5" stroke="#6F3381" :stroke-width="1.5" />
</template>
<defs>
<linearGradient id="linear-gradient" x1="-28.83" y1="770.92" x2="771.05" y2="-28.95" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#06b6d4" />
<stop offset="0.18" stop-color="#155e75" />
<stop offset="0.49" stop-color="#84cc16" />
<stop offset="0.88" stop-color="#10b981" />
<stop offset="0.99" stop-color="#047857" />
</linearGradient>
</defs>
<style scoped lang="scss">
.path-wrapper:hover + .path {
@apply stroke-green-500;
stroke-width: 2;
}
.path:hover {
stroke-width: 2;
}
</style>
<Transition name="layout" :duration="50" mode="in-out">
<path
v-if="selected || isHovering"
style="stroke: url(#linear-gradient)"
:stroke-width="(showSkeleton ? baseStroke * 12 : baseStroke * 8) / (selected || isHovering ? 2 : 1)"
fill="none"
:d="edgePath[0]"
:marker-end="showSkeleton ? markerEnd : ''"
/>
<path
v-else
:id="id"
class="stroke-slate-500"
:style="style"
:stroke-width="showSkeleton ? baseStroke * 4 : baseStroke"
fill="none"
:d="edgePath[0]"
:marker-end="showSkeleton ? markerEnd : ''"
/>
</Transition>
<path class="opacity-0" :stroke-width="showSkeleton ? baseStroke * 12 : baseStroke * 8" fill="none" :d="edgePath[0]" />
<Transition name="layout">
<EdgeText
v-if="data.label?.length > 0"
:key="`edge-text-${id}.${showSkeleton}`"
class="color-transition"
:class="[
selected || isHovering ? 'opacity-100' : 'opacity-0 !pointer-events-none',
showSkeleton ? '!text-6xl' : '!text-xs',
`nc-erd-table-label-${data.label.toLowerCase().replace(' ', '-').replace('\(', '').replace(')', '')}`,
]"
:x="edgePath[1]"
:y="edgePath[2]"
:label="showSkeleton ? data.simpleLabel : data.label"
:label-style="{ fill: 'white', fontSize: `${showSkeleton ? baseStroke * 2 : baseStroke / 2}rem` }"
:label-show-bg="true"
:label-bg-style="{ fill: data.color }"
:label-bg-padding="[8, 6]"
:label-bg-border-radius="2"
/>
</Transition>
<template v-if="!showSkeleton">
<rect
class="nc-erd-edge-rect"
:x="sourceX"
:y="sourceY - 4"
:width="8"
:height="8"
fill="#fff"
:stroke="sourceNode.data.color"
:stroke-width="2"
:transform="`rotate(45,${sourceX + 2},${sourceY - 4})`"
/>
<rect
v-if="data.isManyToMany"
class="nc-erd-edge-rect"
:x="targetX"
:y="targetY - 4"
:width="8"
:height="8"
fill="#fff"
:stroke="sourceNode.data.color"
:stroke-width="2"
:transform="`rotate(45,${targetX + 2},${targetY - 4})`"
/>
<circle
v-else
class="nc-erd-edge-circle"
:cx="targetX"
:cy="targetY"
fill="#fff"
:r="5"
:stroke="targetNode.data.color"
:stroke-width="2"
/>
</template>
</template>

189
packages/nc-gui/components/erd/TableNode.vue

@ -1,125 +1,132 @@
<script lang="ts" setup>
import type { NodeProps } from '@vue-flow/core'
import { Handle, Position } from '@vue-flow/core'
import type { ColumnType, TableType } from 'nocodb-sdk'
import { Handle, Position, useVueFlow } from '@vue-flow/core'
import type { LinkToAnotherRecordType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { MetaInj, computed, provide, toRefs, useNuxtApp } from '#imports'
import type { NodeData } from './utils'
import { MetaInj, computed, provide, refAutoReset, toRef, useNuxtApp, watch } from '#imports'
interface Props extends NodeProps {
data: TableType & { showPkAndFk: boolean; showAllColumns: boolean }
interface Props extends NodeProps<NodeData> {
data: NodeData
showSkeleton: boolean
dragging: boolean
}
const props = defineProps<Props>()
const { data, showSkeleton, dragging } = defineProps<Props>()
const { data } = toRefs(props)
const { viewport } = useVueFlow()
provide(MetaInj, data as Ref<TableType>)
const table = toRef(data, 'table')
const isZooming = refAutoReset(false, 200)
provide(MetaInj, table)
const { $e } = useNuxtApp()
const columns = computed(() => {
// Hide hm ltar created for `mm` relations
return data.value.columns?.filter((col) => !(col.uidt === UITypes.LinkToAnotherRecord && col.system === 1))
})
const pkAndFkColumns = computed(() => {
return columns.value
?.filter(() => data.value.showPkAndFk && data.value.showAllColumns)
.filter((col) => col.pk || col.uidt === UITypes.ForeignKey)
})
const nonPkColumns = computed(() => {
return columns.value
?.filter(
(col: ColumnType) => data.value.showAllColumns || (!data.value.showAllColumns && col.uidt === UITypes.LinkToAnotherRecord),
)
.filter((col: ColumnType) => !col.pk && col.uidt !== UITypes.ForeignKey)
})
const relatedColumnId = (col: Record<string, any>) =>
col.colOptions.type === 'mm' ? col.colOptions.fk_parent_column_id : col.colOptions.fk_child_column_id
const relatedColumnId = (colOptions: LinkToAnotherRecordType | any) =>
colOptions.type === 'mm' ? colOptions.fk_parent_column_id : colOptions.fk_child_column_id
const hasColumns = computed(() => data.pkAndFkColumns.length || data.nonPkColumns.length)
watch(
() => viewport.value.zoom,
() => {
isZooming.value = true
},
)
</script>
<template>
<div
class="h-full flex flex-col min-w-16 bg-gray-50 rounded-lg border-1 nc-erd-table-node"
:class="`nc-erd-table-node-${data.table_name}`"
@click="$e('c:erd:node-click')"
<GeneralTooltip
class="h-full flex flex-1 justify-center items-center"
:modifier-key="showSkeleton || viewport.zoom > 0.35 ? 'Alt' : undefined"
:disabled="dragging || isZooming"
>
<GeneralTooltip modifier-key="Alt">
<template #title> {{ data.table_name }} </template>
<template #title>
<div class="capitalize">{{ table.table_name }}</div>
</template>
<div
class="relative h-full flex flex-col justify-center bg-slate-50 min-w-16 min-h-8 rounded-lg nc-erd-table-node"
:class="[
`nc-erd-table-node-${table.table_name}`,
showSkeleton ? 'cursor-pointer items-center bg-slate-200 min-h-200px min-w-300px px-4' : '',
]"
@click="$e('c:erd:node-click')"
>
<div
class="text-gray-600 text-md py-2 border-b-1 border-gray-200 rounded-t-lg w-full pr-3 pl-2 bg-gray-100 font-semibold flex flex-row items-center"
:class="[showSkeleton ? '' : 'bg-primary bg-opacity-10', hasColumns ? 'border-b-1' : '']"
class="text-slate-600 text-md py-2 border-slate-500 rounded-t-lg w-full h-full px-3 font-semibold flex items-center"
>
<MdiTableLarge v-if="data.type === 'table'" class="text-primary" />
<MdiEyeCircleOutline v-else class="text-primary" />
<div class="flex pl-1.5">
{{ data.title }}
<MdiTableLarge v-if="table.type === 'table'" class="text-primary" :class="showSkeleton ? 'text-6xl !px-2' : ''" />
<MdiEyeCircleOutline v-else class="text-primary" :class="showSkeleton ? 'text-6xl !px-2' : ''" />
<div :class="showSkeleton ? 'text-6xl' : ''" class="flex px-2">
{{ table.title }}
</div>
</div>
</GeneralTooltip>
<div>
<div
v-for="col in pkAndFkColumns"
:key="col.title"
class="w-full border-b-1 py-2 border-gray-100 keys"
:class="`nc-erd-table-node-${data.table_name}-column-${col.column_name}`"
>
<LazySmartsheetHeaderCell v-if="col" :column="col" :hide-menu="true" />
<div v-if="showSkeleton">
<Handle style="left: -20px" class="opacity-0" :position="Position.Left" type="target" :connectable="false" />
<Handle style="right: -15px" class="opacity-0" :position="Position.Right" type="source" :connectable="false" />
</div>
<div class="w-full mb-1"></div>
<div v-for="(col, index) in nonPkColumns" :key="col.title">
<div v-else-if="hasColumns">
<div
class="w-full h-full flex items-center min-w-32 border-gray-100 py-2 px-1"
:class="index + 1 === nonPkColumns.length ? 'rounded-b-lg' : 'border-b-1'"
v-for="col in data.pkAndFkColumns"
:key="col.title"
class="w-full h-full min-w-32 border-b-1 py-2 px-1 border-slate-200 bg-slate-100"
:class="`nc-erd-table-node-${table.table_name}-column-${col.column_name}`"
>
<LazySmartsheetHeaderCell v-if="col" :column="col" :hide-menu="true" />
</div>
<div v-for="(col, index) in data.nonPkColumns" :key="col.title">
<div
v-if="col.uidt === UITypes.LinkToAnotherRecord"
class="flex relative w-full"
:class="`nc-erd-table-node-${data.table_name}-column-${col.title?.toLowerCase().replace(' ', '_')}`"
class="relative w-full h-full flex items-center min-w-32 border-slate-200 py-2 px-1"
:class="index + 1 === data.nonPkColumns.length ? 'rounded-b-lg' : 'border-b-1'"
>
<Handle
:id="`s-${relatedColumnId(col)}-${data.id}`"
class="-right-4 opacity-0"
type="source"
:position="Position.Right"
<div
v-if="col.uidt === UITypes.LinkToAnotherRecord"
class="flex w-full"
:class="`nc-erd-table-node-${table.table_name}-column-${col.title?.toLowerCase().replace(' ', '_')}`"
>
<Handle
:id="`s-${relatedColumnId(col.colOptions)}-${table.id}`"
class="opacity-0 !right-[-1px]"
type="source"
:position="Position.Right"
:connectable="false"
/>
<Handle
:id="`d-${relatedColumnId(col.colOptions)}-${table.id}`"
class="opacity-0 !left-[-1px]"
type="target"
:position="Position.Left"
:connectable="false"
/>
<LazySmartsheetHeaderVirtualCell :column="col" :hide-menu="true" />
</div>
<LazySmartsheetHeaderVirtualCell
v-else-if="isVirtualCol(col)"
:column="col"
:hide-menu="true"
:class="`nc-erd-table-node-${table.table_name}-column-${col.column_name}`"
/>
<Handle
:id="`d-${relatedColumnId(col)}-${data.id}`"
class="-left-1 opacity-0"
type="target"
:position="Position.Left"
<LazySmartsheetHeaderCell
v-else
:column="col"
:hide-menu="true"
:class="`nc-erd-table-node-${table.table_name}-column-${col.column_name}`"
/>
<LazySmartsheetHeaderVirtualCell :column="col" :hide-menu="true" />
</div>
<LazySmartsheetHeaderVirtualCell
v-else-if="isVirtualCol(col)"
:column="col"
:hide-menu="true"
:class="`nc-erd-table-node-${data.table_name}-column-${col.column_name}`"
/>
<LazySmartsheetHeaderCell
v-else
:column="col"
:hide-menu="true"
:class="`nc-erd-table-node-${data.table_name}-column-${col.column_name}`"
/>
</div>
</div>
</div>
</div>
</GeneralTooltip>
</template>
<style scoped lang="scss">
.keys {
background-color: #f6f6f6;
}
</style>

124
packages/nc-gui/components/erd/View.vue

@ -1,12 +1,11 @@
<script setup lang="ts">
import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { ref, useGlobal, useMetas, useProject, watch } from '#imports'
import type { ERDConfig } from './utils'
import { reactive, ref, useMetas, useProject, watch } from '#imports'
const { table } = defineProps<{ table?: TableType }>()
const { includeM2M } = useGlobal()
const { tables: projectTables } = useProject()
const { metas, getMeta } = useMetas()
@ -14,9 +13,8 @@ const { metas, getMeta } = useMetas()
const tables = ref<TableType[]>([])
let isLoading = $ref(true)
const showAdvancedOptions = ref(false)
const config = ref({
const config = reactive<ERDConfig>({
showPkAndFk: true,
showViews: false,
showAllColumns: true,
@ -36,7 +34,8 @@ const loadMetaOfTablesNotInMetas = async (localTables: TableType[]) => {
}
const populateTables = async () => {
let localTables: TableType[] = []
let localTables: TableType[]
if (table) {
// if table is provided only get the table and its related tables
localTables = projectTables.value.filter(
@ -57,110 +56,47 @@ const populateTables = async () => {
tables.value = localTables
.filter(
(t) =>
// todo: table type is missing mm property in type definition
config.value.showMMTables ||
(!config.value.showMMTables && !t.mm) ||
config.showMMTables ||
(!config.showMMTables && !t.mm) ||
// Show mm table if it's the selected table
t.id === table?.id,
)
.filter((t) => (!config.value.showViews && t.type !== 'view') || config.value.showViews)
.filter((t) => config.singleTableMode || (!config.showViews && t.type !== 'view') || config.showViews)
isLoading = false
}
watch(
[config, metas],
async () => {
await populateTables()
},
{
deep: true,
},
)
watch([metas, projectTables], populateTables, {
flush: 'post',
immediate: true,
})
watch(
[projectTables],
async () => {
await populateTables()
},
{ immediate: true },
)
watch(config, populateTables, {
flush: 'post',
deep: true,
})
watch(
() => config.value.showAllColumns,
() => config.showAllColumns,
() => {
config.value.showPkAndFk = config.value.showAllColumns
config.showPkAndFk = config.showAllColumns
},
)
</script>
<template>
<div
class="w-full"
style="height: inherit"
:class="{
'nc-erd-vue-flow': !config.singleTableMode,
'nc-erd-vue-flow-single-table': config.singleTableMode,
}"
>
<div v-if="isLoading" class="h-full w-full flex flex-col justify-center items-center">
<div class="flex flex-row justify-center">
<a-spin size="large" />
</div>
</div>
<div v-else class="relative h-full">
<LazyErdFlow :tables="tables" :config="config" />
<div
class="absolute top-3 right-12 flex-col bg-white py-2 px-4 border-1 border-gray-100 rounded-md z-50 space-y-1 nc-erd-context-menu z-50"
>
<div class="flex flex-row items-center">
<a-checkbox
v-model:checked="config.showAllColumns"
v-e="['c:erd:showAllColumns']"
class="nc-erd-showColumns-checkbox"
/>
<span
class="ml-2 select-none nc-erd-showColumns-label"
style="font-size: 0.65rem"
@dblclick="showAdvancedOptions = true"
>
{{ $t('activity.erd.showColumns') }}
</span>
</div>
<div class="flex flex-row items-center">
<a-checkbox
v-model:checked="config.showPkAndFk"
v-e="['c:erd:showPkAndFk']"
class="nc-erd-showPkAndFk-checkbox"
:class="{
'nc-erd-showPkAndFk-checkbox-enabled': config.showAllColumns,
'nc-erd-showPkAndFk-checkbox-disabled': !config.showAllColumns,
'nc-erd-showPkAndFk-checkbox-checked': config.showPkAndFk,
'nc-erd-showPkAndFk-checkbox-unchecked': !config.showPkAndFk,
}"
:disabled="!config.showAllColumns"
/>
<span class="ml-2 select-none text-[0.65rem]">{{ $t('activity.erd.showPkAndFk') }}</span>
</div>
<div v-if="!table" class="flex flex-row items-center">
<a-checkbox v-model:checked="config.showViews" v-e="['c:erd:showViews']" class="nc-erd-showViews-checkbox" />
<span class="ml-2 select-none text-[0.65rem]">{{ $t('activity.erd.showSqlViews') }}</span>
</div>
<div v-if="!table && showAdvancedOptions && includeM2M" class="flex flex-row items-center">
<a-checkbox v-model:checked="config.showMMTables" v-e="['c:erd:showMMTables']" class="nc-erd-showMMTables-checkbox" />
<span class="ml-2 select-none text-[0.65rem]">{{ $t('activity.erd.showMMTables') }}</span>
</div>
<div v-if="showAdvancedOptions && includeM2M" class="flex flex-row items-center">
<a-checkbox
v-model:checked="config.showJunctionTableNames"
v-e="['c:erd:showJunctionTableNames']"
class="nc-erd-showJunctionTableNames-checkbox"
/>
<span class="ml-2 select-none text-[0.65rem]">{{ $t('activity.erd.showJunctionTableNames') }}</span>
</div>
</div>
<div class="w-full" style="height: inherit" :class="[`nc-erd-vue-flow${config.singleTableMode ? '-single-table' : ''}`]">
<div class="relative h-full">
<LazyErdFlow :tables="tables" :config="config">
<GeneralOverlay v-model="isLoading" inline class="bg-gray-300/50">
<div class="h-full w-full flex flex-col justify-center items-center">
<a-spin size="large" />
</div>
</GeneralOverlay>
<ErdConfigPanel :config="config" />
<ErdHistogramPanel v-if="!config.singleTableMode" />
</LazyErdFlow>
</div>
</div>
</template>

225
packages/nc-gui/components/erd/utils.ts

@ -1,17 +1,38 @@
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import dagre from 'dagre'
import type { Edge, Elements } from '@vue-flow/core'
import type { Edge, EdgeMarker, Elements, Node } from '@vue-flow/core'
import type { MaybeRef } from '@vueuse/core'
import { Position, isEdge, isNode } from '@vue-flow/core'
import { computed, ref, unref, useMetas } from '#imports'
import { MarkerType, Position, isEdge, isNode } from '@vue-flow/core'
import { scaleLinear as d3ScaleLinear } from 'd3-scale'
import tinycolor from 'tinycolor2'
import { computed, ref, unref, useMetas, useTheme } from '#imports'
export interface ErdFlowConfig {
export interface ERDConfig {
showPkAndFk: boolean
showViews: boolean
showAllColumns: boolean
singleTableMode: boolean
showJunctionTableNames: boolean
showMMTables: boolean
}
export interface NodeData {
table: TableType
pkAndFkColumns: ColumnType[]
nonPkColumns: ColumnType[]
showPkAndFk: boolean
showAllColumns: boolean
color: string
columnLength: number
}
export interface EdgeData {
isManyToMany: boolean
isSelfRelation: boolean
label?: string
simpleLabel?: string
color: string
}
interface Relation {
@ -23,8 +44,18 @@ interface Relation {
type: 'mm' | 'hm'
}
export function useErdElements(tables: MaybeRef<TableType[]>, props: MaybeRef<ErdFlowConfig>) {
const elements = ref<Elements>([])
/**
* This util is used to generate the ERD graph elements and layout them
*
* @param tables
* @param props
*/
export function useErdElements(tables: MaybeRef<TableType[]>, props: MaybeRef<ERDConfig>) {
const elements = ref<Elements<NodeData | EdgeData>>([])
const { theme } = useTheme()
const colorScale = d3ScaleLinear<string>().domain([0, 2]).range([theme.value.primaryColor, theme.value.accentColor])
const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({}))
@ -35,6 +66,9 @@ export function useErdElements(tables: MaybeRef<TableType[]>, props: MaybeRef<Er
const erdTables = computed(() => unref(tables))
const config = $computed(() => unref(props))
const nodeWidth = 300
const nodeHeight = $computed(() => (config.showViews && config.showAllColumns ? 50 : 40))
const relations = computed(() =>
erdTables.value.reduce((acc, table) => {
const meta = metasWithIdAsKey.value[table.id!]
@ -87,47 +121,88 @@ export function useErdElements(tables: MaybeRef<TableType[]>, props: MaybeRef<Er
}, [] as Relation[]),
)
const edgeMMTableLabel = (modelId?: string) => {
if (!modelId) return ''
function edgeLabel({ type, source, target, modelId, childColId, parentColId }: Relation) {
const typeLabel = type === 'mm' ? 'many to many' : 'has many'
const parentCol = metasWithIdAsKey.value[source].columns?.find((col) => {
const colOptions = col.colOptions as LinkToAnotherRecordType
if (!colOptions) return false
return (
colOptions.fk_child_column_id === childColId &&
colOptions.fk_parent_column_id === parentColId &&
colOptions.fk_mm_model_id === modelId
)
})
const childCol = metasWithIdAsKey.value[target].columns?.find((col) => {
const colOptions = col.colOptions as LinkToAnotherRecordType
if (!colOptions) return false
return colOptions.fk_parent_column_id === (type === 'mm' ? childColId : parentColId)
})
if (!parentCol || !childCol) return ''
if (type === 'mm') {
if (config.showJunctionTableNames) {
if (!modelId) return ''
const mmModel = metasWithIdAsKey.value[modelId]
const mmModel = metasWithIdAsKey.value[modelId]
if (!mmModel) return ''
if (mmModel.title !== mmModel.table_name) {
return `${mmModel.title} (${mmModel.table_name})`
if (mmModel.title !== mmModel.table_name) {
return [`${mmModel.title} (${mmModel.table_name})`]
}
return [mmModel.title]
}
}
return mmModel.title
return [
// detailed edge label
`[${metasWithIdAsKey.value[source].title}] ${parentCol.title} - ${typeLabel} - ${childCol.title} [${metasWithIdAsKey.value[target].title}]`,
// simple edge label (for skeleton)
`${metasWithIdAsKey.value[source].title} - ${typeLabel} - ${metasWithIdAsKey.value[target].title}`,
]
}
function createNodes() {
return erdTables.value.flatMap((table) => {
if (!table.id) return []
return erdTables.value.reduce<Node<NodeData>[]>((acc, table) => {
if (!table.id) return acc
const columns =
metasWithIdAsKey.value[table.id].columns?.filter(
(col) => config.showAllColumns || (!config.showAllColumns && col.uidt === UITypes.LinkToAnotherRecord),
) || []
return [
{
id: table.id,
data: {
...metasWithIdAsKey.value[table.id],
showPkAndFk: config.showPkAndFk,
showAllColumns: config.showAllColumns,
columnLength: columns.length,
},
type: 'custom',
position: { x: 0, y: 0 },
const pkAndFkColumns = columns.filter(() => config.showPkAndFk).filter((col) => col.pk || col.uidt === UITypes.ForeignKey)
const nonPkColumns = columns.filter((col) => !col.pk && col.uidt !== UITypes.ForeignKey)
acc.push({
id: table.id,
data: {
table: metasWithIdAsKey.value[table.id],
pkAndFkColumns,
nonPkColumns,
showPkAndFk: config.showPkAndFk,
showAllColumns: config.showAllColumns,
columnLength: columns.length,
color: '',
},
]
})
type: 'custom',
position: { x: 0, y: 0 },
})
return acc
}, [])
}
function createEdges() {
return relations.value.reduce<Edge[]>((acc, { source, target, childColId, parentColId, type, modelId }) => {
return relations.value.reduce<Edge<EdgeData>[]>((acc, { source, target, childColId, parentColId, type, modelId }) => {
let sourceColumnId, targetColumnId
let edgeLabel = ''
if (type === 'hm') {
sourceColumnId = childColId
@ -137,22 +212,34 @@ export function useErdElements(tables: MaybeRef<TableType[]>, props: MaybeRef<Er
if (type === 'mm') {
sourceColumnId = parentColId
targetColumnId = childColId
edgeLabel = config.showJunctionTableNames ? edgeMMTableLabel(modelId) : ''
}
if (source !== target) dagreGraph.setEdge(source, target)
const [label, simpleLabel] = edgeLabel({
source,
target,
type,
childColId,
parentColId,
modelId,
})
acc.push({
id: `e-${sourceColumnId}-${source}-${targetColumnId}-${target}-#${edgeLabel}`,
id: `e-${sourceColumnId}-${source}-${targetColumnId}-${target}-#${label}`,
source: `${source}`,
target: `${target}`,
sourceHandle: `s-${sourceColumnId}-${source}`,
targetHandle: `d-${targetColumnId}-${target}`,
type: 'custom',
markerEnd: {
id: 'arrow-colored',
type: MarkerType.ArrowClosed,
},
data: {
isManyToMany: type === 'mm',
isSelfRelation: source === target && sourceColumnId === targetColumnId,
label: edgeLabel,
label,
simpleLabel,
color: '',
},
})
@ -160,46 +247,25 @@ export function useErdElements(tables: MaybeRef<TableType[]>, props: MaybeRef<Er
}, [])
}
function connectNonConnectedNodes() {
const connectedNodes = new Set<string>()
elements.value.forEach((el) => {
if (isEdge(el)) {
connectedNodes.add(el.source)
connectedNodes.add(el.target)
}
})
const nonConnectedNodes = erdTables.value.filter((table) => !connectedNodes.has(table.id!))
if (nonConnectedNodes.length === 0) return
if (nonConnectedNodes.length === 1) {
const firstTable = erdTables.value.find((table) => table.type === 'table' && table.id !== nonConnectedNodes[0].id)
if (!firstTable) return
dagreGraph.setEdge(nonConnectedNodes[0].id!, firstTable.id!)
return
}
const firstNode = nonConnectedNodes[0]
nonConnectedNodes.forEach((node, index) => {
if (index === 0) return
const source = firstNode.id!
const target = node.id!
const boxShadow = (skeleton: boolean, color: string) => ({
border: 'none !important',
boxShadow: `0 0 0 ${skeleton ? '12' : '2'}px ${color}`,
})
dagreGraph.setEdge(source, target)
})
}
const layout = (skeleton = false) => {
elements.value = [...createNodes(), ...createEdges()] as Elements<NodeData | EdgeData>
const layout = () => {
elements.value = [...createNodes(), ...createEdges()]
if (!config.singleTableMode) connectNonConnectedNodes()
elements.value.forEach((el) => {
if (isNode(el)) {
dagreGraph.setNode(el.id, { width: 250, height: 50 * el.data.columnLength })
const node = el as Node<NodeData>
const colLength = node.data!.columnLength
const width = skeleton ? nodeWidth * 3 : nodeWidth
const height = nodeHeight + (skeleton ? 250 : colLength > 0 ? nodeHeight * colLength : nodeHeight)
dagreGraph.setNode(el.id, {
width,
height,
})
} else if (isEdge(el)) {
dagreGraph.setEdge(el.source, el.target)
}
@ -209,10 +275,31 @@ export function useErdElements(tables: MaybeRef<TableType[]>, props: MaybeRef<Er
elements.value.forEach((el) => {
if (isNode(el)) {
const color = colorScale(dagreGraph.predecessors(el.id)!.length)
const nodeWithPosition = dagreGraph.node(el.id)
el.targetPosition = Position.Left
el.sourcePosition = Position.Right
el.position = { x: nodeWithPosition.x, y: nodeWithPosition.y }
el.class = ['rounded-lg'].join(' ')
el.data.color = color
el.style = (n) => {
if (n.selected) {
return boxShadow(skeleton, color)
}
return boxShadow(skeleton, '#64748B')
}
} else if (isEdge(el)) {
const node = elements.value.find((nodes) => nodes.id === el.source)
if (node) {
const color = node.data!.color
el.data.color = color
;(el.markerEnd as EdgeMarker).color = `#${tinycolor(color).toHex()}`
}
}
})
}

71
packages/nc-gui/components/general/Tooltip.vue

@ -1,46 +1,87 @@
<script lang="ts" setup>
import { onKeyStroke } from '@vueuse/core'
import { ref, watch } from '#imports'
import type { CSSProperties } from '@vue/runtime-dom'
import { controlledRef, ref, useElementHover, watch } from '#imports'
interface Props {
// Key to be pressed on hover to trigger the tooltip
modifierKey?: string
wrapperClass?: string
tooltipStyle?: CSSProperties
// force disable tooltip
disabled?: boolean
}
const { modifierKey, wrapperClass } = defineProps<Props>()
const { modifierKey, tooltipStyle, disabled } = defineProps<Props>()
const showTooltip = ref(false)
const el = ref()
const isMouseOver = ref(false)
const showTooltip = controlledRef(false, {
onBeforeChange: (shouldShow) => {
if (shouldShow && disabled) return false
},
})
const isHovering = useElementHover(() => el.value)
const isKeyPressed = ref(false)
if (modifierKey) {
onKeyStroke(modifierKey, (e) => {
onKeyStroke(
(e) => e.key === modifierKey,
(e) => {
e.preventDefault()
if (modifierKey && isMouseOver.value) {
if (isHovering.value) {
showTooltip.value = true
}
})
}
watch(isMouseOver, (val) => {
if (!val) {
isKeyPressed.value = true
},
{ eventName: 'keydown' },
)
onKeyStroke(
(e) => e.key === modifierKey,
(e) => {
e.preventDefault()
showTooltip.value = false
isKeyPressed.value = false
},
{ eventName: 'keyup' },
)
watch([isHovering, () => modifierKey, () => disabled], ([hovering, key, isDisabled]) => {
if (!hovering || isDisabled) {
showTooltip.value = false
return
}
// Show tooltip on mouseover if no modifier key is provided
if (val && !modifierKey) {
if (hovering && !key) {
showTooltip.value = true
return
}
// While hovering if the modifier key was changed and the key is not pressed, hide tooltip
if (hovering && key && !isKeyPressed.value) {
showTooltip.value = false
return
}
// When mouse leaves the element, then re-enters the element while key stays pressed, show the tooltip
if (!showTooltip.value && hovering && key && isKeyPressed.value) {
showTooltip.value = true
}
})
</script>
<template>
<a-tooltip v-model:visible="showTooltip" :trigger="[]">
<a-tooltip v-model:visible="showTooltip" :overlay-style="tooltipStyle" :trigger="[]">
<template #title>
<slot name="title" />
</template>
<div class="w-full" :class="wrapperClass" @mouseenter="isMouseOver = true" @mouseleave="isMouseOver = false">
<div ref="el" class="w-full" :class="$attrs.class" :style="$attrs.style">
<slot />
</div>
</a-tooltip>

57
packages/nc-gui/components/smartsheet/Cell.vue

@ -66,43 +66,6 @@ const syncValue = useDebounceFn(
500,
{ maxWait: 2000 },
)
const isAutoSaved = $computed(() => {
return [
UITypes.SingleLineText,
UITypes.LongText,
UITypes.PhoneNumber,
UITypes.Email,
UITypes.URL,
UITypes.Number,
UITypes.Decimal,
UITypes.Percent,
UITypes.Count,
UITypes.AutoNumber,
UITypes.SpecificDBType,
UITypes.Geometry,
UITypes.Duration,
].includes(column?.value?.uidt as UITypes)
})
const isManualSaved = $computed(() => [UITypes.Currency].includes(column?.value?.uidt as UITypes))
const vModel = computed({
get: () => props.modelValue,
set: (val) => {
if (val !== props.modelValue) {
currentRow.value.rowMeta.changed = true
emit('update:modelValue', val)
if (isAutoSaved) {
syncValue()
} else if (!isManualSaved) {
emit('save')
currentRow.value.rowMeta.changed = true
}
}
},
})
const {
isPrimary,
isURL,
@ -126,8 +89,26 @@ const {
isMultiSelect,
isPercent,
isPhoneNumber,
isAutoSaved,
isManualSaved,
} = useColumn(column)
const vModel = computed({
get: () => props.modelValue,
set: (val) => {
if (val !== props.modelValue) {
currentRow.value.rowMeta.changed = true
emit('update:modelValue', val)
if (isAutoSaved.value) {
syncValue()
} else if (!isManualSaved.value) {
emit('save')
currentRow.value.rowMeta.changed = true
}
}
},
})
const syncAndNavigate = (dir: NavigateDir, e: KeyboardEvent) => {
if (isJSON.value) return
@ -163,7 +144,7 @@ const syncAndNavigate = (dir: NavigateDir, e: KeyboardEvent) => {
<LazyCellUrl v-else-if="isURL" v-model="vModel" />
<LazyCellPhoneNumber v-else-if="isPhoneNumber" v-model="vModel" />
<LazyCellPercent v-else-if="isPercent" v-model="vModel" />
<LazyCellCurrency v-else-if="isCurrency" v-model="vModel" />
<LazyCellCurrency v-else-if="isCurrency" v-model="vModel" @save="emit('save')" />
<LazyCellDecimal v-else-if="isDecimal" v-model="vModel" />
<LazyCellInteger v-else-if="isInt" v-model="vModel" />
<LazyCellFloat v-else-if="isFloat" v-model="vModel" />

2
packages/nc-gui/components/smartsheet/Form.vue

@ -117,7 +117,7 @@ async function submitForm() {
if (e.errorFields.length) return
}
const insertedRowData = await insertRow(formState)
const insertedRowData = await insertRow({ row: formState, oldRow: {}, rowMeta: { new: true } })
if (insertedRowData) {
await syncLTARRefs(insertedRowData)

138
packages/nc-gui/components/smartsheet/Grid.vue

@ -88,6 +88,8 @@ const expandedFormDlg = ref(false)
const expandedFormRow = ref<Row>()
const expandedFormRowState = ref<Record<string, any>>()
const tbodyEl = ref<HTMLElement>()
const gridWrapper = ref<HTMLElement>()
const tableHead = ref<HTMLElement>()
const {
isLoading,
@ -107,6 +109,55 @@ const { getMeta } = useMetas()
const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useGridViewColumnWidth(view)
const getContainerScrollForElement = (
el: HTMLElement,
container: HTMLElement,
offset?: {
top?: number
bottom?: number
left?: number
right?: number
},
) => {
const childPos = el.getBoundingClientRect()
const parentPos = container.getBoundingClientRect()
const relativePos = {
top: childPos.top - parentPos.top,
right: childPos.right - parentPos.right,
bottom: childPos.bottom - parentPos.bottom,
left: childPos.left - parentPos.left,
}
const scroll = {
top: 0,
left: 0,
}
/*
* If the element is to the right of the container, scroll right (positive)
* If the element is to the left of the container, scroll left (negative)
*/
scroll.left =
relativePos.right + (offset?.right || 0) > 0
? container.scrollLeft + relativePos.right + (offset?.right || 0)
: relativePos.left - (offset?.left || 0) < 0
? container.scrollLeft + relativePos.left - (offset?.left || 0)
: container.scrollLeft
/*
* If the element is below the container, scroll down (positive)
* If the element is above the container, scroll up (negative)
*/
scroll.top =
relativePos.bottom + (offset?.bottom || 0) > 0
? container.scrollTop + relativePos.bottom + (offset?.bottom || 0)
: relativePos.top - (offset?.top || 0) < 0
? container.scrollTop + relativePos.top - (offset?.top || 0)
: container.scrollTop
return scroll
}
const { selectCell, selectBlock, selectedRange, clearRangeRows, startSelectRange, selected } = useMultiSelect(
fields,
data,
@ -114,16 +165,48 @@ const { selectCell, selectBlock, selectedRange, clearRangeRows, startSelectRange
isPkAvail,
clearCell,
makeEditable,
() => {
if (selected.row !== null && selected.col !== null) {
(row?: number | null, col?: number | null) => {
row = row ?? selected.row
col = col ?? selected.col
if (row !== undefined && col !== undefined && row !== null && col !== null) {
// get active cell
const td = tbodyEl.value?.querySelectorAll('tr')[selected.row]?.querySelectorAll('td')[selected.col + 1]
if (!td) return
const rows = tbodyEl.value?.querySelectorAll('tr')
const cols = rows?.[row].querySelectorAll('td')
const td = cols?.[col === 0 ? 0 : col + 1]
if (!td || !gridWrapper.value) return
const { height: headerHeight } = tableHead.value!.getBoundingClientRect()
const tdScroll = getContainerScrollForElement(td, gridWrapper.value, { top: headerHeight, bottom: 9, right: 9 })
if (rows && row === rows.length - 2) {
// if last row make 'Add New Row' visible
gridWrapper.value.scrollTo({
top: gridWrapper.value.scrollHeight,
left:
cols && col === cols.length - 2 // if corner cell
? gridWrapper.value.scrollWidth
: tdScroll.left,
behavior: 'smooth',
})
return
}
if (cols && col === cols.length - 2) {
// if last column make 'Add New Column' visible
gridWrapper.value.scrollTo({
top: tdScroll.top,
left: gridWrapper.value.scrollWidth,
behavior: 'smooth',
})
return
}
// scroll into the active cell
td.scrollIntoView({
gridWrapper.value.scrollTo({
top: tdScroll.top,
left: tdScroll.left,
behavior: 'smooth',
block: 'nearest',
inline: 'nearest',
})
}
},
@ -381,7 +464,7 @@ watch(
</div>
</general-overlay>
<div class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-dull">
<div ref="gridWrapper" class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-dull">
<a-dropdown
v-model:visible="contextMenu"
:trigger="isSqlView ? [] : ['contextmenu']"
@ -392,7 +475,7 @@ watch(
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white"
@contextmenu="showContextMenu"
>
<thead>
<thead ref="tableHead">
<tr class="nc-grid-header border-1 bg-gray-100 sticky top[-1px]">
<th>
<div class="w-full h-full bg-gray-100 flex min-w-[70px] pl-5 pr-1 items-center">
@ -479,24 +562,27 @@ watch(
</div>
<span class="flex-1" />
<div v-if="!readOnly && !isLocked" class="nc-expand" :class="{ 'nc-comment': row.rowMeta?.commentCount }">
<span
v-if="row.rowMeta?.commentCount"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
:style="{ backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length] }"
@click="expandForm(row, state)"
>
{{ row.rowMeta.commentCount }}
</span>
<div
v-else
class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:(bg-primary bg-opacity-10)"
>
<MdiArrowExpand
v-e="['c:row-expand']"
class="select-none transform hover:(text-accent scale-120) nc-row-expand"
<a-spin v-if="row.rowMeta.saving" class="!flex items-center" />
<template v-else>
<span
v-if="row.rowMeta?.commentCount"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
:style="{ backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length] }"
@click="expandForm(row, state)"
/>
</div>
>
{{ row.rowMeta.commentCount }}
</span>
<div
v-else
class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:(bg-primary bg-opacity-10)"
>
<MdiArrowExpand
v-e="['c:row-expand']"
class="select-none transform hover:(text-accent scale-120) nc-row-expand"
@click="expandForm(row, state)"
/>
</div>
</template>
</div>
</div>
</td>

36
packages/nc-gui/components/smartsheet/toolbar/Erd.vue

@ -1,14 +1,14 @@
<script lang="ts" setup>
interface Props {
modelValue: boolean
}
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
const meta = inject(MetaInj)
interface Props {
modelValue: boolean
}
const vModel = useVModel(props, 'modelValue', emits)
const selectedView = inject(ActiveViewInj)
@ -23,21 +23,31 @@ const selectedView = inject(ActiveViewInj)
:closable="false"
wrap-class-name="erd-single-table-modal"
transition-name="fade"
:destroy-on-close="true"
>
<div class="flex flex-row justify-between w-full items-center mb-1">
<a-typography-title class="ml-4 select-none" type="secondary" :level="5">
<div class="flex justify-between w-full items-start px-[24px] pt-6 pb-4 border-b-1">
<div class="select-none text-slate-500 font-semibold">
{{ `${$t('title.erdView')}: ${selectedView?.title}` }}
</a-typography-title>
</div>
<a-button type="text" class="!rounded-md border-none -mt-1.5 -mr-1" @click="vModel = false">
<template #icon>
<MdiClose class="cursor-pointer mt-1 nc-modal-close" />
</template>
</a-button>
<div class="flex h-full items-center justify-center rounded group" @click="vModel = false">
<MdiClose class="cursor-pointer mt-1 nc-modal-close group-hover:text-accent text-opacity-100" />
</div>
</div>
<div class="w-full h-full !py-0 !px-2" style="height: 70vh">
<div class="w-full h-70vh">
<LazyErdView :table="meta" />
</div>
</a-modal>
</template>
<style>
.erd-single-table-modal {
.ant-modal {
@apply !top-[50px];
}
.ant-modal-body {
@apply !p-0;
}
}
</style>

7
packages/nc-gui/components/webhook/Editor.vue

@ -543,7 +543,12 @@ onMounted(loadPluginList)
<a-col :span="24">
<a-tabs v-model:activeKey="urlTabKey" type="card" closeable="false" class="shadow-sm">
<a-tab-pane key="body" tab="Body">
<LazyMonacoEditor v-model="hook.notification.payload.body" :validate="false" class="min-h-60 max-h-80" />
<LazyMonacoEditor
v-model="hook.notification.payload.body"
disable-deep-compare
:validate="false"
class="min-h-60 max-h-80"
/>
</a-tab-pane>
<a-tab-pane key="params" tab="Params" force-render>

5
packages/nc-gui/composables/useColumn.ts

@ -56,11 +56,10 @@ export function useColumn(column: Ref<ColumnType | undefined>) {
UITypes.AutoNumber,
UITypes.SpecificDBType,
UITypes.Geometry,
UITypes.Duration,
].includes(uiDatatype.value),
)
const isManualSaved = computed(() =>
[UITypes.Currency, UITypes.Year, UITypes.Time, UITypes.Duration].includes(uiDatatype.value),
)
const isManualSaved = computed(() => [UITypes.Currency].includes(uiDatatype.value))
const isPrimary = computed(() => column.value?.pv)
return {

2
packages/nc-gui/composables/useMultiSelect/index.ts

@ -17,7 +17,7 @@ export function useMultiSelect(
isPkAvail: MaybeRef<boolean>,
clearCell: Function,
makeEditable: Function,
scrollToActiveCell?: () => void,
scrollToActiveCell?: (row?: number | null, col?: number | null) => void,
) {
const { t } = useI18n()

23
packages/nc-gui/composables/useViewData.ts

@ -11,6 +11,7 @@ import {
message,
populateInsertObject,
ref,
until,
useApi,
useGlobal,
useI18n,
@ -200,11 +201,13 @@ export function useViewData(
}
async function insertRow(
row: Record<string, any>,
rowIndex = formattedData.value?.length,
currentRow: Row,
_rowIndex = formattedData.value?.length,
ltarState: Record<string, any> = {},
{ metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {},
) {
const row = currentRow.row
if (currentRow.rowMeta) currentRow.rowMeta.saving = true
try {
const { missingRequiredColumns, insertObj } = await populateInsertObject({
meta: metaValue!,
@ -223,9 +226,9 @@ export function useViewData(
insertObj,
)
formattedData.value?.splice(rowIndex ?? 0, 1, {
row: insertedData,
rowMeta: {},
Object.assign(currentRow, {
row: { ...insertedData, ...row },
rowMeta: { ...(currentRow.rowMeta || {}), new: undefined },
oldRow: { ...insertedData },
})
@ -233,6 +236,8 @@ export function useViewData(
return insertedData
} catch (error: any) {
message.error(await extractSdkResponseErrorMsg(error))
} finally {
if (currentRow.rowMeta) currentRow.rowMeta.saving = false
}
}
@ -241,6 +246,7 @@ export function useViewData(
property: string,
{ metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {},
) {
if (toUpdate.rowMeta) toUpdate.rowMeta.saving = true
try {
const id = extractPkFromRow(toUpdate.row, meta.value?.columns as ColumnType[])
@ -272,6 +278,8 @@ export function useViewData(
Object.assign(toUpdate.oldRow, updatedRowData)
} catch (e: any) {
message.error(`${t('msg.error.rowUpdateFailed')} ${await extractSdkResponseErrorMsg(e)}`)
} finally {
if (toUpdate.rowMeta) toUpdate.rowMeta.saving = false
}
}
@ -281,8 +289,11 @@ export function useViewData(
ltarState?: Record<string, any>,
args: { metaValue?: TableType; viewMetaValue?: ViewType } = {},
) {
// if new row and save is in progress then wait until the save is complete
await until(() => !(row.rowMeta?.new && row.rowMeta?.saving)).toMatch((v) => v)
if (row.rowMeta.new) {
return await insertRow(row.row, formattedData.value.indexOf(row), ltarState, args)
return await insertRow(row, formattedData.value.indexOf(row), ltarState, args)
} else {
await updateRowProperty(row, property!, args)
}

1
packages/nc-gui/lib/types.ts

@ -57,6 +57,7 @@ export interface Row {
selected?: boolean
commentCount?: number
changed?: boolean
saving?: boolean
}
}

644
packages/nc-gui/package-lock.json generated

@ -8,12 +8,13 @@
"dependencies": {
"@ckpack/vue-color": "^1.2.0",
"@vue-flow/additional-components": "^1.1.0",
"@vue-flow/core": "^1.1.1",
"@vue-flow/core": "^1.1.3",
"@vuelidate/core": "^2.0.0-alpha.44",
"@vuelidate/validators": "^2.0.0-alpha.31",
"@vueuse/core": "^9.0.2",
"@vueuse/integrations": "^9.0.2",
"ant-design-vue": "^3.2.11",
"d3-scale": "^4.0.2",
"dagre": "^0.8.5",
"dayjs": "^1.11.3",
"file-saver": "^2.0.5",
@ -56,6 +57,7 @@
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@nuxt/image-edge": "^1.0.0-27657146.da85542",
"@types/axios": "^0.14.0",
"@types/d3": "^7.4.0",
"@types/dagre": "^0.7.48",
"@types/file-saver": "^2.0.5",
"@types/papaparse": "^5.3.2",
@ -2904,6 +2906,259 @@
"@types/node": "*"
}
},
"node_modules/@types/d3": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.0.tgz",
"integrity": "sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==",
"dev": true,
"dependencies": {
"@types/d3-array": "*",
"@types/d3-axis": "*",
"@types/d3-brush": "*",
"@types/d3-chord": "*",
"@types/d3-color": "*",
"@types/d3-contour": "*",
"@types/d3-delaunay": "*",
"@types/d3-dispatch": "*",
"@types/d3-drag": "*",
"@types/d3-dsv": "*",
"@types/d3-ease": "*",
"@types/d3-fetch": "*",
"@types/d3-force": "*",
"@types/d3-format": "*",
"@types/d3-geo": "*",
"@types/d3-hierarchy": "*",
"@types/d3-interpolate": "*",
"@types/d3-path": "*",
"@types/d3-polygon": "*",
"@types/d3-quadtree": "*",
"@types/d3-random": "*",
"@types/d3-scale": "*",
"@types/d3-scale-chromatic": "*",
"@types/d3-selection": "*",
"@types/d3-shape": "*",
"@types/d3-time": "*",
"@types/d3-time-format": "*",
"@types/d3-timer": "*",
"@types/d3-transition": "*",
"@types/d3-zoom": "*"
}
},
"node_modules/@types/d3-array": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz",
"integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==",
"dev": true
},
"node_modules/@types/d3-axis": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.1.tgz",
"integrity": "sha512-zji/iIbdd49g9WN0aIsGcwcTBUkgLsCSwB+uH+LPVDAiKWENMtI3cJEWt+7/YYwelMoZmbBfzA3qCdrZ2XFNnw==",
"dev": true,
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-brush": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.1.tgz",
"integrity": "sha512-B532DozsiTuQMHu2YChdZU0qsFJSio3Q6jmBYGYNp3gMDzBmuFFgPt9qKA4VYuLZMp4qc6eX7IUFUEsvHiXZAw==",
"dev": true,
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-chord": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.1.tgz",
"integrity": "sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw==",
"dev": true
},
"node_modules/@types/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==",
"dev": true
},
"node_modules/@types/d3-contour": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.1.tgz",
"integrity": "sha512-C3zfBrhHZvrpAAK3YXqLWVAGo87A4SvJ83Q/zVJ8rFWJdKejUnDYaWZPkA8K84kb2vDA/g90LTQAz7etXcgoQQ==",
"dev": true,
"dependencies": {
"@types/d3-array": "*",
"@types/geojson": "*"
}
},
"node_modules/@types/d3-delaunay": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz",
"integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==",
"dev": true
},
"node_modules/@types/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-NhxMn3bAkqhjoxabVJWKryhnZXXYYVQxaBnbANu0O94+O/nX9qSjrA1P1jbAQJxJf+VC72TxDX/YJcKue5bRqw==",
"dev": true
},
"node_modules/@types/d3-drag": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.1.tgz",
"integrity": "sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA==",
"dev": true,
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-dsv": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.0.tgz",
"integrity": "sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A==",
"dev": true
},
"node_modules/@types/d3-ease": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz",
"integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==",
"dev": true
},
"node_modules/@types/d3-fetch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.1.tgz",
"integrity": "sha512-toZJNOwrOIqz7Oh6Q7l2zkaNfXkfR7mFSJvGvlD/Ciq/+SQ39d5gynHJZ/0fjt83ec3WL7+u3ssqIijQtBISsw==",
"dev": true,
"dependencies": {
"@types/d3-dsv": "*"
}
},
"node_modules/@types/d3-force": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.3.tgz",
"integrity": "sha512-z8GteGVfkWJMKsx6hwC3SiTSLspL98VNpmvLpEFJQpZPq6xpA1I8HNBDNSpukfK0Vb0l64zGFhzunLgEAcBWSA==",
"dev": true
},
"node_modules/@types/d3-format": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz",
"integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==",
"dev": true
},
"node_modules/@types/d3-geo": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.2.tgz",
"integrity": "sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ==",
"dev": true,
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/d3-hierarchy": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.0.tgz",
"integrity": "sha512-g+sey7qrCa3UbsQlMZZBOHROkFqx7KZKvUpRzI/tAp/8erZWpYq7FgNKvYwebi2LaEiVs1klhUfd3WCThxmmWQ==",
"dev": true
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==",
"dev": true,
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz",
"integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==",
"dev": true
},
"node_modules/@types/d3-polygon": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz",
"integrity": "sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==",
"dev": true
},
"node_modules/@types/d3-quadtree": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz",
"integrity": "sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==",
"dev": true
},
"node_modules/@types/d3-random": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz",
"integrity": "sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==",
"dev": true
},
"node_modules/@types/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==",
"dev": true,
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-scale-chromatic": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz",
"integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==",
"dev": true
},
"node_modules/@types/d3-selection": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.3.tgz",
"integrity": "sha512-Mw5cf6nlW1MlefpD9zrshZ+DAWL4IQ5LnWfRheW6xwsdaWOb6IRRu2H7XPAQcyXEx1D7XQWgdoKR83ui1/HlEA==",
"dev": true
},
"node_modules/@types/d3-shape": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.0.tgz",
"integrity": "sha512-jYIYxFFA9vrJ8Hd4Se83YI6XF+gzDL1aC5DCsldai4XYYiVNdhtpGbA/GM6iyQ8ayhSp3a148LY34hy7A4TxZA==",
"dev": true,
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz",
"integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==",
"dev": true
},
"node_modules/@types/d3-time-format": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz",
"integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==",
"dev": true
},
"node_modules/@types/d3-timer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz",
"integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==",
"dev": true
},
"node_modules/@types/d3-transition": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.2.tgz",
"integrity": "sha512-jo5o/Rf+/u6uerJ/963Dc39NI16FQzqwOc54bwvksGAdVfvDrqDpVeq95bEvPtBwLCVZutAEyAtmSyEMxN7vxQ==",
"dev": true,
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-zoom": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz",
"integrity": "sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ==",
"dev": true,
"dependencies": {
"@types/d3-interpolate": "*",
"@types/d3-selection": "*"
}
},
"node_modules/@types/dagre": {
"version": "0.7.48",
"resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.48.tgz",
@ -2955,6 +3210,12 @@
"@types/node": "*"
}
},
"node_modules/@types/geojson": {
"version": "7946.0.10",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==",
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
@ -3289,9 +3550,9 @@
}
},
"node_modules/@vue-flow/core": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.1.1.tgz",
"integrity": "sha512-zXEmZl9Rxrpi9+EzQoa//u0+pCzStwlISGdWw/WjXvVrBBfg6goW20k66TjHoJbzK3yNRN5b4lBBR0lVWZh/0w==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.1.3.tgz",
"integrity": "sha512-MuJjWLexkZ5RiMY/LmuyRZXiXKo8ttaKSPk02RYP8SoWVj6Kr0XglWh6FJdQE0bQhqpwsXBka+4EQrI4B/ueSw==",
"dependencies": {
"@vueuse/core": "^9.3.0",
"d3-drag": "^3.0.0",
@ -5824,6 +6085,17 @@
"integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==",
"dev": true
},
"node_modules/d3-array": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz",
"integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
@ -5860,6 +6132,14 @@
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
@ -5871,6 +6151,21 @@
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
@ -5879,6 +6174,28 @@
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz",
"integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
@ -9446,6 +9763,14 @@
"node": ">= 0.4"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"engines": {
"node": ">=12"
}
},
"node_modules/ioredis": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.2.3.tgz",
@ -18684,6 +19009,259 @@
"@types/node": "*"
}
},
"@types/d3": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.0.tgz",
"integrity": "sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==",
"dev": true,
"requires": {
"@types/d3-array": "*",
"@types/d3-axis": "*",
"@types/d3-brush": "*",
"@types/d3-chord": "*",
"@types/d3-color": "*",
"@types/d3-contour": "*",
"@types/d3-delaunay": "*",
"@types/d3-dispatch": "*",
"@types/d3-drag": "*",
"@types/d3-dsv": "*",
"@types/d3-ease": "*",
"@types/d3-fetch": "*",
"@types/d3-force": "*",
"@types/d3-format": "*",
"@types/d3-geo": "*",
"@types/d3-hierarchy": "*",
"@types/d3-interpolate": "*",
"@types/d3-path": "*",
"@types/d3-polygon": "*",
"@types/d3-quadtree": "*",
"@types/d3-random": "*",
"@types/d3-scale": "*",
"@types/d3-scale-chromatic": "*",
"@types/d3-selection": "*",
"@types/d3-shape": "*",
"@types/d3-time": "*",
"@types/d3-time-format": "*",
"@types/d3-timer": "*",
"@types/d3-transition": "*",
"@types/d3-zoom": "*"
}
},
"@types/d3-array": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz",
"integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==",
"dev": true
},
"@types/d3-axis": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.1.tgz",
"integrity": "sha512-zji/iIbdd49g9WN0aIsGcwcTBUkgLsCSwB+uH+LPVDAiKWENMtI3cJEWt+7/YYwelMoZmbBfzA3qCdrZ2XFNnw==",
"dev": true,
"requires": {
"@types/d3-selection": "*"
}
},
"@types/d3-brush": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.1.tgz",
"integrity": "sha512-B532DozsiTuQMHu2YChdZU0qsFJSio3Q6jmBYGYNp3gMDzBmuFFgPt9qKA4VYuLZMp4qc6eX7IUFUEsvHiXZAw==",
"dev": true,
"requires": {
"@types/d3-selection": "*"
}
},
"@types/d3-chord": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.1.tgz",
"integrity": "sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw==",
"dev": true
},
"@types/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==",
"dev": true
},
"@types/d3-contour": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.1.tgz",
"integrity": "sha512-C3zfBrhHZvrpAAK3YXqLWVAGo87A4SvJ83Q/zVJ8rFWJdKejUnDYaWZPkA8K84kb2vDA/g90LTQAz7etXcgoQQ==",
"dev": true,
"requires": {
"@types/d3-array": "*",
"@types/geojson": "*"
}
},
"@types/d3-delaunay": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz",
"integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==",
"dev": true
},
"@types/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-NhxMn3bAkqhjoxabVJWKryhnZXXYYVQxaBnbANu0O94+O/nX9qSjrA1P1jbAQJxJf+VC72TxDX/YJcKue5bRqw==",
"dev": true
},
"@types/d3-drag": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.1.tgz",
"integrity": "sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA==",
"dev": true,
"requires": {
"@types/d3-selection": "*"
}
},
"@types/d3-dsv": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.0.tgz",
"integrity": "sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A==",
"dev": true
},
"@types/d3-ease": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz",
"integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==",
"dev": true
},
"@types/d3-fetch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.1.tgz",
"integrity": "sha512-toZJNOwrOIqz7Oh6Q7l2zkaNfXkfR7mFSJvGvlD/Ciq/+SQ39d5gynHJZ/0fjt83ec3WL7+u3ssqIijQtBISsw==",
"dev": true,
"requires": {
"@types/d3-dsv": "*"
}
},
"@types/d3-force": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.3.tgz",
"integrity": "sha512-z8GteGVfkWJMKsx6hwC3SiTSLspL98VNpmvLpEFJQpZPq6xpA1I8HNBDNSpukfK0Vb0l64zGFhzunLgEAcBWSA==",
"dev": true
},
"@types/d3-format": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz",
"integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==",
"dev": true
},
"@types/d3-geo": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.2.tgz",
"integrity": "sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ==",
"dev": true,
"requires": {
"@types/geojson": "*"
}
},
"@types/d3-hierarchy": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.0.tgz",
"integrity": "sha512-g+sey7qrCa3UbsQlMZZBOHROkFqx7KZKvUpRzI/tAp/8erZWpYq7FgNKvYwebi2LaEiVs1klhUfd3WCThxmmWQ==",
"dev": true
},
"@types/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==",
"dev": true,
"requires": {
"@types/d3-color": "*"
}
},
"@types/d3-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz",
"integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==",
"dev": true
},
"@types/d3-polygon": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz",
"integrity": "sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==",
"dev": true
},
"@types/d3-quadtree": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz",
"integrity": "sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==",
"dev": true
},
"@types/d3-random": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz",
"integrity": "sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==",
"dev": true
},
"@types/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==",
"dev": true,
"requires": {
"@types/d3-time": "*"
}
},
"@types/d3-scale-chromatic": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz",
"integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==",
"dev": true
},
"@types/d3-selection": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.3.tgz",
"integrity": "sha512-Mw5cf6nlW1MlefpD9zrshZ+DAWL4IQ5LnWfRheW6xwsdaWOb6IRRu2H7XPAQcyXEx1D7XQWgdoKR83ui1/HlEA==",
"dev": true
},
"@types/d3-shape": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.0.tgz",
"integrity": "sha512-jYIYxFFA9vrJ8Hd4Se83YI6XF+gzDL1aC5DCsldai4XYYiVNdhtpGbA/GM6iyQ8ayhSp3a148LY34hy7A4TxZA==",
"dev": true,
"requires": {
"@types/d3-path": "*"
}
},
"@types/d3-time": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz",
"integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==",
"dev": true
},
"@types/d3-time-format": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz",
"integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==",
"dev": true
},
"@types/d3-timer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz",
"integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==",
"dev": true
},
"@types/d3-transition": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.2.tgz",
"integrity": "sha512-jo5o/Rf+/u6uerJ/963Dc39NI16FQzqwOc54bwvksGAdVfvDrqDpVeq95bEvPtBwLCVZutAEyAtmSyEMxN7vxQ==",
"dev": true,
"requires": {
"@types/d3-selection": "*"
}
},
"@types/d3-zoom": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz",
"integrity": "sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ==",
"dev": true,
"requires": {
"@types/d3-interpolate": "*",
"@types/d3-selection": "*"
}
},
"@types/dagre": {
"version": "0.7.48",
"resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.48.tgz",
@ -18735,6 +19313,12 @@
"@types/node": "*"
}
},
"@types/geojson": {
"version": "7946.0.10",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==",
"dev": true
},
"@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
@ -18967,9 +19551,9 @@
"requires": {}
},
"@vue-flow/core": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.1.1.tgz",
"integrity": "sha512-zXEmZl9Rxrpi9+EzQoa//u0+pCzStwlISGdWw/WjXvVrBBfg6goW20k66TjHoJbzK3yNRN5b4lBBR0lVWZh/0w==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.1.3.tgz",
"integrity": "sha512-MuJjWLexkZ5RiMY/LmuyRZXiXKo8ttaKSPk02RYP8SoWVj6Kr0XglWh6FJdQE0bQhqpwsXBka+4EQrI4B/ueSw==",
"requires": {
"@vueuse/core": "^9.3.0",
"d3-drag": "^3.0.0",
@ -20839,6 +21423,14 @@
"integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==",
"dev": true
},
"d3-array": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz",
"integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==",
"requires": {
"internmap": "1 - 2"
}
},
"d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
@ -20863,6 +21455,11 @@
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="
},
"d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="
},
"d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
@ -20871,11 +21468,39 @@
"d3-color": "1 - 3"
}
},
"d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"requires": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
}
},
"d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="
},
"d3-time": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz",
"integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==",
"requires": {
"d3-array": "2 - 3"
}
},
"d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"requires": {
"d3-time": "1 - 3"
}
},
"d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
@ -23445,6 +24070,11 @@
"side-channel": "^1.0.4"
}
},
"internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="
},
"ioredis": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.2.3.tgz",

4
packages/nc-gui/package.json

@ -17,12 +17,13 @@
"dependencies": {
"@ckpack/vue-color": "^1.2.0",
"@vue-flow/additional-components": "^1.1.0",
"@vue-flow/core": "^1.1.1",
"@vue-flow/core": "^1.1.3",
"@vuelidate/core": "^2.0.0-alpha.44",
"@vuelidate/validators": "^2.0.0-alpha.31",
"@vueuse/core": "^9.0.2",
"@vueuse/integrations": "^9.0.2",
"ant-design-vue": "^3.2.11",
"d3-scale": "^4.0.2",
"dagre": "^0.8.5",
"dayjs": "^1.11.3",
"file-saver": "^2.0.5",
@ -65,6 +66,7 @@
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@nuxt/image-edge": "^1.0.0-27657146.da85542",
"@types/axios": "^0.14.0",
"@types/d3": "^7.4.0",
"@types/dagre": "^0.7.48",
"@types/file-saver": "^2.0.5",
"@types/papaparse": "^5.3.2",

9
packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue

@ -22,6 +22,7 @@ watch(
until(tables)
.toMatch((tables) => tables.length > 0)
.then(() => {
loading.value = true
getMeta(tableTitle as string, true).finally(() => (loading.value = false))
})
},
@ -30,7 +31,13 @@ watch(
</script>
<template>
<div class="w-full h-full">
<div class="w-full h-full relative">
<LazyTabsSmartsheet :active-tab="activeTab" />
<general-overlay :model-value="loading" inline transition class="!bg-opacity-15">
<div class="flex items-center justify-center h-full w-full !bg-white !bg-opacity-85 z-1000">
<a-spin size="large" />
</div>
</general-overlay>
</div>
</template>

52
packages/nc-gui/pages/[projectType]/form/[viewId]/index.vue

@ -68,30 +68,48 @@ p {
}
.nc-form-view {
.nc-input {
@apply w-full rounded p-2 min-h-[40px] flex items-center border-solid border-1 border-gray-300 dark:border-slate-200;
.nc-cell {
&.nc-cell-checkbox {
@apply color-transition !border-0;
input,
&.nc-virtual-cell,
> div {
@apply bg-white dark:(bg-slate-500 text-white);
.ant-btn {
@apply dark:(bg-slate-300);
.nc-icon {
@apply !text-2xl;
}
.chip {
@apply dark:(bg-slate-700 text-white);
.nc-cell-hover-show {
opacity: 100 !important;
div {
background-color: transparent !important;
}
}
}
}
}
.nc-cell {
@apply bg-white dark:bg-slate-500;
&:not(.nc-cell-checkbox) {
@apply bg-white dark:bg-slate-500;
&.nc-input {
@apply w-full rounded p-2 min-h-[40px] flex items-center border-solid border-1 border-gray-300 dark:border-slate-200;
input,
&.nc-virtual-cell,
> div {
@apply bg-white dark:(bg-slate-500 text-white);
.nc-attachment-cell > div {
@apply dark:(bg-slate-100);
.ant-btn {
@apply dark:(bg-slate-300);
}
.chip {
@apply dark:(bg-slate-700 text-white);
}
}
}
.nc-attachment-cell > div {
@apply dark:(bg-slate-100);
}
}
}
}

8
packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue

@ -20,9 +20,9 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
</script>
<template>
<div>
<div class="h-full flex flex-col items-center">
<div
class="color-transition relative flex flex-col justify-center gap-2 w-full max-w-[max(33%,600px)] m-auto py-4 pb-8 px-16 md:(bg-white dark:bg-slate-700 rounded-lg border-1 border-gray-200 shadow-xl) mt-12"
class="color-transition relative flex flex-col justify-center gap-2 w-full max-w-[max(33%,600px)] m-auto py-4 pb-8 px-16 md:(bg-white dark:bg-slate-700 rounded-lg border-1 border-gray-200 shadow-xl)"
>
<template v-if="sharedFormView">
<h1 class="prose-2xl font-bold self-center my-4">{{ sharedFormView.heading }}</h1>
@ -120,6 +120,8 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
</template>
</div>
<GeneralPoweredBy />
<div class="flex items-end">
<GeneralPoweredBy />
</div>
</div>
</template>

41
packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue

@ -195,7 +195,7 @@ onMounted(() => {
</script>
<template>
<div ref="el" class="pt-8 md:p-0 w-full h-full flex flex-col">
<div ref="el" class="survey pt-8 md:p-0 w-full h-full flex flex-col">
<div
v-if="sharedFormView"
style="height: max(40vh, 225px); min-height: 225px"
@ -232,6 +232,7 @@ onMounted(() => {
<LazySmartsheetHeaderCell
v-else
:class="field.uidt === UITypes.Checkbox ? 'nc-form-column-label__checkbox' : ''"
:column="{ ...field, title: field.label || field.title }"
:required="isRequired(field, field.required)"
:hide-menu="true"
@ -260,7 +261,11 @@ onMounted(() => {
{{ error.$message }}
</div>
<div class="block text-[14px]" data-cy="nc-survey-form__field-description">
<div
class="block text-[14px]"
:class="field.uidt === UITypes.Checkbox ? 'text-center' : ''"
data-cy="nc-survey-form__field-description"
>
{{ field.description }}
</div>
@ -425,21 +430,33 @@ onMounted(() => {
@apply overscroll-x-none;
}
.nc-form-column-label {
> * {
@apply !prose-lg;
.survey {
.nc-form-column-label {
> * {
@apply !prose-lg;
}
.nc-icon {
@apply mr-2;
}
}
.nc-icon {
@apply mr-2;
.nc-form-column-label__checkbox {
@apply flex items-center justify-center gap-2 text-left;
}
}
.nc-input {
@apply appearance-none w-full rounded px-2 py-2 my-2 border-solid border-1 border-primary border-opacity-50;
.nc-input {
@apply appearance-none w-full rounded px-2 py-2 my-2 border-solid border-1 border-primary border-opacity-50;
&.nc-cell-checkbox {
> * {
@apply justify-center flex items-center;
}
}
input {
@apply !py-1 !px-1;
input {
@apply !py-1 !px-1;
}
}
}
</style>

5
packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts

@ -944,7 +944,7 @@ export class MysqlUi {
}
}
static getDataTypeForUiType(col: { uidt: UITypes }, idType?: IDType) {
static getDataTypeForUiType(col: { uidt: UITypes, dtxp?: string, colOptions?: any }, idType?: IDType) {
const colProp: any = {};
switch (col.uidt) {
case 'ID':
@ -977,6 +977,9 @@ export class MysqlUi {
break;
case 'MultiSelect':
colProp.dt = 'set';
if (col.colOptions?.options.length > 64 || col.dtxp?.split(',').length > 64) {
colProp.dt = 'text';
}
break;
case 'SingleSelect':
colProp.dt = 'enum';

12
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts

@ -1423,7 +1423,7 @@ class BaseModelSqlv2 {
// const driver = trx ? trx : this.dbDriver;
const query = this.dbDriver(this.tnPath).insert(insertObj);
if (this.isPg || this.isMssql) {
if ((this.isPg || this.isMssql) && this.model.primaryKey) {
query.returning(
`${this.model.primaryKey.column_name} as ${this.model.primaryKey.title}`
);
@ -1431,7 +1431,15 @@ class BaseModelSqlv2 {
}
const ai = this.model.columns.find((c) => c.ai);
if (
let ag: Column;
if (!ai) ag = this.model.columns.find((c) => c.meta?.ag);
// handle if autogenerated primary key is used
if (ag) {
if (!response) await this.extractRawQueryAndExec(query);
response = await this.readByPk(data[ag.title]);
} else if (
!response ||
(typeof response?.[0] !== 'object' && response?.[0] !== null)
) {

123
packages/nocodb/src/lib/meta/api/columnApis.ts

@ -605,6 +605,12 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
) {
colBody.dtxp = "''";
}
if (colBody.dt === 'set') {
if (colBody.colOptions?.options.length > 64) {
colBody.dt = 'text';
}
}
}
}
@ -901,6 +907,12 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
) {
colBody.dtxp = "''";
}
if (colBody.dt === 'set') {
if (colBody.colOptions?.options.length > 64) {
colBody.dt = 'text';
}
}
}
// Handle option delete
@ -935,17 +947,30 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
}
} else if (column.uidt === UITypes.MultiSelect) {
if (driverType === 'mysql' || driverType === 'mysql2') {
await dbDriver.raw(
`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), ',')) WHERE FIND_IN_SET(?, ??)`,
[
table.table_name,
column.column_name,
column.column_name,
option.title,
option.title,
column.column_name,
]
);
if (colBody.dt === 'set') {
await dbDriver.raw(
`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), ',')) WHERE FIND_IN_SET(?, ??)`,
[
table.table_name,
column.column_name,
column.column_name,
option.title,
option.title,
column.column_name,
]
);
} else {
await dbDriver.raw(
`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), ','))`,
[
table.table_name,
column.column_name,
column.column_name,
option.title,
]
);
}
} else if (driverType === 'pg') {
await dbDriver.raw(
`UPDATE ?? SET ?? = array_to_string(array_remove(string_to_array(??, ','), ?), ',')`,
@ -1101,18 +1126,31 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
}
} else if (column.uidt === UITypes.MultiSelect) {
if (driverType === 'mysql' || driverType === 'mysql2') {
await dbDriver.raw(
`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ','))) WHERE FIND_IN_SET(?, ??)`,
[
table.table_name,
column.column_name,
column.column_name,
option.title,
newOp.title,
option.title,
column.column_name,
]
);
if (colBody.dt === 'set') {
await dbDriver.raw(
`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ','))) WHERE FIND_IN_SET(?, ??)`,
[
table.table_name,
column.column_name,
column.column_name,
option.title,
newOp.title,
option.title,
column.column_name,
]
);
} else {
await dbDriver.raw(
`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ',')))`,
[
table.table_name,
column.column_name,
column.column_name,
option.title,
newOp.title,
]
);
}
} else if (driverType === 'pg') {
await dbDriver.raw(
`UPDATE ?? SET ?? = array_to_string(array_replace(string_to_array(??, ','), ?, ?), ',')`,
@ -1174,18 +1212,33 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
}
} else if (column.uidt === UITypes.MultiSelect) {
if (driverType === 'mysql' || driverType === 'mysql2') {
await dbDriver.raw(
`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ','))) WHERE FIND_IN_SET(?, ??)`,
[
table.table_name,
column.column_name,
column.column_name,
ch.temp_title,
newOp.title,
ch.temp_title,
column.column_name,
]
);
if (colBody.dt === 'set') {
await dbDriver.raw(
`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ','))) WHERE FIND_IN_SET(?, ??)`,
[
table.table_name,
column.column_name,
column.column_name,
ch.temp_title,
newOp.title,
ch.temp_title,
column.column_name,
]
);
} else {
await dbDriver.raw(
`UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ',')))`,
[
table.table_name,
column.column_name,
column.column_name,
ch.temp_title,
newOp.title,
ch.temp_title,
column.column_name,
]
);
}
} else if (driverType === 'pg') {
await dbDriver.raw(
`UPDATE ?? SET ?? = array_to_string(array_replace(string_to_array(??, ','), ?, ?), ',')`,

16
packages/nocodb/src/lib/meta/api/sync/helpers/job.ts

@ -446,7 +446,7 @@ export default async (
// TODO fix record mapping (this causes every record to map first option, we can't handle them using data api as they don't provide option id within data we might instead get the correct mapping from schema file )
let dupNo = 1;
const defaultName = (value as any).name;
while (options.find((el) => el.title === (value as any).name)) {
while (options.find((el) => el.title.toLowerCase() === (value as any).name.toLowerCase())) {
(value as any).name = `${defaultName}_${dupNo++}`;
}
options.push({
@ -577,11 +577,15 @@ export default async (
ncCol.colOptions = {
options: [...colOptions.data],
};
// if options are empty, configure '' as default option
ncCol.dtxp =
colOptions.data
.map((el) => `'${el.title.replace(/'/gi, "''")}'`)
.join(',') || "''";
if (['mysql', 'mysql2'].includes(getRootDbType())) {
// if options are empty, configure '' as an option
ncCol.dtxp =
colOptions.data
.map((el) => `'${el.title.replace(/'/gi, "''")}'`)
.join(',') || "''";
}
break;
case undefined:
break;

Loading…
Cancel
Save