mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
refactor: datetime field (#5084)
* refactor: date field support timezone, defaultToCurrentTime, and onUpdateToCurrentTime * refactor: availableTypes unixTimestamp * chore: defaultToCurrentTime * chore: unix timestamp field * fix: bug * chore: field type map * refactor: local improve * fix: bug * fix: bug * chore: timezone test * chore: test * fix: test * fix: test * chore: field setter * chore: test * chore: date only field * chore: test * chore: test * fix: bug * fix: unixTimestamp * fix: unixTimestamp * chore: accuracy * fix: bug * fix: bug * fix: unixTimestamp * fix: unixTimestamp * fix: date & datetime * refactor: add DateFieldInterface * fix: bug * chore: test * chore: test * chore: test * refactor: locale improve * refactor: local improve * fix: bug * refactor: unixTimestamp not support default value * refactor: timezone * refactor: datetimeNoTzFieldInterface * refactor: locale improve * refactor: locale improve * fix: test * fix: bug * chore: datetime no tz field * refactor: datetimeNoTz * refactor: datetime * fix: bug * refactor: timeFormat * style: collection fields style improve * refactor: defaultToCurrentTime * fix: datetime no tz * chore: field type map * fix: bug * fix: bug * refactor: createAt & updateAt * fix: bug * fix: no tz field with timezone * refactor: dateonly * fix: test * chore: data type map * fix: dateonly * fix: dateonly * fix: datetime * refactor: locale improve * refactor: unixtimestamp * fix: merge bug * fix: bug * fix: datetime * fix: datetime no tz * fix: datetime no tz * chore: mysql datetime map * chore: test * chore: test * chore: test * chore: datetimeTz field * fix: no interface option * refactor: update type * refactor: update type * fix: pg no tz field * chore: save iso8601 format to no tz field * fix: test * fix: test * refactor: gannt & calender startTime & endTime * refactor: unixTimestamp * chore: filter of datetime field * chore: test * chore: test * fix: test * fix: datetime no tz filter * chore: test * chore: test * fix: datetime default value in mysql * fix: sqlite test * chore: test * fix: test * fix: test * fix: $dateOn * fix: bug * fix: bug * refactor: datepicker * fix: test * refactor: datePicker * refactor: gantt setting --------- Co-authored-by: katherinehhh <katherine_15995@163.com>
This commit is contained in:
parent
68b3fa78c4
commit
a7df0e3fd3
@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ export class CreatedAtFieldInterface extends CollectionFieldInterface {
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
};
|
||||
availableTypes = ['date'];
|
||||
availableTypes = [];
|
||||
properties = {
|
||||
...defaultProps,
|
||||
...dateTimeProps,
|
||||
|
@ -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;
|
||||
}
|
@ -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")}}',
|
||||
|
@ -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;
|
||||
}
|
@ -46,3 +46,5 @@ export * from './sort';
|
||||
export * from './uuid';
|
||||
export * from './nanoid';
|
||||
export * from './unixTimestamp';
|
||||
export * from './dateOnly';
|
||||
export * from './datetimeNoTz';
|
||||
|
@ -255,6 +255,7 @@ export const dateTimeProps: { [key: string]: ISchema } = {
|
||||
`{{(field) => {
|
||||
field.query('..[].timeFormat').take(f => {
|
||||
f.display = field.value ? 'visible' : 'none';
|
||||
f.value='HH:mm:ss'
|
||||
});
|
||||
}}}`,
|
||||
],
|
||||
|
@ -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 = {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export class UpdatedAtFieldInterface extends CollectionFieldInterface {
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
};
|
||||
availableTypes = ['date'];
|
||||
availableTypes = [];
|
||||
properties = {
|
||||
...defaultProps,
|
||||
...dateTimeProps,
|
||||
|
@ -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,
|
||||
|
@ -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":"仅日期"
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
|
@ -78,18 +78,21 @@ 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');
|
||||
}
|
||||
if (props.dateOnly) {
|
||||
onChange(dateString !== '' ? dateString : undefined);
|
||||
} else {
|
||||
onChange(moment2str(value, props));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -64,6 +64,7 @@ export const DynamicComponent = (props: Props) => {
|
||||
minWidth: 150,
|
||||
...props.style,
|
||||
},
|
||||
utc: false,
|
||||
}),
|
||||
name: 'value',
|
||||
'x-read-pretty': false,
|
||||
|
@ -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 (
|
||||
<DatePicker
|
||||
{...props}
|
||||
value={v}
|
||||
value={value}
|
||||
onChange={(v: any) => {
|
||||
if (onChange) {
|
||||
onChange(getTimestamp(v, accuracy));
|
||||
onChange(v);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
mapReadPretty((props) => {
|
||||
const { value, accuracy = 'second' } = props;
|
||||
const v = useMemo(() => toValue(value, accuracy), [value, accuracy]);
|
||||
return <DatePicker.ReadPretty {...props} value={v} />;
|
||||
const { value } = props;
|
||||
return <DatePicker.ReadPretty {...props} value={value} />;
|
||||
}),
|
||||
);
|
||||
|
@ -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(`
|
||||
<div>
|
||||
@ -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();
|
||||
|
@ -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 {
|
||||
|
@ -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'];
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
250
packages/core/database/src/__tests__/fields/datetime-tz.test.ts
Normal file
250
packages/core/database/src/__tests__/fields/datetime-tz.test.ts
Normal file
@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
@ -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;
|
@ -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);
|
||||
});
|
||||
});
|
@ -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({
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
|
21
packages/core/database/src/fields/date-only-field.ts
Normal file
21
packages/core/database/src/fields/date-only-field.ts
Normal file
@ -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';
|
||||
}
|
17
packages/core/database/src/fields/datetime-field.ts
Normal file
17
packages/core/database/src/fields/datetime-field.ts
Normal file
@ -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';
|
||||
}
|
112
packages/core/database/src/fields/datetime-no-tz-field.ts
Normal file
112
packages/core/database/src/fields/datetime-no-tz-field.ts
Normal file
@ -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);
|
||||
}
|
17
packages/core/database/src/fields/datetime-tz-field.ts
Normal file
17
packages/core/database/src/fields/datetime-tz-field.ts
Normal file
@ -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';
|
||||
}
|
@ -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() {
|
||||
|
@ -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
|
||||
|
84
packages/core/database/src/fields/unix-timestamp-field.ts
Normal file
84
packages/core/database/src/fields/unix-timestamp-field.ts
Normal file
@ -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';
|
||||
}
|
@ -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<TModelAttributes extends {} = any, TCreationAttributes extend
|
||||
return await runner.runSync(options);
|
||||
}
|
||||
|
||||
static callSetters(values, options) {
|
||||
// map values
|
||||
const result = {};
|
||||
for (const key of Object.keys(values)) {
|
||||
const field = this.collection.getField(key);
|
||||
if (field && field.setter) {
|
||||
result[key] = field.setter.call(field, values[key], options, values, key);
|
||||
} else {
|
||||
result[key] = values[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO
|
||||
public toChangedWithAssociations() {
|
||||
// @ts-ignore
|
||||
|
@ -9,80 +9,126 @@
|
||||
|
||||
import { parseDate } from '@nocobase/utils';
|
||||
import { Op } from 'sequelize';
|
||||
import moment from 'moment';
|
||||
|
||||
function isDate(input) {
|
||||
return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
|
||||
}
|
||||
|
||||
const toDate = (date) => {
|
||||
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)}`);
|
||||
|
@ -573,7 +573,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
underscored: this.collection.options.underscored,
|
||||
});
|
||||
|
||||
const values = guard.sanitize(options.values || {});
|
||||
const values = (this.model as typeof Model).callSetters(guard.sanitize(options.values || {}), options);
|
||||
|
||||
const instance = await this.model.create<any>(values, {
|
||||
...options,
|
||||
@ -645,7 +645,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
|
||||
const guard = UpdateGuard.fromOptions(this.model, { ...options, underscored: this.collection.options.underscored });
|
||||
|
||||
const values = guard.sanitize(options.values);
|
||||
const values = (this.model as typeof Model).callSetters(guard.sanitize(options.values || {}), options);
|
||||
|
||||
// NOTE:
|
||||
// 1. better to be moved to separated API like bulkUpdate/updateMany
|
||||
|
@ -8,9 +8,9 @@
|
||||
*/
|
||||
|
||||
const postgres = {
|
||||
'character varying': ['string', 'uuid', 'nanoid', 'encryption'],
|
||||
varchar: ['string', 'uuid', 'nanoid', 'encryption'],
|
||||
char: ['string', 'uuid', 'nanoid', 'encryption'],
|
||||
'character varying': ['string', 'uuid', 'nanoid', 'encryption', 'datetimeNoTz'],
|
||||
varchar: ['string', 'uuid', 'nanoid', 'encryption', 'datetimeNoTz'],
|
||||
char: ['string', 'uuid', 'nanoid', 'encryption', 'datetimeNoTz'],
|
||||
|
||||
character: 'string',
|
||||
text: 'text',
|
||||
@ -18,18 +18,18 @@ const postgres = {
|
||||
name: 'string',
|
||||
|
||||
smallint: ['integer', 'sort'],
|
||||
integer: ['integer', 'sort'],
|
||||
bigint: ['bigInt', 'sort'],
|
||||
integer: ['integer', 'unixTimestamp', 'sort'],
|
||||
bigint: ['bigInt', 'unixTimestamp', 'sort'],
|
||||
decimal: 'decimal',
|
||||
numeric: 'float',
|
||||
real: 'float',
|
||||
'double precision': 'float',
|
||||
|
||||
'timestamp without time zone': 'date',
|
||||
'timestamp with time zone': 'date',
|
||||
'timestamp without time zone': 'datetimeNoTz',
|
||||
'timestamp with time zone': 'datetimeTz',
|
||||
'time without time zone': 'time',
|
||||
|
||||
date: 'date',
|
||||
date: 'dateOnly',
|
||||
boolean: 'boolean',
|
||||
|
||||
json: ['json', 'array'],
|
||||
@ -55,24 +55,24 @@ const mysql = {
|
||||
|
||||
char: ['string', 'uuid', 'nanoid', 'encryption'],
|
||||
varchar: ['string', 'uuid', 'nanoid', 'encryption'],
|
||||
date: 'date',
|
||||
date: 'dateOnly',
|
||||
time: 'time',
|
||||
tinytext: 'text',
|
||||
text: 'text',
|
||||
mediumtext: 'text',
|
||||
longtext: 'text',
|
||||
int: ['integer', 'sort'],
|
||||
'int unsigned': ['integer', 'sort'],
|
||||
integer: ['integer', 'sort'],
|
||||
bigint: ['bigInt', 'sort'],
|
||||
'bigint unsigned': ['bigInt', 'sort'],
|
||||
int: ['integer', 'unixTimestamp', 'sort'],
|
||||
'int unsigned': ['integer', 'unixTimestamp', 'sort'],
|
||||
integer: ['integer', 'unixTimestamp', 'sort'],
|
||||
bigint: ['bigInt', 'unixTimestamp', 'sort'],
|
||||
'bigint unsigned': ['bigInt', 'unixTimestamp', 'sort'],
|
||||
float: 'float',
|
||||
double: 'float',
|
||||
boolean: 'boolean',
|
||||
decimal: 'decimal',
|
||||
year: ['string', 'integer'],
|
||||
datetime: 'date',
|
||||
timestamp: 'date',
|
||||
datetime: ['datetimeNoTz', 'datetimeTz'],
|
||||
timestamp: 'datetimeTz',
|
||||
json: ['json', 'array'],
|
||||
enum: 'string',
|
||||
};
|
||||
@ -84,7 +84,7 @@ const sqlite = {
|
||||
integer: 'integer',
|
||||
real: 'real',
|
||||
|
||||
datetime: 'date',
|
||||
datetime: 'datetimeTz',
|
||||
date: 'date',
|
||||
time: 'time',
|
||||
|
||||
|
@ -213,22 +213,19 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
* @internal
|
||||
*/
|
||||
public perfHistograms = new Map<string, RecordableHistogram>();
|
||||
protected plugins = new Map<string, Plugin>();
|
||||
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<string, Plugin>();
|
||||
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<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
}
|
||||
}
|
||||
|
||||
protected _started: Date | null = null;
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
@ -248,6 +247,8 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return this._started;
|
||||
}
|
||||
|
||||
protected _logger: SystemLogger;
|
||||
|
||||
get logger() {
|
||||
return this._logger;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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) => {
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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 字段
|
||||
|
@ -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'
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
@ -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;
|
||||
|
@ -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 ? (
|
||||
<Tag>{value}</Tag>
|
||||
) : (
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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')
|
||||
|
Loading…
x
Reference in New Issue
Block a user