Browse Source

Merge pull request #7298 from nocodb/fix/expanded-record-ux

Fix/expanded record ux
pull/7330/head
Raju Udava 11 months ago committed by GitHub
parent
commit
9c4efd44db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      packages/nc-gui/components/cell/Checkbox.vue
  2. 34
      packages/nc-gui/components/cell/Currency.vue
  3. 9
      packages/nc-gui/components/cell/DatePicker.vue
  4. 7
      packages/nc-gui/components/cell/DateTimePicker.vue
  5. 10
      packages/nc-gui/components/cell/Decimal.vue
  6. 10
      packages/nc-gui/components/cell/Duration.vue
  7. 9
      packages/nc-gui/components/cell/Email.vue
  8. 7
      packages/nc-gui/components/cell/Float.vue
  9. 19
      packages/nc-gui/components/cell/GeoData.vue
  10. 13
      packages/nc-gui/components/cell/Integer.vue
  11. 9
      packages/nc-gui/components/cell/MultiSelect.vue
  12. 40
      packages/nc-gui/components/cell/Percent.vue
  13. 10
      packages/nc-gui/components/cell/PhoneNumber.vue
  14. 14
      packages/nc-gui/components/cell/Rating.vue
  15. 3
      packages/nc-gui/components/cell/RichText.vue
  16. 5
      packages/nc-gui/components/cell/SingleSelect.vue
  17. 23
      packages/nc-gui/components/cell/Text.vue
  18. 17
      packages/nc-gui/components/cell/TextArea.vue
  19. 7
      packages/nc-gui/components/cell/TimePicker.vue
  20. 9
      packages/nc-gui/components/cell/Url.vue
  21. 9
      packages/nc-gui/components/cell/User.vue
  22. 9
      packages/nc-gui/components/cell/YearPicker.vue
  23. 2
      packages/nc-gui/components/cell/attachment/index.vue
  24. 23
      packages/nc-gui/components/smartsheet/DivDataCell.vue
  25. 17
      packages/nc-gui/components/smartsheet/Form.vue
  26. 2
      packages/nc-gui/components/smartsheet/column/DefaultValue.vue
  27. 29
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  28. 25
      packages/nc-gui/components/virtual-cell/Formula.vue
  29. 20
      packages/nc-gui/components/virtual-cell/Links.vue
  30. 16
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue
  31. 6
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue

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

@ -84,7 +84,7 @@ useSelectedCellKeyupListener(active, (e) => {
<template>
<div
class="flex cursor-pointer w-full h-full items-center focus:outline-transparent"
class="flex cursor-pointer w-full h-full items-center focus:outline-none"
:class="{
'w-full flex-start pl-2': isForm || isGallery || isExpandedFormOpen,
'w-full justify-center': !isForm && !isGallery && !isExpandedFormOpen,
@ -97,7 +97,7 @@ useSelectedCellKeyupListener(active, (e) => {
}"
tabindex="0"
@click="onClick(false, $event)"
@keydown.enter.stop="onClick(false, $event)"
@keydown.enter.stop="onClick(true, $event)"
>
<div
class="flex items-center"

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

@ -1,6 +1,16 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { ColumnInj, EditColumnInj, EditModeInj, IsExpandedFormOpenInj, computed, inject, parseProp, useVModel } from '#imports'
import {
ColumnInj,
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
computed,
inject,
parseProp,
useVModel,
} from '#imports'
interface Props {
modelValue: number | null | undefined
@ -57,7 +67,10 @@ const currency = computed(() => {
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
const submitCurrency = () => {
if (lastSaved.value !== vModel.value) {
@ -78,7 +91,8 @@ onMounted(() => {
:ref="focus"
v-model="vModel"
type="number"
class="w-full h-full text-sm border-none rounded-md outline-none focus:outline-transparent focus:ring-0"
class="w-full h-full text-sm border-none rounded-md py-1 outline-none focus:outline-none focus:ring-0"
:class="isExpandedFormOpen ? 'px-2' : 'px-0'"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="submitCurrency"
@keydown.down.stop
@ -99,3 +113,17 @@ onMounted(() => {
<!-- possibly unexpected string / null with showNull == false -->
<span v-else />
</template>
<style lang="scss" scoped>
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type='number'] {
-moz-appearance: textfield;
}
</style>

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

@ -7,6 +7,7 @@ import {
ColumnInj,
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
ReadonlyInj,
computed,
inject,
@ -41,6 +42,8 @@ const readOnly = inject(ReadonlyInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))
const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
@ -238,10 +241,10 @@ const clickHandler = () => {
<a-date-picker
v-model:value="localState"
:picker="picker"
tabindex="0"
:tabindex="0"
:bordered="false"
class="!w-full !px-1 !border-none"
:class="{ 'nc-null': modelValue === null && showNull }"
class="!w-full !py-1 !border-none"
:class="{ 'nc-null': modelValue === null && showNull, '!px-2': isExpandedFormOpen, '!px-0': !isExpandedFormOpen }"
:format="dateFormat"
:placeholder="placeholder"
:allow-clear="!readOnly && !localState && !isPk"

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

@ -6,6 +6,7 @@ import {
CellClickHookInj,
ColumnInj,
EditColumnInj,
IsExpandedFormOpenInj,
ReadonlyInj,
inject,
isDrawerOrModalExist,
@ -39,6 +40,8 @@ const { t } = useI18n()
const isEditColumn = inject(EditColumnInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))
const column = inject(ColumnInj)!
const isDateInvalid = ref(false)
@ -293,8 +296,8 @@ const isColDisabled = computed(() => {
:disabled="isColDisabled"
:show-time="true"
:bordered="false"
class="!w-full !px-0 !py-1 !border-none"
:class="{ 'nc-null': modelValue === null && showNull }"
class="!w-full !py-1 !border-none"
:class="{ 'nc-null': modelValue === null && showNull, '!px-2': isExpandedFormOpen, '!px-0': !isExpandedFormOpen }"
:format="dateTimeFormat"
:placeholder="placeholder"
:allow-clear="!readOnly && !localState && !isPk"

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

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, inject, useVModel } from '#imports'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, IsFormInj, inject, useVModel } from '#imports'
interface Props {
// when we set a number, then it is number type
@ -63,6 +63,8 @@ const precision = computed(() => {
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
// Handle the arrow keys as its default behavior is to increment/decrement the value
const onKeyDown = (e: any) => {
if (e.key === 'ArrowDown') {
@ -80,7 +82,8 @@ const onKeyDown = (e: any) => {
}
}
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
watch(isExpandedFormOpen, () => {
if (!isExpandedFormOpen.value) {
@ -94,7 +97,8 @@ watch(isExpandedFormOpen, () => {
v-if="editEnabled"
:ref="focus"
v-model="vModel"
class="outline-none !py-2 !px-1 border-none rounded-md w-full h-full !text-sm"
class="outline-none py-1 border-none rounded-md w-full h-full !text-sm"
:class="isExpandedFormOpen ? 'px-2' : 'px-0'"
type="number"
:step="precision"
:placeholder="isEditColumn ? $t('labels.optional') : ''"

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

@ -5,6 +5,7 @@ import {
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
computed,
convertDurationToSeconds,
convertMS2Duration,
@ -83,7 +84,10 @@ const submitDuration = () => {
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
</script>
<template>
@ -92,8 +96,8 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value
v-if="editEnabled"
:ref="focus"
v-model="localState"
class="w-full !border-none !outline-none p-0"
:class="{ '!px-2 !py-1': editEnabled }"
class="w-full !border-none !outline-none py-1"
:class="isExpandedFormOpen ? 'px-2' : 'px-0'"
:placeholder="durationPlaceholder"
@blur="submitDuration"
@keypress="checkDurationFormat($event)"

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

@ -4,6 +4,7 @@ import {
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
IsSurveyFormInj,
computed,
inject,
@ -50,7 +51,10 @@ const validEmail = computed(() => vModel.value && validateEmail(vModel.value))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
watch(
() => editEnabled.value,
@ -70,7 +74,8 @@ watch(
v-if="editEnabled"
:ref="focus"
v-model="vModel"
class="w-full outline-none text-sm px-1 py-2"
class="w-full outline-none text-sm py-1"
:class="isExpandedFormOpen ? 'px-2' : 'px-0'"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.down.stop

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

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, inject, useVModel } from '#imports'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, IsFormInj, inject, useVModel } from '#imports'
interface Props {
// when we set a number, then it is number type
@ -40,7 +40,10 @@ const vModel = computed({
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
</script>
<template>

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

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { GeoLocationType } from 'nocodb-sdk'
import { Modal as AModal, iconMap, latLongToJoinedString, useVModel } from '#imports'
import { Modal as AModal, IsExpandedFormOpenInj, iconMap, latLongToJoinedString, useVModel } from '#imports'
interface Props {
modelValue?: string | null
@ -16,6 +16,8 @@ const emits = defineEmits<Emits>()
const vModel = useVModel(props, 'modelValue', emits)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))
const isExpanded = ref(false)
const isLoading = ref(false)
@ -86,7 +88,8 @@ const openInOSM = () => {
<a-dropdown :is="isExpanded ? AModal : 'div'" v-model:visible="isExpanded" :trigger="['click']">
<div
v-if="!isLocationSet"
class="group cursor-pointer flex gap-1 items-center mx-auto max-w-64 justify-center active:(ring ring-accent ring-opacity-100) rounded border-1 p-1 shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
class="group cursor-pointer flex gap-1 items-center mx-auto max-w-64 justify-center active:(ring ring-accent ring-opacity-100) rounded border-1 p-1 shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500) my-1"
tabindex="0"
>
<div class="flex items-center gap-2" data-testid="nc-geo-data-set-location-button">
<component
@ -98,7 +101,17 @@ const openInOSM = () => {
</div>
</div>
</div>
<div v-else data-testid="nc-geo-data-lat-long-set">{{ latLongStr }}</div>
<div
v-else
data-testid="nc-geo-data-lat-long-set"
tabindex="0"
class="h-full w-full flex items-center py-1 focus-visible:!outline-none focus:!outline-none"
:class="{
'px-2': isExpandedFormOpen,
}"
>
{{ latLongStr }}
</div>
<template #overlay>
<a-form :model="formState" class="flex flex-col w-max-64 border-1 border-gray-200" @finish="handleFinish">
<a-form-item>

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, inject, useVModel } from '#imports'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, IsFormInj, inject, useVModel } from '#imports'
interface Props {
// when we set a number, then it is number type
@ -48,7 +48,10 @@ const vModel = computed({
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
function onKeyDown(e: any) {
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
@ -85,11 +88,9 @@ function onKeyDown(e: any) {
v-if="editEnabled"
:ref="focus"
v-model="vModel"
class="outline-none py-2 px-1 border-none w-full h-full text-sm"
class="outline-none py-1 border-none w-full h-full text-sm"
:class="isExpandedFormOpen ? 'px-2' : 'px-0'"
type="number"
:class="{
'pl-2': isExpandedFormOpen,
}"
style="letter-spacing: 0.06rem"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"

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

@ -8,6 +8,7 @@ import {
ColumnInj,
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsKanbanInj,
ReadonlyInj,
RowHeightInj,
@ -63,6 +64,8 @@ const isEditColumn = inject(EditColumnInj, ref(false))
const rowHeight = inject(RowHeightInj, ref(undefined))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const selectedIds = ref<string[]>([])
const aselect = ref<typeof AntSelect>()
@ -351,7 +354,11 @@ const onFocus = () => {
</script>
<template>
<div class="nc-multi-select h-full w-full flex items-center" :class="{ 'read-only': readOnly }" @click="toggleMenu">
<div
class="nc-multi-select h-full w-full flex items-center"
:class="{ 'read-only': readOnly, 'px-2': isExpandedFormOpen }"
@click="toggleMenu"
>
<div
v-if="!active"
class="flex flex-wrap"

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, inject, useVModel } from '#imports'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, IsFormInj, inject, useVModel } from '#imports'
interface Props {
modelValue?: number | string | null
@ -35,7 +35,10 @@ const vModel = computed({
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
const cellFocused = ref(false)
@ -69,6 +72,7 @@ const onWrapperFocus = () => {
nextTick(() => {
wrapperRef.value?.querySelector('input')?.focus()
wrapperRef.value?.querySelector('input')?.select()
})
}
@ -83,18 +87,22 @@ const onMouseleave = () => {
}
const onTabPress = (e: KeyboardEvent) => {
if (e.shiftKey) {
if (e.shiftKey && (isExpandedFormOpen.value || isForm.value)) {
e.preventDefault()
// Shift + Tab does not work for percent cell
// so we manually focus on the last form item
const focusesNcCellIndex = Array.from(document.querySelectorAll('.nc-expanded-form-row .nc-data-cell')).findIndex((el) => {
const focusesNcCellIndex = Array.from(
document.querySelectorAll(`${isExpandedFormOpen.value ? '.nc-expanded-form-row' : '.nc-form-wrapper'} .nc-data-cell`),
).findIndex((el) => {
return el.querySelector('.nc-filter-value-select') === wrapperRef.value
})
if (focusesNcCellIndex >= 0) {
const nodes = document.querySelectorAll('.nc-expanded-form-row .nc-data-cell')
const nodes = document.querySelectorAll(
`${isExpandedFormOpen.value ? '.nc-expanded-form-row' : '.nc-form-wrapper'} .nc-data-cell`,
)
for (let i = focusesNcCellIndex - 1; i >= 0; i--) {
const lastFormItem = nodes[i].querySelector('[tabindex="0"]') as HTMLElement
if (lastFormItem) {
@ -117,11 +125,11 @@ const onTabPress = (e: KeyboardEvent) => {
@focus="onWrapperFocus"
>
<input
v-if="(!isExpandedFormOpen && editEnabled) || (isExpandedFormOpen && expandedEditEnabled)"
v-if="editEnabled"
:ref="focus"
v-model="vModel"
class="w-full !text-sm !border-none !outline-none focus:ring-0 text-base p-1"
:class="{ '!px-2': editEnabled }"
class="w-full !text-sm !border-none !outline-none focus:ring-0 text-base py-1"
:class="isExpandedFormOpen ? 'px-2' : 'px-0'"
type="number"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="onBlur"
@ -150,3 +158,17 @@ const onTabPress = (e: KeyboardEvent) => {
<span v-else>{{ vModel }}&nbsp;</span>
</div>
</template>
<style lang="scss" scoped>
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type='number'] {
-moz-appearance: textfield;
}
</style>

10
packages/nc-gui/components/cell/PhoneNumber.vue

@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import isMobilePhone from 'validator/lib/isMobilePhone'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, IsSurveyFormInj, computed, inject } from '#imports'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, IsFormInj, IsSurveyFormInj, computed, inject } from '#imports'
interface Props {
modelValue: string | null | number | undefined
@ -42,7 +42,10 @@ const validEmail = computed(() => vModel.value && isMobilePhone(vModel.value))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
watch(
() => editEnabled.value,
@ -62,7 +65,8 @@ watch(
v-if="editEnabled"
:ref="focus"
v-model="vModel"
class="w-full outline-none text-sm px-1 py-2"
class="w-full outline-none text-sm py-1"
:class="isExpandedFormOpen ? 'px-2' : 'px-0'"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.down.stop

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

@ -1,5 +1,13 @@
<script setup lang="ts">
import { ActiveCellInj, ColumnInj, computed, inject, parseProp, useSelectedCellKeyupListener } from '#imports'
import {
ActiveCellInj,
ColumnInj,
IsExpandedFormOpenInj,
computed,
inject,
parseProp,
useSelectedCellKeyupListener,
} from '#imports'
interface Props {
modelValue?: number | null | undefined
@ -13,6 +21,8 @@ const column = inject(ColumnInj)!
const readonly = inject(ReadonlyInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const ratingMeta = computed(() => {
return {
icon: {
@ -65,7 +75,7 @@ watch(rateDomRef, () => {
v-model:value="vModel"
:disabled="readonly"
:count="ratingMeta.max"
:style="`color: ${ratingMeta.color}; padding: 0px 5px`"
:style="`color: ${ratingMeta.color}; padding: ${isExpandedFormOpen ? '0px 8px' : '0px 5px'};`"
@keydown="onKeyPress"
>
<template #character>

3
packages/nc-gui/components/cell/RichText.vue

@ -165,11 +165,12 @@ watch(editorDom, () => {
<template>
<div
class="h-full"
class="h-full focus:outline-none"
:class="{
'flex flex-col flex-grow nc-rich-text-full': props.fullMode,
'nc-rich-text-embed flex flex-col pl-1 w-full': !props.fullMode,
}"
tabindex="0"
>
<div v-if="props.showMenu" class="absolute top-0 right-0.5">
<CellRichTextSelectedBubbleMenu v-if="editor" :editor="editor" embed-mode />

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

@ -8,6 +8,7 @@ import {
ColumnInj,
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
IsKanbanInj,
ReadonlyInj,
@ -47,6 +48,8 @@ const activeCell = inject(ActiveCellInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
// use both ActiveCellInj or EditModeInj to determine the active state
// since active will be false in case of form view
const active = computed(() => activeCell.value || isEditable.value || isForm.value)
@ -272,7 +275,7 @@ const onFocus = () => {
<template>
<div
class="h-full w-full flex items-center nc-single-select focus:outline-transparent"
:class="{ 'read-only': readOnly }"
:class="{ 'read-only': readOnly, 'px-2': isExpandedFormOpen }"
@click="toggleMenu"
@keydown.enter.stop.prevent="toggleMenu"
>

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

@ -1,6 +1,16 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, ReadonlyInj, RowHeightInj, inject, ref, useVModel } from '#imports'
import {
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
ReadonlyInj,
RowHeightInj,
inject,
ref,
useVModel,
} from '#imports'
interface Props {
modelValue?: string | null
@ -24,7 +34,10 @@ const vModel = useVModel(props, 'modelValue', emits)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
</script>
<template>
@ -32,11 +45,9 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value
v-if="!readonly && editEnabled"
:ref="focus"
v-model="vModel"
class="h-full w-full outline-none p-2 bg-transparent"
class="h-full w-full outline-none py-1 bg-transparent"
:class="isExpandedFormOpen ? 'px-2' : 'px-0'"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:class="{
'px-1': isExpandedFormOpen,
}"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop

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

@ -38,7 +38,7 @@ const isForm = inject(IsFormInj, ref(false))
const { showNull } = useGlobal()
const vModel = useVModel(props, 'modelValue', emits, { defaultValue: column?.value.cdf ? String(column?.value.cdf) : '' })
const vModel = useVModel(props, 'modelValue', emits)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
@ -55,7 +55,8 @@ const position = ref<
const isDragging = ref(false)
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLTextAreaElement)?.focus()
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && isForm.value && (el as HTMLTextAreaElement)?.focus()
const height = computed(() => {
if (isExpandedFormOpen.value) return 36 * 4
@ -192,6 +193,7 @@ watch(editEnabled, () => {
minHeight: `${height}px !important`,
}"
@dblclick="onExpand"
@keydown.enter="onExpand"
>
<LazyCellRichText v-model:value="vModel" sync-value-change readonly />
</div>
@ -204,7 +206,7 @@ watch(editEnabled, () => {
:class="{
'p-2': editEnabled,
'py-1 h-full': isForm,
'px-1': isExpandedFormOpen,
'px-2': isExpandedFormOpen,
}"
:style="{
minHeight: `${height}px`,
@ -241,13 +243,8 @@ watch(editEnabled, () => {
<NcTooltip
v-if="!isVisible"
placement="bottom"
class="!absolute right-0 bottom-1 hidden nc-text-area-expand-btn"
:class="{
'right-0 bottom-1': editEnabled,
'!bottom-0': !isRichMode,
'top-1 hidden !group-hover:block': isExpandedFormOpen,
'bottom-1': !isExpandedFormOpen,
}"
class="!absolute right-0 hidden nc-text-area-expand-btn group-hover:block"
:class="isExpandedFormOpen || isForm || isRichMode ? 'top-1' : 'bottom-1'"
>
<template #title>{{ $t('title.expand') }}</template>
<NcButton type="secondary" size="xsmall" data-testid="attachment-cell-file-picker-button" @click.stop="onExpand">

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

@ -3,6 +3,7 @@ import dayjs from 'dayjs'
import {
ActiveCellInj,
EditColumnInj,
IsExpandedFormOpenInj,
ReadonlyInj,
inject,
onClickOutside,
@ -34,6 +35,8 @@ const isEditColumn = inject(EditColumnInj, ref(false))
const column = inject(ColumnInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isTimeInvalid = ref(false)
const dateFormat = isMysql(column.value.source_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
@ -130,8 +133,8 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
:bordered="false"
use12-hours
format="HH:mm"
class="!w-full !px-1 !border-none"
:class="{ 'nc-null': modelValue === null && showNull }"
class="!w-full !py-1 !border-none"
:class="{ 'nc-null': modelValue === null && showNull, '!px-2': isExpandedFormOpen, '!px-0': !isExpandedFormOpen }"
:placeholder="placeholder"
:allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true"

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

@ -6,6 +6,7 @@ import {
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
IsSurveyFormInj,
computed,
inject,
@ -70,7 +71,10 @@ const { cellUrlOptions } = useCellUrlConfig(url)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && isForm.value && (el as HTMLInputElement)?.focus()
watch(
() => editEnabled.value,
@ -92,7 +96,8 @@ watch(
:ref="focus"
v-model="vModel"
:placeholder="isEditColumn ? $t('labels.enterDefaultUrlOptional') : ''"
class="outline-none text-sm w-full px-2 py-2 bg-transparent h-full"
class="outline-none text-sm w-full py-1 bg-transparent h-full"
:class="isExpandedFormOpen ? 'px-2' : 'px-0'"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop

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

@ -9,6 +9,7 @@ import {
ColumnInj,
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsKanbanInj,
ReadonlyInj,
RowHeightInj,
@ -48,6 +49,8 @@ const isEditable = inject(EditModeInj, ref(false))
const activeCell = inject(ActiveCellInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const basesStore = useBases()
const { basesUser } = storeToRefs(basesStore)
@ -260,7 +263,11 @@ const filterOption = (input: string, option: any) => {
</script>
<template>
<div class="nc-user-select h-full w-full flex items-center" :class="{ 'read-only': readOnly }" @click="toggleMenu">
<div
class="nc-user-select h-full w-full flex items-center"
:class="{ 'read-only': readOnly, 'px-2': isExpandedFormOpen }"
@click="toggleMenu"
>
<div
v-if="!active"
class="flex flex-wrap"

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

@ -3,6 +3,7 @@ import dayjs from 'dayjs'
import {
ActiveCellInj,
EditColumnInj,
IsExpandedFormOpenInj,
ReadonlyInj,
computed,
inject,
@ -31,6 +32,8 @@ const editable = inject(EditModeInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isYearInvalid = ref(false)
const { t } = useI18n()
@ -113,11 +116,11 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
<template>
<a-date-picker
v-model:value="localState"
tabindex="0"
:tabindex="0"
picker="year"
:bordered="false"
class="!w-full !px-1 !border-none"
:class="{ 'nc-null': modelValue === null && showNull }"
class="!w-full !py-1 !border-none"
:class="{ 'nc-null': modelValue === null && showNull, '!px-2': isExpandedFormOpen, '!px-0': !isExpandedFormOpen }"
:placeholder="placeholder"
:allow-clear="(!readOnly && !localState && !isPk) || isEditColumn"
:input-read-only="true"

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

@ -184,7 +184,7 @@ const onImageClick = (item: any) => {
height: isForm || isExpandedForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,
}"
class="nc-attachment-cell relative flex color-transition flex items-center w-full xs:(min-h-12 max-h-32)"
:class="{ 'justify-center': !active, 'justify-between': active }"
:class="{ 'justify-center': !active, 'justify-between': active, 'px-2': isExpandedForm }"
>
<LazyCellAttachmentCarousel />

23
packages/nc-gui/components/smartsheet/DivDataCell.vue

@ -4,10 +4,31 @@ import { CurrentCellInj, ref } from '#imports'
const el = ref()
provide(CurrentCellInj, el)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
const onTabPress = () => {
if (!isExpandedFormOpen.value && !isForm.value) return
// Find the focused element
const focusedElement = document.activeElement
if (focusedElement) {
// Check if the focused element is a descendant of the wrapper
const closestWrapper = focusedElement.closest('.nc-data-cell')
// Scroll it into view
if (closestWrapper === el.value) {
el.value?.scrollIntoView({ block: 'center' })
}
}
}
</script>
<template>
<div ref="el" class="select-none nc-data-cell">
<div ref="el" class="select-none nc-data-cell" @keydown.tab="onTabPress">
<slot />
</div>
</template>

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

@ -739,7 +739,7 @@ const onFormItemClick = (element: any) => {
<a-form-item
v-if="isVirtualCol(element)"
:name="element.title"
class="!mb-0 nc-input-required-error"
class="!mb-0 nc-input-required-error nc-form-input-item"
:rules="[
{
required: isRequired(element, element.required),
@ -761,7 +761,7 @@ const onFormItemClick = (element: any) => {
<a-form-item
v-else
:name="element.title"
class="!mb-0 nc-input-required-error"
class="!mb-0 nc-input-required-error nc-form-input-item"
:rules="[
{
required: isRequired(element, element.required),
@ -897,6 +897,11 @@ const onFormItemClick = (element: any) => {
.nc-input {
@apply appearance-none w-full !bg-white rounded px-2 py-2 my-2 border-solid border-1 border-primary border-opacity-50;
&.nc-cell-rating,
&.nc-cell-geodata {
@apply !py-1;
}
:deep(input) {
@apply !px-1;
}
@ -934,4 +939,12 @@ const onFormItemClick = (element: any) => {
}
}
}
.nc-form-input-item .nc-data-cell {
@apply !border-none rounded-none;
&:focus-within {
@apply !border-none;
}
}
</style>

2
packages/nc-gui/components/smartsheet/column/DefaultValue.vue

@ -47,7 +47,7 @@ watch(
<div class="!my-3 text-xs">{{ $t('placeholder.defaultValue') }}</div>
<div class="flex flex-row gap-2">
<div
class="border-1 flex items-center w-full px-3 my-[-4px] border-gray-300 rounded-md"
class="border-1 flex items-center w-full px-3 my-[-4px] border-gray-300 rounded-md sm:min-h-[32px] xs:min-h-13 flex items-center focus-within:(border-brand-500 shadow-none ring-0)"
:class="{
'!border-brand-500': editEnabled,
}"

29
packages/nc-gui/components/smartsheet/expanded-form/Comments.vue

@ -29,8 +29,19 @@ const editLog = ref<AuditType>()
const isEditing = ref<boolean>(false)
const commentInputDomRef = ref<HTMLInputElement>()
const isCommentMode = ref(false)
const focusCommentInput: VNodeRef = (el) => {
if (!isExpandedFormLoading.value && (isCommentMode.value || isExpandedFormCommentMode.value) && !isEditing.value) {
if (isExpandedFormCommentMode.value) {
setTimeout(() => {
isExpandedFormCommentMode.value = false
}, 400)
}
return (el as HTMLInputElement)?.focus()
}
return el
}
const focusInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
function onKeyDown(event: KeyboardEvent) {
@ -55,6 +66,9 @@ function onKeyEsc(event: KeyboardEvent) {
async function onEditComment() {
if (!isEditing.value || !editLog.value) return
isCommentMode.value = true
await updateComment(editLog.value.id!, {
description: editLog.value.description,
})
@ -106,6 +120,7 @@ const isSaving = ref(false)
const saveComment = async () => {
if (isSaving.value) return
isCommentMode.value = true
isSaving.value = true
try {
@ -128,15 +143,6 @@ const onClickAudit = () => {
tab.value = 'audits'
}
watch(commentInputDomRef, () => {
if (commentInputDomRef.value && isExpandedFormCommentMode.value) {
setTimeout(() => {
commentInputDomRef.value?.focus()
isExpandedFormCommentMode.value = false
}, 400)
}
})
</script>
<template>
@ -254,12 +260,13 @@ watch(commentInputDomRef, () => {
<div class="h-14 flex flex-row w-full bg-white py-2.75 px-1.5 items-center rounded-xl border-1 border-gray-200">
<GeneralUserIcon size="base" class="!w-10" :email="user?.email" :name="user?.display_name" />
<a-input
ref="commentInputDomRef"
:ref="focusCommentInput"
v-model:value="comment"
class="!rounded-lg border-1 bg-white !px-2.5 !py-2 !border-gray-200 nc-comment-box !outline-none"
placeholder="Start typing..."
data-testid="expanded-form-comment-input"
:bordered="false"
:disabled="isSaving"
@keyup.enter.prevent="saveComment"
>
</a-input>

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

@ -2,13 +2,25 @@
import { handleTZ } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { CellValueInj, ColumnInj, computed, inject, renderValue, replaceUrlsWithLink, useBase, useGlobal } from '#imports'
import {
CellValueInj,
ColumnInj,
IsExpandedFormOpenInj,
computed,
inject,
renderValue,
replaceUrlsWithLink,
useBase,
useGlobal,
} from '#imports'
// todo: column type doesn't have required property `error` - throws in typecheck
const column = inject(ColumnInj) as Ref<ColumnType & { colOptions: { error: any } }>
const cellValue = inject(CellValueInj)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const { isPg } = useBase()
const { showNull } = useGlobal()
@ -31,10 +43,15 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
</template>
<span>ERR!</span>
</a-tooltip>
<span v-else-if="cellValue === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
<div v-else class="py-2" @dblclick="activateShowEditNonEditableFieldWarning">
<div
v-else
class="py-1"
:class="{
'px-2': isExpandedFormOpen,
}"
@dblclick="activateShowEditNonEditableFieldWarning"
>
<div v-if="urls" v-html="urls" />
<div v-else>{{ result }}</div>

20
packages/nc-gui/components/virtual-cell/Links.vue

@ -3,7 +3,15 @@ import { computed } from '@vue/reactivity'
import type { ColumnType } from 'nocodb-sdk'
import { ref } from 'vue'
import type { Ref } from 'vue'
import { ActiveCellInj, CellValueInj, ColumnInj, IsUnderLookupInj, inject, useSelectedCellKeyupListener } from '#imports'
import {
ActiveCellInj,
CellValueInj,
ColumnInj,
IsExpandedFormOpenInj,
IsUnderLookupInj,
inject,
useSelectedCellKeyupListener,
} from '#imports'
const value = inject(CellValueInj, ref(0))
@ -19,6 +27,8 @@ const readOnly = inject(ReadonlyInj, ref(false))
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const colTitle = computed(() => column.value?.title || '')
const listItemsDlg = ref(false)
@ -120,7 +130,13 @@ watch([listItemsDlg], () => {
</script>
<template>
<div class="flex w-full group items-center nc-links-wrapper" @dblclick.stop="openChildList">
<div
class="flex w-full group items-center nc-links-wrapper py-1"
:class="{
'px-2': isExpandedFormOpen,
}"
@dblclick.stop="openChildList"
>
<div class="block flex-shrink truncate">
<component
:is="isUnderLookup ? 'span' : 'a'"

16
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue

@ -68,6 +68,10 @@ p {
}
.nc-form-view {
.nc-data-cell {
@apply border-solid border-1 !border-gray-300 dark:!border-slate-200;
}
.nc-cell {
@apply bg-white dark:bg-slate-500;
@ -91,7 +95,15 @@ p {
@apply bg-white dark:bg-slate-500;
&.nc-input {
@apply w-full rounded p-2 min-h-[40px] flex items-center border-solid border-1 border-gray-300 dark:border-slate-200;
@apply w-full px-3 min-h-[40px] flex items-center;
&.nc-cell-longtext {
@apply !px-1;
}
&.nc-cell-json {
@apply !h-auto;
}
.duration-cell-wrapper {
@apply w-full;
@ -121,8 +133,6 @@ p {
}
textarea {
@apply px-4 py-2 rounded;
&:focus {
box-shadow: none !important;
}

6
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue

@ -3,7 +3,7 @@ import type { ColumnType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { ref } from 'vue'
import { StreamBarcodeReader } from 'vue-barcode-reader'
import { iconMap, useSharedFormStoreOrThrow } from '#imports'
import { iconMap, useGlobal, useSharedFormStoreOrThrow } from '#imports'
const { sharedFormView, submitForm, v$, formState, notFound, formColumns, submitted, secondsRemain, isLoading } =
useSharedFormStoreOrThrow()
@ -21,6 +21,8 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
return !!(required || (columnObj && columnObj.rqd && !columnObj.cdf))
}
const { isMobileMode } = useGlobal()
const fieldTitleForCurrentScan = ref('')
const scannerIsReady = ref(false)
@ -68,7 +70,7 @@ const onDecode = async (scannedCodeValue: string) => {
</script>
<template>
<div class="h-full flex flex-col items-center">
<div class="h-full flex flex-col items-center" :class="isMobileMode ? 'mobile' : 'desktop'">
<div
class="color-transition flex flex-col justify-center gap-2 w-full max-w-[max(33%,600px)] m-auto py-4 pb-8 px-16 md:(bg-white dark:bg-slate-700 rounded-lg border-1 border-gray-200 shadow-xl)"
>

Loading…
Cancel
Save