Browse Source

Merge pull request #4222 from nocodb/fix/cell-shortcut-issues

Fix: Grid cell - keyboard shortcut issues
pull/4408/head
Pranav C 2 years ago committed by GitHub
parent
commit
d550ca461d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  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. 2
      packages/nocodb/src/lib/Noco.ts
  38. 2
      packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts
  39. 14
      packages/nocodb/src/lib/version-upgrader/ncDataTypesUpgrader.ts
  40. 1
      packages/nocodb/src/run/docker.ts
  41. 1
      packages/nocodb/src/run/dockerRunMysql.ts
  42. 1
      packages/nocodb/src/run/dockerRunPG.ts
  43. 1
      packages/nocodb/src/run/dockerRunPG_CyQuick.ts
  44. 16
      tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts

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

@ -106,6 +106,8 @@ declare module '@vue/runtime-core' {
MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default'] MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default']
MdiAccountOutline: typeof import('~icons/mdi/account-outline')['default'] MdiAccountOutline: typeof import('~icons/mdi/account-outline')['default']
MdiAccountPlusOutline: typeof import('~icons/mdi/account-plus-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'] MdiAlpha: typeof import('~icons/mdi/alpha')['default']
MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default'] MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default']
MdiApi: typeof import('~icons/mdi/api')['default'] MdiApi: typeof import('~icons/mdi/api')['default']
@ -200,6 +202,7 @@ declare module '@vue/runtime-core' {
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default'] MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default']
MdiScriptTextKeyOutline: typeof import('~icons/mdi/script-text-key-outline')['default'] MdiScriptTextKeyOutline: typeof import('~icons/mdi/script-text-key-outline')['default']
MdiScriptTextOutline: typeof import('~icons/mdi/script-text-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'] MdiSlack: typeof import('~icons/mdi/slack')['default']
MdiSort: typeof import('~icons/mdi/sort')['default'] MdiSort: typeof import('~icons/mdi/sort')['default']
MdiStar: typeof import('~icons/mdi/star')['default'] MdiStar: typeof import('~icons/mdi/star')['default']

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

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

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

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

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

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

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ReadonlyInj, inject, ref, useProject, watch } from '#imports' import { ActiveCellInj, ReadonlyInj, inject, ref, useProject, useSelectedCellKeyupListener, watch } from '#imports'
interface Props { interface Props {
modelValue?: string | null modelValue?: string | null
@ -15,6 +15,10 @@ const { isMysql } = useProject()
const readOnly = inject(ReadonlyInj, ref(false)) const readOnly = inject(ReadonlyInj, ref(false))
const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
let isDateInvalid = $ref(false) let isDateInvalid = $ref(false)
const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ' const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
@ -56,6 +60,21 @@ watch(
}, },
{ flush: 'post' }, { 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> </script>
<template> <template>
@ -68,10 +87,10 @@ watch(
:placeholder="isDateInvalid ? 'Invalid date' : ''" :placeholder="isDateInvalid ? 'Invalid date' : ''"
:allow-clear="!readOnly && !localState && !isPk" :allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true" :input-read-only="true"
:dropdown-class-name="`${randomClass} nc-picker-datetime`" :dropdown-class-name="`${randomClass} nc-picker-datetime ${open ? 'active' : ''}`"
:open="readOnly || (localState && isPk) ? false : open" :open="readOnly || (localState && isPk) ? false : open && (active || editable)"
:disabled="readOnly || (localState && isPk)" :disabled="readOnly || (localState && isPk)"
@click="open = !open" @click="open = (active || editable) && !open"
@ok="open = !open" @ok="open = !open"
> >
<template #suffixIcon></template> <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" type="number"
step="0.1" step="0.1"
@blur="editEnabled = false" @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> <span v-else class="text-sm">{{ vModel }}</span>
</template> </template>

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

@ -84,6 +84,11 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
@blur="submitDuration" @blur="submitDuration"
@keypress="checkDurationFormat($event)" @keypress="checkDurationFormat($event)"
@keydown.enter="submitDuration" @keydown.enter="submitDuration"
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
/> />
<span v-else> {{ localState }}</span> <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> </script>
<template> <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"> <a v-else-if="validEmail" class="text-sm underline hover:opacity-75" :href="`mailto:${vModel}`" target="_blank">
{{ vModel }} {{ vModel }}

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

@ -30,6 +30,11 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
type="number" type="number"
step="0.1" step="0.1"
@blur="editEnabled = false" @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> <span v-else class="text-sm">{{ vModel }}</span>
</template> </template>

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

@ -34,6 +34,11 @@ function onKeyDown(evt: KeyboardEvent) {
type="number" type="number"
@blur="editEnabled = false" @blur="editEnabled = false"
@keydown="onKeyDown" @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> <span v-else class="text-sm">{{ vModel }}</span>
</template> </template>

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

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

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

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ColumnInj, EditModeInj, computed, inject } from '#imports' import { ActiveCellInj, ColumnInj, computed, inject, useSelectedCellKeyupListener } from '#imports'
interface Props { interface Props {
modelValue?: number | null | undefined modelValue?: number | null | undefined
@ -11,8 +11,6 @@ const emits = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj)
const ratingMeta = computed(() => { const ratingMeta = computed(() => {
return { return {
icon: { icon: {
@ -29,16 +27,17 @@ const vModel = computed({
get: () => modelValue ?? NaN, get: () => modelValue ?? NaN,
set: (val) => emits('update:modelValue', val), 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> </script>
<template> <template>
<a-rate <a-rate v-model:value="vModel" :count="ratingMeta.max" :style="`color: ${ratingMeta.color}; padding: 0px 5px`">
v-model:value="vModel"
:count="ratingMeta.max"
:style="`color: ${ratingMeta.color}; padding: 0px 5px`"
:class="{ '!ml-[-8px]': !editEnabled }"
:disabled="!editEnabled"
>
<template #character> <template #character>
<MdiStar v-if="ratingMeta.icon.full === 'mdi-star'" class="text-sm" /> <MdiStar v-if="ratingMeta.icon.full === 'mdi-star'" class="text-sm" />
<MdiHeart v-if="ratingMeta.icon.full === 'mdi-heart'" 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 { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionType } from 'nocodb-sdk' import type { SelectOptionType } from 'nocodb-sdk'
import { ActiveCellInj, ColumnInj, IsKanbanInj, ReadonlyInj, computed, inject, ref, useEventListener, watch } from '#imports' import { ActiveCellInj, ColumnInj, IsKanbanInj, ReadonlyInj, computed, inject, ref, useEventListener, watch } from '#imports'
import { useSelectedCellKeyupListener } from '~/composables/useSelectedCellKeyupListener'
interface Props { interface Props {
modelValue?: string | undefined modelValue?: string | undefined
@ -44,15 +45,6 @@ const options = computed<SelectOptionType[]>(() => {
return [] return []
}) })
const handleKeys = (e: KeyboardEvent) => {
switch (e.key) {
case 'Escape':
e.preventDefault()
isOpen.value = false
break
}
}
const handleClose = (e: MouseEvent) => { const handleClose = (e: MouseEvent) => {
if (aselect.value && !aselect.value.$el.contains(e.target)) { if (aselect.value && !aselect.value.$el.contains(e.target)) {
isOpen.value = false isOpen.value = false
@ -63,7 +55,24 @@ const handleClose = (e: MouseEvent) => {
useEventListener(document, 'click', handleClose) useEventListener(document, 'click', handleClose)
watch(isOpen, (n, _o) => { 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> </script>
@ -77,10 +86,10 @@ watch(isOpen, (n, _o) => {
:open="isOpen" :open="isOpen"
:disabled="readOnly" :disabled="readOnly"
:show-arrow="!readOnly && (active || vModel === null)" :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" @select="isOpen = false"
@keydown="handleKeys" @keydown.enter.stop
@click="isOpen = !isOpen" @click="isOpen = active && !isOpen"
> >
<a-select-option <a-select-option
v-for="op of options" v-for="op of options"
@ -110,9 +119,11 @@ watch(isOpen, (n, _o) => {
.rounded-tag { .rounded-tag {
@apply py-0 px-[12px] rounded-[12px]; @apply py-0 px-[12px] rounded-[12px];
} }
:deep(.ant-tag) { :deep(.ant-tag) {
@apply "rounded-tag"; @apply "rounded-tag";
} }
:deep(.ant-select-clear) { :deep(.ant-select-clear) {
opacity: 1; 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="h-full w-full outline-none bg-transparent"
:class="{ '!px-2': editEnabled }" :class="{ '!px-2': editEnabled }"
@blur="editEnabled = false" @blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
/> />
<span v-else>{{ vModel }}</span> <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" @blur="editEnabled = false"
@keydown.alt.enter.stop @keydown.alt.enter.stop
@keydown.shift.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> <span v-else>{{ vModel }}</span>

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

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

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

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

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' 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 { interface Props {
modelValue?: number | string | null modelValue?: number | string | null
@ -13,6 +13,10 @@ const emit = defineEmits(['update:modelValue'])
const readOnly = inject(ReadonlyInj, ref(false)) const readOnly = inject(ReadonlyInj, ref(false))
const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
let isYearInvalid = $ref(false) let isYearInvalid = $ref(false)
const localState = $computed({ const localState = $computed({
@ -55,6 +59,21 @@ watch(
) )
const placeholder = computed(() => (isYearInvalid ? 'Invalid year' : '')) 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> </script>
<template> <template>
@ -66,10 +85,10 @@ const placeholder = computed(() => (isYearInvalid ? 'Invalid year' : ''))
:placeholder="placeholder" :placeholder="placeholder"
:allow-clear="!readOnly && !localState && !isPk" :allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true" :input-read-only="true"
:open="readOnly || (localState && isPk) ? false : open" :open="(readOnly || (localState && isPk)) && !active && !editable ? false : open"
:dropdown-class-name="`${randomClass} nc-picker-year`" :dropdown-class-name="`${randomClass} nc-picker-year ${open ? 'active' : ''}`"
@click="open = !open" @click="open = (active || editable) && !open"
@change="open = !open" @change="open = (active || editable) && !open"
> >
<template #suffixIcon></template> <template #suffixIcon></template>
</a-date-picker> </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 { useProvideAttachmentCell } from './utils'
import { useSortable } from './sort' import { useSortable } from './sort'
import { import {
ActiveCellInj,
DropZoneRef, DropZoneRef,
IsGalleryInj, IsGalleryInj,
IsKanbanInj, IsKanbanInj,
@ -12,6 +13,7 @@ import {
openLink, openLink,
ref, ref,
useDropZone, useDropZone,
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow, useSmartsheetRowStoreOrThrow,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
watch, watch,
@ -113,9 +115,17 @@ watch(
attachments.value = [] attachments.value = []
} }
} }
} else {
if (isPublic.value && isForm.value) {
storedFiles.value = []
} else {
attachments.value = []
}
} }
}, },
{ immediate: true }, {
immediate: true,
},
) )
/** updates attachments array for autosave */ /** updates attachments array for autosave */
@ -136,6 +146,13 @@ watch(
rowState.value[column.value!.title!] = storedFiles.value 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> </script>
<template> <template>

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

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

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

@ -22,6 +22,7 @@ import {
extractPkFromRow, extractPkFromRow,
inject, inject,
isColumnRequiredAndNull, isColumnRequiredAndNull,
isMac,
message, message,
onBeforeUnmount, onBeforeUnmount,
onClickOutside, onClickOutside,
@ -168,52 +169,118 @@ const { selectCell, selectBlock, selectedRange, clearRangeRows, startSelectRange
isPkAvail, isPkAvail,
clearCell, clearCell,
makeEditable, makeEditable,
(row?: number | null, col?: number | null) => { scrollToCell,
row = row ?? selected.row (e: KeyboardEvent) => {
col = col ?? selected.col // ignore navigating if picker(Date, Time, DateTime, Year)
if (row !== undefined && col !== undefined && row !== null && col !== null) { // or single/multi select options is open
// get active cell const activePickerOrDropdownEl = document.querySelector(
const rows = tbodyEl.value?.querySelectorAll('tr') '.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',
const cols = rows?.[row].querySelectorAll('td') )
const td = cols?.[col === 0 ? 0 : col + 1] if (activePickerOrDropdownEl) {
e.preventDefault()
if (!td || !gridWrapper.value) return return true
}
const { height: headerHeight } = tableHead.value!.getBoundingClientRect()
const tdScroll = getContainerScrollForElement(td, gridWrapper.value, { top: headerHeight, bottom: 9, right: 9 }) // if expanded form is active skip keyboard event handling
if (document.querySelector('.nc-drawer-expanded-form.active')) {
if (rows && row === rows.length - 2) { return true
// 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) { const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
// if last column make 'Add New Column' visible if (e.key === ' ') {
gridWrapper.value.scrollTo({ if (selected.row !== null && !editEnabled) {
top: tdScroll.top, e.preventDefault()
left: gridWrapper.value.scrollWidth, const row = data.value[selected.row]
behavior: 'smooth', expandForm(row)
}) return true
return }
} 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({ gridWrapper.value.scrollTo({
top: tdScroll.top, top: tdScroll.top,
left: tdScroll.left, left: gridWrapper.value.scrollWidth,
behavior: 'smooth', behavior: 'smooth',
}) })
return
} }
},
) // scroll into the active cell
gridWrapper.value.scrollTo({
top: tdScroll.top,
left: tdScroll.left,
behavior: 'smooth',
})
}
}
onMounted(loadGridViewColumns) onMounted(loadGridViewColumns)
@ -234,7 +301,7 @@ const showLoading = ref(true)
const skipRowRemovalOnCancel = ref(false) 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[]) const rowId = extractPkFromRow(row.row, meta.value?.columns as ColumnType[])
if (rowId) { if (rowId) {
@ -325,7 +392,7 @@ useEventListener(document, 'keyup', async (e: KeyboardEvent) => {
/** On clicking outside of table reset active cell */ /** On clicking outside of table reset active cell */
const smartTable = ref(null) const smartTable = ref(null)
onClickOutside(smartTable, () => { onClickOutside(smartTable, (e) => {
clearRangeRows() clearRangeRows()
if (selected.col === null) return if (selected.col === null) return
@ -333,6 +400,23 @@ onClickOutside(smartTable, () => {
if (editEnabled && (isVirtualCol(activeCol) || activeCol.uidt === UITypes.JSON)) return 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.row = null
selected.col = null selected.col = null
}) })
@ -383,8 +467,8 @@ const saveOrUpdateRecords = async (args: { metaValue?: TableType; viewMetaValue?
currentRow.rowMeta.changed = false currentRow.rowMeta.changed = false
for (const field of (args.metaValue || meta.value)?.columns ?? []) { for (const field of (args.metaValue || meta.value)?.columns ?? []) {
if (isVirtualCol(field)) continue if (isVirtualCol(field)) continue
if (field.title! in currentRow.row && currentRow.row[field.title!] !== currentRow.oldRow[field.title!]) { if (currentRow.row[field.title!] !== currentRow.oldRow[field.title!]) {
await updateOrSaveRow(currentRow, field.title!, {}, args) 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' }" :body-style="{ 'padding': 0, 'display': 'flex', 'flex-direction': 'column' }"
:closable="false" :closable="false"
class="nc-drawer-expanded-form" class="nc-drawer-expanded-form"
:class="{ 'active': isExpanded }"
> >
<SmartsheetExpandedFormHeader :view="props.view" @cancel="onClose" /> <SmartsheetExpandedFormHeader :view="props.view" @cancel="onClose" />

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

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

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

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue' 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 // todo: column type doesn't have required property `error` - throws in typecheck
const column = inject(ColumnInj) as Ref<ColumnType & { colOptions: { error: any } }> const column = inject(ColumnInj) as Ref<ColumnType & { colOptions: { error: any } }>
@ -10,19 +10,25 @@ const cellValue = inject(CellValueInj)
const { isPg } = useProject() 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 result = computed(() => (isPg.value ? handleTZ(cellValue?.value) : cellValue?.value))
const urls = computed(() => replaceUrlsWithLink(result.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> </script>
<template> <template>
@ -35,15 +41,19 @@ const urls = computed(() => replaceUrlsWithLink(result.value))
<span>ERR!</span> <span>ERR!</span>
</a-tooltip> </a-tooltip>
<div class="p-2" @dblclick="showEditFormulaWarningMessage"> <div class="p-2" @dblclick="showEditFormulaWarning = true">
<div v-if="urls" v-html="urls" /> <div v-if="urls" v-html="urls" />
<div v-else>{{ result }}</div> <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 --> <!-- TODO: i18n -->
Warning: Formula fields should be configured in the field menu dropdown. Warning: Formula fields should be configured in the field menu dropdown.
</div> </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>
</div> </div>
</template> </template>

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

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

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

@ -11,6 +11,7 @@ import {
computed, computed,
inject, inject,
provide, provide,
refAutoReset,
useColumn, useColumn,
useMetas, useMetas,
} from '#imports' } from '#imports'
@ -46,45 +47,73 @@ provide(MetaInj, lookupTableMeta)
provide(CellUrlDisableOverlayInj, ref(true)) provide(CellUrlDisableOverlayInj, ref(true))
const lookupColumnMetaProps = useColumn(lookupColumn) 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> </script>
<template> <template>
<div class="h-full flex gap-1 overflow-x-auto p-1"> <div class="h-full">
<template v-if="lookupColumn"> <div class="h-full flex gap-1 overflow-x-auto p-1">
<!-- Render virtual cell --> <template v-if="lookupColumn">
<div v-if="isVirtualCol(lookupColumn)"> <!-- Render virtual cell -->
<template <div v-if="isVirtualCol(lookupColumn)">
v-if="lookupColumn.uidt === UITypes.LinkToAnotherRecord && lookupColumn.colOptions.type === RelationTypes.BELONGS_TO" <template
> v-if="lookupColumn.uidt === UITypes.LinkToAnotherRecord && lookupColumn.colOptions.type === RelationTypes.BELONGS_TO"
<LazySmartsheetVirtualCell >
<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" v-for="(v, i) of arrValue"
:key="i" :key="i"
:edit-enabled="false" class="min-w-max"
:model-value="v" :class="{
:column="lookupColumn" '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>
<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>
</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> </div>
</template> </template>

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

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

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

@ -1,11 +1,40 @@
<script setup lang="ts"> <script setup lang="ts">
import { CellValueInj, inject } from '#imports' import { CellValueInj, inject, refAutoReset } from '#imports'
const value = inject(CellValueInj) 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> </script>
<template> <template>
<span class="text-center pl-3"> <div>
{{ value }} <span class="text-center pl-3">
</span> {{ 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> </template>

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

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

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

@ -1,6 +1,6 @@
import type { MaybeRef } from '@vueuse/core' import type { MaybeRef } from '@vueuse/core'
import { UITypes } from 'nocodb-sdk' 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 { interface SelectedBlock {
row: number | null row: number | null
@ -13,16 +13,19 @@ interface SelectedBlock {
export function useMultiSelect( export function useMultiSelect(
fields: MaybeRef<any[]>, fields: MaybeRef<any[]>,
data: MaybeRef<any[]>, data: MaybeRef<any[]>,
editEnabled: MaybeRef<boolean>, _editEnabled: MaybeRef<boolean>,
isPkAvail: MaybeRef<boolean | undefined>, isPkAvail: MaybeRef<boolean | undefined>,
clearCell: Function, clearCell: Function,
makeEditable: Function, makeEditable: Function,
scrollToActiveCell?: (row?: number | null, col?: number | null) => void, scrollToActiveCell?: (row?: number | null, col?: number | null) => void,
keyEventHandler?: Function,
) { ) {
const { t } = useI18n() const { t } = useI18n()
const { copy } = useCopy() const { copy } = useCopy()
const editEnabled = ref(_editEnabled)
const selected = reactive<SelectedBlock>({ row: null, col: null }) 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 // 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) { function selectCell(row: number, col: number) {
clearRangeRows() clearRangeRows()
editEnabled.value = false
selected.row = row selected.row = row
selected.col = col selected.col = col
} }
@ -126,6 +130,11 @@ export function useMultiSelect(
}) })
const onKeyDown = async (e: KeyboardEvent) => { const onKeyDown = async (e: KeyboardEvent) => {
// invoke the keyEventHandler if provided and return if it returns true
if (await keyEventHandler?.(e)) {
return
}
if ( if (
!isNaN(selectedRows.startRow) && !isNaN(selectedRows.startRow) &&
!isNaN(selectedRows.startCol) && !isNaN(selectedRows.startCol) &&
@ -148,16 +157,20 @@ export function useMultiSelect(
if (e.shiftKey) { if (e.shiftKey) {
if (selected.col > 0) { if (selected.col > 0) {
selected.col-- selected.col--
editEnabled.value = false
} else if (selected.row > 0) { } else if (selected.row > 0) {
selected.row-- selected.row--
selected.col = unref(columnLength) - 1 selected.col = unref(columnLength) - 1
editEnabled.value = false
} }
} else { } else {
if (selected.col < unref(columnLength) - 1) { if (selected.col < unref(columnLength) - 1) {
selected.col++ selected.col++
editEnabled.value = false
} else if (selected.row < unref(data).length - 1) { } else if (selected.row < unref(data).length - 1) {
selected.row++ selected.row++
selected.col = 0 selected.col = 0
editEnabled.value = false
} }
} }
scrollToActiveCell?.() scrollToActiveCell?.()
@ -170,11 +183,9 @@ export function useMultiSelect(
break break
/** on delete key press clear cell */ /** on delete key press clear cell */
case 'Delete': case 'Delete':
if (!unref(editEnabled)) { e.preventDefault()
e.preventDefault() clearRangeRows()
clearRangeRows() await clearCell(selected as { row: number; col: number })
await clearCell(selected as { row: number; col: number })
}
break break
/** on arrow key press navigate through cells */ /** on arrow key press navigate through cells */
case 'ArrowRight': case 'ArrowRight':
@ -183,6 +194,7 @@ export function useMultiSelect(
if (selected.col < unref(columnLength) - 1) { if (selected.col < unref(columnLength) - 1) {
selected.col++ selected.col++
scrollToActiveCell?.() scrollToActiveCell?.()
editEnabled.value = false
} }
break break
case 'ArrowLeft': case 'ArrowLeft':
@ -192,6 +204,7 @@ export function useMultiSelect(
if (selected.col > 0) { if (selected.col > 0) {
selected.col-- selected.col--
scrollToActiveCell?.() scrollToActiveCell?.()
editEnabled.value = false
} }
break break
case 'ArrowUp': case 'ArrowUp':
@ -201,6 +214,7 @@ export function useMultiSelect(
if (selected.row > 0) { if (selected.row > 0) {
selected.row-- selected.row--
scrollToActiveCell?.() scrollToActiveCell?.()
editEnabled.value = false
} }
break break
case 'ArrowDown': case 'ArrowDown':
@ -210,6 +224,7 @@ export function useMultiSelect(
if (selected.row < unref(data).length - 1) { if (selected.row < unref(data).length - 1) {
selected.row++ selected.row++
scrollToActiveCell?.() scrollToActiveCell?.()
editEnabled.value = false
} }
break break
default: 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'), okText: t('general.yes'),
okType: 'danger', okType: 'danger',
cancelText: t('general.no'), cancelText: t('general.no'),
width: 450,
async onOk() { async onOk() {
try { try {
const meta = (await getMeta(table.id as string, true)) as TableType 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

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

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

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

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

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

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

@ -5,6 +5,7 @@ import NcMetaIO from '../meta/NcMetaIO';
import ncProjectEnvUpgrader from './ncProjectEnvUpgrader'; import ncProjectEnvUpgrader from './ncProjectEnvUpgrader';
import ncProjectEnvUpgrader0011045 from './ncProjectEnvUpgrader0011045'; import ncProjectEnvUpgrader0011045 from './ncProjectEnvUpgrader0011045';
import ncProjectUpgraderV2_0090000 from './ncProjectUpgraderV2_0090000'; import ncProjectUpgraderV2_0090000 from './ncProjectUpgraderV2_0090000';
import ncDataTypesUpgrader from './ncDataTypesUpgrader';
const log = debug('nc:version-upgrader'); const log = debug('nc:version-upgrader');
import { Tele } from 'nc-help'; import { Tele } from 'nc-help';
@ -31,6 +32,7 @@ export default class NcUpgrader {
{ name: '0011043', handler: ncProjectEnvUpgrader }, { name: '0011043', handler: ncProjectEnvUpgrader },
{ name: '0011045', handler: ncProjectEnvUpgrader0011045 }, { name: '0011045', handler: ncProjectEnvUpgrader0011045 },
{ name: '0090000', handler: ncProjectUpgraderV2_0090000 }, { name: '0090000', handler: ncProjectUpgraderV2_0090000 },
{ name: '0098004', handler: ncDataTypesUpgrader },
]; ];
if (!(await ctx.ncMeta.knexConnection?.schema?.hasTable?.('nc_store'))) { if (!(await ctx.ncMeta.knexConnection?.schema?.hasTable?.('nc_store'))) {
return; 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 express from 'express';
import Noco from '../lib/Noco'; import Noco from '../lib/Noco';
process.env.NC_VERSION = '0009044';
const server = express(); const server = express();
server.enable('trust proxy'); server.enable('trust proxy');

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

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

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

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

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

@ -25,7 +25,14 @@ export class SelectOptionCellPageObject extends BasePage {
option: string; option: string;
multiSelect?: boolean; 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(); 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[] }) { 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(); await this.get({ index, columnHeader }).click();
let counter = 0; let counter = 0;

Loading…
Cancel
Save