Browse Source

Merge branch 'develop' into feat/data-apis

pull/5901/head
Pranav C 1 year ago committed by GitHub
parent
commit
df332528e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 13860
      package-lock.json
  2. 4
      packages/nc-gui/components/cell/Currency.vue
  3. 52
      packages/nc-gui/components/cell/DateTimePicker.vue
  4. 4
      packages/nc-gui/components/cell/Decimal.vue
  5. 4
      packages/nc-gui/components/cell/Duration.vue
  6. 4
      packages/nc-gui/components/cell/Email.vue
  7. 4
      packages/nc-gui/components/cell/Float.vue
  8. 4
      packages/nc-gui/components/cell/Integer.vue
  9. 4
      packages/nc-gui/components/cell/Percent.vue
  10. 4
      packages/nc-gui/components/cell/Text.vue
  11. 4
      packages/nc-gui/components/cell/TextArea.vue
  12. 4
      packages/nc-gui/components/cell/Url.vue
  13. 33
      packages/nc-gui/components/dashboard/TreeView.vue
  14. 7
      packages/nc-gui/components/dlg/ProjectDuplicate.vue
  15. 3
      packages/nc-gui/components/dlg/TableCreate.vue
  16. 2
      packages/nc-gui/components/dlg/TableDuplicate.vue
  17. 4
      packages/nc-gui/components/general/EmojiIcons.vue
  18. 7
      packages/nc-gui/components/smartsheet/Cell.vue
  19. 2
      packages/nc-gui/components/smartsheet/Form.vue
  20. 8
      packages/nc-gui/components/smartsheet/Gallery.vue
  21. 35
      packages/nc-gui/components/smartsheet/Grid.vue
  22. 1
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  23. 2
      packages/nc-gui/components/smartsheet/header/Cell.vue
  24. 3
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  25. 5
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  26. 6
      packages/nc-gui/components/virtual-cell/Formula.vue
  27. 5
      packages/nc-gui/components/virtual-cell/HasMany.vue
  28. 3
      packages/nc-gui/components/virtual-cell/Lookup.vue
  29. 5
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  30. 4
      packages/nc-gui/components/virtual-cell/components/ItemChip.vue
  31. 4
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  32. 6
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  33. 3
      packages/nc-gui/composables/useApi/interceptors.ts
  34. 6
      packages/nc-gui/composables/useGridViewColumnWidth.ts
  35. 2
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  36. 55
      packages/nc-gui/composables/useMultiSelect/index.ts
  37. 1
      packages/nc-gui/context/index.ts
  38. 12
      packages/nc-gui/lang/fr.json
  39. 8
      packages/nc-gui/lang/ru.json
  40. 80
      packages/nc-gui/lang/tr.json
  41. 8
      packages/nc-gui/lang/zh-Hans.json
  42. 2
      packages/nc-gui/lib/types.ts
  43. 1
      packages/nc-gui/nuxt-shim.d.ts
  44. 2
      packages/nc-gui/package-lock.json
  45. 4
      packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue
  46. 13
      packages/nc-gui/pages/index/index/index.vue
  47. 2
      packages/nc-gui/plugins/a.dayjs.ts
  48. 17
      packages/nc-gui/plugins/jobs.ts
  49. 7
      packages/nc-gui/store/project.ts
  50. 23
      packages/nc-gui/utils/cell.ts
  51. 8
      packages/nc-gui/utils/dateTimeUtils.ts
  52. 1
      packages/nc-gui/utils/index.ts
  53. 2
      packages/nc-lib-gui/package.json
  54. 4
      packages/nocodb-sdk/package-lock.json
  55. 2
      packages/nocodb-sdk/package.json
  56. 101
      packages/nocodb-sdk/src/lib/Api.ts
  57. 2
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  58. 2
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  59. 2
      packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts
  60. 2
      packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts
  61. 2
      packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts
  62. 10
      packages/nocodb/.eslintrc.js
  63. 20
      packages/nocodb/package-lock.json
  64. 4
      packages/nocodb/package.json
  65. 10
      packages/nocodb/src/Noco.ts
  66. 10
      packages/nocodb/src/app.module.ts
  67. 14
      packages/nocodb/src/cache/RedisCacheMgr.ts
  68. 2
      packages/nocodb/src/cache/RedisMockCacheMgr.ts
  69. 19
      packages/nocodb/src/connection/connection.spec.ts
  70. 37
      packages/nocodb/src/connection/connection.ts
  71. 1
      packages/nocodb/src/controllers/api-tokens.controller.ts
  72. 1
      packages/nocodb/src/controllers/attachments.controller.ts
  73. 1
      packages/nocodb/src/controllers/audits.controller.ts
  74. 1
      packages/nocodb/src/controllers/bases.controller.ts
  75. 1
      packages/nocodb/src/controllers/bulk-data-alias.controller.ts
  76. 4
      packages/nocodb/src/controllers/caches.controller.ts
  77. 1
      packages/nocodb/src/controllers/columns.controller.ts
  78. 1
      packages/nocodb/src/controllers/data-alias-export.controller.ts
  79. 2
      packages/nocodb/src/controllers/data-alias-nested.controller.ts
  80. 1
      packages/nocodb/src/controllers/data-alias.controller.ts
  81. 2
      packages/nocodb/src/controllers/filters.controller.ts
  82. 1
      packages/nocodb/src/controllers/form-columns.controller.ts
  83. 1
      packages/nocodb/src/controllers/galleries.controller.ts
  84. 1
      packages/nocodb/src/controllers/grid-columns.controller.ts
  85. 1
      packages/nocodb/src/controllers/grids.controller.ts
  86. 1
      packages/nocodb/src/controllers/maps.controller.ts
  87. 1
      packages/nocodb/src/controllers/meta-diffs.controller.ts
  88. 1
      packages/nocodb/src/controllers/model-visibilities.controller.ts
  89. 1
      packages/nocodb/src/controllers/old-datas/old-datas.controller.ts
  90. 1
      packages/nocodb/src/controllers/org-users.controller.ts
  91. 10
      packages/nocodb/src/controllers/plugins.controller.ts
  92. 1
      packages/nocodb/src/controllers/project-users.controller.ts
  93. 77
      packages/nocodb/src/controllers/projects.controller.ts
  94. 1
      packages/nocodb/src/controllers/shared-bases.controller.ts
  95. 1
      packages/nocodb/src/controllers/sorts.controller.ts
  96. 1
      packages/nocodb/src/controllers/tables.controller.ts
  97. 117
      packages/nocodb/src/controllers/users/users.controller.ts
  98. 3
      packages/nocodb/src/controllers/view-columns.controller.ts
  99. 6
      packages/nocodb/src/db/BaseModelSql.ts
  100. 287
      packages/nocodb/src/db/BaseModelSqlv2.ts
  101. Some files were not shown because too many files have changed in this diff Show More

13860
package-lock.json generated

File diff suppressed because it is too large Load Diff

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

@ -53,9 +53,9 @@ const currency = computed(() => {
}
})
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
const submitCurrency = () => {
if (lastSaved.value !== vModel.value) {

52
packages/nc-gui/components/cell/DateTimePicker.vue

@ -18,13 +18,14 @@ import {
interface Props {
modelValue?: string | null
isPk?: boolean
isUpdatedFromCopyNPaste?: Record<string, boolean>
}
const { modelValue, isPk } = defineProps<Props>()
const { modelValue, isPk, isUpdatedFromCopyNPaste } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { isMysql } = useProject()
const { isMssql, isXcdbBase } = useProject()
const { showNull } = useGlobal()
@ -44,6 +45,8 @@ const dateTimeFormat = $computed(() => {
return `${dateFormat} ${timeFormat}`
})
let localModelValue = modelValue ? dayjs(modelValue).utc().local() : undefined
let localState = $computed({
get() {
if (!modelValue) {
@ -55,7 +58,45 @@ let localState = $computed({
return undefined
}
return /^\d+$/.test(modelValue) ? dayjs(+modelValue) : dayjs(modelValue)
const isXcDB = isXcdbBase(column.value.base_id)
// cater copy and paste
// when copying a datetime cell, the copied value would be local time
// when pasting a datetime cell, UTC (xcdb) will be saved in DB
// we convert back to local time
if (column.value.title! in (isUpdatedFromCopyNPaste ?? {})) {
localModelValue = dayjs(modelValue).utc().local()
return localModelValue
}
// ext db
if (!isXcDB) {
return /^\d+$/.test(modelValue) ? dayjs(+modelValue) : dayjs(modelValue)
}
if (isMssql(column.value.base_id)) {
// e.g. 2023-04-29T11:41:53.000Z
return dayjs(modelValue)
}
// if cdf is defined, that means the value is auto-generated
// hence, show the local time
if (column?.value?.cdf) {
return dayjs(modelValue).utc().local()
}
// if localModelValue is defined, show localModelValue instead
// localModelValue is set in setter below
if (localModelValue) {
const res = localModelValue
// resetting localModelValue here
// e.g. save in expanded form -> render the correct modelValue
localModelValue = undefined
return res
}
// empty cell - use modelValue in local time
return dayjs(modelValue).utc().local()
},
set(val?: dayjs.Dayjs) {
if (!val) {
@ -64,7 +105,10 @@ let localState = $computed({
}
if (val.isValid()) {
emit('update:modelValue', val?.format(isMysql(column.value.base_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'))
// setting localModelValue to cater NOW function in date picker
localModelValue = dayjs(val)
// send the payload in UTC format
emit('update:modelValue', dayjs(val).utc().format('YYYY-MM-DD HH:mm:ssZ'))
}
},
})

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

@ -36,9 +36,9 @@ const vModel = computed({
},
})
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
</script>
<template>

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

@ -74,9 +74,9 @@ const submitDuration = () => {
isEdited.value = false
}
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
</script>
<template>

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

@ -35,9 +35,9 @@ const vModel = computed({
const validEmail = computed(() => vModel.value && validateEmail(vModel.value))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
watch(
() => editEnabled.value,

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

@ -36,9 +36,9 @@ const vModel = computed({
},
})
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
</script>
<template>

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

@ -36,9 +36,9 @@ const vModel = computed({
},
})
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
function onKeyDown(evt: KeyboardEvent) {
return evt.key === '.' && evt.preventDefault()

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

@ -27,9 +27,9 @@ const vModel = computed({
},
})
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
</script>
<template>

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

@ -23,9 +23,9 @@ const readonly = inject(ReadonlyInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
</script>
<template>

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

@ -19,9 +19,9 @@ const { showNull } = useGlobal()
const vModel = useVModel(props, 'modelValue', emits, { defaultValue: '' })
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLTextAreaElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLTextAreaElement)?.focus()
</script>
<template>

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

@ -63,9 +63,9 @@ const url = computed(() => {
const { cellUrlOptions } = useCellUrlConfig(url)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
watch(
() => editEnabled.value,

33
packages/nc-gui/components/dashboard/TreeView.vue

@ -1,10 +1,11 @@
<script setup lang="ts">
import { nextTick } from '@vue/runtime-core'
import { Icon as IconifyIcon } from '@iconify/vue'
import type { TableType } from 'nocodb-sdk'
import type { Input } from 'ant-design-vue'
import { Dropdown, Tooltip, message } from 'ant-design-vue'
import Sortable from 'sortablejs'
import GithubButton from 'vue-github-button'
import { Icon } from '@iconify/vue'
import type { VNodeRef } from '#imports'
import {
ClientType,
@ -279,6 +280,15 @@ function openAirtableImportDialog(baseId?: string) {
}
}
function scrollToTable(table: TableType) {
// get the table node in the tree view using the data-id attribute
const el = document.querySelector(`.nc-tree-item[data-id="${table?.id}"]`)
// scroll to the table node if found
if (el) {
el.scrollIntoView({ behavior: 'smooth' })
}
}
function openTableCreateDialog(baseId?: string) {
$e('c:table:create:navdraw')
@ -288,6 +298,12 @@ function openTableCreateDialog(baseId?: string) {
'modelValue': isOpen,
'baseId': baseId || bases.value[0].id,
'onUpdate:modelValue': closeDialog,
'onCreate': (table: TableType) => {
// on new table created scroll to the table in the tree view
nextTick(() => {
scrollToTable(table)
})
},
})
function closeDialog() {
@ -399,12 +415,15 @@ const duplicateTable = async (table: TableType) => {
const { close } = useDialog(resolveComponent('DlgTableDuplicate'), {
'modelValue': isOpen,
'table': table,
'onOk': async (jobData: { name: string; id: string }) => {
$jobs.subscribe({ name: jobData.name, id: jobData.id }, undefined, async (status: string, data?: any) => {
'onOk': async (jobData: { id: string }) => {
$jobs.subscribe({ id: jobData.id }, undefined, async (status: string, data?: any) => {
if (status === JobStatus.COMPLETED) {
await loadTables()
const newTable = tables.value.find((el) => el.id === data?.result?.id)
if (newTable) addTableTab(newTable)
await nextTick(() => {
scrollToTable(newTable)
})
} else if (status === JobStatus.FAILED) {
message.error('Failed to duplicate table')
await loadTables()
@ -716,12 +735,12 @@ const duplicateTable = async (table: TableType) => {
<div class="flex items-center" @click.stop>
<component :is="isUIAllowed('tableIconCustomisation') ? Tooltip : 'div'">
<span v-if="table.meta?.icon" :key="table.meta?.icon" class="nc-table-icon flex items-center">
<Icon
<IconifyIcon
:key="table.meta?.icon"
:data-testid="`nc-icon-${table.meta?.icon}`"
class="text-xl"
:icon="table.meta?.icon"
></Icon>
></IconifyIcon>
</span>
<component
:is="icon(table)"
@ -1040,12 +1059,12 @@ const duplicateTable = async (table: TableType) => {
<div class="flex items-center" @click.stop>
<component :is="isUIAllowed('tableIconCustomisation') ? Tooltip : 'div'">
<span v-if="table.meta?.icon" :key="table.meta?.icon" class="nc-table-icon flex items-center">
<Icon
<IconifyIcon
:key="table.meta?.icon"
:data-testid="`nc-icon-${table.meta?.icon}`"
class="text-xl"
:icon="table.meta?.icon"
></Icon>
></IconifyIcon>
</span>
<component
:is="icon(table)"

7
packages/nc-gui/components/dlg/ProjectDuplicate.vue

@ -34,7 +34,12 @@ const isLoading = ref(false)
const _duplicate = async () => {
isLoading.value = true
try {
const jobData = await api.project.duplicate(props.project.id as string, optionsToExclude.value)
const jobData = await api.project.duplicate(props.project.id as string, {
options: optionsToExclude.value,
project: {
meta: props.project.meta,
},
})
props.onOk(jobData as any)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))

3
packages/nc-gui/components/dlg/TableCreate.vue

@ -19,7 +19,7 @@ const props = defineProps<{
baseId: string
}>()
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'create'])
const dialogShow = useVModel(props, 'modelValue', emit)
@ -40,6 +40,7 @@ const { table, createTable, generateUniqueTitle, tables, project } = useTable(as
type: TabType.TABLE,
})
emit('create', table)
dialogShow.value = false
}, props.baseId)

2
packages/nc-gui/components/dlg/TableDuplicate.vue

@ -34,7 +34,7 @@ const isLoading = ref(false)
const _duplicate = async () => {
isLoading.value = true
try {
const jobData = await api.dbTable.duplicate(props.table.project_id!, props.table.id!, optionsToExclude.value)
const jobData = await api.dbTable.duplicate(props.table.project_id!, props.table.id!, { options: optionsToExclude.value })
props.onOk(jobData as any)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))

4
packages/nc-gui/components/general/EmojiIcons.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue'
import { Icon as IconifyIcon } from '@iconify/vue'
import InfiniteLoading from 'v3-infinite-loading'
import { emojiIcons } from '#imports'
@ -48,7 +48,7 @@ const selectIcon = (icon?: string) => {
<div class="flex gap-1 flex-wrap w-full flex-shrink overflow-y-auto scrollbar-thin-dull">
<div v-for="icon of filteredIcons" :key="icon" @click="selectIcon(icon)">
<span class="cursor-pointer nc-emoji-item">
<Icon class="text-xl iconify" :icon="`emojione:${icon}`"></Icon>
<IconifyIcon class="text-xl iconify" :icon="`emojione:${icon}`"></IconifyIcon>
</span>
</div>
<InfiniteLoading @infinite="load"><span /></InfiniteLoading>

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

@ -209,7 +209,12 @@ onUnmounted(() => {
<LazyCellMultiSelect v-else-if="isMultiSelect(column)" v-model="vModel" :row-index="props.rowIndex" />
<LazyCellDatePicker v-else-if="isDate(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellYearPicker v-else-if="isYear(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellDateTimePicker v-else-if="isDateTime(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellDateTimePicker
v-else-if="isDateTime(column, abstractType)"
v-model="vModel"
:is-pk="isPrimaryKey(column)"
:is-updated-from-copy-n-paste="currentRow.rowMeta.isUpdatedFromCopyNPaste"
/>
<LazyCellTimePicker v-else-if="isTime(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellRating v-else-if="isRating(column)" v-model="vModel" />
<LazyCellDuration v-else-if="isDuration(column)" v-model="vModel" />

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

@ -837,7 +837,7 @@ watch(view, (nextView) => {
<style scoped lang="scss">
.nc-editable:hover {
.nc-field-remove-icon {
:deep(.nc-field-remove-icon) {
@apply opacity-100;
}
}

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

@ -7,6 +7,7 @@ import {
IsFormInj,
IsGalleryInj,
IsGridInj,
IsPublicInj,
MetaInj,
NavigateDir,
OpenNewRecordFormHookInj,
@ -62,6 +63,8 @@ provide(IsGridInj, ref(false))
provide(PaginationDataInj, paginationData)
provide(ChangePageInj, changePage)
const isPublic = inject(IsPublicInj, ref(false))
const fields = inject(FieldsInj, ref([]))
const route = useRoute()
@ -125,6 +128,10 @@ const attachments = (record: any): Attachment[] => {
}
const expandForm = (row: RowType, state?: Record<string, any>) => {
if (isPublic.value) {
return
}
const rowId = extractPkFromRow(row.row, meta.value!.columns!)
if (rowId) {
@ -234,6 +241,7 @@ watch(view, async (nextView) => {
:data-testid="`nc-gallery-card-${record.row.id}`"
@click="expandFormClick($event, record)"
@contextmenu="showContextMenu($event, { row: rowIndex })"
:style="isPublic ? { cursor: 'default' } : { cursor: 'pointer' }"
>
<template v-if="galleryData?.fk_cover_image_col_id" #cover>
<a-carousel v-if="!reloadAttachments && attachments(record).length" autoplay class="gallery-carousel" arrows>

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

@ -1,4 +1,5 @@
<script lang="ts" setup>
import { nextTick } from '@vue/runtime-core'
import type { ColumnReqType, ColumnType, GridType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import {
@ -141,11 +142,20 @@ const getContainerScrollForElement = (
) => {
const childPos = el.getBoundingClientRect()
const parentPos = container.getBoundingClientRect()
// provide an extra offset to show the prev/next/up/bottom cell
const extraOffset = 15
const numColWidth = container.querySelector('thead th:nth-child(1)')?.getBoundingClientRect().width ?? 0
const primaryColWidth = container.querySelector('thead th:nth-child(2)')?.getBoundingClientRect().width ?? 0
const stickyColsWidth = numColWidth + primaryColWidth
const relativePos = {
top: childPos.top - parentPos.top,
right: childPos.right - parentPos.right,
bottom: childPos.bottom - parentPos.bottom,
left: childPos.left - parentPos.left,
left: childPos.left - parentPos.left - stickyColsWidth,
}
const scroll = {
@ -159,9 +169,9 @@ const getContainerScrollForElement = (
*/
scroll.left =
relativePos.right + (offset?.right || 0) > 0
? container.scrollLeft + relativePos.right + (offset?.right || 0)
? container.scrollLeft + relativePos.right + (offset?.right || 0) + extraOffset
: relativePos.left - (offset?.left || 0) < 0
? container.scrollLeft + relativePos.left - (offset?.left || 0)
? container.scrollLeft + relativePos.left - (offset?.left || 0) - extraOffset
: container.scrollLeft
/*
@ -170,9 +180,9 @@ const getContainerScrollForElement = (
*/
scroll.top =
relativePos.bottom + (offset?.bottom || 0) > 0
? container.scrollTop + relativePos.bottom + (offset?.bottom || 0)
? container.scrollTop + relativePos.bottom + (offset?.bottom || 0) + extraOffset
: relativePos.top - (offset?.top || 0) < 0
? container.scrollTop + relativePos.top - (offset?.top || 0)
? container.scrollTop + relativePos.top - (offset?.top || 0) - extraOffset
: container.scrollTop
return scroll
@ -316,6 +326,12 @@ const {
return
}
// See DateTimePicker.vue for details
data.value[ctx.row].rowMeta.isUpdatedFromCopyNPaste = {
...data.value[ctx.row].rowMeta.isUpdatedFromCopyNPaste,
[ctx.updatedColumnTitle || columnObj.title]: true,
}
// update/save cell value
await updateOrSaveRow(rowObj, ctx.updatedColumnTitle || columnObj.title)
},
@ -336,6 +352,12 @@ function scrollToCell(row?: number | null, col?: number | null) {
const { height: headerHeight } = tableHead.value!.getBoundingClientRect()
const tdScroll = getContainerScrollForElement(td, gridWrapper.value, { top: headerHeight, bottom: 9, right: 9 })
// if first column set left to 0 since it's sticky it will be visible and calculated value will be wrong
// setting left to 0 will make it scroll to the left
if (col === 0) {
tdScroll.left = 0
}
if (rows && row === rows.length - 2) {
// if last row make 'Add New Row' visible
gridWrapper.value.scrollTo({
@ -625,6 +647,9 @@ const onNavigate = (dir: NavigateDir) => {
}
break
}
nextTick(() => {
scrollToCell()
})
}
const showContextMenu = (e: MouseEvent, target?: { row: number; col: number }) => {

1
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -188,7 +188,6 @@ useEventListener('keydown', (e: KeyboardEvent) => {
<LazySmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="formState" />
<LazySmartsheetColumnBarcodeOptions v-if="formState.uidt === UITypes.Barcode" v-model="formState" />
<LazySmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" />
<LazySmartsheetColumnGeoDataOptions v-if="formState.uidt === UITypes.GeoData" v-model:value="formState" />
<LazySmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<LazySmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />
<LazySmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />

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

@ -52,7 +52,7 @@ const openHeaderMenu = () => {
<span
v-if="column"
class="name"
:class="{ 'cursor-pointer': !isForm && isUIAllowed('edit-column') }"
:class="{ 'cursor-pointer': !isForm && isUIAllowed('edit-column') && !hideMenu }"
style="white-space: nowrap"
:title="column.title"
@dblclick="openHeaderMenu"

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

@ -225,7 +225,8 @@ defineExpose({
/>
<span v-else :key="`${i}dummy`" />
<div :key="`${i}nested`" class="flex">
<span v-if="!i" class="flex items-center">{{ $t('labels.where') }}</span>
<div v-else :key="`${i}nested`" class="flex bob">
<a-select
v-model:value="filter.logical_op"
:dropdown-match-select-width="false"

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

@ -7,6 +7,7 @@ import {
ColumnInj,
IsFormInj,
IsLockedInj,
IsUnderLookupInj,
ReadonlyInj,
ReloadRowDataHookInj,
RowInj,
@ -36,6 +37,8 @@ const isForm = inject(IsFormInj, ref(false))
const isLocked = inject(IsLockedInj, ref(false))
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const { isUIAllowed } = useUIPermission()
const listItemsDlg = ref(false)
@ -89,7 +92,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
</div>
<div
v-if="!readOnly && !isLocked && (isUIAllowed('xcDatatableEditable') || isForm)"
v-if="!readOnly && !isLocked && (isUIAllowed('xcDatatableEditable') || isForm) && !isUnderLookup"
class="flex justify-end gap-1 min-h-[30px] items-center"
>
<GeneralIcon

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

@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { CellValueInj, ColumnInj, computed, handleTZ, inject, replaceUrlsWithLink, useProject } from '#imports'
import { CellValueInj, ColumnInj, computed, handleTZ, inject, renderValue, replaceUrlsWithLink, useProject } from '#imports'
// todo: column type doesn't have required property `error` - throws in typecheck
const column = inject(ColumnInj) as Ref<ColumnType & { colOptions: { error: any } }>
@ -10,7 +10,9 @@ const cellValue = inject(CellValueInj)
const { isPg } = useProject()
const result = computed(() => (isPg(column.value.base_id) ? handleTZ(cellValue?.value) : cellValue?.value))
const result = computed(() =>
isPg(column.value.base_id) ? renderValue(handleTZ(cellValue?.value)) : renderValue(cellValue?.value),
)
const urls = computed(() => replaceUrlsWithLink(result.value))

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

@ -16,6 +16,7 @@ import {
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow,
useUIPermission,
IsUnderLookupInj
} from '#imports'
const column = inject(ColumnInj)!
@ -32,6 +33,8 @@ const readOnly = inject(ReadonlyInj, ref(false))
const isLocked = inject(IsLockedInj)
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const listItemsDlg = ref(false)
const childListDlg = ref(false)
@ -112,7 +115,7 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
</template>
</div>
<div v-if="!isLocked" class="flex justify-end gap-1 min-h-[30px] items-center">
<div v-if="!isLocked && !isUnderLookup" class="flex justify-end gap-1 min-h-[30px] items-center">
<GeneralIcon
icon="expand"
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"

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

@ -5,6 +5,7 @@ import {
CellUrlDisableOverlayInj,
CellValueInj,
ColumnInj,
IsUnderLookupInj,
MetaInj,
computed,
inject,
@ -70,6 +71,8 @@ const arrValue = computed(() => {
provide(MetaInj, lookupTableMeta)
provide(IsUnderLookupInj, ref(true))
provide(CellUrlDisableOverlayInj, ref(true))
const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activateShowEditNonEditableFieldWarning } =

5
packages/nc-gui/components/virtual-cell/ManyToMany.vue

@ -7,6 +7,7 @@ import {
ColumnInj,
IsFormInj,
IsLockedInj,
IsUnderLookupInj,
ReadonlyInj,
ReloadRowDataHookInj,
RowInj,
@ -34,6 +35,8 @@ const readOnly = inject(ReadonlyInj, ref(false))
const isLocked = inject(IsLockedInj)
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const listItemsDlg = ref(false)
const childListDlg = ref(false)
@ -114,7 +117,7 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
</template>
</div>
<div v-if="!isLocked" class="flex justify-end gap-1 min-h-[30px] items-center">
<div v-if="!isLocked && !isUnderLookup" class="flex justify-end gap-1 min-h-[30px] items-center">
<GeneralIcon
icon="expand"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"

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

@ -7,9 +7,9 @@ import {
iconMap,
inject,
ref,
renderValue,
useExpandedFormDetached,
useLTARStoreOrThrow,
useUIPermission,
} from '#imports'
interface Props {
@ -60,7 +60,7 @@ export default {
:class="{ active }"
@click="openExpandedForm"
>
<span class="name">{{ value }}</span>
<span class="name">{{ renderValue(value) }}</span>
<div v-show="active || isForm" v-if="!readOnly && !isLocked && isUIAllowed('xcDatatableEditable')" class="flex items-center">
<component

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

@ -13,10 +13,10 @@ import {
iconMap,
inject,
ref,
renderValue,
useLTARStoreOrThrow,
useSmartsheetRowStoreOrThrow,
useVModel,
watch,
} from '#imports'
const props = defineProps<{ modelValue?: boolean; cellValue: any }>()
@ -148,7 +148,7 @@ const onClick = (row: Row) => {
>
<div class="flex items-center">
<div class="flex-1 overflow-hidden min-w-0">
{{ row[relatedTableDisplayValueProp] }}
{{ renderValue(row[relatedTableDisplayValueProp]) }}
<span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span>
</div>

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

@ -12,11 +12,11 @@ import {
inject,
isDrawerExist,
ref,
renderValue,
useLTARStoreOrThrow,
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow,
useVModel,
watch,
} from '#imports'
const props = defineProps<{ modelValue: boolean }>()
@ -213,7 +213,7 @@ watch(vModel, (nextVal) => {
<component :is="iconMap.reload" class="cursor-pointer text-gray-500 nc-reload" @click="loadChildrenExcludedList" />
<!-- Add new record -->
<!-- Add new record -->
<a-button v-if="!isPublic" type="primary" size="small" @click="expandedFormDlg = true">
{{ $t('activity.addNewRecord') }}
</a-button>
@ -229,7 +229,7 @@ watch(vModel, (nextVal) => {
:class="{ 'nc-selected-row': selectedRowIndex === i }"
@click="linkRow(refRow)"
>
{{ refRow[relatedTableDisplayValueProp] }}
{{ renderValue(refRow[relatedTableDisplayValueProp]) }}
<span class="hidden group-hover:(inline) text-gray-400 text-[11px] ml-1">
({{ $t('labels.primaryKey') }} : {{ getRelatedTableRowId(refRow) }})
</span>

3
packages/nc-gui/composables/useApi/interceptors.ts

@ -67,9 +67,8 @@ export function addAxiosInterceptors(api: Api<any>) {
})
.catch(async (error) => {
await state.signOut()
// todo: handle new user
navigateTo('/signIn')
if (!route.meta.public) navigateTo('/signIn')
return Promise.reject(error)
})

6
packages/nc-gui/composables/useGridViewColumnWidth.ts

@ -13,7 +13,7 @@ import {
watch,
} from '#imports'
export function useGridViewColumnWidth(view: Ref<ViewType | undefined>) {
export function useGridViewColumnWidth(view: Ref<(ViewType & { columns?: GridColumnType[] }) | undefined>) {
const { css, load: loadCss, unload: unloadCss } = useStyleTag('')
const { isUIAllowed } = useUIPermission()
@ -52,7 +52,9 @@ export function useGridViewColumnWidth(view: Ref<ViewType | undefined>) {
const loadGridViewColumns = async () => {
if (!view.value?.id && !isPublic.value) return
const colsData: GridColumnType[] = (isPublic.value ? columns.value : await $api.dbView.gridColumnsList(view.value!.id!)) ?? []
const colsData: GridColumnType[] =
(isPublic.value ? view.value?.columns : await $api.dbView.gridColumnsList(view.value!.id!)) ?? []
gridViewCols.value = colsData.reduce<Record<string, GridColumnType>>(
(o, col) => ({
...o,

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

@ -42,7 +42,7 @@ export default function convertCellData(
if (!parsedDateTime.isValid()) {
throw new Error('Not a valid datetime value')
}
return parsedDateTime.format(dateFormat)
return parsedDateTime.utc().format('YYYY-MM-DD HH:mm:ssZ')
}
case UITypes.Time: {
let parsedTime = dayjs(value)

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

@ -1,3 +1,4 @@
import dayjs from 'dayjs'
import type { MaybeRef } from '@vueuse/core'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
@ -7,6 +8,7 @@ import convertCellData from './convertCellData'
import type { Nullable, Row } from '~/lib'
import {
copyTable,
dateFormats,
extractPkFromRow,
extractSdkResponseErrorMsg,
isMac,
@ -14,6 +16,7 @@ import {
message,
reactive,
ref,
timeFormats,
unref,
useCopy,
useEventListener,
@ -79,6 +82,20 @@ export function useMultiSelect(
activeCell.col = col
}
function constructDateTimeFormat(column: ColumnType) {
const dateFormat = constructDateFormat(column)
const timeFormat = constructTimeFormat(column)
return `${dateFormat} ${timeFormat}`
}
function constructDateFormat(column: ColumnType) {
return parseProp(column?.meta)?.date_format ?? dateFormats[0]
}
function constructTimeFormat(column: ColumnType) {
return parseProp(column?.meta)?.time_format ?? timeFormats[0]
}
async function copyValue(ctx?: Cell) {
try {
if (selectedRange.start !== null && selectedRange.end !== null && !selectedRange.isSingleCell()) {
@ -106,6 +123,43 @@ export function useMultiSelect(
if (typeof textToCopy === 'object') {
textToCopy = JSON.stringify(textToCopy)
}
if (columnObj.uidt === UITypes.Formula) {
textToCopy = textToCopy.replace(/\b(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2})\b/g, (d: string) => {
// TODO(timezone): retrieve the format from the corresponding column meta
// assume hh:mm at this moment
return dayjs(d).utc().local().format('YYYY-MM-DD HH:mm')
})
}
if (columnObj.uidt === UITypes.DateTime || columnObj.uidt === UITypes.Time) {
// remove `"`
// e.g. "2023-05-12T08:03:53.000Z" -> 2023-05-12T08:03:53.000Z
textToCopy = textToCopy.replace(/["']/g, '')
const isMySQL = isMysql(columnObj.base_id)
let d = dayjs(textToCopy)
if (!d.isValid()) {
// insert a datetime value, copy the value without refreshing
// e.g. textToCopy = 2023-05-12T03:49:25.000Z
// feed custom parse format
d = dayjs(textToCopy, isMySQL ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ')
}
// users can change the datetime format in UI
// `textToCopy` would be always in YYYY-MM-DD HH:mm:ss(Z / +xx:yy) format
// therefore, here we reformat to the correct datetime format based on the meta
textToCopy = d.format(
columnObj.uidt === UITypes.DateTime ? constructDateTimeFormat(columnObj) : constructTimeFormat(columnObj),
)
if (columnObj.uidt === UITypes.DateTime && !dayjs(textToCopy).isValid()) {
throw new Error('Invalid DateTime')
}
}
await copy(textToCopy)
message.success(t('msg.info.copiedToClipboard'))
}
@ -154,6 +208,7 @@ export function useMultiSelect(
selectedRange.startRange({ row, col })
selectedRange.endRange({ row, col })
makeActive(row, col)
scrollToActiveCell?.()
isMouseDown = false
}

1
packages/nc-gui/context/index.ts

@ -38,3 +38,4 @@ export const ToggleDialogInj: InjectionKey<Function> = Symbol('toggle-dialog-inj
export const CellClickHookInj: InjectionKey<EventHook<MouseEvent> | undefined> = Symbol('cell-click-injection')
export const SaveRowInj: InjectionKey<(() => void) | undefined> = Symbol('save-row-injection')
export const CurrentCellInj: InjectionKey<Ref<Element | undefined>> = Symbol('current-cell-injection')
export const IsUnderLookupInj: InjectionKey<Ref<boolean>> = Symbol('is-under-lookup-injection')

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

@ -260,7 +260,7 @@
"barcodeFormat": "Format du code-barres",
"qrCodeValueTooLong": "Trop de caractères pour un code QR",
"barcodeValueTooLong": "Trop de caractères pour un code-barres",
"currentLocation": "Current Location",
"currentLocation": "Emplacement actuel",
"lng": "Lng",
"lat": "Lat",
"aggregateFunction": "Fonction agrégée",
@ -385,12 +385,12 @@
"nextRecord": "Ligne suivante",
"previousRecord": "Ligne précédente",
"copyApiURL": "Copier l'URL de l'API",
"createTable": "Create New Table",
"createTable": "Créer une nouvelle table",
"refreshTable": "Actualiser le tableau",
"renameTable": "Rename Table",
"deleteTable": "Delete Table",
"renameTable": "Renommer la table",
"deleteTable": "Supprimer la table",
"addField": "Ajouter un nouveau champ à ce tableau",
"setDisplay": "Set as Display value",
"setDisplay": "Définir comme valeur d'affichage",
"addRow": "Ajouter une nouvelle ligne",
"saveRow": "Enregistrer la ligne",
"saveAndExit": "Enregistrer et quitter",
@ -580,7 +580,7 @@
"afterEnablePwd": "L’accès est restreint par un mot de passe",
"privateLink": "Cette vue est partagée avec un lien privé",
"privateLinkAdditionalInfo": "Les personnes ayant le lien privé peuvent voir uniquement les cellules visibles de cette vue",
"afterFormSubmitted": "Après que le formulaire ait été soumis",
"afterFormSubmitted": "Après que le formulaire a été soumis",
"apiOptions": "Accéder au projet via",
"submitAnotherForm": "Afficher le bouton \"Soumettre un autre formulaire\"",
"showBlankForm": "Montrer un formulaire vierge après 5 secondes",

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

@ -354,7 +354,7 @@
"account": {
"authToken": "Скопировать токен авторизации",
"swagger": "Swagger: REST APIs",
"projInfo": "Скопировать информацию о проекте",
"projInfo": "Информация о проекте",
"themes": "Темы"
},
"sort": "Сортировать",
@ -385,10 +385,10 @@
"nextRecord": "Следующая запись",
"previousRecord": "Предыдущая запись",
"copyApiURL": "Скопируйте URL API",
"createTable": "Create New Table",
"createTable": "Создать новую таблицу",
"refreshTable": "Обновление таблицы",
"renameTable": "Rename Table",
"deleteTable": "Delete Table",
"renameTable": "Переименовать таблицу",
"deleteTable": "Удалить таблицу",
"addField": "Добавить новое поле в эту таблицу",
"setDisplay": "Установить как значение отображения",
"addRow": "Добавить новую строку",

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

@ -39,7 +39,7 @@
"signIn": "KAYIT OL",
"signOut": "Oturumu Kapat",
"required": "Gerekli",
"enableScanner": "Enable Scanner for filling",
"enableScanner": "Doldurmak için tarayıcıyı etkinleştir",
"preferred": "Tercihen",
"mandatory": "Zorunlu",
"loading": "Yükleniyor ...",
@ -76,7 +76,7 @@
"hideField": "Alanı Gizle",
"sortAsc": "Artan Sırala",
"sortDesc": "Azalan Sıralama",
"geoDataField": "GeoData Field"
"geoDataField": "GeoData Alanı"
},
"objects": {
"project": "Proje",
@ -101,7 +101,7 @@
"form": "Form",
"kanban": "Kanban",
"calendar": "Takvim",
"map": "Map"
"map": "Harita"
},
"user": "Kullanıcı",
"users": "Kullanıcılar",
@ -210,8 +210,8 @@
"advancedSettings": "Gelişmiş Ayarlar",
"codeSnippet": "Kod Parçacığı",
"keyboardShortcut": "Klavye Kısayolları",
"generateRandomName": "Generate Random Name",
"findRowByScanningCode": "Find row by scanning a QR or Barcode"
"generateRandomName": "Rastgele Ad Oluştur",
"findRowByScanningCode": "QR veya Barkod okutarak satır bulun"
},
"labels": {
"createdBy": "Tarafından Oluşturuldu",
@ -221,7 +221,7 @@
"viewName": "Görünüm adı",
"viewLink": "Görünüm Linki",
"columnName": "Sütun Adı",
"columnToScanFor": "Column to scan",
"columnToScanFor": "Taranacak sütun",
"columnType": "Sütun Tipi",
"roleName": "Rol Adı",
"roleDescription": "Rol Tanımı",
@ -238,7 +238,7 @@
"action": "Aksiyon",
"actions": "Aksiyonlar",
"operation": "İşlem",
"operationSub": "Sub Operation",
"operationSub": "Alt İşlem",
"operationType": "İşlem türü",
"operationSubType": "İşlem alt-türü",
"description": "Tanım",
@ -260,9 +260,9 @@
"barcodeFormat": "Barkod formatı",
"qrCodeValueTooLong": "QR kodu için çok fazla karakter",
"barcodeValueTooLong": "Barkod için çok fazla karakter",
"currentLocation": "Current Location",
"lng": "Lng",
"lat": "Lat",
"currentLocation": "Geçerli Konum",
"lng": "Boylam",
"lat": "Enlem",
"aggregateFunction": "Birleştirme fonksiyonu",
"dbCreateIfNotExists": "Veritabanı : yoksa oluştur",
"clientKey": "İstemci Anahtarı",
@ -385,18 +385,18 @@
"nextRecord": "Sonraki kayıt",
"previousRecord": "Önceki kayıt",
"copyApiURL": "API linkini kopyala",
"createTable": "Create New Table",
"createTable": "Yeni tablo oluştur",
"refreshTable": "Tabloları Yenile",
"renameTable": "Rename Table",
"deleteTable": "Delete Table",
"renameTable": "Tabloyu Yeniden Adlandır",
"deleteTable": "Tabloyu Sil",
"addField": "Tabloya yeni alan ekle",
"setDisplay": "Set as Display value",
"setDisplay": "Görünüm değeri olarak ayarla",
"addRow": "Yeni satır ekle",
"saveRow": "Satırı kaydet",
"saveAndExit": "Kaydet ve Çık",
"saveAndStay": "Kaydet ve Kal",
"insertRow": "Yeni Satır Ekle",
"duplicateRow": "Duplicate Row",
"duplicateRow": "Satırı Çoğalt",
"deleteRow": "Satırı Sil",
"deleteSelectedRow": "Seçilen Satırları Sil",
"importExcel": "Excel içe aktar",
@ -412,8 +412,8 @@
"changePwd": "Şifre değiştir",
"createView": "Bir görünüm oluştur",
"shareView": "Görünümü paylaş",
"findRowByCodeScan": "Find row by scan",
"fillByCodeScan": "Fill by scan",
"findRowByCodeScan": "Tarayarak satır bul",
"fillByCodeScan": "Tarayarak doldur",
"listSharedView": "Paylaşılan Görünümler",
"ListView": "Görünüm Listesi",
"copyView": "Görünümü kopyala",
@ -429,10 +429,10 @@
"openTab": "Yeni sekme aç",
"iFrame": "Gömülü HTML kodunu kopyalayın",
"addWebhook": "Yeni Webhook ekle",
"enableWebhook": "Enable Webhook",
"testWebhook": "Test Webhook",
"copyWebhook": "Copy Webhook",
"deleteWebhook": "Delete Webhook",
"enableWebhook": "Webhook'u Etkinleştir",
"testWebhook": "Webhook'u Test Et",
"copyWebhook": "Webhook'u Kopyala",
"deleteWebhook": "Webhook'u Sil",
"newToken": "Yeni Token ekle",
"exportZip": "Zip olarak dışa aktar",
"importZip": "Zip olarak içe aktar",
@ -472,10 +472,10 @@
"map": {
"mappedBy": "Mapped By",
"chooseMappingField": "Choose a Mapping Field",
"openInGoogleMaps": "Google Maps",
"openInGoogleMaps": "Google Haritalar",
"openInOpenStreetMap": "OSM"
},
"toggleMobileMode": "Toggle Mobile Mode"
"toggleMobileMode": "Mobil Modu Aç / Kapat"
},
"tooltip": {
"saveChanges": "Değişiklikleri Kaydet",
@ -542,15 +542,15 @@
"orgViewer": "İzleyicinin yeni proje oluşturmasına izin verilmez ancak davet edilen herhangi bir projeye erişebilir."
},
"codeScanner": {
"loadingScanner": "Loading the scanner...",
"selectColumn": "Select a column (QR code or Barcode) that you want to use for finding a row by scanning.",
"moreThanOneRowFoundForCode": "More than one row found for this code. Currently only unique codes are supported.",
"noRowFoundForCode": "No row found for this code for the selected column"
"loadingScanner": "Tarayıcı yükleniyor...",
"selectColumn": "Tarayarak satır bulmak için kullanmak istediğiniz sütunu (QR kodu veya Barkod) seçin.",
"moreThanOneRowFoundForCode": "Bu kod için birden fazla satır bulundu. Şu anda yalnızca benzersiz kodlar desteklenmektedir.",
"noRowFoundForCode": "Seçilen sütunda bu kod için satır bulunamadı"
},
"map": {
"overLimit": "You're over the limit.",
"closeLimit": "You're getting close to the limit.",
"limitNumber": "The limit of markers shown in a Map View is 1000 records."
"overLimit": "Sınırı aştınız.",
"closeLimit": "Sınıra yaklaşıyorsunuz.",
"limitNumber": "Bir Harita Görünümünde en fazla 1000 kayıt gösterilebilir."
},
"footerInfo": "Sayfa başına satır",
"upload": "Yüklenecek Dosyayı Seçin",
@ -634,7 +634,7 @@
"gallery": "Galeri görünümü ekle",
"form": "Form görünümü ekle",
"kanban": "Kanban görünümü ekle",
"map": "Add Map View",
"map": "Harita Görünümü Ekle",
"calendar": "Takvim görünümü ekle"
},
"tablesMetadataInSync": "Tablonun meta verileri senkronize",
@ -666,11 +666,11 @@
"deleteViewConfirmation": "Bu görünümü silmek istediğinizden emin misiniz?",
"deleteTableConfirmation": "Tabloyu silmek istiyor musunuz",
"showM2mTables": "M2M Tablolarını Göster",
"showM2mTablesDesc": "Many-to-many relation is supported via a junction table & is hidden by default. Enable this option to list all such tables along with existing tables.",
"showNullInCells": "Show NULL in Cells",
"showNullInCellsDesc": "Display 'NULL' tag in cells holding NULL value. This helps differentiate against cells holding EMPTY string.",
"showNullAndEmptyInFilter": "Show NULL and EMPTY in Filter",
"showNullAndEmptyInFilterDesc": "Enable 'additional' filters to differentiate fields containing NULL & Empty Strings. Default support for Blank treats both NULL & Empty strings alike.",
"showM2mTablesDesc": "Çoktan çoğa ilişki bir köprü tablosu aracılığıyla oluşturulur ve bu tablo varsayılan olarak gizlenir. Mevcut tablolarla birlikte bu köprü tablolarını da listelemek için bu seçeneği etkinleştirin.",
"showNullInCells": "Hücrelerde NULL Göster",
"showNullInCellsDesc": "NULL değerini taşıyan hücreler için 'NULL' etiketini göster. Bu BOŞ dizi değerleri ile ayırt etmenize yardımcı olur.",
"showNullAndEmptyInFilter": "Filtrelerde NULL ve EMPTY Göster",
"showNullAndEmptyInFilterDesc": "NULL ve Boş dizileri ayırt etmek için 'Ekstra' filtreleri etkinleştir. Varsayılan 'Blank' değeri NULL ve boş dizileri aynı şekilde ele alır.",
"deleteKanbanStackConfirmation": "Bu yığının silinmesi `{stackToBeDeleted}` seçim seçeneğini `{groupingField}` adresinden de kaldıracaktır. Kayıtlar kategorize edilmemiş yığına taşınacaktır.",
"computedFieldEditWarning": "Hesaplanan alan: içerik salt okunurdur. Yeniden yapılandırmak için sütun düzenleme menüsünü kullanın",
"computedFieldDeleteWarning": "Hesaplanan alan: içerik salt okunurdur. İçerik temizlenemiyor.",
@ -698,7 +698,7 @@
"allowedSpecialCharList": "İzin verilen özel karakter listesi"
},
"invalidURL": "Geçersiz URL",
"invalidEmail": "Invalid Email",
"invalidEmail": "Geçersiz E-posta",
"internalError": "Bazı dahili hatalar oluştu",
"templateGeneratorNotFound": "Şablon Oluşturucu bulunamıyor!",
"fileUploadFailed": "Dosya yüklenemedi",
@ -726,7 +726,7 @@
"nameShouldStartWithAnAlphabetOr_": "İsim bir alfabe veya _ ile başlamalıdır",
"followingCharactersAreNotAllowed": "Aşağıdaki karakterlere izin verilmez",
"columnNameRequired": "Sütun adı gereklidir",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"columnNameExceedsCharacters": "Sütun adının uzunluğu maksimum {value} karakter sınırını aşıyor",
"projectNameExceeds50Characters": "Proje adı 50 karakteri aşıyor",
"projectNameCannotStartWithSpace": "Proje adı boşlukla başlayamaz",
"requiredField": "Zorunlu alan",
@ -759,7 +759,7 @@
},
"success": {
"columnDuplicated": "Sütun başarıyla çoğaltıldı",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"rowDuplicatedWithoutSavedYet": "Satır çoğaltıldı (kaydedilmedi)",
"updatedUIACL": "Tablolar için UI ACL başarıyla güncellendi",
"pluginUninstalled": "Eklenti başarıyla kaldırıldı",
"pluginSettingsSaved": "Eklenti ayarları başarıyla kaydedildi",
@ -779,7 +779,7 @@
"userDeletedFromProject": "Kullanıcı projeden başarıyla silindi",
"inviteEmailSent": "Davet E-postası başarıyla gönderildi",
"inviteURLCopied": "Panoya kopyalanan davet URL'si",
"commentCopied": "Comment copied to clipboard",
"commentCopied": "Yorum panoya kopyalandı",
"passwordResetURLCopied": "Panoya kopyalanan parola sıfırlama URL'si",
"shareableURLCopied": "Paylaşılabilir temel URL panoya kopyalandı!",
"embeddableHTMLCodeCopied": "Yerleştirilebilir HTML kodu kopyalandı!",

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

@ -68,7 +68,7 @@
"questions": "问题",
"reachOut": "联系我们",
"betaNote": "此功能仍在测试中。",
"moreInfo": "这里可以找到更多信息",
"moreInfo": "点击此处了解更多信息。",
"logs": "日志",
"groupingField": "分组字段",
"insertAfter": "在右侧插入列",
@ -385,10 +385,10 @@
"nextRecord": "下一条记录",
"previousRecord": "上一条纪录",
"copyApiURL": "复制 API 链接",
"createTable": "Create New Table",
"createTable": "创建新的表格",
"refreshTable": "刷新表格",
"renameTable": "Rename Table",
"deleteTable": "Delete Table",
"renameTable": "重命名表格",
"deleteTable": "删除表格",
"addField": "添加新字段",
"setDisplay": "设置为显示值",
"addRow": "添加新行",

2
packages/nc-gui/lib/types.ts

@ -60,6 +60,8 @@ export interface Row {
commentCount?: number
changed?: boolean
saving?: boolean
// use in datetime picker component
isUpdatedFromCopyNPaste?: Record<string, boolean>
}
}

1
packages/nc-gui/nuxt-shim.d.ts vendored

@ -18,7 +18,6 @@ declare module '#app/nuxt' {
job:
| {
id: string
name: string
}
| any,
subscribedCb?: () => void,

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

@ -110,7 +110,7 @@
}
},
"../nocodb-sdk": {
"version": "0.107.4",
"version": "0.108.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",

4
packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Icon as IconifyIcon } from '@iconify/vue'
import type { TabItem } from '~/lib'
import { TabType } from '~/lib'
import { TabMetaInj, iconMap, provide, storeToRefs, useGlobal, useSidebar, useTabs } from '#imports'
@ -58,7 +58,7 @@ const hideSidebarOnClickOrTouchIfMobileMode = () => {
<template #tab>
<div class="flex items-center gap-2" data-testid="nc-tab-title">
<div class="flex items-center">
<Icon
<IconifyIcon
v-if="tab.meta?.icon"
:icon="tab.meta?.icon"
class="text-xl"

13
packages/nc-gui/pages/index/index/index.vue

@ -90,10 +90,10 @@ const duplicateProject = (project: ProjectType) => {
const { close } = useDialog(resolveComponent('DlgProjectDuplicate'), {
'modelValue': isOpen,
'project': project,
'onOk': async (jobData: { name: string; id: string }) => {
'onOk': async (jobData: { id: string }) => {
await loadProjects()
$jobs.subscribe({ name: jobData.name, id: jobData.id }, undefined, async (status: string) => {
$jobs.subscribe({ id: jobData.id }, undefined, async (status: string) => {
if (status === JobStatus.COMPLETED) {
await loadProjects()
} else if (status === JobStatus.FAILED) {
@ -308,6 +308,7 @@ const copyProjectMeta = async () => {
<div v-if="record.status !== ProjectStatus.JOB" class="flex items-center gap-2">
<component
:is="iconMap.edit"
v-if="isUIAllowed('projectUpdate', true)"
v-e="['c:project:edit:rename']"
class="nc-action-btn"
@click.stop="navigateTo(`/${text}`)"
@ -315,12 +316,18 @@ const copyProjectMeta = async () => {
<component
:is="iconMap.delete"
v-if="isUIAllowed('projectDelete', true)"
class="nc-action-btn"
:data-testid="`delete-project-${record.title}`"
@click.stop="deleteProject(record)"
/>
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-import-menu" @click.stop>
<a-dropdown
v-if="isUIAllowed('duplicateProject', true)"
:trigger="['click']"
overlay-class-name="nc-dropdown-import-menu"
@click.stop
>
<GeneralIcon
icon="threeDotVertical"
class="nc-import-menu outline-0"

2
packages/nc-gui/plugins/a.dayjs.ts

@ -4,6 +4,7 @@ import customParseFormat from 'dayjs/plugin/customParseFormat.js'
import duration from 'dayjs/plugin/duration.js'
import utc from 'dayjs/plugin/utc.js'
import weekday from 'dayjs/plugin/weekday.js'
import timezone from 'dayjs/plugin/timezone.js'
import { defineNuxtPlugin } from '#imports'
export default defineNuxtPlugin(() => {
@ -12,4 +13,5 @@ export default defineNuxtPlugin(() => {
extend(customParseFormat)
extend(duration)
extend(weekday)
extend(timezone)
})

17
packages/nc-gui/plugins/jobs.ts

@ -29,22 +29,22 @@ export default defineNuxtPlugin(async (nuxtApp) => {
await init(nuxtApp.$state.token.value)
}
const send = (name: string, data: any) => {
const send = (evt: string, data: any) => {
if (socket) {
const _id = messageIndex++
socket.emit(name, { _id, data })
socket.emit(evt, { _id, data })
return _id
}
}
const jobs = {
subscribe(
job: { id: string; name: string } | any,
job: { id: string } | any,
subscribedCb?: () => void,
statusCb?: (status: JobStatus, data?: any) => void,
logCb?: (data: { message: string }) => void,
) {
const logFn = (data: { id: string; name: string; data: { message: string } }) => {
const logFn = (data: { id: string; data: { message: string } }) => {
if (data.id === job.id) {
if (logCb) logCb(data.data)
}
@ -61,11 +61,10 @@ export default defineNuxtPlugin(async (nuxtApp) => {
const _id = send('subscribe', job)
const subscribeFn = (data: { _id: number; name: string; id: string }) => {
const subscribeFn = (data: { _id: number; id: string }) => {
if (data._id === _id) {
if (data.id !== job.id || data.name !== job.name) {
if (data.id !== job.id) {
job.id = data.id
job.name = data.name
}
if (subscribedCb) subscribedCb()
socket?.on('log', logFn)
@ -75,10 +74,10 @@ export default defineNuxtPlugin(async (nuxtApp) => {
}
socket?.on('subscribed', subscribeFn)
},
getStatus(name: string, id: string): Promise<string> {
getStatus(id: string): Promise<string> {
return new Promise((resolve) => {
if (socket) {
const _id = send('status', { name, id })
const _id = send('status', { id })
const tempFn = (data: any) => {
if (data._id === _id) {
resolve(data.status)

7
packages/nc-gui/store/project.ts

@ -82,6 +82,10 @@ export const useProject = defineStore('projectStore', () => {
return ['mysql', ClientType.MYSQL].includes(getBaseType(baseId))
}
function isSqlite(baseId?: string) {
return getBaseType(baseId) === ClientType.SQLITE
}
function isMssql(baseId?: string) {
return getBaseType(baseId) === 'mssql'
}
@ -91,7 +95,7 @@ export const useProject = defineStore('projectStore', () => {
}
function isXcdbBase(baseId?: string) {
return bases.value.find((base) => base.id === baseId)?.is_meta
return (bases.value.find((base) => base.id === baseId)?.is_meta as boolean) || false
}
const isSharedBase = computed(() => projectType === 'base')
@ -209,6 +213,7 @@ export const useProject = defineStore('projectStore', () => {
isMysql,
isMssql,
isPg,
isSqlite,
sqlUis,
isSharedBase,
loadProjectMetaInfo,

23
packages/nc-gui/utils/cell.ts

@ -1,5 +1,6 @@
import type { ColumnType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import dayjs from 'dayjs'
export const dataTypeLow = (column: ColumnType) => column.dt?.toLowerCase()
export const isBoolean = (column: ColumnType, abstractType?: any) =>
@ -53,3 +54,25 @@ export const isManualSaved = (column: ColumnType) => [UITypes.Currency].includes
export const isPrimary = (column: ColumnType) => !!column.pv
export const isPrimaryKey = (column: ColumnType) => !!column.pk
// used for LTAR and Formula
export const renderValue = (result?: any) => {
if (!result || typeof result !== 'string') {
return result
}
// convert ISO string (e.g. in MSSQL) to YYYY-MM-DD hh:mm:ssZ
// e.g. 2023-05-18T05:30:00.000Z -> 2023-05-18 11:00:00+05:30
result = result.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/g, (d: string) => {
return dayjs(d).isValid() ? dayjs(d).format('YYYY-MM-DD HH:mm:ssZ') : d
})
// convert all date time values to local time
// the datetime is either YYYY-MM-DD hh:mm:ss (xcdb)
// or YYYY-MM-DD hh:mm:ss+/-xx:yy (ext)
return result.replace(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:[+-]\d{2}:\d{2})?/g, (d: string) => {
// TODO(timezone): retrieve the format from the corresponding column meta
// assume HH:mm at this moment
return dayjs(d).isValid() ? dayjs(d).format('YYYY-MM-DD HH:mm') : d
})
}

8
packages/nc-gui/utils/dateTimeUtils.ts

@ -1,7 +1,13 @@
import dayjs from 'dayjs'
export const timeAgo = (date: any) => {
return dayjs.utc(date).fromNow()
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(date)) {
// if there is no timezone info, consider as UTC
// e.g. 2023-01-01 08:00:00 (MySQL)
date += '+00:00'
}
// show in local time
return dayjs(date).fromNow()
}
export const dateFormats = [

1
packages/nc-gui/utils/index.ts

@ -23,3 +23,4 @@ export * from './browserUtils'
export * from './geoDataUtils'
export * from './mimeTypeUtils'
export * from './parseUtils'
export * from './cell'

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

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

4
packages/nocodb-sdk/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nocodb-sdk",
"version": "0.107.4",
"version": "0.108.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb-sdk",
"version": "0.107.4",
"version": "0.108.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",

2
packages/nocodb-sdk/package.json

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

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

@ -546,7 +546,39 @@ export interface FilterType {
| 'notchecked'
| 'notempty'
| 'notnull'
| 'null';
| 'null'
| null
| (
| 'allof'
| 'anyof'
| 'blank'
| 'btw'
| 'checked'
| 'empty'
| 'eq'
| 'ge'
| 'gt'
| 'gte'
| 'in'
| 'is'
| 'isWithin'
| 'isnot'
| 'le'
| 'like'
| 'lt'
| 'lte'
| 'nallof'
| 'nanyof'
| 'nbtw'
| 'neq'
| 'nlike'
| 'not'
| 'notblank'
| 'notchecked'
| 'notempty'
| 'notnull'
| ('null' & null)
);
/** Comparison Sub-Operator */
comparison_sub_op?:
| 'daysAgo'
@ -589,7 +621,7 @@ export interface FilterType {
| ('yesterday' & null)
);
/** Foreign Key to Column */
fk_column_id?: IdType;
fk_column_id?: StringOrNullType;
/** Foreign Key to Hook */
fk_hook_id?: StringOrNullType;
/** Foreign Key to Model */
@ -664,7 +696,39 @@ export interface FilterReqType {
| 'notchecked'
| 'notempty'
| 'notnull'
| 'null';
| 'null'
| null
| (
| 'allof'
| 'anyof'
| 'blank'
| 'btw'
| 'checked'
| 'empty'
| 'eq'
| 'ge'
| 'gt'
| 'gte'
| 'in'
| 'is'
| 'isWithin'
| 'isnot'
| 'le'
| 'like'
| 'lt'
| 'lte'
| 'nallof'
| 'nanyof'
| 'nbtw'
| 'neq'
| 'nlike'
| 'not'
| 'notblank'
| 'notchecked'
| 'notempty'
| 'notnull'
| ('null' & null)
);
/** Comparison Sub-Operator */
comparison_sub_op?:
| 'daysAgo'
@ -707,7 +771,7 @@ export interface FilterReqType {
| ('yesterday' & null)
);
/** Foreign Key to Column */
fk_column_id?: IdType;
fk_column_id?: StringOrNullType;
/** Belong to which filter ID */
fk_parent_id?: StringOrNullType;
/** Is this filter grouped? */
@ -1603,7 +1667,7 @@ export interface NormalColumnRequestType {
/** Data Type Extra */
dtx?: StringOrNullType;
/** Data Type Extra Precision */
dtxp?: StringOrNullType | number;
dtxp?: string | number | null;
/** Data Type Extra Scale */
dtxs?: StringOrNullType | number;
/** Numeric Precision */
@ -2255,6 +2319,8 @@ export interface UserType {
* @example org-level-viewer
*/
roles?: string;
/** Access token version */
token_version?: string;
}
/**
@ -4032,9 +4098,12 @@ export class Api<
baseDuplicate: (
projectId: IdType,
data: {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
options?: {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
};
project?: object;
},
baseId?: IdType,
params: RequestParams = {}
@ -4078,9 +4147,12 @@ export class Api<
duplicate: (
projectId: IdType,
data: {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
options?: {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
};
project?: object;
},
params: RequestParams = {}
) =>
@ -5118,8 +5190,11 @@ export class Api<
projectId: IdType,
tableId: IdType,
data: {
excludeData?: boolean;
excludeViews?: boolean;
options?: {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
};
},
params: RequestParams = {}
) =>

2
packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts

@ -832,7 +832,7 @@ export class MssqlUi {
}
static getAbstractType(col): any {
switch ((col.dt || col.dt).toLowerCase()) {
switch (col.dt?.toLowerCase()) {
case 'bigint':
case 'smallint':
case 'bit':

2
packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts

@ -871,7 +871,7 @@ export class MysqlUi {
}
static getAbstractType(col): any {
switch (col.dt.toLowerCase()) {
switch (col.dt?.toLowerCase()) {
case 'int':
case 'smallint':
case 'mediumint':

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

@ -689,7 +689,7 @@ export class OracleUi {
}
static getAbstractType(col): any {
switch ((col.dt || col.dt).toLowerCase()) {
switch (col.dt?.toLowerCase()) {
case 'integer':
return 'integer';
case 'bfile':

2
packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts

@ -1358,7 +1358,7 @@ export class PgUi {
}
static getAbstractType(col): any {
switch ((col.dt || col.dt).toLowerCase()) {
switch (col.dt?.toLowerCase()) {
case 'anyenum':
return 'enum';
case 'anynonarray':

2
packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts

@ -572,7 +572,7 @@ export class SnowflakeUi {
}
static getAbstractType(col): any {
switch (col.dt.toUpperCase()) {
switch (col.dt?.toUpperCase()) {
case 'NUMBER':
case 'DECIMAL':
case 'NUMERIC':

10
packages/nocodb/.eslintrc.js

@ -63,12 +63,20 @@ module.exports = {
],
},
],
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'@typescript-eslint/no-this-alias': 'off',
// todo: enable
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-var-requires': 'off',
'no-useless-catch': 'off',
'no-empty': 'off',

20
packages/nocodb/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nocodb",
"version": "0.107.4",
"version": "0.108.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb",
"version": "0.107.4",
"version": "0.108.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@google-cloud/storage": "^5.7.2",
@ -80,7 +80,7 @@
"mysql2": "^3.2.0",
"nanoid": "^3.1.20",
"nc-help": "^0.2.87",
"nc-lib-gui": "0.107.4",
"nc-lib-gui": "0.108.1",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
@ -190,7 +190,7 @@
}
},
"../nocodb-sdk": {
"version": "0.107.4",
"version": "0.108.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -13157,9 +13157,9 @@
}
},
"node_modules/nc-lib-gui": {
"version": "0.107.4",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.107.4.tgz",
"integrity": "sha512-+e0jjJgrBfgLGTTShkcu1QQ2I0QxE/NUCCJ5L8KeZibm71pvgxL/P2hfatSz8PvzSDU/YwXnyXZQkeaSDBzNyA==",
"version": "0.108.1",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.108.1.tgz",
"integrity": "sha512-GYhDzkcqOCQ/pqWpxU8hI/4KcuuQrqkhG9zXmwUx1pQlz9qD70X8bQtjqb5rdvJw6diGdYneMl+wRI48bLiUAg==",
"dependencies": {
"express": "^4.17.1"
}
@ -28442,9 +28442,9 @@
}
},
"nc-lib-gui": {
"version": "0.107.4",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.107.4.tgz",
"integrity": "sha512-+e0jjJgrBfgLGTTShkcu1QQ2I0QxE/NUCCJ5L8KeZibm71pvgxL/P2hfatSz8PvzSDU/YwXnyXZQkeaSDBzNyA==",
"version": "0.108.1",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.108.1.tgz",
"integrity": "sha512-GYhDzkcqOCQ/pqWpxU8hI/4KcuuQrqkhG9zXmwUx1pQlz9qD70X8bQtjqb5rdvJw6diGdYneMl+wRI48bLiUAg==",
"requires": {
"express": "^4.17.1"
}

4
packages/nocodb/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb",
"version": "0.107.4",
"version": "0.108.1",
"description": "NocoDB Backend",
"main": "dist/bundle.js",
"author": {
@ -113,7 +113,7 @@
"mysql2": "^3.2.0",
"nanoid": "^3.1.20",
"nc-help": "^0.2.87",
"nc-lib-gui": "0.107.4",
"nc-lib-gui": "0.108.1",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",

10
packages/nocodb/src/Noco.ts

@ -1,6 +1,6 @@
import path from 'path';
import Sentry, { Handlers } from '@sentry/node';
import { Logger } from '@nestjs/common';
import path from 'path';
import { NestFactory } from '@nestjs/core';
import clear from 'clear';
import * as express from 'express';
@ -30,13 +30,7 @@ export default class Noco {
private static logger = new Logger(Noco.name);
public static get dashboardUrl(): string {
let siteUrl = `http://localhost:${process.env.PORT || 8080}`;
// if (Noco._this?.config?.envs?.[Noco._this?.env]?.publicUrl) {
// siteUrl = Noco._this?.config?.envs?.[Noco._this?.env]?.publicUrl;
// }
if (Noco._this?.config?.envs?.['_noco']?.publicUrl) {
siteUrl = Noco._this?.config?.envs?.['_noco']?.publicUrl;
}
const siteUrl = `http://localhost:${process.env.PORT || 8080}`;
return `${siteUrl}${Noco._this?.config?.dashboardPath}`;
}

10
packages/nocodb/src/app.module.ts

@ -1,6 +1,5 @@
import { Module, RequestMethod } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { BullModule } from '@nestjs/bull';
import { EventEmitterModule as NestJsEventEmitter } from '@nestjs/event-emitter';
import { GlobalExceptionFilter } from './filters/global-exception/global-exception.filter';
import { GlobalMiddleware } from './middlewares/global/global.middleware';
@ -17,7 +16,6 @@ import { AuthTokenStrategy } from './strategies/authtoken.strategy/authtoken.str
import { BaseViewStrategy } from './strategies/base-view.strategy/base-view.strategy';
import { MetasModule } from './modules/metas/metas.module';
import { JobsModule } from './modules/jobs/jobs.module';
import { AppInitService } from './services/app-init.service';
import type { MiddlewareConsumer } from '@nestjs/common';
@Module({
@ -30,13 +28,6 @@ import type { MiddlewareConsumer } from '@nestjs/common';
EventEmitterModule,
JobsModule,
NestJsEventEmitter.forRoot(),
...(process.env['NC_REDIS_URL']
? [
BullModule.forRoot({
redis: process.env.NC_REDIS_URL,
}),
]
: []),
],
providers: [
AuthService,
@ -48,7 +39,6 @@ import type { MiddlewareConsumer } from '@nestjs/common';
AuthTokenStrategy,
BaseViewStrategy,
HookHandlerService,
AppInitService,
],
})
export class AppModule {

14
packages/nocodb/src/cache/RedisCacheMgr.ts vendored

@ -12,8 +12,12 @@ export default class RedisCacheMgr extends CacheMgr {
constructor(config: any) {
super();
this.client = new Redis(config);
// flush the existing db with selected key (Default: 0)
this.client.flushdb();
// avoid flushing db in worker container
if (process.env.NC_WORKER_CONTAINER !== 'true') {
// flush the existing db with selected key (Default: 0)
this.client.flushdb();
}
// TODO(cache): fetch orgs once it's implemented
const orgs = 'noco';
@ -41,7 +45,7 @@ export default class RedisCacheMgr extends CacheMgr {
}
// @ts-ignore
async get(key: string, type: string, config?: any): Promise<any> {
async get(key: string, type: string): Promise<any> {
log(`RedisCacheMgr::get: getting key ${key} with type ${type}`);
if (type === CacheGetType.TYPE_ARRAY) {
return this.client.smembers(key);
@ -131,7 +135,7 @@ export default class RedisCacheMgr extends CacheMgr {
// e.g. arr = ["nc:<orgs>:<scope>:<model_id_1>", "nc:<orgs>:<scope>:<model_id_2>"]
const arr = (await this.get(key, CacheGetType.TYPE_ARRAY)) || [];
log(`RedisCacheMgr::getList: getting list with key ${key}`);
const isNoneList = arr.length && arr[0] === 'NONE';
const isNoneList = arr.length && arr.includes('NONE');
if (isNoneList) {
return Promise.resolve({
@ -244,7 +248,7 @@ export default class RedisCacheMgr extends CacheMgr {
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
log(`RedisCacheMgr::appendToList: append key ${key} to ${listKey}`);
let list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
if (list.length && list[0] === 'NONE') {
if (list.length && list.includes('NONE')) {
list = [];
await this.del(listKey);
}

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

@ -40,7 +40,7 @@ export default class RedisMockCacheMgr extends CacheMgr {
}
// @ts-ignore
async get(key: string, type: string, config?: any): Promise<any> {
async get(key: string, type: string): Promise<any> {
log(`RedisMockCacheMgr::get: getting key ${key} with type ${type}`);
if (type === CacheGetType.TYPE_ARRAY) {
return this.client.smembers(key);

19
packages/nocodb/src/connection/connection.spec.ts

@ -1,19 +0,0 @@
import { Test } from '@nestjs/testing';
import { Connection } from './knex';
import type { TestingModule } from '@nestjs/testing';
describe('Knex', () => {
let provider: Connection;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [Connection],
}).compile();
provider = module.get<Connection>(Connection);
});
it('should be defined', () => {
expect(provider).toBeDefined();
});
});

37
packages/nocodb/src/connection/connection.ts

@ -1,37 +0,0 @@
import { Global, Injectable, Scope } from '@nestjs/common';
import { XKnex } from '../db/CustomKnex';
import NcConfigFactory from '../utils/NcConfigFactory';
import type * as knex from 'knex';
@Injectable({
scope: Scope.DEFAULT,
})
export class Connection {
public static knex: knex.Knex;
public static _config: any;
get knexInstance(): knex.Knex {
return Connection.knex;
}
get config(): knex.Knex {
return Connection._config;
}
// init metadb connection
static async init(): Promise<void> {
Connection._config = await NcConfigFactory.make();
if (!Connection.knex) {
Connection.knex = XKnex({
...this._config.meta.db,
useNullAsDefault: true,
});
}
}
// init metadb connection
async init(): Promise<void> {
return await Connection.init();
}
}

1
packages/nocodb/src/controllers/api-tokens.controller.ts

@ -9,7 +9,6 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {

1
packages/nocodb/src/controllers/attachments.controller.ts

@ -30,7 +30,6 @@ export class AttachmentsController {
@UploadedFiles() files: Array<any>,
@Body() body: any,
@Request() req: any,
@Query('path') path: string,
) {
const attachments = await this.attachmentsService.upload({
files: files,

1
packages/nocodb/src/controllers/audits.controller.ts

@ -10,7 +10,6 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {

1
packages/nocodb/src/controllers/bases.controller.ts

@ -10,7 +10,6 @@ import {
UseGuards,
} from '@nestjs/common';
import { BaseReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {

1
packages/nocodb/src/controllers/bulk-data-alias.controller.ts

@ -10,7 +10,6 @@ import {
Response,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

4
packages/nocodb/src/controllers/caches.controller.ts

@ -1,8 +1,10 @@
import { Controller, Delete, Get } from '@nestjs/common';
import { Controller, Delete, Get, UseGuards } from '@nestjs/common';
import { GlobalGuard } from '../guards/global/global.guard';
import { Acl } from '../middlewares/extract-project-id/extract-project-id.middleware';
import { CachesService } from '../services/caches.service';
@Controller()
@UseGuards(GlobalGuard)
export class CachesController {
constructor(private readonly cachesService: CachesService) {}

1
packages/nocodb/src/controllers/columns.controller.ts

@ -11,7 +11,6 @@ import {
UseGuards,
} from '@nestjs/common';
import { ColumnReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/data-alias-export.controller.ts

@ -1,6 +1,5 @@
import { Controller, Get, Request, Response, UseGuards } from '@nestjs/common';
import * as XLSX from 'xlsx';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

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

@ -130,7 +130,6 @@ export class DataAliasNestedController {
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('refRowId') refRowId: string,
@Param('relationType') relationType: string,
) {
await this.dataAliasNestedService.relationDataRemove({
columnName: columnName,
@ -157,7 +156,6 @@ export class DataAliasNestedController {
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('refRowId') refRowId: string,
@Param('relationType') relationType: string,
) {
await this.dataAliasNestedService.relationDataAdd({
columnName: columnName,

1
packages/nocodb/src/controllers/data-alias.controller.ts

@ -11,7 +11,6 @@ import {
Response,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import { parseHrtimeToSeconds } from '../helpers';
import {

2
packages/nocodb/src/controllers/filters.controller.ts

@ -9,14 +9,12 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { FilterReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {
Acl,
ExtractProjectIdMiddleware,
UseAclMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { FiltersService } from '../services/filters.service';

1
packages/nocodb/src/controllers/form-columns.controller.ts

@ -1,5 +1,4 @@
import { Body, Controller, Param, Patch, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/galleries.controller.ts

@ -9,7 +9,6 @@ import {
UseGuards,
} from '@nestjs/common';
import { GalleryUpdateReqType, ViewCreateReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/grid-columns.controller.ts

@ -1,6 +1,5 @@
import { Body, Controller, Get, Param, Patch, UseGuards } from '@nestjs/common';
import { GridColumnReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/grids.controller.ts

@ -8,7 +8,6 @@ import {
UseGuards,
} from '@nestjs/common';
import { ViewCreateReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/maps.controller.ts

@ -9,7 +9,6 @@ import {
UseGuards,
} from '@nestjs/common';
import { MapUpdateReqType, ViewCreateReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/meta-diffs.controller.ts

@ -6,7 +6,6 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/model-visibilities.controller.ts

@ -8,7 +8,6 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/old-datas/old-datas.controller.ts

@ -11,7 +11,6 @@ import {
Response,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/org-users.controller.ts

@ -10,7 +10,6 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { OrgUserRoles } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';

10
packages/nocodb/src/controllers/plugins.controller.ts

@ -14,11 +14,11 @@ import { Acl } from '../middlewares/extract-project-id/extract-project-id.middle
import { PluginsService } from '../services/plugins.service';
// todo: move to a interceptor
const blockInCloudMw = (_req, res, next) => {
if (process.env.NC_CLOUD === 'true') {
res.status(403).send('Not allowed');
} else next();
};
// const blockInCloudMw = (_req, res, next) => {
// if (process.env.NC_CLOUD === 'true') {
// res.status(403).send('Not allowed');
// } else next();
// };
@Controller()
@UseGuards(GlobalGuard)

1
packages/nocodb/src/controllers/project-users.controller.ts

@ -11,7 +11,6 @@ import {
UseGuards,
} from '@nestjs/common';
import { ProjectUserReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

77
packages/nocodb/src/controllers/projects.controller.ts

@ -11,15 +11,13 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import isDocker from 'is-docker';
import { ProjectReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {
Acl,
ExtractProjectIdMiddleware,
UseAclMiddleware,
UseProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import Noco from '../Noco';
import { packageVersion } from '../utils/packageVersion';
@ -31,9 +29,7 @@ import type { ProjectType } from 'nocodb-sdk';
export class ProjectsController {
constructor(private readonly projectsService: ProjectsService) {}
@UseAclMiddleware({
permissionName: 'projectList',
})
@Acl('projectList')
@Get('/api/v1/db/meta/projects/')
async list(@Query() queryParams: Record<string, any>, @Request() req) {
const projects = await this.projectsService.projectList({
@ -57,7 +53,7 @@ export class ProjectsController {
PackageVersion: packageVersion,
};
}
@Acl('projectGet')
@Get('/api/v1/db/meta/projects/:projectId')
async projectGet(@Param('projectId') projectId: string) {
const project = await this.projectsService.getProjectWithInfo({
@ -68,7 +64,7 @@ export class ProjectsController {
return project;
}
@Acl('projectUpdate')
@Patch('/api/v1/db/meta/projects/:projectId')
async projectUpdate(
@Param('projectId') projectId: string,
@ -82,6 +78,7 @@ export class ProjectsController {
return project;
}
@Acl('projectDelete')
@Delete('/api/v1/db/meta/projects/:projectId')
async projectDelete(@Param('projectId') projectId: string) {
const deleted = await this.projectsService.projectSoftDelete({
@ -91,6 +88,7 @@ export class ProjectsController {
return deleted;
}
@Acl('projectCreate')
@Post('/api/v1/db/meta/projects')
@HttpCode(200)
async projectCreate(@Body() projectBody: ProjectReqType, @Request() req) {
@ -102,66 +100,3 @@ export class ProjectsController {
return project;
}
}
/*
// // Project CRUD
export async function projectCost(req, res) {
let cost = 0;
const project = await Project.getWithInfo(req.params.projectId);
for (const base of project.bases) {
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
const userCount = await ProjectUser.getUsersCount(req.query);
const recordCount = (await sqlClient.totalRecords())?.data.TotalRecords;
if (recordCount > 100000) {
// 36,000 or $79/user/month
cost = Math.max(36000, 948 * userCount);
} else if (recordCount > 50000) {
// $36,000 or $50/user/month
cost = Math.max(36000, 600 * userCount);
} else if (recordCount > 10000) {
// $240/user/yr
cost = Math.min(240 * userCount, 36000);
} else if (recordCount > 1000) {
// $120/user/yr
cost = Math.min(120 * userCount, 36000);
}
}
T.event({
event: 'a:project:cost',
data: {
cost,
},
});
res.json({ cost });
}
export async function hasEmptyOrNullFilters(req, res) {
res.json(await Filter.hasEmptyOrNullFilters(req.params.projectId));
}
export default (router) => {
router.get(
'/api/v1/db/meta/projects/:projectId/cost',
metaApiMetrics,
ncMetaAclMw(projectCost, 'projectCost')
);
router.get(
'/api/v1/db/meta/projects/:projectId/has-empty-or-null-filters',
metaApiMetrics,
ncMetaAclMw(hasEmptyOrNullFilters, 'hasEmptyOrNullFilters')
);
};
* */

1
packages/nocodb/src/controllers/shared-bases.controller.ts

@ -10,7 +10,6 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/sorts.controller.ts

@ -9,7 +9,6 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { SortReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';

1
packages/nocodb/src/controllers/tables.controller.ts

@ -11,7 +11,6 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { TableReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import extractRolesObj from '../utils/extractRolesObj';

117
packages/nocodb/src/controllers/users/users.controller.ts

@ -1,5 +1,3 @@
import { promisify } from 'util';
import { AuditOperationSubTypes, AuditOperationTypes } from 'nocodb-sdk';
import {
Body,
Controller,
@ -19,23 +17,17 @@ import {
Acl,
ExtractProjectIdMiddleware,
} from '../../middlewares/extract-project-id/extract-project-id.middleware';
import Noco from '../../Noco';
import { GoogleStrategy } from '../../strategies/google.strategy/google.strategy';
import extractRolesObj from '../../utils/extractRolesObj';
import { Audit, User } from '../../models';
import { User } from '../../models';
import {
genJwt,
randomTokenString,
setTokenCookie,
} from '../../services/users/helpers';
import { UsersService } from '../../services/users/users.service';
import extractRolesObj from '../../utils/extractRolesObj';
@Controller()
export class UsersController {
constructor(
private readonly usersService: UsersService,
private googleStrategy: GoogleStrategy,
) {}
constructor(private readonly usersService: UsersService) {}
@Post([
'/auth/user/signup',
@ -59,56 +51,14 @@ export class UsersController {
'/api/v1/auth/token/refresh',
])
@HttpCode(200)
async refreshToken(@Request() req: any, @Request() res: any): Promise<any> {
return await this.usersService.refreshToken({
body: req.body,
req,
res,
});
}
async successfulSignIn({ user, err, info, req, res, auditDescription }) {
try {
if (!user || !user.email) {
if (err) {
return res.status(400).send(err);
}
if (info) {
return res.status(400).send(info);
}
return res.status(400).send({ msg: 'Your signin has failed' });
}
await promisify((req as any).login.bind(req))(user);
const refreshToken = randomTokenString();
if (!user.token_version) {
user.token_version = randomTokenString();
}
await User.update(user.id, {
refresh_token: refreshToken,
email: user.email,
token_version: user.token_version,
});
setTokenCookie(res, refreshToken);
await Audit.insert({
op_type: AuditOperationTypes.AUTHENTICATION,
op_sub_type: AuditOperationSubTypes.SIGNIN,
user: user.email,
ip: req.clientIp,
description: auditDescription,
});
res.json({
token: genJwt(user, Noco.getConfig()),
} as any);
} catch (e) {
console.log(e);
throw e;
}
async refreshToken(@Request() req: any, @Response() res: any): Promise<any> {
res.json(
await this.usersService.refreshToken({
body: req.body,
req,
res,
}),
);
}
@Post([
@ -118,8 +68,9 @@ export class UsersController {
])
@UseGuards(AuthGuard('local'))
@HttpCode(200)
async signin(@Request() req) {
return this.usersService.login(req.user);
async signin(@Request() req, @Response() res) {
await this.setRefreshToken({ req, res });
res.json(this.usersService.login(req.user));
}
@Post('/api/v1/auth/user/signout')
@ -136,18 +87,15 @@ export class UsersController {
@Post(`/auth/google/genTokenByCode`)
@HttpCode(200)
@UseGuards(AuthGuard('google'))
async googleSignin(@Request() req) {
return this.usersService.login(req.user);
async googleSignin(@Request() req, @Response() res) {
await this.setRefreshToken({ req, res });
res.json(this.usersService.login(req.user));
}
@Get('/auth/google')
@UseGuards(AuthGuard('google'))
googleAuthenticate(@Request() req) {
// this.googleStrategy.authenticate(req, {
// scope: ['profile', 'email'],
// state: req.query.state,
// callbackURL: req.ncSiteUrl + Noco.getConfig().dashboardPath,
// });
googleAuthenticate() {
// google strategy will take care the request
}
@Get(['/auth/user/me', '/api/v1/db/auth/user/me', '/api/v1/auth/user/me'])
@ -168,7 +116,7 @@ export class UsersController {
@UseGuards(GlobalGuard)
@Acl('passwordChange')
@HttpCode(200)
async passwordChange(@Request() req: any, @Body() body: any): Promise<any> {
async passwordChange(@Request() req: any): Promise<any> {
if (!(req as any).isAuthenticated()) {
NcError.forbidden('Not allowed');
}
@ -188,7 +136,7 @@ export class UsersController {
'/api/v1/auth/password/forgot',
])
@HttpCode(200)
async passwordForgot(@Request() req: any, @Body() body: any): Promise<any> {
async passwordForgot(@Request() req: any): Promise<any> {
await this.usersService.passwordForgot({
siteUrl: (req as any).ncSiteUrl,
body: req.body,
@ -269,4 +217,27 @@ export class UsersController {
return res.status(400).json({ msg: e.message });
}
}
async setRefreshToken({ res, req }) {
const userId = req.user?.id;
if (!userId) return;
const user = await User.get(userId);
if (!user) return;
const refreshToken = randomTokenString();
if (!user.token_version) {
user.token_version = randomTokenString();
}
await User.update(user.id, {
refresh_token: refreshToken,
email: user.email,
token_version: user.token_version,
});
setTokenCookie(res, refreshToken);
}
}

3
packages/nocodb/src/controllers/view-columns.controller.ts

@ -8,8 +8,7 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { ColumnReqType, ViewColumnReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { ViewColumnReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {

6
packages/nocodb/src/db/BaseModelSql.ts

@ -1613,7 +1613,7 @@ class BaseModelSql extends BaseModel {
}
}
async nestedRead(id, { where, fields: fields1, f, ...rest }, trx = null) {
async nestedRead(id, { fields: fields1, f, ...rest }, trx = null) {
rest = Object.assign({}, this.defaultNestedQueryParams, rest);
const { hm: childs = '', bt: parents = '', mm: many = '' } = rest;
@ -1852,7 +1852,7 @@ class BaseModelSql extends BaseModel {
return null;
}
// @ts-ignore
const { tn, cn, vtn, vcn, vrcn, rtn, rcn } =
const { vtn, vcn, vrcn, rtn, rcn } =
this.manyToManyRelations.find(({ vtn }) => assoc === vtn) || {};
const childModel = this.dbModels[rtn];
@ -1894,7 +1894,7 @@ class BaseModelSql extends BaseModel {
return null;
}
// @ts-ignore
const { tn, cn, vtn, vcn, vrcn, rtn, rcn } =
const { vtn, vcn, vrcn, rtn, rcn } =
this.manyToManyRelations.find(({ vtn }) => assoc === vtn) || {};
const childModel = this.dbModels[rtn];

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

@ -1,6 +1,9 @@
import autoBind from 'auto-bind';
import groupBy from 'lodash/groupBy';
import DataLoader from 'dataloader';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import timezone from 'dayjs/plugin/timezone';
import { nocoExecute } from 'nc-help';
import {
AuditOperationSubTypes,
@ -17,7 +20,6 @@ import { v4 as uuidv4 } from 'uuid';
import { extractLimitAndOffset } from '../helpers';
import { NcError } from '../helpers/catchError';
import getAst from '../helpers/getAst';
import { Audit, Column, Filter, Model, Project, Sort, View } from '../models';
import { sanitize, unsanitize } from '../helpers/sqlSanitize';
import {
@ -49,6 +51,9 @@ import type {
import type { Knex } from 'knex';
import type { SortType } from 'nocodb-sdk';
dayjs.extend(utc);
dayjs.extend(timezone);
const GROUP_COL = '__nc_group_id';
const nanoidv2 = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 14);
@ -1348,12 +1353,6 @@ class BaseModelSqlv2 {
const columns = await this.model.getColumns();
for (const column of columns) {
switch (column.uidt) {
case UITypes.Rollup:
{
// @ts-ignore
const colOptions: RollupColumn = await column.getColOptions();
}
break;
case UITypes.Lookup:
{
// @ts-ignore
@ -1600,6 +1599,67 @@ class BaseModelSqlv2 {
if (!checkColumnRequired(column, fields, extractPkAndPv)) continue;
switch (column.uidt) {
case UITypes.DateTime:
if (this.isMySQL) {
// MySQL stores timestamp in UTC but display in timezone
// To verify the timezone, run `SELECT @@global.time_zone, @@session.time_zone;`
// If it's SYSTEM, then the timezone is read from the configuration file
// if a timezone is set in a DB, the retrieved value would be converted to the corresponding timezone
// for example, let's say the global timezone is +08:00 in DB
// the value 2023-01-01 10:00:00 (UTC) would display as 2023-01-01 18:00:00 (UTC+8)
// our existing logic is based on UTC, during the query, we need to take the UTC value
// hence, we use CONVERT_TZ to convert back to UTC value
res[sanitize(column.title || column.column_name)] =
this.dbDriver.raw(
`CONVERT_TZ(??, @@GLOBAL.time_zone, '+00:00')`,
[
`${sanitize(alias || this.model.table_name)}.${
column.column_name
}`,
],
);
break;
} else if (this.isPg) {
// if there is no timezone info,
// convert to database timezone,
// then convert to UTC
if (
column.dt !== 'timestamp with time zone' &&
column.dt !== 'timestamptz'
) {
res[sanitize(column.title || column.column_name)] = this.dbDriver
.raw(
`?? AT TIME ZONE CURRENT_SETTING('timezone') AT TIME ZONE 'UTC'`,
[
`${sanitize(alias || this.model.table_name)}.${
column.column_name
}`,
],
)
.wrap('(', ')');
break;
}
} else if (this.isMssql) {
// if there is no timezone info,
// convert to database timezone,
// then convert to UTC
if (column.dt !== 'datetimeoffset') {
res[sanitize(column.title || column.column_name)] =
this.dbDriver.raw(
`CONVERT(DATETIMEOFFSET, ?? AT TIME ZONE 'UTC')`,
[
`${sanitize(alias || this.model.table_name)}.${
column.column_name
}`,
],
);
break;
}
}
res[sanitize(column.title || column.column_name)] = sanitize(
`${alias || this.model.table_name}.${column.column_name}`,
);
break;
case 'LinkToAnotherRecord':
case 'Lookup':
break;
@ -1727,7 +1787,11 @@ class BaseModelSqlv2 {
await populatePk(this.model, data);
// todo: filter based on view
const insertObj = await this.model.mapAliasToColumn(data);
const insertObj = await this.model.mapAliasToColumn(
data,
this.clientMeta,
this.dbDriver,
);
await this.validate(insertObj);
@ -1865,7 +1929,11 @@ class BaseModelSqlv2 {
async updateByPk(id, data, trx?, cookie?) {
try {
const updateObj = await this.model.mapAliasToColumn(data);
const updateObj = await this.model.mapAliasToColumn(
data,
this.clientMeta,
this.dbDriver,
);
await this.validate(data);
@ -1913,6 +1981,16 @@ class BaseModelSqlv2 {
return this.getTnPath(this.model);
}
public get clientMeta() {
return {
isSqlite: this.isSqlite,
isMssql: this.isMssql,
isPg: this.isPg,
isMySQL: this.isMySQL,
// isSnowflake: this.isSnowflake,
};
}
get isSqlite() {
return this.clientType === 'sqlite3';
}
@ -1941,7 +2019,11 @@ class BaseModelSqlv2 {
// const driver = trx ? trx : await this.dbDriver.transaction();
try {
await populatePk(this.model, data);
const insertObj = await this.model.mapAliasToColumn(data);
const insertObj = await this.model.mapAliasToColumn(
data,
this.clientMeta,
this.dbDriver,
);
let rowId = null;
const postInsertOps = [];
@ -2101,7 +2183,11 @@ class BaseModelSqlv2 {
: await Promise.all(
datas.map(async (d) => {
await populatePk(this.model, d);
return this.model.mapAliasToColumn(d);
return this.model.mapAliasToColumn(
d,
this.clientMeta,
this.dbDriver,
);
}),
);
@ -2185,7 +2271,11 @@ class BaseModelSqlv2 {
const updateDatas = raw
? datas
: await Promise.all(datas.map((d) => this.model.mapAliasToColumn(d)));
: await Promise.all(
datas.map((d) =>
this.model.mapAliasToColumn(d, this.clientMeta, this.dbDriver),
),
);
const prevData = [];
const newData = [];
@ -2242,7 +2332,11 @@ class BaseModelSqlv2 {
) {
try {
let count = 0;
const updateData = await this.model.mapAliasToColumn(data);
const updateData = await this.model.mapAliasToColumn(
data,
this.clientMeta,
this.dbDriver,
);
await this.validate(updateData);
const pkValues = await this._extractPksValues(updateData);
if (pkValues) {
@ -2294,7 +2388,9 @@ class BaseModelSqlv2 {
let transaction;
try {
const deleteIds = await Promise.all(
ids.map((d) => this.model.mapAliasToColumn(d)),
ids.map((d) =>
this.model.mapAliasToColumn(d, this.clientMeta, this.dbDriver),
),
);
const deleted = [];
@ -3187,16 +3283,23 @@ class BaseModelSqlv2 {
} else {
query = sanitize(query);
}
return this.convertAttachmentType(
let data =
this.isPg || this.isSnowflake
? (await this.dbDriver.raw(query))?.rows
: query.slice(0, 6) === 'select' && !this.isMssql
? await this.dbDriver.from(
this.dbDriver.raw(query).wrap('(', ') __nc_alias'),
)
: await this.dbDriver.raw(query),
childTable,
);
: await this.dbDriver.raw(query);
// update attachment fields
data = this.convertAttachmentType(data, childTable);
// update date time fields
data = this.convertDateFormat(data, childTable);
return data;
}
private _convertAttachmentType(
@ -3228,7 +3331,153 @@ class BaseModelSqlv2 {
this._convertAttachmentType(attachmentColumns, d),
);
} else {
this._convertAttachmentType(attachmentColumns, data);
data = this._convertAttachmentType(attachmentColumns, data);
}
}
}
return data;
}
// TODO(timezone): retrieve the format from the corresponding column meta
private _convertDateFormat(
dateTimeColumns: Record<string, any>[],
d: Record<string, any>,
) {
if (!d) return d;
for (const col of dateTimeColumns) {
if (!d[col.title]) continue;
if (col.uidt === UITypes.Formula) {
if (!d[col.title] || typeof d[col.title] !== 'string') {
continue;
}
// remove milliseconds
if (this.isMySQL) {
d[col.title] = d[col.title].replace(/\.000000/g, '');
} else if (this.isMssql) {
d[col.title] = d[col.title].replace(/\.0000000 \+00:00/g, '');
}
if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/g.test(d[col.title])) {
// convert ISO string (e.g. in MSSQL) to YYYY-MM-DD hh:mm:ssZ
// e.g. 2023-05-18T05:30:00.000Z -> 2023-05-18 11:00:00+05:30
d[col.title] = d[col.title].replace(
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/g,
(d: string) => {
if (!dayjs(d).isValid()) return d;
if (this.isSqlite) {
// e.g. DATEADD formula
return dayjs(d).utc().format('YYYY-MM-DD HH:mm:ssZ');
}
return dayjs(d).utc(true).format('YYYY-MM-DD HH:mm:ssZ');
},
);
continue;
}
// convert all date time values to utc
// the datetime is either YYYY-MM-DD hh:mm:ss (xcdb)
// or YYYY-MM-DD hh:mm:ss+/-xx:yy (ext)
d[col.title] = d[col.title].replace(
/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:[+-]\d{2}:\d{2})?/g,
(d: string) => {
if (!dayjs(d).isValid()) {
return d;
}
if (this.isSqlite) {
// if there is no timezone info,
// we assume the input is on NocoDB server timezone
// then we convert to UTC from server timezone
// example: datetime without timezone
// we need to display 2023-04-27 10:00:00 (in HKT)
// we convert d (e.g. 2023-04-27 18:00:00) to utc, i.e. 2023-04-27 02:00:00+00:00
// if there is timezone info,
// we simply convert it to UTC
// example: datetime with timezone
// e.g. 2023-04-27 10:00:00+05:30 -> 2023-04-27 04:30:00+00:00
return dayjs(d)
.tz(Intl.DateTimeFormat().resolvedOptions().timeZone)
.utc()
.format('YYYY-MM-DD HH:mm:ssZ');
}
// set keepLocalTime to true if timezone info is not found
const keepLocalTime = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/g.test(
d,
);
return dayjs(d).utc(keepLocalTime).format('YYYY-MM-DD HH:mm:ssZ');
},
);
continue;
}
let keepLocalTime = true;
if (this.isSqlite) {
if (!col.cdf) {
if (
d[col.title].indexOf('-') === -1 &&
d[col.title].indexOf('+') === -1 &&
d[col.title].slice(-1) !== 'Z'
) {
// if there is no timezone info,
// we assume the input is on NocoDB server timezone
// then we convert to UTC from server timezone
// e.g. 2023-04-27 10:00:00 (IST) -> 2023-04-27 04:30:00+00:00
d[col.title] = dayjs(d[col.title])
.tz(Intl.DateTimeFormat().resolvedOptions().timeZone)
.utc()
.format('YYYY-MM-DD HH:mm:ssZ');
continue;
} else {
// otherwise, we convert from the given timezone to UTC
keepLocalTime = false;
}
}
}
if (
this.isPg &&
(col.dt === 'timestamp with time zone' || col.dt === 'timestamptz')
) {
// postgres - timezone already attached to input
// e.g. 2023-05-11 16:16:51+08:00
keepLocalTime = false;
}
if (d[col.title] instanceof Date) {
// e.g. MSSQL
// Wed May 10 2023 17:47:46 GMT+0800 (Hong Kong Standard Time)
keepLocalTime = false;
}
// e.g. 01.01.2022 10:00:00+05:30 -> 2022-01-01 04:30:00+00:00
// e.g. 2023-05-09 11:41:49 -> 2023-05-09 11:41:49+00:00
d[col.title] = dayjs(d[col.title])
// keep the local time
.utc(keepLocalTime)
// show the timezone even for Mysql
.format('YYYY-MM-DD HH:mm:ssZ');
}
return d;
}
private convertDateFormat(data: Record<string, any>, childTable?: Model) {
// Show the date time in UTC format in API response
// e.g. 2022-01-01 04:30:00+00:00
if (data) {
const dateTimeColumns = (
childTable ? childTable.columns : this.model.columns
).filter(
(c) => c.uidt === UITypes.DateTime || c.uidt === UITypes.Formula,
);
if (dateTimeColumns.length) {
if (Array.isArray(data)) {
data = data.map((d) => this._convertDateFormat(dateTimeColumns, d));
} else {
data = this._convertDateFormat(dateTimeColumns, data);
}
}
}

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

Loading…
Cancel
Save