Browse Source

Merge branch 'develop' into mvp-mobile-layout-and-code-scanner

pull/5114/head
Daniel Spaude 1 year ago
parent
commit
574fc0497a
No known key found for this signature in database
GPG Key ID: 654A3D1FA4F35FFE
  1. 4
      charts/nocodb/templates/pvc.yaml
  2. 1
      packages/nc-gui/components.d.ts
  3. 2
      packages/nc-gui/components/cell/Checkbox.vue
  4. 2
      packages/nc-gui/components/cell/ClampedText.vue
  5. 1
      packages/nc-gui/components/cell/Currency.vue
  6. 3
      packages/nc-gui/components/cell/MultiSelect.vue
  7. 4
      packages/nc-gui/components/cell/Text.vue
  8. 5
      packages/nc-gui/components/cell/TextArea.vue
  9. 19
      packages/nc-gui/components/dlg/KeyboardShortcuts.vue
  10. 57
      packages/nc-gui/components/general/ShortcutLabel.vue
  11. 10
      packages/nc-gui/components/smartsheet/Cell.vue
  12. 7
      packages/nc-gui/components/smartsheet/Grid.vue
  13. 83
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  14. 142
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  15. 103
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  16. 9
      packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
  17. 26
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  18. 7
      packages/nc-gui/components/webhook/Drawer.vue
  19. 3
      packages/nc-gui/components/webhook/Editor.vue
  20. 7
      packages/nc-gui/composables/useExpandedFormStore.ts
  21. 6
      packages/nc-gui/composables/useSelectedCellKeyupListener/index.ts
  22. 16
      packages/nc-gui/composables/useViewData.ts
  23. 20
      packages/nc-gui/composables/useViewFilters.ts
  24. 1
      packages/nc-gui/lang/ar.json
  25. 1
      packages/nc-gui/lang/bn_IN.json
  26. 1
      packages/nc-gui/lang/cs.json
  27. 119
      packages/nc-gui/lang/da.json
  28. 3
      packages/nc-gui/lang/de.json
  29. 1
      packages/nc-gui/lang/en.json
  30. 1
      packages/nc-gui/lang/es.json
  31. 1
      packages/nc-gui/lang/eu.json
  32. 1
      packages/nc-gui/lang/fa.json
  33. 1
      packages/nc-gui/lang/fi.json
  34. 1
      packages/nc-gui/lang/fr.json
  35. 1
      packages/nc-gui/lang/he.json
  36. 1
      packages/nc-gui/lang/hi.json
  37. 1
      packages/nc-gui/lang/hr.json
  38. 1
      packages/nc-gui/lang/id.json
  39. 1
      packages/nc-gui/lang/it.json
  40. 1
      packages/nc-gui/lang/ja.json
  41. 1
      packages/nc-gui/lang/ko.json
  42. 1
      packages/nc-gui/lang/lv.json
  43. 1
      packages/nc-gui/lang/nl.json
  44. 1
      packages/nc-gui/lang/no.json
  45. 1
      packages/nc-gui/lang/pl.json
  46. 1
      packages/nc-gui/lang/pt.json
  47. 1
      packages/nc-gui/lang/pt_BR.json
  48. 1
      packages/nc-gui/lang/ru.json
  49. 1
      packages/nc-gui/lang/sk.json
  50. 1
      packages/nc-gui/lang/sl.json
  51. 1
      packages/nc-gui/lang/sv.json
  52. 1
      packages/nc-gui/lang/th.json
  53. 1
      packages/nc-gui/lang/tr.json
  54. 1
      packages/nc-gui/lang/uk.json
  55. 1
      packages/nc-gui/lang/vi.json
  56. 17
      packages/nc-gui/lang/zh-Hans.json
  57. 1
      packages/nc-gui/lang/zh-Hant.json
  58. 230
      packages/nc-gui/utils/filterUtils.ts
  59. 4091
      packages/nc-plugin/package-lock.json
  60. 51
      packages/noco-docs/content/en/developer-resources/rest-apis.md
  61. 8
      packages/noco-docs/content/en/setup-and-usages/table-operations.md
  62. 4391
      packages/nocodb-sdk/src/lib/Api.ts
  63. 2
      packages/nocodb-sdk/src/lib/globals.ts
  64. 6
      packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts
  65. 11
      packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts
  66. 71
      packages/nocodb/.eslintrc.json
  67. 41
      packages/nocodb/package-lock.json
  68. 10
      packages/nocodb/package.json
  69. 6
      packages/nocodb/src/interface/config.ts
  70. 32
      packages/nocodb/src/lib/Noco.ts
  71. 4
      packages/nocodb/src/lib/cache/NocoCache.ts
  72. 2
      packages/nocodb/src/lib/cache/RedisCacheMgr.ts
  73. 2
      packages/nocodb/src/lib/cache/RedisMockCacheMgr.ts
  74. 36
      packages/nocodb/src/lib/controllers/apiDocs/index.ts
  75. 0
      packages/nocodb/src/lib/controllers/apiDocs/redocHtml.ts
  76. 0
      packages/nocodb/src/lib/controllers/apiDocs/swaggerHtml.ts
  77. 50
      packages/nocodb/src/lib/controllers/apiToken.ctl.ts
  78. 128
      packages/nocodb/src/lib/controllers/attachment.ctl.ts
  79. 73
      packages/nocodb/src/lib/controllers/audit.ctl.ts
  80. 91
      packages/nocodb/src/lib/controllers/base.ctl.ts
  81. 8
      packages/nocodb/src/lib/controllers/cache.ctl.ts
  82. 78
      packages/nocodb/src/lib/controllers/column.ctl.ts
  83. 93
      packages/nocodb/src/lib/controllers/dbData/bulkDataAlias.ctl.ts
  84. 193
      packages/nocodb/src/lib/controllers/dbData/data.ctl.ts
  85. 260
      packages/nocodb/src/lib/controllers/dbData/dataAlias.ctl.ts
  86. 16
      packages/nocodb/src/lib/controllers/dbData/dataAliasExport.ctl.ts
  87. 138
      packages/nocodb/src/lib/controllers/dbData/dataAliasNested.ctl.ts
  88. 270
      packages/nocodb/src/lib/controllers/dbData/helpers.ts
  89. 15
      packages/nocodb/src/lib/controllers/dbData/index.ts
  90. 33
      packages/nocodb/src/lib/controllers/dbData/oldData.ctl.ts
  91. 9
      packages/nocodb/src/lib/controllers/export.ctl.ts
  92. 116
      packages/nocodb/src/lib/controllers/filter.ctl.ts
  93. 99
      packages/nocodb/src/lib/controllers/hook.ctl.ts
  94. 91
      packages/nocodb/src/lib/controllers/hookFilter.ctl.ts
  95. 54
      packages/nocodb/src/lib/controllers/metaDiff.ctl.ts
  96. 34
      packages/nocodb/src/lib/controllers/modelVisibility.ctl.ts
  97. 17
      packages/nocodb/src/lib/controllers/orgLicense.ctl.ts
  98. 64
      packages/nocodb/src/lib/controllers/orgToken.ctl.ts
  99. 154
      packages/nocodb/src/lib/controllers/orgUser.ctl.ts
  100. 37
      packages/nocodb/src/lib/controllers/plugin.ctl.ts
  101. Some files were not shown because too many files have changed in this diff Show More

4
charts/nocodb/templates/pvc.yaml

@ -5,10 +5,10 @@ metadata:
labels:
{{- include "nocodb.selectorLabels" . | nindent 8 }}
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: {{ .Values.storage.size }}
storageClassName: {{ .Values.storage.storageClassName }}
accessModes:
{{- default (toYaml .Values.storage.accessModes) "- ReadWriteMany" | nindent 4 }}
volumeMode: Filesystem

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

@ -89,6 +89,7 @@ declare module '@vue/runtime-core' {
IcTwotoneWidthNormal: typeof import('~icons/ic/twotone-width-normal')['default']
LogosGoogleGmail: typeof import('~icons/logos/google-gmail')['default']
LogosMysqlIcon: typeof import('~icons/logos/mysql-icon')['default']
LogosOracle: typeof import('~icons/logos/oracle')['default']
LogosPostgresql: typeof import('~icons/logos/postgresql')['default']
LogosRedditIcon: typeof import('~icons/logos/reddit-icon')['default']
LogosSnowflakeIcon: typeof import('~icons/logos/snowflake-icon')['default']

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

@ -89,7 +89,7 @@ useSelectedCellKeyupListener(active, (e) => {
<style scoped lang="scss">
.nc-cell-hover-show {
opacity: 0;
opacity: 0.3;
transition: 0.3s opacity;
&:hover {

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

@ -29,7 +29,7 @@ onMounted(() => {
-->
<text-clamp
:key="`clamp-${key}-${props.value?.toString().length || 0}`"
class="w-full h-full break-all"
class="w-full h-full break-word"
:text="`${props.value || ' '}`"
:max-lines="props.lines"
/>

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

@ -81,6 +81,7 @@ onMounted(() => {
@keydown.delete.stop
@selectstart.capture.stop
@mousedown.stop
@contextmenu.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span>

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

@ -32,6 +32,7 @@ interface Props {
modelValue?: string | string[]
rowIndex?: number
disableOptionCreation?: boolean
location?: 'cell' | 'filter'
}
const { modelValue, disableOptionCreation } = defineProps<Props>()
@ -336,7 +337,7 @@ useEventListener(document, 'click', handleClose, true)
v-for="op of options"
:key="op.id || op.title"
:value="op.title"
:data-testid="`select-option-${column.title}-${rowIndex}`"
:data-testid="`select-option-${column.title}-${location === 'filter' ? 'filter' : rowIndex}`"
:class="`nc-select-option-${column.title}-${op.title}`"
@click.stop
>

4
packages/nc-gui/components/cell/Text.vue

@ -14,6 +14,8 @@ const { showNull } = useGlobal()
const editEnabled = inject(EditModeInj)
const rowHeight = inject(RowHeightInj)
const readonly = inject(ReadonlyInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits)
@ -42,5 +44,5 @@ const focus: VNodeRef = (el) => {
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span>
<LazyCellClampedText v-else :value="vModel" :lines="1" />
<LazyCellClampedText v-else :value="vModel" :lines="rowHeight" />
</template>

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

@ -45,3 +45,8 @@ const focus: VNodeRef = (el) => (el as HTMLTextAreaElement)?.focus()
<span v-else>{{ vModel }}</span>
</template>
<style>
textarea:focus {
box-shadow: none;
}
</style>

19
packages/nc-gui/components/dlg/KeyboardShortcuts.vue

@ -15,6 +15,9 @@ const dialogShow = computed({
const renderCmdOrCtrlKey = () => {
return isMac() ? '⌘' : 'CTRL'
}
const renderAltOrOptlKey = () => {
return isMac() ? '⌥' : 'ALT'
}
const shortcutList = [
{
@ -197,6 +200,22 @@ const shortcutList = [
keys: [renderCmdOrCtrlKey(), 'Enter'],
behaviour: 'Save current expanded form item',
},
{
keys: [renderAltOrOptlKey(), '→'],
behaviour: 'Switch to next row',
},
{
keys: [renderAltOrOptlKey(), '←'],
behaviour: 'Switch to previous row',
},
{
keys: [renderAltOrOptlKey(), 'S'],
behaviour: 'Save current expanded form item',
},
{
keys: [renderAltOrOptlKey(), 'N'],
behaviour: 'Create a new row',
},
],
},
]

57
packages/nc-gui/components/general/ShortcutLabel.vue

@ -0,0 +1,57 @@
<script lang="ts" setup>
import { isMac } from '#imports'
const props = defineProps<{
keys: string[]
}>()
const isMacOs = isMac()
const getLabel = (key: string) => {
if (isMacOs) {
switch (key.toLowerCase()) {
case 'alt':
return '⌥'
case 'shift':
return '⇧'
case 'meta':
return '⌘'
case 'control':
case 'ctrl':
return '⌃'
case 'enter':
return '↩'
}
}
switch (key.toLowerCase()) {
case 'arrowup':
return '↑'
case 'arrowdown':
return '↓'
case 'arrowleft':
return '←'
case 'arrowright':
return '→'
}
return key
}
</script>
<template>
<div class="nc-shortcut-label-wrapper">
<div v-for="(key, index) in props.keys" :key="index" class="nc-shortcut-label">
<span>{{ getLabel(key) }}</span>
</div>
</div>
</template>
<style scoped>
.nc-shortcut-label-wrapper {
@apply flex gap-1;
}
.nc-shortcut-label {
@apply text-[0.7rem] leading-6 min-w-5 min-h-5 text-center relative z-0 after:(content-[''] left-0 top-0 -z-1 bg-current opacity-10 absolute w-full h-full rounded) px-1;
}
</style>

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

@ -139,6 +139,15 @@ const isNumericField = computed(() => {
isDuration(column.value)
)
})
// disable contexxtmenu event propagation when cell is in
// editable state and typable (e.g. text area)
// this is to prevent the custom grid view context menu from opening
const onContextmenu = (e: MouseEvent) => {
if (props.editEnabled && isTypableInputColumn(column.value)) {
e.stopPropagation()
}
}
</script>
<template>
@ -151,6 +160,7 @@ const isNumericField = computed(() => {
]"
@keydown.enter.exact="syncAndNavigate(NavigateDir.NEXT, $event)"
@keydown.shift.enter.exact="syncAndNavigate(NavigateDir.PREV, $event)"
@contextmenu="onContextmenu"
>
<template v-if="column">
<LazyCellTextArea v-if="isTextArea(column)" v-model="vModel" />

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

@ -118,6 +118,7 @@ const {
selectedAllRecords,
removeRowIfNew,
navigateToSiblingRow,
getExpandedRowIndex,
} = useViewData(meta, view, xWhere)
const { getMeta } = useMetas()
@ -980,6 +981,8 @@ const closeAddColumnDropdown = () => {
:row-id="routeQuery.rowId"
:view="view"
show-next-prev-icons
:first-row="getExpandedRowIndex() === 0"
:last-row="getExpandedRowIndex() === data.length - 1"
@next="navigateToSiblingRow(NavigateDir.NEXT)"
@prev="navigateToSiblingRow(NavigateDir.PREV)"
/>
@ -1056,7 +1059,7 @@ const closeAddColumnDropdown = () => {
position: sticky !important;
left: 80px;
z-index: 5;
@apply border-r-1 border-r-gray-300;
@apply border-r-2 border-r-gray-300;
}
tbody td:nth-child(2) {
@ -1064,7 +1067,7 @@ const closeAddColumnDropdown = () => {
left: 80px;
z-index: 4;
background: white;
@apply shadow-lg border-r-1 border-r-gray-300;
@apply border-r-2 border-r-gray-300;
}
}

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

@ -18,7 +18,7 @@ const route = useRoute()
const { meta, isSqlView } = useSmartsheetStoreOrThrow()
const { commentsDrawer, displayValue, primaryKey, save: _save, loadRow } = useExpandedFormStoreOrThrow()
const { commentsDrawer, displayValue, primaryKey, save: _save, loadRow, saveRowAndStay } = useExpandedFormStoreOrThrow()
const { isNew, syncLTARRefs, state } = useSmartsheetRowStoreOrThrow()
@ -26,8 +26,6 @@ const { isUIAllowed } = useUIPermission()
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const saveRowAndStay = ref(0)
const save = async () => {
if (isNew.value) {
const data = await _save(state.value)
@ -103,17 +101,6 @@ const onConfirmDeleteRowClick = async () => {
</h5>
<div class="flex-1" />
<a-tooltip placement="bottom">
<template #title>
<div class="text-center w-full">{{ $t('general.reload') }}</div>
</template>
<mdi-reload
v-if="!isNew"
class="nc-icon-transition cursor-pointer select-none text-gray-500 mx-1 min-w-4"
@click="loadRow"
/>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title>
<!-- todo: i18n -->
@ -139,32 +126,6 @@ const onConfirmDeleteRowClick = async () => {
/>
</a-tooltip>
<a-tooltip v-if="!isSqlView" placement="bottom">
<!-- Duplicate row -->
<template #title>
<div class="text-center w-full">{{ $t('activity.duplicateRow') }}</div>
</template>
<MdiContentCopy
v-if="isUIAllowed('xcDatatableEditable') && !isNew"
v-e="['c:row-expand:duplicate']"
class="nc-icon-transition cursor-pointer select-none nc-duplicate-row text-gray-500 mx-1 min-w-4"
@click="!isNew && emit('duplicateRow')"
/>
</a-tooltip>
<a-tooltip v-if="!isSqlView" placement="bottom">
<!-- Delete row -->
<template #title>
<div class="text-center w-full">{{ $t('activity.deleteRow') }}</div>
</template>
<MdiDeleteOutline
v-if="isUIAllowed('xcDatatableEditable') && !isNew"
v-e="['c:row-expand:delete']"
class="nc-icon-transition cursor-pointer select-none nc-delete-row text-gray-500 mx-1 min-w-4"
@click="!isNew && onDeleteRowClick()"
/>
</a-tooltip>
<a-dropdown-button class="nc-expand-form-save-btn" type="primary" :disabled="!isUIAllowed('tableRowUpdate')" @click="save">
<template #icon><MdiMenuDown /></template>
@ -194,17 +155,39 @@ const onConfirmDeleteRowClick = async () => {
</div>
</a-dropdown-button>
<a-tooltip placement="bottom">
<!-- Close -->
<template #title>
<div class="text-center w-full">{{ $t('general.close') }}</div>
<a-dropdown>
<MdiDotsVertical class="nc-icon-transition" />
<template #overlay>
<a-menu>
<a-menu-item v-if="!isNew" @click="loadRow">
<div v-e="['c:row-expand:reload']" class="py-2 flex gap-2 items-center">
<mdi-reload class="nc-icon-transition cursor-pointer select-none text-gray-500 mx-1 min-w-4" />
{{ $t('general.reload') }}
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('xcDatatableEditable') && !isNew" @click="!isNew && emit('duplicateRow')">
<div v-e="['c:row-expand:duplicate']" class="py-2 flex gap-2 a">
<MdiContentCopy class="nc-icon-transition cursor-pointer select-none nc-duplicate-row text-gray-500 mx-1 min-w-4" />
{{ $t('activity.duplicateRow') }}
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('xcDatatableEditable') && !isNew" @click="!isNew && onDeleteRowClick()">
<div v-e="['c:row-expand:delete']" class="py-2 flex gap-2 items-center">
<MdiDeleteOutline class="nc-icon-transition cursor-pointer select-none nc-delete-row text-gray-500 mx-1 min-w-4" />
{{ $t('activity.deleteRow') }}
</div>
</a-menu-item>
<a-menu-item @click="emit('cancel')">
<div v-e="['c:row-expand:delete']" class="py-2 flex gap-2 items-center">
<MdiCloseCircleOutline
class="nc-icon-transition cursor-pointer select-none nc-delete-row text-gray-500 mx-1 min-w-4"
/>
{{ $t('general.close') }}
</div>
</a-menu-item>
</a-menu>
</template>
<MdiCloseCircleOutline
class="nc-icon-transition cursor-pointer select-none nc-close-form text-gray-500 mx-1 min-w-4"
@click="emit('cancel')"
/>
</a-tooltip>
</a-dropdown>
<a-modal v-model:visible="showDeleteRowModal" title="Delete row?" @ok="onConfirmDeleteRowClick">
<p>Are you sure you want to delete this row?</p>
</a-modal>

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

@ -22,6 +22,7 @@ import {
useVModel,
watch,
} from '#imports'
import { useActiveKeyupListener } from '~/composables/useSelectedCellKeyupListener'
import type { Row } from '~/lib'
interface Props {
@ -34,12 +35,16 @@ interface Props {
rowId?: string
view?: ViewType
showNextPrevIcons?: boolean
firstRow?: boolean
lastRow?: boolean
}
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev'])
const key = ref(0)
const { t } = useI18n()
const row = ref(props.row)
@ -64,7 +69,16 @@ const isKanban = inject(IsKanbanInj, ref(false))
provide(MetaInj, meta)
const { commentsDrawer, changedColumns, state: rowState, isNew, loadRow } = useProvideExpandedFormStore(meta, row)
const {
commentsDrawer,
changedColumns,
state: rowState,
isNew,
loadRow,
saveRowAndStay,
syncLTARRefs,
save,
} = useProvideExpandedFormStore(meta, row)
const duplicatingRowInProgress = ref(false)
@ -126,6 +140,25 @@ const onDuplicateRow = () => {
}, 500)
}
const onNext = async () => {
if (changedColumns.value.size > 0) {
await Modal.confirm({
title: 'Do you want to save the changes?',
okText: 'Save',
cancelText: 'Discard',
onOk: async () => {
await save()
emits('next')
},
onCancel: () => {
emits('next')
},
})
} else {
emits('next')
}
}
const reloadParentRowHook = inject(ReloadRowDataHookInj, createEventHook())
// override reload trigger and use it to reload grid and the form itself
@ -152,6 +185,92 @@ const cellWrapperEl = ref<HTMLElement>()
onMounted(() => {
setTimeout(() => (cellWrapperEl.value?.querySelector('input,select,textarea') as HTMLInputElement)?.focus())
})
const addNewRow = () => {
setTimeout(async () => {
row.value = {
row: {},
oldRow: {},
rowMeta: { new: true },
}
rowState.value = {}
key.value++
isExpanded.value = true
}, 500)
}
// attach keyboard listeners to switch between rows
// using alt + left/right arrow keys
useActiveKeyupListener(
isExpanded,
async (e: KeyboardEvent) => {
if (!e.altKey) return
if (e.key === 'ArrowLeft') {
e.stopPropagation()
emits('prev')
} else if (e.key === 'ArrowRight') {
e.stopPropagation()
onNext()
}
// on alt + s save record
else if (e.code === 'KeyS') {
// remove focus from the active input if any
document.activeElement?.blur()
e.stopPropagation()
if (isNew.value) {
const data = await save(rowState.value)
await syncLTARRefs(data)
reloadHook?.trigger(null)
} else {
await save()
reloadHook?.trigger(null)
}
if (!saveRowAndStay.value) {
onClose()
}
// on alt + n create new record
} else if (e.code === 'KeyN') {
// remove focus from the active input if any to avoid unwanted input
;(document.activeElement as HTMLInputElement)?.blur?.()
if (changedColumns.value.size > 0) {
await Modal.confirm({
title: 'Do you want to save the changes?',
okText: 'Save',
cancelText: 'Discard',
onOk: async () => {
await save()
reloadHook?.trigger(null)
addNewRow()
},
onCancel: () => {
addNewRow()
},
})
} else if (isNew.value) {
await Modal.confirm({
title: 'Do you want to save the record?',
okText: 'Save',
cancelText: 'Discard',
onOk: async () => {
const data = await save(rowState.value)
await syncLTARRefs(data)
reloadHook?.trigger(null)
addNewRow()
},
onCancel: () => {
addNewRow()
},
})
} else {
addNewRow()
}
}
},
{ immediate: true },
)
</script>
<script lang="ts">
@ -172,21 +291,25 @@ export default {
>
<SmartsheetExpandedFormHeader :view="props.view" @cancel="onClose" @duplicate-row="onDuplicateRow" />
<div class="!bg-gray-100 rounded flex-1">
<div :key="key" class="!bg-gray-100 rounded flex-1">
<div class="flex h-full nc-form-wrapper items-stretch min-h-[max(70vh,100%)]">
<div class="flex-1 overflow-auto scrollbar-thin-dull nc-form-fields-container relative">
<template v-if="props.showNextPrevIcons">
<a-tooltip placement="bottom">
<a-tooltip v-if="!props.firstRow" placement="bottom">
<template #title>
{{ $t('labels.nextRow') }}
{{ $t('labels.prevRow') }}
<GeneralShortcutLabel class="justify-center" :keys="['Alt', '←']" />
</template>
<MdiChevronRight class="cursor-pointer nc-next-arrow" @click="$emit('next')" />
<MdiChevronLeft class="cursor-pointer nc-prev-arrow" @click="$emit('prev')" />
</a-tooltip>
<a-tooltip placement="bottom">
<a-tooltip v-if="!props.lastRow" placement="bottom">
<template #title>
{{ $t('labels.prevRow') }}
{{ $t('labels.nextRow') }}
<GeneralShortcutLabel class="justify-center" :keys="['Alt', '→']" />
</template>
<MdiChevronLeft class="cursor-pointer nc-prev-arrow" @click="$emit('prev')" />
<MdiChevronRight class="cursor-pointer nc-next-arrow" @click="onNext" />
</a-tooltip>
</template>
<div class="w-[500px] mx-auto">
@ -210,7 +333,8 @@ export default {
:ref="i ? null : (el) => (cellWrapperEl = el)"
class="!bg-white rounded px-1 min-h-[35px] flex items-center mt-2 relative"
>
<LazySmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" />
<LazySmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row"
:column="col" />
<LazySmartsheetCell
v-else

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

@ -6,6 +6,7 @@ import {
MetaInj,
ReloadViewDataHookInj,
comparisonOpList,
comparisonSubOpList,
computed,
inject,
ref,
@ -54,6 +55,7 @@ const {
sync,
saveOrUpdateDebounced,
isComparisonOpAllowed,
isComparisonSubOpAllowed,
} = useViewFilters(
activeView,
parentId,
@ -75,24 +77,43 @@ const filterPrevComparisonOp = ref<Record<string, string>>({})
const filterUpdateCondition = (filter: FilterType, i: number) => {
const col = getColumn(filter)
if (!col) return
if (
col.uidt === UITypes.SingleSelect &&
['anyof', 'nanyof'].includes(filterPrevComparisonOp.value[filter.id]) &&
['anyof', 'nanyof'].includes(filterPrevComparisonOp.value[filter.id!]) &&
['eq', 'neq'].includes(filter.comparison_op!)
) {
// anyof and nanyof can allow multiple selections,
// while `eq` and `neq` only allow one selection
filter.value = ''
filter.value = null
} else if (['blank', 'notblank', 'empty', 'notempty', 'null', 'notnull'].includes(filter.comparison_op!)) {
// since `blank`, `empty`, `null` doesn't require value,
// hence remove the previous value
filter.value = ''
filter.value = null
filter.comparison_sub_op = null
} else if ([UITypes.Date, UITypes.DateTime].includes(col.uidt as UITypes)) {
// for date / datetime,
// the input type could be decimal or datepicker / datetime picker
// hence remove the previous value
filter.value = null
if (
!comparisonSubOpList(filter.comparison_op!)
.map((op) => op.value)
.includes(filter.comparison_sub_op!)
) {
if (filter.comparison_op === 'isWithin') {
filter.comparison_sub_op = 'pastNumberOfDays'
} else {
filter.comparison_sub_op = 'exactDate'
}
}
}
saveOrUpdate(filter, i)
filterPrevComparisonOp.value[filter.id] = filter.comparison_op
$e('a:filter:update', {
logical: filter.logical_op,
comparison: filter.comparison_op,
comparison_sub_op: filter.comparison_sub_op,
})
}
@ -109,7 +130,7 @@ const types = computed(() => {
watch(
() => activeView.value?.id,
(n: string, o: string) => {
(n, o) => {
// if nested no need to reload since it will get reloaded from parent
if (!nested && n !== o && (hookId || !webHook)) loadFilters(hookId as string)
},
@ -137,16 +158,30 @@ const applyChanges = async (hookId?: string, _nested = false) => {
}
const selectFilterField = (filter: Filter, index: number) => {
const col = getColumn(filter)
if (!col) return
// when we change the field,
// the corresponding default filter operator needs to be changed as well
// since the existing one may not be supported for the new field
// e.g. `eq` operator is not supported in checkbox field
// hence, get the first option of the supported operators of the new field
filter.comparison_op = comparisonOpList(getColumn(filter)!.uidt as UITypes).filter((compOp) =>
filter.comparison_op = comparisonOpList(col.uidt as UITypes).filter((compOp) =>
isComparisonOpAllowed(filter, compOp),
)?.[0].value
if ([UITypes.Date, UITypes.DateTime].includes(col.uidt as UITypes) && !['blank', 'notblank'].includes(filter.comparison_op)) {
if (filter.comparison_op === 'isWithin') {
filter.comparison_sub_op = 'pastNumberOfDays'
} else {
filter.comparison_sub_op = 'exactDate'
}
} else {
// reset
filter.comparison_sub_op = null
}
// reset filter value as well
filter.value = ''
filter.value = null
saveOrUpdate(filter, index)
}
@ -163,10 +198,15 @@ defineExpose({
<template>
<div
class="p-4 menu-filter-dropdown bg-gray-50 !border mt-4"
:class="{ 'shadow min-w-[430px] max-h-[max(80vh,500px)] overflow-auto': !nested, 'border-1 w-full': nested }"
class="p-4 menu-filter-dropdown bg-gray-50 !border"
:class="{ 'min-w-[430px]': filters.length, 'shadow max-h-[max(80vh,500px)] overflow-auto': !nested, 'border-1 w-full': nested }"
>
<div v-if="filters && filters.length" class="nc-filter-grid mb-2" @click.stop>
<div
v-if="filters && filters.length"
class="nc-filter-grid mb-2"
:class="{ 'max-h-420px overflow-y-auto': !nested }"
@click.stop
>
<template v-for="(filter, i) in filters" :key="i">
<template v-if="filter.status !== 'delete'">
<template v-if="filter.is_group">
@ -195,7 +235,7 @@ defineExpose({
</a-select>
</div>
<span class="col-span-3" />
<div class="col-span-5">
<div class="col-span-6">
<LazySmartsheetToolbarColumnFilter
v-if="filter.id || filter.children"
:key="filter.id ?? i"
@ -261,24 +301,49 @@ defineExpose({
</template>
</a-select>
<span
<a-select
v-if="
filter.comparison_op &&
['null', 'notnull', 'checked', 'notchecked', 'empty', 'notempty', 'blank', 'notblank'].includes(
filter.comparison_op,
)
[UITypes.Date, UITypes.DateTime].includes(getColumn(filter)?.uidt) &&
!['blank', 'notblank'].includes(filter.comparison_op)
"
:key="`span${i}`"
/>
v-model:value="filter.comparison_sub_op"
:dropdown-match-select-width="false"
class="caption nc-filter-sub_operation-select"
:placeholder="$t('labels.operationSub')"
density="compact"
variant="solo"
:disabled="filter.readOnly"
hide-details
dropdown-class-name="nc-dropdown-filter-comp-sub-op"
@change="filterUpdateCondition(filter, i)"
>
<template v-for="compSubOp of comparisonSubOpList(filter.comparison_op)" :key="compSubOp.value">
<a-select-option v-if="isComparisonSubOpAllowed(filter, compSubOp)" :value="compSubOp.value">
{{ compSubOp.text }}
</a-select-option>
</template>
</a-select>
<span v-else />
<a-checkbox
v-else-if="filter.field && types[filter.field] === 'boolean'"
v-if="filter.field && types[filter.field] === 'boolean'"
v-model:checked="filter.value"
dense
:disabled="filter.readOnly"
@change="saveOrUpdate(filter, i)"
/>
<span
v-else-if="
filter.comparison_sub_op
? comparisonSubOpList(filter.comparison_op).find((op) => op.value === filter.comparison_sub_op)?.ignoreVal ??
false
: comparisonOpList(getColumn(filter)?.uidt).find((op) => op.value === filter.comparison_op)?.ignoreVal ?? false
"
:key="`span${i}`"
/>
<LazySmartsheetToolbarFilterInput
v-else
class="nc-filter-value-select min-w-[120px]"
@ -315,7 +380,7 @@ defineExpose({
<style scoped>
.nc-filter-grid {
grid-template-columns: auto auto auto auto auto;
grid-template-columns: auto auto auto auto auto auto;
@apply grid gap-[12px] items-center;
}

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

@ -117,9 +117,13 @@ const componentMap: Partial<Record<FilterType, any>> = $computed(() => {
// use MultiSelect for SingleSelect columns for anyof / nanyof filters
isSingleSelect: ['anyof', 'nanyof'].includes(props.filter.comparison_op!) ? MultiSelect : SingleSelect,
isMultiSelect: MultiSelect,
isDate: DatePicker,
isDate: ['daysAgo', 'daysFromNow', 'pastNumberOfDays', 'nextNumberOfDays'].includes(props.filter.comparison_sub_op!)
? Decimal
: DatePicker,
isYear: YearPicker,
isDateTime: DateTimePicker,
isDateTime: ['daysAgo', 'daysFromNow', 'pastNumberOfDays', 'nextNumberOfDays'].includes(props.filter.comparison_sub_op!)
? Decimal
: DateTimePicker,
isTime: TimePicker,
isRating: Rating,
isDuration: Duration,
@ -189,6 +193,7 @@ const hasExtraPadding = $computed(() => {
:column="column"
class="flex"
v-bind="componentProps"
location="filter"
/>
</div>
</template>

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

@ -1,4 +1,5 @@
<script setup lang="ts">
import { nextTick } from '@vue/runtime-core'
import type { ColumnType } from 'nocodb-sdk'
import {
ActiveViewInj,
@ -22,7 +23,17 @@ const reloadDataHook = inject(ReloadViewDataHookInj)
const { eventBus } = useSmartsheetStoreOrThrow()
const { sorts, saveOrUpdate, loadSorts, addSort, deleteSort } = useViewSorts(view, () => reloadDataHook?.trigger())
const { sorts, saveOrUpdate, loadSorts, addSort: _addSort, deleteSort } = useViewSorts(view, () => reloadDataHook?.trigger())
const removeIcon = ref<HTMLElement>()
const addSort = () => {
_addSort()
nextTick(() => {
console.log(removeIcon.value)
removeIcon.value?.[removeIcon.value?.length - 1]?.$el?.scrollIntoView()
})
}
const { isMobileMode } = useGlobal()
@ -77,12 +88,18 @@ useMenuCloseOnEsc(open)
</div>
<template #overlay>
<div
class="bg-gray-50 p-6 shadow-lg menu-filter-dropdown min-w-[400px] max-h-[max(80vh,500px)] overflow-auto !border"
:class="{ ' min-w-[400px]': sorts.length }"
class="bg-gray-50 p-6 shadow-lg menu-filter-dropdown max-h-[max(80vh,500px)] overflow-auto !border"
data-testid="nc-sorts-menu"
>
<div v-if="sorts?.length" class="sort-grid mb-2" @click.stop>
<div v-if="sorts?.length" class="sort-grid mb-2 max-h-420px overflow-y-auto" @click.stop>
<template v-for="(sort, i) of sorts" :key="i">
<MdiCloseBox class="nc-sort-item-remove-btn text-grey self-center" small @click.stop="deleteSort(sort, i)" />
<MdiCloseBox
ref="removeIcon"
class="nc-sort-item-remove-btn text-grey self-center"
small
@click.stop="deleteSort(sort, i)"
/>
<LazySmartsheetToolbarFieldListAutoCompleteDropdown
v-model="sort.fk_column_id"
@ -94,6 +111,7 @@ useMenuCloseOnEsc(open)
/>
<a-select
ref=""
v-model:value="sort.direction"
class="shrink grow-0 nc-sort-dir-select !text-xs"
:label="$t('labels.operation')"

7
packages/nc-gui/components/webhook/Drawer.vue

@ -19,6 +19,11 @@ async function editHook(hook: Record<string, any>) {
editOrAdd.value = true
currentHook.value = hook
}
async function addHook() {
editOrAdd.value = true
currentHook.value = undefined
}
</script>
<template>
@ -35,7 +40,7 @@ async function editHook(hook: Record<string, any>) {
<a-layout-content class="px-10 py-5 scrollbar-thin-primary">
<LazyWebhookEditor v-if="editOrAdd" :hook="currentHook" @back-to-list="editOrAdd = false" />
<LazyWebhookList v-else @edit="editHook" @add="editOrAdd = true" />
<LazyWebhookList v-else @edit="editHook" @add="addHook" />
</a-layout-content>
<a-layout-footer class="!bg-white border-t flex">

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

@ -225,7 +225,7 @@ function onNotTypeChange(reset = false) {
}
if (hook.notification.type === 'Slack') {
slackChannels.value = (apps.value && apps.value.Slack && apps.Slack.parsedInput) || []
slackChannels.value = (apps.value && apps.value.Slack && apps.value.Slack.parsedInput) || []
}
if (hook.notification.type === 'Microsoft Teams') {
@ -651,6 +651,7 @@ onMounted(loadPluginList)
</a-checkbox>
<LazySmartsheetToolbarColumnFilter
class="mt-4"
v-if="hook.condition"
ref="filterRef"
:auto-save="false"

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

@ -30,13 +30,15 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
const { t } = useI18n()
const commentsOnly = ref(false)
const commentsOnly = ref(true)
const commentsAndLogs = ref<any[]>([])
const comment = ref('')
const commentsDrawer = ref(false)
const commentsDrawer = ref(true)
const saveRowAndStay = ref(0)
const changedColumns = ref(new Set<string>())
@ -243,6 +245,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
changedColumns,
loadRow,
primaryKey,
saveRowAndStay,
}
}, 'expanded-form-store')

6
packages/nc-gui/composables/useSelectedCellKeyupListener/index.ts

@ -1,15 +1,15 @@
import { isClient } from '@vueuse/core'
import type { Ref } from 'vue'
import type { ComputedRef, Ref } from 'vue'
function useSelectedCellKeyupListener(
selected: Ref<boolean>,
selected: Ref<boolean | undefined> | ComputedRef<boolean | undefined>,
handler: (e: KeyboardEvent) => void,
{ immediate = false }: { immediate?: boolean } = {},
) {
if (isClient) {
watch(
selected,
(nextVal: boolean, _: boolean, cleanup) => {
(nextVal: boolean | undefined, _: boolean | undefined, cleanup) => {
// bind listener when `selected` is truthy
if (nextVal) {
document.addEventListener('keydown', handler, true)

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

@ -477,15 +477,24 @@ export function useViewData(
}
}
const navigateToSiblingRow = async (dir: NavigateDir) => {
// get current expanded row index
const expandedRowIndex = formattedData.value.findIndex(
// get current expanded row index
function getExpandedRowIndex() {
return formattedData.value.findIndex(
(row: Row) => routeQuery.rowId === extractPkFromRow(row.row, meta.value?.columns as ColumnType[]),
)
}
const navigateToSiblingRow = async (dir: NavigateDir) => {
const expandedRowIndex = getExpandedRowIndex()
// calculate next row index based on direction
let siblingRowIndex = expandedRowIndex + (dir === NavigateDir.NEXT ? 1 : -1)
// if unsaved row skip it
while (formattedData.value[siblingRowIndex]?.rowMeta?.new) {
siblingRowIndex = siblingRowIndex + (dir === NavigateDir.NEXT ? 1 : -1)
}
const currentPage = paginationData?.value?.page || 1
// if next row index is less than 0, go to previous page and point to last element
@ -547,5 +556,6 @@ export function useViewData(
removeLastEmptyRow,
removeRowIfNew,
navigateToSiblingRow,
getExpandedRowIndex,
}
}

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

@ -140,6 +140,25 @@ export function useViewFilters(
return isNullOrEmptyOp ? projectMeta.value.showNullAndEmptyInFilter : true
}
const isComparisonSubOpAllowed = (
filter: FilterType,
compOp: {
text: string
value: string
ignoreVal?: boolean
includedTypes?: UITypes[]
excludedTypes?: UITypes[]
},
) => {
if (compOp.includedTypes) {
// include allowed values only if selected column type matches
return filter.fk_column_id && compOp.includedTypes.includes(types.value[filter.fk_column_id])
} else if (compOp.excludedTypes) {
// include not allowed values only if selected column type not matches
return filter.fk_column_id && !compOp.excludedTypes.includes(types.value[filter.fk_column_id])
}
}
const placeholderFilter = (): Filter => {
return {
comparison_op: comparisonOpList(options.value?.[0].uidt as UITypes).filter((compOp) =>
@ -327,5 +346,6 @@ export function useViewFilters(
addFilterGroup,
saveOrUpdateDebounced,
isComparisonOpAllowed,
isComparisonSubOpAllowed,
}
}

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

@ -235,6 +235,7 @@
"action": "إجراء",
"actions": "إجراءات",
"operation": "عملية",
"operationSub": "Sub Operation",
"operationType": "نوع العملية",
"operationSubType": "نوع العملية الفرعية",
"description": "وصف",

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

@ -235,6 +235,7 @@
"action": "করম",
"actions": "কি",
"operation": "অপশন",
"operationSub": "Sub Operation",
"operationType": "অপশন টইপ",
"operationSubType": "অপশন সব-টইপ",
"description": "বরণন",

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

@ -235,6 +235,7 @@
"action": "Akce",
"actions": "Akce",
"operation": "Operace",
"operationSub": "Sub Operation",
"operationType": "Typ operace",
"operationSubType": "Podtyp operace",
"description": "Popis",

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

@ -1,31 +1,31 @@
{
"general": {
"home": "Hjem",
"load": "belastning",
"open": "Åben",
"close": "Tæt",
"home": "Forside",
"load": "Indlæs",
"open": "Åbn",
"close": "Luk",
"yes": "Ja",
"no": "Ingen",
"no": "Nej",
"ok": "Okay",
"and": "Og",
"or": "Eller",
"add": "Tilføje",
"edit": "Redigere",
"remove": "Fjerne",
"save": "Gemme",
"cancel": "Afbestille",
"add": "Tilføj",
"edit": "Redigér",
"remove": "Fjern",
"save": "Gem",
"cancel": "Fortryd",
"submit": "Indsend",
"create": "skab",
"create": "Opret",
"duplicate": "Duplikat",
"insert": "Indsættes",
"delete": "Delete.",
"update": "UPDATE.",
"insert": "Indsæt",
"delete": "Slet",
"update": "Opdatér",
"rename": "Omdøb",
"reload": "Genindlæsning",
"reload": "Genindlæs",
"reset": "Nulstil",
"install": "Installere",
"show": "At vise",
"hide": "Skjule",
"install": "Installer",
"show": "Vis",
"hide": "Skjul",
"showAll": "Vis alt",
"hideAll": "Gem alt",
"showMore": "Vis mere",
@ -75,7 +75,7 @@
"hideField": "Skjul felt",
"sortAsc": "Sortere stigende",
"sortDesc": "Sortere nedadgående",
"geoDataField": "GeoData Field"
"geoDataField": "GeoData-felt"
},
"objects": {
"project": "Projekt",
@ -92,15 +92,15 @@
"records": "Optegnelser.",
"webhook": "WebHook.",
"webhooks": "Webhooks.",
"view": "Udsigt",
"views": "Visninger.",
"view": "Visning",
"views": "Visninger",
"viewType": {
"grid": "Grid.",
"grid": "Grid",
"gallery": "Galleri",
"form": "Formular",
"kanban": "Kanban.",
"calendar": "Kalender",
"map": "Map"
"map": "Kort"
},
"user": "Bruger",
"users": "Brugere",
@ -209,7 +209,7 @@
"advancedSettings": "Avancerede indstillinger",
"codeSnippet": "Kodeuddrag",
"keyboardShortcut": "Tastaturgenveje",
"generateRandomName": "Generate Random Name"
"generateRandomName": "Generér Tilfældigt Navn"
},
"labels": {
"createdBy": "Oprettet af",
@ -217,7 +217,7 @@
"projName": "Projekt navn",
"tableName": "Tabelnavn.",
"viewName": "Se navn",
"viewLink": "Se Link.",
"viewLink": "Vis Link",
"columnName": "Kolonne navn",
"columnType": "Kolonne type",
"roleName": "Rolle navn",
@ -235,6 +235,7 @@
"action": "Handling",
"actions": "Handlinger",
"operation": "Operation",
"operationSub": "Underordnet operation",
"operationType": "Driftstype",
"operationSubType": "Drift Undertype",
"description": "Beskrivelse",
@ -256,7 +257,7 @@
"barcodeFormat": "Stregkodeformat",
"qrCodeValueTooLong": "For mange tegn til en QR-kode",
"barcodeValueTooLong": "For mange tegn til en stregkode",
"yourLocation": "Your Location",
"yourLocation": "Din Placering",
"lng": "Lng",
"lat": "Lat",
"aggregateFunction": "Aggregate Function.",
@ -282,8 +283,8 @@
},
"docReference": "Dokumentreference.",
"selectUserRole": "Vælg brugerrolle",
"childTable": "Børnebord",
"childColumn": "Barn kolonne",
"childTable": "Undertabel",
"childColumn": "Underkolonner",
"linkToAnotherRecord": "Link til en anden post",
"onUpdate": "På opdatering",
"onDelete": "På Delete.",
@ -378,36 +379,36 @@
"reloadRoles": "Genindlæs roller",
"nextPage": "Næste side",
"prevPage": "Forrige side",
"nextRecord": "Næste rekord.",
"previousRecord": "Tidligere rekord.",
"nextRecord": "Næste post",
"previousRecord": "Forrige post",
"copyApiURL": "COPY API URL.",
"createTable": "Tabel Create.",
"refreshTable": "Tabeller opdatere",
"renameTable": "Bord omdøb",
"deleteTable": "TABEL DELETE.",
"createTable": "Opret tabel",
"refreshTable": "Genopfrisk Tabeller",
"renameTable": "Omdøb Tabel",
"deleteTable": "Slet Tabel",
"addField": "Tilføj nyt felt til denne tabel",
"setDisplay": "Set as Display value",
"addRow": "Tilføj ny række",
"saveRow": "Gem ro",
"setDisplay": "Sæt som visningsværdi",
"addRow": "Tilføj ny post",
"saveRow": "Gem række",
"saveAndExit": "Gem og afslutning",
"saveAndStay": "Gem og bliv",
"insertRow": "Indsæt ny række",
"deleteRow": "DELETE ROW.",
"duplicateRow": "Duplicate Row",
"deleteRow": "Slet Række",
"duplicateRow": "Dupliker Række",
"deleteSelectedRow": "Slet de valgte rækker",
"importExcel": "Import Excel.",
"importCSV": "Import CSV.",
"downloadCSV": "Download som CSV.",
"downloadExcel": "Download som XLSX",
"uploadCSV": "Upload CSV.",
"import": "Importere",
"import": "Import",
"importMetadata": "Import metadata.",
"exportMetadata": "Eksport metadata.",
"clearMetadata": "Klare metadata.",
"exportToFile": "Eksporter til filer",
"changePwd": "Skift kodeord",
"createView": "Opret en visning",
"shareView": "Del View",
"shareView": "Del Visning",
"listSharedView": "Shared View List.",
"ListView": "Visninger List",
"copyView": "Kopi visning",
@ -428,23 +429,23 @@
"importZip": "Import Zip.",
"metaSync": "Synkroniser nu",
"settings": "Indstillinger.",
"previewAs": "Forhåndsvisning så",
"resetReview": "Nulstil preview.",
"testDbConn": "Test Database Connection.",
"previewAs": "Forhåndsvisning som",
"resetReview": "Nulstil forhåndsvisning",
"testDbConn": "Test Database Forbindelse",
"removeDbFromEnv": "Fjern databasen fra miljøet",
"editConnJson": "Rediger forbindelse JSON",
"sponsorUs": "Sponsor os",
"sponsorUs": "Sponsorer os",
"sendEmail": "SEND E-MAIL",
"addUserToProject": "Tilføj bruger til projekt",
"getApiSnippet": "Hent API-snippet",
"clearCell": "Klar celle",
"clearCell": "Ryd celle",
"addFilterGroup": "Tilføj filtergruppe",
"linkRecord": "Link record",
"addNewRecord": "Tilføj ny post",
"useConnectionUrl": "Brug forbindelses-URL",
"toggleCommentsDraw": "Toggle kommentarer tegne",
"expandRecord": "Udvid optegnelse",
"deleteRecord": "Slet registrering",
"deleteRecord": "Slet Post",
"erd": {
"showColumns": "Vis kolonner",
"showPkAndFk": "Vis primære og fremmede nøgler",
@ -460,8 +461,8 @@
"addOrEditStack": "Tilføj / Rediger stak"
},
"map": {
"mappedBy": "Mapped By",
"chooseMappingField": "Choose a Mapping Field"
"mappedBy": "Kortlagt af",
"chooseMappingField": "Vælg et kortlægningsfelt"
}
},
"tooltip": {
@ -529,9 +530,9 @@
"orgViewer": "Seeren har ikke lov til at oprette nye projekter, men kan få adgang til alle inviterede projekter."
},
"map": {
"overLimit": "You're over the limit.",
"closeLimit": "You're getting close to the limit.",
"limitNumber": "The limit of markers shown in a Map View is 1000 records."
"overLimit": "Du er over grænsen.",
"closeLimit": "Du nærmer dig grænsen.",
"limitNumber": "Grænsen for antallet af markeringer, der vises i en kortvisning, er 1000 poster."
},
"footerInfo": "Rækker per side",
"upload": "Vælg fil for at uploade",
@ -615,7 +616,7 @@
"gallery": "Tilføj Gallery View.",
"form": "Tilføj formularvisning",
"kanban": "Tilføj Kanban View.",
"map": "Add Map View",
"map": "Tilføj Kortvisning",
"calendar": "Tilføj kalendervisning"
},
"tablesMetadataInSync": "Tabeller Metadata er synkroniseret",
@ -647,11 +648,11 @@
"deleteViewConfirmation": "Er du sikker på, at du vil slette denne visning?",
"deleteTableConfirmation": "Ønsker du at slette tabellen",
"showM2mTables": "Vis M2M-tabeller",
"showM2mTablesDesc": "Many-to-many relation is supported via a junction table & is hidden by default. Enable this option to list all such tables along with existing tables.",
"showNullInCells": "Show NULL in Cells",
"showM2mTablesDesc": "Mange-til-mange-relationer understøttes via en sammenknytnings-tabel (junction table) og er skjult som standard. Aktiver denne indstilling for at få vist alle sådanne tabeller sammen med eksisterende tabeller.",
"showNullInCells": "Vis NULL i celler",
"showNullInCellsDesc": "Display 'NULL' tag in cells holding NULL value. This helps differentiate against cells holding EMPTY string.",
"showNullAndEmptyInFilter": "Show NULL and EMPTY in Filter",
"showNullAndEmptyInFilterDesc": "Enable 'additional' filters to differentiate fields containing NULL & Empty Strings. Default support for Blank treats both NULL & Empty strings alike.",
"showNullAndEmptyInFilter": "Vis NULL og EMPTY i Filter",
"showNullAndEmptyInFilterDesc": "Aktiver \"yderligere\" filtre for at skelne mellem felter, der indeholder NULL og tomme strenge. Standardunderstøttelse for Blank behandler både NULL- og tomme strenge ens.",
"deleteKanbanStackConfirmation": "Hvis du sletter denne stak, fjernes også valgmuligheden `{stackToBeDeleted}` fra `{groupingField}`. Posterne vil blive flyttet til stakken \"uncategorized\".",
"computedFieldEditWarning": "Beregnet felt: indholdet er skrivebeskyttet. Brug kolonne-redigeringsmenuen til at omkonfigurere",
"computedFieldDeleteWarning": "Beregnet felt: indholdet er skrivebeskyttet. Det er ikke muligt at slette indholdet.",
@ -706,7 +707,7 @@
"nameShouldStartWithAnAlphabetOr_": "Navnet skal starte med et alfabet eller _",
"followingCharactersAreNotAllowed": "Følgende tegn er ikke tilladt",
"columnNameRequired": "Kolonnens navn er påkrævet",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"columnNameExceedsCharacters": "Længden af kolonnenavnet overstiger maks. {value} tegn",
"projectNameExceeds50Characters": "Projektnavnet overstiger 50 tegn",
"projectNameCannotStartWithSpace": "Projektnavnet kan ikke begynde med et mellemrum",
"requiredField": "Obligatorisk felt",
@ -739,7 +740,7 @@
},
"success": {
"columnDuplicated": "Kolonne duplikeret med succes",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"rowDuplicatedWithoutSavedYet": "Række duplikeret (ikke gemt)",
"updatedUIACL": "Opdateret UI ACL for tabeller med succes",
"pluginUninstalled": "Plugin afinstalleret med succes",
"pluginSettingsSaved": "Plugin-indstillingerne er gemt med succes",

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

@ -100,7 +100,7 @@
"form": "Formular",
"kanban": "Kanban",
"calendar": "Kalender",
"map": "Karte"
"map": "Map"
},
"user": "Nutzer",
"users": "Benutzer",
@ -235,6 +235,7 @@
"action": "Aktion",
"actions": "Aktionen",
"operation": "Vorgang",
"operationSub": "Sub Operation",
"operationType": "Vorgangstyp",
"operationSubType": "Vorgangsuntertyp",
"description": "Beschreibung",

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

@ -236,6 +236,7 @@
"action": "Action",
"actions": "Actions",
"operation": "Operation",
"operationSub": "Sub Operation",
"operationType": "Operation type",
"operationSubType": "Operation sub-type",
"description": "Description",

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

@ -235,6 +235,7 @@
"action": "Acción",
"actions": "Acciones",
"operation": "Operación",
"operationSub": "Sub Operation",
"operationType": "Tipo de operación",
"operationSubType": "Sub-tipo de operación",
"description": "Descripción",

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

@ -235,6 +235,7 @@
"action": "Ekintza",
"actions": "Ekintzak",
"operation": "Operation",
"operationSub": "Sub Operation",
"operationType": "Operation type",
"operationSubType": "Operation sub-type",
"description": "Deskribapena",

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

@ -235,6 +235,7 @@
"action": "اقدام",
"actions": "اقدامات",
"operation": "عملیات",
"operationSub": "Sub Operation",
"operationType": "نوع عملیات",
"operationSubType": "زیرگونه عملیات",
"description": "شرح",

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

@ -235,6 +235,7 @@
"action": "Toiminta",
"actions": "Toiminnot",
"operation": "Operaatio",
"operationSub": "Sub Operation",
"operationType": "Käyttötyyppi",
"operationSubType": "Käyttö Alatyyppi",
"description": "Kuvaus",

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

@ -235,6 +235,7 @@
"action": "Action",
"actions": "Actions",
"operation": "Opération",
"operationSub": "Sub Operation",
"operationType": "Type d'opération",
"operationSubType": "Sous-type d'opération",
"description": "Description",

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

@ -235,6 +235,7 @@
"action": "פעולה",
"actions": "פעולות",
"operation": "מבצע",
"operationSub": "Sub Operation",
"operationType": "סוג הפעולה",
"operationSubType": "מבצע תת-סוג",
"description": "תיאור",

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

@ -235,6 +235,7 @@
"action": "गतििि",
"actions": "करवई",
"operation": "सलन",
"operationSub": "Sub Operation",
"operationType": "परचलन परकर",
"operationSubType": "परचलन उप-परकर",
"description": "विवरण",

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

@ -235,6 +235,7 @@
"action": "Akcijski",
"actions": "Akcije",
"operation": "Operacija",
"operationSub": "Sub Operation",
"operationType": "Vrsta rada",
"operationSubType": "Operacija pod-vrsti",
"description": "Opis",

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

@ -235,6 +235,7 @@
"action": "Tindakan",
"actions": "Tindakan",
"operation": "Operasi",
"operationSub": "Sub Operation",
"operationType": "Jenis operasi",
"operationSubType": "SUB-tipe operasi",
"description": "Keterangan",

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

@ -235,6 +235,7 @@
"action": "Azione",
"actions": "Azioni",
"operation": "Operazione",
"operationSub": "Sub Operation",
"operationType": "Tipo di operazioni",
"operationSubType": "Sottotipo di operazioni",
"description": "Descrizione",

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

@ -235,6 +235,7 @@
"action": "アクション",
"actions": "アクション",
"operation": "操作",
"operationSub": "Sub Operation",
"operationType": "操作タイプ",
"operationSubType": "操作サブタイプ",
"description": "説明",

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

@ -235,6 +235,7 @@
"action": "동작",
"actions": "행위",
"operation": "작업",
"operationSub": "Sub Operation",
"operationType": "작업 유형",
"operationSubType": "작업 하위 유형",
"description": "설명",

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

@ -235,6 +235,7 @@
"action": "Darbība",
"actions": "Darbības",
"operation": "Operācija",
"operationSub": "Sub Operation",
"operationType": "Operācijas tips",
"operationSubType": "Operācijas apakštips",
"description": "Apraksts",

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

@ -235,6 +235,7 @@
"action": "Actie",
"actions": "Acties",
"operation": "Operatie",
"operationSub": "Sub Operation",
"operationType": "Operatietype",
"operationSubType": "Operatiesubtype",
"description": "Beschrijving",

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

@ -235,6 +235,7 @@
"action": "Handling",
"actions": "Handlinger",
"operation": "Operasjon",
"operationSub": "Sub Operation",
"operationType": "Operasjonstype",
"operationSubType": "OPERATION SUB-TYPE",
"description": "Beskrivelse",

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

@ -235,6 +235,7 @@
"action": "Akcja",
"actions": "Akcje",
"operation": "Operacja",
"operationSub": "Sub Operation",
"operationType": "Typ operacji",
"operationSubType": "Podtypowy akcji",
"description": "Opis",

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

@ -235,6 +235,7 @@
"action": "Açao",
"actions": "Ações",
"operation": "Operação",
"operationSub": "Sub Operation",
"operationType": "Tipo de operação",
"operationSubType": "Sub-tipo de operação",
"description": "Descrição",

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

@ -235,6 +235,7 @@
"action": "Açao",
"actions": "Ações",
"operation": "Operação",
"operationSub": "Sub Operation",
"operationType": "Tipo de operação",
"operationSubType": "Sub-tipo de operação",
"description": "Descrição",

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

@ -235,6 +235,7 @@
"action": "Действие",
"actions": "Действия",
"operation": "Операция",
"operationSub": "Sub Operation",
"operationType": "Тип операции",
"operationSubType": "Подтип операции",
"description": "Описание",

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

@ -235,6 +235,7 @@
"action": "Akcia",
"actions": "Činnosti",
"operation": "Operácia",
"operationSub": "Sub Operation",
"operationType": "Typ operácie",
"operationSubType": "Podtyp operácie",
"description": "Popis",

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

@ -235,6 +235,7 @@
"action": "Akcija",
"actions": "Akcijah",
"operation": "Operacija",
"operationSub": "Sub Operation",
"operationType": "Tip operacije.",
"operationSubType": "Podpis delovanja",
"description": "Opis",

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

@ -235,6 +235,7 @@
"action": "Handling",
"actions": "Handlingar",
"operation": "Drift",
"operationSub": "Sub Operation",
"operationType": "Driftstyp",
"operationSubType": "Driftstyp",
"description": "Beskrivning",

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

@ -235,6 +235,7 @@
"action": "หนงบ",
"actions": "การกระทำ",
"operation": "การดำเนนการ",
"operationSub": "Sub Operation",
"operationType": "ประเภทการทำงาน",
"operationSubType": "การดำเนนงานประเภทยอย",
"description": "คำอธบาย",

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

@ -235,6 +235,7 @@
"action": "Aksiyon",
"actions": "Aksiyonlar",
"operation": "İşlem",
"operationSub": "Sub Operation",
"operationType": "İşlem türü",
"operationSubType": "İşlem alt-türü",
"description": "Tanım",

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

@ -235,6 +235,7 @@
"action": "Дія",
"actions": "Дії",
"operation": "Операція",
"operationSub": "Sub Operation",
"operationType": "Тип операції",
"operationSubType": "Підтип операції",
"description": "Опис",

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

@ -235,6 +235,7 @@
"action": "Hoạt động",
"actions": "Hành động",
"operation": "Hoạt động",
"operationSub": "Sub Operation",
"operationType": "Loại hoạt động",
"operationSubType": "Loại phụ hoạt động",
"description": "Sự miêu tả",

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

@ -75,7 +75,7 @@
"hideField": "隐藏字段",
"sortAsc": "升序",
"sortDesc": "降序",
"geoDataField": "GeoData Field"
"geoDataField": "地理数据字段"
},
"objects": {
"project": "项目",
@ -138,7 +138,7 @@
"Currency": "货币",
"Percent": "百分比",
"Duration": "时长",
"GeoData": "GeoData",
"GeoData": "地理数据",
"Rating": "评分",
"Formula": "公式",
"Rollup": "聚合",
@ -235,6 +235,7 @@
"action": "操作",
"actions": "操作",
"operation": "操作",
"operationSub": "Sub Operation",
"operationType": "操作类型",
"operationSubType": "子操作类型",
"description": "描述",
@ -256,9 +257,9 @@
"barcodeFormat": "条形码码制",
"qrCodeValueTooLong": "字数超出二维码容量",
"barcodeValueTooLong": "字数超出条形码容量",
"yourLocation": "Your Location",
"lng": "Lng",
"lat": "Lat",
"yourLocation": "您所在的位置",
"lng": "经度",
"lat": "纬度",
"aggregateFunction": "汇总功能",
"dbCreateIfNotExists": "自动创建数据库",
"clientKey": "客户端密钥",
@ -529,8 +530,8 @@
"orgViewer": "游客不能创建新项目,仅允许访问受邀项目。"
},
"map": {
"overLimit": "You're over the limit.",
"closeLimit": "You're getting close to the limit.",
"overLimit": "你已经超出了限制。",
"closeLimit": "您已接近上限。",
"limitNumber": "The limit of markers shown in a Map View is 1000 records."
},
"footerInfo": "每页行驶",
@ -615,7 +616,7 @@
"gallery": "添加画廊视图",
"form": "添加表单视图",
"kanban": "添加看板视图",
"map": "Add Map View",
"map": "添加地图视图",
"calendar": "添加日历视图"
},
"tablesMetadataInSync": "表元数据同步",

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

@ -235,6 +235,7 @@
"action": "行動",
"actions": "行動",
"operation": "操作",
"operationSub": "Sub Operation",
"operationType": "操作類型",
"operationSubType": "操作子類型",
"description": "描述",

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

@ -3,7 +3,11 @@ import { UITypes, isNumericCol, numericUITypes } from 'nocodb-sdk'
const getEqText = (fieldUiType: UITypes) => {
if (isNumericCol(fieldUiType)) {
return '='
} else if ([UITypes.SingleSelect, UITypes.Collaborator, UITypes.LinkToAnotherRecord].includes(fieldUiType)) {
} else if (
[UITypes.SingleSelect, UITypes.Collaborator, UITypes.LinkToAnotherRecord, UITypes.Date, UITypes.DateTime].includes(
fieldUiType,
)
) {
return 'is'
}
return 'is equal'
@ -12,7 +16,11 @@ const getEqText = (fieldUiType: UITypes) => {
const getNeqText = (fieldUiType: UITypes) => {
if (isNumericCol(fieldUiType)) {
return '!='
} else if ([UITypes.SingleSelect, UITypes.Collaborator, UITypes.LinkToAnotherRecord].includes(fieldUiType)) {
} else if (
[UITypes.SingleSelect, UITypes.Collaborator, UITypes.LinkToAnotherRecord, UITypes.Date, UITypes.DateTime].includes(
fieldUiType,
)
) {
return 'is not'
}
return 'is not equal'
@ -32,12 +40,40 @@ const getNotLikeText = (fieldUiType: UITypes) => {
return 'is not like'
}
const getGtText = (fieldUiType: UITypes) => {
if ([UITypes.Date, UITypes.DateTime].includes(fieldUiType)) {
return 'is after'
}
return '>'
}
const getLtText = (fieldUiType: UITypes) => {
if ([UITypes.Date, UITypes.DateTime].includes(fieldUiType)) {
return 'is before'
}
return '<'
}
const getGteText = (fieldUiType: UITypes) => {
if ([UITypes.Date, UITypes.DateTime].includes(fieldUiType)) {
return 'is on or after'
}
return '>='
}
const getLteText = (fieldUiType: UITypes) => {
if ([UITypes.Date, UITypes.DateTime].includes(fieldUiType)) {
return 'is on or before'
}
return '<='
}
export const comparisonOpList = (
fieldUiType: UITypes,
): {
text: string
value: string
ignoreVal?: boolean
ignoreVal: boolean
includedTypes?: UITypes[]
excludedTypes?: UITypes[]
}[] => [
@ -56,22 +92,42 @@ export const comparisonOpList = (
{
text: getEqText(fieldUiType),
value: 'eq',
ignoreVal: false,
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment],
},
{
text: getNeqText(fieldUiType),
value: 'neq',
ignoreVal: false,
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment],
},
{
text: getLikeText(fieldUiType),
value: 'like',
excludedTypes: [UITypes.Checkbox, UITypes.SingleSelect, UITypes.MultiSelect, UITypes.Collaborator, ...numericUITypes],
ignoreVal: false,
excludedTypes: [
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Date,
UITypes.DateTime,
...numericUITypes,
],
},
{
text: getNotLikeText(fieldUiType),
value: 'nlike',
excludedTypes: [UITypes.Checkbox, UITypes.SingleSelect, UITypes.MultiSelect, UITypes.Collaborator, ...numericUITypes],
ignoreVal: false,
excludedTypes: [
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Date,
UITypes.DateTime,
...numericUITypes,
],
},
{
text: 'is empty',
@ -85,6 +141,8 @@ export const comparisonOpList = (
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
...numericUITypes,
],
},
@ -100,6 +158,8 @@ export const comparisonOpList = (
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
...numericUITypes,
],
},
@ -116,6 +176,8 @@ export const comparisonOpList = (
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
],
},
{
@ -131,47 +193,63 @@ export const comparisonOpList = (
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
],
},
{
text: 'contains all of',
value: 'allof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect],
},
{
text: 'contains any of',
value: 'anyof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect],
},
{
text: 'does not contain all of',
value: 'nallof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect],
},
{
text: 'does not contain any of',
value: 'nanyof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect],
},
{
text: '>',
text: getGtText(fieldUiType),
value: 'gt',
includedTypes: [...numericUITypes],
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime],
},
{
text: '<',
text: getLtText(fieldUiType),
value: 'lt',
includedTypes: [...numericUITypes],
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime],
},
{
text: '>=',
text: getGteText(fieldUiType),
value: 'gte',
includedTypes: [...numericUITypes],
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime],
},
{
text: '<=',
text: getLteText(fieldUiType),
value: 'lte',
includedTypes: [...numericUITypes],
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime],
},
{
text: 'is within',
value: 'isWithin',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'is blank',
@ -186,3 +264,129 @@ export const comparisonOpList = (
excludedTypes: [UITypes.Checkbox],
},
]
export const comparisonSubOpList = (
// TODO: type
comparison_op: string,
): {
text: string
value: string
ignoreVal: boolean
includedTypes?: UITypes[]
excludedTypes?: UITypes[]
}[] => {
if (comparison_op === 'isWithin') {
return [
{
text: 'the past week',
value: 'pastWeek',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'the past month',
value: 'pastMonth',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'the past year',
value: 'pastYear',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'the next week',
value: 'nextWeek',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'the next month',
value: 'nextMonth',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'the next year',
value: 'nextYear',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'the next number of days',
value: 'nextNumberOfDays',
ignoreVal: false,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'the past number of days',
value: 'pastNumberOfDays',
ignoreVal: false,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
]
}
return [
{
text: 'today',
value: 'today',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'tomorrow',
value: 'tomorrow',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'yesterday',
value: 'yesterday',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'one week ago',
value: 'oneWeekAgo',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'one week from now',
value: 'oneWeekFromNow',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'one month ago',
value: 'oneMonthAgo',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'one month from now',
value: 'oneMonthFromNow',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'number of days ago',
value: 'daysAgo',
ignoreVal: false,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'number of days from now',
value: 'daysFromNow',
ignoreVal: false,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'exact date',
value: 'exactDate',
ignoreVal: false,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
]
}

4091
packages/nc-plugin/package-lock.json generated

File diff suppressed because it is too large Load Diff

51
packages/noco-docs/content/en/developer-resources/rest-apis.md

@ -120,14 +120,20 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| Meta | Patch | dbView | update | /api/v1/db/meta/tables/{tableId} |
| Meta | Delete| dbView | delete | /api/v1/db/meta/tables/{tableId} |
| Meta | Post | dbView | reorder | /api/v1/db/meta/tables/{tableId}/reorder |
| Meta | Post | dbView | formCreate | /api/v1/db/meta/forms |
| Meta | Patch | dbView | formUpdate | /api/v1/db/meta/forms/{formId} |
| Meta | Get | dbView | formRead | /api/v1/db/meta/forms/{formId} |
| Meta | Post | dbView | formCreate | /api/v1/db/meta/tables/{tableId}/forms |
| Meta | Patch | dbView | formUpdate | /api/v1/db/meta/forms/{formViewId} |
| Meta | Get | dbView | formRead | /api/v1/db/meta/forms/{formViewId} |
| Meta | Patch | dbView | formColumnUpdate | /api/v1/db/meta/form-columns/{formViewColumnId} |
| Meta | Post | dbView | galleryCreate | /api/v1/db/meta/galleries |
| Meta | Patch | dbView | galleryUpdate | /api/v1/db/meta/galleries/{galleriesId} |
| Meta | Get | dbView | galleryRead | /api/v1/db/meta/galleries/{galleriesId} |
| Meta | Post | dbView | gridCreate | /api/v1/db/meta/tables/${tableId}/grids |
| Meta | Post | dbView | galleryCreate | /api/v1/db/meta/tables/{tableId}/galleries |
| Meta | Patch | dbView | galleryUpdate | /api/v1/db/meta/galleries/{galleryViewId} |
| Meta | Get | dbView | galleryRead | /api/v1/db/meta/galleries/{galleryViewId} |
| Meta | Post | dbView | kanbanCreate | /api/v1/db/meta/tables/{tableId}/kanbans |
| Meta | Patch | dbView | kanbanUpdate | /api/v1/db/meta/kanban/{kanbanViewId} |
| Meta | Get | dbView | kanbanRead | /api/v1/db/meta/kanbans/{kanbanViewId} |
| Meta | Post | dbView | mapCreate | /api/v1/db/meta/tables/{tableId}/maps |
| Meta | Patch | dbView | mapUpdate | /api/v1/db/meta/maps/{mapViewId} |
| Meta | Get | dbView | mapRead | /api/v1/db/meta/maps/{mapViewId} |
| Meta | Post | dbView | gridCreate | /api/v1/db/meta/tables/{tableId}/grids |
| Meta | Get | dbView | gridColumnsList | /api/v1/db/meta/grids/{gridId}/grid-columns |
| Meta | Patch | dbView | gridColumnUpdate | /api/v1/db/meta/grid-columns/{columnId} |
| Meta | Patch | dbView | update | /api/v1/db/meta/views/{viewId} |
@ -221,11 +227,42 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| btw | between | (colName,btw,val1,val2) |
| nbtw | not between | (colName,nbtw,val1,val2) |
| like | like | (colName,like,%name) |
| isWithin | is Within (Available in `Date` and `DateTime` only) | (colName,isWithin,sub_op) |
| allof | includes all of | (colName,allof,val1,val2,...) |
| anyof | includes any of | (colName,anyof,val1,val2,...) |
| nallof | does not include all of (includes none or some, but not all of) | (colName,nallof,val1,val2,...) |
| nanyof | does not include any of (includes none of) | (colName,nanyof,val1,val2,...) |
## Comparison Sub-Operators
The following sub-operators are available in `Date` and `DateTime` columns.
| Operation | Meaning | Example |
|-----------------|-------------------------|-----------------------------------|
| today | today | (colName,eq,today) |
| tomorrow | tomorrow | (colName,eq,tomorrow) |
| yesterday | yesterday | (colName,eq,yesterday) |
| oneWeekAgo | one week ago | (colName,eq,oneWeekAgo) |
| oneWeekFromNow | one week from now | (colName,eq,oneWeekFromNow) |
| oneMonthAgo | one month ago | (colName,eq,oneMonthAgo) |
| oneMonthFromNow | one month from now | (colName,eq,oneMonthFromNow) |
| daysAgo | number of days ago | (colName,eq,daysAgo,10) |
| daysFromNow | number of days from now | (colName,eq,daysFromNow,10) |
| exactDate | exact date | (colName,eq,exactDate,2022-02-02) |
For `isWithin` in `Date` and `DateTime` columns, the different set of sub-operators are used.
| Operation | Meaning | Example |
|------------------|-------------------------|-----------------------------------------|
| pastWeek | the past week | (colName,isWithin,pastWeek) |
| pastMonth | the past month | (colName,isWithin,pastMonth) |
| pastYear | the past year | (colName,isWithin,pastYear) |
| nextWeek | the next week | (colName,isWithin,nextWeek) |
| nextMonth | the next month | (colName,isWithin,nextMonth) |
| nextYear | the next year | (colName,isWithin,nextYear) |
| nextNumberOfDays | the next number of days | (colName,isWithin,nextNumberOfDays,10) |
| pastNumberOfDays | the past number of days | (colName,isWithin,pastNumberOfDays,10) |
## Logical Operators
| Operation | Example |

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

@ -70,7 +70,8 @@ After the click, it will show a menu and you can enter the column name and choos
You can also click `Show more` for additional menu options.
<img width="445" alt="image" src="https://user-images.githubusercontent.com/35857179/189075678-d18b799f-df13-4f78-a5a5-813e8d3277ae.png">
![Screenshot 2023-03-03 at 8 13 07 PM](https://user-images.githubusercontent.com/86527202/222749857-0e793db2-a5d2-4b54-8d23-2a0cbbec8f5d.png)
<!-- <img width="445" alt="image" src="https://user-images.githubusercontent.com/35857179/189075678-d18b799f-df13-4f78-a5a5-813e8d3277ae.png"> -->
Click `Save` button to create the new column.
@ -86,6 +87,11 @@ You will be able to edit column name & associated datatype using pop-up modal.
<img width="497" alt="image" src="https://user-images.githubusercontent.com/35857179/189077270-7acdc818-3747-4307-93fb-e970cb7936f9.png">
Prior to v0.104.3, Advanced menu by default displayed developer specific database configuration options. To avoid unintended tweaks from user, these are now hidden under an easter egg menu. To enable, double click on `show all`/`hide all` button in column edit modal.
![Screenshot 2023-03-06 at 10 45 26 AM](https://user-images.githubusercontent.com/86527202/223024810-85dac1c6-87ef-4193-90cb-3a05be8ccc1d.png)
### Column Delete
To delete a column, click the down arrow, select `Delete` from the menu.

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

File diff suppressed because it is too large Load Diff

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

@ -55,7 +55,7 @@ export enum AuditOperationSubTypes {
EXPORT_TO_ZIP = 'EXPORT_TO_ZIP',
UPDATED = 'UPDATED',
SIGNIN = 'SIGNIN',
SIGN = 'SIGN',
SIGNUP = 'SIGNUP',
PASSWORD_RESET = 'PASSWORD_RESET',
PASSWORD_FORGOT = 'PASSWORD_FORGOT',
PASSWORD_CHANGE = 'PASSWORD_CHANGE',

6
packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts

@ -1,3 +1,4 @@
import { NormalColumnRequestType } from '../Api'
import UITypes from '../UITypes';
import { IDType } from './index';
@ -794,7 +795,10 @@ export class OracleUi {
}
}
static getDataTypeForUiType(col: { uidt: UITypes }, idType?: IDType) {
static getDataTypeForUiType(
col: { uidt: UITypes | NormalColumnRequestType['uidt'] },
idType?: IDType
) {
const colProp: any = {};
switch (col.uidt) {
case 'ID':

11
packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts

@ -1,3 +1,4 @@
import { BoolType } from '../Api'
import UITypes from '../UITypes';
import { MssqlUi } from './MssqlUi';
@ -56,12 +57,12 @@ export type SqlUIColumn = {
dt?: string;
dtx?: string;
ct?: string;
nrqd?: boolean;
rqd?: boolean;
nrqd?: BoolType;
rqd?: BoolType;
ck?: string;
pk?: boolean;
un?: boolean;
ai?: boolean;
pk?: BoolType;
un?: BoolType;
ai?: BoolType;
cdf?: string | any;
clen?: number | any;
np?: string;

71
packages/nocodb/.eslintrc.json

@ -7,18 +7,8 @@
"env": {
"es6": true
},
"ignorePatterns": [
"node_modules",
"build",
"coverage",
"dist",
"nc"
],
"plugins": [
"import",
"eslint-comments",
"functional"
],
"ignorePatterns": ["node_modules", "build", "coverage", "dist", "nc"],
"plugins": ["import", "eslint-comments", "functional"],
"extends": [
"eslint:recommended",
"plugin:eslint-comments/recommended",
@ -47,6 +37,21 @@
"ignoreCase": true
}
],
"import/order": [
"error",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"object",
"type"
]
}
],
"@typescript-eslint/no-this-alias": "off",
// todo: enable
@ -57,46 +62,6 @@
"no-useless-catch": "off",
"no-empty": "off",
"@typescript-eslint/no-empty-function": "off",
"import/order": "off"
// "@typescript-eslint/member-ordering": [
// "warn" ,
// {
// "default": {
// "memberTypes": [ "static-field",
// "public-field",
// "instance-field",
// "protected-field",
// "private-field",
// "abstract-field",
//
// "public-static-field",
// "protected-static-field",
// "private-static-field",
// "public-instance-field",
// "public-decorated-field",
// "public-abstract-field",
// "protected-instance-field",
// "protected-decorated-field",
// "protected-abstract-field",
// "private-instance-field",
// "private-decorated-field",
// "private-abstract-field",
//
//
//
// "constructor",
//
// "public-static-method",
// "protected-static-method",
// "private-static-method",
// "public-method",
// "protected-method",
// "private-method"
// ]
// }
// }
// ]
"@typescript-eslint/consistent-type-imports": "warn"
}
}

41
packages/nocodb/package-lock.json generated

@ -14,6 +14,7 @@
"@sentry/node": "^6.3.5",
"airtable": "^0.11.3",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"archiver": "^5.0.2",
"auto-bind": "^4.0.0",
"aws-sdk": "^2.829.0",
@ -65,7 +66,7 @@
"multer": "^1.4.2",
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-help": "0.2.85",
"nc-help": "0.2.87",
"nc-lib-gui": "0.105.3",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
@ -120,7 +121,7 @@
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-prettier": "^4.0.0",
"mocha": "^10.1.0",
"nodemon": "^2.0.7",
@ -2261,6 +2262,22 @@
"ajv": ">=5.0.0"
}
},
"node_modules/ajv-formats": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/amdefine": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
@ -11294,9 +11311,9 @@
}
},
"node_modules/nc-help": {
"version": "0.2.85",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.85.tgz",
"integrity": "sha512-EOyrc2PuRUJzv73jHNHmUR6YhC0TlJG0DTY/sug7BF4MJAVPJgyavJnrqkRC7g0NS4xociki9gs5MbLRjlRwtQ==",
"version": "0.2.87",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.87.tgz",
"integrity": "sha512-Zlg06ialvylBEE1qtvjlNKxZrPShzXwvy3WG7nfw+8GngOkQBCTlKguejT2Kq4Gfb5378WPX1APXtsetMKBrRA==",
"dependencies": {
"@rudderstack/rudder-sdk-node": "^1.1.3",
"axios": "^0.21.1",
@ -20919,6 +20936,14 @@
"dev": true,
"requires": {}
},
"ajv-formats": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"requires": {
"ajv": "^8.0.0"
}
},
"amdefine": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
@ -28004,9 +28029,9 @@
"integrity": "sha512-3AryS9uwa5NfISLxMciUonrH7YfXp+nlahB9T7girXIsLQrmwX4MdnuKs32akduCOGpKmjTJSWmATULbuMkbfw=="
},
"nc-help": {
"version": "0.2.85",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.85.tgz",
"integrity": "sha512-EOyrc2PuRUJzv73jHNHmUR6YhC0TlJG0DTY/sug7BF4MJAVPJgyavJnrqkRC7g0NS4xociki9gs5MbLRjlRwtQ==",
"version": "0.2.87",
"resolved": "https://registry.npmjs.org/nc-help/-/nc-help-0.2.87.tgz",
"integrity": "sha512-Zlg06ialvylBEE1qtvjlNKxZrPShzXwvy3WG7nfw+8GngOkQBCTlKguejT2Kq4Gfb5378WPX1APXtsetMKBrRA==",
"requires": {
"@rudderstack/rudder-sdk-node": "^1.1.3",
"axios": "^0.21.1",

10
packages/nocodb/package.json

@ -25,7 +25,7 @@
"obfuscate:build:publish": "npm run build:obfuscate && npm publish .",
"fix": "run-s fix:*",
"fix:prettier": "prettier \"src/**/*.ts\" --write",
"lint": "eslint src --ext .ts",
"lint": "eslint src --ext .ts --fix",
"test": "cross-env TS_NODE_PROJECT=tsconfig.json mocha -r ts-node/register src/__tests__/**/*.test.ts --recursive",
"unit-test": "cross-env TS_NODE_PROJECT=tsconfig.json mocha --require ts-node/register 'src/__tests__/unit/**/*.test.ts' --recursive --check-leaks --exit",
"local:test:unit": "cross-env TS_NODE_PROJECT=./tests/unit/tsconfig.json mocha -r ts-node/register tests/unit/index.test.ts --recursive --timeout 300000 --exit --delay",
@ -43,7 +43,8 @@
"watch:run:pg": "cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG --log-error --project tsconfig.json\"",
"run": "ts-node src/run/docker",
"watch:try": "nodemon -e ts,js -w ./src -x \"ts-node src/run/try --log-error --project tsconfig.json\"",
"example:docker": "ts-node src/run/docker.ts"
"example:docker": "ts-node src/run/docker.ts",
"redoc": "nodemon -e ts,json -w ./src/schema -x \"ts-node src/run/redoc --log-error --project tsconfig.json\""
},
"engines": {
"node": ">=8.9"
@ -54,6 +55,7 @@
"@sentry/node": "^6.3.5",
"airtable": "^0.11.3",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"archiver": "^5.0.2",
"auto-bind": "^4.0.0",
"aws-sdk": "^2.829.0",
@ -105,7 +107,7 @@
"multer": "^1.4.2",
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-help": "0.2.85",
"nc-help": "0.2.87",
"nc-lib-gui": "0.105.3",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
@ -160,7 +162,7 @@
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-prettier": "^4.0.0",
"mocha": "^10.1.0",
"nodemon": "^2.0.7",

6
packages/nocodb/src/interface/config.ts

@ -1,6 +1,6 @@
import { Handler } from 'express';
import * as e from 'express';
import { Knex } from 'knex';
import type { Handler } from 'express';
import type * as e from 'express';
import type { Knex } from 'knex';
export interface Route {
path: string;

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

@ -9,43 +9,43 @@ import clear from 'clear';
import cookieParser from 'cookie-parser';
import debug from 'debug';
import * as express from 'express';
import { Router } from 'express';
import importFresh from 'import-fresh';
import morgan from 'morgan';
import NcToolGui from 'nc-lib-gui';
import requestIp from 'request-ip';
import { v4 as uuidv4 } from 'uuid';
import { NcConfig } from '../interface/config';
import { T } from 'nc-help';
import mkdirp from 'mkdirp';
import { NC_LICENSE_KEY } from './constants';
import Migrator from './db/sql-migrator/lib/KnexMigrator';
import Store from './models/Store';
import NcConfigFactory from './utils/NcConfigFactory';
import { Tele } from 'nc-help';
import NcProjectBuilderCE from './v1-legacy/NcProjectBuilder';
import NcProjectBuilderEE from './v1-legacy/NcProjectBuilderEE';
import { GqlApiBuilder } from './v1-legacy/gql/GqlApiBuilder';
import NcMetaIO from './meta/NcMetaIO';
import NcMetaImplCE from './meta/NcMetaIOImpl';
import NcMetaImplEE from './meta/NcMetaIOImplEE';
import NcMetaMgrCE from './meta/NcMetaMgr';
import NcMetaMgrEE from './meta/NcMetaMgrEE';
import { RestApiBuilder } from './v1-legacy/rest/RestApiBuilder';
import RestAuthCtrlCE from './v1-legacy/rest/RestAuthCtrl';
import RestAuthCtrlEE from './v1-legacy/rest/RestAuthCtrlEE';
import mkdirp from 'mkdirp';
import MetaAPILogger from './meta/MetaAPILogger';
import NcUpgrader from './version-upgrader/NcUpgrader';
import NcMetaMgrv2 from './meta/NcMetaMgrv2';
import NocoCache from './cache/NocoCache';
import registerMetaApis from './meta/api';
import NcPluginMgrv2 from './meta/helpers/NcPluginMgrv2';
import User from './models/User';
import * as http from 'http';
import weAreHiring from './utils/weAreHiring';
import getInstance from './utils/getInstance';
import initAdminFromEnv from './meta/api/userApi/initAdminFromEnv';
import initAdminFromEnv from './services/user/initAdminFromEnv';
import type * as http from 'http';
import type NcMetaMgrv2 from './meta/NcMetaMgrv2';
import type { RestApiBuilder } from './v1-legacy/rest/RestApiBuilder';
import type NcMetaMgrEE from './meta/NcMetaMgrEE';
import type NcMetaMgrCE from './meta/NcMetaMgr';
import type NcMetaIO from './meta/NcMetaIO';
import type { GqlApiBuilder } from './v1-legacy/gql/GqlApiBuilder';
import type { NcConfig } from '../interface/config';
import type { Router } from 'express';
const log = debug('nc:app');
require('dotenv').config();
@ -105,7 +105,7 @@ export default class Noco {
constructor() {
process.env.PORT = process.env.PORT || '8080';
// todo: move
process.env.NC_VERSION = '0105002';
process.env.NC_VERSION = '0105003';
// if env variable NC_MINIMAL_DBS is set, then disable project creation with external sources
if (process.env.NC_MINIMAL_DBS) {
@ -275,10 +275,10 @@ export default class Noco {
}
next();
});
Tele.init({
T.init({
instance: getInstance,
});
Tele.emit('evt_app_started', await User.count());
T.emit('evt_app_started', await User.count());
console.log(`App started successfully.\nVisit -> ${Noco.dashboardUrl}`);
weAreHiring();
return this.router;
@ -531,7 +531,7 @@ export default class Noco {
if (!serverId) {
await Noco._ncMeta.metaInsert('', '', 'nc_store', {
key: 'nc_server_id',
value: (serverId = Tele.id),
value: (serverId = T.id),
});
}
process.env.NC_SERVER_UUID = serverId;

4
packages/nocodb/src/lib/cache/NocoCache.ts vendored

@ -1,7 +1,7 @@
import CacheMgr from './CacheMgr';
import { CacheGetType } from '../utils/globals';
import RedisCacheMgr from './RedisCacheMgr';
import RedisMockCacheMgr from './RedisMockCacheMgr';
import { CacheGetType } from '../utils/globals';
import type CacheMgr from './CacheMgr';
export default class NocoCache {
private static client: CacheMgr;

2
packages/nocodb/src/lib/cache/RedisCacheMgr.ts vendored

@ -1,7 +1,7 @@
import debug from 'debug';
import CacheMgr from './CacheMgr';
import Redis from 'ioredis';
import { CacheDelDirection, CacheGetType, CacheScope } from '../utils/globals';
import CacheMgr from './CacheMgr';
const log = debug('nc:cache');

2
packages/nocodb/src/lib/cache/RedisMockCacheMgr.ts vendored

@ -1,7 +1,7 @@
import debug from 'debug';
import CacheMgr from './CacheMgr';
import Redis from 'ioredis-mock';
import { CacheDelDirection, CacheGetType, CacheScope } from '../utils/globals';
import CacheMgr from './CacheMgr';
const log = debug('nc:cache');
export default class RedisMockCacheMgr extends CacheMgr {

36
packages/nocodb/src/lib/controllers/apiDocs/index.ts

@ -0,0 +1,36 @@
import { Router } from 'express';
import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
import { swaggerService } from '../../services';
import getSwaggerHtml from './swaggerHtml';
import getRedocHtml from './redocHtml';
async function swaggerJson(req, res) {
const swagger = await swaggerService.swaggerJson({
projectId: req.params.projectId,
siteUrl: req.ncSiteUrl,
});
res.json(swagger);
}
function swaggerHtml(_, res) {
res.send(getSwaggerHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
}
function redocHtml(_, res) {
res.send(getRedocHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
}
const router = Router({ mergeParams: true });
// todo: auth
router.get(
'/api/v1/db/meta/projects/:projectId/swagger.json',
ncMetaAclMw(swaggerJson, 'swaggerJson')
);
router.get('/api/v1/db/meta/projects/:projectId/swagger', swaggerHtml);
router.get('/api/v1/db/meta/projects/:projectId/redoc', redocHtml);
export default router;

0
packages/nocodb/src/lib/meta/api/swagger/redocHtml.ts → packages/nocodb/src/lib/controllers/apiDocs/redocHtml.ts

0
packages/nocodb/src/lib/meta/api/swagger/swaggerHtml.ts → packages/nocodb/src/lib/controllers/apiDocs/swaggerHtml.ts

50
packages/nocodb/src/lib/controllers/apiToken.ctl.ts

@ -0,0 +1,50 @@
import { Router } from 'express';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import { apiTokenService } from '../services';
import type { Request, Response } from 'express';
export async function apiTokenList(req: Request, res: Response) {
res.json(await apiTokenService.apiTokenList({ userId: req['user'].id }));
}
export async function apiTokenCreate(req: Request, res: Response) {
res.json(
await apiTokenService.apiTokenCreate({
tokenBody: req.body,
userId: req['user'].id,
})
);
}
export async function apiTokenDelete(req: Request, res: Response) {
res.json(
await apiTokenService.apiTokenDelete({
token: req.params.token,
user: req['user'],
})
);
}
// todo: add reset token api to regenerate token
// deprecated apis
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/projects/:projectId/api-tokens',
metaApiMetrics,
ncMetaAclMw(apiTokenList, 'apiTokenList')
);
router.post(
'/api/v1/db/meta/projects/:projectId/api-tokens',
metaApiMetrics,
ncMetaAclMw(apiTokenCreate, 'apiTokenCreate')
);
router.delete(
'/api/v1/db/meta/projects/:projectId/api-tokens/:token',
metaApiMetrics,
ncMetaAclMw(apiTokenDelete, 'apiTokenDelete')
);
export default router;

128
packages/nocodb/src/lib/controllers/attachment.ctl.ts

@ -0,0 +1,128 @@
import path from 'path';
import { Router } from 'express';
import multer from 'multer';
import { OrgUserRoles, ProjectRoles } from 'nocodb-sdk';
import Noco from '../Noco';
import { MetaTable } from '../utils/globals';
import extractProjectIdAndAuthenticate from '../meta/helpers/extractProjectIdAndAuthenticate';
import catchError, { NcError } from '../meta/helpers/catchError';
import { NC_ATTACHMENT_FIELD_SIZE } from '../constants';
import { getCacheMiddleware } from '../meta/api/helpers';
import { attachmentService } from '../services';
import type { Request, Response } from 'express';
const isUploadAllowedMw = async (req: Request, _res: Response, next: any) => {
if (!req['user']?.id) {
if (!req['user']?.isPublicBase) {
NcError.unauthorized('Unauthorized');
}
}
try {
// check user is super admin or creator
if (
req['user'].roles?.includes(OrgUserRoles.SUPER_ADMIN) ||
req['user'].roles?.includes(OrgUserRoles.CREATOR) ||
req['user'].roles?.includes(ProjectRoles.EDITOR) ||
// if viewer then check at-least one project have editor or higher role
// todo: cache
!!(await Noco.ncMeta
.knex(MetaTable.PROJECT_USERS)
.where(function () {
this.where('roles', ProjectRoles.OWNER);
this.orWhere('roles', ProjectRoles.CREATOR);
this.orWhere('roles', ProjectRoles.EDITOR);
})
.andWhere('fk_user_id', req['user'].id)
.first())
)
return next();
} catch {}
NcError.badRequest('Upload not allowed');
};
export async function upload(req: Request, res: Response) {
const attachments = await attachmentService.upload({
files: (req as any).files,
path: req.query?.path as string,
});
res.json(attachments);
}
export async function uploadViaURL(req: Request, res: Response) {
const attachments = await attachmentService.uploadViaURL({
urls: req.body,
path: req.query?.path as string,
});
res.json(attachments);
}
export async function fileRead(req, res) {
try {
const { img, type } = await attachmentService.fileRead({
path: path.join('nc', 'uploads', req.params?.[0]),
});
res.writeHead(200, { 'Content-Type': type });
res.end(img, 'binary');
} catch (e) {
console.log(e);
res.status(404).send('Not found');
}
}
const router = Router({ mergeParams: true });
router.get(
/^\/dl\/([^/]+)\/([^/]+)\/(.+)$/,
getCacheMiddleware(),
async (req, res) => {
try {
const { img, type } = await attachmentService.fileRead({
path: path.join(
'nc',
req.params[0],
req.params[1],
'uploads',
...req.params[2].split('/')
),
});
res.writeHead(200, { 'Content-Type': type });
res.end(img, 'binary');
} catch (e) {
res.status(404).send('Not found');
}
}
);
router.post(
'/api/v1/db/storage/upload',
multer({
storage: multer.diskStorage({}),
limits: {
fieldSize: NC_ATTACHMENT_FIELD_SIZE,
},
}).any(),
[
extractProjectIdAndAuthenticate,
catchError(isUploadAllowedMw),
catchError(upload),
]
);
router.post(
'/api/v1/db/storage/upload-by-url',
[
extractProjectIdAndAuthenticate,
catchError(isUploadAllowedMw),
catchError(uploadViaURL),
]
);
router.get(/^\/download\/(.+)$/, getCacheMiddleware(), catchError(fileRead));
export default router;

73
packages/nocodb/src/lib/controllers/audit.ctl.ts

@ -0,0 +1,73 @@
import { Router } from 'express';
import Audit from '../models/Audit';
import { PagedResponseImpl } from '../meta/helpers/PagedResponse';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { auditService } from '../services';
import type { Request, Response } from 'express';
export async function commentRow(req: Request<any, any>, res) {
res.json(
await auditService.commentRow({
rowId: req.params.rowId,
user: (req as any).user,
body: req.body,
})
);
}
export async function auditRowUpdate(req: Request<any, any>, res) {
res.json(
await auditService.auditRowUpdate({
rowId: req.params.rowId,
body: req.body,
})
);
}
export async function commentList(req: Request<any, any, any>, res) {
res.json(await Audit.commentsList(req.query));
}
export async function auditList(req: Request, res: Response) {
res.json(
new PagedResponseImpl(
await Audit.projectAuditList(req.params.projectId, req.query),
{
count: await Audit.projectAuditCount(req.params.projectId),
...req.query,
}
)
);
}
export async function commentsCount(req: Request<any, any, any>, res) {
res.json(
await Audit.commentsCount({
fk_model_id: req.query.fk_model_id as string,
ids: req.query.ids as string[],
})
);
}
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/audits/comments',
ncMetaAclMw(commentList, 'commentList')
);
router.post(
'/api/v1/db/meta/audits/comments',
ncMetaAclMw(commentRow, 'commentRow')
);
router.post(
'/api/v1/db/meta/audits/rows/:rowId/update',
ncMetaAclMw(auditRowUpdate, 'auditRowUpdate')
);
router.get(
'/api/v1/db/meta/audits/comments/count',
ncMetaAclMw(commentsCount, 'commentsCount')
);
router.get(
'/api/v1/db/meta/projects/:projectId/audits',
ncMetaAclMw(auditList, 'auditList')
);
export default router;

91
packages/nocodb/src/lib/controllers/base.ctl.ts

@ -0,0 +1,91 @@
import { PagedResponseImpl } from '../meta/helpers/PagedResponse';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import { baseService } from '../services';
import type Base from '../models/Base';
import type { BaseListType } from 'nocodb-sdk';
import type { Request, Response } from 'express';
async function baseGet(req: Request<any>, res: Response<Base>) {
const base = await baseService.baseGetWithConfig({
baseId: req.params.baseId,
});
res.json(base);
}
async function baseUpdate(req: Request<any, any, any>, res: Response<any>) {
const base = await baseService.baseUpdate({
baseId: req.params.baseId,
base: req.body,
projectId: req.params.projectId,
});
res.json(base);
}
async function baseList(
req: Request<any, any, any>,
res: Response<BaseListType>
) {
const bases = await baseService.baseList({
projectId: req.params.projectId,
});
res // todo: pagination
.json({
bases: new PagedResponseImpl(bases, {
count: bases.length,
limit: bases.length,
}),
});
}
export async function baseDelete(
req: Request<any, any, any>,
res: Response<any>
) {
const result = await baseService.baseDelete({
baseId: req.params.baseId,
});
res.json(result);
}
async function baseCreate(req: Request<any, any>, res) {
const base = await baseService.baseCreate({
projectId: req.params.projectId,
base: req.body,
});
res.json(base);
}
const initRoutes = (router) => {
router.get(
'/api/v1/db/meta/projects/:projectId/bases/:baseId',
metaApiMetrics,
ncMetaAclMw(baseGet, 'baseGet')
);
router.patch(
'/api/v1/db/meta/projects/:projectId/bases/:baseId',
metaApiMetrics,
ncMetaAclMw(baseUpdate, 'baseUpdate')
);
router.delete(
'/api/v1/db/meta/projects/:projectId/bases/:baseId',
metaApiMetrics,
ncMetaAclMw(baseDelete, 'baseDelete')
);
router.post(
'/api/v1/db/meta/projects/:projectId/bases',
metaApiMetrics,
ncMetaAclMw(baseCreate, 'baseCreate')
);
router.get(
'/api/v1/db/meta/projects/:projectId/bases',
metaApiMetrics,
ncMetaAclMw(baseList, 'baseList')
);
};
export default initRoutes;

8
packages/nocodb/src/lib/meta/api/cacheApis.ts → packages/nocodb/src/lib/controllers/cache.ctl.ts

@ -1,9 +1,9 @@
import catchError from '../helpers/catchError';
import NocoCache from '../../cache/NocoCache';
import { Router } from 'express';
import catchError from '../meta/helpers/catchError';
import { cacheService } from '../services';
export async function cacheGet(_, res) {
const data = await NocoCache.export();
const data = await cacheService.cacheGet();
res.set({
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="cache-export.json"`,
@ -12,7 +12,7 @@ export async function cacheGet(_, res) {
}
export async function cacheDelete(_, res) {
return res.json(await NocoCache.destroy());
return res.json(await cacheService.cacheDelete());
}
const router = Router();

78
packages/nocodb/src/lib/controllers/column.ctl.ts

@ -0,0 +1,78 @@
import { Router } from 'express';
import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { columnService } from '../services';
import type { ColumnReqType, TableType, UITypes } from 'nocodb-sdk';
import type { Request, Response } from 'express';
export async function columnGet(req: Request, res: Response) {
res.json(await columnService.columnGet({ columnId: req.params.columnId }));
}
export async function columnAdd(
req: Request<any, any, ColumnReqType & { uidt: UITypes }>,
res: Response<TableType>
) {
res.json(
await columnService.columnAdd({
tableId: req.params.tableId,
column: req.body,
req,
})
);
}
export async function columnSetAsPrimary(req: Request, res: Response) {
res.json(
await columnService.columnSetAsPrimary({ columnId: req.params.columnId })
);
}
export async function columnUpdate(req: Request, res: Response<TableType>) {
res.json(
await columnService.columnUpdate({
columnId: req.params.columnId,
column: req.body,
req,
})
);
}
export async function columnDelete(req: Request, res: Response<TableType>) {
res.json(
await columnService.columnDelete({ columnId: req.params.columnId, req })
);
}
const router = Router({ mergeParams: true });
router.post(
'/api/v1/db/meta/tables/:tableId/columns/',
metaApiMetrics,
ncMetaAclMw(columnAdd, 'columnAdd')
);
router.patch(
'/api/v1/db/meta/columns/:columnId',
metaApiMetrics,
ncMetaAclMw(columnUpdate, 'columnUpdate')
);
router.delete(
'/api/v1/db/meta/columns/:columnId',
metaApiMetrics,
ncMetaAclMw(columnDelete, 'columnDelete')
);
router.get(
'/api/v1/db/meta/columns/:columnId',
metaApiMetrics,
ncMetaAclMw(columnGet, 'columnGet')
);
router.post(
'/api/v1/db/meta/columns/:columnId/primary',
metaApiMetrics,
ncMetaAclMw(columnSetAsPrimary, 'columnSetAsPrimary')
);
export default router;

93
packages/nocodb/src/lib/controllers/dbData/bulkDataAlias.ctl.ts

@ -0,0 +1,93 @@
import { Router } from 'express';
import { bulkDataService } from '../../services';
import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
import apiMetrics from '../../meta/helpers/apiMetrics';
import type { Request, Response } from 'express';
async function bulkDataInsert(req: Request, res: Response) {
res.json(
await bulkDataService.bulkDataInsert({
body: req.body,
cookie: req,
projectName: req.params.projectName,
tableName: req.params.tableName,
})
);
}
async function bulkDataUpdate(req: Request, res: Response) {
res.json(
await bulkDataService.bulkDataUpdate({
body: req.body,
cookie: req,
projectName: req.params.projectName,
tableName: req.params.tableName,
})
);
}
// todo: Integrate with filterArrJson bulkDataUpdateAll
async function bulkDataUpdateAll(req: Request, res: Response) {
res.json(
await bulkDataService.bulkDataUpdateAll({
body: req.body,
cookie: req,
projectName: req.params.projectName,
tableName: req.params.tableName,
query: req.query,
})
);
}
async function bulkDataDelete(req: Request, res: Response) {
res.json(
await bulkDataService.bulkDataDelete({
body: req.body,
cookie: req,
projectName: req.params.projectName,
tableName: req.params.tableName,
})
);
}
// todo: Integrate with filterArrJson bulkDataDeleteAll
async function bulkDataDeleteAll(req: Request, res: Response) {
res.json(
await bulkDataService.bulkDataDeleteAll({
// cookie: req,
projectName: req.params.projectName,
tableName: req.params.tableName,
query: req.query,
})
);
}
const router = Router({ mergeParams: true });
router.post(
'/api/v1/db/data/bulk/:orgs/:projectName/:tableName',
apiMetrics,
ncMetaAclMw(bulkDataInsert, 'bulkDataInsert')
);
router.patch(
'/api/v1/db/data/bulk/:orgs/:projectName/:tableName',
apiMetrics,
ncMetaAclMw(bulkDataUpdate, 'bulkDataUpdate')
);
router.patch(
'/api/v1/db/data/bulk/:orgs/:projectName/:tableName/all',
apiMetrics,
ncMetaAclMw(bulkDataUpdateAll, 'bulkDataUpdateAll')
);
router.delete(
'/api/v1/db/data/bulk/:orgs/:projectName/:tableName',
apiMetrics,
ncMetaAclMw(bulkDataDelete, 'bulkDataDelete')
);
router.delete(
'/api/v1/db/data/bulk/:orgs/:projectName/:tableName/all',
apiMetrics,
ncMetaAclMw(bulkDataDeleteAll, 'bulkDataDeleteAll')
);
export default router;

193
packages/nocodb/src/lib/controllers/dbData/data.ctl.ts

@ -0,0 +1,193 @@
import { Router } from 'express';
import { dataService } from '../../services';
import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
import apiMetrics from '../../meta/helpers/apiMetrics';
import type { Request, Response } from 'express';
export async function dataList(req: Request, res: Response) {
res.json(
await dataService.dataListByViewId({
viewId: req.params.viewId,
query: req.query,
})
);
}
export async function mmList(req: Request, res: Response) {
res.json(
await dataService.mmList({
viewId: req.params.viewId,
colId: req.params.colId,
rowId: req.params.rowId,
query: req.query,
})
);
}
export async function mmExcludedList(req: Request, res: Response) {
res.json(
await dataService.mmExcludedList({
viewId: req.params.viewId,
colId: req.params.colId,
rowId: req.params.rowId,
query: req.query,
})
);
}
export async function hmExcludedList(req: Request, res: Response) {
res.json(
await dataService.hmExcludedList({
viewId: req.params.viewId,
colId: req.params.colId,
rowId: req.params.rowId,
query: req.query,
})
);
}
export async function btExcludedList(req: Request, res: Response) {
res.json(
await dataService.btExcludedList({
viewId: req.params.viewId,
colId: req.params.colId,
rowId: req.params.rowId,
query: req.query,
})
);
}
export async function hmList(req: Request, res: Response) {
res.json(
await dataService.hmList({
viewId: req.params.viewId,
colId: req.params.colId,
rowId: req.params.rowId,
query: req.query,
})
);
}
async function dataRead(req: Request, res: Response) {
res.json(
await dataService.dataReadByViewId({
viewId: req.params.viewId,
rowId: req.params.rowId,
query: req.query,
})
);
}
async function dataInsert(req: Request, res: Response) {
res.json(
await dataService.dataInsertByViewId({
viewId: req.params.viewId,
body: req.body,
cookie: req,
})
);
}
async function dataUpdate(req: Request, res: Response) {
res.json(
await dataService.dataUpdateByViewId({
viewId: req.params.viewId,
rowId: req.params.rowId,
body: req.body,
cookie: req,
})
);
}
async function dataDelete(req: Request, res: Response) {
res.json(
await dataService.dataDeleteByViewId({
viewId: req.params.viewId,
rowId: req.params.rowId,
cookie: req,
})
);
}
async function relationDataDelete(req, res) {
await dataService.relationDataDelete({
viewId: req.params.viewId,
colId: req.params.colId,
childId: req.params.childId,
rowId: req.params.rowId,
cookie: req,
});
res.json({ msg: 'success' });
}
//@ts-ignore
async function relationDataAdd(req, res) {
await dataService.relationDataAdd({
viewId: req.params.viewId,
colId: req.params.colId,
childId: req.params.childId,
rowId: req.params.rowId,
cookie: req,
});
res.json({ msg: 'success' });
}
const router = Router({ mergeParams: true });
router.get('/data/:viewId/', apiMetrics, ncMetaAclMw(dataList, 'dataList'));
router.post(
'/data/:viewId/',
apiMetrics,
ncMetaAclMw(dataInsert, 'dataInsert')
);
router.get(
'/data/:viewId/:rowId',
apiMetrics,
ncMetaAclMw(dataRead, 'dataRead')
);
router.patch(
'/data/:viewId/:rowId',
apiMetrics,
ncMetaAclMw(dataUpdate, 'dataUpdate')
);
router.delete(
'/data/:viewId/:rowId',
apiMetrics,
ncMetaAclMw(dataDelete, 'dataDelete')
);
router.get(
'/data/:viewId/:rowId/mm/:colId',
apiMetrics,
ncMetaAclMw(mmList, 'mmList')
);
router.get(
'/data/:viewId/:rowId/hm/:colId',
apiMetrics,
ncMetaAclMw(hmList, 'hmList')
);
router.get(
'/data/:viewId/:rowId/mm/:colId/exclude',
ncMetaAclMw(mmExcludedList, 'mmExcludedList')
);
router.get(
'/data/:viewId/:rowId/hm/:colId/exclude',
ncMetaAclMw(hmExcludedList, 'hmExcludedList')
);
router.get(
'/data/:viewId/:rowId/bt/:colId/exclude',
ncMetaAclMw(btExcludedList, 'btExcludedList')
);
router.post(
'/data/:viewId/:rowId/:relationType/:colId/:childId',
ncMetaAclMw(relationDataAdd, 'relationDataAdd')
);
router.delete(
'/data/:viewId/:rowId/:relationType/:colId/:childId',
ncMetaAclMw(relationDataDelete, 'relationDataDelete')
);
export default router;

260
packages/nocodb/src/lib/controllers/dbData/dataAlias.ctl.ts

@ -0,0 +1,260 @@
import { Router } from 'express';
import { dataService } from '../../services';
import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
import apiMetrics from '../../meta/helpers/apiMetrics';
import { parseHrtimeToSeconds } from '../../meta/api/helpers';
import type { Request, Response } from 'express';
// todo: Handle the error case where view doesnt belong to model
async function dataList(req: Request, res: Response) {
const startTime = process.hrtime();
const responseData = await dataService.dataList({
query: req.query,
projectName: req.params.projectName,
tableName: req.params.tableName,
viewName: req.params.viewName,
});
const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime));
res.setHeader('xc-db-response', elapsedSeconds);
res.json(responseData);
}
async function dataFindOne(req: Request, res: Response) {
res.json(
await dataService.dataFindOne({
query: req.query,
projectName: req.params.projectName,
tableName: req.params.tableName,
viewName: req.params.viewName,
})
);
}
async function dataGroupBy(req: Request, res: Response) {
res.json(
await dataService.dataGroupBy({
query: req.query,
projectName: req.params.projectName,
tableName: req.params.tableName,
viewName: req.params.viewName,
})
);
}
async function dataCount(req: Request, res: Response) {
const countResult = await dataService.dataCount({
query: req.query,
projectName: req.params.projectName,
tableName: req.params.tableName,
viewName: req.params.viewName,
});
res.json(countResult);
}
async function dataInsert(req: Request, res: Response) {
res.json(
await dataService.dataInsert({
projectName: req.params.projectName,
tableName: req.params.tableName,
viewName: req.params.viewName,
body: req.body,
cookie: req,
})
);
}
async function dataUpdate(req: Request, res: Response) {
res.json(
await dataService.dataUpdate({
projectName: req.params.projectName,
tableName: req.params.tableName,
viewName: req.params.viewName,
body: req.body,
cookie: req,
rowId: req.params.rowId,
})
);
}
async function dataDelete(req: Request, res: Response) {
res.json(
await dataService.dataDelete({
projectName: req.params.projectName,
tableName: req.params.tableName,
viewName: req.params.viewName,
cookie: req,
rowId: req.params.rowId,
})
);
}
async function dataRead(req: Request, res: Response) {
res.json(
await dataService.dataRead({
projectName: req.params.projectName,
tableName: req.params.tableName,
viewName: req.params.viewName,
rowId: req.params.rowId,
query: req.query,
})
);
}
async function dataExist(req: Request, res: Response) {
res.json(
await dataService.dataExist({
projectName: req.params.projectName,
tableName: req.params.tableName,
viewName: req.params.viewName,
rowId: req.params.rowId,
query: req.query,
})
);
}
// todo: Handle the error case where view doesnt belong to model
async function groupedDataList(req: Request, res: Response) {
const startTime = process.hrtime();
const groupedData = await dataService.groupedDataList({
projectName: req.params.projectName,
tableName: req.params.tableName,
viewName: req.params.viewName,
query: req.query,
columnId: req.params.columnId,
});
const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime));
res.setHeader('xc-db-response', elapsedSeconds);
res.json(groupedData);
}
const router = Router({ mergeParams: true });
// table data crud apis
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName',
apiMetrics,
ncMetaAclMw(dataList, 'dataList')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/find-one',
apiMetrics,
ncMetaAclMw(dataFindOne, 'dataFindOne')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/groupby',
apiMetrics,
ncMetaAclMw(dataGroupBy, 'dataGroupBy')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/group/:columnId',
apiMetrics,
ncMetaAclMw(groupedDataList, 'groupedDataList')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/exist',
apiMetrics,
ncMetaAclMw(dataExist, 'dataExist')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/count',
apiMetrics,
ncMetaAclMw(dataCount, 'dataCount')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/count',
apiMetrics,
ncMetaAclMw(dataCount, 'dataCount')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId',
apiMetrics,
ncMetaAclMw(dataRead, 'dataRead')
);
router.patch(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId',
apiMetrics,
ncMetaAclMw(dataUpdate, 'dataUpdate')
);
router.delete(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId',
apiMetrics,
ncMetaAclMw(dataDelete, 'dataDelete')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName',
apiMetrics,
ncMetaAclMw(dataList, 'dataList')
);
// table view data crud apis
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName',
apiMetrics,
ncMetaAclMw(dataList, 'dataList')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/find-one',
apiMetrics,
ncMetaAclMw(dataFindOne, 'dataFindOne')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/groupby',
apiMetrics,
ncMetaAclMw(dataGroupBy, 'dataGroupBy')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/group/:columnId',
apiMetrics,
ncMetaAclMw(groupedDataList, 'groupedDataList')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId/exist',
apiMetrics,
ncMetaAclMw(dataExist, 'dataExist')
);
router.post(
'/api/v1/db/data/:orgs/:projectName/:tableName',
apiMetrics,
ncMetaAclMw(dataInsert, 'dataInsert')
);
router.post(
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName',
apiMetrics,
ncMetaAclMw(dataInsert, 'dataInsert')
);
router.patch(
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId',
apiMetrics,
ncMetaAclMw(dataUpdate, 'dataUpdate')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId',
apiMetrics,
ncMetaAclMw(dataRead, 'dataRead')
);
router.delete(
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId',
apiMetrics,
ncMetaAclMw(dataDelete, 'dataDelete')
);
export default router;

16
packages/nocodb/src/lib/meta/api/dataApis/dataAliasExportApis.ts → packages/nocodb/src/lib/controllers/dbData/dataAliasExport.ctl.ts

@ -1,13 +1,11 @@
import { Request, Response, Router } from 'express';
import { Router } from 'express';
import * as XLSX from 'xlsx';
import ncMetaAclMw from '../../helpers/ncMetaAclMw';
import {
extractCsvData,
extractXlsxData,
getViewAndModelFromRequestByAliasOrId,
} from './helpers';
import apiMetrics from '../../helpers/apiMetrics';
import View from '../../../models/View';
import apiMetrics from '../../meta/helpers/apiMetrics';
import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
import { View } from '../../models';
import { extractCsvData, extractXlsxData } from '../../services/dbData/helpers';
import { getViewAndModelFromRequestByAliasOrId } from './helpers';
import type { Request, Response } from 'express';
async function excelDataExport(req: Request, res: Response) {
const { model, view } = await getViewAndModelFromRequestByAliasOrId(req);

138
packages/nocodb/src/lib/controllers/dbData/dataAliasNested.ctl.ts

@ -0,0 +1,138 @@
import { Router } from 'express';
import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
import apiMetrics from '../../meta/helpers/apiMetrics';
import { dataAliasNestedService } from '../../services';
import type { Request, Response } from 'express';
// todo: handle case where the given column is not ltar
export async function mmList(req: Request, res: Response) {
res.json(
await dataAliasNestedService.mmList({
query: req.query,
columnName: req.params.columnName,
rowId: req.params.rowId,
projectName: req.params.projectName,
tableName: req.params.tableName,
})
);
}
export async function mmExcludedList(req: Request, res: Response) {
res.json(
await dataAliasNestedService.mmExcludedList({
query: req.query,
columnName: req.params.columnName,
rowId: req.params.rowId,
projectName: req.params.projectName,
tableName: req.params.tableName,
})
);
}
export async function hmExcludedList(req: Request, res: Response) {
res.json(
await dataAliasNestedService.hmExcludedList({
query: req.query,
columnName: req.params.columnName,
rowId: req.params.rowId,
projectName: req.params.projectName,
tableName: req.params.tableName,
})
);
}
export async function btExcludedList(req: Request, res: Response) {
res.json(
await dataAliasNestedService.btExcludedList({
query: req.query,
columnName: req.params.columnName,
rowId: req.params.rowId,
projectName: req.params.projectName,
tableName: req.params.tableName,
})
);
}
// todo: handle case where the given column is not ltar
export async function hmList(req: Request, res: Response) {
res.json(
await dataAliasNestedService.hmList({
query: req.query,
columnName: req.params.columnName,
rowId: req.params.rowId,
projectName: req.params.projectName,
tableName: req.params.tableName,
})
);
}
//@ts-ignore
async function relationDataRemove(req, res) {
await dataAliasNestedService.relationDataRemove({
columnName: req.params.columnName,
rowId: req.params.rowId,
projectName: req.params.projectName,
tableName: req.params.tableName,
cookie: req,
refRowId: req.params.refRowId,
});
res.json({ msg: 'success' });
}
//@ts-ignore
// todo: Give proper error message when reference row is already related and handle duplicate ref row id in hm
async function relationDataAdd(req, res) {
await dataAliasNestedService.relationDataAdd({
columnName: req.params.columnName,
rowId: req.params.rowId,
projectName: req.params.projectName,
tableName: req.params.tableName,
cookie: req,
refRowId: req.params.refRowId,
});
res.json({ msg: 'success' });
}
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/mm/:columnName/exclude',
apiMetrics,
ncMetaAclMw(mmExcludedList, 'mmExcludedList')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/hm/:columnName/exclude',
apiMetrics,
ncMetaAclMw(hmExcludedList, 'hmExcludedList')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/bt/:columnName/exclude',
apiMetrics,
ncMetaAclMw(btExcludedList, 'btExcludedList')
);
router.post(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/:relationType/:columnName/:refRowId',
apiMetrics,
ncMetaAclMw(relationDataAdd, 'relationDataAdd')
);
router.delete(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/:relationType/:columnName/:refRowId',
apiMetrics,
ncMetaAclMw(relationDataRemove, 'relationDataRemove')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/mm/:columnName',
apiMetrics,
ncMetaAclMw(mmList, 'mmList')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/hm/:columnName',
apiMetrics,
ncMetaAclMw(hmList, 'hmList')
);
export default router;

270
packages/nocodb/src/lib/controllers/dbData/helpers.ts

@ -0,0 +1,270 @@
import { isSystemColumn, UITypes } from 'nocodb-sdk';
import * as XLSX from 'xlsx';
import papaparse from 'papaparse';
import { NcError } from '../../meta/helpers/catchError';
import Project from '../../models/Project';
import Model from '../../models/Model';
import View from '../../models/View';
import Base from '../../models/Base';
import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import Column from '../../models/Column';
import { dataService } from '../../services';
import type LookupColumn from '../../models/LookupColumn';
import type LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';
import type { Request } from 'express';
export async function getViewAndModelFromRequestByAliasOrId(
req:
| Request<{ projectName: string; tableName: string; viewName?: string }>
| Request
) {
const project = await Project.getWithInfoByTitleOrId(req.params.projectName);
const model = await Model.getByAliasOrId({
project_id: project.id,
aliasOrId: req.params.tableName,
});
const view =
req.params.viewName &&
(await View.getByTitleOrId({
titleOrId: req.params.viewName,
fk_model_id: model.id,
}));
if (!model) NcError.notFound('Table not found');
return { model, view };
}
export async function extractXlsxData(param: {
view: View;
query: any;
siteUrl: string;
}) {
const { view, query, siteUrl } = param;
const base = await Base.get(view.base_id);
await view.getModelWithInfo();
await view.getColumns();
view.model.columns = view.columns
.filter((c) => c.show)
.map(
(c) =>
new Column({ ...c, ...view.model.columnsById[c.fk_column_id] } as any)
)
.filter((column) => !isSystemColumn(column) || view.show_system_fields);
const baseModel = await Model.getBaseModelSQL({
id: view.model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
const { offset, dbRows, elapsed } = await dataService.getDbRows({
baseModel,
view,
query,
siteUrl,
});
const fields = query.fields as string[];
const data = XLSX.utils.json_to_sheet(dbRows, { header: fields });
return { offset, dbRows, elapsed, data };
}
export async function extractCsvData(view: View, req: Request) {
const base = await Base.get(view.base_id);
const fields = req.query.fields;
await view.getModelWithInfo();
await view.getColumns();
view.model.columns = view.columns
.filter((c) => c.show)
.map(
(c) =>
new Column({ ...c, ...view.model.columnsById[c.fk_column_id] } as any)
)
.filter((column) => !isSystemColumn(column) || view.show_system_fields);
const baseModel = await Model.getBaseModelSQL({
id: view.model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
const { offset, dbRows, elapsed } = await dataService.getDbRows({
baseModel,
view,
query: req.query,
siteUrl: (req as any).ncSiteUrl,
});
const data = papaparse.unparse(
{
fields: view.model.columns
.sort((c1, c2) =>
Array.isArray(fields)
? fields.indexOf(c1.title as any) - fields.indexOf(c2.title as any)
: 0
)
.filter(
(c) =>
!fields || !Array.isArray(fields) || fields.includes(c.title as any)
)
.map((c) => c.title),
data: dbRows,
},
{
escapeFormulae: true,
}
);
return { offset, dbRows, elapsed, data };
}
//
// async function getDbRows(baseModel, view: View, req: Request) {
// let offset = +req.query.offset || 0;
// const limit = 100;
// // const size = +process.env.NC_EXPORT_MAX_SIZE || 1024;
// const timeout = +process.env.NC_EXPORT_MAX_TIMEOUT || 5000;
// const dbRows = [];
// const startTime = process.hrtime();
// let elapsed, temp;
//
// const listArgs: any = { ...req.query };
// try {
// listArgs.filterArr = JSON.parse(listArgs.filterArrJson);
// } catch (e) {}
// try {
// listArgs.sortArr = JSON.parse(listArgs.sortArrJson);
// } catch (e) {}
//
// for (
// elapsed = 0;
// elapsed < timeout;
// offset += limit,
// temp = process.hrtime(startTime),
// elapsed = temp[0] * 1000 + temp[1] / 1000000
// ) {
// const rows = await nocoExecute(
// await getAst({
// query: req.query,
// includePkByDefault: false,
// model: view.model,
// view,
// }),
// await baseModel.list({ ...listArgs, offset, limit }),
// {},
// req.query
// );
//
// if (!rows?.length) {
// offset = -1;
// break;
// }
//
// for (const row of rows) {
// const dbRow = { ...row };
//
// for (const column of view.model.columns) {
// if (isSystemColumn(column) && !view.show_system_fields) continue;
// dbRow[column.title] = await serializeCellValue({
// value: row[column.title],
// column,
// siteUrl: req['ncSiteUrl'],
// });
// }
// dbRows.push(dbRow);
// }
// }
// return { offset, dbRows, elapsed };
// }
export async function serializeCellValue({
value,
column,
siteUrl,
}: {
column?: Column;
value: any;
siteUrl: string;
}) {
if (!column) {
return value;
}
if (!value) return value;
switch (column?.uidt) {
case UITypes.Attachment: {
let data = value;
try {
if (typeof value === 'string') {
data = JSON.parse(value);
}
} catch {}
return (data || []).map(
(attachment) =>
`${encodeURI(attachment.title)}(${encodeURI(
attachment.path ? `${siteUrl}/${attachment.path}` : attachment.url
)})`
);
}
case UITypes.Lookup:
{
const colOptions = await column.getColOptions<LookupColumn>();
const lookupColumn = await colOptions.getLookupColumn();
return (
await Promise.all(
[...(Array.isArray(value) ? value : [value])].map(async (v) =>
serializeCellValue({
value: v,
column: lookupColumn,
siteUrl,
})
)
)
).join(', ');
}
break;
case UITypes.LinkToAnotherRecord:
{
const colOptions =
await column.getColOptions<LinkToAnotherRecordColumn>();
const relatedModel = await colOptions.getRelatedTable();
await relatedModel.getColumns();
return [...(Array.isArray(value) ? value : [value])]
.map((v) => {
return v[relatedModel.displayValue?.title];
})
.join(', ');
}
break;
default:
if (value && typeof value === 'object') {
return JSON.stringify(value);
}
return value;
}
}
export async function getColumnByIdOrName(
columnNameOrId: string,
model: Model
) {
const column = (await model.getColumns()).find(
(c) =>
c.title === columnNameOrId ||
c.id === columnNameOrId ||
c.column_name === columnNameOrId
);
if (!column)
NcError.notFound(`Column with id/name '${columnNameOrId}' is not found`);
return column;
}

15
packages/nocodb/src/lib/controllers/dbData/index.ts

@ -0,0 +1,15 @@
import dataController from './data.ctl';
import oldDataController from './oldData.ctl';
import dataAliasController from './dataAlias.ctl';
import bulkDataAliasController from './bulkDataAlias.ctl';
import dataAliasNestedController from './dataAliasNested.ctl';
import dataAliasExportController from './dataAliasExport.ctl';
export {
dataController,
oldDataController,
dataAliasController,
bulkDataAliasController,
dataAliasNestedController,
dataAliasExportController,
};

33
packages/nocodb/src/lib/meta/api/dataApis/oldDataApis.ts → packages/nocodb/src/lib/controllers/dbData/oldData.ctl.ts

@ -1,14 +1,15 @@
import { Request, Response, Router } from 'express';
import Model from '../../../models/Model';
import { Router } from 'express';
import { nocoExecute } from 'nc-help';
import Base from '../../../models/Base';
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import View from '../../../models/View';
import ncMetaAclMw from '../../helpers/ncMetaAclMw';
import Project from '../../../models/Project';
import { NcError } from '../../helpers/catchError';
import apiMetrics from '../../helpers/apiMetrics';
import getAst from '../../../db/sql-data-mapper/lib/sql/helpers/getAst';
import Model from '../../models/Model';
import Base from '../../models/Base';
import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import View from '../../models/View';
import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
import Project from '../../models/Project';
import { NcError } from '../../meta/helpers/catchError';
import apiMetrics from '../../meta/helpers/apiMetrics';
import getAst from '../../db/sql-data-mapper/lib/sql/helpers/getAst';
import type { Request, Response } from 'express';
export async function dataList(req: Request, res: Response) {
const { model, view } = await getViewAndModelFromRequest(req);
@ -17,7 +18,7 @@ export async function dataList(req: Request, res: Response) {
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: NcConnectionMgrv2.get(base),
dbDriver: await NcConnectionMgrv2.get(base),
});
const requestObj = await getAst({
@ -50,7 +51,7 @@ export async function dataCount(req: Request, res: Response) {
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: NcConnectionMgrv2.get(base),
dbDriver: await NcConnectionMgrv2.get(base),
});
const listArgs: any = { ...req.query };
@ -73,7 +74,7 @@ async function dataInsert(req: Request, res: Response) {
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: NcConnectionMgrv2.get(base),
dbDriver: await NcConnectionMgrv2.get(base),
});
res.json(await baseModel.insert(req.body, null, req));
@ -86,7 +87,7 @@ async function dataUpdate(req: Request, res: Response) {
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view.id,
dbDriver: NcConnectionMgrv2.get(base),
dbDriver: await NcConnectionMgrv2.get(base),
});
res.json(await baseModel.updateByPk(req.params.rowId, req.body, null, req));
@ -98,7 +99,7 @@ async function dataDelete(req: Request, res: Response) {
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view.id,
dbDriver: NcConnectionMgrv2.get(base),
dbDriver: await NcConnectionMgrv2.get(base),
});
res.json(await baseModel.delByPk(req.params.rowId, null, req));
@ -128,7 +129,7 @@ async function dataRead(req: Request, res: Response) {
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: NcConnectionMgrv2.get(base),
dbDriver: await NcConnectionMgrv2.get(base),
});
res.json(

9
packages/nocodb/src/lib/meta/api/exportApis.ts → packages/nocodb/src/lib/controllers/export.ctl.ts

@ -1,7 +1,8 @@
import { Request, Response, Router } from 'express';
import View from '../../models/View';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { extractCsvData } from './dataApis/helpers';
import { Router } from 'express';
import View from '../models/View';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { extractCsvData } from './dbData/helpers';
import type { Request, Response } from 'express';
async function exportCsv(req: Request, res: Response) {
const view = await View.get(req.params.viewId);

116
packages/nocodb/src/lib/controllers/filter.ctl.ts

@ -0,0 +1,116 @@
import { Router } from 'express';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import { filterService } from '../services';
import type { FilterReqType } from 'nocodb-sdk';
import type { Request, Response } from 'express';
// @ts-ignore
export async function filterGet(req: Request, res: Response) {
res.json(await filterService.filterGet({ filterId: req.params.filterId }));
}
// @ts-ignore
export async function filterList(req: Request, res: Response) {
res.json(
await filterService.filterList({
viewId: req.params.viewId,
})
);
}
// @ts-ignore
export async function filterChildrenRead(req: Request, res: Response) {
const filter = await filterService.filterChildrenList({
filterId: req.params.filterParentId,
});
res.json(filter);
}
export async function filterCreate(req: Request<any, any, FilterReqType>, res) {
const filter = await filterService.filterCreate({
filter: req.body,
viewId: req.params.viewId,
});
res.json(filter);
}
export async function filterUpdate(req, res) {
const filter = await filterService.filterUpdate({
filterId: req.params.filterId,
filter: req.body,
});
res.json(filter);
}
export async function filterDelete(req: Request, res: Response) {
const filter = await filterService.filterDelete({
filterId: req.params.filterId,
});
res.json(filter);
}
export async function hookFilterList(req: Request, res: Response) {
res.json(
await filterService.hookFilterList({
hookId: req.params.hookId,
})
);
}
export async function hookFilterCreate(
req: Request<any, any, FilterReqType>,
res
) {
const filter = await filterService.hookFilterCreate({
filter: req.body,
hookId: req.params.hookId,
});
res.json(filter);
}
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/views/:viewId/filters',
metaApiMetrics,
ncMetaAclMw(filterList, 'filterList')
);
router.post(
'/api/v1/db/meta/views/:viewId/filters',
metaApiMetrics,
ncMetaAclMw(filterCreate, 'filterCreate')
);
router.get(
'/api/v1/db/meta/hooks/:hookId/filters',
ncMetaAclMw(hookFilterList, 'filterList')
);
router.post(
'/api/v1/db/meta/hooks/:hookId/filters',
metaApiMetrics,
ncMetaAclMw(hookFilterCreate, 'filterCreate')
);
router.get(
'/api/v1/db/meta/filters/:filterId',
metaApiMetrics,
ncMetaAclMw(filterGet, 'filterGet')
);
router.patch(
'/api/v1/db/meta/filters/:filterId',
metaApiMetrics,
ncMetaAclMw(filterUpdate, 'filterUpdate')
);
router.delete(
'/api/v1/db/meta/filters/:filterId',
metaApiMetrics,
ncMetaAclMw(filterDelete, 'filterDelete')
);
router.get(
'/api/v1/db/meta/filters/:filterParentId/children',
metaApiMetrics,
ncMetaAclMw(filterChildrenRead, 'filterChildrenRead')
);
export default router;

99
packages/nocodb/src/lib/controllers/hook.ctl.ts

@ -0,0 +1,99 @@
import { Router } from 'express';
import catchError from '../meta/helpers/catchError';
import { PagedResponseImpl } from '../meta/helpers/PagedResponse';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import { hookService } from '../services';
import type { HookListType, HookType } from 'nocodb-sdk';
import type { Request, Response } from 'express';
export async function hookList(
req: Request<any, any, any>,
res: Response<HookListType>
) {
// todo: pagination
res.json(
new PagedResponseImpl(
await hookService.hookList({ tableId: req.params.tableId })
)
);
}
export async function hookCreate(
req: Request<any, HookType>,
res: Response<HookType>
) {
const hook = await hookService.hookCreate({
hook: req.body,
tableId: req.params.tableId,
});
res.json(hook);
}
export async function hookDelete(
req: Request<any, HookType>,
res: Response<any>
) {
res.json(await hookService.hookDelete({ hookId: req.params.hookId }));
}
export async function hookUpdate(
req: Request<any, HookType>,
res: Response<HookType>
) {
res.json(
await hookService.hookUpdate({ hookId: req.params.hookId, hook: req.body })
);
}
export async function hookTest(req: Request<any, any>, res: Response) {
await hookService.hookTest({
hookTest: req.body,
tableId: req.params.tableId,
});
res.json({ msg: 'Success' });
}
export async function tableSampleData(req: Request, res: Response) {
res // todo: pagination
.json(
await hookService.tableSampleData({
tableId: req.params.tableId,
// todo: replace any with type
operation: req.params.operation as any,
})
);
}
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/tables/:tableId/hooks',
metaApiMetrics,
ncMetaAclMw(hookList, 'hookList')
);
router.post(
'/api/v1/db/meta/tables/:tableId/hooks/test',
metaApiMetrics,
ncMetaAclMw(hookTest, 'hookTest')
);
router.post(
'/api/v1/db/meta/tables/:tableId/hooks',
metaApiMetrics,
ncMetaAclMw(hookCreate, 'hookCreate')
);
router.delete(
'/api/v1/db/meta/hooks/:hookId',
metaApiMetrics,
ncMetaAclMw(hookDelete, 'hookDelete')
);
router.patch(
'/api/v1/db/meta/hooks/:hookId',
metaApiMetrics,
ncMetaAclMw(hookUpdate, 'hookUpdate')
);
router.get(
'/api/v1/db/meta/tables/:tableId/hooks/samplePayload/:operation',
metaApiMetrics,
catchError(tableSampleData)
);
export default router;

91
packages/nocodb/src/lib/controllers/hookFilter.ctl.ts

@ -0,0 +1,91 @@
import { Router } from 'express';
import { T } from 'nc-help';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import { hookFilterService } from '../services';
import type { Request, Response } from 'express';
export async function filterGet(req: Request, res: Response) {
const filter = await hookFilterService.filterGet({
hookId: req.params.hookId,
});
res.json(filter);
}
export async function filterList(req: Request, res: Response) {
const filter = await hookFilterService.filterList({
hookId: req.params.hookId,
});
res.json(filter);
}
export async function filterChildrenRead(req: Request, res: Response) {
const filter = await hookFilterService.filterChildrenRead({
hookId: req.params.hookId,
filterParentId: req.params.filterParentId,
});
res.json(filter);
}
export async function filterCreate(req: Request<any, any>, res) {
const filter = await hookFilterService.filterCreate({
filter: req.body,
hookId: req.params.hookId,
});
res.json(filter);
}
export async function filterUpdate(req, res) {
const filter = await hookFilterService.filterUpdate({
filterId: req.params.filterId,
filter: req.body,
hookId: req.params.hookId,
});
res.json(filter);
}
export async function filterDelete(req: Request, res: Response) {
const filter = await hookFilterService.filterDelete({
filterId: req.params.filterId,
});
T.emit('evt', { evt_type: 'hookFilter:deleted' });
res.json(filter);
}
const router = Router({ mergeParams: true });
router.get(
'/hooks/:hookId/filters/',
metaApiMetrics,
ncMetaAclMw(filterList, 'filterList')
);
router.post(
'/hooks/:hookId/filters/',
metaApiMetrics,
ncMetaAclMw(filterCreate, 'filterCreate')
);
router.get(
'/hooks/:hookId/filters/:filterId',
metaApiMetrics,
ncMetaAclMw(filterGet, 'filterGet')
);
router.patch(
'/hooks/:hookId/filters/:filterId',
metaApiMetrics,
ncMetaAclMw(filterUpdate, 'filterUpdate')
);
router.delete(
'/hooks/:hookId/filters/:filterId',
metaApiMetrics,
ncMetaAclMw(filterDelete, 'filterDelete')
);
router.get(
'/hooks/:hookId/filters/:filterParentId/children',
metaApiMetrics,
ncMetaAclMw(filterChildrenRead, 'filterChildrenRead')
);
export default router;

54
packages/nocodb/src/lib/controllers/metaDiff.ctl.ts

@ -0,0 +1,54 @@
import { Router } from 'express';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import { metaDiffService } from '../services';
export async function metaDiff(req, res) {
res.json(await metaDiffService.metaDiff({ projectId: req.params.projectId }));
}
export async function baseMetaDiff(req, res) {
res.json(
await metaDiffService.baseMetaDiff({
baseId: req.params.baseId,
projectId: req.params.projectId,
})
);
}
export async function metaDiffSync(req, res) {
await metaDiffService.metaDiffSync({ projectId: req.params.projectId });
res.json({ msg: 'success' });
}
export async function baseMetaDiffSync(req, res) {
await metaDiffService.baseMetaDiffSync({
projectId: req.params.projectId,
baseId: req.params.baseId,
});
res.json({ msg: 'success' });
}
const router = Router();
router.get(
'/api/v1/db/meta/projects/:projectId/meta-diff',
metaApiMetrics,
ncMetaAclMw(metaDiff, 'metaDiff')
);
router.post(
'/api/v1/db/meta/projects/:projectId/meta-diff',
metaApiMetrics,
ncMetaAclMw(metaDiffSync, 'metaDiffSync')
);
router.get(
'/api/v1/db/meta/projects/:projectId/meta-diff/:baseId',
metaApiMetrics,
ncMetaAclMw(baseMetaDiff, 'baseMetaDiff')
);
router.post(
'/api/v1/db/meta/projects/:projectId/meta-diff/:baseId',
metaApiMetrics,
ncMetaAclMw(baseMetaDiffSync, 'baseMetaDiffSync')
);
export default router;

34
packages/nocodb/src/lib/controllers/modelVisibility.ctl.ts

@ -0,0 +1,34 @@
import { Router } from 'express';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import { modelVisibilityService } from '../services';
async function xcVisibilityMetaSetAll(req, res) {
await modelVisibilityService.xcVisibilityMetaSetAll({
visibilityRule: req.body,
projectId: req.params.projectId,
});
res.json({ msg: 'success' });
}
const router = Router({ mergeParams: true });
router.get(
'/api/v1/db/meta/projects/:projectId/visibility-rules',
metaApiMetrics,
ncMetaAclMw(async (req, res) => {
res.json(
await modelVisibilityService.xcVisibilityMetaGet({
projectId: req.params.projectId,
includeM2M:
req.query.includeM2M === true || req.query.includeM2M === 'true',
})
);
}, 'modelVisibilityList')
);
router.post(
'/api/v1/db/meta/projects/:projectId/visibility-rules',
metaApiMetrics,
ncMetaAclMw(xcVisibilityMetaSetAll, 'modelVisibilitySet')
);
export default router;

17
packages/nocodb/src/lib/meta/api/orgLicenseApis.ts → packages/nocodb/src/lib/controllers/orgLicense.ctl.ts

@ -1,21 +1,15 @@
import { Router } from 'express';
import { OrgUserRoles } from 'nocodb-sdk';
import { NC_LICENSE_KEY } from '../../constants';
import Store from '../../models/Store';
import Noco from '../../Noco';
import { metaApiMetrics } from '../helpers/apiMetrics';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { getAjvValidatorMw } from './helpers';
import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { orgLicenseService } from '../services';
async function licenseGet(_req, res) {
const license = await Store.get(NC_LICENSE_KEY);
res.json({ key: license?.value });
res.json(await orgLicenseService.licenseGet());
}
async function licenseSet(req, res) {
await Store.saveOrUpdate({ value: req.body.key, key: NC_LICENSE_KEY });
await Noco.loadEEState();
await orgLicenseService.licenseSet({ key: req.body.key });
res.json({ msg: 'License key saved' });
}
@ -31,7 +25,6 @@ router.get(
router.post(
'/api/v1/license',
metaApiMetrics,
getAjvValidatorMw('swagger.json#/components/schemas/LicenseReq'),
ncMetaAclMw(licenseSet, 'licenseSet', {
allowedRoles: [OrgUserRoles.SUPER_ADMIN],
blockApiTokenAccess: true,

64
packages/nocodb/src/lib/controllers/orgToken.ctl.ts

@ -0,0 +1,64 @@
import { Router } from 'express';
import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import { getConditionalHandler } from '../meta/helpers/getHandler';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { orgTokenService, orgTokenServiceEE } from '../services';
import type { Request, Response } from 'express';
async function apiTokenList(req, res) {
res.json(
await getConditionalHandler(
orgTokenService.apiTokenList,
orgTokenServiceEE.apiTokenListEE
)({
query: req.query,
user: req['user'],
})
);
}
export async function apiTokenCreate(req: Request, res: Response) {
res.json(
await orgTokenService.apiTokenCreate({
apiToken: req.body,
user: req['user'],
})
);
}
export async function apiTokenDelete(req: Request, res: Response) {
res.json(
await orgTokenService.apiTokenDelete({
token: req.params.token,
user: req['user'],
})
);
}
const router = Router({ mergeParams: true });
router.get(
'/api/v1/tokens',
metaApiMetrics,
ncMetaAclMw(apiTokenList, 'apiTokenList', {
// allowedRoles: [OrgUserRoles.SUPER],
blockApiTokenAccess: true,
})
);
router.post(
'/api/v1/tokens',
metaApiMetrics,
ncMetaAclMw(apiTokenCreate, 'apiTokenCreate', {
// allowedRoles: [OrgUserRoles.SUPER],
blockApiTokenAccess: true,
})
);
router.delete(
'/api/v1/tokens/:token',
metaApiMetrics,
ncMetaAclMw(apiTokenDelete, 'apiTokenDelete', {
// allowedRoles: [OrgUserRoles.SUPER],
blockApiTokenAccess: true,
})
);
export default router;

154
packages/nocodb/src/lib/controllers/orgUser.ctl.ts

@ -0,0 +1,154 @@
import { Router } from 'express';
import { OrgUserRoles } from 'nocodb-sdk';
import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { orgUserService } from '../services';
async function userList(req, res) {
res.json(
await orgUserService.userList({
query: req.query,
})
);
}
async function userUpdate(req, res) {
res.json(
await orgUserService.userUpdate({
user: req.body,
userId: req.params.userId,
})
);
}
async function userDelete(req, res) {
await orgUserService.userDelete({
userId: req.params.userId,
});
res.json({ msg: 'success' });
}
async function userAdd(req, res) {
const result = await orgUserService.userAdd({
user: req.body,
req,
projectId: req.params.projectId,
});
res.json(result);
}
async function userSettings(_req, res): Promise<any> {
await orgUserService.userSettings({});
res.json({});
}
async function userInviteResend(req, res): Promise<any> {
await orgUserService.userInviteResend({
userId: req.params.userId,
req,
});
res.json({ msg: 'success' });
}
async function generateResetUrl(req, res) {
const result = await orgUserService.generateResetUrl({
siteUrl: req.ncSiteUrl,
userId: req.params.userId,
});
res.json(result);
}
async function appSettingsGet(_req, res) {
const settings = await orgUserService.appSettingsGet();
res.json(settings);
}
async function appSettingsSet(req, res) {
await orgUserService.appSettingsSet({
settings: req.body,
});
res.json({ msg: 'Settings saved' });
}
const router = Router({ mergeParams: true });
router.get(
'/api/v1/users',
metaApiMetrics,
ncMetaAclMw(userList, 'userList', {
allowedRoles: [OrgUserRoles.SUPER_ADMIN],
blockApiTokenAccess: true,
})
);
router.patch(
'/api/v1/users/:userId',
metaApiMetrics,
ncMetaAclMw(userUpdate, 'userUpdate', {
allowedRoles: [OrgUserRoles.SUPER_ADMIN],
blockApiTokenAccess: true,
})
);
router.delete(
'/api/v1/users/:userId',
metaApiMetrics,
ncMetaAclMw(userDelete, 'userDelete', {
allowedRoles: [OrgUserRoles.SUPER_ADMIN],
blockApiTokenAccess: true,
})
);
router.post(
'/api/v1/users',
metaApiMetrics,
ncMetaAclMw(userAdd, 'userAdd', {
allowedRoles: [OrgUserRoles.SUPER_ADMIN],
blockApiTokenAccess: true,
})
);
router.post(
'/api/v1/users/settings',
metaApiMetrics,
ncMetaAclMw(userSettings, 'userSettings', {
allowedRoles: [OrgUserRoles.SUPER_ADMIN],
blockApiTokenAccess: true,
})
);
router.post(
'/api/v1/users/:userId/resend-invite',
metaApiMetrics,
ncMetaAclMw(userInviteResend, 'userInviteResend', {
allowedRoles: [OrgUserRoles.SUPER_ADMIN],
blockApiTokenAccess: true,
})
);
router.post(
'/api/v1/users/:userId/generate-reset-url',
metaApiMetrics,
ncMetaAclMw(generateResetUrl, 'generateResetUrl', {
allowedRoles: [OrgUserRoles.SUPER_ADMIN],
blockApiTokenAccess: true,
})
);
router.get(
'/api/v1/app-settings',
metaApiMetrics,
ncMetaAclMw(appSettingsGet, 'appSettingsGet', {
allowedRoles: [OrgUserRoles.SUPER_ADMIN],
blockApiTokenAccess: true,
})
);
router.post(
'/api/v1/app-settings',
metaApiMetrics,
ncMetaAclMw(appSettingsSet, 'appSettingsSet', {
allowedRoles: [OrgUserRoles.SUPER_ADMIN],
blockApiTokenAccess: true,
})
);
export default router;

37
packages/nocodb/src/lib/meta/api/pluginApis.ts → packages/nocodb/src/lib/controllers/plugin.ctl.ts

@ -1,38 +1,36 @@
import { Request, Response, Router } from 'express';
import { Tele } from 'nc-help';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import Plugin from '../../models/Plugin';
import { PluginType } from 'nocodb-sdk';
import NcPluginMgrv2 from '../helpers/NcPluginMgrv2';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { metaApiMetrics } from '../helpers/apiMetrics';
import { getAjvValidatorMw } from './helpers';
import { Router } from 'express';
import { PagedResponseImpl } from '../meta/helpers/PagedResponse';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import { pluginService } from '../services';
import type { PluginType } from 'nocodb-sdk';
import type { Request, Response } from 'express';
export async function pluginList(_req: Request, res: Response) {
res.json(new PagedResponseImpl(await Plugin.list()));
res.json(new PagedResponseImpl(await pluginService.pluginList()));
}
export async function pluginTest(req: Request<any, any>, res: Response) {
Tele.emit('evt', { evt_type: 'plugin:tested' });
res.json(await NcPluginMgrv2.test(req.body));
res.json(await pluginService.pluginTest({ body: req.body }));
}
export async function pluginRead(req: Request, res: Response) {
res.json(await Plugin.get(req.params.pluginId));
res.json(await pluginService.pluginRead({ pluginId: req.params.pluginId }));
}
export async function pluginUpdate(
req: Request<any, any, PluginType>,
res: Response
) {
const plugin = await Plugin.update(req.params.pluginId, req.body);
Tele.emit('evt', {
evt_type: plugin.active ? 'plugin:installed' : 'plugin:uninstalled',
title: plugin.title,
const plugin = await pluginService.pluginUpdate({
pluginId: req.params.pluginId,
plugin: req.body,
});
res.json(plugin);
}
export async function isPluginActive(req: Request, res: Response) {
res.json(await Plugin.isPluginActive(req.params.pluginTitle));
res.json(
await pluginService.isPluginActive({ pluginTitle: req.params.pluginTitle })
);
}
const router = Router({ mergeParams: true });
@ -44,8 +42,6 @@ router.get(
router.post(
'/api/v1/db/meta/plugins/test',
metaApiMetrics,
getAjvValidatorMw('swagger.json#/components/schemas/PluginTestReq'),
ncMetaAclMw(pluginTest, 'pluginTest')
);
router.get(
@ -56,7 +52,6 @@ router.get(
router.patch(
'/api/v1/db/meta/plugins/:pluginId',
metaApiMetrics,
getAjvValidatorMw('swagger.json#/components/schemas/PluginReq'),
ncMetaAclMw(pluginUpdate, 'pluginUpdate')
);
router.get(

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

Loading…
Cancel
Save