| Closure | null */ protected array | Closure | null $recordSelectSearchColumns = null; protected bool | Closure | null $isSearchForcedCaseInsensitive = null; protected bool | Closure $isMultiple = false; public static function getDefaultName(): ?string { return 'associate'; } protected function setUp(): void { parent::setUp(); $this->label(__('filament-actions::associate.single.label')); $this->modalHeading(fn (): string => __('filament-actions::associate.single.modal.heading', ['label' => $this->getModelLabel()])); $this->modalSubmitActionLabel(__('filament-actions::associate.single.modal.actions.associate.label')); $this->modalWidth(MaxWidth::Large); $this->extraModalFooterActions(function (): array { return $this->canAssociateAnother ? [ $this->makeModalSubmitAction('associateAnother', arguments: ['another' => true]) ->label(__('filament-actions::associate.single.modal.actions.associate_another.label')), ] : []; }); $this->successNotificationTitle(__('filament-actions::associate.single.notifications.associated.title')); $this->defaultColor('gray'); $this->form(fn (): array => [$this->getRecordSelect()]); $this->action(function (array $arguments, array $data, Form $form, Table $table): void { /** @var HasMany | MorphMany $relationship */ $relationship = Relation::noConstraints(fn () => $table->getRelationship()); $record = $relationship->getQuery()->find($data['recordId']); foreach (($this->isMultiple ? $record : [$record]) as $record) { if ($record instanceof Model) { $this->record($record); } /** @var BelongsTo $inverseRelationship */ $inverseRelationship = $table->getInverseRelationshipFor($record); $this->process(function () use ($inverseRelationship, $record, $relationship) { $inverseRelationship->associate($relationship->getParent()); $record->save(); }, [ 'inverseRelationship' => $inverseRelationship, 'relationship' => $relationship, ]); } if ($arguments['another'] ?? false) { $this->callAfter(); $this->sendSuccessNotification(); $this->record(null); $form->fill(); $this->halt(); return; } $this->success(); }); } public function recordSelect(?Closure $callback): static { $this->modifyRecordSelectUsing = $callback; return $this; } public function recordSelectOptionsQuery(?Closure $callback): static { $this->modifyRecordSelectOptionsQueryUsing = $callback; return $this; } public function associateAnother(bool | Closure $condition = true): static { $this->canAssociateAnother = $condition; return $this; } /** * @deprecated Use `associateAnother()` instead. */ public function disableAssociateAnother(bool | Closure $condition = true): static { $this->associateAnother(fn (AssociateAction $action): bool => ! $action->evaluate($condition)); return $this; } public function preloadRecordSelect(bool | Closure $condition = true): static { $this->isRecordSelectPreloaded = $condition; return $this; } public function canAssociateAnother(): bool { return (bool) $this->evaluate($this->canAssociateAnother); } public function isRecordSelectPreloaded(): bool { return (bool) $this->evaluate($this->isRecordSelectPreloaded); } /** * @param array | Closure | null $columns */ public function recordSelectSearchColumns(array | Closure | null $columns): static { $this->recordSelectSearchColumns = $columns; return $this; } /** * @return array | null */ public function getRecordSelectSearchColumns(): ?array { return $this->evaluate($this->recordSelectSearchColumns); } public function multiple(bool | Closure $condition = true): static { $this->isMultiple = $condition; return $this; } public function isMultiple(): bool { return (bool) $this->evaluate($this->isMultiple); } public function getRecordSelect(): Select { $table = $this->getTable(); $getOptions = function (int $optionsLimit, ?string $search = null, ?array $searchColumns = []) use ($table): array { /** @var HasMany | MorphMany $relationship */ $relationship = Relation::noConstraints(fn () => $table->getRelationship()); $relationshipQuery = $relationship->getQuery(); if ($this->modifyRecordSelectOptionsQueryUsing) { $relationshipQuery = $this->evaluate($this->modifyRecordSelectOptionsQueryUsing, [ 'query' => $relationshipQuery, 'search' => $search, ]) ?? $relationshipQuery; } if (! isset($relationshipQuery->getQuery()->limit)) { $relationshipQuery->limit($optionsLimit); } $titleAttribute = $this->getRecordTitleAttribute(); $titleAttribute = filled($titleAttribute) ? $relationshipQuery->qualifyColumn($titleAttribute) : null; if (filled($search) && ($searchColumns || filled($titleAttribute))) { /** @var Connection $databaseConnection */ $databaseConnection = $relationshipQuery->getConnection(); $isForcedCaseInsensitive = $this->isSearchForcedCaseInsensitive(); $search = generate_search_term_expression($search, $isForcedCaseInsensitive, $databaseConnection); $searchColumns ??= [$titleAttribute]; $isFirst = true; $relationshipQuery->where(function (Builder $query) use ($databaseConnection, $isFirst, $isForcedCaseInsensitive, $searchColumns, $search): Builder { foreach ($searchColumns as $searchColumn) { $whereClause = $isFirst ? 'where' : 'orWhere'; $query->{$whereClause}( generate_search_column_expression($query->qualifyColumn($searchColumn), $isForcedCaseInsensitive, $databaseConnection), 'like', "%{$search}%", ); $isFirst = false; } return $query; }); } $relationCountHash = $relationship->getRelationCountHash(incrementJoinCount: false); if ($relationship instanceof MorphMany) { $relationshipQuery->whereNotMorphedTo($table->getInverseRelationship(), $relationship->getParent()); } else { $relationshipQuery ->whereDoesntHave($table->getInverseRelationship(), fn (Builder $query): Builder => $query->where( // https://github.com/filamentphp/filament/issues/8067 $relationship->getParent()->getTable() === $relationship->getRelated()->getTable() ? "{$relationCountHash}.{$relationship->getParent()->getKeyName()}" : $relationship->getParent()->getQualifiedKeyName(), $relationship->getParent()->getKey(), )); } if ( filled($titleAttribute) && (! $this->hasCustomRecordTitle()) && ($this->hasCustomRecordTitleAttribute() || (! $this->getTable()->hasCustomRecordTitle())) ) { if (empty($relationshipQuery->getQuery()->orders)) { $relationshipQuery->orderBy($titleAttribute); } return $relationshipQuery ->pluck($titleAttribute, $relationship->getModel()->getQualifiedKeyName()) ->all(); } return $relationshipQuery ->get() ->mapWithKeys(fn (Model $record): array => [$record->getKey() => $this->getRecordTitle($record)]) ->all(); }; $select = Select::make('recordId') ->label(__('filament-actions::associate.single.modal.fields.record_id.label')) ->required() ->multiple($this->isMultiple()) ->searchable($this->getRecordSelectSearchColumns() ?? true) ->getSearchResultsUsing(static fn (Select $component, string $search): array => $getOptions(optionsLimit: $component->getOptionsLimit(), search: $search, searchColumns: $component->getSearchColumns())) ->getOptionLabelUsing(function ($value) use ($table): string { $relationship = Relation::noConstraints(fn () => $table->getRelationship()); return $this->getRecordTitle($relationship->getQuery()->find($value)); }) ->getOptionLabelsUsing(function (array $values) use ($table): array { $relationship = Relation::noConstraints(fn () => $table->getRelationship()); return $relationship->getQuery()->find($values) ->mapWithKeys(fn (Model $record): array => [$record->getKey() => $this->getRecordTitle($record)]) ->all(); }) ->options(fn (Select $component): array => $this->isRecordSelectPreloaded() ? $getOptions(optionsLimit: $component->getOptionsLimit()) : []) ->hiddenLabel(); if ($this->modifyRecordSelectUsing) { $select = $this->evaluate($this->modifyRecordSelectUsing, [ 'select' => $select, ]); } return $select; } public function forceSearchCaseInsensitive(bool | Closure | null $condition = true): static { $this->isSearchForcedCaseInsensitive = $condition; return $this; } public function isSearchForcedCaseInsensitive(): ?bool { return $this->evaluate($this->isSearchForcedCaseInsensitive); } }