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. 50
      packages/nc-gui/components/dashboard/settings/Misc.vue
  11. 250
      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. 72
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  18. 8
      packages/nc-gui/components/smartsheet/header/Menu.vue
  19. 110
      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. 114
      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. 353
      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: jobs:
unit-tests: unit-tests:
runs-on: ubuntu-20.04 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 }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
steps: steps:
- name: Setup Node - name: Setup Node

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

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

4
.github/workflows/release-nocodb.yml

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

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

@ -322,3 +322,14 @@ a {
white-space: pre; white-space: pre;
user-select: auto; 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'] MdiMapMarker: typeof import('~icons/mdi/map-marker')['default']
MdiMapMarkerAlert: typeof import('~icons/mdi/map-marker-alert')['default'] MdiMapMarkerAlert: typeof import('~icons/mdi/map-marker-alert')['default']
MdiMenu: typeof import('~icons/mdi/menu')['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'] MdiMenuDown: typeof import('~icons/mdi/menu-down')['default']
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default'] MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMinusCircleOutline: typeof import('~icons/mdi/minus-circle-outline')['default'] MdiMinusCircleOutline: typeof import('~icons/mdi/minus-circle-outline')['default']

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

@ -23,10 +23,14 @@ onMounted(() => {
<template> <template>
<div ref="wrapper"> <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 <text-clamp
:key="`clamp-${key}-${props.value?.toString().length || 0}`" :key="`clamp-${key}-${props.value?.toString().length || 0}`"
class="w-full h-full break-all" class="w-full h-full break-all"
:text="`${props.value || ''}`" :text="`${props.value || ' '}`"
:max-lines="props.lines" :max-lines="props.lines"
/> />
</div> </div>

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

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

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

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

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

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

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

@ -1,24 +1,64 @@
<script setup lang="ts"> <script setup lang="ts">
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface'
import { useGlobal, useProject, watch } from '#imports' import { useGlobal, useProject, watch } from '#imports'
const { includeM2M, showNull } = useGlobal() const { includeM2M, showNull } = useGlobal()
const { loadTables } = useProject()
const { project, updateProject, projectMeta, loadTables, hasEmptyOrNullFilters } = useProject()
watch(includeM2M, async () => await loadTables()) 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> </script>
<template> <template>
<div class="flex flex-row w-full"> <div class="flex flex-row w-full">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class="flex flex-row items-center w-full mb-4 gap-2"> <div class="flex flex-row items-center w-full mb-4 gap-2">
<!-- Show M2M Tables --> <!-- Show M2M Tables -->
<a-checkbox v-model:checked="includeM2M" v-e="['c:themes:show-m2m-tables']" class="nc-settings-meta-misc"> <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> </a-checkbox>
</div> </div>
<div class="flex flex-row items-center w-full mb-4 gap-2"> <div class="flex flex-row items-center w-full mb-4 gap-2">
<!-- Show NULL --> <!-- 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> </div>
</div> </div>

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

@ -51,6 +51,7 @@ const {
galleryData, galleryData,
changePage, changePage,
addEmptyRow, addEmptyRow,
deleteRow,
navigateToSiblingRow, navigateToSiblingRow,
} = useViewData(meta, view) } = useViewData(meta, view)
@ -85,6 +86,30 @@ const isRowEmpty = (record: any, col: any) => {
return Array.isArray(val) && val.length === 0 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[] => { const attachments = (record: any): Attachment[] => {
try { try {
if (coverImageColumn?.title && record.row[coverImageColumn.title]) { if (coverImageColumn?.title && record.row[coverImageColumn.title]) {
@ -175,113 +200,138 @@ watch(view, async (nextView) => {
</script> </script>
<template> <template>
<div class="flex flex-col h-full w-full overflow-auto nc-gallery" data-testid="nc-gallery-wrapper"> <a-dropdown
<div class="nc-gallery-container grid gap-2 my-4 px-3"> v-model:visible="contextMenu"
<div v-for="record in data" :key="`record-${record.row.id}`"> :trigger="isSqlView ? [] : ['contextmenu']"
<LazySmartsheetRow :row="record"> overlay-class-name="nc-dropdown-grid-context-menu"
<a-card >
hoverable <template #overlay>
class="!rounded-lg h-full overflow-hidden break-all max-w-[450px]" <a-menu class="shadow !rounded !py-0" @click="contextMenu = false">
:data-testid="`nc-gallery-card-${record.row.id}`" <a-menu-item v-if="contextMenuTarget" @click="deleteRow(contextMenuTarget.row)">
@click="expandFormClick($event, record)" <div v-e="['a:row:delete']" class="nc-project-menu-item">
> <!-- Delete Row -->
<template v-if="galleryData?.fk_cover_image_col_id" #cover> {{ $t('activity.deleteRow') }}
<a-carousel v-if="!reloadAttachments && attachments(record).length" autoplay class="gallery-carousel" arrows> </div>
<template #customPaging> </a-menu-item>
<a>
<div class="pt-[12px]"> <a-menu-item v-if="contextMenuTarget" @click="openNewRecordFormHook.trigger()">
<div></div> <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, 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>
<template #customPaging>
<a>
<div class="pt-[12px]">
<div></div>
</div>
</a>
</template>
<template #prevArrow>
<div style="z-index: 1"></div>
</template>
<template #nextArrow>
<div style="z-index: 1"></div>
</template>
<template v-for="(attachment, index) in attachments(record)">
<LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="`carousel-${record.row.id}-${index}`"
class="h-52 object-contain"
:srcs="getPossibleAttachmentSrc(attachment)"
/>
</template>
</a-carousel>
<MdiFileImageBox v-else class="w-full h-48 my-4 text-cool-gray-200" />
</template>
<div v-for="col in fieldsWithoutCover" :key="`record-${record.row.id}-${col.id}`">
<div
v-if="!isRowEmpty(record, col) || isLTAR(col.uidt)"
class="flex flex-col space-y-1 px-4 mb-6 bg-gray-50 rounded-lg w-full"
>
<div class="flex flex-row w-full justify-start border-b-1 border-gray-100 py-2.5">
<div class="w-full text-gray-600">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="true" />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
</div> </div>
</a>
</template>
<template #prevArrow>
<div style="z-index: 1"></div>
</template>
<template #nextArrow>
<div style="z-index: 1"></div>
</template>
<template v-for="(attachment, index) in attachments(record)">
<LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="`carousel-${record.row.id}-${index}`"
class="h-52 object-contain"
:srcs="getPossibleAttachmentSrc(attachment)"
/>
</template>
</a-carousel>
<MdiFileImageBox v-else class="w-full h-48 my-4 text-cool-gray-200" />
</template>
<div v-for="col in fieldsWithoutCover" :key="`record-${record.row.id}-${col.id}`">
<div
v-if="!isRowEmpty(record, col) || isLTAR(col.uidt)"
class="flex flex-col space-y-1 px-4 mb-6 bg-gray-50 rounded-lg w-full"
>
<div class="flex flex-row w-full justify-start border-b-1 border-gray-100 py-2.5">
<div class="w-full text-gray-600">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="true" />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
</div> </div>
</div>
<div class="flex flex-row w-full pb-3 pt-2 pl-2 items-center justify-start"> <div class="flex flex-row w-full pb-3 pt-2 pl-2 items-center justify-start">
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
v-if="isVirtualCol(col)" v-if="isVirtualCol(col)"
v-model="record.row[col.title]" v-model="record.row[col.title]"
:column="col" :column="col"
:row="record" :row="record"
/> />
<LazySmartsheetCell <LazySmartsheetCell
v-else v-else
v-model="record.row[col.title]" v-model="record.row[col.title]"
:column="col" :column="col"
:edit-enabled="false" :edit-enabled="false"
:read-only="true" :read-only="true"
/> />
</div>
</div> </div>
</div> </div>
</div> </a-card>
</a-card> </LazySmartsheetRow>
</LazySmartsheetRow> </div>
</div> </div>
</div>
<div class="flex-1" /> <div class="flex-1" />
<LazySmartsheetPagination /> <LazySmartsheetPagination />
</div>
<Suspense> </a-dropdown>
<LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg" <Suspense>
v-model="expandedFormDlg" <LazySmartsheetExpandedForm
:row="expandedFormRow" v-if="expandedFormRow && expandedFormDlg"
:state="expandedFormRowState" v-model="expandedFormDlg"
:meta="meta" :row="expandedFormRow"
:view="view" :state="expandedFormRowState"
/> :meta="meta"
</Suspense> :view="view"
/>
<Suspense> </Suspense>
<LazySmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg" <Suspense>
:key="route.query.rowId" <LazySmartsheetExpandedForm
v-model="expandedFormOnRowIdDlg" v-if="expandedFormOnRowIdDlg"
:row="{ row: {}, oldRow: {}, rowMeta: {} }" :key="route.query.rowId"
:meta="meta" v-model="expandedFormOnRowIdDlg"
:row-id="route.query.rowId" :row="{ row: {}, oldRow: {}, rowMeta: {} }"
:view="view" :meta="meta"
show-next-prev-icons :row-id="route.query.rowId"
@next="navigateToSiblingRow(NavigateDir.NEXT)" :view="view"
@prev="navigateToSiblingRow(NavigateDir.PREV)" show-next-prev-icons
/> @next="navigateToSiblingRow(NavigateDir.NEXT)"
</Suspense> @prev="navigateToSiblingRow(NavigateDir.PREV)"
</div> />
</Suspense>
</template> </template>
<style scoped> <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="nc-row-no text-xs text-gray-500"
:class="{ toggle: !readOnly, hidden: row.rowMeta.selected }" :class="{ toggle: !readOnly, hidden: row.rowMeta.selected }"
> >
{{ rowIndex + 1 }} {{ ((paginationData.page ?? 1) - 1) * 25 + rowIndex + 1 }}
</div> </div>
<div <div
v-if="!readOnly" v-if="!readOnly"
@ -1056,7 +1056,7 @@ const closeAddColumnDropdown = () => {
position: sticky !important; position: sticky !important;
left: 80px; left: 80px;
z-index: 5; z-index: 5;
@apply border-r-2 border-r-gray-300; @apply border-r-1 border-r-gray-300;
} }
tbody td:nth-child(2) { tbody td:nth-child(2) {
@ -1064,7 +1064,7 @@ const closeAddColumnDropdown = () => {
left: 80px; left: 80px;
z-index: 4; z-index: 4;
background: white; 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" /> <LazySmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<LazySmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" 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" /> <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" /> <LazySmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" v-model:value="formState" />
<LazySmartsheetColumnDateTimeOptions v-if="formState.uidt === UITypes.DateTime" 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 <LazySmartsheetColumnLinkedToAnotherRecordOptions
v-if="!isEdit && formState.uidt === UITypes.LinkToAnotherRecord" v-if="!isEdit && formState.uidt === UITypes.LinkToAnotherRecord"
v-model:value="formState" v-model:value="formState"

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

@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isSystemColumn } from 'nocodb-sdk' import { UITypes, isSystemColumn } from 'nocodb-sdk'
import { getRelationName } from './utils' import { getRelationName } from './utils'
@ -14,7 +15,7 @@ const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj, ref())) const meta = $(inject(MetaInj, ref()))
const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit } = useColumnCreateStoreOrThrow()
const { tables } = $(useProject()) const { tables } = $(useProject())
@ -49,8 +50,20 @@ const columns = $computed<ColumnType[]>(() => {
if (!selectedTable?.id) { if (!selectedTable?.id) {
return [] 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> </script>
<template> <template>
@ -60,7 +73,7 @@ const columns = $computed<ColumnType[]>(() => {
<a-select <a-select
v-model:value="vModel.fk_relation_column_id" v-model:value="vModel.fk_relation_column_id"
dropdown-class-name="!w-64 nc-dropdown-relation-table" 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"> <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"> <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"> <script setup lang="ts">
import { onMounted } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { getRelationName } from './utils' import { getRelationName } from './utils'
@ -14,7 +15,7 @@ const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj, ref())) const meta = $(inject(MetaInj, ref()))
const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit } = useColumnCreateStoreOrThrow()
const { tables } = $(useProject()) const { tables } = $(useProject())
@ -69,8 +70,23 @@ const columns = $computed(() => {
return [] 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> </script>
<template> <template>
@ -80,7 +96,7 @@ const columns = $computed(() => {
<a-select <a-select
v-model:value="vModel.fk_relation_column_id" v-model:value="vModel.fk_relation_column_id"
dropdown-class-name="!w-64 nc-dropdown-relation-table" 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"> <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"> <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 props = defineProps<{ view?: ViewType }>()
const emit = defineEmits(['cancel']) const emit = defineEmits(['cancel', 'duplicateRow'])
const route = useRoute() const route = useRoute()
const { meta, isSqlView } = useSmartsheetStoreOrThrow() 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() 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> </script>
<template> <template>
@ -83,7 +99,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
{{ meta.title }} {{ meta.title }}
</template> </template>
<template v-if="primaryValue">: {{ primaryValue }}</template> <template v-if="displayValue">: {{ displayValue }}</template>
</h5> </h5>
<div class="flex-1" /> <div class="flex-1" />
@ -92,7 +108,11 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<template #title> <template #title>
<div class="text-center w-full">{{ $t('general.reload') }}</div> <div class="text-center w-full">{{ $t('general.reload') }}</div>
</template> </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>
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> <template #title>
@ -101,7 +121,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
</template> </template>
<mdi-link <mdi-link
v-if="!isNew" 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" @click="copyRecordUrl"
/> />
</a-tooltip> </a-tooltip>
@ -114,20 +134,40 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<MdiCommentTextOutline <MdiCommentTextOutline
v-if="isUIAllowed('rowComments') && !isNew" v-if="isUIAllowed('rowComments') && !isNew"
v-e="['c:row-expand:comment-toggle']" 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" @click="commentsDrawer = !commentsDrawer"
/> />
</a-tooltip> </a-tooltip>
<a-button class="!text mx-1 nc-expand-form-close-btn" @click="emit('cancel')"> <a-tooltip v-if="!isSqlView" placement="bottom">
<div class="flex items-center"> <!-- Duplicate row -->
<MdiCloseCircleOutline class="mr-1" /> <template #title>
<!-- Close --> <div class="text-center w-full">{{ $t('activity.duplicateRow') }}</div>
{{ $t('general.close') }} </template>
</div> <MdiContentCopy
</a-button> 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"> <a-dropdown-button class="nc-expand-form-save-btn" type="primary" :disabled="!isUIAllowed('tableRowUpdate')" @click="save">
<template #icon><MdiMenuDown /></template>
<template #overlay> <template #overlay>
<a-menu class="nc-expand-form-save-dropdown-menu"> <a-menu class="nc-expand-form-save-dropdown-menu">
<a-menu-item key="0" class="!py-2 flex gap-2" @click="saveRowAndStay = 0"> <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') }} {{ $t('activity.saveAndStay') }}
</div> </div>
</a-dropdown-button> </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> </div>
</template> </template>
<style scoped>
:deep(svg) {
@apply outline-none;
}
</style>

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

@ -23,10 +23,6 @@ import {
} from '#imports' } from '#imports'
import type { Row } from '~/lib' import type { Row } from '~/lib'
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev'])
interface Props { interface Props {
modelValue?: boolean modelValue?: boolean
row: Row row: Row
@ -39,6 +35,12 @@ interface Props {
showNextPrevIcons?: boolean showNextPrevIcons?: boolean
} }
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev'])
const { t } = useI18n()
const row = ref(props.row) const row = ref(props.row)
const state = toRef(props, 'state') const state = toRef(props, 'state')
@ -60,6 +62,8 @@ provide(MetaInj, meta)
const { commentsDrawer, changedColumns, state: rowState, isNew, loadRow } = useProvideExpandedFormStore(meta, row) const { commentsDrawer, changedColumns, state: rowState, isNew, loadRow } = useProvideExpandedFormStore(meta, row)
const duplicatingRowInProgress = ref(false)
if (props.loadRow) { if (props.loadRow) {
await loadRow() await loadRow()
} }
@ -101,6 +105,23 @@ const onClose = () => {
isExpanded.value = false 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()) const reloadParentRowHook = inject(ReloadRowDataHookInj, createEventHook())
// override reload trigger and use it to reload grid and the form itself // override reload trigger and use it to reload grid and the form itself
@ -139,35 +160,38 @@ export default {
<a-drawer <a-drawer
v-model:visible="isExpanded" v-model:visible="isExpanded"
:footer="null" :footer="null"
width="min(90vw,800px)" :width="commentsDrawer ? 'min(90vw,900px)' : 'min(90vw,700px)'"
:body-style="{ 'padding': 0, 'display': 'flex', 'flex-direction': 'column' }" :body-style="{ 'padding': 0, 'display': 'flex', 'flex-direction': 'column' }"
:closable="false" :closable="false"
class="nc-drawer-expanded-form" class="nc-drawer-expanded-form"
:class="{ active: isExpanded }" :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">
<template v-if="props.showNextPrevIcons">
<a-tooltip placement="bottom">
<template #title>
{{ $t('labels.nextRow') }}
</template>
<MdiChevronRight class="cursor-pointer nc-next-arrow" @click="$emit('next')" />
</a-tooltip>
<a-tooltip placement="bottom">
<template #title>
{{ $t('labels.prevRow') }}
</template>
<MdiChevronLeft class="cursor-pointer nc-prev-arrow" @click="$emit('prev')" />
</a-tooltip>
</template>
<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 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="flex-1 overflow-auto scrollbar-thin-dull nc-form-fields-container relative">
<template v-if="props.showNextPrevIcons">
<a-tooltip placement="bottom">
<template #title>
{{ $t('labels.nextRow') }}
</template>
<MdiChevronRight class="cursor-pointer nc-next-arrow" @click="$emit('next')" />
</a-tooltip>
<a-tooltip placement="bottom">
<template #title>
{{ $t('labels.prevRow') }}
</template>
<MdiChevronLeft class="cursor-pointer nc-prev-arrow" @click="$emit('prev')" />
</a-tooltip>
</template>
<div class="w-[500px] mx-auto"> <div class="w-[500px] mx-auto">
<div v-if="duplicatingRowInProgress" class="flex items-center justify-center h-[100px]">
<a-spin size="large" />
</div>
<div <div
v-for="(col, i) of fields" v-for="(col, i) of fields"
v-else
v-show="!isVirtualCol(col) || !isNew || col.uidt === UITypes.LinkToAnotherRecord" v-show="!isVirtualCol(col) || !isNew || col.uidt === UITypes.LinkToAnotherRecord"
:key="col.title" :key="col.title"
class="mt-2 py-2" class="mt-2 py-2"
@ -234,9 +258,11 @@ export default {
.nc-next-arrow { .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; @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 { .nc-prev-arrow {
@apply left-4 top-4; @apply left-4 top-4;
} }
.nc-next-arrow { .nc-next-arrow {
@apply right-4 top-4; @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 { try {
await $api.dbTableColumn.primaryColumnSet(column?.value?.id as string) await $api.dbTableColumn.primaryColumnSet(column?.value?.id as string)
@ -280,13 +280,13 @@ const hideField = async () => {
</a-menu-item> </a-menu-item>
<a-divider class="!my-0" /> <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"> <div class="nc-column-set-primary nc-header-menu-item">
<MdiStar class="text-primary" /> <MdiStar class="text-primary" />
<!-- todo : tooltip --> <!-- todo : tooltip -->
<!-- Set as Primary value --> <!-- Set as Display value -->
{{ $t('activity.setPrimary') }} {{ $t('activity.setDisplay') }}
</div> </div>
</a-menu-item> </a-menu-item>

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

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FilterType } from 'nocodb-sdk' import type { ColumnType, FilterType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
MetaInj, MetaInj,
@ -42,7 +43,18 @@ const reloadDataHook = inject(ReloadViewDataHookInj)!
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { nestedFilters } = useSmartsheetStoreOrThrow() 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, activeView,
parentId, parentId,
computed(() => autoSave), computed(() => autoSave),
@ -53,16 +65,37 @@ const { filters, nonDeletedFilters, deleteFilter, saveOrUpdate, loadFilters, add
const localNestedFilters = ref() 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 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) saveOrUpdate(filter, i)
filterPrevComparisonOp.value[filter.id] = filter.comparison_op
$e('a:filter:update', { $e('a:filter:update', {
logical: filter.logical_op, logical: filter.logical_op,
comparison: filter.comparison_op, comparison: filter.comparison_op,
}) })
} }
const columns = computed(() => meta.value?.columns)
const types = computed(() => { const types = computed(() => {
if (!meta.value?.columns?.length) { if (!meta.value?.columns?.length) {
return {} return {}
@ -76,7 +109,7 @@ const types = computed(() => {
watch( watch(
() => activeView.value?.id, () => activeView.value?.id,
(n, o) => { (n: string, o: string) => {
// if nested no need to reload since it will get reloaded from parent // if nested no need to reload since it will get reloaded from parent
if (!nested && n !== o && (hookId || !webHook)) loadFilters(hookId as string) if (!nested && n !== o && (hookId || !webHook)) loadFilters(hookId as string)
}, },
@ -86,7 +119,7 @@ loadFilters(hookId as string)
watch( watch(
() => nonDeletedFilters.value.length, () => nonDeletedFilters.value.length,
(length) => { (length: number) => {
emit('update:filtersLength', length ?? 0) emit('update:filtersLength', length ?? 0)
}, },
) )
@ -103,19 +136,23 @@ const applyChanges = async (hookId?: string, _nested = false) => {
} }
} }
const isComparisonOpAllowed = (filter: FilterType, compOp: typeof comparisonOpList[number]) => { const selectFilterField = (filter: Filter, index: number) => {
// show current selected value in list even if not allowed // when we change the field,
if (filter.comparison_op === compOp.value) return true // 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 const updateFilterValue = (value: string, filter: Filter, index: number) => {
if (compOp.includedTypes) { filter.value = value
return filter.fk_column_id && compOp.includedTypes.includes(types.value[filter.fk_column_id]) saveOrUpdateDebounced(filter, index)
}
// 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
} }
defineExpose({ defineExpose({
@ -127,7 +164,7 @@ defineExpose({
<template> <template>
<div <div
class="p-4 menu-filter-dropdown bg-gray-50 !border mt-4" 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> <div v-if="filters && filters.length" class="nc-filter-grid mb-2" @click.stop>
<template v-for="(filter, i) in filters" :key="i"> <template v-for="(filter, i) in filters" :key="i">
@ -189,14 +226,13 @@ defineExpose({
hide-details hide-details
:disabled="filter.readOnly" :disabled="filter.readOnly"
dropdown-class-name="nc-dropdown-filter-logical-op" dropdown-class-name="nc-dropdown-filter-logical-op"
@click.stop
@change="filterUpdateCondition(filter, i)" @change="filterUpdateCondition(filter, i)"
@click.stop
> >
<a-select-option v-for="op of logicalOps" :key="op.value" :value="op.value"> <a-select-option v-for="op of logicalOps" :key="op.value" :value="op.value">
{{ op.text }} {{ op.text }}
</a-select-option> </a-select-option>
</a-select> </a-select>
<LazySmartsheetToolbarFieldListAutoCompleteDropdown <LazySmartsheetToolbarFieldListAutoCompleteDropdown
:key="`${i}_6`" :key="`${i}_6`"
v-model="filter.fk_column_id" v-model="filter.fk_column_id"
@ -204,9 +240,8 @@ defineExpose({
:columns="columns" :columns="columns"
:disabled="filter.readOnly" :disabled="filter.readOnly"
@click.stop @click.stop
@change="saveOrUpdate(filter, i)" @change="selectFilterField(filter, i)"
/> />
<a-select <a-select
v-model:value="filter.comparison_op" v-model:value="filter.comparison_op"
:dropdown-match-select-width="false" :dropdown-match-select-width="false"
@ -219,7 +254,7 @@ defineExpose({
dropdown-class-name="nc-dropdown-filter-comp-op" dropdown-class-name="nc-dropdown-filter-comp-op"
@change="filterUpdateCondition(filter, i)" @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"> <a-select-option v-if="isComparisonOpAllowed(filter, compOp)" :value="compOp.value">
{{ compOp.text }} {{ compOp.text }}
</a-select-option> </a-select-option>
@ -229,19 +264,28 @@ defineExpose({
<span <span
v-if=" v-if="
filter.comparison_op && 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}`" :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 v-else
:key="`${i}_7`" class="nc-filter-value-select min-w-[120px]"
v-model:value="filter.value" :column="getColumn(filter)"
class="nc-filter-value-select" :filter="filter"
:disabled="filter.readOnly || !filter.fk_column_id" @update-filter-value="(value) => updateFilterValue(value, filter, i)"
@click.stop @click.stop
@input="saveOrUpdate(filter, i)"
/> />
</template> </template>
</template> </template>
@ -259,7 +303,7 @@ defineExpose({
<a-button v-if="!webHook" class="text-capitalize !text-gray-500" @click.stop="addFilterGroup"> <a-button v-if="!webHook" class="text-capitalize !text-gray-500" @click.stop="addFilterGroup">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<!-- Add Filter Group --> <!-- Add Filter Group -->
<MdiPlus /> <MdiPlus />
{{ $t('activity.addFilterGroup') }} {{ $t('activity.addFilterGroup') }}
</div> </div>
@ -271,7 +315,7 @@ defineExpose({
<style scoped> <style scoped>
.nc-filter-grid { .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; @apply grid gap-[12px] items-center;
} }

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

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { SelectProps } from 'ant-design-vue' import type { SelectProps } from 'ant-design-vue'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' import { RelationTypes, UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { MetaInj, computed, inject, ref, resolveComponent } from '#imports' import { ActiveViewInj, MetaInj, computed, inject, ref, resolveComponent, useViewColumns } from '#imports'
const { modelValue, isSort } = defineProps<{ const { modelValue, isSort } = defineProps<{
modelValue?: string modelValue?: string
@ -18,10 +18,21 @@ const localValue = computed({
set: (val) => emit('update:modelValue', val), set: (val) => emit('update:modelValue', val),
}) })
const activeView = inject(ActiveViewInj, ref())
const { showSystemFields, metaColumnById } = useViewColumns(activeView, meta)
const options = computed<SelectProps['options']>(() => const options = computed<SelectProps['options']>(() =>
meta.value?.columns meta.value?.columns
?.filter((c: ColumnType) => { ?.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 return false
} else if (isSort) { } else if (isSort) {
/** ignore hasmany and manytomany relations if it's using within sort menu */ /** 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()) 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> </script>
<template> <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 numberOfHiddenFields = computed(() => filteredFieldList.value?.filter((field) => !field.show)?.length)
const gridPrimaryValueField = computed(() => { const gridDisplayValueField = computed(() => {
if (activeView.value?.type !== ViewTypes.GRID) return null if (activeView.value?.type !== ViewTypes.GRID) return null
const pvCol = Object.values(metaColumnById.value)?.find((col) => col?.pv) const pvCol = Object.values(metaColumnById.value)?.find((col) => col?.pv)
return filteredFieldList.value?.find((field) => field.fk_column_id === pvCol?.id) 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)"> <Draggable v-model="fields" item-key="id" @change="onMove($event)">
<template #item="{ element: field, index: index }"> <template #item="{ element: field, index: index }">
<div <div
v-if="filteredFieldList.filter((el) => el !== gridPrimaryValueField).includes(field)" v-if="filteredFieldList.filter((el) => el !== gridDisplayValueField).includes(field)"
:key="field.id" :key="field.id"
class="px-2 py-1 flex items-center" class="px-2 py-1 flex items-center"
:data-testid="`nc-fields-menu-${field.title}`" :data-testid="`nc-fields-menu-${field.title}`"
@ -220,15 +220,15 @@ useMenuCloseOnEsc(open)
</template> </template>
<template v-if="activeView?.type === ViewTypes.GRID" #header> <template v-if="activeView?.type === ViewTypes.GRID" #header>
<div <div
v-if="gridPrimaryValueField" v-if="gridDisplayValueField"
:key="`pv-${gridPrimaryValueField.id}`" :key="`pv-${gridDisplayValueField.id}`"
class="px-2 py-1 flex items-center" class="px-2 py-1 flex items-center"
:data-testid="`nc-fields-menu-${gridPrimaryValueField.title}`" :data-testid="`nc-fields-menu-${gridDisplayValueField.title}`"
@click.stop @click.stop
> >
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> <template #title>
<span class="text-sm">Primary Value</span> <span class="text-sm">Display Value</span>
</template> </template>
<MdiTableKey class="text-xs" /> <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 tab.title = createdTable.title as string
} }
// set primary value // set display value
if (createdTable?.columns?.[0]?.id) { if (createdTable?.columns?.[0]?.id) {
await $api.dbTableColumn.primaryColumnSet(createdTable.columns[0].id as string) 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 { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore( const { loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>, column as Ref<Required<ColumnType>>,
row, row,
isNew, isNew,
@ -85,8 +85,8 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
<template> <template>
<div class="flex w-full chips-wrapper items-center" :class="{ active }"> <div class="flex w-full chips-wrapper items-center" :class="{ active }">
<div class="chips flex items-center flex-1"> <div class="chips flex items-center flex-1">
<template v-if="value && relatedTablePrimaryValueProp"> <template v-if="value && relatedTableDisplayValueProp">
<VirtualCellComponentsItemChip :item="value" :value="value[relatedTablePrimaryValueProp]" @unlink="unlinkRef(value)" /> <VirtualCellComponentsItemChip :item="value" :value="value[relatedTableDisplayValueProp]" @unlink="unlinkRef(value)" />
</template> </template>
</div> </div>

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

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

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

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

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

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

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

@ -49,7 +49,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
const { sharedView } = useSharedView() const { sharedView } = useSharedView()
// getters // getters
const primaryValue = computed(() => { const displayValue = computed(() => {
if (row?.value?.row) { if (row?.value?.row) {
const col = meta?.value?.columns?.find((c) => c.pv) const col = meta?.value?.columns?.find((c) => c.pv)
@ -190,7 +190,8 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
} }
} else { } else {
// No columns to update // 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) addOrEditStackRow(row.value, isNewRow)
} }
message.success(`${primaryValue.value || 'Row'} updated successfully.`) message.success(`${displayValue.value || 'Row'} updated successfully.`)
changedColumns.value = new Set() changedColumns.value = new Set()
} catch (e: any) { } catch (e: any) {
@ -237,7 +238,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
isYou, isYou,
commentsDrawer, commentsDrawer,
row, row,
primaryValue, displayValue,
save, save,
changedColumns, changedColumns,
loadRow, 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) 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 || '' return (relatedTableMeta.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title || ''
}) })
const relatedTablePrimaryKeyProps = computed(() => { const relatedTablePrimaryKeyProps = computed(() => {
return relatedTableMeta.value?.columns?.filter((c) => c.pk)?.map((c) => c.title) ?? [] 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 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), offset: childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1),
where: where:
childrenExcludedListPagination.query && childrenExcludedListPagination.query &&
`(${relatedTablePrimaryValueProp.value},like,${childrenExcludedListPagination.query})`, `(${relatedTableDisplayValueProp.value},like,${childrenExcludedListPagination.query})`,
fields: [relatedTablePrimaryValueProp.value, ...relatedTablePrimaryKeyProps.value], fields: [relatedTableDisplayValueProp.value, ...relatedTablePrimaryKeyProps.value],
} as RequestParams, } as RequestParams,
}, },
) )
@ -142,8 +142,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
offset: childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1), offset: childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1),
where: where:
childrenExcludedListPagination.query && childrenExcludedListPagination.query &&
`(${relatedTablePrimaryValueProp.value},like,${childrenExcludedListPagination.query})`, `(${relatedTableDisplayValueProp.value},like,${childrenExcludedListPagination.query})`,
fields: [relatedTablePrimaryValueProp.value, ...relatedTablePrimaryKeyProps.value], fields: [relatedTableDisplayValueProp.value, ...relatedTablePrimaryKeyProps.value],
} as any, } as any,
) )
} else { } else {
@ -160,7 +160,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
// todo: where clause is missing from type // todo: where clause is missing from type
where: where:
childrenExcludedListPagination.query && childrenExcludedListPagination.query &&
`(${relatedTablePrimaryValueProp.value},like,${childrenExcludedListPagination.query})`, `(${relatedTableDisplayValueProp.value},like,${childrenExcludedListPagination.query})`,
} as any, } as any,
) )
} }
@ -183,7 +183,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
limit: String(childrenListPagination.size), limit: String(childrenListPagination.size),
offset: String(childrenListPagination.size * (childrenListPagination.page - 1)), offset: String(childrenListPagination.size * (childrenListPagination.page - 1)),
where: where:
childrenListPagination.query && `(${relatedTablePrimaryValueProp.value},like,${childrenListPagination.query})`, childrenListPagination.query && `(${relatedTableDisplayValueProp.value},like,${childrenListPagination.query})`,
} as any, } as any,
) )
} else { } else {
@ -198,7 +198,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
limit: String(childrenListPagination.size), limit: String(childrenListPagination.size),
offset: String(childrenListPagination.size * (childrenListPagination.page - 1)), offset: String(childrenListPagination.size * (childrenListPagination.page - 1)),
where: where:
childrenListPagination.query && `(${relatedTablePrimaryValueProp.value},like,${childrenListPagination.query})`, childrenListPagination.query && `(${relatedTableDisplayValueProp.value},like,${childrenListPagination.query})`,
} as any, } as any,
) )
} }
@ -320,13 +320,13 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
return { return {
relatedTableMeta, relatedTableMeta,
loadRelatedTableMeta, loadRelatedTableMeta,
relatedTablePrimaryValueProp, relatedTableDisplayValueProp,
childrenExcludedList, childrenExcludedList,
childrenList, childrenList,
rowId, rowId,
childrenExcludedListPagination, childrenExcludedListPagination,
childrenListPagination, childrenListPagination,
primaryValueProp, displayValueProp,
meta, meta,
unlink, unlink,
link, link,

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

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

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

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

114
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 { ComputedRef, Ref } from 'vue'
import type { SelectProps } from 'ant-design-vue'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import { import {
ActiveViewInj,
IsPublicInj, IsPublicInj,
ReloadViewDataHookInj, MetaInj,
computed, computed,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
inject, inject,
message, message,
ref, ref,
useDebounceFn,
useMetas, useMetas,
useNuxtApp, useNuxtApp,
useProject,
useUIPermission, useUIPermission,
watch, watch,
} from '#imports' } from '#imports'
@ -30,6 +35,8 @@ export function useViewFilters(
const { nestedFilters } = useSmartsheetStoreOrThrow() const { nestedFilters } = useSmartsheetStoreOrThrow()
const { projectMeta } = useProject()
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
@ -68,11 +75,80 @@ export function useViewFilters(
// nonDeletedFilters are those filters that are not deleted physically & virtually // nonDeletedFilters are those filters that are not deleted physically & virtually
const nonDeletedFilters = computed(() => filters.value.filter((f) => f.status !== 'delete')) const nonDeletedFilters = computed(() => filters.value.filter((f) => f.status !== 'delete'))
const placeholderFilter: Filter = { const meta = inject(MetaInj, ref())
comparison_op: 'eq',
value: '', const activeView = inject(ActiveViewInj, ref())
status: 'create',
logical_op: 'and', 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) => { const loadFilters = async (hookId?: string) => {
@ -191,8 +267,6 @@ export function useViewFilters(
fk_parent_id: parentId, fk_parent_id: parentId,
}) })
} }
reloadHook?.trigger()
} catch (e: any) { } catch (e: any) {
console.log(e) console.log(e)
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
@ -201,13 +275,16 @@ export function useViewFilters(
reloadData?.() reloadData?.()
} }
const saveOrUpdateDebounced = useDebounceFn(saveOrUpdate, 500)
const addFilter = () => { const addFilter = () => {
filters.value.push({ ...placeholderFilter }) filters.value.push(placeholderFilter())
$e('a:filter:add', { length: filters.value.length }) $e('a:filter:add', { length: filters.value.length })
} }
const addFilterGroup = async () => { const addFilterGroup = async () => {
const child = { ...placeholderFilter } const child = placeholderFilter()
const placeHolderGroupFilter: Filter = { const placeHolderGroupFilter: Filter = {
is_group: true, is_group: true,
status: 'create', status: 'create',
@ -234,10 +311,21 @@ export function useViewFilters(
return metas?.value?.[view?.value?.fk_model_id as string]?.columns?.length || 0 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() 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": "إعادة تسمية الجدول", "renameTable": "إعادة تسمية الجدول",
"deleteTable": "حذف الجدول", "deleteTable": "حذف الجدول",
"addField": "إضافة حقل جديد إلى هذا الجدول", "addField": "إضافة حقل جديد إلى هذا الجدول",
"setPrimary": "تعيين كقيمة أساسية",
"addRow": "إضافة صف جديد", "addRow": "إضافة صف جديد",
"saveRow": "حفظ الصف", "saveRow": "حفظ الصف",
"saveAndExit": "Save & Exit", "saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay", "saveAndStay": "Save & Stay",
"insertRow": "إدراج صف جديد", "insertRow": "إدراج صف جديد",
"deleteRow": "حذف الصف", "deleteRow": "حذف الصف",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "حذف الصفوف المحددة", "deleteSelectedRow": "حذف الصفوف المحددة",
"importExcel": "استيراد Excel", "importExcel": "استيراد Excel",
"importCSV": "استيراد CSV", "importCSV": "استيراد CSV",
@ -716,6 +716,7 @@
}, },
"success": { "success": {
"columnDuplicated": "Column duplicated successfully", "columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "تم تحديث ACL واجهة المستخدم للجداول بنجاح", "updatedUIACL": "تم تحديث ACL واجهة المستخدم للجداول بنجاح",
"pluginUninstalled": "تم إلغاء تثبيت الإضافة بنجاح", "pluginUninstalled": "تم إلغاء تثبيت الإضافة بنجاح",
"pluginSettingsSaved": "تم حفظ إعدادات الإضافة بنجاح", "pluginSettingsSaved": "تم حفظ إعدادات الإضافة بنجاح",

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

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

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

@ -206,7 +206,7 @@
"advancedSettings": "Pokročilá nastavení", "advancedSettings": "Pokročilá nastavení",
"codeSnippet": "Úryvek kódu", "codeSnippet": "Úryvek kódu",
"keyboardShortcut": "Klávesové zkratky", "keyboardShortcut": "Klávesové zkratky",
"generateRandomName": "Generate Random Name" "generateRandomName": "Vytvořit náhodné jméno"
}, },
"labels": { "labels": {
"createdBy": "Vytvořil/a", "createdBy": "Vytvořil/a",
@ -380,13 +380,13 @@
"renameTable": "Přejmenování tabulky", "renameTable": "Přejmenování tabulky",
"deleteTable": "Tabulka Odstranit", "deleteTable": "Tabulka Odstranit",
"addField": "Přidání nového pole do této tabulky", "addField": "Přidání nového pole do této tabulky",
"setPrimary": "Nastavit jako primární hodnotu",
"addRow": "Přidat nový řádek", "addRow": "Přidat nový řádek",
"saveRow": "Uložit řádek", "saveRow": "Uložit řádek",
"saveAndExit": "Uložit a odejít", "saveAndExit": "Uložit a odejít",
"saveAndStay": "Uložit a zůstat", "saveAndStay": "Uložit a zůstat",
"insertRow": "Vložit nový řádek", "insertRow": "Vložit nový řádek",
"deleteRow": "Odstranit řádek", "deleteRow": "Odstranit řádek",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Odstranit vybrané řádky", "deleteSelectedRow": "Odstranit vybrané řádky",
"importExcel": "Importovat Excel", "importExcel": "Importovat Excel",
"importCSV": "Importovat CSV", "importCSV": "Importovat CSV",
@ -716,6 +716,7 @@
}, },
"success": { "success": {
"columnDuplicated": "Sloupec úspěšně duplikován", "columnDuplicated": "Sloupec úspěšně duplikován",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Úspěšná aktualizace uživatelského rozhraní ACL pro tabulky", "updatedUIACL": "Úspěšná aktualizace uživatelského rozhraní ACL pro tabulky",
"pluginUninstalled": "Plugin byl úspěšně odinstalován", "pluginUninstalled": "Plugin byl úspěšně odinstalován",
"pluginSettingsSaved": "Nastavení zásuvného modulu bylo úspěšně uloženo", "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", "renameTable": "Bord omdøb",
"deleteTable": "TABEL DELETE.", "deleteTable": "TABEL DELETE.",
"addField": "Tilføj nyt felt til denne tabel", "addField": "Tilføj nyt felt til denne tabel",
"setPrimary": "Indstil som primær værdi",
"addRow": "Tilføj ny række", "addRow": "Tilføj ny række",
"saveRow": "Gem ro", "saveRow": "Gem ro",
"saveAndExit": "Gem og afslutning", "saveAndExit": "Gem og afslutning",
"saveAndStay": "Gem og bliv", "saveAndStay": "Gem og bliv",
"insertRow": "Indsæt ny række", "insertRow": "Indsæt ny række",
"deleteRow": "DELETE ROW.", "deleteRow": "DELETE ROW.",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Slet de valgte rækker", "deleteSelectedRow": "Slet de valgte rækker",
"importExcel": "Import Excel.", "importExcel": "Import Excel.",
"importCSV": "Import CSV.", "importCSV": "Import CSV.",
@ -716,6 +716,7 @@
}, },
"success": { "success": {
"columnDuplicated": "Kolonne duplikeret med succes", "columnDuplicated": "Kolonne duplikeret med succes",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Opdateret UI ACL for tabeller med succes", "updatedUIACL": "Opdateret UI ACL for tabeller med succes",
"pluginUninstalled": "Plugin afinstalleret med succes", "pluginUninstalled": "Plugin afinstalleret med succes",
"pluginSettingsSaved": "Plugin-indstillingerne er gemt med succes", "pluginSettingsSaved": "Plugin-indstillingerne er gemt med succes",

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

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

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

@ -381,13 +381,14 @@
"renameTable": "Table Rename", "renameTable": "Table Rename",
"deleteTable": "Table Delete", "deleteTable": "Table Delete",
"addField": "Add new field to this table", "addField": "Add new field to this table",
"setPrimary": "Set as Primary value", "setDisplay": "Set as Display value",
"addRow": "Add new row", "addRow": "Add new row",
"saveRow": "Save row", "saveRow": "Save row",
"saveAndExit": "Save & Exit", "saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay", "saveAndStay": "Save & Stay",
"insertRow": "Insert New Row", "insertRow": "Insert New Row",
"deleteRow": "Delete Row", "deleteRow": "Delete Row",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Delete Selected Rows", "deleteSelectedRow": "Delete Selected Rows",
"importExcel": "Import Excel", "importExcel": "Import Excel",
"importCSV": "Import CSV", "importCSV": "Import CSV",
@ -641,6 +642,11 @@
"deleteViewConfirmation": "Are you sure you want to delete this view?", "deleteViewConfirmation": "Are you sure you want to delete this view?",
"deleteTableConfirmation": "Do you want to delete the table", "deleteTableConfirmation": "Do you want to delete the table",
"showM2mTables": "Show M2M Tables", "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.", "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", "computedFieldEditWarning": "Computed field: contents are read-only. Use column edit menu to reconfigure",
"computedFieldDeleteWarning": "Computed field: contents are read-only. Unable to clear content.", "computedFieldDeleteWarning": "Computed field: contents are read-only. Unable to clear content.",
@ -727,6 +733,7 @@
}, },
"success": { "success": {
"columnDuplicated": "Column duplicated successfully", "columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully", "updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully", "pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully", "pluginSettingsSaved": "Plugin settings saved successfully",

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

@ -380,13 +380,13 @@
"renameTable": "Cambiar el nombre de la tabla", "renameTable": "Cambiar el nombre de la tabla",
"deleteTable": "Borrar tabla", "deleteTable": "Borrar tabla",
"addField": "Añadir nuevo campo a esta tabla", "addField": "Añadir nuevo campo a esta tabla",
"setPrimary": "Establecido como clave primaria",
"addRow": "Añadir nueva fila", "addRow": "Añadir nueva fila",
"saveRow": "Grabar la fila", "saveRow": "Grabar la fila",
"saveAndExit": "Guardar y salir", "saveAndExit": "Guardar y salir",
"saveAndStay": "Ahorrar y quedarse", "saveAndStay": "Ahorrar y quedarse",
"insertRow": "Insertar nueva fila", "insertRow": "Insertar nueva fila",
"deleteRow": "Borrar fila", "deleteRow": "Borrar fila",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Eliminar filas seleccionadas", "deleteSelectedRow": "Eliminar filas seleccionadas",
"importExcel": "Importar Excel", "importExcel": "Importar Excel",
"importCSV": "Importar CSV", "importCSV": "Importar CSV",
@ -716,6 +716,7 @@
}, },
"success": { "success": {
"columnDuplicated": "Columna duplicada con éxito", "columnDuplicated": "Columna duplicada con éxito",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Actualizada con éxito la interfaz de usuario ACL para las tablas", "updatedUIACL": "Actualizada con éxito la interfaz de usuario ACL para las tablas",
"pluginUninstalled": "Plugin desinstalado correctamente", "pluginUninstalled": "Plugin desinstalado correctamente",
"pluginSettingsSaved": "La configuración del plugin se ha guardado 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", "renameTable": "Table Rename",
"deleteTable": "Table Delete", "deleteTable": "Table Delete",
"addField": "Add new field to this table", "addField": "Add new field to this table",
"setPrimary": "Set as Primary value",
"addRow": "Add new row", "addRow": "Add new row",
"saveRow": "Save row", "saveRow": "Save row",
"saveAndExit": "Save & Exit", "saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay", "saveAndStay": "Save & Stay",
"insertRow": "Insert New Row", "insertRow": "Insert New Row",
"deleteRow": "Delete Row", "deleteRow": "Delete Row",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Delete Selected Rows", "deleteSelectedRow": "Delete Selected Rows",
"importExcel": "Import Excel", "importExcel": "Import Excel",
"importCSV": "Import CSV", "importCSV": "Import CSV",
@ -716,6 +716,7 @@
}, },
"success": { "success": {
"columnDuplicated": "Column duplicated successfully", "columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully", "updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully", "pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully", "pluginSettingsSaved": "Plugin settings saved successfully",

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

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

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

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

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

@ -380,13 +380,13 @@
"renameTable": "Renommer le tableau", "renameTable": "Renommer le tableau",
"deleteTable": "Supprimer le tableau", "deleteTable": "Supprimer le tableau",
"addField": "Ajouter un nouveau champ à ce tableau", "addField": "Ajouter un nouveau champ à ce tableau",
"setPrimary": "Définir comme valeur primaire",
"addRow": "Ajouter une nouvelle ligne", "addRow": "Ajouter une nouvelle ligne",
"saveRow": "Enregistrer la ligne", "saveRow": "Enregistrer la ligne",
"saveAndExit": "Enregistrer et quitter", "saveAndExit": "Enregistrer et quitter",
"saveAndStay": "Enregistrer et rester", "saveAndStay": "Enregistrer et rester",
"insertRow": "Insérer une nouvelle ligne", "insertRow": "Insérer une nouvelle ligne",
"deleteRow": "Supprimer la ligne", "deleteRow": "Supprimer la ligne",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Supprimer les lignes sélectionnées", "deleteSelectedRow": "Supprimer les lignes sélectionnées",
"importExcel": "Importer depuis Excel", "importExcel": "Importer depuis Excel",
"importCSV": "Importer un fichier CSV", "importCSV": "Importer un fichier CSV",
@ -716,6 +716,7 @@
}, },
"success": { "success": {
"columnDuplicated": "Colonne dupliquée avec succès", "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", "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", "pluginUninstalled": "Le plugin a été désinstallé avec succès",
"pluginSettingsSaved": "Les paramètres du plugin ont été enregistrés 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": "שולחן שינוי שם", "renameTable": "שולחן שינוי שם",
"deleteTable": "טבלה מחיקה", "deleteTable": "טבלה מחיקה",
"addField": "הוסף שדה חדש לטבלה זו", "addField": "הוסף שדה חדש לטבלה זו",
"setPrimary": "להגדיר כערך ראשי",
"addRow": "הוסף שורה חדשה", "addRow": "הוסף שורה חדשה",
"saveRow": "שמור שורה", "saveRow": "שמור שורה",
"saveAndExit": "Save & Exit", "saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay", "saveAndStay": "Save & Stay",
"insertRow": "הכנס שורה חדשה", "insertRow": "הכנס שורה חדשה",
"deleteRow": "מחק שורה", "deleteRow": "מחק שורה",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "מחק את השורות שנבחרו", "deleteSelectedRow": "מחק את השורות שנבחרו",
"importExcel": "ייבוא Excel", "importExcel": "ייבוא Excel",
"importCSV": "Import CSV", "importCSV": "Import CSV",
@ -716,6 +716,7 @@
}, },
"success": { "success": {
"columnDuplicated": "Column duplicated successfully", "columnDuplicated": "Column duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully", "updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully", "pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully", "pluginSettingsSaved": "Plugin settings saved successfully",

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -380,13 +380,13 @@
"renameTable": "Premenovanie tabuľky", "renameTable": "Premenovanie tabuľky",
"deleteTable": "Tabuľka Vymazať", "deleteTable": "Tabuľka Vymazať",
"addField": "Pridanie nového poľa do tejto tabuľky", "addField": "Pridanie nového poľa do tejto tabuľky",
"setPrimary": "Nastaviť ako primárnu hodnotu",
"addRow": "Pridanie nového riadku", "addRow": "Pridanie nového riadku",
"saveRow": "Uložiť riadok", "saveRow": "Uložiť riadok",
"saveAndExit": "Uložiť a ukončiť", "saveAndExit": "Uložiť a ukončiť",
"saveAndStay": "Uložiť a zostať", "saveAndStay": "Uložiť a zostať",
"insertRow": "Vložiť nový riadok", "insertRow": "Vložiť nový riadok",
"deleteRow": "Odstrániť riadok", "deleteRow": "Odstrániť riadok",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Odstránenie vybraných riadkov", "deleteSelectedRow": "Odstránenie vybraných riadkov",
"importExcel": "Importovať aplikáciu Excel", "importExcel": "Importovať aplikáciu Excel",
"importCSV": "Import CSV", "importCSV": "Import CSV",
@ -716,6 +716,7 @@
}, },
"success": { "success": {
"columnDuplicated": "Stĺpec bol úspešne duplikovaný", "columnDuplicated": "Stĺpec bol úspešne duplikovaný",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Úspešne aktualizované používateľské rozhranie ACL pre tabuľky", "updatedUIACL": "Úspešne aktualizované používateľské rozhranie ACL pre tabuľky",
"pluginUninstalled": "Plugin úspešne odinštalovaný", "pluginUninstalled": "Plugin úspešne odinštalovaný",
"pluginSettingsSaved": "Nastavenia zásuvného modulu boli úspešne uložené", "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", "renameTable": "Preimenuj tabele",
"deleteTable": "Tabela Delete.", "deleteTable": "Tabela Delete.",
"addField": "V to tabelo dodajte novo polje", "addField": "V to tabelo dodajte novo polje",
"setPrimary": "Kot primarna vrednost",
"addRow": "Dodaj novo vrstico", "addRow": "Dodaj novo vrstico",
"saveRow": "Shrani vrstico", "saveRow": "Shrani vrstico",
"saveAndExit": "Shranjevanje in izhod", "saveAndExit": "Shranjevanje in izhod",
"saveAndStay": "Shranjevanje in bivanje", "saveAndStay": "Shranjevanje in bivanje",
"insertRow": "Vstavite novo vrstico", "insertRow": "Vstavite novo vrstico",
"deleteRow": "Izbriši vrstico", "deleteRow": "Izbriši vrstico",
"duplicateRow": "Duplicate Row",
"deleteSelectedRow": "Izbrišite izbrane vrstice", "deleteSelectedRow": "Izbrišite izbrane vrstice",
"importExcel": "Uvoz Excel.", "importExcel": "Uvoz Excel.",
"importCSV": "Uvoz CSV", "importCSV": "Uvoz CSV",
@ -716,6 +716,7 @@
}, },
"success": { "success": {
"columnDuplicated": "Stolpec je bil uspešno podvojen", "columnDuplicated": "Stolpec je bil uspešno podvojen",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"updatedUIACL": "Uspešno posodobljen uporabniški vmesnik ACL za tabele", "updatedUIACL": "Uspešno posodobljen uporabniški vmesnik ACL za tabele",
"pluginUninstalled": "Vtičnik uspešno odstranjen", "pluginUninstalled": "Vtičnik uspešno odstranjen",
"pluginSettingsSaved": "Nastavitve vtičnika so bile uspešno shranjene", "pluginSettingsSaved": "Nastavitve vtičnika so bile uspešno shranjene",

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -31,7 +31,7 @@
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1", "locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"nocodb-sdk": "file:../nocodb-sdk", "nocodb-sdk": "0.105.0",
"papaparse": "^5.3.2", "papaparse": "^5.3.2",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"socket.io-client": "^4.5.1", "socket.io-client": "^4.5.1",
@ -57,6 +57,7 @@
"@iconify-json/clarity": "^1.1.4", "@iconify-json/clarity": "^1.1.4",
"@iconify-json/eva": "^1.1.2", "@iconify-json/eva": "^1.1.2",
"@iconify-json/ic": "^1.1.7", "@iconify-json/ic": "^1.1.7",
"@iconify-json/la": "^1.1.2",
"@iconify-json/logos": "^1.1.14", "@iconify-json/logos": "^1.1.14",
"@iconify-json/lucide": "^1.1.36", "@iconify-json/lucide": "^1.1.36",
"@iconify-json/material-symbols": "^1.1.8", "@iconify-json/material-symbols": "^1.1.8",
@ -102,7 +103,8 @@
} }
}, },
"../nocodb-sdk": { "../nocodb-sdk": {
"version": "0.104.3", "version": "0.105.0",
"extraneous": true,
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
@ -1275,8 +1277,19 @@
"eslint": ">=3.14.1" "eslint": ">=3.14.1"
} }
}, },
"../nocodb-sdk/node_modules/eslint-config-prettier/node_modules/get-stdin": { "node_modules/@iconify-json/la": {
"version": "6.0.0", "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, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -7551,22 +7564,10 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/async-sema": { "node_modules/follow-redirects": {
"version": "3.1.1", "version": "1.15.1",
"dev": true, "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"license": "MIT" "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
},
"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,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -10323,8 +10324,27 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/eslint/node_modules/p-locate": { "node_modules/nocodb-sdk": {
"version": "5.0.0", "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, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -15492,8 +15512,19 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/read-pkg-up/node_modules/path-exists": { "@iconify-json/la": {
"version": "4.0.0", "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, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -19336,7 +19367,30 @@
"version": "1.1.3", "version": "1.1.3",
"dev": true, "dev": true,
"requires": { "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": { "@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": { "node-abi": {
"version": "3.23.0", "version": "3.23.0",
"dev": true, "dev": true,

3
packages/nc-gui/package.json

@ -54,7 +54,7 @@
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1", "locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"nocodb-sdk": "file:../nocodb-sdk", "nocodb-sdk": "0.105.0",
"papaparse": "^5.3.2", "papaparse": "^5.3.2",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"socket.io-client": "^4.5.1", "socket.io-client": "^4.5.1",
@ -80,6 +80,7 @@
"@iconify-json/clarity": "^1.1.4", "@iconify-json/clarity": "^1.1.4",
"@iconify-json/eva": "^1.1.2", "@iconify-json/eva": "^1.1.2",
"@iconify-json/ic": "^1.1.7", "@iconify-json/ic": "^1.1.7",
"@iconify-json/la": "^1.1.2",
"@iconify-json/logos": "^1.1.14", "@iconify-json/logos": "^1.1.14",
"@iconify-json/lucide": "^1.1.36", "@iconify-json/lucide": "^1.1.36",
"@iconify-json/material-symbols": "^1.1.8", "@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 text: string
value: string value: string
ignoreVal?: boolean ignoreVal?: boolean
includedTypes?: UITypes[] includedTypes?: UITypes[]
excludedTypes?: UITypes[] excludedTypes?: UITypes[]
}[] = [ }[] => [
{ {
text: 'is checked', text: 'is checked',
value: 'checked', value: 'checked',
@ -20,44 +54,84 @@ export const comparisonOpList: {
includedTypes: [UITypes.Checkbox], includedTypes: [UITypes.Checkbox],
}, },
{ {
text: 'is equal', text: getEqText(fieldUiType),
value: 'eq', value: 'eq',
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment],
}, },
{ {
text: 'is not equal', text: getNeqText(fieldUiType),
value: 'neq', value: 'neq',
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment],
}, },
{ {
text: 'is like', text: getLikeText(fieldUiType),
value: 'like', value: 'like',
excludedTypes: [UITypes.Checkbox], excludedTypes: [UITypes.Checkbox, UITypes.SingleSelect, UITypes.MultiSelect, UITypes.Collaborator, ...numericUITypes],
}, },
{ {
text: 'is not like', text: getNotLikeText(fieldUiType),
value: 'nlike', value: 'nlike',
excludedTypes: [UITypes.Checkbox], excludedTypes: [UITypes.Checkbox, UITypes.SingleSelect, UITypes.MultiSelect, UITypes.Collaborator, ...numericUITypes],
}, },
{ {
text: 'is empty', text: 'is empty',
value: 'empty', value: 'empty',
ignoreVal: true, 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', text: 'is not empty',
value: 'notempty', value: 'notempty',
ignoreVal: true, 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', text: 'is null',
value: 'null', value: 'null',
ignoreVal: true, ignoreVal: true,
excludedTypes: [
...numericUITypes,
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
],
}, },
{ {
text: 'is not null', text: 'is not null',
value: 'notnull', value: 'notnull',
ignoreVal: true, ignoreVal: true,
excludedTypes: [
...numericUITypes,
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
],
}, },
{ {
text: 'contains all of', text: 'contains all of',
@ -82,21 +156,33 @@ export const comparisonOpList: {
{ {
text: '>', text: '>',
value: 'gt', value: 'gt',
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.SingleSelect], includedTypes: [...numericUITypes],
}, },
{ {
text: '<', text: '<',
value: 'lt', value: 'lt',
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.SingleSelect], includedTypes: [...numericUITypes],
}, },
{ {
text: '>=', text: '>=',
value: 'gte', value: 'gte',
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.SingleSelect], includedTypes: [...numericUITypes],
}, },
{ {
text: '<=', text: '<=',
value: 'lte', 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", "name": "nc-lib-gui",
"version": "0.104.3", "version": "0.105.0",
"description": "NocoDB GUI", "description": "NocoDB GUI",
"author": { "author": {
"name": "NocoDB", "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 ```js
async resetFilter() { async resetFilter() {
await this.waitForResponse({ 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'], httpMethodsToMatch: ['DELETE'],
requestUrlPathToMatch: '/api/v1/db/meta/filters/', 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"> <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" title: "Display Value"
description: "Understanding Primary Value in NocoDB!" description: "Understanding Display Value in NocoDB!"
position: 580 position: 580
category: "Product" category: "Product"
menuTitle: "Primary Value" menuTitle: "Display Value"
--- ---
## What is a Primary Value ? ## What is a Display 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.
- 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. - 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 ? ## What is the use of Display Value ?
- Within a spreadsheet, primary value are always highlighted so that it is easier to recognise what row we are in. - 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 primary value that appears in LinkToAnotheRecord column. - 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"> <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"> <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"> <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. - 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. - 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. - **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. - **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) ![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) ![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. - 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) ![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. - **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. - **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) ![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> <alert>
Note: If your Excel file contains multiple sheets, each sheet will be stored in a separate table. Note: If your Excel file contains multiple sheets, each sheet will be stored in a separate table.
</alert> </alert>

2
packages/nocodb-sdk/package.json

@ -1,6 +1,6 @@
{ {
"name": "nocodb-sdk", "name": "nocodb-sdk",
"version": "0.104.3", "version": "0.105.0",
"description": "NocoDB SDK", "description": "NocoDB SDK",
"main": "build/main/index.js", "main": "build/main/index.js",
"typings": "build/main/index.d.ts", "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/helperFunctions';
export * from './lib/enums'; export * from './lib/enums';
export * from './lib/formulaHelpers'; 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 CustomAPI } from './lib/CustomAPI';
export { default as TemplateGenerator } from './lib/TemplateGenerator'; export { default as TemplateGenerator } from './lib/TemplateGenerator';
export * from './lib/passwordHelpers'; export * from './lib/passwordHelpers';

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

@ -1894,6 +1894,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 * No description
* *
* @tags Project * @tags Project

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

@ -40,6 +40,28 @@ enum UITypes {
Button = 'Button', 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( export function isVirtualCol(
col: col:
| UITypes | UITypes

206
packages/nocodb/package-lock.json generated

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

8
packages/nocodb/package.json

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

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

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

353
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 RollupColumn from '../../../../models/RollupColumn';
import formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2'; import formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2';
import FormulaColumn from '../../../../models/FormulaColumn'; import FormulaColumn from '../../../../models/FormulaColumn';
import { RelationTypes, UITypes } from 'nocodb-sdk'; import { RelationTypes, UITypes, isNumericCol } from 'nocodb-sdk';
import { sanitize } from './helpers/sanitize'; import { sanitize } from './helpers/sanitize';
export default async function conditionV2( export default async function conditionV2(
@ -119,7 +119,7 @@ const parseConditionV2 = async (
? negatedMapping[filter.comparison_op] ? negatedMapping[filter.comparison_op]
: {}), : {}),
fk_model_id: childModel.id, fk_model_id: childModel.id,
fk_column_id: childModel?.primaryValue?.id, fk_column_id: childModel?.displayValue?.id,
}), }),
knex, knex,
aliasCount aliasCount
@ -154,7 +154,7 @@ const parseConditionV2 = async (
? negatedMapping[filter.comparison_op] ? negatedMapping[filter.comparison_op]
: {}), : {}),
fk_model_id: parentModel.id, fk_model_id: parentModel.id,
fk_column_id: parentModel?.primaryValue?.id, fk_column_id: parentModel?.displayValue?.id,
}), }),
knex, knex,
aliasCount aliasCount
@ -209,7 +209,7 @@ const parseConditionV2 = async (
? negatedMapping[filter.comparison_op] ? negatedMapping[filter.comparison_op]
: {}), : {}),
fk_model_id: parentModel.id, fk_model_id: parentModel.id,
fk_column_id: parentModel?.primaryValue?.id, fk_column_id: parentModel?.displayValue?.id,
}), }),
knex, knex,
aliasCount aliasCount
@ -271,46 +271,152 @@ const parseConditionV2 = async (
return (qb: Knex.QueryBuilder) => { return (qb: Knex.QueryBuilder) => {
let [field, val] = [_field, _val]; 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) { switch (filter.comparison_op) {
case 'eq': case 'eq':
qb = qb.where(field, val); 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; break;
case 'neq': case 'neq':
case 'not': case 'not':
qb = qb.where((nestedQb) => { if (qb?.client?.config?.client === 'mysql2') {
nestedQb if (
.whereNot(field, val) [
.orWhereNull(customWhereClause ? _val : _field); UITypes.Duration,
}); UITypes.Currency,
break; UITypes.Percent,
case 'like': UITypes.Number,
if (column.uidt === UITypes.Formula) { UITypes.Decimal,
[field, val] = [val, field]; UITypes.Rollup,
val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%'); ].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 { } else {
val = val.startsWith('%') || val.endsWith('%') ? val : `%${val}%`; qb = qb.where((nestedQb) => {
nestedQb
.whereNot(field, val)
.orWhereNull(customWhereClause ? _val : _field);
});
} }
if (qb?.client?.config?.client === 'pg') { break;
qb = qb.whereRaw('??::text ilike ?', [field, val]); 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 { } else {
qb = qb.where(field, 'like', val); if (column.uidt === UITypes.Formula) {
[field, val] = [val, field];
val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%');
} else {
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; break;
case 'nlike': case 'nlike':
if (column.uidt === UITypes.Formula) { if (!val) {
[field, val] = [val, field]; if (column.uidt === UITypes.Attachment) {
val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%'); 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 { } else {
val = val.startsWith('%') || val.endsWith('%') ? val : `%${val}%`; if (column.uidt === UITypes.Formula) {
} [field, val] = [val, field];
qb.where((nestedQb) => { val = `%${val}%`.replace(/^%'([\s\S]*)'%$/, '%$1%');
if (qb?.client?.config?.client === 'pg') {
nestedQb.whereRaw('??::text not ilike ?', [field, val]);
} else { } else {
nestedQb.whereNot(field, 'like', val); val =
val.startsWith('%') || val.endsWith('%') ? val : `%${val}%`;
} }
nestedQb.orWhereNull(field); qb.where((nestedQb) => {
}); if (qb?.client?.config?.client === 'pg') {
nestedQb.whereRaw('??::text not ilike ?', [field, val]);
} 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; break;
case 'allof': case 'allof':
case 'anyof': case 'anyof':
@ -319,8 +425,8 @@ const parseConditionV2 = async (
{ {
// Condition for filter, without negation // Condition for filter, without negation
const condition = (builder: Knex.QueryBuilder) => { const condition = (builder: Knex.QueryBuilder) => {
const items = val.split(',').map((item) => item.trim()); const items = val?.split(',').map((item) => item.trim());
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items?.length; i++) {
let sql; let sql;
const bindings = [field, `%,${items[i]},%`]; const bindings = [field, `%,${items[i]},%`];
if (qb?.client?.config?.client === 'pg') { if (qb?.client?.config?.client === 'pg') {
@ -355,11 +461,25 @@ const parseConditionV2 = async (
} }
break; break;
case 'gt': 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; break;
case 'ge': case 'ge':
case 'gte': 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; break;
case 'in': case 'in':
qb = qb.whereIn( qb = qb.whereIn(
@ -375,7 +495,9 @@ const parseConditionV2 = async (
else if (filter.value === 'empty') else if (filter.value === 'empty')
qb = qb.where(customWhereClause || field, ''); qb = qb.where(customWhereClause || field, '');
else if (filter.value === 'notempty') else if (filter.value === 'notempty')
qb = qb.whereNot(customWhereClause || field, ''); qb = qb
.whereNot(customWhereClause || field, '')
.orWhereNull(field);
else if (filter.value === 'true') else if (filter.value === 'true')
qb = qb.where(customWhereClause || field, true); qb = qb.where(customWhereClause || field, true);
else if (filter.value === 'false') else if (filter.value === 'false')
@ -396,13 +518,26 @@ const parseConditionV2 = async (
qb = qb.whereNot(customWhereClause || field, false); qb = qb.whereNot(customWhereClause || field, false);
break; break;
case 'lt': 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; break;
case 'le': case 'le':
case 'lte': 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; break;
case 'empty': case 'empty':
if (column.uidt === UITypes.Formula) { if (column.uidt === UITypes.Formula) {
[field, val] = [val, field]; [field, val] = [val, field];
@ -413,7 +548,7 @@ const parseConditionV2 = async (
if (column.uidt === UITypes.Formula) { if (column.uidt === UITypes.Formula) {
[field, val] = [val, field]; [field, val] = [val, field];
} }
qb = qb.whereNot(field, val); qb = qb.whereNot(field, val).orWhereNull(field);
break; break;
case 'null': case 'null':
qb = qb.whereNull(customWhereClause || field); qb = qb.whereNull(customWhereClause || field);
@ -421,6 +556,32 @@ const parseConditionV2 = async (
case 'notnull': case 'notnull':
qb = qb.whereNotNull(customWhereClause || field); qb = qb.whereNotNull(customWhereClause || field);
break; 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': case 'checked':
qb = qb.where(customWhereClause || field, true); qb = qb.where(customWhereClause || field, true);
break; break;
@ -480,19 +641,29 @@ async function generateLookupCondition(
qb.select(`${alias}.${childColumn.column_name}`); qb.select(`${alias}.${childColumn.column_name}`);
await nestedConditionJoin( if (filter.comparison_op === 'blank') {
{ return (qbP: Knex.QueryBuilder) => {
...filter, qbP.whereNotIn(childColumn.column_name, qb);
...(filter.comparison_op in negatedMapping };
? negatedMapping[filter.comparison_op] } else if (filter.comparison_op === 'notblank') {
: {}), return (qbP: Knex.QueryBuilder) => {
}, qbP.whereIn(childColumn.column_name, qb);
lookupColumn, };
qb, } else {
knex, await nestedConditionJoin(
alias, {
aliasCount ...filter,
); ...(filter.comparison_op in negatedMapping
? negatedMapping[filter.comparison_op]
: {}),
},
lookupColumn,
qb,
knex,
alias,
aliasCount
);
}
return (qbP: Knex.QueryBuilder) => { return (qbP: Knex.QueryBuilder) => {
if (filter.comparison_op in negatedMapping) if (filter.comparison_op in negatedMapping)
@ -503,19 +674,29 @@ async function generateLookupCondition(
qb = knex(`${parentModel.table_name} as ${alias}`); qb = knex(`${parentModel.table_name} as ${alias}`);
qb.select(`${alias}.${parentColumn.column_name}`); qb.select(`${alias}.${parentColumn.column_name}`);
await nestedConditionJoin( if (filter.comparison_op === 'blank') {
{ return (qbP: Knex.QueryBuilder) => {
...filter, qbP.whereNotIn(childColumn.column_name, qb);
...(filter.comparison_op in negatedMapping };
? negatedMapping[filter.comparison_op] } else if (filter.comparison_op === 'notblank') {
: {}), return (qbP: Knex.QueryBuilder) => {
}, qbP.whereIn(childColumn.column_name, qb);
lookupColumn, };
qb, } else {
knex, await nestedConditionJoin(
alias, {
aliasCount ...filter,
); ...(filter.comparison_op in negatedMapping
? negatedMapping[filter.comparison_op]
: {}),
},
lookupColumn,
qb,
knex,
alias,
aliasCount
);
}
return (qbP: Knex.QueryBuilder) => { return (qbP: Knex.QueryBuilder) => {
if (filter.comparison_op in negatedMapping) if (filter.comparison_op in negatedMapping)
@ -537,19 +718,29 @@ async function generateLookupCondition(
`${childAlias}.${parentColumn.column_name}` `${childAlias}.${parentColumn.column_name}`
); );
await nestedConditionJoin( if (filter.comparison_op === 'blank') {
{ return (qbP: Knex.QueryBuilder) => {
...filter, qbP.whereNotIn(childColumn.column_name, qb);
...(filter.comparison_op in negatedMapping };
? negatedMapping[filter.comparison_op] } else if (filter.comparison_op === 'notblank') {
: {}), return (qbP: Knex.QueryBuilder) => {
}, qbP.whereIn(childColumn.column_name, qb);
lookupColumn, };
qb, } else {
knex, await nestedConditionJoin(
childAlias, {
aliasCount ...filter,
); ...(filter.comparison_op in negatedMapping
? negatedMapping[filter.comparison_op]
: {}),
},
lookupColumn,
qb,
knex,
childAlias,
aliasCount
);
}
return (qbP: Knex.QueryBuilder) => { return (qbP: Knex.QueryBuilder) => {
if (filter.comparison_op in negatedMapping) if (filter.comparison_op in negatedMapping)
@ -650,7 +841,7 @@ async function nestedConditionJoin(
new Filter({ new Filter({
...filter, ...filter,
fk_model_id: childModel.id, fk_model_id: childModel.id,
fk_column_id: childModel.primaryValue?.id, fk_column_id: childModel.displayValue?.id,
}), }),
knex, knex,
aliasCount, aliasCount,
@ -666,7 +857,7 @@ async function nestedConditionJoin(
new Filter({ new Filter({
...filter, ...filter,
fk_model_id: parentModel.id, fk_model_id: parentModel.id,
fk_column_id: parentModel?.primaryValue?.id, fk_column_id: parentModel?.displayValue?.id,
}), }),
knex, knex,
aliasCount, aliasCount,
@ -682,7 +873,7 @@ async function nestedConditionJoin(
new Filter({ new Filter({
...filter, ...filter,
fk_model_id: parentModel.id, fk_model_id: parentModel.id,
fk_column_id: parentModel.primaryValue?.id, fk_column_id: parentModel.displayValue?.id,
}), }),
knex, knex,
aliasCount, 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('??.??', [ cn = knex.raw('??.??', [
nestedAlias, nestedAlias,
parentModel?.primaryValue?.column_name, parentModel?.displayValue?.column_name,
]); ]);
} }
break; break;
@ -283,7 +283,7 @@ async function _formulaQueryBuilder(
); );
cn = knex.raw('??.??', [ cn = knex.raw('??.??', [
nestedAlias, nestedAlias,
childModel?.primaryValue?.column_name, childModel?.displayValue?.column_name,
]); ]);
} }
break; break;
@ -311,7 +311,7 @@ async function _formulaQueryBuilder(
} }
cn = knex.raw('??.??', [ cn = knex.raw('??.??', [
nestedAlias, nestedAlias,
parentModel?.primaryValue?.column_name, parentModel?.displayValue?.column_name,
]); ]);
} }
@ -424,7 +424,7 @@ async function _formulaQueryBuilder(
let selectQb; let selectQb;
if (relation.type === 'bt') { if (relation.type === 'bt') {
selectQb = knex(parentModel.table_name) selectQb = knex(parentModel.table_name)
.select(parentModel?.primaryValue?.column_name) .select(parentModel?.displayValue?.column_name)
.where( .where(
`${parentModel.table_name}.${parentColumn.column_name}`, `${parentModel.table_name}.${parentColumn.column_name}`,
knex.raw(`??`, [ knex.raw(`??`, [
@ -447,7 +447,7 @@ async function _formulaQueryBuilder(
getAggregateFn(fn)({ getAggregateFn(fn)({
qb, qb,
knex, knex,
cn: childModel?.primaryValue?.column_name, cn: childModel?.displayValue?.column_name,
}) })
) )
.wrap('(', ')'); .wrap('(', ')');
@ -499,7 +499,7 @@ async function _formulaQueryBuilder(
getAggregateFn(fn)({ getAggregateFn(fn)({
qb, qb,
knex, knex,
cn: parentModel?.primaryValue?.column_name, cn: parentModel?.displayValue?.column_name,
}) })
) )
.wrap('(', ')'); .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
? model.primaryKeys.reduce((o, pk) => ({ ...o, [pk.title]: 1 }), {}) ? 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}`, `${nestedAlias}.${parentColumn.column_name}`,
`${prevAlias}.${childColumn.column_name}` `${prevAlias}.${childColumn.column_name}`
) )
.select(parentModel?.primaryValue?.column_name); .select(parentModel?.displayValue?.column_name);
} }
break; break;
case UITypes.Formula: case UITypes.Formula:
@ -201,7 +201,7 @@ export default async function sortV2(
await parentModel.getColumns(); await parentModel.getColumns();
const selectQb = knex(parentModel.table_name) const selectQb = knex(parentModel.table_name)
.select(parentModel?.primaryValue?.column_name) .select(parentModel?.displayValue?.column_name)
.where( .where(
`${parentModel.table_name}.${parentColumn.column_name}`, `${parentModel.table_name}.${parentColumn.column_name}`,
knex.raw(`??`, [ knex.raw(`??`, [

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

@ -1,5 +1,5 @@
import BaseRender from '../../BaseRender'; import BaseRender from '../../BaseRender';
import mapDefaultPrimaryValue from '../../../../../meta/helpers/mapDefaultPrimaryValue'; import mapDefaultDisplayValue from '../../../../../meta/helpers/mapDefaultDisplayValue';
import { UITypes } from 'nocodb-sdk'; import { UITypes } from 'nocodb-sdk';
abstract class BaseModelXcMeta extends BaseRender { abstract class BaseModelXcMeta extends BaseRender {
@ -79,7 +79,7 @@ abstract class BaseModelXcMeta extends BaseRender {
columnsArr.push(columnObj); columnsArr.push(columnObj);
} }
this.mapDefaultPrimaryValue(columnsArr); this.mapDefaultDisplayValue(columnsArr);
return columnsArr; return columnsArr;
} }
@ -127,8 +127,8 @@ abstract class BaseModelXcMeta extends BaseRender {
return virtualColumns; return virtualColumns;
} }
public mapDefaultPrimaryValue(columnsArr: any[]): void { public mapDefaultDisplayValue(columnsArr: any[]): void {
mapDefaultPrimaryValue(columnsArr); 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); columnsArr.push(columnObj);
} }
this.mapDefaultPrimaryValue(columnsArr); this.mapDefaultDisplayValue(columnsArr);
return 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); columnsArr.push(columnObj);
} }
this.mapDefaultPrimaryValue(columnsArr); this.mapDefaultDisplayValue(columnsArr);
return 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); columnsArr.push(columnObj);
} }
this.mapDefaultPrimaryValue(columnsArr); this.mapDefaultDisplayValue(columnsArr);
return 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); columnsArr.push(columnObj);
} }
this.mapDefaultPrimaryValue(columnsArr); this.mapDefaultDisplayValue(columnsArr);
return 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); columnsArr.push(columnObj);
} }
this.mapDefaultPrimaryValue(columnsArr); this.mapDefaultDisplayValue(columnsArr);
return 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); columnsArr.push(columnObj);
} }
this.mapDefaultPrimaryValue(columnsArr); this.mapDefaultDisplayValue(columnsArr);
return 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 { Tele } from 'nc-help';
import validateParams from '../helpers/validateParams'; import validateParams from '../helpers/validateParams';
import { customAlphabet } from 'nanoid';
import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn'; import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';
import { import {
getUniqueColumnAliasName, getUniqueColumnAliasName,
@ -19,9 +18,7 @@ import {
isVirtualCol, isVirtualCol,
LinkToAnotherColumnReqType, LinkToAnotherColumnReqType,
LinkToAnotherRecordType, LinkToAnotherRecordType,
LookupColumnReqType,
RelationTypes, RelationTypes,
RollupColumnReqType,
substituteColumnAliasWithIdInFormula, substituteColumnAliasWithIdInFormula,
substituteColumnIdWithAliasInFormula, substituteColumnIdWithAliasInFormula,
TableType, TableType,
@ -34,15 +31,21 @@ import NcMetaIO from '../NcMetaIO';
import ncMetaAclMw from '../helpers/ncMetaAclMw'; import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { NcError } from '../helpers/catchError'; import { NcError } from '../helpers/catchError';
import getColumnPropsFromUIDT from '../helpers/getColumnPropsFromUIDT'; import getColumnPropsFromUIDT from '../helpers/getColumnPropsFromUIDT';
import mapDefaultPrimaryValue from '../helpers/mapDefaultPrimaryValue'; import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue';
import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import { metaApiMetrics } from '../helpers/apiMetrics'; import { metaApiMetrics } from '../helpers/apiMetrics';
import FormulaColumn from '../../models/FormulaColumn'; import FormulaColumn from '../../models/FormulaColumn';
import KanbanView from '../../models/KanbanView'; import KanbanView from '../../models/KanbanView';
import { MetaTable } from '../../utils/globals'; import { MetaTable } from '../../utils/globals';
import formulaQueryBuilderv2 from '../../db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2'; import formulaQueryBuilderv2 from '../../db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2';
import {
const randomID = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 10); createHmAndBtColumn,
generateFkName,
randomID,
validateLookupPayload,
validateRequiredField,
validateRollupPayload,
} from './helpers';
export enum Altered { export enum Altered {
NEW_COLUMN = 1, NEW_COLUMN = 1,
@ -50,73 +53,6 @@ export enum Altered {
UPDATE_COLUMN = 8, 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) { export async function columnGet(req: Request, res: Response) {
res.json(await Column.get({ colId: req.params.columnId })); res.json(await Column.get({ colId: req.params.columnId }));
} }
@ -153,49 +89,7 @@ export async function columnAdd(
switch (colBody.uidt) { switch (colBody.uidt) {
case UITypes.Rollup: case UITypes.Rollup:
{ {
validateParams( await validateRollupPayload(req.body);
[
'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 Column.insert({ await Column.insert({
...colBody, ...colBody,
@ -205,44 +99,7 @@ export async function columnAdd(
break; break;
case UITypes.Lookup: case UITypes.Lookup:
{ {
validateParams( await validateLookupPayload(req.body);
['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 Column.insert({ await Column.insert({
...colBody, ...colBody,
@ -252,7 +109,6 @@ export async function columnAdd(
break; break;
case UITypes.LinkToAnotherRecord: case UITypes.LinkToAnotherRecord:
// case UITypes.ForeignKey:
{ {
validateParams(['parentId', 'childId', 'type'], req.body); 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)); 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>) { export async function columnUpdate(req: Request, res: Response<TableType>) {
const column = await Column.get({ colId: req.params.columnId }); 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, title: colBody.title,
}); });
} }
await updateRollupOrLookup(colBody, column);
} else { } else {
NcError.notImplemented( NcError.notImplemented(
`Updating ${colBody.uidt} => ${colBody.uidt} is not implemented` `Updating ${colBody.uidt} => ${colBody.uidt} is not implemented`
@ -1712,11 +1592,11 @@ export async function columnDelete(req: Request, res: Response<TableType>) {
await table.getColumns(); await table.getColumns();
const primaryValueColumn = mapDefaultPrimaryValue(table.columns); const displayValueColumn = mapDefaultDisplayValue(table.columns);
if (primaryValueColumn) { if (displayValueColumn) {
await Model.updatePrimaryColumn( await Model.updatePrimaryColumn(
primaryValueColumn.fk_model_id, displayValueColumn.fk_model_id,
primaryValueColumn.id displayValueColumn.id
); );
} }

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

@ -225,7 +225,7 @@ export async function serializeCellValue({
await relatedModel.getColumns(); await relatedModel.getColumns();
return [...(Array.isArray(value) ? value : [value])] return [...(Array.isArray(value) ? value : [value])]
.map((v) => { .map((v) => {
return v[relatedModel.primaryValue?.title]; return v[relatedModel.displayValue?.title];
}) })
.join(', '); .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'; import { populateMeta } from './populateMeta';
export * from './columnHelpers';
export { populateMeta }; export { populateMeta };

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

@ -10,7 +10,7 @@ import getTableNameAlias, {
} from '../../helpers/getTableName'; } from '../../helpers/getTableName';
import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn'; import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn';
import getColumnUiType from '../../helpers/getColumnUiType'; import getColumnUiType from '../../helpers/getColumnUiType';
import mapDefaultPrimaryValue from '../../helpers/mapDefaultPrimaryValue'; import mapDefaultDisplayValue from '../../helpers/mapDefaultDisplayValue';
import { extractAndGenerateManyToManyRelations } from '../metaDiffApis'; import { extractAndGenerateManyToManyRelations } from '../metaDiffApis';
import { ModelTypes, UITypes, ViewTypes } from 'nocodb-sdk'; import { ModelTypes, UITypes, ViewTypes } from 'nocodb-sdk';
import { IGNORE_TABLES } from '../../../utils/common/BaseApiBuilder'; 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); : tableRelations.filter((r) => r.tn === table.tn);
mapDefaultPrimaryValue(columns); mapDefaultDisplayValue(columns);
// add vitual columns // add vitual columns
const virtualColumns = [ 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 { getUniqueColumnAliasName } from '../helpers/getUniqueName';
import NcHelp from '../../utils/NcHelp'; import NcHelp from '../../utils/NcHelp';
import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName'; import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName';
import mapDefaultPrimaryValue from '../helpers/mapDefaultPrimaryValue'; import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue';
import getColumnUiType from '../helpers/getColumnUiType'; import getColumnUiType from '../helpers/getColumnUiType';
import { metaApiMetrics } from '../helpers/apiMetrics'; import { metaApiMetrics } from '../helpers/apiMetrics';
@ -602,7 +602,7 @@ export async function metaDiffSync(req, res) {
await sqlClient.columnList({ tn: table_name }) await sqlClient.columnList({ tn: table_name })
)?.data?.list?.map((c) => ({ ...c, column_name: c.cn })); )?.data?.list?.map((c) => ({ ...c, column_name: c.cn }));
mapDefaultPrimaryValue(columns); mapDefaultDisplayValue(columns);
const model = await Model.insert(project.id, base.id, { const model = await Model.insert(project.id, base.id, {
table_name: table_name, table_name: table_name,
@ -630,7 +630,7 @@ export async function metaDiffSync(req, res) {
await sqlClient.columnList({ tn: table_name }) await sqlClient.columnList({ tn: table_name })
)?.data?.list?.map((c) => ({ ...c, column_name: c.cn })); )?.data?.list?.map((c) => ({ ...c, column_name: c.cn }));
mapDefaultPrimaryValue(columns); mapDefaultDisplayValue(columns);
const model = await Model.insert(project.id, base.id, { const model = await Model.insert(project.id, base.id, {
table_name: table_name, table_name: table_name,
@ -798,7 +798,7 @@ export async function baseMetaDiffSync(req, res) {
await sqlClient.columnList({ tn: table_name }) await sqlClient.columnList({ tn: table_name })
)?.data?.list?.map((c) => ({ ...c, column_name: c.cn })); )?.data?.list?.map((c) => ({ ...c, column_name: c.cn }));
mapDefaultPrimaryValue(columns); mapDefaultDisplayValue(columns);
const model = await Model.insert(project.id, base.id, { const model = await Model.insert(project.id, base.id, {
table_name: table_name, table_name: table_name,
@ -826,7 +826,7 @@ export async function baseMetaDiffSync(req, res) {
await sqlClient.columnList({ tn: table_name }) await sqlClient.columnList({ tn: table_name })
)?.data?.list?.map((c) => ({ ...c, column_name: c.cn })); )?.data?.list?.map((c) => ({ ...c, column_name: c.cn }));
mapDefaultPrimaryValue(columns); mapDefaultDisplayValue(columns);
const model = await Model.insert(project.id, base.id, { const model = await Model.insert(project.id, base.id, {
table_name: table_name, 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 NcConfigFactory from '../../utils/NcConfigFactory';
import { promisify } from 'util'; import { promisify } from 'util';
import { populateMeta } from './helpers'; import { populateMeta } from './helpers';
import Filter from '../../models/Filter';
const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 4); const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 4);
@ -238,6 +239,10 @@ export async function projectCost(req, res) {
res.json({ cost }); res.json({ cost });
} }
export async function hasEmptyOrNullFilters(req, res) {
res.json(await Filter.hasEmptyOrNullFilters(req.params.projectId));
}
export default (router) => { export default (router) => {
router.get( router.get(
'/api/v1/db/meta/projects/:projectId/info', '/api/v1/db/meta/projects/:projectId/info',
@ -274,4 +279,9 @@ export default (router) => {
metaApiMetrics, metaApiMetrics,
ncMetaAclMw(projectList, 'projectList') 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