From a1ac81a73a5ca93f4d0732ba44d2db68d7495fe4 Mon Sep 17 00:00:00 2001 From: Tyler Date: Sat, 26 Dec 2020 19:34:07 -0500 Subject: [PATCH] Add attachments, more pages --- app/Http/Controllers/BookController.php | 21 +- app/Http/Controllers/LocationController.php | 4 +- app/Http/Controllers/LookupController.php | 29 +- app/Http/Controllers/MainController.php | 45 +- app/Models/Book.php | 3 +- app/Models/Location.php | 4 + app/Providers/AppServiceProvider.php | 7 +- .../BookInformation/CachedService.php | 19 + app/Services/BookInformation/GoogleBooks.php | 5 +- composer.json | 1 + composer.lock | 396 +- config/attachments.php | 98 + config/scout.php | 106 + config/scout_elastic.php | 12 + package.json | 10 +- public/js/app.js | 19309 ++-------------- resources/js/app.js | 66 +- resources/js/bootstrap.js | 22 +- resources/views/add.blade.php | 11 +- resources/views/books/show.blade.php | 18 + resources/views/books/table.blade.php | 0 resources/views/index.blade.php | 6 +- resources/views/layouts/main.blade.php | 1 - resources/views/locations/index.blade.php | 21 + resources/views/locations/show.blade.php | 24 + resources/views/partials/navbar.blade.php | 5 +- routes/web.php | 2 +- storage/app/.gitignore | 1 + storage/app/{public => temp}/.gitignore | 2 +- 29 files changed, 2349 insertions(+), 17899 deletions(-) create mode 100644 app/Services/BookInformation/CachedService.php create mode 100644 config/attachments.php create mode 100644 config/scout.php create mode 100644 config/scout_elastic.php create mode 100644 resources/views/books/show.blade.php create mode 100644 resources/views/books/table.blade.php create mode 100644 resources/views/locations/index.blade.php create mode 100644 resources/views/locations/show.blade.php rename storage/app/{public => temp}/.gitignore (52%) diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index cc9ea14..cc2963a 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Models\Book; use Illuminate\Http\Request; class BookController extends Controller { @@ -40,7 +41,13 @@ class BookController extends Controller { * @return \Illuminate\Http\Response */ public function show($id) { - // + $book = Book::find($id); + + if (!$book) { + abort(404); + } + + return view('books.show', [ 'book' => $book ]); } /** @@ -71,6 +78,16 @@ class BookController extends Controller { * @return \Illuminate\Http\Response */ public function destroy($id) { - // + $book = Book::find($id); + + if (!$book) { + abort(404); + } + + $book->delete(); + + return response()->json([ + 'success' => true + ]); } } diff --git a/app/Http/Controllers/LocationController.php b/app/Http/Controllers/LocationController.php index 5c825c2..b534fa6 100644 --- a/app/Http/Controllers/LocationController.php +++ b/app/Http/Controllers/LocationController.php @@ -12,7 +12,7 @@ class LocationController extends Controller { * @return \Illuminate\Http\Response */ public function index() { - // + return view('locations.index', [ 'locations' => Location::paginate(15) ]); } /** @@ -47,7 +47,7 @@ class LocationController extends Controller { abort(404); } - return view('location', [ 'location' => $location ]); + return view('locations.show', [ 'location' => $location, 'rows' => $location->books()->paginate(15) ]); } /** diff --git a/app/Http/Controllers/LookupController.php b/app/Http/Controllers/LookupController.php index b4b17a5..330f20e 100644 --- a/app/Http/Controllers/LookupController.php +++ b/app/Http/Controllers/LookupController.php @@ -2,20 +2,31 @@ namespace App\Http\Controllers; +use App\Models\Book; +use App\Services\BookInformation\BookLookupService; use Cache; -use App\Services\BookInformation\GoogleBooks; -use Symfony\Component\HttpKernel\Exception\HttpException; class LookupController { - public function lookup($isbn) { - if (!preg_match('/^(\d+)$/', $isbn)) { - throw new HttpException(400); + public function lookup(BookLookupService $service, $isbn) { + $result = $service->lookup($isbn); + + $arr = [ + 'success' => false + ]; + + if ($result) { + $arr = array_merge($arr, [ + 'success' => true, + 'data' => $result + ]); } - return Cache::remember('isbn_' . $isbn, 86400, function() use ($isbn) { - $google_books = new GoogleBooks(); + $book = Book::where('barcode', $isbn)->first(); - return $google_books->lookup($isbn); - }); + if ($book) { + $arr['warning'] = 'Item already exists.
Location: ' . $book->location->name; + } + + return response()->json($arr); } } \ No newline at end of file diff --git a/app/Http/Controllers/MainController.php b/app/Http/Controllers/MainController.php index 203c5f8..8ef556c 100644 --- a/app/Http/Controllers/MainController.php +++ b/app/Http/Controllers/MainController.php @@ -5,6 +5,9 @@ namespace App\Http\Controllers; use App\Models\Author; use App\Models\Book; use App\Models\Location; +use App\Services\BookInformation\BookLookupService; +use App\Services\BookInformation\GoogleBooks; +use GuzzleHttp\Client; use Illuminate\Http\Request; use Illuminate\Support\Arr; @@ -16,24 +19,21 @@ class MainController extends Controller { } public function search(Request $request) { - $this->validate($request, [ - 'query' => [ 'required' ] - ]); - - $rows = Book::where('name', 'LIKE', '%' . $request->get('query') . '%')->paginate(15); + $rows = Book::search($request->get('query'))->paginate(15); return view('index', [ 'rows' => $rows ]); } public function add(Request $request) { return view('add', [ + 'locations' => Location::get(), 'old' => array_filter($request->old('books', []), function($item) { return !empty($item['barcode']); }) ]); } - public function save(Request $request) { + public function save(Request $request, BookLookupService $service) { $this->validate($request, [ 'location' => [ 'required' ] ]); @@ -69,6 +69,39 @@ class MainController extends Controller { $authors = array_map(function($author) { return $author->id; }, $authors); $book->authors()->attach($authors); + + // Lookup info from cache + $res = $service->lookup($item['barcode']); + + if (!empty($res)) { + if ($thumbnail = data_get('images.thumbnail', $res)) { + $file = $this->downloadFile($thumbnail); + + $book->attach('thumbnail', $file); + } + } } } + + /** + * Download a file and return the local temp path. + * + * @param $url + * @return string|null + */ + private function downloadFile($url) { + $client = new Client(); + + $path = tempnam(storage_path('app/temp'), 'image'); + + $res = $client->get($url, [ + 'sink' => $path + ]); + + if ($res->getStatusCode() != 200) { + return null; + } + + return $path; + } } diff --git a/app/Models/Book.php b/app/Models/Book.php index 5545eda..3556a37 100644 --- a/app/Models/Book.php +++ b/app/Models/Book.php @@ -3,12 +3,13 @@ namespace App\Models; use App\Services\Search\BookConfigurator; +use Bnb\Laravel\Attachments\HasAttachment; use Illuminate\Database\Eloquent\Model; use ScoutElastic\Searchable; class Book extends Model { - use Searchable; + use Searchable, HasAttachment; protected $indexConfigurator = BookConfigurator::class; diff --git a/app/Models/Location.php b/app/Models/Location.php index 2a7b049..6fc3a54 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -14,4 +14,8 @@ class Location extends Model { protected $fillable = [ 'name', ]; + + public function books() { + return $this->hasMany(Book::class); + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ad1d089..5b07f6f 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,9 @@ namespace App\Providers; +use App\Services\BookInformation\BookLookupService; +use App\Services\BookInformation\CachedService; +use App\Services\BookInformation\GoogleBooks; use Illuminate\Pagination\Paginator; use Schema; use Illuminate\Support\ServiceProvider; @@ -23,6 +26,8 @@ class AppServiceProvider extends ServiceProvider { * @return void */ public function register() { - // + $this->app->singleton(BookLookupService::class, function() { + return new CachedService(new GoogleBooks()); + }); } } diff --git a/app/Services/BookInformation/CachedService.php b/app/Services/BookInformation/CachedService.php new file mode 100644 index 0000000..5193ee2 --- /dev/null +++ b/app/Services/BookInformation/CachedService.php @@ -0,0 +1,19 @@ +service = $service; + } + + public function lookup($isbn) { + return Cache::remember('isbn_' . $isbn, 86400, function() use ($isbn) { + return $this->service->lookup($isbn); + }); + } +} \ No newline at end of file diff --git a/app/Services/BookInformation/GoogleBooks.php b/app/Services/BookInformation/GoogleBooks.php index 7b03106..86a39e7 100644 --- a/app/Services/BookInformation/GoogleBooks.php +++ b/app/Services/BookInformation/GoogleBooks.php @@ -22,9 +22,10 @@ class GoogleBooks implements BookLookupService { */ $volume = Arr::first($results->getItems())->getVolumeInfo(); - return [ + return (object) [ 'title' => $volume->getTitle(), - 'authors' => $volume->getAuthors() + 'authors' => $volume->getAuthors(), + 'images' => $volume->getImageLinks(), ]; } } \ No newline at end of file diff --git a/composer.json b/composer.json index c7d3d77..ef6c413 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "require": { "php": "^7.3|^8.0", "babenkoivan/scout-elasticsearch-driver": "^4.2", + "bnbwebexpertise/laravel-attachments": "^1.0", "fideloper/proxy": "^4.4", "fruitcake/laravel-cors": "^2.0", "google/apiclient": "^2.2", diff --git a/composer.lock b/composer.lock index 0d74439..1ac6206 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "10a519557c1fd55683da44313588c40e", + "content-hash": "7921640c91682ddbd453b7e9bb3f8e2c", "packages": [ { "name": "asm89/stack-cors", @@ -119,6 +119,107 @@ ], "time": "2020-08-13T17:57:09+00:00" }, + { + "name": "bnbwebexpertise/laravel-attachments", + "version": "1.0.22", + "source": { + "type": "git", + "url": "https://github.com/bnbwebexpertise/laravel-attachments.git", + "reference": "d3f4aa024449ad938d564aede64d05e19bfc5ff5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bnbwebexpertise/laravel-attachments/zipball/d3f4aa024449ad938d564aede64d05e19bfc5ff5", + "reference": "d3f4aa024449ad938d564aede64d05e19bfc5ff5", + "shasum": "" + }, + "require": { + "bnbwebexpertise/php-uuid": ">=0.0.2", + "doctrine/dbal": "~2.5", + "illuminate/console": ">=5.5", + "illuminate/database": ">=5.5", + "illuminate/encryption": ">=5.5", + "illuminate/routing": ">=5.5", + "illuminate/support": ">=5.5", + "nesbot/carbon": "^1.20 || ^2.0", + "php": ">=7.0" + }, + "require-dev": { + "laravel/framework": ">=5.5" + }, + "type": "package", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev", + "dev-L5.4": "0.x-dev" + }, + "laravel": { + "providers": [ + "Bnb\\Laravel\\Attachments\\AttachmentsServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Bnb\\Laravel\\Attachments\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "B&B Web Expertise", + "email": "support@bnb.re" + } + ], + "description": "Attach files to your models, retrievable by key, group name or using the Eloquent relationship.", + "time": "2020-06-24T15:19:20+00:00" + }, + { + "name": "bnbwebexpertise/php-uuid", + "version": "0.0.3", + "source": { + "type": "git", + "url": "https://github.com/bnbwebexpertise/php-uuid.git", + "reference": "d60bf8054db1d062f2fc79c43af0ff499a49fbfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bnbwebexpertise/php-uuid/zipball/d60bf8054db1d062f2fc79c43af0ff499a49fbfa", + "reference": "d60bf8054db1d062f2fc79c43af0ff499a49fbfa", + "shasum": "" + }, + "require": { + "ext-bcmath": "*", + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "package", + "autoload": { + "psr-4": { + "Bnb\\Uuid\\": "src/" + }, + "files": [ + "src/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "B&B Web Expertise", + "email": "support@bnb.re" + } + ], + "description": "UUID helpers", + "time": "2017-11-18T13:18:41+00:00" + }, { "name": "brick/math", "version": "0.9.1", @@ -204,6 +305,299 @@ "description": "implementation of xdg base directory specification for php", "time": "2019-12-04T15:06:13+00:00" }, + { + "name": "doctrine/cache", + "version": "1.10.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "13e3381b25847283a91948d04640543941309727" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/13e3381b25847283a91948d04640543941309727", + "reference": "13e3381b25847283a91948d04640543941309727", + "shasum": "" + }, + "require": { + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "alcaeus/mongo-php-adapter": "^1.1", + "doctrine/coding-standard": "^6.0", + "mongodb/mongodb": "^1.1", + "phpunit/phpunit": "^7.0", + "predis/predis": "~1.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2020-07-07T18:54:01+00:00" + }, + { + "name": "doctrine/dbal", + "version": "2.12.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "adce7a954a1c2f14f85e94aed90c8489af204086" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/adce7a954a1c2f14f85e94aed90c8489af204086", + "reference": "adce7a954a1c2f14f85e94aed90c8489af204086", + "shasum": "" + }, + "require": { + "doctrine/cache": "^1.0", + "doctrine/event-manager": "^1.0", + "ext-pdo": "*", + "php": "^7.3 || ^8" + }, + "require-dev": { + "doctrine/coding-standard": "^8.1", + "jetbrains/phpstorm-stubs": "^2019.1", + "phpstan/phpstan": "^0.12.40", + "phpunit/phpunit": "^9.4", + "psalm/plugin-phpunit": "^0.10.0", + "symfony/console": "^2.0.5|^3.0|^4.0|^5.0", + "vimeo/psalm": "^3.17.2" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "lib/Doctrine/DBAL" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlanywhere", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2020-11-14T20:26:58+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "41370af6a30faa9dc0368c4a6814d596e81aba7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/41370af6a30faa9dc0368c4a6814d596e81aba7f", + "reference": "41370af6a30faa9dc0368c4a6814d596e81aba7f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": "<2.9@dev" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2020-05-29T18:28:51+00:00" + }, { "name": "doctrine/inflector", "version": "2.0.3", diff --git a/config/attachments.php b/config/attachments.php new file mode 100644 index 0000000..3e1a478 --- /dev/null +++ b/config/attachments.php @@ -0,0 +1,98 @@ + [ + 'publish' => true, + 'prefix' => 'attachments', + 'middleware' => 'web', + 'pattern' => '/{id}/{name}', + 'shared_pattern' => '/shared/{token}', + 'dropzone' => [ + 'upload_pattern' => '/dropzone', + 'delete_pattern' => '/dropzone/{id}', + ] + ], + + /* + |-------------------------------------------------------------------------- + | Model + |-------------------------------------------------------------------------- + | + | Attachment model used + | + */ + 'attachment_model' => env('ATTACHMENTS_MODEL', \Bnb\Laravel\Attachments\Attachment::class), + + /* + |-------------------------------------------------------------------------- + | Uuid + |-------------------------------------------------------------------------- + | + | Default attachment model uses an UUID column. You can define your own UUID + | generator here : a global function name or a static class method in the form : + | App\Namespace\ClassName@method + | + */ + 'uuid_provider' => 'uuid_v4_base36', + + /* + |-------------------------------------------------------------------------- + | Behaviors + |-------------------------------------------------------------------------- + | + | Configurable behaviors : + | - Concrete files can be delete when the database entry is deleted + | - Dropzone delete can check for CSRF token match (set on upload) + | + */ + 'behaviors' => [ + 'cascade_delete' => env('ATTACHMENTS_CASCADE_DELETE', true), + 'dropzone_check_csrf' => env('ATTACHMENTS_DROPZONE_CHECK_CSRF', true), + ], + + /* + |-------------------------------------------------------------------------- + | Declare the attachment model attributes + |-------------------------------------------------------------------------- + | + | This allow to extend the attachment model with new columns + | `dropzone_attributes` holds the public fields returned after a successful upload via DropzoneController + | + */ + 'attributes' => ['title', 'description', 'key', 'disk', 'filepath', 'group'], + + 'dropzone_attributes' => ['uuid', 'url', 'url_inline', 'filename', 'filetype', 'filesize', 'title', 'description', 'key', 'group'], + + /* + |-------------------------------------------------------------------------- + | Attachment Storage Directory + |-------------------------------------------------------------------------- + | + | Defines the directory prefix where new attachment files are stored + | + */ + 'storage_directory' => [ + 'prefix' => rtrim(env('ATTACHMENTS_STORAGE_DIRECTORY_PREFIX', 'attachments'), '/'), + ], + + /* + |-------------------------------------------------------------------------- + | Database configuration + |-------------------------------------------------------------------------- + | + | Allows to set the database connection name for the module's models + | + */ + 'database' => [ + 'connection' => env('ATTACHMENTS_DATABASE_CONNECTION'), + ], +]; diff --git a/config/scout.php b/config/scout.php new file mode 100644 index 0000000..ad03f73 --- /dev/null +++ b/config/scout.php @@ -0,0 +1,106 @@ + env('SCOUT_DRIVER', 'elastic'), + + /* + |-------------------------------------------------------------------------- + | Index Prefix + |-------------------------------------------------------------------------- + | + | Here you may specify a prefix that will be applied to all search index + | names used by Scout. This prefix may be useful if you have multiple + | "tenants" or applications sharing the same search infrastructure. + | + */ + + 'prefix' => env('SCOUT_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Queue Data Syncing + |-------------------------------------------------------------------------- + | + | This option allows you to control if the operations that sync your data + | with your search engines are queued. When this is set to "true" then + | all automatic data syncing will get queued for better performance. + | + */ + + 'queue' => env('SCOUT_QUEUE', false), + + /* + |-------------------------------------------------------------------------- + | Chunk Sizes + |-------------------------------------------------------------------------- + | + | These options allow you to control the maximum chunk size when you are + | mass importing data into the search engine. This allows you to fine + | tune each of these chunk sizes based on the power of the servers. + | + */ + + 'chunk' => [ + 'searchable' => 500, + 'unsearchable' => 500, + ], + + /* + |-------------------------------------------------------------------------- + | Soft Deletes + |-------------------------------------------------------------------------- + | + | This option allows to control whether to keep soft deleted records in + | the search indexes. Maintaining soft deleted records can be useful + | if your application still needs to search for the records later. + | + */ + + 'soft_delete' => false, + + /* + |-------------------------------------------------------------------------- + | Identify User + |-------------------------------------------------------------------------- + | + | This option allows you to control whether to notify the search engine + | of the user performing the search. This is sometimes useful if the + | engine supports any analytics based on this application's users. + | + | Supported engines: "algolia" + | + */ + + 'identify' => env('SCOUT_IDENTIFY', false), + + /* + |-------------------------------------------------------------------------- + | Algolia Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure your Algolia settings. Algolia is a cloud hosted + | search engine which works great with Scout out of the box. Just plug + | in your application ID and admin API key to get started searching. + | + */ + + 'algolia' => [ + 'id' => env('ALGOLIA_APP_ID', ''), + 'secret' => env('ALGOLIA_SECRET', ''), + ], + +]; diff --git a/config/scout_elastic.php b/config/scout_elastic.php new file mode 100644 index 0000000..50fb67e --- /dev/null +++ b/config/scout_elastic.php @@ -0,0 +1,12 @@ + [ + 'hosts' => [ + env('SCOUT_ELASTIC_HOST', 'localhost:9200'), + ], + ], + 'update_mapping' => env('SCOUT_ELASTIC_UPDATE_MAPPING', true), + 'indexer' => env('SCOUT_ELASTIC_INDEXER', 'single'), + 'document_refresh' => env('SCOUT_ELASTIC_DOCUMENT_REFRESH'), +]; diff --git a/package.json b/package.json index a2da3a0..1804f0a 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,9 @@ "devDependencies": { "@ttskch/select2-bootstrap4-theme": "^1.3.4", "axios": "^0.19", + "bootbox": "^5.5.2", "bootstrap": "^4.0.0", "cross-env": "^7.0", - "datatables.net-bs4": "^1.10.23", - "datatables.net-buttons-bs4": "^1.6.5", "jquery": "^3.2", "laravel-mix": "^5.0.1", "lodash": "^4.17.19", @@ -23,11 +22,6 @@ "resolve-url-loader": "^2.3.1", "sass": "^1.20.1", "sass-loader": "^8.0.0", - "select2": "^4.0.13", - "vue": "^2.5.17", - "vue-template-compiler": "^2.6.10" - }, - "dependencies": { - "vue-router": "^3.4.9" + "select2": "^4.0.13" } } diff --git a/public/js/app.js b/public/js/app.js index e08e44a..70c143c 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1897,6 +1897,1457 @@ module.exports = { }; +/***/ }), + +/***/ "./node_modules/bootbox/bootbox.all.js": +/*!*********************************************!*\ + !*** ./node_modules/bootbox/bootbox.all.js ***! + \*********************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @preserve + * bootbox.js + * version: 5.5.2 + * author: Nick Payne + * license: MIT + * http://bootboxjs.com/ + */ +(function (root, factory) { + 'use strict'; + if (true) { + // AMD + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! jquery */ "./node_modules/jquery/dist/jquery.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else {} +}(this, function init($, undefined) { + 'use strict'; + + // Polyfills Object.keys, if necessary. + // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys + if (!Object.keys) { + Object.keys = (function () { + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function (obj) { + if (typeof obj !== 'function' && (typeof obj !== 'object' || obj === null)) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = [], prop, i; + + for (prop in obj) { + if (hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } + + if (hasDontEnumBug) { + for (i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + + return result; + }; + }()); + } + + var exports = {}; + + var VERSION = '5.5.2'; + exports.VERSION = VERSION; + + var locales = { + ar : { + OK : 'موافق', + CANCEL : 'الغاء', + CONFIRM : 'تأكيد' + }, + bg_BG : { + OK : 'Ок', + CANCEL : 'Отказ', + CONFIRM : 'Потвърждавам' + }, + br : { + OK : 'OK', + CANCEL : 'Cancelar', + CONFIRM : 'Sim' + }, + cs : { + OK : 'OK', + CANCEL : 'Zrušit', + CONFIRM : 'Potvrdit' + }, + da : { + OK : 'OK', + CANCEL : 'Annuller', + CONFIRM : 'Accepter' + }, + de : { + OK : 'OK', + CANCEL : 'Abbrechen', + CONFIRM : 'Akzeptieren' + }, + el : { + OK : 'Εντάξει', + CANCEL : 'Ακύρωση', + CONFIRM : 'Επιβεβαίωση' + }, + en : { + OK : 'OK', + CANCEL : 'Cancel', + CONFIRM : 'OK' + }, + es : { + OK : 'OK', + CANCEL : 'Cancelar', + CONFIRM : 'Aceptar' + }, + eu : { + OK : 'OK', + CANCEL : 'Ezeztatu', + CONFIRM : 'Onartu' + }, + et : { + OK : 'OK', + CANCEL : 'Katkesta', + CONFIRM : 'OK' + }, + fa : { + OK : 'قبول', + CANCEL : 'لغو', + CONFIRM : 'تایید' + }, + fi : { + OK : 'OK', + CANCEL : 'Peruuta', + CONFIRM : 'OK' + }, + fr : { + OK : 'OK', + CANCEL : 'Annuler', + CONFIRM : 'Confirmer' + }, + he : { + OK : 'אישור', + CANCEL : 'ביטול', + CONFIRM : 'אישור' + }, + hu : { + OK : 'OK', + CANCEL : 'Mégsem', + CONFIRM : 'Megerősít' + }, + hr : { + OK : 'OK', + CANCEL : 'Odustani', + CONFIRM : 'Potvrdi' + }, + id : { + OK : 'OK', + CANCEL : 'Batal', + CONFIRM : 'OK' + }, + it : { + OK : 'OK', + CANCEL : 'Annulla', + CONFIRM : 'Conferma' + }, + ja : { + OK : 'OK', + CANCEL : 'キャンセル', + CONFIRM : '確認' + }, + ka : { + OK: 'OK', + CANCEL: 'გაუქმება', + CONFIRM: 'დადასტურება' + }, + ko : { + OK: 'OK', + CANCEL: '취소', + CONFIRM: '확인' + }, + lt : { + OK : 'Gerai', + CANCEL : 'Atšaukti', + CONFIRM : 'Patvirtinti' + }, + lv : { + OK : 'Labi', + CANCEL : 'Atcelt', + CONFIRM : 'Apstiprināt' + }, + nl : { + OK : 'OK', + CANCEL : 'Annuleren', + CONFIRM : 'Accepteren' + }, + no : { + OK : 'OK', + CANCEL : 'Avbryt', + CONFIRM : 'OK' + }, + pl : { + OK : 'OK', + CANCEL : 'Anuluj', + CONFIRM : 'Potwierdź' + }, + pt : { + OK : 'OK', + CANCEL : 'Cancelar', + CONFIRM : 'Confirmar' + }, + ru : { + OK : 'OK', + CANCEL : 'Отмена', + CONFIRM : 'Применить' + }, + sk : { + OK : 'OK', + CANCEL : 'Zrušiť', + CONFIRM : 'Potvrdiť' + }, + sl : { + OK : 'OK', + CANCEL : 'Prekliči', + CONFIRM : 'Potrdi' + }, + sq : { + OK : 'OK', + CANCEL : 'Anulo', + CONFIRM : 'Prano' + }, + sv : { + OK : 'OK', + CANCEL : 'Avbryt', + CONFIRM : 'OK' + }, + sw: { + OK : 'Sawa', + CANCEL : 'Ghairi', + CONFIRM: 'Thibitisha' + }, + ta:{ + OK : 'சரி', + CANCEL : 'ரத்து செய்', + CONFIRM : 'உறுதி செய்' + }, + th : { + OK : 'ตกลง', + CANCEL : 'ยกเลิก', + CONFIRM : 'ยืนยัน' + }, + tr : { + OK : 'Tamam', + CANCEL : 'İptal', + CONFIRM : 'Onayla' + }, + uk : { + OK : 'OK', + CANCEL : 'Відміна', + CONFIRM : 'Прийняти' + }, + vi : { + OK : 'OK', + CANCEL : 'Hủy bỏ', + CONFIRM : 'Xác nhận' + }, + zh_CN : { + OK : 'OK', + CANCEL : '取消', + CONFIRM : '确认' + }, + zh_TW : { + OK : 'OK', + CANCEL : '取消', + CONFIRM : '確認' + } + }; + + var templates = { + dialog: + '', + header: + '', + footer: + '', + closeButton: + '', + form: + '
', + button: + '', + option: + '', + promptMessage: + '
', + inputs: { + text: + '', + textarea: + '', + email: + '', + select: + '', + checkbox: + '
', + radio: + '
', + date: + '', + time: + '', + number: + '', + password: + '', + range: + '' + } + }; + + + var defaults = { + // default language + locale: 'en', + // show backdrop or not. Default to static so user has to interact with dialog + backdrop: 'static', + // animate the modal in/out + animate: true, + // additional class string applied to the top level dialog + className: null, + // whether or not to include a close button + closeButton: true, + // show the dialog immediately by default + show: true, + // dialog container + container: 'body', + // default value (used by the prompt helper) + value: '', + // default input type (used by the prompt helper) + inputType: 'text', + // switch button order from cancel/confirm (default) to confirm/cancel + swapButtonOrder: false, + // center modal vertically in page + centerVertical: false, + // Append "multiple" property to the select when using the "prompt" helper + multiple: false, + // Automatically scroll modal content when height exceeds viewport height + scrollable: false, + // whether or not to destroy the modal on hide + reusable: false + }; + + + // PUBLIC FUNCTIONS + // ************************************************************************************************************* + + // Return all currently registered locales, or a specific locale if "name" is defined + exports.locales = function (name) { + return name ? locales[name] : locales; + }; + + + // Register localized strings for the OK, CONFIRM, and CANCEL buttons + exports.addLocale = function (name, values) { + $.each(['OK', 'CANCEL', 'CONFIRM'], function (_, v) { + if (!values[v]) { + throw new Error('Please supply a translation for "' + v + '"'); + } + }); + + locales[name] = { + OK: values.OK, + CANCEL: values.CANCEL, + CONFIRM: values.CONFIRM + }; + + return exports; + }; + + + // Remove a previously-registered locale + exports.removeLocale = function (name) { + if (name !== 'en') { + delete locales[name]; + } + else { + throw new Error('"en" is used as the default and fallback locale and cannot be removed.'); + } + + return exports; + }; + + + // Set the default locale + exports.setLocale = function (name) { + return exports.setDefaults('locale', name); + }; + + + // Override default value(s) of Bootbox. + exports.setDefaults = function () { + var values = {}; + + if (arguments.length === 2) { + // allow passing of single key/value... + values[arguments[0]] = arguments[1]; + } else { + // ... and as an object too + values = arguments[0]; + } + + $.extend(defaults, values); + + return exports; + }; + + + // Hides all currently active Bootbox modals + exports.hideAll = function () { + $('.bootbox').modal('hide'); + + return exports; + }; + + + // Allows the base init() function to be overridden + exports.init = function (_$) { + return init(_$ || $); + }; + + + // CORE HELPER FUNCTIONS + // ************************************************************************************************************* + + // Core dialog function + exports.dialog = function (options) { + if ($.fn.modal === undefined) { + throw new Error( + '"$.fn.modal" is not defined; please double check you have included ' + + 'the Bootstrap JavaScript library. See https://getbootstrap.com/docs/4.4/getting-started/javascript/ ' + + 'for more details.' + ); + } + + options = sanitize(options); + + if ($.fn.modal.Constructor.VERSION) { + options.fullBootstrapVersion = $.fn.modal.Constructor.VERSION; + var i = options.fullBootstrapVersion.indexOf('.'); + options.bootstrap = options.fullBootstrapVersion.substring(0, i); + } + else { + // Assuming version 2.3.2, as that was the last "supported" 2.x version + options.bootstrap = '2'; + options.fullBootstrapVersion = '2.3.2'; + console.warn('Bootbox will *mostly* work with Bootstrap 2, but we do not officially support it. Please upgrade, if possible.'); + } + + var dialog = $(templates.dialog); + var innerDialog = dialog.find('.modal-dialog'); + var body = dialog.find('.modal-body'); + var header = $(templates.header); + var footer = $(templates.footer); + var buttons = options.buttons; + + var callbacks = { + onEscape: options.onEscape + }; + + body.find('.bootbox-body').html(options.message); + + // Only attempt to create buttons if at least one has + // been defined in the options object + if (getKeyLength(options.buttons) > 0) { + each(buttons, function (key, b) { + var button = $(templates.button); + button.data('bb-handler', key); + button.addClass(b.className); + + switch (key) { + case 'ok': + case 'confirm': + button.addClass('bootbox-accept'); + break; + + case 'cancel': + button.addClass('bootbox-cancel'); + break; + } + + button.html(b.label); + footer.append(button); + + callbacks[key] = b.callback; + }); + + body.after(footer); + } + + if (options.animate === true) { + dialog.addClass('fade'); + } + + if (options.className) { + dialog.addClass(options.className); + } + + if (options.size) { + // Requires Bootstrap 3.1.0 or higher + if (options.fullBootstrapVersion.substring(0, 3) < '3.1') { + console.warn('"size" requires Bootstrap 3.1.0 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.'); + } + + switch (options.size) { + case 'small': + case 'sm': + innerDialog.addClass('modal-sm'); + break; + + case 'large': + case 'lg': + innerDialog.addClass('modal-lg'); + break; + + case 'extra-large': + case 'xl': + innerDialog.addClass('modal-xl'); + + // Requires Bootstrap 4.2.0 or higher + if (options.fullBootstrapVersion.substring(0, 3) < '4.2') { + console.warn('Using size "xl"/"extra-large" requires Bootstrap 4.2.0 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.'); + } + break; + } + } + + if (options.scrollable) { + innerDialog.addClass('modal-dialog-scrollable'); + + // Requires Bootstrap 4.3.0 or higher + if (options.fullBootstrapVersion.substring(0, 3) < '4.3') { + console.warn('Using "scrollable" requires Bootstrap 4.3.0 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.'); + } + } + + if (options.title) { + body.before(header); + dialog.find('.modal-title').html(options.title); + } + + if (options.closeButton) { + var closeButton = $(templates.closeButton); + + if (options.title) { + if (options.bootstrap > 3) { + dialog.find('.modal-header').append(closeButton); + } + else { + dialog.find('.modal-header').prepend(closeButton); + } + } else { + closeButton.prependTo(body); + } + } + + if (options.centerVertical) { + innerDialog.addClass('modal-dialog-centered'); + + // Requires Bootstrap 4.0.0-beta.3 or higher + if (options.fullBootstrapVersion < '4.0.0') { + console.warn('"centerVertical" requires Bootstrap 4.0.0-beta.3 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.'); + } + } + + // Bootstrap event listeners; these handle extra + // setup & teardown required after the underlying + // modal has performed certain actions. + + if(!options.reusable) { + // make sure we unbind any listeners once the dialog has definitively been dismissed + dialog.one('hide.bs.modal', { dialog: dialog }, unbindModal); + } + + if (options.onHide) { + if ($.isFunction(options.onHide)) { + dialog.on('hide.bs.modal', options.onHide); + } + else { + throw new Error('Argument supplied to "onHide" must be a function'); + } + } + + if(!options.reusable) { + dialog.one('hidden.bs.modal', { dialog: dialog }, destroyModal); + } + + if (options.onHidden) { + if ($.isFunction(options.onHidden)) { + dialog.on('hidden.bs.modal', options.onHidden); + } + else { + throw new Error('Argument supplied to "onHidden" must be a function'); + } + } + + if (options.onShow) { + if ($.isFunction(options.onShow)) { + dialog.on('show.bs.modal', options.onShow); + } + else { + throw new Error('Argument supplied to "onShow" must be a function'); + } + } + + dialog.one('shown.bs.modal', { dialog: dialog }, focusPrimaryButton); + + if (options.onShown) { + if ($.isFunction(options.onShown)) { + dialog.on('shown.bs.modal', options.onShown); + } + else { + throw new Error('Argument supplied to "onShown" must be a function'); + } + } + + // Bootbox event listeners; used to decouple some + // behaviours from their respective triggers + + if (options.backdrop === true) { + // A boolean true/false according to the Bootstrap docs + // should show a dialog the user can dismiss by clicking on + // the background. + // We always only ever pass static/false to the actual + // $.modal function because with "true" we can't trap + // this event (the .modal-backdrop swallows it) + // However, we still want to sort-of respect true + // and invoke the escape mechanism instead + dialog.on('click.dismiss.bs.modal', function (e) { + // @NOTE: the target varies in >= 3.3.x releases since the modal backdrop + // moved *inside* the outer dialog rather than *alongside* it + if (dialog.children('.modal-backdrop').length) { + e.currentTarget = dialog.children('.modal-backdrop').get(0); + } + + if (e.target !== e.currentTarget) { + return; + } + + dialog.trigger('escape.close.bb'); + }); + } + + dialog.on('escape.close.bb', function (e) { + // the if statement looks redundant but it isn't; without it + // if we *didn't* have an onEscape handler then processCallback + // would automatically dismiss the dialog + if (callbacks.onEscape) { + processCallback(e, dialog, callbacks.onEscape); + } + }); + + + dialog.on('click', '.modal-footer button:not(.disabled)', function (e) { + var callbackKey = $(this).data('bb-handler'); + + if (callbackKey !== undefined) { + // Only process callbacks for buttons we recognize: + processCallback(e, dialog, callbacks[callbackKey]); + } + }); + + dialog.on('click', '.bootbox-close-button', function (e) { + // onEscape might be falsy but that's fine; the fact is + // if the user has managed to click the close button we + // have to close the dialog, callback or not + processCallback(e, dialog, callbacks.onEscape); + }); + + dialog.on('keyup', function (e) { + if (e.which === 27) { + dialog.trigger('escape.close.bb'); + } + }); + + // the remainder of this method simply deals with adding our + // dialog element to the DOM, augmenting it with Bootstrap's modal + // functionality and then giving the resulting object back + // to our caller + + $(options.container).append(dialog); + + dialog.modal({ + backdrop: options.backdrop, + keyboard: false, + show: false + }); + + if (options.show) { + dialog.modal('show'); + } + + return dialog; + }; + + + // Helper function to simulate the native alert() behavior. **NOTE**: This is non-blocking, so any + // code that must happen after the alert is dismissed should be placed within the callback function + // for this alert. + exports.alert = function () { + var options; + + options = mergeDialogOptions('alert', ['ok'], ['message', 'callback'], arguments); + + // @TODO: can this move inside exports.dialog when we're iterating over each + // button and checking its button.callback value instead? + if (options.callback && !$.isFunction(options.callback)) { + throw new Error('alert requires the "callback" property to be a function when provided'); + } + + // override the ok and escape callback to make sure they just invoke + // the single user-supplied one (if provided) + options.buttons.ok.callback = options.onEscape = function () { + if ($.isFunction(options.callback)) { + return options.callback.call(this); + } + + return true; + }; + + return exports.dialog(options); + }; + + + // Helper function to simulate the native confirm() behavior. **NOTE**: This is non-blocking, so any + // code that must happen after the confirm is dismissed should be placed within the callback function + // for this confirm. + exports.confirm = function () { + var options; + + options = mergeDialogOptions('confirm', ['cancel', 'confirm'], ['message', 'callback'], arguments); + + // confirm specific validation; they don't make sense without a callback so make + // sure it's present + if (!$.isFunction(options.callback)) { + throw new Error('confirm requires a callback'); + } + + // overrides; undo anything the user tried to set they shouldn't have + options.buttons.cancel.callback = options.onEscape = function () { + return options.callback.call(this, false); + }; + + options.buttons.confirm.callback = function () { + return options.callback.call(this, true); + }; + + return exports.dialog(options); + }; + + + // Helper function to simulate the native prompt() behavior. **NOTE**: This is non-blocking, so any + // code that must happen after the prompt is dismissed should be placed within the callback function + // for this prompt. + exports.prompt = function () { + var options; + var promptDialog; + var form; + var input; + var shouldShow; + var inputOptions; + + // we have to create our form first otherwise + // its value is undefined when gearing up our options + // @TODO this could be solved by allowing message to + // be a function instead... + form = $(templates.form); + + // prompt defaults are more complex than others in that + // users can override more defaults + options = mergeDialogOptions('prompt', ['cancel', 'confirm'], ['title', 'callback'], arguments); + + if (!options.value) { + options.value = defaults.value; + } + + if (!options.inputType) { + options.inputType = defaults.inputType; + } + + // capture the user's show value; we always set this to false before + // spawning the dialog to give us a chance to attach some handlers to + // it, but we need to make sure we respect a preference not to show it + shouldShow = (options.show === undefined) ? defaults.show : options.show; + + // This is required prior to calling the dialog builder below - we need to + // add an event handler just before the prompt is shown + options.show = false; + + // Handles the 'cancel' action + options.buttons.cancel.callback = options.onEscape = function () { + return options.callback.call(this, null); + }; + + // Prompt submitted - extract the prompt value. This requires a bit of work, + // given the different input types available. + options.buttons.confirm.callback = function () { + var value; + + if (options.inputType === 'checkbox') { + value = input.find('input:checked').map(function () { + return $(this).val(); + }).get(); + } else if (options.inputType === 'radio') { + value = input.find('input:checked').val(); + } + else { + if (input[0].checkValidity && !input[0].checkValidity()) { + // prevents button callback from being called + return false; + } else { + if (options.inputType === 'select' && options.multiple === true) { + value = input.find('option:selected').map(function () { + return $(this).val(); + }).get(); + } + else { + value = input.val(); + } + } + } + + return options.callback.call(this, value); + }; + + // prompt-specific validation + if (!options.title) { + throw new Error('prompt requires a title'); + } + + if (!$.isFunction(options.callback)) { + throw new Error('prompt requires a callback'); + } + + if (!templates.inputs[options.inputType]) { + throw new Error('Invalid prompt type'); + } + + // create the input based on the supplied type + input = $(templates.inputs[options.inputType]); + + switch (options.inputType) { + case 'text': + case 'textarea': + case 'email': + case 'password': + input.val(options.value); + + if (options.placeholder) { + input.attr('placeholder', options.placeholder); + } + + if (options.pattern) { + input.attr('pattern', options.pattern); + } + + if (options.maxlength) { + input.attr('maxlength', options.maxlength); + } + + if (options.required) { + input.prop({ 'required': true }); + } + + if (options.rows && !isNaN(parseInt(options.rows))) { + if (options.inputType === 'textarea') { + input.attr({ 'rows': options.rows }); + } + } + + break; + + + case 'date': + case 'time': + case 'number': + case 'range': + input.val(options.value); + + if (options.placeholder) { + input.attr('placeholder', options.placeholder); + } + + if (options.pattern) { + input.attr('pattern', options.pattern); + } + + if (options.required) { + input.prop({ 'required': true }); + } + + // These input types have extra attributes which affect their input validation. + // Warning: For most browsers, date inputs are buggy in their implementation of 'step', so + // this attribute will have no effect. Therefore, we don't set the attribute for date inputs. + // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#Setting_maximum_and_minimum_dates + if (options.inputType !== 'date') { + if (options.step) { + if (options.step === 'any' || (!isNaN(options.step) && parseFloat(options.step) > 0)) { + input.attr('step', options.step); + } + else { + throw new Error('"step" must be a valid positive number or the value "any". See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-step for more information.'); + } + } + } + + if (minAndMaxAreValid(options.inputType, options.min, options.max)) { + if (options.min !== undefined) { + input.attr('min', options.min); + } + if (options.max !== undefined) { + input.attr('max', options.max); + } + } + + break; + + + case 'select': + var groups = {}; + inputOptions = options.inputOptions || []; + + if (!$.isArray(inputOptions)) { + throw new Error('Please pass an array of input options'); + } + + if (!inputOptions.length) { + throw new Error('prompt with "inputType" set to "select" requires at least one option'); + } + + // placeholder is not actually a valid attribute for select, + // but we'll allow it, assuming it might be used for a plugin + if (options.placeholder) { + input.attr('placeholder', options.placeholder); + } + + if (options.required) { + input.prop({ 'required': true }); + } + + if (options.multiple) { + input.prop({ 'multiple': true }); + } + + each(inputOptions, function (_, option) { + // assume the element to attach to is the input... + var elem = input; + + if (option.value === undefined || option.text === undefined) { + throw new Error('each option needs a "value" property and a "text" property'); + } + + // ... but override that element if this option sits in a group + + if (option.group) { + // initialise group if necessary + if (!groups[option.group]) { + groups[option.group] = $('').attr('label', option.group); + } + + elem = groups[option.group]; + } + + var o = $(templates.option); + o.attr('value', option.value).text(option.text); + elem.append(o); + }); + + each(groups, function (_, group) { + input.append(group); + }); + + // safe to set a select's value as per a normal input + input.val(options.value); + + break; + + + case 'checkbox': + var checkboxValues = $.isArray(options.value) ? options.value : [options.value]; + inputOptions = options.inputOptions || []; + + if (!inputOptions.length) { + throw new Error('prompt with "inputType" set to "checkbox" requires at least one option'); + } + + // checkboxes have to nest within a containing element, so + // they break the rules a bit and we end up re-assigning + // our 'input' element to this container instead + input = $('
'); + + each(inputOptions, function (_, option) { + if (option.value === undefined || option.text === undefined) { + throw new Error('each option needs a "value" property and a "text" property'); + } + + var checkbox = $(templates.inputs[options.inputType]); + + checkbox.find('input').attr('value', option.value); + checkbox.find('label').append('\n' + option.text); + + // we've ensured values is an array so we can always iterate over it + each(checkboxValues, function (_, value) { + if (value === option.value) { + checkbox.find('input').prop('checked', true); + } + }); + + input.append(checkbox); + }); + break; + + + case 'radio': + // Make sure that value is not an array (only a single radio can ever be checked) + if (options.value !== undefined && $.isArray(options.value)) { + throw new Error('prompt with "inputType" set to "radio" requires a single, non-array value for "value"'); + } + + inputOptions = options.inputOptions || []; + + if (!inputOptions.length) { + throw new Error('prompt with "inputType" set to "radio" requires at least one option'); + } + + // Radiobuttons have to nest within a containing element, so + // they break the rules a bit and we end up re-assigning + // our 'input' element to this container instead + input = $('
'); + + // Radiobuttons should always have an initial checked input checked in a "group". + // If value is undefined or doesn't match an input option, select the first radiobutton + var checkFirstRadio = true; + + each(inputOptions, function (_, option) { + if (option.value === undefined || option.text === undefined) { + throw new Error('each option needs a "value" property and a "text" property'); + } + + var radio = $(templates.inputs[options.inputType]); + + radio.find('input').attr('value', option.value); + radio.find('label').append('\n' + option.text); + + if (options.value !== undefined) { + if (option.value === options.value) { + radio.find('input').prop('checked', true); + checkFirstRadio = false; + } + } + + input.append(radio); + }); + + if (checkFirstRadio) { + input.find('input[type="radio"]').first().prop('checked', true); + } + break; + } + + // now place it in our form + form.append(input); + + form.on('submit', function (e) { + e.preventDefault(); + // Fix for SammyJS (or similar JS routing library) hijacking the form post. + e.stopPropagation(); + + // @TODO can we actually click *the* button object instead? + // e.g. buttons.confirm.click() or similar + promptDialog.find('.bootbox-accept').trigger('click'); + }); + + if ($.trim(options.message) !== '') { + // Add the form to whatever content the user may have added. + var message = $(templates.promptMessage).html(options.message); + form.prepend(message); + options.message = form; + } + else { + options.message = form; + } + + // Generate the dialog + promptDialog = exports.dialog(options); + + // clear the existing handler focusing the submit button... + promptDialog.off('shown.bs.modal', focusPrimaryButton); + + // ...and replace it with one focusing our input, if possible + promptDialog.on('shown.bs.modal', function () { + // need the closure here since input isn't + // an object otherwise + input.focus(); + }); + + if (shouldShow === true) { + promptDialog.modal('show'); + } + + return promptDialog; + }; + + + // INTERNAL FUNCTIONS + // ************************************************************************************************************* + + // Map a flexible set of arguments into a single returned object + // If args.length is already one just return it, otherwise + // use the properties argument to map the unnamed args to + // object properties. + // So in the latter case: + // mapArguments(["foo", $.noop], ["message", "callback"]) + // -> { message: "foo", callback: $.noop } + function mapArguments(args, properties) { + var argn = args.length; + var options = {}; + + if (argn < 1 || argn > 2) { + throw new Error('Invalid argument length'); + } + + if (argn === 2 || typeof args[0] === 'string') { + options[properties[0]] = args[0]; + options[properties[1]] = args[1]; + } else { + options = args[0]; + } + + return options; + } + + + // Merge a set of default dialog options with user supplied arguments + function mergeArguments(defaults, args, properties) { + return $.extend( + // deep merge + true, + // ensure the target is an empty, unreferenced object + {}, + // the base options object for this type of dialog (often just buttons) + defaults, + // args could be an object or array; if it's an array properties will + // map it to a proper options object + mapArguments( + args, + properties + ) + ); + } + + + // This entry-level method makes heavy use of composition to take a simple + // range of inputs and return valid options suitable for passing to bootbox.dialog + function mergeDialogOptions(className, labels, properties, args) { + var locale; + if (args && args[0]) { + locale = args[0].locale || defaults.locale; + var swapButtons = args[0].swapButtonOrder || defaults.swapButtonOrder; + + if (swapButtons) { + labels = labels.reverse(); + } + } + + // build up a base set of dialog properties + var baseOptions = { + className: 'bootbox-' + className, + buttons: createLabels(labels, locale) + }; + + // Ensure the buttons properties generated, *after* merging + // with user args are still valid against the supplied labels + return validateButtons( + // merge the generated base properties with user supplied arguments + mergeArguments( + baseOptions, + args, + // if args.length > 1, properties specify how each arg maps to an object key + properties + ), + labels + ); + } + + + // Checks each button object to see if key is valid. + // This function will only be called by the alert, confirm, and prompt helpers. + function validateButtons(options, buttons) { + var allowedButtons = {}; + each(buttons, function (key, value) { + allowedButtons[value] = true; + }); + + each(options.buttons, function (key) { + if (allowedButtons[key] === undefined) { + throw new Error('button key "' + key + '" is not allowed (options are ' + buttons.join(' ') + ')'); + } + }); + + return options; + } + + + + // From a given list of arguments, return a suitable object of button labels. + // All this does is normalise the given labels and translate them where possible. + // e.g. "ok", "confirm" -> { ok: "OK", cancel: "Annuleren" } + function createLabels(labels, locale) { + var buttons = {}; + + for (var i = 0, j = labels.length; i < j; i++) { + var argument = labels[i]; + var key = argument.toLowerCase(); + var value = argument.toUpperCase(); + + buttons[key] = { + label: getText(value, locale) + }; + } + + return buttons; + } + + + + // Get localized text from a locale. Defaults to 'en' locale if no locale + // provided or a non-registered locale is requested + function getText(key, locale) { + var labels = locales[locale]; + + return labels ? labels[key] : locales.en[key]; + } + + + + // Filter and tidy up any user supplied parameters to this dialog. + // Also looks for any shorthands used and ensures that the options + // which are returned are all normalized properly + function sanitize(options) { + var buttons; + var total; + + if (typeof options !== 'object') { + throw new Error('Please supply an object of options'); + } + + if (!options.message) { + throw new Error('"message" option must not be null or an empty string.'); + } + + // make sure any supplied options take precedence over defaults + options = $.extend({}, defaults, options); + + //make sure backdrop is either true, false, or 'static' + if (!options.backdrop) { + options.backdrop = (options.backdrop === false || options.backdrop === 0) ? false : 'static'; + } else { + options.backdrop = typeof options.backdrop === 'string' && options.backdrop.toLowerCase() === 'static' ? 'static' : true; + } + + // no buttons is still a valid dialog but it's cleaner to always have + // a buttons object to iterate over, even if it's empty + if (!options.buttons) { + options.buttons = {}; + } + + buttons = options.buttons; + + total = getKeyLength(buttons); + + each(buttons, function (key, button, index) { + if ($.isFunction(button)) { + // short form, assume value is our callback. Since button + // isn't an object it isn't a reference either so re-assign it + button = buttons[key] = { + callback: button + }; + } + + // before any further checks make sure by now button is the correct type + if ($.type(button) !== 'object') { + throw new Error('button with key "' + key + '" must be an object'); + } + + if (!button.label) { + // the lack of an explicit label means we'll assume the key is good enough + button.label = key; + } + + if (!button.className) { + var isPrimary = false; + if (options.swapButtonOrder) { + isPrimary = index === 0; + } + else { + isPrimary = index === total - 1; + } + + if (total <= 2 && isPrimary) { + // always add a primary to the main option in a one or two-button dialog + button.className = 'btn-primary'; + } else { + // adding both classes allows us to target both BS3 and BS4 without needing to check the version + button.className = 'btn-secondary btn-default'; + } + } + }); + + return options; + } + + + // Returns a count of the properties defined on the object + function getKeyLength(obj) { + return Object.keys(obj).length; + } + + + // Tiny wrapper function around jQuery.each; just adds index as the third parameter + function each(collection, iterator) { + var index = 0; + $.each(collection, function (key, value) { + iterator(key, value, index++); + }); + } + + + function focusPrimaryButton(e) { + e.data.dialog.find('.bootbox-accept').first().trigger('focus'); + } + + + function destroyModal(e) { + // ensure we don't accidentally intercept hidden events triggered + // by children of the current dialog. We shouldn't need to handle this anymore, + // now that Bootstrap namespaces its events, but still worth doing. + if (e.target === e.data.dialog[0]) { + e.data.dialog.remove(); + } + } + + + function unbindModal(e) { + if (e.target === e.data.dialog[0]) { + e.data.dialog.off('escape.close.bb'); + e.data.dialog.off('click'); + } + } + + + // Handle the invoked dialog callback + function processCallback(e, dialog, callback) { + e.stopPropagation(); + e.preventDefault(); + + // by default we assume a callback will get rid of the dialog, + // although it is given the opportunity to override this + + // so, if the callback can be invoked and it *explicitly returns false* + // then we'll set a flag to keep the dialog active... + var preserveDialog = $.isFunction(callback) && callback.call(dialog, e) === false; + + // ... otherwise we'll bin it + if (!preserveDialog) { + dialog.modal('hide'); + } + } + + // Validate `min` and `max` values based on the current `inputType` value + function minAndMaxAreValid(type, min, max) { + var result = false; + var minValid = true; + var maxValid = true; + + if (type === 'date') { + if (min !== undefined && !(minValid = dateIsValid(min))) { + console.warn('Browsers which natively support the "date" input type expect date values to be of the form "YYYY-MM-DD" (see ISO-8601 https://www.iso.org/iso-8601-date-and-time-format.html). Bootbox does not enforce this rule, but your min value may not be enforced by this browser.'); + } + else if (max !== undefined && !(maxValid = dateIsValid(max))) { + console.warn('Browsers which natively support the "date" input type expect date values to be of the form "YYYY-MM-DD" (see ISO-8601 https://www.iso.org/iso-8601-date-and-time-format.html). Bootbox does not enforce this rule, but your max value may not be enforced by this browser.'); + } + } + else if (type === 'time') { + if (min !== undefined && !(minValid = timeIsValid(min))) { + throw new Error('"min" is not a valid time. See https://www.w3.org/TR/2012/WD-html-markup-20120315/datatypes.html#form.data.time for more information.'); + } + else if (max !== undefined && !(maxValid = timeIsValid(max))) { + throw new Error('"max" is not a valid time. See https://www.w3.org/TR/2012/WD-html-markup-20120315/datatypes.html#form.data.time for more information.'); + } + } + else { + if (min !== undefined && isNaN(min)) { + minValid = false; + throw new Error('"min" must be a valid number. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-min for more information.'); + } + + if (max !== undefined && isNaN(max)) { + maxValid = false; + throw new Error('"max" must be a valid number. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-max for more information.'); + } + } + + if (minValid && maxValid) { + if (max <= min) { + throw new Error('"max" must be greater than "min". See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-max for more information.'); + } + else { + result = true; + } + } + + return result; + } + + function timeIsValid(value) { + return /([01][0-9]|2[0-3]):[0-5][0-9]?:[0-5][0-9]/.test(value); + } + + function dateIsValid(value) { + return /(\d{4})-(\d{2})-(\d{2})/.test(value); + } + + // The Bootbox object + return exports; +})); + /***/ }), /***/ "./node_modules/bootstrap/dist/js/bootstrap.js": @@ -6325,17767 +7776,6 @@ module.exports = { //# sourceMappingURL=bootstrap.js.map -/***/ }), - -/***/ "./node_modules/datatables.net-bs4/js/dataTables.bootstrap4.js": -/*!*********************************************************************!*\ - !*** ./node_modules/datatables.net-bs4/js/dataTables.bootstrap4.js ***! - \*********************************************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! DataTables Bootstrap 4 integration - * ©2011-2017 SpryMedia Ltd - datatables.net/license - */ - -/** - * DataTables integration for Bootstrap 4. This requires Bootstrap 4 and - * DataTables 1.10 or newer. - * - * This file sets the defaults and adds options to DataTables to style its - * controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap - * for further information. - */ -(function( factory ){ - if ( true ) { - // AMD - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! jquery */ "./node_modules/jquery/dist/jquery.js"), __webpack_require__(/*! datatables.net */ "./node_modules/datatables.net/js/jquery.dataTables.js")], __WEBPACK_AMD_DEFINE_RESULT__ = (function ( $ ) { - return factory( $, window, document ); - }).apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } - else {} -}(function( $, window, document, undefined ) { -'use strict'; -var DataTable = $.fn.dataTable; - - -/* Set the defaults for DataTables initialisation */ -$.extend( true, DataTable.defaults, { - dom: - "<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" + - "<'row'<'col-sm-12'tr>>" + - "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", - renderer: 'bootstrap' -} ); - - -/* Default class modification */ -$.extend( DataTable.ext.classes, { - sWrapper: "dataTables_wrapper dt-bootstrap4", - sFilterInput: "form-control form-control-sm", - sLengthSelect: "custom-select custom-select-sm form-control form-control-sm", - sProcessing: "dataTables_processing card", - sPageButton: "paginate_button page-item" -} ); - - -/* Bootstrap paging button renderer */ -DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, buttons, page, pages ) { - var api = new DataTable.Api( settings ); - var classes = settings.oClasses; - var lang = settings.oLanguage.oPaginate; - var aria = settings.oLanguage.oAria.paginate || {}; - var btnDisplay, btnClass, counter=0; - - var attach = function( container, buttons ) { - var i, ien, node, button; - var clickHandler = function ( e ) { - e.preventDefault(); - if ( !$(e.currentTarget).hasClass('disabled') && api.page() != e.data.action ) { - api.page( e.data.action ).draw( 'page' ); - } - }; - - for ( i=0, ien=buttons.length ; i 0 ? - '' : ' disabled'); - break; - - case 'previous': - btnDisplay = lang.sPrevious; - btnClass = button + (page > 0 ? - '' : ' disabled'); - break; - - case 'next': - btnDisplay = lang.sNext; - btnClass = button + (page < pages-1 ? - '' : ' disabled'); - break; - - case 'last': - btnDisplay = lang.sLast; - btnClass = button + (page < pages-1 ? - '' : ' disabled'); - break; - - default: - btnDisplay = button + 1; - btnClass = page === button ? - 'active' : ''; - break; - } - - if ( btnDisplay ) { - node = $('
  • ', { - 'class': classes.sPageButton+' '+btnClass, - 'id': idx === 0 && typeof button === 'string' ? - settings.sTableId +'_'+ button : - null - } ) - .append( $('', { - 'href': '#', - 'aria-controls': settings.sTableId, - 'aria-label': aria[ button ], - 'data-dt-idx': counter, - 'tabindex': settings.iTabIndex, - 'class': 'page-link' - } ) - .html( btnDisplay ) - ) - .appendTo( container ); - - settings.oApi._fnBindAction( - node, {action: button}, clickHandler - ); - - counter++; - } - } - } - }; - - // IE9 throws an 'unknown error' if document.activeElement is used - // inside an iframe or frame. - var activeEl; - - try { - // Because this approach is destroying and recreating the paging - // elements, focus is lost on the select button which is bad for - // accessibility. So we want to restore focus once the draw has - // completed - activeEl = $(host).find(document.activeElement).data('dt-idx'); - } - catch (e) {} - - attach( - $(host).empty().html('