commit
84103d6001
@ -0,0 +1,12 @@ |
|||||||
|
.PHONY: build dev watch prod ana |
||||||
|
|
||||||
|
build: dev |
||||||
|
|
||||||
|
dev: |
||||||
|
npm run dev
|
||||||
|
watch: |
||||||
|
npm run watch
|
||||||
|
ana: |
||||||
|
npm run dev-analyze
|
||||||
|
prod: |
||||||
|
npm run prod
|
@ -0,0 +1,18 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
/** |
||||||
|
* Interface RowData |
||||||
|
* |
||||||
|
* @property int $_id |
||||||
|
*/ |
||||||
|
interface RowData {} |
||||||
|
|
||||||
|
/** |
||||||
|
* Interface DecoratedRow |
||||||
|
* |
||||||
|
* @property bool $_new - row is new in the changeset |
||||||
|
* @property bool $_remove - marked to be removed |
||||||
|
* @property mixed[] $_orig - original values before transformation, key by CID |
||||||
|
* @property string[] $_changed - values that were changed |
||||||
|
*/ |
||||||
|
interface DecoratedRow extends RowData {} |
@ -1,14 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
namespace FlowBox\Exceptions; |
|
||||||
|
|
||||||
class ViewException extends FBRuntimeException |
|
||||||
{ |
|
||||||
public $captured; |
|
||||||
|
|
||||||
public function __construct(\Exception $cause, $captured) |
|
||||||
{ |
|
||||||
$this->captured = $captured; |
|
||||||
parent::__construct($cause); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,174 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
|
||||||
|
namespace App\Http\Controllers; |
||||||
|
|
||||||
|
use App\Models\Table; |
||||||
|
use App\Models\User; |
||||||
|
use App\Tables\Changeset; |
||||||
|
use Illuminate\Http\Request; |
||||||
|
use Illuminate\Support\Facades\Input; |
||||||
|
use MightyPork\Exceptions\SimpleValidationException; |
||||||
|
use MightyPork\Utils\Utils; |
||||||
|
|
||||||
|
class TableEditController extends Controller |
||||||
|
{ |
||||||
|
/** |
||||||
|
* Initialize the session-stored changeset, if not set yet |
||||||
|
* |
||||||
|
* @param Table $table |
||||||
|
* @return Changeset |
||||||
|
*/ |
||||||
|
private function getChangeset(Table $table) |
||||||
|
{ |
||||||
|
if (Input::has('reset')) { |
||||||
|
session()->forget($table->draftSessionKey); |
||||||
|
} |
||||||
|
|
||||||
|
/** @var Changeset $changeset */ |
||||||
|
return session()->remember($table->draftSessionKey, function () use ($table) { |
||||||
|
$changeset = new Changeset(); |
||||||
|
$changeset->table = $table; |
||||||
|
$changeset->revision = $table->revision; |
||||||
|
return $changeset; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private function storeChangeset(Changeset $chs) |
||||||
|
{ |
||||||
|
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); |
||||||
|
|
||||||
|
return redirect($tableModel->viewRoute); |
||||||
|
} |
||||||
|
|
||||||
|
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); |
||||||
|
|
||||||
|
if (Input::has('dump')) { |
||||||
|
dd($changeset); |
||||||
|
} |
||||||
|
|
||||||
|
return $this->{camel_case($tab)}($changeset); |
||||||
|
} |
||||||
|
|
||||||
|
/** @noinspection PhpUnusedPrivateMethodInspection */ |
||||||
|
private function editRows(Changeset $changeset) |
||||||
|
{ |
||||||
|
$revision = $changeset->revision; |
||||||
|
$columns = $changeset->fetchAndTransformColumns(); |
||||||
|
$rows = $revision->rowsData($columns, true, false)->paginate(25, []); |
||||||
|
|
||||||
|
return view('table.propose.edit-rows', [ |
||||||
|
'changeset' => $changeset, |
||||||
|
'table' => $changeset->table, |
||||||
|
'columns' => collect($columns), |
||||||
|
'rows' => $rows, |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
/** @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(Request $request, User $user, string $table) |
||||||
|
{ |
||||||
|
/** @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); |
||||||
|
|
||||||
|
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': |
||||||
|
$isNew = $changeset->isNewRow($input->id); |
||||||
|
$changeset->rowRemove($input->id); |
||||||
|
$resp = $isNew ? null : $changeset->fetchAndTransformRow($input->id); |
||||||
|
break; |
||||||
|
|
||||||
|
case 'row.restore': |
||||||
|
$changeset->rowRestore($input->id); |
||||||
|
$resp = $changeset->fetchAndTransformRow($input->id); |
||||||
|
break; |
||||||
|
|
||||||
|
case 'rows.add': |
||||||
|
$changeset->addBlankRows($input->count); |
||||||
|
|
||||||
|
// rows.add is sent via a form |
||||||
|
if ($input->has('redirect')) { |
||||||
|
return redirect($input->redirect); |
||||||
|
} else { |
||||||
|
$resp = null; |
||||||
|
} |
||||||
|
break; |
||||||
|
|
||||||
|
case 'rows.add-csv': |
||||||
|
try { |
||||||
|
$changeset->addFilledRows(Utils::csvToArray($input->data)); |
||||||
|
} catch (\Exception $e) { |
||||||
|
return $this->backWithErrors(['data' => $e->getMessage()]); |
||||||
|
} |
||||||
|
|
||||||
|
// rows.add-csv is sent via a form |
||||||
|
if ($input->has('redirect')) { |
||||||
|
return redirect($input->redirect); |
||||||
|
} else { |
||||||
|
$resp = null; |
||||||
|
} |
||||||
|
break; |
||||||
|
|
||||||
|
default: |
||||||
|
$resp = "Bad Action"; |
||||||
|
$code = 400; |
||||||
|
break; |
||||||
|
} |
||||||
|
} catch (SimpleValidationException $e) { |
||||||
|
return $this->jsonResponse(['errors' => $e->getMessageBag()->getMessages()], 400); |
||||||
|
} |
||||||
|
|
||||||
|
$this->storeChangeset($changeset); |
||||||
|
|
||||||
|
return $this->jsonResponse($resp, $code); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
|
||||||
|
namespace App\Tables; |
||||||
|
|
||||||
|
|
||||||
|
abstract class BaseNumerator |
||||||
|
{ |
||||||
|
/** @var int */ |
||||||
|
protected $next; |
||||||
|
|
||||||
|
/** @var int */ |
||||||
|
protected $last; |
||||||
|
|
||||||
|
/** |
||||||
|
* BaseNumerator constructor. |
||||||
|
* |
||||||
|
* @param int|int[] $first - first index, or [first, last] |
||||||
|
* @param int|null $last - last index, or null |
||||||
|
*/ |
||||||
|
public function __construct($first, $last = null) |
||||||
|
{ |
||||||
|
if (is_array($first) && $last === null) { |
||||||
|
list($first, $last) = $first; |
||||||
|
} |
||||||
|
|
||||||
|
$this->next = $first; |
||||||
|
$this->last = $last; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get next key, incrementing the internal state |
||||||
|
* |
||||||
|
* @return string |
||||||
|
*/ |
||||||
|
public function next() |
||||||
|
{ |
||||||
|
if (!$this->hasMore()) |
||||||
|
throw new \OutOfBoundsException("Column numerator has run out of allocated GCID slots"); |
||||||
|
|
||||||
|
$key = $this->getKey($this->next); |
||||||
|
$this->next++; |
||||||
|
return $key; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Convert numeric index to a key |
||||||
|
* |
||||||
|
* @param int $index |
||||||
|
* @return mixed |
||||||
|
*/ |
||||||
|
protected function getKey($index) |
||||||
|
{ |
||||||
|
return $index; // simple default |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return bool - true iof there are more keys available |
||||||
|
*/ |
||||||
|
protected function hasMore() |
||||||
|
{ |
||||||
|
return $this->next <= $this->last; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Generate all keys |
||||||
|
* |
||||||
|
* @return \Generator |
||||||
|
*/ |
||||||
|
public function generate() |
||||||
|
{ |
||||||
|
while ($this->hasMore()) { |
||||||
|
yield $this->next(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,467 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
|
||||||
|
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\NotApplicableException; |
||||||
|
use MightyPork\Exceptions\NotExistException; |
||||||
|
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; |
||||||
|
|
||||||
|
/** |
||||||
|
* @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 |
||||||
|
* |
||||||
|
* Key'd by _id |
||||||
|
* |
||||||
|
* @var array|null - [_id -> [_id:…, cid:…, cid:…], …] |
||||||
|
*/ |
||||||
|
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 -> [_id:…, cid:…, cid:…], …] |
||||||
|
*/ |
||||||
|
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. |
||||||
|
* |
||||||
|
* Key'd by id |
||||||
|
* |
||||||
|
* @var array[] - column specification objects, with GCIDs, key'd by CID |
||||||
|
*/ |
||||||
|
public $columnUpdates = []; |
||||||
|
|
||||||
|
/** |
||||||
|
* New columns in the full format, including GCIDs |
||||||
|
* |
||||||
|
* @var array|null - [id -> [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 = []; |
||||||
|
|
||||||
|
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 = $this->$prop; |
||||||
|
} |
||||||
|
|
||||||
|
return $object; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Check if there is any change in this changeset |
||||||
|
* |
||||||
|
* @return bool - any found |
||||||
|
*/ |
||||||
|
public function hasAnyChanges() |
||||||
|
{ |
||||||
|
foreach ($this->walkProps() as $prop) { |
||||||
|
if (!empty($this->$prop)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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 ($row instanceof Row) $row = (object)$row->getAttributes(); |
||||||
|
|
||||||
|
if ($decorate) { |
||||||
|
$row->_remove = false; |
||||||
|
$row->_changed = []; |
||||||
|
$row->_orig = []; |
||||||
|
} |
||||||
|
|
||||||
|
if ($this->isNewRow($row->_id)) { |
||||||
|
if ($decorate) { |
||||||
|
$row->_new = true; |
||||||
|
} |
||||||
|
return $row; |
||||||
|
} |
||||||
|
|
||||||
|
// Removed rows - return as null |
||||||
|
if (in_array($row->_id, $this->removedRows)) { |
||||||
|
if ($decorate) { |
||||||
|
$row->_remove = true; |
||||||
|
} else { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 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); |
||||||
|
} |
||||||
|
|
||||||
|
$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 fetchAndTransformColumns() |
||||||
|
{ |
||||||
|
/** @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 |
||||||
|
foreach ($columns as $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 |
||||||
|
$colsById = collect($columns)->keyBy('id')->all(); |
||||||
|
$newOrder = []; |
||||||
|
foreach ($this->columnOrder as $id) { |
||||||
|
$newOrder[] = $colsById[$id]; |
||||||
|
} |
||||||
|
|
||||||
|
$leftover_keys = array_diff(array_keys($colsById), $this->columnOrder); |
||||||
|
foreach ($leftover_keys as $id) { |
||||||
|
$newOrder[] = $colsById[$id]; |
||||||
|
} |
||||||
|
|
||||||
|
return $cachedColumns = $newOrder; |
||||||
|
} |
||||||
|
|
||||||
|
public function rowRemove(int $id) |
||||||
|
{ |
||||||
|
if ($this->isNewRow($id)) { |
||||||
|
unset($this->newRows[$id]); |
||||||
|
} |
||||||
|
else { |
||||||
|
$this->removedRows[] = $id; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public function rowRestore(int $id) |
||||||
|
{ |
||||||
|
$this->removedRows = array_diff($this->removedRows, [$id]); |
||||||
|
} |
||||||
|
|
||||||
|
public function isNewRow(int $id) |
||||||
|
{ |
||||||
|
return isset($this->newRows[$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); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fetch an existing row from DB, or a new row. |
||||||
|
* |
||||||
|
* @param $id |
||||||
|
* @return object |
||||||
|
*/ |
||||||
|
public function fetchRow(int $id) |
||||||
|
{ |
||||||
|
if ($this->isNewRow($id)) { |
||||||
|
return (object)$this->newRows[$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(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Apply a row update, adding the row to the list of changes, or removing it |
||||||
|
* if all differences were undone. |
||||||
|
* |
||||||
|
* @param array|object $newVals - values of the new row |
||||||
|
*/ |
||||||
|
public function rowUpdate($newVals) |
||||||
|
{ |
||||||
|
$newVals = (object)$newVals; |
||||||
|
|
||||||
|
$_id = $newVals->_id; |
||||||
|
$origRow = $this->fetchRow($_id); |
||||||
|
|
||||||
|
/** @var Column[]|Collection $cols */ |
||||||
|
$cols = collect($this->fetchAndTransformColumns())->keyBy('id'); |
||||||
|
|
||||||
|
$updateObj = []; |
||||||
|
|
||||||
|
foreach ($newVals as $colId => $value) { |
||||||
|
if (starts_with($colId, '_')) continue; // internals |
||||||
|
|
||||||
|
$col = $cols[$colId]; |
||||||
|
$value = $col->cast($value); |
||||||
|
$origValue = $col->cast(isset($origRow->$colId) ? $origRow->$colId : null); |
||||||
|
|
||||||
|
if ($value !== $origValue) { |
||||||
|
$updateObj[$colId] = $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if ($this->isNewRow($_id)) { |
||||||
|
$this->newRows[$_id] = array_merge($this->newRows[$_id], $updateObj); |
||||||
|
} |
||||||
|
else { |
||||||
|
if (!empty($updateObj)) { |
||||||
|
$this->rowUpdates[$_id] = $updateObj; |
||||||
|
} else { |
||||||
|
// remove possible old update record for this row, if nothing changes now |
||||||
|
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($perPage = 25) |
||||||
|
{ |
||||||
|
return collection_paginate($this->newRows, $perPage); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param Column[] $columns |
||||||
|
* @param array $csvArray |
||||||
|
* @param bool $forTableInsert |
||||||
|
* @return \array[][]|Collection |
||||||
|
*/ |
||||||
|
public static function csvToRowsArray($columns, $csvArray, $forTableInsert) |
||||||
|
{ |
||||||
|
/** @var Collection|array[][] $rows */ |
||||||
|
$rows = collect($csvArray)->map(function ($row) use ($columns, $forTableInsert) { |
||||||
|
if (count($row) == 0 || count($row) == 1 && $row[0] == '') return null; // discard empty lines |
||||||
|
if (count($row) != count($columns)) { |
||||||
|
throw new NotApplicableException("All rows must have " . count($columns) . " fields."); |
||||||
|
} |
||||||
|
|
||||||
|
$data = []; |
||||||
|
|
||||||
|
foreach ($row as $i => $val) { |
||||||
|
$col = $columns[$i]; |
||||||
|
|
||||||
|
if (strlen($val) > 1000) { |
||||||
|
// try to stop people inserting unstructured crap / malformed CSV |
||||||
|
throw new NotApplicableException("Value for column {$col->name} too long."); |
||||||
|
} |
||||||
|
$data[$col->id] = $col->cast($val); |
||||||
|
} |
||||||
|
|
||||||
|
if ($forTableInsert) { |
||||||
|
return ['data' => $data]; |
||||||
|
} else { |
||||||
|
return $data; |
||||||
|
} |
||||||
|
})->filter(); |
||||||
|
|
||||||
|
|
||||||
|
$rowNumerator = new RowNumerator(count($csvArray)); |
||||||
|
|
||||||
|
if ($forTableInsert) { |
||||||
|
return $rows->map(function ($row) use (&$rowNumerator) { |
||||||
|
$row['data']['_id'] = $rowNumerator->next(); |
||||||
|
return $row; |
||||||
|
}); |
||||||
|
} |
||||||
|
else { |
||||||
|
return $rows->map(function ($row) use (&$rowNumerator) { |
||||||
|
$row['_id'] = $rowNumerator->next(); |
||||||
|
return $row; |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public function addBlankRows(int $count) |
||||||
|
{ |
||||||
|
$numerator = new RowNumerator($count); |
||||||
|
|
||||||
|
$columns = $this->fetchAndTransformColumns(); |
||||||
|
$template = []; |
||||||
|
foreach ($columns as $column) { |
||||||
|
$template[$column->id] = $column->cast(null); |
||||||
|
} |
||||||
|
|
||||||
|
foreach ($numerator->generate() as $_id) { |
||||||
|
$row = $template; |
||||||
|
$row['_id'] = $_id; |
||||||
|
$this->newRows[$_id] = $row; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public function addFilledRows($csvArray) |
||||||
|
{ |
||||||
|
/** @var Column[] $columns */ |
||||||
|
$columns = array_values($this->fetchAndTransformColumns()); |
||||||
|
|
||||||
|
$rows = self::csvToRowsArray($columns, $csvArray, false) |
||||||
|
->keyBy('_id'); |
||||||
|
|
||||||
|
$this->newRows = array_merge($this->newRows, $rows->all()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,201 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
return [ |
||||||
|
|
||||||
|
/* |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| Debugbar Settings |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| |
||||||
|
| Debugbar is enabled by default, when debug is set to true in app.php. |
||||||
|
| You can override the value by setting enable to true or false instead of null. |
||||||
|
| |
||||||
|
| You can provide an array of URI's that must be ignored (eg. 'api/*') |
||||||
|
| |
||||||
|
*/ |
||||||
|
|
||||||
|
'enabled' => env('DEBUGBAR_ENABLED', null), |
||||||
|
'except' => [ |
||||||
|
// |
||||||
|
], |
||||||
|
|
||||||
|
/* |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| Storage settings |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| |
||||||
|
| DebugBar stores data for session/ajax requests. |
||||||
|
| You can disable this, so the debugbar stores data in headers/session, |
||||||
|
| but this can cause problems with large data collectors. |
||||||
|
| By default, file storage (in the storage folder) is used. Redis and PDO |
||||||
|
| can also be used. For PDO, run the package migrations first. |
||||||
|
| |
||||||
|
*/ |
||||||
|
'storage' => [ |
||||||
|
'enabled' => true, |
||||||
|
'driver' => 'file', // redis, file, pdo, custom |
||||||
|
'path' => storage_path('debugbar'), // For file driver |
||||||
|
'connection' => null, // Leave null for default connection (Redis/PDO) |
||||||
|
'provider' => '' // Instance of StorageInterface for custom driver |
||||||
|
], |
||||||
|
|
||||||
|
/* |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| Vendors |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| |
||||||
|
| Vendor files are included by default, but can be set to false. |
||||||
|
| This can also be set to 'js' or 'css', to only include javascript or css vendor files. |
||||||
|
| Vendor files are for css: font-awesome (including fonts) and highlight.js (css files) |
||||||
|
| and for js: jquery and and highlight.js |
||||||
|
| So if you want syntax highlighting, set it to true. |
||||||
|
| jQuery is set to not conflict with existing jQuery scripts. |
||||||
|
| |
||||||
|
*/ |
||||||
|
|
||||||
|
'include_vendors' => true, |
||||||
|
|
||||||
|
/* |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| Capture Ajax Requests |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| |
||||||
|
| The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors), |
||||||
|
| you can use this option to disable sending the data through the headers. |
||||||
|
| |
||||||
|
| Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools. |
||||||
|
*/ |
||||||
|
|
||||||
|
'capture_ajax' => true, |
||||||
|
'add_ajax_timing' => false, |
||||||
|
|
||||||
|
/* |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| Custom Error Handler for Deprecated warnings |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| |
||||||
|
| When enabled, the Debugbar shows deprecated warnings for Symfony components |
||||||
|
| in the Messages tab. |
||||||
|
| |
||||||
|
*/ |
||||||
|
'error_handler' => false, |
||||||
|
|
||||||
|
/* |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| Clockwork integration |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| |
||||||
|
| The Debugbar can emulate the Clockwork headers, so you can use the Chrome |
||||||
|
| Extension, without the server-side code. It uses Debugbar collectors instead. |
||||||
|
| |
||||||
|
*/ |
||||||
|
'clockwork' => false, |
||||||
|
|
||||||
|
/* |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| DataCollectors |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| |
||||||
|
| Enable/disable DataCollectors |
||||||
|
| |
||||||
|
*/ |
||||||
|
|
||||||
|
'collectors' => [ |
||||||
|
'phpinfo' => true, // Php version |
||||||
|
'messages' => true, // Messages |
||||||
|
'time' => true, // Time Datalogger |
||||||
|
'memory' => true, // Memory usage |
||||||
|
'exceptions' => true, // Exception displayer |
||||||
|
'log' => true, // Logs from Monolog (merged in messages if enabled) |
||||||
|
'db' => true, // Show database (PDO) queries and bindings |
||||||
|
'views' => true, // Views with their data |
||||||
|
'route' => true, // Current route information |
||||||
|
'auth' => true, // Display Laravel authentication status |
||||||
|
'gate' => true, // Display Laravel Gate checks |
||||||
|
'session' => true, // Display session data |
||||||
|
'symfony_request' => true, // Only one can be enabled.. |
||||||
|
'mail' => true, // Catch mail messages |
||||||
|
'laravel' => false, // Laravel version and environment |
||||||
|
'events' => false, // All events fired |
||||||
|
'default_request' => false, // Regular or special Symfony request logger |
||||||
|
'logs' => false, // Add the latest log messages |
||||||
|
'files' => false, // Show the included files |
||||||
|
'config' => false, // Display config settings |
||||||
|
'cache' => false, // Display cache events |
||||||
|
], |
||||||
|
|
||||||
|
/* |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| Extra options |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| |
||||||
|
| Configure some DataCollectors |
||||||
|
| |
||||||
|
*/ |
||||||
|
|
||||||
|
'options' => [ |
||||||
|
'auth' => [ |
||||||
|
'show_name' => true, // Also show the users name/email in the debugbar |
||||||
|
], |
||||||
|
'db' => [ |
||||||
|
'with_params' => true, // Render SQL with the parameters substituted |
||||||
|
'backtrace' => true, // Use a backtrace to find the origin of the query in your files. |
||||||
|
'timeline' => false, // Add the queries to the timeline |
||||||
|
'explain' => [ // Show EXPLAIN output on queries |
||||||
|
'enabled' => false, |
||||||
|
'types' => ['SELECT'], // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+ |
||||||
|
], |
||||||
|
'hints' => true, // Show hints for common mistakes |
||||||
|
], |
||||||
|
'mail' => [ |
||||||
|
'full_log' => false |
||||||
|
], |
||||||
|
'views' => [ |
||||||
|
'data' => false, //Note: Can slow down the application, because the data can be quite large.. |
||||||
|
], |
||||||
|
'route' => [ |
||||||
|
'label' => true // show complete route on bar |
||||||
|
], |
||||||
|
'logs' => [ |
||||||
|
'file' => null |
||||||
|
], |
||||||
|
'cache' => [ |
||||||
|
'values' => true // collect cache values |
||||||
|
], |
||||||
|
], |
||||||
|
|
||||||
|
/* |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| Inject Debugbar in Response |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| |
||||||
|
| Usually, the debugbar is added just before </body>, by listening to the |
||||||
|
| Response after the App is done. If you disable this, you have to add them |
||||||
|
| in your template yourself. See http://phpdebugbar.com/docs/rendering.html |
||||||
|
| |
||||||
|
*/ |
||||||
|
|
||||||
|
'inject' => true, |
||||||
|
|
||||||
|
/* |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| DebugBar route prefix |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| |
||||||
|
| Sometimes you want to set route prefix to be used by DebugBar to load |
||||||
|
| its resources from. Usually the need comes from misconfigured web server or |
||||||
|
| from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97 |
||||||
|
| |
||||||
|
*/ |
||||||
|
'route_prefix' => '_debugbar', |
||||||
|
|
||||||
|
/* |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| DebugBar route domain |
||||||
|
|-------------------------------------------------------------------------- |
||||||
|
| |
||||||
|
| By default DebugBar route served from the same domain that request served. |
||||||
|
| To override default domain, specify it as a non-empty value. |
||||||
|
*/ |
||||||
|
'route_domain' => null, |
||||||
|
]; |
@ -0,0 +1,46 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace MightyPork\Exceptions; |
||||||
|
|
||||||
|
use Illuminate\Contracts\Support\MessageProvider; |
||||||
|
use Illuminate\Support\MessageBag; |
||||||
|
|
||||||
|
class SimpleValidationException extends RuntimeException implements MessageProvider |
||||||
|
{ |
||||||
|
/** @var MessageBag */ |
||||||
|
private $mb; |
||||||
|
|
||||||
|
/** |
||||||
|
* FBValidationException constructor. |
||||||
|
* |
||||||
|
* @param string|MessageProvider $key |
||||||
|
* @param string $message |
||||||
|
*/ |
||||||
|
public function __construct($key, $message = null) |
||||||
|
{ |
||||||
|
if (is_null($message)) { |
||||||
|
$this->mb = $key->getMessageBag(); |
||||||
|
} else { |
||||||
|
$mb = new MessageBag(); |
||||||
|
$mb->add($key, $message); |
||||||
|
$this->mb = $mb; |
||||||
|
} |
||||||
|
|
||||||
|
$str = ''; |
||||||
|
foreach ($this->mb->getMessages() as $key => $errors) { |
||||||
|
$str .= $key . ': ' . implode(', ', $errors) . "\n"; |
||||||
|
} |
||||||
|
|
||||||
|
parent::__construct($str); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the messages for the instance. |
||||||
|
* |
||||||
|
* @return MessageBag |
||||||
|
*/ |
||||||
|
public function getMessageBag() |
||||||
|
{ |
||||||
|
return $this->mb; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,52 @@ |
|||||||
|
window._ = require('./udash'); |
||||||
|
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
|
||||||
|
// });
|
@ -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
|
|
||||||
// });
|
|
@ -0,0 +1,160 @@ |
|||||||
|
<template> |
||||||
|
<table class="table table-hover table-sm table-fixed td-va-middle"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th style="width:3rem" class="border-top-0"></th> |
||||||
|
<th style="width:3rem" class="border-top-0"></th> |
||||||
|
<th v-for="col in columns" :class="colClasses(col)" :title="col.name">{{col.title}}</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
<tr v-for="row in rows" :style="rowStyle(row)"> |
||||||
|
<td> |
||||||
|
<a href="" :class="['btn','btn-outline-danger',{'active': row._remove}]" |
||||||
|
@click.prevent="toggleRowDelete(row._id)"> |
||||||
|
<v-icon :class="row._remove ? 'fa-undo' : 'fa-trash-o'" |
||||||
|
:alt="row._remove ? 'Undo Remove' : 'Remove'" /> |
||||||
|
</a> |
||||||
|
</td> |
||||||
|
<td> |
||||||
|
<a href="" :class="['btn','btn-outline-secondary',{'disabled': row._remove}]" |
||||||
|
@click.prevent="toggleRowEditing(row._id)"> |
||||||
|
<v-icon :class="row._editing ? 'fa-save' : 'fa-pencil'" |
||||||
|
:alt="row._editing ? 'Save' : 'Edit'" /> |
||||||
|
</a> |
||||||
|
</td> |
||||||
|
|
||||||
|
<template v-if="row._editing"> |
||||||
|
<td v-for="col in columns" class="pr-0"> |
||||||
|
<input v-model="row[col.id]" :class="['form-control', { 'is-invalid': row._errors && row._errors[col.id] }]" |
||||||
|
:title="(row._errors && row._errors[col.id]) ? row._errors[col.id][0] : null" |
||||||
|
type="text" @keyup.enter="toggleRowEditing(row._id)"> |
||||||
|
</td> |
||||||
|
</template> |
||||||
|
|
||||||
|
<template v-else> |
||||||
|
<td v-for="col in columns"> |
||||||
|
<span class="text-danger strike" title="Original value" v-if="isChanged(row, col.id)">{{row._orig[col.id]}}</span> |
||||||
|
<span>{{ row[col.id] }}</span> |
||||||
|
<v-icon v-if="isChanged(row, col.id)" |
||||||
|
@click="revertCell(row, col.id)" |
||||||
|
class="fa-undo text-danger pointer" |
||||||
|
alt="Revert Change" /> |
||||||
|
</td> |
||||||
|
</template> |
||||||
|
|
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@import "base"; |
||||||
|
|
||||||
|
</style> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props: { |
||||||
|
route: String, |
||||||
|
xRows: Object, // key'd by _id |
||||||
|
columns: Array, |
||||||
|
lastPage: Boolean, |
||||||
|
}, |
||||||
|
data: function() { |
||||||
|
return { |
||||||
|
rows: this.xRows, |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
busy (yes) { |
||||||
|
$('#draft-busy').css('opacity', yes ? 1 : 0) |
||||||
|
}, |
||||||
|
|
||||||
|
query (data, sucfn, erfn) { |
||||||
|
this.busy(true) |
||||||
|
if (!sucfn) sucfn = ()=>{} |
||||||
|
if (!sucfn) erfn = ()=>{} |
||||||
|
window.axios.post(this.route, data).then(sucfn).catch((error) => { |
||||||
|
console.error(error.message) |
||||||
|
erfn(error.response.data) |
||||||
|
}).then(() => { |
||||||
|
this.busy(false) |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
toggleRowDelete(_id) { |
||||||
|
if (!_.isDefined(this.rows[_id])) return; |
||||||
|
|
||||||
|
let remove = !this.rows[_id]._remove |
||||||
|
|
||||||
|
this.query({ |
||||||
|
action: remove ? 'row.remove' : 'row.restore', |
||||||
|
id: _id |
||||||
|
}, (resp) => { |
||||||
|
// if response is null, this was a New row |
||||||
|
// and it was discarded without a way back - hard drop |
||||||
|
if (_.isEmpty(resp.data)) { |
||||||
|
this.$delete(this.rows, _id) |
||||||
|
} |
||||||
|
else { |
||||||
|
this.$set(this.rows, _id, resp.data) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
submitRowChange(row) { |
||||||
|
this.query({ |
||||||
|
action: 'row.update', |
||||||
|
data: row |
||||||
|
}, (resp) => { |
||||||
|
this.$set(this.rows, resp.data._id, resp.data); |
||||||
|
}, (er) => { |
||||||
|
if (!_.isUndefined(er.errors)) { |
||||||
|
this.$set(this.rows[row._id], '_errors', er.errors); |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
toggleRowEditing(_id) { |
||||||
|
if (this.rows[_id]._remove) return false; // can't edit row marked for removal |
||||||
|
|
||||||
|
let editing = !this.rows[_id]._editing |
||||||
|
|
||||||
|
if (!editing) { |
||||||
|
this.submitRowChange(this.rows[_id]) |
||||||
|
} else { |
||||||
|
this.$set(this.rows[_id], '_editing', true); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
colClasses(col) { |
||||||
|
return [ |
||||||
|
'border-top-0', |
||||||
|
{ |
||||||
|
'text-danger': col._remove, |
||||||
|
'strike': col._remove, |
||||||
|
'text-success': col._new |
||||||
|
} |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
rowStyle(row) { |
||||||
|
return { |
||||||
|
opacity: row._remove ? .8 : 1, |
||||||
|
backgroundColor: |
||||||
|
row._remove ? '#FFC4CC': |
||||||
|
'transparent' |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
isChanged (row, colId) { |
||||||
|
return row._changed && row._changed.indexOf(colId) > -1 |
||||||
|
}, |
||||||
|
|
||||||
|
revertCell(row, colId) { |
||||||
|
this.submitRowChange(_.merge({}, row, { [colId]: row._orig[colId] })) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
@ -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') |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
@ -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) |
||||||
|
}) |
@ -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) |
||||||
|
} |
||||||
|
}) |
@ -0,0 +1,12 @@ |
|||||||
|
// subset of used lodash modules
|
||||||
|
|
||||||
|
export { default as each } from 'lodash/each' |
||||||
|
export { default as isUndefined } from 'lodash/isUndefined' |
||||||
|
export { default as merge } from 'lodash/merge' |
||||||
|
export { default as unset } from 'lodash/unset' |
||||||
|
export { default as isEmpty } from 'lodash/isEmpty' |
||||||
|
|
||||||
|
function isDefined(x) { |
||||||
|
return typeof(x) !== 'undefined'; |
||||||
|
} |
||||||
|
export { isDefined } |
@ -0,0 +1,27 @@ |
|||||||
|
window.Vue = require('vue'); |
||||||
|
|
||||||
|
const ColumnEditorCtor = Vue.component('column-editor', require('./components/ColumnEditor.vue')); |
||||||
|
const RowsEditorCtor = Vue.component('rows-editor', require('./components/RowsEditor.vue')); |
||||||
|
const IconCtor = Vue.component('v-icon', require('./components/Icon.vue')); |
||||||
|
|
||||||
|
// const app = new Vue({
|
||||||
|
// el: '#app'
|
||||||
|
// });
|
||||||
|
|
||||||
|
window.app = { |
||||||
|
ColumnEditor: function(selector, data) { |
||||||
|
new ColumnEditorCtor({ |
||||||
|
propsData: data |
||||||
|
}).$mount(selector); |
||||||
|
}, |
||||||
|
RowsEditor: function(selector, data) { |
||||||
|
new RowsEditorCtor({ |
||||||
|
propsData: data |
||||||
|
}).$mount(selector); |
||||||
|
}, |
||||||
|
Icon: function(selector, data) { |
||||||
|
new IconCtor({ |
||||||
|
propsData: data |
||||||
|
}).$mount(selector); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
.table-fixed { |
||||||
|
table-layout: fixed; |
||||||
|
} |
||||||
|
|
||||||
|
.td-va-middle { |
||||||
|
td, th { |
||||||
|
vertical-align: middle !important; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link" href="{{ route('login') }}"> |
||||||
|
@icon(fa-sign-in pr-1) |
||||||
|
{{ __('Login') }} |
||||||
|
</a> |
||||||
|
|
||||||
|
@if(config('app.allow_regs')) |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link" href="{{ route('register') }}"> |
||||||
|
@icon(fa-user-plus pr-1) |
||||||
|
{{ __('Register') }} |
||||||
|
</a> |
||||||
|
@endif |
@ -0,0 +1,52 @@ |
|||||||
|
@php |
||||||
|
$tab = 'add-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-row-pagination') |
||||||
|
|
||||||
|
@section('header') |
||||||
|
<div class="form-inline py-2 px-1 border-bottom mb-3"> |
||||||
|
{{-- TODO improve layout --}} |
||||||
|
<form action="{{$table->draftUpdateRoute}}" method="POST" class="form-inline"> |
||||||
|
@csrf |
||||||
|
<input type="hidden" name="action" value="rows.add-csv"> |
||||||
|
<input type="hidden" name="redirect" value="{{request()->fullUrl()}}"> |
||||||
|
<label class="pr-2" for="csv-data">Add CSV:</label> |
||||||
|
<textarea name="data" id="csv-data" |
||||||
|
title="{{$errors->has('data') ? $errors->first('data') : ''}}" |
||||||
|
class="form-control mr-1 {{ $errors->has('data')?'is-invalid':'' }}" |
||||||
|
style="width:30em; height:10em">{{old('data')}}</textarea> |
||||||
|
<button class="btn btn-outline-success">Append</button> |
||||||
|
</form> |
||||||
|
|
||||||
|
<div class="flex-grow-1"></div> |
||||||
|
|
||||||
|
<form action="{{$table->draftUpdateRoute}}" method="POST"> |
||||||
|
@csrf |
||||||
|
<input type="hidden" name="action" value="rows.add"> |
||||||
|
<input type="hidden" name="redirect" value="{{request()->fullUrl()}}"> |
||||||
|
<label class="form-inline pr-2" for="newrow-count">Add Empty Rows:</label> |
||||||
|
<input name="count" id="newrow-count" type="number" min=1 step=1 value=1 class="form-control mr-1" style="width:10em"> |
||||||
|
<button class="btn btn-outline-success">Add</button> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
@stop |
||||||
|
|
||||||
|
@section('rows') |
||||||
|
<div id="rows-editor"></div> |
||||||
|
@stop |
||||||
|
|
||||||
|
@push('scripts') |
||||||
|
<script> |
||||||
|
ready(function() { |
||||||
|
app.RowsEditor('#rows-editor', { |
||||||
|
route: {!! toJSON($table->draftUpdateRoute) !!}, |
||||||
|
columns: {!! toJSON($columns) !!}, |
||||||
|
xRows: {!! toJSON($rows->keyBy('_id'), true) !!}, |
||||||
|
}) |
||||||
|
}); |
||||||
|
</script> |
||||||
|
@endpush |
@ -0,0 +1,32 @@ |
|||||||
|
@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-row-pagination') |
||||||
|
|
||||||
|
@section('rows') |
||||||
|
@php |
||||||
|
$transformed = $rows->keyBy('_id')->map(function($r) use ($changeset) { |
||||||
|
/** @var \App\Tables\Changeset $changeset */ |
||||||
|
return $changeset->transformRow($r, true); |
||||||
|
}); |
||||||
|
@endphp |
||||||
|
|
||||||
|
<div id="rows-editor"></div> |
||||||
|
@stop |
||||||
|
|
||||||
|
@push('scripts') |
||||||
|
<script> |
||||||
|
ready(function() { |
||||||
|
app.RowsEditor('#rows-editor', { |
||||||
|
route: {!! toJSON($table->draftUpdateRoute) !!}, |
||||||
|
columns: {!! toJSON($columns) !!}, |
||||||
|
xRows: {!! toJSON($transformed, true) !!}, |
||||||
|
}) |
||||||
|
}); |
||||||
|
</script> |
||||||
|
@endpush |
@ -0,0 +1,31 @@ |
|||||||
|
@php |
||||||
|
/** @var \App\Models\Row[]|Illuminate\Pagination\Paginator $rows */ |
||||||
|
@endphp |
||||||
|
|
||||||
|
@extends('table.propose.layout') |
||||||
|
|
||||||
|
@section('tab-content') |
||||||
|
<div class="col-12"> |
||||||
|
@yield('header') |
||||||
|
</div> |
||||||
|
|
||||||
|
@if($rows->hasPages()) |
||||||
|
<div class="col-md-12 d-flex"> |
||||||
|
<nav class="text-center" aria-label="Pages of the table"> |
||||||
|
{{ $rows->links(null, ['ulClass' => 'mb-0']) }} |
||||||
|
</nav> |
||||||
|
</div> |
||||||
|
@endif |
||||||
|
|
||||||
|
<div class="col-12"> |
||||||
|
@yield('rows') |
||||||
|
</div> |
||||||
|
|
||||||
|
@if($rows->hasPages()) |
||||||
|
<div class="col-md-12 d-flex"> |
||||||
|
<nav class="text-center" aria-label="Pages of the table"> |
||||||
|
{{ $rows->links(null, ['ulClass' => 'mb-0']) }} |
||||||
|
</nav> |
||||||
|
</div> |
||||||
|
@endif |
||||||
|
@stop |
@ -0,0 +1,60 @@ |
|||||||
|
{{-- Basic table view --}} |
||||||
|
|
||||||
|
@extends('layouts.app') |
||||||
|
|
||||||
|
@php |
||||||
|
/** @var \App\Models\Table $table */ |
||||||
|
|
||||||
|
if (!isset($tab) || $tab == '') $tab = 'edit-rows'; |
||||||
|
@endphp |
||||||
|
|
||||||
|
@section('content') |
||||||
|
<div class="row justify-content-start px-3"> |
||||||
|
<div class="d-flex w-100 align-items-center"> |
||||||
|
<small class="flex-grow-1" style="font-size: 120%;"> |
||||||
|
<a href="{{ route('profile.view', $table->owner->name) }}" class="link-no-color">{{ $table->owner->handle }}</a>{{-- |
||||||
|
--}}<span class="px-1">/</span>{{-- |
||||||
|
--}}<b><a href="{{ $table->viewRoute }}" class="link-no-color">{{ $table->name }}</a></b> |
||||||
|
</small> |
||||||
|
|
||||||
|
<h1 class="mx-3"> |
||||||
|
<a href="{{ $table->viewRoute }}" class="link-no-color">{{ $table->title }}</a> |
||||||
|
</h1> |
||||||
|
<a href="{{$table->draftDiscardRoute}}" class="btn btn-outline-danger mr-2" @tooltip(Discard changes)> |
||||||
|
@icon(fa-trash-o, sr:Discard) |
||||||
|
</a> |
||||||
|
@if(user()->ownsTable($table)) |
||||||
|
<a href="" class="btn btn-outline-success" @tooltip(Save the changes and apply them as a new table revision)> |
||||||
|
@icon(fa-save fa-pr)Commit |
||||||
|
</a> |
||||||
|
@else |
||||||
|
<a href="" class="btn btn-outline-success" @tooltip(Submit your changes for review by the table owner)> |
||||||
|
@icon(fa-save fa-pr)Submit |
||||||
|
</a> |
||||||
|
@endif |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<ul class="nav nav-tabs"> |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link{{ $tab=='edit-rows'?' active':'' }}" href="{{ $table->getDraftRoute('edit-rows') }}">Edit Rows</a> |
||||||
|
</li> |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link{{ $tab=='add-rows'?' active':'' }}" href="{{ $table->getDraftRoute('add-rows') }}">Add Rows</a> |
||||||
|
</li> |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link{{ $tab=='manage-columns'?' active':'' }}" href="{{ $table->getDraftRoute('manage-columns') }}">Columns</a> |
||||||
|
</li> |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link{{ $tab=='review'?' active':'' }}" href="{{ $table->getDraftRoute('review') }}">Note & Review</a> |
||||||
|
</li> |
||||||
|
<li class="nav-item ml-auto pt-2 pr-2 opacity-fade" style="opacity:0" id="draft-busy"> |
||||||
|
@icon(fa-hourglass, Working...) |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
|
||||||
|
<div class="row justify-content-center mb-2"> |
||||||
|
@yield('tab-content') |
||||||
|
</div>{{-- End of row --}} |
||||||
|
|
||||||
|
@endsection |
@ -0,0 +1,6 @@ |
|||||||
|
@php($tab='manage-columns') |
||||||
|
@extends('table.propose.layout') |
||||||
|
|
||||||
|
@section('tab-content') |
||||||
|
... |
||||||
|
@stop |
@ -0,0 +1,6 @@ |
|||||||
|
@php($tab='review') |
||||||
|
@extends('table.propose.layout') |
||||||
|
|
||||||
|
@section('tab-content') |
||||||
|
... |
||||||
|
@stop |
Loading…
Reference in new issue