Browse Source

Merge pull request #7330 from nocodb/develop

pull/7331/head 0.203.1
github-actions[bot] 9 months ago committed by GitHub
parent
commit
4aed8e790c
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. 12
      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. 31
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  28. 2
      packages/nc-gui/components/smartsheet/header/CellIcon.ts
  29. 25
      packages/nc-gui/components/virtual-cell/Formula.vue
  30. 20
      packages/nc-gui/components/virtual-cell/Links.vue
  31. 7
      packages/nc-gui/composables/useSharedFormViewStore.ts
  32. 394
      packages/nc-gui/lang/ru.json
  33. 2
      packages/nc-gui/package.json
  34. 16
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue
  35. 6
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue
  36. 112
      packages/nocodb-sdk/src/lib/formulaHelpers.ts
  37. 4
      packages/nocodb/package.json
  38. 4
      pnpm-lock.yaml
  39. 8
      tests/playwright/pages/Dashboard/Grid/Column/UserOptionColumn.ts

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>

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

@ -46,6 +46,15 @@ turndownService.addRule('taskList', {
},
})
turndownService.addRule('strikethrough', {
filter: ['s'],
replacement: (content) => {
return `~${content}~`
},
})
turndownService.keep(['u', 'del'])
const checkListItem = {
name: 'checkListItem',
level: 'block',
@ -156,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,
}"

31
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>

2
packages/nc-gui/components/smartsheet/header/CellIcon.ts

@ -84,7 +84,7 @@ const renderIcon = (column: ColumnType, abstractType: any) => {
} else if (isGeometry(column)) {
return iconMap.calculator
} else if (isUser(column)) {
if ((column.meta as { is_multi: boolean; notify: boolean }).is_multi) {
if ((column.meta as { is_multi?: boolean; notify?: boolean })?.is_multi) {
return iconMap.phUsers
}
return iconMap.phUser

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'"

7
packages/nc-gui/composables/useSharedFormViewStore.ts

@ -60,6 +60,9 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const baseStore = useBase()
const { base } = storeToRefs(baseStore)
const basesStore = useBases()
const { basesUser } = storeToRefs(basesStore)
const { t } = useI18n()
const formState = ref<Record<string, any>>({})
@ -127,6 +130,10 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const relatedMetas = { ...viewMeta.relatedMetas }
Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key]))
if (viewMeta.users) {
basesUser.value.set(viewMeta.base_id, viewMeta.users)
}
} catch (e: any) {
if (e.response && e.response.status === 404) {
notFound.value = true

394
packages/nc-gui/lang/ru.json

@ -1,45 +1,45 @@
{
"dashboards": {
"create_new_dashboard_project": "Create New Interface",
"connect_data_sources": "Connect data sources",
"alert": "Alert",
"alert-message": "No databases have been connected. Connect database bases to build interfaces. Skip this step and add databases from the base home page later.",
"select_database_projects_that_you_want_to_link_to_this_dashboard_projects": "Select Database Bases that you want to link to this Interface.",
"create_interface": "Create interface",
"project_name": "Base Name",
"connect": "Connect",
"create_new_dashboard_project": "Создать новый интерфейс",
"connect_data_sources": "Подключить источники данных",
"alert": "Оповещение",
"alert-message": "Базы данных не подключены. Подключите базы данных для создания интерфейсов. Пропустите этот шаг и добавьте базы данных с основной домашней страницы позже.",
"select_database_projects_that_you_want_to_link_to_this_dashboard_projects": "Выберите базы данных, которые вы хотите связать с этим интерфейсом.",
"create_interface": "Создать интерфейс",
"project_name": "Основное имя",
"connect": "Подключиться",
"buttonActionTypes": {
"open_external_url": "Open external link",
"delete_record": "Delete record",
"update_record": "Update record",
"open_layout": "Open layout"
"open_external_url": "Открыть внешнюю ссылку",
"delete_record": "Удалить запись",
"update_record": "Обновить запись",
"open_layout": "Открыть макет"
},
"widgets": {
"static_text": "Text",
"chart": "Chart",
"table": "Table",
"image": "Image",
"map": "Map",
"button": "Button",
"number": "Number",
"bar_chart": "Bar Chart",
"line_chart": "Line Chart",
"area_chart": "Area Chart",
"pie_chart": "Pie Chart",
"donut_chart": "Donut Chart",
"scatter_plot": "Scatter Plot",
"bubble_chart": "Bubble Chart",
"radar_chart": "Radar Chart",
"polar_area_chart": "Polar Area Chart",
"radial_bar_chart": "Radial Bar Chart",
"heatmap_chart": "Heatmap Chart",
"treemap_chart": "Treemap Chart",
"box_plot_chart": "Box Plot Chart",
"candlestick_chart": "Candlestick Chart"
"static_text": "Текст",
"chart": "Диаграмма",
"table": "Таблица",
"image": "Изображение",
"map": "Карта",
"button": "Кнопка",
"number": "Число",
"bar_chart": "Гистограмма",
"line_chart": "Линейная диаграмма",
"area_chart": "Диаграмма области",
"pie_chart": "Круговая диаграмма",
"donut_chart": "Кольцевая диаграмма",
"scatter_plot": "Рассеянный участок",
"bubble_chart": "Пузырьковая диаграмма",
"radar_chart": "Радарная диаграмма",
"polar_area_chart": "Полярная диаграмма",
"radial_bar_chart": "Радиальная гистограмма",
"heatmap_chart": "Тепловая карта",
"treemap_chart": "Древовидная карта",
"box_plot_chart": "Диаграмма ячеек",
"candlestick_chart": "Японские свечи"
}
},
"general": {
"quit": "Quit",
"quit": "Выйти",
"home": "На главную",
"load": "Загрузка",
"open": "Открыть",
@ -47,29 +47,29 @@
"yes": "Да",
"no": "Нет",
"ok": "Ок",
"back": "Back",
"back": "Назад",
"and": "И",
"or": "Или",
"add": "Добавить",
"edit": "Изменить",
"link": "Link",
"links": "Links",
"link": "Ссылка",
"links": "Ссылки",
"remove": "Удалить",
"import": "Import",
"logout": "Log Out",
"empty": "Empty",
"changeIcon": "Change Icon",
"import": "Импорт",
"logout": "Выйти",
"empty": "Пусто",
"changeIcon": "Сменить иконку",
"save": "Сохранить",
"available": "Available",
"abort": "Abort",
"saving": "Saving",
"available": "Доступно",
"abort": "Прервать",
"saving": "Сохранение",
"cancel": "Отмена",
"null": "Null",
"escape": "Escape",
"hex": "Hex",
"clear": "Clear",
"null": "Пустой",
"escape": "Выход",
"hex": "Гекс",
"clear": "Очистить",
"slack": "Slack",
"comment": "Comment",
"comment": "Комментарий",
"microsoftTeams": "Microsoft Teams",
"discord": "Discord",
"matterMost": "Mattermost",
@ -183,17 +183,17 @@
"sumDistinct": "Sum Distinct",
"avgDistinct": "Avg Distinct",
"join": "Join",
"options": "Options",
"primaryValue": "Primary Value",
"useSurveyMode": "Use Survey Mode",
"shift": "Shift",
"enter": "Enter",
"seconds": "Seconds",
"paste": "Paste"
"options": "Опции",
"primaryValue": "Первичное значение",
"useSurveyMode": "Использовать режим анкеты",
"shift": "Сдвиг",
"enter": "Вход",
"seconds": "Секунды",
"paste": "Вставить"
},
"objects": {
"workspace": "Workspace",
"workspaces": "Workspaces",
"workspace": "Рабочее пространство",
"workspaces": "Рабочие пространства",
"project": "Проект",
"projects": "Проекты",
"table": "Таблица",
@ -210,7 +210,7 @@
"webhooks": "Вебхуки (Webhooks)",
"view": "Представление",
"views": "Представления",
"sidebar": "Sidebar",
"sidebar": "Боковая панель",
"viewType": {
"grid": "Сетка",
"gallery": "Галерея",
@ -223,27 +223,27 @@
"users": "Пользователи",
"role": "Роль",
"roles": "Роли",
"developer": "Developer",
"developer": "Разработчик",
"roleType": {
"owner": "Владелец",
"creator": "Создатель",
"editor": "Редактор",
"commenter": "Комментатор",
"viewer": "Наблюдатель",
"noaccess": "No Access",
"superAdmin": "Super Admin",
"noaccess": "Нет доступа",
"superAdmin": "Супер админ",
"orgLevelCreator": "Уровень Создатель",
"orgLevelViewer": "Уровень Наблюдатель"
},
"sqlVIew": "Представление SQL",
"rowHeight": "Record Height",
"rowHeight": "Высота записи",
"heightClass": {
"short": "Short",
"medium": "Medium",
"tall": "Tall",
"extra": "Extra"
"short": "Коротко",
"medium": "Среднее",
"tall": "Высокий",
"extra": "Экстра"
},
"externalDb": "External Database"
"externalDb": "Внешняя база данных"
},
"datatype": {
"ID": "ID",
@ -298,37 +298,37 @@
"isNotNull": "не равно Null"
},
"title": {
"docs": "Docs",
"forum": "Forum",
"parameter": "Parameter",
"headers": "Headers",
"parameterName": "Parameter Name",
"currencyLocale": "Currency Locale",
"currencyCode": "Currency Code",
"searchMembers": "Search Members",
"noMembersFound": "No members found",
"dateJoined": "Date Joined",
"tokenName": "Token name",
"inDesktop": "in Desktop",
"rowData": "Record data",
"creator": "Creator",
"qrCode": "QR Code",
"termsOfService": "Terms of Service",
"updateSelectedRows": "Update Selected Records",
"noFiltersAdded": "No filters added",
"editCards": "Edit Cards",
"noFieldsFound": "No fields found",
"displayValue": "Display Value",
"expand": "Expand",
"hideAll": "Hide all",
"hideSystemFields": "Hide system fields",
"removeFile": "Remove File",
"hasMany": "Has Many",
"manyToMany": "Many to Many",
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"downloadFile": "Download File",
"docs": "Документация",
"forum": "Форум",
"parameter": "Параметр",
"headers": "Заголовки",
"parameterName": "Имя параметра",
"currencyLocale": "Валютная локаль",
"currencyCode": "Код валюты",
"searchMembers": "Поиск участников",
"noMembersFound": "Участников не найдено",
"dateJoined": "Дата вступления",
"tokenName": "Имя токена",
"inDesktop": "на рабочем столе",
"rowData": "Запись данных",
"creator": "Создатель",
"qrCode": "QR Код",
"termsOfService": "Пользовательское Соглашение",
"updateSelectedRows": "Обновить выбранные записи",
"noFiltersAdded": "Фильтры не добавлены",
"editCards": "Изменить карты",
"noFieldsFound": "Поля не найдены",
"displayValue": "Отображаемое значение",
"expand": "Развернуть",
"hideAll": "Скрыть все",
"hideSystemFields": "Скрыть системные поля",
"removeFile": "Удалить файл",
"hasMany": "Имеет много",
"manyToMany": "Многие ко многим",
"virtualRelation": "Виртуальные отношения",
"linkMore": "Ссылка Подробнее",
"linkMoreRecords": "Связать больше записей",
"downloadFile": "Скачать файл",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
"renamingWs": "Renaming Workspace",
@ -421,56 +421,56 @@
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
"taskList": "Список задач",
"bulletList": "Маркированный список",
"numberedList": "Нумерованный список",
"downloadData": "Скачать данные",
"blockQuote": "Цитата",
"noToken": "Нет токена",
"tokenLimit": "Для каждого пользователя разрешен только один токен",
"duplicateAttachment": "Файл с именем {filename} уже прикреплен",
"viewIdColon": "VIEW ID: {viewId}",
"toAddress": "To Address",
"subject": "Subject",
"body": "Body",
"commaSeparatedMobileNumber": "Comma separated Mobile #",
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"toAddress": "Адрес",
"subject": "Тема",
"body": "Тело письма",
"commaSeparatedMobileNumber": "Мобильные # через запятую",
"headerName": "Название заголовка",
"icon": "Иконка",
"max": "Макс",
"enableRichText": "Включить насыщенный текст",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
"duplicateRecord": "Duplicate record",
"binaryEncodingFormat": "Binary encoding format",
"syntax": "Syntax",
"examples": "Examples",
"durationInfo": "A duration of time in minutes or seconds (e.g. 1:23).",
"addHeader": "Add Header",
"enterDefaultUrlOptional": "Enter default URL (Optional)",
"negative": "Negative",
"discard": "Discard",
"default": "Default",
"defaultNumberPercent": "Default Number (%)",
"durationFormat": "Duration Format",
"dateFormat": "Date Format",
"timeFormat": "Time Format",
"singularLabel": "Singular Label",
"pluralLabel": "Plural Label",
"optional": "(Optional)",
"clickToMake": "Click to make",
"visibleForRole": "visible for role:",
"inUI": "in UI Dashboard",
"projectSettings": "Base Settings",
"clickToHide": "Click to hide",
"clickToDownload": "Click to download",
"forRole": "for role",
"clickToCopyViewID": "Click to copy View ID",
"viewMode": "View Mode",
"searchUsers": "Search Users",
"superAdmin": "Super Admin",
"allTables": "All Tables",
"members": "Members",
"copiedRecordURL": "URL скопированной записи",
"copyRecordURL": "Копировать URL записи",
"duplicateRecord": "Дублировать запись",
"binaryEncodingFormat": "Формат двоичного кодирования",
"syntax": "Синтаксис",
"examples": "Примеры",
"durationInfo": "Длительность времени в минутах или секундах (например, 1:23).",
"addHeader": "Добавить заголовок",
"enterDefaultUrlOptional": "Введите URL по умолчанию (необязательно)",
"negative": "Отрицательный",
"discard": "Сбросить",
"default": "По умолчанию",
"defaultNumberPercent": "Номер по умолчанию (%)",
"durationFormat": "Формат продолжительности",
"dateFormat": "Формат даты",
"timeFormat": "Формат времени",
"singularLabel": "Единственное число",
"pluralLabel": "Множественное число",
"optional": "(Необязательно)",
"clickToMake": "Нажмите, чтобы сделать",
"visibleForRole": "видимый для роли:",
"inUI": "в пользовательском интерфейсе",
"projectSettings": "Базовые настройки",
"clickToHide": "Нажмите, чтобы скрыть",
"clickToDownload": "Нажмите, чтобы загрузить",
"forRole": "для роли",
"clickToCopyViewID": "Нажмите, чтобы скопировать ID вида",
"viewMode": "Режим просмотра",
"searchUsers": "Поиск пользователей",
"superAdmin": "Супер админ",
"allTables": "Все таблицы",
"members": "Участники",
"dataSources": "Data Sources",
"connectDataSource": "Connect a Data Source",
"searchProjects": "Search Bases",
@ -632,56 +632,56 @@
"noAccess": "No access",
"restApis": "Rest APIs",
"apis": "APIs",
"includeData": "Include Data",
"includeView": "Include View",
"includeWebhook": "Include Webhook",
"zoomInToViewColumns": "Zoom in to view columns",
"embedInSite": "Embed this view in your site",
"titleRequired": "title is required.",
"sourceNameRequired": "Source name is required",
"changeWsName": "Change Workspace Name",
"pressEnter": "Press Enter",
"newFormLoaded": "New form will be loaded after",
"webhook": "Webhook"
"includeData": "Включить данные",
"includeView": "Включить вид",
"includeWebhook": "Включить вебхук",
"zoomInToViewColumns": "Увеличить масштаб для просмотра столбцов",
"embedInSite": "Вставить этот вид на ваш сайт",
"titleRequired": "название обязательно.",
"sourceNameRequired": "Имя источника обязательно",
"changeWsName": "Изменить название рабочего пространства",
"pressEnter": "Нажмите Enter",
"newFormLoaded": "Новая форма будет загружена после",
"webhook": "Вебхук"
},
"activity": {
"openInANewTab": "Open in a new tab",
"copyIFrameCode": "Copy IFrame code",
"onCondition": "On Condition",
"bulkDownload": "Bulk Download",
"attachFile": "Attach File",
"viewAttachment": "View Attachments",
"attachmentDrop": "Click or drop a file into cell",
"addFiles": "Add File(s)",
"hideInUI": "Hide in UI",
"addBase": "Add Base",
"addParameter": "Add Parameter",
"submitAnotherForm": "Submit Another Form",
"dragAndDropFieldsHereToAdd": "Drag and drop fields here to add",
"editSource": "Edit Data Source",
"enterText": "Enter text",
"okEditBase": "Ok & Edit Base",
"showInUI": "Show in UI",
"outOfSync": "Out of sync",
"newSource": "New Data Source",
"newWebhook": "New Webhook",
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"openInANewTab": "Открыть в новой вкладке",
"copyIFrameCode": "Копировать код IFrame",
"onCondition": "На Условии",
"bulkDownload": "Массовая загрузка",
"attachFile": "Прикрепить файл",
"viewAttachment": "Просмотр вложений",
"attachmentDrop": "Нажмите или перетащите файл в ячейку",
"addFiles": "Добавить файл(ы)",
"hideInUI": "Скрыть в пользовательском интерфейсе",
"addBase": "Добавить базу",
"addParameter": "Добавить параметр",
"submitAnotherForm": "Отправить другую форму",
"dragAndDropFieldsHereToAdd": "Перетащите поля сюда для добавления",
"editSource": "Изменить источник данных",
"enterText": "Введите текст",
"okEditBase": "Ок и редактировать базу",
"showInUI": "Показать в пользовательском интерфейсе",
"outOfSync": "Не синхронизировано",
"newSource": "Новый источник данных",
"newWebhook": "Новый вебхук",
"enablePublicAccess": "Включить публичный доступ",
"doYouWantToSaveTheChanges": "Вы хотите сохранить изменения?",
"editingAccess": "Редактирование доступа",
"enabledPublicViewing": "Enable public viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
"surveyMode": "Survey Mode",
"rtlOrientation": "RTL Orientation",
"useTheme": "Use Theme",
"copyLink": "Copy Link",
"copiedLink": "Link Copied",
"copyInviteLink": "Copy invite link",
"copiedInviteLink": "Copied invite link",
"restrictAccessWithPassword": "Ограничить доступ паролем",
"manageProjectAccess": "Управление доступом к базе",
"allowDownload": "Разрешить загрузку",
"surveyMode": "Режим опроса",
"rtlOrientation": "Ориентация RTL",
"useTheme": "Использовать тему",
"copyLink": "Скопировать ссылку",
"copiedLink": "Ссылка скопирована",
"copyInviteLink": "Скопируйте ссылку на приглашение",
"copiedInviteLink": "Скопированная ссылка на приглашение",
"copyUrl": "Скопировать URL",
"moreColors": "More Colors",
"moveProject": "Move Base",
"moreColors": "Больше цветов",
"moveProject": "Переместить базу",
"createProject": "Создать проект",
"importProject": "Импорт проекта",
"searchProject": "Найти проект",
@ -692,7 +692,7 @@
"deleteProject": "Удалить проект",
"refreshProject": "Обновить проекты",
"saveProject": "Сохранить проект",
"saveAndQuit": "Save & Quit",
"saveAndQuit": "Сохранить и выйти",
"deleteKanbanStack": "Удалить стек?",
"createProjectExtended": {
"extDB": "Создать подключение к внешней базе данных",
@ -708,7 +708,7 @@
"translate": "Помочь перевести",
"account": {
"authToken": "Скопировать токен авторизации",
"authTokenCopied": "Copied Auth Token",
"authTokenCopied": "Токен аутентификации скопирован",
"swagger": "Swagger: REST APIs",
"projInfo": "Информация о проекте",
"themes": "Темы"
@ -718,8 +718,8 @@
"filter": "Фильтр",
"addFilter": "Добавить фильтр",
"share": "Поделиться",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"groupBy": "Группировать по",
"addSubGroup": "Добавить подгруппу",
"shareBase": {
"label": "Share base",
"disable": "Отключить общую базу",

2
packages/nc-gui/package.json

@ -65,7 +65,7 @@
"locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0",
"monaco-sql-languages": "^0.11.0",
"nocodb-sdk": "0.203.0",
"nocodb-sdk": "workspace:^",
"papaparse": "^5.4.1",
"parse-github-url": "^1.0.2",
"pinia": "^2.1.7",

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)"
>

112
packages/nocodb-sdk/src/lib/formulaHelpers.ts

@ -321,7 +321,6 @@ export const formulas: Record<string, FormulaMeta> = {
validation: {
args: {
rqd: 3,
type: FormulaDataTypes.DATE,
},
custom: (_argTypes: FormulaDataTypes[], parsedTree: any) => {
if (parsedTree.arguments[0].type === JSEPNode.LITERAL) {
@ -377,6 +376,7 @@ export const formulas: Record<string, FormulaMeta> = {
validation: {
args: {
rqd: 1,
type: FormulaDataTypes.DATE,
},
},
syntax: 'DATESTR(date | datetime)',
@ -388,6 +388,7 @@ export const formulas: Record<string, FormulaMeta> = {
validation: {
args: {
rqd: 1,
type: FormulaDataTypes.DATE,
},
},
syntax: 'DAY(date | datetime)',
@ -399,6 +400,7 @@ export const formulas: Record<string, FormulaMeta> = {
validation: {
args: {
rqd: 1,
type: FormulaDataTypes.DATE,
},
},
syntax: 'MONTH(date | datetime)',
@ -410,6 +412,7 @@ export const formulas: Record<string, FormulaMeta> = {
validation: {
args: {
rqd: 1,
type: FormulaDataTypes.DATE,
},
},
syntax: 'DAY(time | datetime)',
@ -425,7 +428,6 @@ export const formulas: Record<string, FormulaMeta> = {
args: {
min: 2,
max: 3,
type: FormulaDataTypes.DATE,
},
custom: (_argTypes: FormulaDataTypes[], parsedTree: any) => {
if (parsedTree.arguments[0].type === JSEPNode.LITERAL) {
@ -814,7 +816,6 @@ export const formulas: Record<string, FormulaMeta> = {
validation: {
args: {
rqd: 0,
type: FormulaDataTypes.DATE,
},
},
description: 'Retrieve the current time and day.',
@ -882,8 +883,29 @@ export const formulas: Record<string, FormulaMeta> = {
validation: {
args: {
rqd: 2,
type: FormulaDataTypes.STRING,
},
custom(argTypes: FormulaDataTypes[], parsedTree) {
if (argTypes[0] !== FormulaDataTypes.STRING) {
throw new FormulaError(
FormulaErrorType.INVALID_ARG,
{
key: 'msg.formula.stringTypeIsExpected',
calleeName: parsedTree.callee?.name?.toUpperCase(),
},
'String type is expected'
);
}
if (argTypes[1] !== FormulaDataTypes.NUMERIC) {
throw new FormulaError(
FormulaErrorType.INVALID_ARG,
{
key: 'msg.formula.numericTypeIsExpected',
calleeName: parsedTree.callee?.name?.toUpperCase(),
},
'Numeric type is expected'
);
}
}
},
description: 'Retrieve the last n characters from the input string.',
syntax: 'RIGHT(str, n)',
@ -897,8 +919,29 @@ export const formulas: Record<string, FormulaMeta> = {
validation: {
args: {
rqd: 2,
type: FormulaDataTypes.STRING,
},
custom(argTypes: FormulaDataTypes[], parsedTree) {
if (argTypes[0] !== FormulaDataTypes.STRING) {
throw new FormulaError(
FormulaErrorType.INVALID_ARG,
{
key: 'msg.formula.stringTypeIsExpected',
calleeName: parsedTree.callee?.name?.toUpperCase(),
},
'String type is expected'
);
}
if (argTypes[1] !== FormulaDataTypes.NUMERIC) {
throw new FormulaError(
FormulaErrorType.INVALID_ARG,
{
key: 'msg.formula.numericTypeIsExpected',
calleeName: parsedTree.callee?.name?.toUpperCase(),
},
'Numeric type is expected'
);
}
}
},
description: 'Retrieve the first n characters from the input string.',
syntax: 'LEFT(str, n)',
@ -913,7 +956,38 @@ export const formulas: Record<string, FormulaMeta> = {
args: {
min: 2,
max: 3,
type: FormulaDataTypes.STRING,
},
custom(argTypes: FormulaDataTypes[], parsedTree) {
if (argTypes[0] !== FormulaDataTypes.STRING) {
throw new FormulaError(
FormulaErrorType.INVALID_ARG,
{
key: 'msg.formula.stringTypeIsExpected',
calleeName: parsedTree.callee?.name?.toUpperCase(),
},
'String type is expected'
);
}
if (argTypes[1] !== FormulaDataTypes.NUMERIC) {
throw new FormulaError(
FormulaErrorType.INVALID_ARG,
{
key: 'msg.formula.numericTypeIsExpected',
calleeName: parsedTree.callee?.name?.toUpperCase(),
},
'Numeric type is expected'
);
}
if (argTypes[2] && argTypes[2] !== FormulaDataTypes.NUMERIC) {
throw new FormulaError(
FormulaErrorType.INVALID_ARG,
{
key: 'msg.formula.numericTypeIsExpected',
calleeName: parsedTree.callee?.name?.toUpperCase(),
},
'Numeric type is expected'
);
}
},
},
description:
@ -933,8 +1007,31 @@ export const formulas: Record<string, FormulaMeta> = {
validation: {
args: {
rqd: 3,
type: FormulaDataTypes.STRING,
},
custom(argTypes: FormulaDataTypes[], parsedTree) {
if (argTypes[0] !== FormulaDataTypes.STRING) {
throw new FormulaError(
FormulaErrorType.INVALID_ARG,
{
key: 'msg.formula.stringTypeIsExpected',
calleeName: parsedTree.callee?.name?.toUpperCase(),
},
'String type is expected'
);
}
for(const i of [1,2]) {
if (argTypes[i] !== FormulaDataTypes.NUMERIC) {
throw new FormulaError(
FormulaErrorType.INVALID_ARG,
{
key: 'msg.formula.numericTypeIsExpected',
calleeName: parsedTree.callee?.name?.toUpperCase(),
},
'Numeric type is expected'
);
}
}
}
},
description: 'Extracts a substring; an alias for SUBSTR.',
syntax: 'MID(str, position, [count])',
@ -1052,7 +1149,6 @@ export const formulas: Record<string, FormulaMeta> = {
args: {
min: 1,
max: 2,
type: FormulaDataTypes.NUMERIC,
},
custom(_argTypes: FormulaDataTypes[], parsedTree: any) {
if (parsedTree.arguments[0].type === JSEPNode.LITERAL) {

4
packages/nocodb/package.json

@ -137,7 +137,7 @@
"ncp": "^2.0.0",
"nestjs-kafka": "^1.0.6",
"nestjs-throttler-storage-redis": "^0.3.3",
"nocodb-sdk": "0.203.0",
"nocodb-sdk": "workspace:^",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"object-sizeof": "^2.6.3",
@ -229,4 +229,4 @@
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
}

4
pnpm-lock.yaml

@ -139,7 +139,7 @@ importers:
specifier: ^0.11.0
version: 0.11.0
nocodb-sdk:
specifier: 0.203.0
specifier: workspace:^
version: link:../nocodb-sdk
papaparse:
specifier: ^5.4.1
@ -686,7 +686,7 @@ importers:
specifier: ^0.3.3
version: 0.3.3(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)(@nestjs/throttler@4.2.1)(ioredis@5.3.2)(reflect-metadata@0.1.14)
nocodb-sdk:
specifier: 0.203.0
specifier: workspace:^
version: link:../nocodb-sdk
nodemailer:
specifier: ^6.4.10

8
tests/playwright/pages/Dashboard/Grid/Column/UserOptionColumn.ts

@ -57,14 +57,14 @@ export class UserOptionColumnPageObject extends BasePage {
for (const op of optionsToSelect) {
await this.selectOption({ option: op });
}
// Press `Escape` to close the dropdown
await this.rootPage.keyboard.press('Escape');
await this.rootPage.locator('.nc-dropdown-user-select-cell').waitFor({ state: 'hidden' });
} else if (!Array.isArray(option)) {
await this.selectOption({ option });
}
// Press `Escape` to close the dropdown
await this.rootPage.keyboard.press('Escape');
await this.rootPage.locator('.nc-dropdown-user-select-cell').waitFor({ state: 'hidden' });
await this.column.save({ isUpdated: true });
}

Loading…
Cancel
Save