Browse Source

fix conflicts

pull/4749/head
flisowna 2 years ago
parent
commit
29dac655dc
  1. 2
      .github/workflows/ci-cd.yml
  2. 2
      .github/workflows/playwright-test-workflow.yml
  3. 4
      .github/workflows/release-nocodb.yml
  4. 11
      packages/nc-gui/assets/style.scss
  5. 1
      packages/nc-gui/components.d.ts
  6. 6
      packages/nc-gui/components/cell/ClampedText.vue
  7. 5
      packages/nc-gui/components/cell/Duration.vue
  8. 12
      packages/nc-gui/components/cell/MultiSelect.vue
  9. 13
      packages/nc-gui/components/cell/SingleSelect.vue
  10. 46
      packages/nc-gui/components/dashboard/settings/Misc.vue
  11. 54
      packages/nc-gui/components/smartsheet/Gallery.vue
  12. 6
      packages/nc-gui/components/smartsheet/Grid.vue
  13. 4
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  14. 19
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  15. 22
      packages/nc-gui/components/smartsheet/column/RollupOptions.vue
  16. 87
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  17. 46
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  18. 8
      packages/nc-gui/components/smartsheet/header/Menu.vue
  19. 108
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  20. 22
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  21. 12
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  22. 194
      packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
  23. 2
      packages/nc-gui/components/template/Editor.vue
  24. 6
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  25. 6
      packages/nc-gui/components/virtual-cell/HasMany.vue
  26. 6
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  27. 4
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  28. 4
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  29. 9
      packages/nc-gui/composables/useExpandedFormStore.ts
  30. 22
      packages/nc-gui/composables/useLTARStore.ts
  31. 14
      packages/nc-gui/composables/useProject.ts
  32. 1
      packages/nc-gui/composables/useViewData.ts
  33. 108
      packages/nc-gui/composables/useViewFilters.ts
  34. 3
      packages/nc-gui/lang/ar.json
  35. 3
      packages/nc-gui/lang/bn_IN.json
  36. 5
      packages/nc-gui/lang/cs.json
  37. 3
      packages/nc-gui/lang/da.json
  38. 3
      packages/nc-gui/lang/de.json
  39. 9
      packages/nc-gui/lang/en.json
  40. 3
      packages/nc-gui/lang/es.json
  41. 3
      packages/nc-gui/lang/eu.json
  42. 3
      packages/nc-gui/lang/fa.json
  43. 3
      packages/nc-gui/lang/fi.json
  44. 3
      packages/nc-gui/lang/fr.json
  45. 3
      packages/nc-gui/lang/he.json
  46. 3
      packages/nc-gui/lang/hi.json
  47. 3
      packages/nc-gui/lang/hr.json
  48. 3
      packages/nc-gui/lang/id.json
  49. 3
      packages/nc-gui/lang/it.json
  50. 3
      packages/nc-gui/lang/ja.json
  51. 3
      packages/nc-gui/lang/ko.json
  52. 3
      packages/nc-gui/lang/lv.json
  53. 3
      packages/nc-gui/lang/nl.json
  54. 3
      packages/nc-gui/lang/no.json
  55. 3
      packages/nc-gui/lang/pl.json
  56. 3
      packages/nc-gui/lang/pt.json
  57. 3
      packages/nc-gui/lang/pt_BR.json
  58. 3
      packages/nc-gui/lang/ru.json
  59. 3
      packages/nc-gui/lang/sk.json
  60. 3
      packages/nc-gui/lang/sl.json
  61. 3
      packages/nc-gui/lang/sv.json
  62. 3
      packages/nc-gui/lang/th.json
  63. 3
      packages/nc-gui/lang/tr.json
  64. 3
      packages/nc-gui/lang/uk.json
  65. 3
      packages/nc-gui/lang/vi.json
  66. 3
      packages/nc-gui/lang/zh-Hans.json
  67. 3
      packages/nc-gui/lang/zh-Hant.json
  68. 123
      packages/nc-gui/package-lock.json
  69. 3
      packages/nc-gui/package.json
  70. 116
      packages/nc-gui/utils/filterUtils.ts
  71. 2
      packages/nc-lib-gui/package.json
  72. 2
      packages/noco-docs/content/en/engineering/playwright.md
  73. 14
      packages/noco-docs/content/en/setup-and-usages/column-operations.md
  74. 36
      packages/noco-docs/content/en/setup-and-usages/display-value.md
  75. 4
      packages/noco-docs/content/en/setup-and-usages/table-operations.md
  76. 2
      packages/nocodb-sdk/package.json
  77. 7
      packages/nocodb-sdk/src/index.ts
  78. 16
      packages/nocodb-sdk/src/lib/Api.ts
  79. 22
      packages/nocodb-sdk/src/lib/UITypes.ts
  80. 206
      packages/nocodb/package-lock.json
  81. 8
      packages/nocodb/package.json
  82. 2
      packages/nocodb/src/lib/Noco.ts
  83. 227
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts
  84. 12
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts
  85. 2
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/getAst.ts
  86. 4
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/sortV2.ts
  87. 8
      packages/nocodb/src/lib/db/sql-mgr/code/models/xc/BaseModelXcMeta.ts
  88. 2
      packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaMssql.ts
  89. 2
      packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaMysql.ts
  90. 2
      packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaOracle.ts
  91. 2
      packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaPg.ts
  92. 2
      packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaSnowflake.ts
  93. 2
      packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaSqlite.ts
  94. 198
      packages/nocodb/src/lib/meta/api/columnApis.ts
  95. 2
      packages/nocodb/src/lib/meta/api/dataApis/helpers.ts
  96. 209
      packages/nocodb/src/lib/meta/api/helpers/columnHelpers.ts
  97. 1
      packages/nocodb/src/lib/meta/api/helpers/index.ts
  98. 4
      packages/nocodb/src/lib/meta/api/helpers/populateMeta.ts
  99. 10
      packages/nocodb/src/lib/meta/api/metaDiffApis.ts
  100. 10
      packages/nocodb/src/lib/meta/api/projectApis.ts
  101. Some files were not shown because too many files have changed in this diff Show More

2
.github/workflows/ci-cd.yml

@ -27,7 +27,7 @@ concurrency:
jobs:
unit-tests:
runs-on: ubuntu-20.04
timeout-minutes: 30
timeout-minutes: 40
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
steps:
- name: Setup Node

2
.github/workflows/playwright-test-workflow.yml

@ -14,7 +14,7 @@ on:
jobs:
playwright:
runs-on: ubuntu-20.04
timeout-minutes: 30
timeout-minutes: 40
steps:
# Reference: https://github.com/pierotofy/set-swap-space/blob/master/action.yml
- name: Set 5gb swap

4
.github/workflows/release-nocodb.yml

@ -112,12 +112,12 @@ jobs:
# Publish Docs
publish-docs:
needs: close-issues
needs: release-docker
uses: ./.github/workflows/publish-docs.yml
secrets:
GH_TOKEN: "${{ secrets.GH_TOKEN }}"
# Sync changes to develop
sync-to-develop:
needs: close-issues
needs: publish-docs
uses: ./.github/workflows/sync-to-develop.yml

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

@ -322,3 +322,14 @@ a {
white-space: pre;
user-select: auto;
}
.nc-drawer-expanded-form .ant-drawer-content-wrapper {
transition: width 0.3s !important;
}
.nc-icon-transition {
@apply transform transition-transform !hover:(scale-115) !active:(scale-100)
}

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

@ -206,6 +206,7 @@ declare module '@vue/runtime-core' {
MdiMapMarker: typeof import('~icons/mdi/map-marker')['default']
MdiMapMarkerAlert: typeof import('~icons/mdi/map-marker-alert')['default']
MdiMenu: typeof import('~icons/mdi/menu')['default']
MdiMenuArrowDown: typeof import('~icons/mdi/menu-arrow-down')['default']
MdiMenuDown: typeof import('~icons/mdi/menu-down')['default']
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMinusCircleOutline: typeof import('~icons/mdi/minus-circle-outline')['default']

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

@ -23,10 +23,14 @@ onMounted(() => {
<template>
<div ref="wrapper">
<!--
using '' for :text in text-clamp would keep the previous cell value after changing a filter
use ' ' instead of '' to trigger update
-->
<text-clamp
:key="`clamp-${key}-${props.value?.toString().length || 0}`"
class="w-full h-full break-all"
:text="`${props.value || ''}`"
:text="`${props.value || ' '}`"
:max-lines="props.lines"
/>
</div>

5
packages/nc-gui/components/cell/Duration.vue

@ -13,9 +13,10 @@ import {
interface Props {
modelValue: number | string | null | undefined
showValidationError: boolean
}
const { modelValue } = defineProps<Props>()
const { modelValue, showValidationError = true } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
@ -99,7 +100,7 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<span v-else> {{ localState }}</span>
<div v-if="showWarningMessage" class="duration-warning">
<div v-if="showWarningMessage && showValidationError" class="duration-warning">
<!-- TODO: i18n -->
Please enter a number
</div>

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

@ -31,9 +31,10 @@ import MdiCloseCircle from '~icons/mdi/close-circle'
interface Props {
modelValue?: string | string[]
rowIndex?: number
disableOptionCreation?: boolean
}
const { modelValue } = defineProps<Props>()
const { modelValue, disableOptionCreation } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
@ -336,6 +337,7 @@ useEventListener(document, 'click', handleClose, true)
:key="op.id || op.title"
:value="op.title"
:data-testid="`select-option-${column.title}-${rowIndex}`"
:class="`nc-select-option-${column.title}-${op.title}`"
@click.stop
>
<a-tag class="rounded-tag" :color="op.color">
@ -354,7 +356,13 @@ useEventListener(document, 'click', handleClose, true)
</a-select-option>
<a-select-option
v-if="searchVal && isOptionMissing && !isPublic && (hasRole('owner', true) || hasRole('creator', true))"
v-if="
searchVal &&
isOptionMissing &&
!isPublic &&
!disableOptionCreation &&
(hasRole('owner', true) || hasRole('creator', true))
"
:key="searchVal"
:value="searchVal"
>

13
packages/nc-gui/components/cell/SingleSelect.vue

@ -18,6 +18,7 @@ import {
inject,
isDrawerOrModalExist,
ref,
useEventListener,
useRoles,
useSelectedCellKeyupListener,
watch,
@ -26,9 +27,10 @@ import {
interface Props {
modelValue?: string | undefined
rowIndex?: number
disableOptionCreation?: boolean
}
const { modelValue } = defineProps<Props>()
const { modelValue, disableOptionCreation } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
@ -263,6 +265,7 @@ useEventListener(document, 'click', handleClose, true)
:key="op.title"
:value="op.title"
:data-testid="`select-option-${column.title}-${rowIndex}`"
:class="`nc-select-option-${column.title}-${op.title}`"
@click.stop
>
<a-tag class="rounded-tag" :color="op.color">
@ -280,7 +283,13 @@ useEventListener(document, 'click', handleClose, true)
</a-tag>
</a-select-option>
<a-select-option
v-if="searchVal && isOptionMissing && !isPublic && (hasRole('owner', true) || hasRole('creator', true))"
v-if="
searchVal &&
isOptionMissing &&
!isPublic &&
!disableOptionCreation &&
(hasRole('owner', true) || hasRole('creator', true))
"
:key="searchVal"
:value="searchVal"
>

46
packages/nc-gui/components/dashboard/settings/Misc.vue

@ -1,10 +1,34 @@
<script setup lang="ts">
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface'
import { useGlobal, useProject, watch } from '#imports'
const { includeM2M, showNull } = useGlobal()
const { loadTables } = useProject()
const { project, updateProject, projectMeta, loadTables, hasEmptyOrNullFilters } = useProject()
watch(includeM2M, async () => await loadTables())
const showNullAndEmptyInFilter = ref(projectMeta.value.showNullAndEmptyInFilter)
async function showNullAndEmptyInFilterOnChange(evt: CheckboxChangeEvent) {
// users cannot hide null & empty option if there is existing null / empty filters
if (!evt.target.checked) {
if (await hasEmptyOrNullFilters()) {
showNullAndEmptyInFilter.value = true
message.warning('Null / Empty filters exist. Please remove them first.')
}
}
const newProjectMeta = {
...projectMeta.value,
showNullAndEmptyInFilter: showNullAndEmptyInFilter.value,
}
// update local state
project.value.meta = newProjectMeta
// update db
await updateProject({
meta: newProjectMeta,
})
}
</script>
<template>
@ -13,12 +37,28 @@ watch(includeM2M, async () => await loadTables())
<div class="flex flex-row items-center w-full mb-4 gap-2">
<!-- Show M2M Tables -->
<a-checkbox v-model:checked="includeM2M" v-e="['c:themes:show-m2m-tables']" class="nc-settings-meta-misc">
{{ $t('msg.info.showM2mTables') }}
{{ $t('msg.info.showM2mTables') }} <br />
<span class="text-gray-500">{{ $t('msg.info.showM2mTablesDesc') }}</span>
</a-checkbox>
</div>
<div class="flex flex-row items-center w-full mb-4 gap-2">
<!-- Show NULL -->
<a-checkbox v-model:checked="showNull" v-e="['c:settings:show-null']" class="nc-settings-show-null">Show NULL</a-checkbox>
<a-checkbox v-model:checked="showNull" v-e="['c:settings:show-null']" class="nc-settings-show-null">
{{ $t('msg.info.showNullInCells') }} <br />
<span class="text-gray-500">{{ $t('msg.info.showNullInCellsDesc') }}</span>
</a-checkbox>
</div>
<div class="flex flex-row items-center w-full mb-4 gap-2">
<!-- Show NULL and EMPTY in Filters -->
<a-checkbox
v-model:checked="showNullAndEmptyInFilter"
v-e="['c:settings:show-null-and-empty-in-filter']"
class="nc-settings-show-null-and-empty-in-filter"
@change="showNullAndEmptyInFilterOnChange"
>
{{ $t('msg.info.showNullAndEmptyInFilter') }} <br />
<span class="text-gray-500">{{ $t('msg.info.showNullAndEmptyInFilterDesc') }}</span>
</a-checkbox>
</div>
</div>
</div>

54
packages/nc-gui/components/smartsheet/Gallery.vue

@ -51,6 +51,7 @@ const {
galleryData,
changePage,
addEmptyRow,
deleteRow,
navigateToSiblingRow,
} = useViewData(meta, view)
@ -85,6 +86,30 @@ const isRowEmpty = (record: any, col: any) => {
return Array.isArray(val) && val.length === 0
}
const { isSqlView } = useSmartsheetStoreOrThrow()
const { isUIAllowed } = useUIPermission()
const hasEditPermission = $computed(() => isUIAllowed('xcDatatableEditable'))
// TODO: extract this code (which is duplicated in grid and gallery) into a separate component
const _contextMenu = ref(false)
const contextMenu = computed({
get: () => _contextMenu.value,
set: (val) => {
if (hasEditPermission) {
_contextMenu.value = val
}
},
})
const contextMenuTarget = ref<{ row: number } | null>(null)
const showContextMenu = (e: MouseEvent, target?: { row: number }) => {
if (isSqlView.value) return
e.preventDefault()
if (target) {
contextMenuTarget.value = target
}
}
const attachments = (record: any): Attachment[] => {
try {
if (coverImageColumn?.title && record.row[coverImageColumn.title]) {
@ -175,15 +200,39 @@ watch(view, async (nextView) => {
</script>
<template>
<a-dropdown
v-model:visible="contextMenu"
:trigger="isSqlView ? [] : ['contextmenu']"
overlay-class-name="nc-dropdown-grid-context-menu"
>
<template #overlay>
<a-menu class="shadow !rounded !py-0" @click="contextMenu = false">
<a-menu-item v-if="contextMenuTarget" @click="deleteRow(contextMenuTarget.row)">
<div v-e="['a:row:delete']" class="nc-project-menu-item">
<!-- Delete Row -->
{{ $t('activity.deleteRow') }}
</div>
</a-menu-item>
<a-menu-item v-if="contextMenuTarget" @click="openNewRecordFormHook.trigger()">
<div v-e="['a:row:insert']" class="nc-project-menu-item">
<!-- Insert New Row -->
{{ $t('activity.insertRow') }}
</div>
</a-menu-item>
</a-menu>
</template>
<div class="flex flex-col h-full w-full overflow-auto nc-gallery" data-testid="nc-gallery-wrapper">
<div class="nc-gallery-container grid gap-2 my-4 px-3">
<div v-for="record in data" :key="`record-${record.row.id}`">
<div v-for="(record, rowIndex) in data" :key="`record-${record.row.id}`">
<LazySmartsheetRow :row="record">
<a-card
hoverable
class="!rounded-lg h-full overflow-hidden break-all max-w-[450px]"
:data-testid="`nc-gallery-card-${record.row.id}`"
@click="expandFormClick($event, record)"
@contextmenu="showContextMenu($event, { row: rowIndex })"
>
<template v-if="galleryData?.fk_cover_image_col_id" #cover>
<a-carousel v-if="!reloadAttachments && attachments(record).length" autoplay class="gallery-carousel" arrows>
@ -255,6 +304,8 @@ watch(view, async (nextView) => {
<div class="flex-1" />
<LazySmartsheetPagination />
</div>
</a-dropdown>
<Suspense>
<LazySmartsheetExpandedForm
@ -281,7 +332,6 @@ watch(view, async (nextView) => {
@prev="navigateToSiblingRow(NavigateDir.PREV)"
/>
</Suspense>
</div>
</template>
<style scoped>

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

@ -793,7 +793,7 @@ const closeAddColumnDropdown = () => {
class="nc-row-no text-xs text-gray-500"
:class="{ toggle: !readOnly, hidden: row.rowMeta.selected }"
>
{{ rowIndex + 1 }}
{{ ((paginationData.page ?? 1) - 1) * 25 + rowIndex + 1 }}
</div>
<div
v-if="!readOnly"
@ -1056,7 +1056,7 @@ const closeAddColumnDropdown = () => {
position: sticky !important;
left: 80px;
z-index: 5;
@apply border-r-2 border-r-gray-300;
@apply border-r-1 border-r-gray-300;
}
tbody td:nth-child(2) {
@ -1064,7 +1064,7 @@ const closeAddColumnDropdown = () => {
left: 80px;
z-index: 4;
background: white;
@apply shadow-lg border-r-2 border-r-gray-300;
@apply shadow-lg border-r-1 border-r-gray-300;
}
}

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

@ -186,10 +186,10 @@ useEventListener('keydown', (e: KeyboardEvent) => {
<LazySmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<LazySmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />
<LazySmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />
<LazySmartsheetColumnLookupOptions v-if="!isEdit && formState.uidt === UITypes.Lookup" v-model:value="formState" />
<LazySmartsheetColumnLookupOptions v-if="formState.uidt === UITypes.Lookup" v-model:value="formState" />
<LazySmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" v-model:value="formState" />
<LazySmartsheetColumnDateTimeOptions v-if="formState.uidt === UITypes.DateTime" v-model:value="formState" />
<LazySmartsheetColumnRollupOptions v-if="!isEdit && formState.uidt === UITypes.Rollup" v-model:value="formState" />
<LazySmartsheetColumnRollupOptions v-if="formState.uidt === UITypes.Rollup" v-model:value="formState" />
<LazySmartsheetColumnLinkedToAnotherRecordOptions
v-if="!isEdit && formState.uidt === UITypes.LinkToAnotherRecord"
v-model:value="formState"

19
packages/nc-gui/components/smartsheet/column/LookupOptions.vue

@ -1,4 +1,5 @@
<script setup lang="ts">
import { onMounted } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import { getRelationName } from './utils'
@ -14,7 +15,7 @@ const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj, ref()))
const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow()
const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit } = useColumnCreateStoreOrThrow()
const { tables } = $(useProject())
@ -49,8 +50,20 @@ const columns = $computed<ColumnType[]>(() => {
if (!selectedTable?.id) {
return []
}
return metas[selectedTable.id].columns.filter((c: ColumnType) => !isSystemColumn(c))
return metas[selectedTable.id].columns.filter((c: ColumnType) => !isSystemColumn(c) && c.id !== vModel.value.id)
})
onMounted(() => {
if (isEdit.value) {
vModel.value.fk_lookup_column_id = vModel.value.colOptions?.fk_lookup_column_id
vModel.value.fk_relation_column_id = vModel.value.colOptions?.fk_relation_column_id
}
})
const onRelationColChange = () => {
vModel.value.fk_lookup_column_id = columns?.[0]?.id
onDataTypeChange()
}
</script>
<template>
@ -60,7 +73,7 @@ const columns = $computed<ColumnType[]>(() => {
<a-select
v-model:value="vModel.fk_relation_column_id"
dropdown-class-name="!w-64 nc-dropdown-relation-table"
@change="onDataTypeChange"
@change="onRelationColChange"
>
<a-select-option v-for="(table, i) of refTables" :key="i" :value="table.col.fk_column_id">
<div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">

22
packages/nc-gui/components/smartsheet/column/RollupOptions.vue

@ -1,4 +1,5 @@
<script setup lang="ts">
import { onMounted } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { getRelationName } from './utils'
@ -14,7 +15,7 @@ const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj, ref()))
const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow()
const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit } = useColumnCreateStoreOrThrow()
const { tables } = $(useProject())
@ -69,8 +70,23 @@ const columns = $computed(() => {
return []
}
return metas[selectedTable.id].columns.filter((c: ColumnType) => !isVirtualCol(c.uidt as UITypes) && !isSystemColumn(c))
return metas[selectedTable.id].columns.filter(
(c: ColumnType) => !isVirtualCol(c.uidt as UITypes) && (!isSystemColumn(c) || c.pk),
)
})
onMounted(() => {
if (isEdit.value) {
vModel.value.fk_relation_column_id = vModel.value.colOptions?.fk_relation_column_id
vModel.value.fk_rollup_column_id = vModel.value.colOptions?.fk_rollup_column_id
vModel.value.rollup_function = vModel.value.colOptions?.rollup_function
}
})
const onRelationColChange = () => {
vModel.value.fk_rollup_column_id = columns?.[0]?.id
onDataTypeChange()
}
</script>
<template>
@ -80,7 +96,7 @@ const columns = $computed(() => {
<a-select
v-model:value="vModel.fk_relation_column_id"
dropdown-class-name="!w-64 nc-dropdown-relation-table"
@change="onDataTypeChange"
@change="onRelationColChange"
>
<a-select-option v-for="(table, i) of refTables" :key="i" :value="table.col.fk_column_id">
<div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">

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

@ -12,13 +12,13 @@ import {
const props = defineProps<{ view?: ViewType }>()
const emit = defineEmits(['cancel'])
const emit = defineEmits(['cancel', 'duplicateRow'])
const route = useRoute()
const { meta, isSqlView } = useSmartsheetStoreOrThrow()
const { commentsDrawer, primaryValue, primaryKey, save: _save, loadRow } = useExpandedFormStoreOrThrow()
const { commentsDrawer, displayValue, primaryKey, save: _save, loadRow } = useExpandedFormStoreOrThrow()
const { isNew, syncLTARRefs, state } = useSmartsheetRowStoreOrThrow()
@ -72,6 +72,22 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
}
}
})
const showDeleteRowModal = ref(false)
const { deleteRowById } = useViewData(meta, ref(props.view))
const onDeleteRowClick = () => {
showDeleteRowModal.value = true
}
const onConfirmDeleteRowClick = async () => {
showDeleteRowModal.value = false
await deleteRowById(primaryKey.value)
reloadTrigger.trigger()
emit('cancel')
message.success('Row deleted')
}
</script>
<template>
@ -83,7 +99,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
{{ meta.title }}
</template>
<template v-if="primaryValue">: {{ primaryValue }}</template>
<template v-if="displayValue">: {{ displayValue }}</template>
</h5>
<div class="flex-1" />
@ -92,7 +108,11 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<template #title>
<div class="text-center w-full">{{ $t('general.reload') }}</div>
</template>
<mdi-reload v-if="!isNew" class="cursor-pointer select-none text-gray-500 mx-1 min-w-4" @click="loadRow" />
<mdi-reload
v-if="!isNew"
class="nc-icon-transition cursor-pointer select-none text-gray-500 mx-1 min-w-4"
@click="loadRow"
/>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title>
@ -101,7 +121,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
</template>
<mdi-link
v-if="!isNew"
class="cursor-pointer select-none text-gray-500 mx-1 nc-copy-row-url min-w-4"
class="nc-icon-transition cursor-pointer select-none text-gray-500 mx-1 nc-copy-row-url min-w-4"
@click="copyRecordUrl"
/>
</a-tooltip>
@ -114,20 +134,40 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<MdiCommentTextOutline
v-if="isUIAllowed('rowComments') && !isNew"
v-e="['c:row-expand:comment-toggle']"
class="cursor-pointer select-none nc-toggle-comments text-gray-500 mx-1 min-w-4"
class="nc-icon-transition cursor-pointer select-none nc-toggle-comments text-gray-500 mx-1 min-w-4"
@click="commentsDrawer = !commentsDrawer"
/>
</a-tooltip>
<a-button class="!text mx-1 nc-expand-form-close-btn" @click="emit('cancel')">
<div class="flex items-center">
<MdiCloseCircleOutline class="mr-1" />
<!-- Close -->
{{ $t('general.close') }}
</div>
</a-button>
<a-tooltip v-if="!isSqlView" placement="bottom">
<!-- Duplicate row -->
<template #title>
<div class="text-center w-full">{{ $t('activity.duplicateRow') }}</div>
</template>
<MdiContentCopy
v-if="isUIAllowed('xcDatatableEditable') && !isNew"
v-e="['c:row-expand:duplicate']"
class="nc-icon-transition cursor-pointer select-none nc-duplicate-row text-gray-500 mx-1 min-w-4"
@click="!isNew && emit('duplicateRow')"
/>
</a-tooltip>
<a-tooltip v-if="!isSqlView" placement="bottom">
<!-- Delete row -->
<template #title>
<div class="text-center w-full">{{ $t('activity.deleteRow') }}</div>
</template>
<MdiDeleteOutline
v-if="isUIAllowed('xcDatatableEditable') && !isNew"
v-e="['c:row-expand:delete']"
class="nc-icon-transition cursor-pointer select-none nc-delete-row text-gray-500 mx-1 min-w-4"
@click="!isNew && onDeleteRowClick()"
/>
</a-tooltip>
<a-dropdown-button class="nc-expand-form-save-btn" type="primary" :disabled="!isUIAllowed('tableRowUpdate')" @click="save">
<template #icon><MdiMenuDown /></template>
<template #overlay>
<a-menu class="nc-expand-form-save-dropdown-menu">
<a-menu-item key="0" class="!py-2 flex gap-2" @click="saveRowAndStay = 0">
@ -153,5 +193,26 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
{{ $t('activity.saveAndStay') }}
</div>
</a-dropdown-button>
<a-tooltip placement="bottom">
<!-- Close -->
<template #title>
<div class="text-center w-full">{{ $t('general.close') }}</div>
</template>
<MdiCloseCircleOutline
class="nc-icon-transition cursor-pointer select-none nc-close-form text-gray-500 mx-1 min-w-4"
@click="emit('cancel')"
/>
</a-tooltip>
<a-modal v-model:visible="showDeleteRowModal" title="Delete row?" @ok="onConfirmDeleteRowClick">
<p>Are you sure you want to delete this row?</p>
</a-modal>
</div>
</template>
<style scoped>
:deep(svg) {
@apply outline-none;
}
</style>

46
packages/nc-gui/components/smartsheet/expanded-form/index.vue

@ -23,10 +23,6 @@ import {
} from '#imports'
import type { Row } from '~/lib'
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev'])
interface Props {
modelValue?: boolean
row: Row
@ -39,6 +35,12 @@ interface Props {
showNextPrevIcons?: boolean
}
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev'])
const { t } = useI18n()
const row = ref(props.row)
const state = toRef(props, 'state')
@ -60,6 +62,8 @@ provide(MetaInj, meta)
const { commentsDrawer, changedColumns, state: rowState, isNew, loadRow } = useProvideExpandedFormStore(meta, row)
const duplicatingRowInProgress = ref(false)
if (props.loadRow) {
await loadRow()
}
@ -101,6 +105,23 @@ const onClose = () => {
isExpanded.value = false
}
const onDuplicateRow = () => {
duplicatingRowInProgress.value = true
const newRow = Object.assign(
{},
{
row: row.value.row,
oldRow: {},
rowMeta: { new: true },
},
)
setTimeout(async () => {
row.value = newRow
duplicatingRowInProgress.value = false
message.success(t('msg.success.rowDuplicatedWithoutSavedYet'))
}, 500)
}
const reloadParentRowHook = inject(ReloadRowDataHookInj, createEventHook())
// override reload trigger and use it to reload grid and the form itself
@ -139,15 +160,17 @@ export default {
<a-drawer
v-model:visible="isExpanded"
:footer="null"
width="min(90vw,800px)"
:width="commentsDrawer ? 'min(90vw,900px)' : 'min(90vw,700px)'"
:body-style="{ 'padding': 0, 'display': 'flex', 'flex-direction': 'column' }"
:closable="false"
class="nc-drawer-expanded-form"
:class="{ active: isExpanded }"
>
<SmartsheetExpandedFormHeader :view="props.view" @cancel="onClose" />
<SmartsheetExpandedFormHeader :view="props.view" @cancel="onClose" @duplicate-row="onDuplicateRow" />
<div class="!bg-gray-100 rounded flex-1 relative">
<div class="!bg-gray-100 rounded flex-1">
<div class="flex h-full nc-form-wrapper items-stretch min-h-[max(70vh,100%)]">
<div class="flex-1 overflow-auto scrollbar-thin-dull nc-form-fields-container relative">
<template v-if="props.showNextPrevIcons">
<a-tooltip placement="bottom">
<template #title>
@ -162,12 +185,13 @@ export default {
<MdiChevronLeft class="cursor-pointer nc-prev-arrow" @click="$emit('prev')" />
</a-tooltip>
</template>
<div class="flex h-full nc-form-wrapper items-stretch min-h-[max(70vh,100%)]">
<div class="flex-1 overflow-auto scrollbar-thin-dull nc-form-fields-container">
<div class="w-[500px] mx-auto">
<div v-if="duplicatingRowInProgress" class="flex items-center justify-center h-[100px]">
<a-spin size="large" />
</div>
<div
v-for="(col, i) of fields"
v-else
v-show="!isVirtualCol(col) || !isNew || col.uidt === UITypes.LinkToAnotherRecord"
:key="col.title"
class="mt-2 py-2"
@ -234,9 +258,11 @@ export default {
.nc-next-arrow {
@apply absolute opacity-70 rounded-full transition-transform transition-background transition-opacity transform bg-white hover:(bg-gray-200) active:(scale-125 opacity-100) text-xl;
}
.nc-prev-arrow {
@apply left-4 top-4;
}
.nc-next-arrow {
@apply right-4 top-4;
}

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

@ -68,7 +68,7 @@ const deleteColumn = () =>
},
})
const setAsPrimaryValue = async () => {
const setAsDisplayValue = async () => {
try {
await $api.dbTableColumn.primaryColumnSet(column?.value?.id as string)
@ -280,13 +280,13 @@ const hideField = async () => {
</a-menu-item>
<a-divider class="!my-0" />
<a-menu-item v-if="!virtual && !column?.pv" @click="setAsPrimaryValue">
<a-menu-item v-if="(!virtual || column?.uidt === UITypes.Formula) && !column?.pv" @click="setAsDisplayValue">
<div class="nc-column-set-primary nc-header-menu-item">
<MdiStar class="text-primary" />
<!-- todo : tooltip -->
<!-- Set as Primary value -->
{{ $t('activity.setPrimary') }}
<!-- Set as Display value -->
{{ $t('activity.setDisplay') }}
</div>
</a-menu-item>

108
packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue

@ -1,5 +1,6 @@
<script setup lang="ts">
import type { FilterType } from 'nocodb-sdk'
import type { ColumnType, FilterType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import {
ActiveViewInj,
MetaInj,
@ -42,7 +43,18 @@ const reloadDataHook = inject(ReloadViewDataHookInj)!
const { $e } = useNuxtApp()
const { nestedFilters } = useSmartsheetStoreOrThrow()
const { filters, nonDeletedFilters, deleteFilter, saveOrUpdate, loadFilters, addFilter, addFilterGroup, sync } = useViewFilters(
const {
filters,
nonDeletedFilters,
deleteFilter,
saveOrUpdate,
loadFilters,
addFilter,
addFilterGroup,
sync,
saveOrUpdateDebounced,
isComparisonOpAllowed,
} = useViewFilters(
activeView,
parentId,
computed(() => autoSave),
@ -53,16 +65,37 @@ const { filters, nonDeletedFilters, deleteFilter, saveOrUpdate, loadFilters, add
const localNestedFilters = ref()
const columns = computed(() => meta.value?.columns)
const getColumn = (filter: Filter) => {
return columns.value?.find((col: ColumnType) => col.id === filter.fk_column_id)
}
const filterPrevComparisonOp = ref<Record<string, string>>({})
const filterUpdateCondition = (filter: FilterType, i: number) => {
const col = getColumn(filter)
if (
col.uidt === UITypes.SingleSelect &&
['anyof', 'nanyof'].includes(filterPrevComparisonOp.value[filter.id]) &&
['eq', 'neq'].includes(filter.comparison_op!)
) {
// anyof and nanyof can allow multiple selections,
// while `eq` and `neq` only allow one selection
filter.value = ''
} else if (['blank', 'notblank', 'empty', 'notempty', 'null', 'notnull'].includes(filter.comparison_op!)) {
// since `blank`, `empty`, `null` doesn't require value,
// hence remove the previous value
filter.value = ''
}
saveOrUpdate(filter, i)
filterPrevComparisonOp.value[filter.id] = filter.comparison_op
$e('a:filter:update', {
logical: filter.logical_op,
comparison: filter.comparison_op,
})
}
const columns = computed(() => meta.value?.columns)
const types = computed(() => {
if (!meta.value?.columns?.length) {
return {}
@ -76,7 +109,7 @@ const types = computed(() => {
watch(
() => activeView.value?.id,
(n, o) => {
(n: string, o: string) => {
// if nested no need to reload since it will get reloaded from parent
if (!nested && n !== o && (hookId || !webHook)) loadFilters(hookId as string)
},
@ -86,7 +119,7 @@ loadFilters(hookId as string)
watch(
() => nonDeletedFilters.value.length,
(length) => {
(length: number) => {
emit('update:filtersLength', length ?? 0)
},
)
@ -103,19 +136,23 @@ const applyChanges = async (hookId?: string, _nested = false) => {
}
}
const isComparisonOpAllowed = (filter: FilterType, compOp: typeof comparisonOpList[number]) => {
// show current selected value in list even if not allowed
if (filter.comparison_op === compOp.value) return true
const selectFilterField = (filter: Filter, index: number) => {
// when we change the field,
// the corresponding default filter operator needs to be changed as well
// since the existing one may not be supported for the new field
// e.g. `eq` operator is not supported in checkbox field
// hence, get the first option of the supported operators of the new field
filter.comparison_op = comparisonOpList(getColumn(filter)!.uidt as UITypes).filter((compOp) =>
isComparisonOpAllowed(filter, compOp),
)?.[0].value
// reset filter value as well
filter.value = ''
saveOrUpdate(filter, index)
}
// include allowed values only if selected column type matches
if (compOp.includedTypes) {
return filter.fk_column_id && compOp.includedTypes.includes(types.value[filter.fk_column_id])
}
// include not allowed values only if selected column type not matches
else if (compOp.excludedTypes) {
return filter.fk_column_id && !compOp.excludedTypes.includes(types.value[filter.fk_column_id])
}
return true
const updateFilterValue = (value: string, filter: Filter, index: number) => {
filter.value = value
saveOrUpdateDebounced(filter, index)
}
defineExpose({
@ -127,7 +164,7 @@ defineExpose({
<template>
<div
class="p-4 menu-filter-dropdown bg-gray-50 !border mt-4"
:class="{ 'shadow min-w-[430px] max-w-[630px] max-h-[max(80vh,500px)] overflow-auto': !nested, 'border-1 w-full': nested }"
:class="{ 'shadow min-w-[430px] max-h-[max(80vh,500px)] overflow-auto': !nested, 'border-1 w-full': nested }"
>
<div v-if="filters && filters.length" class="nc-filter-grid mb-2" @click.stop>
<template v-for="(filter, i) in filters" :key="i">
@ -189,14 +226,13 @@ defineExpose({
hide-details
:disabled="filter.readOnly"
dropdown-class-name="nc-dropdown-filter-logical-op"
@click.stop
@change="filterUpdateCondition(filter, i)"
@click.stop
>
<a-select-option v-for="op of logicalOps" :key="op.value" :value="op.value">
{{ op.text }}
</a-select-option>
</a-select>
<LazySmartsheetToolbarFieldListAutoCompleteDropdown
:key="`${i}_6`"
v-model="filter.fk_column_id"
@ -204,9 +240,8 @@ defineExpose({
:columns="columns"
:disabled="filter.readOnly"
@click.stop
@change="saveOrUpdate(filter, i)"
@change="selectFilterField(filter, i)"
/>
<a-select
v-model:value="filter.comparison_op"
:dropdown-match-select-width="false"
@ -219,7 +254,7 @@ defineExpose({
dropdown-class-name="nc-dropdown-filter-comp-op"
@change="filterUpdateCondition(filter, i)"
>
<template v-for="compOp of comparisonOpList" :key="compOp.value">
<template v-for="compOp of comparisonOpList(getColumn(filter)?.uidt)" :key="compOp.value">
<a-select-option v-if="isComparisonOpAllowed(filter, compOp)" :value="compOp.value">
{{ compOp.text }}
</a-select-option>
@ -229,19 +264,28 @@ defineExpose({
<span
v-if="
filter.comparison_op &&
['null', 'notnull', 'checked', 'notchecked', 'empty', 'notempty'].includes(filter.comparison_op)
['null', 'notnull', 'checked', 'notchecked', 'empty', 'notempty', 'blank', 'notblank'].includes(
filter.comparison_op,
)
"
:key="`span${i}`"
/>
<a-input
<a-checkbox
v-else-if="filter.field && types[filter.field] === 'boolean'"
v-model:checked="filter.value"
dense
:disabled="filter.readOnly"
@change="saveOrUpdate(filter, i)"
/>
<LazySmartsheetToolbarFilterInput
v-else
:key="`${i}_7`"
v-model:value="filter.value"
class="nc-filter-value-select"
:disabled="filter.readOnly || !filter.fk_column_id"
class="nc-filter-value-select min-w-[120px]"
:column="getColumn(filter)"
:filter="filter"
@update-filter-value="(value) => updateFilterValue(value, filter, i)"
@click.stop
@input="saveOrUpdate(filter, i)"
/>
</template>
</template>
@ -271,7 +315,7 @@ defineExpose({
<style scoped>
.nc-filter-grid {
grid-template-columns: 18px 83px 160px auto auto;
grid-template-columns: auto auto auto auto auto;
@apply grid gap-[12px] items-center;
}

22
packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue

@ -1,8 +1,8 @@
<script setup lang="ts">
import type { SelectProps } from 'ant-design-vue'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { MetaInj, computed, inject, ref, resolveComponent } from '#imports'
import { RelationTypes, UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { ActiveViewInj, MetaInj, computed, inject, ref, resolveComponent, useViewColumns } from '#imports'
const { modelValue, isSort } = defineProps<{
modelValue?: string
@ -18,10 +18,21 @@ const localValue = computed({
set: (val) => emit('update:modelValue', val),
})
const activeView = inject(ActiveViewInj, ref())
const { showSystemFields, metaColumnById } = useViewColumns(activeView, meta)
const options = computed<SelectProps['options']>(() =>
meta.value?.columns
?.filter((c: ColumnType) => {
if (c.uidt === UITypes.QrCode || c.uidt === UITypes.Barcode) {
if (isSystemColumn(metaColumnById?.value?.[c.id!])) {
return (
/** if the field is used in filter, then show it anyway */
localValue.value === c.id ||
/** hide system columns if not enabled */
showSystemFields.value
)
} else if (c.uidt === UITypes.QrCode || c.uidt === UITypes.Barcode || c.uidt === UITypes.ID) {
return false
} else if (isSort) {
/** ignore hasmany and manytomany relations if it's using within sort menu */
@ -48,6 +59,11 @@ const options = computed<SelectProps['options']>(() =>
)
const filterOption = (input: string, option: any) => option.label.toLowerCase()?.includes(input.toLowerCase())
// when a new filter is created, select a field by default
if (!localValue.value) {
localValue.value = (options.value?.[0].value as string) || ''
}
</script>
<template>

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

@ -68,7 +68,7 @@ watch(
const numberOfHiddenFields = computed(() => filteredFieldList.value?.filter((field) => !field.show)?.length)
const gridPrimaryValueField = computed(() => {
const gridDisplayValueField = computed(() => {
if (activeView.value?.type !== ViewTypes.GRID) return null
const pvCol = Object.values(metaColumnById.value)?.find((col) => col?.pv)
return filteredFieldList.value?.find((field) => field.fk_column_id === pvCol?.id)
@ -194,7 +194,7 @@ useMenuCloseOnEsc(open)
<Draggable v-model="fields" item-key="id" @change="onMove($event)">
<template #item="{ element: field, index: index }">
<div
v-if="filteredFieldList.filter((el) => el !== gridPrimaryValueField).includes(field)"
v-if="filteredFieldList.filter((el) => el !== gridDisplayValueField).includes(field)"
:key="field.id"
class="px-2 py-1 flex items-center"
:data-testid="`nc-fields-menu-${field.title}`"
@ -220,15 +220,15 @@ useMenuCloseOnEsc(open)
</template>
<template v-if="activeView?.type === ViewTypes.GRID" #header>
<div
v-if="gridPrimaryValueField"
:key="`pv-${gridPrimaryValueField.id}`"
v-if="gridDisplayValueField"
:key="`pv-${gridDisplayValueField.id}`"
class="px-2 py-1 flex items-center"
:data-testid="`nc-fields-menu-${gridPrimaryValueField.title}`"
:data-testid="`nc-fields-menu-${gridDisplayValueField.title}`"
@click.stop
>
<a-tooltip placement="bottom">
<template #title>
<span class="text-sm">Primary Value</span>
<span class="text-sm">Display Value</span>
</template>
<MdiTableKey class="text-xs" />

194
packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue

@ -0,0 +1,194 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import {
ColumnInj,
EditModeInj,
ReadonlyInj,
computed,
isBoolean,
isCurrency,
isDate,
isDateTime,
isDecimal,
isDuration,
isFloat,
isInt,
isMultiSelect,
isPercent,
isRating,
isSingleSelect,
isTextArea,
isTime,
isYear,
provide,
ref,
toRef,
useProject,
} from '#imports'
import type { Filter } from '~/lib'
import SingleSelect from '~/components/cell/SingleSelect.vue'
import MultiSelect from '~/components/cell/MultiSelect.vue'
import DatePicker from '~/components/cell/DatePicker.vue'
import YearPicker from '~/components/cell/YearPicker.vue'
import DateTimePicker from '~/components/cell/DateTimePicker.vue'
import TimePicker from '~/components/cell/TimePicker.vue'
import Rating from '~/components/cell/Rating.vue'
import Duration from '~/components/cell/Duration.vue'
import Percent from '~/components/cell/Percent.vue'
import Currency from '~/components/cell/Currency.vue'
import Decimal from '~/components/cell/Decimal.vue'
import Integer from '~/components/cell/Integer.vue'
import Float from '~/components/cell/Float.vue'
import Text from '~/components/cell/Text.vue'
interface Props {
column: ColumnType
filter: Filter
}
interface Emits {
(event: 'updateFilterValue', model: any): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const column = toRef(props, 'column')
const editEnabled = ref(true)
provide(ColumnInj, column)
provide(EditModeInj, readonly(editEnabled))
provide(ReadonlyInj, ref(false))
const checkTypeFunctions = {
isSingleSelect,
isMultiSelect,
isDate,
isYear,
isDateTime,
isTime,
isRating,
isDuration,
isPercent,
isCurrency,
isDecimal,
isInt,
isFloat,
isTextArea,
}
type FilterType = keyof typeof checkTypeFunctions
const { sqlUi } = $(useProject())
const abstractType = $computed(() => (column.value?.dt && sqlUi ? sqlUi.getAbstractType(column.value) : null))
const checkType = (filterType: FilterType) => {
const checkTypeFunction = checkTypeFunctions[filterType]
if (!column.value || !checkTypeFunction) {
return false
}
return checkTypeFunction(column.value, abstractType)
}
const filterInput = computed({
get: () => {
return props.filter.value
},
set: (value) => {
emit('updateFilterValue', value)
},
})
const booleanOptions = [
{ value: true, label: 'true' },
{ value: false, label: 'false' },
{ value: null, label: 'unset' },
]
const componentMap: Partial<Record<FilterType, any>> = $computed(() => {
return {
// use MultiSelect for SingleSelect columns for anyof / nanyof filters
isSingleSelect: ['anyof', 'nanyof'].includes(props.filter.comparison_op!) ? MultiSelect : SingleSelect,
isMultiSelect: MultiSelect,
isDate: DatePicker,
isYear: YearPicker,
isDateTime: DateTimePicker,
isTime: TimePicker,
isRating: Rating,
isDuration: Duration,
isPercent: Percent,
isCurrency: Currency,
isDecimal: Decimal,
isInt: Integer,
isFloat: Float,
}
})
const filterType = $computed(() => {
return Object.keys(componentMap).find((key) => checkType(key as FilterType))
})
const componentProps = $computed(() => {
switch (filterType) {
case 'isSingleSelect':
case 'isMultiSelect': {
return { disableOptionCreation: true }
}
case 'isPercent':
case 'isDecimal':
case 'isFloat':
case 'isInt': {
return { class: 'h-32px' }
}
case 'isDuration': {
return { showValidationError: false }
}
default: {
return {}
}
}
})
const hasExtraPadding = $computed(() => {
return (
column.value &&
(isInt(column.value, abstractType) ||
isDate(column.value, abstractType) ||
isDateTime(column.value, abstractType) ||
isTime(column.value, abstractType) ||
isYear(column.value, abstractType))
)
})
</script>
<template>
<a-select
v-if="column && isBoolean(column, abstractType)"
v-model:value="filterInput"
:disabled="filter.readOnly"
:options="booleanOptions"
/>
<div
v-else
class="bg-white border-1 flex min-w-120px max-w-170px min-h-32px h-full"
:class="{ 'px-2': hasExtraPadding }"
@mouseup.stop
>
<component
:is="filterType ? componentMap[filterType] : Text"
v-model="filterInput"
:disabled="filter.readOnly"
placeholder="Enter a value"
:column="column"
class="flex"
v-bind="componentProps"
/>
</div>
</template>

2
packages/nc-gui/components/template/Editor.vue

@ -516,7 +516,7 @@ async function importTemplate() {
tab.title = createdTable.title as string
}
// set primary value
// set display value
if (createdTable?.columns?.[0]?.id) {
await $api.dbTableColumn.primaryColumnSet(createdTable.columns[0].id as string)
}

6
packages/nc-gui/components/virtual-cell/BelongsTo.vue

@ -44,7 +44,7 @@ const listItemsDlg = ref(false)
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
const { loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>,
row,
isNew,
@ -85,8 +85,8 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
<template>
<div class="flex w-full chips-wrapper items-center" :class="{ active }">
<div class="chips flex items-center flex-1">
<template v-if="value && relatedTablePrimaryValueProp">
<VirtualCellComponentsItemChip :item="value" :value="value[relatedTablePrimaryValueProp]" @unlink="unlinkRef(value)" />
<template v-if="value && relatedTableDisplayValueProp">
<VirtualCellComponentsItemChip :item="value" :value="value[relatedTableDisplayValueProp]" @unlink="unlinkRef(value)" />
</template>
</div>

6
packages/nc-gui/components/virtual-cell/HasMany.vue

@ -40,7 +40,7 @@ const { isUIAllowed } = useUIPermission()
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
const { loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>,
row,
isNew,
@ -60,9 +60,9 @@ const localCellValue = computed<any[]>(() => {
const cells = computed(() =>
localCellValue.value.reduce((acc, curr) => {
if (!relatedTablePrimaryValueProp.value) return acc
if (!relatedTableDisplayValueProp.value) return acc
const value = curr[relatedTablePrimaryValueProp.value]
const value = curr[relatedTableDisplayValueProp.value]
if (!value) return acc

6
packages/nc-gui/components/virtual-cell/ManyToMany.vue

@ -42,7 +42,7 @@ const { isUIAllowed } = useUIPermission()
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
const { loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>,
row,
isNew,
@ -62,9 +62,9 @@ const localCellValue = computed<any[]>(() => {
const cells = computed(() =>
localCellValue.value.reduce((acc, curr) => {
if (!relatedTablePrimaryValueProp.value) return acc
if (!relatedTableDisplayValueProp.value) return acc
const value = curr[relatedTablePrimaryValueProp.value]
const value = curr[relatedTableDisplayValueProp.value]
if (!value) return acc

4
packages/nc-gui/components/virtual-cell/components/ListChildItems.vue

@ -37,7 +37,7 @@ const {
deleteRelatedRow,
loadChildrenList,
childrenListPagination,
relatedTablePrimaryValueProp,
relatedTableDisplayValueProp,
unlink,
getRelatedTableRowId,
relatedTableMeta,
@ -146,7 +146,7 @@ const onClick = (row: Row) => {
>
<div class="flex items-center">
<div class="flex-1 overflow-hidden min-w-0">
{{ row[relatedTablePrimaryValueProp] }}
{{ row[relatedTableDisplayValueProp] }}
<span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span>
</div>

4
packages/nc-gui/components/virtual-cell/components/ListItems.vue

@ -32,7 +32,7 @@ const {
childrenExcludedList,
loadChildrenExcludedList,
childrenExcludedListPagination,
relatedTablePrimaryValueProp,
relatedTableDisplayValueProp,
link,
getRelatedTableRowId,
relatedTableMeta,
@ -201,7 +201,7 @@ const activeRow = (vNode?: InstanceType<typeof Card>) => {
:class="{ 'nc-selected-row': selectedRowIndex === i }"
@click="linkRow(refRow)"
>
{{ refRow[relatedTablePrimaryValueProp] }}
{{ refRow[relatedTableDisplayValueProp] }}
<span class="hidden group-hover:(inline) text-gray-400 text-[11px] ml-1">
({{ $t('labels.primaryKey') }} : {{ getRelatedTableRowId(refRow) }})
</span>

9
packages/nc-gui/composables/useExpandedFormStore.ts

@ -49,7 +49,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
const { sharedView } = useSharedView()
// getters
const primaryValue = computed(() => {
const displayValue = computed(() => {
if (row?.value?.row) {
const col = meta?.value?.columns?.find((c) => c.pv)
@ -190,7 +190,8 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
}
} else {
// No columns to update
return message.info(t('msg.info.noColumnsToUpdate'))
message.info(t('msg.info.noColumnsToUpdate'))
return
}
}
@ -199,7 +200,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
addOrEditStackRow(row.value, isNewRow)
}
message.success(`${primaryValue.value || 'Row'} updated successfully.`)
message.success(`${displayValue.value || 'Row'} updated successfully.`)
changedColumns.value = new Set()
} catch (e: any) {
@ -237,7 +238,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
isYou,
commentsDrawer,
row,
primaryValue,
displayValue,
save,
changedColumns,
loadRow,

22
packages/nc-gui/composables/useLTARStore.ts

@ -94,14 +94,14 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
await getMeta(colOptions.fk_related_model_id as string)
}
const relatedTablePrimaryValueProp = computed(() => {
const relatedTableDisplayValueProp = computed(() => {
return (relatedTableMeta.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title || ''
})
const relatedTablePrimaryKeyProps = computed(() => {
return relatedTableMeta.value?.columns?.filter((c) => c.pk)?.map((c) => c.title) ?? []
})
const primaryValueProp = computed(() => {
const displayValueProp = computed(() => {
return (meta.value?.columns?.find((c: Required<ColumnType>) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title
})
@ -125,8 +125,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
offset: childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1),
where:
childrenExcludedListPagination.query &&
`(${relatedTablePrimaryValueProp.value},like,${childrenExcludedListPagination.query})`,
fields: [relatedTablePrimaryValueProp.value, ...relatedTablePrimaryKeyProps.value],
`(${relatedTableDisplayValueProp.value},like,${childrenExcludedListPagination.query})`,
fields: [relatedTableDisplayValueProp.value, ...relatedTablePrimaryKeyProps.value],
} as RequestParams,
},
)
@ -142,8 +142,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
offset: childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1),
where:
childrenExcludedListPagination.query &&
`(${relatedTablePrimaryValueProp.value},like,${childrenExcludedListPagination.query})`,
fields: [relatedTablePrimaryValueProp.value, ...relatedTablePrimaryKeyProps.value],
`(${relatedTableDisplayValueProp.value},like,${childrenExcludedListPagination.query})`,
fields: [relatedTableDisplayValueProp.value, ...relatedTablePrimaryKeyProps.value],
} as any,
)
} else {
@ -160,7 +160,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
// todo: where clause is missing from type
where:
childrenExcludedListPagination.query &&
`(${relatedTablePrimaryValueProp.value},like,${childrenExcludedListPagination.query})`,
`(${relatedTableDisplayValueProp.value},like,${childrenExcludedListPagination.query})`,
} as any,
)
}
@ -183,7 +183,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
limit: String(childrenListPagination.size),
offset: String(childrenListPagination.size * (childrenListPagination.page - 1)),
where:
childrenListPagination.query && `(${relatedTablePrimaryValueProp.value},like,${childrenListPagination.query})`,
childrenListPagination.query && `(${relatedTableDisplayValueProp.value},like,${childrenListPagination.query})`,
} as any,
)
} else {
@ -198,7 +198,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
limit: String(childrenListPagination.size),
offset: String(childrenListPagination.size * (childrenListPagination.page - 1)),
where:
childrenListPagination.query && `(${relatedTablePrimaryValueProp.value},like,${childrenListPagination.query})`,
childrenListPagination.query && `(${relatedTableDisplayValueProp.value},like,${childrenListPagination.query})`,
} as any,
)
}
@ -320,13 +320,13 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
return {
relatedTableMeta,
loadRelatedTableMeta,
relatedTablePrimaryValueProp,
relatedTableDisplayValueProp,
childrenExcludedList,
childrenList,
rowId,
childrenExcludedListPagination,
childrenListPagination,
primaryValueProp,
displayValueProp,
meta,
unlink,
link,

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

@ -34,7 +34,9 @@ export const useProject = createSharedComposable(() => {
const projectLoadedHook = createEventHook<ProjectType>()
const project = ref<ProjectType>({})
const bases = computed<BaseType[]>(() => project.value?.bases || [])
const tables = ref<TableType[]>([])
const projectMetaInfo = ref<ProjectMetaInfo | undefined>()
@ -49,10 +51,13 @@ export const useProject = createSharedComposable(() => {
const projectType = $computed(() => route.params.projectType as string)
const projectMeta = computed<Record<string, any>>(() => {
const defaultMeta = {
showNullAndEmptyInFilter: false,
}
try {
return isString(project.value.meta) ? JSON.parse(project.value.meta) : project.value.meta
return (isString(project.value.meta) ? JSON.parse(project.value.meta) : project.value.meta) ?? defaultMeta
} catch (e) {
return {}
return defaultMeta
}
})
@ -169,6 +174,10 @@ export const useProject = createSharedComposable(() => {
$e('c:themes:change')
}
async function hasEmptyOrNullFilters() {
return await api.project.hasEmptyOrNullFilters(projectId.value)
}
const reset = () => {
project.value = {}
tables.value = []
@ -207,5 +216,6 @@ export const useProject = createSharedComposable(() => {
isLoading,
lastOpenedViewMap,
isXcdbBase,
hasEmptyOrNullFilters,
}
})

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

@ -546,5 +546,6 @@ export function useViewData(
removeLastEmptyRow,
removeRowIfNew,
navigateToSiblingRow,
deleteRowById,
}
}

108
packages/nc-gui/composables/useViewFilters.ts

@ -1,15 +1,20 @@
import type { FilterType, ViewType } from 'nocodb-sdk'
import type { ColumnType, FilterType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import type { SelectProps } from 'ant-design-vue'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import {
ActiveViewInj,
IsPublicInj,
ReloadViewDataHookInj,
MetaInj,
computed,
extractSdkResponseErrorMsg,
inject,
message,
ref,
useDebounceFn,
useMetas,
useNuxtApp,
useProject,
useUIPermission,
watch,
} from '#imports'
@ -30,6 +35,8 @@ export function useViewFilters(
const { nestedFilters } = useSmartsheetStoreOrThrow()
const { projectMeta } = useProject()
const isPublic = inject(IsPublicInj, ref(false))
const { $api, $e } = useNuxtApp()
@ -68,12 +75,81 @@ export function useViewFilters(
// nonDeletedFilters are those filters that are not deleted physically & virtually
const nonDeletedFilters = computed(() => filters.value.filter((f) => f.status !== 'delete'))
const placeholderFilter: Filter = {
comparison_op: 'eq',
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const { showSystemFields, metaColumnById } = useViewColumns(activeView, meta)
const options = computed<SelectProps['options']>(() =>
meta.value?.columns?.filter((c: ColumnType) => {
if (isSystemColumn(metaColumnById?.value?.[c.id!])) {
/** hide system columns if not enabled */
return showSystemFields.value
} else if (c.uidt === UITypes.QrCode || c.uidt === UITypes.Barcode || c.uidt === UITypes.ID || c.system) {
return false
} else {
const isVirtualSystemField = c.colOptions && c.system
return !isVirtualSystemField
}
}),
)
const types = computed(() => {
if (!meta.value?.columns?.length) {
return {}
}
return meta.value?.columns?.reduce((obj: any, col: any) => {
obj[col.id] = col.uidt
return obj
}, {})
})
const isComparisonOpAllowed = (
filter: FilterType,
compOp: {
text: string
value: string
ignoreVal?: boolean
includedTypes?: UITypes[]
excludedTypes?: UITypes[]
},
) => {
const isNullOrEmptyOp = ['empty', 'notempty', 'null', 'notnull'].includes(compOp.value)
if (compOp.includedTypes) {
// include allowed values only if selected column type matches
if (filter.fk_column_id && compOp.includedTypes.includes(types.value[filter.fk_column_id])) {
// for 'empty', 'notempty', 'null', 'notnull',
// show them based on `showNullAndEmptyInFilter` in Project Settings
return isNullOrEmptyOp ? projectMeta.value.showNullAndEmptyInFilter : true
} else {
return false
}
} else if (compOp.excludedTypes) {
// include not allowed values only if selected column type not matches
if (filter.fk_column_id && !compOp.excludedTypes.includes(types.value[filter.fk_column_id])) {
// for 'empty', 'notempty', 'null', 'notnull',
// show them based on `showNullAndEmptyInFilter` in Project Settings
return isNullOrEmptyOp ? projectMeta.value.showNullAndEmptyInFilter : true
} else {
return false
}
}
// explicitly include for non-null / non-empty ops
return isNullOrEmptyOp ? projectMeta.value.showNullAndEmptyInFilter : true
}
const placeholderFilter = (): Filter => {
return {
comparison_op: comparisonOpList(options.value?.[0].uidt as UITypes).filter((compOp) =>
isComparisonOpAllowed({ fk_column_id: options.value?.[0].id }, compOp),
)?.[0].value,
value: '',
status: 'create',
logical_op: 'and',
}
}
const loadFilters = async (hookId?: string) => {
if (nestedMode.value) {
@ -191,8 +267,6 @@ export function useViewFilters(
fk_parent_id: parentId,
})
}
reloadHook?.trigger()
} catch (e: any) {
console.log(e)
message.error(await extractSdkResponseErrorMsg(e))
@ -201,13 +275,16 @@ export function useViewFilters(
reloadData?.()
}
const saveOrUpdateDebounced = useDebounceFn(saveOrUpdate, 500)
const addFilter = () => {
filters.value.push({ ...placeholderFilter })
filters.value.push(placeholderFilter())
$e('a:filter:add', { length: filters.value.length })
}
const addFilterGroup = async () => {
const child = { ...placeholderFilter }
const child = placeholderFilter()
const placeHolderGroupFilter: Filter = {
is_group: true,
status: 'create',
@ -234,10 +311,21 @@ export function useViewFilters(
return metas?.value?.[view?.value?.fk_model_id as string]?.columns?.length || 0
},
async (nextColsLength, oldColsLength) => {
async (nextColsLength: number, oldColsLength: number) => {
if (nextColsLength && nextColsLength < oldColsLength) await loadFilters()
},
)
return { filters, nonDeletedFilters, loadFilters, sync, deleteFilter, saveOrUpdate, addFilter, addFilterGroup }
return {
filters,
nonDeletedFilters,
loadFilters,
sync,
deleteFilter,
saveOrUpdate,
addFilter,
addFilterGroup,
saveOrUpdateDebounced,
isComparisonOpAllowed,
}
}

3
packages/nc-gui/lang/ar.json

@ -380,13 +380,13 @@
"renameTable": "إعادة تسمية الجدول",
"deleteTable": "حذف الجدول",
"addField": "إضافة حقل جديد إلى هذا الجدول",
"setPrimary": "تعيين كقيمة أساسية",
"addRow": "إضافة صف جديد",
"saveRow": "حفظ الصف",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "إدراج صف جديد",
"deleteRow": "حذف الصف",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "حذف الصفوف المحددة",
"importExcel": "استيراد Excel",
"importCSV": "استيراد CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "تم تحديث ACL واجهة المستخدم للجداول بنجاح",
"pluginUninstalled": "تم إلغاء تثبيت الإضافة بنجاح",
"pluginSettingsSaved": "تم حفظ إعدادات الإضافة بنجاح",

3
packages/nc-gui/lang/bn_IN.json

@ -380,13 +380,13 @@
"renameTable": "টিল নম পরিবরতন",
"deleteTable": "टबल मि",
"addField": "এই টি নতন কর যত করন",
"setPrimary": "পথমিক মন হিট করন",
"addRow": "নতন সিত করন",
"saveRow": "সিরকষণ করন",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "নতন সিন",
"deleteRow": "সিন",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "নিিত সিিন",
"importExcel": "একল আমদি করন",
"importCSV": "Import CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully",

5
packages/nc-gui/lang/cs.json

@ -206,7 +206,7 @@
"advancedSettings": "Pokročilá nastavení",
"codeSnippet": "Úryvek kódu",
"keyboardShortcut": "Klávesové zkratky",
"generateRandomName": "Generate Random Name"
"generateRandomName": "Vytvořit náhodné jméno"
},
"labels": {
"createdBy": "Vytvořil/a",
@ -380,13 +380,13 @@
"renameTable": "Přejmenování tabulky",
"deleteTable": "Tabulka Odstranit",
"addField": "Přidání nového pole do této tabulky",
"setPrimary": "Nastavit jako primární hodnotu",
"addRow": "Přidat nový řádek",
"saveRow": "Uložit řádek",
"saveAndExit": "Uložit a odejít",
"saveAndStay": "Uložit a zůstat",
"insertRow": "Vložit nový řádek",
"deleteRow": "Odstranit řádek",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Odstranit vybrané řádky",
"importExcel": "Importovat Excel",
"importCSV": "Importovat CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Sloupec úspěšně duplikován",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Úspěšná aktualizace uživatelského rozhraní ACL pro tabulky",
"pluginUninstalled": "Plugin byl úspěšně odinstalován",
"pluginSettingsSaved": "Nastavení zásuvného modulu bylo úspěšně uloženo",

3
packages/nc-gui/lang/da.json

@ -380,13 +380,13 @@
"renameTable": "Bord omdøb",
"deleteTable": "TABEL DELETE.",
"addField": "Tilføj nyt felt til denne tabel",
"setPrimary": "Indstil som primær værdi",
"addRow": "Tilføj ny række",
"saveRow": "Gem ro",
"saveAndExit": "Gem og afslutning",
"saveAndStay": "Gem og bliv",
"insertRow": "Indsæt ny række",
"deleteRow": "DELETE ROW.",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Slet de valgte rækker",
"importExcel": "Import Excel.",
"importCSV": "Import CSV.",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Kolonne duplikeret med succes",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Opdateret UI ACL for tabeller med succes",
"pluginUninstalled": "Plugin afinstalleret med succes",
"pluginSettingsSaved": "Plugin-indstillingerne er gemt med succes",

3
packages/nc-gui/lang/de.json

@ -381,13 +381,13 @@
"renameTable": "Tabelle umbenennen",
"deleteTable": "Tabelle löschen",
"addField": "Neues Feld zu dieser Tabelle hinzufügen",
"setPrimary": "Als Primärwert festlegen",
"addRow": "Neue Zeile hinzufügen",
"saveRow": "Zeile speichern",
"saveAndExit": "Speichern & Verlassen",
"saveAndStay": "Speichern & Bleiben",
"insertRow": "Neue Zeile einfügen",
"deleteRow": "Zeile löschen",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Ausgewählte Zeilen löschen",
"importExcel": "Excel-Datei importieren",
"importCSV": "CSV-Datei importieren",
@ -717,6 +717,7 @@
},
"success": {
"columnDuplicated": "Spalte erfolgreich dupliziert",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "UI ACL für Tabellen erfolgreich aktualisiert",
"pluginUninstalled": "Plugin erfolgreich deinstalliert",
"pluginSettingsSaved": "Plugin-Einstellungen erfolgreich gespeichert",

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

@ -381,13 +381,14 @@
"renameTable": "Table Rename",
"deleteTable": "Table Delete",
"addField": "Add new field to this table",
"setPrimary": "Set as Primary value",
"setDisplay": "Set as Display value",
"addRow": "Add new row",
"saveRow": "Save row",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Insert New Row",
"deleteRow": "Delete Row",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Delete Selected Rows",
"importExcel": "Import Excel",
"importCSV": "Import CSV",
@ -641,6 +642,11 @@
"deleteViewConfirmation": "Are you sure you want to delete this view?",
"deleteTableConfirmation": "Do you want to delete the table",
"showM2mTables": "Show M2M Tables",
"showM2mTablesDesc": "Many-to-many relation is supported via a junction table & is hidden by default. Enable this option to list all such tables along with existing tables.",
"showNullInCells": "Show NULL in Cells",
"showNullInCellsDesc": "Display 'NULL' tag in cells holding NULL value. This helps differentiate against cells holding EMPTY string.",
"showNullAndEmptyInFilter": "Show NULL and EMPTY in Filter",
"showNullAndEmptyInFilterDesc": "Enable 'additional' filters to differentiate fields containing NULL & Empty Strings. Default support for Blank treats both NULL & Empty strings alike.",
"deleteKanbanStackConfirmation": "Deleting this stack will also remove the select option `{stackToBeDeleted}` from the `{groupingField}`. The records will move to the uncategorized stack.",
"computedFieldEditWarning": "Computed field: contents are read-only. Use column edit menu to reconfigure",
"computedFieldDeleteWarning": "Computed field: contents are read-only. Unable to clear content.",
@ -727,6 +733,7 @@
},
"success": {
"columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully",

3
packages/nc-gui/lang/es.json

@ -380,13 +380,13 @@
"renameTable": "Cambiar el nombre de la tabla",
"deleteTable": "Borrar tabla",
"addField": "Añadir nuevo campo a esta tabla",
"setPrimary": "Establecido como clave primaria",
"addRow": "Añadir nueva fila",
"saveRow": "Grabar la fila",
"saveAndExit": "Guardar y salir",
"saveAndStay": "Ahorrar y quedarse",
"insertRow": "Insertar nueva fila",
"deleteRow": "Borrar fila",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Eliminar filas seleccionadas",
"importExcel": "Importar Excel",
"importCSV": "Importar CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Columna duplicada con éxito",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Actualizada con éxito la interfaz de usuario ACL para las tablas",
"pluginUninstalled": "Plugin desinstalado correctamente",
"pluginSettingsSaved": "La configuración del plugin se ha guardado correctamente",

3
packages/nc-gui/lang/eu.json

@ -380,13 +380,13 @@
"renameTable": "Table Rename",
"deleteTable": "Table Delete",
"addField": "Add new field to this table",
"setPrimary": "Set as Primary value",
"addRow": "Add new row",
"saveRow": "Save row",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Insert New Row",
"deleteRow": "Delete Row",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Delete Selected Rows",
"importExcel": "Import Excel",
"importCSV": "Import CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully",

3
packages/nc-gui/lang/fa.json

@ -380,13 +380,13 @@
"renameTable": "تغییر نام جدول",
"deleteTable": "حذف جدول",
"addField": "اضافه کردن فیلد جدید به این جدول",
"setPrimary": "تنظیم به عنوان مقدار اولیه",
"addRow": "اضافه کردن ردیف جدید",
"saveRow": "دخیره ردیف",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "وارد کردن ردیف جدید",
"deleteRow": "حذف ردیف جدید",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "حذف ردیفهای انتخاب شده",
"importExcel": "وارد کردن فایل Excel",
"importCSV": "Import CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully",

3
packages/nc-gui/lang/fi.json

@ -380,13 +380,13 @@
"renameTable": "Taulukko uudelleen",
"deleteTable": "Taulukko poistaa",
"addField": "Lisää uusi kenttä tähän taulukkoon",
"setPrimary": "Aseta ensisijainen arvo",
"addRow": "Lisää uusi rivi",
"saveRow": "Tallenna rivi",
"saveAndExit": "Tallenna & poistu",
"saveAndStay": "Save & Stay",
"insertRow": "Lisää uusi rivi",
"deleteRow": "Poista rivi",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Poista valitut rivit",
"importExcel": "Tuonti excel",
"importCSV": "Tuo CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Sarake monistettu onnistuneesti",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Päivitetty UI ACL taulukoille onnistuneesti",
"pluginUninstalled": "Liitännäisen poisto onnistui onnistuneesti",
"pluginSettingsSaved": "Liitännäisen asetukset tallennettu onnistuneesti",

3
packages/nc-gui/lang/fr.json

@ -380,13 +380,13 @@
"renameTable": "Renommer le tableau",
"deleteTable": "Supprimer le tableau",
"addField": "Ajouter un nouveau champ à ce tableau",
"setPrimary": "Définir comme valeur primaire",
"addRow": "Ajouter une nouvelle ligne",
"saveRow": "Enregistrer la ligne",
"saveAndExit": "Enregistrer et quitter",
"saveAndStay": "Enregistrer et rester",
"insertRow": "Insérer une nouvelle ligne",
"deleteRow": "Supprimer la ligne",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Supprimer les lignes sélectionnées",
"importExcel": "Importer depuis Excel",
"importCSV": "Importer un fichier CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Colonne dupliquée avec succès",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Mise à jour réussie de l'ACL de l'interface utilisateur pour les tables",
"pluginUninstalled": "Le plugin a été désinstallé avec succès",
"pluginSettingsSaved": "Les paramètres du plugin ont été enregistrés avec succès",

3
packages/nc-gui/lang/he.json

@ -380,13 +380,13 @@
"renameTable": "שולחן שינוי שם",
"deleteTable": "טבלה מחיקה",
"addField": "הוסף שדה חדש לטבלה זו",
"setPrimary": "להגדיר כערך ראשי",
"addRow": "הוסף שורה חדשה",
"saveRow": "שמור שורה",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "הכנס שורה חדשה",
"deleteRow": "מחק שורה",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "מחק את השורות שנבחרו",
"importExcel": "ייבוא Excel",
"importCSV": "Import CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully",

3
packages/nc-gui/lang/hi.json

@ -380,13 +380,13 @@
"renameTable": "तिम",
"deleteTable": "टबल मि",
"addField": "इस ति नयड ज",
"setPrimary": "पथमिक मय कप मट कर",
"addRow": "नई पि",
"saveRow": "पि सह",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "नई पि",
"deleteRow": "पि हट",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "चयनित पि हट",
"importExcel": "आयत एकल",
"importCSV": "Import CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully",

3
packages/nc-gui/lang/hr.json

@ -380,13 +380,13 @@
"renameTable": "Preimenovati stolom",
"deleteTable": "Obriši tablicu",
"addField": "Dodajte novo polje na ovu tablicu",
"setPrimary": "Postavite kao primarnu vrijednost",
"addRow": "Dodaj novi red",
"saveRow": "Spremanje retka",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Umetnite novi red",
"deleteRow": "Brisanje retka",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Izbrišite odabrane retke",
"importExcel": "Uvoz Excel",
"importCSV": "Import CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully",

3
packages/nc-gui/lang/id.json

@ -380,13 +380,13 @@
"renameTable": "Ganti nama meja",
"deleteTable": "Table Delete.",
"addField": "Tambahkan bidang baru ke tabel ini",
"setPrimary": "Tetapkan sebagai nilai utama",
"addRow": "Tambahkan baris baru",
"saveRow": "Hemat Baris",
"saveAndExit": "Simpan & Keluar",
"saveAndStay": "Simpan & Menginap",
"insertRow": "Masukkan baris baru.",
"deleteRow": "Hapus Baris",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Hapus baris yang dipilih",
"importExcel": "Impor Excel.",
"importCSV": "Impor CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Kolom berhasil diduplikasi",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Berhasil memperbarui ACL UI untuk tabel",
"pluginUninstalled": "Plugin berhasil dihapus instalasinya",
"pluginSettingsSaved": "Pengaturan plugin berhasil disimpan",

3
packages/nc-gui/lang/it.json

@ -380,13 +380,13 @@
"renameTable": "Rinomina tabella",
"deleteTable": "Elimina tabella",
"addField": "Aggiungi un nuovo campo a questa tabella",
"setPrimary": "Impostare come valore primario",
"addRow": "Aggiungi nuova riga",
"saveRow": "Salva riga",
"saveAndExit": "Salvare e uscire",
"saveAndStay": "Risparmia e resta",
"insertRow": "Inserisci nuova riga",
"deleteRow": "Elimina riga.",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Elimina righe selezionate",
"importExcel": "Importa Excel.",
"importCSV": "Importazione CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Colonna duplicata con successo",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Aggiornamento dell'UI ACL per le tabelle con successo",
"pluginUninstalled": "Il plugin è stato disinstallato con successo",
"pluginSettingsSaved": "Le impostazioni del plugin sono state salvate con successo",

3
packages/nc-gui/lang/ja.json

@ -380,13 +380,13 @@
"renameTable": "テーブル名の変更",
"deleteTable": "テーブルを削除",
"addField": "新しいフィールドを追加",
"setPrimary": "プライマリ値として設定",
"addRow": "行を追加",
"saveRow": "行を保存",
"saveAndExit": "保存して終了",
"saveAndStay": "保存して続ける",
"insertRow": "行を挿入",
"deleteRow": "行を削除",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "選択行を削除",
"importExcel": "エクセルファイルをインポート",
"importCSV": "CSV のインポート",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "カラムを複製しました",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "テーブルの UI ACL を更新しました",
"pluginUninstalled": "プラグインをアンインストールしました",
"pluginSettingsSaved": "プラグインの設定を保存しました",

3
packages/nc-gui/lang/ko.json

@ -380,13 +380,13 @@
"renameTable": "테이블 이름 바꾸기",
"deleteTable": "테이블 삭제",
"addField": "테이블에 새 필드 추가",
"setPrimary": "Primary value로 설정",
"addRow": "행 추가",
"saveRow": "행 저장",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "행 삽입",
"deleteRow": "행 삭제",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "선택한 행 삭제",
"importExcel": "엑셀 가져오기",
"importCSV": "CSV 가져오기",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully",

3
packages/nc-gui/lang/lv.json

@ -380,13 +380,13 @@
"renameTable": "Tabulas pārdēvēšana",
"deleteTable": "Tabulas dzēšana",
"addField": "Jauna lauka pievienošana",
"setPrimary": "Uzstādīt kā primāro atslēgu",
"addRow": "Pievienot ierakstu",
"saveRow": "Saglabāt ierakstu",
"saveAndExit": "Saglabāt un iziet",
"saveAndStay": "Saglabāt un palikt",
"insertRow": "Pievienot jaunu ierakstu",
"deleteRow": "Dzēst ierakstu",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Dzēst izvēlētos ierakstus",
"importExcel": "Importēt Excel",
"importCSV": "CSV importēšana",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Sekmīgi dublēta sleja",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Veiksmīgi atjaunināts UI ACL tabulām",
"pluginUninstalled": "Spraudnis veiksmīgi atinstalēts",
"pluginSettingsSaved": "Veiksmīgi saglabāti spraudņa iestatījumi",

3
packages/nc-gui/lang/nl.json

@ -380,13 +380,13 @@
"renameTable": "Tabel hernoemen",
"deleteTable": "Tabel verwijderen",
"addField": "Voeg nieuw veld toe aan deze tabel",
"setPrimary": "Instellen als primaire waarde",
"addRow": "Nieuwe rij toevoegen",
"saveRow": "Sla rij op",
"saveAndExit": "Opslaan en afsluiten",
"saveAndStay": "Sparen & Blijven",
"insertRow": "Voeg nieuwe rij toe",
"deleteRow": "Verwijder rij",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Verwijder geselecteerde rijen",
"importExcel": "Excel importeren",
"importCSV": "CSV importeren",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Kolom succesvol gedupliceerd",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "UI ACL voor tabellen met succes bijgewerkt",
"pluginUninstalled": "Plugin succesvol verwijderd",
"pluginSettingsSaved": "Plugin-instellingen succesvol opgeslagen",

3
packages/nc-gui/lang/no.json

@ -380,13 +380,13 @@
"renameTable": "Tabell omdøpe",
"deleteTable": "Bordet slett",
"addField": "Legg til nytt felt i denne tabellen",
"setPrimary": "Sett som primærverdi",
"addRow": "Legg til ny rad",
"saveRow": "Lagre rad",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Sett inn ny rad",
"deleteRow": "Slett rad",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Slett utvalgte rader",
"importExcel": "Importer Excel.",
"importCSV": "Import CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully",

3
packages/nc-gui/lang/pl.json

@ -380,13 +380,13 @@
"renameTable": "Zmień nazwę tabeli.",
"deleteTable": "Usuń tabelę",
"addField": "Dodaj nowe pole do tej tabeli",
"setPrimary": "Ustaw jako wartość podstawowa",
"addRow": "Dodaj nowy rząd",
"saveRow": "Zapisz wiersz",
"saveAndExit": "Zapisz i wyjdź",
"saveAndStay": "Oszczędzaj i zostań",
"insertRow": "Wstaw nowy rząd",
"deleteRow": "Usuń rząd",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Usuń wybrane wiersze",
"importExcel": "Importuj Excel.",
"importCSV": "Importuj z CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Kolumna powielona z powodzeniem",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Pomyślnie zaktualizowano UI ACL dla tabel",
"pluginUninstalled": "Wtyczka odinstalowana pomyślnie",
"pluginSettingsSaved": "Ustawienia wtyczki zapisane pomyślnie",

3
packages/nc-gui/lang/pt.json

@ -380,13 +380,13 @@
"renameTable": "Tabela Renomear",
"deleteTable": "Tabela Delete.",
"addField": "Adicionar novo campo a esta tabela",
"setPrimary": "Definido como valor primário",
"addRow": "Adicionar nova linha",
"saveRow": "Salvar linha",
"saveAndExit": "Salvar & Sair",
"saveAndStay": "Salvar & Ficar",
"insertRow": "Insira a nova linha",
"deleteRow": "Excluir linha",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Excluir linhas selecionadas",
"importExcel": "Importar Excel.",
"importCSV": "Importar CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Coluna duplicada com sucesso",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "UI ACL actualizada para tabelas com sucesso",
"pluginUninstalled": "Plugin desinstalado com sucesso",
"pluginSettingsSaved": "Configurações de plugin guardadas com sucesso",

3
packages/nc-gui/lang/pt_BR.json

@ -380,13 +380,13 @@
"renameTable": "Tabela Renomear",
"deleteTable": "Tabela Delete.",
"addField": "Adicionar novo campo a esta tabela",
"setPrimary": "Definido como valor primário",
"addRow": "Adicionar nova linha",
"saveRow": "Salvar linha",
"saveAndExit": "Salvar & Sair",
"saveAndStay": "Salvar & Ficar",
"insertRow": "Insira a nova linha",
"deleteRow": "Excluir linha",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Excluir linhas selecionadas",
"importExcel": "Importar Excel.",
"importCSV": "Importar CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Coluna duplicada com sucesso",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "UI ACL actualizada para tabelas com sucesso",
"pluginUninstalled": "Plugin desinstalado com sucesso",
"pluginSettingsSaved": "Configurações de plugin guardadas com sucesso",

3
packages/nc-gui/lang/ru.json

@ -380,13 +380,13 @@
"renameTable": "Переименовать таблицу",
"deleteTable": "Удалить таблицу",
"addField": "Добавить новое поле в эту таблицу",
"setPrimary": "Установить в качестве основного значения",
"addRow": "Добавить новую строку",
"saveRow": "Сохранить строку",
"saveAndExit": "Сохранить и выйти",
"saveAndStay": "Сохранить и остаться",
"insertRow": "Вставить новую строку",
"deleteRow": "Удалить строку",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Удалить выбранные строки",
"importExcel": "Импорт из Excel",
"importCSV": "Импорт CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Столбец успешно скопирован",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Успешно обновлен пользовательский интерфейс ACL для таблиц",
"pluginUninstalled": "Плагин успешно удален",
"pluginSettingsSaved": "Настройки плагина сохранены",

3
packages/nc-gui/lang/sk.json

@ -380,13 +380,13 @@
"renameTable": "Premenovanie tabuľky",
"deleteTable": "Tabuľka Vymazať",
"addField": "Pridanie nového poľa do tejto tabuľky",
"setPrimary": "Nastaviť ako primárnu hodnotu",
"addRow": "Pridanie nového riadku",
"saveRow": "Uložiť riadok",
"saveAndExit": "Uložiť a ukončiť",
"saveAndStay": "Uložiť a zostať",
"insertRow": "Vložiť nový riadok",
"deleteRow": "Odstrániť riadok",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Odstránenie vybraných riadkov",
"importExcel": "Importovať aplikáciu Excel",
"importCSV": "Import CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Stĺpec bol úspešne duplikovaný",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Úspešne aktualizované používateľské rozhranie ACL pre tabuľky",
"pluginUninstalled": "Plugin úspešne odinštalovaný",
"pluginSettingsSaved": "Nastavenia zásuvného modulu boli úspešne uložené",

3
packages/nc-gui/lang/sl.json

@ -380,13 +380,13 @@
"renameTable": "Preimenuj tabele",
"deleteTable": "Tabela Delete.",
"addField": "V to tabelo dodajte novo polje",
"setPrimary": "Kot primarna vrednost",
"addRow": "Dodaj novo vrstico",
"saveRow": "Shrani vrstico",
"saveAndExit": "Shranjevanje in izhod",
"saveAndStay": "Shranjevanje in bivanje",
"insertRow": "Vstavite novo vrstico",
"deleteRow": "Izbriši vrstico",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Izbrišite izbrane vrstice",
"importExcel": "Uvoz Excel.",
"importCSV": "Uvoz CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Stolpec je bil uspešno podvojen",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Uspešno posodobljen uporabniški vmesnik ACL za tabele",
"pluginUninstalled": "Vtičnik uspešno odstranjen",
"pluginSettingsSaved": "Nastavitve vtičnika so bile uspešno shranjene",

3
packages/nc-gui/lang/sv.json

@ -380,13 +380,13 @@
"renameTable": "Bordsbyte",
"deleteTable": "Bord radera",
"addField": "Lägg till nytt fält till den här tabellen",
"setPrimary": "Ange som primärt värde",
"addRow": "Lägg till ny rad",
"saveRow": "Spara rad",
"saveAndExit": "Spara och avsluta",
"saveAndStay": "Spara och stanna",
"insertRow": "Infoga ny rad",
"deleteRow": "Radera raden",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Ta bort valda rader",
"importExcel": "Import excel",
"importCSV": "Importera CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Kolumnen duplicerades framgångsrikt",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Uppdaterade UI ACL för tabeller framgångsrikt",
"pluginUninstalled": "Plugin avinstallerades framgångsrikt",
"pluginSettingsSaved": "Plugin-inställningarna har sparats framgångsrikt",

3
packages/nc-gui/lang/th.json

@ -380,13 +380,13 @@
"renameTable": "ตารางเปลยนชอ",
"deleteTable": "ลบตาราง",
"addField": "เพมฟลดใหมลงในตารางน",
"setPrimary": "ตงคาเปนคาปฐมภ",
"addRow": "เพมแถวใหม",
"saveRow": "บนทกแถว",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "แทรกแถวใหม",
"deleteRow": "ลบแถว",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "ลบแถวทเลอก",
"importExcel": "นำเขา Excel",
"importCSV": "Import CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully",

3
packages/nc-gui/lang/tr.json

@ -380,13 +380,13 @@
"renameTable": "Tabloyu Yeniden Adlandır",
"deleteTable": "Tabloyu Sil",
"addField": "Tabloya yeni alan ekle",
"setPrimary": "Birincil değer yap",
"addRow": "Yeni satır ekle",
"saveRow": "Satırı kaydet",
"saveAndExit": "Kaydet ve Çık",
"saveAndStay": "Kaydet ve Kal",
"insertRow": "Yeni Satır Ekle",
"deleteRow": "Satırı Sil",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Seçilen Satırları Sil",
"importExcel": "Excel içe aktar",
"importCSV": "CSV içe aktar",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Sütun başarıyla çoğaltıldı",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Tablolar için UI ACL başarıyla güncellendi",
"pluginUninstalled": "Eklenti başarıyla kaldırıldı",
"pluginSettingsSaved": "Eklenti ayarları başarıyla kaydedildi",

3
packages/nc-gui/lang/uk.json

@ -380,13 +380,13 @@
"renameTable": "Перейменувати таблицю",
"deleteTable": "Видалити таблицю",
"addField": "Додати нове поле до цієї таблиці",
"setPrimary": "Встановити як основне значення",
"addRow": "Додати новий рядок",
"saveRow": "Зберегти рядок",
"saveAndExit": "Зберегти та вийти",
"saveAndStay": "Зберегти та залишитись",
"insertRow": "Вставити новий рядок",
"deleteRow": "Видалити рядок",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Видалити вибрані рядки",
"importExcel": "Імпортувати з Excel",
"importCSV": "Імпортувати з CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Стовпець успішно продубльовано",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Успішно оновлено UI ACL для таблиць",
"pluginUninstalled": "Плагін успішно видалено",
"pluginSettingsSaved": "Налаштування плагіну успішно збережено",

3
packages/nc-gui/lang/vi.json

@ -380,13 +380,13 @@
"renameTable": "Đổi tên bảng.",
"deleteTable": "Bảng xóa",
"addField": "Thêm trường mới vào bảng này",
"setPrimary": "Đặt dưới dạng giá trị chính",
"addRow": "Thêm hàng mới",
"saveRow": "Lưu hàng.",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Chèn hàng mới",
"deleteRow": "Xóa hàng",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Xóa các hàng đã chọn",
"importExcel": "Nhập Excel.",
"importCSV": "Import CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully",

3
packages/nc-gui/lang/zh-Hans.json

@ -380,13 +380,13 @@
"renameTable": "重命名表格",
"deleteTable": "删除表格",
"addField": "添加新字段",
"setPrimary": "设置为主要值",
"addRow": "添加新行",
"saveRow": "保存行",
"saveAndExit": "保存并退出",
"saveAndStay": "保存并留在此页",
"insertRow": "插入新行",
"deleteRow": "删除行",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "删除所选行",
"importExcel": "导入 Excel",
"importCSV": "导入 CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "此列的副本创建成功",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "已成功更新表的 UI ACL",
"pluginUninstalled": "插件卸载成功",
"pluginSettingsSaved": "插件设置保存成功",

3
packages/nc-gui/lang/zh-Hant.json

@ -380,13 +380,13 @@
"renameTable": "表重命名",
"deleteTable": "表刪除",
"addField": "將新字段添加到此表",
"setPrimary": "設置為主要值",
"addRow": "新增行",
"saveRow": "儲存行",
"saveAndExit": "儲存並結束",
"saveAndStay": "Save & Stay",
"insertRow": "插入新行",
"deleteRow": "刪除行",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "刪除所選行",
"importExcel": "匯入 Excel",
"importCSV": "匯入 CSV",
@ -716,6 +716,7 @@
},
"success": {
"columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "外掛移除安裝成功",
"pluginSettingsSaved": "外掛設定儲存成功",

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

@ -31,7 +31,7 @@
"leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0",
"nocodb-sdk": "file:../nocodb-sdk",
"nocodb-sdk": "0.105.0",
"papaparse": "^5.3.2",
"qrcode": "^1.5.1",
"socket.io-client": "^4.5.1",
@ -57,6 +57,7 @@
"@iconify-json/clarity": "^1.1.4",
"@iconify-json/eva": "^1.1.2",
"@iconify-json/ic": "^1.1.7",
"@iconify-json/la": "^1.1.2",
"@iconify-json/logos": "^1.1.14",
"@iconify-json/lucide": "^1.1.36",
"@iconify-json/material-symbols": "^1.1.8",
@ -102,7 +103,8 @@
}
},
"../nocodb-sdk": {
"version": "0.104.3",
"version": "0.105.0",
"extraneous": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -1275,8 +1277,19 @@
"eslint": ">=3.14.1"
}
},
"../nocodb-sdk/node_modules/eslint-config-prettier/node_modules/get-stdin": {
"version": "6.0.0",
"node_modules/@iconify-json/la": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@iconify-json/la/-/la-1.1.2.tgz",
"integrity": "sha512-Cv93a5X5n9gYeUeQ7h9z5tmoZzChvwvbCorBQwMQgwCnMQynH6dCKdtbtYsZyT5wH4QYwywv7xgvpBIkqvZgqg==",
"dev": true,
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/logos": {
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/@iconify-json/logos/-/logos-1.1.18.tgz",
"integrity": "sha512-Ra8BV3fJhE/5omYOY2UlDhgggyxxzMTppGH3n9EfFYh9Rpx4+HtE6zp7s+i3glYOqs2Me5HNcDEOkU63kcHqvg==",
"dev": true,
"license": "MIT",
"engines": {
@ -7551,22 +7564,10 @@
"dev": true,
"license": "MIT"
},
"node_modules/async-sema": {
"version": "3.1.1",
"dev": true,
"license": "MIT"
},
"node_modules/async-validator": {
"version": "4.2.5",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"license": "MIT"
},
"node_modules/autoprefixer": {
"version": "10.4.13",
"dev": true,
"node_modules/follow-redirects": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"funding": [
{
"type": "opencollective",
@ -10323,8 +10324,27 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/p-locate": {
"version": "5.0.0",
"node_modules/nocodb-sdk": {
"version": "0.105.0",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.105.0.tgz",
"integrity": "sha512-QqUM8QtX+10UbKmlNjz0FFDSLWB5IV46E7HjGmzPb3Z3Hagq0KQPSXUlWAp2YaHsYsKEmqH7e8N/0AuLL3D/pg==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
}
},
"node_modules/nocodb-sdk/node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/node-abi": {
"version": "3.23.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.23.0.tgz",
"integrity": "sha512-XWte/uvq7hmgY27WesfxLUAPejKUlkEbikhBFaIhxe+XkHa57rXBwYqGjsIyfVXaU8kC0Wp2p/qQroauDKs1XA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -15492,8 +15512,19 @@
"node": ">=6"
}
},
"node_modules/read-pkg-up/node_modules/path-exists": {
"version": "4.0.0",
"@iconify-json/la": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@iconify-json/la/-/la-1.1.2.tgz",
"integrity": "sha512-Cv93a5X5n9gYeUeQ7h9z5tmoZzChvwvbCorBQwMQgwCnMQynH6dCKdtbtYsZyT5wH4QYwywv7xgvpBIkqvZgqg==",
"dev": true,
"requires": {
"@iconify/types": "*"
}
},
"@iconify-json/logos": {
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/@iconify-json/logos/-/logos-1.1.18.tgz",
"integrity": "sha512-Ra8BV3fJhE/5omYOY2UlDhgggyxxzMTppGH3n9EfFYh9Rpx4+HtE6zp7s+i3glYOqs2Me5HNcDEOkU63kcHqvg==",
"dev": true,
"license": "MIT",
"engines": {
@ -19336,7 +19367,30 @@
"version": "1.1.3",
"dev": true,
"requires": {
"@iconify/types": "*"
"flatted": "^3.1.0",
"rimraf": "^3.0.2"
}
},
"flatted": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz",
"integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==",
"dev": true
},
"follow-redirects": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"devOptional": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"@iconify-json/simple-icons": {
@ -27291,6 +27345,25 @@
}
}
},
"nocodb-sdk": {
"version": "0.105.0",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.105.0.tgz",
"integrity": "sha512-QqUM8QtX+10UbKmlNjz0FFDSLWB5IV46E7HjGmzPb3Z3Hagq0KQPSXUlWAp2YaHsYsKEmqH7e8N/0AuLL3D/pg==",
"requires": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
},
"dependencies": {
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
}
}
},
"node-abi": {
"version": "3.23.0",
"dev": true,

3
packages/nc-gui/package.json

@ -54,7 +54,7 @@
"leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0",
"nocodb-sdk": "file:../nocodb-sdk",
"nocodb-sdk": "0.105.0",
"papaparse": "^5.3.2",
"qrcode": "^1.5.1",
"socket.io-client": "^4.5.1",
@ -80,6 +80,7 @@
"@iconify-json/clarity": "^1.1.4",
"@iconify-json/eva": "^1.1.2",
"@iconify-json/ic": "^1.1.7",
"@iconify-json/la": "^1.1.2",
"@iconify-json/logos": "^1.1.14",
"@iconify-json/lucide": "^1.1.36",
"@iconify-json/material-symbols": "^1.1.8",

116
packages/nc-gui/utils/filterUtils.ts

@ -1,12 +1,46 @@
import { UITypes } from 'nocodb-sdk'
import { UITypes, isNumericCol, numericUITypes } from 'nocodb-sdk'
export const comparisonOpList: {
const getEqText = (fieldUiType: UITypes) => {
if (isNumericCol(fieldUiType)) {
return '='
} else if ([UITypes.SingleSelect, UITypes.Collaborator, UITypes.LinkToAnotherRecord].includes(fieldUiType)) {
return 'is'
}
return 'is equal'
}
const getNeqText = (fieldUiType: UITypes) => {
if (isNumericCol(fieldUiType)) {
return '!='
} else if ([UITypes.SingleSelect, UITypes.Collaborator, UITypes.LinkToAnotherRecord].includes(fieldUiType)) {
return 'is not'
}
return 'is not equal'
}
const getLikeText = (fieldUiType: UITypes) => {
if (fieldUiType === UITypes.Attachment) {
return 'filenames contain'
}
return 'is like'
}
const getNotLikeText = (fieldUiType: UITypes) => {
if (fieldUiType === UITypes.Attachment) {
return "filenames doesn't contain"
}
return 'is not like'
}
export const comparisonOpList = (
fieldUiType: UITypes,
): {
text: string
value: string
ignoreVal?: boolean
includedTypes?: UITypes[]
excludedTypes?: UITypes[]
}[] = [
}[] => [
{
text: 'is checked',
value: 'checked',
@ -20,44 +54,84 @@ export const comparisonOpList: {
includedTypes: [UITypes.Checkbox],
},
{
text: 'is equal',
text: getEqText(fieldUiType),
value: 'eq',
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment],
},
{
text: 'is not equal',
text: getNeqText(fieldUiType),
value: 'neq',
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment],
},
{
text: 'is like',
text: getLikeText(fieldUiType),
value: 'like',
excludedTypes: [UITypes.Checkbox],
excludedTypes: [UITypes.Checkbox, UITypes.SingleSelect, UITypes.MultiSelect, UITypes.Collaborator, ...numericUITypes],
},
{
text: 'is not like',
text: getNotLikeText(fieldUiType),
value: 'nlike',
excludedTypes: [UITypes.Checkbox],
excludedTypes: [UITypes.Checkbox, UITypes.SingleSelect, UITypes.MultiSelect, UITypes.Collaborator, ...numericUITypes],
},
{
text: 'is empty',
value: 'empty',
ignoreVal: true,
excludedTypes: [UITypes.Checkbox, UITypes.Rating, UITypes.Number, UITypes.Decimal, UITypes.Percent, UITypes.Currency],
excludedTypes: [
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
...numericUITypes,
],
},
{
text: 'is not empty',
value: 'notempty',
ignoreVal: true,
excludedTypes: [UITypes.Checkbox, UITypes.Rating, UITypes.Number, UITypes.Decimal, UITypes.Percent, UITypes.Currency],
excludedTypes: [
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
...numericUITypes,
],
},
{
text: 'is null',
value: 'null',
ignoreVal: true,
excludedTypes: [
...numericUITypes,
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
],
},
{
text: 'is not null',
value: 'notnull',
ignoreVal: true,
excludedTypes: [
...numericUITypes,
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
],
},
{
text: 'contains all of',
@ -82,21 +156,33 @@ export const comparisonOpList: {
{
text: '>',
value: 'gt',
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.SingleSelect],
includedTypes: [...numericUITypes],
},
{
text: '<',
value: 'lt',
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.SingleSelect],
includedTypes: [...numericUITypes],
},
{
text: '>=',
value: 'gte',
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.SingleSelect],
includedTypes: [...numericUITypes],
},
{
text: '<=',
value: 'lte',
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.SingleSelect],
includedTypes: [...numericUITypes],
},
{
text: 'is blank',
value: 'blank',
ignoreVal: true,
excludedTypes: [UITypes.Checkbox],
},
{
text: 'is not blank',
value: 'notblank',
ignoreVal: true,
excludedTypes: [UITypes.Checkbox],
},
]

2
packages/nc-lib-gui/package.json

@ -1,6 +1,6 @@
{
"name": "nc-lib-gui",
"version": "0.104.3",
"version": "0.105.0",
"description": "NocoDB GUI",
"author": {
"name": "NocoDB",

2
packages/noco-docs/content/en/engineering/playwright.md

@ -189,7 +189,7 @@ This a method which will reset/clear all the filters. Since this is an action me
```js
async resetFilter() {
await this.waitForResponse({
uiAction: this.get().locator('.nc-filter-item-remove-btn').click(),
uiAction: () => this.get().locator('.nc-filter-item-remove-btn').click(),
httpMethodsToMatch: ['DELETE'],
requestUrlPathToMatch: '/api/v1/db/meta/filters/',
});

14
packages/noco-docs/content/en/setup-and-usages/column-operations.md

@ -71,6 +71,16 @@ You can also group several filters together using Filter Group.
<img width="1025" alt="image" src="https://user-images.githubusercontent.com/35857179/189102932-aa0e31ef-554f-4e8b-ae0e-2024b7f4d35b.png">
### Supported filters
### Enable NULL and EMPTY Filters
Currently we support filter types - `is equal`, `is not equal`, `is like`, `is not like`, `is null`, `is not null` for string fields. We also support filter types - `>`, `<`, `>=`, and `<=` for numeric fields. Also we provide `is empty` and `is not empty` for checking if the column is empty or not.
NULL filters (`is null` & `is not null`) and EMPTY filters (`is empty` & `is not empty`) are hidden by default. If you wish to filter out either one only, you may enable `Show NULL and EMPTY Filter` in Project Settings.
![image](https://user-images.githubusercontent.com/35857179/219009085-0308b2a9-10af-4afe-84b6-df52e42fb1a8.png)
Otherwise, we can use Blank filters to filter out cells with NULL values and EMPTY values.
### Supported Filters
Currently we support different types of filters for corresponding columns. Please refer the below matrix for details.
<iframe width="100%" height="700vh" src="https://docs.google.com/spreadsheets/d/e/2PACX-1vTpCNKtA-szaXUKJEO5uuSIRnzUOK793MKnyBz9m2rQcwn7HqK19jPHeER-IIRWH9X56J78wfxXZuuv/pubhtml?gid=427284630&amp;single=true&amp;widget=true&amp;headers=false"></iframe>

36
packages/noco-docs/content/en/setup-and-usages/primary-value.md → packages/noco-docs/content/en/setup-and-usages/display-value.md

@ -1,40 +1,42 @@
---
title: "Primary Value"
description: "Understanding Primary Value in NocoDB!"
title: "Display Value"
description: "Understanding Display Value in NocoDB!"
position: 580
category: "Product"
menuTitle: "Primary Value"
menuTitle: "Display Value"
---
## What is a Primary Value ?
- Primary value as the name stands is the primary or main value within a row of a table that you generally associate that row with.
## What is a Display Value ?
- Display Value as the name stands is the primary or main value within a row of a table that you generally associate that row with.
- It should be usually associated with a column which is uniquely identifiable. However, this uniqueness is not enforced at the database level.
- Before v0.105.0, Display Value was known as Primary Value.
## What is the use of Primary Value ?
- Within a spreadsheet, primary value are always highlighted so that it is easier to recognise what row we are in.
- And when LinkToAnotherRecord is created between two tables - it is the primary value that appears in LinkToAnotheRecord column.
## What is the use of Display Value ?
- Within a spreadsheet, Display Value are always highlighted so that it is easier to recognise what row we are in.
- And when LinkToAnotherRecord is created between two tables - it is the Display Value that appears in LinkToAnotheRecord column.
#### Example : Primary Value highlighted in Actor table
#### Example : Display Value highlighted in Actor table
<img width="646" alt="image" src="https://user-images.githubusercontent.com/35857179/189114321-58ebaa16-20e2-4615-abda-39417a5df5bf.png">
#### Example : Primary Value highlighted in Film table
#### Example : Display Value highlighted in Film table
<img width="643" alt="image" src="https://user-images.githubusercontent.com/35857179/189114462-a7fef0e2-f9ac-4943-98d5-fee9f60a4ab5.png">
#### Example : Primary Value associated when LinkToAnotherRecord is created
#### Example : Display Value associated when LinkToAnotherRecord is created
<img width="311" alt="image" src="https://user-images.githubusercontent.com/35857179/189114548-193acc4d-f714-4204-a560-97668db7884c.png">
## How to set Primary Value ?
## How to set Display Value ?
Click down arrow in the target column. Click `Set as Primary Value`.
Click down arrow in the target column. Click `Set as Display Value`.
<img width="251" alt="image" src="https://user-images.githubusercontent.com/35857179/189114857-b452aa6b-5cdb-4a74-9980-cb839d7d15fd.png">
![image](https://user-images.githubusercontent.com/35857179/219339727-dee5fdea-6db7-4a06-9e48-df7113cc63b1.png)
## How is Primary Value identfied for existing database tables ?
## How is Display Value identfied for existing database tables ?
- It is usually the first column after the primary key which is not a number.
- If there is no column which is not a number then the column adjacent to primary key is chosen.
## Can I change the Primary Value to another column within tables ?
## Can I change the Display Value to another column within tables ?
- Yes, you can use the same way mentioned above to set Primary Value.
- Yes, you can use the same way mentioned above to set Display Value.

4
packages/noco-docs/content/en/setup-and-usages/table-operations.md

@ -159,7 +159,7 @@ You can use Quick Import when you have data from external sources such as Airtab
- **Use First Row as Headers**: If it is checked, the first row will be treated as header row.
- **Import Data**: If it is checked, all data will be imported. Otherwise, only table will be created.
![image](https://user-images.githubusercontent.com/35857179/197454479-1ed18dce-1d0b-4ee3-88b3-9b6a132dea2a.png)
- You can revise the table name by double clicking it, column name and column type. By default, the first column will be chosen as <a href="./primary-value" target="_blank">Primary Value</a> and cannot be deleted.
- You can revise the table name by double clicking it, column name and column type. By default, the first column will be chosen as <a href="./display-value" target="_blank">Display Value</a> and cannot be deleted.
![image](https://user-images.githubusercontent.com/35857179/197454633-5b30323e-2b13-4c55-843a-948c093d373e.png)
- Click `Import` to start importing process. The table will be created and the data will be imported.
![image](https://user-images.githubusercontent.com/35857179/197455547-2d93df5e-a7f0-4c88-af53-990067625967.png)
@ -172,7 +172,7 @@ You can use Quick Import when you have data from external sources such as Airtab
- **Use First Row as Headers**: If it is checked, the first row will be treated as header row.
- **Import Data**: If it is checked, all data will be imported. Otherwise, only table will be created.
![image](https://user-images.githubusercontent.com/35857179/197455788-8dd8a7d1-38f3-48c3-a05e-6ab0cf25045c.png)
- You can revise the table name, column name and column type. By default, the first column will be chosen as <a href="./primary-value" target="_blank">Primary Value</a> and cannot be deleted.
- You can revise the table name, column name and column type. By default, the first column will be chosen as <a href="./display-value" target="_blank">Display Value</a> and cannot be deleted.
<alert>
Note: If your Excel file contains multiple sheets, each sheet will be stored in a separate table.
</alert>

2
packages/nocodb-sdk/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb-sdk",
"version": "0.104.3",
"version": "0.105.0",
"description": "NocoDB SDK",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",

7
packages/nocodb-sdk/src/index.ts

@ -7,7 +7,12 @@ export * from './lib/globals';
export * from './lib/helperFunctions';
export * from './lib/enums';
export * from './lib/formulaHelpers';
export { default as UITypes, isVirtualCol } from './lib/UITypes';
export {
default as UITypes,
numericUITypes,
isNumericCol,
isVirtualCol,
} from './lib/UITypes';
export { default as CustomAPI } from './lib/CustomAPI';
export { default as TemplateGenerator } from './lib/TemplateGenerator';
export * from './lib/passwordHelpers';

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

@ -1897,6 +1897,22 @@ export class Api<
* No description
*
* @tags Project
* @name HasEmptyOrNullFilters
* @request GET:/api/v1/db/meta/projects/{projectId}/has-empty-or-null-filters
* @response `200` `any` OK
*/
hasEmptyOrNullFilters: (projectId: string, params: RequestParams = {}) =>
this.request<any, any>({
path: `/api/v1/db/meta/projects/${projectId}/has-empty-or-null-filters`,
method: 'GET',
format: 'json',
...params,
}),
/**
* No description
*
* @tags Project
* @name AuditList
* @request GET:/api/v1/db/meta/projects/{projectId}/audits
* @response `200` `{

22
packages/nocodb-sdk/src/lib/UITypes.ts

@ -40,6 +40,28 @@ enum UITypes {
Button = 'Button',
}
export const numericUITypes = [
UITypes.Duration,
UITypes.Currency,
UITypes.Percent,
UITypes.Number,
UITypes.Decimal,
UITypes.Rating,
UITypes.Rollup,
];
export function isNumericCol(
col:
| UITypes
| { readonly uidt: UITypes | string }
| ColumnReqType
| ColumnType
) {
return numericUITypes.includes(
<UITypes>(typeof col === 'object' ? col?.uidt : col)
);
}
export function isVirtualCol(
col:
| UITypes

206
packages/nocodb/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nocodb",
"version": "0.104.3",
"version": "0.105.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb",
"version": "0.104.3",
"version": "0.105.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@google-cloud/storage": "^5.7.2",
@ -58,17 +58,17 @@
"lru-cache": "^6.0.0",
"mailersend": "^1.1.0",
"minio": "^7.0.18",
"mkdirp": "^0.5.5",
"mkdirp": "^2.1.3",
"morgan": "^1.10.0",
"mssql": "^6.2.0",
"multer": "^1.4.2",
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-help": "0.2.85",
"nc-lib-gui": "0.104.3",
"nc-lib-gui": "0.105.0",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
"nocodb-sdk": "0.105.0",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"os-locale": "^5.0.0",
@ -154,7 +154,8 @@
}
},
"../nocodb-sdk": {
"version": "0.104.3",
"version": "0.105.0",
"extraneous": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -4258,6 +4259,18 @@
"run-queue": "^1.0.0"
}
},
"node_modules/copy-concurrently/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/copy-descriptor": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
@ -10320,6 +10333,17 @@
"node": ">8 <=18"
}
},
"node_modules/minio/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/minio/node_modules/through2": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz",
@ -10509,14 +10533,17 @@
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.3.tgz",
"integrity": "sha512-sjAkg21peAG9HS+Dkx7hlG9Ztx7HLeKnvB3NQRcu/mltCVmvkF0pisbiTSfDVYTT86XEfZrTUosLdZLStquZUw==",
"bin": {
"mkdirp": "bin/cmd.js"
"mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mocha": {
@ -10873,6 +10900,18 @@
"run-queue": "^1.0.3"
}
},
"node_modules/move-concurrently/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/move-file": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/move-file/-/move-file-1.2.0.tgz",
@ -11020,6 +11059,17 @@
"node": ">= 0.10.0"
}
},
"node_modules/multer/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/multi-stage-sourcemap": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/multi-stage-sourcemap/-/multi-stage-sourcemap-0.3.1.tgz",
@ -11208,9 +11258,9 @@
}
},
"node_modules/nc-lib-gui": {
"version": "0.104.3",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.104.3.tgz",
"integrity": "sha512-gQjq2Yg5vYaai/RiRDrdrWlw+qqglpha0tHuZQ/xGwSuAO9Gn/v62YeeI0DAw+wvzR8CL+p/B5CQcz1j1+gBLw==",
"version": "0.105.0",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.105.0.tgz",
"integrity": "sha512-nUKuiKt4xZqGYGC1juueKAW/2XpygA9X3eD2BCrZjwb3c33MmATwwN9/4LG2CkR1yvA5W9vGhqt+qXq5yfZ29w==",
"dependencies": {
"express": "^4.17.1"
}
@ -11269,8 +11319,13 @@
"dev": true
},
"node_modules/nocodb-sdk": {
"resolved": "../nocodb-sdk",
"link": true
"version": "0.105.0",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.105.0.tgz",
"integrity": "sha512-QqUM8QtX+10UbKmlNjz0FFDSLWB5IV46E7HjGmzPb3Z3Hagq0KQPSXUlWAp2YaHsYsKEmqH7e8N/0AuLL3D/pg==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
}
},
"node_modules/node-abort-controller": {
"version": "3.0.1",
@ -17147,6 +17202,17 @@
"node": ">= 0.12.0"
}
},
"node_modules/utility/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@ -18312,6 +18378,18 @@
"node": ">=0.10.0"
}
},
"node_modules/webpack/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/webpack/node_modules/readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
@ -22365,6 +22443,17 @@
"mkdirp": "^0.5.1",
"rimraf": "^2.5.4",
"run-queue": "^1.0.0"
},
"dependencies": {
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"requires": {
"minimist": "^1.2.6"
}
}
}
},
"copy-descriptor": {
@ -27041,6 +27130,14 @@
"xml2js": "^0.4.15"
},
"dependencies": {
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"requires": {
"minimist": "^1.2.6"
}
},
"through2": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz",
@ -27207,12 +27304,9 @@
}
},
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"requires": {
"minimist": "^1.2.6"
}
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.3.tgz",
"integrity": "sha512-sjAkg21peAG9HS+Dkx7hlG9Ztx7HLeKnvB3NQRcu/mltCVmvkF0pisbiTSfDVYTT86XEfZrTUosLdZLStquZUw=="
},
"mocha": {
"version": "10.1.0",
@ -27483,6 +27577,17 @@
"mkdirp": "^0.5.1",
"rimraf": "^2.5.4",
"run-queue": "^1.0.3"
},
"dependencies": {
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"requires": {
"minimist": "^1.2.6"
}
}
}
},
"move-file": {
@ -27606,6 +27711,16 @@
"on-finished": "^2.3.0",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
},
"dependencies": {
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"requires": {
"minimist": "^1.2.6"
}
}
}
},
"multi-stage-sourcemap": {
@ -27766,9 +27881,9 @@
}
},
"nc-lib-gui": {
"version": "0.104.3",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.104.3.tgz",
"integrity": "sha512-gQjq2Yg5vYaai/RiRDrdrWlw+qqglpha0tHuZQ/xGwSuAO9Gn/v62YeeI0DAw+wvzR8CL+p/B5CQcz1j1+gBLw==",
"version": "0.105.0",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.105.0.tgz",
"integrity": "sha512-nUKuiKt4xZqGYGC1juueKAW/2XpygA9X3eD2BCrZjwb3c33MmATwwN9/4LG2CkR1yvA5W9vGhqt+qXq5yfZ29w==",
"requires": {
"express": "^4.17.1"
}
@ -27815,22 +27930,12 @@
"dev": true
},
"nocodb-sdk": {
"version": "file:../nocodb-sdk",
"version": "0.105.0",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.105.0.tgz",
"integrity": "sha512-QqUM8QtX+10UbKmlNjz0FFDSLWB5IV46E7HjGmzPb3Z3Hagq0KQPSXUlWAp2YaHsYsKEmqH7e8N/0AuLL3D/pg==",
"requires": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.1",
"cspell": "^4.1.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"jsep": "^1.3.6",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"typescript": "^4.0.2"
"jsep": "^1.3.6"
}
},
"node-abort-controller": {
@ -32432,6 +32537,16 @@
"mkdirp": "^0.5.1",
"mz": "^2.7.0",
"unescape": "^1.0.1"
},
"dependencies": {
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"requires": {
"minimist": "^1.2.6"
}
}
}
},
"utils-merge": {
@ -33036,6 +33151,15 @@
"to-regex": "^3.0.2"
}
},
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"requires": {
"minimist": "^1.2.6"
}
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",

8
packages/nocodb/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb",
"version": "0.104.3",
"version": "0.105.0",
"description": "NocoDB Backend",
"main": "dist/bundle.js",
"author": {
@ -98,17 +98,17 @@
"lru-cache": "^6.0.0",
"mailersend": "^1.1.0",
"minio": "^7.0.18",
"mkdirp": "^0.5.5",
"mkdirp": "^2.1.3",
"morgan": "^1.10.0",
"mssql": "^6.2.0",
"multer": "^1.4.2",
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-help": "0.2.85",
"nc-lib-gui": "0.104.3",
"nc-lib-gui": "0.105.0",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
"nocodb-sdk": "0.105.0",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"os-locale": "^5.0.0",

2
packages/nocodb/src/lib/Noco.ts

@ -105,7 +105,7 @@ export default class Noco {
constructor() {
process.env.PORT = process.env.PORT || '8080';
// todo: move
process.env.NC_VERSION = '0104003';
process.env.NC_VERSION = '0104004';
// if env variable NC_MINIMAL_DBS is set, then disable project creation with external sources
if (process.env.NC_MINIMAL_DBS) {

227
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts

@ -8,7 +8,7 @@ import genRollupSelectv2 from './genRollupSelectv2';
import RollupColumn from '../../../../models/RollupColumn';
import formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2';
import FormulaColumn from '../../../../models/FormulaColumn';
import { RelationTypes, UITypes } from 'nocodb-sdk';
import { RelationTypes, UITypes, isNumericCol } from 'nocodb-sdk';
import { sanitize } from './helpers/sanitize';
export default async function conditionV2(
@ -119,7 +119,7 @@ const parseConditionV2 = async (
? negatedMapping[filter.comparison_op]
: {}),
fk_model_id: childModel.id,
fk_column_id: childModel?.primaryValue?.id,
fk_column_id: childModel?.displayValue?.id,
}),
knex,
aliasCount
@ -154,7 +154,7 @@ const parseConditionV2 = async (
? negatedMapping[filter.comparison_op]
: {}),
fk_model_id: parentModel.id,
fk_column_id: parentModel?.primaryValue?.id,
fk_column_id: parentModel?.displayValue?.id,
}),
knex,
aliasCount
@ -209,7 +209,7 @@ const parseConditionV2 = async (
? negatedMapping[filter.comparison_op]
: {}),
fk_model_id: parentModel.id,
fk_column_id: parentModel?.primaryValue?.id,
fk_column_id: parentModel?.displayValue?.id,
}),
knex,
aliasCount
@ -271,37 +271,135 @@ const parseConditionV2 = async (
return (qb: Knex.QueryBuilder) => {
let [field, val] = [_field, _val];
if (
[UITypes.Date, UITypes.DateTime].includes(column.uidt) &&
!val &&
['is', 'isnot'].includes(filter.comparison_op)
) {
// for date & datetime,
// val cannot be empty for non-is & non-isnot filters
return;
}
if (isNumericCol(column.uidt) && typeof val === 'string') {
// convert to number
val = +val;
}
switch (filter.comparison_op) {
case 'eq':
if (qb?.client?.config?.client === 'mysql2') {
if (
[
UITypes.Duration,
UITypes.Currency,
UITypes.Percent,
UITypes.Number,
UITypes.Decimal,
UITypes.Rating,
UITypes.Rollup,
].includes(column.uidt)
) {
qb = qb.where(field, val);
} else {
// mysql is case-insensitive for strings, turn to case-sensitive
qb = qb.whereRaw('BINARY ?? = ?', [field, val]);
}
} else {
qb = qb.where(field, val);
}
if (column.uidt === UITypes.Rating && val === 0) {
// unset rating is considered as NULL
qb = qb.orWhereNull(field);
}
break;
case 'neq':
case 'not':
if (qb?.client?.config?.client === 'mysql2') {
if (
[
UITypes.Duration,
UITypes.Currency,
UITypes.Percent,
UITypes.Number,
UITypes.Decimal,
UITypes.Rollup,
].includes(column.uidt)
) {
qb = qb.where((nestedQb) => {
nestedQb
.whereNot(field, val)
.orWhereNull(customWhereClause ? _val : _field);
});
} else if (column.uidt === UITypes.Rating) {
// unset rating is considered as NULL
if (val === 0) {
qb = qb.whereNot(field, val).whereNotNull(field);
} else {
qb = qb.whereNot(field, val).orWhereNull(field);
}
} else {
// mysql is case-insensitive for strings, turn to case-sensitive
qb = qb.where((nestedQb) => {
nestedQb.whereRaw('BINARY ?? != ?', [field, val]);
if (column.uidt !== UITypes.Rating) {
nestedQb.orWhereNull(customWhereClause ? _val : _field);
}
});
}
} else {
qb = qb.where((nestedQb) => {
nestedQb
.whereNot(field, val)
.orWhereNull(customWhereClause ? _val : _field);
});
}
break;
case 'like':
if (!val) {
if (column.uidt === UITypes.Attachment) {
qb = qb
.orWhereNull(field)
.orWhere(field, '[]')
.orWhere(field, 'null');
} else {
// val is empty -> all values including empty strings but NULL
qb.where(field, '');
qb.orWhereNotNull(field);
}
} else {
if (column.uidt === UITypes.Formula) {
[field, val] = [val, field];
val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%');
} else {
val = val.startsWith('%') || val.endsWith('%') ? val : `%${val}%`;
val =
val.startsWith('%') || val.endsWith('%') ? val : `%${val}%`;
}
if (qb?.client?.config?.client === 'pg') {
qb = qb.whereRaw('??::text ilike ?', [field, val]);
} else {
qb = qb.where(field, 'like', val);
}
}
break;
case 'nlike':
if (!val) {
if (column.uidt === UITypes.Attachment) {
qb.whereNot(field, '')
.whereNot(field, 'null')
.whereNot(field, '[]');
} else {
// val is empty -> all values including NULL but empty strings
qb.whereNot(field, '');
qb.orWhereNull(field);
}
} else {
if (column.uidt === UITypes.Formula) {
[field, val] = [val, field];
val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%');
} else {
val = val.startsWith('%') || val.endsWith('%') ? val : `%${val}%`;
val =
val.startsWith('%') || val.endsWith('%') ? val : `%${val}%`;
}
qb.where((nestedQb) => {
if (qb?.client?.config?.client === 'pg') {
@ -309,8 +407,16 @@ const parseConditionV2 = async (
} else {
nestedQb.whereNot(field, 'like', val);
}
if (val !== '%%') {
// if value is not empty, empty or null should be included
nestedQb.orWhere(field, '');
nestedQb.orWhereNull(field);
} else {
// if value is empty, then only null is included
nestedQb.orWhereNull(field);
}
});
}
break;
case 'allof':
case 'anyof':
@ -319,8 +425,8 @@ const parseConditionV2 = async (
{
// Condition for filter, without negation
const condition = (builder: Knex.QueryBuilder) => {
const items = val.split(',').map((item) => item.trim());
for (let i = 0; i < items.length; i++) {
const items = val?.split(',').map((item) => item.trim());
for (let i = 0; i < items?.length; i++) {
let sql;
const bindings = [field, `%,${items[i]},%`];
if (qb?.client?.config?.client === 'pg') {
@ -355,11 +461,25 @@ const parseConditionV2 = async (
}
break;
case 'gt':
qb = qb.where(field, customWhereClause ? '<' : '>', val);
const gt_op = customWhereClause ? '<' : '>';
qb = qb.where(field, gt_op, val);
if (column.uidt === UITypes.Rating) {
// unset rating is considered as NULL
if (gt_op === '<' && val > 0) {
qb = qb.orWhereNull(field);
}
}
break;
case 'ge':
case 'gte':
qb = qb.where(field, customWhereClause ? '<=' : '>=', val);
const ge_op = customWhereClause ? '<=' : '>=';
qb = qb.where(field, ge_op, val);
if (column.uidt === UITypes.Rating) {
// unset rating is considered as NULL
if (ge_op === '<=' || (ge_op === '>=' && val === 0)) {
qb = qb.orWhereNull(field);
}
}
break;
case 'in':
qb = qb.whereIn(
@ -375,7 +495,9 @@ const parseConditionV2 = async (
else if (filter.value === 'empty')
qb = qb.where(customWhereClause || field, '');
else if (filter.value === 'notempty')
qb = qb.whereNot(customWhereClause || field, '');
qb = qb
.whereNot(customWhereClause || field, '')
.orWhereNull(field);
else if (filter.value === 'true')
qb = qb.where(customWhereClause || field, true);
else if (filter.value === 'false')
@ -396,13 +518,26 @@ const parseConditionV2 = async (
qb = qb.whereNot(customWhereClause || field, false);
break;
case 'lt':
qb = qb.where(field, customWhereClause ? '>' : '<', val);
const lt_op = customWhereClause ? '>' : '<';
qb = qb.where(field, lt_op, val);
if (column.uidt === UITypes.Rating) {
// unset number is considered as NULL
if (lt_op === '<' && val > 0) {
qb = qb.orWhereNull(field);
}
}
break;
case 'le':
case 'lte':
qb = qb.where(field, customWhereClause ? '>=' : '<=', val);
const le_op = customWhereClause ? '>=' : '<=';
qb = qb.where(field, le_op, val);
if (column.uidt === UITypes.Rating) {
// unset number is considered as NULL
if (le_op === '<=' || (le_op === '>=' && val === 0)) {
qb = qb.orWhereNull(field);
}
}
break;
case 'empty':
if (column.uidt === UITypes.Formula) {
[field, val] = [val, field];
@ -413,7 +548,7 @@ const parseConditionV2 = async (
if (column.uidt === UITypes.Formula) {
[field, val] = [val, field];
}
qb = qb.whereNot(field, val);
qb = qb.whereNot(field, val).orWhereNull(field);
break;
case 'null':
qb = qb.whereNull(customWhereClause || field);
@ -421,6 +556,32 @@ const parseConditionV2 = async (
case 'notnull':
qb = qb.whereNotNull(customWhereClause || field);
break;
case 'blank':
if (column.uidt === UITypes.Attachment) {
qb = qb
.whereNull(customWhereClause || field)
.orWhere(field, '[]')
.orWhere(field, 'null');
} else {
qb = qb.whereNull(customWhereClause || field);
if (!isNumericCol(column.uidt)) {
qb = qb.orWhere(field, '');
}
}
break;
case 'notblank':
if (column.uidt === UITypes.Attachment) {
qb = qb
.whereNotNull(customWhereClause || field)
.whereNot(field, '[]')
.whereNot(field, 'null');
} else {
qb = qb.whereNotNull(customWhereClause || field);
if (!isNumericCol(column.uidt)) {
qb = qb.whereNot(field, '');
}
}
break;
case 'checked':
qb = qb.where(customWhereClause || field, true);
break;
@ -480,6 +641,15 @@ async function generateLookupCondition(
qb.select(`${alias}.${childColumn.column_name}`);
if (filter.comparison_op === 'blank') {
return (qbP: Knex.QueryBuilder) => {
qbP.whereNotIn(childColumn.column_name, qb);
};
} else if (filter.comparison_op === 'notblank') {
return (qbP: Knex.QueryBuilder) => {
qbP.whereIn(childColumn.column_name, qb);
};
} else {
await nestedConditionJoin(
{
...filter,
@ -493,6 +663,7 @@ async function generateLookupCondition(
alias,
aliasCount
);
}
return (qbP: Knex.QueryBuilder) => {
if (filter.comparison_op in negatedMapping)
@ -503,6 +674,15 @@ async function generateLookupCondition(
qb = knex(`${parentModel.table_name} as ${alias}`);
qb.select(`${alias}.${parentColumn.column_name}`);
if (filter.comparison_op === 'blank') {
return (qbP: Knex.QueryBuilder) => {
qbP.whereNotIn(childColumn.column_name, qb);
};
} else if (filter.comparison_op === 'notblank') {
return (qbP: Knex.QueryBuilder) => {
qbP.whereIn(childColumn.column_name, qb);
};
} else {
await nestedConditionJoin(
{
...filter,
@ -516,6 +696,7 @@ async function generateLookupCondition(
alias,
aliasCount
);
}
return (qbP: Knex.QueryBuilder) => {
if (filter.comparison_op in negatedMapping)
@ -537,6 +718,15 @@ async function generateLookupCondition(
`${childAlias}.${parentColumn.column_name}`
);
if (filter.comparison_op === 'blank') {
return (qbP: Knex.QueryBuilder) => {
qbP.whereNotIn(childColumn.column_name, qb);
};
} else if (filter.comparison_op === 'notblank') {
return (qbP: Knex.QueryBuilder) => {
qbP.whereIn(childColumn.column_name, qb);
};
} else {
await nestedConditionJoin(
{
...filter,
@ -550,6 +740,7 @@ async function generateLookupCondition(
childAlias,
aliasCount
);
}
return (qbP: Knex.QueryBuilder) => {
if (filter.comparison_op in negatedMapping)
@ -650,7 +841,7 @@ async function nestedConditionJoin(
new Filter({
...filter,
fk_model_id: childModel.id,
fk_column_id: childModel.primaryValue?.id,
fk_column_id: childModel.displayValue?.id,
}),
knex,
aliasCount,
@ -666,7 +857,7 @@ async function nestedConditionJoin(
new Filter({
...filter,
fk_model_id: parentModel.id,
fk_column_id: parentModel?.primaryValue?.id,
fk_column_id: parentModel?.displayValue?.id,
}),
knex,
aliasCount,
@ -682,7 +873,7 @@ async function nestedConditionJoin(
new Filter({
...filter,
fk_model_id: parentModel.id,
fk_column_id: parentModel.primaryValue?.id,
fk_column_id: parentModel.displayValue?.id,
}),
knex,
aliasCount,

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

@ -269,7 +269,7 @@ async function _formulaQueryBuilder(
);
cn = knex.raw('??.??', [
nestedAlias,
parentModel?.primaryValue?.column_name,
parentModel?.displayValue?.column_name,
]);
}
break;
@ -283,7 +283,7 @@ async function _formulaQueryBuilder(
);
cn = knex.raw('??.??', [
nestedAlias,
childModel?.primaryValue?.column_name,
childModel?.displayValue?.column_name,
]);
}
break;
@ -311,7 +311,7 @@ async function _formulaQueryBuilder(
}
cn = knex.raw('??.??', [
nestedAlias,
parentModel?.primaryValue?.column_name,
parentModel?.displayValue?.column_name,
]);
}
@ -424,7 +424,7 @@ async function _formulaQueryBuilder(
let selectQb;
if (relation.type === 'bt') {
selectQb = knex(parentModel.table_name)
.select(parentModel?.primaryValue?.column_name)
.select(parentModel?.displayValue?.column_name)
.where(
`${parentModel.table_name}.${parentColumn.column_name}`,
knex.raw(`??`, [
@ -447,7 +447,7 @@ async function _formulaQueryBuilder(
getAggregateFn(fn)({
qb,
knex,
cn: childModel?.primaryValue?.column_name,
cn: childModel?.displayValue?.column_name,
})
)
.wrap('(', ')');
@ -499,7 +499,7 @@ async function _formulaQueryBuilder(
getAggregateFn(fn)({
qb,
knex,
cn: parentModel?.primaryValue?.column_name,
cn: parentModel?.displayValue?.column_name,
})
)
.wrap('(', ')');

2
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/getAst.ts

@ -24,7 +24,7 @@ const getAst = async ({
...(model.primaryKeys
? model.primaryKeys.reduce((o, pk) => ({ ...o, [pk.title]: 1 }), {})
: {}),
...(model.primaryValue ? { [model.primaryValue.title]: 1 } : {}),
...(model.displayValue ? { [model.displayValue.title]: 1 } : {}),
};
}

4
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/sortV2.ts

@ -153,7 +153,7 @@ export default async function sortV2(
`${nestedAlias}.${parentColumn.column_name}`,
`${prevAlias}.${childColumn.column_name}`
)
.select(parentModel?.primaryValue?.column_name);
.select(parentModel?.displayValue?.column_name);
}
break;
case UITypes.Formula:
@ -201,7 +201,7 @@ export default async function sortV2(
await parentModel.getColumns();
const selectQb = knex(parentModel.table_name)
.select(parentModel?.primaryValue?.column_name)
.select(parentModel?.displayValue?.column_name)
.where(
`${parentModel.table_name}.${parentColumn.column_name}`,
knex.raw(`??`, [

8
packages/nocodb/src/lib/db/sql-mgr/code/models/xc/BaseModelXcMeta.ts

@ -1,5 +1,5 @@
import BaseRender from '../../BaseRender';
import mapDefaultPrimaryValue from '../../../../../meta/helpers/mapDefaultPrimaryValue';
import mapDefaultDisplayValue from '../../../../../meta/helpers/mapDefaultDisplayValue';
import { UITypes } from 'nocodb-sdk';
abstract class BaseModelXcMeta extends BaseRender {
@ -79,7 +79,7 @@ abstract class BaseModelXcMeta extends BaseRender {
columnsArr.push(columnObj);
}
this.mapDefaultPrimaryValue(columnsArr);
this.mapDefaultDisplayValue(columnsArr);
return columnsArr;
}
@ -127,8 +127,8 @@ abstract class BaseModelXcMeta extends BaseRender {
return virtualColumns;
}
public mapDefaultPrimaryValue(columnsArr: any[]): void {
mapDefaultPrimaryValue(columnsArr);
public mapDefaultDisplayValue(columnsArr: any[]): void {
mapDefaultDisplayValue(columnsArr);
}
}

2
packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaMssql.ts

@ -544,7 +544,7 @@ class ModelXcMetaMssql extends BaseModelXcMeta {
columnsArr.push(columnObj);
}
this.mapDefaultPrimaryValue(columnsArr);
this.mapDefaultDisplayValue(columnsArr);
return columnsArr;
}*/

2
packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaMysql.ts

@ -163,7 +163,7 @@ class ModelXcMetaMysql extends BaseModelXcMeta {
columnsArr.push(columnObj);
}
this.mapDefaultPrimaryValue(columnsArr);
this.mapDefaultDisplayValue(columnsArr);
return columnsArr;
}
*/

2
packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaOracle.ts

@ -163,7 +163,7 @@ class ModelXcMetaOracle extends BaseModelXcMeta {
columnsArr.push(columnObj);
}
this.mapDefaultPrimaryValue(columnsArr);
this.mapDefaultDisplayValue(columnsArr);
return columnsArr;
}
*/

2
packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaPg.ts

@ -953,7 +953,7 @@ class ModelXcMetaPg extends BaseModelXcMeta {
columnsArr.push(columnObj);
}
this.mapDefaultPrimaryValue(columnsArr);
this.mapDefaultDisplayValue(columnsArr);
return columnsArr;
}*/

2
packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaSnowflake.ts

@ -953,7 +953,7 @@ class ModelXcMetaSnowflake extends BaseModelXcMeta {
columnsArr.push(columnObj);
}
this.mapDefaultPrimaryValue(columnsArr);
this.mapDefaultDisplayValue(columnsArr);
return columnsArr;
}*/

2
packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaSqlite.ts

@ -504,7 +504,7 @@ class ModelXcMetaSqlite extends BaseModelXcMeta {
columnsArr.push(columnObj);
}
this.mapDefaultPrimaryValue(columnsArr);
this.mapDefaultDisplayValue(columnsArr);
return columnsArr;
}
*/

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

@ -6,7 +6,6 @@ import Column from '../../models/Column';
import { Tele } from 'nc-help';
import validateParams from '../helpers/validateParams';
import { customAlphabet } from 'nanoid';
import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';
import {
getUniqueColumnAliasName,
@ -19,9 +18,7 @@ import {
isVirtualCol,
LinkToAnotherColumnReqType,
LinkToAnotherRecordType,
LookupColumnReqType,
RelationTypes,
RollupColumnReqType,
substituteColumnAliasWithIdInFormula,
substituteColumnIdWithAliasInFormula,
TableType,
@ -34,15 +31,21 @@ import NcMetaIO from '../NcMetaIO';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { NcError } from '../helpers/catchError';
import getColumnPropsFromUIDT from '../helpers/getColumnPropsFromUIDT';
import mapDefaultPrimaryValue from '../helpers/mapDefaultPrimaryValue';
import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue';
import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import { metaApiMetrics } from '../helpers/apiMetrics';
import FormulaColumn from '../../models/FormulaColumn';
import KanbanView from '../../models/KanbanView';
import { MetaTable } from '../../utils/globals';
import formulaQueryBuilderv2 from '../../db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2';
const randomID = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 10);
import {
createHmAndBtColumn,
generateFkName,
randomID,
validateLookupPayload,
validateRequiredField,
validateRollupPayload,
} from './helpers';
export enum Altered {
NEW_COLUMN = 1,
@ -50,73 +53,6 @@ export enum Altered {
UPDATE_COLUMN = 8,
}
// generate unique foreign key constraint name for foreign key
const generateFkName = (parent: TableType, child: TableType) => {
// generate a unique constraint name by taking first 10 chars of parent and child table name (by replacing all non word chars with _)
// and appending a random string of 15 chars maximum length.
// In database constraint name can be upto 64 chars and here we are generating a name of maximum 40 chars
const constraintName = `fk_${parent.table_name
.replace(/\W+/g, '_')
.slice(0, 10)}_${child.table_name
.replace(/\W+/g, '_')
.slice(0, 10)}_${randomID(15)}`;
return constraintName;
};
async function createHmAndBtColumn(
child: Model,
parent: Model,
childColumn: Column,
type?: RelationTypes,
alias?: string,
fkColName?: string,
virtual = false,
isSystemCol = false
) {
// save bt column
{
const title = getUniqueColumnAliasName(
await child.getColumns(),
type === 'bt' ? alias : `${parent.title}`
);
await Column.insert<LinkToAnotherRecordColumn>({
title,
fk_model_id: child.id,
// ref_db_alias
uidt: UITypes.LinkToAnotherRecord,
type: 'bt',
// db_type:
fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id,
fk_related_model_id: parent.id,
virtual,
system: isSystemCol,
fk_index_name: fkColName,
});
}
// save hm column
{
const title = getUniqueColumnAliasName(
await parent.getColumns(),
type === 'hm' ? alias : `${child.title} List`
);
await Column.insert({
title,
fk_model_id: parent.id,
uidt: UITypes.LinkToAnotherRecord,
type: 'hm',
fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id,
fk_related_model_id: child.id,
virtual,
system: isSystemCol,
fk_index_name: fkColName,
});
}
}
export async function columnGet(req: Request, res: Response) {
res.json(await Column.get({ colId: req.params.columnId }));
}
@ -153,49 +89,7 @@ export async function columnAdd(
switch (colBody.uidt) {
case UITypes.Rollup:
{
validateParams(
[
'title',
'fk_relation_column_id',
'fk_rollup_column_id',
'rollup_function',
],
req.body
);
const relation = await (
await Column.get({
colId: (req.body as RollupColumnReqType).fk_relation_column_id,
})
).getColOptions<LinkToAnotherRecordType>();
if (!relation) {
throw new Error('Relation column not found');
}
let relatedColumn: Column;
switch (relation.type) {
case 'hm':
relatedColumn = await Column.get({
colId: relation.fk_child_column_id,
});
break;
case 'mm':
case 'bt':
relatedColumn = await Column.get({
colId: relation.fk_parent_column_id,
});
break;
}
const relatedTable = await relatedColumn.getModel();
if (
!(await relatedTable.getColumns()).find(
(c) =>
c.id === (req.body as RollupColumnReqType).fk_rollup_column_id
)
)
throw new Error('Rollup column not found in related table');
await validateRollupPayload(req.body);
await Column.insert({
...colBody,
@ -205,44 +99,7 @@ export async function columnAdd(
break;
case UITypes.Lookup:
{
validateParams(
['title', 'fk_relation_column_id', 'fk_lookup_column_id'],
req.body
);
const relation = await (
await Column.get({
colId: (req.body as LookupColumnReqType).fk_relation_column_id,
})
).getColOptions<LinkToAnotherRecordType>();
if (!relation) {
throw new Error('Relation column not found');
}
let relatedColumn: Column;
switch (relation.type) {
case 'hm':
relatedColumn = await Column.get({
colId: relation.fk_child_column_id,
});
break;
case 'mm':
case 'bt':
relatedColumn = await Column.get({
colId: relation.fk_parent_column_id,
});
break;
}
const relatedTable = await relatedColumn.getModel();
if (
!(await relatedTable.getColumns()).find(
(c) =>
c.id === (req.body as LookupColumnReqType).fk_lookup_column_id
)
)
throw new Error('Lookup column not found in related table');
await validateLookupPayload(req.body);
await Column.insert({
...colBody,
@ -252,7 +109,6 @@ export async function columnAdd(
break;
case UITypes.LinkToAnotherRecord:
// case UITypes.ForeignKey:
{
validateParams(['parentId', 'childId', 'type'], req.body);
@ -753,6 +609,29 @@ export async function columnSetAsPrimary(req: Request, res: Response) {
res.json(await Model.updatePrimaryColumn(column.fk_model_id, column.id));
}
async function updateRollupOrLookup(colBody: any, column: Column<any>) {
if (
UITypes.Lookup === column.uidt &&
validateRequiredField(colBody, [
'fk_lookup_column_id',
'fk_relation_column_id',
])
) {
await validateLookupPayload(colBody, column.id);
await Column.update(column.id, colBody);
} else if (
UITypes.Rollup === column.uidt &&
validateRequiredField(colBody, [
'fk_relation_column_id',
'fk_rollup_column_id',
'rollup_function',
])
) {
await validateRollupPayload(colBody);
await Column.update(column.id, colBody);
}
}
export async function columnUpdate(req: Request, res: Response<TableType>) {
const column = await Column.get({ colId: req.params.columnId });
@ -824,6 +703,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
title: colBody.title,
});
}
await updateRollupOrLookup(colBody, column);
} else {
NcError.notImplemented(
`Updating ${colBody.uidt} => ${colBody.uidt} is not implemented`
@ -1712,11 +1592,11 @@ export async function columnDelete(req: Request, res: Response<TableType>) {
await table.getColumns();
const primaryValueColumn = mapDefaultPrimaryValue(table.columns);
if (primaryValueColumn) {
const displayValueColumn = mapDefaultDisplayValue(table.columns);
if (displayValueColumn) {
await Model.updatePrimaryColumn(
primaryValueColumn.fk_model_id,
primaryValueColumn.id
displayValueColumn.fk_model_id,
displayValueColumn.id
);
}

2
packages/nocodb/src/lib/meta/api/dataApis/helpers.ts

@ -225,7 +225,7 @@ export async function serializeCellValue({
await relatedModel.getColumns();
return [...(Array.isArray(value) ? value : [value])]
.map((v) => {
return v[relatedModel.primaryValue?.title];
return v[relatedModel.displayValue?.title];
})
.join(', ');
}

209
packages/nocodb/src/lib/meta/api/helpers/columnHelpers.ts

@ -0,0 +1,209 @@
import { customAlphabet } from 'nanoid';
import {
ColumnReqType,
LinkToAnotherRecordType,
LookupColumnReqType,
RelationTypes,
RollupColumnReqType,
TableType,
UITypes,
} from 'nocodb-sdk';
import Column from '../../../models/Column';
import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn';
import LookupColumn from '../../../models/LookupColumn';
import Model from '../../../models/Model';
import { getUniqueColumnAliasName } from '../../helpers/getUniqueName';
import validateParams from '../../helpers/validateParams';
export const randomID = customAlphabet(
'1234567890abcdefghijklmnopqrstuvwxyz_',
10
);
export async function createHmAndBtColumn(
child: Model,
parent: Model,
childColumn: Column,
type?: RelationTypes,
alias?: string,
fkColName?: string,
virtual = false,
isSystemCol = false
) {
// save bt column
{
const title = getUniqueColumnAliasName(
await child.getColumns(),
type === 'bt' ? alias : `${parent.title}`
);
await Column.insert<LinkToAnotherRecordColumn>({
title,
fk_model_id: child.id,
// ref_db_alias
uidt: UITypes.LinkToAnotherRecord,
type: 'bt',
// db_type:
fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id,
fk_related_model_id: parent.id,
virtual,
system: isSystemCol,
fk_col_name: fkColName,
fk_index_name: fkColName,
});
}
// save hm column
{
const title = getUniqueColumnAliasName(
await parent.getColumns(),
type === 'hm' ? alias : `${child.title} List`
);
await Column.insert({
title,
fk_model_id: parent.id,
uidt: UITypes.LinkToAnotherRecord,
type: 'hm',
fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id,
fk_related_model_id: child.id,
virtual,
system: isSystemCol,
fk_col_name: fkColName,
fk_index_name: fkColName,
});
}
}
export async function validateRollupPayload(
payload: ColumnReqType & { uidt: UITypes }
) {
validateParams(
[
'title',
'fk_relation_column_id',
'fk_rollup_column_id',
'rollup_function',
],
payload
);
const relation = await (
await Column.get({
colId: (payload as RollupColumnReqType).fk_relation_column_id,
})
).getColOptions<LinkToAnotherRecordType>();
if (!relation) {
throw new Error('Relation column not found');
}
let relatedColumn: Column;
switch (relation.type) {
case 'hm':
relatedColumn = await Column.get({
colId: relation.fk_child_column_id,
});
break;
case 'mm':
case 'bt':
relatedColumn = await Column.get({
colId: relation.fk_parent_column_id,
});
break;
}
const relatedTable = await relatedColumn.getModel();
if (
!(await relatedTable.getColumns()).find(
(c) => c.id === (payload as RollupColumnReqType).fk_rollup_column_id
)
)
throw new Error('Rollup column not found in related table');
}
export async function validateLookupPayload(
payload: ColumnReqType & { uidt: UITypes },
columnId?: string
) {
validateParams(
['title', 'fk_relation_column_id', 'fk_lookup_column_id'],
payload
);
// check for circular reference
if (columnId) {
let lkCol: LookupColumn | LookupColumnReqType =
payload as LookupColumnReqType;
while (lkCol) {
// check if lookup column is same as column itself
if (columnId === lkCol.fk_lookup_column_id)
throw new Error('Circular lookup reference not allowed');
lkCol = await Column.get({ colId: lkCol.fk_lookup_column_id }).then(
(c: Column) => {
if (c.uidt === 'Lookup') {
return c.getColOptions<LookupColumn>();
}
return null;
}
);
}
}
const relation = await (
await Column.get({
colId: (payload as LookupColumnReqType).fk_relation_column_id,
})
).getColOptions<LinkToAnotherRecordType>();
if (!relation) {
throw new Error('Relation column not found');
}
let relatedColumn: Column;
switch (relation.type) {
case 'hm':
relatedColumn = await Column.get({
colId: relation.fk_child_column_id,
});
break;
case 'mm':
case 'bt':
relatedColumn = await Column.get({
colId: relation.fk_parent_column_id,
});
break;
}
const relatedTable = await relatedColumn.getModel();
if (
!(await relatedTable.getColumns()).find(
(c) => c.id === (payload as LookupColumnReqType).fk_lookup_column_id
)
)
throw new Error('Lookup column not found in related table');
}
export const validateRequiredField = (
payload: Record<string, any>,
requiredProps: string[]
) => {
return requiredProps.every(
(prop) =>
prop in payload && payload[prop] !== undefined && payload[prop] !== null
);
};
// generate unique foreign key constraint name for foreign key
export const generateFkName = (parent: TableType, child: TableType) => {
// generate a unique constraint name by taking first 10 chars of parent and child table name (by replacing all non word chars with _)
// and appending a random string of 15 chars maximum length.
// In database constraint name can be upto 64 chars and here we are generating a name of maximum 40 chars
const constraintName = `fk_${parent.table_name
.replace(/\W+/g, '_')
.slice(0, 10)}_${child.table_name
.replace(/\W+/g, '_')
.slice(0, 10)}_${randomID(15)}`;
return constraintName;
};

1
packages/nocodb/src/lib/meta/api/helpers/index.ts

@ -1,3 +1,4 @@
import { populateMeta } from './populateMeta';
export * from './columnHelpers';
export { populateMeta };

4
packages/nocodb/src/lib/meta/api/helpers/populateMeta.ts

@ -10,7 +10,7 @@ import getTableNameAlias, {
} from '../../helpers/getTableName';
import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn';
import getColumnUiType from '../../helpers/getColumnUiType';
import mapDefaultPrimaryValue from '../../helpers/mapDefaultPrimaryValue';
import mapDefaultDisplayValue from '../../helpers/mapDefaultDisplayValue';
import { extractAndGenerateManyToManyRelations } from '../metaDiffApis';
import { ModelTypes, UITypes, ViewTypes } from 'nocodb-sdk';
import { IGNORE_TABLES } from '../../../utils/common/BaseApiBuilder';
@ -90,7 +90,7 @@ export async function populateMeta(base: Base, project: Project): Promise<any> {
? []
: tableRelations.filter((r) => r.tn === table.tn);
mapDefaultPrimaryValue(columns);
mapDefaultDisplayValue(columns);
// add vitual columns
const virtualColumns = [

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

@ -14,7 +14,7 @@ import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';
import { getUniqueColumnAliasName } from '../helpers/getUniqueName';
import NcHelp from '../../utils/NcHelp';
import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName';
import mapDefaultPrimaryValue from '../helpers/mapDefaultPrimaryValue';
import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue';
import getColumnUiType from '../helpers/getColumnUiType';
import { metaApiMetrics } from '../helpers/apiMetrics';
@ -602,7 +602,7 @@ export async function metaDiffSync(req, res) {
await sqlClient.columnList({ tn: table_name })
)?.data?.list?.map((c) => ({ ...c, column_name: c.cn }));
mapDefaultPrimaryValue(columns);
mapDefaultDisplayValue(columns);
const model = await Model.insert(project.id, base.id, {
table_name: table_name,
@ -630,7 +630,7 @@ export async function metaDiffSync(req, res) {
await sqlClient.columnList({ tn: table_name })
)?.data?.list?.map((c) => ({ ...c, column_name: c.cn }));
mapDefaultPrimaryValue(columns);
mapDefaultDisplayValue(columns);
const model = await Model.insert(project.id, base.id, {
table_name: table_name,
@ -798,7 +798,7 @@ export async function baseMetaDiffSync(req, res) {
await sqlClient.columnList({ tn: table_name })
)?.data?.list?.map((c) => ({ ...c, column_name: c.cn }));
mapDefaultPrimaryValue(columns);
mapDefaultDisplayValue(columns);
const model = await Model.insert(project.id, base.id, {
table_name: table_name,
@ -826,7 +826,7 @@ export async function baseMetaDiffSync(req, res) {
await sqlClient.columnList({ tn: table_name })
)?.data?.list?.map((c) => ({ ...c, column_name: c.cn }));
mapDefaultPrimaryValue(columns);
mapDefaultDisplayValue(columns);
const model = await Model.insert(project.id, base.id, {
table_name: table_name,

10
packages/nocodb/src/lib/meta/api/projectApis.ts

@ -19,6 +19,7 @@ import { extractPropsAndSanitize } from '../helpers/extractProps';
import NcConfigFactory from '../../utils/NcConfigFactory';
import { promisify } from 'util';
import { populateMeta } from './helpers';
import Filter from '../../models/Filter';
const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 4);
@ -238,6 +239,10 @@ export async function projectCost(req, res) {
res.json({ cost });
}
export async function hasEmptyOrNullFilters(req, res) {
res.json(await Filter.hasEmptyOrNullFilters(req.params.projectId));
}
export default (router) => {
router.get(
'/api/v1/db/meta/projects/:projectId/info',
@ -274,4 +279,9 @@ export default (router) => {
metaApiMetrics,
ncMetaAclMw(projectList, 'projectList')
);
router.get(
'/api/v1/db/meta/projects/:projectId/has-empty-or-null-filters',
metaApiMetrics,
ncMetaAclMw(hasEmptyOrNullFilters, 'hasEmptyOrNullFilters')
);
};

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save