fix: improve filters

This commit is contained in:
chenos 2020-12-29 14:53:39 +08:00
parent e692d7bb5f
commit 519f8de40b
6 changed files with 85 additions and 32 deletions

View File

@ -157,14 +157,14 @@ const OP_MAP = {
select: [ select: [
{label: '等于', value: 'eq', selected: true}, {label: '等于', value: 'eq', selected: true},
{label: '不等于', value: 'ne'}, {label: '不等于', value: 'ne'},
{label: '包含', value: '$anyOf'}, {label: '包含', value: 'in'},
{label: '不包含', value: '$noneOf'}, {label: '不包含', value: 'notIn'},
{label: '非空', value: '$notNull'}, {label: '非空', value: '$notNull'},
{label: '为空', value: '$null'}, {label: '为空', value: '$null'},
], ],
multipleSelect: [ multipleSelect: [
{label: '等于', value: 'eq', selected: true}, {label: '等于', value: '$match', selected: true},
{label: '不等于', value: 'ne'}, {label: '不等于', value: '$notMatch'},
{label: '包含', value: '$anyOf'}, {label: '包含', value: '$anyOf'},
{label: '不包含', value: '$noneOf'}, {label: '不包含', value: '$noneOf'},
{label: '非空', value: '$notNull'}, {label: '非空', value: '$notNull'},
@ -217,8 +217,8 @@ const op = {
checkbox: OP_MAP.boolean, checkbox: OP_MAP.boolean,
boolean: OP_MAP.boolean, boolean: OP_MAP.boolean,
select: OP_MAP.select, select: OP_MAP.select,
multipleSelect: OP_MAP.select, multipleSelect: OP_MAP.multipleSelect,
checkboxes: OP_MAP.select, checkboxes: OP_MAP.multipleSelect,
radio: OP_MAP.select, radio: OP_MAP.select,
upload: OP_MAP.file, upload: OP_MAP.file,
attachment: OP_MAP.file, attachment: OP_MAP.file,
@ -237,7 +237,13 @@ const controls = {
string: StringInput, string: StringInput,
textarea: StringInput, textarea: StringInput,
number: InputNumber, number: InputNumber,
percent: InputNumber, percent: (props) => (
<InputNumber
formatter={value => value ? `${value}%` : ''}
parser={value => value.replace('%', '')}
{...props}
/>
),
boolean: BooleanControl, boolean: BooleanControl,
checkbox: BooleanControl, checkbox: BooleanControl,
select: OptionControl, select: OptionControl,
@ -302,13 +308,13 @@ export function FilterItem(props: FilterItemProps) {
const field = fields.find(field => field.name === props.dataSource.column); const field = fields.find(field => field.name === props.dataSource.column);
if (field) { if (field) {
setField(field); setField(field);
setType(field.component.type); let componentType = field.component.type;
// console.log(dataSource); if (field.component.type === 'select' && field.multiple) {
componentType = 'multipleSelect';
}
setType(componentType);
} }
setDataSource({...props.dataSource}); setDataSource({...props.dataSource});
// if (['boolean', 'checkbox'].indexOf(type) !== -1) {
// onChange({...dataSource, op: undefined});
// }
}, [ }, [
props.dataSource, type, props.dataSource, type,
]); ]);
@ -328,10 +334,12 @@ export function FilterItem(props: FilterItemProps) {
<Select value={dataSource.column} <Select value={dataSource.column}
onChange={(value) => { onChange={(value) => {
const field = fields.find(field => field.name === value); const field = fields.find(field => field.name === value);
if (field) { let componentType = field.component.type;
setType(field.component.type); if (field.component.type === 'select' && field.multiple) {
componentType = 'multipleSelect';
} }
onChange({...dataSource, column: value, op: get(op, [field.component.type, 0, 'value']), value: undefined}); setType(componentType);
onChange({...dataSource, column: value, op: get(op, [componentType, 0, 'value']), value: undefined});
}} }}
style={{ width: 120 }} style={{ width: 120 }}
placeholder={'选择字段'}> placeholder={'选择字段'}>

View File

@ -284,4 +284,9 @@ export default class Database {
} }
} }
} }
public getFieldByPath(fieldPath: string) {
const [tableName, fieldName] = fieldPath.split('.');
return this.getTable(tableName).getField(fieldName);
}
} }

View File

@ -312,7 +312,7 @@ export class ARRAY extends Column {
public readonly options: Options.ArrayOptions; public readonly options: Options.ArrayOptions;
public getDataType() { public getDataType() {
return DataTypes.JSON; return DataTypes.JSONB;
} }
public getAttributeOptions() { public getAttributeOptions() {
@ -325,6 +325,9 @@ export class ARRAY extends Column {
} }
export class JSON extends Column { export class JSON extends Column {
public getDataType() {
return DataTypes.JSONB;
}
} }
export class JSONB extends Column { export class JSONB extends Column {

View File

@ -20,7 +20,11 @@ for (const key in Op) {
// 通用 // 通用
// 是否为空:数据库意义的 null // 是否为空:数据库意义的 null
op.set('$null', () => ({ [Op.is]: null })); op.set('$null', (value, {fieldPath, database}) => {
// const field = database.getFieldByPath(fieldPath);
// console.log({field});
return { [Op.is]: null };
});
op.set('$notNull', () => ({ [Op.not]: null })); op.set('$notNull', () => ({ [Op.not]: null }));
op.set('$isTruly', () => ({ op.set('$isTruly', () => ({
@ -55,9 +59,20 @@ op.set('$notEndsWith', (value: string) => ({ [Op.notILike]: `%${value}` }));
// 多选JSON类型 // 多选JSON类型
// 包含组中任意值(命名来源:`Array.prototype.some` // 包含组中任意值(命名来源:`Array.prototype.some`
op.set('$anyOf', (values: any[]) => ({ op.set('$anyOf', (values: any[], options) => {
[Op.or]: toArray(values).map(value => ({ [Op.contains]: value })) if (!values) {
})); return Sequelize.literal('');
}
values = Array.isArray(values) ? values : [values];
if (values.length === 0) {
return Sequelize.literal('');
}
const { field, fieldPath } = options;
const column = fieldPath.split('.').map(name => `"${name}"`).join('.');
const sql = values.map(value => `(${column})::jsonb @> '${JSON.stringify(value)}'`).join(' OR ');
console.log(sql);
return Sequelize.literal(sql);
});
// 包含组中所有值 // 包含组中所有值
op.set('$allOf', (values: any) => ({ [Op.contains]: toArray(values) })); op.set('$allOf', (values: any) => ({ [Op.contains]: toArray(values) }));
// TODO(bug): 不包含组中任意值 // TODO(bug): 不包含组中任意值
@ -66,6 +81,9 @@ op.set('$noneOf', (values: any[], options) => {
return Sequelize.literal(''); return Sequelize.literal('');
} }
values = Array.isArray(values) ? values : [values]; values = Array.isArray(values) ? values : [values];
if (values.length === 0) {
return Sequelize.literal('');
}
const { field, fieldPath } = options; const { field, fieldPath } = options;
const column = fieldPath.split('.').map(name => `"${name}"`).join('.'); const column = fieldPath.split('.').map(name => `"${name}"`).join('.');
const sql = values.map(value => `(${column})::jsonb @> '${JSON.stringify(value)}'`).join(' OR '); const sql = values.map(value => `(${column})::jsonb @> '${JSON.stringify(value)}'`).join(' OR ');
@ -73,14 +91,26 @@ op.set('$noneOf', (values: any[], options) => {
return Sequelize.literal(`not (${sql})`); return Sequelize.literal(`not (${sql})`);
}); });
// 与组中值匹配 // 与组中值匹配
op.set('$match', (values: any[]) => { op.set('$match', (values: any[], options) => {
const array = toArray(values); const array = toArray(values);
return { if (values.length === 0) {
[Op.contains]: array, return Sequelize.literal('');
[Op.contained]: array }
}; const { field, fieldPath } = options;
const column = fieldPath.split('.').map(name => `"${name}"`).join('.');
const sql = `(${column})::jsonb @> '${JSON.stringify(array)}' AND (${column})::jsonb <@ '${JSON.stringify(array)}'`
return Sequelize.literal(sql);
});
op.set('$notMatch', (values: any[], options) => {
const array = toArray(values);
if (values.length === 0) {
return Sequelize.literal('');
}
const { field, fieldPath } = options;
const column = fieldPath.split('.').map(name => `"${name}"`).join('.');
const sql = `(${column})::jsonb @> '${JSON.stringify(array)}' AND (${column})::jsonb <@ '${JSON.stringify(array)}'`
return Sequelize.literal(`not (${sql})`);
// return Sequelize.literal(`(not (${sql})) AND ${column} IS NULL`);
}); });
export default op; export default op;

View File

@ -20,7 +20,7 @@ export function toWhere(options: any, context: ToWhereContext = {}) {
if (Array.isArray(options)) { if (Array.isArray(options)) {
return options.map((item) => toWhere(item, context)); return options.map((item) => toWhere(item, context));
} }
const { prefix, model, associations = {}, ctx, dialect } = context; const { prefix, model, associations = {}, ctx, dialect, database } = context;
const items = {}; const items = {};
// 先处理「点号」的问题 // 先处理「点号」的问题
for (const key in options) { for (const key in options) {
@ -54,7 +54,11 @@ export function toWhere(options: any, context: ToWhereContext = {}) {
switch (typeof opKey) { switch (typeof opKey) {
case 'function': case 'function':
const name = model ? model.options.name.plural : ''; const name = model ? model.options.name.plural : '';
const result = opKey(items[key], { model, fieldPath: name ? `${name}.${prefix}` : prefix }); const result = opKey(items[key], {
model,
database,
fieldPath: name ? `${name}.${prefix}` : prefix,
});
if (result.constructor.name === 'Literal') { if (result.constructor.name === 'Literal') {
values['$__literals'] = values['$__literals'] || []; values['$__literals'] = values['$__literals'] || [];
values['$__literals'].push(result); values['$__literals'].push(result);
@ -184,7 +188,7 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
} }
const { fields = [], filter } = options; const { fields = [], filter } = options;
const { model, sourceAlias, associations = {}, ctx, dialect } = context; const { model, sourceAlias, associations = {}, ctx, database, dialect } = context;
let where = options.where || {}; let where = options.where || {};
@ -193,6 +197,7 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
model, model,
associations, associations,
ctx, ctx,
database,
}) || {}; }) || {};
} }

View File

@ -157,6 +157,7 @@ export const multipleSelect = {
type: 'json', // json 过滤 type: 'json', // json 过滤
filterable: true, filterable: true,
dataSource: [], dataSource: [],
defaultValue: [],
multiple: true, // 需要重点考虑 multiple: true, // 需要重点考虑
component: { component: {
type: 'select', type: 'select',
@ -184,6 +185,7 @@ export const checkboxes = {
type: 'json', type: 'json',
filterable: true, filterable: true,
dataSource: [], dataSource: [],
defaultValue: [],
component: { component: {
type: 'checkboxes', type: 'checkboxes',
}, },
@ -387,7 +389,7 @@ export const createdBy = {
interface: 'createdBy', interface: 'createdBy',
type: 'createdBy', type: 'createdBy',
// name: 'createdBy', // name: 'createdBy',
filterable: true, // filterable: true,
target: 'users', target: 'users',
labelField: 'nickname', labelField: 'nickname',
foreignKey: 'created_by_id', foreignKey: 'created_by_id',
@ -405,7 +407,7 @@ export const updatedBy = {
interface: 'updatedBy', interface: 'updatedBy',
// name: 'updatedBy', // name: 'updatedBy',
type: 'updatedBy', type: 'updatedBy',
filterable: true, // filterable: true,
target: 'users', target: 'users',
labelField: 'nickname', labelField: 'nickname',
foreignKey: 'created_by_id', foreignKey: 'created_by_id',