From 3a7a41a9cb52b38c514265d3335db230715421ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 4 Aug 2018 20:50:00 +0200 Subject: [PATCH 01/10] changeset draft --- app/Http/Controllers/TableController.php | 24 ++++++++- app/Tables/Changeset.php | 69 ++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 app/Tables/Changeset.php diff --git a/app/Http/Controllers/TableController.php b/app/Http/Controllers/TableController.php index 8f84bcf..2633bd5 100644 --- a/app/Http/Controllers/TableController.php +++ b/app/Http/Controllers/TableController.php @@ -31,7 +31,7 @@ class TableController extends Controller ->where('name', $table)->first(); if ($tableModel === null) abort(404, "No such table."); - // make it possible to show other revisions + // option to show other revisions if ($input->has('rev')) { $rev = (int)$input->rev; $revision = $tableModel->revisions()->orderBy('created_at')->skip($rev - 1)->first(); @@ -54,6 +54,28 @@ class TableController extends Controller ]); } + public function draftChange(Request $request, User $user, string $table) + { + /** @var Table $tableModel */ + $tableModel = $user->tables()->with('revision')->where('name', $table)->first(); + if ($tableModel === null) abort(404, "No such table."); + + $revision = $tableModel->revision; + + $columns = Column::columnsFromJson($revision->columns); + + $rows = $revision->rowsData($columns)->paginate(25, []); + + // TODO instantiate changeset and store it in session + + return view('table.view', [ + 'table' => $tableModel, + 'revision' => $revision, + 'columns' => $columns, + 'rows' => $rows, + ]); + } + public function delete(Request $request, User $user, string $table) { /** @var Table $tableModel */ diff --git a/app/Tables/Changeset.php b/app/Tables/Changeset.php new file mode 100644 index 0000000..39cfe88 --- /dev/null +++ b/app/Tables/Changeset.php @@ -0,0 +1,69 @@ + values, ...] + */ + public $rowUpdates; + + /** + * New rows in the full Row::data format, including GRIDs. + * Values are identified by GCIDs from previously defined, or new columns. + * + * @var array|null - [[_id:..., ...], [..., ...], ...] + */ + public $newRows; + + /** + * Rows to be removed + * + * @var int[]|null - GRIDs + */ + public $removedRows; + + /** + * Values changed in column specifications, such as name, title, etc. + * This does not affect the table rows in any way. + * + * @var array[] - column specification objects, with GCIDs + */ + public $columnUpdates; + + /** + * New columns in the full format, including GCIDs + * + * @var array|null - [[id:..., ...], [..., ...], ...] + */ + public $newColumns; + + /** + * When reordering columns, here is the column IDs array + * in the new order. Columns meanwhile removed from the table + * or added to it are to be ignored or appended at the end. + * + * This shall not be filled if merely editing or adding columns, + * unless the order is explicitly adjusted by the user. + * + * @var string[]|null - GCIDs + */ + public $columnOrder; + + /** + * Columns to be removed + * + * The data associated to those columns may or may not be removed from the Rows. + * It does not matter, other than in regard to the table size. + * + * @var int[]|null - GCIDs + */ + public $removedColumns; +} From e17548e5e76f180d8d2a3d4abbe055addf8fd756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 4 Aug 2018 22:25:55 +0200 Subject: [PATCH 02/10] preparing for proposal composition --- app/Http/Controllers/TableController.php | 20 +++-- app/Models/Proposal.php | 60 ++++++++++++++- app/Tables/Changeset.php | 98 +++++++++++++++++++++++- 3 files changed, 167 insertions(+), 11 deletions(-) diff --git a/app/Http/Controllers/TableController.php b/app/Http/Controllers/TableController.php index 2633bd5..08235e4 100644 --- a/app/Http/Controllers/TableController.php +++ b/app/Http/Controllers/TableController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers; use App\Models\Revision; use App\Models\Table; use App\Models\User; +use App\Tables\Changeset; use App\Tables\Column; use App\Tables\ColumnNumerator; use App\Tables\CStructArrayExporter; @@ -57,22 +58,29 @@ class TableController extends Controller public function draftChange(Request $request, User $user, string $table) { /** @var Table $tableModel */ - $tableModel = $user->tables()->with('revision')->where('name', $table)->first(); + $tableModel = $user->tables()->where('name', $table)->first(); if ($tableModel === null) abort(404, "No such table."); - $revision = $tableModel->revision; + $session_key = "proposal_{$tableModel->id}"; - $columns = Column::columnsFromJson($revision->columns); + /** @var Changeset $changeset */ + $changeset = $request->session()->remember($session_key, function () use ($tableModel) { + $changeset = new Changeset(); + $changeset->table = $tableModel; + $changeset->revision = $tableModel->revision; + return $changeset; + }); + $revision = $changeset->revision; + $columns = Column::columnsFromJson($revision->columns); $rows = $revision->rowsData($columns)->paginate(25, []); - // TODO instantiate changeset and store it in session - - return view('table.view', [ + return view('table.propose', [ 'table' => $tableModel, 'revision' => $revision, 'columns' => $columns, 'rows' => $rows, + 'changeset' => $changeset, ]); } diff --git a/app/Models/Proposal.php b/app/Models/Proposal.php index f746405..a56f313 100644 --- a/app/Models/Proposal.php +++ b/app/Models/Proposal.php @@ -3,6 +3,8 @@ namespace App\Models; use App\Models\Concerns\Reportable; +use App\Tables\Changeset; +use MightyPork\Exceptions\NotApplicableException; /** * Change proposal @@ -14,7 +16,7 @@ use App\Models\Concerns\Reportable; * @property int $revision_id * @property int $author_id * @property string $note - * @property object $changes - JSONB + * @property Proposal $changes - JSONB * @property-read User $author * @property-read Table $table * @property-read Revision $revision @@ -43,4 +45,60 @@ class Proposal extends BaseModel { return $this->belongsTo(Table::class); } + + public function getChangesAttribute($value) + { + $changeset = Changeset::fromObject(fromJSON($value)); + $changeset->revision = $this->revision; + $changeset->table = $this->table; + $changeset->note = $this->note; + + return $changeset; + } + + public function setChangesAttribute($value) + { + if ($value instanceof Changeset) { + $this->attributes['changes'] = toJSON($value->toObject()); + } else { + throw new NotApplicableException("Only a Changeset may be set to Proposal->changes"); + } + } + + /** + * Create a new Proposal instance wrapping this changeset, + * owned by the currently logged in User. The created instance + * is NOT saved yet. + * + * @param Changeset $changeset - changeset to hydrate the proposal with + * @return Proposal + */ + public static function fromChangeset(Changeset $changeset) + { + if (!$changeset->hasAnyChanges()) { + throw new NotApplicableException('No changes to propose.'); + } + + if ($changeset->note == null) { + throw new NotApplicableException('Proposal note must be filled.'); + } + + if ($changeset->table == null || !$changeset->table instanceof Table) { + throw new NotApplicableException('Table not assigned to Changeset'); + } + + if ($changeset->revision == null || !$changeset->revision instanceof Revision) { + throw new NotApplicableException('Revision not assigned to Changeset'); + } + + return new Proposal([ + // relations + 'table_id' => $changeset->table->getKey(), + 'revision_id' => $changeset->revision->getKey(), + 'author_id' => \Auth::user()->getKey(), + // the proposal info + 'note' => $changeset->note, + 'changes' => $changeset->toObject(), + ]); + } } diff --git a/app/Tables/Changeset.php b/app/Tables/Changeset.php index 39cfe88..2bcde12 100644 --- a/app/Tables/Changeset.php +++ b/app/Tables/Changeset.php @@ -3,15 +3,44 @@ namespace App\Tables; +use App\Models\Revision; +use App\Models\Table; +use Illuminate\Queue\SerializesModels; +use ReflectionClass; + /** * Object representing a set of table modifications */ class Changeset { + use SerializesModels; + + const object_excluded_properties = [ + 'revision', + 'table', + 'note', + ]; + + /** + * @var Revision - base revision this changeset belongs to + */ + public $revision; + /** - * Rows whose content changed + * @var Table - table this changeset belongs to + */ + public $table; + + /** + * @var string - user's note attached to this changeset (future proposal) + */ + public $note; + + /** + * Rows whose content changed, identified by _id. + * Only changed values are to be filled. Columns are identified by GCIDs * - * @var array|null - [GRID -> values, ...] + * @var array|null - [[_id:…, …], …] */ public $rowUpdates; @@ -19,7 +48,7 @@ class Changeset * New rows in the full Row::data format, including GRIDs. * Values are identified by GCIDs from previously defined, or new columns. * - * @var array|null - [[_id:..., ...], [..., ...], ...] + * @var array|null - [[_id:…, …], …] */ public $newRows; @@ -41,7 +70,7 @@ class Changeset /** * New columns in the full format, including GCIDs * - * @var array|null - [[id:..., ...], [..., ...], ...] + * @var array|null - [[id:…, …], …] */ public $newColumns; @@ -66,4 +95,65 @@ class Changeset * @var int[]|null - GCIDs */ public $removedColumns; + + private function walkProps() + { + $properties = (new ReflectionClass($this))->getProperties(); + + foreach ($properties as $property) { + if (in_array($property->name, self::object_excluded_properties)) { + continue; + } + + yield $property->name; + } + } + + /** + * Reconstruct from a object, such as one found in Proposal. + * Note that the fromProposal() method should be used when the + * proposal is available, as it populates additional fields. + * + * @param \stdClass $changes + * @return Changeset + */ + public static function fromObject($changes) + { + $changeset = new Changeset(); + + foreach ($changeset->walkProps() as $prop) { + if (isset($changes->$prop)) { + $changeset->$prop = $changes->$prop; + } + } + + return $changeset; + } + + /** + * Serialize to an object format that may be stored in a Proposal. + * + * @return \stdClass + */ + public function toObject() + { + $object = new \stdClass(); + + foreach ($this->walkProps() as $prop) { + $object->$prop = array_values($this->$prop); + } + + return $object; + } + + public function hasAnyChanges() + { + foreach ($this->walkProps() as $prop) { + if (!empty($this->$prop)) { + return true; + } + } + + return false; + } } From bb8bc459dc6ac01c40c9a7e717da433a7ab8f8e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 5 Aug 2018 14:45:43 +0200 Subject: [PATCH 03/10] stub of the row editor --- _json_typehints.php | 17 +++ app/Http/Controllers/TableController.php | 29 ---- app/Http/Controllers/TableEditController.php | 66 ++++++++++ app/Models/BaseModel.php | 4 +- app/Models/Revision.php | 4 +- app/Models/Row.php | 2 +- app/Models/Table.php | 33 +++-- app/Providers/AppServiceProvider.php | 11 +- app/Tables/Changeset.php | 124 ++++++++++++++++-- app/Tables/Column.php | 96 ++++++++++++-- app/helpers.php | 4 + public/fonts/fa-dtbl-1-preview.html | 120 +++++++++++------ public/fonts/fa-dtbl-1.css | 39 +++--- public/fonts/fa-dtbl-1.eot | Bin 9984 -> 10520 bytes public/fonts/fa-dtbl-1.svg | 51 ++++--- public/fonts/fa-dtbl-1.ttf | Bin 9804 -> 10340 bytes public/fonts/fa-dtbl-1.woff2 | Bin 4996 -> 5108 bytes resources/assets/js/app.js | 1 + resources/assets/js/components/RowEditor.vue | 86 ++++++++++++ resources/assets/sass/_helpers.scss | 9 ++ resources/assets/sass/app.scss | 2 + .../bootstrap-customizations/_border.scss | 20 +++ .../sass/bootstrap-customizations/_nav.scss | 0 .../sass/bootstrap-customizations/_table.scss | 9 ++ resources/views/table/_rows.blade.php | 7 +- ...ade.php => _view-action-buttons.blade.php} | 40 ++---- resources/views/table/conf.blade.php | 3 +- .../views/table/propose/add-rows.blade.php | 6 + .../views/table/propose/edit-rows.blade.php | 43 ++++++ .../views/table/propose/layout.blade.php | 58 ++++++++ .../table/propose/manage-columns.blade.php | 6 + .../views/table/propose/review.blade.php | 6 + resources/views/table/view.blade.php | 2 +- routes/web.php | 3 + 34 files changed, 725 insertions(+), 176 deletions(-) create mode 100644 _json_typehints.php create mode 100644 app/Http/Controllers/TableEditController.php create mode 100644 resources/assets/js/components/RowEditor.vue create mode 100644 resources/assets/sass/bootstrap-customizations/_nav.scss create mode 100644 resources/assets/sass/bootstrap-customizations/_table.scss rename resources/views/table/{_action-buttons.blade.php => _view-action-buttons.blade.php} (60%) create mode 100644 resources/views/table/propose/add-rows.blade.php create mode 100644 resources/views/table/propose/edit-rows.blade.php create mode 100644 resources/views/table/propose/layout.blade.php create mode 100644 resources/views/table/propose/manage-columns.blade.php create mode 100644 resources/views/table/propose/review.blade.php diff --git a/_json_typehints.php b/_json_typehints.php new file mode 100644 index 0000000..7021d45 --- /dev/null +++ b/_json_typehints.php @@ -0,0 +1,17 @@ +tables()->where('name', $table)->first(); - if ($tableModel === null) abort(404, "No such table."); - - $session_key = "proposal_{$tableModel->id}"; - - /** @var Changeset $changeset */ - $changeset = $request->session()->remember($session_key, function () use ($tableModel) { - $changeset = new Changeset(); - $changeset->table = $tableModel; - $changeset->revision = $tableModel->revision; - return $changeset; - }); - - $revision = $changeset->revision; - $columns = Column::columnsFromJson($revision->columns); - $rows = $revision->rowsData($columns)->paginate(25, []); - - return view('table.propose', [ - 'table' => $tableModel, - 'revision' => $revision, - 'columns' => $columns, - 'rows' => $rows, - 'changeset' => $changeset, - ]); - } - public function delete(Request $request, User $user, string $table) { /** @var Table $tableModel */ diff --git a/app/Http/Controllers/TableEditController.php b/app/Http/Controllers/TableEditController.php new file mode 100644 index 0000000..9d4d484 --- /dev/null +++ b/app/Http/Controllers/TableEditController.php @@ -0,0 +1,66 @@ +id}"; + + if (Input::has('reset')) { + session()->forget($session_key); + } + + /** @var Changeset $changeset */ + return session()->remember($session_key, function () use ($table) { + $changeset = new Changeset(); + $changeset->table = $table; + $changeset->revision = $table->revision; + return $changeset; + }); + } + + public function draft(User $user, string $table, $tab = null) + { + /** @var Table $tableModel */ + $tableModel = $user->tables()->where('name', $table)->first(); + if ($tableModel === null) abort(404, "No such table."); + + if ($tab == null) $tab = 'edit-rows'; + $tabs = ['edit-rows', 'add-rows', 'manage-columns', 'review']; + if (!in_array($tab, $tabs)) abort(404, "No such tab: $tab"); + + $changeset = $this->getChangeset($tableModel); + + return $this->{camel_case($tab)}($changeset); + } + + private function editRows(Changeset $changeset) + { + $revision = $changeset->revision; + $columns = $changeset->transformColumns(); + $rows = $revision->rowsData($columns, true, false)->paginate(25, []); + + return view('table.propose.edit-rows', [ + 'changeset' => $changeset, + 'table' => $changeset->table, + 'columns' => collect($columns), + 'rows' => $rows, + ]); + } +} diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index 8561357..a7fb450 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -26,7 +26,9 @@ class BaseModel extends Model public function getRelationValue($key) { if ($this->exists && !method_exists($this, $key)) { - throw new \LogicException("No attribute or relation ".var_export($key, true)); + if (!isset($this->original[$key])) { + throw new \LogicException("No attribute or relation " . var_export($key, true)); + } } return parent::getRelationValue($key); diff --git a/app/Models/Revision.php b/app/Models/Revision.php index b140623..550c31d 100644 --- a/app/Models/Revision.php +++ b/app/Models/Revision.php @@ -40,12 +40,12 @@ class Revision extends BaseModel * @param Column[] $columns * @return \Illuminate\Database\Query\Builder|static */ - public function rowsData($columns, $withId=true) + public function rowsData($columns, $withId=true, $named=true) { $selects = $withId ? ["data->>'_id' as _id"] : []; foreach ($columns as $col) { - $selects[] = "data->>'$col->id' as $col->name"; + $selects[] = "data->>'$col->id' as " . ($named ? $col->name : $col->id); } return $this->rows()->select([])->selectRaw(implode(', ', $selects)); diff --git a/app/Models/Row.php b/app/Models/Row.php index 1b7220c..ca00961 100644 --- a/app/Models/Row.php +++ b/app/Models/Row.php @@ -6,7 +6,7 @@ namespace App\Models; * Row in a data table * * @property int $id - * @property string $data - JSONB, always containing _id + * @property object|\RowData $data - JSONB, always containing _id */ class Row extends BaseModel { diff --git a/app/Models/Table.php b/app/Models/Table.php index 454d121..f5c351b 100644 --- a/app/Models/Table.php +++ b/app/Models/Table.php @@ -22,7 +22,9 @@ use Illuminate\Database\Eloquent\Collection; * @property string $origin * @property int $visits * @property-read string $viewRoute + * @property-read string $draftRoute * @property-read string $settingsRoute + * @property-read string $deleteRoute * @property-read User $owner * @property-read Table $parentTable * @property-read Table[]|Collection $forks @@ -123,21 +125,32 @@ class Table extends BaseModel public function __get($name) { - if ($name == 'viewRoute') { - return route('table.view', ['user' => $this->cachedOwner()->name, 'table' => $this->name]); - } - - if ($name == 'settingsRoute') { - return route('table.conf', ['user' => $this->cachedOwner()->name, 'table' => $this->name]); - } - - if ($name == 'deleteRoute') { - return route('table.delete', ['user' => $this->cachedOwner()->name, 'table' => $this->name]); + if (ends_with($name, 'Route')) { + $arg = [ + 'user' => $this->cachedOwner()->name, + 'table' => $this->name + ]; + + switch ($name) { + case 'viewRoute': return route('table.view', $arg); + case 'settingsRoute': return route('table.conf', $arg); + case 'draftRoute': return route('table.draft', $arg); + case 'deleteRoute': return route('table.delete', $arg); + } } return parent::__get($name); } + public function getDraftRoute($tab=null) + { + return route('table.draft', [ + 'user' => $this->cachedOwner()->name, + 'table' => $this->name, + 'tab' => $tab, + ]); + } + public function scopeForList(Builder $query) { return $query->with('revision:id,row_count')->with('owner:id,name,title') diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 44d8fb8..d01dda5 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -29,8 +29,17 @@ class AppServiceProvider extends ServiceProvider }); \Blade::directive('tooltip', function($arg) { + $arg = trim($arg); + $placement = ''; + if (starts_with($arg, ['top,', 'bottom,', 'left,', 'right,'])) { + list($placement, $arg) = explode(',', $arg); + $arg = trim($arg); + } $arge = e($arg); - return 'aria-label="' . $arge . '" title="' . $arge . '"'; + + $html = ''; + if ($placement) $html .= 'data-placement="' . $placement . '" '; + return $html . 'data-toggle="tooltip" aria-label="' . $arge . '" title="' . $arge . '"'; }); \Blade::directive('sr', function($arg) { diff --git a/app/Tables/Changeset.php b/app/Tables/Changeset.php index 2bcde12..cc61ce0 100644 --- a/app/Tables/Changeset.php +++ b/app/Tables/Changeset.php @@ -40,39 +40,43 @@ class Changeset * Rows whose content changed, identified by _id. * Only changed values are to be filled. Columns are identified by GCIDs * - * @var array|null - [[_id:…, …], …] + * Key'd by _id + * + * @var array|null - [_id -> [_id:…, cid:…, cid:…], …] */ - public $rowUpdates; + public $rowUpdates = []; /** * New rows in the full Row::data format, including GRIDs. * Values are identified by GCIDs from previously defined, or new columns. * - * @var array|null - [[_id:…, …], …] + * @var array|null - [[_id:…, cid:…, cid:…], …] */ - public $newRows; + public $newRows = []; /** * Rows to be removed * * @var int[]|null - GRIDs */ - public $removedRows; + public $removedRows = []; /** * Values changed in column specifications, such as name, title, etc. * This does not affect the table rows in any way. * - * @var array[] - column specification objects, with GCIDs + * Key'd by id + * + * @var array[] - column specification objects, with GCIDs, key'd by CID */ - public $columnUpdates; + public $columnUpdates = []; /** * New columns in the full format, including GCIDs * * @var array|null - [[id:…, …], …] */ - public $newColumns; + public $newColumns = []; /** * When reordering columns, here is the column IDs array @@ -84,7 +88,7 @@ class Changeset * * @var string[]|null - GCIDs */ - public $columnOrder; + public $columnOrder = []; /** * Columns to be removed @@ -94,7 +98,10 @@ class Changeset * * @var int[]|null - GCIDs */ - public $removedColumns; + public $removedColumns = []; + + /** @var Column[] - loaded and transformed columns, cached from previous call to transformColumns() */ + private $cachedColumns; private function walkProps() { @@ -146,6 +153,11 @@ class Changeset return $object; } + /** + * Check if there is any change in this changeset + * + * @return bool - any found + */ public function hasAnyChanges() { foreach ($this->walkProps() as $prop) { @@ -156,4 +168,96 @@ class Changeset return false; } + + /** + * Decorate / transform a single row for the editor view + * + * @param object|\DecoratedRow $row - row, must be key'd by column ids + * @param bool $decorate - to add extra underscored info for the editor + * @return \DecoratedRow|object|null - null if not decorating and the row was removed + */ + public function transformRow($row, $decorate) + { + if ($decorate) { + $row->_remove = false; + $row->_changed = []; + $row->_orig = []; + } + + // Removed rows + if (in_array($row->_id, $this->removedRows)) { + if ($decorate) { + $row->_remove = true; + } else { + return null; + } + } + + // Changed values + if (isset($this->rowUpdates[$row->_id])) { + $newVals = $this->rowUpdates[$row->_id]; + + if ($decorate) { + $row->_changed = array_keys($newVals); + $row->_orig = array_only((array)$row, $row->_changed); + } + + $row = (object)array_merge((array)$row, $newVals); + } + + // Drop deleted columns + if (!$decorate) { + foreach ($this->removedColumns as $colId) { + unset($row->$colId); + } + } + + unset($row->_row_pivot); + + return $row; + } + + /** + * Decorate / transform columns (loaded from the source revision) + * + * @return Column[] + */ + public function transformColumns() + { + if ($this->cachedColumns) return $this->cachedColumns; + $columns = Column::columnsFromJson($this->revision->columns); + + // Modify columns + $byId = []; + foreach ($columns as $column) { + $byId[$column->id] = $column; + + if (isset($this->columnUpdates[$column->id])) { + $column->modifyByChangeset($this->columnUpdates[$column->id]); + } + + if (in_array($column->id, $this->removedColumns)) { + $column->markForRemoval(); + } + } + + // Append new columns + foreach ($this->newColumns as $newColumn) { + $columns[] = $c =new Column($newColumn); + $c->markAsNew(); + } + + // Reorder + $newOrder = []; + foreach ($this->columnOrder as $id) { + $newOrder[] = $byId[$id]; + } + + $leftover_keys = array_diff(array_keys($byId), $this->columnOrder); + foreach ($leftover_keys as $id) { + $newOrder[] = $byId[$id]; + } + + return $this->cachedColumns = $newOrder; + } } diff --git a/app/Tables/Column.php b/app/Tables/Column.php index 7be9660..1434153 100644 --- a/app/Tables/Column.php +++ b/app/Tables/Column.php @@ -1,9 +1,8 @@ toRemove = true; + } + + /** + * Mark this column as new + */ + public function markAsNew() + { + $this->isNew = true; + } + + /** + * Modify by a changeset + * + * @param array $columnObject + */ + public function modifyByChangeset(array $columnObject) + { + foreach ((array)$columnObject as $key => $value) { + if ($value != $this->$key) { + $this->modified_attribs[] = $key; + $this->orig_attribs[] = $this->$key; + $this->$key = $value; + } + } + } + + public function __get($name) + { + if (property_exists($this, $name)) { + return $this->$name; + } + + if (ends_with($name, '_modified')) { + $basename = str_replace('_modified', '', $name); + if (property_exists($this, $basename)) { + return in_array($basename, $this->modified_attribs); + } + } + + if (ends_with($name, '_orig')) { + $basename = str_replace('_orig', '', $name); + if (property_exists($this, $basename)) { + return $this->orig_attribs[$basename]; + } + } + + throw new NotApplicableException("No such column property: $name"); + } + + /** + * @param $columns + * @return Column[] + */ public static function columnsFromJson($columns) { if (is_string($columns)) { @@ -73,15 +154,6 @@ class Column implements JsonSerializable $this->title = $b->title ?: $b->name; } - public function __get($name) - { - if (property_exists($this, $name)) { - return $this->$name; - } - - throw new NotApplicableException("No such column property: $name"); - } - /** * @return array with keys {name, title, type} */ diff --git a/app/helpers.php b/app/helpers.php index 178ffa1..b0159ee 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -85,6 +85,10 @@ function old_json($name, $default) { // Safe JSON funcs function toJSON($object) { + if (!$object instanceof JsonSerializable && $object instanceof \Illuminate\Contracts\Support\Arrayable) { + $object = $object->toArray(); + } + return \GuzzleHttp\json_encode($object, JSON_UNESCAPED_SLASHES + JSON_UNESCAPED_UNICODE); } diff --git a/public/fonts/fa-dtbl-1-preview.html b/public/fonts/fa-dtbl-1-preview.html index 60afadf..41e09ca 100644 --- a/public/fonts/fa-dtbl-1-preview.html +++ b/public/fonts/fa-dtbl-1-preview.html @@ -175,6 +175,7 @@ .fa-google:before, .fa-history:before, .fa-home:before, +.fa-hourglass:before, .fa-inbox:before, .fa-key-modern:before, .fa-link:before, @@ -187,7 +188,9 @@ .fa-star-o:before, .fa-table:before, .fa-th-list:before, +.fa-times:before, .fa-trash-o:before, +.fa-undo:before, .fa-user:before, .fa-user-circle-o:before, .fa-user-plus:before, @@ -221,24 +224,27 @@ .fa-google:before { content: "\f10b"; } .fa-history:before { content: "\f10c"; } .fa-home:before { content: "\f10d"; } -.fa-inbox:before { content: "\f10e"; } -.fa-key-modern:before { content: "\f10f"; } -.fa-link:before { content: "\f110"; } -.fa-pencil:before { content: "\f111"; } -.fa-plus:before { content: "\f112"; } -.fa-question-circle:before { content: "\f113"; } -.fa-sign-in:before { content: "\f114"; } -.fa-sign-out:before { content: "\f115"; } -.fa-star:before { content: "\f116"; } -.fa-star-o:before { content: "\f117"; } -.fa-table:before { content: "\f118"; } -.fa-th-list:before { content: "\f119"; } -.fa-trash-o:before { content: "\f11a"; } -.fa-user:before { content: "\f11b"; } -.fa-user-circle-o:before { content: "\f11c"; } -.fa-user-plus:before { content: "\f11d"; } -.fa-users:before { content: "\f11e"; } -.fa-wrench:before { content: "\f11f"; } +.fa-hourglass:before { content: "\f10e"; } +.fa-inbox:before { content: "\f10f"; } +.fa-key-modern:before { content: "\f110"; } +.fa-link:before { content: "\f111"; } +.fa-pencil:before { content: "\f112"; } +.fa-plus:before { content: "\f113"; } +.fa-question-circle:before { content: "\f114"; } +.fa-sign-in:before { content: "\f115"; } +.fa-sign-out:before { content: "\f116"; } +.fa-star:before { content: "\f117"; } +.fa-star-o:before { content: "\f118"; } +.fa-table:before { content: "\f119"; } +.fa-th-list:before { content: "\f11a"; } +.fa-times:before { content: "\f11b"; } +.fa-trash-o:before { content: "\f11c"; } +.fa-undo:before { content: "\f11d"; } +.fa-user:before { content: "\f11e"; } +.fa-user-circle-o:before { content: "\f11f"; } +.fa-user-plus:before { content: "\f120"; } +.fa-users:before { content: "\f121"; } +.fa-wrench:before { content: "\f122"; } @@ -254,7 +260,7 @@
-

fa-dtbl-1 contains 32 glyphs:

+

fa-dtbl-1 contains 35 glyphs:

Toggle Preview Characters
@@ -444,6 +450,19 @@
+
+
+ PpPpPpPpPpPpPpPpPpPp +
+
+ 12141618212436486072 +
+
+ + +
+
+
PpPpPpPpPpPpPpPpPpPp @@ -453,7 +472,7 @@
- +
@@ -466,7 +485,7 @@
- +
@@ -479,7 +498,7 @@
- +
@@ -492,7 +511,7 @@
- +
@@ -505,7 +524,7 @@
- +
@@ -518,7 +537,7 @@
- +
@@ -531,7 +550,7 @@
- +
@@ -544,7 +563,7 @@
- +
@@ -557,7 +576,7 @@
- +
@@ -570,7 +589,7 @@
- +
@@ -583,7 +602,7 @@
- +
@@ -596,7 +615,21 @@
- + +
+ + +
+
+ PpPpPpPpPpPpPpPpPpPp +
+
+ 12141618212436486072 +
+
+ + +
@@ -609,7 +642,20 @@
- + +
+ + +
+
+ PpPpPpPpPpPpPpPpPpPp +
+
+ 12141618212436486072 +
+
+ +
@@ -622,7 +668,7 @@
- +
@@ -635,7 +681,7 @@
- +
@@ -648,7 +694,7 @@
- +
@@ -661,7 +707,7 @@
- +
@@ -674,7 +720,7 @@
- +
diff --git a/public/fonts/fa-dtbl-1.css b/public/fonts/fa-dtbl-1.css index 2a98041..c375860 100644 --- a/public/fonts/fa-dtbl-1.css +++ b/public/fonts/fa-dtbl-1.css @@ -52,21 +52,24 @@ .fa-google::before { content: "\f10b"; } .fa-history::before { content: "\f10c"; } .fa-home::before { content: "\f10d"; } -.fa-inbox::before { content: "\f10e"; } -.fa-key-modern::before { content: "\f10f"; } -.fa-link::before { content: "\f110"; } -.fa-pencil::before { content: "\f111"; } -.fa-plus::before { content: "\f112"; } -.fa-question-circle::before { content: "\f113"; } -.fa-sign-in::before { content: "\f114"; } -.fa-sign-out::before { content: "\f115"; } -.fa-star::before { content: "\f116"; } -.fa-star-o::before { content: "\f117"; } -.fa-table::before { content: "\f118"; } -.fa-th-list::before { content: "\f119"; } -.fa-trash-o::before { content: "\f11a"; } -.fa-user::before { content: "\f11b"; } -.fa-user-circle-o::before { content: "\f11c"; } -.fa-user-plus::before { content: "\f11d"; } -.fa-users::before { content: "\f11e"; } -.fa-wrench::before { content: "\f11f"; } +.fa-hourglass::before { content: "\f10e"; } +.fa-inbox::before { content: "\f10f"; } +.fa-key-modern::before { content: "\f110"; } +.fa-link::before { content: "\f111"; } +.fa-pencil::before { content: "\f112"; } +.fa-plus::before { content: "\f113"; } +.fa-question-circle::before { content: "\f114"; } +.fa-sign-in::before { content: "\f115"; } +.fa-sign-out::before { content: "\f116"; } +.fa-star::before { content: "\f117"; } +.fa-star-o::before { content: "\f118"; } +.fa-table::before { content: "\f119"; } +.fa-th-list::before { content: "\f11a"; } +.fa-times::before, .fa-close::before { content: "\f11b"; } +.fa-trash-o::before { content: "\f11c"; } +.fa-undo::before { content: "\f11d"; } +.fa-user::before { content: "\f11e"; } +.fa-user-circle-o::before { content: "\f11f"; } +.fa-user-plus::before { content: "\f120"; } +.fa-users::before { content: "\f121"; } +.fa-wrench::before { content: "\f122"; } diff --git a/public/fonts/fa-dtbl-1.eot b/public/fonts/fa-dtbl-1.eot index 71ba15c9aff0698f1d15cb6712f4467917f0d623..20ac8fb8ebdb9e510c01fba21303987d79a86f56 100644 GIT binary patch delta 892 zcmaJ)pMZ-s#k7cAa{4cYbVs%zc=pP7LycYE;O4$ef>7a&Ea927wY7 z5+c|w@j)U&qPHH_gAWm5^&$kl6%tWBL@)VN5F%Uun@ADT!}*`{`}qIP;hZyfX7wCf zSOqX#xy{oNoU5w@mM+ydg>1l=Ef$jjk-^bkCOf!z;<032@hNC^BbhSr{PnG5S9%x zW#tjBP$G$$*-t?rfSPG-B03aaa;~gWz&hbzBs>#?89YgP6XDXyaAf4-^Vf|4QjGR* zibdmz&ReIq0OUvHxeG`x8r5~1-b>~aKfq)l0^sf9z5ata{VkQ^fYqEF=sAq!YePv5>J;^fuHMMd2{YyZSWv&{I#5bh>$|XO&9Kc? za2wnn@8;L|U1P|&V*F})nc1DWm?dQ;vL0sr6ck~{+?rfvriEF$x-=!QKpP)}E=qJ; zi!qNaFO%dJ^!OV6t(?D>bCe03!NwKj{-b7m6f72DH=xx)eUa4M5cKppYigW*o?t_> zRHSG24r2!ox;iXEz#`0PkV?R>^o9aGu6CO!+S*+`fl#mFKa6AHPZK0fx&8bs-Bo&r zS!ZrTIX!Hi8Cl7}Q>jDQr$m2Jz^F_P5}lqJn+V6_ kYETYYG7{sFk+>l-6^<*eC^J1dtUi|O)emxk>90@s3y}=XAOHXW delta 498 zcmbOc)ZoX)pw7VHqc)MvjOCX7WA}*;t@Ve@3K$sF3>X*~Wc-8mjS?=78m^g&%g}SuL0z9q~}zoiJiW1 z5-9%!XxxO1)Wj4qrMfJj+#Mh`%K!@SO=MRFnk53{t7PPsRMcNs!vN$k?f?o*$jMJm zte;fb12kX;knfe7SW&=G%IE@=w*c}L@)C1XV-B1(0y0a0_FEL>7nhWlMHB+f0y(;h zff1+*h}|FC--_qA`O3h}4isQuxZW+b9Y+6u`|lqI0~<&s2LltsDdpSzP&kHj^Fdi1#?2Gu z)z}$LCht(w;V{v)&^0nJw3uwGt}vNLy^v95a-F&k3j-gc$mDhE5|huUw@r@Gh+t -Created by FontForge 20170805 at Sat Aug 4 10:56:23 2018 +Created by FontForge 20170805 at Sun Aug 5 14:30:22 2018 By ondra The Fork Awesome font is licensed under the SIL OFL 1.1 (http://scripts.sil.org/OFL). Fork Awesome is a fork based of off Font Awesome 4.7.0 by Dave Gandy. More info on licenses at https://forkawesome.github.io @@ -22,7 +22,7 @@ The Fork Awesome font is licensed under the SIL OFL 1.1 (http://scripts.sil.org/ bbox="-0.14014 -256.168 2048 1536.01" underline-thickness="89.6" underline-position="-179.2" - unicode-range="U+0020-F11F" + unicode-range="U+0020-F122" /> - + - - - - - - - - - - - - + - + - - - - diff --git a/public/fonts/fa-dtbl-1.ttf b/public/fonts/fa-dtbl-1.ttf index 922505025bc07b4e50ff4167e682227baa623105..3ec4f7969651dde9578ce0d202a6eea2036ef2bd 100644 GIT binary patch delta 851 zcmaJ!KLE@(<*+nivfSQ4?h7uDEQga4_kgyd zm=OU{jwBYp1_1Y@kd&T@j!XCLtB+7%9pgYmT8zOWIg7jx_wm|fP%rr8~BP`j%AUi!MMuWVT- z=n}f8x?h~kZR^`JTk3f(iBFP81r2~6PC+j!I!#r?sHv?HtZl^QY4Nr*-Uh~2!!fj( zsW|)(Te8U+4BU>tV8_&zLThutHEgf1w-37l&8#H8_`UK_ z(BJRqH1oW<)6wq_4$0nwFb(V%LFSSn#3u1E^V`%qwF0&HUq(IA2sRc?BeA-?ESe7D zaaOQsjuVrSW(l>>;-zmV1<|<&MlIZ=;D@VMIIQcK965re>dVPpno(#OuVSd=TvvjvA-r#G@Kg zLj+Q*m@Id+tQ?)6o0^g0aV2dD8cGt=k#L+&%t>+C5vAs5CzRioCIJXew+aUbn*)4$ MPKthinASpn054O^KmY&$ delta 449 zcmaD7aK@*efsuiMftR6yftew|%`L>Y<&aqc1B03YP*lc0Sl=k&LiQR42F3^=KOs3c zvEY%g=zj(VCJ&&PS8`d20)sV6J_7^u6remydSY?G|NlUf7}PX?e2(;-$~3Xl7fu4@ zp8$=Ukdd00BBoTA1(dr3#AX>8sr89`6WNu4hKT?LR5Ef)DlV*H0J0f(0QnPg@{<$m zCsp3mf8-XnWVN)Vl-!00jpG*Y{w|es53c+(VkI$@*KvD z$s9~NOsox)?UeHbvzq*nRttM zfA9f=X7d;3QohZ)!ZDnid1ZANH{X?0V`ns(%%Y|PBn{POFse*GrKTgpz{e=UD9R|t zD9$LsD9I?rD9tDXbbuVAJfp&73H7$gE7T*{*-MHNi!*d53uzckcGeK%hC5wqI{?jR BYtR4y diff --git a/public/fonts/fa-dtbl-1.woff2 b/public/fonts/fa-dtbl-1.woff2 index 855516621b8b0b872094bc04bc2a23fa062df914..f58599be77e8f4d12eaca623f6da1111633eb820 100644 GIT binary patch literal 5108 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 WFE2n$W+Ny|$fQ=$n_BuNXAS^65wd0g literal 4996 zcmV-~6MO7;Pew8T0RR910271&4FCWD047WT023?#0RR9100000000000000000000 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 O2j-W}_acL7HUIz&_sn + + + + + + + + + + + + + + + + + + + +
{{col.title}}
+ + + + + + + +
+ + + + + diff --git a/resources/assets/sass/_helpers.scss b/resources/assets/sass/_helpers.scss index 42e4cff..c18f61c 100644 --- a/resources/assets/sass/_helpers.scss +++ b/resources/assets/sass/_helpers.scss @@ -16,3 +16,12 @@ .box-shadow { box-shadow: 0 2px 3px rgba(black, .3); } + +.strike { + text-decoration: line-through; +} + +// for busy loaders etc +.opacity-fade { + transition: opacity .3s ease-in-out; +} diff --git a/resources/assets/sass/app.scss b/resources/assets/sass/app.scss index 1b0a595..e0c2512 100644 --- a/resources/assets/sass/app.scss +++ b/resources/assets/sass/app.scss @@ -20,6 +20,8 @@ html { @import "bootstrap-customizations/button"; @import "bootstrap-customizations/responsive"; @import "bootstrap-customizations/typography"; +@import "bootstrap-customizations/nav"; +@import "bootstrap-customizations/table"; .bio-table { td { diff --git a/resources/assets/sass/bootstrap-customizations/_border.scss b/resources/assets/sass/bootstrap-customizations/_border.scss index 67bb404..25ccb19 100644 --- a/resources/assets/sass/bootstrap-customizations/_border.scss +++ b/resources/assets/sass/bootstrap-customizations/_border.scss @@ -15,3 +15,23 @@ .border-2 { border-width: 2px !important; } + +.rounded-top-0 { + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; +} + +.rounded-left-0 { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} + +.rounded-right-0 { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +.rounded-bottom-0 { + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} diff --git a/resources/assets/sass/bootstrap-customizations/_nav.scss b/resources/assets/sass/bootstrap-customizations/_nav.scss new file mode 100644 index 0000000..e69de29 diff --git a/resources/assets/sass/bootstrap-customizations/_table.scss b/resources/assets/sass/bootstrap-customizations/_table.scss new file mode 100644 index 0000000..532da54 --- /dev/null +++ b/resources/assets/sass/bootstrap-customizations/_table.scss @@ -0,0 +1,9 @@ +.table-fixed { + table-layout: fixed; +} + +.td-va-middle { + td, th { + vertical-align: middle !important; + } +} diff --git a/resources/views/table/_rows.blade.php b/resources/views/table/_rows.blade.php index e71908a..3a59768 100644 --- a/resources/views/table/_rows.blade.php +++ b/resources/views/table/_rows.blade.php @@ -4,24 +4,23 @@ @php /** @var object[] $columns */ + /** @var \App\Tables\Changeset[] $changeset */ /** @var \App\Models\Row[] $rows */ @endphp - @foreach($columns as $col) - + @endforeach @foreach($rows as $row) - @foreach($columns as $col) - + @endforeach @endforeach diff --git a/resources/views/table/_action-buttons.blade.php b/resources/views/table/_view-action-buttons.blade.php similarity index 60% rename from resources/views/table/_action-buttons.blade.php rename to resources/views/table/_view-action-buttons.blade.php index f026f42..84785f7 100644 --- a/resources/views/table/_action-buttons.blade.php +++ b/resources/views/table/_view-action-buttons.blade.php @@ -9,21 +9,17 @@ diff --git a/resources/views/table/conf.blade.php b/resources/views/table/conf.blade.php index 032de80..757dec4 100644 --- a/resources/views/table/conf.blade.php +++ b/resources/views/table/conf.blade.php @@ -17,8 +17,7 @@

{{ $table->title }}

- + @icon(fa-table, sr:Back to Table) diff --git a/resources/views/table/propose/add-rows.blade.php b/resources/views/table/propose/add-rows.blade.php new file mode 100644 index 0000000..157dd53 --- /dev/null +++ b/resources/views/table/propose/add-rows.blade.php @@ -0,0 +1,6 @@ +@php($tab='add-rows') +@extends('table.propose.layout') + +@section('tab-content') + ... +@stop diff --git a/resources/views/table/propose/edit-rows.blade.php b/resources/views/table/propose/edit-rows.blade.php new file mode 100644 index 0000000..1ac4791 --- /dev/null +++ b/resources/views/table/propose/edit-rows.blade.php @@ -0,0 +1,43 @@ +@php + $tab = 'edit-rows'; + /** @var \App\Tables\Column[] $columns */ + /** @var \App\Tables\Changeset $changeset */ + /** @var \App\Models\Row[]|Illuminate\Pagination\Paginator $rows */ + /** @var \App\Models\Table $table */ +@endphp + +@extends('table.propose.layout') + +@section('tab-content') + + @if($rows->hasPages()) +
+ +
+ @endif + +
+ @php + $transformed = $rows->keyBy('_id')->map(function($r) use ($changeset) { + /** @var \App\Tables\Changeset $changeset */ + return $changeset->transformRow($r, true); + }); + @endphp + +
_id{{$col->name}} ("{{ $col->title }}") [ {{$col->id}} ]{{ $col->title }}
{{ $row->_id }}{{ $row->{$col->name} }}{{ $row->{$col->name} }}
+
+ + + @if($rows->hasPages()) +
+ +
+ @endif +@stop diff --git a/resources/views/table/propose/layout.blade.php b/resources/views/table/propose/layout.blade.php new file mode 100644 index 0000000..ee8c2af --- /dev/null +++ b/resources/views/table/propose/layout.blade.php @@ -0,0 +1,58 @@ +{{-- Basic table view --}} + +@extends('layouts.app') + +@php +/** @var \App\Models\Table $table */ + +if (!isset($tab) || $tab == '') $tab = 'edit-rows'; +@endphp + +@section('content') +
+
+ + {{ $table->owner->handle }}{{-- + --}}/{{-- + --}}{{ $table->name }} + + +

{{ $table->title }}

+ + @icon(fa-close, sr:Discard) + + @if(user()->ownsTable($table)) + + @icon(fa-save fa-pr)Commit + + @else + + @icon(fa-save fa-pr)Submit + + @endif +
+
+ + + +
+ @yield('tab-content') +
{{-- End of row --}} + +@endsection diff --git a/resources/views/table/propose/manage-columns.blade.php b/resources/views/table/propose/manage-columns.blade.php new file mode 100644 index 0000000..ca34a41 --- /dev/null +++ b/resources/views/table/propose/manage-columns.blade.php @@ -0,0 +1,6 @@ +@php($tab='manage-columns') +@extends('table.propose.layout') + +@section('tab-content') + ... +@stop diff --git a/resources/views/table/propose/review.blade.php b/resources/views/table/propose/review.blade.php new file mode 100644 index 0000000..11f5d7f --- /dev/null +++ b/resources/views/table/propose/review.blade.php @@ -0,0 +1,6 @@ +@php($tab='review') +@extends('table.propose.layout') + +@section('tab-content') + ... +@stop diff --git a/resources/views/table/view.blade.php b/resources/views/table/view.blade.php index ec2ae45..aa8cdf9 100644 --- a/resources/views/table/view.blade.php +++ b/resources/views/table/view.blade.php @@ -17,7 +17,7 @@

{{ $table->title }}

- @include('table._action-buttons') + @include('table._view-action-buttons') diff --git a/routes/web.php b/routes/web.php index a33779f..c68376e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -49,6 +49,9 @@ Route::group(['middleware' => ['auth', 'activated']], function () { Route::get('@{user}/{table}/settings', 'TableController@settings')->name('table.conf'); Route::post('@{user}/{table}/settings', 'TableController@storeSettings')->name('table.storeConf'); Route::post('@{user}/{table}/delete', 'TableController@delete')->name('table.delete'); + + Route::post('@{user}/{table}/draft/update', 'TableEditController@draftUpdate')->name('table.draft-update'); + Route::get('@{user}/{table}/draft/{tab?}', 'TableEditController@draft')->name('table.draft'); }); // Routes for all authed users From a4103e7084a816af77c90add1fef39ba375d40e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 5 Aug 2018 17:07:34 +0200 Subject: [PATCH 04/10] Row edit and delete card working --- app/Http/Controllers/Controller.php | 6 + app/Http/Controllers/TableEditController.php | 59 ++++++++- app/Tables/Changeset.php | 99 ++++++++++++-- app/Tables/Column.php | 29 ++-- porklib/helpers.php | 124 +++++++++++------- resources/assets/js/app.js | 73 +---------- resources/assets/js/base-setup.js | 52 ++++++++ resources/assets/js/bootstrap.js | 55 -------- resources/assets/js/components/RowEditor.vue | 38 +++++- resources/assets/js/{ => lib}/url-slug.js | 0 resources/assets/js/modules/block-collapse.js | 21 +++ resources/assets/js/modules/flash-messages.js | 10 ++ resources/assets/js/modules/form-autoalias.js | 19 +++ resources/assets/js/vue-init.js | 9 ++ .../views/table/propose/layout.blade.php | 10 +- 15 files changed, 394 insertions(+), 210 deletions(-) create mode 100644 resources/assets/js/base-setup.js delete mode 100644 resources/assets/js/bootstrap.js rename resources/assets/js/{ => lib}/url-slug.js (100%) create mode 100644 resources/assets/js/modules/block-collapse.js create mode 100644 resources/assets/js/modules/flash-messages.js create mode 100644 resources/assets/js/modules/form-autoalias.js create mode 100644 resources/assets/js/vue-init.js diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index e05d058..a19a7b0 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use Illuminate\Foundation\Bus\DispatchesJobs; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Routing\Controller as BaseController; use Illuminate\Foundation\Validation\ValidatesRequests; @@ -55,6 +56,11 @@ class Controller extends BaseController 'mastodon', // mastodon fetching previews ]; + protected function jsonResponse($data = [], $code=200) + { + return new JsonResponse($data, $code); + } + // Hacks to allow recursive nesting of validations in string and array format public function makeValidator($data, $rules, $messages = array(), $customAttributes = array()) diff --git a/app/Http/Controllers/TableEditController.php b/app/Http/Controllers/TableEditController.php index 9d4d484..0cb8d37 100644 --- a/app/Http/Controllers/TableEditController.php +++ b/app/Http/Controllers/TableEditController.php @@ -3,10 +3,12 @@ namespace App\Http\Controllers; +use App\Models\Row; use App\Models\Table; use App\Models\User; use App\Tables\Changeset; use App\Tables\Column; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Input; @@ -35,6 +37,14 @@ class TableEditController extends Controller }); } + private function storeChangeset(Changeset $chs) + { + $session_key = "proposal_{$chs->table->id}"; + + session()->put($session_key, $chs); + session()->save(); + } + public function draft(User $user, string $table, $tab = null) { /** @var Table $tableModel */ @@ -47,13 +57,18 @@ class TableEditController extends Controller $changeset = $this->getChangeset($tableModel); + if (Input::has('dump')) { + dd($changeset); + } + return $this->{camel_case($tab)}($changeset); } + /** @noinspection PhpUnusedPrivateMethodInspection */ private function editRows(Changeset $changeset) { $revision = $changeset->revision; - $columns = $changeset->transformColumns(); + $columns = $changeset->fetchAndTransformColumns(); $rows = $revision->rowsData($columns, true, false)->paginate(25, []); return view('table.propose.edit-rows', [ @@ -63,4 +78,46 @@ class TableEditController extends Controller 'rows' => $rows, ]); } + + // TODO other tab handlers + + public function draftUpdate(User $user, string $table, Request $request) + { + /** @var Table $tableModel */ + $tableModel = $user->tables()->where('name', $table)->first(); + if ($tableModel === null) abort(404, "No such table."); + + $changeset = $this->getChangeset($tableModel); + + $input = objBag($request->all(), false); + + $code = 200; + switch ($input->action) { + case 'row.update': + $data = (object)$input->data; + $changeset->rowUpdate($data); + + $resp = $changeset->fetchAndTransformRow($data->_id); + break; + + case 'row.remove': + $changeset->rowRemove($input->id); + $resp = $changeset->fetchAndTransformRow($input->id); + break; + + case 'row.restore': + $changeset->rowRestore($input->id); + $resp = $changeset->fetchAndTransformRow($input->id); + break; + + default: + $resp = "Bad Action"; + $code = 400; + break; + } + + $this->storeChangeset($changeset); + + return $this->jsonResponse($resp, $code); + } } diff --git a/app/Tables/Changeset.php b/app/Tables/Changeset.php index cc61ce0..e049af1 100644 --- a/app/Tables/Changeset.php +++ b/app/Tables/Changeset.php @@ -4,8 +4,11 @@ namespace App\Tables; use App\Models\Revision; +use App\Models\Row; use App\Models\Table; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Collection; +use MightyPork\Exceptions\NotExistException; use ReflectionClass; /** @@ -34,7 +37,7 @@ class Changeset /** * @var string - user's note attached to this changeset (future proposal) */ - public $note; + public $note = ''; /** * Rows whose content changed, identified by _id. @@ -100,9 +103,6 @@ class Changeset */ public $removedColumns = []; - /** @var Column[] - loaded and transformed columns, cached from previous call to transformColumns() */ - private $cachedColumns; - private function walkProps() { $properties = (new ReflectionClass($this))->getProperties(); @@ -147,7 +147,7 @@ class Changeset $object = new \stdClass(); foreach ($this->walkProps() as $prop) { - $object->$prop = array_values($this->$prop); + $object->$prop = $this->$prop; } return $object; @@ -178,6 +178,8 @@ class Changeset */ public function transformRow($row, $decorate) { + if ($row instanceof Row) $row = (object)$row->getAttributes(); + if ($decorate) { $row->_remove = false; $row->_changed = []; @@ -222,16 +224,16 @@ class Changeset * * @return Column[] */ - public function transformColumns() + public function fetchAndTransformColumns() { - if ($this->cachedColumns) return $this->cachedColumns; + /** @var Column[] - loaded and transformed columns, cached from previous call to transformColumns() */ + static $cachedColumns = []; + + if ($cachedColumns) return $cachedColumns; $columns = Column::columnsFromJson($this->revision->columns); // Modify columns - $byId = []; foreach ($columns as $column) { - $byId[$column->id] = $column; - if (isset($this->columnUpdates[$column->id])) { $column->modifyByChangeset($this->columnUpdates[$column->id]); } @@ -248,16 +250,85 @@ class Changeset } // Reorder + $colsById = collect($columns)->keyBy('id')->all(); $newOrder = []; foreach ($this->columnOrder as $id) { - $newOrder[] = $byId[$id]; + $newOrder[] = $colsById[$id]; } - $leftover_keys = array_diff(array_keys($byId), $this->columnOrder); + $leftover_keys = array_diff(array_keys($colsById), $this->columnOrder); foreach ($leftover_keys as $id) { - $newOrder[] = $byId[$id]; + $newOrder[] = $colsById[$id]; + } + + return $cachedColumns = $newOrder; + } + + public function rowRemove(int $id) + { + $this->removedRows[] = $id; + } + + public function rowRestore(int $id) + { + $this->removedRows = array_diff($this->removedRows, [$id]); + } + + public function fetchAndTransformRow(int $id) + { + $r = $this->fetchRow($id); + $transformed = $this->transformRow($r, true); + return $transformed; + } + + public function fetchColumns() + { + return Column::columnsFromJson($this->revision->columns); + } + + public function fetchRow($id) + { + $r = $this->revision->rowsData($this->fetchColumns(), true, false) + ->whereRaw("data->>'_id' = ?", $id)->first(); + + if (! $r) throw new NotExistException("No such row _id = $id in this revision."); + + // remove junk + unset($r->pivot_revision_id); + unset($r->pivot_row_id); + + return (object)$r->getAttributes(); + } + + public function rowUpdate($newVals) + { + $_id = $newVals->_id; + $origRow = $this->fetchRow($_id); + + /** @var Column[]|Collection $cols */ + $cols = collect($this->fetchAndTransformColumns())->keyBy('id'); + + $updateObj = []; + \Debugbar::addMessage(json_encode($cols)); + \Debugbar::addMessage(var_export($newVals, true)); + + foreach ($newVals as $colId => $value) { + if (starts_with($colId, '_')) continue; // internals + + if (!isset($origRow->$colId) || $value != $origRow->$colId) { + $updateObj[$colId] = $cols[$colId]->cast($value); + } } - return $this->cachedColumns = $newOrder; + \Debugbar::addMessage("New: ".json_encode($newVals)); + \Debugbar::addMessage("Orig: ".json_encode($origRow)); + \Debugbar::addMessage("UpdateObj: ".json_encode($updateObj)); + + if (!empty($updateObj)) { + $this->rowUpdates[$_id] = $updateObj; + } else { + // remove possible old update record for this row, if nothing changes now + unset($this->rowUpdates[$_id]); + } } } diff --git a/app/Tables/Column.php b/app/Tables/Column.php index 1434153..3741e1a 100644 --- a/app/Tables/Column.php +++ b/app/Tables/Column.php @@ -10,13 +10,6 @@ use MightyPork\Utils\Utils; /** * Helper class representing one column in a data table. * - * @property-read string $id - * @property-read string $type - * @property-read string $name - * @property-read string $title - * @property-read bool $isNew - * @property-read bool $toRemove - * * @property-read bool $id_modified * @property-read bool $type_modified * @property-read bool $name_modified @@ -33,16 +26,18 @@ class Column implements JsonSerializable, Arrayable 'int', 'bool', 'float', 'string' ]; - private $id; - private $type; - private $name; - private $title; + // marked public to make keyBy() work + + public $id; + public $type; + public $name; + public $title; /** @var bool - column is marked to be deleted by a changeset */ - private $toRemove = false; + public $toRemove = false; /** @var bool - column is new in this changeset */ - private $isNew = false; + public $isNew = false; /** @var array - original attrib values if edited in a changeset */ private $orig_attribs = []; @@ -83,10 +78,6 @@ class Column implements JsonSerializable, Arrayable public function __get($name) { - if (property_exists($this, $name)) { - return $this->$name; - } - if (ends_with($name, '_modified')) { $basename = str_replace('_modified', '', $name); if (property_exists($this, $basename)) { @@ -177,20 +168,24 @@ class Column implements JsonSerializable, Arrayable { switch ($this->type) { case 'int': + if (is_null($value)) return 0; if (is_int($value)) return $value; if (is_float($value)) return round($value); if (is_numeric($value)) return intval($value); throw new NotApplicableException("Could not convert value \"$value\" to int!"); case 'float': + if (is_null($value)) return 0; if (is_int($value) || is_float($value)) return (float)$value; if (is_numeric($value)) return floatval($value); throw new NotApplicableException("Could not convert value \"$value\" to float!"); case 'bool': + if (is_null($value)) return false; return Utils::parseBool($value); case 'string': + if (is_null($value)) return ""; return "$value"; default: diff --git a/porklib/helpers.php b/porklib/helpers.php index 8134d80..77368bb 100644 --- a/porklib/helpers.php +++ b/porklib/helpers.php @@ -65,29 +65,45 @@ function unless($cond, $then, $else = '') * - Undefined keys are returned as null. * - array and object values are wrapped in objBag when returned. */ -class objBag implements JsonSerializable, ArrayAccess { +class objBag implements JsonSerializable, ArrayAccess, \Illuminate\Contracts\Support\Arrayable +{ /** @var object */ - private $wrapped; + private $wrapped; + private $recursive; + + /** + * objBag constructor. + * @param mixed $wrapped - wrapped object/array/class + * @param bool $recursive - return array/object values as objBags too + */ + public function __construct($wrapped, $recursive = true) + { + $this->wrapped = (object)$wrapped; + $this->recursive = $recursive; + } - public function __construct($wrapped) - { - $this->wrapped = (object)$wrapped; - } + /** + * @param $name + * @return null|objBag|mixed + */ + public function __get($name) + { + if ($this->wrapped) { + if (isset($this->wrapped->$name)) { + $x = $this->wrapped->$name; - public function __get($name) - { - if ($this->wrapped) { - if (isset($this->wrapped->$name)) { - $x = $this->wrapped->$name; - if (is_array($x) || is_object($x)) return objBag($x); - return $x; - } - } - - return null; - } + if ($this->recursive && (is_array($x) || is_object($x))) { + return objBag($x); + } - public function __set($name, $value) + return $x; + } + } + + return null; + } + + public function __set($name, $value) { if ($this->wrapped) { $this->wrapped->$name = $value; @@ -102,48 +118,53 @@ class objBag implements JsonSerializable, ArrayAccess { } public function __isset($name) - { - return isset($this->wrapped->$name); - } + { + return isset($this->wrapped->$name); + } - public function get($name, $def = null) - { - if (!isset($this->$name)) return $def; - return $this->$name; - } + /** + * @param $name + * @param null $def + * @return null|array|mixed + */ + public function get($name, $def = null) + { + if (!isset($this->$name)) return $def; + return $this->$name; + } - public function has($name) - { - return isset($this->$name) && $this->$name !== null; - } + public function has($name) + { + return isset($this->$name) && $this->$name !== null; + } - public function unpack() - { - return $this->wrapped; - } + public function unpack() + { + return $this->wrapped; + } public function toArray() { - return(array)$this->wrapped; - } + return (array)$this->wrapped; + } public function all() { return $this->toArray(); } - /** - * Specify data which should be serialized to JSON - * - * @link http://php.net/manual/en/jsonserializable.jsonserialize.php - * @return mixed data which can be serialized by json_encode, - * which is a value of any type other than a resource. - * @since 5.4.0 - */ - public function jsonSerialize() - { - return $this->wrapped; - } + /** + * Specify data which should be serialized to JSON + * + * @link http://php.net/manual/en/jsonserializable.jsonserialize.php + * @return mixed data which can be serialized by json_encode, + * which is a value of any type other than a resource. + * @since 5.4.0 + */ + public function jsonSerialize() + { + return $this->wrapped; + } public function offsetExists($offset) { @@ -168,10 +189,11 @@ class objBag implements JsonSerializable, ArrayAccess { /** * @param $obj + * @param bool $recursive - whether the bag should be recursive * @return objBag */ -function objBag($obj) { - return new \objBag($obj); +function objBag($obj, $recursive=true) { + return new \objBag($obj, $recursive); } /** diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index 22eb7a3..628b4a9 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -1,76 +1,17 @@ -/** - * First we will load all of this project's JavaScript dependencies which - * includes Vue and other libraries. It is a great starting point when - * building robust, powerful web applications using Vue and Laravel. - */ +/* Project entrypoint */ -require('./bootstrap') - -let url_slug = require('./url-slug') +require('./base-setup') +require('./modules/block-collapse') +require('./modules/flash-messages') +require('./modules/form-autoalias') +require('./vue-init') $(function () { // Remove all noscript from forms etc $('noscript').remove(); + // Bootstrap tooltips $('[data-toggle="tooltip"]').tooltip({ container: 'body' }) - - // auto hide flash alerts - let $notifs = $('div.alert').not('.alert-important').addClass('fadeout') - setTimeout(() => { - $notifs.addClass('fade') - setTimeout(() => { - $notifs.addClass('hidden') - }, 500) - }, 2500) - - // toggle collapse when clicked outside link, without drag - $('.block-collapse') - .on('mousedown', (e) => { - let $bc = $(e.target).closest('.block-collapse') - $bc.data('mx', e.screenX); - $bc.data('my', e.screenY); - }) - .on('mouseup', (e) => { - if (e.target.nodeName === 'A') return - let $bc = $(e.target).closest('.block-collapse') - if (typeof $bc.data('mx') !== 'undefined') { - let x0 = +$bc.data('mx'); - let y0 = +$bc.data('my'); - if (Math.abs(x0 - e.screenX) > 5 || Math.abs(y0 - e.screenY) > 5) { - // drag - } else { - $(e.target).closest('.block-collapse').toggleClass('reveal') - } - } - }) }) - -// auto-alias -$(document).on('input keypress paste keyup', 'input[data-autoalias]', function () { - const $this = $(this) - const target_name = $this.data('autoalias') - const delimiter = $this.data('aa-delimiter') || '_' - - const new_alias = url_slug($this.val(), {'delimiter': delimiter}) - - const $target = $(`input[name="${target_name}"]`) - const lastset = $target.data('aa-last-set-val') - - // 1. pick up, or 2. continue - if (new_alias === $target.val() || lastset === $target.val()) { - $target.val(new_alias) - $target.data('aa-last-set-val', new_alias) - } -}) - -window.Vue = require('vue'); - -Vue.component('column-editor', require('./components/ColumnEditor.vue')); -Vue.component('row-editor', require('./components/RowEditor.vue')); -Vue.component('v-icon', require('./components/Icon.vue')); - -const app = new Vue({ - el: '#app' -}); diff --git a/resources/assets/js/base-setup.js b/resources/assets/js/base-setup.js new file mode 100644 index 0000000..3bd82f6 --- /dev/null +++ b/resources/assets/js/base-setup.js @@ -0,0 +1,52 @@ +// window._ = require('lodash'); +window.Popper = require('popper.js').default + +/** + * We'll load jQuery and the Bootstrap jQuery plugin which provides support + * for JavaScript based Bootstrap features such as modals and tabs. This + * code may be modified to fit the specific needs of your application. + */ + +window.$ = window.jQuery = require('jquery') +require('bootstrap') + +/** + * We'll load the axios HTTP library which allows us to easily issue requests + * to our Laravel back-end. This library automatically handles sending the + * CSRF token as a header based on the value of the "XSRF" token cookie. + */ + +window.axios = require('axios') + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest' + +/** + * Next we will register the CSRF Token as a common header with Axios so that + * all outgoing HTTP requests automatically have it attached. This is just + * a simple convenience so we don't have to attach every token manually. + */ + +let token = document.head.querySelector('meta[name="csrf-token"]') + +if (token) { + window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content +} else { + console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token') +} + +/** + * Echo exposes an expressive API for subscribing to channels and listening + * for events that are broadcast by Laravel. Echo and event broadcasting + * allows your team to easily build robust real-time web applications. + */ + +// import Echo from 'laravel-echo' + +// window.Pusher = require('pusher-js'); + +// window.Echo = new Echo({ +// broadcaster: 'pusher', +// key: process.env.MIX_PUSHER_APP_KEY, +// cluster: process.env.MIX_PUSHER_APP_CLUSTER, +// encrypted: true +// }); diff --git a/resources/assets/js/bootstrap.js b/resources/assets/js/bootstrap.js deleted file mode 100644 index c71b30a..0000000 --- a/resources/assets/js/bootstrap.js +++ /dev/null @@ -1,55 +0,0 @@ - -// window._ = require('lodash'); -window.Popper = require('popper.js').default; - -/** - * We'll load jQuery and the Bootstrap jQuery plugin which provides support - * for JavaScript based Bootstrap features such as modals and tabs. This - * code may be modified to fit the specific needs of your application. - */ - -try { - window.$ = window.jQuery = require('jquery'); - require('bootstrap'); -} catch (e) {} - -// /** -// * We'll load the axios HTTP library which allows us to easily issue requests -// * to our Laravel back-end. This library automatically handles sending the -// * CSRF token as a header based on the value of the "XSRF" token cookie. -// */ -// -// window.axios = require('axios'); -// -// window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; -// -// /** -// * Next we will register the CSRF Token as a common header with Axios so that -// * all outgoing HTTP requests automatically have it attached. This is just -// * a simple convenience so we don't have to attach every token manually. -// */ -// -// let token = document.head.querySelector('meta[name="csrf-token"]'); -// -// if (token) { -// window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; -// } else { -// console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token'); -// } - -/** - * Echo exposes an expressive API for subscribing to channels and listening - * for events that are broadcast by Laravel. Echo and event broadcasting - * allows your team to easily build robust real-time web applications. - */ - -// import Echo from 'laravel-echo' - -// window.Pusher = require('pusher-js'); - -// window.Echo = new Echo({ -// broadcaster: 'pusher', -// key: process.env.MIX_PUSHER_APP_KEY, -// cluster: process.env.MIX_PUSHER_APP_CLUSTER, -// encrypted: true -// }); diff --git a/resources/assets/js/components/RowEditor.vue b/resources/assets/js/components/RowEditor.vue index 880851e..0142e72 100644 --- a/resources/assets/js/components/RowEditor.vue +++ b/resources/assets/js/components/RowEditor.vue @@ -17,7 +17,7 @@ - @@ -59,11 +59,43 @@ export default { } }, methods: { + busy (yes) { + $('#draft-busy').css('opacity', yes ? 1 : 0) + }, + query (data, sucfn) { + this.busy(true) + if (!sucfn) sucfn = ()=>{} + window.axios.post(this.route, data).then(sucfn).catch((e) => { + console.error(e) + }).then(() => { + this.busy(false) + }) + }, toggleRowDelete(_id) { - this.$set(this.rows[_id], '_remove', !this.rows[_id]._remove); + let remove = !this.rows[_id]._remove; + + this.query({ + action: remove ? 'row.remove' : 'row.restore', + id: _id + }, (resp) => { + this.$set(this.rows, _id, resp.data); + }) }, toggleRowEditing(_id) { - this.$set(this.rows[_id], '_editing', !this.rows[_id]._editing); + if (this.rows[_id]._remove) return false; // can't edit row marked for removal + + let editing = !this.rows[_id]._editing; + + if (!editing) { + this.query({ + action: 'row.update', + data: this.rows[_id] + }, (resp) => { + this.$set(this.rows, _id, resp.data); + }) + } else { + this.$set(this.rows[_id], '_editing', true); + } }, colClasses(col) { return [ diff --git a/resources/assets/js/url-slug.js b/resources/assets/js/lib/url-slug.js similarity index 100% rename from resources/assets/js/url-slug.js rename to resources/assets/js/lib/url-slug.js diff --git a/resources/assets/js/modules/block-collapse.js b/resources/assets/js/modules/block-collapse.js new file mode 100644 index 0000000..27ff883 --- /dev/null +++ b/resources/assets/js/modules/block-collapse.js @@ -0,0 +1,21 @@ +// toggle collapse when clicked outside link, without drag +$(document) + .on('mousedown', '.block-collapse', function(e) { + let $bc = $(e.target).closest('.block-collapse') + $bc.data('mx', e.screenX) + $bc.data('my', e.screenY) + }) + .on('mouseup', '.block-collapse', function(e) { + if (e.target.nodeName === 'A') return + let $bc = $(e.target).closest('.block-collapse') + + if (typeof $bc.data('mx') !== 'undefined') { + let x0 = +$bc.data('mx') + let y0 = +$bc.data('my') + if (Math.abs(x0 - e.screenX) > 5 || Math.abs(y0 - e.screenY) > 5) { + // drag + } else { + $(e.target).closest('.block-collapse').toggleClass('reveal') + } + } + }) diff --git a/resources/assets/js/modules/flash-messages.js b/resources/assets/js/modules/flash-messages.js new file mode 100644 index 0000000..6ab01e0 --- /dev/null +++ b/resources/assets/js/modules/flash-messages.js @@ -0,0 +1,10 @@ +$(function() { + // auto hide flash alerts + let $notifs = $('div.alert').not('.alert-important').addClass('fadeout') + setTimeout(() => { + $notifs.addClass('fade') + setTimeout(() => { + $notifs.addClass('hidden') + }, 500) + }, 2500) +}) diff --git a/resources/assets/js/modules/form-autoalias.js b/resources/assets/js/modules/form-autoalias.js new file mode 100644 index 0000000..fcb4bf9 --- /dev/null +++ b/resources/assets/js/modules/form-autoalias.js @@ -0,0 +1,19 @@ +let url_slug = require('../lib/url-slug') + +// auto-alias +$(document).on('input keypress paste keyup', 'input[data-autoalias]', function () { + const $this = $(this) + const target_name = $this.data('autoalias') + const delimiter = $this.data('aa-delimiter') || '_' + + const new_alias = url_slug($this.val(), {'delimiter': delimiter}) + + const $target = $(`input[name="${target_name}"]`) + const lastset = $target.data('aa-last-set-val') + + // 1. pick up, or 2. continue + if (new_alias === $target.val() || lastset === $target.val()) { + $target.val(new_alias) + $target.data('aa-last-set-val', new_alias) + } +}) diff --git a/resources/assets/js/vue-init.js b/resources/assets/js/vue-init.js new file mode 100644 index 0000000..5b51bf6 --- /dev/null +++ b/resources/assets/js/vue-init.js @@ -0,0 +1,9 @@ +window.Vue = require('vue'); + +Vue.component('column-editor', require('./components/ColumnEditor.vue')); +Vue.component('row-editor', require('./components/RowEditor.vue')); +Vue.component('v-icon', require('./components/Icon.vue')); + +const app = new Vue({ + el: '#app' +}); diff --git a/resources/views/table/propose/layout.blade.php b/resources/views/table/propose/layout.blade.php index ee8c2af..c2cd99e 100644 --- a/resources/views/table/propose/layout.blade.php +++ b/resources/views/table/propose/layout.blade.php @@ -14,12 +14,16 @@ if (!isset($tab) || $tab == '') $tab = 'edit-rows'; {{ $table->owner->handle }}{{-- --}}/{{-- - --}}{{ $table->name }} + --}} + {{ $table->name }} + -

{{ $table->title }}

+

+ {{ $table->title }} +

- @icon(fa-close, sr:Discard) + @icon(fa-trash-o, sr:Discard) @if(user()->ownsTable($table)) From 06fb01d146e5507d6cf243890ac73184cef91bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 5 Aug 2018 17:59:32 +0200 Subject: [PATCH 05/10] row undos --- app/Http/Controllers/TableEditController.php | 14 +++++- app/Tables/Changeset.php | 47 +++++++++++++------- app/helpers.php | 25 +++++++++++ resources/assets/js/base-setup.js | 2 +- resources/assets/js/components/Icon.vue | 3 +- resources/assets/js/components/RowEditor.vue | 36 ++++++++++----- resources/assets/sass/_helpers.scss | 4 ++ 7 files changed, 102 insertions(+), 29 deletions(-) diff --git a/app/Http/Controllers/TableEditController.php b/app/Http/Controllers/TableEditController.php index 0cb8d37..f546b47 100644 --- a/app/Http/Controllers/TableEditController.php +++ b/app/Http/Controllers/TableEditController.php @@ -79,7 +79,19 @@ class TableEditController extends Controller ]); } - // TODO other tab handlers + /** @noinspection PhpUnusedPrivateMethodInspection */ + private function addRows(Changeset $changeset) + { + $columns = $changeset->fetchAndTransformColumns(); + $rows = $changeset->getAddedRows(25); + + return view('table.propose.add-rows', [ + 'changeset' => $changeset, + 'table' => $changeset->table, + 'columns' => collect($columns), + 'rows' => $rows, + ]); + } public function draftUpdate(User $user, string $table, Request $request) { diff --git a/app/Tables/Changeset.php b/app/Tables/Changeset.php index e049af1..2b33ad9 100644 --- a/app/Tables/Changeset.php +++ b/app/Tables/Changeset.php @@ -6,6 +6,7 @@ namespace App\Tables; use App\Models\Revision; use App\Models\Row; use App\Models\Table; +use Illuminate\Pagination\Paginator; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Collection; use MightyPork\Exceptions\NotExistException; @@ -186,7 +187,7 @@ class Changeset $row->_orig = []; } - // Removed rows + // Removed rows - return as null if (in_array($row->_id, $this->removedRows)) { if ($decorate) { $row->_remove = true; @@ -195,16 +196,19 @@ class Changeset } } - // Changed values - if (isset($this->rowUpdates[$row->_id])) { - $newVals = $this->rowUpdates[$row->_id]; + // if marked for removal, hide changes + if (!$row->_remove) { + // Changed values + if (isset($this->rowUpdates[$row->_id])) { + $newVals = $this->rowUpdates[$row->_id]; - if ($decorate) { - $row->_changed = array_keys($newVals); - $row->_orig = array_only((array)$row, $row->_changed); - } + if ($decorate) { + $row->_changed = array_keys($newVals); + $row->_orig = array_only((array)$row, $row->_changed); + } - $row = (object)array_merge((array)$row, $newVals); + $row = (object)array_merge((array)$row, $newVals); + } } // Drop deleted columns @@ -300,8 +304,16 @@ class Changeset return (object)$r->getAttributes(); } + /** + * Apply a row update, adding the row to the list of changes, or removing it + * if all differences were undone. + * + * @param array|object $newVals + */ public function rowUpdate($newVals) { + $newVals = (object)$newVals; + $_id = $newVals->_id; $origRow = $this->fetchRow($_id); @@ -309,8 +321,6 @@ class Changeset $cols = collect($this->fetchAndTransformColumns())->keyBy('id'); $updateObj = []; - \Debugbar::addMessage(json_encode($cols)); - \Debugbar::addMessage(var_export($newVals, true)); foreach ($newVals as $colId => $value) { if (starts_with($colId, '_')) continue; // internals @@ -320,10 +330,6 @@ class Changeset } } - \Debugbar::addMessage("New: ".json_encode($newVals)); - \Debugbar::addMessage("Orig: ".json_encode($origRow)); - \Debugbar::addMessage("UpdateObj: ".json_encode($updateObj)); - if (!empty($updateObj)) { $this->rowUpdates[$_id] = $updateObj; } else { @@ -331,4 +337,15 @@ class Changeset unset($this->rowUpdates[$_id]); } } + + /** + * Get a page of added rows for display in the editor + * + * @param int $perPage + * @return \Illuminate\Pagination\LengthAwarePaginator|Collection|array + */ + public function getAddedRows(int $perPage = 25) + { + return collection_paginate($this->newRows, $perPage); + } } diff --git a/app/helpers.php b/app/helpers.php index b0159ee..b07fe11 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -95,3 +95,28 @@ function toJSON($object) { function fromJSON($object, $assoc=false) { return \GuzzleHttp\json_decode($object, $assoc); } + +/** + * @param \Illuminate\Support\Collection|array $items + * @param int $per_page + * @param null|\Closure $mapFn + * @return \Illuminate\Pagination\LengthAwarePaginator + */ +function collection_paginate($items, $per_page, $mapFn = null) +{ + if (!$items instanceof \Illuminate\Support\Collection) { + $items = collect($items); + } + + $page = Request::get('page', 1); + + $pageItems = $items->forPage($page, $per_page)->values(); + + return new Illuminate\Pagination\LengthAwarePaginator( + $mapFn ? $pageItems->map($mapFn) : $pageItems, + $items->count(), + $per_page, + Illuminate\Pagination\Paginator::resolveCurrentPage(), + ['path' => Illuminate\Pagination\Paginator::resolveCurrentPath()] + ); +} diff --git a/resources/assets/js/base-setup.js b/resources/assets/js/base-setup.js index 3bd82f6..4f30037 100644 --- a/resources/assets/js/base-setup.js +++ b/resources/assets/js/base-setup.js @@ -1,4 +1,4 @@ -// window._ = require('lodash'); +//window._ = require('lodash'); window.Popper = require('popper.js').default /** diff --git a/resources/assets/js/components/Icon.vue b/resources/assets/js/components/Icon.vue index d69f391..f4950b2 100644 --- a/resources/assets/js/components/Icon.vue +++ b/resources/assets/js/components/Icon.vue @@ -1,12 +1,11 @@ diff --git a/resources/assets/sass/_helpers.scss b/resources/assets/sass/_helpers.scss index c18f61c..c9acd97 100644 --- a/resources/assets/sass/_helpers.scss +++ b/resources/assets/sass/_helpers.scss @@ -25,3 +25,7 @@ .opacity-fade { transition: opacity .3s ease-in-out; } + +.pointer { + cursor: pointer; +} From 6cfd5aa7e9a8cc51f42be47e04fe5c4c434af8e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 5 Aug 2018 18:45:11 +0200 Subject: [PATCH 06/10] add some validation to row edit, better handle setting null cols with empty --- app/Exceptions/Handler.php | 3 +- app/Exceptions/ViewException.php | 14 ---- app/Http/Controllers/TableEditController.php | 77 +++++++++++-------- app/Models/Table.php | 7 ++ app/Tables/Changeset.php | 8 +- app/Tables/Column.php | 5 +- .../Exceptions/SimpleValidationException.php | 46 +++++++++++ resources/assets/js/base-setup.js | 2 +- resources/assets/js/components/RowEditor.vue | 22 ++++-- .../views/table/propose/edit-rows.blade.php | 48 ++++-------- .../propose/layout-row-pagination.blade.php | 27 +++++++ .../views/table/propose/layout.blade.php | 2 +- routes/web.php | 1 + 13 files changed, 168 insertions(+), 94 deletions(-) delete mode 100644 app/Exceptions/ViewException.php create mode 100644 porklib/Exceptions/SimpleValidationException.php create mode 100644 resources/views/table/propose/layout-row-pagination.blade.php diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 6de4f15..8481b6a 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -6,6 +6,7 @@ use Exception; use Illuminate\Auth\AuthenticationException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Validation\ValidationException; +use MightyPork\Exceptions\Exceptions\SimpleValidationException; class Handler extends ExceptionHandler { @@ -15,7 +16,7 @@ class Handler extends ExceptionHandler * @var array */ protected $dontReport = [ - // + SimpleValidationException::class, ]; /** diff --git a/app/Exceptions/ViewException.php b/app/Exceptions/ViewException.php deleted file mode 100644 index 5c21332..0000000 --- a/app/Exceptions/ViewException.php +++ /dev/null @@ -1,14 +0,0 @@ -captured = $captured; - parent::__construct($cause); - } -} diff --git a/app/Http/Controllers/TableEditController.php b/app/Http/Controllers/TableEditController.php index f546b47..16ef366 100644 --- a/app/Http/Controllers/TableEditController.php +++ b/app/Http/Controllers/TableEditController.php @@ -3,14 +3,12 @@ namespace App\Http\Controllers; -use App\Models\Row; use App\Models\Table; use App\Models\User; use App\Tables\Changeset; -use App\Tables\Column; -use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Input; +use MightyPork\Exceptions\SimpleValidationException; class TableEditController extends Controller { @@ -22,14 +20,12 @@ class TableEditController extends Controller */ private function getChangeset(Table $table) { - $session_key = "proposal_{$table->id}"; - if (Input::has('reset')) { - session()->forget($session_key); + session()->forget($table->draftSessionKey); } /** @var Changeset $changeset */ - return session()->remember($session_key, function () use ($table) { + return session()->remember($table->draftSessionKey, function () use ($table) { $changeset = new Changeset(); $changeset->table = $table; $changeset->revision = $table->revision; @@ -39,10 +35,21 @@ class TableEditController extends Controller 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); - session()->save(); + return redirect($tableModel->viewRoute); } public function draft(User $user, string $table, $tab = null) @@ -103,29 +110,33 @@ class TableEditController extends Controller $input = objBag($request->all(), false); - $code = 200; - switch ($input->action) { - case 'row.update': - $data = (object)$input->data; - $changeset->rowUpdate($data); - - $resp = $changeset->fetchAndTransformRow($data->_id); - break; - - case 'row.remove': - $changeset->rowRemove($input->id); - $resp = $changeset->fetchAndTransformRow($input->id); - break; - - case 'row.restore': - $changeset->rowRestore($input->id); - $resp = $changeset->fetchAndTransformRow($input->id); - break; - - default: - $resp = "Bad Action"; - $code = 400; - break; + try { + $code = 200; + switch ($input->action) { + case 'row.update': + $data = (object)$input->data; + $changeset->rowUpdate($data); + + $resp = $changeset->fetchAndTransformRow($data->_id); + break; + + case 'row.remove': + $changeset->rowRemove($input->id); + $resp = $changeset->fetchAndTransformRow($input->id); + break; + + case 'row.restore': + $changeset->rowRestore($input->id); + $resp = $changeset->fetchAndTransformRow($input->id); + break; + + default: + $resp = "Bad Action"; + $code = 400; + break; + } + } catch (SimpleValidationException $e) { + return $this->jsonResponse(['errors' => $e->getMessageBag()->getMessages()], 400); } $this->storeChangeset($changeset); diff --git a/app/Models/Table.php b/app/Models/Table.php index f5c351b..381cd3f 100644 --- a/app/Models/Table.php +++ b/app/Models/Table.php @@ -25,6 +25,9 @@ use Illuminate\Database\Eloquent\Collection; * @property-read string $draftRoute * @property-read string $settingsRoute * @property-read string $deleteRoute + * @property-read string $draftSessionKey + * @property-read string $draftDiscardRoute + * @property-read string $draftUpdateRoute * @property-read User $owner * @property-read Table $parentTable * @property-read Table[]|Collection $forks @@ -136,9 +139,13 @@ class Table extends BaseModel case 'settingsRoute': return route('table.conf', $arg); case 'draftRoute': return route('table.draft', $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); } diff --git a/app/Tables/Changeset.php b/app/Tables/Changeset.php index 2b33ad9..8ba6d96 100644 --- a/app/Tables/Changeset.php +++ b/app/Tables/Changeset.php @@ -325,8 +325,12 @@ class Changeset foreach ($newVals as $colId => $value) { if (starts_with($colId, '_')) continue; // internals - if (!isset($origRow->$colId) || $value != $origRow->$colId) { - $updateObj[$colId] = $cols[$colId]->cast($value); + $col = $cols[$colId]; + $value = $col->cast($value); + $origValue = $col->cast(isset($origRow->$colId) ? $origRow->$colId : null); + + if ($value !== $origValue) { + $updateObj[$colId] = $value; } } diff --git a/app/Tables/Column.php b/app/Tables/Column.php index 3741e1a..d0fb9e4 100644 --- a/app/Tables/Column.php +++ b/app/Tables/Column.php @@ -4,6 +4,7 @@ namespace App\Tables; use Illuminate\Contracts\Support\Arrayable; use JsonSerializable; +use MightyPork\Exceptions\SimpleValidationException; use MightyPork\Exceptions\NotApplicableException; use MightyPork\Utils\Utils; @@ -172,13 +173,13 @@ class Column implements JsonSerializable, Arrayable if (is_int($value)) return $value; if (is_float($value)) return round($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': if (is_null($value)) return 0; if (is_int($value) || is_float($value)) return (float)$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': if (is_null($value)) return false; diff --git a/porklib/Exceptions/SimpleValidationException.php b/porklib/Exceptions/SimpleValidationException.php new file mode 100644 index 0000000..9e51f9e --- /dev/null +++ b/porklib/Exceptions/SimpleValidationException.php @@ -0,0 +1,46 @@ +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; + } +} diff --git a/resources/assets/js/base-setup.js b/resources/assets/js/base-setup.js index 4f30037..4379bb3 100644 --- a/resources/assets/js/base-setup.js +++ b/resources/assets/js/base-setup.js @@ -1,4 +1,4 @@ -//window._ = require('lodash'); +window._ = require('lodash'); window.Popper = require('popper.js').default /** diff --git a/resources/assets/js/components/RowEditor.vue b/resources/assets/js/components/RowEditor.vue index 19a115b..10dd476 100644 --- a/resources/assets/js/components/RowEditor.vue +++ b/resources/assets/js/components/RowEditor.vue @@ -26,14 +26,16 @@