diff --git a/src/Concerns/CanMapDynamicFields.php b/src/Concerns/CanMapDynamicFields.php index d40e915..d44c16b 100644 --- a/src/Concerns/CanMapDynamicFields.php +++ b/src/Concerns/CanMapDynamicFields.php @@ -23,6 +23,16 @@ use Illuminate\Support\Collection; use Livewire\Attributes\On; +/** + * Trait for handling dynamic field mapping and data mutation in forms. + * + * This trait provides functionality to: + * - Map database field configurations to form input components + * - Mutate form data before filling (loading from database) + * - Mutate form data before saving (processing user input) + * - Handle nested fields and builder blocks + * - Resolve custom field types and configurations + */ trait CanMapDynamicFields { private FieldInspector $fieldInspector; @@ -54,55 +64,228 @@ public function refresh(): void // } + /** + * Mutate form data before filling the form with existing values. + * + * This method processes the record's field values and applies any custom + * transformation logic defined in field classes before populating the form. + * + * @param array $data The form data array + * @return array The mutated form data + */ protected function mutateBeforeFill(array $data): array { - if (! isset($this->record) || $this->record->fields->isEmpty()) { + if (! $this->hasValidRecordWithFields()) { return $data; } - $fields = $this->record->fields; + // Extract builder blocks from record values + $builderBlocks = $this->extractBuilderBlocksFromRecord(); + $allFields = $this->getAllFieldsIncludingBuilderFields($builderBlocks); - return $this->mutateFormData($data, $fields, function ($field, $fieldConfig, $fieldInstance, $data) { - if (! empty($fieldConfig['methods']['mutateFormDataCallback'])) { - return $fieldInstance->mutateFormDataCallback($this->record, $field, $data); - } - - $data[$this->record->valueColumn][$field->ulid] = $this->record->values[$field->ulid] ?? null; - - return $data; + return $this->mutateFormData($data, $allFields, function ($field, $fieldConfig, $fieldInstance, $data) use ($builderBlocks) { + return $this->applyFieldFillMutation($field, $fieldConfig, $fieldInstance, $data, $builderBlocks); }); } + /** + * Mutate form data before saving to the database. + * + * This method processes user input and applies any custom transformation logic + * defined in field classes. It also handles special cases for builder blocks + * and nested fields. + * + * @param array $data The form data array + * @return array The mutated form data ready for saving + */ protected function mutateBeforeSave(array $data): array { - if (! isset($this->record)) { + if (! $this->hasValidRecord()) { return $data; } - $values = isset($data[$this->record?->valueColumn]) ? $data[$this->record?->valueColumn] : []; - + $values = $this->extractFormValues($data); if (empty($values)) { return $data; } - $fieldsFromValues = array_keys($values); + $builderBlocks = $this->extractBuilderBlocks($values); + $allFields = $this->getAllFieldsIncludingBuilderFields($builderBlocks); + + return $this->mutateFormData($data, $allFields, function ($field, $fieldConfig, $fieldInstance, $data) use ($builderBlocks) { + return $this->applyFieldSaveMutation($field, $fieldConfig, $fieldInstance, $data, $builderBlocks); + }); + } + + private function hasValidRecordWithFields(): bool + { + return isset($this->record) && ! $this->record->fields->isEmpty(); + } + + private function hasValidRecord(): bool + { + return isset($this->record); + } + + private function extractFormValues(array $data): array + { + return isset($data[$this->record?->valueColumn]) ? $data[$this->record?->valueColumn] : []; + } - $blocks = ModelsField::whereIn('ulid', $fieldsFromValues)->where('field_type', 'builder')->pluck('ulid')->toArray(); - $blocks = collect($values)->filter(fn ($value, $key) => in_array($key, $blocks))->toArray(); + /** + * Extract builder blocks from form values. + * + * Builder blocks are special field types that contain nested fields. + * This method identifies and extracts them for special processing. + * + * @param array $values The form values + * @return array The builder blocks + */ + private function extractBuilderBlocks(array $values): array + { + $builderFieldUlids = ModelsField::whereIn('ulid', array_keys($values)) + ->where('field_type', 'builder') + ->pluck('ulid') + ->toArray(); + + return collect($values) + ->filter(fn ($value, $key) => in_array($key, $builderFieldUlids)) + ->toArray(); + } - $fields = $this->record->fields->merge( - $this->getFieldsFromBlocks($blocks) + /** + * Get all fields including those from builder blocks. + * + * @param array $builderBlocks The builder blocks + * @return Collection All fields to process + */ + private function getAllFieldsIncludingBuilderFields(array $builderBlocks): Collection + { + return $this->record->fields->merge( + $this->getFieldsFromBlocks($builderBlocks) ); + } - return $this->mutateFormData($data, $fields, function ($field, $fieldConfig, $fieldInstance, $data) { - if (! empty($fieldConfig['methods']['mutateBeforeSaveCallback'])) { - return $fieldInstance->mutateBeforeSaveCallback($this->record, $field, $data); + /** + * Apply field-specific mutation logic for form filling. + * + * @param Model $field The field model + * @param array $fieldConfig The field configuration + * @param object $fieldInstance The field instance + * @param array $data The form data + * @param array $builderBlocks The builder blocks + * @return array The mutated data + */ + private function applyFieldFillMutation(Model $field, array $fieldConfig, object $fieldInstance, array $data, array $builderBlocks): array + { + if (! empty($fieldConfig['methods']['mutateFormDataCallback'])) { + $fieldLocation = $this->determineFieldLocation($field, $builderBlocks); + + if ($fieldLocation['isInBuilder']) { + return $this->processBuilderFieldFillMutation($field, $fieldInstance, $data, $fieldLocation['builderData'], $builderBlocks); } - return $data; - }); + return $fieldInstance->mutateFormDataCallback($this->record, $field, $data); + } + + // Default behavior: copy value from record to form data + $data[$this->record->valueColumn][$field->ulid] = $this->record->values[$field->ulid] ?? null; + + return $data; + } + + /** + * Extract builder blocks from record values. + * + * @return array The builder blocks + */ + private function extractBuilderBlocksFromRecord(): array + { + if (! isset($this->record->values) || ! is_array($this->record->values)) { + return []; + } + + $builderFieldUlids = ModelsField::whereIn('ulid', array_keys($this->record->values)) + ->where('field_type', 'builder') + ->pluck('ulid') + ->toArray(); + + return collect($this->record->values) + ->filter(fn ($value, $key) => in_array($key, $builderFieldUlids)) + ->toArray(); + } + + /** + * Process fill mutation for fields inside builder blocks. + * + * @param Model $field The field model + * @param object $fieldInstance The field instance + * @param array $data The form data + * @param array $builderData The builder block data + * @param array $builderBlocks All builder blocks + * @return array The updated form data + */ + private function processBuilderFieldFillMutation(Model $field, object $fieldInstance, array $data, array $builderData, array $builderBlocks): array + { + // Create a mock record with the builder data for the callback + $mockRecord = $this->createMockRecordForBuilder($builderData); + + // Create a temporary data structure for the callback + $tempData = [$this->record->valueColumn => $builderData]; + $tempData = $fieldInstance->mutateFormDataCallback($mockRecord, $field, $tempData); + + // Update the original data structure with the mutated values + $this->updateBuilderBlocksWithMutatedData($builderBlocks, $field, $tempData); + + // Update the main data structure + $data[$this->record->valueColumn] = array_merge($data[$this->record->valueColumn], $builderBlocks); + + return $data; + } + + /** + * Create a mock record for builder field processing. + * + * @param array $builderData The builder block data + * @return object The mock record + */ + private function createMockRecordForBuilder(array $builderData): object + { + $mockRecord = clone $this->record; + $mockRecord->values = $builderData; + + return $mockRecord; + } + + /** + * Update builder blocks with mutated field data. + * + * @param array $builderBlocks The builder blocks to update + * @param Model $field The field being processed + * @param array $tempData The temporary data containing mutated values + */ + private function updateBuilderBlocksWithMutatedData(array &$builderBlocks, Model $field, array $tempData): void + { + foreach ($builderBlocks as $builderUlid => &$builderBlocks) { + if (is_array($builderBlocks)) { + foreach ($builderBlocks as &$block) { + if (isset($block['data']) && is_array($block['data']) && isset($block['data'][$field->ulid])) { + $block['data'][$field->ulid] = $tempData[$this->record->valueColumn][$field->ulid] ?? $block['data'][$field->ulid]; + } + } + } + } } + /** + * Resolve field configuration and create an instance. + * + * This method determines whether to use a custom field implementation + * or fall back to the default field type mapping. + * + * @param Model $field The field model + * @return array Array containing 'config' and 'instance' keys + */ private function resolveFieldConfigAndInstance(Model $field): array { // Try to resolve from custom fields first @@ -116,6 +299,15 @@ private function resolveFieldConfigAndInstance(Model $field): array ]; } + /** + * Extract field models from builder blocks. + * + * Builder blocks contain nested fields that need to be processed. + * This method extracts those field models for processing. + * + * @param array $blocks The builder blocks + * @return Collection The field models from blocks + */ protected function getFieldsFromBlocks(array $blocks): Collection { $processedFields = collect(); @@ -132,6 +324,17 @@ protected function getFieldsFromBlocks(array $blocks): Collection return $processedFields; } + /** + * Apply mutation strategy to all fields recursively. + * + * This method processes each field and its nested children using the provided + * mutation strategy. It handles the hierarchical nature of fields. + * + * @param array $data The form data + * @param Collection $fields The fields to process + * @param callable $mutationStrategy The strategy to apply to each field + * @return array The mutated form data + */ protected function mutateFormData(array $data, Collection $fields, callable $mutationStrategy): array { foreach ($fields as $field) { @@ -140,17 +343,44 @@ protected function mutateFormData(array $data, Collection $fields, callable $mut ['config' => $fieldConfig, 'instance' => $fieldInstance] = $this->resolveFieldConfigAndInstance($field); $data = $mutationStrategy($field, $fieldConfig, $fieldInstance, $data); - if (! empty($field->children)) { - foreach ($field->children as $nestedField) { - ['config' => $nestedFieldConfig, 'instance' => $nestedFieldInstance] = $this->resolveFieldConfigAndInstance($nestedField); - $data = $mutationStrategy($nestedField, $nestedFieldConfig, $nestedFieldInstance, $data); - } - } + $data = $this->processNestedFields($field, $data, $mutationStrategy); + } + + return $data; + } + + /** + * Process nested fields (children) of a parent field. + * + * @param Model $field The parent field + * @param array $data The form data + * @param callable $mutationStrategy The mutation strategy + * @return array The updated form data + */ + private function processNestedFields(Model $field, array $data, callable $mutationStrategy): array + { + if (empty($field->children)) { + return $data; + } + + foreach ($field->children as $nestedField) { + ['config' => $nestedFieldConfig, 'instance' => $nestedFieldInstance] = $this->resolveFieldConfigAndInstance($nestedField); + $data = $mutationStrategy($nestedField, $nestedFieldConfig, $nestedFieldInstance, $data); } return $data; } + /** + * Resolve form field inputs for rendering. + * + * This method converts field models into form input components + * that can be rendered in the UI. + * + * @param mixed $record The record containing fields + * @param bool $isNested Whether this is a nested field + * @return array Array of form input components + */ private function resolveFormFields(mixed $record = null, bool $isNested = false): array { $record = $record ?? $this->record; @@ -174,22 +404,127 @@ private function resolveCustomFields(): Collection ->map(fn ($fieldClass) => new $fieldClass); } + /** + * Resolve a single field input component. + * + * This method creates the appropriate form input component for a field, + * prioritizing custom field implementations over default ones. + * + * @param Model $field The field model + * @param Collection $customFields Available custom fields + * @param mixed $record The record + * @param bool $isNested Whether this is a nested field + * @return object|null The form input component or null if not found + */ private function resolveFieldInput(Model $field, Collection $customFields, mixed $record = null, bool $isNested = false): ?object { $record = $record ?? $this->record; - $inputName = $isNested ? "{$field->ulid}" : "{$record->valueColumn}.{$field->ulid}"; + $inputName = $this->generateInputName($field, $record, $isNested); // Try to resolve from custom fields first (giving them priority) if ($customField = $customFields->get($field->field_type)) { return $customField::make($inputName, $field); } - // // Fall back to standard field type map if no custom field found + // Fall back to standard field type map if no custom field found if ($fieldClass = self::FIELD_TYPE_MAP[$field->field_type] ?? null) { return $fieldClass::make(name: $inputName, field: $field); } return null; } + + private function generateInputName(Model $field, mixed $record, bool $isNested): string + { + return $isNested ? "{$field->ulid}" : "{$record->valueColumn}.{$field->ulid}"; + } + + /** + * Apply field-specific mutation logic for form saving. + * + * This method handles both regular fields and fields within builder blocks. + * Builder blocks require special processing because they contain nested data structures. + * + * @param Model $field The field model + * @param array $fieldConfig The field configuration + * @param object $fieldInstance The field instance + * @param array $data The form data + * @param array $builderBlocks The builder blocks + * @return array The mutated data + */ + private function applyFieldSaveMutation(Model $field, array $fieldConfig, object $fieldInstance, array $data, array $builderBlocks): array + { + if (empty($fieldConfig['methods']['mutateBeforeSaveCallback'])) { + return $data; + } + + $fieldLocation = $this->determineFieldLocation($field, $builderBlocks); + + if ($fieldLocation['isInBuilder']) { + return $this->processBuilderFieldMutation($field, $fieldInstance, $data, $fieldLocation['builderData'], $builderBlocks); + } + + // Regular field processing + return $fieldInstance->mutateBeforeSaveCallback($this->record, $field, $data); + } + + /** + * Determine if a field is inside a builder block and extract its data. + * + * @param Model $field The field to check + * @param array $builderBlocks The builder blocks + * @return array Location information with 'isInBuilder' and 'builderData' keys + */ + private function determineFieldLocation(Model $field, array $builderBlocks): array + { + foreach ($builderBlocks as $builderUlid => $builderBlocks) { + if (is_array($builderBlocks)) { + foreach ($builderBlocks as $block) { + if (isset($block['data']) && is_array($block['data']) && isset($block['data'][$field->ulid])) { + return [ + 'isInBuilder' => true, + 'builderData' => $block['data'], + ]; + } + } + } + } + + return [ + 'isInBuilder' => false, + 'builderData' => null, + ]; + } + + /** + * Process mutation for fields inside builder blocks. + * + * Builder fields require special handling because they're nested within + * a complex data structure that needs to be updated in place. + * + * @param Model $field The field model + * @param object $fieldInstance The field instance + * @param array $data The form data + * @param array $builderData The builder block data + * @param array $builderBlocks All builder blocks + * @return array The updated form data + */ + private function processBuilderFieldMutation(Model $field, object $fieldInstance, array $data, array $builderData, array $builderBlocks): array + { + // Create a mock record with the builder data for the callback + $mockRecord = $this->createMockRecordForBuilder($builderData); + + // Create a temporary data structure for the callback + $tempData = [$this->record->valueColumn => $builderData]; + $tempData = $fieldInstance->mutateBeforeSaveCallback($mockRecord, $field, $tempData); + + // Update the original data structure with the mutated values + $this->updateBuilderBlocksWithMutatedData($builderBlocks, $field, $tempData); + + // Update the main data structure + $data[$this->record->valueColumn] = array_merge($data[$this->record->valueColumn], $builderBlocks); + + return $data; + } } diff --git a/src/Concerns/HasDatalist.php b/src/Concerns/HasDatalist.php index 2d255cf..03045eb 100644 --- a/src/Concerns/HasDatalist.php +++ b/src/Concerns/HasDatalist.php @@ -21,7 +21,7 @@ public static function addDatalistToInput(mixed $input, mixed $field): mixed public static function getDatalistConfig(): array { return array_merge(static::getSelectableValuesConfig(), [ - 'datalistType' => null, + 'datalistType' => [], ]); } diff --git a/src/Concerns/HasOptions.php b/src/Concerns/HasOptions.php index 173bfe7..a8d0801 100644 --- a/src/Concerns/HasOptions.php +++ b/src/Concerns/HasOptions.php @@ -21,7 +21,7 @@ public static function addOptionsToInput(mixed $input, mixed $field): mixed public static function getOptionsConfig(): array { return array_merge(static::getSelectableValuesConfig(), [ - 'optionType' => null, + 'optionType' => [], ]); } diff --git a/src/Concerns/HasSelectableValues.php b/src/Concerns/HasSelectableValues.php index 34d1929..38a450e 100644 --- a/src/Concerns/HasSelectableValues.php +++ b/src/Concerns/HasSelectableValues.php @@ -34,88 +34,150 @@ protected static function resolveResourceModel(string $tableName): ?object protected static function addValuesToInput(mixed $input, mixed $field, string $type, string $method): mixed { + // Ensure field config is properly initialized + if (! static::ensureFieldConfig($field, $type)) { + return $input; + } + $allOptions = []; // Handle relationship options - if (isset($field->config[$type]) && - (is_string($field->config[$type]) && $field->config[$type] === 'relationship') || - (is_array($field->config[$type]) && in_array('relationship', $field->config[$type]))) { + if (static::shouldHandleRelationshipOptions($field, $type)) { + $relationshipOptions = static::buildRelationshipOptions($field); + $allOptions = static::mergeRelationshipOptions($allOptions, $relationshipOptions, $field, $type); + } - $relationshipOptions = []; + // Handle array options + if (static::shouldHandleArrayOptions($field, $type)) { + $allOptions = static::mergeArrayOptions($allOptions, $field, $type); + } - foreach ($field->config['relations'] ?? [] as $relation) { - if (! isset($relation['resource'])) { - continue; - } + // Apply all merged options to the input + if (! empty($allOptions)) { + $input->$method($allOptions); + } - $model = static::resolveResourceModel($relation['resource']); + return $input; + } - if (! $model) { - continue; - } + protected static function ensureFieldConfig(mixed $field, string $type): bool + { + // Ensure field config exists and is an array + if (! isset($field->config) || ! is_array($field->config)) { + return false; + } - $query = $model::query(); + // Ensure the type key exists in the config to prevent undefined array key errors + if (! array_key_exists($type, $field->config)) { + $config = $field->config ?? []; + $config[$type] = null; + $field->config = $config; + } - // Apply filters if they exist - if (isset($relation['relationValue_filters'])) { - foreach ($relation['relationValue_filters'] as $filter) { - if (isset($filter['column'], $filter['operator'], $filter['value'])) { - $query->where($filter['column'], $filter['operator'], $filter['value']); - } - } - } + return true; + } - $results = $query->get(); + protected static function shouldHandleRelationshipOptions(mixed $field, string $type): bool + { + // Ensure $type is a string to prevent array key errors + if (! is_string($type)) { + return false; + } - if ($results->isEmpty()) { - continue; - } + return isset($field->config[$type]) && $field->config[$type] !== null && + (is_string($field->config[$type]) && $field->config[$type] === 'relationship') || + (is_array($field->config[$type]) && in_array('relationship', $field->config[$type])); + } - $opts = $results->pluck($relation['relationValue'] ?? 'name', $relation['relationKey'])->toArray(); + protected static function shouldHandleArrayOptions(mixed $field, string $type): bool + { + // Ensure $type is a string to prevent array key errors + if (! is_string($type)) { + return false; + } - if (count($opts) === 0) { - continue; - } + return isset($field->config[$type]) && $field->config[$type] !== null && + (is_string($field->config[$type]) && $field->config[$type] === 'array') || + (is_array($field->config[$type]) && in_array('array', $field->config[$type])); + } - // Group by resource name - $resourceName = Str::title($relation['resource']); - $relationshipOptions[$resourceName] = $opts; + protected static function buildRelationshipOptions(mixed $field): array + { + $relationshipOptions = []; + + foreach ($field->config['relations'] ?? [] as $relation) { + if (! isset($relation['resource'])) { + continue; + } + + $model = static::resolveResourceModel($relation['resource']); + + if (! $model) { + continue; } - if (! empty($relationshipOptions)) { - // If both types are selected, group relationship options by resource - if (isset($field->config[$type]) && - (is_array($field->config[$type]) && in_array('array', $field->config[$type]))) { - $allOptions = array_merge($allOptions, $relationshipOptions); - } else { - // For single relationship type, merge all options without grouping - $allOptions = array_merge($allOptions, ...array_values($relationshipOptions)); + $query = $model::query(); + + // Apply filters if they exist + if (isset($relation['relationValue_filters'])) { + foreach ($relation['relationValue_filters'] as $filter) { + if (isset($filter['column'], $filter['operator'], $filter['value'])) { + $query->where($filter['column'], $filter['operator'], $filter['value']); + } } } + + $results = $query->get(); + + if ($results->isEmpty()) { + continue; + } + + $opts = $results->pluck($relation['relationValue'] ?? 'name', $relation['relationKey'])->toArray(); + + if (count($opts) === 0) { + continue; + } + + // Group by resource name + $resourceName = Str::title($relation['resource']); + $relationshipOptions[$resourceName] = $opts; } - // Handle array options - if (isset($field->config[$type]) && - (is_string($field->config[$type]) && $field->config[$type] === 'array') || + return $relationshipOptions; + } + + protected static function mergeRelationshipOptions(array $allOptions, array $relationshipOptions, mixed $field, string $type): array + { + if (empty($relationshipOptions)) { + return $allOptions; + } + + // If both types are selected, group relationship options by resource + if (isset($field->config[$type]) && $field->config[$type] !== null && (is_array($field->config[$type]) && in_array('array', $field->config[$type]))) { + return array_merge($allOptions, $relationshipOptions); + } else { + // For single relationship type, merge all options without grouping + return array_merge($allOptions, ...array_values($relationshipOptions)); + } + } - if (isset($field->config['options']) && is_array($field->config['options'])) { - // If both types are selected, group array options - if (isset($field->config[$type]) && - (is_array($field->config[$type]) && in_array('relationship', $field->config[$type]))) { - $allOptions[__('Custom Options')] = $field->config['options']; - } else { - $allOptions = array_merge($allOptions, $field->config['options']); - } - } + protected static function mergeArrayOptions(array $allOptions, mixed $field, string $type): array + { + if (! isset($field->config['options']) || ! is_array($field->config['options'])) { + return $allOptions; } - // Apply all merged options to the input - if (! empty($allOptions)) { - $input->$method($allOptions); + // If both types are selected, group array options + if (isset($field->config[$type]) && $field->config[$type] !== null && + (is_array($field->config[$type]) && in_array('relationship', $field->config[$type]))) { + $allOptions[__('Custom Options')] = $field->config['options']; + } else { + $allOptions = array_merge($allOptions, $field->config['options']); } - return $input; + return $allOptions; } protected static function getSelectableValuesConfig(): array @@ -145,9 +207,9 @@ protected function selectableValuesFormFields(string $type, string $label, strin ]) ->afterStateHydrated(function (Forms\Get $get, Forms\Set $set) use ($type) { $value = $get("config.{$type}"); - if (is_string($value)) { - $set("config.{$type}", [$value]); - } + + // Set correct config value when creating records + $set("config.{$type}", is_array($value) ? $value : (is_bool($value) ? [] : [$value])); }) ->label(__('Type')) ->live(), diff --git a/src/Fields/Select.php b/src/Fields/Select.php index 39bb65e..b163720 100644 --- a/src/Fields/Select.php +++ b/src/Fields/Select.php @@ -8,6 +8,7 @@ use Backstage\Fields\Models\Field; use Filament\Forms; use Filament\Forms\Components\Select as Input; +use Illuminate\Database\Eloquent\Model; class Select extends Base implements FieldContract { @@ -74,6 +75,61 @@ public static function make(string $name, ?Field $field = null): Input return $input; } + public static function mutateFormDataCallback(Model $record, $field, array $data): array + { + if (! property_exists($record, 'valueColumn') || ! isset($record->values[$field->ulid])) { + return $data; + } + + $value = $record->values[$field->ulid]; + $data[$record->valueColumn][$field->ulid] = self::normalizeSelectValue($value, $field); + + return $data; + } + + public static function mutateBeforeSaveCallback(Model $record, $field, array $data): array + { + if (! property_exists($record, 'valueColumn') || ! isset($data[$record->valueColumn][$field->ulid])) { + return $data; + } + + $value = $data[$record->valueColumn][$field->ulid]; + $data[$record->valueColumn][$field->ulid] = self::normalizeSelectValue($value, $field); + + return $data; + } + + /** + * Normalize the select value to an array or a single value. This is needed because the select field can be + * changed from single to multiple or vice versa. + */ + private static function normalizeSelectValue($value, Field $field): mixed + { + $isMultiple = $field->config['multiple'] ?? false; + + // Handle JSON string values + if (is_string($value) && json_validate($value)) { + $value = json_decode($value, true); + } + + // Handle null/empty values consistently + if ($value === null || $value === '') { + return $isMultiple ? [] : null; + } + + // Convert to array if multiple is expected but value is not an array + if ($isMultiple && ! is_array($value)) { + return [$value]; + } + + // Convert array to single value if multiple is not expected + if (! $isMultiple && is_array($value)) { + return empty($value) ? null : reset($value); + } + + return $value; + } + public function getForm(): array { return [ @@ -95,6 +151,8 @@ public function getForm(): array ->inline(false), Forms\Components\Toggle::make('config.multiple') ->label(__('Multiple')) + ->helperText(__('Only first value is used when switching from multiple to single.')) + ->columnSpan(2) ->inline(false), Forms\Components\Toggle::make('config.allowHtml') ->label(__('Allow HTML'))