Browse Source

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

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

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

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

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

@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { Tooltip as ATooltip, Empty } from 'ant-design-vue' import { Tooltip as ATooltip, Empty } from 'ant-design-vue'
import type { AuditType } from 'nocodb-sdk' 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() const { $api } = useNuxtApp()

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

@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { Tooltip as ATooltip, Empty } from 'ant-design-vue' import { Tooltip as ATooltip, Empty } from 'ant-design-vue'
import type { AuditType } from 'nocodb-sdk' 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 { interface Props {
sourceId: string sourceId: string

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

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

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

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

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

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

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

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { dateFormats, timeFormats, useVModel } from '#imports' import { dateFormats, timeFormats } from 'nocodb-sdk'
import { useVModel } from '#imports'
const props = defineProps<{ const props = defineProps<{
value: any 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 type { ListItem as AntListItem } from 'ant-design-vue'
import jsep from 'jsep' import jsep from 'jsep'
import type { ColumnType, FormulaType } from 'nocodb-sdk' import type { ColumnType, FormulaType } from 'nocodb-sdk'
import { UITypes, jsepCurlyHook, substituteColumnIdWithAliasInFormula } from 'nocodb-sdk' import { UITypes, jsepCurlyHook, substituteColumnIdWithAliasInFormula, validateDateWithUnknownFormat } from 'nocodb-sdk'
import { import {
MetaInj, MetaInj,
NcAutocompleteTree, NcAutocompleteTree,
@ -18,7 +18,6 @@ import {
useColumnCreateStoreOrThrow, useColumnCreateStoreOrThrow,
useDebounceFn, useDebounceFn,
useVModel, useVModel,
validateDateWithUnknownFormat,
} from '#imports' } from '#imports'
const props = defineProps<{ const props = defineProps<{

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

@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import type { AuditType } from 'nocodb-sdk' import type { AuditType } from 'nocodb-sdk'
import { timeAgo } from 'nocodb-sdk'
import { Icon } from '@iconify/vue' 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<{ const props = defineProps<{
loading: boolean loading: boolean

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

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

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

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

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

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

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

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HookLogType, HookType } from 'nocodb-sdk' 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 { interface Props {
hook: HookType hook: HookType

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

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

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

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

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

@ -3,14 +3,13 @@ import dayjs from 'dayjs'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { MaybeRef } from '@vueuse/core' import type { MaybeRef } from '@vueuse/core'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk' 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 { parse } from 'papaparse'
import type { Cell } from './cellRange' import type { Cell } from './cellRange'
import { CellRange } from './cellRange' import { CellRange } from './cellRange'
import convertCellData from './convertCellData' import convertCellData from './convertCellData'
import type { Nullable, Row } from '#imports' import type { Nullable, Row } from '#imports'
import { import {
dateFormats,
extractPkFromRow, extractPkFromRow,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
isDrawerOrModalExist, isDrawerOrModalExist,
@ -20,7 +19,6 @@ import {
message, message,
reactive, reactive,
ref, ref,
timeFormats,
unref, unref,
useBase, useBase,
useCopy, 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) { if (columnObj.uidt === UITypes.Time) {
// remove `"` // remove `"`
// e.g. "2023-05-12T08:03:53.000Z" -> 2023-05-12T08:03:53.000Z // 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 { parse } from 'papaparse'
import type { UploadFile } from 'ant-design-vue' import type { UploadFile } from 'ant-design-vue'
import { UITypes } from 'nocodb-sdk' import { UITypes, getDateFormat, validateDateWithUnknownFormat } from 'nocodb-sdk'
import { getDateFormat, validateDateWithUnknownFormat } from '../../utils/dateTimeUtils'
import { import {
extractMultiOrSingleSelectProps, extractMultiOrSingleSelectProps,
getCheckboxValue, getCheckboxValue,

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

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

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

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

3
packages/nocodb-sdk/package.json

@ -39,7 +39,8 @@
}, },
"dependencies": { "dependencies": {
"axios": "^1.6.2", "axios": "^1.6.2",
"jsep": "^1.3.8" "jsep": "^1.3.8",
"dayjs": "^1.11.9"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.13.1", "@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 { default as TemplateGenerator } from '~/lib/TemplateGenerator';
export * from '~/lib/passwordHelpers'; export * from '~/lib/passwordHelpers';
export * from '~/lib/mergeSwaggerSchema'; export * from '~/lib/mergeSwaggerSchema';
export * from '~/lib/dateTimeHelper';

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

@ -4711,6 +4711,14 @@ class BaseModelSqlv2 {
continue; 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; let keepLocalTime = true;
if (this.isSqlite) { if (this.isSqlite) {
@ -4765,7 +4773,10 @@ class BaseModelSqlv2 {
const dateTimeColumns = ( const dateTimeColumns = (
childTable ? childTable.columns : this.model.columns childTable ? childTable.columns : this.model.columns
).filter( ).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 (dateTimeColumns.length) {
if (Array.isArray(data)) { 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 dayjs from 'dayjs';
// import customParseFormat from 'dayjs/plugin/customParseFormat.js'; // import customParseFormat from 'dayjs/plugin/customParseFormat.js';
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2'; import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2';
@ -466,7 +471,13 @@ const parseConditionV2 = async (
: 'YYYY-MM-DD HH:mm:ssZ'; : 'YYYY-MM-DD HH:mm:ssZ';
if ([UITypes.Date, UITypes.DateTime].includes(column.uidt)) { 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 // handle sub operation
switch (filter.comparison_sub_op) { switch (filter.comparison_sub_op) {
case 'today': case 'today':

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

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

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

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

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

@ -1,12 +1,7 @@
import dayjs from 'dayjs';
import { UITypes } from 'nocodb-sdk'; import { UITypes } from 'nocodb-sdk';
import { convertDateFormat } from './convertDateFormat'; import { convertDateFormat } from './convertDateFormat';
import Column from '~/models/Column'; import Column from '~/models/Column';
// todo: tobe fixed
// import customParseFormat from 'dayjs/plugin/customParseFormat';
// extend(customParseFormat);
export function getWeekdayByText(v: string) { export function getWeekdayByText(v: string) {
return { return {
monday: 0, monday: 0,
@ -31,31 +26,6 @@ export function getWeekdayByIndex(idx: number): string {
}[idx || 0]; }[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( export async function convertDateFormatForConcat(
o, o,
columnIdToUidt, 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: axios:
specifier: ^1.6.2 specifier: ^1.6.2
version: 1.6.2 version: 1.6.2
dayjs:
specifier: ^1.11.9
version: 1.11.9
jsep: jsep:
specifier: ^1.3.8 specifier: ^1.3.8
version: 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(); .click();
} }
break; 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': case 'DateTime':
// Date Format // Date Format
await this.get().locator('.nc-date-select').click(); 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.get().locator('.nc-time-select').click();
await this.rootPage.locator('.ant-select-item').locator(`text="${timeFormat}"`).click(); await this.rootPage.locator('.ant-select-item').locator(`text="${timeFormat}"`).click();
break; 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: default:
break; break;
} }

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

@ -29,8 +29,10 @@ export class DateTimeCellPageObject extends BasePage {
async selectDate({ async selectDate({
// date formats in `YYYY-MM-DD` // date formats in `YYYY-MM-DD`
date, date,
skipDate = false,
}: { }: {
date: string; date: string;
skipDate?: boolean;
}) { }) {
// title date format needs to be YYYY-MM-DD // title date format needs to be YYYY-MM-DD
const [year, month, day] = date.split('-'); 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('.ant-picker-year-btn:visible').click();
await this.rootPage.locator(`td[title="${year}"]`).click(); await this.rootPage.locator(`td[title="${year}"]`).click();
if (skipDate) {
await this.rootPage.locator(`td[title="${year}-${month}"]`).click();
return;
}
// configure month // configure month
await this.rootPage.locator('.ant-picker-month-btn:visible').click(); await this.rootPage.locator('.ant-picker-month-btn:visible').click();
await this.rootPage.locator(`td[title="${year}-${month}"]`).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', () => { test.describe('DateTime Column', () => {
if (enableQuickRun()) test.skip(); if (enableQuickRun()) test.skip();
let dashboard: DashboardPage; 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: 'MultiSelect', uidt: UITypes.MultiSelect, dtxp: "'Option1','Option2'" },
{ column_name: 'Rating', uidt: UITypes.Rating }, { column_name: 'Rating', uidt: UITypes.Rating },
{ column_name: 'Checkbox', uidt: UITypes.Checkbox }, { 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 }, { column_name: 'Attachment', uidt: UITypes.Attachment },
]; ];

Loading…
Cancel
Save