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