Browse Source

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

pull/5114/head
Daniel Spaude 2 years 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: labels:
{{- include "nocodb.selectorLabels" . | nindent 8 }} {{- include "nocodb.selectorLabels" . | nindent 8 }}
spec: spec:
accessModes:
- ReadWriteMany
resources: resources:
requests: requests:
storage: {{ .Values.storage.size }} storage: {{ .Values.storage.size }}
storageClassName: {{ .Values.storage.storageClassName }} storageClassName: {{ .Values.storage.storageClassName }}
accessModes:
{{- default (toYaml .Values.storage.accessModes) "- ReadWriteMany" | nindent 4 }}
volumeMode: Filesystem 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'] IcTwotoneWidthNormal: typeof import('~icons/ic/twotone-width-normal')['default']
LogosGoogleGmail: typeof import('~icons/logos/google-gmail')['default'] LogosGoogleGmail: typeof import('~icons/logos/google-gmail')['default']
LogosMysqlIcon: typeof import('~icons/logos/mysql-icon')['default'] LogosMysqlIcon: typeof import('~icons/logos/mysql-icon')['default']
LogosOracle: typeof import('~icons/logos/oracle')['default']
LogosPostgresql: typeof import('~icons/logos/postgresql')['default'] LogosPostgresql: typeof import('~icons/logos/postgresql')['default']
LogosRedditIcon: typeof import('~icons/logos/reddit-icon')['default'] LogosRedditIcon: typeof import('~icons/logos/reddit-icon')['default']
LogosSnowflakeIcon: typeof import('~icons/logos/snowflake-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"> <style scoped lang="scss">
.nc-cell-hover-show { .nc-cell-hover-show {
opacity: 0; opacity: 0.3;
transition: 0.3s opacity; transition: 0.3s opacity;
&:hover { &:hover {

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

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

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

@ -81,6 +81,7 @@ onMounted(() => {
@keydown.delete.stop @keydown.delete.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
@contextmenu.stop
/> />
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span> <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[] modelValue?: string | string[]
rowIndex?: number rowIndex?: number
disableOptionCreation?: boolean disableOptionCreation?: boolean
location?: 'cell' | 'filter'
} }
const { modelValue, disableOptionCreation } = defineProps<Props>() const { modelValue, disableOptionCreation } = defineProps<Props>()
@ -336,7 +337,7 @@ useEventListener(document, 'click', handleClose, true)
v-for="op of options" v-for="op of options"
:key="op.id || op.title" :key="op.id || op.title"
:value="op.title" :value="op.title"
:data-testid="`select-option-${column.title}-${rowIndex}`" :data-testid="`select-option-${column.title}-${location === 'filter' ? 'filter' : rowIndex}`"
:class="`nc-select-option-${column.title}-${op.title}`" :class="`nc-select-option-${column.title}-${op.title}`"
@click.stop @click.stop
> >

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

@ -14,6 +14,8 @@ const { showNull } = useGlobal()
const editEnabled = inject(EditModeInj) const editEnabled = inject(EditModeInj)
const rowHeight = inject(RowHeightInj)
const readonly = inject(ReadonlyInj, ref(false)) const readonly = inject(ReadonlyInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits) 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> <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> </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> <span v-else>{{ vModel }}</span>
</template> </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 = () => { const renderCmdOrCtrlKey = () => {
return isMac() ? '⌘' : 'CTRL' return isMac() ? '⌘' : 'CTRL'
} }
const renderAltOrOptlKey = () => {
return isMac() ? '⌥' : 'ALT'
}
const shortcutList = [ const shortcutList = [
{ {
@ -197,6 +200,22 @@ const shortcutList = [
keys: [renderCmdOrCtrlKey(), 'Enter'], keys: [renderCmdOrCtrlKey(), 'Enter'],
behaviour: 'Save current expanded form item', 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) 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> </script>
<template> <template>
@ -151,6 +160,7 @@ const isNumericField = computed(() => {
]" ]"
@keydown.enter.exact="syncAndNavigate(NavigateDir.NEXT, $event)" @keydown.enter.exact="syncAndNavigate(NavigateDir.NEXT, $event)"
@keydown.shift.enter.exact="syncAndNavigate(NavigateDir.PREV, $event)" @keydown.shift.enter.exact="syncAndNavigate(NavigateDir.PREV, $event)"
@contextmenu="onContextmenu"
> >
<template v-if="column"> <template v-if="column">
<LazyCellTextArea v-if="isTextArea(column)" v-model="vModel" /> <LazyCellTextArea v-if="isTextArea(column)" v-model="vModel" />

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

@ -118,6 +118,7 @@ const {
selectedAllRecords, selectedAllRecords,
removeRowIfNew, removeRowIfNew,
navigateToSiblingRow, navigateToSiblingRow,
getExpandedRowIndex,
} = useViewData(meta, view, xWhere) } = useViewData(meta, view, xWhere)
const { getMeta } = useMetas() const { getMeta } = useMetas()
@ -980,6 +981,8 @@ const closeAddColumnDropdown = () => {
:row-id="routeQuery.rowId" :row-id="routeQuery.rowId"
:view="view" :view="view"
show-next-prev-icons show-next-prev-icons
:first-row="getExpandedRowIndex() === 0"
:last-row="getExpandedRowIndex() === data.length - 1"
@next="navigateToSiblingRow(NavigateDir.NEXT)" @next="navigateToSiblingRow(NavigateDir.NEXT)"
@prev="navigateToSiblingRow(NavigateDir.PREV)" @prev="navigateToSiblingRow(NavigateDir.PREV)"
/> />
@ -1056,7 +1059,7 @@ const closeAddColumnDropdown = () => {
position: sticky !important; position: sticky !important;
left: 80px; left: 80px;
z-index: 5; z-index: 5;
@apply border-r-1 border-r-gray-300; @apply border-r-2 border-r-gray-300;
} }
tbody td:nth-child(2) { tbody td:nth-child(2) {
@ -1064,7 +1067,7 @@ const closeAddColumnDropdown = () => {
left: 80px; left: 80px;
z-index: 4; z-index: 4;
background: white; 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 { 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() const { isNew, syncLTARRefs, state } = useSmartsheetRowStoreOrThrow()
@ -26,8 +26,6 @@ const { isUIAllowed } = useUIPermission()
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook()) const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const saveRowAndStay = ref(0)
const save = async () => { const save = async () => {
if (isNew.value) { if (isNew.value) {
const data = await _save(state.value) const data = await _save(state.value)
@ -103,17 +101,6 @@ const onConfirmDeleteRowClick = async () => {
</h5> </h5>
<div class="flex-1" /> <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"> <a-tooltip placement="bottom">
<template #title> <template #title>
<!-- todo: i18n --> <!-- todo: i18n -->
@ -139,32 +126,6 @@ const onConfirmDeleteRowClick = async () => {
/> />
</a-tooltip> </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"> <a-dropdown-button class="nc-expand-form-save-btn" type="primary" :disabled="!isUIAllowed('tableRowUpdate')" @click="save">
<template #icon><MdiMenuDown /></template> <template #icon><MdiMenuDown /></template>
@ -194,17 +155,39 @@ const onConfirmDeleteRowClick = async () => {
</div> </div>
</a-dropdown-button> </a-dropdown-button>
<a-tooltip placement="bottom"> <a-dropdown>
<!-- Close --> <MdiDotsVertical class="nc-icon-transition" />
<template #title> <template #overlay>
<div class="text-center w-full">{{ $t('general.close') }}</div> <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> </template>
<MdiCloseCircleOutline </a-dropdown>
class="nc-icon-transition cursor-pointer select-none nc-close-form text-gray-500 mx-1 min-w-4"
@click="emit('cancel')"
/>
</a-tooltip>
<a-modal v-model:visible="showDeleteRowModal" title="Delete row?" @ok="onConfirmDeleteRowClick"> <a-modal v-model:visible="showDeleteRowModal" title="Delete row?" @ok="onConfirmDeleteRowClick">
<p>Are you sure you want to delete this row?</p> <p>Are you sure you want to delete this row?</p>
</a-modal> </a-modal>

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

@ -22,6 +22,7 @@ import {
useVModel, useVModel,
watch, watch,
} from '#imports' } from '#imports'
import { useActiveKeyupListener } from '~/composables/useSelectedCellKeyupListener'
import type { Row } from '~/lib' import type { Row } from '~/lib'
interface Props { interface Props {
@ -34,12 +35,16 @@ interface Props {
rowId?: string rowId?: string
view?: ViewType view?: ViewType
showNextPrevIcons?: boolean showNextPrevIcons?: boolean
firstRow?: boolean
lastRow?: boolean
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev']) const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev'])
const key = ref(0)
const { t } = useI18n() const { t } = useI18n()
const row = ref(props.row) const row = ref(props.row)
@ -64,7 +69,16 @@ const isKanban = inject(IsKanbanInj, ref(false))
provide(MetaInj, meta) 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) const duplicatingRowInProgress = ref(false)
@ -126,6 +140,25 @@ const onDuplicateRow = () => {
}, 500) }, 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()) const reloadParentRowHook = inject(ReloadRowDataHookInj, createEventHook())
// override reload trigger and use it to reload grid and the form itself // override reload trigger and use it to reload grid and the form itself
@ -152,6 +185,92 @@ const cellWrapperEl = ref<HTMLElement>()
onMounted(() => { onMounted(() => {
setTimeout(() => (cellWrapperEl.value?.querySelector('input,select,textarea') as HTMLInputElement)?.focus()) 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>
<script lang="ts"> <script lang="ts">
@ -172,21 +291,25 @@ export default {
> >
<SmartsheetExpandedFormHeader :view="props.view" @cancel="onClose" @duplicate-row="onDuplicateRow" /> <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 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"> <div class="flex-1 overflow-auto scrollbar-thin-dull nc-form-fields-container relative">
<template v-if="props.showNextPrevIcons"> <template v-if="props.showNextPrevIcons">
<a-tooltip placement="bottom"> <a-tooltip v-if="!props.firstRow" placement="bottom">
<template #title> <template #title>
{{ $t('labels.nextRow') }} {{ $t('labels.prevRow') }}
<GeneralShortcutLabel class="justify-center" :keys="['Alt', '←']" />
</template> </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>
<a-tooltip placement="bottom">
<a-tooltip v-if="!props.lastRow" placement="bottom">
<template #title> <template #title>
{{ $t('labels.prevRow') }} {{ $t('labels.nextRow') }}
<GeneralShortcutLabel class="justify-center" :keys="['Alt', '→']" />
</template> </template>
<MdiChevronLeft class="cursor-pointer nc-prev-arrow" @click="$emit('prev')" /> <MdiChevronRight class="cursor-pointer nc-next-arrow" @click="onNext" />
</a-tooltip> </a-tooltip>
</template> </template>
<div class="w-[500px] mx-auto"> <div class="w-[500px] mx-auto">
@ -210,7 +333,8 @@ export default {
:ref="i ? null : (el) => (cellWrapperEl = el)" :ref="i ? null : (el) => (cellWrapperEl = el)"
class="!bg-white rounded px-1 min-h-[35px] flex items-center mt-2 relative" 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 <LazySmartsheetCell
v-else v-else

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

@ -6,6 +6,7 @@ import {
MetaInj, MetaInj,
ReloadViewDataHookInj, ReloadViewDataHookInj,
comparisonOpList, comparisonOpList,
comparisonSubOpList,
computed, computed,
inject, inject,
ref, ref,
@ -54,6 +55,7 @@ const {
sync, sync,
saveOrUpdateDebounced, saveOrUpdateDebounced,
isComparisonOpAllowed, isComparisonOpAllowed,
isComparisonSubOpAllowed,
} = useViewFilters( } = useViewFilters(
activeView, activeView,
parentId, parentId,
@ -75,24 +77,43 @@ const filterPrevComparisonOp = ref<Record<string, string>>({})
const filterUpdateCondition = (filter: FilterType, i: number) => { const filterUpdateCondition = (filter: FilterType, i: number) => {
const col = getColumn(filter) const col = getColumn(filter)
if (!col) return
if ( if (
col.uidt === UITypes.SingleSelect && col.uidt === UITypes.SingleSelect &&
['anyof', 'nanyof'].includes(filterPrevComparisonOp.value[filter.id]) && ['anyof', 'nanyof'].includes(filterPrevComparisonOp.value[filter.id!]) &&
['eq', 'neq'].includes(filter.comparison_op!) ['eq', 'neq'].includes(filter.comparison_op!)
) { ) {
// anyof and nanyof can allow multiple selections, // anyof and nanyof can allow multiple selections,
// while `eq` and `neq` only allow one selection // 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!)) { } else if (['blank', 'notblank', 'empty', 'notempty', 'null', 'notnull'].includes(filter.comparison_op!)) {
// since `blank`, `empty`, `null` doesn't require value, // since `blank`, `empty`, `null` doesn't require value,
// hence remove the previous 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) saveOrUpdate(filter, i)
filterPrevComparisonOp.value[filter.id] = filter.comparison_op filterPrevComparisonOp.value[filter.id] = filter.comparison_op
$e('a:filter:update', { $e('a:filter:update', {
logical: filter.logical_op, logical: filter.logical_op,
comparison: filter.comparison_op, comparison: filter.comparison_op,
comparison_sub_op: filter.comparison_sub_op,
}) })
} }
@ -109,7 +130,7 @@ const types = computed(() => {
watch( watch(
() => activeView.value?.id, () => activeView.value?.id,
(n: string, o: string) => { (n, o) => {
// if nested no need to reload since it will get reloaded from parent // if nested no need to reload since it will get reloaded from parent
if (!nested && n !== o && (hookId || !webHook)) loadFilters(hookId as string) if (!nested && n !== o && (hookId || !webHook)) loadFilters(hookId as string)
}, },
@ -137,16 +158,30 @@ const applyChanges = async (hookId?: string, _nested = false) => {
} }
const selectFilterField = (filter: Filter, index: number) => { const selectFilterField = (filter: Filter, index: number) => {
const col = getColumn(filter)
if (!col) return
// when we change the field, // when we change the field,
// the corresponding default filter operator needs to be changed as well // the corresponding default filter operator needs to be changed as well
// since the existing one may not be supported for the new field // since the existing one may not be supported for the new field
// e.g. `eq` operator is not supported in checkbox field // e.g. `eq` operator is not supported in checkbox field
// hence, get the first option of the supported operators of the new 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), isComparisonOpAllowed(filter, compOp),
)?.[0].value )?.[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 // reset filter value as well
filter.value = '' filter.value = null
saveOrUpdate(filter, index) saveOrUpdate(filter, index)
} }
@ -163,10 +198,15 @@ defineExpose({
<template> <template>
<div <div
class="p-4 menu-filter-dropdown bg-gray-50 !border mt-4" class="p-4 menu-filter-dropdown bg-gray-50 !border"
:class="{ 'shadow min-w-[430px] max-h-[max(80vh,500px)] overflow-auto': !nested, 'border-1 w-full': nested }" :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-for="(filter, i) in filters" :key="i">
<template v-if="filter.status !== 'delete'"> <template v-if="filter.status !== 'delete'">
<template v-if="filter.is_group"> <template v-if="filter.is_group">
@ -195,7 +235,7 @@ defineExpose({
</a-select> </a-select>
</div> </div>
<span class="col-span-3" /> <span class="col-span-3" />
<div class="col-span-5"> <div class="col-span-6">
<LazySmartsheetToolbarColumnFilter <LazySmartsheetToolbarColumnFilter
v-if="filter.id || filter.children" v-if="filter.id || filter.children"
:key="filter.id ?? i" :key="filter.id ?? i"
@ -261,24 +301,49 @@ defineExpose({
</template> </template>
</a-select> </a-select>
<span <a-select
v-if=" v-if="
filter.comparison_op && [UITypes.Date, UITypes.DateTime].includes(getColumn(filter)?.uidt) &&
['null', 'notnull', 'checked', 'notchecked', 'empty', 'notempty', 'blank', 'notblank'].includes( !['blank', 'notblank'].includes(filter.comparison_op)
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 <a-checkbox
v-else-if="filter.field && types[filter.field] === 'boolean'" v-if="filter.field && types[filter.field] === 'boolean'"
v-model:checked="filter.value" v-model:checked="filter.value"
dense dense
:disabled="filter.readOnly" :disabled="filter.readOnly"
@change="saveOrUpdate(filter, i)" @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 <LazySmartsheetToolbarFilterInput
v-else v-else
class="nc-filter-value-select min-w-[120px]" class="nc-filter-value-select min-w-[120px]"
@ -315,7 +380,7 @@ defineExpose({
<style scoped> <style scoped>
.nc-filter-grid { .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; @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 // use MultiSelect for SingleSelect columns for anyof / nanyof filters
isSingleSelect: ['anyof', 'nanyof'].includes(props.filter.comparison_op!) ? MultiSelect : SingleSelect, isSingleSelect: ['anyof', 'nanyof'].includes(props.filter.comparison_op!) ? MultiSelect : SingleSelect,
isMultiSelect: MultiSelect, isMultiSelect: MultiSelect,
isDate: DatePicker, isDate: ['daysAgo', 'daysFromNow', 'pastNumberOfDays', 'nextNumberOfDays'].includes(props.filter.comparison_sub_op!)
? Decimal
: DatePicker,
isYear: YearPicker, isYear: YearPicker,
isDateTime: DateTimePicker, isDateTime: ['daysAgo', 'daysFromNow', 'pastNumberOfDays', 'nextNumberOfDays'].includes(props.filter.comparison_sub_op!)
? Decimal
: DateTimePicker,
isTime: TimePicker, isTime: TimePicker,
isRating: Rating, isRating: Rating,
isDuration: Duration, isDuration: Duration,
@ -189,6 +193,7 @@ const hasExtraPadding = $computed(() => {
:column="column" :column="column"
class="flex" class="flex"
v-bind="componentProps" v-bind="componentProps"
location="filter"
/> />
</div> </div>
</template> </template>

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

@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { nextTick } from '@vue/runtime-core'
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
@ -22,7 +23,17 @@ const reloadDataHook = inject(ReloadViewDataHookInj)
const { eventBus } = useSmartsheetStoreOrThrow() 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() const { isMobileMode } = useGlobal()
@ -77,12 +88,18 @@ useMenuCloseOnEsc(open)
</div> </div>
<template #overlay> <template #overlay>
<div <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" 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"> <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 <LazySmartsheetToolbarFieldListAutoCompleteDropdown
v-model="sort.fk_column_id" v-model="sort.fk_column_id"
@ -94,6 +111,7 @@ useMenuCloseOnEsc(open)
/> />
<a-select <a-select
ref=""
v-model:value="sort.direction" v-model:value="sort.direction"
class="shrink grow-0 nc-sort-dir-select !text-xs" class="shrink grow-0 nc-sort-dir-select !text-xs"
:label="$t('labels.operation')" :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 editOrAdd.value = true
currentHook.value = hook currentHook.value = hook
} }
async function addHook() {
editOrAdd.value = true
currentHook.value = undefined
}
</script> </script>
<template> <template>
@ -35,7 +40,7 @@ async function editHook(hook: Record<string, any>) {
<a-layout-content class="px-10 py-5 scrollbar-thin-primary"> <a-layout-content class="px-10 py-5 scrollbar-thin-primary">
<LazyWebhookEditor v-if="editOrAdd" :hook="currentHook" @back-to-list="editOrAdd = false" /> <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-content>
<a-layout-footer class="!bg-white border-t flex"> <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') { 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') { if (hook.notification.type === 'Microsoft Teams') {
@ -651,6 +651,7 @@ onMounted(loadPluginList)
</a-checkbox> </a-checkbox>
<LazySmartsheetToolbarColumnFilter <LazySmartsheetToolbarColumnFilter
class="mt-4"
v-if="hook.condition" v-if="hook.condition"
ref="filterRef" ref="filterRef"
:auto-save="false" :auto-save="false"

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

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

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

@ -1,15 +1,15 @@
import { isClient } from '@vueuse/core' import { isClient } from '@vueuse/core'
import type { Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
function useSelectedCellKeyupListener( function useSelectedCellKeyupListener(
selected: Ref<boolean>, selected: Ref<boolean | undefined> | ComputedRef<boolean | undefined>,
handler: (e: KeyboardEvent) => void, handler: (e: KeyboardEvent) => void,
{ immediate = false }: { immediate?: boolean } = {}, { immediate = false }: { immediate?: boolean } = {},
) { ) {
if (isClient) { if (isClient) {
watch( watch(
selected, selected,
(nextVal: boolean, _: boolean, cleanup) => { (nextVal: boolean | undefined, _: boolean | undefined, cleanup) => {
// bind listener when `selected` is truthy // bind listener when `selected` is truthy
if (nextVal) { if (nextVal) {
document.addEventListener('keydown', handler, true) 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
// get current expanded row index function getExpandedRowIndex() {
const expandedRowIndex = formattedData.value.findIndex( return formattedData.value.findIndex(
(row: Row) => routeQuery.rowId === extractPkFromRow(row.row, meta.value?.columns as ColumnType[]), (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 // calculate next row index based on direction
let siblingRowIndex = expandedRowIndex + (dir === NavigateDir.NEXT ? 1 : -1) 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 const currentPage = paginationData?.value?.page || 1
// if next row index is less than 0, go to previous page and point to last element // 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, removeLastEmptyRow,
removeRowIfNew, removeRowIfNew,
navigateToSiblingRow, navigateToSiblingRow,
getExpandedRowIndex,
} }
} }

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

@ -140,6 +140,25 @@ export function useViewFilters(
return isNullOrEmptyOp ? projectMeta.value.showNullAndEmptyInFilter : true 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 => { const placeholderFilter = (): Filter => {
return { return {
comparison_op: comparisonOpList(options.value?.[0].uidt as UITypes).filter((compOp) => comparison_op: comparisonOpList(options.value?.[0].uidt as UITypes).filter((compOp) =>
@ -327,5 +346,6 @@ export function useViewFilters(
addFilterGroup, addFilterGroup,
saveOrUpdateDebounced, saveOrUpdateDebounced,
isComparisonOpAllowed, isComparisonOpAllowed,
isComparisonSubOpAllowed,
} }
} }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -3,7 +3,11 @@ import { UITypes, isNumericCol, numericUITypes } from 'nocodb-sdk'
const getEqText = (fieldUiType: UITypes) => { const getEqText = (fieldUiType: UITypes) => {
if (isNumericCol(fieldUiType)) { if (isNumericCol(fieldUiType)) {
return '=' 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'
} }
return 'is equal' return 'is equal'
@ -12,7 +16,11 @@ const getEqText = (fieldUiType: UITypes) => {
const getNeqText = (fieldUiType: UITypes) => { const getNeqText = (fieldUiType: UITypes) => {
if (isNumericCol(fieldUiType)) { if (isNumericCol(fieldUiType)) {
return '!=' 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'
} }
return 'is not equal' return 'is not equal'
@ -32,12 +40,40 @@ const getNotLikeText = (fieldUiType: UITypes) => {
return 'is not like' 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 = ( export const comparisonOpList = (
fieldUiType: UITypes, fieldUiType: UITypes,
): { ): {
text: string text: string
value: string value: string
ignoreVal?: boolean ignoreVal: boolean
includedTypes?: UITypes[] includedTypes?: UITypes[]
excludedTypes?: UITypes[] excludedTypes?: UITypes[]
}[] => [ }[] => [
@ -56,22 +92,42 @@ export const comparisonOpList = (
{ {
text: getEqText(fieldUiType), text: getEqText(fieldUiType),
value: 'eq', value: 'eq',
ignoreVal: false,
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment], excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment],
}, },
{ {
text: getNeqText(fieldUiType), text: getNeqText(fieldUiType),
value: 'neq', value: 'neq',
ignoreVal: false,
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment], excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment],
}, },
{ {
text: getLikeText(fieldUiType), text: getLikeText(fieldUiType),
value: 'like', 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), text: getNotLikeText(fieldUiType),
value: 'nlike', 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', text: 'is empty',
@ -85,6 +141,8 @@ export const comparisonOpList = (
UITypes.Attachment, UITypes.Attachment,
UITypes.LinkToAnotherRecord, UITypes.LinkToAnotherRecord,
UITypes.Lookup, UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
...numericUITypes, ...numericUITypes,
], ],
}, },
@ -100,6 +158,8 @@ export const comparisonOpList = (
UITypes.Attachment, UITypes.Attachment,
UITypes.LinkToAnotherRecord, UITypes.LinkToAnotherRecord,
UITypes.Lookup, UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
...numericUITypes, ...numericUITypes,
], ],
}, },
@ -116,6 +176,8 @@ export const comparisonOpList = (
UITypes.Attachment, UITypes.Attachment,
UITypes.LinkToAnotherRecord, UITypes.LinkToAnotherRecord,
UITypes.Lookup, UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
], ],
}, },
{ {
@ -131,47 +193,63 @@ export const comparisonOpList = (
UITypes.Attachment, UITypes.Attachment,
UITypes.LinkToAnotherRecord, UITypes.LinkToAnotherRecord,
UITypes.Lookup, UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
], ],
}, },
{ {
text: 'contains all of', text: 'contains all of',
value: 'allof', value: 'allof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect], includedTypes: [UITypes.MultiSelect],
}, },
{ {
text: 'contains any of', text: 'contains any of',
value: 'anyof', value: 'anyof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect], includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect],
}, },
{ {
text: 'does not contain all of', text: 'does not contain all of',
value: 'nallof', value: 'nallof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect], includedTypes: [UITypes.MultiSelect],
}, },
{ {
text: 'does not contain any of', text: 'does not contain any of',
value: 'nanyof', value: 'nanyof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect], includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect],
}, },
{ {
text: '>', text: getGtText(fieldUiType),
value: 'gt', value: 'gt',
includedTypes: [...numericUITypes], ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime],
}, },
{ {
text: '<', text: getLtText(fieldUiType),
value: 'lt', value: 'lt',
includedTypes: [...numericUITypes], ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime],
}, },
{ {
text: '>=', text: getGteText(fieldUiType),
value: 'gte', value: 'gte',
includedTypes: [...numericUITypes], ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime],
}, },
{ {
text: '<=', text: getLteText(fieldUiType),
value: 'lte', 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', text: 'is blank',
@ -186,3 +264,129 @@ export const comparisonOpList = (
excludedTypes: [UITypes.Checkbox], 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 | Patch | dbView | update | /api/v1/db/meta/tables/{tableId} |
| Meta | Delete| dbView | delete | /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 | reorder | /api/v1/db/meta/tables/{tableId}/reorder |
| Meta | Post | dbView | formCreate | /api/v1/db/meta/forms | | Meta | Post | dbView | formCreate | /api/v1/db/meta/tables/{tableId}/forms |
| Meta | Patch | dbView | formUpdate | /api/v1/db/meta/forms/{formId} | | Meta | Patch | dbView | formUpdate | /api/v1/db/meta/forms/{formViewId} |
| Meta | Get | dbView | formRead | /api/v1/db/meta/forms/{formId} | | Meta | Get | dbView | formRead | /api/v1/db/meta/forms/{formViewId} |
| Meta | Patch | dbView | formColumnUpdate | /api/v1/db/meta/form-columns/{formViewColumnId} | | Meta | Patch | dbView | formColumnUpdate | /api/v1/db/meta/form-columns/{formViewColumnId} |
| Meta | Post | dbView | galleryCreate | /api/v1/db/meta/galleries | | Meta | Post | dbView | galleryCreate | /api/v1/db/meta/tables/{tableId}/galleries |
| Meta | Patch | dbView | galleryUpdate | /api/v1/db/meta/galleries/{galleriesId} | | Meta | Patch | dbView | galleryUpdate | /api/v1/db/meta/galleries/{galleryViewId} |
| Meta | Get | dbView | galleryRead | /api/v1/db/meta/galleries/{galleriesId} | | Meta | Get | dbView | galleryRead | /api/v1/db/meta/galleries/{galleryViewId} |
| Meta | Post | dbView | gridCreate | /api/v1/db/meta/tables/${tableId}/grids | | 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 | 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 | gridColumnUpdate | /api/v1/db/meta/grid-columns/{columnId} |
| Meta | Patch | dbView | update | /api/v1/db/meta/views/{viewId} | | 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) | | btw | between | (colName,btw,val1,val2) |
| nbtw | not between | (colName,nbtw,val1,val2) | | nbtw | not between | (colName,nbtw,val1,val2) |
| like | like | (colName,like,%name) | | 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,...) | | allof | includes all of | (colName,allof,val1,val2,...) |
| anyof | includes any of | (colName,anyof,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,...) | | 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,...) | | 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 ## Logical Operators
| Operation | Example | | 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. 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. 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"> <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 ### Column Delete
To delete a column, click the down arrow, select `Delete` from the menu. 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', EXPORT_TO_ZIP = 'EXPORT_TO_ZIP',
UPDATED = 'UPDATED', UPDATED = 'UPDATED',
SIGNIN = 'SIGNIN', SIGNIN = 'SIGNIN',
SIGN = 'SIGN', SIGNUP = 'SIGNUP',
PASSWORD_RESET = 'PASSWORD_RESET', PASSWORD_RESET = 'PASSWORD_RESET',
PASSWORD_FORGOT = 'PASSWORD_FORGOT', PASSWORD_FORGOT = 'PASSWORD_FORGOT',
PASSWORD_CHANGE = 'PASSWORD_CHANGE', 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 UITypes from '../UITypes';
import { IDType } from './index'; 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 = {}; const colProp: any = {};
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':

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

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

71
packages/nocodb/.eslintrc.json

@ -7,18 +7,8 @@
"env": { "env": {
"es6": true "es6": true
}, },
"ignorePatterns": [ "ignorePatterns": ["node_modules", "build", "coverage", "dist", "nc"],
"node_modules", "plugins": ["import", "eslint-comments", "functional"],
"build",
"coverage",
"dist",
"nc"
],
"plugins": [
"import",
"eslint-comments",
"functional"
],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:eslint-comments/recommended", "plugin:eslint-comments/recommended",
@ -47,6 +37,21 @@
"ignoreCase": true "ignoreCase": true
} }
], ],
"import/order": [
"error",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"object",
"type"
]
}
],
"@typescript-eslint/no-this-alias": "off", "@typescript-eslint/no-this-alias": "off",
// todo: enable // todo: enable
@ -57,46 +62,6 @@
"no-useless-catch": "off", "no-useless-catch": "off",
"no-empty": "off", "no-empty": "off",
"@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-function": "off",
"import/order": "off" "@typescript-eslint/consistent-type-imports": "warn"
// "@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"
// ]
// }
// }
// ]
} }
} }

41
packages/nocodb/package-lock.json generated

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

10
packages/nocodb/package.json

@ -25,7 +25,7 @@
"obfuscate:build:publish": "npm run build:obfuscate && npm publish .", "obfuscate:build:publish": "npm run build:obfuscate && npm publish .",
"fix": "run-s fix:*", "fix": "run-s fix:*",
"fix:prettier": "prettier \"src/**/*.ts\" --write", "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", "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", "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", "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\"", "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", "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\"", "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": { "engines": {
"node": ">=8.9" "node": ">=8.9"
@ -54,6 +55,7 @@
"@sentry/node": "^6.3.5", "@sentry/node": "^6.3.5",
"airtable": "^0.11.3", "airtable": "^0.11.3",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"archiver": "^5.0.2", "archiver": "^5.0.2",
"auto-bind": "^4.0.0", "auto-bind": "^4.0.0",
"aws-sdk": "^2.829.0", "aws-sdk": "^2.829.0",
@ -105,7 +107,7 @@
"multer": "^1.4.2", "multer": "^1.4.2",
"mysql2": "^2.2.5", "mysql2": "^2.2.5",
"nanoid": "^3.1.20", "nanoid": "^3.1.20",
"nc-help": "0.2.85", "nc-help": "0.2.87",
"nc-lib-gui": "0.105.3", "nc-lib-gui": "0.105.3",
"nc-plugin": "0.1.2", "nc-plugin": "0.1.2",
"ncp": "^2.0.0", "ncp": "^2.0.0",
@ -160,7 +162,7 @@
"eslint-config-prettier": "^6.15.0", "eslint-config-prettier": "^6.15.0",
"eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2", "eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0", "eslint-plugin-import": "^2.25.2",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"mocha": "^10.1.0", "mocha": "^10.1.0",
"nodemon": "^2.0.7", "nodemon": "^2.0.7",

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

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

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

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

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

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

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

@ -1,7 +1,7 @@
import debug from 'debug'; import debug from 'debug';
import CacheMgr from './CacheMgr';
import Redis from 'ioredis-mock'; import Redis from 'ioredis-mock';
import { CacheDelDirection, CacheGetType, CacheScope } from '../utils/globals'; import { CacheDelDirection, CacheGetType, CacheScope } from '../utils/globals';
import CacheMgr from './CacheMgr';
const log = debug('nc:cache'); const log = debug('nc:cache');
export default class RedisMockCacheMgr extends CacheMgr { 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 { Router } from 'express';
import catchError from '../meta/helpers/catchError';
import { cacheService } from '../services';
export async function cacheGet(_, res) { export async function cacheGet(_, res) {
const data = await NocoCache.export(); const data = await cacheService.cacheGet();
res.set({ res.set({
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="cache-export.json"`, 'Content-Disposition': `attachment; filename="cache-export.json"`,
@ -12,7 +12,7 @@ export async function cacheGet(_, res) {
} }
export async function cacheDelete(_, res) { export async function cacheDelete(_, res) {
return res.json(await NocoCache.destroy()); return res.json(await cacheService.cacheDelete());
} }
const router = Router(); 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 * as XLSX from 'xlsx';
import ncMetaAclMw from '../../helpers/ncMetaAclMw'; import apiMetrics from '../../meta/helpers/apiMetrics';
import { import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
extractCsvData, import { View } from '../../models';
extractXlsxData, import { extractCsvData, extractXlsxData } from '../../services/dbData/helpers';
getViewAndModelFromRequestByAliasOrId, import { getViewAndModelFromRequestByAliasOrId } from './helpers';
} from './helpers'; import type { Request, Response } from 'express';
import apiMetrics from '../../helpers/apiMetrics';
import View from '../../../models/View';
async function excelDataExport(req: Request, res: Response) { async function excelDataExport(req: Request, res: Response) {
const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); 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 { Router } from 'express';
import Model from '../../../models/Model';
import { nocoExecute } from 'nc-help'; import { nocoExecute } from 'nc-help';
import Base from '../../../models/Base'; import Model from '../../models/Model';
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; import Base from '../../models/Base';
import View from '../../../models/View'; import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import ncMetaAclMw from '../../helpers/ncMetaAclMw'; import View from '../../models/View';
import Project from '../../../models/Project'; import ncMetaAclMw from '../../meta/helpers/ncMetaAclMw';
import { NcError } from '../../helpers/catchError'; import Project from '../../models/Project';
import apiMetrics from '../../helpers/apiMetrics'; import { NcError } from '../../meta/helpers/catchError';
import getAst from '../../../db/sql-data-mapper/lib/sql/helpers/getAst'; 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) { export async function dataList(req: Request, res: Response) {
const { model, view } = await getViewAndModelFromRequest(req); const { model, view } = await getViewAndModelFromRequest(req);
@ -17,7 +18,7 @@ export async function dataList(req: Request, res: Response) {
const baseModel = await Model.getBaseModelSQL({ const baseModel = await Model.getBaseModelSQL({
id: model.id, id: model.id,
viewId: view?.id, viewId: view?.id,
dbDriver: NcConnectionMgrv2.get(base), dbDriver: await NcConnectionMgrv2.get(base),
}); });
const requestObj = await getAst({ const requestObj = await getAst({
@ -50,7 +51,7 @@ export async function dataCount(req: Request, res: Response) {
const baseModel = await Model.getBaseModelSQL({ const baseModel = await Model.getBaseModelSQL({
id: model.id, id: model.id,
viewId: view?.id, viewId: view?.id,
dbDriver: NcConnectionMgrv2.get(base), dbDriver: await NcConnectionMgrv2.get(base),
}); });
const listArgs: any = { ...req.query }; const listArgs: any = { ...req.query };
@ -73,7 +74,7 @@ async function dataInsert(req: Request, res: Response) {
const baseModel = await Model.getBaseModelSQL({ const baseModel = await Model.getBaseModelSQL({
id: model.id, id: model.id,
viewId: view?.id, viewId: view?.id,
dbDriver: NcConnectionMgrv2.get(base), dbDriver: await NcConnectionMgrv2.get(base),
}); });
res.json(await baseModel.insert(req.body, null, req)); 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({ const baseModel = await Model.getBaseModelSQL({
id: model.id, id: model.id,
viewId: view.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)); 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({ const baseModel = await Model.getBaseModelSQL({
id: model.id, id: model.id,
viewId: view.id, viewId: view.id,
dbDriver: NcConnectionMgrv2.get(base), dbDriver: await NcConnectionMgrv2.get(base),
}); });
res.json(await baseModel.delByPk(req.params.rowId, null, req)); 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({ const baseModel = await Model.getBaseModelSQL({
id: model.id, id: model.id,
viewId: view?.id, viewId: view?.id,
dbDriver: NcConnectionMgrv2.get(base), dbDriver: await NcConnectionMgrv2.get(base),
}); });
res.json( 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 { Router } from 'express';
import View from '../../models/View'; import View from '../models/View';
import ncMetaAclMw from '../helpers/ncMetaAclMw'; import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { extractCsvData } from './dataApis/helpers'; import { extractCsvData } from './dbData/helpers';
import type { Request, Response } from 'express';
async function exportCsv(req: Request, res: Response) { async function exportCsv(req: Request, res: Response) {
const view = await View.get(req.params.viewId); 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 { Router } from 'express';
import { OrgUserRoles } from 'nocodb-sdk'; import { OrgUserRoles } from 'nocodb-sdk';
import { NC_LICENSE_KEY } from '../../constants'; import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import Store from '../../models/Store'; import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import Noco from '../../Noco'; import { orgLicenseService } from '../services';
import { metaApiMetrics } from '../helpers/apiMetrics';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { getAjvValidatorMw } from './helpers';
async function licenseGet(_req, res) { async function licenseGet(_req, res) {
const license = await Store.get(NC_LICENSE_KEY); res.json(await orgLicenseService.licenseGet());
res.json({ key: license?.value });
} }
async function licenseSet(req, res) { async function licenseSet(req, res) {
await Store.saveOrUpdate({ value: req.body.key, key: NC_LICENSE_KEY }); await orgLicenseService.licenseSet({ key: req.body.key });
await Noco.loadEEState();
res.json({ msg: 'License key saved' }); res.json({ msg: 'License key saved' });
} }
@ -31,7 +25,6 @@ router.get(
router.post( router.post(
'/api/v1/license', '/api/v1/license',
metaApiMetrics, metaApiMetrics,
getAjvValidatorMw('swagger.json#/components/schemas/LicenseReq'),
ncMetaAclMw(licenseSet, 'licenseSet', { ncMetaAclMw(licenseSet, 'licenseSet', {
allowedRoles: [OrgUserRoles.SUPER_ADMIN], allowedRoles: [OrgUserRoles.SUPER_ADMIN],
blockApiTokenAccess: true, 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 { Router } from 'express';
import { Tele } from 'nc-help'; import { PagedResponseImpl } from '../meta/helpers/PagedResponse';
import { PagedResponseImpl } from '../helpers/PagedResponse'; import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import Plugin from '../../models/Plugin'; import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import { PluginType } from 'nocodb-sdk'; import { pluginService } from '../services';
import NcPluginMgrv2 from '../helpers/NcPluginMgrv2'; import type { PluginType } from 'nocodb-sdk';
import ncMetaAclMw from '../helpers/ncMetaAclMw'; import type { Request, Response } from 'express';
import { metaApiMetrics } from '../helpers/apiMetrics';
import { getAjvValidatorMw } from './helpers';
export async function pluginList(_req: Request, res: Response) { 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) { export async function pluginTest(req: Request<any, any>, res: Response) {
Tele.emit('evt', { evt_type: 'plugin:tested' }); res.json(await pluginService.pluginTest({ body: req.body }));
res.json(await NcPluginMgrv2.test(req.body));
} }
export async function pluginRead(req: Request, res: Response) { 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( export async function pluginUpdate(
req: Request<any, any, PluginType>, req: Request<any, any, PluginType>,
res: Response res: Response
) { ) {
const plugin = await Plugin.update(req.params.pluginId, req.body); const plugin = await pluginService.pluginUpdate({
Tele.emit('evt', { pluginId: req.params.pluginId,
evt_type: plugin.active ? 'plugin:installed' : 'plugin:uninstalled', plugin: req.body,
title: plugin.title,
}); });
res.json(plugin); res.json(plugin);
} }
export async function isPluginActive(req: Request, res: Response) { 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 }); const router = Router({ mergeParams: true });
@ -44,8 +42,6 @@ router.get(
router.post( router.post(
'/api/v1/db/meta/plugins/test', '/api/v1/db/meta/plugins/test',
metaApiMetrics, metaApiMetrics,
getAjvValidatorMw('swagger.json#/components/schemas/PluginTestReq'),
ncMetaAclMw(pluginTest, 'pluginTest') ncMetaAclMw(pluginTest, 'pluginTest')
); );
router.get( router.get(
@ -56,7 +52,6 @@ router.get(
router.patch( router.patch(
'/api/v1/db/meta/plugins/:pluginId', '/api/v1/db/meta/plugins/:pluginId',
metaApiMetrics, metaApiMetrics,
getAjvValidatorMw('swagger.json#/components/schemas/PluginReq'),
ncMetaAclMw(pluginUpdate, 'pluginUpdate') ncMetaAclMw(pluginUpdate, 'pluginUpdate')
); );
router.get( router.get(

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

Loading…
Cancel
Save