Browse Source

Merge branch 'develop' into fix/gui-v2-shared-base-issues

pull/3234/head
Wing-Kam Wong 2 years ago
parent
commit
b4f50a5616
  1. 23
      packages/nc-gui-v2/assets/style-v2.scss
  2. 1
      packages/nc-gui-v2/components.d.ts
  3. 182
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  4. 48
      packages/nc-gui-v2/components/dlg/TableCreate.vue
  5. 10
      packages/nc-gui-v2/components/general/MiniSidebar.vue
  6. 10
      packages/nc-gui-v2/components/general/PreviewAs.vue
  7. 2
      packages/nc-gui-v2/components/general/ShareBaseButton.vue
  8. 55
      packages/nc-gui-v2/components/general/language/Menu.vue
  9. 30
      packages/nc-gui-v2/components/general/language/index.vue
  10. 2
      packages/nc-gui-v2/components/smartsheet/expanded-form/Comments.vue
  11. 2
      packages/nc-gui-v2/components/smartsheet/expanded-form/Header.vue
  12. 2
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue
  13. 2
      packages/nc-gui-v2/components/smartsheet/sidebar/index.vue
  14. 7
      packages/nc-gui-v2/composables/useTable.ts
  15. 22
      packages/nc-gui-v2/layouts/base.vue
  16. 10
      packages/nc-gui-v2/layouts/shared-view.vue
  17. 1
      packages/nc-gui-v2/lib/constants.ts
  18. 106
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue
  19. 19
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index.vue
  20. 44
      packages/nocodb/src/lib/meta/api/columnApis.ts
  21. 8
      packages/nocodb/src/lib/models/Column.ts
  22. 2
      packages/nocodb/src/lib/models/Model.ts

23
packages/nc-gui-v2/assets/style-v2.scss

@ -213,11 +213,25 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
} }
} }
.ant-tabs-dropdown-menu-title-content{ .ant-dropdown-menu-submenu {
@apply flex items-center; @apply !py-0;
.ant-dropdown-menu, .ant-menu {
@apply m-0 p-0;
}
.ant-menu-item {
@apply !m-0 !px-2;
}
} }
.ant-dropdown-menu-submenu-popup {
@apply scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !shadow !rounded;
}
.ant-tabs-dropdown-menu-title-content {
@apply flex items-center;
}
.ant-dropdown-menu-item-group-list { .ant-dropdown-menu-item-group-list {
@apply !mx-0; @apply !mx-0;
@ -231,10 +245,11 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
@apply m-0; @apply m-0;
} }
.ant-dropdown-menu-item { .ant-dropdown-menu-item, .ant-menu-item {
@apply !py-0 active:(ring ring-pink-500); @apply !py-0 active:(ring ring-pink-500);
} }
.ant-dropdown-menu-title-conten{ .ant-dropdown-menu-title-content,
.ant-menu-title-content {
@apply !py-0; @apply !py-0;
} }

1
packages/nc-gui-v2/components.d.ts vendored

@ -15,6 +15,7 @@ declare module '@vue/runtime-core' {
ACardMeta: typeof import('ant-design-vue/es')['CardMeta'] ACardMeta: typeof import('ant-design-vue/es')['CardMeta']
ACarousel: typeof import('ant-design-vue/es')['Carousel'] ACarousel: typeof import('ant-design-vue/es')['Carousel']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
ACol: typeof import('ant-design-vue/es')['Col'] ACol: typeof import('ant-design-vue/es')['Col']
ACollapse: typeof import('ant-design-vue/es')['Collapse'] ACollapse: typeof import('ant-design-vue/es')['Collapse']
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel'] ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']

182
packages/nc-gui-v2/components/dashboard/TreeView.vue

@ -2,11 +2,23 @@
import type { TableType } from 'nocodb-sdk' import type { TableType } from 'nocodb-sdk'
import Sortable from 'sortablejs' import Sortable from 'sortablejs'
import { Empty } from 'ant-design-vue' import { Empty } from 'ant-design-vue'
import { useNuxtApp } from '#app' import {
import { computed, useProject, useTable, useTabs, useUIPermission, watchEffect } from '#imports' computed,
inject,
reactive,
ref,
useDialog,
useNuxtApp,
useProject,
useTable,
useTabs,
useUIPermission,
watchEffect,
} from '#imports'
import DlgAirtableImport from '~/components/dlg/AirtableImport.vue' import DlgAirtableImport from '~/components/dlg/AirtableImport.vue'
import DlgQuickImport from '~/components/dlg/QuickImport.vue' import DlgQuickImport from '~/components/dlg/QuickImport.vue'
import DlgTableCreate from '~/components/dlg/TableCreate.vue' import DlgTableCreate from '~/components/dlg/TableCreate.vue'
import DlgTableRename from '~/components/dlg/TableRename.vue'
import { TabType } from '~/composables' import { TabType } from '~/composables'
import MdiView from '~icons/mdi/eye-circle-outline' import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large' import MdiTableLarge from '~icons/mdi/table-large'
@ -27,16 +39,25 @@ const { isUIAllowed } = useUIPermission()
const isLocked = inject('TreeViewIsLockedInj') const isLocked = inject('TreeViewIsLockedInj')
const tablesById = $computed<Record<string, TableType>>(() => let key = $ref(0)
tables?.value?.reduce((acc: Record<string, TableType>, table: TableType) => {
acc[table.id as string] = table const menuRef = $ref<HTMLLIElement>()
const filterQuery = $ref('')
const activeTable = computed(() => ([TabType.TABLE, TabType.VIEW].includes(activeTab.value?.type) ? activeTab.value.title : null))
const tablesById = $computed(() =>
tables.value?.reduce((acc: Record<string, TableType>, table) => {
acc[table.id!] = table
return acc return acc
}, {}), }, {}),
) )
let key = $ref(0) const filteredTables = $computed(() =>
tables.value?.filter((table) => !filterQuery || table.title.toLowerCase().includes(filterQuery.toLowerCase())),
const menuRef = $ref<HTMLLIElement>() )
let sortable: Sortable let sortable: Sortable
@ -104,37 +125,44 @@ const icon = (table: TableType) => {
} }
} }
const filterQuery = $ref('')
const filteredTables = $computed(() => {
return tables?.value?.filter((table) => !filterQuery || table?.title.toLowerCase()?.includes(filterQuery.toLowerCase()))
})
const contextMenuTarget = reactive<{ type?: 'table' | 'main'; value?: any }>({}) const contextMenuTarget = reactive<{ type?: 'table' | 'main'; value?: any }>({})
const setMenuContext = (type: 'table' | 'main', value?: any) => { const setMenuContext = (type: 'table' | 'main', value?: any) => {
contextMenuTarget.type = type contextMenuTarget.type = type
contextMenuTarget.value = value contextMenuTarget.value = value
$e('c:table:create:navdraw:right-click') $e('c:table:create:navdraw:right-click')
} }
const renameTableDlg = ref(false)
const renameTableMeta = ref()
const showRenameTableDlg = (table: TableType, rightClick = false) => {
$e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options')
renameTableMeta.value = table
renameTableDlg.value = true
}
const reloadTables = async () => { const reloadTables = async () => {
$e('a:table:refresh:navdraw') $e('a:table:refresh:navdraw')
await loadTables() await loadTables()
} }
const addTableTab = (table: TableType) => { const addTableTab = (table: TableType) => {
$e('a:table:open') $e('a:table:open')
addTab({ title: table.title, id: table.id, type: table.type as any }) addTab({ title: table.title, id: table.id, type: table.type as any })
} }
const activeTable = computed(() => { function openRenameTableDialog(table: TableType, rightClick = false) {
return [TabType.TABLE, TabType.VIEW].includes(activeTab.value?.type) ? activeTab.value.title : null $e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options')
})
const isOpen = ref(true)
const { close } = useDialog(DlgTableRename, {
'modelValue': isOpen,
'tableMeta': table,
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
function openQuickImportDialog(type: string) { function openQuickImportDialog(type: string) {
$e(`a:actions:import-${type}`) $e(`a:actions:import-${type}`)
@ -195,30 +223,29 @@ function openTableCreateDialog() {
<div <div
class="pt-2 pl-2 pb-2 flex-1 overflow-y-auto flex flex-column scrollbar-thin-dull" class="pt-2 pl-2 pb-2 flex-1 overflow-y-auto flex flex-column scrollbar-thin-dull"
:class="{ 'mb-[20px]': isSharedBase }" :class="{ 'mb-[20px]': isSharedBase }"
style="direction: rtl"
> >
<div <div class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer" @contextmenu="setMenuContext('main')">
style="direction: ltr"
class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer"
@contextmenu="setMenuContext('main')"
>
<span class="flex-grow text-bold uppercase nc-project-tree text-gray-500 font-weight-bold"> <span class="flex-grow text-bold uppercase nc-project-tree text-gray-500 font-weight-bold">
{{ $t('objects.tables') }} {{ $t('objects.tables') }}
<template v-if="tables?.length"> ({{ tables.length }}) </template> <template v-if="tables?.length"> ({{ tables.length }}) </template>
</span> </span>
</div> </div>
<div style="direction: ltr" class="flex-1">
<div class="flex-1">
<div <div
class="group flex items-center gap-2 pl-5 pr-3 py-2 text-primary/70 hover:(text-primary/100) cursor-pointer select-none" class="group flex items-center gap-2 pl-5 pr-3 py-2 text-primary/70 hover:(text-primary/100) cursor-pointer select-none"
@click="openTableCreateDialog" @click="openTableCreateDialog"
> >
<MdiPlus /> <MdiPlus />
<span class="text-gray-500 group-hover:(text-primary/100) flex-1">{{ $t('tooltip.addTable') }}</span> <span class="text-gray-500 group-hover:(text-primary/100) flex-1">{{ $t('tooltip.addTable') }}</span>
<a-dropdown v-if="!isSharedBase" :trigger="['click']" @click.stop> <a-dropdown v-if="!isSharedBase" :trigger="['click']" @click.stop>
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100" /> <MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay> <template #overlay>
<a-menu class="nc-add-project-menu !py-0 ml-6 rounded text-sm"> <a-menu class="!py-0 rounded text-sm">
<a-menu-item-group title="QUICK IMPORT FROM" class="!px-0 !mx-0"> <a-menu-item-group title="QUICK IMPORT FROM" class="!px-0 !mx-0">
<a-menu-item <a-menu-item
v-if="isUIAllowed('airtableImport')" v-if="isUIAllowed('airtableImport')"
@ -321,23 +348,18 @@ function openTableCreateDialog() {
<MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" /> <MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay> <template #overlay>
<a-menu class="cursor-pointer"> <a-menu class="!py-0 rounded text-sm">
<a-menu-item <a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(table)">
v-if="isUIAllowed('table-rename')" <div class="nc-project-menu-item">
v-t="['c:table:rename']" {{ $t('general.rename') }}
class="!text-xs" </div>
@click="showRenameTableDlg(table)" </a-menu-item>
><div>{{ $t('general.rename') }}</div></a-menu-item
> <a-menu-item v-if="isUIAllowed('table-delete')" @click="() => $e('c:table:delete') && deleteTable(table)">
<div class="nc-project-menu-item">
<a-menu-item {{ $t('general.delete') }}
v-if="isUIAllowed('table-delete')" </div>
v-t="['c:table:delete']" </a-menu-item>
class="!text-xs"
@click="deleteTable(table)"
>
{{ $t('general.delete') }}</a-menu-item
>
</a-menu> </a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
@ -350,35 +372,38 @@ function openTableCreateDialog() {
<div class="flex flex-col align-center"> <div class="flex flex-col align-center">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" /> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<a-button type="primary" @click.stop="openTableCreateDialog">{{ $t('tooltip.addTable') }}</a-button> <a-button type="primary" @click.stop="openTableCreateDialog">
{{ $t('tooltip.addTable') }}
</a-button>
</div> </div>
</a-card> </a-card>
</div> </div>
</div> </div>
<template v-if="!isLocked && !isSharedBase" #overlay> <template v-if="!isLocked && !isSharedBase" #overlay>
<a-menu class="cursor-pointer"> <a-menu class="!py-0 rounded text-sm">
<template v-if="contextMenuTarget.type === 'table'"> <template v-if="contextMenuTarget.type === 'table'">
<a-menu-item <a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(contextMenuTarget.value)">
v-if="isUIAllowed('table-rename')" <div class="nc-project-menu-item">
v-t="['c:table:rename']" {{ $t('general.rename') }}
class="!text-xs" </div>
@click="showRenameTableDlg(contextMenuTarget.value)"
>
{{ $t('general.rename') }}
</a-menu-item> </a-menu-item>
<a-menu-item <a-menu-item
v-if="isUIAllowed('table-delete')" v-if="isUIAllowed('table-delete')"
v-t="['c:table:delete']" @click="() => $e('c:table:delete') && deleteTable(contextMenuTarget.value)"
class="!text-xs"
@click="deleteTable(contextMenuTarget.value)"
> >
{{ $t('general.delete') }} <div class="nc-project-menu-item">
{{ $t('general.delete') }}
</div>
</a-menu-item> </a-menu-item>
</template> </template>
<template v-else> <template v-else>
<a-menu-item v-t="['c:table:reload']" class="!text-xs" @click="reloadTables"> <a-menu-item @click="reloadTables">
{{ $t('general.reload') }} <div class="nc-project-menu-item">
{{ $t('general.reload') }}
</div>
</a-menu-item> </a-menu-item>
</template> </template>
</a-menu> </a-menu>
@ -388,15 +413,8 @@ function openTableCreateDialog() {
<a-divider class="mt-0 mb-0" /> <a-divider class="mt-0 mb-0" />
<div class="items-center flex justify-center p-2"> <div class="items-center flex justify-center p-2">
<!--
Todo : move the component
<GithubStarButton />
-->
<GeneralShareBaseButton class="!mr-0" /> <GeneralShareBaseButton class="!mr-0" />
</div> </div>
<DlgTableRename v-if="renameTableMeta" v-model="renameTableDlg" :table-meta="renameTableMeta" />
</div> </div>
</template> </template>
@ -472,4 +490,28 @@ function openTableCreateDialog() {
@apply pr-6 !border-0; @apply pr-6 !border-0;
} }
} }
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply !mx-0;
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
:deep(.ant-dropdown-menu-item) {
@apply !py-0 active:(ring ring-pink-500);
}
:deep(.ant-dropdown-menu-title-content) {
@apply !p-0;
}
</style> </style>

48
packages/nc-gui-v2/components/dlg/TableCreate.vue

@ -45,6 +45,11 @@ const validators = computed(() => {
}) })
const { validateInfos } = useForm(table, validators) const { validateInfos } = useForm(table, validators)
const systemColumnsCheckboxInfo = SYSTEM_COLUMNS.map((c, index) => ({
value: c,
disabled: index === 0,
}))
onMounted(() => { onMounted(() => {
generateUniqueTitle() generateUniqueTitle()
@ -78,8 +83,8 @@ onMounted(() => {
/> />
</a-form-item> </a-form-item>
<div class="flex justify-end"> <div class="flex justify-end items-center">
<div class="pointer" @click="isAdvanceOptVisible = !isAdvanceOptVisible"> <div class="pointer flex flex-row items-center gap-x-1" @click="isAdvanceOptVisible = !isAdvanceOptVisible">
{{ isAdvanceOptVisible ? 'Hide' : 'Show' }} more {{ isAdvanceOptVisible ? 'Hide' : 'Show' }} more
<MdiMinusCircleOutline v-if="isAdvanceOptVisible" class="text-gray-500" /> <MdiMinusCircleOutline v-if="isAdvanceOptVisible" class="text-gray-500" />
@ -95,32 +100,29 @@ onMounted(() => {
</a-form-item> </a-form-item>
<div> <div>
<div class="mb-5"> <div class="mb-1">
<!-- Add Default Columns --> <!-- Add Default Columns -->
{{ $t('msg.info.addDefaultColumns') }} {{ $t('msg.info.addDefaultColumns') }}
</div> </div>
<a-row> <a-row>
<a-col :span="6"> <a-checkbox-group
<a-tooltip placement="top"> v-model:value="table.columns"
<template #title> :options="systemColumnsCheckboxInfo"
<span>ID column is required, you can rename this later if required.</span> class="!flex flex-row justify-between w-full"
</template> >
<a-checkbox v-model:checked="table.columns.id" disabled>ID</a-checkbox> <template #label="{ value }">
</a-tooltip> <a-tooltip v-if="value === 'id'" placement="top" class="!flex">
</a-col> <template #title>
<span>ID column is required, you can rename this later if required.</span>
<a-col :span="6"> </template>
<a-checkbox v-model:checked="table.columns.title"> title </a-checkbox> ID
</a-col> </a-tooltip>
<div v-else class="flex">
<a-col :span="6"> {{ value }}
<a-checkbox v-model:checked="table.columns.created_at"> created_at </a-checkbox> </div>
</a-col> </template>
</a-checkbox-group>
<a-col :span="6">
<a-checkbox v-model:checked="table.columns.updated_at"> updated_at </a-checkbox>
</a-col>
</a-row> </a-row>
</div> </div>
</div> </div>

10
packages/nc-gui-v2/components/general/MiniSidebar.vue

@ -34,7 +34,7 @@ const logout = () => {
</div> </div>
<template v-if="signedIn" #overlay> <template v-if="signedIn" #overlay>
<a-menu class="ml-2 !py-0 min-w-32 leading-8 !rounded"> <a-menu class="ml-2 !py-0 min-w-32 leading-8 !rounded nc-menu-account">
<a-menu-item-group title="User Settings"> <a-menu-item-group title="User Settings">
<a-menu-item key="email" class="!rounded-t"> <a-menu-item key="email" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user"> <nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user">
@ -129,12 +129,4 @@ const logout = () => {
} }
} }
} }
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
</style> </style>

10
packages/nc-gui-v2/components/general/PreviewAs.vue

@ -77,11 +77,13 @@ watch(previewAs, () => window.location.reload())
<template v-else> <template v-else>
<template v-for="role of roleList" :key="role.title"> <template v-for="role of roleList" :key="role.title">
<a-menu-item :class="`pointer nc-preview-${role.title}`" @click="previewAs = role.title"> <a-menu-item @click="previewAs = role.title">
<div class="nc-project-menu-item group"> <div class="nc-project-menu-item group">
<component :is="roleIcon[role.title]" class="group-hover:text-pink-500" /> <component :is="roleIcon[role.title]" class="group-hover:text-pink-500" />
<span class="capitalize" :class="{ 'x-active--text': role.title === previewAs }">{{ role.title }}</span> <span class="capitalize" :class="{ 'x-active--text': role.title === previewAs }">
{{ role.title }}
</span>
</div> </div>
</a-menu-item> </a-menu-item>
</template> </template>
@ -91,7 +93,9 @@ watch(previewAs, () => window.location.reload())
<div class="nc-project-menu-item group"> <div class="nc-project-menu-item group">
<MdiClose class="group-hover:text-pink-500" /> <MdiClose class="group-hover:text-pink-500" />
<!-- Reset Preview --> <!-- Reset Preview -->
<span class="text-capitalize text-xs whitespace-nowrap">{{ $t('activity.resetReview') }}</span> <span class="text-capitalize text-xs whitespace-nowrap">
{{ $t('activity.resetReview') }}
</span>
</div> </div>
</a-menu-item> </a-menu-item>
</template> </template>

2
packages/nc-gui-v2/components/general/ShareBaseButton.vue

@ -24,7 +24,7 @@ const { isUIAllowed } = useUIPermission()
@click="showUserModal = true" @click="showUserModal = true"
> >
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<mdi-account-supervisor-outline class="mr-1" /> <mdi-account-supervisor-outline class="mr-1 nc-share-base" />
<div>{{ $t('activity.share') }}</div> <div>{{ $t('activity.share') }}</div>
</div> </div>
</a-button> </a-button>

55
packages/nc-gui-v2/components/general/language/Menu.vue

@ -1,52 +1,61 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Language } from '~/lib' import { Language } from '~/lib'
import { onMounted, useGlobal, useI18n, useNuxtApp } from '#imports' import { onMounted, useGlobal, useI18n, useNuxtApp } from '#imports'
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { lang: currentLang } = useGlobal() const { lang: currentLang } = useGlobal()
const { availableLocales = ['en'], locale } = useI18n() const { availableLocales = ['en'], locale } = useI18n()
const languages = $computed(() => availableLocales.sort()) const languages = $computed(() => availableLocales.sort())
const isRtlLang = $computed(() => ['fa'].includes(currentLang.value)) const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
function applyDirection() { function applyDirection() {
const targetDirection = isRtlLang ? 'rtl' : 'ltr' const targetDirection = isRtlLang ? 'rtl' : 'ltr'
const oppositeDirection = targetDirection === 'ltr' ? 'rtl' : 'ltr' const oppositeDirection = targetDirection === 'ltr' ? 'rtl' : 'ltr'
document.body.classList.remove(oppositeDirection) document.body.classList.remove(oppositeDirection)
document.body.classList.add(targetDirection) document.body.classList.add(targetDirection)
document.body.style.direction = targetDirection document.body.style.direction = targetDirection
} }
function changeLanguage(lang: string) { function changeLanguage(lang: string) {
currentLang.value = lang currentLang.value = lang
locale.value = lang locale.value = lang
applyDirection() applyDirection()
$e('c:navbar:lang', { lang }) $e('c:navbar:lang', { lang })
} }
onMounted(() => { onMounted(() => {
applyDirection() applyDirection()
}) })
</script> </script>
<template> <template>
<a-menu class="scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !py-0 rounded"> <a-menu-item
<a-menu-item v-for="lang of languages"
v-for="lang of languages" :key="lang"
:key="lang" :class="lang === locale ? '!bg-primary/10 text-primary' : ''"
:class="lang === locale ? '!bg-primary/10 text-primary' : ''" class="group"
class="group" :value="lang"
:value="lang" @click="changeLanguage(lang)"
@click="changeLanguage(lang)" >
<div :class="lang === locale ? '!font-semibold !text-primary' : ''" class="nc-project-menu-item capitalize">
{{ Language[lang] || lang }}
</div>
</a-menu-item>
<a-menu-item class="mt-1">
<a
href="https://docs.nocodb.com/engineering/translation/#how-to-contribute--for-community-members"
target="_blank"
class="caption py-2 text-primary underline hover:opacity-75"
> >
<div :class="lang === locale ? '!font-semibold !text-primary' : ''" class="nc-project-menu-item capitalize"> {{ $t('activity.translate') }}
{{ Language[lang] || lang }} </a>
</div> </a-menu-item>
</a-menu-item>
<a-menu-item class="mt-1">
<a
href="https://docs.nocodb.com/engineering/translation/#how-to-contribute--for-community-members"
target="_blank"
class="caption py-2 text-primary underline hover:opacity-75"
>
{{ $t('activity.translate') }}
</a>
</a-menu-item>
</a-menu>
</template> </template>

30
packages/nc-gui-v2/components/general/language/index.vue

@ -1,33 +1,11 @@
<script lang="ts" setup>
const { subMenu } = defineProps<{ subMenu?: boolean }>()
</script>
<template> <template>
<GeneralLanguageMenu v-if="subMenu" /> <a-dropdown class="select-none color-transition" :trigger="['click']">
<a-dropdown v-else class="select-none color-transition" :trigger="['click']">
<MaterialSymbolsTranslate v-bind="$attrs" class="md:text-xl cursor-pointer nc-menu-translate" /> <MaterialSymbolsTranslate v-bind="$attrs" class="md:text-xl cursor-pointer nc-menu-translate" />
<template #overlay> <template #overlay>
<GeneralLanguageMenu /> <a-menu class="scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !py-0 rounded">
<GeneralLanguageMenu />
</a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
</template> </template>
<style scoped>
:deep(.ant-dropdown-menu-item-group-list) {
@apply !mx-0;
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
:deep(.ant-dropdown-menu-item) {
@apply !py-0 active:(ring ring-pink-500);
}
</style>

2
packages/nc-gui-v2/components/smartsheet/expanded-form/Comments.vue

@ -62,7 +62,7 @@ watch(
<div class="flex-shrink-1 mt-2 d-flex"> <div class="flex-shrink-1 mt-2 d-flex">
<a-input <a-input
v-model:value="comment" v-model:value="comment"
class="!text-xs" class="!text-xs nc-comment-box"
ghost ghost
:class="{ focus: showborder }" :class="{ focus: showborder }"
@focusin="showborder = true" @focusin="showborder = true"

2
packages/nc-gui-v2/components/smartsheet/expanded-form/Header.vue

@ -58,7 +58,7 @@ const iconColor = '#1890ff'
<component <component
:is="drawerToggleIcon" :is="drawerToggleIcon"
v-if="isUIAllowed('rowComments')" v-if="isUIAllowed('rowComments')"
class="cursor-pointer select-none" class="cursor-pointer select-none nc-toggle-comments"
@click="commentsDrawer = !commentsDrawer" @click="commentsDrawer = !commentsDrawer"
/> />

2
packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue

@ -114,7 +114,7 @@ function onOpenModal(type: ViewTypes, title = '') {
<div class="flex-auto justify-end flex flex-col gap-3 mt-3"> <div class="flex-auto justify-end flex flex-col gap-3 mt-3">
<button <button
v-if="isUIAllowed('virtualViewsCreateOrEdit')" v-if="isUIAllowed('virtualViewsCreateOrEdit')"
class="flex items-center gap-2 w-full mx-3 px-4 py-3 rounded border transform translate-x-4 hover:(translate-x-0 shadow-lg) transition duration-150 ease !text-xs" class="flex items-center gap-2 w-full mx-3 px-4 py-3 rounded border transform translate-x-4 hover:(translate-x-0 shadow-lg) transition duration-150 ease !text-xs nc-webhook-btn"
@click="onWebhooks" @click="onWebhooks"
> >
<mdi-hook />{{ $t('objects.webhooks') }} <mdi-hook />{{ $t('objects.webhooks') }}

2
packages/nc-gui-v2/components/smartsheet/sidebar/index.vue

@ -110,7 +110,7 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
<a-tooltip v-if="isUIAllowed('virtualViewsCreateOrEdit')" placement="left"> <a-tooltip v-if="isUIAllowed('virtualViewsCreateOrEdit')" placement="left">
<template #title> {{ $t('objects.webhooks') }}</template> <template #title> {{ $t('objects.webhooks') }}</template>
<div class="nc-sidebar-right-item hover:after:bg-gray-300"> <div class="nc-sidebar-right-item hover:after:bg-gray-300 nc-webhook-icon">
<MdiHook @click.stop /> <MdiHook @click.stop />
</div> </div>
</a-tooltip> </a-tooltip>

7
packages/nc-gui-v2/composables/useTable.ts

@ -1,16 +1,15 @@
import { Modal, message } from 'ant-design-vue' import { Modal, message } from 'ant-design-vue'
import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import { useProject } from './useProject'
import { TabType } from '~/composables/useTabs'
import { extractSdkResponseErrorMsg } from '~/utils'
import { useNuxtApp } from '#app' import { useNuxtApp } from '#app'
import { TabType } from '~/composables/useTabs'
import { SYSTEM_COLUMNS, extractSdkResponseErrorMsg, useProject } from '#imports'
export function useTable(onTableCreate?: (tableMeta: TableType) => void) { export function useTable(onTableCreate?: (tableMeta: TableType) => void) {
const table = reactive<{ title: string; table_name: string; columns: string[] }>({ const table = reactive<{ title: string; table_name: string; columns: string[] }>({
title: '', title: '',
table_name: '', table_name: '',
columns: ['id', 'title', 'created_at', 'updated_at'], columns: SYSTEM_COLUMNS,
}) })
const { $e, $api } = useNuxtApp() const { $e, $api } = useNuxtApp()

22
packages/nc-gui-v2/layouts/base.vue

@ -60,7 +60,7 @@ const logout = () => {
<template v-if="signedIn && !isSharedBase"> <template v-if="signedIn && !isSharedBase">
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<MdiDotsVertical class="md:text-xl cursor-pointer hover:text-pink-500" @click.prevent /> <MdiDotsVertical class="md:text-xl cursor-pointer hover:text-pink-500 nc-menu-accounts" @click.prevent />
<template #overlay> <template #overlay>
<a-menu class="!py-0 dark:(!bg-gray-800) leading-8 !rounded"> <a-menu class="!py-0 dark:(!bg-gray-800) leading-8 !rounded">
@ -108,26 +108,6 @@ const logout = () => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply !mx-0;
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
:deep(.ant-dropdown-menu-item) {
@apply !py-0 active:(ring ring-pink-500);
}
.nc-lang-btn { .nc-lang-btn {
@apply color-transition flex items-center justify-center fixed bottom-10 right-10 z-99 w-12 h-12 rounded-full shadow-md shadow-gray-500 p-2 !bg-primary text-white active:(ring ring-pink-500) hover:(ring ring-pink-500); @apply color-transition flex items-center justify-center fixed bottom-10 right-10 z-99 w-12 h-12 rounded-full shadow-md shadow-gray-500 p-2 !bg-primary text-white active:(ring ring-pink-500) hover:(ring ring-pink-500);

10
packages/nc-gui-v2/layouts/shared-view.vue

@ -40,13 +40,3 @@ export default {
</a-layout> </a-layout>
</a-layout> </a-layout>
</template> </template>
<style lang="scss" scoped>
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
</style>

1
packages/nc-gui-v2/lib/constants.ts

@ -1,2 +1,3 @@
export const NOCO = 'noco' export const NOCO = 'noco'
export const USER_PROJECT_ROLES = 'user_project_roles' export const USER_PROJECT_ROLES = 'user_project_roles'
export const SYSTEM_COLUMNS = ['id', 'title', 'created_at', 'updated_at']

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

@ -196,7 +196,7 @@ definePageMeta({
<a-menu-item key="copy"> <a-menu-item key="copy">
<div class="nc-project-menu-item group" @click.stop="copyProjectInfo"> <div class="nc-project-menu-item group" @click.stop="copyProjectInfo">
<MdiContentCopy class="group-hover:text-pink-500 nc-copy-project-info" /> <MdiContentCopy class="group-hover:text-pink-500" />
Copy Project Info Copy Project Info
</div> </div>
</a-menu-item> </a-menu-item>
@ -210,14 +210,14 @@ definePageMeta({
class="nc-project-menu-item group" class="nc-project-menu-item group"
@click.stop="openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.ncSiteUrl)" @click.stop="openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.ncSiteUrl)"
> >
<MdiApi class="group-hover:text-pink-500 nc-swagger-api-docs" /> <MdiApi class="group-hover:text-pink-500" />
Swagger: Rest APIs Swagger: Rest APIs
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item key="copy"> <a-menu-item key="copy">
<div v-t="['a:navbar:user:copy-auth-token']" class="nc-project-menu-item group" @click.stop="copyAuthToken"> <div v-t="['a:navbar:user:copy-auth-token']" class="nc-project-menu-item group" @click.stop="copyAuthToken">
<MdiScriptTextKeyOutline class="group-hover:text-pink-500 nc-copy-project-info" /> <MdiScriptTextKeyOutline class="group-hover:text-pink-500" />
Copy Auth Token Copy Auth Token
</div> </div>
</a-menu-item> </a-menu-item>
@ -231,55 +231,17 @@ definePageMeta({
class="nc-project-menu-item group" class="nc-project-menu-item group"
@click="toggleDialog(true, 'teamAndAuth')" @click="toggleDialog(true, 'teamAndAuth')"
> >
<MdiCog class="group-hover:text-pink-500 nc-team-settings" /> <MdiCog class="group-hover:text-pink-500" />
Team & Settings Team & Settings
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-divider /> <a-menu-divider />
<template v-if="signedIn && !isSharedBase"> <a-sub-menu v-if="isUIAllowed('previewAs')" key="preview-as">
<a-sub-menu v-if="isUIAllowed('previewAs')" key="account">
<template #title>
<div class="nc-project-menu-item group">
<MdiAccount class="group-hover:text-pink-500 nc-project-preview" />
Account
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<a-menu class="!py-0 dark:(!bg-gray-800) leading-8 !rounded">
<a-menu-item key="0" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="nc-project-menu-item group no-underline" to="/user">
<MdiAt class="mt-1 group-hover:text-pink-500" />&nbsp;
<span class="prose">{{ email }}</span>
</nuxt-link>
</a-menu-item>
<a-menu-item key="1" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout">
<MdiLogout class="group-hover:(!text-pink-500)" />&nbsp;
<span class="prose">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-menu>
</a-sub-menu>
</template>
<a-sub-menu v-if="isUIAllowed('previewAs')" key="preview-as" v-t="['c:navdraw:preview-as']">
<template #title> <template #title>
<div class="nc-project-menu-item group"> <div v-t="['c:navdraw:preview-as']" class="nc-project-menu-item group">
<MdiFileEyeOutline class="group-hover:text-pink-500 nc-project-preview" /> <MdiFileEyeOutline class="group-hover:text-pink-500" />
Preview Project As Preview Project As
<div class="flex-1" /> <div class="flex-1" />
@ -295,7 +257,7 @@ definePageMeta({
<GeneralPreviewAs /> <GeneralPreviewAs />
</a-sub-menu> </a-sub-menu>
<a-sub-menu v-if="isUIAllowed('previewAs')" key="language"> <a-sub-menu key="language" class="lang-menu scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !py-0">
<template #title> <template #title>
<div class="nc-project-menu-item group"> <div class="nc-project-menu-item group">
<MaterialSymbolsTranslate class="group-hover:text-pink-500 nc-language" /> <MaterialSymbolsTranslate class="group-hover:text-pink-500 nc-language" />
@ -309,12 +271,49 @@ definePageMeta({
</template> </template>
<template #expandIcon></template> <template #expandIcon></template>
<GeneralLanguage sub-menu /> <GeneralLanguageMenu />
</a-sub-menu> </a-sub-menu>
<template v-if="signedIn && !isSharedBase">
<a-sub-menu v-if="isUIAllowed('previewAs')" key="account">
<template #title>
<div class="nc-project-menu-item group">
<MdiAccount class="group-hover:text-pink-500" />
Account
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<a-menu-item key="0" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="nc-project-menu-item group no-underline" to="/user">
<MdiAt class="mt-1 group-hover:text-pink-500" />&nbsp;
<span class="prose-sm">{{ email }}</span>
</nuxt-link>
</a-menu-item>
<a-menu-item key="1" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout">
<MdiLogout class="group-hover:(!text-pink-500)" />&nbsp;
<span class="prose-sm">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-sub-menu>
</template>
</a-menu-item-group> </a-menu-item-group>
</a-menu> </a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
<div class="nc-sidebar-left-toggle-icon hover:after:bg-primary/75 group nc-sidebar-add-row flex align-center px-2"> <div class="nc-sidebar-left-toggle-icon hover:after:bg-primary/75 group nc-sidebar-add-row flex align-center px-2">
<MdiBackburger <MdiBackburger
class="cursor-pointer transform transition-transform duration-500" class="cursor-pointer transform transition-transform duration-500"
@ -337,18 +336,6 @@ definePageMeta({
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
:deep(.ant-dropdown-menu-item) {
@apply !py-0 active:(ring ring-pink-500);
}
:global(#nc-sidebar-left .ant-layout-sider-collapsed) { :global(#nc-sidebar-left .ant-layout-sider-collapsed) {
@apply !w-0 !max-w-0 !min-w-0 overflow-x-hidden; @apply !w-0 !max-w-0 !min-w-0 overflow-x-hidden;
} }
@ -356,6 +343,7 @@ definePageMeta({
.nc-left-sidebar { .nc-left-sidebar {
.nc-sidebar-left-toggle-icon { .nc-sidebar-left-toggle-icon {
@apply opacity-0 transition-opactity duration-200 transition-color text-white/80 hover:text-white/100; @apply opacity-0 transition-opactity duration-200 transition-color text-white/80 hover:text-white/100;
.nc-left-sidebar { .nc-left-sidebar {
@apply !border-r-0; @apply !border-r-0;
} }

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

@ -75,6 +75,7 @@ const { isOpen, toggle } = useSidebar()
:deep(.nc-root-tabs) { :deep(.nc-root-tabs) {
& > .ant-tabs-nav { & > .ant-tabs-nav {
@apply !mb-0 before:(!border-b-0); @apply !mb-0 before:(!border-b-0);
.ant-tabs-extra-content { .ant-tabs-extra-content {
@apply !bg-white/0; @apply !bg-white/0;
} }
@ -106,24 +107,6 @@ const { isOpen, toggle } = useSidebar()
} }
} }
.nc-add-project-menu {
:deep(.ant-dropdown-menu-item-group-list) {
@apply !mx-0;
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
:deep(.ant-dropdown-menu-item) {
@apply !py-0 active:(ring ring-pink-500);
}
}
:deep(.ant-menu-item-selected) { :deep(.ant-menu-item-selected) {
@apply text-inherit !bg-inherit; @apply text-inherit !bg-inherit;
} }

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

@ -557,6 +557,13 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
colBody.colOptions.options.push({ title: '' }); colBody.colOptions.options.push({ title: '' });
} }
} }
// Trim end of enum/set
if (colBody.dt === 'enum' || colBody.dt === 'set') {
for (const opt of colBody.colOptions.options) {
opt.title = opt.title.trimEnd()
}
}
if (colBody.uidt === UITypes.SingleSelect) { if (colBody.uidt === UITypes.SingleSelect) {
colBody.dtxp = (colBody.colOptions?.options.length) colBody.dtxp = (colBody.colOptions?.options.length)
@ -720,21 +727,6 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
) { ) {
colBody = getColumnPropsFromUIDT(colBody, base); colBody = getColumnPropsFromUIDT(colBody, base);
if (colBody.uidt === UITypes.SingleSelect) {
colBody.dtxp = (colBody.colOptions?.options.length)
? `${colBody.colOptions.options.map(o => `'${o.title.replace(/'/gi, '\'\'')}'`).join(',')}`
: '';
} else if (colBody.uidt === UITypes.MultiSelect){
colBody.dtxp = (colBody.colOptions?.options.length)
? `${colBody.colOptions.options.map((o) => {
if(o.title.includes(',')) {
NcError.badRequest('Illegal char(\',\') for MultiSelect');
}
return `'${o.title.replace(/'/gi, '\'\'')}'`;
}).join(',')}`
: '';
}
const baseModel = await Model.getBaseModelSQL({ const baseModel = await Model.getBaseModelSQL({
id: table.id, id: table.id,
dbDriver: NcConnectionMgrv2.get(base) dbDriver: NcConnectionMgrv2.get(base)
@ -809,6 +801,28 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
} }
} }
// Trim end of enum/set
if (colBody.dt === 'enum' || colBody.dt === 'set') {
for (const opt of colBody.colOptions.options) {
opt.title = opt.title.trimEnd()
}
}
if (colBody.uidt === UITypes.SingleSelect) {
colBody.dtxp = (colBody.colOptions?.options.length)
? `${colBody.colOptions.options.map(o => `'${o.title.replace(/'/gi, '\'\'')}'`).join(',')}`
: '';
} else if (colBody.uidt === UITypes.MultiSelect){
colBody.dtxp = (colBody.colOptions?.options.length)
? `${colBody.colOptions.options.map((o) => {
if(o.title.includes(',')) {
NcError.badRequest('Illegal char(\',\') for MultiSelect');
}
return `'${o.title.replace(/'/gi, '\'\'')}'`;
}).join(',')}`
: '';
}
// Handle option delete // Handle option delete
for (const option of column.colOptions.options.filter(oldOp => colBody.colOptions.options.find(newOp => newOp.id === oldOp.id) ? false : true)) { for (const option of column.colOptions.options.filter(oldOp => colBody.colOptions.options.find(newOp => newOp.id === oldOp.id) ? false : true)) {
if (!supportedDrivers.includes(driverType) && column.uidt === UITypes.MultiSelect) { if (!supportedDrivers.includes(driverType) && column.uidt === UITypes.MultiSelect) {

8
packages/nocodb/src/lib/models/Column.ts

@ -246,6 +246,10 @@ export default class Column<T = any> implements ColumnType {
} }
} else { } else {
for (const [i, option] of column.colOptions.options.entries() || [].entries()) { for (const [i, option] of column.colOptions.options.entries() || [].entries()) {
// Trim end of enum/set
if (column.dt === 'enum' || column.dt === 'set') {
option.title = option.title.trimEnd();
}
await SelectOption.insert( await SelectOption.insert(
{ {
...option, ...option,
@ -274,6 +278,10 @@ export default class Column<T = any> implements ColumnType {
} }
} else { } else {
for (const [i, option] of column.colOptions.options.entries() || [].entries()) { for (const [i, option] of column.colOptions.options.entries() || [].entries()) {
// Trim end of enum/set
if (column.dt === 'enum' || column.dt === 'set') {
option.title = option.title.trimEnd();
}
await SelectOption.insert( await SelectOption.insert(
{ {
...option, ...option,

2
packages/nocodb/src/lib/models/Model.ts

@ -94,7 +94,7 @@ export default class Model implements TableType {
public static async insert( public static async insert(
projectId, projectId,
baseId, baseId,
model: TableReqType & { model: Partial<TableReqType> & {
mm?: boolean; mm?: boolean;
created_at?: any; created_at?: any;
updated_at?: any; updated_at?: any;

Loading…
Cancel
Save