Browse Source

Merge pull request #5877 from nocodb/develop

pull/5878/head 0.109.0
github-actions[bot] 1 year ago committed by GitHub
parent
commit
9b63f602e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      lerna.json
  2. 3
      package.json
  3. 1
      packages/nc-gui/components/cell/ClampedText.vue
  4. 4
      packages/nc-gui/components/cell/Currency.vue
  5. 2
      packages/nc-gui/components/cell/DateTimePicker.vue
  6. 4
      packages/nc-gui/components/cell/Decimal.vue
  7. 4
      packages/nc-gui/components/cell/Duration.vue
  8. 4
      packages/nc-gui/components/cell/Email.vue
  9. 4
      packages/nc-gui/components/cell/Float.vue
  10. 4
      packages/nc-gui/components/cell/Integer.vue
  11. 19
      packages/nc-gui/components/cell/MultiSelect.vue
  12. 4
      packages/nc-gui/components/cell/Percent.vue
  13. 16
      packages/nc-gui/components/cell/SingleSelect.vue
  14. 11
      packages/nc-gui/components/cell/Text.vue
  15. 9
      packages/nc-gui/components/cell/TextArea.vue
  16. 4
      packages/nc-gui/components/cell/Url.vue
  17. 49
      packages/nc-gui/components/dashboard/TreeView.vue
  18. 3
      packages/nc-gui/components/dlg/TableCreate.vue
  19. 4
      packages/nc-gui/components/general/EmojiIcons.vue
  20. 4
      packages/nc-gui/components/smartsheet/Cell.vue
  21. 22
      packages/nc-gui/components/smartsheet/Form.vue
  22. 2
      packages/nc-gui/components/smartsheet/Gallery.vue
  23. 222
      packages/nc-gui/components/smartsheet/Grid.vue
  24. 28
      packages/nc-gui/components/smartsheet/Pagination.vue
  25. 16
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  26. 10
      packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue
  27. 5
      packages/nc-gui/components/smartsheet/header/Cell.vue
  28. 5
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  29. 73
      packages/nc-gui/components/tabs/Smartsheet.vue
  30. 17
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  31. 14
      packages/nc-gui/components/virtual-cell/HasMany.vue
  32. 12
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  33. 7
      packages/nc-gui/components/virtual-cell/QrCode.vue
  34. 7
      packages/nc-gui/components/virtual-cell/barcode/Barcode.vue
  35. 49
      packages/nc-gui/components/virtual-cell/components/ItemChip.vue
  36. 4
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  37. 8
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  38. 4
      packages/nc-gui/composables/useMultiSelect/cellRange.ts
  39. 96
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  40. 28
      packages/nc-gui/composables/useMultiSelect/copyValue.ts
  41. 649
      packages/nc-gui/composables/useMultiSelect/index.ts
  42. 197
      packages/nc-gui/composables/useViewData.ts
  43. 2
      packages/nc-gui/context/index.ts
  44. 80
      packages/nc-gui/lang/tr.json
  45. 639
      packages/nc-gui/package-lock.json
  46. 6
      packages/nc-gui/package.json
  47. 4
      packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue
  48. 17
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue
  49. 14
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue
  50. 6
      packages/noco-docs/content/en/setup-and-usages/column-types.md
  51. 4
      packages/nocodb-sdk/package-lock.json
  52. 2
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  53. 2
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  54. 2
      packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts
  55. 2
      packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts
  56. 2
      packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts
  57. 30
      packages/nocodb/package-lock.json
  58. 4
      packages/nocodb/package.json
  59. 4
      packages/nocodb/src/controllers/caches.controller.ts
  60. 33
      packages/nocodb/src/controllers/test/TestResetService/resetMetaSakilaSqliteProject.ts
  61. 2
      packages/nocodb/src/db/BaseModelSqlv2.ts
  62. 12
      packages/nocodb/src/models/Model.ts
  63. 10
      packages/nocodb/src/models/Project.ts
  64. 29
      packages/nocodb/src/models/View.ts
  65. 1
      packages/nocodb/src/modules/datas/helpers.ts
  66. 5
      packages/nocodb/src/services/users/users.service.ts
  67. 2
      packages/nocodb/tests/unit/model/tests/baseModelSql.test.ts
  68. 2
      packages/nocodb/tests/unit/rest/tests/viewRow.test.ts
  69. 70
      renovate.json
  70. 42
      scripts/installLocalSdk.js
  71. 5
      scripts/pkg-executable/index.js
  72. 4
      tests/playwright/package-lock.json
  73. 7
      tests/playwright/pages/Dashboard/Form/index.ts
  74. 5
      tests/playwright/pages/Dashboard/Grid/Column/LTAR/ChildList.ts
  75. 4
      tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts
  76. 17
      tests/playwright/pages/Dashboard/Grid/index.ts
  77. 7
      tests/playwright/pages/Dashboard/common/Cell/DateCell.ts
  78. 10
      tests/playwright/pages/Dashboard/common/Cell/RatingCell.ts
  79. 6
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  80. 81
      tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts
  81. 4
      tests/playwright/tests/db/cellSelection.spec.ts
  82. 116
      tests/playwright/tests/db/columnLtarDragdrop.spec.ts
  83. 1
      tests/playwright/tests/db/columnRelationalExtendedTests.spec.ts
  84. 49
      tests/playwright/tests/db/filters.spec.ts
  85. 426
      tests/playwright/tests/db/keyboardShortcuts.spec.ts
  86. 8
      tests/playwright/tests/db/toolbarOperations.spec.ts
  87. 10
      tests/playwright/tests/db/undo-redo.spec.ts
  88. 2
      tests/playwright/tests/db/viewForm.spec.ts
  89. 10
      tests/playwright/tests/utils/general.ts

6
lerna.json

@ -1,6 +1,10 @@
{ {
"packages": [ "packages": [
"packages/*" "packages/nc-cli",
"packages/nc-gui",
"packages/nc-plugin",
"packages/nocodb",
"packages/nocodb-sdk"
], ],
"version": "independent" "version": "independent"
} }

3
package.json

@ -46,7 +46,8 @@
"start:mysql": "docker-compose -f ./tests/playwright/scripts/docker-compose-mysql-playwright.yml up -d", "start:mysql": "docker-compose -f ./tests/playwright/scripts/docker-compose-mysql-playwright.yml up -d",
"stop:mysql": "docker-compose -f ./tests/playwright/scripts/docker-compose-mysql-playwright.yml down", "stop:mysql": "docker-compose -f ./tests/playwright/scripts/docker-compose-mysql-playwright.yml down",
"start:pg": "docker-compose -f ./tests/playwright/scripts/docker-compose-pg.yml up -d", "start:pg": "docker-compose -f ./tests/playwright/scripts/docker-compose-pg.yml up -d",
"stop:pg": "docker-compose -f ./tests/playwright/scripts/docker-compose-pg.yml down" "stop:pg": "docker-compose -f ./tests/playwright/scripts/docker-compose-pg.yml down",
"install:local-sdk": "node scripts/installLocalSdk.js"
}, },
"dependencies": { "dependencies": {
"express": "^4.18.1", "express": "^4.18.1",

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

@ -13,6 +13,7 @@ const props = defineProps<{
'-webkit-line-clamp': props.lines || 1, '-webkit-line-clamp': props.lines || 1,
'-webkit-box-orient': 'vertical', '-webkit-box-orient': 'vertical',
'overflow': 'hidden', 'overflow': 'hidden',
'white-space': 'pre',
}" }"
> >
{{ props.value || '' }} {{ props.value || '' }}

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 = () => { const submitCurrency = () => {
if (lastSaved.value !== vModel.value) { if (lastSaved.value !== vModel.value) {

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

@ -18,7 +18,7 @@ import {
interface Props { interface Props {
modelValue?: string | null modelValue?: string | null
isPk?: boolean isPk?: boolean
isUpdatedFromCopyNPaste: Record<string, boolean> isUpdatedFromCopyNPaste?: Record<string, boolean>
} }
const { modelValue, isPk, isUpdatedFromCopyNPaste } = defineProps<Props>() const { modelValue, isPk, isUpdatedFromCopyNPaste } = defineProps<Props>()

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> </script>
<template> <template>

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

@ -74,9 +74,9 @@ const submitDuration = () => {
isEdited.value = false 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> </script>
<template> <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 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( watch(
() => editEnabled.value, () => 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> </script>
<template> <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) { function onKeyDown(evt: KeyboardEvent) {
return evt.key === '.' && evt.preventDefault() return evt.key === '.' && evt.preventDefault()

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

@ -10,6 +10,7 @@ import {
ColumnInj, ColumnInj,
IsKanbanInj, IsKanbanInj,
ReadonlyInj, ReadonlyInj,
RowHeightInj,
computed, computed,
enumColor, enumColor,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
@ -46,12 +47,12 @@ const readOnly = inject(ReadonlyInj)!
const active = inject(ActiveCellInj, ref(false)) const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
const rowHeight = inject(RowHeightInj, ref(undefined))
const selectedIds = ref<string[]>([]) const selectedIds = ref<string[]>([])
const aselect = ref<typeof AntSelect>() const aselect = ref<typeof AntSelect>()
@ -93,7 +94,7 @@ const isOptionMissing = computed(() => {
const hasEditRoles = computed(() => hasRole('owner', true) || hasRole('creator', true) || hasRole('editor', true)) const hasEditRoles = computed(() => hasRole('owner', true) || hasRole('creator', true) || hasRole('editor', true))
const editAllowed = computed(() => (hasEditRoles.value || isForm.value) && (active.value || editable.value)) const editAllowed = computed(() => (hasEditRoles.value || isForm.value) && active.value)
const vModel = computed({ const vModel = computed({
get: () => { get: () => {
@ -327,7 +328,17 @@ const selectedOpts = computed(() => {
<template> <template>
<div class="nc-multi-select h-full w-full flex items-center" :class="{ 'read-only': readOnly }" @click="toggleMenu"> <div class="nc-multi-select h-full w-full flex items-center" :class="{ 'read-only': readOnly }" @click="toggleMenu">
<div v-if="!editable && !active" class="flex flex-nowrap"> <div
v-if="!active"
class="flex flex-wrap"
:style="{
'display': '-webkit-box',
'max-width': '100%',
'-webkit-line-clamp': rowHeight || 1,
'-webkit-box-orient': 'vertical',
'overflow': 'hidden',
}"
>
<template v-for="selectedOpt of selectedOpts" :key="selectedOpt.value"> <template v-for="selectedOpt of selectedOpts" :key="selectedOpt.value">
<a-tag class="rounded-tag" :color="selectedOpt.color" :style="{ order: selectedOpt.index }"> <a-tag class="rounded-tag" :color="selectedOpt.color" :style="{ order: selectedOpt.index }">
<span <span

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> </script>
<template> <template>

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

@ -42,8 +42,6 @@ const readOnly = inject(ReadonlyInj)!
const active = inject(ActiveCellInj, ref(false)) const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
const aselect = ref<typeof AntSelect>() const aselect = ref<typeof AntSelect>()
const isOpen = ref(false) const isOpen = ref(false)
@ -92,7 +90,7 @@ const isOptionMissing = computed(() => {
const hasEditRoles = computed(() => hasRole('owner', true) || hasRole('creator', true) || hasRole('editor', true)) const hasEditRoles = computed(() => hasRole('owner', true) || hasRole('creator', true) || hasRole('editor', true))
const editAllowed = computed(() => (hasEditRoles.value || isForm.value) && (active.value || editable.value)) const editAllowed = computed(() => (hasEditRoles.value || isForm.value) && active.value)
const vModel = computed({ const vModel = computed({
get: () => tempSelectedOptState.value ?? modelValue, get: () => tempSelectedOptState.value ?? modelValue,
@ -200,7 +198,7 @@ const search = () => {
// prevent propagation of keydown event if select is open // prevent propagation of keydown event if select is open
const onKeydown = (e: KeyboardEvent) => { const onKeydown = (e: KeyboardEvent) => {
if (isOpen.value && (active.value || editable.value)) { if (isOpen.value && active.value) {
e.stopPropagation() e.stopPropagation()
} }
if (e.key === 'Enter') { if (e.key === 'Enter') {
@ -253,7 +251,7 @@ const selectedOpt = computed(() => {
<template> <template>
<div class="h-full w-full flex items-center nc-single-select" :class="{ 'read-only': readOnly }" @click="toggleMenu"> <div class="h-full w-full flex items-center nc-single-select" :class="{ 'read-only': readOnly }" @click="toggleMenu">
<div v-if="!editable && !active"> <div v-if="!active">
<a-tag v-if="selectedOpt" class="rounded-tag" :color="selectedOpt.color"> <a-tag v-if="selectedOpt" class="rounded-tag" :color="selectedOpt.color">
<span <span
:style="{ :style="{
@ -279,9 +277,9 @@ const selectedOpt = computed(() => {
:bordered="false" :bordered="false"
:open="isOpen && editAllowed" :open="isOpen && editAllowed"
:disabled="readOnly || !editAllowed" :disabled="readOnly || !editAllowed"
:show-arrow="hasEditRoles && !readOnly && (editable || (active && vModel === null))" :show-arrow="hasEditRoles && !readOnly && active && vModel === null"
:dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen && (active || editable) ? 'active' : ''}`" :dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen && active ? 'active' : ''}`"
:show-search="isOpen && (active || editable)" :show-search="isOpen && active"
@select="onSelect" @select="onSelect"
@keydown="onKeydown($event)" @keydown="onKeydown($event)"
@search="search" @search="search"
@ -326,7 +324,7 @@ const selectedOpt = computed(() => {
} }
:deep(.ant-tag) { :deep(.ant-tag) {
@apply "rounded-tag"; @apply "rounded-tag" my-[2px];
} }
:deep(.ant-select-clear) { :deep(.ant-select-clear) {

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, IsExpandedFormOpenInj, ReadonlyInj, inject, ref, useVModel } from '#imports' import { EditModeInj, IsExpandedFormOpenInj, ReadonlyInj, RowHeightInj, inject, ref, useVModel } from '#imports'
interface Props { interface Props {
modelValue?: string | null modelValue?: string | null
@ -14,18 +14,15 @@ const { showNull } = useGlobal()
const editEnabled = inject(EditModeInj) const editEnabled = inject(EditModeInj)
const rowHeight = inject( const rowHeight = inject(RowHeightInj, ref(undefined))
RowHeightInj,
computed(() => undefined),
)
const readonly = inject(ReadonlyInj, ref(false)) const readonly = inject(ReadonlyInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits) 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> </script>
<template> <template>

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

@ -10,18 +10,15 @@ const emits = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj) const editEnabled = inject(EditModeInj)
const rowHeight = inject( const rowHeight = inject(RowHeightInj, ref(undefined))
RowHeightInj,
computed(() => undefined),
)
const { showNull } = useGlobal() const { showNull } = useGlobal()
const vModel = useVModel(props, 'modelValue', emits, { defaultValue: '' }) 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> </script>
<template> <template>

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

@ -63,9 +63,9 @@ const url = computed(() => {
const { cellUrlOptions } = useCellUrlConfig(url) 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( watch(
() => editEnabled.value, () => editEnabled.value,

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

@ -1,10 +1,11 @@
<script setup lang="ts"> <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 { TableType } from 'nocodb-sdk'
import type { Input } from 'ant-design-vue' import type { Input } from 'ant-design-vue'
import { Dropdown, Tooltip, message } from 'ant-design-vue' import { Dropdown, Tooltip, message } from 'ant-design-vue'
import Sortable from 'sortablejs' import Sortable from 'sortablejs'
import GithubButton from 'vue-github-button' import GithubButton from 'vue-github-button'
import { Icon } from '@iconify/vue'
import type { VNodeRef } from '#imports' import type { VNodeRef } from '#imports'
import { import {
ClientType, ClientType,
@ -96,6 +97,8 @@ const initSortable = (el: Element) => {
onEnd: async (evt) => { onEnd: async (evt) => {
const { newIndex = 0, oldIndex = 0 } = evt const { newIndex = 0, oldIndex = 0 } = evt
if(newIndex === oldIndex) return
const itemEl = evt.item as HTMLLIElement const itemEl = evt.item as HTMLLIElement
const item = tablesById[itemEl.dataset.id as string] const item = tablesById[itemEl.dataset.id as string]
@ -184,6 +187,18 @@ const initSortable = (el: Element) => {
}) })
}, },
animation: 150, animation: 150,
setData(dataTransfer, dragEl) {
dataTransfer.setData(
'text/json',
JSON.stringify({
id: dragEl.dataset.id,
title: dragEl.dataset.title,
type: dragEl.dataset.type,
baseId: dragEl.dataset.baseId,
}),
)
},
revertOnSpill: true,
}) })
} }
@ -279,6 +294,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) { function openTableCreateDialog(baseId?: string) {
$e('c:table:create:navdraw') $e('c:table:create:navdraw')
@ -288,6 +312,12 @@ function openTableCreateDialog(baseId?: string) {
'modelValue': isOpen, 'modelValue': isOpen,
'baseId': baseId || bases.value[0].id, 'baseId': baseId || bases.value[0].id,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
'onCreate': (table: TableType) => {
// on new table created scroll to the table in the tree view
nextTick(() => {
scrollToTable(table)
})
},
}) })
function closeDialog() { function closeDialog() {
@ -405,6 +435,9 @@ const duplicateTable = async (table: TableType) => {
await loadTables() await loadTables()
const newTable = tables.value.find((el) => el.id === data?.result?.id) const newTable = tables.value.find((el) => el.id === data?.result?.id)
if (newTable) addTableTab(newTable) if (newTable) addTableTab(newTable)
await nextTick(() => {
scrollToTable(newTable)
})
} else if (status === JobStatus.FAILED) { } else if (status === JobStatus.FAILED) {
message.error('Failed to duplicate table') message.error('Failed to duplicate table')
await loadTables() await loadTables()
@ -699,6 +732,9 @@ const duplicateTable = async (table: TableType) => {
class="nc-tree-item text-sm cursor-pointer group" class="nc-tree-item text-sm cursor-pointer group"
:data-order="table.order" :data-order="table.order"
:data-id="table.id" :data-id="table.id"
:data-base-id="bases[0].id"
:data-type="table.type"
:data-title="table.title"
:data-testid="`tree-view-table-${table.title}`" :data-testid="`tree-view-table-${table.title}`"
@click="addTableTab(table)" @click="addTableTab(table)"
> >
@ -716,12 +752,12 @@ const duplicateTable = async (table: TableType) => {
<div class="flex items-center" @click.stop> <div class="flex items-center" @click.stop>
<component :is="isUIAllowed('tableIconCustomisation') ? Tooltip : 'div'"> <component :is="isUIAllowed('tableIconCustomisation') ? Tooltip : 'div'">
<span v-if="table.meta?.icon" :key="table.meta?.icon" class="nc-table-icon flex items-center"> <span v-if="table.meta?.icon" :key="table.meta?.icon" class="nc-table-icon flex items-center">
<Icon <IconifyIcon
:key="table.meta?.icon" :key="table.meta?.icon"
:data-testid="`nc-icon-${table.meta?.icon}`" :data-testid="`nc-icon-${table.meta?.icon}`"
class="text-xl" class="text-xl"
:icon="table.meta?.icon" :icon="table.meta?.icon"
></Icon> ></IconifyIcon>
</span> </span>
<component <component
:is="icon(table)" :is="icon(table)"
@ -1023,6 +1059,9 @@ const duplicateTable = async (table: TableType) => {
class="nc-tree-item text-sm cursor-pointer group" class="nc-tree-item text-sm cursor-pointer group"
:data-order="table.order" :data-order="table.order"
:data-id="table.id" :data-id="table.id"
:data-title="table.title"
:data-base-id="base.id"
:data-type="table.type"
:data-testid="`tree-view-table-${table.title}`" :data-testid="`tree-view-table-${table.title}`"
@click="addTableTab(table)" @click="addTableTab(table)"
> >
@ -1040,12 +1079,12 @@ const duplicateTable = async (table: TableType) => {
<div class="flex items-center" @click.stop> <div class="flex items-center" @click.stop>
<component :is="isUIAllowed('tableIconCustomisation') ? Tooltip : 'div'"> <component :is="isUIAllowed('tableIconCustomisation') ? Tooltip : 'div'">
<span v-if="table.meta?.icon" :key="table.meta?.icon" class="nc-table-icon flex items-center"> <span v-if="table.meta?.icon" :key="table.meta?.icon" class="nc-table-icon flex items-center">
<Icon <IconifyIcon
:key="table.meta?.icon" :key="table.meta?.icon"
:data-testid="`nc-icon-${table.meta?.icon}`" :data-testid="`nc-icon-${table.meta?.icon}`"
class="text-xl" class="text-xl"
:icon="table.meta?.icon" :icon="table.meta?.icon"
></Icon> ></IconifyIcon>
</span> </span>
<component <component
:is="icon(table)" :is="icon(table)"

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

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

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

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Icon } from '@iconify/vue' import { Icon as IconifyIcon } from '@iconify/vue'
import InfiniteLoading from 'v3-infinite-loading' import InfiniteLoading from 'v3-infinite-loading'
import { emojiIcons } from '#imports' 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 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)"> <div v-for="icon of filteredIcons" :key="icon" @click="selectIcon(icon)">
<span class="cursor-pointer nc-emoji-item"> <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> </span>
</div> </div>
<InfiniteLoading @infinite="load"><span /></InfiniteLoading> <InfiniteLoading @infinite="load"><span /></InfiniteLoading>

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

@ -193,7 +193,7 @@ onUnmounted(() => {
`nc-cell-${(column?.uidt || 'default').toLowerCase()}`, `nc-cell-${(column?.uidt || 'default').toLowerCase()}`,
{ 'text-blue-600': isPrimary(column) && !props.virtual && !isForm }, { 'text-blue-600': isPrimary(column) && !props.virtual && !isForm },
{ 'nc-grid-numeric-cell': isGrid && !isForm && isNumericField }, { 'nc-grid-numeric-cell': isGrid && !isForm && isNumericField },
{ 'h-[40px]': !props.editEnabled && isForm && !isSurveyForm && !isAttachment(column) }, { 'h-[40px]': !props.editEnabled && isForm && !isSurveyForm && !isAttachment(column) && !props.virtual },
]" ]"
@keydown.enter.exact="navigate(NavigateDir.NEXT, $event)" @keydown.enter.exact="navigate(NavigateDir.NEXT, $event)"
@keydown.shift.enter.exact="navigate(NavigateDir.PREV, $event)" @keydown.shift.enter.exact="navigate(NavigateDir.PREV, $event)"
@ -232,8 +232,6 @@ onUnmounted(() => {
<div <div
v-if="(isLocked || (isPublic && readOnly && !isForm) || isSystemColumn(column)) && !isAttachment(column)" v-if="(isLocked || (isPublic && readOnly && !isForm) || isSystemColumn(column)) && !isAttachment(column)"
class="nc-locked-overlay" class="nc-locked-overlay"
@click.stop.prevent
@dblclick.stop.prevent
/> />
</template> </template>
</template> </template>

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

@ -532,10 +532,10 @@ watch(view, (nextView) => {
<!-- Header --> <!-- Header -->
<div v-if="isEditable" class="px-4 lg:px-12"> <div v-if="isEditable" class="px-4 lg:px-12">
<a-form-item v-if="isEditable"> <a-form-item v-if="isEditable">
<a-input <a-textarea
v-model:value="formViewData.heading" v-model:value="formViewData.heading"
class="w-full !font-bold !text-4xl !border-0 !border-b-1 !border-dashed !rounded-none !border-gray-400" class="w-full !font-bold !text-4xl !border-0 !border-b-1 !border-dashed !rounded-none !border-gray-400"
:style="{ borderRightWidth: '0px !important' }" :style="{ borderRightWidth: '0px !important', 'height': '54px', 'min-height': '54px', resize: 'vertical' }"
size="large" size="large"
hide-details hide-details
placeholder="Form Title" placeholder="Form Title"
@ -551,10 +551,10 @@ watch(view, (nextView) => {
<!-- Sub Header --> <!-- Sub Header -->
<div v-if="isEditable" class="px-4 lg:px-12"> <div v-if="isEditable" class="px-4 lg:px-12">
<a-form-item> <a-form-item>
<a-input <a-textarea
v-model:value="formViewData.subheading" v-model:value="formViewData.subheading"
class="w-full !border-0 !border-b-1 !border-dashed !rounded-none !border-gray-400" class="w-full !border-0 !border-b-1 !border-dashed !rounded-none !border-gray-400"
:style="{ borderRightWidth: '0px !important' }" :style="{ borderRightWidth: '0px !important', height: '40px', 'min-height': '40px', resize: 'vertical' }"
size="large" size="large"
hide-details hide-details
:placeholder="$t('msg.info.formDesc')" :placeholder="$t('msg.info.formDesc')"
@ -697,7 +697,7 @@ watch(view, (nextView) => {
<a-form-item <a-form-item
v-if="isVirtualCol(element)" v-if="isVirtualCol(element)"
:name="element.title" :name="element.title"
class="!mb-0" class="!mb-0 nc-input-required-error"
:rules="[ :rules="[
{ {
required: isRequired(element, element.required), required: isRequired(element, element.required),
@ -719,7 +719,7 @@ watch(view, (nextView) => {
<a-form-item <a-form-item
v-else v-else
:name="element.title" :name="element.title"
class="!mb-0" class="!mb-0 nc-input-required-error"
:rules="[ :rules="[
{ {
required: isRequired(element, element.required), required: isRequired(element, element.required),
@ -743,7 +743,7 @@ watch(view, (nextView) => {
</LazySmartsheetDivDataCell> </LazySmartsheetDivDataCell>
</a-form-item> </a-form-item>
<div class="text-gray-500 text-xs" data-testid="nc-form-input-help-text-label">{{ element.description }}</div> <div class="nc-form-help-text text-gray-500 text-xs" data-testid="nc-form-input-help-text-label">{{ element.description }}</div>
</div> </div>
</template> </template>
@ -837,7 +837,7 @@ watch(view, (nextView) => {
<style scoped lang="scss"> <style scoped lang="scss">
.nc-editable:hover { .nc-editable:hover {
.nc-field-remove-icon { :deep(.nc-field-remove-icon) {
@apply opacity-100; @apply opacity-100;
} }
} }
@ -861,6 +861,12 @@ watch(view, (nextView) => {
} }
} }
.nc-form-help-text, .nc-input-required-error {
max-width: 100%;
word-break: break-all;
white-space: pre-line;
}
:deep(.nc-cell-attachment) { :deep(.nc-cell-attachment) {
@apply p-0; @apply p-0;

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

@ -239,9 +239,9 @@ watch(view, async (nextView) => {
hoverable hoverable
class="!rounded-lg h-full overflow-hidden break-all max-w-[450px]" class="!rounded-lg h-full overflow-hidden break-all max-w-[450px]"
:data-testid="`nc-gallery-card-${record.row.id}`" :data-testid="`nc-gallery-card-${record.row.id}`"
:style="isPublic ? { cursor: 'default' } : { cursor: 'pointer' }"
@click="expandFormClick($event, record)" @click="expandFormClick($event, record)"
@contextmenu="showContextMenu($event, { row: rowIndex })" @contextmenu="showContextMenu($event, { row: rowIndex })"
:style="isPublic ? { cursor: 'default' } : { cursor: 'pointer' }"
> >
<template v-if="galleryData?.fk_cover_image_col_id" #cover> <template v-if="galleryData?.fk_cover_image_col_id" #cover>
<a-carousel v-if="!reloadAttachments && attachments(record).length" autoplay class="gallery-carousel" arrows> <a-carousel v-if="!reloadAttachments && attachments(record).length" autoplay class="gallery-carousel" arrows>

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

@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick } from '@vue/runtime-core'
import type { ColumnReqType, ColumnType, GridType, PaginatedType, TableType, ViewType } from 'nocodb-sdk' import type { ColumnReqType, ColumnType, GridType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk' import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
CellUrlDisableOverlayInj, CellUrlDisableOverlayInj,
@ -103,7 +104,8 @@ const expandedFormDlg = ref(false)
const expandedFormRow = ref<Row>() const expandedFormRow = ref<Row>()
const expandedFormRowState = ref<Record<string, any>>() const expandedFormRowState = ref<Record<string, any>>()
const gridWrapper = ref<HTMLElement>() const gridWrapper = ref<HTMLElement>()
const tableHead = ref<HTMLElement>() const tableHeadEl = ref<HTMLElement>()
const tableBodyEl = ref<HTMLElement>()
const isAddingColumnAllowed = $computed(() => !readOnly.value && !isLocked.value && isUIAllowed('add-column') && !isSqlView.value) const isAddingColumnAllowed = $computed(() => !readOnly.value && !isLocked.value && isUIAllowed('add-column') && !isSqlView.value)
@ -116,13 +118,15 @@ const {
formattedData: data, formattedData: data,
updateOrSaveRow, updateOrSaveRow,
changePage, changePage,
addEmptyRow, addEmptyRow: _addEmptyRow,
deleteRow, deleteRow,
deleteSelectedRows, deleteSelectedRows,
selectedAllRecords, selectedAllRecords,
removeRowIfNew, removeRowIfNew,
navigateToSiblingRow, navigateToSiblingRow,
getExpandedRowIndex, getExpandedRowIndex,
deleteRangeOfRows,
bulkUpdateRows,
} = useViewData(meta, view, xWhere) } = useViewData(meta, view, xWhere)
const { getMeta } = useMetas() const { getMeta } = useMetas()
@ -141,11 +145,20 @@ const getContainerScrollForElement = (
) => { ) => {
const childPos = el.getBoundingClientRect() const childPos = el.getBoundingClientRect()
const parentPos = container.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 = { const relativePos = {
top: childPos.top - parentPos.top, top: childPos.top - parentPos.top,
right: childPos.right - parentPos.right, right: childPos.right - parentPos.right,
bottom: childPos.bottom - parentPos.bottom, bottom: childPos.bottom - parentPos.bottom,
left: childPos.left - parentPos.left, left: childPos.left - parentPos.left - stickyColsWidth,
} }
const scroll = { const scroll = {
@ -159,9 +172,9 @@ const getContainerScrollForElement = (
*/ */
scroll.left = scroll.left =
relativePos.right + (offset?.right || 0) > 0 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 : relativePos.left - (offset?.left || 0) < 0
? container.scrollLeft + relativePos.left - (offset?.left || 0) ? container.scrollLeft + relativePos.left - (offset?.left || 0) - extraOffset
: container.scrollLeft : container.scrollLeft
/* /*
@ -170,9 +183,9 @@ const getContainerScrollForElement = (
*/ */
scroll.top = scroll.top =
relativePos.bottom + (offset?.bottom || 0) > 0 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 : relativePos.top - (offset?.top || 0) < 0
? container.scrollTop + relativePos.top - (offset?.top || 0) ? container.scrollTop + relativePos.top - (offset?.top || 0) - extraOffset
: container.scrollTop : container.scrollTop
return scroll return scroll
@ -187,8 +200,9 @@ const {
clearSelectedRange, clearSelectedRange,
copyValue, copyValue,
isCellActive, isCellActive,
tbodyEl,
resetSelectedRange, resetSelectedRange,
makeActive,
selectedRange,
} = useMultiSelect( } = useMultiSelect(
meta, meta,
fields, fields,
@ -196,6 +210,7 @@ const {
$$(editEnabled), $$(editEnabled),
isPkAvail, isPkAvail,
clearCell, clearCell,
clearSelectedRangeOfCells,
makeEditable, makeEditable,
scrollToCell, scrollToCell,
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
@ -219,7 +234,6 @@ const {
if (e.key === ' ') { if (e.key === ' ') {
if (isCellActive.value && !editEnabled && hasEditPermission) { if (isCellActive.value && !editEnabled && hasEditPermission) {
e.preventDefault() e.preventDefault()
clearSelectedRange()
const row = data.value[activeCell.row] const row = data.value[activeCell.row]
expandForm(row) expandForm(row)
return true return true
@ -243,6 +257,9 @@ const {
if (cmdOrCtrl) { if (cmdOrCtrl) {
if (!isCellActive.value) return if (!isCellActive.value) return
// cmdOrCtrl+shift handled in useMultiSelect
if (e.shiftKey) return
switch (e.key) { switch (e.key) {
case 'ArrowUp': case 'ArrowUp':
e.preventDefault() e.preventDefault()
@ -325,6 +342,7 @@ const {
// update/save cell value // update/save cell value
await updateOrSaveRow(rowObj, ctx.updatedColumnTitle || columnObj.title) await updateOrSaveRow(rowObj, ctx.updatedColumnTitle || columnObj.title)
}, },
bulkUpdateRows,
) )
function scrollToCell(row?: number | null, col?: number | null) { function scrollToCell(row?: number | null, col?: number | null) {
@ -333,15 +351,21 @@ function scrollToCell(row?: number | null, col?: number | null) {
if (row !== null && col !== null) { if (row !== null && col !== null) {
// get active cell // get active cell
const rows = tbodyEl.value?.querySelectorAll('tr') const rows = tableBodyEl.value?.querySelectorAll('tr')
const cols = rows?.[row].querySelectorAll('td') const cols = rows?.[row].querySelectorAll('td')
const td = cols?.[col === 0 ? 0 : col + 1] const td = cols?.[col === 0 ? 0 : col + 1]
if (!td || !gridWrapper.value) return if (!td || !gridWrapper.value) return
const { height: headerHeight } = tableHead.value!.getBoundingClientRect() const { height: headerHeight } = tableHeadEl.value!.getBoundingClientRect()
const tdScroll = getContainerScrollForElement(td, gridWrapper.value, { top: headerHeight, bottom: 9, right: 9 }) 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 (rows && row === rows.length - 2) {
// if last row make 'Add New Row' visible // if last row make 'Add New Row' visible
gridWrapper.value.scrollTo({ gridWrapper.value.scrollTo({
@ -412,6 +436,8 @@ const showLoading = ref(true)
const skipRowRemovalOnCancel = ref(false) const skipRowRemovalOnCancel = ref(false)
const preloadColumn = ref<Partial<any>>()
function expandForm(row: Row, state?: Record<string, any>, fromToolbar = false) { function expandForm(row: Row, state?: Record<string, any>, fromToolbar = false) {
const rowId = extractPkFromRow(row.row, meta.value?.columns as ColumnType[]) const rowId = extractPkFromRow(row.row, meta.value?.columns as ColumnType[])
@ -441,6 +467,13 @@ const onXcResizing = (cn: string, event: any) => {
defineExpose({ defineExpose({
loadData, loadData,
openColumnCreate: (data) => {
tableHeadEl.value?.querySelector('th:last-child')?.scrollIntoView({ behavior: 'smooth' })
setTimeout(() => {
addColumnDropdown.value = true
preloadColumn.value = data
}, 500)
},
}) })
// reset context menu target on hide // reset context menu target on hide
@ -542,8 +575,38 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate =
} }
} }
async function clearSelectedRangeOfCells() {
if (!hasEditPermission) return
const start = selectedRange.start
const end = selectedRange.end
const startRow = Math.min(start.row, end.row)
const endRow = Math.max(start.row, end.row)
const startCol = Math.min(start.col, end.col)
const endCol = Math.max(start.col, end.col)
const cols = fields.value.slice(startCol, endCol + 1)
const rows = data.value.slice(startRow, endRow + 1)
const props = []
for (const row of rows) {
for (const col of cols) {
if (!row || !col || !col.title) continue
// TODO handle LinkToAnotherRecord
if (isVirtualCol(col)) continue
row.row[col.title] = null
props.push(col.title)
}
}
await bulkUpdateRows(rows, props)
}
function makeEditable(row: Row, col: ColumnType) { function makeEditable(row: Row, col: ColumnType) {
if (!hasEditPermission || editEnabled || isView) { if (!hasEditPermission || editEnabled || isView || isLocked.value || readOnly.value || isSystemColumn(col)) {
return return
} }
@ -565,6 +628,10 @@ function makeEditable(row: Row, col: ColumnType) {
return return
} }
if ([UITypes.SingleSelect, UITypes.MultiSelect].includes(col.uidt as UITypes)) {
return
}
return (editEnabled = true) return (editEnabled = true)
} }
@ -578,7 +645,7 @@ useEventListener(document, 'keyup', async (e: KeyboardEvent) => {
/** On clicking outside of table reset active cell */ /** On clicking outside of table reset active cell */
const smartTable = ref(null) const smartTable = ref(null)
onClickOutside(tbodyEl, (e) => { onClickOutside(tableBodyEl, (e) => {
// do nothing if context menu was open // do nothing if context menu was open
if (contextMenu.value) return if (contextMenu.value) return
@ -631,6 +698,9 @@ const onNavigate = (dir: NavigateDir) => {
} }
break break
} }
nextTick(() => {
scrollToCell()
})
} }
const showContextMenu = (e: MouseEvent, target?: { row: number; col: number }) => { const showContextMenu = (e: MouseEvent, target?: { row: number; col: number }) => {
@ -754,26 +824,43 @@ eventBus.on(async (event, payload) => {
} }
}) })
const closeAddColumnDropdown = () => { const closeAddColumnDropdown = (scrollToLastCol = false) => {
columnOrder.value = null columnOrder.value = null
addColumnDropdown.value = false addColumnDropdown.value = false
if (scrollToLastCol) {
setTimeout(() => {
const lastAddNewRowHeader = tableHeadEl.value?.querySelector('th:last-child')
if (lastAddNewRowHeader) {
lastAddNewRowHeader.scrollIntoView({ behavior: 'smooth' })
}
}, 200)
}
} }
const confirmDeleteRow = (row: number) => { const confirmDeleteRow = (row: number) => {
Modal.confirm({ try {
title: `Do you want to delete this row?`, deleteRow(row)
wrapClassName: 'nc-modal-row-delete', } catch (e: any) {
okText: 'Yes', message.error(e.message)
okType: 'danger', }
cancelText: 'No', }
onOk() {
try { const deleteSelectedRangeOfRows = () => {
deleteRow(row) deleteRangeOfRows(selectedRange).then(() => {
} catch (e: any) { clearSelectedRange()
message.error(e.message) activeCell.row = null
} activeCell.col = null
}, })
}
function addEmptyRow(row?: number) {
const rowObj = _addEmptyRow(row)
nextTick().then(() => {
clearSelectedRange()
makeActive(row ?? data.value.length - 1, 0)
scrollToCell?.()
}) })
return rowObj
} }
</script> </script>
@ -796,9 +883,9 @@ const confirmDeleteRow = (row: number) => {
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white" class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white"
@contextmenu="showContextMenu" @contextmenu="showContextMenu"
> >
<thead ref="tableHead"> <thead ref="tableHeadEl">
<tr class="nc-grid-header"> <tr class="nc-grid-header">
<th class="w-[80px] min-w-[80px]" data-testid="grid-id-column"> <th class="w-[85px] min-w-[85px]" data-testid="grid-id-column">
<div class="w-full h-full bg-gray-100 flex pl-5 pr-1 items-center" data-testid="nc-check-all"> <div class="w-full h-full bg-gray-100 flex pl-5 pr-1 items-center" data-testid="nc-check-all">
<template v-if="!readOnly"> <template v-if="!readOnly">
<div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div> <div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div>
@ -850,9 +937,10 @@ const confirmDeleteRow = (row: number) => {
<template #overlay> <template #overlay>
<SmartsheetColumnEditOrAddProvider <SmartsheetColumnEditOrAddProvider
v-if="addColumnDropdown" v-if="addColumnDropdown"
:preload="preloadColumn"
:column-position="columnOrder" :column-position="columnOrder"
@submit="closeAddColumnDropdown" @submit="closeAddColumnDropdown(true)"
@cancel="closeAddColumnDropdown" @cancel="closeAddColumnDropdown()"
@click.stop @click.stop
@keydown.stop @keydown.stop
/> />
@ -861,12 +949,12 @@ const confirmDeleteRow = (row: number) => {
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody ref="tbodyEl"> <tbody ref="tableBodyEl">
<LazySmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row"> <LazySmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row">
<template #default="{ state }"> <template #default="{ state }">
<tr <tr
class="nc-grid-row" class="nc-grid-row"
:style="{ height: rowHeight ? `${rowHeight * 1.5}rem` : `1.5rem` }" :style="{ height: rowHeight ? `${rowHeight * 1.8}rem` : `1.8rem` }"
:data-testid="`grid-row-${rowIndex}`" :data-testid="`grid-row-${rowIndex}`"
> >
<td key="row-index" class="caption nc-grid-cell pl-5 pr-1" :data-testid="`cell-Id-${rowIndex}`"> <td key="row-index" class="caption nc-grid-cell pl-5 pr-1" :data-testid="`cell-Id-${rowIndex}`">
@ -925,8 +1013,9 @@ const confirmDeleteRow = (row: number) => {
<SmartsheetTableDataCell <SmartsheetTableDataCell
v-for="(columnObj, colIndex) of fields" v-for="(columnObj, colIndex) of fields"
:key="columnObj.id" :key="columnObj.id"
class="cell relative cursor-pointer nc-grid-cell" class="cell relative nc-grid-cell"
:class="{ :class="{
'cursor-pointer': hasEditPermission,
'active': hasEditPermission && isCellSelected(rowIndex, colIndex), 'active': hasEditPermission && isCellSelected(rowIndex, colIndex),
'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row), 'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row),
'align-middle': !rowHeight || rowHeight === 1, 'align-middle': !rowHeight || rowHeight === 1,
@ -975,35 +1064,43 @@ const confirmDeleteRow = (row: number) => {
</template> </template>
</LazySmartsheetRow> </LazySmartsheetRow>
<tr v-if="isAddingEmptyRowAllowed"> <tr
<td v-if="isAddingEmptyRowAllowed"
v-e="['c:row:add:grid-bottom']" v-e="['c:row:add:grid-bottom']"
:colspan="visibleColLength + 1" class="cursor-pointer"
class="text-left pointer nc-grid-add-new-cell cursor-pointer" @mouseup.stop
@click="addEmptyRow()" @click="addEmptyRow()"
> >
<td class="text-left pointer nc-grid-add-new-cell sticky left-0 !z-5 !border-r-0">
<div class="px-2 w-full flex items-center text-gray-500"> <div class="px-2 w-full flex items-center text-gray-500">
<component :is="iconMap.plus" class="text-pint-500 text-xs ml-2 text-primary" /> <component :is="iconMap.plus" class="text-pint-500 text-xs ml-2 text-primary" />
<span class="ml-1">
{{ $t('activity.addRow') }}
</span>
</div> </div>
</td> </td>
<td :colspan="visibleColLength"></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<template v-if="!isLocked && hasEditPermission" #overlay> <template v-if="!isLocked && hasEditPermission" #overlay>
<a-menu class="shadow !rounded !py-0" @click="contextMenu = false"> <a-menu class="shadow !rounded !py-0" @click="contextMenu = false">
<a-menu-item v-if="contextMenuTarget" @click="confirmDeleteRow(contextMenuTarget.row)"> <a-menu-item
v-if="contextMenuTarget && (selectedRange.isSingleCell() || selectedRange.isSingleRow())"
@click="confirmDeleteRow(contextMenuTarget.row)"
>
<div v-e="['a:row:delete']" class="nc-project-menu-item"> <div v-e="['a:row:delete']" class="nc-project-menu-item">
<!-- Delete Row --> <!-- Delete Row -->
{{ $t('activity.deleteRow') }} {{ $t('activity.deleteRow') }}
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item @click="deleteSelectedRows"> <a-menu-item v-else-if="contextMenuTarget" @click="deleteSelectedRangeOfRows">
<div v-e="['a:row:delete']" class="nc-project-menu-item">
<!-- Delete Rows -->
Delete Rows
</div>
</a-menu-item>
<a-menu-item v-if="data.some((r) => r.rowMeta.selected)" @click="deleteSelectedRows">
<div v-e="['a:row:delete-bulk']" class="nc-project-menu-item"> <div v-e="['a:row:delete-bulk']" class="nc-project-menu-item">
<!-- Delete Selected Rows --> <!-- Delete Selected Rows -->
{{ $t('activity.deleteSelectedRow') }} {{ $t('activity.deleteSelectedRow') }}
@ -1014,6 +1111,7 @@ const confirmDeleteRow = (row: number) => {
<a-menu-item <a-menu-item
v-if=" v-if="
contextMenuTarget && contextMenuTarget &&
selectedRange.isSingleCell() &&
(fields[contextMenuTarget.col].uidt === UITypes.LinkToAnotherRecord || (fields[contextMenuTarget.col].uidt === UITypes.LinkToAnotherRecord ||
!isVirtualCol(fields[contextMenuTarget.col])) !isVirtualCol(fields[contextMenuTarget.col]))
" "
@ -1022,7 +1120,12 @@ const confirmDeleteRow = (row: number) => {
<div v-e="['a:row:clear']" class="nc-project-menu-item">{{ $t('activity.clearCell') }}</div> <div v-e="['a:row:clear']" class="nc-project-menu-item">{{ $t('activity.clearCell') }}</div>
</a-menu-item> </a-menu-item>
<a-menu-item v-if="contextMenuTarget" @click="addEmptyRow(contextMenuTarget.row + 1)"> <!-- Clear cell -->
<a-menu-item v-else @click="clearSelectedRangeOfCells()">
<div v-e="['a:row:clear-range']" class="nc-project-menu-item">Clear Cells</div>
</a-menu-item>
<a-menu-item v-if="contextMenuTarget && selectedRange.isSingleCell()" @click="addEmptyRow(contextMenuTarget.row + 1)">
<div v-e="['a:row:insert']" class="nc-project-menu-item"> <div v-e="['a:row:insert']" class="nc-project-menu-item">
<!-- Insert New Row --> <!-- Insert New Row -->
{{ $t('activity.insertRow') }} {{ $t('activity.insertRow') }}
@ -1040,8 +1143,23 @@ const confirmDeleteRow = (row: number) => {
</a-dropdown> </a-dropdown>
</div> </div>
<LazySmartsheetPagination /> <div
v-if="isAddingEmptyRowAllowed"
class="absolute bottom-1px left-2 z-4"
data-testid="nc-grid-add-new-row"
@click="addEmptyRow()"
>
<a-button v-e="['c:row:add:grid-bottom', { footer: true }]" class="!rounded-xl" size="small">
<div class="flex items-center">
<component :is="iconMap.plus" class="text-pint-500 text-xs" />
<span class="ml-1">
{{ $t('activity.addRow') }}
</span>
</div>
</a-button>
</div>
<LazySmartsheetPagination align-count-on-right> </LazySmartsheetPagination>
<Suspense> <Suspense>
<LazySmartsheetExpandedForm <LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg" v-if="expandedFormRow && expandedFormDlg"
@ -1140,14 +1258,14 @@ const confirmDeleteRow = (row: number) => {
thead th:nth-child(2) { thead th:nth-child(2) {
position: sticky !important; position: sticky !important;
left: 80px; left: 85px;
z-index: 5; z-index: 5;
@apply border-r-2 border-r-gray-300; @apply border-r-2 border-r-gray-300;
} }
tbody td:nth-child(2) { tbody td:nth-child(2) {
position: sticky !important; position: sticky !important;
left: 80px; left: 85px;
z-index: 4; z-index: 4;
background: white; background: white;
@apply border-r-2 border-r-gray-300; @apply border-r-2 border-r-gray-300;

28
packages/nc-gui/components/smartsheet/Pagination.vue

@ -1,6 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ChangePageInj, PaginationDataInj, computed, iconMap, inject } from '#imports' import { ChangePageInj, PaginationDataInj, computed, iconMap, inject } from '#imports'
const props = defineProps<{
alignCountOnRight?: boolean
}>()
const paginatedData = inject(PaginationDataInj)! const paginatedData = inject(PaginationDataInj)!
const changePage = inject(ChangePageInj)! const changePage = inject(ChangePageInj)!
@ -19,11 +23,15 @@ const page = computed({
<template> <template>
<div class="flex items-center mb-1"> <div class="flex items-center mb-1">
<span v-if="count !== null && count !== Infinity" class="caption ml-5 text-gray-500" data-testid="grid-pagination"> <div class="flex-1">
{{ count }} {{ count !== 1 ? $t('objects.records') : $t('objects.record') }} <span
</span> v-if="!alignCountOnRight && count !== null && count !== Infinity"
class="caption ml-5 text-gray-500"
<div class="flex-1" /> data-testid="grid-pagination"
>
{{ count }} {{ count !== 1 ? $t('objects.records') : $t('objects.record') }}
</span>
</div>
<a-pagination <a-pagination
v-if="count !== Infinity" v-if="count !== Infinity"
@ -44,7 +52,15 @@ const page = computed({
</a-input> </a-input>
</div> </div>
<div class="flex-1" /> <div class="flex-1 text-right pr-2">
<span
v-if="alignCountOnRight && count !== null && count !== Infinity"
class="caption mr-5 text-gray-500"
data-testid="grid-pagination"
>
{{ count }} {{ count !== 1 ? $t('objects.records') : $t('objects.record') }}
</span>
</div>
</div> </div>
</template> </template>

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

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ColumnReqType } from 'nocodb-sdk' import type { ColumnReqType, ColumnType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk' import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { import {
IsFormInj, IsFormInj,
@ -25,6 +25,7 @@ import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
import MdiIdentifierIcon from '~icons/mdi/identifier' import MdiIdentifierIcon from '~icons/mdi/identifier'
const props = defineProps<{ const props = defineProps<{
preload?: Partial<ColumnType>
columnPosition?: Pick<ColumnReqType, 'column_order'> columnPosition?: Pick<ColumnReqType, 'column_order'>
}>() }>()
@ -125,6 +126,18 @@ watchEffect(() => {
onMounted(() => { onMounted(() => {
if (!isEdit.value) { if (!isEdit.value) {
generateNewColumnMeta() generateNewColumnMeta()
const { colOptions, ...others } = props.preload || {}
formState.value = {
...formState.value,
...others,
}
if (colOptions) {
onUidtOrIdTypeChange()
formState.value = {
...formState.value,
...colOptions,
}
}
} else { } else {
if (formState.value.pk) { if (formState.value.pk) {
message.info(t('msg.info.editingPKnotSupported')) message.info(t('msg.info.editingPKnotSupported'))
@ -188,7 +201,6 @@ useEventListener('keydown', (e: KeyboardEvent) => {
<LazySmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="formState" /> <LazySmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="formState" />
<LazySmartsheetColumnBarcodeOptions v-if="formState.uidt === UITypes.Barcode" v-model="formState" /> <LazySmartsheetColumnBarcodeOptions v-if="formState.uidt === UITypes.Barcode" v-model="formState" />
<LazySmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="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" /> <LazySmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<LazySmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" 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" /> <LazySmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />

10
packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue

@ -6,6 +6,7 @@ import { MetaInj, inject, ref, toRef, useProvideColumnCreateStore } from '#impor
interface Props { interface Props {
column?: ColumnType column?: ColumnType
columnPosition?: Pick<ColumnReqType, 'column_order'> columnPosition?: Pick<ColumnReqType, 'column_order'>
preload?: Partial<ColumnType>
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@ -16,9 +17,16 @@ const meta = inject(MetaInj, ref())
const column = toRef(props, 'column') const column = toRef(props, 'column')
const preload = toRef(props, 'preload')
useProvideColumnCreateStore(meta, column) useProvideColumnCreateStore(meta, column)
</script> </script>
<template> <template>
<SmartsheetColumnEditOrAdd :column-position="props.columnPosition" @submit="emit('submit')" @cancel="emit('cancel')" /> <SmartsheetColumnEditOrAdd
:preload="preload"
:column-position="props.columnPosition"
@submit="emit('submit')"
@cancel="emit('cancel')"
/>
</template> </template>

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

@ -53,7 +53,7 @@ const openHeaderMenu = () => {
v-if="column" v-if="column"
class="name" class="name"
:class="{ 'cursor-pointer': !isForm && isUIAllowed('edit-column') && !hideMenu }" :class="{ 'cursor-pointer': !isForm && isUIAllowed('edit-column') && !hideMenu }"
style="white-space: nowrap" style="white-space: pre-line"
:title="column.title" :title="column.title"
@dblclick="openHeaderMenu" @dblclick="openHeaderMenu"
>{{ column.title }}</span >{{ column.title }}</span
@ -95,7 +95,6 @@ const openHeaderMenu = () => {
<style scoped> <style scoped>
.name { .name {
max-width: calc(100% - 40px); max-width: calc(100% - 40px);
overflow: hidden; word-break: break-all;
text-overflow: ellipsis;
} }
</style> </style>

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

@ -121,7 +121,7 @@ const closeAddColumnDropdown = () => {
<template #title> <template #title>
{{ tooltipMsg }} {{ tooltipMsg }}
</template> </template>
<span class="name" style="white-space: nowrap" :title="column.title"> {{ column.title }}</span> <span class="name" style="white-space: pre-line" :title="column.title"> {{ column.title }}</span>
</a-tooltip> </a-tooltip>
<span v-if="isVirtualColRequired(column, meta?.columns || []) || required" class="text-red-500">&nbsp;*</span> <span v-if="isVirtualColRequired(column, meta?.columns || []) || required" class="text-red-500">&nbsp;*</span>
@ -164,7 +164,6 @@ const closeAddColumnDropdown = () => {
<style scoped> <style scoped>
.name { .name {
max-width: calc(100% - 40px); max-width: calc(100% - 40px);
overflow: hidden; word-break: break-all;
text-overflow: ellipsis;
} }
</style> </style>

73
packages/nc-gui/components/tabs/Smartsheet.vue

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType, TableType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
FieldsInj, FieldsInj,
@ -29,7 +30,7 @@ const props = defineProps<{
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const { metas } = useMetas() const { metas, getMeta } = useMetas()
const activeTab = toRef(props, 'activeTab') const activeTab = toRef(props, 'activeTab')
@ -64,10 +65,74 @@ provide(
ReadonlyInj, ReadonlyInj,
computed(() => !isUIAllowed('xcDatatableEditable')), computed(() => !isUIAllowed('xcDatatableEditable')),
) )
const grid = ref()
const onDrop = async (event: DragEvent) => {
event.preventDefault()
try {
// Access the dropped data
const data = JSON.parse(event.dataTransfer?.getData('text/json')!)
// Do something with the received data
// if dragged item is not from the same base, return
if (data.baseId !== meta.value?.base_id) return
// if dragged item or opened view is not a table, return
if (data.type !== 'table' || meta.value?.type !== 'table') return
const childMeta = await getMeta(data.id)
const parentMeta = metas.value[meta.value.id!]
if (!childMeta || !parentMeta) return
const parentPkCol = parentMeta.columns?.find((c) => c.pk)
const childPkCol = childMeta.columns?.find((c) => c.pk)
// if already a link column exists, create a new Lookup column
const relationCol = parentMeta.columns?.find((c: ColumnType) => {
if (c.uidt !== UITypes.LinkToAnotherRecord) return false
const ltarOptions = c.colOptions as LinkToAnotherRecordType
if (ltarOptions.type !== 'mm') {
return false
}
if (ltarOptions.fk_related_model_id === childMeta.id) {
return true
}
return false
})
if (relationCol) {
const lookupCol = childMeta.columns?.find((c) => c.pv) ?? childMeta.columns?.[0]
grid.value?.openColumnCreate({
uidt: UITypes.Lookup,
title: `${data.title}Lookup`,
fk_relation_column_id: relationCol.id,
fk_lookup_column_id: lookupCol?.id,
})
} else {
grid.value?.openColumnCreate({
uidt: UITypes.LinkToAnotherRecord,
title: `${data.title}List`,
parentId: parentMeta.id,
childId: childMeta.id,
parentTable: parentMeta.title,
parentColumn: parentPkCol.title,
childTable: childMeta.title,
childColumn: childPkCol?.title,
})
}
} catch (e) {
console.log('error', e)
}
}
</script> </script>
<template> <template>
<div class="nc-container flex h-full"> <div class="nc-container flex h-full" @drop="onDrop" @dragover.prevent>
<div class="flex flex-col h-full flex-1 min-w-0"> <div class="flex flex-col h-full flex-1 min-w-0">
<LazySmartsheetToolbar /> <LazySmartsheetToolbar />
@ -75,7 +140,7 @@ provide(
<template v-if="meta"> <template v-if="meta">
<div class="flex flex-1 min-h-0"> <div class="flex flex-1 min-h-0">
<div v-if="activeView" class="h-full flex-1 min-w-0 min-h-0 bg-gray-50"> <div v-if="activeView" class="h-full flex-1 min-w-0 min-h-0 bg-gray-50">
<LazySmartsheetGrid v-if="isGrid" /> <LazySmartsheetGrid v-if="isGrid" ref="grid" />
<LazySmartsheetGallery v-else-if="isGallery" /> <LazySmartsheetGallery v-else-if="isGallery" />

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

@ -45,7 +45,7 @@ const listItemsDlg = ref(false)
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow() const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore( const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>, column as Ref<Required<ColumnType>>,
row, row,
isNew, isNew,
@ -81,13 +81,24 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
break break
} }
}) })
const belongsToColumn = computed(
() =>
relatedTableMeta.value?.columns?.find((c: any) => c.title === relatedTableDisplayValueProp.value) as ColumnType | undefined,
)
</script> </script>
<template> <template>
<div class="flex w-full chips-wrapper items-center" :class="{ active }"> <div class="flex w-full chips-wrapper items-center" :class="{ active }">
<div class="chips flex items-center flex-1"> <div class="chips flex items-center flex-1">
<template v-if="value && relatedTableDisplayValueProp"> <template v-if="value && relatedTableDisplayValueProp">
<VirtualCellComponentsItemChip :item="value" :value="value[relatedTableDisplayValueProp]" @unlink="unlinkRef(value)" /> <VirtualCellComponentsItemChip
:item="value"
:value="value[relatedTableDisplayValueProp]"
:column="belongsToColumn"
:show-unlink-button="true"
@unlink="unlinkRef(value)"
/>
</template> </template>
</div> </div>
@ -102,7 +113,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
/> />
</div> </div>
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" @attach-record="listItemsDlg = true" /> <LazyVirtualCellComponentsListItems v-model="listItemsDlg" :column="belongsToColumn" @attach-record="listItemsDlg = true" />
</div> </div>
</template> </template>

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

@ -6,6 +6,7 @@ import {
ColumnInj, ColumnInj,
IsFormInj, IsFormInj,
IsLockedInj, IsLockedInj,
IsUnderLookupInj,
ReadonlyInj, ReadonlyInj,
ReloadRowDataHookInj, ReloadRowDataHookInj,
RowInj, RowInj,
@ -16,7 +17,6 @@ import {
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow, useSmartsheetRowStoreOrThrow,
useUIPermission, useUIPermission,
IsUnderLookupInj
} from '#imports' } from '#imports'
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
@ -43,7 +43,7 @@ const { isUIAllowed } = useUIPermission()
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow() const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore( const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>, column as Ref<Required<ColumnType>>,
row, row,
isNew, isNew,
@ -81,6 +81,11 @@ const unlinkRef = async (rec: Record<string, any>) => {
} }
} }
const hasManyColumn = computed(
() =>
relatedTableMeta.value?.columns?.find((c: any) => c.title === relatedTableDisplayValueProp.value) as ColumnType | undefined,
)
const onAttachRecord = () => { const onAttachRecord = () => {
childListDlg.value = false childListDlg.value = false
listItemsDlg.value = true listItemsDlg.value = true
@ -106,6 +111,8 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
:key="i" :key="i"
:item="cell.item" :item="cell.item"
:value="cell.value" :value="cell.value"
:column="hasManyColumn"
:show-unlink-button="true"
@unlink="unlinkRef(cell.item)" @unlink="unlinkRef(cell.item)"
/> />
@ -131,11 +138,12 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
</div> </div>
</template> </template>
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" /> <LazyVirtualCellComponentsListItems v-model="listItemsDlg" :column="hasManyColumn" />
<LazyVirtualCellComponentsListChildItems <LazyVirtualCellComponentsListChildItems
v-model="childListDlg" v-model="childListDlg"
:cell-value="localCellValue" :cell-value="localCellValue"
:column="hasManyColumn"
@attach-record="onAttachRecord" @attach-record="onAttachRecord"
/> />
</div> </div>

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

@ -45,7 +45,7 @@ const { isUIAllowed } = useUIPermission()
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow() const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore( const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>, column as Ref<Required<ColumnType>>,
row, row,
isNew, isNew,
@ -96,6 +96,11 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
break break
} }
}) })
const m2mColumn = computed(
() =>
relatedTableMeta.value?.columns?.find((c: any) => c.title === relatedTableDisplayValueProp.value) as ColumnType | undefined,
)
</script> </script>
<template> <template>
@ -108,6 +113,8 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
:key="i" :key="i"
:item="cell.item" :item="cell.item"
:value="cell.value" :value="cell.value"
:column="m2mColumn"
:show-unlink-button="true"
@unlink="unlinkRef(cell.item)" @unlink="unlinkRef(cell.item)"
/> />
@ -133,11 +140,12 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
</div> </div>
</template> </template>
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" /> <LazyVirtualCellComponentsListItems v-model="listItemsDlg" :column="m2mColumn" />
<LazyVirtualCellComponentsListChildItems <LazyVirtualCellComponentsListChildItems
v-model="childListDlg" v-model="childListDlg"
:cell-value="localCellValue" :cell-value="localCellValue"
:column="m2mColumn"
@attach-record="onAttachRecord" @attach-record="onAttachRecord"
/> />
</div> </div>

7
packages/nc-gui/components/virtual-cell/QrCode.vue

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useQRCode } from '@vueuse/integrations/useQRCode' import { useQRCode } from '@vueuse/integrations/useQRCode'
import type QRCode from 'qrcode' import type QRCode from 'qrcode'
import { RowHeightInj } from '#imports' import { RowHeightInj, computed, inject, ref } from '#imports'
const maxNumberOfAllowedCharsForQrValue = 2000 const maxNumberOfAllowedCharsForQrValue = 2000
@ -22,10 +22,7 @@ const qrCodeOptions: QRCode.QRCodeToDataURLOptions = {
}, },
} }
const rowHeight = inject( const rowHeight = inject(RowHeightInj, ref(undefined))
RowHeightInj,
computed(() => undefined),
)
const qrCode = useQRCode(qrValue, { const qrCode = useQRCode(qrValue, {
...qrCodeOptions, ...qrCodeOptions,

7
packages/nc-gui/components/virtual-cell/barcode/Barcode.vue

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ComputedRef } from 'vue' import type { ComputedRef } from 'vue'
import JsBarcodeWrapper from './JsBarcodeWrapper.vue' import JsBarcodeWrapper from './JsBarcodeWrapper.vue'
import { RowHeightInj } from '#imports' import { RowHeightInj, computed, inject, ref } from '#imports'
const maxNumberOfAllowedCharsForBarcodeValue = 100 const maxNumberOfAllowedCharsForBarcodeValue = 100
@ -32,10 +32,7 @@ const showBarcode = computed(() => barcodeValue?.value.length > 0 && !tooManyCha
const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = useShowNotEditableWarning() const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = useShowNotEditableWarning()
const rowHeight = inject( const rowHeight = inject(RowHeightInj, ref(undefined))
RowHeightInj,
computed(() => undefined),
)
</script> </script>
<template> <template>

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

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { import {
ActiveCellInj, ActiveCellInj,
IsFormInj, IsFormInj,
@ -6,6 +7,7 @@ import {
ReadonlyInj, ReadonlyInj,
iconMap, iconMap,
inject, inject,
isAttachment,
ref, ref,
renderValue, renderValue,
useExpandedFormDetached, useExpandedFormDetached,
@ -15,9 +17,11 @@ import {
interface Props { interface Props {
value?: string | number | boolean value?: string | number | boolean
item?: any item?: any
column: any
showUnlinkButton: boolean
} }
const { value, item } = defineProps<Props>() const { value, item, column, showUnlinkButton } = defineProps<Props>()
const emit = defineEmits(['unlink']) const emit = defineEmits(['unlink'])
@ -56,13 +60,46 @@ export default {
<template> <template>
<div <div
class="chip group py-1 px-2 mr-1 my-1 flex items-center bg-blue-100/60 hover:bg-blue-100/40 rounded-[2px]" class="chip group mr-1 my-1 flex items-center rounded-[2px] flex-row"
:class="{ active }" :class="{ active, 'border-1 py-1 px-2': isAttachment(column) }"
@click="openExpandedForm" @click="openExpandedForm"
> >
<span class="name">{{ renderValue(value) }}</span> <span class="name">
<!-- Render virtual cell -->
<div v-show="active || isForm" v-if="!readOnly && !isLocked && isUIAllowed('xcDatatableEditable')" class="flex items-center"> <div v-if="isVirtualCol(column)">
<template v-if="column.uidt === UITypes.LinkToAnotherRecord">
<LazySmartsheetVirtualCell :edit-enabled="false" :model-value="value" :column="column" :read-only="true" />
</template>
<LazySmartsheetVirtualCell v-else :edit-enabled="false" :read-only="true" :model-value="value" :column="column" />
</div>
<!-- Render normal cell -->
<template v-else>
<div v-if="isAttachment(column) && value && !Array.isArray(value) && typeof value === 'object'">
<LazySmartsheetCell :model-value="value" :column="column" :edit-enabled="false" :read-only="true" />
</div>
<!-- For attachment cell avoid adding chip style -->
<template v-else>
<div
class="min-w-max"
:class="{
'px-1 rounded-full flex-1': !isAttachment(column),
'border-gray-200 rounded border-1': ![UITypes.Attachment, UITypes.MultiSelect, UITypes.SingleSelect].includes(
column.uidt,
),
}"
>
<LazySmartsheetCell :model-value="value" :column="column" :edit-enabled="false" :virtual="true" :read-only="true" />
</div>
</template>
</template>
</span>
<div
v-show="active || isForm"
v-if="showUnlinkButton && !readOnly && !isLocked && isUIAllowed('xcDatatableEditable')"
class="flex items-center"
>
<component <component
:is="iconMap.closeThick" :is="iconMap.closeThick"
class="nc-icon unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500" class="nc-icon unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500"

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

@ -19,7 +19,7 @@ import {
useVModel, useVModel,
} from '#imports' } from '#imports'
const props = defineProps<{ modelValue?: boolean; cellValue: any }>() const props = defineProps<{ modelValue?: boolean; cellValue: any; column: any }>()
const emit = defineEmits(['update:modelValue', 'attachRecord']) const emit = defineEmits(['update:modelValue', 'attachRecord'])
@ -148,7 +148,7 @@ const onClick = (row: Row) => {
> >
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-1 overflow-hidden min-w-0"> <div class="flex-1 overflow-hidden min-w-0">
{{ renderValue(row[relatedTableDisplayValueProp]) }} <VirtualCellComponentsItemChip :value="row[relatedTableDisplayValueProp]" :column="props.column" />
<span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span> <span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span>
</div> </div>

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

@ -19,7 +19,7 @@ import {
useVModel, useVModel,
} from '#imports' } from '#imports'
const props = defineProps<{ modelValue: boolean }>() const props = defineProps<{ modelValue: boolean; column: any }>()
const emit = defineEmits(['update:modelValue', 'addNewRecord']) const emit = defineEmits(['update:modelValue', 'addNewRecord'])
@ -229,7 +229,11 @@ watch(vModel, (nextVal) => {
:class="{ 'nc-selected-row': selectedRowIndex === i }" :class="{ 'nc-selected-row': selectedRowIndex === i }"
@click="linkRow(refRow)" @click="linkRow(refRow)"
> >
{{ renderValue(refRow[relatedTableDisplayValueProp]) }} <VirtualCellComponentsItemChip
:value="refRow[relatedTableDisplayValueProp]"
:column="props.column"
:show-unlink-button="false"
/>
<span class="hidden group-hover:(inline) text-gray-400 text-[11px] ml-1"> <span class="hidden group-hover:(inline) text-gray-400 text-[11px] ml-1">
({{ $t('labels.primaryKey') }} : {{ getRelatedTableRowId(refRow) }}) ({{ $t('labels.primaryKey') }} : {{ getRelatedTableRowId(refRow) }})
</span> </span>

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

@ -20,6 +20,10 @@ export class CellRange {
return !this.isEmpty() && this._start?.col === this._end?.col && this._start?.row === this._end?.row return !this.isEmpty() && this._start?.col === this._end?.col && this._start?.row === this._end?.row
} }
isSingleRow() {
return !this.isEmpty() && this._start?.row === this._end?.row
}
get start(): Cell { get start(): Cell {
return { return {
row: Math.min(this._start?.row ?? NaN, this._end?.row ?? NaN), row: Math.min(this._start?.row ?? NaN, this._end?.row ?? NaN),

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

@ -1,46 +1,71 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType, SelectOptionsType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import type { AppInfo } from '~/composables/useGlobal' import type { AppInfo } from '~/composables/useGlobal'
import { parseProp } from '#imports' import { parseProp } from '#imports'
export default function convertCellData( export default function convertCellData(
args: { from: UITypes; to: UITypes; value: any; column: ColumnType; appInfo: AppInfo }, args: { to: UITypes; value: string; column: ColumnType; appInfo: AppInfo },
isMysql = false, isMysql = false,
isMultiple = false,
) { ) {
const { from, to, value } = args const { to, value, column } = args
if (from === to && ![UITypes.Attachment, UITypes.Date, UITypes.DateTime, UITypes.Time, UITypes.Year].includes(to)) {
return value
}
const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ' const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
// return null if value is empty
if (value === '') return null
switch (to) { switch (to) {
case UITypes.Number: { case UITypes.Number: {
const parsedNumber = Number(value) const parsedNumber = Number(value)
if (isNaN(parsedNumber)) { if (isNaN(parsedNumber)) {
throw new TypeError(`Cannot convert '${value}' to number`) if (isMultiple) {
return null
} else {
throw new TypeError(`Cannot convert '${value}' to number`)
}
} }
return parsedNumber return parsedNumber
} }
case UITypes.Rating: { case UITypes.Rating: {
const parsedNumber = Number(value ?? 0) const parsedNumber = Number(value ?? 0)
if (isNaN(parsedNumber)) { if (isNaN(parsedNumber)) {
throw new TypeError(`Cannot convert '${value}' to rating`) if (isMultiple) {
return null
} else {
throw new TypeError(`Cannot convert '${value}' to rating`)
}
} }
return parsedNumber return parsedNumber
} }
case UITypes.Checkbox: case UITypes.Checkbox:
return Boolean(value) if (typeof value === 'boolean') return value
if (typeof value === 'string') {
const strval = value.trim().toLowerCase()
if (strval === 'true' || strval === '1') return true
if (strval === 'false' || strval === '0' || strval === '') return false
}
return null
case UITypes.Date: { case UITypes.Date: {
const parsedDate = dayjs(value) const parsedDate = dayjs(value)
if (!parsedDate.isValid()) throw new Error('Not a valid date') if (!parsedDate.isValid()) {
if (isMultiple) {
return null
} else {
throw new Error('Not a valid date')
}
}
return parsedDate.format('YYYY-MM-DD') return parsedDate.format('YYYY-MM-DD')
} }
case UITypes.DateTime: { case UITypes.DateTime: {
const parsedDateTime = dayjs(value) const parsedDateTime = dayjs(value)
if (!parsedDateTime.isValid()) { if (!parsedDateTime.isValid()) {
throw new Error('Not a valid datetime value') if (isMultiple) {
return null
} else {
throw new Error('Not a valid datetime value')
}
} }
return parsedDateTime.utc().format('YYYY-MM-DD HH:mm:ssZ') return parsedDateTime.utc().format('YYYY-MM-DD HH:mm:ssZ')
} }
@ -54,7 +79,11 @@ export default function convertCellData(
parsedTime = dayjs(`1999-01-01 ${value}`) parsedTime = dayjs(`1999-01-01 ${value}`)
} }
if (!parsedTime.isValid()) { if (!parsedTime.isValid()) {
throw new Error('Not a valid time value') if (isMultiple) {
return null
} else {
throw new Error('Not a valid time value')
}
} }
return parsedTime.format(dateFormat) return parsedTime.format(dateFormat)
} }
@ -69,7 +98,11 @@ export default function convertCellData(
return parsedDate.format('YYYY') return parsedDate.format('YYYY')
} }
throw new Error('Not a valid year value') if (isMultiple) {
return null
} else {
throw new Error('Not a valid year value')
}
} }
case UITypes.Attachment: { case UITypes.Attachment: {
let parsedVal let parsedVal
@ -77,11 +110,17 @@ export default function convertCellData(
parsedVal = parseProp(value) parsedVal = parseProp(value)
parsedVal = Array.isArray(parsedVal) ? parsedVal : [parsedVal] parsedVal = Array.isArray(parsedVal) ? parsedVal : [parsedVal]
} catch (e) { } catch (e) {
throw new Error('Invalid attachment data') if (isMultiple) {
return null
} else {
throw new Error('Invalid attachment data')
}
} }
if (parsedVal.some((v: any) => v && !(v.url || v.data))) {
throw new Error('Invalid attachment data') if (parsedVal.some((v: any) => v && !(v.url || v.data || v.path))) {
return null
} }
// TODO(refactor): duplicate logic in attachment/utils.ts // TODO(refactor): duplicate logic in attachment/utils.ts
const defaultAttachmentMeta = { const defaultAttachmentMeta = {
...(args.appInfo.ee && { ...(args.appInfo.ee && {
@ -95,7 +134,7 @@ export default function convertCellData(
const attachmentMeta = { const attachmentMeta = {
...defaultAttachmentMeta, ...defaultAttachmentMeta,
...parseProp(args.column?.meta), ...parseProp(column?.meta),
} }
const attachments = [] const attachments = []
@ -134,12 +173,31 @@ export default function convertCellData(
return JSON.stringify(attachments) return JSON.stringify(attachments)
} }
case UITypes.SingleSelect:
case UITypes.MultiSelect: {
// return null if value is empty
if (value === '') return null
const availableOptions = ((column.colOptions as SelectOptionsType)?.options || []).map((o) => o.title)
const vals = value.split(',')
const validVals = vals.filter((v) => availableOptions.includes(v))
// return null if no valid values
if (validVals.length === 0) return null
return validVals.join(',')
}
case UITypes.LinkToAnotherRecord: case UITypes.LinkToAnotherRecord:
case UITypes.Lookup: case UITypes.Lookup:
case UITypes.Rollup: case UITypes.Rollup:
case UITypes.Formula: case UITypes.Formula:
case UITypes.QrCode: case UITypes.QrCode: {
throw new Error(`Unsupported conversion from ${from} to ${to}`) if (isMultiple) {
return undefined
} else {
throw new Error(`Unsupported conversion for ${to}`)
}
}
default: default:
return value return value
} }

28
packages/nc-gui/composables/useMultiSelect/copyValue.ts

@ -1,28 +0,0 @@
import type { ColumnType } from 'nocodb-sdk'
import type { Row } from '~/lib'
export const copyTable = async (rows: Row[], cols: ColumnType[]) => {
let copyHTML = '<table>'
let copyPlainText = ''
rows.forEach((row) => {
let copyRow = '<tr>'
cols.forEach((col) => {
let value = (col.title && row.row[col.title]) ?? ''
if (typeof value === 'object') {
value = JSON.stringify(value)
}
copyRow += `<td>${value}</td>`
copyPlainText = `${copyPlainText} ${value} \t`
})
copyHTML += `${copyRow}</tr>`
copyPlainText = `${copyPlainText.trim()}\n`
})
copyHTML += '</table>'
copyPlainText.trim()
const blobHTML = new Blob([copyHTML], { type: 'text/html' })
const blobPlainText = new Blob([copyPlainText], { type: 'text/plain' })
return navigator.clipboard.write([new ClipboardItem({ [blobHTML.type]: blobHTML, [blobPlainText.type]: blobPlainText })])
}

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

@ -1,16 +1,17 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import type { MaybeRef } from '@vueuse/core' import type { MaybeRef } from '@vueuse/core'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' import { RelationTypes, UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { parse } from 'papaparse'
import type { Cell } from './cellRange' import type { Cell } from './cellRange'
import { CellRange } from './cellRange' import { CellRange } from './cellRange'
import convertCellData from './convertCellData' import convertCellData from './convertCellData'
import type { Nullable, Row } from '~/lib' import type { Nullable, Row } from '~/lib'
import { import {
copyTable,
dateFormats, dateFormats,
extractPkFromRow, extractPkFromRow,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
isDrawerOrModalExist,
isMac, isMac,
isTypableInputColumn, isTypableInputColumn,
message, message,
@ -24,6 +25,7 @@ import {
useI18n, useI18n,
useMetas, useMetas,
useProject, useProject,
useUIPermission,
} from '#imports' } from '#imports'
const MAIN_MOUSE_PRESSED = 0 const MAIN_MOUSE_PRESSED = 0
@ -38,15 +40,15 @@ export function useMultiSelect(
_editEnabled: MaybeRef<boolean>, _editEnabled: MaybeRef<boolean>,
isPkAvail: MaybeRef<boolean | undefined>, isPkAvail: MaybeRef<boolean | undefined>,
clearCell: Function, clearCell: Function,
clearSelectedRangeOfCells: Function,
makeEditable: Function, makeEditable: Function,
scrollToActiveCell?: (row?: number | null, col?: number | null) => void, scrollToCell?: (row?: number | null, col?: number | null) => void,
keyEventHandler?: Function, keyEventHandler?: Function,
syncCellData?: Function, syncCellData?: Function,
bulkUpdateRows?: Function,
) { ) {
const meta = ref(_meta) const meta = ref(_meta)
const tbodyEl = ref<HTMLElement>()
const { t } = useI18n() const { t } = useI18n()
const { copy } = useCopy() const { copy } = useCopy()
@ -57,8 +59,6 @@ export function useMultiSelect(
const { isMysql } = useProject() const { isMysql } = useProject()
let clipboardContext = $ref<{ value: any; uidt: UITypes } | null>(null)
const editEnabled = ref(_editEnabled) const editEnabled = ref(_editEnabled)
let isMouseDown = $ref(false) let isMouseDown = $ref(false)
@ -73,6 +73,9 @@ export function useMultiSelect(
() => !(activeCell.row === null || activeCell.col === null || isNaN(activeCell.row) || isNaN(activeCell.col)), () => !(activeCell.row === null || activeCell.col === null || isNaN(activeCell.row) || isNaN(activeCell.col)),
) )
const { isUIAllowed } = useUIPermission()
const hasEditPermission = $computed(() => isUIAllowed('xcDatatableEditable'))
function makeActive(row: number, col: number) { function makeActive(row: number, col: number) {
if (activeCell.row === row && activeCell.col === col) { if (activeCell.row === row && activeCell.col === col) {
return return
@ -96,6 +99,85 @@ export function useMultiSelect(
return parseProp(column?.meta)?.time_format ?? timeFormats[0] return parseProp(column?.meta)?.time_format ?? timeFormats[0]
} }
const valueToCopy = (rowObj: Row, columnObj: ColumnType) => {
let textToCopy = (columnObj.title && rowObj.row[columnObj.title]) || ''
if (columnObj.uidt === UITypes.Checkbox) {
textToCopy = !!textToCopy
}
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 (!dayjs(textToCopy).isValid()) {
// return empty string for invalid datetime / time
return ''
}
}
if (columnObj.uidt === UITypes.LongText) {
textToCopy = `"${textToCopy.replace(/\"/g, '""')}"`
}
return textToCopy
}
const copyTable = async (rows: Row[], cols: ColumnType[]) => {
let copyHTML = '<table>'
let copyPlainText = ''
rows.forEach((row, i) => {
let copyRow = '<tr>'
cols.forEach((col, i) => {
const value = valueToCopy(row, col)
copyRow += `<td>${value}</td>`
copyPlainText = `${copyPlainText}${value}${cols.length - 1 !== i ? '\t' : ''}`
})
copyHTML += `${copyRow}</tr>`
if (rows.length - 1 !== i) {
copyPlainText = `${copyPlainText}\n`
}
})
copyHTML += '</table>'
const blobHTML = new Blob([copyHTML], { type: 'text/html' })
const blobPlainText = new Blob([copyPlainText], { type: 'text/plain' })
return navigator.clipboard.write([new ClipboardItem({ [blobHTML.type]: blobHTML, [blobPlainText.type]: blobPlainText })])
}
async function copyValue(ctx?: Cell) { async function copyValue(ctx?: Cell) {
try { try {
if (selectedRange.start !== null && selectedRange.end !== null && !selectedRange.isSingleCell()) { if (selectedRange.start !== null && selectedRange.end !== null && !selectedRange.isSingleCell()) {
@ -114,51 +196,7 @@ export function useMultiSelect(
const rowObj = unref(data)[cpRow] const rowObj = unref(data)[cpRow]
const columnObj = unref(fields)[cpCol] const columnObj = unref(fields)[cpCol]
let textToCopy = (columnObj.title && rowObj.row[columnObj.title]) || '' const textToCopy = valueToCopy(rowObj, columnObj)
if (columnObj.uidt === UITypes.Checkbox) {
textToCopy = !!textToCopy
}
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) await copy(textToCopy)
message.success(t('msg.info.copiedToClipboard')) message.success(t('msg.info.copiedToClipboard'))
@ -195,35 +233,65 @@ export function useMultiSelect(
function handleMouseDown(event: MouseEvent, row: number, col: number) { function handleMouseDown(event: MouseEvent, row: number, col: number) {
// if there was a right click on selected range, don't restart the selection // if there was a right click on selected range, don't restart the selection
if (event?.button !== MAIN_MOUSE_PRESSED && isCellSelected(row, col)) { if (
(event?.button !== MAIN_MOUSE_PRESSED || (event?.button === MAIN_MOUSE_PRESSED && event.ctrlKey)) &&
isCellSelected(row, col)
) {
return return
} }
isMouseDown = true isMouseDown = true
// if shift key is pressed, don't restart the selection
if (event.shiftKey) return
selectedRange.startRange({ row, col }) selectedRange.startRange({ row, col })
if (activeCell.row !== row || activeCell.col !== col) {
// clear active cell on selection start
activeCell.row = null
activeCell.col = null
}
} }
const handleCellClick = (event: MouseEvent, row: number, col: number) => { const handleCellClick = (event: MouseEvent, row: number, col: number) => {
isMouseDown = true isMouseDown = true
selectedRange.startRange({ row, col })
// if shift key is pressed, prevent selecting text
if (event.shiftKey && !unref(editEnabled)) {
event.preventDefault()
}
// if shift key is pressed, don't restart the selection (unless there is no active cell)
if (!event.shiftKey || activeCell.col === null || activeCell.row === null) {
selectedRange.startRange({ row, col })
makeActive(row, col)
}
selectedRange.endRange({ row, col }) selectedRange.endRange({ row, col })
makeActive(row, col) scrollToCell?.(row, col)
isMouseDown = false isMouseDown = false
} }
const handleMouseUp = (event: MouseEvent) => { const handleMouseUp = (event: MouseEvent) => {
// timeout is needed, because we want to set cell as active AFTER all the child's click handler's called if (isMouseDown) {
// this is needed e.g. for date field edit, where two clicks had to be done - one to select cell, and another one to open date dropdown isMouseDown = false
setTimeout(() => {
makeActive(selectedRange.start.row, selectedRange.start.col) // timeout is needed, because we want to set cell as active AFTER all the child's click handler's called
}, 0) // this is needed e.g. for date field edit, where two clicks had to be done - one to select cell, and another one to open date dropdown
setTimeout(() => {
// if the editEnabled is false, prevent selecting text on mouseUp // if shift key is pressed, don't change the active cell
if (!unref(editEnabled)) { if (event.shiftKey) return
event.preventDefault() if (selectedRange._start) {
} makeActive(selectedRange._start.row, selectedRange._start.col)
}
}, 0)
isMouseDown = false // if the editEnabled is false, prevent selecting text on mouseUp
if (!unref(editEnabled)) {
event.preventDefault()
}
}
} }
const handleKeyDown = async (e: KeyboardEvent) => { const handleKeyDown = async (e: KeyboardEvent) => {
@ -232,10 +300,12 @@ export function useMultiSelect(
return true return true
} }
if (!isCellActive.value) { if (!isCellActive.value || activeCell.row === null || activeCell.col === null) {
return return
} }
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
/** on tab key press navigate through cells */ /** on tab key press navigate through cells */
switch (e.key) { switch (e.key) {
case 'Tab': case 'Tab':
@ -261,7 +331,7 @@ export function useMultiSelect(
editEnabled.value = false editEnabled.value = false
} }
} }
scrollToActiveCell?.() scrollToCell?.()
break break
/** on enter key press make cell editable */ /** on enter key press make cell editable */
case 'Enter': case 'Enter':
@ -273,49 +343,136 @@ export function useMultiSelect(
/** on delete key press clear cell */ /** on delete key press clear cell */
case 'Delete': case 'Delete':
e.preventDefault() e.preventDefault()
selectedRange.clear()
await clearCell(activeCell as { row: number; col: number }) if (selectedRange.isSingleCell()) {
selectedRange.clear()
await clearCell(activeCell as { row: number; col: number })
} else {
await clearSelectedRangeOfCells()
}
break break
/** on arrow key press navigate through cells */ /** on arrow key press navigate through cells */
case 'ArrowRight': case 'ArrowRight':
e.preventDefault() e.preventDefault()
selectedRange.clear()
if (activeCell.col < unref(columnLength) - 1) { if (e.shiftKey) {
activeCell.col++ if (cmdOrCtrl) {
scrollToActiveCell?.() editEnabled.value = false
editEnabled.value = false selectedRange.endRange({
row: selectedRange._end?.row ?? activeCell.row,
col: unref(columnLength) - 1,
})
scrollToCell?.(selectedRange._end?.row, selectedRange._end?.col)
} else if ((selectedRange._end?.col ?? activeCell.col) < unref(columnLength) - 1) {
editEnabled.value = false
selectedRange.endRange({
row: selectedRange._end?.row ?? activeCell.row,
col: (selectedRange._end?.col ?? activeCell.col) + 1,
})
scrollToCell?.(selectedRange._end?.row, selectedRange._end?.col)
}
} else {
selectedRange.clear()
if (activeCell.col < unref(columnLength) - 1) {
activeCell.col++
selectedRange.startRange({ row: activeCell.row, col: activeCell.col })
scrollToCell?.()
editEnabled.value = false
}
} }
break break
case 'ArrowLeft': case 'ArrowLeft':
e.preventDefault() e.preventDefault()
selectedRange.clear()
if (activeCell.col > 0) { if (e.shiftKey) {
activeCell.col-- if (cmdOrCtrl) {
scrollToActiveCell?.() editEnabled.value = false
editEnabled.value = false selectedRange.endRange({
row: selectedRange._end?.row ?? activeCell.row,
col: 0,
})
scrollToCell?.(selectedRange._end?.row, selectedRange._end?.col)
} else if ((selectedRange._end?.col ?? activeCell.col) > 0) {
editEnabled.value = false
selectedRange.endRange({
row: selectedRange._end?.row ?? activeCell.row,
col: (selectedRange._end?.col ?? activeCell.col) - 1,
})
scrollToCell?.(selectedRange._end?.row, selectedRange._end?.col)
}
} else {
selectedRange.clear()
if (activeCell.col > 0) {
activeCell.col--
selectedRange.startRange({ row: activeCell.row, col: activeCell.col })
scrollToCell?.()
editEnabled.value = false
}
} }
break break
case 'ArrowUp': case 'ArrowUp':
e.preventDefault() e.preventDefault()
selectedRange.clear()
if (activeCell.row > 0) { if (e.shiftKey) {
activeCell.row-- if (cmdOrCtrl) {
scrollToActiveCell?.() editEnabled.value = false
editEnabled.value = false console.log(selectedRange._end?.col)
selectedRange.endRange({
row: 0,
col: selectedRange._end?.col ?? activeCell.col,
})
console.log(selectedRange._end?.col)
scrollToCell?.(selectedRange._end?.row, selectedRange._end?.col)
} else if ((selectedRange._end?.row ?? activeCell.row) > 0) {
editEnabled.value = false
selectedRange.endRange({
row: (selectedRange._end?.row ?? activeCell.row) - 1,
col: selectedRange._end?.col ?? activeCell.col,
})
scrollToCell?.(selectedRange._end?.row, selectedRange._end?.col)
}
} else {
selectedRange.clear()
if (activeCell.row > 0) {
activeCell.row--
selectedRange.startRange({ row: activeCell.row, col: activeCell.col })
scrollToCell?.()
editEnabled.value = false
}
} }
break break
case 'ArrowDown': case 'ArrowDown':
e.preventDefault() e.preventDefault()
selectedRange.clear()
if (activeCell.row < unref(data).length - 1) { if (e.shiftKey) {
activeCell.row++ if (cmdOrCtrl) {
scrollToActiveCell?.() editEnabled.value = false
editEnabled.value = false selectedRange.endRange({
row: unref(data).length - 1,
col: selectedRange._end?.col ?? activeCell.col,
})
scrollToCell?.(selectedRange._end?.row, selectedRange._end?.col)
} else if ((selectedRange._end?.row ?? activeCell.row) < unref(data).length - 1) {
editEnabled.value = false
selectedRange.endRange({
row: (selectedRange._end?.row ?? activeCell.row) + 1,
col: selectedRange._end?.col ?? activeCell.col,
})
scrollToCell?.(selectedRange._end?.row, selectedRange._end?.col)
}
} else {
selectedRange.clear()
if (activeCell.row < unref(data).length - 1) {
activeCell.row++
selectedRange.startRange({ row: activeCell.row, col: activeCell.col })
scrollToCell?.()
editEnabled.value = false
}
} }
break break
default: default:
@ -323,86 +480,21 @@ export function useMultiSelect(
const rowObj = unref(data)[activeCell.row] const rowObj = unref(data)[activeCell.row]
const columnObj = unref(fields)[activeCell.col] const columnObj = unref(fields)[activeCell.col]
if ((!unref(editEnabled) || !isTypableInputColumn(columnObj)) && (isMac() ? e.metaKey : e.ctrlKey)) { if (
(!unref(editEnabled) || !isTypableInputColumn(columnObj)) &&
!isDrawerOrModalExist() &&
(isMac() ? e.metaKey : e.ctrlKey)
) {
switch (e.keyCode) { switch (e.keyCode) {
// copy - ctrl/cmd +c // copy - ctrl/cmd +c
case 67: case 67:
// set clipboard context only if single cell selected
// or if selected range is empty
if (selectedRange.isSingleCell() || (selectedRange.isEmpty() && rowObj && columnObj)) {
clipboardContext = {
value: rowObj.row[columnObj.title!],
uidt: columnObj.uidt as UITypes,
}
} else {
clipboardContext = null
}
await copyValue() await copyValue()
break break
// paste - ctrl/cmd + v // select all - ctrl/cmd +a
case 86: case 65:
try { selectedRange.startRange({ row: 0, col: 0 })
// handle belongs to column selectedRange.endRange({ row: unref(data).length - 1, col: unref(columnLength) - 1 })
if ( break
columnObj.uidt === UITypes.LinkToAnotherRecord &&
(columnObj.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.BELONGS_TO
) {
if (!clipboardContext || typeof clipboardContext.value !== 'object') {
return message.info('Invalid data')
}
rowObj.row[columnObj.title!] = convertCellData(
{
value: clipboardContext.value,
from: clipboardContext.uidt,
to: columnObj.uidt as UITypes,
column: columnObj,
appInfo: unref(appInfo),
},
isMysql(meta.value?.base_id),
)
e.preventDefault()
const foreignKeyColumn = meta.value?.columns?.find(
(column: ColumnType) => column.id === (columnObj.colOptions as LinkToAnotherRecordType)?.fk_child_column_id,
)
const relatedTableMeta = await getMeta((columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id!)
if (!foreignKeyColumn) return
rowObj.row[foreignKeyColumn.title!] = extractPkFromRow(
clipboardContext.value,
(relatedTableMeta as any)!.columns!,
)
return await syncCellData?.({ ...activeCell, updatedColumnTitle: foreignKeyColumn.title })
}
// if it's a virtual column excluding belongs to cell type skip paste
if (isVirtualCol(columnObj)) {
return message.info(t('msg.info.pasteNotSupported'))
}
if (clipboardContext) {
rowObj.row[columnObj.title!] = convertCellData(
{
value: clipboardContext.value,
from: clipboardContext.uidt,
to: columnObj.uidt as UITypes,
column: columnObj,
appInfo: unref(appInfo),
},
isMysql(meta.value?.base_id),
)
e.preventDefault()
syncCellData?.(activeCell)
} else {
clearCell(activeCell as { row: number; col: number }, true)
makeEditable(rowObj, columnObj)
}
} catch (error: any) {
message.error(await extractSdkResponseErrorMsg(error))
}
} }
} }
@ -430,8 +522,230 @@ export function useMultiSelect(
const clearSelectedRange = selectedRange.clear.bind(selectedRange) const clearSelectedRange = selectedRange.clear.bind(selectedRange)
const isPasteable = (row?: Row, col?: ColumnType, showInfo = false) => {
if (!row || !col) {
if (showInfo) {
message.info('Please select a cell to paste')
}
return false
}
// skip pasting virtual columns (including LTAR columns for now) and system columns
if (isVirtualCol(col) || isSystemColumn(col)) {
if (showInfo) {
message.info(t('msg.info.pasteNotSupported'))
}
return false
}
// skip pasting auto increment columns
if (col.ai) {
if (showInfo) {
message.info(t('msg.info.autoIncFieldNotEditable'))
}
return false
}
// skip pasting primary key columns
if (col.pk && !row.rowMeta.new) {
if (showInfo) {
message.info(t('msg.info.editingPKnotSupported'))
}
return false
}
return true
}
const handlePaste = async (e: ClipboardEvent) => {
if (isDrawerOrModalExist()) {
return
}
if (!isCellActive.value) {
return
}
if (unref(editEnabled)) {
return
}
if (activeCell.row === null || activeCell.row === undefined || activeCell.col === null || activeCell.col === undefined) {
return
}
e.preventDefault()
const clipboardData = e.clipboardData?.getData('text/plain') || ''
try {
if (clipboardData?.includes('\n') || clipboardData?.includes('\t')) {
// if the clipboard data contains new line or tab, then it is a matrix or LongText
const parsedClipboard = parse(clipboardData, { delimiter: '\t' })
if (parsedClipboard.errors.length > 0) {
throw new Error(parsedClipboard.errors[0].message)
}
const clipboardMatrix = parsedClipboard.data as string[][]
const pasteMatrixRows = clipboardMatrix.length
const pasteMatrixCols = clipboardMatrix[0].length
const colsToPaste = unref(fields).slice(activeCell.col, activeCell.col + pasteMatrixCols)
const rowsToPaste = unref(data).slice(activeCell.row, activeCell.row + pasteMatrixRows)
const propsToPaste: string[] = []
let pastedRows = 0
for (let i = 0; i < pasteMatrixRows; i++) {
const pasteRow = rowsToPaste[i]
// TODO handle insert new row
if (!pasteRow || pasteRow.rowMeta.new) break
pastedRows++
for (let j = 0; j < pasteMatrixCols; j++) {
const pasteCol = colsToPaste[j]
if (!isPasteable(pasteRow, pasteCol)) {
continue
}
propsToPaste.push(pasteCol.title!)
const pasteValue = convertCellData(
{
value: clipboardMatrix[i][j],
to: pasteCol.uidt as UITypes,
column: pasteCol,
appInfo: unref(appInfo),
},
isMysql(meta.value?.base_id),
true,
)
if (pasteValue !== undefined) {
pasteRow.row[pasteCol.title!] = pasteValue
}
}
}
await bulkUpdateRows?.(rowsToPaste, propsToPaste)
if (pastedRows > 0) {
// highlight the pasted range
selectedRange.startRange({ row: activeCell.row, col: activeCell.col })
selectedRange.endRange({ row: activeCell.row + pastedRows - 1, col: activeCell.col + pasteMatrixCols - 1 })
}
} else {
if (selectedRange.isSingleCell()) {
const rowObj = unref(data)[activeCell.row]
const columnObj = unref(fields)[activeCell.col]
// handle belongs to column
if (
columnObj.uidt === UITypes.LinkToAnotherRecord &&
(columnObj.colOptions as LinkToAnotherRecordType)?.type === RelationTypes.BELONGS_TO
) {
const clipboardContext = JSON.parse(clipboardData!)
rowObj.row[columnObj.title!] = convertCellData(
{
value: clipboardContext,
to: columnObj.uidt as UITypes,
column: columnObj,
appInfo: unref(appInfo),
},
isMysql(meta.value?.base_id),
)
const foreignKeyColumn = meta.value?.columns?.find(
(column: ColumnType) => column.id === (columnObj.colOptions as LinkToAnotherRecordType)?.fk_child_column_id,
)
const relatedTableMeta = await getMeta((columnObj.colOptions as LinkToAnotherRecordType).fk_related_model_id!)
if (!foreignKeyColumn) return
rowObj.row[foreignKeyColumn.title!] = extractPkFromRow(clipboardContext, (relatedTableMeta as any)!.columns!)
return await syncCellData?.({ ...activeCell, updatedColumnTitle: foreignKeyColumn.title })
}
if (!isPasteable(rowObj, columnObj, true)) {
return
}
const pasteValue = convertCellData(
{
value: clipboardData,
to: columnObj.uidt as UITypes,
column: columnObj,
appInfo: unref(appInfo),
},
isMysql(meta.value?.base_id),
)
if (pasteValue !== undefined) {
rowObj.row[columnObj.title!] = pasteValue
}
await syncCellData?.(activeCell)
} else {
const start = selectedRange.start
const end = selectedRange.end
const startRow = Math.min(start.row, end.row)
const endRow = Math.max(start.row, end.row)
const startCol = Math.min(start.col, end.col)
const endCol = Math.max(start.col, end.col)
const cols = unref(fields).slice(startCol, endCol + 1)
const rows = unref(data).slice(startRow, endRow + 1)
const props = []
for (const row of rows) {
// TODO handle insert new row
if (!row || row.rowMeta.new) continue
for (const col of cols) {
if (!col.title) continue
if (!isPasteable(row, col)) {
continue
}
props.push(col.title)
const pasteValue = convertCellData(
{
value: clipboardData,
to: col.uidt as UITypes,
column: col,
appInfo: unref(appInfo),
},
isMysql(meta.value?.base_id),
true,
)
if (pasteValue !== undefined) {
row.row[col.title] = pasteValue
}
}
}
await bulkUpdateRows?.(rows, props)
}
}
} catch (error: any) {
message.error(await extractSdkResponseErrorMsg(error))
}
}
useEventListener(document, 'keydown', handleKeyDown) useEventListener(document, 'keydown', handleKeyDown)
useEventListener(tbodyEl, 'mouseup', handleMouseUp) useEventListener(document, 'mouseup', handleMouseUp)
useEventListener(document, 'paste', handlePaste)
return { return {
isCellActive, isCellActive,
@ -442,7 +756,8 @@ export function useMultiSelect(
isCellSelected, isCellSelected,
activeCell, activeCell,
handleCellClick, handleCellClick,
tbodyEl,
resetSelectedRange, resetSelectedRange,
selectedRange,
makeActive,
} }
} }

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

@ -1,6 +1,7 @@
import { UITypes, ViewTypes } from 'nocodb-sdk' import { UITypes, ViewTypes } from 'nocodb-sdk'
import type { Api, ColumnType, FormColumnType, FormType, GalleryType, PaginatedType, TableType, ViewType } from 'nocodb-sdk' import type { Api, ColumnType, FormColumnType, FormType, GalleryType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import type { CellRange } from '#imports'
import { import {
IsPublicInj, IsPublicInj,
NOCO, NOCO,
@ -447,6 +448,124 @@ export function useViewData(
} }
} }
async function bulkUpdateRows(
rows: Row[],
props: string[],
{ metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {},
undo = false,
) {
const promises = []
for (const row of rows) {
// update changed status
if (row.rowMeta) row.rowMeta.changed = false
// if new row and save is in progress then wait until the save is complete
promises.push(until(() => !(row.rowMeta?.new && row.rowMeta?.saving)).toMatch((v) => v))
}
await Promise.all(promises)
const updateArray = []
for (const row of rows) {
if (row.rowMeta) row.rowMeta.saving = true
const pk = rowPkData(row.row, metaValue?.columns as ColumnType[])
const updateData = props.reduce((acc: Record<string, any>, prop) => {
acc[prop] = row.row[prop]
return acc
}, {} as Record<string, any>)
updateArray.push({ ...updateData, ...pk })
}
if (!undo) {
addUndo({
redo: {
fn: async function redo(redoRows: Row[], props: string[], pg: { page: number; pageSize: number }) {
await bulkUpdateRows(redoRows, props, { metaValue, viewMetaValue }, true)
if (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize) {
for (const toUpdate of redoRows) {
const rowIndex = findIndexByPk(rowPkData(toUpdate.row, meta?.value?.columns as ColumnType[]))
if (rowIndex !== -1) {
const row = formattedData.value[rowIndex]
Object.assign(row.row, toUpdate.row)
Object.assign(row.oldRow, toUpdate.row)
} else {
await loadData()
break
}
}
} else {
await changePage(pg.page)
}
},
args: [clone(rows), clone(props), { page: paginationData.value.page, pageSize: paginationData.value.pageSize }],
},
undo: {
fn: async function undo(undoRows: Row[], props: string[], pg: { page: number; pageSize: number }) {
await bulkUpdateRows(undoRows, props, { metaValue, viewMetaValue }, true)
if (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize) {
for (const toUpdate of undoRows) {
const rowIndex = findIndexByPk(rowPkData(toUpdate.row, meta?.value?.columns as ColumnType[]))
if (rowIndex !== -1) {
const row = formattedData.value[rowIndex]
Object.assign(row.row, toUpdate.row)
Object.assign(row.oldRow, toUpdate.row)
} else {
await loadData()
break
}
}
} else {
await changePage(pg.page)
}
},
args: [
clone(
rows.map((row) => {
return { row: row.oldRow, oldRow: row.row, rowMeta: row.rowMeta }
}),
),
props,
{ page: paginationData.value.page, pageSize: paginationData.value.pageSize },
],
},
scope: defineViewScope({ view: viewMetaValue }),
})
}
await $api.dbTableRow.bulkUpdate(NOCO, metaValue?.project_id as string, metaValue?.id as string, updateArray)
for (const row of rows) {
if (!undo) {
/** update row data(to sync formula and other related columns)
* update only formula, rollup and auto updated datetime columns data to avoid overwriting any changes made by user
*/
Object.assign(
row.row,
metaValue!.columns!.reduce<Record<string, any>>((acc: Record<string, any>, col: ColumnType) => {
if (
col.uidt === UITypes.Formula ||
col.uidt === UITypes.QrCode ||
col.uidt === UITypes.Barcode ||
col.uidt === UITypes.Rollup ||
col.au ||
col.cdf?.includes(' on update ')
)
acc[col.title!] = row.row[col.title!]
return acc
}, {} as Record<string, any>),
)
Object.assign(row.oldRow, row.row)
}
if (row.rowMeta) row.rowMeta.saving = false
}
}
async function changePage(page: number) { async function changePage(page: number) {
paginationData.value.page = page paginationData.value.page = page
await loadData({ await loadData({
@ -615,6 +734,82 @@ export function useViewData(
await syncPagination() await syncPagination()
} }
async function deleteRangeOfRows(cellRange: CellRange) {
if (!cellRange._start || !cellRange._end) return
const start = Math.max(cellRange._start.row, cellRange._end.row)
const end = Math.min(cellRange._start.row, cellRange._end.row)
// plus one because we want to include the end row
let row = start + 1
const removedRowsData: { id?: string; row: Row; rowIndex: number }[] = []
while (row--) {
try {
const { row: rowObj, rowMeta } = formattedData.value[row] as Record<string, any>
if (!rowMeta.new) {
const id = meta?.value?.columns
?.filter((c) => c.pk)
.map((c) => rowObj[c.title as string])
.join('___')
const successfulDeletion = await deleteRowById(id as string)
if (!successfulDeletion) {
continue
}
removedRowsData.push({ id, row: clone(formattedData.value[row]), rowIndex: row })
}
formattedData.value.splice(row, 1)
} catch (e: any) {
return message.error(`${t('msg.error.deleteRowFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
}
if (row === end) break
}
addUndo({
redo: {
fn: async function redo(this: UndoRedoAction, removedRowsData: { id?: string; row: Row; rowIndex: number }[]) {
for (const { id, row } of removedRowsData) {
await deleteRowById(id as string)
const pk: Record<string, string> = rowPkData(row.row, meta?.value?.columns as ColumnType[])
const rowIndex = findIndexByPk(pk)
if (rowIndex !== -1) formattedData.value.splice(rowIndex, 1)
paginationData.value.totalRows = paginationData.value.totalRows! - 1
}
await syncPagination()
},
args: [removedRowsData],
},
undo: {
fn: async function undo(
this: UndoRedoAction,
removedRowsData: { id?: string; row: Row; rowIndex: number }[],
pg: { page: number; pageSize: number },
) {
for (const { row, rowIndex } of removedRowsData.slice().reverse()) {
const pkData = rowPkData(row.row, meta.value?.columns as ColumnType[])
row.row = { ...pkData, ...row.row }
await insertRow(row, {}, {}, true)
if (rowIndex !== -1 && pg.pageSize === paginationData.value.pageSize) {
if (pg.page === paginationData.value.page) {
formattedData.value.splice(rowIndex, 0, row)
} else {
await changePage(pg.page)
}
} else {
await loadData()
}
}
},
args: [removedRowsData, { page: paginationData.value.page, pageSize: paginationData.value.pageSize }],
},
scope: defineViewScope({ view: viewMeta.value }),
})
await syncCount()
await syncPagination()
}
async function loadFormView() { async function loadFormView() {
if (!viewMeta?.value?.id) return if (!viewMeta?.value?.id) return
try { try {
@ -728,7 +923,9 @@ export function useViewData(
deleteRow, deleteRow,
deleteRowById, deleteRowById,
deleteSelectedRows, deleteSelectedRows,
deleteRangeOfRows,
updateOrSaveRow, updateOrSaveRow,
bulkUpdateRows,
selectedAllRecords, selectedAllRecords,
syncCount, syncCount,
syncPagination, syncPagination,

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

@ -23,7 +23,7 @@ export const IsExpandedFormOpenInj: InjectionKey<Ref<boolean>> = Symbol('is-expa
export const CellValueInj: InjectionKey<Ref<any>> = Symbol('cell-value-injection') export const CellValueInj: InjectionKey<Ref<any>> = Symbol('cell-value-injection')
export const ActiveViewInj: InjectionKey<Ref<ViewType>> = Symbol('active-view-injection') export const ActiveViewInj: InjectionKey<Ref<ViewType>> = Symbol('active-view-injection')
export const ReadonlyInj: InjectionKey<Ref<boolean>> = Symbol('readonly-injection') export const ReadonlyInj: InjectionKey<Ref<boolean>> = Symbol('readonly-injection')
export const RowHeightInj: InjectionKey<ComputedRef<1 | 2 | 4 | 6 | undefined>> = Symbol('row-height-injection') export const RowHeightInj: InjectionKey<Ref<1 | 2 | 4 | 6 | undefined>> = Symbol('row-height-injection')
/** when bool is passed, it indicates if a loading spinner should be visible while reloading */ /** when bool is passed, it indicates if a loading spinner should be visible while reloading */
export const ReloadViewDataHookInj: InjectionKey<EventHook<boolean | void>> = Symbol('reload-view-data-injection') export const ReloadViewDataHookInj: InjectionKey<EventHook<boolean | void>> = Symbol('reload-view-data-injection')
export const ReloadViewMetaHookInj: InjectionKey<EventHook<boolean | void>> = Symbol('reload-view-meta-injection') export const ReloadViewMetaHookInj: InjectionKey<EventHook<boolean | void>> = Symbol('reload-view-meta-injection')

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

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

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

@ -30,7 +30,7 @@
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1", "locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"nocodb-sdk": "0.108.1", "nocodb-sdk": "file:../nocodb-sdk",
"papaparse": "^5.3.2", "papaparse": "^5.3.2",
"pinia": "^2.0.33", "pinia": "^2.0.33",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
@ -101,8 +101,8 @@
"nuxt-windicss": "^2.5.0", "nuxt-windicss": "^2.5.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"sass": "^1.53.0", "sass": "^1.53.0",
"unplugin-icons": "^0.14.7", "unplugin-icons": "^0.14.15",
"unplugin-vue-components": "^0.22.4", "unplugin-vue-components": "^0.22.12",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-purge-icons": "^0.9.0", "vite-plugin-purge-icons": "^0.9.0",
"vitest": "^0.18.0", "vitest": "^0.18.0",
@ -111,7 +111,6 @@
}, },
"../nocodb-sdk": { "../nocodb-sdk": {
"version": "0.108.1", "version": "0.108.1",
"extraneous": true,
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
@ -260,9 +259,9 @@
} }
}, },
"node_modules/@antfu/install-pkg": { "node_modules/@antfu/install-pkg": {
"version": "0.1.0", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.1.0.tgz", "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.1.1.tgz",
"integrity": "sha512-VaIJd3d1o7irZfK1U0nvBsHMyjkuyMP3HKYVV53z8DKyulkHKmjhhtccXO51WSPeeSHIeoJEoNOKavYpS7jkZw==", "integrity": "sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"execa": "^5.1.1", "execa": "^5.1.1",
@ -343,9 +342,9 @@
} }
}, },
"node_modules/@antfu/utils": { "node_modules/@antfu/utils": {
"version": "0.5.2", "version": "0.7.4",
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.5.2.tgz", "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.4.tgz",
"integrity": "sha512-CQkeV+oJxUazwjlHD0/3ZD08QWKuGQkhnrKo3e6ly5pd48VUpXbb77q0xMU4+vc2CkJnDS02Eq/M9ugyX20XZA==", "integrity": "sha512-qe8Nmh9rYI/HIspLSTwtbMFPj6dISG6+dJnOguTlPNXtCvS2uezdxscVBb7/3DrmNbQK49TDqpkSQ1chbRGdpQ==",
"dev": true, "dev": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
@ -1235,23 +1234,22 @@
} }
}, },
"node_modules/@iconify/types": { "node_modules/@iconify/types": {
"version": "1.1.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-Jh0llaK2LRXQoYsorIH8maClebsnzTcve+7U3rQUSnC11X4jtPnFuyatqFLvMxZ8MLG8dB4zfHsbPfuvxluONw==", "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="
"dev": true
}, },
"node_modules/@iconify/utils": { "node_modules/@iconify/utils": {
"version": "1.0.33", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-1.0.33.tgz", "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.1.5.tgz",
"integrity": "sha512-vGeAqo7aGPxOQmGdVoXFUOuyN+0V7Lcrx2EvaiRjxUD1x6Om0Tvq2bdm7E24l2Pz++4S0mWMCVFXe/17EtKImQ==", "integrity": "sha512-6MvDI+I6QMvXn5rK9KQGdpEE4mmLTcuQdLZEiX5N+uZB+vc4Yw9K1OtnOgkl8mp4d9X0UrILREyZgF1NUwUt+Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@antfu/install-pkg": "^0.1.0", "@antfu/install-pkg": "^0.1.1",
"@antfu/utils": "^0.5.0", "@antfu/utils": "^0.7.2",
"@iconify/types": "^1.1.0", "@iconify/types": "^2.0.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"kolorist": "^1.5.1", "kolorist": "^1.7.0",
"local-pkg": "^0.4.1" "local-pkg": "^0.4.3"
} }
}, },
"node_modules/@iconify/vue": { "node_modules/@iconify/vue": {
@ -1268,11 +1266,6 @@
"vue": ">=3" "vue": ">=3"
} }
}, },
"node_modules/@iconify/vue/node_modules/@iconify/types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="
},
"node_modules/@intlify/bundle-utils": { "node_modules/@intlify/bundle-utils": {
"version": "2.2.2", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-2.2.2.tgz", "resolved": "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-2.2.2.tgz",
@ -3522,42 +3515,6 @@
"vue": "^2.0.0" "vue": "^2.0.0"
} }
}, },
"node_modules/@types/vue-barcode-reader/node_modules/@vue/compiler-sfc": {
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz",
"integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.18.4",
"postcss": "^8.4.14",
"source-map": "^0.6.1"
}
},
"node_modules/@types/vue-barcode-reader/node_modules/csstype": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==",
"dev": true
},
"node_modules/@types/vue-barcode-reader/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@types/vue-barcode-reader/node_modules/vue": {
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz",
"integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==",
"dev": true,
"dependencies": {
"@vue/compiler-sfc": "2.7.14",
"csstype": "^3.1.0"
}
},
"node_modules/@types/web-bluetooth": { "node_modules/@types/web-bluetooth": {
"version": "0.0.15", "version": "0.0.15",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz",
@ -4453,13 +4410,13 @@
} }
}, },
"node_modules/@windicss/config": { "node_modules/@windicss/config": {
"version": "1.8.7", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@windicss/config/-/config-1.8.7.tgz", "resolved": "https://registry.npmjs.org/@windicss/config/-/config-1.9.0.tgz",
"integrity": "sha512-8n+/Y36j5L3rw2tgMdLjeGRuNV7VYfKoHoraLK6Bk9OJ1MTPd5vv7pekof/uOPWVV7WWjVeZ6CTO8SDbDDW3iw==", "integrity": "sha512-QO4+udbmDIgZwAi89tqUt5nGwBq3IgyELjLn83twZXiIqzOw+77ecCuM0oPSbzWmIbCqXq3wRQHd6Z1u5E/5zQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"debug": "^4.3.4", "debug": "^4.3.4",
"jiti": "^1.14.0", "jiti": "^1.18.2",
"windicss": "^3.5.6" "windicss": "^3.5.6"
}, },
"funding": { "funding": {
@ -4485,16 +4442,16 @@
"dev": true "dev": true
}, },
"node_modules/@windicss/plugin-utils": { "node_modules/@windicss/plugin-utils": {
"version": "1.8.7", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-1.8.7.tgz", "resolved": "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-1.9.0.tgz",
"integrity": "sha512-dfj95olNZyGFDPFMBvE5oq8hA5f0ooUJZjVdWlthS4ek4W1/xNOHDxB6ygWR8LE9zCOXZykApjt1LOhy9Ky2QA==", "integrity": "sha512-omAacM5ExIr9XBUI2z47CyCXJBke4imJZqXW41YgHhRLbahTngbScFk5yxa6dXivDXUpUKqasOPXBJgA4bhHCg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@antfu/utils": "^0.5.2", "@antfu/utils": "^0.7.2",
"@windicss/config": "1.8.7", "@windicss/config": "1.9.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.2.11", "fast-glob": "^3.2.12",
"magic-string": "^0.26.2", "magic-string": "^0.30.0",
"micromatch": "^4.0.5", "micromatch": "^4.0.5",
"windicss": "^3.5.6" "windicss": "^3.5.6"
}, },
@ -4502,6 +4459,18 @@
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
} }
}, },
"node_modules/@windicss/plugin-utils/node_modules/magic-string": {
"version": "0.30.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz",
"integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.13"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@xtuc/ieee754": { "node_modules/@xtuc/ieee754": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@ -4555,32 +4524,6 @@
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
} }
}, },
"node_modules/@zhead/schema-vue/node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"dev": true,
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@zxing/library": { "node_modules/@zxing/library": {
"version": "0.19.2", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.19.2.tgz", "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.19.2.tgz",
@ -8776,6 +8719,7 @@
"version": "1.15.1", "version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"devOptional": true,
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -10644,9 +10588,9 @@
} }
}, },
"node_modules/jiti": { "node_modules/jiti": {
"version": "1.17.2", "version": "1.18.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.17.2.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
"integrity": "sha512-Xf0nU8+8wuiQpLcqdb2HRyHqYwGk2Pd+F7kstyp20ZuqTyCmB9dqpX2NxaxFc1kovraa2bG6c1RL3W7XfapiZg==", "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==",
"bin": { "bin": {
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
@ -10932,9 +10876,9 @@
"dev": true "dev": true
}, },
"node_modules/kolorist": { "node_modules/kolorist": {
"version": "1.5.1", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.5.1.tgz", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
"integrity": "sha512-lxpCM3HTvquGxKGzHeknB/sUjuVoUElLlfYnXZT73K8geR9jQbroGlSCFBax9/0mpGoD3kzcMLnOlGQPJJNyqQ==", "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
"dev": true "dev": true
}, },
"node_modules/langs": { "node_modules/langs": {
@ -12294,21 +12238,8 @@
} }
}, },
"node_modules/nocodb-sdk": { "node_modules/nocodb-sdk": {
"version": "0.108.1", "resolved": "../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.108.1.tgz", "link": true
"integrity": "sha512-XJZjDNNEBSJV9+zldpRXX5x2iMO9pvrnWlbphMQ5GlmtHdcLQXjSr6EP0B3QxCFQzdwYyEpcmEY+/qJGtEyRjA==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
}
},
"node_modules/nocodb-sdk/node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dependencies": {
"follow-redirects": "^1.14.0"
}
}, },
"node_modules/node-abi": { "node_modules/node-abi": {
"version": "3.23.0", "version": "3.23.0",
@ -16366,18 +16297,18 @@
} }
}, },
"node_modules/unplugin-icons": { "node_modules/unplugin-icons": {
"version": "0.14.7", "version": "0.14.15",
"resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.14.7.tgz", "resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.14.15.tgz",
"integrity": "sha512-TrNnEdpaXMdiG5BsCgvU6cv/gSLYvIk1f8wGCGZmOo4wmi3nqYBuqIEuiXhmmyXdDZuRRpCaOzCnCYYZ5H7U8g==", "integrity": "sha512-J6YBA+fUzVM2IZPXCK3Pnk36jYVwQ6lkjRgOnZaXNIxpMDsmwDqrE1AGJ0zUbfuEoOa90OBGc0OPfN1r+qlSIQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@antfu/install-pkg": "^0.1.0", "@antfu/install-pkg": "^0.1.1",
"@antfu/utils": "^0.5.2", "@antfu/utils": "^0.7.2",
"@iconify/utils": "^1.0.33", "@iconify/utils": "^2.0.3",
"debug": "^4.3.4", "debug": "^4.3.4",
"kolorist": "^1.5.1", "kolorist": "^1.6.0",
"local-pkg": "^0.4.1", "local-pkg": "^0.4.2",
"unplugin": "^0.7.0" "unplugin": "^1.0.1"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
@ -16404,53 +16335,39 @@
} }
}, },
"node_modules/unplugin-icons/node_modules/unplugin": { "node_modules/unplugin-icons/node_modules/unplugin": {
"version": "0.7.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-0.7.1.tgz", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz",
"integrity": "sha512-Z6hNDXDNh9aimMkPU1mEjtk+2ova8gh0y7rJeJdGH1vWZOHwF2lLQiQ/R97rv9ymmzEQXsR2fyMet72T8jy6ew==", "integrity": "sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"acorn": "^8.7.1", "acorn": "^8.8.2",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"webpack-sources": "^3.2.3", "webpack-sources": "^3.2.3",
"webpack-virtual-modules": "^0.4.4" "webpack-virtual-modules": "^0.5.0"
},
"peerDependencies": {
"esbuild": ">=0.13",
"rollup": "^2.50.0",
"vite": "^2.3.0 || ^3.0.0-0",
"webpack": "4 || 5"
},
"peerDependenciesMeta": {
"esbuild": {
"optional": true
},
"rollup": {
"optional": true
},
"vite": {
"optional": true
},
"webpack": {
"optional": true
}
} }
}, },
"node_modules/unplugin-icons/node_modules/webpack-virtual-modules": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
"integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
"dev": true
},
"node_modules/unplugin-vue-components": { "node_modules/unplugin-vue-components": {
"version": "0.22.4", "version": "0.22.12",
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.22.4.tgz", "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.22.12.tgz",
"integrity": "sha512-2rRZcM9OnJGXnYxQNfaceEYuPeVACcWySIjy8WBwIiN3onr980TmA3XE5pRJFt8zoQrUA+c46oyIq96noLqrEQ==", "integrity": "sha512-FxyzsuBvMCYPIk+8cgscGBQ345tvwVu+qY5IhE++eorkyvA4Z1TiD/HCiim+Kbqozl10i4K+z+NCa2WO2jexRA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@antfu/utils": "^0.5.2", "@antfu/utils": "^0.7.2",
"@rollup/pluginutils": "^4.2.1", "@rollup/pluginutils": "^5.0.2",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.2.11", "fast-glob": "^3.2.12",
"local-pkg": "^0.4.2", "local-pkg": "^0.4.2",
"magic-string": "^0.26.2", "magic-string": "^0.27.0",
"minimatch": "^5.1.0", "minimatch": "^5.1.1",
"resolve": "^1.22.1", "resolve": "^1.22.1",
"unplugin": "^0.9.0" "unplugin": "^1.0.1"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=14"
@ -16468,6 +16385,34 @@
} }
} }
}, },
"node_modules/unplugin-vue-components/node_modules/@rollup/pluginutils": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
"integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/unplugin-vue-components/node_modules/@types/estree": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
"dev": true
},
"node_modules/unplugin-vue-components/node_modules/brace-expansion": { "node_modules/unplugin-vue-components/node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -16477,10 +16422,22 @@
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
} }
}, },
"node_modules/unplugin-vue-components/node_modules/magic-string": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
"integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.13"
},
"engines": {
"node": ">=12"
}
},
"node_modules/unplugin-vue-components/node_modules/minimatch": { "node_modules/unplugin-vue-components/node_modules/minimatch": {
"version": "5.1.0", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.1"
@ -16490,37 +16447,23 @@
} }
}, },
"node_modules/unplugin-vue-components/node_modules/unplugin": { "node_modules/unplugin-vue-components/node_modules/unplugin": {
"version": "0.9.3", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-0.9.3.tgz", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz",
"integrity": "sha512-GWXxizZG+tobNs8fuGTCeilerkkfZTZax2iivuE4pxLaF9wTnPJHOq8tbLKDb5ohVb+2BXNjrU9xx59yWTUnuw==", "integrity": "sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"acorn": "^8.8.0", "acorn": "^8.8.2",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"webpack-sources": "^3.2.3", "webpack-sources": "^3.2.3",
"webpack-virtual-modules": "^0.4.4" "webpack-virtual-modules": "^0.5.0"
},
"peerDependencies": {
"esbuild": ">=0.13",
"rollup": "^2.50.0",
"vite": "^2.3.0 || ^3.0.0-0",
"webpack": "4 || 5"
},
"peerDependenciesMeta": {
"esbuild": {
"optional": true
},
"rollup": {
"optional": true
},
"vite": {
"optional": true
},
"webpack": {
"optional": true
}
} }
}, },
"node_modules/unplugin-vue-components/node_modules/webpack-virtual-modules": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
"integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
"dev": true
},
"node_modules/unstorage": { "node_modules/unstorage": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/unstorage/-/unstorage-0.6.0.tgz", "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-0.6.0.tgz",
@ -16669,9 +16612,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "2.9.15", "version": "2.9.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.9.15.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.16.tgz",
"integrity": "sha512-fzMt2jK4vQ3yK56te3Kqpkaeq9DkcZfBbzHwYpobasvgYmP2SoAr6Aic05CsB4CzCZbsDv4sujX3pkEGhLabVQ==", "integrity": "sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.14.27", "esbuild": "^0.14.27",
@ -17090,9 +17033,9 @@
"dev": true "dev": true
}, },
"node_modules/vite-node/node_modules/vite": { "node_modules/vite-node/node_modules/vite": {
"version": "3.2.2", "version": "3.2.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz",
"integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==", "integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.15.9", "esbuild": "^0.15.9",
@ -17110,6 +17053,7 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": ">= 14",
"less": "*", "less": "*",
"sass": "*", "sass": "*",
"stylus": "*", "stylus": "*",
@ -17117,6 +17061,9 @@
"terser": "^5.4.0" "terser": "^5.4.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": { "less": {
"optional": true "optional": true
}, },
@ -17260,21 +17207,21 @@
} }
}, },
"node_modules/vite-plugin-windicss": { "node_modules/vite-plugin-windicss": {
"version": "1.8.7", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-1.8.7.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-1.9.0.tgz",
"integrity": "sha512-/zwQ8+RV+MSkbG0IGqsEma6r2R01NzN/aNpNjJD7VVAkxAptNznqDXOObFTskkWfZ+9m6KJZCOuCPgAFtQIzEA==", "integrity": "sha512-w0unPfcbVU5eaISAsFTLgIb41SLhmXoUF75Othu8NqFioe8+DEqiuvJ7/k/LRuEuvI8Rt/OKrY6cNzrB+dykaA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@windicss/plugin-utils": "1.8.7", "@windicss/plugin-utils": "1.9.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"kolorist": "^1.5.1", "kolorist": "^1.8.0",
"windicss": "^3.5.6" "windicss": "^3.5.6"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
}, },
"peerDependencies": { "peerDependencies": {
"vite": "^2.0.1 || ^3.0.0" "vite": "^2.0.1 || ^3.0.0 || ^4.0.0"
} }
}, },
"node_modules/vite/node_modules/rollup": { "node_modules/vite/node_modules/rollup": {
@ -18465,9 +18412,9 @@
} }
}, },
"@antfu/install-pkg": { "@antfu/install-pkg": {
"version": "0.1.0", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.1.0.tgz", "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.1.1.tgz",
"integrity": "sha512-VaIJd3d1o7irZfK1U0nvBsHMyjkuyMP3HKYVV53z8DKyulkHKmjhhtccXO51WSPeeSHIeoJEoNOKavYpS7jkZw==", "integrity": "sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"execa": "^5.1.1", "execa": "^5.1.1",
@ -18520,9 +18467,9 @@
} }
}, },
"@antfu/utils": { "@antfu/utils": {
"version": "0.5.2", "version": "0.7.4",
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.5.2.tgz", "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.4.tgz",
"integrity": "sha512-CQkeV+oJxUazwjlHD0/3ZD08QWKuGQkhnrKo3e6ly5pd48VUpXbb77q0xMU4+vc2CkJnDS02Eq/M9ugyX20XZA==", "integrity": "sha512-qe8Nmh9rYI/HIspLSTwtbMFPj6dISG6+dJnOguTlPNXtCvS2uezdxscVBb7/3DrmNbQK49TDqpkSQ1chbRGdpQ==",
"dev": true "dev": true
}, },
"@babel/code-frame": { "@babel/code-frame": {
@ -19214,23 +19161,22 @@
"dev": true "dev": true
}, },
"@iconify/types": { "@iconify/types": {
"version": "1.1.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-Jh0llaK2LRXQoYsorIH8maClebsnzTcve+7U3rQUSnC11X4jtPnFuyatqFLvMxZ8MLG8dB4zfHsbPfuvxluONw==", "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="
"dev": true
}, },
"@iconify/utils": { "@iconify/utils": {
"version": "1.0.33", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-1.0.33.tgz", "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.1.5.tgz",
"integrity": "sha512-vGeAqo7aGPxOQmGdVoXFUOuyN+0V7Lcrx2EvaiRjxUD1x6Om0Tvq2bdm7E24l2Pz++4S0mWMCVFXe/17EtKImQ==", "integrity": "sha512-6MvDI+I6QMvXn5rK9KQGdpEE4mmLTcuQdLZEiX5N+uZB+vc4Yw9K1OtnOgkl8mp4d9X0UrILREyZgF1NUwUt+Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"@antfu/install-pkg": "^0.1.0", "@antfu/install-pkg": "^0.1.1",
"@antfu/utils": "^0.5.0", "@antfu/utils": "^0.7.2",
"@iconify/types": "^1.1.0", "@iconify/types": "^2.0.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"kolorist": "^1.5.1", "kolorist": "^1.7.0",
"local-pkg": "^0.4.1" "local-pkg": "^0.4.3"
} }
}, },
"@iconify/vue": { "@iconify/vue": {
@ -19239,13 +19185,6 @@
"integrity": "sha512-k4VwcSQpGqJpoyqENRRviFuXlVcquLvQ6BKLNJ6o2amZo7u+3HyALSO79Xyz7Sg68szQGstOk6weaKUF0DJbog==", "integrity": "sha512-k4VwcSQpGqJpoyqENRRviFuXlVcquLvQ6BKLNJ6o2amZo7u+3HyALSO79Xyz7Sg68szQGstOk6weaKUF0DJbog==",
"requires": { "requires": {
"@iconify/types": "^2.0.0" "@iconify/types": "^2.0.0"
},
"dependencies": {
"@iconify/types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="
}
} }
}, },
"@intlify/bundle-utils": { "@intlify/bundle-utils": {
@ -20931,41 +20870,6 @@
"dev": true, "dev": true,
"requires": { "requires": {
"vue": "latest" "vue": "latest"
},
"dependencies": {
"@vue/compiler-sfc": {
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz",
"integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==",
"dev": true,
"requires": {
"@babel/parser": "^7.18.4",
"postcss": "^8.4.14",
"source-map": "^0.6.1"
}
},
"csstype": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"vue": {
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz",
"integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==",
"dev": true,
"requires": {
"@vue/compiler-sfc": "2.7.14",
"csstype": "^3.1.0"
}
}
} }
}, },
"@types/web-bluetooth": { "@types/web-bluetooth": {
@ -21683,13 +21587,13 @@
} }
}, },
"@windicss/config": { "@windicss/config": {
"version": "1.8.7", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@windicss/config/-/config-1.8.7.tgz", "resolved": "https://registry.npmjs.org/@windicss/config/-/config-1.9.0.tgz",
"integrity": "sha512-8n+/Y36j5L3rw2tgMdLjeGRuNV7VYfKoHoraLK6Bk9OJ1MTPd5vv7pekof/uOPWVV7WWjVeZ6CTO8SDbDDW3iw==", "integrity": "sha512-QO4+udbmDIgZwAi89tqUt5nGwBq3IgyELjLn83twZXiIqzOw+77ecCuM0oPSbzWmIbCqXq3wRQHd6Z1u5E/5zQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"debug": "^4.3.4", "debug": "^4.3.4",
"jiti": "^1.14.0", "jiti": "^1.18.2",
"windicss": "^3.5.6" "windicss": "^3.5.6"
} }
}, },
@ -21712,18 +21616,29 @@
"dev": true "dev": true
}, },
"@windicss/plugin-utils": { "@windicss/plugin-utils": {
"version": "1.8.7", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-1.8.7.tgz", "resolved": "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-1.9.0.tgz",
"integrity": "sha512-dfj95olNZyGFDPFMBvE5oq8hA5f0ooUJZjVdWlthS4ek4W1/xNOHDxB6ygWR8LE9zCOXZykApjt1LOhy9Ky2QA==", "integrity": "sha512-omAacM5ExIr9XBUI2z47CyCXJBke4imJZqXW41YgHhRLbahTngbScFk5yxa6dXivDXUpUKqasOPXBJgA4bhHCg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@antfu/utils": "^0.5.2", "@antfu/utils": "^0.7.2",
"@windicss/config": "1.8.7", "@windicss/config": "1.9.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.2.11", "fast-glob": "^3.2.12",
"magic-string": "^0.26.2", "magic-string": "^0.30.0",
"micromatch": "^4.0.5", "micromatch": "^4.0.5",
"windicss": "^3.5.6" "windicss": "^3.5.6"
},
"dependencies": {
"magic-string": {
"version": "0.30.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz",
"integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==",
"dev": true,
"requires": {
"@jridgewell/sourcemap-codec": "^1.4.13"
}
}
} }
}, },
"@xtuc/ieee754": { "@xtuc/ieee754": {
@ -21765,15 +21680,6 @@
"dev": true, "dev": true,
"requires": { "requires": {
"vue-demi": "*" "vue-demi": "*"
},
"dependencies": {
"vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"dev": true,
"requires": {}
}
} }
} }
} }
@ -24810,7 +24716,8 @@
"follow-redirects": { "follow-redirects": {
"version": "1.15.1", "version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"devOptional": true
}, },
"form-data": { "form-data": {
"version": "4.0.0", "version": "4.0.0",
@ -26175,9 +26082,9 @@
} }
}, },
"jiti": { "jiti": {
"version": "1.17.2", "version": "1.18.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.17.2.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
"integrity": "sha512-Xf0nU8+8wuiQpLcqdb2HRyHqYwGk2Pd+F7kstyp20ZuqTyCmB9dqpX2NxaxFc1kovraa2bG6c1RL3W7XfapiZg==" "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg=="
}, },
"js-tokens": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
@ -26355,9 +26262,9 @@
"dev": true "dev": true
}, },
"kolorist": { "kolorist": {
"version": "1.5.1", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.5.1.tgz", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
"integrity": "sha512-lxpCM3HTvquGxKGzHeknB/sUjuVoUElLlfYnXZT73K8geR9jQbroGlSCFBax9/0mpGoD3kzcMLnOlGQPJJNyqQ==", "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
"dev": true "dev": true
}, },
"langs": { "langs": {
@ -27360,22 +27267,22 @@
} }
}, },
"nocodb-sdk": { "nocodb-sdk": {
"version": "0.108.1", "version": "file:../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.108.1.tgz",
"integrity": "sha512-XJZjDNNEBSJV9+zldpRXX5x2iMO9pvrnWlbphMQ5GlmtHdcLQXjSr6EP0B3QxCFQzdwYyEpcmEY+/qJGtEyRjA==",
"requires": { "requires": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.1", "axios": "^0.21.1",
"jsep": "^1.3.6" "cspell": "^4.1.0",
}, "eslint": "^7.8.0",
"dependencies": { "eslint-config-prettier": "^6.11.0",
"axios": { "eslint-plugin-eslint-comments": "^3.2.0",
"version": "0.21.4", "eslint-plugin-functional": "^3.0.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", "eslint-plugin-import": "^2.22.0",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "eslint-plugin-prettier": "^4.0.0",
"requires": { "jsep": "^1.3.6",
"follow-redirects": "^1.14.0" "npm-run-all": "^4.1.5",
} "prettier": "^2.1.1",
} "typescript": "^4.0.2"
} }
}, },
"node-abi": { "node-abi": {
@ -30407,52 +30314,75 @@
} }
}, },
"unplugin-icons": { "unplugin-icons": {
"version": "0.14.7", "version": "0.14.15",
"resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.14.7.tgz", "resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.14.15.tgz",
"integrity": "sha512-TrNnEdpaXMdiG5BsCgvU6cv/gSLYvIk1f8wGCGZmOo4wmi3nqYBuqIEuiXhmmyXdDZuRRpCaOzCnCYYZ5H7U8g==", "integrity": "sha512-J6YBA+fUzVM2IZPXCK3Pnk36jYVwQ6lkjRgOnZaXNIxpMDsmwDqrE1AGJ0zUbfuEoOa90OBGc0OPfN1r+qlSIQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@antfu/install-pkg": "^0.1.0", "@antfu/install-pkg": "^0.1.1",
"@antfu/utils": "^0.5.2", "@antfu/utils": "^0.7.2",
"@iconify/utils": "^1.0.33", "@iconify/utils": "^2.0.3",
"debug": "^4.3.4", "debug": "^4.3.4",
"kolorist": "^1.5.1", "kolorist": "^1.6.0",
"local-pkg": "^0.4.1", "local-pkg": "^0.4.2",
"unplugin": "^0.7.0" "unplugin": "^1.0.1"
}, },
"dependencies": { "dependencies": {
"unplugin": { "unplugin": {
"version": "0.7.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-0.7.1.tgz", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz",
"integrity": "sha512-Z6hNDXDNh9aimMkPU1mEjtk+2ova8gh0y7rJeJdGH1vWZOHwF2lLQiQ/R97rv9ymmzEQXsR2fyMet72T8jy6ew==", "integrity": "sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==",
"dev": true, "dev": true,
"requires": { "requires": {
"acorn": "^8.7.1", "acorn": "^8.8.2",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"webpack-sources": "^3.2.3", "webpack-sources": "^3.2.3",
"webpack-virtual-modules": "^0.4.4" "webpack-virtual-modules": "^0.5.0"
} }
},
"webpack-virtual-modules": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
"integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
"dev": true
} }
} }
}, },
"unplugin-vue-components": { "unplugin-vue-components": {
"version": "0.22.4", "version": "0.22.12",
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.22.4.tgz", "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.22.12.tgz",
"integrity": "sha512-2rRZcM9OnJGXnYxQNfaceEYuPeVACcWySIjy8WBwIiN3onr980TmA3XE5pRJFt8zoQrUA+c46oyIq96noLqrEQ==", "integrity": "sha512-FxyzsuBvMCYPIk+8cgscGBQ345tvwVu+qY5IhE++eorkyvA4Z1TiD/HCiim+Kbqozl10i4K+z+NCa2WO2jexRA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@antfu/utils": "^0.5.2", "@antfu/utils": "^0.7.2",
"@rollup/pluginutils": "^4.2.1", "@rollup/pluginutils": "^5.0.2",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.2.11", "fast-glob": "^3.2.12",
"local-pkg": "^0.4.2", "local-pkg": "^0.4.2",
"magic-string": "^0.26.2", "magic-string": "^0.27.0",
"minimatch": "^5.1.0", "minimatch": "^5.1.1",
"resolve": "^1.22.1", "resolve": "^1.22.1",
"unplugin": "^0.9.0" "unplugin": "^1.0.1"
}, },
"dependencies": { "dependencies": {
"@rollup/pluginutils": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
"integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
"dev": true,
"requires": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^2.3.1"
}
},
"@types/estree": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
"dev": true
},
"brace-expansion": { "brace-expansion": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -30462,26 +30392,41 @@
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
} }
}, },
"magic-string": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
"integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
"dev": true,
"requires": {
"@jridgewell/sourcemap-codec": "^1.4.13"
}
},
"minimatch": { "minimatch": {
"version": "5.1.0", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true, "dev": true,
"requires": { "requires": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.1"
} }
}, },
"unplugin": { "unplugin": {
"version": "0.9.3", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-0.9.3.tgz", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz",
"integrity": "sha512-GWXxizZG+tobNs8fuGTCeilerkkfZTZax2iivuE4pxLaF9wTnPJHOq8tbLKDb5ohVb+2BXNjrU9xx59yWTUnuw==", "integrity": "sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==",
"dev": true, "dev": true,
"requires": { "requires": {
"acorn": "^8.8.0", "acorn": "^8.8.2",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"webpack-sources": "^3.2.3", "webpack-sources": "^3.2.3",
"webpack-virtual-modules": "^0.4.4" "webpack-virtual-modules": "^0.5.0"
} }
},
"webpack-virtual-modules": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
"integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
"dev": true
} }
} }
}, },
@ -30615,9 +30560,9 @@
"integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==" "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw=="
}, },
"vite": { "vite": {
"version": "2.9.15", "version": "2.9.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.9.15.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.16.tgz",
"integrity": "sha512-fzMt2jK4vQ3yK56te3Kqpkaeq9DkcZfBbzHwYpobasvgYmP2SoAr6Aic05CsB4CzCZbsDv4sujX3pkEGhLabVQ==", "integrity": "sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==",
"dev": true, "dev": true,
"requires": { "requires": {
"esbuild": "^0.14.27", "esbuild": "^0.14.27",
@ -30827,9 +30772,9 @@
"dev": true "dev": true
}, },
"vite": { "vite": {
"version": "3.2.2", "version": "3.2.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz",
"integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==", "integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==",
"dev": true, "dev": true,
"requires": { "requires": {
"esbuild": "^0.15.9", "esbuild": "^0.15.9",
@ -30916,14 +30861,14 @@
} }
}, },
"vite-plugin-windicss": { "vite-plugin-windicss": {
"version": "1.8.7", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-1.8.7.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-1.9.0.tgz",
"integrity": "sha512-/zwQ8+RV+MSkbG0IGqsEma6r2R01NzN/aNpNjJD7VVAkxAptNznqDXOObFTskkWfZ+9m6KJZCOuCPgAFtQIzEA==", "integrity": "sha512-w0unPfcbVU5eaISAsFTLgIb41SLhmXoUF75Othu8NqFioe8+DEqiuvJ7/k/LRuEuvI8Rt/OKrY6cNzrB+dykaA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@windicss/plugin-utils": "1.8.7", "@windicss/plugin-utils": "1.9.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"kolorist": "^1.5.1", "kolorist": "^1.8.0",
"windicss": "^3.5.6" "windicss": "^3.5.6"
} }
}, },

6
packages/nc-gui/package.json

@ -54,7 +54,7 @@
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1", "locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"nocodb-sdk": "0.108.1", "nocodb-sdk": "file:../nocodb-sdk",
"papaparse": "^5.3.2", "papaparse": "^5.3.2",
"pinia": "^2.0.33", "pinia": "^2.0.33",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
@ -125,8 +125,8 @@
"nuxt-windicss": "^2.5.0", "nuxt-windicss": "^2.5.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"sass": "^1.53.0", "sass": "^1.53.0",
"unplugin-icons": "^0.14.7", "unplugin-icons": "^0.14.15",
"unplugin-vue-components": "^0.22.4", "unplugin-vue-components": "^0.22.12",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-purge-icons": "^0.9.0", "vite-plugin-purge-icons": "^0.9.0",
"vitest": "^0.18.0", "vitest": "^0.18.0",

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

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

17
packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue

@ -75,9 +75,18 @@ const onDecode = async (scannedCodeValue: string) => {
class="color-transition relative flex flex-col justify-center gap-2 w-full max-w-[max(33%,600px)] m-auto py-4 pb-8 px-16 md:(bg-white dark:bg-slate-700 rounded-lg border-1 border-gray-200 shadow-xl)" class="color-transition relative flex flex-col justify-center gap-2 w-full max-w-[max(33%,600px)] m-auto py-4 pb-8 px-16 md:(bg-white dark:bg-slate-700 rounded-lg border-1 border-gray-200 shadow-xl)"
> >
<template v-if="sharedFormView"> <template v-if="sharedFormView">
<h1 class="prose-2xl font-bold self-center my-4">{{ sharedFormView.heading }}</h1> <h1
class="prose-2xl font-bold self-center my-4"
<h2 v-if="sharedFormView.subheading" class="prose-lg text-slate-500 dark:text-slate-300 self-center mb-4 leading-6"> style="word-break: break-all"
>
{{ sharedFormView.heading }}
</h1>
<h2
v-if="sharedFormView.subheading"
class="prose-lg text-slate-500 dark:text-slate-300 self-center mb-4 leading-6"
style="word-break: break-all"
>
{{ sharedFormView.subheading }} {{ sharedFormView.subheading }}
</h2> </h2>
@ -180,7 +189,7 @@ const onDecode = async (scannedCodeValue: string) => {
</a-button> </a-button>
</LazySmartsheetDivDataCell> </LazySmartsheetDivDataCell>
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1"> <div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1" style="word-break: break-all">
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500"> <div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500">
{{ error.$message }} {{ error.$message }}
</div> </div>

14
packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue

@ -244,13 +244,17 @@ onMounted(() => {
class="max-w-[max(33%,600px)] mx-auto flex flex-col justify-end" class="max-w-[max(33%,600px)] mx-auto flex flex-col justify-end"
> >
<div class="px-4 md:px-0 flex flex-col justify-end"> <div class="px-4 md:px-0 flex flex-col justify-end">
<h1 class="prose-2xl font-bold self-center my-4" data-testid="nc-survey-form__heading"> <h1
class="prose-2xl font-bold self-center my-4"
data-testid="nc-survey-form__heading"
style="word-break: break-all">
{{ sharedFormView.heading }} {{ sharedFormView.heading }}
</h1> </h1>
<h2 <h2
v-if="sharedFormView.subheading && sharedFormView.subheading !== ''" v-if="sharedFormView.subheading && sharedFormView.subheading !== ''"
class="prose-lg text-slate-500 dark:text-slate-300 self-center mb-4 leading-6" class="prose-lg text-slate-500 dark:text-slate-300 self-center mb-4 leading-6"
style="word-break: break-all"
data-testid="nc-survey-form__sub-heading" data-testid="nc-survey-form__sub-heading"
> >
{{ sharedFormView?.subheading }} {{ sharedFormView?.subheading }}
@ -287,7 +291,7 @@ onMounted(() => {
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
v-if="isVirtualCol(field)" v-if="isVirtualCol(field)"
v-model="formState[field.title]" v-model="formState[field.title]"
class="mt-0 nc-input" class="mt-0 nc-input h-auto"
:row="{ row: {}, oldRow: {}, rowMeta: {} }" :row="{ row: {}, oldRow: {}, rowMeta: {} }"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`" :data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field" :column="field"
@ -296,7 +300,7 @@ onMounted(() => {
<LazySmartsheetCell <LazySmartsheetCell
v-else v-else
v-model="formState[field.title]" v-model="formState[field.title]"
class="nc-input" class="nc-input h-auto"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`" :data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field" :column="field"
:edit-enabled="editEnabled[index]" :edit-enabled="editEnabled[index]"
@ -305,11 +309,10 @@ onMounted(() => {
@update:edit-enabled="editEnabled[index] = $event" @update:edit-enabled="editEnabled[index] = $event"
/> />
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1"> <div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1" style="word-break: break-all">
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500"> <div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500">
{{ error.$message }} {{ error.$message }}
</div> </div>
<div <div
class="block text-[14px]" class="block text-[14px]"
:class="field.uidt === UITypes.Checkbox ? 'text-center' : ''" :class="field.uidt === UITypes.Checkbox ? 'text-center' : ''"
@ -350,6 +353,7 @@ onMounted(() => {
:mouse-enter-delay="0.25" :mouse-enter-delay="0.25"
:mouse-leave-delay="0" :mouse-leave-delay="0"
> >
<!-- Ok button for question -->
<button <button
class="bg-opacity-100 scaling-btn flex items-center gap-1" class="bg-opacity-100 scaling-btn flex items-center gap-1"
data-testid="nc-survey-form__btn-next" data-testid="nc-survey-form__btn-next"

6
packages/noco-docs/content/en/setup-and-usages/column-types.md

@ -54,7 +54,7 @@ menuTitle: 'Column Types'
### LinkToAnotherRecord ### LinkToAnotherRecord
For more about Link To Another Record, please visit [here](./link-to-another-record). For more about Link To Another Record, please visit <NuxtLink to="/setup-and-usages/link-to-another-record" target="_blank">here</NuxtLink>.
<!-- ### ForeignKey <!-- ### ForeignKey
#### Available Database Types #### Available Database Types
@ -261,7 +261,7 @@ For more about Link To Another Record, please visit [here](./link-to-another-rec
### Formula ### Formula
For more about formula, please visit [here](./formulas). For more about Formulas, please visit <NuxtLink to="/setup-and-usages/formulas" target="_blank">here</NuxtLink>.
### QR-Code ### QR-Code
@ -289,7 +289,7 @@ Since it's a virtual column, the cell content (Barcode) cannot be changed direct
### Rollup ### Rollup
For more about rollup, please visit [here](./rollup). For more about Rollup, please visit <NuxtLink to="/setup-and-usages/rollup" target="_blank">here</NuxtLink>.
### DateTime ### DateTime

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

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

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

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

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

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

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

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

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

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

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

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

30
packages/nocodb/package-lock.json generated

@ -83,7 +83,7 @@
"nc-lib-gui": "0.108.1", "nc-lib-gui": "0.108.1",
"nc-plugin": "^0.1.3", "nc-plugin": "^0.1.3",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nocodb-sdk": "0.108.1", "nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10", "nodemailer": "^6.4.10",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"os-locale": "^6.0.2", "os-locale": "^6.0.2",
@ -191,7 +191,6 @@
}, },
"../nocodb-sdk": { "../nocodb-sdk": {
"version": "0.108.1", "version": "0.108.1",
"extraneous": true,
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
@ -13207,13 +13206,8 @@
} }
}, },
"node_modules/nocodb-sdk": { "node_modules/nocodb-sdk": {
"version": "0.108.1", "resolved": "../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.108.1.tgz", "link": true
"integrity": "sha512-XJZjDNNEBSJV9+zldpRXX5x2iMO9pvrnWlbphMQ5GlmtHdcLQXjSr6EP0B3QxCFQzdwYyEpcmEY+/qJGtEyRjA==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
}
}, },
"node_modules/node-abort-controller": { "node_modules/node-abort-controller": {
"version": "3.1.1", "version": "3.1.1",
@ -28485,12 +28479,22 @@
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==" "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="
}, },
"nocodb-sdk": { "nocodb-sdk": {
"version": "0.108.1", "version": "file:../nocodb-sdk",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.108.1.tgz",
"integrity": "sha512-XJZjDNNEBSJV9+zldpRXX5x2iMO9pvrnWlbphMQ5GlmtHdcLQXjSr6EP0B3QxCFQzdwYyEpcmEY+/qJGtEyRjA==",
"requires": { "requires": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.1", "axios": "^0.21.1",
"jsep": "^1.3.6" "cspell": "^4.1.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"jsep": "^1.3.6",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"typescript": "^4.0.2"
} }
}, },
"node-abort-controller": { "node-abort-controller": {

4
packages/nocodb/package.json

@ -116,7 +116,7 @@
"nc-lib-gui": "0.108.1", "nc-lib-gui": "0.108.1",
"nc-plugin": "^0.1.3", "nc-plugin": "^0.1.3",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nocodb-sdk": "0.108.1", "nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10", "nodemailer": "^6.4.10",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"os-locale": "^6.0.2", "os-locale": "^6.0.2",
@ -203,4 +203,4 @@
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"testEnvironment": "node" "testEnvironment": "node"
} }
} }

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 { Acl } from '../middlewares/extract-project-id/extract-project-id.middleware';
import { CachesService } from '../services/caches.service'; import { CachesService } from '../services/caches.service';
@Controller() @Controller()
@UseGuards(GlobalGuard)
export class CachesController { export class CachesController {
constructor(private readonly cachesService: CachesService) {} constructor(private readonly cachesService: CachesService) {}

33
packages/nocodb/src/controllers/test/TestResetService/resetMetaSakilaSqliteProject.ts

@ -1,6 +1,7 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import axios from 'axios'; import axios from 'axios';
import type { AxiosResponse } from 'axios';
const sqliteFilePath = (parallelId: string) => { const sqliteFilePath = (parallelId: string) => {
const rootDir = process.cwd(); const rootDir = process.cwd();
@ -46,23 +47,37 @@ const resetMetaSakilaSqliteProject = async ({
if (!isEmptyProject) await seedSakilaSqliteFile(parallelId); if (!isEmptyProject) await seedSakilaSqliteFile(parallelId);
await createProject(token, title, parallelId); await createProject(token, title, parallelId, isEmptyProject);
}; };
const createProject = async ( const createProject = async (
token: string, token: string,
title: string, title: string,
parallelId: string, parallelId: string,
isEmptyProject: boolean,
) => { ) => {
const response = await axios.post( let response: AxiosResponse;
'http://localhost:8080/api/v1/db/meta/projects/', if (isEmptyProject) {
sakilaProjectConfig(title, parallelId), response = await axios.post(
{ 'http://localhost:8080/api/v1/db/meta/projects/',
headers: { { title },
'xc-auth': token, {
headers: {
'xc-auth': token,
},
}, },
}, );
); } else {
response = await axios.post(
'http://localhost:8080/api/v1/db/meta/projects/',
sakilaProjectConfig(title, parallelId),
{
headers: {
'xc-auth': token,
},
},
);
}
if (response.status !== 200) { if (response.status !== 200) {
console.error('Error creating project', response.data); console.error('Error creating project', response.data);
} }

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

@ -172,7 +172,7 @@ class BaseModelSqlv2 {
data.__proto__ = proto; data.__proto__ = proto;
} }
return data ? await nocoExecute(ast, data, {}, query) : {}; return data ? await nocoExecute(ast, data, {}, query) : null;
} }
public async exist(id?: any): Promise<any> { public async exist(id?: any): Promise<any> {

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

@ -570,13 +570,25 @@ export default class Model implements TableType {
// get existing cache // get existing cache
const key = `${CacheScope.MODEL}:${tableId}`; const key = `${CacheScope.MODEL}:${tableId}`;
const o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT); const o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
let oldModel = { ...o };
// update alias // update alias
if (o) { if (o) {
o.title = title; o.title = title;
o.table_name = table_name; o.table_name = table_name;
// set cache // set cache
await NocoCache.set(key, o); await NocoCache.set(key, o);
} else {
oldModel = await this.get(tableId);
} }
// delete alias cache
await NocoCache.del(
`${CacheScope.MODEL}:${oldModel.project_id}:${oldModel.base_id}:${oldModel.title}`,
);
await NocoCache.del(
`${CacheScope.MODEL}:${oldModel.project_id}:${oldModel.title}`,
);
// set meta // set meta
return await ncMeta.metaUpdate( return await ncMeta.metaUpdate(
null, null,

10
packages/nocodb/src/models/Project.ts

@ -8,6 +8,7 @@ import {
import { extractProps } from '../helpers/extractProps'; import { extractProps } from '../helpers/extractProps';
import NocoCache from '../cache/NocoCache'; import NocoCache from '../cache/NocoCache';
import Base from './/Base'; import Base from './/Base';
import { ProjectUser } from './index';
import type { BoolType, MetaType, ProjectType } from 'nocodb-sdk'; import type { BoolType, MetaType, ProjectType } from 'nocodb-sdk';
import type { DB_TYPES } from './/Base'; import type { DB_TYPES } from './/Base';
@ -276,6 +277,15 @@ export default class Project implements ProjectType {
// Todo: Remove the project entry from the connection pool in NcConnectionMgrv2 // Todo: Remove the project entry from the connection pool in NcConnectionMgrv2
static async delete(projectId, ncMeta = Noco.ncMeta): Promise<any> { static async delete(projectId, ncMeta = Noco.ncMeta): Promise<any> {
const users = await ProjectUser.getUsersList({
project_id: projectId,
offset: 0,
limit: 1000,
});
for (const user of users) {
await ProjectUser.delete(projectId, user.id);
}
const bases = await Base.list({ projectId }); const bases = await Base.list({ projectId });
for (const base of bases) { for (const base of bases) {
await base.delete(ncMeta); await base.delete(ncMeta);

29
packages/nocodb/src/models/View.ts

@ -1,3 +1,4 @@
import { title } from 'process';
import { isSystemColumn, UITypes, ViewTypes } from 'nocodb-sdk'; import { isSystemColumn, UITypes, ViewTypes } from 'nocodb-sdk';
import Noco from '../Noco'; import Noco from '../Noco';
import { import {
@ -165,13 +166,19 @@ export default class View implements ViewType {
], ],
}, },
); );
view.meta = parseMetaProp(view);
// todo: cache - titleOrId can be viewId so we need a different scope here if (view) {
await NocoCache.set( await NocoCache.set(
`${CacheScope.VIEW}:${fk_model_id}:${titleOrId}`, `${CacheScope.VIEW}:${fk_model_id}:${view.id}`,
view.id, view,
); );
await NocoCache.set(`${CacheScope.VIEW}:${fk_model_id}:${view.id}`, view); view.meta = parseMetaProp(view);
// todo: cache - titleOrId can be viewId so we need a different scope here
await NocoCache.set(
`${CacheScope.VIEW}:${fk_model_id}:${titleOrId}`,
view.id,
);
}
return view && new View(view); return view && new View(view);
} }
return viewId && this.get(viewId?.id || viewId); return viewId && this.get(viewId?.id || viewId);
@ -952,6 +959,7 @@ export default class View implements ViewType {
// get existing cache // get existing cache
const key = `${CacheScope.VIEW}:${viewId}`; const key = `${CacheScope.VIEW}:${viewId}`;
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT); let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
let oldView = { ...o };
if (o) { if (o) {
// update data // update data
o = { o = {
@ -963,8 +971,15 @@ export default class View implements ViewType {
} }
// set cache // set cache
await NocoCache.set(key, o); await NocoCache.set(key, o);
} else {
oldView = await this.get(viewId);
} }
// reset alias cache
await NocoCache.del(
`${CacheScope.VIEW}:${oldView.fk_model_id}:${oldView.title}`,
);
// if meta data defined then stringify it // if meta data defined then stringify it
if ('meta' in updateObj) { if ('meta' in updateObj) {
updateObj.meta = stringifyMetaProp(updateObj); updateObj.meta = stringifyMetaProp(updateObj);

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

@ -43,6 +43,7 @@ export async function getViewAndModelByAliasOrId(param: {
fk_model_id: model.id, fk_model_id: model.id,
})); }));
if (!model) NcError.notFound('Table not found'); if (!model) NcError.notFound('Table not found');
if (param.viewName && !view) NcError.notFound('View not found');
return { model, view }; return { model, view };
} }

5
packages/nocodb/src/services/users/users.service.ts

@ -32,7 +32,8 @@ import type {
export class UsersService { export class UsersService {
constructor(private metaService: MetaService) {} constructor(private metaService: MetaService) {}
async findOne(email: string) { async findOne(_email: string) {
const email = _email.toLowerCase();
const user = await this.metaService.metaGet(null, null, MetaTable.USERS, { const user = await this.metaService.metaGet(null, null, MetaTable.USERS, {
email, email,
}); });
@ -50,7 +51,7 @@ export class UsersService {
email: string; email: string;
lastname: any; lastname: any;
}) { }) {
return this.metaService.metaInsert2(null, null, MetaTable.USERS, param); return this.metaService.metaInsert2(null, null, MetaTable.USERS, { ...param, email: param.email?.toLowerCase() });
} }
async registerNewUserIfAllowed({ async registerNewUserIfAllowed({

2
packages/nocodb/tests/unit/model/tests/baseModelSql.test.ts

@ -282,7 +282,7 @@ function baseModelSqlTests() {
const deletedRow = await baseModelSql.readByPk(rowIdToDeleted); const deletedRow = await baseModelSql.readByPk(rowIdToDeleted);
expect(deletedRow).to.be.an('object').that.is.empty; expect(deletedRow).to.be.null;
console.log('Delete record', await Audit.projectAuditList(project.id, {})); console.log('Delete record', await Audit.projectAuditList(project.id, {}));
const rowDeletedAudit = (await Audit.projectAuditList(project.id, {})).find( const rowDeletedAudit = (await Audit.projectAuditList(project.id, {})).find(

2
packages/nocodb/tests/unit/rest/tests/viewRow.test.ts

@ -780,7 +780,7 @@ function viewRowTests() {
.send({ .send({
title: 'Test', title: 'Test',
}) })
.expect(400); .expect(404);
}; };
it('Create table row grid wrong grid id', async function () { it('Create table row grid wrong grid id', async function () {

70
renovate.json

@ -0,0 +1,70 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
":dependencyDashboard",
":onlyNpm",
":prConcurrentLimit20",
":autodetectPinVersions",
":label(renovate)",
":rebaseStalePrs",
":semanticPrefixFixDepsChoreOthers",
":separatePatchReleases",
"group:monorepos",
"group:recommended"
],
"baseBranches": [
"develop"
],
"vulnerabilityAlerts": {
"commitMessagePrefix": "chore(renovate): Security update"
},
"schedule": "at any time",
"rangeStrategy": "bump",
"packageRules": [
{
"enabled": false,
"matchPackagePatterns": ["*"],
"matchUpdateTypes": ["major"],
"reviewersFromCodeOwners": true,
"commitMessagePrefix": "chore(renovate):",
"groupName": "major"
},
{
"enabled": false,
"matchPackagePatterns": ["*"],
"matchUpdateTypes": ["minor"],
"commitMessagePrefix": "chore(renovate):",
"groupName": "minor"
},
{
"enabled": false,
"matchPackagePatterns": ["*"],
"matchUpdateTypes": ["patch"],
"commitMessagePrefix": "chore(renovate):",
"groupName": "patch"
},
{
"enabled": false,
"matchPackagePatterns": ["*"],
"matchUpdateTypes": ["pin", "digest"],
"commitMessagePrefix": "chore(renovate):",
"groupName": "pin"
}
],
"ignorePaths": [
"**/node_modules/**",
"**/nc-cli/**",
"**/nc-lib-gui/**",
"**/nc-plugin/**",
"**/nocodb-legacy/**",
"**/test/**",
"**/tests/**",
"**/workflows/**",
"**/charts/**"
],
"assignees": [
"wingkwong"
],
"enabled": false
}

42
scripts/installLocalSdk.js

@ -0,0 +1,42 @@
const { exec } = require('child_process');
const path = require('path');
const sdkPath = path.join(__dirname, '..', 'packages', 'nocodb-sdk');
const guiPath = path.join(__dirname, '..', 'packages', 'nc-gui');
const nocodbPath = path.join(__dirname, '..', 'packages', 'nocodb');
exec(`cd ${sdkPath} && npm i && npm run build`, (err, stdout, stderr) => {
if (err) {
console.error(`Error installing dependencies and building nocodb-sdk: ${err}`);
return;
}
console.log(`Dependencies installed and nocodb-sdk built: ${stdout}`);
const guiPromise = new Promise((resolve, reject) => {
exec(`cd ${guiPath} && npm i ${sdkPath}`, (err, stdout, stderr) => {
if (err) {
reject(`Error installing dependencies for nc-gui: ${err}`);
} else {
resolve(`Dependencies installed for nc-gui: ${stdout}`);
}
});
});
const nocodbPromise = new Promise((resolve, reject) => {
exec(`cd ${nocodbPath} && npm i ${sdkPath}`, (err, stdout, stderr) => {
if (err) {
reject(`Error installing dependencies for nocodb: ${err}`);
} else {
resolve(`Dependencies installed for nocodb: ${stdout}`);
}
});
});
Promise.all([guiPromise, nocodbPromise])
.then((results) => {
console.log(results.join('\n'));
})
.catch((err) => {
console.error(err);
});
});

5
scripts/pkg-executable/index.js

@ -3,9 +3,10 @@ process.env.NC_BINARY_BUILD = 'true';
try { try {
const app = require('express')(); const app = require('express')();
const {Noco} = require("nocodb"); const {Noco} = require("nocodb");
const httpServer = app.listen(process.env.PORT || 8080); const port = process.env.PORT || 8080;
const httpServer = app.listen(port);
app.use(await Noco.init({}, httpServer, app)); app.use(await Noco.init({}, httpServer, app));
console.log(`Visit : localhost:${process.env.PORT}/dashboard`) console.log(`Visit : localhost:${port}/dashboard`)
} catch(e) { } catch(e) {
console.log(e) console.log(e)
} }

4
tests/playwright/package-lock.json generated

@ -39,7 +39,7 @@
} }
}, },
"../../packages/nocodb-sdk": { "../../packages/nocodb-sdk": {
"version": "0.107.0-beta.1", "version": "0.107.5",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
@ -10702,4 +10702,4 @@
"dev": true "dev": true
} }
} }
} }

7
tests/playwright/pages/Dashboard/Form/index.ts

@ -124,8 +124,11 @@ export class FormPage extends BasePage {
async addField({ field, mode }: { mode: string; field: string }) { async addField({ field, mode }: { mode: string; field: string }) {
if (mode === 'dragDrop') { if (mode === 'dragDrop') {
const src = await this.get().locator(`[data-testid="nc-form-hidden-column-${field}"]`); const src = await this.get().locator(`[data-testid="nc-form-hidden-column-${field}"] > div.ant-card-body`);
const dst = await this.get().locator(`.nc-form-drag-Country`); const dst = await this.get().locator(`[data-testid="nc-form-input-Country"]`);
await src.waitFor({ state: 'visible' });
await dst.waitFor({ state: 'visible' });
await src.dragTo(dst, { trial: true });
await src.dragTo(dst); await src.dragTo(dst);
} else if (mode === 'clickField') { } else if (mode === 'clickField') {
const src = await this.get().locator(`[data-testid="nc-form-hidden-column-${field}"]`); const src = await this.get().locator(`[data-testid="nc-form-hidden-column-${field}"]`);

5
tests/playwright/pages/Dashboard/Grid/Column/LTAR/ChildList.ts

@ -31,7 +31,10 @@ export class ChildList extends BasePage {
const childCards = await childList.count(); const childCards = await childList.count();
await expect(childCards).toEqual(cardCount); await expect(childCards).toEqual(cardCount);
for (let i = 0; i < cardCount; i++) { for (let i = 0; i < cardCount; i++) {
await expect(await childList.nth(i).textContent()).toContain(cardTitle[i]); await childList.nth(i).locator('.name').waitFor({ state: 'visible' });
await childList.nth(i).locator('.name').scrollIntoViewIfNeeded();
await this.rootPage.waitForTimeout(100);
await expect(await childList.nth(i).locator('.name').textContent()).toContain(cardTitle[i]);
// icon: unlink // icon: unlink
// icon: delete // icon: delete
await expect( await expect(

4
tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts

@ -29,7 +29,9 @@ export class LinkRecord extends BasePage {
const childCards = await childList.count(); const childCards = await childList.count();
await expect(childCards).toEqual(cardTitle.length); await expect(childCards).toEqual(cardTitle.length);
for (let i = 0; i < cardTitle.length; i++) { for (let i = 0; i < cardTitle.length; i++) {
await expect(await childList.nth(i).textContent()).toContain(cardTitle[i]); await childList.nth(i).locator('.name').scrollIntoViewIfNeeded();
await childList.nth(i).locator('.name').waitFor({ state: 'visible' });
await expect(await childList.nth(i).locator('.name').textContent()).toContain(cardTitle[i]);
} }
} }
} }

17
tests/playwright/pages/Dashboard/Grid/index.ts

@ -77,8 +77,9 @@ export class GridPage extends BasePage {
if (index !== 0) await this.get().locator('.nc-grid-row').nth(0).waitFor({ state: 'attached' }); if (index !== 0) await this.get().locator('.nc-grid-row').nth(0).waitFor({ state: 'attached' });
const rowCount = await this.get().locator('.nc-grid-row').count(); const rowCount = await this.get().locator('.nc-grid-row').count();
await (await this.get().locator('.nc-grid-add-new-cell').elementHandle())?.waitForElementState('stable'); const addNewRowButton: Locator = await this.rootPage.locator(`[data-testid="nc-grid-add-new-row"]`);
await this.get().locator('.nc-grid-add-new-cell').click(); await addNewRowButton.waitFor({ state: 'visible' });
await addNewRowButton.click();
await expect(await this.get().locator('.nc-grid-row')).toHaveCount(rowCount + 1); await expect(await this.get().locator('.nc-grid-row')).toHaveCount(rowCount + 1);
@ -156,7 +157,7 @@ export class GridPage extends BasePage {
// Click text=Delete Row // Click text=Delete Row
await this.rootPage.locator('text=Delete Row').click(); await this.rootPage.locator('text=Delete Row').click();
await this.rootPage.locator('text=Yes').click();
// todo: improve selector // todo: improve selector
await this.rootPage await this.rootPage
.locator('span.ant-dropdown-menu-title-content > nc-project-menu-item') .locator('span.ant-dropdown-menu-title-content > nc-project-menu-item')
@ -166,11 +167,13 @@ export class GridPage extends BasePage {
await this.dashboard.waitForLoaderToDisappear(); await this.dashboard.waitForLoaderToDisappear();
} }
async addRowRightClickMenu(index: number) { async addRowRightClickMenu(index: number, columnHeader = 'Title') {
const rowCount = await this.get().locator('.nc-grid-row').count(); const rowCount = await this.get().locator('.nc-grid-row').count();
await this.get().locator(`td[data-testid="cell-Title-${index}"]`).click({
button: 'right', const cell = await this.get().locator(`td[data-testid="cell-${columnHeader}-${index}"]`).last();
}); await cell.click();
await cell.click({ button: 'right' });
// Click text=Insert New Row // Click text=Insert New Row
await this.rootPage.locator('text=Insert New Row').click(); await this.rootPage.locator('text=Insert New Row').click();
await expect(await this.get().locator('.nc-grid-row')).toHaveCount(rowCount + 1); await expect(await this.get().locator('.nc-grid-row')).toHaveCount(rowCount + 1);

7
tests/playwright/pages/Dashboard/common/Cell/DateCell.ts

@ -1,5 +1,6 @@
import { CellPageObject } from '.'; import { CellPageObject } from '.';
import BasePage from '../../../Base'; import BasePage from '../../../Base';
import { expect } from '@playwright/test';
export class DateCellPageObject extends BasePage { export class DateCellPageObject extends BasePage {
readonly cell: CellPageObject; readonly cell: CellPageObject;
@ -20,6 +21,12 @@ export class DateCellPageObject extends BasePage {
}); });
} }
async verify({ index, columnHeader, date }: { index: number; columnHeader: string; date: string }) {
const cell = await this.get({ index, columnHeader });
await cell.scrollIntoViewIfNeeded();
await expect(cell.locator(`[title="${date}"]`)).toBeVisible();
}
async selectDate({ async selectDate({
// date in format `YYYY-MM-DD` // date in format `YYYY-MM-DD`
date, date,

10
tests/playwright/pages/Dashboard/common/Cell/RatingCell.ts

@ -22,10 +22,10 @@ export class RatingCellPageObject extends BasePage {
}); });
} }
async verify({ index, columnHeader, rating }: { index?: number; columnHeader: string; rating: number }) { async verify({ index, columnHeader, rating }: { index: number; columnHeader: string; rating: number }) {
await this.get({ index, columnHeader }).scrollIntoViewIfNeeded(); const cell = await this.get({ index, columnHeader });
await expect(await this.get({ index, columnHeader }).locator(`div[role="radio"][aria-checked="true"]`)).toHaveCount( await cell.scrollIntoViewIfNeeded();
rating const ratingCount = await cell.locator(`li.ant-rate-star.ant-rate-star-full`).count();
); await expect(ratingCount).toBe(rating);
} }
} }

6
tests/playwright/pages/Dashboard/common/Cell/index.ts

@ -39,9 +39,9 @@ export class CellPageObject extends BasePage {
get({ index, columnHeader }: CellProps): Locator { get({ index, columnHeader }: CellProps): Locator {
if (this.parent instanceof SharedFormPage) { if (this.parent instanceof SharedFormPage) {
return this.parent.get().locator(`[data-testid="nc-form-input-cell-${columnHeader}"]`); return this.parent.get().locator(`[data-testid="nc-form-input-cell-${columnHeader}"]`).first();
} else { } else {
return this.parent.get().locator(`td[data-testid="cell-${columnHeader}-${index}"]`); return this.parent.get().locator(`td[data-testid="cell-${columnHeader}-${index}"]`).first();
} }
} }
@ -286,6 +286,8 @@ export class CellPageObject extends BasePage {
// verify only the elements that are passed in // verify only the elements that are passed in
for (let i = 0; i < value.length; ++i) { for (let i = 0; i < value.length; ++i) {
await chips.nth(i).locator('.name').waitFor({ state: 'visible' });
await chips.nth(i).locator('.name').scrollIntoViewIfNeeded();
await expect(await chips.nth(i).locator('.name')).toHaveText(value[i]); await expect(await chips.nth(i).locator('.name')).toHaveText(value[i]);
} }

81
tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts

@ -37,6 +37,75 @@ export class ToolbarFilterPage extends BasePage {
await this.get().locator(`button:has-text("Add Filter")`).first().click(); await this.get().locator(`button:has-text("Add Filter")`).first().click();
} }
// can reuse code for addFilterGroup and addFilter
// support for subOperation & datatype specific filter operations not supported yet
async addFilterGroup({
title,
operation,
_subOperation: _subOperation,
value,
_locallySaved: _locallySaved = false,
_dataType: _dataType,
_openModal: _openModal = false,
_skipWaitingResponse: _skipWaitingResponse = false, // used for undo (single request, less stable)
filterGroupIndex = 0,
filterLogicalOperator = 'AND',
}: {
title: string;
operation: string;
_subOperation?: string; // for date datatype
value?: string;
_locallySaved?: boolean;
_dataType?: string;
_openModal?: boolean;
_skipWaitingResponse?: boolean;
filterGroupIndex?: number;
filterLogicalOperator?: string;
}) {
await this.get().locator(`button:has-text("Add Filter Group")`).last().click();
const filterDropdown = await this.get().locator('.menu-filter-dropdown').nth(filterGroupIndex);
await filterDropdown.waitFor({ state: 'visible' });
await filterDropdown.locator(`button:has-text("Add Filter")`).first().click();
const selectField = await filterDropdown.locator('.nc-filter-field-select').last();
const selectOperation = await filterDropdown.locator('.nc-filter-operation-select').last();
const selectValue = await filterDropdown.locator('.nc-filter-value-select > input').last();
await selectField.waitFor({ state: 'visible' });
await selectField.click();
const fieldDropdown = await this.rootPage
.locator('div.ant-select-dropdown.nc-dropdown-toolbar-field-list')
.last()
.locator(`div[label="${title}"]:visible`);
await fieldDropdown.waitFor({ state: 'visible' });
await fieldDropdown.click();
await selectOperation.waitFor({ state: 'visible' });
await selectOperation.click();
const operationDropdown = await this.rootPage
.locator('div.ant-select-dropdown.nc-dropdown-filter-comp-op')
.last()
.locator(`.ant-select-item:has-text("${operation}")`);
await operationDropdown.waitFor({ state: 'visible' });
await operationDropdown.click();
await selectValue.waitFor({ state: 'visible' });
await selectValue.fill(value);
if (filterGroupIndex) {
if (filterLogicalOperator === 'OR') {
const logicalButton = await this.rootPage.locator('div.flex.bob').nth(filterGroupIndex - 1);
await logicalButton.waitFor({ state: 'visible' });
await logicalButton.click();
const logicalDropdown = await this.rootPage.locator(
'div.ant-select-dropdown.nc-dropdown-filter-logical-op-group'
);
await logicalDropdown.waitFor({ state: 'visible' });
await logicalDropdown.locator(`.ant-select-item:has-text("${filterLogicalOperator}")`).click();
}
}
}
async add({ async add({
title, title,
operation, operation,
@ -65,7 +134,7 @@ export class ToolbarFilterPage extends BasePage {
await this.rootPage.locator('.nc-filter-field-select').last().click(); await this.rootPage.locator('.nc-filter-field-select').last().click();
if (skipWaitingResponse) { if (skipWaitingResponse) {
this.rootPage await this.rootPage
.locator('div.ant-select-dropdown.nc-dropdown-toolbar-field-list') .locator('div.ant-select-dropdown.nc-dropdown-toolbar-field-list')
.locator(`div[label="${title}"]:visible`) .locator(`div[label="${title}"]:visible`)
.click(); .click();
@ -88,7 +157,7 @@ export class ToolbarFilterPage extends BasePage {
// first() : filter list has >, >= // first() : filter list has >, >=
if (skipWaitingResponse) { if (skipWaitingResponse) {
this.rootPage await this.rootPage
.locator('.nc-dropdown-filter-comp-op') .locator('.nc-dropdown-filter-comp-op')
.locator(`.ant-select-item:has-text("${operation}")`) .locator(`.ant-select-item:has-text("${operation}")`)
.first() .first()
@ -117,7 +186,7 @@ export class ToolbarFilterPage extends BasePage {
// first() : filter list has >, >= // first() : filter list has >, >=
if (skipWaitingResponse) { if (skipWaitingResponse) {
this.rootPage await this.rootPage
.locator('.nc-dropdown-filter-comp-sub-op') .locator('.nc-dropdown-filter-comp-sub-op')
.locator(`.ant-select-item:has-text("${subOperation}")`) .locator(`.ant-select-item:has-text("${subOperation}")`)
.first() .first()
@ -167,7 +236,7 @@ export class ToolbarFilterPage extends BasePage {
await this.rootPage.locator(`.ant-picker-dropdown:visible`); await this.rootPage.locator(`.ant-picker-dropdown:visible`);
if (skipWaitingResponse) { if (skipWaitingResponse) {
this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click(); await this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click();
} else { } else {
await this.waitForResponse({ await this.waitForResponse({
uiAction: () => this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click(), uiAction: () => this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click(),
@ -188,7 +257,7 @@ export class ToolbarFilterPage extends BasePage {
break; break;
case UITypes.Duration: case UITypes.Duration:
if (skipWaitingResponse) { if (skipWaitingResponse) {
this.get().locator('.nc-filter-value-select').locator('input').fill(value); await this.get().locator('.nc-filter-value-select').locator('input').fill(value);
} else { } else {
await this.waitForResponse({ await this.waitForResponse({
uiAction: () => this.get().locator('.nc-filter-value-select').locator('input').fill(value), uiAction: () => this.get().locator('.nc-filter-value-select').locator('input').fill(value),
@ -298,4 +367,4 @@ export class ToolbarFilterPage extends BasePage {
return opListText; return opListText;
} }
} }

4
tests/playwright/tests/db/cellSelection.spec.ts

@ -30,7 +30,7 @@ test.describe('Verify cell selection', () => {
start: { index: 0, columnHeader: 'FirstName' }, start: { index: 0, columnHeader: 'FirstName' },
end: { index: 1, columnHeader: 'LastName' }, end: { index: 1, columnHeader: 'LastName' },
}); });
expect(await grid.copyWithKeyboard()).toBe('MARY \t SMITH\n' + ' PATRICIA \t JOHNSON\n'); expect(await grid.copyWithKeyboard()).toBe('MARY\tSMITH\n' + 'PATRICIA\tJOHNSON');
await dashboard.closeAllTabs(); await dashboard.closeAllTabs();
// #3 when copied with mouse, it copies correct text // #3 when copied with mouse, it copies correct text
@ -40,7 +40,7 @@ test.describe('Verify cell selection', () => {
end: { index: 1, columnHeader: 'LastName' }, end: { index: 1, columnHeader: 'LastName' },
}); });
expect(await grid.copyWithMouse({ index: 0, columnHeader: 'FirstName' })).toBe( expect(await grid.copyWithMouse({ index: 0, columnHeader: 'FirstName' })).toBe(
'MARY \t SMITH\n' + ' PATRICIA \t JOHNSON\n' 'MARY\tSMITH\n' + 'PATRICIA\tJOHNSON'
); );
await dashboard.closeAllTabs(); await dashboard.closeAllTabs();
}); });

116
tests/playwright/tests/db/columnLtarDragdrop.spec.ts

@ -0,0 +1,116 @@
import { expect, Locator, test } from '@playwright/test';
import setup from '../../setup';
import { Api, UITypes } from 'nocodb-sdk';
import { DashboardPage } from '../../pages/Dashboard';
import { GridPage } from '../../pages/Dashboard/Grid';
import { getTextExcludeIconText } from '../utils/general';
let api: Api<any>;
const recordCount = 10;
test.describe('Test table', () => {
let context: any;
let dashboard: DashboardPage;
let grid: GridPage;
const tables = [];
test.beforeEach(async ({ page }) => {
context = await setup({ page, isEmptyProject: true });
dashboard = new DashboardPage(page, context.project);
grid = dashboard.grid;
api = new Api({
baseURL: `http://localhost:8080/`,
headers: {
'xc-auth': context.token,
},
});
const columns = [
{
column_name: 'Id',
title: 'Id',
uidt: UITypes.ID,
},
{
column_name: 'Title',
title: 'Title',
uidt: UITypes.SingleLineText,
pv: true,
},
];
const rows = [];
for (let i = 0; i < recordCount; i++) {
rows.push({
Id: i + 1,
Title: `${i + 1}`,
});
}
// Create tables
const project = await api.project.read(context.project.id);
for (let i = 0; i < 2; i++) {
const table = await api.base.tableCreate(context.project.id, project.bases?.[0].id, {
table_name: `Table${i}`,
title: `Table${i}`,
columns: columns,
});
tables.push(table);
await api.dbTableRow.bulkCreate('noco', context.project.id, tables[i].id, rows);
}
// refresh page
await page.reload();
});
test('drag drop for LTAR, lookup creation', async () => {
await dashboard.treeView.openTable({ title: 'Table0' });
const src = await dashboard.rootPage.locator(`[data-testid="tree-view-table-draggable-handle-Table1"]`);
const dst = await dashboard.rootPage.locator(`[data-testid="grid-row-0"]`);
// drag drop for LTAR column creation
//
await src.dragTo(dst);
const columnAddModal = await dashboard.rootPage.locator(`.nc-dropdown-grid-add-column`);
{
const columnType = await getTextExcludeIconText(await columnAddModal.locator(`.nc-column-type-input`));
const linkTable = await getTextExcludeIconText(
await columnAddModal.locator(`.ant-form-item-control-input`).nth(3)
);
expect(columnType).toContain('LinkToAnotherRecord');
expect(linkTable).toContain('Table1');
// save
await columnAddModal.locator(`.ant-btn-primary`).click();
// verify if column is created
await grid.column.verify({ title: 'Table1List', isVisible: true });
}
// drag drop for lookup column creation
//
await src.dragTo(dst);
{
// const columnAddModal = await dashboard.rootPage.locator(`.nc-dropdown-grid-add-column`);
const columnType = await getTextExcludeIconText(await columnAddModal.locator(`.nc-column-type-input`));
const linkField = await getTextExcludeIconText(
await columnAddModal.locator(`.ant-form-item-control-input`).nth(2)
);
const childColumn = await getTextExcludeIconText(
await columnAddModal.locator(`.ant-form-item-control-input`).nth(3)
);
// validate
expect(columnType).toContain('Lookup');
expect(linkField).toContain('Table1List');
expect(childColumn).toContain('Title');
// save
await columnAddModal.locator(`.ant-btn-primary`).click();
// verify if column is created
await grid.column.verify({ title: 'Table1Lookup', isVisible: true });
}
});
});

1
tests/playwright/tests/db/columnRelationalExtendedTests.spec.ts

@ -1,7 +1,6 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../setup';
import { isPg } from '../../setup/db';
test.describe('Relational Columns', () => { test.describe('Relational Columns', () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;

49
tests/playwright/tests/db/filters.spec.ts

@ -1339,3 +1339,52 @@ test.describe('Filter Tests: Toggle button', () => {
await dashboard.settings.toggleNullEmptyFilters(); await dashboard.settings.toggleNullEmptyFilters();
}); });
}); });
test.describe('Filter Tests: Filter groups', () => {
/**
* Steps
*
* 1. Open table
* 2. Verify filter options : should not include NULL & EMPTY options
* 3. Enable `Show NULL & EMPTY in Filter` in Project Settings
* 4. Verify filter options : should include NULL & EMPTY options
* 5. Add NULL & EMPTY filters
* 6. Disable `Show NULL & EMPTY in Filter` in Project Settings : should not be allowed
* 7. Remove the NULL & EMPTY filters
* 8. Disable `Show NULL & EMPTY in Filter` in Project Settings again : should be allowed
*
*/
test.beforeEach(async ({ page }) => {
context = await setup({ page, isEmptyProject: false });
dashboard = new DashboardPage(page, context.project);
toolbar = dashboard.grid.toolbar;
});
test('Filter: Empty filters', async () => {
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.openTable({ title: 'Country', networkResponse: false });
await toolbar.clickFilter({ networkValidation: false });
await toolbar.filter.addFilterGroup({
title: 'Country',
operation: 'is equal',
value: 'Argentina',
});
await toolbar.clickFilter({ networkValidation: false });
await toolbar.clickFilter({ networkValidation: false });
await toolbar.filter.addFilterGroup({
title: 'Country',
operation: 'is equal',
value: 'Indonesia',
filterGroupIndex: 1,
filterLogicalOperator: 'OR',
});
await toolbar.clickFilter({ networkValidation: false });
await validateRowArray({
rowCount: 2,
});
});
});

426
tests/playwright/tests/db/keyboardShortcuts.spec.ts

@ -104,8 +104,18 @@ test.describe('Verify shortcuts', () => {
await page.waitForTimeout(2000); await page.waitForTimeout(2000);
await grid.cell.verify({ index: 1, columnHeader: 'Country', value: 'NewAlgeria' }); await grid.cell.verify({ index: 1, columnHeader: 'Country', value: 'NewAlgeria' });
}); });
});
test.describe('Clipboard support', () => {
const today = new Date().toISOString().slice(0, 10);
let dashboard: DashboardPage, grid: GridPage;
let context: any;
test.beforeEach(async ({ page }) => {
context = await setup({ page, isEmptyProject: true });
dashboard = new DashboardPage(page, context.project);
grid = dashboard.grid;
test('Clipboard support for cells', async () => {
api = new Api({ api = new Api({
baseURL: `http://localhost:8080/`, baseURL: `http://localhost:8080/`,
headers: { headers: {
@ -114,98 +124,26 @@ test.describe('Verify shortcuts', () => {
}); });
const columns = [ const columns = [
{ { column_name: 'Id', uidt: UITypes.ID },
column_name: 'Id', { column_name: 'SingleLineText', uidt: UITypes.SingleLineText },
title: 'Id', { column_name: 'LongText', uidt: UITypes.LongText },
uidt: UITypes.ID, { column_name: 'Number', uidt: UITypes.Number },
}, { column_name: 'PhoneNumber', uidt: UITypes.PhoneNumber },
{ { column_name: 'Email', uidt: UITypes.Email },
column_name: 'SingleLineText', { column_name: 'URL', uidt: UITypes.URL },
title: 'SingleLineText', { column_name: 'Decimal', uidt: UITypes.Decimal },
uidt: UITypes.SingleLineText, { column_name: 'Percent', uidt: UITypes.Percent },
}, { column_name: 'Currency', uidt: UITypes.Currency },
{ { column_name: 'Duration', uidt: UITypes.Duration },
column_name: 'LongText', { column_name: 'SingleSelect', uidt: UITypes.SingleSelect, dtxp: "'Option1','Option2'" },
title: 'LongText', { column_name: 'MultiSelect', uidt: UITypes.MultiSelect, dtxp: "'Option1','Option2'" },
uidt: UITypes.LongText, { column_name: 'Rating', uidt: UITypes.Rating },
}, { column_name: 'Checkbox', uidt: UITypes.Checkbox },
{ { column_name: 'Date', uidt: UITypes.Date },
column_name: 'Number', { column_name: 'Attachment', uidt: UITypes.Attachment },
title: 'Number',
uidt: UITypes.Number,
},
{
column_name: 'PhoneNumber',
title: 'PhoneNumber',
uidt: UITypes.PhoneNumber,
},
{
column_name: 'Email',
title: 'Email',
uidt: UITypes.Email,
},
{
column_name: 'URL',
title: 'URL',
uidt: UITypes.URL,
},
{
column_name: 'Decimal',
title: 'Decimal',
uidt: UITypes.Decimal,
},
{
column_name: 'Percent',
title: 'Percent',
uidt: UITypes.Percent,
},
{
column_name: 'Currency',
title: 'Currency',
uidt: UITypes.Currency,
},
{
column_name: 'Duration',
title: 'Duration',
uidt: UITypes.Duration,
},
{
column_name: 'SingleSelect',
title: 'SingleSelect',
uidt: UITypes.SingleSelect,
dtxp: "'Option1','Option2'",
},
{
column_name: 'MultiSelect',
title: 'MultiSelect',
uidt: UITypes.MultiSelect,
dtxp: "'Option1','Option2'",
},
{
column_name: 'Rating',
title: 'Rating',
uidt: UITypes.Rating,
},
{
column_name: 'Checkbox',
title: 'Checkbox',
uidt: UITypes.Checkbox,
},
{
column_name: 'Date',
title: 'Date',
uidt: UITypes.Date,
},
{
column_name: 'Attachment',
title: 'Attachment',
uidt: UITypes.Attachment,
},
]; ];
const today = new Date().toISOString().slice(0, 10);
const record = { const record = {
Id: 1,
SingleLineText: 'SingleLineText', SingleLineText: 'SingleLineText',
LongText: 'LongText', LongText: 'LongText',
SingleSelect: 'Option1', SingleSelect: 'Option1',
@ -239,10 +177,8 @@ test.describe('Verify shortcuts', () => {
// reload page // reload page
await dashboard.rootPage.reload(); await dashboard.rootPage.reload();
// close 'Team & Auth' tab // close 'Team & Auth' tab
await dashboard.closeTab({ title: 'Team & Auth' }); await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.openTable({ title: 'Sheet1' }); await dashboard.treeView.openTable({ title: 'Sheet1' });
// ######################################## // ########################################
@ -252,147 +188,201 @@ test.describe('Verify shortcuts', () => {
columnHeader: 'Attachment', columnHeader: 'Attachment',
filePath: `${process.cwd()}/fixtures/sampleFiles/1.json`, filePath: `${process.cwd()}/fixtures/sampleFiles/1.json`,
}); });
});
async function verifyCellContents({ rowIndex }: { rowIndex: number }) {
const responseTable = [
{ type: 'SingleLineText', value: 'SingleLineText' },
{ type: 'LongText', value: 'LongText' },
{ type: 'SingleSelect', value: 'Option1' },
{ type: 'MultiSelect', value: `Option1Option2` },
{ type: 'Number', value: '123' },
{ type: 'PhoneNumber', value: '987654321' },
{ type: 'Email', value: 'test@example.com' },
{ type: 'URL', value: 'nocodb.com' },
{ type: 'Decimal', value: '1.12' },
{ type: 'Percent', value: '80' },
{ type: 'Currency', value: 20 },
{ type: 'Duration', value: '00:08' },
{ type: 'Rating', value: 4 },
{ type: 'Checkbox', value: 'true' },
{ type: 'Date', value: today },
{ type: 'Attachment', value: 1 },
];
// ######################################## for (const { type, value } of responseTable) {
if (type === 'Rating') {
await dashboard.grid.cell.rating.verify({
index: rowIndex,
columnHeader: type,
rating: value,
});
} else if (type === 'Checkbox') {
await dashboard.grid.cell.checkbox.verifyChecked({
index: rowIndex,
columnHeader: type,
});
} else if (type === 'Date') {
await dashboard.grid.cell.date.verify({
index: rowIndex,
columnHeader: type,
date: value,
});
} else if (type === 'Attachment') {
await dashboard.grid.cell.attachment.verifyFileCount({
index: rowIndex,
columnHeader: type,
count: value,
});
} else {
await dashboard.grid.cell.verify({
index: rowIndex,
columnHeader: type,
value,
});
}
}
}
async function verifyClipContents({ rowIndex }: { rowIndex: number }) {
const responseTable = [
{ type: 'SingleLineText', value: 'SingleLineText' },
{ type: 'LongText', value: '"LongText"' },
{ type: 'SingleSelect', value: 'Option1' },
{ type: 'MultiSelect', value: 'Option1,Option2' },
{ type: 'Number', value: '123' },
{ type: 'PhoneNumber', value: '987654321' },
{ type: 'Email', value: 'test@example.com' },
{ type: 'URL', value: 'nocodb.com' },
{ type: 'Decimal', value: '1.12' },
{ type: 'Percent', value: '80' },
{ type: 'Currency', value: 20, options: { parseInt: true } },
{ type: 'Duration', value: 480, options: { parseInt: true } },
{ type: 'Rating', value: '4' },
{ type: 'Checkbox', value: 'true' },
{ type: 'Date', value: today },
{ type: 'Attachment', value: '1.json', options: { jsonParse: true } },
];
await dashboard.grid.cell.copyToClipboard({ for (const { type, value, options } of responseTable) {
index: 0, await dashboard.grid.cell.copyToClipboard(
columnHeader: 'SingleLineText', {
}); index: rowIndex,
expect(await dashboard.grid.cell.getClipboardText()).toBe('SingleLineText'); columnHeader: type,
},
{ position: { x: 1, y: 1 } }
);
if (options?.parseInt) {
expect(parseInt(await dashboard.grid.cell.getClipboardText())).toBe(value);
} else if (options?.jsonParse) {
const attachmentsInfo = JSON.parse(await dashboard.grid.cell.getClipboardText());
expect(attachmentsInfo[0]['title']).toBe('1.json');
} else {
expect(await dashboard.grid.cell.getClipboardText()).toBe(value);
}
}
}
await dashboard.grid.cell.copyToClipboard({ test('single cell- all data types', async () => {
index: 0, await verifyClipContents({ rowIndex: 0 });
columnHeader: 'LongText', });
});
expect(await dashboard.grid.cell.getClipboardText()).toBe('LongText');
await dashboard.grid.cell.copyToClipboard( test('multiple cells - horizontal, all data types', async ({ page }) => {
{ // click first cell, press `Ctrl A` and `Ctrl C`
index: 0, await grid.cell.click({ index: 0, columnHeader: 'Id' });
columnHeader: 'SingleSelect', await page.keyboard.press((await grid.isMacOs()) ? 'Meta+a' : 'Control+a');
}, await page.keyboard.press((await grid.isMacOs()) ? 'Meta+c' : 'Control+c');
{ position: { x: 1, y: 1 } }
);
expect(await dashboard.grid.cell.getClipboardText()).toBe('Option1');
await dashboard.grid.cell.copyToClipboard(
{
index: 0,
columnHeader: 'MultiSelect',
},
{ position: { x: 1, y: 1 } }
);
expect(await dashboard.grid.cell.getClipboardText()).toContain('Option1');
expect(await dashboard.grid.cell.getClipboardText()).toContain('Option2');
await dashboard.grid.cell.copyToClipboard({ /////////////////////////////////////////////////////////////////////////
index: 0,
columnHeader: 'SingleLineText',
});
expect(await dashboard.grid.cell.getClipboardText()).toBe('SingleLineText');
await dashboard.grid.cell.copyToClipboard({ // horizontal multiple cells selection : copy paste
index: 0, // add new row, click on first cell, paste
columnHeader: 'Number', await grid.addNewRow({ index: 1, columnHeader: 'SingleLineText', value: 'aaa' });
}); await dashboard.rootPage.waitForTimeout(1000);
expect(await dashboard.grid.cell.getClipboardText()).toBe('123'); await grid.cell.click({ index: 1, columnHeader: 'SingleLineText' });
await page.keyboard.press('ArrowLeft');
await page.keyboard.press((await grid.isMacOs()) ? 'Meta+v' : 'Control+v');
await verifyCellContents({ rowIndex: 1 });
await dashboard.grid.cell.copyToClipboard({ // reload page
index: 0, await dashboard.rootPage.reload();
columnHeader: 'PhoneNumber', await dashboard.grid.verifyRowCount({ count: 2 });
}); });
expect(await dashboard.grid.cell.getClipboardText()).toBe('987654321');
await dashboard.grid.cell.copyToClipboard( test('multiple cells - vertical', async ({ page }) => {
{ let cellText: string[] = ['aaa', 'bbb', 'ccc', 'ddd', 'eee'];
index: 0, for (let i = 1; i <= 5; i++) {
columnHeader: 'Email', await grid.addNewRow({ index: i, columnHeader: 'SingleLineText', value: cellText[i - 1] });
}, }
{ position: { x: 1, y: 1 } }
);
expect(await dashboard.grid.cell.getClipboardText()).toBe('test@example.com');
await dashboard.grid.cell.copyToClipboard(
{
index: 0,
columnHeader: 'URL',
},
{ position: { x: 1, y: 1 } }
);
expect(await dashboard.grid.cell.getClipboardText()).toBe('nocodb.com');
await dashboard.grid.cell.copyToClipboard({ await grid.cell.click({ index: 1, columnHeader: 'SingleLineText' });
index: 0, await page.keyboard.press('Shift+ArrowDown');
columnHeader: 'Decimal', await page.keyboard.press('Shift+ArrowDown');
}); await page.keyboard.press('Shift+ArrowDown');
expect(await dashboard.grid.cell.getClipboardText()).toBe('1.12'); await page.keyboard.press('Shift+ArrowDown');
await page.keyboard.press('Shift+ArrowDown');
await dashboard.grid.cell.copyToClipboard({ await page.keyboard.press((await grid.isMacOs()) ? 'Meta+c' : 'Control+c');
index: 0, await grid.cell.click({ index: 1, columnHeader: 'LongText' });
columnHeader: 'Percent', await page.keyboard.press((await grid.isMacOs()) ? 'Meta+v' : 'Control+v');
});
expect(await dashboard.grid.cell.getClipboardText()).toBe('80');
await dashboard.grid.cell.copyToClipboard({ // reload page
index: 0, await dashboard.rootPage.reload();
columnHeader: 'Currency',
});
// convert from string to integer
expect(parseInt(await dashboard.grid.cell.getClipboardText())).toBe(20);
await dashboard.grid.cell.copyToClipboard({ // verify copied data
index: 0, for (let i = 1; i <= 5; i++) {
columnHeader: 'Duration', await grid.cell.verify({ index: i, columnHeader: 'LongText', value: cellText[i - 1] });
}); }
expect(parseInt(await dashboard.grid.cell.getClipboardText())).toBe(480);
await dashboard.grid.cell.copyToClipboard( // Block selection
{ await grid.cell.click({ index: 1, columnHeader: 'SingleLineText' });
index: 0, await page.keyboard.press('Shift+ArrowDown');
columnHeader: 'Rating', await page.keyboard.press('Shift+ArrowDown');
}, await page.keyboard.press('Shift+ArrowRight');
{ position: { x: 1, y: 1 } } await page.keyboard.press((await grid.isMacOs()) ? 'Meta+c' : 'Control+c');
); await grid.cell.click({ index: 4, columnHeader: 'SingleLineText' });
expect(await dashboard.grid.cell.getClipboardText()).toBe('4'); await page.keyboard.press((await grid.isMacOs()) ? 'Meta+v' : 'Control+v');
await dashboard.grid.cell.copyToClipboard(
{
index: 0,
columnHeader: 'Checkbox',
},
{ position: { x: 1, y: 1 } }
);
// await new Promise(resolve => setTimeout(resolve, 5000));
expect(await dashboard.grid.cell.getClipboardText()).toBe('true');
await dashboard.grid.cell.click({ // reload page
index: 0, await dashboard.rootPage.reload();
columnHeader: 'Checkbox',
});
await dashboard.grid.cell.copyToClipboard(
{
index: 0,
columnHeader: 'Checkbox',
},
{ position: { x: 1, y: 1 } }
);
expect(await dashboard.grid.cell.getClipboardText()).toBe('false');
await dashboard.grid.cell.copyToClipboard({ // verify copied data
index: 0, for (let i = 4; i <= 5; i++) {
columnHeader: 'Date', await grid.cell.verify({ index: i, columnHeader: 'SingleLineText', value: cellText[i - 4] });
}); await grid.cell.verify({ index: i, columnHeader: 'LongText', value: cellText[i - 4] });
expect(await dashboard.grid.cell.getClipboardText()).toBe(today); }
await dashboard.grid.cell.copyToClipboard( /////////////////////////////////////////////////////////////////////////
{
index: 0, // Meta for block selection
columnHeader: 'Attachment', await grid.cell.click({ index: 1, columnHeader: 'SingleLineText' });
}, await page.keyboard.press(`Shift+${(await grid.isMacOs()) ? 'Meta' : 'Control'}+ArrowDown`);
{ position: { x: 1, y: 1 } } await page.keyboard.press((await grid.isMacOs()) ? 'Meta+c' : 'Control+c');
); await grid.cell.click({ index: 1, columnHeader: 'Email' });
const attachmentsInfo = JSON.parse(await dashboard.grid.cell.getClipboardText()); await page.keyboard.press((await grid.isMacOs()) ? 'Meta+v' : 'Control+v');
expect(attachmentsInfo[0]['title']).toBe('1.json');
// reload page
await dashboard.rootPage.reload();
// verify copied data
// modified cell text after previous block operation
cellText = ['aaa', 'bbb', 'ccc', 'aaa', 'bbb'];
for (let i = 1; i <= 5; i++) {
await grid.cell.verify({ index: i, columnHeader: 'Email', value: cellText[i - 1] });
}
// One copy, multiple paste
await grid.cell.click({ index: 0, columnHeader: 'SingleLineText' });
await page.keyboard.press((await grid.isMacOs()) ? 'Meta+c' : 'Control+c');
await grid.cell.click({ index: 1, columnHeader: 'SingleLineText' });
await page.keyboard.press(`Shift+${(await grid.isMacOs()) ? 'Meta' : 'Control'}+ArrowDown`);
await page.keyboard.press((await grid.isMacOs()) ? 'Meta+v' : 'Control+v');
// reload page
await dashboard.rootPage.reload();
// verify copied data
for (let i = 1; i <= 5; i++) {
await grid.cell.verify({ index: i, columnHeader: 'SingleLineText', value: 'SingleLineText' });
}
}); });
}); });

8
tests/playwright/tests/db/toolbarOperations.spec.ts

@ -77,10 +77,10 @@ test.describe('Toolbar operations (GRID)', () => {
test('row height', async () => { test('row height', async () => {
// define an array of row heights // define an array of row heights
const rowHeight = [ const rowHeight = [
{ title: 'Short', height: '1.5rem' }, { title: 'Short', height: '1.8rem' },
{ title: 'Medium', height: '3rem' }, { title: 'Medium', height: '3.6rem' },
{ title: 'Tall', height: '6rem' }, { title: 'Tall', height: '7.2rem' },
{ title: 'Extra', height: '9rem' }, { title: 'Extra', height: '10.8rem' },
]; ];
// close 'Team & Auth' tab // close 'Team & Auth' tab

10
tests/playwright/tests/db/undo-redo.spec.ts

@ -307,26 +307,26 @@ test.describe('Undo Redo', () => {
const timeOut = 200; const timeOut = 200;
await verifyRowHeight({ height: '1.5rem' }); await verifyRowHeight({ height: '1.8rem' });
// set row height & verify // set row height & verify
await toolbar.clickRowHeight(); await toolbar.clickRowHeight();
await toolbar.rowHeight.click({ title: 'Tall' }); await toolbar.rowHeight.click({ title: 'Tall' });
await new Promise(resolve => setTimeout(resolve, timeOut)); await new Promise(resolve => setTimeout(resolve, timeOut));
await verifyRowHeight({ height: '6rem' }); await verifyRowHeight({ height: '7.2rem' });
await toolbar.clickRowHeight(); await toolbar.clickRowHeight();
await toolbar.rowHeight.click({ title: 'Medium' }); await toolbar.rowHeight.click({ title: 'Medium' });
await new Promise(resolve => setTimeout(resolve, timeOut)); await new Promise(resolve => setTimeout(resolve, timeOut));
await verifyRowHeight({ height: '3rem' }); await verifyRowHeight({ height: '3.6rem' });
await undo({ page }); await undo({ page });
await new Promise(resolve => setTimeout(resolve, timeOut)); await new Promise(resolve => setTimeout(resolve, timeOut));
await verifyRowHeight({ height: '6rem' }); await verifyRowHeight({ height: '7.2rem' });
await undo({ page }); await undo({ page });
await new Promise(resolve => setTimeout(resolve, timeOut)); await new Promise(resolve => setTimeout(resolve, timeOut));
await verifyRowHeight({ height: '1.5rem' }); await verifyRowHeight({ height: '1.8rem' });
}); });
test('Column width', async ({ page }) => { test('Column width', async ({ page }) => {

2
tests/playwright/tests/db/viewForm.spec.ts

@ -55,7 +55,7 @@ test.describe('Form view', () => {
// add & verify (drag-drop) // add & verify (drag-drop)
await form.addField({ field: 'City List', mode: 'dragDrop' }); await form.addField({ field: 'City List', mode: 'dragDrop' });
await form.verifyFormViewFieldsOrder({ await form.verifyFormViewFieldsOrder({
fields: ['LastUpdate', 'City List', 'Country'], fields: ['LastUpdate', 'Country', 'City List'],
}); });
// remove & verify (hide field button) // remove & verify (hide field button)

10
tests/playwright/tests/utils/general.ts

@ -60,4 +60,12 @@ function getBrowserTimezoneOffset() {
return formattedOffset; return formattedOffset;
} }
export { getTextExcludeIconText, isSubset, getIconText, getDefaultPwd, getBrowserTimezoneOffset }; async function keyPress(selector, key) {
const isMac = (await selector.evaluate(() => navigator.platform)).includes('Mac') ? true : false;
if (false === isMac) {
key.replace('Meta', 'Control');
}
await selector.keyboard.press(key);
}
export { getTextExcludeIconText, isSubset, getIconText, getDefaultPwd, getBrowserTimezoneOffset, keyPress };

Loading…
Cancel
Save