Browse Source

Merge branch 'develop' into master

pull/7932/head
Pranav C 8 months ago committed by GitHub
parent
commit
070d5cbeee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .github/workflows/playwright-test-workflow.yml
  2. 6
      packages/nc-gui/assets/nc-icons/onetoone.svg
  3. 19
      packages/nc-gui/components/cell/Checkbox.vue
  4. 11
      packages/nc-gui/components/cell/Currency.vue
  5. 21
      packages/nc-gui/components/cell/Email.vue
  6. 15
      packages/nc-gui/components/cell/Integer.vue
  7. 5
      packages/nc-gui/components/cell/MultiSelect.vue
  8. 38
      packages/nc-gui/components/cell/Percent.vue
  9. 22
      packages/nc-gui/components/cell/SingleSelect.vue
  10. 21
      packages/nc-gui/components/cell/Url.vue
  11. 17
      packages/nc-gui/components/cell/attachment/index.vue
  12. 4
      packages/nc-gui/components/dashboard/Sidebar/TopSection.vue
  13. 4
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  14. 4
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  15. 4
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  16. 10
      packages/nc-gui/components/general/FormBanner.vue
  17. 2
      packages/nc-gui/components/general/ImageCropper.vue
  18. 4
      packages/nc-gui/components/project/View.vue
  19. 2
      packages/nc-gui/components/smartsheet/Cell.vue
  20. 75
      packages/nc-gui/components/smartsheet/Form.vue
  21. 2
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  22. 13
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  23. 4
      packages/nc-gui/components/smartsheet/column/RollupOptions.vue
  24. 9
      packages/nc-gui/components/smartsheet/grid/Table.vue
  25. 6
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  26. 2
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts
  27. 7
      packages/nc-gui/components/smartsheet/toolbar/CreateGroupBy.vue
  28. 7
      packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue
  29. 5
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  30. 18
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  31. 12
      packages/nc-gui/components/virtual-cell/Lookup.vue
  32. 150
      packages/nc-gui/components/virtual-cell/OneToOne.vue
  33. 11
      packages/nc-gui/components/virtual-cell/components/UnLinkedItems.vue
  34. 43
      packages/nc-gui/components/webhook/Editor.vue
  35. 32
      packages/nc-gui/composables/useLTARStore.ts
  36. 4
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  37. 7
      packages/nc-gui/composables/useMultiSelect/index.ts
  38. 44
      packages/nc-gui/composables/useSharedFormViewStore.ts
  39. 11
      packages/nc-gui/composables/useSmartsheetRowStore.ts
  40. 1
      packages/nc-gui/lang/ar.json
  41. 1
      packages/nc-gui/lang/bn_IN.json
  42. 1
      packages/nc-gui/lang/cs.json
  43. 1
      packages/nc-gui/lang/da.json
  44. 1
      packages/nc-gui/lang/de.json
  45. 3
      packages/nc-gui/lang/en.json
  46. 1
      packages/nc-gui/lang/es.json
  47. 1
      packages/nc-gui/lang/eu.json
  48. 1
      packages/nc-gui/lang/fa.json
  49. 1
      packages/nc-gui/lang/fi.json
  50. 1
      packages/nc-gui/lang/fr.json
  51. 3
      packages/nc-gui/lang/he.json
  52. 1
      packages/nc-gui/lang/hi.json
  53. 1
      packages/nc-gui/lang/hr.json
  54. 1
      packages/nc-gui/lang/id.json
  55. 1
      packages/nc-gui/lang/it.json
  56. 1
      packages/nc-gui/lang/ja.json
  57. 1
      packages/nc-gui/lang/ko.json
  58. 1
      packages/nc-gui/lang/lv.json
  59. 1
      packages/nc-gui/lang/nl.json
  60. 1
      packages/nc-gui/lang/no.json
  61. 15
      packages/nc-gui/lang/pl.json
  62. 1
      packages/nc-gui/lang/pt.json
  63. 1
      packages/nc-gui/lang/pt_BR.json
  64. 1
      packages/nc-gui/lang/ru.json
  65. 1
      packages/nc-gui/lang/sk.json
  66. 1
      packages/nc-gui/lang/sl.json
  67. 1
      packages/nc-gui/lang/sv.json
  68. 1
      packages/nc-gui/lang/th.json
  69. 1
      packages/nc-gui/lang/tr.json
  70. 69
      packages/nc-gui/lang/uk.json
  71. 1
      packages/nc-gui/lang/vi.json
  72. 1
      packages/nc-gui/lang/zh-Hans.json
  73. 1
      packages/nc-gui/lang/zh-Hant.json
  74. 2
      packages/nc-gui/package.json
  75. 19
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue
  76. 10
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue
  77. 155
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue
  78. 4
      packages/nc-gui/store/views.ts
  79. 2
      packages/nc-gui/utils/dataUtils.ts
  80. 2
      packages/nc-gui/utils/iconUtils.ts
  81. 3
      packages/nc-gui/utils/virtualCell.ts
  82. 12
      packages/nocodb-sdk/src/lib/Api.ts
  83. 27
      packages/nocodb-sdk/src/lib/globals.ts
  84. 10
      packages/nocodb/package.json
  85. 20
      packages/nocodb/src/controllers/data-alias-nested.controller.ts
  86. 2
      packages/nocodb/src/controllers/old-datas/old-datas.service.ts
  87. 10
      packages/nocodb/src/controllers/public-datas-export.controller.ts
  88. 522
      packages/nocodb/src/db/BaseModelSqlv2.ts
  89. 33
      packages/nocodb/src/db/conditionV2.ts
  90. 95
      packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts
  91. 21
      packages/nocodb/src/db/genRollupSelectv2.ts
  92. 39
      packages/nocodb/src/db/generateLookupSelectQuery.ts
  93. 2
      packages/nocodb/src/db/sortV2.ts
  94. 1
      packages/nocodb/src/db/sql-client/lib/mysql/MysqlClient.ts
  95. 2
      packages/nocodb/src/db/sql-client/lib/pg/PgClient.ts
  96. 5
      packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts
  97. 20
      packages/nocodb/src/filters/global-exception/global-exception.filter.ts
  98. 6
      packages/nocodb/src/helpers/NcPluginMgrv2.ts
  99. 2
      packages/nocodb/src/helpers/PagedResponse.ts
  100. 298
      packages/nocodb/src/helpers/catchError.ts
  101. Some files were not shown because too many files have changed in this diff Show More

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

@ -131,7 +131,7 @@ jobs:
sleep 2 sleep 2
done done
echo "Backend is up" echo "Backend is up"
timeout-minutes: 2 timeout-minutes: 3
- name: Run Playwright Tests - name: Run Playwright Tests
working-directory: ./tests/playwright working-directory: ./tests/playwright
run: E2E_DB_TYPE=${{ inputs.db }} npm run ci:test:shard:${{ inputs.shard }} run: E2E_DB_TYPE=${{ inputs.db }} npm run ci:test:shard:${{ inputs.shard }}

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"> <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="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="#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="purple" 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="M5 8L11 8" stroke="purple" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </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), 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 ( if (
(event?.target as HTMLElement)?.classList?.contains('nc-checkbox') || (event?.target as HTMLElement)?.classList?.contains('nc-checkbox') ||
(event?.target as HTMLElement)?.closest('.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) => { useSelectedCellKeyupListener(active, (e) => {
switch (e.key) { switch (e.key) {
case 'Enter': case 'Enter':
@ -101,8 +114,8 @@ useSelectedCellKeyupListener(active, (e) => {
}" }"
:tabindex="readOnly ? -1 : 0" :tabindex="readOnly ? -1 : 0"
@click="onClick(false, $event)" @click="onClick(false, $event)"
@keydown.enter.stop="!isSurveyForm ? onClick(true, $event) : undefined" @keydown.enter="keydownEnter"
@keydown.space.stop="isSurveyForm ? onClick(true, $event) : undefined" @keydown.space="keydownSpace($event)"
> >
<div <div
class="flex items-center" class="flex items-center"

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

@ -103,12 +103,21 @@ onMounted(() => {
</script> </script>
<template> <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 <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
type="number" 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') : ''" :placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="onBlur" @blur="onBlur"
@keydown.enter="onKeydownEnter" @keydown.enter="onKeydownEnter"

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

@ -5,7 +5,6 @@ import {
EditModeInj, EditModeInj,
IsExpandedFormOpenInj, IsExpandedFormOpenInj,
IsFormInj, IsFormInj,
IsSurveyFormInj,
ReadonlyInj, ReadonlyInj,
computed, computed,
inject, inject,
@ -31,12 +30,14 @@ const editEnabled = inject(EditModeInj)!
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false)) const isEditColumn = inject(EditColumnInj, ref(false))
const readOnly = inject(ReadonlyInj, 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 // 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) const localState = ref(value)
@ -44,7 +45,7 @@ const vModel = computed({
get: () => value, get: () => value,
set: (val) => { set: (val) => {
localState.value = 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) emit('update:modelValue', val)
} }
}, },
@ -52,17 +53,19 @@ const vModel = computed({
const validEmail = computed(() => vModel.value && validateEmail(vModel.value)) const validEmail = computed(() => vModel.value && validateEmail(vModel.value))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) => const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus() !isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
watch( watch(
() => editEnabled.value, () => 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')) message.error(t('msg.error.invalidEmail'))
localState.value = undefined localState.value = undefined
return 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 readOnly = inject(ReadonlyInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
const _vModel = useVModel(props, 'modelValue', emits) const _vModel = useVModel(props, 'modelValue', emits)
const displayValue = computed(() => { const displayValue = computed(() => {
@ -42,15 +46,15 @@ const vModel = computed({
// if we clear / empty a cell in sqlite, // if we clear / empty a cell in sqlite,
// the value is considered as '' // the value is considered as ''
_vModel.value = null _vModel.value = null
} else if (isForm.value && !isEditColumn.value) {
_vModel.value = isNaN(Number(value)) ? value : Number(value)
} else { } else {
_vModel.value = value _vModel.value = value
} }
}, },
}) })
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const inputType = computed(() => (isForm.value && !isEditColumn.value ? 'text' : 'number'))
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) => const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus() !isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
@ -91,7 +95,7 @@ function onKeyDown(e: any) {
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
class="nc-cell-field outline-none py-1 border-none w-full h-full text-sm" 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" style="letter-spacing: 0.06rem"
:placeholder="isEditColumn ? $t('labels.optional') : ''" :placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false" @blur="editEnabled = false"
@ -109,7 +113,8 @@ function onKeyDown(e: any) {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
input[type='number']:focus { input[type='number']:focus,
input[type='text']:focus {
@apply ring-transparent; @apply ring-transparent;
} }

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

@ -371,6 +371,9 @@ const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Tab') { if (e.key === 'Tab') {
isOpen.value = false isOpen.value = false
return return
} else if (e.key === 'Escape' && isForm.value) {
isOpen.value = false
return
} }
e.stopPropagation() e.stopPropagation()
@ -394,7 +397,7 @@ const onFocus = () => {
@click="toggleMenu" @click="toggleMenu"
> >
<div v-if="!isEditColumn && isForm && parseProp(column.meta)?.isList" class="w-full max-w-full"> <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 <a-checkbox
v-for="op of options" v-for="op of options"
:key="op.title" :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 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 isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)! const isForm = inject(IsFormInj)!
@ -46,6 +35,23 @@ const cellFocused = ref(false)
const expandedEditEnabled = 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(() => { const percentMeta = computed(() => {
return { return {
is_progress: false, is_progress: false,
@ -53,6 +59,8 @@ const percentMeta = computed(() => {
} }
}) })
const inputType = computed(() => (isForm.value && !isEditColumn.value ? 'text' : 'number'))
const onBlur = () => { const onBlur = () => {
if (editEnabled) { if (editEnabled) {
editEnabled.value = false editEnabled.value = false
@ -106,7 +114,11 @@ const onTabPress = (e: KeyboardEvent) => {
) )
for (let i = focusesNcCellIndex - 1; i >= 0; i--) { 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) { if (lastFormItem) {
lastFormItem.focus() lastFormItem.focus()
break break
@ -132,7 +144,7 @@ const onTabPress = (e: KeyboardEvent) => {
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
class="nc-cell-field w-full !text-sm !border-none !outline-none focus:ring-0 text-base py-1" 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') : ''" :placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="onBlur" @blur="onBlur"
@focus="onFocus" @focus="onFocus"

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

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

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

@ -7,7 +7,6 @@ import {
EditModeInj, EditModeInj,
IsExpandedFormOpenInj, IsExpandedFormOpenInj,
IsFormInj, IsFormInj,
IsSurveyFormInj,
ReadonlyInj, ReadonlyInj,
computed, computed,
inject, inject,
@ -42,10 +41,12 @@ const disableOverlay = inject(CellUrlDisableOverlayInj, ref(false))
const rowHeight = inject(RowHeightInj, ref(undefined)) const rowHeight = inject(RowHeightInj, ref(undefined))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const readOnly = inject(ReadonlyInj, 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 // 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) const localState = ref(value)
@ -53,7 +54,7 @@ const vModel = computed({
get: () => value, get: () => value,
set: (val) => { set: (val) => {
localState.value = 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) emit('update:modelValue', val)
} }
}, },
@ -72,17 +73,19 @@ const url = computed(() => {
const { cellUrlOptions } = useCellUrlConfig(url) const { cellUrlOptions } = useCellUrlConfig(url)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) => const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus() !isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
watch( watch(
() => editEnabled.value, () => 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')) message.error(t('msg.error.invalidURL'))
localState.value = undefined localState.value = undefined
return return

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

@ -178,6 +178,19 @@ const onImageClick = (item: any) => {
selectedImage.value = item 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> </script>
<template> <template>
@ -211,8 +224,8 @@ const onImageClick = (item: any) => {
data-testid="attachment-cell-file-picker-button" data-testid="attachment-cell-file-picker-button"
tabindex="0" tabindex="0"
@click="open" @click="open"
@keydown.enter="!isSurveyForm ? open($event) : undefined" @keydown.enter="keydownEnter"
@keydown.space="isSurveyForm ? open($event) : undefined" @keydown.space="keydownSpace"
> >
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" /> <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 { appInfo } = useGlobal()
const { meta: metaKey, ctrlKey } = useMagicKeys() const { meta: metaKey, control } = useMagicKeys()
const { isWorkspaceLoading, isWorkspaceSettingsPageOpened } = storeToRefs(workspaceStore) const { isWorkspaceLoading, isWorkspaceSettingsPageOpened } = storeToRefs(workspaceStore)
@ -17,7 +17,7 @@ const { isSharedBase } = storeToRefs(baseStore)
const isCreateProjectOpen = ref(false) const isCreateProjectOpen = ref(false)
const navigateToSettings = () => { const navigateToSettings = () => {
const cmdOrCtrl = isMac() ? metaKey.value : ctrlKey.value const cmdOrCtrl = isMac() ? metaKey.value : control.value
// TODO: Handle cloud case properly // TODO: Handle cloud case properly
navigateToWorkspaceSettings('', cmdOrCtrl) navigateToWorkspaceSettings('', cmdOrCtrl)

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

@ -73,7 +73,7 @@ const { orgRoles, isUIAllowed } = useRoles()
useTabs() useTabs()
const { meta: metaKey, ctrlKey } = useMagicKeys() const { meta: metaKey, control } = useMagicKeys()
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
@ -261,7 +261,7 @@ const onProjectClick = async (base: NcProject, ignoreNavigation?: boolean, toggl
if (!base) { if (!base) {
return return
} }
const cmdOrCtrl = isMac() ? metaKey.value : ctrlKey.value const cmdOrCtrl = isMac() ? metaKey.value : control.value
if (!toggleIsExpanded && !cmdOrCtrl) $e('c:base:open') 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!, baseId: base.value.id!,
}) })
const { meta: metaKey, ctrlKey } = useMagicKeys() const { meta: metaKey, control } = useMagicKeys()
const baseRole = inject(ProjectRoleInj) const baseRole = inject(ProjectRoleInj)
provide(SidebarTableInj, table) provide(SidebarTableInj, table)
@ -108,7 +108,7 @@ const onExpand = async () => {
} }
const onOpenTable = async () => { const onOpenTable = async () => {
if (isMac() ? metaKey.value : ctrlKey.value) { if (isMac() ? metaKey.value : control.value) {
await _openTable(table.value, true) await _openTable(table.value, true)
return return
} }

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

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

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

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

2
packages/nc-gui/components/general/ImageCropper.vue

@ -53,6 +53,7 @@ const handleUploadImage = async (fileToUpload: AttachmentReqType[]) => {
if (uploadResult?.[0]) { if (uploadResult?.[0]) {
emit('submit', { emit('submit', {
...uploadResult[0], ...uploadResult[0],
data: fileToUpload[0].data,
}) })
} else { } else {
emit('submit', fileToUpload[0]) emit('submit', fileToUpload[0])
@ -78,6 +79,7 @@ const handleSaveImage = async () => {
mimetype: imageConfig.type, mimetype: imageConfig.type,
size: blob.size, size: blob.size,
url: previewImage.value.src, url: previewImage.value.src,
data: previewImage.value.src,
}, },
]) ])
}, imageConfig.type) }, imageConfig.type)

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

@ -20,7 +20,7 @@ const { $e } = useNuxtApp()
return openedProject.value?.sources?.[0] return openedProject.value?.sources?.[0]
}) */ }) */
const { isUIAllowed } = useRoles() const { isUIAllowed, baseRoles } = useRoles()
const { base } = storeToRefs(useBase()) 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"> <!-- <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" /> <ErdView :source-id="defaultBase!.id" class="!h-full" />
</a-tab-pane> --> </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> <template #tab>
<div class="tab-title" data-testid="proj-view-tab__access-settings"> <div class="tab-title" data-testid="proj-view-tab__access-settings">
<GeneralIcon icon="users" class="!h-3.5 !w-3.5" /> <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, 'text-brand-500': isPrimary(column) && !props.virtual && !isForm && !isCalendar,
'nc-grid-numeric-cell-right': isGrid && isNumericField && !isEditColumnMenu && !isForm && !isExpandedFormOpen, '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, 'nc-grid-numeric-cell-left': (isForm && isNumericField && isExpandedFormOpen) || isEditColumnMenu,
'!min-h-30': isTextArea(column) && (isForm || isSurveyForm), '!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 tinycolor from 'tinycolor2'
import { Pane, Splitpanes } from 'splitpanes' import { Pane, Splitpanes } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css' import 'splitpanes/dist/splitpanes.css'
import type { FormItemProps } from 'ant-design-vue'
import { import {
type AttachmentResType, type AttachmentResType,
ProjectRoles, 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 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( const updateView = useDebounceFn(
() => { () => {
updateFormView(formViewData.value) updateFormView(formViewData.value)
@ -546,6 +550,50 @@ const handleOnUploadImage = (data: AttachmentResType = null) => {
updateView() 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) => { onClickOutside(draggableRef, (e) => {
if ( if (
(e.target as HTMLElement)?.closest( (e.target as HTMLElement)?.closest(
@ -736,7 +784,7 @@ useEventListener(
<div class="flex justify-center"> <div class="flex justify-center">
<div class="w-full"> <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> <template #message>
<LazyCellRichText <LazyCellRichText
v-if="formViewData?.success_msg?.trim()" v-if="formViewData?.success_msg?.trim()"
@ -806,7 +854,7 @@ useEventListener(
></GeneralImageCropper> ></GeneralImageCropper>
<!-- cover image --> <!-- cover image -->
<div v-if="!parseProp(formViewData?.meta).hide_banner" class="group relative max-w-[max(33%,688px)] mx-auto"> <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="absolute bottom-0 right-0 hidden group-hover:block">
<div class="flex items-center space-x-1 m-2"> <div class="flex items-center space-x-1 m-2">
<NcTooltip :disabled="isEeUI"> <NcTooltip :disabled="isEeUI">
@ -880,7 +928,8 @@ useEventListener(
> >
<LazyCellAttachmentImage <LazyCellAttachmentImage
v-if="formViewData.logo_url" 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" class="flex-none nc-form-logo !object-contain object-left max-h-full max-w-full !m-0"
/> />
<div <div
@ -973,8 +1022,7 @@ useEventListener(
:bordered="false" :bordered="false"
:data-testid="NcForm.heading" :data-testid="NcForm.heading"
:data-title="NcForm.heading" :data-title="NcForm.heading"
@blur="updateView" @update:value="updateView"
@keydown.enter="updateView"
/> />
</a-form-item> </a-form-item>
@ -1199,12 +1247,7 @@ useEventListener(
<a-form-item <a-form-item
:name="element.title" :name="element.title"
class="!my-0 nc-input-required-error nc-form-input-item" class="!my-0 nc-input-required-error nc-form-input-item"
:rules="[ :rules="formElementValidationRules(element)"
{
required: isRequired(element, element.required),
message: `${$t('msg.error.fieldRequired', { value: 'This field' })}`,
},
]"
> >
<LazySmartsheetDivDataCell <LazySmartsheetDivDataCell
class="relative" class="relative"
@ -1725,6 +1768,9 @@ useEventListener(
&.nc-cell-geodata { &.nc-cell-geodata {
@apply !py-1; @apply !py-1;
} }
&.nc-cell-currency {
@apply !py-0 !pl-0 flex items-stretch;
}
:deep(input) { :deep(input) {
@apply !px-1; @apply !px-1;
@ -1732,8 +1778,11 @@ useEventListener(
&.nc-cell-longtext { &.nc-cell-longtext {
@apply p-0 h-auto; @apply p-0 h-auto;
} }
&:not(.nc-cell-longtext) { &.nc-cell:not(.nc-cell-longtext) {
@apply px-2 py-2; @apply p-2;
}
&.nc-virtual-cell {
@apply px-2 py-1;
} }
&.nc-cell-json { &.nc-cell-json {

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

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

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

@ -1,5 +1,5 @@
<script setup lang="ts"> <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 { MetaInj, inject, ref, storeToRefs, useBase, useVModel } from '#imports'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline' import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-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 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> </script>
<template> <template>
<div class="w-full flex flex-col mb-2 mt-4"> <div class="w-full flex flex-col mb-2 mt-4">
<div class="border-2 p-6"> <div class="border-2 p-6">
<a-form-item v-bind="validateInfos.type" class="nc-ltar-relation-type"> <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-group v-model:value="vModel.type" name="type" v-bind="validateInfos.type" class="!flex flex-col gap-2">
<a-radio value="hm">{{ $t('title.hasMany') }}</a-radio> <a-radio value="hm" @dblclick="oneToOneEnabled = !oneToOneEnabled">{{ $t('title.hasMany') }}</a-radio>
<a-radio value="mm">{{ $t('title.manyToMany') }}</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-radio-group>
</a-form-item> </a-form-item>
@ -102,7 +105,7 @@ const isLinks = computed(() => vModel.value.uidt === UITypes.Links)
</div> </div>
<div v-if="advancedOptions" class="flex flex-col p-6 gap-4 border-2 mt-2"> <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"> <template v-if="!isXcdbBase">
<div class="flex flex-row space-x-2"> <div class="flex flex-row space-x-2">
<a-form-item class="flex w-1/2" :label="$t('labels.onUpdate')"> <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"> <script setup lang="ts">
import { onMounted } from '@vue/runtime-core' 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 { getAvailableRollupForUiType, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from '#imports' import type { Ref } from '#imports'
import { import {
@ -58,7 +58,7 @@ const refTables = computed(() => {
.filter( .filter(
(c) => (c) =>
isLinksOrLTAR(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.system &&
c.source_id === meta.value?.source_id, c.source_id === meta.value?.source_id,
) )

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

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

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnReqType, ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType } from 'nocodb-sdk' 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 { import {
ColumnInj, ColumnInj,
IsFormInj, IsFormInj,
@ -122,6 +122,10 @@ const columnTypeName = computed(() => {
if (column.value.uidt === UITypes.LongText && parseProp(column?.value?.meta)?.richMode) { if (column.value.uidt === UITypes.LongText && parseProp(column?.value?.meta)?.richMode) {
return UITypesName.RichText 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] : '' 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 } return { icon: iconMap.hm_solid }
case RelationTypes.BELONGS_TO: case RelationTypes.BELONGS_TO:
return { icon: iconMap.bt_solid } return { icon: iconMap.bt_solid }
case RelationTypes.ONE_TO_ONE:
return { icon: iconMap.oneToOneSolid, color: 'text-blue-500' }
} }
break break
case UITypes.SpecificDBType: case UITypes.SpecificDBType:

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

@ -50,7 +50,12 @@ const options = computed<ColumnType[]>(
return false return false
} else { } else {
/** ignore hasmany and manytomany relations if it's using within group menu */ /** 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)) .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 return false
} else { } else {
/** ignore hasmany and manytomany relations if it's using within sort menu */ /** 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 */ /** 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 return false
} else { } else {
/** ignore hasmany and manytomany relations if it's using within sort menu */ /** 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 */ /** 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 { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore( const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, relatedTableDisplayValuePropId, unlink } =
column as Ref<Required<ColumnType>>, useProvideLTARStore(column as Ref<Required<ColumnType>>, row, isNew, reloadRowTrigger.trigger)
row,
isNew,
reloadRowTrigger.trigger,
)
await loadRelatedTableMeta() await loadRelatedTableMeta()
@ -96,10 +92,14 @@ watch([listItemsDlg], () => {
<template> <template>
<div class="flex w-full chips-wrapper items-center" :class="{ active }"> <div class="flex w-full chips-wrapper items-center" :class="{ active }">
<div class="nc-cell-field chips flex items-center flex-1"> <div class="nc-cell-field chips flex items-center flex-1">
<template v-if="value && relatedTableDisplayValueProp"> <template v-if="value && (relatedTableDisplayValueProp || relatedTableDisplayValuePropId)">
<VirtualCellComponentsItemChip <VirtualCellComponentsItemChip
:item="value" :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" :column="belongsToColumn"
:show-unlink-button="true" :show-unlink-button="true"
@unlink="unlinkRef(value)" @unlink="unlinkRef(value)"
@ -116,7 +116,7 @@ watch([listItemsDlg], () => {
> >
<GeneralIcon <GeneralIcon
:icon="addIcon" :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" @click.stop="listItemsDlg = true"
/> />
</div> </div>

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

@ -77,9 +77,12 @@ watch([lookupColumn, rowHeight], () => {
const arrValue = computed(() => { const arrValue = computed(() => {
if (!cellValue.value) return [] 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 // 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] return [cellValue.value]
// TODO: We are filtering null as cell value can be null. Find the root cause and fix it // 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"> <template v-if="lookupColumn">
<!-- Render virtual cell --> <!-- Render virtual cell -->
<div v-if="isVirtualCol(lookupColumn)" class="flex h-full"> <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 <template
v-if=" v-if="
lookupColumn.uidt !== UITypes.LinkToAnotherRecord || 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 <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>

11
packages/nc-gui/components/virtual-cell/components/UnLinkedItems.vue

@ -51,6 +51,7 @@ const {
unlink, unlink,
row, row,
headerDisplayValue, headerDisplayValue,
resetChildrenExcludedOffsetCount
} = useLTARStoreOrThrow() } = useLTARStoreOrThrow()
const { addLTARRef, isNew, removeLTARRef, state: rowState } = useSmartsheetRowStoreOrThrow() const { addLTARRef, isNew, removeLTARRef, state: rowState } = useSmartsheetRowStoreOrThrow()
@ -101,6 +102,9 @@ watch(
} }
loadChildrenExcludedList(rowState.value) loadChildrenExcludedList(rowState.value)
} }
if(!nextVal){
resetChildrenExcludedOffsetCount()
}
}, },
{ {
immediate: true, immediate: true,
@ -255,6 +259,11 @@ onUnmounted(() => {
childrenExcludedListPagination.query = '' childrenExcludedListPagination.query = ''
window.removeEventListener('keydown', linkedShortcuts) window.removeEventListener('keydown', linkedShortcuts)
}) })
const onFilterChange = () => {
childrenExcludedListPagination.page = 1;
resetChildrenExcludedOffsetCount()
}
</script> </script>
<template> <template>
@ -285,7 +294,7 @@ onUnmounted(() => {
:placeholder="`${$t('general.searchIn')} ${relatedTableMeta?.title}`" :placeholder="`${$t('general.searchIn')} ${relatedTableMeta?.title}`"
class="w-full !rounded-md nc-excluded-search xs:min-h-8" class="w-full !rounded-md nc-excluded-search xs:min-h-8"
size="small" size="small"
@change="childrenExcludedListPagination.page = 1" @change="onFilterChange"
@keydown.capture.stop=" @keydown.capture.stop="
(e) => { (e) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {

43
packages/nc-gui/components/webhook/Editor.vue

@ -79,7 +79,7 @@ let hookRef = reactive<
const isBodyShownEasterEgg = ref(false) const isBodyShownEasterEgg = ref(false)
const isBodyShown = ref(hookRef.version === 'v1' || isEeUI) const isBodyShown = ref(hookRef.version === 'v1' || isEeUI)
const urlTabKey = ref(isBodyShownEasterEgg.value && isBodyShown.value ? 'body' : 'params') const urlTabKey = ref<'params' | 'headers' | 'body'>('params')
const apps: Record<string, any> = ref() const apps: Record<string, any> = ref()
@ -321,14 +321,6 @@ function setHook(newHook: HookType) {
payload: notification.payload, payload: notification.payload,
}, },
}) })
if (hookRef.version === 'v1' || isEeUI) {
urlTabKey.value = 'body'
eventList.value = [
{ text: ['After', 'Insert'], value: ['after', 'insert'] },
{ text: ['After', 'Update'], value: ['after', 'update'] },
{ text: ['After', 'Delete'], value: ['after', 'delete'] },
]
}
} }
function onEventChange() { function onEventChange() {
@ -486,6 +478,13 @@ const getDefaultHookName = (hooks: HookType[]) => {
return extractNextDefaultName([...hooks.map((el) => el?.title || '')], defaultHookName) return extractNextDefaultName([...hooks.map((el) => el?.title || '')], defaultHookName)
} }
const handleToggleEasterEgg = () => {
isBodyShownEasterEgg.value = !isBodyShownEasterEgg.value
if (!(isBodyShown.value && isBodyShownEasterEgg.value) && urlTabKey.value === 'body') {
urlTabKey.value = 'params'
}
}
watch( watch(
() => hookRef.eventOperation, () => hookRef.eventOperation,
() => { () => {
@ -592,7 +591,13 @@ onMounted(async () => {
class="nc-text-field-hook-event capitalize" class="nc-text-field-hook-event capitalize"
dropdown-class-name="nc-dropdown-webhook-event" dropdown-class-name="nc-dropdown-webhook-event"
> >
<a-select-option v-for="(event, i) in eventList" :key="i" class="capitalize" :value="event.value.join(' ')"> <a-select-option
v-for="(event, i) in eventList"
:key="i"
class="capitalize"
:value="event.value.join(' ')"
:disabled="hookRef.version === 'v1' && ['bulkInsert', 'bulkUpdate', 'bulkDelete'].includes(event.value[1])"
>
<div class="flex items-center gap-2 justify-between"> <div class="flex items-center gap-2 justify-between">
<div>{{ event.text.join(' ') }}</div> <div>{{ event.text.join(' ') }}</div>
<component <component
@ -657,7 +662,7 @@ onMounted(async () => {
size="large" size="large"
class="nc-select-hook-url-method" class="nc-select-hook-url-method"
dropdown-class-name="nc-dropdown-hook-notification-url-method" dropdown-class-name="nc-dropdown-hook-notification-url-method"
@dblclick="isBodyShownEasterEgg = !isBodyShownEasterEgg" @dblclick="handleToggleEasterEgg"
> >
<a-select-option v-for="(method, i) in methodList" :key="i" :value="method.title"> <a-select-option v-for="(method, i) in methodList" :key="i" :value="method.title">
<div class="flex items-center gap-2 justify-between"> <div class="flex items-center gap-2 justify-between">
@ -687,6 +692,14 @@ onMounted(async () => {
<a-col :span="24"> <a-col :span="24">
<NcTabs v-model:activeKey="urlTabKey" type="card" closeable="false" class="border-1 !pb-2 !rounded-lg"> <NcTabs v-model:activeKey="urlTabKey" type="card" closeable="false" class="border-1 !pb-2 !rounded-lg">
<a-tab-pane key="params" :tab="$t('title.parameter')" force-render>
<LazyApiClientParams v-model="hookRef.notification.payload.parameters" class="p-4" />
</a-tab-pane>
<a-tab-pane key="headers" :tab="$t('title.headers')" class="nc-tab-headers">
<LazyApiClientHeaders v-model="hookRef.notification.payload.headers" class="!p-4" />
</a-tab-pane>
<a-tab-pane v-if="isBodyShown && isBodyShownEasterEgg" key="body" tab="Body"> <a-tab-pane v-if="isBodyShown && isBodyShownEasterEgg" key="body" tab="Body">
<LazyMonacoEditor <LazyMonacoEditor
v-model="hookRef.notification.payload.body" v-model="hookRef.notification.payload.body"
@ -696,14 +709,6 @@ onMounted(async () => {
/> />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="params" :tab="$t('title.parameter')" force-render>
<LazyApiClientParams v-model="hookRef.notification.payload.parameters" class="p-4" />
</a-tab-pane>
<a-tab-pane key="headers" :tab="$t('title.headers')" class="nc-tab-headers">
<LazyApiClientHeaders v-model="hookRef.notification.payload.headers" class="!p-4" />
</a-tab-pane>
<!-- No in use at this moment --> <!-- No in use at this moment -->
<!-- <a-tab-pane key="auth" tab="Auth"> --> <!-- <a-tab-pane key="auth" tab="Auth"> -->
<!-- <LazyMonacoEditor v-model="hook.notification.payload.auth" class="min-h-60 max-h-80" /> --> <!-- <LazyMonacoEditor v-model="hook.notification.payload.auth" class="min-h-60 max-h-80" /> -->

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

@ -2,11 +2,10 @@ import type {
type ColumnType, type ColumnType,
type LinkToAnotherRecordType, type LinkToAnotherRecordType,
type PaginatedType, type PaginatedType,
RelationTypes,
type RequestParams, type RequestParams,
type TableType, type TableType,
} from 'nocodb-sdk' } 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 { ComputedRef, Ref } from 'vue'
import type { Row } from '#imports' import type { Row } from '#imports'
import { import {
@ -66,6 +65,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
query: '', query: '',
size: 10, size: 10,
}) })
const childrenExcludedOffsetCount = ref(0) const childrenExcludedOffsetCount = ref(0)
const childrenListPagination = reactive({ const childrenListPagination = reactive({
@ -120,7 +120,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const getRelatedTableRowId = (row: Record<string, any>) => { const getRelatedTableRowId = (row: Record<string, any>) => {
return relatedTableMeta.value?.columns return relatedTableMeta.value?.columns
?.filter((c) => c.pk) ?.filter((c) => c.pk)
.map((c) => row?.[c.title as string]) .map((c) => row?.[c.title as string] ?? row?.[c.id as string])
.join('___') .join('___')
} }
@ -134,6 +134,11 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
return (relatedTableMeta.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title || '' return (relatedTableMeta.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title || ''
}) })
// 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(() => { const relatedTablePrimaryKeyProps = computed(() => {
return relatedTableMeta.value?.columns?.filter((c) => c.pk)?.map((c) => c.title) ?? [] return relatedTableMeta.value?.columns?.filter((c) => c.pk)?.map((c) => c.title) ?? []
}) })
@ -186,8 +191,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const loadChildrenExcludedList = async (activeState?: any) => { const loadChildrenExcludedList = async (activeState?: any) => {
if (activeState) newRowState.state = activeState if (activeState) newRowState.state = activeState
try { try {
let offset = let offset = childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1) - childrenExcludedOffsetCount.value
childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1) - childrenExcludedOffsetCount.value
if (offset < 0) { if (offset < 0) {
offset = 0 offset = 0
@ -278,6 +282,12 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
}) })
} }
} catch (e: any) { } 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)}`) message.error(`${t('msg.error.failedToLoadList')}: ${await extractSdkResponseErrorMsg(e)}`)
} finally { } finally {
isChildrenExcludedLoading.value = false isChildrenExcludedLoading.value = false
@ -287,7 +297,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const loadChildrenList = async () => { const loadChildrenList = async () => {
try { try {
isChildrenLoading.value = true 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 if (!rowId.value || !column.value) return
let offset = childrenListPagination.size * (childrenListPagination.page - 1) + childrenListOffsetCount.value let offset = childrenListPagination.size * (childrenListPagination.page - 1) + childrenListOffsetCount.value
@ -438,7 +448,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
} }
isChildrenExcludedListLinked.value[index] = false isChildrenExcludedListLinked.value[index] = false
isChildrenListLinked.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 childrenListCount.value = childrenListCount.value - 1
} }
} catch (e: any) { } catch (e: any) {
@ -505,7 +515,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
isChildrenExcludedListLinked.value[index] = true isChildrenExcludedListLinked.value[index] = true
isChildrenListLinked.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 childrenListCount.value = childrenListCount.value + 1
} else { } else {
isChildrenExcludedListLinked.value = Array(childrenExcludedList.value?.list.length).fill(false) isChildrenExcludedListLinked.value = Array(childrenExcludedList.value?.list.length).fill(false)
@ -540,6 +550,10 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
}) })
}) })
const resetChildrenExcludedOffsetCount = () =>{
childrenExcludedOffsetCount.value = 0;
}
return { return {
relatedTableMeta, relatedTableMeta,
loadRelatedTableMeta, loadRelatedTableMeta,
@ -569,6 +583,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
deleteRelatedRow, deleteRelatedRow,
getRelatedTableRowId, getRelatedTableRowId,
headerDisplayValue, headerDisplayValue,
relatedTableDisplayValuePropId,
resetChildrenExcludedOffsetCount
} }
}, },
'ltar-store', 'ltar-store',

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

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

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

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

@ -12,7 +12,7 @@ import type {
StringOrNullType, StringOrNullType,
TableType, TableType,
} from 'nocodb-sdk' } 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 { isString } from '@vue/shared'
import { filterNullOrUndefinedObjectProperties } from '~/helpers/parsers/parserHelpers' import { filterNullOrUndefinedObjectProperties } from '~/helpers/parsers/parserHelpers'
import { import {
@ -22,6 +22,7 @@ import {
createEventHook, createEventHook,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
isNumericFieldType, isNumericFieldType,
isValidURL,
message, message,
parseProp, parseProp,
provide, provide,
@ -34,6 +35,7 @@ import {
useMetas, useMetas,
useProvideSmartsheetRowStore, useProvideSmartsheetRowStore,
useViewsStore, useViewsStore,
validateEmail,
watch, watch,
} from '#imports' } from '#imports'
import type { SharedViewMeta } from '#imports' import type { SharedViewMeta } from '#imports'
@ -138,7 +140,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
c?.cdf && c?.cdf &&
!/^\w+\(\)|CURRENT_TIMESTAMP$/.test(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 { return {
@ -176,7 +178,8 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
} catch (e: any) { } catch (e: any) {
if (e.response && e.response.status === 404) { if (e.response && e.response.status === 404) {
notFound.value = true notFound.value = true
} else if ((await extractSdkResponseErrorMsg(e)) === ErrorMessages.INVALID_SHARED_VIEW_PASSWORD) { // TODO - handle invalidSharedViewPassword
} else if (await extractSdkResponseErrorMsg(e)) {
passwordDlg.value = true passwordDlg.value = true
if (password.value && password.value !== '') passwordError.value = 'Something went wrong. Please check your credentials.' 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) && !isVirtualCol(column) &&
((column.rqd && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || column.required) ((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 ( } else if (
isLinksOrLTAR(column) && isLinksOrLTAR(column) &&
column.colOptions && column.colOptions &&
@ -216,6 +221,37 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
required: fieldRequired(), 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 return obj

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

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

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Datei löschen", "removeFile": "Datei löschen",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtuelle Relation", "virtualRelation": "Virtuelle Relation",
"linkMore": "Mehr verknüpfen", "linkMore": "Mehr verknüpfen",
"linkMoreRecords": "Weitere Datensätze verknüpfen", "linkMoreRecords": "Weitere Datensätze verknüpfen",

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

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

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

@ -339,6 +339,7 @@
"removeFile": "Eliminar archivo", "removeFile": "Eliminar archivo",
"hasMany": "Tiene muchos", "hasMany": "Tiene muchos",
"manyToMany": "Muchos a Muchos", "manyToMany": "Muchos a Muchos",
"oneToOne": "One to One",
"virtualRelation": "Relación virtual", "virtualRelation": "Relación virtual",
"linkMore": "Enlace más", "linkMore": "Enlace más",
"linkMoreRecords": "Vincular más registros", "linkMoreRecords": "Vincular más registros",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Hainbat ditu", "hasMany": "Hainbat ditu",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -1,7 +1,7 @@
{ {
"dashboards": { "dashboards": {
"create_new_dashboard_project": "Create New Interface", "create_new_dashboard_project": "Create New Interface",
"connect_data_sources": "Connect data sources", "connect_data_sources": "בחר מקור מידע",
"alert": "Alert", "alert": "Alert",
"alert-message": "No databases have been connected. Connect database bases to build interfaces. Skip this step and add databases from the base home page later.", "alert-message": "No databases have been connected. Connect database bases to build interfaces. Skip this step and add databases from the base home page later.",
"select_database_projects_that_you_want_to_link_to_this_dashboard_projects": "Select Database Bases that you want to link to this Interface.", "select_database_projects_that_you_want_to_link_to_this_dashboard_projects": "Select Database Bases that you want to link to this Interface.",
@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Rimuovi File", "removeFile": "Rimuovi File",
"hasMany": "Ha Molti", "hasMany": "Ha Molti",
"manyToMany": "Molti a Molti", "manyToMany": "Molti a Molti",
"oneToOne": "One to One",
"virtualRelation": "Relazione Virtuale", "virtualRelation": "Relazione Virtuale",
"linkMore": "Collega Altro", "linkMore": "Collega Altro",
"linkMoreRecords": "Collega più righe", "linkMoreRecords": "Collega più righe",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "파일삭제", "removeFile": "파일삭제",
"hasMany": "많이", "hasMany": "많이",
"manyToMany": "다대다", "manyToMany": "다대다",
"oneToOne": "One to One",
"virtualRelation": "가상 관계", "virtualRelation": "가상 관계",
"linkMore": "링크 더 보기", "linkMore": "링크 더 보기",
"linkMoreRecords": "더 많은 레코드 연결", "linkMoreRecords": "더 많은 레코드 연결",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -198,7 +198,7 @@
"logo": "Logo", "logo": "Logo",
"dropdown": "Rozwijane menu", "dropdown": "Rozwijane menu",
"list": "Lista", "list": "Lista",
"apply": "Apply" "apply": "Zastosuj"
}, },
"objects": { "objects": {
"day": "Dzień", "day": "Dzień",
@ -339,6 +339,7 @@
"removeFile": "Usuń plik", "removeFile": "Usuń plik",
"hasMany": "Ma wiele", "hasMany": "Ma wiele",
"manyToMany": "Wiele do wielu", "manyToMany": "Wiele do wielu",
"oneToOne": "One to One",
"virtualRelation": "Relacja wirtualna", "virtualRelation": "Relacja wirtualna",
"linkMore": "Połącz więcej", "linkMore": "Połącz więcej",
"linkMoreRecords": "Połącz więcej rekordów", "linkMoreRecords": "Połącz więcej rekordów",
@ -430,7 +431,7 @@
}, },
"selectFieldsFromRightPannelToAddHere": "Wybierz pole z panelu po prawej stronie aby tutaj dodać", "selectFieldsFromRightPannelToAddHere": "Wybierz pole z panelu po prawej stronie aby tutaj dodać",
"noOptionsFound": "Nie znaleziono opcji", "noOptionsFound": "Nie znaleziono opcji",
"surveyFormSubmitConfirmMsg": "Are you sure you want to submit this form?" "surveyFormSubmitConfirmMsg": "Czy na pewno chcesz przesłać ten formularz?"
}, },
"labels": { "labels": {
"selectYear": "Wybierz rok", "selectYear": "Wybierz rok",
@ -682,7 +683,7 @@
"sourceNameRequired": "nazwa źródła jest wymagana", "sourceNameRequired": "nazwa źródła jest wymagana",
"changeWsName": "Zmień nazwę przestrzeni roboczej", "changeWsName": "Zmień nazwę przestrzeni roboczej",
"pressEnter": "Naciśnij Enter", "pressEnter": "Naciśnij Enter",
"newFormLoaded": "Loading new form in", "newFormLoaded": "Ładowanie nowego formularza w",
"webhook": "Webhook", "webhook": "Webhook",
"multiField": { "multiField": {
"newField": "Nowe pole", "newField": "Nowe pole",
@ -703,10 +704,10 @@
"clearSelection": "Wyczyść wybór" "clearSelection": "Wyczyść wybór"
}, },
"activity": { "activity": {
"addMembers": "Add Members", "addMembers": "Dodaj Członków",
"enterEmail": "Enter email addresses", "enterEmail": "Wpisz adresy e-mail",
"inviteToBase": "Invite to Base", "inviteToBase": "Zaproś do bazy",
"addMember": "Add Member to Base", "addMember": "Dodaj członka do bazy",
"noRange": "Widok kalendarza wymaga zakresu dat", "noRange": "Widok kalendarza wymaga zakresu dat",
"goToToday": "Przejdź do dzisiaj", "goToToday": "Przejdź do dzisiaj",
"toggleSidebar": "Przełącz pasek boczny", "toggleSidebar": "Przełącz pasek boczny",

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

@ -339,6 +339,7 @@
"removeFile": "Remover ficheiro", "removeFile": "Remover ficheiro",
"hasMany": "Tem muitos", "hasMany": "Tem muitos",
"manyToMany": "De muitos para muitos", "manyToMany": "De muitos para muitos",
"oneToOne": "One to One",
"virtualRelation": "Relação virtual", "virtualRelation": "Relação virtual",
"linkMore": "Vincular mais", "linkMore": "Vincular mais",
"linkMoreRecords": "Vincular mais registos", "linkMoreRecords": "Vincular mais registos",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Удалить файл", "removeFile": "Удалить файл",
"hasMany": "Имеет много", "hasMany": "Имеет много",
"manyToMany": "Многие ко многим", "manyToMany": "Многие ко многим",
"oneToOne": "One to One",
"virtualRelation": "Виртуальные отношения", "virtualRelation": "Виртуальные отношения",
"linkMore": "Ссылка Подробнее", "linkMore": "Ссылка Подробнее",
"linkMoreRecords": "Связать больше записей", "linkMoreRecords": "Связать больше записей",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -81,33 +81,33 @@
"createEntity": "Створити {entity}", "createEntity": "Створити {entity}",
"creating": "Створення", "creating": "Створення",
"creatingEntity": "Створення {entity}", "creatingEntity": "Створення {entity}",
"details": "Details", "details": "Деталі",
"skip": "Skip", "skip": "Пропустити",
"code": "Code", "code": "Код",
"duplicate": "Дублювати", "duplicate": "Дублювати",
"duplicating": "Duplicating", "duplicating": "Дублювати",
"activate": "Activate", "activate": "Активувати",
"action": "Action", "action": "Дія",
"insert": "Вставити", "insert": "Вставити",
"delete": "Видалити", "delete": "Видалити",
"deleteEntity": "Delete {entity}", "deleteEntity": "Видалити {entity}",
"bulkInsert": "Bulk Insert", "bulkInsert": "Масова вставка",
"bulkDelete": "Bulk Delete", "bulkDelete": "Масове видалення",
"bulkUpdate": "Bulk Update", "bulkUpdate": "Масове оновлення",
"deleting": "Deleting", "deleting": "Видалення",
"update": "Оновити", "update": "Оновити",
"rename": "Перейменувати", "rename": "Перейменувати",
"reload": "Перезавантажити", "reload": "Перезавантажити",
"reset": "Відновити", "reset": "Відновити",
"install": "Встановити", "install": "Встановити",
"show": "Показати", "show": "Показати",
"access": "Access", "access": "Доступ",
"visibility": "Visibility", "visibility": "Видимість",
"hide": "Сховати", "hide": "Сховати",
"deprecated": "Deprecated", "deprecated": "Застаріле",
"showAll": "Показати все", "showAll": "Показати все",
"hideAll": "Сховати все", "hideAll": "Сховати все",
"notFound": "Not found", "notFound": "Не знайдено",
"showMore": "Показати більше", "showMore": "Показати більше",
"showOptions": "Показати опції", "showOptions": "Показати опції",
"hideOptions": "Сховати опції", "hideOptions": "Сховати опції",
@ -127,8 +127,8 @@
"upload": "Завантажити", "upload": "Завантажити",
"download": "Завантажити на ПК", "download": "Завантажити на ПК",
"default": "За замовчуванням", "default": "За замовчуванням",
"base": "Source", "base": "Джерело",
"datasource": "Data Source", "datasource": "Джерело даних",
"more": "Більше", "more": "Більше",
"less": "Менше", "less": "Менше",
"event": "Подія", "event": "Подія",
@ -136,14 +136,14 @@
"after": "Після", "after": "Після",
"before": "До", "before": "До",
"search": "Пошук", "search": "Пошук",
"searchIn": "Search In", "searchIn": "Шукати в",
"notification": "Сповіщення", "notification": "Сповіщення",
"reference": "Посилання", "reference": "Посилання",
"function": "Функція", "function": "Функція",
"confirm": "Підтвердити", "confirm": "Підтвердити",
"generate": "Генерувати", "generate": "Генерувати",
"copy": "Копіювати", "copy": "Копіювати",
"are": "are", "are": "є",
"misc": "Інше", "misc": "Інше",
"lock": "Блокувати", "lock": "Блокувати",
"unlock": "Розблокувати", "unlock": "Розблокувати",
@ -157,25 +157,25 @@
"groupingField": "Поле групування", "groupingField": "Поле групування",
"insertAfter": "Вставити після", "insertAfter": "Вставити після",
"insertBefore": "Вставити перед", "insertBefore": "Вставити перед",
"insertAbove": "Insert above", "insertAbove": "Вставити вище",
"insertBelow": "Insert below", "insertBelow": "Вставити нижче",
"hideField": "Приховати поле", "hideField": "Приховати поле",
"sortAsc": "За зростанням", "sortAsc": "За зростанням",
"sortDesc": "За спаданням", "sortDesc": "За спаданням",
"move": "Move", "move": "Перемістити",
"geoDataField": "Поле геоданих", "geoDataField": "Поле геоданих",
"type": "Type", "type": "Тип",
"name": "Name", "name": "Назва",
"changes": "Changes", "changes": "Зміни",
"new": "New", "new": "Створити",
"old": "Old", "old": "Старий",
"data": "Data", "data": "Дата",
"source": "Source", "source": "Джерело",
"destination": "Destination", "destination": "Призначення",
"active": "Active", "active": "Активний",
"inactive": "Inactive", "inactive": "Неактивний",
"linked": "linked", "linked": "прив'язаний",
"finish": "Finish", "finish": "Закінчити",
"min": "Min", "min": "Min",
"max": "Max", "max": "Max",
"avg": "Avg", "avg": "Avg",
@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

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

@ -339,6 +339,7 @@
"removeFile": "删除文件", "removeFile": "删除文件",
"hasMany": "单对多", "hasMany": "单对多",
"manyToMany": "多对多", "manyToMany": "多对多",
"oneToOne": "One to One",
"virtualRelation": "虚拟关系", "virtualRelation": "虚拟关系",
"linkMore": "链接更多", "linkMore": "链接更多",
"linkMoreRecords": "链接更多记录", "linkMoreRecords": "链接更多记录",

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

@ -339,6 +339,7 @@
"removeFile": "Remove File", "removeFile": "Remove File",
"hasMany": "Has Many", "hasMany": "Has Many",
"manyToMany": "Many to Many", "manyToMany": "Many to Many",
"oneToOne": "One to One",
"virtualRelation": "Virtual Relation", "virtualRelation": "Virtual Relation",
"linkMore": "Link More", "linkMore": "Link More",
"linkMoreRecords": "Link more records", "linkMoreRecords": "Link more records",

2
packages/nc-gui/package.json

@ -117,7 +117,7 @@
"@iconify-json/ion": "^1.1.15", "@iconify-json/ion": "^1.1.15",
"@iconify-json/la": "^1.1.8", "@iconify-json/la": "^1.1.8",
"@iconify-json/logos": "^1.1.42", "@iconify-json/logos": "^1.1.42",
"@iconify-json/lucide": "^1.1.175", "@iconify-json/lucide": "^1.1.176",
"@iconify-json/material-symbols": "^1.1.75", "@iconify-json/material-symbols": "^1.1.75",
"@iconify-json/mdi": "^1.1.64", "@iconify-json/mdi": "^1.1.64",
"@iconify-json/mi": "^1.1.8", "@iconify-json/mi": "^1.1.8",

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

@ -28,7 +28,7 @@ router.afterEach((to) => shouldRedirect(to.name as string))
<template> <template>
<div <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="{ :class="{
'children:(!h-auto my-auto)': sharedViewMeta?.surveyMode, '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; @apply bg-white dark:bg-slate-500 appearance-none;
&.nc-cell-checkbox { &.nc-cell-checkbox {
@ -85,7 +86,7 @@ p {
@apply bg-white dark:bg-slate-500; @apply bg-white dark:bg-slate-500;
&.nc-input { &.nc-input {
@apply w-full; @apply w-full h-10;
&:not(.layout-list) { &:not(.layout-list) {
@apply rounded-lg border-solid border-1 border-gray-200 focus-within:border-brand-500 overflow-hidden; @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; @apply px-3;
} }
} }
&:not(.nc-cell-longtext) { &.nc-cell:not(.nc-cell-longtext) {
@apply p-2; @apply p-2;
} }
&.nc-virtual-cell {
@apply px-2 py-1;
}
&.nc-cell-json { &.nc-cell-json {
@apply h-auto; @apply h-auto;
@ -177,6 +181,13 @@ p {
input.nc-cell-field { input.nc-cell-field {
@apply !py-0 !px-1; @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"> <template v-else-if="submitted">
<div class="flex justify-center"> <div class="flex justify-center">
<div v-if="sharedFormView" class="w-full"> <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> <template #message>
<LazyCellRichText <LazyCellRichText
v-if="sharedFormView?.success_msg?.trim()" v-if="sharedFormView?.success_msg?.trim()"
@ -140,7 +140,6 @@ const onDecode = async (scannedCodeValue: string) => {
v-if="sharedFormView?.submit_another_form" v-if="sharedFormView?.submit_another_form"
type="secondary" type="secondary"
:size="isMobileMode ? 'medium' : 'small'" :size="isMobileMode ? 'medium' : 'small'"
data-testid="nc-survey-form__btn-submit-another-form"
@click="submitted = false" @click="submitted = false"
> >
{{ $t('activity.submitAnotherForm') }} {{ $t('activity.submitAnotherForm') }}
@ -218,6 +217,11 @@ const onDecode = async (scannedCodeValue: string) => {
:column="field" :column="field"
:edit-enabled="!field?.read_only" :edit-enabled="!field?.read_only"
:read-only="field?.read_only" :read-only="field?.read_only"
@update:model-value="
() => {
v$.localState[field.title]?.$validate()
}
"
/> />
<a-button <a-button
v-if="field.enable_scanner" v-if="field.enable_scanner"
@ -232,7 +236,7 @@ const onDecode = async (scannedCodeValue: string) => {
</LazySmartsheetDivDataCell> </LazySmartsheetDivDataCell>
</NcTooltip> </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)"> <template v-if="isVirtualCol(field)">
<div v-for="error of v$.virtual[field.title]?.$errors" :key="`${error}virtual`" class="text-red-500"> <div v-for="error of v$.virtual[field.title]?.$errors" :key="`${error}virtual`" class="text-red-500">
{{ error.$message }} {{ 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 type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { breakpointsTailwind } from '@vueuse/core' import { breakpointsTailwind } from '@vueuse/core'
import tinycolor from 'tinycolor2'
import { import {
DropZoneRef, DropZoneRef,
IsSurveyFormInj, IsSurveyFormInj,
computed, computed,
isValidURL,
onKeyStroke, onKeyStroke,
onMounted, onMounted,
provide, provide,
ref, ref,
useBreakpoints, useBreakpoints,
useI18n,
usePointerSwipe, usePointerSwipe,
useSharedFormStoreOrThrow, useSharedFormStoreOrThrow,
useStepper, useStepper,
validateEmail,
} from '#imports' } from '#imports'
enum TransitionDirection { enum TransitionDirection {
@ -36,8 +34,6 @@ const { md } = useBreakpoints(breakpointsTailwind)
const { v$, formState, formColumns, submitForm, submitted, secondsRemain, sharedFormView, sharedViewMeta, onReset } = const { v$, formState, formColumns, submitForm, submitted, secondsRemain, sharedFormView, sharedViewMeta, onReset } =
useSharedFormStoreOrThrow() useSharedFormStoreOrThrow()
const { t } = useI18n()
const { isMobileMode } = storeToRefs(useConfigStore()) const { isMobileMode } = storeToRefs(useConfigStore())
const isTransitioning = ref(false) 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 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) { function isRequired(column: ColumnType, required = false) {
let columnObj = column let columnObj = column
@ -123,41 +129,20 @@ function animate(target: AnimationTarget) {
}, transitionDuration.value / 2) }, transitionDuration.value / 2)
} }
async function validateColumn() { const validateField = async (title: string, type: 'cell' | 'virtual') => {
const f = field.value! const validationField = type === 'cell' ? v$.value.localState[title] : v$.value.virtual[title]
if (parseProp(f.meta)?.validate && formState.value[f.title!]) {
if (f.uidt === UITypes.Email) { if (validationField) {
if (!validateEmail(formState.value[f.title!])) { return await validationField.$validate()
columnValidationError.value = true } else {
message.error(t('msg.error.invalidEmail')) return true
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
}
}
} }
return true
} }
async function goNext(animationTarget?: AnimationTarget) { 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?.title && !(await validateField(field.value.title, isVirtualCol(field.value) ? 'virtual' : 'cell'))) 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
animate(animationTarget || AnimationTarget.ArrowRight) animate(animationTarget || AnimationTarget.ArrowRight)
@ -172,9 +157,7 @@ async function goNext(animationTarget?: AnimationTarget) {
} }
async function goPrevious(animationTarget?: AnimationTarget) { async function goPrevious(animationTarget?: AnimationTarget) {
if (isFirst.value || !isStarted.value || submitted.value) return if (isFirst.value || !isStarted.value || submitted.value || dialogShow.value) return
columnValidationError.value = false
animate(animationTarget || AnimationTarget.ArrowLeft) animate(animationTarget || AnimationTarget.ArrowLeft)
@ -188,7 +171,7 @@ function focusInput() {
const inputEl = const inputEl =
(document.querySelector('.nc-cell input') as HTMLInputElement) || (document.querySelector('.nc-cell input') as HTMLInputElement) ||
(document.querySelector('.nc-cell textarea') as HTMLTextAreaElement) || (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) { if (inputEl) {
activeCell.value = inputEl activeCell.value = inputEl
@ -207,7 +190,7 @@ function resetForm() {
} }
async function submit() { async function submit() {
if (submitted.value || !(await validateColumn())) return if (submitted.value) return
dialogShow.value = false dialogShow.value = false
submitForm() 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'], () => { onKeyStroke(['ArrowLeft', 'ArrowDown'], () => {
goPrevious(AnimationTarget.ArrowLeft) goPrevious(AnimationTarget.ArrowLeft)
}) })
onKeyStroke(['ArrowRight', 'ArrowUp'], () => { onKeyStroke(['ArrowRight', 'ArrowUp'], () => {
goNext(AnimationTarget.ArrowRight) goNext(AnimationTarget.ArrowRight)
}) })
onKeyStroke(['Enter', 'Space'], () => { onKeyStroke(['Enter'], async (e) => {
if (submitted.value) return if (submitted.value) return
if (!isStarted.value && !submitted.value) { if (!isStarted.value && !submitted.value) {
@ -244,7 +241,8 @@ onKeyStroke(['Enter', 'Space'], () => {
if (dialogShow.value) { if (dialogShow.value) {
submit() submit()
} else { } else {
dialogShow.value = true e.preventDefault()
showSubmitConfirmModal()
} }
} else { } else {
const activeElement = document.activeElement as HTMLElement const activeElement = document.activeElement as HTMLElement
@ -277,16 +275,6 @@ onMounted(() => {
}) })
} }
}) })
watch(
formState,
() => {
columnValidationError.value = false
},
{
deep: true,
},
)
</script> </script>
<template> <template>
@ -372,10 +360,7 @@ watch(
<div class="flex justify-end mt-12"> <div class="flex justify-end mt-12">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="hidden md:flex text-sm items-center gap-1 text-gray-800"> <div class="hidden md:flex text-sm items-center gap-1 text-gray-800">
<span> <span> {{ $t('labels.pressEnter') }} </span>
{{ $t('labels.pressEnter') }}
</span>
<NcBadge class="pl-4 pr-1 h-[21px] text-gray-600"> </NcBadge>
</div> </div>
<NcButton <NcButton
:size="isMobileMode ? 'medium' : 'small'" :size="isMobileMode ? 'medium' : 'small'"
@ -429,6 +414,7 @@ watch(
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`" :data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field" :column="field"
:read-only="field?.read_only" :read-only="field?.read_only"
@update:model-value="validateField(field.title, 'virtual')"
/> />
<LazySmartsheetCell <LazySmartsheetCell
@ -440,12 +426,20 @@ watch(
:column="field" :column="field"
:edit-enabled="!field?.read_only" :edit-enabled="!field?.read_only"
:read-only="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 class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-xs my-2 px-1">
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500"> <template v-if="isVirtualCol(field)">
{{ error.$message }} <div v-for="error of v$.virtual[field.title]?.$errors" :key="`${error}virtual`" class="text-red-500">
</div> {{ 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"> <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') }} {{ $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="ml-1 mt-4 flex w-full text-lg">
<div class="flex-1 flex justify-end"> <div class="flex-1 flex justify-end">
<div v-if="isLast && !v$.$invalid"> <div v-if="isLast">
<NcButton <NcButton
:size="isMobileMode ? 'medium' : 'small'" :size="isMobileMode ? 'medium' : 'small'"
:class=" :class="
@ -466,9 +460,9 @@ watch(
? 'transform translate-y-[1px] translate-x-[1px] ring ring-accent ring-opacity-100' ? '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" data-testid="nc-survey-form__btn-submit-confirm"
@click="dialogShow = true" @click="showSubmitConfirmModal"
> >
{{ $t('general.submit') }} form {{ $t('general.submit') }} form
</NcButton> </NcButton>
@ -477,17 +471,9 @@ watch(
<div v-else class="flex items-center gap-3"> <div v-else class="flex items-center gap-3">
<div <div
class="hidden md:flex text-sm items-center gap-1" 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> <span> {{ $t('labels.pressEnter') }} </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>
</div> </div>
<NcButton <NcButton
:size="isMobileMode ? 'medium' : 'small'" :size="isMobileMode ? 'medium' : 'small'"
@ -498,7 +484,7 @@ watch(
? 'transform translate-y-[2px] translate-x-[2px] after:(!ring !ring-accent !ring-opacity-100)' ? '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()" @click="goNext()"
> >
{{ $t('labels.next') }} {{ $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="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-end items-center gap-4">
<div class="flex justify-center"> <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>
<div v-if="isStarted && !submitted" class="flex items-center gap-3"> <div v-if="isStarted && !submitted" class="flex items-center gap-3">
<NcButton <NcButton
type="secondary" type="secondary"
:size="isMobileMode ? 'medium' : 'small'" :size="isMobileMode ? 'medium' : 'small'"
data-testid="nc-survey-form__icon-prev" data-testid="nc-survey-form__icon-prev"
:disabled="isFirst || v$.localState[field.title]?.$error" :disabled="isFirst"
@click="goPrevious()" @click="goPrevious()"
> >
<GeneralIcon icon="ncArrowLeft" <GeneralIcon icon="ncArrowLeft"
@ -530,7 +528,7 @@ watch(
:size="isMobileMode ? 'medium' : 'small'" :size="isMobileMode ? 'medium' : 'small'"
type="secondary" type="secondary"
data-testid="nc-survey-form__icon-next" data-testid="nc-survey-form__icon-next"
:disabled="isLast || v$.localState[field.title]?.$error || columnValidationError" :disabled="isLast || fieldHasError"
@click="goNext()" @click="goNext()"
> >
<GeneralIcon icon="ncArrowRight" /> <GeneralIcon icon="ncArrowRight" />
@ -552,6 +550,7 @@ watch(
type="primary" type="primary"
:size="isMobileMode ? 'medium' : 'small'" :size="isMobileMode ? 'medium' : 'small'"
data-testid="nc-survey-form__btn-submit" data-testid="nc-survey-form__btn-submit"
class="nc-survey-form-btn-submit"
@click="submit" @click="submit"
> >
{{ $t('general.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 { activeWorkspaceId } = storeToRefs(useWorkspace())
const { meta: metaKey, ctrlKey } = useMagicKeys() const { meta: metaKey, control } = useMagicKeys()
const recentViews = computed<RecentView[]>(() => const recentViews = computed<RecentView[]>(() =>
allRecentViews.value.filter((f) => f.workspaceId === activeWorkspaceId.value).splice(0, 10), allRecentViews.value.filter((f) => f.workspaceId === activeWorkspaceId.value).splice(0, 10),
@ -219,7 +219,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
hardReload?: boolean hardReload?: boolean
doNotSwitchTab?: 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' 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) !/^\w+\(\)|CURRENT_TIMESTAMP$/.test(col.cdf)
) { ) {
const defaultValue = 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 return acc
}, {} as Record<string, any>) }, {} 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 NcCellAttachment from '~icons/nc-icons/cell-attachment'
import NcCircleCheck from '~icons/nc-icons/circle-check' import NcCircleCheck from '~icons/nc-icons/circle-check'
import OnetoOneIcon from '~icons/nc-icons/onetoone'
// keep it for reference // keep it for reference
// todo: remove it after all icons are migrated // todo: remove it after all icons are migrated
@ -358,6 +359,7 @@ export const iconMap = {
mm_solid: ManytoManySolidIcon, mm_solid: ManytoManySolidIcon,
hm_solid: HasManySolidIcon, hm_solid: HasManySolidIcon,
bt_solid: BelongsToSolidIcon, bt_solid: BelongsToSolidIcon,
oneToOneSolid: OnetoOneIcon,
workspaceDefault: MsGroup, workspaceDefault: MsGroup,
project: Project, project: Project,
search: NcSearch, search: NcSearch,

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

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

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

@ -1637,7 +1637,7 @@ export interface LinkToAnotherColumnReqType {
/** The title of the virtual column */ /** The title of the virtual column */
title: string; title: string;
/** The type of the relationship */ /** The type of the relationship */
type: 'bt' | 'hm' | 'mm'; type: 'bt' | 'hm' | 'mm' | 'oo';
/** Abstract type of the relationship */ /** Abstract type of the relationship */
uidt: 'LinkToAnotherRecord' | 'Links'; uidt: 'LinkToAnotherRecord' | 'Links';
/** Is this relationship virtual? */ /** Is this relationship virtual? */
@ -8298,7 +8298,7 @@ export class Api<
baseName: string, baseName: string,
tableName: string, tableName: string,
rowId: any, rowId: any,
relationType: 'mm' | 'hm' | 'bt', relationType: 'mm' | 'hm' | 'bt' | 'oo',
columnName: string, columnName: string,
query?: { query?: {
/** @min 1 */ /** @min 1 */
@ -8346,7 +8346,7 @@ export class Api<
baseName: string, baseName: string,
tableName: string, tableName: string,
rowId: any, rowId: any,
relationType: 'mm' | 'hm' | 'bt', relationType: 'mm' | 'hm' | 'bt' | 'oo',
columnName: string, columnName: string,
refRowId: string, refRowId: string,
query?: { query?: {
@ -8413,7 +8413,7 @@ export class Api<
baseName: string, baseName: string,
tableName: string, tableName: string,
rowId: any, rowId: any,
relationType: 'mm' | 'hm' | 'bt', relationType: 'mm' | 'hm' | 'bt' | 'oo',
columnName: string, columnName: string,
refRowId: string, refRowId: string,
params: RequestParams = {} params: RequestParams = {}
@ -8453,7 +8453,7 @@ export class Api<
baseName: string, baseName: string,
tableName: string, tableName: string,
rowId: any, rowId: any,
relationType: 'mm' | 'hm' | 'bt', relationType: 'mm' | 'hm' | 'bt' | 'oo',
columnName: string, columnName: string,
query?: { query?: {
/** @min 1 */ /** @min 1 */
@ -9347,7 +9347,7 @@ export class Api<
dataNestedList: ( dataNestedList: (
sharedViewUuid: string, sharedViewUuid: string,
rowId: any, rowId: any,
relationType: 'mm' | 'hm' | 'bt', relationType: 'mm' | 'hm' | 'bt' | 'oo',
columnName: string, columnName: string,
query?: { query?: {
/** Which fields to be shown */ /** Which fields to be shown */

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

@ -20,6 +20,7 @@ export enum RelationTypes {
HAS_MANY = 'hm', HAS_MANY = 'hm',
BELONGS_TO = 'bt', BELONGS_TO = 'bt',
MANY_TO_MANY = 'mm', MANY_TO_MANY = 'mm',
ONE_TO_ONE = 'oo',
} }
export enum ExportTypes { export enum ExportTypes {
@ -27,11 +28,6 @@ export enum ExportTypes {
CSV = 'csv', CSV = 'csv',
} }
export enum ErrorMessages {
INVALID_SHARED_VIEW_PASSWORD = 'INVALID_SHARED_VIEW_PASSWORD',
NOT_IMPLEMENTED = 'NOT_IMPLEMENTED',
}
export enum AuditOperationTypes { export enum AuditOperationTypes {
COMMENT = 'COMMENT', COMMENT = 'COMMENT',
DATA = 'DATA', DATA = 'DATA',
@ -128,6 +124,27 @@ export enum NcDataErrorCodes {
NC_ERR_MM_MODEL_NOT_FOUND = 'NC_ERR_MM_MODEL_NOT_FOUND', 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 Roles = OrgUserRoles | ProjectRoles | WorkspaceUserRoles;
type RolesObj = Partial<Record<Roles, boolean>>; type RolesObj = Partial<Record<Roles, boolean>>;

10
packages/nocodb/package.json

@ -68,7 +68,7 @@
"@ntegral/nestjs-sentry": "^4.0.1", "@ntegral/nestjs-sentry": "^4.0.1",
"@sentry/node": "^6.19.7", "@sentry/node": "^6.19.7",
"@techpass/passport-openidconnect": "^0.3.3", "@techpass/passport-openidconnect": "^0.3.3",
"@types/chai": "^4.3.13", "@types/chai": "^4.3.14",
"airtable": "^0.12.2", "airtable": "^0.12.2",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"ajv-formats": "^2.1.1", "ajv-formats": "^2.1.1",
@ -132,13 +132,13 @@
"mysql2": "^3.9.2", "mysql2": "^3.9.2",
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
"nc-help": "0.3.1", "nc-help": "0.3.1",
"nc-lib-gui": "0.204.5", "nc-lib-gui": "0.204.7",
"nc-plugin": "^0.1.3", "nc-plugin": "^0.1.3",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nestjs-kafka": "^1.0.6", "nestjs-kafka": "^1.0.6",
"nestjs-throttler-storage-redis": "^0.4.3", "nestjs-throttler-storage-redis": "^0.4.4",
"nocodb-sdk": "workspace:^", "nocodb-sdk": "workspace:^",
"nodemailer": "^6.9.12", "nodemailer": "^6.9.13",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"object-sizeof": "^2.6.4", "object-sizeof": "^2.6.4",
"os-locale": "^6.0.2", "os-locale": "^6.0.2",
@ -184,7 +184,7 @@
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/mocha": "^10.0.6", "@types/mocha": "^10.0.6",
"@types/multer": "^1.4.11", "@types/multer": "^1.4.11",
"@types/node": "20.11.29", "@types/node": "20.11.30",
"@types/passport-google-oauth20": "^2.0.14", "@types/passport-google-oauth20": "^2.0.14",
"@types/passport-jwt": "^3.0.13", "@types/passport-jwt": "^3.0.13",
"@types/supertest": "^2.0.16", "@types/supertest": "^2.0.16",

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 // todo: handle case where the given column is not ltar
@Get(['/api/v1/db/data/:orgs/:baseName/:tableName/:rowId/hm/:columnName']) @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, titleOrId: req.params.viewName,
fk_model_id: model.id, fk_model_id: model.id,
})); }));
if (!model) NcError.notFound('Table not found'); if (!model) NcError.tableNotFound(req.params.tableName);
return { model, view }; return { model, view };
} }
} }

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

@ -6,7 +6,7 @@ import {
Response, Response,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { ErrorMessages, isSystemColumn, ViewTypes } from 'nocodb-sdk'; import { isSystemColumn, ViewTypes } from 'nocodb-sdk';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { nocoExecute } from 'nc-help'; import { nocoExecute } from 'nc-help';
import papaparse from 'papaparse'; import papaparse from 'papaparse';
@ -35,7 +35,7 @@ export class PublicDatasExportController {
@Param('publicDataUuid') publicDataUuid: string, @Param('publicDataUuid') publicDataUuid: string,
) { ) {
const view = await View.getByUUID(publicDataUuid); const view = await View.getByUUID(publicDataUuid);
if (!view) NcError.notFound('Not found'); if (!view) NcError.viewNotFound(publicDataUuid);
if ( if (
view.type !== ViewTypes.GRID && view.type !== ViewTypes.GRID &&
view.type !== ViewTypes.KANBAN && view.type !== ViewTypes.KANBAN &&
@ -45,7 +45,7 @@ export class PublicDatasExportController {
NcError.notFound('Not found'); NcError.notFound('Not found');
if (view.password && view.password !== req.headers?.['xc-password']) { if (view.password && view.password !== req.headers?.['xc-password']) {
NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); NcError.invalidSharedViewPassword();
} }
const model = await view.getModelWithInfo(); const model = await view.getModelWithInfo();
@ -88,7 +88,7 @@ export class PublicDatasExportController {
const view = await View.getByUUID(req.params.publicDataUuid); const view = await View.getByUUID(req.params.publicDataUuid);
const fields = req.query.fields; const fields = req.query.fields;
if (!view) NcError.notFound('Not found'); if (!view) NcError.viewNotFound(req.params.publicDataUuid);
if ( if (
view.type !== ViewTypes.GRID && view.type !== ViewTypes.GRID &&
view.type !== ViewTypes.KANBAN && view.type !== ViewTypes.KANBAN &&
@ -98,7 +98,7 @@ export class PublicDatasExportController {
NcError.notFound('Not found'); NcError.notFound('Not found');
if (view.password && view.password !== req.headers?.['xc-password']) { if (view.password && view.password !== req.headers?.['xc-password']) {
NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); NcError.invalidSharedViewPassword();
} }
const model = await view.getModelWithInfo(); 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) => { args.column_name.split(',').map(async (col) => {
let column = cols.find((c) => c.column_name === col || c.title === col); let column = cols.find((c) => c.column_name === col || c.title === col);
if (!column) { 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 // 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, (c) => c.column_name === col || c.title === col,
); );
if (!column) { 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 // if qrCode or Barcode replace it with value column nd keep the alias
@ -1805,6 +1805,56 @@ class BaseModelSqlv2 {
?.count; ?.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 // todo: naming & optimizing
public async getBtChildrenExcludedList( public async getBtChildrenExcludedList(
{ colId, cid = null }, { 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( protected async getSelectQueryBuilderForFormula(
column: Column<any>, column: Column<any>,
tableAlias?: string, tableAlias?: string,
@ -2106,6 +2235,141 @@ class BaseModelSqlv2 {
return await readLoader.load(this?.[cCol?.title]); return await readLoader.load(this?.[cCol?.title]);
}; };
// todo : handle mm // 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; break;
@ -2806,7 +3070,7 @@ class BaseModelSqlv2 {
const nestedCols = (await this.model.getColumns()).filter((c) => const nestedCols = (await this.model.getColumns()).filter((c) =>
isLinksOrLTAR(c), isLinksOrLTAR(c),
); );
const postInsertOps = await this.prepareNestedLinkQb({ const { postInsertOps, preInsertOps } = await this.prepareNestedLinkQb({
nestedCols, nestedCols,
data, data,
insertObj, insertObj,
@ -2818,6 +3082,8 @@ class BaseModelSqlv2 {
await this.prepareNocoData(insertObj, true, cookie); await this.prepareNocoData(insertObj, true, cookie);
await Promise.all(preInsertOps.map((f) => f(this.dbDriver)));
let response; let response;
const query = this.dbDriver(this.tnPath).insert(insertObj); const query = this.dbDriver(this.tnPath).insert(insertObj);
@ -2958,6 +3224,7 @@ class BaseModelSqlv2 {
insertObj: Record<string, any>; insertObj: Record<string, any>;
}) { }) {
const postInsertOps: ((rowId: any, trx?: any) => Promise<void>)[] = []; const postInsertOps: ((rowId: any, trx?: any) => Promise<void>)[] = [];
const preInsertOps: ((trx?: any) => Promise<void>)[] = [];
for (const col of nestedCols) { for (const col of nestedCols) {
if (col.title in data) { if (col.title in data) {
const colOptions = await col.getColOptions<LinkToAnotherRecordColumn>(); const colOptions = await col.getColOptions<LinkToAnotherRecordColumn>();
@ -2981,6 +3248,45 @@ class BaseModelSqlv2 {
insertObj[childCol.column_name] = nestedData?.[parentCol.title]; insertObj[childCol.column_name] = nestedData?.[parentCol.title];
} }
break; 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: case RelationTypes.HAS_MANY:
{ {
if (!Array.isArray(nestedData)) continue; if (!Array.isArray(nestedData)) continue;
@ -3033,7 +3339,7 @@ class BaseModelSqlv2 {
} }
} }
} }
return postInsertOps; return { postInsertOps, preInsertOps };
} }
async bulkInsert( async bulkInsert(
@ -3061,6 +3367,7 @@ class BaseModelSqlv2 {
// TODO: ag column handling for raw bulk insert // TODO: ag column handling for raw bulk insert
const insertDatas = raw ? datas : []; const insertDatas = raw ? datas : [];
let postInsertOps: ((rowId: any, trx?: any) => Promise<void>)[] = []; let postInsertOps: ((rowId: any, trx?: any) => Promise<void>)[] = [];
let preInsertOps: ((trx?: any) => Promise<void>)[] = [];
let aiPkCol: Column; let aiPkCol: Column;
let agPkCol: Column; let agPkCol: Column;
@ -3209,11 +3516,14 @@ class BaseModelSqlv2 {
// prepare nested link data for insert only if it is single record insertion // prepare nested link data for insert only if it is single record insertion
if (isSingleRecordInsertion) { if (isSingleRecordInsertion) {
postInsertOps = await this.prepareNestedLinkQb({ const operations = await this.prepareNestedLinkQb({
nestedCols, nestedCols,
data: d, data: d,
insertObj, insertObj,
}); });
postInsertOps = operations.postInsertOps;
preInsertOps = operations.preInsertOps;
} }
insertDatas.push(insertObj); insertDatas.push(insertObj);
@ -3244,6 +3554,8 @@ class BaseModelSqlv2 {
} }
} }
await Promise.all(preInsertOps.map((f) => f(trx)));
let responses; let responses;
// insert one by one as fallback to get ids for sqlite and mysql // insert one by one as fallback to get ids for sqlite and mysql
@ -3371,9 +3683,7 @@ class BaseModelSqlv2 {
if (!pkValues) { if (!pkValues) {
// throw or skip if no pk provided // throw or skip if no pk provided
if (throwExceptionIfNotExist) { if (throwExceptionIfNotExist) {
NcError.unprocessableEntity( NcError.recordNotFound(JSON.stringify(pkValues));
`Record with pk ${JSON.stringify(pkValues)} not found`,
);
} }
continue; continue;
} }
@ -3384,9 +3694,7 @@ class BaseModelSqlv2 {
if (!oldRecord) { if (!oldRecord) {
// throw or skip if no record found // throw or skip if no record found
if (throwExceptionIfNotExist) { if (throwExceptionIfNotExist) {
NcError.unprocessableEntity( NcError.recordNotFound(JSON.stringify(pkValues));
`Record with pk ${JSON.stringify(pkValues)} not found`,
);
} }
continue; continue;
} }
@ -3541,9 +3849,7 @@ class BaseModelSqlv2 {
if (!pkValues) { if (!pkValues) {
// throw or skip if no pk provided // throw or skip if no pk provided
if (throwExceptionIfNotExist) { if (throwExceptionIfNotExist) {
NcError.unprocessableEntity( NcError.recordNotFound(JSON.stringify(pkValues));
`Record with pk ${JSON.stringify(pkValues)} not found`,
);
} }
continue; continue;
} }
@ -3552,9 +3858,7 @@ class BaseModelSqlv2 {
if (!deletedRecord) { if (!deletedRecord) {
// throw or skip if no record found // throw or skip if no record found
if (throwExceptionIfNotExist) { if (throwExceptionIfNotExist) {
NcError.unprocessableEntity( NcError.recordNotFound(JSON.stringify(pkValues));
`Record with pk ${JSON.stringify(pkValues)} not found`,
);
} }
continue; continue;
} }
@ -4118,7 +4422,7 @@ class BaseModelSqlv2 {
!column || !column ||
![UITypes.LinkToAnotherRecord, UITypes.Links].includes(column.uidt) ![UITypes.LinkToAnotherRecord, UITypes.Links].includes(column.uidt)
) )
NcError.notFound('Column not found'); NcError.fieldNotFound(colId);
const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>(); const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>();
@ -4245,6 +4549,54 @@ class BaseModelSqlv2 {
}); });
} }
break; 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( const response = await this.readByPk(
@ -4290,7 +4642,7 @@ class BaseModelSqlv2 {
!column || !column ||
![UITypes.LinkToAnotherRecord, UITypes.Links].includes(column.uidt) ![UITypes.LinkToAnotherRecord, UITypes.Links].includes(column.uidt)
) )
NcError.notFound('Column not found'); NcError.fieldNotFound(colId);
const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>(); const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>();
@ -4395,6 +4747,24 @@ class BaseModelSqlv2 {
}); });
} }
break; 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( const newData = await this.readByPk(
@ -4440,9 +4810,9 @@ class BaseModelSqlv2 {
.getColumns() .getColumns()
.then((cols) => cols?.find((col) => col.id === args.groupColumnId)); .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)) if (isVirtualCol(column))
NcError.notImplemented('Grouping for virtual columns not implemented'); NcError.notImplemented('Grouping for virtual columns');
// extract distinct group column values // extract distinct group column values
let groupingValues: Set<any>; let groupingValues: Set<any>;
@ -4602,9 +4972,9 @@ class BaseModelSqlv2 {
.getColumns() .getColumns()
.then((cols) => cols?.find((col) => col.id === args.groupColumnId)); .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)) if (isVirtualCol(column))
NcError.notImplemented('Grouping for virtual columns not implemented'); NcError.notImplemented('Grouping for virtual columns');
const qb = this.dbDriver(this.tnPath) const qb = this.dbDriver(this.tnPath)
.count('*', { as: 'count' }) .count('*', { as: 'count' })
@ -4767,7 +5137,11 @@ class BaseModelSqlv2 {
} }
idToAliasMap[col.id] = col.title; 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; btMap[col.id] = true;
const btData = Object.values(data).find( const btData = Object.values(data).find(
(d) => d[col.id] && Object.keys(d[col.id]), (d) => d[col.id] && Object.keys(d[col.id]),
@ -5200,7 +5574,7 @@ class BaseModelSqlv2 {
async addLinks({ async addLinks({
cookie, cookie,
childIds, childIds: _childIds,
colId, colId,
rowId, rowId,
}: { }: {
@ -5212,8 +5586,7 @@ class BaseModelSqlv2 {
const columns = await this.model.getColumns(); const columns = await this.model.getColumns();
const column = columns.find((c) => c.id === colId); const column = columns.find((c) => c.id === colId);
if (!column || !isLinksOrLTAR(column)) if (!column || !isLinksOrLTAR(column)) NcError.fieldNotFound(colId);
NcError.notFound(`Link column ${colId} not found`);
const row = await this.readByPk( const row = await this.readByPk(
rowId, rowId,
@ -5224,10 +5597,10 @@ class BaseModelSqlv2 {
// validate rowId // validate rowId
if (!row) { 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>(); const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>();
@ -5241,7 +5614,39 @@ class BaseModelSqlv2 {
const childTn = this.getTnPath(childTable); const childTn = this.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable); 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: case RelationTypes.MANY_TO_MANY:
{ {
const vChildCol = await colOptions.getMMChildColumn(); const vChildCol = await colOptions.getMMChildColumn();
@ -5303,11 +5708,7 @@ class BaseModelSqlv2 {
!childRows.find((r) => r[parentColumn.column_name] === id), !childRows.find((r) => r[parentColumn.column_name] === id),
); );
NcError.unprocessableEntity( NcError.recordNotFound(extractIds(missingIds));
`Child record with id [${extractIdsString(
missingIds,
)}] not found`,
);
} }
insertData = childRows insertData = childRows
@ -5380,11 +5781,7 @@ class BaseModelSqlv2 {
!childRows.find((r) => r[parentColumn.column_name] === id), !childRows.find((r) => r[parentColumn.column_name] === id),
); );
NcError.unprocessableEntity( NcError.recordNotFound(extractIds(missingIds));
`Child record with id [${extractIdsString(
missingIds,
)}] not found`,
);
} }
} }
const updateQb = this.dbDriver(childTn).update({ const updateQb = this.dbDriver(childTn).update({
@ -5438,12 +5835,7 @@ class BaseModelSqlv2 {
}); });
if (!childRow) { if (!childRow) {
NcError.unprocessableEntity( NcError.recordNotFound(extractIds(childIds, true));
`Child record with id [${extractIdsString(
childIds,
true,
)}] not found`,
);
} }
} }
@ -5499,8 +5891,7 @@ class BaseModelSqlv2 {
const columns = await this.model.getColumns(); const columns = await this.model.getColumns();
const column = columns.find((c) => c.id === colId); const column = columns.find((c) => c.id === colId);
if (!column || !isLinksOrLTAR(column)) if (!column || !isLinksOrLTAR(column)) NcError.fieldNotFound(colId);
NcError.notFound(`Link column ${colId} not found`);
const row = await this.readByPk( const row = await this.readByPk(
rowId, rowId,
@ -5511,7 +5902,7 @@ class BaseModelSqlv2 {
// validate rowId // validate rowId
if (!row) { if (!row) {
NcError.notFound(`Record with id '${rowId}' not found`); NcError.recordNotFound(rowId);
} }
if (!childIds.length) return; if (!childIds.length) return;
@ -5585,11 +5976,7 @@ class BaseModelSqlv2 {
), ),
); );
NcError.unprocessableEntity( NcError.recordNotFound(extractIds(missingIds));
`Child record with id [${extractIdsString(
missingIds,
)}] not found`,
);
} }
} }
@ -5672,11 +6059,7 @@ class BaseModelSqlv2 {
), ),
); );
NcError.unprocessableEntity( NcError.recordNotFound(extractIds(missingIds));
`Child record with id [${extractIdsString(
missingIds,
)}] not found`,
);
} }
} }
@ -5734,12 +6117,7 @@ class BaseModelSqlv2 {
}); });
if (!childRow) { if (!childRow) {
NcError.unprocessableEntity( NcError.recordNotFound(extractIds(childIds, true));
`Child record with id [${extractIdsString(
childIds,
true,
)}] not found`,
);
} }
} }
@ -5799,7 +6177,7 @@ class BaseModelSqlv2 {
// validate rowId // validate rowId
if (!row) { if (!row) {
NcError.notFound(`Record with id ${id} not found`); NcError.recordNotFound(id);
} }
const parentCol = await ( const parentCol = await (
@ -6087,7 +6465,7 @@ export function extractSortsObject(
else sort.fk_column_id = aliasColObjMap[s.replace(/^\+/, '')]?.id; else sort.fk_column_id = aliasColObjMap[s.replace(/^\+/, '')]?.id;
if (throwErrorIfInvalid && !sort.fk_column_id) if (throwErrorIfInvalid && !sort.fk_column_id)
NcError.unprocessableEntity(`Invalid field: ${s.replace(/^[+-]/, '')}`); NcError.fieldNotFound(s.replace(/^[+-]/, ''));
return new Sort(sort); return new Sort(sort);
}); });
@ -6258,7 +6636,7 @@ export function extractCondition(
validateFilterComparison(aliasColObjMap[alias].uidt, op, sub_op); validateFilterComparison(aliasColObjMap[alias].uidt, op, sub_op);
} else if (throwErrorIfInvalid) { } else if (throwErrorIfInvalid) {
NcError.unprocessableEntity(`Invalid field: ${alias}`); NcError.invalidFilter(str);
} }
return new Filter({ return new Filter({
@ -6414,13 +6792,13 @@ export function getListArgs(
return obj; return obj;
} }
function extractIdsString( function extractIds(
childIds: (string | number | Record<string, any>)[], childIds: (string | number | Record<string, any>)[],
isBt = false, isBt = false,
) { ) {
return (isBt ? childIds.slice(0, 1) : childIds) return (isBt ? childIds.slice(0, 1) : childIds).map((r) =>
.map((r) => (typeof r === 'object' ? JSON.stringify(r) : r)) typeof r === 'object' ? JSON.stringify(r) : `${r}`,
.join(', '); );
} }
export { BaseModelSqlv2 }; 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 RollupColumn from '~/models/RollupColumn';
import type FormulaColumn from '~/models/FormulaColumn'; import type FormulaColumn from '~/models/FormulaColumn';
import { getColumnName } from '~/db/BaseModelSqlv2'; import { getColumnName } from '~/db/BaseModelSqlv2';
import { type BarcodeColumn, BaseUser, type QrCodeColumn } from '~/models';
import { NcError } from '~/helpers/catchError'; import { NcError } from '~/helpers/catchError';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2'; import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2'; import genRollupSelectv2 from '~/db/genRollupSelectv2';
@ -25,6 +24,7 @@ import Filter from '~/models/Filter';
import generateLookupSelectQuery from '~/db/generateLookupSelectQuery'; import generateLookupSelectQuery from '~/db/generateLookupSelectQuery';
import { getAliasGenerator } from '~/utils'; import { getAliasGenerator } from '~/utils';
import { getRefColumnIfAlias } from '~/helpers'; import { getRefColumnIfAlias } from '~/helpers';
import { type BarcodeColumn, BaseUser, type QrCodeColumn } from '~/models';
// tod: tobe fixed // tod: tobe fixed
// extend(customParseFormat); // extend(customParseFormat);
@ -167,7 +167,7 @@ const parseConditionV2 = async (
const column = await getRefColumnIfAlias(await filter.getColumn()); const column = await getRefColumnIfAlias(await filter.getColumn());
if (!column) { if (!column) {
if (throwErrorIfInvalid) { if (throwErrorIfInvalid) {
NcError.unprocessableEntity(`Invalid field: ${filter.fk_column_id}`); NcError.fieldNotFound(filter.fk_column_id);
} }
return; return;
} }
@ -180,7 +180,16 @@ const parseConditionV2 = async (
await childModel.getColumns(); await childModel.getColumns();
const parentModel = await parentColumn.getModel(); const parentModel = await parentColumn.getModel();
await parentModel.getColumns(); 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 ( if (
['blank', 'notblank', 'checked', 'notchecked'].includes( ['blank', 'notblank', 'checked', 'notchecked'].includes(
filter.comparison_op, filter.comparison_op,
@ -245,7 +254,7 @@ const parseConditionV2 = async (
qbP.whereNotIn(parentColumn.column_name, selectQb); qbP.whereNotIn(parentColumn.column_name, selectQb);
else qbP.whereIn(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 ( if (
['blank', 'notblank', 'checked', 'notchecked'].includes( ['blank', 'notblank', 'checked', 'notchecked'].includes(
filter.comparison_op, filter.comparison_op,
@ -315,7 +324,7 @@ const parseConditionV2 = async (
); );
} else qbP.whereIn(childColumn.column_name, selectQb); } 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 mmModel = await colOptions.getMMModel();
const mmParentColumn = await colOptions.getMMParentColumn(); const mmParentColumn = await colOptions.getMMParentColumn();
const mmChildColumn = await colOptions.getMMChildColumn(); const mmChildColumn = await colOptions.getMMChildColumn();
@ -1247,7 +1256,15 @@ async function generateLookupCondition(
const parentModel = await parentColumn.getModel(); const parentModel = await parentColumn.getModel();
await parentModel.getColumns(); 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( qb = knex(
knex.raw(`?? as ??`, [ knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(childModel.table_name), baseModelSqlv2.getTnPath(childModel.table_name),
@ -1278,7 +1295,7 @@ async function generateLookupCondition(
qbP.whereNotIn(parentColumn.column_name, qb); qbP.whereNotIn(parentColumn.column_name, qb);
else qbP.whereIn(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( qb = knex(
knex.raw(`?? as ??`, [ knex.raw(`?? as ??`, [
baseModelSqlv2.getTnPath(parentModel.table_name), baseModelSqlv2.getTnPath(parentModel.table_name),
@ -1312,7 +1329,7 @@ async function generateLookupCondition(
); );
else qbP.whereIn(childColumn.column_name, qb); 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 mmModel = await relationColumnOptions.getMMModel();
const mmParentColumn = await relationColumnOptions.getMMParentColumn(); const mmParentColumn = await relationColumnOptions.getMMParentColumn();
const mmChildColumn = await relationColumnOptions.getMMChildColumn(); const mmChildColumn = await relationColumnOptions.getMMChildColumn();

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

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

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

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

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

@ -33,7 +33,7 @@ export default async function sortV2(
const column = await getRefColumnIfAlias(await sort.getColumn()); const column = await getRefColumnIfAlias(await sort.getColumn());
if (!column) { if (!column) {
if (throwErrorIfInvalid) { if (throwErrorIfInvalid) {
NcError.unprocessableEntity(`Invalid field: ${sort.fk_column_id}`); NcError.fieldNotFound(sort.fk_column_id);
} }
continue; 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.un ? ' UNSIGNED' : '';
query += n.rqd ? ' NOT NULL' : ' NULL'; query += n.rqd ? ' NOT NULL' : ' NULL';
query += n.ai ? ' auto_increment' : ''; query += n.ai ? ' auto_increment' : '';
query += n.unique ? ` UNIQUE` : '';
const defaultValue = this.sanitiseDefaultValue(n.cdf); const defaultValue = this.sanitiseDefaultValue(n.cdf);
query += defaultValue 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 += n.rqd ? ' NOT NULL' : ' NULL';
query += defaultValue ? ` DEFAULT ${defaultValue}` : ''; query += defaultValue ? ` DEFAULT ${defaultValue}` : '';
query += n.unique ? ` UNIQUE` : '';
} }
} else if (change === 1) { } else if (change === 1) {
query += this.genQuery( query += this.genQuery(
@ -2872,6 +2873,7 @@ class PGClient extends KnexClient {
); );
query += n.rqd ? ' NOT NULL' : ' NULL'; query += n.rqd ? ' NOT NULL' : ' NULL';
query += defaultValue ? ` DEFAULT ${defaultValue}` : ''; query += defaultValue ? ` DEFAULT ${defaultValue}` : '';
query += n.unique ? ` UNIQUE` : '';
query = this.genQuery(`ALTER TABLE ?? ${query};`, [t], shouldSanitize); query = this.genQuery(`ALTER TABLE ?? ${query};`, [t], shouldSanitize);
} else { } else {
if (n.cn !== o.cn) { 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 ''`; : ` DEFAULT ''`;
addNewColumnQuery += n.rqd ? ` NOT NULL` : ' '; addNewColumnQuery += n.rqd ? ` NOT NULL` : ' ';
query += n.unique ? ` UNIQUE` : '';
addNewColumnQuery = this.genQuery( addNewColumnQuery = this.genQuery(
`ALTER TABLE ?? ${addNewColumnQuery};`, `ALTER TABLE ?? ${addNewColumnQuery};`,
[t], [t],
@ -2161,6 +2162,8 @@ class SqliteClient extends KnexClient {
query += n.dtxp && n.dt !== 'text' ? `(${this.genRaw(n.dtxp)})` : ''; query += n.dtxp && n.dt !== 'text' ? `(${this.genRaw(n.dtxp)})` : '';
query += n.cdf ? ` DEFAULT ${this.genValue(n.cdf)}` : ' '; query += n.cdf ? ` DEFAULT ${this.genValue(n.cdf)}` : ' ';
query += n.rqd ? ` NOT NULL` : ' '; query += n.rqd ? ` NOT NULL` : ' ';
// todo: unique constraint should be added using index
// query += n.unique ? ` UNIQUE` : '';
} else if (change === 1) { } else if (change === 1) {
shouldSanitize = true; shouldSanitize = true;
query += this.genQuery( query += this.genQuery(
@ -2175,6 +2178,8 @@ class SqliteClient extends KnexClient {
? ' ' ? ' '
: ` DEFAULT ''`; : ` DEFAULT ''`;
query += n.rqd ? ` NOT NULL` : ' '; 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); query = this.genQuery(`ALTER TABLE ?? ${query};`, [t], shouldSanitize);
} else { } else {
// if(n.cn!==o.cno) { // if(n.cn!==o.cno) {

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

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

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

@ -249,7 +249,7 @@ class NcPluginMgrv2 {
await tempPlugin.init(args?.input); await tempPlugin.init(args?.input);
if (!tempPlugin?.getAdapter()?.test) if (!tempPlugin?.getAdapter()?.test)
NcError.notImplemented('Plugin test is not implemented'); NcError.notImplemented('Plugin Test');
return tempPlugin?.getAdapter()?.test?.(); return tempPlugin?.getAdapter()?.test?.();
} }
@ -263,7 +263,7 @@ class NcPluginMgrv2 {
await tempPlugin.init(args?.input); await tempPlugin.init(args?.input);
if (!tempPlugin?.getAdapter()?.test) if (!tempPlugin?.getAdapter()?.test)
NcError.notImplemented('Plugin test is not implemented'); NcError.notImplemented('Plugin Test');
return tempPlugin?.getAdapter()?.test?.(); return tempPlugin?.getAdapter()?.test?.();
} }
@ -276,7 +276,7 @@ class NcPluginMgrv2 {
await tempPlugin.init(args?.input); await tempPlugin.init(args?.input);
if (!tempPlugin?.getAdapter()?.test) if (!tempPlugin?.getAdapter()?.test)
NcError.notImplemented('Plugin test is not implemented'); NcError.notImplemented('Plugin Test');
return tempPlugin?.getAdapter()?.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 (additionalProps) Object.assign(this, additionalProps);
if (offset && offset >= +count) { 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 { NextFunction, Request, Response } from 'express';
import type { ErrorObject } from 'ajv'; import type { ErrorObject } from 'ajv';
import { defaultLimitConfig } from '~/helpers/extractLimitAndOffset';
export enum DBError { export enum DBError {
TABLE_EXIST = 'TABLE_EXIST', TABLE_EXIST = 'TABLE_EXIST',
@ -372,6 +374,12 @@ export function extractDBError(error): {
case 'EHOSTDOWN': case 'EHOSTDOWN':
message = 'The host is down.'; message = 'The host is down.';
break; break;
default:
// if error message contains -- then extract message after --
if (error.message && error.message.includes('--')) {
message = error.message.split('--')[1];
}
break;
} }
if (message) { if (message) {
@ -400,7 +408,6 @@ export default function (
e instanceof Unauthorized || e instanceof Unauthorized ||
e instanceof Forbidden || e instanceof Forbidden ||
e instanceof NotFound || e instanceof NotFound ||
e instanceof NotImplemented ||
e instanceof UnprocessableEntity e instanceof UnprocessableEntity
) )
) )
@ -409,7 +416,15 @@ export default function (
const dbError = extractDBError(e); const dbError = extractDBError(e);
if (dbError) { 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) { if (e instanceof BadRequest) {
@ -420,16 +435,16 @@ export default function (
return res.status(403).json({ msg: e.message }); return res.status(403).json({ msg: e.message });
} else if (e instanceof NotFound) { } else if (e instanceof NotFound) {
return res.status(404).json({ msg: e.message }); 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) { } else if (e instanceof AjvError) {
return res.status(400).json({ msg: e.message, errors: e.errors }); return res.status(400).json({ msg: e.message, errors: e.errors });
} else if (e instanceof UnprocessableEntity) { } else if (e instanceof UnprocessableEntity) {
return res.status(422).json({ msg: e.message }); return res.status(422).json({ msg: e.message });
} else if (e instanceof NotAllowed) { } else if (e instanceof NotAllowed) {
return res.status(405).json({ msg: e.message }); 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 // if some other error occurs then send 500 and a generic message
res.status(500).json({ msg: 'Internal server error' }); res.status(500).json({ msg: 'Internal server error' });
@ -453,10 +468,6 @@ export class Forbidden extends NcBaseError {}
export class NotFound extends NcBaseError {} export class NotFound extends NcBaseError {}
export class InternalServerError extends NcBaseError {}
export class NotImplemented extends NcBaseError {}
export class UnprocessableEntity extends NcBaseError {} export class UnprocessableEntity extends NcBaseError {}
export class AjvError extends NcBaseError { export class AjvError extends NcBaseError {
@ -468,7 +479,266 @@ export class AjvError extends NcBaseError {
errors: ErrorObject[]; 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 { 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') { static notFound(message = 'Not found') {
throw new NotFound(message); throw new NotFound(message);
} }
@ -485,14 +755,6 @@ export class NcError {
throw new Forbidden(message); 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[] }) { static ajvValidationError(param: { message: string; errors: ErrorObject[] }) {
throw new AjvError(param); throw new AjvError(param);
} }

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

Loading…
Cancel
Save