Browse Source

Merge branch 'develop' into geodata-prototyping-restart

pull/4140/head
flisowna 2 years ago
parent
commit
7ccc114434
  1. 3
      packages/nc-gui/components.d.ts
  2. 21
      packages/nc-gui/components/cell/Checkbox.vue
  3. 5
      packages/nc-gui/components/cell/Currency.vue
  4. 37
      packages/nc-gui/components/cell/DatePicker.vue
  5. 27
      packages/nc-gui/components/cell/DateTimePicker.vue
  6. 5
      packages/nc-gui/components/cell/Decimal.vue
  7. 5
      packages/nc-gui/components/cell/Duration.vue
  8. 13
      packages/nc-gui/components/cell/Email.vue
  9. 5
      packages/nc-gui/components/cell/Float.vue
  10. 5
      packages/nc-gui/components/cell/Integer.vue
  11. 45
      packages/nc-gui/components/cell/MultiSelect.vue
  12. 5
      packages/nc-gui/components/cell/Percent.vue
  13. 19
      packages/nc-gui/components/cell/Rating.vue
  14. 37
      packages/nc-gui/components/cell/SingleSelect.vue
  15. 5
      packages/nc-gui/components/cell/Text.vue
  16. 5
      packages/nc-gui/components/cell/TextArea.vue
  17. 27
      packages/nc-gui/components/cell/TimePicker.vue
  18. 5
      packages/nc-gui/components/cell/Url.vue
  19. 29
      packages/nc-gui/components/cell/YearPicker.vue
  20. 19
      packages/nc-gui/components/cell/attachment/index.vue
  21. 1
      packages/nc-gui/components/smartsheet/Cell.vue
  22. 168
      packages/nc-gui/components/smartsheet/Grid.vue
  23. 1
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  24. 10
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  25. 36
      packages/nc-gui/components/virtual-cell/Formula.vue
  26. 10
      packages/nc-gui/components/virtual-cell/HasMany.vue
  27. 95
      packages/nc-gui/components/virtual-cell/Lookup.vue
  28. 11
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  29. 37
      packages/nc-gui/components/virtual-cell/Rollup.vue
  30. 57
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  31. 29
      packages/nc-gui/composables/useMultiSelect/index.ts
  32. 21
      packages/nc-gui/composables/useSelectedCellKeyupListener/index.ts
  33. 1
      packages/nc-gui/composables/useTable.ts
  34. 2
      packages/nc-gui/utils/browserUtils.ts
  35. 9
      packages/nc-gui/utils/columnUtils.ts
  36. 1
      packages/nc-gui/utils/index.ts
  37. 3
      packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts
  38. 36
      packages/nocodb/package-lock.json
  39. 2
      packages/nocodb/src/lib/Noco.ts
  40. 6
      packages/nocodb/src/lib/plugins/backblaze/Backblaze.ts
  41. 6
      packages/nocodb/src/lib/plugins/linode/LinodeObjectStorage.ts
  42. 6
      packages/nocodb/src/lib/plugins/ovhCloud/OvhCloud.ts
  43. 6
      packages/nocodb/src/lib/plugins/s3/S3.ts
  44. 6
      packages/nocodb/src/lib/plugins/scaleway/ScalewayObjectStorage.ts
  45. 6
      packages/nocodb/src/lib/plugins/spaces/Spaces.ts
  46. 6
      packages/nocodb/src/lib/plugins/upcloud/UpoCloud.ts
  47. 6
      packages/nocodb/src/lib/plugins/vultr/Vultr.ts
  48. 2
      packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts
  49. 14
      packages/nocodb/src/lib/version-upgrader/ncDataTypesUpgrader.ts
  50. 1
      packages/nocodb/src/run/docker.ts
  51. 1
      packages/nocodb/src/run/dockerRunMysql.ts
  52. 1
      packages/nocodb/src/run/dockerRunPG.ts
  53. 1
      packages/nocodb/src/run/dockerRunPG_CyQuick.ts
  54. 16
      tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts

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

@ -108,6 +108,8 @@ declare module '@vue/runtime-core' {
MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default']
MdiAccountOutline: typeof import('~icons/mdi/account-outline')['default']
MdiAccountPlusOutline: typeof import('~icons/mdi/account-plus-outline')['default']
MdiAccountSupervisorOutline: typeof import('~icons/mdi/account-supervisor-outline')['default']
MdiAdd: typeof import('~icons/mdi/add')['default']
MdiAlpha: typeof import('~icons/mdi/alpha')['default']
MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default']
MdiApi: typeof import('~icons/mdi/api')['default']
@ -202,6 +204,7 @@ declare module '@vue/runtime-core' {
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default']
MdiScriptTextKeyOutline: typeof import('~icons/mdi/script-text-key-outline')['default']
MdiScriptTextOutline: typeof import('~icons/mdi/script-text-outline')['default']
MdiShieldKeyOutline: typeof import('~icons/mdi/shield-key-outline')['default']
MdiSlack: typeof import('~icons/mdi/slack')['default']
MdiSort: typeof import('~icons/mdi/sort')['default']
MdiStar: typeof import('~icons/mdi/star')['default']

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

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ColumnInj, IsFormInj, ReadonlyInj, getMdiIcon, inject } from '#imports'
import { ActiveCellInj, ColumnInj, IsFormInj, ReadonlyInj, getMdiIcon, inject, useSelectedCellKeyupListener } from '#imports'
interface Props {
// If the previous cell value was a text, the initial checkbox value is a string type
@ -15,6 +15,8 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const active = inject(ActiveCellInj, ref(false))
let vModel = $computed({
get: () => !!props.modelValue && props.modelValue !== '0' && props.modelValue !== 0,
set: (val) => emits('update:modelValue', val),
@ -37,11 +39,20 @@ const checkboxMeta = $computed(() => {
}
})
function onClick() {
if (!readOnly?.value) {
function onClick(force?: boolean) {
if (!readOnly?.value && (force || active.value)) {
vModel = !vModel
}
}
useSelectedCellKeyupListener(active, (e) => {
switch (e.key) {
case 'Enter':
onClick()
e.stopPropagation()
break
}
})
</script>
<template>
@ -53,7 +64,7 @@ function onClick() {
'nc-cell-hover-show': !vModel && !readOnly,
'opacity-0': readOnly && !vModel,
}"
@click="onClick"
@click="onClick(false)"
>
<div class="px-1 pt-1 rounded-full items-center" :class="{ 'bg-gray-100': !vModel, '!ml-[-8px]': readOnly }">
<Transition name="layout" mode="out-in" :duration="100">
@ -62,6 +73,7 @@ function onClick() {
:style="{
color: checkboxMeta.color,
}"
@click.stop="onClick(true)"
/>
</Transition>
</div>
@ -72,6 +84,7 @@ function onClick() {
.nc-cell-hover-show {
opacity: 0;
transition: 0.3s opacity;
&:hover {
opacity: 0.7;
}

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

@ -61,6 +61,11 @@ onMounted(() => {
v-model="vModel"
class="w-full h-full border-none outline-none px-2"
@blur="submitCurrency"
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
/>
<span v-else-if="vModel">{{ currency }}</span>

37
packages/nc-gui/components/cell/DatePicker.vue

@ -1,6 +1,16 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { ColumnInj, ReadonlyInj, computed, inject, ref, watch } from '#imports'
import {
ActiveCellInj,
ColumnInj,
EditModeInj,
ReadonlyInj,
computed,
inject,
ref,
useSelectedCellKeyupListener,
watch,
} from '#imports'
interface Props {
modelValue?: string | null
@ -15,6 +25,10 @@ const columnMeta = inject(ColumnInj, null)!
const readOnly = inject(ReadonlyInj, ref(false))
const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
let isDateInvalid = $ref(false)
const dateFormat = $computed(() => columnMeta?.value?.meta?.date_format ?? 'YYYY-MM-DD')
@ -57,6 +71,21 @@ watch(
)
const placeholder = computed(() => (isDateInvalid ? 'Invalid date' : ''))
useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
e.stopPropagation()
open.value = true
break
case 'Escape':
if (open.value) {
e.stopPropagation()
open.value = false
}
break
}
})
</script>
<template>
@ -68,9 +97,9 @@ const placeholder = computed(() => (isDateInvalid ? 'Invalid date' : ''))
:placeholder="placeholder"
:allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true"
:dropdown-class-name="`${randomClass} nc-picker-date`"
:open="readOnly || (localState && isPk) ? false : open"
@click="open = !open"
:dropdown-class-name="`${randomClass} nc-picker-date ${open ? 'active' : ''}`"
:open="(readOnly || (localState && isPk)) && !active && !editable ? false : open"
@click="open = (active || editable) && !open"
>
<template #suffixIcon></template>
</a-date-picker>

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { ReadonlyInj, inject, ref, useProject, watch } from '#imports'
import { ActiveCellInj, ReadonlyInj, inject, ref, useProject, useSelectedCellKeyupListener, watch } from '#imports'
interface Props {
modelValue?: string | null
@ -15,6 +15,10 @@ const { isMysql } = useProject()
const readOnly = inject(ReadonlyInj, ref(false))
const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
let isDateInvalid = $ref(false)
const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
@ -56,6 +60,21 @@ watch(
},
{ flush: 'post' },
)
useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
e.stopPropagation()
open.value = true
break
case 'Escape':
if (open.value) {
e.stopPropagation()
open.value = false
}
break
}
})
</script>
<template>
@ -68,10 +87,10 @@ watch(
:placeholder="isDateInvalid ? 'Invalid date' : ''"
:allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true"
:dropdown-class-name="`${randomClass} nc-picker-datetime`"
:open="readOnly || (localState && isPk) ? false : open"
:dropdown-class-name="`${randomClass} nc-picker-datetime ${open ? 'active' : ''}`"
:open="readOnly || (localState && isPk) ? false : open && (active || editable)"
:disabled="readOnly || (localState && isPk)"
@click="open = !open"
@click="open = (active || editable) && !open"
@ok="open = !open"
>
<template #suffixIcon></template>

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

@ -30,6 +30,11 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
type="number"
step="0.1"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
/>
<span v-else class="text-sm">{{ vModel }}</span>
</template>

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

@ -84,6 +84,11 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
@blur="submitDuration"
@keypress="checkDurationFormat($event)"
@keydown.enter="submitDuration"
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
/>
<span v-else> {{ localState }}</span>

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

@ -24,7 +24,18 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script>
<template>
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none text-sm px-2" @blur="editEnabled = false" />
<input
v-if="editEnabled"
:ref="focus"
v-model="vModel"
class="outline-none text-sm px-2"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
/>
<a v-else-if="validEmail" class="text-sm underline hover:opacity-75" :href="`mailto:${vModel}`" target="_blank">
{{ vModel }}

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

@ -30,6 +30,11 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
type="number"
step="0.1"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
/>
<span v-else class="text-sm">{{ vModel }}</span>
</template>

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

@ -34,6 +34,11 @@ function onKeyDown(evt: KeyboardEvent) {
type="number"
@blur="editEnabled = false"
@keydown="onKeyDown"
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
/>
<span v-else class="text-sm">{{ vModel }}</span>
</template>

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

@ -14,6 +14,7 @@ import {
ref,
useEventListener,
useProject,
useSelectedCellKeyupListener,
watch,
} from '#imports'
import MdiCloseCircle from '~icons/mdi/close-circle'
@ -85,17 +86,7 @@ const selectedTitles = computed(() =>
: [],
)
const handleKeys = (e: KeyboardEvent) => {
switch (e.key) {
case 'Escape':
e.preventDefault()
isOpen.value = false
break
case 'Enter':
e.stopPropagation()
break
}
}
const v = Math.floor(Math.random() * 1000)
const handleClose = (e: MouseEvent) => {
if (aselect.value && !aselect.value.$el.contains(e.target)) {
@ -131,7 +122,24 @@ watch(
)
watch(isOpen, (n, _o) => {
if (!n) aselect.value?.$el.blur()
if (!n) {
aselect.value?.$el?.querySelector('input')?.blur()
} else {
aselect.value?.$el?.querySelector('input')?.focus()
}
})
useSelectedCellKeyupListener(active, (e) => {
switch (e.key) {
case 'Escape':
isOpen.value = false
break
case 'Enter':
if (active.value && !isOpen.value) {
isOpen.value = true
}
break
}
})
</script>
@ -139,17 +147,17 @@ watch(isOpen, (n, _o) => {
<a-select
ref="aselect"
v-model:value="vModel"
v-model:open="isOpen"
mode="multiple"
class="w-full"
:bordered="false"
:show-arrow="!readOnly"
:show-search="false"
:open="isOpen"
:disabled="readOnly"
:class="{ '!ml-[-8px]': readOnly }"
dropdown-class-name="nc-dropdown-multi-select-cell"
@keydown="handleKeys"
@click="isOpen = !isOpen"
:dropdown-class-name="`nc-dropdown-multi-select-cell ${isOpen ? 'active' : ''}`"
@keydown.enter.stop
@click="isOpen = active && !isOpen"
>
<a-select-option
v-for="op of options"
@ -221,18 +229,23 @@ watch(isOpen, (n, _o) => {
margin-right: -6px;
margin-left: 3px;
}
.ms-close-icon:before {
display: block;
}
.ms-close-icon:hover {
color: rgba(0, 0, 0, 0.45);
}
.rounded-tag {
@apply py-0 px-[12px] rounded-[12px];
}
:deep(.ant-tag) {
@apply "rounded-tag" my-[2px];
}
:deep(.ant-tag-close-icon) {
@apply "text-slate-500";
}

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

@ -21,6 +21,11 @@ const vModel = useVModel(props, 'modelValue', emits)
class="w-full !border-none text-base"
:class="{ '!px-2': editEnabled }"
type="number"
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
/>
<span v-else>{{ vModel }}</span>
</template>

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

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ColumnInj, EditModeInj, computed, inject } from '#imports'
import { ActiveCellInj, ColumnInj, computed, inject, useSelectedCellKeyupListener } from '#imports'
interface Props {
modelValue?: number | null | undefined
@ -11,8 +11,6 @@ const emits = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj)
const ratingMeta = computed(() => {
return {
icon: {
@ -29,16 +27,17 @@ const vModel = computed({
get: () => modelValue ?? NaN,
set: (val) => emits('update:modelValue', val),
})
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => {
if (/^\d$/.test(e.key)) {
e.stopPropagation()
vModel.value = +e.key === +vModel.value ? 0 : +e.key
}
})
</script>
<template>
<a-rate
v-model:value="vModel"
:count="ratingMeta.max"
:style="`color: ${ratingMeta.color}; padding: 0px 5px`"
:class="{ '!ml-[-8px]': !editEnabled }"
:disabled="!editEnabled"
>
<a-rate v-model:value="vModel" :count="ratingMeta.max" :style="`color: ${ratingMeta.color}; padding: 0px 5px`">
<template #character>
<MdiStar v-if="ratingMeta.icon.full === 'mdi-star'" class="text-sm" />
<MdiHeart v-if="ratingMeta.icon.full === 'mdi-heart'" class="text-sm" />

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

@ -3,6 +3,7 @@ import tinycolor from 'tinycolor2'
import type { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionType } from 'nocodb-sdk'
import { ActiveCellInj, ColumnInj, IsKanbanInj, ReadonlyInj, computed, inject, ref, useEventListener, watch } from '#imports'
import { useSelectedCellKeyupListener } from '~/composables/useSelectedCellKeyupListener'
interface Props {
modelValue?: string | undefined
@ -44,15 +45,6 @@ const options = computed<SelectOptionType[]>(() => {
return []
})
const handleKeys = (e: KeyboardEvent) => {
switch (e.key) {
case 'Escape':
e.preventDefault()
isOpen.value = false
break
}
}
const handleClose = (e: MouseEvent) => {
if (aselect.value && !aselect.value.$el.contains(e.target)) {
isOpen.value = false
@ -63,7 +55,24 @@ const handleClose = (e: MouseEvent) => {
useEventListener(document, 'click', handleClose)
watch(isOpen, (n, _o) => {
if (!n) aselect.value?.$el.blur()
if (!n) {
aselect.value?.$el?.querySelector('input')?.blur()
} else {
aselect.value?.$el?.querySelector('input')?.focus()
}
})
useSelectedCellKeyupListener(active, (e) => {
switch (e.key) {
case 'Escape':
isOpen.value = false
break
case 'Enter':
if (active.value && !isOpen.value) {
isOpen.value = true
}
break
}
})
</script>
@ -77,10 +86,10 @@ watch(isOpen, (n, _o) => {
:open="isOpen"
:disabled="readOnly"
:show-arrow="!readOnly && (active || vModel === null)"
dropdown-class-name="nc-dropdown-single-select-cell"
:dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen ? 'active' : ''}`"
@select="isOpen = false"
@keydown="handleKeys"
@click="isOpen = !isOpen"
@keydown.enter.stop
@click="isOpen = active && !isOpen"
>
<a-select-option
v-for="op of options"
@ -110,9 +119,11 @@ watch(isOpen, (n, _o) => {
.rounded-tag {
@apply py-0 px-[12px] rounded-[12px];
}
:deep(.ant-tag) {
@apply "rounded-tag";
}
:deep(.ant-select-clear) {
opacity: 1;
}

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

@ -27,6 +27,11 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
class="h-full w-full outline-none bg-transparent"
:class="{ '!px-2': editEnabled }"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
/>
<span v-else>{{ vModel }}</span>

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

@ -26,6 +26,11 @@ const focus: VNodeRef = (el) => (el as HTMLTextAreaElement)?.focus()
@blur="editEnabled = false"
@keydown.alt.enter.stop
@keydown.shift.enter.stop
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
/>
<span v-else>{{ vModel }}</span>

27
packages/nc-gui/components/cell/TimePicker.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { ReadonlyInj, inject, onClickOutside, useProject, watch } from '#imports'
import { ActiveCellInj, ReadonlyInj, inject, onClickOutside, useProject, useSelectedCellKeyupListener, watch } from '#imports'
interface Props {
modelValue?: string | null | undefined
@ -15,6 +15,10 @@ const { isMysql } = useProject()
const readOnly = inject(ReadonlyInj, ref(false))
const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
let isTimeInvalid = $ref(false)
const dateFormat = isMysql.value ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
@ -65,6 +69,21 @@ watch(
},
{ flush: 'post' },
)
useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
e.stopPropagation()
open.value = true
break
case 'Escape':
if (open.value) {
e.stopPropagation()
open.value = false
}
break
}
})
</script>
<template>
@ -79,9 +98,9 @@ watch(
:placeholder="isTimeInvalid ? 'Invalid time' : ''"
:allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true"
:open="readOnly || (localState && isPk) ? false : open"
:popup-class-name="`${randomClass} nc-picker-time`"
@click="open = !open"
:open="(readOnly || (localState && isPk)) && !active && !editable ? false : open"
:popup-class-name="`${randomClass} nc-picker-time ${open ? 'active' : ''}`"
@click="open = (active || editable) && !open"
@ok="open = !open"
>
<template #suffixIcon></template>

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

@ -79,6 +79,11 @@ watch(
v-model="vModel"
class="outline-none text-sm w-full px-2"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
/>
<nuxt-link

29
packages/nc-gui/components/cell/YearPicker.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { ReadonlyInj, computed, inject, onClickOutside, ref, watch } from '#imports'
import { ActiveCellInj, ReadonlyInj, computed, inject, onClickOutside, ref, useSelectedCellKeyupListener, watch } from '#imports'
interface Props {
modelValue?: number | string | null
@ -13,6 +13,10 @@ const emit = defineEmits(['update:modelValue'])
const readOnly = inject(ReadonlyInj, ref(false))
const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
let isYearInvalid = $ref(false)
const localState = $computed({
@ -55,6 +59,21 @@ watch(
)
const placeholder = computed(() => (isYearInvalid ? 'Invalid year' : ''))
useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
e.stopPropagation()
open.value = true
break
case 'Escape':
if (open.value) {
e.stopPropagation()
open.value = false
}
break
}
})
</script>
<template>
@ -66,10 +85,10 @@ const placeholder = computed(() => (isYearInvalid ? 'Invalid year' : ''))
:placeholder="placeholder"
:allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true"
:open="readOnly || (localState && isPk) ? false : open"
:dropdown-class-name="`${randomClass} nc-picker-year`"
@click="open = !open"
@change="open = !open"
:open="(readOnly || (localState && isPk)) && !active && !editable ? false : open"
:dropdown-class-name="`${randomClass} nc-picker-year ${open ? 'active' : ''}`"
@click="open = (active || editable) && !open"
@change="open = (active || editable) && !open"
>
<template #suffixIcon></template>
</a-date-picker>

19
packages/nc-gui/components/cell/attachment/index.vue

@ -3,6 +3,7 @@ import { onKeyDown } from '@vueuse/core'
import { useProvideAttachmentCell } from './utils'
import { useSortable } from './sort'
import {
ActiveCellInj,
DropZoneRef,
IsGalleryInj,
IsKanbanInj,
@ -12,6 +13,7 @@ import {
openLink,
ref,
useDropZone,
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow,
useSmartsheetStoreOrThrow,
watch,
@ -113,9 +115,17 @@ watch(
attachments.value = []
}
}
} else {
if (isPublic.value && isForm.value) {
storedFiles.value = []
} else {
attachments.value = []
}
}
},
{ immediate: true },
{
immediate: true,
},
)
/** updates attachments array for autosave */
@ -136,6 +146,13 @@ watch(
rowState.value[column.value!.title!] = storedFiles.value
},
)
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => {
if (e.key === 'Enter' && !isReadonly.value) {
e.stopPropagation()
modalVisible.value = true
}
})
</script>
<template>

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

@ -112,6 +112,7 @@ const vModel = computed({
})
const syncAndNavigate = (dir: NavigateDir, e: KeyboardEvent) => {
console.log('syncAndNavigate', e.target)
if (isJSON.value) return
if (currentRow.value.rowMeta.changed || currentRow.value.rowMeta.new) {

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

@ -22,6 +22,7 @@ import {
extractPkFromRow,
inject,
isColumnRequiredAndNull,
isMac,
message,
onBeforeUnmount,
onClickOutside,
@ -168,52 +169,118 @@ const { selectCell, selectBlock, selectedRange, clearRangeRows, startSelectRange
isPkAvail,
clearCell,
makeEditable,
(row?: number | null, col?: number | null) => {
row = row ?? selected.row
col = col ?? selected.col
if (row !== undefined && col !== undefined && row !== null && col !== null) {
// get active cell
const rows = tbodyEl.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 tdScroll = getContainerScrollForElement(td, gridWrapper.value, { top: headerHeight, bottom: 9, right: 9 })
if (rows && row === rows.length - 2) {
// if last row make 'Add New Row' visible
gridWrapper.value.scrollTo({
top: gridWrapper.value.scrollHeight,
left:
cols && col === cols.length - 2 // if corner cell
? gridWrapper.value.scrollWidth
: tdScroll.left,
behavior: 'smooth',
})
return
}
scrollToCell,
(e: KeyboardEvent) => {
// ignore navigating if picker(Date, Time, DateTime, Year)
// or single/multi select options is open
const activePickerOrDropdownEl = document.querySelector(
'.nc-picker-datetime.active,.nc-dropdown-single-select-cell.active,.nc-dropdown-multi-select-cell.active,.nc-picker-date.active,.nc-picker-year.active,.nc-picker-time.active',
)
if (activePickerOrDropdownEl) {
e.preventDefault()
return true
}
// if expanded form is active skip keyboard event handling
if (document.querySelector('.nc-drawer-expanded-form.active')) {
return true
}
if (cols && col === cols.length - 2) {
// if last column make 'Add New Column' visible
gridWrapper.value.scrollTo({
top: tdScroll.top,
left: gridWrapper.value.scrollWidth,
behavior: 'smooth',
})
return
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
if (e.key === ' ') {
if (selected.row !== null && !editEnabled) {
e.preventDefault()
const row = data.value[selected.row]
expandForm(row)
return true
}
} else if (e.key === 'Escape') {
if (editEnabled) {
editEnabled = false
return true
}
} else if (e.key === 'Enter') {
if (editEnabled) {
editEnabled = false
return true
}
}
if (cmdOrCtrl) {
switch (e.key) {
case 'ArrowUp':
selected.row = 0
selected.col = selected.col ?? 0
scrollToCell?.()
editEnabled = false
return true
case 'ArrowDown':
selected.row = data.value.length - 1
selected.col = selected.col ?? 0
scrollToCell?.()
editEnabled = false
return true
case 'ArrowRight':
selected.row = selected.row ?? 0
selected.col = fields.value?.length - 1
scrollToCell?.()
editEnabled = false
return true
case 'ArrowLeft':
selected.row = selected.row ?? 0
selected.col = 0
scrollToCell?.()
editEnabled = false
return true
}
}
},
)
function scrollToCell(row?: number | null, col?: number | null) {
row = row ?? selected.row
col = col ?? selected.col
if (row !== undefined && col !== undefined && row !== null && col !== null) {
// get active cell
const rows = tbodyEl.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 tdScroll = getContainerScrollForElement(td, gridWrapper.value, { top: headerHeight, bottom: 9, right: 9 })
// scroll into the active cell
if (rows && row === rows.length - 2) {
// if last row make 'Add New Row' visible
gridWrapper.value.scrollTo({
top: gridWrapper.value.scrollHeight,
left:
cols && col === cols.length - 2 // if corner cell
? gridWrapper.value.scrollWidth
: tdScroll.left,
behavior: 'smooth',
})
return
}
if (cols && col === cols.length - 2) {
// if last column make 'Add New Column' visible
gridWrapper.value.scrollTo({
top: tdScroll.top,
left: tdScroll.left,
left: gridWrapper.value.scrollWidth,
behavior: 'smooth',
})
return
}
},
)
// scroll into the active cell
gridWrapper.value.scrollTo({
top: tdScroll.top,
left: tdScroll.left,
behavior: 'smooth',
})
}
}
onMounted(loadGridViewColumns)
@ -234,7 +301,7 @@ const showLoading = ref(true)
const skipRowRemovalOnCancel = ref(false)
const expandForm = (row: Row, state?: Record<string, any>, fromToolbar = false) => {
function expandForm(row: Row, state?: Record<string, any>, fromToolbar = false) {
const rowId = extractPkFromRow(row.row, meta.value?.columns as ColumnType[])
if (rowId) {
@ -325,7 +392,7 @@ useEventListener(document, 'keyup', async (e: KeyboardEvent) => {
/** On clicking outside of table reset active cell */
const smartTable = ref(null)
onClickOutside(smartTable, () => {
onClickOutside(smartTable, (e) => {
clearRangeRows()
if (selected.col === null) return
@ -333,6 +400,23 @@ onClickOutside(smartTable, () => {
if (editEnabled && (isVirtualCol(activeCol) || activeCol.uidt === UITypes.JSON)) return
// ignore unselecting if clicked inside or on the picker(Date, Time, DateTime, Year)
// or single/multi select options
const activePickerOrDropdownEl = document.querySelector(
'.nc-picker-datetime.active,.nc-dropdown-single-select-cell.active,.nc-dropdown-multi-select-cell.active,.nc-picker-date.active,.nc-picker-year.active,.nc-picker-time.active',
)
if (
e.target &&
activePickerOrDropdownEl &&
(activePickerOrDropdownEl === e.target || activePickerOrDropdownEl?.contains(e.target as Element))
)
return
// if expanded form is active skip resetting the active cell
if (document.querySelector('.nc-drawer-expanded-form.active')) {
return
}
selected.row = null
selected.col = null
})
@ -383,8 +467,8 @@ const saveOrUpdateRecords = async (args: { metaValue?: TableType; viewMetaValue?
currentRow.rowMeta.changed = false
for (const field of (args.metaValue || meta.value)?.columns ?? []) {
if (isVirtualCol(field)) continue
if (field.title! in currentRow.row && currentRow.row[field.title!] !== currentRow.oldRow[field.title!]) {
await updateOrSaveRow(currentRow, field.title!, {}, args)
if (currentRow.row[field.title!] !== currentRow.oldRow[field.title!]) {
await updateOrSaveRow(currentRow, field.title!, args)
}
}
}

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

@ -137,6 +137,7 @@ export default {
:body-style="{ 'padding': 0, 'display': 'flex', 'flex-direction': 'column' }"
:closable="false"
class="nc-drawer-expanded-form"
:class="{ 'active': isExpanded }"
>
<SmartsheetExpandedFormHeader :view="props.view" @cancel="onClose" />

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

@ -15,6 +15,7 @@ import {
inject,
ref,
useProvideLTARStore,
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow,
useUIPermission,
} from '#imports'
@ -70,6 +71,15 @@ const unlinkRef = async (rec: Record<string, any>) => {
await unlink(rec)
}
}
useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
listItemsDlg.value = true
e.stopPropagation()
break
}
})
</script>
<template>

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

@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { CellValueInj, ColumnInj, computed, handleTZ, inject, ref, replaceUrlsWithLink, useProject } from '#imports'
import { CellValueInj, ColumnInj, computed, handleTZ, inject, ref, refAutoReset, replaceUrlsWithLink, useProject } from '#imports'
// todo: column type doesn't have required property `error` - throws in typecheck
const column = inject(ColumnInj) as Ref<ColumnType & { colOptions: { error: any } }>
@ -10,19 +10,25 @@ const cellValue = inject(CellValueInj)
const { isPg } = useProject()
const showEditFormulaWarning = ref(false)
const showEditFormulaWarningMessage = () => {
showEditFormulaWarning.value = true
setTimeout(() => {
showEditFormulaWarning.value = false
}, 3000)
}
const result = computed(() => (isPg.value ? handleTZ(cellValue?.value) : cellValue?.value))
const urls = computed(() => replaceUrlsWithLink(result.value))
const timeout = 3000 // in ms
const showEditFormulaWarning = refAutoReset(false, timeout)
const showClearFormulaWarning = refAutoReset(false, timeout)
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
showEditFormulaWarning.value = true
break
case 'Delete':
showClearFormulaWarning.value = true
break
}
})
</script>
<template>
@ -35,15 +41,19 @@ const urls = computed(() => replaceUrlsWithLink(result.value))
<span>ERR!</span>
</a-tooltip>
<div class="p-2" @dblclick="showEditFormulaWarningMessage">
<div class="p-2" @dblclick="showEditFormulaWarning = true">
<div v-if="urls" v-html="urls" />
<div v-else>{{ result }}</div>
<div v-if="showEditFormulaWarning" class="text-left text-wrap mt-2 text-[#e65100]">
<div v-if="showEditFormulaWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
<!-- TODO: i18n -->
Warning: Formula fields should be configured in the field menu dropdown.
</div>
<div v-if="showClearFormulaWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
<!-- TODO: i18n -->
Warning: Computed field - unable to clear text.
</div>
</div>
</div>
</template>

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

@ -13,6 +13,7 @@ import {
inject,
ref,
useProvideLTARStore,
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow,
useUIPermission,
} from '#imports'
@ -81,6 +82,15 @@ const onAttachRecord = () => {
childListDlg.value = false
listItemsDlg.value = true
}
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
listItemsDlg.value = true
e.stopPropagation()
break
}
})
</script>
<template>

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

@ -11,6 +11,7 @@ import {
computed,
inject,
provide,
refAutoReset,
useColumn,
useMetas,
} from '#imports'
@ -46,45 +47,73 @@ provide(MetaInj, lookupTableMeta)
provide(CellUrlDisableOverlayInj, ref(true))
const lookupColumnMetaProps = useColumn(lookupColumn)
const timeout = 3000 // in ms
const showEditWarning = refAutoReset(false, timeout)
const showClearWarning = refAutoReset(false, timeout)
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
showEditWarning.value = true
break
case 'Delete':
showClearWarning.value = true
break
}
})
</script>
<template>
<div class="h-full flex gap-1 overflow-x-auto p-1">
<template v-if="lookupColumn">
<!-- Render virtual cell -->
<div v-if="isVirtualCol(lookupColumn)">
<template
v-if="lookupColumn.uidt === UITypes.LinkToAnotherRecord && lookupColumn.colOptions.type === RelationTypes.BELONGS_TO"
>
<LazySmartsheetVirtualCell
<div class="h-full">
<div class="h-full flex gap-1 overflow-x-auto p-1">
<template v-if="lookupColumn">
<!-- Render virtual cell -->
<div v-if="isVirtualCol(lookupColumn)">
<template
v-if="lookupColumn.uidt === UITypes.LinkToAnotherRecord && lookupColumn.colOptions.type === RelationTypes.BELONGS_TO"
>
<LazySmartsheetVirtualCell
v-for="(v, i) of arrValue"
:key="i"
:edit-enabled="false"
:model-value="v"
:column="lookupColumn"
/>
</template>
<LazySmartsheetVirtualCell v-else :edit-enabled="false" :model-value="arrValue" :column="lookupColumn" />
</div>
<!-- Render normal cell -->
<template v-else>
<!-- For attachment cell avoid adding chip style -->
<div
v-for="(v, i) of arrValue"
:key="i"
:edit-enabled="false"
:model-value="v"
:column="lookupColumn"
/>
class="min-w-max"
:class="{
'bg-gray-100 px-1 rounded-full flex-1': !lookupColumnMetaProps.isAttachment,
' border-gray-200 rounded border-1': ![UITypes.Attachment, UITypes.MultiSelect, UITypes.SingleSelect].includes(
lookupColumn.uidt,
),
}"
>
<LazySmartsheetCell :model-value="v" :column="lookupColumn" :edit-enabled="false" :virtual="true" />
</div>
</template>
<LazySmartsheetVirtualCell v-else :edit-enabled="false" :model-value="arrValue" :column="lookupColumn" />
</div>
<!-- Render normal cell -->
<template v-else>
<!-- For attachment cell avoid adding chip style -->
<div
v-for="(v, i) of arrValue"
:key="i"
class="min-w-max"
:class="{
'bg-gray-100 px-1 rounded-full flex-1': !lookupColumnMetaProps.isAttachment,
' border-gray-200 rounded border-1': ![UITypes.Attachment, UITypes.MultiSelect, UITypes.SingleSelect].includes(
lookupColumn.uidt,
),
}"
>
<LazySmartsheetCell :model-value="v" :column="lookupColumn" :edit-enabled="false" :virtual="true" />
</div>
</template>
</template>
</div>
<div>
<div v-if="showEditWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
<!-- TODO: i18n -->
Warning: Computed field - unable to edit content.
</div>
<div v-if="showClearWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
<!-- TODO: i18n -->
Warning: Computed field - unable to clear content.
</div>
</div>
</div>
</template>

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

@ -2,6 +2,7 @@
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import {
ActiveCellInj,
CellValueInj,
ColumnInj,
IsFormInj,
@ -14,6 +15,7 @@ import {
inject,
ref,
useProvideLTARStore,
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow,
useUIPermission,
} from '#imports'
@ -82,6 +84,15 @@ const onAttachRecord = () => {
childListDlg.value = false
listItemsDlg.value = true
}
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
listItemsDlg.value = true
e.stopPropagation()
break
}
})
</script>
<template>

37
packages/nc-gui/components/virtual-cell/Rollup.vue

@ -1,11 +1,40 @@
<script setup lang="ts">
import { CellValueInj, inject } from '#imports'
import { CellValueInj, inject, refAutoReset } from '#imports'
const value = inject(CellValueInj)
const timeout = 3000 // in ms
const showEditWarning = refAutoReset(false, timeout)
const showClearWarning = refAutoReset(false, timeout)
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
showEditWarning.value = true
break
case 'Delete':
showClearWarning.value = true
break
}
})
</script>
<template>
<span class="text-center pl-3">
{{ value }}
</span>
<div>
<span class="text-center pl-3">
{{ value }}
</span>
<div>
<div v-if="showEditWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
<!-- TODO: i18n -->
Warning: Computed field - unable to edit content.
</div>
<div v-if="showClearWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
<!-- TODO: i18n -->
Warning: Computed field - unable to clear content.
</div>
</div>
</div>
</template>

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

@ -1,18 +1,20 @@
<script lang="ts" setup>
import type { Card } from 'ant-design-vue'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import {
ColumnInj,
Empty,
IsPublicInj,
computed,
inject,
ref,
useLTARStoreOrThrow,
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow,
useVModel,
watch,
} from '#imports'
import { IsPublicInj } from '~/context'
const props = defineProps<{ modelValue: boolean }>()
@ -38,6 +40,8 @@ const { addLTARRef, isNew } = useSmartsheetRowStoreOrThrow()
const isPublic = inject(IsPublicInj, ref(false))
const selectedRowIndex = ref(0)
const linkRow = async (row: Record<string, any>) => {
if (isNew.value) {
addLTARRef(row, column?.value as ColumnType)
@ -54,6 +58,7 @@ watch(vModel, (nextVal, prevVal) => {
childrenExcludedListPagination.query = ''
childrenExcludedListPagination.page = 1
loadChildrenExcludedList()
selectedRowIndex.value = 0
}
})
@ -98,6 +103,49 @@ const newRowState = computed(() => {
watch(expandedFormDlg, (nexVal) => {
if (!nexVal && !isNew.value) vModel.value = false
})
useSelectedCellKeyupListener(vModel, (e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowLeft':
e.stopPropagation()
e.preventDefault()
if (childrenExcludedListPagination.page > 1) childrenExcludedListPagination.page--
break
case 'ArrowRight':
e.stopPropagation()
e.preventDefault()
if (
childrenExcludedList.value?.pageInfo &&
childrenExcludedListPagination.page <
(childrenExcludedList.value.pageInfo.totalRows || 1) / childrenExcludedListPagination.size
)
childrenExcludedListPagination.page++
break
case 'ArrowUp':
selectedRowIndex.value = Math.max(0, selectedRowIndex.value - 1)
e.stopPropagation()
e.preventDefault()
break
case 'ArrowDown':
selectedRowIndex.value = Math.min(childrenExcludedList.value?.list?.length - 1, selectedRowIndex.value + 1)
e.stopPropagation()
e.preventDefault()
break
case 'Enter':
{
const selectedRow = childrenExcludedList.value?.list?.[selectedRowIndex.value]
if (selectedRow) {
linkRow(selectedRow)
e.stopPropagation()
e.preventDefault()
}
}
break
}
})
const activeRow = (vNode?: InstanceType<typeof Card>) => {
vNode?.$el?.scrollIntoView({ block: 'nearest', inline: 'nearest' })
}
</script>
<template>
@ -115,6 +163,7 @@ watch(expandedFormDlg, (nexVal) => {
:placeholder="$t('placeholder.filterQuery')"
class="max-w-[200px]"
size="small"
@keydown.capture.stop
/>
<div class="flex-1" />
@ -132,7 +181,9 @@ watch(expandedFormDlg, (nexVal) => {
<a-card
v-for="(refRow, i) in childrenExcludedList?.list ?? []"
:key="i"
:ref="selectedRowIndex === i ? activeRow : null"
class="!my-4 cursor-pointer hover:(!bg-gray-200/50 shadow-md) group"
:class="{ 'nc-selected-row': selectedRowIndex === i }"
@click="linkRow(refRow)"
>
{{ refRow[relatedTablePrimaryValueProp] }}
@ -175,4 +226,8 @@ watch(expandedFormDlg, (nexVal) => {
:deep(.ant-pagination-item a) {
line-height: 21px !important;
}
:deep(.nc-selected-row) {
@apply !ring;
}
</style>

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

@ -1,6 +1,6 @@
import type { MaybeRef } from '@vueuse/core'
import { UITypes } from 'nocodb-sdk'
import { message, reactive, unref, useCopy, useEventListener, useI18n } from '#imports'
import { message, reactive, ref, unref, useCopy, useEventListener, useI18n } from '#imports'
interface SelectedBlock {
row: number | null
@ -13,16 +13,19 @@ interface SelectedBlock {
export function useMultiSelect(
fields: MaybeRef<any[]>,
data: MaybeRef<any[]>,
editEnabled: MaybeRef<boolean>,
_editEnabled: MaybeRef<boolean>,
isPkAvail: MaybeRef<boolean | undefined>,
clearCell: Function,
makeEditable: Function,
scrollToActiveCell?: (row?: number | null, col?: number | null) => void,
keyEventHandler?: Function,
) {
const { t } = useI18n()
const { copy } = useCopy()
const editEnabled = ref(_editEnabled)
const selected = reactive<SelectedBlock>({ row: null, col: null })
// save the first and the last column where the mouse is down while the value isSelectedRow is true
@ -38,6 +41,7 @@ export function useMultiSelect(
function selectCell(row: number, col: number) {
clearRangeRows()
editEnabled.value = false
selected.row = row
selected.col = col
}
@ -126,6 +130,11 @@ export function useMultiSelect(
})
const onKeyDown = async (e: KeyboardEvent) => {
// invoke the keyEventHandler if provided and return if it returns true
if (await keyEventHandler?.(e)) {
return
}
if (
!isNaN(selectedRows.startRow) &&
!isNaN(selectedRows.startCol) &&
@ -148,16 +157,20 @@ export function useMultiSelect(
if (e.shiftKey) {
if (selected.col > 0) {
selected.col--
editEnabled.value = false
} else if (selected.row > 0) {
selected.row--
selected.col = unref(columnLength) - 1
editEnabled.value = false
}
} else {
if (selected.col < unref(columnLength) - 1) {
selected.col++
editEnabled.value = false
} else if (selected.row < unref(data).length - 1) {
selected.row++
selected.col = 0
editEnabled.value = false
}
}
scrollToActiveCell?.()
@ -170,11 +183,9 @@ export function useMultiSelect(
break
/** on delete key press clear cell */
case 'Delete':
if (!unref(editEnabled)) {
e.preventDefault()
clearRangeRows()
await clearCell(selected as { row: number; col: number })
}
e.preventDefault()
clearRangeRows()
await clearCell(selected as { row: number; col: number })
break
/** on arrow key press navigate through cells */
case 'ArrowRight':
@ -183,6 +194,7 @@ export function useMultiSelect(
if (selected.col < unref(columnLength) - 1) {
selected.col++
scrollToActiveCell?.()
editEnabled.value = false
}
break
case 'ArrowLeft':
@ -192,6 +204,7 @@ export function useMultiSelect(
if (selected.col > 0) {
selected.col--
scrollToActiveCell?.()
editEnabled.value = false
}
break
case 'ArrowUp':
@ -201,6 +214,7 @@ export function useMultiSelect(
if (selected.row > 0) {
selected.row--
scrollToActiveCell?.()
editEnabled.value = false
}
break
case 'ArrowDown':
@ -210,6 +224,7 @@ export function useMultiSelect(
if (selected.row < unref(data).length - 1) {
selected.row++
scrollToActiveCell?.()
editEnabled.value = false
}
break
default:

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

@ -0,0 +1,21 @@
import { isClient } from '@vueuse/core'
import type { Ref } from 'vue'
export function useSelectedCellKeyupListener(selected: Ref<boolean>, handler: (e: KeyboardEvent) => void) {
if (isClient) {
watch(selected, (nextVal, _, cleanup) => {
// bind listener when `selected` is truthy
if (nextVal) {
document.addEventListener('keydown', handler, true)
// if `selected` is falsy then remove the event handler
} else {
document.removeEventListener('keydown', handler, true)
}
// cleanup is called whenever the watcher is re-evaluated or stopped
cleanup(() => {
document.removeEventListener('keydown', handler, true)
})
})
}
}

1
packages/nc-gui/composables/useTable.ts

@ -79,6 +79,7 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void) {
okText: t('general.yes'),
okType: 'danger',
cancelText: t('general.no'),
width: 450,
async onOk() {
try {
const meta = (await getMeta(table.id as string, true)) as TableType

2
packages/nc-gui/utils/browserUtils.ts

@ -0,0 +1,2 @@
// refer - https://stackoverflow.com/a/11752084
export const isMac = () => /Mac/i.test(navigator.platform)

9
packages/nc-gui/utils/columnUtils.ts

@ -25,7 +25,6 @@ import TimerOutline from '~icons/mdi/timer-outline'
import Star from '~icons/mdi/star'
import MathIntegral from '~icons/mdi/math-integral'
import MovieRoll from '~icons/mdi/movie-roll'
import Counter from '~icons/mdi/counter'
import CalendarClock from '~icons/mdi/calendar-clock'
import ID from '~icons/mdi/identifier'
import RulerSquareCompass from '~icons/mdi/ruler-square-compass'
@ -123,18 +122,10 @@ const uiTypes = [
icon: MovieRoll,
virtual: 1,
},
{
name: UITypes.Count,
icon: Counter,
},
{
name: UITypes.DateTime,
icon: CalendarClock,
},
{
name: UITypes.AutoNumber,
icon: Numeric,
},
{
name: UITypes.Geometry,
icon: RulerSquareCompass,

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

@ -19,3 +19,4 @@ export * from './dataUtils'
export * from './userUtils'
export * from './stringUtils'
export * from './memStorage'
export * from './browserUtils'

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

@ -1417,8 +1417,6 @@ export class PgUi {
case 'internal':
case 'interval':
return 'string';
case 'jsonb':
return 'string';
case 'language_handler':
case 'line':
@ -1533,6 +1531,7 @@ export class PgUi {
case 'multipolygon':
return 'string';
case 'json':
case 'jsonb':
return 'json';
}
}

36
packages/nocodb/package-lock.json generated

@ -9337,9 +9337,9 @@
}
},
"node_modules/loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"dev": true,
"dependencies": {
"big.js": "^5.2.2",
@ -17009,9 +17009,9 @@
}
},
"node_modules/webpack-cli/node_modules/loader-utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
"dev": true,
"dependencies": {
"big.js": "^5.2.2",
@ -17424,9 +17424,9 @@
}
},
"node_modules/webpack/node_modules/loader-utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
"dev": true,
"dependencies": {
"big.js": "^5.2.2",
@ -25360,9 +25360,9 @@
"dev": true
},
"loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
@ -31378,9 +31378,9 @@
}
},
"loader-utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
@ -31675,9 +31675,9 @@
}
},
"loader-utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
"dev": true,
"requires": {
"big.js": "^5.2.2",

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

@ -101,7 +101,7 @@ export default class Noco {
constructor() {
process.env.PORT = process.env.PORT || '8080';
// todo: move
process.env.NC_VERSION = '0090000';
process.env.NC_VERSION = '0098004';
// if env variable NC_MINIMAL_DBS is set, then disable project creation with external sources
if (process.env.NC_MINIMAL_DBS) {

6
packages/nocodb/src/lib/plugins/backblaze/Backblaze.ts

@ -18,6 +18,7 @@ export default class Backblaze implements IStorageAdapterV2 {
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
ContentType: file.mimetype,
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
@ -54,11 +55,12 @@ export default class Backblaze implements IStorageAdapterV2 {
url: url,
encoding: null,
},
(err, _, body) => {
(err, httpResponse, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
uploadParams.ContentType = httpResponse.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
@ -126,7 +128,7 @@ export default class Backblaze implements IStorageAdapterV2 {
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: '',
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});

6
packages/nocodb/src/lib/plugins/linode/LinodeObjectStorage.ts

@ -18,6 +18,7 @@ export default class LinodeObjectStorage implements IStorageAdapterV2 {
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
ContentType: file.mimetype,
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
@ -54,11 +55,12 @@ export default class LinodeObjectStorage implements IStorageAdapterV2 {
url: url,
encoding: null,
},
(err, _, body) => {
(err, httpResponse, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
uploadParams.ContentType = httpResponse.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
@ -116,7 +118,7 @@ export default class LinodeObjectStorage implements IStorageAdapterV2 {
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: '',
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});

6
packages/nocodb/src/lib/plugins/ovhCloud/OvhCloud.ts

@ -18,6 +18,7 @@ export default class OvhCloud implements IStorageAdapterV2 {
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
ContentType: file.mimetype,
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
@ -54,11 +55,12 @@ export default class OvhCloud implements IStorageAdapterV2 {
url: url,
encoding: null,
},
(err, _, body) => {
(err, httpResponse, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
uploadParams.ContentType = httpResponse.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
@ -116,7 +118,7 @@ export default class OvhCloud implements IStorageAdapterV2 {
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: '',
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});

6
packages/nocodb/src/lib/plugins/s3/S3.ts

@ -18,6 +18,7 @@ export default class S3 implements IStorageAdapterV2 {
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
ContentType: file.mimetype,
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
@ -53,11 +54,12 @@ export default class S3 implements IStorageAdapterV2 {
url: url,
encoding: null,
},
(err, _, body) => {
(err, httpResponse, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
uploadParams.ContentType = httpResponse.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
@ -119,7 +121,7 @@ export default class S3 implements IStorageAdapterV2 {
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: '',
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});

6
packages/nocodb/src/lib/plugins/scaleway/ScalewayObjectStorage.ts

@ -36,7 +36,7 @@ export default class ScalewayObjectStorage implements IStorageAdapterV2 {
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: '',
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});
@ -68,6 +68,7 @@ export default class ScalewayObjectStorage implements IStorageAdapterV2 {
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
ContentType: file.mimetype,
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
@ -104,11 +105,12 @@ export default class ScalewayObjectStorage implements IStorageAdapterV2 {
url: url,
encoding: null,
},
(err, _, body) => {
(err, httpResponse, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
uploadParams.ContentType = httpResponse.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {

6
packages/nocodb/src/lib/plugins/spaces/Spaces.ts

@ -18,6 +18,7 @@ export default class Spaces implements IStorageAdapterV2 {
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
ContentType: file.mimetype,
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
@ -54,11 +55,12 @@ export default class Spaces implements IStorageAdapterV2 {
url: url,
encoding: null,
},
(err, _, body) => {
(err, httpResponse, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
uploadParams.ContentType = httpResponse.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
@ -124,7 +126,7 @@ export default class Spaces implements IStorageAdapterV2 {
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: '',
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});

6
packages/nocodb/src/lib/plugins/upcloud/UpoCloud.ts

@ -18,6 +18,7 @@ export default class UpoCloud implements IStorageAdapterV2 {
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
ContentType: file.mimetype,
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
@ -54,11 +55,12 @@ export default class UpoCloud implements IStorageAdapterV2 {
url: url,
encoding: null,
},
(err, _, body) => {
(err, httpResponse, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
uploadParams.ContentType = httpResponse.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
@ -114,7 +116,7 @@ export default class UpoCloud implements IStorageAdapterV2 {
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: '',
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});

6
packages/nocodb/src/lib/plugins/vultr/Vultr.ts

@ -18,6 +18,7 @@ export default class Vultr implements IStorageAdapterV2 {
async fileCreate(key: string, file: XcFile): Promise<any> {
const uploadParams: any = {
ACL: 'public-read',
ContentType: file.mimetype,
};
return new Promise((resolve, reject) => {
// Configure the file stream and obtain the upload parameters
@ -54,11 +55,12 @@ export default class Vultr implements IStorageAdapterV2 {
url: url,
encoding: null,
},
(err, _, body) => {
(err, httpResponse, body) => {
if (err) return reject(err);
uploadParams.Body = body;
uploadParams.Key = key;
uploadParams.ContentType = httpResponse.headers['content-type'];
// call S3 to retrieve upload file to specified bucket
this.s3Client.upload(uploadParams, (err1, data) => {
@ -116,7 +118,7 @@ export default class Vultr implements IStorageAdapterV2 {
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {
path: tempFile,
mimetype: '',
mimetype: 'text/plain',
originalname: 'temp.txt',
size: '',
});

2
packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts

@ -5,6 +5,7 @@ import NcMetaIO from '../meta/NcMetaIO';
import ncProjectEnvUpgrader from './ncProjectEnvUpgrader';
import ncProjectEnvUpgrader0011045 from './ncProjectEnvUpgrader0011045';
import ncProjectUpgraderV2_0090000 from './ncProjectUpgraderV2_0090000';
import ncDataTypesUpgrader from './ncDataTypesUpgrader';
const log = debug('nc:version-upgrader');
import { Tele } from 'nc-help';
@ -31,6 +32,7 @@ export default class NcUpgrader {
{ name: '0011043', handler: ncProjectEnvUpgrader },
{ name: '0011045', handler: ncProjectEnvUpgrader0011045 },
{ name: '0090000', handler: ncProjectUpgraderV2_0090000 },
{ name: '0098004', handler: ncDataTypesUpgrader },
];
if (!(await ctx.ncMeta.knexConnection?.schema?.hasTable?.('nc_store'))) {
return;

14
packages/nocodb/src/lib/version-upgrader/ncDataTypesUpgrader.ts

@ -0,0 +1,14 @@
import { UITypes } from 'nocodb-sdk';
import { MetaTable } from '../utils/globals';
import { NcUpgraderCtx } from './NcUpgrader';
// The Count and AutoNumber types are removed
// so convert all existing Count and AutoNumber fields to Number type
export default async function (ctx: NcUpgraderCtx) {
// directly update uidt of all existing Count and AutoNumber fields to Number
await ctx.ncMeta.knex
.update({ uidt: UITypes.Number })
.where({ uidt: UITypes.Count })
.orWhere({ uidt: UITypes.AutoNumber })
.table(MetaTable.COLUMNS);
}

1
packages/nocodb/src/run/docker.ts

@ -2,7 +2,6 @@ import cors from 'cors';
import express from 'express';
import Noco from '../lib/Noco';
process.env.NC_VERSION = '0009044';
const server = express();
server.enable('trust proxy');

1
packages/nocodb/src/run/dockerRunMysql.ts

@ -2,7 +2,6 @@ import cors from 'cors';
import express from 'express';
import Noco from '../lib/Noco';
process.env.NC_VERSION = '0009044';
const server = express();
server.enable('trust proxy');

1
packages/nocodb/src/run/dockerRunPG.ts

@ -2,7 +2,6 @@ import cors from 'cors';
import express from 'express';
import Noco from '../lib/Noco';
process.env.NC_VERSION = '0009044';
const server = express();
server.enable('trust proxy');

1
packages/nocodb/src/run/dockerRunPG_CyQuick.ts

@ -2,7 +2,6 @@ import cors from 'cors';
import express from 'express';
import Noco from '../lib/Noco';
process.env.NC_VERSION = '0009044';
const server = express();
server.enable('trust proxy');

16
tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts

@ -25,7 +25,14 @@ export class SelectOptionCellPageObject extends BasePage {
option: string;
multiSelect?: boolean;
}) {
await this.get({ index, columnHeader }).click();
const selectCell = this.get({ index, columnHeader });
// check if cell active
if (!(await selectCell.getAttribute('class')).includes('active')) {
await selectCell.click();
}
await selectCell.click();
await this.rootPage.getByTestId(`select-option-${columnHeader}-${index}`).getByText(option).click();
@ -88,6 +95,13 @@ export class SelectOptionCellPageObject extends BasePage {
}
async verifyOptions({ index, columnHeader, options }: { index: number; columnHeader: string; options: string[] }) {
const selectCell = this.get({ index, columnHeader });
// check if cell active
if (!(await selectCell.getAttribute('class')).includes('active')) {
await selectCell.click();
}
await this.get({ index, columnHeader }).click();
let counter = 0;

Loading…
Cancel
Save