diff --git a/CHANGELOG.md b/CHANGELOG.md index 77cdae771..9c9b4a2fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Autocomplete items now support `groupTitle` (PR #953) (issue #600) - Add more types in `Utils.Autocomplete` (PR #953) (issue #934) - Add settings `fieldItemKeysForSearch` and `listKeysForSearch` (PR #954) (issue #931) + - Add settings `defaultField` and `defaultOperator` (PR #956) (issue #763) - 6.3.0 - Allow saving and loading config from server (PR #866) (issue #817) - New utils: `compressConfig()`, `decompressConfig()` diff --git a/packages/core/modules/actions/tree.js b/packages/core/modules/actions/tree.js index 8e8c04661..cb576e882 100644 --- a/packages/core/modules/actions/tree.js +++ b/packages/core/modules/actions/tree.js @@ -20,13 +20,13 @@ export const setTree = (config, tree) => ({ * @param {Immutable.List} path * @param {Immutable.Map} properties */ -export const addRule = (config, path, properties, ruleType = "rule", children = null) => ({ +export const addRule = (config, path, properties, ruleType = "rule", children = null, parentRuleGroupPath = null) => ({ type: constants.ADD_RULE, ruleType: ruleType, children: children, path: toImmutableList(path), id: uuid(), - properties: defaultRuleProperties(config).merge(properties || {}), + properties: defaultRuleProperties(config, parentRuleGroupPath).merge(properties || {}), config: config }); diff --git a/packages/core/modules/import/tree.js b/packages/core/modules/import/tree.js index d5e393960..118921a1d 100644 --- a/packages/core/modules/import/tree.js +++ b/packages/core/modules/import/tree.js @@ -17,13 +17,13 @@ export const loadTree = (serTree) => { if (isImmutableTree(serTree)) { return serTree; } else if (isTree(serTree)) { - return jsTreeToImmutable(serTree); + return jsToImmutable(serTree); } else if (typeof serTree == "string" && serTree.startsWith('["~#iM"')) { //tip: old versions of RAQB were saving tree with `transit.toJSON()` // https://github.com/ukrbublik/react-awesome-query-builder/issues/69 throw "You are trying to load query in obsolete serialization format (Immutable string) which is not supported in versions starting from 2.1.17"; } else if (typeof serTree == "string") { - return jsTreeToImmutable(JSON.parse(serTree)); + return jsToImmutable(JSON.parse(serTree)); } else throw "Can't load tree!"; }; @@ -47,8 +47,8 @@ export const isTree = (tree) => { export {isJsonLogic}; -function jsTreeToImmutable(tree) { - return fromJS(tree, function (key, value) { +export function jsToImmutable(tree) { + const imm = fromJS(tree, function (key, value) { let outValue; if (key == "properties") { outValue = value.toOrderedMap(); @@ -83,5 +83,6 @@ function jsTreeToImmutable(tree) { } return outValue; }); + return imm; } diff --git a/packages/core/modules/index.d.ts b/packages/core/modules/index.d.ts index b6cb99e8f..a6f734b75 100644 --- a/packages/core/modules/index.d.ts +++ b/packages/core/modules/index.d.ts @@ -8,6 +8,7 @@ export type Moment = MomentType; export type ImmutableList = ImmList; export type ImmutableMap = ImmMap; export type ImmutableOMap = ImmOMap; +export type AnyImmutable = ImmutableList | ImmutableMap | ImmutableOMap; //////////////// // common @@ -27,7 +28,8 @@ type AnyObject = { }; type Empty = null | undefined; -type IdPath = Array | ImmutableList; +type ImmutablePath = ImmutableList; +type IdPath = Array | ImmutablePath; type Optional = { [P in keyof T]?: T[P]; @@ -75,19 +77,50 @@ export type ConfigContext = { [key: string]: any; }; +export type FlatItem = { + type: ItemType; + parent: string | null; + parentType: ItemType; + caseId: string; + isDefaultCase: boolean; + path: string[]; + lev: number; + leaf: boolean; + index: number; + id: string; + children: string[]; + leafsCount: number; + _top: number; + _height: number; + top: number; + height: number; + bottom: number; + collapsed: boolean; + node: ImmutableItem; + isLocked: boolean; +}; +export type FlatTree = { + flat: string[]; + items: TypedMap; +}; + //////////////// // query value ///////////////// export type RuleValue = boolean | number | string | Date | Array | any; export type FieldPath = string; -export type FieldFuncValue = ImmutableMap<"func" | "args", any>; -export type FieldValue = FieldPath | FieldFuncValue; +export type FieldFuncValueI = ImmutableMap<"func" | "args", any>; +export interface FieldFuncValue { + func: string, + args: Record +} +export type FieldValue = FieldPath | FieldFuncValueI; export type ValueSource = "value" | "field" | "func" | "const"; export type FieldSource = "field" | "func"; export type RuleGroupMode = "struct" | "some" | "array"; -export type ItemType = "group" | "rule_group" | "rule"; +export type ItemType = "group" | "rule_group" | "rule" | "case_group" | "switch_group"; export type ItemProperties = RuleProperties | RuleGroupExtProperties | RuleGroupProperties | GroupProperties; export type TypedValueSourceMap = { @@ -172,7 +205,7 @@ type JsonRule = { export type JsonTree = JsonGroup|JsonSwitchGroup; export type ImmutableTree = ImmutableOMap; - +export type ImmutableItem = ImmutableOMap; //////////////// // Utils @@ -195,8 +228,23 @@ interface SpelConcatNormalValue { } type SpelConcatValue = SpelConcatNormalValue | SpelConcatCaseValue; -export interface Utils { - // export +interface Import { + // tree + getTree(tree: ImmutableTree, light?: boolean, children1AsArray?: boolean): JsonTree; + loadTree(jsonTree: JsonTree): ImmutableTree; + checkTree(tree: ImmutableTree, config: Config): ImmutableTree; + isValidTree(tree: ImmutableTree): boolean; + isImmutableTree(tree: any): boolean; + isTree(tree: any): boolean; // is JsonTree ? + isJsonLogic(value: any): boolean; + jsToImmutable(value: any): AnyImmutable; + // jsonlogic + loadFromJsonLogic(logicTree: JsonLogicTree | undefined, config: Config): ImmutableTree | undefined; + _loadFromJsonLogic(logicTree: JsonLogicTree | undefined, config: Config): [ImmutableTree | undefined, Array]; + // spel + loadFromSpel(spelStr: string, config: Config): [ImmutableTree | undefined, Array]; +} +interface Export { jsonLogicFormat(tree: ImmutableTree, config: Config): JsonLogicResult; // @deprecated queryBuilderFormat(tree: ImmutableTree, config: Config): Object | undefined; @@ -208,59 +256,94 @@ export interface Utils { mongodbFormat(tree: ImmutableTree, config: Config): Object | undefined; _mongodbFormat(tree: ImmutableTree, config: Config): [Object | undefined, Array]; elasticSearchFormat(tree: ImmutableTree, config: Config, syntax?: "ES_6_SYNTAX" | "ES_7_SYNTAX"): Object | undefined; - // load, save - getTree(tree: ImmutableTree, light?: boolean, children1AsArray?: boolean): JsonTree; - loadTree(jsonTree: JsonTree): ImmutableTree; - checkTree(tree: ImmutableTree, config: Config): ImmutableTree; - isValidTree(tree: ImmutableTree): boolean; - getSwitchValues(tree: ImmutableTree): Array; - // import - loadFromJsonLogic(logicTree: JsonLogicTree | undefined, config: Config): ImmutableTree | undefined; - _loadFromJsonLogic(logicTree: JsonLogicTree | undefined, config: Config): [ImmutableTree | undefined, Array]; - loadFromSpel(spelStr: string, config: Config): [ImmutableTree | undefined, Array]; +} +interface Autocomplete { + simulateAsyncFetch(all: ListValues, pageSize?: number, delay?: number): AsyncFetchListValuesFn; + getListValue(value: string | number, listValues: ListValues): ListItem; // get by value + // internal + mergeListValues(oldValues: ListItems, newValues: ListItems, toStart = false): ListItems; + listValueToOption(listItem: ListItem): ListOptionUi; +} +interface ConfigUtils { + compressConfig(config: Config, baseConfig: Config): ZipConfig; + decompressConfig(zipConfig: ZipConfig, baseConfig: Config, ctx?: ConfigContext): Config; + compileConfig(config: Config): Config; + extendConfig(config: Config): Config; + getFieldConfig(config: Config, field: FieldValue): Field | Func | null; + getFuncConfig(config: Config, func: string): Func | null; + getFuncArgConfig(config: Config, func: string, arg: string): FuncArg | null; + getOperatorConfig(config: Config, operator: string, field?: FieldValue): Operator | null; + getFieldWidgetConfig(config: Config, field: FieldValue, operator: string, widget?: string, valueStr?: ValueSource): Widget | null; + isJSX(jsx: any): boolean; + isDirtyJSX(jsx: any): boolean; + cleanJSX(jsx: any): Object; + applyJsonLogic(logic: any, data?: any): any; +} +interface ExportUtils { + spelEscape(val: any): string; + spelFormatConcat(parts: SpelConcatParts): string; + spelImportConcat(val: SpelConcatValue): [SpelConcatParts | undefined, Array]; +} +interface ListUtils { + getTitleInListValues(listValues: ListValues, value: string | number): string; + getListValue(value: string | number, listValues: ListValues): ListItem; // get by value + searchListValue(search: string, listValues: ListValues): ListItem; // search by value and title + listValuesToArray(listValues: ListValues): ListItems; // normalize + toListValue(value: string | number | ListItem, title?: string): ListItem; // create + makeCustomListValue(value: string | number): ListItem; // create +} +interface TreeUtils { + jsToImmutable(value: any): AnyImmutable; + immutableToJs(imm: AnyImmutable): any; + isImmutable(value: any): boolean; + toImmutableList(path: string[]): ImmutablePath; + getItemByPath(tree: ImmutableTree, path: IdPath): ImmutableItem; + expandTreePath(path: ImmutablePath, ...suffix: string[]): ImmutablePath; + expandTreeSubpath(path: ImmutablePath, ...suffix: string[]): ImmutablePath; + fixEmptyGroupsInTree(tree: ImmutableTree): ImmutableTree; + fixPathsInTree(tree: ImmutableTree): ImmutableTree; + getFlatTree(tree: ImmutableTree): FlatTree; + getTotalReordableNodesCountInTree(tree: ImmutableTree): number; + getTotalRulesCountInTree(tree: ImmutableTree): number; + getTreeBadFields(tree: ImmutableTree): Array; + isEmptyTree(tree: ImmutableTree): boolean; +} +interface OtherUtils { + uuid(): string; + deepEqual(a: any, b: any): boolean; + shallowEqual(a: any, b: any, deep = false): boolean; + mergeArraysSmart(a: string[], b: string[]): string[]; + isJsonCompatible(tpl: object, target: object, bag: Record): boolean; // mutates bag isJsonLogic(value: any): boolean; + isJSX(jsx: any): boolean; + isDirtyJSX(jsx: any): boolean; + cleanJSX(jsx: any): Object; + escapeRegExp(str: string): string; + //applyToJS(imm: any): any; // same as immutableToJs + isImmutable(value: any): boolean; + toImmutableList(path: string[]): ImmutablePath; +} + +export interface Utils extends Import, Export { + // case mode + getSwitchValues(tree: ImmutableTree): Array; // other uuid(): string; // ssr compressConfig(config: Config, baseConfig: Config): ZipConfig; decompressConfig(zipConfig: ZipConfig, baseConfig: Config, ctx?: ConfigContext): Config; + // validation + validateTree(tree: ImmutableTree, _oldTree: ImmutableTree, config: Config, oldConfig: Config, removeEmptyGroups?: boolean, removeIncompleteRules?: boolean): ImmutableTree; + validateAndFixTree(tree: ImmutableTree, _oldTree: ImmutableTree, config: Config, oldConfig: Config, removeEmptyGroups?: boolean, removeIncompleteRules?: boolean): ImmutableTree; - Autocomplete: { - simulateAsyncFetch(all: ListValues, pageSize?: number, delay?: number): AsyncFetchListValuesFn; - getListValue(value: string | number, listValues: ListValues): ListItem; // get by value - // internal - mergeListValues(oldValues: ListItems, newValues: ListItems, toStart = false): ListItems; - listValueToOption(listItem: ListItem): ListOptionUi; - }; - ConfigUtils: { - compressConfig(config: Config, baseConfig: Config): ZipConfig; - decompressConfig(zipConfig: ZipConfig, baseConfig: Config, ctx?: ConfigContext): Config; - compileConfig(config: Config): Config; - extendConfig(config: Config): Config; - getFieldConfig(config: Config, field: FieldValue): Field | Func | null; - getFuncConfig(config: Config, func: string): Func | null; - getFuncArgConfig(config: Config, func: string, arg: string): FuncArg | null; - getOperatorConfig(config: Config, operator: string, field?: FieldValue): Operator | null; - getFieldWidgetConfig(config: Config, field: FieldValue, operator: string, widget?: string, valueStr?: ValueSource): Widget | null; - isJsonLogic(value: any): boolean; - isJSX(jsx: any): boolean; - isDirtyJSX(jsx: any): boolean; - cleanJSX(jsx: any): Object; - applyJsonLogic(logic: any, data?: any): any; - }; - ExportUtils: { - spelEscape(val: any): string; - spelFormatConcat(parts: SpelConcatParts): string; - spelImportConcat(val: SpelConcatValue): [SpelConcatParts | undefined, Array], - }, - ListUtils: { - getTitleInListValues(listValues: ListValues, value: string | number): string; - getListValue(value: string | number, listValues: ListValues): ListItem; // get by value - searchListValue(search: string, listValues: ListValues): ListItem; // search by value and title - listValuesToArray(listValues: ListValues): ListItems; // normalize - toListValue(value: string | number | ListItem, title?: string): ListItem; // create - makeCustomListValue(value: string | number): ListItem; // create - } + Import: Import; + Export: Export; + Autocomplete: Autocomplete; + ConfigUtils: ConfigUtils; + ExportUtils: ExportUtils; + ListUtils: ListUtils; + TreeUtils: TreeUtils; + OtherUtils: OtherUtils; } @@ -841,6 +924,7 @@ interface FieldGroup extends BaseField { mode: RuleGroupMode, isSpelArray?: boolean, isSpelItemMap?: boolean, + defaultField?: FieldPath, } interface FieldGroupExt extends BaseField { type: "!group", @@ -848,6 +932,7 @@ interface FieldGroupExt extends BaseField { mode: "array", operators?: Array, defaultOperator?: string, + defaultField?: FieldPath, initialEmptyWhere?: boolean, showNot?: boolean, conjunctions?: Array, @@ -938,6 +1023,8 @@ export interface LocaleSettings { export interface BehaviourSettings { + defaultField?: FieldPath | FieldFuncValue | FieldFuncValueI, + defaultOperator?: string; fieldSources?: Array, valueSourcesInfo?: ValueSourcesInfo, canCompareFieldWithField?: CanCompareFieldWithField | SerializedFunction, diff --git a/packages/core/modules/stores/tree.js b/packages/core/modules/stores/tree.js index 91d0a4cb6..441c31ff9 100644 --- a/packages/core/modules/stores/tree.js +++ b/packages/core/modules/stores/tree.js @@ -4,7 +4,7 @@ import { getTotalRulesCountInTree, fixEmptyGroupsInTree, isEmptyTree, hasChildren, removeIsLockedInTree } from "../utils/treeUtils"; import { - defaultRuleProperties, defaultGroupProperties, defaultOperator, + defaultRuleProperties, defaultGroupProperties, getDefaultOperator, defaultOperatorOptions, defaultRoot, defaultItemProperties } from "../utils/defaultUtils"; import * as constants from "./constants"; @@ -66,8 +66,14 @@ const removeGroup = (state, path, config) => { state = fixEmptyGroupsInTree(state); if (isEmptyTree(state) && !canLeaveEmptyGroup) { - // if whole query is empty, add one empty rule to root - state = addItem(state, new Immutable.List(), "rule", uuid(), defaultRuleProperties(config), config); + // if whole query is empty, add one empty(!) rule to root + const canUseDefaultFieldAndOp = false; + const canGetFirst = false; + state = addItem( + state, new Immutable.List(), "rule", uuid(), + defaultRuleProperties(config, undefined, undefined, canUseDefaultFieldAndOp, canGetFirst), + config + ); } } state = fixPathsInTree(state); @@ -108,8 +114,14 @@ const removeRule = (state, path, config) => { state = fixEmptyGroupsInTree(state); if (isEmptyTree(state) && !canLeaveEmptyGroup) { - // if whole query is empty, add one empty rule to root - state = addItem(state, new Immutable.List(), "rule", uuid(), defaultRuleProperties(config), config); + // if whole query is empty, add one empty(!) rule to root + const canUseDefaultFieldAndOp = false; + const canGetFirst = false; + state = addItem( + state, new Immutable.List(), "rule", uuid(), + defaultRuleProperties(config, undefined, undefined, canUseDefaultFieldAndOp, canGetFirst), + config + ); } } state = fixPathsInTree(state); @@ -341,7 +353,7 @@ const setFieldSrc = (state, path, srcKey, config) => { // clear ALL properties state = state.setIn( expandTreePath(path, "properties"), - defaultRuleProperties(config) + defaultRuleProperties(config, null, null, false) ); } else { // clear non-relevant properties @@ -410,7 +422,7 @@ const setField = (state, path, newField, config, asyncListValues, __isInternal) if (strategy == "keep" && !isChangeToAnotherType) newOperator = lastOp; else if (strategy == "default") - newOperator = defaultOperator(config, newField, false); + newOperator = getDefaultOperator(config, newField, false); else if (strategy == "first") newOperator = getFirstOperator(config, newField); if (newOperator) //found op for strategy diff --git a/packages/core/modules/utils/configUtils.js b/packages/core/modules/utils/configUtils.js index 6d1f8422c..cae241f75 100644 --- a/packages/core/modules/utils/configUtils.js +++ b/packages/core/modules/utils/configUtils.js @@ -225,6 +225,7 @@ function _extendFieldConfig(fieldConfig, config, path = null, isFuncArg = false) if (!isFuncArg) { if (!fieldConfig.operators && operators) fieldConfig.operators = Array.from(new Set(operators)); + fieldConfig._origDefaultOperator = fieldConfig.defaultOperator; if (!fieldConfig.defaultOperator && defaultOperator) fieldConfig.defaultOperator = defaultOperator; } diff --git a/packages/core/modules/utils/defaultUtils.js b/packages/core/modules/utils/defaultUtils.js index d14fe9771..a92424671 100644 --- a/packages/core/modules/utils/defaultUtils.js +++ b/packages/core/modules/utils/defaultUtils.js @@ -1,31 +1,52 @@ import Immutable from "immutable"; import uuid from "./uuid"; -import {getFieldConfig, getOperatorConfig} from "./configUtils"; +import {getFieldConfig, getOperatorConfig, getFieldParts} from "./configUtils"; import {getNewValueForFieldOp, getFirstField, getFirstOperator} from "../utils/ruleUtils"; +import { isImmutable } from "./stuff"; +import { jsToImmutable } from "../import"; -export const defaultField = (config, canGetFirst = true, parentRuleGroupPath = null, fieldSrc = "field") => { - if (fieldSrc !== "field") - return null; // don't return default function (yet) - return typeof config.settings.defaultField === "function" - ? config.settings.defaultField(parentRuleGroupPath) - : (config.settings.defaultField || (canGetFirst ? getFirstField(config, parentRuleGroupPath) : null)); +export const getDefaultField = (config, canGetFirst = true, parentRuleGroupPath = null) => { + const {defaultField} = config.settings; + let f = (!parentRuleGroupPath ? defaultField : getDefaultSubField(config, parentRuleGroupPath)) + || canGetFirst && getFirstField(config, parentRuleGroupPath) + || null; + // if default LHS is func, convert to Immutable + if (f != null && typeof f !== "string" && !isImmutable(f)) { + f = jsToImmutable(f); + } + return f; }; -export const defaultFieldSrc = (config, canGetFirst = true) => { +export const getDefaultSubField = (config, parentRuleGroupPath = null) => { + if (!parentRuleGroupPath) + return null; + const fieldSeparator = config?.settings?.fieldSeparator || "."; + const parentRuleGroupConfig = getFieldConfig(config, parentRuleGroupPath); + let f = parentRuleGroupConfig?.defaultField; + if (f) { + f = [...getFieldParts(parentRuleGroupPath), f].join(fieldSeparator); + } + return f; +}; + +export const getDefaultFieldSrc = (config, canGetFirst = true) => { return canGetFirst && config.settings.fieldSources?.[0] || "field"; }; -export const defaultOperator = (config, field, canGetFirst = true) => { - let fieldConfig = getFieldConfig(config, field); - let fieldOperators = fieldConfig && fieldConfig.operators || []; - let fieldDefaultOperator = fieldConfig && fieldConfig.defaultOperator; - if (!fieldOperators.includes(fieldDefaultOperator)) +export const getDefaultOperator = (config, field, canGetFirst = true) => { + let {defaultOperator} = config.settings; + const fieldConfig = getFieldConfig(config, field); + const fieldOperators = fieldConfig?.operators || []; + if (defaultOperator && !fieldOperators.includes(defaultOperator)) + defaultOperator = null; + let fieldDefaultOperator = fieldConfig?.defaultOperator; + if (fieldDefaultOperator && !fieldOperators.includes(fieldDefaultOperator)) fieldDefaultOperator = null; if (!fieldDefaultOperator && canGetFirst) fieldDefaultOperator = getFirstOperator(config, field); - let op = typeof config.settings.defaultOperator === "function" - ? config.settings.defaultOperator(field, fieldConfig) : fieldDefaultOperator; + const fieldHasExplicitDefOp = fieldConfig?._origDefaultOperator; + const op = fieldHasExplicitDefOp && fieldDefaultOperator || defaultOperator || fieldDefaultOperator; return op; }; @@ -40,20 +61,23 @@ export const defaultOperatorOptions = (config, operator, field) => { ) : null; }; -export const defaultRuleProperties = (config, parentRuleGroupPath = null, item = null) => { - // tip: setDefaultFieldAndOp not documented +export const defaultRuleProperties = (config, parentRuleGroupPath = null, item = null, canUseDefaultFieldAndOp = true, canGetFirst = false) => { let field = null, operator = null, fieldSrc = null; - const {setDefaultFieldAndOp, showErrorMessage} = config.settings; + const {showErrorMessage} = config.settings; if (item) { fieldSrc = item?.properties?.fieldSrc; field = item?.properties?.field; operator = item?.properties?.operator; - } else if (setDefaultFieldAndOp) { - fieldSrc = defaultFieldSrc(config); - field = defaultField(config, true, parentRuleGroupPath, fieldSrc); - operator = defaultOperator(config, field, fieldSrc); + } else if (canUseDefaultFieldAndOp) { + field = getDefaultField(config, canGetFirst, parentRuleGroupPath); + if (field) { + fieldSrc = isImmutable(field) ? "func" : "field"; + } else { + fieldSrc = getDefaultFieldSrc(config); + } + operator = getDefaultOperator(config, field, true); } else { - fieldSrc = defaultFieldSrc(config); + fieldSrc = getDefaultFieldSrc(config); } let current = new Immutable.Map({ fieldSrc: fieldSrc, diff --git a/packages/core/modules/utils/index.js b/packages/core/modules/utils/index.js index 60a77c6be..5ab10e328 100644 --- a/packages/core/modules/utils/index.js +++ b/packages/core/modules/utils/index.js @@ -10,5 +10,6 @@ export * as TreeUtils from "./treeUtils"; export * as ExportUtils from "./export"; export * as ListUtils from "./listValues"; export * as Autocomplete from "./autocomplete"; +export * as OtherUtils from "./stuff"; export {getSwitchValues} from "./treeUtils"; export {compressConfig, decompressConfig} from "./configSerialize"; diff --git a/packages/core/modules/utils/ruleUtils.js b/packages/core/modules/utils/ruleUtils.js index 2fa4b6bab..12a670507 100644 --- a/packages/core/modules/utils/ruleUtils.js +++ b/packages/core/modules/utils/ruleUtils.js @@ -202,7 +202,7 @@ export const getFirstField = (config, parentRuleGroupPath = null) => { let firstField = parentField, key = null, keysPath = []; do { - const subfields = firstField === config ? config.fields : firstField.subfields; + const subfields = firstField === config ? config.fields : firstField?.subfields; if (!subfields || !Object.keys(subfields).length) { firstField = key = null; break; @@ -226,7 +226,7 @@ export const getOperatorsForField = (config, field) => { export const getFirstOperator = (config, field) => { const fieldOps = getOperatorsForField(config, field); - return fieldOps ? fieldOps[0] : null; + return fieldOps?.[0] ?? null; }; export const getFuncPathLabels = (field, config, parentField = null) => { diff --git a/packages/core/modules/utils/stuff.js b/packages/core/modules/utils/stuff.js index bf6718e63..9fce8fc04 100644 --- a/packages/core/modules/utils/stuff.js +++ b/packages/core/modules/utils/stuff.js @@ -1,8 +1,11 @@ import Immutable, { Map } from "immutable"; import omit from "lodash/omit"; +import {default as uuid} from "./uuid"; const isObject = (v) => (typeof v == "object" && v !== null && !Array.isArray(v)); +export {uuid}; + export const widgetDefKeysToOmit = [ "formatValue", "mongoFormatValue", "sqlFormatValue", "jsonLogic", "elasticSearchFormatValue", "spelFormatValue", "spelImportFuncs", "spelImportValue" ]; @@ -116,7 +119,7 @@ function shallowEqualObjects(objA, objB, deep = false) { return true; } -const isImmutable = (v) => { +export const isImmutable = (v) => { return typeof v === "object" && v !== null && typeof v.toJS === "function"; }; diff --git a/packages/core/modules/utils/treeUtils.js b/packages/core/modules/utils/treeUtils.js index 1a7c05735..8a8521ec4 100644 --- a/packages/core/modules/utils/treeUtils.js +++ b/packages/core/modules/utils/treeUtils.js @@ -1,4 +1,8 @@ import Immutable from "immutable"; +import {toImmutableList, isImmutable, applyToJS as immutableToJs} from "./stuff"; +import {jsToImmutable} from "../import/tree"; + +export {toImmutableList, jsToImmutable, immutableToJs, isImmutable}; /** * @param {Immutable.List} path @@ -26,7 +30,7 @@ export const expandTreeSubpath = (path, ...suffix) => /** - * @param {Immutable.Map} path + * @param {Immutable.Map} tree * @param {Immutable.List} path * @return {Immutable.Map} */ @@ -46,27 +50,27 @@ export const getItemByPath = (tree, path) => { * @param {Immutable.Map} tree * @return {Immutable.Map} tree */ -export const removePathsInTree = (tree) => { - let newTree = tree; +// export const removePathsInTree = (tree) => { +// let newTree = tree; - function _processNode (item, path) { - const itemPath = path.push(item.get("id")); - if (item.get("path")) { - newTree = newTree.removeIn(expandTreePath(itemPath, "path")); - } +// function _processNode (item, path) { +// const itemPath = path.push(item.get("id")); +// if (item.get("path")) { +// newTree = newTree.removeIn(expandTreePath(itemPath, "path")); +// } - const children = item.get("children1"); - if (children) { - children.map((child, _childId) => { - _processNode(child, itemPath); - }); - } - } +// const children = item.get("children1"); +// if (children) { +// children.map((child, _childId) => { +// _processNode(child, itemPath); +// }); +// } +// } - _processNode(tree, new Immutable.List()); +// _processNode(tree, new Immutable.List()); - return newTree; -}; +// return newTree; +// }; /** @@ -122,7 +126,7 @@ export const fixPathsInTree = (tree) => { const children = item.get("children1"); if (children) { if (children.constructor.name == "Map") { - // protect: should me OrderedMap, not Map (issue #501) + // protect: should be OrderedMap, not Map (issue #501) newTree = newTree.setIn( expandTreePath(itemPath, "children1"), new Immutable.OrderedMap(children) diff --git a/packages/examples/demo_switch/index.tsx b/packages/examples/demo_switch/index.tsx index 8e16c010a..441bf1750 100644 --- a/packages/examples/demo_switch/index.tsx +++ b/packages/examples/demo_switch/index.tsx @@ -81,6 +81,13 @@ const Demo: React.FC = () => {
         {JSON.stringify(QbUtils.getSwitchValues(state.tree), undefined, 2)}
       
+
+
+
+ Tree: +
+        {JSON.stringify(QbUtils.getTree(state.tree), undefined, 2)}
+      
); diff --git a/packages/tests/specs/InteractionsVanilla.test.js b/packages/tests/specs/InteractionsVanilla.test.js index 971450edd..1b5bf6e5b 100644 --- a/packages/tests/specs/InteractionsVanilla.test.js +++ b/packages/tests/specs/InteractionsVanilla.test.js @@ -3,11 +3,14 @@ const { getTree } = Utils; import * as configs from "../support/configs"; import * as inits from "../support/inits"; import { with_qb } from "../support/utils"; - +import chai from "chai"; +import deepEqualInAnyOrder from "deep-equal-in-any-order"; +chai.use(deepEqualInAnyOrder); +const { expect } = chai; describe("interactions on vanilla", () => { it("click on remove single rule will leave empty rule if canLeaveEmptyGroup=false", async () => { - await with_qb(configs.dont_leave_empty_group, inits.with_number, "JsonLogic", (qb, onChange) => { + await with_qb([configs.dont_leave_empty_group, configs.with_default_field_and_operator], inits.with_number, "JsonLogic", (qb, onChange) => { qb .find(".rule .rule--header button") .first() @@ -50,6 +53,47 @@ describe("interactions on vanilla", () => { }); }); + it("click on add rule will add default rule if defaultField/defaultOperator is present", async () => { + await with_qb([configs.simple_with_numbers_and_str, configs.with_default_field_and_operator], inits.empty, "JsonLogic", (qb, onChange) => { + qb + .find(".group--actions button") + .first() + .simulate("click"); + const changedTree = getTree(onChange.getCall(0).args[0]); + const childKeys = Object.keys(changedTree.children1); + expect(childKeys.length).to.equal(1); + const child = changedTree.children1[childKeys[0]]; + expect(child.properties.field).to.equal("str"); + expect(child.properties.operator).to.equal("like"); + expect(child.properties.value).to.eql([undefined]); + }); + }); + + it("click on add rule will add default rule with func at LHS if defaultField is present", async () => { + await with_qb([configs.simple_with_numbers_and_str, configs.with_default_func_field, configs.with_funcs], inits.empty, "JsonLogic", (qb, onChange) => { + qb + .find(".group--actions button") + .first() + .simulate("click"); + const changedTree = getTree(onChange.getCall(0).args[0]); + const childKeys = Object.keys(changedTree.children1); + expect(childKeys.length).to.equal(1); + const child = changedTree.children1[childKeys[0]]; + expect(child.properties.fieldSrc).to.equal("func"); + expect(child.properties.field).to.deep.equalInAnyOrder({ + func: "LOWER", + args: { + str: { + valueSrc: "field", + value: "str" + } + } + }); + expect(child.properties.operator).to.equal("like"); + expect(child.properties.value).to.eql([undefined]); + }); + }); + it("click on add group will add new group with one empty rule if shouldCreateEmptyGroup=false", async () => { await with_qb(configs.dont_leave_empty_group, inits.with_number, "JsonLogic", (qb, onChange) => { qb @@ -72,6 +116,35 @@ describe("interactions on vanilla", () => { }); }); + it("click on add group will add new group with one default rule if shouldCreateEmptyGroup=false AND defaultField is present", async () => { + await with_qb([configs.dont_leave_empty_group, configs.with_default_field_and_operator], inits.with_number, "JsonLogic", (qb, onChange) => { + qb + .find(".group--actions button") + .at(1) + .simulate("click"); + const changedTree = getTree(onChange.getCall(0).args[0]); + const childKeys = Object.keys(changedTree.children1); + expect(childKeys.length).to.equal(2); + const child = changedTree.children1[childKeys[1]]; + expect(child.type).to.equal("group"); + expect(child.properties.conjunction).to.equal("AND"); //default + const subchildKeys = Object.keys(child.children1); + const subchild = child.children1[subchildKeys[0]]; + expect(JSON.stringify(subchild)).to.eql(JSON.stringify({ + type: "rule", + id: subchild.id, + properties: { + fieldSrc: "field", + field: "str", + operator: "like", + value: [undefined], + valueSrc: ["value"], + valueType: ["text"] + }, + })); + }); + }); + it("change field to of same type will same op & value", async () => { await with_qb(configs.simple_with_numbers_and_str, inits.with_number, "JsonLogic", (qb, onChange) => { qb @@ -147,8 +220,8 @@ describe("interactions on vanilla", () => { }); }); - it("change field from simple to group_ext", async () => { - await with_qb(configs.with_group_array_cars, inits.with_text, "JsonLogic", (qb, onChange) => { + it("change field from simple to group_ext, will add empty subfield", async () => { + await with_qb([configs.with_group_array_cars, configs.with_default_field_and_operator], inits.with_text, "JsonLogic", (qb, onChange) => { qb .find(".rule .rule--field select") .simulate("change", { target: { value: "cars" } }); @@ -160,6 +233,35 @@ describe("interactions on vanilla", () => { expect(child.properties.operator).to.equal("some"); expect(child.properties.conjunction).to.equal("AND"); expect(child.properties.value).to.eql([]); + const subchildKeys = Object.keys(child.children1 || {}); + expect(subchildKeys.length).to.equal(1); + const subchild = child.children1[subchildKeys[0]]; + expect(subchild.properties.field).to.equal(null); + expect(subchild.properties.operator).to.equal(null); + expect(subchild.properties.value).to.eql([]); + }); + }); + + it("change field from simple to group_ext, will add default subfield if defaultField is set in group", async () => { + await with_qb([configs.with_group_array_cars, configs.with_default_field_in_cars], inits.with_text, "JsonLogic", (qb, onChange) => { + qb + .find(".rule .rule--field select") + .simulate("change", { target: { value: "cars" } }); + const changedTree = getTree(onChange.getCall(0).args[0]); + const childKeys = Object.keys(changedTree.children1); + expect(childKeys.length).to.equal(1); + const child = changedTree.children1[childKeys[0]]; + expect(child.properties.field).to.equal("cars"); + expect(child.properties.operator).to.equal("some"); + expect(child.properties.conjunction).to.equal("AND"); + expect(child.properties.value).to.eql([]); + const subchildKeys = Object.keys(child.children1); + expect(subchildKeys.length).to.equal(1); + const subchild = child.children1[subchildKeys[0]]; + expect(subchild.properties.field).to.equal("cars.year"); + expect(subchild.properties.operator).to.equal("equal"); + expect(subchild.properties.value).to.eql([undefined]); + expect(subchild.properties.valueSrc).to.eql(["value"]); }); }); diff --git a/packages/tests/support/configs.js b/packages/tests/support/configs.js index a57cd4b14..bf2781fc8 100644 --- a/packages/tests/support/configs.js +++ b/packages/tests/support/configs.js @@ -49,6 +49,32 @@ export const simple_with_2_numbers = (BasicConfig) => ({ }, }); +export const with_default_field_and_operator = (BasicConfig) => ({ + ...BasicConfig, + settings: { + ...BasicConfig.settings, + defaultField: "str", + defaultOperator: "like", + } +}); + +export const with_default_func_field = (BasicConfig) => ({ + ...BasicConfig, + settings: { + ...BasicConfig.settings, + defaultField: { + func: "LOWER", + args: { + str: { + valueSrc: "field", + value: "str" + } + } + }, + defaultOperator: "like", + } +}); + export const simple_with_numbers_and_str = (BasicConfig) => ({ ...BasicConfig, fields: { @@ -890,6 +916,13 @@ export const with_settings_show_lock = (BasicConfig) => ({ } }); +export const with_default_field_in_cars = (BasicConfig) => merge({}, BasicConfig, { + fields: { + cars: { + defaultField: "year" + } + } +}); export const with_group_array_cars = (BasicConfig) => ({ ...BasicConfig, diff --git a/packages/tests/support/inits.js b/packages/tests/support/inits.js index a06072dba..1693b99e4 100644 --- a/packages/tests/support/inits.js +++ b/packages/tests/support/inits.js @@ -38,6 +38,10 @@ export const tree_with_number = { } }; +export const empty = { + "and": [] +}; + export const with_number = { "and": [{ "==": [ diff --git a/packages/ui/modules/components/containers/GroupContainer.jsx b/packages/ui/modules/components/containers/GroupContainer.jsx index be7c3f8d2..c98d12afd 100644 --- a/packages/ui/modules/components/containers/GroupContainer.jsx +++ b/packages/ui/modules/components/containers/GroupContainer.jsx @@ -8,7 +8,7 @@ import {connect} from "react-redux"; const {defaultGroupConjunction} = Utils.DefaultUtils; -const createGroupContainer = (Group) => +const createGroupContainer = (Group, itemType) => class GroupContainer extends Component { static propTypes = { config: PropTypes.object.isRequired, @@ -124,7 +124,8 @@ const createGroupContainer = (Group) => }; addRule = () => { - this.props.actions.addRule(this.props.path); + const parentRuleGroupPath = itemType == "rule_group" ? this.props.field : null; + this.props.actions.addRule(this.props.path, undefined, undefined, undefined, parentRuleGroupPath); }; // for RuleGroup @@ -243,7 +244,7 @@ const createGroupContainer = (Group) => }; -export default (Group) => { +export default (Group, itemType) => { const ConnectedGroupContainer = connect( (state) => { return { @@ -255,7 +256,7 @@ export default (Group) => { { context } - )(createGroupContainer(Group)); + )(createGroupContainer(Group, itemType)); ConnectedGroupContainer.displayName = "ConnectedGroupContainer"; return ConnectedGroupContainer; diff --git a/packages/ui/modules/components/item/CaseGroup.jsx b/packages/ui/modules/components/item/CaseGroup.jsx index 3f790af1e..5a42e51c1 100644 --- a/packages/ui/modules/components/item/CaseGroup.jsx +++ b/packages/ui/modules/components/item/CaseGroup.jsx @@ -181,5 +181,5 @@ class CaseGroup extends BasicGroup { } -export default GroupContainer(Draggable("group case_group")(WithConfirmFn(CaseGroup))); +export default GroupContainer(Draggable("group case_group")(WithConfirmFn(CaseGroup)), "case_group"); diff --git a/packages/ui/modules/components/item/Group.jsx b/packages/ui/modules/components/item/Group.jsx index 99eb5613f..5ee09b62d 100644 --- a/packages/ui/modules/components/item/Group.jsx +++ b/packages/ui/modules/components/item/Group.jsx @@ -321,4 +321,4 @@ export class BasicGroup extends Component { } } -export default GroupContainer(Draggable("group")(WithConfirmFn(BasicGroup))); +export default GroupContainer(Draggable("group")(WithConfirmFn(BasicGroup)), "group"); diff --git a/packages/ui/modules/components/item/RuleGroup.jsx b/packages/ui/modules/components/item/RuleGroup.jsx index c5ad3d2c8..a44bc31d7 100644 --- a/packages/ui/modules/components/item/RuleGroup.jsx +++ b/packages/ui/modules/components/item/RuleGroup.jsx @@ -100,4 +100,4 @@ class RuleGroup extends BasicGroup { } -export default GroupContainer(Draggable("group rule_group")(WithConfirmFn(RuleGroup))); +export default GroupContainer(Draggable("group rule_group")(WithConfirmFn(RuleGroup)), "rule_group"); diff --git a/packages/ui/modules/components/item/RuleGroupExt.jsx b/packages/ui/modules/components/item/RuleGroupExt.jsx index b1710a1a9..57c2cfb28 100644 --- a/packages/ui/modules/components/item/RuleGroupExt.jsx +++ b/packages/ui/modules/components/item/RuleGroupExt.jsx @@ -232,5 +232,5 @@ class RuleGroupExt extends BasicGroup { } -export default GroupContainer(Draggable("group rule_group_ext")(WithConfirmFn(RuleGroupExt))); +export default GroupContainer(Draggable("group rule_group_ext")(WithConfirmFn(RuleGroupExt)), "rule_group"); diff --git a/packages/ui/modules/components/item/SwitchGroup.jsx b/packages/ui/modules/components/item/SwitchGroup.jsx index bce33afc2..e66940e2b 100644 --- a/packages/ui/modules/components/item/SwitchGroup.jsx +++ b/packages/ui/modules/components/item/SwitchGroup.jsx @@ -130,4 +130,4 @@ class SwitchGroup extends BasicGroup { } -export default GroupContainer(Draggable("group switch_group")(WithConfirmFn(SwitchGroup))); +export default GroupContainer(Draggable("group switch_group")(WithConfirmFn(SwitchGroup)), "switch_group"); diff --git a/packages/ui/modules/utils/stuff.js b/packages/ui/modules/utils/stuff.js index bd008c9d3..94ea316b6 100644 --- a/packages/ui/modules/utils/stuff.js +++ b/packages/ui/modules/utils/stuff.js @@ -97,16 +97,6 @@ function shallowEqualObjects(objA, objB, deep = false) { } -const isImmutable = (v) => { - return typeof v === "object" && v !== null && typeof v.toJS === "function"; -}; - - -// export function toImmutableList(v) { -// return (isImmutable(v) ? v : new Immutable.List(v)); -// } - - const isDev = () => (typeof process !== "undefined" && process.env && process.env.NODE_ENV == "development"); export const getLogger = (devMode = false) => { diff --git a/packages/ui/styles/styles.scss b/packages/ui/styles/styles.scss index 9b8e5583a..3c2d96968 100644 --- a/packages/ui/styles/styles.scss +++ b/packages/ui/styles/styles.scss @@ -738,15 +738,12 @@ $rule_actions: (".rule--fieldsrc", ".widget--valuesrc", ".rule--drag-handler", " /******************************************************************************/ .rule--body.can--shrink--value { - align-items: center; + //align-items: center; .rule--value { flex: 1; & > .rule--widget { - flex: 1; - & > .widget--valuesrc { - display: flex; - align-items: center; - } + width: 100%; + display: flex; & .widget--widget { flex: 1; }