Reuse dashboard logic much more, delete images and files when the associated item is deleted, rework dashboard lists so their items can be configured in the $dashboard_columns of their own class, allow dashboard lists to upload images, delete images when dashboard list items are deleted, add a default image extension variable to dashboard model items rather than hard-coding it so it can be reconfigured, improve the dashboard styles some more, and improve the readme (including documenting the new dashboard list update)

This commit is contained in:
Kevin MacMartin 2022-06-14 01:36:21 -04:00
parent ace3c607ca
commit db7deb0fdc
9 changed files with 397 additions and 166 deletions

View file

@ -70,38 +70,19 @@ class Dashboard
*/ */
public static function getModel($model, $type = null) public static function getModel($model, $type = null)
{ {
$model_name = ucfirst($model); $model_name = implode('', array_map('ucfirst', explode('_', $model)));
// Ensure the model has been declared in the menu if (file_exists(app_path() . '/Models/' . $model_name . '.php')) {
$model_in_menu = false;
foreach (self::$menu as $menu_item) {
if (array_key_exists('submenu', $menu_item)) {
// Check each item if this is a submenu
foreach ($menu_item['submenu'] as $submenu_item) {
if ($submenu_item['model'] == $model) {
$model_in_menu = true;
break;
}
}
} else {
// Check the menu item
if ($menu_item['model'] == $model) {
$model_in_menu = true;
}
}
// Don't bother continuing if we've already confirmed it's in the menu
if ($model_in_menu) {
break;
}
}
if ($model_in_menu && file_exists(app_path() . '/Models/' . $model_name . '.php')) {
$model_class = 'App\\Models\\' . $model_name; $model_class = 'App\\Models\\' . $model_name;
if ($type != null && $type != $model_class::$dashboard_type) { if ($type != null) {
return null; if (is_array($type)) {
if (!in_array($model_class::$dashboard_type, $type)) {
return null;
}
} else if ($type != $model_class::$dashboard_type) {
return null;
}
} }
return new $model_class; return new $model_class;

View file

@ -82,18 +82,11 @@ class DashboardController extends Controller {
if ($model_class != null) { if ($model_class != null) {
if ($id == 'new') { if ($id == 'new') {
$item = null; $item = new $model_class;
} else { } else {
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');
} }
@ -102,13 +95,26 @@ class DashboardController extends Controller {
} }
} }
foreach ($model_class::$dashboard_columns as $column) {
if ($column['type'] === 'list') {
$list_model_class = 'App\\Models\\' . $column['model'];
$list_model_instance = new $list_model_class;
$item->{$column['name']} = [
'model' => $list_model_instance->getTable(),
'list' => $id == 'new' ? [] : $list_model_instance::where($column['foreign'], $item->id)->orderBy($column['sort'])->get()
];
}
}
return view('dashboard.pages.edit-item', [ return view('dashboard.pages.edit-item', [
'heading' => $model_class->getDashboardHeading(), 'heading' => $model_class->getDashboardHeading(),
'model' => $model, 'default_img_ext' => $model_class::$default_image_ext,
'id' => $id, 'model' => $model,
'item' => $item, 'id' => $id,
'help_text' => $model_class::$dashboard_help_text, 'item' => $item,
'columns' => $model_class::$dashboard_columns 'help_text' => $model_class::$dashboard_help_text,
'columns' => $model_class::$dashboard_columns
]); ]);
} else { } else {
abort(404); abort(404);
@ -242,6 +248,8 @@ class DashboardController extends Controller {
} }
// populate connected lists with list items in $request // populate connected lists with list items in $request
$lists = [];
foreach ($request['columns'] as $column) { foreach ($request['columns'] as $column) {
if ($column['type'] === 'list') { if ($column['type'] === 'list') {
$column_name = $column['name']; $column_name = $column['name'];
@ -250,20 +258,44 @@ class DashboardController extends Controller {
if ($dashboard_column['name'] === $column_name) { if ($dashboard_column['name'] === $column_name) {
$foreign = $dashboard_column['foreign']; $foreign = $dashboard_column['foreign'];
$list_model_class = 'App\\Models\\' . $dashboard_column['model']; $list_model_class = 'App\\Models\\' . $dashboard_column['model'];
$list_model_class::where($foreign, $item->id)->delete();
if ($request->has($column_name)) { if ($list_model_class::$dashboard_type == 'list') {
foreach ($request[$column_name] as $index => $row) { $ids = [];
$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) { if ($request->has($column_name)) {
$list_model_item->$key = $value; foreach ($request[$column_name] as $index => $row) {
if ($row['id'] == 'new') {
$list_model_item = new $list_model_class;
} else {
$list_model_item = $list_model_class::find($row['id']);
}
$list_model_item->$foreign = $item->id;
$list_model_item->{$dashboard_column['sort']} = $index;
foreach ($row['data'] as $key => $data) {
if ($data['type'] == 'string') {
$list_model_item->$key = $data['value'];
}
}
$list_model_item->save();
array_push($ids, $list_model_item->id);
} }
$list_model_item->save();
} }
// delete any associated row that wasn't just created or edited
foreach ($list_model_class::where($foreign, $item->id)->whereNotIn('id', $ids)->get() as $list_item) {
$list_item->delete();
}
// store the sets of ids for each list
$lists[$column_name] = $ids;
// stop looping through dashboard columns
break;
} else {
return 'invalid-list-model:' . $dashboard_column['model'];
} }
} }
} }
@ -274,7 +306,10 @@ class DashboardController extends Controller {
$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
return 'id:' . $item->id; return [
'id' => $item->id,
'lists' => $lists
];
} else { } else {
return 'model-access-fail'; return 'model-access-fail';
} }
@ -289,7 +324,7 @@ class DashboardController extends Controller {
'name' => 'required' 'name' => 'required'
]); ]);
$model_class = Dashboard::getModel($request['model'], 'edit'); $model_class = Dashboard::getModel($request['model'], [ 'edit', 'list' ]);
if ($model_class != null) { if ($model_class != null) {
$item = $model_class::find($request['id']); $item = $model_class::find($request['id']);
@ -321,7 +356,7 @@ class DashboardController extends Controller {
'name' => 'required' 'name' => 'required'
]); ]);
$model_class = Dashboard::getModel($request['model'], 'edit'); $model_class = Dashboard::getModel($request['model'], [ 'edit', 'list' ]);
if ($model_class != null) { if ($model_class != null) {
$item = $model_class::find($request['id']); $item = $model_class::find($request['id']);
@ -363,12 +398,16 @@ class DashboardController extends Controller {
return 'permission-fail'; return 'permission-fail';
} }
// delete associated files if they exist // delete associated list items
foreach ($model_class::$dashboard_columns as $column) { foreach ($item::$dashboard_columns as $column) {
if ($column['type'] == 'image') { if ($column['type'] == 'list') {
$item->deleteImage($column['name'], false); $list_model_class = Dashboard::getModel($column['model'], 'list');
} else if ($column['type'] == 'file') {
$item->deleteFile($column['name'], false); if ($list_model_class != null) {
foreach ($list_model_class::where($column['foreign'], $item->id)->get() as $list_item) {
$list_item->delete();
}
}
} }
} }
@ -399,7 +438,7 @@ class DashboardController extends Controller {
'name' => 'required' 'name' => 'required'
]); ]);
$model_class = Dashboard::getModel($request['model'], 'edit'); $model_class = Dashboard::getModel($request['model'], [ 'edit', 'list' ]);
if ($model_class != null) { if ($model_class != null) {
$item = $model_class::find($request['id']); $item = $model_class::find($request['id']);
@ -423,7 +462,7 @@ class DashboardController extends Controller {
'name' => 'required' 'name' => 'required'
]); ]);
$model_class = Dashboard::getModel($request['model'], 'edit'); $model_class = Dashboard::getModel($request['model'], [ 'edit', 'list' ]);
if ($model_class != null) { if ($model_class != null) {
$item = $model_class::find($request['id']); $item = $model_class::find($request['id']);

View file

@ -21,8 +21,8 @@ class Blog extends DashboardModel
[ 'name' => 'created_at', 'title' => 'Date', 'type' => 'display' ], [ 'name' => 'created_at', 'title' => 'Date', 'type' => 'display' ],
[ 'name' => 'title', 'required' => true, 'unique' => true, 'type' => 'string' ], [ 'name' => 'title', 'required' => true, 'unique' => true, 'type' => 'string' ],
[ 'name' => 'body', 'required' => true, 'type' => 'mkd' ], [ 'name' => 'body', 'required' => true, 'type' => 'mkd' ],
[ 'name' => 'header-image', 'title' => 'Header Image', 'type' => 'image', 'delete' => true ], [ 'name' => 'header-image', 'title' => 'Header Image', 'type' => 'image', 'delete' => true, 'ext' => 'jpg' ],
[ 'name' => 'tags', 'type' => 'list', 'model' => 'BlogTags', 'columns' => [ 'name' ], 'foreign' => 'blog_id', 'sort' => 'order' ] [ 'name' => 'tags', 'type' => 'list', 'model' => 'BlogTags', 'foreign' => 'blog_id', 'sort' => 'order' ]
]; ];
public static function getBlogEntries() public static function getBlogEntries()

View file

@ -2,8 +2,8 @@
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class BlogTags extends Model { class BlogTags extends DashboardModel
{
/** /**
* The database table used by the model. * The database table used by the model.
* *
@ -11,4 +11,9 @@ class BlogTags extends Model {
*/ */
protected $table = 'blog_tags'; protected $table = 'blog_tags';
public static $dashboard_type = 'list';
public static $dashboard_columns = [
[ 'type' => 'string', 'name' => 'name' ]
];
} }

View file

@ -117,6 +117,33 @@ class DashboardModel extends Model
*/ */
public static $dashboard_id_link = []; public static $dashboard_id_link = [];
/**
* The default image extension when none is set
*
* @var string
*/
public static $default_image_ext = 'jpg';
/**
* Functionality to run when various events occur
*
* @return null
*/
public static function boot() {
parent::boot();
static::deleting(function($item) {
// delete associated images and files if they exist
foreach ($item::$dashboard_columns as $column) {
if ($column['type'] == 'image') {
$item->deleteImage($column['name'], false);
} else if ($column['type'] == 'file') {
$item->deleteFile($column['name'], false);
}
}
});
}
/** /**
* Returns the dashboard heading * Returns the dashboard heading
* *
@ -155,7 +182,6 @@ class DashboardModel extends Model
$max_width = 0; $max_width = 0;
$max_height = 0; $max_height = 0;
$main_ext = 'jpg';
// Retrieve the column // Retrieve the column
$column = static::getColumn($name); $column = static::getColumn($name);
@ -165,9 +191,11 @@ class DashboardModel extends Model
return 'no-such-column-fail'; return 'no-such-column-fail';
} }
// Update the extension if it's been configured // Use the configured image extension or fall back on the default if none is set
if (array_key_exists('ext', $column)) { if (array_key_exists('ext', $column)) {
$main_ext = $column['ext']; $main_ext = $column['ext'];
} else {
$main_ext = $this::$default_image_ext;
} }
// Create the directory if it doesn't exist // Create the directory if it doesn't exist
@ -254,7 +282,6 @@ class DashboardModel extends Model
} }
// Set up our variables // Set up our variables
$main_ext = 'jpg';
$extensions = []; $extensions = [];
// Retrieve the column // Retrieve the column
@ -265,9 +292,11 @@ class DashboardModel extends Model
return 'no-such-column-fail'; return 'no-such-column-fail';
} }
// Update the extension if it's been configured // Use the configured image extension or fall back on the default if none is set
if (array_key_exists('ext', $column)) { if (array_key_exists('ext', $column)) {
$main_ext = $column['ext']; $main_ext = $column['ext'];
} else {
$main_ext = $this::$default_image_ext;
} }
// Build the set of extensions to delete // Build the set of extensions to delete

View file

@ -14,12 +14,13 @@ A Hypothetical website template for bootstrapping new projects.
## Major Components ## Major Components
* Bootstrap 5 * Bootstrap 5
* Fontawesome 5 * Browsersync
* Gsap 3 * Fontawesome
* Gulp 4 * Gsap
* Jquery 3 * Gulp
* Jquery
* Laravel 9 * Laravel 9
* Sass 1.32 * Sass
* Vue 3 (Optional) * Vue 3 (Optional)
## Setup ## Setup
@ -71,13 +72,13 @@ Reading through its contents is encouraged for a complete understanding of what
* `gulp`: Update the compiled javascript and css in `public/js` and `public/css`, and copy fonts to `public/fonts`. * `gulp`: Update the compiled javascript and css in `public/js` and `public/css`, and copy fonts to `public/fonts`.
* `gulp --production`: Does the same as `gulp` except the compiled javascript and css is minified, and console logging is removed from the javascript (good for production deployments). * `gulp --production`: Does the same as `gulp` except the compiled javascript and css is minified, and console logging is removed from the javascript (good for production deployments).
* `gulp default watch`: Does the same as `gulp` but continues running to watch for changes to files so it can recompile updated assets and reload them in the browser using BrowserSync (good for development environments). * `gulp default watch`: Does the same as `gulp` but continues running to watch for changes to files so it can recompile updated assets and reload them in the browser using Browsersync (good for development environments).
**NOTE**: If `gulp` isn't installed globally or its version is less than `4`, you should use the version included in `node_modules` by running `"$(npm bin)/gulp"` in place of the `gulp` command. **NOTE**: If `gulp` isn't installed globally or its version is less than `4`, you should use the version included in `node_modules` by running `"$(npm bin)/gulp"` in place of the `gulp` command.
### BrowserSync ### Browsersync
BrowserSync is used to keep the browser in sync with your code when running the `watch` task with gulp. Browsersync is used to keep the browser in sync with your code when running the `watch` task with gulp.
## Public ## Public
@ -148,6 +149,13 @@ In a Vue.js component:
## Dashboard ## Dashboard
### Important Note
The naming convention of dashboard database tables and model classes should be the following:
* Database table names should be lower case with hyphen separators: `your_table_name`
* Model classes should be the same name but in camel case with its first character capitalized: `YourTableName.php` and `class YourTableName extends DashboardModel`
### Registration ### Registration
The `REGISTRATION` variable in the `.env` file controls whether a new dashboard user can be registered. The `REGISTRATION` variable in the `.env` file controls whether a new dashboard user can be registered.
@ -229,7 +237,7 @@ Models with their `$dashboard_type` set to `edit` also use:
* `date-time`: Date and time selection tool for date/time data * `date-time`: Date and time selection tool for date/time data
* `mkd`: Multi-line text input field with a markdown editor * `mkd`: Multi-line text input field with a markdown editor
* `select`: Text input via option select * `select`: Text input via option select
* `list`: One or more items saved to a connected table * `list`: One or more `text` or `image` 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
@ -240,9 +248,19 @@ Models with their `$dashboard_type` set to `edit` also use:
* `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
* `delete`: (optional for `file` and `image`) Enables a delete button for the upload when set to true * `delete`: (optional for `file` and `image`) Enables a delete button for the upload when set to true
* `ext`: (required by `file` and optional for `image`) Configures the file extension of the upload (`image` defaults to `jpg`) * `ext`: (required by `file` and optional for `image`) Configures the file extension of the upload (`image` defaults to `jpg`)
* `model`: (required by `list`) The class name of the model that the list will be generated from
* `foreign` (required by `list`) The name of the list table's foreign id column that references the id on the current table
* `sort` (required by `list`) The name of the list table's column that the order will be stored in
* `max_width`: (optional for `image`) Configures the maximum width of an image upload (defaults to `0` which sets no maximum width) * `max_width`: (optional for `image`) Configures the maximum width of an image upload (defaults to `0` which sets no maximum width)
* `max_height`: (optional for `image`) Configures the maximum height of an image upload (defaults to `0` which sets no maximum height) * `max_height`: (optional for `image`) Configures the maximum height of an image upload (defaults to `0` which sets no maximum height)
Models with their `$dashboard_type` set to `list` also use:
* `type`: The column type which can be any of the following:
* `string`: Single-line text input field
* `image`: Fields that contain image uploads
* `ext`: (optional for `image`) Configures the file extension of the upload (`image` defaults to `jpg`)
An example of the `$dashboard_columns` array in a model with its `$dashboard_type` set to `view`: An example of the `$dashboard_columns` array in a model with its `$dashboard_type` set to `view`:
```php ```php
@ -262,6 +280,15 @@ An example of the `$dashboard_columns` array in a model with its `$dashboard_typ
[ 'name' => 'title', 'required' => true, 'unique' => true, 'type' => 'string' ], [ 'name' => 'title', 'required' => true, 'unique' => true, 'type' => 'string' ],
[ 'name' => 'body', 'required' => true, 'type' => 'mkd' ], [ 'name' => 'body', 'required' => true, 'type' => 'mkd' ],
[ 'name' => 'header-image', 'title' => 'Header Image', 'type' => 'image', 'delete' => true, 'ext' => 'jpg' ], [ 'name' => 'header-image', 'title' => 'Header Image', 'type' => 'image', 'delete' => true, 'ext' => 'jpg' ],
[ 'name' => 'tags', 'type' => 'list', 'model' => 'BlogTags', 'columns' => [ 'name' ], 'foreign' => 'blog_id', 'sort' => 'order' ] [ 'name' => 'tags', 'type' => 'list', 'model' => 'BlogTags', 'foreign' => 'blog_id', 'sort' => 'order' ]
];
```
An example of the `$dashboard_columns` array in a model with its `$dashboard_type` set to `list`:
```php
public static $dashboard_columns = [
[ 'type' => 'string', 'name' => 'name' ],
[ 'type' => 'image', 'name' => 'photo' ]
]; ];
``` ```

View file

@ -323,17 +323,17 @@ function editItemInit() {
$currencyInputs = $(".currency-input"), $currencyInputs = $(".currency-input"),
$datePickers = $(".date-picker"), $datePickers = $(".date-picker"),
$mkdEditors = $(".mkd-editor"), $mkdEditors = $(".mkd-editor"),
$fileUploads = $(".file-upload"),
$imgUploads = $(".image-upload"),
$lists = $(".list"), $lists = $(".list"),
$token = $("#token"), $token = $("#token"),
model = $editItem.data("model"), model = $editItem.data("model"),
id = $editItem.data("id"), operation = $editItem.data("id") === "new" ? "create" : "update";
operation = id === "new" ? "create" : "update";
let allowTimes = [], let $imgUploads = [],
$fileUploads = [],
allowTimes = [],
easymde = [], easymde = [],
formData = {}, formData = {},
id = $editItem.data("id"),
submitting = false, submitting = false,
hours, hours,
minutes, minutes,
@ -404,30 +404,33 @@ function editItemInit() {
value = []; value = [];
$this.find(".list-items .list-items-row").each(function(index, row) { $this.find(".list-items .list-items-row").each(function(index, row) {
const rowData = {}; const rowData = {},
id = $(row).data("id");
$(row).find(".list-items-row-input-inner").each(function(index, input) { $(row).find(".list-items-row-content-inner").each(function(index, inner) {
const $input = $(input), const $inner = $(inner),
column = $input.data("column"), $input = $inner.find(".list-input"),
value = $input.val(); column = $inner.data("column");
rowData[column] = value; if ($inner.data("type") === "string") {
rowData[column] = { type: "string", value: $input.val() };
}
}); });
value.push(rowData); value.push({ id: typeof id === "undefined" ? "new" : id, data: rowData });
}); });
addFormData("list", column, value); addFormData("list", column, value);
}); });
}; };
const uploadFile = function(row_id, currentFile) { const uploadFile = function(currentFile) {
let file, fileUpload; let file, fileUpload;
// functionality to run on success // functionality to run on success
const returnSuccess = function() { const returnSuccess = function() {
loadingModal("hide"); loadingModal("hide");
window.location.href = `/dashboard/edit/${model}/${row_id}`; window.location.href = `/dashboard/edit/${model}/${id}`;
}; };
// add the file from the file upload box for file-upload class elements // add the file from the file upload box for file-upload class elements
@ -439,9 +442,9 @@ function editItemInit() {
// add the file, id and model to the formData variable // add the file, id and model to the formData variable
file.append("file", fileUpload.files[0]); file.append("file", fileUpload.files[0]);
file.append("name", $(fileUpload).attr("name")); file.append("name", $(fileUpload).data("column"));
file.append("id", row_id); file.append("id", $(fileUpload).data("id"));
file.append("model", model); file.append("model", $(fileUpload).data("model"));
$.ajax({ $.ajax({
type: "POST", type: "POST",
@ -452,7 +455,7 @@ function editItemInit() {
beforeSend: function(xhr) { xhr.setRequestHeader("X-CSRF-TOKEN", $token.val()); } beforeSend: function(xhr) { xhr.setRequestHeader("X-CSRF-TOKEN", $token.val()); }
}).always(function(response) { }).always(function(response) {
if (response === "success") { if (response === "success") {
uploadFile(row_id, currentFile + 1); uploadFile(currentFile + 1);
} else { } else {
loadingModal("hide"); loadingModal("hide");
@ -462,19 +465,19 @@ function editItemInit() {
} }
}); });
} else { } else {
uploadFile(row_id, currentFile + 1); uploadFile(currentFile + 1);
} }
} else { } else {
returnSuccess(); returnSuccess();
} }
}; };
const uploadImage = function(row_id, currentImage) { const uploadImage = function(currentImage) {
let file, imgUpload; let file, imgUpload;
// functionality to run on success // functionality to run on success
const returnSuccess = function() { const returnSuccess = function() {
uploadFile(row_id, 0); uploadFile(0);
}; };
// add the image from the image upload box for image-upload class elements // add the image from the image upload box for image-upload class elements
@ -486,9 +489,9 @@ function editItemInit() {
// add the file, id and model to the formData variable // add the file, id and model to the formData variable
file.append("file", imgUpload.files[0]); file.append("file", imgUpload.files[0]);
file.append("name", $(imgUpload).attr("name")); file.append("name", $(imgUpload).data("column"));
file.append("id", row_id); file.append("id", $(imgUpload).data("id"));
file.append("model", model); file.append("model", $(imgUpload).data("model"));
$.ajax({ $.ajax({
type: "POST", type: "POST",
@ -499,7 +502,7 @@ function editItemInit() {
beforeSend: function(xhr) { xhr.setRequestHeader("X-CSRF-TOKEN", $token.val()); } beforeSend: function(xhr) { xhr.setRequestHeader("X-CSRF-TOKEN", $token.val()); }
}).always(function(response) { }).always(function(response) {
if (response === "success") { if (response === "success") {
uploadImage(row_id, currentImage + 1); uploadImage(currentImage + 1);
} else { } else {
loadingModal("hide"); loadingModal("hide");
@ -509,7 +512,7 @@ function editItemInit() {
} }
}); });
} else { } else {
uploadImage(row_id, currentImage + 1); uploadImage(currentImage + 1);
} }
} else { } else {
returnSuccess(); returnSuccess();
@ -524,7 +527,7 @@ function editItemInit() {
// initialize image deletion // 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("column");
if (!submitting) { if (!submitting) {
submitting = true; submitting = true;
@ -535,8 +538,8 @@ function editItemInit() {
type: "DELETE", type: "DELETE",
url: "/dashboard/image-delete", url: "/dashboard/image-delete",
data: { data: {
id: id, id: $this.data("id"),
model: model, model: $this.data("model"),
name: name, name: name,
_token: $token.val() _token: $token.val()
} }
@ -559,7 +562,7 @@ function editItemInit() {
// initialize file deletion // 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("column"),
ext = $this.data("ext"); ext = $this.data("ext");
if (!submitting) { if (!submitting) {
@ -571,8 +574,8 @@ function editItemInit() {
type: "DELETE", type: "DELETE",
url: "/dashboard/file-delete", url: "/dashboard/file-delete",
data: { data: {
id: id, id: $this.data("id"),
model: model, model: $this.data("model"),
name: name, name: name,
ext: ext, ext: ext,
_token: $token.val() _token: $token.val()
@ -617,7 +620,7 @@ function editItemInit() {
const $row = $(row); const $row = $(row);
// initialize delete button functionality // initialize delete button functionality
$row.find(".list-items-row-button").off("click").on("click", function() { $row.find(".list-items-row-buttons-delete").off("click").on("click", function() {
$row.remove(); $row.remove();
initSort(); initSort();
contentChanged(); contentChanged();
@ -627,24 +630,40 @@ function editItemInit() {
const initList = function() { const initList = function() {
$list.find(".list-data-row").each(function(rowIndex, row) { $list.find(".list-data-row").each(function(rowIndex, row) {
// Add the values from the current data row to the template const id = $(row).data("id");
// add the values from the current data row to the template
$(row).find(".list-data-row-item").each(function(itemIndex, item) { $(row).find(".list-data-row-item").each(function(itemIndex, item) {
const $item = $(item), const $item = $(item),
column = $item.data("column"), column = $item.data("column"),
value = $item.data("value"); value = $item.data("value"),
type = $item.data("type");
$template.find(".list-items-row-input-inner").each(function(inputIndex, input) { $template.find(".list-items-row-content-inner").each(function(inputIndex, inner) {
const $input = $(input); const $inner = $(inner);
if ($input.data("column") === column) { if ($inner.data("column") === column) {
$input.val(value); if (type === "string") {
$inner.find(".list-input").val(value);
} else if (type === "image" && value !== "") {
$inner.find(".image-link").attr("href", value).addClass("active");
$inner.find(".image-preview").attr("src", value);
}
} }
}); });
}); });
// Add the populated template to the list of items then clear the template values // add the populated template to the list of items
$template.find(".list-items-row").clone().appendTo($items); $template.find(".list-items-row").clone().appendTo($items);
$template.find(".list-items-row-input-inner").val("");
// set the id for the list items row
$items.find(".list-items-row").last().data("id", id);
$items.find(".list-items-row").last().find(".list-input").data("id", id);
// clear the template values
$template.find(".list-input").val("");
$template.find(".image-link").attr("href", "").removeClass("active");
$template.find(".image-preview").attr("src", "");
}); });
initSort(); initSort();
@ -740,6 +759,10 @@ function editItemInit() {
if (!submitting && changes) { if (!submitting && changes) {
submitting = true; submitting = true;
// find the image and file upload inputs
$imgUploads = $(".image-upload");
$fileUploads = $(".file-upload");
// show the loading modal // show the loading modal
loadingModal("show"); loadingModal("show");
@ -755,8 +778,26 @@ function editItemInit() {
}).always(function(response) { }).always(function(response) {
let message = ""; let message = "";
if ((/^id:[0-9][0-9]*$/).test(response)) { if (typeof response.id !== "undefined") {
uploadImage(response.replace(/^id:/, ""), 0); id = response.id;
// Add the appropriate ids to each list item input
if (Object.keys(response.lists).length) {
Object.keys(response.lists).forEach(function(key) {
$(`#${key}`).find(".list-items").first().find(".list-items-row").each(function(index) {
const listItemId = response.lists[key][index];
$(this).data("id", listItemId);
$(this).find(".list-input").data("id", listItemId);
});
});
}
// Add the current row id to each image and file upload input
$(".image-upload, .file-upload").not(".list-input").data("id", response.id);
// Start uploading images (and then files)
uploadImage(0);
} else { } else {
loadingModal("hide"); loadingModal("hide");
@ -764,6 +805,8 @@ function editItemInit() {
message = `<strong>${response.replace(/'/g, "").replace(/^[^:]*:/, "").replace(/,([^,]*)$/, "</strong> and <strong>$1").replace(/,/g, "</strong>, <strong>")}</strong> must be unique`; message = `<strong>${response.replace(/'/g, "").replace(/^[^:]*:/, "").replace(/,([^,]*)$/, "</strong> and <strong>$1").replace(/,/g, "</strong>, <strong>")}</strong> must be unique`;
} else if ((/^required:/).test(response)) { } 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`; message = `<strong>${response.replace(/'/g, "").replace(/^[^:]*:/, "").replace(/,([^,]*)$/, "</strong> and <strong>$1").replace(/,/g, "</strong>, <strong>")}</strong> must not be empty`;
} else if ((/^invalid-list-model:/).test(response)) {
message = `<strong>${response.replace(/'/g, "").replace(/^[^:]*:/, "").replace(/,([^,]*)$/, "</strong> and <strong>$1").replace(/,/g, "</strong>, <strong>")}</strong> is not a valid list`;
} else { } else {
message = `Failed to <strong>${operation}</strong> record`; message = `Failed to <strong>${operation}</strong> record`;
} }

View file

@ -917,6 +917,12 @@ form {
} }
} }
.container-fluid {
@include media-breakpoint-down(sm) {
padding: 0px;
}
}
.list { .list {
margin-bottom: pxrem(15); margin-bottom: pxrem(15);
padding-bottom: pxrem(15); padding-bottom: pxrem(15);
@ -927,17 +933,37 @@ form {
} }
&-items-row { &-items-row {
position: relative;
margin-bottom: pxrem(10); margin-bottom: pxrem(10);
display: flex; padding: pxrem(8) pxrem(8) pxrem(8) pxrem(33);
align-items: center; border-radius: pxrem(6);
background-color: darken($c-input-bg, 2%);
@include media-breakpoint-up(lg) {
display: flex;
padding: pxrem(5) pxrem(5) pxrem(5) pxrem(30);
align-items: center;
}
&:not(:last-child) {
@include media-breakpoint-down(lg) {
margin-bottom: pxrem(15);
}
}
.sort-icon { .sort-icon {
margin-right: pxrem(10); position: absolute;
top: pxrem(11);
left: pxrem(10);
display: inline-block; display: inline-block;
opacity: 1; opacity: 1;
transition: opacity 100ms; transition: opacity 100ms;
cursor: grab; cursor: grab;
@include media-breakpoint-up(lg) {
top: pxrem(9);
}
&-inner { &-inner {
position: relative; position: relative;
top: pxrem(2); top: pxrem(2);
@ -968,17 +994,29 @@ form {
} }
} }
&-input, &-button { &-content, &-button {
&:not(:first-child) { @include media-breakpoint-down(lg) {
margin-left: pxrem(5); width: 100%;
} }
&:not(:last-child) { &:not(:last-child) {
margin-right: pxrem(5); @include media-breakpoint-down(lg) {
margin-bottom: pxrem(10);
}
@include media-breakpoint-up(lg) {
margin-right: pxrem(5);
}
}
&:not(:first-child) {
@include media-breakpoint-up(lg) {
margin-left: pxrem(5);
}
} }
} }
&-input { &-content {
position: relative; position: relative;
&.wide { &.wide {
@ -986,29 +1024,52 @@ form {
} }
&-inner { &-inner {
margin-bottom: 0px; $row-height: pxrem(36);
} display: flex;
height: $row-height;
align-items: center;
&-overlay { .list-input {
overflow: hidden; margin-bottom: 0px;
position: absolute; height: auto;
top: 0px; }
left: 0px;
width: 100%; .image-link {
height: 100%; margin-right: pxrem(10);
padding: pxrem(5) pxrem(8); display: block;
background-color: $c-input-bg; width: pxrem(50);
white-space: nowrap; height: $row-height;
text-overflow: ellipsis; background-color: $c-input-bg;
pointer-events: none;
&:not(.active) {
display: none;
}
.image-preview {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
}
} }
} }
&-button { &-buttons {
min-width: pxrem(70); @include media-breakpoint-up(lg) {
height: $label-height; display: flex;
border: 1px solid fade-out($c-text, 0.75); flex-grow: 1;
border-radius: pxrem(4); justify-content: flex-end;
}
&-delete {
min-width: pxrem(70);
height: $label-height;
border: 1px solid fade-out($c-text, 0.75);
border-radius: pxrem(4);
background-color: $c-dashboard-delete;
color: $c-text-light;
}
} }
} }
@ -1016,6 +1077,11 @@ form {
height: $label-height; height: $label-height;
border: 1px solid fade-out($c-text, 0.75); border: 1px solid fade-out($c-text, 0.75);
border-radius: pxrem(4); border-radius: pxrem(4);
@include media-breakpoint-down(lg) {
margin-top: pxrem(15);
width: 100%;
}
} }
} }
@ -1101,6 +1167,7 @@ form {
padding: pxrem(5) pxrem(10); padding: pxrem(5) pxrem(10);
border-radius: pxrem(5); border-radius: pxrem(5);
text-transform: uppercase; text-transform: uppercase;
text-decoration: none;
transition: background-color 150ms; transition: background-color 150ms;
cursor: pointer; cursor: pointer;
@ -1153,6 +1220,10 @@ form {
margin-top: pxrem(10); margin-top: pxrem(10);
margin-bottom: pxrem(20); margin-bottom: pxrem(20);
} }
@include media-breakpoint-down(sm) {
margin-bottom: 0px;
}
} }
} }

View file

@ -22,7 +22,7 @@
@php @php
$value = $item !== null ? $item[$column['name']] : ''; $value = $item !== null ? $item[$column['name']] : '';
$type = $id == 'new' && array_key_exists('type-new', $column) ? $column['type-new'] : $column['type']; $type = $id == 'new' && array_key_exists('type-new', $column) ? $column['type-new'] : $column['type'];
$ext = array_key_exists('ext', $column) ? $column['ext'] : 'jpg'; $ext = array_key_exists('ext', $column) ? $column['ext'] : $default_img_ext;
@endphp @endphp
@if($type == 'hidden') @if($type == 'hidden')
@ -72,6 +72,13 @@
@endphp @endphp
@endif @endif
@if(gettype($select_title))
@php
$select_value = $select_value ? 1 : 0;
$select_title = $select_title ? 'true' : 'false';
@endphp
@endif
@if($select_value === $value) @if($select_value === $value)
<option value="{{ $select_value }}" selected="selected">{{ $select_title }}</option> <option value="{{ $select_value }}" selected="selected">{{ $select_title }}</option>
@else @else
@ -80,9 +87,14 @@
@endforeach @endforeach
</select> </select>
@elseif($type == 'list') @elseif($type == 'list')
@php
$list_model = App\Dashboard::getModel($value['model']);
$list_columns = $list_model::$dashboard_columns;
@endphp
<div class="list" id="{{ $column['name'] }}"> <div class="list" id="{{ $column['name'] }}">
<div class="list-template"> <div class="list-template">
<div class="list-items-row"> <div class="list-items-row" data-id="new">
<div class="sort-icon" title="Click and drag to reorder"> <div class="sort-icon" title="Click and drag to reorder">
<div class="sort-icon-inner"> <div class="sort-icon-inner">
<div class="sort-icon-inner-bar"></div> <div class="sort-icon-inner-bar"></div>
@ -91,22 +103,46 @@
</div> </div>
</div> </div>
@foreach($column['columns'] as $list_column) @foreach($list_columns as $list_column)
<div class="list-items-row-input {{ count($column['columns']) == 1 ? 'wide' : '' }}"> <div class="list-items-row-content {{ count($list_columns) == 1 ? 'wide' : '' }}">
<input class="list-items-row-input-inner" data-column="{{ $list_column }}" placeholder="{{ $list_column }}" /> <div class="list-items-row-content-inner" data-column="{{ $list_column['name'] }}" data-type="{{ $list_column['type'] }}">
@if($list_column['type'] == 'string')
<input class="list-input" placeholder="{{ $list_column['name'] }}" />
@elseif($list_column['type'] == 'image')
<a class="image-link" href="" target="_blank"><img class="image-preview" src="" /></a>
<input class="list-input image-upload" type="file" data-column="{{ $list_column['name'] }}" data-model="{{ $value['model'] }}" />
@endif
</div>
</div> </div>
@endforeach @endforeach
<button class="list-items-row-button" type="button">Delete</button> <div class="list-items-row-buttons">
<button class="list-items-row-buttons-delete" type="button">Delete</button>
</div>
</div> </div>
</div> </div>
<div class="list-data"> <div class="list-data">
@if($id != 'new') @if($id != 'new')
@foreach($value as $row) @foreach($value['list'] as $row)
<div class="list-data-row"> <div class="list-data-row" data-id="{{ $row['id'] }}">
@foreach($column['columns'] as $list_column) @foreach($list_columns as $list_column)
<div class="list-data-row-item" data-column="{{ $list_column }}" data-value="{{ $row[$list_column] }}"></div> @if($list_column['type'] == 'string')
@php
$list_column_value = $row[$list_column['name']]
@endphp
@elseif($list_column['type'] == 'image')
@php
$list_column_item = $list_model::find($row['id']);
$list_column_image_ext = array_key_exists('ext', $list_column) ? $list_column['ext'] : $default_img_ext;
$list_column_image_path = $list_model->getUploadsPath('image') . $row['id'] . '-' . $list_column['name'] . '.' . $list_column_image_ext;
$list_column_value = file_exists(public_path($list_column_image_path)) ? $list_column_image_path . '?version=' . $list_column_item->timestamp() : '';
@endphp
{{ $list_column_image_path }}
@endif
<div class="list-data-row-item" data-type="{{ $list_column['type'] }}" data-column="{{ $list_column['name'] }}" data-value="{{ $list_column_value }}"></div>
@endforeach @endforeach
</div> </div>
@endforeach @endforeach
@ -121,14 +157,14 @@
$current_image = "/uploads/$model/img/$id-" . $column['name'] . '.' . $ext; $current_image = "/uploads/$model/img/$id-" . $column['name'] . '.' . $ext;
@endphp @endphp
<input class="image-upload" type="file" name="{{ $column['name'] }}" id="{{ $column['name'] }}" /> <input class="image-upload" type="file" data-column="{{ $column['name'] }}" data-model="{{ $model }}" data-id="{{ $id }}" />
@if(file_exists(base_path() . '/public' . $current_image)) @if(file_exists(base_path() . '/public' . $current_image))
<div id="current-image-{{ $column['name'] }}"> <div id="current-image-{{ $column['name'] }}">
<img class="current-image" src="{{ $current_image }}?version={{ $item->timestamp() }}" /> <img class="current-image" src="{{ $current_image }}?version={{ $item->timestamp() }}" />
@if(array_key_exists('delete', $column) && $column['delete']) @if(array_key_exists('delete', $column) && $column['delete'])
<span class="edit-button delete image" data-name="{{ $column['name'] }}"> <span class="edit-button delete image" data-column="{{ $column['name'] }}" data-model="{{ $model }}" data-id="{{ $id }}">
Delete Image Delete Image
</span> </span>
@endif @endif
@ -139,14 +175,14 @@
$current_file = "/uploads/$model/files/$id-" . $column['name'] . '.' . $column['ext']; $current_file = "/uploads/$model/files/$id-" . $column['name'] . '.' . $column['ext'];
@endphp @endphp
<input class="file-upload" type="file" name="{{ $column['name'] }}" id="{{ $column['name'] }}" /> <input class="file-upload" type="file" data-column="{{ $column['name'] }}" data-model="{{ $model }}" data-id="{{ $id }}" />
@if(file_exists(base_path() . '/public' . $current_file)) @if(file_exists(base_path() . '/public' . $current_file))
<div id="current-file-{{ $column['name'] }}"> <div id="current-file-{{ $column['name'] }}">
<a class="edit-button view" href="{{ $current_file }}?version={{ $item->timestamp() }}" target="_blank">View {{ strtoupper($column['ext']) }}</a> <a class="edit-button view" href="{{ $current_file }}?version={{ $item->timestamp() }}" target="_blank">View {{ strtoupper($column['ext']) }}</a>
@if(array_key_exists('delete', $column) && $column['delete']) @if(array_key_exists('delete', $column) && $column['delete'])
<span class="edit-button delete file" data-name="{{ $column['name'] }}"> <span class="edit-button delete file" data-column="{{ $column['name'] }}" data-model="{{ $model }}" data-id="{{ $id }}">
Delete {{ strtoupper($column['ext']) }} Delete {{ strtoupper($column['ext']) }}
</span> </span>
@endif @endif