Browse Source

feat: team & settings modal data sources tab revised

Signed-off-by: mertmit <mertmit99@gmail.com>
pull/3573/head
mertmit 2 years ago
parent
commit
5c433e3086
  1. 7
      packages/nc-gui/components.d.ts
  2. 4
      packages/nc-gui/components/cell/DateTimePicker.vue
  3. 2
      packages/nc-gui/components/cell/MultiSelect.vue
  4. 6
      packages/nc-gui/components/cell/TimePicker.vue
  5. 16
      packages/nc-gui/components/dashboard/TreeView.vue
  6. 3
      packages/nc-gui/components/dashboard/settings/AppStore.vue
  7. 142
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  8. 10
      packages/nc-gui/components/dashboard/settings/Erd.vue
  9. 10
      packages/nc-gui/components/dashboard/settings/Metadata.vue
  10. 127
      packages/nc-gui/components/dashboard/settings/Modal.vue
  11. 11
      packages/nc-gui/components/dashboard/settings/UIAcl.vue
  12. 9
      packages/nc-gui/components/dlg/TableRename.vue
  13. 19
      packages/nc-gui/components/erd/View.vue
  14. 21
      packages/nc-gui/components/general/AddBaseButton.vue
  15. 4
      packages/nc-gui/components/smartsheet/column/CurrencyOptions.vue
  16. 2
      packages/nc-gui/components/virtual-cell/Formula.vue
  17. 11
      packages/nc-gui/composables/useColumnCreateStore.ts
  18. 8
      packages/nc-gui/composables/useProject.ts
  19. 1
      packages/nc-gui/context/index.ts
  20. 9
      packages/nc-gui/lib/enums.ts
  21. 13
      packages/nc-gui/pages/[projectType]/[projectId]/index.vue
  22. 7
      packages/nocodb/src/lib/meta/api/metaDiffApis.ts

7
packages/nc-gui/components.d.ts vendored

@ -10,6 +10,8 @@ declare module '@vue/runtime-core' {
AAlert: typeof import('ant-design-vue/es')['Alert'] AAlert: typeof import('ant-design-vue/es')['Alert']
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete'] AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
ABadgeRibbon: typeof import('ant-design-vue/es')['BadgeRibbon'] ABadgeRibbon: typeof import('ant-design-vue/es')['BadgeRibbon']
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
AButton: typeof import('ant-design-vue/es')['Button'] AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card'] ACard: typeof import('ant-design-vue/es')['Card']
ACardMeta: typeof import('ant-design-vue/es')['CardMeta'] ACardMeta: typeof import('ant-design-vue/es')['CardMeta']
@ -65,7 +67,6 @@ declare module '@vue/runtime-core' {
ATabs: typeof import('ant-design-vue/es')['Tabs'] ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag'] ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea'] ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip'] ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle'] ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger'] AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
@ -144,7 +145,9 @@ declare module '@vue/runtime-core' {
MdiContentSave: typeof import('~icons/mdi/content-save')['default'] MdiContentSave: typeof import('~icons/mdi/content-save')['default']
MdiContentSaveEdit: typeof import('~icons/mdi/content-save-edit')['default'] MdiContentSaveEdit: typeof import('~icons/mdi/content-save-edit')['default']
MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default'] MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default']
MdiDatabaseLockOutline: typeof import('~icons/mdi/database-lock-outline')['default']
MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['default'] MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['default']
MdiDatabasePlusOutline: typeof import('~icons/mdi/database-plus-outline')['default']
MdiDatabaseSync: typeof import('~icons/mdi/database-sync')['default'] MdiDatabaseSync: typeof import('~icons/mdi/database-sync')['default']
MdiDelete: typeof import('~icons/mdi/delete')['default'] MdiDelete: typeof import('~icons/mdi/delete')['default']
MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default'] MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
@ -176,6 +179,7 @@ declare module '@vue/runtime-core' {
MdiFunction: typeof import('~icons/mdi/function')['default'] MdiFunction: typeof import('~icons/mdi/function')['default']
MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default'] MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default']
MdiGithub: typeof import('~icons/mdi/github')['default'] MdiGithub: typeof import('~icons/mdi/github')['default']
MdiGraphOutline: typeof import('~icons/mdi/graph-outline')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default'] MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiHook: typeof import('~icons/mdi/hook')['default'] MdiHook: typeof import('~icons/mdi/hook')['default']
MdiInformation: typeof import('~icons/mdi/information')['default'] MdiInformation: typeof import('~icons/mdi/information')['default']
@ -193,6 +197,7 @@ declare module '@vue/runtime-core' {
MdiMagnify: typeof import('~icons/mdi/magnify')['default'] MdiMagnify: typeof import('~icons/mdi/magnify')['default']
MdiMenu: typeof import('~icons/mdi/menu')['default'] MdiMenu: typeof import('~icons/mdi/menu')['default']
MdiMenuDown: typeof import('~icons/mdi/menu-down')['default'] MdiMenuDown: typeof import('~icons/mdi/menu-down')['default']
MdiMenuIcon: typeof import('~icons/mdi/menu-icon')['default']
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default'] MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMinusCircleOutline: typeof import('~icons/mdi/minus-circle-outline')['default'] MdiMinusCircleOutline: typeof import('~icons/mdi/minus-circle-outline')['default']
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default'] MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']

4
packages/nc-gui/components/cell/DateTimePicker.vue

@ -28,9 +28,11 @@ const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false)) const editable = inject(EditModeInj, ref(false))
const column = inject(ColumnInj)!
let isDateInvalid = $ref(false) let isDateInvalid = $ref(false)
const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ' const dateFormat = isMysql(column.value.base_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
let localState = $computed({ let localState = $computed({
get() { get() {

2
packages/nc-gui/components/cell/MultiSelect.vue

@ -105,7 +105,7 @@ const vModel = computed({
const selectedTitles = computed(() => const selectedTitles = computed(() =>
modelValue modelValue
? typeof modelValue === 'string' ? typeof modelValue === 'string'
? isMysql ? isMysql(column.value.base_id)
? modelValue.split(',').sort((a, b) => { ? modelValue.split(',').sort((a, b) => {
const opa = options.value.find((el) => el.title === a) const opa = options.value.find((el) => el.title === a)
const opb = options.value.find((el) => el.title === b) const opb = options.value.find((el) => el.title === b)

6
packages/nc-gui/components/cell/TimePicker.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ActiveCellInj, ReadonlyInj, inject, onClickOutside, useProject, useSelectedCellKeyupListener, watch } from '#imports' import { ActiveCellInj, ReadonlyInj, inject, onClickOutside, useProject, watch } from '#imports'
interface Props { interface Props {
modelValue?: string | null | undefined modelValue?: string | null | undefined
@ -19,9 +19,11 @@ const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false)) const editable = inject(EditModeInj, ref(false))
const column = inject(ColumnInj)!
let isTimeInvalid = $ref(false) let isTimeInvalid = $ref(false)
const dateFormat = isMysql.value ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ' const dateFormat = isMysql(column.value.base_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
const localState = $computed({ const localState = $computed({
get() { get() {

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

@ -145,7 +145,7 @@ const addTableTab = (table: TableType) => {
addTab({ title: table.title, id: table.id, type: table.type as TabType }) addTab({ title: table.title, id: table.id, type: table.type as TabType })
} }
function openRenameTableDialog(table: TableType, rightClick = false) { function openRenameTableDialog(table: TableType, baseId: string, rightClick = false) {
$e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options') $e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options')
const isOpen = ref(true) const isOpen = ref(true)
@ -153,6 +153,7 @@ function openRenameTableDialog(table: TableType, rightClick = false) {
const { close } = useDialog(resolveComponent('DlgTableRename'), { const { close } = useDialog(resolveComponent('DlgTableRename'), {
'modelValue': isOpen, 'modelValue': isOpen,
'tableMeta': table, 'tableMeta': table,
'baseId': baseId,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -402,7 +403,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<template #overlay> <template #overlay>
<a-menu class="!py-0 rounded text-sm"> <a-menu class="!py-0 rounded text-sm">
<a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(table)"> <a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(table, base.id)">
<div class="nc-project-menu-item" :data-testid="`sidebar-table-rename-${table.title}`"> <div class="nc-project-menu-item" :data-testid="`sidebar-table-rename-${table.title}`">
{{ $t('general.rename') }} {{ $t('general.rename') }}
</div> </div>
@ -567,7 +568,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<a-menu-item <a-menu-item
v-if="isUIAllowed('table-rename')" v-if="isUIAllowed('table-rename')"
:data-testid="`sidebar-table-rename-${table.title}`" :data-testid="`sidebar-table-rename-${table.title}`"
@click="openRenameTableDialog(table)" @click="openRenameTableDialog(table, base.id)"
> >
<div class="nc-project-menu-item"> <div class="nc-project-menu-item">
{{ $t('general.rename') }} {{ $t('general.rename') }}
@ -604,7 +605,10 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<template v-if="!isSharedBase" #overlay> <template v-if="!isSharedBase" #overlay>
<a-menu class="!py-0 rounded text-sm"> <a-menu class="!py-0 rounded text-sm">
<template v-if="contextMenuTarget.type === 'table'"> <template v-if="contextMenuTarget.type === 'table'">
<a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(contextMenuTarget.value, true)"> <a-menu-item
v-if="isUIAllowed('table-rename')"
@click="openRenameTableDialog(contextMenuTarget.value, base.id, true)"
>
<div class="nc-project-menu-item"> <div class="nc-project-menu-item">
{{ $t('general.rename') }} {{ $t('general.rename') }}
</div> </div>
@ -631,6 +635,10 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<a-divider class="!my-0" /> <a-divider class="!my-0" />
<div class="flex items-start flex-col justify-start px-2 py-3 gap-2"> <div class="flex items-start flex-col justify-start px-2 py-3 gap-2">
<LazyGeneralAddBaseButton
class="color-transition py-1.5 px-2 text-primary font-bold cursor-pointer select-none hover:text-accent"
/>
<LazyGeneralShareBaseButton <LazyGeneralShareBaseButton
class="color-transition py-1.5 px-2 text-primary font-bold cursor-pointer select-none hover:text-accent" class="color-transition py-1.5 px-2 text-primary font-bold cursor-pointer select-none hover:text-accent"
/> />

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

@ -70,6 +70,7 @@ onMounted(async () => {
</script> </script>
<template> <template>
<div>
<a-modal <a-modal
v-model:visible="showPluginInstallModal" v-model:visible="showPluginInstallModal"
:class="{ active: showPluginInstallModal }" :class="{ active: showPluginInstallModal }"
@ -90,7 +91,6 @@ onMounted(async () => {
<a-modal <a-modal
v-model:visible="showPluginUninstallModal" v-model:visible="showPluginUninstallModal"
:class="{ active: showPluginUninstallModal }"
:closable="false" :closable="false"
width="24rem" width="24rem"
centered centered
@ -161,6 +161,7 @@ onMounted(async () => {
</div> </div>
</a-card> </a-card>
</div> </div>
</div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">

142
packages/nc-gui/components/dashboard/settings/DataSources.vue

@ -2,29 +2,68 @@
import { Empty } from 'ant-design-vue' import { Empty } from 'ant-design-vue'
import type { BaseType } from 'nocodb-sdk' import type { BaseType } from 'nocodb-sdk'
import CreateBase from './data-sources/CreateBase.vue' import CreateBase from './data-sources/CreateBase.vue'
import Metadata from './Metadata.vue'
import UIAcl from './UIAcl.vue'
import Erd from './Erd.vue'
import { DataSourcesSubTab } from '~/lib'
import { useNuxtApp, useProject } from '#imports' import { useNuxtApp, useProject } from '#imports'
interface Props {
state: string
reload: boolean
}
const props = defineProps<Props>()
const emits = defineEmits(['update:state', 'update:reload'])
const vModel = useVModel(props, 'state', emits)
const vReload = useVModel(props, 'reload', emits)
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { project } = useProject() const { project } = useProject()
let isLoading = $ref(false)
let sources = $ref<BaseType[]>([]) let sources = $ref<BaseType[]>([])
const newSourceTab = $ref(false) let activeBaseId = $ref('')
let metadiffbases = $ref<string[]>([])
async function loadBases() { async function loadBases() {
try { try {
if (!project.value?.id) return if (!project.value?.id) return
isLoading = true vReload.value = true
const baseList = await $api.base.list(project.value?.id) const baseList = await $api.base.list(project.value?.id)
if (baseList.bases.list && baseList.bases.list.length) { if (baseList.bases.list && baseList.bases.list.length) {
sources = baseList.bases.list sources = baseList.bases.list
} }
loadMetaDiff()
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} finally { } finally {
isLoading = false vReload.value = false
}
}
async function loadMetaDiff() {
try {
if (!project.value?.id) return
metadiffbases = []
const metadiff = await $api.project.metaDiffGet(project.value?.id)
for (const model of metadiff) {
if (model.detectedChanges?.length > 0) {
metadiffbases.push(model.base_id)
} }
}
} catch (e) {
console.error(e)
}
}
const baseAction = (baseId: string, action: string) => {
activeBaseId = baseId
vModel.value = action
} }
onMounted(async () => { onMounted(async () => {
@ -32,39 +71,21 @@ onMounted(async () => {
await loadBases() await loadBases()
} }
}) })
watch(
() => props.reload,
async (reload) => {
if (reload) {
await loadBases()
}
},
)
</script> </script>
<template> <template>
<div class="flex flex-row w-full"> <div class="flex flex-row w-full">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class="flex flex-row justify-end items-center w-full mb-4"> <div v-if="props.state === ''" class="max-h-600px overflow-y-auto">
<a-button class="self-start nc-btn-new-datasource" @click="newSourceTab = !newSourceTab">
<div v-if="newSourceTab" class="flex items-center gap-2 text-gray-600 font-light">
<MdiClose class="text-lg group-hover:text-accent" />
Cancel
</div>
<div v-else class="flex items-center gap-2 text-gray-600 font-light">
<MdiDatabaseOutline class="text-lg group-hover:text-accent" />
New
</div>
</a-button>
<!-- Reload -->
<a-button
v-if="!newSourceTab"
v-e="['a:proj-meta:meta-data:reload']"
class="self-start nc-btn-metasync-reload"
@click="loadBases"
>
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiReload :class="{ 'animate-infinite animate-spin !text-success': isLoading }" />
{{ $t('general.reload') }}
</div>
</a-button>
</div>
<div v-if="newSourceTab" class="max-h-600px overflow-y-auto">
<CreateBase />
</div>
<div v-else class="max-h-600px overflow-y-auto">
<a-table <a-table
class="w-full" class="w-full"
size="small" size="small"
@ -75,27 +96,68 @@ onMounted(async () => {
" "
:data-source="sources ?? []" :data-source="sources ?? []"
:pagination="false" :pagination="false"
:loading="isLoading" :loading="vReload"
bordered bordered
> >
<template #emptyText> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" /> </template> <template #emptyText> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" /> </template>
<a-table-column key="type" title="Type" data-index="type" :width="180">
<template #default="{ text }">{{ text }}</template>
</a-table-column>
<a-table-column key="alias" title="Name" data-index="alias"> <a-table-column key="alias" title="Name" data-index="alias">
<template #default="{ text, record }">{{ record.is_meta ? 'BASE' : text }}</template> <template #default="{ text, record }">
{{ record.is_meta ? 'BASE' : text }} <span class="text-gray-400 text-xs">({{ record.type }})</span>
</template>
</a-table-column> </a-table-column>
<a-table-column key="action" :title="$t('labels.actions')" :width="180"> <a-table-column key="action" :title="$t('labels.actions')" :width="180">
<template #default="{ record }"> <template #default="{ record }">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<MdiEditOutline v-e="['c:base:edit:rename']" class="nc-action-btn" /> <a-tooltip>
<template #title>Sync Metadata {{ metadiffbases.includes(record.id) ? '(Out of sync)' : '' }}</template>
<MdiDeleteOutline class="nc-action-btn" /> <MdiDatabaseSync
class="nc-action-btn cursor-pointer outline-0"
:class="metadiffbases.includes(record.id) ? 'text-primary' : ''"
@click="baseAction(record.id, DataSourcesSubTab.Metadata)"
/>
</a-tooltip>
<a-tooltip>
<template #title>UI ACL</template>
<MdiDatabaseLockOutline
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(record.id, DataSourcesSubTab.UIAcl)"
/>
</a-tooltip>
<a-tooltip>
<template #title>ERD</template>
<MdiGraphOutline
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(record.id, DataSourcesSubTab.ERD)"
/>
</a-tooltip>
<a-tooltip>
<template #title>Edit</template>
<MdiEditOutline
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(record.id, DataSourcesSubTab.Edit)"
/>
</a-tooltip>
<a-tooltip>
<template #title>Delete</template>
<MdiDeleteOutline class="nc-action-btn cursor-pointer outline-0" />
</a-tooltip>
</div> </div>
</template> </template>
</a-table-column> </a-table-column>
</a-table> </a-table>
</div> </div>
<div v-else-if="props.state === DataSourcesSubTab.New" class="max-h-600px overflow-y-auto">
<CreateBase />
</div>
<div v-else-if="props.state === DataSourcesSubTab.Metadata" class="max-h-600px overflow-y-auto">
<Metadata :base-id="activeBaseId" />
</div>
<div v-else-if="props.state === DataSourcesSubTab.UIAcl" class="max-h-600px overflow-y-auto">
<UIAcl :base-id="activeBaseId" />
</div>
<div v-else-if="props.state === DataSourcesSubTab.ERD" class="max-h-600px overflow-y-auto">
<Erd :base-id="activeBaseId" />
</div>
</div> </div>
</div> </div>
</template> </template>

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

@ -1,5 +1,13 @@
<script setup lang="ts">
interface Props {
baseId: string
}
const props = defineProps<Props>()
</script>
<template> <template>
<div class="w-full h-full !p-0 h-70vh"> <div class="w-full h-full !p-0 h-70vh">
<ErdView /> <ErdView :base-id="props.baseId" />
</div> </div>
</template> </template>

10
packages/nc-gui/components/dashboard/settings/Metadata.vue

@ -1,6 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { Empty, extractSdkResponseErrorMsg, h, message, useI18n, useNuxtApp, useProject } from '#imports' import { Empty, extractSdkResponseErrorMsg, h, message, useI18n, useNuxtApp, useProject } from '#imports'
interface Props {
baseId: string
}
const props = defineProps<Props>()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { project, loadTables } = useProject() const { project, loadTables } = useProject()
@ -19,7 +25,7 @@ async function loadMetaDiff() {
isLoading = true isLoading = true
isDifferent = false isDifferent = false
metadiff = await $api.project.metaDiffGet(project.value?.id) metadiff = await $api.base.metaDiffGet(project.value?.id, props.baseId)
for (const model of metadiff) { for (const model of metadiff) {
if (model.detectedChanges?.length > 0) { if (model.detectedChanges?.length > 0) {
model.syncState = model.detectedChanges.map((el: any) => el?.msg).join(', ') model.syncState = model.detectedChanges.map((el: any) => el?.msg).join(', ')
@ -38,7 +44,7 @@ async function syncMetaDiff() {
if (!project.value?.id || !isDifferent) return if (!project.value?.id || !isDifferent) return
isLoading = true isLoading = true
await $api.project.metaDiffSync(project.value.id) await $api.base.metaDiffSync(project.value.id, props.baseId)
// Table metadata recreated successfully // Table metadata recreated successfully
message.info(t('msg.info.metaDataRecreated')) message.info(t('msg.info.metaDataRecreated'))
await loadTables() await loadTables()

127
packages/nc-gui/components/dashboard/settings/Modal.vue

@ -1,15 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FunctionalComponent, SVGAttributes } from 'vue' import type { FunctionalComponent, SVGAttributes } from 'vue'
import AppStore from './AppStore.vue'
import DataSources from './DataSources.vue' import DataSources from './DataSources.vue'
import { resolveComponent, useI18n, useNuxtApp, useUIPermission, useVModel, watch } from '#imports' import Misc from './Misc.vue'
import { useNuxtApp } from '#app'
import { useI18n, useUIPermission, useVModel, watch } from '#imports'
import StoreFrontOutline from '~icons/mdi/storefront-outline' import StoreFrontOutline from '~icons/mdi/storefront-outline'
import TeamFillIcon from '~icons/ri/team-fill' import TeamFillIcon from '~icons/ri/team-fill'
import MultipleTableIcon from '~icons/mdi/table-multiple' import MultipleTableIcon from '~icons/mdi/table-multiple'
import NotebookOutline from '~icons/mdi/notebook-outline' import NotebookOutline from '~icons/mdi/notebook-outline'
import FolderCog from '~icons/mdi/folder-cog'
import { DataSourcesSubTab } from '~~/lib'
interface Props { interface Props {
modelValue: boolean modelValue: boolean
openKey?: string openKey?: string
dataSourcesState?: string
} }
interface SubTabGroup { interface SubTabGroup {
@ -41,6 +47,9 @@ const { t } = useI18n()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const dataSourcesState = ref(props.dataSourcesState)
const dataSourcesReload = ref(false)
const tabsInfo: TabGroup = { const tabsInfo: TabGroup = {
teamAndAuth: { teamAndAuth: {
title: t('title.teamAndAuth'), title: t('title.teamAndAuth'),
@ -69,8 +78,6 @@ const tabsInfo: TabGroup = {
$e('c:settings:team-auth') $e('c:settings:team-auth')
}, },
}, },
...(isUIAllowed('appStore')
? {
appStore: { appStore: {
// App Store // App Store
title: t('title.appStore'), title: t('title.appStore'),
@ -78,51 +85,26 @@ const tabsInfo: TabGroup = {
subTabs: { subTabs: {
new: { new: {
title: 'Apps', title: 'Apps',
body: resolveComponent('DashboardSettingsAppStore'), body: AppStore,
}, },
}, },
onClick: () => { onClick: () => {
$e('c:settings:appstore') $e('c:settings:appstore')
}, },
}, },
} dataSources: {
: {}), // Data Sources
projMetaData: { title: 'Data Sources',
// Project Metadata
title: t('title.projMeta'),
icon: MultipleTableIcon, icon: MultipleTableIcon,
subTabs: { subTabs: {
dataSources: { dataSources: {
title: 'Data Sources', title: 'Data Sources',
body: DataSources, body: DataSources,
}, },
metaData: {
// Metadata
title: t('title.metadata'),
body: resolveComponent('DashboardSettingsMetadata'),
},
acl: {
// UI Access Control
title: t('title.uiACL'),
body: resolveComponent('DashboardSettingsUIAcl'),
onClick: () => {
$e('c:table:ui-acl')
},
}, },
erd: {
title: t('title.erdView'),
body: resolveComponent('DashboardSettingsErd'),
onClick: () => { onClick: () => {
$e('c:settings:erd') dataSourcesState.value = ''
}, $e('c:settings:data-sources')
},
misc: {
title: t('general.misc'),
body: resolveComponent('DashboardSettingsMisc'),
},
},
onClick: () => {
$e('c:settings:proj-metadata')
}, },
}, },
audit: { audit: {
@ -140,6 +122,21 @@ const tabsInfo: TabGroup = {
$e('c:settings:audit') $e('c:settings:audit')
}, },
}, },
projectSettings: {
// Project Settings
title: 'Project Settings',
icon: FolderCog,
subTabs: {
misc: {
// Misc
title: 'Misc',
body: Misc,
},
},
onClick: () => {
$e('c:settings:project-settings')
},
},
} }
const firstKeyOfObject = (obj: object) => Object.keys(obj)[0] const firstKeyOfObject = (obj: object) => Object.keys(obj)[0]
@ -163,6 +160,15 @@ watch(
selectedTabKeys = [Object.keys(tabsInfo).find((key) => key === nextOpenKey) || firstKeyOfObject(tabsInfo)] selectedTabKeys = [Object.keys(tabsInfo).find((key) => key === nextOpenKey) || firstKeyOfObject(tabsInfo)]
}, },
) )
watch(
() => props.modelValue,
() => {
dataSourcesState.value = props.dataSourcesState || ''
selectedTabKeys = [Object.keys(tabsInfo).find((key) => key === props.openKey) || firstKeyOfObject(tabsInfo)]
},
{ immediate: true },
)
</script> </script>
<template> <template>
@ -215,7 +221,12 @@ watch(
<!-- Sub Tabs --> <!-- Sub Tabs -->
<a-layout-content class="h-auto px-4 scrollbar-thumb-gray-500"> <a-layout-content class="h-auto px-4 scrollbar-thumb-gray-500">
<a-menu v-model:selectedKeys="selectedSubTabKeys" :open-keys="[]" mode="horizontal"> <a-menu
v-if="selectedTabKeys[0] !== 'dataSources'"
v-model:selectedKeys="selectedSubTabKeys"
:open-keys="[]"
mode="horizontal"
>
<a-menu-item <a-menu-item
v-for="(tab, key) of selectedTab.subTabs" v-for="(tab, key) of selectedTab.subTabs"
:key="key" :key="key"
@ -225,8 +236,52 @@ watch(
{{ tab.title }} {{ tab.title }}
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
<div v-else>
<div class="flex items-center">
<a-breadcrumb class="w-full cursor-pointer">
<a-breadcrumb-item v-if="dataSourcesState !== ''" @click="dataSourcesState = ''">
<a class="!no-underline">Data Sources</a>
</a-breadcrumb-item>
<a-breadcrumb-item v-else @click="dataSourcesState = ''">Data Sources</a-breadcrumb-item>
<a-breadcrumb-item v-if="dataSourcesState !== ''">{{ dataSourcesState }}</a-breadcrumb-item>
</a-breadcrumb>
<div v-if="dataSourcesState === ''" class="flex flex-row justify-end items-center w-full">
<a-button class="self-start nc-btn-new-datasource" @click="dataSourcesState = DataSourcesSubTab.New">
<div v-if="dataSourcesState === ''" class="flex items-center gap-2 text-gray-600 font-light">
<MdiDatabaseOutline class="text-lg group-hover:text-accent" />
New
</div>
</a-button>
<!-- Reload -->
<a-button
v-e="['a:proj-meta:data-sources:reload']"
class="self-start nc-btn-metasync-reload"
@click="dataSourcesReload = true"
>
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiReload :class="{ 'animate-infinite animate-spin !text-success': dataSourcesReload }" />
{{ $t('general.reload') }}
</div>
</a-button>
</div>
</div>
<a-divider style="margin: 10px 0" />
</div>
<component :is="selectedSubTab?.body" class="px-2 py-6" :data-testid="`nc-settings-subtab-${selectedSubTab.title}`" /> <component
:is="selectedSubTab?.body"
v-if="selectedSubTabKeys[0] === 'dataSources'"
v-model:state="dataSourcesState"
v-model:reload="dataSourcesReload"
class="px-2 pb-2"
:data-testid="`nc-settings-subtab-${selectedSubTab.title}`"
/>
<component
:is="selectedSubTab?.body"
v-else
class="px-2 py-6"
:data-testid="`nc-settings-subtab-${selectedSubTab.title}`"
/>
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>
</a-modal> </a-modal>

11
packages/nc-gui/components/dashboard/settings/UIAcl.vue

@ -13,6 +13,12 @@ import {
viewIcons, viewIcons,
} from '#imports' } from '#imports'
interface Props {
baseId: string
}
const props = defineProps<Props>()
const { t } = useI18n() const { t } = useI18n()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
@ -32,8 +38,9 @@ const searchInput = $ref('')
const filteredTables = computed(() => const filteredTables = computed(() =>
tables.filter( tables.filter(
(el) => (el) =>
(typeof el?._ptn === 'string' && el._ptn.toLowerCase().includes(searchInput.toLowerCase())) || el?.base_id === props.baseId &&
(typeof el?.title === 'string' && el.title.toLowerCase().includes(searchInput.toLowerCase())), ((typeof el?._ptn === 'string' && el._ptn.toLowerCase().includes(searchInput.toLowerCase())) ||
(typeof el?.title === 'string' && el.title.toLowerCase().includes(searchInput.toLowerCase()))),
), ),
) )

9
packages/nc-gui/components/dlg/TableRename.vue

@ -21,9 +21,10 @@ import {
interface Props { interface Props {
modelValue?: boolean modelValue?: boolean
tableMeta: TableType tableMeta: TableType
baseId: string
} }
const { tableMeta, ...props } = defineProps<Props>() const { tableMeta, baseId, ...props } = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'updated']) const emit = defineEmits(['update:modelValue', 'updated'])
@ -57,11 +58,11 @@ const validators = computed(() => {
validator: (rule: any, value: any) => { validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
let tableNameLengthLimit = 255 let tableNameLengthLimit = 255
if (isMysql) { if (isMysql(baseId)) {
tableNameLengthLimit = 64 tableNameLengthLimit = 64
} else if (isPg) { } else if (isPg(baseId)) {
tableNameLengthLimit = 63 tableNameLengthLimit = 63
} else if (isMssql) { } else if (isMssql(baseId)) {
tableNameLengthLimit = 128 tableNameLengthLimit = 128
} }
const projectPrefix = project?.value?.prefix || '' const projectPrefix = project?.value?.prefix || ''

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

@ -4,7 +4,7 @@ import { UITypes } from 'nocodb-sdk'
import type { ERDConfig } from './utils' import type { ERDConfig } from './utils'
import { reactive, ref, useMetas, useProject, watch } from '#imports' import { reactive, ref, useMetas, useProject, watch } from '#imports'
const { table } = defineProps<{ table?: TableType }>() const props = defineProps<{ table?: TableType; baseId?: string }>()
const { tables: projectTables } = useProject() const { tables: projectTables } = useProject()
@ -18,7 +18,7 @@ const config = reactive<ERDConfig>({
showPkAndFk: true, showPkAndFk: true,
showViews: false, showViews: false,
showAllColumns: true, showAllColumns: true,
singleTableMode: !!table, singleTableMode: !!props.table,
showMMTables: false, showMMTables: false,
showJunctionTableNames: false, showJunctionTableNames: false,
}) })
@ -34,14 +34,13 @@ const loadMetaOfTablesNotInMetas = async (localTables: TableType[]) => {
} }
const populateTables = async () => { const populateTables = async () => {
let localTables: TableType[] let localTables: TableType[] = []
if (props.table) {
if (table) {
// if table is provided only get the table and its related tables // if table is provided only get the table and its related tables
localTables = projectTables.value.filter( localTables = projectTables.value.filter(
(t) => (t) =>
t.id === table.id || t.id === props.table.id ||
table.columns?.find( props.table.columns?.find(
(column) => (column) =>
column.uidt === UITypes.LinkToAnotherRecord && column.uidt === UITypes.LinkToAnotherRecord &&
(column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id === t.id, (column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id === t.id,
@ -59,7 +58,7 @@ const populateTables = async () => {
config.showMMTables || config.showMMTables ||
(!config.showMMTables && !t.mm) || (!config.showMMTables && !t.mm) ||
// Show mm table if it's the selected table // Show mm table if it's the selected table
t.id === table?.id, t.id === props.table?.id,
) )
.filter((t) => config.singleTableMode || (!config.showViews && t.type !== 'view') || config.showViews) .filter((t) => config.singleTableMode || (!config.showViews && t.type !== 'view') || config.showViews)
@ -76,6 +75,8 @@ watch(config, populateTables, {
deep: true, deep: true,
}) })
const filteredTables = computed(() => tables.value.filter((t) => !props.baseId || t.base_id === props.baseId))
watch( watch(
() => config.showAllColumns, () => config.showAllColumns,
() => { () => {
@ -87,7 +88,7 @@ watch(
<template> <template>
<div class="w-full" style="height: inherit" :class="[`nc-erd-vue-flow${config.singleTableMode ? '-single-table' : ''}`]"> <div class="w-full" style="height: inherit" :class="[`nc-erd-vue-flow${config.singleTableMode ? '-single-table' : ''}`]">
<div class="relative h-full"> <div class="relative h-full">
<LazyErdFlow :tables="tables" :config="config"> <LazyErdFlow :tables="filteredTables" :config="config">
<GeneralOverlay v-model="isLoading" inline class="bg-gray-300/50"> <GeneralOverlay v-model="isLoading" inline class="bg-gray-300/50">
<div class="h-full w-full flex flex-col justify-center items-center"> <div class="h-full w-full flex flex-col justify-center items-center">
<a-spin size="large" /> <a-spin size="large" />

21
packages/nc-gui/components/general/AddBaseButton.vue

@ -0,0 +1,21 @@
<script setup lang="ts">
import { useUIPermission } from '#imports'
const { isUIAllowed } = useUIPermission()
const toggleDialog = inject(ToggleDialogInj, () => {})
</script>
<template>
<div
class="flex items-center w-full pl-3 hover:(text-primary bg-primary bg-opacity-5)"
@click="toggleDialog(true, 'dataSources', 'New')"
>
<div v-if="isUIAllowed('newBase')">
<div class="flex items-center space-x-1">
<MdiDatabasePlusOutline class="mr-1 nc-new-base" />
<div>Data Sources</div>
</div>
</div>
</div>
</template>

4
packages/nc-gui/components/smartsheet/column/CurrencyOptions.vue

@ -49,14 +49,12 @@ const validators = {
], ],
} }
const { setAdditionalValidations, validateInfos } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, isPg } = useColumnCreateStoreOrThrow()
setAdditionalValidations({ setAdditionalValidations({
...validators, ...validators,
}) })
const { isPg } = useProject()
const currencyList = currencyCodes || [] const currencyList = currencyCodes || []
const currencyLocaleList = currencyLocales() || [] const currencyLocaleList = currencyLocales() || []

2
packages/nc-gui/components/virtual-cell/Formula.vue

@ -10,7 +10,7 @@ const cellValue = inject(CellValueInj)
const { isPg } = useProject() const { isPg } = useProject()
const result = computed(() => (isPg.value ? handleTZ(cellValue?.value) : cellValue?.value)) const result = computed(() => (isPg(column.value.base_id) ? handleTZ(cellValue?.value) : cellValue?.value))
const urls = computed(() => replaceUrlsWithLink(result.value)) const urls = computed(() => replaceUrlsWithLink(result.value))

11
packages/nc-gui/composables/useColumnCreateStore.ts

@ -27,7 +27,7 @@ interface ValidationsObj {
const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState( const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState(
(meta: Ref<TableType | undefined>, column: Ref<ColumnType | undefined>) => { (meta: Ref<TableType | undefined>, column: Ref<ColumnType | undefined>) => {
const { sqlUis } = useProject() const { sqlUis, isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc } = useProject()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { getMeta } = useMetas() const { getMeta } = useMetas()
@ -40,6 +40,12 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const isEdit = computed(() => !!column?.value?.id) const isEdit = computed(() => !!column?.value?.id)
const isMysql = computed(() => isMysqlFunc(meta.value?.base_id ? meta.value?.base_id : Object.keys(sqlUis.value)[0]))
const isPg = computed(() => isPgFunc(meta.value?.base_id ? meta.value?.base_id : Object.keys(sqlUis.value)[0]))
const isMssql = computed(() => isMssqlFunc(meta.value?.base_id ? meta.value?.base_id : Object.keys(sqlUis.value)[0]))
const idType = null const idType = null
const additionalValidations = ref<ValidationsObj>({}) const additionalValidations = ref<ValidationsObj>({})
@ -273,6 +279,9 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
isEdit, isEdit,
column, column,
sqlUi, sqlUi,
isMssql,
isPg,
isMysql,
} }
}, },
) )

8
packages/nc-gui/composables/useProject.ts

@ -68,19 +68,19 @@ const [setup, use] = useInjectionState(() => {
return temp return temp
}) })
function getBaseType(baseId: string) { function getBaseType(baseId?: string) {
return bases.value.find((base) => base.id === baseId)?.type || ClientType.MYSQL return bases.value.find((base) => base.id === baseId)?.type || ClientType.MYSQL
} }
function isMysql(baseId: string) { function isMysql(baseId?: string) {
return ['mysql', ClientType.MYSQL].includes(getBaseType(baseId)) return ['mysql', ClientType.MYSQL].includes(getBaseType(baseId))
} }
function isMssql(baseId: string) { function isMssql(baseId?: string) {
return getBaseType(baseId) === 'mssql' return getBaseType(baseId) === 'mssql'
} }
function isPg(baseId: string) { function isPg(baseId?: string) {
return getBaseType(baseId) === 'pg' return getBaseType(baseId) === 'pg'
} }

1
packages/nc-gui/context/index.ts

@ -31,3 +31,4 @@ export const EditModeInj: InjectionKey<Ref<boolean>> = Symbol('edit-mode-injecti
export const SharedViewPasswordInj: InjectionKey<Ref<string | null>> = Symbol('shared-view-password-injection') export const SharedViewPasswordInj: InjectionKey<Ref<string | null>> = Symbol('shared-view-password-injection')
export const CellUrlDisableOverlayInj: InjectionKey<Ref<boolean>> = Symbol('cell-url-disable-url') export const CellUrlDisableOverlayInj: InjectionKey<Ref<boolean>> = Symbol('cell-url-disable-url')
export const DropZoneRef: InjectionKey<Ref<Element | undefined>> = Symbol('drop-zone-ref') export const DropZoneRef: InjectionKey<Ref<Element | undefined>> = Symbol('drop-zone-ref')
export const ToggleDialogInj: InjectionKey<Function> = Symbol('toggle-dialog-injection')

9
packages/nc-gui/lib/enums.ts

@ -85,3 +85,12 @@ export enum SmartsheetStoreEvents {
FIELD_RELOAD = 'field-reload', FIELD_RELOAD = 'field-reload',
FIELD_ADD = 'field-add', FIELD_ADD = 'field-add',
} }
export enum DataSourcesSubTab {
New = 'New',
Metadata = 'Metadata',
ERD = 'ERD',
UIAcl = 'UI ACL',
Misc = 'Misc',
Edit = 'Edit',
}

13
packages/nc-gui/pages/[projectType]/[projectId]/index.vue

@ -61,6 +61,8 @@ const dialogOpen = ref(false)
const openDialogKey = ref<string>() const openDialogKey = ref<string>()
const dataSourcesState = ref<string>()
const dropdownOpen = ref(false) const dropdownOpen = ref(false)
/** Sidebar ref */ /** Sidebar ref */
@ -73,11 +75,14 @@ const logout = () => {
navigateTo('/signin') navigateTo('/signin')
} }
function toggleDialog(value?: boolean, key?: string) { function toggleDialog(value?: boolean, key?: string, dsState?: string) {
dialogOpen.value = value ?? !dialogOpen.value dialogOpen.value = value ?? !dialogOpen.value
openDialogKey.value = key openDialogKey.value = key
dataSourcesState.value = dsState
} }
provide(ToggleDialogInj, toggleDialog)
const handleThemeColor = async (mode: 'swatch' | 'primary' | 'accent', color?: string) => { const handleThemeColor = async (mode: 'swatch' | 'primary' | 'accent', color?: string) => {
switch (mode) { switch (mode) {
case 'swatch': { case 'swatch': {
@ -559,12 +564,12 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
</div> </div>
</div> </div>
<LazyDashboardTreeView /> <LazyDashboardTreeView @create-base-dlg="toggleDialog(true, 'dataSources')" />
</a-layout-sider> </a-layout-sider>
</template> </template>
<div> <div :key="$route.fullPath.split('?')[0]">
<LazyDashboardSettingsModal v-model="dialogOpen" :open-key="openDialogKey" /> <LazyDashboardSettingsModal v-model="dialogOpen" :open-key="openDialogKey" :data-sources-state="dataSourcesState" />
<NuxtPage :page-key="$route.params.projectId" /> <NuxtPage :page-key="$route.params.projectId" />

7
packages/nocodb/src/lib/meta/api/metaDiffApis.ts

@ -42,6 +42,7 @@ const applyChangesPriorityOrder = [
type MetaDiff = { type MetaDiff = {
title?: string; title?: string;
table_name: string; table_name: string;
base_id: string;
type: ModelTypes; type: ModelTypes;
detectedChanges: Array<MetaDiffChange>; detectedChanges: Array<MetaDiffChange>;
}; };
@ -157,6 +158,7 @@ async function getMetaDiff(
if (oldMetaIdx === -1) { if (oldMetaIdx === -1) {
changes.push({ changes.push({
table_name: table.tn, table_name: table.tn,
base_id: base.id,
type: ModelTypes.TABLE, type: ModelTypes.TABLE,
detectedChanges: [ detectedChanges: [
{ {
@ -175,6 +177,7 @@ async function getMetaDiff(
const tableProp: MetaDiff = { const tableProp: MetaDiff = {
title: oldMeta.title, title: oldMeta.title,
table_name: table.tn, table_name: table.tn,
base_id: base.id,
type: ModelTypes.TABLE, type: ModelTypes.TABLE,
detectedChanges: [], detectedChanges: [],
}; };
@ -245,6 +248,7 @@ async function getMetaDiff(
for (const model of oldTableMetas) { for (const model of oldTableMetas) {
changes.push({ changes.push({
table_name: model.table_name, table_name: model.table_name,
base_id: base.id,
type: ModelTypes.TABLE, type: ModelTypes.TABLE,
detectedChanges: [ detectedChanges: [
{ {
@ -430,6 +434,7 @@ async function getMetaDiff(
if (oldMetaIdx === -1) { if (oldMetaIdx === -1) {
changes.push({ changes.push({
table_name: view.tn, table_name: view.tn,
base_id: base.id,
type: ModelTypes.VIEW, type: ModelTypes.VIEW,
detectedChanges: [ detectedChanges: [
{ {
@ -448,6 +453,7 @@ async function getMetaDiff(
const tableProp: MetaDiff = { const tableProp: MetaDiff = {
title: oldMeta.title, title: oldMeta.title,
table_name: view.tn, table_name: view.tn,
base_id: base.id,
type: ModelTypes.VIEW, type: ModelTypes.VIEW,
detectedChanges: [], detectedChanges: [],
}; };
@ -514,6 +520,7 @@ async function getMetaDiff(
for (const model of oldViewMetas) { for (const model of oldViewMetas) {
changes.push({ changes.push({
table_name: model.table_name, table_name: model.table_name,
base_id: base.id,
type: ModelTypes.TABLE, type: ModelTypes.TABLE,
detectedChanges: [ detectedChanges: [
{ {

Loading…
Cancel
Save