add some validation to row edit, better handle setting null cols with empty

editing
Ondřej Hruška 6 years ago
parent 06fb01d146
commit 6cfd5aa7e9
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 3
      app/Exceptions/Handler.php
  2. 14
      app/Exceptions/ViewException.php
  3. 77
      app/Http/Controllers/TableEditController.php
  4. 7
      app/Models/Table.php
  5. 8
      app/Tables/Changeset.php
  6. 5
      app/Tables/Column.php
  7. 46
      porklib/Exceptions/SimpleValidationException.php
  8. 2
      resources/assets/js/base-setup.js
  9. 22
      resources/assets/js/components/RowEditor.vue
  10. 48
      resources/views/table/propose/edit-rows.blade.php
  11. 27
      resources/views/table/propose/layout-row-pagination.blade.php
  12. 2
      resources/views/table/propose/layout.blade.php
  13. 1
      routes/web.php

@ -6,6 +6,7 @@ use Exception;
use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use MightyPork\Exceptions\Exceptions\SimpleValidationException;
class Handler extends ExceptionHandler class Handler extends ExceptionHandler
{ {
@ -15,7 +16,7 @@ class Handler extends ExceptionHandler
* @var array * @var array
*/ */
protected $dontReport = [ protected $dontReport = [
// SimpleValidationException::class,
]; ];
/** /**

@ -1,14 +0,0 @@
<?php
namespace FlowBox\Exceptions;
class ViewException extends FBRuntimeException
{
public $captured;
public function __construct(\Exception $cause, $captured)
{
$this->captured = $captured;
parent::__construct($cause);
}
}

@ -3,14 +3,12 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Row;
use App\Models\Table; use App\Models\Table;
use App\Models\User; use App\Models\User;
use App\Tables\Changeset; use App\Tables\Changeset;
use App\Tables\Column;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Input;
use MightyPork\Exceptions\SimpleValidationException;
class TableEditController extends Controller class TableEditController extends Controller
{ {
@ -22,14 +20,12 @@ class TableEditController extends Controller
*/ */
private function getChangeset(Table $table) private function getChangeset(Table $table)
{ {
$session_key = "proposal_{$table->id}";
if (Input::has('reset')) { if (Input::has('reset')) {
session()->forget($session_key); session()->forget($table->draftSessionKey);
} }
/** @var Changeset $changeset */ /** @var Changeset $changeset */
return session()->remember($session_key, function () use ($table) { return session()->remember($table->draftSessionKey, function () use ($table) {
$changeset = new Changeset(); $changeset = new Changeset();
$changeset->table = $table; $changeset->table = $table;
$changeset->revision = $table->revision; $changeset->revision = $table->revision;
@ -39,10 +35,21 @@ class TableEditController extends Controller
private function storeChangeset(Changeset $chs) private function storeChangeset(Changeset $chs)
{ {
$session_key = "proposal_{$chs->table->id}"; session()->put($chs->table->draftSessionKey, $chs);
}
/**
* Discard draft and redirect to table view
*/
public function discard(User $user, string $table)
{
/** @var Table $tableModel */
$tableModel = $user->tables()->where('name', $table)->first();
if ($tableModel === null) abort(404, "No such table.");
session()->forget($tableModel->draftSessionKey);
session()->put($session_key, $chs); return redirect($tableModel->viewRoute);
session()->save();
} }
public function draft(User $user, string $table, $tab = null) public function draft(User $user, string $table, $tab = null)
@ -103,29 +110,33 @@ class TableEditController extends Controller
$input = objBag($request->all(), false); $input = objBag($request->all(), false);
$code = 200; try {
switch ($input->action) { $code = 200;
case 'row.update': switch ($input->action) {
$data = (object)$input->data; case 'row.update':
$changeset->rowUpdate($data); $data = (object)$input->data;
$changeset->rowUpdate($data);
$resp = $changeset->fetchAndTransformRow($data->_id);
break; $resp = $changeset->fetchAndTransformRow($data->_id);
break;
case 'row.remove':
$changeset->rowRemove($input->id); case 'row.remove':
$resp = $changeset->fetchAndTransformRow($input->id); $changeset->rowRemove($input->id);
break; $resp = $changeset->fetchAndTransformRow($input->id);
break;
case 'row.restore':
$changeset->rowRestore($input->id); case 'row.restore':
$resp = $changeset->fetchAndTransformRow($input->id); $changeset->rowRestore($input->id);
break; $resp = $changeset->fetchAndTransformRow($input->id);
break;
default:
$resp = "Bad Action"; default:
$code = 400; $resp = "Bad Action";
break; $code = 400;
break;
}
} catch (SimpleValidationException $e) {
return $this->jsonResponse(['errors' => $e->getMessageBag()->getMessages()], 400);
} }
$this->storeChangeset($changeset); $this->storeChangeset($changeset);

@ -25,6 +25,9 @@ use Illuminate\Database\Eloquent\Collection;
* @property-read string $draftRoute * @property-read string $draftRoute
* @property-read string $settingsRoute * @property-read string $settingsRoute
* @property-read string $deleteRoute * @property-read string $deleteRoute
* @property-read string $draftSessionKey
* @property-read string $draftDiscardRoute
* @property-read string $draftUpdateRoute
* @property-read User $owner * @property-read User $owner
* @property-read Table $parentTable * @property-read Table $parentTable
* @property-read Table[]|Collection $forks * @property-read Table[]|Collection $forks
@ -136,9 +139,13 @@ class Table extends BaseModel
case 'settingsRoute': return route('table.conf', $arg); case 'settingsRoute': return route('table.conf', $arg);
case 'draftRoute': return route('table.draft', $arg); case 'draftRoute': return route('table.draft', $arg);
case 'deleteRoute': return route('table.delete', $arg); case 'deleteRoute': return route('table.delete', $arg);
case 'draftDiscardRoute': return route('table.draft-discard', $arg);
case 'draftUpdateRoute': return route('table.draft-update', $arg);
} }
} }
if ($name == 'draftSessionKey') return "proposal.{$this->id}";
return parent::__get($name); return parent::__get($name);
} }

@ -325,8 +325,12 @@ class Changeset
foreach ($newVals as $colId => $value) { foreach ($newVals as $colId => $value) {
if (starts_with($colId, '_')) continue; // internals if (starts_with($colId, '_')) continue; // internals
if (!isset($origRow->$colId) || $value != $origRow->$colId) { $col = $cols[$colId];
$updateObj[$colId] = $cols[$colId]->cast($value); $value = $col->cast($value);
$origValue = $col->cast(isset($origRow->$colId) ? $origRow->$colId : null);
if ($value !== $origValue) {
$updateObj[$colId] = $value;
} }
} }

@ -4,6 +4,7 @@ namespace App\Tables;
use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Arrayable;
use JsonSerializable; use JsonSerializable;
use MightyPork\Exceptions\SimpleValidationException;
use MightyPork\Exceptions\NotApplicableException; use MightyPork\Exceptions\NotApplicableException;
use MightyPork\Utils\Utils; use MightyPork\Utils\Utils;
@ -172,13 +173,13 @@ class Column implements JsonSerializable, Arrayable
if (is_int($value)) return $value; if (is_int($value)) return $value;
if (is_float($value)) return round($value); if (is_float($value)) return round($value);
if (is_numeric($value)) return intval($value); if (is_numeric($value)) return intval($value);
throw new NotApplicableException("Could not convert value \"$value\" to int!"); throw new SimpleValidationException($this->id, "Could not convert value \"$value\" to int!");
case 'float': case 'float':
if (is_null($value)) return 0; if (is_null($value)) return 0;
if (is_int($value) || is_float($value)) return (float)$value; if (is_int($value) || is_float($value)) return (float)$value;
if (is_numeric($value)) return floatval($value); if (is_numeric($value)) return floatval($value);
throw new NotApplicableException("Could not convert value \"$value\" to float!"); throw new SimpleValidationException($this->id, "Could not convert value \"$value\" to float!");
case 'bool': case 'bool':
if (is_null($value)) return false; if (is_null($value)) return false;

@ -0,0 +1,46 @@
<?php
namespace MightyPork\Exceptions;
use Illuminate\Contracts\Support\MessageProvider;
use Illuminate\Support\MessageBag;
class SimpleValidationException extends RuntimeException implements MessageProvider
{
/** @var MessageBag */
private $mb;
/**
* FBValidationException constructor.
*
* @param string|MessageProvider $key
* @param string $message
*/
public function __construct($key, $message = null)
{
if (is_null($message)) {
$this->mb = $key->getMessageBag();
} else {
$mb = new MessageBag();
$mb->add($key, $message);
$this->mb = $mb;
}
$str = '';
foreach ($this->mb->getMessages() as $key => $errors) {
$str .= $key . ': ' . implode(', ', $errors) . "\n";
}
parent::__construct($str);
}
/**
* Get the messages for the instance.
*
* @return MessageBag
*/
public function getMessageBag()
{
return $this->mb;
}
}

@ -1,4 +1,4 @@
//window._ = require('lodash'); window._ = require('lodash');
window.Popper = require('popper.js').default window.Popper = require('popper.js').default
/** /**

@ -26,14 +26,16 @@
<template v-if="row._editing"> <template v-if="row._editing">
<td v-for="col in columns" class="pr-0"> <td v-for="col in columns" class="pr-0">
<input v-model="row[col.id]" class="form-control" type="text"> <input v-model="row[col.id]" :class="['form-control', { 'is-invalid': row._errors && row._errors[col.id] }]"
:title="(row._errors && row._errors[col.id]) ? row._errors[col.id][0] : null"
type="text" @keyup.enter="toggleRowEditing(row._id)">
</td> </td>
</template> </template>
<template v-else> <template v-else>
<td v-for="col in columns"> <td v-for="col in columns">
<span class="text-danger strike" title="Original value" v-if="isChanged(row, col.id)">{{row._orig[col.id]}}</span> <span class="text-danger strike" title="Original value" v-if="isChanged(row, col.id)">{{row._orig[col.id]}}</span>
<span>{{ row[col.id] || '' }}</span> <span>{{ row[col.id] }}</span>
<v-icon v-if="isChanged(row, col.id)" <v-icon v-if="isChanged(row, col.id)"
@click="revertCell(row, col.id)" @click="revertCell(row, col.id)"
class="fa-undo text-danger pointer" class="fa-undo text-danger pointer"
@ -52,8 +54,6 @@
</style> </style>
<script> <script>
import { merge } from 'lodash';
export default { export default {
props: { props: {
route: String, route: String,
@ -69,11 +69,13 @@ export default {
busy (yes) { busy (yes) {
$('#draft-busy').css('opacity', yes ? 1 : 0) $('#draft-busy').css('opacity', yes ? 1 : 0)
}, },
query (data, sucfn) { query (data, sucfn, erfn) {
this.busy(true) this.busy(true)
if (!sucfn) sucfn = ()=>{} if (!sucfn) sucfn = ()=>{}
window.axios.post(this.route, data).then(sucfn).catch((e) => { if (!sucfn) erfn = ()=>{}
console.error(e) window.axios.post(this.route, data).then(sucfn).catch((error) => {
console.error(error.message)
erfn(error.response.data)
}).then(() => { }).then(() => {
this.busy(false) this.busy(false)
}) })
@ -94,6 +96,10 @@ export default {
data: row data: row
}, (resp) => { }, (resp) => {
this.$set(this.rows, resp.data._id, resp.data); this.$set(this.rows, resp.data._id, resp.data);
}, (er) => {
if (!_.isUndefined(er.errors)) {
this.$set(this.rows[row._id], '_errors', er.errors);
}
}) })
}, },
toggleRowEditing(_id) { toggleRowEditing(_id) {
@ -127,7 +133,7 @@ export default {
return row._changed.indexOf(colId) > -1 return row._changed.indexOf(colId) > -1
}, },
revertCell(row, colId) { revertCell(row, colId) {
this.submitRowChange(merge({}, row, { [colId]: row._orig[colId] })) this.submitRowChange(_.merge({}, row, { [colId]: row._orig[colId] }))
} }
} }
} }

@ -4,40 +4,24 @@
/** @var \App\Tables\Changeset $changeset */ /** @var \App\Tables\Changeset $changeset */
/** @var \App\Models\Row[]|Illuminate\Pagination\Paginator $rows */ /** @var \App\Models\Row[]|Illuminate\Pagination\Paginator $rows */
/** @var \App\Models\Table $table */ /** @var \App\Models\Table $table */
/** @var \App\Tables\Column[] $columns */
/** @var \App\Tables\Changeset $changeset */
/** @var \App\Models\Table $table */
@endphp @endphp
@extends('table.propose.layout') @extends('table.propose.layout-row-pagination')
@section('tab-content')
@if($rows->hasPages())
<div class="col-md-12 d-flex">
<nav class="text-center" aria-label="Pages of the table">
{{ $rows->links(null, ['ulClass' => 'mb-0']) }}
</nav>
</div>
@endif
<div class="col-12">
@php
$transformed = $rows->keyBy('_id')->map(function($r) use ($changeset) {
/** @var \App\Tables\Changeset $changeset */
return $changeset->transformRow($r, true);
});
@endphp
<table is="row-editor" @section('rows')
route="{{$table->getDraftRoute('update')}}" @php
:columns="{{toJSON($columns)}}" $transformed = $rows->keyBy('_id')->map(function($r) use ($changeset) {
:x-rows="{{toJSON($transformed)}}"> /** @var \App\Tables\Changeset $changeset */
</table> return $changeset->transformRow($r, true);
</div> });
@endphp
@if($rows->hasPages()) <table is="row-editor"
<div class="col-md-12 d-flex"> route="{{$table->draftUpdateRoute}}"
<nav class="text-center" aria-label="Pages of the table"> :columns="{{toJSON($columns)}}"
{{ $rows->links(null, ['ulClass' => 'mb-0']) }} :x-rows="{{toJSON($transformed)}}">
</nav> </table>
</div>
@endif
@stop @stop

@ -0,0 +1,27 @@
@php
/** @var \App\Models\Row[]|Illuminate\Pagination\Paginator $rows */
@endphp
@extends('table.propose.layout')
@section('tab-content')
@if($rows->hasPages())
<div class="col-md-12 d-flex">
<nav class="text-center" aria-label="Pages of the table">
{{ $rows->links(null, ['ulClass' => 'mb-0']) }}
</nav>
</div>
@endif
<div class="col-12">
@yield('rows')
</div>
@if($rows->hasPages())
<div class="col-md-12 d-flex">
<nav class="text-center" aria-label="Pages of the table">
{{ $rows->links(null, ['ulClass' => 'mb-0']) }}
</nav>
</div>
@endif
@stop

@ -22,7 +22,7 @@ if (!isset($tab) || $tab == '') $tab = 'edit-rows';
<h1 class="mx-3"> <h1 class="mx-3">
<a href="{{ $table->viewRoute }}" class="link-no-color">{{ $table->title }}</a> <a href="{{ $table->viewRoute }}" class="link-no-color">{{ $table->title }}</a>
</h1> </h1>
<a href="" class="btn btn-outline-danger mr-2" @tooltip(Discard changes)> <a href="{{$table->draftDiscardRoute}}" class="btn btn-outline-danger mr-2" @tooltip(Discard changes)>
@icon(fa-trash-o, sr:Discard) @icon(fa-trash-o, sr:Discard)
</a> </a>
@if(user()->ownsTable($table)) @if(user()->ownsTable($table))

@ -51,6 +51,7 @@ Route::group(['middleware' => ['auth', 'activated']], function () {
Route::post('@{user}/{table}/delete', 'TableController@delete')->name('table.delete'); Route::post('@{user}/{table}/delete', 'TableController@delete')->name('table.delete');
Route::post('@{user}/{table}/draft/update', 'TableEditController@draftUpdate')->name('table.draft-update'); Route::post('@{user}/{table}/draft/update', 'TableEditController@draftUpdate')->name('table.draft-update');
Route::get('@{user}/{table}/draft/discard', 'TableEditController@discard')->name('table.draft-discard');
Route::get('@{user}/{table}/draft/{tab?}', 'TableEditController@draft')->name('table.draft'); Route::get('@{user}/{table}/draft/{tab?}', 'TableEditController@draft')->name('table.draft');
}); });

Loading…
Cancel
Save