refactor: export plugin (#1460)
* refactor: export plugin * fix: improve code
This commit is contained in:
parent
2f8954b70f
commit
a6aec25343
@ -216,7 +216,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// register database field types
|
// register database field types
|
||||||
for (const [name, field] of Object.entries(FieldTypes)) {
|
for (const [name, field] of Object.entries<any>(FieldTypes)) {
|
||||||
if (['Field', 'RelationField'].includes(name)) {
|
if (['Field', 'RelationField'].includes(name)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { BaseColumnFieldOptions, Field } from './field';
|
|
||||||
import { DataTypes } from 'sequelize';
|
import { DataTypes } from 'sequelize';
|
||||||
|
import { BaseColumnFieldOptions, Field, ValueParser } from './field';
|
||||||
|
|
||||||
export class ArrayField extends Field {
|
export class ArrayField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
@ -28,6 +28,51 @@ export class ArrayField extends Field {
|
|||||||
super.unbind();
|
super.unbind();
|
||||||
this.off('beforeSave', this.sortValue);
|
this.off('beforeSave', this.sortValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildValueParser(ctx: any) {
|
||||||
|
return new ArrayValueParser(this, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ArrayValueParser extends ValueParser {
|
||||||
|
async setValue(value: any) {
|
||||||
|
const { map, set } = this.getOptions();
|
||||||
|
const values = this.toArr(value);
|
||||||
|
if (set.size > 0) {
|
||||||
|
const filtered = values.map((v) => (map.has(v) ? map.get(v) : v)).filter((v) => set.has(v));
|
||||||
|
if (values.length === filtered.length) {
|
||||||
|
this.value = filtered;
|
||||||
|
} else {
|
||||||
|
this.errors.push('No matching option found');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.value = values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptions() {
|
||||||
|
const options = this.field.options?.['uiSchema']?.enum || [];
|
||||||
|
const map = new Map();
|
||||||
|
const set = new Set();
|
||||||
|
for (const option of options) {
|
||||||
|
set.add(option.value);
|
||||||
|
set.add(option.label);
|
||||||
|
map.set(option.label, option.value);
|
||||||
|
}
|
||||||
|
return { map, set };
|
||||||
|
}
|
||||||
|
|
||||||
|
toArr(value) {
|
||||||
|
let values = [];
|
||||||
|
if (!value) {
|
||||||
|
values = [];
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
values = value.split(',');
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
values = value;
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArrayFieldOptions extends BaseColumnFieldOptions {
|
export interface ArrayFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import lodash, { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import { BelongsToOptions as SequelizeBelongsToOptions, Utils } from 'sequelize';
|
import { BelongsToOptions as SequelizeBelongsToOptions, Utils } from 'sequelize';
|
||||||
import { Reference } from '../features/ReferencesMap';
|
import { Reference } from '../features/ReferencesMap';
|
||||||
import { checkIdentifier } from '../utils';
|
import { checkIdentifier } from '../utils';
|
||||||
|
import { ToOneValueParser } from './has-one-field';
|
||||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
|
||||||
export class BelongsToField extends RelationField {
|
export class BelongsToField extends RelationField {
|
||||||
@ -115,6 +116,10 @@ export class BelongsToField extends RelationField {
|
|||||||
collection.model.refreshAttributes();
|
collection.model.refreshAttributes();
|
||||||
// this.collection.removeIndex([this.options.foreignKey]);
|
// this.collection.removeIndex([this.options.foreignKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildValueParser(ctx: any) {
|
||||||
|
return new ToOneValueParser(this, ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BelongsToFieldOptions extends BaseRelationFieldOptions, SequelizeBelongsToOptions {
|
export interface BelongsToFieldOptions extends BaseRelationFieldOptions, SequelizeBelongsToOptions {
|
||||||
|
@ -2,8 +2,10 @@ import { omit } from 'lodash';
|
|||||||
import { BelongsToManyOptions as SequelizeBelongsToManyOptions, Utils } from 'sequelize';
|
import { BelongsToManyOptions as SequelizeBelongsToManyOptions, Utils } from 'sequelize';
|
||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import { Reference } from '../features/ReferencesMap';
|
import { Reference } from '../features/ReferencesMap';
|
||||||
|
import { Repository } from '../repository';
|
||||||
import { checkIdentifier } from '../utils';
|
import { checkIdentifier } from '../utils';
|
||||||
import { BelongsToField } from './belongs-to-field';
|
import { BelongsToField } from './belongs-to-field';
|
||||||
|
import { ValueParser } from './field';
|
||||||
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
|
||||||
export class BelongsToManyField extends RelationField {
|
export class BelongsToManyField extends RelationField {
|
||||||
@ -123,6 +125,48 @@ export class BelongsToManyField extends RelationField {
|
|||||||
this.clearAccessors();
|
this.clearAccessors();
|
||||||
delete collection.model.associations[this.name];
|
delete collection.model.associations[this.name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildValueParser(ctx: any) {
|
||||||
|
return new ToManyValueParser(this, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ToManyValueParser extends ValueParser {
|
||||||
|
async setValue(value: any) {
|
||||||
|
const fieldNames = this.getFileNames();
|
||||||
|
console.log('fieldNames', fieldNames, this.ctx.column);
|
||||||
|
if (this.isInterface('chinaRegion')) {
|
||||||
|
const repository = this.field.database.getRepository(this.field.target) as Repository;
|
||||||
|
this.value = await Promise.all(
|
||||||
|
value.split('/').map(async (v) => {
|
||||||
|
const instance = await repository.findOne({ filter: { [fieldNames.label]: v.trim() } });
|
||||||
|
return instance ? instance.get(fieldNames.value) : v;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const dataIndex = this.ctx?.column?.dataIndex || [];
|
||||||
|
if (Array.isArray(dataIndex) && dataIndex.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const field = this.ctx.column.dataIndex[1];
|
||||||
|
const repository = this.field.database.getRepository(this.field.target) as Repository;
|
||||||
|
this.value = await Promise.all(
|
||||||
|
value.split(',').map(async (v) => {
|
||||||
|
const instance = await repository.findOne({ filter: { [field]: v.trim() } });
|
||||||
|
return instance ? instance.get(fieldNames.value) : v;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BelongsToManyFieldOptions
|
export interface BelongsToManyFieldOptions
|
||||||
|
@ -1,10 +1,42 @@
|
|||||||
import { DataTypes } from 'sequelize';
|
import { DataTypes } from 'sequelize';
|
||||||
import { BaseColumnFieldOptions, Field } from './field';
|
import { BaseColumnFieldOptions, Field, ValueParser } from './field';
|
||||||
|
|
||||||
export class BooleanField extends Field {
|
export class BooleanField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.BOOLEAN;
|
return DataTypes.BOOLEAN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildValueParser(ctx: any) {
|
||||||
|
return new BooleanValueParser(this, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BooleanValueParser extends ValueParser {
|
||||||
|
async setValue(value: any) {
|
||||||
|
// Boolean
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
// Number
|
||||||
|
else if (typeof value === 'number' && [0, 1].includes(value)) {
|
||||||
|
this.value = value === 1;
|
||||||
|
}
|
||||||
|
// String
|
||||||
|
else if (typeof value === 'string') {
|
||||||
|
if (!value) {
|
||||||
|
this.value = null;
|
||||||
|
}
|
||||||
|
if (['1', 'y', 'yes', 'true', '是'].includes(value.toLowerCase())) {
|
||||||
|
this.value = true;
|
||||||
|
} else if (['0', 'n', 'no', 'false', '否'].includes(value.toLowerCase())) {
|
||||||
|
this.value = false;
|
||||||
|
} else {
|
||||||
|
this.errors.push(`${JSON.stringify(this.value)} value invalid`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.errors.push(`${JSON.stringify(this.value)} value invalid`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BooleanFieldOptions extends BaseColumnFieldOptions {
|
export interface BooleanFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
@ -1,10 +1,38 @@
|
|||||||
|
import { moment2str } from '@nocobase/utils';
|
||||||
|
import moment, { isDate, isMoment } from 'moment';
|
||||||
import { DataTypes } from 'sequelize';
|
import { DataTypes } from 'sequelize';
|
||||||
import { BaseColumnFieldOptions, Field } from './field';
|
import { BaseColumnFieldOptions, Field, ValueParser } from './field';
|
||||||
|
|
||||||
export class DateField extends Field {
|
export class DateField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.DATE(3);
|
return DataTypes.DATE(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildValueParser(ctx: any) {
|
||||||
|
return new DateValueParser(this, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DateValueParser extends ValueParser {
|
||||||
|
async setValue(value: any) {
|
||||||
|
if (isMoment(value)) {
|
||||||
|
this.value = value;
|
||||||
|
} else if (isDate(value)) {
|
||||||
|
this.value = value;
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
const props = this.getProps();
|
||||||
|
const m = moment(value);
|
||||||
|
if (m.isValid()) {
|
||||||
|
this.value = moment2str(m, props);
|
||||||
|
} else {
|
||||||
|
this.errors.push('Invalid date');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getProps() {
|
||||||
|
return this.field.options?.uiSchema?.['x-component-props'] || {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DateFieldOptions extends BaseColumnFieldOptions {
|
export interface DateFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
@ -40,9 +40,9 @@ export abstract class Field {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
|
||||||
constructor(options?: any, context?: FieldContext) {
|
constructor(options?: any, context?: FieldContext) {
|
||||||
this.context = context;
|
this.context = context as any;
|
||||||
this.database = context.database;
|
this.database = this.context.database;
|
||||||
this.collection = context.collection;
|
this.collection = this.context.collection;
|
||||||
this.options = options || {};
|
this.options = options || {};
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
@ -138,7 +138,7 @@ export abstract class Field {
|
|||||||
// 排序字段通过 sortable 控制
|
// 排序字段通过 sortable 控制
|
||||||
const sortable = this.collection.options.sortable;
|
const sortable = this.collection.options.sortable;
|
||||||
if (sortable) {
|
if (sortable) {
|
||||||
let sortField: string;
|
let sortField: any;
|
||||||
if (sortable === true) {
|
if (sortable === true) {
|
||||||
sortField = 'sort';
|
sortField = 'sort';
|
||||||
} else if (typeof sortable === 'string') {
|
} else if (typeof sortable === 'string') {
|
||||||
@ -238,4 +238,33 @@ export abstract class Field {
|
|||||||
typeToString() {
|
typeToString() {
|
||||||
return this.dataType.toString();
|
return this.dataType.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildValueParser(ctx: any) {
|
||||||
|
return new ValueParser(this, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValueParser {
|
||||||
|
ctx: any;
|
||||||
|
field: any;
|
||||||
|
value: any;
|
||||||
|
errors = [];
|
||||||
|
|
||||||
|
constructor(field: any, ctx: any) {
|
||||||
|
this.field = field;
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setValue(value: any) {
|
||||||
|
console.log(this.field.name, value);
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,12 @@ import {
|
|||||||
ForeignKeyOptions,
|
ForeignKeyOptions,
|
||||||
HasManyOptions,
|
HasManyOptions,
|
||||||
HasManyOptions as SequelizeHasManyOptions,
|
HasManyOptions as SequelizeHasManyOptions,
|
||||||
Utils,
|
Utils
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import { Reference } from '../features/ReferencesMap';
|
import { Reference } from '../features/ReferencesMap';
|
||||||
import { checkIdentifier } from '../utils';
|
import { checkIdentifier } from '../utils';
|
||||||
|
import { ToManyValueParser } from './belongs-to-many-field';
|
||||||
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
|
||||||
export interface HasManyFieldOptions extends HasManyOptions {
|
export interface HasManyFieldOptions extends HasManyOptions {
|
||||||
@ -185,6 +186,10 @@ export class HasManyField extends RelationField {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
collection.model.refreshAttributes();
|
collection.model.refreshAttributes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildValueParser(ctx: any) {
|
||||||
|
return new ToManyValueParser(this, ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HasManyFieldOptions extends MultipleRelationFieldOptions, SequelizeHasManyOptions {
|
export interface HasManyFieldOptions extends MultipleRelationFieldOptions, SequelizeHasManyOptions {
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import lodash, { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import {
|
import {
|
||||||
AssociationScope,
|
AssociationScope,
|
||||||
DataType,
|
DataType,
|
||||||
ForeignKeyOptions,
|
ForeignKeyOptions,
|
||||||
HasOneOptions,
|
HasOneOptions,
|
||||||
HasOneOptions as SequelizeHasOneOptions,
|
HasOneOptions as SequelizeHasOneOptions,
|
||||||
Utils,
|
Utils
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import { checkIdentifier, snakeCase } from '../utils';
|
|
||||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
|
||||||
import { Reference } from '../features/ReferencesMap';
|
import { Reference } from '../features/ReferencesMap';
|
||||||
|
import { Repository } from '../repository';
|
||||||
|
import { checkIdentifier } from '../utils';
|
||||||
|
import { ValueParser } from './field';
|
||||||
|
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
|
||||||
export interface HasOneFieldOptions extends HasOneOptions {
|
export interface HasOneFieldOptions extends HasOneOptions {
|
||||||
/**
|
/**
|
||||||
@ -191,6 +193,36 @@ export class HasOneField extends RelationField {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
collection.model.refreshAttributes();
|
collection.model.refreshAttributes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildValueParser(ctx: any) {
|
||||||
|
return new ToOneValueParser(this, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ToOneValueParser extends ValueParser {
|
||||||
|
async setValue(value: any) {
|
||||||
|
const fieldNames = this.getFileNames();
|
||||||
|
console.log('fieldNames', fieldNames, this.ctx.column);
|
||||||
|
const dataIndex = this.ctx?.column?.dataIndex || [];
|
||||||
|
if (Array.isArray(dataIndex) && dataIndex.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const field = 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() } });
|
||||||
|
if (instance) {
|
||||||
|
this.value = instance.get(fieldNames.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HasOneFieldOptions extends BaseRelationFieldOptions, SequelizeHasOneOptions {
|
export interface HasOneFieldOptions extends BaseRelationFieldOptions, SequelizeHasOneOptions {
|
||||||
|
@ -1,7 +1,44 @@
|
|||||||
import { DataTypes } from 'sequelize';
|
import { DataTypes } from 'sequelize';
|
||||||
import { BaseColumnFieldOptions, Field } from './field';
|
import { BaseColumnFieldOptions, Field, ValueParser } from './field';
|
||||||
|
|
||||||
export class IntegerField extends Field {
|
abstract class NumberField extends Field {
|
||||||
|
buildValueParser(ctx: any) {
|
||||||
|
return new NumberValueParser(this, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function percent2float(value: string) {
|
||||||
|
const index = value.indexOf('.');
|
||||||
|
if (index === -1) {
|
||||||
|
return parseFloat(value) / 100;
|
||||||
|
}
|
||||||
|
const repeat = value.length - index - 2;
|
||||||
|
const v = parseInt('1' + '0'.repeat(repeat));
|
||||||
|
return (parseFloat(value) * v) / (100 * v);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NumberValueParser extends ValueParser {
|
||||||
|
async setValue(value: any) {
|
||||||
|
if (value === null || value === undefined || typeof value === 'number') {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
if (!value) {
|
||||||
|
this.value = null;
|
||||||
|
} else if (['n/a', '-'].includes(value.toLowerCase())) {
|
||||||
|
this.value = null;
|
||||||
|
} else if (value.endsWith('%')) {
|
||||||
|
this.value = percent2float(value);
|
||||||
|
console.log(value, this.value);
|
||||||
|
} else {
|
||||||
|
const val = +value;
|
||||||
|
this.value = isNaN(val) ? null : val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IntegerField extends NumberField {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.INTEGER;
|
return DataTypes.INTEGER;
|
||||||
}
|
}
|
||||||
@ -11,7 +48,7 @@ export interface IntegerFieldOptions extends BaseColumnFieldOptions {
|
|||||||
type: 'integer';
|
type: 'integer';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BigIntField extends Field {
|
export class BigIntField extends NumberField {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.BIGINT;
|
return DataTypes.BIGINT;
|
||||||
}
|
}
|
||||||
@ -21,7 +58,7 @@ export interface BigIntFieldOptions extends BaseColumnFieldOptions {
|
|||||||
type: 'bigInt';
|
type: 'bigInt';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FloatField extends Field {
|
export class FloatField extends NumberField {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.FLOAT;
|
return DataTypes.FLOAT;
|
||||||
}
|
}
|
||||||
@ -31,7 +68,7 @@ export interface FloatFieldOptions extends BaseColumnFieldOptions {
|
|||||||
type: 'float';
|
type: 'float';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DoubleField extends Field {
|
export class DoubleField extends NumberField {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.DOUBLE;
|
return DataTypes.DOUBLE;
|
||||||
}
|
}
|
||||||
@ -41,7 +78,7 @@ export interface DoubleFieldOptions extends BaseColumnFieldOptions {
|
|||||||
type: 'double';
|
type: 'double';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RealField extends Field {
|
export class RealField extends NumberField {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.REAL;
|
return DataTypes.REAL;
|
||||||
}
|
}
|
||||||
@ -51,7 +88,7 @@ export interface RealFieldOptions extends BaseColumnFieldOptions {
|
|||||||
type: 'real';
|
type: 'real';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DecimalField extends Field {
|
export class DecimalField extends NumberField {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.DECIMAL;
|
return DataTypes.DECIMAL;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,44 @@
|
|||||||
import { DataTypes } from 'sequelize';
|
import { DataTypes } from 'sequelize';
|
||||||
import { BaseColumnFieldOptions, Field } from './field';
|
import { BaseColumnFieldOptions, Field, ValueParser } from './field';
|
||||||
|
|
||||||
export class StringField extends Field {
|
export class StringField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.STRING;
|
return DataTypes.STRING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildValueParser(ctx: any) {
|
||||||
|
return new StringValueParser(this, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StringValueParser extends ValueParser {
|
||||||
|
async setValue(value: any) {
|
||||||
|
const { map, set } = this.getOptions();
|
||||||
|
if (set.size > 0) {
|
||||||
|
if (map.has(value)) {
|
||||||
|
value = map.get(value);
|
||||||
|
}
|
||||||
|
if (set.has(value)) {
|
||||||
|
this.value = value;
|
||||||
|
} else {
|
||||||
|
this.errors.push('No matching option found');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptions() {
|
||||||
|
const options = this.field.options?.['uiSchema']?.enum || [];
|
||||||
|
const map = new Map();
|
||||||
|
const set = new Set();
|
||||||
|
for (const option of options) {
|
||||||
|
set.add(option.value);
|
||||||
|
set.add(option.label);
|
||||||
|
map.set(option.label, option.value);
|
||||||
|
}
|
||||||
|
return { map, set };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StringFieldOptions extends BaseColumnFieldOptions {
|
export interface StringFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
@ -76,3 +76,65 @@ export const str2moment = (value?: string | string[], options: Str2momentOptions
|
|||||||
? toMoment(value, options)
|
? toMoment(value, options)
|
||||||
: value;
|
: value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toStringByPicker = (value, picker) => {
|
||||||
|
if (picker === 'year') {
|
||||||
|
return value.format('YYYY') + '-01-01T00:00:00.000Z';
|
||||||
|
}
|
||||||
|
if (picker === 'month') {
|
||||||
|
return value.format('YYYY-MM') + '-01T00:00:00.000Z';
|
||||||
|
}
|
||||||
|
if (picker === 'quarter') {
|
||||||
|
return value.format('YYYY-MM') + '-01T00:00:00.000Z';
|
||||||
|
}
|
||||||
|
if (picker === 'week') {
|
||||||
|
return value.format('YYYY-MM-DD') + 'T00:00:00.000Z';
|
||||||
|
}
|
||||||
|
return value.format('YYYY-MM-DD') + 'T00:00:00.000Z';
|
||||||
|
};
|
||||||
|
|
||||||
|
const toGmtByPicker = (value: moment.Moment | moment.Moment[], picker?: any) => {
|
||||||
|
if (!value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((val) => toStringByPicker(val, picker));
|
||||||
|
}
|
||||||
|
if (moment.isMoment(value)) {
|
||||||
|
return toStringByPicker(value, picker);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Moment2strOptions {
|
||||||
|
showTime?: boolean;
|
||||||
|
gmt?: boolean;
|
||||||
|
picker?: 'year' | 'month' | 'week' | 'quarter';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const moment2str = (value?: moment.Moment | moment.Moment[], options: Moment2strOptions = {}) => {
|
||||||
|
const { showTime, gmt, picker } = options;
|
||||||
|
if (!value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (showTime) {
|
||||||
|
return gmt ? toGmt(value) : toLocal(value);
|
||||||
|
}
|
||||||
|
return toGmtByPicker(value, picker);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapDateFormat = function () {
|
||||||
|
return (props: any) => {
|
||||||
|
const format = getDefaultFormat(props) as any;
|
||||||
|
const onChange = props.onChange;
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
format: format,
|
||||||
|
value: str2moment(props.value, props),
|
||||||
|
onChange: (value: moment.Moment | moment.Moment[]) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(moment2str(value, props));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -5,6 +5,9 @@
|
|||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nocobase/client": "0.9.0-alpha.2",
|
"@nocobase/client": "0.9.0-alpha.2",
|
||||||
"@nocobase/test": "0.9.0-alpha.2"
|
"@nocobase/test": "0.9.0-alpha.2"
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { Schema, useFieldSchema, useForm } from '@formily/react';
|
import { Schema, useFieldSchema, useForm } from '@formily/react';
|
||||||
import { isEmpty } from '@formily/shared';
|
|
||||||
import {
|
import {
|
||||||
useActionContext,
|
useActionContext,
|
||||||
useAPIClient,
|
useAPIClient,
|
||||||
useBlockRequestContext,
|
useBlockRequestContext,
|
||||||
useCollection,
|
useCollection,
|
||||||
useCollectionManager,
|
useCollectionManager,
|
||||||
useCompile,
|
useCompile
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { message } from 'antd';
|
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -29,45 +27,43 @@ export const useDownloadXlsxTemplateAction = () => {
|
|||||||
const apiClient = useAPIClient();
|
const apiClient = useAPIClient();
|
||||||
const actionSchema = useFieldSchema();
|
const actionSchema = useFieldSchema();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { getCollectionJoinField } = useCollectionManager();
|
const { getCollectionJoinField, getCollectionField } = useCollectionManager();
|
||||||
const { name, title, getField } = useCollection();
|
const { name, title, getField } = useCollection();
|
||||||
const { t } = useTranslation(NAMESPACE);
|
const { t } = useTranslation(NAMESPACE);
|
||||||
const { schema: importSchema } = useImportSchema(actionSchema);
|
const { schema: importSchema } = useImportSchema(actionSchema);
|
||||||
return {
|
return {
|
||||||
async run() {
|
async run() {
|
||||||
const { importColumns, explain } = cloneDeep(importSchema?.['x-action-settings']?.['importSettings'] ?? {});
|
const { importColumns, explain } = cloneDeep(importSchema?.['x-action-settings']?.['importSettings'] ?? {});
|
||||||
try {
|
const columns = importColumns
|
||||||
importColumns.forEach((es) => {
|
.map((column) => {
|
||||||
const { uiSchema, interface: fieldInterface } =
|
const field = getCollectionField(`${name}.${column.dataIndex[0]}`);
|
||||||
getCollectionJoinField(`${name}.${es.dataIndex.join('.')}`) ?? {};
|
if (!field) {
|
||||||
if (isEmpty(uiSchema) && isEmpty(fieldInterface)) {
|
|
||||||
throw new Error(t('Field {{fieldName}} does not exist', { fieldName: es.dataIndex.join('.') }));
|
|
||||||
}
|
|
||||||
es.enum = uiSchema?.enum?.map((e) => ({ value: e.value, label: e.label }));
|
|
||||||
if (!es.enum && uiSchema.type === 'boolean') {
|
|
||||||
es.enum = [
|
|
||||||
{ value: true, label: t('Yes') },
|
|
||||||
{ value: false, label: t('No') },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
es.defaultTitle = compile(uiSchema?.title);
|
|
||||||
if (fieldInterface === 'chinaRegion') {
|
|
||||||
es.dataIndex.push('name');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (field.interface === 'chinaRegion') {
|
||||||
|
column.dataIndex.push('name');
|
||||||
|
}
|
||||||
|
column.defaultTitle = compile(field?.uiSchema?.title) || field.name;
|
||||||
|
if (column.dataIndex.length > 1) {
|
||||||
|
const subField = getCollectionJoinField(`${name}.${column.dataIndex.join('.')}`);
|
||||||
|
if (!subField) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
column.defaultTitle = column.defaultTitle + '/' + compile(subField?.uiSchema?.title) || subField.name;
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
const { data } = await resource.downloadXlsxTemplate(
|
const { data } = await resource.downloadXlsxTemplate(
|
||||||
{
|
{
|
||||||
|
values: {
|
||||||
title: compile(title),
|
title: compile(title),
|
||||||
explain,
|
explain,
|
||||||
columns: JSON.stringify(compile(importColumns)),
|
columns: compile(columns),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'post',
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -82,7 +78,7 @@ export const useImportStartAction = () => {
|
|||||||
const apiClient = useAPIClient();
|
const apiClient = useAPIClient();
|
||||||
const actionSchema = useFieldSchema();
|
const actionSchema = useFieldSchema();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { getCollectionJoinField } = useCollectionManager();
|
const { getCollectionJoinField, getCollectionField } = useCollectionManager();
|
||||||
const { name, title, getField } = useCollection();
|
const { name, title, getField } = useCollection();
|
||||||
const { t } = useTranslation(NAMESPACE);
|
const { t } = useTranslation(NAMESPACE);
|
||||||
const { schema: importSchema } = useImportSchema(actionSchema);
|
const { schema: importSchema } = useImportSchema(actionSchema);
|
||||||
@ -92,47 +88,47 @@ 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'] ?? {});
|
||||||
try {
|
const columns = importColumns
|
||||||
importColumns.forEach((es) => {
|
.map((column) => {
|
||||||
const { uiSchema, interface: fieldInterface } =
|
const field = getCollectionField(`${name}.${column.dataIndex[0]}`);
|
||||||
getCollectionJoinField(`${name}.${es.dataIndex.join('.')}`) ?? {};
|
if (!field) {
|
||||||
if (isEmpty(uiSchema) && isEmpty(fieldInterface)) {
|
|
||||||
throw new Error(t('Field {{fieldName}} does not exist', { fieldName: es.dataIndex.join('.') }));
|
|
||||||
}
|
|
||||||
es.enum = uiSchema?.enum?.map((e) => ({ value: e.value, label: e.label }));
|
|
||||||
if (!es.enum && uiSchema.type === 'boolean') {
|
|
||||||
es.enum = [
|
|
||||||
{ value: true, label: t('Yes') },
|
|
||||||
{ value: false, label: t('No') },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
es.defaultTitle = compile(uiSchema?.title);
|
|
||||||
if (fieldInterface === 'chinaRegion') {
|
|
||||||
es.dataIndex.push('name');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (field.interface === 'chinaRegion') {
|
||||||
|
column.dataIndex.push('name');
|
||||||
|
}
|
||||||
|
column.defaultTitle = compile(field?.uiSchema?.title) || field.name;
|
||||||
|
if (column.dataIndex.length > 1) {
|
||||||
|
const subField = getCollectionJoinField(`${name}.${column.dataIndex.join('.')}`);
|
||||||
|
if (!subField) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
column.defaultTitle = column.defaultTitle + '/' + compile(subField?.uiSchema?.title) || subField.name;
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
})
|
||||||
|
.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);
|
console.log(form, uploadFiles);
|
||||||
formData.append('file', uploadFiles[0]);
|
formData.append('file', uploadFiles[0]);
|
||||||
formData.append('columns', JSON.stringify(importColumns));
|
formData.append('columns', JSON.stringify(columns));
|
||||||
formData.append('explain', explain);
|
formData.append('explain', explain);
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
setImportModalVisible(true);
|
setImportModalVisible(true);
|
||||||
setImportStatus(ImportStatus.IMPORTING);
|
setImportStatus(ImportStatus.IMPORTING);
|
||||||
const { data }: any = await apiClient.axios
|
try {
|
||||||
.post(`${name}:importXlsx`, formData, {
|
const { data }: any = await apiClient.axios.post(`${name}:importXlsx`, formData, {
|
||||||
timeout: 10 * 60 * 1000,
|
timeout: 10 * 60 * 1000,
|
||||||
})
|
});
|
||||||
.catch((err) => {});
|
|
||||||
setImportResult(data);
|
setImportResult(data);
|
||||||
form.reset();
|
form.reset();
|
||||||
await service?.refresh?.();
|
await service?.refresh?.();
|
||||||
setImportStatus(ImportStatus.IMPORTED);
|
setImportStatus(ImportStatus.IMPORTED);
|
||||||
|
} catch (error) {
|
||||||
|
setImportModalVisible(false);
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -2,14 +2,14 @@ import { Context, Next } from '@nocobase/actions';
|
|||||||
import xlsx from 'node-xlsx';
|
import xlsx from 'node-xlsx';
|
||||||
|
|
||||||
export async function downloadXlsxTemplate(ctx: Context, next: Next) {
|
export async function downloadXlsxTemplate(ctx: Context, next: Next) {
|
||||||
let { columns, explain, title } = ctx.action.params;
|
let { columns, explain, title } = ctx.request.body;
|
||||||
if (typeof columns === 'string') {
|
if (typeof columns === 'string') {
|
||||||
columns = JSON.parse(columns);
|
columns = JSON.parse(columns);
|
||||||
}
|
}
|
||||||
const header = columns?.map((column) => column.defaultTitle);
|
const header = columns?.map((column) => column.defaultTitle);
|
||||||
const data = [header];
|
const data = [header];
|
||||||
if (explain?.trim() !== '') {
|
if (explain?.trim() !== '') {
|
||||||
data.push([explain]);
|
data.unshift([explain]);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = xlsx.build([
|
ctx.body = xlsx.build([
|
||||||
|
@ -1,109 +1,163 @@
|
|||||||
import { Context, Next } from '@nocobase/actions';
|
import { Context, Next } from '@nocobase/actions';
|
||||||
import { Repository } from '@nocobase/database';
|
import { Collection, Repository } from '@nocobase/database';
|
||||||
import { cloneDeep } from 'lodash';
|
|
||||||
import xlsx from 'node-xlsx';
|
import xlsx from 'node-xlsx';
|
||||||
import { transform } from '../utils';
|
import XLSX from 'xlsx';
|
||||||
|
import { namespace } from '../../';
|
||||||
|
|
||||||
const IMPORT_LIMIT_COUNT = 10000;
|
const IMPORT_LIMIT_COUNT = 10000;
|
||||||
|
|
||||||
export async function importXlsx(ctx: Context, next: Next) {
|
class Importer {
|
||||||
let { columns } = ctx.request.body as any;
|
repository: Repository;
|
||||||
const { ['file']: file } = ctx;
|
collection: Collection;
|
||||||
|
columns: any[];
|
||||||
|
items: any[][] = [];
|
||||||
|
headerRow;
|
||||||
|
context: Context;
|
||||||
|
|
||||||
|
constructor(ctx: Context) {
|
||||||
const { resourceName, resourceOf } = ctx.action;
|
const { resourceName, resourceOf } = ctx.action;
|
||||||
|
this.context = ctx;
|
||||||
|
this.repository = ctx.db.getRepository<any>(resourceName, resourceOf);
|
||||||
|
this.collection = this.repository.collection;
|
||||||
|
this.parseXlsx();
|
||||||
|
}
|
||||||
|
|
||||||
|
getRows() {
|
||||||
|
const workbook = XLSX.read(this.context.file.buffer, {
|
||||||
|
type: 'buffer',
|
||||||
|
// cellDates: true,
|
||||||
|
// raw: false,
|
||||||
|
});
|
||||||
|
const r = workbook.Sheets[workbook.SheetNames[0]];
|
||||||
|
const rows = XLSX.utils.sheet_to_json<any>(r, { header: 1, defval: null, raw: false });
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseXlsx() {
|
||||||
|
const rows = this.getRows();
|
||||||
|
let columns = this.context.request.body.columns as any[];
|
||||||
if (typeof columns === 'string') {
|
if (typeof columns === 'string') {
|
||||||
columns = JSON.parse(columns);
|
columns = JSON.parse(columns);
|
||||||
}
|
}
|
||||||
const repository = ctx.db.getRepository<any>(resourceName, resourceOf) as Repository;
|
this.columns = columns.map((column) => {
|
||||||
const collection = repository.collection;
|
return {
|
||||||
|
...column,
|
||||||
|
field: this.collection.fields.get(column.dataIndex[0]),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const str = this.columns.map((column) => column.defaultTitle).join('||');
|
||||||
|
for (const row of rows) {
|
||||||
|
if (this.hasHeaderRow()) {
|
||||||
|
if (row && row.join('').trim()) {
|
||||||
|
this.items.push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (str === row.filter((r) => r).join('||')) {
|
||||||
|
this.headerRow = row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
columns = columns?.filter((col) => col?.dataIndex?.length > 0);
|
getFieldByIndex(index) {
|
||||||
const collectionFields = columns.map((col) => collection.fields.get(col.dataIndex[0]));
|
return this.columns[index].field;
|
||||||
const {
|
|
||||||
0: { data: originalList },
|
|
||||||
} = xlsx.parse(file.buffer);
|
|
||||||
const failureData = originalList.splice(IMPORT_LIMIT_COUNT + 1);
|
|
||||||
const titles = originalList.shift();
|
|
||||||
const legalList: any[] = [];
|
|
||||||
if (originalList.length > 0 && titles?.length === columns.length) {
|
|
||||||
// const results = (
|
|
||||||
// await Promise.allSettled<any>(
|
|
||||||
// originalList.map(async (item) => {
|
|
||||||
// try {
|
|
||||||
// const transformResult = await transform({ ctx, record: item, columns, fields: collectionFields });
|
|
||||||
// legalList.push(cloneDeep(item));
|
|
||||||
// return transformResult;
|
|
||||||
// } catch (error) {
|
|
||||||
// failureData.unshift([...item, error.message]);
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
// )
|
|
||||||
// ).filter((item) => 'value' in item && item.value !== undefined);
|
|
||||||
const values: any[] = [];
|
|
||||||
for (const item of originalList) {
|
|
||||||
try {
|
|
||||||
const transformResult = await transform({ ctx, record: item, columns, fields: collectionFields });
|
|
||||||
values.push(transformResult);
|
|
||||||
legalList.push(cloneDeep<any>(item));
|
|
||||||
} catch (error) {
|
|
||||||
failureData.unshift([...item, error.message]);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
//@ts-ignore
|
async getItems() {
|
||||||
// const values = results.map((r) => r.value);
|
const items: any[] = [];
|
||||||
const result = await ctx.db.sequelize.transaction(async (transaction) => {
|
for (const row of this.items) {
|
||||||
let sort: number = 0;
|
const values = {};
|
||||||
if (collection.options.sortable) {
|
const errors = [];
|
||||||
sort = await repository.model.max<number, any>('sort', { transaction });
|
for (let index = 0; index < row.length; index++) {
|
||||||
}
|
if (!this.columns[index]) {
|
||||||
for (const [index, val] of values.entries()) {
|
|
||||||
if (val === undefined || val === null) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
const column = this.columns[index];
|
||||||
let values = { ...val };
|
const { field, defaultTitle } = column;
|
||||||
if (collection.options.sortable) {
|
let value = row[index];
|
||||||
sort += 1;
|
if (value === undefined || value === null) {
|
||||||
values['sort'] = sort;
|
continue;
|
||||||
}
|
}
|
||||||
await repository.create({
|
const parser = field.buildValueParser({ ...this.context, column });
|
||||||
|
await parser.setValue(typeof value === 'string' ? value.trim() : value);
|
||||||
|
value = parser.getValue();
|
||||||
|
if (parser.errors.length > 0) {
|
||||||
|
errors.push(`${defaultTitle}: ${parser.errors.join(';')}`);
|
||||||
|
}
|
||||||
|
if (value === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
values[field.name] = value;
|
||||||
|
}
|
||||||
|
items.push({
|
||||||
|
row,
|
||||||
|
values,
|
||||||
|
errors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSortField() {
|
||||||
|
return !!this.collection.options.sortable;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
return await this.context.db.sequelize.transaction(async (transaction) => {
|
||||||
|
let sort: number = 0;
|
||||||
|
if (this.hasSortField()) {
|
||||||
|
sort = await this.repository.model.max<number, any>('sort', { transaction });
|
||||||
|
}
|
||||||
|
const result: any = [[], []];
|
||||||
|
for (const { row, values, errors } of await this.getItems()) {
|
||||||
|
if (errors.length > 0) {
|
||||||
|
row.push(errors.join(';'));
|
||||||
|
result[1].push(row);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (this.hasSortField()) {
|
||||||
|
values['sort'] = ++sort;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const instance = await this.repository.create({
|
||||||
values,
|
values,
|
||||||
transaction,
|
transaction,
|
||||||
logging: false,
|
logging: false,
|
||||||
|
context: this.context,
|
||||||
});
|
});
|
||||||
|
result[0].push(instance);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const failData = legalList[index];
|
this.context.log.error(error, row);
|
||||||
failData.push(error?.original?.message ?? error.message);
|
result[1].push(row);
|
||||||
failureData.unshift(failData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return result;
|
||||||
successCount: originalList.length - failureData.length,
|
|
||||||
failureCount: failureData.length,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
const header = columns?.map((column) => column.defaultTitle);
|
}
|
||||||
|
|
||||||
|
hasHeaderRow() {
|
||||||
|
return !!this.headerRow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importXlsx(ctx: Context, next: Next) {
|
||||||
|
const importer = new Importer(ctx);
|
||||||
|
|
||||||
|
if (!importer.hasHeaderRow()) {
|
||||||
|
ctx.throw(400, ctx.t('Imported template does not match, please download again.', { ns: namespace }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const [success, failure] = await importer.run();
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
rows: xlsx.build([
|
rows: xlsx.build([
|
||||||
{
|
{
|
||||||
name: file.originalname,
|
name: ctx.file.originalname,
|
||||||
data: [header].concat(failureData),
|
data: [importer.headerRow].concat(failure),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
...result,
|
successCount: success.length,
|
||||||
|
failureCount: failure.length,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
ctx.body = {
|
|
||||||
rows: file.buffer.toJSON(),
|
|
||||||
successCount: 0,
|
|
||||||
failureCount: originalList?.length ?? 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.set({
|
|
||||||
'Content-Type': 'application/octet-stream',
|
|
||||||
// to avoid "invalid character" error in header (RFC)
|
|
||||||
'Content-Disposition': `attachment; filename=${encodeURI('testTitle')}.xlsx`,
|
|
||||||
});
|
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
}
|
}
|
||||||
|
@ -7,4 +7,5 @@ export default {
|
|||||||
'Incorrect date format': '日期格式不正确',
|
'Incorrect date format': '日期格式不正确',
|
||||||
'Incorrect email format': '邮箱格式不正确',
|
'Incorrect email format': '邮箱格式不正确',
|
||||||
'Illegal percentage format': '百分比格式有误',
|
'Illegal percentage format': '百分比格式有误',
|
||||||
|
'Imported template does not match, please download again.': '导入模板不匹配,请检查导入文件标题行或重新下载导入模板'
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user