mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 05:25:52 +00:00
fix: improve filters
This commit is contained in:
parent
e692d7bb5f
commit
519f8de40b
@ -157,14 +157,14 @@ const OP_MAP = {
|
||||
select: [
|
||||
{label: '等于', value: 'eq', selected: true},
|
||||
{label: '不等于', value: 'ne'},
|
||||
{label: '包含', value: '$anyOf'},
|
||||
{label: '不包含', value: '$noneOf'},
|
||||
{label: '包含', value: 'in'},
|
||||
{label: '不包含', value: 'notIn'},
|
||||
{label: '非空', value: '$notNull'},
|
||||
{label: '为空', value: '$null'},
|
||||
],
|
||||
multipleSelect: [
|
||||
{label: '等于', value: 'eq', selected: true},
|
||||
{label: '不等于', value: 'ne'},
|
||||
{label: '等于', value: '$match', selected: true},
|
||||
{label: '不等于', value: '$notMatch'},
|
||||
{label: '包含', value: '$anyOf'},
|
||||
{label: '不包含', value: '$noneOf'},
|
||||
{label: '非空', value: '$notNull'},
|
||||
@ -217,8 +217,8 @@ const op = {
|
||||
checkbox: OP_MAP.boolean,
|
||||
boolean: OP_MAP.boolean,
|
||||
select: OP_MAP.select,
|
||||
multipleSelect: OP_MAP.select,
|
||||
checkboxes: OP_MAP.select,
|
||||
multipleSelect: OP_MAP.multipleSelect,
|
||||
checkboxes: OP_MAP.multipleSelect,
|
||||
radio: OP_MAP.select,
|
||||
upload: OP_MAP.file,
|
||||
attachment: OP_MAP.file,
|
||||
@ -237,7 +237,13 @@ const controls = {
|
||||
string: StringInput,
|
||||
textarea: StringInput,
|
||||
number: InputNumber,
|
||||
percent: InputNumber,
|
||||
percent: (props) => (
|
||||
<InputNumber
|
||||
formatter={value => value ? `${value}%` : ''}
|
||||
parser={value => value.replace('%', '')}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
boolean: BooleanControl,
|
||||
checkbox: BooleanControl,
|
||||
select: OptionControl,
|
||||
@ -302,13 +308,13 @@ export function FilterItem(props: FilterItemProps) {
|
||||
const field = fields.find(field => field.name === props.dataSource.column);
|
||||
if (field) {
|
||||
setField(field);
|
||||
setType(field.component.type);
|
||||
// console.log(dataSource);
|
||||
let componentType = field.component.type;
|
||||
if (field.component.type === 'select' && field.multiple) {
|
||||
componentType = 'multipleSelect';
|
||||
}
|
||||
setType(componentType);
|
||||
}
|
||||
setDataSource({...props.dataSource});
|
||||
// if (['boolean', 'checkbox'].indexOf(type) !== -1) {
|
||||
// onChange({...dataSource, op: undefined});
|
||||
// }
|
||||
}, [
|
||||
props.dataSource, type,
|
||||
]);
|
||||
@ -328,10 +334,12 @@ export function FilterItem(props: FilterItemProps) {
|
||||
<Select value={dataSource.column}
|
||||
onChange={(value) => {
|
||||
const field = fields.find(field => field.name === value);
|
||||
if (field) {
|
||||
setType(field.component.type);
|
||||
let componentType = 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 }}
|
||||
placeholder={'选择字段'}>
|
||||
|
@ -284,4 +284,9 @@ export default class Database {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getFieldByPath(fieldPath: string) {
|
||||
const [tableName, fieldName] = fieldPath.split('.');
|
||||
return this.getTable(tableName).getField(fieldName);
|
||||
}
|
||||
}
|
||||
|
@ -312,7 +312,7 @@ export class ARRAY extends Column {
|
||||
public readonly options: Options.ArrayOptions;
|
||||
|
||||
public getDataType() {
|
||||
return DataTypes.JSON;
|
||||
return DataTypes.JSONB;
|
||||
}
|
||||
|
||||
public getAttributeOptions() {
|
||||
@ -325,6 +325,9 @@ export class ARRAY extends Column {
|
||||
}
|
||||
|
||||
export class JSON extends Column {
|
||||
public getDataType() {
|
||||
return DataTypes.JSONB;
|
||||
}
|
||||
}
|
||||
|
||||
export class JSONB extends Column {
|
||||
|
@ -20,7 +20,11 @@ for (const key in Op) {
|
||||
// 通用
|
||||
|
||||
// 是否为空:数据库意义的 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('$isTruly', () => ({
|
||||
@ -55,9 +59,20 @@ op.set('$notEndsWith', (value: string) => ({ [Op.notILike]: `%${value}` }));
|
||||
// 多选(JSON)类型
|
||||
|
||||
// 包含组中任意值(命名来源:`Array.prototype.some`)
|
||||
op.set('$anyOf', (values: any[]) => ({
|
||||
[Op.or]: toArray(values).map(value => ({ [Op.contains]: value }))
|
||||
}));
|
||||
op.set('$anyOf', (values: any[], options) => {
|
||||
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) }));
|
||||
// TODO(bug): 不包含组中任意值
|
||||
@ -66,6 +81,9 @@ op.set('$noneOf', (values: any[], options) => {
|
||||
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 ');
|
||||
@ -73,14 +91,26 @@ op.set('$noneOf', (values: any[], options) => {
|
||||
return Sequelize.literal(`not (${sql})`);
|
||||
});
|
||||
// 与组中值匹配
|
||||
op.set('$match', (values: any[]) => {
|
||||
op.set('$match', (values: any[], options) => {
|
||||
const array = toArray(values);
|
||||
return {
|
||||
[Op.contains]: array,
|
||||
[Op.contained]: array
|
||||
};
|
||||
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(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;
|
||||
|
@ -20,7 +20,7 @@ export function toWhere(options: any, context: ToWhereContext = {}) {
|
||||
if (Array.isArray(options)) {
|
||||
return options.map((item) => toWhere(item, context));
|
||||
}
|
||||
const { prefix, model, associations = {}, ctx, dialect } = context;
|
||||
const { prefix, model, associations = {}, ctx, dialect, database } = context;
|
||||
const items = {};
|
||||
// 先处理「点号」的问题
|
||||
for (const key in options) {
|
||||
@ -54,7 +54,11 @@ export function toWhere(options: any, context: ToWhereContext = {}) {
|
||||
switch (typeof opKey) {
|
||||
case 'function':
|
||||
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') {
|
||||
values['$__literals'] = values['$__literals'] || [];
|
||||
values['$__literals'].push(result);
|
||||
@ -184,7 +188,7 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
|
||||
}
|
||||
|
||||
const { fields = [], filter } = options;
|
||||
const { model, sourceAlias, associations = {}, ctx, dialect } = context;
|
||||
const { model, sourceAlias, associations = {}, ctx, database, dialect } = context;
|
||||
|
||||
let where = options.where || {};
|
||||
|
||||
@ -193,6 +197,7 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
|
||||
model,
|
||||
associations,
|
||||
ctx,
|
||||
database,
|
||||
}) || {};
|
||||
}
|
||||
|
||||
|
@ -157,6 +157,7 @@ export const multipleSelect = {
|
||||
type: 'json', // json 过滤
|
||||
filterable: true,
|
||||
dataSource: [],
|
||||
defaultValue: [],
|
||||
multiple: true, // 需要重点考虑
|
||||
component: {
|
||||
type: 'select',
|
||||
@ -184,6 +185,7 @@ export const checkboxes = {
|
||||
type: 'json',
|
||||
filterable: true,
|
||||
dataSource: [],
|
||||
defaultValue: [],
|
||||
component: {
|
||||
type: 'checkboxes',
|
||||
},
|
||||
@ -387,7 +389,7 @@ export const createdBy = {
|
||||
interface: 'createdBy',
|
||||
type: 'createdBy',
|
||||
// name: 'createdBy',
|
||||
filterable: true,
|
||||
// filterable: true,
|
||||
target: 'users',
|
||||
labelField: 'nickname',
|
||||
foreignKey: 'created_by_id',
|
||||
@ -405,7 +407,7 @@ export const updatedBy = {
|
||||
interface: 'updatedBy',
|
||||
// name: 'updatedBy',
|
||||
type: 'updatedBy',
|
||||
filterable: true,
|
||||
// filterable: true,
|
||||
target: 'users',
|
||||
labelField: 'nickname',
|
||||
foreignKey: 'created_by_id',
|
||||
|
Loading…
Reference in New Issue
Block a user