mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-11-30 03:08:31 +08:00
fix: import with date field (#5606)
* fix: import with dateOnly and datetimeNoTz field * fix: import with date field * fix: export datetime filed * fix: test * fix: test * fix: test * fix: unixtimestamp import * chore: test
This commit is contained in:
parent
3d512adade
commit
ef1ded8ff2
@ -10,11 +10,11 @@
|
||||
import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__';
|
||||
import { isArr } from '@formily/shared';
|
||||
import {
|
||||
getDefaultFormat,
|
||||
GetDefaultFormatProps,
|
||||
str2moment,
|
||||
Str2momentOptions,
|
||||
Str2momentValue,
|
||||
getDefaultFormat,
|
||||
str2moment,
|
||||
} from '@nocobase/utils/client';
|
||||
import cls from 'classnames';
|
||||
import dayjs from 'dayjs';
|
||||
@ -67,6 +67,7 @@ ReadPretty.DateRangePicker = function DateRangePicker(props: DateRangePickerRead
|
||||
const labels = m.map((m) => m.format(format));
|
||||
return isArr(labels) ? labels.join('~') : labels;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cls(prefixCls, props.className)} style={props.style}>
|
||||
{getLabels()}
|
||||
|
@ -39,7 +39,7 @@ describe('Date time interface', () => {
|
||||
},
|
||||
{
|
||||
name: 'dateTime',
|
||||
type: 'date',
|
||||
type: 'datetime',
|
||||
uiSchema: {
|
||||
['x-component-props']: {
|
||||
showTime: true,
|
||||
@ -74,20 +74,5 @@ describe('Date time interface', () => {
|
||||
expect(await interfaceInstance.toValue(42510)).toBe('2016-05-20T00:00:00.000Z');
|
||||
expect(await interfaceInstance.toValue('42510')).toBe('2016-05-20T00:00:00.000Z');
|
||||
expect(await interfaceInstance.toValue('2016-05-20T00:00:00.000Z')).toBe('2016-05-20T00:00:00.000Z');
|
||||
expect(
|
||||
await interfaceInstance.toValue('2016-05-20 04:22:22', {
|
||||
field: testCollection.getField('dateOnly'),
|
||||
}),
|
||||
).toBe('2016-05-20T00:00:00.000Z');
|
||||
expect(
|
||||
await interfaceInstance.toValue('2016-05-20 01:00:00', {
|
||||
field: testCollection.getField('dateTime'),
|
||||
}),
|
||||
).toBe(dayjs('2016-05-20 01:00:00').toISOString());
|
||||
expect(
|
||||
await interfaceInstance.toValue('2016-05-20 01:00:00', {
|
||||
field: testCollection.getField('dateTimeGmt'),
|
||||
}),
|
||||
).toBe('2016-05-20T01:00:00.000Z');
|
||||
});
|
||||
});
|
||||
|
7
packages/core/database/src/interfaces/date-interface.ts
Normal file
7
packages/core/database/src/interfaces/date-interface.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { DatetimeInterface } from './datetime-interface';
|
||||
|
||||
export class DateInterface extends DatetimeInterface {
|
||||
toString(value: any, ctx?: any): any {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { BaseInterface } from './base-interface';
|
||||
import { getDefaultFormat, moment2str, str2moment } from '@nocobase/utils';
|
||||
import { getDefaultFormat, str2moment } from '@nocobase/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import { getJsDateFromExcel } from 'excel-date-to-js';
|
||||
|
||||
@ -51,11 +51,7 @@ export class DatetimeInterface extends BaseInterface {
|
||||
} else if (isNumeric(value)) {
|
||||
return getJsDateFromExcel(value).toISOString();
|
||||
} else if (typeof value === 'string') {
|
||||
const props = ctx.field?.options?.uiSchema?.['x-component-props'] || {};
|
||||
const m = dayjs(value);
|
||||
if (m.isValid()) {
|
||||
return moment2str(m, props);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid date - ${value}`);
|
||||
|
@ -0,0 +1,49 @@
|
||||
import { DatetimeInterface } from './datetime-interface';
|
||||
import dayjs from 'dayjs';
|
||||
import { getJsDateFromExcel } from 'excel-date-to-js';
|
||||
import { getDefaultFormat, str2moment } from '@nocobase/utils';
|
||||
|
||||
function isDate(v) {
|
||||
return v instanceof Date;
|
||||
}
|
||||
|
||||
function isNumeric(str: any) {
|
||||
if (typeof str === 'number') return true;
|
||||
if (typeof str != 'string') return false;
|
||||
return !isNaN(str as any) && !isNaN(parseFloat(str));
|
||||
}
|
||||
|
||||
export class DatetimeNoTzInterface extends DatetimeInterface {
|
||||
async toValue(value: any, ctx: any = {}): Promise<any> {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const match = /^(\d{4})[-/]?(\d{2})[-/]?(\d{2})$/.exec(value);
|
||||
if (match) {
|
||||
return `${match[1]}-${match[2]}-${match[3]}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (dayjs.isDayjs(value)) {
|
||||
return value;
|
||||
} else if (isDate(value)) {
|
||||
return value;
|
||||
} else if (isNumeric(value)) {
|
||||
const date = getJsDateFromExcel(value);
|
||||
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
|
||||
} else if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid date - ${value}`);
|
||||
}
|
||||
|
||||
toString(value: any, ctx?: any) {
|
||||
const props = this.options?.uiSchema?.['x-component-props'] ?? {};
|
||||
const format = getDefaultFormat(props);
|
||||
const m = str2moment(value, { ...props });
|
||||
return m ? m.format(format) : '';
|
||||
}
|
||||
}
|
@ -12,4 +12,6 @@ export * from './percent-interface';
|
||||
export * from './multiple-select-interface';
|
||||
export * from './select-interface';
|
||||
export * from './datetime-interface';
|
||||
export * from './datetime-no-tz-interface';
|
||||
export * from './boolean-interface';
|
||||
export * from './date-interface';
|
||||
|
@ -10,7 +10,9 @@
|
||||
import Database from '../database';
|
||||
import {
|
||||
BooleanInterface,
|
||||
DateInterface,
|
||||
DatetimeInterface,
|
||||
DatetimeNoTzInterface,
|
||||
MultipleSelectInterface,
|
||||
PercentInterface,
|
||||
SelectInterface,
|
||||
@ -36,6 +38,9 @@ const interfaces = {
|
||||
radioGroup: SelectInterface,
|
||||
percent: PercentInterface,
|
||||
datetime: DatetimeInterface,
|
||||
datetimeNoTz: DatetimeNoTzInterface,
|
||||
unixTimestamp: DatetimeInterface,
|
||||
date: DateInterface,
|
||||
createdAt: DatetimeInterface,
|
||||
updatedAt: DatetimeInterface,
|
||||
boolean: BooleanInterface,
|
||||
|
@ -78,7 +78,7 @@ const toMoment = (val: any, options?: Str2momentOptions) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
const offset = options.utcOffset || -1 * new Date().getTimezoneOffset();
|
||||
const offset = options.utcOffset !== undefined ? options.utcOffset : -1 * new Date().getTimezoneOffset();
|
||||
const { gmt, picker, utc = true } = options;
|
||||
if (dayjs(val).isValid()) {
|
||||
if (!utc) {
|
||||
|
@ -31,6 +31,122 @@ describe('export to xlsx with preset', () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
describe('export with date field', () => {
|
||||
let Post;
|
||||
|
||||
beforeEach(async () => {
|
||||
Post = app.db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{
|
||||
name: 'datetime',
|
||||
type: 'datetime',
|
||||
interface: 'datetime',
|
||||
uiSchema: {
|
||||
'x-component-props': { picker: 'date', dateFormat: 'YYYY-MM-DD', gmt: false, showTime: false, utc: true },
|
||||
type: 'string',
|
||||
'x-component': 'DatePicker',
|
||||
title: 'dateTz',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dateOnly',
|
||||
type: 'dateOnly',
|
||||
interface: 'date',
|
||||
defaultToCurrentTime: false,
|
||||
onUpdateToCurrentTime: false,
|
||||
timezone: true,
|
||||
},
|
||||
{
|
||||
name: 'datetimeNoTz',
|
||||
type: 'datetimeNoTz',
|
||||
interface: 'datetimeNoTz',
|
||||
uiSchema: {
|
||||
'x-component-props': { picker: 'date', dateFormat: 'YYYY-MM-DD', gmt: false, showTime: false, utc: true },
|
||||
type: 'string',
|
||||
'x-component': 'DatePicker',
|
||||
title: 'dateTz',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'unixTimestamp',
|
||||
type: 'unixTimestamp',
|
||||
interface: 'unixTimestamp',
|
||||
uiSchema: {
|
||||
'x-component-props': {
|
||||
picker: 'date',
|
||||
dateFormat: 'YYYY-MM-DD',
|
||||
showTime: true,
|
||||
timeFormat: 'HH:mm:ss',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await app.db.sync();
|
||||
});
|
||||
|
||||
it('should export with datetime field', async () => {
|
||||
await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
datetime: '2024-05-10T01:42:35.000Z',
|
||||
dateOnly: '2024-05-10',
|
||||
datetimeNoTz: '2024-01-01 00:00:00',
|
||||
unixTimestamp: '2024-05-10T01:42:35.000Z',
|
||||
},
|
||||
});
|
||||
|
||||
const exporter = new XlsxExporter({
|
||||
collectionManager: app.mainDataSource.collectionManager,
|
||||
collection: Post,
|
||||
chunkSize: 10,
|
||||
columns: [
|
||||
{ dataIndex: ['title'], defaultTitle: 'Title' },
|
||||
{
|
||||
dataIndex: ['datetime'],
|
||||
defaultTitle: 'datetime',
|
||||
},
|
||||
{
|
||||
dataIndex: ['dateOnly'],
|
||||
defaultTitle: 'dateOnly',
|
||||
},
|
||||
{
|
||||
dataIndex: ['datetimeNoTz'],
|
||||
defaultTitle: 'datetimeNoTz',
|
||||
},
|
||||
{
|
||||
dataIndex: ['unixTimestamp'],
|
||||
defaultTitle: 'unixTimestamp',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const wb = await exporter.run();
|
||||
|
||||
const xlsxFilePath = path.resolve(__dirname, `t_${uid()}.xlsx`);
|
||||
|
||||
try {
|
||||
XLSX.writeFile(wb, xlsxFilePath);
|
||||
|
||||
// read xlsx file
|
||||
const workbook = XLSX.readFile(xlsxFilePath);
|
||||
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||
const sheetData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });
|
||||
|
||||
const firstUser = sheetData[1];
|
||||
expect(firstUser[1]).toEqual('2024-05-10');
|
||||
expect(firstUser[2]).toEqual('2024-05-10');
|
||||
expect(firstUser[3]).toEqual('2024-01-01');
|
||||
expect(firstUser[4]).toEqual('2024-05-10 01:42:35');
|
||||
} finally {
|
||||
fs.unlinkSync(xlsxFilePath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should export with checkbox field', async () => {
|
||||
const Post = app.db.collection({
|
||||
name: 'posts',
|
||||
@ -520,7 +636,7 @@ describe('export to xlsx', () => {
|
||||
title: 'test_date',
|
||||
},
|
||||
name: 'test_date',
|
||||
type: 'date',
|
||||
type: 'datetime',
|
||||
interface: 'datetime',
|
||||
},
|
||||
],
|
||||
@ -548,11 +664,7 @@ describe('export to xlsx', () => {
|
||||
],
|
||||
});
|
||||
|
||||
const wb = await exporter.run({
|
||||
get() {
|
||||
return '+08:00';
|
||||
},
|
||||
});
|
||||
const wb = await exporter.run();
|
||||
|
||||
const xlsxFilePath = path.resolve(__dirname, `t_${uid()}.xlsx`);
|
||||
try {
|
||||
@ -564,7 +676,7 @@ describe('export to xlsx', () => {
|
||||
const sheetData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });
|
||||
|
||||
const firstUser = sheetData[1];
|
||||
expect(firstUser).toEqual(['some_title', '2024-05-10 09:42:35']);
|
||||
expect(firstUser).toEqual(['some_title', '2024-05-10 01:42:35']);
|
||||
} finally {
|
||||
fs.unlinkSync(xlsxFilePath);
|
||||
}
|
||||
|
@ -38,24 +38,143 @@ describe('xlsx importer', () => {
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'date',
|
||||
type: 'datetime',
|
||||
name: 'datetime',
|
||||
interface: 'datetime',
|
||||
},
|
||||
{
|
||||
type: 'datetimeNoTz',
|
||||
name: 'datetimeNoTz',
|
||||
interface: 'datetimeNoTz',
|
||||
uiSchema: {
|
||||
'x-component-props': {
|
||||
picker: 'date',
|
||||
dateFormat: 'YYYY-MM-DD',
|
||||
showTime: true,
|
||||
timeFormat: 'HH:mm:ss',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'dateOnly',
|
||||
name: 'dateOnly',
|
||||
interface: 'date',
|
||||
},
|
||||
{
|
||||
type: 'unixTimestamp',
|
||||
name: 'unixTimestamp',
|
||||
interface: 'unixTimestamp',
|
||||
uiSchema: {
|
||||
'x-component-props': {
|
||||
picker: 'date',
|
||||
dateFormat: 'YYYY-MM-DD',
|
||||
showTime: true,
|
||||
timeFormat: 'HH:mm:ss',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await app.db.sync();
|
||||
});
|
||||
|
||||
it('should import with date', async () => {
|
||||
it('should import with dateOnly', async () => {
|
||||
const columns = [
|
||||
{
|
||||
dataIndex: ['name'],
|
||||
defaultTitle: '姓名',
|
||||
},
|
||||
{
|
||||
dataIndex: ['date'],
|
||||
dataIndex: ['dateOnly'],
|
||||
defaultTitle: '日期',
|
||||
},
|
||||
];
|
||||
|
||||
const templateCreator = new TemplateCreator({
|
||||
collection: User,
|
||||
columns,
|
||||
});
|
||||
|
||||
const template = await templateCreator.run();
|
||||
|
||||
const worksheet = template.Sheets[template.SheetNames[0]];
|
||||
|
||||
XLSX.utils.sheet_add_aoa(
|
||||
worksheet,
|
||||
[
|
||||
['test', 77383],
|
||||
['test2', '2021-10-18'],
|
||||
],
|
||||
{ origin: 'A2' },
|
||||
);
|
||||
|
||||
const importer = new XlsxImporter({
|
||||
collectionManager: app.mainDataSource.collectionManager,
|
||||
collection: User,
|
||||
columns,
|
||||
workbook: template,
|
||||
});
|
||||
|
||||
await importer.run();
|
||||
|
||||
const users = (await User.repository.find()).map((user) => user.toJSON());
|
||||
expect(users[0]['dateOnly']).toBe('2111-11-12');
|
||||
expect(users[1]['dateOnly']).toBe('2021-10-18');
|
||||
});
|
||||
|
||||
it.skipIf(process.env['DB_DIALECT'] === 'sqlite')('should import with datetimeNoTz', async () => {
|
||||
const columns = [
|
||||
{
|
||||
dataIndex: ['name'],
|
||||
defaultTitle: '姓名',
|
||||
},
|
||||
{
|
||||
dataIndex: ['datetimeNoTz'],
|
||||
defaultTitle: '日期',
|
||||
},
|
||||
];
|
||||
|
||||
const templateCreator = new TemplateCreator({
|
||||
collection: User,
|
||||
columns,
|
||||
});
|
||||
|
||||
const template = await templateCreator.run();
|
||||
|
||||
const worksheet = template.Sheets[template.SheetNames[0]];
|
||||
|
||||
XLSX.utils.sheet_add_aoa(
|
||||
worksheet,
|
||||
[
|
||||
['test', 77383],
|
||||
['test2', '2021-10-18'],
|
||||
],
|
||||
{ origin: 'A2' },
|
||||
);
|
||||
|
||||
const importer = new XlsxImporter({
|
||||
collectionManager: app.mainDataSource.collectionManager,
|
||||
collection: User,
|
||||
columns,
|
||||
workbook: template,
|
||||
});
|
||||
|
||||
await importer.run();
|
||||
|
||||
const users = (await User.repository.find()).map((user) => user.toJSON());
|
||||
expect(users[0]['datetimeNoTz']).toBe('2111-11-12 00:00:00');
|
||||
expect(users[1]['datetimeNoTz']).toBe('2021-10-18 00:00:00');
|
||||
});
|
||||
|
||||
it('should import with unixTimestamp', async () => {
|
||||
const columns = [
|
||||
{
|
||||
dataIndex: ['name'],
|
||||
defaultTitle: '姓名',
|
||||
},
|
||||
{
|
||||
dataIndex: ['unixTimestamp'],
|
||||
defaultTitle: '日期',
|
||||
},
|
||||
];
|
||||
@ -80,11 +199,44 @@ describe('xlsx importer', () => {
|
||||
|
||||
await importer.run();
|
||||
|
||||
expect(await User.repository.count()).toBe(1);
|
||||
const users = (await User.repository.find()).map((user) => user.toJSON());
|
||||
expect(moment(users[0]['unixTimestamp']).toISOString()).toEqual('2111-11-12T00:00:00.000Z');
|
||||
});
|
||||
|
||||
const user = await User.repository.findOne();
|
||||
it('should import with datetimeTz', async () => {
|
||||
const columns = [
|
||||
{
|
||||
dataIndex: ['name'],
|
||||
defaultTitle: '姓名',
|
||||
},
|
||||
{
|
||||
dataIndex: ['datetime'],
|
||||
defaultTitle: '日期',
|
||||
},
|
||||
];
|
||||
|
||||
expect(moment(user.get('date')).format('YYYY-MM-DD')).toBe('2111-11-12');
|
||||
const templateCreator = new TemplateCreator({
|
||||
collection: User,
|
||||
columns,
|
||||
});
|
||||
|
||||
const template = await templateCreator.run();
|
||||
|
||||
const worksheet = template.Sheets[template.SheetNames[0]];
|
||||
|
||||
XLSX.utils.sheet_add_aoa(worksheet, [['test', 77383]], { origin: 'A2' });
|
||||
|
||||
const importer = new XlsxImporter({
|
||||
collectionManager: app.mainDataSource.collectionManager,
|
||||
collection: User,
|
||||
columns,
|
||||
workbook: template,
|
||||
});
|
||||
|
||||
await importer.run();
|
||||
|
||||
const users = (await User.repository.find()).map((user) => user.toJSON());
|
||||
expect(moment(users[0]['datetime']).toISOString()).toEqual('2111-11-12T00:00:00.000Z');
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user