move into sonardesk
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
<?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,40 @@
|
||||
<?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,38 @@
|
||||
<?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,8 @@
|
||||
<?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,18 @@
|
||||
<?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,58 @@
|
||||
<?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,27 @@
|
||||
<?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,61 @@
|
||||
<?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,18 @@
|
||||
<?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,12 @@
|
||||
<?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,12 @@
|
||||
<?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,24 @@
|
||||
<?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,15 @@
|
||||
<?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,11 @@
|
||||
<?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,13 @@
|
||||
<?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,28 @@
|
||||
<?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,56 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Builder\Services;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Modules\Builder\Components\ContentComponent;
|
||||
use Modules\Builder\Components\HeadingComponent;
|
||||
use Modules\Builder\Components\HeroComponent;
|
||||
use Modules\Builder\Contracts\BuilderComponent;
|
||||
|
||||
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,19 @@
|
||||
<?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,16 @@
|
||||
<?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"]}
|
||||
Reference in New Issue
Block a user