Browse Source

Nc feat/new date time cell UI (#8546)

* feat(nc-gui): new date picker setup

* feat(nc-gui): new date picker

* fix(nc-gui): date cell form view validation issue

* fix(nc-gui): disable date cell type support in mobile view

* fix(nc-gui): small changes

* feat(nc-gui): new cell year and month picker

* fix(nc-gui): add updated date time picker setup

* feat: update date time cell picker

* fix(nc-gui): add now option in time picker

* fix(nc-gui): small changes

* fix(nc-gui): add support to update month, year from date picker

* fix(nc-gui): update date picker select mont/year flow

* fix(test): date selector test case

* fix(nc-gui): update dateTime cell time picker

* fix(test): update time picker test case

* chore(nc-gui): lint

* fix(nc-gui): invalid date issue

* fix(nc-gui): date time picker tab issue

* fix(nc-gui): year cell test fail issue

* fix(nc-gui): date picker filter test fail issue

* fix(test): survey form test fail issue

* fix(test): update year field fill handler test case

* fix(test): update bulk update test

* fix(nc-gui): datetime multiple api call issue

* fix(test): update timezone related test

* fix(test): timezone related test update

* fix(nc-gui): tab focus issue

* fix(test): filter datetime test udpate

* fix(test): ai review changes

* fix(nc-gui): date picker font weight issue

* fix(nc-gui): update year picker font weight

* fix(nc-gui): show full date from date time cell instead of truncate

* fix(nc-gui): date time picker ui changes

* fix(nc-gui): date time cell width issue

* fix(nc-gui): update datetime time option width according to time format

* fix(nc-gui): disable datetime input if cell is readonly

* fic(nc-gui): add new time picker

* feat(nc-gui): update time picker

* chore(nc-gui): cleanup unwanted code

* fix(test): update time cell test cases

* fix(nc-gui): multiple api calls

* fix(test): update time cell filter & bulk update test cases

* fix(test): revert unrelated changes

* fix(nc-gui): pr review changes

* fix(nc-gui): add clear datetime cell icon in non grid view
pull/8559/head
Ramesh Mane 4 months ago committed by GitHub
parent
commit
56b5264177
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 139
      packages/nc-gui/components/cell/DatePicker.vue
  2. 318
      packages/nc-gui/components/cell/DateTimePicker.vue
  3. 153
      packages/nc-gui/components/cell/TimePicker.vue
  4. 117
      packages/nc-gui/components/cell/YearPicker.vue
  5. 150
      packages/nc-gui/components/nc/DatePicker.vue
  6. 78
      packages/nc-gui/components/nc/DateWeekSelector.vue
  7. 68
      packages/nc-gui/components/nc/MonthYearSelector.vue
  8. 104
      packages/nc-gui/components/nc/TimeSelector.vue
  9. 3
      packages/nc-gui/lang/en.json
  10. 4
      packages/nc-gui/utils/formValidations.ts
  11. 13
      packages/nocodb-sdk/src/lib/dateTimeHelper.ts
  12. 62
      tests/playwright/pages/Dashboard/BulkUpdate/index.ts
  13. 14
      tests/playwright/pages/Dashboard/ExpandedForm/index.ts
  14. 6
      tests/playwright/pages/Dashboard/SurveyForm/index.ts
  15. 101
      tests/playwright/pages/Dashboard/common/Cell/DateTimeCell.ts
  16. 43
      tests/playwright/pages/Dashboard/common/Cell/TimeCell.ts
  17. 2
      tests/playwright/pages/Dashboard/common/Cell/YearCell.ts
  18. 2
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  19. 48
      tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts
  20. 11
      tests/playwright/tests/db/columns/columnDateTime.spec.ts
  21. 2
      tests/playwright/tests/db/views/viewFormShareSurvey.spec.ts

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

@ -43,6 +43,8 @@ const isClearedInputMode = ref<boolean>(false)
const open = ref<boolean>(false)
const tempDate = ref<dayjs.Dayjs | undefined>()
const localState = computed({
get() {
if (!modelValue || isClearedInputMode.value) {
@ -56,7 +58,9 @@ const localState = computed({
const format = picker.value === 'month' ? dateFormat : 'YYYY-MM-DD'
return dayjs(/^\d+$/.test(modelValue) ? +modelValue : modelValue, format)
const value = dayjs(/^\d+$/.test(modelValue) ? +modelValue : modelValue, format)
return value
},
set(val?: dayjs.Dayjs) {
isClearedInputMode.value = false
@ -79,20 +83,36 @@ const localState = computed({
},
})
watchEffect(() => {
if (localState.value) {
tempDate.value = localState.value
}
})
const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
onClickOutside(datePickerRef, (e) => {
if ((e.target as HTMLElement)?.closest(`.${randomClass}`)) return
if ((e.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)) return
datePickerRef.value?.blur?.()
open.value = false
})
const onBlur = (e) => {
if ((e?.relatedTarget as HTMLElement)?.closest(`.${randomClass}`)) return
if (
(e?.relatedTarget as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`) ||
(e?.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)
) {
return
}
open.value = false
}
const onFocus = () => {
open.value = true
}
watch(
open,
(next) => {
@ -165,14 +185,17 @@ const clickHandler = () => {
cellClickHandler()
}
const handleKeydown = (e: KeyboardEvent) => {
if (e.key !== 'Enter') {
const handleKeydown = (e: KeyboardEvent, _open?: boolean) => {
if (e.key !== 'Enter' && e.key !== 'Tab') {
e.stopPropagation()
}
switch (e.key) {
case 'Enter':
open.value = !open.value
e.preventDefault()
localState.value = tempDate.value
open.value = !_open
if (!open.value) {
editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
@ -181,7 +204,7 @@ const handleKeydown = (e: KeyboardEvent) => {
}
return
case 'Escape':
if (open.value) {
if (_open) {
open.value = false
editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
@ -193,9 +216,18 @@ const handleKeydown = (e: KeyboardEvent) => {
datePickerRef.value?.blur?.()
}
return
case 'Tab':
open.value = false
if (isGrid.value) {
editable.value = false
datePickerRef.value?.blur?.()
}
return
default:
if (!open.value && /^[0-9a-z]$/i.test(e.key)) {
if (!_open && /^[0-9a-z]$/i.test(e.key)) {
open.value = true
}
}
@ -232,32 +264,87 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
}
}
})
const handleUpdateValue = (e: Event) => {
const targetValue = (e.target as HTMLInputElement).value
if (!targetValue) {
tempDate.value = undefined
return
}
const value = dayjs(targetValue, dateFormat.value)
if (value.isValid()) {
tempDate.value = value
}
}
function handleSelectDate(value?: dayjs.Dayjs) {
tempDate.value = value
localState.value = value
open.value = false
}
</script>
<template>
<a-date-picker
ref="datePickerRef"
v-model:value="localState"
:disabled="readOnly"
:picker="picker"
:tabindex="0"
:bordered="false"
class="nc-cell-field !w-full !py-1 !border-none !text-current"
<NcDropdown
:visible="isOpen"
:auto-close="false"
:trigger="['click']"
class="nc-cell-field"
:class="[`nc-${randomClass}`, { 'nc-null': modelValue === null && showNull }]"
:format="dateFormat"
:overlay-class-name="`${randomClass} nc-picker-date ${open ? 'active' : ''} !min-w-[260px]`"
>
<div
:title="localState?.format(dateFormat)"
class="nc-date-picker h-full flex items-center justify-between ant-picker-input relative group"
>
<input
ref="datePickerRef"
type="text"
:value="localState?.format(dateFormat) ?? ''"
:placeholder="placeholder"
:allow-clear="!readOnly && !isEditColumn"
:input-read-only="!!isMobileMode"
:dropdown-class-name="`${randomClass} nc-picker-date children:border-1 children:border-gray-200 ${open ? 'active' : ''} `"
:open="isOpen"
class="nc-date-input border-none outline-none !text-current bg-transparent !focus:(border-none outline-none ring-transparent)"
:readonly="readOnly || !!isMobileMode"
@blur="onBlur"
@click="clickHandler"
@keydown="handleKeydown"
@focus="onFocus"
@keydown="handleKeydown($event, open)"
@mouseup.stop
@mousedown.stop
>
<template #suffixIcon></template>
</a-date-picker>
@click="clickHandler"
@input="handleUpdateValue"
/>
<GeneralIcon
v-if="localState"
icon="closeCircle"
class="absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
@click.stop="handleSelectDate()"
/>
</div>
<template #overlay>
<div class="w-[256px]">
<NcDatePicker
v-if="picker === 'month'"
v-model:page-date="tempDate"
v-model:selected-date="localState"
:is-open="isOpen"
type="month"
size="medium"
/>
<NcDatePicker
v-else
v-model:page-date="tempDate"
:is-open="isOpen"
:selected-date="localState"
:is-monday-first="false"
type="date"
size="medium"
@update:selected-date="handleSelectDate"
/>
</div>
</template>
</NcDropdown>
<div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template>

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { dateFormats, isSystemColumn, timeFormats } from 'nocodb-sdk'
import { dateFormats, isSystemColumn, isValidTimeFormat, timeFormats } from 'nocodb-sdk'
interface Props {
modelValue?: string | null
@ -37,18 +37,27 @@ const isDateInvalid = ref(false)
const datePickerRef = ref<HTMLInputElement>()
const timePickerRef = ref<HTMLInputElement>()
const dateTimeFormat = computed(() => {
const dateFormat = parseProp(column?.value?.meta)?.date_format ?? dateFormats[0]
const timeFormat = parseProp(column?.value?.meta)?.time_format ?? timeFormats[0]
return `${dateFormat} ${timeFormat}`
})
const dateFormat = computed(() => parseProp(column?.value?.meta)?.date_format ?? dateFormats[0])
const timeFormat = computed(() => parseProp(column?.value?.meta)?.time_format ?? timeFormats[0])
let localModelValue = modelValue ? dayjs(modelValue).utc().local() : undefined
const isClearedInputMode = ref<boolean>(false)
const open = ref(false)
const tempDate = ref<dayjs.Dayjs | undefined>()
const isDatePicker = ref<boolean>(true)
const localState = computed({
get() {
if (!modelValue || isClearedInputMode.value) {
@ -117,8 +126,18 @@ const localState = computed({
},
})
watchEffect(() => {
if (localState.value) {
tempDate.value = localState.value
}
})
const isColDisabled = computed(() => {
return isSystemColumn(column.value) || readOnly.value || (localState.value && isPk)
})
const isOpen = computed(() => {
if (readOnly.value) return false
if (readOnly.value || isColDisabled.value) return false
return readOnly.value || (localState.value && isPk) ? false : open.value && (active.value || editable.value)
})
@ -126,15 +145,16 @@ const isOpen = computed(() => {
const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
onClickOutside(datePickerRef, (e) => {
if ((e.target as HTMLElement)?.closest(`.${randomClass}`)) return
if ((e.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)) return
datePickerRef.value?.blur?.()
timePickerRef.value?.blur?.()
open.value = false
})
const onBlur = (e) => {
if ((e?.relatedTarget as HTMLElement)?.closest(`.${randomClass}`)) return
open.value = false
const onFocus = (_isDatePicker: boolean) => {
isDatePicker.value = _isDatePicker
open.value = true
}
watch(
@ -142,7 +162,8 @@ watch(
(next) => {
if (next) {
editable.value = true
datePickerRef.value?.focus?.()
isDatePicker.value ? datePickerRef.value?.focus?.() : timePickerRef.value?.focus?.()
onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, (e) => {
if ((e?.target as HTMLElement)?.closest(`.nc-${randomClass}`)) {
@ -162,7 +183,7 @@ const placeholder = computed(() => {
((isForm.value || isExpandedForm.value) && !isDateInvalid.value) ||
(isGrid.value && !showNull.value && !isDateInvalid.value && !isSystemColumn(column.value) && active.value)
) {
return dateTimeFormat.value
return { dateTime: dateTimeFormat.value, date: dateFormat.value, time: timeFormat.value }
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
@ -180,25 +201,6 @@ const cellClickHandler = () => {
open.value = active.value || editable.value
}
function okHandler(val: dayjs.Dayjs | string) {
isClearedInputMode.value = false
if (!val) {
emit('update:modelValue', null)
} else if (dayjs(val).isValid()) {
// setting localModelValue to cater NOW function in date picker
localModelValue = dayjs(val)
// send the payload in UTC format
emit('update:modelValue', dayjs(val).utc().format('YYYY-MM-DD HH:mm:ssZ'))
}
open.value = !open.value
if (!open.value && isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
datePickerRef.value?.blur?.()
editable.value = false
}
}
onMounted(() => {
cellClickHook?.on(cellClickHandler)
})
@ -206,13 +208,8 @@ onUnmounted(() => {
cellClickHook?.on(cellClickHandler)
})
const clickHandler = (e) => {
if ((e.target as HTMLElement).closest(`.nc-${randomClass} .ant-picker-clear`)) {
e.stopPropagation()
emit('update:modelValue', null)
open.value = false
return
}
const clickHandler = (e: MouseEvent, _isDatePicker: boolean = false) => {
isDatePicker.value = _isDatePicker
if (cellClickHook) {
return
@ -220,46 +217,59 @@ const clickHandler = (e) => {
cellClickHandler()
}
const isColDisabled = computed(() => {
return isSystemColumn(column.value) || readOnly.value || (localState.value && isPk)
})
const handleKeydown = (e: KeyboardEvent) => {
const handleKeydown = (e: KeyboardEvent, _open?: boolean, _isDatePicker: boolean = false) => {
if (e.key !== 'Enter') {
e.stopPropagation()
}
switch (e.key) {
case 'Enter':
if (isOpen.value) {
return okHandler((e.target as HTMLInputElement).value)
e.preventDefault()
localState.value = tempDate.value
if (!_isDatePicker) {
e.stopPropagation()
timePickerRef.value?.blur?.()
isDatePicker.value = false
datePickerRef.value?.focus?.()
cellClickHandler()
} else {
open.value = true
datePickerRef.value?.blur?.()
open.value = false
editable.value = false
}
return
case 'Escape':
if (open.value) {
if (_open) {
open.value = false
editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
datePickerRef.value?.blur?.()
_isDatePicker ? datePickerRef.value?.blur?.() : timePickerRef.value?.blur?.()
}
} else {
editable.value = false
datePickerRef.value?.blur?.()
_isDatePicker ? datePickerRef.value?.blur?.() : timePickerRef.value?.blur?.()
}
return
case 'Tab':
open.value = false
if (isGrid.value) {
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
_isDatePicker ? datePickerRef.value?.blur?.() : timePickerRef.value?.blur?.()
if (e.shiftKey && _isDatePicker) {
editable.value = false
} else if (!e.shiftKey && !_isDatePicker) {
editable.value = false
datePickerRef.value?.blur()
} else {
e.stopPropagation()
}
}
return
default:
if (!open.value && /^[0-9a-z]$/i.test(e.key)) {
if (!_open && /^[0-9a-z]$/i.test(e.key)) {
open.value = true
}
}
@ -288,9 +298,9 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
e.preventDefault()
break
default:
if (!isOpen.value && datePickerRef.value && /^[0-9a-z]$/i.test(e.key)) {
if (!isOpen.value && (datePickerRef.value || timePickerRef.value) && /^[0-9a-z]$/i.test(e.key)) {
isClearedInputMode.value = true
datePickerRef.value.focus()
isDatePicker.value ? datePickerRef.value?.focus() : timePickerRef.value?.focus()
editable.value = true
open.value = true
}
@ -299,35 +309,203 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
watch(editable, (nextValue) => {
if (isGrid.value && nextValue && !open.value) {
isDatePicker.value = true
open.value = true
}
})
const handleUpdateValue = (e: Event, _isDatePicker: boolean) => {
let targetValue = (e.target as HTMLInputElement).value
if (_isDatePicker) {
if (!targetValue) {
tempDate.value = undefined
return
}
const date = dayjs(targetValue, dateFormat.value)
if (date.isValid()) {
if (localState.value) {
tempDate.value = dayjs(`${date.format('YYYY-MM-DD')} ${localState.value.format(timeFormat.value)}`)
} else {
tempDate.value = date
}
}
}
if (!_isDatePicker) {
if (!targetValue) {
tempDate.value = dayjs(dayjs().format('YYYY-MM-DD'))
return
}
if (timeFormat.value === 'HH:mm' && targetValue.length > 5) {
targetValue = targetValue.slice(0, 5)
}
if (isValidTimeFormat(targetValue, timeFormat.value)) {
tempDate.value = dayjs(`${(tempDate.value ?? dayjs()).format('YYYY-MM-DD')} ${targetValue}`)
}
}
}
function handleSelectDate(value?: dayjs.Dayjs) {
if (value && localState.value) {
const dateTime = dayjs(`${value.format('YYYY-MM-DD')} ${localState.value.format(timeFormat.value)}`)
tempDate.value = dateTime
localState.value = dateTime
} else {
tempDate.value = value
localState.value = value
}
open.value = false
}
function handleSelectTime(value: dayjs.Dayjs) {
if (!value.isValid()) return
if (localState.value) {
const dateTime = dayjs(`${localState.value.format('YYYY-MM-DD')} ${value.format('HH:mm')}:00`)
tempDate.value = dateTime
localState.value = dateTime
} else {
const dateTime = dayjs(`${dayjs().format('YYYY-MM-DD')} ${value.format('HH:mm')}:00`)
tempDate.value = dateTime
localState.value = dateTime
}
open.value = false
}
const selectedTime = computed(() => {
const result = {
value: '',
label: '',
}
if (localState.value) {
const time = localState.value.format(timeFormat.value)
const [hours, minutes] = time.split(':')
result.value = `${hours}:${minutes}`
result.label = time
}
return result
})
const timeCellMaxWidth = computed(() => {
return {
[timeFormats[0]]: 'max-w-[65px]',
[timeFormats[1]]: 'max-w-[80px]',
[timeFormats[2]]: 'max-w-[110px]',
}[timeFormat.value]
})
</script>
<template>
<a-date-picker
ref="datePickerRef"
:value="localState"
:disabled="isColDisabled"
:show-time="true"
:bordered="false"
class="nc-cell-field nc-cell-picker-datetime !w-full !py-1 !border-none !text-current"
<div class="nc-cell-field group relative">
<NcDropdown
:visible="isOpen"
:placement="isDatePicker ? 'bottomLeft' : 'bottomRight'"
:auto-close="false"
:trigger="['click']"
class="nc-cell-picker-datetime"
:class="[`nc-${randomClass}`, { 'nc-null': modelValue === null && showNull }]"
:format="dateTimeFormat"
:placeholder="placeholder"
:allow-clear="!isColDisabled && !isEditColumn"
:input-read-only="!!isMobileMode"
:dropdown-class-name="`${randomClass} nc-picker-datetime children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
:open="isOpen"
@blur="onBlur"
@click="clickHandler"
@ok="okHandler"
@keydown="handleKeydown"
:overlay-class-name="`${randomClass} nc-picker-datetime ${open ? 'active' : ''} !min-w-[0] overflow-hidden`"
>
<div
:title="localState?.format(dateTimeFormat)"
class="nc-date-picker ant-picker-input flex justify-between gap-2 relative group !w-auto"
>
<div
class="flex-none hover:bg-gray-100 px-1 rounded-md box-border w-[60%] max-w-[110px]"
:class="{
'py-0': isForm,
'py-0.5': !isForm,
'bg-gray-100': isDatePicker && isOpen,
}"
>
<input
ref="datePickerRef"
:value="localState?.format(dateFormat) ?? ''"
:placeholder="typeof placeholder === 'string' ? placeholder : placeholder?.date"
class="nc-date-input w-full !truncate border-transparent outline-none !text-current !bg-transparent !focus:(border-none outline-none ring-transparent)"
:readonly="!!isMobileMode || isColDisabled"
@focus="onFocus(true)"
@keydown="handleKeydown($event, isOpen, true)"
@mouseup.stop
@mousedown.stop
@click.stop="clickHandler($event, true)"
@input="handleUpdateValue($event, true)"
/>
</div>
<div
class="flex-none hover:bg-gray-100 px-1 rounded-md box-border flex-1"
:class="[
`${timeCellMaxWidth}`,
{
'py-0': isForm,
'py-0.5': !isForm,
'bg-gray-100': !isDatePicker && isOpen,
},
]"
>
<input
ref="timePickerRef"
:value="selectedTime.value ? `${selectedTime.label}` : ''"
:placeholder="typeof placeholder === 'string' ? placeholder : placeholder?.time"
class="nc-time-input w-full !truncate border-transparent outline-none !text-current !bg-transparent !focus:(border-none outline-none ring-transparent)"
:readonly="!!isMobileMode || isColDisabled"
@focus="onFocus(false)"
@keydown="handleKeydown($event, open)"
@mouseup.stop
@mousedown.stop
@click.stop="clickHandler($event, false)"
@input="handleUpdateValue($event, false)"
/>
</div>
</div>
<template #overlay>
<div
class="min-w-[72px]"
:class="{
'w-[256px]': isDatePicker,
}"
>
<template #suffixIcon></template>
</a-date-picker>
<NcDatePicker
v-if="isDatePicker"
v-model:page-date="tempDate"
:selected-date="localState"
:is-open="isOpen"
type="date"
size="medium"
@update:selected-date="handleSelectDate"
/>
<template v-else>
<NcTimeSelector
:selected-date="localState"
:min-granularity="30"
is-min-granularity-picker
:is-open="isOpen"
@update:selected-date="handleSelectTime"
/>
</template>
</div>
</template>
</NcDropdown>
<GeneralIcon
v-if="localState && (isExpandedForm || isForm || !isGrid)"
icon="closeCircle"
class="h-4 w-4 absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
@click.stop="handleSelectDate()"
/>
</div>
<div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template>

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { isSystemColumn } from 'nocodb-sdk'
import { isSystemColumn, isValidTimeFormat } from 'nocodb-sdk'
interface Props {
modelValue?: string | null | undefined
@ -43,6 +43,8 @@ const { t } = useI18n()
const open = ref(false)
const tempDate = ref<dayjs.Dayjs | undefined>()
const localState = computed({
get() {
if (!modelValue || isClearedInputMode.value) {
@ -78,20 +80,35 @@ const localState = computed({
},
})
watchEffect(() => {
if (localState.value) {
tempDate.value = localState.value
}
})
const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
onClickOutside(datePickerRef, (e) => {
if ((e.target as HTMLElement)?.closest(`.${randomClass}`)) return
if ((e.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)) return
datePickerRef.value?.blur?.()
open.value = false
})
const onBlur = (e) => {
if ((e?.relatedTarget as HTMLElement)?.closest(`.${randomClass}`)) return
if (
(e?.relatedTarget as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`) ||
(e?.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)
) {
return
}
open.value = false
}
const onFocus = () => {
open.value = true
}
watch(
open,
(next) => {
@ -146,26 +163,38 @@ const clickHandler = () => {
open.value = active.value || editable.value
}
const handleKeydown = (e: KeyboardEvent) => {
const handleKeydown = (e: KeyboardEvent, _open?: boolean) => {
if (e.key !== 'Enter') {
e.stopPropagation()
}
switch (e.key) {
case 'Enter':
open.value = !open.value
e.preventDefault()
localState.value = tempDate.value
open.value = !_open
if (!open.value) {
editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
editable.value = false
datePickerRef.value?.blur?.()
}
}
return
case 'Escape':
if (open.value) {
case 'Tab':
open.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
editable.value = false
datePickerRef.value?.blur?.()
}
return
case 'Escape':
if (_open) {
open.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
editable.value = false
datePickerRef.value?.blur?.()
}
} else {
@ -175,7 +204,7 @@ const handleKeydown = (e: KeyboardEvent) => {
}
return
default:
if (!open.value && /^[0-9a-z]$/i.test(e.key)) {
if (!_open && /^[0-9a-z]$/i.test(e.key)) {
open.value = true
}
}
@ -212,39 +241,93 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
}
}
})
const handleUpdateValue = (e: Event) => {
let targetValue = (e.target as HTMLInputElement).value
if (!targetValue) {
tempDate.value = undefined
return
}
if (targetValue.length > 5) {
targetValue = targetValue.slice(0, 5)
}
if (isValidTimeFormat(targetValue, 'HH:mm')) {
tempDate.value = dayjs(`${dayjs().format('YYYY-MM-DD')} ${targetValue}`)
}
}
function handleSelectTime(value?: dayjs.Dayjs) {
if (!value) {
tempDate.value = undefined
localState.value = undefined
}
if (!value?.isValid()) return
if (localState.value) {
const dateTime = dayjs(`${localState.value.format('YYYY-MM-DD')} ${value.format('HH:mm')}:00`)
tempDate.value = dateTime
localState.value = dateTime
} else {
const dateTime = dayjs(`${dayjs().format('YYYY-MM-DD')} ${value.format('HH:mm')}:00`)
tempDate.value = dateTime
localState.value = dateTime
}
open.value = false
}
</script>
<template>
<a-time-picker
ref="datePickerRef"
v-model:value="localState"
:tabindex="0"
:disabled="readOnly"
:show-time="true"
:bordered="false"
use12-hours
format="HH:mm"
class="nc-cell-field !w-full !py-1 !border-none !text-current"
<NcDropdown
:visible="isOpen"
:auto-close="false"
:trigger="['click']"
class="nc-cell-field"
:class="[`nc-${randomClass}`, { 'nc-null': modelValue === null && showNull }]"
:overlay-class-name="`${randomClass} nc-picker-time ${isOpen ? 'active' : ''} !min-w-[0]`"
>
<div
:title="localState?.format('HH:mm')"
class="nc-time-picker h-full flex items-center justify-between ant-picker-input relative group"
>
<input
ref="datePickerRef"
type="text"
:value="localState?.format('HH:mm') ?? ''"
:placeholder="placeholder"
:allow-clear="!readOnly && !isPk && !isEditColumn"
:input-read-only="!!isMobileMode"
:open="isOpen"
:popup-class-name="`${randomClass} nc-picker-time children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
class="nc-time-input border-none outline-none !text-current bg-transparent !focus:(border-none outline-none ring-transparent)"
:readonly="readOnly || !!isMobileMode"
@blur="onBlur"
@keydown="handleKeydown"
@click="clickHandler"
@ok="open = !open"
@focus="onFocus"
@keydown="handleKeydown($event, isOpen)"
@mouseup.stop
@mousedown.stop
>
<template #suffixIcon></template>
</a-time-picker>
@click="clickHandler"
@input="handleUpdateValue"
/>
<GeneralIcon
v-if="localState"
icon="closeCircle"
class="absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
@click.stop="handleSelectTime()"
/>
</div>
<template #overlay>
<div class="w-[72px]">
<NcTimeSelector
:selected-date="localState"
:min-granularity="30"
is-min-granularity-picker
:is-open="isOpen"
@update:selected-date="handleSelectTime"
/>
</div>
</template>
</NcDropdown>
<div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template>
<style scoped>
:deep(.ant-picker-input > input) {
@apply !text-current;
}
</style>

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

@ -39,6 +39,8 @@ const { t } = useI18n()
const open = ref<boolean>(false)
const tempDate = ref<dayjs.Dayjs | undefined>()
const localState = computed({
get() {
if (!modelValue || isClearedInputMode.value) {
@ -69,16 +71,27 @@ const localState = computed({
},
})
watchEffect(() => {
if (localState.value) {
tempDate.value = localState.value
}
})
const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
onClickOutside(datePickerRef, (e) => {
if ((e.target as HTMLElement)?.closest(`.${randomClass}`)) return
if ((e.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)) return
datePickerRef.value?.blur?.()
open.value = false
})
const onBlur = (e) => {
if ((e?.relatedTarget as HTMLElement)?.closest(`.${randomClass}`)) return
if (
(e?.relatedTarget as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`) ||
(e?.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)
) {
return
}
open.value = false
}
@ -136,14 +149,16 @@ const clickHandler = () => {
open.value = active.value || editable.value
}
const handleKeydown = (e: KeyboardEvent) => {
if (e.key !== 'Enter') {
const handleKeydown = (e: KeyboardEvent, _open?: boolean) => {
if (e.key !== 'Enter' && e.key !== 'Tab') {
e.stopPropagation()
}
switch (e.key) {
case 'Enter':
open.value = !open.value
e.preventDefault()
localState.value = tempDate.value
open.value = !_open
if (!open.value) {
editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
@ -151,9 +166,19 @@ const handleKeydown = (e: KeyboardEvent) => {
}
}
return
case 'Tab':
open.value = false
if (isGrid.value) {
editable.value = false
datePickerRef.value?.blur?.()
}
return
case 'Escape':
if (open.value) {
if (_open) {
open.value = false
editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
@ -166,7 +191,7 @@ const handleKeydown = (e: KeyboardEvent) => {
}
return
default:
if (!open.value && /^[0-9a-z]$/i.test(e.key)) {
if (!_open && /^[0-9a-z]$/i.test(e.key)) {
open.value = true
}
}
@ -203,31 +228,77 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
}
}
})
const handleUpdateValue = (e: Event) => {
const targetValue = (e.target as HTMLInputElement).value
if (!targetValue) {
tempDate.value = undefined
return
}
const value = dayjs(targetValue, 'YYYY')
if (value.isValid()) {
tempDate.value = value
}
}
function handleSelectDate(value?: dayjs.Dayjs) {
tempDate.value = value
localState.value = value
open.value = false
}
</script>
<template>
<a-date-picker
ref="datePickerRef"
v-model:value="localState"
<NcDropdown
:visible="isOpen"
:auto-close="false"
:trigger="['click']"
:disabled="readOnly"
:tabindex="0"
picker="year"
:bordered="false"
class="nc-cell-field !w-full !py-1 !border-none !text-current"
class="nc-cell-field"
:class="[`nc-${randomClass}`, { 'nc-null': modelValue === null && showNull }]"
:overlay-class-name="`${randomClass} nc-picker-year ${open ? 'active' : ''} !min-w-[260px]`"
>
<div
:title="localState?.format('YYYY')"
class="nc-year-picker flex items-center justify-between ant-picker-input relative group"
>
<input
ref="datePickerRef"
type="text"
:value="localState?.format('YYYY') ?? ''"
:placeholder="placeholder"
:allow-clear="!readOnly && !isPk"
:input-read-only="!!isMobileMode"
:open="isOpen"
:dropdown-class-name="`${randomClass} nc-picker-year children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
class="nc-year-input border-none outline-none !text-current bg-transparent !focus:(border-none outline-none ring-transparent)"
:readonly="readOnly || !!isMobileMode"
@blur="onBlur"
@keydown="handleKeydown"
@click="clickHandler"
@keydown="handleKeydown($event, open)"
@mouseup.stop
@mousedown.stop
>
<template #suffixIcon></template>
</a-date-picker>
@click="clickHandler"
@input="handleUpdateValue"
/>
<GeneralIcon
v-if="localState"
icon="closeCircle"
class="absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
@click.stop="handleSelectDate()"
/>
</div>
<template #overlay>
<div class="w-[256px]">
<NcMonthYearSelector
v-model:page-date="tempDate"
v-model:selected-date="localState"
:is-open="isOpen"
is-year-picker
is-cell-input-field
size="medium"
/>
</div>
</template>
</NcDropdown>
<div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template>

150
packages/nc-gui/components/nc/DatePicker.vue

@ -0,0 +1,150 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
interface Props {
size?: 'medium'
selectedDate?: dayjs.Dayjs | null
pageDate?: dayjs.Dayjs
isCellInputField?: boolean
type: 'date' | 'time' | 'year' | 'month'
isOpen: boolean
}
const props = withDefaults(defineProps<Props>(), {
size: 'medium',
selectedDate: null,
pageDate: () => dayjs(),
isCellInputField: false,
type: 'date',
isOpen: false,
})
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:selectedWeek'])
// Page date is the date we use to manage which month/date that is currently being displayed
const pageDate = useVModel(props, 'pageDate', emit)
const selectedDate = useVModel(props, 'selectedDate', emit)
const { type, isOpen } = toRefs(props)
const localPageDate = ref()
const localSelectedDate = ref()
const pickerType = ref<Props['type'] | undefined>()
const pickerStack = ref<Props['type'][]>([])
const tempPickerType = computed(() => pickerType.value || type.value)
const handleUpdatePickerType = (value?: Props['type']) => {
if (value) {
pickerType.value = value
pickerStack.value.push(value)
} else {
if (pickerStack.value.length > 1) {
pickerStack.value.pop()
const lastPicker = pickerStack.value.pop()
pickerType.value = lastPicker
} else {
pickerStack.value = []
pickerType.value = type.value
}
}
}
const localStatePageDate = computed({
get: () => {
if (localPageDate.value) {
return localPageDate.value
}
return pageDate.value
},
set: (value) => {
pageDate.value = value
localPageDate.value = value
emit('update:pageDate', value)
},
})
const localStateSelectedDate = computed({
get: () => {
if (localSelectedDate.value) {
return localSelectedDate.value
}
return pageDate.value
},
set: (value: dayjs.Dayjs) => {
if (!value.isValid()) return
if (pickerType.value === type.value) {
localPageDate.value = value
emit('update:selectedDate', value)
localSelectedDate.value = undefined
return
}
if (['date', 'month'].includes(type.value)) {
if (pickerType.value === 'year') {
localSelectedDate.value = dayjs(localPageDate.value ?? localSelectedDate.value ?? selectedDate.value ?? dayjs()).year(
+value.format('YYYY'),
)
}
if (type.value !== 'month' && pickerType.value === 'month') {
localSelectedDate.value = dayjs(localPageDate.value ?? localSelectedDate.value ?? selectedDate.value ?? dayjs()).month(
+value.format('MM') - 1,
)
}
localPageDate.value = localSelectedDate.value
handleUpdatePickerType()
}
},
})
watch(isOpen, (next) => {
if (!next) {
pickerType.value = type.value
localPageDate.value = undefined
localSelectedDate.value = undefined
pickerStack.value = []
}
})
onUnmounted(() => {
pickerType.value = type.value
localPageDate.value = undefined
localSelectedDate.value = undefined
pickerStack.value = []
})
onMounted(() => {
localPageDate.value = undefined
localSelectedDate.value = undefined
pickerStack.value = []
})
</script>
<template>
<NcDateWeekSelector
v-if="tempPickerType === 'date'"
v-model:page-date="localStatePageDate"
v-model:selected-date="localStateSelectedDate"
:picker-type="pickerType"
:is-monday-first="false"
is-cell-input-field
size="medium"
@update:picker-type="handleUpdatePickerType"
/>
<NcMonthYearSelector
v-if="['month', 'year'].includes(tempPickerType)"
v-model:page-date="localStatePageDate"
v-model:selected-date="localStateSelectedDate"
:picker-type="pickerType"
:is-year-picker="tempPickerType === 'year'"
is-cell-input-field
size="medium"
@update:picker-type="handleUpdatePickerType"
/>
</template>
<style lang="scss" scoped></style>

78
packages/nc-gui/components/nc/DateWeekSelector.vue

@ -13,19 +13,23 @@ interface Props {
start: dayjs.Dayjs
end: dayjs.Dayjs
} | null
isCellInputField?: boolean
pickerType?: 'date' | 'time' | 'year' | 'month'
}
const props = withDefaults(defineProps<Props>(), {
size: 'medium',
selectedDate: null,
isMondayFirst: true,
pageDate: dayjs(),
pageDate: () => dayjs(),
isWeekPicker: false,
activeDates: [] as Array<dayjs.Dayjs>,
activeDates: () => [] as Array<dayjs.Dayjs>,
selectedWeek: null,
hideCalendar: false,
isCellInputField: false,
pickerType: 'date',
})
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:selectedWeek'])
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:selectedWeek', 'update:pickerType'])
// Page date is the date we use to manage which month/date that is currently being displayed
const pageDate = useVModel(props, 'pageDate', emit)
@ -35,6 +39,8 @@ const activeDates = useVModel(props, 'activeDates', emit)
const selectedWeek = useVModel(props, 'selectedWeek', emit)
const pickerType = useVModel(props, 'pickerType', emit)
const days = computed(() => {
if (props.isMondayFirst) {
return ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
@ -47,6 +53,14 @@ const currentMonthYear = computed(() => {
return dayjs(pageDate.value).format('MMMM YYYY')
})
const currentMonth = computed(() => {
return dayjs(pageDate.value).format('MMMM')
})
const currentYear = computed(() => {
return dayjs(pageDate.value).format('YYYY')
})
const selectWeek = (date: dayjs.Dayjs) => {
const dayOffset = +props.isMondayFirst
const dayOfWeek = (date.day() - dayOffset + 7) % 7
@ -102,6 +116,9 @@ const isDayInPagedMonth = (date: dayjs.Dayjs) => {
const handleSelectDate = (date: dayjs.Dayjs) => {
if (props.isWeekPicker) {
selectWeek(date)
} else if (props.isCellInputField) {
selectedDate.value = date
emit('update:selectedDate', date)
} else {
if (!isDayInPagedMonth(date)) {
pageDate.value = date
@ -137,17 +154,30 @@ const paginate = (action: 'next' | 'prev') => {
<template>
<div class="flex flex-col">
<div class="flex justify-between border-b-1 px-3 py-0.5 nc-date-week-header items-center">
<div
class="flex justify-between border-b-1 nc-date-week-header items-center box-border"
:class="{
'px-2 py-1 h-10': isCellInputField,
'px-3 py-0.5': !isCellInputField,
}"
>
<NcTooltip hide-on-click>
<NcButton class="!border-0" size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.arrowLeft" class="h-4 w-4" />
</NcButton>
<template #title>
<span>{{ $t('labels.next') }}</span>
<span>{{ $t('labels.previous') }}</span>
</template>
</NcTooltip>
<span class="text-gray-700 text-sm font-semibold">{{ currentMonthYear }}</span>
<div v-if="isCellInputField" class="text-gray-700 text-sm font-semibold">
<span class="nc-month-picker-btn cursor-pointer hover:text-brand-500" @click="pickerType = 'month'">{{
currentMonth
}}</span>
{{ ' ' }}
<span class="nc-year-picker-btn cursor-pointer hover:text-brand-500" @click="pickerType = 'year'">{{ currentYear }}</span>
</div>
<span v-else class="text-gray-700 text-sm font-semibold">{{ currentMonthYear }}</span>
<NcTooltip hide-on-click>
<NcButton class="!border-0" data-testid="nc-calendar-next-btn" size="small" type="secondary" @click="paginate('next')">
@ -159,7 +189,13 @@ const paginate = (action: 'next' | 'prev') => {
</NcTooltip>
</div>
<div v-if="!hideCalendar" class="max-w-[320px] rounded-y-xl">
<div class="flex py-1 gap-1 px-2.5 rounded-t-xl flex-row border-gray-200 justify-between">
<div class="py-1 px-2.5 h-10">
<div
class="flex gap-1"
:class="{
'border-b-1 border-gray-200 ': isCellInputField,
}"
>
<span
v-for="(day, index) in days"
:key="index"
@ -167,13 +203,22 @@ const paginate = (action: 'next' | 'prev') => {
>{{ day[0] }}</span
>
</div>
<div class="grid gap-1 py-1 px-2.5 nc-date-week-grid-wrapper grid-cols-7">
</div>
<div
class="grid gap-1 py-1 nc-date-week-grid-wrapper grid-cols-7"
:class="{
'px-2': isCellInputField,
'px-2.5': !isCellInputField,
}"
>
<span
v-for="(date, index) in dates"
:key="index"
:class="{
'rounded-lg': !isWeekPicker,
'bg-gray-200 border-1 font-bold ': isSelectedDate(date) && !isWeekPicker && isDayInPagedMonth(date),
'rounded-lg': !isWeekPicker && !isCellInputField,
'border-1 ': isSelectedDate(date) && !isWeekPicker && isDayInPagedMonth(date),
'bg-gray-200 !font-bold': isSelectedDate(date) && !isWeekPicker && isDayInPagedMonth(date) && !isCellInputField,
'bg-gray-300 !font-weight-600': isSelectedDate(date) && !isWeekPicker && isDayInPagedMonth(date) && isCellInputField,
'hover:(border-1 border-gray-200 bg-gray-100)': !isSelectedDate(date) && !isWeekPicker,
'nc-selected-week !font-semibold z-1': isDateInSelectedWeek(date) && isWeekPicker,
'border-none': isWeekPicker,
@ -183,9 +228,13 @@ const paginate = (action: 'next' | 'prev') => {
'nc-selected-week-end': isSameDate(date, selectedWeek?.end),
'rounded-md text-brand-500 !font-semibold nc-calendar-today': isSameDate(date, dayjs()) && isDateInCurrentMonth(date),
'text-gray-500': date.get('day') === 0 || date.get('day') === 6,
'nc-date-item font-weight-400': isCellInputField,
'font-medium': !isCellInputField,
'rounded': !isWeekPicker && isCellInputField,
}"
class="px-1 h-8 w-8 py-1 relative transition border-1 font-medium flex text-gray-700 items-center cursor-pointer justify-center"
class="px-1 h-8 w-8 py-1 relative transition border-1 flex text-gray-700 items-center cursor-pointer justify-center"
data-testid="nc-calendar-date"
:title="isCellInputField ? date.format('YYYY-MM-DD') : undefined"
@click="handleSelectDate(date)"
>
<span
@ -196,11 +245,16 @@ const paginate = (action: 'next' | 'prev') => {
}"
class="absolute top-1 transition right-1 h-1.5 w-1.5 z-2 border-1 rounded-full border-white bg-brand-500"
></span>
<span class="z-2">
<span class="nc-date-item-inner z-2">
{{ date.get('date') }}
</span>
</span>
</div>
<div v-if="isCellInputField" class="flex items-center justify-center px-2 pb-2 pt-1">
<NcButton class="nc-date-picker-now-btn !h-7" size="small" type="secondary" @click="handleSelectDate(dayjs())">
<span class="text-small"> {{ $t('labels.today') }} </span>
</NcButton>
</div>
</div>
</div>
</template>

68
packages/nc-gui/components/nc/MonthYearSelector.vue

@ -6,20 +6,26 @@ interface Props {
pageDate?: dayjs.Dayjs
isYearPicker?: boolean
hideCalendar?: boolean
isCellInputField?: boolean
pickerType?: 'date' | 'time' | 'year' | 'month'
}
const props = withDefaults(defineProps<Props>(), {
selectedDate: null,
pageDate: dayjs(),
pageDate: () => dayjs(),
isYearPicker: false,
hideCalendar: false,
isCellInputField: false,
pickerType: 'date',
})
const emit = defineEmits(['update:selectedDate', 'update:pageDate'])
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:pickerType'])
const pageDate = useVModel(props, 'pageDate', emit)
const selectedDate = useVModel(props, 'selectedDate', emit)
const pickerType = useVModel(props, 'pickerType', emit)
const years = computed(() => {
const date = pageDate.value
const startOfYear = date.startOf('year')
@ -86,24 +92,41 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
<template>
<div class="flex flex-col">
<div class="flex px-2 border-b-1 py-0.5 justify-between items-center">
<div
class="flex border-b-1 justify-between items-center"
:class="{
'px-2 py-1 h-10': isCellInputField,
'px-3 py-0.5': !isCellInputField,
}"
>
<div class="flex">
<NcTooltip hide-on-click>
<NcButton class="!border-0" size="small" type="secondary" @click="paginate('prev')">
<NcButton class="nc-prev-page-btn !border-0" size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.arrowLeft" class="h-4 w-4" />
</NcButton>
<template #title>
<span>{{ $t('labels.next') }}</span>
<span>{{ $t('labels.previous') }}</span>
</template>
</NcTooltip>
</div>
<span class="text-gray-700 font-semibold">{{
isYearPicker ? dayjs(selectedDate).year() : dayjs(pageDate).format('YYYY')
}}</span>
<span
class="nc-year-picker-btn text-gray-700 font-semibold"
:class="{
'cursor-pointer hover:text-brand-500': isCellInputField && !isYearPicker,
}"
@click="!isYearPicker ? (pickerType = 'year') : () => undefined"
>{{
isYearPicker
? isCellInputField
? dayjs(selectedDate).year() || dayjs().year()
: dayjs(selectedDate).year()
: dayjs(pageDate).format('YYYY')
}}</span
>
<div class="flex">
<NcTooltip hide-on-click>
<NcButton class="!border-0" size="small" type="secondary" @click="paginate('next')">
<NcButton class="nc-next-page-btn !border-0" size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.arrowRight" class="h-4 w-4" />
</NcButton>
<template #title>
@ -112,17 +135,29 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
</NcTooltip>
</div>
</div>
<div v-if="!hideCalendar" class="rounded-y-xl px-2.5 py-1 max-w-[350px]">
<div
v-if="!hideCalendar"
class="rounded-y-xl py-1 max-w-[350px]"
:class="{
'px-2': isCellInputField,
'px-2.5': !isCellInputField,
}"
>
<div class="grid grid-cols-4 gap-2">
<template v-if="!isYearPicker">
<span
v-for="(month, id) in months"
:key="id"
:class="{
'!bg-gray-200 !text-brand-900 !font-bold ': isMonthSelected(month),
'bg-gray-200 !text-brand-900 !font-bold': isMonthSelected(month) && !isCellInputField,
'bg-gray-300 !font-weight-600 ': isMonthSelected(month) && isCellInputField,
'hover:(border-1 border-gray-200 bg-gray-100)': !isMonthSelected(month),
'!text-brand-500': dayjs().isSame(month, 'month'),
'font-weight-400 rounded': isCellInputField,
'font-medium rounded-lg': !isCellInputField,
}"
class="h-8 rounded-lg flex items-center transition-all font-medium justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-700 cursor-pointer"
class="nc-month-item h-8 flex items-center transition-all justify-center text-gray-700 cursor-pointer"
:title="isCellInputField ? month.format('YYYY-MM') : undefined"
@click="selectedDate = month"
>
{{ month.format('MMM') }}
@ -133,10 +168,15 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
v-for="(year, id) in years"
:key="id"
:class="{
'!bg-gray-200 !text-brand-500 !font-bold ': compareYear(year, selectedDate),
'bg-gray-200 !text-brand-500 !font-bold ': compareYear(year, selectedDate) && !isCellInputField,
'bg-gray-300 !font-weight-600 ': compareYear(year, selectedDate) && isCellInputField,
'hover:(border-1 border-gray-200 bg-gray-100)': !compareYear(year, selectedDate),
'!text-brand-500': dayjs().isSame(year, 'year'),
'font-weight-400 text-gray-700 rounded': isCellInputField,
'font-medium text-gray-900 rounded-lg': !isCellInputField,
}"
class="h-8 rounded-lg flex items-center transition-all font-medium justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-900 cursor-pointer"
class="nc-year-item h-8 flex items-center transition-all justify-center cursor-pointer"
:title="isCellInputField ? year.format('YYYY') : undefined"
@click="selectedDate = year"
>
{{ year.format('YYYY') }}

104
packages/nc-gui/components/nc/TimeSelector.vue

@ -0,0 +1,104 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
interface Props {
selectedDate: dayjs.Dayjs | null
is12hrFormat?: boolean
isMinGranularityPicker?: boolean
minGranularity?: number
isOpen?: boolean
}
const props = withDefaults(defineProps<Props>(), {
selectedDate: null,
is12hrFormat: false,
isMinGranularityPicker: false,
minGranularity: 30,
isOpen: false,
})
const emit = defineEmits(['update:selectedDate'])
const pageDate = ref<dayjs.Dayjs>(dayjs())
const selectedDate = useVModel(props, 'selectedDate', emit)
const { is12hrFormat, isMinGranularityPicker, minGranularity, isOpen } = toRefs(props)
const timeOptionsWrapperRef = ref<HTMLDivElement>()
const compareTime = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
if (!date1 || !date2) return false
return date1.format('HH:mm') === date2.format('HH:mm')
}
const handleSelectTime = (time: dayjs.Dayjs) => {
pageDate.value = dayjs().set('hour', time.get('hour')).set('minute', time.get('minute'))
selectedDate.value = pageDate.value
// emit('update:selectedDate', pageDate.value)
}
// TODO: 12hr time format & regular time picker
const timeOptions = computed(() => {
return Array.from({ length: is12hrFormat.value ? 12 : 24 }).flatMap((_, h) => {
return (isMinGranularityPicker.value ? [0, minGranularity.value] : Array.from({ length: 60 })).map((_m, m) => {
const time = dayjs()
.set('hour', h)
.set('minute', isMinGranularityPicker.value ? (_m as number) : m)
return time
})
})
})
const handleAutoScroll = (behavior: ScrollBehavior = 'instant') => {
if (!timeOptionsWrapperRef.value || !selectedDate.value) return
setTimeout(() => {
const timeEl = timeOptionsWrapperRef.value?.querySelector(
`[data-testid="time-option-${selectedDate.value?.format('HH:mm')}"]`,
)
timeEl?.scrollIntoView({ behavior, block: 'center' })
}, 50)
}
watch([selectedDate, isOpen], () => {
if (timeOptionsWrapperRef.value && isOpen.value && selectedDate.value) {
handleAutoScroll()
}
})
onMounted(() => {
handleAutoScroll()
})
</script>
<template>
<div class="flex flex-col max-w-[350px]">
<div v-if="isMinGranularityPicker" ref="timeOptionsWrapperRef" class="h-[180px] overflow-y-auto nc-scrollbar-thin">
<div
v-for="time of timeOptions"
:key="time.format('HH:mm')"
class="hover:bg-gray-100 py-1 px-3 text-sm text-gray-600 font-weight-500 text-center cursor-pointer"
:class="{
'nc-selected bg-gray-100': selectedDate && compareTime(time, selectedDate),
}"
:data-testid="`time-option-${time.format('HH:mm')}`"
@click="handleSelectTime(time)"
>
{{ time.format('HH:mm') }}
</div>
</div>
<div v-else></div>
<div class="px-2 py-1 box-border flex items-center justify-center">
<NcButton :tabindex="-1" class="!h-7" size="small" type="secondary" @click="handleSelectTime(dayjs())">
<span class="text-small"> {{ $t('general.now') }} </span>
</NcButton>
</div>
</div>
</template>
<style lang="scss" scoped></style>

3
packages/nc-gui/lang/en.json

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",

4
packages/nc-gui/utils/formValidations.ts

@ -77,7 +77,9 @@ export const requiredFieldValidatorFn = (value: unknown) => {
}
if (typeof value === 'object') {
for (let _ in value) return true
if (Object.keys(value).length > 0) {
return true
}
return false
}

13
packages/nocodb-sdk/src/lib/dateTimeHelper.ts

@ -137,3 +137,16 @@ export const timeAgo = (date: any) => {
// show in local time
return dayjs(date).fromNow();
};
export const isValidTimeFormat = (value: string, format: string) => {
const regexValidator = {
[timeFormats[0]]: /^([01]\d|2[0-3]):[0-5]\d$/,
[timeFormats[1]]: /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/,
[timeFormats[2]]: /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}$/,
};
if (regexValidator[format]) {
return regexValidator[format].test(value);
}
return false;
};

62
tests/playwright/pages/Dashboard/BulkUpdate/index.ts

@ -107,20 +107,24 @@ export class BulkUpdatePage extends BasePage {
.click();
break;
case 'year':
picker = this.rootPage.locator('.ant-picker-dropdown.active');
await field.locator('input').click();
picker = this.rootPage.locator('.nc-picker-year.active');
await picker.waitFor();
await picker.locator(`td[title="${value}"]`).click();
await this.configureYear(value);
break;
case 'time':
picker = this.rootPage.locator('.ant-picker-dropdown.active');
await picker.waitFor();
// eslint-disable-next-line no-case-declarations
const time = value.split(':');
const timeInput = field.locator('.nc-time-input');
await timeInput.click();
// eslint-disable-next-line no-case-declarations
const timePanel = picker.locator('.ant-picker-time-panel-column');
await timePanel.nth(0).locator('li').nth(+time[0]).click();
await timePanel.nth(1).locator('li').nth(+time[1]).click();
await picker.locator('.ant-picker-ok').click();
const dropdown = this.rootPage.locator('.nc-picker-time.active');
await dropdown.waitFor({ state: 'visible' });
await timeInput.fill(value);
await this.rootPage.keyboard.press('Enter');
await dropdown.waitFor({ state: 'hidden' });
break;
case 'singleSelect':
picker = this.rootPage.locator('.ant-select-dropdown.active');
@ -146,27 +150,53 @@ export class BulkUpdatePage extends BasePage {
break;
case 'date':
{
await field.locator('input').click();
const values = value.split('-');
const { year, month, day } = { year: values[0], month: values[1], day: values[2] };
picker = this.rootPage.locator('.ant-picker-dropdown.active');
const monthBtn = picker.locator('.ant-picker-month-btn');
const yearBtn = picker.locator('.ant-picker-year-btn');
picker = this.rootPage.locator('.nc-picker-date.active');
await yearBtn.click();
await picker.waitFor();
await picker.locator(`td[title="${year}"]`).click();
const yearBtn = picker.locator('.nc-year-picker-btn');
await yearBtn.waitFor();
await yearBtn.click();
await this.configureYear(year);
const monthBtn = picker.locator('.nc-month-picker-btn');
await monthBtn.click();
await picker.waitFor();
await picker.locator(`td[title="${year}-${month}"]`).click();
await picker.locator(`span[title="${year}-${month}"]`).click();
await picker.waitFor();
await picker.locator(`td[title="${year}-${month}-${day}"]`).click();
await picker.locator(`span[title="${year}-${month}-${day}"]:visible`).click();
}
break;
}
}
async configureYear(year: string) {
// configure year
await this.rootPage.locator('.nc-year-picker-btn:visible').waitFor();
let flag = true;
while (flag) {
const firstVisibleYear = await this.rootPage.locator('.nc-year-item').first().textContent();
const lastVisibleYear = await this.rootPage.locator('.nc-year-item').last().textContent();
if (+year >= +firstVisibleYear && +year <= +lastVisibleYear) {
flag = false;
} else if (+year < +firstVisibleYear) {
await this.rootPage.locator('.nc-prev-page-btn').click();
} else if (+year > +lastVisibleYear) {
await this.rootPage.locator('.nc-next-page-btn').click();
}
}
await this.rootPage.locator(`span[title="${year}"]`).waitFor();
await this.rootPage.locator(`span[title="${year}"]`).click({ force: true });
}
async save({
awaitResponse = true,
}: {

14
tests/playwright/pages/Dashboard/ExpandedForm/index.ts

@ -93,12 +93,18 @@ export class ExpandedFormPage extends BasePage {
await this.dashboard.linkRecord.select(value);
break;
case 'dateTime':
await field.locator('.nc-cell').click();
await field.locator('.nc-cell .nc-date-input').click();
// eslint-disable-next-line no-case-declarations
const dateTimeObj = new DateTimeCellPageObject(this.dashboard.grid.cell);
await dateTimeObj.selectDate({ date: value.slice(0, 10) });
await dateTimeObj.selectTime({ hour: +value.slice(11, 13), minute: +value.slice(14, 16) });
await dateTimeObj.save();
await dateTimeObj.selectDate({ date: value.slice(0, 10), locator: field.locator('.nc-cell') });
await dateTimeObj.selectTime({
hour: +value.slice(11, 13),
minute: +value.slice(14, 16),
locator: field.locator('.nc-cell'),
fillValue: `${value.slice(11, 13).padStart(2, '0')}:${value.slice(14, 16).padStart(2, '0')}`,
});
break;
}
}

6
tests/playwright/pages/Dashboard/SurveyForm/index.ts

@ -76,11 +76,11 @@ export class SurveyFormPage extends BasePage {
// press enter key
await this.get().locator(`[data-testid="nc-survey-form__input-${param.fieldLabel}"] >> input`).press('Enter');
} else if (param.type === 'DateTime') {
await this.get().locator(`[data-testid="nc-survey-form__input-${param.fieldLabel}"] >> input`).click();
await this.get().locator(`[data-testid="nc-survey-form__input-${param.fieldLabel}"] >> input`).first().click();
const modal = this.rootPage.locator('.nc-picker-datetime');
await expect(modal).toBeVisible();
await modal.locator('.ant-picker-now-btn').click();
await modal.locator('.ant-picker-ok').click();
await modal.locator('.nc-date-picker-now-btn').click();
await modal.waitFor({ state: 'hidden' });
await this.nextButton.click();
}

101
tests/playwright/pages/Dashboard/common/Cell/DateTimeCell.ts

@ -1,3 +1,4 @@
import { Locator } from '@playwright/test';
import { CellPageObject } from '.';
import BasePage from '../../../Base';
@ -30,60 +31,103 @@ export class DateTimeCellPageObject extends BasePage {
// date formats in `YYYY-MM-DD`
date,
skipDate = false,
index,
columnHeader,
locator,
}: {
date: string;
skipDate?: boolean;
index?: number;
columnHeader?: string;
locator?: Locator;
}) {
// title date format needs to be YYYY-MM-DD
const [year, month, day] = date.split('-');
const dateLocator = locator ? locator : this.get({ index, columnHeader });
await dateLocator.click();
await dateLocator.locator('.nc-date-input').click();
// configure year
await this.rootPage.locator('.ant-picker-year-btn:visible').waitFor();
await this.rootPage.locator('.ant-picker-year-btn:visible').click();
await this.rootPage.locator(`td[title="${year}"]`).click();
await this.rootPage.locator('.nc-year-picker-btn:visible').waitFor();
await this.rootPage.locator('.nc-year-picker-btn:visible').click();
await this.rootPage.locator('.nc-year-picker-btn:visible').waitFor();
let flag = true;
while (flag) {
const firstVisibleYear = await this.rootPage.locator('.nc-year-item').first().textContent();
const lastVisibleYear = await this.rootPage.locator('.nc-year-item').last().textContent();
if (+year >= +firstVisibleYear && +year <= +lastVisibleYear) {
flag = false;
} else if (+year < +firstVisibleYear) {
await this.rootPage.locator('.nc-prev-page-btn').click();
} else if (+year > +lastVisibleYear) {
await this.rootPage.locator('.nc-next-page-btn').click();
}
}
await this.rootPage.locator(`span[title="${year}"]`).waitFor();
await this.rootPage.locator(`span[title="${year}"]`).click({ force: true });
if (skipDate) {
await this.rootPage.locator(`td[title="${year}-${month}"]`).click();
await this.rootPage.locator(`span[title="${year}-${month}"]`).click();
return;
}
// configure month
await this.rootPage.locator('.ant-picker-month-btn:visible').click();
await this.rootPage.locator(`td[title="${year}-${month}"]`).click();
await this.rootPage.locator('.nc-month-picker-btn:visible').click();
await this.rootPage.locator(`span[title="${year}-${month}"]`).click();
// configure day
await this.rootPage.locator(`td[title="${year}-${month}-${day}"]:visible`).click();
await this.rootPage.locator(`span[title="${year}-${month}-${day}"]:visible`).click();
}
async selectTime({
// hour: 0 - 23
// minute: 0 - 59
// second: 0 - 59
hour,
minute,
second,
index,
columnHeader,
fillValue,
selectFromPicker = false,
locator,
}: {
hour: number;
minute: number;
second?: number | null;
index?: number;
columnHeader?: string;
fillValue: string;
selectFromPicker?: boolean;
locator?: Locator;
}) {
await this.rootPage
.locator(
`.ant-picker-time-panel-column:nth-child(1) > .ant-picker-time-panel-cell:nth-child(${hour + 1}):visible`
)
.click();
await this.rootPage
.locator(
`.ant-picker-time-panel-column:nth-child(2) > .ant-picker-time-panel-cell:nth-child(${minute + 1}):visible`
)
.click();
if (second != null) {
await this.rootPage
.locator(
`.ant-picker-time-panel-column:nth-child(3) > .ant-picker-time-panel-cell:nth-child(${second + 1}):visible`
)
const timeLocator = locator ? locator : this.get({ index, columnHeader });
await timeLocator.click();
const timeInput = timeLocator.locator('.nc-time-input');
await timeInput.click();
const dropdown = this.rootPage.locator('.nc-picker-datetime.active');
await dropdown.waitFor({ state: 'visible' });
if (!selectFromPicker) {
await timeInput.fill(fillValue);
await this.rootPage.keyboard.press('Enter');
await this.rootPage.keyboard.press('Escape');
} else {
await dropdown
.locator(`[data-testid="time-option-${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}"]`)
.scrollIntoViewIfNeeded();
await dropdown
.locator(`[data-testid="time-option-${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}"]`)
.click();
}
await dropdown.waitFor({ state: 'hidden' });
}
async close() {
@ -94,8 +138,15 @@ export class DateTimeCellPageObject extends BasePage {
const [date, time] = dateTime.split(' ');
const [hour, minute, _second] = time.split(':');
await this.open({ index, columnHeader });
await this.selectDate({ date });
await this.selectTime({ hour: +hour, minute: +minute });
await this.save();
await this.selectDate({ date, index, columnHeader });
await this.selectTime({ hour: +hour, minute: +minute, index, columnHeader, fillValue: time });
}
clickDateInput = async (locator: Locator) => {
await locator.locator('.nc-date-input').click();
};
clickTimeInput = async (locator: Locator) => {
await locator.locator('.nc-time-input').click();
};
}

43
tests/playwright/pages/Dashboard/common/Cell/TimeCell.ts

@ -1,6 +1,6 @@
import { CellPageObject } from '.';
import BasePage from '../../../Base';
import { expect } from '@playwright/test';
import { expect, Locator } from '@playwright/test';
export class TimeCellPageObject extends BasePage {
readonly cell: CellPageObject;
@ -17,7 +17,7 @@ export class TimeCellPageObject extends BasePage {
async verify({ index, columnHeader, value }: { index: number; columnHeader: string; value: string }) {
const cell = this.get({ index, columnHeader });
await cell.scrollIntoViewIfNeeded();
await cell.locator(`input[title="${value}"]`).waitFor({ state: 'visible' });
await cell.locator(`.nc-time-picker[title="${value}"]`).waitFor({ state: 'visible' });
await expect(cell.locator(`[title="${value}"]`)).toBeVisible();
}
@ -27,18 +27,36 @@ export class TimeCellPageObject extends BasePage {
// second: 0 - 59
hour,
minute,
fillValue,
locator,
selectFromPicker = false,
}: {
hour: number;
minute: number;
fillValue: string;
locator: Locator;
selectFromPicker?: boolean;
}) {
const timePanel = this.rootPage.locator('.ant-picker-time-panel-column');
await timePanel.nth(0).locator('.ant-picker-time-panel-cell').nth(hour).click();
await timePanel.nth(1).locator('.ant-picker-time-panel-cell').nth(minute).click();
if (hour < 12) {
await timePanel.nth(2).locator('.ant-picker-time-panel-cell').nth(0).click();
const timeInput = locator.locator('.nc-time-input');
await timeInput.click();
const dropdown = this.rootPage.locator('.nc-picker-time.active');
await dropdown.waitFor({ state: 'visible' });
if (!selectFromPicker) {
await timeInput.fill(fillValue);
await this.rootPage.keyboard.press('Shift+Enter');
await this.rootPage.keyboard.press('Escape');
} else {
await timePanel.nth(2).locator('.ant-picker-time-panel-cell').nth(1).click();
await dropdown
.locator(`[data-testid="time-option-${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}"]`)
.scrollIntoViewIfNeeded();
await dropdown
.locator(`[data-testid="time-option-${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}"]`)
.click();
}
await dropdown.waitFor({ state: 'hidden' });
}
async save() {
@ -49,7 +67,12 @@ export class TimeCellPageObject extends BasePage {
const [hour, minute, _second] = value.split(':');
await this.get({ index, columnHeader }).click();
await this.get({ index, columnHeader }).dblclick();
await this.selectTime({ hour: +hour, minute: +minute });
await this.save();
await this.selectTime({
hour: +hour,
minute: +minute,
fillValue: value,
locator: this.get({ index, columnHeader }),
selectFromPicker: +minute === 0 || +minute === 30,
});
}
}

2
tests/playwright/pages/Dashboard/common/Cell/YearCell.ts

@ -17,7 +17,7 @@ export class YearCellPageObject extends BasePage {
async verify({ index, columnHeader, value }: { index: number; columnHeader: string; value: number }) {
const cell = this.get({ index, columnHeader });
await cell.scrollIntoViewIfNeeded();
await cell.locator(`input[title="${value}"]`).waitFor({ state: 'visible' });
await cell.locator(`.nc-year-picker[title="${value}"]`).waitFor({ state: 'visible' });
await expect(cell.locator(`[title="${value}"]`)).toBeVisible();
}
}

2
tests/playwright/pages/Dashboard/common/Cell/index.ts

@ -200,7 +200,7 @@ export class CellPageObject extends BasePage {
const cell = await this.get({
index,
columnHeader,
}).locator('input');
}).locator('.nc-date-picker');
return await cell.getAttribute('title');
})
.toEqual(expectedValue);

48
tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts

@ -225,36 +225,46 @@ export class ToolbarFilterPage extends BasePage {
switch (dataType) {
case UITypes.Year:
await this.get().locator('.nc-filter-value-select').click();
await this.rootPage.locator(`.ant-picker-dropdown:visible`).waitFor();
await this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click();
await this.rootPage.locator(`.nc-picker-year:visible`).waitFor();
await this.rootPage.locator('.nc-year-picker-btn:visible').waitFor();
await this.get().locator('.nc-filter-value-select .nc-year-input').fill(value);
await this.rootPage.keyboard.press('Enter');
break;
case UITypes.Time:
// eslint-disable-next-line no-case-declarations
const time = value.split(':');
await this.get().locator('.nc-filter-value-select').click();
await this.rootPage.locator(`.ant-picker-dropdown:visible`).waitFor();
await this.rootPage
.locator(`.ant-picker-time-panel-column:nth-child(1)`)
.locator(`.ant-picker-time-panel-cell:has-text("${time[0]}")`)
.click();
await this.rootPage
.locator(`.ant-picker-time-panel-column:nth-child(2)`)
.locator(`.ant-picker-time-panel-cell:has-text("${time[1]}")`)
.click();
await this.rootPage.locator(`.ant-btn-primary:has-text("Ok")`).click();
const timeInput = this.get().locator('.nc-filter-value-select').locator('.nc-time-input');
await timeInput.click();
// eslint-disable-next-line no-case-declarations
const dropdown = this.rootPage.locator('.nc-picker-time.active');
await dropdown.waitFor({ state: 'visible' });
await timeInput.fill(value);
await this.rootPage.keyboard.press('Enter');
await dropdown.waitFor({ state: 'hidden' });
break;
case UITypes.Date:
if (subOperation === 'exact date') {
await this.get().locator('.nc-filter-value-select').click();
await this.rootPage.locator(`.ant-picker-dropdown:visible`).waitFor();
await this.get().locator('.nc-filter-value-select .nc-date-input').click();
const dropdown = this.rootPage.locator(`.nc-picker-date.active`);
await dropdown.waitFor({ state: 'visible' });
const dateItem = dropdown.locator('.nc-date-item').getByText(value);
await dateItem.waitFor();
await dateItem.scrollIntoViewIfNeeded();
await dateItem.hover();
if (skipWaitingResponse) {
await this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click();
await dateItem.click();
await dropdown.waitFor({ state: 'hidden' });
await this.rootPage.waitForTimeout(350);
} else {
await this.waitForResponse({
uiAction: async () =>
await this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click(),
uiAction: async () => {
await dateItem.click();
await dropdown.waitFor({ state: 'hidden' });
},
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: locallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
});

11
tests/playwright/tests/db/columns/columnDateTime.spec.ts

@ -45,6 +45,7 @@ const dateTimeData = [
hour: 4,
minute: 30,
output: '10-12-2022 04:30',
selectFromPicker: true,
},
{
dateFormat: 'DD-MM-YYYY',
@ -122,16 +123,20 @@ test.describe('DateTime Column', () => {
await dashboard.grid.cell.dateTime.selectDate({
date: dateTimeData[i].date,
index: 0,
columnHeader: 'NC_DATETIME_0',
});
await dashboard.grid.cell.dateTime.selectTime({
index: 0,
columnHeader: 'NC_DATETIME_0',
hour: dateTimeData[i].hour,
minute: dateTimeData[i].minute,
second: dateTimeData[i].second,
fillValue: dateTimeData[i].output.split(' ')[1].trim(),
selectFromPicker: !!dateTimeData[i].selectFromPicker,
});
await dashboard.grid.cell.dateTime.save();
await dashboard.grid.cell.verifyDateCell({
index: 0,
columnHeader: 'NC_DATETIME_0',
@ -182,6 +187,8 @@ test.describe('Date Column', () => {
await dashboard.grid.cell.dateTime.selectDate({
date: dateData[i].date,
skipDate: dateData[i].dateFormat === 'YYYY-MM',
index: 0,
columnHeader: 'NC_DATE_0',
});
await dashboard.grid.cell.verifyDateCell({

2
tests/playwright/tests/db/views/viewFormShareSurvey.spec.ts

@ -38,6 +38,8 @@ test.describe('Share form', () => {
await dashboard.form.showAnotherFormRadioButton.click();
await dashboard.form.showAnotherFormAfter5SecRadioButton.click();
await dashboard.rootPage.waitForTimeout(200);
const surveyLink = await dashboard.form.topbar.getSharedViewUrl(true);
await dashboard.rootPage.waitForTimeout(2000);
await dashboard.rootPage.goto(surveyLink);

Loading…
Cancel
Save