first upload all files
This commit is contained in:
23
Modules/Category/Config/assets.php
Normal file
23
Modules/Category/Config/assets.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Define which assets will be available through the asset manager
|
||||
|--------------------------------------------------------------------------
|
||||
| These assets are registered on the asset manager
|
||||
*/
|
||||
'all_assets' => [
|
||||
'admin.category.css' => ['module' => 'category:admin/css/category.css'],
|
||||
'admin.category.js' => ['module' => 'category:admin/js/category.js'],
|
||||
'admin.jstree.js' => ['module' => 'category:admin/js/jstree.js'],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Define which default assets will always be included in your pages
|
||||
| through the asset pipeline
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'required_assets' => [],
|
||||
];
|
||||
10
Modules/Category/Config/permissions.php
Normal file
10
Modules/Category/Config/permissions.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'admin.categories' => [
|
||||
'index' => 'category::permissions.index',
|
||||
'create' => 'category::permissions.create',
|
||||
'edit' => 'category::permissions.edit',
|
||||
'destroy' => 'category::permissions.destroy',
|
||||
],
|
||||
];
|
||||
12
Modules/Category/Database/Factories/ModelFactory.php
Normal file
12
Modules/Category/Database/Factories/ModelFactory.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
use Modules\Category\Entities\Category;
|
||||
|
||||
$factory->define(Category::class, function (Faker\Generator $faker) {
|
||||
return [
|
||||
'name' => $faker->word(),
|
||||
'slug' => $faker->slug(),
|
||||
'on_navigation' => $faker->boolean(),
|
||||
'is_active' => $faker->boolean(),
|
||||
];
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateCategoriesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('categories', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('parent_id')->unsigned()->nullable();
|
||||
$table->string('slug')->unique();
|
||||
$table->integer('position')->unsigned()->nullable();
|
||||
$table->boolean('is_searchable');
|
||||
$table->boolean('is_active');
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('parent_id')->references('id')->on('categories')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('categories');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateCategoryTranslationsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('category_translations', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('category_id')->unsigned();
|
||||
$table->string('locale');
|
||||
$table->string('name');
|
||||
|
||||
$table->unique(['category_id', 'locale']);
|
||||
$table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('category_translations');
|
||||
}
|
||||
}
|
||||
19
Modules/Category/Database/Seeders/CategoryDatabaseSeeder.php
Normal file
19
Modules/Category/Database/Seeders/CategoryDatabaseSeeder.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Category\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Modules\Category\Entities\Category;
|
||||
|
||||
class CategoryDatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
factory(Category::class, 20)->create();
|
||||
}
|
||||
}
|
||||
161
Modules/Category/Entities/Category.php
Normal file
161
Modules/Category/Entities/Category.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Category\Entities;
|
||||
|
||||
use TypiCMS\NestableTrait;
|
||||
use Modules\Media\Entities\File;
|
||||
use Modules\Support\Eloquent\Model;
|
||||
use Modules\Media\Eloquent\HasMedia;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Modules\Product\Entities\Product;
|
||||
use Modules\Support\Eloquent\Sluggable;
|
||||
use Modules\Support\Eloquent\Translatable;
|
||||
|
||||
class Category extends Model
|
||||
{
|
||||
use Translatable, Sluggable, HasMedia, NestableTrait;
|
||||
|
||||
/**
|
||||
* The relations to eager load on every query.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $with = ['translations'];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['parent_id', 'slug', 'position', 'is_searchable', 'is_active'];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = ['translations'];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'is_searchable' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are translatable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $translatedAttributes = ['name'];
|
||||
|
||||
/**
|
||||
* The attribute that will be slugged.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $slugAttribute = 'name';
|
||||
|
||||
/**
|
||||
* Perform any actions required after the model boots.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected static function booted()
|
||||
{
|
||||
static::addActiveGlobalScope();
|
||||
}
|
||||
|
||||
public static function findBySlug($slug)
|
||||
{
|
||||
return static::with('files')->where('slug', $slug)->firstOrNew([]);
|
||||
}
|
||||
|
||||
public function isRoot()
|
||||
{
|
||||
return $this->exists && is_null($this->parent_id);
|
||||
}
|
||||
|
||||
public function url()
|
||||
{
|
||||
return route('categories.products.index', ['category' => $this->slug]);
|
||||
}
|
||||
|
||||
public static function tree()
|
||||
{
|
||||
return Cache::tags('categories')
|
||||
->rememberForever(md5('categories.tree:' . locale()), function () {
|
||||
return static::with('files')
|
||||
->orderByRaw('-position DESC')
|
||||
->get()
|
||||
->nest();
|
||||
});
|
||||
}
|
||||
|
||||
public static function treeList()
|
||||
{
|
||||
return Cache::tags('categories')->rememberForever(md5('categories.tree_list:' . locale()), function () {
|
||||
return static::orderByRaw('-position DESC')
|
||||
->get()
|
||||
->nest()
|
||||
->setIndent('¦–– ')
|
||||
->listsFlattened('name');
|
||||
});
|
||||
}
|
||||
|
||||
public static function searchable()
|
||||
{
|
||||
return Cache::tags('categories')
|
||||
->rememberForever(md5('categories.searchable:' . locale()), function () {
|
||||
return static::where('is_searchable', true)
|
||||
->get()
|
||||
->map(function ($category) {
|
||||
return [
|
||||
'slug' => $category->slug,
|
||||
'name' => $category->name,
|
||||
];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function products()
|
||||
{
|
||||
return $this->belongsToMany(Product::class, 'product_categories');
|
||||
}
|
||||
|
||||
public function getLogoAttribute()
|
||||
{
|
||||
return $this->files->where('pivot.zone', 'logo')->first() ?: new File;
|
||||
}
|
||||
|
||||
public function getBannerAttribute()
|
||||
{
|
||||
return $this->files->where('pivot.zone', 'banner')->first() ?: new File;
|
||||
}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
$attributes = parent::toArray();
|
||||
|
||||
if ($this->relationLoaded('files')) {
|
||||
$attributes += [
|
||||
'logo' => [
|
||||
'id' => $this->logo->id,
|
||||
'path' => $this->logo->path,
|
||||
'exists' => $this->logo->exists,
|
||||
],
|
||||
'banner' => [
|
||||
'id' => $this->banner->id,
|
||||
'path' => $this->banner->path,
|
||||
'exists' => $this->banner->exists,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
}
|
||||
15
Modules/Category/Entities/CategoryTranslation.php
Normal file
15
Modules/Category/Entities/CategoryTranslation.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Category\Entities;
|
||||
|
||||
use Modules\Support\Eloquent\TranslationModel;
|
||||
|
||||
class CategoryTranslation extends TranslationModel
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['name'];
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Category\Http\Controllers\Admin;
|
||||
|
||||
use Modules\Category\Entities\Category;
|
||||
use Modules\Admin\Traits\HasCrudActions;
|
||||
use Modules\Category\Http\Requests\SaveCategoryRequest;
|
||||
|
||||
class CategoryController
|
||||
{
|
||||
use HasCrudActions;
|
||||
|
||||
/**
|
||||
* Model for the resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = Category::class;
|
||||
|
||||
/**
|
||||
* Label of the resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $label = 'category::categories.category';
|
||||
|
||||
/**
|
||||
* View path of the resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $viewPath = 'category::admin.categories';
|
||||
|
||||
/**
|
||||
* Form requests for the resource.
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $validation = SaveCategoryRequest::class;
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
return Category::with('files')->withoutGlobalScope('active')->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy resources by given ids.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
Category::withoutGlobalScope('active')
|
||||
->findOrFail($id)
|
||||
->delete();
|
||||
|
||||
return back()->withSuccess(trans('admin::messages.resource_deleted', ['resource' => $this->getLabel()]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Category\Http\Controllers\Admin;
|
||||
|
||||
use Modules\Category\Entities\Category;
|
||||
use Modules\Category\Services\CategoryTreeUpdater;
|
||||
use Modules\Category\Http\Responses\CategoryTreeResponse;
|
||||
|
||||
class CategoryTreeController
|
||||
{
|
||||
/**
|
||||
* Display category tree in json.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$categories = Category::withoutGlobalScope('active')
|
||||
->orderByRaw('-position DESC')
|
||||
->get();
|
||||
|
||||
return new CategoryTreeResponse($categories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update category tree in storage.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
CategoryTreeUpdater::update(request('category_tree'));
|
||||
|
||||
return trans('category::messages.category_order_saved');
|
||||
}
|
||||
}
|
||||
20
Modules/Category/Http/Controllers/CategoryController.php
Normal file
20
Modules/Category/Http/Controllers/CategoryController.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Category\Http\Controllers;
|
||||
|
||||
use Modules\Category\Entities\Category;
|
||||
|
||||
class CategoryController
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('public.categories.index', [
|
||||
'categories' => Category::all()->nest(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Category\Http\Controllers;
|
||||
|
||||
use Modules\Product\Entities\Product;
|
||||
use Modules\Category\Entities\Category;
|
||||
use Modules\Product\Filters\ProductFilter;
|
||||
use Modules\Product\Http\Controllers\ProductSearch;
|
||||
|
||||
class CategoryProductController
|
||||
{
|
||||
use ProductSearch;
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @param string $slug
|
||||
* @param \Modules\Product\Entities\Product $model
|
||||
* @param \Modules\Product\Filters\ProductFilter $productFilter
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index($slug, Product $model, ProductFilter $productFilter)
|
||||
{
|
||||
request()->merge(['category' => $slug]);
|
||||
|
||||
if (request()->expectsJson()) {
|
||||
return $this->searchProducts($model, $productFilter);
|
||||
}
|
||||
|
||||
$category = Category::findBySlug($slug);
|
||||
|
||||
return view('public.products.index', [
|
||||
'categoryName' => $category->name,
|
||||
'categoryBanner' => $category->banner->path,
|
||||
]);
|
||||
}
|
||||
}
|
||||
45
Modules/Category/Http/Requests/SaveCategoryRequest.php
Normal file
45
Modules/Category/Http/Requests/SaveCategoryRequest.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Category\Http\Requests;
|
||||
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Category\Entities\Category;
|
||||
use Modules\Core\Http\Requests\Request;
|
||||
|
||||
class SaveCategoryRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Available attributes.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $availableAttributes = 'category::attributes';
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'name' => 'required',
|
||||
'slug' => $this->getSlugRules(),
|
||||
'is_active' => 'required|boolean',
|
||||
'is_active' => 'required|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
private function getSlugRules()
|
||||
{
|
||||
$rules = $this->route()->getName() === 'admin.categories.update'
|
||||
? ['required']
|
||||
: ['nullable'];
|
||||
|
||||
$slug = Category::withoutGlobalScope('active')->where('id', $this->id)->value('slug');
|
||||
|
||||
$rules[] = Rule::unique('categories', 'slug')->ignore($slug, 'slug');
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
55
Modules/Category/Http/Responses/CategoryTreeResponse.php
Normal file
55
Modules/Category/Http/Responses/CategoryTreeResponse.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Category\Http\Responses;
|
||||
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
|
||||
class CategoryTreeResponse implements Responsable
|
||||
{
|
||||
/**
|
||||
* Categories collection.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
private $categories;
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Collection $categories
|
||||
*/
|
||||
public function __construct($categories)
|
||||
{
|
||||
$this->categories = $categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP response that represents the object.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function toResponse($request)
|
||||
{
|
||||
return response()->json($this->transform());
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the categories.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
private function transform()
|
||||
{
|
||||
return $this->categories->map(function ($category) {
|
||||
return [
|
||||
'id' => $category->id,
|
||||
'parent' => $category->parent_id ?: '#',
|
||||
'text' => $category->name,
|
||||
'data' => [
|
||||
'position' => $category->position,
|
||||
],
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
24
Modules/Category/Providers/CategoryServiceProvider.php
Normal file
24
Modules/Category/Providers/CategoryServiceProvider.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Category\Providers;
|
||||
|
||||
use Modules\Support\Traits\AddsAsset;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class CategoryServiceProvider extends ServiceProvider
|
||||
{
|
||||
use AddsAsset;
|
||||
|
||||
/**
|
||||
* Bootstrap the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->addAdminAssets('admin.categories.index', [
|
||||
'admin.category.css', 'admin.jstree.js', 'admin.category.js',
|
||||
'admin.media.css', 'admin.media.js',
|
||||
]);
|
||||
}
|
||||
}
|
||||
180
Modules/Category/Resources/assets/admin/js/CategoryForm.js
Normal file
180
Modules/Category/Resources/assets/admin/js/CategoryForm.js
Normal file
@@ -0,0 +1,180 @@
|
||||
import CategoryTree from './CategoryTree';
|
||||
|
||||
export default class {
|
||||
constructor() {
|
||||
let tree = $('.category-tree');
|
||||
|
||||
new CategoryTree(this, tree);
|
||||
|
||||
this.collapseAll(tree);
|
||||
this.expandAll(tree);
|
||||
this.addRootCategory();
|
||||
this.addSubCategory();
|
||||
|
||||
$('#category-form').on('submit', this.submit);
|
||||
|
||||
window.admin.removeSubmitButtonOffsetOn('#image', '.category-details-tab li > a');
|
||||
}
|
||||
|
||||
collapseAll(tree) {
|
||||
$('.collapse-all').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
tree.jstree('close_all');
|
||||
});
|
||||
}
|
||||
|
||||
expandAll(tree) {
|
||||
$('.expand-all').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
tree.jstree('open_all');
|
||||
});
|
||||
}
|
||||
|
||||
addRootCategory() {
|
||||
$('.add-root-category').on('click', () => {
|
||||
this.loading(true);
|
||||
|
||||
$('.add-sub-category').addClass('disabled');
|
||||
|
||||
$('.category-tree').jstree('deselect_all');
|
||||
|
||||
this.clear();
|
||||
|
||||
// Intentionally delay 150ms so that user feel new form is loaded.
|
||||
setTimeout(this.loading, 150, false);
|
||||
});
|
||||
}
|
||||
|
||||
addSubCategory() {
|
||||
$('.add-sub-category').on('click', () => {
|
||||
let selectedId = $('.category-tree').jstree('get_selected')[0];
|
||||
|
||||
if (selectedId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clear();
|
||||
this.loading(true);
|
||||
|
||||
window.form.appendHiddenInput('#category-form', 'parent_id', selectedId);
|
||||
|
||||
// Intentionally delay 150ms so that user feel new form is loaded.
|
||||
setTimeout(this.loading, 150, false);
|
||||
});
|
||||
}
|
||||
|
||||
fetchCategory(id) {
|
||||
this.loading(true);
|
||||
|
||||
$('.add-sub-category').removeClass('disabled');
|
||||
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: route('admin.categories.show', id),
|
||||
success: (category) => {
|
||||
this.update(category);
|
||||
|
||||
this.loading(false);
|
||||
},
|
||||
error: (xhr) => {
|
||||
error(xhr.responseJSON.message);
|
||||
|
||||
this.loading(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
update(category) {
|
||||
window.form.removeErrors();
|
||||
|
||||
$('.btn-delete').removeClass('hide');
|
||||
$('.form-group .help-block').remove();
|
||||
|
||||
$('#confirmation-form').attr('action', route('admin.categories.destroy', category.id));
|
||||
|
||||
$('#id-field').removeClass('hide');
|
||||
|
||||
$('#id').val(category.id);
|
||||
$('#name').val(category.name);
|
||||
|
||||
$('#slug').val(category.slug);
|
||||
$('#slug-field').removeClass('hide');
|
||||
$('.category-details-tab .seo-tab').removeClass('hide');
|
||||
|
||||
$('#is_searchable').prop('checked', category.is_searchable);
|
||||
$('#is_active').prop('checked', category.is_active);
|
||||
|
||||
$('.logo .image-holder-wrapper').html(this.categoryImage('logo', category.logo));
|
||||
$('.banner .image-holder-wrapper').html(this.categoryImage('banner', category.banner));
|
||||
|
||||
$('#category-form input[name="parent_id"]').remove();
|
||||
}
|
||||
|
||||
categoryImage(fieldName, file) {
|
||||
if (! file.exists) {
|
||||
return this.imagePlaceholder();
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="image-holder">
|
||||
<img src="${file.path}">
|
||||
<button type="button" class="btn remove-image" data-input-name="files[${fieldName}]"></button>
|
||||
<input type="hidden" name="files[${fieldName}]" value="${file.id}">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
clear() {
|
||||
$('#id-field').addClass('hide');
|
||||
|
||||
$('#id').val('');
|
||||
$('#name').val('');
|
||||
|
||||
$('#slug').val('');
|
||||
$('#slug-field').addClass('hide');
|
||||
$('.category-details-tab .seo-tab').addClass('hide');
|
||||
|
||||
$('#is_searchable').prop('checked', false);
|
||||
$('#is_active').prop('checked', false);
|
||||
|
||||
$('.logo .image-holder-wrapper').html(this.imagePlaceholder());
|
||||
$('.banner .image-holder-wrapper').html(this.imagePlaceholder());
|
||||
|
||||
$('.btn-delete').addClass('hide');
|
||||
$('.form-group .help-block').remove();
|
||||
|
||||
$('#category-form input[name="parent_id"]').remove();
|
||||
|
||||
$('.general-information-tab a').click();
|
||||
}
|
||||
|
||||
imagePlaceholder() {
|
||||
return `
|
||||
<div class="image-holder placeholder">
|
||||
<i class="fa fa-picture-o"></i>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
loading(state) {
|
||||
if (state === true) {
|
||||
$('.overlay.loader').removeClass('hide');
|
||||
} else {
|
||||
$('.overlay.loader').addClass('hide');
|
||||
}
|
||||
}
|
||||
|
||||
submit(e) {
|
||||
let selectedId = $('.category-tree').jstree('get_selected')[0];
|
||||
|
||||
if (! $('#slug-field').hasClass('hide')) {
|
||||
window.form.appendHiddenInput('#category-form', '_method', 'put');
|
||||
|
||||
$('#category-form').attr('action', route('admin.categories.update', selectedId));
|
||||
}
|
||||
|
||||
e.currentTarget.submit();
|
||||
}
|
||||
}
|
||||
73
Modules/Category/Resources/assets/admin/js/CategoryTree.js
Normal file
73
Modules/Category/Resources/assets/admin/js/CategoryTree.js
Normal file
@@ -0,0 +1,73 @@
|
||||
export default class {
|
||||
constructor(categoryForm, selector) {
|
||||
this.selector = selector;
|
||||
|
||||
$.jstree.defaults.dnd.touch = true;
|
||||
$.jstree.defaults.dnd.copy = false;
|
||||
|
||||
this.fetchCategoryTree();
|
||||
|
||||
// On selecting a category.
|
||||
selector.on('select_node.jstree', (e, node) => categoryForm.fetchCategory(node.selected[0]));
|
||||
|
||||
// Expand categories when jstree is loaded.
|
||||
selector.on('loaded.jstree', () => selector.jstree('open_all'));
|
||||
|
||||
// On updating category tree.
|
||||
this.selector.on('move_node.jstree', (e, data) => {
|
||||
this.updateCategoryTree(data);
|
||||
});
|
||||
}
|
||||
|
||||
fetchCategoryTree() {
|
||||
this.selector.jstree({
|
||||
core: {
|
||||
data: { url: route('admin.categories.tree') },
|
||||
check_callback: true,
|
||||
},
|
||||
plugins: ['dnd'],
|
||||
});
|
||||
}
|
||||
|
||||
updateCategoryTree(data) {
|
||||
this.loading(data.node, true);
|
||||
|
||||
$.ajax({
|
||||
type: 'PUT',
|
||||
url: route('admin.categories.tree.update'),
|
||||
data: { category_tree: this.getCategoryTree() },
|
||||
success: (message) => {
|
||||
success(message);
|
||||
|
||||
this.loading(data.node, false);
|
||||
},
|
||||
error: (xhr) => {
|
||||
error(xhr.responseJSON.message);
|
||||
|
||||
this.loading(data.node, false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getCategoryTree() {
|
||||
let categories = this.selector.jstree(true).get_json('#', { flat: true });
|
||||
|
||||
return categories.reduce((formatted, category) => {
|
||||
return formatted.concat({
|
||||
id: category.id,
|
||||
parent_id: (category.parent === '#') ? null : category.parent,
|
||||
position: category.data.position,
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
loading(node, state) {
|
||||
let nodeElement = this.selector.jstree().get_node(node, true);
|
||||
|
||||
if (state) {
|
||||
$(nodeElement).addClass('jstree-loading');
|
||||
} else {
|
||||
$(nodeElement).removeClass('jstree-loading');
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Modules/Category/Resources/assets/admin/js/main.js
Normal file
3
Modules/Category/Resources/assets/admin/js/main.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import CategoryForm from './CategoryForm';
|
||||
|
||||
new CategoryForm();
|
||||
37
Modules/Category/Resources/assets/admin/sass/main.scss
Normal file
37
Modules/Category/Resources/assets/admin/sass/main.scss
Normal file
@@ -0,0 +1,37 @@
|
||||
@import '~jstree/dist/themes/default/style';
|
||||
|
||||
.category-tree {
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
.add-root-category {
|
||||
display: table;
|
||||
}
|
||||
|
||||
.add-sub-category {
|
||||
display: table;
|
||||
margin: 5px 0 10px;
|
||||
}
|
||||
|
||||
.rtl {
|
||||
.jstree-default {
|
||||
&.jstree-rtl .jstree-node {
|
||||
background-position: 0% 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#category-form .loading-spinner {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.category-details-tab {
|
||||
margin: 0 -15px 0 15px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 991px) {
|
||||
.category-details-tab {
|
||||
margin: 30px 0 0 -15px;
|
||||
}
|
||||
}
|
||||
9
Modules/Category/Resources/lang/en/attributes.php
Normal file
9
Modules/Category/Resources/lang/en/attributes.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'id' => 'ID',
|
||||
'name' => 'Name',
|
||||
'slug' => 'URL',
|
||||
'is_searchable' => 'Searchable',
|
||||
'is_active' => 'Status',
|
||||
];
|
||||
23
Modules/Category/Resources/lang/en/categories.php
Normal file
23
Modules/Category/Resources/lang/en/categories.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'category' => 'Category',
|
||||
'categories' => 'Categories',
|
||||
'tree' => [
|
||||
'add_root_category' => 'Add Root Category',
|
||||
'add_sub_category' => 'Add Subcategory',
|
||||
'collapse_all' => 'Collapse All',
|
||||
'expand_all' => 'Expand All',
|
||||
],
|
||||
'tabs' => [
|
||||
'general' => 'General',
|
||||
'image' => 'Image',
|
||||
'seo' => 'SEO',
|
||||
],
|
||||
'form' => [
|
||||
'show_this_category_in_search_box' => 'Show this category in search box category list',
|
||||
'enable_the_category' => 'Enable the category',
|
||||
'logo' => 'Logo',
|
||||
'banner' => 'Banner',
|
||||
],
|
||||
];
|
||||
5
Modules/Category/Resources/lang/en/messages.php
Normal file
5
Modules/Category/Resources/lang/en/messages.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'category_order_saved' => 'Category order has been saved.',
|
||||
];
|
||||
8
Modules/Category/Resources/lang/en/permissions.php
Normal file
8
Modules/Category/Resources/lang/en/permissions.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'index' => 'Index Categories',
|
||||
'create' => 'Create Categories',
|
||||
'edit' => 'Edit Categories',
|
||||
'destroy' => 'Delete Categories',
|
||||
];
|
||||
5
Modules/Category/Resources/lang/en/sidebar.php
Normal file
5
Modules/Category/Resources/lang/en/sidebar.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'categories' => 'Categories',
|
||||
];
|
||||
@@ -0,0 +1,107 @@
|
||||
@extends('admin::layout')
|
||||
|
||||
@component('admin::components.page.header')
|
||||
@slot('title', trans('category::categories.categories'))
|
||||
|
||||
<li class="active">{{ trans('category::categories.categories') }}</li>
|
||||
@endcomponent
|
||||
|
||||
@section('content')
|
||||
<div class="box box-default">
|
||||
<div class="box-body clearfix">
|
||||
<div class="col-lg-2 col-md-3">
|
||||
<div class="row">
|
||||
<button class="btn btn-default add-root-category">{{ trans('category::categories.tree.add_root_category') }}</button>
|
||||
<button class="btn btn-default add-sub-category disabled">{{ trans('category::categories.tree.add_sub_category') }}</button>
|
||||
|
||||
<div class="m-b-10">
|
||||
<a href="#" class="collapse-all">{{ trans('category::categories.tree.collapse_all') }}</a> |
|
||||
<a href="#" class="expand-all">{{ trans('category::categories.tree.expand_all') }}</a>
|
||||
</div>
|
||||
|
||||
<div class="category-tree"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-10 col-md-9">
|
||||
<div class="tab-wrapper category-details-tab">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="general-information-tab active"><a data-toggle="tab" href="#general-information">{{ trans('category::categories.tabs.general') }}</a></li>
|
||||
|
||||
@hasAccess('admin.media.index')
|
||||
<li class="image-tab"><a data-toggle="tab" href="#image">{{ trans('category::categories.tabs.image') }}</a></li>
|
||||
@endHasAccess
|
||||
|
||||
<li class="seo-tab hide"><a data-toggle="tab" href="#seo">{{ trans('category::categories.tabs.seo') }}</a></li>
|
||||
</ul>
|
||||
|
||||
<form method="POST" action="{{ route('admin.categories.store') }}" class="form-horizontal" id="category-form" novalidate>
|
||||
{{ csrf_field() }}
|
||||
|
||||
<div class="tab-content">
|
||||
<div id="general-information" class="tab-pane fade in active">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div id="id-field" class="hide">
|
||||
{{ Form::text('id', trans('category::attributes.id'), $errors, null, ['disabled' => true]) }}
|
||||
</div>
|
||||
|
||||
{{ Form::text('name', trans('category::attributes.name'), $errors, null, ['required' => true]) }}
|
||||
{{ Form::checkbox('is_searchable', trans('category::attributes.is_searchable'), trans('category::categories.form.show_this_category_in_search_box'), $errors) }}
|
||||
{{ Form::checkbox('is_active', trans('category::attributes.is_active'), trans('category::categories.form.enable_the_category'), $errors) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (auth()->user()->hasAccess('admin.media.index'))
|
||||
<div id="image" class="tab-pane fade">
|
||||
<div class="logo">
|
||||
@include('media::admin.image_picker.single', [
|
||||
'title' => trans('category::categories.form.logo'),
|
||||
'inputName' => 'files[logo]',
|
||||
'file' => (object) ['exists' => false],
|
||||
])
|
||||
</div>
|
||||
|
||||
<div class="banner">
|
||||
@include('media::admin.image_picker.single', [
|
||||
'title' => trans('category::categories.form.banner'),
|
||||
'inputName' => 'files[banner]',
|
||||
'file' => (object) ['exists' => false],
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div id="seo" class="tab-pane fade">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="hide" id="slug-field">
|
||||
{{ Form::text('slug', trans('category::attributes.slug'), $errors) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<button type="submit" class="btn btn-primary" data-loading>
|
||||
{{ trans('admin::admin.buttons.save') }}
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-link text-red btn-delete p-l-0 hide" data-confirm>
|
||||
{{ trans('admin::admin.buttons.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay loader hide">
|
||||
<i class="fa fa-refresh fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
45
Modules/Category/Routes/admin.php
Normal file
45
Modules/Category/Routes/admin.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('categories/tree', [
|
||||
'as' => 'admin.categories.tree',
|
||||
'uses' => 'CategoryTreeController@index',
|
||||
'middleware' => 'can:admin.categories.index',
|
||||
]);
|
||||
|
||||
Route::put('categories/tree', [
|
||||
'as' => 'admin.categories.tree.update',
|
||||
'uses' => 'CategoryTreeController@update',
|
||||
'middleware' => 'can:admin.categories.edit',
|
||||
]);
|
||||
|
||||
Route::get('categories', [
|
||||
'as' => 'admin.categories.index',
|
||||
'uses' => 'CategoryController@index',
|
||||
'middleware' => 'can:admin.categories.index',
|
||||
]);
|
||||
|
||||
Route::post('categories', [
|
||||
'as' => 'admin.categories.store',
|
||||
'uses' => 'CategoryController@store',
|
||||
'middleware' => 'can:admin.categories.create',
|
||||
]);
|
||||
|
||||
Route::get('categories/{id}', [
|
||||
'as' => 'admin.categories.show',
|
||||
'uses' => 'CategoryController@show',
|
||||
'middleware' => 'can:admin.categories.edit',
|
||||
]);
|
||||
|
||||
Route::put('categories/{id}', [
|
||||
'as' => 'admin.categories.update',
|
||||
'uses' => 'CategoryController@update',
|
||||
'middleware' => 'can:admin.categories.edit',
|
||||
]);
|
||||
|
||||
Route::delete('categories/{id}', [
|
||||
'as' => 'admin.categories.destroy',
|
||||
'uses' => 'CategoryController@destroy',
|
||||
'middleware' => 'can:admin.categories.destroy',
|
||||
]);
|
||||
7
Modules/Category/Routes/public.php
Normal file
7
Modules/Category/Routes/public.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('categories', 'CategoryController@index')->name('categories.index');
|
||||
|
||||
Route::get('categories/{category}/products', 'CategoryProductController@index')->name('categories.products.index');
|
||||
95
Modules/Category/Services/CategoryTreeUpdater.php
Normal file
95
Modules/Category/Services/CategoryTreeUpdater.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Category\Services;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Modules\Category\Entities\Category;
|
||||
|
||||
class CategoryTreeUpdater
|
||||
{
|
||||
/**
|
||||
* Update category tree order.
|
||||
*
|
||||
* @param array $tree
|
||||
* @return void
|
||||
*/
|
||||
public static function update(array $tree)
|
||||
{
|
||||
list($ids, $parentIdCases, $positionCases, $params) = static::getDataForQuery($tree);
|
||||
|
||||
$sql = "UPDATE `categories` SET
|
||||
`parent_id` = CASE `id` {$parentIdCases} END,
|
||||
`position` = CASE `id` {$positionCases} END,
|
||||
`updated_at` = ?
|
||||
WHERE `id` IN ({$ids})";
|
||||
|
||||
DB::update($sql, $params);
|
||||
|
||||
Cache::tags('categories')->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data for update query.
|
||||
*
|
||||
* @param array $tree
|
||||
* @return array
|
||||
*/
|
||||
private static function getDataForQuery(array $tree)
|
||||
{
|
||||
$params = [];
|
||||
$ids = [];
|
||||
|
||||
foreach (static::getAttributesList($tree) as $id => $values) {
|
||||
foreach ($values as $column => $value) {
|
||||
$cases[$column][] = "WHEN {$id} THEN ?";
|
||||
$params[$column][] = $value;
|
||||
}
|
||||
|
||||
$ids[] = $id;
|
||||
}
|
||||
|
||||
return static::prepareData($ids, $cases, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attributes list from given tree.
|
||||
*
|
||||
* @param array $tree
|
||||
* @return array
|
||||
*/
|
||||
private static function getAttributesList(array $tree)
|
||||
{
|
||||
$attributes = [];
|
||||
|
||||
foreach ($tree as $position => $category) {
|
||||
$attributes[$category['id']] = [
|
||||
'parent_id' => $category['parent_id'],
|
||||
'position' => $position,
|
||||
];
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare data for update query.
|
||||
*
|
||||
* @param array $ids
|
||||
* @param array $cases
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
private static function prepareData(array $ids, array $cases, array $params)
|
||||
{
|
||||
$ids = implode(',', $ids);
|
||||
$parentIdCases = implode(' ', $cases['parent_id']);
|
||||
$positionCases = implode(' ', $cases['position']);
|
||||
|
||||
$params = array_flatten($params);
|
||||
$params[] = Carbon::now();
|
||||
|
||||
return [$ids, $parentIdCases, $positionCases, $params];
|
||||
}
|
||||
}
|
||||
26
Modules/Category/Sidebar/SidebarExtender.php
Normal file
26
Modules/Category/Sidebar/SidebarExtender.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Category\Sidebar;
|
||||
|
||||
use Maatwebsite\Sidebar\Item;
|
||||
use Maatwebsite\Sidebar\Menu;
|
||||
use Maatwebsite\Sidebar\Group;
|
||||
use Modules\Admin\Sidebar\BaseSidebarExtender;
|
||||
|
||||
class SidebarExtender extends BaseSidebarExtender
|
||||
{
|
||||
public function extend(Menu $menu)
|
||||
{
|
||||
$menu->group(trans('admin::sidebar.content'), function (Group $group) {
|
||||
$group->item(trans('product::sidebar.products'), function (Item $item) {
|
||||
$item->item(trans('category::sidebar.categories'), function (Item $item) {
|
||||
$item->weight(10);
|
||||
$item->route('admin.categories.index');
|
||||
$item->authorize(
|
||||
$this->auth->hasAccess('admin.categories.index')
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
28
Modules/Category/composer.json
Normal file
28
Modules/Category/composer.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "fleetcart/category",
|
||||
"description": "The FleetCart Category Module.",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Envay Soft",
|
||||
"email": "envaysoft@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.0.2",
|
||||
"typicms/nestablecollection": "^1.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\Category\\": ""
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
9
Modules/Category/module.json
Normal file
9
Modules/Category/module.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "Category",
|
||||
"alias": "category",
|
||||
"description": "The category module is responsible for managing categories.",
|
||||
"priority": 70,
|
||||
"providers": [
|
||||
"Modules\\Category\\Providers\\CategoryServiceProvider"
|
||||
]
|
||||
}
|
||||
7
Modules/Category/package.json
Normal file
7
Modules/Category/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "category-module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"jstree": "^3.3.9"
|
||||
}
|
||||
}
|
||||
12
Modules/Category/webpack.mix.js
Normal file
12
Modules/Category/webpack.mix.js
Normal file
@@ -0,0 +1,12 @@
|
||||
let mix = require('laravel-mix');
|
||||
let execSync = require('child_process').execSync;
|
||||
|
||||
mix.js(`${__dirname}/Resources/assets/admin/js/main.js`, `${__dirname}/Assets/admin/js/category.js`)
|
||||
.scripts(`${__dirname}/node_modules/jstree/dist/jstree.js`, `${__dirname}/Assets/admin/js/jstree.js`)
|
||||
.sass(`${__dirname}/Resources/assets/admin/sass/main.scss`, `${__dirname}/Assets/admin/css/category.css`)
|
||||
.copy(`${__dirname}/node_modules/jstree/dist/themes/default/32px.png`, `${__dirname}/Assets/admin/css`)
|
||||
.copy(`${__dirname}/node_modules/jstree/dist/themes/default/40px.png`, `${__dirname}/Assets/admin/css`)
|
||||
.copy(`${__dirname}/node_modules/jstree/dist/themes/default/throbber.gif`, `${__dirname}/Assets/admin/css`)
|
||||
.then(() => {
|
||||
execSync(`npm run rtlcss ${__dirname}/Assets/admin/css/category.css ${__dirname}/Assets/admin/css/category.rtl.css`);
|
||||
});
|
||||
14
Modules/Category/yarn.lock
Normal file
14
Modules/Category/yarn.lock
Normal file
@@ -0,0 +1,14 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
jquery@>=1.9.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787"
|
||||
|
||||
jstree@^3.3.9:
|
||||
version "3.3.9"
|
||||
resolved "https://registry.yarnpkg.com/jstree/-/jstree-3.3.9.tgz#62b47cad3c4fda390d021e5c4f98ee5b3365198a"
|
||||
integrity sha512-jRIbhg+BHrIs1Wm6oiJt3oKTVBE6sWS0PCp2/RlkIUqsLUPWUYgV3q8LfKoi1/E+YMzGtP6BuK4okk+0mwfmhQ==
|
||||
dependencies:
|
||||
jquery ">=1.9.1"
|
||||
Reference in New Issue
Block a user