Browse Source

Merge pull request #6870 from nocodb/feat/yyyy-mm

feat: support yyyy-mm in Datepicker
pull/7186/head
Raju Udava 9 months ago committed by GitHub
parent
commit
73e335084b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      packages/nc-gui/components/cell/DatePicker.vue
  2. 4
      packages/nc-gui/components/cell/DateTimePicker.vue
  3. 3
      packages/nc-gui/components/dashboard/settings/AuditTab.vue
  4. 3
      packages/nc-gui/components/dashboard/settings/BaseAudit.vue
  5. 2
      packages/nc-gui/components/notification/Item/Wrapper.vue
  6. 11
      packages/nc-gui/components/project/AccessSettings.vue
  7. 7
      packages/nc-gui/components/smartsheet/column/DateOptions.vue
  8. 3
      packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue
  9. 3
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  10. 3
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  11. 33
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  12. 5
      packages/nc-gui/components/template/Editor.vue
  13. 3
      packages/nc-gui/components/virtual-cell/Formula.vue
  14. 3
      packages/nc-gui/components/webhook/CallLog.vue
  15. 4
      packages/nc-gui/components/workspace/CollaboratorsList.vue
  16. 3
      packages/nc-gui/components/workspace/ProjectList.vue
  17. 18
      packages/nc-gui/composables/useMultiSelect/index.ts
  18. 3
      packages/nc-gui/helpers/parsers/CSVTemplateAdapter.ts
  19. 3
      packages/nc-gui/helpers/parsers/ExcelTemplateAdapter.ts
  20. 89
      packages/nc-gui/utils/dateTimeUtils.ts
  21. 413
      packages/nc-gui/utils/filterUtils.ts
  22. 1
      packages/nc-gui/utils/index.ts
  23. 3
      packages/nocodb-sdk/package.json
  24. 121
      packages/nocodb-sdk/src/lib/dateTimeHelper.ts
  25. 1
      packages/nocodb-sdk/src/lib/index.ts
  26. 13
      packages/nocodb/src/db/BaseModelSqlv2.ts
  27. 15
      packages/nocodb/src/db/conditionV2.ts
  28. 11
      packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts
  29. 2
      packages/nocodb/src/db/functionMappings/sqlite.ts
  30. 30
      packages/nocodb/src/helpers/formulaFnHelper.ts
  31. 75
      packages/nocodb/src/utils/dateTimeUtils.ts
  32. 3
      pnpm-lock.yaml
  33. 10
      tests/playwright/pages/Dashboard/Grid/Column/index.ts
  34. 7
      tests/playwright/pages/Dashboard/common/Cell/DateTimeCell.ts
  35. 75
      tests/playwright/tests/db/columns/columnDateTime.spec.ts
  36. 2
      tests/playwright/tests/db/features/keyboardShortcuts.spec.ts

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

@ -1,17 +1,24 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { isDateMonthFormat } from 'nocodb-sdk'
import {
ActiveCellInj,
CellClickHookInj,
ColumnInj,
EditColumnInj,
EditModeInj,
IsLockedInj,
ReadonlyInj,
computed,
inject,
isDrawerOrModalExist,
onClickOutside,
onMounted,
onUnmounted,
parseProp,
ref,
useGlobal,
useI18n,
useSelectedCellKeyupListener,
watch,
} from '#imports'
@ -43,6 +50,8 @@ const isDateInvalid = ref(false)
const dateFormat = computed(() => parseProp(columnMeta?.value?.meta)?.date_format ?? 'YYYY-MM-DD')
const picker = computed(() => (isDateMonthFormat(dateFormat.value) ? 'month' : ''))
const localState = computed({
get() {
if (!modelValue) {
@ -54,7 +63,9 @@ const localState = computed({
return undefined
}
return /^\d+$/.test(modelValue) ? dayjs(+modelValue) : dayjs(modelValue)
const format = picker.value === 'month' ? dateFormat : 'YYYY-MM-DD'
return dayjs(/^\d+$/.test(modelValue) ? +modelValue : modelValue, format)
},
set(val?: dayjs.Dayjs) {
if (!val) {
@ -62,6 +73,11 @@ const localState = computed({
return
}
if (picker.value === 'month') {
// reset day to 1st
val = dayjs(val).date(1)
}
if (val.isValid()) {
emit('update:modelValue', val?.format('YYYY-MM-DD'))
}
@ -198,12 +214,15 @@ const updateOpen = (next: boolean) => {
}
const cellClickHook = inject(CellClickHookInj, null)
const cellClickHandler = () => {
open.value = (active.value || editable.value) && !open.value
}
onMounted(() => {
cellClickHook?.on(cellClickHandler)
})
onUnmounted(() => {
cellClickHook?.on(cellClickHandler)
})
@ -219,6 +238,7 @@ const clickHandler = () => {
<template>
<a-date-picker
v-model:value="localState"
:picker="picker"
:bordered="false"
class="!w-full !px-1 !border-none"
:class="{ 'nc-null': modelValue === null && showNull }"

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

@ -1,18 +1,16 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { isSystemColumn } from 'nocodb-sdk'
import { dateFormats, isSystemColumn, timeFormats } from 'nocodb-sdk'
import {
ActiveCellInj,
CellClickHookInj,
ColumnInj,
EditColumnInj,
ReadonlyInj,
dateFormats,
inject,
isDrawerOrModalExist,
parseProp,
ref,
timeFormats,
useBase,
useSelectedCellKeyupListener,
watch,

3
packages/nc-gui/components/dashboard/settings/AuditTab.vue

@ -1,7 +1,8 @@
<script setup lang="ts">
import { Tooltip as ATooltip, Empty } from 'ant-design-vue'
import type { AuditType } from 'nocodb-sdk'
import { ProjectIdInj, h, iconMap, onMounted, storeToRefs, timeAgo, useBase, useGlobal, useI18n, useNuxtApp } from '#imports'
import { timeAgo } from 'nocodb-sdk'
import { ProjectIdInj, h, iconMap, onMounted, storeToRefs, useBase, useGlobal, useI18n, useNuxtApp } from '#imports'
const { $api } = useNuxtApp()

3
packages/nc-gui/components/dashboard/settings/BaseAudit.vue

@ -1,7 +1,8 @@
<script setup lang="ts">
import { Tooltip as ATooltip, Empty } from 'ant-design-vue'
import type { AuditType } from 'nocodb-sdk'
import { h, iconMap, onMounted, storeToRefs, timeAgo, useBase, useGlobal, useI18n, useNuxtApp } from '#imports'
import { timeAgo } from 'nocodb-sdk'
import { h, iconMap, onMounted, storeToRefs, useBase, useGlobal, useI18n, useNuxtApp } from '#imports'
interface Props {
sourceId: string

2
packages/nc-gui/components/notification/Item/Wrapper.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { timeAgo } from '#imports'
import { timeAgo } from 'nocodb-sdk'
const props = defineProps<{
item: {

11
packages/nc-gui/components/project/AccessSettings.vue

@ -1,8 +1,15 @@
<script lang="ts" setup>
import {
OrderedProjectRoles,
OrgUserRoles,
ProjectRoles,
WorkspaceRolesToProjectRoles,
extractRolesObj,
timeAgo,
} from 'nocodb-sdk'
import type { WorkspaceUserRoles } from 'nocodb-sdk'
import { OrderedProjectRoles, OrgUserRoles, ProjectRoles, WorkspaceRolesToProjectRoles, extractRolesObj } from 'nocodb-sdk'
import InfiniteLoading from 'v3-infinite-loading'
import { isEeUI, storeToRefs, timeAgo } from '#imports'
import { isEeUI, storeToRefs } from '#imports'
const basesStore = useBases()
const { getProjectUsers, createProjectUser, updateProjectUser, removeProjectUser } = basesStore

7
packages/nc-gui/components/smartsheet/column/DateOptions.vue

@ -1,5 +1,6 @@
<script setup lang="ts">
import { dateFormats, useVModel } from '#imports'
import { dateFormats, dateMonthFormats } from 'nocodb-sdk'
import { useVModel } from '#imports'
const props = defineProps<{
value: any
@ -17,8 +18,8 @@ if (!vModel.value.meta?.date_format) {
<template>
<a-form-item :label="$t('labels.dateFormat')">
<a-select v-model:value="vModel.meta.date_format" dropdown-class-name="nc-dropdown-date-format">
<a-select-option v-for="(format, i) of dateFormats" :key="i" :value="format">
<a-select v-model:value="vModel.meta.date_format" class="nc-date-select" dropdown-class-name="nc-dropdown-date-format">
<a-select-option v-for="(format, i) of [...dateFormats, ...dateMonthFormats]" :key="i" :value="format">
<div class="flex flex-row items-center">
<div class="text-xs">
{{ format }}

3
packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue

@ -1,5 +1,6 @@
<script setup lang="ts">
import { dateFormats, timeFormats, useVModel } from '#imports'
import { dateFormats, timeFormats } from 'nocodb-sdk'
import { useVModel } from '#imports'
const props = defineProps<{
value: any

3
packages/nc-gui/components/smartsheet/column/FormulaOptions.vue

@ -3,7 +3,7 @@ import type { Ref } from 'vue'
import type { ListItem as AntListItem } from 'ant-design-vue'
import jsep from 'jsep'
import type { ColumnType, FormulaType } from 'nocodb-sdk'
import { UITypes, jsepCurlyHook, substituteColumnIdWithAliasInFormula } from 'nocodb-sdk'
import { UITypes, jsepCurlyHook, substituteColumnIdWithAliasInFormula, validateDateWithUnknownFormat } from 'nocodb-sdk'
import {
MetaInj,
NcAutocompleteTree,
@ -18,7 +18,6 @@ import {
useColumnCreateStoreOrThrow,
useDebounceFn,
useVModel,
validateDateWithUnknownFormat,
} from '#imports'
const props = defineProps<{

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

@ -1,8 +1,9 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import type { AuditType } from 'nocodb-sdk'
import { timeAgo } from 'nocodb-sdk'
import { Icon } from '@iconify/vue'
import { ref, timeAgo, useExpandedFormStoreOrThrow, useGlobal, useRoles, watch } from '#imports'
import { ref, useExpandedFormStoreOrThrow, useGlobal, useRoles, watch } from '#imports'
const props = defineProps<{
loading: boolean

33
packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue

@ -107,12 +107,16 @@ const isFilterDraft = (filter: Filter, col: ColumnType) => {
if (
filter.comparison_op &&
comparisonSubOpList(filter.comparison_op).find((compOp) => compOp.value === filter.comparison_sub_op)?.ignoreVal
comparisonSubOpList(filter.comparison_op, col?.meta?.date_format).find((compOp) => compOp.value === filter.comparison_sub_op)
?.ignoreVal
) {
return false
}
if (comparisonOpList(col.uidt as UITypes).find((compOp) => compOp.value === filter.comparison_op)?.ignoreVal) {
if (
comparisonOpList(col.uidt as UITypes, col?.meta?.date_format).find((compOp) => compOp.value === filter.comparison_op)
?.ignoreVal
) {
return false
}
@ -145,7 +149,7 @@ const filterUpdateCondition = (filter: FilterType, i: number) => {
// hence remove the previous value
filter.value = null
if (
!comparisonSubOpList(filter.comparison_op!)
!comparisonSubOpList(filter.comparison_op!, col?.meta?.date_format)
.map((op) => op.value)
.includes(filter.comparison_sub_op!)
) {
@ -224,8 +228,9 @@ const selectFilterField = (filter: Filter, index: number) => {
// since the existing one may not be supported for the new field
// e.g. `eq` operator is not supported in checkbox field
// hence, get the first option of the supported operators of the new field
filter.comparison_op = comparisonOpList(col.uidt as UITypes).find((compOp) => isComparisonOpAllowed(filter, compOp))
?.value as FilterType['comparison_op']
filter.comparison_op = comparisonOpList(col.uidt as UITypes, col?.meta?.date_format).find((compOp) =>
isComparisonOpAllowed(filter, compOp),
)?.value as FilterType['comparison_op']
if ([UITypes.Date, UITypes.DateTime].includes(col.uidt as UITypes) && !['blank', 'notblank'].includes(filter.comparison_op!)) {
if (filter.comparison_op === 'isWithin') {
@ -297,12 +302,16 @@ const addFilterGroup = async () => {
}
const showFilterInput = (filter: Filter) => {
const col = getColumn(filter)
if (!filter.comparison_op) return false
if (filter.comparison_sub_op) {
return !comparisonSubOpList(filter.comparison_op).find((op) => op.value === filter.comparison_sub_op)?.ignoreVal
return !comparisonSubOpList(filter.comparison_op, getColumn(filter)?.meta?.date_format).find(
(op) => op.value === filter.comparison_sub_op,
)?.ignoreVal
} else {
return !comparisonOpList(getColumn(filter)?.uidt as UITypes).find((op) => op.value === filter.comparison_op)?.ignoreVal
return !comparisonOpList(col?.uidt as UITypes, col?.meta?.date_format).find((op) => op.value === filter.comparison_op)
?.ignoreVal
}
}
@ -427,7 +436,10 @@ onBeforeUnmount(() => {
dropdown-class-name="nc-dropdown-filter-comp-op"
@change="filterUpdateCondition(filter, i)"
>
<template v-for="compOp of comparisonOpList(getColumn(filter)?.uidt)" :key="compOp.value">
<template
v-for="compOp of comparisonOpList(getColumn(filter)?.uidt, getColumn(filter)?.meta?.date_format)"
:key="compOp.value"
>
<a-select-option v-if="isComparisonOpAllowed(filter, compOp)" :value="compOp.value">
{{ compOp.text }}
</a-select-option>
@ -450,7 +462,10 @@ onBeforeUnmount(() => {
dropdown-class-name="nc-dropdown-filter-comp-sub-op"
@change="filterUpdateCondition(filter, i)"
>
<template v-for="compSubOp of comparisonSubOpList(filter.comparison_op)" :key="compSubOp.value">
<template
v-for="compSubOp of comparisonSubOpList(filter.comparison_op, getColumn(filter)?.meta?.date_format)"
:key="compSubOp.value"
>
<a-select-option v-if="isComparisonSubOpAllowed(filter, compSubOp)" :value="compSubOp.value">
{{ compSubOp.text }}
</a-select-option>

5
packages/nc-gui/components/template/Editor.vue

@ -2,7 +2,7 @@
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { UITypes, getDateFormat, getDateTimeFormat, isSystemColumn, isVirtualCol, parseStringDate } from 'nocodb-sdk'
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface'
import { srcDestMappingColumns, tableColumns } from './utils'
import {
@ -18,15 +18,12 @@ import {
extractSdkResponseErrorMsg,
fieldLengthValidator,
fieldRequiredValidator,
getDateFormat,
getDateTimeFormat,
getUIDTIcon,
iconMap,
inject,
message,
nextTick,
onMounted,
parseStringDate,
reactive,
ref,
storeToRefs,

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

@ -1,7 +1,8 @@
<script lang="ts" setup>
import { handleTZ } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { CellValueInj, ColumnInj, computed, handleTZ, inject, renderValue, replaceUrlsWithLink, useBase } from '#imports'
import { CellValueInj, ColumnInj, computed, inject, renderValue, replaceUrlsWithLink, useBase } from '#imports'
// todo: column type doesn't have required property `error` - throws in typecheck
const column = inject(ColumnInj) as Ref<ColumnType & { colOptions: { error: any } }>

3
packages/nc-gui/components/webhook/CallLog.vue

@ -1,6 +1,7 @@
<script setup lang="ts">
import type { HookLogType, HookType } from 'nocodb-sdk'
import { AutomationLogLevel, extractSdkResponseErrorMsg, onBeforeMount, parseProp, timeAgo, useApi, useGlobal } from '#imports'
import { timeAgo } from 'nocodb-sdk'
import { AutomationLogLevel, extractSdkResponseErrorMsg, onBeforeMount, parseProp, useApi, useGlobal } from '#imports'
interface Props {
hook: HookType

4
packages/nc-gui/components/workspace/CollaboratorsList.vue

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { OrderedWorkspaceRoles, WorkspaceUserRoles } from 'nocodb-sdk'
import { storeToRefs, timeAgo, useWorkspace } from '#imports'
import { OrderedWorkspaceRoles, WorkspaceUserRoles, timeAgo } from 'nocodb-sdk'
import { storeToRefs, useWorkspace } from '#imports'
const { workspaceRoles, loadRoles } = useRoles()

3
packages/nc-gui/components/workspace/ProjectList.vue

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { Empty } from 'ant-design-vue'
import type { BaseType } from 'nocodb-sdk'
import { ProjectRoles, ProjectStatus, WorkspaceUserRoles } from 'nocodb-sdk'
import { ProjectRoles, ProjectStatus, WorkspaceUserRoles, timeAgo } from 'nocodb-sdk'
import { nextTick } from '@vue/runtime-core'
import {
NcProjectType,
@ -12,7 +12,6 @@ import {
navigateTo,
ref,
storeToRefs,
timeAgo,
useBases,
useGlobal,
useNuxtApp,

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

@ -3,14 +3,13 @@ import dayjs from 'dayjs'
import type { Ref } from 'vue'
import type { MaybeRef } from '@vueuse/core'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { RelationTypes, UITypes, dateFormats, isDateMonthFormat, isSystemColumn, isVirtualCol, timeFormats } from 'nocodb-sdk'
import { parse } from 'papaparse'
import type { Cell } from './cellRange'
import { CellRange } from './cellRange'
import convertCellData from './convertCellData'
import type { Nullable, Row } from '#imports'
import {
dateFormats,
extractPkFromRow,
extractSdkResponseErrorMsg,
isDrawerOrModalExist,
@ -20,7 +19,6 @@ import {
message,
reactive,
ref,
timeFormats,
unref,
useBase,
useCopy,
@ -155,6 +153,20 @@ export function useMultiSelect(
}
}
if (columnObj.uidt === UITypes.Date) {
const dateFormat = columnObj.meta?.date_format
if (dateFormat && isDateMonthFormat(dateFormat)) {
// any date month format (e.g. YYYY-MM) couldn't be stored in database
// with date type since it is not a valid date
// therefore, we reformat the value here to display with the formatted one
// e.g. 2023-06-03 -> 2023-06
textToCopy = dayjs(textToCopy, dateFormat).format(dateFormat)
} else {
// e.g. 2023-06-03 (in DB) -> 03/06/2023 (in UI)
textToCopy = dayjs(textToCopy, 'YYYY-MM-DD').format(dateFormat)
}
}
if (columnObj.uidt === UITypes.Time) {
// remove `"`
// e.g. "2023-05-12T08:03:53.000Z" -> 2023-05-12T08:03:53.000Z

3
packages/nc-gui/helpers/parsers/CSVTemplateAdapter.ts

@ -1,7 +1,6 @@
import { parse } from 'papaparse'
import type { UploadFile } from 'ant-design-vue'
import { UITypes } from 'nocodb-sdk'
import { getDateFormat, validateDateWithUnknownFormat } from '../../utils/dateTimeUtils'
import { UITypes, getDateFormat, validateDateWithUnknownFormat } from 'nocodb-sdk'
import {
extractMultiOrSingleSelectProps,
getCheckboxValue,

3
packages/nc-gui/helpers/parsers/ExcelTemplateAdapter.ts

@ -1,5 +1,4 @@
import { UITypes } from 'nocodb-sdk'
import { getDateFormat } from '../../utils/dateTimeUtils'
import { UITypes, getDateFormat } from 'nocodb-sdk'
import TemplateGenerator from './TemplateGenerator'
import {
extractMultiOrSingleSelectProps,

89
packages/nc-gui/utils/dateTimeUtils.ts

@ -1,89 +0,0 @@
import dayjs from 'dayjs'
export const timeAgo = (date: any) => {
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(date)) {
// if there is no timezone info, consider as UTC
// e.g. 2023-01-01 08:00:00 (MySQL)
date += '+00:00'
}
// show in local time
return dayjs(date).fromNow()
}
export const dateFormats = [
'YYYY-MM-DD',
'YYYY/MM/DD',
'DD-MM-YYYY',
'MM-DD-YYYY',
'DD/MM/YYYY',
'MM/DD/YYYY',
'DD MM YYYY',
'MM DD YYYY',
'YYYY MM DD',
]
export const timeFormats = ['HH:mm', 'HH:mm:ss']
export const handleTZ = (val: any) => {
if (val === undefined || val === null) {
return
}
if (typeof val !== 'string') {
return val
}
return val.replace(
/((?:-?(?:[1-9][0-9]*)?[0-9]{4})-(?:1[0-2]|0[1-9])-(?:3[01]|0[1-9]|[12][0-9])T(?:2[0-3]|[01][0-9]):(?:[0-5][0-9]):(?:[0-5][0-9])(?:\.[0-9]+)?(?:Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9]))/g,
(i, v) => {
return dayjs(v).format('YYYY-MM-DD HH:mm')
},
)
}
export function validateDateFormat(v: string) {
return dateFormats.includes(v)
}
export function validateDateWithUnknownFormat(v: string) {
for (const format of dateFormats) {
if (dayjs(v, format, true).isValid() as any) {
return true
}
for (const timeFormat of ['HH:mm', 'HH:mm:ss', 'HH:mm:ss.SSS']) {
if (dayjs(v, `${format} ${timeFormat}`, true).isValid() as any) {
return true
}
}
}
return false
}
export function getDateFormat(v: string) {
for (const format of dateFormats) {
if (dayjs(v, format, true).isValid()) {
return format
}
}
return 'YYYY/MM/DD'
}
export function getDateTimeFormat(v: string) {
for (const format of dateFormats) {
for (const timeFormat of timeFormats) {
const dateTimeFormat = `${format} ${timeFormat}`
if (dayjs(v, dateTimeFormat, true).isValid() as any) {
return dateTimeFormat
}
}
}
return 'YYYY/MM/DD'
}
export function parseStringDate(v: string, dateFormat: string) {
const dayjsObj = dayjs(v)
if (dayjsObj.isValid()) {
v = dayjsObj.format('YYYY-MM-DD')
} else {
v = dayjs(v, dateFormat).format('YYYY-MM-DD')
}
return v
}

413
packages/nc-gui/utils/filterUtils.ts

@ -1,4 +1,4 @@
import { UITypes, isNumericCol, numericUITypes } from 'nocodb-sdk'
import { UITypes, isDateMonthFormat, isNumericCol, numericUITypes } from 'nocodb-sdk'
const getEqText = (fieldUiType: UITypes) => {
if (isNumericCol(fieldUiType) || fieldUiType === UITypes.Time) {
@ -70,210 +70,215 @@ const getLteText = (fieldUiType: UITypes) => {
export const comparisonOpList = (
fieldUiType: UITypes,
dateFormat?: string,
): {
text: string
value: string
ignoreVal: boolean
includedTypes?: UITypes[]
excludedTypes?: UITypes[]
}[] => [
{
text: 'is checked',
value: 'checked',
ignoreVal: true,
includedTypes: [UITypes.Checkbox],
},
{
text: 'is not checked',
value: 'notchecked',
ignoreVal: true,
includedTypes: [UITypes.Checkbox],
},
{
text: getEqText(fieldUiType),
value: 'eq',
ignoreVal: false,
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment],
},
{
text: getNeqText(fieldUiType),
value: 'neq',
ignoreVal: false,
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment],
},
{
text: getLikeText(fieldUiType),
value: 'like',
ignoreVal: false,
excludedTypes: [
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
...numericUITypes,
],
},
{
text: getNotLikeText(fieldUiType),
value: 'nlike',
ignoreVal: false,
excludedTypes: [
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
...numericUITypes,
],
},
{
text: 'is empty',
value: 'empty',
ignoreVal: true,
excludedTypes: [
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
...numericUITypes,
],
},
{
text: 'is not empty',
value: 'notempty',
ignoreVal: true,
excludedTypes: [
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
...numericUITypes,
],
},
{
text: 'is null',
value: 'null',
ignoreVal: true,
excludedTypes: [
...numericUITypes,
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
],
},
{
text: 'is not null',
value: 'notnull',
ignoreVal: true,
excludedTypes: [
...numericUITypes,
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
],
},
{
text: 'contains all of',
value: 'allof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect],
},
{
text: 'contains any of',
value: 'anyof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect],
},
{
text: 'does not contain all of',
value: 'nallof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect],
},
{
text: 'does not contain any of',
value: 'nanyof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect],
},
{
text: getGtText(fieldUiType),
value: 'gt',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
},
{
text: getLtText(fieldUiType),
value: 'lt',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
},
{
text: getGteText(fieldUiType),
value: 'gte',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
},
{
text: getLteText(fieldUiType),
value: 'lte',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
},
{
text: 'is within',
value: 'isWithin',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
},
{
text: 'is blank',
value: 'blank',
ignoreVal: true,
excludedTypes: [UITypes.Checkbox, UITypes.Links, UITypes.Rollup],
},
{
text: 'is not blank',
value: 'notblank',
ignoreVal: true,
excludedTypes: [UITypes.Checkbox, UITypes.Links, UITypes.Rollup],
},
]
}[] => {
const isDateMonth = dateFormat && isDateMonthFormat(dateFormat)
return [
{
text: 'is checked',
value: 'checked',
ignoreVal: true,
includedTypes: [UITypes.Checkbox],
},
{
text: 'is not checked',
value: 'notchecked',
ignoreVal: true,
includedTypes: [UITypes.Checkbox],
},
{
text: getEqText(fieldUiType),
value: 'eq',
ignoreVal: false,
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment],
},
{
text: getNeqText(fieldUiType),
value: 'neq',
ignoreVal: false,
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment],
},
{
text: getLikeText(fieldUiType),
value: 'like',
ignoreVal: false,
excludedTypes: [
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
...numericUITypes,
],
},
{
text: getNotLikeText(fieldUiType),
value: 'nlike',
ignoreVal: false,
excludedTypes: [
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
...numericUITypes,
],
},
{
text: 'is empty',
value: 'empty',
ignoreVal: true,
excludedTypes: [
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
...numericUITypes,
],
},
{
text: 'is not empty',
value: 'notempty',
ignoreVal: true,
excludedTypes: [
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
...numericUITypes,
],
},
{
text: 'is null',
value: 'null',
ignoreVal: true,
excludedTypes: [
...numericUITypes,
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
],
},
{
text: 'is not null',
value: 'notnull',
ignoreVal: true,
excludedTypes: [
...numericUITypes,
UITypes.Checkbox,
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
],
},
{
text: 'contains all of',
value: 'allof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect],
},
{
text: 'contains any of',
value: 'anyof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect],
},
{
text: 'does not contain all of',
value: 'nallof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect],
},
{
text: 'does not contain any of',
value: 'nanyof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect],
},
{
text: getGtText(fieldUiType),
value: 'gt',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
},
{
text: getLtText(fieldUiType),
value: 'lt',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
},
{
text: getGteText(fieldUiType),
value: 'gte',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
},
{
text: getLteText(fieldUiType),
value: 'lte',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
},
{
text: 'is within',
value: 'isWithin',
ignoreVal: true,
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime])],
},
{
text: 'is blank',
value: 'blank',
ignoreVal: true,
excludedTypes: [UITypes.Checkbox, UITypes.Links, UITypes.Rollup],
},
{
text: 'is not blank',
value: 'notblank',
ignoreVal: true,
excludedTypes: [UITypes.Checkbox, UITypes.Links, UITypes.Rollup],
},
]
}
export const comparisonSubOpList = (
// TODO: type
comparison_op: string,
dateFormat?: string,
): {
text: string
value: string
@ -281,6 +286,8 @@ export const comparisonSubOpList = (
includedTypes?: UITypes[]
excludedTypes?: UITypes[]
}[] => {
const isDateMonth = dateFormat && isDateMonthFormat(dateFormat)
if (comparison_op === 'isWithin') {
return [
{
@ -338,31 +345,31 @@ export const comparisonSubOpList = (
text: 'today',
value: 'today',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime])],
},
{
text: 'tomorrow',
value: 'tomorrow',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime])],
},
{
text: 'yesterday',
value: 'yesterday',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime])],
},
{
text: 'one week ago',
value: 'oneWeekAgo',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime])],
},
{
text: 'one week from now',
value: 'oneWeekFromNow',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime])],
},
{
text: 'one month ago',
@ -380,16 +387,16 @@ export const comparisonSubOpList = (
text: 'number of days ago',
value: 'daysAgo',
ignoreVal: false,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime])],
},
{
text: 'number of days from now',
value: 'daysFromNow',
ignoreVal: false,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime])],
},
{
text: 'exact date',
text: isDateMonth ? 'exact month' : 'exact date',
value: 'exactDate',
ignoreVal: false,
includedTypes: [UITypes.Date, UITypes.DateTime],

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

@ -1,6 +1,5 @@
export * from './NcAutocompleteTree'
export * from './colorsUtils'
export * from './dateTimeUtils'
export * from './deepCompare'
export * from './formulaUtils'
export * from './durationUtils'

3
packages/nocodb-sdk/package.json

@ -39,7 +39,8 @@
},
"dependencies": {
"axios": "^1.6.2",
"jsep": "^1.3.8"
"jsep": "^1.3.8",
"dayjs": "^1.11.9"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.13.1",

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

@ -0,0 +1,121 @@
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime.js';
import customParseFormat from 'dayjs/plugin/customParseFormat.js';
import duration from 'dayjs/plugin/duration.js';
import utc from 'dayjs/plugin/utc.js';
import weekday from 'dayjs/plugin/weekday.js';
import timezone from 'dayjs/plugin/timezone.js';
dayjs.extend(utc);
dayjs.extend(relativeTime);
dayjs.extend(customParseFormat);
dayjs.extend(duration);
dayjs.extend(weekday);
dayjs.extend(timezone);
export const dateMonthFormats = ['YYYY-MM', 'YYYY MM'];
export const timeFormats = ['HH:mm', 'HH:mm:ss', 'HH:mm:ss.SSS'];
export const dateFormats = [
'YYYY-MM-DD',
'YYYY/MM/DD',
'DD-MM-YYYY',
'MM-DD-YYYY',
'DD/MM/YYYY',
'MM/DD/YYYY',
'DD MM YYYY',
'MM DD YYYY',
'YYYY MM DD',
];
export const isDateMonthFormat = (format: string) =>
dateMonthFormats.includes(format);
export function validateDateWithUnknownFormat(v: string) {
for (const format of dateFormats) {
if (dayjs(v, format, true).isValid() as any) {
return true;
}
for (const timeFormat of timeFormats) {
if (dayjs(v, `${format} ${timeFormat}`, true).isValid() as any) {
return true;
}
}
}
return false;
}
export function getDateFormat(v: string) {
for (const format of dateFormats) {
if (dayjs(v, format, true).isValid()) {
return format;
}
}
return 'YYYY/MM/DD';
}
export function getDateTimeFormat(v: string) {
for (const format of dateFormats) {
for (const timeFormat of timeFormats) {
const dateTimeFormat = `${format} ${timeFormat}`;
if (dayjs(v, dateTimeFormat, true).isValid() as any) {
return dateTimeFormat;
}
}
}
return 'YYYY/MM/DD';
}
export function parseStringDate(v: string, dateFormat: string) {
const dayjsObj = dayjs(v);
if (dayjsObj.isValid()) {
v = dayjsObj.format('YYYY-MM-DD');
} else {
v = dayjs(v, dateFormat).format('YYYY-MM-DD');
}
return v;
}
export function convertToTargetFormat(
v: string,
oldDataFormat,
newDateFormat: string
) {
if (
!dateFormats.includes(oldDataFormat) ||
!dateFormats.includes(newDateFormat)
)
return v;
return dayjs(v, oldDataFormat).format(newDateFormat);
}
export const handleTZ = (val: any) => {
if (val === undefined || val === null) {
return;
}
if (typeof val !== 'string') {
return val;
}
return val.replace(
// match and extract dates and times in the ISO 8601 format
/((?:-?(?:[1-9][0-9]*)?[0-9]{4})-(?:1[0-2]|0[1-9])-(?:3[01]|0[1-9]|[12][0-9])T(?:2[0-3]|[01][0-9]):(?:[0-5][0-9]):(?:[0-5][0-9])(?:\.[0-9]+)?(?:Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9]))/g,
(_, v) => {
return dayjs(v).format('YYYY-MM-DD HH:mm');
}
);
};
export function validateDateFormat(v: string) {
return dateFormats.includes(v);
}
export const timeAgo = (date: any) => {
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(date)) {
// if there is no timezone info, consider as UTC
// e.g. 2023-01-01 08:00:00 (MySQL)
date += '+00:00';
}
// show in local time
return dayjs(date).fromNow();
};

1
packages/nocodb-sdk/src/lib/index.ts

@ -18,3 +18,4 @@ export { default as CustomAPI, FileType } from '~/lib/CustomAPI';
export { default as TemplateGenerator } from '~/lib/TemplateGenerator';
export * from '~/lib/passwordHelpers';
export * from '~/lib/mergeSwaggerSchema';
export * from '~/lib/dateTimeHelper';

13
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -4711,6 +4711,14 @@ class BaseModelSqlv2 {
continue;
}
if (col.uidt === UITypes.Date) {
const dateFormat = col.meta?.date_format;
if (dateFormat) {
d[col.title] = dayjs(d[col.title], dateFormat).format(dateFormat);
}
continue;
}
let keepLocalTime = true;
if (this.isSqlite) {
@ -4765,7 +4773,10 @@ class BaseModelSqlv2 {
const dateTimeColumns = (
childTable ? childTable.columns : this.model.columns
).filter(
(c) => c.uidt === UITypes.DateTime || c.uidt === UITypes.Formula,
(c) =>
c.uidt === UITypes.DateTime ||
c.uidt === UITypes.Date ||
c.uidt === UITypes.Formula,
);
if (dateTimeColumns.length) {
if (Array.isArray(data)) {

15
packages/nocodb/src/db/conditionV2.ts

@ -1,4 +1,9 @@
import { isNumericCol, RelationTypes, UITypes } from 'nocodb-sdk';
import {
isDateMonthFormat,
isNumericCol,
RelationTypes,
UITypes,
} from 'nocodb-sdk';
import dayjs from 'dayjs';
// import customParseFormat from 'dayjs/plugin/customParseFormat.js';
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2';
@ -466,7 +471,13 @@ const parseConditionV2 = async (
: 'YYYY-MM-DD HH:mm:ssZ';
if ([UITypes.Date, UITypes.DateTime].includes(column.uidt)) {
const now = dayjs(new Date());
let now = dayjs(new Date());
const dateFormatFromMeta = column?.meta?.date_format;
if (dateFormatFromMeta && isDateMonthFormat(dateFormatFromMeta)) {
// reset to 1st
now = dayjs(now).date(1);
if (val) val = dayjs(val).date(1);
}
// handle sub operation
switch (filter.comparison_sub_op) {
case 'today':

11
packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts

@ -1,5 +1,9 @@
import jsep from 'jsep';
import { jsepCurlyHook, UITypes } from 'nocodb-sdk';
import {
jsepCurlyHook,
UITypes,
validateDateWithUnknownFormat,
} from 'nocodb-sdk';
import mapFunctionName from '../mapFunctionName';
import genRollupSelectv2 from '../genRollupSelectv2';
import type Column from '~/models/Column';
@ -10,10 +14,7 @@ import type LookupColumn from '~/models/LookupColumn';
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2';
import NocoCache from '~/cache/NocoCache';
import { CacheGetType, CacheScope } from '~/utils/globals';
import {
convertDateFormatForConcat,
validateDateWithUnknownFormat,
} from '~/helpers/formulaFnHelper';
import { convertDateFormatForConcat } from '~/helpers/formulaFnHelper';
import FormulaColumn from '~/models/FormulaColumn';
// todo: switch function based on database

2
packages/nocodb/src/db/functionMappings/sqlite.ts

@ -1,9 +1,9 @@
import dayjs from 'dayjs';
import { convertToTargetFormat, getDateFormat } from 'nocodb-sdk';
import commonFns from './commonFns';
import type { MapFnArgs } from '../mapFunctionName';
import { convertUnits } from '~/helpers/convertUnits';
import { getWeekdayByText } from '~/helpers/formulaFnHelper';
import { convertToTargetFormat, getDateFormat } from '~/utils/dateTimeUtils';
const sqlite3 = {
...commonFns,

30
packages/nocodb/src/helpers/formulaFnHelper.ts

@ -1,12 +1,7 @@
import dayjs from 'dayjs';
import { UITypes } from 'nocodb-sdk';
import { convertDateFormat } from './convertDateFormat';
import Column from '~/models/Column';
// todo: tobe fixed
// import customParseFormat from 'dayjs/plugin/customParseFormat';
// extend(customParseFormat);
export function getWeekdayByText(v: string) {
return {
monday: 0,
@ -31,31 +26,6 @@ export function getWeekdayByIndex(idx: number): string {
}[idx || 0];
}
export function validateDateWithUnknownFormat(v: string) {
const dateFormats = [
'DD-MM-YYYY',
'MM-DD-YYYY',
'YYYY-MM-DD',
'DD/MM/YYYY',
'MM/DD/YYYY',
'YYYY/MM/DD',
'DD MM YYYY',
'MM DD YYYY',
'YYYY MM DD',
];
for (const format of dateFormats) {
if (dayjs(v, format, true).isValid() as any) {
return true;
}
for (const timeFormat of ['HH:mm', 'HH:mm:ss', 'HH:mm:ss.SSS']) {
if (dayjs(v, `${format} ${timeFormat}`, true).isValid() as any) {
return true;
}
}
}
return false;
}
export async function convertDateFormatForConcat(
o,
columnIdToUidt,

75
packages/nocodb/src/utils/dateTimeUtils.ts

@ -1,75 +0,0 @@
import dayjs from 'dayjs';
export const dateFormats = [
'DD-MM-YYYY',
'MM-DD-YYYY',
'YYYY-MM-DD',
'DD/MM/YYYY',
'MM/DD/YYYY',
'YYYY/MM/DD',
'DD MM YYYY',
'MM DD YYYY',
'YYYY MM DD',
];
export function validateDateFormat(v: string) {
return dateFormats.includes(v);
}
export function validateDateWithUnknownFormat(v: string) {
for (const format of dateFormats) {
if (dayjs(v, format, true).isValid() as any) {
return true;
}
for (const timeFormat of ['HH:mm', 'HH:mm:ss', 'HH:mm:ss.SSS']) {
if (dayjs(v, `${format} ${timeFormat}`, true).isValid() as any) {
return true;
}
}
}
return false;
}
export function getDateFormat(v: string) {
for (const format of dateFormats) {
if (dayjs(v, format, true).isValid()) {
return format;
}
}
return 'YYYY/MM/DD';
}
export function getDateTimeFormat(v: string) {
for (const format of dateFormats) {
for (const timeFormat of ['HH:mm', 'HH:mm:ss', 'HH:mm:ss.SSS']) {
const dateTimeFormat = `${format} ${timeFormat}`;
if (dayjs(v, dateTimeFormat, true).isValid() as any) {
return dateTimeFormat;
}
}
}
return 'YYYY/MM/DD';
}
export function parseStringDate(v: string, dateFormat: string) {
const dayjsObj = dayjs(v);
if (dayjsObj.isValid()) {
v = dayjsObj.format('YYYY-MM-DD');
} else {
v = dayjs(v, dateFormat).format('YYYY-MM-DD');
}
return v;
}
export function convertToTargetFormat(
v: string,
oldDataFormat,
newDateFormat: string,
) {
if (
!dateFormats.includes(oldDataFormat) ||
!dateFormats.includes(newDateFormat)
)
return v;
return dayjs(v, oldDataFormat).format(newDateFormat);
}

3
pnpm-lock.yaml

@ -908,6 +908,9 @@ importers:
axios:
specifier: ^1.6.2
version: 1.6.2
dayjs:
specifier: ^1.11.9
version: 1.11.9
jsep:
specifier: ^1.3.8
version: 1.3.8

10
tests/playwright/pages/Dashboard/Grid/Column/index.ts

@ -104,6 +104,11 @@ export class ColumnPageObject extends BasePage {
.click();
}
break;
case 'Date':
// Date Format
await this.get().locator('.nc-date-select').click();
await this.rootPage.locator('.ant-select-item').locator(`text="${dateFormat}"`).click();
break;
case 'DateTime':
// Date Format
await this.get().locator('.nc-date-select').click();
@ -310,6 +315,11 @@ export class ColumnPageObject extends BasePage {
await this.get().locator('.nc-time-select').click();
await this.rootPage.locator('.ant-select-item').locator(`text="${timeFormat}"`).click();
break;
case 'Date':
// Date Format
await this.get().locator('.nc-date-select').click();
await this.rootPage.locator('.ant-select-item').locator(`text="${dateFormat}"`).click();
break;
default:
break;
}

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

@ -29,8 +29,10 @@ export class DateTimeCellPageObject extends BasePage {
async selectDate({
// date formats in `YYYY-MM-DD`
date,
skipDate = false,
}: {
date: string;
skipDate?: boolean;
}) {
// title date format needs to be YYYY-MM-DD
const [year, month, day] = date.split('-');
@ -40,6 +42,11 @@ export class DateTimeCellPageObject extends BasePage {
await this.rootPage.locator('.ant-picker-year-btn:visible').click();
await this.rootPage.locator(`td[title="${year}"]`).click();
if (skipDate) {
await this.rootPage.locator(`td[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();

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

@ -57,6 +57,29 @@ const dateTimeData = [
},
];
const dateData = [
{
dateFormat: 'YYYY-MM-DD',
date: '2022-12-12',
output: '2022-12-12',
},
{
dateFormat: 'YYYY/MM/DD',
date: '2022-12-13',
output: '2022/12/13',
},
{
dateFormat: 'DD-MM-YYYY',
date: '2022-12-10',
output: '10-12-2022',
},
{
dateFormat: 'YYYY-MM',
date: '2022-12-26',
output: '2022-12',
},
];
test.describe('DateTime Column', () => {
if (enableQuickRun()) test.skip();
let dashboard: DashboardPage;
@ -117,3 +140,55 @@ test.describe('DateTime Column', () => {
}
});
});
test.describe('Date Column', () => {
// if (enableQuickRun()) test.skip();
let dashboard: DashboardPage;
let context: NcContext;
test.beforeEach(async ({ page }) => {
context = await setup({ page, isEmptyProject: true });
dashboard = new DashboardPage(page, context.base);
});
test.afterEach(async () => {
await unsetup(context);
});
test('Create Date Column', async () => {
await dashboard.treeView.createTable({ title: 'test_date', baseTitle: context.base.title });
// Create DateTime column
await dashboard.grid.column.create({
title: 'NC_DATE_0',
type: 'Date',
dateFormat: dateData[0].dateFormat,
});
for (let i = 0; i < dateData.length; i++) {
// Edit DateTime column
await dashboard.grid.column.openEdit({
title: 'NC_DATE_0',
type: 'Date',
dateFormat: dateData[i].dateFormat,
});
await dashboard.grid.column.save({ isUpdated: true });
await dashboard.grid.cell.dateTime.open({
index: 0,
columnHeader: 'NC_DATE_0',
});
await dashboard.grid.cell.dateTime.selectDate({
date: dateData[i].date,
skipDate: dateData[i].dateFormat === 'YYYY-MM',
});
await dashboard.grid.cell.verifyDateCell({
index: 0,
columnHeader: 'NC_DATE_0',
value: dateData[i].output,
});
}
});
});

2
tests/playwright/tests/db/features/keyboardShortcuts.spec.ts

@ -173,7 +173,7 @@ test.describe('Clipboard support', () => {
{ column_name: 'MultiSelect', uidt: UITypes.MultiSelect, dtxp: "'Option1','Option2'" },
{ column_name: 'Rating', uidt: UITypes.Rating },
{ column_name: 'Checkbox', uidt: UITypes.Checkbox },
{ column_name: 'Date', uidt: UITypes.Date },
{ column_name: 'Date', uidt: UITypes.Date, meta: { date_format : 'YYYY-MM-DD' }},
{ column_name: 'Attachment', uidt: UITypes.Attachment },
];

Loading…
Cancel
Save