mirror of
https://github.com/filamentphp/filament.git
synced 2026-10-31 11:04:41 +08:00
1090 lines
31 KiB
PHP
1090 lines
31 KiB
PHP
<?php
|
|
|
|
namespace Filament\Forms\Components;
|
|
|
|
use Closure;
|
|
use Filament\Forms\ComponentContainer;
|
|
use Filament\Forms\Components\Actions\Action;
|
|
use Filament\Forms\Contracts\HasForms;
|
|
use Filament\Support\Enums\ActionSize;
|
|
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\HasOneOrMany;
|
|
use Illuminate\Support\Str;
|
|
|
|
use function Filament\Forms\array_move_after;
|
|
use function Filament\Forms\array_move_before;
|
|
|
|
class Repeater extends Field implements Contracts\CanConcealComponents
|
|
{
|
|
use Concerns\CanBeCollapsed;
|
|
use Concerns\CanLimitItemsLength;
|
|
use Concerns\HasContainerGridLayout;
|
|
use Concerns\CanBeCloned;
|
|
|
|
protected string | Closure | null $addActionLabel = null;
|
|
|
|
protected bool | Closure $isAddable = true;
|
|
|
|
protected bool | Closure $isDeletable = true;
|
|
|
|
protected bool | Closure $isReorderable = true;
|
|
|
|
protected bool | Closure $isReorderableWithDragAndDrop = true;
|
|
|
|
protected bool | Closure $isReorderableWithButtons = false;
|
|
|
|
protected bool | Closure $isInset = false;
|
|
|
|
protected ?Collection $cachedExistingRecords = null;
|
|
|
|
protected string | Closure | null $orderColumn = null;
|
|
|
|
protected string | Closure | null $relationship = null;
|
|
|
|
protected string | Closure | null $itemLabel = null;
|
|
|
|
protected Field | Closure | null $simpleField = null;
|
|
|
|
protected ?Closure $modifyRelationshipQueryUsing = null;
|
|
|
|
protected ?Closure $modifyAddActionUsing = null;
|
|
|
|
protected ?Closure $modifyCloneActionUsing = null;
|
|
|
|
protected ?Closure $modifyDeleteActionUsing = null;
|
|
|
|
protected ?Closure $modifyMoveDownActionUsing = null;
|
|
|
|
protected ?Closure $modifyMoveUpActionUsing = null;
|
|
|
|
protected ?Closure $modifyReorderActionUsing = null;
|
|
|
|
protected ?Closure $modifyCollapseActionUsing = null;
|
|
|
|
protected ?Closure $modifyExpandActionUsing = null;
|
|
|
|
protected ?Closure $modifyCollapseAllActionUsing = null;
|
|
|
|
protected ?Closure $modifyExpandAllActionUsing = null;
|
|
|
|
protected ?Closure $mutateRelationshipDataBeforeCreateUsing = null;
|
|
|
|
protected ?Closure $mutateRelationshipDataBeforeFillUsing = null;
|
|
|
|
protected ?Closure $mutateRelationshipDataBeforeSaveUsing = null;
|
|
|
|
/**
|
|
* @var array<string, mixed> | null
|
|
*/
|
|
protected ?array $hydratedDefaultState = null;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->defaultItems(1);
|
|
|
|
$this->afterStateHydrated(static function (Repeater $component, ?array $state): void {
|
|
if (is_array($component->hydratedDefaultState)) {
|
|
$component->mergeHydratedDefaultStateWithChildComponentContainerState();
|
|
|
|
return;
|
|
}
|
|
|
|
$items = [];
|
|
|
|
$simpleField = $component->getSimpleField();
|
|
|
|
foreach ($state ?? [] as $itemData) {
|
|
$items[(string) Str::uuid()] = $simpleField ?
|
|
[$simpleField->getName() => $itemData] :
|
|
$itemData;
|
|
}
|
|
|
|
$component->state($items);
|
|
});
|
|
|
|
$this->registerActions([
|
|
fn (Repeater $component): Action => $component->getAddAction(),
|
|
fn (Repeater $component): Action => $component->getCloneAction(),
|
|
fn (Repeater $component): Action => $component->getCollapseAction(),
|
|
fn (Repeater $component): Action => $component->getCollapseAllAction(),
|
|
fn (Repeater $component): Action => $component->getDeleteAction(),
|
|
fn (Repeater $component): Action => $component->getExpandAction(),
|
|
fn (Repeater $component): Action => $component->getExpandAllAction(),
|
|
fn (Repeater $component): Action => $component->getMoveDownAction(),
|
|
fn (Repeater $component): Action => $component->getMoveUpAction(),
|
|
fn (Repeater $component): Action => $component->getReorderAction(),
|
|
]);
|
|
|
|
$this->mutateDehydratedStateUsing(static function (Repeater $component, ?array $state): array {
|
|
if ($simpleField = $component->getSimpleField()) {
|
|
return collect($state ?? [])
|
|
->values()
|
|
->pluck($simpleField->getName())
|
|
->all();
|
|
}
|
|
|
|
return array_values($state ?? []);
|
|
});
|
|
}
|
|
|
|
public function getAddAction(): Action
|
|
{
|
|
$action = Action::make($this->getAddActionName())
|
|
->label(fn (Repeater $component) => $component->getAddActionLabel())
|
|
->color('gray')
|
|
->action(function (Repeater $component): void {
|
|
$newUuid = (string) Str::uuid();
|
|
|
|
$items = $component->getState();
|
|
$items[$newUuid] = [];
|
|
|
|
$component->state($items);
|
|
|
|
$component->getChildComponentContainers()[$newUuid]->fill();
|
|
|
|
$component->collapsed(false, shouldMakeComponentCollapsible: false);
|
|
|
|
$component->callAfterStateUpdated();
|
|
})
|
|
->button()
|
|
->size(ActionSize::Small)
|
|
->visible(fn (): bool => $this->isAddable());
|
|
|
|
if ($this->modifyAddActionUsing) {
|
|
$action = $this->evaluate($this->modifyAddActionUsing, [
|
|
'action' => $action,
|
|
]) ?? $action;
|
|
}
|
|
|
|
return $action;
|
|
}
|
|
|
|
public function addAction(?Closure $callback): static
|
|
{
|
|
$this->modifyAddActionUsing = $callback;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getAddActionName(): string
|
|
{
|
|
return 'add';
|
|
}
|
|
|
|
public function getCloneAction(): Action
|
|
{
|
|
$action = Action::make($this->getCloneActionName())
|
|
->label(__('filament-forms::components.repeater.actions.clone.label'))
|
|
->icon('heroicon-m-square-2-stack')
|
|
->color('gray')
|
|
->action(function (array $arguments, Repeater $component): void {
|
|
$newUuid = (string) Str::uuid();
|
|
|
|
$items = $component->getState();
|
|
$items[$newUuid] = $items[$arguments['item']];
|
|
|
|
$component->state($items);
|
|
|
|
$component->collapsed(false, shouldMakeComponentCollapsible: false);
|
|
|
|
$component->callAfterStateUpdated();
|
|
})
|
|
->iconButton()
|
|
->size(ActionSize::Small)
|
|
->visible(fn (): bool => $this->isCloneable());
|
|
|
|
if ($this->modifyCloneActionUsing) {
|
|
$action = $this->evaluate($this->modifyCloneActionUsing, [
|
|
'action' => $action,
|
|
]) ?? $action;
|
|
}
|
|
|
|
return $action;
|
|
}
|
|
|
|
public function cloneAction(?Closure $callback): static
|
|
{
|
|
$this->modifyCloneActionUsing = $callback;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getCloneActionName(): string
|
|
{
|
|
return 'clone';
|
|
}
|
|
|
|
public function getDeleteAction(): Action
|
|
{
|
|
$action = Action::make($this->getDeleteActionName())
|
|
->label(__('filament-forms::components.repeater.actions.delete.label'))
|
|
->icon('heroicon-m-trash')
|
|
->color('danger')
|
|
->action(function (array $arguments, Repeater $component): void {
|
|
$items = $component->getState();
|
|
unset($items[$arguments['item']]);
|
|
|
|
$component->state($items);
|
|
|
|
$component->callAfterStateUpdated();
|
|
})
|
|
->iconButton()
|
|
->size(ActionSize::Small)
|
|
->visible(fn (): bool => $this->isDeletable());
|
|
|
|
if ($this->modifyDeleteActionUsing) {
|
|
$action = $this->evaluate($this->modifyDeleteActionUsing, [
|
|
'action' => $action,
|
|
]) ?? $action;
|
|
}
|
|
|
|
return $action;
|
|
}
|
|
|
|
public function deleteAction(?Closure $callback): static
|
|
{
|
|
$this->modifyDeleteActionUsing = $callback;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getDeleteActionName(): string
|
|
{
|
|
return 'delete';
|
|
}
|
|
|
|
public function getMoveDownAction(): Action
|
|
{
|
|
$action = Action::make($this->getMoveDownActionName())
|
|
->label(__('filament-forms::components.repeater.actions.move_down.label'))
|
|
->icon('heroicon-m-arrow-down')
|
|
->color('gray')
|
|
->action(function (array $arguments, Repeater $component): void {
|
|
$items = array_move_after($component->getState(), $arguments['item']);
|
|
|
|
$component->state($items);
|
|
|
|
$component->callAfterStateUpdated();
|
|
})
|
|
->iconButton()
|
|
->size(ActionSize::Small)
|
|
->visible(fn (): bool => $this->isReorderable());
|
|
|
|
if ($this->modifyMoveDownActionUsing) {
|
|
$action = $this->evaluate($this->modifyMoveDownActionUsing, [
|
|
'action' => $action,
|
|
]) ?? $action;
|
|
}
|
|
|
|
return $action;
|
|
}
|
|
|
|
public function moveDownAction(?Closure $callback): static
|
|
{
|
|
$this->modifyMoveDownActionUsing = $callback;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getMoveDownActionName(): string
|
|
{
|
|
return 'moveDown';
|
|
}
|
|
|
|
public function getMoveUpAction(): Action
|
|
{
|
|
$action = Action::make($this->getMoveUpActionName())
|
|
->label(__('filament-forms::components.repeater.actions.move_up.label'))
|
|
->icon('heroicon-m-arrow-up')
|
|
->color('gray')
|
|
->action(function (array $arguments, Repeater $component): void {
|
|
$items = array_move_before($component->getState(), $arguments['item']);
|
|
|
|
$component->state($items);
|
|
|
|
$component->callAfterStateUpdated();
|
|
})
|
|
->iconButton()
|
|
->size(ActionSize::Small)
|
|
->visible(fn (): bool => $this->isReorderable());
|
|
|
|
if ($this->modifyMoveUpActionUsing) {
|
|
$action = $this->evaluate($this->modifyMoveUpActionUsing, [
|
|
'action' => $action,
|
|
]) ?? $action;
|
|
}
|
|
|
|
return $action;
|
|
}
|
|
|
|
public function moveUpAction(?Closure $callback): static
|
|
{
|
|
$this->modifyMoveUpActionUsing = $callback;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getMoveUpActionName(): string
|
|
{
|
|
return 'moveUp';
|
|
}
|
|
|
|
public function getReorderAction(): Action
|
|
{
|
|
$action = Action::make($this->getReorderActionName())
|
|
->label(__('filament-forms::components.repeater.actions.reorder.label'))
|
|
->icon('heroicon-m-arrows-up-down')
|
|
->color('gray')
|
|
->action(function (array $arguments, Repeater $component): void {
|
|
$items = [
|
|
...array_flip($arguments['items']),
|
|
...$component->getState(),
|
|
];
|
|
|
|
$component->state($items);
|
|
|
|
$component->callAfterStateUpdated();
|
|
})
|
|
->livewireClickHandlerEnabled(false)
|
|
->iconButton()
|
|
->size(ActionSize::Small)
|
|
->visible(fn (): bool => $this->isReorderableWithDragAndDrop());
|
|
|
|
if ($this->modifyReorderActionUsing) {
|
|
$action = $this->evaluate($this->modifyReorderActionUsing, [
|
|
'action' => $action,
|
|
]) ?? $action;
|
|
}
|
|
|
|
return $action;
|
|
}
|
|
|
|
public function reorderAction(?Closure $callback): static
|
|
{
|
|
$this->modifyReorderActionUsing = $callback;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getReorderActionName(): string
|
|
{
|
|
return 'reorder';
|
|
}
|
|
|
|
public function getCollapseAction(): Action
|
|
{
|
|
$action = Action::make($this->getCollapseActionName())
|
|
->label(__('filament-forms::components.repeater.actions.collapse.label'))
|
|
->icon('heroicon-m-chevron-up')
|
|
->color('gray')
|
|
->livewireClickHandlerEnabled(false)
|
|
->iconButton()
|
|
->size(ActionSize::Small);
|
|
|
|
if ($this->modifyCollapseActionUsing) {
|
|
$action = $this->evaluate($this->modifyCollapseActionUsing, [
|
|
'action' => $action,
|
|
]) ?? $action;
|
|
}
|
|
|
|
return $action;
|
|
}
|
|
|
|
public function collapseAction(?Closure $callback): static
|
|
{
|
|
$this->modifyCollapseActionUsing = $callback;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getCollapseActionName(): string
|
|
{
|
|
return 'collapse';
|
|
}
|
|
|
|
public function getExpandAction(): Action
|
|
{
|
|
$action = Action::make($this->getExpandActionName())
|
|
->label(__('filament-forms::components.repeater.actions.expand.label'))
|
|
->icon('heroicon-m-chevron-down')
|
|
->color('gray')
|
|
->livewireClickHandlerEnabled(false)
|
|
->iconButton()
|
|
->size(ActionSize::Small);
|
|
|
|
if ($this->modifyExpandActionUsing) {
|
|
$action = $this->evaluate($this->modifyExpandActionUsing, [
|
|
'action' => $action,
|
|
]) ?? $action;
|
|
}
|
|
|
|
return $action;
|
|
}
|
|
|
|
public function expandAction(?Closure $callback): static
|
|
{
|
|
$this->modifyExpandActionUsing = $callback;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getExpandActionName(): string
|
|
{
|
|
return 'expand';
|
|
}
|
|
|
|
public function getCollapseAllAction(): Action
|
|
{
|
|
$action = Action::make($this->getCollapseAllActionName())
|
|
->label(__('filament-forms::components.repeater.actions.collapse_all.label'))
|
|
->color('gray')
|
|
->livewireClickHandlerEnabled(false)
|
|
->link()
|
|
->size(ActionSize::Small);
|
|
|
|
if ($this->modifyCollapseAllActionUsing) {
|
|
$action = $this->evaluate($this->modifyCollapseAllActionUsing, [
|
|
'action' => $action,
|
|
]) ?? $action;
|
|
}
|
|
|
|
return $action;
|
|
}
|
|
|
|
public function collapseAllAction(?Closure $callback): static
|
|
{
|
|
$this->modifyCollapseAllActionUsing = $callback;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getCollapseAllActionName(): string
|
|
{
|
|
return 'collapseAll';
|
|
}
|
|
|
|
public function getExpandAllAction(): Action
|
|
{
|
|
$action = Action::make($this->getExpandAllActionName())
|
|
->label(__('filament-forms::components.repeater.actions.expand_all.label'))
|
|
->color('gray')
|
|
->livewireClickHandlerEnabled(false)
|
|
->link()
|
|
->size(ActionSize::Small);
|
|
|
|
if ($this->modifyExpandAllActionUsing) {
|
|
$action = $this->evaluate($this->modifyExpandAllActionUsing, [
|
|
'action' => $action,
|
|
]) ?? $action;
|
|
}
|
|
|
|
return $action;
|
|
}
|
|
|
|
public function expandAllAction(?Closure $callback): static
|
|
{
|
|
$this->modifyExpandAllActionUsing = $callback;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getExpandAllActionName(): string
|
|
{
|
|
return 'expandAll';
|
|
}
|
|
|
|
public function addActionLabel(string | Closure | null $label): static
|
|
{
|
|
$this->addActionLabel = $label;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use `addActionLabel()` instead.
|
|
*/
|
|
public function createItemButtonLabel(string | Closure | null $label): static
|
|
{
|
|
$this->addActionLabel($label);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function defaultItems(int | Closure $count): static
|
|
{
|
|
$this->default(static function (Repeater $component) use ($count): array {
|
|
$count = $component->evaluate($count);
|
|
|
|
if (! $count) {
|
|
return [];
|
|
}
|
|
|
|
return array_fill(0, $count, $component->isSimple() ? null : []);
|
|
}, shouldPreserveChildState: false);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function default(mixed $state, bool $shouldPreserveChildState = true): static
|
|
{
|
|
parent::default(function (Repeater $component) use ($shouldPreserveChildState, $state) {
|
|
$state = $component->evaluate($state);
|
|
|
|
$simpleField = $component->getSimpleField();
|
|
|
|
$items = [];
|
|
|
|
foreach ($state ?? [] as $itemData) {
|
|
$items[(string) Str::uuid()] = $simpleField ?
|
|
[$simpleField->getName() => $itemData] :
|
|
$itemData;
|
|
}
|
|
|
|
if ($shouldPreserveChildState) {
|
|
$component->hydratedDefaultState = $items;
|
|
}
|
|
|
|
return $items;
|
|
});
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function addable(bool | Closure $condition = true): static
|
|
{
|
|
$this->isAddable = $condition;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function deletable(bool | Closure $condition = true): static
|
|
{
|
|
$this->isDeletable = $condition;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function reorderable(bool | Closure $condition = true): static
|
|
{
|
|
$this->isReorderable = $condition;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use `addable()` instead.
|
|
*/
|
|
public function disableItemCreation(bool | Closure $condition = true): static
|
|
{
|
|
$this->addable(fn (Repeater $component): bool => ! $this->evaluate($condition));
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use `deletable()` instead.
|
|
*/
|
|
public function disableItemDeletion(bool | Closure $condition = true): static
|
|
{
|
|
$this->deletable(fn (Repeater $component): bool => ! $this->evaluate($condition));
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use `reorderable()` instead.
|
|
*/
|
|
public function disableItemMovement(bool | Closure $condition = true): static
|
|
{
|
|
$this->reorderable(fn (Repeater $component): bool => ! $this->evaluate($condition));
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function reorderableWithDragAndDrop(bool | Closure $condition = true): static
|
|
{
|
|
$this->isReorderableWithDragAndDrop = $condition;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function reorderableWithButtons(bool | Closure $condition = true): static
|
|
{
|
|
$this->isReorderableWithButtons = $condition;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @deprecated No longer part of the design system.
|
|
*/
|
|
public function inset(bool | Closure $condition = true): static
|
|
{
|
|
return $this;
|
|
}
|
|
|
|
public function getChildComponents(): array
|
|
{
|
|
if ($simpleField = $this->getSimpleField()) {
|
|
return [$simpleField];
|
|
}
|
|
|
|
return parent::getChildComponents();
|
|
}
|
|
|
|
/**
|
|
* @return array<ComponentContainer>
|
|
*/
|
|
public function getChildComponentContainers(bool $withHidden = false): array
|
|
{
|
|
$relationship = $this->getRelationship();
|
|
|
|
$records = $relationship ? $this->getCachedExistingRecords() : null;
|
|
|
|
$containers = [];
|
|
|
|
foreach ($this->getState() ?? [] as $itemKey => $itemData) {
|
|
$containers[$itemKey] = $this
|
|
->getChildComponentContainer()
|
|
->statePath($itemKey)
|
|
->model($relationship ? $records[$itemKey] ?? $this->getRelatedModel() : null)
|
|
->inlineLabel(false)
|
|
->getClone();
|
|
}
|
|
|
|
return $containers;
|
|
}
|
|
|
|
public function getAddActionLabel(): string
|
|
{
|
|
return $this->evaluate($this->addActionLabel) ?? __('filament-forms::components.repeater.actions.add.label', [
|
|
'label' => Str::lcfirst($this->getLabel()),
|
|
]);
|
|
}
|
|
|
|
public function isReorderable(): bool
|
|
{
|
|
if ($this->isDisabled()) {
|
|
return false;
|
|
}
|
|
|
|
return (bool) $this->evaluate($this->isReorderable);
|
|
}
|
|
|
|
public function isReorderableWithDragAndDrop(): bool
|
|
{
|
|
return $this->evaluate($this->isReorderableWithDragAndDrop) && $this->isReorderable();
|
|
}
|
|
|
|
public function isReorderableWithButtons(): bool
|
|
{
|
|
return $this->evaluate($this->isReorderableWithButtons) && $this->isReorderable();
|
|
}
|
|
|
|
public function isAddable(): bool
|
|
{
|
|
if ($this->isDisabled()) {
|
|
return false;
|
|
}
|
|
|
|
if (filled($this->getMaxItems()) && ($this->getMaxItems() <= $this->getItemsCount())) {
|
|
return false;
|
|
}
|
|
|
|
return (bool) $this->evaluate($this->isAddable);
|
|
}
|
|
|
|
public function isDeletable(): bool
|
|
{
|
|
if ($this->isDisabled()) {
|
|
return false;
|
|
}
|
|
|
|
return (bool) $this->evaluate($this->isDeletable);
|
|
}
|
|
|
|
public function orderColumn(string | Closure | null $column = 'sort'): static
|
|
{
|
|
$this->orderColumn = $column;
|
|
$this->reorderable($column);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use `orderColumn()` instead.
|
|
*/
|
|
public function orderable(string | Closure | null $column = 'sort'): static
|
|
{
|
|
$this->orderColumn($column);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function relationship(string | Closure | null $name = null, ?Closure $modifyQueryUsing = null): static
|
|
{
|
|
$this->relationship = $name ?? $this->getName();
|
|
$this->modifyRelationshipQueryUsing = $modifyQueryUsing;
|
|
|
|
$this->afterStateHydrated(function (Repeater $component) {
|
|
if (! is_array($component->hydratedDefaultState)) {
|
|
return;
|
|
}
|
|
|
|
$component->mergeHydratedDefaultStateWithChildComponentContainerState();
|
|
});
|
|
|
|
$this->loadStateFromRelationshipsUsing(static function (Repeater $component) {
|
|
$component->clearCachedExistingRecords();
|
|
|
|
$component->fillFromRelationship();
|
|
});
|
|
|
|
$this->saveRelationshipsUsing(static function (Repeater $component, HasForms $livewire, ?array $state) {
|
|
if (! is_array($state)) {
|
|
$state = [];
|
|
}
|
|
|
|
$relationship = $component->getRelationship();
|
|
|
|
$existingRecords = $component->getCachedExistingRecords();
|
|
|
|
$recordsToDelete = [];
|
|
|
|
foreach ($existingRecords->pluck($relationship->getRelated()->getKeyName()) as $keyToCheckForDeletion) {
|
|
if (array_key_exists("record-{$keyToCheckForDeletion}", $state)) {
|
|
continue;
|
|
}
|
|
|
|
$recordsToDelete[] = $keyToCheckForDeletion;
|
|
}
|
|
|
|
$relationship
|
|
->whereKey($recordsToDelete)
|
|
->get()
|
|
->each(static fn (Model $record) => $record->delete());
|
|
|
|
$childComponentContainers = $component->getChildComponentContainers();
|
|
|
|
$itemOrder = 1;
|
|
$orderColumn = $component->getOrderColumn();
|
|
|
|
$translatableContentDriver = $livewire->makeFilamentTranslatableContentDriver();
|
|
|
|
foreach ($childComponentContainers as $itemKey => $item) {
|
|
$itemData = $item->getState(shouldCallHooksBefore: false);
|
|
|
|
if ($orderColumn) {
|
|
$itemData[$orderColumn] = $itemOrder;
|
|
|
|
$itemOrder++;
|
|
}
|
|
|
|
if ($record = ($existingRecords[$itemKey] ?? null)) {
|
|
$itemData = $component->mutateRelationshipDataBeforeSave($itemData, record: $record);
|
|
|
|
if ($itemData === null) {
|
|
continue;
|
|
}
|
|
|
|
$translatableContentDriver ?
|
|
$translatableContentDriver->updateRecord($record, $itemData) :
|
|
$record->fill($itemData)->save();
|
|
|
|
continue;
|
|
}
|
|
|
|
$relatedModel = $component->getRelatedModel();
|
|
|
|
$itemData = $component->mutateRelationshipDataBeforeCreate($itemData);
|
|
|
|
if ($itemData === null) {
|
|
continue;
|
|
}
|
|
|
|
if ($translatableContentDriver) {
|
|
$record = $translatableContentDriver->makeRecord($relatedModel, $itemData);
|
|
} else {
|
|
$record = new $relatedModel();
|
|
$record->fill($itemData);
|
|
}
|
|
|
|
$record = $relationship->save($record);
|
|
$item->model($record)->saveRelationships();
|
|
}
|
|
});
|
|
|
|
$this->dehydrated(false);
|
|
|
|
$this->disableItemMovement();
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* After hydrating the state of child component containers, the default state
|
|
* of fields inside the repeater can be lost, if it was defined on the repeater
|
|
* itself. This method merges the hydrated default state with the state of the
|
|
* child component containers, so that the default state of the fields inside
|
|
* the repeater is preserved.
|
|
*/
|
|
protected function mergeHydratedDefaultStateWithChildComponentContainerState(): void
|
|
{
|
|
$state = $this->getState();
|
|
$items = $this->hydratedDefaultState;
|
|
|
|
foreach ($items as $itemKey => $itemData) {
|
|
$items[$itemKey] = [
|
|
...$state[$itemKey] ?? [],
|
|
...$itemData,
|
|
];
|
|
}
|
|
|
|
$this->state($items);
|
|
}
|
|
|
|
public function itemLabel(string | Closure | null $label): static
|
|
{
|
|
$this->itemLabel = $label;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function fillFromRelationship(): void
|
|
{
|
|
$this->state(
|
|
$this->getStateFromRelatedRecords($this->getCachedExistingRecords()),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<array<string, mixed>>
|
|
*/
|
|
protected function getStateFromRelatedRecords(Collection $records): array
|
|
{
|
|
if (! $records->count()) {
|
|
return [];
|
|
}
|
|
|
|
$translatableContentDriver = $this->getLivewire()->makeFilamentTranslatableContentDriver();
|
|
|
|
return $records
|
|
->map(function (Model $record) use ($translatableContentDriver): array {
|
|
$data = $translatableContentDriver ?
|
|
$translatableContentDriver->getRecordAttributesToArray($record) :
|
|
$record->attributesToArray();
|
|
|
|
return $this->mutateRelationshipDataBeforeFill($data);
|
|
})
|
|
->toArray();
|
|
}
|
|
|
|
public function getLabel(): string | Htmlable | null
|
|
{
|
|
if ($this->label === null && $this->hasRelationship()) {
|
|
$label = (string) str($this->getRelationshipName())
|
|
->before('.')
|
|
->kebab()
|
|
->replace(['-', '_'], ' ')
|
|
->ucfirst();
|
|
|
|
return ($this->shouldTranslateLabel) ? __($label) : $label;
|
|
}
|
|
|
|
return parent::getLabel();
|
|
}
|
|
|
|
public function getOrderColumn(): ?string
|
|
{
|
|
return $this->evaluate($this->orderColumn);
|
|
}
|
|
|
|
public function getRelationship(): HasOneOrMany | BelongsToMany | null
|
|
{
|
|
if (! $this->hasRelationship()) {
|
|
return null;
|
|
}
|
|
|
|
return $this->getModelInstance()->{$this->getRelationshipName()}();
|
|
}
|
|
|
|
public function getRelationshipName(): ?string
|
|
{
|
|
return $this->evaluate($this->relationship);
|
|
}
|
|
|
|
public function getCachedExistingRecords(): Collection
|
|
{
|
|
if ($this->cachedExistingRecords) {
|
|
return $this->cachedExistingRecords;
|
|
}
|
|
|
|
$relationship = $this->getRelationship();
|
|
$relationshipQuery = $relationship->getQuery();
|
|
|
|
if ($relationship instanceof BelongsToMany) {
|
|
$relationshipQuery->select([
|
|
$relationship->getTable() . '.*',
|
|
$relationshipQuery->getModel()->getTable() . '.*',
|
|
]);
|
|
}
|
|
|
|
if ($this->modifyRelationshipQueryUsing) {
|
|
$relationshipQuery = $this->evaluate($this->modifyRelationshipQueryUsing, [
|
|
'query' => $relationshipQuery,
|
|
]) ?? $relationshipQuery;
|
|
}
|
|
|
|
if ($orderColumn = $this->getOrderColumn()) {
|
|
$relationshipQuery->orderBy($orderColumn);
|
|
}
|
|
|
|
$relatedKeyName = $relationship->getRelated()->getKeyName();
|
|
|
|
return $this->cachedExistingRecords = $relationshipQuery->get()->mapWithKeys(
|
|
fn (Model $item): array => ["record-{$item[$relatedKeyName]}" => $item],
|
|
);
|
|
}
|
|
|
|
public function getItemLabel(string $uuid): string | Htmlable | null
|
|
{
|
|
return $this->evaluate($this->itemLabel, [
|
|
'state' => $this->getChildComponentContainer($uuid)->getRawState(),
|
|
'uuid' => $uuid,
|
|
]);
|
|
}
|
|
|
|
public function hasItemLabels(): bool
|
|
{
|
|
return $this->itemLabel !== null;
|
|
}
|
|
|
|
public function simple(Field | Closure | null $field): static
|
|
{
|
|
$this->simpleField = $field;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function isSimple(): bool
|
|
{
|
|
return $this->simpleField !== null;
|
|
}
|
|
|
|
public function getSimpleField(): ?Field
|
|
{
|
|
return $this->evaluate($this->simpleField)?->hiddenLabel();
|
|
}
|
|
|
|
public function clearCachedExistingRecords(): void
|
|
{
|
|
$this->cachedExistingRecords = null;
|
|
}
|
|
|
|
public function getRelatedModel(): string
|
|
{
|
|
return $this->getRelationship()->getModel()::class;
|
|
}
|
|
|
|
public function hasRelationship(): bool
|
|
{
|
|
return filled($this->getRelationshipName());
|
|
}
|
|
|
|
public function mutateRelationshipDataBeforeCreateUsing(?Closure $callback): static
|
|
{
|
|
$this->mutateRelationshipDataBeforeCreateUsing = $callback;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param array<array<string, mixed>> $data
|
|
* @return array<array<string, mixed>> | null
|
|
*/
|
|
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<array<string, mixed>> $data
|
|
* @return array<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<array<string, mixed>> $data
|
|
* @return array<array<string, mixed>> | null
|
|
*/
|
|
public function mutateRelationshipDataBeforeSave(array $data, Model $record): ?array
|
|
{
|
|
if ($this->mutateRelationshipDataBeforeSaveUsing instanceof Closure) {
|
|
$data = $this->evaluate(
|
|
$this->mutateRelationshipDataBeforeSaveUsing,
|
|
namedInjections: [
|
|
'data' => $data,
|
|
'record' => $record,
|
|
],
|
|
typedInjections: [
|
|
Model::class => $record,
|
|
$record::class => $record,
|
|
],
|
|
);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
public function canConcealComponents(): bool
|
|
{
|
|
return $this->isCollapsible();
|
|
}
|
|
|
|
/**
|
|
* @return view-string
|
|
*/
|
|
public function getDefaultView(): string
|
|
{
|
|
if ($this->isSimple()) {
|
|
return 'filament-forms::components.simple-repeater';
|
|
}
|
|
|
|
return 'filament-forms::components.repeater';
|
|
}
|
|
}
|