Row adding, some webpack tweaks

editing
Ondřej Hruška 6 years ago
parent 671fb9b024
commit ace61f2a07
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 12
      Makefile
  2. 1
      _json_typehints.php
  3. 2
      app/Http/Controllers/Controller.php
  4. 30
      app/Http/Controllers/TableController.php
  5. 32
      app/Http/Controllers/TableEditController.php
  6. 76
      app/Tables/BaseNumerator.php
  7. 138
      app/Tables/Changeset.php
  8. 2
      app/Tables/Column.php
  9. 25
      app/Tables/ColumnNumerator.php
  10. 19
      app/Tables/RowNumerator.php
  11. 8
      app/helpers.php
  12. 1
      config/app.php
  13. 201
      config/debugbar.php
  14. 1
      package.json
  15. 11
      porklib/Utils/Utils.php
  16. 22
      resources/assets/js/components/RowsEditor.vue
  17. 13
      resources/assets/js/udash.js
  18. 30
      resources/assets/js/vue-init.js
  19. 6
      resources/views/layouts/app.blade.php
  20. 4
      resources/views/table/_rows.blade.php
  21. 24
      resources/views/table/create.blade.php
  22. 54
      resources/views/table/propose/add-rows.blade.php
  23. 21
      resources/views/table/propose/edit-rows.blade.php
  24. 4
      resources/views/table/propose/layout-row-pagination.blade.php
  25. 4
      resources/views/table/propose/layout.blade.php
  26. 20
      webpack.mix.js

@ -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

@ -10,6 +10,7 @@ interface RowData {}
/** /**
* Interface DecoratedRow * Interface DecoratedRow
* *
* @property bool $_new - row is new in the changeset
* @property bool $_remove - marked to be removed * @property bool $_remove - marked to be removed
* @property mixed[] $_orig - original values before transformation, key by CID * @property mixed[] $_orig - original values before transformation, key by CID
* @property string[] $_changed - values that were changed * @property string[] $_changed - values that were changed

@ -56,7 +56,7 @@ class Controller extends BaseController
'mastodon', // mastodon fetching previews 'mastodon', // mastodon fetching previews
]; ];
protected function jsonResponse($data = [], $code=200) protected function jsonResponse($data, $code=200)
{ {
return new JsonResponse($data, $code); return new JsonResponse($data, $code);
} }

@ -207,39 +207,13 @@ class TableController extends Controller
} }
// --- DATA --- // --- DATA ---
$dataCsvLines = array_map('str_getcsv', explode("\n", $input->data)); $dataCsvLines = Utils::csvToArray($input->data);
// Preparing data to insert into the Rows table // Preparing data to insert into the Rows table
$rowsToInsert = null; $rowsToInsert = null;
$rowNumerator = null; $rowNumerator = null;
try { try {
$rowsToInsert = collect($dataCsvLines)->map(function ($row) use ($columns) { $rowsToInsert = Changeset::csvToRowsArray($columns, $dataCsvLines, true)->all();
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);
}
// return in a format prepared for filling eloquent
return ['data' => $data];
})->filter()->all(); // remove nulls, to array
// attach _id labels to all rows
$rowNumerator = new RowNumerator(count($dataCsvLines));
foreach ($rowsToInsert as &$item) {
$item['data']['_id'] = $rowNumerator->next();
}
} catch (\Exception $e) { } catch (\Exception $e) {
return $this->backWithErrors(['data' => $e->getMessage()]); return $this->backWithErrors(['data' => $e->getMessage()]);
} }

@ -9,6 +9,7 @@ use App\Tables\Changeset;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Input;
use MightyPork\Exceptions\SimpleValidationException; use MightyPork\Exceptions\SimpleValidationException;
use MightyPork\Utils\Utils;
class TableEditController extends Controller class TableEditController extends Controller
{ {
@ -100,7 +101,7 @@ class TableEditController extends Controller
]); ]);
} }
public function draftUpdate(User $user, string $table, Request $request) public function draftUpdate(Request $request, User $user, string $table)
{ {
/** @var Table $tableModel */ /** @var Table $tableModel */
$tableModel = $user->tables()->where('name', $table)->first(); $tableModel = $user->tables()->where('name', $table)->first();
@ -121,8 +122,9 @@ class TableEditController extends Controller
break; break;
case 'row.remove': case 'row.remove':
$isNew = $changeset->isNewRow($input->id);
$changeset->rowRemove($input->id); $changeset->rowRemove($input->id);
$resp = $changeset->fetchAndTransformRow($input->id); $resp = $isNew ? null : $changeset->fetchAndTransformRow($input->id);
break; break;
case 'row.restore': case 'row.restore':
@ -130,6 +132,32 @@ class TableEditController extends Controller
$resp = $changeset->fetchAndTransformRow($input->id); $resp = $changeset->fetchAndTransformRow($input->id);
break; 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: default:
$resp = "Bad Action"; $resp = "Bad Action";
$code = 400; $code = 400;

@ -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();
}
}
}

@ -9,6 +9,7 @@ use App\Models\Table;
use Illuminate\Pagination\Paginator; use Illuminate\Pagination\Paginator;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use MightyPork\Exceptions\NotApplicableException;
use MightyPork\Exceptions\NotExistException; use MightyPork\Exceptions\NotExistException;
use ReflectionClass; use ReflectionClass;
@ -54,7 +55,7 @@ class Changeset
* New rows in the full Row::data format, including GRIDs. * New rows in the full Row::data format, including GRIDs.
* Values are identified by GCIDs from previously defined, or new columns. * Values are identified by GCIDs from previously defined, or new columns.
* *
* @var array|null - [[_id:…, cid:…, cid:…], …] * @var array|null - [_id -> [_id:…, cid:…, cid:…], …]
*/ */
public $newRows = []; public $newRows = [];
@ -78,7 +79,7 @@ class Changeset
/** /**
* New columns in the full format, including GCIDs * New columns in the full format, including GCIDs
* *
* @var array|null - [[id:…, …], …] * @var array|null - [id -> [id:…, …], …]
*/ */
public $newColumns = []; public $newColumns = [];
@ -187,6 +188,13 @@ class Changeset
$row->_orig = []; $row->_orig = [];
} }
if ($this->isNewRow($row->_id)) {
if ($decorate) {
$row->_new = true;
}
return $row;
}
// Removed rows - return as null // Removed rows - return as null
if (in_array($row->_id, $this->removedRows)) { if (in_array($row->_id, $this->removedRows)) {
if ($decorate) { if ($decorate) {
@ -249,7 +257,7 @@ class Changeset
// Append new columns // Append new columns
foreach ($this->newColumns as $newColumn) { foreach ($this->newColumns as $newColumn) {
$columns[] = $c =new Column($newColumn); $columns[] = $c = new Column($newColumn);
$c->markAsNew(); $c->markAsNew();
} }
@ -270,7 +278,12 @@ class Changeset
public function rowRemove(int $id) public function rowRemove(int $id)
{ {
$this->removedRows[] = $id; if ($this->isNewRow($id)) {
unset($this->newRows[$id]);
}
else {
$this->removedRows[] = $id;
}
} }
public function rowRestore(int $id) public function rowRestore(int $id)
@ -278,6 +291,11 @@ class Changeset
$this->removedRows = array_diff($this->removedRows, [$id]); $this->removedRows = array_diff($this->removedRows, [$id]);
} }
public function isNewRow(int $id)
{
return isset($this->newRows[$id]);
}
public function fetchAndTransformRow(int $id) public function fetchAndTransformRow(int $id)
{ {
$r = $this->fetchRow($id); $r = $this->fetchRow($id);
@ -290,12 +308,22 @@ class Changeset
return Column::columnsFromJson($this->revision->columns); return Column::columnsFromJson($this->revision->columns);
} }
public function fetchRow($id) /**
* 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) $r = $this->revision->rowsData($this->fetchColumns(), true, false)
->whereRaw("data->>'_id' = ?", $id)->first(); ->whereRaw("data->>'_id' = ?", $id)->first();
if (! $r) throw new NotExistException("No such row _id = $id in this revision."); if (!$r) throw new NotExistException("No such row _id = $id in this revision.");
// remove junk // remove junk
unset($r->pivot_revision_id); unset($r->pivot_revision_id);
@ -308,7 +336,7 @@ class Changeset
* Apply a row update, adding the row to the list of changes, or removing it * Apply a row update, adding the row to the list of changes, or removing it
* if all differences were undone. * if all differences were undone.
* *
* @param array|object $newVals * @param array|object $newVals - values of the new row
*/ */
public function rowUpdate($newVals) public function rowUpdate($newVals)
{ {
@ -334,11 +362,16 @@ class Changeset
} }
} }
if (!empty($updateObj)) { if ($this->isNewRow($_id)) {
$this->rowUpdates[$_id] = $updateObj; $this->newRows[$_id] = array_merge($this->newRows[$_id], $updateObj);
} else { }
// remove possible old update record for this row, if nothing changes now else {
unset($this->rowUpdates[$_id]); if (!empty($updateObj)) {
$this->rowUpdates[$_id] = $updateObj;
} else {
// remove possible old update record for this row, if nothing changes now
unset($this->rowUpdates[$_id]);
}
} }
} }
@ -348,8 +381,87 @@ class Changeset
* @param int $perPage * @param int $perPage
* @return \Illuminate\Pagination\LengthAwarePaginator|Collection|array * @return \Illuminate\Pagination\LengthAwarePaginator|Collection|array
*/ */
public function getAddedRows(int $perPage = 25) public function getAddedRows($perPage = 25)
{ {
return collection_paginate($this->newRows, $perPage); 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());
}
} }

@ -129,7 +129,7 @@ class Column implements JsonSerializable, Arrayable
{ {
$b = objBag($obj); $b = objBag($obj);
if (!$b->has('name')) throw new NotApplicableException('Missing name in column'); if (!$b->has('name') || $b->name == '') throw new NotApplicableException('Missing name in column');
if (!$b->has('type')) throw new NotApplicableException('Missing type in column'); if (!$b->has('type')) throw new NotApplicableException('Missing type in column');
if ($b->name[0] == '_') { // global row ID if ($b->name[0] == '_') { // global row ID

@ -17,36 +17,23 @@ use MightyPork\Utils\Utils;
* Thanks to this uniqueness, it could even be possible to compare or merge forks * Thanks to this uniqueness, it could even be possible to compare or merge forks
* of the same table. * of the same table.
*/ */
class ColumnNumerator class ColumnNumerator extends BaseNumerator
{ {
const ALPHABET = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', const ALPHABET = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']; 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
/** @var int */
private $next;
/** @var int */
private $last;
/** /**
* Create numerator for the given number of columns * Create numerator for the given number of columns
*
* @param int $capacity - how many
*/ */
public function __construct($capacity) public function __construct($capacity)
{ {
list($this->next, $this->last) = Row::allocateColIDs($capacity); parent::__construct(Row::allocateColIDs($capacity));
} }
/** protected function getKey($index)
* Get next column name, incrementing the internal state
*
* @return string
*/
public function next()
{ {
if ($this->next > $this->last) return Utils::alphabetEncode($index, self::ALPHABET);
throw new \OutOfBoundsException("Column numerator has run out of allocated GCID slots");
$key = Utils::alphabetEncode($this->next, self::ALPHABET);
$this->next++;
return $key;
} }
} }

@ -5,28 +5,15 @@ namespace App\Tables;
use App\Models\Row; use App\Models\Row;
class RowNumerator class RowNumerator extends BaseNumerator
{ {
/** @var int */
private $next;
/** @var int */
private $last;
/** /**
* Create a numerator for the given number of rows. * Create a numerator for the given number of rows.
* *
* @param int $capacity * @param int $capacity - how many
*/ */
public function __construct($capacity) public function __construct($capacity)
{ {
list($this->next, $this->last) = Row::allocateRowIDs($capacity); parent::__construct(Row::allocateRowIDs($capacity));
}
public function next()
{
if ($this->next > $this->last)
throw new \OutOfBoundsException("Row numerator has run out of allocated GRID slots");
return $this->next++;
} }
} }

@ -84,7 +84,13 @@ function old_json($name, $default) {
} }
// Safe JSON funcs // Safe JSON funcs
function toJSON($object) { function toJSON($object, $emptyObj=false) {
if ($emptyObj) {
if ((empty($object) || ($object instanceof \Illuminate\Support\Collection && $object->count()==0))) {
return '{}';
}
}
if (!$object instanceof JsonSerializable && $object instanceof \Illuminate\Contracts\Support\Arrayable) { if (!$object instanceof JsonSerializable && $object instanceof \Illuminate\Contracts\Support\Arrayable) {
$object = $object->toArray(); $object = $object->toArray();
} }

@ -215,6 +215,7 @@ return [
'URL' => Illuminate\Support\Facades\URL::class, 'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class, 'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class, 'View' => Illuminate\Support\Facades\View::class,
'Input' => \Illuminate\Support\Facades\Input::class,
// sideload // sideload
'SocialAuth' => AdamWathan\EloquentOAuth\Facades\OAuth::class, 'SocialAuth' => AdamWathan\EloquentOAuth\Facades\OAuth::class,

@ -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,
];

@ -2,6 +2,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "npm run development", "dev": "npm run development",
"dev-analyze": "WP_ANALYZE=1 npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "npm run development -- --watch", "watch": "npm run development -- --watch",
"watch-poll": "npm run watch -- --watch-poll", "watch-poll": "npm run watch -- --watch-poll",

@ -873,7 +873,7 @@ class Utils
* @param int|null $timestamp formatted timestamp, or null for current time * @param int|null $timestamp formatted timestamp, or null for current time
* @return string result * @return string result
*/ */
public static function fdate($format, $timestamp = null) public static function fdate(string $format, $timestamp = null)
{ {
if ($timestamp === null) $timestamp = time(); if ($timestamp === null) $timestamp = time();
@ -889,7 +889,7 @@ class Utils
* @param bool $rough get only approximate time (for estimate) * @param bool $rough get only approximate time (for estimate)
* @return string result * @return string result
*/ */
public static function ftime($secs, $rough = false) public static function ftime(int $secs, $rough = false)
{ {
$d = (int) ($secs / 86400); $d = (int) ($secs / 86400);
$secs -= $d * 86400; $secs -= $d * 86400;
@ -946,7 +946,7 @@ class Utils
* @param $time * @param $time
* @return int seconds * @return int seconds
*/ */
public static function strToSeconds($time) public static function strToSeconds(string $time)
{ {
// seconds pass through // seconds pass through
if (preg_match('/^\d+$/', trim("$time"))) { if (preg_match('/^\d+$/', trim("$time"))) {
@ -1013,4 +1013,9 @@ class Utils
return $key; return $key;
} }
public static function csvToArray(string $data)
{
return array_map('str_getcsv', explode("\n", $data));
}
} }

@ -4,7 +4,7 @@
<tr> <tr>
<th style="width:3rem" class="border-top-0"></th> <th style="width:3rem" class="border-top-0"></th>
<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)">{{col.title}}</th> <th v-for="col in columns" :class="colClasses(col)" :title="col.name">{{col.title}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -59,6 +59,7 @@ export default {
route: String, route: String,
xRows: Object, // key'd by _id xRows: Object, // key'd by _id
columns: Array, columns: Array,
lastPage: Boolean,
}, },
data: function() { data: function() {
return { return {
@ -83,13 +84,22 @@ export default {
}, },
toggleRowDelete(_id) { toggleRowDelete(_id) {
if (!_.isDefined(this.rows[_id])) return;
let remove = !this.rows[_id]._remove let remove = !this.rows[_id]._remove
this.query({ this.query({
action: remove ? 'row.remove' : 'row.restore', action: remove ? 'row.remove' : 'row.restore',
id: _id id: _id
}, (resp) => { }, (resp) => {
this.$set(this.rows, _id, resp.data) // 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)
}
}) })
}, },
@ -131,13 +141,15 @@ export default {
rowStyle(row) { rowStyle(row) {
return { return {
opacity: row._remove? .8 : 1, opacity: row._remove ? .8 : 1,
backgroundColor: row._remove? '#FFC4CC': 'transparent' backgroundColor:
row._remove ? '#FFC4CC':
'transparent'
} }
}, },
isChanged (row, colId) { isChanged (row, colId) {
return row._changed.indexOf(colId) > -1 return row._changed && row._changed.indexOf(colId) > -1
}, },
revertCell(row, colId) { revertCell(row, colId) {

@ -1,5 +1,12 @@
// subset of used lodash modules // subset of used lodash modules
export { default as each } from 'lodash/each'; export { default as each } from 'lodash/each'
export { default as isUndefined } from 'lodash/isUndefined'; export { default as isUndefined } from 'lodash/isUndefined'
export { default as merge } from 'lodash/merge'; 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 }

@ -1,9 +1,27 @@
window.Vue = require('vue'); window.Vue = require('vue');
Vue.component('column-editor', require('./components/ColumnEditor.vue')); const ColumnEditorCtor = Vue.component('column-editor', require('./components/ColumnEditor.vue'));
Vue.component('row-editor', require('./components/RowEditor.vue')); const RowsEditorCtor = Vue.component('rows-editor', require('./components/RowsEditor.vue'));
Vue.component('v-icon', require('./components/Icon.vue')); const IconCtor = Vue.component('v-icon', require('./components/Icon.vue'));
const app = new Vue({ // const app = new Vue({
el: '#app' // 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);
}
}

@ -12,6 +12,12 @@
<!-- Scripts --> <!-- Scripts -->
<script src="{{ asset('js/app.js') }}" defer></script> <script src="{{ asset('js/app.js') }}" defer></script>
<script>
function ready(fn) {
document.addEventListener('DOMContentLoaded', fn);
}
</script>
@stack('scripts')
<!-- Styles --> <!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet"> <link href="{{ asset('css/app.css') }}" rel="stylesheet">

@ -11,14 +11,16 @@
<table class="table table-hover table-sm"> <table class="table table-hover table-sm">
<thead> <thead>
<tr> <tr>
<th>#</th>
@foreach($columns as $col) @foreach($columns as $col)
<th>{{ $col->title }}</th> <th title="{{$col->name}}">{{ $col->title }}</th>
@endforeach @endforeach
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach($rows as $row) @foreach($rows as $row)
<tr> <tr>
<td>{{$row->_id}}</td>
@foreach($columns as $col) @foreach($columns as $col)
<td>{{ $row->{$col->name} }}</td> <td>{{ $row->{$col->name} }}</td>
@endforeach @endforeach

@ -30,23 +30,12 @@
->help('If you took the data from some external site, a book, etc., write it here. ->help('If you took the data from some external site, a book, etc., write it here.
URLs in a full format will be clickable.') !!} URLs in a full format will be clickable.') !!}
{{--!! Widget::textarea('columns', 'Columns')->value($exampleColumns)->height('8em')
->help('
<div class="text-left">
Column parameters in CSV format:
<ul class="pl-3 mb-0">
<li><b>column identifier</b><br>letters, numbers, underscore
<li><b>column data type</b><br>int, string, float, bool
<li><b>column title</b><br>used for display (optional)
</ul>
</div>') !!--}}
<div class="row form-group"> <div class="row form-group">
<label for="field-columns" class="col-md-3 col-form-label text-md-right"> <label for="field-columns" class="col-md-3 col-form-label text-md-right">
Columns Columns
</label> </label>
<div class="col-md-8"> <div class="col-md-8">
<column-editor name="columns" :initial-columns="{{old('columns', toJSON($columns))}}"></column-editor> <div id="column-editor"></div>
<noscript> <noscript>
You have JavaScript disabled; enter columns as JSON array<br> You have JavaScript disabled; enter columns as JSON array<br>
@ -84,3 +73,14 @@
</div> </div>
</form> </form>
@endsection @endsection
@push('scripts')
<script>
ready(function() {
app.ColumnEditor('#column-editor', {
name: 'columns',
initialColumns: {!! old('columns', toJSON($columns)) !!},
})
});
</script>
@endpush

@ -1,6 +1,52 @@
@php($tab='add-rows') @php
@extends('table.propose.layout') $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('tab-content') @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 @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

@ -4,9 +4,6 @@
/** @var \App\Tables\Changeset $changeset */ /** @var \App\Tables\Changeset $changeset */
/** @var \App\Models\Row[]|Illuminate\Pagination\Paginator $rows */ /** @var \App\Models\Row[]|Illuminate\Pagination\Paginator $rows */
/** @var \App\Models\Table $table */ /** @var \App\Models\Table $table */
/** @var \App\Tables\Column[] $columns */
/** @var \App\Tables\Changeset $changeset */
/** @var \App\Models\Table $table */
@endphp @endphp
@extends('table.propose.layout-row-pagination') @extends('table.propose.layout-row-pagination')
@ -19,9 +16,17 @@
}); });
@endphp @endphp
<table is="row-editor" <div id="rows-editor"></div>
route="{{$table->draftUpdateRoute}}"
:columns="{{toJSON($columns)}}"
:x-rows="{{toJSON($transformed)}}">
</table>
@stop @stop
@push('scripts')
<script>
ready(function() {
app.RowsEditor('#rows-editor', {
route: {!! toJSON($table->draftUpdateRoute) !!},
columns: {!! toJSON($columns) !!},
xRows: {!! toJSON($transformed, true) !!},
})
});
</script>
@endpush

@ -5,6 +5,10 @@
@extends('table.propose.layout') @extends('table.propose.layout')
@section('tab-content') @section('tab-content')
<div class="col-12">
@yield('header')
</div>
@if($rows->hasPages()) @if($rows->hasPages())
<div class="col-md-12 d-flex"> <div class="col-md-12 d-flex">
<nav class="text-center" aria-label="Pages of the table"> <nav class="text-center" aria-label="Pages of the table">

@ -14,9 +14,7 @@ if (!isset($tab) || $tab == '') $tab = 'edit-rows';
<small class="flex-grow-1" style="font-size: 120%;"> <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>{{-- <a href="{{ route('profile.view', $table->owner->name) }}" class="link-no-color">{{ $table->owner->handle }}</a>{{--
--}}<span class="px-1">/</span>{{-- --}}<span class="px-1">/</span>{{--
--}}<b> --}}<b><a href="{{ $table->viewRoute }}" class="link-no-color">{{ $table->name }}</a></b>
<a href="{{ $table->viewRoute }}" class="link-no-color">{{ $table->name }}</a>
</b>
</small> </small>
<h1 class="mx-3"> <h1 class="mx-3">

20
webpack.mix.js vendored

@ -13,21 +13,27 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl
| |
*/ */
let plugins = [
new webpack.ProvidePlugin({
'$': 'jquery',
jQuery: 'jquery',
}),
]
if (process.env.WP_ANALYZE) {
plugins.push(new BundleAnalyzerPlugin())
}
mix.webpackConfig({ mix.webpackConfig({
// Force jquery slim - https://code.luasoftware.com/tutorials/webpack/force-jquery-slim-import-in-webpack/ // Force jquery slim - https://code.luasoftware.com/tutorials/webpack/force-jquery-slim-import-in-webpack/
resolve: { resolve: {
extensions: ['.js'], extensions: ['.js'],
alias: { alias: {
'jquery': 'jquery/dist/jquery.slim.js', 'jquery': 'jquery/dist/jquery.slim.js',
'vue$': 'vue/dist/vue.runtime.js',
} }
}, },
plugins: [ plugins,
new webpack.ProvidePlugin({
'$': 'jquery',
jQuery: 'jquery',
}),
new BundleAnalyzerPlugin(),
],
}); });
mix.js('resources/assets/js/app.js', 'public/js') mix.js('resources/assets/js/app.js', 'public/js')

Loading…
Cancel
Save