diff --git a/packages/core/client/src/collection-manager/collectionPlugin.ts b/packages/core/client/src/collection-manager/collectionPlugin.ts index 8d71266aaa..1082961953 100644 --- a/packages/core/client/src/collection-manager/collectionPlugin.ts +++ b/packages/core/client/src/collection-manager/collectionPlugin.ts @@ -52,6 +52,8 @@ import { UUIDFieldInterface, NanoidFieldInterface, UnixTimestampFieldInterface, + DateFieldInterface, + DatetimeNoTzFieldInterface, } from './interfaces'; import { GeneralCollectionTemplate, @@ -173,6 +175,8 @@ export class CollectionPlugin extends Plugin { UUIDFieldInterface, NanoidFieldInterface, UnixTimestampFieldInterface, + DateFieldInterface, + DatetimeNoTzFieldInterface, ]); } diff --git a/packages/core/client/src/collection-manager/interfaces/createdAt.ts b/packages/core/client/src/collection-manager/interfaces/createdAt.ts index 97c0a6da24..c6c18713a8 100644 --- a/packages/core/client/src/collection-manager/interfaces/createdAt.ts +++ b/packages/core/client/src/collection-manager/interfaces/createdAt.ts @@ -28,7 +28,7 @@ export class CreatedAtFieldInterface extends CollectionFieldInterface { 'x-read-pretty': true, }, }; - availableTypes = ['date']; + availableTypes = []; properties = { ...defaultProps, ...dateTimeProps, diff --git a/packages/core/client/src/collection-manager/interfaces/dateOnly.ts b/packages/core/client/src/collection-manager/interfaces/dateOnly.ts new file mode 100644 index 0000000000..defeb80b16 --- /dev/null +++ b/packages/core/client/src/collection-manager/interfaces/dateOnly.ts @@ -0,0 +1,60 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; +import { dateTimeProps, defaultProps, operators } from './properties'; + +export class DateFieldInterface extends CollectionFieldInterface { + name = 'date'; + type = 'object'; + group = 'datetime'; + order = 3; + title = '{{t("DateOnly")}}'; + sortable = true; + default = { + type: 'dateOnly', + uiSchema: { + type: 'string', + 'x-component': 'DatePicker', + 'x-component-props': { + dateOnly: true, + }, + }, + }; + availableTypes = ['dateOnly']; + hasDefaultValue = true; + properties = { + ...defaultProps, + 'uiSchema.x-component-props.dateFormat': { + type: 'string', + title: '{{t("Date format")}}', + 'x-component': 'Radio.Group', + 'x-decorator': 'FormItem', + default: 'YYYY-MM-DD', + enum: [ + { + label: '{{t("Year/Month/Day")}}', + value: 'YYYY/MM/DD', + }, + { + label: '{{t("Year-Month-Day")}}', + value: 'YYYY-MM-DD', + }, + { + label: '{{t("Day/Month/Year")}}', + value: 'DD/MM/YYYY', + }, + ], + }, + }; + filterable = { + operators: operators.datetime, + }; + titleUsable = true; +} diff --git a/packages/core/client/src/collection-manager/interfaces/datetime.ts b/packages/core/client/src/collection-manager/interfaces/datetime.ts index 615d11e9ea..85d6b13c45 100644 --- a/packages/core/client/src/collection-manager/interfaces/datetime.ts +++ b/packages/core/client/src/collection-manager/interfaces/datetime.ts @@ -15,23 +15,39 @@ export class DatetimeFieldInterface extends CollectionFieldInterface { type = 'object'; group = 'datetime'; order = 1; - title = '{{t("Datetime")}}'; + title = '{{t("Datetime(with time zone)")}}'; sortable = true; default = { type: 'date', + defaultToCurrentTime: false, + onUpdateToCurrentTime: false, + timezone: true, uiSchema: { type: 'string', 'x-component': 'DatePicker', 'x-component-props': { showTime: false, + utc: true, }, }, }; - availableTypes = ['date', 'dateOnly', 'string']; + availableTypes = ['date', 'string', 'datetime', 'datetimeTz']; hasDefaultValue = true; properties = { ...defaultProps, ...dateTimeProps, + defaultToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Default value to current time")}}', + }, + onUpdateToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Automatically update timestamp on update")}}', + }, 'uiSchema.x-component-props.gmt': { type: 'boolean', title: '{{t("GMT")}}', diff --git a/packages/core/client/src/collection-manager/interfaces/datetimeNoTz.ts b/packages/core/client/src/collection-manager/interfaces/datetimeNoTz.ts new file mode 100644 index 0000000000..4cebfad159 --- /dev/null +++ b/packages/core/client/src/collection-manager/interfaces/datetimeNoTz.ts @@ -0,0 +1,65 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; +import { dateTimeProps, defaultProps, operators } from './properties'; + +export class DatetimeNoTzFieldInterface extends CollectionFieldInterface { + name = 'datetimeNoTz'; + type = 'object'; + group = 'datetime'; + order = 2; + title = '{{t("Datetime(without time zone)")}}'; + sortable = true; + default = { + type: 'datetimeNoTz', + defaultToCurrentTime: false, + onUpdateToCurrentTime: false, + timezone: false, + uiSchema: { + type: 'string', + 'x-component': 'DatePicker', + 'x-component-props': { + showTime: false, + utc: false, + }, + }, + }; + availableTypes = ['string', 'datetimeNoTz']; + hasDefaultValue = true; + properties = { + ...defaultProps, + ...dateTimeProps, + defaultToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Default value to current server time")}}', + }, + onUpdateToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Automatically update timestamp to the current server time on update")}}', + }, + 'uiSchema.x-component-props.gmt': { + type: 'boolean', + title: '{{t("GMT")}}', + 'x-hidden': true, + 'x-component': 'Checkbox', + 'x-content': '{{t("Use the same time zone (GMT) for all users")}}', + 'x-decorator': 'FormItem', + default: false, + }, + }; + filterable = { + operators: operators.datetime, + }; + titleUsable = true; +} diff --git a/packages/core/client/src/collection-manager/interfaces/index.ts b/packages/core/client/src/collection-manager/interfaces/index.ts index 6778d83413..e2a1dfd747 100644 --- a/packages/core/client/src/collection-manager/interfaces/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/index.ts @@ -46,3 +46,5 @@ export * from './sort'; export * from './uuid'; export * from './nanoid'; export * from './unixTimestamp'; +export * from './dateOnly'; +export * from './datetimeNoTz'; diff --git a/packages/core/client/src/collection-manager/interfaces/properties/index.ts b/packages/core/client/src/collection-manager/interfaces/properties/index.ts index 1ef36e4b61..edb01ef255 100644 --- a/packages/core/client/src/collection-manager/interfaces/properties/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/properties/index.ts @@ -253,10 +253,11 @@ export const dateTimeProps: { [key: string]: ISchema } = { 'x-content': '{{t("Show time")}}', 'x-reactions': [ `{{(field) => { - field.query('..[].timeFormat').take(f => { - f.display = field.value ? 'visible' : 'none'; - }); - }}}`, + field.query('..[].timeFormat').take(f => { + f.display = field.value ? 'visible' : 'none'; + f.value='HH:mm:ss' + }); + }}}`, ], }, 'uiSchema.x-component-props.timeFormat': { diff --git a/packages/core/client/src/collection-manager/interfaces/time.ts b/packages/core/client/src/collection-manager/interfaces/time.ts index dc47c41746..2432a10bcb 100644 --- a/packages/core/client/src/collection-manager/interfaces/time.ts +++ b/packages/core/client/src/collection-manager/interfaces/time.ts @@ -14,7 +14,7 @@ export class TimeFieldInterface extends CollectionFieldInterface { name = 'time'; type = 'object'; group = 'datetime'; - order = 2; + order = 4; title = '{{t("Time")}}'; sortable = true; default = { diff --git a/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx b/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx index 47b3ebc2cf..5b0de8c91a 100644 --- a/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx +++ b/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx @@ -8,31 +8,33 @@ */ import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; -import { dateTimeProps, defaultProps, operators } from './properties'; - +import { defaultProps, operators } from './properties'; export class UnixTimestampFieldInterface extends CollectionFieldInterface { name = 'unixTimestamp'; type = 'object'; group = 'datetime'; - order = 1; + order = 4; title = '{{t("Unix Timestamp")}}'; sortable = true; default = { - type: 'bigInt', + type: 'unixTimestamp', + accuracy: 'second', + timezone: true, + defaultToCurrentTime: false, + onUpdateToCurrentTime: false, uiSchema: { type: 'number', 'x-component': 'UnixTimestamp', 'x-component-props': { - accuracy: 'second', showTime: true, }, }, }; - availableTypes = ['integer', 'bigInt']; - hasDefaultValue = true; + availableTypes = ['unixTimestamp']; + hasDefaultValue = false; properties = { ...defaultProps, - 'uiSchema.x-component-props.accuracy': { + accuracy: { type: 'string', title: '{{t("Accuracy")}}', 'x-component': 'Radio.Group', @@ -43,9 +45,21 @@ export class UnixTimestampFieldInterface extends CollectionFieldInterface { { value: 'second', label: '{{t("Second")}}' }, ], }, + defaultToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Default value to current time")}}', + }, + onUpdateToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Automatically update timestamp on update")}}', + }, }; filterable = { - operators: operators.number, + operators: operators.datetime, }; titleUsable = true; } diff --git a/packages/core/client/src/collection-manager/interfaces/updatedAt.ts b/packages/core/client/src/collection-manager/interfaces/updatedAt.ts index ad969338e7..533c008d7f 100644 --- a/packages/core/client/src/collection-manager/interfaces/updatedAt.ts +++ b/packages/core/client/src/collection-manager/interfaces/updatedAt.ts @@ -28,7 +28,7 @@ export class UpdatedAtFieldInterface extends CollectionFieldInterface { 'x-read-pretty': true, }, }; - availableTypes = ['date']; + availableTypes = []; properties = { ...defaultProps, ...dateTimeProps, diff --git a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts index 16cb0c008c..a0c616c7e3 100644 --- a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts +++ b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts @@ -119,8 +119,8 @@ export abstract class CollectionFieldInterface { }, }, { - dependencies: ['primaryKey', 'unique', 'autoIncrement'], - when: '{{$deps[0]||$deps[1]||$deps[2]}}', + dependencies: ['primaryKey', 'unique', 'autoIncrement', 'defaultToCurrentTime'], + when: '{{$deps[0]||$deps[1]||$deps[2]||$deps[3]}}', fulfill: { state: { hidden: true, diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index 1771db3f99..6d09e9f9f3 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -283,7 +283,7 @@ "Checkbox group": "复选框", "China region": "中国行政区", "Date & Time": "日期 & 时间", - "Datetime": "日期", + "Datetime": "日期时间", "Relation": "关系类型", "Link to": "关联", "Link to description": "用于快速创建表关系,可兼容大多数普通场景。适合非开发人员使用。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。创建后,将同时在目标数据表中生成当前数据表的关联字段。", @@ -974,5 +974,12 @@ "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "在分页时跳过获取表记录总数,以加快加载速度,建议对有大量数据的数据表开启此选项", "The current user only has the UI configuration permission, but don't have view permission for collection \"{{name}}\"": "当前用户只有 UI 配置权限,但没有数据表 \"{{name}}\" 查看权限。", "Plugin dependency version mismatch": "插件依赖版本不一致", - "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "当前插件的依赖版本与应用的版本不一致,可能无法正常工作。您确定要继续激活插件吗?" + "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "当前插件的依赖版本与应用的版本不一致,可能无法正常工作。您确定要继续激活插件吗?", + "Default value to current time": "设置字段默认值为当前时间", + "Automatically update timestamp on update": "当记录更新时自动设置字段值为当前时间", + "Default value to current server time": "设置字段默认值为当前服务端时间", + "Automatically update timestamp to the current server time on update": "当记录更新时自动设置字段值为当前服务端时间", + "Datetime(with time zone)": "日期时间(含时区)", + "Datetime(without time zone)": "日期时间(不含时区)", + "DateOnly":"仅日期" } diff --git a/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts b/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts index a77fbe8183..5ef7d817f5 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts +++ b/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts @@ -102,7 +102,7 @@ describe('moment2str', () => { test('picker is year', () => { const m = dayjs('2023-06-21 10:10:00'); - const str = moment2str(m, { picker: 'year' }); + const str = moment2str(m, { picker: 'year', gmt: true }); expect(str).toBe('2023-01-01T00:00:00.000Z'); }); @@ -132,7 +132,7 @@ describe('moment2str', () => { test('picker is month', () => { const m = dayjs('2023-06-21 10:10:00'); - const str = moment2str(m, { picker: 'month' }); + const str = moment2str(m, { picker: 'month', gmt: true }); expect(str).toBe('2023-06-01T00:00:00.000Z'); }); diff --git a/packages/core/client/src/schema-component/antd/date-picker/util.ts b/packages/core/client/src/schema-component/antd/date-picker/util.ts index 59026b3efb..026778aec5 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/util.ts +++ b/packages/core/client/src/schema-component/antd/date-picker/util.ts @@ -78,17 +78,20 @@ export const mapDatePicker = function () { return (props: any) => { const format = getDefaultFormat(props) as any; const onChange = props.onChange; - return { ...props, format: format, value: str2moment(props.value, props), - onChange: (value: Dayjs | null) => { + onChange: (value: Dayjs | null, dateString) => { if (onChange) { if (!props.showTime && value) { value = value.startOf('day'); } - onChange(moment2str(value, props)); + if (props.dateOnly) { + onChange(dateString !== '' ? dateString : undefined); + } else { + onChange(moment2str(value, props)); + } } }, }; diff --git a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx index 36987f612a..2c5062344e 100644 --- a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx +++ b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx @@ -64,6 +64,7 @@ export const DynamicComponent = (props: Props) => { minWidth: 150, ...props.style, }, + utc: false, }), name: 'value', 'x-read-pretty': false, diff --git a/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx b/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx index 5d19a1e118..e83f9d00bc 100644 --- a/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx +++ b/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx @@ -8,57 +8,32 @@ */ import { connect, mapReadPretty } from '@formily/react'; -import React, { useMemo } from 'react'; +import React from 'react'; import { DatePicker } from '../date-picker'; -import dayjs from 'dayjs'; - -const toValue = (value: any, accuracy) => { - if (value) { - return timestampToDate(value, accuracy); - } - return null; -}; - -function timestampToDate(timestamp, accuracy = 'millisecond') { - if (accuracy === 'second') { - timestamp *= 1000; // 如果精确度是秒级,则将时间戳乘以1000转换为毫秒级 - } - return dayjs(timestamp); -} - -function getTimestamp(date, accuracy = 'millisecond') { - if (accuracy === 'second') { - return dayjs(date).unix(); - } else { - return dayjs(date).valueOf(); // 默认返回毫秒级时间戳 - } -} interface UnixTimestampProps { - value?: number; - accuracy?: 'millisecond' | 'second'; + value?: any; onChange?: (value: number) => void; } export const UnixTimestamp = connect( (props: UnixTimestampProps) => { - const { value, onChange, accuracy = 'second' } = props; - const v = useMemo(() => toValue(value, accuracy), [value, accuracy]); + const { value, onChange } = props; + return ( { if (onChange) { - onChange(getTimestamp(v, accuracy)); + onChange(v); } }} /> ); }, mapReadPretty((props) => { - const { value, accuracy = 'second' } = props; - const v = useMemo(() => toValue(value, accuracy), [value, accuracy]); - return ; + const { value } = props; + return ; }), ); diff --git a/packages/core/client/src/schema-component/antd/unix-timestamp/__tests__/UnixTimestamp.test.tsx b/packages/core/client/src/schema-component/antd/unix-timestamp/__tests__/UnixTimestamp.test.tsx index ba82c73147..c9c107f45c 100644 --- a/packages/core/client/src/schema-component/antd/unix-timestamp/__tests__/UnixTimestamp.test.tsx +++ b/packages/core/client/src/schema-component/antd/unix-timestamp/__tests__/UnixTimestamp.test.tsx @@ -13,11 +13,9 @@ import { UnixTimestamp } from '@nocobase/client'; describe('UnixTimestamp', () => { it('renders without errors', async () => { const { container } = await renderAppOptions({ - Component: UnixTimestamp, - props: { - accuracy: 'millisecond', - }, - value: 0, + Component: UnixTimestamp as any, + props: {}, + value: null, }); expect(container).toMatchInlineSnapshot(`
@@ -69,78 +67,10 @@ describe('UnixTimestamp', () => { `); }); - it('millisecond', async () => { - await renderAppOptions({ - Component: UnixTimestamp, - value: 1712819630000, - props: { - accuracy: 'millisecond', - }, - }); - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('2024-04-11'); - }); - }); - - it('second', async () => { - await renderAppOptions({ - Component: UnixTimestamp, - value: 1712819630, - props: { - accuracy: 'second', - }, - }); - - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('2024-04-11'); - }); - }); - - it('string', async () => { - await renderAppOptions({ - Component: UnixTimestamp, - value: '2024-04-11', - props: { - accuracy: 'millisecond', - }, - }); - - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('2024-04-11'); - }); - }); - - it('change', async () => { - const onChange = vitest.fn(); - await renderAppOptions({ - Component: UnixTimestamp, - value: '2024-04-11', - onChange, - props: { - accuracy: 'millisecond', - }, - }); - await userEvent.click(screen.getByRole('textbox')); - - await waitFor(() => { - expect(screen.queryByRole('table')).toBeInTheDocument(); - }); - - await userEvent.click(document.querySelector('td[title="2024-04-12"]')); - - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('2024-04-12'); - }); - expect(onChange).toBeCalledWith(1712880000000); - }); - it('read pretty', async () => { const { container } = await renderReadPrettyApp({ - Component: UnixTimestamp, + Component: UnixTimestamp as any, value: '2024-04-11', - props: { - accuracy: 'millisecond', - }, }); expect(screen.getByText('2024-04-11')).toBeInTheDocument(); diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index 7311de1cb4..fd0e73825d 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -463,6 +463,7 @@ export const useFilterFormItemInitializerFields = (options?: any) => { 'x-collection-field': `${name}.${field.name}`, 'x-component-props': { component: interfaceConfig?.filterable?.operators?.[0]?.schema?.['x-component'], + utc: false, }, }; if (isAssocField(field)) { @@ -571,6 +572,7 @@ const associationFieldToMenu = ( interface: field.interface, }, 'x-component': 'CollectionField', + 'x-component-props': { utc: false }, 'x-read-pretty': false, 'x-decorator': 'FormItem', 'x-collection-field': `${collectionName}.${schemaName}`, @@ -686,7 +688,7 @@ export const useFilterInheritsFormItemInitializerFields = (options?) => { 'x-component': 'CollectionField', 'x-decorator': 'FormItem', 'x-collection-field': `${name}.${field.name}`, - 'x-component-props': {}, + 'x-component-props': { utc: false }, 'x-read-pretty': field?.uiSchema?.['x-read-pretty'], }; return { diff --git a/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx b/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx index 1e5b4bcc2c..275c98f41b 100644 --- a/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx @@ -89,6 +89,7 @@ export const SchemaSettingsDateFormat = function DateFormatConfig(props: { field 'x-decorator': 'FormItem', 'x-component': 'Checkbox', 'x-content': '{{t("Show time")}}', + 'x-hidden': collectionField?.type === 'dateOnly', 'x-reactions': [ `{{(field) => { field.query('.timeFormat').take(f => { @@ -142,9 +143,10 @@ export const SchemaSettingsDateFormat = function DateFormatConfig(props: { field const schema = { ['x-uid']: fieldSchema['x-uid'], }; - schema['x-component-props'] = fieldSchema['x-component-props'] || {}; + console.log(field.componentProps); + schema['x-component-props'] = field.componentProps || {}; fieldSchema['x-component-props'] = { - ...(fieldSchema['x-component-props'] || {}), + ...(field.componentProps || {}), ...data, }; schema['x-component-props'] = fieldSchema['x-component-props']; diff --git a/packages/core/database/src/__tests__/fields/date-only.test.ts b/packages/core/database/src/__tests__/fields/date-only.test.ts new file mode 100644 index 0000000000..1b6d9a60f7 --- /dev/null +++ b/packages/core/database/src/__tests__/fields/date-only.test.ts @@ -0,0 +1,42 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Database, mockDatabase } from '@nocobase/database'; + +describe('date only', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase({ + timezone: '+08:00', + }); + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should set date field with dateOnly', async () => { + db.collection({ + name: 'tests', + fields: [{ name: 'date1', type: 'dateOnly' }], + }); + + await db.sync(); + + const item = await db.getRepository('tests').create({ + values: { + date1: '2023-03-24', + }, + }); + + expect(item.get('date1')).toBe('2023-03-24'); + }); +}); diff --git a/packages/core/database/src/__tests__/fields/date.test.ts b/packages/core/database/src/__tests__/fields/date.test.ts deleted file mode 100644 index e1a2ff189e..0000000000 --- a/packages/core/database/src/__tests__/fields/date.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -import { mockDatabase } from '../'; -import { Database } from '../../database'; -import { Repository } from '../../repository'; - -describe('date-field', () => { - let db: Database; - let repository: Repository; - - beforeEach(async () => { - db = mockDatabase(); - await db.clean({ drop: true }); - db.collection({ - name: 'tests', - fields: [{ name: 'date1', type: 'date' }], - }); - await db.sync(); - repository = db.getRepository('tests'); - }); - - afterEach(async () => { - await db.close(); - }); - - const createExpectToBe = async (key, actual, expected) => { - const instance = await repository.create({ - values: { - [key]: actual, - }, - }); - return expect(instance.get(key).toISOString()).toEqual(expected); - }; - - test('create', async () => { - // sqlite 时区不能自定义,只有 +00:00,postgres 和 mysql 可以自定义 DB_TIMEZONE - await createExpectToBe('date1', '2023-03-24', '2023-03-24T00:00:00.000Z'); - await createExpectToBe('date1', '2023-03-24T16:00:00.000Z', '2023-03-24T16:00:00.000Z'); - }); - - // dateXX 相关 Operator 都是去 time 比较的 - describe('dateOn', () => { - test('dateOn operator', async () => { - console.log('timezone', db.options.timezone); - // 默认的情况,时区为 db.options.timezone - await repository.find({ - filter: { - date1: { - // 由 db.options.timezone 来处理日期转换,假设是 +08:00 的时区 - // 2023-03-24表示的范围:2023-03-23T16:00:00 ~ 2023-03-24T16:00:00 - $dateOn: '2023-03-24', - }, - }, - }); - - await repository.find({ - filter: { - date1: { - // +06:00 时区 2023-03-24 的范围:2023-03-23T18:00:00 ~ 2023-03-24T18:00:00 - $dateOn: '2023-03-24+06:00', - }, - }, - }); - - await repository.find({ - filter: { - date1: { - // 2023-03-23T20:00:00+08:00 在 +08:00 时区的时间是:2023-03-24 04:00:00 - // 也就是 +08:00 时区 2023-03-24 这一天的范围:2023-03-23T16:00:00 ~ 2023-03-24T16:00:00 - $dateOn: '2023-03-23T20:00:00+08:00', - }, - }, - }); - }); - }); -}); diff --git a/packages/core/database/src/__tests__/fields/datetime-no-tz.test.ts b/packages/core/database/src/__tests__/fields/datetime-no-tz.test.ts new file mode 100644 index 0000000000..e78ada5678 --- /dev/null +++ b/packages/core/database/src/__tests__/fields/datetime-no-tz.test.ts @@ -0,0 +1,145 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Database, mockDatabase } from '@nocobase/database'; +import { sleep } from '@nocobase/test'; + +describe('datetime no tz field', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase({ + timezone: '+01:00', + }); + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should not get timezone part', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'datetimeNoTz' }], + }); + + await db.sync(); + + await db.getRepository('tests').create({ + values: { + date1: '2023-03-24 12:00:00', + }, + }); + + const item = await db.getRepository('tests').findOne(); + expect(item.toJSON()['date1']).toBe('2023-03-24 12:00:00'); + }); + + it('should save datetime with timezone to no tz field', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'datetimeNoTz' }], + }); + + await db.sync(); + + await db.getRepository('tests').create({ + values: { + date1: '2023-03-24T12:00:00.892Z', + }, + }); + + const item = await db.getRepository('tests').findOne(); + expect(item.get('date1')).toBe('2023-03-24 13:00:00'); + }); + + it('should set datetime no tz field', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'datetimeNoTz' }], + }); + + await db.sync(); + + const item = await db.getRepository('tests').create({ + values: { + date1: '2023-03-24 12:00:00', + }, + }); + + expect(item.get('date1')).toBe('2023-03-24 12:00:00'); + }); + + it('should set default to current time', async () => { + const c1 = db.collection({ + name: 'test11', + fields: [ + { + name: 'date1', + type: 'datetimeNoTz', + defaultToCurrentTime: true, + }, + ], + }); + + await db.sync(); + + const instance = await c1.repository.create({}); + const date1 = instance.get('date1'); + expect(date1).toBeTruthy(); + }); + + it('should set to current time when update', async () => { + const c1 = db.collection({ + name: 'test11', + fields: [ + { + name: 'date1', + type: 'datetimeNoTz', + onUpdateToCurrentTime: true, + }, + { + name: 'title', + type: 'string', + }, + ], + }); + + await db.sync(); + + const instance = await c1.repository.create({ + values: { + title: 'test', + }, + }); + + const date1Val = instance.get('date1'); + expect(date1Val).toBeTruthy(); + + await sleep(1000); + + await c1.repository.update({ + values: { + title: 'test2', + }, + filter: { + id: instance.get('id'), + }, + }); + + await instance.reload(); + + const date1Val2 = instance.get('date1'); + expect(date1Val2).toBeTruthy(); + }); +}); diff --git a/packages/core/database/src/__tests__/fields/datetime-tz.test.ts b/packages/core/database/src/__tests__/fields/datetime-tz.test.ts new file mode 100644 index 0000000000..7e6b7ea57b --- /dev/null +++ b/packages/core/database/src/__tests__/fields/datetime-tz.test.ts @@ -0,0 +1,250 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { mockDatabase } from '../'; +import { Database } from '../../database'; +import { Repository } from '../../repository'; + +describe('timezone', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase({ + timezone: '+08:00', + }); + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should save with timezone value', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'datetimeTz' }], + }); + + await db.sync(); + + const repository = db.getRepository('tests'); + + const instance = await repository.create({ values: { date1: '2023-03-23T12:00:00.000Z' } }); + const date1 = instance.get('date1'); + expect(date1.toISOString()).toEqual('2023-03-23T12:00:00.000Z'); + }); + + it('should create field with default value', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'datetimeTz', defaultValue: '2023-03-23T18:00:00.000Z' }], + }); + + let err; + try { + await db.sync(); + } catch (e) { + err = e; + } + + expect(err).toBeUndefined(); + + const repository = db.getRepository('tests'); + + const instance = await repository.create({}); + const date1 = instance.get('date1'); + + expect(date1.toISOString()).toEqual('2023-03-23T18:00:00.000Z'); + }); + + describe('timezone', () => { + test('custom', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'date', timezone: '+06:00' }], + }); + + await db.sync(); + const repository = db.getRepository('tests'); + const instance = await repository.create({ values: { date1: '2023-03-24 00:00:00' } }); + const date1 = instance.get('date1'); + expect(date1.toISOString()).toEqual('2023-03-23T18:00:00.000Z'); + }); + + test('client', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'date', timezone: 'client' }], + }); + + await db.sync(); + const repository = db.getRepository('tests'); + const instance = await repository.create({ + values: { date1: '2023-03-24 01:00:00' }, + context: { + timezone: '+01:00', + }, + }); + const date1 = instance.get('date1'); + expect(date1.toISOString()).toEqual('2023-03-24T00:00:00.000Z'); + }); + + test('server', async () => { + db.collection({ + name: 'tests', + fields: [{ name: 'date1', type: 'date', timezone: 'server' }], + }); + + await db.sync(); + const repository = db.getRepository('tests'); + const instance = await repository.create({ values: { date1: '2023-03-24 08:00:00' } }); + const date1 = instance.get('date1'); + expect(date1.toISOString()).toEqual('2023-03-24T00:00:00.000Z'); + }); + }); +}); + +describe('date-field', () => { + let db: Database; + let repository: Repository; + + beforeEach(async () => { + db = mockDatabase(); + await db.clean({ drop: true }); + db.collection({ + name: 'tests', + fields: [{ name: 'date1', type: 'date' }], + }); + await db.sync(); + repository = db.getRepository('tests'); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should set default to current time', async () => { + const c1 = db.collection({ + name: 'test11', + fields: [ + { + name: 'date1', + type: 'date', + defaultToCurrentTime: true, + }, + ], + }); + + await db.sync(); + + const instance = await c1.repository.create({}); + const date1 = instance.get('date1'); + expect(date1).toBeTruthy(); + }); + + it('should set to current time when update', async () => { + const c1 = db.collection({ + name: 'test11', + fields: [ + { + name: 'date1', + type: 'date', + onUpdateToCurrentTime: true, + }, + { + name: 'title', + type: 'string', + }, + ], + }); + + await db.sync(); + + const instance = await c1.repository.create({ + values: { + title: 'test', + }, + }); + + const date1Val = instance.get('date1'); + expect(date1Val).toBeDefined(); + + console.log('update'); + await c1.repository.update({ + values: { + title: 'test2', + }, + filter: { + id: instance.get('id'), + }, + }); + + await instance.reload(); + + const date1Val2 = instance.get('date1'); + expect(date1Val2).toBeDefined(); + + expect(date1Val2.getTime()).toBeGreaterThan(date1Val.getTime()); + }); + + test('create', async () => { + const createExpectToBe = async (key, actual, expected) => { + const instance = await repository.create({ + values: { + [key]: actual, + }, + }); + return expect(instance.get(key).toISOString()).toEqual(expected); + }; + + // sqlite 时区不能自定义,只有 +00:00,postgres 和 mysql 可以自定义 DB_TIMEZONE + await createExpectToBe('date1', '2023-03-24', '2023-03-24T00:00:00.000Z'); + await createExpectToBe('date1', '2023-03-24T16:00:00.000Z', '2023-03-24T16:00:00.000Z'); + }); + + // dateXX 相关 Operator 都是去 time 比较的 + describe('dateOn', () => { + test('dateOn operator', async () => { + console.log('timezone', db.options.timezone); + // 默认的情况,时区为 db.options.timezone + await repository.find({ + filter: { + date1: { + // 由 db.options.timezone 来处理日期转换,假设是 +08:00 的时区 + // 2023-03-24表示的范围:2023-03-23T16:00:00 ~ 2023-03-24T16:00:00 + $dateOn: '2023-03-24', + }, + }, + }); + + await repository.find({ + filter: { + date1: { + // +06:00 时区 2023-03-24 的范围:2023-03-23T18:00:00 ~ 2023-03-24T18:00:00 + $dateOn: '2023-03-24+06:00', + }, + }, + }); + + await repository.find({ + filter: { + date1: { + // 2023-03-23T20:00:00+08:00 在 +08:00 时区的时间是:2023-03-24 04:00:00 + // 也就是 +08:00 时区 2023-03-24 这一天的范围:2023-03-23T16:00:00 ~ 2023-03-24T16:00:00 + $dateOn: '2023-03-23T20:00:00+08:00', + }, + }, + }); + }); + }); +}); diff --git a/packages/core/database/src/__tests__/fields/unix-timestamp-field.tests.ts b/packages/core/database/src/__tests__/fields/unix-timestamp-field.tests.ts new file mode 100644 index 0000000000..d590653811 --- /dev/null +++ b/packages/core/database/src/__tests__/fields/unix-timestamp-field.tests.ts @@ -0,0 +1,86 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Database, mockDatabase } from '@nocobase/database'; +import moment from 'moment'; + +describe('unix timestamp field', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase(); + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should set default to current time', async () => { + const c1 = db.collection({ + name: 'test11', + fields: [ + { + name: 'date1', + type: 'unixTimestamp', + defaultToCurrentTime: true, + }, + ], + }); + + await db.sync(); + + const instance = await c1.repository.create({}); + const date1 = instance.get('date1'); + expect(date1).toBeDefined(); + + console.log(instance.toJSON()); + }); + + it('should set date value', async () => { + const c1 = db.collection({ + name: 'test12', + fields: [ + { + name: 'date1', + type: 'unixTimestamp', + }, + ], + }); + + await db.sync(); + + await c1.repository.create({ + values: { + date1: '2021-01-01T00:00:00Z', + }, + }); + + const item = await c1.repository.findOne(); + const val = item.get('date1'); + const date = moment(val).utc().format('YYYY-MM-DD HH:mm:ss'); + expect(date).toBe('2021-01-01 00:00:00'); + }); + + describe('timezone', () => { + test('custom', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'unixTimestamp', timezone: '+06:00' }], + }); + + await db.sync(); + const repository = db.getRepository('tests'); + const instance = await repository.create({ values: { date1: '2023-03-24 00:00:00' } }); + const date1 = instance.get('date1'); + expect(date1.toISOString()).toEqual('2023-03-23T18:00:00.000Z'); + }); + }); +}); diff --git a/packages/core/database/src/__tests__/operator/date/date-only.test.ts b/packages/core/database/src/__tests__/operator/date/date-only.test.ts new file mode 100644 index 0000000000..aa1690b148 --- /dev/null +++ b/packages/core/database/src/__tests__/operator/date/date-only.test.ts @@ -0,0 +1,164 @@ +import Database, { mockDatabase, Repository } from '../../../index'; + +describe('dateOnly operator', () => { + let db: Database; + + let repository: Repository; + + afterEach(async () => { + await db.close(); + }); + + beforeEach(async () => { + db = mockDatabase({ + timezone: '+08:00', + }); + + await db.clean({ drop: true }); + const Test = db.collection({ + name: 'tests', + fields: [ + { + name: 'date1', + type: 'dateOnly', + }, + { + type: 'string', + name: 'name', + }, + ], + }); + repository = Test.repository; + await db.sync(); + }); + + test('$dateOn', async () => { + await repository.create({ + values: [ + { + date1: '2023-01-01', + name: 'u0', + }, + { + date1: '2023-01-01', + name: 'u1', + }, + { + date1: '2022-12-31', + name: 'u2', + }, + { + date1: '2022-12-31', + name: 'u3', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2022-12-31', + }, + }); + + expect(count).toBe(2); + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2023-01-01', + }, + }); + + expect(count).toBe(2); + }); + + test('$dateBefore', async () => { + await repository.create({ + values: [ + { + date1: '2023-01-01', + name: 'u0', + }, + { + date1: '2023-01-01', + name: 'u1', + }, + { + date1: '2022-12-31', + name: 'u2', + }, + { + date1: '2022-12-31', + name: 'u3', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateBefore': '2023-01-01', + }, + }); + expect(count).toBe(2); + + count = await repository.count({ + filter: { + 'date1.$dateBefore': '2022-12-31', + }, + }); + + expect(count).toBe(0); + }); + + test('dateBetween', async () => { + await repository.create({ + values: [ + { + date1: '2023-01-01', + name: 'u0', + }, + { + date1: '2023-01-01', + name: 'u1', + }, + { + date1: '2022-12-31', + name: 'u2', + }, + { + date1: '2022-12-31', + name: 'u3', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateBetween': ['2022-12-31', '2023-01-01'], + }, + }); + + expect(count).toBe(4); + + count = await repository.count({ + filter: { + 'date1.$dateBetween': ['2022-12-31', '2023-01-01'], + }, + }); + + expect(count).toBe(4); + + count = await repository.count({ + filter: { + 'date1.$dateBetween': ['2022-12-31', '2022-12-31'], + }, + }); + + expect(count).toBe(2); + }); +}); diff --git a/packages/core/database/src/__tests__/operator/date/datetime-no-tz.test.ts b/packages/core/database/src/__tests__/operator/date/datetime-no-tz.test.ts new file mode 100644 index 0000000000..4d77bb51f6 --- /dev/null +++ b/packages/core/database/src/__tests__/operator/date/datetime-no-tz.test.ts @@ -0,0 +1,201 @@ +import Database, { mockDatabase, Repository } from '../../../index'; + +describe('datetimeNoTz date operator test', () => { + let db: Database; + + let repository: Repository; + + afterEach(async () => { + await db.close(); + }); + + beforeEach(async () => { + db = mockDatabase({ + timezone: '+00:00', + }); + + await db.clean({ drop: true }); + const Test = db.collection({ + name: 'tests', + fields: [ + { + name: 'date1', + type: 'datetimeNoTz', + }, + { + type: 'string', + name: 'name', + }, + ], + }); + repository = Test.repository; + await db.sync(); + }); + + test('$dateOn', async () => { + await repository.create({ + values: [ + { + date1: '2023-01-01 00:00:00', + name: 'u0', + }, + { + date1: '2023-01-01 00:00:00', + name: 'u1', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u2', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u3', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2023-01-01', + }, + }); + + expect(count).toBe(2); + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2022-12-31', + }, + }); + + expect(count).toBe(2); + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2022-12-31 16:00:00', + }, + }); + + expect(count).toBe(2); + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2022-12-31 16:00:01', + }, + }); + + expect(count).toBe(0); + }); + + test('$dateBefore', async () => { + await repository.create({ + values: [ + { + date1: '2023-01-01 00:00:00', + name: 'u0', + }, + { + date1: '2023-01-01 00:00:00', + name: 'u1', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u2', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u3', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateBefore': '2023-01-01', + }, + }); + expect(count).toBe(2); + + count = await repository.count({ + filter: { + 'date1.$dateBefore': '2022-12-31', + }, + }); + + expect(count).toBe(0); + }); + + test('$dateBefore2', async () => { + await repository.create({ + values: [ + { + date1: '2024-09-08 15:33:54', + name: 'u0', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateBefore': '2024-09-08 15:33:55', + }, + }); + + expect(count).toBe(1); + }); + + test('dateBetween', async () => { + await repository.create({ + values: [ + { + date1: '2023-01-01 00:00:00', + name: 'u0', + }, + { + date1: '2023-01-01 00:00:00', + name: 'u1', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u2', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u3', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateBetween': ['2022-12-31', '2023-01-01'], + }, + }); + + expect(count).toBe(4); + + count = await repository.count({ + filter: { + 'date1.$dateBetween': ['2022-12-31 16:00:00', '2023-01-01 00:00:00'], + }, + }); + + expect(count).toBe(4); + + count = await repository.count({ + filter: { + 'date1.$dateBetween': ['2022-12-31 11:00:00', '2022-12-31 17:00:00'], + }, + }); + + expect(count).toBe(2); + }); +}); diff --git a/packages/core/database/src/__tests__/operator/date-operator.test.ts b/packages/core/database/src/__tests__/operator/date/datetime-tz.test.ts similarity index 98% rename from packages/core/database/src/__tests__/operator/date-operator.test.ts rename to packages/core/database/src/__tests__/operator/date/datetime-tz.test.ts index f2726bd438..b722f2437e 100644 --- a/packages/core/database/src/__tests__/operator/date-operator.test.ts +++ b/packages/core/database/src/__tests__/operator/date/datetime-tz.test.ts @@ -7,9 +7,9 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import Database from '../../database'; -import { Repository } from '../../repository'; -import { mockDatabase } from '../index'; +import Database from '../../../database'; +import { Repository } from '../../../repository'; +import { mockDatabase } from '../../index'; describe('date operator test', () => { let db: Database; diff --git a/packages/core/database/src/__tests__/operator/date/unix-timestamp.test.ts b/packages/core/database/src/__tests__/operator/date/unix-timestamp.test.ts new file mode 100644 index 0000000000..16bcde85ea --- /dev/null +++ b/packages/core/database/src/__tests__/operator/date/unix-timestamp.test.ts @@ -0,0 +1,75 @@ +import Database, { mockDatabase, Repository } from '../../../index'; + +describe('unix timestamp date operator test', () => { + let db: Database; + + let repository: Repository; + + afterEach(async () => { + await db.close(); + }); + + beforeEach(async () => { + db = mockDatabase({ + timezone: '+00:00', + }); + + await db.clean({ drop: true }); + const Test = db.collection({ + name: 'tests', + fields: [ + { + name: 'date1', + type: 'unixTimestamp', + }, + { + type: 'string', + name: 'name', + }, + ], + }); + repository = Test.repository; + await db.sync(); + }); + + test('$dateOn', async () => { + await repository.create({ + values: [ + { + date1: '2023-01-01 00:00:00', + name: 'u0', + }, + { + date1: '2023-01-01 00:00:00', + name: 'u1', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u2', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u3', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2023-01-01', + }, + }); + + expect(count).toBe(2); + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2022-12-31', + }, + }); + + expect(count).toBe(2); + }); +}); diff --git a/packages/core/database/src/__tests__/view/view-inference.test.ts b/packages/core/database/src/__tests__/view/view-inference.test.ts index 5636dd79f5..600d3be63a 100644 --- a/packages/core/database/src/__tests__/view/view-inference.test.ts +++ b/packages/core/database/src/__tests__/view/view-inference.test.ts @@ -127,7 +127,11 @@ describe('view inference', function () { }); const createdAt = UserCollection.model.rawAttributes['createdAt'].field; - expect(inferredFields[createdAt]['type']).toBe('date'); + if (db.isMySQLCompatibleDialect()) { + expect(inferredFields[createdAt]['type']).toBe('datetimeNoTz'); + } else { + expect(inferredFields[createdAt]['type']).toBe('datetimeTz'); + } if (db.options.dialect == 'sqlite') { expect(inferredFields['name']).toMatchObject({ diff --git a/packages/core/database/src/database.ts b/packages/core/database/src/database.ts index a3b813b96b..993f1be825 100644 --- a/packages/core/database/src/database.ts +++ b/packages/core/database/src/database.ts @@ -34,7 +34,6 @@ import { import { SequelizeStorage, Umzug } from 'umzug'; import { Collection, CollectionOptions, RepositoryType } from './collection'; import { CollectionFactory } from './collection-factory'; -import { CollectionGroupManager } from './collection-group-manager'; import { ImporterReader, ImportFileExtension } from './collection-importer'; import DatabaseUtils from './database-utils'; import ReferencesMap from './features/references-map'; @@ -42,7 +41,6 @@ import { referentialIntegrityCheck } from './features/referential-integrity-chec import { ArrayFieldRepository } from './field-repository/array-field-repository'; import * as FieldTypes from './fields'; import { Field, FieldContext, RelationField } from './fields'; -import { checkDatabaseVersion } from './helpers'; import { InheritedCollection } from './inherited-collection'; import InheritanceMap from './inherited-map'; import { InterfaceManager } from './interface-manager'; @@ -221,6 +219,9 @@ export class Database extends EventEmitter implements AsyncEmitter { } } + // @ts-ignore + opts.rawTimezone = opts.timezone; + if (options.dialect === 'sqlite') { delete opts.timezone; } else if (!opts.timezone) { @@ -851,7 +852,8 @@ export class Database extends EventEmitter implements AsyncEmitter { * @internal */ async checkVersion() { - return await checkDatabaseVersion(this); + return true; + // return await checkDatabaseVersion(this); } /** diff --git a/packages/core/database/src/fields/date-field.ts b/packages/core/database/src/fields/date-field.ts index f40b27de3e..0fd68e2f2c 100644 --- a/packages/core/database/src/fields/date-field.ts +++ b/packages/core/database/src/fields/date-field.ts @@ -9,9 +9,16 @@ import { DataTypes } from 'sequelize'; import { BaseColumnFieldOptions, Field } from './field'; +import moment from 'moment'; + +const datetimeRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/; + +function isValidDatetime(str) { + return datetimeRegex.test(str); +} export class DateField extends Field { - get dataType() { + get dataType(): any { return DataTypes.DATE(3); } @@ -33,6 +40,90 @@ export class DateField extends Field { return props.gmt; } + init() { + const { name, defaultToCurrentTime, onUpdateToCurrentTime, timezone } = this.options; + + this.resolveTimeZone = (context) => { + // @ts-ignore + const serverTimeZone = this.database.options.rawTimezone; + if (timezone === 'server') { + return serverTimeZone; + } + + if (timezone === 'client') { + return context?.timezone || serverTimeZone; + } + + if (timezone) { + return timezone; + } + + return serverTimeZone; + }; + + this.beforeSave = async (instance, options) => { + const value = instance.get(name); + + if (!value && instance.isNewRecord && defaultToCurrentTime) { + instance.set(name, new Date()); + return; + } + + if (onUpdateToCurrentTime) { + instance.set(name, new Date()); + return; + } + }; + + if (this.options.defaultValue && this.database.isMySQLCompatibleDialect()) { + if (typeof this.options.defaultValue === 'string' && isIso8601(this.options.defaultValue)) { + this.options.defaultValue = moment(this.options.defaultValue) + .utcOffset(this.resolveTimeZone()) + .format('YYYY-MM-DD HH:mm:ss'); + } + } + } + + setter(value, options) { + if (value === null) { + return value; + } + if (value instanceof Date) { + return value; + } + + if (typeof value === 'string' && isValidDatetime(value)) { + const dateTimezone = this.resolveTimeZone(options?.context); + const dateString = `${value} ${dateTimezone}`; + return new Date(dateString); + } + + return value; + } + + additionalSequelizeOptions() { + const { name } = this.options; + // @ts-ignore + const serverTimeZone = this.database.options.rawTimezone; + + return { + get() { + const value = this.getDataValue(name); + + if (value === null || value === undefined) { + return value; + } + + if (typeof value === 'string' && isValidDatetime(value)) { + const dateString = `${value} ${serverTimeZone}`; + return new Date(dateString); + } + + return new Date(value); + }, + }; + } + bind() { super.bind(); @@ -51,9 +142,21 @@ export class DateField extends Field { // @ts-ignore model.refreshAttributes(); } + + this.on('beforeSave', this.beforeSave); + } + + unbind() { + super.unbind(); + this.off('beforeSave', this.beforeSave); } } export interface DateFieldOptions extends BaseColumnFieldOptions { type: 'date'; } + +function isIso8601(str) { + const iso8601StrictRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + return iso8601StrictRegex.test(str); +} diff --git a/packages/core/database/src/fields/date-only-field.ts b/packages/core/database/src/fields/date-only-field.ts new file mode 100644 index 0000000000..5fce9d1b74 --- /dev/null +++ b/packages/core/database/src/fields/date-only-field.ts @@ -0,0 +1,21 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { BaseColumnFieldOptions, Field } from './field'; +import { DataTypes } from 'sequelize'; + +export class DateOnlyField extends Field { + get dataType(): any { + return DataTypes.DATEONLY; + } +} + +export interface DateOnlyFieldOptions extends BaseColumnFieldOptions { + type: 'dateOnly'; +} diff --git a/packages/core/database/src/fields/datetime-field.ts b/packages/core/database/src/fields/datetime-field.ts new file mode 100644 index 0000000000..12eeaff4e6 --- /dev/null +++ b/packages/core/database/src/fields/datetime-field.ts @@ -0,0 +1,17 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { DateField } from './date-field'; +import { BaseColumnFieldOptions } from './field'; + +export class DatetimeField extends DateField {} + +export interface DatetimeFieldOptions extends BaseColumnFieldOptions { + type: 'datetime'; +} diff --git a/packages/core/database/src/fields/datetime-no-tz-field.ts b/packages/core/database/src/fields/datetime-no-tz-field.ts new file mode 100644 index 0000000000..695ace4813 --- /dev/null +++ b/packages/core/database/src/fields/datetime-no-tz-field.ts @@ -0,0 +1,112 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { BaseColumnFieldOptions, Field } from './field'; +import { DataTypes } from 'sequelize'; +import moment from 'moment'; + +class DatetimeNoTzTypeMySQL extends DataTypes.ABSTRACT { + key = 'DATETIME'; +} + +class DatetimeNoTzTypePostgres extends DataTypes.ABSTRACT { + key = 'TIMESTAMP'; +} + +export class DatetimeNoTzField extends Field { + get dataType() { + if (this.database.inDialect('postgres')) { + return DatetimeNoTzTypePostgres; + } + + if (this.database.isMySQLCompatibleDialect()) { + return DatetimeNoTzTypeMySQL; + } + + return DataTypes.STRING; + } + + init() { + const { name, defaultToCurrentTime, onUpdateToCurrentTime } = this.options; + + this.beforeSave = async (instance, options) => { + const value = instance.get(name); + + if (!value && instance.isNewRecord && defaultToCurrentTime) { + instance.set(name, new Date()); + return; + } + + if (onUpdateToCurrentTime) { + instance.set(name, new Date()); + return; + } + }; + } + + additionalSequelizeOptions(): {} { + const { name } = this.options; + + // @ts-ignore + const timezone = this.database.options.rawTimezone || '+00:00'; + + const isPg = this.database.inDialect('postgres'); + + return { + get() { + const val = this.getDataValue(name); + + if (val instanceof Date) { + if (isPg) { + return moment(val).format('YYYY-MM-DD HH:mm:ss'); + } + // format to YYYY-MM-DD HH:mm:ss + const momentVal = moment(val).utcOffset(timezone); + return momentVal.format('YYYY-MM-DD HH:mm:ss'); + } + + return val; + }, + + set(val) { + if (typeof val === 'string' && isIso8601(val)) { + const momentVal = moment(val).utcOffset(timezone); + val = momentVal.format('YYYY-MM-DD HH:mm:ss'); + } + + if (val && val instanceof Date) { + // format to YYYY-MM-DD HH:mm:ss + const momentVal = moment(val).utcOffset(timezone); + val = momentVal.format('YYYY-MM-DD HH:mm:ss'); + } + + return this.setDataValue(name, val); + }, + }; + } + + bind() { + super.bind(); + this.on('beforeSave', this.beforeSave); + } + + unbind() { + super.unbind(); + this.off('beforeSave', this.beforeSave); + } +} + +export interface DatetimeNoTzFieldOptions extends BaseColumnFieldOptions { + type: 'datetimeNoTz'; +} + +function isIso8601(str) { + const iso8601StrictRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + return iso8601StrictRegex.test(str); +} diff --git a/packages/core/database/src/fields/datetime-tz-field.ts b/packages/core/database/src/fields/datetime-tz-field.ts new file mode 100644 index 0000000000..626ce1032f --- /dev/null +++ b/packages/core/database/src/fields/datetime-tz-field.ts @@ -0,0 +1,17 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { DateField } from './date-field'; +import { BaseColumnFieldOptions } from './field'; + +export class DatetimeTzField extends DateField {} + +export interface DatetimeTzFieldOptions extends BaseColumnFieldOptions { + type: 'datetimeTz'; +} diff --git a/packages/core/database/src/fields/field.ts b/packages/core/database/src/fields/field.ts index b9f230dd4f..e4b0b70024 100644 --- a/packages/core/database/src/fields/field.ts +++ b/packages/core/database/src/fields/field.ts @@ -56,7 +56,7 @@ export abstract class Field { return this.options.type; } - abstract get dataType(); + abstract get dataType(): any; isRelationField() { return false; @@ -171,11 +171,13 @@ export abstract class Field { Object.assign(opts, { type: this.database.sequelize.normalizeDataType(this.dataType) }); } + Object.assign(opts, this.additionalSequelizeOptions()); + return opts; } - isSqlite() { - return this.database.sequelize.getDialect() === 'sqlite'; + additionalSequelizeOptions() { + return {}; } typeToString() { diff --git a/packages/core/database/src/fields/index.ts b/packages/core/database/src/fields/index.ts index 610b6f1ad1..8e21d98575 100644 --- a/packages/core/database/src/fields/index.ts +++ b/packages/core/database/src/fields/index.ts @@ -36,6 +36,10 @@ import { UUIDFieldOptions } from './uuid-field'; import { VirtualFieldOptions } from './virtual-field'; import { NanoidFieldOptions } from './nanoid-field'; import { EncryptionField } from './encryption-field'; +import { UnixTimestampFieldOptions } from './unix-timestamp-field'; +import { DateOnlyFieldOptions } from './date-only-field'; +import { DatetimeNoTzField, DatetimeNoTzFieldOptions } from './datetime-no-tz-field'; +import { DatetimeTzFieldOptions } from './datetime-tz-field'; export * from './array-field'; export * from './belongs-to-field'; @@ -43,6 +47,10 @@ export * from './belongs-to-many-field'; export * from './boolean-field'; export * from './context-field'; export * from './date-field'; +export * from './datetime-field'; +export * from './datetime-tz-field'; +export * from './datetime-no-tz-field'; +export * from './date-only-field'; export * from './field'; export * from './has-many-field'; export * from './has-one-field'; @@ -61,6 +69,7 @@ export * from './uuid-field'; export * from './virtual-field'; export * from './nanoid-field'; export * from './encryption-field'; +export * from './unix-timestamp-field'; export type FieldOptions = | BaseFieldOptions @@ -81,6 +90,10 @@ export type FieldOptions = | SetFieldOptions | TimeFieldOptions | DateFieldOptions + | DatetimeTzFieldOptions + | DatetimeNoTzFieldOptions + | DateOnlyFieldOptions + | UnixTimestampFieldOptions | UidFieldOptions | UUIDFieldOptions | NanoidFieldOptions diff --git a/packages/core/database/src/fields/unix-timestamp-field.ts b/packages/core/database/src/fields/unix-timestamp-field.ts new file mode 100644 index 0000000000..f0169f2018 --- /dev/null +++ b/packages/core/database/src/fields/unix-timestamp-field.ts @@ -0,0 +1,84 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { DataTypes } from 'sequelize'; +import { DateField } from './date-field'; +import { BaseColumnFieldOptions } from './field'; + +export class UnixTimestampField extends DateField { + get dataType() { + return DataTypes.BIGINT; + } + + dateToValue(val) { + if (val === null || val === undefined) { + return val; + } + + let { accuracy } = this.options; + + if (this.options?.uiSchema?.['x-component-props']?.accuracy) { + accuracy = this.options?.uiSchema['x-component-props']?.accuracy; + } + + if (!accuracy) { + accuracy = 'second'; + } + + let rationalNumber = 1000; + + if (accuracy === 'millisecond') { + rationalNumber = 1; + } + + return Math.floor(new Date(val).getTime() / rationalNumber); + } + + additionalSequelizeOptions() { + const { name } = this.options; + let { accuracy } = this.options; + + if (this.options?.uiSchema?.['x-component-props']?.accuracy) { + accuracy = this.options?.uiSchema['x-component-props']?.accuracy; + } + + if (!accuracy) { + accuracy = 'second'; + } + + let rationalNumber = 1000; + + if (accuracy === 'millisecond') { + rationalNumber = 1; + } + + return { + get() { + const value = this.getDataValue(name); + if (value === null || value === undefined) { + return value; + } + + return new Date(value * rationalNumber); + }, + set(value) { + if (value === null || value === undefined) { + this.setDataValue(name, value); + } else { + // date to unix timestamp + this.setDataValue(name, Math.floor(new Date(value).getTime() / rationalNumber)); + } + }, + }; + } +} + +export interface UnixTimestampFieldOptions extends BaseColumnFieldOptions { + type: 'unixTimestamp'; +} diff --git a/packages/core/database/src/model.ts b/packages/core/database/src/model.ts index 9152ae6b39..9ec745fcca 100644 --- a/packages/core/database/src/model.ts +++ b/packages/core/database/src/model.ts @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import lodash, { isPlainObject } from 'lodash'; +import lodash from 'lodash'; import { Model as SequelizeModel, ModelStatic } from 'sequelize'; import { Collection } from './collection'; import { Database } from './database'; @@ -50,6 +50,21 @@ export class Model { - if (isDate(date)) { - return date; +const toDate = (date, options: any = {}) => { + const { ctx } = options; + const val = isDate(date) ? date : new Date(date); + const field = ctx.db.getFieldByPath(ctx.fieldPath); + + if (!field) { + return val; } - return new Date(date); + + if (field.constructor.name === 'UnixTimestampField') { + return field.dateToValue(val); + } + + if (field.constructor.name === 'DatetimeNoTzField') { + return moment(val).utcOffset('+00:00').format('YYYY-MM-DD HH:mm:ss'); + } + + if (field.constructor.name === 'DateOnlyField') { + return moment(val).format('YYYY-MM-DD HH:mm:ss'); + } + + return val; }; +function parseDateTimezone(ctx) { + const field = ctx.db.getFieldByPath(ctx.fieldPath); + + if (!field) { + return ctx.db.options.timezone; + } + + if (field.constructor.name === 'DatetimeNoTzField') { + return '+00:00'; + } + + if (field.constructor.name === 'DateOnlyField') { + return '+00:00'; + } + + return ctx.db.options.timezone; +} + +function isDatetimeString(str) { + return /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(str); +} + export default { $dateOn(value, ctx) { const r = parseDate(value, { - timezone: ctx.db.options.timezone, + timezone: parseDateTimezone(ctx), }); + if (typeof r === 'string') { return { - [Op.eq]: toDate(r), + [Op.eq]: toDate(r, { ctx }), }; } + if (Array.isArray(r)) { return { - [Op.and]: [{ [Op.gte]: toDate(r[0]) }, { [Op.lt]: toDate(r[1]) }], + [Op.and]: [{ [Op.gte]: toDate(r[0], { ctx }) }, { [Op.lt]: toDate(r[1], { ctx }) }], }; } + throw new Error(`Invalid Date ${JSON.stringify(value)}`); }, $dateNotOn(value, ctx) { const r = parseDate(value, { - timezone: ctx.db.options.timezone, + timezone: parseDateTimezone(ctx), }); if (typeof r === 'string') { return { - [Op.ne]: toDate(r), + [Op.ne]: toDate(r, { ctx }), }; } if (Array.isArray(r)) { return { - [Op.or]: [{ [Op.lt]: toDate(r[0]) }, { [Op.gte]: toDate(r[1]) }], + [Op.or]: [{ [Op.lt]: toDate(r[0], { ctx }) }, { [Op.gte]: toDate(r[1], { ctx }) }], }; } + throw new Error(`Invalid Date ${JSON.stringify(value)}`); }, $dateBefore(value, ctx) { const r = parseDate(value, { - timezone: ctx.db.options.timezone, + timezone: parseDateTimezone(ctx), }); + if (typeof r === 'string') { return { - [Op.lt]: toDate(r), + [Op.lt]: toDate(r, { ctx }), }; } else if (Array.isArray(r)) { return { - [Op.lt]: toDate(r[0]), + [Op.lt]: toDate(r[0], { ctx }), }; } + throw new Error(`Invalid Date ${JSON.stringify(value)}`); }, $dateNotBefore(value, ctx) { const r = parseDate(value, { - timezone: ctx.db.options.timezone, + timezone: parseDateTimezone(ctx), }); if (typeof r === 'string') { return { - [Op.gte]: toDate(r), + [Op.gte]: toDate(r, { ctx }), }; } else if (Array.isArray(r)) { return { - [Op.gte]: toDate(r[0]), + [Op.gte]: toDate(r[0], { ctx }), }; } throw new Error(`Invalid Date ${JSON.stringify(value)}`); @@ -90,15 +136,15 @@ export default { $dateAfter(value, ctx) { const r = parseDate(value, { - timezone: ctx.db.options.timezone, + timezone: parseDateTimezone(ctx), }); if (typeof r === 'string') { return { - [Op.gt]: toDate(r), + [Op.gt]: toDate(r, { ctx }), }; } else if (Array.isArray(r)) { return { - [Op.gte]: toDate(r[1]), + [Op.gte]: toDate(r[1], { ctx }), }; } throw new Error(`Invalid Date ${JSON.stringify(value)}`); @@ -106,15 +152,15 @@ export default { $dateNotAfter(value, ctx) { const r = parseDate(value, { - timezone: ctx.db.options.timezone, + timezone: parseDateTimezone(ctx), }); if (typeof r === 'string') { return { - [Op.lte]: toDate(r), + [Op.lte]: toDate(r, { ctx }), }; } else if (Array.isArray(r)) { return { - [Op.lt]: toDate(r[1]), + [Op.lt]: toDate(r[1], { ctx }), }; } throw new Error(`Invalid Date ${JSON.stringify(value)}`); @@ -122,11 +168,11 @@ export default { $dateBetween(value, ctx) { const r = parseDate(value, { - timezone: ctx.db.options.timezone, + timezone: parseDateTimezone(ctx), }); if (r) { return { - [Op.and]: [{ [Op.gte]: toDate(r[0]) }, { [Op.lt]: toDate(r[1]) }], + [Op.and]: [{ [Op.gte]: toDate(r[0], { ctx }) }, { [Op.lt]: toDate(r[1], { ctx }) }], }; } throw new Error(`Invalid Date ${JSON.stringify(value)}`); diff --git a/packages/core/database/src/repository.ts b/packages/core/database/src/repository.ts index e1eab58e52..b86301dc49 100644 --- a/packages/core/database/src/repository.ts +++ b/packages/core/database/src/repository.ts @@ -573,7 +573,7 @@ export class Repository(values, { ...options, @@ -645,7 +645,7 @@ export class Repository exten * @internal */ public perfHistograms = new Map(); - protected plugins = new Map(); - protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance(); - protected _started: Date | null = null; - private _authenticated = false; - private _maintaining = false; - private _maintainingCommandStatus: MaintainingCommandStatus; - private _maintainingStatusBeforeCommand: MaintainingCommandStatus | null; - private _actionCommand: Command; - /** * @internal */ public syncManager: SyncManager; public requestLogger: Logger; + protected plugins = new Map(); + protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance(); + private _authenticated = false; + private _maintaining = false; + private _maintainingCommandStatus: MaintainingCommandStatus; + private _maintainingStatusBeforeCommand: MaintainingCommandStatus | null; + private _actionCommand: Command; private sqlLogger: Logger; - protected _logger: SystemLogger; constructor(public options: ApplicationOptions) { super(); @@ -241,6 +238,8 @@ export class Application exten } } + protected _started: Date | null = null; + /** * @experimental */ @@ -248,6 +247,8 @@ export class Application exten return this._started; } + protected _logger: SystemLogger; + get logger() { return this._logger; } diff --git a/packages/core/utils/src/parse-date.ts b/packages/core/utils/src/parse-date.ts index c87ff4f30b..ee55112db8 100644 --- a/packages/core/utils/src/parse-date.ts +++ b/packages/core/utils/src/parse-date.ts @@ -174,10 +174,13 @@ export function parseDate(value: any, options = {} as { timezone?: string }) { if (!value) { return; } + if (Array.isArray(value)) { return parseDateBetween(value, options); } + let timezone = options.timezone || '+00:00'; + const input = value; if (typeof value === 'string') { const match = /(.+)((\+|\-)\d\d\:\d\d)$/.exec(value); @@ -232,10 +235,12 @@ function parseDateBetween(value: any, options = {} as { timezone?: string }) { } const match = /(.+)((\+|\-)\d\d\:\d\d)$/.exec(value); let timezone = options.timezone || '+00:00'; + if (match) { value = match[1]; timezone = match[2]; } + const m = /^(\(|\[)(.+)\,(.+)(\)|\])$/.exec(value); if (!m) { return; diff --git a/packages/core/utils/src/parse-filter.ts b/packages/core/utils/src/parse-filter.ts index 014f64c3c3..f298afcd1e 100644 --- a/packages/core/utils/src/parse-filter.ts +++ b/packages/core/utils/src/parse-filter.ts @@ -107,6 +107,7 @@ const dateValueWrapper = (value: any, timezone?: string) => { if (!value) { return null; } + if (Array.isArray(value)) { if (value.length === 2) { value.push('[]', timezone); @@ -182,6 +183,11 @@ export const parseFilter = async (filter: any, opts: ParseFilterOptions = {}) => } if (isDateOperator(operator)) { const field = getField?.(path); + + if (field?.constructor.name === 'DateOnlyField' || field?.constructor.name === 'DatetimeNoTzField') { + return value; + } + return dateValueWrapper(value, field?.timezone || timezone); } return value; diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx index bead118e6a..6fe74c671a 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx @@ -153,7 +153,7 @@ export const calendarBlockSettings = new SchemaSettings({ return { title: t('End date field'), value: fieldNames.end, - options: getCollectionFieldsOptions(name, 'date', { + options: getCollectionFieldsOptions(name, ['date', 'datetime', 'dateOnly', 'datetimeNoTz'], { association: ['o2o', 'obo', 'oho', 'm2o'], }), onChange: (end) => { diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx index 7b1e008c22..64e937dd61 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx @@ -70,7 +70,7 @@ export const useCreateCalendarBlock = () => { const createCalendarBlock = async ({ item }) => { const stringFieldsOptions = getCollectionFieldsOptions(item.name, 'string', { dataSource: item.dataSource }); - const dateFieldsOptions = getCollectionFieldsOptions(item.name, 'date', { + const dateFieldsOptions = getCollectionFieldsOptions(item.name, ['date', 'datetime', 'dateOnly', 'datetimeNoTz'], { association: ['o2o', 'obo', 'oho', 'm2o'], dataSource: item.dataSource, }); diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/collection-template/general3.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/collection-template/general3.test.ts index 4a30a4bd6f..1e027548be 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/collection-template/general3.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/collection-template/general3.test.ts @@ -75,7 +75,10 @@ test.describe('configure fields', () => { await addField('Attachment'); // 添加 date & time 字段 - await addField('Datetime'); + await addField('Datetime(with time zone)'); + await addField('Datetime(without time zone)'); + await addField('DateOnly'); + await addField('Unix Timestamp'); await addField('Time'); // 添加 relation 字段 diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/utils.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/utils.ts index f858128762..4b81aa3dba 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/utils.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/utils.ts @@ -430,8 +430,11 @@ export type FieldInterface = | 'Markdown' | 'Rich Text' | 'Attachment' - | 'Datetime' + | 'Datetime(with time zone)' + | 'Datetime(without time zone)' + | 'Date' | 'Time' + | 'Unix Timestamp' | 'One to one (belongs to)' | 'One to one (has one)' | 'One to many' diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/fields/datetime.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/fields/datetime.test.ts new file mode 100644 index 0000000000..86e0f326fd --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/fields/datetime.test.ts @@ -0,0 +1,51 @@ +import Database, { Collection as DBCollection } from '@nocobase/database'; +import Application from '@nocobase/server'; +import { createApp } from '../index'; + +describe('datetime', () => { + let db: Database; + let app: Application; + let Collection: DBCollection; + let Field: DBCollection; + + beforeEach(async () => { + app = await createApp(); + db = app.db; + Collection = db.getCollection('collections'); + Field = db.getCollection('fields'); + }); + + afterEach(async () => { + await app.destroy(); + }); + + it('should create datetimeNoTz field', async () => { + await Collection.repository.create({ + values: { + name: 'tests', + fields: [ + { + type: 'datetimeNoTz', + name: 'date1', + }, + ], + }, + context: {}, + }); + + // @ts-ignore + const agent = app.agent(); + + const createRes = await agent.resource('tests').create({ + values: { + date1: '2023-03-24 12:00:00', + }, + }); + + expect(createRes.status).toBe(200); + + // get item + const res = await agent.resource('tests').list(); + expect(res.body.data[0].date1).toBe('2023-03-24 12:00:00'); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/SetFilterTargetKey.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/SetFilterTargetKey.tsx index 57a25ac964..5d3e26f986 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/SetFilterTargetKey.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/SetFilterTargetKey.tsx @@ -38,7 +38,7 @@ export const SetFilterTargetKey = (props) => { const interfaceOptions = app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface( field.interface, ); - if (interfaceOptions.titleUsable) { + if (interfaceOptions?.titleUsable) { return true; } return false; diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/FieldType.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/FieldType.tsx index 403015f0f6..7ea3db0021 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/FieldType.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/FieldType.tsx @@ -11,12 +11,13 @@ import { observer } from '@formily/react'; import { useRecord } from '@nocobase/client'; import { Select, Tag } from 'antd'; import React from 'react'; +import { omit } from 'lodash'; export const FieldType = observer( (props: any) => { const { value, handleFieldChange, onChange } = props; const record = useRecord(); - const item = record; + const item = omit(record, ['__parent', '__collectionName']); return !item?.possibleTypes ? ( {value} ) : ( diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/CollectionFields.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/CollectionFields.tsx index eb5a12ea28..4c2dcb3cfd 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/CollectionFields.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/CollectionFields.tsx @@ -81,13 +81,13 @@ const tableContainer = css` } td, th { - flex: 2; + flex: 2.3; width: 0; &:nth-child(5) { flex: 1.2; } &:last-child { - flex: 1.8; + flex: 1.5; } } .ant-table-selection-column, diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/Gantt.Settings.tsx b/packages/plugins/@nocobase/plugin-gantt/src/client/Gantt.Settings.tsx index 2916d9128f..336420ccc3 100644 --- a/packages/plugins/@nocobase/plugin-gantt/src/client/Gantt.Settings.tsx +++ b/packages/plugins/@nocobase/plugin-gantt/src/client/Gantt.Settings.tsx @@ -366,7 +366,7 @@ export const ganttSettings = new SchemaSettings({ return { title: t('Start date field'), value: fieldNames.start, - options: useOptions('date'), + options: useOptions(['date', 'datetime', 'dateOnly', 'datetimeNoTz']), onChange: (start) => { const fieldNames = field.decoratorProps.fieldNames || {}; fieldNames['start'] = start; @@ -398,7 +398,7 @@ export const ganttSettings = new SchemaSettings({ return { title: t('End date field'), value: fieldNames.end, - options: useOptions('date'), + options: useOptions(['date', 'datetime', 'dateOnly', 'datetimeNoTz']), onChange: (end) => { const fieldNames = field.decoratorProps.fieldNames || {}; fieldNames['end'] = end; diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx index 310b65638b..d09a0bdb74 100644 --- a/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx @@ -80,7 +80,7 @@ export const useCreateGanttBlock = () => { }; }); const dateFields = collectionFields - ?.filter((field) => field.type === 'date') + ?.filter((field) => ['date', 'datetime', 'dateOnly', 'datetimeNoTz'].includes(field.type)) ?.map((field) => { return { label: field?.uiSchema?.title, diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/utils.tsx b/packages/plugins/@nocobase/plugin-gantt/src/client/utils.tsx index be011ba1c3..38a6e30007 100644 --- a/packages/plugins/@nocobase/plugin-gantt/src/client/utils.tsx +++ b/packages/plugins/@nocobase/plugin-gantt/src/client/utils.tsx @@ -13,11 +13,17 @@ import { useTranslation } from 'react-i18next'; export const useGanttTranslation = () => { return useTranslation('gantt'); }; -export const useOptions = (type = 'string') => { +export const useOptions = (type: string | string[] = 'string') => { const compile = useCompile(); const { fields } = useCollection_deprecated(); const options = fields - ?.filter((field) => field.type === type) + ?.filter((field) => { + if (typeof type === 'string') { + return field.type === type; + } else { + return type.includes(field.type); + } + }) ?.map((field) => { return { value: field.name, diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/customFormBlocks1.test.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/customFormBlocks1.test.ts index b8f6586d37..2b8871b5a6 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/customFormBlocks1.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/customFormBlocks1.test.ts @@ -565,7 +565,7 @@ test.describe('field data entry', () => { await page .locator(`button[aria-label^="schema-initializer-Grid-workflowManual:customForm:configureFields-${randomValue}"]`) .hover(); - await page.getByRole('menuitem', { name: 'Datetime', exact: true }).click(); + await page.getByRole('menuitem', { name: 'Datetime(with time zone)' }).click(); await page .getByLabel(`block-item-Input-${randomValue}-Field display name`) .getByRole('textbox')