Browse Source

Merge branch 'develop' into nc-fix/links-excluded-list-modal-offset-bug

pull/7919/head
Pranav C 7 months ago committed by GitHub
parent
commit
a5f17eb2ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      packages/nc-gui/assets/nc-icons/onetoone.svg
  2. 19
      packages/nc-gui/components/cell/Checkbox.vue
  3. 11
      packages/nc-gui/components/cell/Currency.vue
  4. 21
      packages/nc-gui/components/cell/Email.vue
  5. 15
      packages/nc-gui/components/cell/Integer.vue
  6. 5
      packages/nc-gui/components/cell/MultiSelect.vue
  7. 38
      packages/nc-gui/components/cell/Percent.vue
  8. 22
      packages/nc-gui/components/cell/SingleSelect.vue
  9. 21
      packages/nc-gui/components/cell/Url.vue
  10. 17
      packages/nc-gui/components/cell/attachment/index.vue
  11. 4
      packages/nc-gui/components/dashboard/Sidebar/TopSection.vue
  12. 4
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  13. 4
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  14. 4
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  15. 10
      packages/nc-gui/components/general/FormBanner.vue
  16. 4
      packages/nc-gui/components/project/View.vue
  17. 2
      packages/nc-gui/components/smartsheet/Cell.vue
  18. 75
      packages/nc-gui/components/smartsheet/Form.vue
  19. 2
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  20. 13
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  21. 4
      packages/nc-gui/components/smartsheet/column/RollupOptions.vue
  22. 9
      packages/nc-gui/components/smartsheet/grid/Table.vue
  23. 6
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  24. 2
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts
  25. 7
      packages/nc-gui/components/smartsheet/toolbar/CreateGroupBy.vue
  26. 7
      packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue
  27. 5
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  28. 18
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  29. 12
      packages/nc-gui/components/virtual-cell/Lookup.vue
  30. 150
      packages/nc-gui/components/virtual-cell/OneToOne.vue
  31. 23
      packages/nc-gui/composables/useLTARStore.ts
  32. 4
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  33. 7
      packages/nc-gui/composables/useMultiSelect/index.ts
  34. 44
      packages/nc-gui/composables/useSharedFormViewStore.ts
  35. 11
      packages/nc-gui/composables/useSmartsheetRowStore.ts
  36. 3
      packages/nc-gui/lang/en.json
  37. 19
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue
  38. 10
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue
  39. 155
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue
  40. 4
      packages/nc-gui/store/views.ts
  41. 2
      packages/nc-gui/utils/dataUtils.ts
  42. 2
      packages/nc-gui/utils/iconUtils.ts
  43. 3
      packages/nc-gui/utils/virtualCell.ts
  44. 2
      packages/nc-lib-gui/package.json
  45. 2
      packages/nocodb-sdk/package.json
  46. 14
      packages/nocodb-sdk/pnpm-lock.yaml
  47. 27
      packages/nocodb-sdk/src/lib/globals.ts
  48. 20
      packages/nocodb/package.json
  49. 20
      packages/nocodb/src/controllers/data-alias-nested.controller.ts
  50. 2
      packages/nocodb/src/controllers/old-datas/old-datas.service.ts
  51. 10
      packages/nocodb/src/controllers/public-datas-export.controller.ts
  52. 522
      packages/nocodb/src/db/BaseModelSqlv2.ts
  53. 33
      packages/nocodb/src/db/conditionV2.ts
  54. 95
      packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts
  55. 21
      packages/nocodb/src/db/genRollupSelectv2.ts
  56. 39
      packages/nocodb/src/db/generateLookupSelectQuery.ts
  57. 2
      packages/nocodb/src/db/sortV2.ts
  58. 1
      packages/nocodb/src/db/sql-client/lib/mysql/MysqlClient.ts
  59. 2
      packages/nocodb/src/db/sql-client/lib/pg/PgClient.ts
  60. 5
      packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts
  61. 20
      packages/nocodb/src/filters/global-exception/global-exception.filter.ts
  62. 6
      packages/nocodb/src/helpers/NcPluginMgrv2.ts
  63. 2
      packages/nocodb/src/helpers/PagedResponse.ts
  64. 298
      packages/nocodb/src/helpers/catchError.ts
  65. 108
      packages/nocodb/src/helpers/columnHelpers.ts
  66. 14
      packages/nocodb/src/helpers/getAst.ts
  67. 4
      packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts
  68. 18
      packages/nocodb/src/meta/migrations/v2/nc_042_user_block.ts
  69. 2
      packages/nocodb/src/middlewares/extract-ids/extract-ids.middleware.ts
  70. 2
      packages/nocodb/src/models/Filter.ts
  71. 2
      packages/nocodb/src/models/LinkToAnotherRecordColumn.ts
  72. 2
      packages/nocodb/src/models/Model.ts
  73. 2
      packages/nocodb/src/models/Source.ts
  74. 9
      packages/nocodb/src/models/User.ts
  75. 7
      packages/nocodb/src/modules/datas/helpers.ts
  76. 16
      packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts
  77. 11
      packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts
  78. 40
      packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
  79. 6
      packages/nocodb/src/schema/swagger-v2.json
  80. 15
      packages/nocodb/src/schema/swagger.json
  81. 4
      packages/nocodb/src/services/api-docs/api-docs.service.ts
  82. 5
      packages/nocodb/src/services/api-docs/swaggerV2/templates/paths.ts
  83. 8
      packages/nocodb/src/services/base-users/base-users.service.ts
  84. 2
      packages/nocodb/src/services/bases.service.ts
  85. 18
      packages/nocodb/src/services/calendar-datas.service.ts
  86. 2
      packages/nocodb/src/services/calendars.service.ts
  87. 294
      packages/nocodb/src/services/columns.service.ts
  88. 55
      packages/nocodb/src/services/data-alias-nested.service.ts
  89. 39
      packages/nocodb/src/services/data-table.service.ts
  90. 31
      packages/nocodb/src/services/datas.service.ts
  91. 2
      packages/nocodb/src/services/forms.service.ts
  92. 2
      packages/nocodb/src/services/galleries.service.ts
  93. 2
      packages/nocodb/src/services/grids.service.ts
  94. 2
      packages/nocodb/src/services/kanbans.service.ts
  95. 2
      packages/nocodb/src/services/maps.service.ts
  96. 2
      packages/nocodb/src/services/model-visibilities.service.ts
  97. 4
      packages/nocodb/src/services/org-users.service.ts
  98. 35
      packages/nocodb/src/services/public-datas.service.ts
  99. 7
      packages/nocodb/src/services/public-metas.service.ts
  100. 8
      packages/nocodb/src/services/shared-bases.service.ts
  101. Some files were not shown because too many files have changed in this diff Show More

6
packages/nc-gui/assets/nc-icons/onetoone.svg

@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 10C11.8954 10 11 9.10457 11 8C11 6.89543 11.8954 6 13 6C14.1046 6 15 6.89543 15 8C15 9.10457 14.1046 10 13 10Z" stroke="#F3ECFA" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 10C1.89543 10 1 9.10457 1 8C1 6.89543 1.89543 6 3 6C4.10457 6 5 6.89543 5 8C5 9.10457 4.10457 10 3 10Z" stroke="#F3ECFA" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 8L11 8" stroke="#F3ECFA" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 10C11.8954 10 11 9.10457 11 8C11 6.89543 11.8954 6 13 6C14.1046 6 15 6.89543 15 8C15 9.10457 14.1046 10 13 10Z" stroke="purple" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 10C1.89543 10 1 9.10457 1 8C1 6.89543 1.89543 6 3 6C4.10457 6 5 6.89543 5 8C5 9.10457 4.10457 10 3 10Z" stroke="purple" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 8L11 8" stroke="purple" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 631 B

After

Width:  |  Height:  |  Size: 628 B

19
packages/nc-gui/components/cell/Checkbox.vue

@ -63,7 +63,7 @@ const vModel = computed<boolean | number>({
set: (val: any) => emits('update:modelValue', isMssql(column?.value?.source_id) ? +val : val),
})
function onClick(force?: boolean, event?: MouseEvent) {
function onClick(force?: boolean, event?: MouseEvent | KeyboardEvent) {
if (
(event?.target as HTMLElement)?.classList?.contains('nc-checkbox') ||
(event?.target as HTMLElement)?.closest('.nc-checkbox')
@ -75,6 +75,19 @@ function onClick(force?: boolean, event?: MouseEvent) {
}
}
const keydownEnter = (e: KeyboardEvent) => {
if (!isSurveyForm.value) {
onClick(true, e)
e.stopPropagation()
}
}
const keydownSpace = (e: KeyboardEvent) => {
if (isSurveyForm.value) {
onClick(true, e)
e.stopPropagation()
}
}
useSelectedCellKeyupListener(active, (e) => {
switch (e.key) {
case 'Enter':
@ -101,8 +114,8 @@ useSelectedCellKeyupListener(active, (e) => {
}"
:tabindex="readOnly ? -1 : 0"
@click="onClick(false, $event)"
@keydown.enter.stop="!isSurveyForm ? onClick(true, $event) : undefined"
@keydown.space.stop="isSurveyForm ? onClick(true, $event) : undefined"
@keydown.enter="keydownEnter"
@keydown.space="keydownSpace($event)"
>
<div
class="flex items-center"

11
packages/nc-gui/components/cell/Currency.vue

@ -103,12 +103,21 @@ onMounted(() => {
</script>
<template>
<div
v-if="isForm && !isEditColumn"
class="nc-currency-code h-full !bg-gray-100 border-r border-gray-200 px-3 mr-1 flex items-center"
>
<span>
{{ currencyMeta.currency_code }}
</span>
</div>
<input
v-if="!readOnly && editEnabled"
:ref="focus"
v-model="vModel"
type="number"
class="nc-cell-field w-full h-full text-sm border-none rounded-md py-1 outline-none focus:outline-none focus:ring-0"
class="nc-cell-field h-full text-sm border-none rounded-md py-1 outline-none focus:outline-none focus:ring-0"
:class="isForm && !isEditColumn ? 'flex flex-1' : 'w-full'"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="onBlur"
@keydown.enter="onKeydownEnter"

21
packages/nc-gui/components/cell/Email.vue

@ -5,7 +5,6 @@ import {
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
IsSurveyFormInj,
ReadonlyInj,
computed,
inject,
@ -31,12 +30,14 @@ const editEnabled = inject(EditModeInj)!
const column = inject(ColumnInj)!
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const readOnly = inject(ReadonlyInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
// Used in the logic of when to display error since we are not storing the email if it's not valid
const localState = ref(value)
@ -44,7 +45,7 @@ const vModel = computed({
get: () => value,
set: (val) => {
localState.value = val
if (!parseProp(column.value.meta)?.validate || (val && validateEmail(val)) || !val || isSurveyForm.value) {
if (!parseProp(column.value.meta)?.validate || (val && validateEmail(val)) || !val || isForm.value) {
emit('update:modelValue', val)
}
},
@ -52,17 +53,19 @@ const vModel = computed({
const validEmail = computed(() => vModel.value && validateEmail(vModel.value))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
watch(
() => editEnabled.value,
() => {
if (parseProp(column.value.meta)?.validate && !editEnabled.value && localState.value && !validateEmail(localState.value)) {
if (
!isForm.value &&
parseProp(column.value.meta)?.validate &&
!editEnabled.value &&
localState.value &&
!validateEmail(localState.value)
) {
message.error(t('msg.error.invalidEmail'))
localState.value = undefined
return

15
packages/nc-gui/components/cell/Integer.vue

@ -25,6 +25,10 @@ const isEditColumn = inject(EditColumnInj, ref(false))
const readOnly = inject(ReadonlyInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
const _vModel = useVModel(props, 'modelValue', emits)
const displayValue = computed(() => {
@ -42,15 +46,15 @@ const vModel = computed({
// if we clear / empty a cell in sqlite,
// the value is considered as ''
_vModel.value = null
} else if (isForm.value && !isEditColumn.value) {
_vModel.value = isNaN(Number(value)) ? value : Number(value)
} else {
_vModel.value = value
}
},
})
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
const inputType = computed(() => (isForm.value && !isEditColumn.value ? 'text' : 'number'))
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
@ -91,7 +95,7 @@ function onKeyDown(e: any) {
:ref="focus"
v-model="vModel"
class="nc-cell-field outline-none py-1 border-none w-full h-full text-sm"
type="number"
:type="inputType"
style="letter-spacing: 0.06rem"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@ -109,7 +113,8 @@ function onKeyDown(e: any) {
</template>
<style scoped lang="scss">
input[type='number']:focus {
input[type='number']:focus,
input[type='text']:focus {
@apply ring-transparent;
}

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

@ -371,6 +371,9 @@ const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
isOpen.value = false
return
} else if (e.key === 'Escape' && isForm.value) {
isOpen.value = false
return
}
e.stopPropagation()
@ -394,7 +397,7 @@ const onFocus = () => {
@click="toggleMenu"
>
<div v-if="!isEditColumn && isForm && parseProp(column.meta)?.isList" class="w-full max-w-full">
<a-checkbox-group v-model:value="vModel" :disabled="readOnly || !editAllowed" class="nc-field-layout-list">
<a-checkbox-group v-model:value="vModel" :disabled="readOnly || !editAllowed" class="nc-field-layout-list" @click.stop>
<a-checkbox
v-for="op of options"
:key="op.title"

38
packages/nc-gui/components/cell/Percent.vue

@ -24,17 +24,6 @@ const _vModel = useVModel(props, 'modelValue', emits)
const wrapperRef = ref<HTMLElement>()
const vModel = computed({
get: () => _vModel.value,
set: (value) => {
if (value === '') {
_vModel.value = null
} else {
_vModel.value = value
}
},
})
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
@ -46,6 +35,23 @@ const cellFocused = ref(false)
const expandedEditEnabled = ref(false)
const vModel = computed({
get: () => {
return isForm.value && !isEditColumn.value && _vModel.value && !cellFocused.value && !isNaN(Number(_vModel.value))
? `${_vModel.value}%`
: _vModel.value
},
set: (value) => {
if (value === '') {
_vModel.value = null
} else if (isForm.value && !isEditColumn.value) {
_vModel.value = isNaN(Number(value)) ? value : Number(value)
} else {
_vModel.value = value
}
},
})
const percentMeta = computed(() => {
return {
is_progress: false,
@ -53,6 +59,8 @@ const percentMeta = computed(() => {
}
})
const inputType = computed(() => (isForm.value && !isEditColumn.value ? 'text' : 'number'))
const onBlur = () => {
if (editEnabled) {
editEnabled.value = false
@ -106,7 +114,11 @@ const onTabPress = (e: KeyboardEvent) => {
)
for (let i = focusesNcCellIndex - 1; i >= 0; i--) {
const lastFormItem = nodes[i].querySelector('[tabindex="0"]') as HTMLElement
const node = nodes[i]
const lastFormItem = (node.querySelector('[tabindex="0"]') ??
node.querySelector('input') ??
node.querySelector('textarea') ??
node.querySelector('button')) as HTMLElement
if (lastFormItem) {
lastFormItem.focus()
break
@ -132,7 +144,7 @@ const onTabPress = (e: KeyboardEvent) => {
:ref="focus"
v-model="vModel"
class="nc-cell-field w-full !text-sm !border-none !outline-none focus:ring-0 text-base py-1"
type="number"
:type="inputType"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="onBlur"
@focus="onFocus"

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

@ -257,12 +257,26 @@ const onKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
isOpen.value = false
if (isForm.value) return
setTimeout(() => {
aselect.value?.$el.querySelector('.ant-select-selection-search > input').focus()
}, 100)
}
}
const handleKeyDownList = (e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowRight':
case 'ArrowLeft':
// skip
e.stopPropagation()
break
}
}
const onSelect = () => {
isOpen.value = false
isEditable.value = false
@ -315,7 +329,13 @@ const onFocus = () => {
@keydown.enter.stop.prevent="toggleMenu"
>
<div v-if="!isEditColumn && isForm && parseProp(column.meta)?.isList" class="w-full max-w-full">
<a-radio-group v-model:value="vModel" :disabled="readOnly || !editAllowed" class="nc-field-layout-list">
<a-radio-group
v-model:value="vModel"
:disabled="readOnly || !editAllowed"
class="nc-field-layout-list"
@keydown="handleKeyDownList"
@click.stop
>
<a-radio
v-for="op of options"
:key="op.title"

21
packages/nc-gui/components/cell/Url.vue

@ -7,7 +7,6 @@ import {
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
IsSurveyFormInj,
ReadonlyInj,
computed,
inject,
@ -42,10 +41,12 @@ const disableOverlay = inject(CellUrlDisableOverlayInj, ref(false))
const rowHeight = inject(RowHeightInj, ref(undefined))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const readOnly = inject(ReadonlyInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
// Used in the logic of when to display error since we are not storing the url if it's not valid
const localState = ref(value)
@ -53,7 +54,7 @@ const vModel = computed({
get: () => value,
set: (val) => {
localState.value = val
if (!parseProp(column.value.meta)?.validate || (val && isValidURL(val)) || !val || isSurveyForm.value) {
if (!parseProp(column.value.meta)?.validate || (val && isValidURL(val)) || !val || isForm.value) {
emit('update:modelValue', val)
}
},
@ -72,17 +73,19 @@ const url = computed(() => {
const { cellUrlOptions } = useCellUrlConfig(url)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
watch(
() => editEnabled.value,
() => {
if (parseProp(column.value.meta)?.validate && !editEnabled.value && localState.value && !isValidURL(localState.value)) {
if (
!isForm.value &&
parseProp(column.value.meta)?.validate &&
!editEnabled.value &&
localState.value &&
!isValidURL(localState.value)
) {
message.error(t('msg.error.invalidURL'))
localState.value = undefined
return

17
packages/nc-gui/components/cell/attachment/index.vue

@ -178,6 +178,19 @@ const onImageClick = (item: any) => {
selectedImage.value = item
}
const keydownEnter = (e: KeyboardEvent) => {
if (!isSurveyForm.value) {
open(e)
e.stopPropagation()
}
}
const keydownSpace = (e: KeyboardEvent) => {
if (isSurveyForm.value) {
open(e)
e.stopPropagation()
}
}
</script>
<template>
@ -211,8 +224,8 @@ const onImageClick = (item: any) => {
data-testid="attachment-cell-file-picker-button"
tabindex="0"
@click="open"
@keydown.enter="!isSurveyForm ? open($event) : undefined"
@keydown.space="isSurveyForm ? open($event) : undefined"
@keydown.enter="keydownEnter"
@keydown.space="keydownSpace"
>
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />

4
packages/nc-gui/components/dashboard/Sidebar/TopSection.vue

@ -6,7 +6,7 @@ const { isUIAllowed } = useRoles()
const { appInfo } = useGlobal()
const { meta: metaKey, ctrlKey } = useMagicKeys()
const { meta: metaKey, control } = useMagicKeys()
const { isWorkspaceLoading, isWorkspaceSettingsPageOpened } = storeToRefs(workspaceStore)
@ -17,7 +17,7 @@ const { isSharedBase } = storeToRefs(baseStore)
const isCreateProjectOpen = ref(false)
const navigateToSettings = () => {
const cmdOrCtrl = isMac() ? metaKey.value : ctrlKey.value
const cmdOrCtrl = isMac() ? metaKey.value : control.value
// TODO: Handle cloud case properly
navigateToWorkspaceSettings('', cmdOrCtrl)

4
packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue

@ -73,7 +73,7 @@ const { orgRoles, isUIAllowed } = useRoles()
useTabs()
const { meta: metaKey, ctrlKey } = useMagicKeys()
const { meta: metaKey, control } = useMagicKeys()
const { refreshCommandPalette } = useCommandPalette()
@ -261,7 +261,7 @@ const onProjectClick = async (base: NcProject, ignoreNavigation?: boolean, toggl
if (!base) {
return
}
const cmdOrCtrl = isMac() ? metaKey.value : ctrlKey.value
const cmdOrCtrl = isMac() ? metaKey.value : control.value
if (!toggleIsExpanded && !cmdOrCtrl) $e('c:base:open')

4
packages/nc-gui/components/dashboard/TreeView/TableNode.vue

@ -39,7 +39,7 @@ useTableNew({
baseId: base.value.id!,
})
const { meta: metaKey, ctrlKey } = useMagicKeys()
const { meta: metaKey, control } = useMagicKeys()
const baseRole = inject(ProjectRoleInj)
provide(SidebarTableInj, table)
@ -108,7 +108,7 @@ const onExpand = async () => {
}
const onOpenTable = async () => {
if (isMac() ? metaKey.value : ctrlKey.value) {
if (isMac() ? metaKey.value : control.value) {
await _openTable(table.value, true)
return
}

4
packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue

@ -53,7 +53,7 @@ const { activeView } = storeToRefs(useViewsStore())
const { getMeta } = useMetas()
const { meta: metaKey, ctrlKey } = useMagicKeys()
const { meta: metaKey, control } = useMagicKeys()
const table = computed(() => props.table)
const injectedTable = ref(table.value)
@ -89,7 +89,7 @@ const onClick = useDebounceFn(() => {
const handleOnClick = () => {
if (isEditing.value || isStopped.value) return
const cmdOrCtrl = isMac() ? metaKey.value : ctrlKey.value
const cmdOrCtrl = isMac() ? metaKey.value : control.value
if (cmdOrCtrl) {
emits('changeView', vModel.value)

10
packages/nc-gui/components/general/FormBanner.vue

@ -7,6 +7,10 @@ interface Props {
const { bannerImageUrl } = defineProps<Props>()
const { getPossibleAttachmentSrc } = useAttachment()
const getBannerImageSrc = computed(() => {
return getPossibleAttachmentSrc(parseProp(bannerImageUrl))
})
</script>
<template>
@ -15,11 +19,7 @@ const { getPossibleAttachmentSrc } = useAttachment()
:class="!bannerImageUrl ? 'shadow-sm' : ''"
:style="{ aspectRatio: 4 / 1 }"
>
<LazyCellAttachmentImage
v-if="bannerImageUrl"
:srcs="getPossibleAttachmentSrc(parseProp(bannerImageUrl))"
class="nc-form-banner-image object-cover w-full"
/>
<LazyCellAttachmentImage v-if="bannerImageUrl" :srcs="getBannerImageSrc" class="nc-form-banner-image object-cover w-full" />
<div v-else class="h-full flex items-stretch justify-between bg-white">
<div class="flex -mt-1">
<img src="~assets/img/form-banner-left.png" alt="form-banner-left'" />

4
packages/nc-gui/components/project/View.vue

@ -20,7 +20,7 @@ const { $e } = useNuxtApp()
return openedProject.value?.sources?.[0]
}) */
const { isUIAllowed } = useRoles()
const { isUIAllowed, baseRoles } = useRoles()
const { base } = storeToRefs(useBase())
@ -121,7 +121,7 @@ watch(
<!-- <a-tab-pane v-if="defaultBase" key="erd" tab="Base ERD" force-render class="pt-4 pb-12">
<ErdView :source-id="defaultBase!.id" class="!h-full" />
</a-tab-pane> -->
<a-tab-pane v-if="isUIAllowed('newUser')" key="collaborator">
<a-tab-pane v-if="isUIAllowed('newUser', { roles: baseRoles })" key="collaborator">
<template #tab>
<div class="tab-title" data-testid="proj-view-tab__access-settings">
<GeneralIcon icon="users" class="!h-3.5 !w-3.5" />

2
packages/nc-gui/components/smartsheet/Cell.vue

@ -196,7 +196,7 @@ onUnmounted(() => {
{
'text-brand-500': isPrimary(column) && !props.virtual && !isForm && !isCalendar,
'nc-grid-numeric-cell-right': isGrid && isNumericField && !isEditColumnMenu && !isForm && !isExpandedFormOpen,
'h-10': isForm && !isSurveyForm && !isAttachment(column) && !isTextArea(column) && !isJSON(column) && !props.virtual,
'h-10': !isEditColumnMenu && isForm && !isAttachment(column) && !isTextArea(column) && !isJSON(column) && !props.virtual,
'nc-grid-numeric-cell-left': (isForm && isNumericField && isExpandedFormOpen) || isEditColumnMenu,
'!min-h-30': isTextArea(column) && (isForm || isSurveyForm),
},

75
packages/nc-gui/components/smartsheet/Form.vue

@ -3,6 +3,8 @@ import Draggable from 'vuedraggable'
import tinycolor from 'tinycolor2'
import { Pane, Splitpanes } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'
import type { FormItemProps } from 'ant-design-vue'
import {
type AttachmentResType,
ProjectRoles,
@ -186,6 +188,8 @@ const { open, onChange: onChangeFile } = useFileDialog({
const visibleColumns = computed(() => localColumns.value.filter((f) => f.show).sort((a, b) => a.order - b.order))
const getFormLogoSrc = computed(() => getPossibleAttachmentSrc(parseProp(formViewData.value.logo_url)))
const updateView = useDebounceFn(
() => {
updateFormView(formViewData.value)
@ -546,6 +550,50 @@ const handleOnUploadImage = (data: AttachmentResType = null) => {
updateView()
}
const validateFormEmail = async (_rule, value) => {
if (!value) {
return Promise.resolve()
} else if (!validateEmail(value)) {
return Promise.reject(t('msg.error.invalidEmail'))
}
}
const validateFormURL = async (_rule, value) => {
if (!value) {
return Promise.resolve()
} else if (!isValidURL(value)) {
return Promise.reject(t('msg.error.invalidURL'))
}
}
const formElementValidationRules = (element) => {
const rules: FormItemProps['rules'][] = [
{
required: isRequired(element, element.required),
message: t('msg.error.fieldRequired', { value: 'This field' }),
},
]
if (parseProp(element.meta).validate && element.uidt === UITypes.URL) {
rules.push({
validator: validateFormURL,
})
} else if (parseProp(element.meta).validate && element.uidt === UITypes.Email) {
rules.push({
validator: validateFormEmail,
})
}
if ([UITypes.Number, UITypes.Currency, UITypes.Percent].includes(element.uidt)) {
rules.push({
type: 'number',
message: t('msg.plsEnterANumber'),
})
}
return rules
}
onClickOutside(draggableRef, (e) => {
if (
(e.target as HTMLElement)?.closest(
@ -736,7 +784,7 @@ useEventListener(
<div class="flex justify-center">
<div class="w-full">
<a-alert class="!my-4 !py-4 text-left !rounded-lg" type="success" outlined>
<a-alert class="nc-form-success-msg !my-4 !py-4 text-left !rounded-lg" type="success" outlined>
<template #message>
<LazyCellRichText
v-if="formViewData?.success_msg?.trim()"
@ -806,7 +854,7 @@ useEventListener(
></GeneralImageCropper>
<!-- cover image -->
<div v-if="!parseProp(formViewData?.meta).hide_banner" class="group relative max-w-[max(33%,688px)] mx-auto">
<GeneralFormBanner :banner-image-url="formViewData.banner_image_url" />
<GeneralFormBanner :key="formViewData.banner_image_url?.path" :banner-image-url="formViewData.banner_image_url" />
<div class="absolute bottom-0 right-0 hidden group-hover:block">
<div class="flex items-center space-x-1 m-2">
<NcTooltip :disabled="isEeUI">
@ -880,7 +928,8 @@ useEventListener(
>
<LazyCellAttachmentImage
v-if="formViewData.logo_url"
:srcs="getPossibleAttachmentSrc(parseProp(formViewData.logo_url))"
:key="formViewData.logo_url?.path"
:srcs="getFormLogoSrc"
class="flex-none nc-form-logo !object-contain object-left max-h-full max-w-full !m-0"
/>
<div
@ -973,8 +1022,7 @@ useEventListener(
:bordered="false"
:data-testid="NcForm.heading"
:data-title="NcForm.heading"
@blur="updateView"
@keydown.enter="updateView"
@update:value="updateView"
/>
</a-form-item>
@ -1199,12 +1247,7 @@ useEventListener(
<a-form-item
:name="element.title"
class="!my-0 nc-input-required-error nc-form-input-item"
:rules="[
{
required: isRequired(element, element.required),
message: `${$t('msg.error.fieldRequired', { value: 'This field' })}`,
},
]"
:rules="formElementValidationRules(element)"
>
<LazySmartsheetDivDataCell
class="relative"
@ -1725,6 +1768,9 @@ useEventListener(
&.nc-cell-geodata {
@apply !py-1;
}
&.nc-cell-currency {
@apply !py-0 !pl-0 flex items-stretch;
}
:deep(input) {
@apply !px-1;
@ -1732,8 +1778,11 @@ useEventListener(
&.nc-cell-longtext {
@apply p-0 h-auto;
}
&:not(.nc-cell-longtext) {
@apply px-2 py-2;
&.nc-cell:not(.nc-cell-longtext) {
@apply p-2;
}
&.nc-virtual-cell {
@apply px-2 py-1;
}
&.nc-cell-json {

2
packages/nc-gui/components/smartsheet/VirtualCell.vue

@ -20,6 +20,7 @@ import {
isLink,
isLookup,
isMm,
isOo,
isPrimary,
isQrCode,
isRollup,
@ -111,6 +112,7 @@ onUnmounted(() => {
<LazyVirtualCellHasMany v-else-if="isHm(column)" />
<LazyVirtualCellManyToMany v-else-if="isMm(column)" />
<LazyVirtualCellBelongsTo v-else-if="isBt(column)" />
<LazyVirtualCellOneToOne v-else-if="isOo(column)" />
<LazyVirtualCellRollup v-else-if="isRollup(column)" />
<LazyVirtualCellFormula v-else-if="isFormula(column)" />
<LazyVirtualCellQrCode v-else-if="isQrCode(column)" />

13
packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ModelTypes, MssqlUi, SqliteUi, UITypes } from 'nocodb-sdk'
import { ModelTypes, MssqlUi, RelationTypes, SqliteUi, UITypes } from 'nocodb-sdk'
import { MetaInj, inject, ref, storeToRefs, useBase, useVModel } from '#imports'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
@ -52,16 +52,19 @@ const refTables = computed(() => {
const filterOption = (value: string, option: { key: string }) => option.key.toLowerCase().includes(value.toLowerCase())
const isLinks = computed(() => vModel.value.uidt === UITypes.Links)
const isLinks = computed(() => vModel.value.uidt === UITypes.Links && vModel.value.type !== RelationTypes.ONE_TO_ONE)
const oneToOneEnabled = ref(false)
</script>
<template>
<div class="w-full flex flex-col mb-2 mt-4">
<div class="border-2 p-6">
<a-form-item v-bind="validateInfos.type" class="nc-ltar-relation-type">
<a-radio-group v-model:value="vModel.type" name="type" v-bind="validateInfos.type">
<a-radio value="hm">{{ $t('title.hasMany') }}</a-radio>
<a-radio-group v-model:value="vModel.type" name="type" v-bind="validateInfos.type" class="!flex flex-col gap-2">
<a-radio value="hm" @dblclick="oneToOneEnabled = !oneToOneEnabled">{{ $t('title.hasMany') }}</a-radio>
<a-radio value="mm">{{ $t('title.manyToMany') }}</a-radio>
<a-radio v-if="oneToOneEnabled" value="oo">{{ $t('title.oneToOne') }}</a-radio>
</a-radio-group>
</a-form-item>
@ -102,7 +105,7 @@ const isLinks = computed(() => vModel.value.uidt === UITypes.Links)
</div>
<div v-if="advancedOptions" class="flex flex-col p-6 gap-4 border-2 mt-2">
<LazySmartsheetColumnLinkOptions v-model:value="vModel" class="-my-2" />
<LazySmartsheetColumnLinkOptions v-if="isLinks" v-model:value="vModel" class="-my-2" />
<template v-if="!isXcdbBase">
<div class="flex flex-row space-x-2">
<a-form-item class="flex w-1/2" :label="$t('labels.onUpdate')">

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import { onMounted } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, TableType, UITypes } from 'nocodb-sdk'
import { type ColumnType, type LinkToAnotherRecordType, RelationTypes, type TableType, type UITypes } from 'nocodb-sdk'
import { getAvailableRollupForUiType, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from '#imports'
import {
@ -58,7 +58,7 @@ const refTables = computed(() => {
.filter(
(c) =>
isLinksOrLTAR(c) &&
(c.colOptions as LinkToAnotherRecordType).type !== 'bt' &&
![RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes((c.colOptions as LinkToAnotherRecordType).type) &&
!c.system &&
c.source_id === meta.value?.source_id,
)

9
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -35,6 +35,7 @@ import {
isEeUI,
isMac,
isMm,
isOo,
message,
onClickOutside,
onMounted,
@ -294,7 +295,7 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate =
columnObj.id === col.id
) {
if (rowRefs.value) {
if (isBt(columnObj)) {
if (isBt(columnObj) || isOo(columnObj)) {
rowObj.row[columnObj.title] = row.row[columnObj.title]
await rowRefs.value[ctx.row]!.addLTARRef(rowObj.row[columnObj.title], columnObj)
@ -335,7 +336,7 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate =
const columnObj = fields.value[ctx.col]
if (rowId === extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]) && columnObj.id === col.id) {
if (rowRefs.value) {
if (isBt(columnObj)) {
if (isBt(columnObj) || isOo(columnObj)) {
await rowRefs.value[ctx.row]!.clearLTARCell(columnObj)
} else if (isMm(columnObj)) {
await rowRefs.value[ctx.row]!.cleaMMCell(columnObj)
@ -357,7 +358,7 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate =
},
scope: defineViewScope({ view: view.value }),
})
if (isBt(columnObj) && rowRefs.value) await rowRefs.value[ctx.row]!.clearLTARCell(columnObj)
if ((isBt(columnObj) || isOo(columnObj)) && rowRefs.value) await rowRefs.value[ctx.row]!.clearLTARCell(columnObj)
return
}
@ -928,7 +929,7 @@ async function clearSelectedRangeOfCells() {
// TODO handle LinkToAnotherRecord
if (isVirtualCol(col)) {
if ((isBt(col) || isMm(col)) && !isInfoShown) {
if ((isBt(col) || isOo(col) || isMm(col)) && !isInfoShown) {
message.info(t('msg.info.groupClearIsNotSupportedOnLinksColumn'))
isInfoShown = true
}

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { ColumnReqType, ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType } from 'nocodb-sdk'
import { UITypes, UITypesName, substituteColumnIdWithAliasInFormula } from 'nocodb-sdk'
import { RelationTypes, UITypes, UITypesName, substituteColumnIdWithAliasInFormula } from 'nocodb-sdk'
import {
ColumnInj,
IsFormInj,
@ -122,6 +122,10 @@ const columnTypeName = computed(() => {
if (column.value.uidt === UITypes.LongText && parseProp(column?.value?.meta)?.richMode) {
return UITypesName.RichText
}
if (column.value.uidt === UITypes.LinkToAnotherRecord && column.value.colOptions?.type === RelationTypes.ONE_TO_ONE) {
return UITypesName[UITypes.Links]
}
return column.value.uidt ? UITypesName[column.value.uidt] : ''
})

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

@ -30,6 +30,8 @@ const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => {
return { icon: iconMap.hm_solid }
case RelationTypes.BELONGS_TO:
return { icon: iconMap.bt_solid }
case RelationTypes.ONE_TO_ONE:
return { icon: iconMap.oneToOneSolid, color: 'text-blue-500' }
}
break
case UITypes.SpecificDBType:

7
packages/nc-gui/components/smartsheet/toolbar/CreateGroupBy.vue

@ -50,7 +50,12 @@ const options = computed<ColumnType[]>(
return false
} else {
/** ignore hasmany and manytomany relations if it's using within group menu */
return !(isLinksOrLTAR(c) && (c.colOptions as LinkToAnotherRecordType).type !== RelationTypes.BELONGS_TO)
return !(
isLinksOrLTAR(c) &&
![RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes(
(c.colOptions as LinkToAnotherRecordType).type as RelationTypes,
)
)
}
})
.filter((c: ColumnType) => !groupBy.value.find((g) => g.column?.id === c.id))

7
packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue

@ -46,7 +46,12 @@ const options = computed<ColumnType[]>(
return false
} else {
/** ignore hasmany and manytomany relations if it's using within sort menu */
return !(isLinksOrLTAR(c) && (c.colOptions as LinkToAnotherRecordType).type !== RelationTypes.BELONGS_TO)
return !(
isLinksOrLTAR(c) &&
![RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes(
(c.colOptions as LinkToAnotherRecordType).type as RelationTypes,
)
)
/** ignore virtual fields which are system fields ( mm relation ) and qr code fields */
}
})

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

@ -67,7 +67,10 @@ const availableColumns = computed(() => {
return false
} else {
/** ignore hasmany and manytomany relations if it's using within sort menu */
return !(isLinksOrLTAR(c) && (c.colOptions as LinkToAnotherRecordType).type !== RelationTypes.BELONGS_TO)
return !(
isLinksOrLTAR(c) &&
![RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes((c.colOptions as LinkToAnotherRecordType).type)
)
/** ignore virtual fields which are system fields ( mm relation ) and qr code fields */
}
})

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

@ -42,12 +42,8 @@ const listItemsDlg = ref(false)
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>,
row,
isNew,
reloadRowTrigger.trigger,
)
const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, relatedTableDisplayValuePropId, unlink } =
useProvideLTARStore(column as Ref<Required<ColumnType>>, row, isNew, reloadRowTrigger.trigger)
await loadRelatedTableMeta()
@ -96,10 +92,14 @@ watch([listItemsDlg], () => {
<template>
<div class="flex w-full chips-wrapper items-center" :class="{ active }">
<div class="nc-cell-field chips flex items-center flex-1">
<template v-if="value && relatedTableDisplayValueProp">
<template v-if="value && (relatedTableDisplayValueProp || relatedTableDisplayValuePropId)">
<VirtualCellComponentsItemChip
:item="value"
:value="!Array.isArray(value) && typeof value === 'object' ? value[relatedTableDisplayValueProp] : value"
:value="
!Array.isArray(value) && typeof value === 'object'
? value[relatedTableDisplayValueProp] ?? value[relatedTableDisplayValuePropId]
: value
"
:column="belongsToColumn"
:show-unlink-button="true"
@unlink="unlinkRef(value)"
@ -116,7 +116,7 @@ watch([listItemsDlg], () => {
>
<GeneralIcon
:icon="addIcon"
class="text-sm nc-action-icon group-focus:visible invisible text-gray-500/50 hover:text-gray-500 select-none group-hover:(text-gray-500) nc-plus"
class="select-none !text-md text-gray-700 nc-action-icon nc-plus invisible group-hover:visible group-focus:visible"
@click.stop="listItemsDlg = true"
/>
</div>

12
packages/nc-gui/components/virtual-cell/Lookup.vue

@ -77,9 +77,12 @@ watch([lookupColumn, rowHeight], () => {
const arrValue = computed(() => {
if (!cellValue.value) return []
// if lookup column is Attachment and relation type is Belongs to wrap the value in an array
// if lookup column is Attachment and relation type is Belongs/OneToOne to wrap the value in an array
// since the attachment component expects an array or JSON string array
if (lookupColumn.value?.uidt === UITypes.Attachment && relationColumn.value?.colOptions?.type === RelationTypes.BELONGS_TO)
if (
lookupColumn.value?.uidt === UITypes.Attachment &&
[RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes(relationColumn.value?.colOptions?.type)
)
return [cellValue.value]
// TODO: We are filtering null as cell value can be null. Find the root cause and fix it
@ -114,11 +117,12 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
<template v-if="lookupColumn">
<!-- Render virtual cell -->
<div v-if="isVirtualCol(lookupColumn)" class="flex h-full">
<!-- If non-belongs-to LTAR column then pass the array value, else iterate and render -->
<!-- If non-belongs-to and non-one-to-one LTAR column then pass the array value, else iterate and render -->
<template
v-if="
lookupColumn.uidt !== UITypes.LinkToAnotherRecord ||
(lookupColumn.uidt === UITypes.LinkToAnotherRecord && lookupColumn.colOptions.type === RelationTypes.BELONGS_TO)
(lookupColumn.uidt === UITypes.LinkToAnotherRecord &&
[RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes(lookupColumn.colOptions.type))
"
>
<LazySmartsheetVirtualCell

150
packages/nc-gui/components/virtual-cell/OneToOne.vue

@ -0,0 +1,150 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import {
ActiveCellInj,
CellValueInj,
ColumnInj,
IsFormInj,
IsUnderLookupInj,
ReadonlyInj,
ReloadRowDataHookInj,
RowInj,
computed,
createEventHook,
inject,
ref,
useProvideLTARStore,
useRoles,
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow,
} from '#imports'
const column = inject(ColumnInj)!
const reloadRowTrigger = inject(ReloadRowDataHookInj, createEventHook())
const cellValue = inject(CellValueInj, ref<any>(null))
const row = inject(RowInj)!
const active = inject(ActiveCellInj)!
const readOnly = inject(ReadonlyInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const { isUIAllowed } = useRoles()
const listItemsDlg = ref(false)
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, relatedTableDisplayValuePropId, unlink } =
useProvideLTARStore(column as Ref<Required<ColumnType>>, row, isNew, reloadRowTrigger.trigger)
await loadRelatedTableMeta()
const addIcon = computed(() => (cellValue?.value ? 'expand' : 'plus'))
const value = computed(() => {
if (cellValue?.value) {
return cellValue?.value
} else if (isNew.value) {
return state?.value?.[column?.value.title as string]
}
return null
})
const unlinkRef = async (rec: Record<string, any>) => {
if (isNew.value) {
await removeLTARRef(rec, column?.value as ColumnType)
} else {
await unlink(rec)
}
}
useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
listItemsDlg.value = true
e.stopPropagation()
break
}
})
const belongsToColumn = computed(
() =>
relatedTableMeta.value?.columns?.find((c: any) => c.title === relatedTableDisplayValueProp.value) as ColumnType | undefined,
)
const plusBtnRef = ref<HTMLElement | null>(null)
watch([listItemsDlg], () => {
if (!listItemsDlg.value) {
plusBtnRef.value?.focus()
}
})
</script>
<template>
<div class="flex w-full chips-wrapper items-center" :class="{ active }">
<div class="nc-cell-field chips flex items-center flex-1">
<template v-if="value && (relatedTableDisplayValueProp || relatedTableDisplayValuePropId)">
<VirtualCellComponentsItemChip
:item="value"
:value="
!Array.isArray(value) && typeof value === 'object'
? value[relatedTableDisplayValueProp] ?? value[relatedTableDisplayValuePropId]
: value
"
:column="belongsToColumn"
:show-unlink-button="true"
@unlink="unlinkRef(value)"
/>
</template>
</div>
<div
v-if="!readOnly && (isUIAllowed('dataEdit') || isForm) && !isUnderLookup"
ref="plusBtnRef"
class="flex justify-end group gap-1 min-h-[30px] items-center"
tabindex="0"
@keydown.enter.stop="listItemsDlg = true"
>
<GeneralIcon
:icon="addIcon"
class="select-none !text-md text-gray-700 nc-action-icon nc-plus invisible group-hover:visible group-focus:visible"
@click.stop="listItemsDlg = true"
/>
</div>
<LazyVirtualCellComponentsUnLinkedItems
v-if="listItemsDlg"
v-model="listItemsDlg"
:column="belongsToColumn"
@attach-record="listItemsDlg = true"
/>
</div>
</template>
<style scoped lang="scss">
.nc-action-icon {
@apply cursor-pointer;
}
.chips-wrapper:hover,
.chips-wrapper.active {
.nc-action-icon {
@apply inline-block;
}
}
.chips-wrapper:hover {
.nc-action-icon {
@apply visible;
}
}
</style>

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

@ -2,11 +2,10 @@ import type {
type ColumnType,
type LinkToAnotherRecordType,
type PaginatedType,
RelationTypes,
type RequestParams,
type TableType,
} from 'nocodb-sdk'
import { UITypes, dateFormats, parseStringDateTime, timeFormats } from 'nocodb-sdk'
import { RelationTypes, UITypes, dateFormats, parseStringDateTime, timeFormats } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import type { Row } from '#imports'
import {
@ -121,7 +120,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const getRelatedTableRowId = (row: Record<string, any>) => {
return relatedTableMeta.value?.columns
?.filter((c) => c.pk)
.map((c) => row?.[c.title as string])
.map((c) => row?.[c.title as string] ?? row?.[c.id as string])
.join('___')
}
@ -135,6 +134,11 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
return (relatedTableMeta.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title || ''
})
// todo: temp fix, handle in backend
const relatedTableDisplayValuePropId = computed(() => {
return (relatedTableMeta.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.id || ''
})
const relatedTablePrimaryKeyProps = computed(() => {
return relatedTableMeta.value?.columns?.filter((c) => c.pk)?.map((c) => c.title) ?? []
})
@ -187,7 +191,6 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const loadChildrenExcludedList = async (activeState?: any) => {
if (activeState) newRowState.state = activeState
try {
// todo: confirm the use case of `childrenExcludedOffsetCount.value`
let offset = childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1) - childrenExcludedOffsetCount.value
if (offset < 0) {
@ -279,6 +282,12 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
})
}
} catch (e: any) {
// temporary fix to handle when offset is beyond limit
if ((await extractSdkResponseErrorMsg(e)) === 'Offset is beyond the total number of records') {
childrenExcludedListPagination.page = 0
return loadChildrenExcludedList(activeState)
}
message.error(`${t('msg.error.failedToLoadList')}: ${await extractSdkResponseErrorMsg(e)}`)
} finally {
isChildrenExcludedLoading.value = false
@ -288,7 +297,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const loadChildrenList = async () => {
try {
isChildrenLoading.value = true
if (colOptions.value.type === 'bt') return
if ([RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes(colOptions.value.type)) return
if (!rowId.value || !column.value) return
let offset = childrenListPagination.size * (childrenListPagination.page - 1) + childrenListOffsetCount.value
@ -439,7 +448,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
}
isChildrenExcludedListLinked.value[index] = false
isChildrenListLinked.value[index] = false
if (colOptions.value.type !== 'bt') {
if (colOptions.value.type !== RelationTypes.BELONGS_TO && colOptions.value.type !== RelationTypes.ONE_TO_ONE) {
childrenListCount.value = childrenListCount.value - 1
}
} catch (e: any) {
@ -506,7 +515,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
isChildrenExcludedListLinked.value[index] = true
isChildrenListLinked.value[index] = true
if (colOptions.value.type !== 'bt') {
if (colOptions.value.type !== RelationTypes.BELONGS_TO && colOptions.value.type !== RelationTypes.ONE_TO_ONE) {
childrenListCount.value = childrenListCount.value + 1
} else {
isChildrenExcludedListLinked.value = Array(childrenExcludedList.value?.list.length).fill(false)

4
packages/nc-gui/composables/useMultiSelect/convertCellData.ts

@ -2,7 +2,7 @@ import dayjs from 'dayjs'
import type { AttachmentType, ColumnType, LinkToAnotherRecordType, SelectOptionsType } from 'nocodb-sdk'
import { UITypes, getDateFormat, getDateTimeFormat, populateUniqueFileName } from 'nocodb-sdk'
import type { AppInfo } from '~/composables/useGlobal'
import { isBt, isMm, parseProp } from '#imports'
import { isBt, isMm, isOo, parseProp } from '#imports'
export default function convertCellData(
args: { to: UITypes; value: string; column: ColumnType; appInfo: AppInfo; files?: FileList | File[]; oldValue?: unknown },
@ -250,7 +250,7 @@ export default function convertCellData(
return undefined
}
if (isBt(column)) {
if (isBt(column) || isOo(column)) {
const parsedVal = typeof value === 'string' ? JSON.parse(value) : value
if (

7
packages/nc-gui/composables/useMultiSelect/index.ts

@ -33,6 +33,7 @@ import {
isExpandedCellInputExist,
isMac,
isMm,
isOo,
isTypableInputColumn,
message,
parseProp,
@ -158,7 +159,7 @@ export function useMultiSelect(
}
}
if (isBt(columnObj)) {
if (isBt(columnObj) || isOo(columnObj)) {
// fk_related_model_id is used to prevent paste operation in different fk_related_model_id cell
textToCopy = {
fk_related_model_id: (columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id,
@ -859,7 +860,7 @@ export function useMultiSelect(
const pasteCol = colsToPaste[j]
if (!isPasteable(pasteRow, pasteCol)) {
if ((isBt(pasteCol) || isMm(pasteCol)) && !isInfoShown) {
if ((isBt(pasteCol) || isOo(pasteCol) || isMm(pasteCol)) && !isInfoShown) {
message.info(t('msg.info.groupPasteIsNotSupportedOnLinksColumn'))
isInfoShown = true
}
@ -1163,7 +1164,7 @@ export function useMultiSelect(
for (const col of cols) {
if (!col.title || !isPasteable(row, col)) {
if ((isBt(col) || isMm(col)) && !isInfoShown) {
if ((isBt(col) || isOo(pasteCol) || isMm(col)) && !isInfoShown) {
message.info(t('msg.info.groupPasteIsNotSupportedOnLinksColumn'))
isInfoShown = true
}

44
packages/nc-gui/composables/useSharedFormViewStore.ts

@ -12,7 +12,7 @@ import type {
StringOrNullType,
TableType,
} from 'nocodb-sdk'
import { ErrorMessages, RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { isString } from '@vue/shared'
import { filterNullOrUndefinedObjectProperties } from '~/helpers/parsers/parserHelpers'
import {
@ -22,6 +22,7 @@ import {
createEventHook,
extractSdkResponseErrorMsg,
isNumericFieldType,
isValidURL,
message,
parseProp,
provide,
@ -34,6 +35,7 @@ import {
useMetas,
useProvideSmartsheetRowStore,
useViewsStore,
validateEmail,
watch,
} from '#imports'
import type { SharedViewMeta } from '#imports'
@ -138,7 +140,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
c?.cdf &&
!/^\w+\(\)|CURRENT_TIMESTAMP$/.test(c.cdf)
) {
formState.value[c.title] = c.cdf
formState.value[c.title] = typeof c.cdf === 'string' ? c.cdf.replace(/^'|'$/g, '') : c.cdf
}
return {
@ -176,7 +178,8 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
} catch (e: any) {
if (e.response && e.response.status === 404) {
notFound.value = true
} else if ((await extractSdkResponseErrorMsg(e)) === ErrorMessages.INVALID_SHARED_VIEW_PASSWORD) {
// TODO - handle invalidSharedViewPassword
} else if (await extractSdkResponseErrorMsg(e)) {
passwordDlg.value = true
if (password.value && password.value !== '') passwordError.value = 'Something went wrong. Please check your credentials.'
@ -197,7 +200,9 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
!isVirtualCol(column) &&
((column.rqd && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || column.required)
) {
obj.localState[column.title!] = { required: fieldRequired() }
obj.localState[column.title!] = {
required: fieldRequired(),
}
} else if (
isLinksOrLTAR(column) &&
column.colOptions &&
@ -216,6 +221,37 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
required: fieldRequired(),
}
}
if (
!isVirtualCol(column) &&
parseProp(column.meta)?.validate &&
[UITypes.URL, UITypes.Email].includes(column.uidt as UITypes)
) {
if (column.uidt === UITypes.URL) {
obj.localState[column.title!] = {
...(obj.localState[column.title!] || {}),
validateFormURL: helpers.withMessage(t('msg.error.invalidURL'), (value) => {
return value ? isValidURL(value) : true
}),
}
} else if (column.uidt === UITypes.Email) {
obj.localState[column.title!] = {
...(obj.localState[column.title!] || {}),
validateFormEmail: helpers.withMessage(t('msg.error.invalidEmail'), (value) => {
return value ? validateEmail(value) : true
}),
}
}
}
if ([UITypes.Number, UITypes.Currency, UITypes.Percent].includes(column.uidt as UITypes)) {
obj.localState[column.title!] = {
...(obj.localState[column.title!] || {}),
validateFormNumber: helpers.withMessage(t('msg.plsEnterANumber'), (value) => {
return value ? (column.uidt === UITypes.Number ? /^\d+$/.test(value) : /^\d*\.?\d+$/.test(value)) : true
}),
}
}
}
return obj

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

@ -11,6 +11,7 @@ import {
isBt,
isHm,
isMm,
isOo,
message,
ref,
storeToRefs,
@ -56,7 +57,7 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
} else {
state.value[column.title!]!.push(value)
}
} else if (isBt(column)) {
} else if (isBt(column) || isOo(column)) {
state.value[column.title!] = value
}
}
@ -65,7 +66,7 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
const removeLTARRef = async (value: Record<string, any>, column: ColumnType) => {
if (isHm(column) || isMm(column)) {
state.value[column.title!]?.splice(state.value[column.title!]?.indexOf(value), 1)
} else if (isBt(column)) {
} else if (isBt(column) || isOo(column)) {
state.value[column.title!] = null
}
}
@ -114,7 +115,7 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
{ metaValue },
)
}
} else if (isBt(column) && state.value?.[column.title!]) {
} else if ((isBt(column) || isOo(column)) && state.value?.[column.title!]) {
await linkRecord(
id,
extractPkFromRow(state.value?.[column.title!] as Record<string, any>, relatedTableMeta.columns as ColumnType[]),
@ -139,14 +140,14 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
if (isNew.value) {
state.value[column.title!] = null
} else if (currentRow.value) {
if ((<LinkToAnotherRecordType>column.colOptions)?.type === RelationTypes.BELONGS_TO) {
if ([RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes((<LinkToAnotherRecordType>column.colOptions)?.type)) {
if (!currentRow.value.row[column.title!]) return
await $api.dbTableRow.nestedRemove(
NOCO,
base.value.id as string,
meta.value?.id as string,
extractPkFromRow(currentRow.value.row, meta.value?.columns as ColumnType[]),
'bt' as any,
(<LinkToAnotherRecordType>column.colOptions)?.type as any,
column.id as string,
extractPkFromRow(currentRow.value.row[column.title!], relatedTableMeta?.columns as ColumnType[]),
)

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

@ -339,6 +339,7 @@
"removeFile": "Remove File",
"hasMany": "Has Many",
"manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
@ -698,7 +699,7 @@
"hideNocodbBranding": "Hide NocoDB Branding",
"showOnConditions": "Show on condtions",
"showFieldOnConditionsMet": "Shows field only when conditions are met",
"limitOptions": "Limit ptions",
"limitOptions": "Limit options",
"limitOptionsSubtext": "Limit options visible to users by selecting available options",
"clearSelection": "Clear selection"
},

19
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue

@ -28,7 +28,7 @@ router.afterEach((to) => shouldRedirect(to.name as string))
<template>
<div
class="scrollbar-thin-dull h-[100vh] overflow-y-auto overflow-x-hidden color-transition p-4 lg:p-10 nc-form-view relative min-h-[600px]"
class="scrollbar-thin scrollbar-track-transparent scrollbar-thumb-gray-200 hover-scrollbar-thumb-gray-300 h-[100vh] overflow-y-auto overflow-x-hidden flex flex-col color-transition p-4 lg:p-10 nc-form-view min-h-[600px]"
:class="{
'children:(!h-auto my-auto)': sharedViewMeta?.surveyMode,
}"
@ -62,7 +62,8 @@ p {
}
}
.nc-cell {
.nc-cell,
.nc-virtual-cell {
@apply bg-white dark:bg-slate-500 appearance-none;
&.nc-cell-checkbox {
@ -85,7 +86,7 @@ p {
@apply bg-white dark:bg-slate-500;
&.nc-input {
@apply w-full;
@apply w-full h-10;
&:not(.layout-list) {
@apply rounded-lg border-solid border-1 border-gray-200 focus-within:border-brand-500 overflow-hidden;
@ -162,9 +163,12 @@ p {
@apply px-3;
}
}
&:not(.nc-cell-longtext) {
&.nc-cell:not(.nc-cell-longtext) {
@apply p-2;
}
&.nc-virtual-cell {
@apply px-2 py-1;
}
&.nc-cell-json {
@apply h-auto;
@ -177,6 +181,13 @@ p {
input.nc-cell-field {
@apply !py-0 !px-1;
}
&.nc-cell-currency {
@apply !py-0 !pl-0 flex items-stretch;
.nc-currency-code {
@apply !bg-gray-100;
}
}
}
}

10
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue

@ -113,7 +113,7 @@ const onDecode = async (scannedCodeValue: string) => {
<template v-else-if="submitted">
<div class="flex justify-center">
<div v-if="sharedFormView" class="w-full">
<a-alert class="!mt-2 !mb-4 !py-4 text-left !rounded-lg" type="success" outlined>
<a-alert class="nc-shared-form-success-msg !mt-2 !mb-4 !py-4 text-left !rounded-lg" type="success" outlined>
<template #message>
<LazyCellRichText
v-if="sharedFormView?.success_msg?.trim()"
@ -140,7 +140,6 @@ const onDecode = async (scannedCodeValue: string) => {
v-if="sharedFormView?.submit_another_form"
type="secondary"
:size="isMobileMode ? 'medium' : 'small'"
data-testid="nc-survey-form__btn-submit-another-form"
@click="submitted = false"
>
{{ $t('activity.submitAnotherForm') }}
@ -218,6 +217,11 @@ const onDecode = async (scannedCodeValue: string) => {
:column="field"
:edit-enabled="!field?.read_only"
:read-only="field?.read_only"
@update:model-value="
() => {
v$.localState[field.title]?.$validate()
}
"
/>
<a-button
v-if="field.enable_scanner"
@ -232,7 +236,7 @@ const onDecode = async (scannedCodeValue: string) => {
</LazySmartsheetDivDataCell>
</NcTooltip>
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-sm mt-2">
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-xs mt-2">
<template v-if="isVirtualCol(field)">
<div v-for="error of v$.virtual[field.title]?.$errors" :key="`${error}virtual`" class="text-red-500">
{{ error.$message }}

155
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue

@ -2,21 +2,19 @@
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { breakpointsTailwind } from '@vueuse/core'
import tinycolor from 'tinycolor2'
import {
DropZoneRef,
IsSurveyFormInj,
computed,
isValidURL,
onKeyStroke,
onMounted,
provide,
ref,
useBreakpoints,
useI18n,
usePointerSwipe,
useSharedFormStoreOrThrow,
useStepper,
validateEmail,
} from '#imports'
enum TransitionDirection {
@ -36,8 +34,6 @@ const { md } = useBreakpoints(breakpointsTailwind)
const { v$, formState, formColumns, submitForm, submitted, secondsRemain, sharedFormView, sharedViewMeta, onReset } =
useSharedFormStoreOrThrow()
const { t } = useI18n()
const { isMobileMode } = storeToRefs(useConfigStore())
const isTransitioning = ref(false)
@ -80,7 +76,17 @@ const { index, goToPrevious, goToNext, isFirst, isLast, goTo } = useStepper(step
const field = computed(() => formColumns.value?.[index.value])
const columnValidationError = ref(false)
const fieldHasError = computed(() => {
if (field.value?.title) {
if (isVirtualCol(field.value)) {
return v$.value.virtual[field.value.title]?.$error
} else {
return v$.value.localState[field.value.title]?.$error
}
}
return false
})
function isRequired(column: ColumnType, required = false) {
let columnObj = column
@ -123,41 +129,20 @@ function animate(target: AnimationTarget) {
}, transitionDuration.value / 2)
}
async function validateColumn() {
const f = field.value!
if (parseProp(f.meta)?.validate && formState.value[f.title!]) {
if (f.uidt === UITypes.Email) {
if (!validateEmail(formState.value[f.title!])) {
columnValidationError.value = true
message.error(t('msg.error.invalidEmail'))
return false
}
} else if (f.uidt === UITypes.URL) {
if (!isValidURL(formState.value[f.title!])) {
columnValidationError.value = true
message.error(t('msg.error.invalidURL'))
return false
}
}
const validateField = async (title: string, type: 'cell' | 'virtual') => {
const validationField = type === 'cell' ? v$.value.localState[title] : v$.value.virtual[title]
if (validationField) {
return await validationField.$validate()
} else {
return true
}
return true
}
async function goNext(animationTarget?: AnimationTarget) {
columnValidationError.value = false
if (isLast.value || !isStarted.value || submitted.value || dialogShow.value || !field.value || !field.value.title) return
if (isLast.value || !isStarted.value || submitted.value) return
if (!field.value || !field.value.title) return
const validationField = v$.value.localState[field.value.title]
if (validationField) {
const isValid = await validationField.$validate()
if (!isValid) return
}
if (!(await validateColumn())) return
if (field.value?.title && !(await validateField(field.value.title, isVirtualCol(field.value) ? 'virtual' : 'cell'))) return
animate(animationTarget || AnimationTarget.ArrowRight)
@ -172,9 +157,7 @@ async function goNext(animationTarget?: AnimationTarget) {
}
async function goPrevious(animationTarget?: AnimationTarget) {
if (isFirst.value || !isStarted.value || submitted.value) return
columnValidationError.value = false
if (isFirst.value || !isStarted.value || submitted.value || dialogShow.value) return
animate(animationTarget || AnimationTarget.ArrowLeft)
@ -188,7 +171,7 @@ function focusInput() {
const inputEl =
(document.querySelector('.nc-cell input') as HTMLInputElement) ||
(document.querySelector('.nc-cell textarea') as HTMLTextAreaElement) ||
(document.querySelector('.nc-cell [tabindex="0"]') as HTMLInputElement)
(document.querySelector('.nc-cell [tabindex="0"]') as HTMLElement)
if (inputEl) {
activeCell.value = inputEl
@ -207,7 +190,7 @@ function resetForm() {
}
async function submit() {
if (submitted.value || !(await validateColumn())) return
if (submitted.value) return
dialogShow.value = false
submitForm()
}
@ -228,13 +211,27 @@ const handleFocus = () => {
}
}
const showSubmitConfirmModal = async () => {
if (field.value?.title && !(await validateField(field.value.title, isVirtualCol(field.value) ? 'virtual' : 'cell'))) {
return
}
dialogShow.value = true
setTimeout(() => {
// NcButton will only focus if document has already focused element
document.querySelector('.nc-survery-form__confirmation_modal div[tabindex="0"]')?.focus()
document.querySelector('.nc-survey-form-btn-submit.nc-button')?.focus()
}, 50)
}
onKeyStroke(['ArrowLeft', 'ArrowDown'], () => {
goPrevious(AnimationTarget.ArrowLeft)
})
onKeyStroke(['ArrowRight', 'ArrowUp'], () => {
goNext(AnimationTarget.ArrowRight)
})
onKeyStroke(['Enter', 'Space'], () => {
onKeyStroke(['Enter'], async (e) => {
if (submitted.value) return
if (!isStarted.value && !submitted.value) {
@ -244,7 +241,8 @@ onKeyStroke(['Enter', 'Space'], () => {
if (dialogShow.value) {
submit()
} else {
dialogShow.value = true
e.preventDefault()
showSubmitConfirmModal()
}
} else {
const activeElement = document.activeElement as HTMLElement
@ -277,16 +275,6 @@ onMounted(() => {
})
}
})
watch(
formState,
() => {
columnValidationError.value = false
},
{
deep: true,
},
)
</script>
<template>
@ -372,10 +360,7 @@ watch(
<div class="flex justify-end mt-12">
<div class="flex items-center gap-3">
<div class="hidden md:flex text-sm items-center gap-1 text-gray-800">
<span>
{{ $t('labels.pressEnter') }}
</span>
<NcBadge class="pl-4 pr-1 h-[21px] text-gray-600"> </NcBadge>
<span> {{ $t('labels.pressEnter') }} </span>
</div>
<NcButton
:size="isMobileMode ? 'medium' : 'small'"
@ -429,6 +414,7 @@ watch(
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field"
:read-only="field?.read_only"
@update:model-value="validateField(field.title, 'virtual')"
/>
<LazySmartsheetCell
@ -440,12 +426,20 @@ watch(
:column="field"
:edit-enabled="!field?.read_only"
:read-only="field?.read_only"
@update:model-value="validateField(field.title, 'cell')"
/>
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1">
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500">
{{ error.$message }}
</div>
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-xs my-2 px-1">
<template v-if="isVirtualCol(field)">
<div v-for="error of v$.virtual[field.title]?.$errors" :key="`${error}virtual`" class="text-red-500">
{{ error.$message }}
</div>
</template>
<template v-else>
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500">
{{ error.$message }}
</div>
</template>
<div v-if="field.uidt === UITypes.LongText" class="text-sm text-gray-500 flex flex-wrap items-center">
{{ $t('general.shift') }} <MdiAppleKeyboardShift class="mx-1 text-primary" /> + {{ $t('general.enter') }}
@ -458,7 +452,7 @@ watch(
<div class="ml-1 mt-4 flex w-full text-lg">
<div class="flex-1 flex justify-end">
<div v-if="isLast && !v$.$invalid">
<div v-if="isLast">
<NcButton
:size="isMobileMode ? 'medium' : 'small'"
:class="
@ -466,9 +460,9 @@ watch(
? 'transform translate-y-[1px] translate-x-[1px] ring ring-accent ring-opacity-100'
: ''
"
:disabled="v$.localState[field.title]?.$error || columnValidationError"
:disabled="fieldHasError"
data-testid="nc-survey-form__btn-submit-confirm"
@click="dialogShow = true"
@click="showSubmitConfirmModal"
>
{{ $t('general.submit') }} form
</NcButton>
@ -477,17 +471,9 @@ watch(
<div v-else class="flex items-center gap-3">
<div
class="hidden md:flex text-sm items-center gap-1"
:class="v$.localState[field.title]?.$error || columnValidationError ? 'text-gray-200' : 'text-gray-800'"
:class="fieldHasError ? 'text-gray-200' : 'text-gray-800'"
>
<span>
{{ $t('labels.pressEnter') }}
</span>
<NcBadge
class="pl-4 pr-1 h-[21px]"
:class="v$.localState[field.title]?.$error || columnValidationError ? 'text-gray-200' : 'text-gray-600'"
>
</NcBadge>
<span> {{ $t('labels.pressEnter') }} </span>
</div>
<NcButton
:size="isMobileMode ? 'medium' : 'small'"
@ -498,7 +484,7 @@ watch(
? 'transform translate-y-[2px] translate-x-[2px] after:(!ring !ring-accent !ring-opacity-100)'
: '',
]"
:disabled="v$.localState[field.title]?.$error || columnValidationError"
:disabled="fieldHasError"
@click="goNext()"
>
{{ $t('labels.next') }}
@ -513,14 +499,26 @@ watch(
<div class="md:(absolute bottom-0 left-0 right-0 px-4 pb-4) lg:px-10 lg:pb-10">
<div class="flex justify-end items-center gap-4">
<div class="flex justify-center">
<GeneralFormBranding class="inline-flex mx-auto" />
<GeneralFormBranding
class="inline-flex mx-auto"
:style="{
color: tinycolor.isReadable(parseProp(sharedFormView?.meta)?.background_color || '#F9F9FA', '#D5D5D9', {
level: 'AA',
size: 'large',
})
? '#fff'
: tinycolor
.mostReadable(parseProp(sharedFormView?.meta)?.background_color || '#F9F9FA', ['#374151', '#D5D5D9'])
.toHex8String(),
}"
/>
</div>
<div v-if="isStarted && !submitted" class="flex items-center gap-3">
<NcButton
type="secondary"
:size="isMobileMode ? 'medium' : 'small'"
data-testid="nc-survey-form__icon-prev"
:disabled="isFirst || v$.localState[field.title]?.$error"
:disabled="isFirst"
@click="goPrevious()"
>
<GeneralIcon icon="ncArrowLeft"
@ -530,7 +528,7 @@ watch(
:size="isMobileMode ? 'medium' : 'small'"
type="secondary"
data-testid="nc-survey-form__icon-next"
:disabled="isLast || v$.localState[field.title]?.$error || columnValidationError"
:disabled="isLast || fieldHasError"
@click="goNext()"
>
<GeneralIcon icon="ncArrowRight" />
@ -552,6 +550,7 @@ watch(
type="primary"
:size="isMobileMode ? 'medium' : 'small'"
data-testid="nc-survey-form__btn-submit"
class="nc-survey-form-btn-submit"
@click="submit"
>
{{ $t('general.submit') }}

4
packages/nc-gui/store/views.ts

@ -28,7 +28,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
const { activeWorkspaceId } = storeToRefs(useWorkspace())
const { meta: metaKey, ctrlKey } = useMagicKeys()
const { meta: metaKey, control } = useMagicKeys()
const recentViews = computed<RecentView[]>(() =>
allRecentViews.value.filter((f) => f.workspaceId === activeWorkspaceId.value).splice(0, 10),
@ -219,7 +219,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
hardReload?: boolean
doNotSwitchTab?: boolean
}) => {
const cmdOrCtrl = isMac() ? metaKey.value : ctrlKey.value
const cmdOrCtrl = isMac() ? metaKey.value : control.value
const routeName = 'index-typeOrId-baseId-index-index-viewId-viewTitle-slugs'

2
packages/nc-gui/utils/dataUtils.ts

@ -107,7 +107,7 @@ export const rowDefaultData = (columns: ColumnType[] = []) => {
!/^\w+\(\)|CURRENT_TIMESTAMP$/.test(col.cdf)
) {
const defaultValue = col.cdf
acc[col.title!] = typeof defaultValue === 'string' ? defaultValue.replace(/^'/, '').replace(/'$/, '') : defaultValue
acc[col.title!] = typeof defaultValue === 'string' ? defaultValue.replace(/^'|'$/g, '') : defaultValue
}
return acc
}, {} as Record<string, any>)

2
packages/nc-gui/utils/iconUtils.ts

@ -166,6 +166,7 @@ import NcCellSystemText from '~icons/nc-icons/system-text'
import NcCellAttachment from '~icons/nc-icons/cell-attachment'
import NcCircleCheck from '~icons/nc-icons/circle-check'
import OnetoOneIcon from '~icons/nc-icons/onetoone'
// keep it for reference
// todo: remove it after all icons are migrated
@ -358,6 +359,7 @@ export const iconMap = {
mm_solid: ManytoManySolidIcon,
hm_solid: HasManySolidIcon,
bt_solid: BelongsToSolidIcon,
oneToOneSolid: OnetoOneIcon,
workspaceDefault: MsGroup,
project: Project,
search: NcSearch,

3
packages/nc-gui/utils/virtualCell.ts

@ -15,6 +15,9 @@ export const isMm = (column: ColumnType) =>
export const isBt = (column: ColumnType) =>
isLTAR(column.uidt!, column.colOptions) && column.colOptions?.type === RelationTypes.BELONGS_TO
export const isOo = (column: ColumnType) =>
isLTAR(column.uidt!, column.colOptions) && column.colOptions?.type === RelationTypes.ONE_TO_ONE
export const isLookup = (column: ColumnType) => column.uidt === UITypes.Lookup
export const isRollup = (column: ColumnType) => column.uidt === UITypes.Rollup
export const isFormula = (column: ColumnType) => column.uidt === UITypes.Formula

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

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

2
packages/nocodb-sdk/package.json

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

14
packages/nocodb-sdk/pnpm-lock.yaml

@ -6,8 +6,8 @@ settings:
dependencies:
axios:
specifier: ^1.6.7
version: 1.6.7
specifier: ^1.6.8
version: 1.6.8
dayjs:
specifier: ^1.11.10
version: 1.11.10
@ -1389,10 +1389,10 @@ packages:
engines: {node: '>= 0.4'}
dev: true
/axios@1.6.7:
resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==}
/axios@1.6.8:
resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==}
dependencies:
follow-redirects: 1.15.4
follow-redirects: 1.15.6
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
@ -2415,8 +2415,8 @@ packages:
resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==}
dev: true
/follow-redirects@1.15.4:
resolution: {integrity: sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==}
/follow-redirects@1.15.6:
resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'

27
packages/nocodb-sdk/src/lib/globals.ts

@ -20,6 +20,7 @@ export enum RelationTypes {
HAS_MANY = 'hm',
BELONGS_TO = 'bt',
MANY_TO_MANY = 'mm',
ONE_TO_ONE = 'oo',
}
export enum ExportTypes {
@ -27,11 +28,6 @@ export enum ExportTypes {
CSV = 'csv',
}
export enum ErrorMessages {
INVALID_SHARED_VIEW_PASSWORD = 'INVALID_SHARED_VIEW_PASSWORD',
NOT_IMPLEMENTED = 'NOT_IMPLEMENTED',
}
export enum AuditOperationTypes {
COMMENT = 'COMMENT',
DATA = 'DATA',
@ -128,6 +124,27 @@ export enum NcDataErrorCodes {
NC_ERR_MM_MODEL_NOT_FOUND = 'NC_ERR_MM_MODEL_NOT_FOUND',
}
export enum NcErrorType {
AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED',
API_TOKEN_NOT_ALLOWED = 'API_TOKEN_NOT_ALLOWED',
WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND',
BASE_NOT_FOUND = 'BASE_NOT_FOUND',
SOURCE_NOT_FOUND = 'SOURCE_NOT_FOUND',
TABLE_NOT_FOUND = 'TABLE_NOT_FOUND',
VIEW_NOT_FOUND = 'VIEW_NOT_FOUND',
FIELD_NOT_FOUND = 'FIELD_NOT_FOUND',
RECORD_NOT_FOUND = 'RECORD_NOT_FOUND',
ERROR_DUPLICATE_RECORD = 'ERROR_DUPLICATE_RECORD',
USER_NOT_FOUND = 'USER_NOT_FOUND',
INVALID_OFFSET_VALUE = 'INVALID_OFFSET_VALUE',
INVALID_LIMIT_VALUE = 'INVALID_LIMIT_VALUE',
INVALID_FILTER = 'INVALID_FILTER',
INVALID_SHARED_VIEW_PASSWORD = 'INVALID_SHARED_VIEW_PASSWORD',
NOT_IMPLEMENTED = 'NOT_IMPLEMENTED',
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
DATABASE_ERROR = 'DATABASE_ERROR',
}
type Roles = OrgUserRoles | ProjectRoles | WorkspaceUserRoles;
type RolesObj = Partial<Record<Roles, boolean>>;

20
packages/nocodb/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb",
"version": "0.204.6",
"version": "0.204.7",
"description": "NocoDB Backend",
"main": "dist/bundle.js",
"author": {
@ -53,22 +53,22 @@
"@graphql-tools/merge": "^6.2.17",
"@jm18457/kafkajs-msk-iam-authentication-mechanism": "^3.1.2",
"@nestjs/bull": "^10.0.1",
"@nestjs/common": "^10.3.3",
"@nestjs/common": "^10.3.4",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.3.3",
"@nestjs/core": "^10.3.4",
"@nestjs/event-emitter": "^2.0.4",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mapped-types": "^2.0.5",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.3.3",
"@nestjs/platform-socket.io": "^10.3.3",
"@nestjs/platform-express": "^10.3.4",
"@nestjs/platform-socket.io": "^10.3.4",
"@nestjs/serve-static": "^4.0.1",
"@nestjs/throttler": "^5.1.2",
"@nestjs/websockets": "^10.3.3",
"@nestjs/websockets": "^10.3.4",
"@ntegral/nestjs-sentry": "^4.0.1",
"@sentry/node": "^6.19.7",
"@techpass/passport-openidconnect": "^0.3.3",
"@types/chai": "^4.3.12",
"@types/chai": "^4.3.13",
"airtable": "^0.12.2",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
@ -76,7 +76,7 @@
"auto-bind": "^4.0.0",
"aws-kcl": "^2.2.5",
"aws-sdk": "^2.1550.0",
"axios": "^1.6.7",
"axios": "^1.6.8",
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.2",
"boxen": "^5.1.2",
@ -132,11 +132,11 @@
"mysql2": "^3.9.2",
"nanoid": "^3.3.7",
"nc-help": "0.3.1",
"nc-lib-gui": "0.204.6",
"nc-lib-gui": "0.204.5",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nestjs-kafka": "^1.0.6",
"nestjs-throttler-storage-redis": "^0.4.2",
"nestjs-throttler-storage-redis": "^0.4.3",
"nocodb-sdk": "workspace:^",
"nodemailer": "^6.9.12",
"object-hash": "^3.0.0",

20
packages/nocodb/src/controllers/data-alias-nested.controller.ts

@ -98,6 +98,26 @@ export class DataAliasNestedController {
});
}
@Get([
'/api/v1/db/data/:orgs/:baseName/:tableName/:rowId/oo/:columnName/exclude',
])
@Acl('ooExcludedList')
async ooExcludedList(
@Req() req: Request,
@Param('columnName') columnName: string,
@Param('rowId') rowId: string,
@Param('baseName') baseName: string,
@Param('tableName') tableName: string,
) {
return await this.dataAliasNestedService.ooExcludedList({
query: req.query,
columnName: columnName,
rowId: rowId,
baseName: baseName,
tableName: tableName,
});
}
// todo: handle case where the given column is not ltar
@Get(['/api/v1/db/data/:orgs/:baseName/:tableName/:rowId/hm/:columnName'])

2
packages/nocodb/src/controllers/old-datas/old-datas.service.ts

@ -136,7 +136,7 @@ export class OldDatasService {
titleOrId: req.params.viewName,
fk_model_id: model.id,
}));
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(req.params.tableName);
return { model, view };
}
}

10
packages/nocodb/src/controllers/public-datas-export.controller.ts

@ -6,7 +6,7 @@ import {
Response,
UseGuards,
} from '@nestjs/common';
import { ErrorMessages, isSystemColumn, ViewTypes } from 'nocodb-sdk';
import { isSystemColumn, ViewTypes } from 'nocodb-sdk';
import * as XLSX from 'xlsx';
import { nocoExecute } from 'nc-help';
import papaparse from 'papaparse';
@ -35,7 +35,7 @@ export class PublicDatasExportController {
@Param('publicDataUuid') publicDataUuid: string,
) {
const view = await View.getByUUID(publicDataUuid);
if (!view) NcError.notFound('Not found');
if (!view) NcError.viewNotFound(publicDataUuid);
if (
view.type !== ViewTypes.GRID &&
view.type !== ViewTypes.KANBAN &&
@ -45,7 +45,7 @@ export class PublicDatasExportController {
NcError.notFound('Not found');
if (view.password && view.password !== req.headers?.['xc-password']) {
NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
NcError.invalidSharedViewPassword();
}
const model = await view.getModelWithInfo();
@ -88,7 +88,7 @@ export class PublicDatasExportController {
const view = await View.getByUUID(req.params.publicDataUuid);
const fields = req.query.fields;
if (!view) NcError.notFound('Not found');
if (!view) NcError.viewNotFound(req.params.publicDataUuid);
if (
view.type !== ViewTypes.GRID &&
view.type !== ViewTypes.KANBAN &&
@ -98,7 +98,7 @@ export class PublicDatasExportController {
NcError.notFound('Not found');
if (view.password && view.password !== req.headers?.['xc-password']) {
NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
NcError.invalidSharedViewPassword();
}
const model = await view.getModelWithInfo();

522
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -632,7 +632,7 @@ class BaseModelSqlv2 {
args.column_name.split(',').map(async (col) => {
let column = cols.find((c) => c.column_name === col || c.title === col);
if (!column) {
throw NcError.notFound('Column not found');
throw NcError.fieldNotFound(col);
}
// if qrCode or Barcode replace it with value column nd keep the alias
@ -851,7 +851,7 @@ class BaseModelSqlv2 {
(c) => c.column_name === col || c.title === col,
);
if (!column) {
throw NcError.notFound('Column not found');
throw NcError.fieldNotFound(col);
}
// if qrCode or Barcode replace it with value column nd keep the alias
@ -1805,6 +1805,56 @@ class BaseModelSqlv2 {
?.count;
}
// todo: naming & optimizing
public async countExcludedOneToOneChildren(
{ colId, cid = null },
args,
): Promise<any> {
const { where } = this._getListArgs(args as any);
const relColumn = (await this.model.getColumns()).find(
(c) => c.id === colId,
);
const relColOptions =
(await relColumn.getColOptions()) as LinkToAnotherRecordColumn;
const rcn = (await relColOptions.getParentColumn()).column_name;
const parentTable = await (
await relColOptions.getParentColumn()
).getModel();
const cn = (await relColOptions.getChildColumn()).column_name;
const childTable = await (await relColOptions.getChildColumn()).getModel();
const childTn = this.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable);
const rtn = parentTn;
const tn = childTn;
await childTable.getColumns();
// one-to-one relation is combination of both hm and bt to identify table which have
// foreign key column(similar to bt) we are adding a boolean flag `bt` under meta
const isBt = relColumn.meta?.bt;
const qb = this.dbDriver(isBt ? rtn : tn)
.where((qb) => {
qb.whereNotIn(
isBt ? rcn : cn,
this.dbDriver(isBt ? tn : rtn)
.select(isBt ? cn : rcn)
.where(_wherePk((isBt ? childTable : parentTable).primaryKeys, cid))
.whereNotNull(isBt ? cn : rcn),
).orWhereNull(isBt ? rcn : cn);
})
.count(`*`, { as: 'count' });
const aliasColObjMap = await parentTable.getAliasColObjMap();
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
await conditionV2(this, filterObj, qb);
return (await this.execAndParse(qb, null, { raw: true, first: true }))
?.count;
}
// todo: naming & optimizing
public async getBtChildrenExcludedList(
{ colId, cid = null },
@ -1877,6 +1927,85 @@ class BaseModelSqlv2 {
});
}
// todo: naming & optimizing
public async getExcludedOneToOneChildrenList(
{ colId, cid = null },
args,
): Promise<any> {
const { where, ...rest } = this._getListArgs(args as any);
const relColumn = (await this.model.getColumns()).find(
(c) => c.id === colId,
);
const relColOptions =
(await relColumn.getColOptions()) as LinkToAnotherRecordColumn;
const rcn = (await relColOptions.getParentColumn()).column_name;
const parentTable = await (
await relColOptions.getParentColumn()
).getModel();
const cn = (await relColOptions.getChildColumn()).column_name;
const childTable = await (await relColOptions.getChildColumn()).getModel();
const parentModel = await Model.getBaseModelSQL({
dbDriver: this.dbDriver,
model: parentTable,
});
const childModel = await Model.getBaseModelSQL({
dbDriver: this.dbDriver,
model: childTable,
});
const rtn = this.getTnPath(parentTable);
const tn = this.getTnPath(childTable);
await childTable.getColumns();
// one-to-one relation is combination of both hm and bt to identify table which have
// foreign key column(similar to bt) we are adding a boolean flag `bt` under meta
const isBt = relColumn.meta?.bt;
const qb = this.dbDriver(isBt ? rtn : tn).where((qb) => {
qb.whereNotIn(
isBt ? rcn : cn,
this.dbDriver(isBt ? tn : rtn)
.select(isBt ? cn : rcn)
.where(_wherePk((isBt ? childTable : parentTable).primaryKeys, cid))
.whereNotNull(isBt ? cn : rcn),
).orWhereNull(isBt ? rcn : cn);
});
if (+rest?.shuffle) {
await this.shuffle({ qb });
}
await (isBt ? parentModel : childModel).selectObject({ qb });
const aliasColObjMap = await parentTable.getAliasColObjMap();
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
await conditionV2(this, filterObj, qb);
// sort by primary key if not autogenerated string
// if autogenerated string sort by created_at column if present
if (parentTable.primaryKey && parentTable.primaryKey.ai) {
qb.orderBy(parentTable.primaryKey.column_name);
} else if (
parentTable.columns.find((c) => c.column_name === 'created_at')
) {
qb.orderBy('created_at');
}
applyPaginate(qb, rest);
const proto = await (isBt ? parentModel : childModel).getProto();
const data = await this.execAndParse(
qb,
await (isBt ? parentTable : childTable).getColumns(),
);
return data.map((c) => {
c.__proto__ = proto;
return c;
});
}
protected async getSelectQueryBuilderForFormula(
column: Column<any>,
tableAlias?: string,
@ -2106,6 +2235,141 @@ class BaseModelSqlv2 {
return await readLoader.load(this?.[cCol?.title]);
};
// todo : handle mm
} else if (colOptions.type === 'oo') {
const isBt = column.meta?.bt;
if (isBt) {
// @ts-ignore
const colOptions =
(await column.getColOptions()) as LinkToAnotherRecordColumn;
const pCol = await Column.get({
colId: colOptions.fk_parent_column_id,
});
const cCol = await Column.get({
colId: colOptions.fk_child_column_id,
});
// use dataloader to get batches of parent data together rather than getting them individually
// it takes individual keys and callback is invoked with an array of values and we can get the
// result for all those together and return the value in the same order as in the array
// this way all parents data extracted together
const readLoader = new DataLoader(
async (_ids: string[]) => {
// handle binary(16) foreign keys
const ids = _ids.map((id) => {
if (pCol.ct !== 'binary(16)') return id;
// Cast the id to string.
const idAsString = id + '';
// Check if the id is a UUID and the column is binary(16)
const isUUIDBinary16 =
idAsString.length === 36 || idAsString.length === 32;
// If the id is a UUID and the column is binary(16), convert the id to a Buffer. Otherwise, return null to indicate that the id is not a UUID.
const idAsUUID = isUUIDBinary16
? idAsString.length === 32
? idAsString.replace(
/(.{8})(.{4})(.{4})(.{4})(.{12})/,
'$1-$2-$3-$4-$5',
)
: idAsString
: null;
return idAsUUID
? Buffer.from(idAsUUID.replace(/-/g, ''), 'hex')
: id;
});
const data = await (
await Model.getBaseModelSQL({
id: pCol.fk_model_id,
dbDriver: this.dbDriver,
})
).list(
{
fieldsSet: (readLoader as any).args?.fieldsSet,
filterArr: [
new Filter({
id: null,
fk_column_id: pCol.id,
fk_model_id: pCol.fk_model_id,
value: ids as any[],
comparison_op: 'in',
}),
],
},
{
ignoreViewFilterAndSort: true,
ignorePagination: true,
},
);
const groupedList = groupBy(data, pCol.title);
return _ids.map(
async (id: string) => groupedList?.[id]?.[0],
);
},
{
cache: false,
},
);
// defining BelongsTo read resolver method
proto[column.title] = async function (args?: any) {
if (
this?.[cCol?.title] === null ||
this?.[cCol?.title] === undefined
)
return null;
(readLoader as any).args = args;
return await readLoader.load(this?.[cCol?.title]);
};
} else {
const listLoader = new DataLoader(
async (ids: string[]) => {
if (ids.length > 1) {
const data = await this.multipleHmList(
{
colId: column.id,
ids,
},
(listLoader as any).args,
);
return ids.map((id: string) =>
data[id] ? data[id]?.[0] : null,
);
} else {
return [
(
await this.hmList(
{
colId: column.id,
id: ids[0],
},
(listLoader as any).args,
)
)?.[0] ?? null,
];
}
},
{
cache: false,
},
);
const self: BaseModelSqlv2 = this;
proto[
column.uidt === UITypes.Links
? `_nc_lk_${column.title}`
: column.title
] = async function (args): Promise<any> {
(listLoader as any).args = args;
return listLoader.load(
getCompositePk(self.model.primaryKeys, this),
);
};
}
}
}
break;
@ -2806,7 +3070,7 @@ class BaseModelSqlv2 {
const nestedCols = (await this.model.getColumns()).filter((c) =>
isLinksOrLTAR(c),
);
const postInsertOps = await this.prepareNestedLinkQb({
const { postInsertOps, preInsertOps } = await this.prepareNestedLinkQb({
nestedCols,
data,
insertObj,
@ -2818,6 +3082,8 @@ class BaseModelSqlv2 {
await this.prepareNocoData(insertObj, true, cookie);
await Promise.all(preInsertOps.map((f) => f(this.dbDriver)));
let response;
const query = this.dbDriver(this.tnPath).insert(insertObj);
@ -2958,6 +3224,7 @@ class BaseModelSqlv2 {
insertObj: Record<string, any>;
}) {
const postInsertOps: ((rowId: any, trx?: any) => Promise<void>)[] = [];
const preInsertOps: ((trx?: any) => Promise<void>)[] = [];
for (const col of nestedCols) {
if (col.title in data) {
const colOptions = await col.getColOptions<LinkToAnotherRecordColumn>();
@ -2981,6 +3248,45 @@ class BaseModelSqlv2 {
insertObj[childCol.column_name] = nestedData?.[parentCol.title];
}
break;
case RelationTypes.ONE_TO_ONE:
{
const isBt = col.meta?.bt;
const childCol = await colOptions.getChildColumn();
const childModel = await childCol.getModel();
await childModel.getColumns();
if (isBt) {
// todo: unlink the ref record
preInsertOps.push(async (trx: any = this.dbDriver) => {
await trx(this.getTnPath(childModel.table_name))
.update({
[childCol.column_name]: null,
})
.where(
childCol.column_name,
nestedData[childModel.primaryKey.title],
);
});
if (typeof nestedData !== 'object') continue;
const childCol = await colOptions.getChildColumn();
const parentCol = await colOptions.getParentColumn();
insertObj[childCol.column_name] = nestedData?.[parentCol.title];
} else {
postInsertOps.push(async (rowId, trx: any = this.dbDriver) => {
await trx(this.getTnPath(childModel.table_name))
.update({
[childCol.column_name]: rowId,
})
.where(
childModel.primaryKey.column_name,
nestedData[childModel.primaryKey.title],
);
});
}
}
break;
case RelationTypes.HAS_MANY:
{
if (!Array.isArray(nestedData)) continue;
@ -3033,7 +3339,7 @@ class BaseModelSqlv2 {
}
}
}
return postInsertOps;
return { postInsertOps, preInsertOps };
}
async bulkInsert(
@ -3061,6 +3367,7 @@ class BaseModelSqlv2 {
// TODO: ag column handling for raw bulk insert
const insertDatas = raw ? datas : [];
let postInsertOps: ((rowId: any, trx?: any) => Promise<void>)[] = [];
let preInsertOps: ((trx?: any) => Promise<void>)[] = [];
let aiPkCol: Column;
let agPkCol: Column;
@ -3209,11 +3516,14 @@ class BaseModelSqlv2 {
// prepare nested link data for insert only if it is single record insertion
if (isSingleRecordInsertion) {
postInsertOps = await this.prepareNestedLinkQb({
const operations = await this.prepareNestedLinkQb({
nestedCols,
data: d,
insertObj,
});
postInsertOps = operations.postInsertOps;
preInsertOps = operations.preInsertOps;
}
insertDatas.push(insertObj);
@ -3244,6 +3554,8 @@ class BaseModelSqlv2 {
}
}
await Promise.all(preInsertOps.map((f) => f(trx)));
let responses;
// insert one by one as fallback to get ids for sqlite and mysql
@ -3371,9 +3683,7 @@ class BaseModelSqlv2 {
if (!pkValues) {
// throw or skip if no pk provided
if (throwExceptionIfNotExist) {
NcError.unprocessableEntity(
`Record with pk ${JSON.stringify(pkValues)} not found`,
);
NcError.recordNotFound(JSON.stringify(pkValues));
}
continue;
}
@ -3384,9 +3694,7 @@ class BaseModelSqlv2 {
if (!oldRecord) {
// throw or skip if no record found
if (throwExceptionIfNotExist) {
NcError.unprocessableEntity(
`Record with pk ${JSON.stringify(pkValues)} not found`,
);
NcError.recordNotFound(JSON.stringify(pkValues));
}
continue;
}
@ -3541,9 +3849,7 @@ class BaseModelSqlv2 {
if (!pkValues) {
// throw or skip if no pk provided
if (throwExceptionIfNotExist) {
NcError.unprocessableEntity(
`Record with pk ${JSON.stringify(pkValues)} not found`,
);
NcError.recordNotFound(JSON.stringify(pkValues));
}
continue;
}
@ -3552,9 +3858,7 @@ class BaseModelSqlv2 {
if (!deletedRecord) {
// throw or skip if no record found
if (throwExceptionIfNotExist) {
NcError.unprocessableEntity(
`Record with pk ${JSON.stringify(pkValues)} not found`,
);
NcError.recordNotFound(JSON.stringify(pkValues));
}
continue;
}
@ -4118,7 +4422,7 @@ class BaseModelSqlv2 {
!column ||
![UITypes.LinkToAnotherRecord, UITypes.Links].includes(column.uidt)
)
NcError.notFound('Column not found');
NcError.fieldNotFound(colId);
const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>();
@ -4245,6 +4549,54 @@ class BaseModelSqlv2 {
});
}
break;
case RelationTypes.ONE_TO_ONE:
{
const isBt = column.meta?.bt;
// todo: unlink if it's already mapped
// unlink already mapped record if any
await this.execAndParse(
this.dbDriver(childTn)
.where({
[childColumn.column_name]: this.dbDriver.from(
this.dbDriver(parentTn)
.select(parentColumn.column_name)
.where(
_wherePk(parentTable.primaryKeys, isBt ? childId : rowId),
)
.first()
.as('___cn_alias'),
),
})
.update({ [childColumn.column_name]: null }),
null,
{ raw: true },
);
await this.execAndParse(
this.dbDriver(childTn)
.update({
[childColumn.column_name]: this.dbDriver.from(
this.dbDriver(parentTn)
.select(parentColumn.column_name)
.where(
_wherePk(parentTable.primaryKeys, isBt ? childId : rowId),
)
.first()
.as('___cn_alias'),
),
})
.where(_wherePk(childTable.primaryKeys, isBt ? rowId : childId)),
null,
{ raw: true },
);
await this.updateLastModified({
model: parentTable,
rowIds: [childId],
cookie,
});
}
break;
}
const response = await this.readByPk(
@ -4290,7 +4642,7 @@ class BaseModelSqlv2 {
!column ||
![UITypes.LinkToAnotherRecord, UITypes.Links].includes(column.uidt)
)
NcError.notFound('Column not found');
NcError.fieldNotFound(colId);
const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>();
@ -4395,6 +4747,24 @@ class BaseModelSqlv2 {
});
}
break;
case RelationTypes.ONE_TO_ONE:
{
const isBt = column.meta?.bt;
await this.execAndParse(
this.dbDriver(childTn)
.where(_wherePk(childTable.primaryKeys, isBt ? rowId : childId))
.update({ [childColumn.column_name]: null }),
null,
{ raw: true },
);
await this.updateLastModified({
model: parentTable,
rowIds: [childId],
cookie,
});
}
break;
}
const newData = await this.readByPk(
@ -4440,9 +4810,9 @@ class BaseModelSqlv2 {
.getColumns()
.then((cols) => cols?.find((col) => col.id === args.groupColumnId));
if (!column) NcError.notFound('Column not found');
if (!column) NcError.fieldNotFound(args.groupColumnId);
if (isVirtualCol(column))
NcError.notImplemented('Grouping for virtual columns not implemented');
NcError.notImplemented('Grouping for virtual columns');
// extract distinct group column values
let groupingValues: Set<any>;
@ -4602,9 +4972,9 @@ class BaseModelSqlv2 {
.getColumns()
.then((cols) => cols?.find((col) => col.id === args.groupColumnId));
if (!column) NcError.notFound('Column not found');
if (!column) NcError.fieldNotFound(args.groupColumnId);
if (isVirtualCol(column))
NcError.notImplemented('Grouping for virtual columns not implemented');
NcError.notImplemented('Grouping for virtual columns');
const qb = this.dbDriver(this.tnPath)
.count('*', { as: 'count' })
@ -4767,7 +5137,11 @@ class BaseModelSqlv2 {
}
idToAliasMap[col.id] = col.title;
if (col.colOptions?.type === 'bt') {
if (
[RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes(
col.colOptions?.type,
)
) {
btMap[col.id] = true;
const btData = Object.values(data).find(
(d) => d[col.id] && Object.keys(d[col.id]),
@ -5200,7 +5574,7 @@ class BaseModelSqlv2 {
async addLinks({
cookie,
childIds,
childIds: _childIds,
colId,
rowId,
}: {
@ -5212,8 +5586,7 @@ class BaseModelSqlv2 {
const columns = await this.model.getColumns();
const column = columns.find((c) => c.id === colId);
if (!column || !isLinksOrLTAR(column))
NcError.notFound(`Link column ${colId} not found`);
if (!column || !isLinksOrLTAR(column)) NcError.fieldNotFound(colId);
const row = await this.readByPk(
rowId,
@ -5224,10 +5597,10 @@ class BaseModelSqlv2 {
// validate rowId
if (!row) {
NcError.notFound(`Record with id '${rowId}' not found`);
NcError.recordNotFound(rowId);
}
if (!childIds.length) return;
if (!_childIds.length) return;
const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>();
@ -5241,7 +5614,39 @@ class BaseModelSqlv2 {
const childTn = this.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable);
switch (colOptions.type) {
let relationType = colOptions.type;
let childIds = _childIds;
if (relationType === RelationTypes.ONE_TO_ONE) {
relationType = column.meta?.bt
? RelationTypes.BELONGS_TO
: RelationTypes.HAS_MANY;
childIds = childIds.slice(0, 1);
// unlink
await this.execAndParse(
this.dbDriver(childTn)
.where({
[childColumn.column_name]: this.dbDriver.from(
this.dbDriver(parentTn)
.select(parentColumn.column_name)
.where(
_wherePk(
parentTable.primaryKeys,
column.meta?.bt ? childIds[0] : rowId,
),
)
.first()
.as('___cn_alias'),
),
})
.update({ [childColumn.column_name]: null }),
null,
{ raw: true },
);
}
switch (relationType) {
case RelationTypes.MANY_TO_MANY:
{
const vChildCol = await colOptions.getMMChildColumn();
@ -5303,11 +5708,7 @@ class BaseModelSqlv2 {
!childRows.find((r) => r[parentColumn.column_name] === id),
);
NcError.unprocessableEntity(
`Child record with id [${extractIdsString(
missingIds,
)}] not found`,
);
NcError.recordNotFound(extractIds(missingIds));
}
insertData = childRows
@ -5380,11 +5781,7 @@ class BaseModelSqlv2 {
!childRows.find((r) => r[parentColumn.column_name] === id),
);
NcError.unprocessableEntity(
`Child record with id [${extractIdsString(
missingIds,
)}] not found`,
);
NcError.recordNotFound(extractIds(missingIds));
}
}
const updateQb = this.dbDriver(childTn).update({
@ -5438,12 +5835,7 @@ class BaseModelSqlv2 {
});
if (!childRow) {
NcError.unprocessableEntity(
`Child record with id [${extractIdsString(
childIds,
true,
)}] not found`,
);
NcError.recordNotFound(extractIds(childIds, true));
}
}
@ -5499,8 +5891,7 @@ class BaseModelSqlv2 {
const columns = await this.model.getColumns();
const column = columns.find((c) => c.id === colId);
if (!column || !isLinksOrLTAR(column))
NcError.notFound(`Link column ${colId} not found`);
if (!column || !isLinksOrLTAR(column)) NcError.fieldNotFound(colId);
const row = await this.readByPk(
rowId,
@ -5511,7 +5902,7 @@ class BaseModelSqlv2 {
// validate rowId
if (!row) {
NcError.notFound(`Record with id '${rowId}' not found`);
NcError.recordNotFound(rowId);
}
if (!childIds.length) return;
@ -5585,11 +5976,7 @@ class BaseModelSqlv2 {
),
);
NcError.unprocessableEntity(
`Child record with id [${extractIdsString(
missingIds,
)}] not found`,
);
NcError.recordNotFound(extractIds(missingIds));
}
}
@ -5672,11 +6059,7 @@ class BaseModelSqlv2 {
),
);
NcError.unprocessableEntity(
`Child record with id [${extractIdsString(
missingIds,
)}] not found`,
);
NcError.recordNotFound(extractIds(missingIds));
}
}
@ -5734,12 +6117,7 @@ class BaseModelSqlv2 {
});
if (!childRow) {
NcError.unprocessableEntity(
`Child record with id [${extractIdsString(
childIds,
true,
)}] not found`,
);
NcError.recordNotFound(extractIds(childIds, true));
}
}
@ -5799,7 +6177,7 @@ class BaseModelSqlv2 {
// validate rowId
if (!row) {
NcError.notFound(`Record with id ${id} not found`);
NcError.recordNotFound(id);
}
const parentCol = await (
@ -6087,7 +6465,7 @@ export function extractSortsObject(
else sort.fk_column_id = aliasColObjMap[s.replace(/^\+/, '')]?.id;
if (throwErrorIfInvalid && !sort.fk_column_id)
NcError.unprocessableEntity(`Invalid field: ${s.replace(/^[+-]/, '')}`);
NcError.fieldNotFound(s.replace(/^[+-]/, ''));
return new Sort(sort);
});
@ -6258,7 +6636,7 @@ export function extractCondition(
validateFilterComparison(aliasColObjMap[alias].uidt, op, sub_op);
} else if (throwErrorIfInvalid) {
NcError.unprocessableEntity(`Invalid field: ${alias}`);
NcError.invalidFilter(str);
}
return new Filter({
@ -6414,13 +6792,13 @@ export function getListArgs(
return obj;
}
function extractIdsString(
function extractIds(
childIds: (string | number | Record<string, any>)[],
isBt = false,
) {
return (isBt ? childIds.slice(0, 1) : childIds)
.map((r) => (typeof r === 'object' ? JSON.stringify(r) : r))
.join(', ');
return (isBt ? childIds.slice(0, 1) : childIds).map((r) =>
typeof r === 'object' ? JSON.stringify(r) : `${r}`,
);
}
export { BaseModelSqlv2 };

33
packages/nocodb/src/db/conditionV2.ts

@ -16,7 +16,6 @@ import type LookupColumn from '~/models/LookupColumn';
import type RollupColumn from '~/models/RollupColumn';
import type FormulaColumn from '~/models/FormulaColumn';
import { getColumnName } from '~/db/BaseModelSqlv2';
import { type BarcodeColumn, BaseUser, type QrCodeColumn } from '~/models';
import { NcError } from '~/helpers/catchError';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2';
@ -25,6 +24,7 @@ import Filter from '~/models/Filter';
import generateLookupSelectQuery from '~/db/generateLookupSelectQuery';
import { getAliasGenerator } from '~/utils';
import { getRefColumnIfAlias } from '~/helpers';
import { type BarcodeColumn, BaseUser, type QrCodeColumn } from '~/models';
// tod: tobe fixed
// extend(customParseFormat);
@ -167,7 +167,7 @@ const parseConditionV2 = async (
const column = await getRefColumnIfAlias(await filter.getColumn());
if (!column) {
if (throwErrorIfInvalid) {
NcError.unprocessableEntity(`Invalid field: ${filter.fk_column_id}`);
NcError.fieldNotFound(filter.fk_column_id);
}
return;
}
@ -180,7 +180,16 @@ const parseConditionV2 = async (
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
if (colOptions.type === RelationTypes.HAS_MANY) {
let relationType = colOptions.type;
if (relationType === RelationTypes.ONE_TO_ONE) {
relationType = column.meta?.bt
? RelationTypes.BELONGS_TO
: RelationTypes.HAS_MANY;
}
if (relationType === RelationTypes.HAS_MANY) {
if (
['blank', 'notblank', 'checked', 'notchecked'].includes(
filter.comparison_op,
@ -245,7 +254,7 @@ const parseConditionV2 = async (
qbP.whereNotIn(parentColumn.column_name, selectQb);
else qbP.whereIn(parentColumn.column_name, selectQb);
};
} else if (colOptions.type === RelationTypes.BELONGS_TO) {
} else if (relationType === RelationTypes.BELONGS_TO) {
if (
['blank', 'notblank', 'checked', 'notchecked'].includes(
filter.comparison_op,
@ -315,7 +324,7 @@ const parseConditionV2 = async (
);
} else qbP.whereIn(childColumn.column_name, selectQb);
};
} else if (colOptions.type === RelationTypes.MANY_TO_MANY) {
} else if (relationType === RelationTypes.MANY_TO_MANY) {
const mmModel = await colOptions.getMMModel();
const mmParentColumn = await colOptions.getMMParentColumn();
const mmChildColumn = await colOptions.getMMChildColumn();
@ -1247,7 +1256,15 @@ async function generateLookupCondition(
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
if (relationColumnOptions.type === RelationTypes.HAS_MANY) {
let relationType = relationColumnOptions.type;
if (relationType === RelationTypes.ONE_TO_ONE) {
relationType = relationColumn.meta?.bt
? RelationTypes.BELONGS_TO
: RelationTypes.HAS_MANY;
}
if (relationType === RelationTypes.HAS_MANY) {
qb = knex(
knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(childModel.table_name),
@ -1278,7 +1295,7 @@ async function generateLookupCondition(
qbP.whereNotIn(parentColumn.column_name, qb);
else qbP.whereIn(parentColumn.column_name, qb);
};
} else if (relationColumnOptions.type === RelationTypes.BELONGS_TO) {
} else if (relationType === RelationTypes.BELONGS_TO) {
qb = knex(
knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(parentModel.table_name),
@ -1312,7 +1329,7 @@ async function generateLookupCondition(
);
else qbP.whereIn(childColumn.column_name, qb);
};
} else if (relationColumnOptions.type === RelationTypes.MANY_TO_MANY) {
} else if (relationType === RelationTypes.MANY_TO_MANY) {
const mmModel = await relationColumnOptions.getMMModel();
const mmParentColumn = await relationColumnOptions.getMMParentColumn();
const mmChildColumn = await relationColumnOptions.getMMChildColumn();

95
packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts

@ -2,6 +2,7 @@ import jsep from 'jsep';
import {
FormulaDataTypes,
jsepCurlyHook,
RelationTypes,
UITypes,
validateDateWithUnknownFormat,
validateFormulaAndExtractTreeWithType,
@ -141,14 +142,14 @@ async function _formulaQueryBuilder(
aliasToColumn[col.id] = async (): Promise<any> => {
let aliasCount = 0;
let selectQb;
let isMany = false;
let isArray = false;
const alias = `__nc_formula${aliasCount++}`;
const lookup = await col.getColOptions<LookupColumn>();
{
const relationCol = await lookup.getRelationColumn();
const relation =
await relationCol.getColOptions<LinkToAnotherRecordColumn>();
// if (relation.type !== 'bt') continue;
// if (relation.type !== RelationTypes.BELONGS_TO) continue;
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
@ -156,8 +157,17 @@ async function _formulaQueryBuilder(
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
switch (relation.type) {
case 'bt':
let relationType = relation.type;
if (relationType === RelationTypes.ONE_TO_ONE) {
relationType = relationCol.meta?.bt
? RelationTypes.BELONGS_TO
: RelationTypes.HAS_MANY;
}
switch (relationType) {
case RelationTypes.BELONGS_TO:
selectQb = knex(
knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(parentModel.table_name),
@ -173,8 +183,8 @@ async function _formulaQueryBuilder(
]),
);
break;
case 'hm':
isMany = true;
case RelationTypes.HAS_MANY:
isArray = relation.type !== RelationTypes.ONE_TO_ONE;
selectQb = knex(
knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(childModel.table_name),
@ -190,9 +200,9 @@ async function _formulaQueryBuilder(
]),
);
break;
case 'mm':
case RelationTypes.MANY_TO_MANY:
{
isMany = true;
isArray = true;
const mmModel = await relation.getMMModel();
const mmParentColumn = await relation.getMMParentColumn();
const mmChildColumn = await relation.getMMChildColumn();
@ -236,7 +246,7 @@ async function _formulaQueryBuilder(
await relationCol.getColOptions<LinkToAnotherRecordColumn>();
// if any of the relation in nested lookup is
// not belongs to then ignore the sort option
// if (relation.type !== 'bt') continue;
// if (relation.type !== RelationTypes.BELONGS_TO) continue;
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
@ -245,8 +255,16 @@ async function _formulaQueryBuilder(
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
switch (relation.type) {
case 'bt':
let relationType = relation.type;
if (relationType === RelationTypes.ONE_TO_ONE) {
relationType = relationCol.meta?.bt
? RelationTypes.BELONGS_TO
: RelationTypes.HAS_MANY;
}
switch (relationType) {
case RelationTypes.BELONGS_TO:
{
selectQb.join(
knex.raw(`?? as ??`, [
@ -258,9 +276,9 @@ async function _formulaQueryBuilder(
);
}
break;
case 'hm':
case RelationTypes.HAS_MANY:
{
isMany = true;
isArray = relation.type !== RelationTypes.ONE_TO_ONE;
selectQb.join(
knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(childModel.table_name),
@ -271,8 +289,8 @@ async function _formulaQueryBuilder(
);
}
break;
case 'mm': {
isMany = true;
case RelationTypes.MANY_TO_MANY: {
isArray = true;
const mmModel = await relation.getMMModel();
const mmParentColumn = await relation.getMMParentColumn();
const mmChildColumn = await relation.getMMChildColumn();
@ -324,7 +342,7 @@ async function _formulaQueryBuilder(
).builder;
// selectQb.select(builder);
if (isMany) {
if (isArray) {
const qb = selectQb;
selectQb = (fn) =>
knex
@ -346,7 +364,7 @@ async function _formulaQueryBuilder(
const nestedAlias = `__nc_formula${aliasCount++}`;
const relation =
await lookupColumn.getColOptions<LinkToAnotherRecordColumn>();
// if (relation.type !== 'bt') continue;
// if (relation.type !== RelationTypes.BELONGS_TO) continue;
const colOptions =
(await lookupColumn.getColOptions()) as LinkToAnotherRecordColumn;
@ -357,8 +375,17 @@ async function _formulaQueryBuilder(
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
let cn;
switch (relation.type) {
case 'bt':
let relationType = relation.type;
if (relationType === RelationTypes.ONE_TO_ONE) {
relationType = relationCol.meta?.bt
? RelationTypes.BELONGS_TO
: RelationTypes.HAS_MANY;
}
switch (relationType) {
case RelationTypes.BELONGS_TO:
{
selectQb.join(
knex.raw(`?? as ??`, [
@ -374,9 +401,9 @@ async function _formulaQueryBuilder(
]);
}
break;
case 'hm':
case RelationTypes.HAS_MANY:
{
isMany = true;
isArray = relation.type !== RelationTypes.ONE_TO_ONE;
selectQb.join(
knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(childModel.table_name),
@ -391,9 +418,9 @@ async function _formulaQueryBuilder(
]);
}
break;
case 'mm':
case RelationTypes.MANY_TO_MANY:
{
isMany = true;
isArray = true;
const mmModel = await relation.getMMModel();
const mmParentColumn =
await relation.getMMParentColumn();
@ -434,7 +461,7 @@ async function _formulaQueryBuilder(
`${prevAlias}.${childColumn.column_name}`,
);
if (isMany) {
if (isArray) {
const qb = selectQb;
selectQb = (fn) =>
knex
@ -464,7 +491,7 @@ async function _formulaQueryBuilder(
aliasToColumn,
formulaOption.getParsedTree(),
);
if (isMany) {
if (isArray) {
const qb = selectQb;
selectQb = (fn) =>
knex
@ -483,7 +510,7 @@ async function _formulaQueryBuilder(
break;
default:
{
if (isMany) {
if (isArray) {
const qb = selectQb;
selectQb = (fn) =>
knex
@ -529,7 +556,7 @@ async function _formulaQueryBuilder(
aliasToColumn[col.id] = async (): Promise<any> => {
const alias = `__nc_formula_ll`;
const relation = await col.getColOptions<LinkToAnotherRecordColumn>();
// if (relation.type !== 'bt') continue;
// if (relation.type !== RelationTypes.BELONGS_TO) continue;
const colOptions =
(await col.getColOptions()) as LinkToAnotherRecordColumn;
@ -540,8 +567,16 @@ async function _formulaQueryBuilder(
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
let relationType = relation.type;
if (relationType === RelationTypes.ONE_TO_ONE) {
relationType = col.meta?.bt
? RelationTypes.BELONGS_TO
: RelationTypes.HAS_MANY;
}
let selectQb;
if (relation.type === 'bt') {
if (relationType === RelationTypes.BELONGS_TO) {
selectQb = knex(baseModelSqlv2.getTnPath(parentModel.table_name))
.select(parentModel?.displayValue?.column_name)
.where(
@ -555,7 +590,7 @@ async function _formulaQueryBuilder(
}.${childColumn.column_name}`,
]),
);
} else if (relation.type == 'hm') {
} else if (relationType == RelationTypes.HAS_MANY) {
const qb = knex(baseModelSqlv2.getTnPath(childModel.table_name))
// .select(knex.raw(`GROUP_CONCAT(??)`, [childModel?.pv?.title]))
.where(
@ -582,7 +617,7 @@ async function _formulaQueryBuilder(
.wrap('(', ')');
// getAggregateFn();
} else if (relation.type == 'mm') {
} else if (relationType == RelationTypes.MANY_TO_MANY) {
// todo:
// const qb = knex(childModel.title)
// // .select(knex.raw(`GROUP_CONCAT(??)`, [childModel?.pv?.title]))

21
packages/nocodb/src/db/genRollupSelectv2.ts

@ -51,6 +51,27 @@ export default async function ({
knex.ref(`${refTableAlias}.${childCol.column_name}`),
),
};
case RelationTypes.ONE_TO_ONE:
return {
builder: knex(
knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(childModel?.table_name),
refTableAlias,
]),
)
[columnOptions.rollup_function as string]?.(
knex.ref(`${refTableAlias}.${rollupColumn.column_name}`),
)
.where(
knex.ref(
`${alias || baseModelSqlv2.getTnPath(parentModel.table_name)}.${
parentCol.column_name
}`,
),
'=',
knex.ref(`${refTableAlias}.${childCol.column_name}`),
),
};
case RelationTypes.MANY_TO_MANY: {
const mmModel = await relationColumnOption.getMMModel();
const mmChildCol = await relationColumnOption.getMMChildColumn();

39
packages/nocodb/src/db/generateLookupSelectQuery.ts

@ -70,8 +70,15 @@ export default async function generateLookupSelectQuery({
const relation =
await relationCol.getColOptions<LinkToAnotherRecordColumn>();
// if not belongs to then throw error as we don't support
if (relation.type === RelationTypes.BELONGS_TO) {
let relationType = relation.type;
if (relationType === RelationTypes.ONE_TO_ONE) {
relationType = relationCol.meta?.bt
? RelationTypes.BELONGS_TO
: RelationTypes.HAS_MANY;
}
if (relationType === RelationTypes.BELONGS_TO) {
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
@ -92,10 +99,7 @@ export default async function generateLookupSelectQuery({
}`,
]),
);
}
// if not belongs to then throw error as we don't support
else if (relation.type === RelationTypes.HAS_MANY) {
} else if (relationType === RelationTypes.HAS_MANY) {
isBtLookup = false;
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
@ -117,10 +121,7 @@ export default async function generateLookupSelectQuery({
}`,
]),
);
}
// if not belongs to then throw error as we don't support
else if (relation.type === RelationTypes.MANY_TO_MANY) {
} else if (relationType === RelationTypes.MANY_TO_MANY) {
isBtLookup = false;
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
@ -191,9 +192,17 @@ export default async function generateLookupSelectQuery({
const relation =
await relationCol.getColOptions<LinkToAnotherRecordColumn>();
let relationType = relation.type;
if (relationType === RelationTypes.ONE_TO_ONE) {
relationType = relationCol.meta?.bt
? RelationTypes.BELONGS_TO
: RelationTypes.HAS_MANY;
}
// if any of the relation in nested lookupColOpt is
// not belongs to then throw error as we don't support
if (relation.type === RelationTypes.BELONGS_TO) {
if (relationType === RelationTypes.BELONGS_TO) {
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
@ -209,7 +218,7 @@ export default async function generateLookupSelectQuery({
`${nestedAlias}.${parentColumn.column_name}`,
`${prevAlias}.${childColumn.column_name}`,
);
} else if (relation.type === RelationTypes.HAS_MANY) {
} else if (relationType === RelationTypes.HAS_MANY) {
isBtLookup = false;
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
@ -226,7 +235,7 @@ export default async function generateLookupSelectQuery({
`${nestedAlias}.${childColumn.column_name}`,
`${prevAlias}.${parentColumn.column_name}`,
);
} else if (relation.type === RelationTypes.MANY_TO_MANY) {
} else if (relationType === RelationTypes.MANY_TO_MANY) {
isBtLookup = false;
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
@ -425,8 +434,6 @@ export default async function generateLookupSelectQuery({
};
}
NcError.notImplemented(
'Database not supported this operation on Lookup/LTAR',
);
NcError.notImplemented('This operation on Lookup/LTAR for this database');
}
}

2
packages/nocodb/src/db/sortV2.ts

@ -33,7 +33,7 @@ export default async function sortV2(
const column = await getRefColumnIfAlias(await sort.getColumn());
if (!column) {
if (throwErrorIfInvalid) {
NcError.unprocessableEntity(`Invalid field: ${sort.fk_column_id}`);
NcError.fieldNotFound(sort.fk_column_id);
}
continue;
}

1
packages/nocodb/src/db/sql-client/lib/mysql/MysqlClient.ts

@ -2496,6 +2496,7 @@ class MysqlClient extends KnexClient {
query += n.un ? ' UNSIGNED' : '';
query += n.rqd ? ' NOT NULL' : ' NULL';
query += n.ai ? ' auto_increment' : '';
query += n.unique ? ` UNIQUE` : '';
const defaultValue = this.sanitiseDefaultValue(n.cdf);
query += defaultValue
? `

2
packages/nocodb/src/db/sql-client/lib/pg/PgClient.ts

@ -2863,6 +2863,7 @@ class PGClient extends KnexClient {
);
query += n.rqd ? ' NOT NULL' : ' NULL';
query += defaultValue ? ` DEFAULT ${defaultValue}` : '';
query += n.unique ? ` UNIQUE` : '';
}
} else if (change === 1) {
query += this.genQuery(
@ -2872,6 +2873,7 @@ class PGClient extends KnexClient {
);
query += n.rqd ? ' NOT NULL' : ' NULL';
query += defaultValue ? ` DEFAULT ${defaultValue}` : '';
query += n.unique ? ` UNIQUE` : '';
query = this.genQuery(`ALTER TABLE ?? ${query};`, [t], shouldSanitize);
} else {
if (n.cn !== o.cn) {

5
packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts

@ -2132,6 +2132,7 @@ class SqliteClient extends KnexClient {
? ' '
: ` DEFAULT ''`;
addNewColumnQuery += n.rqd ? ` NOT NULL` : ' ';
query += n.unique ? ` UNIQUE` : '';
addNewColumnQuery = this.genQuery(
`ALTER TABLE ?? ${addNewColumnQuery};`,
[t],
@ -2161,6 +2162,8 @@ class SqliteClient extends KnexClient {
query += n.dtxp && n.dt !== 'text' ? `(${this.genRaw(n.dtxp)})` : '';
query += n.cdf ? ` DEFAULT ${this.genValue(n.cdf)}` : ' ';
query += n.rqd ? ` NOT NULL` : ' ';
// todo: unique constraint should be added using index
// query += n.unique ? ` UNIQUE` : '';
} else if (change === 1) {
shouldSanitize = true;
query += this.genQuery(
@ -2175,6 +2178,8 @@ class SqliteClient extends KnexClient {
? ' '
: ` DEFAULT ''`;
query += n.rqd ? ` NOT NULL` : ' ';
// todo: unique constraint should be added using index
// query += n.unique ? ` UNIQUE` : '';
query = this.genQuery(`ALTER TABLE ?? ${query};`, [t], shouldSanitize);
} else {
// if(n.cn!==o.cno) {

20
packages/nocodb/src/filters/global-exception/global-exception.filter.ts

@ -8,9 +8,8 @@ import {
BadRequest,
extractDBError,
Forbidden,
InternalServerError,
NcBaseErrorv2,
NotFound,
NotImplemented,
Unauthorized,
UnprocessableEntity,
} from '~/helpers/catchError';
@ -37,7 +36,6 @@ export class GlobalExceptionFilter implements ExceptionFilter {
exception instanceof Unauthorized ||
exception instanceof Forbidden ||
exception instanceof NotFound ||
exception instanceof NotImplemented ||
exception instanceof UnprocessableEntity ||
exception instanceof NotFoundException ||
exception instanceof ThrottlerException
@ -83,22 +81,18 @@ export class GlobalExceptionFilter implements ExceptionFilter {
exception.getStatus?.() === 404
) {
return response.status(404).json({ msg: exception.message });
} else if (
exception instanceof InternalServerError ||
exception.getStatus?.() === 500
) {
return response.status(500).json({ msg: exception.message });
} else if (
exception instanceof NotImplemented ||
exception.getStatus?.() === 501
) {
return response.status(501).json({ msg: exception.message });
} else if (exception instanceof AjvError) {
return response
.status(400)
.json({ msg: exception.message, errors: exception.errors });
} else if (exception instanceof UnprocessableEntity) {
return response.status(422).json({ msg: exception.message });
} else if (exception instanceof NcBaseErrorv2) {
return response.status(exception.code).json({
error: exception.error,
message: exception.message,
details: exception.details,
});
}
// handle different types of exceptions

6
packages/nocodb/src/helpers/NcPluginMgrv2.ts

@ -249,7 +249,7 @@ class NcPluginMgrv2 {
await tempPlugin.init(args?.input);
if (!tempPlugin?.getAdapter()?.test)
NcError.notImplemented('Plugin test is not implemented');
NcError.notImplemented('Plugin Test');
return tempPlugin?.getAdapter()?.test?.();
}
@ -263,7 +263,7 @@ class NcPluginMgrv2 {
await tempPlugin.init(args?.input);
if (!tempPlugin?.getAdapter()?.test)
NcError.notImplemented('Plugin test is not implemented');
NcError.notImplemented('Plugin Test');
return tempPlugin?.getAdapter()?.test?.();
}
@ -276,7 +276,7 @@ class NcPluginMgrv2 {
await tempPlugin.init(args?.input);
if (!tempPlugin?.getAdapter()?.test)
NcError.notImplemented('Plugin test is not implemented');
NcError.notImplemented('Plugin Test');
return tempPlugin?.getAdapter()?.test?.();
}

2
packages/nocodb/src/helpers/PagedResponse.ts

@ -42,7 +42,7 @@ export class PagedResponseImpl<T> {
if (additionalProps) Object.assign(this, additionalProps);
if (offset && offset >= +count) {
NcError.badRequest('Offset is beyond the total number of records');
NcError.invalidOffsetValue(offset);
}
}

298
packages/nocodb/src/helpers/catchError.ts

@ -1,5 +1,7 @@
import { NcErrorType } from 'nocodb-sdk';
import type { NextFunction, Request, Response } from 'express';
import type { ErrorObject } from 'ajv';
import { defaultLimitConfig } from '~/helpers/extractLimitAndOffset';
export enum DBError {
TABLE_EXIST = 'TABLE_EXIST',
@ -372,6 +374,12 @@ export function extractDBError(error): {
case 'EHOSTDOWN':
message = 'The host is down.';
break;
default:
// if error message contains -- then extract message after --
if (error.message && error.message.includes('--')) {
message = error.message.split('--')[1];
}
break;
}
if (message) {
@ -400,7 +408,6 @@ export default function (
e instanceof Unauthorized ||
e instanceof Forbidden ||
e instanceof NotFound ||
e instanceof NotImplemented ||
e instanceof UnprocessableEntity
)
)
@ -409,7 +416,15 @@ export default function (
const dbError = extractDBError(e);
if (dbError) {
return res.status(400).json(dbError);
const error = new NcBaseErrorv2(NcErrorType.DATABASE_ERROR, {
params: dbError.message,
details: dbError,
});
return res.status(error.code).json({
error: error.error,
message: error.message,
details: error.details,
});
}
if (e instanceof BadRequest) {
@ -420,16 +435,16 @@ export default function (
return res.status(403).json({ msg: e.message });
} else if (e instanceof NotFound) {
return res.status(404).json({ msg: e.message });
} else if (e instanceof InternalServerError) {
return res.status(500).json({ msg: e.message });
} else if (e instanceof NotImplemented) {
return res.status(501).json({ msg: e.message });
} else if (e instanceof AjvError) {
return res.status(400).json({ msg: e.message, errors: e.errors });
} else if (e instanceof UnprocessableEntity) {
return res.status(422).json({ msg: e.message });
} else if (e instanceof NotAllowed) {
return res.status(405).json({ msg: e.message });
} else if (e instanceof NcBaseErrorv2) {
return res
.status(e.code)
.json({ error: e.error, message: e.message, details: e.details });
}
// if some other error occurs then send 500 and a generic message
res.status(500).json({ msg: 'Internal server error' });
@ -453,10 +468,6 @@ export class Forbidden extends NcBaseError {}
export class NotFound extends NcBaseError {}
export class InternalServerError extends NcBaseError {}
export class NotImplemented extends NcBaseError {}
export class UnprocessableEntity extends NcBaseError {}
export class AjvError extends NcBaseError {
@ -468,7 +479,266 @@ export class AjvError extends NcBaseError {
errors: ErrorObject[];
}
const errorHelpers: {
[key in NcErrorType]: {
message: string | ((...params: string[]) => string);
code: number;
};
} = {
[NcErrorType.INTERNAL_SERVER_ERROR]: {
message: (message: string) => message || `Internal server error`,
code: 500,
},
[NcErrorType.DATABASE_ERROR]: {
message: (message: string) =>
message || `There was an error while running the query`,
code: 500,
},
[NcErrorType.AUTHENTICATION_REQUIRED]: {
message: 'Authentication required to access this resource',
code: 401,
},
[NcErrorType.API_TOKEN_NOT_ALLOWED]: {
message: 'This request is not allowed with API token',
code: 401,
},
[NcErrorType.WORKSPACE_NOT_FOUND]: {
message: (id: string) => `Workspace '${id}' not found`,
code: 404,
},
[NcErrorType.BASE_NOT_FOUND]: {
message: (id: string) => `Base '${id}' not found`,
code: 404,
},
[NcErrorType.SOURCE_NOT_FOUND]: {
message: (id: string) => `Source '${id}' not found`,
code: 404,
},
[NcErrorType.TABLE_NOT_FOUND]: {
message: (id: string) => `Table '${id}' not found`,
code: 404,
},
[NcErrorType.VIEW_NOT_FOUND]: {
message: (id: string) => `View '${id}' not found`,
code: 404,
},
[NcErrorType.FIELD_NOT_FOUND]: {
message: (id: string) => `Field '${id}' not found`,
code: 404,
},
[NcErrorType.RECORD_NOT_FOUND]: {
message: (...ids: string[]) => {
const isMultiple = Array.isArray(ids) && ids.length > 1;
return `Record${isMultiple ? 's' : ''} '${ids.join(', ')}' not found`;
},
code: 404,
},
[NcErrorType.ERROR_DUPLICATE_RECORD]: {
message: (...ids: string[]) => {
const isMultiple = Array.isArray(ids) && ids.length > 1;
return `Record${isMultiple ? 's' : ''} '${ids.join(
', ',
)}' already exists`;
},
code: 422,
},
[NcErrorType.USER_NOT_FOUND]: {
message: (idOrEmail: string) => {
const isEmail = idOrEmail.includes('@');
return `User ${
isEmail ? 'with email' : 'with id'
} '${idOrEmail}' not found`;
},
code: 404,
},
[NcErrorType.INVALID_OFFSET_VALUE]: {
message: (offset: string) => `Offset value '${offset}' is invalid`,
code: 422,
},
[NcErrorType.INVALID_LIMIT_VALUE]: {
message: `Limit value should be between ${defaultLimitConfig.limitMin} and ${defaultLimitConfig.limitMax}`,
code: 422,
},
[NcErrorType.INVALID_FILTER]: {
message: (filter: string) => `Filter '${filter}' is invalid`,
code: 422,
},
[NcErrorType.INVALID_SHARED_VIEW_PASSWORD]: {
message: 'Invalid shared view password',
code: 403,
},
[NcErrorType.NOT_IMPLEMENTED]: {
message: (feature: string) => `${feature} is not implemented`,
code: 501,
},
};
function generateError(
type: NcErrorType,
args?: NcErrorArgs,
): {
message: string;
code: number;
details?: any;
} {
const errorHelper = errorHelpers[type];
const { params, customMessage, details } = args || {};
if (!errorHelper) {
return {
message: 'An error occurred',
code: 500,
details: details,
};
}
let message: string;
const messageHelper = customMessage || errorHelper.message;
if (typeof messageHelper === 'function') {
message = messageHelper(...(Array.isArray(params) ? params : [params]));
} else {
message = messageHelper;
}
return {
message,
code: errorHelper.code,
details: details,
};
}
type NcErrorArgs = {
params?: string | string[];
customMessage?: string | ((...args: string[]) => string);
details?: any;
};
export class NcBaseErrorv2 extends NcBaseError {
error: NcErrorType;
code: number;
details?: any;
constructor(error: NcErrorType, args?: NcErrorArgs) {
const errorHelper = generateError(error, args);
super(errorHelper.message);
this.error = error;
this.code = errorHelper.code;
this.details = args?.details;
}
}
export class NcError {
static authenticationRequired(args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.AUTHENTICATION_REQUIRED, args);
}
static apiTokenNotAllowed(args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.API_TOKEN_NOT_ALLOWED, args);
}
static workspaceNotFound(id: string, args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.WORKSPACE_NOT_FOUND, {
params: id,
...args,
});
}
static baseNotFound(id: string, args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.BASE_NOT_FOUND, {
params: id,
...args,
});
}
static sourceNotFound(id: string, args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.SOURCE_NOT_FOUND, {
params: id,
...args,
});
}
static tableNotFound(id: string, args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.TABLE_NOT_FOUND, {
params: id,
...args,
});
}
static userNotFound(id: string, args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.USER_NOT_FOUND, {
params: id,
...args,
});
}
static viewNotFound(id: string, args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.VIEW_NOT_FOUND, {
params: id,
...args,
});
}
static recordNotFound(id: string | string[], args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.RECORD_NOT_FOUND, {
params: id,
...args,
});
}
static duplicateRecord(id: string | string[], args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.ERROR_DUPLICATE_RECORD, {
params: id,
...args,
});
}
static fieldNotFound(id: string, args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.FIELD_NOT_FOUND, {
params: id,
...args,
});
}
static invalidOffsetValue(offset: string | number, args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.INVALID_OFFSET_VALUE, {
params: `${offset}`,
...args,
});
}
static invalidLimitValue(args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.INVALID_LIMIT_VALUE, {
...args,
});
}
static invalidFilter(filter: string, args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.INVALID_FILTER, {
params: filter,
...args,
});
}
static invalidSharedViewPassword(args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.INVALID_SHARED_VIEW_PASSWORD, {
...args,
});
}
static notImplemented(feature: string = 'Feature', args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.NOT_IMPLEMENTED, {
params: feature,
...args,
});
}
static internalServerError(message: string, args?: NcErrorArgs) {
throw new NcBaseErrorv2(NcErrorType.INTERNAL_SERVER_ERROR, {
params: message,
...args,
});
}
static notFound(message = 'Not found') {
throw new NotFound(message);
}
@ -485,14 +755,6 @@ export class NcError {
throw new Forbidden(message);
}
static internalServerError(message = 'Internal server error') {
throw new InternalServerError(message);
}
static notImplemented(message = 'Not implemented') {
throw new NotImplemented(message);
}
static ajvValidationError(param: { message: string; errors: ErrorObject[] }) {
throw new AjvError(param);
}

108
packages/nocodb/src/helpers/columnHelpers.ts

@ -1,12 +1,15 @@
import { customAlphabet } from 'nanoid';
import { getAvailableRollupForUiType, UITypes } from 'nocodb-sdk';
import {
getAvailableRollupForUiType,
RelationTypes,
UITypes,
} from 'nocodb-sdk';
import { pluralize, singularize } from 'inflection';
import type {
BoolType,
ColumnReqType,
LinkToAnotherRecordType,
LookupColumnReqType,
RelationTypes,
RollupColumnReqType,
TableType,
} from 'nocodb-sdk';
@ -92,6 +95,90 @@ export async function createHmAndBtColumn(
}
}
/**
* Creates a column with a one-to-one (1:1) relationship.
* @param {Model} child - The child model.
* @param {Model} parent - The parent model.
* @param {Column} childColumn - The child column.
* @param {RelationTypes} [type] - The type of relationship.
* @param {string} [alias] - The alias for the column.
* @param {string} [fkColName] - The foreign key column name.
* @param {BoolType} [virtual=false] - Whether the column is virtual.
* @param {boolean} [isSystemCol=false] - Whether the column is a system column.
* @param {any} [columnMeta=null] - Metadata for the column.
* @param {any} [colExtra] - Additional column parameters.
*/
export async function createOOColumn(
child: Model,
parent: Model,
childColumn: Column,
type?: RelationTypes,
alias?: string,
fkColName?: string,
virtual: BoolType = false,
isSystemCol = false,
columnMeta = null,
colExtra?: any,
) {
// save bt column
{
const title = getUniqueColumnAliasName(
await child.getColumns(),
`${parent.title}`,
);
await Column.insert<LinkToAnotherRecordColumn>({
title,
fk_model_id: child.id,
// ref_db_alias
uidt: UITypes.LinkToAnotherRecord,
type: RelationTypes.ONE_TO_ONE,
fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id,
fk_related_model_id: parent.id,
virtual,
// if self referencing treat it as system field to hide from ui
system: isSystemCol || parent.id === child.id,
fk_col_name: fkColName,
fk_index_name: fkColName,
// ...(colExtra || {}),
meta: {
...(colExtra?.meta || {}),
// one-to-one relation is combination of both hm and bt to identify table which have
// foreign key column(similar to bt) we are adding a boolean flag `bt` under meta
bt: true,
},
});
}
// save hm column
{
const title = getUniqueColumnAliasName(
await parent.getColumns(),
alias || child.title,
);
const meta = {
plural: columnMeta?.plural || pluralize(child.title),
singular: columnMeta?.singular || singularize(child.title),
};
await Column.insert({
title,
fk_model_id: parent.id,
uidt: UITypes.LinkToAnotherRecord,
type: 'oo',
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,
meta,
...(colExtra || {}),
});
}
}
export async function validateRollupPayload(payload: ColumnReqType | Column) {
validateParams(
[
@ -176,12 +263,10 @@ export async function validateLookupPayload(
);
}
}
const relation = await (
await Column.get({
colId: (payload as LookupColumnReqType).fk_relation_column_id,
})
).getColOptions<LinkToAnotherRecordType>();
const column = await Column.get({
colId: (payload as LookupColumnReqType).fk_relation_column_id,
});
const relation = await column.getColOptions<LinkToAnotherRecordType>();
if (!relation) {
throw new Error('Relation column not found');
@ -200,6 +285,13 @@ export async function validateLookupPayload(
colId: relation.fk_parent_column_id,
});
break;
case 'oo':
relatedColumn = await Column.get({
colId: column.meta?.bt
? relation.fk_parent_column_id
: relation.fk_child_column_id,
});
break;
}
const relatedTable = await relatedColumn.getModel();

14
packages/nocodb/src/helpers/getAst.ts

@ -115,7 +115,7 @@ const getAst = async ({
(f) => !colAliasMap[f] && !aliasColMap[f],
);
if (invalidFields.length) {
NcError.unprocessableEntity(`Invalid field: ${invalidFields[0]}`);
NcError.fieldNotFound(invalidFields.join(', '));
}
}
} else {
@ -287,7 +287,17 @@ const extractRelationDependencies = async (
dependencyFields.fieldsSet.add(
await relationColumnOpts.getChildColumn().then((col) => col.title),
);
break;
case RelationTypes.ONE_TO_ONE:
if (relationColumn.meta?.bt) {
dependencyFields.fieldsSet.add(
await relationColumnOpts.getChildColumn().then((col) => col.title),
);
} else {
dependencyFields.fieldsSet.add(
await relationColumnOpts.getParentColumn().then((col) => col.title),
);
}
break;
}
};

4
packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts

@ -28,6 +28,7 @@ import * as nc_038_formula_parsed_tree_column from '~/meta/migrations/v2/nc_038_
import * as nc_039_sqlite_alter_column_types from '~/meta/migrations/v2/nc_039_sqlite_alter_column_types';
import * as nc_040_form_view_alter_column_types from '~/meta/migrations/v2/nc_040_form_view_alter_column_types';
import * as nc_041_calendar_view from '~/meta/migrations/v2/nc_041_calendar_view';
import * as nc_042_user_block from '~/meta/migrations/v2/nc_042_user_block';
// Create a custom migration source class
export default class XcMigrationSourcev2 {
@ -67,6 +68,7 @@ export default class XcMigrationSourcev2 {
'nc_039_sqlite_alter_column_types',
'nc_040_form_view_alter_column_types',
'nc_041_calendar_view',
'nc_042_user_block',
]);
}
@ -136,6 +138,8 @@ export default class XcMigrationSourcev2 {
return nc_040_form_view_alter_column_types;
case 'nc_041_calendar_view':
return nc_041_calendar_view;
case 'nc_042_user_block':
return nc_042_user_block;
}
}
}

18
packages/nocodb/src/meta/migrations/v2/nc_042_user_block.ts

@ -0,0 +1,18 @@
import type { Knex } from 'knex';
import { MetaTable } from '~/utils/globals';
const up = async (knex: Knex) => {
await knex.schema.alterTable(MetaTable.USERS, (table) => {
table.boolean('blocked').defaultTo(false);
table.string('blocked_reason');
});
};
const down = async (knex: Knex) => {
await knex.schema.alterTable(MetaTable.USERS, (table) => {
table.dropColumn('blocked');
table.dropColumn('blocked_reason');
});
};
export { up, down };

2
packages/nocodb/src/middlewares/extract-ids/extract-ids.middleware.ts

@ -253,7 +253,7 @@ export class AclMiddleware implements NestInterceptor {
const roles: Record<string, boolean> = extractRolesObj(userScopeRole);
if (req?.user?.is_api_token && blockApiTokenAccess) {
NcError.forbidden('Not allowed with API token');
NcError.apiTokenNotAllowed();
}
if (
(!allowedRoles || allowedRoles.some((role) => roles?.[role])) &&

2
packages/nocodb/src/models/Filter.ts

@ -96,7 +96,7 @@ export default class Filter implements FilterType {
} else if (filter.fk_column_id) {
model = await Column.get({ colId: filter.fk_column_id }, ncMeta);
} else {
NcError.badRequest('Invalid filter');
NcError.invalidFilter(JSON.stringify(filter));
}
if (model != null) {

2
packages/nocodb/src/models/LinkToAnotherRecordColumn.ts

@ -20,7 +20,7 @@ export default class LinkToAnotherRecordColumn {
ur?: string;
fk_index_name?: string;
type: 'hm' | 'bt' | 'mm';
type: 'hm' | 'bt' | 'mm' | 'oo';
virtual: BoolType = false;
mmModel?: Model;

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

@ -765,7 +765,7 @@ export default class Model implements TableType {
const model = await this.getWithInfo({ id: tableId });
const newPvCol = model.columns.find((c) => c.id === columnId);
if (!newPvCol) NcError.badRequest('Column not found');
if (!newPvCol) NcError.fieldNotFound(columnId);
// drop existing primary column/s
for (const col of model.columns?.filter((c) => c.pv) || []) {

2
packages/nocodb/src/models/Source.ts

@ -112,7 +112,7 @@ export default class Source implements SourceType {
) {
const oldBase = await Source.get(sourceId, false, ncMeta);
if (!oldBase) NcError.badRequest('Wrong source id!');
if (!oldBase) NcError.sourceNotFound(sourceId);
const updateObj = extractProps(source, [
'alias',

9
packages/nocodb/src/models/User.ts

@ -33,6 +33,9 @@ export default class User implements UserType {
display_name?: string;
avatar?: string;
blocked?: boolean;
blocked_reason?: string;
constructor(data: User) {
Object.assign(this, data);
}
@ -246,7 +249,7 @@ export default class User implements UserType {
const user = await this.get(userId, ncMeta);
if (!user) NcError.badRequest('User not found');
if (!user) NcError.userNotFound(userId);
// clear all user related cache
await this.clearCache(userId, ncMeta);
@ -264,7 +267,7 @@ export default class User implements UserType {
) {
const user = args.user ?? (await this.get(userId, ncMeta));
if (!user) NcError.badRequest('User not found');
if (!user) NcError.userNotFound(userId);
const baseRoles = await new Promise((resolve) => {
if (args.baseId) {
@ -292,7 +295,7 @@ export default class User implements UserType {
protected static async clearCache(userId: string, ncMeta = Noco.ncMeta) {
const user = await this.get(userId, ncMeta);
if (!user) NcError.badRequest('User not found');
if (!user) NcError.userNotFound(userId);
// todo: skip base user cache delete based on flag
const bases = await BaseUser.getProjectsList(userId, {}, ncMeta);

7
packages/nocodb/src/modules/datas/helpers.ts

@ -37,7 +37,7 @@ export async function getViewAndModelByAliasOrId(param: {
aliasOrId: param.tableName,
});
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(param.tableName);
const view =
param.viewName &&
@ -45,7 +45,7 @@ export async function getViewAndModelByAliasOrId(param: {
titleOrId: param.viewName,
fk_model_id: model.id,
}));
if (param.viewName && !view) NcError.notFound('View not found');
if (param.viewName && !view) NcError.viewNotFound(param.viewName);
return { model, view };
}
@ -234,8 +234,7 @@ export async function getColumnByIdOrName(
c.column_name === columnNameOrId,
);
if (!column)
NcError.notFound(`Column with id/name '${columnNameOrId}' is not found`);
if (!column) NcError.fieldNotFound(columnNameOrId);
return column;
}

16
packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts

@ -3,7 +3,7 @@ import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import papaparse from 'papaparse';
import debug from 'debug';
import { isLinksOrLTAR, isVirtualCol } from 'nocodb-sdk';
import { isLinksOrLTAR, isVirtualCol, RelationTypes } from 'nocodb-sdk';
import { Base, Column, Model, Source } from '~/models';
import { BasesService } from '~/services/bases.service';
import {
@ -202,7 +202,9 @@ export class DuplicateProcessor {
.filter(
(c) =>
isLinksOrLTAR(c) &&
c.colOptions.type === 'bt' &&
(c.colOptions.type === RelationTypes.BELONGS_TO ||
(c.colOptions.type === RelationTypes.ONE_TO_ONE &&
c.meta?.bt)) &&
c.colOptions.fk_related_model_id === modelId,
)
.map((c) => c.id);
@ -342,7 +344,9 @@ export class DuplicateProcessor {
.filter(
(c) =>
isLinksOrLTAR(c) &&
c.colOptions.type === 'bt' &&
(c.colOptions.type === RelationTypes.BELONGS_TO ||
(c.colOptions.type === RelationTypes.ONE_TO_ONE &&
c.meta?.bt)) &&
c.colOptions.fk_related_model_id === sourceModel.id,
)
.map((c) => c.id);
@ -522,7 +526,11 @@ export class DuplicateProcessor {
colId: id,
});
if (col) {
if (col.colOptions?.type === 'bt') {
if (
col.colOptions?.type === RelationTypes.BELONGS_TO ||
(col.colOptions?.type === RelationTypes.ONE_TO_ONE &&
col.meta?.bt)
) {
const childCol = await Column.get({
source_id: destBase.id,
colId: col.colOptions.fk_child_column_id,

11
packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts

@ -1,5 +1,5 @@
import { Readable } from 'stream';
import { isLinksOrLTAR, UITypes, ViewTypes } from 'nocodb-sdk';
import { isLinksOrLTAR, RelationTypes, UITypes, ViewTypes } from 'nocodb-sdk';
import { unparse } from 'papaparse';
import debug from 'debug';
import { Injectable } from '@nestjs/common';
@ -47,8 +47,7 @@ export class ExportService {
let pgSerialLastVal;
if (!model)
return NcError.badRequest(`Model not found for id '${modelId}'`);
if (!model) return NcError.tableNotFound(modelId);
const fndProject = bases.find((p) => p.id === model.base_id);
const base = fndProject || (await Base.get(model.base_id));
@ -387,7 +386,8 @@ export class ExportService {
for (const column of model.columns.filter(
(col) =>
col.uidt === UITypes.LinkToAnotherRecord &&
col.colOptions?.type === 'bt',
(col.colOptions?.type === RelationTypes.BELONGS_TO ||
(col.colOptions?.type === RelationTypes.ONE_TO_ONE && col.meta?.bt)),
)) {
await column.getColOptions();
const fkCol = model.columns.find(
@ -702,8 +702,7 @@ export class ExportService {
const source = await Source.get(param.sourceId);
if (!source)
throw NcError.badRequest(`Source not found for id '${param.sourceId}'`);
if (!source) NcError.sourceNotFound(param.sourceId);
const base = await Base.get(source.base_id);

40
packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts

@ -1,4 +1,10 @@
import { isLinksOrLTAR, isVirtualCol, UITypes, ViewTypes } from 'nocodb-sdk';
import {
isLinksOrLTAR,
isVirtualCol,
RelationTypes,
UITypes,
ViewTypes,
} from 'nocodb-sdk';
import { Injectable, Logger } from '@nestjs/common';
import papaparse from 'papaparse';
import debug from 'debug';
@ -88,13 +94,11 @@ export class ImportService {
const base = await Base.get(param.baseId);
if (!base)
return NcError.badRequest(`Base not found for id '${param.baseId}'`);
if (!base) return NcError.baseNotFound(param.baseId);
const source = await Source.get(param.sourceId);
if (!source)
return NcError.badRequest(`Source not found for id '${param.sourceId}'`);
if (!source) return NcError.sourceNotFound(param.sourceId);
const tableReferences = new Map<string, Model>();
const linkMap = new Map<string, string>();
@ -317,7 +321,10 @@ export class ImportService {
}
}
}
} else if (colOptions.type === 'hm') {
} else if (
colOptions.type === RelationTypes.HAS_MANY ||
(colOptions.type === RelationTypes.ONE_TO_ONE && !col.meta?.bt)
) {
// delete col.column_name as it is not required and will cause ajv error (null for LTAR)
delete col.column_name;
@ -517,7 +524,10 @@ export class ImportService {
}
}
}
} else if (colOptions.type === 'hm') {
} else if (
colOptions.type === RelationTypes.HAS_MANY ||
(colOptions.type === RelationTypes.ONE_TO_ONE && !col.meta?.bt)
) {
if (
!linkMap.has(
`${colOptions.fk_parent_column_id}::${colOptions.fk_child_column_id}`,
@ -639,7 +649,10 @@ export class ImportService {
}
}
}
} else if (colOptions.type === 'bt') {
} else if (
colOptions.type === RelationTypes.BELONGS_TO ||
(colOptions.type === RelationTypes.ONE_TO_ONE && col.meta?.bt)
) {
if (
!linkMap.has(
`${colOptions.fk_parent_column_id}::${colOptions.fk_child_column_id}`,
@ -1342,9 +1355,8 @@ export class ImportService {
const destProject = await Base.get(baseId);
const destBase = await Source.get(sourceId);
if (!destProject || !destBase) {
throw NcError.badRequest('Base or Source not found');
}
if (!destProject) return NcError.baseNotFound(baseId);
if (!destBase) return NcError.sourceNotFound(sourceId);
switch (src.type) {
case 'local': {
@ -1466,7 +1478,11 @@ export class ImportService {
colId: id,
});
if (col) {
if (col.colOptions?.type === 'bt') {
if (
col.colOptions?.type === RelationTypes.BELONGS_TO ||
(col.colOptions?.type === RelationTypes.ONE_TO_ONE &&
col.meta?.bt)
) {
const childCol = await Column.get({
source_id: destBase.id,
colId: col.colOptions.fk_child_column_id,

6
packages/nocodb/src/schema/swagger-v2.json

@ -7901,7 +7901,8 @@
"enum": [
"mm",
"hm",
"bt"
"bt",
"oo"
]
},
"name": "relationType",
@ -15137,7 +15138,8 @@
"enum": [
"bt",
"hm",
"mm"
"mm",
"oo"
],
"type": "string",
"description": "The type of the relationship"

15
packages/nocodb/src/schema/swagger.json

@ -11983,7 +11983,8 @@
"enum": [
"mm",
"hm",
"bt"
"bt",
"oo"
]
},
"name": "relationType",
@ -12095,7 +12096,8 @@
"enum": [
"mm",
"hm",
"bt"
"bt",
"oo"
]
},
"name": "relationType",
@ -12320,7 +12322,8 @@
"enum": [
"mm",
"hm",
"bt"
"bt",
"oo"
]
},
"name": "relationType",
@ -12993,7 +12996,8 @@
"enum": [
"mm",
"hm",
"bt"
"bt",
"oo"
]
},
"name": "relationType",
@ -21298,7 +21302,8 @@
"enum": [
"bt",
"hm",
"mm"
"mm",
"oo"
],
"type": "string",
"description": "The type of the relationship"

4
packages/nocodb/src/services/api-docs/api-docs.service.ts

@ -9,7 +9,7 @@ export class ApiDocsService {
async swaggerJson(param: { baseId: string; siteUrl: string }) {
const base = await Base.get(param.baseId);
if (!base) NcError.notFound();
if (!base) NcError.baseNotFound(param.baseId);
const models = await Model.list({
base_id: param.baseId,
@ -38,7 +38,7 @@ export class ApiDocsService {
async swaggerJsonV2(param: { baseId: string; siteUrl: string }) {
const base = await Base.get(param.baseId);
if (!base) NcError.notFound();
if (!base) NcError.baseNotFound(param.baseId);
const models = await Model.list({
base_id: param.baseId,

5
packages/nocodb/src/services/api-docs/swaggerV2/templates/paths.ts

@ -322,6 +322,11 @@ export const getModelPaths = async (ctx: {
},
],
},
'Example 2': {
value: {
Id: 4,
},
},
},
},
},

8
packages/nocodb/src/services/base-users/base-users.service.ts

@ -92,7 +92,7 @@ export class BaseUsersService {
const base = await Base.get(param.baseId);
if (!base) {
return NcError.badRequest('Invalid base id');
return NcError.baseNotFound(param.baseId);
}
if (user) {
@ -102,7 +102,7 @@ export class BaseUsersService {
const base = await Base.get(param.baseId);
if (!base) {
return NcError.badRequest('Invalid base id');
return NcError.baseNotFound(param.baseId);
}
if (baseUser && baseUser.roles) {
@ -200,7 +200,7 @@ export class BaseUsersService {
const base = await Base.get(param.baseId);
if (!base) {
return NcError.badRequest('Invalid base id');
return NcError.baseNotFound(param.baseId);
}
if (param.baseUser.roles.includes(ProjectRoles.OWNER)) {
@ -306,7 +306,7 @@ export class BaseUsersService {
const base = await Base.get(param.baseId);
if (!base) {
return NcError.badRequest('Invalid base id');
return NcError.baseNotFound(param.baseId);
}
const invite_token = uuidv4();

2
packages/nocodb/src/services/bases.service.ts

@ -111,7 +111,7 @@ export class BasesService {
const base = await Base.getWithInfo(param.baseId);
if (!base) {
NcError.notFound('Base not found');
NcError.baseNotFound(param.baseId);
}
await Base.softDelete(param.baseId);

18
packages/nocodb/src/services/calendar-datas.service.ts

@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common';
import { ErrorMessages, ViewTypes } from 'nocodb-sdk';
import { ViewTypes } from 'nocodb-sdk';
import dayjs from 'dayjs';
import type { CalendarRangeType, FilterType } from 'nocodb-sdk';
import { CalendarRange, Model, View } from '~/models';
@ -29,7 +29,7 @@ export class CalendarDatasService {
const view = await View.get(viewId);
if (!view) NcError.notFound('View not found');
if (!view) NcError.viewNotFound(viewId);
if (view.type !== ViewTypes.CALENDAR)
NcError.badRequest('View is not a calendar view');
@ -70,13 +70,13 @@ export class CalendarDatasService {
const { sharedViewUuid, password, query = {} } = param;
const view = await View.getByUUID(sharedViewUuid);
if (!view) NcError.notFound('Not found');
if (!view) NcError.viewNotFound(sharedViewUuid);
if (view.type !== ViewTypes.CALENDAR) {
NcError.notFound('Not found');
NcError.notFound('View is not a calendar view');
}
if (view.password && view.password !== password) {
return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
return NcError.invalidSharedViewPassword();
}
return this.getCalendarRecordCount({
@ -97,13 +97,13 @@ export class CalendarDatasService {
const { sharedViewUuid, password, query = {} } = param;
const view = await View.getByUUID(sharedViewUuid);
if (!view) NcError.notFound('Not found');
if (!view) NcError.viewNotFound(sharedViewUuid);
if (view.type !== ViewTypes.CALENDAR) {
NcError.notFound('Not found');
NcError.notFound('View is not a calendar view');
}
if (view.password && view.password !== password) {
return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
return NcError.invalidSharedViewPassword();
}
return this.getCalendarDataList({
@ -131,7 +131,7 @@ export class CalendarDatasService {
const view = await View.get(viewId);
if (!view) NcError.notFound('View not found');
if (!view) NcError.viewNotFound(viewId);
if (view.type !== ViewTypes.CALENDAR)
NcError.badRequest('View is not a calendar view');

2
packages/nocodb/src/services/calendars.service.ts

@ -77,7 +77,7 @@ export class CalendarsService {
const view = await View.get(param.calendarViewId);
if (!view) {
NcError.badRequest('View not found');
NcError.viewNotFound(param.calendarViewId);
}
const res = await CalendarView.update(param.calendarViewId, param.calendar);

294
packages/nocodb/src/services/columns.service.ts

@ -31,6 +31,7 @@ import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import ProjectMgrv2 from '~/db/sql-mgr/v2/ProjectMgrv2';
import {
createHmAndBtColumn,
createOOColumn,
generateFkName,
randomID,
sanitizeColumnName,
@ -305,9 +306,7 @@ export class ColumnsService {
await this.updateRollupOrLookup(colBody, column);
} else {
NcError.notImplemented(
`Updating ${colBody.uidt} => ${colBody.uidt} is not implemented`,
);
NcError.notImplemented(`Updating ${colBody.uidt} => ${colBody.uidt}`);
}
} else if (
[
@ -320,9 +319,7 @@ export class ColumnsService {
UITypes.ForeignKey,
].includes(colBody.uidt)
) {
NcError.notImplemented(
`Updating ${colBody.uidt} => ${colBody.uidt} is not implemented`,
);
NcError.notImplemented(`Updating ${colBody.uidt} => ${colBody.uidt}`);
} else if (
[
UITypes.CreatedTime,
@ -1316,9 +1313,7 @@ export class ColumnsService {
...colBody,
});
} else {
NcError.notImplemented(
`Updating ${column.uidt} => ${colBody.uidt} is not supported at the moment`,
);
NcError.notImplemented(`Updating ${column.uidt} => ${colBody.uidt}`);
}
} else if (column.uidt === UITypes.User) {
if ([UITypes.SingleLineText, UITypes.Email].includes(colBody.uidt)) {
@ -1411,9 +1406,7 @@ export class ColumnsService {
...colBody,
});
} else {
NcError.notImplemented(
`Updating ${column.uidt} => ${colBody.uidt} is not supported at the moment`,
);
NcError.notImplemented(`Updating ${column.uidt} => ${colBody.uidt}`);
}
} else {
colBody = await getColumnPropsFromUIDT(colBody, source);
@ -1643,7 +1636,7 @@ export class ColumnsService {
colExtra,
});
this.appHooksService.emit(AppEvents.RELATION_DELETE, {
this.appHooksService.emit(AppEvents.RELATION_CREATE, {
column: {
...colBody,
fk_model_id: param.tableId,
@ -2146,6 +2139,20 @@ export class ColumnsService {
});
}
break;
case 'oo':
{
await this.deleteOoRelation({
relationColOpt,
source,
childColumn,
childTable,
parentColumn,
parentTable,
sqlMgr,
ncMeta,
});
}
break;
case 'mm':
{
const mmTable = await relationColOpt.getMMModel(ncMeta);
@ -2271,7 +2278,7 @@ export class ColumnsService {
});
break;
case UITypes.ForeignKey: {
NcError.notImplemented();
NcError.notImplemented(`Support for ${column.uidt}`);
break;
}
case UITypes.SingleSelect: {
@ -2395,8 +2402,154 @@ export class ColumnsService {
}
if (!relationColOpt?.virtual && !virtual) {
// todo: handle relation delete exception
// Ensure relation deletion is not attempted for virtual relations
try {
// Attempt to delete the foreign key constraint from the database
await sqlMgr.sqlOpPlus(source, 'relationDelete', {
childColumn: childColumn.column_name,
childTable: childTable.table_name,
parentTable: parentTable.table_name,
parentColumn: parentColumn.column_name,
foreignKeyName,
});
} catch (e) {
console.log(e.message);
}
}
}
if (!relationColOpt) return;
const columnsInRelatedTable: Column[] = await relationColOpt
.getRelatedTable(ncMeta)
.then((m) => m.getColumns(ncMeta));
const relType = relationColOpt.type === 'bt' ? 'hm' : 'bt';
for (const c of columnsInRelatedTable) {
if (c.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt = await c.getColOptions<LinkToAnotherRecordColumn>(ncMeta);
if (
colOpt.fk_parent_column_id === parentColumn.id &&
colOpt.fk_child_column_id === childColumn.id &&
colOpt.type === relType
) {
await Column.delete(c.id, ncMeta);
break;
}
}
// delete virtual columns
await Column.delete(relationColOpt.fk_column_id, ncMeta);
if (!ignoreFkDelete) {
const cTable = await Model.getWithInfo(
{
id: childTable.id,
},
ncMeta,
);
// if virtual column delete all index before deleting the column
if (relationColOpt?.virtual) {
const indexes =
(
await sqlMgr.sqlOp(source, 'indexList', {
tn: cTable.table_name,
})
)?.data?.list ?? [];
for (const index of indexes) {
if (index.cn !== childColumn.column_name) continue;
await sqlMgr.sqlOpPlus(source, 'indexDelete', {
...index,
tn: cTable.table_name,
columns: [childColumn.column_name],
indexName: index.key_name,
});
}
}
const tableUpdateBody = {
...cTable,
tn: cTable.table_name,
originalColumns: cTable.columns.map((c) => ({
...c,
cn: c.column_name,
cno: c.column_name,
})),
columns: cTable.columns.map((c) => {
if (c.id === childColumn.id) {
return {
...c,
cn: c.column_name,
cno: c.column_name,
altered: Altered.DELETE_COLUMN,
};
} else {
(c as any).cn = c.column_name;
}
return c;
}),
};
await sqlMgr.sqlOpPlus(source, 'tableUpdate', tableUpdateBody);
}
// delete foreign key column
await Column.delete(childColumn.id, ncMeta);
};
deleteOoRelation = async (
{
relationColOpt,
source,
childColumn,
childTable,
parentColumn,
parentTable,
sqlMgr,
ncMeta = Noco.ncMeta,
virtual,
}: {
relationColOpt: LinkToAnotherRecordColumn;
source: Source;
childColumn: Column;
childTable: Model;
parentColumn: Column;
parentTable: Model;
sqlMgr: SqlMgrv2;
ncMeta?: MetaService;
virtual?: boolean;
},
ignoreFkDelete = false,
) => {
if (childTable) {
let foreignKeyName;
// if relationColOpt is not provided, extract it from child table
// and get the foreign key name for dropping the foreign key
if (!relationColOpt) {
foreignKeyName = (
(
await childTable.getColumns(ncMeta).then(async (cols) => {
for (const col of cols) {
if (col.uidt === UITypes.LinkToAnotherRecord) {
const colOptions =
await col.getColOptions<LinkToAnotherRecordColumn>(ncMeta);
if (colOptions.fk_related_model_id === parentTable.id) {
return { colOptions };
}
}
}
})
)?.colOptions as LinkToAnotherRecordType
).fk_index_name;
} else {
foreignKeyName = relationColOpt.fk_index_name;
}
if (!relationColOpt?.virtual && !virtual) {
// Ensure relation deletion is not attempted for virtual relations
try {
// Attempt to delete the foreign key constraint from the database
await sqlMgr.sqlOpPlus(source, 'relationDelete', {
childColumn: childColumn.column_name,
childTable: childTable.table_name,
@ -2623,6 +2776,102 @@ export class ColumnsService {
isLinks,
param.colExtra,
);
} else if ((param.column as LinkToAnotherColumnReqType).type === 'oo') {
// populate fk column name
const fkColName = getUniqueColumnName(
await child.getColumns(),
`${parent.table_name}_id`,
);
let foreignKeyName;
{
// Create foreign key column for one-to-one relationship
const newColumn = {
cn: fkColName, // Column name in the database
title: fkColName, // Human-readable title for the column
column_name: fkColName, // Column name in the database ( used in sql client )
rqd: false,
pk: false,
ai: false,
cdf: null,
dt: parent.primaryKey.dt,
dtxp: parent.primaryKey.dtxp,
dtxs: parent.primaryKey.dtxs,
un: parent.primaryKey.un,
altered: Altered.NEW_COLUMN,
unique: 1, // Ensure the foreign key column is unique for one-to-one relationships
};
const tableUpdateBody = {
...child,
tn: child.table_name,
originalColumns: child.columns.map((c) => ({
...c,
cn: c.column_name,
})),
columns: [
...child.columns.map((c) => ({
...c,
cn: c.column_name,
})),
newColumn,
],
};
await sqlMgr.sqlOpPlus(param.source, 'tableUpdate', tableUpdateBody);
const { id } = await Column.insert({
...newColumn,
uidt: UITypes.ForeignKey,
fk_model_id: child.id,
});
childColumn = await Column.get({ colId: id });
// ignore relation creation if virtual
if (!(param.column as LinkToAnotherColumnReqType).virtual) {
foreignKeyName = generateFkName(parent, child);
// create relation
await sqlMgr.sqlOpPlus(param.source, 'relationCreate', {
childColumn: fkColName,
childTable: child.table_name,
parentTable: parent.table_name,
onDelete: 'NO ACTION',
onUpdate: 'NO ACTION',
type: 'real',
parentColumn: parent.primaryKey.column_name,
foreignKeyName,
});
}
// todo: create index for virtual relations as well
// create index for foreign key in pg
if (
param.source.type === 'pg' ||
(param.column as LinkToAnotherColumnReqType).virtual
) {
await this.createColumnIndex({
column: new Column({
...newColumn,
fk_model_id: child.id,
}),
source: param.source,
sqlMgr,
});
}
}
await createOOColumn(
child,
parent,
childColumn,
(param.column as LinkToAnotherColumnReqType).type as RelationTypes,
(param.column as LinkToAnotherColumnReqType).title,
foreignKeyName,
(param.column as LinkToAnotherColumnReqType).virtual,
null,
param.column['meta'],
param.colExtra,
);
} else if ((param.column as LinkToAnotherColumnReqType).type === 'mm') {
const aTn = `${param.base?.prefix ?? ''}_nc_m2m_${randomID()}`;
const aTnAlias = aTn;
@ -2843,10 +3092,11 @@ export class ColumnsService {
non_unique: nonUnique,
indexName,
};
sqlMgr.sqlOpPlus(source, 'indexCreate', indexArgs);
await sqlMgr.sqlOpPlus(source, 'indexCreate', indexArgs);
}
async updateRollupOrLookup(colBody: any, column: Column<any>) {
// Validate rollup or lookup payload before proceeding with the update
if (
UITypes.Lookup === column.uidt &&
validateRequiredField(colBody, [
@ -2854,6 +3104,7 @@ export class ColumnsService {
'fk_relation_column_id',
])
) {
// Perform additional validation for lookup payload
await validateLookupPayload(colBody, column.id);
await Column.update(column.id, colBody);
} else if (
@ -2864,6 +3115,7 @@ export class ColumnsService {
'rollup_function',
])
) {
// Perform additional validation for rollup payload
await validateRollupPayload(colBody);
await Column.update(column.id, colBody);
}
@ -2875,7 +3127,7 @@ export class ColumnsService {
});
if (!table) {
NcError.badRequest('Table not found');
NcError.tableNotFound(tableId);
}
const columns = await table.getColumns();
@ -2903,7 +3155,7 @@ export class ColumnsService {
});
if (!table) {
NcError.badRequest('Table not found');
NcError.tableNotFound(tableId);
}
const columns = await table.getColumns();
@ -2917,13 +3169,13 @@ export class ColumnsService {
const source = await Source.get(table.source_id);
if (!source) {
NcError.badRequest('Source not found');
NcError.sourceNotFound(table.source_id);
}
const base = await source.getProject();
if (!base) {
NcError.badRequest('Base not found');
NcError.baseNotFound(source.base_id);
}
const dbDriver = await NcConnectionMgrv2.get(source);
@ -2971,7 +3223,7 @@ export class ColumnsService {
}
const failedOps = [];
// Perform operations in a loop, capturing any errors for individual operations
for (const op of ops) {
const column = op.column;

55
packages/nocodb/src/services/data-alias-nested.service.ts

@ -22,7 +22,7 @@ export class DataAliasNestedService {
) {
const { model, view } = await getViewAndModelByAliasOrId(param);
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(param.tableName);
const source = await Source.get(model.source_id);
@ -69,7 +69,7 @@ export class DataAliasNestedService {
},
) {
const { model, view } = await getViewAndModelByAliasOrId(param);
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(param.tableName);
const source = await Source.get(model.source_id);
@ -111,7 +111,7 @@ export class DataAliasNestedService {
) {
const { model, view } = await getViewAndModelByAliasOrId(param);
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(param.tableName);
const source = await Source.get(model.source_id);
@ -153,7 +153,7 @@ export class DataAliasNestedService {
},
) {
const { model, view } = await getViewAndModelByAliasOrId(param);
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(param.tableName);
const source = await Source.get(model.source_id);
@ -186,6 +186,47 @@ export class DataAliasNestedService {
...param.query,
});
}
async ooExcludedList(
param: PathParams & {
query: any;
columnName: string;
rowId: string;
},
) {
const { model, view } = await getViewAndModelByAliasOrId(param);
if (!model) NcError.notFound('Table not found');
const source = await Source.get(model.source_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(source),
});
const column = await getColumnByIdOrName(param.columnName, model);
const data = await baseModel.getExcludedOneToOneChildrenList(
{
colId: column.id,
cid: param.rowId,
},
param.query,
);
const count = await baseModel.countExcludedOneToOneChildren(
{
colId: column.id,
cid: param.rowId,
},
param.query,
);
return new PagedResponseImpl(data, {
count,
...param.query,
});
}
// todo: handle case where the given column is not ltar
async hmList(
@ -197,7 +238,7 @@ export class DataAliasNestedService {
) {
const { model, view } = await getViewAndModelByAliasOrId(param);
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(param.tableName);
const source = await Source.get(model.source_id);
@ -243,7 +284,7 @@ export class DataAliasNestedService {
},
) {
const { model, view } = await getViewAndModelByAliasOrId(param);
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(param.tableName);
const source = await Source.get(model.source_id);
@ -275,7 +316,7 @@ export class DataAliasNestedService {
},
) {
const { model, view } = await getViewAndModelByAliasOrId(param);
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(param.tableName);
const source = await Source.get(model.source_id);

39
packages/nocodb/src/services/data-table.service.ts

@ -52,7 +52,7 @@ export class DataTableService {
});
if (!row) {
NcError.notFound('Row not found');
NcError.recordNotFound(param.rowId);
}
return row;
@ -184,7 +184,7 @@ export class DataTableService {
const model = await Model.get(param.modelId);
if (!model) {
NcError.notFound(`Table with id '${param.modelId}' not found`);
NcError.tableNotFound(param.modelId);
}
if (param.baseId && model.base_id !== param.baseId) {
@ -196,7 +196,7 @@ export class DataTableService {
if (param.viewId) {
view = await View.get(param.viewId);
if (!view || (view.fk_model_id && view.fk_model_id !== param.modelId)) {
NcError.unprocessableEntity(`View with id '${param.viewId}' not found`);
NcError.viewNotFound(param.viewId);
}
}
@ -278,7 +278,7 @@ export class DataTableService {
});
if (!(await baseModel.exist(param.rowId))) {
NcError.notFound(`Record with id '${param.rowId}' not found`);
NcError.recordNotFound(`${param.rowId}`);
}
const column = await this.getColumn(param);
@ -356,8 +356,7 @@ export class DataTableService {
private async getColumn(param: { modelId: string; columnId: string }) {
const column = await Column.get({ colId: param.columnId });
if (!column)
NcError.notFound(`Column with id '${param.columnId}' not found`);
if (!column) NcError.fieldNotFound(param.columnId);
if (column.fk_model_id !== param.modelId)
NcError.badRequest('Column not belong to model');
@ -419,8 +418,7 @@ export class DataTableService {
this.validateIds(param.refRowIds);
const { model, view } = await this.getModelAndView(param);
if (!model)
NcError.notFound('Table with id ' + param.modelId + ' not found');
if (!model) NcError.tableNotFound(param.modelId);
const source = await Source.get(model.source_id);
@ -503,9 +501,7 @@ export class DataTableService {
operationMap.deleteAll &&
!(await baseModel.exist(operationMap.deleteAll.rowId))
) {
NcError.notFound(
`Record with id '${operationMap.deleteAll.rowId}' not found`,
);
NcError.recordNotFound(operationMap.deleteAll.rowId);
} else if (operationMap.copy && operationMap.paste) {
const [existsCopyRow, existsPasteRow] = await Promise.all([
baseModel.exist(operationMap.copy.rowId),
@ -513,17 +509,13 @@ export class DataTableService {
]);
if (!existsCopyRow && !existsPasteRow) {
NcError.notFound(
`Record with id '${operationMap.copy.rowId}' and '${operationMap.paste.rowId}' not found`,
NcError.recordNotFound(
`'${operationMap.copy.rowId}' and '${operationMap.paste.rowId}'`,
);
} else if (!existsCopyRow) {
NcError.notFound(
`Record with id '${operationMap.copy.rowId}' not found`,
);
NcError.recordNotFound(operationMap.copy.rowId);
} else if (!existsPasteRow) {
NcError.notFound(
`Record with id '${operationMap.paste.rowId}' not found`,
);
NcError.recordNotFound(operationMap.paste.rowId);
}
}
@ -640,7 +632,7 @@ export class DataTableService {
const set = new Set<string>();
for (const rowId of rowIds) {
if (rowId === undefined || rowId === null)
NcError.unprocessableEntity('Invalid row id ' + rowId);
NcError.recordNotFound(rowId);
if (map.has(rowId)) {
set.add(rowId);
} else {
@ -648,12 +640,9 @@ export class DataTableService {
}
}
if (set.size > 0)
NcError.unprocessableEntity(
'Child record with id [' + [...set].join(', ') + '] are duplicated',
);
if (set.size > 0) NcError.duplicateRecord([...set]);
} else if (rowIds === undefined || rowIds === null) {
NcError.unprocessableEntity('Invalid row id ' + rowIds);
NcError.recordNotFound(rowIds);
}
}

31
packages/nocodb/src/services/datas.service.ts

@ -295,7 +295,7 @@ export class DatasService {
});
if (!row) {
NcError.notFound('Row not found');
NcError.recordNotFound(param.rowId);
}
return row;
@ -390,7 +390,7 @@ export class DatasService {
id: view?.fk_model_id || param.viewId,
});
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(view?.fk_model_id || param.viewId);
return await this.getDataList({ model, view, query: param.query });
}
@ -407,7 +407,7 @@ export class DatasService {
id: view?.fk_model_id || param.viewId,
});
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(view?.fk_model_id || param.viewId);
const source = await Source.get(model.source_id);
@ -468,7 +468,7 @@ export class DatasService {
id: view?.fk_model_id || param.viewId,
});
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(view?.fk_model_id || param.viewId);
const source = await Source.get(model.source_id);
@ -529,7 +529,7 @@ export class DatasService {
id: view?.fk_model_id || param.viewId,
});
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(view?.fk_model_id || param.viewId);
const source = await Source.get(model.source_id);
@ -590,7 +590,7 @@ export class DatasService {
id: view?.fk_model_id || param.viewId,
});
if (!model) return NcError.notFound('Table not found');
if (!model) return NcError.tableNotFound(view?.fk_model_id || param.viewId);
const source = await Source.get(model.source_id);
@ -651,7 +651,7 @@ export class DatasService {
id: view?.fk_model_id || param.viewId,
});
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(view?.fk_model_id || param.viewId);
const source = await Source.get(model.source_id);
@ -703,7 +703,7 @@ export class DatasService {
const model = await Model.getByIdOrName({
id: param.viewId,
});
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(param.viewId);
const source = await Source.get(model.source_id);
@ -733,7 +733,7 @@ export class DatasService {
const model = await Model.getByIdOrName({
id: param.viewId,
});
if (!model) return NcError.notFound('Table not found');
if (!model) return NcError.tableNotFound(param.viewId);
const source = await Source.get(model.source_id);
@ -754,7 +754,7 @@ export class DatasService {
const model = await Model.getByIdOrName({
id: param.viewId,
});
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(param.viewId);
const source = await Source.get(model.source_id);
@ -779,7 +779,7 @@ export class DatasService {
const model = await Model.getByIdOrName({
id: param.viewId,
});
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(param.viewId);
const source = await Source.get(model.source_id);
@ -804,7 +804,7 @@ export class DatasService {
id: view?.fk_model_id || param.viewId,
});
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(view?.fk_model_id || param.viewId);
const source = await Source.get(model.source_id);
@ -837,7 +837,7 @@ export class DatasService {
id: view?.fk_model_id || param.viewId,
});
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(view?.fk_model_id || param.viewId);
const source = await Source.get(model.source_id);
@ -875,7 +875,7 @@ export class DatasService {
titleOrId: req.params.viewName,
fk_model_id: model.id,
}));
if (!model) NcError.notFound('Table not found');
if (!model) NcError.tableNotFound(req.params.tableName);
return { model, view };
}
@ -982,8 +982,7 @@ export class DatasService {
c.column_name === columnNameOrId,
);
if (!column)
NcError.notFound(`Column with id/name '${columnNameOrId}' is not found`);
if (!column) NcError.fieldNotFound(columnNameOrId);
return column;
}

2
packages/nocodb/src/services/forms.service.ts

@ -77,7 +77,7 @@ export class FormsService {
const view = await View.get(param.formViewId);
if (!view) {
NcError.badRequest('View not found');
NcError.viewNotFound(param.formViewId);
}
const res = await FormView.update(param.formViewId, param.form);

2
packages/nocodb/src/services/galleries.service.ts

@ -76,7 +76,7 @@ export class GalleriesService {
const view = await View.get(param.galleryViewId);
if (!view) {
NcError.badRequest('View not found');
NcError.viewNotFound(param.galleryViewId);
}
const res = await GalleryView.update(param.galleryViewId, param.gallery);

2
packages/nocodb/src/services/grids.service.ts

@ -67,7 +67,7 @@ export class GridsService {
const view = await View.get(param.viewId);
if (!view) {
NcError.badRequest('View not found');
NcError.viewNotFound(param.viewId);
}
const res = await GridView.update(param.viewId, param.grid);

2
packages/nocodb/src/services/kanbans.service.ts

@ -78,7 +78,7 @@ export class KanbansService {
const view = await View.get(param.kanbanViewId);
if (!view) {
NcError.badRequest('View not found');
NcError.viewNotFound(param.kanbanViewId);
}
const res = await KanbanView.update(param.kanbanViewId, param.kanban);

2
packages/nocodb/src/services/maps.service.ts

@ -70,7 +70,7 @@ export class MapsService {
const view = await View.get(param.mapViewId);
if (!view) {
NcError.badRequest('View not found');
NcError.viewNotFound(param.mapViewId);
}
const res = await MapView.update(param.mapViewId, param.map);

2
packages/nocodb/src/services/model-visibilities.service.ts

@ -24,7 +24,7 @@ export class ModelVisibilitiesService {
const base = await Base.getWithInfo(param.baseId);
if (!base) {
NcError.badRequest('Base not found');
NcError.baseNotFound(param.baseId);
}
for (const d of param.visibilityRule) {

4
packages/nocodb/src/services/org-users.service.ts

@ -201,7 +201,7 @@ export class OrgUsersService {
const user = await User.get(param.userId);
if (!user) {
NcError.badRequest(`User with id '${param.userId}' not found`);
NcError.userNotFound(param.userId);
}
const invite_token = uuidv4();
@ -247,7 +247,7 @@ export class OrgUsersService {
const user = await User.get(param.userId);
if (!user) {
NcError.badRequest(`User with id '${param.userId}' not found`);
NcError.userNotFound(param.userId);
}
const token = uuidv4();
await User.update(user.id, {

35
packages/nocodb/src/services/public-datas.service.ts

@ -1,12 +1,7 @@
import path from 'path';
import { Injectable } from '@nestjs/common';
import { nanoid } from 'nanoid';
import {
ErrorMessages,
populateUniqueFileName,
UITypes,
ViewTypes,
} from 'nocodb-sdk';
import { populateUniqueFileName, UITypes, ViewTypes } from 'nocodb-sdk';
import slash from 'slash';
import { nocoExecute } from 'nc-help';
@ -36,7 +31,7 @@ export class PublicDatasService {
const { sharedViewUuid, password, query = {} } = param;
const view = await View.getByUUID(sharedViewUuid);
if (!view) NcError.notFound('Not found');
if (!view) NcError.viewNotFound(sharedViewUuid);
if (
view.type !== ViewTypes.GRID &&
view.type !== ViewTypes.KANBAN &&
@ -48,7 +43,7 @@ export class PublicDatasService {
}
if (view.password && view.password !== password) {
return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
return NcError.invalidSharedViewPassword();
}
const model = await Model.getByIdOrName({
@ -104,7 +99,7 @@ export class PublicDatasService {
}) {
const view = await View.getByUUID(param.sharedViewUuid);
if (!view) NcError.notFound('Not found');
if (!view) NcError.viewNotFound(param.sharedViewUuid);
if (
view.type !== ViewTypes.GRID &&
@ -115,7 +110,7 @@ export class PublicDatasService {
}
if (view.password && view.password !== param.password) {
return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
return NcError.invalidSharedViewPassword();
}
const model = await Model.getByIdOrName({
@ -201,14 +196,14 @@ export class PublicDatasService {
}) {
const view = await View.getByUUID(param.sharedViewUuid);
if (!view) NcError.notFound('Not found');
if (!view) NcError.viewNotFound(param.sharedViewUuid);
if (view.type !== ViewTypes.GRID) {
NcError.notFound('Not found');
}
if (view.password && view.password !== param.password) {
return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
return NcError.invalidSharedViewPassword();
}
const model = await Model.getByIdOrName({
@ -261,11 +256,11 @@ export class PublicDatasService {
}) {
const view = await View.getByUUID(param.sharedViewUuid);
if (!view) NcError.notFound();
if (!view) NcError.viewNotFound(param.sharedViewUuid);
if (view.type !== ViewTypes.FORM) NcError.notFound();
if (view.password && view.password !== param.password) {
return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
return NcError.invalidSharedViewPassword();
}
const model = await Model.getByIdOrName({
@ -434,14 +429,14 @@ export class PublicDatasService {
}) {
const view = await View.getByUUID(param.sharedViewUuid);
if (!view) NcError.notFound('Not found');
if (!view) NcError.viewNotFound(param.sharedViewUuid);
if (view.type !== ViewTypes.FORM && view.type !== ViewTypes.GALLERY) {
NcError.notFound('Not found');
}
if (view.password && view.password !== param.password) {
NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
NcError.invalidSharedViewPassword();
}
const column = await Column.get({ colId: param.columnId });
@ -492,7 +487,7 @@ export class PublicDatasService {
}) {
const view = await View.getByUUID(param.sharedViewUuid);
if (!view) NcError.notFound('Not found');
if (!view) NcError.viewNotFound(param.sharedViewUuid);
if (
view.type !== ViewTypes.GRID &&
view.type !== ViewTypes.KANBAN &&
@ -503,7 +498,7 @@ export class PublicDatasService {
}
if (view.password && view.password !== param.password) {
NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
NcError.invalidSharedViewPassword();
}
const column = await getColumnByIdOrName(
@ -567,7 +562,7 @@ export class PublicDatasService {
}) {
const view = await View.getByUUID(param.sharedViewUuid);
if (!view) NcError.notFound('Not found');
if (!view) NcError.viewNotFound(param.sharedViewUuid);
if (
view.type !== ViewTypes.GRID &&
view.type !== ViewTypes.KANBAN &&
@ -578,7 +573,7 @@ export class PublicDatasService {
}
if (view.password && view.password !== param.password) {
NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
NcError.invalidSharedViewPassword();
}
const column = await getColumnByIdOrName(

7
packages/nocodb/src/services/public-metas.service.ts

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common';
import {
ErrorMessages,
isCreatedOrLastModifiedByCol,
RelationTypes,
UITypes,
@ -19,10 +18,10 @@ export class PublicMetasService {
client?: string;
} = await View.getByUUID(param.sharedViewUuid);
if (!view) NcError.notFound('Not found');
if (!view) NcError.viewNotFound(param.sharedViewUuid);
if (view.password && view.password !== param.password) {
NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
NcError.invalidSharedViewPassword();
}
await view.getFilters();
@ -171,7 +170,7 @@ export class PublicMetasService {
const base = await Base.getByUuid(param.sharedBaseUuid);
if (!base) {
NcError.notFound();
NcError.baseNotFound(param.sharedBaseUuid);
}
return { base_id: base.id };

8
packages/nocodb/src/services/shared-bases.service.ts

@ -42,7 +42,7 @@ export class SharedBasesService {
}
if (!base) {
NcError.badRequest('Invalid base id');
NcError.baseNotFound(param.baseId);
}
const data: any = {
@ -86,7 +86,7 @@ export class SharedBasesService {
}
if (!base) {
NcError.badRequest('Invalid base id');
NcError.baseNotFound(param.baseId);
}
if (roles === 'editor' && process.env.NC_CLOUD === 'true') {
@ -137,7 +137,7 @@ export class SharedBasesService {
const base = await Base.get(param.baseId);
if (!base) {
NcError.badRequest('Invalid base id');
NcError.baseNotFound(param.baseId);
}
const data: any = {
uuid: null,
@ -159,7 +159,7 @@ export class SharedBasesService {
const base = await Base.get(param.baseId);
if (!base) {
NcError.badRequest('Invalid base id');
NcError.baseNotFound(param.baseId);
}
const data: any = {
uuid: base.uuid,

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

Loading…
Cancel
Save