From 5421686504b0d1928a5bbf9112293afb2fe0e7f3 Mon Sep 17 00:00:00 2001 From: chenos Date: Sun, 19 Feb 2023 17:35:23 +0800 Subject: [PATCH] feat: support for importing attachments (#1466) --- packages/core/database/package.json | 1 + .../src/__tests__/value-parsers/base.test.ts | 20 ++ .../src/__tests__/value-parsers/date.test.ts | 67 ++++++ .../__tests__/value-parsers/to-many.test.ts | 206 +++++++++++++++++ .../__tests__/value-parsers/to-one.test.ts | 60 +++++ .../src/value-parsers/array-value-parser.ts | 12 - .../src/value-parsers/base-value-parser.ts | 16 ++ .../src/value-parsers/boolean-value-parser.ts | 4 +- .../src/value-parsers/date-value-parser.ts | 15 +- .../src/value-parsers/json-value-parser.ts | 14 +- .../src/value-parsers/number-value-parser.ts | 4 +- .../src/value-parsers/to-many-value-parser.ts | 113 ++++++---- .../src/value-parsers/to-one-value-parser.ts | 17 +- packages/plugins/import/README.md | 213 +++++++++++++++++- packages/plugins/import/README.zh-CN.md | 213 +++++++++++++++++- .../plugins/import/src/client/useFields.ts | 12 +- .../import/src/client/useImportAction.ts | 12 +- 17 files changed, 916 insertions(+), 83 deletions(-) create mode 100644 packages/core/database/src/__tests__/value-parsers/base.test.ts create mode 100644 packages/core/database/src/__tests__/value-parsers/date.test.ts create mode 100644 packages/core/database/src/__tests__/value-parsers/to-many.test.ts create mode 100644 packages/core/database/src/__tests__/value-parsers/to-one.test.ts diff --git a/packages/core/database/package.json b/packages/core/database/package.json index b299a6cb1..d5ba4a972 100644 --- a/packages/core/database/package.json +++ b/packages/core/database/package.json @@ -10,6 +10,7 @@ "async-mutex": "^0.3.2", "cron-parser": "4.4.0", "deepmerge": "^4.2.2", + "excel-date-to-js": "^1.1.5", "flat": "^5.0.2", "glob": "^7.1.6", "mathjs": "^10.6.1", diff --git a/packages/core/database/src/__tests__/value-parsers/base.test.ts b/packages/core/database/src/__tests__/value-parsers/base.test.ts new file mode 100644 index 000000000..e4bfee23f --- /dev/null +++ b/packages/core/database/src/__tests__/value-parsers/base.test.ts @@ -0,0 +1,20 @@ +import { BaseValueParser as ValueParser } from '../../value-parsers'; + +describe('number value parser', () => { + let parser: ValueParser; + + beforeEach(() => { + parser = new ValueParser({}, {}); + }); + + it('should be converted to an array', () => { + expect(parser.toArr('A/B', '/')).toEqual(['A', 'B']); + expect(parser.toArr('A,B')).toEqual(['A', 'B']); + expect(parser.toArr('A, B')).toEqual(['A', 'B']); + expect(parser.toArr('A, B')).toEqual(['A', 'B']); + expect(parser.toArr('A, B ')).toEqual(['A', 'B']); + expect(parser.toArr('A, B ')).toEqual(['A', 'B']); + expect(parser.toArr('A、 B')).toEqual(['A', 'B']); + expect(parser.toArr('A ,, B')).toEqual(['A', 'B']); + }); +}); diff --git a/packages/core/database/src/__tests__/value-parsers/date.test.ts b/packages/core/database/src/__tests__/value-parsers/date.test.ts new file mode 100644 index 000000000..58c30f088 --- /dev/null +++ b/packages/core/database/src/__tests__/value-parsers/date.test.ts @@ -0,0 +1,67 @@ +import moment from 'moment'; +import { Database, mockDatabase } from '../..'; +import { DateValueParser } from '../../value-parsers'; + +describe('number value parser', () => { + let parser: DateValueParser; + let db: Database; + + beforeEach(() => { + db = mockDatabase(); + db.collection({ + name: 'tests', + fields: [ + { + name: 'date', + type: 'date', + }, + { + name: 'dateOnly', + type: 'date', + uiSchema: { + ['x-component-props']: { + showTime: false, + gmt: false, + }, + }, + }, + { + name: 'dateTime', + type: 'date', + uiSchema: { + ['x-component-props']: { + showTime: true, + }, + }, + }, + { + name: 'dateTimeGmt', + type: 'date', + uiSchema: { + ['x-component-props']: { + showTime: true, + gmt: true, + }, + }, + }, + ], + }); + parser = new DateValueParser({}, {}); + }); + + const expectValue = (value, field = 'date') => { + const collection = db.getCollection('tests'); + parser = new DateValueParser(collection.getField(field), {}); + parser.setValue(value); + return expect(parser.getValue()); + }; + + it('should be correct', () => { + expectValue(42510).toBe('2016-05-20T00:00:00.000Z'); + expectValue('42510').toBe('2016-05-20T00:00:00.000Z'); + expectValue('2016-05-20T00:00:00.000Z').toBe('2016-05-20T00:00:00.000Z'); + expectValue('2016-05-20 04:22:22', 'dateOnly').toBe('2016-05-20T00:00:00.000Z'); + expectValue('2016-05-20 01:00:00', 'dateTime').toBe(moment('2016-05-20 01:00:00').toISOString()); + expectValue('2016-05-20 01:00:00', 'dateTimeGmt').toBe('2016-05-20T01:00:00.000Z'); + }); +}); diff --git a/packages/core/database/src/__tests__/value-parsers/to-many.test.ts b/packages/core/database/src/__tests__/value-parsers/to-many.test.ts new file mode 100644 index 000000000..3de642236 --- /dev/null +++ b/packages/core/database/src/__tests__/value-parsers/to-many.test.ts @@ -0,0 +1,206 @@ +import { Database, mockDatabase } from '../..'; +import { ToManyValueParser } from '../../value-parsers'; + +describe('number value parser', () => { + let parser: ToManyValueParser; + let db: Database; + + beforeEach(async () => { + db = mockDatabase(); + db.collection({ + name: 'posts', + fields: [ + { + type: 'belongsToMany', + name: 'tags', + }, + { + type: 'belongsToMany', + name: 'attachments', + interface: 'attachment', + }, + ], + }); + db.collection({ + name: 'attachments', + fields: [], + }); + db.collection({ + name: 'tags', + fields: [ + { + type: 'string', + name: 'name', + }, + ], + }); + await db.sync(); + const tag = db.getRepository('tags'); + await tag.create({ + values: { name: 'tag1' }, + }); + }); + + afterEach(async () => { + await db.close(); + }); + + const setValue = async (value) => { + const post = db.getCollection('posts'); + parser = new ToManyValueParser(post.getField('tags'), { + column: { + dataIndex: ['tags', 'name'], + }, + }); + await parser.setValue(value); + }; + + const setAttachment = async (value) => { + const post = db.getCollection('posts'); + parser = new ToManyValueParser(post.getField('attachments'), {}); + await parser.setValue(value); + }; + + it('should be [1]', async () => { + await setValue('tag1'); + expect(parser.errors.length).toBe(0); + expect(parser.getValue()).toEqual([1]); + }); + + it('should be null', async () => { + await setValue('tag2'); + expect(parser.errors.length).toBe(1); + expect(parser.getValue()).toBeNull(); + }); + + it('should be attachment', async () => { + await setAttachment('https://www.nocobase.com/images/logo.png'); + expect(parser.errors.length).toBe(0); + expect(parser.getValue()).toMatchObject([ + { + title: 'logo.png', + extname: '.png', + filename: 'logo.png', + url: 'https://www.nocobase.com/images/logo.png', + }, + ]); + }); +}); + +describe.only('china region', () => { + let parser: ToManyValueParser; + let db: Database; + + beforeEach(async () => { + db = mockDatabase(); + db.collection({ + name: 'users', + fields: [ + { + type: 'belongsToMany', + name: 'chinaRegion', + target: 'chinaRegions', + interface: 'chinaRegion', + targetKey: 'code', + sortBy: 'level', + }, + ], + }); + db.collection({ + name: 'chinaRegions', + autoGenId: false, + fields: [ + { + name: 'code', + type: 'string', + // unique: true, + primaryKey: true, + }, + { + name: 'name', + type: 'string', + }, + { + name: 'parent', + type: 'belongsTo', + target: 'chinaRegions', + targetKey: 'code', + foreignKey: 'parentCode', + }, + { + name: 'children', + type: 'hasMany', + target: 'chinaRegions', + sourceKey: 'code', + foreignKey: 'parentCode', + }, + { + name: 'level', + type: 'integer', + }, + ], + }); + await db.sync(); + const areas = require('china-division/dist/areas.json'); + const cities = require('china-division/dist/cities.json'); + const provinces = require('china-division/dist/provinces.json'); + const ChinaRegion = db.getModel('chinaRegions'); + await ChinaRegion.bulkCreate( + provinces.map((item) => ({ + code: item.code, + name: item.name, + level: 1, + })), + ); + await ChinaRegion.bulkCreate( + cities.map((item) => ({ + code: item.code, + name: item.name, + level: 2, + parentCode: item.provinceCode, + })), + ); + await ChinaRegion.bulkCreate( + areas.map((item) => ({ + code: item.code, + name: item.name, + level: 3, + parentCode: item.cityCode, + })), + ); + }); + + afterEach(async () => { + await db.close(); + }); + + const setValue = async (value) => { + const r = db.getCollection('users'); + parser = new ToManyValueParser(r.getField('chinaRegion'), {}); + await parser.setValue(value); + }; + + it('should be correct', async () => { + await setValue('北京市/市辖区'); + expect(parser.errors.length).toBe(0); + expect(parser.getValue()).toEqual(['11', '1101']); + + await setValue('北京市 / 市辖区'); + expect(parser.errors.length).toBe(0); + expect(parser.getValue()).toEqual(['11', '1101']); + + await setValue('天津市 / 市辖区'); + expect(parser.errors.length).toBe(0); + expect(parser.getValue()).toEqual(['12', '1201']); + }); + + it('should be null', async () => { + await setValue('北京市2 / 市辖区'); + expect(parser.errors.length).toBe(1); + expect(parser.getValue()).toBeNull(); + + await setValue('北京市 / 市辖区 2'); + expect(parser.errors.length).toBe(1); + expect(parser.getValue()).toBeNull(); + }); +}); diff --git a/packages/core/database/src/__tests__/value-parsers/to-one.test.ts b/packages/core/database/src/__tests__/value-parsers/to-one.test.ts new file mode 100644 index 000000000..14480a8eb --- /dev/null +++ b/packages/core/database/src/__tests__/value-parsers/to-one.test.ts @@ -0,0 +1,60 @@ +import { Database, mockDatabase } from '../..'; +import { ToManyValueParser } from '../../value-parsers'; + +describe('number value parser', () => { + let parser: ToManyValueParser; + let db: Database; + + beforeEach(async () => { + db = mockDatabase(); + db.collection({ + name: 'posts', + fields: [ + { + type: 'belongsTo', + name: 'user', + }, + ], + }); + db.collection({ + name: 'users', + fields: [ + { + type: 'string', + name: 'name', + }, + ], + }); + await db.sync(); + const r = db.getRepository('users'); + await r.create({ + values: { name: 'user1' }, + }); + }); + + afterEach(async () => { + await db.close(); + }); + + const setValue = async (value) => { + const post = db.getCollection('posts'); + parser = new ToManyValueParser(post.getField('user'), { + column: { + dataIndex: ['user', 'name'], + }, + }); + await parser.setValue(value); + }; + + it('should be correct', async () => { + await setValue('user1'); + expect(parser.errors.length).toBe(0); + expect(parser.getValue()).toEqual([1]); + }); + + it('should be null', async () => { + await setValue('user2'); + expect(parser.errors.length).toBe(1); + expect(parser.getValue()).toBeNull(); + }); +}); diff --git a/packages/core/database/src/value-parsers/array-value-parser.ts b/packages/core/database/src/value-parsers/array-value-parser.ts index 278010d38..ff68fdcbe 100644 --- a/packages/core/database/src/value-parsers/array-value-parser.ts +++ b/packages/core/database/src/value-parsers/array-value-parser.ts @@ -27,16 +27,4 @@ export class ArrayValueParser extends BaseValueParser { } return { map, set }; } - - toArr(value) { - let values: string[] = []; - if (!value) { - values = []; - } else if (typeof value === 'string') { - values = value.split(','); - } else if (Array.isArray(value)) { - values = value; - } - return values; - } } diff --git a/packages/core/database/src/value-parsers/base-value-parser.ts b/packages/core/database/src/value-parsers/base-value-parser.ts index b04e2afbb..d4cd7e948 100644 --- a/packages/core/database/src/value-parsers/base-value-parser.ts +++ b/packages/core/database/src/value-parsers/base-value-parser.ts @@ -10,6 +10,22 @@ export class BaseValueParser { this.value = null; } + trim(value: any) { + return typeof value === 'string' ? value.trim() : value; + } + + toArr(value: any, splitter?: string) { + let values: string[] = []; + if (!value) { + values = []; + } else if (typeof value === 'string') { + values = value.split(splitter || /,|,|、/); + } else if (Array.isArray(value)) { + values = value; + } + return values.map((v) => this.trim(v)).filter(Boolean); + } + toString() { return this.value; } diff --git a/packages/core/database/src/value-parsers/boolean-value-parser.ts b/packages/core/database/src/value-parsers/boolean-value-parser.ts index 45b680dd5..e84b51e8b 100644 --- a/packages/core/database/src/value-parsers/boolean-value-parser.ts +++ b/packages/core/database/src/value-parsers/boolean-value-parser.ts @@ -20,10 +20,10 @@ export class BooleanValueParser extends BaseValueParser { } else if (['0', 'n', 'no', 'false', '否'].includes(value.toLowerCase())) { this.value = false; } else { - this.errors.push(`Value invalid - ${JSON.stringify(this.value)}`); + this.errors.push(`Invalid value - ${JSON.stringify(this.value)}`); } } else { - this.errors.push(`Value invalid - ${JSON.stringify(this.value)}`); + this.errors.push(`Invalid value - ${JSON.stringify(this.value)}`); } } } diff --git a/packages/core/database/src/value-parsers/date-value-parser.ts b/packages/core/database/src/value-parsers/date-value-parser.ts index 32631e4ba..2169132e4 100644 --- a/packages/core/database/src/value-parsers/date-value-parser.ts +++ b/packages/core/database/src/value-parsers/date-value-parser.ts @@ -1,6 +1,13 @@ import { moment2str } from '@nocobase/utils'; +import { getJsDateFromExcel } from 'excel-date-to-js'; import moment, { isDate, isMoment } from 'moment'; -import { BaseValueParser } from "./base-value-parser"; +import { BaseValueParser } from './base-value-parser'; + +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 DateValueParser extends BaseValueParser { async setValue(value: any) { @@ -8,6 +15,12 @@ export class DateValueParser extends BaseValueParser { this.value = value; } else if (isDate(value)) { this.value = value; + } else if (isNumeric(value)) { + try { + this.value = getJsDateFromExcel(value).toISOString(); + } catch (error) { + this.errors.push(`Invalid date - ${error.message}`); + } } else if (typeof value === 'string') { const props = this.getProps(); const m = moment(value); diff --git a/packages/core/database/src/value-parsers/json-value-parser.ts b/packages/core/database/src/value-parsers/json-value-parser.ts index 792eb154b..14ed3e6c9 100644 --- a/packages/core/database/src/value-parsers/json-value-parser.ts +++ b/packages/core/database/src/value-parsers/json-value-parser.ts @@ -2,6 +2,18 @@ import { BaseValueParser } from './base-value-parser'; export class JsonValueParser extends BaseValueParser { async setValue(value: any) { - + if (typeof value === 'string') { + if (value.trim() === '') { + this.value = null; + } else { + try { + this.value = JSON.parse(value); + } catch (error) { + this.errors.push(error.message); + } + } + } else { + this.value = value; + } } } diff --git a/packages/core/database/src/value-parsers/number-value-parser.ts b/packages/core/database/src/value-parsers/number-value-parser.ts index acd013b1a..a82268b2d 100644 --- a/packages/core/database/src/value-parsers/number-value-parser.ts +++ b/packages/core/database/src/value-parsers/number-value-parser.ts @@ -17,13 +17,13 @@ export class NumberValueParser extends BaseValueParser { value = +value; } if (isNaN(value)) { - this.errors.push(`Value invalid - "${value}"`); + this.errors.push(`Invalid value - "${value}"`); } else { this.value = value; } } } else { - this.errors.push(`Value invalid - ${JSON.stringify(value)}`); + this.errors.push(`Invalid value - ${JSON.stringify(value)}`); } } } diff --git a/packages/core/database/src/value-parsers/to-many-value-parser.ts b/packages/core/database/src/value-parsers/to-many-value-parser.ts index 991cea895..af8f78fb9 100644 --- a/packages/core/database/src/value-parsers/to-many-value-parser.ts +++ b/packages/core/database/src/value-parsers/to-many-value-parser.ts @@ -1,54 +1,85 @@ +import { basename, extname } from 'path'; import { Repository } from '../repository'; import { BaseValueParser } from './base-value-parser'; export class ToManyValueParser extends BaseValueParser { - async setValue(value: any) { - const fieldNames = this.getFileNames(); - if (this.isInterface('chinaRegion')) { - const repository = this.field.database.getRepository(this.field.target) as Repository; - try { - this.value = await Promise.all( - value.split('/').map(async (v) => { - const instance = await repository.findOne({ filter: { [fieldNames.label]: v.trim() } }); - if (!instance) { - throw new Error(`"${v}" does not exist`); - } - return instance.get(fieldNames.value); - }), - ); - } catch (error) { - this.errors.push(error.message); + setAccessors = { + attachment: 'setAttachments', + chinaRegion: 'setChinaRegion', + }; + + async setAttachments(value: any) { + this.value = this.toArr(value).map((url: string) => { + return { + title: basename(url), + extname: extname(url), + filename: basename(url), + url, + }; + }); + } + + async setChinaRegion(value: any) { + const repository = this.field.database.getRepository(this.field.target) as Repository; + try { + const values = []; + const names = this.toArr(value, '/'); + let parentCode = null; + for (const name of names) { + const instance = await repository.findOne({ + filter: { + name: name.trim(), + parentCode, + }, + }); + if (!instance) { + throw new Error(`"${value}" does not exist`); + } + parentCode = instance.get('code'); + values.push(parentCode); } - } else { - const dataIndex = this.ctx?.column?.dataIndex || []; - if (Array.isArray(dataIndex) && dataIndex.length < 2) { - this.errors.push(`data index invalid`); - return; - } - const key = this.ctx.column.dataIndex[1]; - const repository = this.field.database.getRepository(this.field.target) as Repository; - try { - this.value = await Promise.all( - value.split(',').map(async (v) => { - const instance = await repository.findOne({ filter: { [key]: v.trim() } }); - if (!instance) { - throw new Error(`"${v}" does not exist`); - } - return instance.get(fieldNames.value); - }), - ); - } catch (error) { - this.errors.push(error.message); + if (values.length !== names.length) { + throw new Error(`"${value}" does not exist`); } + this.value = values; + } catch (error) { + this.errors.push(error.message); } } - getFileNames() { - const fieldNames = this.field.options?.uiSchema?.['x-component-props']?.['fieldNames'] || {}; - return { label: 'id', value: 'id', ...fieldNames }; + async setAssociations(value: any) { + const dataIndex = this.ctx?.column?.dataIndex || []; + if (Array.isArray(dataIndex) && dataIndex.length < 2) { + this.errors.push(`data index invalid`); + return; + } + const key = this.ctx.column.dataIndex[1]; + const repository = this.field.database.getRepository(this.field.target) as Repository; + try { + this.value = await Promise.all( + this.toArr(value).map(async (v) => { + const instance = await repository.findOne({ filter: { [key]: v } }); + if (!instance) { + throw new Error(`"${v}" does not exist`); + } + return instance.get(this.field.targetKey || 'id'); + }), + ); + } catch (error) { + this.errors.push(error.message); + } } - isInterface(name) { - return this.field.options.interface === name; + async setValue(value: any) { + const setAccessor = this.setAccessors[this.getInterface()] || 'setAssociations'; + await this[setAccessor](value); + } + + getInterface() { + return this.field?.options?.interface as string; + } + + isInterface(name: string) { + return this.getInterface() === name; } } diff --git a/packages/core/database/src/value-parsers/to-one-value-parser.ts b/packages/core/database/src/value-parsers/to-one-value-parser.ts index 0585dd84e..f5f4fa17d 100644 --- a/packages/core/database/src/value-parsers/to-one-value-parser.ts +++ b/packages/core/database/src/value-parsers/to-one-value-parser.ts @@ -3,27 +3,18 @@ import { BaseValueParser } from './base-value-parser'; export class ToOneValueParser extends BaseValueParser { async setValue(value: any) { - const fieldNames = this.getFileNames(); const dataIndex = this.ctx?.column?.dataIndex || []; if (Array.isArray(dataIndex) && dataIndex.length < 2) { + this.errors.push(`data index invalid`); return; } - const field = this.ctx.column.dataIndex[1]; + const key = this.ctx.column.dataIndex[1]; const repository = this.field.database.getRepository(this.field.target) as Repository; - const instance = await repository.findOne({ filter: { [field]: value.trim() } }); + const instance = await repository.findOne({ filter: { [key]: this.trim(value) } }); if (instance) { - this.value = instance.get(fieldNames.value); + this.value = instance.get(this.field.targetKey || 'id'); } else { this.errors.push(`"${value}" does not exist`); } } - - getFileNames() { - const fieldNames = this.field.options?.uiSchema?.['x-component-props']?.['fieldNames'] || {}; - return { label: 'id', value: 'id', ...fieldNames }; - } - - isInterface(name) { - return this.field.options.interface === name; - } } diff --git a/packages/plugins/import/README.md b/packages/plugins/import/README.md index 938722f9e..858ebdfe5 100644 --- a/packages/plugins/import/README.md +++ b/packages/plugins/import/README.md @@ -8,4 +8,215 @@ Excel 数据导入插件。 内置插件无需手动安装激活。 -## 使用方法 +## 导入说明 + +### 数字类型字段 + +支持数字和百分比,`N/A` 或 `-` 的文案会被过滤掉 + +| 数字1 | 百分比 | 数字2 | 数字3 | +| -- | -- | -- | -- | +| 123 | 25% | N/A | - | + +转 JSON 之后为 + +```ts +{ + "数字1": 123, + "百分比": 0.25, + "数字2": null, + "数字3": null, +} +``` + +### 布尔类型字段 + +输入文案支持(英文不区分大小写): + +- `Yes` `Y` `True` `1` `是` +- `No` `N` `False` `0` `否` + +| 字段1 | 字段2 | 字段3 | 字段4 | 字段4 | +| -- | -- | -- | -- | -- | +| 否 | 是 | Y | true | 0 | + +转 JSON 之后为 + +```ts +{ + "字段1": false, + "字段2": true, + "字段3": true, + "字段4": true, + "字段5": false, +} +``` + +### 日期类型字段 + +| DateOnly | Local(+08:00) | GMT | +| -- | -- | -- | +| 2023-01-18 22:22:22 | 2023-01-18 22:22:22 | 2023-01-18 22:22:22 | + +转 JSON 之后为 + +```ts +{ + "DateOnly": "2023-01-18T00:00:00.000Z", + "Local(+08:00)": "2023-01-18T14:22:22.000Z", + "GMT": "2023-01-18T22:22:22.000Z", +} +``` + +### 选择类型字段 + +选项值和选项标签都可作为导入文案,多个选项之间以以逗号(`,` `,`)或顿号(`、`)区分 + +如字段 `优先级` 的可选项包括: + +| 选项值 | 选项标签 | +| -- | -- | +| low | 低 | +| medium | 中 | +| high | 低 | + +选项值和选项标签都可作为导入文案 + +| 优先级 | +| -- | +| 高 | +| low | + +转 JSON 之后为 + +```ts +[ + { "优先级": "high" }, + { "优先级": "low" }, +] +``` + +### 中国行政区字段 + +| 地区1 | 地区2 | +| -- | -- | +| 北京市/市辖区 | 天津市/市辖区 | + +转 JSON 之后为 + +```ts +{ + "地区1": ["11","1101"], + "地区2": ["12","1201"] +} +``` + +### 附件字段 + +| 附件 | +| --| +| https://www.nocobase.com/images/logo.png | + +转 JSON 之后为 + +```ts +{ + "附件": [ + { + "filename": "logo.png", + "title": "logo.png", + "extname": ".png", + "url": "https://www.nocobase.com/images/logo.png" + } + ] +} +``` + +### 关系类型字段 + +多条数据以逗号(`,` `,`)或顿号(`、`)区分 + +| 部门/名称 | 分类/标题 | +| -- | -- | +| 开发组 | 分类1、分类2 | + +转 JSON 之后为 + +```ts +{ + "部门": [1], // 1 为部门名称为「开发组」的记录 ID + "分类": [1,2], // 1,2 为分类标题为「分类1」和「分类2」的记录 ID +} +``` + +### JSON 类型字段 + +| JSON1 | +| -- | +| {"key":"value"} | + +转 JSON 之后为 + +```ts +{ + "JSON": {"key":"value"} +} +``` + +### 地图几何图形类型 + +| Point | Line | Polygon | Circle | +| -- | -- | -- | -- | +| 1,2 | (1,2),(3,4) | (1,2),(3,4),(1,2) | 1,2,3 | + +转 JSON 之后为 + +```ts +{ + "Point": [1,2], + "Line": [[1,2], [3,4]], + "Polygon": [[1,2], [3,4], [1,2]], + "Circle": [1,2,3] +} +``` + +## 自定义导入格式 + +通过 `db.registerFieldValueParsers()` 方法注册自定义的 `ValueParser`,如: + +```ts +import { BaseValueParser } from '@nocobase/database'; + +class PointValueParser extends BaseValueParser { + async setValue(value) { + if (Array.isArray(value)) { + this.value = value; + } else if (typeof value === 'string') { + this.value = value.split(','); + } else { + this.errors.push('Value invalid'); + } + } +} + +const db = new Database(); + +// type=point 的字段导入时,将通过 PointValueParser 解析数据 +db.registerFieldValueParsers({ + point: PointValueParser, +}); +``` + +导入示例 + +| Point | +| --| +| 1,2 | + +转 JSON 之后为 + +```ts +{ + "Point": [1,2] +} +``` diff --git a/packages/plugins/import/README.zh-CN.md b/packages/plugins/import/README.zh-CN.md index ef02d799c..68c6546c7 100644 --- a/packages/plugins/import/README.zh-CN.md +++ b/packages/plugins/import/README.zh-CN.md @@ -8,4 +8,215 @@ Excel 数据导入插件。 内置插件无需手动安装激活。 -## 使用方法 +## 导入说明 + +### 数字类型字段 + +支持数字和百分比,`N/A` 或 `-` 的文案会被过滤掉 + +| 数字1 | 百分比 | 数字2 | 数字3 | +| -- | -- | -- | -- | +| 123 | 25% | N/A | - | + +转 JSON 之后为 + +```ts +{ + "数字1": 123, + "百分比": 0.25, + "数字2": null, + "数字3": null, +} +``` + +### 布尔类型字段 + +输入文案支持(英文不区分大小写): + +- `Yes` `Y` `True` `1` `是` +- `No` `N` `False` `0` `否` + +| 字段1 | 字段2 | 字段3 | 字段4 | 字段4 | +| -- | -- | -- | -- | -- | +| 否 | 是 | Y | true | 0 | + +转 JSON 之后为 + +```ts +{ + "字段1": false, + "字段2": true, + "字段3": true, + "字段4": true, + "字段5": false, +} +``` + +### 日期类型字段 + +| DateOnly | Local(+08:00) | GMT | +| -- | -- | -- | +| 2023-01-18 22:22:22 | 2023-01-18 22:22:22 | 2023-01-18 22:22:22 | + +转 JSON 之后为 + +```ts +{ + "DateOnly": "2023-01-18T00:00:00.000Z", + "Local(+08:00)": "2023-01-18T14:22:22.000Z", + "GMT": "2023-01-18T22:22:22.000Z", +} +``` + +### 选择类型字段 + +选项值和选项标签都可作为导入文案,多个选项之间以以逗号(`,` `,`)或顿号(`、`)区分 + +如字段 `优先级` 的可选项包括: + +| 选项值 | 选项标签 | +| -- | -- | +| low | 低 | +| medium | 中 | +| high | 低 | + +选项值和选项标签都可作为导入文案 + +| 优先级 | +| -- | +| 高 | +| low | + +转 JSON 之后为 + +```ts +[ + { "优先级": "high" }, + { "优先级": "low" }, +] +``` + +### 中国行政区字段 + +| 地区1 | 地区2 | +| -- | -- | +| 北京市/市辖区 | 天津市/市辖区 | + +转 JSON 之后为 + +```ts +{ + "地区1": ["11","1101"], + "地区2": ["12","1201"] +} +``` + +### 附件字段 + +| 附件 | +| --| +| https://www.nocobase.com/images/logo.png | + +转 JSON 之后为 + +```ts +{ + "附件": [ + { + "filename": "logo.png", + "title": "logo.png", + "extname": ".png", + "url": "https://www.nocobase.com/images/logo.png" + } + ] +} +``` + +### 关系类型字段 + +多条数据以逗号(`,` `,`)或顿号(`、`)区分 + +| 部门/名称 | 分类/标题 | +| -- | -- | +| 开发组 | 分类1、分类2 | + +转 JSON 之后为 + +```ts +{ + "部门": [1], // 1 为部门名称为「开发组」的记录 ID + "分类": [1,2], // 1,2 为分类标题为「分类1」和「分类2」的记录 ID +} +``` + +### JSON 类型字段 + +| JSON1 | +| -- | +| {"key":"value"} | + +转 JSON 之后为 + +```ts +{ + "JSON": {"key":"value"} +} +``` + +### 地图几何图形类型 + +| Point | Line | Polygon | Circle | +| -- | -- | -- | -- | +| 1,2 | (1,2),(3,4) | (1,2),(3,4),(1,2) | 1,2,3 | + +转 JSON 之后为 + +```ts +{ + "Point": [1,2], + "Line": [[1,2], [3,4]], + "Polygon": [[1,2], [3,4], [1,2]], + "Circle": [1,2,3] +} +``` + +## 自定义导入格式 + +通过 `db.registerFieldValueParsers()` 方法注册自定义的 `ValueParser`,如: + +```ts +import { BaseValueParser } from '@nocobase/database'; + +class PointValueParser extends BaseValueParser { + async setValue(value) { + if (Array.isArray(value)) { + this.value = value; + } else if (typeof value === 'string') { + this.value = value.split(','); + } else { + this.errors.push('Value invalid'); + } + } +} + +const db = new Database(); + +// type=point 的字段导入时,将通过 PointValueParser 解析数据 +db.registerFieldValueParsers({ + point: PointValueParser, +}); +``` + +导入示例 + +| Point | +| --| +| 1,2 | + +转 JSON 之后为 + +```ts +{ + "Point": [1,2] +} +``` diff --git a/packages/plugins/import/src/client/useFields.ts b/packages/plugins/import/src/client/useFields.ts index a4cd3a10a..95895b162 100644 --- a/packages/plugins/import/src/client/useFields.ts +++ b/packages/plugins/import/src/client/useFields.ts @@ -1,17 +1,17 @@ import { useCollectionManager } from '@nocobase/client'; const EXCLUDE_INTERFACES = [ - 'icon', - 'formula', - 'attachment', - 'markdown', - 'richText', + // 'icon', + // 'formula', + // 'attachment', + // 'markdown', + // 'richText', 'id', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy', - 'sequence', + // 'sequence', ]; export const useFields = (collectionName: string) => { diff --git a/packages/plugins/import/src/client/useImportAction.ts b/packages/plugins/import/src/client/useImportAction.ts index f8ddf27bb..d7d2cd867 100644 --- a/packages/plugins/import/src/client/useImportAction.ts +++ b/packages/plugins/import/src/client/useImportAction.ts @@ -22,6 +22,13 @@ const useImportSchema = (s: Schema) => { return { schema }; }; +const toArr = (v: any) => { + if (!v || !Array.isArray(v)) { + return []; + } + return v; +}; + export const useDownloadXlsxTemplateAction = () => { const { service, resource } = useBlockRequestContext(); const apiClient = useAPIClient(); @@ -34,7 +41,7 @@ export const useDownloadXlsxTemplateAction = () => { return { async run() { const { importColumns, explain } = cloneDeep(importSchema?.['x-action-settings']?.['importSettings'] ?? {}); - const columns = importColumns + const columns = toArr(importColumns) .map((column) => { const field = getCollectionField(`${name}.${column.dataIndex[0]}`); if (!field) { @@ -88,7 +95,7 @@ export const useImportStartAction = () => { return { async run() { const { importColumns, explain } = cloneDeep(importSchema?.['x-action-settings']?.['importSettings'] ?? {}); - const columns = importColumns + const columns = toArr(importColumns) .map((column) => { const field = getCollectionField(`${name}.${column.dataIndex[0]}`); if (!field) { @@ -110,7 +117,6 @@ export const useImportStartAction = () => { .filter(Boolean); let formData = new FormData(); const uploadFiles = form.values.upload.map((f) => f.originFileObj); - console.log(form, uploadFiles); formData.append('file', uploadFiles[0]); formData.append('columns', JSON.stringify(columns)); formData.append('explain', explain);