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. 169
      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. 153
      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']
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
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']
ACard: typeof import('ant-design-vue/es')['Card']
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']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
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']
MdiContentSaveEdit: typeof import('~icons/mdi/content-save-edit')['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']
MdiDatabasePlusOutline: typeof import('~icons/mdi/database-plus-outline')['default']
MdiDatabaseSync: typeof import('~icons/mdi/database-sync')['default']
MdiDelete: typeof import('~icons/mdi/delete')['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']
MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default']
MdiGithub: typeof import('~icons/mdi/github')['default']
MdiGraphOutline: typeof import('~icons/mdi/graph-outline')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiHook: typeof import('~icons/mdi/hook')['default']
MdiInformation: typeof import('~icons/mdi/information')['default']
@ -193,6 +197,7 @@ declare module '@vue/runtime-core' {
MdiMagnify: typeof import('~icons/mdi/magnify')['default']
MdiMenu: typeof import('~icons/mdi/menu')['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']
MdiMinusCircleOutline: typeof import('~icons/mdi/minus-circle-outline')['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 column = inject(ColumnInj)!
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({
get() {

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

@ -105,7 +105,7 @@ const vModel = computed({
const selectedTitles = computed(() =>
modelValue
? typeof modelValue === 'string'
? isMysql
? isMysql(column.value.base_id)
? modelValue.split(',').sort((a, b) => {
const opa = options.value.find((el) => el.title === a)
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">
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 {
modelValue?: string | null | undefined
@ -19,9 +19,11 @@ const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
const column = inject(ColumnInj)!
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({
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 })
}
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')
const isOpen = ref(true)
@ -153,6 +153,7 @@ function openRenameTableDialog(table: TableType, rightClick = false) {
const { close } = useDialog(resolveComponent('DlgTableRename'), {
'modelValue': isOpen,
'tableMeta': table,
'baseId': baseId,
'onUpdate:modelValue': closeDialog,
})
@ -402,7 +403,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<template #overlay>
<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}`">
{{ $t('general.rename') }}
</div>
@ -567,7 +568,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<a-menu-item
v-if="isUIAllowed('table-rename')"
:data-testid="`sidebar-table-rename-${table.title}`"
@click="openRenameTableDialog(table)"
@click="openRenameTableDialog(table, base.id)"
>
<div class="nc-project-menu-item">
{{ $t('general.rename') }}
@ -604,7 +605,10 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<template v-if="!isSharedBase" #overlay>
<a-menu class="!py-0 rounded text-sm">
<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">
{{ $t('general.rename') }}
</div>
@ -631,6 +635,10 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<a-divider class="!my-0" />
<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
class="color-transition py-1.5 px-2 text-primary font-bold cursor-pointer select-none hover:text-accent"
/>

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

@ -70,96 +70,97 @@ onMounted(async () => {
</script>
<template>
<a-modal
v-model:visible="showPluginInstallModal"
:class="{ active: showPluginInstallModal }"
:closable="false"
centered
min-height="300"
:footer="null"
wrap-class-name="nc-modal-plugin-install"
v-bind="$attrs"
>
<LazyDashboardSettingsAppInstall
v-if="pluginApp && showPluginInstallModal"
:id="pluginApp.id"
@close="showPluginInstallModal = false"
@saved="saved()"
/>
</a-modal>
<a-modal
v-model:visible="showPluginUninstallModal"
:class="{ active: showPluginUninstallModal }"
:closable="false"
width="24rem"
centered
:footer="null"
wrap-class-name="nc-modal-plugin-uninstall"
>
<div class="flex flex-col h-full">
<div class="flex flex-row justify-center mt-2 text-center w-full text-base">
{{ `Click on confirm to reset ${pluginApp && pluginApp.title}` }}
</div>
<div class="flex mt-6 justify-center space-x-2">
<a-button @click="showPluginUninstallModal = false"> {{ $t('general.cancel') }} </a-button>
<a-button type="primary" danger @click="resetPlugin"> {{ $t('general.confirm') }} </a-button>
</div>
</div>
</a-modal>
<div class="grid grid-cols-2 gap-x-2 gap-y-4 mt-4">
<a-card
v-for="(app, i) in apps"
:key="i"
:class="`relative flex overflow-x-hidden app-item-card !shadow-sm rounded-md w-full nc-app-store-card-${app.title}`"
:body-style="{ width: '100%' }"
<div>
<a-modal
v-model:visible="showPluginInstallModal"
:class="{ active: showPluginInstallModal }"
:closable="false"
centered
min-height="300"
:footer="null"
wrap-class-name="nc-modal-plugin-install"
v-bind="$attrs"
>
<div class="install-btn flex flex-row justify-end space-x-1">
<a-button v-if="app.parsedInput" size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-edit">
<IcRoundEdit class="pr-0.5" :height="12" />
Edit
</div>
</a-button>
<a-button v-if="app.parsedInput" size="small" outlined @click="showResetPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-reset">
<MdiCloseCircleOutline />
<div class="flex ml-0.5">Reset</div>
</div>
</a-button>
<a-button v-else size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-install">
<MdiPlus />
Install
</div>
</a-button>
<LazyDashboardSettingsAppInstall
v-if="pluginApp && showPluginInstallModal"
:id="pluginApp.id"
@close="showPluginInstallModal = false"
@saved="saved()"
/>
</a-modal>
<a-modal
v-model:visible="showPluginUninstallModal"
:closable="false"
width="24rem"
centered
:footer="null"
wrap-class-name="nc-modal-plugin-uninstall"
>
<div class="flex flex-col h-full">
<div class="flex flex-row justify-center mt-2 text-center w-full text-base">
{{ `Click on confirm to reset ${pluginApp && pluginApp.title}` }}
</div>
<div class="flex mt-6 justify-center space-x-2">
<a-button @click="showPluginUninstallModal = false"> {{ $t('general.cancel') }} </a-button>
<a-button type="primary" danger @click="resetPlugin"> {{ $t('general.confirm') }} </a-button>
</div>
</div>
<div class="flex flex-row space-x-2 items-center justify-start w-full">
<div class="flex w-20 pl-3">
<img
v-if="app.title !== 'SMTP'"
class="avatar"
alt="logo"
:style="{
backgroundColor: app.title === 'SES' ? '#242f3e' : '',
}"
:src="app.logo"
/>
<div v-else />
</a-modal>
<div class="grid grid-cols-2 gap-x-2 gap-y-4 mt-4">
<a-card
v-for="(app, i) in apps"
:key="i"
:class="`relative flex overflow-x-hidden app-item-card !shadow-sm rounded-md w-full nc-app-store-card-${app.title}`"
:body-style="{ width: '100%' }"
>
<div class="install-btn flex flex-row justify-end space-x-1">
<a-button v-if="app.parsedInput" size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-edit">
<IcRoundEdit class="pr-0.5" :height="12" />
Edit
</div>
</a-button>
<a-button v-if="app.parsedInput" size="small" outlined @click="showResetPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-reset">
<MdiCloseCircleOutline />
<div class="flex ml-0.5">Reset</div>
</div>
</a-button>
<a-button v-else size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-install">
<MdiPlus />
Install
</div>
</a-button>
</div>
<div class="flex flex-col flex-1 w-3/5 pl-3">
<a-typography-title :level="5">{{ app.title }}</a-typography-title>
<div class="flex flex-row space-x-2 items-center justify-start w-full">
<div class="flex w-20 pl-3">
<img
v-if="app.title !== 'SMTP'"
class="avatar"
alt="logo"
:style="{
backgroundColor: app.title === 'SES' ? '#242f3e' : '',
}"
:src="app.logo"
/>
<div v-else />
</div>
<div class="flex flex-col flex-1 w-3/5 pl-3">
<a-typography-title :level="5">{{ app.title }}</a-typography-title>
{{ app.description }}
{{ app.description }}
</div>
</div>
</div>
</a-card>
</a-card>
</div>
</div>
</template>

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

@ -2,69 +2,90 @@
import { Empty } from 'ant-design-vue'
import type { BaseType } from 'nocodb-sdk'
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'
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 { project } = useProject()
let isLoading = $ref(false)
let sources = $ref<BaseType[]>([])
const newSourceTab = $ref(false)
let activeBaseId = $ref('')
let metadiffbases = $ref<string[]>([])
async function loadBases() {
try {
if (!project.value?.id) return
isLoading = true
vReload.value = true
const baseList = await $api.base.list(project.value?.id)
if (baseList.bases.list && baseList.bases.list.length) {
sources = baseList.bases.list
}
loadMetaDiff()
} catch (e) {
console.error(e)
} 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 () => {
if (sources.length === 0) {
await loadBases()
}
})
watch(
() => props.reload,
async (reload) => {
if (reload) {
await loadBases()
}
},
)
</script>
<template>
<div class="flex flex-row w-full">
<div class="flex flex-col w-full">
<div class="flex flex-row justify-end items-center w-full mb-4">
<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">
<div v-if="props.state === ''" class="max-h-600px overflow-y-auto">
<a-table
class="w-full"
size="small"
@ -75,27 +96,68 @@ onMounted(async () => {
"
:data-source="sources ?? []"
:pagination="false"
:loading="isLoading"
:loading="vReload"
bordered
>
<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">
<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 key="action" :title="$t('labels.actions')" :width="180">
<template #default="{ record }">
<div class="flex items-center gap-2">
<MdiEditOutline v-e="['c:base:edit:rename']" class="nc-action-btn" />
<MdiDeleteOutline class="nc-action-btn" />
<a-tooltip>
<template #title>Sync Metadata {{ metadiffbases.includes(record.id) ? '(Out of sync)' : '' }}</template>
<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>
</template>
</a-table-column>
</a-table>
</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>
</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>
<div class="w-full h-full !p-0 h-70vh">
<ErdView />
<ErdView :base-id="props.baseId" />
</div>
</template>

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

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

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

@ -1,15 +1,21 @@
<script setup lang="ts">
import type { FunctionalComponent, SVGAttributes } from 'vue'
import AppStore from './AppStore.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 TeamFillIcon from '~icons/ri/team-fill'
import MultipleTableIcon from '~icons/mdi/table-multiple'
import NotebookOutline from '~icons/mdi/notebook-outline'
import FolderCog from '~icons/mdi/folder-cog'
import { DataSourcesSubTab } from '~~/lib'
interface Props {
modelValue: boolean
openKey?: string
dataSourcesState?: string
}
interface SubTabGroup {
@ -41,6 +47,9 @@ const { t } = useI18n()
const { $e } = useNuxtApp()
const dataSourcesState = ref(props.dataSourcesState)
const dataSourcesReload = ref(false)
const tabsInfo: TabGroup = {
teamAndAuth: {
title: t('title.teamAndAuth'),
@ -69,60 +78,33 @@ const tabsInfo: TabGroup = {
$e('c:settings:team-auth')
},
},
...(isUIAllowed('appStore')
? {
appStore: {
// App Store
title: t('title.appStore'),
icon: StoreFrontOutline,
subTabs: {
new: {
title: 'Apps',
body: resolveComponent('DashboardSettingsAppStore'),
},
},
onClick: () => {
$e('c:settings:appstore')
},
},
}
: {}),
projMetaData: {
// Project Metadata
title: t('title.projMeta'),
appStore: {
// App Store
title: t('title.appStore'),
icon: StoreFrontOutline,
subTabs: {
new: {
title: 'Apps',
body: AppStore,
},
},
onClick: () => {
$e('c:settings:appstore')
},
},
dataSources: {
// Data Sources
title: 'Data Sources',
icon: MultipleTableIcon,
subTabs: {
dataSources: {
title: 'Data Sources',
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: () => {
$e('c:settings:erd')
},
},
misc: {
title: t('general.misc'),
body: resolveComponent('DashboardSettingsMisc'),
},
},
onClick: () => {
$e('c:settings:proj-metadata')
dataSourcesState.value = ''
$e('c:settings:data-sources')
},
},
audit: {
@ -140,6 +122,21 @@ const tabsInfo: TabGroup = {
$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]
@ -163,6 +160,15 @@ watch(
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>
<template>
@ -215,7 +221,12 @@ watch(
<!-- Sub Tabs -->
<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
v-for="(tab, key) of selectedTab.subTabs"
:key="key"
@ -225,8 +236,52 @@ watch(
{{ tab.title }}
</a-menu-item>
</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>
</a-modal>

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

@ -13,6 +13,12 @@ import {
viewIcons,
} from '#imports'
interface Props {
baseId: string
}
const props = defineProps<Props>()
const { t } = useI18n()
const { $api, $e } = useNuxtApp()
@ -32,8 +38,9 @@ const searchInput = $ref('')
const filteredTables = computed(() =>
tables.filter(
(el) =>
(typeof el?._ptn === 'string' && el._ptn.toLowerCase().includes(searchInput.toLowerCase())) ||
(typeof el?.title === 'string' && el.title.toLowerCase().includes(searchInput.toLowerCase())),
el?.base_id === props.baseId &&
((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 {
modelValue?: boolean
tableMeta: TableType
baseId: string
}
const { tableMeta, ...props } = defineProps<Props>()
const { tableMeta, baseId, ...props } = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'updated'])
@ -57,11 +58,11 @@ const validators = computed(() => {
validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => {
let tableNameLengthLimit = 255
if (isMysql) {
if (isMysql(baseId)) {
tableNameLengthLimit = 64
} else if (isPg) {
} else if (isPg(baseId)) {
tableNameLengthLimit = 63
} else if (isMssql) {
} else if (isMssql(baseId)) {
tableNameLengthLimit = 128
}
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 { reactive, ref, useMetas, useProject, watch } from '#imports'
const { table } = defineProps<{ table?: TableType }>()
const props = defineProps<{ table?: TableType; baseId?: string }>()
const { tables: projectTables } = useProject()
@ -18,7 +18,7 @@ const config = reactive<ERDConfig>({
showPkAndFk: true,
showViews: false,
showAllColumns: true,
singleTableMode: !!table,
singleTableMode: !!props.table,
showMMTables: false,
showJunctionTableNames: false,
})
@ -34,14 +34,13 @@ const loadMetaOfTablesNotInMetas = async (localTables: TableType[]) => {
}
const populateTables = async () => {
let localTables: TableType[]
if (table) {
let localTables: TableType[] = []
if (props.table) {
// if table is provided only get the table and its related tables
localTables = projectTables.value.filter(
(t) =>
t.id === table.id ||
table.columns?.find(
t.id === props.table.id ||
props.table.columns?.find(
(column) =>
column.uidt === UITypes.LinkToAnotherRecord &&
(column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id === t.id,
@ -59,7 +58,7 @@ const populateTables = async () => {
config.showMMTables ||
(!config.showMMTables && !t.mm) ||
// 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)
@ -76,6 +75,8 @@ watch(config, populateTables, {
deep: true,
})
const filteredTables = computed(() => tables.value.filter((t) => !props.baseId || t.base_id === props.baseId))
watch(
() => config.showAllColumns,
() => {
@ -87,7 +88,7 @@ watch(
<template>
<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">
<LazyErdFlow :tables="filteredTables" :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" />

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({
...validators,
})
const { isPg } = useProject()
const currencyList = currencyCodes || []
const currencyLocaleList = currencyLocales() || []

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

@ -10,7 +10,7 @@ const cellValue = inject(CellValueInj)
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))

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

@ -27,7 +27,7 @@ interface ValidationsObj {
const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState(
(meta: Ref<TableType | undefined>, column: Ref<ColumnType | undefined>) => {
const { sqlUis } = useProject()
const { sqlUis, isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc } = useProject()
const { $api } = useNuxtApp()
const { getMeta } = useMetas()
@ -40,6 +40,12 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
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 additionalValidations = ref<ValidationsObj>({})
@ -273,6 +279,9 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
isEdit,
column,
sqlUi,
isMssql,
isPg,
isMysql,
}
},
)

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

@ -68,19 +68,19 @@ const [setup, use] = useInjectionState(() => {
return temp
})
function getBaseType(baseId: string) {
function getBaseType(baseId?: string) {
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))
}
function isMssql(baseId: string) {
function isMssql(baseId?: string) {
return getBaseType(baseId) === 'mssql'
}
function isPg(baseId: string) {
function isPg(baseId?: string) {
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 CellUrlDisableOverlayInj: InjectionKey<Ref<boolean>> = Symbol('cell-url-disable-url')
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_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 dataSourcesState = ref<string>()
const dropdownOpen = ref(false)
/** Sidebar ref */
@ -73,11 +75,14 @@ const logout = () => {
navigateTo('/signin')
}
function toggleDialog(value?: boolean, key?: string) {
function toggleDialog(value?: boolean, key?: string, dsState?: string) {
dialogOpen.value = value ?? !dialogOpen.value
openDialogKey.value = key
dataSourcesState.value = dsState
}
provide(ToggleDialogInj, toggleDialog)
const handleThemeColor = async (mode: 'swatch' | 'primary' | 'accent', color?: string) => {
switch (mode) {
case 'swatch': {
@ -559,12 +564,12 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
</div>
</div>
<LazyDashboardTreeView />
<LazyDashboardTreeView @create-base-dlg="toggleDialog(true, 'dataSources')" />
</a-layout-sider>
</template>
<div>
<LazyDashboardSettingsModal v-model="dialogOpen" :open-key="openDialogKey" />
<div :key="$route.fullPath.split('?')[0]">
<LazyDashboardSettingsModal v-model="dialogOpen" :open-key="openDialogKey" :data-sources-state="dataSourcesState" />
<NuxtPage :page-key="$route.params.projectId" />

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

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

Loading…
Cancel
Save