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. 206
      packages/nc-gui/components/smartsheet/Grid.vue
  24. 24
      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. 47
      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. 84
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  40. 28
      packages/nc-gui/composables/useMultiSelect/copyValue.ts
  41. 503
      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. 15
      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. 2
      packages/nocodb/package.json
  59. 4
      packages/nocodb/src/controllers/caches.controller.ts
  60. 19
      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. 17
      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. 2
      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. 79
      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. 406
      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/nc-cli",
"packages/nc-gui",
"packages/nc-plugin",
"packages/nocodb",
"packages/nocodb-sdk"
],
"version": "independent"
}

3
package.json

@ -46,7 +46,8 @@
"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",
"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": {
"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-box-orient': 'vertical',
'overflow': 'hidden',
'white-space': 'pre',
}"
>
{{ 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 = () => {
if (lastSaved.value !== vModel.value) {

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

@ -18,7 +18,7 @@ import {
interface Props {
modelValue?: string | null
isPk?: boolean
isUpdatedFromCopyNPaste: Record<string, boolean>
isUpdatedFromCopyNPaste?: Record<string, boolean>
}
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>
<template>

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

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

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

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

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

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

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

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

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

@ -10,6 +10,7 @@ import {
ColumnInj,
IsKanbanInj,
ReadonlyInj,
RowHeightInj,
computed,
enumColor,
extractSdkResponseErrorMsg,
@ -46,12 +47,12 @@ const readOnly = inject(ReadonlyInj)!
const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const rowHeight = inject(RowHeightInj, ref(undefined))
const selectedIds = ref<string[]>([])
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 editAllowed = computed(() => (hasEditRoles.value || isForm.value) && (active.value || editable.value))
const editAllowed = computed(() => (hasEditRoles.value || isForm.value) && active.value)
const vModel = computed({
get: () => {
@ -327,7 +328,17 @@ const selectedOpts = computed(() => {
<template>
<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">
<a-tag class="rounded-tag" :color="selectedOpt.color" :style="{ order: selectedOpt.index }">
<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>
<template>

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

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

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

@ -1,6 +1,6 @@
<script setup lang="ts">
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 {
modelValue?: string | null
@ -14,18 +14,15 @@ const { showNull } = useGlobal()
const editEnabled = inject(EditModeInj)
const rowHeight = inject(
RowHeightInj,
computed(() => undefined),
)
const rowHeight = inject(RowHeightInj, ref(undefined))
const readonly = inject(ReadonlyInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
</script>
<template>

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

@ -10,18 +10,15 @@ const emits = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj)
const rowHeight = inject(
RowHeightInj,
computed(() => undefined),
)
const rowHeight = inject(RowHeightInj, ref(undefined))
const { showNull } = useGlobal()
const vModel = useVModel(props, 'modelValue', emits, { defaultValue: '' })
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLTextAreaElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLTextAreaElement)?.focus()
</script>
<template>

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

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

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

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

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

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

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

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

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

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

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

@ -532,10 +532,10 @@ watch(view, (nextView) => {
<!-- Header -->
<div v-if="isEditable" class="px-4 lg:px-12">
<a-form-item v-if="isEditable">
<a-input
<a-textarea
v-model:value="formViewData.heading"
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"
hide-details
placeholder="Form Title"
@ -551,10 +551,10 @@ watch(view, (nextView) => {
<!-- Sub Header -->
<div v-if="isEditable" class="px-4 lg:px-12">
<a-form-item>
<a-input
<a-textarea
v-model:value="formViewData.subheading"
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"
hide-details
:placeholder="$t('msg.info.formDesc')"
@ -697,7 +697,7 @@ watch(view, (nextView) => {
<a-form-item
v-if="isVirtualCol(element)"
:name="element.title"
class="!mb-0"
class="!mb-0 nc-input-required-error"
:rules="[
{
required: isRequired(element, element.required),
@ -719,7 +719,7 @@ watch(view, (nextView) => {
<a-form-item
v-else
:name="element.title"
class="!mb-0"
class="!mb-0 nc-input-required-error"
:rules="[
{
required: isRequired(element, element.required),
@ -743,7 +743,7 @@ watch(view, (nextView) => {
</LazySmartsheetDivDataCell>
</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>
</template>
@ -837,7 +837,7 @@ watch(view, (nextView) => {
<style scoped lang="scss">
.nc-editable:hover {
.nc-field-remove-icon {
:deep(.nc-field-remove-icon) {
@apply opacity-100;
}
}
@ -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) {
@apply p-0;

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

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

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

@ -1,6 +1,7 @@
<script lang="ts" setup>
import { nextTick } from '@vue/runtime-core'
import type { ColumnReqType, ColumnType, GridType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import {
ActiveViewInj,
CellUrlDisableOverlayInj,
@ -103,7 +104,8 @@ const expandedFormDlg = ref(false)
const expandedFormRow = ref<Row>()
const expandedFormRowState = ref<Record<string, any>>()
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)
@ -116,13 +118,15 @@ const {
formattedData: data,
updateOrSaveRow,
changePage,
addEmptyRow,
addEmptyRow: _addEmptyRow,
deleteRow,
deleteSelectedRows,
selectedAllRecords,
removeRowIfNew,
navigateToSiblingRow,
getExpandedRowIndex,
deleteRangeOfRows,
bulkUpdateRows,
} = useViewData(meta, view, xWhere)
const { getMeta } = useMetas()
@ -141,11 +145,20 @@ const getContainerScrollForElement = (
) => {
const childPos = el.getBoundingClientRect()
const parentPos = container.getBoundingClientRect()
// provide an extra offset to show the prev/next/up/bottom cell
const extraOffset = 15
const numColWidth = container.querySelector('thead th:nth-child(1)')?.getBoundingClientRect().width ?? 0
const primaryColWidth = container.querySelector('thead th:nth-child(2)')?.getBoundingClientRect().width ?? 0
const stickyColsWidth = numColWidth + primaryColWidth
const relativePos = {
top: childPos.top - parentPos.top,
right: childPos.right - parentPos.right,
bottom: childPos.bottom - parentPos.bottom,
left: childPos.left - parentPos.left,
left: childPos.left - parentPos.left - stickyColsWidth,
}
const scroll = {
@ -159,9 +172,9 @@ const getContainerScrollForElement = (
*/
scroll.left =
relativePos.right + (offset?.right || 0) > 0
? container.scrollLeft + relativePos.right + (offset?.right || 0)
? container.scrollLeft + relativePos.right + (offset?.right || 0) + extraOffset
: relativePos.left - (offset?.left || 0) < 0
? container.scrollLeft + relativePos.left - (offset?.left || 0)
? container.scrollLeft + relativePos.left - (offset?.left || 0) - extraOffset
: container.scrollLeft
/*
@ -170,9 +183,9 @@ const getContainerScrollForElement = (
*/
scroll.top =
relativePos.bottom + (offset?.bottom || 0) > 0
? container.scrollTop + relativePos.bottom + (offset?.bottom || 0)
? container.scrollTop + relativePos.bottom + (offset?.bottom || 0) + extraOffset
: relativePos.top - (offset?.top || 0) < 0
? container.scrollTop + relativePos.top - (offset?.top || 0)
? container.scrollTop + relativePos.top - (offset?.top || 0) - extraOffset
: container.scrollTop
return scroll
@ -187,8 +200,9 @@ const {
clearSelectedRange,
copyValue,
isCellActive,
tbodyEl,
resetSelectedRange,
makeActive,
selectedRange,
} = useMultiSelect(
meta,
fields,
@ -196,6 +210,7 @@ const {
$$(editEnabled),
isPkAvail,
clearCell,
clearSelectedRangeOfCells,
makeEditable,
scrollToCell,
(e: KeyboardEvent) => {
@ -219,7 +234,6 @@ const {
if (e.key === ' ') {
if (isCellActive.value && !editEnabled && hasEditPermission) {
e.preventDefault()
clearSelectedRange()
const row = data.value[activeCell.row]
expandForm(row)
return true
@ -243,6 +257,9 @@ const {
if (cmdOrCtrl) {
if (!isCellActive.value) return
// cmdOrCtrl+shift handled in useMultiSelect
if (e.shiftKey) return
switch (e.key) {
case 'ArrowUp':
e.preventDefault()
@ -325,6 +342,7 @@ const {
// update/save cell value
await updateOrSaveRow(rowObj, ctx.updatedColumnTitle || columnObj.title)
},
bulkUpdateRows,
)
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) {
// get active cell
const rows = tbodyEl.value?.querySelectorAll('tr')
const rows = tableBodyEl.value?.querySelectorAll('tr')
const cols = rows?.[row].querySelectorAll('td')
const td = cols?.[col === 0 ? 0 : col + 1]
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 })
// if first column set left to 0 since it's sticky it will be visible and calculated value will be wrong
// setting left to 0 will make it scroll to the left
if (col === 0) {
tdScroll.left = 0
}
if (rows && row === rows.length - 2) {
// if last row make 'Add New Row' visible
gridWrapper.value.scrollTo({
@ -412,6 +436,8 @@ const showLoading = ref(true)
const skipRowRemovalOnCancel = ref(false)
const preloadColumn = ref<Partial<any>>()
function expandForm(row: Row, state?: Record<string, any>, fromToolbar = false) {
const rowId = extractPkFromRow(row.row, meta.value?.columns as ColumnType[])
@ -441,6 +467,13 @@ const onXcResizing = (cn: string, event: any) => {
defineExpose({
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
@ -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) {
if (!hasEditPermission || editEnabled || isView) {
if (!hasEditPermission || editEnabled || isView || isLocked.value || readOnly.value || isSystemColumn(col)) {
return
}
@ -565,6 +628,10 @@ function makeEditable(row: Row, col: ColumnType) {
return
}
if ([UITypes.SingleSelect, UITypes.MultiSelect].includes(col.uidt as UITypes)) {
return
}
return (editEnabled = true)
}
@ -578,7 +645,7 @@ useEventListener(document, 'keyup', async (e: KeyboardEvent) => {
/** On clicking outside of table reset active cell */
const smartTable = ref(null)
onClickOutside(tbodyEl, (e) => {
onClickOutside(tableBodyEl, (e) => {
// do nothing if context menu was open
if (contextMenu.value) return
@ -631,6 +698,9 @@ const onNavigate = (dir: NavigateDir) => {
}
break
}
nextTick(() => {
scrollToCell()
})
}
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
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) => {
Modal.confirm({
title: `Do you want to delete this row?`,
wrapClassName: 'nc-modal-row-delete',
okText: 'Yes',
okType: 'danger',
cancelText: 'No',
onOk() {
try {
deleteRow(row)
} catch (e: any) {
message.error(e.message)
}
},
}
const deleteSelectedRangeOfRows = () => {
deleteRangeOfRows(selectedRange).then(() => {
clearSelectedRange()
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>
@ -796,9 +883,9 @@ const confirmDeleteRow = (row: number) => {
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white"
@contextmenu="showContextMenu"
>
<thead ref="tableHead">
<thead ref="tableHeadEl">
<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">
<template v-if="!readOnly">
<div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div>
@ -850,9 +937,10 @@ const confirmDeleteRow = (row: number) => {
<template #overlay>
<SmartsheetColumnEditOrAddProvider
v-if="addColumnDropdown"
:preload="preloadColumn"
:column-position="columnOrder"
@submit="closeAddColumnDropdown"
@cancel="closeAddColumnDropdown"
@submit="closeAddColumnDropdown(true)"
@cancel="closeAddColumnDropdown()"
@click.stop
@keydown.stop
/>
@ -861,12 +949,12 @@ const confirmDeleteRow = (row: number) => {
</th>
</tr>
</thead>
<tbody ref="tbodyEl">
<tbody ref="tableBodyEl">
<LazySmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row">
<template #default="{ state }">
<tr
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}`"
>
<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
v-for="(columnObj, colIndex) of fields"
:key="columnObj.id"
class="cell relative cursor-pointer nc-grid-cell"
class="cell relative nc-grid-cell"
:class="{
'cursor-pointer': hasEditPermission,
'active': hasEditPermission && isCellSelected(rowIndex, colIndex),
'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row),
'align-middle': !rowHeight || rowHeight === 1,
@ -975,35 +1064,43 @@ const confirmDeleteRow = (row: number) => {
</template>
</LazySmartsheetRow>
<tr v-if="isAddingEmptyRowAllowed">
<td
<tr
v-if="isAddingEmptyRowAllowed"
v-e="['c:row:add:grid-bottom']"
:colspan="visibleColLength + 1"
class="text-left pointer nc-grid-add-new-cell cursor-pointer"
class="cursor-pointer"
@mouseup.stop
@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">
<component :is="iconMap.plus" class="text-pint-500 text-xs ml-2 text-primary" />
<span class="ml-1">
{{ $t('activity.addRow') }}
</span>
</div>
</td>
<td :colspan="visibleColLength"></td>
</tr>
</tbody>
</table>
<template v-if="!isLocked && hasEditPermission" #overlay>
<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">
<!-- Delete Row -->
{{ $t('activity.deleteRow') }}
</div>
</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">
<!-- Delete Selected Rows -->
{{ $t('activity.deleteSelectedRow') }}
@ -1014,6 +1111,7 @@ const confirmDeleteRow = (row: number) => {
<a-menu-item
v-if="
contextMenuTarget &&
selectedRange.isSingleCell() &&
(fields[contextMenuTarget.col].uidt === UITypes.LinkToAnotherRecord ||
!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>
</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">
<!-- Insert New Row -->
{{ $t('activity.insertRow') }}
@ -1040,8 +1143,23 @@ const confirmDeleteRow = (row: number) => {
</a-dropdown>
</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>
<LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg"
@ -1140,14 +1258,14 @@ const confirmDeleteRow = (row: number) => {
thead th:nth-child(2) {
position: sticky !important;
left: 80px;
left: 85px;
z-index: 5;
@apply border-r-2 border-r-gray-300;
}
tbody td:nth-child(2) {
position: sticky !important;
left: 80px;
left: 85px;
z-index: 4;
background: white;
@apply border-r-2 border-r-gray-300;

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

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

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

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

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

@ -6,6 +6,7 @@ import { MetaInj, inject, ref, toRef, useProvideColumnCreateStore } from '#impor
interface Props {
column?: ColumnType
columnPosition?: Pick<ColumnReqType, 'column_order'>
preload?: Partial<ColumnType>
}
const props = defineProps<Props>()
@ -16,9 +17,16 @@ const meta = inject(MetaInj, ref())
const column = toRef(props, 'column')
const preload = toRef(props, 'preload')
useProvideColumnCreateStore(meta, column)
</script>
<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>

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

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

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

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

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

@ -1,5 +1,6 @@
<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 {
ActiveViewInj,
FieldsInj,
@ -29,7 +30,7 @@ const props = defineProps<{
const { isUIAllowed } = useUIPermission()
const { metas } = useMetas()
const { metas, getMeta } = useMetas()
const activeTab = toRef(props, 'activeTab')
@ -64,10 +65,74 @@ provide(
ReadonlyInj,
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>
<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">
<LazySmartsheetToolbar />
@ -75,7 +140,7 @@ provide(
<template v-if="meta">
<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">
<LazySmartsheetGrid v-if="isGrid" />
<LazySmartsheetGrid v-if="isGrid" ref="grid" />
<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 { loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>,
row,
isNew,
@ -81,13 +81,24 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
break
}
})
const belongsToColumn = computed(
() =>
relatedTableMeta.value?.columns?.find((c: any) => c.title === relatedTableDisplayValueProp.value) as ColumnType | undefined,
)
</script>
<template>
<div class="flex w-full chips-wrapper items-center" :class="{ active }">
<div class="chips flex items-center flex-1">
<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>
</div>
@ -102,7 +113,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
/>
</div>
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" @attach-record="listItemsDlg = true" />
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" :column="belongsToColumn" @attach-record="listItemsDlg = true" />
</div>
</template>

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

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

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

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

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

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

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

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

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

@ -1,4 +1,5 @@
<script lang="ts" setup>
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import {
ActiveCellInj,
IsFormInj,
@ -6,6 +7,7 @@ import {
ReadonlyInj,
iconMap,
inject,
isAttachment,
ref,
renderValue,
useExpandedFormDetached,
@ -15,9 +17,11 @@ import {
interface Props {
value?: string | number | boolean
item?: any
column: any
showUnlinkButton: boolean
}
const { value, item } = defineProps<Props>()
const { value, item, column, showUnlinkButton } = defineProps<Props>()
const emit = defineEmits(['unlink'])
@ -56,13 +60,46 @@ export default {
<template>
<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="{ active }"
class="chip group mr-1 my-1 flex items-center rounded-[2px] flex-row"
:class="{ active, 'border-1 py-1 px-2': isAttachment(column) }"
@click="openExpandedForm"
>
<span class="name">{{ renderValue(value) }}</span>
<span class="name">
<!-- Render virtual cell -->
<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="!readOnly && !isLocked && isUIAllowed('xcDatatableEditable')" class="flex items-center">
<div
v-show="active || isForm"
v-if="showUnlinkButton && !readOnly && !isLocked && isUIAllowed('xcDatatableEditable')"
class="flex items-center"
>
<component
:is="iconMap.closeThick"
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,
} from '#imports'
const props = defineProps<{ modelValue?: boolean; cellValue: any }>()
const props = defineProps<{ modelValue?: boolean; cellValue: any; column: any }>()
const emit = defineEmits(['update:modelValue', 'attachRecord'])
@ -148,7 +148,7 @@ const onClick = (row: Row) => {
>
<div class="flex items-center">
<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>
</div>

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

@ -19,7 +19,7 @@ import {
useVModel,
} from '#imports'
const props = defineProps<{ modelValue: boolean }>()
const props = defineProps<{ modelValue: boolean; column: any }>()
const emit = defineEmits(['update:modelValue', 'addNewRecord'])
@ -229,7 +229,11 @@ watch(vModel, (nextVal) => {
:class="{ 'nc-selected-row': selectedRowIndex === i }"
@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">
({{ $t('labels.primaryKey') }} : {{ getRelatedTableRowId(refRow) }})
</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
}
isSingleRow() {
return !this.isEmpty() && this._start?.row === this._end?.row
}
get start(): Cell {
return {
row: Math.min(this._start?.row ?? NaN, this._end?.row ?? NaN),

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

@ -1,47 +1,72 @@
import dayjs from 'dayjs'
import type { ColumnType } from 'nocodb-sdk'
import type { ColumnType, SelectOptionsType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import type { AppInfo } from '~/composables/useGlobal'
import { parseProp } from '#imports'
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,
isMultiple = false,
) {
const { from, to, value } = args
if (from === to && ![UITypes.Attachment, UITypes.Date, UITypes.DateTime, UITypes.Time, UITypes.Year].includes(to)) {
return value
}
const { to, value, column } = args
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) {
case UITypes.Number: {
const parsedNumber = Number(value)
if (isNaN(parsedNumber)) {
if (isMultiple) {
return null
} else {
throw new TypeError(`Cannot convert '${value}' to number`)
}
}
return parsedNumber
}
case UITypes.Rating: {
const parsedNumber = Number(value ?? 0)
if (isNaN(parsedNumber)) {
if (isMultiple) {
return null
} else {
throw new TypeError(`Cannot convert '${value}' to rating`)
}
}
return parsedNumber
}
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: {
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')
}
case UITypes.DateTime: {
const parsedDateTime = dayjs(value)
if (!parsedDateTime.isValid()) {
if (isMultiple) {
return null
} else {
throw new Error('Not a valid datetime value')
}
}
return parsedDateTime.utc().format('YYYY-MM-DD HH:mm:ssZ')
}
case UITypes.Time: {
@ -54,8 +79,12 @@ export default function convertCellData(
parsedTime = dayjs(`1999-01-01 ${value}`)
}
if (!parsedTime.isValid()) {
if (isMultiple) {
return null
} else {
throw new Error('Not a valid time value')
}
}
return parsedTime.format(dateFormat)
}
case UITypes.Year: {
@ -69,19 +98,29 @@ export default function convertCellData(
return parsedDate.format('YYYY')
}
if (isMultiple) {
return null
} else {
throw new Error('Not a valid year value')
}
}
case UITypes.Attachment: {
let parsedVal
try {
parsedVal = parseProp(value)
parsedVal = Array.isArray(parsedVal) ? parsedVal : [parsedVal]
} catch (e) {
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
const defaultAttachmentMeta = {
...(args.appInfo.ee && {
@ -95,7 +134,7 @@ export default function convertCellData(
const attachmentMeta = {
...defaultAttachmentMeta,
...parseProp(args.column?.meta),
...parseProp(column?.meta),
}
const attachments = []
@ -134,12 +173,31 @@ export default function convertCellData(
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.Lookup:
case UITypes.Rollup:
case UITypes.Formula:
case UITypes.QrCode:
throw new Error(`Unsupported conversion from ${from} to ${to}`)
case UITypes.QrCode: {
if (isMultiple) {
return undefined
} else {
throw new Error(`Unsupported conversion for ${to}`)
}
}
default:
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 })])
}

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

@ -1,16 +1,17 @@
import dayjs from 'dayjs'
import type { MaybeRef } from '@vueuse/core'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { RelationTypes, UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { parse } from 'papaparse'
import type { Cell } from './cellRange'
import { CellRange } from './cellRange'
import convertCellData from './convertCellData'
import type { Nullable, Row } from '~/lib'
import {
copyTable,
dateFormats,
extractPkFromRow,
extractSdkResponseErrorMsg,
isDrawerOrModalExist,
isMac,
isTypableInputColumn,
message,
@ -24,6 +25,7 @@ import {
useI18n,
useMetas,
useProject,
useUIPermission,
} from '#imports'
const MAIN_MOUSE_PRESSED = 0
@ -38,15 +40,15 @@ export function useMultiSelect(
_editEnabled: MaybeRef<boolean>,
isPkAvail: MaybeRef<boolean | undefined>,
clearCell: Function,
clearSelectedRangeOfCells: Function,
makeEditable: Function,
scrollToActiveCell?: (row?: number | null, col?: number | null) => void,
scrollToCell?: (row?: number | null, col?: number | null) => void,
keyEventHandler?: Function,
syncCellData?: Function,
bulkUpdateRows?: Function,
) {
const meta = ref(_meta)
const tbodyEl = ref<HTMLElement>()
const { t } = useI18n()
const { copy } = useCopy()
@ -57,8 +59,6 @@ export function useMultiSelect(
const { isMysql } = useProject()
let clipboardContext = $ref<{ value: any; uidt: UITypes } | null>(null)
const editEnabled = ref(_editEnabled)
let isMouseDown = $ref(false)
@ -73,6 +73,9 @@ export function useMultiSelect(
() => !(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) {
if (activeCell.row === row && activeCell.col === col) {
return
@ -96,24 +99,7 @@ export function useMultiSelect(
return parseProp(column?.meta)?.time_format ?? timeFormats[0]
}
async function copyValue(ctx?: Cell) {
try {
if (selectedRange.start !== null && selectedRange.end !== null && !selectedRange.isSingleCell()) {
const cprows = unref(data).slice(selectedRange.start.row, selectedRange.end.row + 1) // slice the selected rows for copy
const cpcols = unref(fields).slice(selectedRange.start.col, selectedRange.end.col + 1) // slice the selected cols for copy
await copyTable(cprows, cpcols)
message.success(t('msg.info.copiedToClipboard'))
} else {
// if copy was called with context (right click position) - copy value from context
// else if there is just one selected cell, copy it's value
const cpRow = ctx?.row ?? activeCell.row
const cpCol = ctx?.col ?? activeCell.col
if (cpRow != null && cpCol != null) {
const rowObj = unref(data)[cpRow]
const columnObj = unref(fields)[cpCol]
const valueToCopy = (rowObj: Row, columnObj: ColumnType) => {
let textToCopy = (columnObj.title && rowObj.row[columnObj.title]) || ''
if (columnObj.uidt === UITypes.Checkbox) {
@ -155,11 +141,63 @@ export function useMultiSelect(
columnObj.uidt === UITypes.DateTime ? constructDateTimeFormat(columnObj) : constructTimeFormat(columnObj),
)
if (columnObj.uidt === UITypes.DateTime && !dayjs(textToCopy).isValid()) {
throw new Error('Invalid DateTime')
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) {
try {
if (selectedRange.start !== null && selectedRange.end !== null && !selectedRange.isSingleCell()) {
const cprows = unref(data).slice(selectedRange.start.row, selectedRange.end.row + 1) // slice the selected rows for copy
const cpcols = unref(fields).slice(selectedRange.start.col, selectedRange.end.col + 1) // slice the selected cols for copy
await copyTable(cprows, cpcols)
message.success(t('msg.info.copiedToClipboard'))
} else {
// if copy was called with context (right click position) - copy value from context
// else if there is just one selected cell, copy it's value
const cpRow = ctx?.row ?? activeCell.row
const cpCol = ctx?.col ?? activeCell.col
if (cpRow != null && cpCol != null) {
const rowObj = unref(data)[cpRow]
const columnObj = unref(fields)[cpCol]
const textToCopy = valueToCopy(rowObj, columnObj)
await copy(textToCopy)
message.success(t('msg.info.copiedToClipboard'))
}
@ -195,35 +233,65 @@ export function useMultiSelect(
function handleMouseDown(event: MouseEvent, row: number, col: number) {
// 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
}
isMouseDown = true
// if shift key is pressed, don't restart the selection
if (event.shiftKey) return
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) => {
isMouseDown = true
// 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 })
selectedRange.endRange({ row, col })
makeActive(row, col)
}
selectedRange.endRange({ row, col })
scrollToCell?.(row, col)
isMouseDown = false
}
const handleMouseUp = (event: MouseEvent) => {
if (isMouseDown) {
isMouseDown = false
// timeout is needed, because we want to set cell as active AFTER all the child's click handler's called
// 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(() => {
makeActive(selectedRange.start.row, selectedRange.start.col)
// if shift key is pressed, don't change the active cell
if (event.shiftKey) return
if (selectedRange._start) {
makeActive(selectedRange._start.row, selectedRange._start.col)
}
}, 0)
// if the editEnabled is false, prevent selecting text on mouseUp
if (!unref(editEnabled)) {
event.preventDefault()
}
isMouseDown = false
}
}
const handleKeyDown = async (e: KeyboardEvent) => {
@ -232,10 +300,12 @@ export function useMultiSelect(
return true
}
if (!isCellActive.value) {
if (!isCellActive.value || activeCell.row === null || activeCell.col === null) {
return
}
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
/** on tab key press navigate through cells */
switch (e.key) {
case 'Tab':
@ -261,7 +331,7 @@ export function useMultiSelect(
editEnabled.value = false
}
}
scrollToActiveCell?.()
scrollToCell?.()
break
/** on enter key press make cell editable */
case 'Enter':
@ -273,94 +343,322 @@ export function useMultiSelect(
/** on delete key press clear cell */
case 'Delete':
e.preventDefault()
if (selectedRange.isSingleCell()) {
selectedRange.clear()
await clearCell(activeCell as { row: number; col: number })
} else {
await clearSelectedRangeOfCells()
}
break
/** on arrow key press navigate through cells */
case 'ArrowRight':
e.preventDefault()
if (e.shiftKey) {
if (cmdOrCtrl) {
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++
scrollToActiveCell?.()
selectedRange.startRange({ row: activeCell.row, col: activeCell.col })
scrollToCell?.()
editEnabled.value = false
}
}
break
case 'ArrowLeft':
e.preventDefault()
if (e.shiftKey) {
if (cmdOrCtrl) {
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--
scrollToActiveCell?.()
selectedRange.startRange({ row: activeCell.row, col: activeCell.col })
scrollToCell?.()
editEnabled.value = false
}
}
break
case 'ArrowUp':
e.preventDefault()
if (e.shiftKey) {
if (cmdOrCtrl) {
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--
scrollToActiveCell?.()
selectedRange.startRange({ row: activeCell.row, col: activeCell.col })
scrollToCell?.()
editEnabled.value = false
}
}
break
case 'ArrowDown':
e.preventDefault()
if (e.shiftKey) {
if (cmdOrCtrl) {
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++
scrollToActiveCell?.()
selectedRange.startRange({ row: activeCell.row, col: activeCell.col })
scrollToCell?.()
editEnabled.value = false
}
}
break
default:
{
const rowObj = unref(data)[activeCell.row]
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) {
// copy - ctrl/cmd +c
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,
await copyValue()
break
// select all - ctrl/cmd +a
case 65:
selectedRange.startRange({ row: 0, col: 0 })
selectedRange.endRange({ row: unref(data).length - 1, col: unref(columnLength) - 1 })
break
}
}
if (unref(editEnabled) || e.ctrlKey || e.altKey || e.metaKey) {
return true
}
/** on letter key press make cell editable and empty */
if (e.key.length === 1) {
if (!unref(isPkAvail) && !rowObj.rowMeta.new) {
// Update not allowed for table which doesn't have primary Key
return message.info(t('msg.info.updateNotAllowedWithoutPK'))
}
if (isTypableInputColumn(columnObj) && makeEditable(rowObj, columnObj) && columnObj.title) {
rowObj.row[columnObj.title] = ''
}
// editEnabled = true
}
} else {
clipboardContext = null
}
await copyValue()
break
// paste - ctrl/cmd + v
case 86:
}
}
const resetSelectedRange = () => selectedRange.clear()
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
) {
if (!clipboardContext || typeof clipboardContext.value !== 'object') {
return message.info('Invalid data')
}
const clipboardContext = JSON.parse(clipboardData!)
rowObj.row[columnObj.title!] = convertCellData(
{
value: clipboardContext.value,
from: clipboardContext.uidt,
value: clipboardContext,
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,
@ -370,68 +668,84 @@ export function useMultiSelect(
if (!foreignKeyColumn) return
rowObj.row[foreignKeyColumn.title!] = extractPkFromRow(
clipboardContext.value,
(relatedTableMeta as any)!.columns!,
)
rowObj.row[foreignKeyColumn.title!] = extractPkFromRow(clipboardContext, (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 (!isPasteable(rowObj, columnObj, true)) {
return
}
if (clipboardContext) {
rowObj.row[columnObj.title!] = convertCellData(
const pasteValue = convertCellData(
{
value: clipboardContext.value,
from: clipboardContext.uidt,
value: clipboardData,
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))
}
}
if (pasteValue !== undefined) {
rowObj.row[columnObj.title!] = pasteValue
}
if (unref(editEnabled) || e.ctrlKey || e.altKey || e.metaKey) {
return true
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
}
/** on letter key press make cell editable and empty */
if (e.key.length === 1) {
if (!unref(isPkAvail) && !rowObj.rowMeta.new) {
// Update not allowed for table which doesn't have primary Key
return message.info(t('msg.info.updateNotAllowedWithoutPK'))
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
}
if (isTypableInputColumn(columnObj) && makeEditable(rowObj, columnObj) && columnObj.title) {
rowObj.row[columnObj.title] = ''
}
// editEnabled = true
}
await bulkUpdateRows?.(rows, props)
}
break
}
} catch (error: any) {
message.error(await extractSdkResponseErrorMsg(error))
}
}
const resetSelectedRange = () => selectedRange.clear()
const clearSelectedRange = selectedRange.clear.bind(selectedRange)
useEventListener(document, 'keydown', handleKeyDown)
useEventListener(tbodyEl, 'mouseup', handleMouseUp)
useEventListener(document, 'mouseup', handleMouseUp)
useEventListener(document, 'paste', handlePaste)
return {
isCellActive,
@ -442,7 +756,8 @@ export function useMultiSelect(
isCellSelected,
activeCell,
handleCellClick,
tbodyEl,
resetSelectedRange,
selectedRange,
makeActive,
}
}

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

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

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

@ -30,7 +30,7 @@
"leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0",
"nocodb-sdk": "0.108.1",
"nocodb-sdk": "file:../nocodb-sdk",
"papaparse": "^5.3.2",
"pinia": "^2.0.33",
"qrcode": "^1.5.1",
@ -101,8 +101,8 @@
"nuxt-windicss": "^2.5.0",
"prettier": "^2.7.1",
"sass": "^1.53.0",
"unplugin-icons": "^0.14.7",
"unplugin-vue-components": "^0.22.4",
"unplugin-icons": "^0.14.15",
"unplugin-vue-components": "^0.22.12",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-purge-icons": "^0.9.0",
"vitest": "^0.18.0",
@ -111,7 +111,6 @@
},
"../nocodb-sdk": {
"version": "0.108.1",
"extraneous": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -260,9 +259,9 @@
}
},
"node_modules/@antfu/install-pkg": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.1.0.tgz",
"integrity": "sha512-VaIJd3d1o7irZfK1U0nvBsHMyjkuyMP3HKYVV53z8DKyulkHKmjhhtccXO51WSPeeSHIeoJEoNOKavYpS7jkZw==",
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.1.1.tgz",
"integrity": "sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==",
"dev": true,
"dependencies": {
"execa": "^5.1.1",
@ -343,9 +342,9 @@
}
},
"node_modules/@antfu/utils": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.5.2.tgz",
"integrity": "sha512-CQkeV+oJxUazwjlHD0/3ZD08QWKuGQkhnrKo3e6ly5pd48VUpXbb77q0xMU4+vc2CkJnDS02Eq/M9ugyX20XZA==",
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.4.tgz",
"integrity": "sha512-qe8Nmh9rYI/HIspLSTwtbMFPj6dISG6+dJnOguTlPNXtCvS2uezdxscVBb7/3DrmNbQK49TDqpkSQ1chbRGdpQ==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/antfu"
@ -1235,23 +1234,22 @@
}
},
"node_modules/@iconify/types": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-1.1.0.tgz",
"integrity": "sha512-Jh0llaK2LRXQoYsorIH8maClebsnzTcve+7U3rQUSnC11X4jtPnFuyatqFLvMxZ8MLG8dB4zfHsbPfuvxluONw==",
"dev": true
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="
},
"node_modules/@iconify/utils": {
"version": "1.0.33",
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-1.0.33.tgz",
"integrity": "sha512-vGeAqo7aGPxOQmGdVoXFUOuyN+0V7Lcrx2EvaiRjxUD1x6Om0Tvq2bdm7E24l2Pz++4S0mWMCVFXe/17EtKImQ==",
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.1.5.tgz",
"integrity": "sha512-6MvDI+I6QMvXn5rK9KQGdpEE4mmLTcuQdLZEiX5N+uZB+vc4Yw9K1OtnOgkl8mp4d9X0UrILREyZgF1NUwUt+Q==",
"dev": true,
"dependencies": {
"@antfu/install-pkg": "^0.1.0",
"@antfu/utils": "^0.5.0",
"@iconify/types": "^1.1.0",
"@antfu/install-pkg": "^0.1.1",
"@antfu/utils": "^0.7.2",
"@iconify/types": "^2.0.0",
"debug": "^4.3.4",
"kolorist": "^1.5.1",
"local-pkg": "^0.4.1"
"kolorist": "^1.7.0",
"local-pkg": "^0.4.3"
}
},
"node_modules/@iconify/vue": {
@ -1268,11 +1266,6 @@
"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": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-2.2.2.tgz",
@ -3522,42 +3515,6 @@
"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": {
"version": "0.0.15",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz",
@ -4453,13 +4410,13 @@
}
},
"node_modules/@windicss/config": {
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/@windicss/config/-/config-1.8.7.tgz",
"integrity": "sha512-8n+/Y36j5L3rw2tgMdLjeGRuNV7VYfKoHoraLK6Bk9OJ1MTPd5vv7pekof/uOPWVV7WWjVeZ6CTO8SDbDDW3iw==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@windicss/config/-/config-1.9.0.tgz",
"integrity": "sha512-QO4+udbmDIgZwAi89tqUt5nGwBq3IgyELjLn83twZXiIqzOw+77ecCuM0oPSbzWmIbCqXq3wRQHd6Z1u5E/5zQ==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"jiti": "^1.14.0",
"jiti": "^1.18.2",
"windicss": "^3.5.6"
},
"funding": {
@ -4485,16 +4442,16 @@
"dev": true
},
"node_modules/@windicss/plugin-utils": {
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-1.8.7.tgz",
"integrity": "sha512-dfj95olNZyGFDPFMBvE5oq8hA5f0ooUJZjVdWlthS4ek4W1/xNOHDxB6ygWR8LE9zCOXZykApjt1LOhy9Ky2QA==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-1.9.0.tgz",
"integrity": "sha512-omAacM5ExIr9XBUI2z47CyCXJBke4imJZqXW41YgHhRLbahTngbScFk5yxa6dXivDXUpUKqasOPXBJgA4bhHCg==",
"dev": true,
"dependencies": {
"@antfu/utils": "^0.5.2",
"@windicss/config": "1.8.7",
"@antfu/utils": "^0.7.2",
"@windicss/config": "1.9.0",
"debug": "^4.3.4",
"fast-glob": "^3.2.11",
"magic-string": "^0.26.2",
"fast-glob": "^3.2.12",
"magic-string": "^0.30.0",
"micromatch": "^4.0.5",
"windicss": "^3.5.6"
},
@ -4502,6 +4459,18 @@
"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": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@ -4555,32 +4524,6 @@
"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": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.19.2.tgz",
@ -8776,6 +8719,7 @@
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"devOptional": true,
"funding": [
{
"type": "individual",
@ -10644,9 +10588,9 @@
}
},
"node_modules/jiti": {
"version": "1.17.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.17.2.tgz",
"integrity": "sha512-Xf0nU8+8wuiQpLcqdb2HRyHqYwGk2Pd+F7kstyp20ZuqTyCmB9dqpX2NxaxFc1kovraa2bG6c1RL3W7XfapiZg==",
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
"integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==",
"bin": {
"jiti": "bin/jiti.js"
}
@ -10932,9 +10876,9 @@
"dev": true
},
"node_modules/kolorist": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.5.1.tgz",
"integrity": "sha512-lxpCM3HTvquGxKGzHeknB/sUjuVoUElLlfYnXZT73K8geR9jQbroGlSCFBax9/0mpGoD3kzcMLnOlGQPJJNyqQ==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
"integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
"dev": true
},
"node_modules/langs": {
@ -12294,21 +12238,8 @@
}
},
"node_modules/nocodb-sdk": {
"version": "0.108.1",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.108.1.tgz",
"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"
}
"resolved": "../nocodb-sdk",
"link": true
},
"node_modules/node-abi": {
"version": "3.23.0",
@ -16366,18 +16297,18 @@
}
},
"node_modules/unplugin-icons": {
"version": "0.14.7",
"resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.14.7.tgz",
"integrity": "sha512-TrNnEdpaXMdiG5BsCgvU6cv/gSLYvIk1f8wGCGZmOo4wmi3nqYBuqIEuiXhmmyXdDZuRRpCaOzCnCYYZ5H7U8g==",
"version": "0.14.15",
"resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.14.15.tgz",
"integrity": "sha512-J6YBA+fUzVM2IZPXCK3Pnk36jYVwQ6lkjRgOnZaXNIxpMDsmwDqrE1AGJ0zUbfuEoOa90OBGc0OPfN1r+qlSIQ==",
"dev": true,
"dependencies": {
"@antfu/install-pkg": "^0.1.0",
"@antfu/utils": "^0.5.2",
"@iconify/utils": "^1.0.33",
"@antfu/install-pkg": "^0.1.1",
"@antfu/utils": "^0.7.2",
"@iconify/utils": "^2.0.3",
"debug": "^4.3.4",
"kolorist": "^1.5.1",
"local-pkg": "^0.4.1",
"unplugin": "^0.7.0"
"kolorist": "^1.6.0",
"local-pkg": "^0.4.2",
"unplugin": "^1.0.1"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
@ -16404,53 +16335,39 @@
}
},
"node_modules/unplugin-icons/node_modules/unplugin": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-0.7.1.tgz",
"integrity": "sha512-Z6hNDXDNh9aimMkPU1mEjtk+2ova8gh0y7rJeJdGH1vWZOHwF2lLQiQ/R97rv9ymmzEQXsR2fyMet72T8jy6ew==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz",
"integrity": "sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==",
"dev": true,
"dependencies": {
"acorn": "^8.7.1",
"acorn": "^8.8.2",
"chokidar": "^3.5.3",
"webpack-sources": "^3.2.3",
"webpack-virtual-modules": "^0.4.4"
},
"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
}
"webpack-virtual-modules": "^0.5.0"
}
},
"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": {
"version": "0.22.4",
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.22.4.tgz",
"integrity": "sha512-2rRZcM9OnJGXnYxQNfaceEYuPeVACcWySIjy8WBwIiN3onr980TmA3XE5pRJFt8zoQrUA+c46oyIq96noLqrEQ==",
"version": "0.22.12",
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.22.12.tgz",
"integrity": "sha512-FxyzsuBvMCYPIk+8cgscGBQ345tvwVu+qY5IhE++eorkyvA4Z1TiD/HCiim+Kbqozl10i4K+z+NCa2WO2jexRA==",
"dev": true,
"dependencies": {
"@antfu/utils": "^0.5.2",
"@rollup/pluginutils": "^4.2.1",
"@antfu/utils": "^0.7.2",
"@rollup/pluginutils": "^5.0.2",
"chokidar": "^3.5.3",
"debug": "^4.3.4",
"fast-glob": "^3.2.11",
"fast-glob": "^3.2.12",
"local-pkg": "^0.4.2",
"magic-string": "^0.26.2",
"minimatch": "^5.1.0",
"magic-string": "^0.27.0",
"minimatch": "^5.1.1",
"resolve": "^1.22.1",
"unplugin": "^0.9.0"
"unplugin": "^1.0.1"
},
"engines": {
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -16477,10 +16422,22 @@
"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": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
@ -16490,37 +16447,23 @@
}
},
"node_modules/unplugin-vue-components/node_modules/unplugin": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-0.9.3.tgz",
"integrity": "sha512-GWXxizZG+tobNs8fuGTCeilerkkfZTZax2iivuE4pxLaF9wTnPJHOq8tbLKDb5ohVb+2BXNjrU9xx59yWTUnuw==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz",
"integrity": "sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==",
"dev": true,
"dependencies": {
"acorn": "^8.8.0",
"acorn": "^8.8.2",
"chokidar": "^3.5.3",
"webpack-sources": "^3.2.3",
"webpack-virtual-modules": "^0.4.4"
},
"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
}
"webpack-virtual-modules": "^0.5.0"
}
},
"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": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/unstorage/-/unstorage-0.6.0.tgz",
@ -16669,9 +16612,9 @@
}
},
"node_modules/vite": {
"version": "2.9.15",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.9.15.tgz",
"integrity": "sha512-fzMt2jK4vQ3yK56te3Kqpkaeq9DkcZfBbzHwYpobasvgYmP2SoAr6Aic05CsB4CzCZbsDv4sujX3pkEGhLabVQ==",
"version": "2.9.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.9.16.tgz",
"integrity": "sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==",
"dev": true,
"dependencies": {
"esbuild": "^0.14.27",
@ -17090,9 +17033,9 @@
"dev": true
},
"node_modules/vite-node/node_modules/vite": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz",
"integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz",
"integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==",
"dev": true,
"dependencies": {
"esbuild": "^0.15.9",
@ -17110,6 +17053,7 @@
"fsevents": "~2.3.2"
},
"peerDependencies": {
"@types/node": ">= 14",
"less": "*",
"sass": "*",
"stylus": "*",
@ -17117,6 +17061,9 @@
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
@ -17260,21 +17207,21 @@
}
},
"node_modules/vite-plugin-windicss": {
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-1.8.7.tgz",
"integrity": "sha512-/zwQ8+RV+MSkbG0IGqsEma6r2R01NzN/aNpNjJD7VVAkxAptNznqDXOObFTskkWfZ+9m6KJZCOuCPgAFtQIzEA==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-1.9.0.tgz",
"integrity": "sha512-w0unPfcbVU5eaISAsFTLgIb41SLhmXoUF75Othu8NqFioe8+DEqiuvJ7/k/LRuEuvI8Rt/OKrY6cNzrB+dykaA==",
"dev": true,
"dependencies": {
"@windicss/plugin-utils": "1.8.7",
"@windicss/plugin-utils": "1.9.0",
"debug": "^4.3.4",
"kolorist": "^1.5.1",
"kolorist": "^1.8.0",
"windicss": "^3.5.6"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vite": "^2.0.1 || ^3.0.0"
"vite": "^2.0.1 || ^3.0.0 || ^4.0.0"
}
},
"node_modules/vite/node_modules/rollup": {
@ -18465,9 +18412,9 @@
}
},
"@antfu/install-pkg": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.1.0.tgz",
"integrity": "sha512-VaIJd3d1o7irZfK1U0nvBsHMyjkuyMP3HKYVV53z8DKyulkHKmjhhtccXO51WSPeeSHIeoJEoNOKavYpS7jkZw==",
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.1.1.tgz",
"integrity": "sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==",
"dev": true,
"requires": {
"execa": "^5.1.1",
@ -18520,9 +18467,9 @@
}
},
"@antfu/utils": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.5.2.tgz",
"integrity": "sha512-CQkeV+oJxUazwjlHD0/3ZD08QWKuGQkhnrKo3e6ly5pd48VUpXbb77q0xMU4+vc2CkJnDS02Eq/M9ugyX20XZA==",
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.4.tgz",
"integrity": "sha512-qe8Nmh9rYI/HIspLSTwtbMFPj6dISG6+dJnOguTlPNXtCvS2uezdxscVBb7/3DrmNbQK49TDqpkSQ1chbRGdpQ==",
"dev": true
},
"@babel/code-frame": {
@ -19214,23 +19161,22 @@
"dev": true
},
"@iconify/types": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-1.1.0.tgz",
"integrity": "sha512-Jh0llaK2LRXQoYsorIH8maClebsnzTcve+7U3rQUSnC11X4jtPnFuyatqFLvMxZ8MLG8dB4zfHsbPfuvxluONw==",
"dev": true
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="
},
"@iconify/utils": {
"version": "1.0.33",
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-1.0.33.tgz",
"integrity": "sha512-vGeAqo7aGPxOQmGdVoXFUOuyN+0V7Lcrx2EvaiRjxUD1x6Om0Tvq2bdm7E24l2Pz++4S0mWMCVFXe/17EtKImQ==",
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.1.5.tgz",
"integrity": "sha512-6MvDI+I6QMvXn5rK9KQGdpEE4mmLTcuQdLZEiX5N+uZB+vc4Yw9K1OtnOgkl8mp4d9X0UrILREyZgF1NUwUt+Q==",
"dev": true,
"requires": {
"@antfu/install-pkg": "^0.1.0",
"@antfu/utils": "^0.5.0",
"@iconify/types": "^1.1.0",
"@antfu/install-pkg": "^0.1.1",
"@antfu/utils": "^0.7.2",
"@iconify/types": "^2.0.0",
"debug": "^4.3.4",
"kolorist": "^1.5.1",
"local-pkg": "^0.4.1"
"kolorist": "^1.7.0",
"local-pkg": "^0.4.3"
}
},
"@iconify/vue": {
@ -19239,13 +19185,6 @@
"integrity": "sha512-k4VwcSQpGqJpoyqENRRviFuXlVcquLvQ6BKLNJ6o2amZo7u+3HyALSO79Xyz7Sg68szQGstOk6weaKUF0DJbog==",
"requires": {
"@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": {
@ -20931,41 +20870,6 @@
"dev": true,
"requires": {
"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": {
@ -21683,13 +21587,13 @@
}
},
"@windicss/config": {
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/@windicss/config/-/config-1.8.7.tgz",
"integrity": "sha512-8n+/Y36j5L3rw2tgMdLjeGRuNV7VYfKoHoraLK6Bk9OJ1MTPd5vv7pekof/uOPWVV7WWjVeZ6CTO8SDbDDW3iw==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@windicss/config/-/config-1.9.0.tgz",
"integrity": "sha512-QO4+udbmDIgZwAi89tqUt5nGwBq3IgyELjLn83twZXiIqzOw+77ecCuM0oPSbzWmIbCqXq3wRQHd6Z1u5E/5zQ==",
"dev": true,
"requires": {
"debug": "^4.3.4",
"jiti": "^1.14.0",
"jiti": "^1.18.2",
"windicss": "^3.5.6"
}
},
@ -21712,18 +21616,29 @@
"dev": true
},
"@windicss/plugin-utils": {
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-1.8.7.tgz",
"integrity": "sha512-dfj95olNZyGFDPFMBvE5oq8hA5f0ooUJZjVdWlthS4ek4W1/xNOHDxB6ygWR8LE9zCOXZykApjt1LOhy9Ky2QA==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-1.9.0.tgz",
"integrity": "sha512-omAacM5ExIr9XBUI2z47CyCXJBke4imJZqXW41YgHhRLbahTngbScFk5yxa6dXivDXUpUKqasOPXBJgA4bhHCg==",
"dev": true,
"requires": {
"@antfu/utils": "^0.5.2",
"@windicss/config": "1.8.7",
"@antfu/utils": "^0.7.2",
"@windicss/config": "1.9.0",
"debug": "^4.3.4",
"fast-glob": "^3.2.11",
"magic-string": "^0.26.2",
"fast-glob": "^3.2.12",
"magic-string": "^0.30.0",
"micromatch": "^4.0.5",
"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": {
@ -21765,15 +21680,6 @@
"dev": true,
"requires": {
"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": {
"version": "1.15.1",
"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": {
"version": "4.0.0",
@ -26175,9 +26082,9 @@
}
},
"jiti": {
"version": "1.17.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.17.2.tgz",
"integrity": "sha512-Xf0nU8+8wuiQpLcqdb2HRyHqYwGk2Pd+F7kstyp20ZuqTyCmB9dqpX2NxaxFc1kovraa2bG6c1RL3W7XfapiZg=="
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
"integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg=="
},
"js-tokens": {
"version": "4.0.0",
@ -26355,9 +26262,9 @@
"dev": true
},
"kolorist": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.5.1.tgz",
"integrity": "sha512-lxpCM3HTvquGxKGzHeknB/sUjuVoUElLlfYnXZT73K8geR9jQbroGlSCFBax9/0mpGoD3kzcMLnOlGQPJJNyqQ==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
"integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
"dev": true
},
"langs": {
@ -27360,22 +27267,22 @@
}
},
"nocodb-sdk": {
"version": "0.108.1",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.108.1.tgz",
"integrity": "sha512-XJZjDNNEBSJV9+zldpRXX5x2iMO9pvrnWlbphMQ5GlmtHdcLQXjSr6EP0B3QxCFQzdwYyEpcmEY+/qJGtEyRjA==",
"version": "file:../nocodb-sdk",
"requires": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.1",
"jsep": "^1.3.6"
},
"dependencies": {
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
}
"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-abi": {
@ -30407,52 +30314,75 @@
}
},
"unplugin-icons": {
"version": "0.14.7",
"resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.14.7.tgz",
"integrity": "sha512-TrNnEdpaXMdiG5BsCgvU6cv/gSLYvIk1f8wGCGZmOo4wmi3nqYBuqIEuiXhmmyXdDZuRRpCaOzCnCYYZ5H7U8g==",
"version": "0.14.15",
"resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.14.15.tgz",
"integrity": "sha512-J6YBA+fUzVM2IZPXCK3Pnk36jYVwQ6lkjRgOnZaXNIxpMDsmwDqrE1AGJ0zUbfuEoOa90OBGc0OPfN1r+qlSIQ==",
"dev": true,
"requires": {
"@antfu/install-pkg": "^0.1.0",
"@antfu/utils": "^0.5.2",
"@iconify/utils": "^1.0.33",
"@antfu/install-pkg": "^0.1.1",
"@antfu/utils": "^0.7.2",
"@iconify/utils": "^2.0.3",
"debug": "^4.3.4",
"kolorist": "^1.5.1",
"local-pkg": "^0.4.1",
"unplugin": "^0.7.0"
"kolorist": "^1.6.0",
"local-pkg": "^0.4.2",
"unplugin": "^1.0.1"
},
"dependencies": {
"unplugin": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-0.7.1.tgz",
"integrity": "sha512-Z6hNDXDNh9aimMkPU1mEjtk+2ova8gh0y7rJeJdGH1vWZOHwF2lLQiQ/R97rv9ymmzEQXsR2fyMet72T8jy6ew==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz",
"integrity": "sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==",
"dev": true,
"requires": {
"acorn": "^8.7.1",
"acorn": "^8.8.2",
"chokidar": "^3.5.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": {
"version": "0.22.4",
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.22.4.tgz",
"integrity": "sha512-2rRZcM9OnJGXnYxQNfaceEYuPeVACcWySIjy8WBwIiN3onr980TmA3XE5pRJFt8zoQrUA+c46oyIq96noLqrEQ==",
"version": "0.22.12",
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.22.12.tgz",
"integrity": "sha512-FxyzsuBvMCYPIk+8cgscGBQ345tvwVu+qY5IhE++eorkyvA4Z1TiD/HCiim+Kbqozl10i4K+z+NCa2WO2jexRA==",
"dev": true,
"requires": {
"@antfu/utils": "^0.5.2",
"@rollup/pluginutils": "^4.2.1",
"@antfu/utils": "^0.7.2",
"@rollup/pluginutils": "^5.0.2",
"chokidar": "^3.5.3",
"debug": "^4.3.4",
"fast-glob": "^3.2.11",
"fast-glob": "^3.2.12",
"local-pkg": "^0.4.2",
"magic-string": "^0.26.2",
"minimatch": "^5.1.0",
"magic-string": "^0.27.0",
"minimatch": "^5.1.1",
"resolve": "^1.22.1",
"unplugin": "^0.9.0"
"unplugin": "^1.0.1"
},
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -30462,26 +30392,41 @@
"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": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"requires": {
"brace-expansion": "^2.0.1"
}
},
"unplugin": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-0.9.3.tgz",
"integrity": "sha512-GWXxizZG+tobNs8fuGTCeilerkkfZTZax2iivuE4pxLaF9wTnPJHOq8tbLKDb5ohVb+2BXNjrU9xx59yWTUnuw==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz",
"integrity": "sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==",
"dev": true,
"requires": {
"acorn": "^8.8.0",
"acorn": "^8.8.2",
"chokidar": "^3.5.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=="
},
"vite": {
"version": "2.9.15",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.9.15.tgz",
"integrity": "sha512-fzMt2jK4vQ3yK56te3Kqpkaeq9DkcZfBbzHwYpobasvgYmP2SoAr6Aic05CsB4CzCZbsDv4sujX3pkEGhLabVQ==",
"version": "2.9.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.9.16.tgz",
"integrity": "sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==",
"dev": true,
"requires": {
"esbuild": "^0.14.27",
@ -30827,9 +30772,9 @@
"dev": true
},
"vite": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz",
"integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz",
"integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==",
"dev": true,
"requires": {
"esbuild": "^0.15.9",
@ -30916,14 +30861,14 @@
}
},
"vite-plugin-windicss": {
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-1.8.7.tgz",
"integrity": "sha512-/zwQ8+RV+MSkbG0IGqsEma6r2R01NzN/aNpNjJD7VVAkxAptNznqDXOObFTskkWfZ+9m6KJZCOuCPgAFtQIzEA==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-1.9.0.tgz",
"integrity": "sha512-w0unPfcbVU5eaISAsFTLgIb41SLhmXoUF75Othu8NqFioe8+DEqiuvJ7/k/LRuEuvI8Rt/OKrY6cNzrB+dykaA==",
"dev": true,
"requires": {
"@windicss/plugin-utils": "1.8.7",
"@windicss/plugin-utils": "1.9.0",
"debug": "^4.3.4",
"kolorist": "^1.5.1",
"kolorist": "^1.8.0",
"windicss": "^3.5.6"
}
},

6
packages/nc-gui/package.json

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

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

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

15
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)"
>
<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"
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">
<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 }}
</h2>
@ -180,7 +189,7 @@ const onDecode = async (scannedCodeValue: string) => {
</a-button>
</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">
{{ error.$message }}
</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"
>
<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 }}
</h1>
<h2
v-if="sharedFormView.subheading && sharedFormView.subheading !== ''"
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"
>
{{ sharedFormView?.subheading }}
@ -287,7 +291,7 @@ onMounted(() => {
<LazySmartsheetVirtualCell
v-if="isVirtualCol(field)"
v-model="formState[field.title]"
class="mt-0 nc-input"
class="mt-0 nc-input h-auto"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field"
@ -296,7 +300,7 @@ onMounted(() => {
<LazySmartsheetCell
v-else
v-model="formState[field.title]"
class="nc-input"
class="nc-input h-auto"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field"
:edit-enabled="editEnabled[index]"
@ -305,11 +309,10 @@ onMounted(() => {
@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">
{{ error.$message }}
</div>
<div
class="block text-[14px]"
:class="field.uidt === UITypes.Checkbox ? 'text-center' : ''"
@ -350,6 +353,7 @@ onMounted(() => {
:mouse-enter-delay="0.25"
:mouse-leave-delay="0"
>
<!-- Ok button for question -->
<button
class="bg-opacity-100 scaling-btn flex items-center gap-1"
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
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
#### Available Database Types
@ -261,7 +261,7 @@ For more about Link To Another Record, please visit [here](./link-to-another-rec
### 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
@ -289,7 +289,7 @@ Since it's a virtual column, the cell content (Barcode) cannot be changed direct
### 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

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

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

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

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

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

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

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

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

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

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

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

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

30
packages/nocodb/package-lock.json generated

@ -83,7 +83,7 @@
"nc-lib-gui": "0.108.1",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nocodb-sdk": "0.108.1",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"os-locale": "^6.0.2",
@ -191,7 +191,6 @@
},
"../nocodb-sdk": {
"version": "0.108.1",
"extraneous": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -13207,13 +13206,8 @@
}
},
"node_modules/nocodb-sdk": {
"version": "0.108.1",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.108.1.tgz",
"integrity": "sha512-XJZjDNNEBSJV9+zldpRXX5x2iMO9pvrnWlbphMQ5GlmtHdcLQXjSr6EP0B3QxCFQzdwYyEpcmEY+/qJGtEyRjA==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
}
"resolved": "../nocodb-sdk",
"link": true
},
"node_modules/node-abort-controller": {
"version": "3.1.1",
@ -28485,12 +28479,22 @@
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="
},
"nocodb-sdk": {
"version": "0.108.1",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.108.1.tgz",
"integrity": "sha512-XJZjDNNEBSJV9+zldpRXX5x2iMO9pvrnWlbphMQ5GlmtHdcLQXjSr6EP0B3QxCFQzdwYyEpcmEY+/qJGtEyRjA==",
"version": "file:../nocodb-sdk",
"requires": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.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": {

2
packages/nocodb/package.json

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

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

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

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

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

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

@ -172,7 +172,7 @@ class BaseModelSqlv2 {
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> {

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

@ -570,13 +570,25 @@ export default class Model implements TableType {
// get existing cache
const key = `${CacheScope.MODEL}:${tableId}`;
const o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
let oldModel = { ...o };
// update alias
if (o) {
o.title = title;
o.table_name = table_name;
// set cache
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
return await ncMeta.metaUpdate(
null,

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

@ -8,6 +8,7 @@ import {
import { extractProps } from '../helpers/extractProps';
import NocoCache from '../cache/NocoCache';
import Base from './/Base';
import { ProjectUser } from './index';
import type { BoolType, MetaType, ProjectType } from 'nocodb-sdk';
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
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 });
for (const base of bases) {
await base.delete(ncMeta);

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

@ -1,3 +1,4 @@
import { title } from 'process';
import { isSystemColumn, UITypes, ViewTypes } from 'nocodb-sdk';
import Noco from '../Noco';
import {
@ -165,13 +166,19 @@ export default class View implements ViewType {
],
},
);
if (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,
);
await NocoCache.set(`${CacheScope.VIEW}:${fk_model_id}:${view.id}`, view);
}
return view && new View(view);
}
return viewId && this.get(viewId?.id || viewId);
@ -952,6 +959,7 @@ export default class View implements ViewType {
// get existing cache
const key = `${CacheScope.VIEW}:${viewId}`;
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
let oldView = { ...o };
if (o) {
// update data
o = {
@ -963,8 +971,15 @@ export default class View implements ViewType {
}
// set cache
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' in 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,
}));
if (!model) NcError.notFound('Table not found');
if (param.viewName && !view) NcError.notFound('View not found');
return { model, view };
}

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

@ -32,7 +32,8 @@ import type {
export class UsersService {
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, {
email,
});
@ -50,7 +51,7 @@ export class UsersService {
email: string;
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({

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

@ -282,7 +282,7 @@ function baseModelSqlTests() {
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, {}));
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({
title: 'Test',
})
.expect(400);
.expect(404);
};
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 {
const app = require('express')();
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));
console.log(`Visit : localhost:${process.env.PORT}/dashboard`)
console.log(`Visit : localhost:${port}/dashboard`)
} catch(e) {
console.log(e)
}

2
tests/playwright/package-lock.json generated

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

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 }) {
if (mode === 'dragDrop') {
const src = await this.get().locator(`[data-testid="nc-form-hidden-column-${field}"]`);
const dst = await this.get().locator(`.nc-form-drag-Country`);
const src = await this.get().locator(`[data-testid="nc-form-hidden-column-${field}"] > div.ant-card-body`);
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);
} else if (mode === 'clickField') {
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();
await expect(childCards).toEqual(cardCount);
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: delete
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();
await expect(childCards).toEqual(cardTitle.length);
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' });
const rowCount = await this.get().locator('.nc-grid-row').count();
await (await this.get().locator('.nc-grid-add-new-cell').elementHandle())?.waitForElementState('stable');
await this.get().locator('.nc-grid-add-new-cell').click();
const addNewRowButton: Locator = await this.rootPage.locator(`[data-testid="nc-grid-add-new-row"]`);
await addNewRowButton.waitFor({ state: 'visible' });
await addNewRowButton.click();
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
await this.rootPage.locator('text=Delete Row').click();
await this.rootPage.locator('text=Yes').click();
// todo: improve selector
await this.rootPage
.locator('span.ant-dropdown-menu-title-content > nc-project-menu-item')
@ -166,11 +167,13 @@ export class GridPage extends BasePage {
await this.dashboard.waitForLoaderToDisappear();
}
async addRowRightClickMenu(index: number) {
async addRowRightClickMenu(index: number, columnHeader = 'Title') {
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
await this.rootPage.locator('text=Insert New Row').click();
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 BasePage from '../../../Base';
import { expect } from '@playwright/test';
export class DateCellPageObject extends BasePage {
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({
// date in format `YYYY-MM-DD`
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 }) {
await this.get({ index, columnHeader }).scrollIntoViewIfNeeded();
await expect(await this.get({ index, columnHeader }).locator(`div[role="radio"][aria-checked="true"]`)).toHaveCount(
rating
);
async verify({ index, columnHeader, rating }: { index: number; columnHeader: string; rating: number }) {
const cell = await this.get({ index, columnHeader });
await cell.scrollIntoViewIfNeeded();
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 {
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 {
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
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]);
}

79
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();
}
// 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({
title,
operation,
@ -65,7 +134,7 @@ export class ToolbarFilterPage extends BasePage {
await this.rootPage.locator('.nc-filter-field-select').last().click();
if (skipWaitingResponse) {
this.rootPage
await this.rootPage
.locator('div.ant-select-dropdown.nc-dropdown-toolbar-field-list')
.locator(`div[label="${title}"]:visible`)
.click();
@ -88,7 +157,7 @@ export class ToolbarFilterPage extends BasePage {
// first() : filter list has >, >=
if (skipWaitingResponse) {
this.rootPage
await this.rootPage
.locator('.nc-dropdown-filter-comp-op')
.locator(`.ant-select-item:has-text("${operation}")`)
.first()
@ -117,7 +186,7 @@ export class ToolbarFilterPage extends BasePage {
// first() : filter list has >, >=
if (skipWaitingResponse) {
this.rootPage
await this.rootPage
.locator('.nc-dropdown-filter-comp-sub-op')
.locator(`.ant-select-item:has-text("${subOperation}")`)
.first()
@ -167,7 +236,7 @@ export class ToolbarFilterPage extends BasePage {
await this.rootPage.locator(`.ant-picker-dropdown:visible`);
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 {
await this.waitForResponse({
uiAction: () => this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click(),
@ -188,7 +257,7 @@ export class ToolbarFilterPage extends BasePage {
break;
case UITypes.Duration:
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 {
await this.waitForResponse({
uiAction: () => this.get().locator('.nc-filter-value-select').locator('input').fill(value),

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

@ -30,7 +30,7 @@ test.describe('Verify cell selection', () => {
start: { index: 0, columnHeader: 'FirstName' },
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();
// #3 when copied with mouse, it copies correct text
@ -40,7 +40,7 @@ test.describe('Verify cell selection', () => {
end: { index: 1, columnHeader: 'LastName' },
});
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();
});

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 { DashboardPage } from '../../pages/Dashboard';
import setup from '../../setup';
import { isPg } from '../../setup/db';
test.describe('Relational Columns', () => {
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();
});
});
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,
});
});
});

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

@ -104,8 +104,18 @@ test.describe('Verify shortcuts', () => {
await page.waitForTimeout(2000);
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({
baseURL: `http://localhost:8080/`,
headers: {
@ -114,98 +124,26 @@ test.describe('Verify shortcuts', () => {
});
const columns = [
{
column_name: 'Id',
title: 'Id',
uidt: UITypes.ID,
},
{
column_name: 'SingleLineText',
title: 'SingleLineText',
uidt: UITypes.SingleLineText,
},
{
column_name: 'LongText',
title: 'LongText',
uidt: UITypes.LongText,
},
{
column_name: 'Number',
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,
},
{ column_name: 'Id', uidt: UITypes.ID },
{ column_name: 'SingleLineText', uidt: UITypes.SingleLineText },
{ column_name: 'LongText', uidt: UITypes.LongText },
{ column_name: 'Number', uidt: UITypes.Number },
{ column_name: 'PhoneNumber', uidt: UITypes.PhoneNumber },
{ column_name: 'Email', uidt: UITypes.Email },
{ column_name: 'URL', uidt: UITypes.URL },
{ column_name: 'Decimal', uidt: UITypes.Decimal },
{ column_name: 'Percent', uidt: UITypes.Percent },
{ column_name: 'Currency', uidt: UITypes.Currency },
{ column_name: 'Duration', uidt: UITypes.Duration },
{ column_name: 'SingleSelect', uidt: UITypes.SingleSelect, dtxp: "'Option1','Option2'" },
{ column_name: 'MultiSelect', uidt: UITypes.MultiSelect, dtxp: "'Option1','Option2'" },
{ column_name: 'Rating', uidt: UITypes.Rating },
{ column_name: 'Checkbox', uidt: UITypes.Checkbox },
{ column_name: 'Date', uidt: UITypes.Date },
{ column_name: 'Attachment', uidt: UITypes.Attachment },
];
const today = new Date().toISOString().slice(0, 10);
const record = {
Id: 1,
SingleLineText: 'SingleLineText',
LongText: 'LongText',
SingleSelect: 'Option1',
@ -239,10 +177,8 @@ test.describe('Verify shortcuts', () => {
// reload page
await dashboard.rootPage.reload();
// close 'Team & Auth' tab
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.openTable({ title: 'Sheet1' });
// ########################################
@ -252,147 +188,201 @@ test.describe('Verify shortcuts', () => {
columnHeader: 'Attachment',
filePath: `${process.cwd()}/fixtures/sampleFiles/1.json`,
});
// ########################################
await dashboard.grid.cell.copyToClipboard({
index: 0,
columnHeader: 'SingleLineText',
});
expect(await dashboard.grid.cell.getClipboardText()).toBe('SingleLineText');
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 },
];
await dashboard.grid.cell.copyToClipboard({
index: 0,
columnHeader: 'LongText',
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,
});
expect(await dashboard.grid.cell.getClipboardText()).toBe('LongText');
}
}
}
await dashboard.grid.cell.copyToClipboard(
{
index: 0,
columnHeader: 'SingleSelect',
},
{ position: { x: 1, y: 1 } }
);
expect(await dashboard.grid.cell.getClipboardText()).toBe('Option1');
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 } },
];
for (const { type, value, options } of responseTable) {
await dashboard.grid.cell.copyToClipboard(
{
index: 0,
columnHeader: 'MultiSelect',
index: rowIndex,
columnHeader: type,
},
{ position: { x: 1, y: 1 } }
);
expect(await dashboard.grid.cell.getClipboardText()).toContain('Option1');
expect(await dashboard.grid.cell.getClipboardText()).toContain('Option2');
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({
index: 0,
columnHeader: 'SingleLineText',
test('single cell- all data types', async () => {
await verifyClipContents({ rowIndex: 0 });
});
expect(await dashboard.grid.cell.getClipboardText()).toBe('SingleLineText');
await dashboard.grid.cell.copyToClipboard({
index: 0,
columnHeader: 'Number',
});
expect(await dashboard.grid.cell.getClipboardText()).toBe('123');
test('multiple cells - horizontal, all data types', async ({ page }) => {
// click first cell, press `Ctrl A` and `Ctrl C`
await grid.cell.click({ index: 0, columnHeader: 'Id' });
await page.keyboard.press((await grid.isMacOs()) ? 'Meta+a' : 'Control+a');
await page.keyboard.press((await grid.isMacOs()) ? 'Meta+c' : 'Control+c');
await dashboard.grid.cell.copyToClipboard({
index: 0,
columnHeader: 'PhoneNumber',
/////////////////////////////////////////////////////////////////////////
// horizontal multiple cells selection : copy paste
// add new row, click on first cell, paste
await grid.addNewRow({ index: 1, columnHeader: 'SingleLineText', value: 'aaa' });
await dashboard.rootPage.waitForTimeout(1000);
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 });
// reload page
await dashboard.rootPage.reload();
await dashboard.grid.verifyRowCount({ count: 2 });
});
expect(await dashboard.grid.cell.getClipboardText()).toBe('987654321');
await dashboard.grid.cell.copyToClipboard(
{
index: 0,
columnHeader: 'Email',
},
{ position: { x: 1, y: 1 } }
);
expect(await dashboard.grid.cell.getClipboardText()).toBe('test@example.com');
test('multiple cells - vertical', async ({ page }) => {
let cellText: string[] = ['aaa', 'bbb', 'ccc', 'ddd', 'eee'];
for (let i = 1; i <= 5; i++) {
await grid.addNewRow({ index: i, columnHeader: 'SingleLineText', value: cellText[i - 1] });
}
await dashboard.grid.cell.copyToClipboard(
{
index: 0,
columnHeader: 'URL',
},
{ position: { x: 1, y: 1 } }
);
expect(await dashboard.grid.cell.getClipboardText()).toBe('nocodb.com');
await grid.cell.click({ index: 1, columnHeader: 'SingleLineText' });
await page.keyboard.press('Shift+ArrowDown');
await page.keyboard.press('Shift+ArrowDown');
await page.keyboard.press('Shift+ArrowDown');
await page.keyboard.press('Shift+ArrowDown');
await page.keyboard.press('Shift+ArrowDown');
await dashboard.grid.cell.copyToClipboard({
index: 0,
columnHeader: 'Decimal',
});
expect(await dashboard.grid.cell.getClipboardText()).toBe('1.12');
await page.keyboard.press((await grid.isMacOs()) ? 'Meta+c' : 'Control+c');
await grid.cell.click({ index: 1, columnHeader: 'LongText' });
await page.keyboard.press((await grid.isMacOs()) ? 'Meta+v' : 'Control+v');
await dashboard.grid.cell.copyToClipboard({
index: 0,
columnHeader: 'Percent',
});
expect(await dashboard.grid.cell.getClipboardText()).toBe('80');
// reload page
await dashboard.rootPage.reload();
await dashboard.grid.cell.copyToClipboard({
index: 0,
columnHeader: 'Currency',
});
// convert from string to integer
expect(parseInt(await dashboard.grid.cell.getClipboardText())).toBe(20);
// verify copied data
for (let i = 1; i <= 5; i++) {
await grid.cell.verify({ index: i, columnHeader: 'LongText', value: cellText[i - 1] });
}
await dashboard.grid.cell.copyToClipboard({
index: 0,
columnHeader: 'Duration',
});
expect(parseInt(await dashboard.grid.cell.getClipboardText())).toBe(480);
// Block selection
await grid.cell.click({ index: 1, columnHeader: 'SingleLineText' });
await page.keyboard.press('Shift+ArrowDown');
await page.keyboard.press('Shift+ArrowDown');
await page.keyboard.press('Shift+ArrowRight');
await page.keyboard.press((await grid.isMacOs()) ? 'Meta+c' : 'Control+c');
await grid.cell.click({ index: 4, columnHeader: 'SingleLineText' });
await page.keyboard.press((await grid.isMacOs()) ? 'Meta+v' : 'Control+v');
await dashboard.grid.cell.copyToClipboard(
{
index: 0,
columnHeader: 'Rating',
},
{ position: { x: 1, y: 1 } }
);
expect(await dashboard.grid.cell.getClipboardText()).toBe('4');
// reload page
await dashboard.rootPage.reload();
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');
// verify copied data
for (let i = 4; i <= 5; i++) {
await grid.cell.verify({ index: i, columnHeader: 'SingleLineText', value: cellText[i - 4] });
await grid.cell.verify({ index: i, columnHeader: 'LongText', value: cellText[i - 4] });
}
await dashboard.grid.cell.click({
index: 0,
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({
index: 0,
columnHeader: 'Date',
});
expect(await dashboard.grid.cell.getClipboardText()).toBe(today);
// Meta for block selection
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+c' : 'Control+c');
await grid.cell.click({ index: 1, columnHeader: 'Email' });
await page.keyboard.press((await grid.isMacOs()) ? 'Meta+v' : 'Control+v');
await dashboard.grid.cell.copyToClipboard(
{
index: 0,
columnHeader: 'Attachment',
},
{ position: { x: 1, y: 1 } }
);
const attachmentsInfo = JSON.parse(await dashboard.grid.cell.getClipboardText());
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 () => {
// define an array of row heights
const rowHeight = [
{ title: 'Short', height: '1.5rem' },
{ title: 'Medium', height: '3rem' },
{ title: 'Tall', height: '6rem' },
{ title: 'Extra', height: '9rem' },
{ title: 'Short', height: '1.8rem' },
{ title: 'Medium', height: '3.6rem' },
{ title: 'Tall', height: '7.2rem' },
{ title: 'Extra', height: '10.8rem' },
];
// close 'Team & Auth' tab

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

@ -307,26 +307,26 @@ test.describe('Undo Redo', () => {
const timeOut = 200;
await verifyRowHeight({ height: '1.5rem' });
await verifyRowHeight({ height: '1.8rem' });
// set row height & verify
await toolbar.clickRowHeight();
await toolbar.rowHeight.click({ title: 'Tall' });
await new Promise(resolve => setTimeout(resolve, timeOut));
await verifyRowHeight({ height: '6rem' });
await verifyRowHeight({ height: '7.2rem' });
await toolbar.clickRowHeight();
await toolbar.rowHeight.click({ title: 'Medium' });
await new Promise(resolve => setTimeout(resolve, timeOut));
await verifyRowHeight({ height: '3rem' });
await verifyRowHeight({ height: '3.6rem' });
await undo({ page });
await new Promise(resolve => setTimeout(resolve, timeOut));
await verifyRowHeight({ height: '6rem' });
await verifyRowHeight({ height: '7.2rem' });
await undo({ page });
await new Promise(resolve => setTimeout(resolve, timeOut));
await verifyRowHeight({ height: '1.5rem' });
await verifyRowHeight({ height: '1.8rem' });
});
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)
await form.addField({ field: 'City List', mode: 'dragDrop' });
await form.verifyFormViewFieldsOrder({
fields: ['LastUpdate', 'City List', 'Country'],
fields: ['LastUpdate', 'Country', 'City List'],
});
// remove & verify (hide field button)

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

@ -60,4 +60,12 @@ function getBrowserTimezoneOffset() {
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