mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2026-11-03 12:49:58 -06:00
New filter option
This commit is contained in:
1 parent
57af6c76f5
commit
90f80dc7b1
18 files changed
+160
-36
No files matched your search
@@ -75,14 +75,16 @@ final class ShowController extends Controller
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
'sort' => $sort,
|
||||
'filter' => $filter,
|
||||
'start' => $start,
|
||||
'end' => $end,
|
||||
'active' => $active,
|
||||
'date' => $date,
|
||||
] = $request->attributes->all();
|
||||
|
||||
// get list of accounts. Count it and split it.
|
||||
$this->repository->resetAccountOrder();
|
||||
$collection = $this->repository->getAccountsByType($types, $sort, $active);
|
||||
$collection = $this->repository->getAccountsByType($types, $sort, $filter, $active);
|
||||
$count = $collection->count();
|
||||
$dbFields = $this->isAllDatabaseSort($sort, 'Account');
|
||||
$accounts = $collection;
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace FireflyIII\Api\V1\Requests;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Override;
|
||||
use RuntimeException;
|
||||
|
||||
@@ -52,7 +53,7 @@ abstract class AggregateFormRequest extends ApiRequest
|
||||
parent::initialize($query, $request, $attributes, $cookies, $files, $server, $content);
|
||||
|
||||
// instantiate all subrequests and share current requests' bags with them
|
||||
// Log::debug('Initializing AggregateFormRequest.');
|
||||
Log::debug('Initializing AggregateFormRequest.');
|
||||
|
||||
/** @var array|string $config */
|
||||
foreach ($this->getRequests() as $config) {
|
||||
@@ -61,7 +62,7 @@ abstract class AggregateFormRequest extends ApiRequest
|
||||
if (!is_a($requestClass, Request::class, true)) {
|
||||
throw new RuntimeException('getRequests() must return class-strings of subclasses of Request');
|
||||
}
|
||||
// Log::debug(sprintf('Initializing subrequest %s', $requestClass));
|
||||
Log::debug(sprintf('Initializing subrequest %s', $requestClass));
|
||||
|
||||
$instance = new $requestClass();
|
||||
$this->requests[] = $instance;
|
||||
@@ -96,7 +97,7 @@ abstract class AggregateFormRequest extends ApiRequest
|
||||
// register all subrequests' validators
|
||||
foreach ($this->requests as $request) {
|
||||
if (method_exists($request, 'withValidator')) {
|
||||
// Log::debug(sprintf('Process withValidator from class %s', $request::class));
|
||||
Log::debug(sprintf('Process withValidator from class %s', $request::class));
|
||||
$request->withValidator($validator);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,14 +45,15 @@ class FilterRequest extends ApiRequest
|
||||
$this->filterClass = $config['filter_class'] ?? null;
|
||||
|
||||
if (null === $this->filterClass) {
|
||||
throw new RuntimeException('FilterRequest requires a filter_class config');
|
||||
//throw new RuntimeException('FilterRequest requires a filter_class config');
|
||||
$this->filterClass = 'Account';
|
||||
}
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'filter.*' => ['min:0','max:255', $this->required, new IsValidFilterInstruction((string) $this->filterClass)],
|
||||
'filter' => ['min:0','max:255', $this->required, new IsValidFilterInstruction((string) $this->filterClass)],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -62,14 +63,23 @@ class FilterRequest extends ApiRequest
|
||||
if (count($validator->failed()) > 0) {
|
||||
return;
|
||||
}
|
||||
var_dump($this->input('filter'));exit;
|
||||
$filter = $this->convertToFilter();
|
||||
});
|
||||
|
||||
|
||||
return;
|
||||
$validator->after(function (Validator $validator): void {
|
||||
if (count($validator->failed()) > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filter = $this->convertToFilter('');
|
||||
$this->attributes->set('filter', $filter);
|
||||
});
|
||||
}
|
||||
private function convertToFilter(string $value): array {
|
||||
var_dump($value);
|
||||
exit;
|
||||
return [];
|
||||
// var_dump($value);
|
||||
// exit;
|
||||
// return [];
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,6 @@ class ShowRequest extends AggregateFormRequest
|
||||
{
|
||||
return [
|
||||
[PaginationRequest::class, 'sort_class' => Account::class],
|
||||
[FilterRequest::class, 'filter_class' => Account::class],
|
||||
DateRangeRequest::class,
|
||||
ActiveObjectRequest::class,
|
||||
DateRequest::class,
|
||||
|
||||
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Api\V1\Requests;
|
||||
|
||||
use FireflyIII\Rules\IsValidFilterInstruction;
|
||||
use FireflyIII\Rules\IsValidSortInstruction;
|
||||
use FireflyIII\Support\Facades\Preferences;
|
||||
use FireflyIII\User;
|
||||
@@ -50,6 +51,7 @@ class PaginationRequest extends ApiRequest
|
||||
{
|
||||
return [
|
||||
'sort' => ['nullable', new IsValidSortInstruction((string) $this->sortClass)],
|
||||
'filter' => ['nullable', new IsValidFilterInstruction((string)$this->sortClass)],
|
||||
'limit' => ['numeric', 'min:1', 'max:131337'],
|
||||
'page' => ['numeric', 'min:1', 'max:131337'],
|
||||
];
|
||||
@@ -57,24 +59,29 @@ class PaginationRequest extends ApiRequest
|
||||
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
|
||||
$validator->after(function (Validator $validator): void {
|
||||
if (count($validator->failed()) > 0) {
|
||||
return;
|
||||
}
|
||||
$data = $validator->getData();
|
||||
$limit = $this->integerFromValue($data['limit'] ?? null);
|
||||
|
||||
$limit = $this->convertInteger('limit');
|
||||
if (0 === $limit) {
|
||||
// get default for user:
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
$limit = (int) Preferences::getForUser($user, 'listPageSize', 50)->data;
|
||||
}
|
||||
$page = $this->convertInteger('page');
|
||||
$page = $this->integerFromValue($data['page'] ?? '1');
|
||||
|
||||
$page = clamp(value: $page, min: 1, max: 2 ** 16);
|
||||
$offset = ($page - 1) * $limit;
|
||||
$sort = null !== $this->sortClass ? $this->convertSortParameters('sort', $this->sortClass) : $this->get('sort');
|
||||
$sort = null !== $this->sortClass ? $this->convertSortParameters('sort', $this->sortClass) : '';
|
||||
$filter = null !== $this->sortClass ? $this->convertFilterParameters('filter', $this->sortClass) : [];
|
||||
$this->attributes->set('limit', $limit);
|
||||
$this->attributes->set('sort', $sort);
|
||||
$this->attributes->set('filter', $filter);
|
||||
$this->attributes->set('page', $page);
|
||||
$this->attributes->set('offset', $offset);
|
||||
});
|
||||
|
||||
@@ -175,7 +175,11 @@ class AttachmentHelper implements AttachmentHelperInterface
|
||||
$parts = explode('/', $attachment->fileName());
|
||||
$file = $parts[count($parts) - 1];
|
||||
Log::debug(sprintf('Write file to disk in file named "%s"', $file));
|
||||
$this->uploadDisk->put($file, $content);
|
||||
$res = $this->uploadDisk->put($file, $content);
|
||||
if(false === $res) {
|
||||
Log::error('Could not write to disk.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// update attachment.
|
||||
$attachment->md5 = md5_file($path);
|
||||
|
||||
@@ -225,7 +225,7 @@ class AccountRepository implements AccountRepositoryInterface, UserGroupInterfac
|
||||
return $query->get(['accounts.*']);
|
||||
}
|
||||
|
||||
public function getAccountsByType(array $types, ?array $sort = [], ?bool $filterActive = null): Collection
|
||||
public function getAccountsByType(array $types, ?array $sort = [], ?array $filter = [], ?bool $filterActive = null): Collection
|
||||
{
|
||||
$res = array_intersect([
|
||||
AccountTypeEnum::ASSET->value,
|
||||
@@ -244,6 +244,8 @@ class AccountRepository implements AccountRepositoryInterface, UserGroupInterfac
|
||||
// add sort parameters
|
||||
$allowed = config('firefly.allowed_db_sort_parameters.Account', []);
|
||||
$sorted = 0;
|
||||
$sort = $sort ?? [];
|
||||
$filter = $filter ?? [];
|
||||
if (0 !== count($sort)) {
|
||||
foreach ($sort as $param) {
|
||||
if (in_array($param[0], $allowed, true)) {
|
||||
@@ -252,6 +254,16 @@ class AccountRepository implements AccountRepositoryInterface, UserGroupInterfac
|
||||
}
|
||||
}
|
||||
}
|
||||
$allowed = config('firefly.allowed_filter_parameters.Account', []);
|
||||
if (0 !== count($filter)) {
|
||||
foreach ($filter as $field => $search) {
|
||||
if (in_array($field, $allowed, true)) {
|
||||
if('' !== (string) $search) {
|
||||
$query->whereLike($field, sprintf('%%%s%%', $search));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === $sorted) {
|
||||
if (0 !== count($res)) {
|
||||
|
||||
@@ -90,7 +90,7 @@ interface AccountRepositoryInterface
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public function getAccountsByType(array $types, ?array $sort = [], ?bool $filterActive = null): Collection;
|
||||
public function getAccountsByType(array $types, ?array $sort = [], ?array $filter = [], ?bool $filterActive = null): Collection;
|
||||
|
||||
public function getAccountsByTypeForGroup(array $types, ?array $sort = []): Collection;
|
||||
|
||||
|
||||
@@ -33,11 +33,29 @@ class IsValidFilterInstruction implements ValidationRule
|
||||
#[\Override]
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
if('' === (string)$value) {
|
||||
if(!is_array($value)) {
|
||||
return;
|
||||
}
|
||||
var_dump($attribute);
|
||||
var_dump($value);
|
||||
// TODO: Implement validate() method.
|
||||
if(0 === count($value)) {
|
||||
return;
|
||||
}
|
||||
$shortClass = str_replace('FireflyIII\Models\\', '', $this->class);
|
||||
$validParameters = config(sprintf('firefly.allowed_filter_parameters.%s', $shortClass));
|
||||
if (!is_array($validParameters)) {
|
||||
$fail('validation.no_filter_instructions')->translate(['object' => $shortClass]);
|
||||
|
||||
return;
|
||||
}
|
||||
foreach($value as $key => $search) {
|
||||
if(!in_array($key, $validParameters)) {
|
||||
$fail('validation.no_filter_instructions')->translate(['object' => $shortClass]);
|
||||
return;
|
||||
}
|
||||
if(strlen($search) > 50) {
|
||||
$fail('validation.no_filter_instructions')->translate(['object' => $shortClass]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -186,6 +186,27 @@ trait ConvertsDataTypes
|
||||
return $sortParameters;
|
||||
}
|
||||
|
||||
public function convertFilterParameters(string $field, string $class): array
|
||||
{
|
||||
$parameter = request()->query->all($field);
|
||||
|
||||
if(!is_array($parameter)) {
|
||||
return [];
|
||||
}
|
||||
if(0 === count($parameter)) {
|
||||
return [];
|
||||
}
|
||||
$shortClass = str_replace('FireflyIII\Models\\', '', $class);
|
||||
$validParameters = config(sprintf('firefly.allowed_filter_parameters.%s', $shortClass));
|
||||
$return = [];
|
||||
foreach($parameter as $key => $search) {
|
||||
if(in_array($key, $validParameters) && strlen($search) <= 50) {
|
||||
$return[$key] = trim($search);
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return string value.
|
||||
*/
|
||||
|
||||
+5
-1
@@ -848,7 +848,11 @@ return [
|
||||
|
||||
// dynamic date ranges are as follows:
|
||||
'dynamic_date_ranges' => ['last7', 'last30', 'last90', 'last365', 'MTD', 'QTD', 'YTD'],
|
||||
|
||||
'allowed_filter_parameters' => [
|
||||
'Account' => [
|
||||
'name'
|
||||
],
|
||||
],
|
||||
'allowed_sort_parameters' => [
|
||||
'Account' => [
|
||||
'id',
|
||||
|
||||
@@ -25,7 +25,7 @@ import Alpine from "alpinejs";
|
||||
import {getVariable} from "../../store/get-variable.js";
|
||||
import Put from "../../api/model/account/put.js";
|
||||
import Get from "../../api/model/account/get.js";
|
||||
import {format} from "date-fns";
|
||||
import format from '../../util/format.js';
|
||||
import formatMoney from "../../util/format-money.js";
|
||||
import i18next from "i18next";
|
||||
import {addDrag} from "../shared/drag-and-droppable-rows.js";
|
||||
@@ -52,21 +52,42 @@ let index = function () {
|
||||
sums: {},
|
||||
debts: {},
|
||||
differences: {},
|
||||
filter: {
|
||||
name: 'chec',
|
||||
},
|
||||
formatMoney: formatMoney,
|
||||
objectToQueryString(obj, prefix) {
|
||||
return Object.keys(obj).map(objKey => {
|
||||
if (obj.hasOwnProperty(objKey)) {
|
||||
const key = prefix ? `${prefix}[${objKey}]` : objKey;
|
||||
const value = obj[objKey];
|
||||
|
||||
return typeof value === "object" ?
|
||||
this.objectToQueryString(value, key) :
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}).join("&");
|
||||
},
|
||||
|
||||
updateHistory() {
|
||||
if (history.pushState) {
|
||||
// TODO fix this.
|
||||
let obj = {
|
||||
page: this.page,
|
||||
column: this.sortColumn,
|
||||
direction: this.sortDirection,
|
||||
filter: this.filter
|
||||
};
|
||||
let string = new URLSearchParams(obj).toString();
|
||||
string = this.objectToQueryString(obj);
|
||||
let newUrl =
|
||||
window.location.protocol +
|
||||
"//" +
|
||||
window.location.host +
|
||||
window.location.pathname +
|
||||
"?page=" +
|
||||
this.page +
|
||||
"&column=" +
|
||||
this.sortColumn +
|
||||
"&direction=" +
|
||||
this.sortDirection;
|
||||
"?" +string;
|
||||
window.history.pushState({path: newUrl}, "", newUrl);
|
||||
window.store.set(this.storageKey, {column: this.sortColumn, direction: this.sortDirection});
|
||||
}
|
||||
@@ -135,7 +156,9 @@ let index = function () {
|
||||
let input = document.querySelector(`div.search-filter[data-column="${column}"]`);
|
||||
input.classList.add('d-none');
|
||||
|
||||
// TODO also clear the search filter!!!!
|
||||
// TODO hardcoded!!
|
||||
this.filter.name = '';
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,6 +214,7 @@ let index = function () {
|
||||
page: this.page,
|
||||
type: this.objectType,
|
||||
start: start,
|
||||
filter: this.filter,
|
||||
end: end,
|
||||
})
|
||||
.then((response) => {
|
||||
@@ -330,7 +354,7 @@ let index = function () {
|
||||
if (null === date) {
|
||||
return "";
|
||||
}
|
||||
return format(new Date(date), i18next.t("config.date_time_fns_short", {lng: window.store.get("locale")}));
|
||||
return format(new Date(date), i18next.t("config.date_time_fns_short", {lng: window.store.get("locale")}), window.store.get("locale"));
|
||||
},
|
||||
handlePageClick(e) {
|
||||
let link = e.currentTarget;
|
||||
|
||||
@@ -336,7 +336,7 @@ let create = function () {
|
||||
this.processUploadError(event);
|
||||
});
|
||||
document.addEventListener("upload-failed", (event) => {
|
||||
// console.log('Now in event listener "upload-failed"');
|
||||
console.log('Now in event listener "upload-failed"');
|
||||
this.processUploadError(event);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -344,6 +344,11 @@ let transactions = function () {
|
||||
this.autoStep();
|
||||
});
|
||||
|
||||
document.addEventListener("upload-failed", (event) => {
|
||||
console.log('Now in event listener "upload-failed"');
|
||||
this.processUploadError(event);
|
||||
});
|
||||
|
||||
// add some event listeners
|
||||
document.addEventListener("upload-success", () => {
|
||||
// console.log('Trigger on event "upload-success"');
|
||||
|
||||
@@ -95,8 +95,12 @@ export default function (date, formatStr = "PP", locale) {
|
||||
if (typeof locale === "undefined") {
|
||||
locale = window.__localeId__.replace("_", "");
|
||||
}
|
||||
let res = '(DATE ERROR)';
|
||||
try {
|
||||
res = format(date, formatStr, {locale: locales[locale] ?? locales[locale.slice(0, 2)] ?? locales["enUS"],});
|
||||
} catch(error) {
|
||||
console.warn('Could not format date: ', error);
|
||||
}
|
||||
// console.log("Locale is " + locale);
|
||||
return format(date, formatStr, {
|
||||
locale: locales[locale] ?? locales[locale.slice(0, 2)] ?? locales["enUS"], // or global.__localeId__
|
||||
});
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
|
||||
export default function serialize (obj, prefix) {
|
||||
let str = [], p;
|
||||
for (p in obj) {
|
||||
if (Object.hasOwn(obj, p)) {
|
||||
let k = prefix ? prefix + "[" + p + "]" : p, v = obj[p];
|
||||
str.push((v !== null && typeof v === "object") ? serialize(v, k) : encodeURIComponent(k) + "=" + encodeURIComponent(v));
|
||||
}
|
||||
}
|
||||
return str.join("&");
|
||||
}
|
||||
@@ -31,6 +31,7 @@ return [
|
||||
'invalid_transaction_type_list' => 'Invalid transaction type list',
|
||||
'limit_exists' => 'There is already a budget limit (amount) for this budget and currency in the given period.',
|
||||
'invalid_sort_instruction' => 'The sort instruction is invalid for an object of type ":object".',
|
||||
'no_filter_instructions' => 'The filter instruction is invalid for an object of type ":object".',
|
||||
'invalid_sort_instruction_index' => 'The sort instruction at index #:index is invalid for an object of type ":object".',
|
||||
'no_sort_instructions' => 'There are no sort instructions defined for an object of type ":object".',
|
||||
'webhook_budget_info' => 'Cannot deliver budget information for transaction related webhooks.',
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<span data-column="name" class="title">{{ trans('list.name') }}</span>
|
||||
<span data-column="name" class="search-spacer"> </span><em data-column="name" class="search-button search-button-name bi bi-search"></em>
|
||||
<div data-column="name" class="search-filter search-filter-name d-none input-group">
|
||||
<input type="search" class="form-control form-control-sm" placeholder="Filter name...">
|
||||
<input type="search" x-model="filter.name" class="form-control form-control-sm" placeholder="Filter name...">
|
||||
<button class="btn btn-outline-secondary btn-sm hide-button" data-column="name" type="button"><em class="bi bi-x text-danger"></em></button>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
Reference in new issue
Block a user