Browse Source

CSV import, also from file, and more UX improvements

pull/35/head
Ondřej Hruška 4 years ago
parent
commit
a3df7a724c
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 4
      Makefile
  2. 1
      _json_typehints.php
  3. 59
      app/Http/Controllers/TableEditController.php
  4. 28
      app/Tables/Changeset.php
  5. 18
      app/View/WidgetFactory.php
  6. 21
      porklib/Utils/Utils.php
  7. 108
      public/fonts/fa-dtbl-1-preview.html
  8. 40
      public/fonts/fa-dtbl-1.css
  9. BIN
      public/fonts/fa-dtbl-1.eot
  10. 51
      public/fonts/fa-dtbl-1.svg
  11. BIN
      public/fonts/fa-dtbl-1.ttf
  12. BIN
      public/fonts/fa-dtbl-1.woff2
  13. 1
      resources/assets/fa-config/wanted.ini
  14. 26
      resources/assets/js/components/ColumnEditor.vue
  15. 114
      resources/assets/js/components/RowsEditor.vue
  16. 5
      resources/assets/js/components/_base.scss
  17. 49
      resources/views/table/propose/add-rows-csv.blade.php
  18. 1
      resources/views/table/propose/add-rows.blade.php

4
Makefile

@ -10,3 +10,7 @@ ana:
npm run dev-analyze
prod:
npm run prod
bc:
php artisan view:clear
php artisan cache:clear
php artisan ide-helper:generate

1
_json_typehints.php

@ -13,6 +13,7 @@ interface RowData {}
* @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 mixed[] $_loadvals - values after transformation, key by CID / for use by JS to detect changes since page load
* @property string[] $_changed - values that were changed
*/
interface DecoratedRow extends RowData {}

59
app/Http/Controllers/TableEditController.php

@ -60,8 +60,9 @@ class TableEditController extends Controller
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");
$method = camel_case($tab);
if (!method_exists($this, $method)) abort(404, "No such tab: $tab");
$changeset = $this->getChangeset($tableModel);
@ -69,7 +70,7 @@ class TableEditController extends Controller
dd($changeset);
}
return $this->{camel_case($tab)}($changeset);
return $this->$method($changeset);
}
/** @noinspection PhpUnusedPrivateMethodInspection */
@ -101,6 +102,18 @@ class TableEditController extends Controller
]);
}
/** @noinspection PhpUnusedPrivateMethodInspection */
private function addRowsCsv(Changeset $changeset)
{
$columns = $changeset->fetchAndTransformColumns();
return view('table.propose.add-rows-csv', [
'changeset' => $changeset,
'table' => $changeset->table,
'columns' => collect($columns),
]);
}
/** @noinspection PhpUnusedPrivateMethodInspection */
private function manageColumns(Changeset $changeset)
{
@ -143,13 +156,43 @@ class TableEditController extends Controller
$resp = $updated;
break;
case 'row.add-csv':
$fname = 'csv-file';
if ($request->hasFile($fname)) {
try {
$file = $request->file($fname);
if ($file->isValid() && $file->isReadable()) {
$handle = $file->openFile();
$csv = Utils::csvToArray($handle);
if (empty($csv)) throw new \Exception("Failed to parse CSV file.");
$changeset->addFilledRows($csv);
$handle = null;
} else {
throw new \Exception("File not valid.");
}
} catch (\Exception $e) {
return $this->backWithErrors(['csv-file' => $e->getMessage()]);
}
}
else {
try {
$text = trim($input->data);
if (empty($text)) throw new \Exception("Empty CSV field and no file uploaded.");
$changeset->addFilledRows(Utils::csvToArray($text));
} catch (\Exception $e) {
return $this->backWithErrors(['data' => $e->getMessage()]);
}
}
break;
case 'row.remove':
$isNew = $changeset->isNewRow($input->id);
$changeset->rowRemove($input->id);
$resp = $isNew ? null : $changeset->fetchAndTransformRow($input->id);
break;
case 'rows.remove-empty-new':
case 'row.remove-empty-new':
$changeset->removeEmptyNewRows();
break;
@ -162,14 +205,6 @@ class TableEditController extends Controller
$changeset->addBlankRows($input->count);
break;
case 'rows.add-csv':
try {
$changeset->addFilledRows(Utils::csvToArray($input->data));
} catch (\Exception $e) {
return $this->backWithErrors(['data' => $e->getMessage()]);
}
break;
case 'col.update':
$data = (object)$input->data;
$resp = $changeset->columnUpdate($data);

28
app/Tables/Changeset.php

@ -188,16 +188,19 @@ class Changeset
$row->_orig = [];
}
if ($decorate) {
$row->_orig = array_diff((array)$row, []);
// remove junk
unset($row->_orig['_id']);
unset($row->_orig['_new']);
unset($row->_orig['_remove']);
unset($row->_orig['_changed']);
unset($row->_orig['_orig']);
}
if ($this->isNewRow($row->_id)) {
if ($decorate) {
$row->_new = true;
$row->_orig = array_diff((array)$row, []);
// remove junk
unset($row->_orig['_id']);
unset($row->_orig['_new']);
unset($row->_orig['_remove']);
unset($row->_orig['_changed']);
unset($row->_orig['_orig']);
}
return $row;
}
@ -235,6 +238,16 @@ class Changeset
unset($row->_row_pivot);
if ($decorate) {
$row->_loadvals = array_diff((array)$row, []);
// remove junk
unset($row->_loadvals['_id']);
unset($row->_loadvals['_new']);
unset($row->_loadvals['_remove']);
unset($row->_loadvals['_changed']);
unset($row->_loadvals['_orig']);
}
return $row;
}
@ -582,6 +595,7 @@ class Changeset
'type' => "string",
'title' => "Column {$num}",
'id' => $cid,
'_new' => true,
];
$this->newColumns[$cid] = $col;

18
app/View/WidgetFactory.php

@ -38,6 +38,19 @@ class WidgetFactory
"</div>";
}
public function labeledPar($label, $text, $extraClasses='', $escape=true)
{
if (false === strpos($extraClasses, 'mb-')) $extraClasses .= ' mb-2';
return
"<div class=\"row\">".
"<label class=\"text-md-right col-form-label col-md-$this->labelCols\">".e($label)."</label>".
"<p class=\"form-control-plaintext col-md-$this->fieldCols".e($extraClasses)."\">".
($escape ? e($text) : $text) .
"</p>".
"</div>";
}
/**
* Convert the given string to a-href if it is a link.
*
@ -85,6 +98,11 @@ class WidgetFactory
return $this->baseWidget('input', $name, $label)->type('email');
}
public function file($name, $label)
{
return $this->baseWidget('input', $name, $label)->type('file');
}
public function checkbox($name, $label)
{
return (new CheckboxWidget('checkbox', $name, $label))

21
porklib/Utils/Utils.php

@ -1014,8 +1014,25 @@ class Utils
return $key;
}
public static function csvToArray(string $data)
/**
* @param \SplFileObject|string $data
* @return array
*/
public static function csvToArray($data)
{
return array_map('str_getcsv', explode("\n", $data));
if ($data instanceof \SplFileObject) {
$data->setFlags(\SplFileObject::SKIP_EMPTY | \SplFileObject::DROP_NEW_LINE);
$lines = [];
while (! $data->eof()) {
$line = $data->fgetcsv();
if ($line !== null) {
$lines[] = $line;
}
}
return $lines;
}
else {
return array_map('str_getcsv', explode("\n", $data));
}
}
}

108
public/fonts/fa-dtbl-1-preview.html

@ -184,6 +184,7 @@
.fa-key-modern:before,
.fa-link:before,
.fa-moon-o:before,
.fa-paper-plane-o:before,
.fa-pencil:before,
.fa-plus:before,
.fa-question-circle:before,
@ -198,6 +199,7 @@
.fa-th-list:before,
.fa-trash-o:before,
.fa-undo:before,
.fa-upload:before,
.fa-user:before,
.fa-user-circle-o:before,
.fa-user-plus:before,
@ -240,25 +242,27 @@
.fa-key-modern:before { content: "\f114"; }
.fa-link:before { content: "\f115"; }
.fa-moon-o:before { content: "\f116"; }
.fa-pencil:before { content: "\f117"; }
.fa-plus:before { content: "\f118"; }
.fa-question-circle:before { content: "\f119"; }
.fa-reply:before { content: "\f11a"; }
.fa-sign-in:before { content: "\f11b"; }
.fa-sign-out:before { content: "\f11c"; }
.fa-spinner:before { content: "\f11d"; }
.fa-star:before { content: "\f11e"; }
.fa-star-o:before { content: "\f11f"; }
.fa-sun-o:before { content: "\f120"; }
.fa-table:before { content: "\f121"; }
.fa-th-list:before { content: "\f122"; }
.fa-trash-o:before { content: "\f123"; }
.fa-undo:before { content: "\f124"; }
.fa-user:before { content: "\f125"; }
.fa-user-circle-o:before { content: "\f126"; }
.fa-user-plus:before { content: "\f127"; }
.fa-users:before { content: "\f128"; }
.fa-wrench:before { content: "\f129"; }
.fa-paper-plane-o:before { content: "\f117"; }
.fa-pencil:before { content: "\f118"; }
.fa-plus:before { content: "\f119"; }
.fa-question-circle:before { content: "\f11a"; }
.fa-reply:before { content: "\f11b"; }
.fa-sign-in:before { content: "\f11c"; }
.fa-sign-out:before { content: "\f11d"; }
.fa-spinner:before { content: "\f11e"; }
.fa-star:before { content: "\f11f"; }
.fa-star-o:before { content: "\f120"; }
.fa-sun-o:before { content: "\f121"; }
.fa-table:before { content: "\f122"; }
.fa-th-list:before { content: "\f123"; }
.fa-trash-o:before { content: "\f124"; }
.fa-undo:before { content: "\f125"; }
.fa-upload:before { content: "\f126"; }
.fa-user:before { content: "\f127"; }
.fa-user-circle-o:before { content: "\f128"; }
.fa-user-plus:before { content: "\f129"; }
.fa-users:before { content: "\f12a"; }
.fa-wrench:before { content: "\f12b"; }
</style>
<!--[if lte IE 8]><script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
@ -274,7 +278,7 @@
<body class="characters-off">
<div id="page" class="container">
<header>
<h1>fa-dtbl-1 contains 42 glyphs:</h1>
<h1>fa-dtbl-1 contains 44 glyphs:</h1>
<a onclick="toggleCharacters(); return false;" href="#">Toggle Preview Characters</a>
</header>
@ -581,6 +585,19 @@
</div>
</div>
<div class="glyph">
<div class="preview-glyphs">
<span class="step size-12"><span class="letters">Pp</span><i id="fa-paper-plane-o" class="fa-paper-plane-o"></i></span><span class="step size-14"><span class="letters">Pp</span><i id="fa-paper-plane-o" class="fa-paper-plane-o"></i></span><span class="step size-16"><span class="letters">Pp</span><i id="fa-paper-plane-o" class="fa-paper-plane-o"></i></span><span class="step size-18"><span class="letters">Pp</span><i id="fa-paper-plane-o" class="fa-paper-plane-o"></i></span><span class="step size-21"><span class="letters">Pp</span><i id="fa-paper-plane-o" class="fa-paper-plane-o"></i></span><span class="step size-24"><span class="letters">Pp</span><i id="fa-paper-plane-o" class="fa-paper-plane-o"></i></span><span class="step size-36"><span class="letters">Pp</span><i id="fa-paper-plane-o" class="fa-paper-plane-o"></i></span><span class="step size-48"><span class="letters">Pp</span><i id="fa-paper-plane-o" class="fa-paper-plane-o"></i></span><span class="step size-60"><span class="letters">Pp</span><i id="fa-paper-plane-o" class="fa-paper-plane-o"></i></span><span class="step size-72"><span class="letters">Pp</span><i id="fa-paper-plane-o" class="fa-paper-plane-o"></i></span>
</div>
<div class="preview-scale">
<span class="step">12</span><span class="step">14</span><span class="step">16</span><span class="step">18</span><span class="step">21</span><span class="step">24</span><span class="step">36</span><span class="step">48</span><span class="step">60</span><span class="step">72</span>
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-paper-plane-o" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf117;" />
</div>
</div>
<div class="glyph">
<div class="preview-glyphs">
<span class="step size-12"><span class="letters">Pp</span><i id="fa-pencil" class="fa-pencil"></i></span><span class="step size-14"><span class="letters">Pp</span><i id="fa-pencil" class="fa-pencil"></i></span><span class="step size-16"><span class="letters">Pp</span><i id="fa-pencil" class="fa-pencil"></i></span><span class="step size-18"><span class="letters">Pp</span><i id="fa-pencil" class="fa-pencil"></i></span><span class="step size-21"><span class="letters">Pp</span><i id="fa-pencil" class="fa-pencil"></i></span><span class="step size-24"><span class="letters">Pp</span><i id="fa-pencil" class="fa-pencil"></i></span><span class="step size-36"><span class="letters">Pp</span><i id="fa-pencil" class="fa-pencil"></i></span><span class="step size-48"><span class="letters">Pp</span><i id="fa-pencil" class="fa-pencil"></i></span><span class="step size-60"><span class="letters">Pp</span><i id="fa-pencil" class="fa-pencil"></i></span><span class="step size-72"><span class="letters">Pp</span><i id="fa-pencil" class="fa-pencil"></i></span>
@ -590,7 +607,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-pencil" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf117;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf118;" />
</div>
</div>
@ -603,7 +620,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-plus" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf118;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf119;" />
</div>
</div>
@ -616,7 +633,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-question-circle" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf119;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf11a;" />
</div>
</div>
@ -629,7 +646,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-reply" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf11a;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf11b;" />
</div>
</div>
@ -642,7 +659,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-sign-in" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf11b;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf11c;" />
</div>
</div>
@ -655,7 +672,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-sign-out" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf11c;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf11d;" />
</div>
</div>
@ -668,7 +685,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-spinner" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf11d;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf11e;" />
</div>
</div>
@ -681,7 +698,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-star" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf11e;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf11f;" />
</div>
</div>
@ -694,7 +711,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-star-o" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf11f;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf120;" />
</div>
</div>
@ -707,7 +724,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-sun-o" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf120;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf121;" />
</div>
</div>
@ -720,7 +737,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-table" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf121;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf122;" />
</div>
</div>
@ -733,7 +750,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-th-list" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf122;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf123;" />
</div>
</div>
@ -746,7 +763,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-trash-o" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf123;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf124;" />
</div>
</div>
@ -759,7 +776,20 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-undo" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf124;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf125;" />
</div>
</div>
<div class="glyph">
<div class="preview-glyphs">
<span class="step size-12"><span class="letters">Pp</span><i id="fa-upload" class="fa-upload"></i></span><span class="step size-14"><span class="letters">Pp</span><i id="fa-upload" class="fa-upload"></i></span><span class="step size-16"><span class="letters">Pp</span><i id="fa-upload" class="fa-upload"></i></span><span class="step size-18"><span class="letters">Pp</span><i id="fa-upload" class="fa-upload"></i></span><span class="step size-21"><span class="letters">Pp</span><i id="fa-upload" class="fa-upload"></i></span><span class="step size-24"><span class="letters">Pp</span><i id="fa-upload" class="fa-upload"></i></span><span class="step size-36"><span class="letters">Pp</span><i id="fa-upload" class="fa-upload"></i></span><span class="step size-48"><span class="letters">Pp</span><i id="fa-upload" class="fa-upload"></i></span><span class="step size-60"><span class="letters">Pp</span><i id="fa-upload" class="fa-upload"></i></span><span class="step size-72"><span class="letters">Pp</span><i id="fa-upload" class="fa-upload"></i></span>
</div>
<div class="preview-scale">
<span class="step">12</span><span class="step">14</span><span class="step">16</span><span class="step">18</span><span class="step">21</span><span class="step">24</span><span class="step">36</span><span class="step">48</span><span class="step">60</span><span class="step">72</span>
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-upload" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf126;" />
</div>
</div>
@ -772,7 +802,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-user" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf125;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf127;" />
</div>
</div>
@ -785,7 +815,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-user-circle-o" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf126;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf128;" />
</div>
</div>
@ -798,7 +828,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-user-plus" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf127;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf129;" />
</div>
</div>
@ -811,7 +841,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-users" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf128;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf12a;" />
</div>
</div>
@ -824,7 +854,7 @@
</div>
<div class="usage">
<input class="class" type="text" readonly="readonly" onClick="this.select();" value=".fa-wrench" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf129;" />
<input class="point" type="text" readonly="readonly" onClick="this.select();" value="&amp;#xf12b;" />
</div>
</div>

40
public/fonts/fa-dtbl-1.css vendored

@ -61,22 +61,24 @@
.fa-key-modern::before { content: "\f114"; }
.fa-link::before { content: "\f115"; }
.fa-moon-o::before { content: "\f116"; }
.fa-pencil::before { content: "\f117"; }
.fa-plus::before { content: "\f118"; }
.fa-question-circle::before { content: "\f119"; }
.fa-reply::before { content: "\f11a"; }
.fa-sign-in::before { content: "\f11b"; }
.fa-sign-out::before { content: "\f11c"; }
.fa-spinner::before { content: "\f11d"; }
.fa-star::before { content: "\f11e"; }
.fa-star-o::before { content: "\f11f"; }
.fa-sun-o::before { content: "\f120"; }
.fa-table::before { content: "\f121"; }
.fa-th-list::before { content: "\f122"; }
.fa-trash-o::before { content: "\f123"; }
.fa-undo::before { content: "\f124"; }
.fa-user::before { content: "\f125"; }
.fa-user-circle-o::before { content: "\f126"; }
.fa-user-plus::before { content: "\f127"; }
.fa-users::before { content: "\f128"; }
.fa-wrench::before { content: "\f129"; }
.fa-paper-plane-o::before { content: "\f117"; }
.fa-pencil::before { content: "\f118"; }
.fa-plus::before { content: "\f119"; }
.fa-question-circle::before { content: "\f11a"; }
.fa-reply::before { content: "\f11b"; }
.fa-sign-in::before { content: "\f11c"; }
.fa-sign-out::before { content: "\f11d"; }
.fa-spinner::before { content: "\f11e"; }
.fa-star::before { content: "\f11f"; }
.fa-star-o::before { content: "\f120"; }
.fa-sun-o::before { content: "\f121"; }
.fa-table::before { content: "\f122"; }
.fa-th-list::before { content: "\f123"; }
.fa-trash-o::before { content: "\f124"; }
.fa-undo::before { content: "\f125"; }
.fa-upload::before { content: "\f126"; }
.fa-user::before { content: "\f127"; }
.fa-user-circle-o::before { content: "\f128"; }
.fa-user-plus::before { content: "\f129"; }
.fa-users::before { content: "\f12a"; }
.fa-wrench::before { content: "\f12b"; }

BIN
public/fonts/fa-dtbl-1.eot

Binary file not shown.

51
public/fonts/fa-dtbl-1.svg

@ -5,7 +5,7 @@
-->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<metadata>
Created by FontForge 20170805 at Fri Aug 10 18:09:07 2018
Created by FontForge 20170805 at Fri Aug 10 18:42:51 2018
By ondra
The Fork Awesome font is licensed under the SIL OFL 1.1 (http://scripts.sil.org/OFL). Fork Awesome is a fork based of off Font Awesome 4.7.0 by Dave Gandy. More info on licenses at https://forkawesome.github.io
</metadata>
@ -19,10 +19,10 @@ The Fork Awesome font is licensed under the SIL OFL 1.1 (http://scripts.sil.org/
panose-1="2 0 5 3 0 0 0 0 0 0"
ascent="1536"
descent="-256"
bbox="-0.14014 -256.168 2048 1536.01"
bbox="-0.202124 -256.168 2048 1536.16"
underline-thickness="89.6"
underline-position="-179.2"
unicode-range="U+0020-F129"
unicode-range="U+0020-F12B"
/>
<missing-glyph />
<glyph glyph-name="space" unicode=" " horiz-adv-x="200"
@ -109,69 +109,76 @@ c0 76 31 150 85 203l147 146c54 54 127 83 203 83c77 0 150 -30 204 -85l206 -207c54
<glyph glyph-name="moon-o" unicode="&#xf116;" horiz-adv-x="1471"
d="M1262 233c-36 -6 -73 -9 -110 -9c-371 0 -672 301 -672 672c0 127 37 251 104 357c-266 -79 -456 -323 -456 -613c0 -353 287 -640 640 -640c193 0 374 88 494 233zM1465 318c-125 -271 -399 -446 -697 -446c-423 0 -768 345 -768 768c0 415 325 752 739 767
c28 1 51 -15 61 -39c11 -25 4 -54 -15 -72c-114 -104 -177 -246 -177 -400c0 -300 244 -544 544 -544c79 0 155 17 228 51c25 11 53 6 72 -13s24 -48 13 -72z" />
<glyph glyph-name="pencil" unicode="&#xf117;" horiz-adv-x="1515"
<glyph glyph-name="paper-plane-o" unicode="&#xf117;" horiz-adv-x="1792"
d="M1764 1525c21 -15 31 -39 27 -64l-256 -1536c-3 -19 -15 -35 -32 -45c-9 -5 -20 -8 -31 -8c-8 0 -16 2 -24 5l-527 215l-298 -327c-12 -14 -29 -21 -47 -21c-8 0 -16 1 -23 4c-25 10 -41 34 -41 60v452l-472 193c-23 9 -38 30 -40 55c-2 24 11 47 32 59l1664 960
c21 13 48 12 68 -2zM1422 26l221 1323l-1434 -827l336 -137l863 639l-478 -797z" />
<glyph glyph-name="pencil" unicode="&#xf118;" horiz-adv-x="1515"
d="M363 0l91 91l-235 235l-91 -91v-107h128v-128h107zM886 928c0 13 -9 22 -22 22c-6 0 -12 -2 -17 -7l-542 -542c-5 -5 -7 -11 -7 -17c0 -13 9 -22 22 -22c6 0 12 2 17 7l542 542c5 5 7 11 7 17zM832 1120l416 -416l-832 -832h-416v416zM1515 1024c0 -34 -14 -67 -37 -90
l-166 -166l-416 416l166 165c23 24 56 38 90 38s67 -14 91 -38l235 -234c23 -24 37 -57 37 -91z" />
<glyph glyph-name="plus" unicode="&#xf118;" horiz-adv-x="1408"
<glyph glyph-name="plus" unicode="&#xf119;" horiz-adv-x="1408"
d="M1408 800v-192c0 -53 -43 -96 -96 -96h-416v-416c0 -53 -43 -96 -96 -96h-192c-53 0 -96 43 -96 96v416h-416c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h416v416c0 53 43 96 96 96h192c53 0 96 -43 96 -96v-416h416c53 0 96 -43 96 -96z" />
<glyph glyph-name="question-circle" unicode="&#xf119;"
<glyph glyph-name="question-circle" unicode="&#xf11a;"
d="M896 160v192c0 18 -14 32 -32 32h-192c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h192c18 0 32 14 32 32zM1152 832c0 183 -192 320 -364 320c-163 0 -285 -70 -371 -213c-9 -14 -5 -32 8 -42l132 -100c5 -4 12 -6 19 -6c9 0 19 4 25 12c47 60 67 78 86 92
c17 12 50 24 86 24c64 0 123 -41 123 -85c0 -52 -27 -78 -88 -106c-71 -32 -168 -115 -168 -212v-36c0 -18 14 -32 32 -32h192c18 0 32 14 32 32c0 23 29 72 76 99c76 43 180 101 180 253zM1536 640c0 -424 -344 -768 -768 -768s-768 344 -768 768s344 768 768 768
s768 -344 768 -768z" />
<glyph glyph-name="reply" unicode="&#xf11a;" horiz-adv-x="1792"
<glyph glyph-name="reply" unicode="&#xf11b;" horiz-adv-x="1792"
d="M1792 416c0 -140 -70 -323 -127 -451c-11 -23 -22 -55 -37 -76c-7 -10 -14 -17 -28 -17c-20 0 -32 16 -32 35c0 16 4 34 5 50c3 41 5 82 5 123c0 477 -283 560 -714 560h-224v-256c0 -35 -29 -64 -64 -64c-17 0 -33 7 -45 19l-512 512c-12 12 -19 28 -19 45s7 33 19 45
l512 512c12 12 28 19 45 19c35 0 64 -29 64 -64v-256h224c328 0 736 -58 875 -403c42 -106 53 -221 53 -333z" />
<glyph glyph-name="sign-in" unicode="&#xf11b;"
<glyph glyph-name="sign-in" unicode="&#xf11c;"
d="M1184 640c0 -17 -7 -33 -19 -45l-544 -544c-12 -12 -28 -19 -45 -19c-35 0 -64 29 -64 64v288h-448c-35 0 -64 29 -64 64v384c0 35 29 64 64 64h448v288c0 35 29 64 64 64c17 0 33 -7 45 -19l544 -544c12 -12 19 -28 19 -45zM1536 992v-704c0 -159 -129 -288 -288 -288
h-320c-17 0 -32 15 -32 32c0 28 -13 96 32 96h320c88 0 160 72 160 160v704c0 88 -72 160 -160 160h-288c-25 0 -64 -5 -64 32c0 28 -13 96 32 96h320c159 0 288 -129 288 -288z" />
<glyph glyph-name="sign-out" unicode="&#xf11c;" horiz-adv-x="1568"
<glyph glyph-name="sign-out" unicode="&#xf11d;" horiz-adv-x="1568"
d="M640 96c0 -28 13 -96 -32 -96h-320c-159 0 -288 129 -288 288v704c0 159 129 288 288 288h320c17 0 32 -15 32 -32c0 -28 13 -96 -32 -96h-320c-88 0 -160 -72 -160 -160v-704c0 -88 72 -160 160 -160h288c25 0 64 5 64 -32zM1568 640c0 -17 -7 -33 -19 -45l-544 -544
c-12 -12 -28 -19 -45 -19c-35 0 -64 29 -64 64v288h-448c-35 0 -64 29 -64 64v384c0 35 29 64 64 64h448v288c0 35 29 64 64 64c17 0 33 -7 45 -19l544 -544c12 -12 19 -28 19 -45z" />
<glyph glyph-name="spinner" unicode="&#xf11d;" horiz-adv-x="1664"
<glyph glyph-name="spinner" unicode="&#xf11e;" horiz-adv-x="1664"
d="M462 142c0 -70 -57 -128 -128 -128c-70 0 -128 58 -128 128c0 71 58 128 128 128c71 0 128 -57 128 -128zM960 -64c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128s128 -57 128 -128zM256 640c0 -71 -57 -128 -128 -128s-128 57 -128 128s57 128 128 128
s128 -57 128 -128zM1458 142c0 -70 -58 -128 -128 -128c-71 0 -128 58 -128 128c0 71 57 128 128 128c70 0 128 -57 128 -128zM494 1138c0 -88 -72 -160 -160 -160s-160 72 -160 160s72 160 160 160s160 -72 160 -160zM1664 640c0 -71 -57 -128 -128 -128s-128 57 -128 128
s57 128 128 128s128 -57 128 -128zM1024 1344c0 -106 -86 -192 -192 -192s-192 86 -192 192s86 192 192 192s192 -86 192 -192zM1554 1138c0 -124 -101 -224 -224 -224c-124 0 -224 100 -224 224c0 123 100 224 224 224c123 0 224 -101 224 -224z" />
<glyph glyph-name="star" unicode="&#xf11e;" horiz-adv-x="1664"
<glyph glyph-name="star" unicode="&#xf11f;" horiz-adv-x="1664"
d="M1664 889c0 -18 -13 -35 -26 -48l-363 -354l86 -500c1 -7 1 -13 1 -20c0 -26 -12 -50 -41 -50c-14 0 -28 5 -40 12l-449 236l-449 -236c-13 -7 -26 -12 -40 -12c-29 0 -42 24 -42 50c0 7 1 13 2 20l86 500l-364 354c-12 13 -25 30 -25 48c0 30 31 42 56 46l502 73
l225 455c9 19 26 41 49 41s40 -22 49 -41l225 -455l502 -73c24 -4 56 -16 56 -46z" />
<glyph glyph-name="star-o" unicode="&#xf11f;" horiz-adv-x="1664"
<glyph glyph-name="star-o" unicode="&#xf120;" horiz-adv-x="1664"
d="M1137 532l306 297l-422 62l-189 382l-189 -382l-422 -62l306 -297l-73 -421l378 199l377 -199zM1664 889c0 -18 -13 -35 -26 -48l-363 -354l86 -500c1 -7 1 -13 1 -20c0 -27 -12 -50 -41 -50c-14 0 -28 5 -40 12l-449 236l-449 -236c-13 -7 -26 -12 -40 -12
c-29 0 -42 24 -42 50c0 7 1 13 2 20l86 500l-364 354c-12 13 -25 30 -25 48c0 30 31 42 56 46l502 73l225 455c9 19 26 41 49 41s40 -22 49 -41l225 -455l502 -73c24 -4 56 -16 56 -46z" />
<glyph glyph-name="sun-o" unicode="&#xf120;" horiz-adv-x="1707"
<glyph glyph-name="sun-o" unicode="&#xf121;" horiz-adv-x="1707"
d="M1430 640c0 318 -258 576 -576 576s-576 -258 -576 -576s258 -576 576 -576s576 258 576 576zM1706 363c-3 -10 -11 -17 -20 -20l-292 -96v-306c0 -10 -5 -20 -13 -26c-9 -6 -19 -8 -29 -4l-292 94l-180 -248c-6 -8 -16 -13 -26 -13s-20 5 -26 13l-180 248l-292 -94
c-10 -4 -20 -2 -29 4c-8 6 -13 16 -13 26v306l-292 96c-9 3 -17 10 -20 20s-2 21 4 29l180 248l-180 248c-6 9 -7 19 -4 29s11 17 20 20l292 96v306c0 10 5 20 13 26c9 6 19 8 29 4l292 -94l180 248c12 16 40 16 52 0l180 -248l292 94c10 4 20 2 29 -4c8 -6 13 -16 13 -26
v-306l292 -96c9 -3 17 -10 20 -20s2 -20 -4 -29l-180 -248l180 -248c6 -8 7 -19 4 -29z" />
<glyph glyph-name="table" unicode="&#xf121;" horiz-adv-x="1664"
<glyph glyph-name="table" unicode="&#xf122;" horiz-adv-x="1664"
d="M512 160v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM512 544v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM1024 160v192c0 18 -14 32 -32 32h-320
c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM512 928v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM1024 544v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192
c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM1536 160v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM1024 928v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32z
M1536 544v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM1536 928v192c0 18 -14 32 -32 32h-320c-18 0 -32 -14 -32 -32v-192c0 -18 14 -32 32 -32h320c18 0 32 14 32 32zM1664 1248v-1088c0 -88 -72 -160 -160 -160
h-1344c-88 0 -160 72 -160 160v1088c0 88 72 160 160 160h1344c88 0 160 -72 160 -160z" />
<glyph glyph-name="th-list" unicode="&#xf122;" horiz-adv-x="1792"
<glyph glyph-name="th-list" unicode="&#xf123;" horiz-adv-x="1792"
d="M512 288v-192c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h320c53 0 96 -43 96 -96zM512 800v-192c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h320c53 0 96 -43 96 -96zM1792 288v-192c0 -53 -43 -96 -96 -96h-960
c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h960c53 0 96 -43 96 -96zM512 1312v-192c0 -53 -43 -96 -96 -96h-320c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h320c53 0 96 -43 96 -96zM1792 800v-192c0 -53 -43 -96 -96 -96h-960c-53 0 -96 43 -96 96v192c0 53 43 96 96 96
h960c53 0 96 -43 96 -96zM1792 1312v-192c0 -53 -43 -96 -96 -96h-960c-53 0 -96 43 -96 96v192c0 53 43 96 96 96h960c53 0 96 -43 96 -96z" />
<glyph glyph-name="trash-o" unicode="&#xf123;" horiz-adv-x="1408"
<glyph glyph-name="trash-o" unicode="&#xf124;" horiz-adv-x="1408"
d="M512 800v-576c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v576c0 18 14 32 32 32h64c18 0 32 -14 32 -32zM768 800v-576c0 -18 -14 -32 -32 -32h-64c-18 0 -32 14 -32 32v576c0 18 14 32 32 32h64c18 0 32 -14 32 -32zM1024 800v-576c0 -18 -14 -32 -32 -32h-64
c-18 0 -32 14 -32 32v576c0 18 14 32 32 32h64c18 0 32 -14 32 -32zM1152 76v948h-896v-948c0 -48 27 -76 32 -76h832c5 0 32 28 32 76zM480 1152h448l-48 117c-3 4 -12 10 -17 11h-317c-6 -1 -14 -7 -17 -11zM1408 1120v-64c0 -18 -14 -32 -32 -32h-96v-948
c0 -110 -72 -204 -160 -204h-832c-88 0 -160 90 -160 200v952h-96c-18 0 -32 14 -32 32v64c0 18 14 32 32 32h309l70 167c20 49 80 89 133 89h320c53 0 113 -40 133 -89l70 -167h309c18 0 32 -14 32 -32z" />
<glyph glyph-name="undo" unicode="&#xf124;"
<glyph glyph-name="undo" unicode="&#xf125;"
d="M1536 640c0 -423 -345 -768 -768 -768c-229 0 -445 101 -591 277c-10 13 -9 32 2 43l137 138c7 6 16 9 25 9c9 -1 18 -5 23 -12c98 -127 245 -199 404 -199c282 0 512 230 512 512s-230 512 -512 512c-131 0 -255 -50 -348 -137l137 -138c19 -18 24 -46 14 -69
c-10 -24 -33 -40 -59 -40h-448c-35 0 -64 29 -64 64v448c0 26 16 49 40 59c23 10 51 5 69 -14l130 -129c141 133 332 212 529 212c423 0 768 -345 768 -768z" />
<glyph glyph-name="user" unicode="&#xf125;" horiz-adv-x="1280"
<glyph glyph-name="upload" unicode="&#xf126;" horiz-adv-x="1664"
d="M1280 64c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1536 64c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1664 288v-320c0 -53 -43 -96 -96 -96h-1472c-53 0 -96 43 -96 96v320c0 53 43 96 96 96h427c27 -74 98 -128 181 -128
h256c83 0 154 54 181 128h427c53 0 96 -43 96 -96zM1339 936c-10 -24 -33 -40 -59 -40h-256v-448c0 -35 -29 -64 -64 -64h-256c-35 0 -64 29 -64 64v448h-256c-26 0 -49 16 -59 40c-10 23 -5 51 14 69l448 448c12 13 29 19 45 19s33 -6 45 -19l448 -448
c19 -18 24 -46 14 -69z" />
<glyph glyph-name="user" unicode="&#xf127;" horiz-adv-x="1280"
d="M1280 137c0 -146 -96 -265 -213 -265h-854c-117 0 -213 119 -213 265c0 263 65 567 327 567c81 -79 191 -128 313 -128s232 49 313 128c262 0 327 -304 327 -567zM1024 1024c0 -212 -172 -384 -384 -384s-384 172 -384 384s172 384 384 384s384 -172 384 -384z" />
<glyph glyph-name="user-circle-o" unicode="&#xf126;" horiz-adv-x="1792"
<glyph glyph-name="user-circle-o" unicode="&#xf128;" horiz-adv-x="1792"
d="M896 1536c495 0 896 -401 896 -896c0 -492 -399 -896 -896 -896c-496 0 -896 403 -896 896c0 495 401 896 896 896zM1515 185c93 128 149 285 149 455c0 423 -345 768 -768 768s-768 -345 -768 -768c0 -170 56 -327 149 -455c36 179 123 327 306 327
c81 -79 191 -128 313 -128s232 49 313 128c183 0 270 -148 306 -327zM1280 832c0 -212 -172 -384 -384 -384s-384 172 -384 384s172 384 384 384s384 -172 384 -384z" />
<glyph glyph-name="user-plus" unicode="&#xf127;" horiz-adv-x="2048"
<glyph glyph-name="user-plus" unicode="&#xf129;" horiz-adv-x="2048"
d="M704 640c-212 0 -384 172 -384 384s172 384 384 384s384 -172 384 -384s-172 -384 -384 -384zM1664 512h352c17 0 32 -15 32 -32v-192c0 -17 -15 -32 -32 -32h-352v-352c0 -17 -15 -32 -32 -32h-192c-17 0 -32 15 -32 32v352h-352c-17 0 -32 15 -32 32v192
c0 17 15 32 32 32h352v352c0 17 15 32 32 32h192c17 0 32 -15 32 -32v-352zM928 288c0 -70 58 -128 128 -128h256v-238c-49 -36 -111 -50 -171 -50h-874c-160 0 -267 96 -267 259c0 226 53 573 346 573c16 0 27 -7 39 -17c98 -75 193 -122 319 -122s221 47 319 122
c12 10 23 17 39 17c85 0 160 -32 217 -96h-223c-70 0 -128 -58 -128 -128v-192z" />
<glyph glyph-name="users" unicode="&#xf128;" horiz-adv-x="1920"
<glyph glyph-name="users" unicode="&#xf12a;" horiz-adv-x="1920"
d="M593 640c-104 -3 -198 -48 -265 -128h-134c-100 0 -194 48 -194 159c0 81 -3 353 124 353c21 0 125 -85 260 -85c46 0 90 8 133 23c-3 -22 -5 -44 -5 -66c0 -91 29 -181 81 -256zM1664 3c0 -162 -107 -259 -267 -259h-874c-160 0 -267 97 -267 259c0 226 53 573 346 573
c34 0 158 -139 358 -139s324 139 358 139c293 0 346 -347 346 -573zM640 1280c0 -141 -115 -256 -256 -256s-256 115 -256 256s115 256 256 256s256 -115 256 -256zM1344 896c0 -212 -172 -384 -384 -384s-384 172 -384 384s172 384 384 384s384 -172 384 -384zM1920 671
c0 -111 -94 -159 -194 -159h-134c-67 80 -161 125 -265 128c52 75 81 165 81 256c0 22 -2 44 -5 66c43 -15 87 -23 133 -23c135 0 239 85 260 85c127 0 124 -272 124 -353zM1792 1280c0 -141 -115 -256 -256 -256s-256 115 -256 256s115 256 256 256s256 -115 256 -256z" />
<glyph glyph-name="wrench" unicode="&#xf129;" horiz-adv-x="1641"
<glyph glyph-name="wrench" unicode="&#xf12b;" horiz-adv-x="1641"
d="M363 64c0 35 -29 64 -64 64s-64 -29 -64 -64s29 -64 64 -64s64 29 64 64zM1007 484l-682 -682c-23 -23 -56 -37 -90 -37s-67 14 -91 37l-106 108c-24 23 -38 56 -38 90s14 67 38 91l681 681c52 -131 157 -236 288 -288zM1641 919c0 -33 -12 -74 -23 -106
c-63 -178 -234 -301 -423 -301c-247 0 -448 201 -448 448s201 448 448 448c73 0 168 -22 229 -63c10 -7 16 -16 16 -28c0 -11 -7 -22 -16 -28l-293 -169v-224l193 -107c33 19 265 165 285 165s32 -15 32 -35z" />
</font>

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

BIN
public/fonts/fa-dtbl-1.ttf

Binary file not shown.

BIN
public/fonts/fa-dtbl-1.woff2

Binary file not shown.

1
resources/assets/fa-config/wanted.ini

@ -44,6 +44,7 @@ undo # Used in table editor
spinner
paper-plane-o # save proposal
upload # Upload a file
# dark mode
sun-o

26
resources/assets/js/components/ColumnEditor.vue

@ -5,7 +5,12 @@ Complex animated column editor for the table edit page
<template>
<div>
<input type="hidden" :name="name" :value="JSON.stringify(columns)" v-if="newTable">
<table :class="[{'table': !newTable}, {'new-table': newTable}, {'mt-3': !newTable}, 'table-narrow', 'table-sm', 'table-fixed', 'td-va-middle']">
<table :class="[
{'table': !newTable},
{'new-table': newTable},
{'mt-3': !newTable},
'table-narrow', 'table-sm', 'table-fixed', 'td-va-middle'
]">
<thead>
<tr>
<th v-if="sortable"></th>
@ -16,7 +21,12 @@ Complex animated column editor for the table edit page
</tr>
</thead>
<transition-group name="col-list" tag="tbody" ref="col-list">
<tr v-for="(col, i) in columns" :key="col.id" :ref="`col${i}`" :class="{dragging: col._dragging, 'text-success': col._new}">
<tr v-for="(col, i) in columns" :key="col.id" :ref="`col${i}`"
:class="{
dragging: col._dragging,
'text-success': col._new,
remove: col._remove
}">
<td v-if="sortable">
<span class="btn-group">
<button type="button" class="btn btn-outline-secondary drag-btn"
@ -74,17 +84,17 @@ Complex animated column editor for the table edit page
<td class="text-nowrap"><!--
Save button
--><a href="" :class="['mr-1','btn','btn-outline-secondary',{'disabled': col._remove}]"
--><a href="" :class="['mr-1', 'btn', 'btn-outline-secondary', {'disabled': col._remove}]"
v-if="!newTable"
@click.prevent="toggleColEditing(col)">
<v-icon :class="col._editing ? 'fa-save' : 'fa-pencil'"
:alt="col._editing ? 'Save' : 'Edit'" />
<v-icon v-if="col._editing" class="fa-save" alt="Save" />
<v-icon v-else class="fa-pencil" alt="Edit" />
</a><!--
Delete button
--><button type="button" :class="delBtnClass(col)"
@click.prevent="toggleColDelete(col)">
<v-icon :class="col._remove ? 'fa-undo' : 'fa-trash-o'"
:alt="col._remove ? 'Undo Remove' : 'Remove'" />
<v-icon v-if="col._remove" class="fa-undo" alt="Undo Remove" />
<v-icon v-else class="fa-trash-o" alt="Remove" />
</button><!--
Add button
--><button type="button" :class="['x-add-btn', 'btn', 'btn-outline-secondary']"
@ -387,7 +397,7 @@ export default {
if (cell == 'title') w = '14'
return {width: `${w}rem`};
}
},
}
}
</script>

114
resources/assets/js/components/RowsEditor.vue

@ -11,7 +11,7 @@ Rows are identified by row._id, columns by col.id
<template>
<div>
<div class="col-md-12 d-flex mt-3" v-if="newRows || hasPages">
<div class="col-md-12 d-flex mt-3">
<div v-if="hasPages" v-html="pager"></div>
<div class="flex-grow-1"></div>
@ -21,52 +21,68 @@ Rows are identified by row._id, columns by col.id
<input type="hidden" name="action" value="rows.add">
<input type="hidden" name="redirect" :value="pageUrl">
<input name="count" id="newrow-count" type="number"
min=1 step=1 value=1 class="form-control mr-1" style="width: 10em">
min=1 step=1 value=1 class="form-control mr-1" style="width: 8em">
<button class="btn btn-outline-success">Add Rows</button>
</form>
<a href="" class="btn btn-outline-success mr-2" v-if="newRows">
<a :href="loadCsvUrl" class="btn btn-outline-success mr-2" v-if="newRows && loadCsvUrl">
<v-icon class="fa-file-excel-o fa-pr" alt="CSV"></v-icon><!--
-->CSV
</a>
<form :action="route" method="POST" v-if="newRows" class="form-inline mr-2">
<input type="hidden" name="_token" :value="csrf">
<input type="hidden" name="action" value="rows.remove-empty-new">
<input type="hidden" name="action" value="row.remove-empty-new">
<input type="hidden" name="redirect" :value="pageUrl">
<button class="btn btn-outline-danger">Remove Empty</button>
</form>
<button @click="saveAllChanges" type="button" v-if="newRows"
<button @click="saveAllChanges" type="button"
:class="['btn', this.dirtyRows ? 'btn-info' : 'btn-outline-secondary']">
Save Rows
</button>
</div>
<div class="col-md-12 mt-3">
<table class="table table-hover table-sm table-fixed td-va-middle">
<table :class="[
'table', 'table-sm', 'table-fixed', 'td-va-middle',
{'new-rows': newRows}
]">
<thead>
<tr>
<th style="width:6rem"></th>
<th v-for="col in columns" :class="colClasses(col)" :title="col.name">{{col.title}}</th>
<th style="width:2rem"><!-- revert icon --></th>
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :style="rowStyle(row)">
<tr v-for="row in rows" :class="row._remove ? 'remove' : ''">
<td class="text-nowrap">
<button :class="['mr-1', 'btn', 'btn-outline-danger', {'active': row._remove}]"
<button :class="[
'mr-1', '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'" />
</button><!--
--><button v-if="newRows" :class="['mr-1', 'btn', isRowChanged(row) ? 'btn-info' : 'btn-outline-secondary']"
--><button v-if="newRows"
:class="[
'mr-1', 'btn',
isRowChanged(row) ? 'btn-info' : 'btn-outline-secondary'
]"
@click.prevent="submitRowChange(row)">
<v-icon class="fa-save" alt="Save" />
</button><!--
--><button v-else :class="['btn', 'btn-outline-secondary', {'disabled': row._remove}]"
--><button v-else
:class="[
'btn',
isRowChanged(row) ? 'btn-info' : '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'" />
<v-icon v-if="row._editing" class="fa-save" alt="Save" />
<v-icon v-else class="fa-pencil" alt="Edit" />
</button>
</td>
@ -90,6 +106,13 @@ Rows are identified by row._id, columns by col.id
alt="Revert Change" />
</td>
</template>
<td style="text-align: center;">
<v-icon @click="discardEdit(row)"
v-if="isRowChanged(row) && (row._editing || newRows)"
class="fa-undo text-danger pointer"
alt="Revert Change" />
</td>
</tr>
</tbody>
</table>
@ -107,6 +130,10 @@ Rows are identified by row._id, columns by col.id
th {
border-top: 0 none;
}
table.new-rows td {
border: 0 none;
}
</style>
<script>
@ -128,6 +155,8 @@ Rows are identified by row._id, columns by col.id
csrf: String,
/** Full URL of the current page */
pageUrl: String,
/** URL to import CSV */
loadCsvUrl: String,
/** Indicate this is the Add Rows page */
newRows: {
type: Boolean,
@ -144,12 +173,14 @@ Rows are identified by row._id, columns by col.id
},
watch: {
dirtyRows: function (value) {
console.log(`Dirty Rows ${value}`)
if (value > 0) {
$(window).on('beforeunload', function () {
return 'Some row changes are not saved.';
});
return 'Some row changes are not saved.'
})
} else {
$(window).off('beforeunload');
$(window).off('beforeunload')
}
}
},
@ -222,24 +253,24 @@ Rows are identified by row._id, columns by col.id
}
},
/** Compute style for a row */
rowStyle (row) {
return {
opacity: row._remove ? .8 : 1,
backgroundColor:
row._remove ? '#FFC4CC' :
'transparent'
}
},
/** Test if a value cell is changed */
isChanged (row, colId) {
return row._changed && row._changed.indexOf(colId) > -1
},
/** Test if any field in the row is changed */
isRowChanged(row) {
return row._changed && row._changed.length
isRowChanged (row, cached = false) {
if (this.newRows) {
return row._changed && row._changed.length
} else {
if (cached) return row._dirty
// check against loadvals array
for (const [key, value] of Object.entries(row._loadvals)) {
// changed if differs from orig value and also from previous value from revision
if (row[key] != value && row[key] != row._orig[key]) return true
}
return false
}
},
/** Revert a value cell */
@ -247,10 +278,9 @@ Rows are identified by row._id, columns by col.id
this.submitRowChange(_.merge({}, row, {[colId]: row._orig[colId]}))
},
markRowDirty(row) {
if (!this.newRows) return
let wasDirty = this.isRowChanged(row)
/** Mark row as dirty and needing a save */
markRowDirty (row) {
let wasDirty = this.isRowChanged(row, true)
let changes = []
_.each(row._orig, (v, k) => {
@ -259,14 +289,17 @@ Rows are identified by row._id, columns by col.id
}
})
this.rows[row._id]._changed = changes;
this.rows[row._id]._changed = changes
let isDirty = this.isRowChanged(row)
if (wasDirty && !isDirty) this.dirtyRows--
else if (!wasDirty && isDirty) this.dirtyRows++
row._dirty = isDirty
},
saveAllChanges() {
/** Save all changed rows */
saveAllChanges () {
let toChange = _.filter(this.rows, (r) => {
return this.isRowChanged(r)
})
@ -286,6 +319,21 @@ Rows are identified by row._id, columns by col.id
// this.$set(this.rows[row._id], '_errors', er.errors)
// }
})
},
discardEdit (row) {
let wasDirty = this.isRowChanged(row)
_.each(this.newRows ? row._orig : row._loadvals, (val, id) => {
row[id] = val
})
row._editing = false
row._dirty = false
if (this.newRows) row._changed = []
this.$set(this.rows, row._id, row)
if (wasDirty) this.dirtyRows--
}
}
}

5
resources/assets/js/components/_base.scss vendored

@ -1 +1,6 @@
@import "../../sass/bootstrap-base";
tr.remove, tr.remove:hover {
//opacity: .8;
background-color: #FFC4CC;
}

49
resources/views/table/propose/add-rows-csv.blade.php

@ -8,19 +8,42 @@
@extends('table.propose.layout')
@section('tab-content')
<div class="form-inline py-2 px-1">
<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>
<form action="{{$table->draftUpdateRoute}}" method="POST" class="form col-md-12 mt-3" enctype="multipart/form-data">
@csrf
<input type="hidden" name="action" value="row.add-csv">
<input type="hidden" name="redirect" value="{{$table->getDraftRoute('add-rows')}}">
<?php Widget::setLayout(3,7) ?>
{!! Widget::header(3, "Import CSV") !!}
@php
$cols = [];
foreach ($columns as $column) {
$cols[] = '<b>'.e($column->title) . '</b>';
}
@endphp
{!! Widget::par('Append rows from pasted CSV lines or uploaded CSV file') !!}
{{-- TODO interactive widget to select which cols are included, and in which order --}}
{!! Widget::labeledPar('Columns', implode(', ', $cols), '', false) !!}
{!! Widget::textarea('data', 'CSV as text')->help('
Comma-separated values, one row per line.
Do not put spaces after commas, they would be
included in the values.
')->minHeight('10em') !!}
{!! Widget::file('csv-file', 'CSV file')->accept("text/csv") !!}
<div class="row form-group">
<div class="col-md-7 offset-md-3">
<button class="btn btn-outline-success">
@icon(fa-upload fa-pr)Upload
</button>
</div>
</div>
</form>
@stop
@push('scripts')

1
resources/views/table/propose/add-rows.blade.php

@ -32,6 +32,7 @@
xRows: {!! toJSON($xrows, true) !!},
newRows: true, // indicate all are new
pageUrl: @json(request()->fullUrl()),
loadCsvUrl: @json($table->getDraftRoute('add-rows-csv')),
csrf: @json(csrf_token()),
})

Loading…
Cancel
Save