[增添]添加了datasource的setting数据库以及默认值

This commit is contained in:
makotocc0107
2024-08-27 09:57:44 +08:00
parent d111dfaea4
commit 72eb990970
10955 changed files with 978898 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
<?php
namespace Filament\Forms\Commands\Aliases;
use Filament\Forms\Commands;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'forms:field')]
class MakeFieldCommand extends Commands\MakeFieldCommand
{
protected $hidden = true;
protected $signature = 'forms:field {name} {--F|force}';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Filament\Forms\Commands\Aliases;
use Filament\Forms\Commands;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'forms:layout')]
class MakeLayoutComponentCommand extends Commands\MakeLayoutComponentCommand
{
protected $hidden = true;
protected $signature = 'forms:layout {name} {--F|force}';
}

View File

@@ -0,0 +1,182 @@
<?php
namespace Filament\Forms\Commands\Concerns;
use Filament\Forms;
use Illuminate\Support\Str;
trait CanGenerateForms
{
protected function getResourceFormSchema(string $model): string
{
$model = $this->getModel($model);
if (blank($model)) {
return '//';
}
$schema = $this->getModelSchema($model);
$table = $this->getModelTable($model);
$components = [];
foreach ($schema->getColumns($table) as $column) {
if ($column['auto_increment']) {
continue;
}
$columnName = $column['name'];
if (str($columnName)->is([
app($model)->getKeyName(),
'created_at',
'deleted_at',
'updated_at',
'*_token',
])) {
continue;
}
$type = $this->parseColumnType($column);
$componentData = [];
$componentData['type'] = match (true) {
$type['name'] === 'boolean' => Forms\Components\Toggle::class,
$type['name'] === 'date' => Forms\Components\DatePicker::class,
in_array($type['name'], ['datetime', 'timestamp']) => Forms\Components\DateTimePicker::class,
$type['name'] === 'text' => Forms\Components\Textarea::class,
$columnName === 'image', str($columnName)->startsWith('image_'), str($columnName)->contains('_image_'), str($columnName)->endsWith('_image') => Forms\Components\FileUpload::class,
default => Forms\Components\TextInput::class,
};
if (str($columnName)->endsWith('_id')) {
$guessedRelationshipName = $this->guessBelongsToRelationshipName($columnName, $model);
if (filled($guessedRelationshipName)) {
$guessedRelationshipTitleColumnName = $this->guessBelongsToRelationshipTitleColumnName($columnName, app($model)->{$guessedRelationshipName}()->getModel()::class);
$componentData['type'] = Forms\Components\Select::class;
$componentData['relationship'] = [$guessedRelationshipName, $guessedRelationshipTitleColumnName];
}
}
if (in_array($columnName, [
'id',
'sku',
'uuid',
])) {
$componentData['label'] = [Str::upper($columnName)];
}
if ($componentData['type'] === Forms\Components\TextInput::class) {
if (str($columnName)->contains(['email'])) {
$componentData['email'] = [];
}
if (str($columnName)->contains(['password'])) {
$componentData['password'] = [];
}
if (str($columnName)->contains(['phone', 'tel'])) {
$componentData['tel'] = [];
}
}
if ($componentData['type'] === Forms\Components\FileUpload::class) {
$componentData['image'] = [];
}
if (! $column['nullable']) {
$componentData['required'] = [];
}
if (in_array($type['name'], [
'integer',
'decimal',
'float',
'double',
'money',
])) {
if ($componentData['type'] === Forms\Components\TextInput::class) {
$componentData['numeric'] = [];
}
if (filled($column['default'])) {
$componentData['default'] = [$this->parseDefaultExpression($column, $model)];
}
if (in_array($columnName, [
'cost',
'money',
'price',
]) || $type['name'] === 'money') {
$componentData['prefix'] = ['$'];
}
} elseif (in_array($componentData['type'], [
Forms\Components\TextInput::class,
Forms\Components\Textarea::class,
]) && isset($type['length'])) {
$componentData['maxLength'] = [$type['length']];
if (filled($column['default'])) {
$componentData['default'] = [$this->parseDefaultExpression($column, $model)];
}
}
if ($componentData['type'] === Forms\Components\Textarea::class) {
$componentData['columnSpanFull'] = [];
}
$components[$columnName] = $componentData;
}
$output = count($components) ? '' : '//';
foreach ($components as $componentName => $componentData) {
// Constructor
$output .= (string) str($componentData['type'])->after('Filament\\');
$output .= '::make(\'';
$output .= $componentName;
$output .= '\')';
unset($componentData['type']);
// Configuration
foreach ($componentData as $methodName => $parameters) {
$output .= PHP_EOL;
$output .= ' ->';
$output .= $methodName;
$output .= '(';
$output .= collect($parameters)
->map(function (mixed $parameterValue, int | string $parameterName): string {
$parameterValue = match (true) {
/** @phpstan-ignore-next-line */
is_bool($parameterValue) => $parameterValue ? 'true' : 'false',
/** @phpstan-ignore-next-line */
is_null($parameterValue) => 'null',
is_numeric($parameterValue) => $parameterValue,
default => "'{$parameterValue}'",
};
if (is_numeric($parameterName)) {
return $parameterValue;
}
return "{$parameterName}: {$parameterValue}";
})
->implode(', ');
$output .= ')';
}
// Termination
$output .= ',';
if (! (array_key_last($components) === $componentName)) {
$output .= PHP_EOL;
}
}
return $output;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Filament\Forms\Commands;
use Filament\Support\Commands\Concerns\CanManipulateFiles;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
use function Laravel\Prompts\text;
#[AsCommand(name: 'make:form-field')]
class MakeFieldCommand extends Command
{
use CanManipulateFiles;
protected $description = 'Create a new form field class and view';
protected $signature = 'make:form-field {name?} {--F|force}';
public function handle(): int
{
$field = (string) str($this->argument('name') ?? text(
label: 'What is the field name?',
placeholder: 'RangeSlider',
required: true,
))
->trim('/')
->trim('\\')
->trim(' ')
->replace('/', '\\');
$fieldClass = (string) str($field)->afterLast('\\');
$fieldNamespace = str($field)->contains('\\') ?
(string) str($field)->beforeLast('\\') :
'';
$view = str($field)
->prepend('forms\\components\\')
->explode('\\')
->map(static fn ($segment) => Str::kebab($segment))
->implode('.');
$path = app_path(
(string) str($field)
->prepend('Forms\\Components\\')
->replace('\\', '/')
->append('.php'),
);
$viewPath = resource_path(
(string) str($view)
->replace('.', '/')
->prepend('views/')
->append('.blade.php'),
);
if (! $this->option('force') && $this->checkForCollision([
$path,
])) {
return static::INVALID;
}
$this->copyStubToApp('Field', $path, [
'class' => $fieldClass,
'namespace' => 'App\\Forms\\Components' . ($fieldNamespace !== '' ? "\\{$fieldNamespace}" : ''),
'view' => $view,
]);
if (! $this->fileExists($viewPath)) {
$this->copyStubToApp('FieldView', $viewPath);
}
$this->components->info("Filament form field [{$path}] created successfully.");
return static::SUCCESS;
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Filament\Forms\Commands;
use Filament\Forms\Commands\Concerns\CanGenerateForms;
use Filament\Support\Commands\Concerns\CanIndentStrings;
use Filament\Support\Commands\Concerns\CanManipulateFiles;
use Filament\Support\Commands\Concerns\CanReadModelSchemas;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
use function Laravel\Prompts\select;
use function Laravel\Prompts\text;
#[AsCommand(name: 'make:livewire-form')]
class MakeFormCommand extends Command
{
use CanGenerateForms;
use CanIndentStrings;
use CanManipulateFiles;
use CanReadModelSchemas;
protected $description = 'Create a new Livewire component containing a Filament form';
protected $signature = 'make:livewire-form {name?} {model?} {--E|edit} {--G|generate} {--F|force}';
public function handle(): int
{
$component = (string) str($this->argument('name') ?? text(
label: 'What is the form name?',
placeholder: 'Products/CreateProduct',
required: true,
))
->trim('/')
->trim('\\')
->trim(' ')
->replace('/', '\\');
$componentClass = (string) str($component)->afterLast('\\');
$componentNamespace = str($component)->contains('\\') ?
(string) str($component)->beforeLast('\\') :
'';
$view = str($component)
->replace('\\', '/')
->prepend('Livewire/')
->explode('/')
->map(fn ($segment) => Str::lower(Str::kebab($segment)))
->implode('.');
$model = (string) str($this->argument('model') ??
text(
label: 'What is the model name?',
placeholder: 'Product',
required: $this->option('edit')
))->replace('/', '\\');
$modelClass = (string) str($model)->afterLast('\\');
if ($this->option('edit')) {
$isEditForm = true;
} elseif (filled($model)) {
$isEditForm = select(
label: 'Which namespace would you like to create this in?',
options: [
'Create',
'Edit',
]
) === 'Edit';
} else {
$isEditForm = false;
}
$path = (string) str($component)
->prepend('/')
->prepend(app_path('Livewire/'))
->replace('\\', '/')
->replace('//', '/')
->append('.php');
$viewPath = resource_path(
(string) str($view)
->replace('.', '/')
->prepend('views/')
->append('.blade.php'),
);
if (! $this->option('force') && $this->checkForCollision([$path, $viewPath])) {
return static::INVALID;
}
$this->copyStubToApp(filled($model) ? ($isEditForm ? 'EditForm' : 'CreateForm') : 'Form', $path, [
'class' => $componentClass,
'model' => $model,
'modelClass' => $modelClass,
'namespace' => 'App\\Livewire' . ($componentNamespace !== '' ? "\\{$componentNamespace}" : ''),
'schema' => $this->indentString((filled($model) && $this->option('generate')) ? $this->getResourceFormSchema(
'App\\Models\\' . $model,
) : '//', 4),
'view' => $view,
]);
$this->copyStubToApp('FormView', $viewPath, [
'submitAction' => filled($model) ? ($isEditForm ? 'save' : 'create') : 'submit',
]);
$this->components->info("Filament form [{$path}] created successfully.");
return static::SUCCESS;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Filament\Forms\Commands;
use Filament\Support\Commands\Concerns\CanManipulateFiles;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
use function Laravel\Prompts\text;
#[AsCommand(name: 'make:form-layout')]
class MakeLayoutComponentCommand extends Command
{
use CanManipulateFiles;
protected $description = 'Create a new form layout component class and view';
protected $signature = 'make:form-layout {name?} {--F|force}';
public function handle(): int
{
$component = (string) str($this->argument('name') ?? text(
label: 'What is the layout name?',
placeholder: 'Wizard',
required: true,
))
->trim('/')
->trim('\\')
->trim(' ')
->replace('/', '\\');
$componentClass = (string) str($component)->afterLast('\\');
$componentNamespace = str($component)->contains('\\') ?
(string) str($component)->beforeLast('\\') :
'';
$view = str($component)
->prepend('forms\\components\\')
->explode('\\')
->map(fn ($segment) => Str::kebab($segment))
->implode('.');
$path = app_path(
(string) str($component)
->prepend('Forms\\Components\\')
->replace('\\', '/')
->append('.php'),
);
$viewPath = resource_path(
(string) str($view)
->replace('.', '/')
->prepend('views/')
->append('.blade.php'),
);
if (! $this->option('force') && $this->checkForCollision([
$path,
])) {
return static::INVALID;
}
$this->copyStubToApp('LayoutComponent', $path, [
'class' => $componentClass,
'namespace' => 'App\\Forms\\Components' . ($componentNamespace !== '' ? "\\{$componentNamespace}" : ''),
'view' => $view,
]);
if (! $this->fileExists($viewPath)) {
$this->copyStubToApp('LayoutComponentView', $viewPath);
}
$this->components->info("Filament form layout component [{$path}] created successfully.");
return static::SUCCESS;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Filament\Forms;
use Filament\Forms\Contracts\HasForms;
use Filament\Support\Components\ViewComponent;
use Filament\Support\Concerns\HasExtraAttributes;
use Illuminate\Database\Eloquent\Model;
class ComponentContainer extends ViewComponent
{
use Concerns\BelongsToLivewire;
use Concerns\BelongsToModel;
use Concerns\BelongsToParentComponent;
use Concerns\CanBeDisabled;
use Concerns\CanBeHidden;
use Concerns\CanBeValidated;
use Concerns\Cloneable;
use Concerns\HasColumns;
use Concerns\HasComponents;
use Concerns\HasFieldWrapper;
use Concerns\HasInlineLabels;
use Concerns\HasOperation;
use Concerns\HasState;
use Concerns\HasStateBindingModifiers;
use Concerns\ListensToEvents;
use Concerns\SupportsComponentFileAttachments;
use Concerns\SupportsFileUploadFields;
use Concerns\SupportsSelectFields;
use HasExtraAttributes;
protected string $view = 'filament-forms::component-container';
protected string $evaluationIdentifier = 'container';
protected string $viewIdentifier = 'container';
final public function __construct(HasForms $livewire)
{
$this->livewire($livewire);
}
public static function make(HasForms $livewire): static
{
$static = app(static::class, ['livewire' => $livewire]);
$static->configure();
return $static;
}
/**
* @return array<mixed>
*/
protected function resolveDefaultClosureDependencyForEvaluationByName(string $parameterName): array
{
return match ($parameterName) {
'livewire' => [$this->getLivewire()],
'model' => [$this->getModel()],
'record' => [$this->getRecord()],
default => parent::resolveDefaultClosureDependencyForEvaluationByName($parameterName),
};
}
/**
* @return array<mixed>
*/
protected function resolveDefaultClosureDependencyForEvaluationByType(string $parameterType): array
{
$record = $this->getRecord();
if (! $record) {
return parent::resolveDefaultClosureDependencyForEvaluationByType($parameterType);
}
return match ($parameterType) {
Model::class, $record::class => [$record],
default => parent::resolveDefaultClosureDependencyForEvaluationByType($parameterType),
};
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Filament\Forms\Components;
use Closure;
use Filament\Forms\Components\Actions\Action;
use Filament\Support\Concerns\HasAlignment;
use Filament\Support\Concerns\HasVerticalAlignment;
class Actions extends Component
{
use HasAlignment;
use HasVerticalAlignment;
protected string $view = 'filament-forms::components.actions';
protected bool | Closure $isFullWidth = false;
/**
* @param array<Action> $actions
*/
final public function __construct(array $actions)
{
$this->actions($actions);
}
/**
* @param array<Action> $actions
*/
public static function make(array $actions): static
{
$static = app(static::class, ['actions' => $actions]);
$static->configure();
return $static;
}
/**
* @param array<Action> $actions
*/
public function actions(array $actions): static
{
$this->childComponents(array_map(
fn (Action $action): Component => $action->toFormComponent(),
$actions,
));
return $this;
}
public function fullWidth(bool | Closure $isFullWidth = true): static
{
$this->isFullWidth = $isFullWidth;
return $this;
}
public function isFullWidth(): bool
{
return (bool) $this->evaluate($this->isFullWidth);
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Filament\Forms\Components\Actions;
use Exception;
use Filament\Actions\Concerns\HasMountableArguments;
use Filament\Actions\MountableAction;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Js;
class Action extends MountableAction
{
use Concerns\BelongsToComponent;
use HasMountableArguments;
public function getLivewireCallMountedActionName(): string
{
return 'callMountedFormComponentAction';
}
public function getLivewireClickHandler(): ?string
{
if (! $this->isLivewireClickHandlerEnabled()) {
return null;
}
if (is_string($this->action)) {
return $this->action;
}
if ($event = $this->getLivewireEventClickHandler()) {
return $event;
}
$argumentsParameter = '';
if (count($arguments = $this->getArguments())) {
$argumentsParameter .= ', ';
$argumentsParameter .= Js::from($arguments);
$argumentsParameter .= '';
}
$componentKey = $this->getComponent()->getKey();
if (blank($componentKey)) {
$componentClass = $this->getComponent()::class;
throw new Exception("The form component [{$componentClass}] must have a [key()] set in order to use actions. This [key()] must be a unique identifier for the component.");
}
return "mountFormComponentAction('{$componentKey}', '{$this->getName()}'{$argumentsParameter})";
}
public function toFormComponent(): ActionContainer
{
$component = ActionContainer::make($this);
$this->component($component);
return $component;
}
/**
* @return array<mixed>
*/
protected function resolveDefaultClosureDependencyForEvaluationByName(string $parameterName): array
{
return match ($parameterName) {
'component' => [$this->getComponent()],
'context', 'operation' => [$this->getComponent()->getContainer()->getOperation()],
'get' => [$this->getComponent()->getGetCallback()],
'model' => [$this->getComponent()->getModel()],
'record' => [$this->getComponent()->getRecord()],
'set' => [$this->getComponent()->getSetCallback()],
'state' => [$this->getComponent()->getState()],
default => parent::resolveDefaultClosureDependencyForEvaluationByName($parameterName),
};
}
/**
* @return array<mixed>
*/
protected function resolveDefaultClosureDependencyForEvaluationByType(string $parameterType): array
{
$record = $this->getComponent()->getRecord();
if (! $record) {
return parent::resolveDefaultClosureDependencyForEvaluationByType($parameterType);
}
return match ($parameterType) {
Model::class, $record::class => [$record],
default => parent::resolveDefaultClosureDependencyForEvaluationByType($parameterType),
};
}
public function getInfolistName(): string
{
return 'mountedFormComponentActionInfolist';
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Filament\Forms\Components\Actions;
use Filament\Forms\Components\Component;
class ActionContainer extends Component
{
protected string $view = 'filament-forms::components.actions.action-container';
protected Action $action;
final public function __construct(Action $action)
{
$this->action = $action;
$this->registerActions([$action]);
}
public static function make(Action $action): static
{
$static = app(static::class, ['action' => $action]);
$static->configure();
return $static;
}
public function getKey(): string
{
return parent::getKey() ?? "{$this->getStatePath()}.{$this->action->getName()}Action";
}
public function isHidden(): bool
{
return $this->action->isHidden();
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Filament\Forms\Components\Actions\Concerns;
use Filament\Forms\Components\Component;
use Filament\Forms\Contracts\HasForms;
trait BelongsToComponent
{
protected Component $component;
public function component(Component $component): static
{
$this->component = $component;
return $this;
}
public function getComponent(): Component
{
return $this->component;
}
public function getLivewire(): HasForms
{
return $this->getComponent()->getLivewire();
}
}

View File

@@ -0,0 +1,871 @@
<?php
namespace Filament\Forms\Components;
use Closure;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use League\Flysystem\UnableToCheckFileExistence;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Throwable;
class BaseFileUpload extends Field implements Contracts\HasNestedRecursiveValidationRules
{
use Concerns\HasNestedRecursiveValidationRules;
use Concerns\HasUploadingMessage;
/**
* @var array<string> | Arrayable | Closure | null
*/
protected array | Arrayable | Closure | null $acceptedFileTypes = null;
protected bool | Closure $isDeletable = true;
protected bool | Closure $isDownloadable = false;
protected bool | Closure $isOpenable = false;
protected bool | Closure $isPreviewable = true;
protected bool | Closure $isReorderable = false;
protected string | Closure | null $directory = null;
protected string | Closure | null $diskName = null;
protected bool | Closure $isMultiple = false;
protected int | Closure | null $maxSize = null;
protected int | Closure | null $minSize = null;
protected int | Closure | null $maxFiles = null;
protected int | Closure | null $minFiles = null;
protected bool | Closure $shouldPreserveFilenames = false;
protected bool | Closure $shouldMoveFiles = false;
protected bool | Closure $shouldStoreFiles = true;
protected bool | Closure $shouldFetchFileInformation = true;
protected string | Closure | null $fileNamesStatePath = null;
protected string | Closure $visibility = 'public';
protected ?Closure $deleteUploadedFileUsing = null;
protected ?Closure $getUploadedFileNameForStorageUsing = null;
protected ?Closure $getUploadedFileUsing = null;
protected ?Closure $reorderUploadedFilesUsing = null;
protected ?Closure $saveUploadedFileUsing = null;
protected function setUp(): void
{
parent::setUp();
$this->afterStateHydrated(static function (BaseFileUpload $component, string | array | null $state): void {
if (blank($state)) {
$component->state([]);
return;
}
$shouldFetchFileInformation = $component->shouldFetchFileInformation();
$files = collect(Arr::wrap($state))
->filter(static function (string $file) use ($component, $shouldFetchFileInformation): bool {
if (blank($file)) {
return false;
}
if (! $shouldFetchFileInformation) {
return true;
}
try {
return $component->getDisk()->exists($file);
} catch (UnableToCheckFileExistence $exception) {
return false;
}
})
->mapWithKeys(static fn (string $file): array => [((string) Str::uuid()) => $file])
->all();
$component->state($files);
});
$this->afterStateUpdated(static function (BaseFileUpload $component, $state) {
if ($state instanceof TemporaryUploadedFile) {
return;
}
if (blank($state)) {
return;
}
if (is_array($state)) {
return;
}
$component->state([(string) Str::uuid() => $state]);
});
$this->beforeStateDehydrated(static function (BaseFileUpload $component): void {
$component->saveUploadedFiles();
});
$this->dehydrateStateUsing(static function (BaseFileUpload $component, ?array $state): string | array | null | TemporaryUploadedFile {
$files = array_values($state ?? []);
if ($component->isMultiple()) {
return $files;
}
return $files[0] ?? null;
});
$this->getUploadedFileUsing(static function (BaseFileUpload $component, string $file, string | array | null $storedFileNames): ?array {
/** @var FilesystemAdapter $storage */
$storage = $component->getDisk();
$shouldFetchFileInformation = $component->shouldFetchFileInformation();
if ($shouldFetchFileInformation) {
try {
if (! $storage->exists($file)) {
return null;
}
} catch (UnableToCheckFileExistence $exception) {
return null;
}
}
$url = null;
if ($component->getVisibility() === 'private') {
try {
$url = $storage->temporaryUrl(
$file,
now()->addMinutes(5),
);
} catch (Throwable $exception) {
// This driver does not support creating temporary URLs.
}
}
$url ??= $storage->url($file);
return [
'name' => ($component->isMultiple() ? ($storedFileNames[$file] ?? null) : $storedFileNames) ?? basename($file),
'size' => $shouldFetchFileInformation ? $storage->size($file) : 0,
'type' => $shouldFetchFileInformation ? $storage->mimeType($file) : null,
'url' => $url,
];
});
$this->getUploadedFileNameForStorageUsing(static function (BaseFileUpload $component, TemporaryUploadedFile $file) {
return $component->shouldPreserveFilenames() ? $file->getClientOriginalName() : (Str::ulid() . '.' . $file->getClientOriginalExtension());
});
$this->saveUploadedFileUsing(static function (BaseFileUpload $component, TemporaryUploadedFile $file): ?string {
try {
if (! $file->exists()) {
return null;
}
} catch (UnableToCheckFileExistence $exception) {
return null;
}
if (
$component->shouldMoveFiles() &&
($component->getDiskName() == (fn (): string => $this->disk)->call($file))
) {
$newPath = trim($component->getDirectory() . '/' . $component->getUploadedFileNameForStorage($file), '/');
$component->getDisk()->move((fn (): string => $this->path)->call($file), $newPath);
return $newPath;
}
$storeMethod = $component->getVisibility() === 'public' ? 'storePubliclyAs' : 'storeAs';
return $file->{$storeMethod}(
$component->getDirectory(),
$component->getUploadedFileNameForStorage($file),
$component->getDiskName(),
);
});
}
protected function callAfterStateUpdatedHook(Closure $hook): void
{
/** @var array<string | TemporaryUploadedFile> $state */
$state = $this->getState() ?? [];
/** @var array<string | TemporaryUploadedFile> $oldState */
$oldState = $this->getOldState() ?? [];
$this->evaluate($hook, [
'state' => $this->isMultiple() ? $state : Arr::first($state),
'old' => $this->isMultiple() ? $oldState : Arr::first($oldState),
]);
}
/**
* @param array<string> | Arrayable | Closure $types
*/
public function acceptedFileTypes(array | Arrayable | Closure $types): static
{
$this->acceptedFileTypes = $types;
$this->rule(static function (BaseFileUpload $component) {
$types = implode(',', ($component->getAcceptedFileTypes() ?? []));
return "mimetypes:{$types}";
});
return $this;
}
public function deletable(bool | Closure $condition = true): static
{
$this->isDeletable = $condition;
return $this;
}
public function directory(string | Closure | null $directory): static
{
$this->directory = $directory;
return $this;
}
public function disk(string | Closure | null $name): static
{
$this->diskName = $name;
return $this;
}
public function downloadable(bool | Closure $condition = true): static
{
$this->isDownloadable = $condition;
return $this;
}
public function openable(bool | Closure $condition = true): static
{
$this->isOpenable = $condition;
return $this;
}
public function reorderable(bool | Closure $condition = true): static
{
$this->isReorderable = $condition;
return $this;
}
public function previewable(bool | Closure $condition = true): static
{
$this->isPreviewable = $condition;
return $this;
}
/**
* @deprecated Use `downloadable()` instead.
*/
public function enableDownload(bool | Closure $condition = true): static
{
$this->downloadable($condition);
return $this;
}
/**
* @deprecated Use `openable()` instead.
*/
public function enableOpen(bool | Closure $condition = true): static
{
$this->openable($condition);
return $this;
}
/**
* @deprecated Use `reorderable()` instead.
*/
public function enableReordering(bool | Closure $condition = true): static
{
$this->reorderable($condition);
return $this;
}
/**
* @deprecated Use `previewable()` instead.
*/
public function disablePreview(bool | Closure $condition = true): static
{
$this->previewable(fn (BaseFileUpload $component): bool => ! $component->evaluate($condition));
return $this;
}
public function storeFileNamesIn(string | Closure | null $statePath): static
{
$this->fileNamesStatePath = $statePath;
return $this;
}
public function preserveFilenames(bool | Closure $condition = true): static
{
$this->shouldPreserveFilenames = $condition;
return $this;
}
public function moveFiles(bool | Closure $condition = true): static
{
$this->shouldMoveFiles = $condition;
return $this;
}
/**
* @deprecated Use `moveFiles()` instead.
*/
public function moveFile(bool | Closure $condition = true): static
{
$this->moveFiles($condition);
return $this;
}
public function fetchFileInformation(bool | Closure $condition = true): static
{
$this->shouldFetchFileInformation = $condition;
return $this;
}
public function maxSize(int | Closure | null $size): static
{
$this->maxSize = $size;
$this->rule(static function (BaseFileUpload $component): string {
$size = $component->getMaxSize();
return "max:{$size}";
});
return $this;
}
public function minSize(int | Closure | null $size): static
{
$this->minSize = $size;
$this->rule(static function (BaseFileUpload $component): string {
$size = $component->getMinSize();
return "min:{$size}";
});
return $this;
}
public function maxFiles(int | Closure | null $count): static
{
$this->maxFiles = $count;
return $this;
}
public function minFiles(int | Closure | null $count): static
{
$this->minFiles = $count;
return $this;
}
public function multiple(bool | Closure $condition = true): static
{
$this->isMultiple = $condition;
return $this;
}
public function storeFiles(bool | Closure $condition = true): static
{
$this->shouldStoreFiles = $condition;
return $this;
}
/**
* @deprecated Use `storeFiles()` instead.
*/
public function storeFile(bool | Closure $condition = true): static
{
$this->storeFiles($condition);
return $this;
}
public function visibility(string | Closure | null $visibility): static
{
$this->visibility = $visibility;
return $this;
}
public function deleteUploadedFileUsing(?Closure $callback): static
{
$this->deleteUploadedFileUsing = $callback;
return $this;
}
public function getUploadedFileUsing(?Closure $callback): static
{
$this->getUploadedFileUsing = $callback;
return $this;
}
public function reorderUploadedFilesUsing(?Closure $callback): static
{
$this->reorderUploadedFilesUsing = $callback;
return $this;
}
public function saveUploadedFileUsing(?Closure $callback): static
{
$this->saveUploadedFileUsing = $callback;
return $this;
}
public function isDeletable(): bool
{
return (bool) $this->evaluate($this->isDeletable);
}
public function isDownloadable(): bool
{
return (bool) $this->evaluate($this->isDownloadable);
}
public function isOpenable(): bool
{
return (bool) $this->evaluate($this->isOpenable);
}
public function isPreviewable(): bool
{
return (bool) $this->evaluate($this->isPreviewable);
}
public function isReorderable(): bool
{
return (bool) $this->evaluate($this->isReorderable);
}
/**
* @return array<string> | null
*/
public function getAcceptedFileTypes(): ?array
{
$types = $this->evaluate($this->acceptedFileTypes);
if ($types instanceof Arrayable) {
$types = $types->toArray();
}
return $types;
}
public function getDirectory(): ?string
{
return $this->evaluate($this->directory);
}
public function getDisk(): Filesystem
{
return Storage::disk($this->getDiskName());
}
public function getDiskName(): string
{
return $this->evaluate($this->diskName) ?? config('filament.default_filesystem_disk');
}
public function getMaxFiles(): ?int
{
return $this->evaluate($this->maxFiles);
}
public function getMinFiles(): ?int
{
return $this->evaluate($this->minFiles);
}
public function getMaxSize(): ?int
{
return $this->evaluate($this->maxSize);
}
public function getMinSize(): ?int
{
return $this->evaluate($this->minSize);
}
public function getVisibility(): string
{
return $this->evaluate($this->visibility);
}
public function shouldPreserveFilenames(): bool
{
return (bool) $this->evaluate($this->shouldPreserveFilenames);
}
public function shouldMoveFiles(): bool
{
return (bool) $this->evaluate($this->shouldMoveFiles);
}
public function shouldFetchFileInformation(): bool
{
return (bool) $this->evaluate($this->shouldFetchFileInformation);
}
public function shouldStoreFiles(): bool
{
return (bool) $this->evaluate($this->shouldStoreFiles);
}
public function getFileNamesStatePath(): ?string
{
if (! $this->fileNamesStatePath) {
return null;
}
return $this->generateRelativeStatePath($this->fileNamesStatePath);
}
/**
* @return array<mixed>
*/
public function getValidationRules(): array
{
$rules = [
$this->getRequiredValidationRule(),
'array',
];
if (filled($count = $this->getMaxFiles())) {
$rules[] = "max:{$count}";
}
if (filled($count = $this->getMinFiles())) {
$rules[] = "min:{$count}";
}
$rules[] = function (string $attribute, array $value, Closure $fail): void {
$files = array_filter($value, fn (TemporaryUploadedFile | string $file): bool => $file instanceof TemporaryUploadedFile);
$name = $this->getName();
$validationMessages = $this->getValidationMessages();
$validator = Validator::make(
[$name => $files],
["{$name}.*" => ['file', ...parent::getValidationRules()]],
$validationMessages ? ["{$name}.*" => $validationMessages] : [],
["{$name}.*" => $this->getValidationAttribute()],
);
if (! $validator->fails()) {
return;
}
$fail($validator->errors()->first());
};
return $rules;
}
public function deleteUploadedFile(string $fileKey): static
{
$file = $this->removeUploadedFile($fileKey);
if (blank($file)) {
return $this;
}
$callback = $this->deleteUploadedFileUsing;
if (! $callback) {
return $this;
}
$this->evaluate($callback, [
'file' => $file,
]);
return $this;
}
public function removeUploadedFile(string $fileKey): string | TemporaryUploadedFile | null
{
$files = $this->getState();
$file = $files[$fileKey] ?? null;
if (! $file) {
return null;
}
if (is_string($file)) {
$this->removeStoredFileName($file);
} elseif ($file instanceof TemporaryUploadedFile) {
$file->delete();
}
unset($files[$fileKey]);
$this->state($files);
return $file;
}
public function removeStoredFileName(string $file): void
{
$statePath = $this->fileNamesStatePath;
if (blank($statePath)) {
return;
}
$this->evaluate(function (BaseFileUpload $component, Get $get, Set $set) use ($file, $statePath) {
if (! $component->isMultiple()) {
$set($statePath, null);
return;
}
$fileNames = $get($statePath) ?? [];
if (array_key_exists($file, $fileNames)) {
unset($fileNames[$file]);
}
$set($statePath, $fileNames);
});
}
/**
* @param array<array-key> $fileKeys
*/
public function reorderUploadedFiles(array $fileKeys): void
{
if (! $this->isReorderable) {
return;
}
$fileKeys = array_flip($fileKeys);
$state = collect($this->getState())
->sortBy(static fn ($file, $fileKey) => $fileKeys[$fileKey] ?? null) // $fileKey may not be present in $fileKeys if it was added to the state during the reorder call
->all();
$this->state($state);
}
/**
* @return array<array{name: string, size: int, type: string, url: string} | null> | null
*/
public function getUploadedFiles(): ?array
{
$urls = [];
foreach ($this->getState() ?? [] as $fileKey => $file) {
if ($file instanceof TemporaryUploadedFile) {
$urls[$fileKey] = null;
continue;
}
$callback = $this->getUploadedFileUsing;
if (! $callback) {
return [$fileKey => null];
}
$urls[$fileKey] = $this->evaluate($callback, [
'file' => $file,
'storedFileNames' => $this->getStoredFileNames(),
]) ?: null;
}
return $urls;
}
public function saveUploadedFiles(): void
{
if (blank($this->getState())) {
$this->state([]);
return;
}
if (! $this->shouldStoreFiles()) {
return;
}
$state = array_filter(array_map(function (TemporaryUploadedFile | string $file) {
if (! $file instanceof TemporaryUploadedFile) {
return $file;
}
$callback = $this->saveUploadedFileUsing;
if (! $callback) {
$file->delete();
return $file;
}
$storedFile = $this->evaluate($callback, [
'file' => $file,
]);
if ($storedFile === null) {
return null;
}
$this->storeFileName($storedFile, $file->getClientOriginalName());
$file->delete();
return $storedFile;
}, Arr::wrap($this->getState())));
if ($this->isReorderable && ($callback = $this->reorderUploadedFilesUsing)) {
$state = $this->evaluate($callback, [
'state' => $state,
]);
}
$this->state($state);
}
public function storeFileName(string $file, string $fileName): void
{
$statePath = $this->fileNamesStatePath;
if (blank($statePath)) {
return;
}
$this->evaluate(function (BaseFileUpload $component, Get $get, Set $set) use ($file, $fileName, $statePath) {
if (! $component->isMultiple()) {
$set($statePath, $fileName);
return;
}
$fileNames = $get($statePath) ?? [];
$fileNames[$file] = $fileName;
$set($statePath, $fileNames);
});
}
/**
* @return string | array<string, string> | null
*/
public function getStoredFileNames(): string | array | null
{
$state = null;
$statePath = $this->fileNamesStatePath;
if (filled($statePath)) {
$state = $this->evaluate(fn (Get $get) => $get($statePath));
}
if (blank($state) && $this->isMultiple()) {
return [];
}
return $state;
}
public function isMultiple(): bool
{
return (bool) $this->evaluate($this->isMultiple);
}
public function getUploadedFileNameForStorageUsing(?Closure $callback): static
{
$this->getUploadedFileNameForStorageUsing = $callback;
return $this;
}
public function getUploadedFileNameForStorage(TemporaryUploadedFile $file): string
{
return $this->evaluate($this->getUploadedFileNameForStorageUsing, [
'file' => $file,
]);
}
/**
* @return array<string, string>
*/
public function getStateToDehydrate(): array
{
$state = parent::getStateToDehydrate();
if ($fileNamesStatePath = $this->getFileNamesStatePath()) {
$state = [
...$state,
$fileNamesStatePath => $this->getStoredFileNames(),
];
}
return $state;
}
/**
* @param array<string, array<mixed>> $rules
*/
public function dehydrateValidationRules(array &$rules): void
{
parent::dehydrateValidationRules($rules);
if ($fileNamesStatePath = $this->getFileNamesStatePath()) {
$rules[$fileNamesStatePath] = ['nullable'];
}
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Filament\Forms\Components;
/**
* @deprecated Use `CheckboxList` with the `relationship()` method instead.
*/
class BelongsToManyCheckboxList extends CheckboxList
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Filament\Forms\Components;
/**
* @deprecated Use `MultiSelect` with the `relationship()` method instead.
*/
class BelongsToManyMultiSelect extends MultiSelect
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Filament\Forms\Components;
/**
* @deprecated Use `Select` with the `relationship()` method instead.
*/
class BelongsToSelect extends Select
{
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
<?php
namespace Filament\Forms\Components\Builder;
use Closure;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Concerns;
use Illuminate\Contracts\Support\Htmlable;
class Block extends Component
{
use Concerns\HasName {
getLabel as getDefaultLabel;
}
use Concerns\HasPreview;
protected string | Closure | null $icon = null;
protected int | Closure | null $maxItems = null;
final public function __construct(string $name)
{
$this->name($name);
}
public static function make(string $name): static
{
$static = app(static::class, ['name' => $name]);
$static->configure();
return $static;
}
public function icon(string | Closure | null $icon): static
{
$this->icon = $icon;
return $this;
}
public function getIcon(): ?string
{
return $this->evaluate($this->icon);
}
public function maxItems(int | Closure | null $maxItems): static
{
$this->maxItems = $maxItems;
return $this;
}
public function getMaxItems(): ?int
{
return $this->evaluate($this->maxItems);
}
/**
* @param array<string, mixed> | null $state
*/
public function getLabel(?array $state = null, ?string $uuid = null): string | Htmlable
{
return $this->evaluate(
$this->label,
['state' => $state, 'uuid' => $uuid],
) ?? $this->getDefaultLabel();
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Filament\Forms\Components;
/**
* @deprecated Use `Section` with an empty heading instead.
*/
class Card extends Section
{
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Filament\Forms\Components;
class Checkbox extends Field
{
use Concerns\CanBeAccepted;
use Concerns\CanBeInline;
use Concerns\CanFixIndistinctState;
use Concerns\HasExtraInputAttributes;
/**
* @var view-string
*/
protected string $view = 'filament-forms::components.checkbox';
protected function setUp(): void
{
parent::setUp();
$this->default(false);
$this->afterStateHydrated(static function (Checkbox $component, $state): void {
$component->state((bool) $state);
});
$this->rule('boolean');
}
}

View File

@@ -0,0 +1,308 @@
<?php
namespace Filament\Forms\Components;
use Closure;
use Filament\Forms\Components\Actions\Action;
use Filament\Support\Enums\ActionSize;
use Filament\Support\Services\RelationshipJoiner;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Str;
class CheckboxList extends Field implements Contracts\CanDisableOptions, Contracts\HasNestedRecursiveValidationRules
{
use Concerns\CanBeSearchable;
use Concerns\CanDisableOptions;
use Concerns\CanDisableOptionsWhenSelectedInSiblingRepeaterItems;
use Concerns\CanFixIndistinctState;
use Concerns\CanLimitItemsLength;
use Concerns\HasDescriptions;
use Concerns\HasExtraInputAttributes;
use Concerns\HasGridDirection;
use Concerns\HasNestedRecursiveValidationRules;
use Concerns\HasOptions;
use Concerns\HasPivotData;
/**
* @var view-string
*/
protected string $view = 'filament-forms::components.checkbox-list';
protected string | Closure | null $relationshipTitleAttribute = null;
protected ?Closure $getOptionLabelFromRecordUsing = null;
protected string | Closure | null $relationship = null;
protected bool | Closure $isBulkToggleable = false;
protected ?Closure $modifySelectAllActionUsing = null;
protected ?Closure $modifyDeselectAllActionUsing = null;
protected function setUp(): void
{
parent::setUp();
$this->default([]);
$this->afterStateHydrated(static function (CheckboxList $component, $state) {
if (is_array($state)) {
return;
}
$component->state([]);
});
$this->searchDebounce(0);
$this->registerActions([
fn (CheckboxList $component): Action => $component->getSelectAllAction(),
fn (CheckboxList $component): Action => $component->getDeselectAllAction(),
]);
}
public function getSelectAllAction(): Action
{
$action = Action::make($this->getSelectAllActionName())
->label(__('filament-forms::components.checkbox_list.actions.select_all.label'))
->livewireClickHandlerEnabled(false)
->link()
->size(ActionSize::Small);
if ($this->modifySelectAllActionUsing) {
$action = $this->evaluate($this->modifySelectAllActionUsing, [
'action' => $action,
]) ?? $action;
}
return $action;
}
public function selectAllAction(?Closure $callback): static
{
$this->modifySelectAllActionUsing = $callback;
return $this;
}
public function getSelectAllActionName(): string
{
return 'selectAll';
}
public function getDeselectAllAction(): Action
{
$action = Action::make($this->getDeselectAllActionName())
->label(__('filament-forms::components.checkbox_list.actions.deselect_all.label'))
->livewireClickHandlerEnabled(false)
->link()
->size(ActionSize::Small);
if ($this->modifyDeselectAllActionUsing) {
$action = $this->evaluate($this->modifyDeselectAllActionUsing, [
'action' => $action,
]) ?? $action;
}
return $action;
}
public function deselectAllAction(?Closure $callback): static
{
$this->modifyDeselectAllActionUsing = $callback;
return $this;
}
public function getDeselectAllActionName(): string
{
return 'deselectAll';
}
public function relationship(string | Closure | null $name = null, string | Closure | null $titleAttribute = null, ?Closure $modifyQueryUsing = null): static
{
$this->relationship = $name ?? $this->getName();
$this->relationshipTitleAttribute = $titleAttribute;
$this->options(static function (CheckboxList $component) use ($modifyQueryUsing): array {
$relationship = Relation::noConstraints(fn () => $component->getRelationship());
$relationshipQuery = app(RelationshipJoiner::class)->prepareQueryForNoConstraints($relationship);
if ($modifyQueryUsing) {
$relationshipQuery = $component->evaluate($modifyQueryUsing, [
'query' => $relationshipQuery,
]) ?? $relationshipQuery;
}
if ($component->hasOptionLabelFromRecordUsingCallback()) {
return $relationshipQuery
->get()
->mapWithKeys(static fn (Model $record) => [
$record->{Str::afterLast($relationship->getQualifiedRelatedKeyName(), '.')} => $component->getOptionLabelFromRecord($record),
])
->toArray();
}
$relationshipTitleAttribute = $component->getRelationshipTitleAttribute();
if (empty($relationshipQuery->getQuery()->orders)) {
$relationshipQuery->orderBy($relationshipQuery->qualifyColumn($relationshipTitleAttribute));
}
if (str_contains($relationshipTitleAttribute, '->')) {
if (! str_contains($relationshipTitleAttribute, ' as ')) {
$relationshipTitleAttribute .= " as {$relationshipTitleAttribute}";
}
} else {
$relationshipTitleAttribute = $relationshipQuery->qualifyColumn($relationshipTitleAttribute);
}
return $relationshipQuery
->pluck($relationshipTitleAttribute, $relationship->getQualifiedRelatedKeyName())
->toArray();
});
$this->loadStateFromRelationshipsUsing(static function (CheckboxList $component, ?array $state) use ($modifyQueryUsing): void {
$relationship = $component->getRelationship();
if ($modifyQueryUsing) {
$component->evaluate($modifyQueryUsing, [
'query' => $relationship->getQuery(),
]);
}
/** @var Collection $relatedRecords */
$relatedRecords = $relationship->getResults();
$component->state(
// Cast the related keys to a string, otherwise Livewire does not
// know how to handle deselection.
//
// https://github.com/filamentphp/filament/issues/1111
$relatedRecords
->pluck($relationship->getRelatedKeyName())
->map(static fn ($key): string => strval($key))
->all(),
);
});
$this->saveRelationshipsUsing(static function (CheckboxList $component, ?array $state) use ($modifyQueryUsing) {
$relationship = $component->getRelationship();
if ($modifyQueryUsing) {
$component->evaluate($modifyQueryUsing, [
'query' => $relationship->getQuery(),
]);
}
/** @var Collection $relatedRecords */
$relatedRecords = $relationship->getResults();
$recordsToDetach = array_diff(
$relatedRecords
->pluck($relationship->getRelatedKeyName())
->map(static fn ($key): string => strval($key))
->all(),
$state ?? [],
);
if (count($recordsToDetach) > 0) {
$relationship->detach($recordsToDetach);
}
$pivotData = $component->getPivotData();
if ($pivotData === []) {
$relationship->sync($state ?? [], detaching: false);
return;
}
$relationship->syncWithPivotValues($state ?? [], $pivotData, detaching: false);
});
$this->dehydrated(false);
return $this;
}
public function bulkToggleable(bool | Closure $condition = true): static
{
$this->isBulkToggleable = $condition;
return $this;
}
public function getOptionLabelFromRecordUsing(?Closure $callback): static
{
$this->getOptionLabelFromRecordUsing = $callback;
return $this;
}
public function hasOptionLabelFromRecordUsingCallback(): bool
{
return $this->getOptionLabelFromRecordUsing !== null;
}
public function getOptionLabelFromRecord(Model $record): string | Htmlable
{
return $this->evaluate(
$this->getOptionLabelFromRecordUsing,
namedInjections: [
'record' => $record,
],
typedInjections: [
Model::class => $record,
$record::class => $record,
],
);
}
public function getRelationshipTitleAttribute(): ?string
{
return $this->evaluate($this->relationshipTitleAttribute);
}
public function getLabel(): string | Htmlable | null
{
if ($this->label === null && $this->getRelationship()) {
$label = (string) str($this->getRelationshipName())
->before('.')
->kebab()
->replace(['-', '_'], ' ')
->ucfirst();
return ($this->shouldTranslateLabel) ? __($label) : $label;
}
return parent::getLabel();
}
public function getRelationship(): ?BelongsToMany
{
$name = $this->getRelationshipName();
if (blank($name)) {
return null;
}
return $this->getModelInstance()->{$name}();
}
public function getRelationshipName(): ?string
{
return $this->evaluate($this->relationship);
}
public function isBulkToggleable(): bool
{
return (bool) $this->evaluate($this->isBulkToggleable);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Filament\Forms\Components;
use Closure;
use Filament\Support\Concerns\HasExtraAlpineAttributes;
class ColorPicker extends Field implements Contracts\HasAffixActions
{
use Concerns\HasAffixes;
use Concerns\HasExtraInputAttributes;
use Concerns\HasPlaceholder;
use HasExtraAlpineAttributes;
/**
* @var view-string
*/
protected string $view = 'filament-forms::components.color-picker';
protected string | Closure $format = 'hex';
public function format(string | Closure $format): static
{
$this->format = $format;
return $this;
}
public function hex(): static
{
$this->format('hex');
return $this;
}
public function hsl(): static
{
$this->format('hsl');
return $this;
}
public function rgb(): static
{
$this->format('rgb');
return $this;
}
public function rgba(): static
{
$this->format('rgba');
return $this;
}
public function getFormat(): string
{
return $this->evaluate($this->format);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Filament\Forms\Components;
use Filament\Forms\Concerns\HasColumns;
use Filament\Forms\Concerns\HasStateBindingModifiers;
use Filament\Support\Components\ViewComponent;
use Filament\Support\Concerns\CanGrow;
use Filament\Support\Concerns\HasExtraAttributes;
use Illuminate\Database\Eloquent\Model;
class Component extends ViewComponent
{
use CanGrow;
use Concerns\BelongsToContainer;
use Concerns\BelongsToModel;
use Concerns\CanBeConcealed;
use Concerns\CanBeDisabled;
use Concerns\CanBeHidden;
use Concerns\CanBeRepeated;
use Concerns\CanSpanColumns;
use Concerns\Cloneable;
use Concerns\HasActions;
use Concerns\HasChildComponents;
use Concerns\HasFieldWrapper;
use Concerns\HasId;
use Concerns\HasInlineLabel;
use Concerns\HasKey;
use Concerns\HasLabel;
use Concerns\HasMaxWidth;
use Concerns\HasMeta;
use Concerns\HasState;
use Concerns\ListensToEvents;
use HasColumns;
use HasExtraAttributes;
use HasStateBindingModifiers;
protected string $evaluationIdentifier = 'component';
/**
* @return array<mixed>
*/
protected function resolveDefaultClosureDependencyForEvaluationByName(string $parameterName): array
{
return match ($parameterName) {
'context', 'operation' => [$this->getContainer()->getOperation()],
'get' => [$this->getGetCallback()],
'livewire' => [$this->getLivewire()],
'model' => [$this->getModel()],
'record' => [$this->getRecord()],
'set' => [$this->getSetCallback()],
'state' => [$this->getState()],
default => parent::resolveDefaultClosureDependencyForEvaluationByName($parameterName),
};
}
/**
* @return array<mixed>
*/
protected function resolveDefaultClosureDependencyForEvaluationByType(string $parameterType): array
{
$record = $this->getRecord();
if (! $record) {
return parent::resolveDefaultClosureDependencyForEvaluationByType($parameterType);
}
return match ($parameterType) {
Model::class, $record::class => [$record],
default => parent::resolveDefaultClosureDependencyForEvaluationByType($parameterType),
};
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Filament\Forms\ComponentContainer;
use Filament\Forms\Contracts\HasForms;
trait BelongsToContainer
{
protected ComponentContainer $container;
public function container(ComponentContainer $container): static
{
$this->container = $container;
return $this;
}
public function getContainer(): ComponentContainer
{
return $this->container;
}
public function getLivewire(): HasForms
{
return $this->getContainer()->getLivewire();
}
}

View File

@@ -0,0 +1,189 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Illuminate\Database\Eloquent\Model;
trait BelongsToModel
{
protected Model | string | Closure | null $model = null;
protected ?Closure $loadStateFromRelationshipsUsing = null;
protected ?Closure $saveRelationshipsUsing = null;
protected ?Closure $saveRelationshipsBeforeChildrenUsing = null;
protected bool | Closure $shouldSaveRelationshipsWhenDisabled = false;
protected bool | Closure $shouldSaveRelationshipsWhenHidden = false;
public function model(Model | string | Closure | null $model = null): static
{
$this->model = $model;
return $this;
}
public function saveRelationships(): void
{
$callback = $this->saveRelationshipsUsing;
if (! $callback) {
return;
}
if (! ($this->getRecord()?->exists)) {
return;
}
if ((! $this->shouldSaveRelationshipsWhenDisabled()) && $this->isDisabled()) {
return;
}
if ((! $this->shouldSaveRelationshipsWhenHidden()) && $this->isHidden()) {
return;
}
$this->evaluate($callback);
}
public function saveRelationshipsBeforeChildren(): void
{
$callback = $this->saveRelationshipsBeforeChildrenUsing;
if (! $callback) {
return;
}
if (! ($this->getRecord()?->exists)) {
return;
}
if ((! $this->shouldSaveRelationshipsWhenDisabled()) && $this->isDisabled()) {
return;
}
if ((! $this->shouldSaveRelationshipsWhenHidden()) && $this->isHidden()) {
return;
}
$this->evaluate($callback);
}
public function loadStateFromRelationships(bool $andHydrate = false): void
{
$callback = $this->loadStateFromRelationshipsUsing;
if (! $callback) {
return;
}
if (! $this->getRecord()?->exists) {
return;
}
$this->evaluate($callback);
if ($andHydrate) {
$this->callAfterStateHydrated();
foreach ($this->getChildComponentContainers() as $container) {
$container->callAfterStateHydrated();
}
$this->fillStateWithNull();
}
}
public function saveRelationshipsUsing(?Closure $callback): static
{
$this->saveRelationshipsUsing = $callback;
return $this;
}
public function saveRelationshipsBeforeChildrenUsing(?Closure $callback): static
{
$this->saveRelationshipsBeforeChildrenUsing = $callback;
return $this;
}
public function saveRelationshipsWhenDisabled(bool | Closure $condition = true): static
{
$this->shouldSaveRelationshipsWhenDisabled = $condition;
return $this;
}
public function shouldSaveRelationshipsWhenDisabled(): bool
{
return (bool) $this->evaluate($this->shouldSaveRelationshipsWhenDisabled);
}
public function saveRelationshipsWhenHidden(bool | Closure $condition = true): static
{
$this->shouldSaveRelationshipsWhenHidden = $condition;
return $this;
}
public function shouldSaveRelationshipsWhenHidden(): bool
{
return (bool) $this->evaluate($this->shouldSaveRelationshipsWhenHidden);
}
public function loadStateFromRelationshipsUsing(?Closure $callback): static
{
$this->loadStateFromRelationshipsUsing = $callback;
return $this;
}
public function getModel(): ?string
{
$model = $this->evaluate($this->model);
if ($model instanceof Model) {
return $model::class;
}
if (filled($model)) {
return $model;
}
return $this->getContainer()->getModel();
}
public function getRecord(): ?Model
{
$model = $this->evaluate($this->model);
if ($model instanceof Model) {
return $model;
}
if (is_string($model)) {
return null;
}
return $this->getContainer()->getRecord();
}
public function getModelInstance(): ?Model
{
$model = $this->evaluate($this->model);
if ($model === null) {
return $this->getContainer()->getModelInstance();
}
if ($model instanceof Model) {
return $model;
}
return app($model);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait CanAllowHtml
{
protected bool | Closure $isHtmlAllowed = false;
public function allowHtml(bool | Closure $condition = true): static
{
$this->isHtmlAllowed = $condition;
return $this;
}
public function isHtmlAllowed(): bool
{
return (bool) $this->evaluate($this->isHtmlAllowed);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait CanBeAccepted
{
public function accepted(bool | Closure $condition = true): static
{
$this->rule('accepted', $condition);
return $this;
}
public function declined(bool | Closure $condition = true): static
{
$this->rule('declined', $condition);
return $this;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Components\Field;
trait CanBeAutocapitalized
{
protected bool | string | Closure | null $autocapitalize = null;
public function autocapitalize(bool | string | Closure | null $autocapitalize = true): static
{
$this->autocapitalize = $autocapitalize;
return $this;
}
/**
* @deprecated Use `autocapitalize()` instead.
*/
public function disableAutocapitalize(bool | Closure $condition = true): static
{
$this->autocapitalize(static function (Field $component) use ($condition): ?bool {
return $component->evaluate($condition) ? false : null;
});
return $this;
}
public function getAutocapitalize(): ?string
{
return match ($autocapitalize = $this->evaluate($this->autocapitalize)) {
true => 'on',
false => 'off',
default => $autocapitalize,
};
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Components\Field;
trait CanBeAutocompleted
{
protected bool | string | Closure | null $autocomplete = null;
public function autocomplete(bool | string | Closure | null $autocomplete = true): static
{
$this->autocomplete = $autocomplete;
return $this;
}
/**
* @deprecated Use `autocomplete()` instead.
*/
public function disableAutocomplete(bool | Closure $condition = true): static
{
$this->autocomplete(static function (Field $component) use ($condition): ?bool {
return $component->evaluate($condition) ? false : null;
});
return $this;
}
public function getAutocomplete(): ?string
{
return match ($autocomplete = $this->evaluate($this->autocomplete)) {
true => 'on',
false => 'off',
default => $autocomplete,
};
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait CanBeAutofocused
{
protected bool | Closure $isAutofocused = false;
public function autofocus(bool | Closure $condition = true): static
{
$this->isAutofocused = $condition;
return $this;
}
public function isAutofocused(): bool
{
return (bool) $this->evaluate($this->isAutofocused);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait CanBeCloned
{
protected bool | Closure $isCloneable = false;
public function cloneable(bool | Closure $condition = true): static
{
$this->isCloneable = $condition;
return $this;
}
public function isCloneable(): bool
{
if ($this->isDisabled()) {
return false;
}
return (bool) $this->evaluate($this->isCloneable);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\ComponentContainer;
trait CanBeCollapsed
{
protected bool | Closure $isCollapsed = false;
protected bool | Closure | null $isCollapsible = null;
protected bool | Closure $shouldPersistCollapsed = false;
public function collapsed(bool | Closure $condition = true, bool $shouldMakeComponentCollapsible = true): static
{
$this->isCollapsed = $condition;
if ($shouldMakeComponentCollapsible && ($this->isCollapsible === null)) {
$this->collapsible();
}
return $this;
}
public function isCollapsed(?ComponentContainer $item = null): bool
{
return (bool) $this->evaluate($this->isCollapsed, ['item' => $item]);
}
public function collapsible(bool | Closure | null $condition = true): static
{
$this->isCollapsible = $condition;
return $this;
}
public function isCollapsible(): bool
{
return (bool) ($this->evaluate($this->isCollapsible) ?? false);
}
public function persistCollapsed(bool | Closure $condition = true): static
{
$this->shouldPersistCollapsed = $condition;
return $this;
}
public function shouldPersistCollapsed(): bool
{
return (bool) $this->evaluate($this->shouldPersistCollapsed);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait CanBeCompacted
{
protected bool | Closure $isCompact = false;
public function compact(bool | Closure $condition = true): static
{
$this->isCompact = $condition;
return $this;
}
public function isCompact(): bool
{
return (bool) $this->evaluate($this->isCompact);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Contracts\CanConcealComponents;
trait CanBeConcealed
{
protected Component | bool | null $cachedConcealingComponent = null;
public function getConcealingComponent(): ?Component
{
if (filled($this->cachedConcealingComponent)) {
return $this->cachedConcealingComponent ?: null;
}
$parentComponent = $this->getContainer()->getParentComponent();
if (! $parentComponent) {
$this->cachedConcealingComponent = false;
} elseif ($parentComponent instanceof CanConcealComponents && $parentComponent->canConcealComponents()) {
$this->cachedConcealingComponent = $parentComponent;
} else {
$this->cachedConcealingComponent = $parentComponent->getConcealingComponent();
}
return $this->cachedConcealingComponent ?: null;
}
public function isConcealed(): bool
{
return (bool) $this->getConcealingComponent();
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Components\Component;
use Filament\Forms\Contracts\HasForms;
use Illuminate\Support\Arr;
trait CanBeDisabled
{
protected bool | Closure $isDisabled = false;
public function disabled(bool | Closure $condition = true): static
{
$this->isDisabled = $condition;
$this->dehydrated(fn (Component $component): bool => ! $component->evaluate($condition));
return $this;
}
/**
* @param string | array<string> $operations
*/
public function disabledOn(string | array $operations): static
{
$this->disabled(static function (HasForms $livewire, string $operation) use ($operations): bool {
foreach (Arr::wrap($operations) as $disabledOperation) {
if ($disabledOperation === $operation || $livewire instanceof $disabledOperation) {
return true;
}
}
return false;
});
return $this;
}
public function isDisabled(): bool
{
return $this->evaluate($this->isDisabled) || $this->getContainer()->isDisabled();
}
public function isEnabled(): bool
{
return ! $this->isDisabled();
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Components\Component;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Get;
use Illuminate\Support\Arr;
trait CanBeHidden
{
protected bool | Closure $isHidden = false;
protected bool | Closure $isVisible = true;
public function hidden(bool | Closure $condition = true): static
{
$this->isHidden = $condition;
return $this;
}
/**
* @param string | array<string> $operations
*/
public function hiddenOn(string | array $operations): static
{
$this->hidden(static function (HasForms $livewire, string $operation) use ($operations): bool {
foreach (Arr::wrap($operations) as $hiddenOperation) {
if ($hiddenOperation === $operation || $livewire instanceof $hiddenOperation) {
return true;
}
}
return false;
});
return $this;
}
public function hiddenWhenAllChildComponentsHidden(): static
{
$this->hidden(static function (Component $component): bool {
foreach ($component->getChildComponentContainers() as $childComponentContainer) {
foreach ($childComponentContainer->getComponents(withHidden: false) as $childComponent) {
return false;
}
}
return true;
});
return $this;
}
/**
* @param string | array<string> $paths
*/
public function whenTruthy(string | array $paths): static
{
$paths = Arr::wrap($paths);
$this->hidden(static function (Get $get) use ($paths): bool {
foreach ($paths as $path) {
if (! $get($path)) {
return true;
}
}
return false;
});
return $this;
}
/**
* @param string | array<string> $paths
*/
public function whenFalsy(string | array $paths): static
{
$paths = Arr::wrap($paths);
$this->hidden(static function (Get $get) use ($paths): bool {
foreach ($paths as $path) {
if ((bool) $get($path)) {
return true;
}
}
return false;
});
return $this;
}
public function visible(bool | Closure $condition = true): static
{
$this->isVisible = $condition;
return $this;
}
/**
* @param string | array<string> $operations
*/
public function visibleOn(string | array $operations): static
{
$this->visible(static function (string $operation, HasForms $livewire) use ($operations): bool {
foreach (Arr::wrap($operations) as $visibleOperation) {
if ($visibleOperation === $operation || $livewire instanceof $visibleOperation) {
return true;
}
}
return false;
});
return $this;
}
public function isHidden(): bool
{
if ($this->evaluate($this->isHidden)) {
return true;
}
return ! $this->evaluate($this->isVisible);
}
public function isVisible(): bool
{
return ! $this->isHidden();
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait CanBeInline
{
protected bool | Closure $isInline = true;
public function inline(bool | Closure $condition = true): static
{
$this->isInline = $condition;
return $this;
}
public function isInline(): bool
{
if ($this->hasInlineLabel()) {
return false;
}
return (bool) $this->evaluate($this->isInline);
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Components\Contracts\CanHaveNumericState;
trait CanBeLengthConstrained
{
protected int | Closure | null $length = null;
protected int | Closure | null $maxLength = null;
protected int | Closure | null $minLength = null;
public function length(int | Closure | null $length): static
{
$this->length = $length;
$this->maxLength = $length;
$this->minLength = $length;
return $this;
}
public function maxLength(int | Closure | null $length): static
{
$this->maxLength = $length;
return $this;
}
public function minLength(int | Closure | null $length): static
{
$this->minLength = $length;
return $this;
}
public function getLength(): ?int
{
return $this->evaluate($this->length);
}
public function getMaxLength(): ?int
{
return $this->evaluate($this->maxLength);
}
public function getMinLength(): ?int
{
return $this->evaluate($this->minLength);
}
/**
* @return array<string>
*/
public function getLengthValidationRules(): array
{
$isNumeric = $this instanceof CanHaveNumericState && $this->isNumeric();
if (filled($length = $this->getLength())) {
return $isNumeric ?
["digits:{$length}"] :
["size:{$length}"];
}
$rules = [];
if (filled($maxLength = $this->getMaxLength())) {
$rules[] = $isNumeric ?
"max_digits:{$maxLength}" :
"max:{$maxLength}";
}
if (filled($minLength = $this->getMinLength())) {
$rules[] = $isNumeric ?
"min_digits:{$minLength}" :
"min:{$minLength}";
}
return $rules;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait CanBeMarkedAsRequired
{
protected bool | Closure | null $isMarkedAsRequired = null;
public function markAsRequired(bool | Closure | null $condition = true): static
{
$this->isMarkedAsRequired = $condition;
return $this;
}
public function isMarkedAsRequired(): bool
{
return $this->evaluate($this->isMarkedAsRequired) ?? $this->isRequired();
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait CanBeNative
{
protected bool | Closure $isNative = true;
public function native(bool | Closure $condition = true): static
{
$this->isNative = $condition;
return $this;
}
public function isNative(): bool
{
return (bool) $this->evaluate($this->isNative);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait CanBePreloaded
{
protected bool | Closure $isPreloaded = false;
public function preload(bool | Closure $condition = true): static
{
$this->isPreloaded = $condition;
return $this;
}
public function isPreloaded(): bool
{
return (bool) $this->evaluate($this->isPreloaded);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Contracts\HasForms;
use Illuminate\Support\Arr;
trait CanBeReadOnly
{
protected bool | Closure $isReadOnly = false;
public function readOnly(bool | Closure $condition = true): static
{
$this->isReadOnly = $condition;
return $this;
}
/**
* @param string | array<string> $operations
*/
public function readOnlyOn(string | array $operations): static
{
$this->readOnly(static function (HasForms $livewire, string $operation) use ($operations): bool {
foreach (Arr::wrap($operations) as $readOnlyOperation) {
if ($readOnlyOperation === $operation || $livewire instanceof $readOnlyOperation) {
return true;
}
}
return false;
});
return $this;
}
public function isReadOnly(): bool
{
return (bool) $this->evaluate($this->isReadOnly);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Filament\Forms\Components\Repeater;
trait CanBeRepeated
{
protected Repeater | bool | null $cachedParentRepeater = null;
public function getParentRepeater(): ?Repeater
{
if (filled($this->cachedParentRepeater)) {
return $this->cachedParentRepeater ?: null;
}
$parentComponent = $this->getContainer()->getParentComponent();
if (! $parentComponent) {
$this->cachedParentRepeater = false;
} elseif ($parentComponent instanceof Repeater) {
$this->cachedParentRepeater = $parentComponent;
} else {
$this->cachedParentRepeater = $parentComponent->getParentRepeater();
}
return $this->cachedParentRepeater ?: null;
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Illuminate\Contracts\Support\Htmlable;
trait CanBeSearchable
{
protected bool | Closure $isSearchable = false;
protected string | Htmlable | Closure | null $noSearchResultsMessage = null;
protected int | Closure $searchDebounce = 1000;
protected string | Closure | null $searchingMessage = null;
protected string | Htmlable | Closure | null $searchPrompt = null;
protected bool | Closure $shouldSearchLabels = true;
protected bool | Closure $shouldSearchValues = false;
public function searchable(bool | Closure $condition = true): static
{
$this->isSearchable = $condition;
return $this;
}
public function noSearchResultsMessage(string | Htmlable | Closure | null $message): static
{
$this->noSearchResultsMessage = $message;
return $this;
}
public function searchDebounce(int | Closure $debounce): static
{
$this->searchDebounce = $debounce;
return $this;
}
public function searchingMessage(string | Closure | null $message): static
{
$this->searchingMessage = $message;
return $this;
}
public function searchPrompt(string | Htmlable | Closure | null $message): static
{
$this->searchPrompt = $message;
return $this;
}
public function searchLabels(bool | Closure | null $condition = true): static
{
$this->shouldSearchLabels = $condition;
return $this;
}
public function searchValues(bool | Closure | null $condition = true): static
{
$this->shouldSearchValues = $condition;
return $this;
}
public function getNoSearchResultsMessage(): string | Htmlable
{
return $this->evaluate($this->noSearchResultsMessage) ?? __('filament-forms::components.select.no_search_results_message');
}
public function getSearchPrompt(): string | Htmlable
{
return $this->evaluate($this->searchPrompt) ?? __('filament-forms::components.select.search_prompt');
}
public function shouldSearchLabels(): bool
{
return (bool) $this->evaluate($this->shouldSearchLabels);
}
public function shouldSearchValues(): bool
{
return (bool) $this->evaluate($this->shouldSearchValues);
}
/**
* @return array<string>
*/
public function getSearchableOptionFields(): array
{
return [
...($this->shouldSearchLabels() ? ['label'] : []),
...($this->shouldSearchValues() ? ['value'] : []),
];
}
public function getSearchDebounce(): int
{
return $this->evaluate($this->searchDebounce);
}
public function getSearchingMessage(): string
{
return $this->evaluate($this->searchingMessage) ?? __('filament-forms::components.select.searching_message');
}
public function isSearchable(): bool
{
return (bool) $this->evaluate($this->isSearchable);
}
}

View File

@@ -0,0 +1,853 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Components\Contracts\CanBeLengthConstrained;
use Filament\Forms\Components\Contracts\HasNestedRecursiveValidationRules;
use Filament\Forms\Components\Field;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\Rules\Unique;
trait CanBeValidated
{
protected bool | Closure $isRequired = false;
protected string | Closure | null $regexPattern = null;
/**
* @var array<mixed>
*/
protected array $rules = [];
/**
* @var array<string, string | Closure>
*/
protected array $validationMessages = [];
protected string | Closure | null $validationAttribute = null;
public function activeUrl(bool | Closure $condition = true): static
{
$this->rule('active_url', $condition);
return $this;
}
public function alpha(bool | Closure $condition = true): static
{
$this->rule('alpha', $condition);
return $this;
}
public function alphaDash(bool | Closure $condition = true): static
{
$this->rule('alpha_dash', $condition);
return $this;
}
public function alphaNum(bool | Closure $condition = true): static
{
$this->rule('alpha_num', $condition);
return $this;
}
public function ascii(bool | Closure $condition = true): static
{
$this->rule('ascii', $condition);
return $this;
}
public function confirmed(bool | Closure $condition = true): static
{
$this->rule('confirmed', $condition);
return $this;
}
/**
* @param array<scalar> | Arrayable | string | Closure $values
*/
public function doesntStartWith(array | Arrayable | string | Closure $values, bool | Closure $condition = true): static
{
$this->rule(static function (Field $component) use ($values) {
$values = $component->evaluate($values);
if ($values instanceof Arrayable) {
$values = $values->toArray();
}
if (is_array($values)) {
$values = implode(',', $values);
}
return 'doesnt_start_with:' . $values;
}, $condition);
return $this;
}
/**
* @param array<scalar> | Arrayable | string | Closure $values
*/
public function doesntEndWith(array | Arrayable | string | Closure $values, bool | Closure $condition = true): static
{
$this->rule(static function (Field $component) use ($values) {
$values = $component->evaluate($values);
if ($values instanceof Arrayable) {
$values = $values->toArray();
}
if (is_array($values)) {
$values = implode(',', $values);
}
return 'doesnt_end_with:' . $values;
}, $condition);
return $this;
}
/**
* @param array<scalar> | Arrayable | string | Closure $values
*/
public function endsWith(array | Arrayable | string | Closure $values, bool | Closure $condition = true): static
{
$this->rule(static function (Field $component) use ($values) {
$values = $component->evaluate($values);
if ($values instanceof Arrayable) {
$values = $values->toArray();
}
if (is_array($values)) {
$values = implode(',', $values);
}
return 'ends_with:' . $values;
}, $condition);
return $this;
}
public function enum(string | Closure $enum): static
{
$this->rule(static function (Field $component) use ($enum) {
$enum = $component->evaluate($enum);
return new Enum($enum);
}, static fn (Field $component): bool => filled($component->evaluate($enum)));
return $this;
}
public function exists(string | Closure | null $table = null, string | Closure | null $column = null, ?Closure $modifyRuleUsing = null): static
{
$this->rule(static function (Field $component, ?string $model) use ($column, $modifyRuleUsing, $table) {
$table = $component->evaluate($table) ?? $model;
$column = $component->evaluate($column) ?? $component->getName();
$rule = Rule::exists($table, $column);
if ($modifyRuleUsing) {
$rule = $component->evaluate($modifyRuleUsing, [
'rule' => $rule,
]) ?? $rule;
}
return $rule;
}, static fn (Field $component, ?string $model): bool => (bool) ($component->evaluate($table) ?? $model));
return $this;
}
public function filled(bool | Closure $condition = true): static
{
$this->rule('filled', $condition);
return $this;
}
public function hexColor(bool | Closure $condition = true): static
{
$this->rule('hex_color', $condition);
return $this;
}
/**
* @param array<scalar> | Arrayable | string | Closure $values
*/
public function in(array | Arrayable | string | Closure $values, bool | Closure $condition = true): static
{
$this->rule(static function (Field $component) use ($values) {
$values = $component->evaluate($values);
if ($values instanceof Arrayable) {
$values = $values->toArray();
}
if (is_string($values)) {
$values = array_map('trim', explode(',', $values));
}
return Rule::in($values);
}, $condition);
return $this;
}
public function ip(bool | Closure $condition = true): static
{
$this->rule('ip', $condition);
return $this;
}
public function ipv4(bool | Closure $condition = true): static
{
$this->rule('ipv4', $condition);
return $this;
}
public function ipv6(bool | Closure $condition = true): static
{
$this->rule('ipv6', $condition);
return $this;
}
public function json(bool | Closure $condition = true): static
{
$this->rule('json', $condition);
return $this;
}
public function macAddress(bool | Closure $condition = true): static
{
$this->rule('mac_address', $condition);
return $this;
}
public function multipleOf(int | Closure $value): static
{
$this->rule(static function (Field $component) use ($value) {
return 'multiple_of:' . $component->evaluate($value);
}, static fn (Field $component): bool => filled($component->evaluate($value)));
return $this;
}
/**
* @param array<scalar> | Arrayable | string | Closure $values
*/
public function notIn(array | Arrayable | string | Closure $values, bool | Closure $condition = true): static
{
$this->rule(static function (Field $component) use ($values) {
$values = $component->evaluate($values);
if ($values instanceof Arrayable) {
$values = $values->toArray();
}
if (is_string($values)) {
$values = array_map('trim', explode(',', $values));
}
return Rule::notIn($values);
}, $condition);
return $this;
}
public function notRegex(string | Closure | null $pattern): static
{
$this->rule(static function (Field $component) use ($pattern) {
return 'not_regex:' . $component->evaluate($pattern);
}, static fn (Field $component): bool => filled($component->evaluate($pattern)));
return $this;
}
public function nullable(bool | Closure $condition = true): static
{
$this->required(static function (Field $component) use ($condition): bool {
return ! $component->evaluate($condition);
});
return $this;
}
public function prohibited(bool | Closure $condition = true): static
{
$this->rule('prohibited', $condition);
return $this;
}
public function prohibitedIf(string | Closure $statePath, mixed $stateValues, bool $isStatePathAbsolute = false): static
{
return $this->multiFieldValueComparisonRule('prohibited_if', $statePath, $stateValues, $isStatePathAbsolute);
}
public function prohibitedUnless(string | Closure $statePath, mixed $stateValues, bool $isStatePathAbsolute = false): static
{
return $this->multiFieldValueComparisonRule('prohibited_unless', $statePath, $stateValues, $isStatePathAbsolute);
}
/**
* @param array<string> | string | Closure $statePaths
*/
public function prohibits(array | string | Closure $statePaths, bool $isStatePathAbsolute = false): static
{
return $this->multiFieldComparisonRule('prohibits', $statePaths, $isStatePathAbsolute);
}
public function required(bool | Closure $condition = true): static
{
$this->isRequired = $condition;
return $this;
}
public function requiredIf(string | Closure $statePath, mixed $stateValues, bool $isStatePathAbsolute = false): static
{
return $this->multiFieldValueComparisonRule('required_if', $statePath, $stateValues, $isStatePathAbsolute);
}
public function requiredIfAccepted(string | Closure $statePath, bool $isStatePathAbsolute = false): static
{
return $this->fieldComparisonRule('required_if_accepted', $statePath, $isStatePathAbsolute);
}
public function requiredUnless(string | Closure $statePath, mixed $stateValues, bool $isStatePathAbsolute = false): static
{
return $this->multiFieldValueComparisonRule('required_unless', $statePath, $stateValues, $isStatePathAbsolute);
}
/**
* @param string | array<string> | Closure $statePaths
*/
public function requiredWith(string | array | Closure $statePaths, bool $isStatePathAbsolute = false): static
{
return $this->multiFieldComparisonRule('required_with', $statePaths, $isStatePathAbsolute);
}
/**
* @param string | array<string> | Closure $statePaths
*/
public function requiredWithAll(string | array | Closure $statePaths, bool $isStatePathAbsolute = false): static
{
return $this->multiFieldComparisonRule('required_with_all', $statePaths, $isStatePathAbsolute);
}
/**
* @param string | array<string> | Closure $statePaths
*/
public function requiredWithout(string | array | Closure $statePaths, bool $isStatePathAbsolute = false): static
{
return $this->multiFieldComparisonRule('required_without', $statePaths, $isStatePathAbsolute);
}
/**
* @param string | array<string> | Closure $statePaths
*/
public function requiredWithoutAll(string | array | Closure $statePaths, bool $isStatePathAbsolute = false): static
{
return $this->multiFieldComparisonRule('required_without_all', $statePaths, $isStatePathAbsolute);
}
public function regex(string | Closure | null $pattern): static
{
$this->regexPattern = $pattern;
return $this;
}
/**
* @param array<scalar> | Arrayable | string | Closure $values
*/
public function startsWith(array | Arrayable | string | Closure $values, bool | Closure $condition = true): static
{
$this->rule(static function (Field $component) use ($values) {
$values = $component->evaluate($values);
if ($values instanceof Arrayable) {
$values = $values->toArray();
}
if (is_array($values)) {
$values = implode(',', $values);
}
return 'starts_with:' . $values;
}, $condition);
return $this;
}
public function string(bool | Closure $condition = true): static
{
$this->rule('string', $condition);
return $this;
}
public function ulid(bool | Closure $condition = true): static
{
$this->rule('ulid', $condition);
return $this;
}
public function uuid(bool | Closure $condition = true): static
{
$this->rule('uuid', $condition);
return $this;
}
public function rule(mixed $rule, bool | Closure $condition = true): static
{
$this->rules = [
...$this->rules,
[$rule, $condition],
];
return $this;
}
/**
* @param string | array<mixed> | Closure $rules
*/
public function rules(string | array | Closure $rules, bool | Closure $condition = true): static
{
if ($rules instanceof Closure) {
$this->rules = [
...$this->rules,
[$rules, $condition],
];
return $this;
}
if (is_string($rules)) {
$rules = explode('|', $rules);
}
$this->rules = [
...$this->rules,
...array_map(static fn (string | object $rule): array => [$rule, $condition], $rules),
];
return $this;
}
public function after(string | Closure $date, bool $isStatePathAbsolute = false): static
{
return $this->dateComparisonRule('after', $date, $isStatePathAbsolute);
}
public function afterOrEqual(string | Closure $date, bool $isStatePathAbsolute = false): static
{
return $this->dateComparisonRule('after_or_equal', $date, $isStatePathAbsolute);
}
public function before(string | Closure $date, bool $isStatePathAbsolute = false): static
{
return $this->dateComparisonRule('before', $date, $isStatePathAbsolute);
}
public function beforeOrEqual(string | Closure $date, bool $isStatePathAbsolute = false): static
{
return $this->dateComparisonRule('before_or_equal', $date, $isStatePathAbsolute);
}
public function different(string | Closure $statePath, bool $isStatePathAbsolute = false): static
{
return $this->fieldComparisonRule('different', $statePath, $isStatePathAbsolute);
}
public function gt(string | Closure $statePath, bool $isStatePathAbsolute = false): static
{
return $this->fieldComparisonRule('gt', $statePath, $isStatePathAbsolute);
}
public function gte(string | Closure $statePath, bool $isStatePathAbsolute = false): static
{
return $this->fieldComparisonRule('gte', $statePath, $isStatePathAbsolute);
}
public function lt(string | Closure $statePath, bool $isStatePathAbsolute = false): static
{
return $this->fieldComparisonRule('lt', $statePath, $isStatePathAbsolute);
}
public function lte(string | Closure $statePath, bool $isStatePathAbsolute = false): static
{
return $this->fieldComparisonRule('lte', $statePath, $isStatePathAbsolute);
}
public function same(string | Closure $statePath, bool $isStatePathAbsolute = false): static
{
return $this->fieldComparisonRule('same', $statePath, $isStatePathAbsolute);
}
public function unique(string | Closure | null $table = null, string | Closure | null $column = null, Model | Closure | null $ignorable = null, bool $ignoreRecord = false, ?Closure $modifyRuleUsing = null): static
{
$this->rule(static function (Field $component, ?string $model) use ($column, $ignorable, $ignoreRecord, $modifyRuleUsing, $table) {
$table = $component->evaluate($table) ?? $model;
$column = $component->evaluate($column) ?? $component->getName();
$ignorable = ($ignoreRecord && ! $ignorable) ?
$component->getRecord() :
$component->evaluate($ignorable);
$rule = Rule::unique($table, $column)
->when(
$ignorable,
fn (Unique $rule) => $rule->ignore(
$ignorable->getOriginal($ignorable->getKeyName()),
$ignorable->getQualifiedKeyName(),
),
);
if ($modifyRuleUsing) {
$rule = $component->evaluate($modifyRuleUsing, [
'rule' => $rule,
]) ?? $rule;
}
return $rule;
}, fn (Field $component, ?string $model): bool => (bool) ($component->evaluate($table) ?? $model));
return $this;
}
public function distinct(): static
{
$this->rule(static function (Field $component, mixed $state) {
return function (string $attribute, mixed $value, Closure $fail) use ($component, $state) {
if (blank($state)) {
return;
}
$repeater = $component->getParentRepeater();
if (! $repeater) {
return;
}
$repeaterStatePath = $repeater->getStatePath();
$componentItemStatePath = (string) str($component->getStatePath())
->after("{$repeaterStatePath}.")
->after('.');
$repeaterItemKey = (string) str($component->getStatePath())
->after("{$repeaterStatePath}.")
->beforeLast(".{$componentItemStatePath}");
$repeaterSiblingState = Arr::except($repeater->getState(), [$repeaterItemKey]);
if (empty($repeaterSiblingState)) {
return;
}
$validationMessages = $component->getValidationMessages();
if (is_bool($state)) {
$isSiblingItemSelected = collect($repeaterSiblingState)
->pluck($componentItemStatePath)
->contains(true);
if ($state && $isSiblingItemSelected) {
$fail(__($validationMessages['distinct.only_one_must_be_selected'] ?? 'filament-forms::validation.distinct.only_one_must_be_selected', ['attribute' => $component->getValidationAttribute()]));
return;
}
if ($state || $isSiblingItemSelected) {
return;
}
$fail(__($validationMessages['distinct.must_be_selected'] ?? 'filament-forms::validation.distinct.must_be_selected', ['attribute' => $component->getValidationAttribute()]));
return;
}
if (is_array($state)) {
$hasSiblingStateIntersections = collect($repeaterSiblingState)
->filter(fn (array $item): bool => filled(array_intersect(data_get($item, $componentItemStatePath, []), $state)))
->isNotEmpty();
if (! $hasSiblingStateIntersections) {
return;
}
$fail(__($validationMessages['distinct'] ?? 'validation.distinct', ['attribute' => $component->getValidationAttribute()]));
return;
}
$hasDuplicateSiblingState = collect($repeaterSiblingState)
->pluck($componentItemStatePath)
->contains($state);
if (! $hasDuplicateSiblingState) {
return;
}
$fail(__($validationMessages['distinct'] ?? 'validation.distinct', ['attribute' => $component->getValidationAttribute()]));
};
});
return $this;
}
public function validationAttribute(string | Closure | null $label): static
{
$this->validationAttribute = $label;
return $this;
}
/**
* @param array<string, string | Closure> $messages
*/
public function validationMessages(array $messages): static
{
$this->validationMessages = $messages;
return $this;
}
public function getRegexPattern(): ?string
{
return $this->evaluate($this->regexPattern);
}
public function getRequiredValidationRule(): string
{
return $this->isRequired() ? 'required' : 'nullable';
}
public function getValidationAttribute(): string
{
return $this->evaluate($this->validationAttribute) ?? Str::lcfirst($this->getLabel());
}
/**
* @return array<string, string>
*/
public function getValidationMessages(): array
{
$messages = [];
foreach ($this->validationMessages as $rule => $message) {
$messages[$rule] = $this->evaluate($message);
}
return array_filter($messages);
}
/**
* @return array<mixed>
*/
public function getValidationRules(): array
{
$rules = [
$this->getRequiredValidationRule(),
...($this instanceof CanBeLengthConstrained ? $this->getLengthValidationRules() : []),
];
if (filled($regexPattern = $this->getRegexPattern())) {
$rules[] = "regex:{$regexPattern}";
}
foreach ($this->rules as [$rule, $condition]) {
if (is_numeric($rule)) {
$rules[] = $this->evaluate($condition);
continue;
}
if (! $this->evaluate($condition)) {
continue;
}
$rule = $this->evaluate($rule);
if (is_array($rule)) {
$rules = [
...$rules,
...$rule,
];
continue;
}
$rules[] = $rule;
}
return $rules;
}
/**
* @param array<string, array<string, string>> $messages
*/
public function dehydrateValidationMessages(array &$messages): void
{
$statePath = $this->getStatePath();
if (count($componentMessages = $this->getValidationMessages())) {
foreach ($componentMessages as $rule => $message) {
$messages["{$statePath}.{$rule}"] = $message;
}
}
}
/**
* @param array<string, array<mixed>> $rules
*/
public function dehydrateValidationRules(array &$rules): void
{
$statePath = $this->getStatePath();
if (count($componentRules = $this->getValidationRules())) {
$rules[$statePath] = $componentRules;
}
if (! $this instanceof HasNestedRecursiveValidationRules) {
return;
}
$nestedRecursiveValidationRules = $this->getNestedRecursiveValidationRules();
if (! count($nestedRecursiveValidationRules)) {
return;
}
$rules["{$statePath}.*"] = $nestedRecursiveValidationRules;
}
public function dehydrateValidationAttributes(array &$attributes): void
{
$attributes[$this->getStatePath()] = $this->getValidationAttribute();
}
public function isRequired(): bool
{
return (bool) $this->evaluate($this->isRequired);
}
public function dateComparisonRule(string $rule, string | Closure $date, bool $isStatePathAbsolute = false): static
{
$this->rule(static function (Field $component) use ($date, $isStatePathAbsolute, $rule): string {
$date = $component->evaluate($date);
if (! (strtotime($date) || $isStatePathAbsolute)) {
$containerStatePath = $component->getContainer()->getStatePath();
if ($containerStatePath) {
$date = "{$containerStatePath}.{$date}";
}
}
return "{$rule}:{$date}";
}, fn (Field $component): bool => (bool) $component->evaluate($date));
return $this;
}
public function fieldComparisonRule(string $rule, string | Closure $statePath, bool $isStatePathAbsolute = false): static
{
$this->rule(static function (Field $component) use ($isStatePathAbsolute, $rule, $statePath): string {
$statePath = $component->evaluate($statePath);
if (! $isStatePathAbsolute) {
$containerStatePath = $component->getContainer()->getStatePath();
if ($containerStatePath) {
$statePath = "{$containerStatePath}.{$statePath}";
}
}
return "{$rule}:{$statePath}";
}, fn (Field $component): bool => (bool) $component->evaluate($statePath));
return $this;
}
/**
* @param array<string> | string | Closure $statePaths
*/
public function multiFieldComparisonRule(string $rule, array | string | Closure $statePaths, bool $isStatePathAbsolute = false): static
{
$this->rule(static function (Field $component) use ($isStatePathAbsolute, $rule, $statePaths): string {
$statePaths = $component->evaluate($statePaths);
if (! $isStatePathAbsolute) {
if (is_string($statePaths)) {
$statePaths = explode(',', $statePaths);
}
$containerStatePath = $component->getContainer()->getStatePath();
if ($containerStatePath) {
$statePaths = array_map(function ($statePath) use ($containerStatePath) {
$statePath = trim($statePath);
return "{$containerStatePath}.{$statePath}";
}, $statePaths);
}
}
if (is_array($statePaths)) {
$statePaths = implode(',', $statePaths);
}
return "{$rule}:{$statePaths}";
}, fn (Field $component): bool => (bool) $component->evaluate($statePaths));
return $this;
}
public function multiFieldValueComparisonRule(string $rule, string | Closure $statePath, mixed $stateValues, bool $isStatePathAbsolute = false): static
{
$this->rule(static function (Field $component) use ($isStatePathAbsolute, $rule, $statePath, $stateValues): string {
$statePath = $component->evaluate($statePath);
$stateValues = $component->evaluate($stateValues);
if (! $isStatePathAbsolute) {
$containerStatePath = $component->getContainer()->getStatePath();
if ($containerStatePath) {
$statePath = "{$containerStatePath}.{$statePath}";
}
}
if (is_array($stateValues)) {
$stateValues = implode(',', $stateValues);
} elseif (is_bool($stateValues)) {
$stateValues = $stateValues ? 'true' : 'false';
}
return "{$rule}:{$statePath},{$stateValues}";
}, fn (Field $component): bool => (bool) $component->evaluate($statePath));
return $this;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Illuminate\Support\Collection;
trait CanDisableOptions
{
protected bool | Closure | null $isOptionDisabled = null;
public function disableOptionWhen(bool | Closure $callback): static
{
$this->isOptionDisabled = $callback;
return $this;
}
/**
* @return array<string>
*/
public function getEnabledOptions(): array
{
return collect($this->getOptions())
->reduce(function (Collection $carry, $label, $value): Collection {
if (is_array($label)) {
return $carry->merge($label);
}
return $carry->put($value, $label);
}, collect())
->filter(fn ($label, $value) => ! $this->isOptionDisabled($value, $label))
->all();
}
/**
* @param array-key $value
*/
public function isOptionDisabled($value, string $label): bool
{
if ($this->isOptionDisabled === null) {
return false;
}
return (bool) $this->evaluate($this->isOptionDisabled, [
'label' => $label,
'value' => $value,
]);
}
public function hasDynamicDisabledOptions(): bool
{
return $this->isOptionDisabled instanceof Closure;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Contracts\CanDisableOptions;
use Illuminate\Support\Arr;
trait CanDisableOptionsWhenSelectedInSiblingRepeaterItems
{
public function disableOptionsWhenSelectedInSiblingRepeaterItems(): static
{
$this->distinct();
$this->live();
$this->disableOptionWhen(static function (Component & CanDisableOptions $component, string $value, mixed $state) {
$repeater = $component->getParentRepeater();
if (! $repeater) {
return false;
}
return collect($repeater->getState())
->pluck(
(string) str($component->getStatePath())
->after("{$repeater->getStatePath()}.")
->after('.'),
)
->flatten()
->diff(Arr::wrap($state))
->filter(fn (mixed $siblingItemState): bool => filled($siblingItemState))
->contains($value);
});
return $this;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Filament\Forms\Components\Component;
use Filament\Forms\Set;
use Illuminate\Support\Arr;
trait CanFixIndistinctState
{
public function fixIndistinctState(): static
{
$this->distinct();
$this->live();
$this->afterStateUpdated(static function (Component $component, mixed $state, Set $set) {
if (blank($state)) {
return;
}
$repeater = $component->getParentRepeater();
if (! $repeater) {
return;
}
$repeaterStatePath = $repeater->getStatePath();
$componentItemStatePath = (string) str($component->getStatePath())
->after("{$repeaterStatePath}.")
->after('.');
$repeaterItemKey = (string) str($component->getStatePath())
->after("{$repeaterStatePath}.")
->beforeLast(".{$componentItemStatePath}");
$repeaterSiblingState = Arr::except($repeater->getState(), [$repeaterItemKey]);
if (empty($repeaterSiblingState)) {
return;
}
if (is_array($state)) {
collect($repeaterSiblingState)
->filter(fn (array $itemState): bool => filled(array_intersect(data_get($itemState, $componentItemStatePath, []), $state)))
->map(fn (array $itemState): array => collect(data_get($itemState, $componentItemStatePath) ?? [])
->diff($state)
->values()
->all())
->each(fn (array $newSiblingItemState, string $itemKey) => $set(
path: "{$repeaterStatePath}.{$itemKey}.{$componentItemStatePath}",
state: $newSiblingItemState,
isAbsolute: true,
));
return;
}
collect($repeaterSiblingState)
->map(fn (array $itemState): mixed => data_get($itemState, $componentItemStatePath))
->filter(function (mixed $siblingItemComponentState) use ($state): bool {
if ($siblingItemComponentState === false) {
return false;
}
if (blank($siblingItemComponentState)) {
return false;
}
return $siblingItemComponentState === $state;
})
->each(fn (mixed $siblingItemComponentState, string $itemKey) => $set(
path: "{$repeaterStatePath}.{$itemKey}.{$componentItemStatePath}",
state: match ($siblingItemComponentState) {
true => false,
default => null,
},
isAbsolute: true,
));
});
return $this;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Illuminate\Support\Str;
trait CanGenerateUuids
{
protected Closure | bool | null $generateUuidUsing = null;
public function generateUuidUsing(Closure | bool | null $callback): static
{
$this->generateUuidUsing = $callback;
return $this;
}
public function generateUuid(): ?string
{
if ($this->generateUuidUsing) {
return $this->evaluate($this->generateUuidUsing);
}
if ($this->generateUuidUsing === false) {
return null;
}
return (string) Str::uuid();
}
public static function fake(): Closure
{
return static::configureUsing(
fn ($component) => $component->generateUuidUsing(false),
);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Components\Component;
trait CanLimitItemsLength
{
protected int | Closure | null $maxItems = null;
protected int | Closure | null $minItems = null;
public function maxItems(int | Closure | null $count): static
{
$this->maxItems = $count;
$this->rule('array');
$this->rule(static function (Component $component): string {
/** @var static $component */
$count = $component->getMaxItems();
return "max:{$count}";
});
return $this;
}
public function minItems(int | Closure | null $count): static
{
$this->minItems = $count;
$this->rule('array');
$this->rule(static function (Component $component): string {
/** @var static $component */
$count = $component->getMinItems();
return "min:{$count}";
});
return $this;
}
public function getMaxItems(): ?int
{
return $this->evaluate($this->maxItems);
}
public function getMinItems(): ?int
{
return $this->evaluate($this->minItems);
}
public function getItemsCount(): int
{
$state = $this->getState();
return is_array($state) ? count($state) : 0;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait CanSelectPlaceholder
{
protected bool | Closure | null $canSelectPlaceholder = true;
public function selectablePlaceholder(bool | Closure $condition = true): static
{
$this->canSelectPlaceholder = $condition;
return $this;
}
/**
* @deprecated Use `selectablePlaceholder()` instead.
*/
public function disablePlaceholderSelection(bool | Closure $condition = true): static
{
$this->selectablePlaceholder(fn (): bool => ! $this->evaluate($condition));
return $this;
}
public function canSelectPlaceholder(): bool
{
return (bool) $this->evaluate($this->canSelectPlaceholder);
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait CanSpanColumns
{
/**
* @var array<string, int | string | Closure | null>
*/
protected array $columnSpan = [
'default' => 1,
'sm' => null,
'md' => null,
'lg' => null,
'xl' => null,
'2xl' => null,
];
/**
* @var array<string, int | string | Closure | null>
*/
protected array $columnStart = [
'default' => null,
'sm' => null,
'md' => null,
'lg' => null,
'xl' => null,
'2xl' => null,
];
/**
* @param array<string, int | string | Closure | null> | int | string | Closure | null $span
*/
public function columnSpan(array | int | string | Closure | null $span): static
{
if (! is_array($span)) {
$span = [
'default' => $span,
];
}
$this->columnSpan = [
...$this->columnSpan,
...$span,
];
return $this;
}
public function columnSpanFull(): static
{
$this->columnSpan('full');
return $this;
}
/**
* @param array<string, int | string | Closure | null> | int | string | Closure | null $start
*/
public function columnStart(array | int | string | Closure | null $start): static
{
if (! is_array($start)) {
$start = [
'default' => $start,
];
}
$this->columnStart = [
...$this->columnStart,
...$start,
];
return $this;
}
/**
* @return array<string, int | string | Closure | null> | int | string | null
*/
public function getColumnSpan(int | string | null $breakpoint = null): array | int | string | null
{
$span = $this->columnSpan;
if ($breakpoint !== null) {
return $this->evaluate($span[$breakpoint] ?? null);
}
return array_map(
fn (array | int | string | Closure | null $value): array | int | string | null => $this->evaluate($value),
$span,
);
}
/**
* @return array<string, int | string | Closure | null> | int | string | null
*/
public function getColumnStart(int | string | null $breakpoint = null): array | int | string | null
{
$start = $this->columnStart;
if ($breakpoint !== null) {
return $this->evaluate($start[$breakpoint] ?? null);
}
return array_map(
fn (array | int | string | Closure | null $value): array | int | string | null => $this->evaluate($value),
$start,
);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Filament\Forms\Components\Component;
trait Cloneable
{
protected function cloneChildComponents(): static
{
if (is_array($this->childComponents)) {
$this->childComponents = array_map(
fn (Component $component): Component => $component->getClone(),
$this->childComponents,
);
}
return $this;
}
public function getClone(): static
{
$clone = clone $this;
$clone->flushCachedAbsoluteStatePath();
$clone->cloneChildComponents();
return $clone;
}
}

View File

@@ -0,0 +1,285 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\ComponentContainer;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Contracts\CanEntangleWithSingularRelationships;
use Filament\Forms\Contracts\HasForms;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphOne;
trait EntanglesStateWithSingularRelationship
{
protected ?Model $cachedExistingRecord = null;
protected ?string $relationship = null;
protected ?Closure $mutateRelationshipDataBeforeCreateUsing = null;
protected ?Closure $mutateRelationshipDataBeforeFillUsing = null;
protected ?Closure $mutateRelationshipDataBeforeSaveUsing = null;
public function relationship(string $name, bool | Closure $condition = true): static
{
$this->relationship = $name;
$this->statePath($name);
$this->loadStateFromRelationshipsUsing(static function (Component | CanEntangleWithSingularRelationships $component) {
$component->clearCachedExistingRecord();
$component->fillFromRelationship();
});
$this->saveRelationshipsBeforeChildrenUsing(static function (Component | CanEntangleWithSingularRelationships $component, HasForms $livewire) use ($condition): void {
$record = $component->getCachedExistingRecord();
if (! $component->evaluate($condition)) {
$record?->delete();
return;
}
if ($record) {
return;
}
$relationship = $component->getRelationship();
if ($relationship instanceof BelongsTo) {
return;
}
$data = $component->getChildComponentContainer()->getState(shouldCallHooksBefore: false);
$data = $component->mutateRelationshipDataBeforeCreate($data);
$relatedModel = $component->getRelatedModel();
$translatableContentDriver = $livewire->makeFilamentTranslatableContentDriver();
if ($translatableContentDriver) {
$record = $translatableContentDriver->makeRecord($relatedModel, $data);
} else {
$record = new $relatedModel();
$record->fill($data);
}
$relationship->save($record);
$component->cachedExistingRecord($record);
});
$this->saveRelationshipsUsing(static function (Component | CanEntangleWithSingularRelationships $component, HasForms $livewire) use ($condition): void {
if (! $component->evaluate($condition)) {
return;
}
$data = $component->getChildComponentContainer()->getState(shouldCallHooksBefore: false);
$record = $component->getCachedExistingRecord();
$translatableContentDriver = $livewire->makeFilamentTranslatableContentDriver();
if ($record) {
$data = $component->mutateRelationshipDataBeforeSave($data);
$translatableContentDriver ?
$translatableContentDriver->updateRecord($record, $data) :
$record->fill($data)->save();
return;
}
$relationship = $component->getRelationship();
if (! ($relationship instanceof BelongsTo)) {
return;
}
$data = $component->mutateRelationshipDataBeforeCreate($data);
$relatedModel = $component->getRelatedModel();
if ($translatableContentDriver) {
$record = $translatableContentDriver->makeRecord($relatedModel, $data);
} else {
$record = new $relatedModel();
$record->fill($data);
}
$record->save();
$relationship->associate($record);
$relationship->getParent()->save();
$component->cachedExistingRecord($record);
});
$this->dehydrated(false);
return $this;
}
public function fillFromRelationship(): void
{
$record = $this->getCachedExistingRecord();
if (! $record) {
$this->getChildComponentContainer()->fill(andCallHydrationHooks: false, andFillStateWithNull: false);
return;
}
$data = $this->mutateRelationshipDataBeforeFill(
$this->getStateFromRelatedRecord($record),
);
$this->getChildComponentContainer()->fill($data, andCallHydrationHooks: false, andFillStateWithNull: false);
}
/**
* @return array<string, mixed>
*/
protected function getStateFromRelatedRecord(Model $record): array
{
if ($translatableContentDriver = $this->getLivewire()->makeFilamentTranslatableContentDriver()) {
return $translatableContentDriver->getRecordAttributesToArray($record);
}
return $record->attributesToArray();
}
/**
* @param array-key $key
*/
public function getChildComponentContainer($key = null): ComponentContainer
{
$container = parent::getChildComponentContainer($key);
$relationship = $this->getRelationship();
if (! $relationship) {
return $container;
}
return $container->model($this->getCachedExistingRecord() ?? $this->getRelatedModel());
}
public function getRelationship(): BelongsTo | HasOne | MorphOne | null
{
$name = $this->getRelationshipName();
if (blank($name)) {
return null;
}
return $this->getModelInstance()->{$name}();
}
public function getRelationshipName(): ?string
{
return $this->relationship;
}
public function getRelatedModel(): ?string
{
return $this->getRelationship()?->getModel()::class;
}
public function cachedExistingRecord(?Model $record): static
{
$this->cachedExistingRecord = $record;
return $this;
}
public function getCachedExistingRecord(): ?Model
{
if ($this->cachedExistingRecord) {
return $this->cachedExistingRecord;
}
$record = $this->getRelationship()?->getResults();
if (! $record?->exists) {
return null;
}
return $this->cachedExistingRecord = $record;
}
public function clearCachedExistingRecord(): void
{
$this->cachedExistingRecord = null;
}
public function mutateRelationshipDataBeforeCreateUsing(?Closure $callback): static
{
$this->mutateRelationshipDataBeforeCreateUsing = $callback;
return $this;
}
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
public function mutateRelationshipDataBeforeCreate(array $data): array
{
if ($this->mutateRelationshipDataBeforeCreateUsing instanceof Closure) {
$data = $this->evaluate($this->mutateRelationshipDataBeforeCreateUsing, [
'data' => $data,
]);
}
return $data;
}
public function mutateRelationshipDataBeforeSaveUsing(?Closure $callback): static
{
$this->mutateRelationshipDataBeforeSaveUsing = $callback;
return $this;
}
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
public function mutateRelationshipDataBeforeFill(array $data): array
{
if ($this->mutateRelationshipDataBeforeFillUsing instanceof Closure) {
$data = $this->evaluate($this->mutateRelationshipDataBeforeFillUsing, [
'data' => $data,
]);
}
return $data;
}
public function mutateRelationshipDataBeforeFillUsing(?Closure $callback): static
{
$this->mutateRelationshipDataBeforeFillUsing = $callback;
return $this;
}
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
public function mutateRelationshipDataBeforeSave(array $data): array
{
if ($this->mutateRelationshipDataBeforeSaveUsing instanceof Closure) {
$data = $this->evaluate($this->mutateRelationshipDataBeforeSaveUsing, [
'data' => $data,
]);
}
return $data;
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Contracts\HasAffixActions;
use Filament\Forms\Components\Contracts\HasExtraItemActions;
use Filament\Forms\Components\Contracts\HasFooterActions;
use Filament\Forms\Components\Contracts\HasHeaderActions;
use Filament\Forms\Components\Contracts\HasHintActions;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
trait HasActions
{
/**
* @var array<Action> | null
*/
protected ?array $cachedActions = null;
/**
* @var array<string, Action | Closure>
*/
protected array $actions = [];
protected Model | string | null $actionFormModel = null;
/**
* @param array<Action | Closure> $actions
*/
public function registerActions(array $actions): static
{
$this->actions = [
...$this->actions,
...$actions,
];
return $this;
}
public function getAction(string $name): ?Action
{
return $this->getActions()[$name] ?? null;
}
/**
* @return array<string, Action>
*/
public function getActions(): array
{
return $this->cachedActions ??= $this->cacheActions();
}
/**
* @return array<Action>
*/
public function cacheActions(): array
{
$this->cachedActions = [];
if ($this instanceof HasAffixActions) {
$this->cachedActions = [
...$this->cachedActions,
...$this->getPrefixActions(),
...$this->getSuffixActions(),
];
}
if ($this instanceof HasExtraItemActions) {
$this->cachedActions = [
...$this->cachedActions,
...$this->getExtraItemActions(),
];
}
if ($this instanceof HasFooterActions) {
$this->cachedActions = [
...$this->cachedActions,
...$this->getFooterActions(),
];
}
if ($this instanceof HasHeaderActions) {
$this->cachedActions = [
...$this->cachedActions,
...$this->getHeaderActions(),
];
}
if ($this instanceof HasHintActions) {
$this->cachedActions = [
...$this->cachedActions,
...$this->getHintActions(),
];
}
foreach ($this->actions as $registeredAction) {
foreach (Arr::wrap($this->evaluate($registeredAction)) as $action) {
$this->cachedActions[$action->getName()] = $this->prepareAction($action);
}
}
return $this->cachedActions;
}
public function prepareAction(Action $action): Action
{
return $action->component($this);
}
public function actionFormModel(Model | string | null $model): static
{
$this->actionFormModel = $model;
return $this;
}
public function getActionFormModel(): Model | string | null
{
return $this->actionFormModel ?? $this->getRecord() ?? $this->getModel();
}
public function hasAction(string $name): bool
{
return array_key_exists($name, $this->getActions());
}
}

View File

@@ -0,0 +1,269 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Components\Actions\Action;
use Filament\Support\Enums\ActionSize;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Arr;
trait HasAffixes
{
/**
* @var array<Action> | null
*/
protected ?array $cachedSuffixActions = null;
/**
* @var array<Action | Closure>
*/
protected array $suffixActions = [];
protected string | Htmlable | Closure | null $suffixLabel = null;
/**
* @var array<Action> | null
*/
protected ?array $cachedPrefixActions = null;
/**
* @var array<Action | Closure>
*/
protected array $prefixActions = [];
protected string | Htmlable | Closure | null $prefixLabel = null;
protected string | Closure | null $prefixIcon = null;
/**
* @var string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | Closure | null
*/
protected string | array | Closure | null $prefixIconColor = null;
protected string | Closure | null $suffixIcon = null;
/**
* @var string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | Closure | null
*/
protected string | array | Closure | null $suffixIconColor = null;
protected bool | Closure $isPrefixInline = false;
protected bool | Closure $isSuffixInline = false;
public function prefix(string | Htmlable | Closure | null $label, bool | Closure $isInline = false): static
{
$this->prefixLabel = $label;
$this->inlinePrefix($isInline);
return $this;
}
public function postfix(string | Htmlable | Closure | null $label, bool | Closure $isInline = false): static
{
return $this->suffix($label, $isInline);
}
public function prefixAction(Action | Closure $action, bool | Closure $isInline = false): static
{
$this->prefixActions([$action], $isInline);
return $this;
}
/**
* @param array<Action | Closure> $actions
*/
public function prefixActions(array $actions, bool | Closure $isInline = false): static
{
$this->prefixActions = [
...$this->prefixActions,
...$actions,
];
$this->inlinePrefix($isInline);
return $this;
}
public function suffixAction(Action | Closure $action, bool | Closure $isInline = false): static
{
$this->suffixActions([$action], $isInline);
return $this;
}
/**
* @param array<Action | Closure> $actions
*/
public function suffixActions(array $actions, bool | Closure $isInline = false): static
{
$this->suffixActions = [
...$this->suffixActions,
...$actions,
];
$this->inlineSuffix($isInline);
return $this;
}
public function suffix(string | Htmlable | Closure | null $label, bool | Closure $isInline = false): static
{
$this->suffixLabel = $label;
$this->inlineSuffix($isInline);
return $this;
}
public function inlinePrefix(bool | Closure $isInline = true): static
{
$this->isPrefixInline = $isInline;
return $this;
}
public function inlineSuffix(bool | Closure $isInline = true): static
{
$this->isSuffixInline = $isInline;
return $this;
}
public function prefixIcon(string | Closure | null $icon, bool | Closure $isInline = false): static
{
$this->prefixIcon = $icon;
$this->inlinePrefix($isInline);
return $this;
}
/**
* @param string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | Closure | null $color
*/
public function prefixIconColor(string | array | Closure | null $color = null): static
{
$this->prefixIconColor = $color;
return $this;
}
public function suffixIcon(string | Closure | null $icon, bool | Closure $isInline = false): static
{
$this->suffixIcon = $icon;
$this->inlineSuffix($isInline);
return $this;
}
/**
* @param string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | Closure | null $color
*/
public function suffixIconColor(string | array | Closure | null $color = null): static
{
$this->suffixIconColor = $color;
return $this;
}
/**
* @return array<Action>
*/
public function getPrefixActions(): array
{
return $this->cachedPrefixActions ?? $this->cachePrefixActions();
}
/**
* @return array<Action>
*/
public function cachePrefixActions(): array
{
$this->cachedPrefixActions = [];
foreach ($this->prefixActions as $prefixAction) {
foreach (Arr::wrap($this->evaluate($prefixAction)) as $action) {
$this->cachedPrefixActions[$action->getName()] = $this->prepareAction(
$action
->defaultSize(ActionSize::Small)
->defaultView(Action::ICON_BUTTON_VIEW),
);
}
}
return $this->cachedPrefixActions;
}
/**
* @return array<Action>
*/
public function getSuffixActions(): array
{
return $this->cachedSuffixActions ?? $this->cacheSuffixActions();
}
/**
* @return array<Action>
*/
public function cacheSuffixActions(): array
{
$this->cachedSuffixActions = [];
foreach ($this->suffixActions as $suffixAction) {
foreach (Arr::wrap($this->evaluate($suffixAction)) as $action) {
$this->cachedSuffixActions[$action->getName()] = $this->prepareAction(
$action
->defaultSize(ActionSize::Small)
->defaultView(Action::ICON_BUTTON_VIEW),
);
}
}
return $this->cachedSuffixActions;
}
public function getPrefixLabel(): string | Htmlable | null
{
return $this->evaluate($this->prefixLabel);
}
public function getSuffixLabel(): string | Htmlable | null
{
return $this->evaluate($this->suffixLabel);
}
public function getPrefixIcon(): ?string
{
return $this->evaluate($this->prefixIcon);
}
public function getSuffixIcon(): ?string
{
return $this->evaluate($this->suffixIcon);
}
/**
* @return string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null
*/
public function getPrefixIconColor(): string | array | null
{
return $this->evaluate($this->prefixIconColor);
}
/**
* @return string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null
*/
public function getSuffixIconColor(): string | array | null
{
return $this->evaluate($this->suffixIconColor);
}
public function isPrefixInline(): bool
{
return (bool) $this->evaluate($this->isPrefixInline);
}
public function isSuffixInline(): bool
{
return (bool) $this->evaluate($this->isSuffixInline);
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\ComponentContainer;
use Filament\Forms\Components\Component;
trait HasChildComponents
{
/**
* @var array<Component> | Closure
*/
protected array | Closure $childComponents = [];
/**
* @param array<Component> | Closure $components
*/
public function childComponents(array | Closure $components): static
{
$this->childComponents = $components;
return $this;
}
/**
* @param array<Component> | Closure $components
*/
public function schema(array | Closure $components): static
{
$this->childComponents($components);
return $this;
}
/**
* @return array<Component>
*/
public function getChildComponents(): array
{
return $this->evaluate($this->childComponents);
}
/**
* @param array-key $key
*/
public function getChildComponentContainer($key = null): ComponentContainer
{
if (filled($key) && array_key_exists($key, $containers = $this->getChildComponentContainers())) {
return $containers[$key];
}
return ComponentContainer::make($this->getLivewire())
->parentComponent($this)
->components($this->getChildComponents());
}
/**
* @return array<ComponentContainer>
*/
public function getChildComponentContainers(bool $withHidden = false): array
{
if (! $this->hasChildComponentContainer($withHidden)) {
return [];
}
return [$this->getChildComponentContainer()];
}
public function hasChildComponentContainer(bool $withHidden = false): bool
{
if ((! $withHidden) && $this->isHidden()) {
return false;
}
if ($this->getChildComponents() === []) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Support\Contracts\HasColor as ColorInterface;
use Illuminate\Contracts\Support\Arrayable;
use UnitEnum;
trait HasColors
{
/**
* @var array<string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null> | Arrayable | Closure | null
*/
protected array | Arrayable | Closure | null $colors = null;
/**
* @param array<string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null> | Arrayable | Closure | null $colors
*/
public function colors(array | Arrayable | Closure | null $colors): static
{
$this->colors = $colors;
return $this;
}
/**
* @return string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null
*/
public function getColor(mixed $value): string | array | null
{
return $this->getColors()[$value] ?? null;
}
/**
* @return array<string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null>
*/
public function getColors(): array
{
$colors = $this->evaluate($this->colors);
if ($colors instanceof Arrayable) {
$colors = $colors->toArray();
}
if (
is_string($this->options) &&
enum_exists($enum = $this->options) &&
is_a($enum, ColorInterface::class, allow_string: true)
) {
return array_reduce($enum::cases(), function (array $carry, ColorInterface & UnitEnum $case): array {
$carry[$case?->value ?? $case->name] = $case->getColor();
return $carry;
}, []);
}
return $colors ?? [];
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Filament\Forms\Components\Concerns;
trait HasContainerGridLayout
{
/**
* @var array<string, int | string | null> | null
*/
protected ?array $gridColumns = null;
/**
* @param array<string, int | string | null> | int | string | null $columns
*/
public function grid(array | int | string | null $columns = 2): static
{
if (! is_array($columns)) {
$columns = [
'lg' => $columns,
];
}
$this->gridColumns = [
...($this->gridColumns ?? []),
...$columns,
];
return $this;
}
/**
* @return array<string, int | string | null> | int | string | null
*/
public function getGridColumns(?string $breakpoint = null): array | int | string | null
{
$columns = $this->gridColumns ?? [
'default' => 1,
'sm' => null,
'md' => null,
'lg' => null,
'xl' => null,
'2xl' => null,
];
if ($breakpoint !== null) {
return $columns[$breakpoint] ?? null;
}
return $columns;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Illuminate\Contracts\Support\Arrayable;
trait HasDatalistOptions
{
/**
* @var array<string> | Arrayable | Closure | null
*/
protected array | Arrayable | Closure | null $datalistOptions = null;
/**
* @param array<string> | Arrayable | Closure | null $options
*/
public function datalist(array | Arrayable | Closure | null $options): static
{
$this->datalistOptions = $options;
return $this;
}
/**
* @return array<string> | null
*/
public function getDatalistOptions(): ?array
{
$options = $this->evaluate($this->datalistOptions);
if ($options instanceof Arrayable) {
$options = $options->toArray();
}
return $options;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Support\Contracts\HasDescription;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Htmlable;
use UnitEnum;
trait HasDescriptions
{
/**
* @var array<string | Htmlable> | Arrayable | Closure
*/
protected array | Arrayable | Closure $descriptions = [];
/**
* @param array<string | Htmlable> | Arrayable | Closure $descriptions
*/
public function descriptions(array | Arrayable | Closure $descriptions): static
{
$this->descriptions = $descriptions;
return $this;
}
/**
* @param array-key $value
*/
public function hasDescription($value): bool
{
return array_key_exists($value, $this->getDescriptions());
}
/**
* @param array-key $value
*/
public function getDescription($value): string | Htmlable | null
{
return $this->getDescriptions()[$value] ?? null;
}
/**
* @return array<string | Htmlable>
*/
public function getDescriptions(): array
{
$descriptions = $this->evaluate($this->descriptions);
if ($descriptions instanceof Arrayable) {
$descriptions = $descriptions->toArray();
}
if (
empty($descriptions) &&
is_string($this->options) &&
enum_exists($this->options) &&
is_a($this->options, HasDescription::class, allow_string: true)
) {
$descriptions = array_reduce($this->options::cases(), function (array $carry, HasDescription & UnitEnum $case): array {
if (filled($description = $case->getDescription())) {
$carry[$case?->value ?? $case->name] = $description;
}
return $carry;
}, []);
}
return $descriptions;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Filament\Support\Concerns\HasExtraAlpineAttributes as BaseTrait;
/**
* @deprecated Use `\Filament\Support\Concerns\HasExtraAlpineAttributes` instead.
* @see BaseTrait
*/
trait HasExtraAlpineAttributes
{
use BaseTrait;
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Filament\Support\Concerns\HasExtraAttributes as BaseTrait;
/**
* @deprecated Use `\Filament\Support\Concerns\HasExtraAttributes` instead.
* @see BaseTrait
*/
trait HasExtraAttributes
{
use BaseTrait;
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Illuminate\View\ComponentAttributeBag;
trait HasExtraFieldWrapperAttributes
{
/**
* @var array<array<mixed> | Closure>
*/
protected array $extraFieldWrapperAttributes = [];
/**
* @param array<mixed> | Closure $attributes
*/
public function extraFieldWrapperAttributes(array | Closure $attributes, bool $merge = false): static
{
if ($merge) {
$this->extraFieldWrapperAttributes[] = $attributes;
} else {
$this->extraFieldWrapperAttributes = [$attributes];
}
return $this;
}
/**
* @return array<mixed>
*/
public function getExtraFieldWrapperAttributes(): array
{
$temporaryAttributeBag = new ComponentAttributeBag();
foreach ($this->extraFieldWrapperAttributes as $extraFieldWrapperAttributes) {
$temporaryAttributeBag = $temporaryAttributeBag->merge($this->evaluate($extraFieldWrapperAttributes));
}
return $temporaryAttributeBag->getAttributes();
}
public function getExtraFieldWrapperAttributesBag(): ComponentAttributeBag
{
return new ComponentAttributeBag($this->getExtraFieldWrapperAttributes());
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Illuminate\View\ComponentAttributeBag;
trait HasExtraInputAttributes
{
/**
* @var array<array<mixed> | Closure>
*/
protected array $extraInputAttributes = [];
/**
* @param array<mixed> | Closure $attributes
*/
public function extraInputAttributes(array | Closure $attributes, bool $merge = false): static
{
if ($merge) {
$this->extraInputAttributes[] = $attributes;
} else {
$this->extraInputAttributes = [$attributes];
}
return $this;
}
/**
* @return array<mixed>
*/
public function getExtraInputAttributes(): array
{
$temporaryAttributeBag = new ComponentAttributeBag();
foreach ($this->extraInputAttributes as $extraInputAttributes) {
$temporaryAttributeBag = $temporaryAttributeBag->merge($this->evaluate($extraInputAttributes));
}
return $temporaryAttributeBag->getAttributes();
}
public function getExtraInputAttributeBag(): ComponentAttributeBag
{
return new ComponentAttributeBag($this->getExtraInputAttributes());
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Components\Actions\Action;
use Filament\Support\Enums\ActionSize;
use Illuminate\Support\Arr;
trait HasExtraItemActions
{
/**
* @var array<Action | Closure>
*/
protected array $extraItemActions = [];
/**
* @var array<Action> | null
*/
protected ?array $cachedExtraItemActions = null;
/**
* @param array<Action | Closure> $actions
*/
public function extraItemActions(array $actions): static
{
$this->extraItemActions = [
...$this->extraItemActions,
...$actions,
];
return $this;
}
/**
* @return array<Action>
*/
public function getExtraItemActions(): array
{
return $this->cachedExtraItemActions ?? $this->cacheExtraItemActions();
}
/**
* @return array<Action>
*/
public function cacheExtraItemActions(): array
{
$this->cachedExtraItemActions = [];
foreach ($this->extraItemActions as $extraItemAction) {
foreach (Arr::wrap($this->evaluate($extraItemAction)) as $action) {
$this->cachedExtraItemActions[$action->getName()] = $this->prepareAction(
$action
->defaultColor('gray')
->defaultSize(ActionSize::Small)
->defaultView(Action::ICON_BUTTON_VIEW),
);
}
}
return $this->cachedExtraItemActions;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasFieldWrapper
{
protected string | Closure | null $fieldWrapperView = null;
public function fieldWrapperView(string | Closure | null $view): static
{
$this->fieldWrapperView = $view;
return $this;
}
public function getFieldWrapperView(): string
{
return $this->getCustomFieldWrapperView() ??
$this->getContainer()->getCustomFieldWrapperView() ??
'filament-forms::field-wrapper';
}
public function getCustomFieldWrapperView(): ?string
{
return $this->evaluate($this->fieldWrapperView);
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\UnableToCheckFileExistence;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use SplFileInfo;
use Throwable;
trait HasFileAttachments
{
protected string | Closure | null $fileAttachmentsDirectory = null;
protected string | Closure | null $fileAttachmentsDiskName = null;
protected ?Closure $getUploadedAttachmentUrlUsing = null;
protected ?Closure $saveUploadedFileAttachmentsUsing = null;
protected string | Closure $fileAttachmentsVisibility = 'public';
public function fileAttachmentsDirectory(string | Closure | null $directory): static
{
$this->fileAttachmentsDirectory = $directory;
return $this;
}
public function fileAttachmentsDisk(string | Closure | null $name): static
{
$this->fileAttachmentsDiskName = $name;
return $this;
}
public function saveUploadedFileAttachment(TemporaryUploadedFile $attachment): ?string
{
if ($callback = $this->saveUploadedFileAttachmentsUsing) {
$file = $this->evaluate($callback, [
'file' => $attachment,
]);
} else {
$file = $this->handleFileAttachmentUpload($attachment);
}
if ($callback = $this->getUploadedAttachmentUrlUsing) {
return $this->evaluate($callback, [
'file' => $file,
]);
}
return $this->handleUploadedAttachmentUrlRetrieval($file);
}
public function fileAttachmentsVisibility(string | Closure $visibility): static
{
$this->fileAttachmentsVisibility = $visibility;
return $this;
}
public function getUploadedAttachmentUrlUsing(?Closure $callback): static
{
$this->getUploadedAttachmentUrlUsing = $callback;
return $this;
}
public function saveUploadedFileAttachmentsUsing(?Closure $callback): static
{
$this->saveUploadedFileAttachmentsUsing = $callback;
return $this;
}
public function getFileAttachmentsDirectory(): ?string
{
return $this->evaluate($this->fileAttachmentsDirectory);
}
public function getFileAttachmentsDisk(): Filesystem
{
return Storage::disk($this->getFileAttachmentsDiskName());
}
public function getFileAttachmentsDiskName(): string
{
return $this->evaluate($this->fileAttachmentsDiskName) ?? config('filament.default_filesystem_disk');
}
public function getFileAttachmentsVisibility(): string
{
return $this->evaluate($this->fileAttachmentsVisibility);
}
protected function handleFileAttachmentUpload(SplFileInfo $file): mixed
{
$storeMethod = $this->getFileAttachmentsVisibility() === 'public' ? 'storePublicly' : 'store';
return $file->{$storeMethod}($this->getFileAttachmentsDirectory(), $this->getFileAttachmentsDiskName());
}
protected function handleUploadedAttachmentUrlRetrieval(mixed $file): ?string
{
/** @var FilesystemAdapter $storage */
$storage = $this->getFileAttachmentsDisk();
try {
if (! $storage->exists($file)) {
return null;
}
} catch (UnableToCheckFileExistence $exception) {
return null;
}
if ($storage->getVisibility($file) === 'private') {
try {
return $storage->temporaryUrl(
$file,
now()->addMinutes(5),
);
} catch (Throwable $exception) {
// This driver does not support creating temporary URLs.
}
}
return $storage->url($file);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Components\Actions\Action;
use Filament\Support\Concerns\HasFooterActionsAlignment;
use Illuminate\Support\Arr;
trait HasFooterActions
{
use HasFooterActionsAlignment;
/**
* @var array<Action> | null
*/
protected ?array $cachedFooterActions = null;
/**
* @var array<Action | Closure>
*/
protected array $footerActions = [];
/**
* @param array<Action | Closure> $actions
*/
public function footerActions(array $actions): static
{
$this->footerActions = [
...$this->footerActions,
...$actions,
];
return $this;
}
/**
* @return array<Action>
*/
public function getFooterActions(): array
{
return $this->cachedFooterActions ?? $this->cacheFooterActions();
}
/**
* @return array<Action>
*/
public function cacheFooterActions(): array
{
$this->cachedFooterActions = [];
foreach ($this->footerActions as $footerAction) {
foreach (Arr::wrap($this->evaluate($footerAction)) as $action) {
$this->cachedFooterActions[$action->getName()] = $this->prepareAction($action);
}
}
return $this->cachedFooterActions;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasGridDirection
{
protected string | Closure | null $gridDirection = null;
public function gridDirection(string | Closure | null $gridDirection): static
{
$this->gridDirection = $gridDirection;
return $this;
}
public function getGridDirection(): ?string
{
return $this->evaluate($this->gridDirection);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Components\Actions\Action;
use Illuminate\Support\Arr;
trait HasHeaderActions
{
/**
* @var array<Action> | null
*/
protected ?array $cachedHeaderActions = null;
/**
* @var array<Action | Closure>
*/
protected array $headerActions = [];
/**
* @param array<Action | Closure> $actions
*/
public function headerActions(array $actions): static
{
$this->headerActions = [
...$this->headerActions,
...$actions,
];
return $this;
}
/**
* @return array<Action>
*/
public function getHeaderActions(): array
{
return $this->cachedHeaderActions ?? $this->cacheHeaderActions();
}
/**
* @return array<Action>
*/
public function cacheHeaderActions(): array
{
$this->cachedHeaderActions = [];
foreach ($this->headerActions as $headerAction) {
foreach (Arr::wrap($this->evaluate($headerAction)) as $action) {
$this->cachedHeaderActions[$action->getName()] = $this->prepareAction($action);
}
}
return $this->cachedHeaderActions;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Illuminate\Contracts\Support\Htmlable;
trait HasHelperText
{
protected string | Htmlable | Closure | null $helperText = null;
public function helperText(string | Htmlable | Closure | null $text): static
{
$this->helperText = $text;
return $this;
}
public function getHelperText(): string | Htmlable | null
{
return $this->evaluate($this->helperText);
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Components\Actions\Action;
use Filament\Support\Enums\ActionSize;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Arr;
trait HasHint
{
protected string | Htmlable | Closure | null $hint = null;
/**
* @var array<Action> | null
*/
protected ?array $cachedHintActions = null;
/**
* @var array<Action | Closure>
*/
protected array $hintActions = [];
/**
* @var string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | Closure | null
*/
protected string | array | Closure | null $hintColor = null;
protected string | Closure | null $hintIcon = null;
protected string | Closure | null $hintIconTooltip = null;
public function hint(string | Htmlable | Closure | null $hint): static
{
$this->hint = $hint;
return $this;
}
/**
* @param string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | Closure | null $color
*/
public function hintColor(string | array | Closure | null $color): static
{
$this->hintColor = $color;
return $this;
}
public function hintIcon(string | Closure | null $icon, string | Closure | null $tooltip = null): static
{
$this->hintIcon = $icon;
$this->hintIconTooltip($tooltip);
return $this;
}
public function hintIconTooltip(string | Closure | null $tooltip): static
{
$this->hintIconTooltip = $tooltip;
return $this;
}
public function hintAction(Action | Closure $action): static
{
$this->hintActions([$action]);
return $this;
}
/**
* @param array<Action | Closure> $actions
*/
public function hintActions(array $actions): static
{
$this->hintActions = [
...$this->hintActions,
...$actions,
];
return $this;
}
public function getHint(): string | Htmlable | null
{
return $this->evaluate($this->hint);
}
/**
* @return string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null
*/
public function getHintColor(): string | array | null
{
return $this->evaluate($this->hintColor);
}
public function getHintIcon(): ?string
{
return $this->evaluate($this->hintIcon);
}
public function getHintIconTooltip(): ?string
{
return $this->evaluate($this->hintIconTooltip);
}
/**
* @return array<Action>
*/
public function getHintActions(): array
{
return $this->cachedHintActions ?? $this->cacheHintActions();
}
/**
* @return array<Action>
*/
public function cacheHintActions(): array
{
$this->cachedHintActions = [];
foreach ($this->hintActions as $hintAction) {
foreach (Arr::wrap($this->evaluate($hintAction)) as $action) {
$this->cachedHintActions[$action->getName()] = $this->prepareAction(
$action
->defaultSize(ActionSize::Small)
->defaultView(Action::LINK_VIEW),
);
}
}
return $this->cachedHintActions;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Support\Contracts\HasIcon as IconInterface;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Htmlable;
use UnitEnum;
trait HasIcons
{
/**
* @var array<string | Htmlable | null> | Arrayable | Closure | null
*/
protected array | Arrayable | Closure | null $icons = null;
/**
* @param array<string | Htmlable | null> | Arrayable | Closure | null $icons
*/
public function icons(array | Arrayable | Closure | null $icons): static
{
$this->icons = $icons;
return $this;
}
public function getIcon(mixed $value): string | Htmlable | null
{
return $this->getIcons()[$value] ?? null;
}
/**
* @return array<string | Htmlable | null>
*/
public function getIcons(): array
{
$icons = $this->evaluate($this->icons);
if ($icons instanceof Arrayable) {
$icons = $icons->toArray();
}
if (
is_string($this->options) &&
enum_exists($enum = $this->options) &&
is_a($enum, IconInterface::class, allow_string: true)
) {
return array_reduce($enum::cases(), function (array $carry, IconInterface & UnitEnum $case): array {
$carry[$case?->value ?? $case->name] = $case->getIcon();
return $carry;
}, []);
}
return $icons ?? [];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasId
{
protected string | Closure | null $id = null;
public function id(string | Closure | null $id): static
{
$this->id = $id;
return $this;
}
public function getId(): ?string
{
return $this->evaluate($this->id);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasInlineLabel
{
protected bool | Closure | null $hasInlineLabel = null;
public function inlineLabel(bool | Closure | null $condition = true): static
{
$this->hasInlineLabel = $condition;
return $this;
}
public function hasInlineLabel(): ?bool
{
return $this->evaluate($this->hasInlineLabel) ?? $this->getContainer()->hasInlineLabel();
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasInputMode
{
protected string | Closure | null $inputMode = null;
public function inputMode(string | Closure | null $mode): static
{
$this->inputMode = $mode;
return $this;
}
public function getInputMode(): ?string
{
return $this->evaluate($this->inputMode);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasKey
{
protected string | Closure | null $key = null;
public function key(string | Closure | null $key): static
{
$this->key = $key;
return $this;
}
public function getKey(): ?string
{
return $this->evaluate($this->key);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Illuminate\Contracts\Support\Htmlable;
trait HasLabel
{
protected bool | Closure $isLabelHidden = false;
protected string | Htmlable | Closure | null $label = null;
protected bool $shouldTranslateLabel = false;
/**
* @deprecated Use `hiddenLabel()` instead.
*/
public function disableLabel(bool | Closure $condition = true): static
{
$this->hiddenLabel($condition);
return $this;
}
public function hiddenLabel(bool | Closure $condition = true): static
{
$this->isLabelHidden = $condition;
return $this;
}
public function label(string | Htmlable | Closure | null $label): static
{
$this->label = $label;
return $this;
}
public function translateLabel(bool $shouldTranslateLabel = true): static
{
$this->shouldTranslateLabel = $shouldTranslateLabel;
return $this;
}
public function getLabel(): string | Htmlable | null
{
$label = $this->evaluate($this->label);
return (is_string($label) && $this->shouldTranslateLabel) ?
__($label) :
$label;
}
public function isLabelHidden(): bool
{
return (bool) $this->evaluate($this->isLabelHidden);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasLoadingMessage
{
protected string | Closure | null $loadingMessage = null;
public function loadingMessage(string | Closure | null $message): static
{
$this->loadingMessage = $message;
return $this;
}
public function getLoadingMessage(): string
{
return $this->evaluate($this->loadingMessage) ?? __('filament-forms::components.select.loading_message');
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasMaxHeight
{
protected string | Closure | null $maxHeight = null;
public function maxHeight(string | Closure | null $height): static
{
$this->maxHeight = $height;
return $this;
}
public function getMaxHeight(): ?string
{
return $this->evaluate($this->maxHeight);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Support\Enums\MaxWidth;
trait HasMaxWidth
{
protected MaxWidth | string | Closure | null $maxWidth = null;
public function maxWidth(MaxWidth | string | Closure | null $width): static
{
$this->maxWidth = $width;
return $this;
}
public function getMaxWidth(): MaxWidth | string | null
{
return $this->evaluate($this->maxWidth);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Illuminate\Support\Arr;
trait HasMeta
{
/**
* @var array<string, mixed>
*/
protected array $meta = [];
public function meta(string $key, mixed $value): static
{
$this->meta[$key] = $value;
return $this;
}
/**
* @param string | array<string> | null $keys
*/
public function getMeta(string | array | null $keys = null): mixed
{
if (is_array($keys)) {
return Arr::only($this->meta, $keys);
}
if (is_string($keys)) {
return Arr::get($this->meta, $keys);
}
return $this->meta;
}
/**
* @param string | array<string> $keys
*/
public function hasMeta(string | array $keys): bool
{
return Arr::has($this->meta, $keys);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasMinHeight
{
protected string | Closure | null $minHeight = '11.25rem';
public function minHeight(string | Closure | null $height): static
{
$this->minHeight = $height;
return $this;
}
public function getMinHeight(): ?string
{
return $this->evaluate($this->minHeight);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Illuminate\Contracts\Support\Htmlable;
trait HasName
{
protected string $name;
public function name(string $name): static
{
$this->name = $name;
return $this;
}
public function getName(): string
{
return $this->name;
}
public function getLabel(): string | Htmlable | null
{
$label = parent::getLabel() ?? (string) str($this->getName())
->afterLast('.')
->kebab()
->replace(['-', '_'], ' ')
->ucfirst();
return (is_string($label) && $this->shouldTranslateLabel) ?
__($label) :
$label;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasNestedRecursiveValidationRules
{
/**
* @var array<mixed>
*/
protected array $nestedRecursiveValidationRules = [];
/**
* @param string | array<mixed> $rules
*/
public function nestedRecursiveRules(string | array $rules, bool | Closure $condition = true): static
{
if (is_string($rules)) {
$rules = explode('|', $rules);
}
$this->nestedRecursiveValidationRules = [
...$this->nestedRecursiveValidationRules,
...array_map(static fn (string | object $rule): array => [$rule, $condition], $rules),
];
return $this;
}
/**
* @return array<mixed>
*/
public function getNestedRecursiveValidationRules(): array
{
$rules = [];
foreach ($this->nestedRecursiveValidationRules as [$rule, $condition]) {
if (is_numeric($rule)) {
$rules[] = $this->evaluate($condition);
} elseif ($this->evaluate($condition)) {
$rules[] = $this->evaluate($rule);
}
}
return $rules;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Support\Contracts\HasLabel as LabelInterface;
use Illuminate\Contracts\Support\Arrayable;
use UnitEnum;
trait HasOptions
{
/**
* @var array<string | array<string>> | Arrayable | string | Closure | null
*/
protected array | Arrayable | string | Closure | null $options = null;
/**
* @param array<string | array<string>> | Arrayable | string | Closure | null $options
*/
public function options(array | Arrayable | string | Closure | null $options): static
{
$this->options = $options;
return $this;
}
/**
* @return array<string | array<string>>
*/
public function getOptions(): array
{
$options = $this->evaluate($this->options) ?? [];
if (
is_string($options) &&
enum_exists($enum = $options)
) {
if (is_a($enum, LabelInterface::class, allow_string: true)) {
return array_reduce($enum::cases(), function (array $carry, LabelInterface & UnitEnum $case): array {
$carry[$case?->value ?? $case->name] = $case->getLabel() ?? $case->name;
return $carry;
}, []);
}
return array_reduce($enum::cases(), function (array $carry, UnitEnum $case): array {
$carry[$case?->value ?? $case->name] = $case->name;
return $carry;
}, []);
}
if ($options instanceof Arrayable) {
$options = $options->toArray();
}
return $options;
}
public function hasDynamicOptions(): bool
{
return $this->options instanceof Closure;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasPivotData
{
/**
* @var array<string, mixed> | Closure
*/
protected array | Closure $pivotData = [];
/**
* @param array<string, mixed> | Closure $data
*/
public function pivotData(array | Closure $data): static
{
$this->pivotData = $data;
return $this;
}
/**
* @return array<string, mixed>
*/
public function getPivotData(): array
{
return $this->evaluate($this->pivotData) ?? [];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasPlaceholder
{
protected string | Closure | null $placeholder = null;
public function placeholder(string | Closure | null $placeholder): static
{
$this->placeholder = $placeholder;
return $this;
}
public function getPlaceholder(): ?string
{
return $this->evaluate($this->placeholder);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Illuminate\Contracts\View\View;
trait HasPreview
{
protected string | Closure | null $preview = null;
public function preview(string | Closure | null $preview): static
{
$this->preview = $preview;
return $this;
}
/**
* @param array<string, mixed> $data
*/
public function renderPreview(array $data): View
{
return view(
$this->evaluate($this->preview),
$data,
);
}
}

View File

@@ -0,0 +1,517 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
use Filament\Forms\Components\Component;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Livewire\Livewire;
use function Livewire\store;
trait HasState
{
protected ?Closure $afterStateHydrated = null;
/**
* @var array<Closure>
*/
protected array $afterStateUpdated = [];
protected ?Closure $beforeStateDehydrated = null;
protected mixed $defaultState = null;
protected ?Closure $dehydrateStateUsing = null;
protected ?Closure $mutateDehydratedStateUsing = null;
protected ?Closure $mutateStateForValidationUsing = null;
protected bool $hasDefaultState = false;
protected bool | Closure $isDehydrated = true;
protected bool | Closure $isDehydratedWhenHidden = false;
protected ?string $statePath = null;
protected string $cachedAbsoluteStatePath;
/**
* @var string | array<string> | Closure | null
*/
protected string | array | Closure | null $stripCharacters = null;
/**
* @var array<string>
*/
protected array $cachedStripCharacters;
public function afterStateHydrated(?Closure $callback): static
{
$this->afterStateHydrated = $callback;
return $this;
}
public function clearAfterStateUpdatedHooks(): static
{
$this->afterStateUpdated = [];
return $this;
}
public function afterStateUpdated(?Closure $callback): static
{
$this->afterStateUpdated[] = $callback;
return $this;
}
public function beforeStateDehydrated(?Closure $callback): static
{
$this->beforeStateDehydrated = $callback;
return $this;
}
public function callAfterStateHydrated(): static
{
if ($callback = $this->afterStateHydrated) {
$this->evaluate($callback);
}
return $this;
}
public function callAfterStateUpdated(): static
{
foreach ($this->afterStateUpdated as $callback) {
$runId = spl_object_id($callback) . md5(json_encode($this->getState()));
if (store($this)->has('executedAfterStateUpdatedCallbacks', iKey: $runId)) {
continue;
}
$this->callAfterStateUpdatedHook($callback);
store($this)->push('executedAfterStateUpdatedCallbacks', value: $runId, iKey: $runId);
}
return $this;
}
protected function callAfterStateUpdatedHook(Closure $hook): void
{
$this->evaluate($hook, [
'old' => $this->getOldState(),
]);
}
public function callBeforeStateDehydrated(): static
{
if ($callback = $this->beforeStateDehydrated) {
$this->evaluate($callback);
}
return $this;
}
public function default(mixed $state): static
{
$this->defaultState = $state;
$this->hasDefaultState = true;
return $this;
}
public function dehydrated(bool | Closure $condition = true): static
{
$this->isDehydrated = $condition;
return $this;
}
public function dehydratedWhenHidden(bool | Closure $condition = true): static
{
$this->isDehydratedWhenHidden = $condition;
return $this;
}
public function formatStateUsing(?Closure $callback): static
{
$this->afterStateHydrated(fn (Component $component) => $component->state($component->evaluate($callback)));
return $this;
}
/**
* @return array<string, mixed>
*/
public function getStateToDehydrate(): array
{
if ($callback = $this->dehydrateStateUsing) {
return [$this->getStatePath() => $this->evaluate($callback)];
}
return [$this->getStatePath() => $this->getState()];
}
/**
* @param array<string, mixed> $state
*/
public function dehydrateState(array &$state, bool $isDehydrated = true): void
{
if (! ($isDehydrated && $this->isDehydrated())) {
if ($this->hasStatePath()) {
Arr::forget($state, $this->getStatePath());
return;
}
// If the component is not dehydrated, but it has child components,
// we need to dehydrate the child component containers while
// informing them that they are not dehydrated, so that their
// child components get removed from the state.
foreach ($this->getChildComponentContainers() as $container) {
$container->dehydrateState($state, isDehydrated: false);
}
return;
}
if ($this->getStatePath(isAbsolute: false)) {
foreach ($this->getStateToDehydrate() as $key => $value) {
Arr::set($state, $key, $value);
}
}
foreach ($this->getChildComponentContainers() as $container) {
if ($container->isHidden()) {
continue;
}
$container->dehydrateState($state, $isDehydrated);
}
}
public function dehydrateStateUsing(?Closure $callback): static
{
$this->dehydrateStateUsing = $callback;
return $this;
}
/**
* @param array<string, mixed> | null $hydratedDefaultState
*/
public function hydrateState(?array &$hydratedDefaultState, bool $andCallHydrationHooks = true): void
{
$this->hydrateDefaultState($hydratedDefaultState);
foreach ($this->getChildComponentContainers(withHidden: true) as $container) {
$container->hydrateState($hydratedDefaultState, $andCallHydrationHooks);
}
if ($andCallHydrationHooks) {
$this->callAfterStateHydrated();
}
}
public function fill(): void
{
$defaults = [];
$this->hydrateDefaultState($defaults);
}
/**
* @param array<string, mixed> | null $hydratedDefaultState
*/
public function hydrateDefaultState(?array &$hydratedDefaultState): void
{
if ($hydratedDefaultState === null) {
$this->loadStateFromRelationships();
$state = $this->getState();
// Hydrate all arrayable state objects as arrays by converting
// them to collections, then using `toArray()`.
if (is_array($state) || $state instanceof Arrayable) {
$this->state(collect($state)->toArray());
}
return;
}
$statePath = $this->getStatePath();
if (Arr::has($hydratedDefaultState, $statePath)) {
return;
}
if (! $this->hasDefaultState()) {
$this->hasStatePath() && $this->state(null);
return;
}
$defaultState = $this->getDefaultState();
$this->state($defaultState);
Arr::set($hydratedDefaultState, $statePath, $defaultState);
}
public function fillStateWithNull(): void
{
if (! Arr::has((array) $this->getLivewire(), $this->getStatePath())) {
$this->state(null);
}
foreach ($this->getChildComponentContainers(withHidden: true) as $container) {
$container->fillStateWithNull();
}
}
public function mutateDehydratedState(mixed $state): mixed
{
$state = $this->stripCharactersFromState($state);
if (! $this->mutateDehydratedStateUsing) {
return $state;
}
return $this->evaluate(
$this->mutateDehydratedStateUsing,
['state' => $state],
);
}
public function mutateStateForValidation(mixed $state): mixed
{
$state = $this->stripCharactersFromState($state);
if (! $this->mutateStateForValidationUsing) {
return $state;
}
return $this->evaluate(
$this->mutateStateForValidationUsing,
['state' => $state],
);
}
protected function stripCharactersFromState(mixed $state): mixed
{
if (! is_string($state)) {
return $state;
}
$stripCharacters = $this->getStripCharacters();
if (empty($stripCharacters)) {
return $state;
}
return str_replace($stripCharacters, '', $state);
}
public function mutatesDehydratedState(): bool
{
return ($this->mutateDehydratedStateUsing instanceof Closure) || $this->hasStripCharacters();
}
public function mutatesStateForValidation(): bool
{
return ($this->mutateStateForValidationUsing instanceof Closure) || $this->hasStripCharacters();
}
public function hasStripCharacters(): bool
{
return filled($this->getStripCharacters());
}
public function mutateDehydratedStateUsing(?Closure $callback): static
{
$this->mutateDehydratedStateUsing = $callback;
return $this;
}
public function mutateStateForValidationUsing(?Closure $callback): static
{
$this->mutateStateForValidationUsing = $callback;
return $this;
}
public function state(mixed $state): static
{
$livewire = $this->getLivewire();
data_set($livewire, $this->getStatePath(), $this->evaluate($state));
return $this;
}
public function statePath(?string $path): static
{
$this->statePath = $path;
return $this;
}
public function getDefaultState(): mixed
{
return $this->evaluate($this->defaultState);
}
public function getState(): mixed
{
$state = data_get($this->getLivewire(), $this->getStatePath());
if (is_array($state)) {
return $state;
}
if (blank($state)) {
return null;
}
return $state;
}
public function getOldState(): mixed
{
if (! Livewire::isLivewireRequest()) {
return null;
}
$state = $this->getLivewire()->getOldFormState($this->getStatePath());
if (blank($state)) {
return null;
}
return $state;
}
public function getStatePath(bool $isAbsolute = true): string
{
if (! $isAbsolute) {
return $this->statePath ?? '';
}
if (isset($this->cachedAbsoluteStatePath)) {
return $this->cachedAbsoluteStatePath;
}
$pathComponents = [];
if ($containerStatePath = $this->getContainer()->getStatePath()) {
$pathComponents[] = $containerStatePath;
}
if ($this->hasStatePath()) {
$pathComponents[] = $this->statePath;
}
return $this->cachedAbsoluteStatePath = implode('.', $pathComponents);
}
public function hasStatePath(): bool
{
return filled($this->statePath);
}
protected function hasDefaultState(): bool
{
return $this->hasDefaultState;
}
public function isDehydrated(): bool
{
return (bool) $this->evaluate($this->isDehydrated);
}
public function isDehydratedWhenHidden(): bool
{
return (bool) $this->evaluate($this->isDehydratedWhenHidden);
}
public function isHiddenAndNotDehydrated(): bool
{
if (! $this->isHidden()) {
return false;
}
return ! $this->isDehydratedWhenHidden();
}
public function getGetCallback(): Get
{
return new Get($this);
}
public function getSetCallback(): Set
{
return new Set($this);
}
public function generateRelativeStatePath(string | Component $path, bool $isAbsolute = false): string
{
if ($path instanceof Component) {
return $path->getStatePath();
}
if ($isAbsolute) {
return $path;
}
$containerPath = $this->getContainer()->getStatePath();
while (str($path)->startsWith('../')) {
$containerPath = Str::contains($containerPath, '.') ?
(string) str($containerPath)->beforeLast('.') :
null;
$path = (string) str($path)->after('../');
}
if (blank($containerPath)) {
return $path;
}
return "{$containerPath}.{$path}";
}
protected function flushCachedAbsoluteStatePath(): void
{
unset($this->cachedAbsoluteStatePath);
}
/**
* @param string | array<string> | Closure | null $characters
*/
public function stripCharacters(string | array | Closure | null $characters): static
{
$this->stripCharacters = $characters;
return $this;
}
/**
* @return array<string>
*/
public function getStripCharacters(): array
{
return $this->cachedStripCharacters ??= Arr::wrap($this->evaluate($this->stripCharacters));
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasStep
{
protected int | float | string | Closure | null $step = null;
public function step(int | float | string | Closure | null $interval): static
{
$this->step = $interval;
return $this;
}
public function getStep(): int | float | string | null
{
return $this->evaluate($this->step);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasToggleColors
{
/**
* @var string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | Closure | null
*/
protected string | array | Closure | null $offColor = null;
/**
* @var string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | Closure | null
*/
protected string | array | Closure | null $onColor = null;
/**
* @param string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | Closure | null $color
*/
public function offColor(string | array | Closure | null $color): static
{
$this->offColor = $color;
return $this;
}
/**
* @param string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | Closure | null $color
*/
public function onColor(string | array | Closure | null $color): static
{
$this->onColor = $color;
return $this;
}
/**
* @return string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null
*/
public function getOffColor(): string | array | null
{
return $this->evaluate($this->offColor);
}
/**
* @return string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null
*/
public function getOnColor(): string | array | null
{
return $this->evaluate($this->onColor);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasToggleIcons
{
protected string | Closure | null $offIcon = null;
protected string | Closure | null $onIcon = null;
public function offIcon(string | Closure | null $icon): static
{
$this->offIcon = $icon;
return $this;
}
public function onIcon(string | Closure | null $icon): static
{
$this->onIcon = $icon;
return $this;
}
public function getOffIcon(): ?string
{
return $this->evaluate($this->offIcon);
}
public function getOnIcon(): ?string
{
return $this->evaluate($this->onIcon);
}
public function hasOffIcon(): bool
{
return (bool) $this->getOffIcon();
}
public function hasOnIcon(): bool
{
return (bool) $this->getOnIcon();
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait HasUploadingMessage
{
protected string | Closure | null $uploadingMessage = null;
public function uploadingMessage(string | Closure | null $message): static
{
$this->uploadingMessage = $message;
return $this;
}
public function getUploadingMessage(): string
{
return $this->evaluate($this->uploadingMessage) ?? __('filament::components/button.messages.uploading_file');
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait InteractsWithToolbarButtons
{
public function disableAllToolbarButtons(bool $condition = true): static
{
if ($condition) {
$this->toolbarButtons = [];
}
return $this;
}
/**
* @param array<string> $buttonsToDisable
*/
public function disableToolbarButtons(array $buttonsToDisable = []): static
{
$this->toolbarButtons = array_values(array_filter(
$this->getToolbarButtons(),
static fn ($button) => ! in_array($button, $buttonsToDisable),
));
return $this;
}
/**
* @param array<string> $buttonsToEnable
*/
public function enableToolbarButtons(array $buttonsToEnable = []): static
{
$this->toolbarButtons = [
...$this->getToolbarButtons(),
...$buttonsToEnable,
];
return $this;
}
/**
* @param array<string> | Closure $buttons
*/
public function toolbarButtons(array | Closure $buttons = []): static
{
$this->toolbarButtons = $buttons;
return $this;
}
/**
* @return array<string>
*/
public function getToolbarButtons(): array
{
return $this->evaluate($this->toolbarButtons);
}
/**
* @param string | array<string> $button
*/
public function hasToolbarButton(string | array $button): bool
{
if (is_array($button)) {
$buttons = $button;
return (bool) count(array_intersect($buttons, $this->getToolbarButtons()));
}
return in_array($button, $this->getToolbarButtons());
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Filament\Forms\Components\Concerns;
use Closure;
trait ListensToEvents
{
/**
* @var array<string, array<Closure>>
*/
protected array $listeners = [];
public function dispatchEvent(string $event, mixed ...$parameters): static
{
foreach ($this->getListeners($event) as $callback) {
$callback($this, ...$parameters);
}
return $this;
}
/**
* @param array<string, array<Closure>> $listeners
*/
public function registerListeners(array $listeners): static
{
foreach ($listeners as $event => $callbacks) {
$this->listeners[$event] = [
...$this->getListeners($event),
...$callbacks,
];
}
return $this;
}
/**
* @return array<string | int, array<Closure> | Closure>
*/
public function getListeners(?string $event = null): array
{
$listeners = $this->listeners;
if ($event) {
return $listeners[$event] ?? [];
}
return $listeners;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Filament\Forms\Components\Contracts;
interface CanBeLengthConstrained
{
public function getLength(): ?int;
public function getMaxLength(): ?int;
public function getMinLength(): ?int;
/**
* @return array<string>
*/
public function getLengthValidationRules(): array;
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Filament\Forms\Components\Contracts;
interface CanConcealComponents
{
public function canConcealComponents(): bool;
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Filament\Forms\Components\Contracts;
use Closure;
interface CanDisableOptions
{
public function disableOptionWhen(bool | Closure $callback): static;
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Filament\Forms\Components\Contracts;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphOne;
interface CanEntangleWithSingularRelationships
{
public function cachedExistingRecord(?Model $record): static;
public function clearCachedExistingRecord(): void;
public function fillFromRelationship(): void;
public function getCachedExistingRecord(): ?Model;
public function getRelatedModel(): ?string;
public function getRelationship(): BelongsTo | HasOne | MorphOne | null;
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
public function mutateRelationshipDataBeforeFill(array $data): array;
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
public function mutateRelationshipDataBeforeCreate(array $data): array;
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
public function mutateRelationshipDataBeforeSave(array $data): array;
}

Some files were not shown because too many files have changed in this diff Show More