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:
ChengLei Shao 2024-11-07 21:05:58 +08:00 committed by GitHub
parent 3d512adade
commit ef1ded8ff2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 348 additions and 39 deletions

View File

@ -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()}

View File

@ -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');
});
});

View File

@ -0,0 +1,7 @@
import { DatetimeInterface } from './datetime-interface';
export class DateInterface extends DatetimeInterface {
toString(value: any, ctx?: any): any {
return value;
}
}

View File

@ -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}`);

View File

@ -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) : '';
}
}

View File

@ -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';

View File

@ -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,

View File

@ -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) {

View File

@ -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);
}

View File

@ -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');
});
});