Browse Source

Merge pull request #4504 from nocodb/feat/improved-field-menu

Feat: Grid - Add advanced options in column header menu
pull/4545/head
navi 2 years ago committed by GitHub
parent
commit
e0e84987fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      packages/nc-gui/assets/style.scss
  2. 5
      packages/nc-gui/components.d.ts
  3. 2
      packages/nc-gui/components/account/SignupSettings.vue
  4. 26
      packages/nc-gui/components/smartsheet/Grid.vue
  5. 9
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  6. 5
      packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue
  7. 36
      packages/nc-gui/components/smartsheet/header/Cell.vue
  8. 191
      packages/nc-gui/components/smartsheet/header/Menu.vue
  9. 29
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  10. 10
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts
  11. 4
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue
  12. 16
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  13. 11
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  14. 6
      packages/nc-gui/composables/useColumnCreateStore.ts
  15. 6
      packages/nc-gui/composables/useSmartsheetStore.ts
  16. 1
      packages/nc-gui/lang/en.json
  17. 8
      packages/nc-gui/lib/enums.ts
  18. 16
      packages/nc-gui/utils/columnUtils.ts
  19. 196
      packages/nocodb-sdk/src/lib/Api.ts
  20. 50
      packages/nocodb/src/lib/meta/api/columnApis.ts
  21. 6
      packages/nocodb/src/lib/meta/api/sortApis.ts
  22. 7
      packages/nocodb/src/lib/models/Column.ts
  23. 8
      packages/nocodb/src/lib/models/GridViewColumn.ts
  24. 71
      packages/nocodb/src/lib/models/Sort.ts
  25. 42
      packages/nocodb/src/lib/models/View.ts
  26. 437
      scripts/sdk/swagger.json

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

@ -86,6 +86,11 @@ a {
@apply relative after:(absolute top-[-2px] right-[-2px] w-[8px] h-[8px] rounded-full bg-primary content-[''] !z-20); @apply relative after:(absolute top-[-2px] right-[-2px] w-[8px] h-[8px] rounded-full bg-primary content-[''] !z-20);
} }
// badge with count
.nc-count-badge {
@apply absolute flex items-center top-[-6px] right-[-6px] px-1 min-w-[14px] h-[14px] rounded-full bg-primary bg-opacity-100 text-white !text-[9px] !z-21;
}
// for highlighting toolbar menu item // for highlighting toolbar menu item
.nc-active-btn > .ant-btn { .nc-active-btn > .ant-btn {
@apply bg-primary bg-opacity-20 hover:(bg-primary bg-opacity-20); @apply bg-primary bg-opacity-20 hover:(bg-primary bg-opacity-20);

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

@ -168,6 +168,7 @@ declare module '@vue/runtime-core' {
MdiFileEyeOutline: typeof import('~icons/mdi/file-eye-outline')['default'] MdiFileEyeOutline: typeof import('~icons/mdi/file-eye-outline')['default']
MdiFileImageBox: typeof import('~icons/mdi/file-image-box')['default'] MdiFileImageBox: typeof import('~icons/mdi/file-image-box')['default']
MdiFilePlusOutline: typeof import('~icons/mdi/file-plus-outline')['default'] MdiFilePlusOutline: typeof import('~icons/mdi/file-plus-outline')['default']
MdiFileReplaceOutline: typeof import('~icons/mdi/file-replace-outline')['default']
MdiFileUploadOutline: typeof import('~icons/mdi/file-upload-outline')['default'] MdiFileUploadOutline: typeof import('~icons/mdi/file-upload-outline')['default']
MdiFilterOutline: typeof import('~icons/mdi/filter-outline')['default'] MdiFilterOutline: typeof import('~icons/mdi/filter-outline')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default'] MdiFlag: typeof import('~icons/mdi/flag')['default']
@ -210,11 +211,15 @@ declare module '@vue/runtime-core' {
MdiShieldKeyOutline: typeof import('~icons/mdi/shield-key-outline')['default'] MdiShieldKeyOutline: typeof import('~icons/mdi/shield-key-outline')['default']
MdiSlack: typeof import('~icons/mdi/slack')['default'] MdiSlack: typeof import('~icons/mdi/slack')['default']
MdiSort: typeof import('~icons/mdi/sort')['default'] MdiSort: typeof import('~icons/mdi/sort')['default']
MdiSortAscending: typeof import('~icons/mdi/sort-ascending')['default']
MdiSortDescending: typeof import('~icons/mdi/sort-descending')['default']
MdiStar: typeof import('~icons/mdi/star')['default'] MdiStar: typeof import('~icons/mdi/star')['default']
MdiStarOutline: typeof import('~icons/mdi/star-outline')['default'] MdiStarOutline: typeof import('~icons/mdi/star-outline')['default']
MdiStorefrontOutline: typeof import('~icons/mdi/storefront-outline')['default'] MdiStorefrontOutline: typeof import('~icons/mdi/storefront-outline')['default']
MdiTable: typeof import('~icons/mdi/table')['default'] MdiTable: typeof import('~icons/mdi/table')['default']
MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default'] MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default']
MdiTableColumnPlusAfter: typeof import('~icons/mdi/table-column-plus-after')['default']
MdiTableColumnPlusBefore: typeof import('~icons/mdi/table-column-plus-before')['default']
MdiTableLarge: typeof import('~icons/mdi/table-large')['default'] MdiTableLarge: typeof import('~icons/mdi/table-large')['default']
MdiText: typeof import('~icons/mdi/text')['default'] MdiText: typeof import('~icons/mdi/text')['default']
MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default'] MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default']

2
packages/nc-gui/components/account/SignupSettings.vue

@ -51,6 +51,6 @@ loadSettings()
<style scoped> <style scoped>
:deep(.ant-checkbox-wrapper) { :deep(.ant-checkbox-wrapper) {
@apply !flex-row-reverse !flex !justify-start gap-4; @apply !flex-row-reverse !flex !justify-start gap-4;
justify-content: start; justify-content: flex-start;
} }
</style> </style>

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

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk' import type { ColumnReqType, ColumnType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk' import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
@ -11,11 +11,13 @@ import {
IsGridInj, IsGridInj,
IsLockedInj, IsLockedInj,
MetaInj, MetaInj,
NavigateDir,
OpenNewRecordFormHookInj, OpenNewRecordFormHookInj,
PaginationDataInj, PaginationDataInj,
ReadonlyInj, ReadonlyInj,
ReloadRowDataHookInj, ReloadRowDataHookInj,
ReloadViewDataHookInj, ReloadViewDataHookInj,
SmartsheetStoreEvents,
computed, computed,
createEventHook, createEventHook,
enumColor, enumColor,
@ -42,7 +44,6 @@ import {
watch, watch,
} from '#imports' } from '#imports'
import type { Row } from '~/lib' import type { Row } from '~/lib'
import { NavigateDir } from '~/lib'
const { t } = useI18n() const { t } = useI18n()
@ -71,7 +72,7 @@ const isView = false
let editEnabled = $ref(false) let editEnabled = $ref(false)
const { xWhere, isPkAvail, isSqlView } = useSmartsheetStoreOrThrow() const { xWhere, isPkAvail, isSqlView, eventBus } = useSmartsheetStoreOrThrow()
const visibleColLength = $computed(() => fields.value?.length) const visibleColLength = $computed(() => fields.value?.length)
@ -544,6 +545,20 @@ watch(
}, },
{ immediate: true }, { immediate: true },
) )
const columnOrder = ref<Pick<ColumnReqType, 'column_order'> | null>(null)
eventBus.on(async (event, payload) => {
if (event === SmartsheetStoreEvents.FIELD_ADD) {
columnOrder.value = payload
addColumnDropdown.value = true
}
})
const closeAddColumnDropdown = () => {
columnOrder.value = null
addColumnDropdown.value = false
}
</script> </script>
<template> <template>
@ -619,8 +634,9 @@ watch(
<template #overlay> <template #overlay>
<SmartsheetColumnEditOrAddProvider <SmartsheetColumnEditOrAddProvider
v-if="addColumnDropdown" v-if="addColumnDropdown"
@submit="addColumnDropdown = false" :column-position="columnOrder"
@cancel="addColumnDropdown = false" @submit="closeAddColumnDropdown"
@cancel="closeAddColumnDropdown"
@click.stop @click.stop
@keydown.stop @keydown.stop
/> />

9
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useEventListener } from '@vueuse/core' import type { ColumnReqType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk' import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { import {
IsFormInj, IsFormInj,
@ -13,6 +13,7 @@ import {
ref, ref,
uiTypes, uiTypes,
useColumnCreateStoreOrThrow, useColumnCreateStoreOrThrow,
useEventListener,
useI18n, useI18n,
useMetas, useMetas,
useNuxtApp, useNuxtApp,
@ -22,6 +23,10 @@ import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline' import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
import MdiIdentifierIcon from '~icons/mdi/identifier' import MdiIdentifierIcon from '~icons/mdi/identifier'
const props = defineProps<{
columnPosition?: Pick<ColumnReqType, 'column_order'>
}>()
const emit = defineEmits(['submit', 'cancel']) const emit = defineEmits(['submit', 'cancel'])
const { formState, generateNewColumnMeta, addOrUpdate, onAlter, onUidtOrIdTypeChange, validateInfos, isEdit } = const { formState, generateNewColumnMeta, addOrUpdate, onAlter, onUidtOrIdTypeChange, validateInfos, isEdit } =
@ -71,7 +76,7 @@ const reloadMetaAndData = async () => {
} }
async function onSubmit() { async function onSubmit() {
const saved = await addOrUpdate(reloadMetaAndData) const saved = await addOrUpdate(reloadMetaAndData, props.columnPosition)
if (!saved) return if (!saved) return

5
packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue

@ -1,10 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
// todo: Remove this "Provider" component and use the "EditOrAdd" component directly // todo: Remove this "Provider" component and use the "EditOrAdd" component directly
import type { ColumnType } from 'nocodb-sdk' import type { ColumnReqType, ColumnType } from 'nocodb-sdk'
import { MetaInj, inject, ref, toRef, useProvideColumnCreateStore } from '#imports' import { MetaInj, inject, ref, toRef, useProvideColumnCreateStore } from '#imports'
interface Props { interface Props {
column?: ColumnType & { meta: any } column?: ColumnType & { meta: any }
columnPosition?: Pick<ColumnReqType, 'column_order'>
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@ -19,5 +20,5 @@ useProvideColumnCreateStore(meta, column)
</script> </script>
<template> <template>
<SmartsheetColumnEditOrAdd @submit="emit('submit')" @cancel="emit('cancel')" /> <SmartsheetColumnEditOrAdd :column-position="props.columnPosition" @submit="emit('submit')" @cancel="emit('cancel')" />
</template> </template>

36
packages/nc-gui/components/smartsheet/header/Cell.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk' import type { ColumnReqType, ColumnType } from 'nocodb-sdk'
import { ColumnInj, IsFormInj, IsKanbanInj, inject, provide, ref, toRef, useUIPermission } from '#imports' import { ColumnInj, IsFormInj, IsKanbanInj, inject, provide, ref, toRef, useUIPermission } from '#imports'
const props = defineProps<{ column: ColumnType & { meta: any }; required?: boolean | number; hideMenu?: boolean }>() const props = defineProps<{ column: ColumnType & { meta: any }; required?: boolean | number; hideMenu?: boolean }>()
@ -17,6 +17,18 @@ const { isUIAllowed } = useUIPermission()
provide(ColumnInj, column) provide(ColumnInj, column)
const editColumnDropdown = ref(false) const editColumnDropdown = ref(false)
const columnOrder = ref<Pick<ColumnReqType, 'column_order'> | null>(null)
const addField = async (payload) => {
columnOrder.value = payload
editColumnDropdown.value = true
}
const closeAddColumnDropdown = () => {
columnOrder.value = null
editColumnDropdown.value = false
}
</script> </script>
<template> <template>
@ -25,14 +37,25 @@ const editColumnDropdown = ref(false)
:class="{ 'h-full': column, '!text-gray-400': isKanban }" :class="{ 'h-full': column, '!text-gray-400': isKanban }"
> >
<SmartsheetHeaderCellIcon v-if="column" /> <SmartsheetHeaderCellIcon v-if="column" />
<span v-if="column" class="name" style="white-space: nowrap" :title="column.title">{{ column.title }}</span> <span
v-if="column"
class="name cursor-pointer"
style="white-space: nowrap"
:title="column.title"
@dblclick="editColumnDropdown = true"
>{{ column.title }}</span
>
<span v-if="(column.rqd && !column.cdf) || required" class="text-red-500">&nbsp;*</span> <span v-if="(column.rqd && !column.cdf) || required" class="text-red-500">&nbsp;*</span>
<template v-if="!hideMenu"> <template v-if="!hideMenu">
<div class="flex-1" /> <div class="flex-1" />
<LazySmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" @edit="editColumnDropdown = true" /> <LazySmartsheetHeaderMenu
v-if="!isForm && isUIAllowed('edit-column')"
@add-column="addField"
@edit="editColumnDropdown = true"
/>
</template> </template>
<a-dropdown <a-dropdown
@ -47,10 +70,11 @@ const editColumnDropdown = ref(false)
<template #overlay> <template #overlay>
<SmartsheetColumnEditOrAddProvider <SmartsheetColumnEditOrAddProvider
v-if="editColumnDropdown" v-if="editColumnDropdown"
:column="column" :column="columnOrder ? null : column"
:column-position="columnOrder"
class="w-full" class="w-full"
@submit="editColumnDropdown = false" @submit="closeAddColumnDropdown"
@cancel="editColumnDropdown = false" @cancel="closeAddColumnDropdown"
@click.stop @click.stop
@keydown.stop @keydown.stop
/> />

191
packages/nc-gui/components/smartsheet/header/Menu.vue

@ -1,27 +1,40 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { LinkToAnotherRecordType } from 'nocodb-sdk' import type { LinkToAnotherRecordType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk' import { RelationTypes, UITypes } from 'nocodb-sdk'
import { import {
ActiveViewInj,
ColumnInj, ColumnInj,
IsLockedInj, IsLockedInj,
MetaInj, MetaInj,
Modal, Modal,
ReloadViewDataHookInj,
SmartsheetStoreEvents,
defineEmits,
defineProps,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
getUniqueColumnName,
inject, inject,
message, message,
useI18n, useI18n,
useMetas, useMetas,
useNuxtApp, useNuxtApp,
useSmartsheetStoreOrThrow,
} from '#imports' } from '#imports'
const { virtual = false } = defineProps<{ virtual?: boolean }>() const { virtual = false } = defineProps<{ virtual?: boolean }>()
const emit = defineEmits(['edit']) const emit = defineEmits(['edit', 'addColumn'])
const { eventBus } = useSmartsheetStoreOrThrow()
const column = inject(ColumnInj) const column = inject(ColumnInj)
const reloadDataHook = inject(ReloadViewDataHookInj)
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj) const isLocked = inject(IsLockedInj)
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
@ -49,7 +62,7 @@ const deleteColumn = () =>
} }
$e('a:column:delete') $e('a:column:delete')
} catch (e: any) { } catch (e) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
}, },
@ -69,6 +82,131 @@ const setAsPrimaryValue = async () => {
message.error(t('msg.error.primaryColumnUpdateFailed')) message.error(t('msg.error.primaryColumnUpdateFailed'))
} }
} }
const sortByColumn = async (direction: 'asc' | 'desc') => {
try {
$e('a:sort:add', { from: 'column-menu' })
await $api.dbTableSort.create(view.value?.id as string, {
fk_column_id: column!.value.id,
direction,
push_to_top: true,
})
eventBus.emit(SmartsheetStoreEvents.SORT_RELOAD)
reloadDataHook?.trigger()
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const duplicateColumn = async () => {
let columnCreatePayload = {}
// generate duplicate column name
const duplicateColumnName = getUniqueColumnName(`${column!.value.title}_copy`, meta!.value!.columns!)
// construct column create payload
switch (column.value.uidt) {
case UITypes.LinkToAnotherRecord:
case UITypes.Lookup:
case UITypes.Rollup:
case UITypes.Formula:
return message.info('Not available at the moment')
case UITypes.SingleSelect:
case UITypes.MultiSelect:
columnCreatePayload = {
...column!.value!,
title: duplicateColumnName,
column_name: duplicateColumnName,
id: undefined,
order: undefined,
colOptions: {
options:
column.value.colOptions?.options?.map((option: Record<string, any>) => ({
...option,
id: undefined,
})) ?? [],
},
}
break
default:
columnCreatePayload = {
...column!.value!,
...(column!.value.colOptions ?? {}),
title: duplicateColumnName,
column_name: duplicateColumnName,
id: undefined,
colOptions: undefined,
order: undefined,
}
break
}
try {
const gridViewColumnList = await $api.dbViewColumn.list(view.value?.id as string)
const currentColumnIndex = gridViewColumnList.findIndex((f) => f.fk_column_id === column!.value.id)
let newColumnOrder
if (currentColumnIndex === gridViewColumnList.length - 1) {
newColumnOrder = gridViewColumnList[currentColumnIndex].order + 1
} else {
newColumnOrder = (gridViewColumnList[currentColumnIndex].order! + gridViewColumnList[currentColumnIndex + 1]?.order) / 2
}
await $api.dbTableColumn.create(meta!.value!.id!, {
...columnCreatePayload,
column_order: {
order: newColumnOrder,
view_id: view.value?.id as string,
},
})
await getMeta(meta!.value!.id!, true)
eventBus.emit(SmartsheetStoreEvents.FIELD_RELOAD)
message.success(t('msg.success.columnDuplicated'))
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
// add column before or after current column
const addColumn = async (before = false) => {
const gridViewColumnList = await $api.dbViewColumn.list(view.value?.id as string)
const currentColumnIndex = gridViewColumnList.findIndex((f) => f.fk_column_id === column!.value.id)
let newColumnOrder
if (before) {
if (currentColumnIndex === 0) {
newColumnOrder = gridViewColumnList[currentColumnIndex].order / 2
} else {
newColumnOrder = (gridViewColumnList[currentColumnIndex].order! + gridViewColumnList[currentColumnIndex - 1]?.order) / 2
}
} else {
if (currentColumnIndex === gridViewColumnList.length - 1) {
newColumnOrder = gridViewColumnList[currentColumnIndex].order + 1
} else {
newColumnOrder = (gridViewColumnList[currentColumnIndex].order! + gridViewColumnList[currentColumnIndex + 1]?.order) / 2
}
}
emit('addColumn', {
column_order: {
order: newColumnOrder,
view_id: view.value?.id as string,
},
})
}
// hide the field in view
const hideField = async () => {
const gridViewColumnList = await $api.dbViewColumn.list(view.value?.id as string)
const currentColumn = gridViewColumnList.find((f) => f.fk_column_id === column!.value.id)
await $api.dbViewColumn.update(view.value.id, currentColumn.id, { show: false })
eventBus.emit(SmartsheetStoreEvents.FIELD_RELOAD)
}
</script> </script>
<template> <template>
@ -84,6 +222,53 @@ const setAsPrimaryValue = async () => {
{{ $t('general.edit') }} {{ $t('general.edit') }}
</div> </div>
</a-menu-item> </a-menu-item>
<template v-if="column.uidt !== UITypes.LinkToAnotherRecord || column.colOptions.type !== RelationTypes.BELONGS_TO">
<a-divider class="!my-0" />
<a-menu-item @click="sortByColumn('asc')">
<div class="nc-column-insert-after nc-header-menu-item">
<MdiSortAscending class="text-primary" />
Sort Ascending
</div>
</a-menu-item>
<a-menu-item @click="sortByColumn('desc')">
<div class="nc-column-insert-before nc-header-menu-item">
<MdiSortDescending class="text-primary" />
Sort Descending
</div>
</a-menu-item>
</template>
<a-divider class="!my-0" />
<a-menu-item @click="hideField">
<div class="nc-column-insert-before nc-header-menu-item">
<MdiEyeOffOutline class="text-primary" />
Hide Field
</div>
</a-menu-item>
<a-divider class="!my-0" />
<a-menu-item
v-if="column.uidt !== UITypes.LinkToAnotherRecord && column.uidt !== UITypes.Lookup && !column.pk"
@click="duplicateColumn"
>
<div class="nc-column-duplicate nc-header-menu-item">
<MdiFileReplaceOutline class="text-primary" />
Duplicate
</div>
</a-menu-item>
<a-menu-item @click="addColumn()">
<div class="nc-column-insert-after nc-header-menu-item">
<MdiTableColumnPlusAfter class="text-primary" />
Insert After
</div>
</a-menu-item>
<a-menu-item @click="addColumn(true)">
<div class="nc-column-insert-before nc-header-menu-item">
<MdiTableColumnPlusBefore class="text-primary" />
Insert before
</div>
</a-menu-item>
<a-divider class="!my-0" />
<a-menu-item v-if="!virtual" @click="setAsPrimaryValue"> <a-menu-item v-if="!virtual" @click="setAsPrimaryValue">
<div class="nc-column-set-primary nc-header-menu-item"> <div class="nc-column-set-primary nc-header-menu-item">

29
packages/nc-gui/components/smartsheet/header/VirtualCell.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType } from 'nocodb-sdk' import type { ColumnReqType, ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType } from 'nocodb-sdk'
import { substituteColumnIdWithAliasInFormula } from 'nocodb-sdk' import { substituteColumnIdWithAliasInFormula } from 'nocodb-sdk'
import { import {
ColumnInj, ColumnInj,
@ -99,6 +99,18 @@ const tooltipMsg = computed(() => {
} }
return '' return ''
}) })
const columnOrder = ref<Pick<ColumnReqType, 'column_order'> | null>(null)
const addField = async (payload) => {
columnOrder.value = payload
editColumnDropdown.value = true
}
const closeAddColumnDropdown = () => {
columnOrder.value = null
editColumnDropdown.value = false
}
</script> </script>
<template> <template>
@ -117,7 +129,12 @@ const tooltipMsg = computed(() => {
<template v-if="!hideMenu"> <template v-if="!hideMenu">
<div class="flex-1" /> <div class="flex-1" />
<LazySmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" :virtual="true" @edit="editColumnDropdown = true" /> <LazySmartsheetHeaderMenu
v-if="!isForm && isUIAllowed('edit-column')"
:virtual="true"
@add-column="addField"
@edit="editColumnDropdown = true"
/>
</template> </template>
<a-dropdown <a-dropdown
@ -128,14 +145,14 @@ const tooltipMsg = computed(() => {
overlay-class-name="nc-dropdown-edit-column" overlay-class-name="nc-dropdown-edit-column"
> >
<div /> <div />
<template #overlay> <template #overlay>
<SmartsheetColumnEditOrAddProvider <SmartsheetColumnEditOrAddProvider
v-if="editColumnDropdown" v-if="editColumnDropdown"
:column="column" :column="columnOrder ? null : column"
:column-position="columnOrder"
class="w-full" class="w-full"
@submit="editColumnDropdown = false" @submit="closeAddColumnDropdown"
@cancel="editColumnDropdown = false" @cancel="closeAddColumnDropdown"
@click.stop @click.stop
@keydown.stop @keydown.stop
/> />

10
packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts

@ -12,7 +12,7 @@ import QrCodeScan from '~icons/mdi/qrcode-scan'
import RollupIcon from '~icons/mdi/movie-roll' import RollupIcon from '~icons/mdi/movie-roll'
import CountIcon from '~icons/mdi/counter' import CountIcon from '~icons/mdi/counter'
import SpecificDBTypeIcon from '~icons/mdi/database-settings' import SpecificDBTypeIcon from '~icons/mdi/database-settings'
import TableColumnPlusBefore from '~icons/mdi/table-column-plus-before' import MdiTextSearchVariant from '~icons/mdi/text-search-variant'
const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => { const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => {
switch (column.uidt) { switch (column.uidt) {
@ -35,13 +35,13 @@ const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => {
case UITypes.Lookup: case UITypes.Lookup:
switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) { switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY: case RelationTypes.MANY_TO_MANY:
return { icon: TableColumnPlusBefore, color: 'text-accent' } return { icon: MdiTextSearchVariant, color: 'text-accent' }
case RelationTypes.HAS_MANY: case RelationTypes.HAS_MANY:
return { icon: TableColumnPlusBefore, color: 'text-yellow-500' } return { icon: MdiTextSearchVariant, color: 'text-yellow-500' }
case RelationTypes.BELONGS_TO: case RelationTypes.BELONGS_TO:
return { icon: TableColumnPlusBefore, color: 'text-sky-500' } return { icon: MdiTextSearchVariant, color: 'text-sky-500' }
} }
return { icon: TableColumnPlusBefore, color: 'text-grey' } return { icon: MdiTextSearchVariant, color: 'text-grey' }
case UITypes.Rollup: case UITypes.Rollup:
switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) { switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY: case RelationTypes.MANY_TO_MANY:

4
packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue

@ -71,13 +71,15 @@ useMenuCloseOnEsc(open)
<template> <template>
<a-dropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-filter-menu"> <a-dropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-filter-menu">
<div :class="{ 'nc-badge nc-active-btn': filtersLength }"> <div :class="{ 'nc-active-btn': filtersLength }">
<a-button v-e="['c:filter']" class="nc-filter-menu-btn nc-toolbar-btn txt-sm" :disabled="isLocked"> <a-button v-e="['c:filter']" class="nc-filter-menu-btn nc-toolbar-btn txt-sm" :disabled="isLocked">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<MdiFilterOutline /> <MdiFilterOutline />
<!-- Filter --> <!-- Filter -->
<span class="text-capitalize !text-sm font-weight-normal">{{ $t('activity.filter') }}</span> <span class="text-capitalize !text-sm font-weight-normal">{{ $t('activity.filter') }}</span>
<MdiMenuDown class="text-grey" /> <MdiMenuDown class="text-grey" />
<span v-if="filtersLength" class="nc-count-badge">{{ filtersLength }}</span>
</div> </div>
</a-button> </a-button>
</div> </div>

16
packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue

@ -16,6 +16,7 @@ import {
resolveComponent, resolveComponent,
useMenuCloseOnEsc, useMenuCloseOnEsc,
useNuxtApp, useNuxtApp,
useSmartsheetStoreOrThrow,
useViewColumns, useViewColumns,
watch, watch,
} from '#imports' } from '#imports'
@ -46,8 +47,17 @@ const {
hideAll, hideAll,
saveOrUpdate, saveOrUpdate,
metaColumnById, metaColumnById,
loadViewColumns,
} = useViewColumns(activeView, meta, () => reloadDataHook.trigger()) } = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
const { eventBus } = useSmartsheetStoreOrThrow()
eventBus.on((event) => {
if (event === SmartsheetStoreEvents.FIELD_RELOAD) {
loadViewColumns()
}
})
watch( watch(
sortedAndFilteredFields, sortedAndFilteredFields,
(v) => { (v) => {
@ -56,7 +66,7 @@ watch(
{ immediate: true }, { immediate: true },
) )
const isAnyFieldHidden = computed(() => filteredFieldList.value?.some((field) => !field.show)) const numberOfHiddenFields = computed(() => filteredFieldList.value?.filter((field) => !field.show)?.length)
const onMove = (_event: { moved: { newIndex: number } }) => { const onMove = (_event: { moved: { newIndex: number } }) => {
// todo : sync with server // todo : sync with server
@ -128,7 +138,7 @@ useMenuCloseOnEsc(open)
<template> <template>
<a-dropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-fields-menu"> <a-dropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-fields-menu">
<div :class="{ 'nc-badge nc-active-btn': isAnyFieldHidden }"> <div :class="{ 'nc-active-btn': numberOfHiddenFields }">
<a-button v-e="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked"> <a-button v-e="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<MdiEyeOffOutline /> <MdiEyeOffOutline />
@ -137,6 +147,8 @@ useMenuCloseOnEsc(open)
<span class="text-capitalize !text-sm font-weight-normal">{{ $t('objects.fields') }}</span> <span class="text-capitalize !text-sm font-weight-normal">{{ $t('objects.fields') }}</span>
<MdiMenuDown class="text-grey" /> <MdiMenuDown class="text-grey" />
<span v-if="numberOfHiddenFields" class="nc-count-badge">{{ numberOfHiddenFields }}</span>
</div> </div>
</a-button> </a-button>
</div> </div>

11
packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue

@ -10,6 +10,7 @@ import {
inject, inject,
ref, ref,
useMenuCloseOnEsc, useMenuCloseOnEsc,
useSmartsheetStoreOrThrow,
useViewSorts, useViewSorts,
watch, watch,
} from '#imports' } from '#imports'
@ -19,8 +20,16 @@ const view = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
const reloadDataHook = inject(ReloadViewDataHookInj) const reloadDataHook = inject(ReloadViewDataHookInj)
const { eventBus } = useSmartsheetStoreOrThrow()
const { sorts, saveOrUpdate, loadSorts, addSort, deleteSort } = useViewSorts(view, () => reloadDataHook?.trigger()) const { sorts, saveOrUpdate, loadSorts, addSort, deleteSort } = useViewSorts(view, () => reloadDataHook?.trigger())
eventBus.on((event) => {
if (event === SmartsheetStoreEvents.SORT_RELOAD) {
loadSorts()
}
})
const columns = computed(() => meta.value?.columns || []) const columns = computed(() => meta.value?.columns || [])
const columnByID = computed(() => const columnByID = computed(() =>
@ -54,6 +63,8 @@ useMenuCloseOnEsc(open)
<!-- Sort --> <!-- Sort -->
<span class="text-capitalize !text-sm font-weight-normal">{{ $t('activity.sort') }}</span> <span class="text-capitalize !text-sm font-weight-normal">{{ $t('activity.sort') }}</span>
<MdiMenuDown class="text-grey" /> <MdiMenuDown class="text-grey" />
<span v-if="sorts?.length" class="nc-count-badge">{{ sorts.length }}</span>
</div> </div>
</a-button> </a-button>
</div> </div>

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

@ -1,5 +1,5 @@
import clone from 'just-clone' import clone from 'just-clone'
import type { ColumnType, TableType } from 'nocodb-sdk' import type { ColumnReqType, ColumnType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { import {
@ -191,7 +191,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
if (cdf) formState.value.cdf = formState.value.cdf || null if (cdf) formState.value.cdf = formState.value.cdf || null
} }
const addOrUpdate = async (onSuccess: () => void) => { const addOrUpdate = async (onSuccess: () => void, columnPosition?: Pick<ColumnReqType, 'column_order'>) => {
try { try {
if (!(await validate())) return if (!(await validate())) return
} catch (e) { } catch (e) {
@ -228,7 +228,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
// }; // };
// } // }
} }
await $api.dbTableColumn.create(meta.value?.id as string, formState.value) await $api.dbTableColumn.create(meta.value?.id as string, { ...formState.value, ...columnPosition })
/** if LTAR column then force reload related table meta */ /** if LTAR column then force reload related table meta */
if (formState.value.uidt === UITypes.LinkToAnotherRecord && meta.value?.id !== formState.value.childId) { if (formState.value.uidt === UITypes.LinkToAnotherRecord && meta.value?.id !== formState.value.childId) {

6
packages/nc-gui/composables/useSmartsheetStore.ts

@ -1,7 +1,8 @@
import { ViewTypes } from 'nocodb-sdk' import { ViewTypes } from 'nocodb-sdk'
import type { FilterType, KanbanType, SortType, TableType, ViewType } from 'nocodb-sdk' import type { FilterType, KanbanType, SortType, TableType, ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { computed, ref, unref, useFieldQuery, useInjectionState, useNuxtApp, useProject } from '#imports' import { computed, ref, unref, useEventBus, useFieldQuery, useInjectionState, useNuxtApp, useProject } from '#imports'
import type { SmartsheetStoreEvents } from '~/lib'
const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState( const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
( (
@ -19,6 +20,8 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
const { search } = useFieldQuery(view) const { search } = useFieldQuery(view)
const eventBus = useEventBus<SmartsheetStoreEvents>(Symbol('SmartsheetStore'))
// getters // getters
const isLocked = computed(() => view.value?.lock_type === 'locked') const isLocked = computed(() => view.value?.lock_type === 'locked')
const isPkAvail = computed(() => (meta.value as TableType)?.columns?.some((c) => c.pk)) const isPkAvail = computed(() => (meta.value as TableType)?.columns?.some((c) => c.pk))
@ -63,6 +66,7 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
sorts, sorts,
nestedFilters, nestedFilters,
isSqlView, isSqlView,
eventBus,
} }
}, },
'smartsheet-store', 'smartsheet-store',

1
packages/nc-gui/lang/en.json

@ -695,6 +695,7 @@
"futureRelease": "Coming soon!" "futureRelease": "Coming soon!"
}, },
"success": { "success": {
"columnDuplicated": "Column duplicated successfully",
"updatedUIACL": "Updated UI ACL for tables successfully", "updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully", "pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully", "pluginSettingsSaved": "Plugin settings saved successfully",

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

@ -77,3 +77,11 @@ export enum TabType {
VIEW = 'view', VIEW = 'view',
AUTH = 'auth', AUTH = 'auth',
} }
export enum SmartsheetStoreEvents {
SORT_RELOAD = 'sort-reload',
FILTER_RELOAD = 'filter-reload',
DATA_RELOAD = 'data-reload',
FIELD_RELOAD = 'field-reload',
FIELD_ADD = 'field-add',
}

16
packages/nc-gui/utils/columnUtils.ts

@ -1,7 +1,6 @@
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk' import { RelationTypes, UITypes } from 'nocodb-sdk'
import LinkVariant from '~icons/mdi/link-variant' import LinkVariant from '~icons/mdi/link-variant'
import TableColumnPlusBefore from '~icons/mdi/table-column-plus-before'
import QrCodeScan from '~icons/mdi/qrcode-scan' import QrCodeScan from '~icons/mdi/qrcode-scan'
import FormatColorText from '~icons/mdi/format-color-text' import FormatColorText from '~icons/mdi/format-color-text'
import TextSubject from '~icons/mdi/text-subject' import TextSubject from '~icons/mdi/text-subject'
@ -28,6 +27,7 @@ import MovieRoll from '~icons/mdi/movie-roll'
import CalendarClock from '~icons/mdi/calendar-clock' import CalendarClock from '~icons/mdi/calendar-clock'
import ID from '~icons/mdi/identifier' import ID from '~icons/mdi/identifier'
import RulerSquareCompass from '~icons/mdi/ruler-square-compass' import RulerSquareCompass from '~icons/mdi/ruler-square-compass'
import MdiTextSearchVariant from '~icons/mdi/text-search-variant'
const uiTypes = [ const uiTypes = [
{ {
@ -37,7 +37,7 @@ const uiTypes = [
}, },
{ {
name: UITypes.Lookup, name: UITypes.Lookup,
icon: TableColumnPlusBefore, icon: MdiTextSearchVariant,
virtual: 1, virtual: 1,
}, },
{ {
@ -180,4 +180,14 @@ const isColumnRequiredAndNull = (col: ColumnType, row: Record<string, any>) => {
return isColumnRequired(col) && (row[col.title!] === undefined || row[col.title!] === null) return isColumnRequired(col) && (row[col.title!] === undefined || row[col.title!] === null)
} }
export { uiTypes, getUIDTIcon, isColumnRequiredAndNull, isColumnRequired, isVirtualColRequired } const getUniqueColumnName = (initName: string, columns: ColumnType[]) => {
let name = initName
let i = 1
while (columns.find((c) => c.title === name)) {
name = `${initName}_${i}`
i++
}
return name
}
export { uiTypes, getUIDTIcon, getUniqueColumnName, isColumnRequiredAndNull, isColumnRequired, isVirtualColRequired }

196
packages/nocodb-sdk/src/lib/Api.ts

@ -571,91 +571,109 @@ export interface HookLogType {
updated_at?: string; updated_at?: string;
} }
export type ColumnReqType = export interface NormalColumnRequestType {
| { uidt?:
uidt?: | 'ID'
| 'ID' | 'SingleLineText'
| 'SingleLineText' | 'LongText'
| 'LongText' | 'Attachment'
| 'Attachment' | 'Checkbox'
| 'Checkbox' | 'MultiSelect'
| 'MultiSelect' | 'SingleSelect'
| 'SingleSelect' | 'Collaborator'
| 'Collaborator' | 'Date'
| 'Date' | 'Year'
| 'Year' | 'Time'
| 'Time' | 'PhoneNumber'
| 'PhoneNumber' | 'Email'
| 'Email' | 'URL'
| 'URL' | 'Number'
| 'Number' | 'Decimal'
| 'Decimal' | 'Currency'
| 'Currency' | 'Percent'
| 'Percent' | 'Duration'
| 'Duration' | 'Rating'
| 'Rating' | 'Count'
| 'Count' | 'DateTime'
| 'DateTime' | 'CreateTime'
| 'CreateTime' | 'LastModifiedTime'
| 'LastModifiedTime' | 'AutoNumber'
| 'AutoNumber' | 'Geometry'
| 'Geometry' | 'JSON'
| 'JSON' | 'SpecificDBType'
| 'SpecificDBType' | 'Barcode'
| 'Barcode' | 'Button';
| 'Button'; id?: string;
id?: string; base_id?: string;
base_id?: string; fk_model_id?: string;
fk_model_id?: string; title?: string;
title?: string; dt?: string;
dt?: string; np?: string;
np?: string; ns?: string;
ns?: string; clen?: string | number;
clen?: string | number; cop?: string;
cop?: string; pk?: boolean;
pk?: boolean; pv?: boolean;
pv?: boolean; rqd?: boolean;
rqd?: boolean; column_name?: string;
column_name?: string; un?: boolean;
un?: boolean; ct?: string;
ct?: string; ai?: boolean;
ai?: boolean; unique?: boolean;
unique?: boolean; cdf?: string;
cdf?: string; cc?: string;
cc?: string; csn?: string;
csn?: string; dtx?: string;
dtx?: string; dtxp?: string;
dtxp?: string; dtxs?: string;
dtxs?: string; au?: boolean;
au?: boolean; }
''?: string;
} export interface LinkToAnotherColumnReqType {
| { uidt: 'LinkToAnotherRecord';
uidt: 'LinkToAnotherRecord'; title: string;
title: string; virtual?: boolean;
parentId: string; parentId: string;
childId: string; childId: string;
type: 'hm' | 'bt' | 'mm'; type: 'hm' | 'bt' | 'mm';
} }
| {
uidt?: 'Rollup'; export interface RollupColumnReqType {
title?: string; uidt?: 'Rollup';
fk_relation_column_id?: string; title?: string;
fk_rollup_column_id?: string; fk_relation_column_id?: string;
rollup_function?: string; fk_rollup_column_id?: string;
} rollup_function?: string;
| { }
uidt?: 'Lookup';
title?: string; export interface LookupColumnReqType {
fk_relation_column_id?: string; uidt?: 'Lookup';
fk_lookup_column_id?: string; title?: string;
} fk_relation_column_id?: string;
| { fk_lookup_column_id?: string;
uidt?: string; }
formula_raw?: string;
formula?: string; export interface FormulaColumnReqType {
title?: string; uidt?: string;
}; formula_raw?: string;
formula?: string;
title?: string;
}
export type ColumnReqType = (
| NormalColumnRequestType
| LinkToAnotherColumnReqType
| RollupColumnReqType
| FormulaColumnReqType
| LookupColumnReqType
) & {
column_name?: string;
title?: string;
column_order?: {
view_id?: string;
order?: number;
};
};
export interface UserInfoType { export interface UserInfoType {
id?: string; id?: string;
@ -2600,7 +2618,13 @@ export class Api<
* @request POST:/api/v1/db/meta/views/{viewId}/sorts * @request POST:/api/v1/db/meta/views/{viewId}/sorts
* @response `200` `void` OK * @response `200` `void` OK
*/ */
create: (viewId: string, data: SortType, params: RequestParams = {}) => create: (
viewId: string,
data: SortType & {
push_to_top?: boolean;
},
params: RequestParams = {}
) =>
this.request<void, any>({ this.request<void, any>({
path: `/api/v1/db/meta/views/${viewId}/sorts`, path: `/api/v1/db/meta/views/${viewId}/sorts`,
method: 'POST', method: 'POST',

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

@ -15,9 +15,13 @@ import {
import { import {
AuditOperationSubTypes, AuditOperationSubTypes,
AuditOperationTypes, AuditOperationTypes,
ColumnReqType,
isVirtualCol, isVirtualCol,
LinkToAnotherColumnReqType,
LinkToAnotherRecordType, LinkToAnotherRecordType,
LookupColumnReqType,
RelationTypes, RelationTypes,
RollupColumnReqType,
substituteColumnAliasWithIdInFormula, substituteColumnAliasWithIdInFormula,
substituteColumnIdWithAliasInFormula, substituteColumnIdWithAliasInFormula,
TableType, TableType,
@ -96,7 +100,10 @@ async function createHmAndBtColumn(
} }
} }
export async function columnAdd(req: Request, res: Response<TableType>) { export async function columnAdd(
req: Request<any, any, ColumnReqType & { uidt: UITypes }>,
res: Response<TableType>
) {
const table = await Model.getWithInfo({ const table = await Model.getWithInfo({
id: req.params.tableId, id: req.params.tableId,
}); });
@ -121,7 +128,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
NcError.badRequest('Duplicate column alias'); NcError.badRequest('Duplicate column alias');
} }
let colBody = req.body; let colBody: any = req.body;
switch (colBody.uidt) { switch (colBody.uidt) {
case UITypes.Rollup: case UITypes.Rollup:
{ {
@ -137,7 +144,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
const relation = await ( const relation = await (
await Column.get({ await Column.get({
colId: req.body.fk_relation_column_id, colId: (req.body as RollupColumnReqType).fk_relation_column_id,
}) })
).getColOptions<LinkToAnotherRecordType>(); ).getColOptions<LinkToAnotherRecordType>();
@ -163,7 +170,8 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
const relatedTable = await relatedColumn.getModel(); const relatedTable = await relatedColumn.getModel();
if ( if (
!(await relatedTable.getColumns()).find( !(await relatedTable.getColumns()).find(
(c) => c.id === req.body.fk_rollup_column_id (c) =>
c.id === (req.body as RollupColumnReqType).fk_rollup_column_id
) )
) )
throw new Error('Rollup column not found in related table'); throw new Error('Rollup column not found in related table');
@ -183,7 +191,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
const relation = await ( const relation = await (
await Column.get({ await Column.get({
colId: req.body.fk_relation_column_id, colId: (req.body as LookupColumnReqType).fk_relation_column_id,
}) })
).getColOptions<LinkToAnotherRecordType>(); ).getColOptions<LinkToAnotherRecordType>();
@ -209,7 +217,8 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
const relatedTable = await relatedColumn.getModel(); const relatedTable = await relatedColumn.getModel();
if ( if (
!(await relatedTable.getColumns()).find( !(await relatedTable.getColumns()).find(
(c) => c.id === req.body.fk_lookup_column_id (c) =>
c.id === (req.body as LookupColumnReqType).fk_lookup_column_id
) )
) )
throw new Error('Lookup column not found in related table'); throw new Error('Lookup column not found in related table');
@ -227,14 +236,21 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
validateParams(['parentId', 'childId', 'type'], req.body); validateParams(['parentId', 'childId', 'type'], req.body);
// get parent and child models // get parent and child models
const parent = await Model.getWithInfo({ id: req.body.parentId }); const parent = await Model.getWithInfo({
const child = await Model.getWithInfo({ id: req.body.childId }); id: (req.body as LinkToAnotherColumnReqType).parentId,
});
const child = await Model.getWithInfo({
id: (req.body as LinkToAnotherColumnReqType).childId,
});
let childColumn: Column; let childColumn: Column;
const sqlMgr = await ProjectMgrv2.getSqlMgr({ const sqlMgr = await ProjectMgrv2.getSqlMgr({
id: base.project_id, id: base.project_id,
}); });
if (req.body.type === 'hm' || req.body.type === 'bt') { if (
(req.body as LinkToAnotherColumnReqType).type === 'hm' ||
(req.body as LinkToAnotherColumnReqType).type === 'bt'
) {
// populate fk column name // populate fk column name
const fkColName = getUniqueColumnName( const fkColName = getUniqueColumnName(
await child.getColumns(), await child.getColumns(),
@ -285,7 +301,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
childColumn = await Column.get({ colId: id }); childColumn = await Column.get({ colId: id });
// ignore relation creation if virtual // ignore relation creation if virtual
if (!req.body.virtual) { if (!(req.body as LinkToAnotherColumnReqType).virtual) {
// create relation // create relation
await sqlMgr.sqlOpPlus(base, 'relationCreate', { await sqlMgr.sqlOpPlus(base, 'relationCreate', {
childColumn: fkColName, childColumn: fkColName,
@ -315,11 +331,11 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
child, child,
parent, parent,
childColumn, childColumn,
req.body.type, (req.body as LinkToAnotherColumnReqType).type as RelationTypes,
req.body.title, (req.body as LinkToAnotherColumnReqType).title,
req.body.virtual (req.body as LinkToAnotherColumnReqType).virtual
); );
} else if (req.body.type === 'mm') { } else if ((req.body as LinkToAnotherColumnReqType).type === 'mm') {
const aTn = `${project?.prefix ?? ''}_nc_m2m_${randomID()}`; const aTn = `${project?.prefix ?? ''}_nc_m2m_${randomID()}`;
const aTnAlias = aTn; const aTnAlias = aTn;
@ -378,7 +394,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
columns: associateTableCols, columns: associateTableCols,
}); });
if (!req.body.virtual) { if (!(req.body as LinkToAnotherColumnReqType).virtual) {
const rel1Args = { const rel1Args = {
...req.body, ...req.body,
childTable: aTn, childTable: aTn,
@ -412,7 +428,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
childCol, childCol,
null, null,
null, null,
req.body.virtual, (req.body as LinkToAnotherColumnReqType).virtual,
true true
); );
await createHmAndBtColumn( await createHmAndBtColumn(
@ -421,7 +437,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
parentCol, parentCol,
null, null,
null, null,
req.body.virtual, (req.body as LinkToAnotherColumnReqType).virtual,
true true
); );

6
packages/nocodb/src/lib/meta/api/sortApis.ts

@ -4,7 +4,7 @@ import Model from '../../models/Model';
import { Tele } from 'nc-help'; import { Tele } from 'nc-help';
// @ts-ignore // @ts-ignore
import { PagedResponseImpl } from '../helpers/PagedResponse'; import { PagedResponseImpl } from '../helpers/PagedResponse';
import { SortListType, TableReqType, TableType } from 'nocodb-sdk'; import { SortListType, SortType, TableType } from 'nocodb-sdk';
// @ts-ignore // @ts-ignore
import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2'; import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2';
// @ts-ignore // @ts-ignore
@ -28,11 +28,11 @@ export async function sortList(
} }
// @ts-ignore // @ts-ignore
export async function sortCreate(req: Request<any, any, TableReqType>, res) { export async function sortCreate(req: Request<any, any, SortType>, res) {
const sort = await Sort.insert({ const sort = await Sort.insert({
...req.body, ...req.body,
fk_view_id: req.params.viewId, fk_view_id: req.params.viewId,
}); } as Sort);
Tele.emit('evt', { evt_type: 'sort:created' }); Tele.emit('evt', { evt_type: 'sort:created' });
res.json(sort); res.json(sort);
} }

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

@ -5,7 +5,7 @@ import RollupColumn from './RollupColumn';
import SelectOption from './SelectOption'; import SelectOption from './SelectOption';
import Model from './Model'; import Model from './Model';
import NocoCache from '../cache/NocoCache'; import NocoCache from '../cache/NocoCache';
import { AllowedColumnTypesForQrCode, ColumnType, UITypes } from 'nocodb-sdk'; import { ColumnReqType, AllowedColumnTypesForQrCode, ColumnType, UITypes } from 'nocodb-sdk';
import { import {
CacheDelDirection, CacheDelDirection,
CacheGetType, CacheGetType,
@ -74,7 +74,7 @@ export default class Column<T = any> implements ColumnType {
[key: string]: any; [key: string]: any;
fk_model_id: string; fk_model_id: string;
uidt: UITypes | string; uidt: UITypes | string;
}, } & Pick<ColumnReqType, 'column_order'>,
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
) { ) {
if (!column.fk_model_id) NcError.badRequest('Missing model id'); if (!column.fk_model_id) NcError.badRequest('Missing model id');
@ -130,7 +130,6 @@ export default class Column<T = any> implements ColumnType {
} }
if (!column.uidt) throw new Error('UI Datatype not found'); if (!column.uidt) throw new Error('UI Datatype not found');
const order = 1;
const row = await ncMeta.metaInsert2( const row = await ncMeta.metaInsert2(
null, //column.project_id || column.base_id, null, //column.project_id || column.base_id,
null, //column.db_alias, null, //column.db_alias,
@ -152,8 +151,8 @@ export default class Column<T = any> implements ColumnType {
{ {
fk_column_id: row.id, fk_column_id: row.id,
fk_model_id: column.fk_model_id, fk_model_id: column.fk_model_id,
order,
show: true, show: true,
column_order: column.column_order,
}, },
ncMeta ncMeta
); );

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

@ -70,9 +70,11 @@ export default class GridViewColumn implements GridColumnType {
const insertObj = { const insertObj = {
fk_view_id: column.fk_view_id, fk_view_id: column.fk_view_id,
fk_column_id: column.fk_column_id, fk_column_id: column.fk_column_id,
order: await ncMeta.metaGetNextOrder(MetaTable.GRID_VIEW_COLUMNS, { order:
fk_view_id: column.fk_view_id, column?.order ??
}), (await ncMeta.metaGetNextOrder(MetaTable.GRID_VIEW_COLUMNS, {
fk_view_id: column.fk_view_id,
})),
show: column.show, show: column.show,
project_id: column.project_id, project_id: column.project_id,
base_id: column.base_id, base_id: column.base_id,

71
packages/nocodb/src/lib/models/Sort.ts

@ -34,18 +34,22 @@ export default class Sort {
}); });
} }
public static async insert(sortObj: Partial<Sort>, ncMeta = Noco.ncMeta) { public static async insert(
sortObj: Partial<Sort> & { push_to_top?: boolean },
ncMeta = Noco.ncMeta
) {
// todo: implement a generic function // todo: implement a generic function
const order = const order = sortObj.push_to_top
(+( ? 1
await ncMeta : (+(
.knex(MetaTable.SORT) await ncMeta
.max('order', { as: 'order' }) .knex(MetaTable.SORT)
.where({ .max('order', { as: 'order' })
fk_view_id: sortObj.fk_view_id, .where({
}) fk_view_id: sortObj.fk_view_id,
.first() })
)?.order || 0) + 1; .first()
)?.order || 0) + 1;
const insertObj = { const insertObj = {
id: sortObj.id, id: sortObj.id,
@ -62,20 +66,39 @@ export default class Sort {
insertObj.base_id = model.base_id; insertObj.base_id = model.base_id;
} }
const row = await ncMeta.metaInsert2(null, null, MetaTable.SORT, insertObj); // increment existing order
if (sortObj.push_to_top) {
await NocoCache.appendToList( await ncMeta
CacheScope.SORT, .knex(MetaTable.SORT)
[sortObj.fk_view_id], .where({
`${CacheScope.SORT}:${row.id}` fk_view_id: sortObj.fk_view_id,
); })
.increment('order', 1);
await NocoCache.appendToList( }
CacheScope.SORT,
[sortObj.fk_column_id],
`${CacheScope.SORT}:${row.id}`
);
const row = await ncMeta.metaInsert2(null, null, MetaTable.SORT, insertObj);
if (sortObj.push_to_top) {
// todo: delete cache
const sortList = await ncMeta.metaList2(null, null, MetaTable.SORT, {
condition: { fk_view_id: sortObj.fk_view_id },
orderBy: {
order: 'asc',
},
});
await NocoCache.setList(CacheScope.SORT, [sortObj.fk_view_id], sortList);
} else {
await NocoCache.appendToList(
CacheScope.SORT,
[sortObj.fk_view_id],
`${CacheScope.SORT}:${row.id}`
);
await NocoCache.appendToList(
CacheScope.SORT,
[sortObj.fk_column_id],
`${CacheScope.SORT}:${row.id}`
);
}
return this.get(row.id, ncMeta); return this.get(row.id, ncMeta);
} }

42
packages/nocodb/src/lib/models/View.ts

@ -13,7 +13,13 @@ import GalleryView from './GalleryView';
import GridViewColumn from './GridViewColumn'; import GridViewColumn from './GridViewColumn';
import Sort from './Sort'; import Sort from './Sort';
import Filter from './Filter'; import Filter from './Filter';
import { isSystemColumn, UITypes, ViewType, ViewTypes } from 'nocodb-sdk'; import {
ColumnReqType,
isSystemColumn,
UITypes,
ViewType,
ViewTypes,
} from 'nocodb-sdk';
import GalleryViewColumn from './GalleryViewColumn'; import GalleryViewColumn from './GalleryViewColumn';
import FormViewColumn from './FormViewColumn'; import FormViewColumn from './FormViewColumn';
import KanbanViewColumn from './KanbanViewColumn'; import KanbanViewColumn from './KanbanViewColumn';
@ -442,9 +448,9 @@ export default class View implements ViewType {
param: { param: {
fk_column_id: any; fk_column_id: any;
fk_model_id: any; fk_model_id: any;
order; order?: number;
show; show;
}, } & Pick<ColumnReqType, 'column_order'>,
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta
) { ) {
const insertObj = { const insertObj = {
@ -456,33 +462,21 @@ export default class View implements ViewType {
const views = await this.list(param.fk_model_id, ncMeta); const views = await this.list(param.fk_model_id, ncMeta);
for (const view of views) { for (const view of views) {
const modifiedInsertObj = { ...insertObj, fk_view_id: view.id };
if (param.column_order?.view_id === view.id) {
modifiedInsertObj.order = param.column_order?.order;
}
switch (view.type) { switch (view.type) {
case ViewTypes.GRID: case ViewTypes.GRID:
await GridViewColumn.insert( await GridViewColumn.insert(modifiedInsertObj, ncMeta);
{
...insertObj,
fk_view_id: view.id,
},
ncMeta
);
break; break;
case ViewTypes.GALLERY: case ViewTypes.GALLERY:
await GalleryViewColumn.insert( await GalleryViewColumn.insert(modifiedInsertObj, ncMeta);
{
...insertObj,
fk_view_id: view.id,
},
ncMeta
);
break; break;
case ViewTypes.KANBAN: case ViewTypes.KANBAN:
await KanbanViewColumn.insert( await KanbanViewColumn.insert(modifiedInsertObj, ncMeta);
{
...insertObj,
fk_view_id: view.id,
},
ncMeta
);
break; break;
} }
} }

437
scripts/sdk/swagger.json

@ -2286,7 +2286,19 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/Sort" "allOf": [
{
"$ref": "#/components/schemas/Sort"
},
{
"type": "object",
"properties": {
"push_to_top": {
"type": "boolean"
}
}
}
]
} }
} }
} }
@ -8957,213 +8969,254 @@
} }
} }
}, },
"ColumnReq": { "NormalColumnRequest": {
"oneOf": [ "properties": {
{ "uidt": {
"properties": { "type": "string",
"uidt": { "enum": [
"type": "string", "ID",
"enum": [ "SingleLineText",
"ID", "LongText",
"SingleLineText", "Attachment",
"LongText", "Checkbox",
"Attachment", "MultiSelect",
"Checkbox", "SingleSelect",
"MultiSelect", "Collaborator",
"SingleSelect", "Date",
"Collaborator", "Year",
"Date", "Time",
"Year", "PhoneNumber",
"Time", "Email",
"PhoneNumber", "URL",
"Email", "Number",
"URL", "Decimal",
"Number", "Currency",
"Decimal", "Percent",
"Currency", "Duration",
"Percent", "Rating",
"Duration", "Count",
"Rating", "DateTime",
"Count", "CreateTime",
"DateTime", "LastModifiedTime",
"CreateTime", "AutoNumber",
"LastModifiedTime", "Geometry",
"AutoNumber", "JSON",
"Geometry", "SpecificDBType",
"JSON", "Barcode",
"SpecificDBType", "Button"
"Barcode", ]
"Button"
]
},
"id": {
"type": "string"
},
"base_id": {
"type": "string"
},
"fk_model_id": {
"type": "string"
},
"title": {
"type": "string"
},
"dt": {
"type": "string"
},
"np": {
"type": "string"
},
"ns": {
"type": "string"
},
"clen": {
"type": [
"string",
"integer"
]
},
"cop": {
"type": "string"
},
"pk": {
"type": "boolean"
},
"pv": {
"type": "boolean"
},
"rqd": {
"type": "boolean"
},
"column_name": {
"type": "string"
},
"un": {
"type": "boolean"
},
"ct": {
"type": "string"
},
"ai": {
"type": "boolean"
},
"unique": {
"type": "boolean"
},
"cdf": {
"type": "string"
},
"cc": {
"type": "string"
},
"csn": {
"type": "string"
},
"dtx": {
"type": "string"
},
"dtxp": {
"type": "string"
},
"dtxs": {
"type": "string"
},
"au": {
"type": "boolean"
},
"": {
"type": "string"
}
}
}, },
{ "id": {
"properties": { "type": "string"
"uidt": { },
"type": "string", "base_id": {
"enum": [ "type": "string"
"LinkToAnotherRecord" },
] "fk_model_id": {
}, "type": "string"
"title": { },
"type": "string" "title": {
}, "type": "string"
"parentId": { },
"type": "string" "dt": {
}, "type": "string"
"childId": { },
"type": "string" "np": {
}, "type": "string"
"type": { },
"type": "string", "ns": {
"enum": [ "type": "string"
"hm", },
"bt", "clen": {
"mm" "type": [
] "string",
} "integer"
},
"required": [
"uidt",
"title",
"parentId",
"childId",
"type"
] ]
}, },
{ "cop": {
"properties": { "type": "string"
"uidt": {
"type": "string",
"enum": [
"Rollup"
]
},
"title": {
"type": "string"
},
"fk_relation_column_id": {
"type": "string"
},
"fk_rollup_column_id": {
"type": "string"
},
"rollup_function": {
"type": "string"
}
}
}, },
"pk": {
"type": "boolean"
},
"pv": {
"type": "boolean"
},
"rqd": {
"type": "boolean"
},
"column_name": {
"type": "string"
},
"un": {
"type": "boolean"
},
"ct": {
"type": "string"
},
"ai": {
"type": "boolean"
},
"unique": {
"type": "boolean"
},
"cdf": {
"type": "string"
},
"cc": {
"type": "string"
},
"csn": {
"type": "string"
},
"dtx": {
"type": "string"
},
"dtxp": {
"type": "string"
},
"dtxs": {
"type": "string"
},
"au": {
"type": "boolean"
}
}
},
"LinkToAnotherColumnReq": {
"properties": {
"uidt": {
"type": "string",
"enum": [
"LinkToAnotherRecord"
]
},
"title": {
"type": "string"
},
"virtual": {
"type": "boolean"
},
"parentId": {
"type": "string"
},
"childId": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"hm",
"bt",
"mm"
]
}
},
"required": [
"uidt",
"title",
"parentId",
"childId",
"type"
]
},
"RollupColumnReq": {
"properties": {
"uidt": {
"type": "string",
"enum": [
"Rollup"
]
},
"title": {
"type": "string"
},
"fk_relation_column_id": {
"type": "string"
},
"fk_rollup_column_id": {
"type": "string"
},
"rollup_function": {
"type": "string"
}
}
},
"LookupColumnReq": {
"properties": {
"uidt": {
"type": "string",
"enum": [
"Lookup"
]
},
"title": {
"type": "string"
},
"fk_relation_column_id": {
"type": "string"
},
"fk_lookup_column_id": {
"type": "string"
}
}
},
"FormulaColumnReq": {
"properties": {
"uidt": {
"type": "string"
},
"formula_raw": {
"type": "string"
},
"formula": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"ColumnReq": {
"allOf": [
{ {
"properties": { "oneOf": [
"uidt": { {
"type": "string", "$ref": "#/components/schemas/NormalColumnRequest"
"enum": [
"Lookup"
]
}, },
"title": { {
"type": "string" "$ref": "#/components/schemas/LinkToAnotherColumnReq"
}, },
"fk_relation_column_id": { {
"type": "string" "$ref": "#/components/schemas/RollupColumnReq"
}, },
"fk_lookup_column_id": { {
"type": "string" "$ref": "#/components/schemas/FormulaColumnReq"
},
{
"$ref": "#/components/schemas/LookupColumnReq"
} }
} ]
}, },
{ {
"type": "object",
"properties": { "properties": {
"uidt": { "column_name": {
"type": "string"
},
"formula_raw": {
"type": "string"
},
"formula": {
"type": "string" "type": "string"
}, },
"title": { "title": {
"type": "string" "type": "string"
},
"column_order": {
"type": "object",
"properties": {
"view_id": {
"type": "string"
},
"order": {
"type": "integer"
}
}
} }
} }
} }

Loading…
Cancel
Save