stub of the row editor
This commit is contained in:
@@ -55,35 +55,6 @@ class TableController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function draftChange(Request $request, User $user, string $table)
|
||||
{
|
||||
/** @var Table $tableModel */
|
||||
$tableModel = $user->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 */
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Table;
|
||||
use App\Models\User;
|
||||
use App\Tables\Changeset;
|
||||
use App\Tables\Column;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Input;
|
||||
|
||||
class TableEditController extends Controller
|
||||
{
|
||||
/**
|
||||
* Initialize the session-stored changeset, if not set yet
|
||||
*
|
||||
* @param Table $table
|
||||
* @return Changeset
|
||||
*/
|
||||
private function getChangeset(Table $table)
|
||||
{
|
||||
$session_key = "proposal_{$table->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
+1
-1
@@ -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
|
||||
{
|
||||
|
||||
+22
-9
@@ -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 (ends_with($name, 'Route')) {
|
||||
$arg = [
|
||||
'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]);
|
||||
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')
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+114
-10
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+84
-12
@@ -1,9 +1,8 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Tables;
|
||||
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use JsonSerializable;
|
||||
use MightyPork\Exceptions\NotApplicableException;
|
||||
use MightyPork\Utils\Utils;
|
||||
@@ -15,8 +14,20 @@ use MightyPork\Utils\Utils;
|
||||
* @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
|
||||
* @property-read bool $title_modified
|
||||
*
|
||||
* @property-read string $id_orig
|
||||
* @property-read string $type_orig
|
||||
* @property-read string $name_orig
|
||||
* @property-read string $title_orig
|
||||
*/
|
||||
class Column implements JsonSerializable
|
||||
class Column implements JsonSerializable, Arrayable
|
||||
{
|
||||
const colTypes = [
|
||||
'int', 'bool', 'float', 'string'
|
||||
@@ -27,6 +38,76 @@ class Column implements JsonSerializable
|
||||
private $name;
|
||||
private $title;
|
||||
|
||||
/** @var bool - column is marked to be deleted by a changeset */
|
||||
private $toRemove = false;
|
||||
|
||||
/** @var bool - column is new in this changeset */
|
||||
private $isNew = false;
|
||||
|
||||
/** @var array - original attrib values if edited in a changeset */
|
||||
private $orig_attribs = [];
|
||||
/** @var array - list of attrib names that are modified by a changeset */
|
||||
private $modified_attribs = [];
|
||||
|
||||
/**
|
||||
* Mark for removal (used in editing GUI)
|
||||
*/
|
||||
public function markForRemoval()
|
||||
{
|
||||
$this->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}
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user