Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e2b7a5e
schemas (wip)
Baspa Sep 10, 2025
94e4faa
add selecttree
Baspa Sep 10, 2025
a65a64f
add grouping
Baspa Sep 10, 2025
e6b7459
Fix styling
Baspa Sep 10, 2025
fb0f224
add fieldset schema
Baspa Sep 10, 2025
51dcc2c
Merge branch 'feat/form-layout-components' of github.com:backstagephp…
Baspa Sep 10, 2025
359e53f
Fix styling
Baspa Sep 10, 2025
2ebe975
use get key name
Baspa Sep 10, 2025
5af1ebf
new trait to render schemas with fields
Baspa Sep 10, 2025
a1041c0
Merge branch 'feat/form-layout-components' of github.com:backstagephp…
Baspa Sep 10, 2025
0b0bc41
Fix styling
Baspa Sep 10, 2025
66257c7
feat: table columns in repeater
Baspa Sep 11, 2025
852de44
wip (needs to be tested thoroughly, also in Backstage)
Baspa Sep 18, 2025
da63411
test
Baspa Sep 18, 2025
bcd799f
Merge branch 'feat/form-layout-components' of github.com:backstagephp…
Baspa Sep 18, 2025
91720fb
Merge branch 'main' of github.com:backstagephp/fields into feat/form-…
Baspa Sep 26, 2025
d130fdb
Fix styling
Baspa Sep 26, 2025
6ee4055
wip?
Baspa Oct 1, 2025
d5f6804
Merge branch 'feat/form-layout-components' of github.com:backstagephp…
Baspa Oct 10, 2025
1365c2c
Merge branch 'main' into feat/form-layout-components
Baspa Oct 10, 2025
ab1030f
Fix styling
Baspa Oct 10, 2025
c61d623
fix: phpstan issue
Baspa Oct 10, 2025
037c057
fix: tests
Baspa Oct 10, 2025
ba61ff4
wip
Baspa Oct 10, 2025
a37e651
Merge branch 'main' into feat/form-layout-components
Baspa Dec 11, 2025
5c3fdae
Update Repeater to exclusively use tableMode
Baspa Dec 11, 2025
8c82d52
repeater improvements
Baspa Dec 11, 2025
b75a1bf
styles: fix styling issues
Baspa Dec 11, 2025
e02ce68
fix phpstan issues
Baspa Dec 11, 2025
ba26403
Merge branch 'feat/form-layout-components' of github.com:backstagephp…
Baspa Dec 11, 2025
ec0cad2
styles: fix styling issues
Baspa Dec 11, 2025
4587a32
remove logs
Baspa Dec 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions database/migrations/add_schema_id_to_fields_table.php.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up()
{
Schema::table('fields', function (Blueprint $table) {
$table->ulid('schema_id')->nullable()->after('group');
$table->foreign('schema_id')->references('ulid')->on('schemas')->onDelete('set null');
});
}

public function down()
{
Schema::table('fields', function (Blueprint $table) {
$table->dropForeign(['schema_id']);
$table->dropColumn('schema_id');
});
}
};
35 changes: 35 additions & 0 deletions database/migrations/create_schemas_table.php.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?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('schemas', function (Blueprint $table) {
$table->ulid('ulid')->primary();
$table->string('name');
$table->string('slug');
$table->string('field_type');
$table->json('config')->nullable();
$table->integer('position')->default(0);
$table->string('model_type');
$table->string('model_key');
$table->ulid('parent_ulid')->nullable();
$table->timestamps();

$table->index(['model_type', 'model_key']);
$table->index(['model_type', 'model_key', 'position']);
$table->index(['parent_ulid']);

$table->unique(['model_type', 'model_key', 'slug']);
});
}

public function down(): void
{
Schema::dropIfExists('schemas');
}
};
106 changes: 75 additions & 31 deletions src/Concerns/CanMapDynamicFields.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,11 @@ trait CanMapDynamicFields
'tags' => Tags::class,
];

public function boot(): void
{
$this->fieldInspector = app(FieldInspector::class);
}

#[On('refreshFields')]
public function refresh(): void
#[On('refreshSchemas')]
public function refreshFields(): void
{
//
// Custom refresh logic for fields
}

/**
Expand Down Expand Up @@ -123,17 +119,21 @@ protected function mutateBeforeSave(array $data): array

private function hasValidRecordWithFields(): bool
{
return isset($this->record) && ! $this->record->fields->isEmpty();
return property_exists($this, 'record') && isset($this->record) && ! $this->record->fields->isEmpty();
}

private function hasValidRecord(): bool
{
return isset($this->record);
return property_exists($this, 'record') && isset($this->record);
}

private function extractFormValues(array $data): array
{
return isset($data[$this->record?->valueColumn]) ? $data[$this->record?->valueColumn] : [];
if (! property_exists($this, 'record') || ! $this->record) {
return [];
}

return isset($data[$this->record->valueColumn]) ? $data[$this->record->valueColumn] : [];
}

/**
Expand Down Expand Up @@ -165,6 +165,10 @@ private function extractBuilderBlocks(array $values): array
*/
private function getAllFieldsIncludingBuilderFields(array $builderBlocks): Collection
{
if (! property_exists($this, 'record') || ! $this->record) {
return collect();
}

return $this->record->fields->merge(
$this->getFieldsFromBlocks($builderBlocks)
);
Expand All @@ -189,11 +193,17 @@ private function applyFieldFillMutation(Model $field, array $fieldConfig, object
return $this->processBuilderFieldFillMutation($field, $fieldInstance, $data, $fieldLocation['builderData'], $builderBlocks);
}

return $fieldInstance->mutateFormDataCallback($this->record, $field, $data);
if (property_exists($this, 'record') && $this->record) {
return $fieldInstance->mutateFormDataCallback($this->record, $field, $data);
}

return $data;
}

// Default behavior: copy value from record to form data
$data[$this->record->valueColumn][$field->ulid] = $this->record->values[$field->ulid] ?? null;
if (property_exists($this, 'record') && $this->record) {
$data[$this->record->valueColumn][$field->ulid] = $this->record->values[$field->ulid] ?? null;
}

return $data;
}
Expand All @@ -205,7 +215,7 @@ private function applyFieldFillMutation(Model $field, array $fieldConfig, object
*/
private function extractBuilderBlocksFromRecord(): array
{
if (! isset($this->record->values) || ! is_array($this->record->values)) {
if (! property_exists($this, 'record') || ! $this->record || ! isset($this->record->values) || ! is_array($this->record->values)) {
return [];
}

Expand Down Expand Up @@ -235,14 +245,20 @@ private function processBuilderFieldFillMutation(Model $field, object $fieldInst
$mockRecord = $this->createMockRecordForBuilder($builderData);

// Create a temporary data structure for the callback
$tempData = [$this->record->valueColumn => $builderData];
$tempData = $fieldInstance->mutateFormDataCallback($mockRecord, $field, $tempData);
if (property_exists($this, 'record') && $this->record) {
$tempData = [$this->record->valueColumn => $builderData];
$tempData = $fieldInstance->mutateFormDataCallback($mockRecord, $field, $tempData);
} else {
$tempData = [];
}

// Update the original data structure with the mutated values
$this->updateBuilderBlocksWithMutatedData($builderBlocks, $field, $tempData);

// Update the main data structure
$data[$this->record->valueColumn] = array_merge($data[$this->record->valueColumn], $builderBlocks);
if (property_exists($this, 'record') && $this->record) {
$data[$this->record->valueColumn] = array_merge($data[$this->record->valueColumn], $builderBlocks);
}

return $data;
}
Expand All @@ -255,6 +271,9 @@ private function processBuilderFieldFillMutation(Model $field, object $fieldInst
*/
private function createMockRecordForBuilder(array $builderData): object
{
if (! property_exists($this, 'record') || ! $this->record) {
throw new \RuntimeException('Record property is not available');
}
$mockRecord = clone $this->record;
$mockRecord->values = $builderData;

Expand All @@ -274,7 +293,9 @@ private function updateBuilderBlocksWithMutatedData(array &$builderBlocks, Model
if (is_array($builderBlocks)) {
foreach ($builderBlocks as &$block) {
if (isset($block['data']) && is_array($block['data']) && isset($block['data'][$field->ulid])) {
$block['data'][$field->ulid] = $tempData[$this->record->valueColumn][$field->ulid] ?? $block['data'][$field->ulid];
if (property_exists($this, 'record') && $this->record) {
$block['data'][$field->ulid] = $tempData[$this->record->valueColumn][$field->ulid] ?? $block['data'][$field->ulid];
}
}
}
}
Expand All @@ -292,6 +313,11 @@ private function updateBuilderBlocksWithMutatedData(array &$builderBlocks, Model
*/
private function resolveFieldConfigAndInstance(Model $field): array
{
// Initialize field inspector if not already done
if (! isset($this->fieldInspector)) {
$this->fieldInspector = app(FieldInspector::class);
}

// Try to resolve from custom fields first
$fieldConfig = Fields::resolveField($field->field_type) ?
$this->fieldInspector->initializeCustomField($field->field_type) :
Expand Down Expand Up @@ -391,9 +417,9 @@ private function processNestedFields(Model $field, array $data, callable $mutati
*/
private function resolveFormFields(mixed $record = null, bool $isNested = false): array
{
$record = $record ?? $this->record;
$record = $record ?? (property_exists($this, 'record') ? $this->record : null);

if (! isset($record->fields) || $record->fields->isEmpty()) {
if (! $record || ! isset($record->fields) || $record->fields->isEmpty()) {
return [];
}

Expand All @@ -409,7 +435,7 @@ private function resolveFormFields(mixed $record = null, bool $isNested = false)
private function resolveCustomFields(): Collection
{
return collect(Fields::getFields())
->map(fn ($fieldClass) => new $fieldClass);
->mapWithKeys(fn ($fieldClass, $key) => [$key => $fieldClass]);
}

/**
Expand All @@ -426,26 +452,36 @@ private function resolveCustomFields(): Collection
*/
private function resolveFieldInput(Model $field, Collection $customFields, mixed $record = null, bool $isNested = false): ?object
{
$record = $record ?? $this->record;
$record = $record ?? (property_exists($this, 'record') ? $this->record : null);

if (! $record) {
return null;
}

$inputName = $this->generateInputName($field, $record, $isNested);

// Try to resolve from custom fields first (giving them priority)
if ($customField = $customFields->get($field->field_type)) {
return $customField::make($inputName, $field);
if ($customFieldClass = $customFields->get($field->field_type)) {
$input = $customFieldClass::make($inputName, $field);

return $input;
}

// Fall back to standard field type map if no custom field found
if ($fieldClass = self::FIELD_TYPE_MAP[$field->field_type] ?? null) {
return $fieldClass::make(name: $inputName, field: $field);
$input = $fieldClass::make(name: $inputName, field: $field);

return $input;
}

return null;
}

private function generateInputName(Model $field, mixed $record, bool $isNested): string
{
return $isNested ? "{$field->ulid}" : "{$record->valueColumn}.{$field->ulid}";
$name = $isNested ? "{$field->ulid}" : "{$record->valueColumn}.{$field->ulid}";

return $name;
}

/**
Expand Down Expand Up @@ -474,7 +510,11 @@ private function applyFieldSaveMutation(Model $field, array $fieldConfig, object
}

// Regular field processing
return $fieldInstance->mutateBeforeSaveCallback($this->record, $field, $data);
if (property_exists($this, 'record') && $this->record) {
return $fieldInstance->mutateBeforeSaveCallback($this->record, $field, $data);
}

return $data;
}

/**
Expand Down Expand Up @@ -532,18 +572,22 @@ private function processBuilderFieldMutation(Model $field, object $fieldInstance
$mockRecord = $this->createMockRecordForBuilder($block['data']);

// Create a temporary data structure for the callback
$tempData = [$this->record->valueColumn => $block['data']];
$tempData = $fieldInstance->mutateBeforeSaveCallback($mockRecord, $field, $tempData);
if (property_exists($this, 'record') && $this->record) {
$tempData = [$this->record->valueColumn => $block['data']];
$tempData = $fieldInstance->mutateBeforeSaveCallback($mockRecord, $field, $tempData);

if (isset($tempData[$this->record->valueColumn][$field->ulid])) {
$block['data'][$field->ulid] = $tempData[$this->record->valueColumn][$field->ulid];
if (isset($tempData[$this->record->valueColumn][$field->ulid])) {
$block['data'][$field->ulid] = $tempData[$this->record->valueColumn][$field->ulid];
}
}
}
}
}
}

$data[$this->record->valueColumn] = array_merge($data[$this->record->valueColumn], $builderBlocks);
if (property_exists($this, 'record') && $this->record) {
$data[$this->record->valueColumn] = array_merge($data[$this->record->valueColumn], $builderBlocks);
}

return $data;
}
Expand Down
Loading