diff --git a/app/Api/V1/Controllers/Models/Account/ShowController.php b/app/Api/V1/Controllers/Models/Account/ShowController.php index bcbf124e39..1f08080d89 100644 --- a/app/Api/V1/Controllers/Models/Account/ShowController.php +++ b/app/Api/V1/Controllers/Models/Account/ShowController.php @@ -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; diff --git a/app/Api/V1/Requests/AggregateFormRequest.php b/app/Api/V1/Requests/AggregateFormRequest.php index 95aaf8788e..10eeb5a1ba 100644 --- a/app/Api/V1/Requests/AggregateFormRequest.php +++ b/app/Api/V1/Requests/AggregateFormRequest.php @@ -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); } } diff --git a/app/Api/V1/Requests/Generic/FilterRequest.php b/app/Api/V1/Requests/Generic/FilterRequest.php index 89a6c95efd..0910cbc528 100644 --- a/app/Api/V1/Requests/Generic/FilterRequest.php +++ b/app/Api/V1/Requests/Generic/FilterRequest.php @@ -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 []; } } diff --git a/app/Api/V1/Requests/Models/Account/ShowRequest.php b/app/Api/V1/Requests/Models/Account/ShowRequest.php index e5b8c26d4f..ac5c632efd 100644 --- a/app/Api/V1/Requests/Models/Account/ShowRequest.php +++ b/app/Api/V1/Requests/Models/Account/ShowRequest.php @@ -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, diff --git a/app/Api/V1/Requests/PaginationRequest.php b/app/Api/V1/Requests/PaginationRequest.php index cd1f7d7579..1ebbbc4370 100644 --- a/app/Api/V1/Requests/PaginationRequest.php +++ b/app/Api/V1/Requests/PaginationRequest.php @@ -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); }); diff --git a/app/Helpers/Attachments/AttachmentHelper.php b/app/Helpers/Attachments/AttachmentHelper.php index d750a31989..b386a90184 100644 --- a/app/Helpers/Attachments/AttachmentHelper.php +++ b/app/Helpers/Attachments/AttachmentHelper.php @@ -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); diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index 71efe6641b..c42ca914c9 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -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)) { diff --git a/app/Repositories/Account/AccountRepositoryInterface.php b/app/Repositories/Account/AccountRepositoryInterface.php index b3a1ca89d3..cb00317fa8 100644 --- a/app/Repositories/Account/AccountRepositoryInterface.php +++ b/app/Repositories/Account/AccountRepositoryInterface.php @@ -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; diff --git a/app/Rules/IsValidFilterInstruction.php b/app/Rules/IsValidFilterInstruction.php index 5fc32153ab..ed0e409306 100644 --- a/app/Rules/IsValidFilterInstruction.php +++ b/app/Rules/IsValidFilterInstruction.php @@ -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; } } diff --git a/app/Support/Request/ConvertsDataTypes.php b/app/Support/Request/ConvertsDataTypes.php index 818fcd44de..97a204abd8 100644 --- a/app/Support/Request/ConvertsDataTypes.php +++ b/app/Support/Request/ConvertsDataTypes.php @@ -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. */ diff --git a/config/firefly.php b/config/firefly.php index 635d85e261..cb9fb617e8 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -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', diff --git a/resources/assets/v3/js/pages/accounts/index.js b/resources/assets/v3/js/pages/accounts/index.js index 33bec441d2..2eb10a446e 100644 --- a/resources/assets/v3/js/pages/accounts/index.js +++ b/resources/assets/v3/js/pages/accounts/index.js @@ -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; diff --git a/resources/assets/v3/js/pages/transactions/create.js b/resources/assets/v3/js/pages/transactions/create.js index 19449febdc..5c877e4751 100644 --- a/resources/assets/v3/js/pages/transactions/create.js +++ b/resources/assets/v3/js/pages/transactions/create.js @@ -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); }); }, diff --git a/resources/assets/v3/js/pages/transactions/edit.js b/resources/assets/v3/js/pages/transactions/edit.js index 6727d95496..e7144f0d34 100644 --- a/resources/assets/v3/js/pages/transactions/edit.js +++ b/resources/assets/v3/js/pages/transactions/edit.js @@ -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"'); diff --git a/resources/assets/v3/js/util/format.js b/resources/assets/v3/js/util/format.js index a1a50a709d..8da335d829 100644 --- a/resources/assets/v3/js/util/format.js +++ b/resources/assets/v3/js/util/format.js @@ -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; } diff --git a/resources/assets/v3/js/util/serialize.js b/resources/assets/v3/js/util/serialize.js new file mode 100644 index 0000000000..04ccef3f04 --- /dev/null +++ b/resources/assets/v3/js/util/serialize.js @@ -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("&"); +} diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index f882d87ba7..8cab50b1f7 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -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.', diff --git a/resources/views/accounts/index.blade.php b/resources/views/accounts/index.blade.php index 2a234449a2..535bf605ab 100644 --- a/resources/views/accounts/index.blade.php +++ b/resources/views/accounts/index.blade.php @@ -35,7 +35,7 @@ {{ trans('list.name') }}  
- +