diff --git a/application/Espo/Core/SelectManagers/Base.php b/application/Espo/Core/SelectManagers/Base.php index aa0bfac2d6..98ad14a892 100644 --- a/application/Espo/Core/SelectManagers/Base.php +++ b/application/Espo/Core/SelectManagers/Base.php @@ -742,7 +742,7 @@ class Base $attribute = $w['attribute']; } if ($attribute) { - if (isset($w['type']) && in_array($w['type'], ['isLinked', 'isNotLinked', 'linkedWith', 'isUserFromTeams'])) { + if (isset($w['type']) && in_array($w['type'], ['isLinked', 'isNotLinked', 'linkedWith', 'notLinkedWith', 'isUserFromTeams'])) { if (in_array($attribute, $this->getAcl()->getScopeForbiddenFieldList($this->getEntityType()))) { throw new Forbidden(); } @@ -958,6 +958,10 @@ class Base $attribute = $item['attribute']; } + if (!is_null($attribute) && !is_string($attribute)) { + throw new Error('Bad attribute in where statement'); + } + if (!empty($attribute) && !empty($item['type'])) { $methodName = 'getWherePart' . ucfirst($attribute) . ucfirst($item['type']); if (method_exists($this, $methodName)) { @@ -1159,14 +1163,14 @@ class Base break; case 'isNotLinked': if (!$result) break; - $alias = $attribute . 'IsNotLinkedFilter'; + $alias = $attribute . 'IsNotLinkedFilter' . strval(rand(10000, 99999));; $part[$alias . '.id'] = null; $this->setDistinct(true, $result); $this->addLeftJoin([$attribute, $alias], $result); break; case 'isLinked': if (!$result) break; - $alias = $attribute . 'IsLinkedFilter'; + $alias = $attribute . 'IsLinkedFilter' . strval(rand(10000, 99999));; $part[$alias . '.id!='] = null; $this->setDistinct(true, $result); $this->addLeftJoin([$attribute, $alias], $result); @@ -1176,35 +1180,75 @@ class Base $link = $attribute; if (!$seed->hasRelation($link)) break; + $alias = $link . 'Filter' . strval(rand(10000, 99999)); + + $value = $item['value']; + + if (is_null($value)) break; + + $relationType = $seed->getRelationType($link); + + if ($relationType == 'manyMany') { + $this->addLeftJoin([$link, $alias], $result); + $midKeys = $seed->getRelationParam($link, 'midKeys'); + + if (!empty($midKeys)) { + $key = $midKeys[1]; + $part[$alias . 'Middle.' . $key] = $value; + } + } else if ($relationType == 'hasMany') { + $this->addLeftJoin([$link, $alias], $result); + + $part[$alias . '.id'] = $value; + } else if ($relationType == 'belongsTo') { + $key = $seed->getRelationParam($link, 'key'); + if (!empty($key)) { + $part[$key] = $value; + } + } else if ($relationType == 'hasOne') { + $this->addLeftJoin([$link, $alias], $result); + $part[$alias . '.id'] = $value; + } else { + break;; + } + $this->setDistinct(true, $result); + break; + case 'notLinkedWith': + $seed = $this->getSeed(); + $link = $attribute; + if (!$seed->hasRelation($link)) break; + $value = $item['value']; if (is_null($value)) break; $relationType = $seed->getRelationType($link); + $alias = $link . 'NotLinkedFilter' . strval(rand(10000, 99999)); + if ($relationType == 'manyMany') { - $this->addLeftJoin([$link, $link . 'Filter'], $result); + $this->addLeftJoin([$link, $alias], $result); $midKeys = $seed->getRelationParam($link, 'midKeys'); if (!empty($midKeys)) { $key = $midKeys[1]; - $part[$link . 'Filter' . 'Middle.' . $key] = $value; + $result['joinConditions'][$alias] = [$key => $value]; + $part[$alias . 'Middle.' . $key] = null; } } else if ($relationType == 'hasMany') { - $alias = $link . 'Filter'; $this->addLeftJoin([$link, $alias], $result); - - $part[$alias . '.id'] = $value; + $result['joinConditions'][$alias] = ['id' => $value]; + $part[$alias . '.id'] = null; } else if ($relationType == 'belongsTo') { $key = $seed->getRelationParam($link, 'key'); if (!empty($key)) { - $part[$key] = $value; + $part[$key . '!='] = $value; } } else if ($relationType == 'hasOne') { - $this->addLeftJoin([$link, $link . 'Filter'], $result); - $part[$link . 'Filter' . '.id'] = $value; + $this->addLeftJoin([$link, alias], $result); + $part[$alias . '.id!='] = $value; } else { - return; + break; } $this->setDistinct(true, $result); } diff --git a/application/Espo/ORM/DB/Query/Base.php b/application/Espo/ORM/DB/Query/Base.php index fc7e81fb59..9969757320 100644 --- a/application/Espo/ORM/DB/Query/Base.php +++ b/application/Espo/ORM/DB/Query/Base.php @@ -850,6 +850,48 @@ abstract class Base return implode(' ', $joinsArr); } + protected function buildJoinConditionStatement($alias, $f, $v) + { + $join = ''; + + $operator = '='; + + if (!preg_match('/^[a-z0-9]+$/i', $f)) { + foreach (self::$comparisonOperators as $op => $opDb) { + if (strpos($f, $op) !== false) { + $f = trim(str_replace($op, '', $f)); + $operator = $opDb; + break; + } + } + } + + $join .= " AND {$alias}." . $this->toDb($this->sanitize($f)) . ""; + if (is_array($v)) { + $arr = []; + foreach ($v as $item) { + $arr[] = $this->pdo->quote($item); + } + $operator = "IN"; + if ($operator == '<>') { + $operator = 'NOT IN'; + } + if (count($arr)) { + $join .= " " . $operator . " (" . implode(', ', $arr) . ")"; + } else { + if ($operator === 'IN') { + $join .= " IS NULL"; + } else { + $join .= " IS NOT NULL"; + } + } + } else { + $join .= " " . $operator . " " . $this->pdo->quote($v); + } + + return $join; + } + protected function getJoinRelated(IEntity $entity, $relationName, $left = false, $conditions = array(), $joinAlias = null) { $relOpt = $entity->relations[$relationName]; @@ -890,7 +932,7 @@ abstract class Base $conditions = array_merge($conditions, $relOpt['conditions']); } foreach ($conditions as $f => $v) { - $join .= " AND {$midAlias}." . $this->toDb($this->sanitize($f)) . " = " . $this->pdo->quote($v); + $join .= $this->buildJoinConditionStatement($midAlias, $f, $v); } $join .= " {$pre}JOIN `{$distantTable}` AS `{$alias}` ON {$alias}." . $this->toDb($foreignKey) . " = {$midAlias}." . $this->toDb($distantKey) @@ -906,13 +948,15 @@ abstract class Base $alias = $joinAlias; - // TODO conditions - $join = "{$pre}JOIN `{$distantTable}` AS `{$alias}` ON {$this->toDb($entity->getEntityType())}." . $this->toDb('id') . " = {$alias}." . $this->toDb($foreignKey) . " AND " . "{$alias}.deleted = " . $this->pdo->quote(0) . ""; + foreach ($conditions as $f => $v) { + $join .= $this->buildJoinConditionStatement($alias, $f, $v); + } + return $join; case IEntity::HAS_CHILDREN: diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index f31478555a..811f837020 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -525,7 +525,8 @@ "isFromTeams": "Is From Team", "isNot": "Is Not", "isNotOneOf": "Is Not One Of", - "anyOf": "Any Of" + "anyOf": "Any Of", + "noneOf": "None Of" }, "varcharSearchRanges": { "equals": "Equals", diff --git a/client/src/views/fields/link-multiple.js b/client/src/views/fields/link-multiple.js index 7c8078d5de..885e124888 100644 --- a/client/src/views/fields/link-multiple.js +++ b/client/src/views/fields/link-multiple.js @@ -58,7 +58,7 @@ Espo.define('views/fields/link-multiple', 'views/fields/base', function (Dep) { sortable: false, - searchTypeList: ['anyOf', 'isEmpty', 'isNotEmpty'], + searchTypeList: ['anyOf', 'isEmpty', 'isNotEmpty', 'noneOf'], data: function () { var ids = this.model.get(this.idsName); @@ -148,7 +148,7 @@ Espo.define('views/fields/link-multiple', 'views/fields/base', function (Dep) { }, handleSearchType: function (type) { - if (~['anyOf'].indexOf(type)) { + if (~['anyOf', 'noneOf'].indexOf(type)) { this.$el.find('div.link-group-container').removeClass('hidden'); } else { this.$el.find('div.link-group-container').addClass('hidden'); @@ -349,6 +349,18 @@ Espo.define('views/fields/link-multiple', 'views/fields/base', function (Dep) { } }; return data; + } else if (type === 'noneOf') { + var values = this.ids || []; + + var data = { + type: 'notLinkedWith', + value: this.ids || [], + nameHash: this.nameHash, + data: { + type: type + } + }; + return data; } else if (type === 'isEmpty') { var data = { type: 'isNotLinked', diff --git a/client/src/views/fields/link.js b/client/src/views/fields/link.js index c1b16ae216..d96d8c1758 100644 --- a/client/src/views/fields/link.js +++ b/client/src/views/fields/link.js @@ -432,8 +432,8 @@ Espo.define('views/fields/link', 'views/fields/base', function (Dep) { value: [ { type: 'notIn', - attribute: this.searchData.oneOfIdList, - value: value + attribute: this.idName, + value: this.searchData.oneOfIdList }, { type: 'isNull',