diff --git a/app/Helpers/Collector/Extensions/MetaCollection.php b/app/Helpers/Collector/Extensions/MetaCollection.php index 334260173b..9f23d89abb 100644 --- a/app/Helpers/Collector/Extensions/MetaCollection.php +++ b/app/Helpers/Collector/Extensions/MetaCollection.php @@ -917,6 +917,7 @@ trait MetaCollection $list = $tags->pluck('tag')->toArray(); $filter = static function (array $object) use ($list): bool { foreach ($object['transactions'] as $transaction) { + app('log')->debug(sprintf('Transaction has %d tag(s)', count($transaction['tags']))); foreach ($transaction['tags'] as $tag) { if (in_array($tag['name'], $list, true)) { return false; diff --git a/app/Helpers/Collector/GroupCollector.php b/app/Helpers/Collector/GroupCollector.php index 030f3edc2a..60a3fe14a1 100644 --- a/app/Helpers/Collector/GroupCollector.php +++ b/app/Helpers/Collector/GroupCollector.php @@ -515,7 +515,6 @@ class GroupCollector implements GroupCollectorInterface // add to query: $this->query->orWhereIn('transaction_journals.transaction_group_id', $groupIds); } - $result = $this->query->get($this->fields); // now to parse this into an array. @@ -823,10 +822,15 @@ class GroupCollector implements GroupCollectorInterface private function postFilterCollection(Collection $collection): Collection { $currentCollection = $collection; + + app('log')->debug(sprintf('GroupCollector: postFilterCollection has %d filter(s) and %d transaction(s).', count($this->postFilters), count($currentCollection))); + + /** * @var Closure $function */ foreach ($this->postFilters as $function) { + app('log')->debug('Applying filter...'); $nextCollection = new Collection(); // loop everything in the current collection // and save it (or not) in the new collection. @@ -843,6 +847,7 @@ class GroupCollector implements GroupCollectorInterface $nextCollection->push($item); } $currentCollection = $nextCollection; + app('log')->debug(sprintf('GroupCollector: postFilterCollection has %d transaction(s) left.', count($currentCollection))); } return $currentCollection; } diff --git a/app/Repositories/Tag/TagRepository.php b/app/Repositories/Tag/TagRepository.php index 654309c22d..acee9cb444 100644 --- a/app/Repositories/Tag/TagRepository.php +++ b/app/Repositories/Tag/TagRepository.php @@ -87,7 +87,7 @@ class TagRepository implements TagRepositoryInterface */ public function get(): Collection { - return $this->user->tags()->orderBy('tag', 'ASC')->get(); + return $this->user->tags()->orderBy('tag', 'ASC')->get(['tags.*']); } /** @@ -454,4 +454,24 @@ class TagRepository implements TagRepositoryInterface /** @var Location|null */ return $tag->locations()->first(); } + + /** + * @inheritDoc + */ + public function tagStartsWith(string $query): Collection + { + $search = sprintf('%s%%', $query); + + return $this->user->tags()->where('tag', 'LIKE', $search)->get(['tags.*']); + } + + /** + * @inheritDoc + */ + public function tagEndsWith(string $query): Collection + { + $search = sprintf('%%%s', $query); + + return $this->user->tags()->where('tag', 'LIKE', $search)->get(['tags.*']); + } } diff --git a/app/Repositories/Tag/TagRepositoryInterface.php b/app/Repositories/Tag/TagRepositoryInterface.php index 8343eb4725..0bbaaad862 100644 --- a/app/Repositories/Tag/TagRepositoryInterface.php +++ b/app/Repositories/Tag/TagRepositoryInterface.php @@ -153,6 +153,24 @@ interface TagRepositoryInterface */ public function searchTag(string $query): Collection; + /** + * Find one or more tags that start with the string in the query + * + * @param string $query + * + * @return Collection + */ + public function tagStartsWith(string $query): Collection; + + /** + * Find one or more tags that start with the string in the query + * + * @param string $query + * + * @return Collection + */ + public function tagEndsWith(string $query): Collection; + /** * Search the users tags. * diff --git a/app/Support/Search/OperatorQuerySearch.php b/app/Support/Search/OperatorQuerySearch.php index 0d1299f83b..5f4af55847 100644 --- a/app/Support/Search/OperatorQuerySearch.php +++ b/app/Support/Search/OperatorQuerySearch.php @@ -57,7 +57,6 @@ use Gdbots\QueryParser\QueryParser; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use LogicException; -use PragmaRX\Random\Generators\StringGenerator; use TypeError; /** @@ -82,6 +81,9 @@ class OperatorQuerySearch implements SearchInterface private array $validOperators; private array $words; + private array $excludeTags; + private array $includeTags; + /** * OperatorQuerySearch constructor. * @@ -93,6 +95,8 @@ class OperatorQuerySearch implements SearchInterface $this->operators = new Collection(); $this->page = 1; $this->words = []; + $this->excludeTags = []; + $this->includeTags = []; $this->prohibitedWords = []; $this->invalidOperators = []; $this->limit = 25; @@ -167,6 +171,8 @@ class OperatorQuerySearch implements SearchInterface foreach ($query1->getNodes() as $searchNode) { $this->handleSearchNode($searchNode); } + $this->parseTagInstructions(); + $this->collector->setSearchWords($this->words); $this->collector->excludeSearchWords($this->prohibitedWords); @@ -868,7 +874,8 @@ class OperatorQuerySearch implements SearchInterface case 'tag_is': $result = $this->tagRepository->findByTag($value); if (null !== $result) { - $this->collector->setTags(new Collection([$result])); + $this->includeTags[] = $result->id; + $this->includeTags = array_unique($this->includeTags); } // no tags found means search must result in nothing. if (null === $result) { @@ -876,11 +883,79 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; + case 'tag_contains': + $tags = $this->tagRepository->searchTag($value); + if (0 === $tags->count()) { + app('log')->info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); + $this->collector->findNothing(); + } + if ($tags->count() > 0) { + $ids = array_values($tags->pluck('id')->toArray()); + $this->includeTags = array_unique(array_merge($this->includeTags, $ids)); + } + break; + case 'tag_starts': + $tags = $this->tagRepository->tagStartsWith($value); + if (0 === $tags->count()) { + app('log')->info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); + $this->collector->findNothing(); + } + if ($tags->count() > 0) { + $ids = array_values($tags->pluck('id')->toArray()); + $this->includeTags = array_unique(array_merge($this->includeTags, $ids)); + } + break; + case '-tag_starts': + $tags = $this->tagRepository->tagStartsWith($value); + if (0 === $tags->count()) { + app('log')->info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); + $this->collector->findNothing(); + } + if ($tags->count() > 0) { + $ids = array_values($tags->pluck('id')->toArray()); + $this->excludeTags = array_unique(array_merge($this->includeTags, $ids)); + } + break; + case 'tag_ends': + $tags = $this->tagRepository->tagEndsWith($value); + if (0 === $tags->count()) { + app('log')->info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); + $this->collector->findNothing(); + } + if ($tags->count() > 0) { + $ids = array_values($tags->pluck('id')->toArray()); + $this->includeTags = array_unique(array_merge($this->includeTags, $ids)); + } + break; + case '-tag_ends': + $tags = $this->tagRepository->tagEndsWith($value); + if (0 === $tags->count()) { + app('log')->info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); + $this->collector->findNothing(); + } + if ($tags->count() > 0) { + $ids = array_values($tags->pluck('id')->toArray()); + $this->excludeTags = array_unique(array_merge($this->includeTags, $ids)); + } + break; + case '-tag_contains': + $tags = $this->tagRepository->searchTag($value)->keyBy('id'); + + if (0 === $tags->count()) { + app('log')->info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); + $this->collector->findNothing(); + } + if ($tags->count() > 0) { + $ids = array_values($tags->pluck('id')->toArray()); + $this->excludeTags = array_unique(array_merge($this->excludeTags, $ids)); + } + break; case '-tag_is': case 'tag_is_not': $result = $this->tagRepository->searchTag($value); if ($result->count() > 0) { - $this->collector->setWithoutSpecificTags($result); + $this->excludeTags[] = $result->id; + $this->excludeTags = array_unique($this->excludeTags); } break; // @@ -1443,7 +1518,7 @@ class OperatorQuerySearch implements SearchInterface * * @param string $value * @param SearchDirection $searchDirection - * @param StringPosition $stringPosition + * @param StringPosition $stringPosition * @param bool $prohibited * @SuppressWarnings(PHPMD.BooleanArgumentFlag) */ @@ -2160,4 +2235,42 @@ class OperatorQuerySearch implements SearchInterface $this->limit = $limit; $this->collector->setLimit($this->limit); } + + /** + * @return void + * @throws \Psr\Container\ContainerExceptionInterface + * @throws \Psr\Container\NotFoundExceptionInterface + */ + private function parseTagInstructions(): void + { + app('log')->debug('Now in parseTagInstructions()'); + // if exclude tags, remove excluded tags. + if (count($this->excludeTags) > 0) { + app('log')->debug(sprintf('%d exclude tag(s)', count($this->excludeTags))); + $collection = new Collection; + foreach ($this->excludeTags as $tagId) { + $tag = $this->tagRepository->find($tagId); + if (null !== $tag) { + app('log')->debug(sprintf('Exclude tag "%s"', $tag->tag)); + $collection->push($tag); + } + } + app('log')->debug(sprintf('Selecting all tags except %d excluded tag(s).', $collection->count())); + $this->collector->setWithoutSpecificTags($collection); + } + // if include tags, include them: + if (count($this->includeTags) > 0) { + app('log')->debug(sprintf('%d include tag(s)', count($this->includeTags))); + $collection = new Collection; + foreach ($this->includeTags as $tagId) { + $tag = $this->tagRepository->find($tagId); + if (null !== $tag) { + app('log')->debug(sprintf('Include tag "%s"', $tag->tag)); + $collection->push($tag); + } + } + $this->collector->setTags($collection); + } + + } } diff --git a/config/search.php b/config/search.php index 6e065e4f07..ec80652cff 100644 --- a/config/search.php +++ b/config/search.php @@ -34,6 +34,9 @@ return [ 'tag_is' => ['alias' => false, 'needs_context' => true,], 'tag_is_not' => ['alias' => false, 'needs_context' => true,], 'tag' => ['alias' => true, 'alias_for' => 'tag_is', 'needs_context' => true,], + 'tag_contains' => ['alias' => false, 'needs_context' => true,], + 'tag_ends' => ['alias' => false, 'needs_context' => true,], + 'tag_starts' => ['alias' => false, 'needs_context' => true,], 'description_is' => ['alias' => false, 'needs_context' => true,], 'description' => ['alias' => true, 'alias_for' => 'description_is', 'needs_context' => true,], 'description_contains' => ['alias' => false, 'needs_context' => true,], diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 6ce596ff03..77dd08b645 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -449,6 +449,10 @@ return [ 'search_modifier_transaction_type' => 'Transaction type is ":value"', 'search_modifier_not_transaction_type' => 'Transaction type is not ":value"', 'search_modifier_tag_is' => 'Tag is ":value"', + 'search_modifier_tag_contains' => 'Tag contains ":value"', + 'search_modifier_not_tag_contains' => 'Tag does not contain ":value"', + 'search_modifier_tag_ends' => 'Tag ends with ":value"', + 'search_modifier_tag_starts' => 'Tag starts with ":value"', 'search_modifier_not_tag_is' => 'No tag is ":value"', 'search_modifier_date_on_year' => 'Transaction is in year ":value"', 'search_modifier_not_date_on_year' => 'Transaction is not in year ":value"',