First version of line edit.

This commit is contained in:
James Cole 2024-03-16 22:00:25 +01:00
parent 845eaed8d7
commit f0fa21dead
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
10 changed files with 401 additions and 5 deletions

View File

@ -0,0 +1,81 @@
<?php
/*
* UpdateController.php
* Copyright (c) 2024 james@firefly-iii.org.
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V2\Controllers\Model\Account;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Model\Account\UpdateRequest;
use FireflyIII\Models\Account;
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface;
use FireflyIII\Transformers\V2\AccountTransformer;
use Illuminate\Http\JsonResponse;
class UpdateController extends Controller
{
public const string RESOURCE_KEY = 'accounts';
private AccountRepositoryInterface $repository;
/**
* AccountController constructor.
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
$this->repository = app(AccountRepositoryInterface::class);
// new way of user group validation
$userGroup = $this->validateUserGroup($request);
if (null !== $userGroup) {
$this->repository->setUserGroup($userGroup);
}
return $next($request);
}
);
}
/**
* TODO this endpoint is not yet reachable.
*/
public function update(UpdateRequest $request, Account $account): JsonResponse
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
$data = $request->getUpdateData();
$data['type'] = config('firefly.shortNamesByFullName.'.$account->accountType->type);
$account = $this->repository->update($account, $data);
$account->refresh();
app('preferences')->mark();
$transformer = new AccountTransformer();
$transformer->setParameters($this->parameters);
return response()
->api($this->jsonApiObject('accounts', $account, $transformer))
->header('Content-Type', self::CONTENT_TYPE)
;
}
}

View File

@ -0,0 +1,119 @@
<?php
/*
* UpdateRequest.php
* Copyright (c) 2024 james@firefly-iii.org.
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V2\Request\Model\Account;
use FireflyIII\Models\Account;
use FireflyIII\Models\Location;
use FireflyIII\Rules\IsBoolean;
use FireflyIII\Rules\UniqueAccountNumber;
use FireflyIII\Rules\UniqueIban;
use FireflyIII\Support\Request\AppendsLocationData;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Foundation\Http\FormRequest;
class UpdateRequest extends FormRequest
{
use ChecksLogin;
use AppendsLocationData;
use ConvertsDataTypes;
/**
* TODO is a duplicate of the v1 update thing.
* @return array
*/
public function getUpdateData(): array
{
$fields = [
'name' => ['name', 'convertString'],
'active' => ['active', 'boolean'],
'include_net_worth' => ['include_net_worth', 'boolean'],
'account_type_name' => ['type', 'convertString'],
'virtual_balance' => ['virtual_balance', 'convertString'],
'iban' => ['iban', 'convertString'],
'BIC' => ['bic', 'convertString'],
'account_number' => ['account_number', 'convertString'],
'account_role' => ['account_role', 'convertString'],
'liability_type' => ['liability_type', 'convertString'],
'opening_balance' => ['opening_balance', 'convertString'],
'opening_balance_date' => ['opening_balance_date', 'convertDateTime'],
'cc_type' => ['credit_card_type', 'convertString'],
'cc_monthly_payment_date' => ['monthly_payment_date', 'convertDateTime'],
'notes' => ['notes', 'stringWithNewlines'],
'interest' => ['interest', 'convertString'],
'interest_period' => ['interest_period', 'convertString'],
'order' => ['order', 'convertInteger'],
'currency_id' => ['currency_id', 'convertInteger'],
'currency_code' => ['currency_code', 'convertString'],
'liability_direction' => ['liability_direction', 'convertString'],
'liability_amount' => ['liability_amount', 'convertString'],
'liability_start_date' => ['liability_start_date', 'date'],
];
$data = $this->getAllData($fields);
return $this->appendLocationData($data, null);
}
/**
* TODO is a duplicate of the v1 UpdateRequest method.
*
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
/** @var Account $account */
$account = $this->route()->parameter('account');
$accountRoles = implode(',', config('firefly.accountRoles'));
$types = implode(',', array_keys(config('firefly.subTitlesByIdentifier')));
$ccPaymentTypes = implode(',', array_keys(config('firefly.ccTypes')));
$rules = [
'name' => sprintf('min:1|max:1024|uniqueAccountForUser:%d', $account->id),
'type' => sprintf('in:%s', $types),
'iban' => ['iban', 'nullable', new UniqueIban($account, $this->convertString('type'))],
'bic' => 'bic|nullable',
'account_number' => ['min:1', 'max:255', 'nullable', new UniqueAccountNumber($account, $this->convertString('type'))],
'opening_balance' => 'numeric|required_with:opening_balance_date|nullable',
'opening_balance_date' => 'date|required_with:opening_balance|nullable',
'virtual_balance' => 'numeric|nullable',
'order' => 'numeric|nullable',
'currency_id' => 'numeric|exists:transaction_currencies,id',
'currency_code' => 'min:3|max:51|exists:transaction_currencies,code',
'active' => [new IsBoolean()],
'include_net_worth' => [new IsBoolean()],
'account_role' => sprintf('in:%s|nullable|required_if:type,asset', $accountRoles),
'credit_card_type' => sprintf('in:%s|nullable|required_if:account_role,ccAsset', $ccPaymentTypes),
'monthly_payment_date' => 'date|nullable|required_if:account_role,ccAsset|required_if:credit_card_type,monthlyFull',
'liability_type' => 'required_if:type,liability|in:loan,debt,mortgage',
'liability_direction' => 'required_if:type,liability|in:credit,debit',
'interest' => 'required_if:type,liability|min:0|max:100|numeric',
'interest_period' => 'required_if:type,liability|in:daily,monthly,yearly',
'notes' => 'min:0|max:32768',
];
return Location::requestRules($rules);
}
}

View File

@ -28,6 +28,7 @@ use FireflyIII\Models\Account;
use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Services\Internal\Update\AccountUpdateService;
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Support\Collection;
@ -291,4 +292,12 @@ class AccountRepository implements AccountRepositoryInterface
return $dbQuery->take($limit)->get(['accounts.*']);
}
#[\Override] public function update(Account $account, array $data): Account
{
/** @var AccountUpdateService $service */
$service = app(AccountUpdateService::class);
return $service->update($account, $data);
}
}

View File

@ -72,5 +72,7 @@ interface AccountRepositoryInterface
public function setUser(User $user): void;
public function update(Account $account,array $data): Account;
public function setUserGroup(UserGroup $userGroup): void;
}

View File

@ -0,0 +1,36 @@
/*
* list.js
* Copyright (c) 2022 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {api} from "../../../../boot/axios";
import format from "date-fns/format";
export default class Put {
/**
*
* @param identifier
* @param params
* @returns {Promise<AxiosResponse<any>>}
*/
put(identifier, params) {
return api.put('/api/v2/accounts/' + identifier, params);
}
}

View File

@ -28,6 +28,8 @@ import '@ag-grid-community/styles/ag-grid.css';
import '@ag-grid-community/styles/ag-theme-alpine.css';
import '../../css/grid-ff3-theme.css';
import Get from "../../api/v2/model/account/get.js";
import GenericEditor from "../../support/editable/GenericEditor.js";
import Put from "../../api/v2/model/account/put.js";
// set type from URL
const urlParts = window.location.href.split('/');
@ -51,6 +53,7 @@ let index = function () {
enabled: true
},
},
editors: {},
sortingColumn: '',
sortDirection: '',
accounts: [],
@ -75,7 +78,36 @@ let index = function () {
this.notifications.wait.text = i18next.t('firefly.wait_loading_data')
this.loadAccounts();
},
submitInlineEdit(e) {
e.preventDefault();
const newTarget = e.currentTarget;
const index = newTarget.dataset.index;
const newValue = document.querySelectorAll('[data-index="'+index+'input"]')[0].value ?? '';
if('' === newValue) {
return;
}
// submit the field in an update thing?
const fieldName = this.editors[index].options.field;
const params = {};
params[fieldName] = newValue;
console.log(params);
console.log('New value is ' + newValue + ' for account #' + this.editors[index].options.id);
(new Put()).put(this.editors[index].options.id, params);
},
cancelInlineEdit(e) {
const newTarget = e.currentTarget;
const index = newTarget.dataset.index;
this.editors[index].cancel();
},
triggerEdit(e) {
const target = e.currentTarget;
const index = target.dataset.index;
// get parent:
this.editors[index] = new GenericEditor();
this.editors[index].setElement(target);
this.editors[index].init();
this.editors[index].replace();
},
loadAccounts() {
this.notifications.wait.show = true;
this.notifications.wait.text = i18next.t('firefly.wait_loading_data')
@ -100,13 +132,13 @@ let index = function () {
currency_code: current.attributes.currency_code,
native_current_balance: current.attributes.native_current_balance,
native_currency_code: current.attributes.native_currency_code,
last_activity: null === current.attributes.last_activity ? '' : format(new Date(current.attributes.last_activity),'P'),
last_activity: null === current.attributes.last_activity ? '' : format(new Date(current.attributes.last_activity), 'P'),
};
this.accounts.push(account);
}
}
this.notifications.wait.show = false;
// add click trigger thing.
});
},
}

View File

@ -22,7 +22,7 @@ $color-mode-type: media-query;
$link-decoration: none !default;
$font-family-sans-serif: "Roboto", sans-serif;
$danger: #CD5029 !default;
$danger: #CD5029 !default;
$primary: #1E6581 !default;
$success: #64B624 !default;
@ -43,7 +43,13 @@ $success: #64B624 !default;
// @import "~bootstrap-sass/assets/stylesheets/bootstrap";
// hover buttons
.hidden-edit-button {
cursor: pointer;
}
td:not(:hover) .hidden-edit-button {
visibility: hidden;
}

View File

@ -0,0 +1,107 @@
/*
* GenericEditor.js
* Copyright (c) 2024 james@firefly-iii.org.
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
export default class GenericEditor {
setElement(element) {
console.log('GenericEditor.setElement()', element);
this.element = element;
this.parent = element.parentElement;
this.options = {};
}
init() {
// grab some options from element itself:
this.options.type = this.element.dataset.type;
this.options.id = this.element.dataset.id;
this.options.value = this.element.dataset.value;
this.options.index = this.element.dataset.index;
this.options.model = this.element.dataset.model;
this.options.field = this.element.dataset.field;
//this.options.field = this.element.dataset.type;
console.log('GenericEditor['+this.options.index+'].init()');
}
replace() {
console.log('GenericEditor['+this.options.index+'].replace()');
// save old HTML in data field (does that work, is it safe?)
this.options.original = this.element.parentElement.innerHTML;
if (this.options.type === 'text') {
this.replaceText();
}
}
replaceText() {
console.log('GenericEditor['+this.options.index+'].replaceText()');
let html = this.formStart() + this.rowStart();
// input field:
html += this.columnStart('7') + this.label() + this.textField() + this.closeDiv();
// add submit button
html += this.columnStart('5') + this.buttonGroup() + this.closeDiv();
// close column and form:
html += this.closeDiv() + this.closeForm();
this.element.parentElement.innerHTML = html;
}
textField() {
return '<input data-index="' + this.options.index + 'input" autocomplete="off" type="text" class="form-control form-control-sm" id="input" name="name" value="' + this.options.value + '" placeholder="' + this.options.value + '" autofocus>';
}
closeDiv() {
return '</div>';
}
closeForm() {
return '</form>';
}
formStart() {
return '<form class="form-inline">';
}
rowStart() {
return '<div class="row">';
}
columnStart(param) {
if ('' === param) {
return '<div class="col">';
}
return '<div class="col-' + param + '">';
}
label() {
return '<label class="sr-only" for="input">Field value</label>';
}
buttonGroup() {
return '<div class="btn-group btn-group-sm" role="group" aria-label="Options">'+
'<button data-index="'+this.options.index+'" type="button" @click="cancelInlineEdit" class="btn btn-danger"><em class="fa-solid fa-xmark text-white"></em></button>'+
'<button data-index="'+this.options.index+'" type="submit" @click="submitInlineEdit" class="btn btn-success"><em class="fa-solid fa-check"></em></button>' +
'</div>';
}
cancel() {
console.log('GenericEditor['+this.options.index+'].cancel()');
console.log(this.element);
console.log(this.parent);
this.parent.innerHTML = this.options.original;
}
submitInlineEdit(e) {
console.log('Submit?');
}
}

View File

@ -99,6 +99,7 @@
<a :href="'./accounts/show/' + account.id">
<span x-text="account.name"></span>
</a>
<em :data-index="account.id + 'name'" @click="triggerEdit" data-type="text" data-model="Account" :data-id="account.id" data-field="name" :data-value="account.name" class="hidden-edit-button inline-edit-button fa-solid fa-pencil" data-id="1"></em>
</td>
<td>
<span x-text="account.type"></span>
@ -147,6 +148,8 @@
</div>
</div>
@endsection
@section('scripts')
@vite(['resources/assets/v2/pages/accounts/index.js'])

View File

@ -106,6 +106,7 @@ Route::group(
static function (): void {
Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']);
Route::get('{account}', ['uses' => 'ShowController@show', 'as' => 'show']);
Route::put('{account}', ['uses' => 'UpdateController@update', 'as' => 'update']);
}
);