Add support for linking dashboard tables, adding lists connected to other tables, formatted currency inputs, multi-line text fields without markdown, paginated edit lists (with working search), errors for columns not being unique or required (with modal popup), improve comments, and use links instead of javascript for the edit and new buttons

This commit is contained in:
Kevin MacMartin 2020-04-24 00:22:42 -04:00
parent bf1b3ea9aa
commit 7f9f9bce1b
14 changed files with 893 additions and 96 deletions

View file

@ -52,13 +52,19 @@ class DashboardController extends Controller {
$model_class = Dashboard::getModel($model, 'edit'); $model_class = Dashboard::getModel($model, 'edit');
if ($model_class != null) { if ($model_class != null) {
$data = $model_class::getDashboardData(true);
return view('dashboard.pages.edit-list', [ return view('dashboard.pages.edit-list', [
'heading' => $model_class::getDashboardHeading($model), 'heading' => $model_class::getDashboardHeading($model),
'model' => $model, 'model' => $model,
'rows' => $model_class::getDashboardData(), 'rows' => $data['rows'],
'paramdisplay' => $data['paramdisplay'],
'query' => $model_class::getQueryString(),
'display' => $model_class::$dashboard_display, 'display' => $model_class::$dashboard_display,
'button' => $model_class::$dashboard_button, 'button' => $model_class::$dashboard_button,
'idlink' => $model_class::$dashboard_id_link,
'sortcol' => $model_class::$dashboard_reorder ? $model_class::$dashboard_sort_column : false, 'sortcol' => $model_class::$dashboard_reorder ? $model_class::$dashboard_sort_column : false,
'paginate' => $model_class::$items_per_page !== 0,
'create' => $model_class::$create, 'create' => $model_class::$create,
'delete' => $model_class::$delete, 'delete' => $model_class::$delete,
'filter' => $model_class::$filter, 'filter' => $model_class::$filter,
@ -81,6 +87,13 @@ class DashboardController extends Controller {
if ($model_class::where('id', $id)->exists()) { if ($model_class::where('id', $id)->exists()) {
$item = $model_class::find($id); $item = $model_class::find($id);
foreach ($model_class::$dashboard_columns as $column) {
if ($column['type'] === 'list') {
$list_model_class = 'App\\Models\\' . $column['model'];
$item->{$column['name']} = $list_model_class::where($column['foreign'], $item->id)->orderBy($column['sort'])->get();
}
}
if (is_null($item) || !$item->userCheck()) { if (is_null($item) || !$item->userCheck()) {
return view('errors.no-such-record'); return view('errors.no-such-record');
} }
@ -181,12 +194,83 @@ class DashboardController extends Controller {
} }
} }
// populate the eloquent object with the remaining items in $request // check to ensure required columns have values
foreach ($request['columns'] as $column) { $empty = [];
$item->$column = $request[$column];
foreach ($model_class::$dashboard_columns as $column) {
if ($request->has($column['name']) && array_key_exists('required', $column) && $column['required'] && ($request[$column['name']] == '' || $request[$column['name']] == null)) {
if (array_key_exists('title', $column)) {
array_push($empty, "'" . $column['title'] . "'");
} else {
array_push($empty, "'" . ucfirst($column['name']) . "'");
}
}
} }
// save the new or updated item if (count($empty) > 0) {
return 'required:' . implode(',', $empty);
}
// check to ensure unique columns are unique
$not_unique = [];
foreach ($model_class::$dashboard_columns as $column) {
if ($request->has($column['name']) && array_key_exists('unique', $column) && $column['unique'] && $model_class::where($column['name'], $request[$column['name']])->where('id', '!=', $item->id)->exists()) {
if (array_key_exists('title', $column)) {
array_push($not_unique, "'" . $column['title'] . "'");
} else {
array_push($not_unique, "'" . ucfirst($column['name']) . "'");
}
}
}
if (count($not_unique) > 0) {
return 'not-unique:' . implode(',', $not_unique);
}
// populate the eloquent object with the non-list items in $request
foreach ($request['columns'] as $column) {
if ($column['type'] !== 'list') {
$column_name = $column['name'];
$item->$column_name = $request[$column_name];
}
}
// save the item if it's new so we can access its id
if ($request['id'] == 'new') {
$item->save();
}
// populate connected lists with list items in $request
foreach ($request['columns'] as $column) {
if ($column['type'] === 'list') {
$column_name = $column['name'];
foreach ($model_class::$dashboard_columns as $dashboard_column) {
if ($dashboard_column['name'] === $column_name) {
$foreign = $dashboard_column['foreign'];
$list_model_class = 'App\\Models\\' . $dashboard_column['model'];
$list_model_class::where($foreign, $item->id)->delete();
if ($request->has($column_name)) {
foreach ($request[$column_name] as $index => $row) {
$list_model_item = new $list_model_class;
$list_model_item->$foreign = $item->id;
$list_model_item->{$dashboard_column['sort']} = $index;
foreach ($row as $key => $value) {
$list_model_item->$key = $value;
}
$list_model_item->save();
}
}
}
}
}
}
// update the item
$item->save(); $item->save();
// return the id number in the format '^id:[0-9][0-9]*$' on success // return the id number in the format '^id:[0-9][0-9]*$' on success

View file

@ -9,6 +9,8 @@ class Blog extends DashboardModel
{ {
protected $table = 'blog'; protected $table = 'blog';
public static $items_per_page = 10;
public static $dashboard_type = 'edit'; public static $dashboard_type = 'edit';
public static $dashboard_help_text = '<strong>NOTE</strong>: Tags should be separated by semicolons'; public static $dashboard_help_text = '<strong>NOTE</strong>: Tags should be separated by semicolons';
@ -18,10 +20,10 @@ class Blog extends DashboardModel
public static $dashboard_columns = [ public static $dashboard_columns = [
[ 'name' => 'user_id', 'type' => 'user' ], [ 'name' => 'user_id', 'type' => 'user' ],
[ 'name' => 'created_at', 'title' => 'Date', 'type' => 'display' ], [ 'name' => 'created_at', 'title' => 'Date', 'type' => 'display' ],
[ 'name' => 'title', 'type' => 'text' ], [ 'name' => 'title', 'required' => true, 'unique' => true, 'type' => 'string' ],
[ 'name' => 'body', 'type' => 'mkd' ], [ 'name' => 'body', 'required' => true, 'type' => 'mkd' ],
[ 'name' => 'tags', 'type' => 'text' ], [ 'name' => 'header-image', 'title' => 'Header Image', 'type' => 'image', 'delete' => true ],
[ 'name' => 'header-image', 'title' => 'Header Image', 'type' => 'image', 'delete' => true ] [ 'name' => 'tags', 'type' => 'list', 'model' => 'BlogTags', 'columns' => [ 'name' ], 'foreign' => 'blog_id', 'sort' => 'order' ]
]; ];
public static function getBlogEntries() public static function getBlogEntries()

14
app/Models/BlogTags.php Normal file
View file

@ -0,0 +1,14 @@
<?php namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class BlogTags extends Model {
/**
* The database table used by the model.
*
* @var string
*/
protected $table = 'blog_tags';
}

View file

@ -52,6 +52,20 @@ class DashboardModel extends Model
*/ */
public static $filter = true; public static $filter = true;
/*
* Number of items per page (0 for unlimited)
*
* @var number
*/
public static $items_per_page = 0;
/*
* Query parameters to remember
*
* @var number
*/
public static $valid_query_params = [];
/* /*
* Dashboard help text * Dashboard help text
* *
@ -88,12 +102,19 @@ class DashboardModel extends Model
public static $dashboard_sort_direction = 'desc'; public static $dashboard_sort_direction = 'desc';
/** /**
* The dashboard buttons * The dashboard button
* *
* @var array * @var array
*/ */
public static $dashboard_button = []; public static $dashboard_button = [];
/**
* The dashboard id link
*
* @var array
*/
public static $dashboard_id_link = [];
/** /**
* Returns the dashboard heading * Returns the dashboard heading
* *
@ -130,15 +151,56 @@ class DashboardModel extends Model
return $column_data; return $column_data;
} }
/**
* Performs a search against the columns in $dashboard_display
*
* @return array
*/
public static function searchDisplay($term, $query = null)
{
if (static::$filter) {
$first = true;
if ($query === null) {
$query = self::orderBy(static::$dashboard_sort_column, static::$dashboard_sort_direction);
}
foreach (static::$dashboard_display as $display) {
$type = '';
foreach (static::$dashboard_columns as $column) {
if ($column['name'] === $display) {
$type = $column['type'];
}
}
if ($type !== '' && $type !== 'image') {
if ($first) {
$query->where($display, 'LIKE', '%' . $term . '%');
} else {
$query->orWhere($display, 'LIKE', '%' . $term . '%');
}
$first = false;
}
}
return $query;
} else {
return [];
}
}
/** /**
* Returns data for the dashboard * Returns data for the dashboard
* *
* @return array * @return array
*/ */
public static function getDashboardData() public static function getDashboardData($include_param_display = false)
{ {
$sort_direction = static::$dashboard_reorder ? 'desc' : static::$dashboard_sort_direction; $sort_direction = static::$dashboard_reorder ? 'desc' : static::$dashboard_sort_direction;
$query = self::orderBy(static::$dashboard_sort_column, $sort_direction); $query = self::orderBy(static::$dashboard_sort_column, $sort_direction);
$query_param_display = [];
foreach (static::$dashboard_columns as $column) { foreach (static::$dashboard_columns as $column) {
if (array_key_exists('type', $column) && $column['type'] == 'user') { if (array_key_exists('type', $column) && $column['type'] == 'user') {
@ -147,7 +209,71 @@ class DashboardModel extends Model
} }
} }
return $query->get(); if (count(static::$valid_query_params) > 0) {
foreach (static::$valid_query_params as $param) {
if (request()->query($param['key'], null) != null) {
if ($include_param_display) {
$query_param_model = 'App\\Models\\' . $param['model'];
$query_param_column = $query_param_model::find(request()->query($param['key']));
if ($query_param_column !== null) {
array_push($query_param_display, [
'title' => $param['title'],
'value' => $query_param_column[$param['display']]
]);
}
}
$query->where($param['column'], request()->query($param['key']));
}
}
}
if (static::$items_per_page === 0) {
$results = $query->get();
} else {
if (static::$filter && request()->query('search', null) != null) {
$query = static::searchDisplay(request()->query('search'), $query);
}
$results = $query->paginate(static::$items_per_page);
}
if ($include_param_display) {
return [
'rows' => $results,
'paramdisplay' => $query_param_display
];
} else {
return $results;
}
}
/**
* Retrieves the current query string containing valid query parameters
*
* @return string
*/
public static function getQueryString()
{
$valid_query_params = static::$valid_query_params;
$string = '';
if (static::$items_per_page !== 0 && static::$filter) {
array_push($valid_query_params, [ 'key' => 'search' ]);
}
foreach ($valid_query_params as $param) {
if (request()->query($param['key'], null) != null) {
if ($string !== '') {
$string .= '&';
}
$string .= $param['key'] . '=' . request()->query($param['key']);
}
}
return $string;
} }
/** /**

View file

@ -19,7 +19,6 @@ class AddBlogTable extends Migration
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->string('title')->nullable(); $table->string('title')->nullable();
$table->text('body')->nullable(); $table->text('body')->nullable();
$table->text('tags')->nullable();
$table->timestamps(); $table->timestamps();
}); });
} }

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddBlogTagsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('blog_tags', function(Blueprint $table) {
$table->id();
$table->bigInteger('blog_id')->unsigned();
$table->foreign('blog_id')->references('id')->on('blog')->onDelete('cascade');
$table->string('name')->nullable();
$table->integer('order')->default(0);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('blog_tags');
}
}

3
gulpfile.js vendored
View file

@ -63,7 +63,8 @@ const jsDashboardLibs = [
"node_modules/flatpickr/dist/flatpickr.js", "node_modules/flatpickr/dist/flatpickr.js",
"node_modules/sortablejs/Sortable.js", "node_modules/sortablejs/Sortable.js",
"node_modules/list.js/dist/list.js", "node_modules/list.js/dist/list.js",
"node_modules/easymde/dist/easymde.min.js" "node_modules/easymde/dist/easymde.min.js",
"node_modules/autonumeric/dist/autoNumeric.js"
]; ];
// CSS libraries for the dashboard // CSS libraries for the dashboard

5
package-lock.json generated
View file

@ -1232,6 +1232,11 @@
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
}, },
"autonumeric": {
"version": "4.5.13",
"resolved": "https://registry.npmjs.org/autonumeric/-/autonumeric-4.5.13.tgz",
"integrity": "sha512-GHpM0cGWTzFVDQyOgpXlNoqe8fDn6minpGEauLgDnGb4k24WmF8GInLnhIGWoJXisHZuWmTeKqFcIBnf5c2DaA=="
},
"autoprefixer": { "autoprefixer": {
"version": "9.7.6", "version": "9.7.6",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.6.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.6.tgz",

View file

@ -11,6 +11,7 @@
"@babel/core": "^7.9.0", "@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.5", "@babel/preset-env": "^7.9.5",
"@fortawesome/fontawesome-free": "^5.13.0", "@fortawesome/fontawesome-free": "^5.13.0",
"autonumeric": "^4.5.13",
"autoprefixer": "^9.7.6", "autoprefixer": "^9.7.6",
"babelify": "^10.0.0", "babelify": "^10.0.0",
"bootstrap": "4.4.1", "bootstrap": "4.4.1",

View file

@ -179,12 +179,15 @@ These are variables that only function when the `$dashboard_type` variable is se
* `$dashboard_reorder`: A boolean determining whether to render drag handles to reorder the items in the list * `$dashboard_reorder`: A boolean determining whether to render drag handles to reorder the items in the list
* `$dashboard_sort_column`: A string containing the column used to sort the list (this column should be an `integer` when `$dashboard_reorder` is true) * `$dashboard_sort_column`: A string containing the column used to sort the list (this column should be an `integer` when `$dashboard_reorder` is true)
* `$dashboard_sort_direction`: When `$dashboard_reorder` is false this determines the sort direction (this can be `desc` for descending or `asc` ascending) * `$dashboard_sort_direction`: When `$dashboard_reorder` is false this determines the sort direction (this can be `desc` for descending or `asc` ascending)
* `$dashboard_button`: An array containing the following items in this order: * `$dashboard_button`: Add a dashboard button with custom functionality by populating an array containing the following items in this order:
* The title * The title
* Confirmation text asking the user to confirm * Confirmation text asking the user to confirm
* A "success" message to display when the response is `success` * A "success" message to display when the response is `success`
* A "failure" message to display when the response is not `success` * A "failure" message to display when the response is not `success`
* The URL to send the POST request to with the respective `id` in the request variable * The URL to send the POST request to with the respective `id` in the request variable
* `$dashboard_id_link`: Add a dashboard button linking to another list filtered by the current item by populating an array containing the following items in this order:
* The title
* The URL to link to where the id will come after the rest
##### Configuring the columns ##### Configuring the columns
@ -198,15 +201,20 @@ All models use the following attributes:
Models with their `$dashboard_type` set to `edit` also use: Models with their `$dashboard_type` set to `edit` also use:
* `type`: The column type which can be any of the following: * `type`: The column type which can be any of the following:
* `text`: Text input field for text data
* `mkd`: Markdown editor for text data containing markdown
* `date`: Date and time selection tool for date/time data
* `select`: Text input via option select
* `hidden`: Fields that will contain values to pass to the update function but won't appear on the page (this must be used for the sort column) * `hidden`: Fields that will contain values to pass to the update function but won't appear on the page (this must be used for the sort column)
* `user`: This should point to a foreign key that references the id on the users table; setting this will bind items to the user that created them
* `string`: Single-line text input field
* `text`: Multi-line text input field
* `currency`: Text input field for currency data
* `date`: Date and time selection tool for date/time data
* `mkd`: Multi-line text input field with a markdown editor
* `select`: Text input via option select
* `list`: One or more items saved to a connected table
* `image`: Fields that contain image uploads * `image`: Fields that contain image uploads
* `file`: Fields that contains file uploads * `file`: Fields that contains file uploads
* `display`: Displayed information that can't be edited * `display`: Displayed information that can't be edited
* `user`: This should point to a foreign key that references the id on the users table; setting this will bind items to the user that created them * `required`: If set an error will be displayed if the field has no value
* `unique`: If set an error will be displayed if another row in the table has the same value for a given column
* `type-new`: This takes the same options as `type` and overrides it when creating new items (eg: to allow input on a field during creation but not after) * `type-new`: This takes the same options as `type` and overrides it when creating new items (eg: to allow input on a field during creation but not after)
* `options` (required by `select`) Takes an array of options that are either strings or arrays containing the keys `title` (for what will display with the option) and `value` (for what will be recorded) * `options` (required by `select`) Takes an array of options that are either strings or arrays containing the keys `title` (for what will display with the option) and `value` (for what will be recorded)
* `name`: (required by `file` and `image`) Used along with the record id to determine the filename * `name`: (required by `file` and `image`) Used along with the record id to determine the filename
@ -229,9 +237,9 @@ An example of the `$dashboard_columns` array in a model with its `$dashboard_typ
public static $dashboard_columns = [ public static $dashboard_columns = [
[ 'name' => 'user_id', 'type' => 'user' ], [ 'name' => 'user_id', 'type' => 'user' ],
[ 'name' => 'created_at', 'title' => 'Date', 'type' => 'display' ], [ 'name' => 'created_at', 'title' => 'Date', 'type' => 'display' ],
[ 'name' => 'title', 'type' => 'text' ], [ 'name' => 'title', 'required' => true, 'unique' => true, 'type' => 'string' ],
[ 'name' => 'body', 'type' => 'mkd' ], [ 'name' => 'body', 'required' => true, 'type' => 'mkd' ],
[ 'name' => 'tags', 'type' => 'text' ], [ 'name' => 'header-image', 'title' => 'Header Image', 'type' => 'image', 'delete' => true ],
[ 'name' => 'header-image', 'title' => 'Header Image', 'type' => 'image', 'delete' => true ] [ 'name' => 'tags', 'type' => 'list', 'model' => 'BlogTags', 'columns' => [ 'name' ], 'foreign' => 'blog_id', 'sort' => 'order' ]
]; ];
``` ```

View file

@ -135,7 +135,7 @@ function showAlert(message, command) {
$acceptButton.on("click", closeAlertModal); $acceptButton.on("click", closeAlertModal);
// set the message with the supplied message // set the message with the supplied message
$message.text(message); $message.html(message);
// show the alert modal // show the alert modal
$alertModal.css({ $alertModal.css({
@ -151,15 +151,6 @@ function editListInit() {
$token = $("#token"), $token = $("#token"),
model = $editList.data("model"); model = $editList.data("model");
// initialize new button functionality
const newButtonInit = function() {
const $newButton = $(".btn.new-button");
$newButton.on("click", function() {
window.location.href = "/dashboard/edit/" + model + "/new";
});
};
// initialize delete button functionality // initialize delete button functionality
const deleteButtonInit = function() { const deleteButtonInit = function() {
const $deleteButtons = $(".btn.delete-button"); const $deleteButtons = $(".btn.delete-button");
@ -282,11 +273,32 @@ function editListInit() {
} }
}; };
newButtonInit(); // initialize search functionality if the search-form element exists
const searchFormInit = function() {
const $form = $("#search-form");
if ($form.length) {
$form.on("submit", function(e) {
const term = $form.find(".search").val();
let url = $form.data("url");
e.preventDefault();
if (term !== "") {
url += `?search=${term}`;
}
window.location.href = url;
});
}
};
deleteButtonInit(); deleteButtonInit();
actionButtonInit(); actionButtonInit();
sortRowInit(); sortRowInit();
filterInputInit(); filterInputInit();
searchFormInit();
} }
// initialize edit item functionality // initialize edit item functionality
@ -295,10 +307,12 @@ function editItemInit() {
$submit = $("#submit"), $submit = $("#submit"),
$backButton = $("#back"), $backButton = $("#back"),
$textInputs = $(".text-input"), $textInputs = $(".text-input"),
$currencyInputs = $(".currency-input"),
$datePickers = $(".date-picker"), $datePickers = $(".date-picker"),
$mkdEditors = $(".mkd-editor"), $mkdEditors = $(".mkd-editor"),
$fileUploads = $(".file-upload"), $fileUploads = $(".file-upload"),
$imgUploads = $(".image-upload"), $imgUploads = $(".image-upload"),
$lists = $(".list"),
$token = $("#token"), $token = $("#token"),
model = $editItem.data("model"), model = $editItem.data("model"),
id = $editItem.data("id"), id = $editItem.data("id"),
@ -315,12 +329,12 @@ function editItemInit() {
// fill the formData object with data from all the form fields // fill the formData object with data from all the form fields
const getFormData = function() { const getFormData = function() {
// function to add a column and value to the formData object // function to add a column and value to the formData object
const addFormData = function(column, value) { const addFormData = function(type, column, value) {
// add the value to a key with the column name // add the value to a key with the column name
formData[column] = value; formData[column] = value;
// add the column to the array of columns // add the column to the array of columns
formData.columns.push(column); formData.columns.push({ type: type, name: column });
}; };
// reset the formData object // reset the formData object
@ -334,31 +348,63 @@ function editItemInit() {
// create an empty array to contain the list of columns // create an empty array to contain the list of columns
formData.columns = []; formData.columns = [];
// add values from the contents of text-input class elements // add values from the contents of text-input elements
$textInputs.each(function() { $textInputs.each(function() {
const $this = $(this), const $this = $(this),
column = $this.attr("id"), column = $this.attr("id"),
value = $this.val(); value = $this.val();
addFormData(column, value); addFormData("text", column, value);
}); });
// add values from the contents of date-picker class elements // add values from the contents of date-picker elements
$datePickers.each(function() { $datePickers.each(function() {
const $this = $(this), const $this = $(this),
column = $this.attr("id"), column = $this.attr("id"),
value = $this.val(); value = $this.val();
addFormData(column, value); addFormData("date", column, value);
}); });
// add values from the contents of the markdown editor for mkd-editor class elements // add values from the contents of currency-input elements
$currencyInputs.each(function() {
const $this = $(this),
column = $this.attr("id"),
value = AutoNumeric.getNumericString(this);
addFormData("text", column, value);
});
// add values from the contents of the markdown editor for mkd-editor elements
$mkdEditors.each(function() { $mkdEditors.each(function() {
const $this = $(this), const $this = $(this),
column = $this.attr("id"), column = $this.attr("id"),
value = easymde[column].value(); value = easymde[column].value();
addFormData(column, value); addFormData("text", column, value);
});
// add values from list-items inputs
$lists.each(function() {
const $this = $(this),
column = $this.attr("id"),
value = [];
$this.find(".list-items .list-items-row").each(function(index, row) {
const rowData = {};
$(row).find(".list-items-row-input-inner").each(function(index, input) {
const $input = $(input),
column = $input.data("column"),
value = $input.val();
rowData[column] = value;
});
value.push(rowData);
});
addFormData("list", column, value);
}); });
}; };
@ -463,6 +509,7 @@ function editItemInit() {
$submit.removeClass("no-input"); $submit.removeClass("no-input");
}; };
// initialize image deletion
$(".edit-button.delete.image").on("click", function(e) { $(".edit-button.delete.image").on("click", function(e) {
const $this = $(this), const $this = $(this),
name = $this.data("name"); name = $this.data("name");
@ -497,6 +544,7 @@ function editItemInit() {
} }
}); });
// initialize file deletion
$(".edit-button.delete.file").on("click", function(e) { $(".edit-button.delete.file").on("click", function(e) {
const $this = $(this), const $this = $(this),
name = $this.data("name"), name = $this.data("name"),
@ -533,21 +581,89 @@ function editItemInit() {
} }
}); });
// allow start time selection to start on the hour and every 15 minutes after // initialize list item functionality
$lists.each(function(index, list) {
const $list = $(list),
$template = $list.find(".list-template"),
$items = $list.find(".list-items");
let sortable = undefined;
const initSort = function() {
if (typeof sortable !== "undefined") {
sortable.destroy();
}
sortable = Sortable.create($items[0], {
handle: ".sort-icon",
onUpdate: contentChanged
});
};
const initDelete = function() {
$items.find(".list-items-row").each(function(index, row) {
const $row = $(row);
// initialize delete button functionality
$row.find(".list-items-row-button").off("click").on("click", function() {
$row.remove();
initSort();
contentChanged();
});
});
};
const initList = function() {
$list.find(".list-data-row").each(function(rowIndex, row) {
// Add the values from the current data row to the template
$(row).find(".list-data-row-item").each(function(itemIndex, item) {
const $item = $(item),
column = $item.data("column"),
value = $item.data("value");
$template.find(".list-items-row-input-inner").each(function(inputIndex, input) {
const $input = $(input);
if ($input.data("column") === column) {
$input.val(value);
}
});
});
// Add the populated template to the list of items then clear the template values
$template.find(".list-items-row").clone().appendTo($items);
$template.find(".list-items-row-input-inner").val("");
});
initSort();
initDelete();
};
$list.find(".list-add-button").on("click", function() {
$template.find(".list-items-row").clone().appendTo($items);
initDelete();
initSort();
contentChanged();
});
initList();
});
// allow the date picker start time selection to start on the hour and every 15 minutes after
for (hours = 0; hours <= 23; hours++) { for (hours = 0; hours <= 23; hours++) {
for (minutes = 0; minutes <= 3; minutes++) { for (minutes = 0; minutes <= 3; minutes++) {
allowTimes.push(hours + ":" + (minutes === 0 ? "00" : minutes * 15)); allowTimes.push(hours + ":" + (minutes === 0 ? "00" : minutes * 15));
} }
} }
// enable the datepicker for each element with the date-picker class // enable the datepicker for date-picker elements
$datePickers.each(function() { $datePickers.each(function() {
$(this).flatpickr({ $(this).flatpickr({
enableTime: true enableTime: true
}); });
}); });
// enable the markdown editor for each element with the mkd-editor class // enable the markdown editor for mkd-editor elements
$mkdEditors.each(function() { $mkdEditors.each(function() {
const $this = $(this), const $this = $(this),
column = $this.attr("id"); column = $this.attr("id");
@ -583,6 +699,14 @@ function editItemInit() {
}, 500); }, 500);
}); });
// enable currency formatting for currency-input elements
new AutoNumeric.multiple($currencyInputs.toArray(), {
currencySymbol: "$",
rawValueDivisor: 0.01,
allowDecimalPadding: false,
modifyValueOnWheel: false
});
// watch for changes to input and select element contents // watch for changes to input and select element contents
$editItem.find("input, select").on("input change", contentChanged); $editItem.find("input, select").on("input change", contentChanged);
@ -617,12 +741,22 @@ function editItemInit() {
url: "/dashboard/update", url: "/dashboard/update",
data: formData data: formData
}).always(function(response) { }).always(function(response) {
let message = "";
if ((/^id:[0-9][0-9]*$/).test(response)) { if ((/^id:[0-9][0-9]*$/).test(response)) {
uploadImage(response.replace(/^id:/, ""), 0); uploadImage(response.replace(/^id:/, ""), 0);
} else { } else {
loadingModal("hide"); loadingModal("hide");
showAlert("Failed to " + operation + " record", function() { if ((/^not-unique:/).test(response)) {
message = `<strong>${response.replace(/'/g, "").replace(/^[^:]*:/, "").replace(/,([^,]*)$/, "</strong> and <strong>$1").replace(/,/g, "</strong>, <strong>")}</strong> must be unique`;
} else if ((/^required:/).test(response)) {
message = `<strong>${response.replace(/'/g, "").replace(/^[^:]*:/, "").replace(/,([^,]*)$/, "</strong> and <strong>$1").replace(/,/g, "</strong>, <strong>")}</strong> must not be empty`;
} else {
message = `Failed to <strong>${operation}</strong> record`;
}
showAlert(message, function() {
submitting = false; submitting = false;
}); });
} }

View file

@ -271,6 +271,23 @@ body {
} }
} }
.search-form {
display: flex;
width: 100%;
&-input {
display: block;
width: 100%;
}
&-submit {
margin-left: 10px;
height: 32px;
border: 1px solid fade-out($c-text, 0.75);
border-radius: 4px;
}
}
.search { .search {
margin-bottom: 10px; margin-bottom: 10px;
width: 100%; width: 100%;
@ -478,6 +495,20 @@ body {
&.btn-link { &.btn-link {
color: $c-dashboard-light; color: $c-dashboard-light;
} }
&.btn-outline {
background-color: transparent;
color: $c-text;
}
&.btn-inactive {
pointer-events: none;
}
&.btn-disabled {
opacity: 0.5;
pointer-events: none;
}
} }
.view-table-container { .view-table-container {
@ -536,6 +567,123 @@ body {
} }
} }
.pagination-navigation-bar {
margin-bottom: 10px;
margin-left: 4px;
display: flex;
width: calc(100% - 8px);
@include media-breakpoint-down(xs) {
flex-direction: column;
}
@include media-breakpoint-up(sm) {
justify-content: space-between;
}
&-arrow {
position: relative;
font-size: 0px;
@include media-breakpoint-down(xs) {
height: 35px;
}
&.prev {
@include media-breakpoint-down(xs) {
margin-bottom: 8px;
}
@include media-breakpoint-up(sm) {
margin-right: ($grid-gutter-width / 2);
}
}
&.next {
transform: rotate(180deg);
@include media-breakpoint-down(xs) {
margin-top: 8px;
}
@include media-breakpoint-up(sm) {
margin-left: ($grid-gutter-width / 2);
}
}
&:before, &:after {
content: "";
position: absolute;
top: calc(50% - 1px);
left: 25%;
width: 16px;
height: 2px;
background-color: $c-text-light;
@include media-breakpoint-down(xs) {
left: 50%;
}
}
&:before {
transform: rotate(-45deg);
transform-origin: bottom left;
}
&:after {
transform: rotate(45deg);
transform-origin: top left;
}
}
&-page-count {
display: flex;
@include media-breakpoint-down(xs) {
justify-content: center;
}
.btn {
$spacing: 3px;
&:not(:first-child) {
margin-left: $spacing;
}
&:not(:last-child) {
margin-right: $spacing;
}
&.space {
position: relative;
&:after {
content: "...";
position: absolute;
top: 0px;
line-height: 25px;
}
&:first-child {
margin-right: 20px;
&:after {
left: calc(100% + 7px);
}
}
&:last-child {
margin-left: 20px;
&:after {
right: calc(100% + 7px);
}
}
}
}
}
}
.list-group { .list-group {
margin-bottom: 0px; margin-bottom: 0px;
@ -715,6 +863,108 @@ form {
} }
} }
.list {
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid fade-out($c-text, 0.75);
&-template, &-data {
display: none;
}
&-items-row {
margin-bottom: 10px;
display: flex;
align-items: center;
.sort-icon {
margin-right: 10px;
display: inline-block;
opacity: 1;
transition: opacity 100ms;
cursor: grab;
&-inner {
position: relative;
top: 2px;
display: inline-block;
width: 12px;
height: 14px;
&-bar {
position: absolute;
left: 0px;
width: 100%;
height: 2px;
background-color: $c-text;
&:nth-child(1) {
top: 2px;
}
&:nth-child(2) {
top: 50%;
transform: translateY(-50%);
}
&:nth-child(3) {
bottom: 2px;
}
}
}
}
&-input, &-button {
&:not(:first-child) {
margin-left: 5px;
}
&:not(:last-child) {
margin-right: 5px;
}
}
&-input {
position: relative;
&.wide {
width: 100%;
}
&-inner {
margin-bottom: 0px;
}
&-overlay {
overflow: hidden;
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
padding: 5px 8px;
background-color: $c-input-bg;
white-space: nowrap;
text-overflow: ellipsis;
pointer-events: none;
}
}
&-button {
min-width: 70px;
height: $label-height;
border: 1px solid fade-out($c-text, 0.75);
border-radius: 4px;
}
}
&-add-button {
height: $label-height;
border: 1px solid fade-out($c-text, 0.75);
border-radius: 4px;
}
}
.picker__holder { .picker__holder {
overflow-y: hidden; overflow-y: hidden;
@ -1023,6 +1273,7 @@ form {
.card { .card {
position: relative; position: relative;
margin: 0px; margin: 0px;
width: 100%;
max-width: 500px; max-width: 500px;
.btn { .btn {

View file

@ -27,13 +27,17 @@
@elseif($type == 'user') @elseif($type == 'user')
<input class="text-input" type="hidden" name="{{ $column['name'] }}" id="{{ $column['name'] }}" value="{{ Auth::id() }}" /> <input class="text-input" type="hidden" name="{{ $column['name'] }}" id="{{ $column['name'] }}" value="{{ Auth::id() }}" />
@elseif($type != 'display' || $id != 'new') @elseif($type != 'display' || $id != 'new')
<div class="col-12 col-md-2"> <div class="col-12 col-md-4 col-lg-3">
<label for="{{ $column['name'] }}">{{ array_key_exists('title', $column) ? $column['title'] : ucfirst($column['name']) }}:</label> <label for="{{ $column['name'] }}">{{ array_key_exists('title', $column) ? $column['title'] : ucfirst($column['name']) }}:</label>
</div> </div>
<div class="col-12 col-md-10"> <div class="col-12 col-md-8 col-lg-9">
@if($type == 'text') @if($type == 'string')
<input class="text-input" type="text" name="{{ $column['name'] }}" id="{{ $column['name'] }}" value="{{ $value }}" /> <input class="text-input" type="text" name="{{ $column['name'] }}" id="{{ $column['name'] }}" value="{{ $value }}" />
@elseif($type == 'text')
<textarea class="text-input" name="{{ $column['name'] }}" id="{{ $column['name'] }}">{{ $value }}</textarea>
@elseif($type == 'currency')
<input class="currency-input" type="text" name="{{ $column['name'] }}" id="{{ $column['name'] }}" value="{{ $value }}" autocomplete="off" />
@elseif($type == 'date') @elseif($type == 'date')
<input class="date-picker" type="text" name="{{ $column['name'] }}" id="{{ $column['name'] }}" value="{{ $value == '' ? date('Y-m-d', time()) : preg_replace('/:[0-9][0-9]$/', '', $value) }}" /> <input class="date-picker" type="text" name="{{ $column['name'] }}" id="{{ $column['name'] }}" value="{{ $value == '' ? date('Y-m-d', time()) : preg_replace('/:[0-9][0-9]$/', '', $value) }}" />
@elseif($type == 'mkd') @elseif($type == 'mkd')
@ -58,6 +62,45 @@
@endif @endif
@endforeach @endforeach
</select> </select>
@elseif($type == 'list')
<div class="list" id="{{ $column['name'] }}">
<div class="list-template">
<div class="list-items-row">
<div class="sort-icon" title="Click and drag to reorder">
<div class="sort-icon-inner">
<div class="sort-icon-inner-bar"></div>
<div class="sort-icon-inner-bar"></div>
<div class="sort-icon-inner-bar"></div>
</div>
</div>
@foreach($column['columns'] as $list_column)
@set('placeholder', $column['name'] == 'included' || $column['name'] == 'recommended' ? '' : $list_column)
<div class="list-items-row-input {{ count($column['columns']) == 1 ? 'wide' : '' }}">
<input class="list-items-row-input-inner" data-column="{{ $list_column }}" placeholder="{{ $placeholder }}" />
</div>
@endforeach
<button class="list-items-row-button" type="button">Delete</button>
</div>
</div>
<div class="list-data">
@if($id != 'new')
@foreach($value as $row)
<div class="list-data-row">
@foreach($column['columns'] as $list_column)
<div class="list-data-row-item" data-column="{{ $list_column }}" data-value="{{ $row[$list_column] }}"></div>
@endforeach
</div>
@endforeach
@endif
</div>
<div class="list-items"></div>
<button class="list-add-button" type="button">Add</button>
</div>
@elseif($type == 'image') @elseif($type == 'image')
@set('current_image', "/uploads/$model/img/$id-" . $column['name'] . '.jpg') @set('current_image', "/uploads/$model/img/$id-" . $column['name'] . '.jpg')
<input class="image-upload" type="file" name="{{ $column['name'] }}" id="{{ $column['name'] }}" /> <input class="image-upload" type="file" name="{{ $column['name'] }}" id="{{ $column['name'] }}" />

View file

@ -6,7 +6,7 @@
@endif @endif
@if($create) @if($create)
<button type="button" class="new-button btn btn-secondary">New</button> <a href="/dashboard/edit/{{ $model }}/new" class="new-button btn btn-secondary">New</a>
@endif @endif
@endsection @endsection
@ -14,10 +14,99 @@
<div id="edit-list-wrapper"> <div id="edit-list-wrapper">
<input type="hidden" id="token" value="{{ csrf_token() }}" /> <input type="hidden" id="token" value="{{ csrf_token() }}" />
@if($filter) @if(count($paramdisplay))
<input id="filter-input" class="search" placeholder="Filter" /> @foreach($paramdisplay as $param)
<div>Showing {{ $heading }} with a {{ $param['title'] }} of "{{ $param['value'] }}"</div>
@endforeach
@endif @endif
@if($filter)
@if(!$paginate)
<input id="filter-input" class="search" placeholder="Filter" />
@else
<form
id="search-form"
class="search-form"
data-url="{{ url()->current() }}">
<input
class="search-form-input search"
placeholder="Search"
value="{{ request()->query('search') }}"
/>
<input
type="submit"
class="search-form-submit"
value="Search"
/>
</form>
@endif
@endif
@if($paginate && $rows->lastPage() !== 1)
<div class="pagination-navigation-bar">
<a
class="pagination-navigation-bar-arrow prev btn btn-primary {{ $rows->onFirstPage() ? 'btn-disabled' : '' }}"
href="/dashboard/edit/{{ $model }}?page={{ $rows->onFirstPage() ? 1 : $rows->currentPage() - 1 }}{{ $query !== '' ? ('&' . $query) : '' }}">
Previous Page
</a>
<div class="pagination-navigation-bar-page-count">
@set('pages_around', 2)
@set('start_page', $rows->currentPage() - $pages_around)
@if($start_page < 1)
@set('start_page', 1)
@elseif($start_page + $pages_around > $rows->lastPage())
@set('start_page', $rows->lastPage() - $pages_around)
@endif
@if($start_page > 1)
<a
class="pagination-navigation-bar-pages-number btn btn-outline space"
href="/dashboard/edit/{{ $model }}?page=1{{ $query !== '' ? ('&' . $query) : '' }}">
1
</a>
@endif
@for($page = $start_page; $page < $start_page + 1 + $pages_around * 2; $page++)
@if($page === $rows->currentPage())
<div class="pagination-navigation-bar-pages-number btn btn-inactive">{{ $page }}</div>
@elseif($page <= $rows->lastPage())
<a
class="pagination-navigation-bar-pages-number btn btn-outline"
href="/dashboard/edit/{{ $model }}?page={{ $page }}{{ $query !== '' ? ('&' . $query) : '' }}">
{{ $page }}
</a>
@endif
@endfor
@if($start_page + $pages_around * 2 < $rows->lastPage())
<a
class="pagination-navigation-bar-pages-number btn btn-outline space"
href="/dashboard/edit/{{ $model }}?page={{ $rows->lastPage() }}{{ $query !== '' ? ('&' . $query) : '' }}">
{{ $rows->lastPage() }}
</a>
@endif
</div>
<a
class="pagination-navigation-bar-arrow next btn btn-primary {{ $rows->hasMorePages() ? '' : 'btn-disabled' }}"
href="/dashboard/edit/{{ $model }}?page={{ $rows->hasMorePages() ? $rows->currentPage() + 1 : $rows->currentPage() }}{{ $query !== '' ? ('&' . $query) : '' }}">
Next Page
</a>
</div>
@endif
@if(request()->query('search', null) != null && count($rows) == 0)
<div class="help-text text-center">No Matching {{ $heading }} Found</div>
@else
<ul id="edit-list" class="list-group edit-list list" data-model="{{ $model }}" {{ $sortcol != false ? "data-sort=$sortcol" : '' }}> <ul id="edit-list" class="list-group edit-list list" data-model="{{ $model }}" {{ $sortcol != false ? "data-sort=$sortcol" : '' }}>
@foreach($rows as $row) @foreach($rows as $row)
<li class="list-group-item {{ $sortcol != false ? 'sortable' : '' }}" data-id="{{ $row['id'] }}"> <li class="list-group-item {{ $sortcol != false ? 'sortable' : '' }}" data-id="{{ $row['id'] }}">
@ -48,6 +137,10 @@
<button type="button" class="action-button btn btn-secondary" data-confirmation="{{ $button[1] }}" data-success="{{ $button[2] }}" data-error="{{ $button[3] }}" data-url="{{ $button[4] }}">{{ $button[0] }}</button> <button type="button" class="action-button btn btn-secondary" data-confirmation="{{ $button[1] }}" data-success="{{ $button[2] }}" data-error="{{ $button[3] }}" data-url="{{ $button[4] }}">{{ $button[0] }}</button>
@endif @endif
@if(!empty($idlink))
<a class="btn btn-secondary" href="{{ $idlink[1] }}{{ $row['id'] }}">{{ $idlink[0] }}</a>
@endif
<a class="edit-button btn btn-warning" href="/dashboard/edit/{{ $model }}/{{ $row['id'] }}">Edit</a> <a class="edit-button btn btn-warning" href="/dashboard/edit/{{ $model }}/{{ $row['id'] }}">Edit</a>
@if($delete) @if($delete)
@ -57,5 +150,6 @@
</li> </li>
@endforeach @endforeach
</ul> </ul>
@endif
</div> </div>
@endsection @endsection