feat: support for importing attachments (#1466)
This commit is contained in:
		
							parent
							
								
									01663da7ec
								
							
						
					
					
						commit
						5421686504
					
				@ -10,6 +10,7 @@
 | 
				
			|||||||
    "async-mutex": "^0.3.2",
 | 
					    "async-mutex": "^0.3.2",
 | 
				
			||||||
    "cron-parser": "4.4.0",
 | 
					    "cron-parser": "4.4.0",
 | 
				
			||||||
    "deepmerge": "^4.2.2",
 | 
					    "deepmerge": "^4.2.2",
 | 
				
			||||||
 | 
					    "excel-date-to-js": "^1.1.5",
 | 
				
			||||||
    "flat": "^5.0.2",
 | 
					    "flat": "^5.0.2",
 | 
				
			||||||
    "glob": "^7.1.6",
 | 
					    "glob": "^7.1.6",
 | 
				
			||||||
    "mathjs": "^10.6.1",
 | 
					    "mathjs": "^10.6.1",
 | 
				
			||||||
 | 
				
			|||||||
@ -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']);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -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');
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -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();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -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();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -27,16 +27,4 @@ export class ArrayValueParser extends BaseValueParser {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    return { map, set };
 | 
					    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;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,22 @@ export class BaseValueParser {
 | 
				
			|||||||
    this.value = null;
 | 
					    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() {
 | 
					  toString() {
 | 
				
			||||||
    return this.value;
 | 
					    return this.value;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -20,10 +20,10 @@ export class BooleanValueParser extends BaseValueParser {
 | 
				
			|||||||
      } else if (['0', 'n', 'no', 'false', '否'].includes(value.toLowerCase())) {
 | 
					      } else if (['0', 'n', 'no', 'false', '否'].includes(value.toLowerCase())) {
 | 
				
			||||||
        this.value = false;
 | 
					        this.value = false;
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        this.errors.push(`Value invalid - ${JSON.stringify(this.value)}`);
 | 
					        this.errors.push(`Invalid value - ${JSON.stringify(this.value)}`);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.errors.push(`Value invalid - ${JSON.stringify(this.value)}`);
 | 
					      this.errors.push(`Invalid value - ${JSON.stringify(this.value)}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,13 @@
 | 
				
			|||||||
import { moment2str } from '@nocobase/utils';
 | 
					import { moment2str } from '@nocobase/utils';
 | 
				
			||||||
 | 
					import { getJsDateFromExcel } from 'excel-date-to-js';
 | 
				
			||||||
import moment, { isDate, isMoment } from 'moment';
 | 
					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 {
 | 
					export class DateValueParser extends BaseValueParser {
 | 
				
			||||||
  async setValue(value: any) {
 | 
					  async setValue(value: any) {
 | 
				
			||||||
@ -8,6 +15,12 @@ export class DateValueParser extends BaseValueParser {
 | 
				
			|||||||
      this.value = value;
 | 
					      this.value = value;
 | 
				
			||||||
    } else if (isDate(value)) {
 | 
					    } else if (isDate(value)) {
 | 
				
			||||||
      this.value = 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') {
 | 
					    } else if (typeof value === 'string') {
 | 
				
			||||||
      const props = this.getProps();
 | 
					      const props = this.getProps();
 | 
				
			||||||
      const m = moment(value);
 | 
					      const m = moment(value);
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,18 @@ import { BaseValueParser } from './base-value-parser';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export class JsonValueParser extends BaseValueParser {
 | 
					export class JsonValueParser extends BaseValueParser {
 | 
				
			||||||
  async setValue(value: any) {
 | 
					  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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -17,13 +17,13 @@ export class NumberValueParser extends BaseValueParser {
 | 
				
			|||||||
          value = +value;
 | 
					          value = +value;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (isNaN(value)) {
 | 
					        if (isNaN(value)) {
 | 
				
			||||||
          this.errors.push(`Value invalid - "${value}"`);
 | 
					          this.errors.push(`Invalid value - "${value}"`);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          this.value = value;
 | 
					          this.value = value;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.errors.push(`Value invalid - ${JSON.stringify(value)}`);
 | 
					      this.errors.push(`Invalid value - ${JSON.stringify(value)}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,54 +1,85 @@
 | 
				
			|||||||
 | 
					import { basename, extname } from 'path';
 | 
				
			||||||
import { Repository } from '../repository';
 | 
					import { Repository } from '../repository';
 | 
				
			||||||
import { BaseValueParser } from './base-value-parser';
 | 
					import { BaseValueParser } from './base-value-parser';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ToManyValueParser extends BaseValueParser {
 | 
					export class ToManyValueParser extends BaseValueParser {
 | 
				
			||||||
  async setValue(value: any) {
 | 
					  setAccessors = {
 | 
				
			||||||
    const fieldNames = this.getFileNames();
 | 
					    attachment: 'setAttachments',
 | 
				
			||||||
    if (this.isInterface('chinaRegion')) {
 | 
					    chinaRegion: 'setChinaRegion',
 | 
				
			||||||
      const repository = this.field.database.getRepository(this.field.target) as Repository;
 | 
					  };
 | 
				
			||||||
      try {
 | 
					
 | 
				
			||||||
        this.value = await Promise.all(
 | 
					  async setAttachments(value: any) {
 | 
				
			||||||
          value.split('/').map(async (v) => {
 | 
					    this.value = this.toArr(value).map((url: string) => {
 | 
				
			||||||
            const instance = await repository.findOne({ filter: { [fieldNames.label]: v.trim() } });
 | 
					      return {
 | 
				
			||||||
            if (!instance) {
 | 
					        title: basename(url),
 | 
				
			||||||
              throw new Error(`"${v}" does not exist`);
 | 
					        extname: extname(url),
 | 
				
			||||||
            }
 | 
					        filename: basename(url),
 | 
				
			||||||
            return instance.get(fieldNames.value);
 | 
					        url,
 | 
				
			||||||
          }),
 | 
					      };
 | 
				
			||||||
        );
 | 
					    });
 | 
				
			||||||
      } catch (error) {
 | 
					  }
 | 
				
			||||||
        this.errors.push(error.message);
 | 
					
 | 
				
			||||||
 | 
					  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 {
 | 
					      if (values.length !== names.length) {
 | 
				
			||||||
      const dataIndex = this.ctx?.column?.dataIndex || [];
 | 
					        throw new Error(`"${value}" does not exist`);
 | 
				
			||||||
      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);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      this.value = values;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      this.errors.push(error.message);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getFileNames() {
 | 
					  async setAssociations(value: any) {
 | 
				
			||||||
    const fieldNames = this.field.options?.uiSchema?.['x-component-props']?.['fieldNames'] || {};
 | 
					    const dataIndex = this.ctx?.column?.dataIndex || [];
 | 
				
			||||||
    return { label: 'id', value: 'id', ...fieldNames };
 | 
					    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) {
 | 
					  async setValue(value: any) {
 | 
				
			||||||
    return this.field.options.interface === name;
 | 
					    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;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -3,27 +3,18 @@ import { BaseValueParser } from './base-value-parser';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export class ToOneValueParser extends BaseValueParser {
 | 
					export class ToOneValueParser extends BaseValueParser {
 | 
				
			||||||
  async setValue(value: any) {
 | 
					  async setValue(value: any) {
 | 
				
			||||||
    const fieldNames = this.getFileNames();
 | 
					 | 
				
			||||||
    const dataIndex = this.ctx?.column?.dataIndex || [];
 | 
					    const dataIndex = this.ctx?.column?.dataIndex || [];
 | 
				
			||||||
    if (Array.isArray(dataIndex) && dataIndex.length < 2) {
 | 
					    if (Array.isArray(dataIndex) && dataIndex.length < 2) {
 | 
				
			||||||
 | 
					      this.errors.push(`data index invalid`);
 | 
				
			||||||
      return;
 | 
					      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 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) {
 | 
					    if (instance) {
 | 
				
			||||||
      this.value = instance.get(fieldNames.value);
 | 
					      this.value = instance.get(this.field.targetKey || 'id');
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.errors.push(`"${value}" does not exist`);
 | 
					      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;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
				
			|||||||
@ -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]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
				
			|||||||
@ -1,17 +1,17 @@
 | 
				
			|||||||
import { useCollectionManager } from '@nocobase/client';
 | 
					import { useCollectionManager } from '@nocobase/client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const EXCLUDE_INTERFACES = [
 | 
					const EXCLUDE_INTERFACES = [
 | 
				
			||||||
  'icon',
 | 
					  // 'icon',
 | 
				
			||||||
  'formula',
 | 
					  // 'formula',
 | 
				
			||||||
  'attachment',
 | 
					  // 'attachment',
 | 
				
			||||||
  'markdown',
 | 
					  // 'markdown',
 | 
				
			||||||
  'richText',
 | 
					  // 'richText',
 | 
				
			||||||
  'id',
 | 
					  'id',
 | 
				
			||||||
  'createdAt',
 | 
					  'createdAt',
 | 
				
			||||||
  'createdBy',
 | 
					  'createdBy',
 | 
				
			||||||
  'updatedAt',
 | 
					  'updatedAt',
 | 
				
			||||||
  'updatedBy',
 | 
					  'updatedBy',
 | 
				
			||||||
  'sequence',
 | 
					  // 'sequence',
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useFields = (collectionName: string) => {
 | 
					export const useFields = (collectionName: string) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -22,6 +22,13 @@ const useImportSchema = (s: Schema) => {
 | 
				
			|||||||
  return { schema };
 | 
					  return { schema };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const toArr = (v: any) => {
 | 
				
			||||||
 | 
					  if (!v || !Array.isArray(v)) {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return v;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useDownloadXlsxTemplateAction = () => {
 | 
					export const useDownloadXlsxTemplateAction = () => {
 | 
				
			||||||
  const { service, resource } = useBlockRequestContext();
 | 
					  const { service, resource } = useBlockRequestContext();
 | 
				
			||||||
  const apiClient = useAPIClient();
 | 
					  const apiClient = useAPIClient();
 | 
				
			||||||
@ -34,7 +41,7 @@ export const useDownloadXlsxTemplateAction = () => {
 | 
				
			|||||||
  return {
 | 
					  return {
 | 
				
			||||||
    async run() {
 | 
					    async run() {
 | 
				
			||||||
      const { importColumns, explain } = cloneDeep(importSchema?.['x-action-settings']?.['importSettings'] ?? {});
 | 
					      const { importColumns, explain } = cloneDeep(importSchema?.['x-action-settings']?.['importSettings'] ?? {});
 | 
				
			||||||
      const columns = importColumns
 | 
					      const columns = toArr(importColumns)
 | 
				
			||||||
        .map((column) => {
 | 
					        .map((column) => {
 | 
				
			||||||
          const field = getCollectionField(`${name}.${column.dataIndex[0]}`);
 | 
					          const field = getCollectionField(`${name}.${column.dataIndex[0]}`);
 | 
				
			||||||
          if (!field) {
 | 
					          if (!field) {
 | 
				
			||||||
@ -88,7 +95,7 @@ export const useImportStartAction = () => {
 | 
				
			|||||||
  return {
 | 
					  return {
 | 
				
			||||||
    async run() {
 | 
					    async run() {
 | 
				
			||||||
      const { importColumns, explain } = cloneDeep(importSchema?.['x-action-settings']?.['importSettings'] ?? {});
 | 
					      const { importColumns, explain } = cloneDeep(importSchema?.['x-action-settings']?.['importSettings'] ?? {});
 | 
				
			||||||
      const columns = importColumns
 | 
					      const columns = toArr(importColumns)
 | 
				
			||||||
        .map((column) => {
 | 
					        .map((column) => {
 | 
				
			||||||
          const field = getCollectionField(`${name}.${column.dataIndex[0]}`);
 | 
					          const field = getCollectionField(`${name}.${column.dataIndex[0]}`);
 | 
				
			||||||
          if (!field) {
 | 
					          if (!field) {
 | 
				
			||||||
@ -110,7 +117,6 @@ export const useImportStartAction = () => {
 | 
				
			|||||||
        .filter(Boolean);
 | 
					        .filter(Boolean);
 | 
				
			||||||
      let formData = new FormData();
 | 
					      let formData = new FormData();
 | 
				
			||||||
      const uploadFiles = form.values.upload.map((f) => f.originFileObj);
 | 
					      const uploadFiles = form.values.upload.map((f) => f.originFileObj);
 | 
				
			||||||
      console.log(form, uploadFiles);
 | 
					 | 
				
			||||||
      formData.append('file', uploadFiles[0]);
 | 
					      formData.append('file', uploadFiles[0]);
 | 
				
			||||||
      formData.append('columns', JSON.stringify(columns));
 | 
					      formData.append('columns', JSON.stringify(columns));
 | 
				
			||||||
      formData.append('explain', explain);
 | 
					      formData.append('explain', explain);
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user