Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 1 addition & 36 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,39 +251,4 @@ Fixes a typo in locking action visibility and adds a locked column to form resou
Schema::table('filament_forms', function (Blueprint $table) {
$table->boolean('locked')->default(false);
});



































```
```
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,30 @@ Key configuration options include:
- **Custom Page Classes**: Override the default `ShowForm` and `ShowEntry` pages for guest and app panels
- **Custom Middleware**: Override the default `SetFormPanel` middleware for panel context switching
- **Route URIs**: Customize the form and entry route paths (`filament-form-uri`, `filament-form-user-uri`)
- **Options Editor**: Choose TagsInput (`tags`, default) or KeyValue (`key_value`) for Select / Radio / Checkbox List options. Supports a global string, a per-type map, or a Closure (`options_editor`)

See `config/filament-form-builder.php` for all available configuration options.

#### Options editor examples

```php
// Global TagsInput (default — unchanged for existing apps)
'options_editor' => 'tags',

// Global KeyValue (value => label)
'options_editor' => 'key_value',

// Per field type (enum case names)
'options_editor' => [
'RADIO' => 'key_value',
'SELECT' => 'tags',
'default' => 'tags',
],

// Closure
'options_editor' => fn (?string $type): string => $type === 'RADIO' ? 'key_value' : 'tags',
```

### Private entries

You can restrict who can view or export form entries on a per-form basis. When **Private entries** is enabled for a form, the package uses your application’s policy to decide visibility.
Expand Down
31 changes: 31 additions & 0 deletions config/filament-form-builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,35 @@
|
*/
'user_model' => 'App\Models\User',

/*
|--------------------------------------------------------------------------
| Options Editor
|--------------------------------------------------------------------------
|
| How option lists are edited for Select, Select Multiple, Radio, and
| Checkbox List fields in the admin Fields relation manager.
|
| Default remains TagsInput (`tags`) so existing apps are unchanged.
| Use KeyValue (`key_value`) when options are stored as value => label
| maps (for example AI quizzes with keys a/b/c/d).
|
| Supported shapes:
| - string: 'tags' or 'key_value' for every options-capable type
| - array: per-type map (keys are FilamentFieldTypeEnum names), optional
| 'default' fallback
| - Closure: fn (?string $type): string
|
| Examples:
| 'options_editor' => 'tags',
| 'options_editor' => 'key_value',
| 'options_editor' => [
| 'RADIO' => 'key_value',
| 'SELECT' => 'tags',
| 'default' => 'tags',
| ],
| 'options_editor' => fn (?string $type): string => $type === 'RADIO' ? 'key_value' : 'tags',
|
*/
'options_editor' => 'tags',
];
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
use Filament\Actions\EditAction;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
Expand All @@ -24,6 +24,7 @@
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Tapp\FilamentFormBuilder\Enums\FilamentFieldTypeEnum;
use Tapp\FilamentFormBuilder\Support\OptionsEditor;

class FilamentFormFieldsRelationManager extends RelationManager
{
Expand All @@ -37,90 +38,85 @@ public static function getTitle(Model $ownerRecord, string $pageClass): string
public function form(Schema $schema): Schema
{
return $schema
->columns(2)
->components([
Select::make('type')
->options(function () {
return collect(FilamentFieldTypeEnum::cases())
->mapWithKeys(fn ($type) => [$type->name => $type->fieldName()])
->sortBy(fn ($label, $key) => $label)
->toArray();
})
->columnSpan(function ($state) {
if (! empty($state) && FilamentFieldTypeEnum::fromString($state)->hasOptions()) {
return 1;
}

return 2;
})
->required()
->live(),
Grid::make(3)
->schema([
Select::make('type')
->options(function () {
return collect(FilamentFieldTypeEnum::cases())
->mapWithKeys(fn ($type) => [$type->name => $type->fieldName()])
->sortBy(fn ($label, $key) => $label)
->toArray();
})
->required()
->live()
->columnSpan(1),
TextInput::make('order')
->default(function () {
return $this->getOwnerRecord()->filamentFormFields()->count() + 1;
})
->numeric()
->columnSpan(1),
Toggle::make('required')
->inline(false)
->visible(function (Get $get) {
return $get('type') !== FilamentFieldTypeEnum::REPEATER->name
&& $get('type') !== FilamentFieldTypeEnum::HEADING->name;
})
->columnSpan(1),
])
->columnSpanFull(),
Textarea::make('label')
->required()
->label(function (Get $get) {
return $get('type') === FilamentFieldTypeEnum::HEADING->name ? 'Heading' : 'Label';
}),
TagsInput::make('options')
->placeholder('Add options')
->hint('Press enter after inputting each option')
->visible(function (Get $get) {
if ($get('type')) {
return FilamentFieldTypeEnum::fromString($get('type'))->hasOptions();
}

return false;
}),
})
->columnSpanFull(),
self::optionsEditor(),
Textarea::make('hint')
->label(function (Get $get) {
return $get('type') === FilamentFieldTypeEnum::HEADING->name ? 'Subheading' : 'Hint';
}),
})
->columnSpanFull(),
// TagsInput::make('rules')
// ->placeholder('Add rules')
// ->hint('view list of available rules here, https://laravel.com/docs/11.x/validation#available-validation-rules')
// ->visible(function (Get $get) {
// return $get('type') !== FilamentFieldTypeEnum::REPEATER->name
// && $get('type') !== FilamentFieldTypeEnum::HEADING->name;
// }),
TextInput::make('order')
->default(function () {
return $this->getOwnerRecord()->filamentFormFields()->count() + 1;
})
->numeric(),
Toggle::make('required')
->visible(function (Get $get) {
return $get('type') !== FilamentFieldTypeEnum::REPEATER->name
&& $get('type') !== FilamentFieldTypeEnum::HEADING->name;
}),
Repeater::make('schema')
->label('Fields')
->schema([
Textarea::make('label')
->required(),
Select::make('type')
->options(function () {
$options = collect(FilamentFieldTypeEnum::cases())
->filter(fn ($type) => $type !== FilamentFieldTypeEnum::REPEATER)
->mapWithKeys(fn ($type) => [$type->name => $type->fieldName()])
->toArray();
Grid::make(2)
->schema([
Select::make('type')
->options(function () {
$options = collect(FilamentFieldTypeEnum::cases())
->filter(fn ($type) => $type !== FilamentFieldTypeEnum::REPEATER)
->mapWithKeys(fn ($type) => [$type->name => $type->fieldName()])
->toArray();

return $options;
})
return $options;
})
->required()
->live()
->columnSpan(1),
Toggle::make('required')
->inline(false)
->columnSpan(1),
])
->columnSpanFull(),
Textarea::make('label')
->required()
->live(),
TagsInput::make('options')
->placeholder('Add options')
->hint('Press enter after inputting each option')
->visible(function (Get $get) {
if ($get('type')) {
return FilamentFieldTypeEnum::fromString($get('type'))->hasOptions();
}

return false;
}),
Textarea::make('hint'),
->columnSpanFull(),
self::optionsEditor(),
Textarea::make('hint')
->columnSpanFull(),
// TagsInput::make('rules')
// ->placeholder('Add rules')
// ->hint('view list of available rules here, https://laravel.com/docs/11.x/validation#available-validation-rules'),
Toggle::make('required'),
])
->columns(2)
->columnSpanFull()
Expand All @@ -140,7 +136,17 @@ public function table(Table $table): Table
->modelLabel(config('filament-form-builder.admin-panel-filament-form-field-name'))
->reorderable('order')
->columns([
TextColumn::make('label'),
TextColumn::make('label')
->limit(60)
->tooltip(function (TextColumn $column): ?string {
$state = $column->getState();

if (! is_string($state) || strlen($state) <= $column->getCharacterLimit()) {
return null;
}

return $state;
}),
TextColumn::make('order')
->numeric()
->sortable(),
Expand Down Expand Up @@ -205,4 +211,11 @@ public function table(Table $table): Table
]),
]);
}

private static function optionsEditor(): Grid
{
return Grid::make(1)
->schema(fn (Get $get): array => OptionsEditor::components($get('type')))
->columnSpanFull();
}
}
20 changes: 11 additions & 9 deletions src/Models/Traits/BelongsToTenant.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,18 @@ public static function bootBelongsToTenant(): void
return;
}

// Register the dynamic relationship
// Register the dynamic relationship. The relation name must be passed explicitly:
// Eloquent otherwise guesses it from the backtrace, which resolves to this closure
// and breaks `associate()` and any later `refresh()` / `load()` on the model.
static::resolveRelationUsing(
static::getTenantRelationshipName(),
function ($model) {
return $model->belongsTo(config('filament-form-builder.tenancy.model'), static::getTenantColumnName());
return $model->belongsTo(
config('filament-form-builder.tenancy.model'),
static::getTenantColumnName(),
null,
static::getTenantRelationshipName(),
);
}
);

Expand All @@ -34,14 +41,12 @@ function ($model) {
return;
}

$tenantRelationshipName = static::getTenantRelationshipName();

// Try to get tenant from Filament context (Filament's standard method)
// This handles top-level resources created outside Filament's Resource observers
if (class_exists(Filament::class)) {
$tenant = Filament::getTenant();
if ($tenant) {
$model->{$tenantRelationshipName}()->associate($tenant);
$model->{$tenantColumnName} = $tenant->getKey();

return;
}
Expand All @@ -55,10 +60,7 @@ function ($model) {
$parentForm = $parentFormClass::find($parentFormId);

if ($parentForm) {
$parentTenant = $parentForm->{$tenantRelationshipName};
if ($parentTenant) {
$model->{$tenantRelationshipName}()->associate($parentTenant);
}
$model->{$tenantColumnName} = $parentForm->{$tenantColumnName};
}
}
});
Expand Down
Loading