mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-09-19 19:01:58 +00:00
Auto commit for release 'develop' on 2025-01-05
This commit is contained in:
@@ -4,6 +4,7 @@ Over time, many people have contributed to Firefly III. Their efforts are not al
|
|||||||
Please find below all the people who contributed to the Firefly III code. Their names are mentioned in the year of their first contribution.
|
Please find below all the people who contributed to the Firefly III code. Their names are mentioned in the year of their first contribution.
|
||||||
|
|
||||||
## 2024
|
## 2024
|
||||||
|
- Sobuno
|
||||||
- TasneemTantawy
|
- TasneemTantawy
|
||||||
- Antônio Franco
|
- Antônio Franco
|
||||||
- yparitcher
|
- yparitcher
|
||||||
|
@@ -35,13 +35,13 @@ class BillObserver
|
|||||||
{
|
{
|
||||||
public function created(Bill $bill): void
|
public function created(Bill $bill): void
|
||||||
{
|
{
|
||||||
// Log::debug('Observe "created" of a bill.');
|
// Log::debug('Observe "created" of a bill.');
|
||||||
$this->updateNativeAmount($bill);
|
$this->updateNativeAmount($bill);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleting(Bill $bill): void
|
public function deleting(Bill $bill): void
|
||||||
{
|
{
|
||||||
// app('log')->debug('Observe "deleting" of a bill.');
|
// app('log')->debug('Observe "deleting" of a bill.');
|
||||||
foreach ($bill->attachments()->get() as $attachment) {
|
foreach ($bill->attachments()->get() as $attachment) {
|
||||||
$attachment->delete();
|
$attachment->delete();
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ class BillObserver
|
|||||||
|
|
||||||
public function updated(Bill $bill): void
|
public function updated(Bill $bill): void
|
||||||
{
|
{
|
||||||
// Log::debug('Observe "updated" of a bill.');
|
// Log::debug('Observe "updated" of a bill.');
|
||||||
$this->updateNativeAmount($bill);
|
$this->updateNativeAmount($bill);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -376,6 +376,7 @@ class GroupCollector implements GroupCollectorInterface
|
|||||||
{
|
{
|
||||||
if (0 === count($array)) {
|
if (0 === count($array)) {
|
||||||
Log::debug('No excluded search words provided, skipping.');
|
Log::debug('No excluded search words provided, skipping.');
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
Log::debug(sprintf('%d excluded search words provided.', count($array)));
|
Log::debug(sprintf('%d excluded search words provided.', count($array)));
|
||||||
@@ -952,6 +953,7 @@ class GroupCollector implements GroupCollectorInterface
|
|||||||
{
|
{
|
||||||
if (0 === count($array)) {
|
if (0 === count($array)) {
|
||||||
Log::debug('No words in array');
|
Log::debug('No words in array');
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
Log::debug(sprintf('%d word(s) in array', count($array)));
|
Log::debug(sprintf('%d word(s) in array', count($array)));
|
||||||
|
@@ -89,14 +89,14 @@ class CreateController extends Controller
|
|||||||
// build triggers from query, if present.
|
// build triggers from query, if present.
|
||||||
$query = (string) $request->get('from_query');
|
$query = (string) $request->get('from_query');
|
||||||
if ('' !== $query) {
|
if ('' !== $query) {
|
||||||
$search = app(SearchInterface::class);
|
$search = app(SearchInterface::class);
|
||||||
$search->parseQuery($query);
|
$search->parseQuery($query);
|
||||||
$words = $search->getWords();
|
$words = $search->getWords();
|
||||||
$excludedWords = $search->getExcludedWords();
|
$excludedWords = $search->getExcludedWords();
|
||||||
$operators = $search->getOperators()->toArray();
|
$operators = $search->getOperators()->toArray();
|
||||||
if (count($words) > 0) {
|
if (count($words) > 0) {
|
||||||
session()->flash('warning', trans('firefly.rule_from_search_words', ['string' => implode('', $words)]));
|
session()->flash('warning', trans('firefly.rule_from_search_words', ['string' => implode('', $words)]));
|
||||||
foreach($words as $word) {
|
foreach ($words as $word) {
|
||||||
$operators[] = [
|
$operators[] = [
|
||||||
'type' => 'description_contains',
|
'type' => 'description_contains',
|
||||||
'value' => $word,
|
'value' => $word,
|
||||||
@@ -105,14 +105,14 @@ class CreateController extends Controller
|
|||||||
}
|
}
|
||||||
if (count($excludedWords) > 0) {
|
if (count($excludedWords) > 0) {
|
||||||
session()->flash('warning', trans('firefly.rule_from_search_words', ['string' => implode('', $excludedWords)]));
|
session()->flash('warning', trans('firefly.rule_from_search_words', ['string' => implode('', $excludedWords)]));
|
||||||
foreach($excludedWords as $excludedWord) {
|
foreach ($excludedWords as $excludedWord) {
|
||||||
$operators[] = [
|
$operators[] = [
|
||||||
'type' => '-description_contains',
|
'type' => '-description_contains',
|
||||||
'value' => $excludedWord,
|
'value' => $excludedWord,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$oldTriggers = $this->parseFromOperators($operators);
|
$oldTriggers = $this->parseFromOperators($operators);
|
||||||
}
|
}
|
||||||
// var_dump($oldTriggers);exit;
|
// var_dump($oldTriggers);exit;
|
||||||
|
|
||||||
|
@@ -85,14 +85,14 @@ class EditController extends Controller
|
|||||||
// build triggers from query, if present.
|
// build triggers from query, if present.
|
||||||
$query = (string) $request->get('from_query');
|
$query = (string) $request->get('from_query');
|
||||||
if ('' !== $query) {
|
if ('' !== $query) {
|
||||||
$search = app(SearchInterface::class);
|
$search = app(SearchInterface::class);
|
||||||
$search->parseQuery($query);
|
$search->parseQuery($query);
|
||||||
$words = $search->getWords();
|
$words = $search->getWords();
|
||||||
$excludedWords = $search->getExcludedWords();
|
$excludedWords = $search->getExcludedWords();
|
||||||
$operators = $search->getOperators()->toArray();
|
$operators = $search->getOperators()->toArray();
|
||||||
if (count($words) > 0) {
|
if (count($words) > 0) {
|
||||||
session()->flash('warning', trans('firefly.rule_from_search_words', ['string' => implode('', $words)]));
|
session()->flash('warning', trans('firefly.rule_from_search_words', ['string' => implode('', $words)]));
|
||||||
foreach($words as $word) {
|
foreach ($words as $word) {
|
||||||
$operators[] = [
|
$operators[] = [
|
||||||
'type' => 'description_contains',
|
'type' => 'description_contains',
|
||||||
'value' => $word,
|
'value' => $word,
|
||||||
@@ -101,14 +101,14 @@ class EditController extends Controller
|
|||||||
}
|
}
|
||||||
if (count($excludedWords) > 0) {
|
if (count($excludedWords) > 0) {
|
||||||
session()->flash('warning', trans('firefly.rule_from_search_words', ['string' => implode('', $excludedWords)]));
|
session()->flash('warning', trans('firefly.rule_from_search_words', ['string' => implode('', $excludedWords)]));
|
||||||
foreach($excludedWords as $excludedWord) {
|
foreach ($excludedWords as $excludedWord) {
|
||||||
$operators[] = [
|
$operators[] = [
|
||||||
'type' => '-description_contains',
|
'type' => '-description_contains',
|
||||||
'value' => $excludedWord,
|
'value' => $excludedWord,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$oldTriggers = $this->parseFromOperators($operators);
|
$oldTriggers = $this->parseFromOperators($operators);
|
||||||
}
|
}
|
||||||
// has old input?
|
// has old input?
|
||||||
if (null !== $request->old() && is_array($request->old()) && count($request->old()) > 0) {
|
if (null !== $request->old() && is_array($request->old()) && count($request->old()) > 0) {
|
||||||
|
@@ -51,8 +51,8 @@ class SearchServiceProvider extends ServiceProvider
|
|||||||
static function (): GdbotsQueryParser|QueryParser {
|
static function (): GdbotsQueryParser|QueryParser {
|
||||||
$implementation = config('search.query_parser');
|
$implementation = config('search.query_parser');
|
||||||
|
|
||||||
return match($implementation) {
|
return match ($implementation) {
|
||||||
'new' => app(QueryParser::class),
|
'new' => app(QueryParser::class),
|
||||||
default => app(GdbotsQueryParser::class),
|
default => app(GdbotsQueryParser::class),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -505,7 +505,7 @@ class Navigation
|
|||||||
{
|
{
|
||||||
$format = 'Y-m-d';
|
$format = 'Y-m-d';
|
||||||
$diff = $start->diffInMonths($end, true);
|
$diff = $start->diffInMonths($end, true);
|
||||||
//Log::debug(sprintf('preferredCarbonFormat(%s, %s) = %f', $start->format('Y-m-d'), $end->format('Y-m-d'), $diff));
|
// Log::debug(sprintf('preferredCarbonFormat(%s, %s) = %f', $start->format('Y-m-d'), $end->format('Y-m-d'), $diff));
|
||||||
if ($diff >= 1.001) {
|
if ($diff >= 1.001) {
|
||||||
// Log::debug(sprintf('Return Y-m because %s', $diff));
|
// Log::debug(sprintf('Return Y-m because %s', $diff));
|
||||||
$format = 'Y-m';
|
$format = 'Y-m';
|
||||||
|
@@ -44,7 +44,6 @@ use FireflyIII\Support\Search\QueryParser\Node;
|
|||||||
use FireflyIII\Support\Search\QueryParser\FieldNode;
|
use FireflyIII\Support\Search\QueryParser\FieldNode;
|
||||||
use FireflyIII\Support\Search\QueryParser\StringNode;
|
use FireflyIII\Support\Search\QueryParser\StringNode;
|
||||||
use FireflyIII\Support\Search\QueryParser\NodeGroup;
|
use FireflyIII\Support\Search\QueryParser\NodeGroup;
|
||||||
|
|
||||||
use FireflyIII\Support\ParseDateString;
|
use FireflyIII\Support\ParseDateString;
|
||||||
use FireflyIII\User;
|
use FireflyIII\User;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
@@ -147,6 +146,7 @@ class OperatorQuerySearch implements SearchInterface
|
|||||||
public function parseQuery(string $query): void
|
public function parseQuery(string $query): void
|
||||||
{
|
{
|
||||||
app('log')->debug(sprintf('Now in parseQuery("%s")', $query));
|
app('log')->debug(sprintf('Now in parseQuery("%s")', $query));
|
||||||
|
|
||||||
/** @var QueryParserInterface $parser */
|
/** @var QueryParserInterface $parser */
|
||||||
$parser = app(QueryParserInterface::class);
|
$parser = app(QueryParserInterface::class);
|
||||||
app('log')->debug(sprintf('Using %s as implementation for QueryParserInterface', get_class($parser)));
|
app('log')->debug(sprintf('Using %s as implementation for QueryParserInterface', get_class($parser)));
|
||||||
@@ -182,18 +182,22 @@ class OperatorQuerySearch implements SearchInterface
|
|||||||
switch (true) {
|
switch (true) {
|
||||||
case $node instanceof StringNode:
|
case $node instanceof StringNode:
|
||||||
$this->handleStringNode($node, $flipProhibitedFlag);
|
$this->handleStringNode($node, $flipProhibitedFlag);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case $node instanceof FieldNode:
|
case $node instanceof FieldNode:
|
||||||
$this->handleFieldNode($node, $flipProhibitedFlag);
|
$this->handleFieldNode($node, $flipProhibitedFlag);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case $node instanceof NodeGroup:
|
case $node instanceof NodeGroup:
|
||||||
$this->handleNodeGroup($node, $flipProhibitedFlag);
|
$this->handleNodeGroup($node, $flipProhibitedFlag);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
app('log')->error(sprintf('Cannot handle node %s', get_class($node)));
|
app('log')->error(sprintf('Cannot handle node %s', get_class($node)));
|
||||||
|
|
||||||
throw new FireflyException(sprintf('Firefly III search can\'t handle "%s"-nodes', get_class($node)));
|
throw new FireflyException(sprintf('Firefly III search can\'t handle "%s"-nodes', get_class($node)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,19 +211,17 @@ class OperatorQuerySearch implements SearchInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private function handleStringNode(StringNode $node, bool $flipProhibitedFlag): void
|
private function handleStringNode(StringNode $node, bool $flipProhibitedFlag): void
|
||||||
{
|
{
|
||||||
$string = $node->getValue();
|
$string = $node->getValue();
|
||||||
|
|
||||||
$prohibited = $node->isProhibited($flipProhibitedFlag);
|
$prohibited = $node->isProhibited($flipProhibitedFlag);
|
||||||
|
|
||||||
if($prohibited) {
|
if ($prohibited) {
|
||||||
app('log')->debug(sprintf('Exclude string "%s" from search string', $string));
|
app('log')->debug(sprintf('Exclude string "%s" from search string', $string));
|
||||||
$this->prohibitedWords[] = $string;
|
$this->prohibitedWords[] = $string;
|
||||||
}
|
}
|
||||||
if(!$prohibited) {
|
if (!$prohibited) {
|
||||||
app('log')->debug(sprintf('Add string "%s" to search string', $string));
|
app('log')->debug(sprintf('Add string "%s" to search string', $string));
|
||||||
$this->words[] = $string;
|
$this->words[] = $string;
|
||||||
}
|
}
|
||||||
@@ -230,39 +232,39 @@ class OperatorQuerySearch implements SearchInterface
|
|||||||
*/
|
*/
|
||||||
private function handleFieldNode(FieldNode $node, bool $flipProhibitedFlag): void
|
private function handleFieldNode(FieldNode $node, bool $flipProhibitedFlag): void
|
||||||
{
|
{
|
||||||
$operator = strtolower($node->getOperator());
|
$operator = strtolower($node->getOperator());
|
||||||
$value = $node->getValue();
|
$value = $node->getValue();
|
||||||
$prohibited = $node->isProhibited($flipProhibitedFlag);
|
$prohibited = $node->isProhibited($flipProhibitedFlag);
|
||||||
|
|
||||||
$context = config(sprintf('search.operators.%s.needs_context', $operator));
|
$context = config(sprintf('search.operators.%s.needs_context', $operator));
|
||||||
|
|
||||||
// is an operator that needs no context, and value is false, then prohibited = true.
|
// is an operator that needs no context, and value is false, then prohibited = true.
|
||||||
if ('false' === $value && in_array($operator, $this->validOperators, true) && false === $context && !$prohibited) {
|
if ('false' === $value && in_array($operator, $this->validOperators, true) && false === $context && !$prohibited) {
|
||||||
$prohibited = true;
|
$prohibited = true;
|
||||||
$value = 'true';
|
$value = 'true';
|
||||||
}
|
}
|
||||||
// if the operator is prohibited, but the value is false, do an uno reverse
|
// if the operator is prohibited, but the value is false, do an uno reverse
|
||||||
if ('false' === $value && $prohibited && in_array($operator, $this->validOperators, true) && false === $context) {
|
if ('false' === $value && $prohibited && in_array($operator, $this->validOperators, true) && false === $context) {
|
||||||
$prohibited = false;
|
$prohibited = false;
|
||||||
$value = 'true';
|
$value = 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
// must be valid operator:
|
// must be valid operator:
|
||||||
$inArray = in_array($operator, $this->validOperators, true);
|
$inArray = in_array($operator, $this->validOperators, true);
|
||||||
if ($inArray) {
|
if ($inArray) {
|
||||||
if ($this->updateCollector($operator, $value, $prohibited)) {
|
if ($this->updateCollector($operator, $value, $prohibited)) {
|
||||||
$this->operators->push([
|
$this->operators->push([
|
||||||
'type' => self::getRootOperator($operator),
|
'type' => self::getRootOperator($operator),
|
||||||
'value' => $value,
|
'value' => $value,
|
||||||
'prohibited' => $prohibited,
|
'prohibited' => $prohibited,
|
||||||
]);
|
]);
|
||||||
app('log')->debug(sprintf('Added operator type "%s"', $operator));
|
app('log')->debug(sprintf('Added operator type "%s"', $operator));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!$inArray) {
|
if (!$inArray) {
|
||||||
app('log')->debug(sprintf('Added INVALID operator type "%s"', $operator));
|
app('log')->debug(sprintf('Added INVALID operator type "%s"', $operator));
|
||||||
$this->invalidOperators[] = [
|
$this->invalidOperators[] = [
|
||||||
'type' => $operator,
|
'type' => $operator,
|
||||||
'value' => $value,
|
'value' => $value,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -35,8 +35,8 @@ class FieldNode extends Node
|
|||||||
|
|
||||||
public function __construct(string $operator, string $value, bool $prohibited = false)
|
public function __construct(string $operator, string $value, bool $prohibited = false)
|
||||||
{
|
{
|
||||||
$this->operator = $operator;
|
$this->operator = $operator;
|
||||||
$this->value = $value;
|
$this->value = $value;
|
||||||
$this->prohibited = $prohibited;
|
$this->prohibited = $prohibited;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -41,17 +41,17 @@ class GdbotsQueryParser implements QueryParserInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return NodeGroup
|
|
||||||
* @throws FireflyException
|
* @throws FireflyException
|
||||||
*/
|
*/
|
||||||
public function parse(string $query): NodeGroup
|
public function parse(string $query): NodeGroup
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$result = $this->parser->parse($query);
|
$result = $this->parser->parse($query);
|
||||||
$nodes = array_map(
|
$nodes = array_map(
|
||||||
fn(GdbotsNode\Node $node) => $this->convertNode($node),
|
fn (GdbotsNode\Node $node) => $this->convertNode($node),
|
||||||
$result->getNodes()
|
$result->getNodes()
|
||||||
);
|
);
|
||||||
|
|
||||||
return new NodeGroup($nodes);
|
return new NodeGroup($nodes);
|
||||||
} catch (\LogicException|\TypeError $e) {
|
} catch (\LogicException|\TypeError $e) {
|
||||||
fwrite(STDERR, "Setting up GdbotsQueryParserTest\n");
|
fwrite(STDERR, "Setting up GdbotsQueryParserTest\n");
|
||||||
@@ -78,9 +78,10 @@ class GdbotsQueryParser implements QueryParserInterface
|
|||||||
|
|
||||||
case $node instanceof GdbotsNode\Subquery:
|
case $node instanceof GdbotsNode\Subquery:
|
||||||
Log::debug('Subquery');
|
Log::debug('Subquery');
|
||||||
|
|
||||||
return new NodeGroup(
|
return new NodeGroup(
|
||||||
array_map(
|
array_map(
|
||||||
fn(GdbotsNode\Node $subNode) => $this->convertNode($subNode),
|
fn (GdbotsNode\Node $subNode) => $this->convertNode($subNode),
|
||||||
$node->getNodes()
|
$node->getNodes()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@@ -47,10 +47,11 @@ abstract class Node
|
|||||||
public function isProhibited(bool $flipFlag): bool
|
public function isProhibited(bool $flipFlag): bool
|
||||||
{
|
{
|
||||||
if ($flipFlag) {
|
if ($flipFlag) {
|
||||||
//Log::debug(sprintf('This %s is (flipped) now prohibited: %s',get_class($this), var_export(!$this->prohibited, true)));
|
// Log::debug(sprintf('This %s is (flipped) now prohibited: %s',get_class($this), var_export(!$this->prohibited, true)));
|
||||||
return !$this->prohibited;
|
return !$this->prohibited;
|
||||||
}
|
}
|
||||||
//Log::debug(sprintf('This %s is (not flipped) now prohibited: %s',get_class($this), var_export($this->prohibited, true)));
|
|
||||||
|
// Log::debug(sprintf('This %s is (not flipped) now prohibited: %s',get_class($this), var_export($this->prohibited, true)));
|
||||||
return $this->prohibited;
|
return $this->prohibited;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -37,11 +37,10 @@ class NodeGroup extends Node
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Node[] $nodes
|
* @param Node[] $nodes
|
||||||
* @param bool $prohibited
|
|
||||||
*/
|
*/
|
||||||
public function __construct(array $nodes, bool $prohibited = false)
|
public function __construct(array $nodes, bool $prohibited = false)
|
||||||
{
|
{
|
||||||
$this->nodes = $nodes;
|
$this->nodes = $nodes;
|
||||||
$this->prohibited = $prohibited;
|
$this->prohibited = $prohibited;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -52,23 +52,22 @@ class QueryParser implements QueryParserInterface
|
|||||||
private string $query;
|
private string $query;
|
||||||
private int $position = 0;
|
private int $position = 0;
|
||||||
|
|
||||||
/** @return NodeGroup */
|
|
||||||
public function parse(string $query): NodeGroup
|
public function parse(string $query): NodeGroup
|
||||||
{
|
{
|
||||||
Log::debug(sprintf('Parsing query in QueryParser: "%s"', $query));
|
Log::debug(sprintf('Parsing query in QueryParser: "%s"', $query));
|
||||||
$this->query = $query;
|
$this->query = $query;
|
||||||
$this->position = 0;
|
$this->position = 0;
|
||||||
|
|
||||||
return $this->buildNodeGroup(false);
|
return $this->buildNodeGroup(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return NodeGroup */
|
|
||||||
private function buildNodeGroup(bool $isSubquery, bool $prohibited = false): NodeGroup
|
private function buildNodeGroup(bool $isSubquery, bool $prohibited = false): NodeGroup
|
||||||
{
|
{
|
||||||
$nodes = [];
|
$nodes = [];
|
||||||
$nodeResult = $this->buildNextNode($isSubquery);
|
$nodeResult = $this->buildNextNode($isSubquery);
|
||||||
|
|
||||||
while ($nodeResult->node !== null) {
|
while (null !== $nodeResult->node) {
|
||||||
$nodes[] = $nodeResult->node;
|
$nodes[] = $nodeResult->node;
|
||||||
if ($nodeResult->isSubqueryEnd) {
|
if ($nodeResult->isSubqueryEnd) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -90,13 +89,15 @@ class QueryParser implements QueryParserInterface
|
|||||||
|
|
||||||
// If we're in a quoted string, we treat all characters except another quote as ordinary characters
|
// If we're in a quoted string, we treat all characters except another quote as ordinary characters
|
||||||
if ($inQuotes) {
|
if ($inQuotes) {
|
||||||
if ($char !== '"') {
|
if ('"' !== $char) {
|
||||||
$tokenUnderConstruction .= $char;
|
$tokenUnderConstruction .= $char;
|
||||||
$this->position++;
|
++$this->position;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// char is "
|
// char is "
|
||||||
$this->position++;
|
++$this->position;
|
||||||
|
|
||||||
return new NodeResult(
|
return new NodeResult(
|
||||||
$this->createNode($tokenUnderConstruction, $fieldName, $prohibited),
|
$this->createNode($tokenUnderConstruction, $fieldName, $prohibited),
|
||||||
false
|
false
|
||||||
@@ -105,47 +106,53 @@ class QueryParser implements QueryParserInterface
|
|||||||
|
|
||||||
switch ($char) {
|
switch ($char) {
|
||||||
case '-':
|
case '-':
|
||||||
if ($tokenUnderConstruction === '') {
|
if ('' === $tokenUnderConstruction) {
|
||||||
// A minus sign at the beginning of a token indicates prohibition
|
// A minus sign at the beginning of a token indicates prohibition
|
||||||
Log::debug('Indicate prohibition');
|
Log::debug('Indicate prohibition');
|
||||||
$prohibited = true;
|
$prohibited = true;
|
||||||
}
|
}
|
||||||
if ($tokenUnderConstruction !== '') {
|
if ('' !== $tokenUnderConstruction) {
|
||||||
// In any other location, it's just a normal character
|
// In any other location, it's just a normal character
|
||||||
$tokenUnderConstruction .= $char;
|
$tokenUnderConstruction .= $char;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '"':
|
case '"':
|
||||||
if ($tokenUnderConstruction === '') {
|
if ('' === $tokenUnderConstruction) {
|
||||||
// A quote sign at the beginning of a token indicates the start of a quoted string
|
// A quote sign at the beginning of a token indicates the start of a quoted string
|
||||||
$inQuotes = true;
|
$inQuotes = true;
|
||||||
}
|
}
|
||||||
if ($tokenUnderConstruction !== '') {
|
if ('' !== $tokenUnderConstruction) {
|
||||||
// In any other location, it's just a normal character
|
// In any other location, it's just a normal character
|
||||||
$tokenUnderConstruction .= $char;
|
$tokenUnderConstruction .= $char;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '(':
|
case '(':
|
||||||
if ($tokenUnderConstruction === '') {
|
if ('' === $tokenUnderConstruction) {
|
||||||
// A left parentheses at the beginning of a token indicates the start of a subquery
|
// A left parentheses at the beginning of a token indicates the start of a subquery
|
||||||
$this->position++;
|
++$this->position;
|
||||||
return new NodeResult($this->buildNodeGroup(true, $prohibited),
|
|
||||||
false
|
return new NodeResult(
|
||||||
|
$this->buildNodeGroup(true, $prohibited),
|
||||||
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// In any other location, it's just a normal character
|
// In any other location, it's just a normal character
|
||||||
$tokenUnderConstruction .= $char;
|
$tokenUnderConstruction .= $char;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ')':
|
case ')':
|
||||||
// A right parentheses while in a subquery means the subquery ended,
|
// A right parentheses while in a subquery means the subquery ended,
|
||||||
// thus also signaling the end of any node currently being built
|
// thus also signaling the end of any node currently being built
|
||||||
if ($isSubquery) {
|
if ($isSubquery) {
|
||||||
$this->position++;
|
++$this->position;
|
||||||
|
|
||||||
return new NodeResult(
|
return new NodeResult(
|
||||||
$tokenUnderConstruction !== ''
|
'' !== $tokenUnderConstruction
|
||||||
? $this->createNode($tokenUnderConstruction, $fieldName, $prohibited)
|
? $this->createNode($tokenUnderConstruction, $fieldName, $prohibited)
|
||||||
: null,
|
: null,
|
||||||
true
|
true
|
||||||
@@ -153,40 +160,44 @@ class QueryParser implements QueryParserInterface
|
|||||||
}
|
}
|
||||||
// In any other location, it's just a normal character
|
// In any other location, it's just a normal character
|
||||||
$tokenUnderConstruction .= $char;
|
$tokenUnderConstruction .= $char;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
case ':':
|
case ':':
|
||||||
if ($tokenUnderConstruction !== '') {
|
if ('' !== $tokenUnderConstruction) {
|
||||||
// If we meet a colon with a left-hand side string, we know we're in a field and are about to set up the value
|
// If we meet a colon with a left-hand side string, we know we're in a field and are about to set up the value
|
||||||
$fieldName = $tokenUnderConstruction;
|
$fieldName = $tokenUnderConstruction;
|
||||||
$tokenUnderConstruction = '';
|
$tokenUnderConstruction = '';
|
||||||
}
|
}
|
||||||
if ($tokenUnderConstruction === '') {
|
if ('' === $tokenUnderConstruction) {
|
||||||
// In any other location, it's just a normal character
|
// In any other location, it's just a normal character
|
||||||
$tokenUnderConstruction .= $char;
|
$tokenUnderConstruction .= $char;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ' ':
|
case ' ':
|
||||||
// A space indicates the end of a token construction if non-empty, otherwise it's just ignored
|
// A space indicates the end of a token construction if non-empty, otherwise it's just ignored
|
||||||
if ($tokenUnderConstruction !== '') {
|
if ('' !== $tokenUnderConstruction) {
|
||||||
$this->position++;
|
++$this->position;
|
||||||
|
|
||||||
return new NodeResult(
|
return new NodeResult(
|
||||||
$this->createNode($tokenUnderConstruction, $fieldName, $prohibited),
|
$this->createNode($tokenUnderConstruction, $fieldName, $prohibited),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
$tokenUnderConstruction .= $char;
|
$tokenUnderConstruction .= $char;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->position++;
|
++$this->position;
|
||||||
}
|
}
|
||||||
|
|
||||||
$finalNode = $tokenUnderConstruction !== '' || $fieldName !== ''
|
$finalNode = '' !== $tokenUnderConstruction || '' !== $fieldName
|
||||||
? $this->createNode($tokenUnderConstruction, $fieldName, $prohibited)
|
? $this->createNode($tokenUnderConstruction, $fieldName, $prohibited)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -195,11 +206,13 @@ class QueryParser implements QueryParserInterface
|
|||||||
|
|
||||||
private function createNode(string $token, string $fieldName, bool $prohibited): Node
|
private function createNode(string $token, string $fieldName, bool $prohibited): Node
|
||||||
{
|
{
|
||||||
if (strlen($fieldName) > 0) {
|
if ('' !== $fieldName) {
|
||||||
Log::debug(sprintf('Create FieldNode %s:%s (%s)', $fieldName, $token, var_export($prohibited, true)));
|
Log::debug(sprintf('Create FieldNode %s:%s (%s)', $fieldName, $token, var_export($prohibited, true)));
|
||||||
|
|
||||||
return new FieldNode(trim($fieldName), trim($token), $prohibited);
|
return new FieldNode(trim($fieldName), trim($token), $prohibited);
|
||||||
}
|
}
|
||||||
Log::debug(sprintf('Create StringNode "%s" (%s)', $token, var_export($prohibited, true)));
|
Log::debug(sprintf('Create StringNode "%s" (%s)', $token, var_export($prohibited, true)));
|
||||||
|
|
||||||
return new StringNode(trim($token), $prohibited);
|
return new StringNode(trim($token), $prohibited);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -28,7 +28,6 @@ namespace FireflyIII\Support\Search\QueryParser;
|
|||||||
interface QueryParserInterface
|
interface QueryParserInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @return NodeGroup
|
|
||||||
* @throws \LogicException
|
* @throws \LogicException
|
||||||
* @throws \TypeError
|
* @throws \TypeError
|
||||||
*/
|
*/
|
||||||
|
@@ -34,7 +34,7 @@ class StringNode extends Node
|
|||||||
|
|
||||||
public function __construct(string $value, bool $prohibited = false)
|
public function __construct(string $value, bool $prohibited = false)
|
||||||
{
|
{
|
||||||
$this->value = $value;
|
$this->value = $value;
|
||||||
$this->prohibited = $prohibited;
|
$this->prohibited = $prohibited;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -38,9 +38,11 @@ interface SearchInterface
|
|||||||
public function getModifiers(): Collection;
|
public function getModifiers(): Collection;
|
||||||
|
|
||||||
public function getOperators(): Collection;
|
public function getOperators(): Collection;
|
||||||
|
|
||||||
public function getWords(): array;
|
public function getWords(): array;
|
||||||
|
|
||||||
public function getWordsAsString(): string;
|
public function getWordsAsString(): string;
|
||||||
|
|
||||||
public function getExcludedWords(): array;
|
public function getExcludedWords(): array;
|
||||||
|
|
||||||
public function hasModifiers(): bool;
|
public function hasModifiers(): bool;
|
||||||
|
@@ -81,7 +81,7 @@ return [
|
|||||||
'running_balance_column' => env('USE_RUNNING_BALANCE', false),
|
'running_balance_column' => env('USE_RUNNING_BALANCE', false),
|
||||||
// see cer.php for exchange rates feature flag.
|
// see cer.php for exchange rates feature flag.
|
||||||
],
|
],
|
||||||
'version' => 'develop/2025-01-03',
|
'version' => 'develop/2025-01-05',
|
||||||
'api_version' => '2.1.0', // field is no longer used.
|
'api_version' => '2.1.0', // field is no longer used.
|
||||||
'db_version' => 25,
|
'db_version' => 25,
|
||||||
|
|
||||||
|
@@ -23,7 +23,7 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'operators' => [
|
'operators' => [
|
||||||
'user_action' => ['alias' => false, 'needs_context' => true],
|
'user_action' => ['alias' => false, 'needs_context' => true],
|
||||||
'account_id' => ['alias' => false, 'needs_context' => true],
|
'account_id' => ['alias' => false, 'needs_context' => true],
|
||||||
'reconciled' => ['alias' => false, 'needs_context' => false],
|
'reconciled' => ['alias' => false, 'needs_context' => false],
|
||||||
|
12
package-lock.json
generated
12
package-lock.json
generated
@@ -3133,9 +3133,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.10.4",
|
"version": "22.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
|
||||||
"integrity": "sha512-99l6wv4HEzBQhvaU/UGoeBoCK61SCROQaCCGyQSgX2tEQ3rKkNZ2S7CEWnS/4s1LV+8ODdK21UeyR1fHP2mXug==",
|
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -10067,9 +10067,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.83.0",
|
"version": "1.83.1",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.83.0.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.83.1.tgz",
|
||||||
"integrity": "sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw==",
|
"integrity": "sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@@ -36,7 +36,7 @@
|
|||||||
"is_reconciled_fields_dropped": "Da diese Buchung abgeglichen ist, k\u00f6nnen Sie weder die Konten noch den\/die Betrag\/Betr\u00e4ge aktualisieren.",
|
"is_reconciled_fields_dropped": "Da diese Buchung abgeglichen ist, k\u00f6nnen Sie weder die Konten noch den\/die Betrag\/Betr\u00e4ge aktualisieren.",
|
||||||
"tags": "Schlagw\u00f6rter",
|
"tags": "Schlagw\u00f6rter",
|
||||||
"no_budget": "(kein Budget)",
|
"no_budget": "(kein Budget)",
|
||||||
"no_bill": "(no subscription)",
|
"no_bill": "(kein Abonnement)",
|
||||||
"category": "Kategorie",
|
"category": "Kategorie",
|
||||||
"attachments": "Anh\u00e4nge",
|
"attachments": "Anh\u00e4nge",
|
||||||
"notes": "Notizen",
|
"notes": "Notizen",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"destination_account_reconciliation": "Sie k\u00f6nnen das Zielkonto einer Kontenausgleichsbuchung nicht bearbeiten.",
|
"destination_account_reconciliation": "Sie k\u00f6nnen das Zielkonto einer Kontenausgleichsbuchung nicht bearbeiten.",
|
||||||
"source_account_reconciliation": "Sie k\u00f6nnen das Quellkonto einer Kontenausgleichsbuchung nicht bearbeiten.",
|
"source_account_reconciliation": "Sie k\u00f6nnen das Quellkonto einer Kontenausgleichsbuchung nicht bearbeiten.",
|
||||||
"budget": "Budget",
|
"budget": "Budget",
|
||||||
"bill": "Subscription",
|
"bill": "Abonnement",
|
||||||
"you_create_withdrawal": "Sie haben eine Ausgabe erstellt.",
|
"you_create_withdrawal": "Sie haben eine Ausgabe erstellt.",
|
||||||
"you_create_transfer": "Sie erstellen eine Umbuchung.",
|
"you_create_transfer": "Sie erstellen eine Umbuchung.",
|
||||||
"you_create_deposit": "Sie haben eine Einnahme erstellt.",
|
"you_create_deposit": "Sie haben eine Einnahme erstellt.",
|
||||||
@@ -130,15 +130,15 @@
|
|||||||
"response": "Antwort",
|
"response": "Antwort",
|
||||||
"visit_webhook_url": "Webhook-URL besuchen",
|
"visit_webhook_url": "Webhook-URL besuchen",
|
||||||
"reset_webhook_secret": "Webhook Secret zur\u00fccksetzen",
|
"reset_webhook_secret": "Webhook Secret zur\u00fccksetzen",
|
||||||
"header_exchange_rates": "Exchange rates",
|
"header_exchange_rates": "Wechselkurse",
|
||||||
"exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in <a href=\"https:\/\/docs.firefly-iii.org\/LOL_NOT_FINISHED_YET_TODO\">the documentation<\/a>.",
|
"exchange_rates_intro": "Firefly III unterst\u00fctzt das Herunterladen und Verwenden von Wechselkursen. Lesen Sie mehr dar\u00fcber in <a href=\u201ehttps:\/\/docs.firefly-iii.org\/LOL_NOT_FINISHED_YET_TODO\u201c>der Dokumentation<\/a>.",
|
||||||
"exchange_rates_from_to": "Between {from} and {to} (and the other way around)",
|
"exchange_rates_from_to": "Zwischen {from} und {to} (und umgekehrt)",
|
||||||
"exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.",
|
"exchange_rates_intro_rates": "Firefly III verwendet die folgenden Wechselkurse. Der Kehrwert wird automatisch berechnet, wenn er nicht angegeben wurde. Wenn f\u00fcr das Datum der Transaktion kein Wechselkurs vorhanden ist, sucht Firefly III in der Vergangenheit nach einem Kurs. Wenn keine vorhanden sind, wird der Kurs \u201e1\u201c verwendet.",
|
||||||
"header_exchange_rates_rates": "Exchange rates",
|
"header_exchange_rates_rates": "Wechselkurse",
|
||||||
"header_exchange_rates_table": "Table with exchange rates",
|
"header_exchange_rates_table": "Tabelle mit Wechselkursen",
|
||||||
"help_rate_form": "On this day, how many {to} will you get for one {from}?",
|
"help_rate_form": "An diesem Tag, wie viel {to} werden Sie f\u00fcr {from} bekommen?",
|
||||||
"add_new_rate": "Add a new exchange rate",
|
"add_new_rate": "Neuen Wechselkurs hinzuf\u00fcgen",
|
||||||
"save_new_rate": "Save new rate"
|
"save_new_rate": "Neuen Kurs speichern"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
@@ -162,14 +162,14 @@
|
|||||||
},
|
},
|
||||||
"list": {
|
"list": {
|
||||||
"active": "Aktiivinen?",
|
"active": "Aktiivinen?",
|
||||||
"trigger": "Trigger",
|
"trigger": "Ehto",
|
||||||
"response": "Response",
|
"response": "Vastaus",
|
||||||
"delivery": "Delivery",
|
"delivery": "Toimitus",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"secret": "Secret"
|
"secret": "Salainen tunniste"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"html_language": "fi",
|
"html_language": "fi",
|
||||||
"date_time_fns": "MMMM do, yyyy @ HH:mm:ss"
|
"date_time_fns": "do MMMM yyyy @\u00a0HH:mm:ss"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -21,7 +21,7 @@
|
|||||||
"apply_rules_checkbox": "Appliquer les r\u00e8gles",
|
"apply_rules_checkbox": "Appliquer les r\u00e8gles",
|
||||||
"fire_webhooks_checkbox": "Lancer les webhooks",
|
"fire_webhooks_checkbox": "Lancer les webhooks",
|
||||||
"no_budget_pointer": "Vous semblez n\u2019avoir encore aucun budget. Vous devriez en cr\u00e9er un sur la page des <a href=\"budgets\">budgets<\/a>. Les budgets peuvent vous aider \u00e0 garder une trace des d\u00e9penses.",
|
"no_budget_pointer": "Vous semblez n\u2019avoir encore aucun budget. Vous devriez en cr\u00e9er un sur la page des <a href=\"budgets\">budgets<\/a>. Les budgets peuvent vous aider \u00e0 garder une trace des d\u00e9penses.",
|
||||||
"no_bill_pointer": "You seem to have no subscription yet. You should create some on the <a href=\"subscriptions\">subscription<\/a>-page. Subscriptions can help you keep track of expenses.",
|
"no_bill_pointer": "Vous semblez n\u2019avoir encore aucun abonnement. Vous devriez en cr\u00e9er un sur la page des <a href=\"subscriptions\">abonnements<\/a>. Les abonnements peuvent vous aider \u00e0 garder une trace des d\u00e9penses.",
|
||||||
"source_account": "Compte source",
|
"source_account": "Compte source",
|
||||||
"hidden_fields_preferences": "Vous pouvez activer plus d'options d'op\u00e9rations dans vos <a href=\"preferences\">param\u00e8tres<\/a>.",
|
"hidden_fields_preferences": "Vous pouvez activer plus d'options d'op\u00e9rations dans vos <a href=\"preferences\">param\u00e8tres<\/a>.",
|
||||||
"destination_account": "Compte de destination",
|
"destination_account": "Compte de destination",
|
||||||
|
@@ -36,7 +36,7 @@
|
|||||||
"is_reconciled_fields_dropped": "\u041f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u044d\u0442\u0430 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044f \u0441\u0432\u0435\u0440\u0435\u043d\u0430, \u0432\u044b \u043d\u0435 \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0447\u0435\u0442\u0430, \u043d\u0438 \u0441\u0443\u043c\u043c\u0443(\u044b).",
|
"is_reconciled_fields_dropped": "\u041f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u044d\u0442\u0430 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044f \u0441\u0432\u0435\u0440\u0435\u043d\u0430, \u0432\u044b \u043d\u0435 \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0447\u0435\u0442\u0430, \u043d\u0438 \u0441\u0443\u043c\u043c\u0443(\u044b).",
|
||||||
"tags": "\u041c\u0435\u0442\u043a\u0438",
|
"tags": "\u041c\u0435\u0442\u043a\u0438",
|
||||||
"no_budget": "(\u0432\u043d\u0435 \u0431\u044e\u0434\u0436\u0435\u0442\u0430)",
|
"no_budget": "(\u0432\u043d\u0435 \u0431\u044e\u0434\u0436\u0435\u0442\u0430)",
|
||||||
"no_bill": "(no subscription)",
|
"no_bill": "(\u043d\u0435\u0442 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438)",
|
||||||
"category": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f",
|
"category": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f",
|
||||||
"attachments": "\u0412\u043b\u043e\u0436\u0435\u043d\u0438\u044f",
|
"attachments": "\u0412\u043b\u043e\u0436\u0435\u043d\u0438\u044f",
|
||||||
"notes": "\u0417\u0430\u043c\u0435\u0442\u043a\u0438",
|
"notes": "\u0417\u0430\u043c\u0435\u0442\u043a\u0438",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"destination_account_reconciliation": "\u0412\u044b \u043d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0447\u0451\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0432\u0435\u0440\u044f\u0435\u043c\u043e\u0439 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438.",
|
"destination_account_reconciliation": "\u0412\u044b \u043d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0447\u0451\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0432\u0435\u0440\u044f\u0435\u043c\u043e\u0439 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438.",
|
||||||
"source_account_reconciliation": "\u0412\u044b \u043d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0447\u0451\u0442-\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0434\u043b\u044f \u0441\u0432\u0435\u0440\u044f\u0435\u043c\u043e\u0439 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438.",
|
"source_account_reconciliation": "\u0412\u044b \u043d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0447\u0451\u0442-\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0434\u043b\u044f \u0441\u0432\u0435\u0440\u044f\u0435\u043c\u043e\u0439 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438.",
|
||||||
"budget": "\u0411\u044e\u0434\u0436\u0435\u0442",
|
"budget": "\u0411\u044e\u0434\u0436\u0435\u0442",
|
||||||
"bill": "Subscription",
|
"bill": "\u041f\u043e\u0434\u043f\u0438\u0441\u043a\u0430",
|
||||||
"you_create_withdrawal": "\u0412\u044b \u0441\u043e\u0437\u0434\u0430\u0451\u0442\u0435 \u0440\u0430\u0441\u0445\u043e\u0434.",
|
"you_create_withdrawal": "\u0412\u044b \u0441\u043e\u0437\u0434\u0430\u0451\u0442\u0435 \u0440\u0430\u0441\u0445\u043e\u0434.",
|
||||||
"you_create_transfer": "\u0412\u044b \u0441\u043e\u0437\u0434\u0430\u0451\u0442\u0435 \u043f\u0435\u0440\u0435\u0432\u043e\u0434.",
|
"you_create_transfer": "\u0412\u044b \u0441\u043e\u0437\u0434\u0430\u0451\u0442\u0435 \u043f\u0435\u0440\u0435\u0432\u043e\u0434.",
|
||||||
"you_create_deposit": "\u0412\u044b \u0441\u043e\u0437\u0434\u0430\u0451\u0442\u0435 \u0434\u043e\u0445\u043e\u0434.",
|
"you_create_deposit": "\u0412\u044b \u0441\u043e\u0437\u0434\u0430\u0451\u0442\u0435 \u0434\u043e\u0445\u043e\u0434.",
|
||||||
@@ -130,15 +130,15 @@
|
|||||||
"response": "\u041e\u0442\u0432\u0435\u0442",
|
"response": "\u041e\u0442\u0432\u0435\u0442",
|
||||||
"visit_webhook_url": "\u041f\u043e\u0441\u0435\u0442\u0438\u0442\u044c URL \u0432\u0435\u0431\u0445\u0443\u043a\u0430",
|
"visit_webhook_url": "\u041f\u043e\u0441\u0435\u0442\u0438\u0442\u044c URL \u0432\u0435\u0431\u0445\u0443\u043a\u0430",
|
||||||
"reset_webhook_secret": "",
|
"reset_webhook_secret": "",
|
||||||
"header_exchange_rates": "Exchange rates",
|
"header_exchange_rates": "\u041a\u0443\u0440\u0441\u044b \u0432\u0430\u043b\u044e\u0442",
|
||||||
"exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in <a href=\"https:\/\/docs.firefly-iii.org\/LOL_NOT_FINISHED_YET_TODO\">the documentation<\/a>.",
|
"exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in <a href=\"https:\/\/docs.firefly-iii.org\/LOL_NOT_FINISHED_YET_TODO\">the documentation<\/a>.",
|
||||||
"exchange_rates_from_to": "Between {from} and {to} (and the other way around)",
|
"exchange_rates_from_to": "\u041c\u0435\u0436\u0434\u0443 {from} \u0438 {to} (\u0438 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442)",
|
||||||
"exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.",
|
"exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.",
|
||||||
"header_exchange_rates_rates": "Exchange rates",
|
"header_exchange_rates_rates": "\u041a\u0443\u0440\u0441\u044b \u0432\u0430\u043b\u044e\u0442",
|
||||||
"header_exchange_rates_table": "Table with exchange rates",
|
"header_exchange_rates_table": "\u0422\u0430\u0431\u043b\u0438\u0446\u0430 \u0441 \u043e\u0431\u043c\u0435\u043d\u043d\u044b\u043c\u0438 \u043a\u0443\u0440\u0441\u0430\u043c\u0438",
|
||||||
"help_rate_form": "On this day, how many {to} will you get for one {from}?",
|
"help_rate_form": "On this day, how many {to} will you get for one {from}?",
|
||||||
"add_new_rate": "Add a new exchange rate",
|
"add_new_rate": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u043e\u0431\u043c\u0435\u043d\u043d\u044b\u0439 \u043a\u0443\u0440\u0441",
|
||||||
"save_new_rate": "Save new rate"
|
"save_new_rate": "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u0442\u0430\u0440\u0438\u0444"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"url": "\u0421\u0441\u044b\u043b\u043a\u0430",
|
"url": "\u0421\u0441\u044b\u043b\u043a\u0430",
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
"webhook_delivery": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430",
|
"webhook_delivery": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430",
|
||||||
"from_currency_to_currency": "{from} → {to}",
|
"from_currency_to_currency": "{from} → {to}",
|
||||||
"to_currency_from_currency": "{to} → {from}",
|
"to_currency_from_currency": "{to} → {from}",
|
||||||
"rate": "Rate"
|
"rate": "\u041e\u0446\u0435\u043d\u0438\u0442\u044c"
|
||||||
},
|
},
|
||||||
"list": {
|
"list": {
|
||||||
"active": "\u0410\u043a\u0442\u0438\u0432\u0435\u043d?",
|
"active": "\u0410\u043a\u0442\u0438\u0432\u0435\u043d?",
|
||||||
|
@@ -21,7 +21,7 @@
|
|||||||
"apply_rules_checkbox": "\u5e94\u7528\u89c4\u5219",
|
"apply_rules_checkbox": "\u5e94\u7528\u89c4\u5219",
|
||||||
"fire_webhooks_checkbox": "\u89e6\u53d1 webhook",
|
"fire_webhooks_checkbox": "\u89e6\u53d1 webhook",
|
||||||
"no_budget_pointer": "\u60a8\u8fd8\u6ca1\u6709\u9884\u7b97\uff0c\u60a8\u5e94\u8be5\u5728<a href=\"budgets\">\u9884\u7b97\u9875\u9762<\/a>\u8fdb\u884c\u521b\u5efa\u3002\u9884\u7b97\u53ef\u4ee5\u5e2e\u52a9\u60a8\u8ffd\u8e2a\u652f\u51fa\u3002",
|
"no_budget_pointer": "\u60a8\u8fd8\u6ca1\u6709\u9884\u7b97\uff0c\u60a8\u5e94\u8be5\u5728<a href=\"budgets\">\u9884\u7b97\u9875\u9762<\/a>\u8fdb\u884c\u521b\u5efa\u3002\u9884\u7b97\u53ef\u4ee5\u5e2e\u52a9\u60a8\u8ffd\u8e2a\u652f\u51fa\u3002",
|
||||||
"no_bill_pointer": "You seem to have no subscription yet. You should create some on the <a href=\"subscriptions\">subscription<\/a>-page. Subscriptions can help you keep track of expenses.",
|
"no_bill_pointer": "\u60a8\u4f3c\u4e4e\u8fd8\u6ca1\u6709\u8ba2\u9605\u3002\u60a8\u5e94\u8be5\u5728 <a href=\"subscriptions\">\u8ba2\u9605<\/a>-\u9875\u9762\u4e0a\u521b\u5efa\u4e00\u4e9b\u3002\u8ba2\u9605\u53ef\u4ee5\u5e2e\u52a9\u60a8\u8ddf\u8e2a\u8d39\u7528\u3002",
|
||||||
"source_account": "\u6765\u6e90\u8d26\u6237",
|
"source_account": "\u6765\u6e90\u8d26\u6237",
|
||||||
"hidden_fields_preferences": "\u60a8\u53ef\u4ee5\u5728<a href=\"preferences\">\u504f\u597d\u8bbe\u5b9a<\/a>\u4e2d\u542f\u7528\u66f4\u591a\u4ea4\u6613\u9009\u9879\u3002",
|
"hidden_fields_preferences": "\u60a8\u53ef\u4ee5\u5728<a href=\"preferences\">\u504f\u597d\u8bbe\u5b9a<\/a>\u4e2d\u542f\u7528\u66f4\u591a\u4ea4\u6613\u9009\u9879\u3002",
|
||||||
"destination_account": "\u76ee\u6807\u8d26\u6237",
|
"destination_account": "\u76ee\u6807\u8d26\u6237",
|
||||||
|
@@ -15,128 +15,128 @@ abstract class AbstractQueryParserInterfaceParseQueryTest extends TestCase
|
|||||||
{
|
{
|
||||||
abstract protected function createParser(): QueryParserInterface;
|
abstract protected function createParser(): QueryParserInterface;
|
||||||
|
|
||||||
public static function queryDataProvider(): array
|
public static function queryDataProvider(): iterable
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'empty query' => [
|
'empty query' => [
|
||||||
'query' => '',
|
'query' => '',
|
||||||
'expected' => new NodeGroup([])
|
'expected' => new NodeGroup([]),
|
||||||
],
|
],
|
||||||
'simple word' => [
|
'simple word' => [
|
||||||
'query' => 'groceries',
|
'query' => 'groceries',
|
||||||
'expected' => new NodeGroup([new StringNode('groceries')])
|
'expected' => new NodeGroup([new StringNode('groceries')]),
|
||||||
],
|
],
|
||||||
'prohibited word' => [
|
'prohibited word' => [
|
||||||
'query' => '-groceries',
|
'query' => '-groceries',
|
||||||
'expected' => new NodeGroup([new StringNode('groceries', true)])
|
'expected' => new NodeGroup([new StringNode('groceries', true)]),
|
||||||
],
|
],
|
||||||
'prohibited field' => [
|
'prohibited field' => [
|
||||||
'query' => '-amount:100',
|
'query' => '-amount:100',
|
||||||
'expected' => new NodeGroup([new FieldNode('amount', '100', true)])
|
'expected' => new NodeGroup([new FieldNode('amount', '100', true)]),
|
||||||
],
|
],
|
||||||
'quoted word' => [
|
'quoted word' => [
|
||||||
'query' => '"test phrase"',
|
'query' => '"test phrase"',
|
||||||
'expected' => new NodeGroup([new StringNode('test phrase')])
|
'expected' => new NodeGroup([new StringNode('test phrase')]),
|
||||||
],
|
],
|
||||||
'prohibited quoted word' => [
|
'prohibited quoted word' => [
|
||||||
'query' => '-"test phrase"',
|
'query' => '-"test phrase"',
|
||||||
'expected' => new NodeGroup([new StringNode('test phrase', true)])
|
'expected' => new NodeGroup([new StringNode('test phrase', true)]),
|
||||||
],
|
],
|
||||||
'multiple words' => [
|
'multiple words' => [
|
||||||
'query' => 'groceries shopping market',
|
'query' => 'groceries shopping market',
|
||||||
'expected' => new NodeGroup([
|
'expected' => new NodeGroup([
|
||||||
new StringNode('groceries'),
|
new StringNode('groceries'),
|
||||||
new StringNode('shopping'),
|
new StringNode('shopping'),
|
||||||
new StringNode('market')
|
new StringNode('market'),
|
||||||
])
|
]),
|
||||||
],
|
],
|
||||||
'field operator' => [
|
'field operator' => [
|
||||||
'query' => 'amount:100',
|
'query' => 'amount:100',
|
||||||
'expected' => new NodeGroup([new FieldNode('amount', '100')])
|
'expected' => new NodeGroup([new FieldNode('amount', '100')]),
|
||||||
],
|
],
|
||||||
'quoted field value with single space' => [
|
'quoted field value with single space' => [
|
||||||
'query' => 'description:"test phrase"',
|
'query' => 'description:"test phrase"',
|
||||||
'expected' => new NodeGroup([new FieldNode('description', 'test phrase')])
|
'expected' => new NodeGroup([new FieldNode('description', 'test phrase')]),
|
||||||
],
|
],
|
||||||
'multiple fields' => [
|
'multiple fields' => [
|
||||||
'query' => 'amount:100 category:food',
|
'query' => 'amount:100 category:food',
|
||||||
'expected' => new NodeGroup([
|
'expected' => new NodeGroup([
|
||||||
new FieldNode('amount', '100'),
|
new FieldNode('amount', '100'),
|
||||||
new FieldNode('category', 'food')
|
new FieldNode('category', 'food'),
|
||||||
])
|
]),
|
||||||
],
|
],
|
||||||
'simple subquery' => [
|
'simple subquery' => [
|
||||||
'query' => '(amount:100 category:food)',
|
'query' => '(amount:100 category:food)',
|
||||||
'expected' => new NodeGroup([
|
'expected' => new NodeGroup([
|
||||||
new NodeGroup([
|
new NodeGroup([
|
||||||
new FieldNode('amount', '100'),
|
new FieldNode('amount', '100'),
|
||||||
new FieldNode('category', 'food')
|
new FieldNode('category', 'food'),
|
||||||
])
|
]),
|
||||||
])
|
]),
|
||||||
],
|
],
|
||||||
'prohibited subquery' => [
|
'prohibited subquery' => [
|
||||||
'query' => '-(amount:100 category:food)',
|
'query' => '-(amount:100 category:food)',
|
||||||
'expected' => new NodeGroup([
|
'expected' => new NodeGroup([
|
||||||
new NodeGroup([
|
new NodeGroup([
|
||||||
new FieldNode('amount', '100'),
|
new FieldNode('amount', '100'),
|
||||||
new FieldNode('category', 'food')
|
new FieldNode('category', 'food'),
|
||||||
], true)
|
], true),
|
||||||
])
|
]),
|
||||||
],
|
],
|
||||||
'nested subquery' => [
|
'nested subquery' => [
|
||||||
'query' => '(amount:100 (description:"test" category:food))',
|
'query' => '(amount:100 (description:"test" category:food))',
|
||||||
'expected' => new NodeGroup([
|
'expected' => new NodeGroup([
|
||||||
new NodeGroup([
|
new NodeGroup([
|
||||||
new FieldNode('amount', '100'),
|
new FieldNode('amount', '100'),
|
||||||
new NodeGroup([
|
new NodeGroup([
|
||||||
new FieldNode('description', 'test'),
|
new FieldNode('description', 'test'),
|
||||||
new FieldNode('category', 'food')
|
new FieldNode('category', 'food'),
|
||||||
])
|
]),
|
||||||
])
|
]),
|
||||||
])
|
]),
|
||||||
],
|
],
|
||||||
'mixed words and operators' => [
|
'mixed words and operators' => [
|
||||||
'query' => 'groceries amount:50 shopping',
|
'query' => 'groceries amount:50 shopping',
|
||||||
'expected' => new NodeGroup([
|
'expected' => new NodeGroup([
|
||||||
new StringNode('groceries'),
|
new StringNode('groceries'),
|
||||||
new FieldNode('amount', '50'),
|
new FieldNode('amount', '50'),
|
||||||
new StringNode('shopping')
|
new StringNode('shopping'),
|
||||||
])
|
]),
|
||||||
],
|
],
|
||||||
'subquery after field value' => [
|
'subquery after field value' => [
|
||||||
'query' => 'amount:100 (description:"market" category:food)',
|
'query' => 'amount:100 (description:"market" category:food)',
|
||||||
'expected' => new NodeGroup([
|
'expected' => new NodeGroup([
|
||||||
new FieldNode('amount', '100'),
|
new FieldNode('amount', '100'),
|
||||||
new NodeGroup([
|
new NodeGroup([
|
||||||
new FieldNode('description', 'market'),
|
new FieldNode('description', 'market'),
|
||||||
new FieldNode('category', 'food')
|
new FieldNode('category', 'food'),
|
||||||
])
|
]),
|
||||||
])
|
]),
|
||||||
],
|
],
|
||||||
'word followed by subquery' => [
|
'word followed by subquery' => [
|
||||||
'query' => 'groceries (amount:100 description_contains:"test")',
|
'query' => 'groceries (amount:100 description_contains:"test")',
|
||||||
'expected' => new NodeGroup([
|
'expected' => new NodeGroup([
|
||||||
new StringNode('groceries'),
|
new StringNode('groceries'),
|
||||||
new NodeGroup([
|
new NodeGroup([
|
||||||
new FieldNode('amount', '100'),
|
new FieldNode('amount', '100'),
|
||||||
new FieldNode('description_contains', 'test')
|
new FieldNode('description_contains', 'test'),
|
||||||
])
|
]),
|
||||||
])
|
]),
|
||||||
],
|
],
|
||||||
'nested subquery with prohibited field' => [
|
'nested subquery with prohibited field' => [
|
||||||
'query' => '(amount:100 (description_contains:"test payment" -has_attachments:true))',
|
'query' => '(amount:100 (description_contains:"test payment" -has_attachments:true))',
|
||||||
'expected' => new NodeGroup([
|
'expected' => new NodeGroup([
|
||||||
new NodeGroup([
|
new NodeGroup([
|
||||||
new FieldNode('amount', '100'),
|
new FieldNode('amount', '100'),
|
||||||
new NodeGroup([
|
new NodeGroup([
|
||||||
new FieldNode('description_contains', 'test payment'),
|
new FieldNode('description_contains', 'test payment'),
|
||||||
new FieldNode('has_attachments', 'true', true)
|
new FieldNode('has_attachments', 'true', true),
|
||||||
])
|
]),
|
||||||
])
|
]),
|
||||||
])
|
]),
|
||||||
],
|
],
|
||||||
'complex nested subqueries' => [
|
'complex nested subqueries' => [
|
||||||
'query' => 'shopping (amount:50 market (-category:food word description:"test phrase" (has_notes:true)))',
|
'query' => 'shopping (amount:50 market (-category:food word description:"test phrase" (has_notes:true)))',
|
||||||
'expected' => new NodeGroup([
|
'expected' => new NodeGroup([
|
||||||
new StringNode('shopping'),
|
new StringNode('shopping'),
|
||||||
new NodeGroup([
|
new NodeGroup([
|
||||||
@@ -147,51 +147,52 @@ abstract class AbstractQueryParserInterfaceParseQueryTest extends TestCase
|
|||||||
new StringNode('word'),
|
new StringNode('word'),
|
||||||
new FieldNode('description', 'test phrase'),
|
new FieldNode('description', 'test phrase'),
|
||||||
new NodeGroup([
|
new NodeGroup([
|
||||||
new FieldNode('has_notes', 'true')
|
new FieldNode('has_notes', 'true'),
|
||||||
])
|
]),
|
||||||
])
|
]),
|
||||||
])
|
]),
|
||||||
])
|
]),
|
||||||
],
|
],
|
||||||
'word with multiple spaces' => [
|
'word with multiple spaces' => [
|
||||||
'query' => '"multiple spaces"',
|
'query' => '"multiple spaces"',
|
||||||
'expected' => new NodeGroup([new StringNode('multiple spaces')])
|
'expected' => new NodeGroup([new StringNode('multiple spaces')]),
|
||||||
],
|
],
|
||||||
'field with multiple spaces in value' => [
|
'field with multiple spaces in value' => [
|
||||||
'query' => 'description:"multiple spaces here"',
|
'query' => 'description:"multiple spaces here"',
|
||||||
'expected' => new NodeGroup([new FieldNode('description', 'multiple spaces here')])
|
'expected' => new NodeGroup([new FieldNode('description', 'multiple spaces here')]),
|
||||||
],
|
],
|
||||||
'unmatched right parenthesis in word' => [
|
'unmatched right parenthesis in word' => [
|
||||||
'query' => 'test)word',
|
'query' => 'test)word',
|
||||||
'expected' => new NodeGroup([new StringNode('test)word')])
|
'expected' => new NodeGroup([new StringNode('test)word')]),
|
||||||
],
|
],
|
||||||
'unmatched right parenthesis in field' => [
|
'unmatched right parenthesis in field' => [
|
||||||
'query' => 'description:test)phrase',
|
'query' => 'description:test)phrase',
|
||||||
'expected' => new NodeGroup([new FieldNode('description', 'test)phrase')])
|
'expected' => new NodeGroup([new FieldNode('description', 'test)phrase')]),
|
||||||
],
|
],
|
||||||
'subquery followed by word' => [
|
'subquery followed by word' => [
|
||||||
'query' => '(amount:100 category:food) shopping',
|
'query' => '(amount:100 category:food) shopping',
|
||||||
'expected' => new NodeGroup([
|
'expected' => new NodeGroup([
|
||||||
new NodeGroup([
|
new NodeGroup([
|
||||||
new FieldNode('amount', '100'),
|
new FieldNode('amount', '100'),
|
||||||
new FieldNode('category', 'food')
|
new FieldNode('category', 'food'),
|
||||||
]),
|
]),
|
||||||
new StringNode('shopping')
|
new StringNode('shopping'),
|
||||||
])
|
]),
|
||||||
]
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider queryDataProvider
|
* @dataProvider queryDataProvider
|
||||||
* @param string $query The query string to parse
|
*
|
||||||
* @param Node $expected The expected parse result
|
* @param string $query The query string to parse
|
||||||
|
* @param Node $expected The expected parse result
|
||||||
*/
|
*/
|
||||||
public function testQueryParsing(string $query, Node $expected): void
|
public function testQueryParsing(string $query, Node $expected): void
|
||||||
{
|
{
|
||||||
$actual = $this->createParser()->parse($query);
|
$actual = $this->createParser()->parse($query);
|
||||||
|
|
||||||
$this->assertEquals($expected, $actual);
|
self::assertSame($expected, $actual);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Tests\unit\Support\Search\QueryParser;
|
namespace Tests\unit\Support\Search\QueryParser;
|
||||||
|
|
||||||
use FireflyIII\Support\Search\QueryParser\GdbotsQueryParser;
|
use FireflyIII\Support\Search\QueryParser\GdbotsQueryParser;
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Tests\unit\Support\Search\QueryParser;
|
namespace Tests\unit\Support\Search\QueryParser;
|
||||||
|
|
||||||
use FireflyIII\Support\Search\QueryParser\QueryParser;
|
use FireflyIII\Support\Search\QueryParser\QueryParser;
|
||||||
use FireflyIII\Support\Search\QueryParser\QueryParserInterface;
|
use FireflyIII\Support\Search\QueryParser\QueryParserInterface;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @group unit-test
|
* @group unit-test
|
||||||
* @group support
|
* @group support
|
||||||
|
Reference in New Issue
Block a user