@react-awesome-query-builder-dev/ui
Version:
User-friendly query builder for React. Core React UI
189 lines (167 loc) • 6.59 kB
JSX
import React, { Component } from "react";
import { Utils } from "@react-awesome-query-builder-dev/core";
import PropTypes from "prop-types";
import {truncateString} from "../../utils/stuff";
import {useOnPropsChanged} from "../../utils/reactUtils";
import last from "lodash/last";
import keys from "lodash/keys";
const {getFieldPathLabels} = Utils.RuleUtils;
const {getFieldConfig, getFieldParts, getFieldPathParts} = Utils.ConfigUtils;
export default class Field extends Component {
static propTypes = {
id: PropTypes.string,
groupId: PropTypes.string,
config: PropTypes.object.isRequired,
selectedField: PropTypes.any,
selectedFieldSrc: PropTypes.string,
selectedFieldType: PropTypes.string,
parentField: PropTypes.string,
customProps: PropTypes.object,
readonly: PropTypes.bool,
//actions
setField: PropTypes.func.isRequired,
setFieldSrc: PropTypes.func,
};
constructor(props) {
super(props);
useOnPropsChanged(this);
this.onPropsChanged(props);
}
onPropsChanged(nextProps) {
const prevProps = this.props;
const keysForMeta = ["selectedField", "selectedFieldSrc", "selectedFieldType", "config", "parentField"];
const needUpdateMeta = !this.meta || keysForMeta.map(k => (nextProps[k] !== prevProps[k])).filter(ch => ch).length > 0;
if (needUpdateMeta) {
this.meta = this.getMeta(nextProps);
}
}
getMeta({selectedField, selectedFieldType, config, parentField}) {
let selectedKey = selectedField;
const {maxLabelsLength, fieldSeparatorDisplay, fieldPlaceholder, fieldSeparator} = config.settings;
const isFieldSelected = !!selectedField;
const placeholder = !isFieldSelected ? truncateString(fieldPlaceholder, maxLabelsLength) : null;
const currField = isFieldSelected ? getFieldConfig(config, selectedKey) : null;
const selectedOpts = currField || {};
const selectedKeys = getFieldPathParts(selectedKey, config);
const selectedPath = getFieldPathParts(selectedKey, config, true);
const selectedLabel = this.getFieldLabel(currField, selectedKey, config);
const partsLabels = getFieldPathLabels(selectedKey, config);
let selectedFullLabel = partsLabels ? partsLabels.join(fieldSeparatorDisplay) : null;
if (selectedFullLabel == selectedLabel || parentField)
selectedFullLabel = null;
const selectedAltLabel = selectedOpts.label2 || selectedOpts.tooltip;
const parentFieldPath = getFieldParts(parentField, config);
const parentFieldConfig = parentField ? getFieldConfig(config, parentField) : null;
const sourceFields = parentField ? parentFieldConfig && parentFieldConfig.subfields : config.fields;
const lookingForFieldType = !isFieldSelected && selectedFieldType;
const items = this.buildOptions(parentFieldPath, config, sourceFields, lookingForFieldType, parentFieldPath);
// Field source has been chnaged, no new field selected, but op & value remains
const errorText = lookingForFieldType ? "Please select field" : null;
if (selectedKey && typeof selectedKey === "object") {
// can happen due to incorrect rule state: field is Map{func, args} but fieldSrc is not "func"
selectedKey = undefined;
}
return {
placeholder, items, parentField,
selectedKey, selectedKeys, selectedPath, selectedLabel, selectedOpts, selectedAltLabel, selectedFullLabel,
errorText,
};
}
getFieldLabel(fieldOpts, fieldKey, config) {
if (!fieldKey) return null;
let maxLabelsLength = config.settings.maxLabelsLength;
let fieldParts = getFieldParts(fieldKey, config);
let label = fieldOpts?.label || last(fieldParts);
label = truncateString(label, maxLabelsLength);
return label;
}
buildOptions(parentFieldPath, config, fields, fieldType = undefined, path = null, optGroup = null) {
if (!fields)
return null;
const {fieldSeparator, fieldSeparatorDisplay} = config.settings;
const prefix = path?.length ? path.join(fieldSeparator) + fieldSeparator : "";
const countFieldsMatchesType = (fields) => {
return Object.keys(fields || {}).reduce((acc, fieldKey) => {
const field = fields[fieldKey];
if (field.type === "!struct") {
return acc + countFieldsMatchesType(field.subfields);
} else {
return acc + (field.type === fieldType ? 1 : 0);
}
}, 0);
};
return keys(fields).map(fieldKey => {
const fullFieldPath = [...(path ?? []), fieldKey];
const field = fields[fieldKey];
const label = this.getFieldLabel(field, fullFieldPath, config);
const partsLabels = getFieldPathLabels(fullFieldPath, config);
let fullLabel = partsLabels.join(fieldSeparatorDisplay);
if (fullLabel == label || parentFieldPath?.length)
fullLabel = null;
const altLabel = field.label2;
const tooltip = field.tooltip;
const disabled = field.disabled;
if (field.hideForSelect)
return undefined;
if (field.type == "!struct") {
const items = this.buildOptions(parentFieldPath, config, field.subfields, fieldType, fullFieldPath, {
label,
tooltip,
});
const hasItemsMatchesType = countFieldsMatchesType(field.subfields) > 0;
return {
disabled,
key: fieldKey,
path: prefix+fieldKey,
label,
fullLabel,
altLabel,
tooltip,
items,
matchesType: hasItemsMatchesType,
};
} else {
const matchesType = fieldType !== undefined ? field.type === fieldType : undefined;
return {
disabled,
key: fieldKey,
path: prefix+fieldKey,
label,
fullLabel,
altLabel,
tooltip,
grouplabel: optGroup?.label,
group: optGroup,
matchesType,
};
}
}).filter(o => !!o);
}
setField = (field, asyncListValues, _meta = {}) => {
const {id} = this.props;
if (!_meta.widgetId) {
const widgetId = [
id,
"L",
-1,
].join(":");
_meta.widgetId = widgetId;
}
this.props.setField(field, asyncListValues, _meta);
};
render() {
const {config, customProps, setFieldSrc, readonly, id, groupId} = this.props;
const {renderField} = config.settings;
const renderProps = {
id,
groupId,
config,
customProps,
readonly,
setField: this.setField,
setFieldSrc,
...this.meta
};
return renderField(renderProps, config.ctx);
}
}