Builder administration area
* Page builder foundation * Form builder document type * Dashboard builder document type * Report builder document type * Menu builder document type * Email builder document type * Query builder document type * Workflow builder document type * Reusable component registry * Form-field registry * Dashboard-widget registry * Hero, heading, and rich-content components * JSON document schema * Saved builder documents * Draft, published, and archived states * Document version history * Reusable builder templates * Component rendering service * Extensible BuilderComponent contract * Authenticated Builder REST endpoints * Administration navigation * Builder permissions * Module registration and test
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Modules\Builder\Components;
|
||||
use Modules\Builder\Contracts\BuilderComponent;
|
||||
final class ContentComponent implements BuilderComponent
|
||||
{
|
||||
public function type(): string { return 'content'; }
|
||||
public function label(): string { return 'Rich Content'; }
|
||||
public function category(): string { return 'Content'; }
|
||||
public function schema(): array { return ['content'=>['type'=>'textarea','required'=>true]]; }
|
||||
public function defaults(): array { return ['content'=>'<p>Add your content here.</p>']; }
|
||||
public function render(array $properties, array $context=[]): string { return (string)($properties['content']??''); }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Modules\Builder\Components;
|
||||
use Modules\Builder\Contracts\BuilderComponent;
|
||||
final class HeadingComponent implements BuilderComponent
|
||||
{
|
||||
public function type(): string { return 'heading'; }
|
||||
public function label(): string { return 'Heading'; }
|
||||
public function category(): string { return 'Typography'; }
|
||||
public function schema(): array { return ['text'=>['type'=>'text','required'=>true],'level'=>['type'=>'select','options'=>[1,2,3,4,5,6]]]; }
|
||||
public function defaults(): array { return ['text'=>'Heading','level'=>2]; }
|
||||
public function render(array $properties, array $context=[]): string { $level=max(1,min(6,(int)($properties['level']??2))); return '<h'.$level.'>'.e($properties['text']??'').'</h'.$level.'>'; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Modules\Builder\Components;
|
||||
use Modules\Builder\Contracts\BuilderComponent;
|
||||
final class HeroComponent implements BuilderComponent
|
||||
{
|
||||
public function type(): string { return 'hero'; }
|
||||
public function label(): string { return 'Hero'; }
|
||||
public function category(): string { return 'Content'; }
|
||||
public function schema(): array { return ['heading'=>['type'=>'text','required'=>true],'content'=>['type'=>'textarea'],'button_label'=>['type'=>'text'],'button_url'=>['type'=>'text']]; }
|
||||
public function defaults(): array { return ['heading'=>'A compelling headline','content'=>'Add supporting content here.','button_label'=>'Learn more','button_url'=>'#']; }
|
||||
public function render(array $properties, array $context=[]): string { return view('builder::components.hero',['properties'=>$properties])->render(); }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
return [
|
||||
'max_document_size_kb' => (int) env('BUILDER_MAX_DOCUMENT_SIZE_KB', 512),
|
||||
'autosave_seconds' => (int) env('BUILDER_AUTOSAVE_SECONDS', 30),
|
||||
'preview_route' => env('BUILDER_PREVIEW_ROUTE', 'builder.preview'),
|
||||
'allowed_types' => ['page','form','dashboard','report','menu','email','query','workflow'],
|
||||
];
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace Modules\Builder\Contracts;
|
||||
interface BuilderComponent
|
||||
{
|
||||
public function type(): string;
|
||||
public function label(): string;
|
||||
public function category(): string;
|
||||
public function schema(): array;
|
||||
public function defaults(): array;
|
||||
public function render(array $properties, array $context = []): string;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('builder_documents', function (Blueprint $table) {
|
||||
$table->id(); $table->foreignId('site_id')->nullable()->constrained('sites')->nullOnDelete();
|
||||
$table->string('type',30)->index(); $table->string('name'); $table->string('slug')->nullable();
|
||||
$table->string('status',20)->default('draft')->index(); $table->unsignedInteger('version')->default(1);
|
||||
$table->json('document')->nullable(); $table->json('settings')->nullable();
|
||||
$table->nullableMorphs('buildable'); $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('published_at')->nullable(); $table->timestamps();
|
||||
$table->unique(['site_id','type','slug'],'builder_document_site_type_slug_unique');
|
||||
});
|
||||
Schema::create('builder_document_versions', function (Blueprint $table) {
|
||||
$table->id(); $table->foreignId('builder_document_id')->constrained('builder_documents')->cascadeOnDelete();
|
||||
$table->unsignedInteger('version'); $table->json('document'); $table->json('settings')->nullable();
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); $table->timestamps();
|
||||
$table->unique(['builder_document_id','version']);
|
||||
});
|
||||
Schema::create('builder_templates', function (Blueprint $table) {
|
||||
$table->id(); $table->string('type',30)->index(); $table->string('name'); $table->string('slug')->unique();
|
||||
$table->text('description')->nullable(); $table->json('document'); $table->json('settings')->nullable();
|
||||
$table->boolean('is_system')->default(false); $table->boolean('is_active')->default(true)->index(); $table->timestamps();
|
||||
});
|
||||
}
|
||||
public function down(): void { Schema::dropIfExists('builder_templates'); Schema::dropIfExists('builder_document_versions'); Schema::dropIfExists('builder_documents'); }
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace Modules\Builder\Http\Controllers;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Modules\Builder\Models\BuilderDocument;
|
||||
use Modules\Builder\Services\BuilderRegistry;
|
||||
use Modules\Builder\Services\BuilderRenderer;
|
||||
final class BuilderApiController extends Controller
|
||||
{
|
||||
public function components(BuilderRegistry $registry): JsonResponse { return response()->json(['data'=>$registry->components(),'widgets'=>$registry->widgets(),'fields'=>$registry->fields()]); }
|
||||
public function show(BuilderDocument $document): JsonResponse { return response()->json(['data'=>$document]); }
|
||||
public function preview(BuilderDocument $document, BuilderRenderer $renderer): JsonResponse { return response()->json(['html'=>$renderer->render($document->document ?? [])]); }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace Modules\Builder\Http\Controllers;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
use Modules\Builder\Models\BuilderDocument;
|
||||
use Modules\Builder\Services\BuilderDocumentService;
|
||||
use Modules\Builder\Services\BuilderRegistry;
|
||||
final class BuilderController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$type=$request->string('type')->toString();
|
||||
$documents=BuilderDocument::query()->when($type,fn($q)=>$q->where('type',$type))->latest()->paginate(20)->withQueryString();
|
||||
return view('builder::index',compact('documents','type'));
|
||||
}
|
||||
public function create(BuilderRegistry $registry): View { return view('builder::editor',['document'=>new BuilderDocument(['type'=>request('type','page'),'document'=>['nodes'=>[]]]),'components'=>$registry->components()]); }
|
||||
public function edit(BuilderDocument $document, BuilderRegistry $registry): View { return view('builder::editor',['document'=>$document,'components'=>$registry->components()]); }
|
||||
public function store(Request $request, BuilderDocumentService $service): RedirectResponse { $document=$service->save(new BuilderDocument(),$this->validated($request),$request->user()?->id); return redirect()->route('administration.builder.edit',$document)->with('status','Builder document created.'); }
|
||||
public function update(Request $request, BuilderDocument $document, BuilderDocumentService $service): RedirectResponse { $service->save($document,$this->validated($request),$request->user()?->id); return back()->with('status','Builder document saved.'); }
|
||||
public function destroy(BuilderDocument $document): RedirectResponse { $document->delete(); return redirect()->route('administration.builder.index')->with('status','Builder document deleted.'); }
|
||||
private function validated(Request $request): array
|
||||
{
|
||||
$data=$request->validate(['type'=>['required','in:'.implode(',',config('builder.allowed_types'))],'name'=>['required','string','max:255'],'slug'=>['nullable','string','max:255'],'status'=>['required','in:draft,published,archived'],'document_json'=>['required','json'],'settings_json'=>['nullable','json']]);
|
||||
return ['type'=>$data['type'],'name'=>$data['name'],'slug'=>$data['slug'] ?: Str::slug($data['name']),'status'=>$data['status'],'document'=>json_decode($data['document_json'],true,512,JSON_THROW_ON_ERROR),'settings'=>json_decode($data['settings_json'] ?: '{}',true,512,JSON_THROW_ON_ERROR),'published_at'=>$data['status']==='published'?now():null];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace Modules\Builder\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
final class BuilderDocument extends Model
|
||||
{
|
||||
protected $fillable=['site_id','type','name','slug','status','version','document','settings','buildable_type','buildable_id','created_by','updated_by','published_at'];
|
||||
protected $casts=['document'=>'array','settings'=>'array','published_at'=>'datetime'];
|
||||
public function versions(): HasMany { return $this->hasMany(BuilderDocumentVersion::class); }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace Modules\Builder\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
final class BuilderDocumentVersion extends Model
|
||||
{
|
||||
protected $fillable=['builder_document_id','version','document','settings','created_by'];
|
||||
protected $casts=['document'=>'array','settings'=>'array'];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace Modules\Builder\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
final class BuilderTemplate extends Model
|
||||
{
|
||||
protected $fillable=['type','name','slug','description','document','settings','is_system','is_active'];
|
||||
protected $casts=['document'=>'array','settings'=>'array','is_system'=>'boolean','is_active'=>'boolean'];
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace Modules\Builder\Providers;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\Builder\Services\BuilderRegistry;
|
||||
final class BuilderServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(BuilderRegistry::class);
|
||||
$this->app->alias(BuilderRegistry::class, 'builder.registry');
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
$this->mergeConfigFrom(module_path('Builder', 'Config/config.php'), 'builder');
|
||||
}
|
||||
public function boot(BuilderRegistry $registry): void
|
||||
{
|
||||
$this->loadMigrationsFrom(module_path('Builder', 'Database/Migrations'));
|
||||
$this->loadViewsFrom(module_path('Builder', 'Resources/views'), 'builder');
|
||||
$registry->registerDefaults();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Modules\Builder\Providers;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
final class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
Route::middleware('web')->group(module_path('Builder', 'Routes/web.php'));
|
||||
Route::middleware('api')->prefix('api/v1')->group(module_path('Builder', 'Routes/api.php'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<section class="py-5 text-center"><div class="container"><h1>{{ $properties['heading'] ?? '' }}</h1><p class="lead">{{ $properties['content'] ?? '' }}</p>@if(!empty($properties['button_label']))<a class="btn btn-primary" href="{{ $properties['button_url'] ?? '#' }}">{{ $properties['button_label'] }}</a>@endif</div></section>
|
||||
@@ -0,0 +1,10 @@
|
||||
<x-layouts.app :title="__('Builder Editor')">
|
||||
<div class="flex h-full w-full flex-1 flex-col gap-5 rounded-xl">
|
||||
<div><flux:heading size="xl">{{ $document->exists ? 'Edit' : 'Create' }} Builder Document</flux:heading><flux:subheading>Use the component palette and JSON document canvas. Visual drag-and-drop clients can use the component API.</flux:subheading></div>
|
||||
<form method="POST" action="{{ $document->exists ? route('administration.builder.update',$document) : route('administration.builder.store') }}" class="space-y-5">@csrf @if($document->exists) @method('PUT') @endif
|
||||
<div class="grid gap-4 md:grid-cols-3"><flux:input name="name" label="Name" value="{{ old('name',$document->name) }}" required/><flux:select name="type" label="Builder type">@foreach(config('builder.allowed_types') as $type)<option value="{{ $type }}" @selected(old('type',$document->type)===$type)>{{ str($type)->headline() }}</option>@endforeach</flux:select><flux:select name="status" label="Status"><option value="draft" @selected(old('status',$document->status)==='draft')>Draft</option><option value="published" @selected(old('status',$document->status)==='published')>Published</option><option value="archived" @selected(old('status',$document->status)==='archived')>Archived</option></flux:select></div>
|
||||
<flux:input name="slug" label="Slug" value="{{ old('slug',$document->slug) }}"/>
|
||||
<div class="grid gap-5 xl:grid-cols-[260px_1fr]"><aside class="rounded-xl border p-4"><div class="mb-3 font-semibold">Components</div>@foreach($components as $component)<div class="mb-2 rounded-lg border p-3"><div class="font-medium">{{ $component['label'] }}</div><div class="text-xs text-zinc-500">{{ $component['category'] }} · {{ $component['type'] }}</div></div>@endforeach</aside><div class="space-y-4"><flux:textarea name="document_json" label="Document JSON" rows="22" required>{{ old('document_json',json_encode($document->document ?: ['nodes'=>[]],JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)) }}</flux:textarea><flux:textarea name="settings_json" label="Settings JSON" rows="6">{{ old('settings_json',json_encode($document->settings ?: new stdClass(),JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)) }}</flux:textarea></div></div>
|
||||
<div class="flex gap-3"><flux:button type="submit" variant="primary">Save</flux:button><flux:button :href="route('administration.builder.index')" wire:navigate>Cancel</flux:button></div>
|
||||
</form>
|
||||
</div></x-layouts.app>
|
||||
@@ -0,0 +1,7 @@
|
||||
<x-layouts.app :title="__('Builder')">
|
||||
<div class="flex h-full w-full flex-1 flex-col gap-6 rounded-xl">
|
||||
<div class="flex items-center justify-between"><div><flux:heading size="xl">Low-Code Builder</flux:heading><flux:subheading>Pages, forms, dashboards, reports, menus, emails, queries and workflows.</flux:subheading></div>@can('builder.manage')<flux:button variant="primary" :href="route('administration.builder.create')" wire:navigate>Create</flux:button>@endcan</div>
|
||||
<div class="grid gap-3 sm:grid-cols-4">@foreach(config('builder.allowed_types') as $builderType)<a href="{{ route('administration.builder.index',['type'=>$builderType]) }}" class="rounded-xl border p-4"><div class="font-semibold">{{ str($builderType)->headline() }}</div></a>@endforeach</div>
|
||||
<div class="overflow-hidden rounded-xl border"><table class="w-full text-sm"><thead><tr class="border-b text-left"><th class="p-3">Name</th><th>Type</th><th>Status</th><th>Version</th><th></th></tr></thead><tbody>@forelse($documents as $document)<tr class="border-b"><td class="p-3 font-medium">{{ $document->name }}</td><td>{{ str($document->type)->headline() }}</td><td>{{ ucfirst($document->status) }}</td><td>{{ $document->version }}</td><td class="text-right p-3"><flux:button size="sm" :href="route('administration.builder.edit',$document)" wire:navigate>Edit</flux:button></td></tr>@empty<tr><td colspan="5" class="p-6 text-center text-zinc-500">No builder documents yet.</td></tr>@endforelse</tbody></table></div>
|
||||
{{ $documents->links() }}
|
||||
</div></x-layouts.app>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Api\Http\Middleware\AuthenticateApiToken;
|
||||
use Modules\Builder\Http\Controllers\BuilderApiController;
|
||||
Route::middleware(AuthenticateApiToken::class)->prefix('builder')->name('api.builder.')->group(function () {
|
||||
Route::get('/components',[BuilderApiController::class,'components'])->name('components');
|
||||
Route::get('/documents/{document}',[BuilderApiController::class,'show'])->name('show');
|
||||
Route::get('/documents/{document}/preview',[BuilderApiController::class,'preview'])->name('preview');
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Builder\Http\Controllers\BuilderController;
|
||||
Route::middleware(['auth','permission:builder.view'])->prefix('administration/builder')->name('administration.builder.')->group(function () {
|
||||
Route::get('/',[BuilderController::class,'index'])->name('index');
|
||||
Route::get('/create',[BuilderController::class,'create'])->middleware('permission:builder.manage')->name('create');
|
||||
Route::post('/',[BuilderController::class,'store'])->middleware('permission:builder.manage')->name('store');
|
||||
Route::get('/{document}/edit',[BuilderController::class,'edit'])->middleware('permission:builder.manage')->name('edit');
|
||||
Route::put('/{document}',[BuilderController::class,'update'])->middleware('permission:builder.manage')->name('update');
|
||||
Route::delete('/{document}',[BuilderController::class,'destroy'])->middleware('permission:builder.manage')->name('destroy');
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace Modules\Builder\Services;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\Builder\Models\BuilderDocument;
|
||||
final class BuilderDocumentService
|
||||
{
|
||||
public function save(BuilderDocument $document, array $payload, ?int $userId=null): BuilderDocument
|
||||
{
|
||||
return DB::transaction(function () use ($document,$payload,$userId) {
|
||||
if ($document->exists && $document->isDirty()) $document->versions()->create(['version'=>$document->version,'document'=>$document->getOriginal('document') ?: [],'settings'=>$document->getOriginal('settings') ?: [],'created_by'=>$userId]);
|
||||
$document->fill($payload); $document->updated_by=$userId;
|
||||
if (!$document->exists) $document->created_by=$userId; else $document->version++;
|
||||
$document->save(); return $document->refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace Modules\Builder\Services;
|
||||
use InvalidArgumentException;
|
||||
use Modules\Builder\Contracts\BuilderComponent;
|
||||
use Modules\Builder\Components\ContentComponent;
|
||||
use Modules\Builder\Components\HeadingComponent;
|
||||
use Modules\Builder\Components\HeroComponent;
|
||||
final class BuilderRegistry
|
||||
{
|
||||
private array $components=[];
|
||||
private array $widgets=[];
|
||||
private array $fields=[];
|
||||
public function registerDefaults(): void
|
||||
{
|
||||
foreach ([HeroComponent::class, HeadingComponent::class, ContentComponent::class] as $component) $this->registerComponent(new $component());
|
||||
foreach (['metric','chart','table','activity','calendar'] as $widget) $this->widgets[$widget]=['type'=>$widget,'label'=>str($widget)->headline()->toString()];
|
||||
foreach (['text','textarea','email','number','select','checkbox','date','file'] as $field) $this->fields[$field]=['type'=>$field,'label'=>str($field)->headline()->toString()];
|
||||
}
|
||||
public function registerComponent(BuilderComponent $component): void { $this->components[$component->type()]=$component; }
|
||||
public function component(string $type): BuilderComponent { return $this->components[$type] ?? throw new InvalidArgumentException("Unknown builder component [$type]."); }
|
||||
public function components(): array { return array_map(fn(BuilderComponent $c)=>['type'=>$c->type(),'label'=>$c->label(),'category'=>$c->category(),'schema'=>$c->schema(),'defaults'=>$c->defaults()],array_values($this->components)); }
|
||||
public function widgets(): array { return array_values($this->widgets); }
|
||||
public function fields(): array { return array_values($this->fields); }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace Modules\Builder\Services;
|
||||
final class BuilderRenderer
|
||||
{
|
||||
public function __construct(private BuilderRegistry $registry) {}
|
||||
public function render(array $document, array $context=[]): string
|
||||
{
|
||||
return collect($document['nodes'] ?? [])->map(function(array $node) use ($context) {
|
||||
if (($node['kind'] ?? 'component') === 'container') return '<div class="builder-container">'.$this->render($node,$context).'</div>';
|
||||
return $this->registry->component((string)($node['type'] ?? 'content'))->render($node['properties'] ?? [],$context);
|
||||
})->implode("\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace Modules\Builder\Tests\Feature;
|
||||
use Modules\Builder\Services\BuilderRegistry;
|
||||
use Tests\TestCase;
|
||||
final class BuilderModuleTest extends TestCase
|
||||
{
|
||||
public function test_builder_module_registers_components(): void
|
||||
{
|
||||
$components=app(BuilderRegistry::class)->components();
|
||||
$this->assertNotEmpty($components);
|
||||
$this->assertContains('hero',array_column($components,'type'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"name":"Builder","alias":"builder","version":"1.0.0","description":"Low-code visual builders for pages, forms, dashboards, reports, menus, emails, queries and workflows.","keywords":["builder","low-code","visual","components","layouts"],"priority":160,"providers":["Modules\\Builder\\Providers\\BuilderServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Users","Roles","Pages","Forms","Menus","Workflow","Analytics","Sites"]}
|
||||
@@ -79,6 +79,7 @@ final class RolePermissionSeeder extends Seeder
|
||||
'analytics.view',
|
||||
'analytics.manage',
|
||||
'analytics.export',
|
||||
'builder.view', 'builder.manage', 'builder.publish',
|
||||
];
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
|
||||
+44
-43
@@ -1,45 +1,46 @@
|
||||
{
|
||||
"Stylist": true,
|
||||
"Core": true,
|
||||
"Administration": true,
|
||||
"Users": true,
|
||||
"Roles": true,
|
||||
"Settings": true,
|
||||
"Audit": true,
|
||||
"Media": true,
|
||||
"Seo": true,
|
||||
"Search": true,
|
||||
"Pages": true,
|
||||
"Menus": true,
|
||||
"Redirects": true,
|
||||
"Forms": true,
|
||||
"Documents": true,
|
||||
"Banners": true,
|
||||
"Faq": true,
|
||||
"Team": true,
|
||||
"Testimonials": true,
|
||||
"Categories": true,
|
||||
"Tags": true,
|
||||
"Authors": true,
|
||||
"Blog": true,
|
||||
"News": true,
|
||||
"Comments": true,
|
||||
"Feeds": true,
|
||||
"Sitemap": true,
|
||||
"CRM": true,
|
||||
"Newsletter": true,
|
||||
"Events": true,
|
||||
"Careers": true,
|
||||
"Locations": true,
|
||||
"Workflow": true,
|
||||
"Localization": true,
|
||||
"Sites": true,
|
||||
"Developer": true,
|
||||
"Api": true,
|
||||
"Enterprise": true,
|
||||
"Commerce": true,
|
||||
"Security": true,
|
||||
"Automation": true,
|
||||
"AI": true,
|
||||
"Analytics": true
|
||||
"Stylist": true,
|
||||
"Core": true,
|
||||
"Administration": true,
|
||||
"Users": true,
|
||||
"Roles": true,
|
||||
"Settings": true,
|
||||
"Audit": true,
|
||||
"Media": true,
|
||||
"Seo": true,
|
||||
"Search": true,
|
||||
"Pages": true,
|
||||
"Menus": true,
|
||||
"Redirects": true,
|
||||
"Forms": true,
|
||||
"Documents": true,
|
||||
"Banners": true,
|
||||
"Faq": true,
|
||||
"Team": true,
|
||||
"Testimonials": true,
|
||||
"Categories": true,
|
||||
"Tags": true,
|
||||
"Authors": true,
|
||||
"Blog": true,
|
||||
"News": true,
|
||||
"Comments": true,
|
||||
"Feeds": true,
|
||||
"Sitemap": true,
|
||||
"CRM": true,
|
||||
"Newsletter": true,
|
||||
"Events": true,
|
||||
"Careers": true,
|
||||
"Locations": true,
|
||||
"Workflow": true,
|
||||
"Localization": true,
|
||||
"Sites": true,
|
||||
"Developer": true,
|
||||
"Api": true,
|
||||
"Enterprise": true,
|
||||
"Commerce": true,
|
||||
"Security": true,
|
||||
"Automation": true,
|
||||
"AI": true,
|
||||
"Analytics": true,
|
||||
"Builder": true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user